OperateMchTicket 返回 1600001:先查详情再动手,附 platformkf 处理链上的几个问题
1600001 只有一种原因
OperateMchTicket 的错误码只有两个:40097 参数错误、1600001 该工单已处理。后者属于状态问题:工单一旦进入 ticket_state=2(待平台处理),说明商家已经提交过方案,此时再调处理接口一定会命中 1600001。
产生同一结果的情况有三种:回调重投(ec_platform_kf_create_mch_ticket / ec_platform_kf_ticket_timeout 只带 ticket_id,业务侧不幂等就会重放)、后台人工已经处理、脚本失败后重试。这三种情况在接口日志上看起来一模一样,只靠错误码排查效率很低。
判断“处理过没有”,查详情比反复调用处理接口更划算
GetMchTicketDetail 的返回里已经有足够依据:
merchant_reply、merchant_operate_time:示例里都是0,是未处理形态merchant_operate:含urls(凭证)、operate_source、over_operate_timeplatform_operate.is_archiving_over_time:归档超时标记platform_review_result=0:平台还没审
按 ticket_state 走一遍预检,再决定要不要调 Operate,比拿 1600001 当探针便宜得多——错一次只是拿到一个错误码,状态没变,还得回头再查。
截止时间相差的那一秒
示例里 ticket_deal_end_time=1772074398,merchant_operate.over_operate_time=1772074399,两者差 1 秒。据此可以把判据定成:截至 ticket_deal_end_time 那一秒仍可处理,从下一毫秒起算超时。
这是从示例数据反推的判据,不是官方声明,上线前用自己账号的真实工单验证一遍。另外注意别用 update_time 或本地机器时间做判断,接口返回的秒级时间戳才是基准。
description 是 HTML 片段,不是纯文本
description 字段带的是 HTML 片段,示例里含 [「规则详情」](https://store.weixin.qq.com/chengzhang/webdoc/wiki/...)。两个问题:
- 直接落到后台详情页渲染,等于把外部 HTML 拼进页面;
- 落库时不处理、展示时再统一转义,标签会二次转义成可见的尖括号文本。
入库前过 htmlspecialchars 或白名单标签过滤,展示侧按富文本渲染;这条规则要和 user_info.description 一起处理,两个字段的形态一致。
必填规则两处不一致
字段表把 description 和 confer_time 的必填都标成“是”,字段说明写的却是 is_must_description / is_must_confer_time 为 true 时才必填。照表里的“必填”做本地校验,会把合法的、只传 reply_option 的请求直接挡掉,最后表现为“按文档校验不过,不校验反而通”。
正确顺序是先调 GetTicketClassConfig 拿 mch_reply_options.conf_list[],找到当前 ticket_class_type(催发货为 9)下的 option_list,再看所选 reply_option 的 is_must_description / is_must_urls / is_must_confer_time,逐项决定传不传。顺带也能校验 reply_option 是否属于该分类,把 40097 提前拦掉。
拉列表补漏单会错位
GetMchTicketList 用 start 偏移量分页,默认 limit=10,建议不超过 50,默认按创建时间降序。补漏单时按 start 一页页翻,如果拉取期间有新工单插到列表最前面,后续页整体后移,结果是漏单或重复。
改成时间窗:用 search_start_time / search_end_time 卡住工单创建时间区间,再按 ticket_id 本地去重。分页参数保留,但只作为窗口内的翻页手段,不作为增量依据。
粘不动的那个 curl
查询工单分类配置的官方示例里,URL 被拼了两遍:
POST https://api.weixin.qq.comhttps://api.weixin.qq.com/channels/ec/platformkf/getticketclassconfig?access_token=ACCESS_TOKEN?
照抄一定失败。正确形态:
curl -s -X POST "https://api.weixin.qq.com/channels/ec/platformkf/getticketclassconfig?access_token=ACCESS_TOKEN"
调用约定
四个接口都是 POST,路径统一在 /channels/ec/platformkf/ 下:getticketclassconfig、getmchticketlist、getmchticketdetail、operatemchticket。access_token 走 query string,权限集 ID 131,服务商代调用用 authorizer_access_token。
查详情与处理:
# 1. 先查详情,确认状态
curl -s -X POST "https://api.weixin.qq.com/channels/ec/platformkf/getmchticketdetail?access_token=ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"ticket_id":"123"}'
# 2. 状态允许再提交
curl -s -X POST "https://api.weixin.qq.com/channels/ec/platformkf/operatemchticket?access_token=ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"ticket_id":"123","reply_option":1,"description":"123"}'
处理前预检(PHP)
function canOperate(string $token, string $ticketId): bool
{
$url = 'https://api.weixin.qq.com/channels/ec/platformkf/getmchticketdetail'
. '?access_token=' . urlencode($token);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(['ticket_id' => $ticketId]),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
]);
$raw = curl_exec($ch);
curl_close($ch);
$resp = json_decode((string)$raw, true);
if (($resp['errcode'] ?? -1) !== 0) {
return false; // 详情都拉不到,别拿 Operate 去试
}
$t = $resp['merchant_ticket'];
// 已进入待平台处理,说明商家提交过方案,再调必撞 1600001
if ((int)$t['ticket_state'] === 2) {
return false;
}
// 已有商家侧操作痕迹
if ((int)$t['merchant_operate_time'] > 0) {
return false;
}
if (!empty($t['merchant_operate']['urls'])) {
return false;
}
// 截止秒本身仍可处理,超一秒即视为超时
if (time() > (int)$t['ticket_deal_end_time']) {
return false;
}
return true;
}
真实调用前补一步 GetTicketClassConfig,确认 reply_option 在 option_list 里,并按 is_must_* 决定 description / confer_time 是否传。
预检清单
- 已调
GetMchTicketDetail,errcode=0且拿到merchant_ticket ticket_state != 2(等于 2 直接跳过,交给平台侧)merchant_operate_time == 0、merchant_operate.urls为空- 当前时间
<= ticket_deal_end_time(截止秒仍可处理) reply_option来自GetTicketClassConfig对应分类的option_listdescription/confer_time按is_must_description/is_must_confer_time判断是否必传,而不是照字段表的“必填”description中的 HTML 已转义或过白名单再入库- 回调处理按
ticket_id幂等,重投不会二次调用Operate - 补漏单走
search_start_time/search_end_time时间窗,本地按ticket_id去重 - 分类配置接口 URL 未沿用拼接错误的示例
- 服务商代调用传的是
authorizer_access_token,权限集 131 已授权