HANDOFF.md 33 KB

集物星球爬虫 · 加密/接入 交接文档

新窗口接续用。核心:签名 X_SIG_1 已破解;必须用 curl_cffi 发请求(requests 会被 TLS 风控判为非真机、返回 code:200 空壳,curl_cffi 才出 code:1 真数据);sessionId 靠脚本自登录拿。以上均已实测验证。域名 https://api.jiwuplanet.com,全 POST+JSON。

1. 签名 X_SIG_1(核心加密流程,已逐字节验证)

每个请求带 6 个头:X_VERSION / X_TIME / X_SID / X_RAND / X_PLATFORM / X_SIG_1

X_SIG_1 = base64_std( HMAC_SHA256( key, msg ) )
key = "EahycFpDpewggO2rksvQwTlnhCbTHFx6"          # 生产环境密钥(逆向所得)
msg = path + "2.12.2" + "150" + sessionId + X_TIME + X_RAND + canonicalBody
  • path:URL 路径(不含域名/query),如 /search/app/index/top
  • X_VERSION="2.12.2",X_PLATFORM="150"(常量)
  • X_TIME:秒级时间戳字符串 str(int(time.time()))
  • X_RAND:6 位随机 str(random.randint(100000,999999))
  • canonicalBodyjson.dumps(body, sort_keys=True, separators=(",",":"), ensure_ascii=False)——递归按 key 排序、紧凑、非 ASCII 不转义;实际发送的 body 就是这个串(与被签名内容完全一致)
  • sessionId 三处必须同值X_SID 头 = body 里 sessionId 字段 = 签名串里的 sessionId

验证基准(用抓包固定参数复算,结果一致): path=/search/app/index/top,X_TIME=1787045062,X_SID=ccab0d5917cd4e289d07b820e4497989,X_RAND=295778,body={"currentPage":"1","limit":"10","productType":"3","sessionId":"ccab0d5917cd4e289d07b820e4497989","systemBusinessType":5} → X_SIG_1 = tZuw8vRCz2DVb6RSLv0KBlKgcctmBCxi07SiDH3em+k=(与真实抓包逐字符相同)。

2. TLS 指纹(关键,否则拿不到数据)

  • 必须 curl_cffiimpersonate="chrome"pip install curl_cffi
  • 直连proxies={"http":None,"https":None}(本机有 HTTP(S)_PROXY=127.0.0.1:7890 的 Clash,间歇开着,requests 默认走它会卡死)。
  • 记忆点:code:200 + data.result:[] = 被风控/会话无效的空壳;code:1 + data.records[...] = 真成功。

3. sessionId / 脚本自登录(已验证可用)

  • 登录态最小化(2026/08/19 实测)jiwu_core.do_request(..., need_auth=False) 默认免登录——用随机未绑定 sid 签名、不调 regLogin。实测免登录:在售 index/top、已售 corp/history、详情 merchantGoodsId(随机 sid 即出 code:1);需登录(need_auth=True):商家 hotRecommend、购买记录 publicity/user/group/pager、拆卡报告 gift/report(随机 sid 返 code:200 空壳)。
  • sessionId = 客户端自造 32 位小写 hex,regLogin 上报后服务端沿用该值。
  • 自登录:POST /acct/user/regLogin,成功 code==1,返回 data.sessionId
  • 账号:mobile=19521500850, loginPassword=pass2022, deviceNumber=40b03f4bec96675f(账号 ccc123, userId 21588067)。
  • 登录 body(自造 sid 填进去):

    {"appVersions":"2.12.2","deviceNumber":"40b03f4bec96675f","identificationNumber":"19521500850","ip":"10.1.10.1","language":"ZH","loginPassword":"pass2022","loginType":"ACCOUNT","mobile":"19521500850","mobileCode":"86","platformInfo":"Google Pixel 5","platformType":"ANDROID_USER","sessionId":"<自造sid>","systemBusinessType":6,"terminalVersions":"11"}
    

登录 / 会话流程图(核心:无 token,客户端自造 sessionId;「签名是门票、sessionId 是身份」;三处 sessionId 头 X_SID/body/签名串须同值):

