多店铺待发货提醒

SKILL商业运营
商业运营 未标注权限 许可证不明确
安装这个技能

微信小店(视频号小店)订单与售后数据采集:通过开放接口(channels/ec)拉取订单列表、订单详情、发货物流、售后单,存为本地 JSON,识别「退货退款」与「物流拦截退货」,并用第三方物流轨迹(快递100)批量验证「包裹被退回」真伪,生成订单导出 / 销售分析 / 退回订单 Excel。当用户提到微信小店/视频号小店订单采集、订单导出、订单数据同步、增量拉单、销售数据同步、小店 GMV 统计、售后单采集、退货退款/拦截退货统计、产生退回的订单、包裹被退回、物流拦截、按售后完成时间筛选退回订单、按售后服务完成时间/申请时间查询、物流轨迹查询、快递100 轨迹验证、发货单号查物流、订单状态查询等需求时使用本技能。

一句话选型结论

按资料字段整理

多店铺待发货提醒是商业运营类的 Agent Skill,安装方式未标注,权限级别未标注,许可证不明确,有可直接复制的安装命令。技能本质是给模型读的说明书,效果取决于模型与客户端;装完先用一个小任务验证,再放进正式流程。

分类商业运营 安装方式未标注 权限级别未标注权限 许可证许可证不明确
累计安装 未标注 本周 未标注
GitHub 星标 未标注 仓库 未标注
下载量 未标注 技能市场下载数
权限级别 未标注权限 安装前请确认风险范围
许可证 许可证不明确 未说明

安装

未标注
在支持 Skills 的客户端里执行,装完问一句「你有哪些技能」验证
skillhub install user_2e4bebd7/wxshop-order-status-collect
  1. 1
    确认客户端支持

    确认你的客户端支持 Skills(Claude、Codex、Cursor 等),并升级到较新的版本。

  2. 2
    拿到技能文件

    执行上面的命令安装;也可以把仓库里的 SKILL.md 手动放进客户端的 skills 目录。

  3. 3
    刷新技能列表

    重新打开客户端,或刷新技能列表,让新放进来的技能被加载。

  4. 4
    验证是否生效

    问一句「你有哪些技能」,确认列表里出现它,再拿一个小任务试跑。

技能档案

商业运营
字段来自技能仓库与公开索引,中文说明由本站标注
作者 / 维护方user_2e4bebd7仓库 未标注 · 版本 未标注
宿主客户端未标注技能可被这些客户端加载
安装方式未标注安装包 community
权限级别未标注权限安装前请确认风险范围
许可证未说明许可证不明确,商用前请自行确认条款
仓库可用性available技能仓库当前是否可访问
最近核验未标注上游最后核验时间 · 数据更新 1791624749
正文体量110.9 KBSKILL.md 原文大小,越大说明说明书写得越细

典型使用场景

3 类
按分类整理的常见用法,具体以 SKILL.md 为准
写运营方案

按目标产出活动方案与执行清单。

分析数据与指标

从报表里找问题、给结论。

整理客户与销售材料

把客户信息与话术整理成可直接用的材料。

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有标注权限级别,实际能做什么要装好后确认。
  • 许可证不明确,商用或二次分发前先到仓库确认条款。
  • 没有做站内核验,内容以作者仓库为准。
  • 技能只是说明书,实际效果取决于模型与客户端,装完先用小任务试一遍。

更适合

  • 需要在商业运营场景里给模型补一套做法的人。
  • 愿意先读一遍 SKILL.md、确认它会让模型做什么的人。

不太适合

  • 对许可证要求很严、必须明确可商用的人。
  • 只想装完就自动变强、不打算看说明书的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
技能权限级别安装方式累计安装宿主客户端安装命令
多店铺待发货提醒当前 未标注权限 未标注 未标注 未标注 有
MX 股票筛选器 联网访问 ClawHub 市场 79.9 万 OpenClaw 有
交易 未标注权限 ClawHub 市场 58.1 万 OpenClaw 有
营销技巧 只读 ClawHub 市场 54.0 万 OpenClaw 有
百灵鸟好 联网访问 GitHub 仓库 52.6 万 Codex、Claude、Cursor 有
股票信息浏览器 接触敏感数据 ClawHub 市场 40.5 万 OpenClaw 有

SKILL.md 全文

110.9 KB
模型实际读到的说明书原文,来自技能仓库

微信小店订单状态采集 Skill

> ⚠️ 本技能为上架版,已做数据脱敏:文中所有订单号、运单号、店铺名称均为占位符;
> 具体单量等经营数字已统一泛化为 N,仅表示"存在该量级/先后关系",不代表真实比例或数值。
> 使用前请把你自己的店铺凭据填入 stores.example.json(复制为 stores.json)后使用。

概述

本技能用于从微信小店开放 API 增量采集订单、发货、售后数据,并按统一结构存储到本地 JSON 文件,最后输出本次采集统计;同时支持识别「退货退款」与「物流拦截退货」,并生成销售分析类 Excel。

两种执行方式:

  • A. 直接调接口采集(下文"执行流程")—— 灵活可定制,适合售后 / 拦截退货等专项分析
  • B. 用内置脚本一键出表(scripts/fetch_orders.py)—— 适合常规订单导出 + 销售汇总分析,一条命令产出 Excel

资源文件

路径 用途
scripts/fetch_orders.py 一键采集脚本:拉单 → 扁平化 → 输出「订单明细 + 汇总分析」Excel(含快递公司/发货单号/发货时间/物流方式),支持按时间范围 / 增量模式;同时导出 target_orders.json(英文键 + waybill_id_list),可直接被 trace_query.py 消费
scripts/fetch_aftersales.py 售后单采集脚本(⚠️ 售后类需求的首选,见第零步「范围限定」+「时间基准三选一」):按时间枚举全部售后单 → 拉详情 → 筛出退货退款/拦截退货 → 出 Excel(含运单号/快递公司/商家备注/售后完成时间)。比逐单翻订单快 20 倍以上。--time-basis complete\|create 切时间口径(默认 complete);--only-completed 只采已完成售后(推荐,更省请求);--from-cache 可跳过接口秒级重建结果;--workers N(默认 6,上限 10)并发拉售后/订单详情——2026-09-13 提速改造:每条记录独立请求、只改并发不改判定口径,实测 N 个/秒(旧串行版 1.4 个/秒,7 倍提速),大店拉详情从 20+ 分钟降到 3~5 分钟
scripts/ewaybill_callback.py 路径 B:物流轨迹推送回调服务。实时接收 ewaybill_push_path,捕获 status=6(退回)并落库,同时拿到 ewaybill_order_id。含 --selftest 端到端自检
scripts/trace_query.py 路径 D:物流轨迹批量查询(✅ 已用实测店铺真实凭据跑通)。三后端(快递100 / 快递鸟 / 小店官方面单),按运单号或面单ID批量查轨迹,判定「退回/拒收」,出 Excel。支持一单多包裹(waybill_id_list)。省配额开关:--only-aftersale(售后范围)、--only-intercept(只查拦截退货单,最省)、--order-id(定点复核某几单)
scripts/merge_trace_report.py 终版报告合并:把 fetch_aftersales.py 的售后结果 + trace_query.py 的轨迹结果合成一份带「物流轨迹验证」列的退回订单 Excel(3 表:退回订单汇总 / 物流轨迹铁证 / 口径与边界说明),并自动标出「备注法漏掉」的单
scripts/probe_endpoints.py 端点存在性探针:批量试接口路径,靠 40066 invalid url 判定端点是否存在。新接口不确定能不能用、或怀疑有未文档化的轨迹接口时先跑它,别靠猜
scripts/fetch_shop_info.py 店铺基本信息采集:GET /channels/ec/basics/info/get 拉店名/头像/主体类型/经营状态,落盘 shop_info.json(展示层读 nickname 当店名,不用写死)。⚠️ 该端点必须 GET,POST 报 43001
scripts/fetch_ship_status.py 发货状态权威判定(★ 「未发货退款」必须用它,别信本地表):按 order_id 批量调 /channels/ec/order/get,以 order_detail.delivery_info.delivery_product_info(包裹数组长度)+ ship_done_time 判定是否已发货,落盘 ship_status.json。增量(只查 ship_status.json 里没有的单)、多线程(--workers,默认 6)、原子写。支持 --ids-file <文件>(每行一个 order_id)/ --days N(只查最近 N 天下单的)/ --limit N。输出 {"<order_id>": {"shipped": bool, "pkg_count": int, "ship_done_time": ts, "order_status": ..., "as_cnt": int, "checked_ts": ts}}
scripts/stores_util.py 多店注册表解析(★ 所有采集脚本的唯一凭据入口,防串店):读根目录 stores.json,按 店铺名/appid 解析凭据与数据目录;--store all 返回 None 由调用方循环。每次运行打印「本次采集店铺」横幅
scripts/convert_orders_cache.py orders_cache(中文键)→ 展示层四表转换:fetch_orders 产出中文键 orders_cache.json,本脚本转成 orders.json/order_products.json/deliveries.json(字段映射与工作台 server.py convert_orders_cache() 逐字段一致)。合并方向富行优先:fetch_aftersales 写的同名 orders.json 行带 merchant_notes/products/waybill_id(拦截判定靠它),同单并存时保留富行,转换行只回填缺失订单
references/kf-session-scrape.md 客服工作台「会话查询」全量抓取与知识库蒸馏(2026-09-16 两店全量实测):见下方「最新结论速查」2026-09-N 条;含行摘要字段含义、两类克隆行识别、弹窗「关不掉」死坑、就绪轮询、空会话判定、速率与并发、蒸馏要点

★ 多店采集(2026-09-11 起,5 家店已接入)

  • 注册表:根目录 stores.json(明文,用户要求)。当前 5 家:
    店铺A(<STORE_A_APPID>,主店,data_dir=base 基目录) / 店铺B(<STORE_B_APPID>) /
    店铺C(<STORE_C_APPID>) / 店铺D(<STORE_D_APPID>) /
    店铺E(<STORE_E_APPID>,2026-09-14 接入)
  • ⚠️ 注册表是「两个系统共用」的唯一凭据真源:订单工作台(Python)与
    快递面单匹配系统(&lt;工作目录&gt;/shipmatch/,Node+SQLite,端口 8788)都从这里取店;加店只需改这一处。
  • 数据目录:主店 = ~/.workbuddy/data/wxshop/(基目录,历史数据不动);新店 = ~/.workbuddy/data/wxshop/stores/&lt;appid&gt;/
  • 用法:所有采集脚本统一加 --store &lt;店铺名或appid&gt;:
    python fetch_orders.py --store 店铺C --days 30 python fetch_aftersales.py --store 店铺C --start 2026-06-13 --end 2026-09-11 python fetch_ship_status.py --store 店铺C --ids-file ids.json python fetch_shop_info.py --store 店铺C python convert_orders_cache.py --store 店铺C
    不传 --out 时默认写该店数据目录;每次运行第一行打印「本次采集店铺」横幅,看到店名不对立即 Ctrl+C。
  • 反串店三道闸:① 凭据只从 stores.json 解析(--store 优先于环境变量);② 数据按店分目录物理隔离;
    ③ 每次运行打印店名横幅。传了注册表里不存在的 --store 会直接报错退出,绝不盲采。
  • 新店接入标准流程:① stores.json 加一条(name/appid/secret/data_dir=stores/&lt;appid&gt;)→
    ② fetch_shop_info.py --store X 验证凭据+拿店名 → ③ fetch_orders.py --store X --days 30 →
    ④ fetch_aftersales.py --store X --start &lt;起点&gt; --end 今天 → ⑤ convert_orders_cache.py --store X →
    ⑥ fetch_ship_status.py --store X --ids-file &lt;该店售后订单号&gt;。全部通过即接入完成。
  • ⚠️ 已知坑(已处理):fetch_aftersales.py 会把「仅售后订单」的 orders.json 写到输出目录根,
    可能盖掉 convert_orders_cache.py 转出的全量表 —— 售后采完后重跑一次 convert(富行优先合并,两边的行都保留)。
  • token/限速是按店独立的,多家店可并行采集互不影响;同一家店内脚本自带 0.2~0.3s 限速。

| references/api.md | 微信小店订单相关接口完整参考(请求体、返回结构、status 枚举、错误码、物流轨迹 4 路径) |
| assets/config.example.json | 采集脚本配置样例(appid / secret / out_dir) |
| assets/callback_config.example.json | 回调服务配置样例(token / encoding_aes_key / appid / port / path) |
| assets/trace_providers.example.json | 轨迹查询配置样例(字段说明) |
| assets/trace_providers.json | 轨迹查询已跑通的实际配置(含快递100 凭据 + phone=店铺寄件人手机号),可直接用 |

店铺配置

小店开放接口凭据

  • AppID:<STORE_A_APPID>
  • AppSecret:<STORE_A_APP_SECRET>

物流轨迹(路径 D)凭据 —— 已跑通,直接可用,见 assets/trace_providers.json

  • provider:kuaidi100
  • customer:48B5E3EFB8F20A1B7926AC1BC53489EA
  • key(授权key):ICinTvHv8286
  • phone(必填):&lt;发货人手机号&gt; —— 店铺寄件人手机号。⚠️ 中通/顺丰查询要求 phone,而小店接口的收货人手机号是脱敏的且无解密接口,故填寄件人

关键前提:中通/顺丰查询必须带手机号 → 用店铺寄件人手机号(官方允许收/寄件人任填一个),这是 D 路能跑通的必要条件

前置条件:

  • 调用账号需具备"获取订单列表"权限(权限集 id 131);服务商代调用改用 authorizer_access_token
  • 依赖 requests 与 openpyxl(用受管 Python 时先确认已装,缺则安装到指定 venv)
  • 脚本方式需提供凭据:config.json(复制 assets/config.example.json)或环境变量 WX_STORE_APPID / WX_STORE_SECRET

脚本方式快速开始(方式 B)

python &quot;&lt;skill目录&gt;/scripts/fetch_orders.py&quot; --config config.json --start 2026-09-01 --end 2026-09-09 --out ./output
python &quot;&lt;skill目录&gt;/scripts/fetch_orders.py&quot; --config config.json --days 7 --out ./output          # 最近 7 天
python &quot;&lt;skill目录&gt;/scripts/fetch_orders.py&quot; --config config.json --incremental --out ./output     # 增量(自上次同步点)

脚本自动按 7 天切分窗口、内置 0.3s 限速与 token 失效重试,输出 微信小店订单_&lt;时间戳&gt;.xlsx(订单明细 + 汇总分析)、orders_cache.json(增量合并缓存)、sync_state.json(增量游标)。

查"某时间段产生退回的订单"用这个:

python &quot;&lt;skill目录&gt;/scripts/fetch_aftersales.py&quot; --config config.json --start 2026-08-01 --end 2026-09-01 --out ./output --excel

