The theme was renamed Zibll/子比 → AKSRT → Jio, but upstream branding survived the sweeps in CSS classes, the SVG sprite prefix, a persisted widget field ID, JS events, admin Vue class names, test infrastructure and docblocks. Renames: - zib-widget / zib-widget-wrap / zib-slider → jio-widget-box / jio-widget-box-wrap / jio-slider-box (jio-widget is taken by the modern widget shell, jio-box-* by the card primitives) - zib-partners-scroll + zps-* → jio-partners-scroll + jps-* - zib-combo-info-card + zci-* → jio-combo-info-card + jci-* - zibsvg- → jiosvg-, including the exported picker helpers and the fa-zibsvg- family-prefix guard - zibpay_type → pay_type; the dual-read fallback in PostQuery collapses to a single key - [zibajax="submit"] dropped (no PHP emitter); zib_ajax.success → jio_ajax.success - aks-* admin Vue classes → jio-* - zib_palette() → palette() User-facing vendor traces are gone: the floating "more" button default no longer ships a zibll.com link or 子比主题 marketing copy, the notice widget ships no preset promo entries, and the four 官网教程 links are removed from the post-list admin schema. Test infrastructure renamed to neutral names (docker-compose.reference.yml, Caddyfile, article-baseline.json, modern-widgets-contract.test.cjs) with their DB/volume/env/slug identifiers following. Kept intentionally, documented in tests/fixtures/README.md: the golden fixture reference fingerprint (a SHA-256 record of an external directory, read by validate-widget-golden / generate-widget-golden-fixture / audit-widget-system), the doesNotMatch guards that prove the old tokens are gone, and the reference theme's own meta key in the visual fixtures. No backward-compat or data migration: saved widget configs holding old icon keys or the old field ID silently lose them, per the project's rewrite-over-compatibility rule. Verified: 132/132 node contract tests, php tests/widget-system/run.php (146 assertions), audit-admin-contracts, audit-psr4-paths and validate-widget-golden all green; tracked bundles regenerated and in sync. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
817 lines
40 KiB
PHP
817 lines
40 KiB
PHP
<?php
|
||
/**
|
||
* @package Jio
|
||
*/
|
||
|
||
namespace Jio\Settings;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
class PaymentRuntime {
|
||
|
||
/**
|
||
* Paid content / download policy.
|
||
*
|
||
* @return array<string,mixed>
|
||
*/
|
||
public static function get_paid_content_settings(): array {
|
||
$settings = Settings::get_all();
|
||
|
||
$box_position = sanitize_key((string) ($settings['pay_box_position'] ?? 'top'));
|
||
// legacy: box_top | top | bottom | box_bottom
|
||
if (!in_array($box_position, ['box_top', 'top', 'bottom', 'box_bottom'], true)) {
|
||
$box_position = 'top';
|
||
}
|
||
|
||
return [
|
||
// 下载门槛永远支持积分购买模式(不再由页面开关控制)。
|
||
'download_credit' => Settings::credits_enabled(),
|
||
'default_cost' => Settings::download_default_cost(),
|
||
// 付费内容仅文章。
|
||
'paid_post_types' => ['post'],
|
||
'vip_free_download' => false,
|
||
'vip_exempt_roles' => [],
|
||
'catalog_note' => '',
|
||
'manual_delivery_note' => __('付费或人工确认内容请在订单备注中记录交付方式。', 'jio'),
|
||
'no_logged_in' => !isset($settings['pay_no_logged_in']) || !empty($settings['pay_no_logged_in']),
|
||
'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' => 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' => !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']),
|
||
'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' => !empty($settings['coupon_post_s']),
|
||
'coupon_desc' => sanitize_textarea_field((string) ($settings['coupon_desc'] ?? '')),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Content coupon global switches (legacy coupon_post_s).
|
||
*
|
||
* @return array{enabled:bool,desc:string}
|
||
*/
|
||
public static function get_content_coupon_settings(): array {
|
||
$settings = self::get_paid_content_settings();
|
||
return [
|
||
'enabled' => !empty($settings['coupon_post_s']),
|
||
'desc' => (string) ($settings['coupon_desc'] ?? ''),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Balance recharge settings (legacy pay_balance_* contract).
|
||
*
|
||
* @return array<string,mixed>
|
||
*/
|
||
public static function get_payment_recharge_settings(): array {
|
||
$settings = Settings::get_all();
|
||
|
||
// 该选项可能存字符串(旧格式)或数组(结构化保存):先判类型再转换,
|
||
// 避免对数组做强转触发 "Array to string conversion" 警告。
|
||
$raw_packages = $settings['pay_balance_product'] ?? null;
|
||
$packages = is_array($raw_packages)
|
||
? self::format_product_lines($raw_packages)
|
||
: (string) ($raw_packages ?? "30|30|入门包\n98|108|推荐包");
|
||
$desc = sanitize_textarea_field((string) ($settings['pay_balance_desc'] ?? ''));
|
||
|
||
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),
|
||
'help_text' => $desc !== '' ? $desc : __('当前支持人工确认与测试联调;接入真实网关时复用同一组字段。', 'jio'),
|
||
'product' => [
|
||
'name' => sanitize_text_field((string) ($settings['pay_balance_product_name'] ?? '')),
|
||
'price' => self::money_decimal($settings['pay_balance_price'] ?? '0.00', '0.00'),
|
||
'tag' => sanitize_text_field((string) ($settings['pay_balance_tag'] ?? '元')),
|
||
'tag_class' => sanitize_text_field((string) ($settings['pay_balance_tag_class'] ?? '')),
|
||
'custom_s' => !isset($settings['pay_balance_product_custom_s']) || !empty($settings['pay_balance_product_custom_s']),
|
||
'custom_limit' => max(1, min(99999, (int) ($settings['pay_balance_product_custom_limit'] ?? 9999))),
|
||
],
|
||
'transfer' => [
|
||
'enabled' => !empty($settings['pay_balance_transfer_s']),
|
||
'service_charge' => max(0, min(50, (int) ($settings['pay_balance_transfer_service_charge'] ?? 0))),
|
||
'desc' => sanitize_textarea_field((string) ($settings['pay_balance_transfer_desc'] ?? '')),
|
||
],
|
||
'pass_charge' => [
|
||
// 三个独立开关:各类型卡密只进入各自的购买弹窗(收款接口同款拆分)。
|
||
'balance' => [
|
||
'enabled' => !empty($settings['pay_balance_pass_charge_s']),
|
||
'desc' => sanitize_textarea_field((string) ($settings['pay_balance_pass_charge_desc'] ?? '')),
|
||
'only_password' => !empty($settings['pay_balance_pass_charge_only_password']),
|
||
],
|
||
'credits' => [
|
||
'enabled' => !empty($settings['points_pass_exchange_s']),
|
||
'desc' => sanitize_textarea_field((string) ($settings['points_pass_exchange_desc'] ?? '')),
|
||
'only_password' => !empty($settings['points_pass_exchange_only_password']),
|
||
],
|
||
'vip' => [
|
||
'enabled' => !empty($settings['pay_vip_pass_charge_s']),
|
||
// 保存端已按 post 级 HTML 放行(schema sanitize_type html),
|
||
// 输出端不能再被 textarea 净化剥掉标签。
|
||
'desc' => wp_kses_post((string) ($settings['pay_vip_pass_charge_desc'] ?? '')),
|
||
// 默认非单密码(卡号+密码),对齐参考主题 pay_vip_pass_charge_only_password 默认 false。
|
||
'only_password' => !empty($settings['pay_vip_pass_charge_only_password']),
|
||
],
|
||
],
|
||
'points_exchange' => [
|
||
'enabled' => !empty($settings['pay_balance_points_exchange_s']),
|
||
'rate' => max(1, min(10000, (int) ($settings['pay_balance_points_exchange_rate'] ?? 100))),
|
||
'min_points' => max(1, min(10000000, (int) ($settings['pay_balance_points_exchange_min_points'] ?? 100))),
|
||
'daily_max_points' => max(1, min(10000000, (int) ($settings['pay_balance_points_exchange_daily_max_points'] ?? 100000))),
|
||
'desc' => sanitize_textarea_field((string) ($settings['pay_balance_points_exchange_desc'] ?? '')),
|
||
'products_raw' => is_array($settings['pay_points_product'] ?? null)
|
||
? self::format_points_product_lines($settings['pay_points_product'])
|
||
: (string) ($settings['pay_points_product'] ?? ''),
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Resolve a configured VIP icon, falling back to the bundled level asset.
|
||
*/
|
||
private static function get_vip_icon_url(int $level, string $configured): string {
|
||
$configured = esc_url_raw($configured);
|
||
if ($configured !== '') {
|
||
return $configured;
|
||
}
|
||
|
||
$filename = 'vip-' . $level . '.svg';
|
||
if (defined('JIO_DIR') && is_file(JIO_DIR . '/assets/img/user/vip/' . $filename)) {
|
||
return defined('JIO_URI') ? JIO_URI . '/assets/img/user/vip/' . $filename : '';
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* Dual-level VIP settings (legacy pay_user_vip_1 / pay_user_vip_2).
|
||
*
|
||
* @return array<string,mixed>
|
||
*/
|
||
public static function get_payment_vip_settings(): array {
|
||
$settings = Settings::get_all();
|
||
|
||
$vip1_enabled = !empty($settings['pay_user_vip_1_s']);
|
||
$vip2_enabled = !empty($settings['pay_user_vip_2_s']);
|
||
$enabled = $vip1_enabled || $vip2_enabled;
|
||
|
||
$name1_html = trim(wp_kses_post((string) ($settings['pay_user_vip_1_name'] ?? '')));
|
||
$name2_html = trim(wp_kses_post((string) ($settings['pay_user_vip_2_name'] ?? '')));
|
||
$name1 = sanitize_text_field(wp_strip_all_tags($name1_html));
|
||
$name2 = sanitize_text_field(wp_strip_all_tags($name2_html));
|
||
if ($name1 === '') {
|
||
$name1 = __('黄金会员', 'jio');
|
||
$name1_html = $name1;
|
||
}
|
||
if ($name2 === '') {
|
||
$name2 = __('钻石会员', 'jio');
|
||
$name2_html = $name2;
|
||
}
|
||
|
||
$badge = $vip2_enabled ? $name2 : ($vip1_enabled ? $name1 : __('VIP', 'jio'));
|
||
|
||
$renew_price_type = sanitize_key((string) ($settings['vip_renew_price_type'] ?? 'discount'));
|
||
if (!in_array($renew_price_type, ['same', 'discount', 'reduce', 'customize'], true)) {
|
||
$renew_price_type = 'discount';
|
||
}
|
||
|
||
$desc = wp_kses_post((string) ($settings['pay_user_vip_desc'] ?? __('开通VIP会员,享受会员专属折扣以及多项特权', 'jio')));
|
||
$more = (string) ($settings['pay_user_vip_more'] ?? '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>');
|
||
|
||
return [
|
||
'enabled' => $enabled,
|
||
'nav_pay_vip' => !isset($settings['nav_pay_vip']) || !empty($settings['nav_pay_vip']),
|
||
'nav_user_pay_vip' => !isset($settings['nav_user_pay_vip']) || !empty($settings['nav_user_pay_vip']),
|
||
'name' => $vip1_enabled ? $name1 : $badge,
|
||
'badge_text' => $badge,
|
||
'desc' => $desc,
|
||
// 管理员编辑的富文本说明:与保存端同为 post 级 HTML(wp_kses_post),
|
||
// 不再用权益列表的窄白名单二次剥标签(h/img/hr/table 等)。
|
||
'more' => wp_kses_post($more),
|
||
'levels' => [
|
||
1 => [
|
||
'enabled' => $vip1_enabled,
|
||
'name' => $name1,
|
||
'name_html' => $name1_html,
|
||
'desc' => wp_kses_post((string) ($settings['pay_user_vip_1_desc'] ?? __('尊享专属特权', 'jio'))),
|
||
'equity' => wp_kses_post((string) (($settings['pay_user_vip_1_equity'] ?? '') !== '' ? $settings['pay_user_vip_1_equity'] : '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>')),
|
||
'img_icon' => self::get_vip_icon_url(1, (string) ($settings['vip_1_img_icon'] ?? '')),
|
||
'product_s' => !isset($settings['vip_1_product_s']) || !empty($settings['vip_1_product_s']),
|
||
'products' => self::normalize_vip_product_items($settings['vip_1_product'] ?? null, self::vip_product_defaults()),
|
||
'renew_products' => self::normalize_vip_product_items($settings['vip_1_renew_product'] ?? null, self::vip_renew_product_defaults(1)),
|
||
'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_1'] ?? 0))),
|
||
],
|
||
2 => [
|
||
'enabled' => $vip2_enabled,
|
||
'name' => $name2,
|
||
'name_html' => $name2_html,
|
||
'desc' => wp_kses_post((string) ($settings['pay_user_vip_2_desc'] ?? __('尊享专属特权', 'jio'))),
|
||
'equity' => wp_kses_post((string) (($settings['pay_user_vip_2_equity'] ?? '') !== '' ? $settings['pay_user_vip_2_equity'] : '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>')),
|
||
'img_icon' => self::get_vip_icon_url(2, (string) ($settings['vip_2_img_icon'] ?? '')),
|
||
'product_s' => !isset($settings['vip_2_product_s']) || !empty($settings['vip_2_product_s']),
|
||
'products' => self::normalize_vip_product_items($settings['vip_2_product'] ?? null, self::vip_product_defaults()),
|
||
'renew_products' => self::normalize_vip_product_items($settings['vip_2_renew_product'] ?? null, self::vip_renew_product_defaults(2)),
|
||
'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_2'] ?? 0))),
|
||
],
|
||
],
|
||
'benefit' => [
|
||
'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit'] ?? 0))),
|
||
],
|
||
'renew' => [
|
||
'enabled' => !isset($settings['vip_renew']) || !empty($settings['vip_renew']),
|
||
'desc' => wp_kses_post((string) ($settings['vip_renew_desc'] ?? '立即续费会员,畅享VIP权益')),
|
||
'price_type' => $renew_price_type,
|
||
'discount' => max(0.1, min(9.9, (float) ($settings['vip_renew_discount'] ?? 8))),
|
||
'reduce' => self::money_decimal($settings['vip_renew_reduce'] ?? '20.00', '20.00', 50000),
|
||
],
|
||
'upgrade' => self::normalize_vip_upgrade_settings($settings),
|
||
'points_exchange' => [
|
||
'enabled' => !empty($settings['pay_vip_points_exchange_s']),
|
||
'desc' => wp_kses_post((string) ($settings['pay_vip_points_exchange_desc'] ?? '')),
|
||
'packages' => self::normalize_vip_points_packages($settings['pay_vip_points_exchange_product'] ?? null),
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Creator-income policy. Cash and points intentionally use separate ratio sets.
|
||
*
|
||
* @return array{enabled:bool,cycle_days:int,ratios:array<string,array<string,int>>,note:string}
|
||
*/
|
||
public static function get_payment_revenue_settings(): array {
|
||
$settings = Settings::get_all();
|
||
$note = trim((string) ($settings['payment_revenue_note'] ?? ''));
|
||
$clamp_ratio = static fn ($value): int => max(0, min(100, (int) $value));
|
||
|
||
return [
|
||
'enabled' => !empty($settings['pay_income_s']),
|
||
'cycle_days' => max(1, min(365, (int) ($settings['payment_revenue_cycle_days'] ?? 30))),
|
||
'ratios' => [
|
||
'cash' => [
|
||
'ordinary' => $clamp_ratio($settings['payment_revenue_cash_ratio_user'] ?? 60),
|
||
'vip1' => $clamp_ratio($settings['payment_revenue_cash_ratio_vip_1'] ?? 70),
|
||
'vip2' => $clamp_ratio($settings['payment_revenue_cash_ratio_vip_2'] ?? 80),
|
||
],
|
||
'points' => [
|
||
'ordinary' => $clamp_ratio($settings['payment_revenue_points_ratio_user'] ?? 0),
|
||
'vip1' => $clamp_ratio($settings['payment_revenue_points_ratio_vip_1'] ?? 0),
|
||
'vip2' => $clamp_ratio($settings['payment_revenue_points_ratio_vip_2'] ?? 0),
|
||
],
|
||
],
|
||
'note' => $note !== '' ? wp_kses_post($note) : __('<p>订单完成并经过结算周期后,作者分成会自动转入对应的钱包。</p>', 'jio'),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 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));
|
||
|
||
$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['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['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_by_level[0],
|
||
'order_types_by_level' => $types_by_level,
|
||
'disabled_code_stops_commission' => !empty($settings['affiliate_disabled_code_stops_commission']),
|
||
'window_days' => (int) CreditsRuntime::get_referral_settings()['window_days'],
|
||
'promotion_text' => sanitize_textarea_field((string) ($settings['pay_rebate_text_details'] ?? '')),
|
||
'details_title' => sanitize_text_field((string) ($settings['pay_rebate_text_details_title'] ?? '返佣详解')),
|
||
'poster' => [
|
||
'enabled' => !isset($settings['pay_rebate_poster_s']) || !empty($settings['pay_rebate_poster_s']),
|
||
'image' => esc_url_raw((string) ($settings['pay_rebate_poster_img'] ?? '')),
|
||
'title' => sanitize_text_field((string) ($settings['pay_rebate_poster_title'] ?? '')),
|
||
'description' => sanitize_text_field((string) ($settings['pay_rebate_poster_desc'] ?? '')),
|
||
'banner_default' => esc_url_raw((string) ($settings['pay_rebate_poster_banner'] ?? '')),
|
||
'banner_spare' => get_template_directory_uri() . '/assets/img/share_img.jpg',
|
||
'bottom' => sanitize_text_field((string) ($settings['pay_rebate_poster_desc_bottom'] ?? '')),
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 某等级推广者可获返佣的订单类型;空数组代表该等级用户不参与推广返佣。
|
||
*
|
||
* @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('jio_get_user_vip_level') ? (int) jio_get_user_vip_level($user_id) : 0;
|
||
return self::affiliate_ratio_for_level($level);
|
||
}
|
||
public static function affiliate_ratio_for_level(int $level): int {
|
||
$settings = self::get_payment_affiliate_settings();
|
||
$key = $level >= 2 ? 'vip_2' : ($level >= 1 ? 'vip_1' : 'user');
|
||
return (int) ($settings['ratios'][$key] ?? 0);
|
||
}
|
||
public static function get_affiliate_settings(): array { return self::get_payment_affiliate_settings(); }
|
||
|
||
/**
|
||
* 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();
|
||
|
||
$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'])
|
||
|| !empty($settings['points_pass_exchange_s'])
|
||
|| !empty($settings['pay_vip_pass_charge_s']);
|
||
|
||
return [
|
||
'help_text' => __('微信 / 支付宝可在官方与易支付之间选择;PayPal、Stripe 独立开关。异步回调由各网关验签。', 'jio'),
|
||
'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'] ?? '')),
|
||
'qrcode' => !array_key_exists('epay_qrcode', $settings) || !empty($settings['epay_qrcode']),
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Order display / checkout policy (legacy pay_* + Jio order ops).
|
||
*
|
||
* @return array<string,mixed>
|
||
*/
|
||
public static function get_order_settings(): array {
|
||
$s = Settings::get_all();
|
||
|
||
$expiry = max(5, min(1440, (int) ($s['payment_order_expiry'] ?? 30)));
|
||
$cookie_day = max(1, min(31, (int) ($s['pay_cookie_day'] ?? 15)));
|
||
|
||
$box_position = sanitize_key((string) ($s['pay_box_position'] ?? 'top'));
|
||
if (!in_array($box_position, ['box_top', 'top', 'bottom', 'box_bottom'], true)) {
|
||
$box_position = 'top';
|
||
}
|
||
|
||
$status_keys = ['pending', 'paid', 'refunded', 'closed'];
|
||
$status_defaults = ['待支付', '已支付', '已退款', '已关闭'];
|
||
$status_texts = [];
|
||
foreach ($status_keys as $i => $key) {
|
||
$val = trim((string) ($s["payment_order_status_{$key}"] ?? ''));
|
||
$status_texts[$key] = $val !== '' ? $val : $status_defaults[$i];
|
||
}
|
||
|
||
return [
|
||
'expiry' => $expiry,
|
||
'show_user' => !isset($s['pay_show_user']) || !empty($s['pay_show_user']),
|
||
'show_paycount' => !isset($s['pay_show_paycount']) || !empty($s['pay_show_paycount']),
|
||
'no_logged_in' => !isset($s['pay_no_logged_in']) || !empty($s['pay_no_logged_in']),
|
||
'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']),
|
||
'mark' => sanitize_text_field((string) ($s['pay_mark'] ?? '付费')),
|
||
'box_position' => $box_position,
|
||
'show_ip' => !empty($s['payment_show_ip']),
|
||
'show_device' => !empty($s['payment_show_device']),
|
||
'show_order_no' => !isset($s['payment_show_order_no']) || !empty($s['payment_show_order_no']),
|
||
'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' => 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' => '',
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Return the UTC expiry time for a newly created payable order.
|
||
*
|
||
* Keeping this in the payment runtime prevents individual checkout
|
||
* flows from drifting away from the admin "payment_order_expiry" setting.
|
||
*/
|
||
public static function get_order_expiry_at(?int $now = null): string {
|
||
$settings = self::get_order_settings();
|
||
$timestamp = $now ?? time();
|
||
$minutes = max(5, min(1440, (int) ($settings['expiry'] ?? 30)));
|
||
|
||
return gmdate('Y-m-d H:i:s', $timestamp + $minutes * MINUTE_IN_SECONDS);
|
||
}
|
||
|
||
/**
|
||
* @return array{min_amount:string,service_charge_rate:int,daily_count:int,daily_limit:string,reset_time:string,payout_channels:string[]}
|
||
*/
|
||
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']; }
|
||
return [
|
||
'min_amount' => self::money_decimal($s['affiliate_min_withdrawal'] ?? '50.00', '50.00', 99999900),
|
||
'service_charge_rate' => max(0, min(100, (int) ($s['affiliate_withdrawal_fee'] ?? 5))),
|
||
'daily_count' => max(0, min(99999, (int) ($s['affiliate_daily_withdrawal_count'] ?? 3))),
|
||
'daily_limit' => self::money_decimal($s['affiliate_daily_withdrawal_limit'] ?? '500.00', '500.00', 99999900),
|
||
'reset_time' => preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', (string) ($s['affiliate_withdrawal_reset_time'] ?? '00:00'))
|
||
? (string) $s['affiliate_withdrawal_reset_time']
|
||
: '00:00',
|
||
'payout_channels' => $channels,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Whether a registered adapter is admin-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);
|
||
|
||
$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';
|
||
}
|
||
|
||
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'])
|
||
|| !empty($settings['points_pass_exchange_s'])
|
||
|| !empty($settings['pay_vip_pass_charge_s']);
|
||
case 'epay':
|
||
return $wechat === 'epay' || $alipay === 'epay';
|
||
case 'epay_alipay':
|
||
return $alipay === 'epay';
|
||
case 'epay_wxpay':
|
||
return $wechat === 'epay';
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private static function count_structured_lines(string $raw): int {
|
||
$lines = preg_split('/[\r\n]+/', $raw, -1, PREG_SPLIT_NO_EMPTY);
|
||
$count = 0;
|
||
foreach ((array) $lines as $line) {
|
||
if (trim((string) $line) !== '') {
|
||
$count++;
|
||
}
|
||
}
|
||
return $count;
|
||
}
|
||
|
||
private static function money_decimal(mixed $value, string $default, int $max_minor = PHP_INT_MAX): string {
|
||
try {
|
||
if (!is_string($value)) {
|
||
throw new \InvalidArgumentException('money_decimal_must_be_string');
|
||
}
|
||
$amount = \Jio\Ledger\AssetAmount::balance(trim($value));
|
||
return $amount->minor() <= $max_minor ? $amount->decimal() : $default;
|
||
} catch (\InvalidArgumentException $e) {
|
||
return $default;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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 {
|
||
$lines = [];
|
||
foreach ($products as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$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);
|
||
}
|
||
|
||
/**
|
||
* Format points product rows (reference-theme group shape:
|
||
* points 积分 / pay_price 销售金额 / tag 小标签) into "积分|支付|标签" lines.
|
||
*
|
||
* @param array<int,mixed> $products
|
||
*/
|
||
private static function format_points_product_lines(array $products): string {
|
||
$lines = [];
|
||
foreach ($products as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$points = max(0, (int) ($row['points'] ?? 0));
|
||
$pay_price = max(0, (float) ($row['pay_price'] ?? 0));
|
||
$title = trim((string) ($row['tag'] ?? ''));
|
||
$lines[] = $points . '|' . $pay_price . '|' . $title;
|
||
}
|
||
return implode("\n", $lines);
|
||
}
|
||
|
||
/**
|
||
* Approximate days for catalog labels / order meta; activation prefers
|
||
* calendar time+unit math.
|
||
*/
|
||
private static function duration_to_days(int $time, string $unit = 'day'): int {
|
||
if ($time <= 0) {
|
||
return 0;
|
||
}
|
||
return $unit === 'month' ? $time * 30 : $time;
|
||
}
|
||
|
||
/**
|
||
* 会员商品默认两行(参考主题 vip_tab 的 group 默认值)。
|
||
*
|
||
* @return array<int,array<string,mixed>>
|
||
*/
|
||
private static function vip_product_defaults(): array {
|
||
return [
|
||
['price' => '99.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||
['price' => '199.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* @return array<int,array<string,mixed>>
|
||
*/
|
||
private static function vip_renew_product_defaults(int $level): array {
|
||
if ($level === 2) {
|
||
return [
|
||
['price' => '269.00', 'show_price' => '599.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||
['price' => '369.00', 'show_price' => '899.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||
];
|
||
}
|
||
return [
|
||
['price' => '69.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||
['price' => '169.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 标签颜色可选值(b / jb 前缀两类)。
|
||
*
|
||
* @return array<int,string>
|
||
*/
|
||
private static function vip_tag_palette_keys(): array {
|
||
return ['b-red', 'b-yellow', 'b-cyan', 'b-blue', 'b-green', 'b-purple', 'b-black', 'jb-red', 'jb-pink', 'jb-yellow', 'jb-cyan', 'jb-blue', 'jb-green', 'jb-purple', 'jb-vip1', 'jb-vip2'];
|
||
}
|
||
|
||
/**
|
||
* Normalize a structured 会员商品 group into purchasable rows.
|
||
*
|
||
* @param mixed $raw Stored group value.
|
||
* @param array<int,array<string,mixed>> $fallback Default rows when unset.
|
||
* @return array<int,array{price:string,show_price:string,tag:string,tag_class:string,time:int,unit:string,days:int}>
|
||
*/
|
||
private static function normalize_vip_product_items($raw, array $fallback): array {
|
||
$rows = is_array($raw) ? array_values($raw) : [];
|
||
if ($rows === []) {
|
||
$rows = $fallback;
|
||
}
|
||
$palette = self::vip_tag_palette_keys();
|
||
$out = [];
|
||
foreach ($rows as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$price = self::money_decimal($row['price'] ?? '', '0.00');
|
||
if (bccomp($price, '0.00', 2) <= 0) {
|
||
continue;
|
||
}
|
||
$time = max(0, (int) ($row['time'] ?? 0));
|
||
$unit = ((string) ($row['unit'] ?? 'month')) === 'day' ? 'day' : 'month';
|
||
$tag_class = (string) ($row['tag_class'] ?? '');
|
||
if (!in_array($tag_class, $palette, true)) {
|
||
$tag_class = '';
|
||
}
|
||
$out[] = [
|
||
'price' => $price,
|
||
'show_price' => self::money_decimal($row['show_price'] ?? '', '0.00'),
|
||
'tag' => wp_kses_post((string) ($row['tag'] ?? '')),
|
||
'tag_class' => $tag_class,
|
||
'time' => $time,
|
||
'unit' => $unit,
|
||
'days' => self::duration_to_days($time, $unit),
|
||
];
|
||
}
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* Normalize the structured 升级价格 accordion.
|
||
*
|
||
* @param array<string,mixed> $settings
|
||
* @return array<string,mixed>
|
||
*/
|
||
private static function normalize_vip_upgrade_settings(array $settings): array {
|
||
$raw = isset($settings['vip_upgrade_product']) && is_array($settings['vip_upgrade_product'])
|
||
? $settings['vip_upgrade_product']
|
||
: [];
|
||
$palette = self::vip_tag_palette_keys();
|
||
$money = static function ($value, string $default): string {
|
||
return self::money_decimal(is_string($value) ? $value : (is_numeric($value) ? (string) $value : ''), $default);
|
||
};
|
||
$tag = static function ($value, string $default): string {
|
||
$text = trim((string) $value);
|
||
return $text !== '' ? wp_kses_post($text) : $default;
|
||
};
|
||
$tag_class = static function ($value) use ($palette): string {
|
||
$key = (string) $value;
|
||
return in_array($key, $palette, true) ? $key : '';
|
||
};
|
||
$switch = static function ($value, bool $default): bool {
|
||
return isset($value) ? !empty($value) : $default;
|
||
};
|
||
|
||
return [
|
||
'enabled' => !isset($settings['vip_upgrade']) || !empty($settings['vip_upgrade']),
|
||
'desc' => wp_kses_post((string) ($settings['vip_upgrade_desc'] ?? '升级VIP会员,享更多会员权益')),
|
||
'month_to_month_s' => $switch($raw['month_to_month_s'] ?? null, true),
|
||
'unit_price' => $money($raw['unit_price'] ?? null, '10.00'),
|
||
'unit_tag' => $tag($raw['unit_tag'] ?? null, '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐'),
|
||
'unit_tag_class' => $tag_class($raw['unit_tag_class'] ?? 'jb-yellow'),
|
||
'jump_s' => $switch($raw['jump_s'] ?? null, false),
|
||
'jump_price' => $money($raw['jump_price'] ?? null, '199.00'),
|
||
'jump_show_price' => $money($raw['jump_show_price'] ?? null, '299.00'),
|
||
'jump_tag' => $tag($raw['jump_tag'] ?? null, '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠'),
|
||
'jump_tag_class' => $tag_class($raw['jump_tag_class'] ?? 'jb-red'),
|
||
'permanent_price' => $money($raw['permanent_price'] ?? null, '199.00'),
|
||
'permanent_show_price' => $money($raw['permanent_show_price'] ?? null, '299.00'),
|
||
'permanent_tag' => $tag($raw['permanent_tag'] ?? null, '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠'),
|
||
'permanent_tag_class' => $tag_class($raw['permanent_tag_class'] ?? 'jb-red'),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 积分兑换明细默认行(对齐旧 textarea 的开箱配置)。
|
||
*
|
||
* @return array<int,array<string,mixed>>
|
||
*/
|
||
private static function vip_points_package_defaults(): array {
|
||
return [
|
||
['points' => 500, 'level' => 1, 'time' => 30, 'unit' => 'day'],
|
||
['points' => 5000, 'level' => 1, 'time' => 'Permanent'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Normalize the structured 积分兑换明细 group. `time` is the literal string
|
||
* "Permanent" for a permanent grant, otherwise a day/month count.
|
||
*
|
||
* @param mixed $raw
|
||
* @return array<int,array{points:int,level:int,time:int,unit:string,is_permanent:bool,days:int}>
|
||
*/
|
||
private static function normalize_vip_points_packages($raw): array {
|
||
$rows = is_array($raw) ? array_values($raw) : self::vip_points_package_defaults();
|
||
$out = [];
|
||
foreach ($rows as $row) {
|
||
if (!is_array($row)) {
|
||
continue;
|
||
}
|
||
$points = max(0, (int) ($row['points'] ?? 0));
|
||
$level = (int) ($row['level'] ?? 1);
|
||
$time_raw = trim((string) ($row['time'] ?? ''));
|
||
if ($points <= 0 || $level < 1 || $level > 2 || $time_raw === '') {
|
||
continue;
|
||
}
|
||
$is_permanent = strcasecmp($time_raw, 'Permanent') === 0;
|
||
$time = $is_permanent ? 0 : max(0, (int) $time_raw);
|
||
$unit = ((string) ($row['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||
$out[] = [
|
||
'points' => $points,
|
||
'level' => $level,
|
||
'time' => $time,
|
||
'unit' => $unit,
|
||
'is_permanent' => $is_permanent,
|
||
'days' => $is_permanent ? 0 : self::duration_to_days($time, $unit),
|
||
];
|
||
}
|
||
return $out;
|
||
}
|
||
}
|