flowchart TD
    A["do_request(path, body, need_auth)"] --> B{"need_auth?"}
    B -- "False 免登录(默认)" --> C["get_anon_sid()<br/>随机自造 sid,不调 regLogin"]
    B -- "True 需登录" --> D["ensure_session()"]
    D --> E{"缓存 sid 未过期?<br/>SESSION_TTL=1800s"}
    E -- "是" --> F["复用缓存 sid"]
    E -- "否" --> G["login(): 自造 sid<br/>→ regLogin 绑定 → 写缓存"]
    C --> H["sessionId 写入 body.sessionId + 头 X_SID + 签名串<br/>(三处同值)"]
    F --> H
    G --> H
    H --> I["_sign(): X_SIG_1 = base64(HMAC_SHA256(key, msg))<br/>msg = path+版本+平台+sessionId+时间+随机+body"]
    I --> J["curl_cffi POST(impersonate=chrome,直连)"]
    J --> K{"code == 1 ?"}
    K -- "是" --> L["成功,返回 JSON"]
    K -- "否 · 免登录" --> M["轮换匿名 sid,下轮再来"]
    K -- "否 · 需登录" --> N["清缓存 → 重登,最多再试 1 次"]
  • 免登录(need_auth=False,默认):在售 index/top、已售 corp/history、详情 merchantGoodsId——匿名随机 sid,不暴露账号。
  • 需登录(need_auth=True):商家 hotRecommend、购买记录 publicity/user/group/pager、拆卡报告 gift/report——ensure_session 复用/续期真会话。
  • 动机:登录态是账号被风控关联/封号主要抓手,走「最小登录面」,能免登录就不登录。

4. 自包含验证脚本(存成 verify.py,python verify.py 应打印 code=1 + ccc123 + 真实商品)

