先讲「商户系统 + 支付渠道」协作的通用思路,再结合苍穹外卖项目代码。
零基础入门
如果你刚接触支付对接,先记住下面 4 句话:
- 预支付(统一下单)是什么:你的服务端向微信申请一笔「待用户确认支付」的订单,拿到
prepay_id等参数,交给小程序/APP 调起收银台。 - 支付回调是什么:用户付完款后,微信服务器异步 POST 到你的
notify_url,通知支付结果;你的系统据此改订单状态。 - 商户订单号(out_trade_no)是什么:你自己业务库里的订单号,贯穿「下单 → 预支付 → 回调」,用来把渠道通知和本地订单对上。
- 为什么强调幂等与验签:网络会重试,回调可能来多次;通知必须能验真,业务更新必须重复执行结果不变。
先看一个最小例子(不依赖本项目)
// 1)用户端:拿到调起支付所需的参数(由服务端向渠道下单后返回)
OrderPaymentVO params = orderService.payment(dto);
// 2)渠道:支付成功后 POST 到你的回调地址
// 3)回调里:解析订单号 → 查单 → 若未支付则改为已支付
void onPaySuccess(String outTradeNo) {
// update order set pay_status = paid where number = ? and ...
}
预支付负责「拉起收银台」,回调负责「把钱的结果写回你自己的订单」。
一、对接支付的简洁实现思路
目标:把「渠道交互」和「业务状态」拆开
我们不希望 Controller 里堆满签名、HTTP 客户端细节,而是:
- 工具层:封装调用微信 V3 接口(下单、签名、证书)。
- 业务层:决定金额口径、订单号、何时视为已支付、如何更新状态与副作用(如来单提醒)。
- 回调层:读 body、解密(若使用加密资源)、取
out_trade_no、调业务、按协议应答微信。
sequenceDiagram
title 运行流程:从预下单到支付成功
participant App as 小程序
participant API as 商户服务端
participant WX as 微信支付
participant DB as 数据库
App->>API: PUT /user/order/payment(订单号)
API->>WX: JSAPI 统一下单
WX-->>API: prepay_id 等
API-->>App: 调起支付参数(timeStamp、paySign 等)
App->>WX: 用户确认支付
WX->>API: POST /notify/paySuccess(加密通知)
API->>API: 解密、取 out_trade_no
API->>DB: 更新订单为已支付、待接单
API-->>WX: 200 + SUCCESS
最小代码骨架
public OrderPaymentVO payment(OrdersPaymentDTO dto) {
JSONObject wx = weChatPayUtil.pay(dto.getOrderNumber(), amount, desc, openid);
// 已支付等业务分支处理
return mapToPaymentVO(wx);
}
public void paySuccess(String outTradeNo) {
Orders order = orderMapper.getByNumber(...);
if (alreadyPaid(order)) return; // 生产强烈建议幂等
orderMapper.update(paidState(order));
}
核心思想:支付是外部系统,不可靠、可重复;本地订单状态机要扛住重复通知与超时。
二、再看苍穹外卖项目是怎么实现的
先看业务场景
| 场景 | 谁发起 | 系统做什么 |
|---|---|---|
| 用户点击支付 | 用户端接口 | 向微信下单,返回小程序调起参数 |
| 微信通知支付成功 | 微信服务器 | 解密回调体,取商户订单号,更新订单并推送来单提醒 |
| 订单已支付仍发起支付 | 用户端接口 | 微信返回 ORDERPAID,抛业务异常提示 |
用户端支付接口(Controller)
@PutMapping("/payment")
@ApiOperation("订单支付")
public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
log.info("订单支付:{}", ordersPaymentDTO);
OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
log.info("生成预支付交易单:{}", orderPaymentVO);
return Result.success(orderPaymentVO);
}
预支付与重复支付分支(Service)
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
User user = userMapper.getById(userId);
//调用微信支付接口,生成预支付交易单
JSONObject jsonObject = weChatPayUtil.pay(
ordersPaymentDTO.getOrderNumber(), //商户订单号
new BigDecimal(0.01), //支付金额,单位 元
"苍穹外卖订单", //商品描述
user.getOpenid() //微信用户的openid
);
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;
}
执行顺序可以概括为:
- 从
BaseContext取当前用户,查openid。 - 调用
WeChatPayUtil.pay,用商户订单号向微信下单并组装二次签名参数。 - 若返回
ORDERPAID,说明已支付,抛OrderBusinessException走全局异常统一返回。 - 否则把 JSON 映射为
OrderPaymentVO返回前端。
支付成功回写与来单提醒(Service)
public void paySuccess(String outTradeNo) {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
// 根据订单号查询当前用户的订单
Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);
// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
//通过websocket向客户端浏览器推送消息 type orderId content
Map map = new HashMap();
map.put("type",1); // 1表示来单提醒 2表示客户催单
map.put("orderId",ordersDB.getId());
map.put("content","订单号:" + outTradeNo);
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
}
支付回调:解密与转调业务(PayNotifyController)
@RequestMapping("/paySuccess")
public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
//读取数据
String body = readData(request);
log.info("支付成功回调:{}", body);
//数据解密
String plainText = decryptData(body);
log.info("解密后的文本:{}", plainText);
JSONObject jsonObject = JSON.parseObject(plainText);
String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
String transactionId = jsonObject.getString("transaction_id");//微信支付交易号
log.info("商户平台订单号:{}", outTradeNo);
log.info("微信支付交易号:{}", transactionId);
//业务处理,修改订单状态、来单提醒
orderService.paySuccess(outTradeNo);
//给微信响应
responseToWeixin(response);
}
解密使用微信提供的 AesUtil 与商户 apiV3Key:
private String decryptData(String body) throws Exception {
JSONObject resultObject = JSON.parseObject(body);
JSONObject resource = resultObject.getJSONObject("resource");
String ciphertext = resource.getString("ciphertext");
String nonce = resource.getString("nonce");
String associatedData = resource.getString("associated_data");
AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
//密文解密
String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
nonce.getBytes(StandardCharsets.UTF_8),
ciphertext);
return plainText;
}
微信侧下单与调起参数(WeChatPayUtil)
统一下单请求体要点:out_trade_no、notify_url、amount(分)、payer.openid。
private String jsapi(String orderNum, BigDecimal total, String description, String openid) throws Exception {
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);
}
拿到 prepay_id 后做二次签名,返回小程序调起支付所需字段:
public JSONObject pay(String orderNum, BigDecimal total, String description, String openid) throws Exception {
//统一下单,生成预支付交易单
String bodyAsString = jsapi(orderNum, total, description, openid);
//解析返回结果
JSONObject jsonObject = JSON.parseObject(bodyAsString);
System.out.println(jsonObject);
String prepayId = jsonObject.getString("prepay_id");
if (prepayId != null) {
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonceStr = RandomStringUtils.randomNumeric(32);
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();
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());
//构造数据给微信小程序,用于调起微信支付
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;
}
return jsonObject;
}
三、预支付接口与支付回调的区别(容易混淆)
| 类型 | 典型路径 | 是否带用户 JWT | 关注点 |
|---|---|---|---|
| 预支付 | /user/order/payment | 通常有(用户端拦截器) | 金额、订单号、openid、返回调起参数 |
| 支付回调 | /notify/paySuccess | 微信服务器调用,无用户登录态 | 解密、验签(生产需按官方指引补全)、幂等更新、正确应答 |
当前项目里 paySuccess 使用 BaseContext.getCurrentId() 与 getByNumberAndUserId 联查订单。回调入口不在 /user/ 下,ThreadLocal 中往往没有当前用户;这是教学代码与生产实践之间的典型差异点。生产上更稳妥的做法是:仅用 out_trade_no(或再加渠道侧约束)定位订单,并在更新前判断「若已支付则直接返回」,避免重复回调重复推消息。
四、注意事项与可选增强
- 密钥与证书:私钥路径、
apiV3Key、平台证书勿提交仓库,用环境变量或配置中心。 - 幂等:
paySuccess入口建议「已是已支付则 return」,避免 WebSocket 重复推送。 - 金额:示例里固定
0.01元便于联调;上线应使用订单实付金额并校验与微信通知一致。 - 回调安全:除解密外,应按微信支付 V3 要求完成签名验证与重放防护。
- 补偿:回调丢失时可定时「按商户订单号查单」对齐状态,与定时关单任务配合。
总结
支付链路可以记三句话:预下单把收银台拉起来,回调把支付结果写进订单,工具类把微信协议细节藏起来。
苍穹外卖里,OrderController.payment → WeChatPayUtil.pay 完成预支付;PayNotifyController 解密后调用 OrderServiceImpl.paySuccess 推进状态并推送来单提醒。
附录:相关源码路径
sky-server/src/main/java/com/sky/controller/user/OrderController.java
sky-server/src/main/java/com/sky/controller/nofity/PayNotifyController.java
sky-server/src/main/java/com/sky/service/impl/OrderServiceImpl.java
sky-common/src/main/java/com/sky/utils/WeChatPayUtil.java
sky-common/src/main/java/com/sky/properties/WeChatProperties.java
参考
苍穹外卖www.bilibili.com/video/BV1TP…
deekseek-v4