hyapp-server/docs/host-agency-bd-admin-architecture.md
2026-05-01 00:32:00 +08:00

480 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Host, Agency And BD Admin Architecture
本文只定义后台/Admin 侧实现政策配置、工资周期、工资单、审核、钱包入账、调整单、后台审计和运营干预。App 侧的主播申请、Agency 成员、BD 邀请和用户可见查询见 [Host Agency BD App Architecture](./host-agency-bd-architecture.md)。
这里的工资统一指发放到主播侧 `USD_BALANCE` 的业务奖励。提现审核和人工打款仍由 `wallet-service` 拥有Admin 只负责把审核通过的工资单入账到工资余额,不直接处理线下打款。
## Goals
- 后台可以配置 host、agency、bd 三类工资政策,并按区域生效。
- 后台可以选择日结、周结、半月结、月结,不把结算周期写死在代码里。
- 后台可以生成工资周期,查看每个 host/Agency/BD/BD Leader 的明细来源。
- 后台审批后,`host-service` 以幂等方式调用 `wallet-service``USD_BALANCE` 入账。
- 后台可以对迟到事件、人工修正、争议处理创建调整单,不能直接修改已入账工资项。
- 后台可以创建或停用 BD Leader、BD、Agency也可以关闭 Agency所有动作必须有审计记录。
## Non-Goals
- 不在 Admin 里承载 App 长连接、房间麦位、房间核心状态或 IM 投递。
- 不让 `wallet-service` 理解 host/Agency/BD 政策。钱包只接收已审批的入账请求。
- 不用 Redis 保存政策、工资周期、工资单或审计事实。Redis 只能做短 TTL 查询缓存和互斥锁兜底。
- 不允许后台在不留审计的情况下改关系、改政策、改工资金额。
- 不允许已 posted 的工资单原地改金额;必须创建反向或补差调整项。
## Service Boundaries
```mermaid
graph LR
Admin["Admin Console"] --> Gateway["gateway-service"]
Gateway --> Host["host-service"]
Gateway --> User["user-service"]
Gateway --> Wallet["wallet-service"]
Host --> HostDB[("host MySQL")]
Wallet --> WalletDB[("wallet MySQL")]
User --> UserDB[("user MySQL")]
RoomOutbox[("room_outbox")] --> MQ["MQ / event bus"]
WalletOutbox[("wallet_outbox")] --> MQ
MQ --> Host
Host --> HostOutbox[("host_outbox")]
HostOutbox --> BI["BI / notification / risk"]
```
| Service | Admin Responsibility |
| --- | --- |
| `gateway-service` | Admin HTTP 入口、后台鉴权、request envelope、调用内部 gRPC |
| `user-service` | 用户、区域、账号状态查询;区域不由后台请求体伪造 |
| `room-service` | 只产出上麦和礼物事件,不接收后台工资命令 |
| `wallet-service` | `USD_BALANCE` 入账、提现冻结、提现审核状态和钱包流水 |
| `host-service` | 政策、周期、工资单、关系管理、统计聚合、审计、outbox |
## Admin Permission Model
后台权限至少拆成四类,避免一个运营账号同时拥有配置政策和发钱能力。
| Permission | Can Do | Cannot Do |
| --- | --- | --- |
| `host_relation_admin` | 创建/停用 BD Leader、BD、Agency关闭 Agency | 审批工资入账 |
| `salary_policy_admin` | 创建政策草稿、启停政策、配置周期 | 直接发工资 |
| `salary_reviewer` | 查看工资单、驳回、请求重算 | 调用钱包入账 |
| `salary_approver` | 审批工资周期、触发 posting | 修改政策和关系 |
所有后台命令必须写 `admin_audit_logs`。审计记录要包含 `request_id``command_id`、管理员用户、目标对象、前后状态摘要和原因。
```sql
admin_audit_logs(
audit_id BIGINT NOT NULL PRIMARY KEY,
command_id VARCHAR(96) NOT NULL,
admin_user_id BIGINT NOT NULL,
action VARCHAR(64) NOT NULL,
target_type VARCHAR(64) NOT NULL,
target_id VARCHAR(96) NOT NULL,
reason VARCHAR(512) NOT NULL DEFAULT '',
before_json JSON NULL,
after_json JSON NULL,
request_id VARCHAR(96) NOT NULL DEFAULT '',
created_at_ms BIGINT NOT NULL,
UNIQUE KEY uk_admin_audit_command (command_id)
);
```
## Policy Model
政策必须版本化。工资结算使用周期生成时锁定的 policy snapshot不能用当前 active policy 重算历史。
```sql
salary_policies(
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 (region_id, policy_type, status)
);
salary_policy_rules(
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 (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(
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 (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(
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 (region_id, cycle_type, period_start_ms, period_end_ms)
);
salary_items(
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 (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`
- Sum `mic_publishing_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. `host-service` 查询所有 `approved` salary items。
3. 对每个 item 调用 `wallet-service CreditSalaryBalance`,幂等键为 `salary:{salary_item_id}`
4. `wallet-service` credit `USD_BALANCE` 并返回 `wallet_transaction_id`
5. `host-service` 保存 `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(
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 HostAdminService {
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);
}
```
当前项目仍在开发阶段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 要锁目标用户的 `host_profiles``agencies``bd_profiles`,避免和 App 邀请接受并发造成身份冲突。
## Event And Outbox
`host-service` should write its own outbox:
| 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 admin notifications, BI, risk systems, and export jobs. They must not be required for salary facts to commit.
## 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](./host-agency-bd-implementation-sequence.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.