# HarborForge — Proposal / Essential / Story Restricted 迁移说明 > 更新时间:2026-03-29 > 本文档描述 Propose → Proposal 重命名、Proposal Accept 语义变更、Essential 新增、以及 story/* restricted 的迁移细节与兼容策略。 --- ## 一、Propose → Proposal 命名调整 ### 1.1 变更范围 | 层 | 旧命名 | 新命名 | 涉及文件 | |---|--------|--------|---------| | **数据库表** | `proposes` | `proposals`(新表名) | Alembic migration | | **Model** | `Propose`, `ProposeStatus` | `Proposal`, `ProposalStatus` | `models/propose.py` → `models/proposal.py` | | **Schema** | `ProposeCreate`, `ProposeUpdate`, `ProposeResponse`, `ProposeStatusEnum`, `ProposeBase` | `ProposalCreate`, `ProposalUpdate`, `ProposalResponse`, `ProposalStatusEnum`, `ProposalBase` | `schemas/schemas.py` | | **Router** | `proposes.py`, 路由前缀 `/proposes` | `proposals.py`, 路由前缀 `/proposals` | `api/routers/proposes.py` → `api/routers/proposals.py` | | **Helper 函数** | `_find_propose()`, `_serialize_propose()`, `_generate_propose_code()`, `_can_edit_propose()` | `_find_proposal()`, `_serialize_proposal()`, `_generate_proposal_code()`, `_can_edit_proposal()` | 同上 router 文件 | | **Code 字段** | `propose_code` | `proposal_code` | Model、Schema、序列化逻辑 | | **Activity log** | `entity_type="propose"` | `entity_type="proposal"` | `log_activity()` 调用处 | | **权限 key** | `propose.accept`, `propose.reject`, `propose.reopen` | `proposal.accept`, `proposal.reject`, `proposal.reopen` | `check_permission()` 调用处、RBAC 数据 | | **前端** | 页面标题、菜单、API 调用中的 `propose` 文案 | 统一为 `proposal` | 前端组件、路由、API 调用 | | **CLI** | `hf propose`, `hf propose-create`, `hf propose-accept` 等 | `hf proposal`, `hf proposal create`, `hf proposal accept` 等 | CLI 命令注册与 help 文案 | ### 1.2 数据库迁移策略 ```sql -- Alembic migration ALTER TABLE proposes RENAME TO proposals; ALTER TABLE proposals CHANGE propose_code proposal_code VARCHAR(64); ``` - `proposal_code` 的已有编码值(如 `HF:P00001`)不变,仅字段名变更 - 所有外键引用(如 `proposals.id`)保持不变(表内 PK 不变) ### 1.3 API 兼容策略 **方案:直接切换,不保留旧路由** 理由: - HarborForge 目前为内部系统,无外部消费者 - CLI 和前端同步更新,不存在旧客户端兼容问题 - 保留兼容别名会增加长期维护负担 具体做法: 1. 后端路由从 `/projects/{pid}/proposes` 切换为 `/projects/{pid}/proposals` 2. 前端所有 API 调用同步更新 3. CLI 命令同步更新 4. **不保留** `/proposes` 旧路由 > 如果未来需要面向外部提供 API,应在 API 版本化层面处理兼容,而非在路由别名层面。 --- ## 二、Proposal Accept 语义变化 ### 2.1 旧行为 ``` Proposal (open) ──accept──▶ Proposal (accepted) │ ▼ 单个 story/feature Task feat_task_id 回填到 Proposal ``` - Accept 时自动创建 **1 个** `story/feature` task - `feat_task_id` 字段记录生成的 task ID ### 2.2 新行为 ``` Proposal (open) ├── Essential (feature) ──┐ ├── Essential (improvement) ─┤── accept ──▶ Proposal (accepted) └── Essential (refactor) ──┘ │ ▼ story/feature Task ← from Essential(feature) story/improvement Task ← from Essential(improvement) story/refactor Task ← from Essential(refactor) 全部落入选定 Milestone ``` - Proposal 下维护多个 **Essential**(可落地核心条目) - Accept 时,遍历该 Proposal 下全部 Essential - 按 Essential.type 映射生成对应 `story/*` task: - `feature` → `story/feature` - `improvement` → `story/improvement` - `refactor` → `story/refactor` - 生成目标 Milestone 在 Accept 时明确选择(与旧行为一致) - **批量创建在同一个数据库事务中完成** ### 2.3 Accept 前置条件变更 | 条件 | 旧 | 新 | |------|----|----| | 操作者权限 | `propose.accept` | `proposal.accept` | | 目标 Milestone | 必须选择,状态 `open` | 同上 | | Essential 数量 | 无此概念 | **必须至少 1 个 Essential**,否则 Accept 报错 | ### 2.4 Accept 返回值变更 - 旧:返回 Proposal 详情(含 `feat_task_id`) - 新:返回 Proposal 详情 + 嵌套的 Essential 列表 + 每个 Essential 生成的 task 信息 --- ## 三、story/* 改为 Restricted 的影响面 ### 3.1 当前状态 后端 `tasks.py` 中已有 restricted 机制: ```python RESTRICTED_TYPE_SUBTYPES = { ("story", "feature"), } ``` 目前仅 `story/feature` 是 restricted。 ### 3.2 变更内容 将 **所有 `story/*` 组合** 标记为 restricted: ```python RESTRICTED_TYPE_SUBTYPES = { ("story", "feature"), ("story", "improvement"), ("story", "refactor"), } ``` ### 3.3 影响面 | 入口 | 影响 | 处理 | |------|------|------| | **通用 Task Create API** (`POST /projects/{pid}/milestones/{mid}/tasks`) | 无法创建任何 `story/*` task | 已有 `_validate_task_type_subtype()` 校验,扩展 restricted 集合即可 | | **Proposal Accept** | 内部受控入口,`allow_restricted=True` | 保留,唯一合法创建 `story/*` 的途径 | | **前端 Task 创建表单** | `story` 类型从选择器中移除或标为不可选 | 前端调整 | | **CLI `hf task create`** | 阻止 `--type story` | CLI 侧校验或依赖后端 403 | | **CSV 导入**(如有) | 阻止 `story/*` 行 | 导入逻辑需加校验 | | **Milestone 下 Task Create** (`milestones.py` 路由) | 已有 `story/feature` 限制逻辑 | 扩展为全部 `story/*` | ### 3.4 已有数据兼容 - 历史上通过旧 Accept 创建的 `story/feature` task **保持不变** - 不需要迁移或修改已有 story task - restricted 仅影响 **新建**,不影响已有记录的读取/更新/状态流转 --- ## 四、旧数据与旧接口兼容策略 ### 4.1 `feat_task_id` 字段 | 处理方式 | 说明 | |---------|------| | **保留字段,标记为 deprecated** | 数据库列保留,避免破坏已有数据 | | **只读** | API 继续返回 `feat_task_id`(已有 Proposal 的值不丢失) | | **新 Proposal 不再写入** | 新的 Accept 流程不再设置 `feat_task_id` | | **新增追踪方式** | 通过 Essential → Task 的关联关系(`essential_id` 或关联表)替代 | | **未来清理** | 等所有旧 Proposal 迁移完毕后,可选择 DROP 该列 | ### 4.2 旧 Proposal 数据读取 - 旧的 `accepted` 状态 Proposal 仍有 `feat_task_id`,前端继续支持 "View Generated Task" 跳转 - 旧 Proposal 没有 Essential,详情页 Essential 区域显示空列表 - 不需要为旧 Proposal 补建 Essential 记录 ### 4.3 Proposal Code 格式 - 已有编码如 `HF:P00001` 保持不变 - 新 Proposal 继续使用相同格式 - 字段名从 `propose_code` 变为 `proposal_code`,但值的格式不变 ### 4.4 权限 Key 迁移 | 旧 | 新 | 迁移方式 | |----|----|----| | `propose.accept` | `proposal.accept` | 数据库中更新 RBAC permission 记录 | | `propose.reject` | `proposal.reject` | 同上 | | `propose.reopen` | `proposal.reopen` | 同上 | ```sql -- RBAC permission key migration UPDATE permissions SET key = REPLACE(key, 'propose.', 'proposal.') WHERE key LIKE 'propose.%'; ``` > 如果权限 key 存储在 code 中而非数据库,则只需修改代码常量。 ### 4.5 Activity Log 兼容 - 旧 activity log 中 `entity_type="propose"` 的记录保留不动 - 前端展示时做兼容映射:`propose` → 显示为 "Proposal" - 新 activity log 统一使用 `entity_type="proposal"` --- ## 五、Essential 新增概要 ### 5.1 数据模型 ``` Essential ├── id (PK) ├── essential_code (UNIQUE) — 格式参考 ProjectCode / TaskCode ├── proposal_id (FK → proposals.id) ├── type (Enum: feature, improvement, refactor) ├── title ├── description (nullable) ├── created_by_id (FK → users.id, nullable) ├── created_at └── updated_at ``` ### 5.2 EssentialCode 编码 - 格式:`{proposal_code}:E{i:05x}` - 示例:`HF:P00001:E00001` - 每个 Proposal 独立递增 ### 5.3 Essential 生命周期 1. Proposal `open` 状态下可创建/编辑/删除 Essential 2. Proposal Accept 时,全部 Essential 用于生成 `story/*` task 3. Proposal 进入 `accepted`/`rejected` 后,Essential 不可修改 ### 5.4 API 端点 | 操作 | 方法 | 路径 | |------|------|------| | 列出 | GET | `/projects/{pid}/proposals/{proposal_id}/essentials` | | 创建 | POST | `/projects/{pid}/proposals/{proposal_id}/essentials` | | 详情 | GET | `/projects/{pid}/proposals/{proposal_id}/essentials/{id}` | | 编辑 | PATCH | `/projects/{pid}/proposals/{proposal_id}/essentials/{id}` | | 删除 | DELETE | `/projects/{pid}/proposals/{proposal_id}/essentials/{id}` | --- ## 六、迁移执行顺序 推荐按以下顺序执行,确保每一步可独立验证: 1. **后端 Model/Schema 重命名** — `Propose` → `Proposal`(BE-PR-001, BE-PR-002) 2. **数据库 migration** — 表名、字段名变更 3. **新增 Essential Model/Schema** — (BE-PR-003, BE-PR-004, BE-PR-005) 4. **新增 Essential CRUD API** — (BE-PR-006) 5. **重构 Accept 逻辑** — (BE-PR-007, BE-PR-008) 6. **收紧 story restricted** — (BE-PR-009) 7. **清理 feat_task_id** — (BE-PR-010) 8. **前端同步更新** — (FE-PR-001 ~ FE-PR-005) 9. **CLI 同步更新** — (CLI-PR-001 ~ CLI-PR-004) 10. **测试补全** — (BE-PR-011, TEST-BE-PR-001, TEST-FE-PR-001, TEST-CLI-PR-001) --- ## 七、风险与注意事项 1. **表重命名的 downtime** — `ALTER TABLE RENAME` 在 MySQL 中是瞬时操作,但需确保无长事务锁表 2. **权限 key 更新** — 如果有缓存层,需要在迁移后清理缓存 3. **前后端同步部署** — 建议同时部署前后端,避免前端调旧路由 404 4. **回滚方案** — 如需回滚,反向执行表重命名和字段重命名即可;Essential 表作为新增表,回滚时 DROP 即可