编程 platformkf 报 1600001「该工单已处理」:先查详情再动手,别拿 Operate 当探针

2026-11-15 00:04:05

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_time
  • platform_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/...)。两个问题:

  1. 直接落到后台详情页渲染,等于把外部 HTML 拼进页面;
  2. 落库时不处理、展示时再统一转义,标签会二次转义成可见的尖括号文本。

入库前过 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_list
  • description / 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 已授权

推荐文章

程序员茄子在线接单