charley bad34d628d feat(core): 支持逐商家带 token 拉取在售列表并调整相关逻辑 1 hete
..
README.md bad34d628d feat(core): 支持逐商家带 token 拉取在售列表并调整相关逻辑 1 hete
YamlLoader.py eb12d24a32 feat(wechat): 新增企业微信群机器人消息发送工具模块 1 hónapja
application.yml eb12d24a32 feat(wechat): 新增企业微信群机器人消息发送工具模块 1 hónapja
deca_on_sale_daily_spider.py ca74925444 feat(deca_on_sale): 新增得卡DECA每日采集爬虫及统计报表功能 1 hónapja
deca_on_sale_report.py bad34d628d feat(core): 支持逐商家带 token 拉取在售列表并调整相关逻辑 1 hete
deca_wechat.py ca74925444 feat(deca_on_sale): 新增得卡DECA每日采集爬虫及统计报表功能 1 hónapja
km_spdier.py 65dbc3dcdf fix(auto_send_wx_msg): 为企微消息上传与发送添加网络重试机制 3 hete
mysql_pool.py eb12d24a32 feat(wechat): 新增企业微信群机器人消息发送工具模块 1 hónapja
requirements.txt ca74925444 feat(deca_on_sale): 新增得卡DECA每日采集爬虫及统计报表功能 1 hónapja

README.md

得卡 DECA App 逆向分析文档

目标:采集得卡「商家展位列表 / 在售商品列表」,供业务侧统计 新增 / 商家账号 / 商品售卖进度。 核心难点:所有接口带 signature 请求头签名校验,部分接口需 JWT 登录态。 结论:signature 已 100% 还原(无加壳、纯 Java),登录+续签闭环打通,可长期无人值守。 日期:2026/08/02 | Python 3.12.10

⚠️ 2026/09/05 接口变更:站方把免 token 的全站在售 home/search 砍成「首屏推荐」(只返前 20 条、page≥2 空、pageSize>2010001),merchant/exhibition/list 商家列表也改为需 token 且只返首屏 30 家。 影响:原「空 query 翻页拿全量在售」失效,在售全量只能靠逐商家 groupbuy/merchant/on-sale-list(需 token)枚举(配合库内已存商家 ID);groupbuy/detail 详情仍免 token。 据此改为一机一账号部署:购买记录(服务器 B,账号 18040545025)、提醒+在售报告(电脑 C,账号 18086100242)、已售+随机团(电脑 A,主号不动)。token.json 现支持内嵌 phone/password/countryCode,密码登录兜底按本机账号走、互不顶号。详见根 README.md 第一节与项目记忆。


