# 宫中有喜 — 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` 已安全存储
- [ ] 登录失败锁定策略参数已确认（次数、时长）
