实战避坑:从零配置微信支付商家券在小程序中的完整发券与核销流程(含代码片段)
当用户在小程序下单时看到"满100减20"的优惠券,点击领取后自动抵扣——这背后需要开发者打通商家券从创建到核销的全链路。本文将用一个真实案例拆解微信支付商家券的完整生命周期,重点解决跨商户号发券、证书配置异常等高频踩坑点。
1. 环境准备与权限配置
1.1 商户平台基础设置
登录微信支付商户平台后,需完成三个关键操作:
- 开通商家券功能:路径为
产品中心->商家券->开通,需提交商户资质审核(通常1个工作日内完成) - 配置API证书:在
账户中心->API安全下载的证书包含:apiclient_cert.pem(商户证书)apiclient_key.pem(私钥文件)- 保存到项目
/cert目录下,注意不要提交到代码仓库
证书常见报错处理:
NO_CERTIFICATE:检查证书路径是否包含中文或特殊字符SIGN_ERROR:确认商户号与证书的匹配关系
- 设置IP白名单:在
API安全->IP白名单添加服务器公网IP,否则调用接口会返回IP_NOT_ALLOWED
1.2 小程序端配置
在小程序管理后台需完成:
// app.json中声明发券插件 { "plugins": { "coupon": { "version": "1.2.0", "provider": "wx2351867456a12345" } } }- 插件申请需提供小程序AppID和商户号绑定关系证明
- 开发版和体验版需先调用
wx.login获取用户openid
2. 创建商家券批次
2.1 接口参数设计
通过/v3/marketing/busifavor/stocks接口创建券批次时,关键参数如下:
| 参数 | 类型 | 必填 | 示例值 | 说明 |
|---|---|---|---|---|
| stock_name | string | 是 | 618大促满减券 | 批次名称用户可见 |
| belong_merchant | string | 是 | 1234567890 | 制券商户号 |
| coupon_use_rule | object | 是 | - | 使用规则配置 |
| available_begin_time | string | 是 | 20240520T13:29:35+08:00 | ISO8601格式 |
| available_end_time | string | 是 | 20240620T23:59:59+08:00 | 需大于开始时间 |
典型请求体示例:
{ "stock_type": "NORMAL", "out_request_no": "BUSIFAVOR_001", "coupon_use_rule": { "coupon_available_time": { "available_begin_time": "2024-05-20T00:00:00+08:00", "available_end_time": "2024-06-20T23:59:59+08:00" }, "fixed_normal_coupon": { "discount_amount": 2000, "transaction_minimum": 10000 } }, "stock_send_rule": { "max_coupons": 10000, "max_coupons_per_user": 3 } }2.2 跨商户号发券方案
当制券商户(A)与发券商户(B)不同时,需额外配置:
- 商户A在
营销中心->跨商户发券添加商户B到白名单 - 创建批次时在
stock_send_rule中指定merchant_option为ALLOW_ALL或添加特定商户号 - 发券接口调用方使用商户B的证书和密钥
高频报错
INVALID_REQUEST往往是由于未配置跨商户关系导致,可通过接口返回的err_code_des字段确认具体原因
3. 小程序发券实战
3.1 插件接入流程
在小程序页面中嵌入发券按钮:
<coupon-plugin coupon-list="{{couponList}}" bind:getcoupon="handleGetCoupon" ></coupon-plugin>对应的JS逻辑:
Page({ data: { couponList: [{ stock_id: '123456', coupon_id: 'COUPON_001' }] }, handleGetCoupon(e) { const { code, coupon } = e.detail if (code === 'SUCCESS') { this.setData({ hasCoupon: true }) } else { wx.showToast({ title: '领取失败', icon: 'none' }) } } })3.2 服务端发券API
当需要精准控制发券逻辑时(如仅对特定用户组发放),可使用服务端接口:
import requests from auth import generate_authorization url = "https://api.mch.weixin.qq.com/v3/marketing/busifavor/coupons/send" headers = { "Authorization": generate_authorization("POST", url), "Content-Type": "application/json" } data = { "stock_id": "123456", "out_request_no": "SEND_001", "appid": "wx1234567890abcdef", "send_request_no": "REQ_20240520001", "openid": "o4FAL6h7jN12p34kQq5d7i8U90x1" } response = requests.post(url, json=data, headers=headers) print(response.json())关键验证点:
out_request_no需保证业务唯一性防止重复发券- 单个用户领取次数受
max_coupons_per_user限制 - 异步通知需配置
notify_url处理发放结果
4. 订单核销全流程
4.1 可用券查询
在订单页展示用户可用优惠券:
wx.request({ url: 'https://api.weixin.qq.com/wxa/business/getuserbusifavor', data: { openid: '用户openid', merchant_id: '发券商户号' }, success(res) { console.log('可用券列表:', res.data.coupons) } })4.2 核销接口调用
当用户选择使用优惠券时,调用核销接口:
public JSONObject consumeCoupon(String couponCode, String merchantId) throws Exception { String url = "https://api.mch.weixin.qq.com/v3/marketing/busifavor/coupons/use"; String nonceStr = WXPayUtil.generateNonceStr(); Map<String, String> params = new HashMap<>(); params.put("coupon_code", couponCode); params.put("use_request_no", "USE_" + System.currentTimeMillis()); params.put("merchant_id", merchantId); String body = JSONObject.toJSONString(params); String auth = AuthUtil.buildAuthorization("POST", url, body); CloseableHttpClient client = HttpClients.createDefault(); HttpPost httpPost = new HttpPost(url); httpPost.setHeader("Authorization", auth); httpPost.setHeader("Content-Type", "application/json"); httpPost.setEntity(new StringEntity(body)); CloseableHttpResponse response = client.execute(httpPost); return JSONObject.parseObject(EntityUtils.toString(response.getEntity())); }4.3 核销结果处理
典型响应结果及对应处理:
{ "code": "SUCCESS", "coupon_code": "ABC123", "wechatpay_use_time": "2024-05-20T15:30:00+08:00" }异常情况处理方案:
COUPON_NOT_FOUND:检查券码是否已过期或被核销USER_ILLEGAL:确认openid与领券用户一致FREQUENCY_LIMIT:避免短时间高频调用
5. 调试与监控方案
5.1 日志记录策略
建议在关键节点记录日志:
- 发券成功/失败记录用户openid和券批次
- 核销时保存原始请求和响应数据
- 异步通知处理结果
示例日志结构:
CREATE TABLE coupon_logs ( id BIGINT PRIMARY KEY AUTO_INCREMENT, event_type VARCHAR(20) COMMENT 'SEND/USE/CALLBACK', openid VARCHAR(32), coupon_code VARCHAR(64), request_data TEXT, response_data TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );5.2 微信支付沙箱环境
在开发阶段使用沙箱环境验证流程:
- 在商户平台
账户中心->开发配置开启沙箱 - 调用
/v3/marketing/busifavor/stocks接口时添加头信息:X-Wechatpay-Sandbox: true - 测试专用证书需单独下载
沙箱环境特有限制:
- 券批次有效期最大24小时
- 单批次最多发放10张券
- 不支持跨商户号发券测试
6. 性能优化实践
6.1 高频发券优化
当面临大促期间高并发发券需求时:
- 采用异步发券模式,先快速响应用户请求,后台队列处理实际发券
- 使用本地缓存记录已领券用户,减轻数据库压力
- 批量查询接口替代单次查询
Redis缓存示例:
def check_coupon_limit(user_id, stock_id): key = f"coupon:{stock_id}:{user_id}" count = redis_client.incr(key) if count == 1: redis_client.expire(key, 86400) # 24小时过期 return count <= MAX_PER_USER6.2 核销接口降级方案
当微信支付接口不稳定时的应对策略:
- 本地记录核销状态,标记为"处理中"
- 定时任务补偿处理失败记录
- 超过重试次数后转为人工处理
补偿任务设计:
const failedRecords = await db.query( 'SELECT * FROM coupon_consumption WHERE status = "PENDING" AND created_at > ?', [new Date(Date.now() - 3600000)] ); for (const record of failedRecords) { try { const result = await wechatPay.consumeCoupon(record.coupon_code); await db.updateStatus(record.id, 'SUCCESS'); } catch (error) { await db.incrementRetryCount(record.id); } }在实际项目中,我们发现证书过期导致的故障占比最高,建议在证书到期前30天启动自动更新流程。通过定期调用/v3/certificates接口验证证书有效性,可提前发现并替换即将过期的证书文件。