后端 (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设计/模块详设/小程序设计/部署方案
59 KiB
59 KiB
宫中有喜 — 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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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 示例:
{
"conditions": [
{"field": "amount", "operator": ">=", "value": 5000, "flow_variant": "high_amount"},
{"field": "amount", "operator": "<", "value": 5000, "flow_variant": "normal"}
]
}
4.13 审批步骤表 (approval_flow_steps)
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)
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)
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)
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 逻辑(伪代码)
-- 基础查询(以客户列表为例)
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
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
// 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
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
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 中间件注册
// 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 路由示例
// 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
// 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
// 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 业务集成示例
// 签约合同时触发审批
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 按钮级权限指令
<!-- 前端使用示例 -->
<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>
// 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 在权限/角色/用户关联变更时主动清除相关缓存。
// 示例:角色权限变更时清除关联用户缓存
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 默认角色与权限映射
// 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已安全存储 - 登录失败锁定策略参数已确认(次数、时长)