diff --git a/docs/api/README.md b/docs/api/README.md index b1d3377..4ade4e6 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -3,7 +3,7 @@ > Base URL:生产 `https://app-api.shaguabijia.com`;本地联调 `http://<开发机>:8770` > 协议:HTTP / JSON,请求与响应体均 `application/json`,字段统一 **snake_case** > 鉴权:需鉴权的接口在请求头带 `Authorization: Bearer ` -> 最后更新:2026-07-03(补全缺失文档:ad/watch-report, wallet/transfer-auth 族, coupon/session+stats+completed-today+prompt 族, invite 族, user/onboarding, platform/flags+ad-config+app-version, intent/step+precoupon/step, analytics/events, order/report, report 族, feedback/config+records, trace/finalize。文档移至分类子目录,新增 mock 入参/出参示例) +> 最后更新:2026-07-09(① 比价透传改「软鉴权 + trace_id 签发 + harvest 落库」(#112 尾声帧 `trace/epilogue` 一并补录);② 新端点:`user/onboarding/reset`(#114)、`GET /internal/launch-confirm-samples`(#91);③ 参数更新:提现族 `source` 分账(#82/#121)、`wallet/account` 邀请奖励金余额、美团 feed/top-sales 按城市过滤(#116)、admin 调现金 `account` 目标账户(#95);④ **Admin 索引补全到当前全量**:新家族 roles(#117/#126)/coupon-data(#99)/device-liveness(#80)/event-logs(#83)/price-reports(#94)/CPS 运营台/提现审核族,及 feedbacks 采纳拒绝(#94/#105)、marquee 模式与真实条浏览(#122/#123)等。上一次 2026-07-03) > 架构:`app/api/v1/` 只放很轻的接口层;穿山甲/微信支付/极光/短信/美团等 SDK 集成的重逻辑在 `app/integrations/`,实现细节见 [docs/integrations/](../integrations/README.md)。 --- @@ -27,17 +27,18 @@ | 8e | `GET /api/v1/coupon/completed-today` | 无 | [详情](./coupon/coupon-completed-today.md)(这台设备今天是否已跑完整轮领券) | | 8f | `POST /api/v1/coupon/completed-today/reset` | 无 | [详情](./coupon/coupon-completed-today.md)(重置今日已完成,开发用) | | 8g | `GET /api/v1/coupon/stats` | Bearer | [详情](./coupon/coupon-stats.md)(累计领券数,「我的」页战绩卡) | -| 8h | `POST /api/v1/coupon/session` | 无 | [详情](./coupon/coupon/coupon-session.md)(领券流水上报,admin 看板数据源) | +| 8h | `POST /api/v1/coupon/session` | 无 | [详情](./coupon/coupon-session.md)(领券流水上报,admin 看板数据源) | | 9 | `POST /api/v1/meituan/coupons` | 无 | [详情](./meituan/meituan-coupons.md) | -| 10 | `POST /api/v1/meituan/feed` | 无 | [详情](./meituan/meituan-feed.md) | +| 10 | `POST /api/v1/meituan/feed` | 无 | [详情](./meituan/meituan-feed.md)(`rec` tab 离线库 + **按城市过滤** #116) | | 11 | `POST /api/v1/meituan/referral-link` | 无 | [详情](./meituan/meituan-referral-link.md) | -| 11a | `POST /api/v1/meituan/top-sales` | 无 | [详情](./meituan/meituan-top-sales.md)(销量榜:离线库 `meituan_coupon` 按销量降序 + 跨源去重,不实时打美团) | -| **比价透传**(前缀 `/api/v1`,外卖 MVP;与 `coupon/step` 同为透传 pricebot-backend;下按 Phase 流程列,均不鉴权) ||| -| 12 | `POST /api/v1/intent/recognize` | 无 | [详情](./intent/compare-intent-recognize.md)(Phase 1 意图识别,单次,多数源) | -| 12a | `POST /api/v1/intent/precoupon/step` | 无 | [详情](./intent/intent-step.md)(Phase 0 意图识别前先用券,仅美团源) | -| 12b | `POST /api/v1/intent/step` | 无 | [详情](./intent/intent-step.md)(Phase 1 多帧意图识别,仅淘宝源,循环到 done) | -| 13 | `POST /api/v1/price/step` | 无 | [详情](./intent/compare-price-step.md)(Phase 2 步进) | -| 13a | `POST /api/v1/trace/finalize` | 无 | [详情](./other/trace-finalize.md)(比价 trace 收尾上云,终止/未识别拿 trace_url) | +| 11a | `POST /api/v1/meituan/top-sales` | 无 | [详情](./meituan/meituan-top-sales.md)(同城销量榜:离线库按销量降序 + 跨源去重 + 城市过滤 #116,不实时打美团) | +| **比价透传**(前缀 `/api/v1`,透传 pricebot-backend;**软鉴权 OptionalUser** + 首帧签发 trace_id + harvest 落 `comparison_record`,2026-07 起不再是纯透传) ||| +| 12 | `POST /api/v1/intent/recognize` | 软 | [详情](./intent/compare-intent-recognize.md)(Phase 1 意图识别,单次,多数源;mint 帧建 running 行) | +| 12a | `POST /api/v1/intent/precoupon/step` | 软 | [详情](./intent/intent-step.md)(Phase 0 意图识别前先用券,仅美团源) | +| 12b | `POST /api/v1/intent/step` | 软 | [详情](./intent/intent-step.md)(Phase 1 多帧意图识别,仅淘宝源,循环到 done) | +| 13 | `POST /api/v1/price/step` | 软 | [详情](./intent/compare-price-step.md)(Phase 2 步进;done 帧 harvest 写终态) | +| 13a | `POST /api/v1/trace/finalize` | 软 | [详情](./other/trace-finalize.md)(比价 trace 收尾上云 + 夭折落库,终止/未识别拿 trace_url) | +| 13b | `POST /api/v1/trace/epilogue` | 软 | [详情](./other/trace-finalize.md)(结果页尾声帧:App 结果页截图入 trace,纯透传不落库,#112) | | **比价记录**(前缀 `/api/v1/compare`;按用户落库,**鉴权**,区别于上面不鉴权的透传) ||| | 12a | `POST /api/v1/compare/record` | Bearer | [详情](./compare/compare-record-report.md) | | 12b | `GET /api/v1/compare/records` | Bearer | [详情](./compare/compare-records.md) | @@ -54,7 +55,7 @@ | **上报更低价**(前缀 `/api/v1/report`;众包纠偏,人工审核发奖) ||| | R1 | `POST /api/v1/report` | Bearer | [详情](./other/report-submit.md)(提交上报更低价,multipart:比价记录ID+平台+价格+截图1-4张) | | R2 | `GET /api/v1/report/records` | Bearer | [详情](./other/report-records.md)(上报记录列表,?status=pending/approved/rejected 可选筛选) | -| **好友邀请**(前缀 `/api/v1/invite`;注册即生效,双方各发 1 万金币) ||| +| **好友邀请**(前缀 `/api/v1/invite`;绑定注册即生效但**不发奖**,#113 起好友「比价并下单」才给邀请人发**邀请奖励金**,经 `POST /order/report` 触发) ||| | I1 | `GET /api/v1/invite/me` | Bearer | [详情](./invite/invite-me.md)(我的邀请码+分享链接+已邀人数/已得金币) | | I2 | `GET /api/v1/invite/invitees` | Bearer | [详情](./invite/invite-invitees.md)(我邀请的人列表,limit/offset 分页) | | I3 | `POST /api/v1/invite/landing-track` | 无 | [详情](./invite/invite-bind.md)(落地页 dl.html 访问上报指纹,剪贴板归因兜底;浏览器无 token) | @@ -68,9 +69,9 @@ | 19 | `POST /api/v1/wallet/bind-wechat` | Bearer | [详情](./wallet/wallet-bind-wechat.md) | | 20 | `POST /api/v1/wallet/unbind-wechat` | Bearer | [详情](./wallet/wallet-unbind-wechat.md) | | 21 | `GET /api/v1/wallet/withdraw-info` | Bearer | [详情](./wallet/wallet-withdraw-info.md) | -| 22 | `POST /api/v1/wallet/withdraw` | Bearer | [详情](./wallet/wallet-withdraw.md) | +| 22 | `POST /api/v1/wallet/withdraw` | Bearer | [详情](./wallet/wallet-withdraw.md)(`source` 分账:coin_cash / invite_cash,#121) | | 23 | `GET /api/v1/wallet/withdraw/status` | Bearer | [详情](./wallet/wallet-withdraw-status.md) | -| 24 | `GET /api/v1/wallet/withdraw-orders` | Bearer | [详情](./wallet/wallet-withdraw-orders.md) | +| 24 | `GET /api/v1/wallet/withdraw-orders` | Bearer | [详情](./wallet/wallet-withdraw-orders.md)(可按 `source` 过滤) | | 24a | `POST /api/v1/wallet/transfer-auth` | Bearer | [详情](./wallet/wallet-transfer-auth.md)(开启免确认到账,申请授权,返回拉起微信授权页的 package) | | 24b | `GET /api/v1/wallet/transfer-auth/status` | Bearer | [详情](./wallet/wallet-transfer-auth.md)(查免确认授权状态,从微信授权页返回后轮询) | | 24c | `POST /api/v1/wallet/transfer-auth/close` | Bearer | [详情](./wallet/wallet-transfer-auth.md)(关闭免确认到账,解除授权) | @@ -99,6 +100,7 @@ | 36 | `POST /api/v1/user/avatar` | Bearer | [详情](./user/user-avatar.md) | | 36a | `POST /api/v1/user/onboarding/complete` | Bearer | [详情](./user/user-onboarding.md)(标记新手引导完成,按 账号+device_id 幂等,跨卸载重装持久) | | 36b | `GET /api/v1/user/onboarding/status` | Bearer | [详情](./user/user-onboarding.md)(查该 账号+设备 是否走过引导,运营在 admin 删记录即触发重走) | +| 36c | `POST /api/v1/user/onboarding/reset` | Bearer | [详情](./user/user-onboarding.md)(重置本设备引导标记,下次登录重走,#114) | | 37 | `DELETE /api/v1/user` | Bearer | [详情](./user/user-delete.md) | | **帮助与反馈**(前缀 `/api/v1/feedback`) ||| | 38 | `POST /api/v1/feedback` | Bearer | [详情](./other/feedback.md) | @@ -127,43 +129,37 @@ | N4 | `POST /internal/store-mapping/invalidate` | 内部密钥 | [详情](./internal/internal.md)(标记某平台 shopId 缓存 deeplink 失效) | | N5 | `POST /internal/launch-confirm-sample` | 内部密钥 | [详情](./internal/internal.md)(启动确认窗兜底样本落 `launch_confirm_sample`) | | N6 | `POST /internal/app-version` | 内部密钥 | [详情](./internal/internal.md)(发布流程写最新 App 版本,落 `app_config`) | +| N7 | `GET /internal/launch-confirm-samples` | 内部密钥 | [详情](./internal/internal.md)(样本列表,供 pricebot distill 脚本聚合沉淀回静态规则,#91) | | **静态资源**(StaticFiles 挂载,见下方 `/media` 静态服务) ||| | - | `GET /media/avatars/` | 无 | 用户头像;返回二进制图片 | | - | `GET /media/feedback/` | 无 | 反馈截图;返回二进制图片 | -| **运营后台 Admin**(独立子应用 `app/admin/`,前缀 `/admin/api`,独立进程 + 独立 admin JWT。鉴权列:`admin`=任意已登录管理员,`operator`/`finance`/`super_admin`=需对应角色(`super_admin` 恒通过)) ||| -| A1 | `POST /admin/api/auth/login` | 无 | [详情](./admin/auth/admin-auth-login.md) | -| A2 | `GET /admin/api/auth/me` | admin | [详情](./admin/auth/admin-auth-me.md) | -| A3 | `GET /admin/api/stats/overview` | admin | [详情](./admin/admin-stats-overview.md) | -| A4 | `GET /admin/api/users` | admin | [详情](./admin/users/admin-users-list.md) | -| A5 | `GET /admin/api/users/{user_id}` | admin | [详情](./admin/users/admin-user-detail.md) | -| A6 | `POST /admin/api/users/{user_id}/status` | operator | [详情](./admin/users/admin-user-status.md) | -| A7 | `POST /admin/api/users/{user_id}/coins` | finance | [详情](./admin/users/admin-user-coins.md) | -| A8 | `POST /admin/api/users/{user_id}/cash` | finance | [详情](./admin/users/admin-user-cash.md) | -| A9 | `GET /admin/api/wallet/coin-transactions` | admin | [详情](./admin/wallet/admin-wallet-coin-transactions.md) | -| A10 | `GET /admin/api/wallet/cash-transactions` | admin | [详情](./admin/wallet/admin-wallet-cash-transactions.md) | -| A11 | `GET /admin/api/withdraws` | admin | [详情](./admin/withdraws/admin-withdraws-list.md) | -| A12 | `POST /admin/api/withdraws/reconcile` | finance | [详情](./admin/withdraws/admin-withdraw-reconcile.md) | -| A13 | `POST /admin/api/withdraws/{out_bill_no}/refresh` | finance | [详情](./admin/withdraws/admin-withdraw-refresh.md) | -| A14 | `GET /admin/api/feedbacks` | admin | [详情](./admin/feedbacks/admin-feedbacks-list.md) | -| A15 | `POST /admin/api/feedbacks/{feedback_id}/handle` | operator | [详情](./admin/feedbacks/admin-feedback-handle.md) | -| A16 | `GET /admin/api/admins` | super_admin | [详情](./admin/admins/admin-admins-list.md) | -| A17 | `POST /admin/api/admins` | super_admin | [详情](./admin/admins/admin-admin-create.md) | -| A18 | `PATCH /admin/api/admins/{admin_id}` | super_admin | [详情](./admin/admins/admin-admin-update.md) | -| A19 | `GET /admin/api/audit-logs` | admin | [详情](./admin/admin-audit-logs.md) | -| A20 | `GET /admin/api/dashboard-display` | admin | [详情](./admin/admin-dashboard-display.md) | -| A21 | `PATCH /admin/api/dashboard-display/{metric}` | operator | [详情](./admin/admin-dashboard-display.md) | -| A22 | `GET /admin/api/marquee-seeds` | admin | [详情](./admin/admin-marquee-seeds.md) | -| A23 | `POST /admin/api/marquee-seeds` | operator | [详情](./admin/admin-marquee-seeds.md) | -| A24 | `PATCH /admin/api/marquee-seeds/{seed_id}` | operator | [详情](./admin/admin-marquee-seeds.md) | -| A25 | `DELETE /admin/api/marquee-seeds/{seed_id}` | operator | [详情](./admin/admin-marquee-seeds.md) | -| A26 | `POST /admin/api/marquee-seeds/bulk` | operator | [详情](./admin/admin-marquee-seeds.md) | -| A27 | `GET /admin/api/marquee-seeds/preview` | admin | [详情](./admin/admin-marquee-seeds.md) | -| A28 | `GET /admin/api/ad-coin-audit` | admin | [详情](./admin/ad/admin-ad-coin-audit.md)(看广告金币公式复算对账,只读) | -| A29 | `GET /admin/api/ad-revenue-report` | admin | [详情](./admin/ad/admin-ad-revenue-report.md)(广告收益报表:按用户/日期/类型/应用/代码位 聚合 条数/收益/金币,只读) | +| **运营后台 Admin**(独立子应用 `app/admin/`,前缀 `/admin/api`,独立进程 + 独立 admin JWT。鉴权列:`admin`=任意已登录管理员,`operator`/`finance`/`super_admin`=需对应角色;#117 起可见页由 [admin_role](../database/admin_role.md) 数据驱动,`super_admin` 恒通过) ||| +| A1 | `POST /admin/api/auth/login` · `GET /auth/me` | 无 / admin | [详情](./admin/auth/admin-auth-login.md) / [me](./admin/auth/admin-auth-me.md)(me 返回有效可见页 `pages`) | +| A2 | `GET /admin/api/stats/overview` | admin | [详情](./admin/admin-stats-overview.md)(大盘核心指标;#103 按 trace 聚合 + 京东收益 #90 + feed_scene 口径 #125) | +| A3 | `GET /admin/api/event-logs` | admin | [详情](./admin/admin-event-logs.md)(埋点日志检索,#83) | +| **A·用户**:`GET /users`(筛选排序分页)、`GET /users/{id}`(360 详情)、`GET /{id}/reward-stats` + `GET /{id}/coin-records`(提现详情联查)、`POST /{id}/status`(封禁)、`POST /{id}/debug-trace`(调试链接权限)、`POST /{id}/coins`、`POST /{id}/cash`(#95 `account` 目标账户) ||| [列表](./admin/users/admin-users-list.md) / [详情](./admin/users/admin-user-detail.md) / [状态+debug-trace](./admin/users/admin-user-status.md) / [金币](./admin/users/admin-user-coins.md) / [现金](./admin/users/admin-user-cash.md) | +| A4 | `GET /admin/api/wallet/coin-transactions` / `cash-transactions` | admin | [金币](./admin/wallet/admin-wallet-coin-transactions.md) / [现金](./admin/wallet/admin-wallet-cash-transactions.md) | +| **A·提现审核台**:`GET /withdraws`(列表)、`/summary`、`/health-check`(finance)、`/ledger-check`(#121 分账对账)、`/{out_bill_no}`(详情)、`POST /reconcile`、单笔 `refresh`/`approve`/`reject`、批量 `bulk/refresh`/`bulk/approve`/`bulk/reject` ||| [列表](./admin/withdraws/admin-withdraws-list.md) / [审核族](./admin/withdraws/admin-withdraw-review.md) / [对账](./admin/withdraws/admin-withdraw-reconcile.md) / [查单](./admin/withdraws/admin-withdraw-refresh.md) | +| **A·反馈**:`GET /feedbacks`、`/summary`、`POST /{id}/approve`(采纳发币 #94)、`/{id}/reject`、`/{id}/handle` ||| [列表](./admin/feedbacks/admin-feedbacks-list.md) / [审核族](./admin/feedbacks/admin-feedback-handle.md) | +| A5 | `GET`/`PATCH` `/admin/api/feedback-config`,`POST`/`DELETE` `…/image` | operator | 反馈页「加群二维码」卡配置(admin 侧;C 端读见 38a)(无单独文档,见 `app/admin/routers/feedback_qr.py`) | +| **A·上报更低价**:`GET /price-reports`、`/summary`、`POST /{id}/approve|reject`(#94) ||| [审核族](./admin/admin-price-reports.md) | +| A6 | `GET /admin/api/comparison-records`(+`/{id}` 详情) | admin | 比价记录检索(按 user/phone/**店与商品名模糊搜** #117 筛;详情含 LLM 调用明细)(无单独文档,见 `app/admin/routers/comparison.py`) | +| A7 | `GET /admin/api/coupon-data`(+`/user-records`) | admin | [详情](./admin/admin-coupon-data.md)(领券数据看板,#99) | +| A8 | `GET /admin/api/device-liveness`(+`/stats`) | admin | [详情](./admin/admin-device-liveness.md)(设备存活监控,#80) | +| A9 | `GET /onboarding/devices`、`POST /devices/{id}/reset`、`POST /reset-all` | operator | 新手引导记录管理(按设备聚合/重置)(无单独文档,见 `app/admin/routers/onboarding.py`) | +| **A·轮播**:`GET /marquee-seeds`、`/preview`、`/real-records`(#123)、`GET`/`PATCH` `/mode`(#122,模式落 `app_config`)、`POST`(+`/bulk`、`/batch-delete`、`/batch-enable`)、`PATCH`/`DELETE` `/{seed_id}` ||| [详情](./admin/admin-marquee-seeds.md) | +| A10 | `GET / PATCH /admin/api/dashboard-display` | admin / operator | [详情](./admin/admin-dashboard-display.md)(首页三统计配置) | +| A11 | `GET /admin/api/ad-coin-audit` | admin | [详情](./admin/ad/admin-ad-coin-audit.md)(看广告金币公式复算对账,只读) | +| A12 | `GET /admin/api/ad-revenue-report` | admin | [详情](./admin/ad/admin-ad-revenue-report.md)(广告收益报表:分页/场景/`app_env` 筛 + **DAU/ARPU** #120;真实收益侧接穿山甲日表 #92) | +| A13 | `GET / PATCH /admin/api/ad-config` | operator/finance | 广告配置(穿山甲 ID/验签密钥/各场景开关;C 端只读版见 40b)(无单独文档,见 `app/admin/routers/ad_config.py`) | +| A14 | `GET /admin/api/config`、`PATCH /config/{key}` | operator/finance | 运营可配置项([app_config](../database/app_config.md):奖励常量/提现地板价等;#117 修系统配置下发)(无单独文档,见 `app/admin/routers/config.py`) | +| **A·管理员与角色**(super_admin):`GET`/`POST` `/admins`、`PATCH`/`DELETE` `/admins/{id}`(#126 删除+`pages_override`)、`GET`/`POST` `/roles`、`GET /roles/catalog`、`PATCH`/`DELETE` `/roles/{id}`(#117/#126 自定义角色) ||| [列表](./admin/admins/admin-admins-list.md) / [建](./admin/admins/admin-admin-create.md) / [改+删](./admin/admins/admin-admin-update.md) / [角色](./admin/admin-roles.md) | +| A15 | `GET /admin/api/audit-logs` | admin | [详情](./admin/admin-audit-logs.md) | +| **A·CPS 运营台**:群/活动 CRUD、`POST /referral-links`、`POST /orders/reconcile`(美团+京东 #90)、`GET /orders`、`/stats`、群 `timeseries`/`daily`/`wx-users`/`day-users`(#79) ||| [详情](./admin/admin-cps.md) | | - | `GET /admin/api/health` | 无 | admin 健康检查(无单独文档) | > ⚠️ 美团三个接口当前**无鉴权**,且 `referral-link` 的 `sid` 允许客户端传值覆盖默认渠道——见各接口"备注"。 -> `coupon/step` 及外卖比价的 `intent/recognize`、`intent/precoupon/step`、`intent/step`、`price/step`、`trace/finalize` 都透传到 pricebot-backend,**MVP 阶段均不鉴权**(device_id 透传,待补 JWT——见 `app/api/v1/compare.py`)。 +> `coupon/step` 透传到 pricebot-backend,**仍不鉴权**(device_id 区分设备,待补 JWT)。外卖比价透传族(`intent/*`、`price/step`、`trace/finalize|epilogue`)2026-07 起改**软鉴权 OptionalUser**:带 Bearer 则比价记录绑 `user_id`,不带也放行;并由 app-server 首帧签发 `trace_id` + harvest 落 `comparison_record`(见 `app/api/v1/compare.py` 模块注释)。 > 福利相关业务接口(wallet/signin/tasks/savings、`ad/reward-status`、`ad/feed-reward`)均需 **Bearer**;`wallet/exchange-info` 是静态规则无鉴权;`ad/pangle-callback` 不走 JWT、靠穿山甲**验签**;`ad/test-grant` **仅本地联调**(开关控制,生产 404)。 > 金额字段一律以**分**为单位(`*_cents`)。 diff --git a/docs/api/admin/admin-coupon-data.md b/docs/api/admin/admin-coupon-data.md new file mode 100644 index 0000000..ad443ee --- /dev/null +++ b/docs/api/admin/admin-coupon-data.md @@ -0,0 +1,17 @@ +# /admin/api/coupon-data — 领券数据看板(#99) + +> 所属:Admin 子应用(前缀 `/admin/api`) | 鉴权:admin(任意已登录管理员) | 表 [coupon_session](../../database/coupon_session.md) | [← 返回 API 索引](../README.md) + +数据源是客户端两段上报的 `coupon_session`(一次领券任务一行:发起建行/收尾更新)。看板量化:发起数、完成率、**中途流失**(started 无终态)、平均/分位耗时、各平台耗时、机型/ROM 维度。 + +## 端点 + +| 方法 + 路径 | 说明 | +|---|---| +| `GET /admin/api/coupon-data` | 看板聚合:发起/完成数 + 耗时分位 + 按天趋势 + 逐条明细(按 `started_date` 区间 + `app_env` 筛,默认只看 prod 防测试数据串台) | +| `GET /admin/api/coupon-data/user-records` | 某用户全部领券记录(用户列表点手机号抽屉:领券次数 + 记录列表) | + +## 说明 +- 明细行 LEFT JOIN `user` 出手机号/昵称(匿名领券行 user 列为空)。 +- 「发起平台」列按 `origin_package` 区分:空=App 内首页发起,包名=从对应外卖 App 弹券引导发起。 +- 耗时口径:`elapsed_ms` 客户端全程计时(只统计 completed)。 diff --git a/docs/api/admin/admin-cps.md b/docs/api/admin/admin-cps.md new file mode 100644 index 0000000..d05a1b0 --- /dev/null +++ b/docs/api/admin/admin-cps.md @@ -0,0 +1,28 @@ +# /admin/api/cps — CPS 群发联盟运营台(群/活动/短链/对账) + +> 所属:Admin 子应用(前缀 `/admin/api/cps`) | 鉴权:读=admin,写=operator/finance(对账) | 表 [cps_group](../../database/cps_group.md) / [cps_activity](../../database/cps_activity.md) / [cps_link](../../database/cps_link.md) / [cps_click](../../database/cps_click.md) / [cps_order](../../database/cps_order.md) / [cps_wx_user](../../database/cps_wx_user.md) | [← 返回 API 索引](../README.md) +> +> 业务与授权流程详见 [guides/CPS发券分发与微信授权](../../guides/CPS发券分发与微信授权.md);C 端落地短链见 [cps-redirect](../other/cps-redirect.md)。 + +私域社群 CPS 的完整运营链:建群(拿 `sid`)→ 建活动(券/物料)→ 生成群发短链 `/c/{code}` → 用户点击/复制口令 → 联盟订单按 `sid` 归群对账,汇成「点击→下单→佣金」漏斗。 + +## 端点 + +| 方法 + 路径 | 说明 | +|---|---| +| `GET / POST /admin/api/cps/groups`,`PATCH / DELETE /groups/{id}` | 推广群 CRUD;含美团平台的群自动分配 `sid` | +| `GET / POST /admin/api/cps/activities`,`PATCH / DELETE /activities/{id}` | 可推广活动 CRUD(美团 actId / 淘宝淘口令 / 京东链接) | +| `POST /admin/api/cps/upload-image`、`GET /activity-images` | 活动落地页图上传 / 已有图列表(新建复用) | +| `POST /admin/api/cps/referral-links` | 批量生成群×活动短链(美团经 sid 转链) | +| `POST /admin/api/cps/orders/reconcile` | 拉联盟订单对账(美团 `query_order` + 京东联盟 #90,`order_id` 幂等 upsert;finance) | +| `GET /admin/api/cps/orders` | 订单明细(游标分页,可按 sid / 状态筛) | +| `GET /admin/api/cps/stats` | 按群对账统计(点击/订单/GMV/预估与结算佣金) | +| `GET /admin/api/cps/groups/{id}/timeseries` | 群点击时序(天/小时级 PV/UV/复制,折线图) | +| `GET /admin/api/cps/groups/{id}/daily` | 群每天明细大表格(点击+订单按天合并;#79 起支持按天下钻) | +| `GET /admin/api/cps/groups/{id}/wx-users` | 群内微信用户(领券画像:头像/昵称/领券次数) | +| `GET /admin/api/cps/groups/{id}/day-users` | 某天该群按用户的领券/点击 + 每人点过的券(#79) | + +## 说明 +- 订单与点击**只能在群(sid)维度汇合**,无法对到单笔(联盟只回传 sid)。 +- 京东单有效性按 `jd_valid_code`,美团按 `mt_status`(4 取消/5 风控不计佣,6 结算才到账);淘宝无对账 API,对账列显示 `-`。 +- #119 修美团 `pay_time` 入库为空导致大盘时间窗漏算。 diff --git a/docs/api/admin/admin-device-liveness.md b/docs/api/admin/admin-device-liveness.md new file mode 100644 index 0000000..2af7bfd --- /dev/null +++ b/docs/api/admin/admin-device-liveness.md @@ -0,0 +1,15 @@ +# /admin/api/device-liveness — 设备存活监控(#80) + +> 所属:Admin 子应用(前缀 `/admin/api`) | 鉴权:admin | 表 [device_liveness](../../database/device_liveness.md) | [← 返回 API 索引](../README.md) + +无障碍保护存活的后台视角:哪些设备开过保护(`ever_protected`)、现在在线还是掉线(心跳超时,#107 起阈值 1 小时)、首次开启时间(`first_protected_at`)。 + +## 端点 + +| 方法 + 路径 | 说明 | +|---|---| +| `GET /admin/api/device-liveness/stats` | 顶部卡片统计:设备总数 / 开过保护 / 当前在线 / 掉线数 | +| `GET /admin/api/device-liveness` | 设备存活列表(游标分页):在线情况/设备 id/归属用户 筛选 + 排序,**默认掉线置顶**;行含最近心跳、首次开启、App 版本、push token 有无 | + +## 说明 +- 「在线」= `last_heartbeat_at` 距今 < 超时阈值;掉线召回链路(worker 置 `kill_alert_pending` → 客户端 pull)见表文档。 diff --git a/docs/api/admin/admin-event-logs.md b/docs/api/admin/admin-event-logs.md new file mode 100644 index 0000000..9dd0c69 --- /dev/null +++ b/docs/api/admin/admin-event-logs.md @@ -0,0 +1,15 @@ +# /admin/api/event-logs — 埋点日志(#83) + +> 所属:Admin 子应用(前缀 `/admin/api`) | 鉴权:admin | 表 [analytics_event](../../database/analytics_event.md) | [← 返回 API 索引](../README.md) + +客户端埋点(`POST /api/v1/analytics/events` 批量上报)的后台检索页。 + +## 端点 + +| 方法 + 路径 | 说明 | +|---|---| +| `GET /admin/api/event-logs` | 埋点事件列表(游标分页):可按 `event` / `device_id` / `user_id` 筛;行含事件名、props、页面、机型/系统/网络、client_ts | + +## 说明 +- 纯只读;无聚合报表(要分析导出后自己算)。 +- 时间轴用 `client_ts`(事件真实发生时刻),入库时间受客户端攒批影响。 diff --git a/docs/api/admin/admin-marquee-seeds.md b/docs/api/admin/admin-marquee-seeds.md index 8478cb7..2db6d82 100644 --- a/docs/api/admin/admin-marquee-seeds.md +++ b/docs/api/admin/admin-marquee-seeds.md @@ -2,7 +2,7 @@ > 所属:Admin 组(前缀 `/admin/api/marquee-seeds`) | 鉴权:Admin Bearer(改需 operator/super) | [← 返回 API 索引](../README.md) -管理首页轮播「真实+种子混播」的兜底种子。种子是「生成规则」:`masked_user` 可空(空→feed 随机合成名)、金额是 `[min_cents, max_cents]` 区间(feed 每次随机取值)。用户侧 feed 见 [platform-savings-feed](./platform-savings-feed.md);表见 [ops_marquee_seed](../database/ops_marquee_seed.md)。金额单位:分(前端 ÷100 显示元)。 +管理首页轮播「真实+种子混播」的兜底种子。种子是「生成规则」:`masked_user` 可空(空→feed 随机合成名)、金额是 `[min_cents, max_cents]` 区间(feed 每次随机取值)。用户侧 feed 见 [platform-savings-feed](../savings/platform-savings-feed.md);表见 [ops_marquee_seed](../../database/ops_marquee_seed.md)。金额单位:分(前端 ÷100 显示元)。 ## 复用结构 OpsMarqueeSeedOut | 字段 | 类型 | 说明 | @@ -19,9 +19,20 @@ 出参 `200`:`list[OpsMarqueeSeedOut]`(按 `sort_order,id`)。 ## GET /admin/api/marquee-seeds/preview — 预览实际混播 feed -预览客户端实际会看到的轮播(真实记录会插队、种子随机抽取 / 金额随机 / 名字合成),供运营对效果。**含随机,每次结果不同**。 +预览客户端实际会看到的轮播(真实记录会插队、种子随机抽取 / 金额随机 / 名字合成),供运营对效果。**含随机,每次结果不同**;#122 起按**当前数据源模式**实时预览(mixed/real/seed 各自的真实产出)。 - 入参:`limit`(query,1~30,默认 8) -- 出参 `200`:`{"items": [{masked_user, saved_amount_cents, time}]}`(条目同 [platform-savings-feed](./platform-savings-feed.md)) +- 出参 `200`:`{"items": [{masked_user, saved_amount_cents, time}]}`(条目同 [platform-savings-feed](../savings/platform-savings-feed.md)) + +## GET /admin/api/marquee-seeds/real-records — 分页浏览当前模式下可展示的真实记录(#123) +审核用:看「真实条」到底会拿哪些 `comparison_record` 上轮播(真实条**不按用户去重**,打乱+去连簇后混播;默认昵称归「无昵称」脱敏档,#122)。 +- 入参:`limit` / `cursor`(游标分页) +- 出参 `200`:`{"items": [...], "next_cursor": int|null}` + +## GET /admin/api/marquee-seeds/mode — 首页轮播数据源模式 +出参:`{"mode": "mixed" | "real" | "seed"}`(混播 / 只真实 / 只种子)。 + +## PATCH /admin/api/marquee-seeds/mode — 改数据源模式(带审计) +- 入参:`{"mode": "mixed" | "real" | "seed"}`;operator 起。改动客户端重拉 feed 生效。 ## POST /admin/api/marquee-seeds — 新增(带审计) 入参 `OpsMarqueeSeedCreate`:`masked_user`(可选,空 / 不传 → 随机合成)、`min_cents`(必填,≥0)、`max_cents`(必填,≥min,≤1000 元)、`enabled`(默认 true)、`sort_order`(默认 0)。出参:新建的 `OpsMarqueeSeedOut`。`400`=金额非法。 diff --git a/docs/api/admin/admin-price-reports.md b/docs/api/admin/admin-price-reports.md new file mode 100644 index 0000000..438c76c --- /dev/null +++ b/docs/api/admin/admin-price-reports.md @@ -0,0 +1,16 @@ +# /admin/api/price-reports — 上报更低价审核(#94) + +> 所属:Admin 子应用(前缀 `/admin/api/price-reports`) | 鉴权:读=admin,审=operator | 表 [price_report](../../database/price_report.md) | [← 返回 API 索引](../README.md) +> +> C 端提交/查询见 [report-submit](../other/report-submit.md) / [report-records](../other/report-records.md)。 + +用户众包「上报更低价」的人工审核台:审截图与价格,通过发固定金币。 + +## 端点 + +| 方法 + 路径 | 说明 | +|---|---| +| `GET /admin/api/price-reports` | 上报列表(状态筛选 + 游标分页,含截图、关联比价记录快照) | +| `GET /admin/api/price-reports/summary` | 审核统计(pending/approved/rejected 计数) | +| `POST /admin/api/price-reports/{report_id}/approve` | 通过 → 发固定金币(`grant_coins` 同事务)+ 带审计 | +| `POST /admin/api/price-reports/{report_id}/reject` | 拒绝(填原因,用户端可见)+ 带审计 | diff --git a/docs/api/admin/admin-roles.md b/docs/api/admin/admin-roles.md new file mode 100644 index 0000000..0c00cd3 --- /dev/null +++ b/docs/api/admin/admin-roles.md @@ -0,0 +1,19 @@ +# /admin/api/roles — 角色与可见页管理(RBAC,#117/#126) + +> 所属:Admin 子应用(前缀 `/admin/api`,独立 admin JWT) | 鉴权:**super_admin** | 表 [admin_role](../../database/admin_role.md) | [← 返回 API 索引](../README.md) + +后台 RBAC 的数据驱动层:内建三角色(`super_admin`/`finance`/`operator`,`is_builtin=true` 不可删)+ **自定义角色**(勾任意页面组合)。管理员的有效可见页 = `admin_user.pages_override`(个人覆盖,非空优先)∪ 否则取其角色 `pages`;`super_admin` 恒全通。 + +## 端点 + +| 方法 + 路径 | 说明 | +|---|---| +| `GET /admin/api/roles` | 角色列表:`[{id, name, label, pages, is_builtin, admin_count}]`(含每个角色的使用人数) | +| `GET /admin/api/roles/catalog` | 页面权限目录(分组):全部可勾选的页面 key(来自 `app/admin/permissions.py` 常量,不落库),前端渲染勾选面板用 | +| `POST /admin/api/roles` | 新增自定义角色 `{name, pages}`(**name(key)= label = 输入名称**;不能叫 `super_admin`,重名 `409`);带审计 | +| `PATCH /admin/api/roles/{role_id}` | 改展示名/可见页(内建角色的 `pages` 也可调);带审计 | +| `DELETE /admin/api/roles/{role_id}` | 删角色;**内建(`is_builtin`)与在用(有 `admin_user.role` 引用)不可删**(400);带审计 | + +## 说明 +- 管理员个人覆盖在 [`PATCH /admin/api/admins/{id}`](./admins/admin-admin-update.md) 的 `pages_override` 字段改,不在本组。 +- 加新后台页面要同步登记 `permissions.py` 目录,表里只存勾选结果(目录变更无需迁移)。 diff --git a/docs/api/admin/admins/admin-admin-update.md b/docs/api/admin/admins/admin-admin-update.md index 9b2247d..d6b57f2 100644 --- a/docs/api/admin/admins/admin-admin-update.md +++ b/docs/api/admin/admins/admin-admin-update.md @@ -8,12 +8,13 @@ |---|---|---|---| | `admin_id` | int | ✓ | 目标管理员 id | -**application/json**(三字段都可选,只改传了的;至少传一个): +**application/json**(字段都可选,只改传了的;至少传一个): | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| -| `role` | string | ✗ | 改角色,枚举:`super_admin` / `finance` / `operator` | +| `role` | string | ✗ | 改角色:内建 `super_admin` / `finance` / `operator` **或自定义角色 name**(#117/#126,见 [admin-roles](../admin-roles.md)) | +| `pages_override` | list[string] \| null | ✗ | 个人可见页覆盖(#126):非空优先于角色 pages;传 `null` 清覆盖回归角色 | | `status` | string | ✗ | 启停,枚举:`active`(启用)/ `disabled`(禁用) | -| `password` | string | ✗ | 重置密码,8–72 字(传则覆盖原密码) | +| `password` | string | ✗ | 重置密码,8–72 字(传则覆盖原密码,同时更新 `plain_password` 明文副本) | ## 出参 响应 `200`:`AdminOut`(更新后的管理员) @@ -24,9 +25,15 @@ | `username` | string | 账号 | | `role` | string | 角色 | | `status` | string | 状态 | +| `pages` | list[string] | **有效可见页**(pages_override 优先,否则角色 pages;super_admin 全量) | | `created_at` | datetime | 创建时间(UTC) | | `last_login_at` | datetime \| null | 上次登录时间 | +--- + +## DELETE /admin/api/admins/{admin_id} — 删除管理员(#126) +物理删除(区别于禁用);**不可删自己**(`400`);带审计(`action=admin.delete`)。鉴权同本组(super_admin)。出参 `{"deleted": true}`。 + ## 错误码 - `400` 不能禁用自己(`admin_id == 当前 admin.id` 且 `status=disabled`) / 无任何变更字段(三字段全空) - `401` 未带 admin token / token 无效或过期 / 管理员被禁用 @@ -35,5 +42,5 @@ - `422` `role`/`status` 非法枚举 / `password` 长度不在 8–72 ## 说明 -- 更新成功后写一条审计:`action=admin.update`、`target_type=admin`、`target_id=admin_id`、`detail` 为本次实际变更字段(如 `{"role": "...", "status": "...", "password": "reset"}`,密码只记 `reset` 不记明文)。见 [admin_audit_log](../database/admin_audit_log.md)。 -- 数据表见 [admin_user](../database/admin_user.md)。 +- 更新成功后写一条审计:`action=admin.update`、`target_type=admin`、`target_id=admin_id`、`detail` 为本次实际变更字段(如 `{"role": "...", "status": "...", "password": "reset"}`,密码只记 `reset` 不记明文)。见 [admin_audit_log](../../../database/admin_audit_log.md)。 +- 数据表见 [admin_user](../../../database/admin_user.md)。 diff --git a/docs/api/admin/feedbacks/admin-feedback-handle.md b/docs/api/admin/feedbacks/admin-feedback-handle.md index c0ea532..4e45880 100644 --- a/docs/api/admin/feedbacks/admin-feedback-handle.md +++ b/docs/api/admin/feedbacks/admin-feedback-handle.md @@ -1,6 +1,24 @@ -# POST /admin/api/feedbacks/{feedback_id}/handle — 标记反馈已处理 +# /admin/api/feedbacks — 反馈审核族(采纳/拒绝/标记处理/统计) -> 所属:Admin·反馈 组(前缀 `/admin/api/feedbacks`) | 鉴权:Bearer admin_token(角色:`operator`,`super_admin` 恒通过,`require_role("operator")`) | [← 返回 API 索引](../../README.md) +> 所属:Admin·反馈 组(前缀 `/admin/api/feedbacks`) | 鉴权:Bearer admin_token(角色:`operator`,`super_admin` 恒通过,`require_role("operator")`;summary 任意 admin) | [← 返回 API 索引](../../README.md) +> +> 列表见 [admin-feedbacks-list](./admin-feedbacks-list.md)。#94 引入 采纳(发金币)/拒绝 审核语义,#105 加运营回复 `admin_reply`;旧「标记已处理」保留。 + +## POST /admin/api/feedbacks/{feedback_id}/approve — 采纳并发金币(#94) +- body:`{reward_coins?(int,可 0), admin_reply?(string,用户可见), review_note?(string,内部)}` +- 行为:`status → adopted`;`reward_coins > 0` 时走 `grant_coins` 同事务发币(流水 `coin_transaction`);写审计(`detail.after="adopted"`)。已终态(非 pending/new)→ `400`。 +- 出参:更新后的 FeedbackOut。 + +## POST /admin/api/feedbacks/{feedback_id}/reject — 拒绝采纳(#94) +- body:`{reject_reason(string,用户可见), admin_reply?, review_note?}` +- 行为:`status → rejected`;写审计(`detail.after="rejected"`)。已终态 → `400`。 + +## GET /admin/api/feedbacks/summary — 审核统计 +- 出参:各状态计数(`pending` / `adopted` / `rejected` / `handled`),审核台顶部卡片用;任意 admin 可读。 + +--- + +## POST /admin/api/feedbacks/{feedback_id}/handle — 标记反馈已处理(旧口径) ## 入参 - 路径:`feedback_id`(int) @@ -18,6 +36,6 @@ - `422` `feedback_id` 非合法 int ## 说明 -- 写操作记审计 [admin_audit_log](../database/admin_audit_log.md):`action="feedback.handle"`、`target_type="feedback"`、`target_id=`、`detail={"before": <原 status>, "after": "handled"}`、`ip=<客户端 IP>`。 +- 写操作记审计 [admin_audit_log](../../../database/admin_audit_log.md):`action="feedback.handle"`、`target_type="feedback"`、`target_id=`、`detail={"before": <原 status>, "after": "handled"}`、`ip=<客户端 IP>`。 - 状态变更与审计写入在同一事务(`commit=False` 后统一 `db.commit()`)。 -- 关联表 [feedback](../database/feedback.md)。 +- 关联表 [feedback](../../../database/feedback.md)。 diff --git a/docs/api/admin/users/admin-user-cash.md b/docs/api/admin/users/admin-user-cash.md index 7cbc25b..7f11e7a 100644 --- a/docs/api/admin/users/admin-user-cash.md +++ b/docs/api/admin/users/admin-user-cash.md @@ -10,6 +10,7 @@ | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `mode` | string | ✗ | `delta`(默认)=增减 / `set`=设为指定值 | +| `account` | string | ✗ | 目标账户(#95):`coin_cash`(默认,金币兑换的现金)/ `invite_cash`(邀请奖励金)。两本账物理隔离、各调各 | | `amount_cents` | int | ✓ | `delta` 模式:现金变动(分,正=发放,负=扣减,不可为 0);`set` 模式:目标现金值(分,须 ≥ 0) | | `reason` | string | ✓ | 操作原因,1–128 字(必填,入审计与流水备注) | @@ -28,7 +29,7 @@ - 金额单位一律为**分**(`*_cents`);本接口只动现金余额,不涉及金币。 - **set 模式**:读当前余额算出差值 `delta = target - 当前余额`,再复用同一套写入逻辑(故只写一笔差值流水)。目标值须 ≥ 0;差值为 0(已等于目标)直接拒绝。 - 扣减保护:实际写入的 `delta < 0` 时若扣减后现金余额 < 0 直接拒绝(运营误操作保护);set 模式目标值 ≥ 0 天然不会扣成负。 -- 现金变动写流水 [cash_transaction](../database/cash_transaction.md):`biz_type` 实际差值为正记 `admin_grant`、为负记 `admin_deduct`(set 模式同理,不新增流水类型),`remark = admin:`(截断至 128 字)。 -- 写操作记审计 [admin_audit_log](../database/admin_audit_log.md):`action = user.cash.grant`,`target_type = user`,`target_id = user_id`,`detail = {amount_cents(=实际差值), balance_after_cents, reason}`;set 模式额外带 `{mode:"set", target_cents, before_cents}`。并记录操作 IP。 +- 现金变动按 `account` 写对应账本流水:`coin_cash` → [cash_transaction](../../../database/cash_transaction.md),`invite_cash` → [invite_cash_transaction](../../../database/invite_cash_transaction.md)(#95);`biz_type` 实际差值为正记 `admin_grant`、为负记 `admin_deduct`(set 模式同理,不新增流水类型),`remark = admin:`(截断至 128 字)。 +- 写操作记审计 [admin_audit_log](../../../database/admin_audit_log.md):`action = user.cash.grant`,`target_type = user`,`target_id = user_id`,`detail = {amount_cents(=实际差值), balance_after_cents, reason}`;set 模式额外带 `{mode:"set", target_cents, before_cents}`。并记录操作 IP。 - 现金变动 + 审计在同一事务原子提交(改钱必留痕)。 -- 关联用户表 [user](../database/user.md);现金账户 [coin_account](../database/coin_account.md);现金流水 [cash_transaction](../database/cash_transaction.md)。 +- 关联用户表 [user](../../../database/user.md);现金账户 [coin_account](../../../database/coin_account.md);现金流水 [cash_transaction](../../../database/cash_transaction.md)。 diff --git a/docs/api/admin/users/admin-user-detail.md b/docs/api/admin/users/admin-user-detail.md index d0d14ab..9eb46e0 100644 --- a/docs/api/admin/users/admin-user-detail.md +++ b/docs/api/admin/users/admin-user-detail.md @@ -38,6 +38,15 @@ - `422` `user_id` 非整数 ## 说明 -- 金币三项(`coin_balance` / `cash_balance_cents` / `total_coin_earned`)读 [coin_account](../database/coin_account.md);从未发生金币动作(账户不存在)时统一返回 0。 +- 金币三项(`coin_balance` / `cash_balance_cents` / `total_coin_earned`)读 [coin_account](../../../database/coin_account.md);从未发生金币动作(账户不存在)时统一返回 0。 - 各 count 为聚合数,明细历史走带 `user_id` 过滤的分页接口(金币流水 / 现金流水 / 提现 / 比价 / 反馈)。 -- 关联用户表 [user](../database/user.md);金币账户 [coin_account](../database/coin_account.md);提现单 [withdraw_order](../database/withdraw_order.md);比价记录 [comparison_record](../database/comparison_record.md);反馈 [feedback](../database/feedback.md)。 +- 关联用户表 [user](../../../database/user.md);金币账户 [coin_account](../../../database/coin_account.md);提现单 [withdraw_order](../../../database/withdraw_order.md);比价记录 [comparison_record](../../../database/comparison_record.md);反馈 [feedback](../../../database/feedback.md)。 + +--- + +## 族内配套端点(提现详情页联查用) + +| 方法 + 路径 | 说明 | +|---|---| +| `GET /admin/api/users/{user_id}/reward-stats` | 用户提现/看广告统计(按时间窗口):提现审核时评估该用户金币来源是否健康 | +| `GET /admin/api/users/{user_id}/coin-records` | 用户金币发放记录(按时间窗口分页):提现详情底部表,逐笔看发币来源 | diff --git a/docs/api/admin/users/admin-user-status.md b/docs/api/admin/users/admin-user-status.md index 3645c33..4638f99 100644 --- a/docs/api/admin/users/admin-user-status.md +++ b/docs/api/admin/users/admin-user-status.md @@ -21,5 +21,11 @@ ## 说明 - 业务写(改用户状态)与审计写在同一事务原子提交:改了就有痕、有痕就真改了。 -- 写操作记审计 [admin_audit_log](../database/admin_audit_log.md):`action = user.status.set`,`target_type = user`,`target_id = user_id`,`detail = {before, after}`,并记录操作 IP。 -- 关联用户表 [user](../database/user.md)。 +- 写操作记审计 [admin_audit_log](../../../database/admin_audit_log.md):`action = user.status.set`,`target_type = user`,`target_id = user_id`,`detail = {before, after}`,并记录操作 IP。 +- 关联用户表 [user](../../../database/user.md)。 + +--- + +## POST /admin/api/users/{user_id}/debug-trace — 开关调试链接权限 +- body:`{"enabled": true|false}` → 写 `user.debug_trace_enabled`(带审计 `action=user.debug_trace.set`)。 +- 开了的用户在比价完成弹窗 + 比价记录页可见「复制调试链接」按钮(trace_url);运营按用户灰度排障用。鉴权同本组(operator)。 diff --git a/docs/api/admin/withdraws/admin-withdraw-review.md b/docs/api/admin/withdraws/admin-withdraw-review.md new file mode 100644 index 0000000..a547d97 --- /dev/null +++ b/docs/api/admin/withdraws/admin-withdraw-review.md @@ -0,0 +1,25 @@ +# /admin/api/withdraws — 提现审核台(审核/批量/对账族) + +> 所属:Admin 子应用(前缀 `/admin/api/withdraws`) | 鉴权:读=admin,写=**finance** | 表 [withdraw_order](../../../database/withdraw_order.md) | [← 返回 API 索引](../../README.md) +> +> 列表见 [admin-withdraws-list](./admin-withdraws-list.md);单笔查单见 [admin-withdraw-refresh](./admin-withdraw-refresh.md);超时对账见 [admin-withdraw-reconcile](./admin-withdraw-reconcile.md)。本文覆盖其余审核台端点。 + +提现状态机:`reviewing`(发起即扣款待审)→ 通过 `pending`(微信转账在途)→ `success`/`failed`(失败退款);拒绝 `rejected`(退款)。**#121 起按 `withdraw_order.source` 分账**:退款/流水落 `cash_transaction`(coin_cash)或 `invite_cash_transaction`(invite_cash)。 + +## 端点 + +| 方法 + 路径 | 鉴权 | 说明 | +|---|---|---| +| `GET /admin/api/withdraws/summary` | admin | 审核台统计:各状态计数(待审/在途/成功/失败/拒绝) | +| `GET /admin/api/withdraws/health-check` | finance | 提现配置健康检查(证书/密钥路径/商户配置就位与否;暴露路径故限 finance+super) | +| `GET /admin/api/withdraws/ledger-check` | admin | **资金账本校验**(#121):按 `source` 分账核对「提现单 ↔ 流水」金额闭环,邀请奖励金提现纳入对账 | +| `GET /admin/api/withdraws/{out_bill_no}` | admin | 提现单详情(审核台抽屉;用户维度联查另走 `users/{id}/reward-stats` + `coin-records`) | +| `POST /admin/api/withdraws/{out_bill_no}/approve` | finance | 审核通过 → 发起微信打款(`reviewing`→`pending`→查单归一化);带审计 | +| `POST /admin/api/withdraws/{out_bill_no}/reject` | finance | 审核拒绝 → 按 source 退款 + `rejected`;带审计 | +| `POST /admin/api/withdraws/bulk/refresh` | finance | 批量刷新查单(勾选多笔) | +| `POST /admin/api/withdraws/bulk/approve` | finance | 批量审核通过并打款 | +| `POST /admin/api/withdraws/bulk/reject` | finance | 批量审核拒绝并退款 | + +## 说明 +- 批量接口逐单处理、逐单落审计,单笔失败不中断整批(返回逐单结果)。 +- 结果不明时先查单再定夺、绝不盲目退款(防退款后又到账),同 C 端口径。 diff --git a/docs/api/intent/compare-intent-recognize.md b/docs/api/intent/compare-intent-recognize.md index e1a3d2c..dc7a2b0 100644 --- a/docs/api/intent/compare-intent-recognize.md +++ b/docs/api/intent/compare-intent-recognize.md @@ -1,13 +1,13 @@ -# POST /api/v1/intent/recognize — 外卖比价 Phase 1 意图识别(透传到 pricebot) +# POST /api/v1/intent/recognize — 外卖比价 Phase 1 意图识别(透传 + 首帧 harvest 建行) -> 所属:Compare 组(前缀 `/api/v1`,外卖比价) | 鉴权:**无(MVP 阶段不鉴权)** | [← 返回 API 索引](../README.md) +> 所属:Compare 组(前缀 `/api/v1`,外卖比价) | 鉴权:**软鉴权 OptionalUser**(带 JWT 则绑 `user_id`,不带也放行) | [← 返回 API 索引](../README.md) ## 入参 -任意 JSON body,**不做 schema 校验**,原样透传给上游。后端仅从中读 `device_id`、`trace_id`、`step` 用于日志。 +任意 JSON body,**不做 schema 校验**,原样透传给上游。后端从中读 `device_id`、`trace_id`、`step`、`device_info` 用于日志与落库。 客户端实际传源平台购物车页的无障碍树采集结果(pricebot 协议里的 `screens`:`cart_page_1` / `cart_page_2`)。 ## 出参 -pricebot-backend 的响应**原样返回**(JSON object)。典型含 `result`(店名)、`calibration`(含 `source_platform_id` / `items` / `price`),客户端在 `step=0` 把它透传进 `/price/step`。 +pricebot-backend 的响应**原样返回**(JSON object),并在顶层补 `trace_id`。典型含 `result`(店名)、`calibration`(含 `source_platform_id` / `items` / `price`),客户端在 `step=0` 把它透传进 `/price/step`。 ## 错误码 - `400` body 不是合法 JSON @@ -18,7 +18,7 @@ pricebot-backend 的响应**原样返回**(JSON object)。典型含 `result` 外卖比价由客户端无障碍引擎在源平台(淘宝闪购 / 美团 / 京东外卖)购物车页点悬浮球触发 → 调本接口拿 `query` + `calibration` → 进入 `/price/step` 循环。 -⚠️ **MVP 阶段不鉴权**(同 `coupon/step`):`device_id` 透传给 pricebot 区分设备,后端拿不到 `user_id` → 行为暂绑不到登录用户。待补 JWT,见 [待办与技术债.md](../guides/待办与技术债.md) P1。 +**trace_id 签发 + harvest 建行(2026-07 起,`compare.py`)**:客户端首帧可不带 `trace_id`——app-server 用 uuid 签发、注入转发 body、回填响应顶层;**仅签发那帧**按 `trace_id` 建 [comparison_record](../../database/comparison_record.md) 的 `running` 行(幂等,best-effort),done / finalize 帧再补终态。**软鉴权**:带 Bearer 则记录绑 `user_id`,匿名行 `user_id` 暂空、由后续 `/compare/record` 上报补。 **相关配置**: - `PRICEBOT_BASE_URL`(默认 `http://localhost:8000`) diff --git a/docs/api/intent/compare-price-step.md b/docs/api/intent/compare-price-step.md index ce904eb..370aec2 100644 --- a/docs/api/intent/compare-price-step.md +++ b/docs/api/intent/compare-price-step.md @@ -1,22 +1,26 @@ -# POST /api/v1/price/step — 外卖比价 Phase 2 步进(透传到 pricebot) +# POST /api/v1/price/step — 外卖比价 Phase 2 步进(透传 + done 帧 harvest 落库) -> 所属:Compare 组(前缀 `/api/v1`,外卖比价) | 鉴权:**无(MVP 阶段不鉴权)** | [← 返回 API 索引](../README.md) +> 所属:Compare 组(前缀 `/api/v1`,外卖比价) | 鉴权:**软鉴权 OptionalUser**(带 JWT 则绑 `user_id`,不带也放行) | [← 返回 API 索引](../README.md) ## 入参 -任意 JSON body,**不做 schema 校验**,原样透传给上游。后端仅从中读 `device_id`、`trace_id`、`step` 用于日志。 +任意 JSON body,**不做 schema 校验**,原样透传给上游。后端从中读 `device_id`、`trace_id`、`step`、`device_info` 用于日志与落库。 客户端逐帧上报 `screen_state` + 上一步 `action_result`;`step=0` 还带 `query` + `calibration`(来自 Phase 1)。 ## 出参 -pricebot-backend 的响应**原样返回**(JSON object)。含 `action`(tap / set_text / launch / wait / done…)、`continue`、`status`;最终 `done` 帧带 `comparison_results`(源 + 各目标平台到手价,按价升序)。 +pricebot-backend 的响应**原样返回**(JSON object),并在顶层补 `trace_id`(见下「trace_id 签发」)。含 `action`(tap / set_text / launch / wait / done…)、`continue`、`status`、每帧顶层 `trace_url`;最终 `done` 帧带 `comparison_results`(源 + 各目标平台到手价,按价升序)。 ## 错误码 - `400` body 不是合法 JSON - `502` pricebot 上游不可达(网络错误)或返回 5xx ## 说明 -把请求体原样转发到 `PRICEBOT_BASE_URL` 的 `/api/price/step`(去掉 `/v1`,async httpx)。**多轮循环**:客户端按返回的 `action` 操作手机、再上报下一帧,直到 `continue=false`。真正的目标驱动比价逻辑(多目标平台串行复现订单、读到手价、聚合排序)在 **pricebot-backend**,本接口只是"透传壳"。 +把请求体原样转发到 `PRICEBOT_BASE_URL` 的 `/api/price/step`(去掉 `/v1`,共享 httpx 单例)。**多轮循环**:客户端按返回的 `action` 操作手机、再上报下一帧,直到 `continue=false`。真正的目标驱动比价逻辑(多目标平台串行复现订单、读到手价、聚合排序)在 **pricebot-backend**。 -⚠️ **MVP 阶段不鉴权**(同 `coupon/step`)。 +**不再是纯透传壳(2026-07 起,`compare.py`)**: +- **trace_id 签发**:客户端首帧可不带 `trace_id`——app-server 用 uuid 签发、注入转发 body、回填进响应顶层 `trace_id`,客户端后续帧都带它(老客户端自带则原样用)。 +- **harvest 落库**([comparison_record](../../database/comparison_record.md)):首帧(mint 时)建 `running` 行 → **done 帧** `harvest_done` 写 `success`/`failed` + 派生 `best_*`/`saved_amount_cents`。写库 best-effort(threadpool 独立 session),失败不连累透传。 +- **软鉴权**:新客户端带 Bearer → 记录绑 `user_id`;老客户端/匿名 → `user_id` 暂空,由其后续 `POST /compare/record` 上报补(灰度期两条写路径按 `trace_id` reconcile,success 不降级)。 +- 邀请发奖**不在这里**:#113 起口径为好友「比价并下单」,发放在 `POST /order/report`。 **相关配置**: - `PRICEBOT_BASE_URL`(默认 `http://localhost:8000`;生产部署应与 pricebot-backend 同内网——比价一单 30~80 步、逐帧多一跳,走公网延迟会累积) diff --git a/docs/api/internal/internal.md b/docs/api/internal/internal.md index a5dd18c..d335d2f 100644 --- a/docs/api/internal/internal.md +++ b/docs/api/internal/internal.md @@ -14,12 +14,13 @@ | 方法 + 路径 | 落库 | 说明 | |---|---|---| -| `POST /internal/price-observation` | [`price_observation`](../database/price_observation.md) | 比价 done 帧整批价格事实上报;`(trace_id, platform, scope)` 幂等,返回 `{inserted, skipped}` | -| `GET /internal/store-mapping/lookup` | (只读 [`store_mapping`](../database/store_mapping.md)) | 比价前按 `source_platform`+`name`(+`lat`/`lng`)反查各目标平台已沉淀的店铺 id/deeplink;命中→pricebot 直接 deeplink 省现场搜店 | +| `POST /internal/price-observation` | [`price_observation`](../../database/price_observation.md) | 比价 done 帧整批价格事实上报;`(trace_id, platform, scope)` 幂等,返回 `{inserted, skipped}` | +| `GET /internal/store-mapping/lookup` | (只读 [`store_mapping`](../../database/store_mapping.md)) | 比价前按 `source_platform`+`name`(+`lat`/`lng`)反查各目标平台已沉淀的店铺 id/deeplink;命中→pricebot 直接 deeplink 省现场搜店 | | `POST /internal/store-mapping` | `store_mapping` | 跨平台「同一家店」身份映射上报;`trace_id` 幂等、填空合并,返回 `{inserted, row_id}` | | `POST /internal/store-mapping/invalidate` | `store_mapping` | 标记某平台 `shop_id` 的缓存 deeplink 失效(pricebot 撞错误页回退时报);当前支持 `taobao`/`jd`,其它平台 no-op,返回 `{ok, affected}` | -| `POST /internal/launch-confirm-sample` | [`launch_confirm_sample`](../database/launch_confirm_sample.md) | 启动确认窗 LLM 兜底放行后回写样本(host 包 + 弹窗树 + plan + locale);**都上报、不去重**,返回 `{id}` | -| `POST /internal/app-version` | [`app_config`](../database/app_config.md)(key=`latest_app_version`) | **发布流程**(非 pricebot)出 APK 后写最新版本号/下载链接/sha256;客户端再 `GET /api/v1/platform/app-version` 读做 OTA。也是应急改版本信息(紧急下线/改 `apk_url`)入口 | +| `POST /internal/launch-confirm-sample` | [`launch_confirm_sample`](../../database/launch_confirm_sample.md) | 启动确认窗 LLM 兜底放行后回写样本(host 包 + 弹窗树 + plan + locale);**都上报、不去重**,返回 `{id}` | +| `GET /internal/launch-confirm-samples` | (只读 `launch_confirm_sample`) | 样本列表(#91,供 pricebot `distill_launch_confirm.py` 聚合沉淀回静态规则);可选筛 `exec_success` / `host_package` / `since_days`,`limit` 默认 1000 | +| `POST /internal/app-version` | [`app_config`](../../database/app_config.md)(key=`latest_app_version`) | **发布流程**(非 pricebot)出 APK 后写最新版本号/下载链接/sha256;客户端再 `GET /api/v1/platform/app-version` 读做 OTA。也是应急改版本信息(紧急下线/改 `apk_url`)入口 | ## 错误 - `401` 密钥不匹配 / 缺失。 diff --git a/docs/api/invite/invite-bind.md b/docs/api/invite/invite-bind.md index afe9bcf..cc93ba7 100644 --- a/docs/api/invite/invite-bind.md +++ b/docs/api/invite/invite-bind.md @@ -6,7 +6,7 @@ ## POST /bind — 绑定邀请人 -把当前登录用户(被邀请人)绑定到某邀请码。支持三种归因路径:clipboard(首启读剪贴板)、manual(手动输入邀请码)、fingerprint(指纹兜底反查)。绑定成功双方各发 1 万金币。 +把当前登录用户(被邀请人)绑定到某邀请码。支持三种归因路径:clipboard(首启读剪贴板)、manual(手动输入邀请码)、fingerprint(指纹兜底反查)。**#113 起绑定只建关系、不发奖**——发奖后置到被邀请人「比价并实际下单」(`POST /order/report` 触发,给邀请人发**邀请奖励金**,`compare_reward_granted` 幂等闸一人一次)。 ### 入参 @@ -46,14 +46,14 @@ Mock 入参(指纹兜底): | 字段 | 类型 | 说明 | |---|---|---| | `status` | string | `success` / `already_bound` / `invalid_code` / `self_invite` / `not_eligible` / `fp_not_found` | -| `coins_awarded` | int | 本次给当前用户(被邀请人)发的金币 | +| `coins_awarded` | int | 兼容保留字段(#113 前"绑定即发金币"口径)。**#113 起新绑定恒 0**,前端不应再据此展示发奖 | | `message` | string | 给前端直接展示的文案 | Mock 出参: ```json { "status": "success", - "coins_awarded": 10000, + "coins_awarded": 0, "message": "邀请绑定成功" } ``` diff --git a/docs/api/invite/invite-invitees.md b/docs/api/invite/invite-invitees.md index 7709457..f402f0b 100644 --- a/docs/api/invite/invite-invitees.md +++ b/docs/api/invite/invite-invitees.md @@ -25,7 +25,8 @@ GET /api/v1/invite/invitees?limit=5&offset=0 | `items` | list[InviteeItem] | 被邀请人列表 | | `items[].display_name` | string | 显示名(昵称 → 微信昵称 → 脱敏手机号,后端已兜底) | | `items[].avatar_url` | string \| null | 头像 URL;null = 前端画默认色块 | -| `items[].coins` | int | 这次邀请给邀请人发的金币 | +| `items[].coins` | int | 这次邀请给邀请人发的金币(**历史留痕**:#113 前旧口径的发放额;新绑定恒 0) | +| `items[].is_compared` | bool | 该好友是否已完成过一次比价(#113:好友列表据此分「邀请成功 / 去提醒」,在途列表只取 `false` 的) | | `items[].invited_at` | datetime | 邀请绑定时间(ISO 8601 UTC) | | `total` | int | 我邀请的总人数 | | `has_more` | bool | 还有下一页吗 | @@ -37,13 +38,15 @@ Mock 出参: { "display_name": "省钱小王", "avatar_url": "/media/avatars/u2_f1e2d3c4b5a60708.jpg", - "coins": 10000, + "coins": 0, + "is_compared": true, "invited_at": "2026-06-28T14:30:00Z" }, { "display_name": "138****1234", "avatar_url": null, - "coins": 10000, + "coins": 0, + "is_compared": false, "invited_at": "2026-07-01T09:15:00Z" } ], diff --git a/docs/api/meituan/meituan-top-sales.md b/docs/api/meituan/meituan-top-sales.md index 2e7df8e..08a6c65 100644 --- a/docs/api/meituan/meituan-top-sales.md +++ b/docs/api/meituan/meituan-top-sales.md @@ -2,20 +2,21 @@ > 所属:美团 CPS 组(前缀 `/api/v1/meituan`,**全部无鉴权**) | 鉴权:无 | [← 返回 API 索引](../README.md) > -> 数据来自离线库 [database/meituan_coupon](../database/meituan_coupon.md);**不实时打美团**(美团搜索对销量排序支持差、且有 402 限流)。 +> 数据来自离线库 [database/meituan_coupon](../../database/meituan_coupon.md);**不实时打美团**(美团搜索对销量排序支持差、且有 402 限流)。 ## 入参 | 字段 | 类型 | 必填 | 默认 | 说明 | |---|---|---|---|---| | `page` | int | ❌ | 1 | ≥1 | | `page_size` | int | ❌ | 20 | 1–50 | -| `platform` | int \| null | ❌ | null | 1 只外卖 / 2 只到店 / 不填=全部(全城销量) | +| `platform` | int \| null | ❌ | null | 1 只外卖 / 2 只到店 / 不填=全部 | +| `longitude` / `latitude` | float \| null | ❌(实际必带) | null | 设备坐标(#116):服务端离线反查城市(`utils/geo` + `meituan_city`)→ **只返回同城券**;老客户端不带坐标 → 返空 + `status=degraded`(不 422、不误返全城) | ## 出参 -响应 `200`:`{ items: CouponCard[], has_next: bool, search_id: null, status: "ok"|"empty"|"degraded" }`。`CouponCard` 见 [API 索引](./README.md#复用数据结构);`status` 语义见 [feed 接口](./meituan-feed.md#status-字段前端据此显示占位)。 +响应 `200`:`{ items: CouponCard[], has_next: bool, search_id: null, status: "ok"|"empty"|"degraded" }`。`CouponCard` 见 [API 索引](../README.md#复用数据结构);`status` 语义见 [feed 接口](./meituan-feed.md#status-字段前端据此显示占位)。 ## 说明 -- 从 `meituan_coupon` 取 `sale_volume_num` 非空的券,`DISTINCT ON(dedup_key)` 跨源去重(每个「品牌|名|价」只留销量最高一条,同销量再按佣金),按销量降序分页;每页只对当前 ~20 条做 `from_raw` 解析(翻页快,不全表拉取)。 +- 从 `meituan_coupon` 取 `sale_volume_num` 非空 **且 `city_id` = 反查城市** 的券(#116,同城销量榜),`DISTINCT ON(dedup_key)` 跨源去重(每个「品牌|名|价」只留销量最高一条,同销量再按佣金),按销量降序分页;每页只对当前 ~20 条做 `from_raw` 解析(翻页快,不全表拉取)。 - **不依赖 MT 凭证**(纯库查询)。库为空(prod 刚部署 / ETL 未跑完)→ `status=empty`;库查询异常 → `status=degraded`。均返 `200`、不抛 5xx。 - **仅 PostgreSQL**(`DISTINCT ON` 为 PG 专用)。 diff --git a/docs/api/other/trace-finalize.md b/docs/api/other/trace-finalize.md index 19b7fc1..42607a1 100644 --- a/docs/api/other/trace-finalize.md +++ b/docs/api/other/trace-finalize.md @@ -1,9 +1,13 @@ -# POST /api/v1/trace/finalize — 比价 trace 收尾上云 +# POST /api/v1/trace/finalize + /trace/epilogue — 比价 trace 收尾族 -> 所属:透传端点(前缀 `/api/v1`,外卖比价) | 鉴权:无(MVP 阶段不鉴权) | [← 返回 API 索引](../README.md) +> 所属:透传端点(前缀 `/api/v1`,外卖比价) | 鉴权:软鉴权 OptionalUser | [← 返回 API 索引](../README.md) + +## POST /api/v1/trace/finalize — 收尾上云(+夭折落库) 透传到 pricebot-backend。用户终止 / Phase 1 未识别没走到 done 帧时,pricebot 没上云也没回传 `trace_url`。客户端收尾时打这个,pricebot 按 `trace_id` 一致性 hash 落到处理这条 trace 的同一进程(dir_cache 在那才能算对 trace 目录),打包上云返回 `{trace_url}`。 +**顺手夭折落库(2026-07 起)**:app-server 把该 trace 的 [comparison_record](../../database/comparison_record.md) `running` 行更新成夭折终态(`harvest_abort`,**不降级已 success**)。body 可带 `status`(`cancelled`/`failed`)+ `reason`;老客户端只带 `trace_id` → 默认 `cancelled`,其后续 `/compare/record` 上报再补精确态。 + ## 入参 透传 pricebot,客户端按 pricebot 协议组装。关键字段: @@ -41,5 +45,15 @@ Mock 出参: ## 说明 - 一致性 hash 按 `trace_id` 路由到同一 pricebot 实例(确保 dir_cache 命中) -- MVP 阶段不鉴权 +- 软鉴权(OptionalUser,同比价透传族) - 与 `/intent/recognize`、`/price/step` 等同属外卖比价透传族 + +--- + +## POST /api/v1/trace/epilogue — 结果页尾声帧(#112) + +App 收到 done、渲染完**结果页**后,把自己页面的截图(base64,body ~几百 KB)传给 pricebot 存进 trace 目录并触发重传——trace 里补上「用户实际看到的汇总页」(步骤帧只有目标 App 画面)。 + +- **纯透传壳**:不建行、不落库(该 trace 的比价记录已由 done / finalize 落终态)。 +- 入参:`{device_id, trace_id, screenshot(base64), ...}`(pricebot 协议);出参:pricebot 原样响应。 +- 错误码同 finalize(`400` / `502`)。 diff --git a/docs/api/user/user-onboarding.md b/docs/api/user/user-onboarding.md index 3985f29..2adaac4 100644 --- a/docs/api/user/user-onboarding.md +++ b/docs/api/user/user-onboarding.md @@ -61,7 +61,24 @@ Mock 入参: --- +## POST /api/v1/user/onboarding/reset — 重置新手引导(#114) + +删除(当前账号, `device_id`)的完成标记 → 该设备下次登录/进 App 重走引导。给客户端「设置 → 重看新手引导」入口用(此前只能运营在 admin 删记录)。 + +### 入参 +| 字段 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `device_id` | string | ✅ | 硬件级 ANDROID_ID(与 complete 一致) | + +### 出参 +```json +{"ok": true} +``` +幂等:无标记时也返回 ok。 + +--- + ## 说明 -- 替代原 `force_onboarding`(按用户)→ 改设备维度后,运营删记录即触发重走 -- 幂等:重复标记不报错 +- 替代原 `force_onboarding`(按用户)→ 改设备维度后,运营删记录(或用户自己 reset)即触发重走 +- 幂等:重复标记 / 重复重置都不报错 - `device_id` 为空时 `status` 一律返回未完成 diff --git a/docs/api/wallet/wallet-account.md b/docs/api/wallet/wallet-account.md index fc8c12f..ba523ba 100644 --- a/docs/api/wallet/wallet-account.md +++ b/docs/api/wallet/wallet-account.md @@ -11,8 +11,9 @@ | 字段 | 类型 | 说明 | |---|---|---| | `coin_balance` | int | 当前金币余额 | -| `cash_balance_cents` | int | 当前现金余额(分) | +| `cash_balance_cents` | int | 当前现金余额(分,金币兑换账) | +| `invite_cash_balance_cents` | int | 邀请奖励金余额(分,与现金**物理隔离**的第二本账,#82;好友比价并下单发奖入账,提现走 `source=invite_cash`) | | `total_coin_earned` | int | 累计赚取金币 | ## 说明 -账户不存在时自动创建(零余额)。福利页「我的资产」卡的数据源。 +账户不存在时自动创建(零余额)。福利页「我的资产」卡的数据源;邀请页「奖励金」余额也读它。 diff --git a/docs/api/wallet/wallet-withdraw-orders.md b/docs/api/wallet/wallet-withdraw-orders.md index 3c86f7c..9a1c3d5 100644 --- a/docs/api/wallet/wallet-withdraw-orders.md +++ b/docs/api/wallet/wallet-withdraw-orders.md @@ -8,9 +8,10 @@ |---|---|---|---|---| | `limit` | int | ❌ | 20 | 1–100 | | `cursor` | int | ❌ | null | 上一页末条 `id`,首页不传 | +| `source` | string | ❌ | null | 按账户来源过滤:`coin_cash` / `invite_cash`;不传=全部(#121) | ## 出参 -响应 `200`:`{ items: WithdrawOrderOut[], next_cursor: int|null }`(分页见 [索引#游标分页约定](./README.md#游标分页约定)) +响应 `200`:`{ items: WithdrawOrderOut[], next_cursor: int|null }`(分页见 [索引#游标分页约定](../README.md#游标分页约定)) **WithdrawOrderOut** @@ -19,7 +20,7 @@ | `id` | int | 单 id(也是游标) | | `out_bill_no` | string | 商户提现单号 | | `amount_cents` | int | 提现额(分) | -| `status` | string | `pending` / `success` / `failed` | +| `status` | string | `reviewing`(待审核)/ `pending` / `success` / `failed` / `rejected` | | `wechat_state` | string \| null | 微信侧原始状态 | | `fail_reason` | string \| null | 失败原因 | | `created_at` | datetime | 发起时间 | diff --git a/docs/api/wallet/wallet-withdraw.md b/docs/api/wallet/wallet-withdraw.md index 6c0d55f..18d57b1 100644 --- a/docs/api/wallet/wallet-withdraw.md +++ b/docs/api/wallet/wallet-withdraw.md @@ -2,13 +2,14 @@ > 所属:Wallet 组(前缀 `/api/v1/wallet`) | 鉴权:Bearer | 限流:同 IP ≤20 次/分 | [← 返回 API 索引](../README.md) > -> 集成实现:见 [integrations/wxpay](../integrations/wxpay.md)(微信 V3 商家转账、签名、实名加密)。 +> 集成实现:见 [integrations/wxpay](../../integrations/wxpay.md)(微信 V3 商家转账、签名、实名加密)。 ## 入参 | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `amount_cents` | int | ✅(>0) | 提现金额(分),须落在 `[min_cents, max_cents]` | +| `source` | string | ❌(默认 `coin_cash`) | 提现账户来源(#121 分账):`coin_cash`(金币兑换的现金)/ `invite_cash`(邀请奖励金)。按它扣对应余额、流水落对应账本([cash_transaction](../../database/cash_transaction.md) / [invite_cash_transaction](../../database/invite_cash_transaction.md)) | | `user_name` | string | ❌ | 实名(达额时微信商家转账要求,可空) | | `out_bill_no` | string | ❌ | **客户端幂等键(商户单号)**:同号重试不重复转账;不传则服务端生成 | diff --git a/docs/database/OVERVIEW.md b/docs/database/OVERVIEW.md index 2c25d00..5f5f525 100644 --- a/docs/database/OVERVIEW.md +++ b/docs/database/OVERVIEW.md @@ -2,7 +2,7 @@ > 跨表视角。单表字段级细节看同目录 `<表名>.md`(索引见 [README](./README.md))。 > 本文专门回答三件「跨表」的事:**① 每块 App 功能用到哪些表 ② 什么操作往哪张表写 ③ 表和表怎么连(join key,含没有外键约束、靠业务字段对齐的语义关联)**。 -> **范围**:业务表全部在 `shaguabijia-app-server`(SQLAlchemy 2.0 + SQLite 开发 / PostgreSQL 生产)。`pricebot-backend`(比价/领券 Agent)是纯内存态、**无任何表**;Android 客户端只有 EncryptedSharedPreferences / SharedPreferences、**无关系库**。共 **40 张业务表** + `alembic_version`(框架的迁移版本指针)。注意「比价/领券**过程**」始终在 pricebot 内存态跑、**不落库**——只有**结果**回 app-server 才落库:领券结果落 `coupon_*` 三张今日状态表;比价结果分两路——客户端带 JWT 上报「我的记录」落 `comparison_record`,pricebot 另经 `app/api/internal/` server→server 把客观价格/门店事实落 `price_observation`/`store_mapping`(不鉴权、匿名也记)+ 启动确认窗兜底样本落 `launch_confirm_sample`。此外好友邀请(`invite_*` 2 张)与美团 CPS 群发联盟(`cps_*` 6 张,含落地页微信身份 `cps_wx_user`)是两个独立子系统。无障碍存活监控 `device_liveness`(#65)按用户设备维度记心跳、检掉线召回。 +> **范围**:业务表全部在 `shaguabijia-app-server`(SQLAlchemy 2.0 + SQLite 开发 / PostgreSQL 生产)。`pricebot-backend`(比价/领券 Agent)是纯内存态、**无任何表**;Android 客户端只有 EncryptedSharedPreferences / SharedPreferences、**无关系库**。共 **45 张业务表** + `alembic_version`(框架的迁移版本指针)。注意「比价/领券**过程**」始终在 pricebot 内存态跑、**不落库**——只有**结果**回 app-server 才落库:领券结果落 `coupon_*` 三张今日状态表 + `coupon_session` 任务流水(#99);比价记录 2026-07 起**由 app-server 透传壳 harvest 直接落库**(`comparison_record`:帧0 建 running 行 → done/finalize 写终态,老客户端带 JWT 的 `POST /compare/record` 仅作兜底),pricebot 另经 `app/api/internal/` server→server 把客观价格/门店事实落 `price_observation`/`store_mapping`(不鉴权、匿名也记)+ 启动确认窗兜底样本落 `launch_confirm_sample`。此外好友邀请(`invite_*` 2 张 + 独立账本 `invite_cash_transaction`)与 CPS 群发联盟(`cps_*` 6 张,含落地页微信身份 `cps_wx_user`;`cps_order` 已含京东联盟单)是两个独立子系统。无障碍存活监控 `device_liveness`(#65)按用户设备维度记心跳、检掉线召回;客户端埋点落 `analytics_event`(#83)。 --- @@ -12,7 +12,7 @@ | App 位置 / 动作 | 表 | 说明 | |---|---|---| | 比价/领券**过程**(看屏→决策→操作) | (无) | 在 pricebot-backend 内存态跑,**过程不落库**;只有结果回到 app-server 才落库 | -| 「我的比价记录」列表 / 详情 | [`comparison_record`](./comparison_record.md) | 每次比价 done 后客户端带 JWT 上报一条完整明细 | +| 「我的比价记录」列表 / 详情 | [`comparison_record`](./comparison_record.md) | **app-server 透传壳 harvest 落库**(2026-07 起):帧0 建 `running` 行 → done 帧写 success/failed → `trace/finalize` 写 cancelled/failed;老客户端带 JWT 的 `POST /compare/record` 兜底 | | 比价战绩里程碑(逐档领金币) | [`comparison_milestone_claim`](./comparison_milestone_claim.md) | 累计成功比价 N 次解锁;进度读 `comparison_record` 计数 | | profile「累计省了 / 省钱战绩 / 省钱明细」 | [`savings_record`](./savings_record.md) | 真实下单归因(source=compare)+ 无真实数据时 demo 兜底 | | 「上报更低价」提交 / 列表 | [`price_report`](./price_report.md) | 众包纠偏:用户举证某平台更便宜,人工审核发奖 | @@ -27,6 +27,7 @@ | 切外卖 App 时是否弹领券引导窗 | [`coupon_prompt_engagement`](./coupon_state.md) | 今天 engage 过(点领/点拒)就不再弹;判断维度 device_id | | 首页「去领取」卡是否置灰 | [`coupon_daily_completion`](./coupon_state.md) | 今天跑完整轮(到 done)就置灰;判断维度 device_id | | 每张券领取结果留痕 | [`coupon_claim_record`](./coupon_state.md) | 资产/画像/排查/CPS;当前**不参与**判断 | +| (admin 领券看板)一次领券任务全程流水 | [`coupon_session`](./coupon_session.md) | 客户端 `POST /coupon/session` 两段上报(发起建行/收尾更新);发起数、完成率、中途流失、平均耗时都从这算(#99) | ### 钱包 / 福利(看广告赚钱闭环) | App 位置 / 动作 | 表 | 说明 | @@ -37,7 +38,9 @@ | 每日签到 | [`signin_record`](./signin_record.md) + [`signin_boost_record`](./signin_boost_record.md) | 7 天循环发币;签到后看广告可膨胀一次 | | 一次性任务(开消息提醒等) | [`user_task`](./user_task.md) | 领一次发币 | | 看激励视频赚金币 | [`ad_reward_record`](./ad_reward_record.md) + [`ad_watch_log`](./ad_watch_log.md) + [`ad_ecpm_record`](./ad_ecpm_record.md) | 独立数据流:发奖 / 旧版观看时长 / 收益对账 | -| 信息流广告结算 | [`ad_feed_reward_record`](./ad_feed_reward_record.md) | 每展示满 10 秒累计一份奖励,完成后一次性入账 | +| 信息流/Draw 广告结算 | [`ad_feed_reward_record`](./ad_feed_reward_record.md) | 每展示满 10 秒累计一份奖励,完成后一次性入账;`ad_type`(feed/draw)+`feed_scene`(compare/coupon)分形态/场景 | +| (无 App UI)穿山甲后台收益对账 | [`ad_pangle_daily_revenue`](./ad_pangle_daily_revenue.md) | 定时脚本拉 GroMore 数据 API 落日表(#92);admin 收益报表/大盘的「真实收益」侧 | +| 好友比价并下单发奖 / 邀请奖励金 | [`invite_cash_transaction`](./invite_cash_transaction.md) + `coin_account.invite_cash_balance_cents` | 与现金**物理隔离**的第二本账(#82/#113):`POST /order/report` 触发 `invite_reward` 入账;`source=invite_cash` 提现出账 | | 金币兑现金 | `coin_account` + `coin_transaction` + `cash_transaction` | exchange_out + exchange_in 两笔流水 | | 提现到微信零钱 | [`withdraw_order`](./withdraw_order.md) + [`wechat_transfer_authorization`](./wechat_transfer_authorization.md) + `cash_transaction` | 人工审核 + 微信商家转账 | | 绑定微信(提现前置) | `user`.wechat_* | openid 唯一,一微信一账号 | @@ -53,8 +56,9 @@ ### 好友邀请(注册增长) | App 位置 / 动作 | 表 | 说明 | |---|---|---| -| 输入/剪贴板邀请码绑定 | [`invite_relation`](./invite_relation.md) | 注册即生效,邀请人+被邀请人各发 1 万金币;`invitee_user_id` 唯一=幂等防重复发奖 | +| 输入/剪贴板邀请码绑定 | [`invite_relation`](./invite_relation.md) | 注册即生效(**绑定不发奖**,#113);`invitee_user_id` 唯一;`compare_reward_granted` 幂等闸=好友**比价并下单**后才给邀请人发奖励金 | | 落地页访问指纹(剪贴板归因兜底) | [`invite_fingerprint`](./invite_fingerprint.md) | 剪贴板没拿到码时,用 (ip+机型+屏幕) 7 天内反查邀请人 | +| 邀请奖励金入账/提现 | [`invite_cash_transaction`](./invite_cash_transaction.md) | 独立账本(见上钱包节);余额在 `coin_account.invite_cash_balance_cents` | ### 美团 CPS 群发联盟(私域社群比价,运营后台驱动 · 群发选品→点击→对账漏斗) | 后台/用户动作 | 表 | 说明 | @@ -66,10 +70,19 @@ | 微信内打开落地页授权 | [`cps_wx_user`](./cps_wx_user.md) | 服务号网页授权拿 openid(base 静默)/ 昵称头像 unionid(userinfo,点领券触发),记首次来源群 | | 定时拉美团联盟订单对账 | [`cps_order`](./cps_order.md) | `query_order` 按 sid 归群,串成点击→下单→佣金漏斗 | +### 埋点 / 首页门面 / 选品缓存 +| 位置 / 动作 | 表 | 说明 | +|---|---|---| +| 客户端行为埋点 | [`analytics_event`](./analytics_event.md) | `POST /analytics/events` 批量上报,一事件一行(#83);admin「埋点日志」检索 | +| 首页三统计数字 | [`ops_stat_config`](./ops_stat_config.md) | real/manual/random 三模式,admin 配 | +| 首页轮播(省钱动态) | [`ops_marquee_seed`](./ops_marquee_seed.md) | 种子条目,与真实 `comparison_record` 混播(数据源模式 mixed/real/seed 可切) | +| 首页推荐/销量榜离线选品 | [`meituan_coupon`](./meituan_coupon.md) | 美团 CPS 券缓存,定时 ETL 灌入;`feed?tab=rec` 与 `top-sales` 纯库出、不实时打美团 | + ### 运营后台 admin(独立子应用 `app/admin/`,端口 8771,独立鉴权) | 后台模块 | 表 | 说明 | |---|---|---| -| 管理员账号 / 登录 | [`admin_user`](./admin_user.md) | 与 C 端 `user` 完全隔离,独立 JWT + RBAC | +| 管理员账号 / 登录 | [`admin_user`](./admin_user.md) | 与 C 端 `user` 完全隔离,独立 JWT + RBAC;`pages_override` 个人可见页覆盖 | +| 角色 / 可见页配置 | [`admin_role`](./admin_role.md) | 内建三角色 + 自定义角色(#117/#126);`admin_user.role` 按名引用 | | 操作审计 | [`admin_audit_log`](./admin_audit_log.md) | 每个写操作落一条,只增不改不删 | | 运营可配置项(改奖励常量) | [`app_config`](./app_config.md) | 空表 = 用代码默认;后台改了即覆盖 | | 用户/钱包/提现/反馈管理 | 跨读写上面的 C 端表 | 见下「写入路径」admin 段 | @@ -92,8 +105,8 @@ | 签到膨胀 `POST /signin/boost` | `signin_boost_record`(C) + `coin_account`(U) + `coin_transaction`(C `signin_boost`) | 同事务;同日一次 | | 领任务 `POST /tasks/claim` | `user_task`(C) + `coin_account`(U) + `coin_transaction`(C `task_`) | 同事务 | | 金币兑现金 `POST /wallet/exchange` | `coin_account`(U) + `coin_transaction`(C `exchange_out` −) + `cash_transaction`(C `exchange_in` +) | 同事务 | -| 发起提现 `POST /wallet/withdraw` | `withdraw_order`(C `reviewing`) + `coin_account`(U 扣现金) + `cash_transaction`(C `withdraw` −) | 同事务,**不打款** | -| 查提现状态 / 用户取消 `GET /wallet/withdraw/status` | `withdraw_order`(U) + 失败→`cash_transaction`(C `withdraw_refund` +) | | +| 发起提现 `POST /wallet/withdraw` | `withdraw_order`(C `reviewing`,记 `source`) + `coin_account`(U 按 source 扣对应余额) + 流水(C −:`cash_transaction.withdraw` 或 `invite_cash_transaction.invite_withdraw`) | 同事务,**不打款**;#121 按 `source` 分账 | +| 查提现状态 / 用户取消 `GET /wallet/withdraw/status` | `withdraw_order`(U) + 失败→对应账本退款流水(C `withdraw_refund` / `invite_withdraw_refund` +) | | | 穿山甲发奖 S2S 回调 `POST /ad/pangle-callback` | `ad_reward_record`(C)+ granted→`coin_account`(U)+`coin_transaction`(C `reward_video`/`signin_boost`) | `trans_id` 幂等 | | 看广告时长上报 `POST /ad/watch-report` | `ad_watch_log`(C) | | | 广告 eCPM 上报 `POST /ad/ecpm-report` | `ad_ecpm_record`(C) | | @@ -101,13 +114,17 @@ | 注册设备 / 更新 push token `POST /device/register` | `device_liveness`(C/U upsert) | `(user_id, device_id)` 幂等;只在传入非空时更新 `registration_id`/版本 | | 无障碍服务心跳 `POST /device/heartbeat` | `device_liveness`(C/U) | 心跳也能自注册;`accessibility_enabled=true` 时刷 `last_heartbeat_at`、置 `alive`、清 `notified_at` | | 客户端 ack 掉线提醒 `POST /device/liveness/ack` | `device_liveness`(U) | 清 `kill_alert_pending`(幂等;下次真掉线 worker 再置) | -| 比价 done 上报 `POST /compare/record` | `comparison_record`(C 或 U) | `(user_id, trace_id)` 幂等覆盖 | +| 比价透传(帧0 / done / finalize)`POST /intent/recognize`、`/price/step`、`/trace/finalize` | `comparison_record`(C `running` → U 终态) | **harvest 三段式**(2026-07):帧0 mint trace_id 建 running 行 → done 写 success/failed → finalize 写 cancelled/failed(不降级 success);best-effort,写失败不连累透传 | +| 比价 done 上报 `POST /compare/record`(老客户端兜底) | `comparison_record`(C 或 U) | `trace_id` 幂等覆盖(唯一键已从 `(user_id,trace_id)` 改单列) | | 领里程碑 `POST /compare/milestone/claim` | `comparison_milestone_claim`(C) | **当前不发币**(coin_awarded=0) | -| 支付归因上报 `POST /order/report` | `savings_record`(C `source=compare`) | `(user_id, client_event_id)` 幂等 | +| 支付归因上报 `POST /order/report` | `savings_record`(C `source=compare`)+ 触发邀请发奖:`invite_relation`(U `compare_reward_granted`)+`coin_account`(U invite_cash)+`invite_cash_transaction`(C `invite_reward`) | `(user_id, client_event_id)` 幂等;#113 邀请发奖口径=好友**比价并下单**,`compare_reward_granted` 幂等闸,同事务 | +| 批量埋点 `POST /analytics/events` | `analytics_event`(C 批量) | 不强制登录;每批 ≤200 条 | +| 领券任务流水 `POST /coupon/session` | `coupon_session`(C/U upsert) | `trace_id` 幂等:发起建行、收尾更新同一行(#99) | +| 重置新手引导 `POST /user/onboarding/reset` | `onboarding_completion`(**D** 该 设备+账号 行) | #114,删完成标记 → 下次登录重走引导 | | 首次进 profile 省钱页且无真实记录 | `savings_record`(C `source=demo`) | 懒种子,`ensure_seeded` 按 user 幂等 | | 上报更低价 `POST /report` | `price_report`(C) | 读 `comparison_record.best_price_cents` 校验 | | 提交反馈 `POST /feedback` | `feedback`(C) | | -| 绑定邀请 `POST /invite/bind` | `invite_relation`(C `effective`) + `coin_account`(U×2) + `coin_transaction`(C `invite_inviter` + `invite_invitee`) | 同事务;`invitee_user_id` 唯一幂等,双方各发 1 万金币 | +| 绑定邀请 `POST /invite/bind` | `invite_relation`(C `effective`) | `invitee_user_id` 唯一幂等。**#113 起绑定不发奖**——发奖延后到好友比价并下单(`POST /order/report` 行),发的是邀请奖励金非金币 | | 落地页归因 `POST /invite/landing-track` | `invite_fingerprint`(C) | 剪贴板归因兜底线索,登录后用 (ip+机型+屏幕) 反查 | | 领券首帧 `POST /api/v1/coupon/step`(step=0) | `coupon_prompt_engagement`(C/U `claim_started`) | `(device_id, package, 北京日)` 幂等;best-effort | | 领券每帧结果 `POST /api/v1/coupon/step` | `coupon_claim_record`(C/U) | `(device_id, coupon_id, 北京日)` 幂等;best-effort | @@ -121,11 +138,17 @@ | 后台操作 | 写入 | 操作 | |---|---|---| | 手动增减金币 | `coin_account`(U)+`coin_transaction`(C `admin_grant`/`admin_deduct`)+`admin_audit_log`(C) | 同事务 | -| 改用户状态(禁用/启用) | `user`(U)+`admin_audit_log`(C) | 同事务 | -| 审核通过提现 | `withdraw_order`(U→pending/success/failed)+`wechat_transfer_authorization`(C/U)+失败时`cash_transaction`(refund)+`admin_audit_log`(C) | | -| 审核拒绝提现 | `withdraw_order`(U→rejected)+`cash_transaction`(C `withdraw_refund`)+`admin_audit_log`(C) | | -| 处理反馈 | `feedback`(U)+`admin_audit_log`(C) | 同事务 | -| 改运营配置 | `app_config`(C/U)+`admin_audit_log`(C) | 同事务 | +| 手动增减现金(#95 支持目标账户) | `coin_account`(U 按 `account` 选列)+ 流水(C:`cash_transaction` 或 `invite_cash_transaction`,`admin_grant`/`admin_deduct`)+`admin_audit_log`(C) | 同事务;`account=coin_cash`/`invite_cash` 两本账各调各 | +| 改用户状态(禁用/启用)/ 调试链接权限 | `user`(U `status` / `debug_trace_enabled`)+`admin_audit_log`(C) | 同事务 | +| 审核通过提现(含批量) | `withdraw_order`(U→pending/success/failed)+`wechat_transfer_authorization`(C/U)+失败时按 source 退款流水+`admin_audit_log`(C) | | +| 审核拒绝提现(含批量) | `withdraw_order`(U→rejected)+按 source 退款流水(C `withdraw_refund`/`invite_withdraw_refund`)+`admin_audit_log`(C) | | +| 反馈采纳/拒绝/标记处理 | `feedback`(U→adopted/rejected/handled + `admin_reply`/`reject_reason`)+采纳发币时 `coin_account`(U)+`coin_transaction`(C)+`admin_audit_log`(C) | 同事务(#94/#105) | +| 上报更低价审核 | `price_report`(U→approved/rejected)+通过发币 `coin_account`(U)+`coin_transaction`(C)+`admin_audit_log`(C) | 同事务 | +| 改运营配置 / 广告配置 / 反馈页二维码 | `app_config`(C/U)+`admin_audit_log`(C) | 同事务 | +| 轮播种子/数据源模式 | `ops_marquee_seed`(C/U/D)+`app_config`(模式 mixed/real/seed)+`admin_audit_log`(C) | #122/#123 预览/浏览只读 | +| 角色管理(#117/#126) | `admin_role`(C/U/D)+`admin_audit_log`(C) | 内建/在用角色不可删 | +| 管理员管理 | `admin_user`(C/U/**D** #126)+`admin_audit_log`(C) | 删除为物理删,不可删自己 | +| CPS 建群/活动/生成短链/拉单对账 | `cps_group`/`cps_activity`(C/U/D)、`cps_link`(C)、`cps_order`(C/U upsert;美团 `query_order` + 京东联盟 #90) | 见 CPS 段 | | 任意写操作 | `admin_audit_log`(C,**永不 U/D**) | | > 没有任何表会被业务流程物理 DELETE。注销是软删(改 user 行),其余只 C/U(领券 `/prompt/reset`、`/completed-today/reset` 是开发用删除,非业务流程)。 @@ -137,7 +160,9 @@ | 后台批量生成短链 `POST /admin/api/cps/referral-links` | `cps_link`(C) | 每 群×活动 一条;美团经 `sid` 转链拿 `target_url`(同群同活动重复生成产生多条) | | 用户点群发短链 `GET /c/{code}` / `POST /c/{code}/copy` | `cps_click`(C `visit`/`copy`) | 公开端点不鉴权;`group_id`/`sid` 从 link 冗余进来免 join | | 微信落地页授权回调 `GET /wx/oauth/cb` | `cps_wx_user`(C/U upsert) | 按 `openid` 幂等;base 拿 openid,userinfo 补昵称/unionid(非 None 才覆盖);任何失败兜底回落地页不阻断领券 | -| 定时拉美团联盟订单对账 | `cps_order`(C/U upsert) | `query_order` 按 `sid` 归群;`order_id` 幂等(状态会变,重复拉则更新) | +| 定时拉联盟订单对账(美团 `query_order` + 京东联盟 #90) | `cps_order`(C/U upsert) | 按 `sid` 归群;`order_id` 幂等(状态会变,重复拉则更新);京东单 `platform='jd'` + `jd_valid_code` 判有效 | +| 定时拉穿山甲 GroMore 后台收益(#92,systemd 每天 10:30) | `ad_pangle_daily_revenue`(C/U upsert) | 收益报表/大盘「真实收益」侧;与客户端上报的 eCPM 侧互为对照 | +| 定时 ETL 灌美团 CPS 选品库 | `meituan_coupon`(C/U) | `feed?tab=rec` / `top-sales` 纯库出的数据源 | | pricebot 比价 done 内部上报 `POST /internal/price-observation` | `price_observation`(C 批量) | `(trace_id,platform,scope)` 幂等;**不走 JWT、靠 `X-Internal-Secret`**(未配→503) | | pricebot 比价 done 内部上报 `POST /internal/store-mapping` | `store_mapping`(C/U 填空合并) | `trace_id` 幂等;另有 `lookup` 反查 + `invalidate` deeplink 失效标记(淘宝/京东) | | pricebot LLM 兜底放行启动确认窗后上报 `POST /internal/launch-confirm-sample` | `launch_confirm_sample`(C) | **都上报、不去重**;靠 `X-Internal-Secret`(未配→503) | @@ -150,7 +175,7 @@ ## 三、表间关系 & Join Key ### 硬外键(数据库 FK 约束) -- **18 张用户维度表 `.user_id` → `user.id`**:`coin_account`(同时是 PK)、`coin_transaction`、`cash_transaction`、`withdraw_order`、`wechat_transfer_authorization`(同时是 PK)、`signin_record`、`signin_boost_record`、`user_task`、`comparison_record`、`comparison_milestone_claim`、`savings_record`、`ad_reward_record`、`ad_watch_log`、`ad_ecpm_record`、`ad_feed_reward_record`、`price_report`、`feedback`、`device_liveness`。 +- **19 张用户维度表 `.user_id` → `user.id`**:`coin_account`(同时是 PK)、`coin_transaction`、`cash_transaction`、`invite_cash_transaction`、`withdraw_order`、`wechat_transfer_authorization`(同时是 PK)、`signin_record`、`signin_boost_record`、`user_task`、`comparison_record`(2026-07 起 `user_id` **可空**——harvest 帧0 建行时软鉴权可能拿不到)、`comparison_milestone_claim`、`savings_record`、`ad_reward_record`、`ad_watch_log`、`ad_ecpm_record`、`ad_feed_reward_record`、`price_report`、`feedback`、`device_liveness`。 - `admin_audit_log.admin_id` → `admin_user.id`。 - `price_report.comparison_record_id` → `comparison_record.id`(可空:关联记录被删后仍留上报历史)。 - **邀请两表** → `user.id`:`invite_relation.inviter_user_id`、`invite_relation.invitee_user_id`(唯一)、`invite_fingerprint.inviter_user_id`——注意 FK 列名是 `inviter`/`invitee_user_id`,不是 `user_id`。 @@ -173,13 +198,24 @@ | biz_type | ref_id 指向 | amount 符号 | |---|---|---| - | `withdraw` / `withdraw_refund` | `withdraw_order.out_bill_no` | − / + | + | `withdraw` / `withdraw_refund` | `withdraw_order.out_bill_no`(`source=coin_cash` 的单) | − / + | | `exchange_in` | null | + | + | `admin_grant` / `admin_deduct` | null(原因在 `remark`=`admin:`) | + / − | + +- **`invite_cash_transaction.ref_id`**(邀请奖励金账本,#82): + + | biz_type | ref_id 指向 | amount 符号 | + |---|---|---| + | `invite_reward` | 被邀请人 `user.id`(字符串) | + | + | `invite_withdraw` / `invite_withdraw_refund` | `withdraw_order.out_bill_no`(`source=invite_cash` 的单) | − / + | + | `admin_grant` / `admin_deduct` | null | + / − | - **`comparison_record.store_name` ≈ `savings_record.shop_name`**:无 id 关联,按**店名字符串相等**给比价记录打「已下单」标记(瞬态,不写库)。两边店名同源 = 比价意图识别阶段的门店 query,语义=**店级**(同店比价多次会一并标已下单)。 - **广告流会话关联**:`ad_reward_record.ad_session_id` 可与 `ad_ecpm_record.ad_session_id` 对齐;`ad_watch_log` 仍是旧版兼容统计,不逐条参与发奖。 - **里程碑解锁进度不存库**:`comparison_milestone_claim` 只记「哪几档已领」;进度 = `comparison_record` 里 `status='success'` 的 `count`。 -- **领券三表无硬 FK,全靠软关联**:`coupon_prompt_engagement` / `coupon_daily_completion` / `coupon_claim_record` 的 `user_id` **软指** `user.id`(可空、有登录态才记、不进唯一键、不阻塞判断);`trace_id` **软指** pricebot work_logs(排查回指);唯一键都以 `device_id` + 北京自然日为主(详见 [`coupon_state.md`](./coupon_state.md))。 +- **领券三表无硬 FK,全靠软关联**:`coupon_prompt_engagement` / `coupon_daily_completion` / `coupon_claim_record` 的 `user_id` **软指** `user.id`(可空、有登录态才记、不进唯一键、不阻塞判断);`trace_id` **软指** pricebot work_logs(排查回指);唯一键都以 `device_id` + 北京自然日为主(详见 [`coupon_state.md`](./coupon_state.md))。[`coupon_session`](./coupon_session.md)(#99)同口径:`trace_id` 唯一 upsert、`user_id` 软指(admin 明细 LEFT JOIN 出手机号)。 +- **`analytics_event`**(#83)与 **`coupon_session`** 均无硬 FK:埋点/流水不鉴权也收,`device_id`/`user_id` 只作维度。 +- **`admin_user.role` 按名语义引用 `admin_role.name`**(无硬 FK,#117):删除保护在应用层(在用/内建角色不可删);个人 `pages_override` 优先于角色 `pages`。 - **`onboarding_completion.(user_id, device_id)`**:`user_id` 语义关联 `user.id`(无硬 FK,同 `coupon_*` 设备表),`device_id` = 客户端硬件级 `ANDROID_ID`(≠ 领券 per-install `device_id`)。登录读、走完引导写,决定是否再展示新手引导。 - **CPS 群发 6 表全靠 `sid` / id / `openid` 语义串联(无硬 FK)**:`cps_link.group_id`→`cps_group.id`、`cps_link.activity_id`→`cps_activity.id`、`cps_click.link_id`→`cps_link.id`、`cps_wx_user.first_group_id`→`cps_group.id`;**点击与订单无法对到单笔**,只在群维度(`sid`)汇合——`cps_order.sid` ≈ `cps_group.sid` ≈ `cps_link.sid` ≈ `cps_click.sid`(仅美团有 sid,淘宝/京东无)。统计按群聚合,故 `group_id`/`sid` 冗余进 `cps_click` 免 join。`cps_wx_user` 按 `openid` 自成用户身份维度,与 `cps_click`/`cps_order` 无 id 级 join。 - **比价沉淀两表(`price_observation` / `store_mapping`)**:`trace_id` 软指 pricebot work_logs(与 `comparison_record.trace_id` 同源但不互 join,各存各视角);`source_user_id` / `source_device_id` 软指用户/设备(可空,匿名也记)。`store_mapping.lookup` 靠**店名字符串精确相等** + geo 取最近,非 id 级 join。 @@ -188,10 +224,10 @@ ``` user ─1:1─ coin_account user ─1:1─ wechat_transfer_authorization -user ─1:N─ { coin_transaction, cash_transaction, withdraw_order, signin_record, - signin_boost_record, user_task, comparison_record, comparison_milestone_claim, - savings_record, ad_reward_record, ad_watch_log, ad_ecpm_record, ad_feed_reward_record, - price_report, feedback, device_liveness } +user ─1:N─ { coin_transaction, cash_transaction, invite_cash_transaction, withdraw_order, + signin_record, signin_boost_record, user_task, comparison_record(user_id 可空), + comparison_milestone_claim, savings_record, ad_reward_record, ad_watch_log, + ad_ecpm_record, ad_feed_reward_record, price_report, feedback, device_liveness } (device_liveness 硬 FK; (user_id,device_id) 唯一) user ─1:N─ onboarding_completion (user_id, 无硬 FK; (user_id,device_id) 去重) comparison_record ─1:N─ price_report (comparison_record_id, 可空) @@ -199,6 +235,11 @@ admin_user ─1:N─ admin_audit_log app_config (独立, 无外键, key 为主键) coupon_prompt_engagement / coupon_daily_completion / coupon_claim_record (独立, 无硬 FK; 维度=device_id+北京日, user_id/trace_id 仅软关联) +coupon_session (独立, 无硬 FK; trace_id 唯一=一次领券任务一行, #99) +analytics_event (独立, 无硬 FK; 埋点事件流, device/user 仅维度, #83) +admin_role ◀──语义(role 按 name 引用, 无FK)── admin_user (pages_override 个人覆盖, #117/#126) +ops_stat_config / ops_marquee_seed / meituan_coupon / ad_pangle_daily_revenue + (独立运营/缓存/对账表, 无外键) user ─1:N─ invite_relation (inviter_user_id 硬 FK); invitee_user_id ─1:1─ user (唯一硬 FK) user ─1:N─ invite_fingerprint (inviter_user_id 硬 FK) cps_activity / cps_group ──语义(无FK)──▶ cps_link ─1:N─ cps_click @@ -212,13 +253,13 @@ launch_confirm_sample (独立, 无硬 FK; 都上报不去 ## 四、资金模型(金币 / 现金 / 提现,三层) -1. **余额快照** `coin_account`:`coin_balance`(金币个数)+ `cash_balance_cents`(现金分),一用户一行,读取展示用。 -2. **流水账本** `coin_transaction` / `cash_transaction`:每次变动写一笔,`balance_after*` 记变动后余额,可逐笔回溯对账。 -3. **唯一发金币入口** `repositories/wallet.grant_coins`:更新快照 + 写流水,**不 commit**,由调用方在同一事务里 commit(保证"记录"和"加币"原子化)。signin / signin_boost / task / ad_reward / feed_ad_reward / exchange / admin 都走它,靠 `biz_type` 区分来源。 +1. **余额快照** `coin_account`:`coin_balance`(金币个数)+ `cash_balance_cents`(现金分)+ `invite_cash_balance_cents`(邀请奖励金分,#82),一用户一行,读取展示用。 +2. **流水账本** `coin_transaction` / `cash_transaction` / `invite_cash_transaction`:每次变动写一笔,`balance_after*` 记变动后余额,可逐笔回溯对账。**现金与邀请奖励金是两本物理隔离的账**——发放口径与提现对账各自独立。 +3. **唯一变动入口**:金币走 `repositories/wallet.grant_coins`,邀请奖励金走 `grant_invite_cash`——都是「更新快照 + 写流水,**不 commit**,由调用方同一事务 commit」。signin / signin_boost / task / ad_reward / feed_ad_reward / exchange / admin 走 `grant_coins`;`invite_reward` / admin 调整走 `grant_invite_cash`,靠 `biz_type` 区分来源。 - **汇率**:`10000 金币 = 1 元 = 100 分`(`rewards.COIN_PER_YUAN`);兑换额必须是整分倍数。 -- **提现状态机**:`reviewing`(发起即原子扣现金、待人工审核、**不打款**)→ 审核通过 `pending`(微信转账在途)→ `success` / `failed`(失败自动退款);审核拒绝 `rejected`(退款)。扣款/退款都写 `cash_transaction`,`out_bill_no` 幂等,孤儿 pending 单由 `reconcile_pending_withdraws` 对账兜底。 -- **防超额**:扣现金用带条件 `UPDATE ... WHERE cash_balance_cents >= amount`,并发/重试不会双扣。 +- **提现状态机**:`reviewing`(发起即原子扣款、待人工审核、**不打款**)→ 审核通过 `pending`(微信转账在途)→ `success` / `failed`(失败自动退款);审核拒绝 `rejected`(退款)。**按 `withdraw_order.source` 分账**(#121):`coin_cash` 单的扣款/退款写 `cash_transaction`,`invite_cash` 单写 `invite_cash_transaction`;`out_bill_no` 幂等,孤儿 pending 单由 `reconcile_pending_withdraws` 对账兜底,admin `withdraws/ledger-check` 分账校验「单 ↔ 流水」。 +- **防超额**:扣款用带条件 `UPDATE ... WHERE <对应余额列> >= amount`(按 source 选 `cash_balance_cents` / `invite_cash_balance_cents`),并发/重试不会双扣。 --- diff --git a/docs/database/README.md b/docs/database/README.md index bd4c0d4..e173694 100644 --- a/docs/database/README.md +++ b/docs/database/README.md @@ -3,13 +3,13 @@ > 数据库:SQLite 起步(`data/app.db`),生产可切 PostgreSQL(改 `DATABASE_URL`)。 > ORM:SQLAlchemy 2.0(`app/models/`),迁移:Alembic(`alembic/versions/`,`render_as_batch` 兼容 SQLite)。 > 金额字段一律存**整数**:金币=个数,现金=**分**(`*_cents`)。时间列 `DateTime(timezone=True)`。 -> 最后更新:2026-06-23(补 3 张表文档:`device_liveness`(#65 无障碍存活监控)、`cps_wx_user`(CPS 落地页微信身份)、`launch_confirm_sample`(启动确认窗兜底样本);`comparison_record` 补 `input_tokens`/`output_tokens`。上一次 2026-06-17 补全 CPS 群发 5 张 `cps_*` + 好友邀请 2 张 `invite_*` + 比价沉淀 `price_observation`/`store_mapping` 并全表 review 对齐 model;含 [OVERVIEW 总览](./OVERVIEW.md)) +> 最后更新:2026-07-09(补 5 张新表文档并入索引:`coupon_session`(#99 领券任务流水)、`analytics_event`(#83 埋点)、`invite_cash_transaction`(#82 邀请奖励金账本)、`admin_role`(#117/#126 RBAC 角色)、`ad_pangle_daily_revenue`(#92,文档已有、补进索引);同步改动列:`comparison_record.product_names`、`withdraw_order.source`、`coin_account.invite_cash_balance_cents`、`feedback` 审核/环境列、`cps_order` 京东列、`device_liveness.first_protected_at`、`ad_feed_reward_record.ad_type/feed_scene`、`admin_user.plain_password/pages_override`。上一次 2026-06-23) > 🧭 **先看 [OVERVIEW.md — 表 × 功能 × 关系](./OVERVIEW.md)**:跨表的「每块功能用哪些表 / 什么操作写哪张表 / 表间 join key」都在那;本页只做**单表索引**,点进每张表的详情看字段级说明。 --- -## 表总览(40 张业务表 + `alembic_version` 框架表) +## 表总览(45 张业务表 + `alembic_version` 框架表) ### 账号 / 反馈 | 表 | 用途 | 模型 | 文档 | @@ -22,7 +22,7 @@ ### 好友邀请(注册增长) | 表 | 用途 | 模型 | 文档 | |---|---|---|---| -| `invite_relation` | 邀请绑定关系(注册即生效,双方各发1万金币;`invitee_user_id` 唯一=幂等防重复发奖) | `models/invite.py` | [详情](./invite_relation.md) | +| `invite_relation` | 邀请绑定关系(注册即生效但**绑定不发奖** #113;好友比价并下单后发邀请奖励金,`compare_reward_granted` 幂等闸;`invitee_user_id` 唯一) | `models/invite.py` | [详情](./invite_relation.md) | | `invite_fingerprint` | 剪贴板归因失败时的指纹兜底(落地页记 ip+屏幕+机型,登录后反查邀请人) | `models/invite_fingerprint.py` | [详情](./invite_fingerprint.md) | ### 钱包 / 福利(看广告赚钱闭环) @@ -30,8 +30,9 @@ |---|---|---|---| | `coin_account` | 金币+现金余额快照(一用户一行) | `models/wallet.py` | [详情](./coin_account.md) | | `coin_transaction` | 金币流水账本 | `models/wallet.py` | [详情](./coin_transaction.md) | -| `cash_transaction` | 现金流水账本(分) | `models/wallet.py` | [详情](./cash_transaction.md) | -| `withdraw_order` | 提现单(现金→微信零钱,含人工审核态) | `models/wallet.py` | [详情](./withdraw_order.md) | +| `cash_transaction` | 现金流水账本(分,金币兑换账) | `models/wallet.py` | [详情](./cash_transaction.md) | +| `invite_cash_transaction` | 邀请奖励金流水账本(分,与现金物理隔离;好友比价并下单发奖 + `source=invite_cash` 提现) | `models/wallet.py` | [详情](./invite_cash_transaction.md) | +| `withdraw_order` | 提现单(现金→微信零钱,含人工审核态;`source` 分账 coin_cash/invite_cash) | `models/wallet.py` | [详情](./withdraw_order.md) | | `wechat_transfer_authorization` | 微信免确认转账授权(一用户一行) | `models/wallet.py` | [详情](./wechat_transfer_authorization.md) | | `signin_record` | 签到记录(7 天循环) | `models/signin.py` | [详情](./signin_record.md) | | `signin_boost_record` | 签到后看广告膨胀记录 | `models/signin.py` | [详情](./signin_boost_record.md) | @@ -39,7 +40,8 @@ | `ad_reward_record` | 看激励视频发奖记录(S2S 回调,trans_id 幂等) | `models/ad_reward.py` | [详情](./ad_reward_record.md) | | `ad_watch_log` | 看广告观看时长(旧版兼容字段) | `models/ad_watch_log.py` | [详情](./ad_watch_log.md) | | `ad_ecpm_record` | 广告展示 eCPM 上报(收益对账) | `models/ad_ecpm.py` | [详情](./ad_ecpm_record.md) | -| `ad_feed_reward_record` | 信息流广告结算记录(10 秒一份,client_event_id 幂等) | `models/ad_feed_reward.py` | [详情](./ad_feed_reward_record.md) | +| `ad_feed_reward_record` | 信息流/Draw 广告结算记录(10 秒一份,client_event_id 幂等;`ad_type`+`feed_scene` 分形态/场景) | `models/ad_feed_reward.py` | [详情](./ad_feed_reward_record.md) | +| `ad_pangle_daily_revenue` | 穿山甲 GroMore 后台收益日表(定时拉取,收益报表/大盘真实收益源,#92) | `models/ad_pangle_revenue.py` | [详情](./ad_pangle_daily_revenue.md) | ### 比价 / 省钱 | 表 | 用途 | 模型 | 文档 | @@ -62,6 +64,7 @@ | `coupon_prompt_engagement` | 领券引导窗频控源(今日是否已 engage,按 device+package+日) | `models/coupon_state.py` | [详情](./coupon_state.md) | | `coupon_daily_completion` | 首页「去领取」置灰源(今日是否已跑完整轮) | `models/coupon_state.py` | [详情](./coupon_state.md) | | `coupon_claim_record` | 每张券领取结果沉淀(资产/画像/排查,不参与判断) | `models/coupon_state.py` | [详情](./coupon_state.md) | +| `coupon_session` | 一次领券任务一行的全程流水(发起/终态/耗时/机型,admin 领券看板数据源,#99) | `models/coupon_state.py` | [详情](./coupon_session.md) | ### 美团 CPS 券缓存 | 表 | 用途 | 模型 | 文档 | @@ -84,10 +87,16 @@ | `ops_stat_config` | 首页三统计展示配置(real/manual/random) | `models/ops_stat_config.py` | [详情](./ops_stat_config.md) | | `ops_marquee_seed` | 首页轮播种子(真实不足时兜底混播) | `models/ops_marquee_seed.py` | [详情](./ops_marquee_seed.md) | +### 埋点 +| 表 | 用途 | 模型 | 文档 | +|---|---|---|---| +| `analytics_event` | 客户端埋点事件流(批量上报,一事件一行;admin 埋点日志检索,#83) | `models/analytics_event.py` | [详情](./analytics_event.md) | + ### 运营后台 admin(独立子应用 `app/admin/`,独立鉴权) | 表 | 用途 | 模型 | 文档 | |---|---|---|---| -| `admin_user` | 管理员账号(独立 JWT + RBAC) | `models/admin.py` | [详情](./admin_user.md) | +| `admin_user` | 管理员账号(独立 JWT + RBAC;`pages_override` 个人可见页覆盖) | `models/admin.py` | [详情](./admin_user.md) | +| `admin_role` | 后台角色→可见页配置(内建三角色 + 自定义角色,#117/#126) | `models/admin_role.py` | [详情](./admin_role.md) | | `admin_audit_log` | 操作审计日志(只追加) | `models/admin.py` | [详情](./admin_audit_log.md) | | `app_config` | 运营可配置项(覆盖 rewards 常量) | `models/app_config.py` | [详情](./app_config.md) | diff --git a/docs/database/ad_feed_reward_record.md b/docs/database/ad_feed_reward_record.md index f6edbfb..41b721c 100644 --- a/docs/database/ad_feed_reward_record.md +++ b/docs/database/ad_feed_reward_record.md @@ -11,6 +11,8 @@ | `id` | Integer | PK | 自增主键 | | `client_event_id` | String(64) | UNIQUE, NOT NULL | 客户端幂等事件 id | | `ad_session_id` | String(64) | index, nullable | 客户端生成的一次信息流广告会话 id | +| `ad_type` | String(16) | nullable, default `feed` | 广告形态:`feed`(信息流)/ `draw`(Draw 视频流)。旧数据 NULL 视为 feed(迁移随 #83 入库) | +| `feed_scene` | String(16) | nullable | 展示场景:`compare`(比价期)/ `coupon`(领券期)。比价与领券共用同一 Draw 代码位,收益/大盘按它分场景归集(#125 修比价/领券奖励金币恒 0 即改按本列汇总) | | `user_id` | Integer | FK → `user.id`, index, NOT NULL | 用户 | | `reward_date` | String(10) | index, NOT NULL | 北京时间日期 `YYYY-MM-DD` | | `duration_seconds` | Integer | NOT NULL | 整场比价累计观看秒数(轮播各条相加) | diff --git a/docs/database/admin_role.md b/docs/database/admin_role.md new file mode 100644 index 0000000..71b49ee --- /dev/null +++ b/docs/database/admin_role.md @@ -0,0 +1,32 @@ +# admin_role — 运营后台角色(RBAC 可见页配置) + +> 模型 `app/models/admin_role.py` · 仓库 `app/admin/repositories/admin_role.py` · 权限目录 `app/admin/permissions.py` · 接口 admin `GET/POST /admin/api/roles`、`PATCH/DELETE /admin/api/roles/{id}`、`GET /admin/api/roles/catalog`(`app/admin/routers/roles.py`) · [← 索引](./README.md) · [总览](./OVERVIEW.md) + +运营后台的**角色 → 可见页面**配置表。#117 把 RBAC 从「代码写死三角色」升级为数据驱动:内建角色(`super_admin`/`finance`/`operator`)种子进表(`is_builtin=true`,不可删),`super_admin` 可另建**自定义角色**(#126)勾选任意页面组合。`admin_user.role` 存角色 `name` 按名引用本表;单个管理员还可用 `admin_user.pages_override` 在角色之上覆盖个人可见页。 + +## 用在哪 / 增删改查 +- **C(插入)**:内建三角色随迁移种子;`POST /admin/api/roles`(super_admin)新建自定义角色。 +- **U(更新)**:`PATCH /admin/api/roles/{id}` 改展示名/可见页(内建角色的 `pages` 也可调)。 +- **D(删除)**:`DELETE /admin/api/roles/{id}`——**内建角色与在用角色(有 admin_user.role 引用)不可删**。 +- **R**:登录/每次 admin 鉴权时按 `admin_user.role` 查本表算有效可见页(`pages_override` 非空则以覆盖为准);`GET /admin/api/roles/catalog` 返回全部可配页面目录(分组,来自 `permissions.py` 常量,不落库)。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `name` | String(32) | UNIQUE, index, NOT NULL | 角色标识(被 `admin_user.role` 按名引用):内建 `super_admin`/`finance`/`operator`;**自定义角色 name(key)= label = 创建时的输入名称**(不能叫 super_admin,重名 409) | +| `label` | String(32) | NOT NULL, default `""` | 展示名(后台下拉里显示) | +| `pages` | JSON | NOT NULL, default `[]` | 可见页面 key 列表(全集见 `permissions.py` 目录;前端按它渲染菜单,后端接口守卫同源校验)。**`super_admin` 行 pages 存空数组**,有效可见页特判为全部 | +| `is_builtin` | Boolean | NOT NULL, default false | 内建角色标记(不可删;`super_admin` 恒过所有守卫,不依赖 pages) | +| `created_at` | DateTime(tz) | server_default now() | | + +## 关系 / Join Key +- ← `admin_user.role` **按 `name` 语义引用**(无硬 FK;删除保护在应用层:在用角色不可删)。 +- 与 `admin_user.pages_override` 的关系:有效可见页 = `pages_override`(个人覆盖,非空优先)∪ 否则取角色 `pages`;`super_admin` 无视两者恒全通。 + +## 索引与约束 +- PK `id`;UNIQUE+index `name`。 + +## 注意 +- 页面 key 目录维护在代码 `app/admin/permissions.py`(加新后台页面要同步登记),表里只存勾选结果——目录变更不需要迁移。 +- 角色守卫兼容旧语义:`require_role("finance")` 等旧代码路径仍工作,内建角色名不可改。 diff --git a/docs/database/admin_user.md b/docs/database/admin_user.md index 7e21f68..7690bbe 100644 --- a/docs/database/admin_user.md +++ b/docs/database/admin_user.md @@ -1,13 +1,13 @@ # admin_user — 运营后台管理员账号 -> 模型 `app/models/admin.py` · 仓库 `app/admin/repositories/admin_user.py` · 接口 [admin-auth-login](../api/admin-auth-login.md) / [admin-admins-list](../api/admin-admins-list.md) / [admin-admin-create](../api/admin-admin-create.md) / [admin-admin-update](../api/admin-admin-update.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md) +> 模型 `app/models/admin.py` · 仓库 `app/admin/repositories/admin_user.py` · 接口 [admin-auth-login](../api/admin/auth/admin-auth-login.md) / [admin-admins-list](../api/admin/admins/admin-admins-list.md) / [admin-admin-create](../api/admin/admins/admin-admin-create.md) / [admin-admin-update](../api/admin/admins/admin-admin-update.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md) 运营后台(`app/admin/` 子应用,端口 8771)的管理员账号,与 App 用户(`user` 表)**完全隔离**:独立 JWT secret、独立鉴权链。密码 bcrypt 存哈希,带角色做 RBAC 权限分级。 ## 用在哪 / 增删改查 - **C(插入)**:① 首个管理员用 `scripts/create_admin.py` 命令行创建(无自助注册);② `super_admin` 在后台「管理员管理」`POST` 新建子管理员。 -- **U(更新)**:登录成功刷 `last_login_at`;`super_admin` 改他人 `role`/`status`/重置密码(`admin-admin-update`)。 -- **D**:无(禁用走 `status='disabled'`,token 立即失效)。 +- **U(更新)**:登录成功刷 `last_login_at`;`super_admin` 改他人 `role`/`status`/重置密码/`pages_override`(`admin-admin-update`)。 +- **D**:`DELETE /admin/api/admins/{id}`(#126,super_admin,带审计;不可删自己)。禁用仍走 `status='disabled'`(token 立即失效)。 - **R**:每个 admin 请求经 `admin/deps` 解 admin token 查本表(校验 `status=='active'` + 角色守卫);管理员列表。 ## 字段 @@ -15,8 +15,10 @@ |---|---|---|---| | `id` | Integer | PK, autoincrement | 被 `admin_audit_log.admin_id` 引用 | | `username` | String(64) | UNIQUE, index, NOT NULL | 登录名 | -| `password_hash` | String(255) | NOT NULL | bcrypt 哈希(明文不落库;⚠️ bcrypt 72 字节截断) | -| `role` | String(20) | NOT NULL, default `operator` | 取值:`super_admin`(全权+管账号)/ `finance`(钱:提现+金币)/ `operator`(用户+反馈+大盘) | +| `password_hash` | String(255) | NOT NULL | bcrypt 哈希(⚠️ bcrypt 72 字节截断) | +| `plain_password` | String(128) | nullable | **明文密码副本**(#117,`super_admin` 在管理员列表可见,便于线下派发/找回;创建/重置密码时同步写)。安全上是有意取舍:后台仅内网+super_admin 可见 | +| `role` | String(20) | NOT NULL, default `operator` | 角色名,按 `name` 引用 [`admin_role`](./admin_role.md)(#117 起数据驱动):内建 `super_admin`(恒全权)/ `finance` / `operator`,或自定义角色(#126) | +| `pages_override` | JSON | nullable | **个人可见页覆盖**(#126):非空时优先于角色 `pages`;NULL=跟随角色。页面 key 目录见 `app/admin/permissions.py` | | `status` | String(20) | NOT NULL, default `active` | 取值:`active` / `disabled`(禁用后 token 立即失效) | | `created_at` | DateTime(tz) | server_default now(), NOT NULL | 创建时间 | | `last_login_at` | DateTime(tz) | nullable | 最近登录时间(登录成功时更新) | @@ -30,4 +32,4 @@ ## 注意 - **鉴权隔离**:admin token `typ=admin` + 独立 `ADMIN_JWT_SECRET`(≠ App 的 `JWT_SECRET_KEY`),App 用户 token 无法当 admin 用;admin 无 refresh,过期(默认 12h)重登。 -- **RBAC**:`super_admin` 恒过所有角色守卫(`require_role`);`finance` 管钱、`operator` 管用户/反馈/大盘。具体守卫见各接口文档。 +- **RBAC**:`super_admin` 恒过所有角色守卫(`require_role`);`finance` 管钱、`operator` 管用户/反馈/大盘;#117 起可见页由 [`admin_role`](./admin_role.md)`.pages` 数据驱动 + `pages_override` 个人覆盖,自定义角色见 `POST /admin/api/roles`。具体守卫见各接口文档。 diff --git a/docs/database/analytics_event.md b/docs/database/analytics_event.md new file mode 100644 index 0000000..8583d58 --- /dev/null +++ b/docs/database/analytics_event.md @@ -0,0 +1,39 @@ +# analytics_event — 客户端埋点事件流 + +> 模型 `app/models/analytics_event.py` · 仓库 `app/repositories/analytics.py` · 接口 C 端 `POST /api/v1/analytics/events`([analytics-events](../api/other/analytics-events.md),批量,不强制登录);admin `GET /admin/api/event-logs`(`app/admin/routers/event_logs.py`) · [← 索引](./README.md) · [总览](./OVERVIEW.md) + +客户端行为埋点的落地表,**一事件一行**(append-only)。客户端攒批上报(每批 ≤200 条),服务端展开逐条插入;`props` 装事件自带的任意维度。当前主要供 admin「埋点日志」检索与手工分析,无自动聚合任务。#83 新增(2026-06-27)。 + +## 用在哪 / 增删改查 +- **C(插入)**:`POST /api/v1/analytics/events`(不强制登录:带 JWT 则记 `user_id`,匿名只记 `device_id`)。服务端补 `client_ip`。 +- **U / D**:无。只追加。 +- **R**:admin `GET /admin/api/event-logs`(游标分页,可按 `event`/`device_id`/`user_id` 筛)。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `event` | String(64) | index, NOT NULL | 事件名(客户端约定,如页面曝光/点击类事件名) | +| `props` | JSON | nullable | 事件属性(任意维度,客户端原样传) | +| `device_id` | String(64) | index, NOT NULL | 客户端 per-install id(匿名也有) | +| `user_id` | Integer | index, nullable | 登录态才有。**无硬 FK** | +| `session_id` | String(64) | index, nullable | 客户端会话 id(一次冷启动一个) | +| `client_ts` | BigInteger | NOT NULL | 事件发生时间(客户端 epoch ms) | +| `sent_at` | BigInteger | nullable | 客户端上报时间(epoch ms);与 `client_ts` 差 = 攒批延迟 | +| `page` | String(64) | nullable | 事件所在页面 | +| `client_ip` | String(64) | nullable | 服务端从请求头取 | +| `oem` / `os` / `model` | String | nullable | 厂商 / 系统版本 / 机型 | +| `app_ver` | String(32) | nullable | App versionName | +| `network` | String(16) | nullable | 网络类型(wifi/cellular) | +| `channel` | String(32) | nullable | 分发渠道 | +| `created_at` | DateTime(tz) | server_default now() | 入库时间 | + +## 关系 / Join Key +- `user_id` 软指 `user.id`、`device_id` 与其他表的 per-install id 同源——均无硬 FK(埋点不鉴权、匿名也收,不能被外键约束卡住)。 + +## 索引与约束 +- PK `id`;index `event`、`device_id`、`user_id`、`session_id`。 + +## 注意 +- 每批最多 200 条,超出整批 422;单条字段超长按模型截断口径处理。 +- 时间轴分析用 `client_ts`(事件真实发生时刻),`created_at` 只是入库时刻(受攒批影响)。 diff --git a/docs/database/cash_transaction.md b/docs/database/cash_transaction.md index f782184..93566a6 100644 --- a/docs/database/cash_transaction.md +++ b/docs/database/cash_transaction.md @@ -1,17 +1,20 @@ # cash_transaction — 现金流水账本(分) -> 模型 `app/models/wallet.py` · 仓库 `app/repositories/wallet.py` · 接口 [wallet-cash-transactions](../api/wallet-cash-transactions.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md) +> 模型 `app/models/wallet.py` · 仓库 `app/repositories/wallet.py` · 接口 [wallet-cash-transactions](../api/wallet/wallet-cash-transactions.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md) 现金每变动一笔就记一行(单位:**分**,记变动后余额)。金币兑现金、提现、提现退款都落这里。**只增不改不删**。 +> **只管「金币兑换现金」这本账**(`coin_account.cash_balance_cents`)。**邀请奖励金**是另一本物理隔离的账 → [`invite_cash_transaction`](./invite_cash_transaction.md)(同构表);`source=invite_cash` 的提现流水**不落本表**。 + ## 用在哪 / 增删改查 -- **C(插入)**:三个来源,每次写一笔: +- **C(插入)**:四个来源,每次写一笔: | 动作 / endpoint | `biz_type` | `amount_cents` | `ref_id` 指向 | |---|---|---|---| | 金币兑现金 `POST /wallet/exchange` | `exchange_in` | + | null(配套 `coin_transaction.exchange_out`) | - | 发起提现 `POST /wallet/withdraw` | `withdraw` | −(扣现金) | `withdraw_order.out_bill_no` | + | 发起提现 `POST /wallet/withdraw`(`source=coin_cash`) | `withdraw` | −(扣现金) | `withdraw_order.out_bill_no` | | 提现失败/取消/审核拒绝退款 | `withdraw_refund` | +(退回) | `withdraw_order.out_bill_no` | + | admin 手动调整 `POST /admin/api/users/{id}/cash`(`account=coin_cash`) | `admin_grant` / `admin_deduct` | + / − | null(原因记 `remark`=`admin:`) | - **U / D**:无。账本只追加。 - **R**:`GET /wallet/cash-transactions`(现金明细,`id` 倒序游标);admin 跨用户现金流水。 diff --git a/docs/database/coin_account.md b/docs/database/coin_account.md index 7401abd..a2fd0b7 100644 --- a/docs/database/coin_account.md +++ b/docs/database/coin_account.md @@ -1,6 +1,6 @@ # coin_account — 金币 + 现金余额快照 -> 模型 `app/models/wallet.py` · 仓库 `app/repositories/wallet.py` · 接口 [wallet-account](../api/wallet-account.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md) +> 模型 `app/models/wallet.py` · 仓库 `app/repositories/wallet.py` · 接口 [wallet-account](../api/wallet/wallet-account.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md) 一用户一行的余额快照,App「资产卡 / 钱包」读它展示。每次余额变动都另写一笔流水(`coin_transaction` / `cash_transaction`)并记 `balance_after`,出问题逐笔回溯。`user_id` 既是主键也是外键(一对一)。详见 [总览 §四 资金模型](./OVERVIEW.md#四资金模型金币--现金--提现三层)。 @@ -15,7 +15,8 @@ |---|---|---|---| | `user_id` | Integer | **PK + FK→user.id** | 用户(一用户一行);既是主键也是外键 | | `coin_balance` | Integer | NOT NULL, default 0 | 当前金币余额(个数);= 历次 `coin_transaction.amount` 之和 | -| `cash_balance_cents` | Integer | NOT NULL, default 0 | 当前现金余额(分);= 历次 `cash_transaction.amount_cents` 之和 | +| `cash_balance_cents` | Integer | NOT NULL, default 0 | 当前现金余额(分,**金币兑换账本**);= 历次 `cash_transaction.amount_cents` 之和 | +| `invite_cash_balance_cents` | Integer | NOT NULL, default 0 | 当前**邀请奖励金**余额(分,与现金物理隔离的第二本现金账);= 历次 [`invite_cash_transaction`](./invite_cash_transaction.md)`.amount_cents` 之和。好友比价并下单发奖入账(#113),`source=invite_cash` 提现出账(#121) | | `total_coin_earned` | Integer | NOT NULL, default 0 | 累计赚取金币(**只增不减**,仅正向 grant 累加),用于"历史总收益"展示 | | `updated_at` | DateTime(tz) | server_default now(), onupdate now() | 最后更新时间 | @@ -27,5 +28,5 @@ - PK `user_id`(同时是 FK→user.id)。 ## 注意 -- **唯一发金币入口** `wallet.grant_coins`:更新本表 + 写 `coin_transaction`,**不 commit**,由调用方同事务提交(发币与业务记录原子化)。 -- 扣现金用 `UPDATE ... WHERE cash_balance_cents >= amount` 原子条件扣减,并发/重试不会超额。 +- **唯一发金币入口** `wallet.grant_coins`:更新本表 + 写 `coin_transaction`,**不 commit**,由调用方同事务提交(发币与业务记录原子化)。邀请奖励金同款:**唯一变动入口 `wallet.grant_invite_cash`**(更新 `invite_cash_balance_cents` + 写 `invite_cash_transaction`)。 +- 扣现金用 `UPDATE ... WHERE <对应余额列> >= amount` 原子条件扣减(按提现 `source` 选 `cash_balance_cents` 或 `invite_cash_balance_cents`),并发/重试不会超额。 diff --git a/docs/database/comparison_record.md b/docs/database/comparison_record.md index 225f29e..5ea3064 100644 --- a/docs/database/comparison_record.md +++ b/docs/database/comparison_record.md @@ -1,6 +1,6 @@ # comparison_record — 比价记录(每次比价完整明细) -> 模型 `app/models/comparison.py` · 仓库 `app/repositories/comparison.py` · 接口 [compare-record-report](../api/compare-record-report.md) / [compare-records](../api/compare-records.md) / [compare-record-detail](../api/compare-record-detail.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md) +> 模型 `app/models/comparison.py` · 仓库 `app/repositories/comparison.py` · 接口 [compare-record-report](../api/compare/compare-record-report.md) / [compare-records](../api/compare/compare-records.md) / [compare-record-detail](../api/compare/compare-record-detail.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md) 每完成一次比价(外卖/电商/领券)记一行。**写入以 app-server 后端 harvest 为主**(2026-07 起):比价透传壳 `compare.py` 在帧0(pricebot 出 trace_id)即建 `running` 行,随 done / `trace/finalize` 逐步补全成终态,客户端不再主动 POST 记录;老客户端仍可走**带 JWT** 的 `POST /compare/record` 兜底(灰度期两条写路径按 `trace_id` reconcile)。App「我的比价记录」列表/详情的数据源,也是比价战绩里程碑解锁进度的计数源(`status='success'` 条数),还被「上报更低价」反查原最低价。 @@ -8,9 +8,9 @@ > 与 [`price_observation`](./price_observation.md) / `store_mapping` 的区别:本表是**用户视角**(登录后按 `user_id` 存「我的比价记录」);后两张是 server 侧无条件沉淀的**平台/门店视角客观事实**(价格事实 / 跨平台店铺身份映射),与本表 `trace_id` 同源但不互相 join,各存各的视角。 ## 用在哪 / 增删改查 -- **C / U(harvest 为主,按 `trace_id` 幂等)**:透传壳 `compare.py` 三段式落库(`app/repositories/comparison.py`)——`harvest_running`(帧0 建 `running` 行)→ `harvest_done`(done 帧转 `success`/`failed` + 派生 `best_*`/`saved_amount_cents` + 返 `newly_success` 供幂等发邀请奖)→ `harvest_abort`(`trace/finalize` 转 `cancelled`/`failed`,**不降级已 success**)。老客户端仍可 `POST /compare/record`(`upsert_record`,按 `trace_id` 查、整行覆盖)兜底。`best_*`/`saved_amount_cents`/`is_source_best`/`status` 一律由 `_derive` 从 `comparison_results` 算出(协议已按 price 升序、rank=1 最便宜),不信客户端自算。 +- **C / U(harvest 为主,按 `trace_id` 幂等)**:透传壳 `compare.py` 三段式落库(`app/repositories/comparison.py`)——`harvest_running`(帧0 建 `running` 行)→ `harvest_done`(done 帧转 `success`/`failed` + 派生 `best_*`/`saved_amount_cents`;`newly_success` 仅留日志观测,**不在此发邀请奖**——#113 已把发奖口径移到「实际下单」`POST /order/report`)→ `harvest_abort`(`trace/finalize` 转 `cancelled`/`failed`,**不降级已 success**)。老客户端仍可 `POST /compare/record`(`upsert_record`,按 `trace_id` 查、整行覆盖)兜底。`best_*`/`saved_amount_cents`/`is_source_best`/`status` 一律由 `_derive` 从 `comparison_results` 算出(协议已按 price 升序、rank=1 最便宜),不信客户端自算。 - **D**:无(关联的 `price_report` 也只把 `comparison_record_id` 置空,不删本表)。 -- **R**:`GET /compare/records`(列表,`created_at` 倒序游标 + 「已下单」标记)、`GET /compare/records/{id}`(详情,限本人);`count_success` 给里程碑;`get_stats`(`status='success'` 计数 + `saved_amount_cents` 求和)给 [`GET /compare/stats`](../api/compare-stats.md) 喂「我的」页省钱战绩卡(完成比价 + 累计发现可省,**比价口径**);`report.py` 反查 `best_*`;admin 大盘/明细。 +- **R**:`GET /compare/records`(列表,`created_at` 倒序游标 + 「已下单」标记)、`GET /compare/records/{id}`(详情,限本人);`count_success` 给里程碑;`get_stats`(`status='success'` 计数 + `saved_amount_cents` 求和)给 [`GET /compare/stats`](../api/compare/compare-stats.md) 喂「我的」页省钱战绩卡(完成比价 + 累计发现可省,**比价口径**);`report.py` 反查 `best_*`;admin 大盘/明细。 ## 字段 | 列 | 类型 | 约束 / 默认 | 说明(取值 / join) | @@ -30,6 +30,7 @@ | `saved_amount_cents` | Integer | nullable | 源价 − 最优价(可 0/负:源平台本就最便宜) | | `is_source_best` | Boolean | nullable | 源平台就是最便宜(= 这次没省到) | | `store_name` | String(128) | nullable | 店铺名。**与 `savings_record.shop_name` 按字符串相等关联**,给本记录打「已下单」 | +| `product_names` | String(512) | nullable | 菜品/商品名拼接串(`items[].name` 顿号连接,超长截断),**专供 admin 比价记录按店/商品模糊搜索**(#117:对 JSON 列做 LIKE 不可移植,冗余成扁平列;写入时随 harvest/upsert 同步生成)。C 端不读它 | | `total_dish_count` / `skipped_dish_count` | Integer | nullable | 菜品总数 / 目标平台没找到被跳过数 | | `status` | String(16) | NOT NULL, default `success` | 取值:`running`(harvest 帧0 建行、比价进行中)/ `success`(有非源且有价的目标结果)/ `failed`(出错/没采到目标价)/ `cancelled`(用户终止 / Phase1 未识别,`harvest_abort` 写,**不降级已 success**)。**里程碑只数 success** | | `information` | String(256) | nullable | done 帧文案;成功=摘要,失败=具体原因(前端失败时当原因展示) | diff --git a/docs/database/coupon_session.md b/docs/database/coupon_session.md new file mode 100644 index 0000000..dcbc650 --- /dev/null +++ b/docs/database/coupon_session.md @@ -0,0 +1,44 @@ +# coupon_session — 领券任务全程流水(admin「领券数据」看板数据源) + +> 模型 `app/models/coupon_state.py`(`CouponSession`) · 仓库 `app/repositories/coupon_state.py`(`upsert_session`) · 接口 C 端 `POST /api/v1/coupon/session`([coupon-session](../api/coupon/coupon-session.md));admin `GET /admin/api/coupon-data/*`(聚合看板 + 明细,`app/admin/routers/coupon_data.py`) · [← 索引](./README.md) · [总览](./OVERVIEW.md) + +**一次领券任务一行**(`trace_id` 唯一),记从发起(`started`)到收尾(`completed`/`failed`/`abandoned`)的全程:总耗时、各平台耗时、领到张数、机型/ROM、发起来源。与同文件的三张「今日状态」表([coupon_state](./coupon_state.md))分工不同:那三张按「设备×日」管**频控/置灰**,本表按「一次任务」管**漏斗与体验指标**——admin「领券数据」看板的发起数、完成率、中途流失(started 无终态)、平均耗时都从这算。#99 新增(2026-06-30)。 + +## 用在哪 / 增删改查 +- **C / U(upsert,按 `trace_id`)**:客户端 `POST /api/v1/coupon/session` **两段上报**——发起时建行(`status='started'`,带 platforms/origin_package/机型),收尾时按同 `trace_id` 更新同一行(终态 + `elapsed_ms` + `platform_elapsed` + `claimed_count`)。发起即落库 → 收尾丢失(App 被杀/断网)的行永远停在 `started` = 中途流失,可量化。 +- **D**:无。 +- **R**:admin `GET /admin/api/coupon-data`(按 `started_date` × `app_env` 聚合趋势/漏斗 + 逐条明细,join `user` 出手机号)+ `GET /admin/api/coupon-data/user-records`(用户抽屉:某用户全部领券记录)。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `trace_id` | String(64) | NOT NULL, **UNIQUE**(`uq_coupon_session_trace`) | 一次领券唯一 id(客户端 UUID,全程贯穿),upsert 幂等键;与 pricebot work_logs / `coupon_claim_record.trace_id` 同源 | +| `device_id` | String(64) | NOT NULL | 客户端 per-install id | +| `user_id` | Integer | index, nullable | 登录态才带(admin join `user` 出手机号/昵称);匿名领券为空。**无硬 FK** | +| `status` | String(16) | NOT NULL | `started`(发起)/ `completed` / `failed` / `abandoned`(用户中止)。`started` 无终态 = 中途流失 | +| `app_env` | String(16) | index, nullable | `prod` / `dev`(客户端 BuildConfig.DEBUG)。admin 报表默认只看 prod,防测试数据串台 | +| `platforms` | JSON(PG: JSONB) | nullable | 发起勾选平台 `["meituan-waimai", ...]`;空=全领 | +| `origin_package` | String(64) | nullable | 发起来源外卖 App 包名;null=App 内(傻瓜比价首页)发起,非空=切到美团/淘宝/京东被弹券引导发起。admin「发起平台」列据此区分 | +| `device_model` | String(128) | nullable | 机型(Build.MANUFACTURER + MODEL) | +| `rom` | String(64) | nullable | ROM(OemDetector,如 `ColorOS 14`) | +| `started_at` | DateTime(tz) | NOT NULL | 发起时刻(客户端墙钟) | +| `started_date` | Date | NOT NULL | 发起的 Asia/Shanghai 自然日;admin 按天聚合/筛选(复合索引) | +| `finished_at` | DateTime(tz) | nullable | 收尾时刻(服务端 now);未收尾(流失)为空 | +| `elapsed_ms` | Integer | nullable | 全程耗时(客户端点发起→收尾计时,权威);平均/分位只统计 completed | +| `platform_elapsed` | JSON(PG: JSONB) | nullable | 各平台领券耗时 `{"meituan-waimai": 3200, ...}`(ms) | +| `claimed_count` | Integer | nullable | 本次领到张数(收尾上报) | +| `trace_url` | String(512) | nullable | pricebot 公网调试链接(admin 明细可点开复盘) | +| `created_at` / `updated_at` | DateTime(tz) | server_default now() / +onupdate | | + +## 关系 / Join Key +- `user_id` 软指 `user.id`(无硬 FK,可空;admin 明细 LEFT JOIN 出手机号)。 +- `trace_id` 与 pricebot work_logs、[`coupon_claim_record`](./coupon_state.md).`trace_id` 同源(一次任务),无 id 级 join。 +- `device_id` 与领券三表同源(per-install id)。 + +## 索引与约束 +- PK `id`;UNIQUE `trace_id`;index `user_id`、`app_env`;复合 index `ix_coupon_session_date_env`(`started_date`, `app_env`)= admin 看板主查询路径。 + +## 注意 +- 写库 best-effort 口径与领券三表一致:上报失败不影响领券本身(且本表数据源是客户端**独立上报**,不是 `/coupon/step` 透传顺手写)。 +- `elapsed_ms` 以客户端计时为准(墙钟差不影响);`finished_at - started_at` 只做时刻留痕,不用来算时长。 diff --git a/docs/database/coupon_state.md b/docs/database/coupon_state.md index c6559b8..baa7105 100644 --- a/docs/database/coupon_state.md +++ b/docs/database/coupon_state.md @@ -1,6 +1,7 @@ # coupon_state — 领券今日状态三张表(弹窗频控 / 首页置灰 / 领券记录) > 模型 `app/models/coupon_state.py` · 仓库 `app/repositories/coupon_state.py` · 接口 `app/api/v1/coupon.py`(prefix `/api/v1/coupon`) · [← 索引](./README.md) · [总览](./OVERVIEW.md) +> 同模型文件里还有第四张表 [`coupon_session`](./coupon_session.md)(一次领券任务一行的全程流水,admin「领券数据」看板数据源,#99)——维度与本文三张「设备×日」状态表不同,单独成文。 领券(优惠券自动化)联动产生的三张「今日状态」表,都挂在领券透传端点 `POST /api/v1/coupon/step` 这条链路上(pricebot 跑领券,结果回 app-server 落库;**领券过程本身在 pricebot 内存态跑、不落库**)。三表各管一件事: diff --git a/docs/database/cps_order.md b/docs/database/cps_order.md index ad022a2..44c02fe 100644 --- a/docs/database/cps_order.md +++ b/docs/database/cps_order.md @@ -2,7 +2,7 @@ > 模型 `app/models/cps_order.py` · 仓库 `app/admin/repositories/cps.py`(`reconcile_orders` / `_map_order_fields` / `list_orders`、统计 `group_stats`) · 接口 admin `POST /admin/api/cps/orders/reconcile`、`GET /admin/api/cps/orders`、`GET /admin/api/cps/stats` · [← 索引](./README.md) · [总览](./OVERVIEW.md) -从美团联盟 `query_order` 按时间窗拉回、按 `sid` 归群的订单明细,是 CPS 群发漏斗的**最下游"赚了多少佣金"**:用户点 [`cps_link`](./cps_link.md)(携群 [`cps_group`](./cps_group.md) 的 `sid`)下单后,美团把订单连同 `sid` 回传,这里按 `sid` 归群做对账,与 [`cps_click`](./cps_click.md) 的点击量在 `group_stats` 汇成"点击→下单→佣金"漏斗。**仅美团有此表**——淘宝/京东无对账 API,统计里对账字段显示 `-`。 +从联盟 API 按时间窗拉回、按 `sid` 归群的订单明细,是 CPS 群发漏斗的**最下游"赚了多少佣金"**:用户点 [`cps_link`](./cps_link.md)(携群 [`cps_group`](./cps_group.md) 的 `sid`)下单后,联盟把订单连同 `sid` 回传,这里按 `sid` 归群做对账,与 [`cps_click`](./cps_click.md) 的点击量在 `group_stats` 汇成"点击→下单→佣金"漏斗。**平台覆盖**:初版仅美团(`query_order`);#90(2026-06-30)接入**京东联盟**订单(拉单进同一张表,`platform='jd'`,配 `jd_*`/`external_*` 列,喂数据大盘);淘宝仍无对账 API,统计里对账字段显示 `-`。 ## 用在哪 / 增删改查 - **C/U(upsert)**:`POST /admin/api/cps/orders/reconcile`(`reconcile_orders`,需 `finance` 角色)。调 `meituan.query_order(sid?, start_time, end_time, page, limit=100)` 分页拉单(`max_pages=200` 防死循环),每条经 `_map_order_fields` 转字段,按 `order_id` **upsert**:不存在则 insert,存在则逐字段覆盖(订单状态会随时间变 付款→完成→结算/退款,重复拉则更新)。返回 `{fetched, inserted, updated, pages}`。`order_id` 全局唯一即幂等键。 @@ -15,7 +15,10 @@ | 列 | 类型 | 约束 / 默认 | 说明(取值 / join / 源字段) | |---|---|---|---| | `id` | Integer | PK, autoincrement | | -| `order_id` | String(64) | **UNIQUE, index, NOT NULL** | 美团订单号(加密串),`orderId`。upsert 幂等键 | +| `platform` | String(20) | index, NOT NULL, default `meituan` | 订单来源联盟:`meituan` / `jd`(#90)。统计/大盘按它分平台 | +| `order_id` | String(64) | **UNIQUE, index, NOT NULL** | 订单号(美团加密串 `orderId`;京东为联盟订单号)。upsert 幂等键 | +| `external_order_id` | String(128) | index, nullable | 平台原始订单号(#90,京东 `orderId`;美团 null) | +| `external_row_id` | String(128) | index, nullable | 平台订单行号(#90,京东一单多 sku 时区分行;美团 null) | | `sid` | String(64) | index, **nullable** | 渠道追踪位 = 群 `sid`,源 `sid`。**按它归群聚合**;历史无 sid 订单为空 | | `act_id` | String(64) | index, nullable | 活动物料 ID,源 `actId`(转字符串) | | `biz_line` | Integer | nullable | 业务线,源 `businessLine`。`1=外卖` | @@ -25,9 +28,14 @@ | `commission_rate` | String(16) | nullable | 佣金率,源 `commissionRate`。`"300"=3%`、`"10"=0.1%`(原样字符串,前端解释) | | `refund_price_cents` | Integer | nullable | 退款金额(分),源 `refundPrice`(元) | | `refund_profit_cents` | Integer | nullable | 退款佣金(分),源 `refundProfit`(元) | -| `mt_status` | String(8) | index, nullable | 美团订单状态,源 `status`:`2`付款 `3`完成 `4`取消 `5`风控 `6`结算 | +| `estimated_commission_cents` | Integer | nullable | 预估佣金(分,#90 京东口径;美团用 `commission_cents`) | +| `actual_commission_cents` | Integer | nullable | 实际/结算佣金(分,#90 京东口径) | +| `mt_status` | String(8) | index, nullable | 美团订单状态,源 `status`:`2`付款 `3`完成 `4`取消 `5`风控 `6`结算;京东订单为 null | +| `jd_valid_code` | String(16) | index, nullable | 京东订单有效码(#90,联盟 `validCode`,判有效/无效/风控);美团订单为 null | | `invalid_reason` | String(128) | nullable | 失效原因,源 `invalidReason` | | `product_name` | String(512) | nullable | 商品名,源 `productName`(超 500 截断) | +| `settle_month` | String(16) | nullable | 结算月份(#90 京东) | +| `site_id` / `position_id` / `pid` | String(128) | nullable | 京东推广位维度(#90):站点/推广位/联盟 pid,归因用 | | `pay_time` | DateTime(tz) | index, nullable | 付款时间,源 `payTime`(秒级 ts)。统计按时间窗过滤的就是它 | | `mt_update_time` | DateTime(tz) | nullable | 美团侧更新时间,源 `updateTime`(秒级 ts) | | `raw` | JSON / JSONB | NOT NULL, default `{}` | `query_order` 单条原始 dataList,留底排查/补字段 | @@ -48,4 +56,5 @@ - **"未归群"独立行**:`group_stats` 遍历完所有群后,对剩下的、有订单但 `sid` 不属任何现存群的 sid(历史遗留如 `wonderableai`、或别处来源、或群被删),单列一行 `group_id=None`、有对账无点击。所以本表 `sid` 可空/可孤立是设计内的,不是脏数据。 - **金额/时间统一转换的原因**:`query_order` 返回金额是「元」字符串、时间是秒级 ts;`_yuan_to_cents`(Decimal 防浮点,`"null"`/空 → None)与 `_ts_to_dt`(转 tz-aware UTC,前端按北京展示)在入库时归一,与全站"金额存分、时间存 tz-aware"口径对齐。`raw` 整条留底,字段不够时不用重拉。 - **upsert 而非 append**:同一订单会被多次拉到(状态变化),按 `order_id` 覆盖即可拿到最新状态;`reconcile` 可重复跑(幂等)。 -- **本表自始至终只服务美团**:初版(`277f9b1`)就定型,`3a40f61` 接淘宝/京东时**没动本表**——那俩平台无对账 API,统计里对账列直接给 `None`(前端显示 `-`)。这是"对账 = 美团专属"的边界。 +- **平台边界的演变**:初版(`277f9b1`)只服务美团;#90(2026-06-30)接入**京东联盟**拉单进同一张表(`platform='jd'` + `external_*`/`jd_valid_code`/`estimated|actual_commission_cents` 等列,有效性按 `jd_valid_code` 判,喂 admin 数据大盘的京东收益)。**淘宝仍无对账 API**,统计对账列给 `None`(前端显示 `-`)。 +- **#119 修 `pay_time` 缺失**:早期美团拉单部分订单 `payTime` 空导致 `pay_time` 为 null → 大盘按时间窗过滤漏算美团收益;#119 起入库补齐/回填,时间窗统计以 `pay_time` 为准。 diff --git a/docs/database/device_liveness.md b/docs/database/device_liveness.md index 277c367..f2ef1ea 100644 --- a/docs/database/device_liveness.md +++ b/docs/database/device_liveness.md @@ -9,7 +9,7 @@ ## 用在哪 / 增删改查 - **C / U(upsert)**:`register_or_update`(`POST /device/register`,App 拿到 push token 时调)按 `(user_id, device_id)` upsert,只在传入非空时更新 `registration_id`/`platform`/`app_version`;`touch_heartbeat`(`POST /device/heartbeat`,无障碍服务存活时周期调,**心跳也能自注册**)在 `accessibility_enabled=true` 时刷 `last_heartbeat_at`、置 `ever_protected=true`、状态机重置回 `alive`、清 `notified_at`(掉线恢复→下次再断才再推一条)。 - **U(worker)**:`mark_notified`(`heartbeat_monitor_worker` 检出掉线后)置 `liveness_state='notified'` + `notified_at` + **`kill_alert_pending=True`**;`ack_kill_alert`(`POST /device/liveness/ack`,客户端弹过引导后)清 `kill_alert_pending`(幂等)。 -- **R**:`list_overdue`(worker 扫描:`ever_protected=true` + `liveness_state='alive'` + `last_heartbeat_at` 早于 `now - timeout`)→ 掉线设备列表;`get_device`(`GET /device/liveness`,客户端进 App 拉本机是否被判掉线过)。 +- **R**:`list_overdue`(worker 扫描:`ever_protected=true` + `liveness_state='alive'` + `last_heartbeat_at` 早于 `now - timeout`)→ 掉线设备列表;`get_device`(`GET /device/liveness`,客户端进 App 拉本机是否被判掉线过);admin `GET /admin/api/device-liveness/stats` + 列表(#80,后台「设备存活监控」页:总数/在线/掉线卡片 + 明细)。 - **D**:无。 ## 字段 @@ -22,6 +22,7 @@ | `platform` | String(16) | NOT NULL, default `android` | | | `app_version` | String(32) | nullable | 上报时 App 版本 | | `ever_protected` | Boolean | NOT NULL, default false | 收到过 service 心跳即 true(=该设备开过无障碍,功能对它有意义)。`list_overdue` 的过滤前提 | +| `first_protected_at` | DateTime(tz) | nullable | **首次**开启无障碍(首次收到心跳)的时刻(#80);置 `ever_protected=true` 时一并写、之后不再变。admin 设备存活监控用它算「开启保护耗时/转化」 | | `last_heartbeat_at` | DateTime(tz) | index, nullable | 最近一次 service 心跳时间(存活证明);超时即视为保护掉线 | | `last_report_protection_on` | Boolean | NOT NULL, default false | 最近一次上报的无障碍开关状态(观测用) | | `liveness_state` | String(16) | NOT NULL, default `unknown` | 状态机:`unknown` → `alive`(收到 service 心跳)→ `silent`/`notified`(扫描发现超时并已推送);心跳恢复 handler 重置回 `alive` | @@ -40,4 +41,4 @@ ## 注意 - **`kill_alert_pending` 为什么和 `liveness_state` 解耦**:服务随 App 重启会先发心跳把 `state` 重置回 `alive`,若复用 `state` 判「待提醒」,客户端进 App 这一刻可能恰好已被重置 → 漏看这次掉线。故另设一个只由 worker 置、只由客户端 ack 清的标记,规避竞态。 - **`list_overdue` 本期不要求有 `registration_id`**:本期只做终端打印检测、未真推送,没接极光 token 的设备也要检出。 -- 心跳超时阈值由 worker 的 `timeout_minutes` 决定(不在表里)。 +- 心跳超时阈值由 worker 的 `timeout_minutes` 决定(不在表里);#107 起默认 **1 小时**(原 10 分钟误报率高:息屏/省电模式下心跳会正常停发)。 diff --git a/docs/database/feedback.md b/docs/database/feedback.md index d916b30..f867e0c 100644 --- a/docs/database/feedback.md +++ b/docs/database/feedback.md @@ -1,14 +1,14 @@ -# feedback — 用户帮助与反馈 +# feedback — 用户帮助与反馈(含审核发奖) -> 模型 `app/models/feedback.py` · 仓库 `app/repositories/feedback.py` · 接口 [feedback](../api/feedback.md) · admin [admin-feedbacks-list](../api/admin-feedbacks-list.md) / [admin-feedback-handle](../api/admin-feedback-handle.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md) +> 模型 `app/models/feedback.py` · 仓库 `app/repositories/feedback.py` · 接口 C 端 [feedback](../api/other/feedback.md) / [feedback-records](../api/other/feedback-records.md);admin `GET /admin/api/feedbacks`、`/summary`、`POST /{id}/approve|reject|handle`(`app/admin/routers/feedback.py`) · [← 索引](./README.md) · [总览](./OVERVIEW.md) -App「帮助与反馈」每次提交写一行。`content` 必填;`contact` 原必填,**原型改版后客户端不再采集,新数据存空串**(列保持 NOT NULL、免迁移,历史数据仍有值);`images` 为可选截图(≤6 张)。后台人工处理后置 `handled`。与 `price_report`(结构化上报更低价)不同,本表是**自由文本**反馈。 +App「帮助与反馈」每次提交写一行。2026-06 起演进为**轻审核工单**:运营在后台采纳(`adopted`,可发金币)/拒绝(`rejected`,填原因)并可写**运营回复**(#105),C 端「我的反馈」列表把状态与回复展示给用户;提交侧自动采集**来源/场景 + 端环境**(App 版本/机型/ROM/Android 版本,#94)供排障。与 `price_report`(结构化上报更低价)不同,本表是**自由文本**反馈。 ## 用在哪 / 增删改查 -- **C(插入)**:`POST /api/v1/feedback`(multipart:`content` + 可选 `contact` + 可选 `images`;`create_feedback`)。截图先经 `core.media` 落 `/media/feedback/` 拿相对路径,再随反馈写入,`status='new'`。 -- **U(更新)**:admin 处理反馈 `update_feedback_status` → `status='handled'`(同事务写 `admin_audit_log`)。 +- **C(插入)**:`POST /api/v1/feedback`(multipart:`content` + 可选 `contact`/`images`/`source`/`scene`;客户端自动带 `app_version`/`device_model`/`rom_name`/`android_version`)→ `status='pending'`。截图经 `core.media` 落 `/media/feedback/`。 +- **U(更新,admin,均写 `admin_audit_log`)**:`approve`(→`adopted`,可选发金币 `reward_coins`,走 `grant_coins` 同事务)/ `reject`(→`rejected`,`reject_reason`)/ `handle`(→`handled`,旧口径「标记已处理」保留)。三者均可写 `admin_reply`(用户可见回复)与 `review_note`(内部备注)。 - **D**:无。 -- **R**:admin 反馈列表(可按 `status` 筛)。C 端当前无"我的反馈列表"读接口。 +- **R**:C 端 `GET /api/v1/feedback/records`(我的反馈历史,展示状态/回复/奖励);admin 列表(状态/来源筛选)+ `summary`(各状态计数)。 ## 字段 | 列 | 类型 | 约束 / 默认 | 说明(取值 / join) | @@ -16,17 +16,31 @@ App「帮助与反馈」每次提交写一行。`content` 必填;`contact` 原 | `id` | Integer | PK, autoincrement | | | `user_id` | Integer | FK→user.id, index, NOT NULL | 提交用户 | | `content` | Text | NOT NULL | 反馈正文 | -| `contact` | String(128) | NOT NULL | 联系方式(微信/QQ/手机)。客户端改版后不再采集,新数据为空串;列仍 NOT NULL | -| `images` | JSON | nullable | 截图相对 URL 列表 `/media/feedback/...`;无图为 NULL | -| `status` | String(16) | NOT NULL, default `new` | 取值:`new`(待处理)/ `handled`(已处理) | +| `contact` | String(128) | NOT NULL | 联系方式。客户端改版后不再采集,新数据为空串;列仍 NOT NULL | +| `source` | String(16) | NOT NULL, default `profile`, index | 提交入口来源(#105):`profile`(设置/我的页普通反馈)/ 比价场景值;客户端显式传优先,未传由 `scene` 有无派生 | +| `scene` | String(32) | nullable | 比价反馈的「问题场景」(找错商品/优惠不对/比价太慢…,#105);比价结果页反馈才有,普通反馈为 NULL | +| `images` | JSON | nullable | 截图相对 URL 列表 `/media/feedback/...` | +| `app_version` | String(32) | nullable | 提交时 App versionName(#94) | +| `device_model` | String(64) | nullable | 机型 Build.MODEL(#94) | +| `rom_name` | String(32) | nullable | ROM(OemDetector:ColorOS/MIUI/…,#94) | +| `android_version` | String(16) | nullable | Android 版本(#94) | +| `status` | String(16) | NOT NULL, default `pending`, index | `pending`(待处理)/ `adopted`(已采纳,#94)/ `rejected`(已拒绝)/ `handled`(旧「已处理」口径,兼容保留;历史另有 `new`) | +| `reject_reason` | String(256) | nullable | 拒绝原因(用户可见) | +| `reward_coins` | Integer | nullable | 采纳发的金币数(未发为 null;发币走 `coin_transaction`) | +| `review_note` | String(256) | nullable | 运营内部备注(不外露) | +| `admin_reply` | String(256) | nullable | **运营回复**(用户可见,#105;C 端反馈历史展示) | +| `reviewed_by_admin_id` | Integer | nullable | 审核管理员 id(软指 `admin_user.id`) | +| `reviewed_at` | DateTime(tz) | nullable | 审核时间 | | `created_at` | DateTime(tz) | server_default now(), index | 提交时间 | ## 关系 / Join Key - `user_id` → `user.id`(多对一)。 -- admin 处理时被 `admin_audit_log` 记录(`target_type='feedback'`、`target_id`=本行 id)。 +- admin 审核动作被 `admin_audit_log` 记录(`target_type='feedback'`);`reviewed_by_admin_id` 软指 `admin_user.id`(无硬 FK)。 +- 采纳发奖时写 `coin_transaction`(发币入口 `grant_coins` 同事务)。 ## 索引与约束 -- PK `id`;index `user_id`、`created_at`。 +- PK `id`;index `user_id`、`status`、`created_at`。 ## 注意 - `images` 用通用 `JSON`(本表**未**用 JSONB variant,与 comparison/savings 不同)。 +- 状态机是单向的:`pending → adopted/rejected/handled`。`approve`/`reject` 只接受 `pending`(或历史 `new`)态,重复审核报 400;旧口径 `handle` **幂等不校验原状态**(重复调用结果一致)。 diff --git a/docs/database/invite_cash_transaction.md b/docs/database/invite_cash_transaction.md new file mode 100644 index 0000000..0207feb --- /dev/null +++ b/docs/database/invite_cash_transaction.md @@ -0,0 +1,42 @@ +# invite_cash_transaction — 邀请奖励金流水账本(分) + +> 模型 `app/models/wallet.py`(`InviteCashTransaction`) · 仓库 `app/repositories/wallet.py`(`grant_invite_cash` / 提现路径按 `source` 分账) · 接口 [wallet-account](../api/wallet/wallet-account.md)(余额)/ [wallet-withdraw](../api/wallet/wallet-withdraw.md)(`source=invite_cash` 提现) · [← 索引](./README.md) · [总览](./OVERVIEW.md) + +**邀请奖励金**(现金,分)的流水账本,与金币兑换的 `cash_transaction` **物理隔离**、结构同构:`balance_after_cents` 记的是 `coin_account.invite_cash_balance_cents`。隔离原因:邀请奖励金有独立的发放口径(好友**比价并下单**才发,#113)与独立的提现对账(`withdraw_order.source='invite_cash'`,#121),混在现金账本里无法分账校验。#82 新增(2026-06-27)。 + +## 用在哪 / 增删改查 +- **C(插入)**:每次奖励金变动一笔: + + | 动作 / endpoint | `biz_type` | `amount_cents` | `ref_id` 指向 | + |---|---|---|---| + | 好友比价并下单发奖(`POST /order/report` → `invite.try_reward_on_compare` → `grant_invite_cash`) | `invite_reward` | + | 被邀请人 `user.id`(字符串) | + | 发起提现 `POST /wallet/withdraw`(`source=invite_cash`) | `invite_withdraw` | − | `withdraw_order.out_bill_no` | + | 该提现失败/审核拒绝退款 | `invite_withdraw_refund` | + | `withdraw_order.out_bill_no` | + | admin 手动调整 `POST /admin/api/users/{id}/cash`(`account=invite_cash`,#95) | `admin_grant` / `admin_deduct` | + / − | null(原因记 `remark`=`admin:`) | + +- **U / D**:无。账本只追加。 +- **R**:admin 用户 360 / 提现资金账本校验 `GET /admin/api/withdraws/ledger-check`(按 `source` 分账对账,#121);C 端邀请页战绩(`invite.get_reward_stats`:余额 + 累计已提)。 + +## 字段 +| 列 | 类型 | 约束 / 默认 | 说明 | +|---|---|---|---| +| `id` | Integer | PK, autoincrement | | +| `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户(= 邀请人) | +| `amount_cents` | Integer | NOT NULL | 本笔变动(分);正=入账(发奖/退款),负=出账(提现) | +| `balance_after_cents` | Integer | NOT NULL | 本笔后奖励金余额(= 当时 `coin_account.invite_cash_balance_cents`) | +| `biz_type` | String(32) | NOT NULL | `invite_reward` / `invite_withdraw` / `invite_withdraw_refund` | +| `ref_id` | String(64) | nullable | 见上表:发奖=被邀请人 id;提现/退款=`withdraw_order.out_bill_no` | +| `remark` | String(128) | nullable | 用户可见备注(如「好友比价奖励」「提现到微信零钱(待审核)」) | +| `created_at` | DateTime(tz) | server_default now(), index | | + +## 关系 / Join Key +- `user_id` → `user.id`(多对一,硬 FK)。 +- `ref_id` →(withdraw 类)`withdraw_order.out_bill_no`(软关联;该单 `source='invite_cash'`)/(invite_reward)被邀请人 `user.id`。 +- 与 `invite_relation` 的联动:发奖同事务置 `invite_relation.compare_reward_granted=true`(幂等闸,一个被邀请人只发一次)。 + +## 索引与约束 +- PK `id`;index `user_id`、`created_at`。 + +## 注意 +- **唯一变动入口 `wallet.grant_invite_cash`**:更新 `coin_account.invite_cash_balance_cents` + 写本表,**不 commit**,调用方同事务提交(与 `grant_coins` 同款约定)。 +- 提现按 `source` 走不同账本:`coin_cash` → `cash_transaction`,`invite_cash` → 本表;`withdraw_order` 两种共用一张表靠 `source` 列区分。 diff --git a/docs/database/invite_relation.md b/docs/database/invite_relation.md index 590b20f..61b65a2 100644 --- a/docs/database/invite_relation.md +++ b/docs/database/invite_relation.md @@ -2,15 +2,16 @@ > 模型 `app/models/invite.py`(`InviteRelation`) · 仓库 `app/repositories/invite.py` · 接口 `app/api/v1/invite.py`:`POST /api/v1/invite/bind`(写)、`GET /api/v1/invite/me`(战绩)、`GET /api/v1/invite/invitees`(列表) · [← 索引](./README.md) · [总览](./OVERVIEW.md) -一行 = 一次**成功的**邀请绑定。被邀请人(B)用邀请人(A)的邀请码完成绑定后写入,**注册即生效**:同一事务里给 A、B 各发 1 万金币(= 1 元,可提现)。数据来自 `bind()`,触发它的归因来源有三种(`channel`)。这是邀请功能的**结果表 / 账本**;邀请码本身存在 `user.invite_code`,不在这张表。 +一行 = 一次**成功的**邀请绑定。被邀请人(B)用邀请人(A)的邀请码完成绑定后写入,**绑定关系注册即生效,但绑定本身不发奖**(#113 起):发奖后置到 B **完成比价并实际下单**(`POST /order/report` → `try_reward_on_compare`),给 A 发**邀请奖励金**(入独立账本 [invite_cash_transaction](./invite_cash_transaction.md),非金币),本表 `compare_reward_granted` 做幂等闸。数据来自 `bind()`,触发它的归因来源有三种(`channel`)。这是邀请功能的**结果表 / 账本**;邀请码本身存在 `user.invite_code`,不在这张表。 ## 用在哪 / 增删改查 -- **C(插入)**:`POST /api/v1/invite/bind`(`bind_invite`)→ `invite_repo.bind()`。过四道防线后建一行 `status='effective'`,**同事务**复用 `wallet.grant_coins` 给双方发金币(`grant_coins` 只 flush,由 `bind()` 统一 commit)→ "建关系 + 双方加金币"原子。 - - **幂等键 = `invitee_user_id` 唯一**:一个 B 只能被绑一次,重复请求返回 `already_bound`,不重复发奖(仿 `ad_reward_record.trans_id` 思路)。并发下两请求同时插同一 invitee → 后者撞唯一约束 `IntegrityError`,`bind()` rollback 后改判 `already_bound` 兜底。 - - 四道防线(`bind()` 内):① invitee 已绑 → `already_bound`;② 无效码 / 邀请人非 `active` → `invalid_code`;③ 自邀(inviter==invitee)→ `self_invite`;④ 新人闸 `_is_new_user`(B 的 `created_at` 在 `rewards.INVITE_NEW_USER_WINDOW_HOURS`=72h 内)→ 否则 `not_eligible`。只有全过才插行 + 发奖。 -- **U / D**:无。这张表只增不改不删(纯账本)。 +- **C(插入)**:`POST /api/v1/invite/bind`(`bind_invite`)→ `invite_repo.bind()`。过四道防线后建一行 `status='effective'`。**#113 起绑定只建关系、不发奖**(`inviter_coin`/`invitee_coin` 新行恒 0)。 + - **幂等键 = `invitee_user_id` 唯一**:一个 B 只能被绑一次,重复请求返回 `already_bound`(仿 `ad_reward_record.trans_id` 思路)。并发下两请求同时插同一 invitee → 后者撞唯一约束 `IntegrityError`,`bind()` rollback 后改判 `already_bound` 兜底。 + - 四道防线(`bind()` 内):① invitee 已绑 → `already_bound`;② 无效码 / 邀请人非 `active` → `invalid_code`;③ 自邀(inviter==invitee)→ `self_invite`;④ 新人闸 `_is_new_user`(B 的 `created_at` 在 `rewards.INVITE_NEW_USER_WINDOW_HOURS`=72h 内)→ 否则 `not_eligible`。 +- **U(发奖,#113)**:B 比价后下单 `POST /order/report` → `try_reward_on_compare(invitee_user_id)`:`compare_reward_granted=False` 才发 → 置 `True` + 记 `compare_reward_cents`/`compare_rewarded_at`,**同事务** `wallet.grant_invite_cash` 给 A 入邀请奖励金 → "置闸 + 入账"原子,一个 B 只发一次。 +- **D**:无。 - **R**: - - `GET /api/v1/invite/me`(`my_invite`)→ `get_stats(inviter_id)`:`count(*)` 得已邀人数、`sum(inviter_coin)` 得累计金币。 + - `GET /api/v1/invite/me`(`my_invite`)→ `get_stats(inviter_id)`:`count(*)` 得已邀人数、`sum(inviter_coin)` 得累计金币(历史口径);另调 `get_reward_stats` 出邀请奖励金余额/累计已提(读 `coin_account.invite_cash_balance_cents` + `withdraw_order(source=invite_cash)`)。 - `GET /api/v1/invite/invitees`(`my_invitees`)→ `get_invitees(inviter_id, limit, offset)`:join `user` 出被邀请人列表(倒序分页),名字降级兜底 `nickname → wechat_nickname → 脱敏手机号`,`coins` 取 `inviter_coin`。 ## 字段 @@ -20,15 +21,18 @@ | `inviter_user_id` | Integer | FK→user.id, index, NOT NULL | 邀请人(A) | | `invitee_user_id` | Integer | FK→user.id, **UNIQUE**, index, NOT NULL | 被邀请人(B)。**唯一 = 幂等键**,一个 B 只能被归因一次 | | `channel` | String(16) | NOT NULL, default `clipboard` | 归因来源:`clipboard`(剪贴板自动)/ `manual`(手动填码)/ `fingerprint`(指纹反查兜底)。入库前 `[:16]` 截断 | -| `status` | String(16) | NOT NULL, default `effective` | 当前只有 `effective`(注册即生效)。**预留** `pending`/`effective`:将来若改"完成首单才生效"时启用 | -| `inviter_coin` | Integer | NOT NULL, default 0 | 本次给 A 发的金币(记账留痕,=`rewards.INVITE_INVITER_COINS`=10000) | -| `invitee_coin` | Integer | NOT NULL, default 0 | 本次给 B 发的金币(=`rewards.INVITE_INVITEE_COINS`=10000) | +| `status` | String(16) | NOT NULL, default `effective` | 当前只有 `effective`(绑定关系注册即生效)。「发奖后置」没有走 status,而是用下面的 `compare_reward_granted` 闸表达 | +| `inviter_coin` | Integer | NOT NULL, default 0 | **历史留痕**(#113 前"绑定即发金币"时代给 A 发的金币,当时=10000);#113 起新绑定恒 0 | +| `invitee_coin` | Integer | NOT NULL, default 0 | 同上,给 B 发的金币历史留痕;新绑定恒 0 | +| `compare_reward_granted` | Boolean | NOT NULL, default false | **发奖幂等闸**(#113):B 首次「比价并下单」后置 true,一个 B 只给 A 发一次邀请奖励金 | +| `compare_reward_cents` | Integer | NOT NULL, default 0 | 实发的邀请奖励金(分,留痕;改常量不影响历史行) | +| `compare_rewarded_at` | DateTime(tz) | nullable | 发奖时间 | | `created_at` | DateTime(tz) | server_default now(), index | 绑定时间(= 列表倒序键) | ## 关系 / Join Key - `inviter_user_id` → `user.id`(硬 FK,多对一):一个 A 可邀多个 B。 - `invitee_user_id` → `user.id`(硬 FK,**一对一**,唯一约束):一个 B 至多一行。 -- 发金币时落 `coin_transaction`:`biz_type='invite_inviter'`(给 A,`ref_id=invitee.id`)/ `biz_type='invite_invitee'`(给 B,`ref_id=inviter.id`),双方 `ref_id` 互指对方便于对账。 +- 流水关联:**#113 前**的绑定发金币落 `coin_transaction`(`biz_type='invite_inviter'`/`'invite_invitee'`,`ref_id` 互指对方)——现为历史类型,新绑定不再产生;**#113 起**发奖落 [`invite_cash_transaction`](./invite_cash_transaction.md)(`biz_type='invite_reward'`,`ref_id=被邀请人 id`)。 - `get_invitees` 用 `InviteRelation JOIN user ON user.id = invitee_user_id` 取被邀请人资料;`total` 单独 `count` 算 `has_more`。 ## 索引与约束 @@ -39,8 +43,8 @@ ## 注意 - **防重复发奖三道**(`repositories/invite.py` docstring):① `invitee_user_id` 唯一(应用层 `_relation_of_invitee` 先查 + DB 唯一约束并发兜底);② 自邀屏蔽;③ 手机号天然唯一(每个 B = 一个真实手机号账号)= 限制刷量规模。 -- **`status` 当前恒为 `effective`**:产品取"注册即生效"而非"完成首单才生效",`pending` 取值是为后者预留、目前不写入。 -- **`inviter_coin`/`invitee_coin` 是留痕字段**:写死当时发的金币值,即便日后改奖励常量,历史行仍保留发奖时的额度,便于对账。 +- **`status` 当前恒为 `effective`**(绑定关系维度);「发奖后置」由 `compare_reward_granted` 闸表达,没有引入 `pending` 状态。 +- **金币留痕字段已冻结**:`inviter_coin`/`invitee_coin` 只反映 #113 前旧口径的历史发放额,便于对账;新奖励额看 `compare_reward_cents`。 - **`channel` 三种取值**:`clipboard`(deferred-deeplink 主路径,落地页写剪贴板、首启读回)/ `manual`(用户在邀请页手输)/ `fingerprint`(剪贴板被覆盖时走指纹兜底,见 [invite_fingerprint](./invite_fingerprint.md));三者都汇入同一个 `bind()`,只 `channel` 不同。 -- **风控缺口(#24 设计文档「上线前还差什么」标注)**:1 万金币可提现且**邀请人无总数上限**,接码平台批量注册新号绑同码即可刷;上线前需加 inviter 上限 + 基础风控。当前 MVP 仅靠"手机号唯一 + 72h 新人闸"挡。 +- **风控演进**:#24 时代的缺口是"注册即发 1 万可提现金币、接码批量刷"——**#113 把发奖后置到真实比价+下单**(且发的是邀请奖励金独立账本),批量注册空号不再直接得利,刷奖成本显著抬高;inviter 总数上限等进一步风控仍待补。 - **alembic 多 head**:本表迁移 `invite_code_and_relation`(`down_revision=11a1d08c6f55`)与 `coupon_state_tables` 是同一父的兄弟迁移,合 main 前需建 merge 迁移,否则 prod `alembic upgrade head` 撞 `Multiple head revisions`(#24 设计文档已警示)。 diff --git a/docs/database/withdraw_order.md b/docs/database/withdraw_order.md index 9b34897..e06c0e0 100644 --- a/docs/database/withdraw_order.md +++ b/docs/database/withdraw_order.md @@ -1,6 +1,6 @@ # withdraw_order — 提现单(现金 → 微信零钱) -> 模型 `app/models/wallet.py` · 仓库 `app/repositories/wallet.py` · 接口 [wallet-withdraw](../api/wallet-withdraw.md) / [wallet-withdraw-status](../api/wallet-withdraw-status.md) / [wallet-withdraw-orders](../api/wallet-withdraw-orders.md) · admin [admin-withdraws-list](../api/admin-withdraws-list.md) / [admin-withdraw-refresh](../api/admin-withdraw-refresh.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md) +> 模型 `app/models/wallet.py` · 仓库 `app/repositories/wallet.py` · 接口 [wallet-withdraw](../api/wallet/wallet-withdraw.md) / [wallet-withdraw-status](../api/wallet/wallet-withdraw-status.md) / [wallet-withdraw-orders](../api/wallet/wallet-withdraw-orders.md) · admin [admin-withdraws-list](../api/admin/withdraws/admin-withdraws-list.md) / [admin-withdraw-refresh](../api/admin/withdraws/admin-withdraw-refresh.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md) 用户把现金余额提到微信零钱的工单。**含人工审核**(2026-06 起):发起即扣现金、进 `reviewing` 待审核、**不打款**;管理员后台审核通过才真正发起微信商家转账,拒绝则退款。 @@ -26,7 +26,8 @@ reviewing ──admin 审核拒绝──▶ rejected(已退款) |---|---|---|---| | `id` | Integer | PK, autoincrement | | | `user_id` | Integer | FK→user.id, index, NOT NULL | 归属用户 | -| `out_bill_no` | String(64) | UNIQUE, index, NOT NULL | 商户单号(幂等键 + 微信查单键)。客户端可传(`[0-9A-Za-z_-]{8,32}`),不传则服务端 `uuid4().hex`。**被 `cash_transaction.ref_id` 引用** | +| `out_bill_no` | String(64) | UNIQUE, index, NOT NULL | 商户单号(幂等键 + 微信查单键)。客户端可传(`[0-9A-Za-z_-]{8,32}`),不传则服务端 `uuid4().hex`。**被 `cash_transaction.ref_id` / `invite_cash_transaction.ref_id` 引用**(按 `source` 落对应账本) | +| `source` | String(16) | NOT NULL, default `coin_cash` | 提现账户来源(#121 分账):`coin_cash`(金币兑换的现金,扣 `coin_account.cash_balance_cents`、流水落 `cash_transaction`)/ `invite_cash`(邀请奖励金,扣 `invite_cash_balance_cents`、流水落 [`invite_cash_transaction`](./invite_cash_transaction.md)) | | `amount_cents` | Integer | NOT NULL | 提现金额(分) | | `user_name` | String(64) | nullable | 提现实名;微信**达额转账要求实名**,发起时存下、审核打款时传给微信 | | `status` | String(16) | NOT NULL, default `reviewing` | 归一化状态:`reviewing`(待审核,已扣款未打款)/ `pending`(打款在途)/ `success` / `failed`(打款失败已退)/ `rejected`(审核拒绝已退) | @@ -39,7 +40,7 @@ reviewing ──admin 审核拒绝──▶ rejected(已退款) ## 关系 / Join Key - `user_id` → `user.id`(多对一)。 -- `out_bill_no` ← 被 `cash_transaction.ref_id` 引用(发起 `withdraw` 一笔 −,失败/拒绝 `withdraw_refund` 一笔 +)。 +- `out_bill_no` ← 被流水 `ref_id` 引用,**账本按 `source` 分**:`coin_cash` 单 → `cash_transaction`(`withdraw` − / `withdraw_refund` +);`invite_cash` 单 → `invite_cash_transaction`(`invite_withdraw` − / `invite_withdraw_refund` +)。admin `GET /admin/api/withdraws/ledger-check` 按 source 分账做「单 ↔ 流水」对账(#121)。 - 打款方式依赖 `wechat_transfer_authorization`(用户有生效授权 → 免确认转账,否则确认模式)。 ## 索引与约束 diff --git a/docs/后端技术实现.md b/docs/后端技术实现.md index 6d39c3b..40f90fa 100644 --- a/docs/后端技术实现.md +++ b/docs/后端技术实现.md @@ -3,7 +3,7 @@ > 域名:`app-api.shaguabijia.com`(HTTPS,nginx 反代) > 仓库:`shaguabijia-app-server` > 接口协议详见 [`docs/api/`](./api/)(索引 + 一接口一文件) -> 最后更新:2026-06-23(§7 数据模型改为指向 OVERVIEW、§5 美团 CPS 对齐多 tab feed/4 接口、§6 厘清 coupon/step 有写库副作用 ≠ 纯透传壳、§6.5 补 `/internal/app-version`、Alembic 迁移数更新) +> 最后更新:2026-07-09(§1/§6.2 比价透传改「软鉴权 + trace_id 签发 + harvest 落库」不再是纯透传;§3 目录树补全到当前 22 个 v1 路由 + core 三 worker + utils/geo;§1 补 邀请奖励金/埋点/领券看板/设备存活;§5 美团 feed/top-sales 按城市过滤 #116;§7 45 张表、98 迁移;§8 部署对齐 ecs1 git-clone + systemd 定时器;§10 刷新。上一次 2026-06-23) --- @@ -13,17 +13,23 @@ | 能力 | 说明 | |---|---| -| **账号与登录** | 极光一键登录 + 短信验证码登录(mock)→ 签发 JWT | +| **账号与登录** | 极光一键登录 + 短信验证码登录(SMS_MOCK 切换)→ 签发 JWT;新手引导标记(设备+账号,含用户自助重置 #114) | | **用户资料** | 昵称 / 头像(上传图片含魔数嗅探) / 注销账号(软删除+匿名化) | -| **美团 CPS 选品** | 透传美团联盟优惠券(外卖/到店)、点击换取推广链接(分佣)、未配凭证时降级返空 | -| **领券透传** | `/coupon/step` 透传到 pricebot-backend(一键领券核心,MVP 不鉴权,前端已接通) | -| **外卖比价透传** | `/intent/recognize` + `/price/step` 透传到 pricebot-backend(food MVP,MVP 不鉴权) | -| **金币 / 现金钱包** | 金币账户/流水/兑换/微信绑定/提现单 11 端点 | +| **美团 CPS 选品** | feed 多 tab(rec 离线库 / distance 实时)+ 销量榜,**按设备坐标离线反查城市、只出同城券**(#116);点击换推广链接(分佣);未配凭证降级返空 | +| **领券透传** | `/coupon/step` 透传到 pricebot-backend(一键领券核心,不鉴权,前端已接通)+ best-effort 写领券三表;`/coupon/session` 领券任务流水(admin 看板,#99) | +| **外卖比价透传 + 落库** | `/intent/*` + `/price/step` + `/trace/finalize`、`/trace/epilogue` 透传 pricebot,**软鉴权 + 首帧签发 trace_id + harvest 三段式落 `comparison_record`**(2026-07 起,见 §6.2) | +| **金币 / 现金钱包** | 金币账户/流水/兑换/微信绑定/提现单;**邀请奖励金独立账本**(#82,与现金物理隔离,`source` 分账提现 #121) | +| **好友邀请** | 邀请码/落地页指纹归因;发奖口径=好友**比价并下单**(#113,发邀请奖励金) | | **签到 + 任务 + 省钱战绩** | 福利模块 | -| **看广告发奖** | 穿山甲 GroMore 激励视频 S2S 回调(SHA256 验签)+ 4 态 CTA 冷却 | -| **帮助与反馈** | 用户提交反馈(含截图)+ 静态 `/media` 服务 | +| **看广告发奖** | 穿山甲 GroMore 激励视频 S2S 回调(SHA256 验签)+ 信息流/Draw 结算(ad_type/feed_scene 分场景)+ 穿山甲后台收益日表拉取(#92) | +| **帮助与反馈** | 反馈工单(来源/场景/端环境采集 + admin 采纳发币/拒绝/运营回复,#94/#105)+ 静态 `/media` 服务 | +| **埋点** | `/analytics/events` 批量上报落 `analytics_event`(#83) | +| **设备存活监控** | 无障碍心跳 + 掉线检出(worker,超时 1h #107)+ 召回 ack;admin 存活看板(#80) | +| **平台配置 / OTA** | `/platform/*`:门面统计、feature flags、广告配置下发、App 版本检查更新(全不鉴权) | +| **CPS 群发联盟** | `/c/{code}` 短链落地 + 微信授权 + 联盟对账(美团+京东 #90),运营台在 admin | +| **运营后台 admin** | 独立子应用(8771,独立 JWT):用户/钱包/提现审核/反馈/大盘/收益报表/领券看板/RBAC 自定义角色(#117/#126)等,全量端点见 [api/README](./api/README.md) Admin 段 | -**本服务自身没有"比价"实现**——比价/领券业务的核心都在 pricebot-backend(另一个 repo),本服务对 step 类接口做"透传壳"。无爬虫、无 LLM,但**有钱包/福利等业务模型**(数据模型从早期 1 张 `user` 表已扩展到 **40 张业务表**,见 §7 / [database/OVERVIEW.md](./database/OVERVIEW.md))。 +**"比价/领券"的执行核心在 pricebot-backend**(另一个 repo),本服务对 step 类接口做透传;但 2026-07 起比价透传**不再是纯壳**——app-server 直接负责比价记录落库(§6.2)。无爬虫、无 LLM,业务模型已扩展到 **45 张业务表**(见 §7 / [database/OVERVIEW.md](./database/OVERVIEW.md))。仓库根另有 `h5/`(mine 我的页 + shared bridge/api,#89,H5 化的我的页静态资源)。 --- @@ -52,81 +58,56 @@ ``` app/ -├── main.py # FastAPI 入口:注册全部 router、CORS、/health、lifespan +├── main.py # FastAPI 入口:注册 21 个 v1 router + 4 个 internal router、CORS、 +│ # lifespan(预热 pricebot client + 离线地理库,启 3 个后台 worker)、 +│ # /media 静态服务 + /media/shaguabijia.apk 官网直链(强制下载头) ├── api/ -│ ├── deps.py # 共享依赖:get_current_user(鉴权)、get_db(注入 session) -│ └── v1/ # 接口层(薄):解析请求 → 调 repositories/integration → 组装响应 + HTTP 错误码 -│ ├── auth.py # 登录 6 端点(极光一键登录 / 短信 send+login / refresh / me / logout) -│ ├── user.py # 用户资料 3 端点(改昵称 / 上传头像 / 注销账号) -│ ├── feedback.py # 帮助与反馈 1 端点(提交反馈含截图) -│ ├── coupon.py # 领券透传 /coupon/step(转发 pricebot,MVP 不鉴权) -│ ├── compare.py # 外卖比价透传 /intent/recognize + /price/step(转发 pricebot,MVP 不鉴权) -│ ├── compare_record.py# 比价记录 3 端点(上报 /compare/record + 列表 /compare/records + 详情;鉴权,区别于上面透传) -│ ├── meituan.py # 美团 3 端点 + feed 拼接(_interleave / _TOPIC_ROUNDS),未配 MT_CPS 凭证降级返空 -│ ├── wallet.py # 钱包/提现 11 端点(余额/流水/兑换/绑微信/提现/查单) -│ ├── signin.py # 签到 2 端点(状态 / 执行签到) -│ ├── tasks.py # 一次性任务 2 端点(列表 / 领取) -│ ├── savings.py # 省钱 3 端点(汇总 / 战绩 / 明细) -│ └── ad.py # 看广告发奖 3 端点(穿山甲 S2S 回调 / 进度+本轮冷却 / 联调发奖) -├── schemas/ # Pydantic:API 收发的数据契约(与客户端对齐字段看这里) -│ ├── auth.py -│ ├── user.py # 改昵称请求 + OkResponse -│ ├── feedback.py # 反馈出参(请求是 multipart,在 router 直接校验) -│ ├── meituan.py -│ ├── welfare.py # 钱包/签到/任务/省钱 收发模型 -│ ├── compare_record.py # 比价记录上报/列表/详情 收发模型(字段对齐 pricebot calibration + done.params) -│ └── ad.py # 看广告发奖收发模型 -├── integrations/ # 外部服务/SDK 客户端(重逻辑:签名/加解密/外部 HTTP) -│ ├── jiguang.py # 极光 REST 验 token + RSA 解密(多 padding 试错) -│ ├── meituan.py # 美团 CPS 网关签名 + query_coupon / get_referral_link -│ ├── 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 # 发奖/兑换/提现额度等业务常量与换算(2026-05 加 VIDEO_ROUND_REQUIRED_COUNT / VIDEO_ROUND_COOLDOWN_SECONDS) -│ ├── media.py # 用户上传文件(头像/反馈截图)落盘 + 魔数嗅探 + 随机文件名 -│ └── logging.py -├── repositories/ # 数据访问 + 事务(早期叫 crud,2026-05 统一并入此目录) -│ ├── user.py # get_user_by_id / by_phone / upsert_for_login / update_nickname / set_avatar_url / soft_delete_account -│ ├── feedback.py # 提交反馈写库 -│ ├── wallet.py # 账户/流水/兑换/提现单(调 integrations/wxpay) -│ ├── signin.py # 签到记录 / 连续天数 / 档位 -│ ├── task.py # 一次性任务领取 -│ ├── savings.py # 省钱汇总 / 战绩 / 明细 -│ ├── comparison.py # 比价记录 upsert(user_id+trace_id 幂等)+ best/saved/status 派生 + 分页 -│ └── ad_reward.py # 看广告发奖(按 trans_id 幂等 + 每日上限 + 本轮冷却派生) -├── models/ # ORM 表结构 -│ ├── user.py # user(含微信 openid/nickname/avatar) -│ ├── feedback.py # 用户反馈(content/contact/images JSON 列/status) -│ ├── wallet.py # 金币账户 / 金币流水 / 现金流水 / 提现单 -│ ├── signin.py # 签到记录 -│ ├── task.py # 任务领取记录 -│ ├── savings.py # 省钱明细 / 店铺菜品 / dishes(PG 上 JSONB) -│ ├── comparison.py # 比价记录(完整明细;含 4 个 JSON(B) 列 + raw_payload;独立于 savings) -│ └── ad_reward.py # 看广告发奖记录 -└── db/ - ├── base.py # DeclarativeBase - └── session.py # engine + get_db(非 SQLite 时启 pool: size=10/overflow=20/recycle=3600) +│ ├── deps.py # 共享依赖:get_current_user / OptionalUser(软鉴权) / get_db +│ ├── internal/ # server→server 内部端点(X-Internal-Secret):app_version / launch_confirm / price / store +│ └── v1/ # 接口层(薄),21 个路由文件: +│ ├── auth.py user.py feedback.py # 登录 / 资料+引导(含 reset #114) / 反馈工单 +│ ├── coupon.py # 领券透传 step + session 流水 + prompt/completed 频控族 +│ ├── compare.py # 比价透传 + trace_id 签发 + harvest 落库(§6.2) +│ ├── compare_record.py compare_milestone.py # 比价记录(鉴权兜底上报/列表/详情/stats)+ 里程碑 +│ ├── meituan.py # CPS 选品 4 端点(feed 多 tab / top-sales 按城市 #116) +│ ├── wallet.py wxpay.py # 钱包/提现(source 分账 #121)/transfer-auth 族 + 微信回调 stub +│ ├── signin.py tasks.py savings.py # 福利 +│ ├── ad.py # 激励视频 S2S + 信息流/Draw 结算 + eCPM/noshow/watch +│ ├── invite.py order.py report.py # 邀请 / 支付归因(触发邀请发奖 #113) / 上报更低价 +│ ├── analytics.py device.py platform.py # 埋点 #83 / 无障碍存活 #65 / 门面+flags+ad-config+OTA +│ └── cps_redirect.py # /c/{code} 短链落地 + 微信 OAuth(挂域名根) +├── admin/ # 运营后台独立子应用(8771,独立 JWT;app.main 不 import 它) +│ ├── main.py deps.py security.py permissions.py # 入口 / 鉴权链 / RBAC 页面目录(#117) +│ └── routers/ # 23 个路由:users wallet withdraw feedback(+qr) dashboard comparison +│ # coupon_data device_liveness event_logs price_report onboarding +│ # ops_marquee_seed ops_stat_config ad_audit ad_revenue ad_config +│ # config admins roles(#126) audit auth cps +├── schemas/ # Pydantic 契约(17 个文件,与客户端对齐字段看这里) +├── integrations/ # 外部 SDK(重逻辑):jiguang / meituan(S-Ca 签名) / sms / pangle / wxpay +├── core/ # 基础设施:config(+config_schema 运营可配项定义) / security / ratelimit / +│ │ # rewards / media / logging / ad_cooldown / test_account(测试号免验证码 #69) +│ ├── pricebot_router.py # pricebot 多实例一致性 hash(ketama 1000 虚节点,按 trace_id 亲和) +│ ├── pricebot_client.py # 共享 httpx AsyncClient 单例(#87:免每请求重建 SSL 上下文、绕进程代理) +│ ├── withdraw_reconcile_worker.py # 提现对账 worker(lifespan 启动) +│ ├── heartbeat_monitor_worker.py # 无障碍心跳掉线检出 worker(#65,超时 1h #107) +│ └── daily_exchange_worker.py # 金币自动兑换 worker +├── utils/ # geo.py(离线经纬度→城市反查,~2.5M 行 CSV+KDTree,启动预热) +│ # + meituan_city.py(城市→美团 city_id,#116) +├── repositories/ # 数据访问 + 事务(28 个文件;comparison.py 含 harvest_running/done/abort 三段) +├── models/ # ORM 表结构(34 个文件,45 张业务表,见 database/OVERVIEW.md) +└── db/ # DeclarativeBase + engine/get_db(非 SQLite 启 pool 10+20) -alembic/ # 数据库迁移(versions/ 11+ 个迁移含 feedback_table / convert_dishes_jsonb / merge 等) -deploy/ # systemd(.service) + nginx(.conf) -secrets/ # 极光 RSA 私钥 / 微信支付证书(不入 git,仅 .gitkeep 占位) -scripts/ - ├── init_postgres.py # 一键 PG 初始化:建用户 + 建库 + 写 .env + 跑迁移(2026-05 新增,见已知 bug §10) - ├── migrate.sh # 单独跑 alembic upgrade head(部署/CI 用) - ├── reset_signin.py # 重置今日签到 - ├── reset_welfare.py # 重置福利数据 - ├── reconcile_withdraws.py # 提现对账 - └── sim_pangle_callback.py # 模拟穿山甲回调 -tests/ # pytest(auth / health / welfare / withdraw / ad_reward / coupon_proxy / compare_proxy) -run.sh # 本地启动脚本(自动先跑迁移再起服务) -docs/api/ # API 接口文档(索引 README + 一接口一文件) -docs/integrations/ # 集成层实现文档(SDK 签名/加解密/协议细节) -docs/database/数据库迁移.md # Alembic 迁移指南(如何建表/升级/新增迁移) -docs/database/postgres-migration.md # SQLite → PostgreSQL 切换指南(配套 scripts/init_postgres.py) +h5/ # H5 静态页(#89):mine 我的页 + shared bridge/api +alembic/ # 数据库迁移(98 个,含 14+ merge;单 head,见 §7) +deploy/ # systemd:app-server + admin 两服务;定时器 meituan-etl(选品 ETL)/ +│ # pangle-revenue(穿山甲收益,每天 10:30 #100)/daily-exchange;nginx 配置 +secrets/ # 极光 RSA 私钥 / 微信支付证书(不入 git) +scripts/ # init_postgres / migrate.sh / create_admin / 美团券 ETL(pull_meituan_coupons + +│ # load_meituan_coupon_tsv) / sync_pangle_revenue(#92) / publish_apk / +│ # reconcile_withdraws / reset_* / sim_pangle_callback / seed_mock_*(造数) +tests/ # pytest(外部集成全 monkeypatch,不打真 HTTP) +run.sh # 本地启动(钉定 .venv 解释器 #75,先迁移再起服务) +docs/api/ docs/database/ docs/integrations/ docs/guides/ # 文档(各自带索引) ``` > **命名说明**:`api/v1/` 的 `v1` 用于 URL 版本化(移动端无法强制即时升级,需新旧版本并存能力);`integrations` 装外部 SDK 集成、`repositories` 装数据访问、`core` 装基础设施,三者分离。**数据访问层统一在 `repositories/`**(早期叫 `crud/`,2026-05 已整体并入,`crud/` 不再存在)。`coupon.py` 是领券透传,勿与 `meituan.py` 里的 `coupons`(券列表)混淆。 @@ -190,15 +171,15 @@ POST /api/v1/auth/sms/login { phone, code } → 任意 6 位通过 → upsert ### 5.2 对外四个接口 -> 接口级入参/出参/各 tab 行为详见 [api/meituan-feed.md](./api/meituan-feed.md) 等,本节只讲后端形态。 +> 接口级入参/出参/各 tab 行为详见 [api/meituan/meituan-feed.md](./api/meituan/meituan-feed.md) 等,本节只讲后端形态。 - `coupons`:对外的搜索/榜单接口(底层 `query_coupon`),**客户端暂未接入**。 - `feed`:首页推荐流,**已是多 tab**(入参 `tab`): - - `rec` 智能推荐:走**离线库 `meituan_coupon`**(筛佣金率≥3%、`DISTINCT ON` 去重、按销量降序分页),**纯库查询、不打美团、不依赖 MT 凭证**(实测同城热销中位佣金 ~0.8%,实时筛≥3% 每页剩 0–1 条又撞 402,故从库出)。不显示距离。 + - `rec` 智能推荐:走**离线库 `meituan_coupon`**(筛佣金率≥3%、`DISTINCT ON` 去重、按销量降序分页),**纯库查询、不打美团、不依赖 MT 凭证**(实测同城热销中位佣金 ~0.8%,实时筛≥3% 每页剩 0–1 条又撞 402,故从库出)。不显示距离。**#116 起按城市过滤**:设备经纬度经 `utils/geo`(离线反查,启动预热)+ `meituan_city` 映射成美团 `city_id`,只出同城券;拿不到坐标/城市 → 返空 + `status=degraded`。 - `distance` 距离最近:实时拉外卖+到店两路,按用户坐标由近及远。 - 默认(空 tab):旧的逐轮分页混合 feed(2 外卖 + 1 到店交叉,写死 3 页爆款/今日必推/精选+限时,第 4 页返空),**仅老客户端兼容**。 - 任何失败场景返 `200` + 空 `items` + `status=degraded`(不抛 5xx)。 -- `top-sales`:独立的销量榜接口,离线库 `meituan_coupon` 按销量降序 + 跨源去重,**不实时打美团**。 +- `top-sales`:独立的销量榜接口,离线库 `meituan_coupon` 按销量降序 + 跨源去重 + **同城过滤**(#116,同 rec 的城市反查),**不实时打美团**。 - `referral-link`:换推广链接,客户端取 `link_map["3"]`(deeplink)优先跳美团 App。 - `query_coupon`:底层取数(被 `coupons`/`distance` feed 共用),不直接对外。 @@ -212,7 +193,7 @@ POST /api/v1/auth/sms/login { phone, code } → 任意 6 位通过 → upsert ## 6. 领券透传(coupon/step) -产品"一键领券"的核心接入点,**真正的领券逻辑不在本服务**——在另一个 repo `pricebot-backend`(GoalEngine + 事件驱动)。但 `coupon/step` **不是纯透传壳**:它在转发 pricebot 之余,还**best-effort 写库**(纯透传壳是 `compare.py` 的 `intent/recognize`、`price/step`,它们只转发不写库)。 +产品"一键领券"的核心接入点,**真正的领券逻辑不在本服务**——在另一个 repo `pricebot-backend`(GoalEngine + 事件驱动)。但 `coupon/step` **不是纯透传壳**:它在转发 pricebot 之余,还**best-effort 写库**。(比价透传 `compare.py` 2026-07 起同样带落库副作用,见 §6.2——现在全站已没有"只转发不写库"的透传端点,只有 `trace/epilogue` 例外。) ``` 客户端 → POST /api/v1/coupon/step (任意 JSON body,含 device_id/trace_id/step) @@ -231,6 +212,29 @@ POST /api/v1/auth/sms/login { phone, code } → 任意 6 位通过 → upsert - **错误**:body 非合法 JSON → 400;pricebot 不可达或返回 5xx → 502。 - **配置**:`PRICEBOT_BASE_URL`(默认 `http://localhost:8000`)、`PRICEBOT_REQUEST_TIMEOUT_SEC`(默认 30s,因领券单帧最多 wait 6s)。 - **现状**:**前端已接通**——首页「去领取」→ `CouponPromptDialog` → 权限检查 → 无障碍引擎 `startCouponClaim` → 循环调本接口,逐张券下发 launch/wait/done。pricebot-backend 不在本目录。 +- **配套**:`POST /coupon/session`(#99)由客户端**独立两段上报**(发起建行/收尾更新)落 `coupon_session` 流水,供 admin「领券数据」看板算发起数/完成率/中途流失/耗时——与 step 透传链路解耦。 + +--- + +## 6.2 外卖比价透传:软鉴权 + trace_id 签发 + harvest 落库(2026-07) + +`compare.py` 的 `/intent/recognize`、`/intent/step`、`/intent/precoupon/step`、`/price/step`、`/trace/finalize`、`/trace/epilogue` 透传 pricebot 之余,**由 app-server 直接落库比价记录**(不再依赖客户端 POST /compare/record): + +``` +客户端首帧(可不带 trace_id) + → compare._forward:app-server 用 uuid 签发 trace_id、注入转发 body、回填响应顶层 + → [harvest_running] 仅 mint 那帧:按 trace_id 建 comparison_record 的 running 行 + → 后续帧原样透传(原始 bytes 快路,不再写库) + → done 帧(price/step)→ [harvest_done] 写 success/failed + 派生 best_*/saved_amount_cents + → 用户终止/未识别 → 客户端打 /trace/finalize → [harvest_abort] 写 cancelled/failed(不降级已 success) + → /trace/epilogue(#112):App 结果页截图透传入 trace,纯透传、不落库 +``` + +- **软鉴权(OptionalUser)**:新客户端带 JWT → 记录绑 `user_id`;老客户端不带 → `user_id` 暂空,由其后续带 JWT 的 `POST /compare/record` 上报补齐(灰度期两条写路径按 `trace_id` reconcile)。 +- **写库全 best-effort**:`run_in_threadpool` + 独立 SessionLocal,失败只 warning、绝不连累比价返回(同 coupon.py 口径)。 +- **trace_url** 从 pricebot 响应顶层取(pricebot 每帧都带);查看权限按 `user.debug_trace_enabled` 控制。 +- **邀请发奖不在 harvest**:#113 口径 = 好友「比价并下单」,发放在 `POST /order/report`(`invite.try_reward_on_compare`,`compare_reward_granted` 幂等闸,发**邀请奖励金**入独立账本)。 +- 透传基建:`core/pricebot_router.pick_pricebot`(按 trace_id 一致性 hash 选实例)+ `core/pricebot_client`(共享 httpx 单例,#87)。 --- @@ -240,13 +244,13 @@ POST /api/v1/auth/sms/login { phone, code } → 任意 6 位通过 → upsert **启动确认窗兜底样本**(2026-06):国产 ROM 打开别的 App 时弹"想要打开 XX"确认窗,pricebot 对没见过文案(繁体/英文/ROM 改版)的窗用 LLM 兜底放行后,把样本 POST 到 [`/internal/launch-confirm-sample`](../app/api/internal/launch_confirm.py) 落 `launch_confirm_sample` 表(host 包 + 弹窗树 + LLM plan + 设备 locale/机型),供研发定期人工沉淀回 pricebot 的规则 yaml(回到快路径)。**需 pricebot 与 app-server 两边 `.env` 配同一 `INTERNAL_API_SECRET` 才生效**(未配则 pricebot 侧跳过上报、不影响比价/领券)。 -> 同类内部回写端点(同走 `X-Internal-Secret`)还有:`/internal/price-observation`(价格观测)、`/internal/store-mapping` + `/store-mapping/lookup` + `/store-mapping/invalidate`(跨平台店铺映射沉淀/反查/失效)等 pricebot 比价资产沉淀;以及 **`/internal/app-version`(OTA)**——**发布流程**(非 pricebot)出 APK 后写最新版本号/下载链接/sha256 落 `app_config`,客户端再 `GET /api/v1/platform/app-version` 读做检查更新。完整 6 个内部端点见 [api/internal.md](./api/internal.md)。 +> 同类内部回写端点(同走 `X-Internal-Secret`)还有:`/internal/price-observation`(价格观测)、`/internal/store-mapping` + `/store-mapping/lookup` + `/store-mapping/invalidate`(跨平台店铺映射沉淀/反查/失效)等 pricebot 比价资产沉淀;`GET /internal/launch-confirm-samples`(#91,样本列表供 pricebot `distill_launch_confirm.py` 聚合沉淀回静态规则);以及 **`/internal/app-version`(OTA)**——**发布流程**(非 pricebot)出 APK 后写最新版本号/下载链接/sha256 落 `app_config`,客户端再 `GET /api/v1/platform/app-version` 读做检查更新。完整 7 个内部端点见 [api/internal/internal.md](./api/internal/internal.md)。 --- ## 7. 数据模型 -**完整表清单不在本文维护**(避免双份漂移)——共 **40 张业务表** + `alembic_version` 框架表,逐表字段级说明 + 跨表关系/写入路径见 **[database/OVERVIEW.md](./database/OVERVIEW.md)**(总览)与 [database/README.md](./database/README.md)(一表一文件索引)。生产 PG / 开发可回退 SQLite。 +**完整表清单不在本文维护**(避免双份漂移)——共 **45 张业务表** + `alembic_version` 框架表,逐表字段级说明 + 跨表关系/写入路径见 **[database/OVERVIEW.md](./database/OVERVIEW.md)**(总览)与 [database/README.md](./database/README.md)(一表一文件索引)。生产 PG / 开发可回退 SQLite。2026-06 下旬以来新增:`coupon_session`(#99)、`analytics_event`(#83)、`invite_cash_transaction`(#82)、`admin_role`(#117)、`ad_pangle_daily_revenue`(#92)。 本文只保留分层与高频维度的速查:数据访问统一在 `repositories/`,ORM 表结构在 `models/`(钱包/福利在 `wallet.py`/`signin.py`/`task.py`、比价在 `comparison.py`、领券今日状态在 `coupon_state.py`、CPS 群发在 `cps_*.py`、pricebot 内部沉淀在 `price_observation.py`/`store_mapping.py`/`launch_confirm_sample.py`、无障碍存活在 `device.py`)。下面单列最常用的 `user` 表字段。 @@ -265,15 +269,15 @@ POST /api/v1/auth/sms/login { phone, code } → 任意 6 位通过 → upsert `upsert_user_for_login`:phone 存在则更新 `last_login_at`,不存在则注册(注册即登录)。 -**Alembic 迁移**:`alembic/versions/` 当前约 **78 个迁移文件**(含 14 个合并迁移),当前单一 head `4dc2af7ebe74`(2026-06-23 合并 #65 device 链 + comparison token 列两条并行链)。多人/多分支并行改表频繁产生多 head,靠 `alembic merge` 收敛回单 head。规模随改表增长,**别背具体链**,以 `alembic history`/`alembic heads` 实时输出为准。详见 [数据库迁移.md](./database/数据库迁移.md)。 +**Alembic 迁移**:`alembic/versions/` 当前 **98 个迁移文件**(含 14+ 个合并迁移),当前单一 head `admin_user_pages_override`(2026-07-08,#126)。多人/多分支并行改表频繁产生多 head,靠 `alembic merge` 收敛回单 head。规模随改表增长,**别背具体链**,以 `alembic history`/`alembic heads` 实时输出为准。⚠️ revision id 长度 ≤32 字符(`alembic_version` 列宽,超长部署时截断报错——0.2.1 实踩)。详见 [数据库迁移.md](./database/数据库迁移.md)。 --- ## 8. 配置与部署 -配置见 `core/config.py`(pydantic-settings 读 `.env`)。分组:环境、`DATABASE_URL`(默认 SQLite,生产应切 PG)、JWT、极光(`JG_*`)、短信(`SMS_MOCK` 默认 true)、美团(`MT_CPS_*`)、**pricebot 上游(`PRICEBOT_BASE_URL` / `PRICEBOT_REQUEST_TIMEOUT_SEC=30` / `PRICEBOT_COMPARE_TIMEOUT_SEC=60`)**、**媒体存储(`MEDIA_ROOT=./data/media` / `MEDIA_URL_PREFIX=/media`)**、CORS。 +配置见 `core/config.py`(pydantic-settings 读 `.env`)。分组:环境、`DATABASE_URL`(默认 SQLite,生产应切 PG)、JWT(+独立 `ADMIN_JWT_SECRET`)、极光(`JG_*`)、短信(`SMS_MOCK` 默认 true)、美团(`MT_CPS_*`)、**pricebot 上游(`PRICEBOT_BASE_URL` / 多实例 `PRICEBOT_INSTANCES` / `PRICEBOT_REQUEST_TIMEOUT_SEC=30` / `PRICEBOT_COMPARE_TIMEOUT_SEC=60`)**、**内部密钥(`INTERNAL_API_SECRET`,internal 族)**、穿山甲(`PANGLE_*`,含收益 API)、微信支付(`WXPAY_*`)、**媒体存储(`MEDIA_ROOT=./data/media` / `MEDIA_URL_PREFIX=/media`)**、CORS;运营可覆盖的业务常量在 `core/config_schema.py` 定义、`app_config` 表落值(admin `GET/PATCH /config`)。 -**生产部署**:systemd `shaguabijia-app-server.service`(WorkingDirectory `/opt/shaguabijia-app-server`,`EnvironmentFile=.env`,uvicorn 监听 `127.0.0.1:8770`,`--workers 1`)+ nginx 443 反代 → 8770。**无 Docker**。**PostgreSQL 16** 同机部署。 +**生产部署(2026-06-06 起在 ecs1)**:双 systemd 服务 `shaguabijia-app-server.service`(uvicorn `127.0.0.1:8770`,`--workers 1`)+ `shaguabijia-admin.service`(8771),WorkingDirectory `/opt/shaguabijia-app-server`,`EnvironmentFile=.env`,nginx 443 反代 `app-api.shaguabijia.com` → 8770。**无 Docker**。**PostgreSQL 16** 同机部署。代码走 **git clone(main 分支)+ Gitea 只读部署密钥**,日常发布用服务器上的 `deploy` 一键命令 / `release.sh` 发车 / deploy.shaguabijia.com 发车台(不再 rsync 整目录;`secrets/` 私钥证书单独放置、不入 git)。 ```bash # 首次 PG 初始化(新机器或新环境): @@ -281,12 +285,13 @@ ssh server "sudo apt install -y postgresql-16 && sudo systemctl enable --now pos ssh server "cd /opt/shaguabijia-app-server && .venv/bin/python scripts/init_postgres.py" # 该脚本会建业务用户 + 建库 + 写 .env 的 DATABASE_URL + 跑 alembic upgrade head -# 后续日常部署: -rsync -avz --exclude='.venv' --exclude='__pycache__' --exclude='data' --exclude='secrets/*.pem' ./ server:/opt/shaguabijia-app-server/ -scp secrets/jverify_rsa_private.pem server:/opt/shaguabijia-app-server/secrets/ # 私钥单独传,不入 git -ssh server "cd /opt/shaguabijia-app-server && .venv/bin/alembic upgrade head && systemctl restart shaguabijia-app-server" +# 日常部署(deploy 一键命令等价动作): +ssh server "cd /opt/shaguabijia-app-server && git pull && .venv/bin/alembic upgrade head \ + && systemctl restart shaguabijia-app-server shaguabijia-admin" ``` +**systemd 定时器**(`deploy/`,各带 .md 运维手册):`meituan-etl`(美团 CPS 选品 ETL → `meituan_coupon`,rec/top-sales 数据源)、`pangle-revenue`(穿山甲 GroMore 后台收益拉取,每天 10:30,#100)、`daily-exchange`(金币自动兑换;lifespan 里另有 in-process worker,两套并存以部署材料为准)。 + 完整 PG 切换流程见 [docs/database/postgres-migration.md](./database/postgres-migration.md)。 **生产 checklist(均为上线必查)**: @@ -325,8 +330,8 @@ conda activate price # 首次:pip install -e . | logout 无服务端失效 | 靠客户端清 token;后续加 jti 黑名单表(注销账号也是同问题——软删后旧 token 仍能用到自然过期) | | 美团接口无鉴权 + sid 可覆盖 | 评估加鉴权/锁定 sid(注意首页要求未登录可见) | | 美团接口未配凭证降级 | 未配 `MT_CPS_APP_KEY` 时 3 端点返空(不报 502),`/feed` 跟"已配但调用失败"路径无法区分——见 [integrations/meituan](./integrations/meituan.md) | -| 领券/比价依赖 pricebot | `coupon/step` / `intent/recognize` / `price/step` 仅透传,真正逻辑在 pricebot-backend;前端已接通领券链路,比价 food MVP 也已接通 | -| agent 系列接口 MVP 不鉴权 | 拿不到 user_id → 无法采集"哪个用户领了/买了什么"用户级画像(商业模式核心资产)。见 [待办与技术债.md](./guides/待办与技术债.md) P1 | +| 领券/比价依赖 pricebot | 执行核心在 pricebot-backend;比价透传 2026-07 起带 harvest 落库(§6.2),领券 `coupon/step` 带三表副作用,均非纯壳 | +| 领券 step 仍不鉴权;比价已软鉴权 | 比价透传族改 OptionalUser:带 JWT 即绑 `user_id`(用户级画像已能落到比价记录);`coupon/step` 仍拿不到 user_id(device 维度),老客户端比价记录靠 `/compare/record` 兜底补绑。见 [待办与技术债.md](./guides/待办与技术债.md) | | SMS 已接极光(2026-06-03) | real 模式自定义验证码,上线只需 `SMS_MOCK=false`(复用极光凭证)。详见 [integrations/sms](./integrations/sms.md) | | 短信冷却存内存 | 扩 worker 前需迁移到 Redis | | `MEDIA_ROOT` 进程内 serve | 头像/反馈截图当前用 FastAPI StaticFiles,生产建议 nginx 直 serve 该目录 |