Files
access-manage/README.md
T
2026-05-26 11:07:37 +08:00

472 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.example # 本地环境变量示例,复制为 .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.example` | 可提交到仓库的环境变量示例。新开发者复制成 `.env.development` 后再启动项目。 |
| `.gitignore` | 忽略本地环境变量、依赖目录、编译产物和系统文件,避免把无关文件推到仓库。 |
| `AGENTS.md` | Agent 工具读取的入口文件,当前通过 `@RTK.md` 引入项目规则。 |
| `RTK.md` | 本项目的协作规则,例如使用中文说明、保持分层、改目录时同步 README。 |
| `migrations/` | 数据库迁移目录。所有建表、改表、初始化基础数据的 SQL 都放在这里。 |
| `src/app.ts` | 创建 Fastify 应用,注册路由,处理健康检查和全局错误。 |
| `src/server.ts` | 真正启动 HTTP 服务,监听端口,并处理优雅停机。 |
| `src/config/env.ts` | 使用 zod 校验环境变量,避免配置错误拖到请求阶段才暴露。 |
| `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` | 项目信息、依赖和常用脚本。 |
| `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
```
复制环境变量示例:
```bash
cp .env.example .env.development
```
本地配置示例见 [.env.example](./.env.example)
```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` | 运行 `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、索引、事务和参数绑定是如何工作的。