- 需求文档 §3.15: 手动归档(⋯菜单+连带子任务)/自动归档(项目级档位 3天/一周/一月/永不)/归档排除/游标分页大量查询/恢复/彻底删除/配置入口/权限 - 功能设计 §15: Project.autoArchive + Task.archived/archivedAt/archiveSource、 归档/恢复/归档查询 API、自动归档双保险执行(读时惰性+小时cron)、 边界处理; 覆盖自检表更新为 157 条 - 数据库设计: Project 补 notes/taskKinds/icon/autoArchive 字段, Task 补归档三字段与两索引, 迁移清单标注 20261026000000_task_archive 待实施
468 lines
21 KiB
Markdown
468 lines
21 KiB
Markdown
# iBoard V0.2 任务模块 数据库设计文档
|
||
|
||
| 项 | 内容 |
|
||
| --- | --- |
|
||
| 版本 | V0.2(对应分支 `dev-0.2`,应用版本 `0.2.0`) |
|
||
| 文档状态 | **设计基线**(待评审,未落库) |
|
||
| 数据库 | MariaDB / MySQL 8(Prisma `provider = "mysql"`) |
|
||
| 字符集 | `utf8mb4 / utf8mb4_unicode_ci` |
|
||
| 时区约定 | 应用层统一 `Asia/Shanghai`,库内 `DATETIME` 存 UTC |
|
||
| 相关文件 | `prisma/schema.prisma`、`prisma/migrations/20261024xxxxxx_add_task_module/migration.sql` |
|
||
| 关联文档 | `任务模块需求文档.md`、`任务模块设计文档-框架设计.md`、`任务模块功能设计.md`、`任务模块界面设计.html` |
|
||
|
||
---
|
||
|
||
## 1. 概述
|
||
|
||
任务模块新增 **6 张表**,并把既有 `User` 表补上反向关系(不改 `User` 结构):
|
||
|
||
| 表 | 作用 | 归属 |
|
||
| --- | --- | --- |
|
||
| `Project` | 项目(隔离与权限单位) | 项目域 |
|
||
| `ProjectMember` | 项目成员(权限关系) | 项目域 |
|
||
| `BoardColumn` | 看板列(名称 / 顺序 / 状态绑定 / 保留选项) | 看板域 |
|
||
| `Task` | 任务与子任务(自关联,一层) | 任务域 |
|
||
| `TaskMember` | 任务成员(多人标记) | 任务域 |
|
||
| `TaskTag` | 任务标签 | 任务域 |
|
||
|
||
### 1.1 设计原则
|
||
|
||
1. **列是实体,不是枚举。** 看板列允许增删改与排序(T-COL),若仍用 `plan/doing/action/done` 枚举,则"改名"无从表达、"排序"只能堆在 JSON 里、"删除列"会留下悬空字符串。因此独立建 `BoardColumn` 表,`Task` 用 `columnId` 外键引用。
|
||
2. **状态与列双落库,互不推导。** `Task.status`(三值)与 `Task.columnId` 是两个正交维度;未绑定的列(默认 `Action`)必须允许二者不一致(T-BD-08、T-BD-16),任何"由状态反推列"或"由列反推状态"的写法都会丢失信息。联动只在写接口中按项目配置即时裁决。
|
||
3. **配置不下沉到任务。** 绑定与保留选项只存在 `BoardColumn` 上,任务不冗余任何配置副本——改配置立即对所有任务生效,且不需要回填。
|
||
4. **按项目隔离。** 所有业务表都可通过 `projectId` 归集,权限校验统一查 `ProjectMember`。
|
||
5. **软删除优先。** 项目用 `archived`;任务删除为级联物理删除(T-ST-11),但删除前有二次确认。
|
||
6. **迁移只增不改。** 新增需求一律新迁移文件,不修改已执行的迁移。
|
||
|
||
---
|
||
|
||
## 2. 概念模型(ER)
|
||
|
||
```
|
||
┌────────────────┐
|
||
│ User │ (既有表, 仅补反向关系)
|
||
└───┬────────┬───┘
|
||
│ │
|
||
┌──────────▼──┐ ┌──▼──────────────┐
|
||
│ProjectMember│ │ TaskMember │
|
||
│(权限关系) │ │ (任务成员) │
|
||
└──────┬──────┘ └──▲──────────────┘
|
||
│ * │ *
|
||
│ │
|
||
┌───▼──────┐ ┌─┴──────┐
|
||
│ Project │1─*│ Task │*─1┌────────────┐
|
||
└───┬──────┘ └───┬────┘ │ BoardColumn│
|
||
│ * │ * └────────────┘
|
||
┌────▼───────┐ │ ▲
|
||
│BoardColumn │1────*┘ │
|
||
└────────────┘ (columnId) │
|
||
│
|
||
┌────────┐ │
|
||
│TaskTag │*──────────────────┘
|
||
└────────┘
|
||
|
||
Task 自关联: parentId ── 子任务(一层, 仅顶层任务可作父)
|
||
```
|
||
|
||
基数说明:
|
||
|
||
| 关系 | 基数 | 级联 |
|
||
| --- | --- | --- |
|
||
| User → ProjectMember | 1 : N | Cascade |
|
||
| Project → ProjectMember | 1 : N | Cascade |
|
||
| Project → BoardColumn | 1 : N | Cascade |
|
||
| Project → Task | 1 : N | Cascade |
|
||
| BoardColumn → Task | 1 : N | **RESTRICT**(有任务的列不可直接删) |
|
||
| Task → Task(parent) | 1 : N | Cascade |
|
||
| Task → TaskMember | 1 : N | Cascade |
|
||
| Task → TaskTag | 1 : N | Cascade |
|
||
| User → Task(assignee / creator) | 1 : N | SetNull / Cascade(见下) |
|
||
|
||
> `BoardColumn → Task` 使用 `RESTRICT` 而非 `Cascade`:防止误删列时静默丢任务。删除列必须先在其接口内把任务迁移走(事务内改 `columnId`),迁移完成后再删列。
|
||
|
||
---
|
||
|
||
## 3. 表结构
|
||
|
||
### 3.1 `Project` — 项目
|
||
|
||
| 字段 | 类型 | 空 | 默认 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | `INT` PK AI | 否 | — | 主键 |
|
||
| `name` | `VARCHAR(50)` | 否 | — | 项目名 |
|
||
| `ownerId` | `INT` | 否 | — | 创建人(可退出项目,故与成员表分离) |
|
||
| `isPersonal` | `BOOLEAN` | 否 | `false` | 个人默认项目:不可删、不可加成员(T-PJ-02/03) |
|
||
| `archived` | `BOOLEAN` | 否 | `false` | 归档(T-PJ-09) |
|
||
| `notes` | `JSON` | 否 | `[]` | 项目信息块 `[{title, content}]`(T-PJ-13) |
|
||
| `taskKinds` | `JSON` | **是** | `NULL` | 任务类型标签列表;`NULL` 时服务端回落默认四类(T-TK-14) |
|
||
| `icon` | `TEXT` | **是** | `NULL` | 项目图标 data URL(客户端裁剪 96x96 JPEG) |
|
||
| `autoArchive` | `VARCHAR(8)` | 否 | `'never'` | 自动归档档位:`3d`/`1w`/`1m`/`never`(T-ARC-02) |
|
||
| `createdAt` | `DATETIME(3)` | 否 | `now()` | — |
|
||
| `updatedAt` | `DATETIME(3)` | 否 | — | `@updatedAt` |
|
||
|
||
索引:
|
||
|
||
| 名称 | 字段 | 用途 |
|
||
| --- | --- | --- |
|
||
| `idx_project_owner` | `(ownerId)` | 查"我的项目" |
|
||
| `idx_project_archived` | `(archived)` | 归档分区 |
|
||
|
||
**约束说明**
|
||
- `isPersonal` 唯一性由应用层保证(每个用户至多一个):`UNIQUE(ownerId, isPersonal)` 不可用(普通项目也是 `false`),改为在创建时查询判重。
|
||
|
||
### 3.2 `ProjectMember` — 项目成员
|
||
|
||
| 字段 | 类型 | 空 | 默认 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | `INT` PK AI | 否 | — | — |
|
||
| `projectId` | `INT` | 否 | — | FK → `Project.id` `ON DELETE CASCADE` |
|
||
| `userId` | `INT` | 否 | — | FK → `User.id` `ON DELETE CASCADE` |
|
||
| `permission` | `VARCHAR(16)` | 否 | `'member'` | 预留权限分级(T-PM-03),本期恒为 `member` |
|
||
| `createdAt` | `DATETIME(3)` | 否 | `now()` | 加入时间 |
|
||
|
||
索引:
|
||
|
||
| 名称 | 字段 | 唯一 | 用途 |
|
||
| --- | --- | --- | --- |
|
||
| `uk_member_project_user` | `(projectId, userId)` | ✔ | 防重复加入;权限查询 |
|
||
| `idx_member_user` | `(userId)` | — | "我参与的项目" |
|
||
|
||
> **注意**:视图记忆与上次项目**不在此表**。视图记忆存设备端(T-VW-06、T-DT-06),早期草案把它放在 `ProjectMember` 上,已在评审中否决。
|
||
|
||
### 3.3 `BoardColumn` — 看板列
|
||
|
||
**本模块新增的核心实体**,承载三件事:显示顺序、状态绑定、已完成保留选项。
|
||
|
||
| 字段 | 类型 | 空 | 默认 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | `INT` PK AI | 否 | — | — |
|
||
| `projectId` | `INT` | 否 | — | FK → `Project.id` `ON DELETE CASCADE` |
|
||
| `name` | `VARCHAR(20)` | 否 | — | 列名,项目内唯一(T-COL-07) |
|
||
| `sortOrder` | `INT` | 否 | `0` | 显示顺序;**决定自动归位取"第一个匹配列"**(T-BD-15) |
|
||
| `boundStatus` | `VARCHAR(8)` | **是** | `NULL` | `NULL`=不绑定;或 `todo`/`doing`/`done`(T-BD-02) |
|
||
| `retention` | `VARCHAR(8)` | 否 | `'forever'` | `never`/`today`/`3d`/`week`/`month`/`forever`(T-FL-02) |
|
||
| `createdAt` | `DATETIME(3)` | 否 | `now()` | — |
|
||
| `updatedAt` | `DATETIME(3)` | 否 | — | — |
|
||
|
||
索引:
|
||
|
||
| 名称 | 字段 | 唯一 | 用途 |
|
||
| --- | --- | --- | --- |
|
||
| `uk_column_project_name` | `(projectId, name)` | ✔ | 列名不重名(T-COL-07) |
|
||
| `idx_column_project_sort` | `(projectId, sortOrder)` | — | 看板渲染与归位查找 |
|
||
|
||
**默认数据**(新建项目时由应用层在事务内写入,T-COL-01、T-BD-09):
|
||
|
||
| sortOrder | name | boundStatus | retention |
|
||
| --- | --- | --- | --- |
|
||
| 0 | `Plan` | `todo` | `forever` |
|
||
| 1 | `Doing` | `doing` | `forever` |
|
||
| 2 | `Action` | `NULL` | `forever` |
|
||
| 3 | `Done` | `done` | `forever` |
|
||
|
||
> **为什么保留选项默认为 `forever` 而不是 `never`**:默认应"看得见",避免用户误以为数据丢失;隐藏是主动行为。
|
||
|
||
**约束与校验**
|
||
|
||
| 规则 | 实现层 |
|
||
| --- | --- |
|
||
| 列数 1~10(上限待确认,T-COL-06) | 应用层:新增/删除前计数校验 |
|
||
| 至少保留 1 列(T-EX-17) | 应用层:删除前 count > 1 |
|
||
| 删列必须指定迁移目标(T-EX-16) | 应用层:接口必须带 `moveToColumnId` |
|
||
| `boundStatus` 取值合法 | 应用层白名单校验 |
|
||
| `retention` 取值合法 | 应用层白名单校验 |
|
||
|
||
### 3.4 `Task` — 任务 / 子任务
|
||
|
||
| 字段 | 类型 | 空 | 默认 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | `INT` PK AI | 否 | — | — |
|
||
| `projectId` | `INT` | 否 | — | FK → `Project.id` `ON DELETE CASCADE` |
|
||
| `columnId` | `INT` | 否 | — | FK → `BoardColumn.id` `ON DELETE RESTRICT`(T-BD-01) |
|
||
| `parentId` | `INT` | **是** | `NULL` | 非空 = 一级子任务;FK → `Task.id` `ON DELETE CASCADE`(T-ST-01/11) |
|
||
| `title` | `VARCHAR(200)` | 否 | — | 唯一必填(T-TK-02);应用层限 30 字(T-TK-14) |
|
||
| `description` | `TEXT` | 否 | `''` | 快速添加超长文本的余文并入此处(T-TK-14) |
|
||
| `kind` | `VARCHAR(20)` | **是** | `NULL` | 任务类型标签,取值见 `Project.taskKinds` |
|
||
| `markDate` | `DATETIME(3)` | **是** | `NULL` | 时间标记,完整日期存储、展示 `MM-dd`(T-TK-15;迁移 20261025120000) |
|
||
| `status` | `VARCHAR(8)` | 否 | `'todo'` | `todo`/`doing`/`done`(T-TK-03) |
|
||
| `doneAt` | `DATETIME(3)` | **是** | `NULL` | 完成时刻;**保留选项判定依据**(T-FL-10) |
|
||
| `assigneeId` | `INT` | **是** | `NULL` | 负责人(单人);新建默认创建者(T-TK-09) |
|
||
| `creatorId` | `INT` | 否 | — | 创建人;拷贝时为操作者(T-CP-04) |
|
||
| `extras` | `JSON` | 否 | `[]` | 扩展信息卡片 `[{label,value,url?}]`(T-TK-04/05) |
|
||
| `sortOrder` | `INT` | 否 | `0` | 列内/列表内排序 |
|
||
| `archived` | `BOOLEAN` | 否 | `false` | 归档标记;主查询一律 `archived = false`(T-ARC-01/03) |
|
||
| `archivedAt` | `DATETIME(3)` | **是** | `NULL` | 归档时刻;归档查询的过滤/排序键(T-ARC-04) |
|
||
| `archiveSource` | `VARCHAR(8)` | **是** | `NULL` | `'manual'` / `'auto'` 溯源(T-ARC-01/02) |
|
||
| `createdAt` | `DATETIME(3)` | 否 | `now()` | — |
|
||
| `updatedAt` | `DATETIME(3)` | 否 | — | — |
|
||
|
||
外键与索引:
|
||
|
||
| 名称 | 字段 | 类型 | 说明 |
|
||
| --- | --- | --- | --- |
|
||
| `fk_task_project` | `(projectId)` | Cascade | 删项目连带删任务 |
|
||
| `fk_task_column` | `(columnId)` | **Restrict** | 有任务的列不可直接删(见 §2 说明) |
|
||
| `fk_task_parent` | `(parentId)` | Cascade | 删父任务连带删子任务(T-EX-07) |
|
||
| `fk_task_assignee` | `(assigneeId)` | SetNull | 用户删除时清空负责人 |
|
||
| `fk_task_creator` | `(creatorId)` | Cascade | 用户删除时连带删其创建的任务* |
|
||
| `idx_task_project_column` | `(projectId, columnId)` | — | 看板按列查询 |
|
||
| `idx_task_project_status` | `(projectId, status)` | — | 列表/统计 |
|
||
| `idx_task_parent` | `(parentId)` | — | 取子任务 |
|
||
| `idx_task_done_at` | `(projectId, doneAt)` | — | 保留窗口计算 |
|
||
| `idx_task_project_archived` | `(projectId, archived, archivedAt)` | — | 已归档查询(T-ARC-04) |
|
||
| `idx_task_project_status_arch` | `(projectId, status, archived)` | — | 主列表/看板排除归档(T-ARC-03) |
|
||
|
||
> \* `creatorId` 用 Cascade 会丢失"他人创建的任务"的历史;更稳妥的做法是 `Restrict` 并禁止删除有任务的用户。**建议改为 `Restrict`**,与既有 `User` 删除行为保持一致(本期系统无删除用户入口)。
|
||
|
||
**校验规则**
|
||
|
||
| 规则 | 位置 |
|
||
| --- | --- |
|
||
| `title` trim 后非空且 ≤ 200 | 应用层 |
|
||
| `parentId` 非空时,其父任务 `parentId` 必须为空(禁止二级,T-ST-01) | 应用层 |
|
||
| 子任务 `projectId` == 父任务 `projectId`(T-EX-12) | 应用层 + 事务内校验 |
|
||
| `parentId` 不得指向自身或其后代(T-EX-15) | 应用层 |
|
||
| `status='done'` 时写 `doneAt=now()`;退出 `done` 时置 `NULL` | 应用层(领域统一裁决) |
|
||
| 归档/恢复父任务连带全部后代,同事务内更新 `archived`/`archivedAt`/`archiveSource`(T-ARC-01/05) | 应用层 |
|
||
| `autoArchive` 枚举 `3d`/`1w`/`1m`/`never`,非法 400(T-ARC-02) | 应用层 |
|
||
| `assigneeId` 必须是 `ProjectMember` 中的用户(T-AS-03) | 应用层 |
|
||
|
||
### 3.5 `TaskMember` — 任务成员
|
||
|
||
| 字段 | 类型 | 空 | 默认 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | `INT` PK AI | 否 | — | — |
|
||
| `taskId` | `INT` | 否 | — | FK → `Task.id` `ON DELETE CASCADE` |
|
||
| `userId` | `INT` | 否 | — | FK → `User.id` `ON DELETE CASCADE` |
|
||
|
||
索引:`uk_task_member (taskId, userId)` 唯一。
|
||
|
||
### 3.6 `TaskTag` — 任务标签
|
||
|
||
| 字段 | 类型 | 空 | 默认 | 说明 |
|
||
| --- | --- | --- | --- | --- |
|
||
| `id` | `INT` PK AI | 否 | — | — |
|
||
| `taskId` | `INT` | 否 | — | FK → `Task.id` `ON DELETE CASCADE` |
|
||
| `name` | `VARCHAR(20)` | 否 | — | 标签名 |
|
||
|
||
索引:`idx_tag_task (taskId)`。
|
||
|
||
---
|
||
|
||
## 4. Prisma Schema(草案)
|
||
|
||
```prisma
|
||
model Project {
|
||
id Int @id @default(autoincrement())
|
||
name String @db.VarChar(50)
|
||
ownerId Int
|
||
isPersonal Boolean @default(false)
|
||
archived Boolean @default(false)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
owner User @relation("ProjectOwner", fields: [ownerId], references: [id])
|
||
members ProjectMember[]
|
||
columns BoardColumn[]
|
||
tasks Task[]
|
||
|
||
@@index([ownerId])
|
||
@@index([archived])
|
||
@@map("Project")
|
||
}
|
||
|
||
model ProjectMember {
|
||
id Int @id @default(autoincrement())
|
||
projectId Int
|
||
userId Int
|
||
permission String @default("member") @db.VarChar(16)
|
||
createdAt DateTime @default(now())
|
||
|
||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([projectId, userId])
|
||
@@index([userId])
|
||
@@map("ProjectMember")
|
||
}
|
||
|
||
model BoardColumn {
|
||
id Int @id @default(autoincrement())
|
||
projectId Int
|
||
name String @db.VarChar(20)
|
||
sortOrder Int @default(0)
|
||
boundStatus String? @db.VarChar(8) // null | todo | doing | done
|
||
retention String @default("forever") @db.VarChar(8)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
tasks Task[]
|
||
|
||
@@unique([projectId, name])
|
||
@@index([projectId, sortOrder])
|
||
@@map("BoardColumn")
|
||
}
|
||
|
||
model Task {
|
||
id Int @id @default(autoincrement())
|
||
projectId Int
|
||
columnId Int
|
||
parentId Int?
|
||
title String @db.VarChar(200)
|
||
description String @default("") @db.Text
|
||
status String @default("todo") @db.VarChar(8)
|
||
doneAt DateTime?
|
||
assigneeId Int?
|
||
creatorId Int
|
||
extras Json @default("[]")
|
||
sortOrder Int @default(0)
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
column BoardColumn @relation(fields: [columnId], references: [id], onDelete: Restrict)
|
||
parent Task? @relation("Subtask", fields: [parentId], references: [id], onDelete: Cascade)
|
||
children Task[] @relation("Subtask")
|
||
assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id], onDelete: SetNull)
|
||
creator User @relation("TaskCreator", fields: [creatorId], references: [id], onDelete: Restrict)
|
||
|
||
members TaskMember[]
|
||
tags TaskTag[]
|
||
|
||
@@index([projectId, columnId])
|
||
@@index([projectId, status])
|
||
@@index([parentId])
|
||
@@index([projectId, doneAt])
|
||
@@map("Task")
|
||
}
|
||
|
||
model TaskMember {
|
||
id Int @id @default(autoincrement())
|
||
taskId Int
|
||
userId Int
|
||
|
||
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
||
@@unique([taskId, userId])
|
||
@@map("TaskMember")
|
||
}
|
||
|
||
model TaskTag {
|
||
id Int @id @default(autoincrement())
|
||
taskId Int
|
||
name String @db.VarChar(20)
|
||
|
||
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([taskId])
|
||
@@map("TaskTag")
|
||
}
|
||
```
|
||
|
||
`User` 需补反向关系(不改字段):
|
||
|
||
```prisma
|
||
model User {
|
||
// …既有字段…
|
||
ownedProjects Project[] @relation("ProjectOwner")
|
||
projectMembers ProjectMember[]
|
||
assignedTasks Task[] @relation("TaskAssignee")
|
||
createdTasks Task[] @relation("TaskCreator")
|
||
taskMembers TaskMember[]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 关键查询
|
||
|
||
| 场景 | 查询 |
|
||
| --- | --- |
|
||
| 我的项目 | `SELECT p.* FROM Project p JOIN ProjectMember m ON m.projectId=p.id WHERE m.userId=? AND NOT p.archived` |
|
||
| 项目详情 + 列 | `SELECT * FROM BoardColumn WHERE projectId=? ORDER BY sortOrder` |
|
||
| 看板任务(含子任务) | 顶层:`WHERE projectId=? AND parentId IS NULL`;子任务:`WHERE parentId IN (...)` 一次取回后在内存分组(避免 N+1) |
|
||
| 权限校验 | `SELECT 1 FROM ProjectMember WHERE projectId=? AND userId=?` |
|
||
| 保留窗口过滤 | 服务端可选:`status <> 'done' OR (doneAt >= ?)`;前端另有一份同口径实现 |
|
||
| 自动归位选列 | 内存中对已加载的 `columns`(按 sortOrder)`find(c => c.boundStatus === status)` |
|
||
| 已归档查询(T-ARC-04) | `WHERE projectId=? AND archived=1 [AND kind=?] [AND columnId=?] [AND archivedAt BETWEEN ? AND ?] [AND title LIKE ?] ORDER BY archivedAt DESC, id DESC LIMIT ?+1`;多取 1 条判定 `nextCursor` |
|
||
|
||
**防 N+1**:一次请求返回 `{ project, columns, members, tasks }`,子任务与父任务同批返回,由前端按 `parentId` 分组。
|
||
|
||
---
|
||
|
||
## 6. 迁移与初始化
|
||
|
||
### 6.1 迁移文件
|
||
|
||
`prisma/migrations/20261024xxxxxx_add_task_module/migration.sql`:
|
||
|
||
1. 建 `Project`、`ProjectMember`、`BoardColumn`、`Task`、`TaskMember`、`TaskTag` 六张表;
|
||
2. 建外键(注意 `Task.columnId` 为 `RESTRICT`);
|
||
3. 建唯一索引与普通索引;
|
||
4. **不做数据回填**(本模块为新功能,无历史数据)。
|
||
|
||
后续增量迁移:
|
||
|
||
| 迁移 | 内容 | 状态 |
|
||
| --- | --- | --- |
|
||
| `20261022120000_add_checkin_checklist` | 打卡/清单模块 | 已上线 |
|
||
| `20261025030000_task_kind` | `Project.taskKinds` / `Task.kind` | 已上线 |
|
||
| `20261025120000_task_mark_date` | `Task.markDate` | 已上线 |
|
||
| `20261025130000_project_icon` | `Project.icon` | 已上线 |
|
||
| `20261026000000_task_archive` | 任务归档三字段 + 两索引(T-ARC,DDL 见 §3.1/§3.4 与功能设计 §15) | **待实施** |
|
||
|
||
### 6.2 个人默认项目初始化
|
||
|
||
```sql
|
||
-- 应用层逻辑(伪 SQL),首次访问 /api/projects 时执行
|
||
INSERT INTO Project (name, ownerId, isPersonal, createdAt, updatedAt)
|
||
SELECT CONCAT(u.name, '的任务'), u.id, 1, NOW(3), NOW(3)
|
||
FROM User u
|
||
WHERE u.id = ?
|
||
AND NOT EXISTS (SELECT 1 FROM Project p WHERE p.ownerId = u.id AND p.isPersonal = 1);
|
||
|
||
-- 随后为该项目的 4 个默认列插入 BoardColumn(同事务)
|
||
```
|
||
|
||
### 6.3 新建普通项目
|
||
|
||
同事务内:建 `Project`(`isPersonal=0`)→ 写 `ProjectMember(owner)` → 写 4 个默认 `BoardColumn`。**任一步失败整体回滚**,避免出现"无列项目"。
|
||
|
||
---
|
||
|
||
## 7. 容量与性能估算
|
||
|
||
| 项 | 估算 | 依据 |
|
||
| --- | --- | --- |
|
||
| 单项目任务 | 千级 / 年 | T-NF-05 |
|
||
| 单项目列 | ≤ 10 | T-COL-06 |
|
||
| `Task` 行宽 | ~400 B + JSON | 含 `extras` |
|
||
| 单项目年增量 | ≈ 400 KB | 1k 任务 × 400B |
|
||
| 索引开销 | 约表大小 30% | 4 个二级索引 |
|
||
|
||
**无需分表**;按 `projectId` 的查询均可走索引。
|
||
|
||
---
|
||
|
||
## 8. 与既有模块的关系
|
||
|
||
| 项 | 说明 |
|
||
| --- | --- |
|
||
| `User` 表 | 仅补反向关系,**不改结构**,不影响打卡/宏观/监控模块 |
|
||
| 打卡模块(`Habit`/`CheckIn`) | 完全独立,同为"按用户隔离";任务模块改为"按项目隔离" |
|
||
| 迁移顺序 | 在 `20261022120000_add_checkin_checklist` 之后追加,互不依赖 |
|
||
| 时区 | 沿用 Asia/Shanghai 应用层口径;`doneAt` 用 `DATETIME(3)` 存 UTC |
|
||
|
||
---
|
||
|
||
## 9. 未决事项
|
||
|
||
| # | 事项 | 当前处理 |
|
||
| --- | --- | --- |
|
||
| 1 | 列数上限是否 10 | 按 10 设计(T-COL-06 标注"待确认"),若定其他值只改常量 |
|
||
| 2 | `Task.creatorId` 外键行为 | 草案建议 `Restrict`(见 §3.4 注),需与"用户删除策略"一并确认 |
|
||
| 3 | 孤儿项目(无成员)的维护 | 本期不处理(T-PJ-08),由后台流程清理;数据库层保留数据 |
|
||
| 4 | `extras` 条目数量上限 | 建议 ≤ 20,未在需求中约束,设计预留 |
|