466 lines
14 KiB
Markdown
466 lines
14 KiB
Markdown
# access-manage
|
||
|
||
`access-manage` 是一个用于学习 MySQL CRUD 的门店员工权限管理后端项目。
|
||
|
||
项目刻意使用 `mysql2` 直接写 SQL,不引入 ORM。这样可以更直接地学习表设计、索引、外键、事务、软删除、参数化查询和分层代码组织。
|
||
|
||
## 技术栈
|
||
|
||
- Node.js + TypeScript
|
||
- Fastify
|
||
- MySQL 8.4
|
||
- mysql2
|
||
- zod
|
||
- Docker Compose
|
||
|
||
## 项目能力
|
||
|
||
- 门店管理:查询、新增、修改、软删除门店。
|
||
- 角色管理:查询、新增、修改、删除角色。
|
||
- 员工管理:分页查询、新增、修改、启用/停用、软删除员工。
|
||
- 员工角色:一个员工可以绑定多个角色。
|
||
- 数据校验:使用 zod 校验路径参数、查询参数和请求体。
|
||
- 数据库迁移:使用 `migrations/*.sql` 管理建表和初始化数据。
|
||
- 事务处理:创建/更新员工和角色绑定时使用事务,避免部分成功。
|
||
|
||
## 目录结构
|
||
|
||
```text
|
||
.
|
||
├── .agents/
|
||
│ └── skills/
|
||
│ └── readme-structure-sync/ # README 和目录结构同步维护规则
|
||
├── .env.development # 本地开发环境变量文件,不提交到仓库
|
||
├── .gitignore # Git 忽略规则,排除本地配置、依赖和编译产物
|
||
├── AGENTS.md # Codex/Agent 入口指令,当前指向 RTK.md
|
||
├── RTK.md # 项目协作规则和开发约定
|
||
├── migrations/ # 数据库迁移 SQL
|
||
│ ├── 001_initial_schema.sql # 创建基础表结构
|
||
│ └── 002_seed_demo_data.sql # 初始化演示门店和角色
|
||
├── src/
|
||
│ ├── app.ts # 创建 Fastify 应用、注册路由、统一错误处理
|
||
│ ├── server.ts # 启动 HTTP 服务和优雅停机
|
||
│ ├── config/
|
||
│ │ └── env.ts # 环境变量校验
|
||
│ ├── db/
|
||
│ │ ├── migrate.ts # 执行 migrations 目录下的 SQL
|
||
│ │ └── pool.ts # MySQL 连接池
|
||
│ ├── modules/
|
||
│ │ ├── catalog/ # 门店和角色模块
|
||
│ │ └── employees/ # 员工 CRUD 模块
|
||
│ └── shared/ # 通用响应结构和业务错误
|
||
├── docker-compose.yml # 本地 MySQL
|
||
├── package.json
|
||
├── pnpm-lock.yaml
|
||
├── README.md
|
||
└── tsconfig.json
|
||
```
|
||
|
||
### 目录和关键文件说明
|
||
|
||
| 路径 | 作用 |
|
||
| --- | --- |
|
||
| `.agents/skills/readme-structure-sync/` | 项目内 skill。约定当目录、重要文件或 `package.json` 脚本变化时,同步更新 README。 |
|
||
| `.env.development` | 当前项目本地开发使用的环境变量文件,`package.json` 脚本会显式读取它;该文件只保留在本机,不提交到仓库。 |
|
||
| `.gitignore` | 忽略本地环境变量、依赖目录、编译产物和系统文件,避免把无关文件推到仓库。 |
|
||
| `AGENTS.md` | Agent 工具读取的入口文件,当前通过 `@RTK.md` 引入项目规则。 |
|
||
| `RTK.md` | 本项目的协作规则,例如使用中文说明、保持分层、改目录时同步 README。 |
|
||
| `migrations/` | 数据库迁移目录。所有建表、改表、初始化基础数据的 SQL 都放在这里。 |
|
||
| `src/app.ts` | 创建 Fastify 应用,注册路由,处理健康检查和全局错误。 |
|
||
| `src/server.ts` | 真正启动 HTTP 服务,监听端口,并处理优雅停机。 |
|
||
| `src/config/env.ts` | 使用 zod 校验 `.env.development` 中的环境变量,避免配置错误拖到请求阶段才暴露。 |
|
||
| `src/db/migrate.ts` | 执行 `migrations/*.sql`,并用 `schema_migrations` 记录已执行迁移。 |
|
||
| `src/db/pool.ts` | 创建 MySQL 连接池,提供数据库健康检查和关闭连接的方法。 |
|
||
| `src/modules/catalog/` | 门店和角色模块,负责基础资料接口。 |
|
||
| `src/modules/employees/` | 员工模块,负责员工分页、详情、新增、修改、状态变更和软删除。 |
|
||
| `src/shared/` | 跨模块复用的响应结构和业务错误类型。 |
|
||
| `docker-compose.yml` | 本地开发用 MySQL 容器配置。 |
|
||
| `package.json` | 项目信息、依赖和常用脚本;脚本会读取现有 `.env.development`。 |
|
||
| `pnpm-lock.yaml` | pnpm 锁文件,保证依赖版本一致。 |
|
||
| `tsconfig.json` | TypeScript 编译配置。 |
|
||
|
||
## 分层约定
|
||
|
||
项目按 `controller -> service -> repository` 分层:
|
||
|
||
- `controller`:处理 HTTP 请求,校验入参,调用 service,返回响应。
|
||
- `service`:处理业务规则,例如门店是否存在、手机号是否重复、角色能否删除。
|
||
- `repository`:只负责 SQL 查询和数据库字段转换。
|
||
- `schema`:使用 zod 定义接口入参规则。
|
||
- `types`:定义接口输入输出类型。
|
||
|
||
这个分层是学习重点之一。新增接口时,优先保持同样结构。
|
||
|
||
## 环境准备
|
||
|
||
需要先安装:
|
||
|
||
- Node.js
|
||
- pnpm
|
||
- Docker Desktop
|
||
|
||
安装依赖:
|
||
|
||
```bash
|
||
pnpm install
|
||
```
|
||
|
||
本地开发直接使用现有的 `.env.development`,当前配置如下:
|
||
|
||
```env
|
||
NODE_ENV=development
|
||
PORT=3500
|
||
|
||
DB_HOST=127.0.0.1
|
||
DB_PORT=3307
|
||
DB_USER=access_user
|
||
DB_PASSWORD=access_pass
|
||
DB_NAME=access_manage
|
||
DB_CONNECTION_LIMIT=10
|
||
```
|
||
|
||
## 启动步骤
|
||
|
||
1. 启动 MySQL:
|
||
|
||
```bash
|
||
pnpm mysql:up
|
||
```
|
||
|
||
本机连接端口是 `3307`,容器内 MySQL 端口仍然是 `3306`。这样可以减少和本机已有 MySQL 的冲突。
|
||
|
||
2. 执行数据库迁移:
|
||
|
||
```bash
|
||
pnpm db:migrate
|
||
```
|
||
|
||
迁移会创建这些表:
|
||
|
||
- `stores`:门店表
|
||
- `roles`:角色表
|
||
- `employees`:员工表
|
||
- `employee_roles`:员工角色关系表
|
||
- `schema_migrations`:迁移记录表
|
||
|
||
3. 启动后端:
|
||
|
||
```bash
|
||
pnpm dev
|
||
```
|
||
|
||
服务默认运行在:
|
||
|
||
```text
|
||
http://localhost:3500
|
||
```
|
||
|
||
4. 健康检查:
|
||
|
||
```bash
|
||
curl http://localhost:3500/health
|
||
```
|
||
|
||
正常响应示例:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"status": "ok",
|
||
"database": "up",
|
||
"now": "2026-05-26T00:00:00.000Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
## package.json 脚本说明
|
||
|
||
这些脚本定义在 [package.json](./package.json) 的 `scripts` 字段里。
|
||
|
||
| 命令 | 作用 | 什么时候用 |
|
||
| ------------------- | --------------------------------------------------------------------------- | -------------------------------------------------- |
|
||
| `pnpm dev` | 使用现有 `.env.development` 启动开发服务,并通过 `tsx watch` 监听代码变化。 | 日常开发接口时使用。 |
|
||
| `pnpm build` | 使用 `tsc` 编译 TypeScript,输出到 `dist/`。 | 准备运行编译产物或发布前验证时使用。 |
|
||
| `pnpm start` | 使用现有 `.env.development` 运行 `dist/server.js`。 | 已经执行过 `pnpm build` 后,用编译产物启动服务。 |
|
||
| `pnpm typecheck` | 执行 `tsc --noEmit`,只检查类型,不生成文件。 | 改 TypeScript 代码后快速确认类型是否正确。 |
|
||
| `pnpm db:migrate` | 使用现有 `.env.development` 运行 `src/db/migrate.ts`,按顺序执行 `migrations/*.sql`。 | 第一次启动项目、拉到新迁移、改数据库结构后使用。 |
|
||
| `pnpm db:shell` | 进入 Docker 容器里的 MySQL 命令行。 | 需要手动查看表结构或查询数据时使用。 |
|
||
| `pnpm mysql:up` | 启动本地 MySQL 容器。 | 开发前先启动数据库。 |
|
||
| `pnpm mysql:down` | 停止并移除本地 MySQL 容器。 | 不再需要本地数据库容器时使用。 |
|
||
|
||
重要顺序通常是:
|
||
|
||
```bash
|
||
pnpm mysql:up
|
||
pnpm db:migrate
|
||
pnpm dev
|
||
```
|
||
|
||
## 接口响应格式
|
||
|
||
成功响应:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {}
|
||
}
|
||
```
|
||
|
||
分页响应:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"items": [],
|
||
"pagination": {
|
||
"page": 1,
|
||
"pageSize": 20,
|
||
"total": 0,
|
||
"totalPages": 0
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
错误响应:
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"error": {
|
||
"code": "VALIDATION_ERROR",
|
||
"message": "请求参数不合法",
|
||
"details": []
|
||
}
|
||
}
|
||
```
|
||
|
||
## 门店接口示例
|
||
|
||
查询门店选项:
|
||
|
||
```bash
|
||
curl http://localhost:3500/api/stores
|
||
```
|
||
|
||
查询包含停用门店的列表:
|
||
|
||
```bash
|
||
curl 'http://localhost:3500/api/stores?includeInactive=true'
|
||
```
|
||
|
||
新增门店:
|
||
|
||
```bash
|
||
curl -X POST http://localhost:3500/api/stores \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"name": "人民广场店",
|
||
"address": "上海市黄浦区人民广场",
|
||
"phone": "021-12345678",
|
||
"status": "ACTIVE"
|
||
}'
|
||
```
|
||
|
||
修改门店:
|
||
|
||
```bash
|
||
curl -X PATCH http://localhost:3500/api/stores/1 \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"phone": "021-87654321"
|
||
}'
|
||
```
|
||
|
||
删除门店:
|
||
|
||
```bash
|
||
curl -X DELETE http://localhost:3500/api/stores/1
|
||
```
|
||
|
||
门店下还有员工时,不能停用或删除门店。
|
||
|
||
## 角色接口示例
|
||
|
||
查询角色:
|
||
|
||
```bash
|
||
curl http://localhost:3500/api/roles
|
||
```
|
||
|
||
新增角色:
|
||
|
||
```bash
|
||
curl -X POST http://localhost:3500/api/roles \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"code": "shift_leader",
|
||
"name": "班组长",
|
||
"description": "负责当班现场协调"
|
||
}'
|
||
```
|
||
|
||
修改角色:
|
||
|
||
```bash
|
||
curl -X PATCH http://localhost:3500/api/roles/1 \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"description": "负责门店日常管理和权限审批"
|
||
}'
|
||
```
|
||
|
||
删除角色:
|
||
|
||
```bash
|
||
curl -X DELETE http://localhost:3500/api/roles/1
|
||
```
|
||
|
||
角色已绑定员工时,不能删除。
|
||
|
||
## 员工接口示例
|
||
|
||
新增员工:
|
||
|
||
```bash
|
||
curl -X POST http://localhost:3500/api/employees \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"storeId": 1,
|
||
"name": "张三",
|
||
"phone": "13812345678",
|
||
"roleIds": [1, 2],
|
||
"remark": "早班员工"
|
||
}'
|
||
```
|
||
|
||
查询员工列表:
|
||
|
||
```bash
|
||
curl 'http://localhost:3500/api/employees?page=1&pageSize=20'
|
||
```
|
||
|
||
按门店和状态筛选:
|
||
|
||
```bash
|
||
curl 'http://localhost:3500/api/employees?storeId=1&status=ACTIVE&page=1&pageSize=20'
|
||
```
|
||
|
||
按姓名或手机号搜索:
|
||
|
||
```bash
|
||
curl 'http://localhost:3500/api/employees?keyword=张三'
|
||
```
|
||
|
||
查询员工详情:
|
||
|
||
```bash
|
||
curl http://localhost:3500/api/employees/1
|
||
```
|
||
|
||
修改员工:
|
||
|
||
```bash
|
||
curl -X PATCH http://localhost:3500/api/employees/1 \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{
|
||
"name": "张三丰",
|
||
"roleIds": [1]
|
||
}'
|
||
```
|
||
|
||
停用员工:
|
||
|
||
```bash
|
||
curl -X PATCH http://localhost:3500/api/employees/1/status \
|
||
-H 'Content-Type: application/json' \
|
||
-d '{"status": "INACTIVE"}'
|
||
```
|
||
|
||
软删除员工:
|
||
|
||
```bash
|
||
curl -X DELETE http://localhost:3500/api/employees/1
|
||
```
|
||
|
||
同一个门店下,未删除员工的手机号不能重复。员工软删除后,可以重新录入相同手机号。
|
||
|
||
## 数据库迁移说明
|
||
|
||
迁移文件放在 [migrations](./migrations) 目录下。
|
||
|
||
当前迁移:
|
||
|
||
- [001_initial_schema.sql](./migrations/001_initial_schema.sql):创建门店、角色、员工、员工角色关系表。
|
||
- [002_seed_demo_data.sql](./migrations/002_seed_demo_data.sql):写入一个示例门店和几个常见角色。
|
||
|
||
执行 `pnpm db:migrate` 时,脚本会:
|
||
|
||
1. 创建 `schema_migrations` 迁移记录表。
|
||
2. 读取 `migrations` 目录里的 `.sql` 文件。
|
||
3. 按文件名排序执行未执行过的迁移。
|
||
4. 把已执行文件名写入 `schema_migrations`。
|
||
|
||
后续改表时,建议新增迁移文件,例如:
|
||
|
||
```text
|
||
migrations/003_add_employee_email.sql
|
||
```
|
||
|
||
不要随意修改已经在数据库执行过的旧迁移,否则不同环境的数据库结构可能不一致。
|
||
|
||
## 学习重点
|
||
|
||
- `stores.deleted_at` 和 `employees.deleted_at` 用于软删除。
|
||
- `employees.active_phone` 是生成列,用来实现“同一门店未删除员工手机号唯一”。
|
||
- `employee_roles` 是多对多关系表。
|
||
- `repository` 使用参数化查询,避免 SQL 注入。
|
||
- `service` 使用事务保证员工信息和角色绑定同时成功或同时失败。
|
||
- `app.ts` 统一处理 zod 校验错误、业务错误和数据库唯一索引冲突。
|
||
|
||
## README 维护规则
|
||
|
||
本项目有一个项目内 skill:[readme-structure-sync](./.agents/skills/readme-structure-sync/SKILL.md)。
|
||
|
||
只要发生以下任一变化,就必须在同一次修改里更新 README:
|
||
|
||
- 新增、删除、重命名或移动目录。
|
||
- 新增、删除、重命名或移动重要源码文件。
|
||
- 修改 `package.json` 的 `scripts`。
|
||
- 修改启动流程、数据库迁移流程或本地环境变量。
|
||
|
||
README 至少要同步这些部分:
|
||
|
||
- `目录结构`
|
||
- `目录和关键文件说明`
|
||
- `package.json 脚本说明`
|
||
- `启动步骤`
|
||
- `数据库迁移说明`
|
||
|
||
## 常见问题
|
||
|
||
### 连接不上数据库
|
||
|
||
先确认 MySQL 容器是否启动:
|
||
|
||
```bash
|
||
docker compose ps
|
||
```
|
||
|
||
再确认 `.env.development` 里的 `DB_PORT` 是 `3307`。
|
||
|
||
### 迁移执行过了,为什么再次运行会跳过
|
||
|
||
`src/db/migrate.ts` 会把执行过的文件名写入 `schema_migrations`。再次运行时,如果文件名已存在,就会跳过,避免重复建表或重复插入数据。
|
||
|
||
### 删除员工后,为什么数据库里还有记录
|
||
|
||
这是软删除。删除接口会把 `deleted_at` 设置为当前时间,并把状态改成 `INACTIVE`。这样可以保留历史数据,同时普通查询会过滤掉已删除记录。
|
||
|
||
### 为什么不使用 ORM
|
||
|
||
这个项目的目标是学习 SQL 和 MySQL 基础能力。直接使用 `mysql2` 可以更清楚地看到 SQL、索引、事务和参数绑定是如何工作的。
|