外卖1

适用对象:已了解 Java 方法调用和基本 HTTP 请求,希望对照时序图理解微信小程序支付的学习者。整理日期:2026年10月25日。

阅读目标:区分商户下单、预支付标识、后端二次签名、客户端调起收银台及服务端通知。本报告基于用户提供的代码进行静态分析,保留讨论中的澄清;未执行真实支付或验证线上配置。源码节选、解释性改写和补充示例分别标明。

核心结论

图中步骤按系统之间的交互划分,不是一个步骤对应一个 Java 方法。post() 发送第5步的下单请求,也接收第6步的微信响应。PayNotifyController 主要接收第13步通知,并调用业务层完成第14步。取得 prepay_id 或返回支付参数,都不代表用户已经付款。

原始时序图与代码定位

图片1.png

图中步骤 交互方向与任务 源码定位
4:申请微信支付 小程序向商户后端申请支付参数 OrderController.payment();PUT /user/order/payment
5:调用微信下单接口 商户后端向微信创建预支付交易单 pay() → jsapi() → post() → execute()
6:返回预支付交易标识 微信返回 prepay_id,商户读取响应 post() 接收响应,pay() 解析
7:将组合数据再次签名 商户后端生成调起支付签名 pay() 构造签名串并计算 paySign
8:返回支付参数 商户后端把五项参数交给小程序 JSONObject → OrderPaymentVO → Result.success()
9:用户确认支付 用户表达支付意愿、小程序执行交互 前端按钮与事件处理,不是后端独立接口
10:调起微信支付 小程序调起微信收银台 wx.requestPayment() 或 uni-app 对应封装
11~12:返回并显示支付结果 微信客户端向小程序回调、前端展示 requestPayment 的 success / fail 回调
13:推送支付结果 微信后台向商户发送通知 PayNotifyController.paySuccessNotify()
14:更新订单状态 商户业务层更新数据库 OrderServiceImpl.paySuccess() → orderMapper.update()

第11步客户端结果与第13步服务端通知是两条不同路径,不能假定服务端通知一定在前端显示结果后才发生。前端回调用于界面反馈,订单支付状态应以后端通知或查单结果为准。

规则依据:微信支付:小程序调起支付及回调说明。

支付入口:从第4步进入第5步

OrderController 类级路径为 /user/order,方法级路径为 /payment,HTTP 方法为 PUT,完整接口为 PUT /user/order/payment。@RequestBody 将 JSON 请求体反序列化为 OrdersPaymentDTO。DTO 包含 orderNumber 和 payMethod;当前业务方法未按 payMethod 分支处理支付方式。

源码节选:控制器将请求交给业务层

1
2
3
4
5
6
@PutMapping("/payment")
public Result<OrderPaymentVO> payment(
@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
return Result.success(orderPaymentVO);
}

源码节选:业务层准备下单参数

1
2
3
4
5
6
7
8
9
Long userId = BaseContext.getCurrentId();
User user = userMapper.getById(userId);

JSONObject jsonObject = weChatPayUtil.pay(
ordersPaymentDTO.getOrderNumber(),
new BigDecimal(0.01),
"苍穹外卖订单",
user.getOpenid()
);

BaseContext 取得当前登录用户在本系统中的 ID,userMapper 查询其用户记录,取出微信 openid。userId 是本系统标识;openid 是用户在对应小程序 AppID 下的微信标识。传给工具类的四项分别是商户订单号、元为单位的金额、商品描述与付款人 openid。

当前源码将金额写死为 0.01 元,没有读取订单实际金额。金额常量宜用 new BigDecimal(“0.01”) 避免从 double 构造的精度问题;真实业务应由后端根据订单确定金额。

第5步:组装参数并调用微信下单接口

pay() 先调用本地 jsapi();jsapi() 构造 JSON,再调用本地 post();真正跨网络发送请求发生在 HTTP 客户端 execute()。创建 JSONObject、HttpPost 本身不会发送请求。

