多端业务系统全景图:一次需求到底要改哪里
一套业务系统同时有 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 用户可能长时间不刷新。接口变更必须允许新旧版本短期共存。
排错时从哪里开始
收到“页面点了没反应”时,按顺序检查:
- 页面有没有真的触发请求;
- 请求是否带上
requestId; - 服务端是否识别出正确用户和业务范围;
- 业务规则在哪一步拒绝;
- 数据库是否已经提交;
- 外部动作是否还在处理中;
- 客户端是否用旧缓存覆盖了新结果。
不要一开始就在所有仓库里搜索错误文案。先确定请求停在哪个边界。
最小文档集
长期维护多端系统,至少保留四份资料:
- 客户端—业务能力矩阵;
- 登录方式如何转换成统一身份;
- 关键业务状态和允许的转换;
- 发布顺序、验证步骤和回滚方法。
能用测试验证的内容,不要只靠文档提醒。例如错误结构用契约测试,重复提交用并发测试,导航链接用站点构建验证。
最后记住三句话
- 仓库不是业务边界,业务动作才是。
- 客户端负责体验,服务端负责最终规则。
- 每个跨端需求都要从点击一直检查到数据、外部结果和回滚。
下一篇可以继续阅读跨端 API 契约,把不同客户端的请求和错误统一起来。