# 得卡 DECA 爬虫项目 · 交接文档 > 用途:新开的 Claude 窗口读此文件即可接续工作,无需回溯历史对话。 > 目标平台:得卡 DECA App(`api.decalive.com`)。项目根目录:`D:\work\2026-08-02(deca_spider)`。 > 最后更新:2026/08/10。 --- ## 1. 项目概况 抓取得卡 DECA 平台数据(商家 / 商品 / 卡密 / 拆卡报告 / 已售拼团),入 MySQL, 并对指定商家做「新品上架 + 进度过半」实时提醒。当前监控商家:**881226408**。 - Python:3.12.10 - 公共库:`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`。 --- ## 2. 文件清单与职责 | 文件 | 职责 | |---|---| | `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 机制)。 | ### deca_sold_core.py 关键函数(供 import 复用) - `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` 共用。 --- ## 3. 数据库表(`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_amount` - 随机团(选队随机/剩余随机)按 teams 逐队精算写入;固定价团保持 NULL。 - 报告端统一 `COALESCE(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,避免"没卖动"重复占位。 - 用途:进度曲线、售卖速度、热销时段等统计。SQL 举例见 docs 里的文档。 - **`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「去重逻辑演进」)。 --- ## 4. onsale_alert_spider.py 当前逻辑(重点) **配置区**(文件顶部): ```python 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` 行为**: 1. 拉在售:`fetch_onsale` 复用 daily 的 `fetch_all_onsale`(home/search **免 token** 拉全站在售、只拉不落库),再按 `merchant_user_id` 筛出本商家、按 `product_code` 去重。不再翻 `on-sale-list`。 2. **提醒门槛按「上架时间」判定**(关键):对库里没见过的商品,打一次详情拿 `publishAt`(上架时间),与**本轮窗口起点**(最近的 `RUN_START`,默认 20:30、可传参,`_window_start`)比较: - **新品上架**:`publishAt >= 窗口起点` → 记入新品提醒(`new_notified` 先记 0,发成功再置 1)。 - **静默建档**:`publishAt` 早于窗口起点(或取不到上架时间)的老货 → 直接入库、`new_notified=1`,**不提醒**。 - **进度过半**:`sold/card` 首次 ≥ 0.5 → 记入过半提醒(`half_notified` 逻辑独立不动)。 3. **首次查询(库内该商家零记录)不再发全量快照**——冷启动与常规轮走**同一套**判断(`is_cold` 只用于打日志),照样只提醒「窗口起点后新上架」,窗口起点前的老货一律静默建档。即:首次查询**不会**把当前所有在售商品打包推送,但**会**提醒本轮窗口起点(默认 20:30)之后才上架的新品。 4. **发送渠道(2026/08/11 起默认企微 `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 之后新上架的商品。 ```bash python onsale_alert_spider.py 17:00 # 位置参数 python onsale_alert_spider.py --start 17:00 # 等价写法 ``` --- ## 5. 待办 / 未决事项 1. **企微 `WEBHOOK_URL` 换正式群**:当前 `WEBHOOK_URL` 指**测试群**,正式上线换正式群机器人 key——**只改根目录 `auto_send_wx_msg.py` 一处**(三脚本共用这一份)。 2. ~~**登录改 refreshToken 续期**~~(2026/08/20 已处理):token 以 refreshToken 续期为主 + 密码登录兜底(带 5 次熔断);多进程踩废 refreshToken 的根因已用跨进程锁根治(见 §7)。密码登录实测 code=0 可用、暂未触发阿里云验证码;若日后被验证码拦,`login` 会累计失败到 5 次熔断并需人工介入。 3. **`stats_sold.sql` 字段核对**:第四段用到 `p.completed_at`,需确认 schema 里确有该列,否则调整。 4.(可选)PC 微信渠道已停用(`deca_wechat`),`onsale_alert_spider.py` 切回 `SEND_CHANNEL="pc"` 可复用。 --- ## 6. 全局规范提醒(来自 CLAUDE.md) - 建表:表名 `_record` 结尾、自增 `id` 主键、时间字段固定 `gmt_create_time`/`gmt_modified_time`(datetime)。 - 只增不改的数据走 `INSERT IGNORE` + 业务唯一键去重。 - 数据库默认只读;写操作需先说明并获确认。 - 函数强制完整 Google Style docstring;关键逻辑行内注释解释「为什么」。 - 版本号写入前用 `--version` 实时探测,禁止凭记忆。 --- ## 7. 变更记录 ### 2026/08/20 · token 续签多进程踩踏根治 + 密码登录兜底 + team_spider 迁根目录 **现象**:`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`)**: 1. **跨进程文件锁 `_token_lock`**(Windows `msvcrt` / POSIX `fcntl`,进程崩溃由 OS 自动释放、无残留死锁;拿不到锁 30s 超时兜底放行)。 2. **`ensure_token` 重写**:内存 access 未临期走**免锁快路径**;临期/缺失才抢锁,锁内先 `load_token()` 读磁盘最新 refresh、再**双检**(别的进程刚续好就直接复用、不重复续签),最后才续签。→ N 进程同时过期只续签 1 次、refresh 只轮换 1 次,物理上杜绝踩踏。 3. **密码登录兜底 `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。 --- ### 2026/08/20 · 在售提醒运行窗口开始时间改命令行可传参(onsale_alert_spider.py) **需求**:`RUN_START` 原写死 20:30,希望能按需指定——传 `17:00` 就 17:00 开始,且 17:00 之后新上架的才提醒。 **改动**:`RUN_START` 这一个常量本就同时驱动「运行窗口起点」与「新上架时间门槛」两处逻辑,故只把它做成命令行可覆盖、默认 20:30,两处一起跟着变,无需拆两个参数。 1. 新增 `_parse_start_time`(解析 `HH:MM[:SS]`,非法格式 argparse 报错)、`_parse_args`(位置参数与 `--start` 等价,位置参数优先)。 2. `__main__` 解析后覆盖模块级 `RUN_START` 并打日志;未传则保持默认 20:30。 3. 配套把写死的 "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`**。 --- ### 2026/08/11 · 在售商品进度时间序列(`deca_onsale_product_progress_record`) **背景**:在售提醒能提示"进度过半"但只是事件,没有历史轨迹。主公做统计需要**完整进度曲线**(什么时候卖到多少百分比、每小时卖多少、热销时段)。 **方案**: 1. 新表 `deca_onsale_product_progress_record`(append-only 时间序列),字段见 §3。用 `product_code` 关联主表,商品静态信息不复制。 2. `buy_record_spider::snapshot_onsale_progress`(新增函数),挂在 `ingest_onsale` 里 `get_onsale_products` 之后。每 60 秒(复用现有节奏)跑一次:先拿 progress 表每商品最新 sold_count 作基线,跟 onsale 主表当前状态比对,**sold_count 变化 或 从未记录** 才 INSERT——避免"没卖动"重复占位,数据量省 3~5 倍。 3. `unit_price` 存历史值(随机团单价会变,主表覆盖后就丢了当时值)。 **已验证**:测试跑 2 轮:首轮写 201 条(基线)、2 秒后次轮只 9 条(真卖动的 9 个团)。热销例子:GB26080604252 短短 3 秒 sold `11055→11125`,两行都留下。 **完整设计**:`docs/在售商品进度时间序列_采集与统计_20260811.md`(含常用统计 SQL)。 **运维**:只要 `buy_record_spider` 常驻跑,本表自动增长。停机期间不记,跟 buy_record 一样属"停机=断档"。 --- ### 2026/08/11 · 随机团总价口径重构(选队随机 / 剩余随机) **背景**:得卡的"选队随机 / 剩余随机"其实是**同一个团的两个阶段**(选队卖不动→转剩余)。每支球队价格不同、便宜的先卖光——现有 `unit_price × sold_count` 严重失真(选队随机团甚至商品级 `unit_price=0`,直接算出 0 元;已售剩余随机的 `top.soldCount/totalCardCount` 是**原箱母单聚合数**,多个子团共用同一个 10697,`unit×sold` 会把 74727 算成 47.8 万)。 **关键约束(实测)**:`team-options` **只在选队随机状态可查**;一旦转成剩余随机就永久返 `29000 "商家开启剩余随机中"`。**任何接口都无法回补每队原始 cardCount**(详情快照里也没有),必须在选队阶段提前抓、存表。 **方案**: 1. 新表 `deca_groupbuy_team_record` 存逐队明细(`data_source` 区分 team_options / snapshot);两张商品表加列 `team_total_amount decimal(14,2)`。 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 回落原公式)。 3. 已售侧 `deca_sold_core.fill_details` 顺手做剩余随机 snapshot 存表 + 算 `Σ 单价×availableStock`(不新增网络请求,复用已拉到的 detail);`run_pipeline` 加 `backfill_team_amount` 步骤 3.5 存量收敛。 4. 报告端:`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)。 --- ### 2026/08/08 · 在售提醒改「上架时间门槛」+ 废弃冷启动快照(onsale_alert_spider.py) 1. **废弃 `COLD_START_PUSH`**:删除「首次查询发全量在售快照」这条分支与开关。冷启动不再推送快照。 2. **新增运行窗口**:仅每天 `RUN_START`(20:30)~次日 `RUN_END`(03:00) 轮询,窗口外休眠到下次开窗(`_in_run_window` / `_seconds_to_window` / `schedule_task`)。 3. **新品判定改为按上架时间**:新增 `get_detail_meta`(一次详情同时取 shareCode + `publishAt`)、`_window_start`、`_is_new_arrival`。只有 `publishAt >= 本轮窗口起点(20:30)` 才当新品提醒,早于窗口起点或取不到时间的一律静默建档。冷启动与常规轮共用此判断,**首次查询不再区别对待**(仅日志提示)。 4. **拉取免 token 化**:`fetch_onsale` 改为复用 `fetch_all_onsale`(home/search 免登录拉全站在售)再按商家筛,不再翻 `on-sale-list`。 > 注:下方 2026/08/04 记录中提到的"冷启动快照"分支,已由本次改版整体移除,仅作历史保留。 ### 2026/08/04 · 在售提醒三处修复(onsale_alert_spider.py) 1. **补 `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 对齐),其余列全部对齐。 2. **修分享链接取值路径**:详情接口 `groupbuy/detail` 的分享码在 **`data.shareCode`(data 顶层)**, 原代码取 `data.shareResp.shareCode`(该字段实际为 `null`),导致提醒无链接。已改 `get_share_code`。 注:在售列表 `on-sale-list` 返回项**不含** shareCode,分享码只能走详情接口,`get_share_code` 必需保留。 3. **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,不受锁屏影响)。 ### 2026/08/05 · 已售统计 SQL 改造(stats_sold.sql) 1. **统计口径改为时间窗**:从「整个昨天」改为按 `completed_at` 落在 **[昨天 17:00:00, 今天 03:00:00](含两端)** 的成交。 起点 `(CURDATE() - INTERVAL 1 DAY) + INTERVAL 17 HOUR`、终点 `CURDATE() + INTERVAL 3 HOUR`,每天跑一次。 注意:该窗口**不覆盖当天 03:00~17:00 的成交**(每天窗口为傍晚到次日凌晨),如需覆盖白天再调边界。 2. **Navicat 适配**:去掉 `SET @stat_day` 会话变量,边界表达式**内联**进每段查询,两段 SELECT 各自独立、可单独选中执行 (原变量方案在只选中单条 SELECT 时因 SET 未执行导致查不出数据)。 3. **去掉「商家ID」列**:商家段结果不再输出 `merchant_user_id`(`GROUP BY` 保留不影响)。 `completed_at` 为 varchar(32) 存标准 `YYYY-MM-DD HH:MM:SS`,与 datetime 边界直接比较即可,无需 STR_TO_DATE。 ### 2026/08/05 · 新增已售每日报告脚本 + 统计脚本归拢 stats/ - **新建 `stats/daily_report.py`**:生成 Excel 单 Sheet 分区报告(时间窗口径同 stats_sold.sql)。结构: 平台汇总 → 881226408 汇总+每条组队明细 → 274584650 汇总+每条组队明细 → 其他商家打包汇总。 「一个拼团商品=一个组队」(组队售卖);明细列:团名/系列/类型/单价/份数/总份数/进度/总金额/中卡人数/中卡张数/成交时间/回放。 「人数」= 拆卡报告 `hit_user_nickname` 去重(近似,仅覆盖有报告的商品,非真实参团人头)。 从根目录跑:`python stats/daily_report.py` → 根目录输出 `得卡已售每日报告.xlsx`。 - **踩坑**:MySQL 8 保留字 `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。 ### 2026/08/06 · token.json 全局共用一份(根目录与 on_sale/ 子项目) **背景**:`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`)作单一真相源。 - 根目录各脚本:cwd=根目录,`TOKEN_FILE="token.json"` 天然指向这份,**不改**。 - `on_sale/deca_daily_spider.py`:已把 `TOKEN_FILE` 改成**绝对路径**指向同一份(见该文件配置区,2026/08/06)。 - **无并发写冲突**:on_sale 的 09:00/15:00 落在根目录运行时段(默认 20:30~次日06:00)之外,两边不会同时刷 token。⚠️ 若用命令行把 alert 起点提前到 09:00/15:00 附近,需留意可能与 on_sale 刷 token 撞窗口。 **运维**:refreshToken 若彻底失效,只需 APP 重新登录抓包、把新 refreshToken 写回这**一份** token.json, 两个项目一起恢复;不要再复制多份副本。(关联待办 5.2「登录改 refreshToken 续期」) ### 2026/08/06 · 新增购买记录采集子项目(buy_record_analysis/) **目标**:绕开商品详情页「购买记录」板块**固定只显示 10 条**的限制,持续累积买家名单到 `deca_buy_record`。 **关键发现**(反编译 + 真机实测双向验证): - **不存在独立的购买记录接口**(反编译扫遍全库 90+ 个 `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` 商家全在售模式,动态刷新(下线自动移出)。 - **自适应频率**:每轮拉完取 10 条 `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)`,挂了每小时重试。 **去重踩过两次坑(含实测反例)**: 1. **`(product_code, user_id, card_count, purchased_at_minute)` 分钟桶** → 100% 重复入库。原因:相对文本每 60 秒 +1,反推 ts 漂移 60s 跨桶。 2. **`(product_code, user_id, card_count)` 名单粒度** → 漏单。反例:`王**要 x10` 在 10/12 分钟前各一笔(间隔 2 分钟)被合并成一条。 3. **最终解 · 订单粒度 + 反推 ts ±70s 应用层判重**:数学基础是「同一笔订单反推 ts 漂移永远 < 60s、不同笔订单间隔 ≥ 2 分钟时反推 ts 差 ≥ 60s」。DB 唯一键含 `purchased_at_ts` 仅作兜底;进程启动/新商品纳入时用 `load_order_index` 从库恢复 `_order_idx` 内存索引,避免重启后重复。实测反例场景(王**要多笔)已正确区分入库。 **已知局限**: - **昵称脱敏**是服务端行为(详情接口的 `nickname` 返回就是 `D**l`/`匿名用户`),客户端拿不到真实名。**顺带发现**拆卡报告 `hit_user_nickname` **不脱敏**,但两表间无 `user_id` 关联字段,只能靠昵称文本模糊 join。若未来找到「userId → 用户资料」接口,可在本表加 `real_nickname` 列批量补拉。 - 同一用户在 **~1 分钟内**下两笔**完全相同份数**的单会被当同一笔(因 `purchasedAt` 只精确到分钟,数据上无法与漂移区分)。此情形极罕见。 - **停机 = 断档**:中断期间的买家永久丢失(10 条窗口早滚走了),生产必须 `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 行起)。