自动按天枚举售后单(接口限制单次 ≤24h)、拉详情、拉相关订单、分类退货退款/拦截退货,输出 产生退回的订单_&lt;起&gt;_&lt;止&gt;.xlsx(4 个工作表,含口径与边界说明)。

执行流程

⚠️ 第零步:范围限定(默认规则,务必先做)

凡涉及「查退回 / 拦截 / 退货退款 / 售后相关」的需求,一律先把范围限定在「售后管理」板块内的订单
—— 即先用 /channels/ec/aftersale/getaftersalelist 枚举售后单,拿到 order_id 集合,
只对这批 order_id 调订单详情,不要先全量拉几千个订单再去筛。

理由(实测):

做法 请求量(8/1~9/1) 耗时 是否漏单
❌ 全量订单 → 逐单详情 → 再筛售后 2643 次详情 ~35 min 容易漏(早前只取 status=200/250 漏掉约 10%)
✅ 售后管理枚举 → 只拉涉及的订单详情 ≈ 70 + 售后单数 次 1~3 min 不漏(以售后台账为准)
  • 实现:scripts/fetch_aftersales.py --start ... --end ... --excel(已内置,含运单号/快递公司/商家备注)
  • 订单详情的用途仅限于补字段(运单号、快递公司、商家备注、订单状态),不用它来圈定范围。
  • 需要全量订单做销售分析(GMV/Top10)时才走 scripts/fetch_orders.py,那是另一类需求,不要混用。

⚠️ 时间基准三选一:完成时间(默认) / 申请时间 / 下单时间

「售后管理」后台的筛选里有两个时间,加上订单自身的下单时间,共三种口径。
2026-09-11 起默认口径 = 售后完成时间 complete(工作台/展示层已同步),其余两个仍支持:

用户问法(示例) 时间基准 对应后台筛选项 脚本参数
「完成时间在 8.1~9.1 的退回订单」「这段时间办结/处理完的」(默认口径) 售后完成时间 complete_time 「完成时间」 --time-basis complete(默认)
「8.1~9.1 产生退回的订单」「这段时间的退货退款」 售后申请时间 create_time 「申请时间」 --time-basis create
「8.1~9.1 下的单后来退货的」 订单下单时间 (订单维度,用 fetch_orders.py) 先 fetch_orders.py,勿用售后枚举硬套
  • 判据:问法里出现「完成 / 办结 / 处理完 / 结案」→ complete;
    「产生 / 有 / 发生 / 这段时间的」→ 用户若在追问售后台账,仍建议 complete(默认),要追"申请节奏"才用 create。
  • 多口径并存、按需切换:同一区间按申请时间与按完成时间各是一套数,不得用一个口径的结果去顶替另一个;
    拿不准时默认 complete 并在口径页写明,需要时两个都跑一并交付。
  • complete 口径必须放宽枚举窗口:getaftersalelist 只能按申请时间过滤(入参只有
    begin_create_time / end_create_time),所以查"完成时间落在区间"时必须把枚举窗口向前放宽,
    否则会漏掉"申请在上月、完成在本月初"的单。
    脚本已自动向前放宽 30 天(实测售后办结最长 7 天,中位 0 天);向后无需放宽(完成时间必然 ≥ 申请时间)。
  • Excel 与口径页会自动写明本次用的是哪个时间基准,退回订单汇总 表同时给出「售后时间」与「售后完成时间」两列,便于交叉核对。

★ 采集范围:仅已完成售后(--only-completed,2026-09-11 新增)

「已完成售后」的判定口径(展示层 wxshop-display 的 AFTERSALE_DONE_STATUS 必须与这里保持一致):

已完成 = status ∈ {MERCHANT_REFUND_SUCCESS 商家已退款,
                  MERCHANT_RETURN_SUCCESS 退货完成已退款,
                  PLATFORM_REFUND_SUCCESS 平台退款成功}
        且 complete_time &gt; 0
  • 用户取消(USER_CANCELD)/ 已关闭(CLOSED)/ 商家拒绝(MERCHANT_FAIL)虽然也带 complete_time,
    但属于"流程终结 ≠ 完成",不算已完成
    (实测:这三个状态的 complete_time 覆盖率 100%,极易误判)。
  • 加 --only-completed 后,过滤发生在拉订单详情之前:处理中/已取消/被拒的售后不再白拉订单详情,
    请求量显著下降。实测(2026-09-11,"今天·仅已完成"):枚举 N 个售后单 → 过滤后 N 个,
    订单详情请求从 ~227 次降到 7 次。
  • 拦截单不会因此漏掉:拦截单本身就是售后单,完成售后(退款办结)后同样进入"已完成"集合
    (实测店铺 N 笔拦截单中 N 笔已完成;其余 3 笔是"包裹拦截中/待用户确认",本就尚未办结)。
  • 工作台「采集售后」弹窗的「售后范围」选项即对应此参数,默认勾选「仅已完成售后」。

第一步:获取 access_token

  • 接口:GET https://api.weixin.qq.com/cgi-bin/token
  • 参数:grant_type=client_credential, appid=你的AppID, secret=你的AppSecret
  • 从返回的 JSON 中提取 access_token
  • 示例返回:{"access_token":"xxx","expires_in":7200}
  • 如果返回中有 errcode,说明凭证错误,请检查 AppID 和 AppSecret

第二步:拉取订单列表(增量采集)

  • 接口:POST https://api.weixin.qq.com/channels/ec/order/list/get
  • 请求头:Content-Type: application/json
  • 请求体:
{
  &quot;create_time_range&quot;: {
    &quot;start_time&quot;: &quot;从 sync_log.json 读取的上次采集时间戳(首次为7天前)&quot;,
    &quot;end_time&quot;: &quot;当前时间戳&quot;
  },
  &quot;page_size&quot;: 100,
  &quot;next_key&quot;: &quot;&quot;
}
  • 注意:时间范围最大 7 天,首次运行建议用最近 7 天
  • 时间范围字段二选一(跨度过大会返回 31042,需缩小窗口重试):
  • create_time_range(下单时间)—— 适合"拉取某段时间内新产生的订单"
  • update_time_range(更新时间)—— 适合增量同步,能捕获老订单的状态变化(发货、退款、完成);内置脚本 fetch_orders.py 用的就是这个
  • ⚠️ 两者数据量差别很大:实测 1 天窗口下,update_time_range 返回 N 单,而 create_time_range 约 N 单/天(因为前者包含状态变化的老订单,如未付款单自动关闭、发货、退款)。只想看"这段时间的新订单"必须用 create_time_range,否则会多出 4~5 倍的旧单
  • 其他可选参数:status(按状态过滤,只能取 10/12/13/20/21/30/100/250,传 200 会报 40097)、openid(按买家筛选)
  • ⚠️ 必须至少传一个时间范围,否则报 40097(即使传了 status 也不行)
  • ⚠️ 返回字段是 order_id_list(订单 ID 数组),不是 order_list
  • 分页判断:循环拉取直到返回的 has_more 为 false(next_key 为空串也可以停止),收集所有订单 ID
  • 🔴 翻页只认 has_more,绝不能用 next_key 判停(2026-09-13 实测,违反会死循环):
    带 status 过滤 + 时间范围查询时,has_more=False 的响应里 next_key 仍返回一个非空残留值(约 92 字符),
    带着 next_key 翻下一页会得到 0 单但不报错 —— 如果用「next_key 非空就继续翻」的写法,
    会在同一页无限循环。正确写法:if not d.get(&quot;has_more&quot;): break,next_key 只在 has_more 为真时才带上。
    建议再加单窗口最大页数熔断(如 50 页)兜底
  • 每页最多 N 条;请求间隔建议 ≥0.2s,避免频率限制
  • ⚠️ 门店订单量可能很大(上千单)。订单详情需逐单拉取,耗时数分钟,应将采集脚本写入 .py 文件用 run_in_background 后台运行,并每 N 单增量落盘(断点续传:已存在的 order_id 跳过)
  • ⚠️ 前台直接运行长脚本(大量网络请求循环)可能被沙箱 SIGTERM 终止且无任何输出,务必后台运行

第三步:获取每个订单的详情

对第二步得到的每个订单 ID,调用:

  • 接口:POST https://api.weixin.qq.com/channels/ec/order/get
  • 请求体:{"order_id": "订单ID"}
  • 返回结构:res.order 为订单主体,res.order.order_detail 为订单明细
  • 从返回中提取以下字段:
  • order_id(订单号)
  • status(订单状态,完整枚举见下)
  • create_time(下单时间,Unix 秒级时间戳)
  • order_detail.product_infos(商品列表,字段为扁平结构:title=商品名、sale_price=价格(分)、sku_cnt=数量、sku_attrs=规格;不是嵌套的 product.title)
  • order_detail.delivery_info.delivery_product_info[].waybill_id(发货单号)、delivery_name(快递公司)、delivery_time(发货时间)
  • order_detail.delivery_info.ship_done_time(发货完成时间,0 表示未发货)
  • order_detail.ext_info.merchant_notes(商家备注)、customer_notes(客户备注)、merchant_notes_tag_color
  • ⚠️ aftersale_detail 在 order 主体层(res.order.aftersale_detail),不在 order_detail 里
  • ⚠️ 订单 status 完整枚举(未映射会被误存为数字):
值 含义 可否作为 status 过滤参数
10 待付款 ✅
12 礼物待收下 ✅
13 一起买待成团 ✅
17 支付成功待核销 ❌(官方文档有,未实测)
20 待发货(含部分发货) ✅
21 部分发货 ✅
30 待收货(已发货) ✅
100 完成 ✅
200 全部商品售后中(整单售后中) ❌ 不可过滤(会报 40097)
250 未付款取消 / 已关闭 ✅

> ✅ API 权威枚举(2026-09-10 实测):给 order/list/get 传非法 status 时,接口会直接回显合法值 ——
> status: expecting status in [10 12 13 20 21 30 100 250] failed
> 即可过滤状态只有这 8 个;200 不在其中(它只出现在订单详情里,无法作为过滤条件),17 亦未在列。
>
> ⚠️ 实测店铺数据校验(N 单):status=200 共 N 单,N 单全部含售后单且均已支付 → 证实 200=整单售后中;status=250 共 N 单,抽样 N 单全部未支付、无售后单、无发货记录 → 证实 250=未付款取消。此二者曾分别被误记为"售后取消""已关闭",以本表为准。

  • ⚠️ 其他常用字段位置:
  • 金额:order_detail.price_info → order_price(用户实付)、product_price(商品总价)、freight(运费)、discounted_price(商家优惠)、merchant_receieve_price(商家实收,注意官方拼写是 receieve);单位均为分
  • 支付:order_detail.pay_info → pay_time(支付时间,未支付为 0)、transaction_id(支付单号)、payment_method(1=微信支付,2=先用后付,3=抽奖0元,4=积分兑换)
  • 收货:order_detail.delivery_info.address_info → user_name、province_name、city_name、tel_number
  • 备注:order_detail.ext_info → user_notes(用户备注)、merchant_notes(商家备注)
  • 商品行:product_infos[].real_price(该 SKU 实付总价)、sku_code(商家 SKU 编码)、on_aftersale_sku_cnt / finish_aftersale_sku_cnt(售后中/已完成售后数量)
  • ⚠️ 售后单优先取 aftersale_detail.aftersale_order_list[].aftersale_order_id(aftersale_order_id 顶层字段常为空);售后单 status 是字符串枚举(见下方「常见售后单 status 全量枚举」),不是数字码
  • 售后单 type:REFUND=仅退款,RETURN=退货退款
  • ⚠️ 订单详情接口不含物流轨迹/包裹状态字段 —— "包裹被退回"必须走电子面单轨迹,详见下文「物流轨迹与"包裹退回"识别」

⭐ 售后单枚举的正确做法(重要:别用笨办法)

不要为了找售后单而逐单翻几千个订单详情。官方有专门的售后单列表接口:

  • 接口:POST https://api.weixin.qq.com/channels/ec/aftersale/getaftersalelist
  • 请求体(时间区间二选一,必须成对传):
  • begin_create_time + end_create_time(按售后单创建时间)
  • 或 begin_update_time + end_update_time(按更新时间,适合增量)
  • ⚠️ 区间不得大于 24 小时,否则报 10020004 非法的时间区间,不得大于24小时 → 按天循环
  • 分页:next_key(从第二页开始传),返回 after_sale_order_id_list、has_more、next_key
  • 权限集 id:131(与订单列表相同)
  • 拿到 after_sale_order_id 后再调 getaftersaleorder 取详情

> 效率对比:实测店铺 8/1~9/1 有 N 个订单,但售后单只有几百个。用本接口按天枚举 ≈ 70 次请求,秒级完成;逐单翻订单详情需要 2600+ 次请求、约 35 分钟。
> 准确度对比:本接口能拿到下单时间在区间外、但售后发生在区间内的单,逐单翻订单会漏掉这类。

物流轨迹与"包裹退回"识别(2026-09-10 穷尽实测)

"包裹被退回"的官方标识 = 物流轨迹节点 status = 6(退回)。 轨迹节点状态枚举:

0=待揽件、1=揽收、2=运输中、3=派件、4=入柜、5=签收、6=退回、7=转寄、8=异常、9=出柜、99=其他未知

先分清两件事(2026-09-10 实测澄清,避免重复踩坑)

想读的东西 能否直读 位置
发货物流信息:运单号、快递公司、发货时间、面单编码 ✅ 能直读 /channels/ec/order/get → delivery_info.delivery_product_info[]
物流轨迹节点:揽收/运输/派件/签收/被退回 ❌ 不在订单详情里 只能走下表 A/B/C/D 之一
  • delivery_product_info[].waybill_id = 真实运单号(实测店铺如 &lt;运单号&gt;),可直接喂第三方 API。
  • delivery_info.ewaybill_order_code = 88 字符串(&lt;面单加密串...),是面单加密串,不能当 ewaybill_order_id 用(实测 → 40097 invalid args)。
  • 列表接口 /channels/ec/order/list/get 只回 order_id_list,不含运单号 → 要运单号必须逐单拉详情。
  • ✅ 实测店铺已发货订单的运单覆盖率实测 100%(5/5),D 路入参可全自动获得,无需人工整理。

核心结论:微信小店侧没有"按运单号/订单号查轨迹"的接口;全网只有 4 条路,各有硬门槛。

路径 接口 能否按运单号查 是否含"退回"标识 实测店铺可用性
A 小店面单详情 /channels/ec/logistics/ewaybill/biz/order/get ❌ 需 ewaybill_order_id(Uint64) ✅ status=6 有权限,但拿不到 ID
B 小店轨迹推送 事件 ewaybill_push_path — 被动推送 ✅ status=6 需配回调 URL(要服务器)
C 微信物流助手 /cgi-bin/express/business/path/get ✅ delivery_id+waybill_id ❌ 枚举里没有"退回" ❌ 48001
D 第三方快递 API 快递100/快递鸟/阿里云等 ✅ waybill_id+delivery_id ✅ 文案含"退回" ✅ 需注册 key

