255 lines
16 KiB
Markdown
255 lines
16 KiB
Markdown
# FTB 智能项目管理系统 — 架构文档
|
||
|
||
## 项目定位
|
||
|
||
FTB 是一个面向中小型研发团队的项目管理平台,集成 AI 能力辅助决策。核心层级:
|
||
|
||
```
|
||
产品(Product) → 项目(Project) → 版本(Version) → 需求/任务/用例/Bug
|
||
```
|
||
|
||
不是给测试团队的工具,也不是 Jira 替代品。**核心目标是让产品/项目经理在一个页面看到:"开发完了吗 → 验收过了吗 → 还有多少 BUG → 能不能发布"**。
|
||
|
||
## 心智模型(重要)
|
||
|
||
系统设计遵循**三条维度线**的分离:
|
||
|
||
```
|
||
语义线(Why) 执行线(How) 质量线(Quality)
|
||
───────────────── ───────────────── ────────────────
|
||
Requirement Version TestCase
|
||
解释为什么做 驱动状态流转 └─ Bug
|
||
不参与流程 所有状态看版本 质量闭环
|
||
```
|
||
|
||
**Requirement(需求)= 语义层**,解释功能范围和业务原因,不驱动流程。
|
||
**Version(版本)= 执行主线**,所有任务归属版本,状态由版本派生;需求只是可选语义分组。
|
||
**TestCase + Bug = 质量闭环**,挂在版本上验收。
|
||
|
||
## 技术栈
|
||
|
||
| 层 | 技术 | 说明 |
|
||
|---|------|------|
|
||
| 前端 | Next.js 14 (App Router) | TypeScript + 客户端组件为主 |
|
||
| UI | Tailwind CSS + Shadcn/ui | 紧凑信息密度、现代风格 |
|
||
| 状态 | Zustand | 每个领域一个 store |
|
||
| 持久化 | PostgreSQL AppData(V2.1) | 业务数据走 NestJS `/data/:key`,不再以浏览器存储为主 |
|
||
| 后端 | NestJS + Prisma + PostgreSQL(V2) | 已接入通用数据文档层,后续再逐表关系化 |
|
||
| AI | Anthropic SDK(V3 远景) | 健康度/风险预警/排期建议 |
|
||
|
||
## 模块结构
|
||
|
||
```
|
||
apps/web/
|
||
├── app/
|
||
│ ├── products/ # 产品列表 + 详情(点击项目可跳转)
|
||
│ ├── projects/[id]/ # 项目详情:版本卡片/状态胶囊
|
||
│ ├── versions/ # 版本列表 + 详情
|
||
│ ├── requirements/ # 需求池
|
||
│ ├── workspace/ # 与我相关(聚合工作台)
|
||
│ └── admin/ # 系统管理(任务类型字典等)
|
||
├── components/
|
||
│ ├── product/ # 产品组件
|
||
│ ├── version/ # 版本组件(PlanTab, CapsuleStages, MemberChips...)
|
||
│ ├── dev-task/ # 开发任务组件
|
||
│ ├── test-case/ # 测试用例组件
|
||
│ ├── bug/ # Bug 组件
|
||
│ └── requirement/ # 需求组件
|
||
├── lib/
|
||
│ ├── derive.ts # 派生数据(flattenVersions/flattenProjects)
|
||
│ ├── linkage-engine.ts # 跨模块联动引擎(需求↔DevTask 派生状态)
|
||
│ ├── workspace-engine.ts # 工作台聚合引擎(统一WorkItem)
|
||
│ ├── version-status.ts # 版本执行态推导
|
||
│ ├── version-plan.ts # 计划任务(调研/产品/UI)
|
||
│ ├── dev-task.ts # 开发任务
|
||
│ ├── test-case.ts # 测试用例
|
||
│ ├── bug.ts # Bug
|
||
│ └── requirement.ts # 需求
|
||
└── stores/ # Zustand stores(每个领域独立)
|
||
```
|
||
|
||
## 关键架构原则
|
||
|
||
### 1. 引擎模式(关键)
|
||
|
||
跨模块的数据派生通过**引擎层**处理,不是各 store 互相调用:
|
||
|
||
- **linkage-engine.ts**:从 DevTask 状态派生需求"实际进度",新建模块只需在此聚合
|
||
- **workspace-engine.ts**:聚合 WorkItem 给"与我相关"页面用
|
||
|
||
引擎是**纯函数**,输入多个 store 数据,输出统一视图。引擎使应用各模块松耦合,新增模块时不需要改其他模块的代码。
|
||
|
||
### 2. 派生胜过存储
|
||
|
||
时间字段、状态、进度都尽量从底层数据派生,而不是手填:
|
||
|
||
- 实际工时 = `startDate → completedAt` 时间差(精确到 0.5h)
|
||
- 阶段耗时 = 所有阶段任务的最早开始 → 最晚完成
|
||
- 需求开发状态 = 从 DevTask 状态聚合
|
||
- 版本执行态 = 从所有 DevTask + TestCase + Bug 聚合
|
||
- 阶段进度 = 已完成子任务/总子任务
|
||
|
||
### 3. 时间戳精确到分钟
|
||
|
||
所有"实际开始/完成"时间都是 ISO 时间戳(含时分秒),不是日期。展示时统一 `slice(0, 16).replace('T', ' ')`。
|
||
|
||
### 4. 数据联动
|
||
|
||
修改 A 影响 B 时,**B 通过 filter A 派生**,不要双向写。例:
|
||
- 需求关联到版本:需求设 `versionId`,版本详情 `requirements.filter(r => r.versionId === id)`
|
||
- 任务/用例归属版本:新数据优先使用 `versionId`;旧 DevTask 可通过 `requirementId -> Requirement.versionId` 兼容推导
|
||
- 删除版本:清理孤儿数据(PlanTask/DevTask/TestCase/Bug + 释放 Requirement.versionId)
|
||
|
||
## 状态机概览
|
||
|
||
| 实体 | 状态流转 |
|
||
|------|---------|
|
||
| Requirement | pending_review → adopted → planned → developing → testing → released → closed(rejected 可回 pending_review)|
|
||
| VersionPlan | pending → in_progress → completed |
|
||
| DevTask | todo → in_progress → testing → submitted(终态,提测=已完成)|
|
||
| TestCase | pending → running → passed/failed/blocked |
|
||
| Bug | open → fixing → fixed → verifying → closed/rejected |
|
||
|
||
DevTask 没有"已完成"状态,"已提测"就是终态——开发交付完成,后续 Bug 单独流转。
|
||
|
||
## 数据持久化
|
||
|
||
**V2.1(当前):** 通用服务端文档表 `app_data`
|
||
- 后端:`apps/server/src/modules/data/` 提供 `GET/PUT /api/v1/data/:key`
|
||
- 数据库:Prisma `AppData` 模型,表名 `app_data`,`key` 为主键,`value` 为 JSONB
|
||
- 一致性:`GET` 返回 `updatedAt` 派生的 `version`;前端保存时带上最近读取的 `version`,后端用 `key + updatedAt` 原子更新,版本不匹配返回 `409 APP_DATA_CONFLICT`
|
||
- 前端:各 Zustand store 保持现有数据形状,通过 `apps/web/lib/server-data.ts` 读写服务端
|
||
- 覆盖范围:产品/项目/版本树、需求池、调研/产品方案/UI 计划、开发任务、测试用例、Bug、成员/角色/部门、任务类型、任务工时日志、加班记录
|
||
- 浏览器仅保留登录会话(`ftb_auth_session` / `ftb_auth_persist`),不再作为业务数据主存储
|
||
|
||
**后续 V2.2(计划):** 将 `app_data` 中稳定的数据形状逐步拆成关系表和领域 CRUD API。拆表前不要恢复业务 localStorage 缓存,避免线上部署后出现多端数据分叉。
|
||
|
||
## 生产部署层(2026-07-01)
|
||
|
||
当前仓库已补齐云服务器生产部署基线:
|
||
|
||
- `Dockerfile.web`:以 monorepo 根目录为 build context,构建 `@ftb/shared` 和 Next.js 前端,运行 `pnpm --filter web start`。
|
||
- `Dockerfile.server`:构建 `@ftb/shared` 和 NestJS 后端,执行 `prisma generate`,运行 `pnpm --filter server start:prod`。
|
||
- `docker-compose.prod.yml`:编排 `web`、`server`、`postgres`、`redis`、`nginx` 五个服务,服务间通过 Docker 内网通信,对外只暴露 Nginx 80 端口。
|
||
- `docker-compose.local.yml`:本地服务器/局域网部署入口,同样编排五个服务,默认对外暴露 `8080`,使用独立 `local_*` volumes,避免与云服务器生产数据混用。
|
||
- `deploy/nginx/default.conf.template`:同域反向代理,`/api/` 转发到 NestJS,其他路径转发到 Next.js。
|
||
- `.env.production.example` / `.env.local-server.example`:生产和本地服务器配置模板;正式部署复制为 `.env.production` 或 `.env.local-server`,不提交真实密钥。
|
||
|
||
生产持久化边界:
|
||
|
||
- 业务主数据在 PostgreSQL `postgres_data` volume 中。
|
||
- Redis AOF 在 `redis_data` volume 中。
|
||
- AI Provider 配置文件在 `server_data` volume 中,对应容器路径 `/app/apps/server/data`。
|
||
|
||
生产数据库初始化使用 Prisma migration:`pnpm --filter server db:deploy`。本地开发仍可使用 `pnpm db:migrate`。
|
||
|
||
## 权限模型(轻量)
|
||
|
||
V1 仅做前端校验,无后端鉴权:
|
||
- 版本 `members` 字段限定参与者
|
||
- 版本列表/详情按 `members.contains(currentUser)` 过滤
|
||
- 创建版本时自动加入创建者
|
||
- 版本 `members` 为空时所有人可见(兼容旧数据)
|
||
|
||
V2 接入后端后改为基于 `ProjectMember` 表的 RBAC(Owner/Admin/Member/Viewer)。
|
||
|
||
## AI Agent 层
|
||
|
||
详细规范见 `agent-spec.md`。要点:
|
||
|
||
- AI Agent 不是一个独立服务,而是嵌在前端的"特定调用入口"。当前 V3.1 仅 Prototype Decompose Agent。
|
||
- Agent 写入数据时必须带 `aiDraft: true` 标记,列表中视觉区分(紫色边)。用户编辑后自动清除标记。
|
||
- DevTask / TestCase 加入 `references[]` 字段,记录任务/用例的来源(需求 / 原型批注)。Agent 和人工创建均强制至少 1 条引用。
|
||
- 原型中有明确功能但没有匹配到关联需求时,AI 可以生成无需求ID分组草案:写入任务/用例的 `requirementName`,不创建 Requirement,不加入关联需求列表。
|
||
- 原型链接**不在 Version 上独立存储**,而是来自产品方案 (VersionPlan type=product) 已完成计划的 `resultUrl`。约定:提交产品方案的成果就是原型。
|
||
- AI 服务实现走 **NestJS 后端**(`apps/server/src/modules/ai/`),不走 Next.js API Route。
|
||
- API Key 通过 **`/admin/ai-config` 页面配置**(仅超管可见),存到 `apps/server/data/ai-config.json`,不入 git;环境变量 `ANTHROPIC_API_KEY` 作为兜底。
|
||
|
||
新增涉及 AI 的实体字段:
|
||
|
||
| 实体 | 字段 | 类型 | 说明 |
|
||
|------|------|------|------|
|
||
| DevTask | versionId | string | 执行归属版本;无需求ID分组也通过它进入版本 |
|
||
| DevTask | requirementId | string? | 正式需求 ID;无需求ID分组为空 |
|
||
| DevTask | requirementName | string? | 无正式需求 ID 时的展示分组名 |
|
||
| DevTask | references | Reference[]? | 引用来源(需求/原型批注) |
|
||
| DevTask | aiDraft | boolean? | AI 草案标记 |
|
||
| DevTask | aiDraftAt | string? | AI 生成时间戳 |
|
||
| DevTask | aiEstimateHours | number? | AI 预估耗时;执行人预估仍写 estimateHours |
|
||
| TestCase | requirementName | string? | 无正式需求 ID 时的展示分组名 |
|
||
| TestCase | references | Reference[]? | 同上 |
|
||
| TestCase | aiDraft | boolean? | 同上 |
|
||
| TestCase | aiDraftAt | string? | 同上 |
|
||
| TestCase | aiEstimateHours | number? | AI 预估耗时;执行人预估仍写 estimateHours |
|
||
|
||
## 版本模块规则层(V2.2 设计约束)
|
||
|
||
版本详情里的计划完成、需求候选和任务类型映射必须走规则层:
|
||
|
||
- `version-plan-workflow.ts`:调研/产品方案/UI 设计的子任务、需求覆盖、成果提交和完成条件。
|
||
- `requirement-selector.ts`:当前版本所属项目下可关联需求的候选筛选,默认只返回 `status === 'adopted'` 的项目需求。
|
||
- `task-category.ts`:DevTask/TestCase 共用任务类型字典,`id` 用于存储,AI 输出的 `taskTypeName` 必须是可复用类型;可复用开发类型可在采纳时自动追加到字典,测试用例未知类型回退到已有测试分类,`code` 仅作可选语义映射。
|
||
|
||
页面组件只消费规则层输出,不直接拼完成条件或候选筛选条件。
|
||
## Work Activity Daily Report Layer (2026-06-26)
|
||
|
||
The personal daily report is derived from two inputs:
|
||
|
||
- `work-activities`: append-only activity records created by successful business actions.
|
||
- `task-worklogs`: legacy/manual worklog records that still contribute hours and written work content.
|
||
|
||
`work-activity-factory.ts` owns the mapping from domain actions to reportable activity semantics. Zustand stores call this factory after a successful operation, then append the result through `useWorkActivityStore`.
|
||
|
||
`workspace-daily-report.ts` remains a pure aggregation engine. It groups today's current-user activity into delivery, progress, creation, risk, and note sections, and also detects in-progress work that started before today but has no activity or progress note today.
|
||
|
||
This is intentionally not a generic rules engine or event bus. The rule surface is explicit, typed, and local to the workspace/daily-report use case.
|
||
|
||
## Xiaobao Warning Layer (2026-06-29)
|
||
|
||
Xiaobao Warning is a version-level release-risk capability shown above `/workspace` in the main navigation. It answers whether a version can ship on the expected release date, why it may not, roughly how long it may slip, and which release window is safer.
|
||
|
||
The rule surface stays in pure frontend engines:
|
||
|
||
- `xiaobao-risk.ts`: risk score, level, forecast release date, confidence, and current snapshot.
|
||
- `xiaobao-risk-evidence.ts`: version work aggregation, daily report/activity evidence, and silent-risk detection.
|
||
- `xiaobao-risk-trend.ts`: daily snapshots, trend detection, and snapshot signatures.
|
||
- `xiaobao-risk-ai.ts`: AI trigger policy, cache signature, and backend request mapping.
|
||
|
||
Managers with `xiaobao.warning:manage` can see all unfinished versions. Non-managers with `xiaobao.warning:view` can only see unfinished versions where the current user is in `version.members`.
|
||
|
||
AI explains rule results only. It writes interpretation cache to `xiaobao-risk-insights` and never mutates Version, Requirement, DevTask, TestCase, Bug, or Member data. Risk snapshots are saved to `xiaobao-risk-snapshots` when the page is opened. The first version uses page-triggered analysis rather than a background scheduled Agent.
|
||
|
||
Per-user warning read state is saved to `xiaobao-warning-views`. The read marker stores `userId + versionId + risk signature`, so the sidebar can turn the Xiaobao badge blue when any visible risk has a completed unread update, then return to the red risk-count badge after the user opens every updated warning. AI interpretation that is still generating only shows the "updating" notice and must not produce the blue update badge yet.
|
||
## V2.2 Partitioned Domain Data Layer (2026-07-03)
|
||
|
||
V2.2 starts the move from AppData JSON documents to relation tables for high-volume domains. The first database foundation is partitioned from the start so future growth does not require a disruptive rewrite of primary keys, unique constraints, and foreign-key references.
|
||
|
||
High-growth business tables use fixed hash partitions:
|
||
|
||
- `requirements`: hash partitioned by `product_id`; primary key is `(id, product_id)`.
|
||
- `dev_tasks`, `test_cases`, and `bugs`: hash partitioned by `version_id`; primary key is `(id, version_id)`.
|
||
|
||
Append-only evidence and history tables use range partitions by `created_at`:
|
||
|
||
- `work_activities`
|
||
- `task_worklogs`
|
||
- `overtime_records`
|
||
- `xiaobao_risk_snapshots`
|
||
- `xiaobao_risk_insights`
|
||
- `ai_logs`
|
||
|
||
Partitioned tables must include the partition key in every primary key and business unique constraint. Cross-table references to partitioned tables use composite foreign keys when the referencing row naturally carries the partition key, for example `(requirement_id, requirement_product_id)` and `(test_case_id, test_case_version_id)`. Polymorphic activity references remain logical references.
|
||
|
||
Xiaobao uses two storage shapes: `xiaobao_risk_summaries` keeps one current row per version for fast reads, while `xiaobao_risk_snapshots` stores append-only history for trend analysis.
|
||
|
||
The AppData migration path is staged through a pure mapper plus preview service. `AppDataV22MigrationService.preview()` reads the allowed `app_data` keys, maps legacy JSON into partition-key-ready rows, and reports row counts plus skipped records before any insert path is enabled.
|
||
|
||
The first V2.2 read layer is query-first and AppData-compatible. `V22QueryModule` exposes scoped read APIs for version detail, requirement pool, workspace, and Xiaobao warning summaries:
|
||
|
||
- Version detail reads only the current version's requirements, plans, dev tasks, test cases, and bugs.
|
||
- Requirement pool queries require `productId`, so list/search/filter operations stay on the `requirements.product_id` partition key.
|
||
- Workspace reads only the current user's unfinished plans, dev tasks, test cases, and bugs.
|
||
- Xiaobao warning reads `xiaobao_risk_summaries` first, then maps the precomputed summary into the existing warning UI shape.
|
||
|
||
During the V2.2 compatibility window, writes still go through the existing AppData stores. The frontend consumes V2.2 relation-table results for render-heavy pages and falls back to AppData only when the V2.2 read is unavailable or empty.
|