import hmac, hashlib, base64, json, time, random
from curl_cffi import requests as creq
KEY = "EahycFpDpewggO2rksvQwTlnhCbTHFx6"; BASE = "https://api.jiwuplanet.com"
NOP = {"http": None, "https": None}
def cb(o): return json.dumps(o, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
def post(path, body, sid):
    body = dict(body); body["sessionId"] = sid; b = cb(body)
    xt = str(int(time.time())); xr = str(random.randint(100000, 999999))
    msg = path + "2.12.2" + "150" + sid + xt + xr + b
    sig = base64.b64encode(hmac.new(KEY.encode(), msg.encode(), hashlib.sha256).digest()).decode()
    h = {"X_VERSION": "2.12.2", "X_TIME": xt, "X_SID": sid, "X_RAND": xr, "X_PLATFORM": "150",
         "X_SIG_1": sig, "Content-Type": "application/json;charset=UTF-8",
         "User-Agent": "okhttp/3.12.10", "Host": "api.jiwuplanet.com"}
    return creq.post(BASE + path, headers=h, data=b.encode(), timeout=20, impersonate="chrome", proxies=NOP).json()
# 1) 自登录
sid = hashlib.md5(f"{time.time()}{random.random()}".encode()).hexdigest()
acct = {"appVersions":"2.12.2","deviceNumber":"40b03f4bec96675f","identificationNumber":"19521500850",
        "ip":"10.1.10.1","language":"ZH","loginPassword":"pass2022","loginType":"ACCOUNT","mobile":"19521500850",
        "mobileCode":"86","platformInfo":"Google Pixel 5","platformType":"ANDROID_USER","systemBusinessType":6,"terminalVersions":"11"}
j = post("/acct/user/regLogin", acct, sid); d = j.get("data") or {}; sid = d.get("sessionId") or sid
print("登录:", j.get("code"), d.get("nickName"), d.get("userId"))
# 2) 在售列表
j = post("/search/app/index/top", {"currentPage":"1","limit":"10","productType":"3","systemBusinessType":5}, sid)
dd = j.get("data") or {}; recs = dd.get("records") or []
print("在售:", j.get("code"), "total=", dd.get("totalCount"), "首条=", recs[0]["goodsName"] if recs else None)

5. 接口清单(成功 code==1,列表在 data.records,data.totalCount)

板块 path 关键参数
在售 /search/app/index/top currentPage,limit,productType(1福袋2变风3错版卡4原盒),systemBusinessType:5
商家(142家) /search/app/index/corp/hotRecommend currentPage,limit,systemBusinessType:6
已售 /search/app/corp/history corpInfoId,currentPage,limit,systemBusinessType:5
商品详情 /search/app/merchantGoodsId免登录 goodsId,systemBusinessType:5。监控判结束用。⚠️ 库存字段=stock(总)/surplusStock(剩余)、价格=price/highestPrice与 index/top 的 stockAmount/residueStockAmount 不同名。(返回里 goodsBuyRspDtoList 只有 6 条预览,不是全量购买记录)
购买记录 /order/merchant/app/query/gift/publicity/user/group/pager(赠品公示·玩家维度,需登录 currentPage,giftBusinessName:"",goodsId,limit,systemBusinessType:5。每条=一个买家 userId/userNick/picId/count,翻页拿全量(如 1660823 共 18 人)。不 DB 去重,靠已售表 buy_fetched 状态位控制每商品抓一次(取全→批量入库→置位)。publicity/pager(不带 user/group) 是幻影接口勿用
拆卡报告 /goods/gift/report/query/search/pager需登录,sbt=6 goodsId,currentPage,limit。字段:giftReportId(唯一递增)/serialItemName(开出卡名)/resAddrList/winnerStatus/anonymousStatus/userId(有值)/corpId/corpName/goodsName/username(脱敏)/createTime/myGiftStatus。已售商品可事后翻页查全量
  • jake corpInfoId=100716(Jake球星卡)九叔 corpInfoId=100715(九叔的喷火龙)
  • 金额字段 ÷ 1000000 = 元(实测 amount=99000000 → App ¥99.00。旧文档 ÷10000 是错的)。入库前已用 core.to_yuan 换算成元存 DECIMAL(12,2),库里/报告直接是元;监控读实时接口(原始值)仍 ÷1000000 展示。
  • 商品字段:goodsId/goodsName/productType/corpInfoId/corpInfoName/amount/highestPrice/lowestPrice/stockAmount/residueStockAmount/specificationName/soldTime/liveReplayUrl…

6. 环境注意(血泪教训)

  • Bash 跑在隔离沙箱:看不到真实磁盘文件、写入也不落盘。别用 ls/python xxx.py 判断文件在不在(会误报"文件不存在")。跑脚本让主公在本机跑,或用"关闭沙箱 + 同一条命令内 heredoc 建+跑"。
  • Write 工具写真实盘、主公能看到;但偶发不稳,关键文件建议同时贴聊天由主公复制。
  • 逆向产物在 decompiled/(jadx,包名 com.jiwu.star),签名逻辑在 com/jiwu/star/api/LogInterceptor.java;密钥在 EnvironmentManager,HMAC 在 HmacUtils.hmacSHA256Base64

7. 五大板块进度(全部已落地,2026/08/19)

  • 已验证:接口全通、签名/自登录/TLS 全打通、企微推送已通(测试群 webhook key b8d398e2-f27e-42ce-af78-336867460122)。
  • 参考骨架:D:\work\2026-08-02(deca_spider)——整个流程/产出/逻辑与之一致,只是加解密不同。报告都是 openpyxl 生成 Excel 走 send_wechat_group_file 发文件(不是 markdown);已售报告里重点商家各占一个独立 sheet。
板块 脚本 说明
1 在售抓取 jw_onsale_spider.py 免登录全量翻 index/top upsert jw_onsale_product_record+下架对账;同步写每日快照 jw_onsale_daily_record(商品+日期 upsert 保最新,供在售趋势差分);每天 09/15/20/01 四档
2+3 已售+购买记录+拆卡报告 jw_sold_spider.py hotRecommend(需登录)商家→corp/history(免登录)逐商家只增 jw_sold_product_record;再对全站已售商品抓:购买记录=赠品公示玩家维度 publicity/user/group/pager(需登录, 翻页拿全量, fetch-once)落 jw_player_record;拆卡报告 gift/report(需登录, 结束 REPORT_REFETCH_DAYS 天内每轮重查、INSERT IGNORE 去重)落 jw_report_record;每天 08:00(配合已售报告业务日窗口, 06:00 后再采)
4 每日报告 jw_onsale_report.py(09/15/20/01 四档) + jw_sold_report.py(09:10) 只读库→Excel→企微发文件;共用样式库 jw_report_excel.py(openpyxl)。在售每档各出一份带小时文件、含「在售趋势」sheet(快照差分);已售按业务日窗口 [昨17:00,今06:00]、COALESCE(finish_time,soldout_time) 过滤,Jake/九叔 各独立 sheet(汇总+明细+购买人数),金额已是元
5 在售监控 jw_onsale_alert.py 手写轮询(默认全天、每轮随机 60~90s),全量翻 index/top(免登录) 按 corpInfoId 过滤 Jake(100716)/九叔(100715);三类告警 新品上架/进度过半(≥50%)/一车结束,去重靠 jw_onsale_alert_record 三标记位 + 内存 _seen_onsale;企微 markdown 推送
  • 依赖:openpyxl(报告用)已装。运行=5 个常驻进程各自 python xxx.py(在售/已售/两报告/监控)。schema 共 7 张表(在售商品/已售商品/商家/购买记录/拆卡报告/监控告警/在售每日快照 jw_onsale_daily_record)。
  • 建表提醒:schema 经多轮调整(金额列改 DECIMAL 存元;两商品表加 12 个详情列 + detail_fetched;已售表加 buy_fetchedjw_player_record 改玩家维度买家、无唯一键靠状态位去重;新增在售每日快照表 jw_onsale_daily_record)。CREATE IF NOT EXISTS 不会改已存在的表,故建议 DROP 掉全部 jw_ 表后 python init_db.py 重建(只加新表可直接重跑 init_db)。
  • 登录态最小化:hotRecommend(商家列表)、publicity/user/group/pager(购买记录)、gift/report(拆卡报告) need_auth=True;在售/已售/商品详情全免登录。
  • 人数口径:已售报告「购买人数」取自 jw_player_record(COUNT DISTINCT user_id);监控结束战报「购买人数」=玩家维度接口 totalCount(实时准确)。
  • 详情字段坑:merchantGoodsIdstock/surplusStock/price/highestPrice别用 index/top 的 stockAmount/residueStockAmount(监控 confirm_ended/send_ended 已按详情字段名修正)。
  • 详情补全:两商品表带一组详情字段(img/goods_type/price/original_price/plan_up_time/off_shelf_time/stock/surplus_stock/status/collection_card_name/gift_way/random_way/gift_series(赠品系列=cardGoods.giftSeries, 报告"系列"列用它)),由 jw_detail.enrich_detail(merchantGoodsId, 免登录) 逐商品补,各表 detail_fetched 状态位控制每商品补一次(在售 upsert 不覆盖这些列)。img = FILE_DOMAIN + resList 首图 resAddrFILE_DOMAIN=https://files.jiwustar.com/(逆向 EnvironmentManager 正式环境);collection_card_name/gift_way/random_way 在详情 cardGoods 子对象。
  • 两处待主公实测确认(不影响跑,只影响判定精度)
    1. 监控「新品上架」用列表字段 soldTime(上架/开售时间) ≥ 本场窗口起点(最近一个 RUN_START) 判新(is_new_arrival/_window_start)。若实测 soldTime 语义不同,改这两处。
    2. 监控「一车结束」二次确认:打 /search/app/merchantGoodsId 详情,surplusStock<=0(售罄) 或 已过 offShelfTime(下架/销售结束时间) 判结束(confirm_ended)。若 offShelfTime 语义不同,改此处。
  • 临时/探测脚本(_probe3/verify_sign/scan/tls_replay/测试_能否写py)已删。

8. 变更记录(每次修改/优化后在此追加一条,方便重开窗口接续)

2026/08/27(企微发送加重试,修偶发 TLS 瞬断导致报表发不出)

  • 现象:08/27 早上在售/已售两份日报都生成成功,但发企微都失败,日志同一报错 企微素材上传异常: ... SSLEOFError(8, '[SSL: UNEXPECTED_EOF_WHILE_READING]...')onsale_report_20260827.log:14sold_report_20260827.log:12)。
  • 根因:不是业务错误(key/文件/文案都正常,手动握手 qyapi.weixin.qq.com:443 秒连 TLSv1.3)。是上传 Excel 素材(upload_media)时到腾讯的 TLS 连接偶发被中间网络设备掐断(OpenSSL 3.0 把「对端没发 close_notify 就断开」从静默 EOF 改成硬报错 UNEXPECTED_EOF_WHILE_READING)。放大器:auto_send_wx_msg._upload_media 原为裸 try/except,一次瞬断就 return None 整份放弃、零重试,两轮恰好都撞上。
  • 修复auto_send_wx_msg.py):按骨架规范加 tenacity 重试。新增 after_log 回调 + 两个带 @retry 的内部函数 _do_upload_media(文件上传)/_post_json_with_retry(文案/文件消息);策略 = 仅对 RequestException(网络/TLS)重试、间隔 2→4→8s 递增、最多 4 次,业务错误码(errcode≠0)不重试直接失败。关键:上传函数open 放进被重试的函数体内,每次重试重开文件(multipart 流读过就到 EOF,不重开会传空体)。发送目标/业务逻辑/对外签名不变。
  • 实测:测试群(key=b8d398e2)文案+文件 errcode 均 0,通过。依赖tenacity(已装)。
  • 待办提醒:上线正式群时只改 auto_send_wx_msg.py:25WEBHOOK_URL(现指测试群,正式群 key=2d41b34f 已注释在下一行)。

