HK PLAY · TECHNICAL SPECIFICATION

禮物分帳與結算規格

Gift Revenue Split & Settlement — 資料表結構 · 分帳算法 · 對帳流程
版本v1.4(草案)
日期2026-11-05
對應原型hkplay-app-preview.html / hkplay-admin-portal.html
資料庫PostgreSQL 14+
配套檔案hkplay-schema-gift-settlement.sql
hkplay-schema-diamond-adjust.sql
一句話總結:觀眾付出的鑽石 = 主播的流水(1:1);PK 倍率只乘「血條分」,永遠不會乘到金額上。分帳由資料庫計算、費率下單即鎖定、帳本只增不改,四層對帳把差額壓到 0。

1名詞與鐵律

名詞單位定義
流水 gross鑽石觀眾為這筆禮物實際扣掉的鑽石 = 單價 × 數量。這是唯一的收入基數。
血條分 score分PK 用。round(gross × 計分係數 × 倍率),只影響輸贏、榜單、曝光。
平台抽佣鑽石按「主播費率」計算(未設定則用全局費率),floor(gross × pct%)。費率只跟主播/合約走,不隨金額大小變動。
稅/通道費鑽石可選,floor(gross × tax%);預設 0。
主播實收鑽石gross − 抽佣 − 稅。餘數歸主播,永不為負。
可提現鑽石已過 T+N 凍結期、扣掉已提現後仍留在主播帳上的金額。
倍率×僅 PK 期間有效(開場隨機 2–5、中途 1.5)。不參與任何金額運算。

四條鐵律(違反即為 bug)

  1. 金額與倍率物理隔離:gross 只由 unit_price × qty 產生。DB 的 CHECK 已寫死,程式想乘也乘不進去。
  2. 三項加總必須等於流水:platform + tax + host = gross,一分不多一分不少(DB 約束)。
  3. 費率下單即鎖定:每張單記 rule_version_id 與當時 platform_pct;日後改費率不回寫歷史。
  4. 帳本只增不改:退款靠反向分錄沖銷,永不 UPDATE/DELETE 既有分錄。每筆業務分錄借貸合計 = 0。

2分帳算法

gross = unit_price_diamond × qty
pct = host_rate(host_id) ?? rule_pct // 主播單獨費率優先;否則全局費率(現行 50%)
platform = floor(gross × pct / 100)
tax = floor(gross × tax_pct / 100)
host = gross − platform − tax // 餘數歸主播

選 floor 而不是四捨五入的原因:① 主播實收永遠是整數且不為負;② 平台不會因為 0.5 鑽而抽走小禮物的全部價值(1 鑽禮物在 50% 費率下平台抽 0)。三項加總天然等於流水,對帳不會出現 1 鑽的鬼差。

費率政策(v1.2 · 與後台「💰 收益與抽佣」一致)

項目費率說明
全局默認平台 50% / 主播 50%所有主播的起點費率,2026-11 起生效。
主播單獨費率0 – 90%(可自訂)按合約/等級逐個設定,優先於全局;後台可單獨改,亦可勾選多個主播批量套用同一個費率。
大額訂單同費率不讓利:送 100 鑽與送 100 萬鑽,同一主播的費率完全一樣。平台不因金額大而降低抽佣。
進階分層預設關閉只為特殊合約保留(commission_tiers);一旦啟用且某層低於全局,即屬「大額讓利」,需財務書面批准。
為何以主播為維度,而不是以禮物金額為維度:同一個禮物可能由不同主播收,平台對不同主播的議價能力不同(新人/簽約/頭部);而金額大小與平台成本無關(支付通道費按儲值計,不按禮物單價計)。所以費率掛在主播身上,金額只影響流水,不影響費率。

實算對照(單份 · 全局 50%)

禮物單價抽佣%流水平台主播實收PK ×1.5 血條分主播實收(有倍率)
心心150%10121
打氣10050%100505015050
星光大道68850%6883443441,032344
金龍9,99950%9,9994,9995,00014,9995,000

最後兩欄就是你要的效果:血條衝得快,但主播後台看到的鑽石完全不變。9,999 鑽的大禮物一樣抽 50%,沒有「大額優惠」。

3資料模型

