feat(admin): 首页门面数字保底/护栏 + 轮播种子真实化与批量 + 比价记录索引 (#42)

- ops_stat: 真实值 offset→保底(max);初始基数过只增不减护栏
- ops_marquee: 脱敏名手机尾号+中文昵称混合;金额长尾;时间随机抖动;真实+种子不足兜底补满;真实记录查询 ~30s 缓存
- comparison_record: 复合索引 (status, created_at) + 迁移(PG CONCURRENTLY);修复 alembic 多 head(merge 改挂 onboarding_completion)
- 轮播种子批量删除/启用接口 + ids 上限;docs 同步
- 附带并行会话 WIP:admin users / wallet

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

---------

Co-authored-by: OuYingJun1024 <1034284404@qq.com>
Reviewed-on: #42
Co-authored-by: ouzhou <ouzhou@wonderable.ai>
Co-committed-by: ouzhou <ouzhou@wonderable.ai>
This commit was merged in pull request #42.
This commit is contained in:
ouzhou
2026-06-10 22:26:51 +08:00
committed by marco
parent a753843804
commit db399a40fa
20 changed files with 399 additions and 66 deletions
+2 -2
View File
@@ -27,7 +27,7 @@
| `random_kind` | string | 自增长方式:`mult` ×倍率 / `add` +绝对增量 |
| `random_step_min` | int | add 模式增量下限(基础单位) |
| `random_step_max` | int | add 模式增量上限(基础单位) |
| `real_offset` | int | real 模式基数偏移(基础单位):展示 = 真实值 + 偏移 |
| `real_offset` | int | real 模式保底值(基础单位):展示 = max(真实值, 保底值)。字段名沿用 real_offset(语义已改为"保底" |
| `allow_decrease` | bool | 是否允许展示值下降(默认 false = 只增不减) |
| `random_current` | int \| null | 当前展示值(所有模式,基础单位) |
| `random_last_tick_at` | string \| null | 上次 tick 时刻(ISO |
@@ -52,7 +52,7 @@
| `random_kind` | string | 自增长方式:`mult` / `add` |
| `random_step_min` | int | add 增量下限(基础单位,≥0) |
| `random_step_max` | int | add 增量上限(基础单位,≥下限) |
| `real_offset` | int | real 基数偏移(基础单位,≥0 |
| `real_offset` | int | real 保底值(基础单位,≥0:展示 = max(真实值, 保底值) |
| `allow_decrease` | bool | 允许展示值下降(默认 false=只增不减) |
| `random_initial` | int | 切 random 的初始基数(基础单位);**留空则用当前真实值播种** |
| `apply_now` | bool | 立即更新:不等更新钟点,保存后马上刷新一次(random 走一档,real/manual 取应有值,均经护栏) |
+11 -1
View File
@@ -31,6 +31,16 @@
- 入参 `OpsMarqueeSeedBulkCreate`:`count`(1~200)、`min_cents``max_cents``enabled`(默认 true)
- 出参 `200`:`list[OpsMarqueeSeedOut]`(新建的 count 条)。`400`=数量 / 金额非法。
## POST /admin/api/marquee-seeds/batch-delete — 批量删除(带审计)
对选中的多条种子一次删除(一个事务 + 一条审计)。
- 入参 `OpsMarqueeSeedBatchDelete`:`ids`(int 数组;空数组 = no-op;不存在的 id 自动跳过)
- 出参 `200`:`{"deleted": <实际删除条数>}`
## POST /admin/api/marquee-seeds/batch-enable — 批量启用 / 停用(带审计)
对选中的多条种子一次置启用态。
- 入参 `OpsMarqueeSeedBatchEnable`:`ids`(int 数组)、`enabled`(bool)
- 出参 `200`:`{"updated": <实际更新条数>}`
## PATCH /admin/api/marquee-seeds/{seed_id} — 改(带审计)
入参 `OpsMarqueeSeedUpdate`(均可选):`masked_user` / `min_cents` / `max_cents` / `enabled` / `sort_order`
- `masked_user` 约定:**不传**=不改;**传空串**=清空(改回随机合成);**传值**=固定该名。
@@ -41,5 +51,5 @@
出参 `200`:`{"deleted": true}``404`=种子不存在。
## 说明
- 写操作需 operator(super 恒可),均写 `admin_audit_log`(action=`ops_marquee_seed.create` / `update` / `delete` / `bulk_generate`)。预览为只读,任意已登录 admin 可用。
- 写操作需 operator(super 恒可),均写 `admin_audit_log`(action=`ops_marquee_seed.create` / `update` / `delete` / `bulk_generate` / `batch_delete` / `batch_enable`)。预览为只读,任意已登录 admin 可用。
- 改动客户端**进首页 / 回前台**重拉 feed 时生效。
+6 -4
View File
@@ -15,13 +15,15 @@
| 字段 | 类型 | 说明 |
|---|---|---|
| `items` | list | 轮播条目数组(最多 `limit` 条) |
| `items[].masked_user` | string | 脱敏用户名,`用户********a52` |
| `items[].masked_user` | string | 脱敏用户名,手机尾号(`138****5678`)/ 中文昵称(`省钱**`)混合风格 |
| `items[].saved_amount_cents` | int | 节省(分),客户端 ÷100 显示「x.xx 元」 |
| `items[].time` | string | 北京时间 `HH:MM:SS` |
## 说明
- 真实条:`comparison_record``status='success'``0 < saved_amount_cents ≤ 300 元` 的近期记录(金额超 300 元视为异常 / bug 值剔除,防「节省 999 元」穿帮),**按 `user_id` 去重**(同一用户只取最新一条,避免单人刷屏);用户名按 `user_id` 哈希脱敏(`用户********`+3 位)。
- 种子条:`ops_marquee_seed`(`enabled=true`),仅在真实去重后不足 `limit` 时补齐;**从启用种子中公平随机抽取**(不再固定取前 N,所有种子都有机会露出)。每条种子:用户名留空则按脱敏格式**随机合成并避开同屏撞名**,金额在 `[min_cents, max_cents]` 区间**随机值**(固定金额则 min==max)。
- **`time` 为合成的「最近」时间**(从当前北京时间往前递减,首条约 20 秒前,其余每条 2~6 分钟):社会证明轮播保证永远像刚发生,不受旧测试数据 / 低谷期记录影响。真实的用户/金额不变,只换展示时间
- 真实条:`comparison_record``status='success'``0 < saved_amount_cents ≤ 300 元` 的近期记录(金额超 300 元视为异常 / bug 值剔除,防「节省 999 元」穿帮),**按 `user_id` 去重**(同一用户只取最新一条,避免单人刷屏);用户名按 `user_id` **确定性合成**手机尾号 / 中文昵称混合脱敏名(同用户恒定)。
- 种子条:`ops_marquee_seed`(`enabled=true`),仅在真实去重后不足 `limit` 时补齐;**从启用种子中公平随机抽取**(不再固定取前 N,所有种子都有机会露出)。每条种子:用户名留空或旧 `用户****xxx` 模板名则**随机合成混合风格名并避开同屏撞名**(自愈历史种子),金额在 `[min_cents, max_cents]` 取**长尾随机值**(小额居多、偶尔大额,固定金额则 min==max)。
- 兜底:真实 + 种子仍凑不满 `limit`(种子被全停用 / 太少)时,用**内置合成条补满**,保证轮播既不空也不稀疏
- **`time` 为合成的「最近」时间**(从当前北京时间往前**随机抖动**递减,首条几十秒前,其余多数 1~5 分钟、偶尔扎堆或较长):社会证明轮播保证永远像刚发生且节奏自然不机械,不受旧测试数据 / 低谷期记录影响。真实的用户/金额不变,只换展示时间。
- 因含随机(抽取 / 金额 / 合成名),**每次请求结果都不同**——这是轮播想要的鲜活感。
- **性能**:首页人人都看、真实数据变化慢,真实记录原始行带 **~30s 进程内缓存**(展示层随机仍每次重算,只省 DB 往返;新记录最多晚 30s 进轮播)。查询走复合索引 `ix_comparison_status_created (status, created_at)`,避免随数据量增大全表扫。
- 逻辑见 `app/repositories/ops_marquee.py`;运营管理种子见 [admin-marquee-seeds](./admin-marquee-seeds.md)。
+1 -1
View File
@@ -18,7 +18,7 @@
## 说明
三指标各自独立选模式,且**统一「定时刷新」**:客户端看到的展示值只在「更新时间(按更新间隔)」对齐的北京钟点(`anchor + k*间隔`)刷新一次,平时不变。每次刷新按模式算新值:
- **real**:重新快照查库 + 基数偏移`help_users`=有过 `status='success'` 比价记录的去重用户数;`total_compares`=成功比价记录数;`total_saved_cents`=成功记录 `saved_amount_cents` 求和(**只计 `0<saved≤300 元` 的条目**,剔除 bug 异常大值);最终展示 = 真实值 + 运营配的 `real_offset`
- **real**:重新快照查库,再与保底值取大`help_users`=有过 `status='success'` 比价记录的去重用户数;`total_compares`=成功比价记录数;`total_saved_cents`=成功记录 `saved_amount_cents` 求和(**只计 `0<saved≤300 元` 的条目**,剔除 bug 异常大值);最终展示 = `max(真实值, 运营配的 real_offset 保底值)`
- **manual**:取运营当前手填的固定值(改值到下个更新钟点生效)。
- **random**:在上次展示值上按方式增长——`mult` ×一个 [1.0,1.5] 随机倍率 / `add` +一个 [下限,上限] 随机绝对增量(跨几个钟点走几次)。
- **只增不减**:默认门面数字不回退(real/manual 新值低于当前则保持),除非运营开「允许下降」。
+2 -2
View File
@@ -2,13 +2,13 @@
> 模型 `app/models/ops_marquee_seed.py` | 关联接口 [platform-savings-feed](../api/platform-savings-feed.md) / [admin-marquee-seeds](../api/admin-marquee-seeds.md) | [← 表索引](./README.md)
首页「用户****xxx 比价后节省 xx 元」轮播的兜底假数据。轮播真实数据源是全平台比价记录,真实不足 N 条时用本表启用的种子补齐「混播」,保证轮播不空。种子是「生成规则」而非死记录:用户名可空(→随机合成)、金额是区间(→随机取值)。运营后台增删改。逻辑见 `app/repositories/ops_marquee.py`
首页「XXX 比价后节省 xx 元」轮播的兜底假数据。轮播真实数据源是全平台比价记录,真实不足 N 条时用本表启用的种子补齐「混播」,保证轮播不空。种子是「生成规则」而非死记录:用户名可空(→随机合成手机尾号/中文昵称混合名)、金额是区间(→随机取值)。运营后台增删改。逻辑见 `app/repositories/ops_marquee.py`
## 字段
| 列 | 类型 | 约束 / 默认 | 说明 |
|---|---|---|---|
| `id` | Integer | PK, autoincrement | |
| `masked_user` | String(64) | NULL 可空 | 脱敏用户名,`用户********a52`(整串存)。**留空 → feed 展示时按脱敏格式随机合成**(`用户********`+3 位 hex,避开同屏撞名),风格与真实条一致、永不穿帮 |
| `masked_user` | String(64) | NULL 可空 | 脱敏用户名,手机尾号(`138****5678`)/ 中文昵称(`省钱**`)风格(整串存)。**留空或旧 `用户****xxx` 模板名 → feed 展示时随机合成混合风格名**(避开同屏撞名、自愈历史种子),风格与真实条一致、永不穿帮 |
| `min_cents` | Integer | NOT NULL | 节省金额区间下限(分) |
| `max_cents` | Integer | NOT NULL | 节省金额区间上限(分);feed 每次在 `[min,max]` 随机取值,**固定金额则 min==max** |
| `enabled` | Boolean | NOT NULL, default true | 停用的不参与混播 |
+2 -2
View File
@@ -21,7 +21,7 @@
| `random_kind` | String(8) | NOT NULL, default `mult` | 自增长方式:`mult` ×倍率 / `add` +绝对增量 |
| `random_step_min` | Integer | NOT NULL, default 0 | add 模式增量下限(基础单位) |
| `random_step_max` | Integer | NOT NULL, default 0 | add 模式增量上限(基础单位) |
| `real_offset` | Integer | NOT NULL, default 0 | real 模式基数偏移(基础单位):展示 = 真实值 + 偏移 |
| `real_offset` | Integer | NOT NULL, default 0 | real 模式保底值(基础单位):展示 = max(真实值, 保底值)。列名沿用 real_offset(语义已从"偏移"改为"保底") |
| `allow_decrease` | Boolean | NOT NULL, default false | 是否允许展示值下降(默认 false = 只增不减) |
| `updated_by_admin_id` | Integer | nullable | 最后修改的管理员 |
| `updated_at` | DateTime(tz) | server_default now(), onupdate now() | 更新时间 |
@@ -39,7 +39,7 @@
`random_current` 现是**所有模式**的「当前展示值」(不止 random),用户侧 `/stats` 直接返回它。
- 触发边界 = 北京时间 `anchor + k*interval`(interval=`random_tick_seconds`,anchor=`random_anchor_minutes*60`,sub-day 间隔取 `anchor % interval` 作相位)。例:每天 09:00 / 每小时 :30 / 每 5 分刻度。
- 读取(`get_display_values`/`get_config`)时跑 `_refresh`:统计 `random_last_tick_at``now` 跨过几个边界 N,N≥1 才刷新:
- **real** → `random_current` = 重新查库的真实值 + `real_offset`(跨多少边界都只取最新),**经只增不减护栏**。
- **real** → `random_current` = `max(重新查库的真实值, real_offset 保底值)`(跨多少边界都只取最新),**经只增不减护栏**。
- **manual** → `random_current` = 当前 `manual_value`,**经只增不减护栏**。
- **random** → 按 `random_kind` 走 N 个周期:`mult` 连乘随机倍率(`randint(min,max)/1000`,恒 ≥1.0)/ `add` 连加随机增量(`randint(step_min,step_max)`,≥0)。天然只增。
- **只增不减护栏**(`allow_decrease=false`,默认):real/manual 刷新时若新目标值 < 当前 `random_current`,保持当前值不回退(门面忌缩水)。开 `allow_decrease` 才允许下降。