A 的细节:传真实运单号/订单号 → 10025010 电子面单订单不存在(业务错误 ≠ 无权限);传 ewaybill_order_code 字段名 → 40097 invalid args(字段不被接受)。即:必须 Uint64 数字面单 ID,而订单接口只给不透明的 88 字符加密串,无法反解。已把电子面单全部 N 个接口 + 物流发货分类全枚举过,确认没有"按运单号/订单号反查面单"的接口。

B 的细节:waybill_info 携带 ewaybill_order_id(UINT64) + waybill_id + status + desc。这是唯一能合法拿到 ewaybill_order_id 的入口 —— 配好回调后,既拿到实时退回事件,也拿到 ID,之后可随时调 A 查完整轨迹。文档:https://developers.weixin.qq.com/doc/store/shop/notify/merchant_callback/ewaybill_push_path

C 的细节:action_type 合法值仅 100001揽件成功、100002揽件失败、100003分配业务员、200001更新运输轨迹、300002开始派送、300003签收成功、300004签收失败、400001订单取消、400002订单滞留 —— 无"退回"节点,退回只能靠 action_msg 文本匹配。且适用范围为"小程序(仅认证)",微信小店 access_token 调用直接 48001。若商家名下有已认证小程序,可用其 access_token 调用。

D 的细节:微信小店订单里已含 delivery_product_info[].waybill_id + delivery_id,
直接可作为第三方接口入参,可回溯历史、无需小程序。
实测(9/1~9/2 共 N 单):已发货订单运单覆盖率 23/23 = 100%;
快递公司以中通 ZTO 为主,但出现过顺丰 &lt;运单号&gt;(手工发货,非电子面单渠道)。

实测店铺实况(可作为环境基线):/channels/ec/logistics/ewaybill/biz/account/get 返回 4 个网点账号、shop_id=&lt;SHOP_ID&gt;、/delivery/get 仅列出中通 ZTO。
⚠️ 但"已开通列表"不等于"每单都走电子面单" —— 实测存在顺丰手工发货订单(&lt;运单号&gt;),
这类单没有 ewaybill_order_id,路径 A/B 对其无效,只能走 D 或 C。

已验证不存在的路径(不要浪费时间,40066 invalid url 是可靠的端点存在性探针):
- 第一轮 5 个:/channels/ec/logistics/ewaybill/biz/order/list/get、/getbywaybill、/channels/ec/logistics/trace/get、/channels/ec/order/ewaybilltrack/get、/channels/ec/order/logistics/get
- 第二轮 N 个(2026-09-10 穷举):ewaybill/biz/order/{getbyordercode,getbyewbcode,getbywaybillid,trace/get}、ewaybill/biz/trace/list/get、order/waybill/get、order/delivery/trace/get、order/logistics/trace/get、logistics/delivery/trace/get、logistics/express/trace/get、order/getshipsinfo、order/deliveryinfo/get、logistics/querytrace/get、logistics/trace/query
- /cgi-bin/express/delivery/open_msg/get_delivery_list、trace_waybill → 48001

> 累计 N 个端点名验证完毕,结论可以定死:小店开放接口无轨迹能力,除 A(面单详情)外别无他法。 完整清单见 references/api.md §5。

自己动手验证端点(scripts/probe_endpoints.py) —— 怀疑有未文档化的接口时用它,别靠猜:

python &quot;&lt;skill目录&gt;/scripts/probe_endpoints.py&quot; --config config.json \
       --endpoint /channels/ec/logistics/ewaybill/biz/order/get \
       --endpoint /channels/ec/order/logistics/get \
       --param order_id=&lt;订单号&gt;
# 路径较多时写成文件(每行一个),用 --endpoint-file urls.txt
  • 判定口径:40066 invalid url = 端点不存在;返回业务错误码(如 10025010 面单不存在、40097 参数错)= 端点存在,只是参数/权限问题。这是最省事的"接口存不存在"探针
  • ⚠️ Windows 下 --config / --endpoint-file 必须写 Windows 形式路径(把 /tmp/x 传给原生 Python 会 FileNotFound)

实践结论(两条路均已落地为脚本,见下两节):
- 回溯历史 → 默认靠「售后单 WAIT_PACKAGE_INTERCEPT」+「商家备注含"拦截"」两个信号;要更硬的证据就上 D(第三方轨迹):scripts/trace_query.py
- 要实时精确(含"包裹被退回"铁证)+ 想拿到 ewaybill_order_id → 配 B(回调):scripts/ewaybill_callback.py,捕获后用 A 回溯该单完整轨迹
- 轨迹类判定无法纯靠小店 API 批量回溯,这是接口设计限制,不是取数方式问题

路径 B 落地:物流轨迹推送回调服务(scripts/ewaybill_callback.py,已内置并自检通过)

这是免费拿到"包裹被退回"官方铁证的唯一途径,同时也是获得 ewaybill_order_id 的唯一入口(有了它才能用接口 A 回溯完整轨迹)。

官方机制(已严格按文档实现,--selftest 端到端 5 项全通过):

环节 规则
后台配置 微信小店后台 →「服务市场」-「自研」-「消息推送」;填 URL(必须 http 80 或 https 443,需公网可达)、Token、EncodingAESKey(43 位)
固定项 消息加解密方式只能「安全模式」、数据格式只能 JSON,均不可选
URL 验证 微信发 GET,带 signature/timestamp/nonce/echostr;signature = sha1(字典序排序(token,timestamp,nonce)拼接);验签通过后原样返回 echostr
推送 POST,URL 带 signature/timestamp/nonce/openid/encrypt_type=aes/msg_signature;包体 {&quot;ToUserName&quot;:..., &quot;Encrypt&quot;:&quot;...&quot;}
验签 msg_signature = sha1(字典序排序(token, timestamp, nonce, Encrypt)拼接)。⚠️ 用 msg_signature,不是 URL 上的 signature
解密 AESKey = base64decode(EncodingAESKey + &quot;=&quot;)(32 字节);AES-256-CBC + PKCS#7;IV = AESKey 前 16 字节;FullStr = random(16B) + msg_len(4B 网络字节序) + msg + appid,需校验 appid
回包 明文回 success 即可,无需加密

推送体业务字段:waybill_info.{ewaybill_order_id, waybill_id, update_time, status, desc},status 枚举与轨迹节点一致(6=退回)。

落库产物(默认在 ~/.workbuddy/data/wxshop/):
- ewaybill_events.jsonl —— 原始推送流水(追加,不覆盖)
- waybill_index.json —— 按运单号聚合,含 is_return、ewaybill_order_id、history[]、order_id(自动关联本地订单数据)

PY=&quot;&lt;python&gt;&quot;; S=&quot;&lt;skill目录&gt;/scripts/ewaybill_callback.py&quot;
&quot;$PY&quot; &quot;$S&quot; --selftest --token t --aeskey &quot;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&quot;   # 自检,不依赖公网
&quot;$PY&quot; &quot;$S&quot; --config assets/callback_config.json --serve                                  # 启动常驻服务
&quot;$PY&quot; &quot;$S&quot; --config assets/callback_config.json --list                                   # 看已捕获的退回运单

⚠️ 部署前提:需要一台公网可达的服务器(80/443)。也可用内网穿透(frp / ngrok / cpolar)把本机端口映射出去。自检不需要公网,可先验证链路。
⚠️ 免踩坑:encoding_aes_key 必须是 43 位(base64 字符集);Token 必须与后台完全一致,否则 URL 验证就过不了。

路径 D 落地:第三方物流轨迹批量查询(scripts/trace_query.py,已内置并验证)

三后端可插拔,判定退回按三档输出(是 / 疑似 / 否),并给出判定依据便于人工复核:

后端 入参 是否可回溯历史 退回依据
kuaidi100 waybill_id + com 编码 ✅ state ∈ {4 退签, 6 退回, 14 拒签} 或轨迹文案命中关键词
kdniao waybill_id + ShipperCode ✅ state=4(问题件)→ 疑似;文案命中 → 是
wxshop ewaybill_order_id(须字符串) 仅回调捕获过的单 轨迹节点 status=6

关键实现细节与坑(都已实测):
- 快递100 怎么开通拿 key(照着做 3 步):
1. 打开 https://api.kuaidi100.com/ 注册账号(手机号即可)→ 完成实名认证(个人认证即可)
2. 控制台开通 「实时查询与订阅推送」→「实时查询」(开店即有免费试用额度)
3. 进入 「实时查询与订阅推送」→「概览」→「授权参数」,页面上直接给出两项凭据:
授权key(形如 kJ****63)= 配置里的 key,customer(形如 07****7A)= 配置里的 customer
→ 拿到后填进 assets/trace_providers.json 的 kuaidi100.{customer,key},provider 设为 kuaidi100 即可跑
- 快递100:sign = MD5(param + key + customer).upper();param 是 JSON 字符串 {&quot;com&quot;:&quot;zhongtong&quot;,&quot;num&quot;:&quot;...&quot;,&quot;resultv2&quot;:&quot;1&quot;}。⚠️ 中通/顺丰 phone 必填 —— 而小店接口的手机号是脱敏的且无解密接口,所以直接填店铺自己的发货人手机号(官方允许收/寄件人任填一个;2026-09-17 复测:中通单带发货人后四位→8 节点全轨迹,申通实测不需要 phone,带上也不碍事);⚠️ 同一单号查询间隔 ≥30 分钟,否则锁单(脚本默认缓存 TTL 1800s)
- ⚠️ 小店官方 delivery_id(ZTO/STO/YTO/YUNDA/SF/EMS/JD/POST…)不能直传快递100 的 com —— 直传报「参数错误」,必须映射成小写码(ZTO→zhongtong、STO→shentong、YTO→yuantong、YUNDA→yunda、SF→shunfeng、EMS→ems、JD→jd、POST→youzhengguonei、HTKY→huitongkuaidi…)。认不出的代码用中文名猜("中通快递"含"中通"→zhongtong)再 lower() 兜底;完整映射表实装在 &lt;本地映射表模块&gt; 的 WX_COM_MAP(2026-09-17 实测:官方 ZTO 直传报错、映射 zhongtong 后 ok)。⚠️ 与快递鸟相反(下条)——别把两家的映射规则搞混
- 快递100 最坑的一点:customer(授权码)无效时,对任意 com 编码都统一返回 {&quot;result&quot;:false,&quot;returnCode&quot;:&quot;400&quot;,&quot;message&quot;:&quot;找不到对应公司&quot;} —— 连官方文档示例的 yuantong 也一样报。所以看到这句先查 customer/key,不要去改快递公司映射表
- 快递鸟:DataSign = base64(MD5(RequestData + AppKey)),RequestType=1002,DataType=2;其 ShipperCode 多为大写且与微信小店 delivery_id 基本一致(中通都是 ZTO),映射成本更低
- 误判防护:脚本内置排除规则,像"如拒收将退回发件人"这类通知性文案不会被判为退回(已单测覆盖)

✅ 标准三步流程("查某时间段产生退回的订单"的推荐跑法)

S=&lt;skill目录&gt;/scripts

# ① 售后管理范围内枚举 + 补订单详情(拿运单号)——约 4 分钟,含运单号
python $S/fetch_aftersales.py --config config.json --start 2026-08-01 --end 2026-09-01 \
       --out ./wxshop_out --excel

# ② 物流轨迹验证(先只验拦截退货单最省配额;要全覆盖就把 --only-intercept 换成 --only-aftersale)
python $S/trace_query.py --config trace_providers.json --provider kuaidi100 \
       --source ./wxshop_out --only-intercept --excel --out ./trace_out

# ③ 合并成终版报告(自动标出&quot;备注法漏掉&quot;的单)
python $S/merge_trace_report.py --orders ./wxshop_out/orders.json \
       --aftersales ./wxshop_out/aftersales.json --trace ./trace_out/trace_results.json \
       --start 2026-08-01 --end 2026-09-01 --out ./产生退回的订单_含物流轨迹验证.xlsx

省配额经验:实测店铺 8/1~9/1 有 N 个涉售后订单,其中已发货仅 N 个有运单号 →
--only-intercept 只查 5 条、--only-aftersale 查 N 条。
要"准确"就用 --only-aftersale(N 条也能查出备注漏掉的单);只做定点复核用 --only-intercept。

PY=&quot;&lt;python&gt;&quot;; T=&quot;&lt;skill目录&gt;/scripts/trace_query.py&quot;
cp &quot;&lt;skill目录&gt;/assets/trace_providers.example.json&quot; tp.json   # 填入密钥
&quot;$PY&quot; &quot;$T&quot; --config tp.json --provider kuaidi100 --source &lt;订单数据目录&gt; --only-aftersale --dry-run   # 先干跑看请求
&quot;$PY&quot; &quot;$T&quot; --config tp.json --only-aftersale --out ./trace_out --excel                               # 实跑并出表

省配额开关(实测店铺 8/1~9/1 实测口径):售后范围内涉单 N 个,其中已发货(有运单号)仅 N 个。

开关 实际查询量 适用场景
--only-intercept 5 条(仅备注判定为拦截的单) 只为复核已知拦截单,最省配额
--only-aftersale N 条(所有已发货的涉售后单) ⭐ 要"准确/不漏单"必须用这个 —— 备注漏掉的单只有在这里才会被查出来
--order-id xxx 指定条数 定点复核某几单
  • 结果缓存于 trace_out/trace_cache.json,支持断点续跑;--refresh 强制重查
  • 出表含 4 个 sheet:汇总 / 全部运单 / 判定退回(红底标出)/ 查询失败
  • --dry-run 可先看将发出的请求(含运单号与快递编码)再决定是否实跑

⭐ 路径 D 实战记录(2026-09-10,实测店铺真实凭据,全链路已验证)

项目 实测结果
凭证 / 签名 / phone ✅ 全部通过,无一次失败(400 找不到对应公司 / 503 签名失败 均未出现)
已发货售后订单查询 21/21 成功,0 失败,总耗时约 23 秒(--only-aftersale;限速 0.6s/次)
命中「包裹被退回」 7 单(5 单原备注含"拦截" + 2 单备注未标)
返回的典型铁证文案 快件已在 XX 被退回签收,签收人:退回商家/发件人(state=4 退签)
退货退款 8 单的原发货单 全部显示"已签收" → 属正常(买家另寄回),报告中标「不适用」

结论:--only-aftersale 21 次请求、23 秒,就能把整个区间的退回单一次性验证完 —— 成本极低,没有任何理由跳过轨迹验证。

