library-picturebook-activity/docs/design/public/ugc-work-status-redesign.md
En 8995e2f2e2 feat: 公众端多项功能增强——短信登录、作品状态优化、创作流程组件 keep-alive
后端:
- 新增手机号验证码登录接口及 PublicSmsLoginDto
- LeaiSync 作品同步状态阈值从 CATALOGED 调整为 DUBBED
- UgcWork 实体字段微调、数据库迁移脚本修正

前端:
- Login 页面支持用户名/手机号双模式登录
- public.ts 新增 loginBySms、sendSmsCode API
- AI 创作流程全部视图添加 keep-alive 组件名导出
- CreatingView 生成逻辑优化
- WelcomeView 欢迎页增强
- BookReaderView、作品库等页面细节修复

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 11:19:42 +08:00

20 KiB
Raw Blame History

UGC 作品状态机重设计 — 产品设计方案

所属端:用户端(公众端)+ 超管端 + 后端 状态:方案已确认,前端 UI 优先实施 创建日期2026-04-09 最后更新2026-04-09


1. 背景

1.1 当前问题

用户作品(UgcWork)的状态当前只有 4 个用户可见状态

draft / pending_review / published / rejected

这种状态划分把两个语义完全不同的场景混在了 draft 里:

场景 当前归属 实际语义
AI 还在生成 / 用户没保存编目信息 draft 半成品,没做完
用户做完了完整作品但不想公开 draft 成品但私有

混在一起的后果:

  • 用户在作品库 "草稿" tab 看到混杂——既有"待继续"的半成品,也有"已完成等待发布"的私有作品
  • 没有"已完成但私有"的归属,用户想暂存自己的成品作品没地方放
  • "公开发布"是隐藏在创作流程末端的一次性决策,用户错过了就只能看着自己的作品停在 draft

1.2 业界对比

主流内容平台都做了「还没做完」和「做完了但不想公开」的区分:

平台 半成品 成品但私有 公开
抖音 草稿箱 仅自己可见 公开发布
小红书 草稿 私密笔记 公开发布
B 站 草稿箱 仅自己可见 已发布(公开)
Medium Draft Unlisted Published

我们的方案对齐这个共识。

1.3 目标

引入新的中间状态 unpublished(未发布),明确语义边界:

  • 草稿draft = 作品技术上还没完成(生成失败 / 还在生成 / 没完成配音)
  • 未发布unpublished = 作品技术上完整(配音已完成),但用户没主动公开
  • 审核中pending_review = 用户主动提交审核
  • 已发布published = 审核通过,发现页可见
  • 被拒绝rejected = 审核未通过,可改后重新提交

并支持:

  • 作品在状态间双向流转(已发布可以下架回未发布、被拒可以改后重交)
  • 「公开发布」从一次性决策变成可后悔、可推迟的常驻按钮

2. 状态机设计

2.1 完整状态流转图

                     ┌──────────────────────────┐
                     │       创作流程开始        │
                     │   /p/create/upload       │
                     └────────────┬─────────────┘
                                  │
                                  ▼
                          ┌───────────────┐
                          │     DRAFT     │  半成品:还在生成或未完成配音
                          │     草稿       │
                          └───────┬───────┘
                                  │
                                  │ 用户在 EditInfoView 点
                                  │   保存 / 去配音 / 立即发布
                                  │ 任意按钮 = 配音完成
                                  ▼
                          ┌───────────────┐
                          │  UNPUBLISHED  │  成品私有:已配音,可发布
                          │    未发布      │◀──────────────┐
                          └───────┬───────┘               │
                                  │                       │
                                  │ 用户在详情页点         │
                                  │   "公开发布"          │
                                  ▼                       │
                          ┌───────────────┐               │
                          │PENDING_REVIEW │  排队待审核    │
                          │    审核中      │               │
                          └───┬───────┬───┘               │
                              │       │                   │
                  超管通过    │       │ 超管拒绝          │
                              ▼       ▼                   │
                  ┌───────────────┐ ┌───────────────┐    │
                  │   PUBLISHED   │ │   REJECTED    │    │
                  │    已发布      │ │    被拒绝      │    │
                  │  发现页可见    │ │               │    │
                  └───────┬───────┘ └───────┬───────┘    │
                          │                 │             │
                          │ 用户/超管下架    │ 用户改完重交  │
                          └─────────────────┴─────────────┘
                                            │
                                  rejected → pending_review
                                  published → unpublished

2.2 状态流转表

