# cps_activity — CPS 可推广活动池(运营预存的券/物料)

> 模型 `app/models/cps_activity.py` · 仓库 `app/admin/repositories/cps.py`(`create_activity` / `list_activities` / `get_activity` / `delete_activity` / `list_activity_images`) · 接口 admin `POST /admin/api/cps/activities`、`GET /admin/api/cps/activities`、`DELETE /admin/api/cps/activities/{id}`、`POST /admin/api/cps/upload-image`、`GET /admin/api/cps/activity-images` · [← 索引](./README.md) · [总览](./OVERVIEW.md)

运营把常推的活动(券)预存成池,生成群发链接时从池里挑,省得每次记物料 ID。数据**全由运营在后台手填/上传**,无外部同步。在 CPS 群发漏斗里它是**最上游的"卖什么"**:活动(本表)× 群([`cps_group`](./cps_group.md))经"批量生成短链"组合出一条条 [`cps_link`](./cps_link.md)。一条活动可被多个群、多次复用生成 link(一对多,无外键)。

## 用在哪 / 增删改查
- **C(插入)**:`POST /admin/api/cps/activities`(`create_activity`,需 `operator` 角色)。按 `platform` 分支校验:`meituan` 必须填 `act_id` 或 `product_view_sign` 之一;`taobao`/`jd` 必须填 `payload`,且 `taobao` 还必须有 `image_url`(落地页图)。淘宝图经 `POST /admin/api/cps/upload-image`(`media.save_cps_image`)上传得绝对 URL,或经 `GET /admin/api/cps/activity-images` 选已有。无幂等键,重复提交建多条。
- **U(更新)**:**无更新端点**。改活动 = 删了重建。`status`(active/archived)列存在但目前没有切换它的路由(待确认是否前端规划)。
- **R(读)**:`GET /admin/api/cps/activities`(`list_activities`,游标分页,可按 `platform`/`status` 过滤,登录即可);生成 link 时 `get_activity` 按 id 取;淘宝落地页渲染时 `cps_redirect` 用 `db.get(CpsActivity, link.activity_id)` 取 `image_url`;`list_activity_images` 取所有 distinct 非空 `image_url` 供新建复用。
- **D(删除)**:`DELETE /admin/api/cps/activities/{id}`(`delete_activity`,`operator`)。**硬删,不级联** `cps_link`——已发出去的短链继续可用;淘宝落地页若取不到活动(被删),`cps_redirect` 用兜底默认图 `/media/taobao_landing.jpg`。

## 字段
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|---|---|---|---|
| `id` | Integer | PK, autoincrement | 被 `cps_link.activity_id` 语义引用(非 FK) |
| `platform` | String(20) | NOT NULL, default `meituan` | `meituan` / `taobao` / `jd`。决定转链/落地方式,也决定下面哪些字段必填 |
| `name` | String(128) | NOT NULL | 活动名,运营自定,后台列表/审计展示 |
| `act_id` | String(64) | nullable | **美团**联盟「我要推广-活动推广」第一列的物料 ID(actId)。转链 `get_referral_link` 入参 |
| `product_view_sign` | String(128) | nullable | **美团**商品券 productViewSign,与 `act_id` 二选一转链(多数活动用 `act_id`) |
| `payload` | Text | nullable | **淘宝**=整段淘口令文本(落地页原样供复制);**京东**=推广链接(落地页 302 跳)。美团不用此列(用 `act_id`)。生成 link 时直接作 `target_url` |
| `image_url` | String(512) | nullable | **淘宝**落地页主视觉图,存绝对 URL(基于 `CPS_REDIRECT_BASE`,跨域 admin-web 可显示)。落地页按活动展示对应图 |
| `status` | String(20) | NOT NULL, default `active` | `active` / `archived`(目前无切换入口,主要靠删) |
| `remark` | String(256) | nullable | 运营备注 |
| `created_at` | DateTime(tz) | server_default now(), NOT NULL | 创建时间。**无 `updated_at`**(本表无更新路径) |

## 关系 / Join Key
- `id` ← `cps_link.activity_id`(一活动多 link,**语义关联,无 ForeignKey**;删活动不级联删 link)。
- `id` ← `cps_redirect` 渲染淘宝落地页时按 `link.activity_id` 反查本表取 `image_url`。
- 与 [`cps_group`](./cps_group.md) 无直接列关系:两者在"批量生成短链"(`generate_referral_links`)时按 **platform 是否在群范围内**组合,产物是 [`cps_link`](./cps_link.md)。

## 索引与约束
- PK `id`。
- **无唯一约束、无额外索引、无外键**(运营低频、量级小,按 id 主键访问即可)。

## 注意
- **`platform` 是多平台演进的核心**:初版(commit `277f9b1`)只有美团,字段就是 `act_id` + `product_view_sign`;`3a40f61` 接淘宝/京东时**复用本表**,只加了 `payload`(异构标识塞一列);`d55f47f` 再加 `image_url` 把淘宝落地页图"做活"(此前是写死一张图)。所以三平台的"卖点"挤在同一张表,靠 `platform` 分流——这是省表的刻意取舍。
- **美团二选一**:`act_id` 与 `product_view_sign` 对应 `get_referral_link` 的两种转链入参,router 层强制至少填一个。
- **淘宝必须有图**:`taobao` 落地页是我们自渲染的 H5(`cps_redirect._taobao_landing_html`),没图页面就空,故建活动时强制。`d55f47f` 的迁移把存量淘宝活动的 `image_url` 回填成此前写死的 `/media/taobao_landing.jpg`,无缝衔接。
- **`image_url` 存绝对 URL 的原因**:落地页域(`CPS_REDIRECT_BASE`)与 admin-web 跨域,存绝对值两边都能取(`media.to_abs_media_url`)。dev 未配 `CPS_REDIRECT_BASE` 时退化为相对路径。