拦截退货(物流拦截退回)识别 —— 关键

拦截退货在售后单里表现为"仅退款"(type=REFUND),无法只靠 type 区分。可靠信号按优先级:

  1. ⭐ 物流轨迹 = state ∈ {4 退签, 6 退回, 14 拒签},或轨迹文案含"退回"(如"快件已被退回签收,签收人:退回商家/发件人")
    —— 这是唯一权威判据,必须优先用它。实现:scripts/trace_query.py(快递100 已跑通,见下文)
  2. 售后单 status = WAIT_PACKAGE_INTERCEPT(包裹拦截中) —— API 层最直接的拦截标识(示例售后单 <售后单号>),但实测很少出现
  3. 商家备注 order_detail.ext_info.merchant_notes 含"拦截" —— 店铺实际写法:"已发货拦截"、"已发货,拦截退回",且 delivery_info.ship_done_time &gt; 0。⚠️ 仅作辅助,会漏单
  4. 辅助:已发货 + 仅退款成功 + return_type=0(无退回物流),即拦截成功后货品由快递退回商家

> 🔴 实测校准(2026-08-01~09-01,已用快递100 逐单验证 N 个已发货售后订单)
>
> | 方法 | 识别到的退回单 |
> |---|---|
> | 商家备注法(旧) | 5 单 |
> | ⭐ 物流轨迹法 | 7 单 |
> | 备注法漏掉 | 2 单 —— 备注分别写 "取消寄件" 和 空白,物流轨迹明确显示"被退回签收" |
>
> 结论:只靠商家备注会漏约 30%(2/7)。凡是要"准确",必须跑物流轨迹验证。
> 漏单的两种形态:① 备注写"取消寄件"(商家主动召回) ② 备注完全空白(运营忘记标注)。
>
> ⚠️ 另注意区分:退货退款单查原发货单号一定会显示"已签收" —— 因为买家是收到货之后才申请退货,
> 货是买家另行寄回的(看售后单的 return_info.waybill_id)。所以这类单标「不适用」,不能算"未退回"。
> 8/1~9/1 实测 8 单退货退款的原发货单轨迹全部为"已签收",完全符合预期。

⚠️ 拦截失败时平台会自动把售后类型改成退货退款(引导用户退货),因此存在"既在拦截备注里、又是 RETURN 类型"的订单,统计时需去重(退回类型记为"退货退款+拦截退货")。

「退回类型」列的取值(与 fetch_aftersales.py / merge_trace_report.py 保持一致,勿再自创写法):

取值 含义
退货退款 存在 type=RETURN 的售后单
拦截退货 无退货退款售后,物流轨迹确认包裹被退回
退货退款+拦截退货 两者都成立(既有退货退款售后,轨迹也显示被退回)
拦截退货(备注定) 仅商家备注含"拦截",未做轨迹验证(无 key 时的降级结果,可能漏)
后缀 ←备注未标拦截 ⚠️ 该单备注没写"拦截",是靠物流轨迹才发现的(就是被备注法漏掉的那些单)

⚠️ 参考样本:订单 <订单号>(merchant_notes="已发货拦截",售后为 REFUND 无物流、已退款,运单 <运单号> 中通)

✅ 快递100 已验证可用(2026-09-10):实测店铺 N 个已发货售后订单逐单查询,0 失败;
5 单拦截退货全部拿到铁证"快件已被退回签收,签收人:退回商家/发件人"。配置见 assets/trace_providers.example.json(phone 填店铺寄件人 <发货人手机号>)。

⚠️ 采集时必须覆盖所有订单状态的售后单:早期只取 status=200/250 的订单会漏掉约 10% 的售后单(如已发货(30)状态下的拦截/退货售后单)。应以订单详情的 aftersale_detail.aftersale_order_list 为准,逐单遍历所有状态。

⚠️ 拦截证据必须"落盘固化":一次拦截,永久拦截(2026-09-11 新增,勿回退)

为什么必须做:拦截的三档证据全都会随时间变化 ——

证据 为什么会消失
售后单 status=WAIT_PACKAGE_INTERCEPT(包裹拦截中) 售后一办结就翻成 MERCHANT_REFUND_SUCCESS(商家已退款),"拦截中"这个状态永久消失
商家备注含"拦截" 运营改备注 / 清理备注,历史备注不在接口里留痕
物流轨迹 第三方 key 过期后无法回溯查询(且需另配 key)

证据一消失,订单就从「物流拦截」里凭空掉出来 → 表现为同一筛选条件,上午查有、下午查没有。
(实测:全库 N 单拦截全部靠商家备注认定,售后状态法命中 0 单 —— 说明这条链路非常脆弱。)

现行做法:工作台每次采集收尾都会调用 stamp_evidence(),把已经看到过的证据写进归档:

  • 订单表:merchant_notes(含顶层与 ext_info)含"拦截" → 写 _intercept_ever=true、_intercept_src=&quot;商家备注含“拦截”&quot;
  • 售后表:is_intercept=true 或 status 含 INTERCEPT/拦截 → 写 _intercept_seen=true
  • 合并时这些标记只增不减;展示层再做 OR。只写一次,永久有效。

> 给本技能脚本改采集落盘逻辑时,不要删除或覆盖这些下划线开头的证据字段(_intercept_ever /
> _intercept_seen / _intercept_src)。它们不是缓存,是"结果只会更全"的历史事实。
>
> 同理:工作台侧的落盘一律走原子写(临时文件 → fsync → os.replace,并保留 .bak),
> 且读失败时先回退 .bak,绝不把"读不出来"当成"没有数据" —— 旧写法曾因读失败返回空列表,
> 调用方随即用空列表覆盖写回,把整份归档清空。

常见售后单 status 全量枚举(实测)

WAIT_PACKAGE_INTERCEPT=包裹拦截中、USER_WAIT_RETURN=待买家退货、MERCHANT_WAIT_RECEIPT=待商家收货、USER_WAIT_CONFIRM_UPDATE=待用户确认协商、MERCHANT_RETURN_SUCCESS=退货完成已退款、MERCHANT_REFUND_SUCCESS=已退款、MERCHANT_FAIL=商家拒绝、USER_CANCELD=用户已取消、CLOSED=已关闭

> ⚠️ 作废终态 ≠ 产生退回(2026-09-13 展示层踩坑回写):类型 RETURN(退货退款)+ 状态
> USER_CANCELD/CLOSED/MERCHANT_FAIL 的售后没有任何实际退回动作(买家取消了售后,货正常签收),
> 判定"是否产生退回"必须先排除这些作废终态再数类型——实例订单 &lt;订单号&gt;。
> 本技能 fetch_aftersales.py 已用 valid(排除 EXCLUDE_AS_STATUS)判定,展示层
> display_query.py 的 is_return 也已于 2026-09-13 同步修复(排除 AFTERSALE_VOID_STATUS)。

> 📊 实测样本(2026-09-10,实测店铺 6/1~9/10 共 N 个售后单):
> - 按类型:REFUND(仅退款)N 个、RETURN(退货退款)N 个 —— 仅退款占 77%,退货退款仅 23%
> - 按状态:MERCHANT_REFUND_SUCCESS 954、MERCHANT_RETURN_SUCCESS 223、USER_CANCELD 57、MERCHANT_FAIL 11、MERCHANT_WAIT_RECEIPT 7、USER_WAIT_RETURN 3、USER_WAIT_CONFIRM_UPDATE 2、WAIT_PACKAGE_INTERCEPT 1
> - 可见"已完成"的售后占绝大多数,WAIT_PACKAGE_INTERCEPT(包裹拦截中)是瞬时状态,只在拦截进行中的短暂窗口内可见,历史查询几乎抓不到 → 拦截退货不能只靠它识别

> 同一订单可能有多个售后单(如先申请退货后取消、再申请仅退款),统计退款金额时要排除 USER_CANCELD 的售后单,避免重复计费。

第四步:获取售后单详情(如果订单有售后)

如果第三步返回中存在 aftersale_detail:

  • 接口:POST https://api.weixin.qq.com/channels/ec/aftersale/getaftersaleorder
  • 请求体:{"after_sale_order_id": "售后单号"}
  • ⚠️ 返回键名是 after_sale_order(下划线分隔),不是 aftersale_order
  • type 字段:REFUND=仅退款,RETURN=退货退款
  • status 是字符串枚举:WAIT_MERCHANT_CONFIRM=待商家审核,WAIT_BUYER_RETURN=待买家退货,WAIT_MERCHANT_RECEIVE=待商家收货,MERCHANT_REFUND_SUCCESS=商家已退款,MERCHANT_RECEIVE_SUCCESS=商家已收货,CLOSED=已关闭
  • 退款金额在 refund_info.amount(单位:分),售后原因代码在 refund_info.refund_reason,原因文案在 reason_text
  • return_info.return_type(退回方式):0=无物流,1=自行寄回,2=上门取件;退货单号在 return_info.waybill_id,快递在 return_info.delivery_name
  • ⚠️ 拦截退货不体现在 return_info 里(不会出现"拦截"字样),但售后单 status 会出现 WAIT_PACKAGE_INTERCEPT(拦截中);已完成的拦截则表现为仅退款成功 + 商家备注含"拦截",见上文「拦截退货识别」
  • 完整字段:after_sale_order_id, status, order_id, openid, product_info, details(receive_product=用户是否收到货), refund_info, return_info, merchant_upload_info, create_time, update_time, reason, reason_text, type, sub_type, event, gen_scene, complaint_id, complete_time, need_offline_refund, refund_resp

第五步:存储数据到本地 JSON

存储路径:~/.workbuddy/data/wxshop/

  • orders.json:[{"order_id":"xxx","status":30,"create_time":"2026-01-01 12:00:00"}, ...]
  • order_products.json:[{"order_id":"xxx","product_name":"商品名","sku_spec":"颜色:红;尺寸:L","price":99.9,"quantity":1}, ...]
  • deliveries.json:[{"order_id":"xxx","waybill_id":"SF123456","delivery_id":"顺丰速运","deliver_time":"2026-01-02 10:00:00"}, ...]
  • aftersales.json:[{"order_id":"xxx","after_sale_order_id":"AS123456","status":"退货中","type":"RETURN","is_intercept":true,"return_waybill_id":"SF123456"}, ...]
  • sync_log.json:{"last_sync_time": 时间戳}

去重规则:以 order_id 为主键,已存在则更新,不存在则新增。

第六步:输出统计

采集完成后,输出:

  • 本次采集订单总数
  • 新增订单数
  • 有售后的订单数
  • 退货退款订单数(type=RETURN)
  • 拦截退货 / 包裹退回订单数 —— 按优先级判定:① 物流轨迹 state ∈ {4,6,14} 或文案含"退回" > ② 售后 status=WAIT_PACKAGE_INTERCEPT > ③ 商家备注含"拦截"(仅辅助,实测会漏约 30%,见「拦截退货识别」)
  • 产生退回订单总数(两类去重后的并集)

> ⚠️ 不要把备注法当成最终结果。有快递100 凭据时,必须跑 trace_query.py --only-aftersale + merge_trace_report.py 出终版;
> 无凭据时才降级用备注法,并在口径页注明"可能漏单"。

交付模板("产生退回的订单")

用户要"产生退回的订单"时,首选 merge_trace_report.py 产出的「物流轨迹验证版」(3 表):
1. 退回订单汇总:退回类型、订单号、售后单号、下单/售后时间、订单状态、商品、数量、退款金额、快递公司、发货单号、退回单号 / 退回快递 / 退回单号来源、售后类型、售后状态、退回方式、售后原因、商家备注、物流轨迹验证、轨迹状态、退回依据 + 合计行(轨迹新发现的单黄底标出)
2. 物流轨迹铁证:逐单号列出 退回判定 / 轨迹状态 / 最新轨迹文案 / 时间 / 节点数(判定为退回的红底)
3. 口径与边界说明:时间基准、数据来源、判定优先级(轨迹 > 售后类型 > 备注)、「退回单号」取法、「物流轨迹验证」列怎么读、备注法漏了几单

⭐「退回单号」列 —— 用户明确要求的字段,两种退回来源取法不同,别搞混

退回类型 「退回单号」取什么 「退回单号来源」标
退货退款 买家寄回的快递单号(售后单 return_info.waybill_id),如 &lt;运单号&gt; 买家寄回
拦截退货 / 拒收退回(物流轨迹确认被退回) 退回的就是原发货单号(即「发货单号」列那个号) 原发货单退回
两者都成立(退货退款+拦截退货) 两个都给:买家寄回号 / 原发货单号 买家寄回 + 原发货单退回
无退回单号(如退货申请被取消) 空 空
  • 买家寄回的快递公司取售后单 return_info.delivery_name;⚠️ 可能与寄出时不是同一家(实测有中通寄出、顺丰寄回)
  • 一个订单有多次退货时,多个单号用 / 连接去重
  • 两版报告均已内置该列,口径一致:merge_trace_report.py(轨迹验证版)、fetch_aftersales.py(4 表降级版)
  • ⚠️ 数据源差异:4 表降级版没有轨迹,其「拦截退货」只能靠备注判定 → 给出的原发货单号未经轨迹复核

> 「物流轨迹验证」三态很重要,别用简单的"是/否":
> 是(包裹被退回) / 不适用(原单已签收,退货走买家寄回单) / 否(正常送达) / 未验证。
> 退货退款单查原发货单号必然显示"已签收"(买家收到货才退货),标成"否"会误导。

> ✅ 实测店铺已有可用凭据(assets/trace_providers.json),所以"查退回订单"默认一律走轨迹验证版 —— 21 次请求 / 23 秒,成本可忽略,没有理由跳过。
> 仅当第三方 key 不可用时才降级用 fetch_aftersales.py 的 4 表版,但必须在口径页注明"拦截退货依据商家备注判定,可能漏单(实测漏 30%)"。

指定时间段查询的推荐流程(最快最准)

用户说"查 X 月 X 日~Y 月 Y 日产生退回的订单"时,按以下顺序做:

  1. 先定时间基准(第零步「时间基准三选一」):用户说"完成/办结"→ --time-basis complete(默认);
    明确指"申请/发起"才用 create。售后管理场景再加 --only-completed 只采已完成售后。
  2. 用 getaftersalelist 按申请时间按天枚举窗口 → 得到全部售后单号(complete 口径下窗口需向前放宽,脚本已自动处理)
  3. 批量调 getaftersaleorder 取详情 → 按所选时间基准落在区间内筛选(create_time 或 complete_time);
    --only-completed 时在这里先过滤掉非已完成售后,再进入第 3 步(省请求)
  4. 取这些售后单对应的 order_id(只需几十~几百个),逐个调 order/get 拿商品、商家备注、发货运单号
  5. 分类:type=RETURN → 退货退款;商家备注含"拦截" → 拦截退货(辅助);排除 USER_CANCELD
  6. ⭐ 用运单号跑 trace_query.py 做物流轨迹验证(这才是不漏单的关键)→ 再跑 merge_trace_report.py 出终版
  7. 生成 Excel(必含口径说明页,写明时间基准与采集范围)

