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