起始状态 触发 目标状态 触发方 接口
(无) 创建作品 DRAFT 系统 leai 创作流程内部
DRAFT 配音完成leai status → DUBBED UNPUBLISHED 系统webhook 同步) LeaiSyncService
UNPUBLISHED 用户点「公开发布」 PENDING_REVIEW 用户 POST /public/works/{id}/publish
UNPUBLISHED 用户补充配音/编辑 UNPUBLISHED 用户 更新内容接口
PENDING_REVIEW 审核通过 PUBLISHED 超管 POST /content-review/works/{id}/approve
PENDING_REVIEW 审核拒绝 REJECTED 超管 POST /content-review/works/{id}/reject
PENDING_REVIEW 用户撤回 UNPUBLISHED 用户 POST /public/works/{id}/withdraw(新增)
PUBLISHED 用户下架 UNPUBLISHED 用户 POST /public/works/{id}/unpublish(新增)
PUBLISHED 超管强制下架 UNPUBLISHED 或 TAKEN_DOWN 超管 POST /content-review/works/{id}/takedown
REJECTED 改完重交 PENDING_REVIEW 用户 POST /public/works/{id}/publish

2.3 状态可见性矩阵

状态 用户作品库可见 用户详情页可见 发现页可见 超管端可见
DRAFT ✓(草稿 tab ✓(继续创作)
UNPUBLISHED ✓(未发布 tab ✓(带"公开发布"按钮)
PENDING_REVIEW ✓(审核中 tab ✓(带"撤回"按钮) ✓(待审核队列)
PUBLISHED ✓(已发布 tab ✓(带"下架"按钮) ✓(已发布管理)
REJECTED ✓(被拒绝 tab ✓(带"修改后重交"按钮) ✓(历史)

3. 当前现状调研

3.1 后端现状

UgcWork.status 字段

  • 类型:Integer(非 String
  • 文件:backend-java/src/main/java/com/competition/modules/ugc/entity/UgcWork.java:45
  • 语义:当前直接复用了 leai 进度状态,没有独立的发布状态字段
// 当前注释
// -1=FAILED, 0=DRAFT, 1=PENDING, 2=PROCESSING,
// 3=COMPLETED, 4=CATALOGED, 5=DUBBED

Migration 历史映射

backend-java/src/main/resources/db/migration/V5__leai_integration.sql 把旧 varchar status 转换成 INT 时的映射:

draft           → 0
pending_review  → 1   (与 leai PENDING 复用!)
processing      → 2   (与 leai PROCESSING 复用)
published       → 3   (与 leai COMPLETED 复用!)
rejected        → -1  (与 leai FAILED 复用!)
taken_down      → -2

关键问题:本地的 published 和 leai 的 COMPLETED 共享 status=3,本地的 pending_review 和 leai 的 PENDING 共享 status=1,本地的 rejected 和 leai 的 FAILED 共享 status=-1。这种语义复用是历史包袱,也是这次重设计的根本原因。

Webhook 同步逻辑

  • 文件:backend-java/.../leai/service/LeaiSyncService.java
  • 当前规则:remoteStatus > localStatus 时全量更新
  • 缺失leai status=4CATALOGED到本地 unpublished 的映射尚未实现

现有审核 / 管理接口

接口 路径 当前行为
公众端发布 POST /public/works/{id}/publish draft/rejected → 1被复用为 pending_review
超管审核通过 POST /content-review/works/{id}/approve → 3被复用为 published
超管审核拒绝 POST /content-review/works/{id}/reject → -1被复用为 rejected
超管下架 POST /content-review/works/{id}/takedown → -2taken_down
超管恢复 POST /content-review/works/{id}/restore → 3published
超管撤销审核 POST /content-review/works/{id}/revoke → 1pending_review

3.2 前端现状

类型定义

  • 文件:frontend/src/api/public.ts:380-405
  • UserWork.status: string,枚举:draft / pending_review / published / rejected / taken_down
  • 跟后端 Integer 不一致——必有中间转换层(待验证),暂未确认转换发生在 controller 还是 mapper

公众端页面

文件 当前作用 改造需求
views/public/works/Index.vue 作品列表5 tab 加 unpublished tab、加状态颜色映射
views/public/works/Detail.vue 作品详情 加「公开发布」「下架」「撤回」按钮,根据 status 切换显示
views/public/works/Publish.vue 单独的发布页draft → pending_review 删除(功能并入 EditInfoView 和 Detail.vue
views/public/create/views/EditInfoView.vue 创作流程内编辑信息 三按钮语义调整:保存 / 去配音 / 立即发布

超管端页面

文件 当前作用 改造需求
views/content/WorkReview.vue 审核 pending_review 作品 状态映射加 unpublished虽然超管看不到
views/content/WorkManagement.vue 管理 published / taken_down 作品 下架目标改为 unpublished替代 taken_down

4. 后端 schema 改动

4.1 推荐方案拆字段leai 进度 vs 本地发布状态解耦)

核心思想:把 leai 进度状态和本地发布状态分到两个独立字段,彻底解决语义复用。

-- 现有status INT混淆 leai 进度 + 本地发布状态)
-- 改为:
ALTER TABLE t_ugc_work
  ADD COLUMN leai_status INT NOT NULL DEFAULT 0
    COMMENT 'leai 创作进度: -1=FAILED, 0=INIT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED';

-- status 字段重定义为本地发布状态VARCHAR 跟前端对齐)
ALTER TABLE t_ugc_work
  MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'draft'
    COMMENT '本地发布状态: draft/unpublished/pending_review/published/rejected';

4.2 数据迁移

新建 V9__split_work_status.sql

-- 1. 加 leai_status 字段
ALTER TABLE t_ugc_work ADD COLUMN leai_status INT NOT NULL DEFAULT 0;

-- 2. 把现有 status (INT) 的值复制到 leai_status
UPDATE t_ugc_work SET leai_status = status;

-- 3. 改 status 字段类型为 VARCHAR
ALTER TABLE t_ugc_work MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'draft';

-- 4. 按 leai_status 回填本地 status
UPDATE t_ugc_work SET status = CASE
  WHEN leai_status = -2 THEN 'unpublished'  -- 原 taken_down 视为未发布
  WHEN leai_status = -1 THEN 'draft'        -- 原 rejected 视为草稿(无法判断是创作失败还是审核拒绝)
  WHEN leai_status = 0  THEN 'draft'
  WHEN leai_status = 1  THEN 'unpublished'  -- 原 pending_review 兼容数据视为未发布(保守,让用户重新主动发布)
  WHEN leai_status = 2  THEN 'draft'
  WHEN leai_status = 3  THEN 'unpublished'  -- 原 published 兼容数据视为未发布(同上,避免老数据自动公开)
  WHEN leai_status = 4  THEN 'unpublished'
  WHEN leai_status = 5  THEN 'unpublished'
  ELSE 'draft'
END;

-- 5. 加索引(按状态筛选会很常用)
CREATE INDEX idx_ugc_work_status ON t_ugc_work(status);
CREATE INDEX idx_ugc_work_leai_status ON t_ugc_work(leai_status);

保守策略:现有 published / pending_review 数据回填为 unpublished 而不是直接保留 published。原因是状态复用导致无法区分"用户真的发布过的"和"刚创作完成的",让用户重新主动发布最安全。

4.3 不推荐的备选方案:保留单字段

不改 schema沿用 status INT,在 0-5 之外加 10=unpublished, 11=pending_review_local, 12=published_local

缺点

  • 语义混乱继续存在
  • 同事维护时容易把 leai 数字和本地数字搞混
  • migration 简单但留长期技术债

5. 改动清单

5.1 后端P0

  • 新建 WorkPublishStatus 枚举类 backend-java/.../ugc/enums/WorkPublishStatus.java
    public enum WorkPublishStatus {
      DRAFT, UNPUBLISHED, PENDING_REVIEW, PUBLISHED, REJECTED
    }
    
  • UgcWork 实体改造
    • leaiStatus: Integer(保持 leai 进度)
    • status: String(本地发布状态,对应新枚举)
  • 新建 migration V9__split_work_status.sql(见 4.2
  • LeaiSyncService.updateStatusForward:当 leai status 推进到 DUBBED 时,自动把本地 status 从 draft 升到 unpublished如果当前不是更高级状态
  • PublicUserWorkService.publish():检查作品 status == 'unpublished',改为 unpublished → pending_review;同时支持 rejected → pending_review
  • 新增 POST /public/works/{id}/unpublish:用户主动下架,published → unpublished
  • 新增 POST /public/works/{id}/withdraw:用户撤回审核,pending_review → unpublished
  • PublicContentReviewService 审核动作语义对齐
    • approve: pending_review → published
    • reject: pending_review → rejected
    • takedown: published → unpublished参见 6.C 关于 taken_down 的决策)

5.2 公众端前端P0

  • UserWork.status 类型加 unpublished frontend/src/api/public.ts
  • works/Index.vue 作品库列表
    • tabs 数组加 { key: 'unpublished', label: '未发布' }
    • statusTextMap 加 unpublished: '未发布'
    • 卡片状态标签样式加 &.unpublished(紫色淡)
    • 按 status 显示空状态文案
  • works/Detail.vue 作品详情
    • 整体配色清理紫粉化emoji + 橙色清理)
    • 核心:根据 status 显示不同操作按钮:
      • unpublished → 「公开发布」(主操作,紫粉渐变) + 「编辑信息」 + 「补充配音」 + 「删除」
      • pending_review → 「撤回审核」 + 「删除」
      • published → 「下架」 + 「删除」
      • rejected → 「修改后重交」 + 「删除」
  • create/views/EditInfoView.vue 三按钮语义调整
    • 「保存」 → unpublished跳作品库 ?tab=unpublished
    • 「去配音」 → DubbingView配音完成后还是 unpublished
    • 「立即发布」 → 一次性走完 unpublished + 公开发布两步,跳作品库 ?tab=pending_review
  • 删除 views/public/works/Publish.vue(功能并入 EditInfoView 和 Detail.vue
  • 作品库 query.tab 已支持 unpublished 参数(已在前次 commit 完成)

5.3 超管端前端P1

  • views/content/WorkReview.vue:状态映射加 unpublished 显示(虽然超管的待审核 tab 看不到 unpublished但作品历史详情可能展示
  • views/content/WorkManagement.vue:下架动作目标改为 unpublished如果保留 taken_down 概念则不改)

6. 待拍板细节

6.A schema 改动幅度 → 已确认

采用方案 1拆字段leai_status INT + status VARCHAR,语义彻底解耦。

6.B Publish.vue 处理 → 已确认删除

独立的发布页在新逻辑下没有存在意义,删除。功能拆到:

  • 编辑信息 → EditInfoView创作流程内+ Detail.vue「编辑信息」按钮流程外回头改
  • 公开发布 → Detail.vue「公开发布」按钮

6.C 超管下架的目标状态 → 待确认

两种方案:

方案 1合并:超管下架 = 用户下架 = published → unpublished,作品回到用户私有空间,用户可以再次发布

方案 2区分:超管下架 = published → taken_down(独立状态),用户下架 = published → unpublishedtaken_down 的作品不允许用户再次发布,相当于"被禁言"

讨论建议:除非有"超管强制永久下架"的硬性需求,方案 1 更简洁。如果担心被超管下架的违规作品被用户再次提交,可以在审核环节让超管选择「拒绝并禁止重交」。

6.D 已确认的产品决策

问题 决策
已发布作品下架 → 未发布 需要
被拒绝作品改完重交 可以
草稿能否直接发布 不能(必须先编目完整成为 unpublished
超管端不显示 unpublished 用户私有,超管不可见

7. 执行顺序

7.1 第一阶段:前端 UI 层(不改后端)

目标:让产品/设计/PM 能看到完整的新交互体验,前端用 dev mock 模拟 unpublished 状态。

  • 作品库 Index.vue 加 unpublished tab + 颜色 + 文案
  • 作品库 Detail.vue 重做:清 emoji + 紫粉配色 + 根据 status 切换按钮
  • EditInfoView 三按钮语义调整 + dev mock 跳转
  • 删除 Publish.vue
  • 前端 UserWork.status 类型扩展

交付物:完整的可点击 UI 流程dev 模式无后端可走通)

7.2 第二阶段:后端 schema + service

前置依赖:第一阶段 UI 已确认,产品验收通过

  • 新建 WorkPublishStatus 枚举
  • 新建 V9__split_work_status.sql migration
  • UgcWork 实体加 leaiStatus + 改 status 类型
  • LeaiSyncService 加 leai → 本地 status 映射逻辑
  • PublicUserWorkService publish/unpublish/withdraw 接口实现
  • PublicContentReviewService 审核动作 status 语义对齐

7.3 第三阶段:联调 + 超管端调整

  • 前端去 dev mock接入真实接口
  • 超管端 WorkReview / WorkManagement 状态映射调整
  • 端到端测试:完整跑通从创作 → 编目 → 未发布 → 公开发布 → 审核 → 已发布 → 下架 → 重新发布的全流程

8. 相关文档


附录 A术语对照

术语 含义 备注
leai 乐读派外部创作服务 提供 AI 绘本生成能力
leai status 乐读派的创作进度状态 数字 -1 ~ 5
本地发布状态 我们自己定义的作品发布状态 字符串5 个值
CATALOGED leai 状态 4编目完成 用户在 EditInfoView 保存信息后到达
DUBBED leai 状态 5配音完成 配音是可选步骤
未发布 unpublished 本次新增状态 已编目完成但未公开

附录 B状态命名为什么是 unpublished 而不是 private

  • private 在很多平台是"永久不公开"的语义(比如 GitHub private repo
  • unpublished 强调"还没发布"的暂时性,跟"已发布 published"形成对立
  • 跟 Medium 的 Draft / Unlisted / Published 三态命名风格一致
  • 本地化为"未发布"也是中文用户最直观的理解