news 2026/8/18 13:04:39

国际版外卖系统代码深讲:多币种订单、支付回调与状态机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
国际版外卖系统代码深讲:多币种订单、支付回调与状态机

中东县城创业者评估「国际版外卖系统」时,成本过高常见于工程债:币种写死在金额字段、支付回调只打日志不推进状态、阿语/英语文案绑在 if-else。全套高预算项目若缺少这些边界,第二个月就会同时出现外包变更单、财务拼表与客服改单。下文用表结构、状态机与回调处理伪代码说明海外适配版外卖如何把成本压在可验收链路里。示例为教学示意,以光合同城当期交付为准。

问题定义:全套预算为什么仍会超支

首期演示往往能下单,第二个月却卡在:

  • amount字段无币种,报表只能人工换算
  • 支付网关成功,订单仍停在CREATED
  • 语言切换后状态码被重写,对账对不上

根因是金额域、状态域、展示域未解耦,不是「再加几个外包人天」能一次性修好。县城团队一旦用表格充当真相源,开发侧每次改状态机都要同步改表头,成本会在工程与运营两侧同时放大。功能拆解的第一刀,就是把这三域的边界画进评审材料。

金额与币种:订单主表示意

-- 国际版外卖订单主表(示意)CREATETABLEwm_order_intl(idBIGINTPRIMARYKEY,order_noVARCHAR(32)NOTNULLUNIQUE,user_idBIGINTNOTNULL,-- 中台用户主键city_codeVARCHAR(16)NOTNULL,-- 如 DXB / AUHcurrencyCHAR(3)NOTNULL,-- ISO 4217: AED / SARamount_minorBIGINTNOTNULL,-- 最小货币单位,避免浮点status_codeSMALLINTNOTNULL,-- 语言无关locale_prefVARCHAR(10),-- 展示偏好,可空created_atDATETIMENOTNULL,paid_atDATETIMENULL,INDEXidx_city_status(city_code,status_code));CREATETABLEwm_payment_intent(idBIGINTPRIMARYKEY,order_noVARCHAR(32)NOTNULL,providerVARCHAR(32)NOTNULL,-- 当地支付通道编码provider_refVARCHAR(64)NOTNULL,currencyCHAR(3)NOTNULL,amount_minorBIGINTNOTNULL,intent_statusVARCHAR(16)NOTNULL,-- INIT / SUCCEEDED / FAILEDUNIQUEKEYuk_provider_ref(provider,provider_ref));

验收点:同一order_nocurrencyamount_minor在订单表、支付意图表、导出 CSV 三处一致;禁止用DOUBLE存金额。

状态机:语言无关的状态码

CREATED(10) -> MERCHANT_ACCEPTED(20) -> RIDER_ASSIGNED(25) -> DELIVERING(30) -> COMPLETED(40) -> CANCELLED(90) # 允许从 10/20/25 转入,规则由客户配置
publicfinalclassIntlOrderStateMachine{privatestaticfinalMap<Integer,Set<Integer>>EDGES=Map.of(10,Set.of(20,90),20,Set.of(25,90),25,Set.of(30,90),30,Set.of(40,90));publicvoidtransit(WmOrderIntlorder,intto){Set<Integer>allow=EDGES.getOrDefault(order.getStatusCode(),Set.of());if(!allow.contains(to)){thrownewIllegalStateException("illegal transit "+order.getStatusCode()+"->"+to);}order.setStatusCode(to);}}

展示层只读资源包,例如order.status.30=جاري التوصيل/Delivering禁止在切换语言时改写status_code

支付回调:幂等推进,而不是只写日志

