1名詞與鐵律
| 名詞 | 單位 | 定義 |
|---|---|---|
| 流水 gross | 鑽石 | 觀眾為這筆禮物實際扣掉的鑽石 = 單價 × 數量。這是唯一的收入基數。 |
| 血條分 score | 分 | PK 用。round(gross × 計分係數 × 倍率),只影響輸贏、榜單、曝光。 |
| 平台抽佣 | 鑽石 | 按「主播費率」計算(未設定則用全局費率),floor(gross × pct%)。費率只跟主播/合約走,不隨金額大小變動。 |
| 稅/通道費 | 鑽石 | 可選,floor(gross × tax%);預設 0。 |
| 主播實收 | 鑽石 | gross − 抽佣 − 稅。餘數歸主播,永不為負。 |
| 可提現 | 鑽石 | 已過 T+N 凍結期、扣掉已提現後仍留在主播帳上的金額。 |
| 倍率 | × | 僅 PK 期間有效(開場隨機 2–5、中途 1.5)。不參與任何金額運算。 |
四條鐵律(違反即為 bug)
- 金額與倍率物理隔離:
gross只由unit_price × qty產生。DB 的CHECK已寫死,程式想乘也乘不進去。 - 三項加總必須等於流水:
platform + tax + host = gross,一分不多一分不少(DB 約束)。 - 費率下單即鎖定:每張單記
rule_version_id與當時platform_pct;日後改費率不回寫歷史。 - 帳本只增不改:退款靠反向分錄沖銷,永不
UPDATE/DELETE既有分錄。每筆業務分錄借貸合計 = 0。
2分帳算法
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 血條分 | 主播實收(有倍率) |
|---|---|---|---|---|---|---|---|
| 心心 | 1 | 50% | 1 | 0 | 1 | 2 | 1 |
| 打氣 | 100 | 50% | 100 | 50 | 50 | 150 | 50 |
| 星光大道 | 688 | 50% | 688 | 344 | 344 | 1,032 | 344 |
| 金龍 | 9,999 | 50% | 9,999 | 4,999 | 5,000 | 14,999 | 5,000 |
最後兩欄就是你要的效果:血條衝得快,但主播後台看到的鑽石完全不變。9,999 鑽的大禮物一樣抽 50%,沒有「大額優惠」。
3資料模型
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_matches | PK 結束 | 只存比分與彙總流水,不重複記帳(明細在 gift_orders)。賽果不因退款改寫。 |
4落單與入帳時序
- 取現行費率版本 + 主播費率:
SELECT id, default_pct FROM commission_rule_versions WHERE effective_to IS NULL,再查host_commission_rates(host_id)。有主播費率就用它,否則用全局default_pct。整個請求只取一次,避免中途換版。 - 校驗:禮物上架、庫存 ≥ qty、觀眾鑽石 ≥ 單價 × qty、單筆上限。任一失敗即中止,不產生任何記錄。
- 同一交易內:扣觀眾鑽石 → 扣禮物庫存 →
INSERT gift_orders(只傳單價、數量、版本、倍率分)→fn_post_gift_order()寫 4 條分錄。 - DB trigger 算錢:
gross = unit × qty、pct = COALESCE(host_rate, default_pct)、floor抽佣、host = 餘數、算available_at。應用層不參與任何金額計算。費率不因金額大小變動。 - PK 計分:另寫
score_gain = round(gross × 每鑽分數 × 倍率)與score_multiplier,同一張單的pk_match_id指向場次。 - 回傳:訂單號、主播實收、剩餘鑽石。App 顯示的「主播實收」直接取資料庫回傳值,不在前端算。
- 冪等:客戶端帶
Idempotency-Key,落庫時對(user_id, idem_key)建唯一索引;重試回傳同一張單,不重複扣鑽、不重複入帳。
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%' -- 備註
);
- 優先級:
host_commission_rates→default_pct。主播有自訂就用自訂,冇就自動跟全局(改全局即時跟隨)。 - 批量與單獨同一條路徑:批量只是同一個交易內多寫幾行,共用
batch_id;失敗整批回滾,不會出現「改了一半」。 - 生效時間:新版本/新主播費率
effective_from = now(),立即對新單生效;已下的單永遠用舊費率(gift_orders.platform_pct已鎖)。 - 追溯:不允許。要補償請用
adjust分錄,保留審計軌跡。 - 變更紀錄:
commission_rate_changes只增不改(見第 14 節同款 trigger),批量操作記一條、列出全部主播與新舊值,方便日後對帳。 - 稅費:稅率是版本參數,跟抽佣一起版本化,避免「抽佣改了稅沒改」的半套狀態。
- App 顯示:前端只在「送禮前」讀
/api/rates/current?host_id=做試算顯示,實際金額以落庫回傳為準。
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/沖銷分錄。 |
fee_hkd = 手續費(固定 + 比例,可為 0)
hkd_net = hkd_gross − fee_hkd
匯率是版本參數 diamond_to_hkd,但提現單會再存一份 hkd_rate 快照 —— 日後調匯率不影響已提交的單。
7退款、沖銷與負餘額
- 退款不改原單金額,只把
status改refunded,並寫一組反向分錄(ref_type='refund')。 - 若主播已把這筆錢提走,主播帳面會變負 —— 允許負餘額,由下次收益自動抵扣;負餘額期間不可提現。
- PK 賽果不因退款改寫:
score_gain是已發生的比賽事實,退款只影響錢,不影響輸贏與榜單(除非你有「作弊刷分」判定,那走人工撤榜流程)。 - 退款窗口建議: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。
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
- 凍結影響面:L2/L3 有差異時,先把涉及的
host_id標記payout_hold=true,避免帶錯帳出去。 - 定位:用
ref_type + ref_id回查該業務的 4 條分錄;再看是否為重複入帳(UNIQUE已擋)、漏入帳(交易 rollback?)或人工改數。 - 修正:只准補
adjust分錄(正負成對),不准 UPDATE/DELETE 原分錄;原單金額永不改。 - 結案:
recon_diffs.status='resolved'+ 備註 + 操作人;同時解除payout_hold。 - 月結:本月所有
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驗收用例
| # | 情境 | 預期結果 |
|---|---|---|
| 1 | 100 鑽禮物,×1,全局 50% | gross 100 / platform 50 / host 50 / score 100 |
| 2 | 同一禮物,PK 中途 ×1.5 | gross 100 / platform 50 / host 50 / score 150(金額三項與用例 1 完全相同) |
| 3 | 9,999 鑽,開場 ×3.4 | gross 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 +1 | L3 對帳應抓出 diff = -1 並生成工單 |
| 13 | 提現 8,000 鑽(min 1,000,匯率 0.1) | hkd_gross 800.00,申請後 available −8,000、frozen +8,000,不可重複申請 |
12上線順序
- 建表:執行
hkplay-schema-gift-settlement.sql(純新增,不動現有表)。 - 雙寫觀察:App 送禮時同時寫
gift_orders,但收益仍用舊邏輯;跑 3 天,確認 L2 差異為 0。 - 切換真源:主播後台、結算報表改讀
gift_orders/ 帳本;舊欄位轉為唯讀。 - 開提現:先內部帳號小額測試(含駁回、負餘額、凍結退回)。
- 灰度費率:後台改費率走「開新版本」,觀察 24 小時的實際抽佣率是否等於設定值。
- 月結演練:完整跑一次 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 節講的是「錢怎麼從觀眾流到主播」。這一節講另一個入口:平台自己發鑽石給會員(測試額度、活動補償、客訴補償、對帳調整)。這條路同樣會產生負債,而且比禮物更容易被內部人濫用,所以它必須是雙人复核的,不能是「後台按一下就直接改餘額」。
14.1 角色與職責
| 角色 | 可否申請 | 可否審批 | 可以做什麼 |
|---|---|---|---|
operator 運營 | 可以 | 不可 | 提交補鑽/扣鑽申請、填原因與備註、撤回自己的單 |
supervisor 主管 | 可以 | 可以(≤ 10 萬) | 審批他人的申請;不可審批自己的 |
finance 財務 | 可以 | 可以(不限) | 大額(> 10 萬)必需由財務核准;負責月結與沖銷 |
admin 系統管理 | 可以 | 可以(不限) | 改政策限額、停用操作員;日常不應參與具體單據 |
「不可審批自己的」是資料庫層的約束(fn_adj_approve 直接比對 requested_by),不是前端隱藏按鈕。
14.2 資料模型
| 表 | 作用 | 不可變? |
|---|---|---|
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 | 審批人數達到 required_approvals(且財務要求滿足) | 寫入 applied_by;不動作餘額 |
approved → applied | trigger 自動,不是人手動按 | 寫帳本、更新餘額、回填 balance_before/after、applied_at |
pending → rejected | 審批人按駁回,必須填理由 | 不產生帳本分錄 |
pending → cancelled | 申請人自己撤回 | 不產生帳本分錄 |
applied → reversed | 財務開反向單 | 原單不動;新單沖一筆反向 delta,兩張單互相連結 |
14.4 分級授權矩陣
| 情境 | 需要幾人 | 角色要求 | 理由 |
|---|---|---|---|
| 派發 ≤ 10,000 鑽 | 1 人 | 主管以上 | 日常小額補償,別讓流程卡死客服 |
| 派發 10,001–100,000 鑽 | 2 人 | 主管以上,且互不相同 | 已是可觀金額,需要第二雙眼睛 |
| 派發 > 100,000 鑽 | 2 人 | 其中 1 人必須財務/管理員 | 大額等於直接發錢,財務要留痕 |
| 扣減(任何金額) | 至少 2 人 | 主管以上 | 扣鑽最容易引起客訴與內部盜用,一律雙簽 |
| 沖銷已入帳的單 | 財務 1 人 | finance / admin | 沖銷不改原單,責任清楚 |
| 改政策限額 | admin | admin | 改限額等於改風控,屬系統設定 |
14.5 提交與審批時序
- 填單:選會員(
member_id)、方向、數量、原因代碼、備註。前端同時產生一個Idempotency-Key(UUID),連按兩次不會變成兩筆。 - 建立 pending:
fn_adj_request()依金額算出required_approvals與need_finance,寫入申請單與第一筆request動作。 - 通知:推播/IM 給有審批權的人;同一張單不可由申請人自己出現在待審清單。
- 審批:
fn_adj_approve()檢查:不是自己的單、角色足夠、沒重複批同一張。每批一次寫一筆approve動作。 - 達標即入帳:人數夠 →
approved→ trigger 檢查餘額(扣減不可為負)→ 寫member_diamond_ledger→ 更新餘額 → 標記applied。 - 對帳:
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_check | 0 行;有行即差異工單 |
| 每張已入帳單都有分錄 | left join member_diamond_ledger | 0 行(孤兒單=錢不知去向) |
| 鑽石總負債 | v_diamond_liability | outstanding = 已發行 − 已消耗,對得上錢包總和 |
| 今日誰發了多少 | 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 上線順序
- 建表:執行
hkplay-schema-diamond-adjust.sql(含政策與 4 個示範員工)。 - 只開「派發」:先讓客服能用小額補償(≤ 1 萬、1 人批),觀察兩週。
- 開「扣減」:雙簽生效;同時把「直接改餘額」的權限從所有人身上收回。
- 接錢包:App 錢包改讀
member_diamond_balances;localStorage 只留作離線快取。 - 月結:把
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 | 付款方式 | ACH | ACH 自動轉賬 / FPS 轉數快 / TT 電匯。 |
| 9 | 付款用途 | 直播收益 | 按角色:貨款結算 / 配送費 / 車費結算。 |
| 10 | 附言 | PO202611050003 主播提現 | 顯示在收款人月結單,方便對帳。 |
| 11 | 付款人參考編號 | PO202611050003 | 唯一,用來對銀行回單。 |
| 12 | 手續費承擔 | SHA | SHA 共同 / OUR 付款方 / BEN 收款方。 |
中銀不同分行、不同網銀版本的範本欄位次序會有差異,所以後台把欄位做成可勾選、可調次序,匯出的 Excel 首行即表頭,直接對應「批量付款上載」。欄位設定存 localStorage,正式版存 bank_file_templates。
15.5 出款時序
- T0 申請:App 或客服提交 → 檢查最低提現、可提餘額、實名認證 → 建單並凍結金額。
- T0 審核:主管/財務核准或駁回;駁回即時解凍。
- T1 建批次:財務勾選已核准的單 → 一個批次 = 一個檔案,總額必須等於該批
Σ hkd_net。 - T1 匯出:下載
.xlsx(或 CSV)→ 上載中銀企業網銀 → 銀行覆核。 - T2 回單:銀行成功後回寫流水號,批次標記
paid;退票的單標failed並解凍。 - 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 上線順序
- 建表:執行
hkplay-schema-gift-settlement.sql的 8–9 節(payouts / payout_batches / payee_bank_accounts)。 - 先開放主播:小額(≤ 5,000 鑽)人工審核,財務手動出款,驗證中銀檔案能被網銀接受。
- 再開商戶/騎手/司機:接上各自的結算週期(商戶 4–8 日、騎手週結、司機日結)。
- 自動化:銀行回單以 CSV 導入 staging,自動匹配
bank_ref,差額建工單。 - 權限收口:出款權限只留財務,操作日誌保留 7 年(條款 3.6 帳目可查)。