update
This commit is contained in:
161
docs/superpowers/specs/2026-09-21-street-main-detail-design.md
Normal file
161
docs/superpowers/specs/2026-09-21-street-main-detail-design.md
Normal file
@ -0,0 +1,161 @@
|
||||
# 街拍主副图设计
|
||||
|
||||
- 日期:2026-09-21
|
||||
- 状态:设计已确认,待用户审阅规格
|
||||
- 范围:街拍图增加「主图 / 副图」分组——审核页人工指定、详情页按组折叠、公开 API 保守扩展
|
||||
- **不含**:重复检测(见 `2026-09-21-duplicate-review-design.md`)、来源幂等(见 `2026-09-20-ingest-idempotency-design.md`)
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
**现状**:街拍图是扁平列表。`street_snap_draft_images` / `street_snap_images` 都**没有** `is_detail` 之类字段,而走秀侧(`brand_runway_draft_images` / `brand_runway_images`)**已经有**「主图 + 细节图」模型(`is_detail` + `look_index`)。
|
||||
|
||||
**动机**(用户原话):一个人可能被拍多张,平铺会很占位置。
|
||||
|
||||
**为什么只能人工指定**:源站不提供任何人物/分组信息。实测 theImpression 一篇文章:
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| `<figure>` 数量 | 167(就是街拍图本身) |
|
||||
| `<figcaption>` 数量 | **0** |
|
||||
| 街拍图的 `alt` | **全空**(`alt=""`) |
|
||||
| 非空 `alt` | 仅 11 个,且全是侧栏广告与无关标题(如 `Mugler Fall 2026 Ad Campaign`) |
|
||||
|
||||
(文件名含摄影师帧号如 `milano-str-f26-0007-1`,帧号接近可能是连拍,但对「同一个人不同时间被拍」无效,不采用。)
|
||||
|
||||
---
|
||||
|
||||
## 2. 决策记录
|
||||
|
||||
| 决策点 | 结论 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| 主副如何确定 | **人工在审核页指定** | 源站无人物信息;用户选择纯人工 |
|
||||
| 交互方式 | **不引入 JavaScript**:两阶段「先选定主图 → 勾选图片并入」+ 每张图的「并入上一张」快捷 | 后台目前**零 JS**(全仓 `internal/handler` 无 `<script>`/`fetch`,全是同步表单 POST);且拖拽需同屏看见起止点,无法处理「第 200 张是第 1 张的副图」这类跨屏场景(用户已指出该问题) |
|
||||
| 当前主图的保持方式 | 通过 **URL 查询参数 `?main=<imgID>`** 传递 | 零 JS 下无状态、可刷新、可后退、可加书签 |
|
||||
| 归属关系存储 | `is_detail` + **`parent_image_id`**(存所属主图的**行 id**,不存序号) | 序号会因重排/插入/删除而失效,存 id 稳定 |
|
||||
| 详情页渲染 | **按组折叠** | 用户选定 |
|
||||
| 公开 API | **保守扩展**:保留现有扁平 `images` 字段不变,**新增**分组字段 | 直接改结构会打断前端;新增字段让前端择期迁移 |
|
||||
| `image_count` 语义 | **不变**(仍计全部图) | 改语义会同时改动公开列表卡片的「N 张」,需要前端同步,另议 |
|
||||
| runway 侧 | 不受影响 | 已有自己的主/细节模型 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 详细设计
|
||||
|
||||
### 3.1 数据模型
|
||||
|
||||
`street_snap_draft_images` 与 `street_snap_images` 各新增两列:
|
||||
|
||||
```sql
|
||||
is_detail smallint not null default 0 -- 0=主图 1=副图
|
||||
parent_image_id integer not null default 0 -- 副图指向所属主图的「行 id」;主图为 0
|
||||
```
|
||||
|
||||
**不变量**:`is_detail = 1` 的行,其 `parent_image_id` 必须指向**同一草稿 / 同一专辑内**一张 `is_detail = 0` 的行。
|
||||
|
||||
- **写入侧**保证该不变量(并入操作先校验主图存在且属于同一 owner)。
|
||||
- **读侧**容错:父行不存在(或已被软删)时,把该副图**按主图渲染**,不隐藏、不报错。
|
||||
|
||||
### 3.2 审核页交互(零 JS)
|
||||
|
||||
**当前主图**:由 URL 查询参数 `?main=<imgID>` 承载。页面顶部渲染一条常驻提示条「当前主图:#12 · 伦敦 Day 3 第 12 张」,直接在页面上可见,刷新后仍在。
|
||||
|
||||
**三个同步表单 POST 路由**(全部返回 302 回列表/详情,与后台现有风格一致):
|
||||
|
||||
| 路由 | 作用 |
|
||||
| --- | --- |
|
||||
| `POST /admin/reviews/:kind/:id/images/attach` | 表单含 `main=<主图行id>` + 多个 `img=<副图行id>`(checkbox)→ **批量并入**。这是主路径 |
|
||||
| `POST /admin/reviews/:kind/:id/images/:img/attach-prev` | **并入上一张**(相邻快捷,零勾选)。覆盖「连拍同一个人」这一最常见场景 |
|
||||
| `POST /admin/reviews/:kind/:id/images/:img/detach` | **拆出**:把副图恢复为主图(`is_detail=0, parent_image_id=0`) |
|
||||
|
||||
**不单设「设为主图」路由**:选定主图 = 在页面上点击某张图的「设为主图」链接,它只是把 URL 变成 `?main=<imgID>`(一个普通 GET 链接,无需服务端写入)。随后其他图的 checkbox 表单通过**隐藏字段**携带该 `main` 值。
|
||||
|
||||
这样「选定主图 → 滚到任意位置勾选 → 并入」全程**不需要同屏**,且天然支持批量。
|
||||
|
||||
### 3.3 详情页渲染(按组折叠)
|
||||
|
||||
- 主图区:逐个渲染主图卡片(大图 + 名称)
|
||||
- 每个主图卡片下方:一条**窄的副图缩略条**,副图带「拆出」按钮
|
||||
- 未分组的图按主图渲染(默认 `is_detail=0`,因此**存量数据无需迁移即可正常显示**)
|
||||
- 主图卡片上显示「N 张副图」计数
|
||||
|
||||
### 3.4 晋升到正式表:必须重建父引用
|
||||
|
||||
`SaveStreetSnapFromDraft` 需要同步复制 `is_detail` / `parent_image_id`。
|
||||
|
||||
⚠️ **关键陷阱**:草稿晋升会**软删正式表旧图并整批重建**,新插入的正式图行拿到的是**全新的行 id**。因此**不能直接复制 `parent_image_id`**——旧 id 指向的是草稿表的行。必须先建立「草稿行 id → 新正式行 id」的映射,再把副图的 `parent_image_id` 改写为新 id。
|
||||
|
||||
漏掉这一步会导致:副图的父引用指向一个不存在(或属于别的图)的 id,详情页折叠结构错乱。
|
||||
|
||||
### 3.5 公开 API(保守扩展)
|
||||
|
||||
**保留** `PublicStreetSnapDetail.Images`(扁平、按 `sort_order` 升序)不变——现有前端不受影响。
|
||||
|
||||
**新增**分组字段:
|
||||
|
||||
```go
|
||||
type PublicStreetSnapGroup struct {
|
||||
Image PublicArticleImage `json:"image"` // 主图
|
||||
Details []PublicArticleImage `json:"details"` // 该主图下的副图(可为空)
|
||||
}
|
||||
|
||||
type PublicStreetSnapDetail struct {
|
||||
UID string
|
||||
Title string
|
||||
Cover string
|
||||
Images []PublicArticleImage // 保持原样(扁平全部)
|
||||
Groups []PublicStreetSnapGroup // 新增;未分组时为每张主图一个 group、details 为空
|
||||
Favorited bool
|
||||
}
|
||||
```
|
||||
|
||||
`Groups` 与 `Images` **同源同序**:`Groups` 只是 `Images` 按主副关系重排后的视图,不引入新的数据来源。
|
||||
|
||||
**列表**(`PublicStreetSnap`)不动。
|
||||
|
||||
### 3.6 明确不做
|
||||
|
||||
- **算法自动分组**(用户选择纯人工)。注:phash 只能判「画面像」,判不了「是不是同一个人」,做建议必然有误报;日后若要加,也只应作为提示。
|
||||
- **拖拽交互**(需引入 JS;且跨屏场景不可行)。
|
||||
- **修改 `image_count` 语义**。
|
||||
|
||||
---
|
||||
|
||||
## 4. 影响面
|
||||
|
||||
```
|
||||
internal/model/street_snap_draft.go +is_detail / +parent_image_id
|
||||
internal/model/street_snap.go +is_detail / +parent_image_id
|
||||
internal/repository/review_repository.go attach / attach-prev / detach 三个写方法
|
||||
internal/repository/review_repository.go SaveStreetSnapFromDraft 重建父引用
|
||||
internal/service/review_service.go DraftImageRef 加分组信息;分组写入的校验
|
||||
internal/handler/backstage_handler.go 详情页折叠渲染 + 常驻主图条 + 三个表单
|
||||
internal/router/backstage.go 三个新路由
|
||||
internal/dto/street_snap.go +Groups(保守扩展)
|
||||
internal/service/street_snap_service.go 详情组装 Groups
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 测试
|
||||
|
||||
### 5.1 单元测试(不依赖数据库)
|
||||
|
||||
- 详情组装:主图 + 其副图归到同一 group;未分组的图各自成组。
|
||||
- 父引用失效的容错:`parent_image_id` 指向不存在/已软删的行时,该图按主图渲染,不丢图。
|
||||
|
||||
### 5.2 集成测试(需 PostgreSQL)
|
||||
|
||||
- 并入:`is_detail` / `parent_image_id` 正确写入;重复并入同一主图幂等。
|
||||
- 拆出:恢复为主图且 `parent_image_id` 归零。
|
||||
- 跨 owner 拦截:主图与副图不属于同一草稿时拒绝写入(不变量)。
|
||||
- **晋升重建父引用**:草稿有「1 主 + 2 副」时晋升,正式表中 2 张副图的 `parent_image_id` 必须指向**新的**主图行 id(而非草稿表的旧 id)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 待确认
|
||||
|
||||
1. 公开 API 采用「保留扁平 `images` + 新增 `groups`」的保守方案。若你希望**直接改成**只返回分组结构(更干净但会打断现有前端),需要另行确认。
|
||||
2. `image_count` 暂不改语义(仍计全部图)。若希望改成「只算主图」(与 runway 一致,含义变成「N 个人」),需要前端同步,另议。
|
||||
3. 分组的「组序号」不落库,由读时按 `sort_order` 顺序推导(主图顺序即组序)。
|
||||
Reference in New Issue
Block a user