commission_rule_versions version · effective_from default_pct · tax_pct diamond_to_hkd · T+N commission_tiers up_to_diamond platform_pct seq gift_orders 送禮流水 · 唯一收入來源 unit_price_diamond × qty = gross_diamond rule_version_id · platform_pct(快照) platform_diamond / tax_diamond / host_diamond score_multiplier · score_gain 不參與金額 status · available_at · pk_match_id CHECK gross = platform + tax + host CHECK gross = unit_price × qty host_ledger account_code amount_diamond 正貸負借 ref_type + ref_id(冪等) 每筆業務 Σ = 0 append-only host_balances frozen / available / paid_out 可由帳本完全重算 (對帳基準) pk_matches score_l / score_r opening_multiplier gross_l / gross_r(彙總) 1:N 下單引用版本 選層 借貸分錄 餘額 僅比分 衍生表(不重複記帳):settlement_batches 日結 · recon_diffs 差異工單 · payouts 提現

v1.3 新增 host_commission_rates(主播單獨費率,優先於全局)與 commission_rate_changes(變更紀錄,append-only);為保持圖面可讀,未在圖中單獨畫出,關係見下表。

完整 DDL(含 trigger、view、種子):hkplay-schema-gift-settlement.sql · 在瀏覽器開啟 | 第 14 節的 DDL:hkplay-schema-diamond-adjust.sql · 在瀏覽器開啟

每張表在管什麼

表寫入時機職責/注意
commission_rule_versions改全局費率時新增費率版本 + 全局參數(默認 50%、匯率、T+N、最低提現)。同時只允許一個 effective_to IS NULL 的現行版本。
host_commission_rates主播費率變更主播單獨費率(0–90%),優先於全局。可逐個寫入,亦可一個批次 batch_id 批量寫入多個主播;只影響之後的新單。歷史查 commission_rate_changes。
commission_tiers跟版本一起進階分層,預設只有一層 50%。每版最多一個無上限層(up_to_diamond IS NULL)。任何低於全局的層視為「大額讓利」,需財務批准。
gift_orders每次送禮核心表。金額由 trigger 算、由 CHECK 鎖死。是收益唯一的計數單位。
host_ledger送禮 / 退款 / 提現雙分錄帳本,正負號表示貸借。每筆業務合計 0,UNIQUE(ref_type, ref_id, account_code) 保證重試不會重複入帳。
host_balances由帳本彙總快取餘額(frozen / available / paid_out)。任何時候都必須等於帳本重算結果,否則出 recon_diffs。
settlement_batches每日 03:00日結快照。差異不為 0 時該批不得轉 reconciled。
payouts主播申請提現提現單。鎖定當時匯率,記錄手續費與銀行流水號。
recon_diffs對帳任務差異工單。原單不可改,只能人工判定後補 adjust 分錄。
pk_matchesPK 結束只存比分與彙總流水,不重複記帳(明細在 gift_orders)。賽果不因退款改寫。

4落單與入帳時序

  1. 取現行費率版本 + 主播費率:SELECT id, default_pct FROM commission_rule_versions WHERE effective_to IS NULL,再查 host_commission_rates(host_id)。有主播費率就用它,否則用全局 default_pct。整個請求只取一次,避免中途換版。
  2. 校驗:禮物上架、庫存 ≥ qty、觀眾鑽石 ≥ 單價 × qty、單筆上限。任一失敗即中止,不產生任何記錄。
  3. 同一交易內:扣觀眾鑽石 → 扣禮物庫存 → INSERT gift_orders(只傳單價、數量、版本、倍率分)→ fn_post_gift_order() 寫 4 條分錄。
  4. DB trigger 算錢:gross = unit × qty、pct = COALESCE(host_rate, default_pct)、floor 抽佣、host = 餘數、算 available_at。應用層不參與任何金額計算。費率不因金額大小變動。
  5. PK 計分:另寫 score_gain = round(gross × 每鑽分數 × 倍率) 與 score_multiplier,同一張單的 pk_match_id 指向場次。
  6. 回傳:訂單號、主播實收、剩餘鑽石。App 顯示的「主播實收」直接取資料庫回傳值,不在前端算。
  7. 冪等:客戶端帶 Idempotency-Key,落庫時對 (user_id, idem_key) 建唯一索引;重試回傳同一張單,不重複扣鑽、不重複入帳。
WebSocket/PK 場次要注意:PK 的倍率在場次開始時抽定(opening_multiplier),送禮當下由伺服器依場次時鐘判斷「此刻是哪一段」,不可信客戶端上報的倍率。客戶端只負責顯示。

5費率版本化與生效

費率有兩層:全局(所有主播的默認)與主播單獨(按合約/等級)。兩者都不是「改舊值」,而是留痕跡:全局關舊開新版本,主播費率變更寫一筆 append-only 紀錄。

-- A. 改全局:關舊開新(此刻之前下的單仍指向舊版)
UPDATE commission_rule_versions SET effective_to = now() WHERE effective_to IS NULL;