2026/08/21(大检查:报告数据修复 + 全表审计)

  • 一键启动 run_all.py(主公要部署服务器):把 5 个常驻任务(在售抓取/已售抓取/在售日报/已售日报/在售监控)各拉成独立子进程并守护——巡检存活、退出的隔 5s 自动重启、Ctrl+C 优雅关闭。服务器只跑 python run_all.py 即可(各任务仍各自维护 schedule/日志、互不干扰;单跑调试仍可 python jw_xxx.py)。README §7 运行已改为推荐 run_all。
  • 在售概览对齐 decajw_onsale_report.build_overview 加指标「今日累计已售(份)」= SUM(sold_count) WHERE is_on_sale=1;改用 指标|数值 表头(write_table)替代原标题条;「今日新上架商品」→「今日新增商品」。⚠️「今日新增商家」今天因重建商家表(所有商家 gmt_create_time=今天)暂显全量 82,以后不再 DROP 重建商家表即恢复为当天真正新增。
  • 报告「gift_series」列序前移gift_series 由表末尾 ALTER MODIFY ... AFTER goods_ip_name 移到 IP名 后(两商品表);schema 同步。纯物理列序、不动数据。
  • 报告「系列」字段取错修复(主公发现):报告的「系列」原用 goods_ip_name(goodsIPName,只是 IP 大类如"球星卡/宝可梦"),但 App「赠品详情/赠品系列」页显示的系列cardGoods.giftSeries(如"2025-26 Topps Chrome Updates Hobby")。giftSeries 只在商品详情(merchantGoodsId)里。修复:两商品表加详情列 gift_series(schema + ALTER 现有表);jw_detail DETAIL_COLS 增 gift_series、parse 取 cardGoods.giftSeries(兜底顶层 specifications);已售报告 产品系列榜按 gift_series 分组、明细「系列」列用 gift_series;在售报告 商品明细「IP系列」列改为「系列」用 gift_series。已 reset detail_fetched 重跑 enrich_detail 回填(已售 123/123 到位)。:gift_series 靠详情补全,新商品由 enrich_detail 自动填、无需改 parse。
  • 已售 GMV/人均=0 根因修复jw_sold_report._sold_gmv 原用 sold=stock−residue,但已售历史 corp/historyresidueStockAmount 对成交(组齐)团恒 = 总份数(实测全 123 行 residue==stock,且 SUM(购买记录 buy_count)==stock_amount)→ 算出 sold=0、GMV=0。改为 已售团 sold=stock_amount(成交即全售出)。GMV/人均/占比/系列榜/GMV榜/集中度/进度% 全部随之修正。
  • 参与人数:人次→跨团去重人头:原平台/明细汇总/其他商家用「各团去重买家相加」(人次),与用户榜(去重人头)对不上、也不符 deca。新增 fetch_corp_distinct_buyers/fetch_platform_distinct_buyers(JOIN 购买记录 COUNT DISTINCT user_id),平台/各商家汇总均用跨团去重人头;人均消费=GMV/人头。明细列「参与人数(本团)」仍用 buyers_map(每团去重)。
  • 商家表字段名修复 + 最终定案(主公反馈 jw_shop_record 除 id 外全空):parse_shop 原字段名写错(hotRecommend 无 corpName/fansAmount/goodsAmount 等)→全 NULL。查清 hotRecommend 记录字段实为 corpInfoId/corpInfoName/number(粉丝数,主公确认)/saleNum(在售商品数,与在售表吻合)/corpLogouserId查询者本人账号id(每行恒等=21588067)、非商家id,不存;好评/新品/简介 接口体系根本无(CommonShop 模型也没有)。 jw_shop_record 最终定为 corp_info_id/corp_name/fans_amount(number)/onsale_amount(saleNum)(中途曾一度精简掉 fans_amount,后确认 number=粉丝又加回)。parse_shop/get_shops/schema 已改、DROP 重建回填。在售报告「今日新增商家/其他商家」两 sheet 列 = 商家|粉丝数|在售商品数|今日新上架(粉丝取 shop 表、在售数/新上架从在售商品表 JOIN)。
  • 商家列表翻页 bug 修复(只入库了 10 个商家)get_shops 末页判定原为 len(recs) < PAGE_LIMIT(20),但 hotRecommend 服务端每页固定只回 10 条(响应 data.limit=10,无视请求的 limit=20)→ 第 1 页 10<20 就误判末页停了,只入库 10 个商家。改为用响应里的实际每页大小 data.limit 判末页(len(recs) < data.limit 才停)。实测全量翻页 82 个商家(9 页)、其中 29 个有在售。已重跑 get_shops 入库 82 家(粉丝 Top: Jake 426/卡愣子潮玩 184/棠棠潮玩 80…)。
  • 已售改为循环库中全量商家(主公提出):main_task 原为「corp_ids = get_shops() 返回啥就循环啥」(只本轮 hotRecommend 返回的)。改为 get_shops() 先刷新商家表,再 SELECT corp_info_id FROM jw_shop_record全量商家循环抓已售(受 TARGET_CORPS 可选过滤)。理由:hotRecommend 每天轮换只返一批热门商家,jw_shop_record 随天数累积覆盖更全;对全量商家跑 corp/history(免登录)才不漏。代价:每轮多几十个 corp/history 请求(多数无已售、秒回空),免登录无账号风险。
  • live_replay_url 拼完整域名:接口返回的 liveReplayUrl 是相对路径 /live/xxx.mp4,需拼直播域名 https://play.jiwustar.com(逆向 EnvironmentManager RELEASE 的 BASE_PULL_STREAM_URL=https://play.jiwustar.com/live/,路径已含 /live/ 故只拼 host)。新增 jw_sold_spider.full_live_url(),parse_sold 用它;库内历史 123 条已 UPDATE ... CONCAT('https://play.jiwustar.com', live_replay_url) WHERE live_replay_url LIKE '/live/%' 回填、残留 0。
  • 在售「类型」列补 str()jw_onsale_report.build_productsPRODUCT_TYPE_NAME.get(ptype,...)get(str(ptype),...),与已售统一,防 product_type 为 int 时显示数字码。
  • 查清的非 bug(真实数据):① corp/history 实测仅 Jake(100716) 有 123 条已售,九叔(100715)/羽佳/其他 totalCount=0 → 已售报告只有 Jake、九叔明细/其他商家空是真实数据,非漏抓。② hotRecommend 是「热门推荐」商家、每页 10 条需翻页(全量约 82 家,见翻页修复条);不含好评/新品/简介(粉丝=number 有)。
  • fork 深审确认无误(勿误改):在售 sold=stock−residue(在售剩余是真实值,口径对)、GMV/占比/人均/环比除零护栏、窗口边界含两端、金额=单价映射、用户榜 SUM(buy_count×amount)、col_types 与列数对齐、pct 传 0~1。全表 NULL 审计:已售/购买记录/拆卡报告各列均正常填充(仅 pic_id 匿名买家空、finish_time 今日团 25 条空但 soldout_time 兜底)。

