feat(withdrawal): make payout channels a configurable sortable list
The payout channel was a two-item checkbox (alipay / wechat). Sites that pay out through a bank transfer, USDT or any other route had no way to express it, and every surface behind it — the withdrawal modal, the server-side check, the admin review list — hard-coded those two names. affiliate_payout_channels is now a repeater on both the PHP and Vue schemas, using the same draggable component as the post filters and gateway config. Each row carries a stable key, a display label, an account placeholder, an optional note and an enabled flag. Disabled channels disappear from the front end but stay resolvable, so historical withdrawal records still render their original name. PaymentRuntime::payout_channels() normalises the stored structure: it drops rows with an empty key or label, de-duplicates keys, and falls back to the built-in two channels when nothing is configured. enabled_payout_channels() feeds the front end and payout_channel() resolves a single key for record display. CommissionLedger's validation now compares against the configured, enabled keys instead of a literal allowlist. The withdrawal modal renders the configured list and swaps the account placeholder and note when the channel changes; credits.js drives that. The admin review page reads the same settings for its method labels and filter, and the review dialog gained a "渠道说明" row so the operator sees the payout instruction, hidden when empty. Default channels live in SchemaPayment as plain data — the runtime imports them rather than the reverse, because the schema layer must not depend on a runtime settings class (doing it the other way made the schema harness fatal). Verification: drove payout_channels() through default / custom / dirty / duplicate inputs and confirmed enabled filtering, note lookup and disabled-channel resolution. Added payment-withdrawal-channels-contract.test.cjs covering the schema shape, runtime helpers, front-end wiring and admin read path. 873 tests pass, PSR-4 audit clean, admin SPA builds.
This commit is contained in:
1 parent
bcba0f598a
commit
dcd306b428
10 files changed
+267
-20
No files matched your search
@@ -506,12 +506,10 @@ class PaymentRuntime {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{min_amount:string,service_charge_rate:int,daily_count:int,daily_limit:string,reset_time:string,payout_channels:string[]}
|
||||
* @return array{min_amount:string,service_charge_rate:int,daily_count:int,daily_limit:string,reset_time:string,payout_channels:list<array{key:string,label:string,account_hint:string,note:string,enabled:bool}>}
|
||||
*/
|
||||
public static function get_withdrawal_settings(): array {
|
||||
$s = Settings::get_all();
|
||||
$channels = array_values(array_intersect(['alipay', 'wechat'], array_map('sanitize_key', (array) ($s['affiliate_payout_channels'] ?? ['alipay', 'wechat']))));
|
||||
if (!$channels) { $channels = ['alipay', 'wechat']; }
|
||||
$reset_time_raw = (string) ($s['affiliate_withdrawal_reset_time'] ?? '00:00');
|
||||
return [
|
||||
'min_amount' => self::money_decimal($s['affiliate_min_withdrawal'] ?? '50.00', '50.00', 99999900),
|
||||
@@ -521,10 +519,70 @@ class PaymentRuntime {
|
||||
'reset_time' => preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $reset_time_raw)
|
||||
? $reset_time_raw
|
||||
: '00:00',
|
||||
'payout_channels' => $channels,
|
||||
'payout_channels' => self::payout_channels($s['affiliate_payout_channels'] ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化后台配置的收款渠道。
|
||||
*
|
||||
* 渠道由后台拖拽结构定义,可自定义名称与提示;这里只做结构校验与去重,
|
||||
* 不再限定具体渠道,未配置时回落到支付宝/微信两个内置渠道。
|
||||
*
|
||||
* @param mixed $raw
|
||||
* @return list<array{key:string,label:string,account_hint:string,note:string,enabled:bool}>
|
||||
*/
|
||||
public static function payout_channels($raw): array {
|
||||
$raw = is_array($raw) ? $raw : [];
|
||||
$channels = [];
|
||||
$seen = [];
|
||||
foreach ($raw as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$key = sanitize_key((string) ($row['key'] ?? ''));
|
||||
$label = trim(sanitize_text_field((string) ($row['label'] ?? '')));
|
||||
if ($key === '' || $label === '' || isset($seen[$key])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$key] = true;
|
||||
$channels[] = [
|
||||
'key' => $key,
|
||||
'label' => $label,
|
||||
'account_hint' => trim(sanitize_text_field((string) ($row['account_hint'] ?? ''))),
|
||||
'note' => trim(sanitize_textarea_field((string) ($row['note'] ?? ''))),
|
||||
'enabled' => !isset($row['enabled']) || !empty($row['enabled']),
|
||||
];
|
||||
}
|
||||
if (!$channels) {
|
||||
$channels = \Jio\Admin\Schema\SchemaPayment::default_payout_channels();
|
||||
}
|
||||
return $channels;
|
||||
}
|
||||
|
||||
/**
|
||||
* 前台可选的收款渠道(仅启用项)。
|
||||
*
|
||||
* @return list<array{key:string,label:string,account_hint:string,note:string,enabled:bool}>
|
||||
*/
|
||||
public static function enabled_payout_channels(): array {
|
||||
return array_values(array_filter(
|
||||
self::get_withdrawal_settings()['payout_channels'],
|
||||
static fn (array $channel): bool => !empty($channel['enabled'])
|
||||
));
|
||||
}
|
||||
|
||||
/** 按渠道标识取配置;找不到时返回 null(含已停用的渠道,用于历史记录展示)。 */
|
||||
public static function payout_channel(string $key): ?array {
|
||||
$key = sanitize_key($key);
|
||||
foreach (self::get_withdrawal_settings()['payout_channels'] as $channel) {
|
||||
if ($channel['key'] === $key) {
|
||||
return $channel;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a registered adapter is admin-enabled.
|
||||
* Gateway ids (wechatpay) map to the reference-theme channel selectors:
|
||||
|
||||
Reference in new issue
Block a user