INSERT INTO commission_rule_versions (version, default_pct, tax_pct, diamond_to_hkd,
                                      min_payout_diamond, settle_t_days, created_by, note)
VALUES (2, 50, 0, 0.100, 1000, 7, 'admin:1001', '2026-11 政策:統一 50%,大額不讓利');
-- 分層預設只有一個「無上限」層,即所有單價同費率
INSERT INTO commission_tiers (rule_version_id, seq, up_to_diamond, platform_pct)
VALUES (currval('commission_rule_versions_id_seq'), 1, NULL, 50);

-- B. 單獨設定某位主播(優先於全局)
INSERT INTO host_commission_rates (host_id, platform_pct, effective_from, created_by, note)
VALUES ('host_10086', 45, now(), 'admin:1001', '簽約主播合約價')
ON CONFLICT (host_id) DO UPDATE
  SET platform_pct = EXCLUDED.platform_pct,
      effective_from = EXCLUDED.effective_from,
      created_by = EXCLUDED.created_by,
      note = EXCLUDED.note;

-- C. 批量:勾選多個主播套同一費率(一個 batch_id 一次寫入)
SELECT fn_set_host_rates(
  ARRAY['host_10086','host_10087','host_10088'],  -- 主播
  50,                                             -- 平台抽佣 %
  'admin:1001',                                   -- 操作人
  '雙 11 前統一 50%'                               -- 備註
);

6凍結期、可提現、提現

階段觸發動作
凍結 frozen送禮成功主播餘額 +host_diamond,狀態 frozen;available_at = created_at + T。
解凍 available排程每 10 分鐘UPDATE ... WHERE available_at <= now() AND status='settled':frozen → available。
申請提現 requested主播手動檢查 available ≥ min_payout;提交即把該筆鑽石從 available 移入凍結(防重複申請),生成 payouts。
審核 / 打款 paid財務寫 ledger(ref_type='payout', 負值),paid_out_diamond += ,記錄銀行流水號。
駁回 rejected風控 / 資料錯鑽石由凍結退回 available,記 payout_fee/沖銷分錄。
hkd_gross = diamond_amount × hkd_rate  // 匯率鎖定在申請那一刻
fee_hkd = 手續費(固定 + 比例,可為 0)
hkd_net = hkd_gross − fee_hkd

匯率是版本參數 diamond_to_hkd,但提現單會再存一份 hkd_rate 快照 —— 日後調匯率不影響已提交的單。

7退款、沖銷與負餘額

  1. 退款不改原單金額,只把 status 改 refunded,並寫一組反向分錄(ref_type='refund')。
  2. 若主播已把這筆錢提走,主播帳面會變負 —— 允許負餘額,由下次收益自動抵扣;負餘額期間不可提現。
  3. PK 賽果不因退款改寫:score_gain 是已發生的比賽事實,退款只影響錢,不影響輸贏與榜單(除非你有「作弊刷分」判定,那走人工撤榜流程)。
  4. 退款窗口建議:T+7 內可退(未過凍結期),逾時只接受爭議單。這條要寫進用戶協議。

8PK 與收益的隔離(架構層保證)

血條分(可乘倍率)

score_gain = round(gross × score_per_diamond × score_multiplier)

欄位:score_multiplier、score_gain、pk_matches.score_l/r

用途:勝負、榜單、曝光加權。可被退款以外的任何規則調整。

真實金額(不可乘倍率)

gross = unit_price × qty(DB CHECK 寫死)

欄位:gross_diamond、platform_diamond、tax_diamond、host_diamond

沒有任何欄位可以被倍率影響;trigger 也不讀 score_multiplier。

最常見的錯法(請在 code review 擋住):把 score_gain 當成「收入分」去算抽佣,或在 App 前端用「血條分 ÷ 每鑽分數」反推主播收益。兩者在有倍率的場次會差 1.5–5 倍,且對帳一定對不上。

9對帳:四層 + 差異處理

層頻率檢查內容不通過時
L1每筆(寫入時)DB CHECK:gross = unit×qty、gross = platform+tax+host、帳本每筆業務 Σ=0寫入直接失敗(阻斷)
L2每日 03:00訂單 vs 帳本:v_recon_order_vs_ledger 的 diff_host / diff_platform 必須全 0生成 recon_diffs(kind='order_vs_ledger')
L3每日 03:10餘額 vs 帳本:v_recon_balance 的 diff 必須全 0(frozen+available+paid_out 應等於帳本累計)生成 kind='balance_vs_ledger',該主播暫停提現
L4每週 / 每月渠道對帳(儲值側):Apple / Google / Stripe 實收 − 平台手續費 = 已發放鑽石成本;提現側:銀行回單 vs payouts財務線下工單

