docs: 新建 docs/integrations 集成层文档 + api 文档变薄
- 新增 docs/integrations/(README + jiguang/sms/meituan/pangle/wxpay 各一篇): 记录各 SDK 集成的签名/验签/加解密/协议细节与踩坑(对齐 app/integrations 分层) - docs/api 接口文档变薄: 受影响接口头部加「集成实现」链接,把验签/RSA/CPS 签名 等重逻辑描述移到集成文档;ad-pangle-callback 验签段精简为指针 - docs/api/README 补架构说明(api 薄 / 重逻辑在 integrations) - 后端技术实现.md 目录树补全(福利/钱包/签到/省钱/发奖端点 + crud/integrations 现状), pangle/wxpay 标注移入 integrations - ad_reward_golive_checklist 路径同步 core→integrations Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -37,11 +37,21 @@
|
||||
|
||||
## B. 生产真正发奖链路(S2S)—— 阻塞于穿山甲后台
|
||||
|
||||
- [ ] **穿山甲后台配 S2S 回调 URL** → `https://app-api.shaguabijia.com/api/v1/ad/pangle-callback`
|
||||
- [ ] **拿到真实验签方案 / 公钥**(后台奖励校验密钥;大概率 RSA)
|
||||
- [ ] **换验签**:`app/core/pangle.py` 的 `verify_callback_sign` 从占位 HMAC-SHA256 改成穿山甲真实方案
|
||||
- [ ] 对齐回调字段名(`user_id` / `trans_id` / 奖励数量等);客户端已透传 `setUserID` + `setMediaExtra("uid:...")`
|
||||
- [ ] 生产 `.env`:`PANGLE_CALLBACK_ENABLED=true` + `PANGLE_REWARD_SECRET=<真实密钥>`(配齐才不返 503)
|
||||
> 已确认走 **GroMore 广告位层级**回调(客户端 useMediation(true);规范 supportcenter/26240),
|
||||
> **不是**联盟代码位层级(5416)。验签算法 / 响应格式 / reward_amount 解析**代码已按 GroMore 规范实现**
|
||||
> (2026-05-27),剩下的是后台配置 + 填密钥。
|
||||
|
||||
- [ ] **GroMore 后台配回调**:GroMore 聚合管理 → 搜广告位ID → 编辑 → 勾选「服务端激励回调」→
|
||||
回调 URL 填 `https://app-api.shaguabijia.com/api/v1/ad/pangle-callback`(用域名,别用 IP)。
|
||||
⚠️ 广告位层级配了就**别再在代码位层级重复配**(会导致发奖出问题)。
|
||||
- [ ] **拿 m-key(安全密钥)**:就在上面"编辑广告位"页获取,填到生产 `.env` 的 `PANGLE_REWARD_SECRET`。
|
||||
(注:这是 GroMore 广告位的 m-key,**不是**联盟代码位那个 Security Key。)
|
||||
- [x] ~~换验签~~:`app/integrations/pangle.py` 已实现 `sign = SHA256("{m-key}:{trans_id}")`(GroMore 真实算法)。
|
||||
- [x] ~~响应格式~~:已返回 GroMore 要求的 `{"is_verify": bool, "reason": int}`。
|
||||
- [x] ~~reward_amount~~:回调按 `reward_amount` 发金币(`rewards.resolve_ad_reward_coin`,带回退/夹紧);
|
||||
**后台广告位"奖励数量"须配成与 `AD_REWARD_COIN`(=100)一致**,保证"广告内展示/进度预告/到账"三者一致。
|
||||
- [x] ~~透传 user_id~~:客户端已 `setUserID(userId)` + `setMediaExtra("uid:...")`。
|
||||
- [ ] 生产 `.env`:`PANGLE_CALLBACK_ENABLED=true` + `PANGLE_REWARD_SECRET=<m-key>`(配齐才不返 503)
|
||||
|
||||
## C. 部署 + 包名
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
> 协议:HTTP / JSON,请求与响应体均 `application/json`,字段统一 **snake_case**
|
||||
> 鉴权:需鉴权的接口在请求头带 `Authorization: Bearer <access_token>`
|
||||
> 最后更新:2026-05-27
|
||||
> 架构:`app/api/v1/` 只放很轻的接口层;穿山甲/微信支付/极光/短信/美团等 SDK 集成的重逻辑在 `app/integrations/`,实现细节见 [docs/integrations/](../integrations/README.md)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,28 +1,40 @@
|
||||
# GET /api/v1/ad/pangle-callback — 穿山甲激励视频发奖回调(S2S)
|
||||
# GET /api/v1/ad/pangle-callback — 穿山甲 GroMore 激励视频发奖回调(S2S)
|
||||
|
||||
> 所属:Ad 组(前缀 `/api/v1/ad`) | 鉴权:**无 JWT,靠验签**(穿山甲服务器调用) | 限流:同 IP ≤300 次/分 | [← 返回 API 索引](./README.md)
|
||||
> 所属:Ad 组(前缀 `/api/v1/ad`) | 鉴权:**无 JWT,靠验签**(穿山甲 GroMore 服务器调用) | 限流:同 IP ≤300 次/分 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> ⚠️ 我们客户端用 `useMediation(true)`(GroMore 融合),回调走 **GroMore 广告位层级**(规范见 supportcenter/26240),**不是**联盟代码位层级(5416)。两者密钥、响应格式都不同,别混。后台配置入口:**GroMore 聚合管理 → 搜广告位ID → 编辑 → 勾选「服务端激励回调」**(广告位层级配了就别再在代码位层级重复配,会冲突)。
|
||||
>
|
||||
> 集成实现:见 [integrations/pangle](../integrations/pangle.md)(验签算法、m-key 来源、设计动机)。
|
||||
|
||||
## 入参(query,由穿山甲拼装)
|
||||
穿山甲在激励视频播完后回调,带一组 query 参数,关键:
|
||||
## 入参(query,由 GroMore 拼装)
|
||||
GroMore 以 GET 回调,关键参数:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `user_id` | string | 客户端起播时传入的用户标识(须为数字 = 本系统 user.id) |
|
||||
| `trans_id` | string | 穿山甲交易号(**幂等键**) |
|
||||
| `sign` | string | 穿山甲签名(用 `PANGLE_REWARD_SECRET` 验) |
|
||||
| `reward_name` | string | 奖励名(可选,入库备注) |
|
||||
| … | | 其余参与签名的参数 |
|
||||
| `user_id` | string | 客户端 `setUserID` 传入的用户标识(须为数字 = 本系统 user.id) |
|
||||
| `trans_id` | string | 交易号(**幂等键** + **唯一参与签名的字段**) |
|
||||
| `reward_name` | string | 奖励名(广告位配置,入库备注) |
|
||||
| `reward_amount` | int | 奖励数量(广告位配置)→ **本次发放金币**。缺/坏/≤0 回退 `AD_REWARD_COIN`,超 `MAX_AD_REWARD_COIN` 夹紧 |
|
||||
| `extra` | string | 客户端透传的 customData(可空,入库备注) |
|
||||
| `mediation_rit` | string | 代码位 ID(GroMore 带,目前仅入 raw 备查) |
|
||||
| `prime_rit` | string | 广告位 ID(同上) |
|
||||
| `adn_name` | string | 实际出广告的 ADN 名(同上,可用于收益分析) |
|
||||
| `ecpm` | string | 本次广告 eCPM(同上,可用于收益分析) |
|
||||
| `sign` | string | 签名,见下 |
|
||||
|
||||
**验签**:`sign = SHA256("{m-key}:{trans_id}")` 十六进制(只签 `trans_id`,其余参数不参与)。算法细节、m-key 来源、为什么这样设计 → 见集成文档 [integrations/pangle](../integrations/pangle.md)。
|
||||
|
||||
## 出参
|
||||
响应 `200`:`PangleCallbackOut`
|
||||
响应 `200`,**响应体必须是 `{"is_verify": bool, "reason": int}`**(GroMore 规范)。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `is_valid` | bool | `true`=已受理(发奖成功 或 当日达上限 或 user 非法,均不让穿山甲重试) |
|
||||
| `is_verify` | bool | `true`=校验通过、发放奖励(发奖成功 或 当日达上限,均算已处理、不重试) |
|
||||
| `reason` | int | `is_verify=false` 时的错误码,透传客户端 SDK:`1`=参数缺/坏,`2`=user 不存在;成功为 `0` |
|
||||
|
||||
## 错误码
|
||||
- `403` 验签失败(`bad sign`,留给真请求重试)
|
||||
- `503` 回调未配置(`pangle_callback_configured=false`)
|
||||
|
||||
## 说明
|
||||
**发奖唯一可信入口**:验签 → 取 `user_id`/`trans_id` → 幂等发金币(按 `trans_id` 去重 + 每日上限)。客户端不参与发奖,被破解也刷不到钱。验签过但参数缺/坏或 user 不存在 → 受理但不发(`is_valid` 仍据情况返回,避免无意义重试)。
|
||||
**发奖唯一可信入口**:验签 → 取 `user_id` → 按 `reward_amount` 解析金币 → 幂等发金币(按 `trans_id` 去重 + 每日上限)。客户端不参与发奖,被破解也刷不到钱。验签过但参数缺/坏或 user 不存在 → 不发(`is_verify=false` + `reason`);granted / capped → `is_verify=true` + `reason=0`。
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# POST /api/v1/auth/jverify-login — 极光一键登录
|
||||
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/jiguang](../integrations/jiguang.md)(极光核验链路、RSA 解密策略、私钥配对踩坑)。
|
||||
|
||||
## 入参
|
||||
|
||||
@@ -17,4 +19,4 @@
|
||||
- `403` 账号被禁用
|
||||
|
||||
## 说明
|
||||
后端拿 `login_token` 调极光 `loginTokenVerify` 验真 → 取回 RSA 加密的手机号 → 用私钥解密 → 注册即登录(手机号不存在则建号)→ 签发 JWT。
|
||||
后端验真 `login_token` 取回并解密手机号 → 注册即登录(手机号不存在则建号)→ 签发 JWT。极光核验 + RSA 解密的实现细节见 [integrations/jiguang](../integrations/jiguang.md)。
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# POST /api/v1/auth/sms/login — 手机号 + 验证码登录
|
||||
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/sms](../integrations/sms.md)(验证码校验逻辑)。
|
||||
|
||||
## 入参
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# POST /api/v1/auth/sms/send — 发送短信验证码
|
||||
|
||||
> 所属:Auth 组(前缀 `/api/v1/auth`) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/sms](../integrations/sms.md)(mock 模式、频控、接真供应商 TODO)。
|
||||
|
||||
## 入参
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# POST /api/v1/meituan/coupons — 券列表 / 搜索
|
||||
|
||||
> 所属:美团 CPS 组(前缀 `/api/v1/meituan`,**全部无鉴权**) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/meituan](../integrations/meituan.md)(CPS S-Ca 签名、入参换算坑)。
|
||||
|
||||
## 入参
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# POST /api/v1/meituan/feed — 首页混合推荐流
|
||||
|
||||
> 所属:美团 CPS 组(前缀 `/api/v1/meituan`,**全部无鉴权**) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/meituan](../integrations/meituan.md)(CPS S-Ca 签名、入参换算坑)。
|
||||
|
||||
## 入参
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# POST /api/v1/meituan/referral-link — 换取推广链接
|
||||
|
||||
> 所属:美团 CPS 组(前缀 `/api/v1/meituan`,**全部无鉴权**) | 鉴权:无 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/meituan](../integrations/meituan.md)(CPS S-Ca 签名、入参换算坑)。
|
||||
|
||||
## 入参
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# POST /api/v1/wallet/bind-wechat — 微信授权 code 换 openid 并绑定
|
||||
|
||||
> 所属:Wallet 组(前缀 `/api/v1/wallet`) | 鉴权:Bearer | 限流:同 IP ≤10 次/分 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/wxpay](../integrations/wxpay.md)(code 换 openid / userinfo)。
|
||||
|
||||
## 入参
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# POST /api/v1/wallet/unbind-wechat — 解绑微信(清空 openid)
|
||||
|
||||
> 所属:Wallet 组(前缀 `/api/v1/wallet`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/wxpay](../integrations/wxpay.md)。
|
||||
|
||||
## 入参
|
||||
无(用户由 token 确定)。
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# GET /api/v1/wallet/withdraw-info — 提现额度 / 绑定状态
|
||||
|
||||
> 所属:Wallet 组(前缀 `/api/v1/wallet`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/wxpay](../integrations/wxpay.md)(提现链路)。
|
||||
|
||||
## 入参
|
||||
无(用户由 token 确定)。
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# GET /api/v1/wallet/withdraw/status — 查提现单状态(轮询)
|
||||
|
||||
> 所属:Wallet 组(前缀 `/api/v1/wallet`) | 鉴权:Bearer | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/wxpay](../integrations/wxpay.md)(查单 / 撤销转账)。
|
||||
|
||||
## 入参(query)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# POST /api/v1/wallet/withdraw — 发起提现到微信零钱
|
||||
|
||||
> 所属:Wallet 组(前缀 `/api/v1/wallet`) | 鉴权:Bearer | 限流:同 IP ≤20 次/分 | [← 返回 API 索引](./README.md)
|
||||
>
|
||||
> 集成实现:见 [integrations/wxpay](../integrations/wxpay.md)(微信 V3 商家转账、签名、实名加密)。
|
||||
|
||||
## 入参
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# 集成层(integrations)文档 — 索引
|
||||
|
||||
> 本目录记录 `app/integrations/` 下各**第三方 SDK / 外部服务**的集成内部实现:签名、加解密、协议细节、配置项、踩坑。
|
||||
> 与 [docs/api/](../api/README.md) 的区别:`docs/api/` 描述**对外 HTTP 接口契约**(入参/出参/错误码);本目录描述**接口背后调外部服务的重逻辑**。
|
||||
|
||||
## 架构约定
|
||||
- **`app/api/v1/` 接口层只放很轻的东西**:解析请求 → 调 service/crud → 组装响应 + HTTP 错误码映射。
|
||||
- **集成 SDK 的重逻辑(签名/验签/加解密/外部 HTTP 调用)一律放 `app/integrations/`**。换签名方案、换供应商只动这一层,api 层不受影响。
|
||||
- 集成模块统一从 `app.core.config.settings` 读配置;密钥/证书**懒加载**(文件缺失不在 import 时炸进程,只在真正调用时抛各自的 `XxxError`)。
|
||||
|
||||
## 集成清单
|
||||
|
||||
| 模块 | 文件 | 用途 | 关联 API 文档 |
|
||||
|---|---|---|---|
|
||||
| 极光 jiguang | `app/integrations/jiguang.py` | 一键登录服务端核验 + RSA 解密手机号 | [auth-jverify-login](../api/auth-jverify-login.md) |
|
||||
| 短信 sms | `app/integrations/sms.py` | 验证码发送 / 校验(**当前 mock**) | [auth-sms-send](../api/auth-sms-send.md) · [auth-sms-login](../api/auth-sms-login.md) |
|
||||
| 美团 meituan | `app/integrations/meituan.py` | CPS 券查询 / 换推广链 | [meituan-coupons](../api/meituan-coupons.md) · [meituan-feed](../api/meituan-feed.md) · [meituan-referral-link](../api/meituan-referral-link.md) |
|
||||
| 穿山甲 pangle | `app/integrations/pangle.py` | 激励视频发奖回调**验签** | [ad-pangle-callback](../api/ad-pangle-callback.md) |
|
||||
| 微信支付 wxpay | `app/integrations/wxpay.py` | 商家转账到零钱(提现)+ code 换 openid(绑定) | [wallet-withdraw](../api/wallet-withdraw.md) 等 |
|
||||
|
||||
## 详情
|
||||
- [极光一键登录 jiguang](./jiguang.md)
|
||||
- [短信验证码 sms](./sms.md)
|
||||
- [美团 CPS meituan](./meituan.md)
|
||||
- [穿山甲发奖验签 pangle](./pangle.md)
|
||||
- [微信支付 / 提现 wxpay](./wxpay.md)
|
||||
@@ -0,0 +1,36 @@
|
||||
# 极光一键登录 — 服务端核验 + RSA 解密手机号(jiguang)
|
||||
|
||||
> 文件:`app/integrations/jiguang.py` | 关联接口:[auth-jverify-login](../api/auth-jverify-login.md) | [← 集成索引](./README.md)
|
||||
|
||||
## 作用
|
||||
极光一键登录的服务端环节:拿客户端的 `loginToken` 调极光 REST 验真,取回 RSA 加密的手机号并解密成明文。
|
||||
|
||||
## 链路
|
||||
```
|
||||
Android SDK loginAuth → loginToken
|
||||
→ 本服务 POST 极光 /v1/web/loginTokenVerify (Basic Auth = AppKey:MasterSecret)
|
||||
→ 极光返回 RSA 公钥加密的手机号 (base64)
|
||||
→ 用配套 RSA 私钥解密
|
||||
→ 返回明文手机号
|
||||
```
|
||||
|
||||
## 函数 / 异常
|
||||
- `verify_and_get_phone(login_token)` —— 对外入口,验证 + 解密一把梭。
|
||||
- 失败统一抛 `JiguangError`,api 层 catch 后翻成 4xx/5xx(验证失败/解密失败 → `502`)。
|
||||
- RSA 私钥**懒加载并缓存**(`_private_key_cache`):文件缺失抛 `JiguangError`(让单请求失败 + 日志,而不是 import 时炸进程)。
|
||||
|
||||
## RSA 解密策略(关键)
|
||||
极光文档没明说 padding。实测 PKCS1v15 套在 OAEP 密文上会"假成功"返回垃圾(`UnicodeDecodeError 0x8e`)。因此按 **OAEP-SHA1 → OAEP-SHA256 → PKCS1v15** 顺序尝试,谁能解出 **11 位纯数字手机号**就用谁;base64 先按 4 字节对齐补 `=`(各运营商可能返回不带 `=` 的 base64)。
|
||||
|
||||
## 配置
|
||||
| 配置项 | 说明 |
|
||||
|---|---|
|
||||
| `JG_APP_KEY` / `JG_MASTER_SECRET` | 极光 AppKey / MasterSecret(拼 Basic Auth) |
|
||||
| `JG_PRIVATE_KEY_PATH` | RSA 私钥 .pem 路径 |
|
||||
| `JG_VERIFY_ENDPOINT` | 极光验证接口 URL(`/v1/web/loginTokenVerify`) |
|
||||
| `JG_REQUEST_TIMEOUT_SEC` | 请求超时 |
|
||||
|
||||
## 踩坑
|
||||
- **私钥必须与极光控制台为该 appkey 登记的公钥配对**,否则三种 padding 全失败、报 `all RSA paddings failed; last error: Decryption failed`。私钥由相关开发同学生成提供。
|
||||
- **换私钥后必须重启后端**——`_private_key_cache` 会缓存旧 key。
|
||||
- 极光验证响应 `code != 8000` 视为失败;缺 `phone` 字段也失败。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 美团联盟 CPS 开放接口(meituan)
|
||||
|
||||
> 文件:`app/integrations/meituan.py` | 关联接口:[meituan-coupons](../api/meituan-coupons.md) · [meituan-feed](../api/meituan-feed.md) · [meituan-referral-link](../api/meituan-referral-link.md) | [← 集成索引](./README.md)
|
||||
|
||||
## 作用
|
||||
调美团联盟 CPS 开放接口:查券(列表/搜索)+ 换推广链。
|
||||
|
||||
## 函数 / 异常
|
||||
| 函数 | 说明 |
|
||||
|---|---|
|
||||
| `query_coupon(...)` | 券查询/搜索 → `/cps_open/common/api/v1/query_coupon` |
|
||||
| `get_referral_link(...)` | 换推广链 → `/cps_open/common/api/v1/get_referral_link` |
|
||||
| 异常 `MeituanCpsError` | HTTP 错误 / 业务 `code != 0`,api 层翻成 `502` |
|
||||
|
||||
## 签名机制(关键)
|
||||
类阿里云网关 **S-Ca** 头,但 `stringToSign` **只有 4 段**:
|
||||
```
|
||||
METHOD\n + Content-MD5\n + Headers(按 key 升序 "k:v\n") + Url
|
||||
```
|
||||
**没有 Accept / Content-Type / Date 行**(加了就签名失败)。签名 = HMAC-SHA256(stringToSign, AppSecret) 的 base64。参与签名的头:`S-Ca-Timestamp,S-Ca-App`(在 `S-Ca-Signature-Headers` 里声明)。
|
||||
|
||||
## 入参换算 / 坑
|
||||
- 经纬度 **× 1_000_000 取整**传给美团。
|
||||
- 榜单字段是 **`listTopiId`**(美团接口的拼写,少个 `c`,别"纠正"成 `listTopicId`)。
|
||||
- 有 `searchText` 时走搜索、`sort_field` 默认 6(离我最近);否则按 `listTopiId` 出榜单。
|
||||
- 换链 `sid` 不传则用 `MT_CPS_DEFAULT_SID`;`linkTypeList` 默认 `[1,3]`。
|
||||
|
||||
## 配置
|
||||
| 配置项 | 说明 |
|
||||
|---|---|
|
||||
| `MT_CPS_APP_KEY` / `MT_CPS_APP_SECRET` | CPS 网关 AppKey / AppSecret |
|
||||
| `MT_CPS_HOST` | 网关 host |
|
||||
| `MT_CPS_DEFAULT_SID` | 默认推广位 sid(渠道) |
|
||||
| `MT_CPS_TIMEOUT_SEC` | 请求超时 |
|
||||
|
||||
## 备注
|
||||
美团接口当前**无鉴权**,且换链的 `sid` 允许客户端传值覆盖默认渠道——见对应 api 文档「备注」。
|
||||
@@ -0,0 +1,29 @@
|
||||
# 穿山甲激励视频发奖回调 — 验签(pangle)
|
||||
|
||||
> 文件:`app/integrations/pangle.py` | 关联接口:[ad-pangle-callback](../api/ad-pangle-callback.md) | [← 集成索引](./README.md)
|
||||
|
||||
## 作用
|
||||
穿山甲激励视频播完后,穿山甲**服务器**会 GET 回调我们的 `/api/v1/ad/pangle-callback`(带 `user_id` / `trans_id` / `sign` 等 query)。本模块负责**验签**,确认回调真来自穿山甲而非伪造请求;验签通过后由 crud 幂等发金币。客户端不参与发奖,被破解也刷不到钱。
|
||||
|
||||
## 验签方案(关键)
|
||||
**`sign = SHA256("{m-key}:{trans_id}")` 的十六进制串。**
|
||||
|
||||
- 是**普通 SHA256**,不是 HMAC,也不是 RSA。
|
||||
- **只对 `m-key:trans_id` 这一个字符串**签名,`user_id` / `reward_amount` 等其余参数**不参与**签名。它们的可信度由"能算出正确 sign = 知道 m-key"间接保证——伪造者没有 m-key 连合法 sign 都造不出,自然无法注入假 `user_id`。
|
||||
- 这是穿山甲 **GroMore「广告位 → 服务端激励回调」官方规范**(supportcenter/26240)。我们客户端是 `useMediation(true)` 融合,回调走 **GroMore 广告位层级**,不是联盟代码位层级。
|
||||
|
||||
## 函数
|
||||
| 函数 | 说明 |
|
||||
|---|---|
|
||||
| `build_sign(trans_id, secret) -> str` | 计算 `SHA256("{secret}:{trans_id}")` hex。自验签测试 / 模拟回调脚本共用,保证两端一致 |
|
||||
| `verify_callback_sign(params, secret) -> bool` | 校验回调签名。密钥空 / 缺 `trans_id` 一律失败;比较用 `hmac.compare_digest` 定长比较防时序侧信道 |
|
||||
|
||||
## 配置
|
||||
| 配置项 | 说明 |
|
||||
|---|---|
|
||||
| `PANGLE_REWARD_SECRET` | m-key(安全密钥)。后台「GroMore 聚合管理 → 搜广告位 ID → 编辑」处获取 |
|
||||
|
||||
## 踩坑
|
||||
- **别用联盟代码位那套**:联盟代码位层级用的是另一套 Security Key + `isValid` 响应体,与 GroMore 广告位层级不通用。我们走 GroMore,别接错。
|
||||
- 验签失败时 api 层返 `403`(留给真请求重试);验签过但参数缺/坏或 user 不存在,则受理不发奖、不让穿山甲重试。
|
||||
- 本地无公网、回调打不到时,用 [ad-test-grant](../api/ad-test-grant.md) 模拟(仅联调,开关控制)。
|
||||
@@ -0,0 +1,24 @@
|
||||
# 短信验证码(sms)
|
||||
|
||||
> 文件:`app/integrations/sms.py` | 关联接口:[auth-sms-send](../api/auth-sms-send.md) · [auth-sms-login](../api/auth-sms-login.md) | [← 集成索引](./README.md)
|
||||
|
||||
## 作用
|
||||
手机号 + 验证码登录的验证码发送 / 校验。**当前是 mock 模式**(占坑),未接真实短信供应商。
|
||||
|
||||
## 函数 / 异常
|
||||
| 函数 | 行为 |
|
||||
|---|---|
|
||||
| `send_code(phone) -> int` | mock 下不真发、只 log;返回距下次可发的秒数。进程内存表 `_last_sent` 记 `phone → 发送时间`,`SMS_SEND_INTERVAL_SEC` 内再发抛 `SmsError`(发送过频)。真供应商未接时抛 `SmsError("sms provider not configured")` |
|
||||
| `verify_code(phone, code) -> bool` | mock 下**任意 6 位数字**均通过(配合前台 demo);真模式未实现返回 `False` |
|
||||
| 异常 `SmsError` | 发送过频 / 验证码不对,api 层 catch 后翻成 4xx(过频 `429`) |
|
||||
|
||||
## 配置
|
||||
| 配置项 | 说明 |
|
||||
|---|---|
|
||||
| `SMS_MOCK` | 是否 mock(当前 `true`) |
|
||||
| `SMS_SEND_INTERVAL_SEC` | 同号两次发送最小间隔(防过频) |
|
||||
|
||||
## 后续(接真实供应商时)
|
||||
- `send_code()` 改调供应商 API(阿里云 / 腾讯云),把生成的 6 位码存 cache(redis / sqlite)。
|
||||
- `verify_code()` 比对 cache 里的码,且**验过即作废**。
|
||||
- 进程内存方案占坑期够用;切真实供应商时一并切 redis(多进程/多机时内存表不共享,频控会失效)。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 微信支付 V3 — 商家转账到零钱(提现)+ code 换 openid(wxpay)
|
||||
|
||||
> 文件:`app/integrations/wxpay.py` | 关联接口:[wallet-withdraw](../api/wallet-withdraw.md) · [wallet-withdraw-status](../api/wallet-withdraw-status.md) · [wallet-bind-wechat](../api/wallet-bind-wechat.md) · [wallet-withdraw-info](../api/wallet-withdraw-info.md) | [← 集成索引](./README.md)
|
||||
|
||||
## 作用
|
||||
两件事:①**提现**——微信支付 V3「商家转账到零钱」;②**绑定**——微信授权 code 换 openid / 昵称头像。移植自 elderhelper,密钥**懒加载**(文件缺失不在 import 时炸,只在真正调用时抛 `WxPayNotConfiguredError`,与极光私钥同款策略)。
|
||||
|
||||
## 函数
|
||||
| 函数 | 说明 |
|
||||
|---|---|
|
||||
| `create_transfer(openid, amount_fen, out_bill_no, user_name=None)` | 发起商家转账到零钱。返回 `{status_code, data}` |
|
||||
| `query_transfer(out_bill_no)` | 按商户单号查转账单状态 |
|
||||
| `cancel_transfer(out_bill_no)` | 撤销转账单(仅 `WAIT_USER_CONFIRM`/`ACCEPTED` 可撤)。用户没在确认页确认就离开时调 |
|
||||
| `encrypt_sensitive(plain)` | 用微信支付平台公钥 OAEP(SHA1) 加密敏感信息(实名) |
|
||||
| `code_to_openid(code)` | code 换 openid(`sns/oauth2`),失败抛 `ValueError` |
|
||||
| `code_to_userinfo(code)` | code 换 openid + 拉昵称头像,返回 `{openid, nickname, avatar_url, raw}` |
|
||||
|
||||
## 签名机制
|
||||
- 请求签名:**商户 API 私钥** RSA-SHA256/PKCS1v15,构造 V3 `Authorization` 头(`WECHATPAY2-SHA256-RSA2048`,含 mchid/nonce/timestamp/serial_no/signature)。
|
||||
- 敏感字段(实名):微信支付**平台公钥** OAEP(SHA1) 加密,请求头带 `Wechatpay-Serial`。
|
||||
- 实名传递阈值:`amount_fen >= 30`(沿用 elderhelper,实际以微信侧要求为准)才加密带 `user_name`。
|
||||
- 转账接口路径:`/v3/fund-app/mch-transfer/transfer-bills`,host `https://api.mch.weixin.qq.com`。
|
||||
|
||||
## 配置
|
||||
| 配置项 | 说明 |
|
||||
|---|---|
|
||||
| `WECHAT_APP_ID` / `WECHAT_APP_SECRET` | 微信开放平台移动应用 appid / secret(换 openid 用) |
|
||||
| `WXPAY_MCH_ID` / `WXPAY_MCH_SERIAL_NO` | 商户号 / 商户证书序列号 |
|
||||
| `WXPAY_MCH_PRIVATE_KEY_PATH` | 商户 API 私钥 .pem 路径 |
|
||||
| `WXPAY_PUBLIC_KEY_PATH` / `WXPAY_PUBLIC_KEY_ID` | 微信支付平台公钥 .pem 路径 / 公钥 ID |
|
||||
| `WXPAY_TRANSFER_SCENE_ID` | 转账场景 ID |
|
||||
| `WXPAY_REQUEST_TIMEOUT_SEC` | 请求超时 |
|
||||
|
||||
## 踩坑
|
||||
- **userinfo 可能脱敏**:微信隐私新政下 `sns/userinfo` 可能返回昵称「微信用户」/灰头像,`nickname`/`avatar_url` 可能为空,调用方需兜底(绑定不因此失败,openid 已拿到)。
|
||||
- `cancel_transfer` 是提现「轮询查单时若仍 `WAIT_USER_CONFIRM`」的退款依据(见 [wallet-withdraw-status](../api/wallet-withdraw-status.md) 的副作用)。
|
||||
- 凭证/证书未配置时所有转账类调用抛 `WxPayNotConfiguredError`,api 层翻成 `503`。
|
||||
+41
-16
@@ -41,44 +41,69 @@
|
||||
|
||||
## 3. 分层架构与目录结构
|
||||
|
||||
标准 FastAPI 分层,一个请求自上而下穿过:**api(薄,收发) → integrations(外部) / repositories(数据) → models / db**。
|
||||
标准 FastAPI 分层,一个请求自上而下穿过:**api(薄,收发) → integrations(外部 SDK) / crud · repositories(数据) → models / db**。重逻辑(SDK 签名/加解密/外部 HTTP)落 `integrations`,集成层细节见 [docs/integrations/](./integrations/README.md)。
|
||||
|
||||
```
|
||||
app/
|
||||
├── main.py # FastAPI 入口:注册 3 个 router、CORS、/health、lifespan
|
||||
├── main.py # FastAPI 入口:注册全部 router、CORS、/health、lifespan
|
||||
├── api/
|
||||
│ ├── deps.py # 共享依赖:get_current_user(鉴权)、get_db(注入 session)
|
||||
│ └── v1/
|
||||
│ ├── auth.py # 登录 6 个端点
|
||||
│ ├── coupon.py # 领券透传 /coupon/step(转发 pricebot,唯一需鉴权的业务接口)
|
||||
│ └── meituan.py # 美团 3 个端点 + feed 拼接逻辑(_interleave / _TOPIC_ROUNDS)
|
||||
│ └── v1/ # 接口层(薄):解析请求 → 调 crud/integration → 组装响应 + HTTP 错误码
|
||||
│ ├── auth.py # 登录 6 端点(极光一键登录 / 短信 send+login / refresh / me / logout)
|
||||
│ ├── coupon.py # 领券透传 /coupon/step(转发 pricebot)
|
||||
│ ├── meituan.py # 美团 3 端点 + feed 拼接(_interleave / _TOPIC_ROUNDS)
|
||||
│ ├── wallet.py # 钱包/提现 11 端点(余额/流水/兑换/绑微信/提现/查单)
|
||||
│ ├── signin.py # 签到 2 端点(状态 / 执行签到)
|
||||
│ ├── tasks.py # 一次性任务 2 端点(列表 / 领取)
|
||||
│ ├── savings.py # 省钱 3 端点(汇总 / 战绩 / 明细)
|
||||
│ └── ad.py # 看广告发奖 3 端点(穿山甲 S2S 回调 / 进度 / 联调发奖)
|
||||
├── schemas/ # Pydantic:API 收发的数据契约(与客户端对齐字段看这里)
|
||||
│ ├── auth.py
|
||||
│ └── meituan.py
|
||||
├── integrations/ # 外部服务客户端(2026-05 从 core 拆出)
|
||||
│ ├── meituan.py
|
||||
│ ├── welfare.py # 钱包/签到/任务/省钱 收发模型
|
||||
│ └── ad.py # 看广告发奖收发模型
|
||||
├── integrations/ # 外部服务/SDK 客户端(重逻辑:签名/加解密/外部 HTTP)
|
||||
│ ├── jiguang.py # 极光 REST 验 token + RSA 解密(多 padding 试错)
|
||||
│ ├── meituan.py # 美团 CPS 网关签名 + query_coupon / get_referral_link
|
||||
│ └── sms.py # 短信验证码(mock,进程内存冷却表)
|
||||
├── core/ # 基础设施(仅保留)
|
||||
│ ├── sms.py # 短信验证码(mock,进程内存冷却表)
|
||||
│ ├── pangle.py # 穿山甲激励视频发奖回调验签(SHA256,2026-05 从 core 移入)
|
||||
│ └── wxpay.py # 微信支付 V3 商家转账(提现)+ code 换 openid(2026-05 从 core 移入)
|
||||
├── core/ # 基础设施(无外部业务集成)
|
||||
│ ├── config.py # pydantic-settings
|
||||
│ ├── security.py # JWT 签发/校验
|
||||
│ ├── ratelimit.py # 同 IP 滑动窗口限流依赖
|
||||
│ ├── rewards.py # 发奖/兑换/提现额度等业务常量与换算
|
||||
│ └── logging.py
|
||||
├── repositories/ # 数据访问(2026-05 由 crud 改名)
|
||||
├── crud/ # 福利业务数据访问 + 事务(钱包/签到/任务/省钱/发奖)
|
||||
│ ├── wallet.py # 账户/流水/兑换/提现单(调 integrations/wxpay)
|
||||
│ ├── signin.py # 签到记录 / 连续天数 / 档位
|
||||
│ ├── task.py # 一次性任务领取
|
||||
│ ├── savings.py # 省钱汇总 / 战绩 / 明细
|
||||
│ └── ad_reward.py # 看广告发奖(按 trans_id 幂等 + 每日上限)
|
||||
├── repositories/ # user 域数据访问(2026-05 由 crud 改名沿用)
|
||||
│ └── user.py # get_user_by_id / get_user_by_phone / upsert_user_for_login
|
||||
├── models/user.py # ORM:user 表结构
|
||||
├── models/ # ORM 表结构
|
||||
│ ├── user.py # user(含微信 openid/nickname/avatar)
|
||||
│ ├── wallet.py # 金币账户 / 金币流水 / 现金流水 / 提现单
|
||||
│ ├── signin.py # 签到记录
|
||||
│ ├── task.py # 任务领取记录
|
||||
│ ├── savings.py # 省钱明细 / 店铺菜品
|
||||
│ └── ad_reward.py # 看广告发奖记录
|
||||
└── db/
|
||||
├── base.py # DeclarativeBase
|
||||
└── session.py # engine + get_db
|
||||
|
||||
alembic/ # 数据库迁移(versions/ 目前仅 init_user_table)
|
||||
alembic/ # 数据库迁移(versions/ 目前 9 个迁移,down_revision 链式)
|
||||
deploy/ # systemd(.service) + nginx(.conf)
|
||||
secrets/ # 极光 RSA 私钥(不入 git,仅 .gitkeep 占位)
|
||||
tests/ # pytest(conftest + test_auth + test_health)
|
||||
secrets/ # 极光 RSA 私钥 / 微信支付证书(不入 git,仅 .gitkeep 占位)
|
||||
scripts/ # 运维脚本(对账 / 重置签到 / 重置福利 / 模拟穿山甲回调)
|
||||
tests/ # pytest(auth / health / welfare / withdraw / ad_reward / coupon_proxy)
|
||||
run.sh # 本地启动脚本
|
||||
docs/api/ # API 接口文档(索引 README + 一接口一文件)
|
||||
docs/integrations/ # 集成层实现文档(SDK 签名/加解密/协议细节)
|
||||
```
|
||||
|
||||
> **命名说明**:`api/v1/` 的 `v1` 用于 URL 版本化(移动端无法强制即时升级,需新旧版本并存能力);`integrations` 装外部集成、`repositories` 装数据访问,与 `core`(基础设施)分离。`coupon.py` 是领券透传,勿与 `meituan.py` 里的 `coupons`(券列表)混淆。
|
||||
> **命名说明**:`api/v1/` 的 `v1` 用于 URL 版本化(移动端无法强制即时升级,需新旧版本并存能力);`integrations` 装外部 SDK 集成、`crud`/`repositories` 装数据访问、`core` 装基础设施,三者分离。**数据访问层有两个目录是历史遗留**:user 域在 `repositories/`(早期 crud 改名),后来的福利业务在 `crud/`,职责相同。`coupon.py` 是领券透传,勿与 `meituan.py` 里的 `coupons`(券列表)混淆。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user