Add a scaffold script that replaces the bq prefix with the new project name.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
bql
2026-10-27 22:46:06 +08:00
commit bc84c95a89
128 changed files with 9700 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
# 功能模块文档撰写说明
适用文件:`doc/功能模块/<模块名>/<模块名>(目录名).md`。
## 目录
- [主文档写什么](#主文档写什么)
- [一条主业务](#一条主业务)
- [多条事件业务](#多条事件业务)
- [业务文档写什么](#业务文档写什么)
单文档形态见本节「一条主业务」。拆分形态的示例是 [编辑器主文档](./功能模块(示例).md) 和 [绘制](./绘制(示例).md)。
## 主文档写什么
### 1. 模块目录结构
写出该 `src/features/[feature]` 内部的目录和承担业务的文件。空目录按仓库现状保留。
### 2. 功能定义
一段话定义这个模块做什么。下面列出它包含的业务。每条业务一句话。
### 3. 核心业务层与核心模型
分两节,不合并成「核心类」。
核心业务层是 ViewModel。每个 ViewModel 写定义、文件路径和方法。公开方法必写,承担业务的私有方法一并列出。方法写签名和这个方法做什么。页面组件不列入。
核心模型写聚合根、它包含的实体,以及不变条件。每个类型写定义、文件路径和领域方法。包含关系用类图。外部只能调用聚合根的方法。存储位置、接口报文字段不写进实体。
需要画出页面怎样向聚合提交动作时,另用用例图:参与者写 ViewModel,用例写聚合根的方法名。类图和用例图不要重复同一件事。
## 一条主业务
模块只有一条要讲述的主业务时,不另建文件。主文档增加第 4 节「核心业务」,放一张时序图。图中只用类名和方法名,图下用中文按调用顺序写清这条业务。
只有一条主业务的模块,目录是:
```markdown
## 目录
- [模块目录结构](#模块目录结构)
- [功能定义](#功能定义)
- [核心业务层](#核心业务层)
- [核心模型](#核心模型)
- [核心业务](#核心业务)
```
「核心业务」里直接放时序图。例如项目模块的保存:调用方 → `ProjectApplication.save` → `IProjectRepository`。应用层先按 `store` 选定实现,领域端口本身不区分本地和远程。`list`、`get`、`save`、`create` 写在功能定义。落笔、撤销和重做写在聚合根。
## 多条事件业务
同一模块有多条事件业务时,主文档不放时序图。第 4 节改为「业务文档」,作为索引链到同目录下的各篇。
编辑器按事件拆开,例如:
```text
doc/功能模块/编辑器模块/
├── 编辑器模块(editor).md
├── 打开项目.md
├── 绘制.md
└── 撤销与重做.md
```
主文档目录里列出这些文件。
## 业务文档写什么
每篇业务文档包含目录,以及:
1. 业务定义:这个事件做什么,从哪个操作开始。
2. 时序图:参与者使用主文档里的类名,箭头文字使用方法名。图下用中文按顺序说明这次事件。
方法表留在主文档的核心业务层。业务文档点名本事件调用的方法。

View File

@@ -0,0 +1,124 @@
# 安全模块
## 目录
- [模块目录结构](#模块目录结构)
- [功能定义](#功能定义)
- [核心业务层](#核心业务层)
- [SecurityViewModel](#securityviewmodel)
- [核心模型](#核心模型)
- [核心业务](#核心业务)
- [登录](#登录)
## 模块目录结构
```text
src/features/security/
├── application/
│ └── security_application.ts
├── domain/
│ ├── entities/
│ │ ├── auth_credentials.ts
│ │ ├── register_credentials.ts
│ │ ├── login_result.ts
│ │ └── user.ts
│ ├── enum/
│ │ └── auth_enum.ts
│ └── repositories/
│ └── i_security_repository.ts
├── i18n/
├── infra/
│ └── persistence/
│ └── security_repository.ts
├── presentation/
│ ├── profile/
│ └── security/
└── security.ts
```
`security.ts` 导入文案和 `SecurityRepository`。`profile` 页面没有用例。
## 功能定义
安全模块提供身份用例。
- `login(credentials)`:校验身份,得到 `LoginResult`。用例再把其中的访问凭证写入会话。
- `register(credentials)`:注册一个用户。
- `logout()`:清除会话中的凭证。
这些方法在 `SecurityApplication`。端口方法名不带 `remote`。
## 核心业务层
### SecurityViewModel
登录与注册页的状态。文件:`src/features/security/presentation/security/security_view_model.ts`。继承 `PageViewModel`。
状态:`activeTab`、`isLoading`、`errorMessage`、`loginForm`、`registerForm`。
| 方法 | 说明 |
|---|---|
| `switchTab(tab: AuthEnum): void` | 在 `LOGIN` 与 `REGISTER` 之间切换,并清空 `errorMessage` |
| `submit(): Promise<boolean>` | 表单已经由领域对象判定有效后,登录调用 `login`,注册调用 `register` |
| `dispose(): void` | 清空 `errorMessage` |
`submit` 在 `isValid` 不为真时,用 `validationErrorKey` 取出文案并返回 `false`。登录成功返回 `true`。注册成功后切回 `LOGIN` 并返回 `false`。
## 核心模型
| 类型 | 文件 | 定义 |
|---|---|---|
| `AuthCredentials` | `src/features/security/domain/entities/auth_credentials.ts` | 登录凭证。用户名不少于 3 个字符,密码不少于 6 个字符 |
| `RegisterCredentials` | `src/features/security/domain/entities/register_credentials.ts` | 注册凭证。在登录凭证上增加一次确认密码,两次密码必须相同 |
| `User` | `src/features/security/domain/entities/user.ts` | 用户。持有 `id`、`username`、`roles` |
| `LoginResult` | `src/features/security/domain/entities/login_result.ts` | 一次成功的登录。持有 `User` 和 `accessToken` |
| `ISecurityRepository` | `src/features/security/domain/repositories/i_security_repository.ts` | `login` 返回 `LoginResult`,`register` 接收 `RegisterCredentials` |
```mermaid
classDiagram
class AuthCredentials {
username
password
isValid
validationErrorKey
}
class RegisterCredentials {
confirmPassword
isValid
validationErrorKey
}
class User {
id
username
roles
hasPermission(permission)
}
class LoginResult {
user
accessToken
}
AuthCredentials <|-- RegisterCredentials
LoginResult --> User
```
`User.hasPermission` 在角色包含 `admin` 或指定权限时返回真。登录失败不产生 `LoginResult`。
## 核心业务
### 登录
```mermaid
sequenceDiagram
participant SecurityViewModel
participant SecurityApplication
participant ISecurityRepository
participant LoginResult
participant TokenStore
SecurityViewModel->>SecurityApplication: login(credentials)
SecurityApplication->>ISecurityRepository: login(credentials)
ISecurityRepository-->>SecurityApplication: LoginResult
SecurityApplication->>TokenStore: save(accessToken)
```
`SecurityViewModel.submit` 在 `activeTab` 为 `LOGIN` 时调用 `login`。仓储返回的是 `User` 和 `accessToken`,不是未标明类型的用户字段。用例只把 `accessToken` 交给 `TokenStore.save`。`AUTH_CHANGED` 是会话发出的界面通知,不是领域事件。通知怎样到达页面,见 [会话](../../基础应用/会话(session).md)。

View File

@@ -0,0 +1,28 @@
# 打开项目
## 目录
- [业务定义](#业务定义)
- [时序](#时序)
## 业务定义
编辑页根据路由中的 `projectId` 和 `store` 载入 `Project`,并清空尚未落笔的草稿。
本事件使用 `EditorViewModel.open`。参数 `store` 必须是枚举 `ProjectStore` 的取值。
## 时序
```mermaid
sequenceDiagram
participant EditorPage
participant EditorViewModel
participant ProjectApplication
participant Project
EditorPage->>EditorViewModel: open(id, store)
EditorViewModel->>ProjectApplication: get(store, id)
ProjectApplication-->>EditorViewModel: Project
```
`id` 或 `store` 为空时,`errorMessage` 为缺少项目。`store` 不在 `ProjectStore` 的取值里时,`errorMessage` 为项目存储无效。这两种情况都不再请求。读到的聚合写入 `project`,并记下 `store`,同时清空 `draft`。没有这个项目时,`errorMessage` 为找不到项目。

View File

@@ -0,0 +1,31 @@
# 撤销与重做
## 目录
- [业务定义](#业务定义)
- [时序](#时序)
## 业务定义
编辑器请求 `Project` 后退或前进一份文档,并把聚合保存。快照由 `Project` 持有。
本事件使用 `EditorViewModel` 的 `undo`、`redo`。
## 时序
```mermaid
sequenceDiagram
participant EditorPage
participant EditorViewModel
participant Project
participant ProjectApplication
EditorPage->>EditorViewModel: undo()
EditorViewModel->>Project: undo()
EditorViewModel->>ProjectApplication: save(store, project)
EditorPage->>EditorViewModel: redo()
EditorViewModel->>Project: redo()
EditorViewModel->>ProjectApplication: save(store, project)
```
`Project.undo` 没有更早的快照时返回失败,页面不调用 `save`。成功时聚合把当前文档换回上一份快照。`Project.redo` 没有更新的快照时同样不保存。成功时聚合恢复下一份快照。两次成功之后都按 `store` 调用 `ProjectApplication.save`。

View File

@@ -0,0 +1,32 @@
# 绘制
## 目录
- [业务定义](#业务定义)
- [时序](#时序)
## 业务定义
用户在画布上按下、移动、抬起指针。编辑器按当前工具形成草稿,再交给 `Project.addShape`。聚合接受这次落笔后,用例保存整个 `Project`。
本事件使用 `EditorViewModel` 的 `pointerDown`、`pointerMove`、`pointerUp`。是否落笔由 `Project.addShape` 决定。
## 时序
```mermaid
sequenceDiagram
participant EditorPage
participant EditorViewModel
participant Project
participant ProjectApplication
EditorPage->>EditorViewModel: pointerDown(x, y)
EditorViewModel->>EditorViewModel: draft
EditorPage->>EditorViewModel: pointerMove(x, y)
EditorViewModel->>EditorViewModel: draft.x2 / draft.y2
EditorPage->>EditorViewModel: pointerUp()
EditorViewModel->>Project: addShape(shape)
EditorViewModel->>ProjectApplication: save(store, project)
```
`pointerDown` 要求已经打开项目,并用当前 `EditorTool` 创建 `DraftShape`。`pointerMove` 只在存在草稿时调用 `DraftShape.move`。`pointerUp` 先清掉草稿,再按工具生成 `LineShape` 或 `RectShape` 并调用 `addShape`。位移小于 2 时 `addShape` 拒绝,不保存。接受时,聚合自己记下文档快照并写入图形,然后 `save` 按页面上的 `store` 写回仓储。

View File

@@ -0,0 +1,92 @@
# 编辑器模块
## 目录
- [模块目录结构](#模块目录结构)
- [功能定义](#功能定义)
- [核心业务层](#核心业务层)
- [EditorViewModel](#editorviewmodel)
- [核心模型](#核心模型)
- [业务文档](#业务文档)
## 模块目录结构
```text
src/features/editor/
├── domain/
│ ├── entities/
│ │ └── draft_shape.ts
│ └── enum/
│ └── editor_tool.ts
└── presentation/
└── editor/
├── editor.tsx
├── editor.scss
└── editor_view_model.ts
```
编辑器不定义自己的聚合和用例。文档规则在项目模块的 `Project` 上。当前工具和未落笔的草稿是编辑器自己的领域对象。
## 功能定义
编辑器把页面动作提交给已经载入的 `Project`,再交给项目用例保存。
- 按 `projectId` 和 `store` 载入聚合。
- 按当前工具收集指针草稿,落笔时调用 `Project.addShape`。
- 调用 `Project.undo` 或 `Project.redo`,再保存。
## 核心业务层
### EditorViewModel
编辑页的状态和操作。文件:`src/features/editor/presentation/editor/editor_view_model.ts`。继承 `PageViewModel`。
状态:`project`、`store`、`tool`、`draft`、`errorMessage`。撤销栈不在这里。
| 方法 | 说明 |
|---|---|
| `open(id: string, store: string \| null): Promise<void>` | 按 `store` 读取 `Project`,并清空 `draft` |
| `setTool(tool: EditorTool): void` | 切换 `EditorTool.LINE` 或 `EditorTool.RECT` |
| `pointerDown(x: number, y: number): void` | 在已有项目时记下草稿起点 |
| `pointerMove(x: number, y: number): void` | 更新草稿终点 |
| `pointerUp(): void` | 用草稿调用 `Project.addShape`,接受后保存 |
| `undo(): void` | 调用 `Project.undo`,成功后保存 |
| `redo(): void` | 调用 `Project.redo`,成功后保存 |
`store` 只记录这次页面从本地还是远程打开,供 `ProjectApplication.get` 和 `save` 选择仓储。
## 核心模型
编辑器定义当前工具和未落笔的草稿。已提交的图形和撤销规则仍在项目模块的 `Project` 上,见 [项目模块](../项目模块/项目模块(project).md)。
| 类型 | 文件 | 定义 |
|---|---|---|
| `EditorTool` | `src/features/editor/domain/enum/editor_tool.ts` | 当前工具。`LINE` 与 `RECT` |
| `DraftShape` | `src/features/editor/domain/entities/draft_shape.ts` | 一笔尚未提交的草稿。持有工具和起点、终点 |
```mermaid
flowchart LR
EditorViewModel((EditorViewModel))
open([open])
addShape([addShape])
undo([undo])
redo([redo])
Project
EditorViewModel --> open
EditorViewModel --> addShape
EditorViewModel --> undo
EditorViewModel --> redo
open --> Project
addShape --> Project
undo --> Project
redo --> Project
```
`open` 载入 `Project`。`addShape`、`undo`、`redo` 都调用这个聚合根,不调用 `Document`。
## 业务文档
- [打开项目](./打开项目.md)
- [绘制](./绘制.md)
- [撤销与重做](./撤销与重做.md)

View File

@@ -0,0 +1,133 @@
# 项目模块
## 目录
- [模块目录结构](#模块目录结构)
- [功能定义](#功能定义)
- [核心业务层](#核心业务层)
- [ProjectListViewModel](#projectlistviewmodel)
- [核心模型](#核心模型)
- [核心业务](#核心业务)
- [保存项目](#保存项目)
## 模块目录结构
```text
src/features/project/
├── application/
│ └── project_application.ts
├── domain/
│ ├── entities/
│ │ ├── document.ts
│ │ └── project.ts
│ ├── enum/
│ │ └── project_store.ts
│ └── repositories/
│ └── i_project_repository.ts
├── infra/
│ └── persistence/
│ ├── local_project_repository.ts
│ └── remote_project_repository.ts
├── presentation/
│ ├── local_list/
│ ├── project_list/
│ ├── project_row/
│ └── remote_list/
└── project.ts
```
`project.ts` 导入 `LocalProjectRepository` 和 `RemoteProjectRepository`。两者都实现 `IProjectRepository`,由应用层按 `store` 选择。`store` 是领域枚举 `ProjectStore`,取值 `local` 与 `remote`,不是 `Project` 的字段。
## 功能定义
项目模块维护 `Project` 聚合,并按 `store` 选择仓储。
- `list(store)`:列出该存储位置的项目。
- `get(store, id)`:读取一个项目。
- `save(store, project)`:调用 `Project.touch` 后保存整个聚合。
- `create(store, name)`:通过 `Project.create` 新建项目并保存。`local` 写入本地仓储,`remote` 写入远程仓储。
这些方法在 `ProjectApplication`。页面和编辑器不直接选择仓储实现。
## 核心业务层
### ProjectListViewModel
本地列表和远程列表页的状态。文件:`src/features/project/presentation/project_list/project_list_view_model.ts`。继承 `PageViewModel`。
状态:`projects`、`errorMessage`。
| 方法 | 说明 |
|---|---|
| `load(store: ProjectStore): Promise<void>` | 调用 `ProjectApplication.list`,写入 `projects` |
`ProjectStore` 定义在 `domain/enum/project_store.ts`。`ProjectListPage` 在进入页面时调用 `load`。本地页传入 `ProjectStore.LOCAL`,远程页传入 `ProjectStore.REMOTE`。
## 核心模型
`Project` 是聚合根。外部只通过它修改文档和图形。
| 类型 | 文件 | 定义 |
|---|---|---|
| `Project` | `src/features/project/domain/entities/project.ts` | 聚合根。持有 `id`、`name`、`updatedAt`、`document`,以及撤销栈 |
| `ProjectStore` | `src/features/project/domain/enum/project_store.ts` | 存储位置。`LOCAL` 与 `REMOTE` |
| `Document` | `src/features/project/domain/entities/document.ts` | 聚合内部的图形文档,持有 `shapes` |
| `LineShape` | 同上 | 一条线段 |
| `RectShape` | 同上 | 一个矩形 |
| `IProjectRepository` | `src/features/project/domain/repositories/i_project_repository.ts` | `list`、`get`、`save`。不区分存储位置 |
```mermaid
classDiagram
class Project {
id
name
updatedAt
create(name)
touch()
addShape(shape)
undo()
redo()
}
class Document {
shapes
}
class LineShape {
id
x1
y1
x2
y2
}
class RectShape {
id
x
y
width
height
}
Project --> Document
Document --> LineShape
Document --> RectShape
```
`Project.create` 生成 `id`、名称和空文档。`touch` 把 `updatedAt` 写成当前时间。
`addShape` 是唯一写入图形的方法。线段或矩形的位移小于 2 时拒绝,文档保持不变。通过时,先记下当前文档快照,再把图形放入 `Document`。
`undo` 在没有更早的快照时不做。否则把当前文档换回上一份快照。`redo` 按相反方向恢复。快照存在聚合内部,不放在页面 ViewModel 里。
## 核心业务
### 保存项目
```mermaid
sequenceDiagram
participant ProjectApplication
participant Project
participant IProjectRepository
ProjectApplication->>Project: touch()
ProjectApplication->>IProjectRepository: save(project)
```
`ProjectApplication` 按 `store` 把 `IProjectRepository` 指到 `LocalProjectRepository` 或 `RemoteProjectRepository`,再调用 `save`。`list`、`get`、`create` 使用同一次选择。领域端口上看不到 `local` 或 `remote`。

View File

@@ -0,0 +1,87 @@
# 首页
## 目录
- [模块目录结构](#模块目录结构)
- [功能定义](#功能定义)
- [核心业务层](#核心业务层)
- [HomeViewModel](#homeviewmodel)
- [核心模型](#核心模型)
- [核心业务](#核心业务)
- [列出并新建](#列出并新建)
## 模块目录结构
```text
src/features/home/
└── presentation/
└── home/
├── home.tsx
├── home.scss
└── home_view_model.ts
```
首页没有 `application`、`domain`、`infra`。
## 功能定义
首页不持有独立用例。它调用项目模块的 `list` 和 `create`。
- 进入页面时按 `local` 与 `remote` 各列出一份项目。
- 新建时调用 `ProjectApplication.create`。聚合由 `Project.create` 生成,再按 `store` 保存。
已登录状态来自安全模块一侧的 `authenticated`。未登录时,`projectId` 不交给编辑器。
## 核心业务层
### HomeViewModel
首页的项目列表状态。文件:`src/features/home/presentation/home/home_view_model.ts`。继承 `PageViewModel`。
状态:`cloudProjects`、`localProjects`、`errorMessage`。
| 方法 | 说明 |
|---|---|
| `ready(): Promise<void>` | 进入页面时调用 `reload` |
| `reload(): Promise<void>` | 调用 `ProjectApplication.list(ProjectStore.REMOTE)` 和 `list(ProjectStore.LOCAL)` |
| `createLocal(): Promise<Project>` | 调用 `ProjectApplication.create(ProjectStore.LOCAL)` |
`local` 和 `remote` 是传给用例的 `store`,用来选择仓储。它们不是 `Project` 的属性。
## 核心模型
首页不定义实体。它使用项目模块的聚合根 `Project`。
```mermaid
flowchart LR
HomeViewModel((HomeViewModel))
list([list])
create([create])
Project
HomeViewModel --> list
HomeViewModel --> create
list --> Project
create --> Project
```
`list` 的结果写入 `cloudProjects` 或 `localProjects`。`create` 返回新建的 `Project`。
## 核心业务
### 列出并新建
```mermaid
sequenceDiagram
participant HomeViewModel
participant ProjectApplication
participant Project
HomeViewModel->>ProjectApplication: list(remote)
HomeViewModel->>ProjectApplication: list(local)
HomeViewModel->>ProjectApplication: create(local)
ProjectApplication->>Project: create(name)
ProjectApplication-->>HomeViewModel: Project
```
`ready` 触发两次 `list`。其中一次失败时,对应列表为空,错误写入 `errorMessage`。`create` 返回的 `Project.id` 与本次使用的 `store` 一起交给编辑器。