CRMEB 多商户商城「余额 + 微信组合支付」开发记录
功能概述:订单支付新增「余额 + 微信」组合支付——余额最多支付订单金额的 n%(比例后台可配置),余额不足或超出上限的部分由微信支付兜底。
一、需求说明
1.1 现状
现有支付弹窗支持 5 种单一渠道支付:消费积分(balance)、微信(weixin)、支付宝(alipay)、补贴券(xiaofeiquan)、线下(offline)。用户余额不足全额时,无法部分使用余额,只能全额走微信/支付宝,导致用户余额使用率低、大额订单全额外付。
1.2 目标与规则
| 规则 | 说明 |
|---|---|
| 余额上限 | 余额最多支付订单金额的 n%(n 为后台系统配置项,建议键名 pay_balance_ratio,范围 0–100) |
| 兜底渠道 | 剩余部分由微信支付兜底(支付宝不参与组合) |
| 金额拆分 | balance_amount = min(用户可用余额, 订单金额 × n%);wechat_amount = 订单金额 − balance_amount |
1.3 边界条件
| 场景 | 处理 |
|---|---|
| 用户可用余额 = 0 | 隐藏组合入口,走单一微信支付 |
| 用户可用余额 ≥ 订单金额 | 不进入组合,走原有「余额全款」单一支付 |
| 余额足够 n% 但不足以支付剩余 | 按 min(余额, 金额×n%) 拆分,剩余全走微信 |
| 微信支付失败 / 超时 | 不扣余额,订单保持待支付,可重新发起 |
| 支付宝 | 不参与组合,保持原逻辑(H5/小程序走 order_pay_back 中转) |
二、总体方案
采用「微信只支付兜底金额」的单笔支付拆分方案(非全额微信 + 余额退款方案):
前端在支付弹窗展示「余额 + 微信」组合项,显示「余额抵扣 X 元 + 微信支付 Y 元」。
用户选组合支付并确认 → 前端携带 balance_amount / wechat_amount 调下单接口(金额建议由后端按 n% 规则计算返回,前端只展示)。
后端下单时仅对 wechat_amount 发起微信支付——微信侧看到的订单金额、回调金额都是 wechat_amount,订单总金额仍为原金额。
余额部分在微信回调确认支付成功后,与订单状态更新放同一事务扣减。
微信回调链路保持 CRMEB 原有结构不变:wechatNotify → WechatService::handleNotify → event('pay_success_{attach}') → 事件监听器 → 订单状态更新。监听器内兼容组合支付订单:识别已扣余额部分,仅登记微信流水 trade_no 并置 paid=1。
三、前端修改记录(uni-app)
3.1 支付方式列表 getPayTypeList()
在 payMode 数组中新增「组合支付」项:
{
name: '余额+微信支付',
icon: 'icon-a-ic_wechatpay',
value: 'combine_weixin',
title: '余额抵扣 {balance_amount} 元,微信支付 {wechat_amount} 元',
payStatus: res.data.yue_pay_status && res.data.pay_weixin_open
}展示条件:余额开关与微信开关同时开启、now_money > 0、now_money < pay_price。比例 n% 与拆分金额建议由后端计算并随接口返回,前端只做展示。
3.2 watch orderPayList / getOrderConfig()
现有链路保留:Vuex orderPayList 更新 → deep watch 触发 → payMode = nVal → getOrderConfig()。getOrderConfig() 拉取订单配置后,若为组合支付订单,把后端返回的 balance_amount / wechat_amount 存入 data 用于展示。
3.3 goPay() 组合支付分支
} else if (paytype == 'combine_weixin') {
type = 'combine_weixin';
// #ifdef H5
type = this.$wechat.isWeixin() ? 'combine_weixin' : 'combine_weixin_h5';
// #endif
}前置校验:balance_amount > 0、wechat_amount > 0、余额足以支付本次抵扣部分。下单时携带 balance_amount / wechat_amount 参数,支付成功后跳转沿用原微信成功路径(/pages/order_pay_status/index?status=1)。
四、后端修改记录(ThinkPHP / CRMEB 多商户)
4.1 下单接口(orderPay / PayServices::pay)
接收组合支付参数 balance_amount / wechat_amount(或后端按 n% 规则自行计算);
校验:balance_amount <= min(now_money, pay_price × n%);wechat_amount = pay_price − balance_amount >= 0;
微信支付下单金额 = wechat_amount(注意以「分」为单位),订单总金额仍为 pay_price;
订单落库记录组合拆分信息(建议订单表新增字段 balance_amount、wechat_amount);
attach 沿用业务标识(如 order),回调事件名 pay_success_order 不变。
4.2 余额扣减
时机:微信回调确认支付成功后,与订单状态更新同一事务扣减,避免「微信未付成功、余额已扣」无法回滚。实现:调用余额变动服务(user_bill)扣减 balance_amount,扣减前校验余额充足。
4.3 微信回调链路(结构不变,仅监听器兼容)
event('pay_success_' . $notify['attach'], [
'order_sn' => $notify['out_trade_no'],
'data' => $notify,
'is_combine' => 0,
]);注意:组合支付订单的微信回调 total_fee 是兜底部分(wechat_amount),不是订单全款——监听器必须识别组合订单,不能用回调金额覆盖订单应付款,否则对账/金额校验会出错。
4.4 事件注册与监听器
'pay_success_order' => [\crmeb\listens\pay\OrderPaySuccessListen::class],
OrderPaySuccessListen::handle() 处理流程:① 用 order_sn(= out_trade_no)查订单;② 幂等:paid == 1 直接返回(防微信重复回调);③ 若是组合支付订单:校验并扣减余额 balance_amount → 更新订单 paid = 1、pay_time、trade_no = 微信流水(对应 wechat_amount);④ 触发后续业务:分佣、库存、短信/模板消息、订单状态记录。
五、组合支付时序
用户选择「余额 + 微信」组合支付
→ 前端展示抵扣金额(balance_amount / wechat_amount)
→ 点击确认 goPay() → 调下单接口
→ 后端校验 n% 规则 → 生成订单并记录拆分 → 仅对 wechat_amount 发起微信支付
→ 用户在微信完成支付(金额 = wechat_amount)
→ 微信异步回调 wechatNotify → handleNotify 验签
→ event('pay_success_order') → OrderPaySuccessListen
① 查订单 ② 幂等 ③ 扣减余额 ④ 更新订单已支付
→ 返回 SUCCESS → 前端跳转 order_pay_status?status=1六、涉及文件清单
| 端 | 文件 / 模块 | 改动 |
|---|---|---|
| 前端 | 支付弹窗组件(order_pay) | 新增组合支付项、展示抵扣金额、watch 兼容 |
| 前端 | goPay() | 新增组合支付渠道分支与校验 |
| 前端 | api/order.js | 下单接口增加组合支付参数 |
| 后端 | PayServices / OrderPayServices | 支持组合支付下单、金额拆分与校验 |
| 后端 | 订单模型 / 数据表 | 新增 balance_amount / wechat_amount 字段 |
| 后端 | Notify 控制器 / WechatService | 基本不变(验签 + 协议分发) |
| 后端 | app/event.php | pay_success_order 注册(已有,确认存在) |
| 后端 | OrderPaySuccessListen | 兼容组合订单(余额扣减 + 状态更新) |
| 后端 | StoreOrderSuccessServices::paySuccess | 组合支付订单落库处理 |
| 后台 | 系统配置 | 新增 n% 比例配置项 pay_balance_ratio |
七、已知问题与注意事项
前端 goPages URL 拼接 bug:goPages + 'status=1'(缺 &)、goPages + '?status=1'(误用 ?)需统一改为 goPages + '&status=' + (成功 ? 1 : 0);
getPayList / getPayTypeList 命名歧义:getPayList() 调用的是 $util.getPayTypeList,组件内又定义了同名方法,需核对 util 实现是否内部转发;
小程序 routine 分支 complete 回调里 if (res.errMsg == 'requestPayment:cancel') 的 res 是外层下单接口响应(非支付回调),判断恒不成立,可删除;
回调金额口径:组合支付回调金额 = 微信部分,监听器与对账须区分;
余额扣减时机:推荐支付成功后同事务扣减,若下单即扣需处理失败回滚;
n% 配置:后台新增系统配置 pay_balance_ratio,前端展示值以后端计算为准;
支付宝不参与组合,避免引入支付宝组合分支增加复杂度;
微信下单金额为「分」,后端拆分计算须注意单位换算,避免精度问题。
八、验收清单
支付弹窗在「余额 > 0 且 余额 < 订单金额」时展示组合项,金额拆分显示正确;
组合支付下单后微信侧支付金额 = wechat_amount(非全款);
微信支付成功后订单置为已支付,余额扣减 balance_amount,流水记录 trade_no;
微信重复回调不重复扣余额、不重复改状态(幂等);
微信支付失败时余额未被扣减,订单仍可重新支付;
余额占比超过 n% 时被限制(金额校验生效);
后端配置 pay_balance_ratio 调整后无需改代码即时生效;
H5 / 小程序 / APP 三端支付成功后均正确跳转 order_pay_status;
原单一渠道支付回归通过。
记录说明:本文基于对现有支付弹窗(uni-app)与微信回调链路(ThinkPHP / CRMEB 多商户)的代码梳理整理;「n%」为待配置项(建议键名 pay_balance_ratio),具体默认值由运营确认。