差異處理 SOP

  1. 凍結影響面:L2/L3 有差異時,先把涉及的 host_id 標記 payout_hold=true,避免帶錯帳出去。
  2. 定位:用 ref_type + ref_id 回查該業務的 4 條分錄;再看是否為重複入帳(UNIQUE 已擋)、漏入帳(交易 rollback?)或人工改數。
  3. 修正:只准補 adjust 分錄(正負成對),不准 UPDATE/DELETE 原分錄;原單金額永不改。
  4. 結案:recon_diffs.status='resolved' + 備註 + 操作人;同時解除 payout_hold。
  5. 月結:本月所有 settlement_batches 必須 reconciled,且 sum(diff_diamond) = 0,財務才可出報表。

每日對帳 SQL(可直接排程)

-- L2:訂單 vs 帳本
INSERT INTO recon_diffs (biz_date, kind, ref, expected, actual)
SELECT current_date, 'order_vs_ledger', order_no::text, host_diamond, ledger_host
  FROM v_recon_order_vs_ledger
 WHERE diff_host <> 0 OR diff_platform <> 0
ON CONFLICT DO NOTHING;

-- L3:餘額 vs 帳本
INSERT INTO recon_diffs (biz_date, kind, ref, expected, actual)
SELECT current_date, 'balance_vs_ledger', host_id::text, ledger_total, book_total
  FROM v_recon_balance
 WHERE diff <> 0
ON CONFLICT DO NOTHING;

-- 日結批次
INSERT INTO settlement_batches (biz_date, orders_count, gross_diamond, platform_diamond, tax_diamond, host_diamond, status)
SELECT created_at::date, count(*), sum(gross_diamond), sum(platform_diamond), sum(tax_diamond), sum(host_diamond), 'closed'
  FROM gift_orders WHERE status = 'settled' AND created_at::date = current_date - 1
 GROUP BY 1
ON CONFLICT (biz_date) DO NOTHING;

10要盯的指標

指標公式用途
實際抽佣率Σplatform / Σgross與設定值對照;偏差 > 2 個百分點代表用戶集中在大額層
主播實收率Σhost / Σgross對外溝通、主播招募
退款率Σrefund_gross / Σgross> 3% 要查刷單
可提現負債Σ(available + frozen)現金流準備金,週報必看
對帳差異筆數count(recon_diffs where open)必須為 0;不為 0 不得月結
PK 場均流水Σgross(pk) / 場次評估倍率規則是否刺激消費(看錢,不看血條)

11驗收用例

#情境預期結果
1100 鑽禮物,×1,全局 50%gross 100 / platform 50 / host 50 / score 100
2同一禮物,PK 中途 ×1.5gross 100 / platform 50 / host 50 / score 150(金額三項與用例 1 完全相同)
39,999 鑽,開場 ×3.4gross 9,999 / platform 4,999 / host 5,000 / score 33,997(大額一樣 50%,不讓利)
4金額不影響費率:100 vs 100,000 鑽兩者同一主播,費率都是 50%(platform 50 / 50,000),後台無「大額優惠」入口
5極小額:1 鑽 × 10 份gross 10 / platform 5 / host 5;單筆 1 鑽時 platform 0 / host 1(floor 的預期行為,平台不抽走小禮物)
6主播 A 單獨設 45%,主播 B 跟全局A:100 鑽 → platform 45 / host 55;B:同一禮物 → platform 50 / host 50
7批量把 3 位主播設為 45%3 位即時生效;只產生一條變更紀錄(batch_id 相同、列出 3 人與新舊值);失敗整批回滾
8刪除主播自訂費率即刻跟隨全局;之後改全局,該主播自動跟隨
9改費率後查舊單舊單 platform_pct、host_diamond 不變(含主播單獨費率變更);新單用新費率
10重複提交同一 Idempotency-Key只產生 1 張單、1 組分錄;第二次回傳同一 order_no
11退款一張已提現的單帳本沖銷、主播餘額可為負、不可再提現、PK 比分不變
12手動把 host_balances.available +1L3 對帳應抓出 diff = -1 並生成工單
13提現 8,000 鑽(min 1,000,匯率 0.1)hkd_gross 800.00,申請後 available −8,000、frozen +8,000,不可重複申請