一条命令即可完成 1~4 + 出表:

# 完成时间口径(默认,自动向前放宽枚举窗口 30 天)+ 仅已完成售后(推荐)
python fetch_aftersales.py --start 2026-08-01 --end 2026-09-01 --out ./out_done --excel --only-completed
# 申请时间口径(要追&quot;申请节奏&quot;时用)
python fetch_aftersales.py --start 2026-08-01 --end 2026-09-01 --out ./out --excel --time-basis create
# 全部售后(含处理中/已取消/被拒,排查用)
python fetch_aftersales.py --start 2026-08-01 --end 2026-09-01 --out ./out_all --excel
# 轨迹验证版合并(--time-basis 须与采集一致)
python merge_trace_report.py --orders ./out_done/orders.json --aftersales ./out_done/aftersales.json \
    --trace ./trace_out/trace_results.json --start 2026-08-01 --end 2026-09-01 --time-basis complete --out ./终版.xlsx

> ⏱️ 对比:实测店铺 8/1~9/1 用此法只需 ~108 次订单详情请求(约 4 分钟)+ 21 次轨迹查询(约 23 秒);
> 若按"先拉区间内所有订单(N 单)再逐单找售后",需 2600+ 次请求、约 35 分钟,且会漏掉"下单在区间外、售后在区间内"的单。
> ⚠️ 但备注法本身会漏:8/1~9/1 备注法 N 单 → 轨迹验证后 N 单(多出 2 单备注没写"拦截")。

金额列用 ¥#,##0.00 格式;日期列 YYYY-MM-DD HH:mm;明细 ≥10 行开筛选、冻结表头。
⚠️ 合计行金额建议写静态值:本机无 LibreOffice、无 formulas 包时 recalc 无法产出缓存值,公式会在预览中显示空白。

交付模板(销售分析 / 订单导出)

用户要"订单导出""销售数据""GMV 统计"时,交付一张 Excel,含 2 个工作表(scripts/fetch_orders.py 已内置此模板,可直接用脚本产出):

  1. 订单明细:每 SKU 一行,含订单号、订单状态、下单/更新时间、支付时间与方式、实付金额、商品总价、运费、商家优惠、商家实收、收货省市、买家 openid、用户备注、商家备注、商品标题、SKU 属性、数量、单价、SKU 实付总价、商家 SKU 编码
  2. 汇总分析:
    - 每日概况(按支付时间统计支付订单数、支付件数、销售额)
    - 订单状态分布
    - 商品 TOP10(按实付金额)
    - 每日销售额柱状图

统计口径:GMV 按"支付时间"归集,未支付 / 已取消订单不计入;表头深蓝底白字加粗、冻结首行、明细 ≥10 行开筛选。

> ⚠️ 重要口径陷阱:fetch_orders.py 用的是 update_time_range,所以它的「每日概况」是按支付日期汇总所有被同步到的订单,其中包含很久以前下单、只是近期状态变化的旧订单(实测:1 天窗口拉回 N 单,支付日期跨度从 4 月到 9 月)。
> - 若用户问的是「这段时间卖了多少」,必须用 create_time_range 重新拉,或按"下单时间"过滤后再统计,否则 GMV 会被历史订单撑大
> - 若用户问的是「当前整体状态/累计」,用 update_time_range 的结果是对的
> - 汇报时必须说明口径,不要直接把脚本输出的合计数当作"当日/当周销售额"

错误码速查

错误码 含义与处理
40001 / 42001 access_token 失效 → 重新获取后重试
40013 / 40125 appid 或 secret 无效 → 检查凭据
45009 频率限制 → 降速重试(脚本内置 sleep 重试)
31042 请求内订单过多 → 缩短时间范围(≤7 天)重试
40097 请求体参数不正确。两种典型:① invalid args, at least one of create_time_range, update_time_range should be given → 没传时间范围(这是最常见原因,不是 status 的问题);② status: expecting status in [...] → status 值非法(可过滤值见上文状态表)
47001 请求体格式 / 类型不正确
606006 next_key 与上一页请求参数不一致 → 分页时其余参数必须保持不变
48001 api unauthorized(如微信物流 trace_waybill 未开通)

数据安全

订单明细含买家 openid、收货人姓名/电话/地址等个人信息:

  • 仅保存在用户本地输出目录(~/.workbuddy/data/wxshop/ 或脚本 --out 指定目录),不要外发或上传到对话中
  • 向用户展示汇总结果时默认脱敏(openid 截断、不展示收货人姓名与电话)
  • 凭据按用户要求以明文保存在本文件"店铺配置",注意本文件不要分享给他人

📌 最新结论速查(滚动更新,最新在最上)

2026-09-20 · 首次出现「当日截至 16:02 五店零发货」——批次未开始,非故障(用「同点位历史对照」判真伪)

  • 结果:待发货 N(店铺A 28 / 店铺D 3 / 其余 0);今日已发货 5 店全 0;
    面单余额 N(店铺E中通,连续 3 日未变);detail_errors 全 0;飞书 code=0、企微 errcode=0 推送成功。
  • 「0 发货」如何排除故障(本次新增判据,很重要):当「今日已发货」为 0 时,自比自(重跑)证明不了任何事。
    本次采用两条独立证据:
    1. 同点位历史对照:09-18 / 09-19 在 15:51~15:57 均有批量发货(124 / N 单),本日同一时段窗口内为 0;
    2. 流转健康度:店铺A本日 status∈{30,100} 流转 N 单、详情 err=0,逐单 ship_done_time 全部落在历史日期
    (即"旧单今天确认收货/更新",不是新发货),证明 token、列表接口、详情接口、字段映射全部正常。
    ⇒ 结论:该店本日还没开始打单(周日节奏),待发货从昨日 0 累积到 28 属真实积压增长,需次日跟进是否补发。
  • 四次测量全一致:daily_snapshot.py 主跑 15:53 与复核跑 15:59 逐项相同;
    ship_today_final.py 窗口至 15:56 与 16:02 两次均 0。→ 未标注「⚠️ 数据待核验」。
  • 流程照旧(09-18 定的四步):主跑 --no-push → 末窗 ship_today_final.py → 复核跑取新鲜待发货 → push_YYYYMMDD.py 推送。
  • 新增资产:out/push_20260920.py(note 里的 flow_scanned 改为从 ship_today_final.json 动态取,不再手写)。

2026-09-18 · 🔴 批量发货窗口缺陷第二次实证:同一口径 15:51 得 N 单、15:58 得 N 单(差 2.6 倍)

  • 实测:今天批次 ship_done_time 集中在 15:50(N 单)与 15:55(N 单)两个批次。
    daily_snapshot.py(15:51 起跑,窗口末端紧贴运行时点)读店铺A「今日已发货」N;
    末窗脚本(窗口 00:00~15:58:37 + 尾窗补漏)读 N;16:07 再跑 daily_snapshot.py 也读 N
    (此时窗口已覆盖批次)→ 47 是结构性低估,124 为真值。与 09-17 同一缺陷、同一形态。
  • ★ 算术闭合验证法(新增,最省事的自证):主跑启动时待发货(77) + 主跑已发货(47) = 末窗已发货(124)。
    两轴互证,一次算清,不必再翻明细。
  • 待发货「大额归零」是真值不是故障:15:36 那轮 N 单 → 16:03 已 0 单(随批次清空)。
    核验:create_time_range / update_time_range 双口径近 90 天均 0、26 窗口 err=0;
    近 3 天各状态流转正常(店铺A 30:634 / 100:1993 / 250:552)→ token 有效、status 过滤正常。
  • 新增可复用脚本(工作目录 out/):
    ship_today_final.py(★ 末窗两段并集:主窗 + 详情耗时期间新发货的尾窗补漏,按 order_id 去重,最可靠)、
    pending_recheck_20260918.py(90 天待发货 list-only 快速复核,~30s)、
    pending_dual_90d_20260918.py(双口径 90 天待发货 + 近 3 天状态分布,~1.5min)、
    push_20260918.py(读 daily_snapshot.json 的待发货/面单余额 + ship_today_final.json 的今日已发货,出 Excel + 双通道推送)。
  • 仍建议尽早改 daily_snapshot.py:把「今日已发货」的列表窗口末端在每个查询时重新取 time.time(),
    并在详情阶段后再补扫一次 (T1, T2] 取并集 —— 否则每日 15:50 那次定时跑的「今日已发货」会长期低报(今天低报 N 单)。

2026-09-17 晚 · token 字段名/快递中文名/轨迹接口/详情页反爬 四连结论(AI客服项目实测)

  • /cgi-bin/stable_token 请求体字段名必须是 secret:传 appsecret(照文档字面写)报
    41004 appsecret missing——错误名极具误导性(不是凭据错,是字段名错)。老接口
    GET /cgi-bin/token?appid=&amp;secret= 依旧可用,可作兜底。
  • /channels/ec/order/get 的 delivery_product_info[0] 含 delivery_name(快递中文名,如「中通快递」),
    与 delivery_id(代码 ZTO/STO)并列,展示层直接用中文名,不用维护映射表。
  • 官方确无「按运单号/订单查物流轨迹」接口(再次穷举确认):logistics/trace/get、
    order/logisticsinfo/get、logistics/express/trace/get 全 40066 invalid url;
    ewaybill/biz/order/get 传运单号报 10025010(只认回调捕获的 ewaybill_order_id)。完整轨迹仍只有:
    回调推送 / 第三方 key(快递100 poll/query.do MD5 sign 或快递鸟 1002)。
  • 商家后台订单详情页(store.weixin.qq.com/shop/order/detail?orderid=)有反自动化:CDP 开的新 tab、
    reload、bring_to_front 全部只渲染外壳(body≈107 字仅侧栏),内容也不在 iframe;真人手动打开才渲染。
    要拿页面上的轨迹数据 → 只能人工复制或截图 OCR,脚本路走不通。
  • 快递100 免费网页通道对隐私面单软拦截:www.kuaidi100.com/query?type=&lt;com&gt;&amp;postid= 返回
    message=ok, ischeck=1, state=3 但 data 只有单节点「查无结果」(时间戳=查询时刻),不是真轨迹。
    免费路不要指望,正规路=注册后的 poll/query.do(customer+key)。
  • 中通快递公司官网公开查询端点(zto.com/API/getTrackByNumber 等)2026-09 实测 404/DNS 失败,不存在。

2026-09-17 · 电子面单余额可查:account/get 必须 need_balance=true(漏传会静默返回假 0)

  • 接口:POST /channels/ec/logistics/ewaybill/biz/account/get,请求体必须 {&quot;need_balance&quot;: true, &quot;limit&quot;: 10}——
    ⚠️ 不传 need_balance 不报错,但 available/recycled/allocated/cancel 全是假 0(实测踩过);
    balance_retcode=0 / balance_retmsg=OK 才说明快递公司侧库存查询成功。
  • 字段语义(官方文档):available=面单余额(月结型账号忽略该字段)、allocated=累计取单、recycled=累计回收、cancel=累计取消;
    status=3 才是「已绑定」(5=绑定未通过,统计时必须过滤);acct_type:0 普通账号 / 1 共享账号。
  • 实测五店(2026-09-17):店铺E中通账号 <网点客户编号>(<网点所在地>)余额 0、累计回收 8(与后台「电子面单账户」页一致,证明 need_balance 生效);
    店铺B YTO 账号累计取单 37443;店铺D无面单账号(报 10025120 电子面单帐号不存在),遍历多店时必须容错。
  • 已接入 out/daily_snapshot.py:每次推送在合计后附「📮 店铺E快递面单余额:X 单」(余额 0 带 ⚠️),
    结果写入 daily_snapshot.json 的 ewaybill_balance,Excel 汇总表 E 店行备注列同步。
  • 余额对比探针:out/ewaybill_balance_probe.py(五店 need_balance=true 全量对比)。

2026-09-16 · 学往期客服会话:唯一路径是读工作台,且「关弹窗」是个死坑(详见 references/kf-session-scrape.md)

  • 会话学习只能读工作台:开放接口无历史会话查询(N 个候选路径穷举,仅 commkf/sendmsg|getmsg 存在且多店 48001)。
    入口 https://store.weixin.qq.com/shop/kf/staff/session(人员→客服考核→会话查询),必须 headed。
  • ⚠️ 别设抓取页数上限:实测 shop_003 近30天 120 页/N 条、shop_004 43 页/N 条。
    一页只有 N 个真实会话(a.kf-link 有 N 个,另一半是左右固定列克隆行)。按 20 页上限只能拿到 N 条。
  • ⚠️ 克隆行有两种形态:摘要 订单 | 会话明细 和 只有 会话明细 —— 后者必须一起过滤,
    否则每页多抓 5 条同内容重复;再加「(标题,条数,前3条文本) 指纹」兜底去重。
  • ⚠️ 弹窗里没有 .weui-desktop-dialog__closebtn(元素不存在)。旧「关弹窗」逻辑每次白等 5.3s、每单两次 = 10.6s/单。
    实测:页面常驻 ~N 个 .chat-history-dialog,点任意一行会刷新最后一个,根本不用关。去掉后 15s/单 → 2.4s/单。
    ❌ JS remove() 删节点会让之后点行失效;❌ Escape 无效;❌ 不要找 mask(不存在)。
  • 就绪判定用轮询(标题匹配 + 消息条数连续两次不变),抓完必须校验标题,否则会读到上一会话残留内容。
  • 空会话(msgs=0)是真无内容(行摘要时长/参评率/回复率全为 -,弹窗显示 chat-empty +「暂无聊天记录/服务已结束」),
    但 ⚠️ 不能反推(实测 N 条非空会话也是全 -);对它设 --empty-grace 3s 提前认定,别干等超时。
  • 不同店铺(不同账号)可并行;同店切勿并发。产物须支持断点续跑。
  • 蒸馏:客服昵称判定要排除会话标题里的买家名(量大会把话多买家误判成客服);做共享知识库时
    按客服答复聚类而非买家提问原文;共享层条目必须人工过一遍(自动聚类会把「好的呢亲亲」当高频答案)。
  • 可复用脚本(D:\AI客服系统\):scrape_all.py(全量抓,支持 --from-page/--to-page/--out 分段与续跑)、
    probe_pages.py(数总页数)、probe_empty.py(复核空会话 DOM)、parse_sessions.py、summarize_qa.py、kb_from_qa.py。

