UniApp 小程序 + SpringBoot 微信支付完整全流程
·
文章标题
UniApp 前端调起微信支付 + SpringBoot 后端统一下单 + 支付回调 MQTT 设备推送完整实现(自助饺子售货机分表库存、分润业务)
文章标签
#UniApp #SpringBoot #微信支付 #小程序支付 #自助售货机 #MQTT 设备推送 #分表库存 #微信支付回调
文章目录
- 业务整体流程介绍
- UniApp 前端支付请求完整代码
- SpringBoot 后端统一下单接口 wx_pay_unifiedOrder
- 微信支付异步通知回调接口 wx_pay_notify(核心业务处理)
- 完整业务逻辑拆解 5.1 前端交互逻辑 5.2 后端下单校验、分表创建、库存判断、生成支付参数 5.3 支付回调:订单状态更新、库存出库、分润计算、MQTT 推送设备
- 现有代码存在的缺陷与风险
- 生产环境优化改造方案
一、业务整体流程介绍
本项目为校园 / 社区无人值守 AI 饺子自助售卖机完整支付链路,整体流程:
- 用户小程序选饺子商品,点击付款,前端携带设备码、手机号、用户 openid、商品列表请求后端统一下单接口;
- 后端做多层校验:Redis 登录 Token 校验、设备是否存在、用户手机号是否存在、库存是否充足;
- 自动创建当年分表(订单表、订单明细表、出入库表、分润表),组装微信统一下单参数调用微信支付 SDK;
- 数据库预创建销售订单、订单明细,返回微信支付 5 个核心签名参数给前端;
- UniApp 调用
uni.requestPayment唤起微信原生支付弹窗; - 用户支付完成后,微信服务器异步回调后端
wx_pay_notify接口; - 回调内校验微信签名、更新订单为已付款、扣减库存、自动计算渠道 / 投资人分润、通过 MQTT 下发订单信息给线下饺子机,设备自动煮饺子出餐。
技术栈:UniApp (Vue) + SpringBoot + 微信 JSAPI 支付 + Redis 登录校验 + MQTT 设备通信 + MySQL 年度分表 + JdbcTemplate 批量分润。
二、UniApp 前端支付请求完整代码
javascript
运行
PaymentMethod() {
var that = this
uni.showLoading({
title: '加载中......'
})
uni.request({
url: that.baseLocalUrl + "/API/wx_pay_unifiedOrder",
method: 'POST',
dataType: 'json',
data: {
code: that.code, // 设备编码
phone: that.phone, // 用户手机号
token: that.token, // 登录凭证Redis校验
openid: that.openid, // 小程序用户openid
body: '售卖机付款单', // 微信订单描述
productJsonStr: JSON.stringify(that.list), // 选购商品数组JSON
total_fee:1 // 测试固定金额,生产替换为实际总价
},
headers: {
'Content-Type': "application/json;charset=utf-8"
},
success: result => {
// 登录凭证失效
if (result.data.status == 'token_fail') {
uni.hideLoading()
uni.showToast({ icon: 'none', title: '登录已失效,请重新登录', mask: true, duration: 2000 })
return
}
// 设备不存在
if (result.data.status == 'device_code_fail') {
uni.hideLoading()
uni.showToast({ icon: 'none', title: '该设备不存在', mask: true, duration: 2000 })
return
}
// 用户手机号未注册
if (result.data.status == 'user_phone_fail') {
uni.hideLoading()
uni.showToast({ icon: 'none', title: '用户信息不存在', mask: true, duration: 2000 })
return
}
// 商品库存不足
if (result.data.status == 'stock_fail') {
uni.hideLoading()
uni.showToast({ icon: 'none', title: '商品库存不足', mask: true, duration: 2000 })
return
}
// 后端下单失败
if (result.data.status == 'fail') {
uni.hideLoading()
uni.showToast({ icon: 'none', title: '创建订单失败', mask: true, duration: 2000 })
return
}
// 下单成功,唤起微信支付
uni.requestPayment({
provider: 'wxpay',
timeStamp: result.data.payParams.timeStamp,
nonceStr: result.data.payParams.nonceStr,
package: result.data.payParams.package,
signType: result.data.payParams.signType,
paySign: result.data.payParams.paySign,
// 支付成功跳转订单页
success: function(res) {
uni.hideLoading()
uni.navigateTo({ url: `/pagesOther/mOrder/mOrder?index=1` })
console.log('支付成功回调:' + JSON.stringify(res));
},
// 用户取消/支付失败
fail: function(err) {
uni.hideLoading()
console.log('支付失败:' + JSON.stringify(err));
}
});
},
// 网络请求异常
fail(ex) {
uni.hideLoading()
console.log(ex.message)
uni.showToast({ icon: 'none', title: ex.message, mask: true, duration: 2000 })
}
})
},
前端逻辑说明
- 请求前弹出 Loading 遮罩,防止重复点击;
- 统一接收后端 5 种业务状态码,分别给出对应 Toast 提示;
- 下单成功拿到微信支付五要素,调用小程序原生
uni.requestPayment拉起微信; - 支付成功自动跳转我的订单页面,方便用户查看取餐码;
- 网络异常统一捕获提示,提升自助机用户体验。
三、SpringBoot 后端统一下单接口 wx_pay_unifiedOrder
java
运行
@ResponseBody
@RequestMapping(value = "/API/wx_pay_unifiedOrder", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
public Map<String, Object> wx_pay_unifiedOrder(@RequestBody JSONObject jsonParam) {
Map<String, Object> map = new HashMap<>();
try
{
// 1. 解析前端传参
String code = jsonParam.getString("code");
String openid = jsonParam.getString("openid");
String phone = jsonParam.getString("phone");
String token = jsonParam.getString("token");
JSONArray jsonArray =jsonParam.getJSONArray("productJsonStr");
String body = jsonParam.getString("body");
String total_fee = jsonParam.getString("total_fee");
// 2. Redis登录Token校验,一次性token校验后直接销毁
boolean is_exist = redisTemplate.hasKey(token);
if(is_exist == false)
{
map.put("status","token_fail");
return map;
}else
{
redisTemplate.delete(token);
}
// 3. 初始化年度分表名称,自动检查表并创建
SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
Date date = new Date();
String tableName = "tb_pay_form_" + yearFormat.format(date);
String tableItemName = "tb_pay_form_item_" + yearFormat.format(date);
String outStockTableName = "tb_out_stock_" + yearFormat.format(date);
String outStockItemTableName = "tb_out_stock_item_" + yearFormat.format(date);
String inStockTableName = "tb_in_stock_" + yearFormat.format(date);
String inStockItemTableName = "tb_in_stock_item_" + yearFormat.format(date);
String profitAmountTableName = "tb_profit_amount_" + yearFormat.format(date);
// 循环判断所有业务分表,不存在自动创建
if (payFormNaticeService.tableExists(tableName) == false) {
payFormNaticeService.createPayFormTable(tableName);
}
if (payFormNaticeService.tableExists(tableItemName) == false) {
payFormNaticeService.createPayFormItemTable(tableItemName);
}
if (outStockNaticeService.tableExists(outStockTableName) == false) {
outStockNaticeService.createOutStockTable(outStockTableName);
}
if (outStockNaticeService.tableExists(outStockItemTableName) == false) {
outStockNaticeService.createOutStockItemTable(outStockItemTableName);
}
if (inStockNaticeService.tableExists(inStockTableName) == false) {
inStockNaticeService.createInStockTable(inStockTableName);
}
if (inStockNaticeService.tableExists(inStockItemTableName) == false) {
inStockNaticeService.createInStockItemTable(inStockItemTableName);
}
if (profitAmountNaticeService.tableExists(profitAmountTableName) == false) {
profitAmountNaticeService.createTable(profitAmountTableName);
}
// 4. 校验设备是否存在
List<Device> deviceList = deviceService.findDeviceByCode(code);
if(deviceList.size() <= 0)
{
map.put("status","device_code_fail");
return map;
}
// 5. 校验用户手机号是否存在
List<UserInfo> userInfoList = userService.findByNamePhone(phone);
if(userInfoList.size() <= 0)
{
map.put("status","user_phone_fail");
return map;
}
// 6. 生成唯一商户订单号(年份+UUID,长度24位符合微信规范)
UUID out_trade_no = UUID.randomUUID();
String out_trade_noStr = out_trade_no.toString().replace("-", "");
out_trade_noStr = out_trade_noStr.substring(0, 24);
out_trade_noStr = yearFormat.format(date)+"@"+out_trade_noStr;
// 7. 组装订单主表实体
PayForm bean = new PayForm();
bean.setName("售货机购买");
bean.setAdd_date(new Date());
bean.setId(out_trade_noStr);
bean.setStatus("未取餐");
bean.setWrite_off_status("未取餐");
bean.setPay_status("未付款");
bean.setDevice_id(deviceList.get(0).getId());
bean.setUser_id(userInfoList.get(0).getId());
// 8. 解析商品数组,组装订单明细
List<PayFormItem> payFormItemList = new ArrayList<>();
String pay_form_item_id = UUID.randomUUID().toString().replace("-", "");
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
JsonParser parser = new JsonParser();
JsonObject productJsonObject = parser.parse(jsonObject.get("product").toString()).getAsJsonObject();
PayFormItem payFormItem = new PayFormItem();
payFormItem.setAdd_date(new Date());
payFormItem.setId(yearFormat.format(date)+"@"+pay_form_item_id);
payFormItem.setPay_form_id(out_trade_noStr);
payFormItem.setName("销售单");
payFormItem.setProduct_id(productJsonObject.get("id").toString());
payFormItem.setQuantity(Integer.parseInt(jsonObject.get("buy_number").toString()));
payFormItem.setAmount(Double.parseDouble(productJsonObject.get("price").toString()) *Integer.parseInt(jsonObject.get("buy_number").toString()));
payFormItem.setStatus("启用");
payFormItemList.add(payFormItem);
}
// 9. 调用微信统一下单SDK
Map<String, String> params = new HashMap<>();
params.put("body",body);
params.put("out_trade_no",out_trade_noStr);
params.put("total_fee",total_fee);
params.put("spbill_create_ip","134.175.222.0");
params.put("openid",openid);
Map<String, String> payParams = wxPayService.unifiedOrder(params);
if(payParams == null)
{
map.put("status","fail");
return map;
}
// 10. 插入订单、明细、扣减库存;返回-3代表库存不足
int insertResult =payFormNaticeService.inserPayFormTarget(tableName,tableItemName,profitAmountTableName,bean,payFormItemList,inStockTableName,inStockItemTableName,outStockTableName,outStockItemTableName);
if(insertResult == -3){
map.put("status", "stock_fail");
return map;
}
if (insertResult > 0)
{
map.put("payParams",payParams);
map.put("status","success");
return map;
}
map.put("status", "fail");
return map;
} catch (Exception ex) {
ex.printStackTrace();
}
map.put("status","fail");
return map;
}
后端下单接口核心逻辑
- 登录校验:前端 Token 一次性使用,校验通过直接删除 Redis Key,防止重复下单;
- 分表自动创建:按年份拆分订单、库存、分润表,避免单表数据过大卡顿;
- 三层业务校验:设备合法性、用户手机号合法性、商品库存充足性;
- 商户订单号规范:拼接年份 + UUID,满足微信 24 位以内单号要求;
- 预生成订单:下单成功即插入订单数据,库存同步锁定,防止超卖;
- 对接微信 SDK:传入 openid、金额、商户单号,返回前端唤起支付所需 5 个签名参数。
四、微信支付异步通知回调接口 wx_pay_notify(业务核心)
java
运行
@ResponseBody
@RequestMapping(value = "/API/wx_pay_notify", method = RequestMethod.POST, produces = "application/json;charset=UTF-8")
public String wx_pay_notify(@RequestBody String notifyData) {
String status = "";
try
{
status = wxPayService.notify(notifyData);
// 验签失败直接返回FAIL
if(!"SUCCESS".equals(status)){
return "FAIL";
}
// 解析微信回调XML参数
Map<String, String> notifyMap = WXPayUtil.xmlToMap(notifyData);
if (!WXPayUtil.isSignatureValid(notifyMap, key)) {
return "FAIL";
}
if ("SUCCESS".equals(notifyMap.get("result_code"))) {
String out_trade_no = (String) notifyMap.get("out_trade_no");
String transaction_id = (String) notifyMap.get("transaction_id");
SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");
Date date = new Date();
// 分表名称复用下单接口规则
String tableName = "tb_pay_form_" + yearFormat.format(date);
String tableItemName = "tb_pay_form_item_" + yearFormat.format(date);
String outStockTableName = "tb_out_stock_" + yearFormat.format(date);
String outStockItemTableName = "tb_out_stock_item_" + yearFormat.format(date);
String profitAmountTableName = "tb_profit_amount_" + yearFormat.format(date);
// 检查表,不存在自动创建
if (payFormNaticeService.tableExists(tableName) == false) {
payFormNaticeService.createPayFormTable(tableName);
}
if (payFormNaticeService.tableExists(tableItemName) == false) {
payFormNaticeService.createPayFormItemTable(tableItemName);
}
if (outStockNaticeService.tableExists(outStockTableName) == false) {
outStockNaticeService.createOutStockTable(outStockTableName);
}
if (outStockNaticeService.tableExists(outStockItemTableName) == false) {
outStockNaticeService.createOutStockItemTable(outStockItemTableName);
}
if (profitAmountNaticeService.tableExists(profitAmountTableName) == false) {
profitAmountNaticeService.createTable(profitAmountTableName);
}
// 更新订单状态为已付款,扣减库存
int updateCount = payFormNaticeService.updatePayFormByStatusTarget(tableName,profitAmountTableName,out_trade_no,transaction_id,"未取餐","未取餐","已付款",outStockTableName,outStockItemTableName);
if(updateCount > 0){
// 查询完整订单信息,用于MQTT推送与分润计算
List<PayForm> payFormList = payFormNaticeService.getPayFormByOutTradeNoTarget(tableName,tableItemName,out_trade_no);
PayForm order = payFormList.get(0);
// 1. 计算渠道/投资人分润
String plan_profit_item_sql = "SELECT ppi.*,pp.id as 'plan_profit_id' from tb_plan_profit_item as ppi LEFT JOIN tb_plan_profit as pp ON ppi.plan_profit_id = pp.id LEFT JOIN tb_device as td ON pp.id = td.plan_profit_id where td.id = '"+order.getDevice_id()+"'";
List<Map<String, Object>> profitRuleList = jdbcTemplate.queryForList(plan_profit_item_sql);
StringBuilder batchProfitSql = new StringBuilder();
// 遍历分润规则批量生成插入SQL
for (Map<String, Object> rule : profitRuleList) {
double profitRate = Double.parseDouble(rule.get("profit").toString());
double totalAmount = order.getTotal_amount();
double profitTotal = totalAmount * profitRate;
double wxHandlingFee = profitTotal * 0.006; // 千六微信手续费
double realProfit = profitTotal - wxHandlingFee;
String insertSql = " INSERT INTO tb_total_profit (id,name,org_type_id,device_id,in_amount,out_amount) "
+" VALUES ('"+order.getDevice_id()+"@"+rule.get("org_type_id").toString()+"', '分润', '"+rule.get("org_type_id").toString()+"','"+ order.getDevice_id()+"',"+realProfit+",0) "
+" ON DUPLICATE KEY UPDATE in_amount = in_amount + VALUES(in_amount); ";
batchProfitSql.append(insertSql);
}
// 批量执行分润台账插入
jdbcTemplate.update(batchProfitSql.toString());
// 2. MQTT推送订单至线下AI饺子机,设备收到自动制作饺子
JSONObject mqttMsg = new JSONObject();
mqttMsg.put("id",order.getId());
mqttMsg.put("quantity",order.getTotal_quantity());
mqttMsg.put("amount",order.getTotal_amount());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
mqttMsg.put("add_date",sdf.format(order.getAdd_date()));
String deviceCode = order.getDevice_code();
String mqttTopic = "/NewOrder/" + deviceCode;
Gson gson = new Gson();
String mqttJson = gson.toJson(mqttMsg);
// MQTT断线重连逻辑
if(mqttClientService.isConnectStatus() == false) {
mqttClientService.initMqttClient();
Thread.sleep(50);
}
mqttClientService.publishSafe(order.getId(),mqttTopic, mqttJson);
return "SUCCESS";
}else{
return "FAIL";
}
}
} catch (Exception ex) {
logger.error("支付回调处理异常",ex);
}
return "FAIL";
}
回调接口四大核心业务
- 微信签名校验:防止伪造回调请求,非法请求直接返回
FAIL,微信会持续重试; - 订单状态变更:将订单从「未付款」更新为「已付款」,同步扣减商品库存;
- 自动分润计算:根据设备绑定的分润方案,扣除 0.6% 微信手续费后,批量写入渠道 / 投资人收益台账;
- MQTT 下发订单给饺子机:支付成功推送订单编号、商品数量、金额到设备专属主题,无人值守机器自动启动煮饺流程,生成取餐码供用户扫码取餐。
五、现有代码存在的缺陷与线上风险
5.1 安全漏洞
- SQL 字符串拼接,存在注入风险 分润 SQL、设备查询 SQL 直接拼接
device_id、org_type_id等参数,恶意字符可注入篡改数据; - 微信回调无幂等控制 微信回调会多次重试,同一笔支付多次回调会重复扣库存、重复计算分润、多次推送 MQTT 消息,造成业务错乱;
- 固定微信支付 IP 写死 统一下单
spbill_create_ip硬编码静态 IP,服务器更换公网 IP 后支付会失败。
5.2 稳定性问题
- 大量无意义
Thread.sleep(20/50)阻塞线程,高并发下单、回调时拉长接口响应,容易触发微信超时重试; - 全局超大 try-catch,异常无详细日志、无告警,线上故障无法快速定位;
- MQTT 发布无重试、无消息持久化,设备离线时订单丢失,用户付款不出餐;
- 分表创建串行循环执行,首次启动服务大量表校验阻塞接口。
5.3 并发与数据一致性
- 库存扣减、订单插入无数据库事务,插入订单成功但库存不足时,会生成已付款但无货的脏订单;
- 分润批量 SQL 一次性拼接超长字符串,数据量大时超出数据库 SQL 长度限制,批量插入失败;
- 无分布式锁,多实例部署时回调并发更新同一条订单,引发数据覆盖。
5.4 代码规范缺陷
- 魔法值硬编码:0.006 手续费、分表前缀、30 分钟取餐超时、微信主题常量全部写死;
- 工具耦合严重:下单、库存、分润、MQTT 全部耦合在一个回调方法,职责不单一,难以维护;
- 前端测试金额
total_fee:1硬编码,上线容易忘记修改造成金额异常。
六、生产环境优化改造方案
6.1 安全加固
- 全部 SQL 替换为
JdbcTemplate预编译传参,杜绝字符串拼接; - 回调增加幂等锁:Redis 存入
paid_商户单号,处理前判断存在直接返回 SUCCESS,防止重复回调; - 动态获取服务器公网 IP,移除硬编码
spbill_create_ip。
6.2 事务与并发保障
- 下单、库存扣减、订单插入添加
@Transactional事务,全部成功或全部回滚; - MQTT 消息持久化、本地缓存消息,设备离线时缓存消息,重连后补发;
- 分润拆分分批执行,限制单次批量条数,避免超长 SQL 报错。
6.3 代码解耦与规范优化
- 抽取常量类统一管理分表前缀、手续费、MQTT 主题、状态码;
- 拆分回调逻辑:订单更新、库存、分润、MQTT 推送拆分为独立私有方法;
- 删除所有无用
Thread.sleep,使用异步线程池处理 MQTT、分润耗时逻辑,不阻塞回调响应; - 前端
total_fee由商品总价自动计算,移除测试写死 1 元的代码。
6.4 监控告警
- 全部异常打印完整堆栈,接入钉钉 / 邮件告警;
- 增加支付失败、库存不足、MQTT 离线、回调重试次数监控;
- 分表预创建改为项目启动一次性执行,不再每次接口请求循环检查表。
七、适用项目场景
- 校园 / 社区无人自助饺子机、售货机、取餐柜小程序支付系统;
- 微信 JSAPI 小程序支付后端完整落地模板;
- 带设备硬件联动(MQTT 下发指令)的自助零售支付业务;
- 多渠道投资人分润、年度分表库存管理的支付后台。

所有评论(0)