# 经理中心资源赠送架构 本文定义后台资源列表的“赠送权限”开关、H5 经理中心资源赠送、以及可复用用户查询接口的服务边界。资源 catalog、用户资源权益和赠送事实仍按 [Resource And Resource Group Architecture](./资源组与礼物架构.md) 归属 `wallet-service`;经理、Agency、BD 等业务身份仍按 [Host, Agency And BD App Architecture](./主播公会BD架构.md) 归属 `user-service`。 ## Goals - 后台资源列表增加“赠送权限” switch,新建资源默认打开;关闭后资源仍可存在、可用于其他业务,但不能出现在经理中心赠送列表,也不能被经理中心赠送。 - `server/admin` 和后台前端支持读取、创建、编辑和切换该字段,但不成为资源配置事实 owner。 - H5 经理中心提供资源赠送列表、赠送给用户接口和通用用户查询入口。 - 经理身份必须由服务端校验,客户端不能自报 `is_manager`。 - 用户查询接口按业务场景复用,首个场景是经理中心赠送;后续代理中心、申请成为主播、BD 中心可以复用同一个 user-service 查询能力,但必须传入明确 scene 并执行各自权限和字段裁剪。 ## Non-Goals - 不新增独立 `manager-service` 或 `resource-service`。 - 不把用户资源余额、背包、赠送记录写入 `server/admin` 数据库。 - 不允许 H5 客户端提交资源价格、金币数量、权益有效期或赠送策略。 - 不支持经理中心赠送资源组;资源组仍只允许后台或明确的系统命令赠送。 - 不在 room-service 中校验经理身份或资源赠送权限。 - 不把通用用户查询做成无权限全局搜索;所有查询必须带 scene,且返回字段按 scene 收敛。 ## Service Ownership | Service | Owns | Does Not Own | | --- | --- | --- | | `wallet-service` | `resources.manager_grant_enabled`、资源赠送白名单判断、资源赠送事务、钱包入账、用户资源权益、resource outbox | 用户是否经理、用户资料搜索、后台账号权限 | | `user-service` | 用户主数据、短号解析、通用用户查询、经理/Agency/BD/主播/币商等业务身份能力判定 | 资源 catalog、钱包余额、用户资源权益 | | `gateway-service` | H5 HTTP `/api/v1` envelope、App 鉴权、scene 参数、调用 user-service 校验能力和目标用户、调用 wallet-service 赠送命令 | 资源事实、用户身份事实、钱包/背包落账 | | `server/admin` | 后台鉴权、资源列表/编辑表单、操作审计、调用 wallet-service 管理 RPC | 资源事实、H5 经理身份、用户资源背包 | | H5 经理中心 | 展示资源列表、选择目标用户、提交赠送命令 | 权限判断、目标用户真实性、赠送数量和有效期计算 | ## Core Concepts | Concept | Meaning | | --- | --- | | `manager_grant_enabled` | 资源是否允许由经理中心赠送。它不是后台能否赠送的开关;后台仍使用现有 `grantable` 和后台权限控制。 | | `manager_center` | App H5 业务入口,由 gateway 鉴权后进入。首版经理身份映射为 active Agency owner,后续如果引入 Agency manager 表,只扩展 user-service 能力判定。 | | `business user lookup` | 通用用户查询能力。调用方必须传 `scene`,user-service 按 scene 校验 actor 能力并裁剪返回字段。 | | `manager resource grant` | 经理中心发起的单资源赠送命令。落库仍是 `resource_grants`,`grant_source=manager_center`,`operator_user_id=actor_user_id`。 | ## Admin Resource Switch `resources` 增加字段: ```sql ALTER TABLE resources ADD COLUMN manager_grant_enabled BOOLEAN NOT NULL DEFAULT TRUE, ADD KEY idx_resources_manager_grant (app_code, manager_grant_enabled, status, grantable, sort_order); ``` 字段语义: - 默认值为 `TRUE`,满足“后台资源列表赠送权限 switch 默认打开”。 - 关闭 switch 只影响经理中心赠送列表和经理中心赠送命令。 - `status=disabled` 的资源即使 `manager_grant_enabled=TRUE`,也不能出现在 H5 赠送列表。 - `grantable=FALSE` 的资源即使 `manager_grant_enabled=TRUE`,也不能被任何赠送命令发放。 - 历史已经赠送出去的权益不受该字段变更影响。 后台接口保持当前 `server/admin -> wallet-service` 边界: ```http GET /api/v1/admin/resources POST /api/v1/admin/resources PUT /api/v1/admin/resources/{resource_id} POST /api/v1/admin/resources/{resource_id}/enable POST /api/v1/admin/resources/{resource_id}/disable ``` DTO/RPC 字段: - `wallet.v1.Resource.manager_grant_enabled` - `CreateResourceRequest.manager_grant_enabled` - `UpdateResourceRequest.manager_grant_enabled` - `ListResourcesRequest.manager_grant_only` `server/admin` 只做字段透传和操作审计。新增或编辑资源时,如果请求体不传 `managerGrantEnabled`,admin-server 必须向 wallet-service 传 `true`,避免前端漏字段导致默认关闭。 ## Manager Capability Boundary 首版经理身份定义为: - 当前用户拥有 active `Agency`,且 `agencies.owner_user_id = actor_user_id`。 - `Agency.status=active`。 - 该能力由 `user-service` host domain 判断,gateway 不能只读 access token 或客户端传参。 建议在 user-service 增加通用能力接口: ```proto message CheckBusinessCapabilityRequest { RequestMeta meta = 1; int64 actor_user_id = 2; string capability = 3; } message CheckBusinessCapabilityResponse { bool allowed = 1; string reason = 2; } ``` 首版 capability: | Capability | Allowed When | | --- | --- | | `manager_center` | active Agency owner | | `manager_resource_grant` | active Agency owner | 后续如果出现 Agency 授权管理员,不改 gateway 和 wallet-service,只在 user-service host domain 增加 `agency_managers` 或对应 read model,并扩展 capability 判定。 ## H5 App APIs 所有接口走 gateway `/api/v1` envelope。access token 解析、`RequestMeta.request_id`、`app_code`、`client_ip`、`user_agent` 由 gateway 透传到内部 gRPC。 ### 赠送列表 ```http GET /api/v1/manager-center/resource-grants/resources?resource_type=badge&page=1&page_size=20 ``` 处理规则: 1. gateway 从 token 取 `actor_user_id`。 2. gateway 调 user-service `CheckBusinessCapability(capability=manager_resource_grant)`。 3. gateway 调 wallet-service `ListResources(active_only=true, manager_grant_only=true, resource_type, page, page_size)`。 4. wallet-service 只返回 `status=active AND grantable=TRUE AND manager_grant_enabled=TRUE` 的资源。 响应字段只返回 H5 展示和提交所需字段: ```json { "items": [ { "resource_id": "101", "resource_type": "badge", "name": "VIP Badge", "asset_url": "https://...", "preview_url": "https://...", "wallet_asset_type": "", "wallet_asset_amount": 0, "sort_order": 10 } ], "total": 1, "page": 1, "page_size": 20 } ``` H5 不能从该接口获得后台审计字段、`created_by_user_id`、`updated_by_user_id` 或完整 `metadata_json`。如果某类资源需要额外展示配置,必须在 wallet-service 先定义稳定的展示 DTO,不能把原始后台 metadata 透出。 ### 赠送给用户 ```http POST /api/v1/manager-center/resource-grants ``` 请求: ```json { "command_id": "mgr-grant-20260511-0001", "target_user_id": "900001", "resource_id": "101", "reason": "manager_center" } ``` 处理规则: 1. gateway 校验当前用户拥有 `manager_resource_grant` capability。 2. gateway 调 user-service `GetUser(target_user_id)`,确认目标用户属于同一 `app_code`、存在、未禁用、未封禁。 3. gateway 不接受客户端提交 `quantity`、`duration_ms`、`wallet_asset_amount`。 4. gateway 调 wallet-service `GrantResource`: - `grant_source=manager_center` - `operator_user_id=actor_user_id` - `target_user_id=resolved target` - `quantity=1` - `duration_ms=0` - `reason=manager_center` 5. wallet-service 在同一事务中校验资源 `status=active`、`grantable=TRUE`、`manager_grant_enabled=TRUE`,再按现有资源策略落 `resource_grants`、`resource_grant_items`、钱包账本或用户资源权益。 wallet-service 不能只依赖赠送列表过滤。`GrantResource` 必须在写事务内根据 `grant_source=manager_center` 重新锁定资源并校验 `manager_grant_enabled`,防止客户端绕过列表直接提交资源 ID。 幂等规则沿用现有 `resource_grants.command_id`: - 同一 `app_code + command_id`、同一 payload 重试返回原 grant。 - 同一 `command_id` 换目标用户或资源返回冲突。 - `request_id` 只做链路追踪,不能当幂等键。 ## Common User Lookup 通用用户查询不要挂在经理中心专属模块里。建议 gateway 暴露统一入口: ```http GET /api/v1/business/users/lookup?scene=manager_resource_grant&keyword=123456&page_size=20 ``` user-service 新增或扩展查询 RPC: ```proto message BusinessUserLookupRequest { RequestMeta meta = 1; int64 actor_user_id = 2; string scene = 3; string keyword = 4; int32 page_size = 5; } message BusinessUserLookupItem { int64 user_id = 1; string display_user_id = 2; string username = 3; string avatar = 4; UserStatus status = 5; int64 region_id = 6; string region_code = 7; string region_name = 8; } message BusinessUserLookupResponse { repeated BusinessUserLookupItem users = 1; } ``` scene 权限矩阵: | Scene | Actor Requirement | Query Scope | | --- | --- | --- | | `manager_resource_grant` | `manager_resource_grant` capability | active users in same `app_code` | | `agency_application` | logged-in active user | active agencies or agency owner candidates,按后续申请流程定义 | | `bd_invite_agency` | active BD or BD Leader | same-region active users,不能已有 active Agency owner | | `bd_invite_bd` | active BD Leader | same-region active users,不能已有 active BD | 首版只实现 `manager_resource_grant`。未实现 scene 必须返回 `INVALID_ARGUMENT`,不能默认降级成全局用户搜索。 查询安全规则: - `keyword` 必须是短号、用户 ID 或至少 2 个字符的昵称片段。 - 数字 keyword 优先按 current display_user_id 精确匹配,再按 user_id 精确匹配。 - `page_size` 最大 20。 - 只返回必要展示字段,不返回手机号、登录方式、设备、邀请码绑定、钱包余额或三方身份。 - 被禁用、封禁、跨 `app_code` 用户不返回。 - gateway 对 lookup 和 grant 都要做限流;lookup 不能成为撞库枚举入口。 ## Sequence ```mermaid sequenceDiagram participant H5 as H5 Manager Center participant G as gateway-service participant U as user-service participant W as wallet-service H5->>G: GET /manager-center/resource-grants/resources G->>U: CheckBusinessCapability(actor, manager_resource_grant) U-->>G: allowed G->>W: ListResources(active_only, manager_grant_only) W-->>G: grantable resources G-->>H5: resources H5->>G: GET /business/users/lookup?scene=manager_resource_grant G->>U: BusinessUserLookup(actor, scene, keyword) U-->>G: minimal users G-->>H5: users H5->>G: POST /manager-center/resource-grants G->>U: CheckBusinessCapability(actor, manager_resource_grant) U-->>G: allowed G->>U: GetUser(target_user_id) U-->>G: active target G->>W: GrantResource(source=manager_center) W-->>G: resource grant G-->>H5: grant result ``` ## Validation Rules | Rule | Owner | Error | | --- | --- | --- | | actor is not active manager | user-service | `PERMISSION_DENIED` | | lookup scene unknown | user-service | `INVALID_ARGUMENT` | | target user not found, disabled, banned, or different app | user-service/gateway | `NOT_FOUND` or `PERMISSION_DENIED` | | resource disabled | wallet-service | `CONFLICT` | | resource `grantable=FALSE` | wallet-service | `CONFLICT` | | resource `manager_grant_enabled=FALSE` and source is `manager_center` | wallet-service | `PERMISSION_DENIED` | | H5 submits quantity or duration | gateway | ignore or reject;首版建议 reject `INVALID_ARGUMENT` | | duplicate command_id with changed payload | wallet-service | `LEDGER_CONFLICT` | ## Event And Audit - 后台切换 `manager_grant_enabled` 产出 `ResourceChanged` outbox,便于 H5 资源列表缓存刷新。 - 经理中心赠送成功产出 `ResourceGranted` 和必要的 `UserResourceChanged`/`WalletBalanceChanged` 事件。 - `resource_grants.grant_source='manager_center'` 是 App 赠送审计来源。 - `operator_user_id` 记录发起赠送的经理用户 ID,不记录后台 admin 用户。 - gateway 应在结构化日志记录 `request_id`、`actor_user_id`、`target_user_id`、`resource_id`、`grant_id` 和错误 reason。 ## Implementation Order 1. `api/proto/wallet/v1/wallet.proto` 增加 `manager_grant_enabled` 和 `manager_grant_only`,运行 `make proto`。 2. `wallet-service` MySQL 初始化脚本增加 `resources.manager_grant_enabled`,repository list/create/update/status/GrantResource 路径全部带字段和校验。 3. `server/admin` resource DTO/request 增加 `managerGrantEnabled`;后台资源列表 switch 默认打开,编辑时透传。 4. `user-service` 增加 `CheckBusinessCapability` 和 `BusinessUserLookup`,首版只开放 `manager_resource_grant` scene。 5. `gateway-service` 增加 H5 经理中心三个入口:赠送资源列表、通用用户查询、赠送资源。 6. 补 OpenAPI、前端生成代码和 H5 页面联调。 7. 补测试:wallet manager switch、admin 透传、user lookup 权限、gateway H5 流程和幂等冲突。 ## Acceptance Criteria - 新建资源不传 `managerGrantEnabled` 时,后台列表 switch 显示打开。 - 后台关闭某资源赠送权限后,H5 经理中心赠送列表不返回该资源。 - H5 绕过列表直接提交关闭权限的 `resource_id`,wallet-service 拒绝赠送。 - 非经理用户调用赠送列表、用户查询或赠送接口均返回权限错误。 - 经理用户可以查询目标用户并完成单资源赠送,`resource_grants.grant_source=manager_center`,`operator_user_id` 为经理用户 ID。 - 同一 `command_id` 重试不重复发放;换目标或资源返回冲突。