2026-09-15 · 统计「历史某一天实际发出多少单」:必须三源合并(漏计实测 6 单)

  • 口径:订单详情 order_detail.delivery_info.ship_done_time 落在目标日 00:00~24:00(东八区)。
  • ⚠️ 不能只扫目标日当天的 update_time 窗口:订单发出后若被确认收货/退款,update_time 会漂移到更晚,
    当天窗口会漏。扫描窗口必须放宽为 [目标日 00:00, 现在],并按 ≤7 天分片(跨度 >7 天报 40097)。
  • ⚠️ 不能只查 status=30/100,实测同一天发出的单里有 6 单后来退款(详情 status=200)。正确做法是三源合并去重:
    1. status ∈ {30,100}(待收货/已完成)
    2. status = 250(已关闭)—— ⚠️ 这条过滤会返回详情 status=200(售后中)的单,过滤值 ≠ 结果状态,别拿过滤值当状态用
    3. status=200 不是合法过滤值(报 40097,合法集合只有 [10,12,13,20,21,30,100,250])
    → 要抓「发出后退款」的单,只能走售后接口:getaftersalelist 按申请时间枚举 [目标日 → 现在](单次 ≤24h),
    再 getaftersaleorder 取 order_id 并入候选集
  • 实测(2026-09-09):店铺A发出 N 单(有效 77 + 售后中 6)、店铺D 1 单,五店合计 N 单。
    仅扫当天窗口只能拿到 N 单 → 漏 6 单退款单。
  • 可复用脚本:&lt;工作目录&gt;/out/ship0909_final.py(改 T0 即可换目标日;内部已含三源合并)。
  • 字段完整性核验法:对候选全量拉详情,统计 ship_done_time 为空/0 的条数——实测两店均为 0,
    可用它证明「某天单数偏低」是真实业务量而非字段缺失(店铺D日发货量实测仅 1~9 单/天)。
  • 附带:daily_snapshot.py 的「今日已发货」用 status∈{30,100},会漏掉当日发出、当日退款的极少单;
    如需严格口径可把售后接口并入(当前用户已锁定口径,未改动)。

2026-09-14 · 「今日已发货」必须用 ship_done_time;订单状态枚举补充 200/250;当日口径脚本

  • 「今日已发货」最终口径(2026-09-14 用户两次纠偏后确认):待发货=当前存量(status=20 近90天回溯);今日已发货=delivery_info.ship_done_time(真实发货时间)落在今日 0 点~统计时刻的单数。
    ❌ 口径一(已否):「今日有状态更新且 status∈{30,100}」——把今天确认收货的历史单算进去,五店虚报 512 vs 实际 220。
    ❌ 口径二(已否):「仅统计今日下单的订单」——用户不要这种切法。
    ✅ 口径三(采用):存量待发货 + ship_done_time 今日单数,16:42 版报告实测与用户后台完全一致(220)。
  • 订单状态枚举补充(实测出现):200=售后/退款中、250=已关闭。
    ⚠️ 区分两件事:查询过滤参数 status 的合法值只有 [10,12,13,20,21,30,100,250](传 200/300/400 报 40097);而订单详情返回的 status 字段还会出现 200 等值。写采集代码时别把二者混用。
  • 每日监测正式脚本:&lt;工作目录&gt;/out/daily_snapshot.py(五店串行;最终口径=存量待发货 + ship_done_time 今日发货;--no-push 免推送校验模式;自动刷新 Excel 并推双 webhook)。
  • 双 webhook 推送:飞书自定义群机器人(open.feishu.cn/open-apis/bot/v2/hook/…,interactive 卡片无需鉴权)+ 企微群机器人 markdown,均已验证可用。
  • ⚠️ webhook 发的消息无法 API 撤回(飞书 webhook 不返回 message_id;企微机器人无撤回接口),发错只能群内手动撤回——推送前务必核验口径。

2026-09-14 · ✅ 某店待发货「0 单」怎么判真伪:全状态扫描法(重跑一致性 ≠ 充分证据)

  • 场景:店铺A前一日 N 单,今日采集 0 单(降幅 100%),触发"疑似漏拉"告警。
  • 只看重跑一致性不够:两次重跑都是 0,仍可能是查询参数/凭据的系统性故障。
  • 判定法:另写一份诊断脚本,对同一 token 按全部合法 status [10,12,13,20,21,30,100,250]
    逐个用 7 天窗口扫 90 天,看去重后的状态分布:
  • 其他状态有大量单、status=20 为 0 → 真实已全部发货(本例:100:6733 / 250:2647 / 30:650 / 20:0,
    status=30「待收货」的 N 单正是"发货后"的去向,与 N 单被发走完全吻合)→ 数据可信,正常推送。
  • 所有状态皆 0 → 才是真故障(token 无权限 / AppID 不对 / 店铺无数据),需查凭据后重跑。
  • 语义对照:20=待发货、30=待收货、100=已完成、250=已取消。
  • 更强的两级证据(2026-09-14 实跑验证,建议在上述扫描之外再补上):
  • ① 双口径交叉核验:同一轮核验里分别用 create_time_range 与 update_time_range 各扫一遍
    (90 天 / 7 天分片),比对两次的待发货 ID 集合。集合相等 → 排除"只有某一时间轴漏拉"。
    实测五店两口径均为 0 单且集合一致。
  • ② 单笔状态追踪(最硬、成本最低):取上一轮命中的任意 order_id,调 order/get 看其当前
    status 与 update_time。实测该单 status 已 20 → 30、update_time 刷新为当日 15:55:05 →
    直接证明"清空"源于真实发货。这一条只需 1 次请求,比"其他状态有单"更具说服力,优先级最高。
  • ③ 组合结论:只有当"重跑一致 + 双口径一致 + 单笔已流转 + 其他状态有单"四条全中,才可判定
    0 单为真;四条中缺任一条(尤其单笔仍是 20)都要按漏拉处理并标注「⚠️ 数据待核验」。
  • 附带确认:update_time_range/create_time_range 跨度硬上限 7 天,超过即报
    40097 invalid args, update_time_range.end_time - update_time_range.start_time is larger than 7 days
    → 分片窗口不是性能优化,而是接口强制要求,--days 30/90 必须内部切 7 天片。
  • ⚠️ access_token 是「每 AppID 单例」,同一 AppID 的多个脚本/进程绝不能并发跑(2026-09-14 踩到):
    微信 cgi-bin/token 每次调用都会签发新 token 并立即作废上一枚。因此「采集脚本 + 核验脚本」
    若并行启动(或采集与诊断同时跑同一 appid),后启动的一方会把前者的 token 顶掉 →
    前者中途开始报 40001、列表静默回 0,看起来像"店铺没单",实为被自己踢下线。
    铁律:涉及同一批店铺的脚本一律串行执行(跑完一个再跑下一个),不要为了省时间用
    &amp; / 并行工具调用同时开多个入口。多店铺之间无此问题(不同 AppID 各自 token)。
  • 采集脚本写到文件的结果,total_ids(列表去重后的 ID 数)与 orders(详情成功数)必须相等、
    errors 为空 → 这是"详情没漏拉"的自检项;任一不等即为不完整采集,须重跑。

2026-09-16 · ⚠️ 「与上轮差异 >50%」这条重跑规则在小基数下会天天误触发;且待发货数会在几分钟内变化

  • 实测:2026-09-15 五店待发货全 0;2026-09-16 15:55 店铺A 1、其余 4 店 0。
    按「差异 >50%(或大额→0)」字面判定,0→1、0→4 都算「触发」→ 白跑一轮 4.5 分钟复核。
  • 结论:该规则的分母在 0/个位数时无意义。建议实操按绝对量判异常(如 |待发货变化| ≥ 5 单
    或 总量 ≥ N 单且降幅 &gt;50%),纯 0→1 这类归为正常波动,直接推、不必重跑。
    规则本意是抓"机器人故障/漏拉",而漏拉的典型表现是大额直接归零,不是个位数抖动。
  • 另一个实测事实(重要):待发货数在分钟级就会变。16:01 复核时店铺A create口径=1 / update口径=1(双口径一致),
    16:03 再查已为 0 单 —— 那笔单在这 2 分钟内真实发货了。
    ⇒ 待发货预警天然带时间快照属性,推送里的数字不必也不应在推送后再去追平;报告时注明采集时刻即可。
    ⇒ 也说明"两次重跑必须完全相等"这一自检项在小基数下不成立(业务数据在动,不是脚本不稳)。
  • 可复用核验脚本(本次新建,日期无关):&lt;工作目录&gt;/out/quick_verify.py
    (由 quick_verify_20260915.py 复制而来,内部全用 NOW - N*86400 相对窗口,跨天可直接复用)。
    38 秒跑完五店:A) status=20 近14天 create_time_range/update_time_range 双口径集合比对;
    B) 近3天 8 个合法状态分布。判停只认 has_more、剥代理、严格串行。
    → 后续遇"0 单/个位数存疑"先跑它,不要跑 90 天版 verify_pending_axis.py(要 5–8 分钟)。
  • 本次五店近3天状态分布(证明 token 有效 + 过滤正常 + 各店在流转):
    店铺A {20:1, 30:643, 100:1810, 250:560}、店铺B {100:18, 250:6}、店铺C {30:2, 100:10, 250:2}、
    店铺D {30:15, 100:391, 250:18}、店铺E {30:7, 100:50, 250:5}。
  • 待发货明细定位脚本:&lt;工作目录&gt;/out/pending_detail_20260916.py(口径与 daily_snapshot.py 完全一致,
    逐笔输出 order_id/下单时间/更新状态/实付/商品,便于直接跟进那几笔未发货单)。

2026-09-17 · 🔴🔴 daily_snapshot.py 今日发货结构性漏统计:窗口末端紧贴运行时点,读到「0 单」;门店批量发货批次集中在跑完后几分钟

  • 现象:15:53 主跑五店「今日已发货全 0」,但同日 16:14 独立探针(同口径、窗口 00:00~16:14)得 N 单
    (店铺A 59 + 店铺D 4)。两次都「没错」,差别只在观测窗口末端。
  • 根因(不是 bug,是口径时点设计缺陷):daily_snapshot.py 的 NOW 是启动时常量,
    扫 paged(tok, TODAY0, NOW, st) 且紧跟运行时点结束;而店铺A当日发货批次的 ship_done_time
    集中在 15:55:03 ~ 15:58:37(脚本 15:53 起跑)→ 整批落在窗口外。
    门店是「批量集中发货」模式(一次打单、整批回传),不是均匀发货,所以哪怕只差 2 分钟也可能漏掉整天的主要批量。
  • 判定证据链(以后照这个查):
    1. 逐单打印候选集的 ship_done_time(out/ship_today_probe.py)→ 看到 N 单的 ship_done_time
    全部 = update_time 减 1 秒、且落在 15:55~15:58,而非散落全天 → 证明是一批集中发货;
    2. 同一批单在 15:57 那次 --no-push 复核里读到 0(当时 NOW≈15:59:0x,但列表 update_time_range 末端
    同样紧贴,且 status=30 列表刷新滞后)→ 说明重跑一致性在本场景不构成证据;
    3. 把窗口放宽到 16:21 再跑(out/ship_today_authoritative.py)→ 稳定得 N 单,两次一致。
  • ⚠️ 关键教训:--no-push 复核用同一份代码、同一个 NOW 常量语义 → 同源同错,
    在「窗口边界批次」类问题上完全无效。必须换观测窗口(把 NOW 往后推)才是独立证据。
  • 正确做法(建议下次改 daily_snapshot.py):
  • 让 NOW 在每个窗口查询时重新取 time.time()(而非启动常量),至少让列表窗口末端跟到最新;
  • 或:发货窗口末端回退 N 分钟(如 NOW - 300)并额外补扫一批,两者取并集;
  • 或最简单:把「今日已发货」的列表窗口末端设为 NOW,但在最后再取一次 time.time() 补扫 (NOW_old, NOW_new]——
    对小店足够,对店铺A这种批量店才不漏批。
  • 短窗口注意 7 天硬上限(本场景只有几小时,无风险)。
  • 本日权威值(16:21 快照):待发货 店铺A 107 / 店铺D 8 / 其余 3 店 0(合计 N);
    今日已发货 店铺A 59 + 店铺D 4 = N(90 分钟口径)。均为真实值,detail_errors=0。
  • 复用脚本:out/ship_today_authoritative.py(权威今日发货统计 + 窗口末端灵敏度)、
    out/ship_today_probe.py(逐单打印 ship_done_time,定位批次)、out/ship_today_verify.py(早前探针版)。

2026-09-18 · 🔴🔴 同一缺陷第二次实证(47 vs 124),新增「末窗并集」标准解法与「算术闭合」自证法

  • 完整时序(本次运行 15:51~16:10,五店):
    | 观测 | 窗口末端 | 店铺A「今日已发货」 | 店铺A「待发货」 |
    |---|---|---|---|
    | 主跑 daily_snapshot.py --no-push | ~15:51:40 | N | N |
    | ship_today_final.py run1 | ~15:58:37 | N(尾窗补入 60) | — |
    | ship_today_final.py run2(复核) | ~16:02:45 | N(尾窗补入 2) | — |
    | pending_recheck_20260918.py | 16:03 | — | 0 |
    | daily_snapshot.py --no-push 再跑 | ~16:08 | N | 0 |
  • 批次形态:当日 N 单发货中 N 单集中在 15:48~15:57;按分钟分布
    15:48:5 / 15:50:45 / 15:51:2 / 15:52:3 / 15:53:8 / 15:54:6 / 15:55:54 / 15:57:6,
    另有 1 单在 11:23——再次证明是两批集中打单,不是均匀发货。
  • ★ 末窗并集算法(out/ship_today_final.py,推荐作标准复核):
    ① 第 1 段扫 update_time_range=[今日00:00, T1](T1 在列表查询前取 time.time())的 status∈{30,100};
    ② 逐单取 delivery_info.ship_done_time;
    ③ 第 2 段用尾部窗口 [T1-1800, T2] 再扫一次,只取第 1 段未见过的 order_id(详情阶段新发生的发货);
    ④ 计数口径不变:ship_done_time ∈ [今日00:00, T2],按 order_id 去重 → 不重不漏。
    本质:update_time 会随发货事件刷新,所以「在详情阶段结束后再扫一次尾部」即可把批次收全。
  • ★ 算术闭合自证法:主跑启动时待发货 + 主跑已发货 = 末窗已发货(77 + 47 = 124 ✓)。
    两个独立轴在同一时刻的读数互相约束,一眼判定「低报还是真少」,比逐单翻明细快得多。
    同法可用于店铺D(待发货 2 + 已发货 3 = 末窗 5 ✓)。
  • 待发货口径注意:待发货是时点库存,会在十几分钟内被批次清零。
    本次 15:36 的 N 单 → 16:03 的 0 单属真实清空;推送时应取最接近推送时刻的一次测量
    (本次用 16:08 那次 daily_snapshot.py 的 0,而非 15:51 的 77),并同时在群里说明批次原因,避免误判为故障。
  • 面单余额会随发货持续消耗:15:55 读 1876、16:08 读 1870(店铺E中通账号,非 0,无告警)。
  • 复用脚本:out/ship_today_final.py、out/pending_recheck_20260918.py、out/pending_dual_90d_20260918.py、
    out/push_20260918.py;主跑日志 out/run_log_20260918_1554.txt / _1607.txt。

