feat(cps): 活动支持编辑(PATCH /activities/{id})

- CpsActivityUpdate schema + repo update_activity(部分更新,非 None 覆盖)
- router 按「合并后最终值」校验平台必填项(同新建口径),写审计 cps.activity.update
- 可改名/平台/对应字段/落地页图/备注/状态

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 22:13:35 +08:00
parent c78cfc838f
commit 4ee6de2548
34 changed files with 288 additions and 76 deletions
+15 -12
View File
@@ -4,7 +4,7 @@
领券(优惠券自动化)联动产生的三张「今日状态」表,都挂在领券透传端点 `POST /api/v1/coupon/step` 这条链路上(pricebot 跑领券,结果回 app-server 落库;**领券过程本身在 pricebot 内存态跑、不落库**)。三表各管一件事:
- **`coupon_prompt_engagement`** — 弹窗频控源。按 `(device, 自然日)` 记「今天是否对领券引导窗表达过**意向**」(点「一键领取」=`claim_started` / 点拒绝关闭=`dismissed` 都算)。切到外卖 App 时据此决定弹不弹:今天 engage 过就不再弹。
- **`coupon_prompt_engagement`** — 弹窗频控源。按 `(device, App 包名, 自然日)` 记「今天**这个 App** 是否对领券引导窗表达过**意向**」(弹出即记 `shown` / 点「一键领取」=`claim_started` / 点拒绝关闭=`dismissed` 都算)。切到外卖 App 时据此决定弹不弹:今天**该 App** engage 过就不再弹该 App。频控维度自 2026-06-14 起含 `package`,美团/淘宝/京东各自独立、互不压制
- **`coupon_daily_completion`** — 首页置灰源。按 `(device, 自然日)` 记「今天是否已**跑完整轮**领券(到 done 帧)」。首页「去领取」卡据此置灰:今天跑完了就不能再领。
- **`coupon_claim_record`** — 资产沉淀层。按 `(device, 券, 自然日)` 记每张券的领取结果(success/already_claimed/failed/skipped),**纯沉淀**(资产/画像/排查/CPS 归因),当前**不参与**「要不要领 / 弹不弹」的判断。
@@ -19,35 +19,38 @@
---
## coupon_prompt_engagement — 弹窗频控(今日是否已对引导窗表达意向)
## coupon_prompt_engagement — 弹窗频控(今日这个 App 是否已对引导窗表达意向)
`(device_id, engage_date)` 唯一,一台设备一天一条;今天 engage 过(领或拒)就不再弹。
`(device_id, package, engage_date)` 唯一,一台设备、一个 App、一天一条;今天**这个 App** engage 过(弹/领/拒)就不再弹该 App。各 App 独立:美团弹过不压淘宝/京东
### 用在哪 / 增删改查
- **C / U(幂等 upsert**`mark_engagement`条触发:
- **C / U(幂等 upsert**`mark_engagement`条触发:
- `POST /api/v1/coupon/prompt/shown`(引导窗弹出那刻上报)→ 记 `shown`(频控主判据:弹出即占用今天这个 App 的「一次」,领/拒/无视都算用掉);
- `POST /api/v1/coupon/step``step==0`(领券首帧=用户已发起领券)→ 记 `claim_started`
- `POST /api/v1/coupon/prompt/dismiss`(用户点关闭引导窗;server 在透传链路看不到「拒绝」,必须客户端通知)→ 记 `dismissed`
- 已有今天那条则覆盖 `engage_type`(并补 `user_id`),否则插入。
- **D**`POST /api/v1/coupon/prompt/reset``reset_today_engagement`)—— 删这台设备今天那条,开发设置「重置今日领券弹窗状态」按钮调,测频控用;删后今天又能弹。
- **R**`GET /api/v1/coupon/prompt/should-show?device_id=…``has_engaged_today`)→ `should_show = not 今天已 engage`。客户端切外卖 App 前查,纯后台判据
- 已有今天那条则覆盖 `engage_type`(并补 `user_id`),否则插入。`step` 帧的 `package` 取 step body 的 `package`,缺/空则退化为 `""` 占位(全局态)。
- **D**`POST /api/v1/coupon/prompt/reset``reset_today_engagement`)—— 删这台设备今天**所有 App** 的 engagement(不按 package 过滤:重置=从头测,清全部 App 最符预期),开发设置「重置今日领券弹窗状态」按钮调;删后今天又能弹。
- **R**`GET /api/v1/coupon/prompt/should-show?device_id=…&package=…``has_engaged_today`)→ `should_show = not 今天该 App 已 engage`。客户端切外卖 App 前`package` 查(老客户端不带 → `""` 全局态)
### 字段
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|---|---|---|---|
| `id` | Integer | PK, autoincrement | |
| `device_id` | String(64) | NOT NULL | 判断/聚合维度;客户端 `getOrCreateDeviceId`,重装会变 |
| `package` | String(64) | NOT NULL, server_default `""` | 触发弹窗的目标 App 包名(`com.sankuai.meituan` / `com.taobao.taobao` / `com.jingdong.app.mall`)。频控维度,各 App 独立。旧行(2026-06-14 改造前)无此值 → 迁移用占位 `""` 填 |
| `user_id` | Integer | index, 可空 | 登录态有就记(资产);不进唯一键、不阻塞判断 |
| `engage_date` | **Date** | NOT NULL | **北京时间**自然日(`today_cn()` |
| `engage_type` | String(16) | NOT NULL | `claim_started`(点一键领取)/ `dismissed`(点拒绝关闭);仅记录区分,**判断只看「今天有没有这条」,type 不影响弹不弹** |
| `engage_type` | String(16) | NOT NULL | `shown`(自动弹出即记)/ `claim_started`(点一键领取)/ `dismissed`(点拒绝关闭);仅记录区分,**判断只看「今天这个 App 有没有这条」,type 不影响弹不弹** |
| `created_at` | DateTime(tz) | server_default now() | |
| `updated_at` | DateTime(tz) | server_default now(), onupdate now() | |
### 索引与约束
- PK `id`index `user_id`UNIQUE(`device_id`, `engage_date`) = `uq_coupon_engage_device_date`(一台设备一天一条)。
- PK `id`index `user_id`UNIQUE(`device_id`, `package`, `engage_date`) = `uq_coupon_engage_device_pkg_date`(一台设备、一个 App、一天一条)。
- 历史:原为 UNIQUE(`device_id`, `engage_date`) = `uq_coupon_engage_device_date`2026-06-14 迁移 `coupon_engage_per_package` 加入 `package` 维度改成现状(修「任一 App 弹过就把整台设备当天标记、压住其余 App」的 bug)。
### 注意
- `device_id` 重装会变 → 重装当新设备,今天重新弹一次(产品预期)。
- 判断只看「今天这台设备有没有这条」,不看 `engage_type`领或拒都算 engage 过、都不再弹)。
- 判断只看「今天这台设备这个 App 有没有这条」,不看 `engage_type`弹/领/拒都算 engage 过、都不再弹该 App)。
---
@@ -57,7 +60,7 @@
### 用在哪 / 增删改查
- **C / U(幂等 upsert**`mark_completed_today`,由 `POST /api/v1/coupon/step` 在 pricebot 返回 `action.command == "done"` 那帧调。pricebot 把中途单券 done 改写成 `wait+continue=true`,只有整套全跑完那帧才保留 `command=="done"`,故 **done 已等价「整轮完成」**。已有今天那条则补 `user_id`/`trace_id`,否则插入。
- **U / D**:无业务删除
- **D**`POST /api/v1/coupon/completed-today/reset``reset_today_completion`)—— 删这台设备今天那条,首页「去领取」卡恢复可点。与 `/prompt/reset` 配套,开发设置「重置今日领券弹窗状态」一键把今日状态全清;MVP 不鉴权
- **R**`GET /api/v1/coupon/completed-today?device_id=…``has_completed_today`)→ `completed`。客户端据此把首页「去领取」卡置灰、不可点。
### 字段
@@ -119,5 +122,5 @@
## 三表共性小结
- 数据流向:客户端 → `POST /api/v1/coupon/step`(透传给 pricebot)→ 结果回写这三张表(best-effort,写库失败不影响领券)。
- 唯一键都含 `device_id` + 某个北京自然日列;`user_id` 永远是可空旁路(资产留痕,不进唯一键、不阻塞判断)。
- 唯一键都含 `device_id` + 某个北京自然日列engagement 还含 `package`,按 App 频控)`user_id` 永远是可空旁路(资产留痕,不进唯一键、不阻塞判断)。
- 无硬外键:`user_id` 软指 `user.id``trace_id` 软指 pricebot work_logs(详见 [OVERVIEW → 表间关系 & Join Key](./OVERVIEW.md))。