- 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
10 KiB
10 KiB
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 数据库迁移策略
-- 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 和前端同步更新,不存在旧客户端兼容问题
- 保留兼容别名会增加长期维护负担
具体做法:
- 后端路由从
/projects/{pid}/proposes切换为/projects/{pid}/proposals - 前端所有 API 调用同步更新
- CLI 命令同步更新
- 不保留
/proposes旧路由
如果未来需要面向外部提供 API,应在 API 版本化层面处理兼容,而非在路由别名层面。
二、Proposal Accept 语义变化
2.1 旧行为
Proposal (open) ──accept──▶ Proposal (accepted)
│
▼
单个 story/feature Task
feat_task_id 回填到 Proposal
- Accept 时自动创建 1 个
story/featuretask 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/featureimprovement→story/improvementrefactor→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 机制:
RESTRICTED_TYPE_SUBTYPES = {
("story", "feature"),
}
目前仅 story/feature 是 restricted。
3.2 变更内容
将 所有 story/* 组合 标记为 restricted:
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/featuretask 保持不变 - 不需要迁移或修改已有 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 |
同上 |
-- 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 生命周期
- Proposal
open状态下可创建/编辑/删除 Essential - Proposal Accept 时,全部 Essential 用于生成
story/*task - 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} |
六、迁移执行顺序
推荐按以下顺序执行,确保每一步可独立验证:
- 后端 Model/Schema 重命名 —
Propose→Proposal(BE-PR-001, BE-PR-002) - 数据库 migration — 表名、字段名变更
- 新增 Essential Model/Schema — (BE-PR-003, BE-PR-004, BE-PR-005)
- 新增 Essential CRUD API — (BE-PR-006)
- 重构 Accept 逻辑 — (BE-PR-007, BE-PR-008)
- 收紧 story restricted — (BE-PR-009)
- 清理 feat_task_id — (BE-PR-010)
- 前端同步更新 — (FE-PR-001 ~ FE-PR-005)
- CLI 同步更新 — (CLI-PR-001 ~ CLI-PR-004)
- 测试补全 — (BE-PR-011, TEST-BE-PR-001, TEST-FE-PR-001, TEST-CLI-PR-001)
七、风险与注意事项
- 表重命名的 downtime —
ALTER TABLE RENAME在 MySQL 中是瞬时操作,但需确保无长事务锁表 - 权限 key 更新 — 如果有缓存层,需要在迁移后清理缓存
- 前后端同步部署 — 建议同时部署前后端,避免前端调旧路由 404
- 回滚方案 — 如需回滚,反向执行表重命名和字段重命名即可;Essential 表作为新增表,回滚时 DROP 即可