多商户外卖系统上云,首先要理清订单、支付、退款和结算明细之间的关系:交易发生时保存当次规则,收到重复消息时避免重复记账,已经结算的订单发生退款时追加调整记录,再根据可追踪的数据生成报表。云服务器和数据库承载这些业务,账单是否正确仍取决于应用层的设计。
微订面向外卖、跑腿和本地生活平台提供多角色系统,其公开产品资料包含商家结算、配送员结算、多站点及私有化部署。本文由微订整理,以这些业务场景讨论阿里云上的一种可选设计。文中的事件模型和部署组合属于通用方案建议,并非微订现网架构披露或已经完成的项目测试报告。
一、先定义四类记录,避免一个状态解释所有问题
“订单完成”描述的是交易履约阶段,“支付成功”描述的是资金收取结果,“退款成功”对应一次退款结果,“商家已结算”则涉及结算批次。它们有联系,但更新时点和核对依据不同。
| 记录 | 建议保留的标识和信息 | 核对用途 |
|---|---|---|
| 业务订单 | 平台、商家、订单号、交易金额、优惠承担方、费用规则版本 | 还原交易条件和费用计算依据 |
| 支付记录 | 支付单号、渠道交易号、金额、币种、核验状态 | 将业务订单与渠道收款结果对应 |
| 退款记录 | 退款单号、原支付单号、退款金额、渠道结果 | 区分多次部分退款及其处理进度 |
| 结算明细 | 商家、订单号、原始费用、调整项、账期、结算批次 | 解释本期应结、已结及待处理差异 |
如果一次支付覆盖多个商家的订单,还要保存支付单与各商家子订单的金额分配关系。否则只能看见一笔总收款,退款和对账时却找不到各商家的对应金额。
金额统一采用最小货币单位整数,或精确定义的十进制类型,并明确币种和舍入规则。不能用前端传入的合计金额直接记账;服务端需要按可信商品、优惠与费用规则重新核算,核对支付渠道返回的信息。