12上線順序

  1. 建表:執行 hkplay-schema-gift-settlement.sql(純新增,不動現有表)。
  2. 雙寫觀察:App 送禮時同時寫 gift_orders,但收益仍用舊邏輯;跑 3 天,確認 L2 差異為 0。
  3. 切換真源:主播後台、結算報表改讀 gift_orders / 帳本;舊欄位轉為唯讀。
  4. 開提現:先內部帳號小額測試(含駁回、負餘額、凍結退回)。
  5. 灰度費率:後台改費率走「開新版本」,觀察 24 小時的實際抽佣率是否等於設定值。
  6. 月結演練:完整跑一次 L1–L4 + 月結報表,再對外公佈。

13待你拍板

題目選項與影響
抽佣是否按主播分級已定:B。全局 50% 為底,host_commission_rates 按主播覆寫(0–90%),後台可單獨或批量設定;不設金額分層優惠。
公會分成建議獨立一層:平台 → 公會 → 主播,避免把公會比例混進抽佣。
退款政策PK 期間送禮是否可退?建議「未過凍結期可退、已用於 PK 的不可退」,寫入協議。
稅/通道費位置Apple / Google 的 30% 屬於儲值側成本,別在禮物側再扣一次,否則重複計費。
提現手續費固定 HK$X 還是 %?建議固定 + 每月首筆免手續費。
T+N 與最低提現現為 T+7 / 1,000 鑽。若要搶主播,可改 T+3 / 500 鑽,但現金流壓力上升。

14會員鑽石派發與審批

第 1–13 節講的是「錢怎麼從觀眾流到主播」。這一節講另一個入口:平台自己發鑽石給會員(測試額度、活動補償、客訴補償、對帳調整)。這條路同樣會產生負債,而且比禮物更容易被內部人濫用,所以它必須是雙人复核的,不能是「後台按一下就直接改餘額」。

為什麼不能直接 UPDATE 餘額:①對不到帳——報表說發了 10 萬,錢包只多了 8 萬,查不出是哪一筆;②可以自批——同一個人申請又核准,出事沒有第二雙眼睛;③答不出「誰給的」——監管或審計一問就卡住。所以正式版的餘額只能由帳本推導,直接改餘額的 SQL 應該被權限擋掉。

14.1 角色與職責

角色可否申請可否審批可以做什麼
operator 運營可以不可提交補鑽/扣鑽申請、填原因與備註、撤回自己的單
supervisor 主管可以可以(≤ 10 萬)審批他人的申請;不可審批自己的
finance 財務可以可以(不限)大額(> 10 萬)必需由財務核准;負責月結與沖銷
admin 系統管理可以可以(不限)改政策限額、停用操作員;日常不應參與具體單據

「不可審批自己的」是資料庫層的約束(fn_adj_approve 直接比對 requested_by),不是前端隱藏按鈕。

14.2 資料模型

staff_accounts role / active operator·supervisor ·finance·admin members nickname / status (沿用既有會員表) diamond_adjustments 申請單 direction(in/out) · amount > 0 status · required_approvals idempotency_key(UNIQUE) balance_before / balance_after reverses_id / reversed_by_id requested_by → staff_accounts member_id → members …_adjustment_actions request/approve/reject /cancel/apply/reverse actor_role(快照) append-only member_diamond_ledger delta · balance_after ref_type/ref_id(冪等) append-only member_diamond_balances 餘額快取 · 可由帳本重算 提交 指定會員 審批動作 核准後入帳 餘額
表作用不可變?
diamond_adjustments申請單:誰、給誰、多少、為什麼、批到第幾關入帳後金額鎖死,只能開反向單
diamond_adjustment_actions每一次審批動作(含角色快照),用來證明「有幾個人真的看過」append-only(UPDATE/DELETE 直接報錯)
member_diamond_ledger鑽石帳本:delta + balance_after,與 gift_orders 共用同一本帳append-only
member_diamond_balances餘額快取,只為查詢速度;對帳時以帳本重算為準可由帳本完全重建
diamond_adjustment_policy單筆級距、每日上限、是否禁止負數、冷卻秒數單行設定,改動留 updated_at

14.3 狀態機

pending 待審批 approved 審批通過 applied 已入帳 reversed 已沖銷 rejected 已駁回 cancelled 申請人撤回 批足人數 trigger 入帳 反向單 駁回 撤回
轉換誰觸發同時發生什麼
pending → approved審批人數達到 required_approvals(且財務要求滿足)寫入 applied_by;不動作餘額
approved → appliedtrigger 自動,不是人手動按寫帳本、更新餘額、回填 balance_before/after、applied_at
pending → rejected審批人按駁回,必須填理由不產生帳本分錄
pending → cancelled申請人自己撤回不產生帳本分錄
applied → reversed財務開反向單原單不動;新單沖一筆反向 delta,兩張單互相連結
為什麼要有 approved 這個中間態:真實系統常需要「批了但先不入帳」(例如等財務放行、或跨日生效)。把「批」與「入帳」分開,日後要加「預約生效」不必改狀態機。目前的做法是批完立刻自動入帳,兩個狀態相隔毫秒,但結構上留了餘地。

