Files
yuezi-saas/docs/04-rbac-design.md
T
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

59 KiB
Raw Blame History

宫中有喜 — 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 '上级部门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)

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 '所属门店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)

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)

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)

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)

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 '审批角色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)

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 '门店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 逻辑(伪代码)

-- 基础查询(以客户列表为例)
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

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 转交目标用户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 业务集成示例

// 签约合同时触发审批
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)

十四、部署检查清单

  • permissionsmenus 种子数据已完整导入
  • 平台超管账号已创建(platform_super_admin 角色)
  • 各门店默认角色已初始化(Seeder 按 stores 表遍历创建)
  • Redis 已配置并可连接(用于权限缓存)
  • 操作日志队列 Worker 已启动(php artisan queue:work --queue=logs
  • 审批超时定时任务已注册(schedule:run 每分钟执行)
  • HTTPS 证书已部署,HTTP 自动跳转 HTTPS
  • operation_logs 表已配置按月归档策略
  • 敏感字段加密密钥 APP_KEY 已安全存储
  • 登录失败锁定策略参数已确认(次数、时长)