2026-09-13 · 🔴 order/list/get 翻页只认 has_more:has_more=False 时 next_key 仍非空,拿它判停会死循环

  • 实测(5 店待发货监测,status=20 + update_time_range):最后一页响应 has_more=False 但 next_key 仍返回非空(~92 字符)残留值;
    带着 next_key 翻页 → 返回 0 单、不报错 → 「next_key 为空才停」的写法同页无限循环(曾卡 9 分钟靠熔断才发现)。
  • 正确判据:if not d.get(&quot;has_more&quot;): break;next_key 仅在 has_more 为真时随下一页请求带上;建议加单窗口 50 页熔断。
  • 同场新增:status=20 监测必须带时间范围(不传时间范围报 40097,即使有 status 也不行),
    用 update_time_range 按 ≤7 天窗口回溯(本次 90 天 / 13 窗口)即可覆盖全部存量待发货单(老单无状态变化,update_time=创建时间,窗口越久越保险)。
  • 多店待发货监测实操口径:列表只回 order_id_list,商品名/下单时间需逐单调 order/get(6 并发 + 0.03s 间隔,N 单约 10 秒);商品名取 order_detail.product_infos[].title。
  • ⚠️ 店铺凭据核对:本次发现「店铺D/店铺E 是同一 AppID(<STORE_D_APPID>)配了两个不同 AppSecret」,
    实测 D 的 secret 有效、E 的报 40125 invalid appsecret —— 同一 AppID 只有一个有效 secret,两条"店铺"实为一家店,采集前先核对凭据表。

2026-09-11 · ★★★ 字段名陷阱:商品名是 title 不是 product_name;金额单位是「分」;「有单无详」会让 37% 的订单没有商品名与金额

  • 接口字段(实测确认,别再猜) —— order_detail.product_infos[]:
    | 要的字段 | 接口真实位置 | 备注 |
    |---|---|---|
    | 商品名称 | title | 没有 product_name 这个键!取错就是全空 |
    | 规格 | sku_attrs[].attr_value(配 attr_key) | 是对象数组,不是字符串 |
    | 数量 / 单价 | sku_cnt / sale_price | 单价单位分 |
    | SKU 实付 | real_price | 单位分,已扣券 |
    | 订单实付 | order_detail.price_info.order_price | 单位分;原价 original_order_price、运费 freight |
    → 一律 ÷100 才是元。order_status、create_time 在 order 顶层。
  • ★「有单无详」缺口(本次审计实测):基目录四表有 1,N 单,但只有 N 单真拉过订单详情
    (= orders_cache.json 的行数,其字段完好);另 N 单只有「订单号+状态+下单时间」。
    再叠加 N 单"只在售后表里"的订单,合计 1,345 行(37.4%)既无商品名也无金额:
    ⑤ 全部售后里 811/1260、④ 未发货退款里 549/860 都受影响 → 看板「区间总金额」只覆盖 2,251/3,596 行,
    真实金额被低估,列表与导出该列显示 -。
  • 修复路径:按订单号批量调 order/get,用上面的字段映射回补四表(约 1,345 次请求,6 并发 3~5 分钟)。
    建议把这一步做进常规采集,否则每次只看售后的同步都会继续留缺口。
  • 验证方法(可复用,脚本在 .workbuddy/diag/):
    verify_order_set.py(订单号集合对账)、verify_aftersale_set.py(售后单号对账)、
    verify_ship_sample.py(发货判定随机抽样独立复核)、audit_local_data.py(内部一致性)。
    ⚠️ 跑接口前先 env -u http_proxy -u https_proxy …,否则会话代理会 502。
  • 本次验证结果:售后单号 177/177 完全一致;发货判定随机抽 N 单 100% 一致;
    订单号对账漏 N 单(全部是 09-10 17:01 后新单,属数据陈旧,非逻辑错);反向 0 单。

2026-09-11 · ★★★ 「未发货」只认订单详情接口 —— 本地"没有发货记录"≠未发货(否则未发货退款虚高 30%)

  • 坑:实测店铺有 N 笔订单只存在于售后表里(订单表/发货表从没拉过它们),本地看起来"零发货记录",
    会被顺理成章地判成「未发货」。实测抽样 N 笔问订单详情接口,5 笔其实早已发货 —— 本地口径把大批
    已发货的退款单算进了「未发货退款」(虚高口径 N 笔)。
  • 权威依据(/channels/ec/order/get → order_detail.delivery_info,实测确认):
  • delivery_product_info(包裹数组)长度 > 0 → 已发货
  • ship_done_time > 0 → 已发货
  • 两者都为空/0 → 未发货
  • 落地:scripts/fetch_ship_status.py 批量查并落 ~/.workbuddy/data/wxshop/ship_status.json;
    展示层 display_query._apply_ship_status() 读它覆盖 shipped,产出
    ship_api(True/False/None)、unshipped_confirmed(仅 API 明确没发货才 True)、
    unshipped_refund(= 有售后 ∧ unshipped_confirmed)。
  • 实测结果:N 笔查出 400 已发货 / 860 未发货 → 「未发货退款」由 1122 纠正为 N。
  • 判定优先级:接口说已发货 → 一律算已发货(本地有记录也一致);
    接口说没发货 且 本地也无发货痕迹 → 才算未发货(有矛盾按已发货,宁可不误报)。
    没查到的单不算未发货(宁可少报,不要虚高)。
  • 工作台采集流程里已内置为 ②.5 步 run_ship_enrich:只把区间内有售后的订单号送去富集(不盲采全量)。

2026-09-11 · ★★★ 拦截证据必须落盘固化(一次拦截,永久拦截)+ 落盘一律原子写

  • 拦截的三档证据都会随时间消失:售后一办结,WAIT_PACKAGE_INTERCEPT(包裹拦截中)就翻成
    MERCHANT_REFUND_SUCCESS;商家也会改备注。证据没了,订单就从「物流拦截」里掉出去 →
    同一筛选条件上午查有、下午查没有。实测:全库 N 单拦截全靠商家备注认定,
    售后状态法命中 0 单,链路极脆弱。
  • 修法:采集收尾调用 stamp_evidence() 写 _intercept_ever / _intercept_seen / _intercept_src
    到归档;合并只增不减,展示层 OR。这些下划线字段不是缓存,改采集逻辑时不要删。
  • 落盘一律原子写(临时文件 → fsync → os.replace,保留 .bak);读失败先回退 .bak,
    绝不把"读不出来"当"没有数据"——旧写法会因此把整份归档覆盖成空,命中数凭空少几单且不可逆。
  • 详见「拦截退货识别」小节里的 ⚠️ 拦截证据必须&quot;落盘固化&quot; 一段。

2026-09-11 · ★★ 一个订单可能有多条售后单;多份数据快照合并时,"旧快照"会盖掉"新快照"(漏单真凶)

  • 一个 order_id 可以挂多条售后单:/channels/ec/order/get 的
    aftersale_detail.aftersale_order_list[] 会列出该订单全部售后单(含很久以前办结的)。
    实测 &lt;订单号&gt;:挂着 &lt;售后单号&gt;(2026-04-29 申请、4-30 用户已取消)
    与 &lt;售后单号&gt;(2026-09-10 商家已退款)两条。
    → getaftersalelist 按时间窗枚举只能采到窗口内的那条,历史那条天然采不到(正常,不影响统计)。
    → 要核实"某单到底有几条售后"时用订单详情接口交叉核对,别只看本地 aftersales.json。
  • 用户报"筛选区间漏单"时,先查"同一售后单在多份快照里的状态是否冲突":
    data/wxshop/ 下多份落盘(基目录扁平表 / range_* 归档)会同时存在同一条售后单,
    旧归档存的是当时的状态。实测 range_20260701_20260901 里仍是
    MERCHANT_WAIT_RECEIPT(待商家收货) 且 complete_time=0,而 range_aftersales 里已是
    MERCHANT_RETURN_SUCCESS(退货完成已退款)、complete_time 有值 —— 两边的
    "非空字段个数"打平,合并时先加载的旧快照胜出,导致该订单按"售后完成时间"筛选时整单消失。
    → 合并口径必须按进度取舍(有 complete_time ≫ 无;取消/关闭/被拒要降权),详见展示技能
    wxshop-display 的 _aftersale_score。两技能必须同步此规则。
  • 新增状态码 MERCHANT_PROCESSING = 商家处理中(已补进 AS_STATUS;展示层同步补)。
    PLATFORM_REFUND_SUCCESS = 平台退款成功,也是"已完成"三态之一。

2026-09-11 · ★ 新增「仅已完成售后」采集范围(--only-completed)+ 默认口径改为「售后完成时间」

  • 用户确认:只筛已完成售后不会丢拦截单——拦截单本身就是售后单,完成售后后同样进入"已完成"集合
    (实测 N 笔拦截单 N 笔已完成,其余 3 笔是"包裹拦截中/待用户确认",本就未办结)。
  • 新参数 --only-completed:只保留「已完成」售后单(已退款 / 退货完成已退款 / 平台退款成功,且 complete_time&gt;0)。
    过滤发生在拉订单详情之前 → 实测("今天·仅已完成"):枚举 318 售后单 → 过滤 N 个(处理中/取消/被拒)→ N 个,
    订单详情请求从 ~227 次降到 7 次,这就是"缩小采集范围提升效率"的落点。
  • 已完成判定口径(易错):USER_CANCELD / CLOSED / MERCHANT_FAIL 也带 complete_time(覆盖率 100%),
    但那是"流程终结"不是"完成",必须排除。枚举常量 AS_DONE_STATUS 与展示层 AFTERSALE_DONE_STATUS 必须同步改。
  • 默认时间口径改为 --time-basis complete(工作台实时同步固定按此口径取数并自动向前放宽 30 天);
    页面筛选仍可在 售后完成时间 / 售后申请时间 / 下单时间 三种口径间切换。
  • ⚠️ 新坑(代理 502):助手会话代理 http://127.0.0.1:xxxxx 端口活着但对 api.weixin.qq.com 回 502,
    采集子进程继承该代理后全军覆没(ProxyError: Tunnel connection failed: 502)。
    已在工作台 server.py sanitize_env() 加固:微信 API 能直连就一律剥掉代理,不要只判断"端口通不通"。
  • fetch_aftersales.py --from-cache 现在不再要求凭据(纯本地重建,Api 延迟到真调接口时才创建)。

2026-09-11 · ⏱️ 「采集口径」与「展示口径」必须一致:抓的是售后申请时间,筛就不能按下单时间

  • 采集侧按售后申请时间取数(fetch_aftersales.py --start/--end),但展示侧(工作台 / wxshop-display)原先按下单时间筛窗口 →
    「点今天显示 0 笔」「最近7天退货少了近一半」的根因就在这。
  • 实测(实测店铺 3,N 笔,2026-09-11):「售后申请 − 下单」间隔 中位 3.1 天 / 平均 3.8 天 / 最长 15.9 天,
    9% 的退货单间隔 > 7 天。最近7天退货:下单口径 N 笔 vs 售后口径 N 笔(漏 43%);今天:下单口径 0 笔 vs 售后口径 1 笔。
  • 规则:凡是「退货 / 拦截 / 产生退回」这类售后视角的查询或统计,时间过滤一律用售后申请时间(as_ts / aftersale_dt);
    只有纯订单/销售视角才用下单时间。展示层保留切换开关,默认售后口径。
  • 「退回单号」也是同类坑:拦截/拒收退回的单没有买家寄回号,退回的就是原发货单号。
    早前展示层只读售后单 return_waybill_id → 拦截单整列空白(实测 31/31 全空)。正确取法见下方「退回单号取法」小节,两层已同步实现。

2026-09-10 · ⚡ 售后管理工作台确立「售后优先」采集策略(fetch_aftersales.py 是主路径,fetch_orders.py 降级为可选)

  • 需求方(售后管理工作台)只需要产生售后的订单 → 用 fetch_aftersales.py --start X --end Y 按天枚举售后单即可,
    请求量 = 天数(枚举)+ 售后单数×2(售后详情 + 关联订单详情);实测 7 天窗口 185 售后单 ≈ 5~6 分钟(沙箱代理 ~1s/请求)。
    对比 fetch_orders.py 全量增量(1,N 单 ≈ 19 分钟)快 3~4 倍,且直接就是三板块要的数据。
  • --pad-days N:枚举窗口向前后各放宽 N 天(覆盖"下单早于区间"的售后),工作台默认 1;成本极低(每天 1~2 次列表请求),建议别设 0
  • --time-basis complete:按售后完成时间筛选,脚本会自动向前放宽 30 天枚举(申请早于区间、完成落在区间的很常见,实测最长 7 天);慢但全
  • ⚠️ fetch_aftersales.py 是覆盖写:aftersales.json / orders.json 固定文件名直接 json.dump,同一路径重复跑会抹掉之前窗口的数据。
    下游(工作台 server)必须先让它写临时目录,再按主键 after_sale_order_id / order_id 合并进持久目录(同键新覆盖旧),否则点"今天"会把"最近7天"的数据抹掉
  • ⚠️ 并发坑(工作台 server 侧踩的):threading.Lock 不可重入——在 with _lock: 块内调用内部又拿同一把锁的日志函数会永久死锁,所有后续请求全部挂起。日志必须在锁外写

2026-09-10 · 🏪 店铺基本信息接口已跑通:GET /channels/ec/basics/info/get(新脚本 fetch_shop_info.py)

  • 端点 GET https://api.weixin.qq.com/channels/ec/basics/info/get?access_token=…,⚠️ 必须 GET——POST 会报 43001 require GET method(探针判"存在"时要注意这个坑)
  • 返回 info:nickname(店名,实测=「店铺A」)、headimg_url、subject_type(主体类型,实测店铺=企业)、status(open_finished=正常营业)、username(原始 ID gh_…)、open_timestamp
  • 穷举存档:/channels/ec/store/info/get、/channels/ec/shop/info/get、/channels/ec/basics/shop/get、/channels/ec/account/get、/channels/ec/merchant/get、/channels/ec/basics/account/get、/channels/ec/basics/store/get 共 7 个候选全部 40066 invalid url——店铺信息只有这一个入口
  • 用法:fetch_shop_info.py --out &lt;数据目录&gt; → 落盘 shop_info.json(展示层/工作台直接读 nickname 当店名,不用再写死);凭据走环境变量 WX_STORE_APPID/WX_STORE_SECRET 或 --config

