代码 投诉回调收不到、回复一直 400、complete 调早了:微信支付消费者投诉 2.0 服务端排障

2026-10-22 09:02:14

投诉回调收不到、回复一直 400、complete 调早了:微信支付消费者投诉 2.0 服务端排障

前提:已接 APIv3(商户私钥签名、平台证书/公钥验签、APIv3 密钥解密)。只讲投诉单的接收、回复、完结,不涉及退款/争议资金处理。

一、后台能看到投诉单,系统里没有

投诉通知回调地址不是商户平台里填一下就生效的,必须调创建投诉通知回调接口提交 URL。没调这个接口,微信不会推。文档写得很直白:回调地址通过创建投诉通知回调接口中提交的参数 URL 设置,设置规范和回调 IP 列表见「回调通知注意事项」。

回调是 POST,body 为 JSON,结构同其他 APIv3 通知:

id、create_time、event_type、resource_type、summary、
resource{algorithm, ciphertext, original_type, associated_data, nonce}

event_type 只有两种:COMPLAINT.CREATE(产生新投诉)、COMPLAINT.STATE_CHANGE(状态变化)。original_type 固定 complaint。

resource 用 APIv3 密钥 + nonce + associated_data 做 AEAD_AES_256_GCM 解密,解出来只有两个字段:complaint_id、action_type。金额、用户留言、订单号都不在通知里——必须拿 complaint_id 调查询投诉单详情。

action_type 枚举:

CREATE_COMPLAINT、CONTINUE_COMPLAINT、USER_RESPONSE、RESPONSE_BY_PLATFORM、SELLER_REFUND、MERCHANT_RESPONSE、MERCHANT_CONFIRM_COMPLETE、USER_APPLY_PLATFORM_SERVICE、USER_CANCEL_PLATFORM_SERVICE、PLATFORM_SERVICE_FINISHED;申请退款单相关还会来 MERCHANT_APPROVE_REFUND、MERCHANT_REJECT_REFUND、REFUND_SUCCESS,这几类会更新投诉单状态,收到后同样要查详情。

二、应答:5 秒、幂等、别放行探测流量

5 秒内完成验签并应答。验签用 Wechatpay-Timestamp / Wechatpay-Nonce / body 组串,Wechatpay-Serial 选证书(公钥序列号形如 PUB_KEY_ID_3000000001)。

  • 验签通过:200 或 204,无 body。
  • 失败:5XX 或 4XX,body {"code":"FAIL","message":"失败"}。

失败或超时后,通知频率 60S × 10 次,之后 300S 间隔重试,最多持续 2 小时,最多 10 次(文档原文如此,实际次数以文档为准)。

会重复投递,幂等必须做:先查本地该 complaint_id 的处理状态,已处理直接返回成功;并发加锁,防函数重入。

签名探测流量以 WECHATPAY/SIGNTEST/ 开头,照常验签失败返回 4XX,别放行。

三、回复接口一直 400 的几种原因

POST /v3/merchant-service/complaints-v2/{complaint_id}/response

Header:Authorization(APIv3 签名)、Accept: application/json、Content-Type: application/json。

Body:

  • complainted_mchid:被诉商户号,必填 string(64)
  • response_content:必填。接口文档写 500 字符以内,开发指引里的示例注释写「限制 200 个字符以内」,两处不一致。按 500 传但留余量最稳。
  • response_images:选填,数组,元素是「商户上传反馈图片」接口返回的 media_id,最多 4 张
  • jump_url + jump_url_text:文字链,灰度功能,需联系行业运营
  • mini_program_jump_info{appid, path, text}:小程序跳转,灰度

成功返回 204 无 body。错误码只列了四个:PARAM_ERROR 400、INVALID_REQUEST 400、SIGN_ERROR 401、SYSTEM_ERROR 500。

对着这四个码排查,常见 400 是:

  • complainted_mchid 填了自己主商户号,而不是被诉商户号(服务商/多商户号场景最容易踩)
  • response_images 直接填了图片 URL,而不是 media_id
  • response_content 超过字符上限
  • complaint_id 用了别的单号

首次回复成功后,投诉单状态从「待处理」变「处理中」。所以「回复了就以为完事」是错的,单子还停在进行中。

四、complete 什么时候调

POST /v3/merchant-service/complaints-v2/{complaint_id}/complete

Body 只有 complainted_mchid,返回 204。

文档明确:务必在真实处理完用户投诉后再调,会向用户下发满意度评价,重复投诉率是考核客诉处理质量的重要指标之一。

提前调 → 用户不满意 → 重新投诉 → 单量翻倍,而且历史单的满意度评价一起被拉低。反方向也有问题:处理完了但一直不调 complete,单子挂在进行中,同样不好。

五、回调没收到怎么补

调查询投诉单详情用 complaint_id 查;没有 ID 就调查询投诉单列表按时间范围拉。文档 Q&A 给的限频:商户反馈 API 每分钟 60 次,查询投诉单列表 API 每分钟 200 次。

每天定时对账一次:列表接口拉昨天到今天的单,跟本地投诉表比对,缺的补进队列。

六、代码骨架(Go,片段未在本机实测)

// 回调:验签 → 解密 resource → 取 complaint_id → 幂等 INSERT → 异步查详情 → 204
func HandleComplaintNotify(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
if !verify(r.Header.Get("Wechatpay-Timestamp"),
r.Header.Get("Wechatpay-Nonce"), string(body),
r.Header.Get("Wechatpay-Serial"), r.Header.Get("Wechatpay-Signature")) {
w.WriteHeader(401)
w.Write([]byte(`{"code":"FAIL","message":"失败"}`))
return
}
var n Notify
json.Unmarshal(body, &n)
plain := decryptAEAD(n.Resource.Ciphertext, n.Resource.Nonce, n.Resource.AssociatedData)
var res struct {
ComplaintID string `json:"complaint_id"`
ActionType  string `json:"action_type"`
}
json.Unmarshal(plain, &res)
if !repo.TryInsert(res.ComplaintID) { // INSERT ... ON CONFLICT DO NOTHING
w.WriteHeader(204)
return
}
go queue.Push(res.ComplaintID) // 异步查详情
w.WriteHeader(204)
}

// 回复:注意 complainted_mchid 用被诉商户号,图片先走上传接口拿 media_id
func ReplyComplaint(complaintID, subMchID, content string, mediaIDs []string) error {
payload := map[string]any{
"complainted_mchid": subMchID,
"response_content":  content, // 文档 500,示例注释 200,留余量
}
if len(mediaIDs) > 0 {
payload["response_images"] = mediaIDs[:min(4, len(mediaIDs))]
}
return PostV3("/v3/merchant-service/complaints-v2/"+complaintID+"/response", payload) // 期望 204
}

七、文档链接

  • 投诉通知回调:
  • 回复投诉:
  • 完结投诉:
  • 服务商参考:

推荐文章

程序员茄子在线接单