diff --git a/API.md b/API.md new file mode 100644 index 0000000..d7934f3 --- /dev/null +++ b/API.md @@ -0,0 +1,1070 @@ +# 蜜雪冰城企业管理系统 —— 后端 API 文档 + +> 蜜雪冰城总部用于管理加盟商、合同、员工、产品、供应商及售后服务的企业信息管理系统后端服务。 + +## 目录 + +- [快速开始](#快速开始) +- [技术栈](#技术栈) +- [项目结构](#项目结构) +- [认证机制](#认证机制) +- [API 接口总览](#api-接口总览) + - [用户认证](#1-用户认证) + - [用户管理](#2-用户管理) + - [加盟商管理](#3-加盟商管理) + - [员工管理](#4-员工管理) + - [合同管理](#5-合同管理) + - [售后管理](#6-售后管理) + - [产品管理](#7-产品管理) + - [供应商管理](#8-供应商管理) +- [统一响应格式](#统一响应格式) +- [数据库设计](#数据库设计) + - [表结构详情](#表结构详情) + - [表关系 ER 图](#表关系-er-图) +- [RBAC 权限系统](#rbac-权限系统) + - [角色定义](#角色定义) + - [权限列表](#权限列表) + - [角色-权限映射](#角色-权限映射) + - [数据范围隔离](#数据范围隔离) +- [预置账号](#预置账号) + +--- + +## 快速开始 + +### 环境要求 + +- Node.js >= 18 +- MySQL 5.7+ / MariaDB 10.2+ + +### 安装与运行 + +```bash +# 1. 安装依赖 +npm install + +# 2. 配置环境变量 +cp .env.example .env +# 编辑 .env 填入数据库连接信息和 JWT 密钥 + +# 3. 启动开发服务(自动重启) +npm run dev + +# 4. 启动生产服务 +npm start + +# 5. 运行集成测试(需先启动服务) +node test-all.js +``` + +### 环境变量 (.env) + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `PORT` | 服务端口 | `3000` | +| `DB_HOST` | MySQL 主机 | — | +| `DB_PORT` | MySQL 端口 | `3306` | +| `DB_USER` | MySQL 用户名 | — | +| `DB_PASSWORD` | MySQL 密码 | — | +| `DB_NAME` | 数据库名 | `backmanager` | +| `JWT_SECRET` | JWT 签名密钥 | — | +| `JWT_EXPIRES_IN` | Token 有效期 | `2h` | + +> 首次启动时,系统会自动创建数据库、建表、初始化角色权限和预置用户。 + +--- + +## 技术栈 + +| 技术 | 用途 | +|------|------| +| Express 5 | Web 框架 | +| mysql2/promise | MySQL 驱动(连接池 + Promise) | +| jsonwebtoken | JWT 认证 | +| bcryptjs | 密码哈希加密 | +| dotenv | 环境变量管理 | + +--- + +## 项目结构 + +``` +backmanager-server/ +├── server.js # 入口:Express 应用、鉴权中间件、路由注册 +├── db.js # 数据库:连接池、建表、种子数据 +├── middleware/ +│ └── permissions.js # RBAC:权限检查 + 数据范围过滤 +├── routes/ +│ ├── users.js # 用户登录/信息/CRUD +│ ├── customers.js # 加盟商 CRUD +│ ├── employees.js # 员工 CRUD +│ ├── contracts.js # 合同 CRUD +│ ├── afterSales.js # 售后 CRUD +│ ├── products.js # 产品 CRUD +│ └── suppliers.js # 供应商 CRUD +├── test-all.js # 集成测试(覆盖 6 个角色的权限与数据隔离) +├── .env.example # 环境变量模板 +└── package.json +``` + +--- + +## 认证机制 + +所有 API(除登录外)需要在请求头中携带 JWT Token: + +``` +Authorization: Bearer +``` + +### Token 获取 + +通过 `POST /api/user/login` 登录获取 Token。 + +### Token 有效载荷 (Payload) + +```json +{ + "id": 1, + "username": "admin", + "name": "系统管理员", + "role_id": 1, + "roleName": "admin", + "department": "信息技术部", + "permissions": ["customer:read", "customer:create", "customer:update", "customer:delete", "contract:read", "..."] +} +``` + +### 认证流程 + +``` +客户端请求 → auth 中间件验证 Token → checkPermission 检查权限 → 路由处理 → 数据范围过滤(getDataScope)→ 返回响应 +``` + +### 错误响应 + +| HTTP 状态码 | 场景 | +|------------|------| +| 401 | 未携带 Token 或 Token 无效/过期 | +| 403 | 权限不足(无对应操作权限或数据不在可见范围内) | + +--- + +## API 接口总览 + +### 通用说明 + +- **分页参数**:所有列表接口支持 `page`(页码,从 1 开始,默认 1)和 `pageSize`(每页条数,默认 10,最大 100) +- **分页响应**:列表接口返回 `{ list, total, page, pageSize, totalPages }` +- **搜索参数**:通过 URL Query String 传递,如 `GET /api/customers?name=张&phone=138` +- **部分更新**:PUT 接口只传需要修改的字段即可,未传字段保持不变 + +--- + +### 1. 用户认证 + +#### POST /api/user/login — 登录 + +无需 Token。 + +**请求体:** + +```json +{ + "username": "admin", + "password": "123456" +} +``` + +**成功响应:** + +```json +{ + "code": 200, + "message": "ok", + "data": { + "token": "eyJhbGciOiJIUzI1NiIs...", + "userInfo": { + "id": 1, + "username": "admin", + "name": "系统管理员", + "role_id": 1, + "roleName": "admin", + "department": "信息技术部", + "permissions": ["customer:read", "customer:create", "..."] + } + } +} +``` + +**错误响应:** + +| code | message | +|------|---------| +| 400 | 用户名和密码必填 | +| 400 | 账号或密码错误 | +| 403 | 账号已被禁用,请联系管理员 | + +#### GET /api/user/info — 获取当前用户信息 + +需要 Token。返回当前登录用户的完整信息(含角色名、员工名)。 + +#### POST /api/user/logout — 登出 + +需要 Token。服务端无状态,仅返回成功应答,客户端自行清除 Token。 + +#### PUT /api/user/password — 修改密码 + +需要 Token。修改当前登录用户自己的密码。 + +**请求体:** + +```json +{ + "oldPassword": "123456", + "newPassword": "654321" +} +``` + +**校验规则:** +- 旧密码和新密码必填 +- 新密码至少 6 位 +- 新密码不能与旧密码相同 + +--- + +### 2. 用户管理 + +> 需要 `user:manage` 权限(仅系统管理员拥有)。 + +#### GET /api/users — 用户列表 + +**查询参数:** + +| 参数 | 说明 | 示例 | +|------|------|------| +| `username` | 按用户名模糊搜索 | `?username=admin` | +| `role_id` | 按角色 ID 过滤 | `?role_id=2` | +| `is_active` | 按状态过滤(0-禁用, 1-启用) | `?is_active=1` | +| `page` | 页码 | `?page=1` | +| `pageSize` | 每页条数 | `?pageSize=10` | + +**响应数据字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | number | 用户 ID | +| `username` | string | 用户名 | +| `is_active` | number | 状态:0-禁用, 1-启用 | +| `role_id` | number | 角色 ID | +| `role_name` | string | 角色标识(如 `admin`) | +| `role_description` | string | 角色描述(如 系统管理员) | +| `employee_id` | number | 关联员工 ID | +| `real_name` | string | 真实姓名(来自员工表) | +| `department` | string | 所属部门 | +| `created_at` | datetime | 创建时间 | +| `updated_at` | datetime | 更新时间 | + +#### GET /api/users/:id — 用户详情 + +返回单个用户完整信息。 + +#### POST /api/users — 创建用户 + +**请求体:** + +| 字段 | 必填 | 说明 | +|------|------|------| +| `username` | ✅ | 用户名(3-50 字符) | +| `password` | ✅ | 密码(至少 6 位) | +| `role_id` | ❌ | 角色 ID | +| `employee_id` | ❌ | 关联员工 ID | +| `department` | ❌ | 所属部门(若填了 employee_id 会自动同步) | +| `is_active` | ❌ | 状态,默认 1 | + +#### PUT /api/users/:id — 更新用户 + +**可更新字段:** `username`、`role_id`、`employee_id`、`department`、`is_active`、`password` + +**限制:** +- 不能修改自己的角色 +- 不能禁用自己 + +#### DELETE /api/users/:id — 删除用户 + +**限制:** 不能删除自己。 + +--- + +### 3. 加盟商管理 + +> 需要 `customer:read` / `customer:create` / `customer:update` / `customer:delete` 权限。 + +#### GET /api/customers — 加盟商列表 + +**查询参数:** + +| 参数 | 说明 | +|------|------| +| `name` | 按加盟商名称模糊搜索 | +| `phone` | 按电话模糊搜索 | +| `province` | 按省份精确匹配 | +| `city` | 按城市精确匹配 | + +**响应数据字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | number | 加盟商 ID | +| `name` | string | 加盟商姓名 | +| `phone` | string | 联系电话 | +| `province` | string | 省 | +| `city` | string | 市 | +| `district` | string | 区 | +| `address` | string | 详细地址 | +| `email` | string | 电子邮箱 | +| `remark` | string | 备注信息 | +| `responsible_user_id` | number | 负责人用户 ID | +| `created_at` | datetime | 创建时间 | +| `updated_at` | datetime | 更新时间 | + +#### POST /api/customers — 新增加盟商 + +**请求体:** + +| 字段 | 必填 | 说明 | +|------|------|------| +| `name` | ✅ | 加盟商姓名 | +| `phone` | ❌ | 联系电话 | +| `province` | ❌ | 省 | +| `city` | ❌ | 市 | +| `district` | ❌ | 区 | +| `address` | ❌ | 详细地址 | +| `email` | ❌ | 电子邮箱 | +| `remark` | ❌ | 备注 | +| `responsible_user_id` | ❌ | 负责人(默认为当前登录用户) | + +#### PUT /api/customers/:id — 更新加盟商 + +所有字段均可选,只传需要修改的字段。 + +#### DELETE /api/customers/:id — 删除加盟商 + +--- + +### 4. 员工管理 + +> 需要 `employee:read` / `employee:create` / `employee:update` / `employee:delete` 权限。 + +#### GET /api/employees — 员工列表 + +**查询参数:** + +| 参数 | 说明 | +|------|------| +| `name` | 按姓名模糊搜索 | +| `department` | 按部门模糊搜索 | +| `status` | 按在职状态过滤(0-离职, 1-在职) | + +**响应数据字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | number | 员工 ID | +| `name` | string | 姓名 | +| `gender` | string | 性别(男/女) | +| `age` | number | 年龄 | +| `education` | string | 学历 | +| `department` | string | 所属部门 | +| `entry_date` | date | 入职时间 | +| `position` | string | 职务/岗位 | +| `salary` | number | 工资金额 | +| `phone` | string | 联系电话 | +| `email` | string | 电子邮箱 | +| `status` | number | 在职状态:0-离职, 1-在职 | +| `remark` | string | 备注 | +| `created_at` | datetime | 创建时间 | +| `updated_at` | datetime | 更新时间 | + +#### POST /api/employees — 新增员工 + +**请求体:** + +| 字段 | 必填 | 说明 | +|------|------|------| +| `name` | ✅ | 员工姓名 | +| `gender` | ❌ | 性别 | +| `age` | ❌ | 年龄 | +| `education` | ❌ | 学历 | +| `department` | ❌ | 部门 | +| `entry_date` | ❌ | 入职时间(格式:YYYY-MM-DD) | +| `position` | ❌ | 职务 | +| `salary` | ❌ | 工资 | +| `phone` | ❌ | 电话 | +| `email` | ❌ | 邮箱 | +| `status` | ❌ | 在职状态,默认 1 | +| `remark` | ❌ | 备注 | + +#### PUT /api/employees/:id — 更新员工 + +所有字段均可选,只传需要修改的字段。 + +#### DELETE /api/employees/:id — 删除员工 + +--- + +### 5. 合同管理 + +> 需要 `contract:read` / `contract:create` / `contract:update` / `contract:delete` 权限。 + +#### GET /api/contracts — 合同列表 + +**查询参数:** + +| 参数 | 说明 | +|------|------| +| `status` | 按合同状态过滤(草稿/生效/完成/作废) | +| `customer_name` | 按客户名称模糊搜索 | +| `contract_no` | 按合同编号模糊搜索 | +| `employee_name` | 按业务员姓名模糊搜索 | + +**响应数据字段(含 JOIN 字段):** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | number | 合同 ID | +| `customer_id` | number | 客户 ID | +| `customer_name` | string | 客户名称(JOIN 自 customers) | +| `contract_name` | string | 合同名称 | +| `contract_no` | string | 合同编号 | +| `contract_content` | string | 合同内容/条款 | +| `amount` | number | 合同金额 | +| `effective_date` | date | 生效日期 | +| `expiry_date` | date | 到期日期 | +| `employee_id` | number | 业务员 ID | +| `employee_name` | string | 业务员姓名(JOIN 自 employees) | +| `status` | string | 状态:草稿/生效/完成/作废 | +| `remark` | string | 备注 | +| `type` | string | 合同类型:`franchise`(加盟)/ `supply`(采购) | +| `responsible_user_id` | number | 负责人用户 ID | +| `created_at` | datetime | 创建时间 | +| `updated_at` | datetime | 更新时间 | + +#### POST /api/contracts — 新增合同 + +**请求体:** + +| 字段 | 必填 | 说明 | +|------|------|------| +| `customer_id` | ✅ | 关联客户 ID | +| `contract_name` | ✅ | 合同名称 | +| `contract_no` | ❌ | 合同编号 | +| `contract_content` | ❌ | 合同内容 | +| `amount` | ❌ | 合同金额 | +| `effective_date` | ❌ | 生效日期 | +| `expiry_date` | ❌ | 到期日期 | +| `employee_id` | ❌ | 业务员 ID | +| `status` | ❌ | 状态,默认 `生效` | +| `remark` | ❌ | 备注 | +| `type` | ❌ | 合同类型(见下方说明) | +| `responsible_user_id` | ❌ | 负责人(默认为当前用户) | + +**合同类型自动设置规则:** +- 采购经理创建的合同自动设为 `supply` +- 其他角色默认 `franchise`,也可手动指定 + +#### PUT /api/contracts/:id — 更新合同 + +所有字段均可选。可更新字段包括 `type` 和 `responsible_user_id`。 + +#### DELETE /api/contracts/:id — 删除合同 + +--- + +### 6. 售后管理 + +> 需要 `after_sale:read` / `after_sale:create` / `after_sale:update` / `after_sale:delete` 权限。 + +#### GET /api/after-sales — 售后列表 + +**查询参数:** + +| 参数 | 说明 | +|------|------| +| `handle_status` | 按处理状态过滤(待处理/处理中/已完成) | +| `customer_name` | 按客户名称模糊搜索 | + +**响应数据字段(含 JOIN 字段):** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | number | 售后记录 ID | +| `customer_id` | number | 客户 ID | +| `customer_name` | string | 客户名称(JOIN 自 customers) | +| `feedback` | string | 客户反馈意见 | +| `employee_id` | number | 处理业务员 ID | +| `employee_name` | string | 业务员姓名(JOIN 自 employees) | +| `handle_method` | string | 处理方式/解决方案 | +| `handle_status` | string | 处理状态:待处理/处理中/已完成 | +| `service_date` | date | 售后日期 | +| `remark` | string | 备注 | +| `responsible_user_id` | number | 负责人用户 ID | +| `created_at` | datetime | 创建时间 | +| `updated_at` | datetime | 更新时间 | + +#### POST /api/after-sales — 新增售后 + +**请求体:** + +| 字段 | 必填 | 说明 | +|------|------|------| +| `customer_id` | ✅ | 关联客户 ID | +| `feedback` | ✅ | 售后反馈内容 | +| `employee_id` | ❌ | 处理业务员 ID | +| `handle_method` | ❌ | 处理方式 | +| `handle_status` | ❌ | 处理状态,默认 `待处理` | +| `service_date` | ❌ | 售后日期 | +| `remark` | ❌ | 备注 | +| `responsible_user_id` | ❌ | 负责人(默认为当前用户) | + +#### PUT /api/after-sales/:id — 更新售后 + +所有字段均可选。 + +#### DELETE /api/after-sales/:id — 删除售后 + +--- + +### 7. 产品管理 + +> 需要 `product:read` / `product:create` / `product:update` / `product:delete` 权限。 +> 无数据范围隔离,拥有权限即可查看所有产品。 + +#### GET /api/products — 产品列表 + +**查询参数:** + +| 参数 | 说明 | +|------|------| +| `name` | 按产品名称模糊搜索 | +| `type` | 按产品类型模糊搜索 | +| `supplier` | 按供应商模糊搜索 | + +**响应数据字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | number | 产品 ID | +| `name` | string | 产品名称 | +| `type` | string | 产品类型/分类 | +| `quantity` | number | 库存数量 | +| `price` | number | 单价 | +| `unit` | string | 计量单位(默认"件") | +| `specification` | string | 规格/型号 | +| `supplier` | string | 供应商 | +| `remark` | string | 备注 | +| `created_at` | datetime | 创建时间 | +| `updated_at` | datetime | 更新时间 | + +#### POST /api/products — 新增产品 + +**请求体:** + +| 字段 | 必填 | 说明 | +|------|------|------| +| `name` | ✅ | 产品名称 | +| `type` | ❌ | 类型 | +| `quantity` | ❌ | 库存数量,默认 0 | +| `price` | ❌ | 单价,默认 0.00 | +| `unit` | ❌ | 单位,默认"件" | +| `specification` | ❌ | 规格 | +| `supplier` | ❌ | 供应商 | +| `remark` | ❌ | 备注 | + +#### PUT /api/products/:id — 更新产品 + +所有字段均可选。 + +#### DELETE /api/products/:id — 删除产品 + +--- + +### 8. 供应商管理 + +> 需要 `supplier:read` / `supplier:create` / `supplier:update` / `supplier:delete` 权限。 +> 无数据范围隔离,拥有权限即可查看所有供应商。 + +#### GET /api/suppliers — 供应商列表 + +**查询参数:** + +| 参数 | 说明 | +|------|------| +| `name` | 按供应商名称模糊搜索 | +| `type` | 按类型模糊搜索 | +| `status` | 按状态过滤(0-停用, 1-正常) | + +**响应数据字段:** + +| 字段 | 类型 | 说明 | +|------|------|------| +| `id` | number | 供应商 ID | +| `name` | string | 供应商名称 | +| `contact` | string | 联系人 | +| `phone` | string | 联系电话 | +| `address` | string | 地址 | +| `type` | string | 类型(原材料/包装/设备) | +| `content` | string | 备注说明 | +| `status` | number | 状态:0-停用, 1-正常 | +| `created_at` | datetime | 创建时间 | +| `updated_at` | datetime | 更新时间 | + +#### POST /api/suppliers — 新增供应商 + +**请求体:** + +| 字段 | 必填 | 说明 | +|------|------|------| +| `name` | ✅ | 供应商名称 | +| `contact` | ❌ | 联系人 | +| `phone` | ❌ | 联系电话 | +| `address` | ❌ | 地址 | +| `type` | ❌ | 类型 | +| `content` | ❌ | 备注说明 | +| `status` | ❌ | 状态,默认 1 | + +#### PUT /api/suppliers/:id — 更新供应商 + +所有字段均可选。 + +#### DELETE /api/suppliers/:id — 删除供应商 + +--- + +## 统一响应格式 + +### 成功响应 + +```json +{ + "code": 0, + "message": "ok", + "data": { ... } +} +``` + +### 列表响应 + +```json +{ + "code": 0, + "message": "ok", + "data": { + "list": [ ... ], + "total": 100, + "page": 1, + "pageSize": 10, + "totalPages": 10 + } +} +``` + +### 错误响应 + +```json +{ + "code": 403, + "message": "权限不足" +} +``` + +| HTTP 状态码 | code | 说明 | +|------------|------|------| +| 400 | 400 | 参数错误 / 校验失败 | +| 401 | 401 | 未登录 / Token 无效 | +| 403 | 403 | 权限不足 / 账号被禁用 | +| 404 | 404 | 资源不存在 | +| 409 | 409 | 数据冲突(如用户名重复) | +| 500 | 500 | 服务器内部错误 | + +--- + +## 数据库设计 + +### 表结构详情 + +#### 1. users — 系统用户表 + +存储系统登录账号,每个用户关联一个角色和一个员工。 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | INT | PK, AUTO_INCREMENT | 用户 ID | +| `username` | VARCHAR(50) | NOT NULL, UNIQUE | 用户名(登录账号) | +| `password` | VARCHAR(255) | NOT NULL | 密码(bcrypt 哈希) | +| `is_active` | TINYINT(1) | NOT NULL DEFAULT 1 | 账号状态:0-禁用, 1-启用 | +| `role_id` | INT | NULLABLE | 角色 ID → roles.id | +| `employee_id` | INT | NULLABLE | 员工 ID → employees.id | +| `department` | VARCHAR(100) | NULLABLE | 所属部门(冗余字段,从 employees 同步) | +| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +#### 2. roles — 角色表 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | INT | PK, AUTO_INCREMENT | 角色 ID | +| `name` | VARCHAR(50) | NOT NULL, UNIQUE | 角色标识(英文) | +| `description` | VARCHAR(200) | NULLABLE | 角色描述(中文) | +| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | + +#### 3. permissions — 权限表 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | INT | PK, AUTO_INCREMENT | 权限 ID | +| `name` | VARCHAR(100) | NOT NULL, UNIQUE | 权限标识(`resource:action` 格式) | +| `description` | VARCHAR(200) | NULLABLE | 权限描述(中文) | +| `resource` | VARCHAR(50) | NOT NULL | 资源名称 | +| `action` | VARCHAR(50) | NOT NULL | 操作类型 | + +#### 4. role_permissions — 角色-权限关联表 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `role_id` | INT | PK (联合) | 角色 ID → roles.id | +| `permission_id` | INT | PK (联合) | 权限 ID → permissions.id | + +#### 5. customers — 加盟商信息表 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | INT | PK, AUTO_INCREMENT | 加盟商 ID | +| `name` | VARCHAR(100) | NOT NULL, INDEX | 加盟商姓名 | +| `phone` | VARCHAR(20) | INDEX | 联系电话 | +| `province` | VARCHAR(50) | — | 省 | +| `city` | VARCHAR(50) | — | 市 | +| `district` | VARCHAR(50) | — | 区 | +| `address` | VARCHAR(200) | — | 详细地址 | +| `email` | VARCHAR(100) | — | 电子邮箱 | +| `remark` | TEXT | — | 备注信息 | +| `responsible_user_id` | INT | — | 负责人用户 ID → users.id | +| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +#### 6. employees — 员工信息表 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | INT | PK, AUTO_INCREMENT | 员工 ID | +| `name` | VARCHAR(100) | NOT NULL, INDEX | 姓名 | +| `gender` | VARCHAR(4) | — | 性别:男/女 | +| `age` | INT | — | 年龄 | +| `education` | VARCHAR(50) | — | 学历 | +| `department` | VARCHAR(100) | INDEX | 所属部门 | +| `entry_date` | DATE | — | 入职时间 | +| `position` | VARCHAR(100) | — | 职务/岗位 | +| `salary` | DECIMAL(10,2) | — | 工资金额 | +| `phone` | VARCHAR(20) | — | 联系电话 | +| `email` | VARCHAR(100) | — | 电子邮箱 | +| `status` | TINYINT(1) | NOT NULL DEFAULT 1 | 在职状态:0-离职, 1-在职 | +| `remark` | TEXT | — | 备注 | +| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +#### 7. contracts — 合同信息表 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | INT | PK, AUTO_INCREMENT | 合同 ID | +| `customer_id` | INT | NOT NULL, INDEX | 客户 ID → customers.id | +| `contract_name` | VARCHAR(200) | NOT NULL | 合同名称 | +| `contract_no` | VARCHAR(100) | — | 合同编号 | +| `contract_content` | TEXT | — | 合同内容/条款 | +| `amount` | DECIMAL(12,2) | — | 合同金额 | +| `effective_date` | DATE | INDEX | 生效日期 | +| `expiry_date` | DATE | — | 到期日期 | +| `employee_id` | INT | INDEX | 业务员 ID → employees.id | +| `status` | VARCHAR(20) | NOT NULL DEFAULT '生效' | 状态:草稿/生效/完成/作废 | +| `remark` | TEXT | — | 备注 | +| `type` | VARCHAR(20) | NOT NULL DEFAULT 'franchise' | 类型:`franchise`(加盟) / `supply`(采购) | +| `responsible_user_id` | INT | — | 负责人用户 ID → users.id | +| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +#### 8. after_sales — 售后信息表 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | INT | PK, AUTO_INCREMENT | 售后记录 ID | +| `customer_id` | INT | NOT NULL, INDEX | 客户 ID → customers.id | +| `feedback` | TEXT | NOT NULL | 客户反馈意见 | +| `employee_id` | INT | INDEX | 处理业务员 ID → employees.id | +| `handle_method` | TEXT | — | 处理方式/解决方案 | +| `handle_status` | VARCHAR(20) | NOT NULL DEFAULT '待处理', INDEX | 状态:待处理/处理中/已完成 | +| `service_date` | DATE | — | 售后日期 | +| `remark` | TEXT | — | 备注 | +| `responsible_user_id` | INT | — | 负责人用户 ID → users.id | +| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +#### 9. products — 产品信息表 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | INT | PK, AUTO_INCREMENT | 产品 ID | +| `name` | VARCHAR(200) | NOT NULL, INDEX | 产品名称 | +| `type` | VARCHAR(100) | INDEX | 产品类型/分类 | +| `quantity` | INT | NOT NULL DEFAULT 0 | 库存数量 | +| `price` | DECIMAL(10,2) | NOT NULL DEFAULT 0.00 | 单价 | +| `unit` | VARCHAR(20) | DEFAULT '件' | 计量单位 | +| `specification` | VARCHAR(200) | — | 规格/型号 | +| `supplier` | VARCHAR(200) | — | 供应商名称 | +| `remark` | TEXT | — | 备注 | +| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +#### 10. suppliers — 供应商信息表 + +| 字段 | 类型 | 约束 | 说明 | +|------|------|------|------| +| `id` | INT | PK, AUTO_INCREMENT | 供应商 ID | +| `name` | VARCHAR(200) | NOT NULL, INDEX | 供应商名称 | +| `contact` | VARCHAR(100) | — | 联系人 | +| `phone` | VARCHAR(50) | — | 联系电话 | +| `address` | VARCHAR(500) | — | 地址 | +| `type` | VARCHAR(50) | — | 类型(原材料/包装/设备) | +| `content` | TEXT | — | 备注说明 | +| `status` | TINYINT(1) | NOT NULL DEFAULT 1 | 状态:0-停用, 1-正常 | +| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | 更新时间 | + +--- + +### 表关系 ER 图 + +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ +│ roles │ │ permissions │ │ role_permissions │ +│──────────────│ │──────────────│ │──────────────────│ +│ id (PK) │◄─┐ │ id (PK) │◄─┐ │ role_id (PK,FK) │ +│ name │ │ │ name │ └──│ permission_id │ +│ description │ │ │ description │ │ (PK,FK) │ +└──────────────┘ │ │ resource │ └──────────────────┘ + │ │ action │ + │ └──────────────┘ + │ +┌──────────────┐ │ ┌──────────────┐ +│ users │ │ │ employees │ +│──────────────│ │ │──────────────│ +│ id (PK) │ │ │ id (PK) │ +│ username │ │ │ name │ +│ password │ │ │ gender │ +│ is_active │ │ │ age │ +│ role_id (FK)─┼──┘ │ education │ +│ employee_id │─ ─ ─│ department │◄──────── users.department (冗余) +│ (FK) ──────┼──→ │ entry_date │ +│ department │ │ position │ +│ (冗余) │ │ salary │ +└──────┬───────┘ │ phone │ + │ │ email │ + │ │ status │ + │ └──────┬───────┘ + │ │ + ▼ ▼ +┌──────────────────────────────────────────────┐ +│ customers (加盟商) │ +│──────────────────────────────────────────────│ +│ id (PK) │ +│ name, phone, province, city, district, ... │ +│ responsible_user_id ──────────→ users.id │ +└────────────────────┬─────────────────────────┘ + │ + ┌──────────┴──────────┐ + ▼ ▼ +┌──────────────────┐ ┌──────────────────┐ +│ contracts │ │ after_sales │ +│──────────────────│ │──────────────────│ +│ id (PK) │ │ id (PK) │ +│ customer_id (FK) │ │ customer_id (FK) │ +│ → customers.id │ │ → customers.id │ +│ contract_name │ │ feedback │ +│ contract_no │ │ employee_id (FK) │ +│ amount │ │ → employees.id │ +│ employee_id (FK) │ │ handle_method │ +│ → employees.id │ │ handle_status │ +│ status │ │ service_date │ +│ type │ │ responsible_user │ +│ franchise/ │ │ _id → users.id │ +│ supply │ └──────────────────┘ +│ responsible_user │ +│ _id → users.id │ +└──────────────────┘ + +┌──────────────┐ ┌──────────────┐ +│ products │ │ suppliers │ +│──────────────│ │──────────────│ +│ id (PK) │ │ id (PK) │ +│ name │ │ name │ +│ type │ │ contact │ +│ quantity │ │ phone │ +│ price │ │ address │ +│ unit │ │ type │ +│ specification│ │ content │ +│ supplier │ │ status │ +└──────────────┘ └──────────────┘ +``` + +**关系说明:** + +| 关系 | 类型 | 说明 | +|------|------|------| +| users → roles | 多对一 | 一个用户属于一个角色,一个角色可以有多个用户 | +| users → employees | 一对一 | 一个用户关联一个员工(通过 employee_id) | +| roles ↔ permissions | 多对多 | 通过 role_permissions 中间表关联 | +| customers → users | 多对一 | 通过 responsible_user_id 指定负责人 | +| contracts → customers | 多对一 | 通过 customer_id 关联客户 | +| contracts → employees | 多对一 | 通过 employee_id 关联业务员 | +| contracts → users | 多对一 | 通过 responsible_user_id 指定负责人 | +| after_sales → customers | 多对一 | 通过 customer_id 关联客户 | +| after_sales → employees | 多对一 | 通过 employee_id 关联业务员 | +| after_sales → users | 多对一 | 通过 responsible_user_id 指定负责人 | + +> **注意**:数据库未设置外键约束,引用完整性由应用代码保证(插入/更新前检查关联记录是否存在)。 + +--- + +## RBAC 权限系统 + +### 角色定义 + +系统预置 6 个角色,对应蜜雪冰城总部的组织架构: + +| 角色标识 | 中文名 | 所属部门 | 职责概述 | +|---------|--------|---------|---------| +| `admin` | 系统管理员 | 信息技术部 | 系统全权管理,拥有所有权限 | +| `general_manager` | 总经理 | 总经理办公室 | 全局只读,查看所有业务数据 | +| `franchise_manager` | 招商经理 | 招商部 | 管理加盟商和加盟合同 | +| `operations_manager` | 运营经理 | 运营部 | 维护加盟商信息、处理售后 | +| `procurement_manager` | 采购经理 | 采购部 | 管理产品、供应商和采购合同 | +| `finance` | 财务人员 | 财务部 | 只读查看业务数据(加盟商、合同、售后、员工) | + +### 权限列表 + +共 25 个权限,按 `资源:操作` 格式命名: + +| 权限标识 | 中文描述 | +|---------|---------| +| **加盟商 (customer)** | | +| `customer:read` | 查看加盟商 | +| `customer:create` | 新增加盟商 | +| `customer:update` | 修改加盟商 | +| `customer:delete` | 删除加盟商 | +| **合同 (contract)** | | +| `contract:read` | 查看合同 | +| `contract:create` | 新增合同 | +| `contract:update` | 修改合同 | +| `contract:delete` | 删除合同 | +| **售后 (after_sale)** | | +| `after_sale:read` | 查看售后 | +| `after_sale:create` | 新增售后 | +| `after_sale:update` | 修改售后 | +| `after_sale:delete` | 删除售后 | +| **产品 (product)** | | +| `product:read` | 查看产品原料 | +| `product:create` | 新增产品原料 | +| `product:update` | 修改产品原料 | +| `product:delete` | 删除产品原料 | +| **供应商 (supplier)** | | +| `supplier:read` | 查看供应商 | +| `supplier:create` | 新增供应商 | +| `supplier:update` | 修改供应商 | +| `supplier:delete` | 删除供应商 | +| **员工 (employee)** | | +| `employee:read` | 查看员工 | +| `employee:create` | 新增员工 | +| `employee:update` | 修改员工 | +| `employee:delete` | 删除员工 | +| **用户 (user)** | | +| `user:manage` | 管理用户账号 | + +### 角色-权限映射 + +| 权限 | admin | general_manager | franchise_manager | operations_manager | procurement_manager | finance | +|------|:-----:|:---------------:|:-----------------:|:------------------:|:-------------------:|:-------:| +| customer:read | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | +| customer:create | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | +| customer:update | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | +| customer:delete | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | +| contract:read | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| contract:create | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | +| contract:update | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | +| contract:delete | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | +| after_sale:read | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | +| after_sale:create | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | +| after_sale:update | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | +| after_sale:delete | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | +| product:read | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | +| product:create | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | +| product:update | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | +| product:delete | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | +| supplier:read | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | +| supplier:create | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | +| supplier:update | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | +| supplier:delete | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | +| employee:read | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| employee:create | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| employee:update | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| employee:delete | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| user:manage | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | + +### 数据范围隔离 + +拥有操作权限 ≠ 能看到所有数据。系统通过 `getDataScope()` 函数在 SQL 查询层面做数据隔离: + +| 资源 | admin | general_manager | franchise_manager | operations_manager | procurement_manager | finance | +|------|-------|-----------------|-------------------|--------------------|--------------------|---------| +| customers | 全部 | 全部 | 仅自己负责 | 仅自己负责 | — | 全部 | +| contracts | 全部 | 全部 | 仅自己负责的加盟合同 | 仅自己负责的加盟合同 | 仅采购合同 | 全部 | +| after_sales | 全部 | 全部 | — | 仅自己负责 | — | 全部 | +| employees | 全部 | 全部 | 仅本部门 | 仅本部门 | 仅本部门 | 全部 | +| products | 全部 | 全部 | — | — | 全部 | — | +| suppliers | 全部 | 全部 | — | — | 全部 | — | +| users | 全部 | — | — | — | — | — | + +> **"—"** 表示该角色无此资源的权限,请求会被权限中间件直接拦截(返回 403)。 + +**隔离机制说明:** +- **负责人类资源**(customers、contracts、after_sales):通过 `responsible_user_id` 字段过滤,只能看到自己负责的记录 +- **合同类型过滤**(contracts):除了负责人类过滤,还按合同 `type` 字段过滤(franchise/supply) +- **部门类资源**(employees):通过 `department` 字段过滤,只能看到自己部门的员工 +- **全局类资源**(products、suppliers):无数据范围过滤,只要有权限就能看到全部 + +--- + +## 预置账号 + +系统首次启动时自动创建以下账号,密码均为 `123456`: + +| 用户名 | 姓名 | 角色 | 部门 | 职位 | +|--------|------|------|------|------| +| `admin` | 系统管理员 | admin | 信息技术部 | 系统管理员 | +| `zhangchao` | 张超 | general_manager | 总经理办公室 | 总经理 | +| `liming` | 李明 | franchise_manager | 招商部 | 招商经理 | +| `wangli` | 王丽 | operations_manager | 运营部 | 运营经理 | +| `zhaoqiang` | 赵强 | procurement_manager | 采购部 | 采购经理 | +| `chenfang` | 陈芳 | finance | 财务部 | 财务主管 | + +### 快速测试 + +```bash +# 以管理员身份登录 +curl -X POST http://127.0.0.1:3000/api/user/login \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"123456"}' + +# 以招商经理身份登录(只能看到自己负责的加盟商) +curl -X POST http://127.0.0.1:3000/api/user/login \ + -H "Content-Type: application/json" \ + -d '{"username":"liming","password":"123456"}' +``` diff --git a/db.js b/db.js index 1730d87..76fcac0 100644 --- a/db.js +++ b/db.js @@ -59,9 +59,9 @@ async function initDB() { city VARCHAR(50) DEFAULT NULL COMMENT '市', district VARCHAR(50) DEFAULT NULL COMMENT '区', address VARCHAR(200) DEFAULT NULL COMMENT '详细地址', - customer_type VARCHAR(20) DEFAULT 'Normal' COMMENT '客户类型: VIP-地区总代理, Normal-普通代理', email VARCHAR(100) DEFAULT NULL COMMENT '电子邮箱', remark TEXT DEFAULT NULL COMMENT '备注信息', + responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID (用于数据范围隔离)', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (id), @@ -69,8 +69,6 @@ async function initDB() { KEY idx_customers_phone (phone) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='加盟商信息表' `) - // 确保 customer_type 列存在(兼容旧表) - try { await pool.query(`ALTER TABLE customers ADD COLUMN customer_type VARCHAR(20) DEFAULT 'Normal' COMMENT '客户类型: VIP-地区总代理, Normal-普通代理' AFTER address`) } catch {} await pool.query(` CREATE TABLE IF NOT EXISTS employees ( id INT NOT NULL AUTO_INCREMENT COMMENT '员工ID (主键)', @@ -125,6 +123,8 @@ async function initDB() { employee_id INT DEFAULT NULL COMMENT '业务员ID (关联员工表)', status VARCHAR(20) NOT NULL DEFAULT '生效' COMMENT '合同状态: 草稿/生效/完成/作废', remark TEXT DEFAULT NULL COMMENT '备注', + type VARCHAR(20) NOT NULL DEFAULT 'franchise' COMMENT '合同类型: franchise-加盟合同, supply-采购合同', + responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID (用于数据范围隔离)', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (id), @@ -143,6 +143,7 @@ async function initDB() { handle_status VARCHAR(20) NOT NULL DEFAULT '待处理' COMMENT '处理状态: 待处理/处理中/已完成', service_date DATE DEFAULT NULL COMMENT '售后日期', remark TEXT DEFAULT NULL COMMENT '备注', + responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID (用于数据范围隔离)', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (id), @@ -203,27 +204,7 @@ async function initDB() { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商信息表' `) - // ============ 2.4 旧表迁移(兼容已有数据库) ============ - // users 表:删除 real_name、role,新增 is_active、role_id、employee_id、department - try { await pool.query(`ALTER TABLE users DROP COLUMN real_name`) } catch {} - try { await pool.query(`ALTER TABLE users DROP COLUMN role`) } catch {} - try { await pool.query(`ALTER TABLE users CHANGE COLUMN status is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态: 0-禁用, 1-启用'`) } catch {} - try { await pool.query(`ALTER TABLE users ADD COLUMN role_id INT DEFAULT NULL COMMENT '角色ID' AFTER is_active`) } catch {} - try { await pool.query(`ALTER TABLE users ADD COLUMN employee_id INT DEFAULT NULL COMMENT '员工ID' AFTER role_id`) } catch {} - try { await pool.query(`ALTER TABLE users ADD COLUMN department VARCHAR(100) DEFAULT NULL COMMENT '所属部门' AFTER employee_id`) } catch {} - - // customers 表:新增 responsible_user_id - try { await pool.query(`ALTER TABLE customers ADD COLUMN responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID'`) } catch {} - - // contracts 表:新增 type - try { await pool.query(`ALTER TABLE contracts ADD COLUMN type VARCHAR(20) DEFAULT 'franchise' COMMENT '合同类型: franchise-加盟合同, supply-采购合同'`) } catch {} - // contracts 表:新增 responsible_user_id(用于数据范围隔离) - try { await pool.query(`ALTER TABLE contracts ADD COLUMN responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID'`) } catch {} - - // after_sales 表:新增 responsible_user_id(用于数据范围隔离) - try { await pool.query(`ALTER TABLE after_sales ADD COLUMN responsible_user_id INT DEFAULT NULL COMMENT '负责人用户ID'`) } catch {} - - // ============ 2.5 种子数据 ============ + // ============ 2.4 种子数据 ============ await seedRolesAndPermissions() await seedDefaultUsers() } diff --git a/routes/customers.js b/routes/customers.js index a13f73f..1bc6a7e 100644 --- a/routes/customers.js +++ b/routes/customers.js @@ -14,7 +14,7 @@ function pagination(query) { async function list(req, res) { try { const { page, pageSize, offset } = pagination(req.query) - const { name, phone, province, city, customer_type } = req.query + const { name, phone, province, city } = req.query let where = 'WHERE 1=1' const params = [] @@ -45,10 +45,6 @@ async function list(req, res) { where += ' AND city = ?' params.push(city) } - if (customer_type) { - where += ' AND customer_type = ?' - params.push(customer_type) - } // 查总数 const [[{ total }]] = await pool.query( @@ -109,7 +105,7 @@ async function detail(req, res) { async function create(req, res) { const { name, phone, province, city, district, - address, customer_type, email, remark, responsible_user_id, + address, email, remark, responsible_user_id, } = req.body || {} if (!name) { @@ -121,10 +117,10 @@ async function create(req, res) { const ownerId = responsible_user_id || req.user.id const [result] = await pool.query( - `INSERT INTO customers (name, phone, province, city, district, address, customer_type, email, remark, responsible_user_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO customers (name, phone, province, city, district, address, email, remark, responsible_user_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [name, phone || null, province || null, city || null, district || null, - address || null, customer_type || 'Normal', email || null, remark || null, ownerId] + address || null, email || null, remark || null, ownerId] ) const [rows] = await pool.query('SELECT * FROM customers WHERE id = ?', [result.insertId]) res.json({ code: 0, message: 'ok', data: rows[0] }) @@ -139,7 +135,7 @@ async function update(req, res) { const { id } = req.params const fields = [ 'name', 'phone', 'province', 'city', 'district', - 'address', 'customer_type', 'email', 'remark', 'responsible_user_id', + 'address', 'email', 'remark', 'responsible_user_id', ] try { diff --git a/test-all.js b/test-all.js index d54900b..9e1e7cd 100644 --- a/test-all.js +++ b/test-all.js @@ -457,7 +457,7 @@ async function main() { r = await req('POST', '/api/customers', { name: 'CRUD测试加盟商', phone: '13800007777', province: '河南', city: '郑州', - customer_type: 'VIP', email: 'test@mixue.com', + email: 'test@mixue.com', }, tokens.admin) check('admin POST /api/customers (创建)', r, 200) const custCrudId = r.body?.data?.id @@ -478,9 +478,6 @@ async function main() { r = await req('GET', '/api/customers?name=CRUD', null, tokens.admin) check('GET /api/customers?name=CRUD (搜索)', r, 200) - - r = await req('GET', '/api/customers?customer_type=VIP', null, tokens.admin) - check('GET /api/customers?customer_type=VIP (筛选)', r, 200) } r = await req('GET', '/api/customers/99999', null, tokens.admin)