后端 (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设计/模块详设/小程序设计/部署方案
1393 lines
59 KiB
Markdown
1393 lines
59 KiB
Markdown
# 宫中有喜 — 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 '上级部门ID(0=顶级)',
|
||
`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 '所属门店ID(NULL=平台级用户)',
|
||
`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 '所属门店ID(NULL=平台级角色)',
|
||
`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 '父权限ID(0=顶级模块)',
|
||
`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 '父菜单ID(0=顶级)',
|
||
`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 '图标 class(Element 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 '审批角色ID(approve_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 '门店ID(NULL=平台级操作)',
|
||
`user_id` BIGINT UNSIGNED NULL COMMENT '操作人ID(NULL=系统自动操作)',
|
||
`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. DataScope(Service/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 转交目标用户ID(action=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` 已安全存储
|
||
- [ ] 登录失败锁定策略参数已确认(次数、时长)
|