源码节选:jsapi() 构造请求体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
JSONObject jsonObject = new JSONObject();
jsonObject.put("appid", weChatProperties.getAppid());
jsonObject.put("mchid", weChatProperties.getMchid());
jsonObject.put("description", description);
jsonObject.put("out_trade_no", orderNum);
jsonObject.put("notify_url", weChatProperties.getNotifyUrl());

JSONObject amount = new JSONObject();
amount.put("total", total.multiply(new BigDecimal(100))
.setScale(2, BigDecimal.ROUND_HALF_UP).intValue());
amount.put("currency", "CNY");
jsonObject.put("amount", amount);

JSONObject payer = new JSONObject();
payer.put("openid", openid);
jsonObject.put("payer", payer);

String body = jsonObject.toJSONString();
return post(JSAPI, body);
字段 来源 作用
appid WeChatProperties 发起交易的小程序标识
mchid WeChatProperties 收款商户号
description 业务层实参 本例为“苍穹外卖订单”
out_trade_no ordersPaymentDTO.orderNumber 关联商户业务订单
notify_url WeChatProperties 第13步微信发送支付通知的商户地址,不是小程序跳转页面
amount.total total 元乘以100并转整数 以分为单位的整数金额
amount.currency 固定 CNY 人民币
payer.openid 查询到的用户记录 指明对应小程序下的付款人

金额示例:0.01 元 × 100 = 1 分;12.50 元 × 100 = 1250 分。setScale(2, ROUND_HALF_UP) 先保留两位小数,intValue() 再转整数;intValue() 本身会截掉小数部分。规范的两位小数人民币金额乘以100后已经是整数。

请求体结构示例,非真实交易数据

1
2
3
4
5
6
7
8
9
{
"appid": "小程序AppID",
"mchid": "商户号",
"description": "苍穹外卖订单",
"out_trade_no": "商户订单号",
"notify_url": "商户支付通知接口地址",
"amount": { "total": 1, "currency": "CNY" },
"payer": { "openid": "当前用户的openid" }
}

JSAPI 常量对应 https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi,使用 POST。该接口创建预支付交易单并返回调起支付所需的标识。

接口依据:微信支付:JSAPI/小程序下单。

源码核心调用节选:请求头配置未全部列出

1
2
3
4
5
CloseableHttpClient httpClient = getClient();
HttpPost httpPost = new HttpPost(url);
// 原方法还设置了请求头。
httpPost.setEntity(new StringEntity(body, "UTF-8"));
CloseableHttpResponse response = httpClient.execute(httpPost);
  • getClient():创建配置好签名、验签能力的 HTTP 客户端。
  • new HttpPost(url):构造目标 POST 请求。
  • Accept: application/json:希望接收 JSON;Content-Type: application/json:声明请求体是 JSON。
  • new StringEntity(body, “UTF-8”):用 UTF-8 将 JSON 字符串放入请求体。
  • execute(httpPost):发送下单请求,并等待取得微信 HTTP 响应。

getClient() 使用 withMerchant(mchid, mchSerialNo, merchantPrivateKey) 配置商户号、商户证书序列号及商户私钥;使用 withWechatPay(wechatPayCertificates) 配置微信支付平台证书。源码所用的 WechatPayHttpClientBuilder 负责请求签名和响应验签,因此 post() 没有手写第一次签名算法。

第6步:微信返回 prepay_id,商户读取响应

成功响应示意

1
2
3
{
"prepay_id": "wx示例预支付交易标识"
}

第6步的返回方是微信后台,接收方是商户系统。post() 不生成 prepay_id,它负责接收并读取微信的响应。响应中的 prepay_id 表示可以用于调起支付的预支付交易标识,不能作为已付款凭证。

源码节选:读取响应并在本地返回

1
2
String bodyAsString = EntityUtils.toString(response.getEntity());
return bodyAsString;

response.getEntity() 取得 HTTP 响应体,EntityUtils.toString() 将其读取为字符串。随后沿 post() → jsapi() → pay() 的本地调用链返回。

源码节选:pay() 解析预支付标识

1
2
3
String bodyAsString = jsapi(orderNum, total, description, openid);
JSONObject jsonObject = JSON.parseObject(bodyAsString);
String prepayId = jsonObject.getString("prepay_id");

