# 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、索引、事务和参数绑定是如何工作的。