图:微订官网多角色产品展示,用于说明订单涉及的业务参与方。
二、保存费用快照,才能解释跨账期变化
假设某商家在账期中途调整了佣金规则。若财务报表读取订单时,统一使用当前费率重新计算,调整前的订单也会被套用新规则。
建议在约定的计费事件发生时保存费用快照,包括规则版本、生效时间、计费基数、各方优惠承担金额、计算结果和舍入方式。计费事件可以由合同和业务确定为支付、完成或其他节点,但必须明确,不能在不同报表中随意变化。
退款也需要有自己的业务编号。下面用一组简化数字说明已结算订单如何处理调整。
示例假设:订单金额为 100 元,平台佣金为 10 元,商家应收为 90 元;暂不计配送费、优惠补贴和渠道手续费。结算后成功退款 30 元,约定佣金按退款比例退还。这些数字仅用于说明计算关系,不是微订费率或报价。
按照上述假设,本次应冲减平台佣金 3 元、商家应收 27 元。调整后,该订单累计平台佣金为 7 元,累计商家应收为 63 元,两者合计 70 元,与原收款减退款一致。
原结算单应继续保留当时的 90 元商家应收记录,新退款在后续账期形成关联原订单、退款单与原结算批次的负向调整。如果原款已付给商家,还需按约定处理追收或后续抵扣,不能把账面调整当作已经收回资金。
商家应收、平台费用、退款和渠道实际到账,需要分别核对。不同项目的优惠、配送费、渠道费承担规则不同,不能套用一条固定比例公式。
三、在阿里云上按职责组织组件
一种可选组合如下。这里按职责分层,不要求项目一开始就部署多个数据库或微服务。
用户端 / 商家端 / 骑手端 / 平台管理端
↓
应用服务:鉴权与业务处理
↓
RDS MySQL:业务记录 + 本地事件表
↓
事件投递任务 → RocketMQ
↓
对账明细处理任务
↓
报表表或查询库
↓
异步导出任务 → 私有 OSS
应用服务可以根据已有运行环境选择 ECS 等计算资源。订单与事件表需要满足下文的本地事务条件;报表可以先使用单独的表、账号和查询任务,再根据测量结果决定是否增加只读实例或独立查询库。
只增加一个 schema,并不会获得独立的 CPU、内存和 I/O。报表查询也不一定直接锁住订单写入,但大范围扫描可能竞争资源、增加连接占用,影响在线请求。因此,应先定位慢查询、连接池、复制延迟和资源瓶颈,再决定扩容或隔离。
Redis 可用于合适的缓存场景。资金核对仍应依据持久化交易记录、业务账务明细和渠道账单,避免把可能失效的缓存当作最终依据。
四、事件能重试,账务结果只生效一次
订单更新成功后再单独发送消息,会出现一个故障窗口:数据库提交了,进程却在发消息前退出。反过来,消息先发出而业务事务回滚,也会产生不一致。
可以采用本地事件表,也称 Outbox:在同一个数据库事务里写入业务变化和待发送事件。事务提交后,投递任务读取事件并发送至 RocketMQ,发送确认后再记录投递结果。发送结果不明时允许重试,因此消费侧仍必须处理重复。
阿里云 RocketMQ 文档明确提醒,同一消息可能重复投递,业务应使用业务唯一标识进行幂等处理,不能仅依赖消息 ID。
可把以下结构作为评审草图:
业务事件
所属平台:platform_id
事件类型:payment_confirmed / refund_confirmed
业务唯一标识:payment_id / refund_id
原订单标识:order_id
业务版本、发生时间、记录时间
金额、币种、费用快照或快照引用
每个处理器的去重键
处理器名称 + 所属平台 + 事件类型 + 业务唯一标识
处理器名称用于区分不同订阅用途。同一退款事件可以分别更新财务明细与运营统计,各处理器内部只生效一次,不能因为财务已经消费,就让统计处理器漏掉该事件。
在同一个处理器中,去重记录与业务明细的变更应处于同一事务。提交成功后再确认消费。否则,“先记已处理、后写明细”可能造成漏账,“先写明细、后记已处理”则可能造成重复。
去重键也不能只用订单号。一笔订单可能分多次退款,每次有效退款都应有独立业务标识。同一个退款编号若出现不同金额,应进入冲突核对,不应简单忽略。
消息去重解决的是重复处理。退款先于原交易投影到达、旧版本晚到等情况,还需要版本校验、依赖等待或补偿任务。持续失败的事件要保留错误原因和人工处理入口,再结合来源记录进行对账补漏。
五、导出报表要写清截止口径
财务开始导出时,后台可能还在接收订单和退款。只靠分页查询“当前记录”,不同页可能对应不同的数据时点。
建议为一次导出创建任务,保存查询条件、商家范围、业务时间范围、账期、数据版本或已处理事件水位。这里的“水位”指本次报表确认处理到哪一批事件,用来说明数据完整到什么位置。
分页结果应来自同一可复现的数据快照,或来自已经冻结的账期明细。仅添加一个发生时间截止条件,仍可能漏掉截止之后才到达、但业务发生时间较早的消息。迟到事件应按约定生成差异报告或新版本,不能静默覆盖已交付的旧报表。
导出完成后,将文件放入私有 OSS,由应用核对下载人的商家和角色权限后签发短期访问链接。签名链接本身具有访问能力,应避免写入公开日志;有效期、下载审计和文件保留时间按项目要求设置。
只读实例能分担部分查询负载,但可能存在复制延迟。需要接近实时的财务报表时,应先检查数据完整性,再决定输出结果或明确提示截止时间。

图:微订官网平台产品展示。本文的云组件与事件设计属于方案建议。
六、用异常用例检验对账方案
正常订单跑通后,还需要验证重复、延迟和跨账期情形。下面的表可用于开发评审及交付演示。
| 测试情形 | 预期观察 |
|---|---|
| 同一支付或退款通知重复到达 | 相同业务结果只记一次,重复请求可追踪 |
| 同一订单发生两次有效部分退款 | 两个退款单分别处理,累计金额与渠道记录一致 |
| 同一退款编号携带不同金额 | 系统记录冲突并停止自动入账 |
| 账期内变更佣金规则 | 历史订单按原规则快照计算,新单按约定生效时间处理 |
| 商家结算后发生退款 | 原结算单保留,新账期生成可追溯调整 |
| 消费任务提交后、确认前退出 | 重投后没有新增重复明细 |
| 导出期间出现迟到退款事件 | 报表版本、截止口径及后续调整可以解释 |
| 商家尝试下载其他商家的报表 | 业务授权校验拒绝请求,不签发下载链接 |
| 数据恢复或消息补投 | 校验原交易、事件与明细的完整性,补投不会重复记账 |
采用微订或其他成熟系统时,可以把这些用例对应到标准功能、配置项和定制范围,再明确云资源、接口、运维及交付责任。微订官网已公开商家与配送员结算、多方分账等能力,实际执行仍受已开通支付通道规则、业务主体和项目配置约束。
应用账务计算正确与资金实际结算完成,需要各自的证据。云数据库、消息队列或软件中的“结算成功”标签,都不能单独证明渠道款项已到账。验收时沿订单号、支付单号、退款单号和结算批次核对,财务才能解释一笔金额为什么出现在这张账单里。