三端(后台、小程序、设备)并行开发时,联调顺序直接决定返工量。这里讲的是 ToyNet 项目的实践:为什么不能先接设备再接小程序,以及各阶段的桩数据策略。
为什么后台必须先行
后台 API 是唯一的事实源。小程序和设备都依赖后台数据,这种依赖关系不可逆转。
ToyNet 的架构很清楚:
- 后台提供 REST API(用户、商品、订单、设备管理)和 MQTT 消息队列(设备指令下发)
- 小程序是 API 消费者(调用 REST 接口)
- 设备是 MQTT 消费者(订阅
toynet/device/{deviceSn}/command主题、执行命令、上报状态)
这意味着小程序开发的第一天就需要后台 API 存在。不是非得完全实现,但至少得有契约——定义好接口名、参数、返回值格式。设备也是一样,需要后台先把 MQTT 消息格式定下来。
反序的代价。如果先做设备再做小程序,会发生什么?小程序开发期间没有可读的数据。具体例子:小程序要显示"设备列表"和"设备状态",调的是 /api/v1/devices/list。如果后台这个接口还不存在,小程序工程师得猜测设备状态的格式——也许是 { status: 'online' },也许是 { online: true },也许还要加 battery 和 lastSeenAt。用错了格式渲染页面,即使设备硬件工作正常,小程序也没法验证这个流程。几周后后台实现了,返回格式和小程序猜的不一样,小程序要改、测试、再验证设备——这一圈返工没有价值,本可以避免。
类似的问题还会出现在 MQTT 指令格式上。先做设备时,硬件工程师定义了命令格式,比如 { action: 'play', audioId: 123 }。后台做到这一步时,根据产品需求改成了 { cmd: 'play_audio', params: { id: 123 } }。小程序要跟着改,设备固件也要改。如果顺序对了,后台先定好格式,这些改动就不会出现。
可行的分阶段顺序
第1阶段:后台自测
目标:后台 6 个核心模块全部能跑,包括鉴权、设备管理、商品、订单、AI、MQTT。
在 ToyNet 里的对应:Plan 1–6(2026-04-09 到 2026-04-10)。
桩数据策略:
- 用 Jest 单测 + 集成测试验证每个 service
- MongoDB 用 mongodb-memory-server 在内存中启动,每次测试自动清理
- 不需要真实设备,MQTT broker 可以离线或用 mock 实现
- test fixtures 里写好标准数据:用户对象、设备对象、订单对象,每个测试复用
具体做法:
server/tests/unit/modules/devices/devices.service.test.js
server/tests/integration/devices.test.js
npm test -- --coverage
验收标准是覆盖率 ≥80%,所有 API 路由至少走过一遍。这时候后台是独立的黑盒,没有外部依赖。
第2阶段:后台+小程序
目标:小程序能从后台读数据、写数据,覆盖游客浏览和登录用户操作。
在 ToyNet 里的对应:Phase 1–3(2026-04-12 到 2026-04-13 期间)。
依赖关系:
- Phase 1(骨架)需要后台 Plan 1(基础:auth、middleware、request client)
- Phase 2(首页+商品)需要后台 Plan 1 + Plan 4(products API)
- Phase 3(下单+支付)需要后台 Plan 1 + Plan 4(orders/payments API)
桩数据策略:
- 后台完全真实:真实 MongoDB,真实 API 服务
- 小程序在微信开发者工具中:用假 WeChat openid(例如 test-user-001)写死在开发配置里,调用真实后台 API
- 支付模块:后台在测试环境下返回 mock 支付结果(检测
NODE_ENV=development时跳过真实微信支付 SDK,直接返回 success) - 认证用工具函数
requireLogin(),小程序 service 的request()自动读 localStorage 里的 fake token,后台鉴权中间件识别这个 test token 就返回授权 - 不真实的数据(设备状态、MQTT 消息)用固定 fixture 返回
具体流程:
- 启动后台 dev 服务器
npm run dev,默认连接本地 MongoDB - 打开微信开发者工具,小程序项目配置指向
http://localhost:3000 - 小程序登录页点"授权",调后台
/auth/login,后台返回 token,小程序存入 localStorage - 跳转首页,调
/products?limit=10,后台返回商品列表,小程序渲染轮播图 - 找到接口缺失或返回格式不对,后台补齐、更新 Swagger 文档,小程序跟着改
这个阶段没有真实设备,完全是前后端的对接。设备相关的代码(比如 Phase 2 里的 services/devices.ts 里的 getDeviceList())是空实现或返回 mock 数据,例如:
export async function getDeviceList() {
if (process.env.NODE_ENV === 'test') {
return { list: [{ id: 'mock-dev-1', name: '玩具1', status: 'online' }], total: 1 }
}
return request({ url: '/devices', skipAuth: false })
}
这样既能让页面渲染通过,又不依赖真实设备存在。
第3阶段:后台+设备
目标:设备能接收后台指令、上报状态。小程序和设备可以不通信,但后台要能驱动设备。
在 ToyNet 里的对应:Phase 4(设备详情、扫码绑定)。
依赖关系:
- Phase 4 需要后台 Plan 3(devices/MQTT 模块)
- Phase 4 不需要 Phase 2/3 完全完成,但需要后台 device 模型已定义
桩数据策略:
- 后台:真实 MQTT broker(EMQX 或阿里云/腾讯云 MQTT 服务),真实设备在线或可控上线
- 小程序:可以继续用假数据调试 UI,设备相关的 API 调用和页面暂时 mock 返回
- 关键路径是后台 → MQTT broker → 设备的指令下发和状态回复,这一段必须真实
具体流程:
- 启动后台 MQTT 服务(连接真实 broker)
- 连接真实设备到 broker(设备订阅
toynet/device/{deviceSn}/command主题) - 后台代码调用
publishDeviceCommand(deviceSn, { cmd: 'set_volume', params: { volume: 50 } }) - 验证:
- MQTT broker 收到消息
- 设备接收到消息
- 设备执行(音量调到 50)
- 设备上报反馈到后台
toynet/device/{deviceSn}/status - 后台记录状态变化
这个阶段会发现的问题通常是:MQTT topic 路径理解偏差(后台发到 device/123/cmd,设备监听的是 device/123/command)、命令参数类型错误(发的是 string 但设备期望 number)、设备离线处理逻辑不对、消息丢失重试机制缺失。这些问题不会影响后台+小程序的对接(那是上一阶段已验证的),只能来自设备硬件的理解差异。
离线设备的处理。Plan 3 设计了一个细节:设备离线时,POST /devices/:id/command 仍然接受请求,返回 422 错误码 42240,告诉客户端"设备离线,指令已入队但不保证送达"。这样做是为了解耦小程序和设备的强依赖关系——小程序不用关心设备是否在线,后台负责队列和重试。真实测试时:
- 准备一台设备,上线后发一条指令,确认执行成功
- 拔掉设备电源,让它离线
- 再发一条指令,应该收到 42240 错误,指令被入队
- 给设备重新通电,它上线
- 确认设备优先处理队列里的待发指令
- 验证指令执行顺序和完整性
这个测试不复杂,但很关键。它验证了后台的异步处理能力,这对小程序的用户体验很重要——用户不会在"点了按钮设备没反应"时尴尬,后台会自动重试。
第4阶段:三端
目标:小程序操作 → 后台处理 → 设备执行 → 实时反馈给小程序,完整闭环。
在 ToyNet 里的对应:Phase 5(录音上传、MQTT 推送到设备)。
依赖关系:
- Phase 5 需要 Phase 4 的设备模块已稳定
- 需要后台 Plan 5(AI 对话)+ Plan 3(MQTT 服务)已完成
- 需要设备硬件支持
play_custom_audio命令(可能需要固件升级,这是前期沟通清楚的)
桩数据策略:
- 全真实:真实小程序用户、真实后台、真实 MQTT broker、真实设备
- 不用 mock,不用假数据
- 唯一的前置条件是:设备硬件能力清单已确认(支持哪些命令、指令超时时间、失败重试策略)
具体流程:
- 小程序用户点"录制"按钮,录音 5 秒钟
- 上传音频文件到后台
POST /api/v1/recordings/upload - 后台接收、持久化音频(存 COS 或本地),返回音频 URL
- 后台业务逻辑获取该用户绑定的所有设备,对每一台设备发 MQTT 指令:
device/{sn}/command消息内容{ cmd: 'play_custom_audio', params: { url: 'https://...', duration: 5 } } - 设备收到消息,下载音频,开始播放
- 设备每秒上报播放状态(进度、音量、是否完成)到后台 MQTT 主题
device/{sn}/status - 后台通过 WebSocket 或 Server-Sent Event 推送状态更新给小程序
- 小程序实时显示设备播放进度条、播放完成提示
这个阶段是三端各司其职的验证。一旦前三个阶段都通过了,这里通常没有架构问题——问题可能来自:
- 硬件支持度(设备固件不支持某个命令)
- 网络延迟(MQTT 消息延迟超过 1 秒)
- 音频格式不兼容(设备不支持某种编码)
- 存储容量(音频文件过大)
但这些都不是联调顺序的问题,都是已知的工程约束。
为什么这个顺序正确
- 依赖关系清晰:每一步都在前一步的基础上加新功能,不会无限反复。后台是基座,小程序依赖后台 API,设备依赖后台 MQTT,没有循环依赖。
- 快速反馈:每个阶段都能独立验证,不用等所有功能都做完。第 1 阶段后端工程师就能 100% 确认"后台自己能跑"。第 2 阶段小程序工程师就能 100% 确认"小程序能调后台 API"。没有"可能、应该、估计"。
- 问题隔离:如果第 N 阶段失败,问题一定在这一步加的新东西里,前面的层都已经验证过。例如第 3 阶段如果设备收不到指令,不用怀疑后台 API(已在第 2 阶段通过了),直接看 MQTT 通信。这样定位问题的时间从"无限"缩短到"可控"。
- 资源利用:设备成本高(采购、维护、通电、运输、故障修理)。如果先做设备再做小程序,小程序开发期间所有设备都得 24/7 开着,浪费电,加速磨损。如果后台+小程序联调期间设备全关着,开发速度反而快。
- 并行开发:三个角色(后端、小程序、硬件)可以真正并行。后端做 Plan 1-2 时,小程序工程师看 Swagger 文档设计页面框架;硬件工程师看 MQTT topic 设计固件消息处理。等后端出了初版 API,小程序才开始集成;硬件到了 Plan 3 才开始真实测试。如果反序,硬件工程师得等小程序做完才能知道"产品需要什么",这是典型的串行瓶颈。
反序的代价。如果先做设备再做小程序,流程变成:设备完成 → 小程序根据设备现状开发 → 发现产品需求与设备能力不匹配 → 改硬件或改产品需求 → 小程序重做。这一圈返工,设备的每次改动都要测试,成本指数级上升。而且到了后期,后台 API 还没对接上来,小程序用 mock 数据做了一堆假验证,等真接口来了又全得改。
每个阶段的验收输出物
- 第1阶段:
- 后台源码(完整目录结构)
- Jest 覆盖率报告(≥80%)
- Swagger API 文档(自动生成,所有路由都有示例)
.env.example和数据库初始化脚本- 通过标准:
npm test全部 PASS,npm start能启动
- 第2阶段:
- 小程序源码(所有 Phase 1-3 功能代码)
- Jest 测试报告(services 层覆盖率 ≥80%)
- 真机测试截图和视频(登录、首页、搜索、下单、支付成功页)
- 小程序 Swagger 文档(调用了哪些后台接口)
- 通过标准:真机打开小程序能正常浏览商品、能登录、能加购
- 第3阶段:
- 设备 MQTT 通信日志(broker 收到的所有消息 + 时间戳)
- 设备指令执行记录(对于每条指令记录:发出时刻、设备接收时刻、执行结果、完成时刻)
- 离线重试测试报告(设备离线时发指令、再上线的执行顺序)
- 通过标准:指令下发成功率 100%,设备离线重试有效
- 第4阶段:
- 端到端完整流程录屏 3 分钟(从小程序用户点录制,到设备播放完成,中间显示所有 MQTT 消息)
- 性能指标(从小程序点击到设备反馈的延迟、音频传输成功率)
- 边界情况测试报告(设备离线、网络断线、文件过大、格式不支持等)
- 通过标准:真机录音并推送到设备,设备成功播放,小程序显示进度
何时切换到真实数据
每个阶段 mock 和真实的分界线很明确,不要超前也不要延后:
- 不要延后:后台 API 已写好了还继续用 mock,小程序就永远发现不了接口格式不匹配的问题。
- 不要超前:设备硬件还没到,不用强行对接真实 MQTT broker,浪费时间调试网络问题。
经验是:看依赖关系。小程序需要后台 API 才能测,但小程序的 UI 测试不需要真设备;设备需要后台 MQTT 才能下指令,但这时候小程序可以继续 mock。按这个粒度切分,三个角色都不会被卡住。
■