Files
access-manage/README.md
T
2026-05-26 12:30:38 +08:00

586 lines
20 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
- @fastify/jwt
- Docker Compose
## 项目能力
- 门店管理:查询、新增、修改、软删除门店。
- 角色管理:管理员可查看角色;超级管理员可新增、修改、删除自定义角色,服务端内置角色不可变更。
- 员工管理:分页查询、新增、修改、启用/停用、软删除员工。
- 员工角色:一个员工可以绑定多个角色。
- 登录账号:超级管理员和员工都可以登录。
- 后台权限:超级管理员拥有所有权限;管理员可管理门店和员工、只读角色;店长只看当前门店员工。
- 固定权限:菜单和动作权限由服务端写死,前端只按接口返回结果展示。
- JWT 鉴权:登录后签发 token,除健康检查和登录外,接口都需要 Bearer token。
- 数据校验:使用 zod 校验路径参数、查询参数和请求体。
- 数据库迁移:使用 `migrations/*.sql` 管理建表和初始化数据。
- 事务处理:创建/更新员工和角色绑定时使用事务,避免部分成功。
## 目录结构
```text
.
├── .agents/
│ └── skills/
│ └── readme-structure-sync/ # README 和目录结构同步维护规则
├── .env.development # 默认本地开发环境变量文件,不提交到仓库
├── .env.local / .env.production # 可选环境变量文件,不提交到仓库
├── .gitignore # Git 忽略规则,排除本地配置、依赖和编译产物
├── AGENTS.md # Codex/Agent 入口指令,当前指向 RTK.md
├── RTK.md # 项目协作规则和开发约定
├── migrations/ # 数据库迁移 SQL
│ ├── 001_initial_schema.sql # 创建基础表结构
│ ├── 002_seed_demo_data.sql # 初始化演示门店和角色
│ ├── 003_create_super_admins.sql # 创建超级管理员表和默认账号
│ ├── 004_add_employee_login_fields.sql # 给员工补充登录字段
│ └── 005_refine_employee_login_and_role_policy.sql # 调整员工默认密码、手机号唯一和系统角色
├── src/
│ ├── app.ts # 创建 Fastify 应用、注册路由、统一错误处理
│ ├── server.ts # 启动 HTTP 服务和优雅停机
│ ├── config/
│ │ └── env.ts # 环境变量校验
│ ├── db/
│ │ ├── migrate.ts # 执行 migrations 目录下的 SQL
│ │ └── pool.ts # MySQL 连接池
│ ├── modules/
│ │ ├── auth/ # 登录、当前用户和 JWT 鉴权模块
│ │ ├── catalog/ # 门店和角色模块
│ │ ├── employees/ # 员工 CRUD 模块
│ │ └── permissions/ # 服务端固定菜单和动作权限策略
│ └── 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` 脚本默认读取的本地开发环境变量文件;该文件只保留在本机,不提交到仓库。 |
| `.env.local` / `.env.production` | 本机已有的其他环境变量文件;代码已允许 `NODE_ENV=local``NODE_ENV=production`,切换脚本时可以复用。 |
| `.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/auth/` | 登录鉴权模块,负责后台登录、员工端登录、密码校验、JWT 签发、当前用户查询和权限 guard。 |
| `src/modules/catalog/` | 门店和角色模块,负责基础资料接口。 |
| `src/modules/employees/` | 员工模块,负责员工分页、详情、新增、修改、状态变更和软删除。 |
| `src/modules/permissions/` | 服务端固定权限策略,返回前端菜单、动作权限和权限策略说明。 |
| `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
JWT_SECRET=请使用至少 32 位的随机字符串
JWT_EXPIRES_IN=2h
```
代码允许的 `NODE_ENV` 值是:`local``development``test``production`。如果改用 `.env.local``.env.production` 启动,也需要包含同样的 `JWT_SECRET``JWT_EXPIRES_IN`
## 启动步骤
1. 启动 MySQL
```bash
pnpm mysql:up
```
本机连接端口是 `3307`,容器内 MySQL 端口仍然是 `3306`。这样可以减少和本机已有 MySQL 的冲突。
2. 执行数据库迁移:
```bash
pnpm db:migrate
```
迁移会创建这些表:
- `stores`:门店表
- `roles`:角色表
- `employees`:员工表
- `employee_roles`:员工角色关系表
- `super_admins`:超级管理员表
- `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": []
}
}
```
## 登录和鉴权
本项目有两类可登录账号:
- 超级管理员:拥有所有后台管理权限。
- 员工:都可以通过员工端接口登录;后台登录只开放给有后台菜单权限的员工,例如 `admin``store_manager`
默认本地超级管理员账号由 [003_create_super_admins.sql](./migrations/003_create_super_admins.sql) 初始化:
```text
账号:admin
密码:Admin@123456
```
员工登录字段由 [004_add_employee_login_fields.sql](./migrations/004_add_employee_login_fields.sql) 和 [005_refine_employee_login_and_role_policy.sql](./migrations/005_refine_employee_login_and_role_policy.sql) 初始化。已有员工和新建员工默认密码是:
```text
账号:员工手机号
密码:pw111111
```
后台登录获取 token。超级管理员、管理员和店长使用这个接口:
```bash
curl -X POST http://localhost:3500/api/auth/admin/login \
-H 'Content-Type: application/json' \
-d '{
"username": "admin",
"password": "Admin@123456"
}'
```
`POST /api/auth/login` 也保留为后台登录的兼容入口。
员工端登录使用独立接口,给后续 toc 项目使用:
```bash
curl -X POST http://localhost:3500/api/auth/employee/login \
-H 'Content-Type: application/json' \
-d '{
"username": "13812345678",
"password": "pw111111"
}'
```
响应里的 `data.token` 就是后续接口要使用的 JWT。
响应里的 `data.user.permissions` 是服务端计算出的固定权限点;菜单和按钮动作以 `/api/permissions/me` 返回结果为准。
为了方便测试,可以先把 token 保存成 shell 变量:
```bash
TOKEN="把登录响应里的 data.token 粘贴到这里"
```
获取当前登录用户:
```bash
curl http://localhost:3500/api/auth/me \
-H "Authorization: Bearer $TOKEN"
```
`/health``/api/auth/login` 外,当前接口都需要带上:
```bash
-H "Authorization: Bearer $TOKEN"
```
获取当前账号菜单和动作权限:
```bash
curl http://localhost:3500/api/permissions/me \
-H "Authorization: Bearer $TOKEN"
```
查看服务端固定权限策略:
```bash
curl http://localhost:3500/api/permissions/policies \
-H "Authorization: Bearer $TOKEN"
```
如果员工账号没有后台菜单权限,可以通过员工端登录并访问 `/api/auth/me`,但访问门店、角色、员工等后台管理接口会返回 `403 FORBIDDEN`
### 后台菜单权限
| 菜单 | 超级管理员 | 管理员 `admin` | 店长 `store_manager` | 其他员工 |
| --- | --- | --- | --- | --- |
| 门店管理 | 查看、新增、修改、删除 | 查看、新增、修改、删除 | 不可见 | 不可见 |
| 角色管理 | 查看、新增、修改、删除自定义角色 | 仅查看 | 不可见 | 不可见 |
| 员工管理 | 查看全部、新增、修改、删除 | 查看全部、新增、修改、删除 | 仅查看当前门店员工 | 不可见 |
| 权限管理 | 查看 | 查看 | 不可见 | 不可见 |
## 门店接口示例
查询门店选项:
```bash
curl http://localhost:3500/api/stores \
-H "Authorization: Bearer $TOKEN"
```
查询包含停用门店的列表:
```bash
curl 'http://localhost:3500/api/stores?includeInactive=true' \
-H "Authorization: Bearer $TOKEN"
```
新增门店:
```bash
curl -X POST http://localhost:3500/api/stores \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "人民广场店",
"address": "上海市黄浦区人民广场",
"phone": "021-12345678",
"status": "ACTIVE"
}'
```
修改门店:
```bash
curl -X PATCH http://localhost:3500/api/stores/1 \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"phone": "021-87654321"
}'
```
删除门店:
```bash
curl -X DELETE http://localhost:3500/api/stores/1 \
-H "Authorization: Bearer $TOKEN"
```
门店下还有员工时,不能停用或删除门店。
## 角色接口示例
角色管理页面只有超级管理员和管理员可见。管理员只能看;超级管理员可以新增、修改、删除自定义角色。服务端内置角色不可修改或删除。
自定义角色默认不绑定后台菜单权限;后台菜单权限仍由服务端固定策略控制。
查询角色:
```bash
curl http://localhost:3500/api/roles \
-H "Authorization: Bearer $TOKEN"
```
新增自定义角色:
```bash
curl -X POST http://localhost:3500/api/roles \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"code": "regional_manager",
"name": "区域经理",
"description": "自定义角色示例"
}'
```
## 员工接口示例
新增员工:
```bash
curl -X POST http://localhost:3500/api/employees \
-H "Authorization: Bearer $TOKEN" \
-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' \
-H "Authorization: Bearer $TOKEN"
```
按门店和状态筛选:
```bash
curl 'http://localhost:3500/api/employees?storeId=1&status=ACTIVE&page=1&pageSize=20' \
-H "Authorization: Bearer $TOKEN"
```
按姓名或手机号搜索:
```bash
curl 'http://localhost:3500/api/employees?keyword=张三' \
-H "Authorization: Bearer $TOKEN"
```
查询员工详情:
```bash
curl http://localhost:3500/api/employees/1 \
-H "Authorization: Bearer $TOKEN"
```
修改员工:
```bash
curl -X PATCH http://localhost:3500/api/employees/1 \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "张三丰",
"roleIds": [1]
}'
```
停用员工:
```bash
curl -X PATCH http://localhost:3500/api/employees/1/status \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"status": "INACTIVE"}'
```
软删除员工:
```bash
curl -X DELETE http://localhost:3500/api/employees/1 \
-H "Authorization: Bearer $TOKEN"
```
员工手机号就是登录账号,未删除员工的手机号不能重复。员工软删除后,可以重新录入相同手机号。
## 数据库迁移说明
迁移文件放在 [migrations](./migrations) 目录下。
当前迁移:
- [001_initial_schema.sql](./migrations/001_initial_schema.sql):创建门店、角色、员工、员工角色关系表。
- [002_seed_demo_data.sql](./migrations/002_seed_demo_data.sql):写入一个示例门店和几个常见角色。
- [003_create_super_admins.sql](./migrations/003_create_super_admins.sql):创建超级管理员表,并初始化本地登录账号。
- [004_add_employee_login_fields.sql](./migrations/004_add_employee_login_fields.sql):给员工补充登录密码哈希和最后登录时间。
- [005_refine_employee_login_and_role_policy.sql](./migrations/005_refine_employee_login_and_role_policy.sql):员工默认密码改为 `pw111111`,手机号改为全局唯一,并标记服务端内置角色。
执行 `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` 是生成列,用来实现“未删除员工手机号全局唯一”。
- `employees.password_hash` 让员工也能登录,默认本地密码是 `pw111111`
- `employee_roles` 是多对多关系表。
- `super_admins` 保存超级管理员账号,密码使用 PBKDF2 哈希,禁止存明文。
- 菜单和动作权限由 `src/modules/permissions/` 固定,前端根据 `/api/permissions/me` 渲染。
- `admin` 角色可查看角色、管理门店和员工;`store_manager` 只能查看当前门店员工。
- JWT 鉴权在 `src/modules/auth/` 中实现,`permissionGuard` 按固定权限点保护接口。
- `repository` 使用参数化查询,避免 SQL 注入。
- `service` 使用事务保证员工信息和角色绑定同时成功或同时失败。
- `app.ts` 统一注册 JWT、业务路由和错误处理,并处理 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、索引、事务和参数绑定是如何工作的。