|
|
3 هفته پیش | |
|---|---|---|
| .. | ||
| HANDOFF.md | 3 هفته پیش | |
| README.md | 3 هفته پیش | |
| YamlLoader.py | 3 هفته پیش | |
| application.yml | 3 هفته پیش | |
| auto_send_wx_msg.py | 3 هفته پیش | |
| init_db.py | 3 هفته پیش | |
| jiwu_core.py | 3 هفته پیش | |
| jw_detail.py | 3 هفته پیش | |
| jw_onsale_alert.py | 3 هفته پیش | |
| jw_onsale_report.py | 3 هفته پیش | |
| jw_onsale_spider.py | 3 هفته پیش | |
| jw_report_excel.py | 3 هفته پیش | |
| jw_sold_report.py | 3 هفته پیش | |
| jw_sold_spider.py | 3 هفته پیش | |
| mysql_pool.py | 3 هفته پیش | |
| requirements.txt | 3 هفته پیش | |
| run_all.py | 3 هفته پیش | |
| schema.sql | 3 هفته پیش | |
集物星球 App 数据采集项目:在售 / 已售 / 购买记录三路抓取,每日在售·已售 Excel 报告自动推企业微信,以及针对重点商家(Jake / 九叔)的在售上架实时监控。
https://api.jiwuplanet.com,全部 POST + JSON。code == 1;code == 200 且 data 为空 是被风控/会话无效的空壳。deca_spider,仅加解密(签名算法)不同。逆向 / 接入的完整细节(签名逐字节验证、TLS 指纹、自登录抓包基准)见
HANDOFF.md。
| 关键点 | 说明 |
|---|---|
| 请求签名 | 每请求带 6 个头 X_VERSION / X_TIME / X_SID / X_RAND / X_PLATFORM / X_SIG_1;X_SIG_1 = base64(HMAC_SHA256(key, msg)),拼接与密钥见 jiwu_core._sign(密钥常量 API_SIGN_SECRET_KEY)。 |
| TLS 指纹 | 必须用 curl_cffi(impersonate="chrome")发请求;requests 的指纹会被判为非真机、返回空壳。已封装在 jiwu_core._post。 |
| 直连 | 显式 proxies={"http":None,"https":None},绕开本机 Clash 代理环境变量。 |
| 鉴权 | 无 token,sessionId 即会话;客户端自造 32 位 hex 经 regLogin 绑定,服务端沿用。三处(头 X_SID、body sessionId、签名串)须同值。脚本自登录见 jiwu_core.login(账号常量 LOGIN_ACCOUNT)。 |
| 金额单位 | 接口原始值 ÷ 1000000 = 元(实测 99000000 → ¥99.00)。入库前已由 core.to_yuan 换算成元存 DECIMAL,故库里/报告里的金额直接就是元。 |
jiwu_core)核心:无 token,客户端自造 sessionId;「签名是门票、sessionId 是身份」。免登录接口用随机身份糊弄过签名即可取数,只有服务端强制认身份的接口才 regLogin 绑一个真身份。三处 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 复用/续期真会话(SESSION_TTL 内复用,超时自动重登)。2026-08-17(jiwu_spider)/
├── jiwu_core.py # 核心库:签名、自登录拿 sessionId、curl_cffi 通用请求
├── auto_send_wx_msg.py # 企业微信群机器人:发文本/markdown、发文件(Excel)
├── schema.sql # 建表 DDL(6 张 jw_ 表)
├── init_db.py # 读 schema.sql 幂等建表
├── run_all.py # ★一键启动:5 个常驻任务各拉子进程 + 守护自动重启(服务器部署跑这个)
├── requirements.txt # 依赖清单(版本实测;pip install -r requirements.txt)
│
├── jw_detail.py # 商品详情补全工具(merchantGoodsId 免登录,补两表详情字段 + img)
├── jw_onsale_spider.py # 板块1 在售抓取(免登录,每日)+ 详情补全
├── jw_sold_spider.py # 板块2 已售抓取 + 详情补全 + 拆卡报告 + 购买记录(全站已售)
│
├── jw_report_excel.py # 报表 Excel 样式工具库(openpyxl,两报告共用)
├── jw_onsale_report.py # 板块4 在售日报 → 企微
├── jw_sold_report.py # 板块4 已售日报 → 企微(Jake/九叔 各独立 sheet)
├── jw_onsale_alert.py # 板块5 在售监控 / 上架提醒
│
├── application.yml # MySQL 连接配置(mysql_pool 读取)
├── HANDOFF.md # 逆向/接入交接文档
└── reports/ logs/ # Excel 报表留档 / 运行日志(自动生成)
数据库连接池 mysql_pool.MySQLConnectionPool 来自全局公共库 charley-utils,读运行目录下的 application.yml。
| # | 板块 | 脚本 | 定时 | 落表 / 产出 |
|---|---|---|---|---|
| 1 | 在售抓取 | jw_onsale_spider.py |
每天 09/15/20/01 四档 | 免登录全量抓 index/top → jw_onsale_product_record(upsert + 下架对账 is_on_sale);同步写每日快照 jw_onsale_daily_record(供在售趋势差分) |
| 2+3 | 已售抓取 + 购买记录 + 拆卡报告 | jw_sold_spider.py |
每天 08:00 | 商家列表(需登录)→各商家已售 corp/history(免登录) jw_sold_product_record;再对全站已售商品抓:详情补全(免登录) + 拆卡报告 jw_report_record(需登录) + 购买记录 jw_player_record(玩家维度, 需登录) |
| 4 | 每日报告 | jw_onsale_report.py / jw_sold_report.py |
在售 09/15/20/01 四档 / 已售 09:10 | 生成 Excel → 企微发文件;只读库不抓取(在售每档各出一份带小时的文件) |
| 5 | 在售监控 | jw_onsale_alert.py |
手写轮询(默认全天,每轮 60~90s 随机) | 三类告警推企微;去重状态落 jw_onsale_alert_record |
购买记录与拆卡报告都并入「已售抓取」,对全站已售商品抓取:
- 购买记录 = 赠品公示·玩家维度
publicity/user/group/pager(需登录),翻页拿全量买家,字段 userId/userNick/picId/count。不做 DB 去重:靠jw_sold_product_record.buy_fetched状态位控制每商品抓一次——翻页取全→批量入库成功→再置buy_fetched=1,失败不置位、下轮重试(买家列表售罄后即固定)。- 拆卡报告 =
gift/report(需登录),有giftReportId+userId+中奖状态;因更新可能不及时,对结束REPORT_REFETCH_DAYS(默认3) 天内的已售商品每轮重查(INSERT IGNORE按 giftReportId 去重、自然补齐),老车只补抓从未抓过的。
板块4 报告细节:openpyxl 生成 Excel(jw_report_excel.py 提供统一样式),通过 auto_send_wx_msg.send_wechat_group_file 以「文件消息」发到企微群。
概览 / 今日新增商家 / 其他商家(存量商家 gmt_create_time!=CURDATE())/ 商品明细(今日新上架行淡红高亮)/ 在售趋势(按每日快照差分,最近 4 天在售商家/拼团数与净新增)/ 上架时段分布(HOUR(sold_time) 24 桶、近 7 日、█ 条形)。每天 09/15/20/01 四档各生成一份带小时的独立文件。平台总览(KPI + 当日环比vs昨日同窗口 + 商家GMV集中度Top1/3/5/10)/ 产品系列榜(按 goods_ip_name) / 商家GMV榜 / 运营节奏(重点商家快照 + 成交时段分布) / Jake明细(汇总+逐团明细+购买记录覆盖检测) / Jake用户排行榜 / 九叔明细 / 九叔用户排行榜 / 其他商家。GMV=已售数×单价元;参与人数取 jw_player_record 去重 user_id。业务日窗口:成交完成时间 COALESCE(finish_time, soldout_time) 落在 [昨17:00, 今06:00](含两端)算「昨天」的已售(对齐 deca 的 completed_at 口径,凌晨仍开播故延到 06:00);采集 08:00 先落库、报告 09:10 再读。板块5 监控细节:index/top 无按商家过滤参数,故全量翻 4 个 productType 后按 corp_info_id 客户端过滤重点商家。三类告警:
soldTime ≥ 本场窗口起点(最近一个 RUN_START)(对齐 deca 最新逻辑);更早上架的老货静默建档不刷屏。(总份数-剩余)/总份数 ≥ HALF_THRESHOLD(0.5),首次达标才发。surplusStock<=0(售罄) 或 已过 offShelfTime(下架/销售结束时间),任一即结束;战报含标题/价格/售出件数/购买人数(玩家维度 totalCount)。运行窗口:对齐 deca 走窗口制 [RUN_START 20:30, RUN_END 06:00](RUN_ALL_DAY=False),RUN_START 同时是「本场新上架」判定门槛;集物直播时段不同可改 RUN_START/RUN_END 或设 RUN_ALL_DAY=True 全天。去重靠 jw_onsale_alert_record 的 new_notified / half_notified / ended_notified 三标记位 + 进程内 _seen_onsale 集合(只播报程序运行后才结束的车)。新品/结束为「发送成功才置位」,失败下轮补发;过半「先置位再发」。
schema.sql)| 表 | 用途 | 主要唯一键 |
|---|---|---|
jw_onsale_product_record |
在售商品最新状态 | goods_id |
jw_sold_product_record |
已售商品(只增) | goods_id |
jw_shop_record |
商家最新状态 | corp_info_id |
jw_player_record |
购买记录(玩家维度买家列表,只增;不 DB 去重,靠已售表 buy_fetched 控制每商品抓一次) |
无唯一键 |
jw_report_record |
拆卡报告(含 userId/中奖状态,只增) | gift_report_id |
jw_onsale_alert_record |
在售监控提醒去重状态 | goods_id |
jw_onsale_daily_record |
在售每日快照(商品+日期,四档 upsert 保最新,供在售趋势差分) | goods_id + snapshot_date |
统一规范:自增 id 物理主键;时间字段 gmt_create_time / gmt_modified_time;金额字段(amount/price/original_price/highest_price/lowest_price)入库前已 ÷1000000 换算成「元」存 DECIMAL(12,2)。
详情补全字段:两个商品表都带一组来自商品详情 merchantGoodsId(免登录)的字段——img(商品主图完整URL) / goods_type / price / original_price / plan_up_time / off_shelf_time / stock / surplus_stock / status / collection_card_name / gift_way / random_way。这些列表接口没有,由 jw_detail.enrich_detail 逐商品补拉,用各表 detail_fetched 状态位控制每商品只补一次(在售的列表 upsert 不覆盖这些列)。
img = FILE_DOMAIN + resList 首图 resAddr;FILE_DOMAIN = https://files.jiwustar.com/(逆向 EnvironmentManager 正式环境所得,接口只给相对路径)。collection_card_name / gift_way / random_way 来自详情的 cardGoods 子对象。| 板块 | path | 登录 | 关键参数 / 说明 |
|---|---|---|---|
| 在售列表 | /search/app/index/top |
🟢 免登录 | currentPage, limit, productType(1福袋2变风盒3错版卡4原盒), systemBusinessType:5 |
| 已售历史 | /search/app/corp/history |
🟢 免登录 | corpInfoId, currentPage, limit, systemBusinessType:5 |
| 商品详情 | /search/app/merchantGoodsId |
🟢 免登录 | goodsId, systemBusinessType:5(监控判结束用;字段 stock/surplusStock/price/highestPrice) |
| 购买记录 | /order/merchant/app/query/gift/publicity/user/group/pager |
🔴 需登录 | 赠品公示·玩家维度:currentPage, giftBusinessName:"", goodsId, limit, systemBusinessType:5;每条=一个买家 userId/userNick/picId/count,翻页拿全量 |
| 拆卡报告 | /goods/gift/report/query/search/pager |
🔴 需登录 | goodsId, currentPage, limit, systemBusinessType:6;字段 giftReportId(唯一)/serialItemName/userId/winnerStatus/createTime |
| 商家列表 | /search/app/index/corp/hotRecommend |
🔴 需登录 | currentPage, limit, systemBusinessType:6 |
| 登录 | /acct/user/regLogin |
— | 见 jiwu_core.LOGIN_ACCOUNT |
登录态最小化(已实测,2026/08/19):jiwu_core.do_request 默认 need_auth=False 走免登录(匿名随机 sid 签名、不调 regLogin,不暴露账号);只有商家列表 hotRecommend、购买记录 publicity/user/group/pager、拆卡报告 gift/report 实测服务端强制登录,才传 need_auth=True。即在售/已售/商品详情抓取全程零登录。
重点商家:Jake球星卡 corpInfoId=100716、九叔的喷火龙 corpInfoId=100715。
依赖:curl_cffi、loguru、tenacity、schedule、requests、openpyxl,以及全局公共库 charley-utils(提供 mysql_pool / YamlLoader,依赖 pymysql、DBUtils、PyYAML)。
pip install -r requirements.txt
# charley-utils 为本地 editable 安装:pip install -e D:\work\common\charley-utils
配置 application.yml 里的 MySQL 连接(host/port/username/password/db)。
# 1) 建表(幂等 CREATE IF NOT EXISTS)。
# schema 经多轮调整,若库里已有旧结构的表,CREATE IF NOT EXISTS 不会改动它们,
# 建议先 DROP 掉全部 jw_ 表再重建:
# mysql> DROP TABLE IF EXISTS jw_onsale_product_record, jw_sold_product_record, jw_shop_record,
# jw_player_record, jw_report_record, jw_onsale_alert_record, jw_onsale_daily_record;
python init_db.py
# 2) 一键启动全部任务(推荐,服务器部署用这个)
# run_all.py 会把下面 5 个任务各拉成独立子进程并守护(崩溃自动重启)
python run_all.py
单跑(仅调试用):想单独跑某个任务时直接跑对应脚本即可,不必用 run_all。
python jw_onsale_spider.py # 在售(每天 09/15/20/01 四档,免登录,含每日快照)
python jw_sold_spider.py # 已售 + 拆卡报告 + 购买记录(每天 08:00)
python jw_onsale_report.py # 在售日报(09/15/20/01 四档发企微)
python jw_sold_report.py # 已售日报(09:10 发企微)
python jw_onsale_alert.py # 在售监控(默认窗口起点 20:30)
# python jw_onsale_alert.py 17:00 或 --start 17:00 # 传窗口起点(该时间后新上架才提醒)
生产部署:直接 python run_all.py(自带子进程守护/自动重启);如需开机自启/进程托管,可再用 nssm / supervisor / pm2 托管 run_all.py 这一个进程即可。各脚本 schedule_task() 里可取消注释 main_task/run_once 立即跑一次用于调试。
| 位置 | 配置 | 说明 |
|---|---|---|
auto_send_wx_msg.WEBHOOK_URL |
企微群机器人 webhook | 当前为测试群,上线换正式群只改这一处 |
jw_onsale_alert.WATCH_CORPS |
{100716, 100715} |
监控的重点商家 |
jw_onsale_alert.HALF_THRESHOLD |
0.5 |
进度过半阈值 |
jw_onsale_alert.RUN_ALL_DAY / RUN_START / RUN_END |
运行窗口 | 默认 False 走 [20:30, 06:00] 跨午夜窗口(对齐 deca,RUN_START 兼作新品判定门槛);置 True 则全天 |
jw_sold_report.FOCUS_CORPS |
Jake / 九叔 | 已售报告独立成 sheet 的商家 |
jiwu_core.SESSION_TTL |
1800 |
sessionId 复用时长,超时自动重登 |
jiwu_core.NO_PROXY |
直连 | 如需代理在此改 |
jw_onsale_alert.is_new_arrival 假定列表字段 soldTime 为上架时间(依 schema 注释)。若实测为售罄/成交时间,需改判定逻辑或改用其它上架时间字段。jw_onsale_alert.confirm_ended 依赖商品详情返回 surplusStock(剩余库存)。若语义不同,需调整结束判定。jiwu_core 统一带 sessionId + 签名,会话失效自动重登一次重试。reports/ 目录、文件名带日期、发送后不删除(留档)。logs/,保留 7 天。