代码 微信支付回调 event_type 从 32 扩到 128:你的 VARCHAR(32) 不会自己变长

2026-11-07 21:03:13

微信支付回调 event_type 从 32 扩到 128:你的 VARCHAR(32) 不会自己变长

微信支付商户文档中心《更新日志》更新时间 2026.11.05:

  • 商户文档中心:
  • 合作伙伴文档中心:

这次改了什么

2026-11 涉及回调字段的变更共三处,都是同一件事——event_type【通知类型】字段长度由 32 扩展为 128:

通知归属
《免确认收款授权结果通知》商家转账
《会员卡事件通知》商家名片会员
《图片生成回调通知(单券/多次优惠)》商品券

同一份日志里还有一条与回调无关的调整:【智慧商圈】《商圈会员积分同步》的请求参数 appid【小程序AppID】、openid【顾客OpenID】由"必填"改为"选填"。

再往前翻一个月,2026-10 的记录是同一类问题的另一面——枚举扩值:

  • 【商户开户意愿确认】《提交申请单》的 finance_type 新增枚举值 SMALL_LOAN,用于标识小额贷款机构,小额贷款机构不再归入 OTHER 类型。
  • 【平台收付通(商户进件)】【特约商户进件】同样新增 SMALL_LOAN。

长度上限是契约,不是自动生效的

API v3 异步通知里的 event_type 是字符串,此前常见取值形如 MCHTRANSFER.BILL_RECEIVED、COUPON.SEND。扩到 128 意味着后续可能下发更长的枚举值。

扩长本身不会让微信报错,但商户侧这些东西不会跟着变:

  • 建表时的 VARCHAR(32)
  • 落库前的 JSON Schema maxLength
  • 代码里的校验白名单、正则、枚举类

上游放宽约束,下游的旧约束就变成了隐性 bug。上线前先扫一遍:

SELECT TABLE_NAME, COLUMN_NAME, CHARACTER_MAXIMUM_LENGTH
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND COLUMN_NAME = 'event_type';

落库这一层:严格模式报错,非严格模式静默截断

MySQL 严格模式(STRICT_TRANS_TABLES)下,超长会直接抛:

SQLSTATE[22001]: String data, right truncated: 1406 Data too long for column 'event_type' at row 1

非严格模式下不报错,直接截断。截断之后 event_type 已经不是原值,如果你拿它做幂等键或去重维度,两个长度前缀相同的不同事件会被折叠成一条,后到的那条被当成重复丢弃。

表结构先改:

ALTER TABLE wxpay_notify_log
MODIFY COLUMN event_type VARCHAR(128) NOT NULL DEFAULT ''
COMMENT '通知类型,微信支付可能继续扩展';

业务表里如果也冗余存了 event_type,一并改。字段长度宁可留余量,128 是当前上限,不是永久上限。

未知 event_type 不要返回非 2xx

API v3 回调要求商户成功处理后在 5 秒内返回 200 或 204,否则微信支付按策略重试。

把不认识的 event_type 当成错误、返回 4xx/5xx,结果是微信一直重试,你一直拒收,双方卡在同一条通知上。正确做法是:记录原始通知体,正常应答成功,再离线补处理。

但要注意一个反向的坑:没有落库的事件不能直接当已处理。应答成功只是告诉微信"我收到了",不代表业务已经完成。原始报文(含 id、event_type、resource)必须留下来,作为补单依据。

分发代码

一个最小可用的处理器,PHP 版。核心顺序是:先落原始报文,再分发,分发失败也照样应答成功。

beginTransaction();
$stmt = $pdo->prepare(
'INSERT INTO wxpay_notify_log
(notify_id, event_type, resource_type, body, status, created_at)
VALUES (?, ?, ?, ?, 0, NOW())
ON DUPLICATE KEY UPDATE notify_id = notify_id'
);
$stmt->execute([$id, $eventType, $resourceType, $raw]);

if ($stmt->rowCount() === 0) {
// 重复投递,已收过,直接应答
$pdo->commit();
http_response_code(204);
exit;
}
$pdo->commit();

// 2. 分发。重活丢队列,保证 5 秒内返回
try {
switch ($eventType) {
case 'MCHTRANSFER.BILL_RECEIVED':
handleTransferBillReceived($notify['resource']);
break;

case 'MCHTRANSFER.AUTHORIZATION_RESULT':
handleTransferAuthResult($notify['resource']);
break;

case 'MEMBERCARD.EVENT':
handleMemberCardEvent($notify['resource']);
break;

case 'COUPON.IMAGE_GENERATED':
handleCouponImageGenerated($notify['resource']);
break;

default:
// 未知类型:不报错,标记待补处理
markNotifyStatus($pdo, $id, 2);
break;
}

markNotifyStatus($pdo, $id, 1);
} catch (Throwable $e) {
// 处理失败也算已收到,留给离线任务重放
error_log("wxpay notify {$id} handle failed: " . $e->getMessage());
markNotifyStatus($pdo, $id, 3);
}

http_response_code(204);

status 约定:0 待处理、1 已处理、2 未知类型待补、3 处理失败待重放。离线任务按 status IN (0,2,3) 扫描,从 body 里重新解密 resource 补单。

幂等键用通知 id

不要用 event_type + resource 拼幂等键:

  • event_type 本身在扩值、扩长,今天截断明天变值,作为键的一半不稳定;
  • resource 是 AES-GCM 密文,同一业务对象在不同事件里内容不同,得先解密才能拿到业务主键,去重成本高;
  • 一旦 event_type 被截断,两个不同事件会算出同一个键,直接丢事件。

id 是微信支付给每条通知的原生唯一标识,重复投递时不变,直接建唯一索引就是最省事的去重:

UNIQUE KEY uk_notify_id (notify_id)

业务侧的幂等仍然要做。通知去重只解决"同一条报文收两次",解决不了"同一笔业务产生两条通知"——那要在业务表上用业务单号再加一层唯一约束。

10 月的 SMALL_LOAN 是同一类问题

finance_type 新增 SMALL_LOAN 属于枚举扩值,和 event_type 扩长是同一件事的两个方向:上游的取值集合在变,下游的校验逻辑是静态的。

进件代码里如果写了 finance_type in ('ENTERPRISE', 'INDIVIDUAL', 'OTHER') 之类的白名单,或者用 switch 穷举所有类型并在 default 里抛异常,SMALL_LOAN 进件会直接失败。这类字段的处理原则一致:校验只做格式校验,取值走 default 分支兜底并打日志,不要把"没见过的值"当成非法值。

复制全文 生成海报 微信支付 回调 接口对接

推荐文章

程序员茄子在线接单