JSON.parseObject() 把 JSON 字符串转换成对象;getString(“prepay_id”) 按字段取出标识。若未取得该字段,工具类不会执行二次签名,而会返回解析后的原始响应对象。

返回类型 方向 含义
HTTP 响应 微信后台 → 商户系统 图中的第6步
Java 方法 return post() → jsapi() → pay() 商户内部传递响应数据,不是新的跨系统交互

发送与接收的对应关系

1
2
3
4
5
6
7
jsapi() 调用 post()
→ post() 发送 HTTP 请求:第5步
→ 微信处理下单
→ 微信返回包含 prepay_id 的响应:第6步
→ post() 读取响应体并返回给 jsapi()
→ jsapi() 返回给 pay()
→ pay() 解析标识,开始第7步签名

因此,“jsapi() 调用 post() 是第5步”和“post() 接收第6步响应”可以同时成立。execute() 跨越发送和等待过程,方法边界不等于时序图步骤边界。

编号 产生方 用途
out_trade_no 商户系统 关联自己的业务订单
prepay_id 微信后台 供客户端调起支付,取得它不代表付款完成
transaction_id 微信后台 标识微信支付交易,在后续支付结果等数据中取得

第7步:商户后端为调起支付生成第二次签名

第一次签名用于商户后端调用微信下单接口,由 SDK 处理;第二次签名用于小程序调起支付,由 pay() 计算。二次签名不是把第一次签名结果再签一遍。

源码节选:生成时间戳及随机字符串

1
2
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonceStr = RandomStringUtils.randomNumeric(32);

currentTimeMillis() 是毫秒级 Unix 时间,除以1000转成秒,再转为字符串;randomNumeric(32) 生成32位数字字符组成的随机字符串,用于增加请求随机性。

源码节选:构造四行待签名数据

1
2
3
4
5
6
7
8
9
10
11
ArrayList<Object> list = new ArrayList<>();
list.add(weChatProperties.getAppid());
list.add(timeStamp);
list.add(nonceStr);
list.add("prepay_id=" + prepayId);

StringBuilder stringBuilder = new StringBuilder();
for (Object o : list) {
stringBuilder.append(o).append("\n");
}
String signMessage = stringBuilder.toString();

四项按小程序 AppID、时间戳、随机字符串、package 的值排列。第四项必须是 prepay_id=xxx,不能只使用 xxx。每项末尾都有换行符,最后一项也不例外。

解释性改写:签名串的等价结构

1
2
3
4
String signMessage = appid + "\n"
+ timeStamp + "\n"
+ nonceStr + "\n"
+ "prepay_id=" + prepayId + "\n";

规则依据:微信支付:小程序调起支付签名。

源码节选:签名与Base64编码

1
2
3
4
5
6
7
8
byte[] message = signMessage.getBytes();
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(PemUtil.loadPrivateKey(
new FileInputStream(new File(
weChatProperties.getPrivateKeyFilePath()))));
signature.update(message);
String packageSign =
Base64.getEncoder().encodeToString(signature.sign());
  • getBytes():把签名串转成字节。源码使用默认字符集,可明确写为 getBytes(StandardCharsets.UTF_8)。
  • Signature.getInstance(“SHA256withRSA”):选择签名算法。
  • PemUtil.loadPrivateKey():读取商户 API 私钥;initSign():初始化签名。这里不是使用 API v3 对称密钥。
  • update(message):输入待签名数据;sign():计算签名字节。
  • Base64 编码:把签名字节转成便于 JSON 传输的字符串,作为 paySign。Base64 不是加密,数字签名也不是把支付参数加密成密文。

源码节选:生成五项前端参数

1
2
3
4
5
6
7
JSONObject jo = new JSONObject();
jo.put("timeStamp", timeStamp);
jo.put("nonceStr", nonceStr);
jo.put("package", "prepay_id=" + prepayId);
jo.put("signType", "RSA");
jo.put("paySign", packageSign);
return jo;

appid 参与签名,但原生 wx.requestPayment() 不需要单独传入 appId;调用运行在对应的小程序环境中。签名时的小程序 AppID 必须与下单及实际调起支付的小程序一致。前端应使用后端返回的原始时间戳、随机字符串及 package,不能重新生成。

