DOC-002: Proposal/Essential/Story restricted migration guide
- Document Propose → Proposal naming changes across all layers - Document Proposal Accept semantic change (single task → Essential-based batch) - Document story/* restricted expansion and impact analysis - Document feat_task_id deprecation and backward compatibility strategy - Document migration execution order and rollback plan
This commit is contained in:
263
docs/proposal-essential-story-migration.md
Normal file
263
docs/proposal-essential-story-migration.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# 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 即可
|
||||
Reference in New Issue
Block a user