做外卖项目最核心的闭环就是用户下单和订单支付。我接触“苍穹外卖”这个项目也有段时间了它不单单是一个CRUD练习而是把一套真实的C端交易链路浓缩在了几个小时点外卖的流程里。很多朋友卡在“下单成功了但支付回调不通”“订单状态乱跳”“部署到Linux后接口404”这些坎上说到底还是对业务状态机和数据一致性的理解不够透。这篇我就把用户下单、订单支付这两块完整拆开从数据库设计讲到代码实现再到Linux部署把我在实际开发中踩过的坑和验证过的方案一起说清楚。想把这套流程吃透的朋友建议照着思路自己走一遍。1. 项目整体设计与核心模块拆解很多做苍穹外卖的同学一上来就闷头写代码写完登录写菜品写到下单才发现前面埋了一堆雷。我建议先站在全局看这条链路再动手。1.1 下单支付链路全景用户下单和订单支付不是两个独立接口而是同一条状态链上的两个环节。完整链路大概是这样的用户点击“去结算”的时候前端会先请求购物车数据回显菜品清单和总价。用户确认后调用“用户下单”接口后端生成订单记录并返回订单号和应付金额。前端拿到订单号后调起微信支付完成支付后微信服务器异步通知后端后端更新订单状态为“待接单”前端再通过轮询或回调刷新页面状态。这里有几个容易忽略的点下单接口本身不扣款它只负责创建订单和锁定金额。支付动作发生在第三方微信后端要能接收异步通知。订单状态必须以服务端为准不能信任前端传过来的任何状态。这条链路里订单模块是主干支付模块是支干二者的连接点就是“订单状态”。所以设计状态机比写接口还要优先。1.2 技术选型与架构取舍苍穹外卖这个项目主流技术栈是Spring Boot MyBatis MySQL Redis外加微信支付V3。这个组合做小规模外卖系统是够用的也贴近大多数公司的真实落地情况。为什么选Redis在这个项目里它至少干三件事存购物车、存验证码、做分布式锁。下单场景下Redis还能帮我们对“同一用户并发下单”做限制。虽然数据量不大但它的存在能把响应时间压到几十毫秒数据库压力也会小很多。MySQL是必须的订单和明细这种核心交易数据必须落盘且支持事务。MyBatis则负责把SQL和Java对象做映射写复杂多表查询时比JPA更容易控制。微信支付这块我建议直接对接“Native下单”或“JSAPI下单”。苍穹外卖如果是电脑端演示Native最合适如果要用手机浏览器或小程序就得上JSAPI。不管哪种核心都是统一下单、回调验签、查询订单三个接口。1.3 订单状态机的设计思路状态机是整个订单系统的灵魂。没有状态机代码里全是if else写着写着就乱了。苍穹外卖的订单状态通常包括状态值含义触发动作1待付款用户下单成功2待接单支付成功3已接单商家接单4派送中骑手取餐5已完成用户确认或自动完成6已取消超时未支付/用户取消/商家拒单不同版本不同7退款退款申请中扩展项设计状态机有两个原则我建议记住第一状态只能单向流转或者按明确的规则跳转。比如从“待付款”可以到“已取消”但不能直接到“已完成”。第二状态变更必须记录操作人。谁在什么时间把订单从1改成2后续排查问题的时候一眼就能看到。最好在订单表里加一个operation_time和operator字段或者单独建操作日志表。我实际开发中发现状态机如果一开始没设计对后面加退款、加超时关单代码就会越改越乱到处是状态判断很容易把“待付款”直接改成“已完成”这单数据就废了。所以下单前先把状态图在纸上画清楚比多写十个接口都值。2. 数据库设计与订单表结构很多人觉得数据库设计就是建几张表等写到订单明细查询才发现当时少建了字段又要回改表。苍穹外卖这种项目订单相关的表至少要有四张订单表、订单明细表、购物车表、支付回调日志表。2.1 订单主表的核心字段订单主表存一笔订单的汇总信息。下面是核心字段不是全部但少一个后面都难过id主键用雪花算法生成别用自增。自增ID在对外场景下容易被爬也不方便分表。order_number订单编号展示给用户看的一般用时间戳随机数或日期串自增序号。下单人心里有数客服也靠它追溯。user_id用户ID建索引这是查询频度最高的条件。amount订单总金额单位用分存整数。这个极其重要千万别用浮点型存金额。pay_amount实付金额优惠券抵扣后真正要付的钱。设计优惠模块时必须有这个字段。status订单状态用tinyint存配合状态机常量。pay_status支付状态0未支付1已支付2退款。这个字段和status分开别混在status里。payment_time支付时间后续查超时单、对账都要用。address_book_id地址ID如果做外卖配送需要把地址快照冗余进来。用户改了地址不影响历史订单。remark备注用户填的“不要辣”“多加醋”之类。再强调一次金额存储用int存分。比如10块钱存1000。别用decimal(10,2)虽然能算对但后续接支付接口做分转元、元转分容易出错而且不同语言的浮点精度坑多。我见过太多项目因为金额用double导致对账差一分钱排查到崩溃。2.2 订单明细表与购物车表订单是一个主表单明细就是主表的下级表。外卖场景下明细表记录每个菜品的快照信息。关键字段有id主键。order_id关联订单主表的id建索引。dish_id / setmeal_id菜品或套餐ID。dish_name菜品名称下单那一刻的名称别等菜品改名影响历史记录。dish_image菜品图片结算页展示用。dish_flavor口味比如“微辣、少冰”。number购买数量。amount该明细的小计金额也是分存储。这里有个设计要点明细表里必须冗余菜品名称、价格、口味。这不是脏数据而是订单快照的必要设计。数据库只要保存“下单那一刻”的信息就够了商品表后续怎么改名、改价都影响不到已产生的订单。购物车表就简单多了它是“未提交的订单草稿”。字段一般有id、user_id、dish_id、setmeal_id、dish_flavor、number、create_time。购物车数据存Redis比较多但苍穹外卖用MySQL表也完全可以用户量小逻辑更直观。下单成功后记得按user_id清空购物车。我个人更喜欢购物车落库加一个user_id唯一索引或组合索引这样查询“用户购物车”就是一条SQL的事而且不需要考虑Redis和DB数据一致性问题。小项目用表比用缓存简单可靠。2.3 索引设计与金额精度索引设计直接影响订单查询性能。订单表最少需要的索引包括user_id查“我的订单”。status运营后台按状态筛选。order_number唯一索引订单号防重复也是用户和客服最常输入的查询条件。create_time按时间范围查单。order_number用唯一索引是必须的能防住并发生成同一订单号虽然雪花ID几乎不可能重复但数据库层面再加一道保险心里踏实。再就是金额精度问题。明细表中每个菜品的小计还有主表的总额全部用int存分。计算的时候如何算总额总额 Σ(明细小计) // 明细小计 单价(分) × 数量优惠金额、配送费单独用字段存比如delivery_amount、discount_amount。这样对账时公式清晰amount 菜品总额 配送费 - 优惠金额如果只存一个总金额后面退款、售后的拆分会很麻烦。2.4 数据库设计文档的整理习惯刚才说的这些字段设计整理成数据库设计文档时我建议至少包含下面几块每个表的用途说明一句话讲清楚。字段名、类型、是否允许为空、默认值、注释。索引清单说明每个索引是为了支撑什么查询。关键状态字段的枚举说明status字段每个值代表什么。ER关系描述订单主表和明细表、购物车表之间的关联。很多人图省事不写文档等部署到Linux后想查一个字段的含义又要翻代码。苍穹外卖这种教学级别的项目养成写文档的习惯比项目本身更值钱。3. 用户下单流程的完整实现3.1 下单接口的请求与响应设计苍穹外卖的下单接口一般设计成POST请求路径类似/user/order/submit前端传的东西很简单地址簿ID、备注、预期送达时间。菜品数据不传后端直接从购物车表查。别让前端把菜品列表传过来那样后端还要校验菜品是否属于用户购物车平白增加漏洞。请求参数DTO大概长这样public class OrdersSubmitDTO { private Long addressBookId; // 地址簿ID private String remark; // 备注 private LocalDateTime estimatedDeliveryTime; // 预计送达时间 }响应VO返回订单ID、订单号、应付金额这些是前端调支付必须有的数据。这里有个细节下单接口必须做参数校验addressBookId为空要报错estimatedDeliveryTime不能早于当前时间。如果连这些基础校验都没有后面维护会很难受。3.2 下单核心事务逻辑下单接口是典型的事务场景一个方法里要做的事很多任何一个环节失败都不能让部分数据落库。核心流程分五步简单说校验地址簿是否属于当前用户防止用别人地址下单。查询用户购物车如果为空直接抛异常。遍历购物车计算每项明细的小计累加总额生成订单主表数据。批量插入订单明细表。清空购物车返回订单VO。这五步必须包在同一个事务里任何一步失败全部回滚。用Spring的Transactional注解就能搞定。代码逻辑核心大概是这样的Transactional(rollbackFor Exception.class) public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) { // 1. 校验地址簿 AddressBook addressBook addressBookMapper.getById(ordersSubmitDTO.getAddressBookId()); if (addressBook null || !addressBook.getUserId().equals(currentUserId())) { throw new BusinessException(地址不存在); } // 2. 查询购物车 ListShoppingCart cartList shoppingCartMapper.listByUserId(currentUserId()); if (cartList null || cartList.isEmpty()) { throw new BusinessException(购物车为空无法下单); } // 3. 构造订单主表 Orders order new Orders(); order.setOrderNumber(OrderNumberGenerator.generate()); order.setUserId(currentUserId()); order.setStatus(Orders.PENDING_PAYMENT); // 待付款 order.setPayStatus(Orders.UN_PAID); // amount累加 long totalAmount 0; ListOrderDetail detailList new ArrayList(); for (ShoppingCart cart : cartList) { long subTotal cart.getAmount() * cart.getNumber(); totalAmount subTotal; OrderDetail detail new OrderDetail(); detail.setDishId(cart.getDishId()); detail.setDishName(cart.getDishName()); detail.setDishFlavor(cart.getDishFlavor()); detail.setNumber(cart.getNumber()); detail.setAmount(cart.getAmount()); detailList.add(detail); } order.setAmount(totalAmount); order.setPayAmount(totalAmount); orderMapper.insert(order); // 4. 批量插入明细 detailList.forEach(detail - detail.setOrderId(order.getId())); orderDetailMapper.batchInsert(detailList); // 5. 清空购物车 shoppingCartMapper.cleanByUserId(currentUserId()); // 6. 返回VO OrderSubmitVO vo new OrderSubmitVO(); vo.setId(order.getId()); vo.setOrderNumber(order.getOrderNumber()); vo.setPayAmount(order.getPayAmount()); return vo; }这个逻辑看起来简单但写的时候有几个容易踩的坑必须先设置明细的orderId再批量插入不然外键为空。金额计算推荐用long类型避免浮点误差。Transactional要写在public方法上自调用会失效。整个事务里不要调用第三方HTTP接口。支付操作要放在下单事务提交之后否则微信支付回调来了订单还没提交容易出问题。我实际遇到过一个问题下单事务里同步调用了短信通知服务短信服务超时导致下单事务被拉长MySQL连接被占满。后来把所有外部调用都改成事务提交后异步执行瞬间好了。你后面做苍穹外卖扩展功能时也要记住“事务里只做数据库操作”。3.3 并发下单与防重处理并发场景是小项目最容易忽略的。如果用户手抖点了两次“提交订单”可能出现两笔一样的订单。防重可以从两个层面控制第一数据库层订单号唯一索引兜底。就算代码生成重复订单号插入时也会报错。第二应用层用Redis分布式锁对同一用户加锁。String lockKey order:submit: userId; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 10, TimeUnit.SECONDS); if (!locked) { throw new BusinessException(正在处理中请勿重复操作); } try { // 下单逻辑 } finally { redisTemplate.delete(lockKey); }下单成功后释放锁。这样可以防住同一用户在一秒内重复提交订单。分布式锁这块不用引入Redisson那么重的东西简单的setIfAbsent就能扛住教学项目的量。还有超时未支付的订单可以用定时任务处理每分钟扫描一次“待付款但超过15分钟”的订单把状态改成已取消。这个任务要独立于下单逻辑别放在下单接口里判断。3.4 下单后的跳转与前端交互下单成功后前端拿到订单号下一步要做的就是唤起支付。这里有一个细节要注意不要把下单和支付放在一个接口里。前台拿到订单号后再调支付预下单接口这样如果支付环节出问题订单数据还在可以重新发起支付。苍穹外卖一般用微信支付前端拿到后端返回的支付参数后调起收银台。我曾经见过把支付参数直接返回给前端让它存的——这不对。支付参数应尽量只在后端和微信服务器之间流转前端只是拿到一个code_url或者调起参数用来展示二维码或小程序支付。4. 订单支付的完整实现支付是订单系统的下半场。很多人在这一步被卡住不是因为代码难而是因为没弄懂微信支付的交互流程和验签机制。4.1 微信支付V3接入的前置条件对接微信支付V3需要准备几样东西商户号mchid。API v3密钥32字节的随机字符串。商户私钥用于生成请求签名。商户证书序列号。微信支付平台证书用于验签回调。支付回调域名必须是HTTPS且在微信商户平台配置过。这些配置项在苍穹外卖项目对应的application.yml里要配置好wechat: pay: mchid: 你的商户号 api-v3-key: 你的APIv3密钥 private-key-path: classpath:apiclient_key.pem merchant-serial-number: 商户证书序列号 notify-url: https://你的域名/api/user/order/pay/notify这里说一下证书路径。不要把所有证书都打进jar包里。部署到Linux时可以把证书放到/opt/wechat/cert/目录下配置文件里写绝对路径这样更新证书不用重新打包。4.2 支付预下单参数构建与签名流程微信支付V3的预下单接口本质上就是向后端发一个POST请求请求体是JSON但必须加上Authorization请求头做签名。签名生成逻辑提炼一下构造签名串HTTP方法\n请求路径\n请求时间戳\n请求随机数\n请求体\n用小程序的SHA256withRSA对签名串做签名用的是商户私钥。把签名结果Base64编码拼到Authorization头里。苍穹外卖的场景是JSAPI支付预下单请求地址是https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi请求体包括appid、mchid、description、out_trade_no、notify_url、amount等。我这边的经验是用现成的SDKwechatpay-java是官方维护的马老师他们团队也做过封装能省掉很多签名的Debug时间。自己写签名逻辑很痛苦格式错一个字符就是各种401。4.3 支付结果回调与验签解密这是整个支付模块最重要的环节回调处理不好用户支付成功了但订单还是待付款客服电话会被打爆。微信支付成功后会向notify_url发起POST请求通知内容包含resource字段资源内容是经过加密的。处理回调的逻辑分三步第一步验签。用微信支付平台证书验证通知签名确保通知真的是微信发来的而不是伪造的请求。第二步解密。用APIv3密钥对resource中的密文做AES-256-GCM解密拿到明文数据里面包含out_trade_no、transaction_id、trade_state、amount等。第三步业务处理。根据out_trade_no查订单判断订单状态是否已经是“已支付”如果是直接返回成功防止重复处理。如果订单是“待付款”则更新支付状态、订单状态为待接单记录支付流水号和支付时间。关键防重代码如下// 1. 先查订单 Orders order orderMapper.getByOrderNumber(outTradeNo); if (order null) { return 订单不存在; } // 2. 幂等判断已经支付过的直接返回成功不重复处理 if (order.getPayStatus() 1) { return SUCCESS; } // 3. 校验金额 if (!order.getPayAmount().equals(Integer.parseInt(amount))) { log.error(支付金额与订单金额不一致orderId:{}, order.getId()); return FAIL; } // 4. 更新订单状态 order.setPayStatus(1); order.setStatus(Orders.TO_BE_CONFIRMED); order.setPayTime(LocalDateTime.now()); orderMapper.update(order);金额校验不能省。如果存在黑客伪造回调或者支付金额被篡改这里能挡住。服务端的经验是回调处理一定要做幂等微信可能会因为网络问题重复发送通知重复处理会让状态数据错乱。回调接口返回的字符串微信要求是纯文本SUCCESS或FAIL。注意不要返回JSON微信支付回调接口不认。4.4 支付状态轮询与订单校验除了回调客户端还需要主动查询订单状态。因为微信回调有时候会有延迟几秒用户支付完看到页面没变化会一直点“已支付”。苍穹外卖的订单支付轮询设计一般是前端在用户付款窗口期每隔两秒调用一次“查询订单支付状态”接口。后端收到请求后可以先去本地查订单状态如果还是待付款再调用微信支付查单接口以微信的最新状态为准。private boolean isPaid(String orderNumber) { // 先查本地 Orders order orderMapper.getByOrderNumber(orderNumber); if (order.getPayStatus() 1) { return true; } // 本地没更新查微信 // 调用微信查单接口获取订单状态 // 如果时间是已支付则更新本地订单状态 return false; }这样做的目的很明确保证本地订单状态和微信支付状态最终一致。因为回调可能延迟、可能丢虽然概率低主动查单能兜底。4.5 超时未支付自动关单用户提交订单后一直不付钱这种“僵尸订单”要定期清理。苍穹外卖一般在订单提交后15-30分钟未支付系统自动把订单置为“已取消”释放库存。实现方式有两种一种是Spring定时任务每分钟扫一次待付款订单把创建时间超过15分钟的置为取消。这种适合小项目一个Scheduled(cron 0 * * * * ?)注解就搞定。另一种是延迟队列或Redis的key过期监听但这个方案有可靠性和延迟问题教学项目不建议。定时任务写起来很简单但也有坑。我有一次接口全正常但订单还是不能自动取消查了半天发现是定时任务没生效。排查后发现是EnableScheduling注解忘了加或者加了但定时任务被多个实例重复执行。部署的时候要确认项目只部署了一个实例或者给定时任务加分布式锁不然每个实例都会跑一次出现重复关单的现象。5. 部署到Linux的实操要点很多人都折在部署这一步。本地跑得好好的一到Linux服务器就各种404、数据库连不上、支付回调进不来。我梳理下自己在部署苍穹外卖到Linux时反复用到的几个步骤和注意点。5.1 Maven打包与配置分离打包命令上我建议先跳过测试测试环境没有数据库配置跑测试必挂mvn clean package -DskipTests打包完成后生成两个jar包分别是sky-server-1.0-SNAPSHOT.jar和sky-take-out-1.0-SNAPSHOT.jar之类的服务端和公共模块。只需要部署server这个jar。配置分离上强烈建议把application.yml里的配置改成外部配置。Spring Boot启动时外部配置文件优先级高于jar包内部。部署时把配置文件放到/opt/sky/conf/目录启动命令指定java -jar /opt/sky/sky-server.jar --spring.config.location/opt/sky/conf/application.yml这样以后改数据库密码、改微信配置不用重新打包。5.2 MySQL与Redis部署检查部署前MySQL和Redis必须检查的事包括MySQL字符集必须是utf8mb4因为菜品名称可能包含表情符号。项目里用到的表结构要提前执行SQL脚本别等启动后才发现表不存在。Redis密码要设置同时确认application.yml里的密码和实际一致。防火墙要放行3306和6379端口但不要对公网开放。数据库连接池的地址用127.0.0.1还是公网IP要看你MySQL和项目是否在同一台机器。我建议小项目把MySQL、Redis、应用都放同一台服务器用内网IP连接最省心。如果要分开部署那就在安全组规则里限定来源IP别直接把3306大盘口暴露出去。5.3 Nginx配置前端静态资源前端页面一般是静态文件用Nginx托管。一个典型的配置是server { listen 80; server_name yourdomain.com; # 前端静态资源 location / { root /opt/sky/frontend; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有个细节微信支付回调的URL要能被公网访问到。如果你是服务器上直接用Nginx反代那需要配置SSL证书因为微信支付强制要求HTTPS回调。本地联调时可以用内网穿透工具但走线上流程就用正规HTTPS域名。5.4 systemd守护进程配置在Linux上部署Spring Boot jar直接java -jar启动的问题是关掉终端进程就没了。正确做法是用systemd来管理。创建/etc/systemd/system/sky-server.service[Unit] DescriptionSky Take-out Server Afternetwork.target [Service] Typesimple Userroot WorkingDirectory/opt/sky ExecStart/usr/bin/java -jar /opt/sky/sky-server.jar --spring.config.location/opt/sky/conf/application.yml Restartalways RestartSec10 [Install] WantedBymulti-user.target然后执行systemctl daemon-reload systemctl enable sky-server systemctl start sky-server用systemd的好处是进程挂了自动重启服务器重启后服务会自动拉起还能用journalctl -u sky-server -f实时看日志。我部署项目从不用nohup java -jar那种方式维护起来太难受了。日志这块建议在application.yml里配置滚动日志文件别让日志全打在nohup.out里。配置一个logback-spring.xml按天滚动保留30天排查问题时直接查日志文件比翻控制台快得多。5.5 部署后的自检清单部署完成后我习惯按这个顺序自检服务是否启动成功curl http://127.0.0.1:8080/api/user/login能返回JSON说明服务活着。数据库是否连通登录接口走一遍能查到用户就是DB没问题。Redis是否连通看看购物车能不能正常添加删除。前端页面能否加载浏览器打开域名看页面是否正常。支付回调能否到达看日志里有没有微信回调相关的请求记录或者先下一个小额测试单走通支付。自检顺序不要乱从底层往上层排查哪里断了就处理哪里。如果你部署后接口返回“连接超时”“Connection refused”先查进程再查端口再查防火墙90%的部署问题都能在这三步里找到答案。6. 常见问题与排查技巧实录这章写我在实际开发、调试、部署苍穹外卖下单支付功能时反复遇到的一些问题和解决路径做成速查表给大家参考。6.1 高频问题速查表问题现象可能原因排查/解决方式下单成功但购物车没清空清空逻辑在事务中执行失败或购物车表有外键关联看事务是否回滚清空按userId执行回调收到但订单还是待付款回调里没做幂等/没更新对字段打开回调日志看返回报文确认状态字段更新为2微信支付提示“签名错误”证书路径不对、私钥格式错误、请求时间戳偏差大检查证书路径是否可读用官方SDK重新生成签名支付回调请求403/404Nginx没把微信回调路径正确代理核对location匹配规则放开对/api/user/order/notify的代理定时关单不生效没加EnableScheduling或多实例重复执行检查启动类注解或加Redis锁下单并发产生重复订单前端重复提交没做幂等后端用Redis锁或订单号唯一索引兜底服务启动报数据库连接失败数据库IP/密码配置错或MySQL没启动用mysql -h主机 -u用户 -p手动测试连接前端页面加载但接口404Vue路由history模式导致刷新404Nginx里写try_files规则6.2 订单状态异常排查思路如果发现订单卡在“待付款”用户却已经扣款了按下面顺序排查第一步查数据库订单表的pay_status和status。如果status已经是2但页面还是待付款那是前端轮询逻辑的问题。第二步查支付回调日志。看微信有没有发回调回调验签是否通过。微信商户平台有“交易账单”和“回调日志”可以查不知道回调是否进来时先看商户平台的数据。第三步查应用日志看有没有异常堆栈。如果回调进来了但抛异常了重点看是反序列化问题还是业务校验问题。逻辑上始终记住一句话以支付平台的交易状态为准以服务端落库的结果为准。前端显示只是表象改前端显示的判断条件很容易但要先确认后端数据对不对。6.3 我的几条避坑经验第一支付金额一定要以服务端计算的为准。前端传多少钱就收多少钱这是大忌。下单接口从购物车算总额回调校验付款金额和订单一致这两个地方都要做校验。第二回调日志必须完整记录。请求头、响应体、验签结果、解密后的明文全部打到日志里。别看日志占空间排查问题的时候它能省你几个小时。我处理线上支付问题第一件事永远是翻回调日志。第三下单和支付不要揉在一起。有些同学为了让“流程简单”一个接口里既创建订单又调起支付结果支付失败时订单已经创建了还要处理回滚或补偿。分离出来各自维护自己的状态调起来才好定位。第四不要乱改状态枚举。项目上线后状态枚举的数值不要变动不然历史数据全乱。如果实在要加状态在末尾追加不要重新编号。结尾一点个人体会苍穹外卖这个项目做到下单支付闭环才算真正摸到了交易系统的边。我在做这个模块时最大的感受是代码倒是其次最关键是脑子里的状态机和数据流要清晰。每一笔订单从哪里来、经过哪些状态、在哪个节点和微信支付对齐这些想明白了编码只是自然而然的事。我自己踩得最深的一个坑是本地联调支付回调时用了HTTP的测试地址结果微信的HTTPS回调一直进不来。后来统一用服务器地址做联调再配合商户平台的回调日志排查才把整个链路理顺。所以做支付功能时环境准备一定要提前确认。建议你照着上面的顺序走先设计好表结构再写下单事务再接微信支付回调最后部署上线。每一步验证通过了再往下走这样真正交付的时候出问题的概率会小很多。