第8步:转换为 VO 并返回小程序

源码节选:检查特定错误并封装VO

1
2
3
4
5
6
7
if (jsonObject.getString("code") != null
&& jsonObject.getString("code").equals("ORDERPAID")) {
throw new OrderBusinessException("该订单已支付");
}
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));
return vo;

成功时,工具类返回的 jsonObject 已包含五项调起支付参数,而不是最初仅包含 prepay_id 的微信响应。失败时可能返回微信错误对象;当前业务层只专门处理 ORDERPAID,没有覆盖全部下单失败情况。

toJavaObject() 按匹配字段转为 OrderPaymentVO。四个同名字段直接映射;package 与 packageStr 名字不同,需要手动 setPackageStr()。Java 的 package 是关键字,不能直接用作普通字段名。

工具类JSON 接口VO 微信前端API
timeStamp timeStamp timeStamp
nonceStr nonceStr nonceStr
package packageStr package
signType signType signType,本例RSA
paySign paySign paySign

控制器返回 Result.success(orderPaymentVO),Spring 将返回对象序列化成 JSON 发给小程序。此时只是支付参数准备完成,还没有因此发生扣款。

第9~10步:用户交互与客户端调起支付

第9步是用户和界面的交互,不是后端独立方法。在商户页面点击“去支付”表示支付意愿;在微信收银台输入密码或使用指纹确认付款,发生在第10步调起收银台之后。

压缩包 project-rjwm-weixin-uniapp/pages/order/index.vue 第97行附近,将“去支付”按钮绑定到 payOrderHandle()。对应 index.js 第133行附近,当前启用代码调用 submitOrderSubmit(params),成功后直接跳转 /pages/order/success。api.js 中 submitOrderSubmit() 对应 POST /order/submit。

压缩包当前启用逻辑节选

1
2
3
4
5
6
7
submitOrderSubmit(params).then(res => {
if (res.code === 1) {
uni.redirectTo({
url: '/pages/order/success'
})
}
})

这条启用链路是提交业务订单后跳转“下单成功”页面,没有在此调用后端 payment 接口或拉起微信收银台。按钮文字“去支付”和页面跳转本身不能证明支付已经接通。

同一 index.js 第161~205行附近保留了一段被注释的旧支付逻辑:uni.requestPayment() 写死 signType: ‘MD5’,读取 params.package,并通过旧 payOrder 接口取得数据。它与当前后端的 RSA、VO 字段 packageStr、PUT /user/order/payment 不匹配,不能直接解除注释后视为接通。

补充示例:与当前后端匹配的原生小程序调用,非现有启用源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// payParams 是 PUT /user/order/payment 响应中的业务数据。
// 提取业务数据的层级取决于项目的请求封装。
wx.requestPayment({
timeStamp: payParams.timeStamp,
nonceStr: payParams.nonceStr,
package: payParams.packageStr,
signType: payParams.signType,
paySign: payParams.paySign,
success(res) {
console.log('客户端返回支付成功', res);
// 按业务需要向后端确认订单状态后展示。
},
fail(err) {
console.log('支付取消或失败', err);
},
complete() {
// 成功或失败都会执行,可用于结束加载状态。
}
});

左侧 package 是微信 API 的固定参数名,右侧 packageStr 是后端 VO 字段。signType 应使用后端返回的 RSA。uni-app 使用 uni.requestPayment() 对应封装时,核心支付参数也要按上述关系映射。

调用支付 API 后,微信客户端调起收银台,用户完成支付或退出后触发相应回调。fail 不只表示取消,也可能是签名或参数错误。success 回调主要用于前端反馈,不应直接替代后端订单支付状态确认。

接口依据:微信支付:小程序调起支付。

第13~14步:PayNotifyController 接收通知并更新订单

PayNotifyController 主要接收图中第13步。类上的 /notify 与方法上的 /paySuccess 拼成 /notify/paySuccess,由微信后台调用。第5步的 notify_url 应指向该接口的实际可访问地址;本次未核验具体配置值和网络可达性。