2026/08/20

  • 拆卡报告表改名jw_gift_report_recordjw_report_record。同步改:schema.sql(CREATE TABLE,索引名 uk_gift_report_id/列名 gift_report_id 跟列走、不动)、jw_sold_spider.py(insert_many 目标表 + 重查去重 SQL + 顶部注释/docstring)、README.md、本文档。需重建库(DROP 旧表后 python init_db.py)。
  • 购买记录表改名jw_buy_recordjw_player_record。同步改:schema.sql(CREATE TABLE,索引 idx_goods/idx_user 不含表名、不动)、jw_sold_spider.py(insert_many 目标表 + 注释/docstring)、jw_sold_report.py(购买人数 COUNT DISTINCT SQL + docstring)、README.md、本文档。需重建库(DROP 旧表后 python init_db.py)。
  • 监控脚本改名jw_onsale_monitor.pyjw_onsale_alert.py(与表名 jw_onsale_alert_record 配对、贴合"上架提醒"交付物)。纯文件重命名,无其它 .py import 它(独立常驻脚本直接跑);模块 docstring 未自指文件名、不用改。同步改:README.md(目录树/板块表/配置项/待实测项)、本文档。不涉及库变更。
  • 对齐 deca 两套报告口径(主公确认):
    • 已售业务日窗口jw_sold_report.pyWIN_SOLDDATE(gmt_create_time)=CURDATE() 改为 COALESCE(finish_time, soldout_time) 落在 [昨17:00, 今06:00](含两端),对齐 deca 的 completed_at 口径;平台总览新增「统计窗口」展示。配套:已售采集 jw_sold_spider.py 定时 00:30 → 08:00(窗口 06:00 关闭后再采、报告 09:10 前齐全),并清掉 fetch_sold_for_corp 里调试用的 print(j)、把 schedule_task 里启动即跑的 main_task(log=logger) 注释回去。
    • 在售四档 + 完整复刻jw_onsale_spider.py/jw_onsale_report.py 定时都从单点改成 09/15/20/01 四档(上午/下午/晚上/凌晨场);报告每档各出一份带小时文件名(_%Y%m%d_%H时)。新增每日快照表 jw_onsale_daily_record(唯一键 goods_id+snapshot_date,四档 upsert 只刷量价、保留商品/商家/日期),采集每页 upsert_daily_snapshot 同步写入;报告加「在售趋势」sheet:fetch_onsale_trend 按快照集合差分(当日集合−前日集合=净新增商家/拼团),列头 日期|在售商家数|在售拼团数|新增商家数(差分)|新增拼团数(差分),倒序最近 4 天。
    • 需建新表jw_onsale_daily_record 已进 schema.sql,重跑 python init_db.py 即可(CREATE IF NOT EXISTS 只补新表)。同步改 README(板块表/表清单/运行命令/DROP 列表)。
  • 在售报告补齐到 6 sheet(对齐 deca)jw_onsale_report.py 新增两 sheet——「其他商家」(存量商家 gmt_create_time!=CURDATE(),商家表 LEFT JOIN 在售表,列 商家/粉丝数/在售商品数/今日新增商品,在售数倒序 LIMIT 100) 和「上架时段分布」(按 HOUR(sold_time) 24 桶、近 7 日,带 █ 条形)。最终顺序:概览/今日新增商家/其他商家/商品明细/在售趋势/上架时段分布。已实测生成 6 sheet 通过。
  • alert 同步 deca 最新逻辑jw_onsale_alert.py,得卡 onsale_alert_spider.py 2026/08 改版):
    1. 新品判断:由「soldTime 最近 60 分钟」改为「soldTime ≥ 本场窗口起点(最近一个 RUN_START)」,新增 _window_start();删除 NEW_ARRIVAL_LOOKBACK_MIN
    2. 结束判断confirm_ended 由「仅 surplusStock≤0」改为「surplusStock≤0 OR 已过 offShelfTime(下架/销售结束时间)」,二者任一即结束。
    3. 运行窗口RUN_ALL_DAY 由 True 改 False,对齐 deca 窗口制 [20:30, 06:00];RUN_START 同时是新品判定门槛。如集物直播时段不同,改 RUN_START/RUN_END 或设回 RUN_ALL_DAY=True。
    4. 命令行时间参数(对齐 deca argparse):_parse_start_time(校验 HH:MM/HH:MM:SS→规范化 HH:MM) + _parse_args(位置参数 start--start 等价、位置优先);__main__ 里不传保持默认 RUN_START,传了则 RUN_START=_start 覆盖模块全局(_window_start/_in_run_window/_seconds_to_next_window 三处裸读该全局,一改齐变)。用法:python jw_onsale_alert.py 17:00--start 17:00。已实测 --help/校验/非法格式报错通过。
    5. 其余(三标记位+_seen_onsale去重、发成功才置位/过半先置位、免登录全站取数、企微 markdown 不挂链接)集物本就与 deca 一致,未动。
  • 已售报告重写为 9 sheet(对齐 deca 8 sheet + 集物两重点商家都有购买数据各加一个用户榜)jw_sold_report.py 全量重写。sheet:①平台总览(KPI+当日环比vs昨日同窗口 WIN_SOLD_YDAY+商家GMV集中度Top1/3/5/10) ②产品系列榜(goods_ip_name,Top15) ③商家GMV榜(Top10) ④运营节奏(重点商家快照:已售团数/参与人数/规格分布 + 成交时段分布 HOUR(COALESCE(finish,soldout)) 24桶近7日) ⑤Jake明细(汇总+逐团明细+购买记录覆盖检测 buy_fetched) ⑥Jake用户排行榜(jw_player_record join 已售表算金额) ⑦九叔明细 ⑧九叔用户排行榜 ⑨其他商家。GMV=已售数×单价元、已售数=stock-residue、参与人数=jw_player_record 去重 user_id。deca 的「进度25/50/75%里程碑」列集物无进度轨迹、略去。已实测 9 sheet 生成通过。同步改 README。
  • 已售报告排版对齐 deca 实际输出(读了 deca stats/得卡已售每日报告_*.xlsx + stats/daily_report.py:988-1210 build 代码逐 sheet 比对,首版是「重新设计」标签对不上、返工):标题「集物星球 · 已售每日统计报告」+「成交时间窗」;平台汇总用 deca 标签(商家数/销售额/成团数/参与人数/均拼单价/人均消费);环比行(组齐GMV/成团数/活跃商家数/T均单价);运营快照(今日新开团/已组齐/规格分布,新开团=sold_time 落窗口内);明细列(序号|团名(商品标题)|系列|类型|单价|总份数|进度%|总金额|参与人数(本团)|开售时间|成交时间|售卖时长);覆盖检测用「成交X团·采到Y团·漏采Z团(用户排行见「…」sheet)」+漏采逐行「 - {goods_id} {goods_name}」。两处有意差异:集物两重点商家都有真实购买记录→参与人数全用真实去重买家(deca 非重点商家用中卡近似)、其他商家亦然;集物无进度轨迹→去掉 25/50/75%里程碑列。售卖时长=成交时间-开售时间「X小时Y分」。已实测发测试群通过。
  • 已售报告排版 bug 修复(标题截断):主公反馈"显示不完全/格式乱"。根因:write_section_title合并单元格——合并区内文字无法溢出,列窄(尤其空数据 autosize 压到最小)时长标题被切在合并区右边界("占平台组齐总 ("、"当日 Top15,"都是这么被切的;公式栏文字其实是全的,纯显示截断)。修法照抄 deca _write_section_title/_style_row:改为「不合并 + 给若干列铺蓝底(空单元格) + 文字写 A 列不换行」,让文字自然溢出到右侧有色空单元格上(非合并单元格文字能无限溢出、永不截断);铺色跨度固定 = 调用方给的 ncols同一 sheet 内所有分区标题条传同一 ncols → 色条等长、严谨;之前用随标题长度自适应的跨度导致同 sheet 内长短不一被主公吐槽,已改)。为保证窄表(空数据 autosize 压窄)时色条仍罩住长标题:_autosize 改「只增不减」(不覆盖预设更宽列宽),并给 build_series/build_merchant_rank 首列预设 24/22 宽。已用脚本全 sheet 复检:各 sheet 标题条 span 统一 + 每条色条宽 ≥ 标题显示宽(全部罩住)。
  • 登录/会话流程图(README + HANDOFF):README 新增 §1.1「登录 / 会话流程(jiwu_core)」、HANDOFF §3 末尾同步一份,用 Mermaid flowchart 画 do_request 决策链(免登录匿名 sid vs 需登录 ensure_session/login → 三处 sessionId 同值 → _sign 签名 → curl_cffi 发 → code==1 判定/失效重试),并列出免登录/需登录各接口与"最小登录面"动机。纯文档。另:新增 write_title()(大标题不合并不填色、自然溢出);build_overview 大标题改 write_title、"成交时间窗"改纯文本行(不再当 KV 挤窄 B 列折 4 行)。KV 蓝底标签/橙底数值保留(deca _write_summary_block 就是这配色)。:样式库在售/已售共用,两报告同时生效。数据全 0 属正常:已售表 60 行成交时间在 08-17~08-19 白天、不落今天窗口 [昨17:00,今06:00],非 bug。

