Files
yuezi-saas/docs/04-rbac-design.md
li 11c177a432 feat: 第一阶段基础框架搭建
后端 (Laravel 10 + Sanctum):
- RBAC 四层权限系统 (users→roles→permissions→menus)
- 门店隔离中间件 (BelongsToStore Trait + StoreIsolation Middleware)
- 操作日志中间件 (自动记录写操作)
- 权限检查中间件 (CheckPermission)
- 16张数据库表迁移 (系统基础+RBAC+字典+配置)
- 11个 Eloquent Model
- Auth API (登录/登出/用户信息)
- 系统设置模块 CRUD (门店/部门/职务/用户/角色/菜单/权限/字典/日志)
- 45条 RESTful API 路由
- InitSeeder 初始数据 (超管/角色/76权限/31菜单)

前端 (Vue 3 + Element Plus + Vite):
- Axios 请求封装 + Token 注入
- Pinia 状态管理 (user + permission store)
- 动态路由 (服务端菜单→前端路由自动生成)
- 后台布局 (侧边栏+顶栏+主内容区)
- 登录页 + 仪表盘首页
- 系统设置 7 个 CRUD 页面

技术方案文档 (7卷):
- 技术总览/数据库设计/API规范/RBAC设计/模块详设/小程序设计/部署方案
2026-03-13 19:52:09 +08:00

