# 集物星球爬虫 · 加密/接入 交接文档
> 新窗口接续用。核心:签名 `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))`
- `canonicalBody`:`json.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_cffi`(`impersonate="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 填进去):
```json
{"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/签名串须同值):
```mermaid
flowchart TD
A["do_request(path, body, need_auth)"] --> B{"need_auth?"}
B -- "False 免登录(默认)" --> C["get_anon_sid()
随机自造 sid,不调 regLogin"]
B -- "True 需登录" --> D["ensure_session()"]
D --> E{"缓存 sid 未过期?
SESSION_TTL=1800s"}
E -- "是" --> F["复用缓存 sid"]
E -- "否" --> G["login(): 自造 sid
→ regLogin 绑定 → 写缓存"]
C --> H["sessionId 写入 body.sessionId + 头 X_SID + 签名串
(三处同值)"]
F --> H
G --> H
H --> I["_sign(): X_SIG_1 = base64(HMAC_SHA256(key, msg))
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 + 真实商品)
```python
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_fetched`;`jw_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(实时准确)。
- 详情字段坑:`merchantGoodsId` 用 `stock`/`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 首图 resAddr**,`FILE_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:14`、`sold_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:25` 的 `WEBHOOK_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。
- **在售概览对齐 deca**:`jw_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/history` 的 `residueStockAmount` 对成交(组齐)团恒 = 总份数(实测全 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`(**在售商品数**,与在售表吻合)/`corpLogo`;`userId` 是**查询者本人账号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_products` 的 `PRODUCT_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_record` → `jw_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_record` → `jw_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.py` → `jw_onsale_alert.py`(与表名 `jw_onsale_alert_record` 配对、贴合"上架提醒"交付物)。纯文件重命名,无其它 .py `import` 它(独立常驻脚本直接跑);模块 docstring 未自指文件名、不用改。同步改:`README.md`(目录树/板块表/配置项/待实测项)、本文档。**不涉及库变更。**
- **对齐 deca 两套报告口径**(主公确认):
- **已售业务日窗口**:`jw_sold_report.py` 的 `WIN_SOLD` 从 `DATE(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/校验/非法格式报错通过。
- 其余(三标记位+`_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 页(新品通常靠前) / 找更轻的按商家在售接口。