diff --git a/assets/admin/src/schema/sections/payment/withdrawal.js b/assets/admin/src/schema/sections/payment/withdrawal.js
index b5891db4..13236ec2 100644
--- a/assets/admin/src/schema/sections/payment/withdrawal.js
+++ b/assets/admin/src/schema/sections/payment/withdrawal.js
@@ -1,6 +1,14 @@
/**
* 提现策略 — 提现门槛、每日限制、收款渠道与提现协议。
*/
+
+/** 默认收款渠道:支付宝与微信,与参考主题一致。 */
+export function defaultPayoutChannels() {
+ return [
+ { key: 'alipay', label: '支付宝收款码', account_hint: '姓名 + 支付宝账号', note: '', enabled: true },
+ { key: 'wechat', label: '微信收款码', account_hint: '姓名 + 微信号', note: '', enabled: true },
+ ]
+}
export const withdrawalSchema = {
key: 'payment.withdrawal',
title: '提现策略',
@@ -12,7 +20,7 @@ export const withdrawalSchema = {
affiliate_daily_withdrawal_count: 3,
affiliate_daily_withdrawal_limit: '500.00',
affiliate_withdrawal_reset_time: '00:00',
- affiliate_payout_channels: ['alipay', 'wechat'],
+ affiliate_payout_channels: defaultPayoutChannels(),
pay_rebate_withdraw_text_details: '
可提现金额达到50元后即可发起提现
申请提现需后台人工处理,一般2-3小时,请耐心等待
如有其它疑问,请与客服联系
',
},
groups: [
@@ -40,7 +48,21 @@ export const withdrawalSchema = {
title: '收款渠道',
icon: 'fa fas fa-qrcode',
fields: [
- { key: 'affiliate_payout_channels', label: '可用收款渠道', type: 'checkbox', default: ['alipay', 'wechat'], sanitize_type: 'array', options: [{ label: '支付宝收款码', value: 'alipay' }, { label: '微信收款码', value: 'wechat' }] },
+ {
+ key: 'affiliate_payout_channels',
+ label: '收款渠道',
+ type: 'repeater',
+ addText: '添加收款渠道',
+ default: defaultPayoutChannels(),
+ description: '用户提现时可选择的收款方式,可拖拽排序、自定义名称。停用的渠道不会在前台展示,但历史提现记录仍按原名称显示。',
+ fields: [
+ { key: 'key', label: '渠道标识', type: 'input', default: '', help: '英文小写,用于区分渠道(如 alipay、wechat、bank)。保存后请勿随意修改,历史提现记录依赖它。' },
+ { key: 'label', label: '渠道名称', type: 'input', default: '', help: '前台下拉里显示的名称,可自定义。' },
+ { key: 'account_hint', label: '收款信息提示', type: 'input', default: '', help: '用户填写收款账户时的占位提示,如「姓名 + 支付宝账号」。' },
+ { key: 'note', label: '渠道说明', type: 'textarea', default: '', rows: 2, help: '选中该渠道时在收款信息下方展示的补充说明,可留空。' },
+ { key: 'enabled', label: '启用', type: 'switch', default: true },
+ ],
+ },
],
},
{
diff --git a/assets/js/admin-withdrawals.js b/assets/js/admin-withdrawals.js
index 9acaec69..a4d92cbf 100644
--- a/assets/js/admin-withdrawals.js
+++ b/assets/js/admin-withdrawals.js
@@ -47,7 +47,16 @@
});
form.querySelector('[data-field="status"]').value = mode;
dialog.querySelectorAll('[data-summary]').forEach(function (dd) {
- dd.textContent = button.dataset[dd.getAttribute('data-summary')] || '—';
+ var raw = button.dataset[dd.getAttribute('data-summary')] || '';
+ // 渠道说明是可选的,留空时整行隐藏,不留一条破折号。
+ var row = dd.parentElement;
+ if (dd.getAttribute('data-summary') === 'method_note' && raw === '') {
+ if (row) row.hidden = true;
+ dd.textContent = '';
+ return;
+ }
+ if (row) row.hidden = false;
+ dd.textContent = raw || '—';
});
if (note) note.value = '';
resetSubmits();
diff --git a/assets/js/credits.js b/assets/js/credits.js
index 68890005..ff11bdfe 100644
--- a/assets/js/credits.js
+++ b/assets/js/credits.js
@@ -392,6 +392,26 @@
});
applyMethodVisibility((modal.querySelector('input[name="payment_method"]') || {}).value || '');
if (!form) return;
+ // 收款渠道由后台拖拽结构定义:切换时同步收款信息提示与渠道说明。
+ var methodField = form.querySelector('[data-jio-withdraw-method]');
+ var accountField = form.querySelector('[data-jio-withdraw-account]');
+ var noteField = form.querySelector('[data-jio-withdraw-note]');
+ function syncChannelHint() {
+ if (!methodField) return;
+ var option = methodField.options[methodField.selectedIndex];
+ if (!option) return;
+ var hint = option.getAttribute('data-hint') || '';
+ var note = option.getAttribute('data-note') || '';
+ if (accountField) accountField.setAttribute('placeholder', hint || '姓名 + 账户 / 卡号');
+ if (noteField) {
+ noteField.textContent = note;
+ noteField.hidden = note === '';
+ }
+ }
+ if (methodField) {
+ methodField.addEventListener('change', syncChannelHint);
+ syncChannelHint();
+ }
resetOperationKeyOnChange(form);
form.addEventListener('submit', function (e) {
e.preventDefault();
diff --git a/inc/helpers/user-center-renderers/withdraw-modal.php b/inc/helpers/user-center-renderers/withdraw-modal.php
index f4685a55..74470509 100644
--- a/inc/helpers/user-center-renderers/withdraw-modal.php
+++ b/inc/helpers/user-center-renderers/withdraw-modal.php
@@ -13,10 +13,11 @@ try {
}
$withdraw_usage = is_array($withdraw_usage ?? null) ? $withdraw_usage : ['remaining_count' => 0, 'remaining_amount' => '0.00'];
$withdraw_fee_rate = (int) ($withdraw_settings['service_charge_rate'] ?? 5);
-$withdraw_channels = array_values(array_intersect(['alipay', 'wechat'], (array) ($withdraw_settings['payout_channels'] ?? ['alipay', 'wechat'])));
-if (!$withdraw_channels) {
- $withdraw_channels = ['alipay', 'wechat'];
-}
+// 渠道由后台「提现策略 → 收款渠道」的拖拽结构定义,可自定义名称与提示。
+$withdraw_channels = array_values(array_filter(
+ (array) ($withdraw_settings['payout_channels'] ?? []),
+ static fn ($channel): bool => is_array($channel) && !empty($channel['enabled']) && (string) ($channel['key'] ?? '') !== ''
+));
?>
@@ -30,11 +31,16 @@ if (!$withdraw_channels) {
-
+
+
>
diff --git a/src/Admin/Native/PayWithdrawals.php b/src/Admin/Native/PayWithdrawals.php
index f51548ee..4a994946 100644
--- a/src/Admin/Native/PayWithdrawals.php
+++ b/src/Admin/Native/PayWithdrawals.php
@@ -176,12 +176,40 @@ class PayWithdrawals {
];
}
- /** @return array */
+ /**
+ * 收款渠道标识 → 名称。
+ *
+ * 渠道由后台「提现策略 → 收款渠道」的拖拽结构定义,可自定义名称;这里读同一份
+ * 配置,停用的渠道也会返回,历史提现记录才能按原名称展示。
+ *
+ * @return array
+ */
private function method_labels(): array {
- return [
- 'alipay' => __('支付宝', 'jio'),
- 'wechat' => __('微信', 'jio'),
- ];
+ $labels = [];
+ $settings = class_exists('\Jio\Settings\Settings') ? \Jio\Settings\Settings::get_withdrawal_settings() : [];
+ foreach ((array) ($settings['payout_channels'] ?? []) as $channel) {
+ if (!is_array($channel) || (string) ($channel['key'] ?? '') === '') {
+ continue;
+ }
+ $labels[(string) $channel['key']] = (string) ($channel['label'] ?? $channel['key']);
+ }
+ return $labels;
+ }
+
+ /** 渠道补充说明(后台「收款渠道 → 渠道说明」),审核弹窗里展示给操作人。 */
+ private function method_notes(): array {
+ $notes = [];
+ $settings = class_exists('\Jio\Settings\Settings') ? \Jio\Settings\Settings::get_withdrawal_settings() : [];
+ foreach ((array) ($settings['payout_channels'] ?? []) as $channel) {
+ if (!is_array($channel) || (string) ($channel['key'] ?? '') === '') {
+ continue;
+ }
+ $note = trim((string) ($channel['note'] ?? ''));
+ if ($note !== '') {
+ $notes[(string) $channel['key']] = $note;
+ }
+ }
+ return $notes;
}
private function format_money(string $decimal): string {
diff --git a/src/Admin/Native/PayWithdrawalsList.php b/src/Admin/Native/PayWithdrawalsList.php
index 882f8a37..8f141525 100644
--- a/src/Admin/Native/PayWithdrawalsList.php
+++ b/src/Admin/Native/PayWithdrawalsList.php
@@ -176,6 +176,7 @@ trait PayWithdrawalsList {
'gross' => $this->format_money($gross),
'source' => $source_label,
'payee' => $method_label . ($account !== '' ? ' · ' . $account : ''),
+ 'method_note' => (string) ($this->method_notes()[$method] ?? ''),
];
$attrs = '';
foreach ($data as $key => $value) {
@@ -328,6 +329,7 @@ trait PayWithdrawalsList {
'gross' => __('申请金额', 'jio'),
'source' => __('来源', 'jio'),
'payee' => __('收款信息', 'jio'),
+ 'method_note' => __('渠道说明', 'jio'),
] as $field => $label) {
echo '' . esc_html($label) . '';
}
diff --git a/src/Admin/Schema/SchemaPayment.php b/src/Admin/Schema/SchemaPayment.php
index 2957880f..952eafad 100644
--- a/src/Admin/Schema/SchemaPayment.php
+++ b/src/Admin/Schema/SchemaPayment.php
@@ -557,7 +557,13 @@ class SchemaPayment {
['key' => 'affiliate_withdrawal_reset_time', 'label' => '重置时间', 'type' => 'input', 'default' => '00:00', 'help' => '按站点 WordPress 时区重置,格式 HH:MM。'],
]],
['key' => 'channels', 'title' => '收款渠道', 'fields' => [
- ['key' => 'affiliate_payout_channels', 'label' => '可用收款渠道', 'type' => 'checkbox', 'options' => [['value' => 'alipay', 'label' => '支付宝收款码'], ['value' => 'wechat', 'label' => '微信收款码']], 'default' => ['alipay', 'wechat']],
+ ['key' => 'affiliate_payout_channels', 'label' => '收款渠道', 'type' => 'repeater', 'addText' => '添加收款渠道', 'default' => self::default_payout_channels(), 'description' => '用户提现时可选择的收款方式,可拖拽排序、自定义名称。停用的渠道不会在前台展示,但历史提现记录仍按原名称显示。', 'fields' => [
+ ['key' => 'key', 'label' => '渠道标识', 'type' => 'input', 'default' => '', 'help' => '英文小写,用于区分渠道(如 alipay、wechat、bank)。保存后请勿随意修改,历史提现记录依赖它。'],
+ ['key' => 'label', 'label' => '渠道名称', 'type' => 'input', 'default' => '', 'help' => '前台下拉里显示的名称,可自定义。'],
+ ['key' => 'account_hint', 'label' => '收款信息提示', 'type' => 'input', 'default' => '', 'help' => '用户填写收款账户时的占位提示,如「姓名 + 支付宝账号」。'],
+ ['key' => 'note', 'label' => '渠道说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'help' => '选中该渠道时在收款信息下方展示的补充说明,可留空。'],
+ ['key' => 'enabled', 'label' => '启用', 'type' => 'switch', 'default' => true],
+ ]],
]],
['key' => 'agreement', 'title' => '提现协议', 'fields' => [
['key' => 'pay_rebate_withdraw_text_details', 'label' => '提现协议', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '可提现金额达到50元后即可发起提现
申请提现需后台人工处理,一般2-3小时,请耐心等待
如有其它疑问,请与客服联系
', 'rows' => 4, 'help' => '用户申请提现时展示的内容,建议为提现须知等,支持HTML代码'],
@@ -568,6 +574,18 @@ class SchemaPayment {
];
}
+ /**
+ * 提现收款渠道的默认结构:与参考主题一致的两个内置渠道。
+ *
+ * @return list
+ */
+ public static function default_payout_channels(): array {
+ return [
+ ['key' => 'alipay', 'label' => '支付宝收款码', 'account_hint' => '姓名 + 支付宝账号', 'note' => '', 'enabled' => true],
+ ['key' => 'wechat', 'label' => '微信收款码', 'account_hint' => '姓名 + 微信号', 'note' => '', 'enabled' => true],
+ ];
+ }
+
/**
* 会员商品/续费商品:标题行显示价格与时长摘要(行默认收起由 SPA 统一处理)。
*
diff --git a/src/Payment/CommissionLedger.php b/src/Payment/CommissionLedger.php
index 7158f4e4..8bdd4c87 100644
--- a/src/Payment/CommissionLedger.php
+++ b/src/Payment/CommissionLedger.php
@@ -525,7 +525,9 @@ final class CommissionLedger {
}
$minimum=\Jio\Ledger\AssetAmount::balance(is_string($settings['min_amount'] ?? null) ? $settings['min_amount'] : '');
if ($amount->minor() < $minimum->minor()) return new \WP_Error('affiliate_withdraw_min',__('未达到最低提现金额。','jio'),['status'=>400]);
- if (!in_array($method,(array)$settings['payout_channels'],true) || $account==='') return new \WP_Error('affiliate_withdraw_account',__('请提供有效的收款渠道和收款码。','jio'),['status'=>400]);
+ // 渠道来自后台可自定义的拖拽结构:按标识比对启用项,不再限定内置渠道。
+ $channel_keys = array_map(static fn($c) => (string)($c['key'] ?? ''), array_filter((array)$settings['payout_channels'], static fn($c) => !empty($c['enabled'])));
+ if (!in_array($method,$channel_keys,true) || $account==='') return new \WP_Error('affiliate_withdraw_account',__('请提供有效的收款渠道和收款码。','jio'),['status'=>400]);
$result=\Jio\Payment\Withdrawal::get_instance()->create_balance_request($user_id,$amount->minor(),$method,$account,$settings,$operation_key,$source); if(is_wp_error($result)) return $result; return new \WP_REST_Response(['success'=>true,'data'=>$result],201);
}
private function balance_decimal(string $value): string {
diff --git a/src/Settings/PaymentRuntime.php b/src/Settings/PaymentRuntime.php
index 753cd191..6afdf73c 100644
--- a/src/Settings/PaymentRuntime.php
+++ b/src/Settings/PaymentRuntime.php
@@ -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}
*/
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
+ */
+ 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
+ */
+ 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:
diff --git a/tests/widget-system/payment-withdrawal-channels-contract.test.cjs b/tests/widget-system/payment-withdrawal-channels-contract.test.cjs
new file mode 100644
index 00000000..9c3fedb7
--- /dev/null
+++ b/tests/widget-system/payment-withdrawal-channels-contract.test.cjs
@@ -0,0 +1,82 @@
+/**
+ * 提现收款渠道契约。
+ *
+ * 渠道由后台「提现策略 → 收款渠道」的拖拽结构定义,可自定义名称与提示,
+ * 不再固定为支付宝/微信两个勾选项。前台下拉、后端校验与后台审核页都读同一份配置。
+ */
+'use strict';
+
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const test = require('node:test');
+
+const root = path.resolve(__dirname, '..', '..');
+const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
+
+test('收款渠道在两侧 schema 都是可拖拽的 repeater', () => {
+ const php = read('src/Admin/Schema/SchemaPayment.php');
+ const js = read('assets/admin/src/schema/sections/payment/withdrawal.js');
+
+ for (const source of [php, js]) {
+ assert.match(source, /affiliate_payout_channels/);
+ assert.match(source, /(?:type:\s*'repeater'|'type'\s*=>\s*'repeater')/);
+ // 结构:key / label / account_hint / note / enabled
+ for (const field of ['key', 'label', 'account_hint', 'note', 'enabled']) {
+ assert.match(source, new RegExp(`key:\\s*'${field}'|'key'\\s*=>\\s*'${field}'`), field);
+ }
+ }
+
+ // 不再是勾选列表。
+ assert.doesNotMatch(php, /affiliate_payout_channels[^\n]*'type'\s*=>\s*'checkbox'/);
+ assert.doesNotMatch(js, /affiliate_payout_channels[^\n]*type:\s*'checkbox'/);
+});
+
+test('运行时按自定义渠道解析,未配置时回落内置两项', () => {
+ const runtime = read('src/Settings/PaymentRuntime.php');
+
+ assert.match(runtime, /public static function payout_channels\(\$raw\): array/);
+ assert.match(runtime, /public static function enabled_payout_channels\(\): array/);
+ assert.match(runtime, /public static function payout_channel\(string \$key\): \?array/);
+
+ // 不再把渠道白名单硬编码成 alipay/wechat。
+ assert.doesNotMatch(runtime, /array_intersect\(\['alipay', 'wechat'\]/);
+
+ // 默认渠道由 schema 层唯一定义(纯数据,不依赖运行时),运行时引用它。
+ assert.match(read('src/Admin/Schema/SchemaPayment.php'), /public static function default_payout_channels\(\): array/);
+ assert.match(runtime, /SchemaPayment::default_payout_channels\(\)/);
+});
+
+test('前台弹窗与后端校验都按渠道 key 工作', () => {
+ const modal = read('inc/helpers/user-center-renderers/withdraw-modal.php');
+ const ledger = read('src/Payment/CommissionLedger.php');
+ const client = read('assets/js/credits.js');
+
+ // 弹窗按配置渲染,携带自定义名称与提示。
+ assert.doesNotMatch(modal, /array_intersect\(\['alipay', 'wechat'\]/);
+ assert.match(modal, /data-hint="<\?php echo esc_attr/);
+ assert.match(modal, /data-note="<\?php echo esc_attr/);
+ assert.match(modal, /data-jio-withdraw-note/);
+
+ // 切换渠道时同步提示文案。
+ assert.match(client, /function syncChannelHint\(\)/);
+ assert.match(client, /methodField\.addEventListener\('change', syncChannelHint\)/);
+
+ // 后端只校验渠道存在且启用,不再限定内置两项。
+ assert.match(ledger, /\$channel_keys = array_map/);
+ assert.doesNotMatch(ledger, /in_array\(\$method,\s*\[?'alipay'/);
+});
+
+test('后台提现审核读取同一份渠道配置', () => {
+ const page = read('src/Admin/Native/PayWithdrawals.php');
+ const list = read('src/Admin/Native/PayWithdrawalsList.php');
+
+ // 渠道名来自设置,而不是硬编码映射。
+ assert.match(page, /private function method_labels\(\): array/);
+ assert.match(page, /get_withdrawal_settings\(\)/);
+ assert.doesNotMatch(page, /'alipay' => __\('支付宝'/);
+ assert.match(page, /private function method_notes\(\): array/);
+
+ // 审核弹窗展示渠道说明。
+ assert.match(list, /'method_note' => __\('渠道说明'/);
+});