# 店铺级角色继承功能设计 ## Context ### 当前系统状态 **RBAC 架构**: - 系统使用标准 RBAC(Role-Based Access Control)模型 - 角色分配粒度:账号级别(`tb_account_role` 表维护账号-角色关联) - 权限检查流程:`Account → AccountRole → Role → RolePermission → Permission` - 权限缓存:Redis 缓存用户权限 30 分钟 **用户类型**: 1. 超级管理员(UserType=1):跳过权限检查 2. 平台用户(UserType=2):账号级角色,可分配多个平台角色 3. 代理账号(UserType=3):账号级角色,可分配 1 个客户角色,归属于店铺(shop_id) 4. 企业账号(UserType=4):账号级角色,可分配 1 个客户角色,归属于企业(enterprise_id) 5. 个人客户(UserType=5):无角色系统 **店铺层级结构**: - 支持最多 7 级代理层级(通过 `tb_shop.parent_id` 维护) - 一个店铺可有多个代理账号(员工) - 店铺层级用于数据权限过滤(GORM Callback 自动注入 `WHERE shop_id IN (...)` 条件) ### 问题 在 MVP 阶段,一个店铺内的所有账号权限通常一致(如 10 个员工都是"代理店长"角色)。平台需要为每个账号逐一分配角色,操作繁琐且容易出错。 ### 约束 - 必须保持向后兼容,不能破坏现有账号级角色功能 - 仅适用于代理账号(UserType=3),其他用户类型保持现状 - 必须遵循项目架构分层:Handler → Service → Store → Model - 禁止使用外键约束和 GORM 关联关系 ## Goals / Non-Goals ### Goals 1. **简化 MVP 阶段操作**:平台可在店铺层面设置默认角色,店铺内所有账号自动继承 2. **支持未来扩展**:保留账号级角色覆盖能力,特殊账号可单独设置角色 3. **完全向后兼容**:现有账号级角色功能不受影响,不设置店铺角色的店铺行为保持一致 4. **清晰的继承规则**:账号级角色优先,无则继承店铺级角色 ### Non-Goals 1. **不支持企业级角色继承**:企业账号保持账号级角色(一企业一账号,暂无批量需求) 2. **不支持平台级角色继承**:平台账号数量少且权限差异大,不适合继承 3. **不支持多角色继承**:店铺只能设置单个角色(代理账号最大角色数为 1) 4. **不支持角色叠加**:账号角色和店铺角色二选一,不取并集 ## Decisions ### 决策 1:角色继承规则(默认继承 + 账号级覆盖) **选择**:账号级角色优先,无则继承店铺级角色 **替代方案考虑**: | 方案 | 优点 | 缺点 | 决策 | |------|------|------|------| | A. 强制继承 | 最简单,MVP 体验最好 | 未来扩展需要数据迁移 | ❌ 不选 | | B. 默认继承 + 覆盖 | 简单且灵活,无缝升级 | 逻辑稍复杂(需判断优先级) | ✅ 选择 | | C. 角色叠加(并集) | 最灵活 | 难理解,容易造成权限混乱 | ❌ 不选 | **理由**: - MVP 阶段:不设置账号角色,自动继承店铺 → 达到简化目标 - 未来扩展:特殊账号(如财务)可单独设置角色 → 覆盖店铺默认 - 优先级明确:账号角色 > 店铺角色,易于理解和调试 **实现逻辑**: ```go func GetRoleIDsForAccount(accountID) []uint { // 1. 查询账号级角色 accountRoles := GetRoleIDsByAccountID(accountID) if len(accountRoles) > 0 { return accountRoles // 有账号角色,不继承 } // 2. 查询账号所属店铺 account := GetAccountByID(accountID) if account.UserType != UserTypeAgent || account.ShopID == nil { return [] // 非代理账号或无店铺,无继承 } // 3. 查询店铺级角色(继承) shopRoles := GetRoleIDsByShopID(account.ShopID) return shopRoles } ``` ### 决策 2:用户类型范围(仅代理账号) **选择**:仅对代理账号(UserType=3)启用店铺级角色继承 **理由**: - **代理账号**:有批量需求(一个店铺多个员工) - **平台账号**:数量少(<10 个),权限差异大,不适合继承 - **企业账号**:一企业一账号,暂无批量需求 - **超级管理员**:跳过权限检查,无角色 - **个人客户**:无角色系统 **影响**:角色解析逻辑中需要判断 `UserType == 3` 才执行店铺角色查询。 ### 决策 3:数据库设计(新增 tb_shop_role 表) **选择**:新增 `tb_shop_role` 表,保留 `tb_account_role` 表 **表结构**: ```sql CREATE TABLE tb_shop_role ( id SERIAL PRIMARY KEY, shop_id INT NOT NULL, role_id INT NOT NULL, status INT NOT NULL DEFAULT 1, -- 0=禁用 1=启用 creator INT NOT NULL, updater INT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, UNIQUE (shop_id, role_id) WHERE deleted_at IS NULL ); ``` **索引设计**: - `idx_shop_role_shop_id`:查询店铺角色(高频) - `idx_shop_role_role_id`:查询角色被哪些店铺使用(低频) - `idx_shop_role_deleted_at`:软删除过滤 **理由**: - 保留 `tb_account_role`:向后兼容,不影响现有功能 - 新增 `tb_shop_role`:明确语义,避免表字段冗余 - 禁止外键约束:遵循项目规范,关联在代码层维护 ### 决策 4:角色类型校验(只能分配客户角色) **选择**:店铺只能分配客户角色(RoleType=2) **校验逻辑**: ```go for _, role := range roles { if role.RoleType != constants.RoleTypeCustomer { return errors.New("店铺只能分配客户角色") } } ``` **理由**: - 代理账号只能分配客户角色(RoleType=2) - 平台角色(RoleType=1)只能分配给平台用户 - 防止配置错误导致权限混乱 ### 决策 5:缓存失效策略(清理店铺下所有账号缓存) **选择**:店铺角色修改时,清理该店铺下所有账号的权限缓存 **实现**: ```go func (s *ShopRoleStore) clearShopRoleCache(ctx context.Context, shopID uint) { // 查询该店铺下所有账号 var accountIDs []uint s.db.Model(&Account{}).Where("shop_id = ?", shopID).Pluck("id", &accountIDs) // 逐个清理权限缓存 for _, accountID := range accountIDs { cacheKey := constants.RedisUserPermissionsKey(accountID) s.redisClient.Del(ctx, cacheKey) } } ``` **理由**: - 店铺角色修改后,继承该角色的账号权限立即生效 - 有账号级角色的账号不受影响(因为优先级更高) - 下次权限检查时,自动使用新的继承逻辑重建缓存 ### 决策 6:API 设计(RESTful 风格) **新增接口**: - `POST /api/admin/shops/:shop_id/roles` - 分配店铺角色 - `GET /api/admin/shops/:shop_id/roles` - 查询店铺角色 - `DELETE /api/admin/shops/:shop_id/roles/:role_id` - 删除店铺角色 **请求体设计**: ```json // POST /api/admin/shops/:shop_id/roles { "role_ids": [5] // 传空数组 = 清空所有角色 } ``` **响应体设计**: ```json // GET /api/admin/shops/:shop_id/roles { "code": 0, "msg": "success", "data": { "shop_id": 10, "roles": [ { "shop_id": 10, "role_id": 5, "role_name": "代理店长", "role_desc": "代理店铺管理员", "status": 1 } ] } } ``` **权限检查**: - 使用现有的 `middleware.CanManageShop()` 验证权限 - 平台用户和管理该店铺的代理才能操作 ### 决策 7:依赖注入(通过结构体字段) **Service 层依赖**: ```go // internal/service/shop/service.go type Service struct { shopStore *postgres.ShopStore shopRoleStore *postgres.ShopRoleStore // 新增 roleStore *postgres.RoleStore accountStore *postgres.AccountStore } // internal/service/permission/service.go type Service struct { permissionStore *postgres.PermissionStore accountRoleStore *postgres.AccountRoleStore rolePermStore *postgres.RolePermissionStore accountService *account.Service // 新增:用于调用角色解析 redisClient *redis.Client } ``` **Bootstrap 注册**: ```go // internal/bootstrap/stores.go stores := &Stores{ // ... 现有 stores ShopRole: postgres.NewShopRoleStore(deps.DB, deps.Redis), // 新增 } // internal/bootstrap/handlers.go handlers := &Handlers{ // ... 现有 handlers ShopRole: admin.NewShopRoleHandler(services.Shop), // 新增 } ``` ## Risks / Trade-offs ### 风险 1:角色解析增加数据库查询 **风险**:每次权限检查需要额外查询店铺角色(如果账号无角色) **缓解措施**: - 权限结果在 Redis 缓存 30 分钟,大部分请求不会触发查询 - 店铺角色修改频率极低,缓存命中率高 - 查询有索引支持(`idx_shop_role_shop_id`),查询速度快(< 5ms) **性能影响评估**: - 最坏情况:首次权限检查增加 1 次查询(~ 5ms) - 后续请求:从缓存读取(~ 0.1ms) - 预计对 API P95 响应时间影响 < 5ms,满足性能要求 ### 风险 2:缓存失效可能影响多个账号 **风险**:店铺角色修改时,需清理该店铺下所有账号缓存,可能导致短暂性能下降 **缓解措施**: - 缓存清理是异步操作,不阻塞主流程 - 店铺角色修改是低频操作(平均每天 < 10 次) - 缓存重建是懒加载(下次请求时才重建),不会集中请求数据库 **最坏情况**:店铺有 100 个账号,角色修改后,下次 100 个账号同时请求 → 产生 100 次查询。但这种情况极少,且 PostgreSQL 可承受(连接池默认 100)。 ### 风险 3:继承规则理解偏差 **风险**:用户可能不理解"账号角色优先"规则,误以为店铺角色修改会影响所有账号 **缓解措施**: - UI 层明确标识继承状态("继承自店铺" vs "账号单独设置") - 店铺角色设置页面显示"影响范围:10 个账号,1 个有单独设置不受影响" - API 文档和操作指南明确说明继承规则 ### Trade-off 1:灵活性 vs 复杂度 **Trade-off**:选择"默认继承 + 覆盖"模式增加了逻辑复杂度 **权衡**: - 增加的复杂度:角色解析逻辑从 1 次查询变为最多 2 次查询(先查账号,再查店铺) - 获得的灵活性:MVP 简化 + 未来无缝扩展,无需数据迁移 - 结论:复杂度增加有限(~20 行代码),灵活性收益显著,权衡合理 ### Trade-off 2:用户类型限制 vs 通用性 **Trade-off**:仅支持代理账号,不支持企业和平台账号 **权衡**: - 限制原因:企业账号暂无批量需求(一企业一账号),平台账号不适合继承 - 未来扩展:如果企业需要多账号,可复制同样逻辑创建 `tb_enterprise_role` 表 - 结论:优先解决当前痛点(代理店铺批量分配),避免过度设计 ## Migration Plan ### 部署步骤 1. **数据库迁移**(无需停机): ```bash # 执行迁移 migrate -path migrations -database "postgres://..." up ``` - 创建 `tb_shop_role` 表 - 不影响现有数据和功能 2. **代码部署**(滚动更新): - 部署新版本 API 服务 - 新增接口向后兼容,不影响现有功能 3. **验证**: - 调用 `POST /api/admin/shops/:id/roles` 设置店铺角色 - 验证该店铺下账号权限生效 - 验证有账号角色的账号不受影响 ### 回滚策略 **如需回滚**: 1. 回滚代码到旧版本 2. 保留 `tb_shop_role` 表(不删除,避免数据丢失) 3. 清理所有权限缓存:`redis-cli KEYS "user:permissions:*" | xargs redis-cli DEL` 4. 旧版本代码忽略 `tb_shop_role` 表,继续使用 `tb_account_role` **数据一致性**: - 回滚不影响 `tb_account_role` 数据 - `tb_shop_role` 数据保留,重新部署新版本后继续生效 ## Open Questions ### Q1: 是否需要支持多角色继承? **当前设计**:店铺只能设置单个角色(代理账号最大角色数为 1) **未来考虑**:如果代理账号需要支持多角色(如"代理店长" + "销售专员"),需要: - 修改 `constants.GetMaxRolesForUserType()` 返回值 - 修改 `AssignRolesToShop()` 支持多角色分配 - 修改角色解析逻辑支持多角色继承 **决策时机**:需求明确后再调整(暂不实现) ### Q2: 是否需要记录角色继承历史? **当前设计**:不记录继承历史,只记录当前状态 **未来考虑**:如果需要审计"某账号在某时间段继承了哪个店铺角色",需要: - 修改操作审计日志,记录角色继承关系变更 - 修改权限检查日志,记录使用的是账号角色还是店铺角色 **决策时机**:审计需求明确后再实现(暂不实现) ### Q3: 账号转移店铺后,角色如何处理? **当前设计**:账号转移店铺后,自动继承新店铺角色(如果无账号级角色) **替代方案**:账号转移店铺时,是否应该清除账号级角色? **建议**:保持现状(账号角色不跟随店铺),理由: - 账号角色是独立设置的,转移店铺不应影响 - 如果需要重新继承新店铺角色,可手动删除账号角色 **决策时机**:观察实际使用情况后决定(暂不修改)