feat(revenue): immediate commission credit with independent withdrawable pool

Aligns affiliate commission with the reference theme: commission is credited
to a dedicated COMMISSION asset pool the moment an order is paid, removing the
7-day settlement freeze, the settlement cron, and the settlement_days setting.

- Ledger: add AssetType::COMMISSION (isolated from the recharge balance wallet)
- CommissionLedger: credit-on-record + opportunistic debt repayment; refunds
  claw back the full commission from the pool, carrying any shortfall as debt
  (aksrt_commission_debt) that future commission pays down first
- Withdrawal: make asset-aware via a source discriminator (commission vs
  balance); each pool keeps independent daily limits, pending gate, and
  reject-refund. Balance withdrawal (recharge + creator income) is unchanged
- Fix pay_rebate_self_s: honor self-commission at purchase time
- Revenue card: show pool balance + 累计/已提现 (drop 待结算), debt when present
- Schema 2.0.0 -> 2.1.0: add commission_debt, drop freeze columns
This commit is contained in:
2026-09-28 21:27:44 +08:00
parent cded20ae02
commit 61667aa03c
22 changed files with 569 additions and 268 deletions
+23 -5
View File
@@ -934,8 +934,24 @@
modal.dataset.aksrtWithdrawInitialized = '1';
var form = modal.querySelector('[data-aksrt-withdraw-form]');
var busy = false;
// 提现来源随打开按钮传入:commission 走佣金池,其余走余额钱包。
// 每个来源的每日次数/额度不同,按钮上带了各自的 min/remaining,
// 打开时覆盖到金额输入框,提交时把 source 一并回传。
document.querySelectorAll('[data-aksrt-withdraw-open]').forEach(function (btn) {
btn.addEventListener('click', function () { openModal(modal); });
btn.addEventListener('click', function () {
var src = btn.getAttribute('data-aksrt-withdraw-source') || 'balance';
modal.dataset.aksrtWithdrawSource = src === 'commission' ? 'commission' : 'balance';
var amountEl = form ? form.querySelector('[data-aksrt-withdraw-amount]') : null;
if (amountEl) {
var min = btn.getAttribute('data-withdraw-min');
var remAmt = btn.getAttribute('data-withdraw-remaining-amount');
var remCnt = btn.getAttribute('data-withdraw-remaining-count');
if (min !== null) amountEl.setAttribute('data-withdraw-min', min);
if (remAmt !== null) amountEl.setAttribute('data-withdraw-remaining-amount', remAmt);
if (remCnt !== null) amountEl.setAttribute('data-withdraw-remaining-count', remCnt);
}
openModal(modal);
});
});
modal.querySelectorAll('[data-aksrt-withdraw-close]').forEach(function (btn) {
btn.addEventListener('click', function () { closeModal(modal); });
@@ -978,10 +994,11 @@
}
busy = true;
notyf('正在提交提现申请…', 'load', '', 'aksrt-balance-withdraw-toast');
var source = modal.dataset.aksrtWithdrawSource === 'commission' ? 'commission' : 'balance';
api('/earnings/withdrawals', {
method: 'POST',
headers: { 'Idempotency-Key': operationKey(form) },
body: JSON.stringify({ amount: amount, method: method, account: account })
body: JSON.stringify({ amount: amount, method: method, account: account, source: source })
}).then(function (r) {
busy = false;
if (!r.ok || (r.json && r.json.code && !r.json.success)) {
@@ -994,11 +1011,12 @@
if (data && data.withdrawal_number) successMessage += '(' + data.withdrawal_number + ')';
notyf(successMessage, 'success', 5, 'aksrt-balance-withdraw-toast');
closeModal(modal);
var panel = modal.closest ? modal.closest('.dashboard-panel') : null;
panel = panel || document.getElementById('panel-credits');
// 佣金提现刷新推广收益面板,余额提现刷新余额面板;各自拉取对应片段。
var panelName = source === 'commission' ? 'revenue' : 'credits';
var panel = document.getElementById('panel-' + panelName) || (modal.closest ? modal.closest('.dashboard-panel') : null);
var rest = (window.aksrtData && (window.aksrtData.restUrl || window.aksrtData.apiUrl)) || (window.aksrtUserCenter && window.aksrtUserCenter.restUrl) || '';
if (panel && rest) {
fetch(rest.replace(/\/$/, '') + '/user-center/panel/credits', { credentials: 'same-origin', headers: { 'X-WP-Nonce': (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || '' } })
fetch(rest.replace(/\/$/, '') + '/user-center/panel/' + panelName, { credentials: 'same-origin', headers: { 'X-WP-Nonce': (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || '' } })
.then(function (response) { return response.json(); })
.then(function (payload) {
if (!payload || !payload.success || !payload.data || typeof payload.data.html !== 'string') return;