2026/08/19

  • 金额单位更正:所有金额 ÷1000000=元(实测 amount=99000000→App ¥99.00),原文档写的 ÷10000 是错的。且入库前用 core.to_yuan 换算成元存 DECIMAL(12,2)(parse_product/parse_sold/parse_detail_fields),报告 yuan() 改透传不再除;监控读实时接口(原始值)仍 ÷1000000 展示。
  • 购买记录接口定案/order/merchant/app/query/gift/publicity/user/group/pager(赠品公示·玩家维度,需登录),翻页拿全量买家(userId/userNick/picId/count)。曾误用详情内嵌 goodsBuyRspDtoList(仅6条预览)和幻影接口 publicity/pager,均已废弃。jw_player_record 无唯一键,靠已售表 buy_fetched 状态位「取全→入库→置位」控制每商品抓一次。
  • 拆卡报告gift/report(需登录,sbt=6) 落 jw_report_record;全站已售、结束 REPORT_REFETCH_DAYS(3)天内每轮重查(INSERT IGNORE 补迟到更新)。
  • 详情补全:新增 jw_detail.py,两商品表补 12 个详情列 + detail_fetched(img=FILE_DOMAIN+resList首图;FILE_DOMAIN=https://files.jiwustar.com/;collection_card_name/gift_way/random_way 在 cardGoods)。
  • 登录态最小化do_request(need_auth=False) 默认免登录;仅 hotRecommend/购买记录/拆卡报告 需登录。
  • 板块结构:在售(jw_onsale_spider)独立日更;购买记录+拆卡报告并入已售(jw_sold_spider)。曾短暂把在售+购买记录合并成 jw_onsale_buy_spider,后因购买记录可对已售事后查全量而拆回、该文件已删。
  • 详情字段坑:merchantGoodsId 用 stock/surplusStock/price/highestPrice(≠ index/top 的 stockAmount/residueStockAmount);监控 confirm_ended/send_ended 已按详情字段名修正。
  • 自检修正:清掉遗留的幻影接口/旧字段引用(jiwu_core main 自测第 3 项改玩家维度接口;相关 docstring/注释)。全量 py_compile 通过、无悬空函数引用。
  • ⚠️ 待优化(下次再聊):监控 fetch_watch_onsale 每 60~90s 全量翻 index/top 4 个 productType 的所有页——全站在售量大时请求数偏高、有风控风险。可选优化:加大轮询间隔 / 只翻前 N 页(新品通常靠前) / 找更轻的按商家在售接口。