480 lines
19 KiB
Markdown
480 lines
19 KiB
Markdown
# 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.
|