源码节选:接收支付通知

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("/paySuccess")
public void paySuccessNotify(HttpServletRequest request,
HttpServletResponse response) throws Exception {
String body = readData(request);
String plainText = decryptData(body);

JSONObject jsonObject = JSON.parseObject(plainText);
String outTradeNo = jsonObject.getString("out_trade_no");
String transactionId = jsonObject.getString("transaction_id");

orderService.paySuccess(outTradeNo);
responseToWeixin(response);
}
  • readData(request):通过 request.getReader() 逐行读取微信通知的请求体。
  • decryptData(body):从 resource 中取出 ciphertext、nonce、associated_data,使用配置中的 API v3 密钥和 AesUtil 解密业务数据。
  • 解析明文 JSON:取得商户订单号 out_trade_no 和微信支付交易号 transaction_id。当前代码记录 transactionId,没有将其传入业务层持久化。
  • orderService.paySuccess(outTradeNo):进入第14步更新业务订单。
  • responseToWeixin(response):返回 HTTP 200 及 SUCCESS JSON,确认本次通知已处理。这是对第13步通知请求的响应,不是第11步给小程序的支付结果。

源码节选:第14步更新订单

1
2
3
4
5
6
7
8
9
10
public void paySuccess(String outTradeNo) {
Orders ordersDB = orderMapper.getByNumber(outTradeNo);
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
}

getByNumber() 按商户订单号查询记录,然后按数据库主键更新:订单状态为待接单、支付状态为已支付、结账时间为当前时间。当前代码没有在这里设置支付方式,虽然方法注释提及支付方式;应以实际赋值语句为准。

所提供 Controller 展示了读取、解密和状态更新,没有在该类内展示通知签名验证、交易状态和金额核对、重复通知幂等控制。是否由其他层实现,本次资料无法确认;本文不将教学片段视为已经完整验证的生产支付实现。

已确认的实现差异与阅读边界

观察 源码证据 影响
金额固定为一分钱 payment() 中 new BigDecimal(0.01) 不代表已经按实际订单金额收款
旧前端支付调用被注释 压缩包 pages/order/index.js 当前启用链路只提交订单并跳转
签名类型不匹配 旧前端MD5,后端RSA 需要按当前后端参数对接
预支付字段名不同 JSON package,VO packageStr 前端映射为 package: payParams.packageStr
错误处理有限 只专门检查ORDERPAID 其他失败响应不能当作成功参数
客户端与后台结果分离 requestPayment 与通知 Controller 是不同入口 成功页面不是数据库已支付状态的替代证据

以上为静态观察,没有修改业务代码,没有执行登录、下单、扣款或退款。示例值不是真实交易数据,报告不包含私钥、API 密钥或访问令牌。

资料范围与来源

  • 后端教学文件:F:/sky-take-out/资料/day08/微信支付功能代码/ 下的 OrderController.java、OrderServiceImpl.java、OrderService.java、OrderMapper.java、OrderMapper.xml、PayNotifyController.java。
  • 用户补充工具类:D:/idea21workplace/sky-take-out/sky-take-out/sky-common/src/main/java/com/sky/utils/WeChatPayUtil.java。
  • DTO / VO:D:/idea21workplace/sky-take-out/sky-take-out/sky-pojo/src/main/java/com/sky/ 下的 dto/OrdersPaymentDTO.java 与 vo/OrderPaymentVO.java。
  • 小程序压缩包:F:/sky-take-out/微信小程序源代码/mp-weixin.zip。核对其 pages/order/index.js、pages/order/index.vue、pages/api/api.js、pages/order/success.vue 及 utils/request.js。
  • 原始时序图:用户提供的 C:/Users/wzh/Desktop/图片1.png,已嵌入本文。

规则参考:JSAPI/小程序下单;小程序调起支付签名;小程序调起支付与回调。

资料来自不同目录和前端压缩包,不能自动视为同一版本的完整工程。本报告按实际可见代码描述,并明确列出不匹配处。代码行号可能随学习注释和后续编辑变化,定位时优先按类名、方法名和接口路径。