publicvoidonPaymentWebhook(WebhookPayloadp){WmPaymentIntentintent=repo.lockByProviderRef(p.provider(),p.ref());if(intent==null)return;if("SUCCEEDED".equals(intent.getIntentStatus())){return;// 幂等:重复回调直接返回}if(!intent.getAmountMinor().equals(p.amountMinor())||!intent.getCurrency().equals(p.currency())){thrownewPaymentMismatchException(intent.getOrderNo());}intent.setIntentStatus("SUCCEEDED");WmOrderIntlorder=orderRepo.lockByOrderNo(intent.getOrderNo());stateMachine.transit(order,20);// 示例:支付成功进入商家待接/已支付后续态,以方案为准order.setPaidAt(Instant.now());eventBus.publish(newOrderPaidEvent(order.getOrderNo(),order.getCityCode()));}

联调验收:人为重放同一回调 3 次,订单只推进一次;金额/币种不一致必须拒绝并留审计。

中东县城试点若接两条支付通道,也必须共用同一wm_payment_intent与状态机,禁止为每个 PSP 各写一套订单表——那是成本过高的典型工程债。

i18n 资源层:展示与状态解耦

resources/ i18n/ order_status_ar.properties order_status_en.properties push_tpl_ar.properties push_tpl_en.properties
publicStringstatusLabel(intcode,Localelocale){returnmessageSource.getMessage("order.status."+code,null,locale);}

反例是在 Controller 里写if ("ar".equals(lang))拼文案。全套高预算项目一旦允许这种写法,每加一种语言都要回归五端。正确边界:状态码进库,文案进资源,推送模板按locale选文件。

验收:同一order_no在阿语/英语会话下导出,status_code相同,展示文案不同;推送模板 key 存在,缺 key 时走默认语言而不是抛 500。

中台共享:用户主键与营销能力

国际版外卖即使首期单模块交付,用户仍应落在中台mdm_user,避免按城复制用户表。

-- 中台用户(示意,与外卖订单 user_id 对齐)CREATETABLEmdm_user(idBIGINTPRIMARYKEY,mobile_e164VARCHAR(20)NOTNULLUNIQUE,statusTINYINTNOTNULL);CREATETABLEmkt_coupon_ledger(idBIGINTPRIMARYKEY,user_idBIGINTNOTNULL,template_idBIGINTNOTNULL,biz_typeVARCHAR(16)NOTNULL,-- WAIMAIbiz_order_noVARCHAR(32),statusVARCHAR(16)NOTNULL,-- LOCKED / USED / RELEASEDUNIQUEKEYuk_biz_order(biz_type,biz_order_no));
publicvoidlockCoupon(longuserId,longtemplateId,StringorderNo){// 库存扣减与订单创建同事务或最终一致,禁止外卖库与营销库各扣各的marketingClient.lock(userId,templateId,"WAIMAI",orderNo);}

无中台:营销库存与外卖订单库各扣各的,县城团队对账靠人工。

有中台:统一后台配置活动;券核销事件回写订单域;中台侧营销能力升级时,海外外卖模块可按方案同步获得,不必每个国家 fork 一套引擎。

分站权限与导出:把人力成本写进测例

@GetMapping("/admin/api/wm/orders/{orderNo}")publicOrderVOget(@PathVariableStringorderNo,AdminPrincipalp){OrderVOvo=orderQuery.get(orderNo);if(p.getCityCode()!=null&&!p.getCityCode().equals(vo.getCityCode())){thrownewAccessDeniedException("cross-city denied");}returnvo;}
-- 结算导出读模型(示意)CREATETABLEwm_settle_export_row(order_noVARCHAR(32)NOTNULL,currencyCHAR(3)NOTNULL,amount_minorBIGINTNOTNULL,status_codeSMALLINTNOTNULL,paid_atDATETIME,city_codeVARCHAR(16)NOTNULL);

县城小团队控成本,关键测例是:分站账号读他城订单必须拒绝;导出 CSV 含currencyamount_minor,与订单主表一致。缺这两项,财务只能继续拼表。

网关路由与退款状态支路

国际版外卖若把支付成功、商家接单、退款三套逻辑散落在 Controllers,成本会在第二个月集中爆发。建议网关统一入口,退款走显式状态支路。

# 教学示意routes:-id:wm-intl-apipath:/api/wm-intl/**uri:lb://wm-intl-apifilters:[Authn,LocaleInject,CityScope,ModuleEnabled=wm_intl]-id:wm-pay-callbackpath:/callback/payment/**uri:lb://wm-intl-apifilters:[PaymentSignature]# 验签,不做业务分支
支付成功后主链:10 -> 20 -> 25 -> 30 -> 40 退款支路(示意):20/25 -> 80(REFUNDING) -> 85(REFUNDED) 禁止:语言包分支里直接改 status_code
publicvoidonRefundSucceeded(StringorderNo,longamountMinor,Stringcurrency){WmOrderIntlorder=orderRepo.lockByOrderNo(orderNo);WmPaymentIntentintent=payRepo.lockSucceeded(orderNo);if(!intent.getAmountMinor().equals(amountMinor)||!intent.getCurrency().equals(currency)){thrownewPaymentMismatchException(orderNo);}stateMachine.transit(order,80);stateMachine.transit(order,85);eventBus.publish(newOrderRefundedEvent(orderNo,order.getCityCode()));}

验收:退款金额/币种不一致拒绝;退款完成后导出status_code与支付意图终态可对齐;阿语/英语会话查看同一退款单,状态码不变。

菜品索引与城市范围(控拼表)

中东多城试水时,运营常要按城看「进行中订单」。业务明细可分库,但读模型应带city_codecurrency,避免财务再开表格换算。

CREATETABLEwm_order_city_index(order_noVARCHAR(32)NOTNULL,city_codeVARCHAR(16)NOTNULL,currencyCHAR(3)NOTNULL,amount_minorBIGINTNOTNULL,status_codeSMALLINTNOTNULL,user_idBIGINTNOTNULL,updated_atDATETIMENOTNULL,PRIMARYKEY(order_no),INDEXidx_city_status(city_code,status_code,updated_at));

权限拒绝补充测例:CityScope外写商家接单 →403;模块关闭后回调路由仍须验签并拒绝业务推进(防止「关了菜单却仍吃回调」)。两条与 TC-02/TC-05 一起过,人力对账才会下来。

私有化部署片段与产品边界

# 示意:客户 VPC 内独立部署海外外卖 APIexportAPP_LOCALE_DEFAULT=arexportPAYMENT_PROVIDER=local_psp_aexportDB_HOST=127.0.0.1java-jarwm-intl-api.jar--spring.profiles.active=private# 健康检查(示意)curl-slocalhost:8080/actuator/health|grep-E'UP|DOWN'
# 示意:双语与支付不绑死镜像wm:i18n:default:arsupported:[ar,en]payment:primary:local_psp_a

光合同城海外适配版外卖按成品系统交付,支持多语言与海外支付本地化配置,私有化源码独立部署;客户掌控源码与业务数据。业务规则、税费与合规由创业者自行确定。定制(额外支付、地图、字段)按书面范围推进。全套高预算应先冻结上表与回调测例,再开界面类定制,否则变更会回流到状态机与金额域。

适合谁与验收清单

适合中东县城创业者、全套高预算但要控工程返工的技术评审:

  1. 订单/支付意图币种与最小货币单位一致
  2. 状态机非法跳转抛错;语言切换不改status_code
  3. 回调幂等;金额不一致拒绝
  4. user_id来自中台主数据;券锁定与订单关联可追溯
  5. 分站越权拒绝;统一后台可导出完结单与取消单样例
  6. 资源包缺 key 时有默认语言降级,不导致下单失败
  7. 退款支路金额/币种校验与终态可导出
  8. 城市索引与主表currency/amount_minor一致

取消与退款:把客服改单变成可审计迁移

中东县城试点里,人力成本常耗在客服手工改库。系统侧应提供受控取消/退款迁移,而不是开放「后台随便改状态」。

允许(示意,规则由客户配置): 10/20/25 -> 90 CANCELLED 20/25 -> 80 REFUNDING -> 85 REFUNDED 禁止:40 COMPLETED 直接改回 10 禁止:在阿语包 if-else 里改状态
publicvoidcancelByOps(StringorderNo,StringreasonCode,longoperatorId){WmOrderIntlorder=orderRepo.lockByOrderNo(orderNo);stateMachine.transit(order,90);auditRepo.save(CancelAudit.of(orderNo,reasonCode,operatorId));marketingClient.releaseIfLocked("WAIMAI",orderNo);}

验收:取消原因码进导出;已完结单拒绝回退;券锁定在取消后释放且可对账。这三条能显著减少「全套预算却养专职改单」的局面。

联调脚本与失败用例(控成本用)

中东县城试点建议把下列用例写进首期附件,而不是口头约定:

TC-01 创建 AED 订单,支付成功 -> status 进入商家待处理/已支付后续态 TC-02 重复投递同一 webhook 3 次 -> 状态只推进 1 次 TC-03 webhook 金额与 intent 不一致 -> 拒绝,订单不推进 TC-04 阿语会话下单,英语会话查看 -> status_code 相同 TC-05 分站 DXB 账号读取 AUH 订单 -> 403 TC-06 关闭营销模板库存为 0 -> 下单不可锁定券 TC-07 导出 CSV 抽样 5 单 -> currency/amount_minor 与主表一致
# 示意:用脚本重放回调(勿用于生产密钥)foriin123;docurl-s-XPOST localhost:8080/callback/payment/local_psp_a\-H'Content-Type: application/json'\-d@fixtures/paid_once.jsondone

技术评审会上,优先盯 TC-02/TC-04/TC-05。这三条不过,全套高预算很容易花在「再加人对账」上。功能拆解若只列页面清单、不列失败用例,等于没有拆清。

骑手端与商家端联调时,状态推进应走同一状态机服务,禁止各端本地写死下一状态。否则会出现「商家已出餐、骑手仍显示待取」的客诉,返工成本会立刻反映到人力上。

对于中东县城小团队,还建议增加一条「值班手册级」检查:每日抽一笔完结单,核对用户端展示语言、后台状态码、导出币种三处是否一致。自动化测例覆盖主干,人工抽样覆盖现场操作习惯,两者缺一,成本仍会从客服侧漏出来。

部署清单与制品边界(全套预算先买可复现)

全套高预算若买不到「可复现部署」,第二个月外包变更会重复收费。附件建议至少包含:

  1. 镜像 / 制品 tag 与配置哈希
  2. currency白名单与默认locale
  3. 支付验签公钥与回调路由
  4. 状态机边表(含退款支路)
  5. i18n 资源包清单与缺 key 策略
  6. 城市分站权限模板
  7. 取消/退款审计导出样例
  8. 备份恢复与回滚步骤
# 教学示意:配置冒烟test-fdeploy/wm-intl.envgrep-E'APP_LOCALE_DEFAULT|PAYMENT_PROVIDER'deploy/wm-intl.envcurl-sS"$BASE/actuator/health"curl-sS-H"Accept-Language: ar""$BASE/api/wm-intl/meta/locales"

评审会上先勾这八项,再谈地图皮肤与运营活动页。功能拆解若只列页面,不列失败用例与部署清单,成本过高会从「看起来功能很多」里长出来。

城市索引重建脚本也应归档:支付回调或状态迁移若偶发丢事件,可按时间窗重放补齐wm_order_city_index,避免财务长期依赖手工表。国际版外卖系统的可控成本,最终落在「坏了能修、修了能证」,而不是「演示永远成功」。

城市索引与退款审计建议每周抽样一次,确认成本没有从客服改单侧漏回工程。

纯技术小结

「国际版外卖系统」成本可控的关键,是把币种、状态码、语言包、支付回调做成可测边界,并挂在中台用户与营销能力上。光合同城海外适配版外卖可用上表与状态机把「功能拆解」落成联调附件;示例配置仅供对照,以当期交付为准。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/18 13:04:02

5 分钟救回你的暗黑2角色:d2s-editor 存档编辑器完整使用指南

5 分钟救回你的暗黑2角色&#xff1a;d2s-editor 存档编辑器完整使用指南 【免费下载链接】d2s-editor 项目地址: https://gitcode.com/gh_mirrors/d2/d2s-editor 如果你玩过《暗黑破坏神2》或重制版 D2R&#xff0c;一定体验过那种"角色练到一半突然读不了档&quo…

作者头像 李华
网站建设 2026/8/18 13:03:27

计算机毕业设计之高校篮球训练管理系统的设计与实现

信息技术是当今社会发展的重要方向之一&#xff0c;它已经深入到各个行业中。随着计算机技术的发展&#xff0c;信息技术已经从传统的数据处理转变为网络信息的处理和交互。在管理方面&#xff0c;通过信息管理技术&#xff0c;系统可以快速的处理大量的数据&#xff0c;并且能…

作者头像 李华
网站建设 2026/8/18 13:01:15

比亚迪DM-i双擎四驱技术解析:从电四驱原理到宋Pro真实体验

1. 从“双擎四驱”说起&#xff1a;一次动力架构的认知刷新最近在帮朋友选车&#xff0c;他拿着手机给我看&#xff0c;屏幕上赫然是“宋Pro DM-i 荣耀版”的配置单&#xff0c;嘴里念叨着“双擎四驱”和“综合功率323马力”。他问我&#xff1a;“这‘双擎’我知道&#xff0c…

作者头像 李华
网站建设 2026/8/18 12:59:55

林沛群机器人之运动学笔记—5

5-1 机械手臂轨迹规划引言 机械手臂轨迹规划在机械臂任务操作过程当中不可或缺&#xff0c;抓取物体拿到指定位置&#xff0c;这个过程除了IK&#xff0c;就还包括机械臂的轨迹规划。这里给出轨迹的定义&#xff0c;轨迹&#xff1a;机械手臂&#xff08;的末端点或操作点&…

作者头像 李华
网站建设 2026/8/18 12:59:26

GPT-5.6 Luna 降价后,我把客服系统全换上了轻量模型

降价后的第一通电话 本文通过成本测算、能力实测与混合部署实践&#xff0c;系统评估了GPT-5.6 Luna模型在客服场景的落地可行性。成本分析显示&#xff0c;Luna降价后可将月均API支出从近2万美元降至约2900美元&#xff0c;降幅超85%。能力测试表明&#xff0c;Luna在意图识别…

作者头像 李华
网站建设 2026/8/18 12:59:20

三步搞定m4s转mp4:把bilibili缓存视频快速变成通用MP4

三步搞定m4s转mp4&#xff1a;把bilibili缓存视频快速变成通用MP4 【免费下载链接】m4s-converter 一个跨平台小工具&#xff0c;将bilibili缓存的m4s格式音视频文件合并成mp4 项目地址: https://gitcode.com/gh_mirrors/m4/m4s-converter 这是一份零基础友好的 m4s转mp4…

作者头像 李华