524 lines
24 KiB
Markdown
524 lines
24 KiB
Markdown
# Host, Agency And BD Admin Architecture
|
||
|
||
本文只定义后台/Admin 侧实现:政策配置、工资周期、工资单、审核、钱包入账、调整单、后台审计和运营干预。App 侧的主播申请、Agency 成员、BD 邀请和用户可见查询见 [Host Agency BD App Architecture](./主播公会BD架构.md)。
|
||
|
||
这里的工资统一指发放到主播侧 `USD_BALANCE` 的业务奖励。提现审核和人工打款仍由 `wallet-service` 拥有,Admin 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。
|
||
|
||
本文不新增独立 `host-service` 微服务。host、Agency、BD、政策和工资后台能力属于 user-services,代码落在当前仓库的 `services/user-service` host domain。
|
||
|
||
## Goals
|
||
|
||
- 后台可以配置 host、agency、bd 三类工资政策,并按区域生效。
|
||
- 后台可以选择日结、周结、半月结、月结,不把结算周期写死在代码里。
|
||
- 后台可以生成工资周期,查看每个 host/Agency/BD/BD Leader 的明细来源。
|
||
- 后台审批后,`user-service` host domain 以幂等方式调用 `wallet-service` 给 `USD_BALANCE` 入账。
|
||
- 后台可以对迟到事件、人工修正、争议处理创建调整单,不能直接修改已入账工资项。
|
||
- 后台可以创建或停用 BD Leader、BD、Agency,也可以关闭 Agency;后台可以查看和新增币商 `coin_seller`;所有动作必须有审计记录。
|
||
|
||
## Non-Goals
|
||
|
||
- 不在 Admin 里承载 App 长连接、房间麦位、房间核心状态或 IM 投递。
|
||
- 不让 `wallet-service` 理解 host/Agency/BD 政策。钱包只接收已审批的入账请求。
|
||
- 不用 Redis 保存政策、工资周期、工资单或审计事实。Redis 只能做短 TTL 查询缓存和互斥锁兜底。
|
||
- 不允许后台在不留审计的情况下改关系、改政策、改工资金额。
|
||
- 不允许已 posted 的工资单原地改金额;必须创建反向或补差调整项。
|
||
|
||
## Service Boundaries
|
||
|
||
```mermaid
|
||
graph LR
|
||
Admin["Admin Console"] --> AdminServer["hyapp-admin-server"]
|
||
AdminServer --> User["user-service\nhost domain"]
|
||
AdminServer --> Wallet["wallet-service"]
|
||
|
||
AdminServer --> AdminDB[("hyapp_admin MySQL")]
|
||
User --> UserDB[("user MySQL")]
|
||
Wallet --> WalletDB[("wallet MySQL")]
|
||
|
||
RoomOutbox[("room_outbox")] --> MQ["MQ / event bus"]
|
||
WalletOutbox[("wallet_outbox")] --> MQ
|
||
MQ --> User
|
||
|
||
User --> HostOutbox[("host_outbox")]
|
||
HostOutbox --> Message["message inbox owner"]
|
||
HostOutbox --> BI["BI / risk / export"]
|
||
```
|
||
|
||
| Service | Admin Responsibility |
|
||
| --- | --- |
|
||
| `hyapp-admin-server` | Admin HTTP 入口、后台鉴权、request envelope、后台操作审计;使用独立 `hyapp_admin` 库 |
|
||
| `user-service` | 用户、区域、账号状态查询;host/Agency/BD/coin_seller 政策、周期、工资单、关系管理、统计聚合、幂等结果、领域 outbox;App/客户端不能提交区域,后台关系命令的 `region_id` 只能来自 `hyapp-admin-server` 鉴权审计入口 |
|
||
| `room-service` | 只产出上麦和礼物事件,不接收后台工资命令 |
|
||
| `wallet-service` | `COIN`、`COIN_SELLER_COIN`、`USD_BALANCE` 入账、提现冻结、提现审核状态和钱包流水 |
|
||
|
||
## Admin Permission Model
|
||
|
||
后台权限至少拆成四类,避免一个运营账号同时拥有配置政策和发钱能力。
|
||
|
||
| Permission | Can Do | Cannot Do |
|
||
| --- | --- | --- |
|
||
| `host_relation_admin` | 创建/停用 BD Leader、BD、Agency,关闭 Agency,查看和新增 coin_seller | 审批工资入账 |
|
||
| `salary_policy_admin` | 创建政策草稿、启停政策、配置周期 | 直接发工资 |
|
||
| `recharge_policy_admin` | 配置区域充值汇率,例如 80000 金币记 1 美元充值 | 重算历史充值金额 |
|
||
| `salary_reviewer` | 查看工资单、驳回、请求重算 | 调用钱包入账 |
|
||
| `salary_approver` | 审批工资周期、触发 posting | 修改政策和关系 |
|
||
|
||
所有后台命令必须由 `hyapp-admin-server` 写入 `hyapp_admin.admin_operation_logs`。`user-service` 只保存 host/Agency/BD 业务事实、`command_id` 幂等结果和领域事件,不创建也不写后台审计表,避免后台库与用户业务库耦合。
|
||
|
||
币商后台入口同样遵守这个边界:`hyapp-admin-server` 负责权限和审计,`user-service` 保存 `coin_seller_profiles` 身份事实,`wallet-service` 保存 `COIN_SELLER_COIN` 余额和转账流水。后台列表可以聚合展示币商身份、用户基础资料和币商余额,但不能把余额字段落到 admin 库或 user 库。
|
||
|
||
币商给玩家转金币会形成用户充值记录,充值金额由 `wallet-service.wallet_recharge_policies` 的区域政策快照决定;后台修改充值政策只能影响新交易,不能重算历史 `wallet_recharge_records`。
|
||
|
||
| Permission | Kind | Usage |
|
||
| --- | --- | --- |
|
||
| `coin-seller:view` | `menu` | 币商列表、币商余额展示 |
|
||
| `coin-seller:create` | `button` | 新增币商身份 |
|
||
| `coin-seller:update` | `button` | 启用或禁用币商身份 |
|
||
|
||
后台管理某个 App 的 host、Agency、BD、政策或工资时,必须先在后台上下文中选择 `app_code`。`hyapp_admin` 库不按 App 拆库;审计行可以在 `detail` 中记录 `app_code`。业务事实仍写入 user/wallet 等业务库,并由对应表的 `app_code` 隔离。
|
||
|
||
当前 `user_id` 是内部稳定主键,后台仍必须在所有 host/Agency/BD 查询、幂等和审计 detail 中带上 `app_code`。如果某些现有表还使用全局 `PRIMARY KEY(user_id)`、`PRIMARY KEY(command_id)` 或 `PRIMARY KEY(event_id)`,这只能说明当前实现暂时要求这些 ID 全局唯一;不能据此省略 `app_code`,也不能在多 App 写入前声称同一个 `command_id` 可以跨 App 复用。
|
||
|
||
```sql
|
||
admin_operation_logs(
|
||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||
request_id VARCHAR(64) NOT NULL DEFAULT '',
|
||
user_id BIGINT UNSIGNED NOT NULL,
|
||
username VARCHAR(64) NOT NULL,
|
||
action VARCHAR(80) NOT NULL,
|
||
resource VARCHAR(120),
|
||
resource_id VARCHAR(80) NOT NULL DEFAULT '',
|
||
status VARCHAR(32) NOT NULL,
|
||
detail TEXT,
|
||
created_at_ms BIGINT NOT NULL,
|
||
INDEX idx_admin_operation_logs_request_id (request_id),
|
||
INDEX idx_admin_operation_logs_resource_id (resource_id),
|
||
INDEX idx_admin_operation_logs_action (action)
|
||
);
|
||
```
|
||
|
||
Phase 2 关系管理的 `detail` 至少包含 `command_id`、目标对象、最终状态和 `reason`;如后续要做结构化前后快照,应在 `hyapp_admin` 库新增专用审计表或扩展 `admin_operation_logs`,不能把审计行写入 user-service 库。
|
||
|
||
## Policy Model
|
||
|
||
政策必须版本化。工资结算使用周期生成时锁定的 policy snapshot,不能用当前 active policy 重算历史。
|
||
|
||
```sql
|
||
salary_policies(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
policy_id BIGINT NOT NULL PRIMARY KEY,
|
||
policy_type VARCHAR(32) NOT NULL,
|
||
region_id BIGINT NOT NULL,
|
||
name VARCHAR(128) NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
version VARCHAR(64) NOT NULL,
|
||
effective_from_ms BIGINT NOT NULL,
|
||
effective_to_ms BIGINT NULL,
|
||
settlement_cadence VARCHAR(32) NOT NULL,
|
||
created_by_user_id BIGINT NOT NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
KEY idx_salary_policies_region_type (app_code, region_id, policy_type, status)
|
||
);
|
||
|
||
salary_policy_rules(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
rule_id BIGINT NOT NULL PRIMARY KEY,
|
||
policy_id BIGINT NOT NULL,
|
||
level_no INT NOT NULL,
|
||
min_mic_minutes BIGINT NOT NULL DEFAULT 0,
|
||
min_gift_points BIGINT NOT NULL DEFAULT 0,
|
||
min_effective_days INT NOT NULL DEFAULT 0,
|
||
min_base_salary_usd_minor BIGINT NOT NULL DEFAULT 0,
|
||
fixed_salary_usd_minor BIGINT NOT NULL DEFAULT 0,
|
||
commission_bps INT NOT NULL DEFAULT 0,
|
||
metadata_json JSON NULL,
|
||
UNIQUE KEY uk_salary_policy_level (app_code, policy_id, level_no)
|
||
);
|
||
```
|
||
|
||
| Type | Applies To | Base |
|
||
| --- | --- | --- |
|
||
| `host` | active host work stats | mic minutes, gift points, effective days |
|
||
| `agency` | Agency owner extra salary | Agency hosts' host salary sum, count, or agency-specific metrics |
|
||
| `bd` | BD and BD Leader salary | downstream hosts' host salary sum |
|
||
|
||
BD 的基数只能是 host salary。Agency salary 和 BD salary 不能递归进入 BD 基数,否则工资会复利膨胀且无法审计。
|
||
|
||
## Settlement Cadence
|
||
|
||
后台可以按区域和政策类型选择 `daily`、`weekly`、`semi_monthly`、`monthly`。
|
||
|
||
```sql
|
||
salary_cycle_configs(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
config_id BIGINT NOT NULL PRIMARY KEY,
|
||
region_id BIGINT NOT NULL,
|
||
policy_type VARCHAR(32) NOT NULL,
|
||
settlement_cadence VARCHAR(32) NOT NULL,
|
||
timezone VARCHAR(64) NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
KEY idx_salary_cycle_config_scope (app_code, region_id, policy_type, status)
|
||
);
|
||
```
|
||
|
||
同一区域、同政策类型只允许一个 active config。MySQL 没有 partial unique index 时,不要用 nullable 或 status 唯一键硬凑;在启用配置时用事务锁住同 scope 的 active rows,再做状态切换。
|
||
|
||
| Cadence | Rule |
|
||
| --- | --- |
|
||
| `daily` | local date 00:00:00 to next date 00:00:00 |
|
||
| `weekly` | configured week start, default Monday |
|
||
| `semi_monthly` | day 1-15 and day 16-end |
|
||
| `monthly` | calendar month |
|
||
|
||
计算必须使用配置时区。数据库保存 UTC 毫秒边界,同时保存后台导出需要的展示日期。
|
||
|
||
## Payroll Model
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["host_daily_stats"] --> B["salary cycle calculation"]
|
||
B --> C["host salary items"]
|
||
C --> D["agency salary items"]
|
||
C --> E["BD or BD Leader salary items"]
|
||
C --> F["review"]
|
||
D --> F
|
||
E --> F
|
||
F --> G["approve"]
|
||
G --> H["wallet CreditSalaryBalance"]
|
||
H --> I["USD_BALANCE"]
|
||
```
|
||
|
||
```sql
|
||
salary_cycles(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
cycle_id BIGINT NOT NULL PRIMARY KEY,
|
||
region_id BIGINT NOT NULL,
|
||
cycle_type VARCHAR(32) NOT NULL,
|
||
period_start_ms BIGINT NOT NULL,
|
||
period_end_ms BIGINT NOT NULL,
|
||
display_period VARCHAR(64) NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
policy_snapshot_json JSON NOT NULL,
|
||
source_watermark_ms BIGINT NOT NULL,
|
||
created_by_user_id BIGINT NOT NULL,
|
||
approved_by_user_id BIGINT NULL,
|
||
posted_at_ms BIGINT NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
UNIQUE KEY uk_salary_cycle (app_code, region_id, cycle_type, period_start_ms, period_end_ms)
|
||
);
|
||
|
||
salary_items(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
salary_item_id BIGINT NOT NULL PRIMARY KEY,
|
||
cycle_id BIGINT NOT NULL,
|
||
receiver_user_id BIGINT NOT NULL,
|
||
receiver_role VARCHAR(32) NOT NULL,
|
||
salary_component VARCHAR(32) NOT NULL,
|
||
region_id BIGINT NOT NULL,
|
||
relation_scope_key VARCHAR(160) NOT NULL,
|
||
agency_id BIGINT NULL,
|
||
bd_user_id BIGINT NULL,
|
||
bd_leader_user_id BIGINT NULL,
|
||
policy_id BIGINT NOT NULL,
|
||
policy_version VARCHAR(64) NOT NULL,
|
||
hit_level INT NOT NULL,
|
||
base_host_salary_usd_minor BIGINT NOT NULL DEFAULT 0,
|
||
mic_minutes BIGINT NOT NULL DEFAULT 0,
|
||
gift_points BIGINT NOT NULL DEFAULT 0,
|
||
effective_days INT NOT NULL DEFAULT 0,
|
||
amount_usd_minor BIGINT NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '',
|
||
metadata_json JSON NULL,
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL,
|
||
UNIQUE KEY uk_salary_item_component (app_code, cycle_id, receiver_user_id, salary_component, relation_scope_key)
|
||
);
|
||
|
||
salary_item_sources(
|
||
salary_item_id BIGINT NOT NULL,
|
||
source_salary_item_id BIGINT NOT NULL,
|
||
source_host_user_id BIGINT NOT NULL,
|
||
source_amount_usd_minor BIGINT NOT NULL,
|
||
PRIMARY KEY (salary_item_id, source_salary_item_id)
|
||
);
|
||
```
|
||
|
||
`relation_scope_key` 必须非空,例如 `agency:{agency_id}`、`bd:{bd_user_id}`、`leader:{leader_user_id}`、`none:{region_id}`。不要把 nullable `agency_id` 或 `bd_user_id` 直接放进唯一键后期待 MySQL 帮你保证幂等。
|
||
|
||
## Salary Cycle State
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> open
|
||
open --> calculating
|
||
calculating --> draft
|
||
draft --> approved
|
||
approved --> posting
|
||
posting --> posted
|
||
posting --> post_failed
|
||
post_failed --> posting
|
||
draft --> voided
|
||
posted --> adjusted
|
||
```
|
||
|
||
| Status | Meaning |
|
||
| --- | --- |
|
||
| `open` | 周期已创建但未计算 |
|
||
| `calculating` | 正在生成工资项 |
|
||
| `draft` | 已计算,等待后台审核 |
|
||
| `approved` | 已审核,可以入账 |
|
||
| `posting` | 正在调用钱包入账 |
|
||
| `posted` | 全部工资项已入账 |
|
||
| `post_failed` | 部分或全部入账失败,可重试 |
|
||
| `voided` | 入账前作废 |
|
||
| `adjusted` | 已通过调整项修正 |
|
||
|
||
`posted` cycle 不可变。迟到事件和人工修正只能创建 adjustment cycle 或 adjustment salary item。
|
||
|
||
## Salary Calculation Rules
|
||
|
||
### Host Salary
|
||
|
||
输入来自 App 文档定义的 `host_daily_stats`。基础麦上在线事实先按所有用户沉淀,详见 [User Mic Online Time Architecture](./用户麦上在线时长架构.md);`host_daily_stats` 是工资需要的关系快照派生表。
|
||
|
||
- Sum `mic_online_ms` within cycle。
|
||
- Sum `gift_point_received` within cycle。
|
||
- Sum `effective_day`。
|
||
- 使用 stats 中的 `agency_id/bd_user_id/bd_leader_user_id` 关系快照做归属。
|
||
|
||
规则匹配:
|
||
|
||
```text
|
||
find highest host policy rule where:
|
||
mic_minutes >= min_mic_minutes
|
||
gift_points >= min_gift_points
|
||
effective_days >= min_effective_days
|
||
```
|
||
|
||
结果:
|
||
|
||
- 命中的规则产生 `host_salary` item。
|
||
- `amount_usd_minor = fixed_salary_usd_minor`。
|
||
- 未命中规则时金额为 0;是否保留 0 元 draft item 由后台报表需求决定。
|
||
|
||
### Agency Salary
|
||
|
||
Agency owner 有两个收益组件:
|
||
|
||
- `host_salary`: owner 自己作为 host 工作产生。
|
||
- `agency_salary`: owner 作为 Agency 获得的额外收益。
|
||
|
||
默认 Agency 基数使用同周期、同 Agency 下所有 host salary item:
|
||
|
||
```text
|
||
agency_base = sum(host_salary.amount_usd_minor where agency_id = agency.agency_id)
|
||
agency_salary = rule.fixed_salary + agency_base * rule.commission_bps / 10000
|
||
```
|
||
|
||
如果产品需要按礼物积分、有效主播数或开播天数计算 Agency 工资,必须把输入字段显式写进 rule 和 item snapshot,不允许结算时从当前 membership 反推。
|
||
|
||
### BD And BD Leader Salary
|
||
|
||
BD Leader 也是 BD,所以二者使用同一个 `bd_salary` 组件。`receiver_role` 区分 `bd` 和 `bd_leader`;同一 receiver 和同一来源 host salary 不能同时生成 BD item 和 Leader item。
|
||
|
||
普通 BD 基数:
|
||
|
||
```text
|
||
bd_source_items = distinct host_salary items where bd_user_id = bd.user_id
|
||
bd_base = sum(bd_source_items.amount_usd_minor)
|
||
bd_salary = bd_base * bd_policy.commission_bps / 10000 + fixed_salary
|
||
```
|
||
|
||
BD Leader 基数:
|
||
|
||
```text
|
||
leader_source_items = distinct host_salary items where
|
||
bd_user_id = leader.user_id
|
||
or bd_leader_user_id = leader.user_id
|
||
leader_base = sum(leader_source_items.amount_usd_minor)
|
||
leader_salary = leader_base * bd_policy.commission_bps / 10000 + fixed_salary
|
||
```
|
||
|
||
Leader 直接拥有 Agency 下线时,推荐快照同时设置 `bd_user_id = leader.user_id` 和 `bd_leader_user_id = leader.user_id`;工资生成按 `source_host_salary.salary_item_id` 去重,所以这批 host 对该 leader 只算一次。下级 BD 拥有的 Agency 则设置 `bd_user_id = bd.user_id`、`bd_leader_user_id = leader.user_id`。
|
||
|
||
## Posting To Wallet
|
||
|
||
工资入账必须通过 `wallet-service`,不能直接改钱包表。
|
||
|
||
1. Admin 审批 cycle。
|
||
2. `user-service` host domain 查询所有 `approved` salary items。
|
||
3. 对每个 item 调用 `wallet-service CreditSalaryBalance`,幂等键为 `salary:{salary_item_id}`。
|
||
4. `wallet-service` credit `USD_BALANCE` 并返回 `wallet_transaction_id`。
|
||
5. `user-service` host domain 保存 `wallet_transaction_id`,把 item 标记为 `posted`。
|
||
6. 如果中途失败,只重试未 posted item,幂等键保持不变。
|
||
|
||
Wallet metadata 必须包含 `cycle_id`、`salary_item_id`、`receiver_role`、`salary_component`、`policy_id`、`policy_version`、`period_start_ms`、`period_end_ms`。
|
||
|
||
## Adjustments
|
||
|
||
调整只能补差或冲正,不直接修改 posted item。
|
||
|
||
```sql
|
||
salary_adjustments(
|
||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
|
||
adjustment_id BIGINT NOT NULL PRIMARY KEY,
|
||
source_cycle_id BIGINT NOT NULL,
|
||
source_salary_item_id BIGINT NULL,
|
||
receiver_user_id BIGINT NOT NULL,
|
||
amount_usd_minor BIGINT NOT NULL,
|
||
reason VARCHAR(512) NOT NULL,
|
||
status VARCHAR(32) NOT NULL,
|
||
created_by_user_id BIGINT NOT NULL,
|
||
approved_by_user_id BIGINT NULL,
|
||
wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '',
|
||
created_at_ms BIGINT NOT NULL,
|
||
updated_at_ms BIGINT NOT NULL
|
||
);
|
||
```
|
||
|
||
调整项也必须走审批和钱包幂等入账。负数调整如果钱包账户余额不足,不能直接扣成负数;应该进入人工处理状态,由提现审核或后续工资抵扣策略处理。
|
||
|
||
## Admin APIs
|
||
|
||
External HTTP stays in `gateway-service` and must use `/api/v1` envelope.
|
||
|
||
```http
|
||
POST /api/v1/admin/host/policies
|
||
PUT /api/v1/admin/host/policies/{policy_id}
|
||
POST /api/v1/admin/host/policies/{policy_id}/enable
|
||
POST /api/v1/admin/host/policies/{policy_id}/disable
|
||
|
||
POST /api/v1/admin/salary-cycles/calculate
|
||
GET /api/v1/admin/salary-cycles
|
||
GET /api/v1/admin/salary-cycles/{cycle_id}
|
||
GET /api/v1/admin/salary-cycles/{cycle_id}/items
|
||
POST /api/v1/admin/salary-cycles/{cycle_id}/approve
|
||
POST /api/v1/admin/salary-cycles/{cycle_id}/post
|
||
POST /api/v1/admin/salary-cycles/{cycle_id}/void
|
||
POST /api/v1/admin/salary-items/{salary_item_id}/adjust
|
||
|
||
POST /api/v1/admin/bd-leaders
|
||
POST /api/v1/admin/bds
|
||
POST /api/v1/admin/agencies
|
||
POST /api/v1/admin/agencies/{agency_id}/close
|
||
POST /api/v1/admin/agencies/{agency_id}/join-enabled
|
||
```
|
||
|
||
Admin APIs require separate admin auth and audit. App users must never call admin policy or settlement endpoints.
|
||
|
||
## Internal gRPC Sketch
|
||
|
||
```proto
|
||
service UserHostAdminService {
|
||
rpc CreateSalaryPolicy(CreateSalaryPolicyRequest) returns (CreateSalaryPolicyResponse);
|
||
rpc UpdateSalaryPolicy(UpdateSalaryPolicyRequest) returns (UpdateSalaryPolicyResponse);
|
||
rpc SetSalaryPolicyStatus(SetSalaryPolicyStatusRequest) returns (SetSalaryPolicyStatusResponse);
|
||
|
||
rpc CalculateSalaryCycle(CalculateSalaryCycleRequest) returns (CalculateSalaryCycleResponse);
|
||
rpc GetSalaryCycle(GetSalaryCycleRequest) returns (GetSalaryCycleResponse);
|
||
rpc ApproveSalaryCycle(ApproveSalaryCycleRequest) returns (ApproveSalaryCycleResponse);
|
||
rpc PostSalaryCycle(PostSalaryCycleRequest) returns (PostSalaryCycleResponse);
|
||
rpc AdjustSalaryItem(AdjustSalaryItemRequest) returns (AdjustSalaryItemResponse);
|
||
|
||
rpc CreateBDLeader(CreateBDLeaderRequest) returns (CreateBDLeaderResponse);
|
||
rpc CreateBD(CreateBDRequest) returns (CreateBDResponse);
|
||
rpc CreateAgency(CreateAgencyRequest) returns (CreateAgencyResponse);
|
||
rpc CloseAgency(CloseAgencyRequest) returns (CloseAgencyResponse);
|
||
}
|
||
```
|
||
|
||
`CreateBDLeaderRequest` 必须携带 `region_id`。user-service 校验该区域 active,锁定目标用户行,必要时更新 `users.region_id`,再创建 `bd_profiles`;同一个 `command_id` 重试只返回已创建的 BD Leader 事实,不重复迁移。
|
||
这属于允许改变用户区域的后台关系管理路径之一;完整来源见 [User Region Change Sources](./用户区域与国家开发.md#user-region-change-sources)。
|
||
|
||
当前项目仍在开发阶段,proto 可以按当前事实调整;修改 `api/proto` 后必须运行 `make proto` 并提交生成文件。
|
||
|
||
## Concurrency And Idempotency
|
||
|
||
- Admin 命令必须携带 `command_id`,同一 `command_id` 重试返回同一结果。
|
||
- Policy enable 必须锁定同区域、同 type 的 active policy,防止同时启用多个冲突政策。
|
||
- Cycle create 使用唯一键 `(region_id, cycle_type, period_start_ms, period_end_ms)`。
|
||
- Cycle calculation 使用 deterministic item key。重跑同一个 `draft` cycle 可以替换 draft items 或返回现有结果,不能重复插入。
|
||
- Cycle approve 必须锁 cycle 当前状态为 `draft`。
|
||
- Posting 使用 `salary_item_id` 作为钱包幂等键。`post_failed` 重试只处理未 posted item。
|
||
- Relationship admin action 要锁目标用户的 `users`、`host_profiles`、`agencies`、`bd_profiles`,避免和 App 邀请接受并发造成身份冲突。后台创建 BD Leader 需要在同一事务内更新 `users.region_id` 和 `bd_profiles.region_id`。
|
||
|
||
## Event And Outbox
|
||
|
||
`user-service` host domain should write host outbox rows in the user database:
|
||
|
||
| Event | When |
|
||
| --- | --- |
|
||
| `SalaryPolicyChanged` | policy created, enabled, disabled, or updated |
|
||
| `SalaryCycleCalculated` | draft generated |
|
||
| `SalaryCycleApproved` | cycle approved |
|
||
| `SalaryCyclePosted` | all salary items posted to wallet |
|
||
| `SalaryItemPosted` | one salary item credited to wallet |
|
||
| `SalaryAdjustmentCreated` | adjustment draft created |
|
||
| `SalaryAdjustmentPosted` | adjustment credited or reversed in wallet |
|
||
| `AdminHostRelationChanged` | admin creates/stops BD/Agency/Leader |
|
||
|
||
Outbox consumers can update App message inbox, BI, risk systems, and export jobs. They must not be required for salary facts to commit.
|
||
|
||
## App Message Notifications
|
||
|
||
后台关系和工资动作需要通知 App 用户时,统一进入 App `消息` tab 的 `system` 分区,具体 message inbox 设计见 [App Message Tab Architecture](./App消息Tab架构.md)。`hyapp-admin-server` 只负责后台权限和审计;App 消息事实由 message inbox owner 落库。
|
||
|
||
| Admin/Salary Fact | Target User | Message Type | Action |
|
||
| --- | --- | --- | --- |
|
||
| Admin creates BD Leader | target user | `system` | `bd_profile_detail` |
|
||
| Admin creates BD | target user | `system` | `bd_profile_detail` |
|
||
| Admin creates Agency | owner user | `system` | `agency_detail` |
|
||
| Agency closed by admin | owner and active members | `system` | `agency_detail` |
|
||
| Salary cycle item posted | salary receiver | `system` | `salary_item_detail` |
|
||
| Salary adjustment posted | affected receiver | `system` | `salary_adjustment_detail` |
|
||
| Salary posting failed after approval | no App user notification by default | internal alert |
|
||
|
||
工资、关系和钱包入账事实不依赖消息投递。message inbox 消费失败时只能重试或告警,不能回滚 salary cycle、salary item 或 wallet transaction。
|
||
|
||
## Edge Cases
|
||
|
||
| Case | Rule |
|
||
| --- | --- |
|
||
| Policy changes mid-cycle | Existing open/draft cycle keeps selected policy snapshot; future cycles use new active policy |
|
||
| Host changes Agency during cycle | Salary uses `host_daily_stats` relation snapshot and can split across agencies |
|
||
| Late room event after posted cycle | Create adjustment item; do not mutate posted salary item |
|
||
| Wallet posting partially fails | Retry unposted items with same idempotency key |
|
||
| Admin accidentally posts twice | Wallet idempotency must prevent duplicate `USD_BALANCE` credit |
|
||
| Agency closed mid-cycle | Work before close remains attributable; future App joins/search stop immediately |
|
||
| User region changed | Existing cycle uses event snapshots; new policy/cycle matching follows stats region |
|
||
| Negative adjustment exceeds balance | Move to manual handling; do not force wallet negative unless wallet explicitly supports debt |
|
||
|
||
## Implementation Order
|
||
|
||
整体开发顺序单独维护在 [Host Agency BD Implementation Sequence](./主播公会BD实施顺序.md)。Admin 文档只保留后台政策、工资、入账、调整和权限审计设计,避免和 App 实施顺序混在一起。
|
||
|
||
## Verification Matrix
|
||
|
||
| Scenario | Expected |
|
||
| --- | --- |
|
||
| Enable two host policies for same region | Second enable rejected or first disabled in same transaction |
|
||
| Calculate daily cycle twice | Same draft result; no duplicate salary items |
|
||
| Host meets highest rule | Highest matching level selected |
|
||
| Agency salary generated | Base uses host salary items under that Agency only |
|
||
| BD Leader owns direct Agency | Source host salary counted once for the leader |
|
||
| BD under Leader owns Agency | Host salary contributes to BD and leader according to snapshot |
|
||
| Cycle posted twice | Wallet idempotency prevents duplicate `USD_BALANCE` credit |
|
||
| Posting fails after partial success | Retry posts only unposted items |
|
||
| Posted item needs correction | Adjustment item created; original item remains immutable |
|
||
| Admin closes Agency | Agency hidden from App search; existing facts remain auditable |
|
||
|
||
## Critical Rules
|
||
|
||
- Admin owns policies and salary cycles; App owns user-initiated relationship flows.
|
||
- Wallet is the only owner of `USD_BALANCE` and withdrawal state.
|
||
- Posted salary items are immutable.
|
||
- BD/BD Leader base is downstream host salary only.
|
||
- Leader direct Agency salary source must be de-duplicated.
|
||
- Every admin mutation must be audited and idempotent.
|
||
- Redis cannot be the source of truth for policies, salary, relationships, or audit.
|