1. 目标信息

  • App:得卡 DECA 得卡DECA_1.0.0.apk(appVersion 1.0.0)
  • 接口域名https://api.decalive.com
  • 网络栈:OkHttp 5.3.2(User-Agent: okhttp/5.3.2
  • 鉴权/加密措施
    • 每个请求都带 signature(32 位十六进制,双重 MD5)+ currentTime(13 位毫秒时间戳)。
    • 部分接口需 Authorization: Bearer <JWT>,access token 有效期 15 分钟
  • 加固情况无加壳、无字符串加密、签名不依赖 Native(so);混淆为中等(类名字母混淆,salt 明文硬编码)。

涉及接口

用途 方法 & 路径 是否需登录 请求体
商家展位列表 POST /api/v1/app/merchant/exhibition/list (2026/09/05 起需 token,且只返首屏 30 家) {"page":N}
在售商品列表 POST /api/v1/app/groupbuy/merchant/on-sale-list (在售全量唯一入口) {"merchantUserId","page","pageSize":20}
全站在售(首屏) POST /api/v1/app/home/search 否(2026/09/05 起被砍成首屏 20、不可翻页,已弃用于全量采集) {"query":"","saleStatus":0,"page","pageSize":20,...}
密码登录 POST /api/v1/app/auth/password/login {"countryCode","password","phone"}
Token 续签 POST /api/v1/app/auth/token/refresh {"refreshToken"}

2. 分析过程

  1. 抓包:得到 3 个业务请求 + 1 个登录请求的 curl(含 4 组真实 signature/currentTime/body 样本,用于最终验证)。
  2. 反编译:jadx 1.5.3 反编译 APK(--no-res),产物 16044 个 java 文件,classes.dex/classes2.dex 均为真实业务码,无壳。
  3. 定位签名:从头部装配器 mi/w.java(组装全部公共头 + signature)顺藤摸瓜到签名计算核心 mf/c0.java

关键类

类 / 方法 作用
mi.w.f(Context, url) 头部装配器,组装公共头 + signature
mf.c0.a(Map) 签名计算核心:生成 currentTime + 双重 MD5
mf.c0.b(String) MD5 → 小写 hex
mf.b0 每个参数 entry 渲染为 key+value(无分隔符)的 lambda
eg.p Retrofit 认证接口(login / sms / wechat / aliyun / token refresh)

3. 逆向思路

  • 障碍:所有接口非签名不可用;商品接口还要登录态,且 token 15 分钟即过期,无法直接长期跑。
  • 突破
    1. mf/c0.a() 读出签名逻辑——参数排序 + 双重 MD5 + 固定 salt,salt 是明文常量。
    2. 用抓包的 4 个样本反推确认拼接细节(key 升序、key 直接拼 value、无 = 无分隔符、只取时间戳后 6 位)。
    3. 登录接口样本(无 Authorization)也能仅凭 body 算出一致签名 → 证明签名不依赖 token,可脚本自动登录。
    4. eg/*.java 发现 token/refresh + refreshToken 存储 → 续签闭环成立。

4. 算法要点

4.1 signature 生成规则(已用 4 样本验证一致)

plain      = 参数按 key 升序,每项 "key"+"value" 直接拼接(无分隔符),剔除 value 为 null 的项
inner      = md5(plain)                         # 32 位小写 hex
signature  = md5( inner + currentTime[-6:] + "biu_card_nbclass" )
  • 哈希:MD5,UTF-8,输出小写 hex,双重 MD5。
  • 固定 salt(硬编码于 mf/c0.java):biu_card_nbclass
  • currentTime:13 位毫秒时间戳,同一个值同时放进 currentTime 头;签名只用其后 6 位
  • 空 body:plain=""inner=md5(""),无特殊分支。
  • Authorization / token 不参与签名。

4.2 Python 复现

import hashlib, time
SALT = "biu_card_nbclass"
def md5(s): return hashlib.md5(s.encode("utf-8")).hexdigest()
def make_signature(params: dict, current_time: str) -> str:
    clean = {k: v for k, v in params.items() if v is not None}
    plain = "".join(f"{k}{clean[k]}" for k in sorted(clean))
    return md5(md5(plain) + current_time[-6:] + SALT)
# current_time = str(int(time.time()*1000)),同一值放进 currentTime 头

4.3 验证样本(4/4 全中)

plain currentTime signature
page1 1785662750434 c0670b1219
page2 1785662751198 0dea97b713
merchantUserId593731690page1pageSize20 1785664667848 5416ca7036
countryCode86passwordpass2022phone19521500850 1785665137653 81464e9bf4

4.4 登录 / Token

  • password/login 响应 dataaccessToken / refreshToken / expiresIn(秒,900=15分钟) / userId 等。
  • 续签:token/refresh body {"refreshToken":...},返回同结构新 token。
  • 脚本策略:access 剩余 <60s 先续签,续签失败回退重新登录(登录已验证,绝对兜底)。

5. 踩坑记录

  1. body 必须紧凑 JSON:抓包 Content-Length:10 对应 {"page":1}(无空格)。发送用 json.dumps(..., separators=(",",":")),否则 Content-Length 与实际不符。签名本身只用参数串,与 body 序列化格式无关。
  2. Windows 控制台 GBK:脚本打印 emoji/部分字符会 UnicodeEncodeError。运行加 PYTHONIOENCODING=utf-8 PYTHONUTF8=1,日志文件本身写 UTF-8 无碍。
  3. token 15 分钟过期:一轮采集可能超 15 分钟,必须在请求前动态检查并续签,不能只登录一次。
  4. 数据库握手被拒(2013):内网库 100.64.0.25 需网络/白名单就绪,TCP 通但 MySQL 握手断时,先排查 VPN/授权白名单,非代码问题。
  5. jadx 插件 NPE:为旧版本编译的 jadx-ai-mcp 插件会导致 jadx 启动崩溃,反编译前需临时清空插件配置。
  6. 很多商家 activeGroupbuyCount=0:即当前无在售,属正常;采商品时对每个商家都会发一次接口确认。
  7. 微信自动发送库 wxauto 已下架:PyPI 上原 wxauto 已被作者移除(pip install wxautofrom versions: none),现改推 wxauto4(对应微信 4.0 客户端,免费)/ wxautox4(付费 plus)。安装 pip install wxauto4;import 名即 wxauto4,用 WeChat().SendFiles(filepath, who, exact) 发文件。若 PC 微信仍是 3.9 旧版,则改用 wxautox。发送依赖微信窗口可见的 UI 自动化,无人值守时需保证微信一直登录、窗口不最小化。

6. 举一反三

  • 同类 App 签名套路sign = hash( 排序参数串 + 时间戳(部分位) + 固定 salt ) 是移动端最常见范式。定位思路统一:先找组装请求头的拦截器/装配器,再找里面调用的 hash 工具方法,salt 常为明文常量。
  • 是否 URL 编码mf.b0.a 声明抛 UnsupportedEncodingException,暗示内部可能对 value 做 URLEncoder.encode(v,"UTF-8")(空格→+)。当前样本均无特殊字符无法区分;若日后遇到搜索关键词等含空格/中文的参数,需对 value 先编码再拼。
  • 嵌套/数组参数c0 会把嵌套对象递归转 TreeMap、数组保序后按 toString 拼接。当前接口均为扁平 body,遇到嵌套 body 时需另取样本校准渲染格式。
  • 长期无人值守要点:签名 + 自动登录 + 续签三者齐备才算闭环;只解签名不解 token 续签,跑不过 15 分钟。

附:项目文件

文件 说明
deca_daily_spider.py 常驻每日爬虫(登录→商家→在售商品+每日快照;采完自动出图并发微信)
deca_report.py 统计报表出图(KPI + 今日新增商家 + 各商家在售/售卖进度),生成 reports/ 下 PNG,并按开关自动发微信
deca_wechat.py 通过 wxauto4 把图片发到 PC 微信好友/群
schema.sql 三张表 DDL:deca_shop_record / deca_product_record / deca_product_daily_record
stats.sql 统计查询 SQL(新增 / 商家账号 / 售卖进度趋势)
init_db.py 一键建表
requirements.txt 依赖清单(版本实测)

部署

  1. 装依赖:pip install -r requirements.txtwxauto4 若受 requires-python 限制加 --ignore-requires-python);本地公共库 charley-utils 需 editable 安装(见 requirements 尾注)。
  2. 建表:python init_db.py
  3. 常驻运行:python deca_daily_spider.py(每天 09:00 / 15:00 及启动各采集一轮,采完自动生成报表并发微信)。

报表与微信推送

  • 报表图输出到 reports/deca_report_YYYYMMDD_HHMMSS.png,命名到秒,一天多张互不覆盖。
  • 微信发送依赖 PC 微信 4.0 已登录且窗口可见;目标好友备注名须精确等于 deca_report.WECHAT_TO(默认 backup)。
  • 手动补发:python deca_wechat.py "reports/xxx.png" backup
  • 关闭自动发送:把 deca_report.SEND_WECHAT 置为 False

登录与 token(避开验证码)

  • 密码登录 password/login 受阿里云验证码2.0 风控,脚本无法自动过验证码;改走 token/refresh 续期(无验证码)。
  • 首次:在 APP 正常登录后抓包,把 refreshToken 粘入运行目录 token.json2026/09/05 起推荐直接写本机账号凭据,程序首次会自动密码登录补 access/refresh{"phone":"18040545025","password":"147258369@ww","countryCode":"86"}deca_sold_core.load_token/save_token/login 会读取并保留这些字段,兜底登录按本机账号走、便于人工分辨该机用哪个号)。旧格式 {"access":null,"refresh":"<粘这里>","exp":0} 仍兼容。
  • 之后爬虫用它自动续期并滚动写回 token.json;refreshToken 失效(过期/被踢)时日志会报警,需重新抓包更新。
  • 商品上架时间/结束时间来自详情接口 POST api/v1/app/groupbuy/detail(body {"code":商品code},需 token),仅对今日新增商品拉取,存入 deca_product_recordpublish_at/sale_start_at/sale_end_at。商家入驻时间接口无,新增按入库时间判定。