14.4 分級授權矩陣

情境需要幾人角色要求理由
派發 ≤ 10,000 鑽1 人主管以上日常小額補償,別讓流程卡死客服
派發 10,001–100,000 鑽2 人主管以上,且互不相同已是可觀金額,需要第二雙眼睛
派發 > 100,000 鑽2 人其中 1 人必須財務/管理員大額等於直接發錢,財務要留痕
扣減(任何金額)至少 2 人主管以上扣鑽最容易引起客訴與內部盜用,一律雙簽
沖銷已入帳的單財務 1 人finance / admin沖銷不改原單,責任清楚
改政策限額adminadmin改限額等於改風控,屬系統設定

14.5 提交與審批時序

  1. 填單:選會員(member_id)、方向、數量、原因代碼、備註。前端同時產生一個 Idempotency-Key(UUID),連按兩次不會變成兩筆。
  2. 建立 pending:fn_adj_request() 依金額算出 required_approvals 與 need_finance,寫入申請單與第一筆 request 動作。
  3. 通知:推播/IM 給有審批權的人;同一張單不可由申請人自己出現在待審清單。
  4. 審批:fn_adj_approve() 檢查:不是自己的單、角色足夠、沒重複批同一張。每批一次寫一筆 approve 動作。
  5. 達標即入帳:人數夠 → approved → trigger 檢查餘額(扣減不可為負)→ 寫 member_diamond_ledger → 更新餘額 → 標記 applied。
  6. 對帳:v_diamond_balance_check 必須 0 行;每日把 Σ adjustment 與錢包端的變動比對。

14.6 六條防線

① 不可自批

資料庫比對 requested_by = actor_id,同一人也不行重複核准。

② 不可改帳本

member_diamond_ledger 與 ..._actions 都掛了 UPDATE/DELETE 直接報錯的 trigger。

③ 不可扣成負數

入帳前 FOR UPDATE 鎖餘額列再檢查,不足就整筆失敗,不會出現負錢包。

④ 不可重複提交

idempotency_key UNIQUE;重送只回原本那張單。

⑤ 不可無限發

單一操作員每日上限、單一會員每日上限;超過要升級審批或隔日再辦。

⑥ 不可無痕

申請、核准、駁回、撤回、沖銷全留 actor 與 actor_role 快照,角色日後被改也不影響歷史。

14.7 對帳與報表

檢查SQL/視圖期望
餘額 = 帳本加總v_diamond_balance_check0 行;有行即差異工單
每張已入帳單都有分錄left join member_diamond_ledger0 行(孤兒單=錢不知去向)
鑽石總負債v_diamond_liabilityoutstanding = 已發行 − 已消耗,對得上錢包總和
今日誰發了多少v_staff_daily_usage不超過 daily_limit_per_staff
大額異動v_adj_large每日專人看一遍
待審批積壓v_adj_pending超過 24 小時未處理要提醒
-- 每日對帳:發出去的鑽石 vs 錢包實際增加
SELECT b.member_id,
       b.balance                        AS wallet_balance,
       COALESCE(l.sum_delta, 0)         AS ledger_sum,
       b.balance - COALESCE(l.sum_delta, 0) AS diff
  FROM member_diamond_balances b
  LEFT JOIN (SELECT member_id, sum(delta) sum_delta
               FROM member_diamond_ledger GROUP BY member_id) l USING (member_id)
 WHERE b.balance <> COALESCE(l.sum_delta, 0);     -- 必須 0 行

14.8 與禮物、等級、PK 的邊界

項目補鑽會影響嗎說明
累計充值 / 財富等級不會補鑽不是充值。若計入,等於可以「刷等級」,也讓營收數字失真。
粉絲燈牌親密值不會親密值只由「實際送禮」產生,避免用補鑽刷燈牌。
主播收益 / 抽佣不會沒有禮物就沒有流水;補鑽本身不產生主播收入。
PK 血條不會補鑽不入 PK 計分;PK 只認送禮事件。
平台鑽石負債會補出去的每一顆都是負債,必須計入 v_diamond_liability。
退款 / 沖銷走反向單不刪原單;沖銷後餘額可回到補之前,但歷史完整保留。
一顆鑽石的壽命要能講清楚:發行(充值或派發)→ 持有 → 消耗(送禮)→ 主播收益 → 平台抽佣。任何一個環節少了帳本分錄,月結一定對不上。補鑽是「發行」,不是「憑空多出來的餘額」。