2026-09-10 · ⚠️ fetch_orders.py 的 orders_cache.json 是中文键,展示层(wxshop-display)读不了

  • fetch_orders.py 落盘的 orders_cache.json 行结构为中文键(订单号/订单状态/下单时间/商品标题/SKU属性/单价(元)/数量/发货单号(、连接多包裹)快递公司/发货时间/订单实付金额(元)…),且每 SKU 一行;而展示层 display_query.py 只认英文键形态(A:order_id/status/create_time… 或 B:富集内嵌 products[])
  • 衔接方案(已落地):&lt;工作目录&gt;/workbench/server.py 的 convert_orders_cache() 把中文键缓存转成形态 A 四表(orders.json / order_products.json / deliveries.json,商品与物流每 SKU / 每包裹一行)后按 order_id 合并回基目录——顺带修复了旧 order_products.json「一单一行、多商品被压平」的缺陷
  • fetch_aftersales.py 的输出(orders.json 富集格式 + aftersales.json 英文键)天然兼容展示层,直接落到数据子目录(如 range_aftersales/)即可被 --data-dir auto 合并
  • 采集脚本本身不要改成英文键输出:Excel 交付与 trace_query.py 都依赖中文键 / target_orders.json 现有结构

2026-09-10 · ⏱️ 双时间口径(申请时间 / 完成时间)都要支持、都保留结果

用户明确要求:两种口径都要保留,技能要能按不同问法给出对应结果(不是二选一)。

用户问法 时间口径 参数 8/1~9/1 实测(轨迹验证版)
"8.1~9.1 产生退回的订单" 售后申请时间 --time-basis create(默认) N 单 / 轨迹铁证 7 单 / ¥870.00
"完成时间在 8.1~9.1 的退回订单" 售后完成时间 --time-basis complete N 单 / 轨迹铁证 7 单 / ¥850.10
  • 两口径差异来自跨月办结:&lt;订单号&gt;(申请 08-30、完成 09-02)只落在申请口径内;
    本期「完成在区间、申请在区间之前」为 0 单(无跨月申请),但通用场景仍需放宽窗口
  • complete_time 是最容易被忽略的字段:后台「售后管理」有"申请时间 / 完成时间"两个筛选框,而 API 的
    getaftersalelist 只支持按申请时间枚举(入参只有 begin_create_time/end_create_time)
    → 完成口径必须向前放宽枚举窗口(脚本自动向前 30 天;实测办结中位 0 天、最长 7 天;向后无需放宽)
  • 两版报告都新增 「售后完成时间」列(与「售后时间」并列),标题与口径页自动写明本次时间基准,便于交叉核对
  • 交付命名约定:产生退回的订单_按申请时间_起_止.xlsx / 产生退回的订单_按完成时间_起_止.xlsx

2026-09-10 · ➕ 新增「退回单号」列(应要求:两种退回来源分别对应)

  • 退货退款 → 「退回单号」= 买家寄回的快递单号(售后单 return_info.waybill_id,如 &lt;运单号&gt;),来源标 买家寄回
  • 拦截 / 拒收退回(物流轨迹确认被退回)→ 「退回单号」= 原发货单号(即包裹被退回的那个号),来源标 原发货单退回
  • 两者都成立 → 两个号都给:买家寄回号 / 原发货单号
  • 配套列:退回快递(买家寄回的快递,实测可能与寄出不同家:中通寄出、顺丰寄回)、退回单号来源(★ 必看,避免误读)
  • 已内置到两版报告:merge_trace_report.py(轨迹验证版)+ fetch_aftersales.py(4 表降级版),口径一致
  • 实测店铺 8/1~9/1 实测:8 单退货退款全部有买家寄回单号(中通 5 / 顺丰 3);7 单拦截退回全部对应原发货单号

2026-09-10 · ✅ 端到端全链路跑通并交付(技能已同步本轮全部优化)

  • 终版交付物:产生退回的订单_含物流轨迹验证_0801-0901.xlsx(3 表:退回订单汇总 / 物流轨迹铁证 / 口径与边界说明)
  • 最终数字:8/1~9/1 产生退回 N 单(退货退款 8 / 包裹退回 7),退款合计 ¥870.00;其中 7 单拿到物流轨迹铁证
  • 标准跑法(三条命令,约 5 分钟):fetch_aftersales.py(圈范围 + 拿运单号)→ trace_query.py --only-aftersale(轨迹验证)→ merge_trace_report.py(出终版报告)
  • 本轮确定并固化下来的能力:
    1. 范围限定:售后类需求一律先在「售后管理」枚举订单(请求量 2643 → ~百级)
    2. 运单号全自动:订单详情 delivery_product_info[].waybill_id 覆盖率 100%,D 路入参无需人工整理
    3. 轨迹验证:快递100 跑通(N 单 0 失败 / 23 秒),成为判"包裹被退回"的权威依据
    4. 备注法会漏单:实测漏 30%,只作辅助信号
    5. 口径三态:「物流轨迹验证」列必须用 是 / 不适用 / 否 / 未验证,退货退款单查原单必然"已签收",标"否"会误读
    6. 端点探针:probe_endpoints.py,40066 invalid url 判定端点不存在

2026-09-10 · 🔴 重大准确性修正:物流轨迹已跑通,且证明「商家备注法会漏单」

  • ✅ 快递100 路径 D 全链路跑通(真实凭据,N 个已发货售后订单逐单查询,0 失败)。配置关键项:phone 必须填 —— 中通/顺丰要求手机号,而小店接口的收货人手机号脱敏且无解密接口,故填店铺寄件人手机号(实测店铺 183xx…→ 用 &lt;发货人手机号&gt;)
  • 🔴 备注法会漏单,实测漏 30%:8/1~9/1 商家备注法识别 5 单拦截退货,物流轨迹实际 7 单;
    漏掉的 2 单备注分别写 "取消寄件" 和 完全空白,但快递轨迹明确是"快件已被退回签收,签收人:退回商家/发件人"
  • 🔴 退回订单总数修正:N 单 → N 单,退款合计 ¥688.30 → ¥870.00
  • 判定优先级定死:物流轨迹(state ∈ {4 退签, 6 退回, 14 拒签} 或文案含"退回")> 售后类型 type=RETURN > 商家备注含"拦截"
  • ⚠️ 退货退款单查原发货单号必然显示"已签收"(买家收到货才退货,货另寄回),这类标「不适用」不能标"否"。实测 8 单退货退款的原单轨迹全部"已签收",符合预期
  • 新增脚本 scripts/merge_trace_report.py:售后结果 + 轨迹结果 → 终版报告(带「物流轨迹验证」列,自动黄底标出备注漏掉的单)
  • trace_query.py 新增节省配额开关:--only-intercept(只查拦截退货单,5 条)、--order-id(定点复核)

2026-09-10 · 「订单详情能否直读发货物流信息」——能读一半,实测澄清

  • ✅ 能直读:/channels/ec/order/get → delivery_info.delivery_product_info[] 给出真实运单号 waybill_id、delivery_id/delivery_name、发货时间、deliver_method。已发货订单实测覆盖率 23/23 = 100%
  • ❌ 不能直读:物流轨迹节点(含 status=6 退回)不在订单详情里;/channels/ec/order/list/get 只回 order_id_list,连运单号都没有,必须逐单拉详情
  • 端点穷举:两轮共验证 N 个端点名,除 ewaybill/biz/order/get(要 Uint64 面单ID)外全部 40066 invalid url → 小店开放接口无轨迹能力的结论可以定死。探针工具已固化为 scripts/probe_endpoints.py
  • ewaybill_order_code 定性纠正:88 字符加密串(非 base62、非面单ID),传该字段名报 40097 invalid args,不能替代 ewaybill_order_id
  • 实测店铺快递不止中通:/delivery/get 只列中通 ZTO,但实测存在顺丰手工发货单(&lt;运单号&gt;)→ 这类单无 ewaybill_order_id,A/B 路对其无效,只能走 D/C
  • 新增判据(实测 N 单):status=200(全部商品售后中) 且有运单号 → 已发货后退款/拦截;无运单号 → 未发货退款(样本 N 单中 N 单未发货、1 单备注"已发货,拦截退回")
  • 代码改动:fetch_orders.py 新增 快递公司/发货单号/发货时间/物流方式 四列 + 导出 target_orders.json(D 路入参从此全自动,无需人工整理);trace_query.py 支持一单多包裹 waybill_id_list
  • 新增第零步「范围限定」:售后类需求默认只在「售后管理」枚举出的订单范围内作业,请求量从 2643 降到 ~百级

2026-09-10 · 两条路都已落地为可运行脚本

  • 路径 B(实时铁证):scripts/ewaybill_callback.py —— 物流轨迹推送回调服务,已按官方规范实现(安全模式 + JSON、msg_signature 验签、AES-256-CBC/PKCS#7 解密),--selftest 端到端 5 项全通过。落库 ewaybill_events.jsonl + waybill_index.json。运行前提:公网可达的 80/443 + 后台「服务市场」-「自研」-「消息推送」配置
  • 路径 D(历史回溯):scripts/trace_query.py —— 三后端(快递100 / 快递鸟 / 小店官方面单)批量查轨迹,判定退回分「是/疑似/否」三档并给出依据;--only-aftersale 省配额;带缓存与断点续跑。运行前提:第三方 key(快递100 的 customer+key,或快递鸟的 EBusinessID+AppKey)
  • 重要纠正:/channels/ec/logistics/ewaybill/biz/order/get 的 ewaybill_order_id 必须传字符串;传 JSON 数字报 47001(此前"必须 Uint64 数字"的记录易误导,已改正)
  • 新增坑:快递100 customer 无效时对任意 com 编码都返回 400 找不到对应公司,别误改快递公司映射表

2026-09-10 · 物流轨迹能否"按发货单号查"

  • 不能。微信小店 API 无按运单号/订单号查轨迹的入口(电子面单 N 个接口全枚举确认)
  • 想拿到官方"包裹被退回"铁证(轨迹 status=6),仅两条路:配 ewaybill_push_path 回调(实时) 或 第三方快递轨迹 API(可回溯)
  • 详见「物流轨迹与"包裹退回"识别」章节的 4 条路径对比表 + 两条路的落地实现

🔄 技能维护约定(重要)

用户要求:本技能需持续更新,不另建新技能。

每次执行采集任务后,若发现以下任何一类新信息,必须在同一轮对话内回写到本文件:

  1. 接口变化:新增/变更的接口路径、返回字段名、请求参数(如字段从 order_list 改为 order_id_list 这类)
  2. 字段结构:新发现的字段位置或结构(如 aftersale_detail 在 order 主体层而非 order_detail)
  3. 枚举值:新出现的 status / type / return_type / event 等枚举取值及含义
  4. 业务规则:新的判定逻辑(如"拦截失败会自动转退货退款")、需去重/排除的规则
  5. 踩坑经验:导致数据错误或采集失败的原因(字段取错、漏采范围、沙箱终止等)
  6. 时间口径(2026-09-10 新增,用户强调):任何"某时间段"的需求都先分清是申请时间还是完成时间
    (见第零步「时间基准二选一」);两个口径的结果都要能给、且都要保留,不得用一个顶替另一个。
    完成口径必须放宽枚举窗口(getaftersalelist 只能按申请时间过滤)

回写位置:优先补进对应章节(字段说明 / 拦截识别 / 枚举表 / 交付模板),无对应章节则在末尾新增小节,并在本约定上方保留最新结论。

> ⚠️ 严禁把上述内容写进临时笔记就结束——必须落到本 SKILL.md,否则下次采集会重复踩坑。

配套技能:查询与展示层(2026-09-10 新增)

本技能负责采集;把本地四表查出来给人看由另一个技能负责,不要在本技能里重复实现菜单/表格逻辑:

  • 技能:wxshop-display(wxshop-display 交互式展示系统) → ~/.workbuddy/skills/wxshop-display/
  • 入口脚本:scripts/display_query.py(菜单引导 / 四表关联 / 筛选 / Markdown 表格 + 汇总 / 导出 CSV / 单笔明细)
  • 用户的「打开订单查询 / 查询订单 / 显示订单 / 最近7天退货订单 / 本月物流拦截订单」等纯查询展示需求 → 走该技能
  • 它复用本技能的状态码映射、售后状态中文名、拦截判定优先级(轨迹 > 售后状态 > 商家备注);改一处要同时改两处
  • 它只读 ~/.workbuddy/data/wxshop/ 及其子目录(--data-dir auto 自动合并去重),不改数据、不调接口;
    数据缺失时由它引导用户回到本技能采集
  • ★ 它读本技能产出的 ship_status.json(_apply_ship_status):展示层的 ship_api /
    unshipped_confirmed / unshipped_refund 全靠这个文件。该文件由 fetch_ship_status.py 产出,
    没跑过它 → 展示层「④ 未发货退款」会是 0(未查到的单一律不判未发货)。两个技能更新时此依赖别忘。
  • 两个技能都已支持的条件:unshipped_refund(④ 未发货退款)/ all_aftersale(⑤ 全部售后订单,含处理中)。
    这两个条件按「售后发生时间」(申请时间或任一完成时间)落窗,不受所选时间口径限制 —— 改动时两处必须同步。

技能合并说明(2026-09-10)

本技能已合并吸收以下两个原技能,它们已从技能库移除,不要再新建同类技能:

  • wechat-store-orders → 其采集脚本(scripts/fetch_orders.py)、接口参考(references/api.md)、配置样例(assets/config.example.json)与销售分析交付模板已并入本技能
  • data-collector → skill-creator 生成的空白模板(无实际内容),已直接删除

合并前的原始副本备份在:&lt;工作目录&gt;/.workbuddy/backup/skills-merged-20260910/

常见问题

5 问
答案由本站依据条目字段整理
多店铺待发货提醒怎么安装?

在支持 Skills 的客户端里执行:skillhub install user_2e4bebd7/wxshop-order-status-collect。安装方式为未标注。装完刷新技能列表或重开客户端。

多店铺待发货提醒支持哪些客户端?

资料没有标注宿主客户端,但只要客户端支持 Skills(Claude、Codex、Cursor 等),一般都能加载。

多店铺待发货提醒权限高吗,安全吗?

权限级别为未标注权限。安装前先看它会读取或写入哪些内容。

多店铺待发货提醒收费吗?

本站不代收费用,也没有标注具体价格。是否收费取决于技能本身和所用的模型或客户端,正式使用前按作者说明确认。

多店铺待发货提醒用什么许可证?

许可证标注为 未说明(许可证不明确)。商用或二次分发前请到仓库确认条款原文。

热门专题

按场景挑技能
全部专题 →

技能内容以作者仓库为准;分类、权限级别与许可证状态的中文说明为中文整理,最近核验 未标注。