微信支付回调 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 分支兜底并打日志,不要把"没见过的值"当成非法值。