用途:新开的 Claude 窗口读此文件即可接续工作,无需回溯历史对话。 目标平台:得卡 DECA App(
api.decalive.com)。项目根目录:D:\work\2026-08-02(deca_spider)。 最后更新:2026/08/10。
抓取得卡 DECA 平台数据(商家 / 商品 / 卡密 / 拆卡报告 / 已售拼团),入 MySQL, 并对指定商家做「新品上架 + 进度过半」实时提醒。当前监控商家:881226408。
charley-utils(editable 安装),直接 from mysql_pool import MySQLConnectionPool。application.yml(mysql.host/port/username/password/db)。requests、parsel、loguru、tenacity、schedule、pymysql、DBUtils、PyYAML、wxauto4。| 文件 | 职责 |
|---|---|
deca_sold_core.py |
核心库:签名/token/请求/解析全在这。被各爬虫 import(import deca_sold_core as core)。关键函数见下。 |
sold_history_spider.py |
已售拼团历史全量抓取(一次性跑完存量)。 |
sold_daily_spider.py |
已售拼团每日增量抓取(常驻定时)。 |
onsale_alert_spider.py |
在售提醒:监控 881226408 在售商品,按上架时间提醒本轮窗口起点后新上架 + 进度过半。默认每天 20:30~次日06:00 常驻轮询、窗口外休眠;开始时间可命令行传参(python onsale_alert_spider.py 17:00,2026/08/20)。 |
deca_wechat.py |
PC 版微信(wxauto4)发送:send_files() 发文件、send_text() 发文本。2026/08/11 起已停用、保留可切回。 |
auto_send_wx_msg.py |
企微群机器人发送(2026/08/11 起为主发送渠道,三脚本共用唯一一份):send_wechat_group_msg() 发 text/markdown_v2、send_wechat_group_file() 发文件(Excel)。只在根目录留一份,on_sale/、stats/ 脚本用 sys.path 引导 import 根目录这份。换群只改这一处 WEBHOOK_URL,当前指测试群。 |
schema.sql |
全部建表 DDL(含 deca_onsale_alert_record 提醒表)。 |
init_db.py |
读 schema.sql 逐条建表(幂等)。python init_db.py。 |
stats/daily_report.py |
已售每日报告:生成 Excel 单 Sheet 分区报告(平台/881../274../其他商家)。根目录跑 python stats/daily_report.py。 |
stats/stats_sold.sql |
统计 SQL(Navicat 手动跑):平台大盘 + 商家 881226408 明细,按 completed_at [昨天17点,今天3点] 窗口。 |
stats/export_teams_excel.py |
把 球队.json 的 teams(中文名/英文名/价格/数量) 导出到 Excel。 |
stats.sql |
早期统计 SQL(历史遗留,以 stats/stats_sold.sql 为准)。 |
buy_record_analysis/buy_record_spider.py |
购买记录常驻采集:多商品自适应频率轮询详情页 purchaseRecords(10 条窗口),去重累积买家名单到 deca_buy_record。两种模式:WATCH_CODES 白名单单/多商品测试 vs 空则跟 MERCHANT_ID 商家全在售。详见子项目 README。 |
buy_record_analysis/README.md |
购买记录采集子项目文档(数据模型/去重逻辑演进/使用说明)。 |
deca_team_spider.py |
随机团 teams 采集(2026/08/11 新增;2026/08/20 由 on_sale/ 迁回根目录):每 5 分钟一轮,选队随机团抓 team-options 存表 + 算总价、剩余随机团存 snapshot + 用「原始总价 − 当前剩余」实时算总价。必须常驻——转成剩余随机后 team-options 永久返 29000,原始 cardCount 再无接口能补。 |
docs/选队随机与剩余随机_总价口径与采集_20260811.md |
随机团总价口径完整设计文档(业务模型/公式/接口坑/数据模型/常见问题)。改这块前务必先读。 |
docs/在售商品进度时间序列_采集与统计_20260811.md |
进度快照文档:表结构、变化才写机制、常用统计 SQL(画曲线/算速度/找热销时段)。做进度类统计前必读。 |
README.md |
逆向分析文档(签名/token 机制)。 |
PAGE_SIZE = 20:列表每页条数。after_log(retry_state):tenacity 重试回调(业务函数首参约定为 log)。make_signature(params, current_time):请求签名。do_request(log, path, body, need_auth=False) / do_get(...):带重试的 POST/GET,返回 JSON。ensure_token(log) / refresh_token / login:token 管理。ensure_token 加跨进程文件锁(_token_lock)串行化「读→续签→写回」防多进程踩废 refreshToken;续签失败自动 login 密码登录兜底(带 5 次熔断)。(2026/08/20 根治,详见 §7)parse_product(item) / parse_shop / parse_kami / parse_report:各实体解析成 dict。run_pipeline(log, pool, incremental):历史/增量抓取总编排。upsert_team_row / fetch_team_options_or_none / compute_random_team_amount_and_persist / backfill_team_amount(2026/08/11 新增):随机团 teams 明细存储与总价计算,供 fill_details 与 deca_team_spider 共用。deca_ 前缀,详见 schema.sql)deca_shop_record、deca_product_record、deca_kami_record、deca_report_record、deca_sold_record 等。deca_groupbuy_team_record(2026/08/11 新增,随机团 teams 明细)关键列:
product_code、play_type_name、data_source(team_options/snapshot)、
team_id、unit_price、card_count(选队阶段有;剩余快照 NULL)、available_stock、
sold_count、snapshot_total_quantity、captured_at。(product_code, team_id, data_source):选队阶段每轮覆盖成最新;剩余快照冻结一次。team_total_amount decimal(14,2):
deca_onsale_product_record.team_total_amount / deca_product_record.team_total_amountCOALESCE(team_total_amount, unit_price*sold_count)。deca_onsale_product_progress_record(2026/08/11 新增,在售商品进度时间序列,append-only)关键列:
product_code (关联)、merchant_user_id、sold_count、available_stock、card_count、
progress_pct、unit_price(存历史值,随机团单价会变)、captured_at。(product_code, captured_at) + (captured_at)。buy_record_spider::snapshot_onsale_progress 每 60 秒一次,只对 sold_count 变化的商品 INSERT,避免"没卖动"重复占位。deca_onsale_alert_record(在售提醒状态表)关键列:
product_code(uk)、merchant_user_id(idx)、merchant_name、title、unit_price、
card_count、sold_count、progress(decimal 5,2)、available_stock、groupbuy_status_name、
share_code(varchar 512, 旧分享码列;2026/08/11 起消息去链接后不再写入,保留不影响)、
new_notified(tinyint 0/1)、half_notified(tinyint 0/1)、
gmt_create_time、gmt_modified_time。new_notified/half_notified 两个标记位,每类每商品只提醒一次。deca_buy_record(购买记录累积表,buy_record_analysis/buy_record_spider.py 用)关键列:
product_code、user_id、nickname(服务端已脱敏)、card_count、
purchased_at_text(相对文本原文)、purchased_at_ts(反推的绝对时间戳,秒)、
purchased_at(datetime)、first_seen_at、gmt_create_time、gmt_modified_time。uk_buy(product_code, user_id, card_count, purchased_at_ts) 仅作 DB 兜底;
主判重在应用层:(product_code, user_id, card_count) 分组 + 反推 purchased_at_ts ±70s 视为同一笔(详见子项目 README「去重逻辑演进」)。配置区(文件顶部):
MERCHANT_ID = "881226408" # 监控商家
HALF_THRESHOLD = 0.5 # 过半阈值
SEND_CHANNEL = "qywx" # qywx=企微群机器人(默认) / pc=PC微信(wxauto4,可切回)
WX_TARGET = "得卡-通知" # PC 微信发送目标(仅 SEND_CHANNEL="pc" 时用)
MIN_INTERVAL_SEC = 60 # 轮询随机间隔下限
MAX_INTERVAL_SEC = 90 # 轮询随机间隔上限
RUN_START = dtime(20, 30) # 运行窗口开始默认 20:30;命令行可传参覆盖(2026/08/20),同时是「新上架」门槛
RUN_END = dtime(6, 0) # 运行窗口结束:次日 06:00(跨午夜;2026/08/15 由 03:00 延来)
MAX_PROD_PAGES = 100 # 在售翻页保护上限
# 注:COLD_START_PUSH 已废弃(2026/08/08)——不再有"首次查询发全量快照"这个开关。
每轮 run_once 行为:
fetch_onsale 复用 daily 的 fetch_all_onsale(home/search 免 token 拉全站在售、只拉不落库),再按 merchant_user_id 筛出本商家、按 product_code 去重。不再翻 on-sale-list。publishAt(上架时间),与本轮窗口起点(最近的 RUN_START,默认 20:30、可传参,_window_start)比较:
publishAt >= 窗口起点 → 记入新品提醒(new_notified 先记 0,发成功再置 1)。publishAt 早于窗口起点(或取不到上架时间)的老货 → 直接入库、new_notified=1,不提醒。sold/card 首次 ≥ 0.5 → 记入过半提醒(half_notified 逻辑独立不动)。is_cold 只用于打日志),照样只提醒「窗口起点后新上架」,窗口起点前的老货一律静默建档。即:首次查询不会把当前所有在售商品打包推送,但会提醒本轮窗口起点(默认 20:30)之后才上架的新品。SEND_CHANNEL="qywx"):新品/过半各发一条 markdown_v2、一车结束战报每车一条,均走 auto_send_wx_msg.send_wechat_group_msg。PC 微信合并发送(_dispatch_pc_combined)保留、切回 "pc" 才用。通知条目格式(_fmt_item/_fmt_ended):标题 + 信息行(价格/份数或进度%/余·共)。2026/08/11 起不挂商品链接——企微渠道标题 markdown 加粗,PC 渠道纯文本直出。三类提醒:新商品上架 / 拼团进度过半 / 一车结束战报。
为什么去掉商品链接(2026/08/11 逆向结论):分享落地页前端拿 shareCode 调 POST /api/v1/app/groupbuy/share-detail(免 token/签名)。groupbuy/detail 免 token 返回的 data.shareCode,share-detail 一律 code=10001 分享链接已失效;只有带 token(登录态) 拉详情返回的 shareCode 才 code=0 成功。shareCode 是登录态绑定令牌,为不引入登录态(降账号风控)直接放弃链接:已删 get_share_code/_ensure_share_code、get_detail_meta→get_publish_at、_insert_alert 不再写 share_code 列。
运行:python onsale_alert_spider.py(默认 20:30 开始、企微渠道,需 auto_send_wx_msg.WEBHOOK_URL 配好)。
可指定开始时间(2026/08/20):位置参数或 --start,格式 HH:MM[:SS]。传 17:00 即窗口起点改 17:00——17:00 后才开始轮询,且只提醒 17:00 之后新上架的商品。
python onsale_alert_spider.py 17:00 # 位置参数
python onsale_alert_spider.py --start 17:00 # 等价写法
WEBHOOK_URL 换正式群:当前 WEBHOOK_URL 指测试群,正式上线换正式群机器人 key——只改根目录 auto_send_wx_msg.py 一处(三脚本共用这一份)。login 会累计失败到 5 次熔断并需人工介入。stats_sold.sql 字段核对:第四段用到 p.completed_at,需确认 schema 里确有该列,否则调整。
4.(可选)PC 微信渠道已停用(deca_wechat),onsale_alert_spider.py 切回 SEND_CHANNEL="pc" 可复用。_record 结尾、自增 id 主键、时间字段固定 gmt_create_time/gmt_modified_time(datetime)。INSERT IGNORE + 业务唯一键去重。--version 实时探测,禁止凭记忆。现象:deca_team_spider.py 连日 team-options 全线失败、日志刷屏「token 续签无返回 access:当前账号长期未登录」(0814~0819 续签成功 0 次);同期已售抓取正常,故最初误判为账号问题。实测当前 refreshToken 续签 0.5s 成功 → 账号本身没坏。
根因(多进程踩踏):多个共用根目录 token.json 的进程 + 得卡 refreshToken「一次性轮换」(续签成功即发新 refresh、旧的立即作废)+ ensure_token 只认进程内存 refresh(原 if not _TOKEN.get('refresh'): load_token(),内存一旦有值就永不重读磁盘)。任一进程续签即轮换 refresh、其它常驻进程内存那份立刻作废;team_spider 是 5 分钟一轮的常驻进程,一旦被踩废便永久失败(从不 reload),只有免 token 的在售能采、team-options(need_auth) 全废。已售正常是因为 sold_daily 每天单次运行、每次新进程读磁盘最新 refresh,踩踏窗口极小。(早在 2026/08/06 §7 就预警过 refresh 轮换踩踏,但当时只统一为一份 token.json,未解决并发轮换 + 内存不 reload。)
修复(改 deca_sold_core.py):
_token_lock(Windows msvcrt / POSIX fcntl,进程崩溃由 OS 自动释放、无残留死锁;拿不到锁 30s 超时兜底放行)。ensure_token 重写:内存 access 未临期走免锁快路径;临期/缺失才抢锁,锁内先 load_token() 读磁盘最新 refresh、再双检(别的进程刚续好就直接复用、不重复续签),最后才续签。→ N 进程同时过期只续签 1 次、refresh 只轮换 1 次,物理上杜绝踩踏。login:refreshToken 续签失败自动密码登录换新 token(接口 /api/v1/app/auth/password/login,签名同 make_signature,实测 code=0)。带 MAX_LOGIN_ATTEMPTS=5 次熔断:累计登录失败达 5 次不再请求登录接口(避免账号异常/验证码时狂调),登录成功或续签成功即清零。凭证在 core 配置区 LOGIN_PHONE/PASSWORD/COUNTRY_CODE(敏感,勿外传/勿提交公开仓库)。验证:① 3 进程并发抢 token → 只续签 1 次、成功 3/3、0 长期未登录;② 置无效 refresh → 续签失败(复现「长期未登录」)→ 自动密码登录兜底 → token.json 复有效、熔断计数 0。详见 docs/优化记录_token踩踏根治与登录兜底_20260820.md。
运维提醒:更新代码后需重启所有共用 token.json 的常驻进程(尤其 deca_team_spider.py,启动命令已变为根目录 python deca_team_spider.py),让进程内存捡起有效 refresh。
需求:RUN_START 原写死 20:30,希望能按需指定——传 17:00 就 17:00 开始,且 17:00 之后新上架的才提醒。
改动:RUN_START 这一个常量本就同时驱动「运行窗口起点」与「新上架时间门槛」两处逻辑,故只把它做成命令行可覆盖、默认 20:30,两处一起跟着变,无需拆两个参数。
_parse_start_time(解析 HH:MM[:SS],非法格式 argparse 报错)、_parse_args(位置参数与 --start 等价,位置参数优先)。__main__ 解析后覆盖模块级 RUN_START 并打日志;未传则保持默认 20:30。RUN_START(docstring、窗口/首次运行/静默建档日志)。用法:python onsale_alert_spider.py 17:00 或 --start 17:00;无参默认 20:30。
验证:py_compile 通过(Python 3.12.10);argparse 逻辑隔离测试(默认/位置/--start/带秒/非法)全过。详见 docs/优化记录_deca_spider_20260820.md。
deca_onsale_product_progress_record)背景:在售提醒能提示"进度过半"但只是事件,没有历史轨迹。主公做统计需要完整进度曲线(什么时候卖到多少百分比、每小时卖多少、热销时段)。
方案:
deca_onsale_product_progress_record(append-only 时间序列),字段见 §3。用 product_code 关联主表,商品静态信息不复制。buy_record_spider::snapshot_onsale_progress(新增函数),挂在 ingest_onsale 里 get_onsale_products 之后。每 60 秒(复用现有节奏)跑一次:先拿 progress 表每商品最新 sold_count 作基线,跟 onsale 主表当前状态比对,sold_count 变化 或 从未记录 才 INSERT——避免"没卖动"重复占位,数据量省 3~5 倍。unit_price 存历史值(随机团单价会变,主表覆盖后就丢了当时值)。已验证:测试跑 2 轮:首轮写 201 条(基线)、2 秒后次轮只 9 条(真卖动的 9 个团)。热销例子:GB26080604252 短短 3 秒 sold 11055→11125,两行都留下。
完整设计:docs/在售商品进度时间序列_采集与统计_20260811.md(含常用统计 SQL)。
运维:只要 buy_record_spider 常驻跑,本表自动增长。停机期间不记,跟 buy_record 一样属"停机=断档"。
背景:得卡的"选队随机 / 剩余随机"其实是同一个团的两个阶段(选队卖不动→转剩余)。每支球队价格不同、便宜的先卖光——现有 unit_price × sold_count 严重失真(选队随机团甚至商品级 unit_price=0,直接算出 0 元;已售剩余随机的 top.soldCount/totalCardCount 是原箱母单聚合数,多个子团共用同一个 10697,unit×sold 会把 74727 算成 47.8 万)。
关键约束(实测):team-options 只在选队随机状态可查;一旦转成剩余随机就永久返 29000 "商家开启剩余随机中"。任何接口都无法回补每队原始 cardCount(详情快照里也没有),必须在选队阶段提前抓、存表。
方案:
deca_groupbuy_team_record 存逐队明细(data_source 区分 team_options / snapshot);两张商品表加列 team_total_amount decimal(14,2)。on_sale/deca_team_spider.py 常驻,每 5 分钟一轮:选队随机团抓 team-options→存表+算 Σ 单价×(cardCount-availableStock);剩余随机团存 detail snapshot(首次冻结)+ 用 Σ 单价×cardCount(存表原始) − detail.unitPrice×detail.availableStock 算实时总价。上线前已是剩余随机、库里没原始 cardCount 的老团,team_total_amount=NULL(数据缺失,报告端 COALESCE 回落原公式)。deca_sold_core.fill_details 顺手做剩余随机 snapshot 存表 + 算 Σ 单价×availableStock(不新增网络请求,复用已拉到的 detail);run_pipeline 加 backfill_team_amount 步骤 3.5 存量收敛。on_sale/deca_on_sale_report.py 的 prod_sql 末尾 SELECT COALESCE(team_total_amount, unit_price*sold_count) AS total_amount;stats/daily_report.py 4 处销售额 SQL 同样 COALESCE。已验证:已完成剩余随机 12/12 存量回补成功(如 GB26081095285=74727.50、GB26081062215=70776.00、GB26081009714=51157.50);在售 team_spider 一轮跑通(选队 2 成功 / 剩余 3 成功,3 个失败是刚转剩余随机的正常业务态)。
完整设计:见 docs/选队随机与剩余随机_总价口径与采集_20260811.md(改这块前必读)。
运维:deca_team_spider.py 需常驻(python deca_team_spider.py,2026/08/20 由 on_sale/ 迁回根目录);与 alert / sold_daily 共用根目录 token.json(续签已加跨进程锁 + 登录兜底防踩踏,见 §7 2026/08/20)。
COLD_START_PUSH:删除「首次查询发全量在售快照」这条分支与开关。冷启动不再推送快照。RUN_START(20:30)~次日 RUN_END(03:00) 轮询,窗口外休眠到下次开窗(_in_run_window / _seconds_to_window / schedule_task)。get_detail_meta(一次详情同时取 shareCode + publishAt)、_window_start、_is_new_arrival。只有 publishAt >= 本轮窗口起点(20:30) 才当新品提醒,早于窗口起点或取不到时间的一律静默建档。冷启动与常规轮共用此判断,首次查询不再区别对待(仅日志提示)。fetch_onsale 改为复用 fetch_all_onsale(home/search 免登录拉全站在售)再按商家筛,不再翻 on-sale-list。
> 注:下方 2026/08/04 记录中提到的"冷启动快照"分支,已由本次改版整体移除,仅作历史保留。补 share_code 列:库里 deca_onsale_alert_record 是加该列前建的旧结构,缺 share_code,
脚本查询报 1054 Unknown column。已 ALTER TABLE ... ADD COLUMN share_code varchar(512)
... AFTER groupbuy_status_name(与 schema.sql 对齐),其余列全部对齐。
修分享链接取值路径:详情接口 groupbuy/detail 的分享码在 data.shareCode(data 顶层),
原代码取 data.shareResp.shareCode(该字段实际为 null),导致提醒无链接。已改 get_share_code。
注:在售列表 on-sale-list 返回项不含 shareCode,分享码只能走详情接口,get_share_code 必需保留。
new_notified「发失败不丢 + 改 0 能重发」(half_notified 逻辑不动):
new_notified 先记 0,消息发送成功后才批量置 1(新增 _mark_new_notified)。
_dispatch / _dispatch_pc_combined 改为返回 bool;send_text 失败(如锁屏)→ 标记保持 0 → 下轮自动重发。run_once 的 SELECT 补上 new_notified;已在表但 new_notified=0(上次发失败残留 / 人工改回 0)
的商品重新纳入新品提醒,发成功后再置 1。踩坑(PC 微信 wxauto4):报错 'NoneType' object has no attribute 'GroupControl' = wxauto4 抓不到微信主窗口,
根因通常是电脑锁屏(锁屏后 Windows 挂起桌面 UI 渲染,UI 自动化够不着窗口),或微信被关/最小化。
对策:跑该脚本时段别锁屏(电源设置关掉自动锁屏/睡眠);无人值守需锁屏则把 SEND_CHANNEL 切 "qywx"(企微机器人走 HTTP,不受锁屏影响)。
completed_at 落在 [昨天 17:00:00, 今天 03:00:00](含两端) 的成交。
起点 (CURDATE() - INTERVAL 1 DAY) + INTERVAL 17 HOUR、终点 CURDATE() + INTERVAL 3 HOUR,每天跑一次。
注意:该窗口不覆盖当天 03:00~17:00 的成交(每天窗口为傍晚到次日凌晨),如需覆盖白天再调边界。SET @stat_day 会话变量,边界表达式内联进每段查询,两段 SELECT 各自独立、可单独选中执行
(原变量方案在只选中单条 SELECT 时因 SET 未执行导致查不出数据)。merchant_user_id(GROUP BY 保留不影响)。
completed_at 为 varchar(32) 存标准 YYYY-MM-DD HH:MM:SS,与 datetime 边界直接比较即可,无需 STR_TO_DATE。stats/daily_report.py:生成 Excel 单 Sheet 分区报告(时间窗口径同 stats_sold.sql)。结构:
平台汇总 → 881226408 汇总+每条组队明细 → 274584650 汇总+每条组队明细 → 其他商家打包汇总。
「一个拼团商品=一个组队」(组队售卖);明细列:团名/系列/类型/单价/份数/总份数/进度/总金额/中卡人数/中卡张数/成交时间/回放。
「人数」= 拆卡报告 hit_user_nickname 去重(近似,仅覆盖有报告的商品,非真实参团人头)。
从根目录跑:python stats/daily_report.py → 根目录输出 得卡已售每日报告.xlsx。groups 不能直接当列别名(报 1064),改用 grp。stats/ —— daily_report.py、stats_sold.sql、export_teams_excel.py(球队.json→Excel)。
三者都从项目根目录运行(cwd=根,共用根的 application.yml,源文件如 球队.json 也在根)。deca_kami_record 为空(FILL_KAMI 关)→ 无球队维度;274584650 窗口内仅 1 拼团且无报告→人数为 0。背景:on_sale/ 子项目(在售采集 deca_daily_spider.py,每天 09:00/15:00)与根目录项目
(sold_daily_spider.py 03:00、onsale_alert_spider.py 默认 20:30~次日06:00,起点可传参)用的是同一个得卡账号,
两套代码各有一份自包含的 token 管理(ensure_token/refresh_token,逻辑逐行一致)。
问题:TOKEN_FILE 原本是相对运行目录的 "token.json"。若各存一份副本,一旦后端
refreshToken 滚动更新(用一次换新、旧的作废),高频跑的一方(alert 一晚上刷很多次)会不断把
新 RT 写进自己那份,另一份副本的 RT 停在复制那一刻 → 迟早变死副本 → 续期失败 → 退密码登录撞验证码崩掉。
决策:全局只维护一份 token.json(D:\work\2026-08-02(deca_spider)\token.json)作单一真相源。
TOKEN_FILE="token.json" 天然指向这份,不改。on_sale/deca_daily_spider.py:已把 TOKEN_FILE 改成绝对路径指向同一份(见该文件配置区,2026/08/06)。运维:refreshToken 若彻底失效,只需 APP 重新登录抓包、把新 refreshToken 写回这一份 token.json, 两个项目一起恢复;不要再复制多份副本。(关联待办 5.2「登录改 refreshToken 续期」)
目标:绕开商品详情页「购买记录」板块固定只显示 10 条的限制,持续累积买家名单到 deca_buy_record。
关键发现(反编译 + 真机实测双向验证):
api/v1/... 路径,无 purchase-records/buy-record 类端点)。groupbuy/detail 请求体只有 code 一个字段,实测 15 种试探参数(page/pageSize/limit/purchaseRecordLimit 等)服务端 100% 忽略、恒返 10 条。hasMore/nextCursor 分页游标。
→ 唯一可行路径 = 周期轮询详情 + 应用层去重累积。主脚本 buy_record_analysis/buy_record_spider.py:
WATCH_CODES 非空 → 白名单模式(单/多商品测试);空 → 走 MERCHANT_ID 商家全在售模式,动态刷新(下线自动移出)。purchasedAt 反推时间戳跨度 span,下次间隔 = span // 3,硬夹在 [MIN_INTERVAL_SEC=0, MAX_INTERVAL_SEC=600]。全局 GLOBAL_MIN_GAP_SEC=0.2 兜底防瞬时限流。core.USE_PROXY = True(快代理隧道)默认开。groupbuy/detail 实测免登录(不带 token 也返回 code=0 + 10 条),故白名单模式全程不碰 token;仅商家模式拉在售列表 on-sale-list 需要 token(不带会 code=10002),该模式启动才 ensure_token(token.json 沿用全局单一策略)。deca_buy_record 由 schema.sql / init_db.py 负责(职责单一)。main_task @retry(stop=100, wait=3600),挂了每小时重试。去重踩过两次坑(含实测反例):
(product_code, user_id, card_count, purchased_at_minute) 分钟桶 → 100% 重复入库。原因:相对文本每 60 秒 +1,反推 ts 漂移 60s 跨桶。(product_code, user_id, card_count) 名单粒度 → 漏单。反例:王**要 x10 在 10/12 分钟前各一笔(间隔 2 分钟)被合并成一条。purchased_at_ts 仅作兜底;进程启动/新商品纳入时用 load_order_index 从库恢复 _order_idx 内存索引,避免重启后重复。实测反例场景(王**要多笔)已正确区分入库。已知局限:
nickname 返回就是 D**l/匿名用户),客户端拿不到真实名。顺带发现拆卡报告 hit_user_nickname 不脱敏,但两表间无 user_id 关联字段,只能靠昵称文本模糊 join。若未来找到「userId → 用户资料」接口,可在本表加 real_nickname 列批量补拉。purchasedAt 只精确到分钟,数据上无法与漂移区分)。此情形极罕见。nohup / tmux / supervisor 保活。清理:test_buy_record.py 已删除(功能被 buy_record_spider.py 白名单模式全面覆盖,且旧 DDL 与新表结构不兼容)。子项目当前 3 个正式交付物:buy_record_spider.py、README.md、schema.sql 里的 deca_buy_record DDL 段(第 204 行起)。