Files
shaguabijia-app-server/docs/看广告赚金币上线清单.md
T
ouzhou 04edb50acc feat(admin/ad): 提现自动对账后台开关 + 客户端广告 flags 端点 + 穿山甲多 m-key 验签 (#59)
- 自动对账接入 app_config 运行时配置(新增 bool 配置类型):env 部署总闸 + DB 运营日常开关
  双层;worker 每轮读 DB 即时生效,健康检查改报「env AND DB」实际生效态
- 新增 GET /api/v1/platform/flags(不鉴权)下发 comparing_ad_enabled 远程 kill-switch,
  客户端拉取后缓存;空库回退默认 True
- 穿山甲发奖回调支持多激励位 m-key(三命名项 PANGLE_REWARD_SECRET_TEST/_DEDICATED/_PROD
  + 旧逗号分隔合并去重),verify_callback_sign_any 逐个验签任一通过即受理;向后兼容旧单 key
- 补 test_platform / bool config 用例;app_config 文档同步

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: OuYingJun1024 <1034284404@qq.com>
Reviewed-on: #59
Co-authored-by: ouzhou <ouzhou@wonderable.ai>
Co-committed-by: ouzhou <ouzhou@wonderable.ai>
2026-06-17 10:37:54 +08:00

8.0 KiB

「看广告赚金币」上线 Checklist

跨两个仓库:shaguabijia-app-server(后端发奖)+ shaguabijia-app-android(客户端看广告)。 截至 2026-05-26:本地 + debug 包已端到端验证通过(出广告 → 发奖 → 金币到账 → 明细显示「看广告奖励」), 但尚未在生产上线。本文件记录上线前必须做的事,尤其是一堆"仅本地/debug 用、上线前必须清理"的脚手架。

设计要点(别改坏)

  • 发奖只走服务端:激励视频播完,穿山甲服务器 S2S 回调后端 /api/v1/ad/pangle-callback 发金币。 客户端不发奖,onRewardArrived 只触发"去后端刷余额"。客户端被破解也刷不到钱。
  • 后端发奖幂等(按 trans_id 去重)+ 每日上限(DAILY_AD_REWARD_LIMIT,按北京时间)。
  • 关键常量:app/core/rewards.pyAD_REWARD_COIN=100DAILY_AD_REWARD_LIMIT=500VIDEO_ROUND_REQUIRED_COUNT=1VIDEO_ROUND_COOLDOWN_SECONDS=3(每次广告关闭后 3 秒短冷却)。旧的观看时长闸 DAILY_AD_WATCH_SECONDS_LIMIT=0 表示停用。
  • 4 态 CTA(2026-05-29 PR #5):客户端任务行按钮在 Normal / Loading / Capped / CoolingDown 之间切,全由后端 reward-statusround_count + cooldown_until 派生(权威源),跨设备/重装/杀进程一致。详见 api/ad-reward-status

A. ⚠️ 上线前必须清理的"测试/调试专用"脚手架

这些是为了"没部署公网也能本地验证"加的,带到生产 = 体验差 / 绕过反作弊 / 引入废弃工具,务必清理。

A1. 后端:本地模拟发奖接口

  • .env 删除 / 设为 false:AD_REWARD_TEST_GRANT_ENABLED=false(默认就是 false;它一开,已登录客户端就能自助发奖)
  • (可选)确认生产 .env 没这行;/api/v1/ad/test-grant 在 false 时返回 404
  • 涉及:app/api/v1/ad.pytest_grantapp/core/config.pyAD_REWARD_TEST_GRANT_ENABLED

A2. 客户端:test-grant 调用

  • WelfareViewModel.onAdRewardEarned()if (BuildConfig.DEBUG) { repo.adTestGrant() } —— release 包(BuildConfig.DEBUG=false)本就跳过,确认 release 构建不含该调用即可(结构上已隔离,无需改码;打 release 包验一次)
  • 涉及:WelfareViewModel.ktWelfareRepository.adTestGrant()WelfareApi.adTestGrantTestGrantDto(留着无害,只要 release 不调用 + 后端 404)

A3. 客户端:穿山甲测试工具(广告预览)

  • app/build.gradle.kts:移除 debugImplementation(files("libs/tools-release.aar"))
  • 删除 app/libs/tools-release.aar
  • gradle.properties:移除 android.enableJetifier=true(仅为兼容 tools-release.aar 的旧 support 库而开,Jetifier 已废弃)
  • 删除 app/src/debug/.../ad/TestToolLauncher.kt + app/src/release/.../ad/TestToolLauncher.kt(空桩)
  • WelfareScreen.kt:移除「【调试】唤起穿山甲测试工具」入口(if (BuildConfig.DEBUG){...} 那段)+ TestToolLauncher import
  • 注:这些都 debug-only,不影响 release 功能;清掉是为了不留废弃依赖

B. 生产真正发奖链路(S2S)—— 阻塞于穿山甲后台

已确认走 GroMore 广告位层级回调(客户端 useMediation(true);规范 supportcenter/26240), 不是联盟代码位层级(5416)。验签算法 / 响应格式 / reward_amount 解析代码已按 GroMore 规范实现 (2026-05-27),剩下的是后台配置 + 填密钥。

🔴 2026-06-16 实测的头号阻塞:穿山甲打线上后端会超时。真机看广告时 SDK 日志 onRewardArrived valid=false errorCode=50002,errorMsg = Get "https://app-api.shaguabijia.com/api/v1/ad/pangle-callback...": context deadline exceeded。 而我们自己 curl 线上是秒回。说明穿山甲机房到线上后端这条网络路径有问题(疑似 nginx/云防火墙对 机房/境外 IP 限流或拦截)。m-key 配对了也救不了——穿山甲根本没把请求送达。 上线前必须单独验 「线上 app-api.shaguabijia.com 对穿山甲机房可达且响应够快」,否则用户看完发不了奖。

多激励位已支持(2026-06-16):内部测试期 测试位/专属位/正式位 并存,m-key 各配一行 (PANGLE_REWARD_SECRET_TEST/_TEST_DEDICATED/_PROD),verify_callback_sign_any 逐个试。本地已验通。

  • GroMore 后台配回调:GroMore 聚合管理 → 搜广告位ID → 编辑 → 勾选「服务端激励回调」→ 回调 URL 填 https://app-api.shaguabijia.com/api/v1/ad/pangle-callback(用域名,别用 IP)。 ⚠️ 广告位层级配了就别再在代码位层级重复配(会导致发奖出问题)。
  • 拿 m-key(安全密钥):就在上面"编辑广告位"页获取(每个激励位各一把),填到生产 .env 的命名变量 (正式位填 PANGLE_REWARD_SECRET_PROD;内部测试期 测试位/专属位 也并存就填 _TEST/_TEST_DEDICATED)。 (注:这是 GroMore 广告位的 m-key,不是联盟代码位那个 Security Key。)
  • 换验签:app/integrations/pangle.py 已实现 sign = SHA256("{m-key}:{trans_id}")(GroMore 真实算法)。
  • 响应格式:已返回 GroMore 要求的 {"is_verify": bool, "reason": int}
  • reward_amount:回调按 reward_amount 发金币(rewards.resolve_ad_reward_coin,带回退/夹紧); 后台广告位"奖励数量"须配成与 AD_REWARD_COIN(=100)一致,保证"广告内展示/进度预告/到账"三者一致。
  • 透传 user_id:客户端已 setUserID(userId) + setMediaExtra("uid:...")
  • 生产 .env:PANGLE_CALLBACK_ENABLED=true + 至少一个 m-key 命名变量(配齐才不返 503)+ 确认 AD_REWARD_TEST_GRANT_ENABLED=false(绕过反作弊,生产红线)。改 .env重启 uvicorn (watchfiles 不监听 .env)。验证:外部 curl 回调 URL 从 503403 bad sign 即生效。

C. 部署 + 包名

  • 后端部署到公网(由服务器管理员;/opt/shaguabijia-app-server,uvicorn 127.0.0.1:8770,nginx 反代)
  • 跑迁移:alembic upgrade head(包含 ad_reward_recordsignin_boost_recordad_feed_reward_record 等表)
  • 包名定稿:当前 com.jishisongfu.shaguabijia。穿山甲(APP_ID 5830519)、极光、微信都绑"包名 + 签名",定了再上,别再换
    • 微信提现链路当前因复用 elderhelper 的 appid + 包名切换已 dead,要恢复需申请傻瓜比价自己的微信 appid(另见客户端 build.gradle 注释)
  • (可选,提升真实填充)集成 MSA OAID SDK:申请证书(绑包名、审核几天)。App 侧当前 getDevOaid=null,有 OAID 后投放匹配 + 填充会明显改善

D. 提现对账(顺带,与本功能独立但同属上线必做)

  • 生产给 scripts/reconcile_withdraws.pycron / systemd timer(每 5~10 分钟跑一次)
    • 否则"扣款后微信无回调"的孤儿提现单没人兜底退款(客户端已加 ON_RESUME 无条件查单,但 cron 是最后防线)

E. 端到端验收(生产链路通后)

  • 真机看完一条真实激励视频 → 穿山甲 S2S 回调 → 后端发奖 → 客户端余额真到账
  • 幂等:同一 trans_id 不重复发
  • 每日上限:超过 DAILY_AD_REWARD_LIMIT 返回 capped、不发币、按钮显示「已达上限」(Capped 态)
  • 短冷却:看完 1 张 → 按钮变 CoolingDown 显 MM:SS 倒计时不可点 → 3 秒后自动恢复 Normal
  • 弹窗 limit note:cooldown_until!=null → "广告冷却中,3秒后再来";used_today>=daily_limit → "今日视频已到限额,明天再来"
  • 跨设备/杀进程一致:在手机 A 看完 1 张进入短冷却 → 手机 B(同账号)登录后 reward-status 也返回相同的 cooldown_until(权威源在后端,客户端不存本地)
  • 中途退出广告 → 客户端提示「未看完视频,本次没有奖励哦」,且不发奖
  • 收益明细显示「看广告奖励」(biz_type=ad_reward)

关键阻塞链

B(拿验签方案/配回调)→ E(端到端验收);A(清理)可随时做;C(部署)与 B 并行。