Files
2026-06-02 12:23:00 +08:00

745 lines
28 KiB
Markdown
Raw Permalink 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
## 项目能力
- 门店管理:查询、新增、修改、停用、软删除门店,门店详情可查看员工。
- 角色管理:拥有 `role:manage` 的账号可新增、修改、软删除自定义角色,服务端内置角色不可变更。
- 员工管理:分页查询、新增、修改、启用/停用、修改密码、重置临时密码、移除和软删除员工。
- 员工角色:一个员工可以绑定多个角色。
- 登录账号:超级管理员和员工都可以登录。
- 后台权限:超级管理员拥有所有权限;角色权限由 `role_permissions` 动态分配。
- 动态权限:菜单和按钮动作由 `/api/permissions/me` 返回,前端可通过权限管理页分配角色权限。
- 员工工作台:提供员工端首屏聚合、公告、任务、今日排班和排班列表接口。
- 后台工作台:提供公告、任务、排班管理接口。
- 凭据安全:禁止查看明文密码,只支持本人改密、下级员工临时密码重置和审计追踪。
- 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 # 项目协作规则和开发约定
├── docs/
│ └── API.md # 前端对接接口文档
├── deploy/
│ └── server/
│ ├── create-env.sh # 在服务器生成真实 .env 和 .env.production
│ └── docker-compose.mysql.yml # 服务器 MySQL Compose 模板
├── 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 # 调整员工默认密码、手机号唯一和系统角色
│ ├── 006_create_role_permissions.sql # 创建角色权限关系表并初始化默认权限
│ ├── 007_add_soft_delete_to_roles_and_relations.sql # 给角色和关系表补充逻辑删除字段并移除级联删除
│ └── 008_add_role_user_workbench_tables.sql # 新增员工工作台、排班和凭据审计表
├── src/
│ ├── app.ts # 创建 Fastify 应用、注册路由、统一错误处理
│ ├── server.ts # 启动 HTTP 服务和优雅停机
│ ├── config/
│ │ └── env.ts # 环境变量校验
│ ├── db/
│ │ ├── migrate.ts # 执行 migrations 目录下的 SQL
│ │ └── pool.ts # MySQL 连接池
│ ├── modules/
│ │ ├── announcements/ # 公告后台管理和员工端公告模块
│ │ ├── auth/ # 登录、当前用户、本人改密和 JWT 鉴权模块
│ │ ├── catalog/ # 门店和角色模块
│ │ ├── credentials/ # 凭据重置和凭据审计模块
│ │ ├── employees/ # 员工 CRUD 模块
│ │ ├── mobile/ # 员工端首屏聚合模块
│ │ ├── permissions/ # 权限点定义、角色权限分配和菜单动作策略
│ │ ├── shifts/ # 排班后台管理和员工端排班模块
│ │ └── tasks/ # 任务后台管理和员工端任务模块
│ └── 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。 |
| `docs/API.md` | 面向前端对接的完整接口文档,包含认证、权限、字段约束、示例请求响应和错误码。 |
| `deploy/server/` | 服务器部署辅助文件。`create-env.sh` 在服务器本地生成真实环境变量,`docker-compose.mysql.yml` 用于启动服务器 MySQL。 |
| `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/announcements/` | 公告模块,负责后台公告管理、员工端可见公告、已读记录和未读统计。 |
| `src/modules/auth/` | 登录鉴权模块,负责后台登录、员工端登录、本人改密、密码校验、JWT 签发、当前用户查询和权限 guard。 |
| `src/modules/catalog/` | 门店和角色模块,负责基础资料接口、门店详情员工列表和门店移除员工入口。 |
| `src/modules/credentials/` | 凭据安全模块,负责下级员工临时密码重置、密码状态和凭据审计。 |
| `src/modules/employees/` | 员工模块,负责员工分页、详情、新增、修改、状态变更、密码维护和软删除。 |
| `src/modules/mobile/` | 员工端聚合模块,负责 `/api/mobile/bootstrap` 首屏数据。 |
| `src/modules/permissions/` | 权限模块,维护权限点定义、角色权限分配、当前用户菜单动作权限和权限策略说明。 |
| `src/modules/shifts/` | 排班模块,负责后台排班管理和员工端排班查询。 |
| `src/modules/tasks/` | 任务模块,负责后台任务管理、员工端任务处理和任务事件日志。 |
| `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`:员工角色关系表
- `role_permissions`:角色权限关系表
- `super_admins`:超级管理员表
- `announcements` / `announcement_targets` / `announcement_reads`:公告、目标范围和已读记录。
- `tasks` / `task_assignees` / `task_events`:任务、分配员工和处理日志。
- `shifts`:员工排班表。
- `credential_audits` / `employee_password_states`:凭据操作审计和员工改密状态。
- `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"
}
}
```
## 服务器部署准备
服务器部署时不要直接使用本地 `.env.development`,也不要把本机 `.env.*` 打进部署包。真实密码只应该在服务器本地生成和保存。
1. 生成服务器真实环境变量:
```bash
cd /srv/www/access-manage
bash deploy/server/create-env.sh /srv/www/access-manage
```
这个脚本会生成两个不提交到仓库的文件:
```text
/srv/www/access-manage/.env
/srv/www/access-manage/.env.production
```
其中 `.env` 给 MySQL 容器使用,`.env.production` 给 Node 后端使用。如果 `.env` 已经存在,脚本会读取现有 `MYSQL_PASSWORD`,只补 `.env.production`;如果 `.env.production` 已经存在,脚本会拒绝覆盖。
2. 启动服务器 MySQL
```bash
docker-compose -f deploy/server/docker-compose.mysql.yml up -d mysql
```
服务器模板会把 MySQL 数据持久化到:
```text
/srv/data/access-manage/mysql
```
当前模板默认后端通过服务器本机端口连接 MySQL:
```env
DB_HOST=127.0.0.1
DB_PORT=3307
```
3. 安装生产依赖、迁移、启动:
```bash
pnpm install --prod --frozen-lockfile
pnpm db:migrate:prod
pnpm start:prod
```
`pnpm build:dev``pnpm build:pro` 只生成编译后的 `dist/` 目录。服务器上不需要再执行 `pnpm build`
4. 健康检查:
```bash
curl http://127.0.0.1:3500/health
```
只有返回里出现 `database: "up"`,才代表后端服务和 MySQL 都连通。
如果手动压缩部署,至少需要包含 `dist/``migrations/``deploy/``package.json``pnpm-lock.yaml`。不要把本机 `.env.*``node_modules/``src/``tsconfig.json``.git/` 打进部署包。
可以在本机执行:
```bash
pnpm build:dev
```
命令执行完成后会得到最新的 `dist/`。如果需要压缩上传,由部署者手动选择要打包的文件;正式生产构建时可以执行 `pnpm build:pro`
## package.json 脚本说明
这些脚本定义在 [package.json](./package.json) 的 `scripts` 字段里。
| 命令 | 作用 | 什么时候用 |
| ------------------- | --------------------------------------------------------------------------- | -------------------------------------------------- |
| `pnpm dev` | 使用现有 `.env.development` 启动开发服务,并通过 `tsx watch` 监听代码变化。 | 日常开发接口时使用。 |
| `pnpm build` | 使用 `tsc` 编译 TypeScript,输出到 `dist/`。 | 准备运行编译产物或发布前验证时使用。 |
| `pnpm build:dev` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | 准备开发/测试服务器部署产物时使用。 |
| `pnpm build:pro` | 清空旧 `dist/`,再执行 `pnpm build` 生成新的 `dist/`。 | 准备正式生产部署产物时使用。 |
| `pnpm start` | 使用现有 `.env.development` 运行 `dist/server.js`。 | 已经执行过 `pnpm build` 后,用编译产物启动服务。 |
| `pnpm start:prod` | 使用 `.env.production` 运行 `dist/server.js`。 | 服务器生产环境启动编译产物时使用。 |
| `pnpm typecheck` | 执行 `tsc --noEmit`,只检查类型,不生成文件。 | 改 TypeScript 代码后快速确认类型是否正确。 |
| `pnpm db:migrate` | 使用现有 `.env.development` 运行 `src/db/migrate.ts`,按顺序执行 `migrations/*.sql`。 | 第一次启动项目、拉到新迁移、改数据库结构后使用。 |
| `pnpm db:migrate:prod` | 使用 `.env.production` 运行 `dist/db/migrate.js`,按顺序执行 `migrations/*.sql`。 | 服务器生产环境建表、升级表结构或初始化基础数据时使用。 |
| `pnpm db:shell` | 进入 Docker 容器里的 MySQL 命令行。 | 需要手动查看表结构或查询数据时使用。 |
| `pnpm mysql:up` | 启动本地 MySQL 容器。 | 开发前先启动数据库。 |
| `pnpm mysql:down` | 停止并移除本地 MySQL 容器。 | 不再需要本地数据库容器时使用。 |
重要顺序通常是:
```bash
pnpm mysql:up
pnpm db:migrate
pnpm dev
```
## 接口文档
完整前端对接文档见 [docs/API.md](./docs/API.md),包含认证、权限、字段约束、全部接口、示例请求响应、常见错误码和 C 端正式版新增模块说明。
## 接口响应格式
成功响应:
```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"
```
查看可分配权限点定义:
```bash
curl http://localhost:3500/api/permissions/definitions \
-H "Authorization: Bearer $TOKEN"
```
给角色分配权限:
```bash
curl -X PUT http://localhost:3500/api/permissions/roles/5 \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"permissions":["store:view","store:manage","permission:view","permission:manage"]}'
```
如果员工账号没有后台菜单权限,可以通过员工端登录并访问 `/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 http://localhost:3500/api/stores/1 \
-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 -X DELETE http://localhost:3500/api/stores/1/employees/2 \
-H "Authorization: Bearer $TOKEN"
```
门店下还有员工时,可以停用门店;该门店员工会返回 `storeStatus: "INACTIVE"` 和“门店被禁用”状态标签。门店下还有员工时,仍不能删除门店。
## 角色接口示例
角色管理页面由 `role:view` 控制可见性,由 `role:manage` 控制新增、修改、删除。服务端内置角色不可修改或删除。
自定义角色默认不绑定后台菜单权限;可以在权限管理页面给角色分配权限后,再把角色绑定给员工。
查询角色:
```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 PATCH http://localhost:3500/api/employees/1/password \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"oldPassword": "pw111111", "newPassword": "NewPw111111"}'
```
重置员工为初始密码:
```bash
curl -X PATCH http://localhost:3500/api/employees/1/password/reset \
-H "Authorization: Bearer $TOKEN"
```
软删除员工:
```bash
curl -X DELETE http://localhost:3500/api/employees/1 \
-H "Authorization: Bearer $TOKEN"
```
员工手机号就是登录账号,未删除员工的手机号不能重复。员工软删除后,可以重新录入相同手机号。员工返回结构包含 `statusTags`;所属门店停用时会额外包含“门店被禁用”标签。
## 数据库迁移说明
迁移文件放在 [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`,手机号改为全局唯一,并标记服务端内置角色。
- [006_create_role_permissions.sql](./migrations/006_create_role_permissions.sql):创建角色权限关系表,并初始化 `admin``store_manager` 的默认权限。
- [007_add_soft_delete_to_roles_and_relations.sql](./migrations/007_add_soft_delete_to_roles_and_relations.sql):给角色、员工角色关系和角色权限关系补充逻辑删除字段,移除关系表旧的 `ON DELETE CASCADE` 级联删除语义。
- [008_add_role_user_workbench_tables.sql](./migrations/008_add_role_user_workbench_tables.sql):新增公告、公告目标、公告已读、任务、任务分配、任务事件、排班、凭据审计和员工密码状态表,并初始化工作台相关权限。
执行 `pnpm db:migrate``pnpm db:migrate:prod` 时,脚本会:
1. 创建 `schema_migrations` 迁移记录表。
2. 读取 `migrations` 目录里的 `.sql` 文件。
3. 按文件名排序执行未执行过的迁移。
4. 把已执行文件名写入 `schema_migrations`
后续改表时,建议新增迁移文件,例如:
```text
migrations/003_add_employee_email.sql
```
不要随意修改已经在数据库执行过的旧迁移,否则不同环境的数据库结构可能不一致。
## 学习重点
- `stores.deleted_at``employees.deleted_at``roles.deleted_at` 用于软删除。
- `employees.active_phone` 是生成列,用来实现“未删除员工手机号全局唯一”。
- `roles.active_code` 是生成列,用来实现“未删除角色编码唯一”。
- `employees.password_hash` 让员工也能登录,默认本地密码是 `pw111111`
- `employee_roles` 是员工和角色的多对多关系表,解绑时写入 `deleted_at`
- `role_permissions` 保存角色和权限点的多对多关系,权限解绑时写入 `deleted_at`,权限分配保存后会在接口鉴权时实时生效。
- `announcements``tasks``shifts` 支撑员工端工作台和后台管理工作流。
- `credential_audits` 只记录凭据操作,不记录明文密码;临时密码只在重置接口响应中返回一次。
- `super_admins` 保存超级管理员账号,密码使用 PBKDF2 哈希,禁止存明文。
- 权限点定义由 `src/modules/permissions/` 固定,角色拥有的权限点由 `role_permissions` 动态决定。
- 前端根据 `/api/permissions/me` 渲染菜单和按钮,根据 `/api/permissions/definitions` 渲染可分配权限点。
- `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`;角色删除接口会写入 `roles.deleted_at`。员工角色关系、角色权限关系的解绑也会写入各自的 `deleted_at`,迁移也会移除旧关系表上的级联物理删除约束。这样可以保留历史数据,同时普通查询会过滤掉已删除记录。
### 为什么不使用 ORM
这个项目的目标是学习 SQL 和 MySQL 基础能力。直接使用 `mysql2` 可以更清楚地看到 SQL、索引、事务和参数绑定是如何工作的。