# 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.