1393 lines
59 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.
# 宫中有喜 — RBAC 权限系统设计 v1.0
## 一、设计目标
| 目标 | 说明 |
|------|------|
| 多门店数据隔离 | 每条业务数据强绑 `store_id`,中间件层自动注入过滤 |
| 四层权限模型 | User → Role → Permission → Menu,支持按钮级控制 |
| 灵活审批流 | 可视化配置多级审批,支持会签 / 或签 / 条件分支 |
| 完整审计链 | 所有写操作自动记录,满足医疗/月子行业合规要求 |
| 最小权限原则 | 默认拒绝,显式授权,禁止越权访问 |
---
## 二、RBAC 四层模型
```
┌──────────────────────────────────────────────────────────────┐
│ 权限判定流程 │
│ │
│ Request │
│ │ │
│ ▼ │
│ Auth Middleware (Sanctum Token) │
│ │ │
│ ▼ │
│ StoreIsolation Middleware │
│ │ 注入 store_id → 全局 Scope 过滤 │
│ ▼ │
│ CheckPermission Middleware │
│ │ User → user_roles → roles → role_permissions │
│ │ → permissions → 比对路由所需权限标识 │
│ ▼ │
│ Controller (业务逻辑) │
│ │ │
│ ▼ │
│ OperationLog Middleware (响应后异步写日志) │
│ │
└──────────────────────────────────────────────────────────────┘
```
### 2.1 四层关系
```
用户 (users)
│ N:M
角色 (roles)
│ N:M N:M
├──────────► 权限 (permissions)
└──────────► 菜单 (menus)
```
- **用户**:系统登录主体,含内部员工与客户
- **角色**:权限集合的逻辑分组,绑定门店
- **权限**:最小授权单元,对应 API 路由 + 操作标识
- **菜单**:前端路由 + 页面按钮,控制界面可见性
### 2.2 权限标识命名规范
```
{module}:{sub_module}:{action}
示例:
crm:customer:list # CRM - 客户列表
crm:customer:create # CRM - 新建客户
crm:customer:update # CRM - 编辑客户
crm:customer:delete # CRM - 删除客户
crm:customer:export # CRM - 导出客户
room:booking:approve # 房务 - 预定审批
care:record:create # 护理 - 新增记录
finance:bill:void # 财务 - 作废账单
system:role:assign # 系统 - 角色分配
```
### 2.3 数据权限范围(data_scope
| 值 | 含义 | SQL 过滤条件 |
|----|------|-------------|
| `all` | 全部数据 | 不追加过滤(仅平台超管) |
| `store` | 本门店全部 | `WHERE store_id = ?` |
| `department` | 本部门 | `WHERE department_id IN (?)` |
| `self` | 仅本人 | `WHERE created_by = ?` |
| `custom` | 自定义部门 | `WHERE department_id IN (配置列表)` |
---
## 三、系统角色清单
### 3.1 平台级角色
| 角色 | 代码 | data_scope | 说明 |
|------|------|-----------|------|
| 平台超级管理员 | `platform_super_admin` | `all` | 跨门店全权,不可删除 |
### 3.2 门店级角色
| 角色 | 代码 | data_scope | 典型权限 |
|------|------|-----------|----------|
| 门店管理员/店长 | `store_manager` | `store` | 本店全部功能 + 审批终审 |
| 销售/客服 | `sales` | `department` | CRM 全模块 + 签约 |
| 前台/房务 | `front_desk` | `store` | 预约、入住、退房、房态 |
| 护理护士 | `nurse` | `department` | 护理档案、记录、医嘱 |
| 膳食/厨房 | `kitchen` | `department` | 排餐、送餐、食材管理 |
| 服务技师/产康师 | `therapist` | `self` | 服务预约、产康记录 |
| 仓库/采购 | `warehouse` | `department` | 进销存全流程 |
| 财务 | `finance` | `store` | 收付款、账单、报表 |
| 人事 | `hr` | `store` | 员工档案、考勤、薪资 |
| 月嫂中心管理员 | `nanny_center_admin` | `store` | 月嫂调度、评价、合同 |
| 月嫂督导 | `nanny_supervisor` | `department` | 月嫂排班、质检 |
| 月嫂个人 | `nanny` | `self` | 个人排班、服务记录 |
### 3.3 客户角色
| 角色 | 代码 | data_scope | 说明 |
|------|------|-----------|------|
| 客户 | `client` | `self` | 微信小程序访问,仅查看本人数据 |
---
## 四、数据库表设计
### 4.1 门店表 (stores)
```sql
CREATE TABLE `stores` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '门店ID',
`name` VARCHAR(100) NOT NULL COMMENT '门店名称',
`code` VARCHAR(20) NOT NULL COMMENT '门店编码(唯一标识)',
`contact_person` VARCHAR(50) NULL COMMENT '联系人',
`contact_phone` VARCHAR(20) NULL COMMENT '联系电话',
`province` VARCHAR(50) NULL COMMENT '省',
`city` VARCHAR(50) NULL COMMENT '市',
`district` VARCHAR(50) NULL COMMENT '区',
`address` VARCHAR(255) NULL COMMENT '详细地址',
`logo` VARCHAR(500) NULL COMMENT '门店 Logo URL',
`business_license` VARCHAR(500) NULL COMMENT '营业执照图片 URL',
`license_no` VARCHAR(50) NULL COMMENT '营业执照号',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用 2=试用',
`expires_at` DATE NULL COMMENT 'SaaS 服务到期日',
`max_users` INT UNSIGNED NOT NULL DEFAULT 50 COMMENT '最大用户数',
`settings` JSON NULL COMMENT '门店个性化配置(JSON',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`created_by` BIGINT UNSIGNED NULL COMMENT '创建人',
`updated_by` BIGINT UNSIGNED NULL COMMENT '更新人',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` TIMESTAMP NULL COMMENT '软删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_status` (`status`),
KEY `idx_deleted_at` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='门店表';
```
### 4.2 部门表 (departments)
```sql
CREATE TABLE `departments` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '部门ID',
`store_id` BIGINT UNSIGNED NOT NULL COMMENT '所属门店ID',
`parent_id` BIGINT UNSIGNED NULL DEFAULT 0 COMMENT '上级部门ID0=顶级)',
`name` VARCHAR(100) NOT NULL COMMENT '部门名称',
`code` VARCHAR(50) NULL COMMENT '部门编码',
`leader_id` BIGINT UNSIGNED NULL COMMENT '部门负责人用户ID',
`phone` VARCHAR(20) NULL COMMENT '部门电话',
`email` VARCHAR(100) NULL COMMENT '部门邮箱',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用',
`created_by` BIGINT UNSIGNED NULL COMMENT '创建人',
`updated_by` BIGINT UNSIGNED NULL COMMENT '更新人',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` TIMESTAMP NULL COMMENT '软删除时间',
PRIMARY KEY (`id`),
KEY `idx_store_id` (`store_id`),
KEY `idx_parent_id` (`parent_id`),
KEY `idx_leader_id` (`leader_id`),
KEY `idx_deleted_at` (`deleted_at`),
CONSTRAINT `fk_departments_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='部门表';
```
### 4.3 职务表 (positions)
```sql
CREATE TABLE `positions` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '职务ID',
`store_id` BIGINT UNSIGNED NOT NULL COMMENT '所属门店ID',
`name` VARCHAR(100) NOT NULL COMMENT '职务名称',
`code` VARCHAR(50) NULL COMMENT '职务编码',
`level` TINYINT NOT NULL DEFAULT 1 COMMENT '职级:1=普通 2=主管 3=经理 4=总监 5=总经理',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用',
`created_by` BIGINT UNSIGNED NULL COMMENT '创建人',
`updated_by` BIGINT UNSIGNED NULL COMMENT '更新人',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` TIMESTAMP NULL COMMENT '软删除时间',
PRIMARY KEY (`id`),
KEY `idx_store_id` (`store_id`),
KEY `idx_deleted_at` (`deleted_at`),
CONSTRAINT `fk_positions_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='职务表';
```
### 4.4 用户表 (users)
```sql
CREATE TABLE `users` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`store_id` BIGINT UNSIGNED NULL COMMENT '所属门店IDNULL=平台级用户)',
`department_id` BIGINT UNSIGNED NULL COMMENT '所属部门ID',
`position_id` BIGINT UNSIGNED NULL COMMENT '职务ID',
`username` VARCHAR(50) NOT NULL COMMENT '登录账号',
`password` VARCHAR(255) NOT NULL COMMENT '密码(bcrypt',
`name` VARCHAR(50) NOT NULL COMMENT '真实姓名',
`employee_no` VARCHAR(30) NULL COMMENT '工号',
`avatar` VARCHAR(500) NULL COMMENT '头像 URL',
`gender` TINYINT NOT NULL DEFAULT 0 COMMENT '性别:0=未知 1=男 2=女',
`phone` VARCHAR(20) NULL COMMENT '手机号',
`email` VARCHAR(100) NULL COMMENT '邮箱',
`id_card` VARCHAR(20) NULL COMMENT '身份证号(加密存储)',
`user_type` TINYINT NOT NULL DEFAULT 1 COMMENT '用户类型:1=内部员工 2=客户 3=月嫂',
`wx_openid` VARCHAR(64) NULL COMMENT '微信小程序 OpenID',
`wx_unionid` VARCHAR(64) NULL COMMENT '微信 UnionID',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用 2=锁定',
`last_login_at` TIMESTAMP NULL COMMENT '最后登录时间',
`last_login_ip` VARCHAR(45) NULL COMMENT '最后登录IP',
`password_changed_at` TIMESTAMP NULL COMMENT '密码最后修改时间',
`login_fail_count` TINYINT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数',
`locked_until` TIMESTAMP NULL COMMENT '锁定截止时间',
`created_by` BIGINT UNSIGNED NULL COMMENT '创建人',
`updated_by` BIGINT UNSIGNED NULL COMMENT '更新人',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` TIMESTAMP NULL COMMENT '软删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
UNIQUE KEY `uk_phone` (`phone`),
UNIQUE KEY `uk_wx_openid` (`wx_openid`),
KEY `idx_store_id` (`store_id`),
KEY `idx_department_id` (`department_id`),
KEY `idx_position_id` (`position_id`),
KEY `idx_user_type` (`user_type`),
KEY `idx_status` (`status`),
KEY `idx_employee_no` (`employee_no`),
KEY `idx_deleted_at` (`deleted_at`),
CONSTRAINT `fk_users_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE,
CONSTRAINT `fk_users_department` FOREIGN KEY (`department_id`) REFERENCES `departments` (`id`) ON UPDATE CASCADE,
CONSTRAINT `fk_users_position` FOREIGN KEY (`position_id`) REFERENCES `positions` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
```
### 4.5 角色表 (roles)
```sql
CREATE TABLE `roles` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`store_id` BIGINT UNSIGNED NULL COMMENT '所属门店IDNULL=平台级角色)',
`name` VARCHAR(100) NOT NULL COMMENT '角色名称',
`code` VARCHAR(50) NOT NULL COMMENT '角色编码(英文标识)',
`description` VARCHAR(255) NULL COMMENT '角色描述',
`data_scope` VARCHAR(20) NOT NULL DEFAULT 'self' COMMENT '数据权限范围:all/store/department/self/custom',
`is_system` TINYINT NOT NULL DEFAULT 0 COMMENT '是否系统内置角色:0=否 1=是(不可删除)',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用',
`created_by` BIGINT UNSIGNED NULL COMMENT '创建人',
`updated_by` BIGINT UNSIGNED NULL COMMENT '更新人',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` TIMESTAMP NULL COMMENT '软删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_store_code` (`store_id`, `code`),
KEY `idx_store_id` (`store_id`),
KEY `idx_status` (`status`),
KEY `idx_deleted_at` (`deleted_at`),
CONSTRAINT `fk_roles_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';
```
### 4.6 权限表 (permissions)
```sql
CREATE TABLE `permissions` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '权限ID',
`parent_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父权限ID0=顶级模块)',
`name` VARCHAR(100) NOT NULL COMMENT '权限名称',
`code` VARCHAR(100) NOT NULL COMMENT '权限标识(module:sub:action',
`type` TINYINT NOT NULL DEFAULT 1 COMMENT '类型:1=模块 2=菜单 3=操作/按钮 4=API',
`method` VARCHAR(10) NULL COMMENT 'HTTP 方法:GET/POST/PUT/DELETE',
`path` VARCHAR(255) NULL COMMENT 'API 路径(如 /api/v1/customers',
`description` VARCHAR(255) NULL COMMENT '权限描述',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_parent_id` (`parent_id`),
KEY `idx_type` (`type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='权限表';
```
> **说明**:权限表为全局表,不绑 `store_id`。权限定义全局统一,通过 `role_permissions` 关联到角色实现门店级差异化授权。
### 4.7 菜单表 (menus)
```sql
CREATE TABLE `menus` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`parent_id` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父菜单ID0=顶级)',
`name` VARCHAR(100) NOT NULL COMMENT '菜单名称',
`code` VARCHAR(100) NOT NULL COMMENT '菜单标识(与前端路由 name 对应)',
`type` TINYINT NOT NULL DEFAULT 1 COMMENT '类型:1=目录 2=菜单 3=按钮',
`icon` VARCHAR(100) NULL COMMENT '图标 classElement Plus icon',
`path` VARCHAR(255) NULL COMMENT '前端路由路径',
`component` VARCHAR(255) NULL COMMENT '前端组件路径',
`redirect` VARCHAR(255) NULL COMMENT '重定向地址',
`permission` VARCHAR(100) NULL COMMENT '所需权限标识(关联 permissions.code',
`is_visible` TINYINT NOT NULL DEFAULT 1 COMMENT '是否在菜单栏显示:0=隐藏 1=显示',
`is_cache` TINYINT NOT NULL DEFAULT 0 COMMENT '是否缓存(keep-alive):0=否 1=是',
`is_external` TINYINT NOT NULL DEFAULT 0 COMMENT '是否外链:0=否 1=是',
`sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0=禁用 1=启用',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_parent_id` (`parent_id`),
KEY `idx_type` (`type`),
KEY `idx_sort` (`sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单表';
```
> **说明**:菜单表同样为全局表。通过 `role_menus` 实现不同角色看到不同菜单。前端登录后请求 `/api/v1/menus/user` 获取动态路由。
### 4.8 角色-权限关联表 (role_permissions)
```sql
CREATE TABLE `role_permissions` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID',
`permission_id` BIGINT UNSIGNED NOT NULL COMMENT '权限ID',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_permission` (`role_id`, `permission_id`),
KEY `idx_permission_id` (`permission_id`),
CONSTRAINT `fk_rp_role` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_rp_permission` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-权限关联表';
```
### 4.9 用户-角色关联表 (user_roles)
```sql
CREATE TABLE `user_roles` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_role` (`user_id`, `role_id`),
KEY `idx_role_id` (`role_id`),
CONSTRAINT `fk_ur_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_ur_role` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户-角色关联表';
```
### 4.10 角色-菜单关联表 (role_menus)
```sql
CREATE TABLE `role_menus` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID',
`menu_id` BIGINT UNSIGNED NOT NULL COMMENT '菜单ID',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`),
KEY `idx_menu_id` (`menu_id`),
CONSTRAINT `fk_rm_role` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_rm_menu` FOREIGN KEY (`menu_id`) REFERENCES `menus` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-菜单关联表';
```
### 4.11 角色-自定义部门关联表 (role_departments)
```sql
CREATE TABLE `role_departments` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID',
`department_id` BIGINT UNSIGNED NOT NULL COMMENT '部门ID',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_department` (`role_id`, `department_id`),
KEY `idx_department_id` (`department_id`),
CONSTRAINT `fk_rd_role` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_rd_department` FOREIGN KEY (`department_id`) REFERENCES `departments` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-自定义部门关联表(data_scope=custom时生效)';
```
### 4.12 审批流配置表 (approval_flows)
```sql
CREATE TABLE `approval_flows` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '审批流ID',
`store_id` BIGINT UNSIGNED NOT NULL COMMENT '所属门店ID',
`name` VARCHAR(100) NOT NULL COMMENT '审批流名称',
`code` VARCHAR(50) NOT NULL COMMENT '审批流编码(如 contract_sign, purchase_order',
`module` VARCHAR(50) NOT NULL COMMENT '所属模块(crm/room/finance/stock 等)',
`description` VARCHAR(255) NULL COMMENT '描述',
`trigger_condition` JSON NULL COMMENT '触发条件(金额阈值、类别等 JSON 配置)',
`is_enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用:0=否 1=是',
`version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '版本号(每次修改递增)',
`created_by` BIGINT UNSIGNED NULL COMMENT '创建人',
`updated_by` BIGINT UNSIGNED NULL COMMENT '更新人',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` TIMESTAMP NULL COMMENT '软删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_store_code` (`store_id`, `code`),
KEY `idx_module` (`module`),
KEY `idx_deleted_at` (`deleted_at`),
CONSTRAINT `fk_af_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批流配置表';
```
**trigger_condition JSON 示例**
```json
{
"conditions": [
{"field": "amount", "operator": ">=", "value": 5000, "flow_variant": "high_amount"},
{"field": "amount", "operator": "<", "value": 5000, "flow_variant": "normal"}
]
}
```
### 4.13 审批步骤表 (approval_flow_steps)
```sql
CREATE TABLE `approval_flow_steps` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '步骤ID',
`flow_id` BIGINT UNSIGNED NOT NULL COMMENT '审批流ID',
`step_number` INT UNSIGNED NOT NULL COMMENT '步骤序号(从1开始)',
`name` VARCHAR(100) NOT NULL COMMENT '步骤名称',
`approve_type` TINYINT NOT NULL DEFAULT 1 COMMENT '审批类型:1=指定用户 2=指定角色 3=部门负责人 4=发起人上级',
`approve_mode` TINYINT NOT NULL DEFAULT 1 COMMENT '审批模式:1=或签(任一通过) 2=会签(全部通过)',
`approver_ids` JSON NULL COMMENT '审批人ID列表(approve_type=1时)',
`approver_role_id` BIGINT UNSIGNED NULL COMMENT '审批角色IDapprove_type=2时)',
`timeout_hours` INT UNSIGNED NULL COMMENT '超时小时数(NULL=不限)',
`timeout_action` TINYINT NULL COMMENT '超时动作:1=自动通过 2=自动驳回 3=转交上级',
`flow_variant` VARCHAR(50) NULL COMMENT '流程分支标识(对应 trigger_condition 的 flow_variant',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_flow_variant_step` (`flow_id`, `flow_variant`, `step_number`),
KEY `idx_approver_role` (`approver_role_id`),
CONSTRAINT `fk_afs_flow` FOREIGN KEY (`flow_id`) REFERENCES `approval_flows` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批步骤表';
```
### 4.14 审批记录表 (approval_records)
```sql
CREATE TABLE `approval_records` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '审批记录ID',
`store_id` BIGINT UNSIGNED NOT NULL COMMENT '所属门店ID',
`flow_id` BIGINT UNSIGNED NOT NULL COMMENT '审批流ID',
`flow_version` INT UNSIGNED NOT NULL COMMENT '发起时的审批流版本',
`record_no` VARCHAR(30) NOT NULL COMMENT '审批单号(如 APR-20260313-0001',
`title` VARCHAR(200) NOT NULL COMMENT '审批标题',
`business_type` VARCHAR(50) NOT NULL COMMENT '业务类型(如 contract/purchase/leave',
`business_id` BIGINT UNSIGNED NOT NULL COMMENT '关联业务记录ID',
`business_data` JSON NULL COMMENT '提交时的业务数据快照(JSON',
`applicant_id` BIGINT UNSIGNED NOT NULL COMMENT '申请人ID',
`current_step` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '当前审批步骤序号',
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0=待审批 1=审批中 2=已通过 3=已驳回 4=已撤回 5=已取消',
`result_remark` TEXT NULL COMMENT '最终审批意见',
`completed_at` TIMESTAMP NULL COMMENT '审批完成时间',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_record_no` (`record_no`),
KEY `idx_store_id` (`store_id`),
KEY `idx_flow_id` (`flow_id`),
KEY `idx_business` (`business_type`, `business_id`),
KEY `idx_applicant` (`applicant_id`),
KEY `idx_status` (`status`),
KEY `idx_created_at` (`created_at`),
CONSTRAINT `fk_ar_store` FOREIGN KEY (`store_id`) REFERENCES `stores` (`id`) ON UPDATE CASCADE,
CONSTRAINT `fk_ar_flow` FOREIGN KEY (`flow_id`) REFERENCES `approval_flows` (`id`) ON UPDATE CASCADE,
CONSTRAINT `fk_ar_applicant` FOREIGN KEY (`applicant_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批记录表';
```
### 4.15 审批记录明细表 (approval_record_details)
```sql
CREATE TABLE `approval_record_details` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '明细ID',
`record_id` BIGINT UNSIGNED NOT NULL COMMENT '审批记录ID',
`step_number` INT UNSIGNED NOT NULL COMMENT '步骤序号',
`step_name` VARCHAR(100) NOT NULL COMMENT '步骤名称',
`approver_id` BIGINT UNSIGNED NOT NULL COMMENT '审批人ID',
`action` TINYINT NOT NULL DEFAULT 0 COMMENT '审批动作:0=待处理 1=通过 2=驳回 3=转交 4=自动通过(超时)',
`remark` TEXT NULL COMMENT '审批意见',
`attachments` JSON NULL COMMENT '附件列表(JSON 数组)',
`assigned_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '分配时间',
`handled_at` TIMESTAMP NULL COMMENT '处理时间',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_record_id` (`record_id`),
KEY `idx_approver_id` (`approver_id`),
KEY `idx_action` (`action`),
KEY `idx_step` (`record_id`, `step_number`),
CONSTRAINT `fk_ard_record` FOREIGN KEY (`record_id`) REFERENCES `approval_records` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_ard_approver` FOREIGN KEY (`approver_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='审批记录明细表';
```
### 4.16 操作日志表 (operation_logs)
```sql
CREATE TABLE `operation_logs` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '日志ID',
`store_id` BIGINT UNSIGNED NULL COMMENT '门店IDNULL=平台级操作)',
`user_id` BIGINT UNSIGNED NULL COMMENT '操作人IDNULL=系统自动操作)',
`user_name` VARCHAR(50) NULL COMMENT '操作人姓名(冗余,防止用户删除后无法溯源)',
`module` VARCHAR(50) NOT NULL COMMENT '功能模块(crm/room/care/finance 等)',
`action` VARCHAR(50) NOT NULL COMMENT '操作动作(create/update/delete/export/login/logout 等)',
`title` VARCHAR(200) NOT NULL COMMENT '操作描述',
`method` VARCHAR(10) NOT NULL COMMENT 'HTTP 方法',
`url` VARCHAR(500) NOT NULL COMMENT '请求 URL',
`route_name` VARCHAR(100) NULL COMMENT 'Laravel 路由名称',
`request_body` JSON NULL COMMENT '请求参数(脱敏后,排除 password 等)',
`response_code` INT NULL COMMENT 'HTTP 响应状态码',
`response_body` JSON NULL COMMENT '响应摘要(可选,仅记录关键字段)',
`diff_data` JSON NULL COMMENT '数据变更对比({field: {old, new}}',
`ip` VARCHAR(45) NOT NULL COMMENT '客户端 IP(支持 IPv6',
`user_agent` VARCHAR(500) NULL COMMENT '浏览器 User-Agent',
`duration_ms` INT UNSIGNED NULL COMMENT '请求耗时(毫秒)',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '操作结果:0=失败 1=成功',
`error_message` TEXT NULL COMMENT '失败时的错误信息',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_store_id` (`store_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_module_action` (`module`, `action`),
KEY `idx_created_at` (`created_at`),
KEY `idx_ip` (`ip`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
```
> **日志保留策略**:生产环境建议按月分表或定期归档至冷存储,保留至少 180 天热数据。
---
## 五、权限控制原则与查询逻辑
### 5.1 门店隔离三原则
| # | 原则 | 实现 |
|---|------|------|
| 1 | 所有业务表必须包含 `store_id` 字段 | Migration 检查 |
| 2 | 所有 Model 查询自动附加 `store_id` 条件 | Global Scope |
| 3 | 跨门店查询必须经过平台超管身份校验 | 中间件白名单 |
### 5.2 数据过滤 SQL 逻辑(伪代码)
```sql
-- 基础查询(以客户列表为例)
SELECT c.* FROM customers c
WHERE c.deleted_at IS NULL
-- 第一层:门店隔离(StoreIsolation 中间件自动注入)
AND c.store_id = :current_user_store_id
-- 第二层:数据权限范围(根据角色 data_scope
AND (
CASE :data_scope
WHEN 'all' THEN 1=1 -- 平台超管
WHEN 'store' THEN c.store_id = :current_user_store_id -- 本店全部(已由第一层覆盖)
WHEN 'department' THEN c.department_id = :current_user_dept_id -- 本部门
WHEN 'self' THEN c.created_by = :current_user_id -- 仅本人
WHEN 'custom' THEN c.department_id IN (:custom_dept_ids) -- 自定义部门
END
)
-- 第三层:功能权限(CheckPermission 中间件在路由层已拦截,此处无需重复)
-- 第四层:负责人绑定(业务级过滤,部分模块适用)
AND (
:require_assignee = FALSE
OR c.assignee_id = :current_user_id
)
ORDER BY c.created_at DESC;
```
### 5.3 权限判定流程图
```
接收请求
├─ 1. Auth:Token 有效? ──── 否 → 401
├─ 2. StoreIsolation
│ 用户 store_id 与请求资源 store_id 匹配?
│ ├── 不匹配且非超管 → 403
│ └── 匹配或超管 → 继续
├─ 3. CheckPermission
│ 路由标注的权限标识 是否在用户权限集中?
│ ├── 不在 → 403
│ └── 在 → 继续
├─ 4. DataScopeService/Model 层):
│ 根据角色 data_scope 自动追加查询条件
└─ 5. OperationLog(响应后):
记录本次操作到 operation_logs
```
---
## 六、Laravel 中间件实现方案
### 6.1 StoreIsolation 中间件
**文件路径**`app/Http/Middleware/StoreIsolation.php`
```php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class StoreIsolation
{
/**
* 门店隔离中间件
*
* 执行逻辑:
* 1. 从认证用户获取 store_id
* 2. 平台超管(store_id=NULL 且角色为 platform_super_admin)跳过隔离
* 3. 将 store_id 注入到 Request 和全局 Scope
* 4. 所有 Model 查询自动追加 WHERE store_id = ?
*/
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if (!$user) {
abort(401, 'Unauthenticated');
}
// 平台超管可跨门店操作
if ($user->isPlatformSuperAdmin()) {
// 如果请求中指定了 store_id(超管切换门店视角),使用请求中的值
$storeId = $request->header('X-Store-Id') ?? $request->input('store_id');
if ($storeId) {
app()->instance('current_store_id', (int) $storeId);
}
return $next($request);
}
// 普通用户必须属于某门店
if (!$user->store_id) {
abort(403, '用户未绑定门店');
}
// 注入当前门店ID到容器,供 Global Scope 使用
app()->instance('current_store_id', $user->store_id);
return $next($request);
}
}
```
**配套 Global Scope**(所有含 `store_id` 的 Model 使用 `StoreScope` Trait):
```php
<?php
// app/Traits/BelongsToStore.php
namespace App\Traits;
use Illuminate\Database\Eloquent\Builder;
trait BelongsToStore
{
protected static function bootBelongsToStore(): void
{
// 查询时自动追加 store_id 条件
static::addGlobalScope('store', function (Builder $builder) {
if (app()->bound('current_store_id')) {
$builder->where($builder->getModel()->getTable() . '.store_id', app('current_store_id'));
}
});
// 创建时自动填充 store_id
static::creating(function ($model) {
if (!$model->store_id && app()->bound('current_store_id')) {
$model->store_id = app('current_store_id');
}
});
}
}
```
### 6.2 CheckPermission 中间件
**文件路径**`app/Http/Middleware/CheckPermission.php`
```php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\Response;
class CheckPermission
{
/**
* 权限检查中间件
*
* 路由定义示例:
* Route::get('/customers', [CustomerController::class, 'index'])
* ->middleware('permission:crm:customer:list');
*
* 执行逻辑:
* 1. 从路由中间件参数提取所需权限标识
* 2. 平台超管拥有全部权限,直接放行
* 3. 从 Redis 缓存获取用户权限集(缓存 key: permissions:user:{id}
* 4. 缓存未命中则查库:user → user_roles → roles → role_permissions → permissions
* 5. 比对是否包含所需权限,不通过返回 403
*/
public function handle(Request $request, Closure $next, string ...$permissions): Response
{
$user = $request->user();
if (!$user) {
abort(401, 'Unauthenticated');
}
// 平台超管拥有全部权限
if ($user->isPlatformSuperAdmin()) {
return $next($request);
}
// 获取用户权限集(带缓存)
$userPermissions = $this->getUserPermissions($user->id);
// 检查是否拥有所需权限(任一匹配即可)
foreach ($permissions as $permission) {
if (in_array($permission, $userPermissions)) {
return $next($request);
}
}
abort(403, '没有操作权限');
}
/**
* 获取用户权限集(Redis 缓存 + 数据库回源)
* 缓存时间:30 分钟,角色/权限变更时主动清除
*/
protected function getUserPermissions(int $userId): array
{
$cacheKey = "permissions:user:{$userId}";
return Cache::remember($cacheKey, 1800, function () use ($userId) {
return \App\Models\User::find($userId)
->roles()
->where('roles.status', 1)
->with(['permissions' => fn($q) => $q->where('permissions.status', 1)])
->get()
->pluck('permissions')
->flatten()
->pluck('code')
->unique()
->values()
->toArray();
});
}
}
```
### 6.3 OperationLog 中间件
**文件路径**`app/Http/Middleware/OperationLog.php`
```php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class OperationLog
{
/**
* 操作日志中间件
*
* 执行逻辑:
* 1. 仅记录写操作(POST/PUT/PATCH/DELETE+ 指定的读操作(如导出)
* 2. 请求参数脱敏(移除 password / token / secret 等字段)
* 3. 使用队列异步写入,不阻塞主请求
* 4. 记录请求耗时、变更对比(diff_data)
*
* 路由定义示例:
* Route::post('/customers', ...)->middleware('operation.log:crm,create,新建客户');
*/
public function handle(Request $request, Closure $next, string $module = '', string $action = '', string $title = ''): Response
{
$startTime = microtime(true);
$response = $next($request);
// 仅记录写操作,GET 请求默认跳过(除非显式标注)
if ($request->isMethod('GET') && empty($module)) {
return $response;
}
// 异步写入日志(使用 Laravel Job 队列)
dispatch(new \App\Jobs\WriteOperationLog([
'store_id' => app()->bound('current_store_id') ? app('current_store_id') : null,
'user_id' => $request->user()?->id,
'user_name' => $request->user()?->name,
'module' => $module ?: $this->guessModule($request),
'action' => $action ?: strtolower($request->method()),
'title' => $title ?: $this->guessTitle($request),
'method' => $request->method(),
'url' => $request->fullUrl(),
'route_name' => $request->route()?->getName(),
'request_body' => $this->sanitizeParams($request->all()),
'response_code' => $response->getStatusCode(),
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'duration_ms' => (int) ((microtime(true) - $startTime) * 1000),
'status' => $response->getStatusCode() < 400 ? 1 : 0,
]));
return $response;
}
/**
* 参数脱敏:移除敏感字段
*/
protected function sanitizeParams(array $params): array
{
$sensitiveKeys = ['password', 'password_confirmation', 'token', 'secret', 'id_card', 'credit_card'];
foreach ($sensitiveKeys as $key) {
if (isset($params[$key])) {
$params[$key] = '[REDACTED]';
}
}
return $params;
}
protected function guessModule(Request $request): string
{
// 从路由路径推断模块:/api/v1/crm/customers → crm
$segments = $request->segments();
return $segments[2] ?? 'system';
}
protected function guessTitle(Request $request): string
{
$method = $request->method();
return match ($method) {
'POST' => '新建记录',
'PUT', 'PATCH' => '更新记录',
'DELETE' => '删除记录',
default => '操作',
};
}
}
```
### 6.4 中间件注册
```php
// bootstrap/app.php (Laravel 10/11)
// 或 app/Http/Kernel.php
// 路由中间件别名
protected $middlewareAliases = [
// ...
'store.isolation' => \App\Http\Middleware\StoreIsolation::class,
'permission' => \App\Http\Middleware\CheckPermission::class,
'operation.log' => \App\Http\Middleware\OperationLog::class,
];
// 中间件组
protected $middlewareGroups = [
'admin' => [
'auth:sanctum',
'store.isolation',
'operation.log',
],
];
```
### 6.5 路由示例
```php
// routes/api.php
Route::prefix('v1')->middleware('admin')->group(function () {
// CRM 模块
Route::prefix('crm')->group(function () {
Route::get('/customers', [CustomerController::class, 'index'])
->middleware('permission:crm:customer:list')
->middleware('operation.log:crm,list,查看客户列表');
Route::post('/customers', [CustomerController::class, 'store'])
->middleware('permission:crm:customer:create')
->middleware('operation.log:crm,create,新建客户');
Route::put('/customers/{id}', [CustomerController::class, 'update'])
->middleware('permission:crm:customer:update')
->middleware('operation.log:crm,update,更新客户');
Route::delete('/customers/{id}', [CustomerController::class, 'destroy'])
->middleware('permission:crm:customer:delete')
->middleware('operation.log:crm,delete,删除客户');
Route::post('/customers/export', [CustomerController::class, 'export'])
->middleware('permission:crm:customer:export')
->middleware('operation.log:crm,export,导出客户');
});
// 系统管理
Route::prefix('system')->group(function () {
Route::apiResource('roles', RoleController::class)
->middleware('permission:system:role:list,system:role:create,system:role:update,system:role:delete');
Route::post('/roles/{id}/permissions', [RoleController::class, 'assignPermissions'])
->middleware('permission:system:role:assign');
Route::get('/menus/user', [MenuController::class, 'userMenus']); // 获取当前用户菜单(无需额外权限)
});
});
```
---
## 七、DataScope 查询 Trait
```php
<?php
// app/Traits/DataScopeTrait.php
namespace App\Traits;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
trait DataScopeTrait
{
/**
* 根据当前用户角色的 data_scope 自动追加查询条件
*
* 使用方式:Controller 中调用
* Customer::query()->dataScope()->paginate();
*/
public function scopeDataScope(Builder $query): Builder
{
$user = Auth::user();
if (!$user || $user->isPlatformSuperAdmin()) {
return $query;
}
// 获取用户所有角色中最宽的 data_scope
$dataScope = $user->getWidestDataScope();
return match ($dataScope) {
'all' => $query,
'store' => $query, // StoreIsolation 已处理
'department' => $query->where($this->getTable() . '.department_id', $user->department_id),
'self' => $query->where($this->getTable() . '.created_by', $user->id),
'custom' => $query->whereIn(
$this->getTable() . '.department_id',
$user->getCustomDepartmentIds()
),
default => $query->where($this->getTable() . '.created_by', $user->id), // 兜底:仅本人
};
}
}
```
---
## 八、审批流引擎设计
### 8.1 审批流程图
```
发起申请
├─ 1. 匹配审批流:按 module + code + trigger_condition 定位
├─ 2. 创建 approval_records(状态=0待审批)
├─ 3. 生成 approval_record_details(按步骤创建待审批明细)
├─ 4. 通知审批人(站内消息 + 微信模板消息)
├─ 5. 审批人操作:
│ ├── 通过 → 检查当前步骤是否全部完成
│ │ ├── 或签模式:任一通过即进入下一步
│ │ └── 会签模式:全部通过才进入下一步
│ ├── 驳回 → 审批结束,状态=3已驳回
│ └── 转交 → 更新审批人,重新通知
├─ 6. 进入下一步 → 重复步骤 4-5
└─ 7. 最后一步通过 → 审批结束,状态=2已通过
└── 触发业务回调(如:合同生效、采购单下发)
```
### 8.2 审批服务核心方法
```php
<?php
// app/Services/ApprovalService.php
namespace App\Services;
class ApprovalService
{
/**
* 发起审批
*
* @param string $flowCode 审批流编码
* @param int $businessId 业务记录ID
* @param string $businessType 业务类型
* @param string $title 审批标题
* @param array $businessData 业务数据快照
*/
public function submit(string $flowCode, int $businessId, string $businessType, string $title, array $businessData): ApprovalRecord;
/**
* 审批操作(通过/驳回/转交)
*
* @param int $recordId 审批记录ID
* @param int $action 动作:1=通过 2=驳回 3=转交
* @param string $remark 审批意见
* @param int|null $transferTo 转交目标用户IDaction=3时必填)
*/
public function handle(int $recordId, int $action, string $remark = '', ?int $transferTo = null): void;
/**
* 撤回申请(仅申请人可操作,且仅在第一步未审批时可撤回)
*/
public function withdraw(int $recordId): void;
/**
* 获取用户待审批列表
*/
public function getPendingList(int $userId, int $perPage = 15): LengthAwarePaginator;
/**
* 超时自动处理(定时任务调用)
*/
public function handleTimeout(): void;
}
```
### 8.3 业务集成示例
```php
// 签约合同时触发审批
class ContractService
{
public function create(array $data): Contract
{
$contract = Contract::create($data);
// 金额 >= 5000 需要审批
if ($contract->amount >= 5000) {
app(ApprovalService::class)->submit(
flowCode: 'contract_sign',
businessId: $contract->id,
businessType: 'contract',
title: "合同审批:{$contract->customer_name} ¥{$contract->amount}",
businessData: $contract->toArray()
);
$contract->update(['status' => Contract::STATUS_PENDING_APPROVAL]);
}
return $contract;
}
}
```
---
## 九、前端动态路由与按钮权限
### 9.1 登录后菜单获取
```
前端登录成功
├─ 1. POST /api/v1/auth/login → 获取 Token + 用户基础信息
├─ 2. GET /api/v1/menus/user → 获取用户可访问的菜单树
│ 后端查询链:user → user_roles → roles → role_menus → menus
│ 返回:树形菜单 JSON(含 path、component、permission
├─ 3. 前端 vue-router 动态注册路由
│ router.addRoute(menuToRoute(menuItem))
└─ 4. GET /api/v1/permissions/user → 获取用户权限标识列表
前端存入 Pinia Store,用于按钮级 v-permission 指令
```
### 9.2 按钮级权限指令
```vue
<!-- 前端使用示例 -->
<template>
<el-button v-permission="'crm:customer:create'" @click="handleAdd">
新建客户
</el-button>
<el-button v-permission="'crm:customer:export'" @click="handleExport">
导出
</el-button>
<el-button v-permission="'crm:customer:delete'" type="danger" @click="handleDelete">
删除
</el-button>
</template>
```
```javascript
// src/directives/permission.js
export default {
mounted(el, binding) {
const userPermissions = usePermissionStore().permissions; // string[]
const required = binding.value;
if (!userPermissions.includes(required)) {
el.parentNode?.removeChild(el);
}
},
};
```
---
## 十、权限缓存策略
| 缓存 Key | 内容 | TTL | 清除时机 |
|-----------|------|-----|----------|
| `permissions:user:{id}` | 用户权限标识列表 | 30 min | 角色变更 / 权限变更 / 用户角色变更 |
| `menus:user:{id}` | 用户菜单树 | 30 min | 菜单变更 / 角色菜单变更 |
| `roles:store:{store_id}` | 门店角色列表 | 60 min | 角色增删改 |
| `data_scope:user:{id}` | 用户数据权限范围 | 30 min | 角色变更 |
**缓存清除方式**:通过 Laravel Event + Listener 在权限/角色/用户关联变更时主动清除相关缓存。
```php
// 示例:角色权限变更时清除关联用户缓存
class RolePermissionChanged
{
public function handle(RolePermissionUpdated $event): void
{
$roleId = $event->roleId;
$userIds = UserRole::where('role_id', $roleId)->pluck('user_id');
foreach ($userIds as $userId) {
Cache::forget("permissions:user:{$userId}");
Cache::forget("menus:user:{$userId}");
Cache::forget("data_scope:user:{$userId}");
}
}
}
```
---
## 十一、安全加固措施
### 11.1 登录安全
| 策略 | 实现 |
|------|------|
| 密码强度 | 最低 8 位,含大小写字母 + 数字 |
| 登录失败锁定 | 连续 5 次失败锁定 30 分钟 |
| 密码有效期 | 90 天强制修改(可配置) |
| 敏感操作二次验证 | 删除数据、修改权限需输入密码或短信验证码 |
### 11.2 API 安全
| 策略 | 实现 |
|------|------|
| Token 有效期 | Sanctum Token 默认 8 小时,可配置 |
| 并发登录限制 | 同一账号仅保留最新 Token(可配置多端登录) |
| 接口限流 | 全局 60 次/分钟,敏感接口 10 次/分钟(throttle 中间件) |
| 传输加密 | 全站 HTTPS + HSTS |
### 11.3 数据安全
| 策略 | 实现 |
|------|------|
| 敏感字段加密 | 身份证号、银行卡号使用 AES-256-CBC 加密存储 |
| SQL 注入防护 | Eloquent ORM 参数绑定 + 禁止 `DB::raw()` 裸拼接 |
| 越权防护 | Model 层 Global Scope + 中间件双重检查 |
| 日志脱敏 | 请求日志自动过滤 password / token / secret 字段 |
---
## 十二、种子数据(Seeder
系统初始化时需要预置以下数据:
### 12.1 默认权限树(部分示例)
```
system # 系统管理
├── system:store # 门店管理
│ ├── system:store:list
│ ├── system:store:create
│ ├── system:store:update
│ └── system:store:delete
├── system:department # 部门管理
├── system:position # 职务管理
├── system:user # 用户管理
│ ├── system:user:list
│ ├── system:user:create
│ ├── system:user:update
│ ├── system:user:delete
│ ├── system:user:reset_password
│ └── system:user:assign_role
├── system:role # 角色管理
│ ├── system:role:list
│ ├── system:role:create
│ ├── system:role:update
│ ├── system:role:delete
│ └── system:role:assign
├── system:menu # 菜单管理
├── system:log # 日志管理
└── system:approval_flow # 审批流管理
crm # CRM 客户管理
├── crm:lead # 线索管理
├── crm:customer # 客户管理
├── crm:contract # 签约管理
├── crm:followup # 跟进记录
└── crm:questionnaire # 问卷调查
room # 房务管理
├── room:room # 房间管理
├── room:booking # 预定管理
├── room:checkin # 入住管理
└── room:housekeeping # 保洁管理
care # 护理管理
├── care:archive # 护理档案
├── care:plan # 护理计划
├── care:record # 护理记录
├── care:alert # 异常告警
└── care:indicator # 健康指标
meal # 月子餐管理
├── meal:menu_plan # 排餐计划
├── meal:delivery # 送餐管理
├── meal:ingredient # 食材管理
└── meal:review # 餐食评价
service # 服务与产康
├── service:project # 服务项目
├── service:booking # 服务预约
├── service:record # 服务记录
└── service:package # 套餐管理
nanny # 月嫂管理
├── nanny:profile # 月嫂档案
├── nanny:schedule # 排班调度
├── nanny:evaluation # 质检评价
├── nanny:contract # 月嫂合同
└── nanny:settlement # 结算管理
stock # 进销存
├── stock:product # 商品管理
├── stock:inventory # 库存管理
├── stock:purchase # 采购管理
├── stock:inbound # 入库管理
└── stock:outbound # 出库管理
finance # 财务管理
├── finance:bill # 账单管理
├── finance:payment # 收付款
├── finance:invoice # 发票管理
├── finance:report # 财务报表
└── finance:settlement # 结算管理
hr # 人事管理
├── hr:employee # 员工档案
├── hr:attendance # 考勤管理
├── hr:salary # 薪资管理
└── hr:performance # 绩效管理
report # 统计报表
├── report:dashboard # 数据看板
├── report:business # 业务报表
└── report:export # 报表导出
```
### 12.2 默认角色与权限映射
```php
// database/seeders/RolePermissionSeeder.php
$rolePermissionMap = [
'store_manager' => ['*'], // 本门店全部权限
'sales' => ['crm:*', 'room:booking:list', 'report:dashboard:list'],
'front_desk' => ['room:*', 'crm:customer:list', 'crm:customer:update'],
'nurse' => ['care:*', 'meal:delivery:list'],
'kitchen' => ['meal:*', 'stock:ingredient:*'],
'therapist' => ['service:*'],
'warehouse' => ['stock:*'],
'finance' => ['finance:*', 'report:*'],
'hr' => ['hr:*', 'system:user:list', 'system:department:list'],
'nanny_center_admin' => ['nanny:*'],
'nanny_supervisor' => ['nanny:schedule:*', 'nanny:evaluation:*'],
'nanny' => ['nanny:profile:self', 'nanny:schedule:self'],
'client' => [], // 客户端走独立路由,不经后台权限体系
];
```
---
## 十三、ER 关系总览
```
stores ─────────────────────────────────────────────────┐
│ 1:N │
├── departments (parent_id 自引用形成树) │
│ │ 1:N │
│ └── users │
│ │ N:M │
│ └── user_roles ── roles │
│ │ N:M │ N:M │
│ ├── role_permissions │
│ │ └── permissions │
│ ├── role_menus │
│ │ └── menus │
│ └── role_departments │
│ └── departments │
│ │
├── positions │
│ │
├── approval_flows │
│ │ 1:N │
│ └── approval_flow_steps │
│ │
├── approval_records │
│ │ 1:N │
│ └── approval_record_details │
│ │
└── operation_logs │
permissions (全局表,无 store_id) ────────────────────┘
menus (全局表,无 store_id)
```
---
## 十四、部署检查清单
- [ ] `permissions``menus` 种子数据已完整导入
- [ ] 平台超管账号已创建(`platform_super_admin` 角色)
- [ ] 各门店默认角色已初始化(Seeder 按 `stores` 表遍历创建)
- [ ] Redis 已配置并可连接(用于权限缓存)
- [ ] 操作日志队列 Worker 已启动(`php artisan queue:work --queue=logs`
- [ ] 审批超时定时任务已注册(`schedule:run` 每分钟执行)
- [ ] HTTPS 证书已部署,HTTP 自动跳转 HTTPS
- [ ] `operation_logs` 表已配置按月归档策略
- [ ] 敏感字段加密密钥 `APP_KEY` 已安全存储
- [ ] 登录失败锁定策略参数已确认(次数、时长)