14.9 驗收用例(新增)

#情境預期結果
11運營提交 5,000 鑽(operator)狀態 pending、需 1 人核准;餘額未變
12同一人按核准拒絕:不可審批自己的申請;單仍在 pending
13主管核准 5,000 鑽狀態 applied、帳本 +5,000、餘額 +5,000、balance_before/after 已回填
14扣減 3,000 鑽,僅 1 人核准仍為 pending(扣減永遠需 2 人)
15扣減 999,999 鑽(餘額只有 5,000)入帳失敗並回滾,餘額不變,單可改為駁回
16同一 Idempotency-Key 連送 3 次只產生 1 張單,三次回傳同一個 adj_no
17直接 UPDATE member_diamond_ledger報錯:帳本只增不改不刪
18沖銷一張已入帳的 5,000 鑽新增 −5,000 分錄、原單標 reversed、餘額回到原值;原單金額不變

14.10 上線順序

  1. 建表:執行 hkplay-schema-diamond-adjust.sql(含政策與 4 個示範員工)。
  2. 只開「派發」:先讓客服能用小額補償(≤ 1 萬、1 人批),觀察兩週。
  3. 開「扣減」:雙簽生效;同時把「直接改餘額」的權限從所有人身上收回。
  4. 接錢包:App 錢包改讀 member_diamond_balances;localStorage 只留作離線快取。
  5. 月結:把 v_diamond_liability 與 v_diamond_balance_check 納入月結報告,差異必須 0。

15提現與出款(中國銀行批量付款)

第 6 節講的是「主播什麼時候可以提」,這一節講「錢怎麼真的離開公司」。四個角色共用同一條線:主播(鑽石收益)、商戶(貨款)、騎手(配送費)、司機(車費)。全部由平台以中國銀行(香港)公司帳戶統一發放,財務經手、雙人复核。

15.1 狀態機

狀態由誰觸發動作與資金影響
pending 待審核收款方 / 客服代辦建立提現單,金額即從 available 移入凍結(防重複申請);不動帳本。
approved 已核准主管或財務核准通過,進入「待出款」。申請人不可自批;營運角色無核准權。
paid 已出款財務寫 ledger(ref_type='payout') 負值、回寫銀行流水號、批次標記完成。
rejected 已駁回主管或財務凍結解除、金額退回 available;必填原因,App 可見。
failed 銀行退回財務銀行退票(戶名/賬號不符)。金額退回 available,可修正資料後重提。

paid 與 failed 都由「批次」驅動:一個批次 = 一個銀行檔案 = 一次上載。批次內單筆失敗只影響該筆,其餘照常入帳。

15.2 角色矩陣

動作營運 operator主管 supervisor財務 finance
代客提交提現申請✓✓✓
核准 / 駁回✗✓✓
核准自己提交的單✗✗✗
建立出款批次 / 匯出中銀檔案✗✗✓
標記已出款 / 銀行退回✗✗✓
改手續費 / 付款戶口✗✗✓

15.3 金額、費用與凍結

項目規則說明
可提現已過 T+N 且未被佔用主播:Σ host_diamond(available_at ≤ now) − 待審核 − 已核准 − 已出款。商戶/騎手/司機用港幣帳。
凍結中不可提① 未過 T+N 的收益;② 已提交但未出款的金額。App 與後台顯示同一個數。
最低提現主播 1,000 鑽 · 其他 HK$100與第 6 節一致,跟隨後台設定。
匯率1 鑽 = HK$0.1提交時快照存 hkd_rate;之後改匯率不影響已提交的單。
手續費固定 HK$ + 百分比net = gross − fee;目前 0(推廣期免手續費)。負數一律夾成 0。
實付寫入銀行檔案的金額永遠是 hkd_net,不是申請金額,也不是含手續費的毛額。

15.4 中銀批量付款檔案(預設欄位)

