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

19 KiB
Raw Blame History

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-serviceUSD_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_idcommand_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

后台可以按区域和政策类型选择 dailyweeklysemi_monthlymonthly

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_idbd_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_ms within cycle。
  • Sum gift_point_received within 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_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

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 区分 bdbd_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_idbd_leader_user_id = leader.user_id;工资生成按 source_host_salary.salary_item_id 去重,所以这批 host 对该 leader 只算一次。下级 BD 拥有的 Agency 则设置 bd_user_id = bd.user_idbd_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_idsalary_item_idreceiver_rolesalary_componentpolicy_idpolicy_versionperiod_start_msperiod_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。重跑同一个 draft cycle 可以替换 draft items 或返回现有结果,不能重复插入。
  • Cycle approve 必须锁 cycle 当前状态为 draft
  • Posting 使用 salary_item_id 作为钱包幂等键。post_failed 重试只处理未 posted item。
  • Relationship admin action 要锁目标用户的 host_profilesagenciesbd_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_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.