feat(payment): 支付&付费菜单按参考主题重构并新增提现策略页

- 子菜单重组为知识付费/余额充值/VIP会员/创作分成/推广返佣/收款接口/提现策略
- 收款接口页 1:1 复刻 Zibll 双下拉结构,仅保留支付宝/微信/PayPal/Stripe/易支付
- 订单前缀/流水号前缀/提现单号前缀配置保留并移入收款接口页
- 知识付费/推广返佣按参考主题全字段复刻,余额充值并入充值卡配置
- 新增 tabbed/between_number/page_select 表单控件及后端 pages 搜索源
- 清理旧 SPA 组件、无消费者 REST 端点与废弃配置键(不写双路兼容)
- 更新存量契约测试至新 schema 文件
This commit is contained in:
2026-09-16 19:18:28 +08:00
parent fb5cdf247d
commit a48007a44d
36 changed files with 2042 additions and 2273 deletions
+173 -82
View File
@@ -34,27 +34,29 @@ class PaymentRuntime {
'catalog_note' => '',
'manual_delivery_note' => __('付费或人工确认内容请在订单备注中记录交付方式。', 'aksrt'),
'no_logged_in' => !isset($settings['pay_no_logged_in']) || !empty($settings['pay_no_logged_in']),
'down_alone_page' => false,
'free_logged_show' => !isset($settings['pay_free_logged_show']) || !empty($settings['pay_free_logged_show']),
'down_alone_page' => !empty($settings['down_alone_page']),
'display' => [
'mark' => sanitize_text_field((string) ($settings['pay_mark'] ?? '付费')),
'currency_symbol' => '¥',
'currency_symbol' => sanitize_text_field((string) ($settings['pay_currency_mark'] ?? '¥')),
'currency_unit' => sanitize_text_field((string) ($settings['pay_currency_unit'] ?? '元')),
'show_user' => !isset($settings['pay_show_user']) || !empty($settings['pay_show_user']),
'show_paycount' => !isset($settings['pay_show_paycount']) || !empty($settings['pay_show_paycount']),
'box_position' => $box_position,
],
'download' => [
'enabled' => true,
'remote' => true,
'verify_nonce' => true,
'user_log' => 50,
'post_log' => 50,
'remote_size' => 2048,
'remote_ext' => 'zip,rar,7z',
'alone_page' => false,
'remote' => !isset($settings['down_remote_to_local_s']) || !empty($settings['down_remote_to_local_s']),
'verify_nonce' => !isset($settings['down_verify_nonce']) || !empty($settings['down_verify_nonce']),
'user_log' => max(0, (int) ($settings['down_user_log'] ?? 50)),
'post_log' => max(0, (int) ($settings['down_post_log'] ?? 50)),
'remote_size' => max(0, (int) ($settings['down_remote_to_local_size'] ?? 2048)),
'remote_ext' => sanitize_text_field((string) ($settings['down_remote_to_local_ext'] ?? 'zip,rar,7z')),
'alone_page' => !empty($settings['down_alone_page']),
],
// 内容收银台优惠码:不再提供全局开关,默认关闭。
'coupon_post_s' => false,
'coupon_desc' => '',
// 内容收银台优惠码:全局开关 + 默认说明。
'coupon_post_s' => !empty($settings['coupon_post_s']),
'coupon_desc' => sanitize_textarea_field((string) ($settings['coupon_desc'] ?? '')),
];
}
@@ -87,6 +89,7 @@ class PaymentRuntime {
return [
'enabled' => !empty($settings['pay_balance_s']),
'withdraw_enabled' => !empty($settings['pay_balance_withdraw_s']),
'desc' => $desc,
'packages_raw' => $packages,
'package_count' => self::count_structured_lines($packages),
@@ -279,28 +282,64 @@ class PaymentRuntime {
}
/**
* Affiliate policy. Layout follows the reference theme: one rebate rule
* set per user tier (pay_rebate_user_s / _1 / _2 + pay_rebate_ratio*).
*
* @return array<string,mixed>
*/
public static function get_payment_affiliate_settings(): array {
$settings = Settings::get_all();
$clamp_ratio = static fn($value): int => max(0, min(100, (int) $value));
$types = (array) ($settings['affiliate_order_types'] ?? ['membership', 'content']);
$types = array_values(array_intersect(['membership', 'content', 'credits'], array_map('sanitize_key', $types)));
if (!$types) { $types = ['membership', 'content']; }
$resolve_types = static function (array $types): array {
$types = array_values(array_intersect(['all', 'membership', 'content', 'credits'], array_map('sanitize_key', $types)));
// 「全部订单」展开为全部可返佣订单类型。
if (in_array('all', $types, true)) {
return ['membership', 'content', 'credits'];
}
return $types;
};
$types_by_level = [
0 => $resolve_types((array) ($settings['pay_rebate_user_s'] ?? [])),
1 => $resolve_types((array) ($settings['pay_rebate_user_s_1'] ?? ['all'])),
2 => $resolve_types((array) ($settings['pay_rebate_user_s_2'] ?? ['all'])),
];
$judgment = sanitize_key((string) ($settings['pay_rebate_judgment'] ?? 'all'));
if (!in_array($judgment, ['all', 'link'], true)) {
$judgment = 'all';
}
return [
'enabled' => !empty($settings['affiliate_enabled']),
'enabled' => !empty($settings['pay_rebate_s']),
'judgment' => $judgment,
'show_users' => !isset($settings['pay_rebate_show_users']) || !empty($settings['pay_rebate_show_users']),
'self_rebate' => !isset($settings['pay_rebate_self_s']) || !empty($settings['pay_rebate_self_s']),
'ratios' => [
'user' => $clamp_ratio($settings['affiliate_ratio_user'] ?? 5),
'vip_1' => $clamp_ratio($settings['affiliate_ratio_vip_1'] ?? 5),
'vip_2' => $clamp_ratio($settings['affiliate_ratio_vip_2'] ?? 5),
'user' => $clamp_ratio($settings['pay_rebate_ratio'] ?? 5),
'vip_1' => $clamp_ratio($settings['pay_rebate_ratio_vip_1'] ?? 10),
'vip_2' => $clamp_ratio($settings['pay_rebate_ratio_vip_2'] ?? 20),
],
'order_types' => $types,
'order_types' => $types_by_level[0],
'order_types_by_level' => $types_by_level,
'window_days' => (int) CreditsRuntime::get_referral_settings()['window_days'],
'settlement_days' => max(0, min(90, (int) ($settings['affiliate_settlement_days'] ?? 7))),
'promotion_text' => sanitize_textarea_field((string) ($settings['affiliate_promotion_text'] ?? '')),
'promotion_text' => sanitize_textarea_field((string) ($settings['pay_rebate_text_details'] ?? '')),
];
}
/**
* 某等级推广者可获返佣的订单类型;空数组代表该等级用户不参与推广返佣。
*
* @return array<int,string>
*/
public static function affiliate_order_types_for_level(int $level): array {
$settings = self::get_payment_affiliate_settings();
$key = $level >= 2 ? 2 : ($level >= 1 ? 1 : 0);
return (array) ($settings['order_types_by_level'][$key] ?? []);
}
public static function affiliate_enabled(): bool { return !empty(self::get_payment_affiliate_settings()['enabled']); }
public static function affiliate_ratio_for_user(int $user_id): int {
$level = function_exists('aksrt_get_user_vip_level') ? (int) aksrt_get_user_vip_level($user_id) : 0;
@@ -318,26 +357,55 @@ class PaymentRuntime {
}
/**
* Gateway channel selection. WeChat / Alipay each pick between the
* official SDK and 易支付; PayPal / Stripe are standalone toggles.
*
* @return array<string,mixed>
*/
public static function get_payment_gateway_settings(): array {
$settings = Settings::get_all();
$gateway_keys = ['wechat', 'alipay', 'paypal', 'stripe', 'balance', 'card_pass', 'epay'];
$gateways = [];
foreach ($gateway_keys as $gw) {
$gateways[$gw] = !empty($settings["payment_{$gw}_enabled"]);
$wechat = sanitize_key((string) ($settings['pay_wechat_sdk_options'] ?? 'null'));
if (!in_array($wechat, ['official_wechat', 'epay', 'null'], true)) {
$wechat = 'null';
}
$alipay = sanitize_key((string) ($settings['pay_alipay_sdk_options'] ?? 'null'));
if (!in_array($alipay, ['official_alipay', 'epay', 'null'], true)) {
$alipay = 'null';
}
$paypal_enabled = !empty($settings['pay_paypal_sdk_s']);
$stripe_enabled = !empty($settings['pay_stripe_sdk_s']);
$balance_enabled = !empty($settings['pay_balance_s']);
$card_pass_enabled = !empty($settings['pay_balance_pass_charge_s']);
return [
'help_text' => __('以渠道开关与凭据是否齐全为准;异步回调由各网关验签。卡密为兑换入口,不进入收银台。', 'aksrt'),
'gateways' => $gateways,
'epay' => [
'apiurl' => esc_url_raw((string) ($settings['epay_apiurl'] ?? '')),
'pid' => sanitize_text_field((string) ($settings['epay_pid'] ?? '')),
'key' => sanitize_text_field((string) ($settings['epay_key'] ?? '')),
'channel_alipay' => !array_key_exists('epay_channel_alipay', $settings) || !empty($settings['epay_channel_alipay']),
'channel_wxpay' => !array_key_exists('epay_channel_wxpay', $settings) || !empty($settings['epay_channel_wxpay']),
'device' => sanitize_key((string) ($settings['epay_device'] ?? 'auto')),
'help_text' => __('微信 / 支付宝可在官方与易支付之间选择;PayPal、Stripe 独立开关。异步回调由各网关验签。', 'aksrt'),
'wechat_sdk_options' => $wechat,
'alipay_sdk_options' => $alipay,
'paypal_enabled' => $paypal_enabled,
'stripe_enabled' => $stripe_enabled,
'balance_enabled' => $balance_enabled,
'card_pass_enabled' => $card_pass_enabled,
'gateways' => [
'wechat' => $wechat === 'official_wechat',
'alipay' => $alipay === 'official_alipay',
'paypal' => $paypal_enabled,
'stripe' => $stripe_enabled,
'balance' => $balance_enabled,
'card_pass' => $card_pass_enabled,
'epay' => $wechat === 'epay' || $alipay === 'epay',
],
'currency' => [
'mark' => sanitize_text_field((string) ($settings['pay_currency_mark'] ?? '¥')),
'unit' => sanitize_text_field((string) ($settings['pay_currency_unit'] ?? '元')),
],
'custom_title_s' => !empty($settings['pay_sdk_custom_title_s']),
'custom_title' => sanitize_text_field((string) ($settings['pay_sdk_custom_title'] ?? '')),
'epay' => [
'apiurl' => esc_url_raw((string) ($settings['epay_apiurl'] ?? '')),
'pid' => sanitize_text_field((string) ($settings['epay_pid'] ?? '')),
'key' => sanitize_text_field((string) ($settings['epay_key'] ?? '')),
'device' => sanitize_key((string) ($settings['epay_device'] ?? 'auto')),
],
];
}
@@ -352,7 +420,6 @@ class PaymentRuntime {
$expiry = max(5, min(1440, (int) ($s['payment_order_expiry'] ?? 30)));
$cookie_day = max(1, min(31, (int) ($s['pay_cookie_day'] ?? 15)));
$max_minutes = max(5, min(1440, (int) ($s['payment_order_max_minutes'] ?? 30)));
$box_position = sanitize_key((string) ($s['pay_box_position'] ?? 'top'));
if (!in_array($box_position, ['box_top', 'top', 'bottom', 'box_bottom'], true)) {
@@ -378,7 +445,6 @@ class PaymentRuntime {
'cookie_day' => $cookie_day,
'no_logged_remind' => sanitize_textarea_field((string) ($s['pay_no_logged_remind'] ?? '')),
'wait_float' => !isset($s['pay_wait_float_s']) || !empty($s['pay_wait_float_s']),
'order_max_minutes' => $max_minutes,
'mark' => sanitize_text_field((string) ($s['pay_mark'] ?? '付费')),
'box_position' => $box_position,
'show_ip' => !empty($s['payment_show_ip']),
@@ -387,13 +453,39 @@ class PaymentRuntime {
'show_create_time' => !isset($s['payment_show_create_time']) || !empty($s['payment_show_create_time']),
'show_pay_time' => !isset($s['payment_show_pay_time']) || !empty($s['payment_show_pay_time']),
'status_texts' => $status_texts,
'service' => [
'text' => sanitize_textarea_field((string) ($s['pay_service'] ?? '')),
'icon' => sanitize_text_field((string) ($s['pay_service_icon'] ?? '')),
'value' => sanitize_text_field((string) ($s['pay_service_value'] ?? '')),
'qq' => sanitize_text_field((string) ($s['pay_service_qq'] ?? '')),
'wechat' => esc_url_raw((string) ($s['pay_service_wechat'] ?? '')),
],
'service' => self::format_service_items((array) ($s['pay_service'] ?? [])),
];
}
/**
* 客户服务:参考主题为 {value, icon} 数组;首项同时回填旧单值键,兼容既有消费方。
*
* @param array<int,mixed> $items
* @return array<string,mixed>
*/
private static function format_service_items(array $items): array {
$clean = [];
foreach ($items as $row) {
if (!is_array($row)) {
continue;
}
$value = sanitize_textarea_field((string) ($row['value'] ?? ''));
if ($value === '') {
continue;
}
$clean[] = [
'value' => $value,
'icon' => sanitize_text_field((string) ($row['icon'] ?? '')),
];
}
$first = $clean[0] ?? [];
return [
'items' => $clean,
'text' => (string) ($first['value'] ?? ''),
'icon' => (string) ($first['icon'] ?? ''),
'value' => (string) ($first['value'] ?? ''),
'qq' => '',
'wechat' => '',
];
}
@@ -462,48 +554,43 @@ class PaymentRuntime {
/**
* Whether a registered adapter is admin-enabled.
* Maps gateway ids (wechatpay) to schema toggles (payment_wechat_enabled).
* Gateway ids (wechatpay) map to the reference-theme channel selectors:
* pay_wechat_sdk_options / pay_alipay_sdk_options / pay_paypal_sdk_s / pay_stripe_sdk_s.
*/
public static function is_gateway_enabled(string $gateway_id): bool {
$settings = Settings::get_all();
$gateway_id = sanitize_key($gateway_id);
$map = [
'alipay' => 'payment_alipay_enabled',
'wechatpay' => 'payment_wechat_enabled',
'paypal' => 'payment_paypal_enabled',
'stripe' => 'payment_stripe_enabled',
'balance' => 'payment_balance_enabled',
'card_pass' => 'payment_card_pass_enabled',
'epay' => 'payment_epay_enabled',
'epay_alipay' => 'payment_epay_enabled',
'epay_wxpay' => 'payment_epay_enabled',
];
$key = $map[$gateway_id] ?? '';
if ($key === '') {
return false;
$wechat = sanitize_key((string) ($settings['pay_wechat_sdk_options'] ?? 'null'));
if (!in_array($wechat, ['official_wechat', 'epay', 'null'], true)) {
$wechat = 'null';
}
if ($gateway_id === 'balance') {
// Master: 余额充值 → 启用余额功能. Optional kill-switch: payment_balance_enabled=false.
// Missing key (legacy sites) follows pay_balance_s so "open wallet" alone enables pay.
if (empty($settings['pay_balance_s'])) {
return false;
}
if (array_key_exists('payment_balance_enabled', $settings) && empty($settings['payment_balance_enabled'])) {
return false;
}
return true;
$alipay = sanitize_key((string) ($settings['pay_alipay_sdk_options'] ?? 'null'));
if (!in_array($alipay, ['official_alipay', 'epay', 'null'], true)) {
$alipay = 'null';
}
if (empty($settings[$key])) {
return false;
switch ($gateway_id) {
case 'alipay':
return $alipay === 'official_alipay';
case 'wechatpay':
return $wechat === 'official_wechat';
case 'paypal':
return !empty($settings['pay_paypal_sdk_s']);
case 'stripe':
return !empty($settings['pay_stripe_sdk_s']);
case 'balance':
return !empty($settings['pay_balance_s']);
case 'card_pass':
return !empty($settings['pay_balance_pass_charge_s']);
case 'epay':
return $wechat === 'epay' || $alipay === 'epay';
case 'epay_alipay':
return $alipay === 'epay';
case 'epay_wxpay':
return $wechat === 'epay';
}
// Epay sub-channels require the matching channel switch (default on when key missing).
if ($gateway_id === 'epay_alipay') {
return !array_key_exists('epay_channel_alipay', $settings) || !empty($settings['epay_channel_alipay']);
}
if ($gateway_id === 'epay_wxpay') {
return !array_key_exists('epay_channel_wxpay', $settings) || !empty($settings['epay_channel_wxpay']);
}
return true;
return false;
}
private static function count_structured_lines(string $raw): int {
@@ -530,6 +617,9 @@ class PaymentRuntime {
}
/**
* Format recharge product rows (reference-theme group shape:
* price 充值金额 / pay_price 销售金额 / tag 小标签) into "支付|到账|标题" lines.
*
* @param array<int,mixed> $products
*/
private static function format_product_lines(array $products): string {
@@ -538,10 +628,11 @@ class PaymentRuntime {
if (!is_array($row)) {
continue;
}
$price = $row['pay_price'] ?? $row['price'] ?? '';
$value = $row['value'] ?? $row['balance'] ?? $price;
$title = $row['title'] ?? $row['name'] ?? '';
$lines[] = $price . '|' . $value . '|' . $title;
$price = max(0, (float) ($row['price'] ?? 0));
$pay_price = max(0, (float) ($row['pay_price'] ?? 0));
$pay = $pay_price > 0 ? $pay_price : $price;
$title = trim((string) ($row['tag'] ?? ''));
$lines[] = $pay . '|' . $price . '|' . $title;
}
return implode("\n", $lines);
}