# shop-role-management Specification ## Purpose 提供店铺级角色管理能力,允许平台为代理店铺设置默认角色,该店铺下所有账号自动继承,简化 MVP 阶段的批量角色分配操作。 ## Requirements ### Requirement: 分配店铺角色 系统 SHALL 提供接口允许平台用户或店铺管理员为店铺分配角色。 **接口**: `POST /api/admin/shops/:shop_id/roles` **请求体**: ```json { "role_ids": [5] // 角色 ID 列表,传空数组表示清空所有角色 } ``` **响应体**: ```json { "code": 0, "msg": "success", "data": [ { "id": 1, "shop_id": 10, "role_id": 5, "status": 1, "created_at": "2026-02-02T10:00:00Z" } ], "timestamp": "2026-02-02T10:00:00Z" } ``` #### Scenario: 成功分配单个角色 - **WHEN** 平台用户调用 `POST /api/admin/shops/10/roles` 请求体为 `{"role_ids": [5]}` - **AND** 角色 ID 5 存在且为客户角色(RoleType=2) - **AND** 店铺 ID 10 存在 - **THEN** 系统创建店铺-角色关联记录 - **AND** 返回 HTTP 200 和关联记录 - **AND** 清理该店铺下所有账号的权限缓存 #### Scenario: 清空店铺所有角色 - **WHEN** 平台用户调用 `POST /api/admin/shops/10/roles` 请求体为 `{"role_ids": []}` - **THEN** 系统删除该店铺的所有角色关联 - **AND** 返回 HTTP 200 和空数组 - **AND** 清理该店铺下所有账号的权限缓存 #### Scenario: 替换现有角色 - **WHEN** 店铺已分配角色 ID 5 - **AND** 平台用户调用 `POST /api/admin/shops/10/roles` 请求体为 `{"role_ids": [7]}` - **THEN** 系统删除原有角色 ID 5 的关联 - **AND** 创建新的角色 ID 7 的关联 - **AND** 返回 HTTP 200 和新关联记录 #### Scenario: 角色类型校验失败 - **WHEN** 平台用户调用 `POST /api/admin/shops/10/roles` 请求体为 `{"role_ids": [3]}` - **AND** 角色 ID 3 是平台角色(RoleType=1) - **THEN** 返回 HTTP 400 错误码 `errors.CodeInvalidParam` - **AND** 错误消息为"店铺只能分配客户角色" - **AND** 不创建任何关联记录 #### Scenario: 店铺不存在 - **WHEN** 平台用户调用 `POST /api/admin/shops/999/roles` - **AND** 店铺 ID 999 不存在 - **THEN** 返回 HTTP 404 错误码 `errors.CodeNotFound` - **AND** 错误消息为"店铺不存在" #### Scenario: 权限不足 - **WHEN** 代理用户调用 `POST /api/admin/shops/20/roles` - **AND** 店铺 ID 20 不在该代理的管理范围内(不是自己店铺或下级店铺) - **THEN** 返回 HTTP 403 错误码 `errors.CodeForbidden` - **AND** 错误消息为"无权限操作该资源或资源不存在" ### Requirement: 查询店铺角色 系统 SHALL 提供接口查询店铺已分配的角色列表。 **接口**: `GET /api/admin/shops/:shop_id/roles` **响应体**: ```json { "code": 0, "msg": "success", "data": { "shop_id": 10, "roles": [ { "shop_id": 10, "role_id": 5, "role_name": "代理店长", "role_desc": "代理店铺管理员", "status": 1 } ] }, "timestamp": "2026-02-02T10:00:00Z" } ``` #### Scenario: 查询已分配角色 - **WHEN** 平台用户调用 `GET /api/admin/shops/10/roles` - **AND** 店铺 ID 10 已分配角色 ID 5 - **THEN** 返回 HTTP 200 和角色详情列表 - **AND** 包含角色名称、描述等信息 #### Scenario: 查询未分配角色的店铺 - **WHEN** 平台用户调用 `GET /api/admin/shops/10/roles` - **AND** 店铺 ID 10 未分配任何角色 - **THEN** 返回 HTTP 200 - **AND** `roles` 字段为空数组 #### Scenario: 店铺不存在 - **WHEN** 平台用户调用 `GET /api/admin/shops/999/roles` - **AND** 店铺 ID 999 不存在 - **THEN** 返回 HTTP 404 错误码 `errors.CodeNotFound` - **AND** 错误消息为"店铺不存在" #### Scenario: 权限不足 - **WHEN** 代理用户调用 `GET /api/admin/shops/20/roles` - **AND** 店铺 ID 20 不在该代理的管理范围内 - **THEN** 返回 HTTP 403 错误码 `errors.CodeForbidden` - **AND** 错误消息为"无权限操作该资源或资源不存在" ### Requirement: 删除店铺角色 系统 SHALL 提供接口删除店铺的特定角色关联。 **接口**: `DELETE /api/admin/shops/:shop_id/roles/:role_id` **响应体**: ```json { "code": 0, "msg": "success", "data": null, "timestamp": "2026-02-02T10:00:00Z" } ``` #### Scenario: 成功删除角色 - **WHEN** 平台用户调用 `DELETE /api/admin/shops/10/roles/5` - **AND** 店铺 ID 10 存在 - **AND** 店铺已分配角色 ID 5 - **THEN** 系统删除该关联记录 - **AND** 返回 HTTP 200 - **AND** 清理该店铺下所有账号的权限缓存 #### Scenario: 删除不存在的角色关联 - **WHEN** 平台用户调用 `DELETE /api/admin/shops/10/roles/5` - **AND** 店铺 ID 10 未分配角色 ID 5 - **THEN** 返回 HTTP 200(幂等操作) - **AND** 不执行任何数据库操作 #### Scenario: 店铺不存在 - **WHEN** 平台用户调用 `DELETE /api/admin/shops/999/roles/5` - **AND** 店铺 ID 999 不存在 - **THEN** 返回 HTTP 404 错误码 `errors.CodeNotFound` - **AND** 错误消息为"店铺不存在" #### Scenario: 权限不足 - **WHEN** 代理用户调用 `DELETE /api/admin/shops/20/roles/5` - **AND** 店铺 ID 20 不在该代理的管理范围内 - **THEN** 返回 HTTP 403 错误码 `errors.CodeForbidden` - **AND** 错误消息为"无权限操作该资源或资源不存在" ### Requirement: 数据库表结构 系统 SHALL 创建 `tb_shop_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` - 软删除过滤 #### Scenario: 唯一性约束 - **WHEN** 尝试为同一店铺分配同一角色两次 - **THEN** 数据库返回唯一性约束冲突错误 - **AND** 系统捕获错误并返回友好错误消息 #### Scenario: 软删除机制 - **WHEN** 删除店铺角色关联 - **THEN** 系统设置 `deleted_at` 字段为当前时间 - **AND** 后续查询自动过滤 `deleted_at IS NOT NULL` 的记录 ### Requirement: 缓存失效策略 系统 SHALL 在店铺角色变更时清理相关账号的权限缓存。 #### Scenario: 分配角色时清理缓存 - **WHEN** 为店铺 ID 10 分配角色 - **THEN** 系统查询该店铺下所有账号 ID 列表 - **AND** 遍历删除每个账号的权限缓存键 `user:permissions:{account_id}` - **AND** 下次权限检查时,账号会重新查询并继承新角色 #### Scenario: 删除角色时清理缓存 - **WHEN** 删除店铺 ID 10 的角色关联 - **THEN** 系统查询该店铺下所有账号 ID 列表 - **AND** 遍历删除每个账号的权限缓存键 - **AND** 下次权限检查时,账号将无角色(如果无账号级角色) #### Scenario: 账号有自己角色时不受影响 - **WHEN** 店铺角色变更 - **AND** 某账号有自己的账号级角色 - **THEN** 该账号的权限缓存被清理 - **AND** 下次权限检查时,继续使用账号级角色(不继承店铺角色) ### Requirement: 权限控制 店铺角色管理接口 SHALL 实施权限控制,只有有权限的用户才能操作。 **权限规则**: - 超级管理员(UserType=1):可操作所有店铺 - 平台用户(UserType=2):可操作所有店铺 - 代理用户(UserType=3):只能操作自己店铺及下级店铺 - 企业用户(UserType=4):无权限操作店铺角色 #### Scenario: 超级管理员操作任意店铺 - **WHEN** 超级管理员调用店铺角色管理接口 - **THEN** 跳过权限检查 - **AND** 允许操作任意店铺 #### Scenario: 平台用户操作任意店铺 - **WHEN** 平台用户调用店铺角色管理接口 - **THEN** 允许操作任意店铺 #### Scenario: 代理用户操作下级店铺 - **WHEN** 代理用户(shop_id=10)调用店铺角色管理接口 - **AND** 目标店铺 ID 15 是店铺 10 的下级店铺 - **THEN** 调用 `middleware.CanManageShop(ctx, 15, shopStore)` - **AND** 返回 nil(有权限) - **AND** 允许操作 #### Scenario: 代理用户操作无关店铺 - **WHEN** 代理用户(shop_id=10)调用店铺角色管理接口 - **AND** 目标店铺 ID 20 不是店铺 10 的下级店铺 - **THEN** 调用 `middleware.CanManageShop(ctx, 20, shopStore)` - **AND** 返回 error(无权限) - **AND** 拒绝操作 #### Scenario: 企业用户尝试操作店铺角色 - **WHEN** 企业用户调用店铺角色管理接口 - **THEN** 返回 HTTP 403 错误码 `errors.CodeForbidden` - **AND** 错误消息为"无权限操作该资源或资源不存在" ### Requirement: 业务规则校验 店铺角色分配 SHALL 执行业务规则校验,确保数据一致性。 #### Scenario: 角色存在性校验 - **WHEN** 分配店铺角色时指定角色 ID 列表 - **THEN** 系统查询所有角色是否存在 - **AND** 如果部分角色不存在,返回错误"部分角色不存在" - **AND** 不创建任何关联记录(原子操作) #### Scenario: 角色状态校验 - **WHEN** 分配店铺角色时指定角色 ID - **AND** 该角色的 `status` 字段为 0(禁用) - **THEN** 返回错误"角色已禁用" - **AND** 不创建关联记录 #### Scenario: 角色类型校验 - **WHEN** 分配店铺角色时指定角色 ID - **AND** 该角色的 `role_type` 字段为 1(平台角色) - **THEN** 返回错误"店铺只能分配客户角色" - **AND** 不创建关联记录 #### Scenario: 店铺存在性校验 - **WHEN** 分配店铺角色时指定店铺 ID - **AND** 该店铺不存在或已软删除 - **THEN** 返回错误"店铺不存在" - **AND** 不执行任何操作