#欄位範例說明
1序號1由 1 連續遞增,不可跳號。
2收款人賬戶號碼012-888-12345678文字格式,保留前導零;不含空格。
3收款人名稱CHAN MEI LING與銀行開戶名完全一致(英文大寫),否則退票。
4收款銀行代號012中銀香港 012、滙豐 004、恒生 024、渣打 003。
5收款分行代號812沒有填 000。
6金額500.00數字格式、兩位小數、幣別 HKD。
7幣別HKD固定 HKD。
8付款方式ACHACH 自動轉賬 / FPS 轉數快 / TT 電匯。
9付款用途直播收益按角色:貨款結算 / 配送費 / 車費結算。
10附言PO202611050003 主播提現顯示在收款人月結單,方便對帳。
11付款人參考編號PO202611050003唯一,用來對銀行回單。
12手續費承擔SHASHA 共同 / OUR 付款方 / BEN 收款方。

中銀不同分行、不同網銀版本的範本欄位次序會有差異,所以後台把欄位做成可勾選、可調次序,匯出的 Excel 首行即表頭,直接對應「批量付款上載」。欄位設定存 localStorage,正式版存 bank_file_templates。

15.5 出款時序

  1. T0 申請:App 或客服提交 → 檢查最低提現、可提餘額、實名認證 → 建單並凍結金額。
  2. T0 審核:主管/財務核准或駁回;駁回即時解凍。
  3. T1 建批次:財務勾選已核准的單 → 一個批次 = 一個檔案,總額必須等於該批 Σ hkd_net。
  4. T1 匯出:下載 .xlsx(或 CSV)→ 上載中銀企業網銀 → 銀行覆核。
  5. T2 回單:銀行成功後回寫流水號,批次標記 paid;退票的單標 failed 並解凍。
  6. T3 對帳:銀行月結單 Σ 必須等於 Σ payouts(hkd_net where status='paid'),差額建工單。
為什麼要「批次」而不是逐筆出款:銀行按檔案收費、按批覆核。逐筆出款等於每次都要人手登入網銀,出錯無人覆核;批次化之後,一個檔案對應一張審批紀錄,月結只要對批次總額。

15.6 銀行退回與重出

情況處理
戶名與賬號不符標 failed、金額解凍;通知收款方更新銀行資料後重提,不可在原單上改帳號。
賬戶已關閉 / 凍結同上,並在收款方檔案標 payout_hold,人工核實前不可再提。
銀行已扣款但回單未到批次保持 approved,不得重複匯出(同一批次只能有一個成功檔案),待回單確認。
重複上載同一批次銀行端以「付款人參考編號」去重;系統端批次已 paid 即鎖定,不能再次標記。

15.7 對帳視圖

-- 批次 vs 明細:總額必須相等,筆數必須相等
SELECT b.batch_no,
       b.total_hkd,
       sum(p.hkd_net)              AS sum_detail,
       count(*)                    AS n_detail,
       b.total_hkd - sum(p.hkd_net) AS diff       -- 必須 0
  FROM payout_batches b
  JOIN payouts p ON p.batch_id = b.id
 GROUP BY b.id, b.batch_no, b.total_hkd;

-- 已出款 vs 銀行回單(回單先入 staging 表)
SELECT p.payout_no, p.hkd_net, s.amount, p.hkd_net - s.amount AS diff
  FROM payouts p
  LEFT JOIN bank_statements s ON s.ref = p.bank_ref
 WHERE p.status = 'paid' AND (s.amount IS NULL OR s.amount <> p.hkd_net);

15.8 驗收用例(新增)

#情境預期結果
19主播可提 81,400 鑽,申請 5,000 鑽建單 pending、可提變 76,400、凍結 +5,000、HK$500.00
20申請 500 鑽(低於 1,000)拒絕:低於最低提現,不建單
21申請超過可提(含凍結中)拒絕:超出可提現餘額;凍結中金額不可提
22營運 amy 核准自己提交的單拒絕:不可自批(先於權限檢查)
23財務建立批次並匯出 Excel筆數/總額與批次一致;賬號為文字、保留前導零
24營運嘗試標記已出款拒絕:只有財務可出款
25標記銀行退回狀態 failed、金額解凍、可重新申請
26批次匯出後再匯出一次允許(同一檔案可重下),但批次標記 paid 後不可再改

15.9 上線順序

  1. 建表:執行 hkplay-schema-gift-settlement.sql 的 8–9 節(payouts / payout_batches / payee_bank_accounts)。
  2. 先開放主播:小額(≤ 5,000 鑽)人工審核,財務手動出款,驗證中銀檔案能被網銀接受。
  3. 再開商戶/騎手/司機:接上各自的結算週期(商戶 4–8 日、騎手週結、司機日結)。
  4. 自動化:銀行回單以 CSV 導入 staging,自動匹配 bank_ref,差額建工單。
  5. 權限收口:出款權限只留財務,操作日誌保留 7 年(條款 3.6 帳目可查)。