Files
wordpress-theme-aksrt/src/Settings/PaymentRuntime.php
T
kate522andClaude Opus 5 427968a721 chore(branding): remove leftover upstream theme identifiers
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>
2026-11-02 00:06:54 +08:00

817 lines
40 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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;
}
}