Skip to content

多端业务系统全景图:一次需求到底要改哪里

一套业务系统同时有 PC 后台、员工 H5、用户 H5 和微信小程序时,最常见的问题不是“代码放在哪”,而是:改一个功能,哪些端会受影响?

本文来自一组长期维护的真实项目。示例代码保留了项目里的典型写法,但已经删除业务名称、接口地址、内部参数和用户数据。

读完你能解决什么

  • 接到需求后,快速判断要检查哪些仓库和模块;
  • 知道一条请求怎样从页面走到数据库;
  • 避免只改当前页面,漏掉其他客户端;
  • 给多人协作准备一份简单的影响范围清单。

先看系统长什么样

这组项目可以抽象成下面的结构:

text
PC 管理后台 ─────┐
员工移动 H5 ─────┤
用户移动 H5 ─────┼──▶ PHP / Yaf 服务端 ──▶ 数据库、缓存、外部服务
微信小程序 ──────┘

四个客户端的界面和运行环境不同,但最终都依赖同一批业务规则。例如“是否允许取消”“当前用户能否操作”“金额如何计算”,最终应由服务端确认。

客户端可以提前提示,不能成为最后裁判。

不要按仓库理解需求,要按业务动作理解

假设需求是“用户可以取消预约”。如果只看小程序页面,很容易漏掉这些问题:

层次要确认的问题
小程序或 H5按钮什么时候显示?重复点击怎么办?
API请求需要哪些参数?重复请求会怎样?
身份与权限谁能取消?能否取消别人的记录?
业务规则哪些状态允许取消?是否超过时间限制?
数据是否要释放占用、写状态历史?
外部动作是否要退款或发通知?失败后怎么办?
其他客户端PC 后台和另一个 H5 是否也要显示新状态?

这就是“垂直切片”:从用户点击开始,一直检查到数据和外部结果。

一条请求的真实旅程

text
用户点击提交

客户端校验并锁定按钮

请求层附带登录信息和 requestId

服务端入口解析参数与当前用户

业务服务检查权限和状态

事务内写入数据

提交后异步发送通知

客户端刷新最终状态

只要能把一次需求画成这条链,开发、测试和排错都会容易很多。

项目中的入口代码是什么样

真实服务端使用 Yaf,多种客户端对应不同入口,但入口的核心动作很简单:加载配置、选择应用目录、启动应用。

下面是根据项目代码缩短后的示例:

php
<?php

$application = new Yaf_Application($configFile);
$application
    ->bootstrap()
    ->run();

入口不适合写具体业务。否则同一规则会在多个入口里复制,修改时很难保证一致。

更合适的分工是:

text
入口:启动哪个应用
控制器:接收什么请求
业务服务:这次操作能不能做
模型或仓储:数据怎样读写
基础设施:缓存、消息、支付等怎样调用

客户端请求层是什么样

PC 项目使用独立的请求封装,再由 API 模块调用。简化后类似:

js
import axios from 'axios'

const request = axios.create({
  baseURL: process.env.BASE_API,
  timeout: 15000
})

request.interceptors.response.use(
  response => response.data,
  error => Promise.reject(normalizeError(error))
)

移动 H5 和小程序的底层 API 不一样,但页面最好得到同样的结果:

js
async function cancelAppointment(id) {
  try {
    const data = await api.cancelAppointment({ id })
    showSuccess(data)
  } catch (error) {
    showFailure(error)
  }
}

页面不应关心底层使用 Axios、XMLHttpRequest 还是 wx.request

身份信息要在服务端重新确认

不同客户端可能使用不同登录方式:

text
PC:账号会话
H5:网页授权或 Cookie
小程序:平台登录码换业务会话

进入业务服务前,它们应统一成类似的上下文:

php
final class RequestContext
{
    public $actorId;
    public $tenantId;
    public $roles;
    public $channel;
    public $requestId;

    public function __construct($actorId, $tenantId, array $roles, $channel, $requestId)
    {
        $this->actorId = $actorId;
        $this->tenantId = $tenantId;
        $this->roles = $roles;
        $this->channel = $channel;
        $this->requestId = $requestId;
    }
}

这里使用 PHP 7.3 也能解析的显式属性写法,便于旧 Yaf 项目直接理解;如果运行环境已经统一到 PHP 8.2+,可以再使用构造器属性提升和 readonly

客户端传来的用户 ID、组织 ID 或角色不能直接当作可信结果。服务端必须从有效会话中恢复,并再次校验权限。

用一张表记录功能覆盖范围

项目大了以后,最实用的架构文档往往不是大图,而是这张表:

业务能力PC员工 H5用户 H5小程序服务端模块
登录管理账号员工身份用户授权平台登录身份模块
预约查询和调整确认创建创建预约模块
订单管理创建查询支付订单模块
通知配置接收接收订阅消息通知模块

每次需求评审先更新这张表,再确定修改文件。它能直接暴露“某个端没有考虑到”。

一个需求的简单开发清单

以“取消预约”为例,可以写成:

text
[ ] 哪些客户端有取消入口
[ ] 哪些状态允许取消
[ ] 当前用户是否有权操作
[ ] 同一个请求到达两次会怎样
[ ] 数据库需要同时修改哪些事实
[ ] 通知或退款失败后怎样恢复
[ ] 其他客户端怎样看到最新状态
[ ] 新旧客户端同时在线是否兼容
[ ] 出问题时怎样关闭或回滚

这份清单比“前端改页面、后端加接口”更接近真实工作。

常见错误

只改看得见的页面

页面按钮消失了,不代表 API 已限制;另一个客户端仍可能提交。权限和状态规则必须在服务端执行。

把公共参数散落在每个接口中

登录信息、渠道、版本和追踪标识应由统一请求层添加。手工拼接迟早会漏。

数据提交后直接调用外部服务

数据库已经成功,通知却失败时,如果没有任务记录就无法恢复。重要外部动作应留下可重试记录。

假设所有客户端同时上线

H5 可能被缓存,小程序需要审核,PC 用户可能长时间不刷新。接口变更必须允许新旧版本短期共存。

排错时从哪里开始

收到“页面点了没反应”时,按顺序检查:

  1. 页面有没有真的触发请求;
  2. 请求是否带上 requestId
  3. 服务端是否识别出正确用户和业务范围;
  4. 业务规则在哪一步拒绝;
  5. 数据库是否已经提交;
  6. 外部动作是否还在处理中;
  7. 客户端是否用旧缓存覆盖了新结果。

不要一开始就在所有仓库里搜索错误文案。先确定请求停在哪个边界。

最小文档集

长期维护多端系统,至少保留四份资料:

  • 客户端—业务能力矩阵;
  • 登录方式如何转换成统一身份;
  • 关键业务状态和允许的转换;
  • 发布顺序、验证步骤和回滚方法。

能用测试验证的内容,不要只靠文档提醒。例如错误结构用契约测试,重复提交用并发测试,导航链接用站点构建验证。

最后记住三句话

  1. 仓库不是业务边界,业务动作才是。
  2. 客户端负责体验,服务端负责最终规则。
  3. 每个跨端需求都要从点击一直检查到数据、外部结果和回滚。

下一篇可以继续阅读跨端 API 契约,把不同客户端的请求和错误统一起来。

为复用而记录,为理解而整理。