17 KiB
access-manage
access-manage 是一个用于学习 MySQL CRUD 的门店员工权限管理后端项目。
项目刻意使用 mysql2 直接写 SQL,不引入 ORM。这样可以更直接地学习表设计、索引、外键、事务、软删除、参数化查询和分层代码组织。
技术栈
- Node.js + TypeScript
- Fastify
- MySQL 8.4
- mysql2
- zod
- @fastify/jwt
- Docker Compose
项目能力
- 门店管理:查询、新增、修改、软删除门店。
- 角色管理:查询服务端固定角色,用于员工权限分配。
- 员工管理:分页查询、新增、修改、启用/停用、软删除员工。
- 员工角色:一个员工可以绑定多个角色。
- 登录账号:超级管理员和员工都可以登录。
- 后台权限:超级管理员拥有所有权限;员工只有绑定
admin角色时才能访问后台管理接口。 - 固定角色:店长、收银员、后厨、兼职、管理员是服务端固定角色,不提供角色新增、修改、删除接口。
- JWT 鉴权:登录后签发 token,除健康检查和登录外,接口都需要 Bearer token。
- 数据校验:使用 zod 校验路径参数、查询参数和请求体。
- 数据库迁移:使用
migrations/*.sql管理建表和初始化数据。 - 事务处理:创建/更新员工和角色绑定时使用事务,避免部分成功。
目录结构
.
├── .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 # 给员工补充登录字段
├── src/
│ ├── app.ts # 创建 Fastify 应用、注册路由、统一错误处理
│ ├── server.ts # 启动 HTTP 服务和优雅停机
│ ├── config/
│ │ └── env.ts # 环境变量校验
│ ├── db/
│ │ ├── migrate.ts # 执行 migrations 目录下的 SQL
│ │ └── pool.ts # MySQL 连接池
│ ├── modules/
│ │ ├── auth/ # 登录、当前用户和 JWT 鉴权模块
│ │ ├── 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 脚本默认读取的本地开发环境变量文件;该文件只保留在本机,不提交到仓库。 |
.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/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
安装依赖:
pnpm install
本地开发默认使用现有的 .env.development,当前需要这些变量:
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。
启动步骤
- 启动 MySQL:
pnpm mysql:up
本机连接端口是 3307,容器内 MySQL 端口仍然是 3306。这样可以减少和本机已有 MySQL 的冲突。
- 执行数据库迁移:
pnpm db:migrate
迁移会创建这些表:
stores:门店表roles:角色表employees:员工表employee_roles:员工角色关系表super_admins:超级管理员表schema_migrations:迁移记录表
- 启动后端:
pnpm dev
服务默认运行在:
http://localhost:3500
- 健康检查:
curl http://localhost:3500/health
正常响应示例:
{
"success": true,
"data": {
"status": "ok",
"database": "up",
"now": "2026-05-26T00:00:00.000Z"
}
}
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 容器。 | 不再需要本地数据库容器时使用。 |
重要顺序通常是:
pnpm mysql:up
pnpm db:migrate
pnpm dev
接口响应格式
成功响应:
{
"success": true,
"data": {}
}
分页响应:
{
"success": true,
"data": {
"items": [],
"pagination": {
"page": 1,
"pageSize": 20,
"total": 0,
"totalPages": 0
}
}
}
错误响应:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数不合法",
"details": []
}
}
登录和鉴权
本项目有两类可登录账号:
- 超级管理员:拥有所有后台管理权限。
- 员工:都可以登录;只有绑定
admin角色的员工才能访问后台管理接口。
默认本地超级管理员账号由 003_create_super_admins.sql 初始化:
账号:admin
密码:Admin@123456
员工登录字段由 004_add_employee_login_fields.sql 初始化。已有员工和新建员工默认密码是:
账号:员工手机号
密码:Employee@123456
登录获取 token:
curl -X POST http://localhost:3500/api/auth/login \
-H 'Content-Type: application/json' \
-d '{
"username": "admin",
"password": "Admin@123456"
}'
响应里的 data.token 就是后续接口要使用的 JWT。
响应里的 data.user.canManage 表示当前账号是否能访问后台管理接口。
为了方便测试,可以先把 token 保存成 shell 变量:
TOKEN="把登录响应里的 data.token 粘贴到这里"
获取当前登录用户:
curl http://localhost:3500/api/auth/me \
-H "Authorization: Bearer $TOKEN"
除 /health 和 /api/auth/login 外,当前接口都需要带上:
-H "Authorization: Bearer $TOKEN"
如果员工账号没有 admin 角色,可以登录并访问 /api/auth/me,但访问门店、角色、员工等后台管理接口会返回 403 FORBIDDEN。
门店接口示例
查询门店选项:
curl http://localhost:3500/api/stores \
-H "Authorization: Bearer $TOKEN"
查询包含停用门店的列表:
curl 'http://localhost:3500/api/stores?includeInactive=true' \
-H "Authorization: Bearer $TOKEN"
新增门店:
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"
}'
修改门店:
curl -X PATCH http://localhost:3500/api/stores/1 \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"phone": "021-87654321"
}'
删除门店:
curl -X DELETE http://localhost:3500/api/stores/1 \
-H "Authorization: Bearer $TOKEN"
门店下还有员工时,不能停用或删除门店。
角色接口示例
角色是服务端固定权限集合,只允许查询,不允许通过接口新增、修改或删除。
查询角色:
curl http://localhost:3500/api/roles \
-H "Authorization: Bearer $TOKEN"
员工接口示例
新增员工:
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": "早班员工"
}'
查询员工列表:
curl 'http://localhost:3500/api/employees?page=1&pageSize=20' \
-H "Authorization: Bearer $TOKEN"
按门店和状态筛选:
curl 'http://localhost:3500/api/employees?storeId=1&status=ACTIVE&page=1&pageSize=20' \
-H "Authorization: Bearer $TOKEN"
按姓名或手机号搜索:
curl 'http://localhost:3500/api/employees?keyword=张三' \
-H "Authorization: Bearer $TOKEN"
查询员工详情:
curl http://localhost:3500/api/employees/1 \
-H "Authorization: Bearer $TOKEN"
修改员工:
curl -X PATCH http://localhost:3500/api/employees/1 \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "张三丰",
"roleIds": [1]
}'
停用员工:
curl -X PATCH http://localhost:3500/api/employees/1/status \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"status": "INACTIVE"}'
软删除员工:
curl -X DELETE http://localhost:3500/api/employees/1 \
-H "Authorization: Bearer $TOKEN"
同一个门店下,未删除员工的手机号不能重复。员工软删除后,可以重新录入相同手机号。
数据库迁移说明
迁移文件放在 migrations 目录下。
当前迁移:
- 001_initial_schema.sql:创建门店、角色、员工、员工角色关系表。
- 002_seed_demo_data.sql:写入一个示例门店和几个常见角色。
- 003_create_super_admins.sql:创建超级管理员表,并初始化本地登录账号。
- 004_add_employee_login_fields.sql:给员工补充登录密码哈希和最后登录时间。
执行 pnpm db:migrate 时,脚本会:
- 创建
schema_migrations迁移记录表。 - 读取
migrations目录里的.sql文件。 - 按文件名排序执行未执行过的迁移。
- 把已执行文件名写入
schema_migrations。
后续改表时,建议新增迁移文件,例如:
migrations/003_add_employee_email.sql
不要随意修改已经在数据库执行过的旧迁移,否则不同环境的数据库结构可能不一致。
学习重点
stores.deleted_at和employees.deleted_at用于软删除。employees.active_phone是生成列,用来实现“同一门店未删除员工手机号唯一”。employees.password_hash让员工也能登录,默认本地密码是Employee@123456。employee_roles是多对多关系表。super_admins保存超级管理员账号,密码使用 PBKDF2 哈希,禁止存明文。- 角色定义由服务端固定,
admin角色用于判断员工是否能访问后台管理接口。 - JWT 鉴权在
src/modules/auth/中实现,managementGuard统一保护后台管理接口。 repository使用参数化查询,避免 SQL 注入。service使用事务保证员工信息和角色绑定同时成功或同时失败。app.ts统一注册 JWT、业务路由和错误处理,并处理 zod 校验错误、业务错误和数据库唯一索引冲突。
README 维护规则
本项目有一个项目内 skill:readme-structure-sync。
只要发生以下任一变化,就必须在同一次修改里更新 README:
- 新增、删除、重命名或移动目录。
- 新增、删除、重命名或移动重要源码文件。
- 修改
package.json的scripts。 - 修改启动流程、数据库迁移流程或本地环境变量。
README 至少要同步这些部分:
目录结构目录和关键文件说明package.json 脚本说明启动步骤数据库迁移说明
常见问题
连接不上数据库
先确认 MySQL 容器是否启动:
docker compose ps
再确认 .env.development 里的 DB_PORT 是 3307。
迁移执行过了,为什么再次运行会跳过
src/db/migrate.ts 会把执行过的文件名写入 schema_migrations。再次运行时,如果文件名已存在,就会跳过,避免重复建表或重复插入数据。
删除员工后,为什么数据库里还有记录
这是软删除。删除接口会把 deleted_at 设置为当前时间,并把状态改成 INACTIVE。这样可以保留历史数据,同时普通查询会过滤掉已删除记录。
为什么不使用 ORM
这个项目的目标是学习 SQL 和 MySQL 基础能力。直接使用 mysql2 可以更清楚地看到 SQL、索引、事务和参数绑定是如何工作的。