19 KiB
Host, Agency And BD Admin Architecture
本文只定义后台/Admin 侧实现:政策配置、工资周期、工资单、审核、钱包入账、调整单、后台审计和运营干预。App 侧的主播申请、Agency 成员、BD 邀请和用户可见查询见 Host Agency BD App Architecture。
这里的工资统一指发放到主播侧 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
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、管理员用户、目标对象、前后状态摘要和原因。
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 重算历史。
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。
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
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"]
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
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_mswithin cycle。 - Sum
gift_point_receivedwithin cycle。 - Sum
effective_day。 - 使用 stats 中的
agency_id/bd_user_id/bd_leader_user_id关系快照做归属。
规则匹配:
find highest host policy rule where:
mic_minutes >= min_mic_minutes
gift_points >= min_gift_points
effective_days >= min_effective_days
结果:
- 命中的规则产生
host_salaryitem。 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:
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 基数:
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 基数:
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,不能直接改钱包表。
- Admin 审批 cycle。
host-service查询所有approvedsalary items。- 对每个 item 调用
wallet-service CreditSalaryBalance,幂等键为salary:{salary_item_id}。 wallet-servicecreditUSD_BALANCE并返回wallet_transaction_id。host-service保存wallet_transaction_id,把 item 标记为posted。- 如果中途失败,只重试未 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。
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.
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
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。重跑同一个
draftcycle 可以替换 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。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_BALANCEand 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.