Merge pull request 'refactor(classes): split oversize modules + bootstrap hygiene' (#191) from worktree-organize-inc-classes into main

Reviewed-on: CYL/wordpress-theme-aksrt#191
This commit is contained in:
2026-07-17 11:44:29 +08:00
28 changed files with 7350 additions and 6688 deletions
+4
View File
@@ -199,6 +199,10 @@ if (class_exists('AKSRT_Download_State')) {
if (class_exists('AKSRT_Docs_Presenter')) {
AKSRT_Docs_Presenter::get_instance();
}
// Homepage module preview REST + query var (render still via helpers / static methods)
if (class_exists('AKSRT_Home_Modules')) {
AKSRT_Home_Modules::get_instance()->init();
}
if (class_exists('AKSRT_Content_Visibility')) {
AKSRT_Content_Visibility::get_instance()->init();
}
+18 -8
View File
@@ -67,6 +67,7 @@ require_once AKSRT_DIR . '/inc/classes/core/setup-traits/trait-aksrt-setup-part-
require_once AKSRT_DIR . '/inc/classes/core/setup-traits/trait-aksrt-setup-part-2.php';
require_once AKSRT_DIR . '/inc/classes/core/class-aksrt-setup.php';
require_once AKSRT_DIR . '/inc/classes/core/scripts-traits/trait-aksrt-scripts-part-1b.php';
require_once AKSRT_DIR . '/inc/classes/core/scripts-traits/trait-aksrt-scripts-part-1c.php';
require_once AKSRT_DIR . '/inc/classes/core/scripts-traits/trait-aksrt-scripts-part-2.php';
require_once AKSRT_DIR . '/inc/classes/core/class-aksrt-scripts.php';
require_once AKSRT_DIR . '/inc/classes/frontend/class-aksrt-inline-style-builder.php';
@@ -93,6 +94,7 @@ require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-integration
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-extension-runtime.php';
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-forum-shop.php';
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings-capabilities.php';
require_once AKSRT_DIR . '/inc/classes/settings/trait-aksrt-settings-facade-bulk.php';
require_once AKSRT_DIR . '/inc/classes/settings/class-aksrt-settings.php';
// --- admin: SPA controllers + schema + native ops ---
@@ -210,6 +212,7 @@ require_once AKSRT_DIR . '/inc/classes/auth/class-aksrt-email-rollback.php';
// --- user ---
require_once AKSRT_DIR . '/inc/classes/user/class-aksrt-user.php';
require_once AKSRT_DIR . '/inc/classes/user/class-aksrt-user-notifications.php';
require_once AKSRT_DIR . '/inc/classes/user/class-aksrt-user-center.php';
require_once AKSRT_DIR . '/inc/classes/user/class-aksrt-user-home.php';
require_once AKSRT_DIR . '/inc/classes/user/class-aksrt-user-restriction.php';
@@ -231,6 +234,9 @@ require_once AKSRT_DIR . '/inc/classes/core/class-aksrt-db-migrator.php';
// --- credits / wallet ---
require_once AKSRT_DIR . '/inc/classes/credits/class-aksrt-credits.php';
require_once AKSRT_DIR . '/inc/classes/credits/class-aksrt-balance.php';
require_once AKSRT_DIR . '/inc/classes/credits/balance-service-traits/trait-aksrt-balance-service-rest.php';
require_once AKSRT_DIR . '/inc/classes/credits/balance-service-traits/trait-aksrt-balance-service-charge.php';
require_once AKSRT_DIR . '/inc/classes/credits/balance-service-traits/trait-aksrt-balance-service-points.php';
require_once AKSRT_DIR . '/inc/classes/credits/class-aksrt-balance-service.php';
require_once AKSRT_DIR . '/inc/classes/credits/class-aksrt-points-free.php';
require_once AKSRT_DIR . '/inc/classes/credits/class-aksrt-credits-earn.php';
@@ -239,13 +245,19 @@ require_once AKSRT_DIR . '/inc/classes/credits/class-aksrt-invite.php';
// --- content pay / download / VIP ---
require_once AKSRT_DIR . '/inc/classes/content/class-aksrt-download.php';
require_once AKSRT_DIR . '/inc/classes/content/content-pay-traits/trait-aksrt-content-pay-rest.php';
require_once AKSRT_DIR . '/inc/classes/content/content-pay-traits/trait-aksrt-content-pay-orders.php';
require_once AKSRT_DIR . '/inc/classes/content/content-pay-traits/trait-aksrt-content-pay-helpers.php';
require_once AKSRT_DIR . '/inc/classes/content/class-aksrt-content-pay.php';
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-topic-pay.php';
require_once AKSRT_DIR . '/inc/classes/forum/class-aksrt-plate-follow-pay.php';
require_once AKSRT_DIR . '/inc/classes/content/vip-service-traits/trait-aksrt-vip-service-rest.php';
require_once AKSRT_DIR . '/inc/classes/content/vip-service-traits/trait-aksrt-vip-service-purchase.php';
require_once AKSRT_DIR . '/inc/classes/content/vip-service-traits/trait-aksrt-vip-service-packages.php';
require_once AKSRT_DIR . '/inc/classes/content/class-aksrt-vip-service.php';
require_once AKSRT_DIR . '/inc/classes/content/class-aksrt-download-state.php';
require_once AKSRT_DIR . '/inc/classes/core/class-aksrt-docs-presenter.php';
// CMS 首页模块(类存在;模板侧目前主要通过 helpers 渲染)
// CMS 首页模块(预览 REST + 默认模板;模板主路径仍在 helpers)
require_once AKSRT_DIR . '/inc/classes/core/class-aksrt-home-modules.php';
require_once AKSRT_DIR . '/inc/classes/content/class-aksrt-content-visibility.php';
@@ -271,6 +283,8 @@ require_once AKSRT_DIR . '/inc/classes/messages/class-aksrt-private-message.php'
require_once AKSRT_DIR . '/inc/classes/messages/class-aksrt-report-center.php';
require_once AKSRT_DIR . '/inc/classes/payment/class-aksrt-coupon.php';
require_once AKSRT_DIR . '/inc/classes/payment/class-aksrt-order.php';
require_once AKSRT_DIR . '/inc/classes/payment/order-repository-traits/trait-aksrt-order-repository-core.php';
require_once AKSRT_DIR . '/inc/classes/payment/order-repository-traits/trait-aksrt-order-repository-admin.php';
require_once AKSRT_DIR . '/inc/classes/payment/class-aksrt-order-repository.php';
require_once AKSRT_DIR . '/inc/classes/payment/class-aksrt-payment-gateway.php';
require_once AKSRT_DIR . '/inc/classes/payment/gateways/class-aksrt-alipay-gateway.php';
@@ -345,19 +359,15 @@ require_once AKSRT_DIR . '/inc/classes/core/class-aksrt-post-meta-box.php';
require_once AKSRT_DIR . '/inc/classes/core/class-aksrt-resource-meta-box.php';
// --- widgets ---
require_once AKSRT_DIR . '/inc/classes/widgets/class-aksrt-widgets-original.php';
require_once AKSRT_DIR . '/inc/classes/widgets/class-aksrt-widgets-new.php';
require_once AKSRT_DIR . '/inc/classes/widgets/class-aksrt-widgets-batch1.php';
// 自动加载 widgets 目录下的所有小工具文件(含 registry 之外的独立 widget)
// 包文件(多 widget 合一)+ 独立 widget 文件 + 注册中心。
// 使用单一 glob:目录内所有 class-*.php(含 original/new/batch1/registry)。
// require_once 幂等;新增 widget 只需丢进 widgets/ 即可被加载。
$aksrt_widgets_dir = AKSRT_DIR . '/inc/classes/widgets/';
if (is_dir($aksrt_widgets_dir)) {
foreach (glob($aksrt_widgets_dir . 'class-*.php') as $aksrt_widget_file) {
// Registry 文件单独 require,避免与下方显式加载重复声明;glob 会再次命中
// 但 require_once 幂等,安全。
require_once $aksrt_widget_file;
}
}
require_once AKSRT_DIR . '/inc/classes/widgets/class-aksrt-widgets.php';
// ============================================================
// 阶段3:核心功能模块(Gravatar替换 / SMTP邮件)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,497 @@
<?php
/**
* Extracted trait: AKSRT_Content_Pay_Helpers
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Content_Pay_Helpers {
/**
* Cashier preview payload.
*
* @return array|WP_Error
*/
public function preview(int $post_id, int $user_id = 0, string $coupon_code = '', string $price_type = '') {
$post = get_post($post_id);
if (!($post instanceof WP_Post) || $post->post_status !== 'publish') {
return new WP_Error('aksrt_invalid', __('内容不存在', 'aksrt'), ['status' => 404]);
}
$meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post) : [];
$pay_type = AKSRT_Domain_Model::normalize_pay_type((string) ($meta['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO));
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_NO) {
return new WP_Error('aksrt_not_paid', __('该内容无需购买', 'aksrt'), ['status' => 400]);
}
$uid = $user_id > 0 ? $user_id : get_current_user_id();
$priced = function_exists('aksrt_get_user_pay_price')
? aksrt_get_user_pay_price($post, $uid)
: ['amount' => 0.0, 'base' => 0.0, 'modo' => '0', 'currency' => '¥', 'is_free' => true, 'pay_limit' => 0, 'vip_level' => 0, 'original' => 0.0, 'vip1' => 0.0, 'vip2' => 0.0];
// Optional overbuy override for preview display.
if ($price_type === 'download_limit_over') {
$over = max(0.0, (float) ($meta['download_limit_over_price'] ?? 0));
if ($over > 0) {
$priced['amount'] = $over;
$priced['base'] = $over;
$priced['is_free'] = false;
}
}
$pay_modo = (($priced['modo'] ?? '0') === 'points') ? 'points' : '0';
$pay_limit = (int) ($priced['pay_limit'] ?? 0);
$vip_level = (int) ($priced['vip_level'] ?? 0);
$amount = (float) ($priced['amount'] ?? 0);
$base = (float) ($priced['base'] ?? $amount);
$over_price = max(0.0, (float) ($meta['download_limit_over_price'] ?? 0));
$list_amount = $amount;
$rebate_discount = 0.0;
$coupon_discount = 0.0;
$coupon_payload = null;
$coupon_error = '';
$referrer_active = false;
$referrer_id = 0;
$rebate_price = 0.0;
$payable = $amount;
$coupon_allowed = $pay_modo !== 'points' && $this->is_coupon_allowed($post_id, $meta);
$coupon_desc = '';
if ($coupon_allowed) {
$coupon_desc = trim((string) ($meta['coupon_desc'] ?? ''));
if ($coupon_desc === '' && class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_content_coupon_settings')) {
$coupon_desc = (string) (AKSRT_Settings::get_content_coupon_settings()['desc'] ?? '');
}
}
if ($pay_modo !== 'points') {
$discounted = $this->apply_cash_discounts($priced, $post, $meta, $uid, $coupon_code, true);
if (is_wp_error($discounted)) {
// Preview: soft-fail coupon (show error, keep base payable without coupon).
$coupon_error = $discounted->get_error_message();
$discounted = $this->apply_cash_discounts($priced, $post, $meta, $uid, '', true);
}
if (!is_wp_error($discounted)) {
$list_amount = (float) ($discounted['list_amount'] ?? $amount);
$rebate_discount = (float) ($discounted['rebate_discount'] ?? 0);
$coupon_discount = (float) ($discounted['coupon_discount'] ?? 0);
$coupon_payload = $discounted['coupon'] ?? null;
$payable = (float) ($discounted['payable'] ?? $amount);
$referrer_id = (int) ($discounted['referrer_id'] ?? 0);
$referrer_active = $referrer_id > 0;
$rebate_price = (float) ($discounted['rebate_price'] ?? 0);
$amount = $payable;
}
}
$has_paid = false;
$paid_order = null;
if ($uid > 0 && class_exists('AKSRT_Order')) {
$has_paid = AKSRT_Order::get_instance()->has_paid_access(
$uid,
AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
$post_id
);
if ($has_paid && function_exists('aksrt_get_user_content_paid_order')) {
$paid_order = aksrt_get_user_content_paid_order($post_id, $uid);
}
}
if (!$has_paid && $uid <= 0 && function_exists('aksrt_get_guest_content_paid_order')) {
$guest_order = aksrt_get_guest_content_paid_order($post_id);
if ($guest_order) {
$has_paid = true;
$paid_order = $guest_order;
}
}
$pending_order = null;
if ($uid > 0 && !$has_paid && class_exists('AKSRT_Order_Repository')) {
$pending_order = $this->find_pending_content_order($uid, $post_id);
}
$need_vip = $pay_limit > 0 && $vip_level < $pay_limit;
$allow_guest = function_exists('aksrt_allow_guest_content_pay') && aksrt_allow_guest_content_pay($post);
$currency = (string) ($priced['currency'] ?? ($pay_modo === 'points'
? (class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt'))
: '¥'));
$balance = ($uid > 0 && class_exists('AKSRT_Credits')) ? AKSRT_Credits::balance($uid) : 0;
$methods = $this->available_payment_methods($pay_modo, $uid > 0);
$amount_label = $this->format_amount_label($amount, $pay_modo, $currency);
$base_label = $this->format_amount_label($base, $pay_modo, $currency);
$list_label = $this->format_amount_label($list_amount, $pay_modo, $currency);
$over_label = $over_price > 0 ? $this->format_amount_label($over_price, $pay_modo, $currency) : '';
$vip_page = function_exists('aksrt_get_user_center_tab_url')
? aksrt_get_user_center_tab_url('credits')
: (function_exists('aksrt_get_user_center_url')
? aksrt_get_user_center_url()
: home_url('/'));
$free_limit = $uid > 0 && function_exists('aksrt_get_user_free_download_limit')
? aksrt_get_user_free_download_limit($uid) : 0;
$free_used = $uid > 0 && function_exists('aksrt_get_user_free_downloaded_number')
? aksrt_get_user_free_downloaded_number($uid) : 0;
$quota_over = $free_limit > 0 && $free_used >= $free_limit;
return [
'post_id' => $post_id,
'title' => get_the_title($post),
'permalink' => get_permalink($post),
'pay_type' => $pay_type,
'pay_type_label' => AKSRT_Domain_Model::pay_type_labels()[$pay_type] ?? $pay_type,
'pay_modo' => $pay_modo,
'pay_limit' => $pay_limit,
'vip_level' => $vip_level,
'amount' => $pay_modo === 'points' ? (int) ceil($amount) : round($amount, 2),
'base' => $pay_modo === 'points' ? (int) ceil($base) : round($base, 2),
'list_amount' => $pay_modo === 'points' ? (int) ceil($list_amount) : round($list_amount, 2),
'list_amount_label' => $list_label,
'payable' => $pay_modo === 'points' ? (int) ceil($payable) : round($payable, 2),
'original' => (float) ($priced['original'] ?? 0),
'vip1' => (float) ($priced['vip1'] ?? 0),
'vip2' => (float) ($priced['vip2'] ?? 0),
'currency' => $currency,
'amount_label' => $amount_label,
'base_label' => $base_label,
'is_free' => !empty($priced['is_free']) || $amount <= 0,
'has_paid_access' => $has_paid,
'paid_order' => $paid_order ? [
'order_number' => (string) ($paid_order['order_number'] ?? ''),
'paid_at' => (string) ($paid_order['paid_at'] ?? $paid_order['created_at'] ?? ''),
'amount' => (float) ($paid_order['amount'] ?? 0),
'currency' => (string) ($paid_order['currency'] ?? ''),
'payment_method' => (string) ($paid_order['payment_method'] ?? ''),
] : null,
'pending_order' => $pending_order ? [
'order_number' => (string) ($pending_order['order_number'] ?? ''),
'amount' => (float) ($pending_order['amount'] ?? 0),
'payment_method' => (string) ($pending_order['payment_method'] ?? ''),
'created_at' => (string) ($pending_order['created_at'] ?? ''),
] : null,
'need_login' => $uid <= 0 && !$allow_guest,
'allow_guest' => $allow_guest,
'need_vip' => $need_vip,
'need_purchase' => !$has_paid && !$need_vip && $amount > 0,
'logged_in' => $uid > 0,
'balance' => $balance,
'balance_enough' => $pay_modo !== 'points' || $balance >= (int) ceil($amount),
'payment_methods' => $methods,
'login_url' => wp_login_url(get_permalink($post)),
'vip_url' => $vip_page,
'credits_url' => $vip_page,
'download_limit_over_price' => $pay_modo === 'points' ? (int) ceil($over_price) : round($over_price, 2),
'download_limit_over_label' => $over_label,
'allow_limit_over' => $over_price > 0,
'quota_over' => $quota_over,
'free_limit' => $free_limit,
'free_used' => $free_used,
'coupon_allowed' => $coupon_allowed,
'coupon_desc' => $coupon_desc,
'coupon_code' => $coupon_code,
'coupon' => $coupon_payload,
'coupon_error' => $coupon_error,
'rebate_discount' => round($rebate_discount, 2),
'rebate_discount_label' => $rebate_discount > 0 ? ('-' . $this->format_amount_label($rebate_discount, $pay_modo, $currency)) : '',
'coupon_discount' => round($coupon_discount, 2),
'coupon_discount_label' => $coupon_discount > 0 ? ('-' . $this->format_amount_label($coupon_discount, $pay_modo, $currency)) : '',
'referrer_active' => $referrer_active,
'discounts' => [
'list_amount' => round($list_amount, 2),
'rebate_discount' => round($rebate_discount, 2),
'coupon_discount' => round($coupon_discount, 2),
'total_discount' => round(max(0, $list_amount - $payable), 2),
'payable' => round($payable, 2),
'rebate_price' => round($rebate_price, 2),
],
// zibll: points_deduction runtime hard-off; card_pass not a content payment method.
'points_deduction_allowed' => function_exists('aksrt_is_allow_points_deduction')
? (bool) aksrt_is_allow_points_deduction('content')
: false,
'card_pass_allowed' => function_exists('aksrt_is_content_card_pass_pay_allowed')
? (bool) aksrt_is_content_card_pass_pay_allowed($post)
: false,
'price_type' => $price_type,
];
}
/**
* @return array{post:WP_Post,meta:array,priced:array}|WP_Error
*/
private function assert_purchasable(int $post_id, int $user_id, string $price_type = '') {
$post = get_post($post_id);
if (!($post instanceof WP_Post) || $post->post_status !== 'publish') {
return new WP_Error('aksrt_invalid', __('内容不存在', 'aksrt'), ['status' => 404]);
}
$meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post) : [];
$pay_type = AKSRT_Domain_Model::normalize_pay_type((string) ($meta['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO));
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_NO && $price_type !== 'download_limit_over') {
return new WP_Error('aksrt_not_paid', __('该内容无需购买', 'aksrt'), ['status' => 400]);
}
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP) {
return new WP_Error('aksrt_need_vip', __('该内容为 VIP 专属,请开通会员。', 'aksrt'), ['status' => 403]);
}
$pay_modo_meta = (($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0';
$allow_guest = function_exists('aksrt_allow_guest_content_pay') && aksrt_allow_guest_content_pay($post);
if ($user_id <= 0) {
if ($pay_modo_meta === 'points' || !$allow_guest) {
return new WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), [
'status' => 401,
'login_url' => wp_login_url(get_permalink($post)),
]);
}
}
$priced = function_exists('aksrt_get_user_pay_price')
? aksrt_get_user_pay_price($post, $user_id)
: ['amount' => 0.0, 'modo' => '0', 'pay_limit' => 0, 'vip_level' => 0, 'is_free' => true, 'base' => 0.0];
// Free daily quota overbuy: override amount, ignore VIP discount (zibll).
if ($price_type === 'download_limit_over') {
$over = max(0.0, (float) ($meta['download_limit_over_price'] ?? 0));
if ($over <= 0) {
return new WP_Error('aksrt_no_overbuy', __('未开放超额下载购买。', 'aksrt'), ['status' => 400]);
}
$priced['amount'] = $over;
$priced['base'] = $over;
$priced['is_free'] = false;
$priced['vip_amount'] = -1.0;
}
$pay_limit = (int) ($priced['pay_limit'] ?? 0);
$vip_level = (int) ($priced['vip_level'] ?? 0);
if ($pay_limit > 0 && $vip_level < $pay_limit && $price_type !== 'download_limit_over') {
return new WP_Error('aksrt_need_vip', sprintf(
/* translators: %d: required VIP level */
__('该资源仅限 VIP%s 及以上会员购买/下载。', 'aksrt'),
max(1, $pay_limit)
), [
'status' => 403,
'pay_limit' => $pay_limit,
]);
}
if ($user_id > 0 && class_exists('AKSRT_Order') && AKSRT_Order::get_instance()->has_paid_access(
$user_id,
AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
$post_id
)) {
return new WP_Error('aksrt_already_paid', __('您已购买该内容。', 'aksrt'), [
'status' => 200,
'has_paid_access' => true,
'reload' => true,
]);
}
if ($user_id <= 0 && function_exists('aksrt_guest_has_paid_access') && aksrt_guest_has_paid_access($post_id)) {
return new WP_Error('aksrt_already_paid', __('您已购买该内容。', 'aksrt'), [
'status' => 200,
'has_paid_access' => true,
'reload' => true,
]);
}
return [
'post' => $post,
'meta' => $meta,
'priced' => $priced,
];
}
/**
* @return array<int, array{id:string,name:string}>
*/
private function available_payment_methods(string $pay_modo, bool $logged_in = true): array {
if ($pay_modo === 'points') {
return [
['id' => 'points', 'name' => __('积分支付', 'aksrt')],
];
}
if (function_exists('aksrt_list_cash_payment_methods')) {
return aksrt_list_cash_payment_methods($logged_in);
}
return $logged_in
? [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]
: [['id' => 'alipay', 'name' => __('支付宝', 'aksrt')]];
}
private function is_coupon_allowed(int $post_id, array $meta): bool {
if (!empty($meta['coupon_s'])) {
return true;
}
if (function_exists('aksrt_is_content_coupon_allowed')) {
return aksrt_is_content_coupon_allowed($post_id);
}
if (class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_content_coupon_settings')) {
return !empty(AKSRT_Settings::get_content_coupon_settings()['enabled']);
}
if (class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_paid_content_settings')) {
return !empty(AKSRT_Settings::get_paid_content_settings()['coupon_post_s']);
}
return false;
}
/**
* @return array{id:int,ratio:float}
*/
private function resolve_referrer_context(int $user_id): array {
$empty = ['id' => 0, 'ratio' => 0.0];
if (!class_exists('AKSRT_Settings') || !method_exists('AKSRT_Settings', 'get_payment_affiliate_settings')) {
return $empty;
}
$aff = AKSRT_Settings::get_payment_affiliate_settings();
if (empty($aff['enabled'])) {
return $empty;
}
$referrer_id = 0;
if (class_exists('AKSRT_Invite') && method_exists(AKSRT_Invite::get_instance(), 'get_referrer')) {
$referrer_id = (int) AKSRT_Invite::get_instance()->get_referrer($user_id);
}
if ($referrer_id <= 0) {
return $empty;
}
$ratio = (float) ($aff['ratio'] ?? 0);
$user_ratio = is_array($aff['user_ratio'] ?? null) ? $aff['user_ratio'] : [];
$ref_vip = 0;
if (function_exists('aksrt_get_user_vip_level')) {
$ref_vip = (int) aksrt_get_user_vip_level($referrer_id);
}
if ($ref_vip >= 2 && isset($user_ratio['vip2']) && (float) $user_ratio['vip2'] > 0) {
$ratio = (float) $user_ratio['vip2'];
} elseif ($ref_vip >= 1 && isset($user_ratio['vip1']) && (float) $user_ratio['vip1'] > 0) {
$ratio = (float) $user_ratio['vip1'];
} elseif (isset($user_ratio['user'])) {
$ratio = (float) $user_ratio['user'];
}
$ratio = max(0.0, min(100.0, $ratio));
if ($ratio <= 0) {
return $empty;
}
return ['id' => $referrer_id, 'ratio' => $ratio];
}
/**
* Cash discount chain: VIP-priced list → 推广折扣 → coupon → payable + commission snapshot.
*
* @param array<string,mixed> $priced
* @param array<string,mixed> $meta
* @return array|WP_Error
*/
private function apply_cash_discounts(array $priced, WP_Post $post, array $meta, int $user_id, string $coupon_code = '', bool $soft_coupon = false) {
$list_amount = round(max(0.0, (float) ($priced['amount'] ?? 0)), 2);
$pay = $list_amount;
$rebate_discount = 0.0;
$coupon_discount = 0.0;
$coupon_meta = [];
$ref = $this->resolve_referrer_context($user_id);
$referrer_id = (int) ($ref['id'] ?? 0);
$ratio = (float) ($ref['ratio'] ?? 0);
// 1) Buyer-facing 推广折扣 (fixed).
$fixed = max(0.0, (float) ($meta['pay_rebate_discount'] ?? 0));
if ($referrer_id > 0 && $ratio > 0 && $fixed > 0 && $pay > 0) {
$rebate_discount = round(min($pay, $fixed), 2);
$pay = round(max(0.0, $pay - $rebate_discount), 2);
}
// 2) Coupon after rebate discount.
$coupon_code = sanitize_text_field($coupon_code);
if ($coupon_code !== '') {
if (!$this->is_coupon_allowed((int) $post->ID, $meta)) {
$err = new WP_Error('aksrt_coupon_not_allowed', __('当前内容不支持优惠码。', 'aksrt'), ['status' => 400]);
if (!$soft_coupon) {
return $err;
}
} elseif (!class_exists('AKSRT_Coupon')) {
$err = new WP_Error('aksrt_coupon_unavailable', __('优惠券系统不可用。', 'aksrt'), ['status' => 500]);
if (!$soft_coupon) {
return $err;
}
} else {
$result = AKSRT_Coupon::get_instance()->validate($coupon_code, $pay, [
'user_id' => max(0, $user_id),
'product_ids' => [(int) $post->ID],
'context' => 'content',
]);
if (empty($result['valid'])) {
$err = new WP_Error(
'aksrt_coupon_error',
(string) ($result['message'] ?? __('优惠码不可用。', 'aksrt')),
['status' => 400]
);
if (!$soft_coupon) {
return $err;
}
} else {
$coupon_discount = round(max(0.0, (float) ($result['discount'] ?? 0)), 2);
if ($coupon_discount > $pay) {
$coupon_discount = $pay;
}
$pay = round(max(0.0, $pay - $coupon_discount), 2);
$c = is_array($result['coupon'] ?? null) ? $result['coupon'] : [];
$coupon_meta = [
'id' => (int) ($c['id'] ?? 0),
'code' => (string) ($c['code'] ?? $coupon_code),
'type' => (string) ($c['type'] ?? ''),
'value' => (float) ($c['value'] ?? 0),
'discount' => $coupon_discount,
];
}
}
}
$payable = round(max(0.0, $pay), 2);
$rebate_price = 0.0;
if ($referrer_id > 0 && $ratio > 0 && $payable > 0) {
$rebate_price = round($payable * $ratio / 100, 2);
}
return [
'list_amount' => $list_amount,
'rebate_discount' => $rebate_discount,
'coupon_discount' => $coupon_discount,
'coupon' => $coupon_meta,
'payable' => $payable,
'referrer_id' => $referrer_id,
'rebate_price' => $rebate_price,
'ratio' => $ratio,
];
}
private function format_amount_label(float $amount, string $pay_modo, string $currency): string {
if ($amount <= 0) {
return (string) __('免费', 'aksrt');
}
$n = function_exists('aksrt_format_price') ? aksrt_format_price($amount) : (string) $amount;
if ($pay_modo === 'points') {
return $n . ' ' . $currency;
}
return $currency . $n;
}
}
@@ -0,0 +1,547 @@
<?php
/**
* Extracted trait: AKSRT_Content_Pay_Orders
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Content_Pay_Orders {
/**
* Bump content sales when a resource order is paid.
*
* @param string $order_number
* @param array $paid_order
*/
public function on_order_paid(string $order_number, array $paid_order): void {
$product_type = sanitize_key((string) ($paid_order['product_type'] ?? ''));
if ($product_type !== AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE) {
return;
}
$post_id = (int) ($paid_order['product_id'] ?? 0);
if ($post_id <= 0) {
return;
}
// Only count normal posts (content pay), not necessarily forum topics.
$post = get_post($post_id);
if (!($post instanceof WP_Post) || $post->post_type !== 'post') {
return;
}
if (function_exists('aksrt_bump_content_sales_volume')) {
aksrt_bump_content_sales_volume($post_id, 1);
} else {
$key = AKSRT_Domain_Model::RESOURCE_SALES_VOLUME_KEY;
$cur = (int) get_post_meta($post_id, $key, true);
update_post_meta($post_id, $key, $cur + 1);
}
// Guest cookie unlock (user_id=0).
$uid = (int) ($paid_order['user_id'] ?? 0);
if ($uid <= 0 && function_exists('aksrt_set_guest_content_pay_cookies')) {
// Ensure full order row for token material.
if (class_exists('AKSRT_Order_Repository')) {
$full = AKSRT_Order_Repository::get_instance()->get($order_number);
if (is_array($full)) {
$paid_order = array_merge($full, $paid_order);
}
}
aksrt_set_guest_content_pay_cookies($paid_order);
}
// Consume coupon + write affiliate commission (cash content only; idempotent).
$this->finalize_content_order_side_effects($order_number, $paid_order);
}
/**
* On paid: burn coupon (if any) and create affiliate rebate log.
*/
private function finalize_content_order_side_effects(string $order_number, array $order): void {
$meta = $this->decode_order_note_meta($order);
if (empty($meta['content_pay'])) {
return;
}
// Points: no coupon burn / no cash commission.
if (($meta['pay_modo'] ?? '') === 'points') {
return;
}
$buyer_id = (int) ($order['user_id'] ?? 0);
// Coupon burn (shared helper; idempotent via coupon_usage.order_number).
if (function_exists('aksrt_burn_order_coupon')) {
aksrt_burn_order_coupon($order_number, $meta, $buyer_id);
}
// Affiliate commission (does not reduce buyer pay).
$referrer_id = (int) ($meta['referrer_id'] ?? 0);
$rebate_price = round((float) ($meta['rebate_price'] ?? 0), 2);
$pay_amount = round((float) ($order['amount'] ?? $meta['payable'] ?? 0), 2);
if ($referrer_id <= 0 || $rebate_price <= 0 || $pay_amount <= 0) {
return;
}
if (!class_exists('AKSRT_Rebate')) {
return;
}
if ($this->rebate_log_exists($order_number, $referrer_id)) {
return;
}
AKSRT_Rebate::get_instance()->create([
'user_id' => $referrer_id,
'order_number' => $order_number,
'source_user_id' => $buyer_id,
'amount' => $rebate_price,
'type' => AKSRT_Rebate::TYPE_ORDER,
'note' => sprintf(
/* translators: 1: order number 2: post id */
__('内容推广返佣 订单 %1$s 内容 #%2$d', 'aksrt'),
$order_number,
(int) ($order['product_id'] ?? $meta['post_id'] ?? 0)
),
]);
}
/**
* @param array<string,mixed> $order
* @return array<string,mixed>
*/
private function decode_order_note_meta(array $order): array {
if (is_array($order['meta_payload'] ?? null) && $order['meta_payload'] !== []) {
return $order['meta_payload'];
}
$note = (string) ($order['note'] ?? '');
if ($note === '') {
return [];
}
$decoded = json_decode($note, true);
return is_array($decoded) ? $decoded : [];
}
private function rebate_log_exists(string $order_number, int $referrer_id): bool {
global $wpdb;
$table = AKSRT_DB_Migrator::table('rebate_log');
if ($table === '' || $order_number === '') {
return false;
}
$found = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$table} WHERE order_number = %s AND user_id = %d LIMIT 1",
$order_number,
$referrer_id
));
return !empty($found);
}
/**
* Points one-shot purchase → permanent resource order.
*
* @return array|WP_Error
*/
public function create_and_pay_points(int $post_id, int $user_id, string $price_type = '') {
$assert = $this->assert_purchasable($post_id, $user_id, $price_type);
if (is_wp_error($assert)) {
return $assert;
}
$priced = $assert['priced'];
if ((($priced['modo'] ?? '0') !== 'points')) {
return new WP_Error('aksrt_wrong_modo', __('该内容不支持积分支付。', 'aksrt'), ['status' => 400]);
}
$amount = (int) ceil((float) ($priced['amount'] ?? 0));
if ($amount <= 0) {
return [
'success' => true,
'data' => [
'free' => true,
'message' => __('无需支付,已可访问。', 'aksrt'),
'reload' => true,
],
];
}
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Credits')) {
return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
}
$balance = AKSRT_Credits::balance($user_id);
if ($balance < $amount) {
return new WP_Error('aksrt_credits_insufficient', __('积分不足,暂时无法支付。', 'aksrt'), [
'status' => 402,
'balance' => $balance,
'cost' => $amount,
]);
}
$note = wp_json_encode([
'content_pay' => 1,
'pay_modo' => 'points',
'post_id' => $post_id,
'price_type' => $price_type,
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => (float) ($priced['base'] ?? $amount),
'amount' => $amount,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = AKSRT_Order_Repository::get_instance()->create([
'user_id' => $user_id,
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
'product_id' => $post_id,
'amount' => (float) $amount,
'currency' => 'points',
'status' => AKSRT_Order::STATUS_PENDING,
'payment_method' => 'points',
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
}
$deducted = AKSRT_Credits::deduct(
$user_id,
$amount,
'content_points_pay',
['type' => 'post', 'id' => $post_id, 'order' => $order_number],
sprintf(__('积分购买《%s》', 'aksrt'), get_the_title($post_id))
);
if (is_wp_error($deducted) || $deducted === false) {
AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [
'note' => __('积分扣除失败,自动取消', 'aksrt'),
]);
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败。', 'aksrt');
return new WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败。', 'aksrt'), ['status' => 400]);
}
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => 'points',
'payment_transaction_id' => 'points_' . $order_number,
'note' => sprintf(__('积分支付 %d 积分。', 'aksrt'), $amount),
]);
if (!$paid) {
AKSRT_Credits::add(
$user_id,
$amount,
'content_points_pay_refund',
['type' => 'post', 'id' => $post_id, 'order' => $order_number],
sprintf(__('购买失败退款:%s', 'aksrt'), get_the_title($post_id))
);
return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败,已尝试退回积分。', 'aksrt'), ['status' => 500]);
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => $amount,
'pay_modo' => 'points',
'balance' => AKSRT_Credits::balance($user_id),
'message' => __('支付成功', 'aksrt'),
'reload' => true,
],
];
}
/**
* Cash order create + balance/gateway pay.
*
* @return array|WP_Error
*/
public function create_cash_order(int $post_id, int $user_id, string $payment_method = 'balance', string $price_type = '', string $coupon_code = '') {
$assert = $this->assert_purchasable($post_id, $user_id, $price_type);
if (is_wp_error($assert)) {
return $assert;
}
$post = $assert['post'];
$meta = $assert['meta'];
$priced = $assert['priced'];
if ((($priced['modo'] ?? '0') === 'points')) {
return new WP_Error('aksrt_wrong_modo', __('该内容仅支持积分支付。', 'aksrt'), ['status' => 400]);
}
// Card pass cannot stack coupon (zibll). Content cashier does not sell via card_pass.
$payment_method = sanitize_key($payment_method);
if ($payment_method === 'card_pass') {
if (!function_exists('aksrt_is_content_card_pass_pay_allowed') || !aksrt_is_content_card_pass_pay_allowed($post)) {
return new WP_Error(
'aksrt_card_pass_not_allowed',
__('内容购买不支持卡密支付,请在用户中心使用卡密兑换余额/会员/积分。', 'aksrt'),
['status' => 400]
);
}
$coupon_code = '';
}
$discounted = $this->apply_cash_discounts($priced, $post, $meta, $user_id, $coupon_code, false);
if (is_wp_error($discounted)) {
return $discounted;
}
$amount = round((float) ($discounted['payable'] ?? 0), 2);
$list_amount = round((float) ($discounted['list_amount'] ?? $amount), 2);
$rebate_discount = round((float) ($discounted['rebate_discount'] ?? 0), 2);
$coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2);
$coupon_meta = is_array($discounted['coupon'] ?? null) ? $discounted['coupon'] : [];
$referrer_id = (int) ($discounted['referrer_id'] ?? 0);
$rebate_price = round((float) ($discounted['rebate_price'] ?? 0), 2);
if ($amount <= 0) {
// Free after discount (or free content): create 0-paid order for access + burn coupon.
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
}
$note = wp_json_encode([
'content_pay' => 1,
'pay_modo' => '0',
'post_id' => $post_id,
'price_type' => $price_type,
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => (float) ($priced['base'] ?? $list_amount),
'list_amount' => $list_amount,
'rebate_discount' => $rebate_discount,
'coupon_discount' => $coupon_discount,
'discount' => $coupon_discount,
'coupon' => $coupon_meta,
'coupon_code' => (string) ($coupon_meta['code'] ?? $coupon_code),
'amount' => 0,
'payable' => 0,
'referrer_id' => $referrer_id,
'rebate_price' => 0,
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
'guest' => $user_id <= 0 ? 1 : 0,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = AKSRT_Order_Repository::get_instance()->create([
'user_id' => max(0, $user_id),
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
'product_id' => $post_id,
'amount' => 0.0,
'currency' => 'CNY',
'status' => AKSRT_Order::STATUS_PENDING,
'payment_method' => 'free',
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
}
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => 'free',
'payment_transaction_id' => 'free_' . $order_number,
'note' => __('优惠后无需支付。', 'aksrt'),
]);
if (!$paid) {
return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => 0,
'pay_modo' => '0',
'free' => true,
'message' => __('无需支付,已可访问。', 'aksrt'),
'reload' => true,
],
];
}
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
}
if ($payment_method === '') {
$payment_method = $user_id > 0 ? 'balance' : 'alipay';
}
if ($user_id <= 0 && in_array($payment_method, ['balance', 'points'], true)) {
$payment_method = 'alipay';
}
$coupon_code_norm = (string) ($coupon_meta['code'] ?? $coupon_code);
// Resume existing pending content order for this post when amount + coupon match (logged-in only).
$pending = $user_id > 0 ? $this->find_pending_content_order($user_id, $post_id) : null;
$can_resume = false;
if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) {
$pending_meta = $this->decode_order_note_meta($pending);
$pending_coupon = (string) ($pending_meta['coupon_code'] ?? '');
if (is_array($pending_meta['coupon'] ?? null)) {
$pending_coupon = (string) ($pending_meta['coupon']['code'] ?? $pending_coupon);
}
if ($pending_coupon === $coupon_code_norm) {
$can_resume = true;
}
}
if ($can_resume) {
$order_number = (string) ($pending['order_number'] ?? '');
$order = $pending;
} else {
$note = wp_json_encode([
'content_pay' => 1,
'pay_modo' => '0',
'post_id' => $post_id,
'price_type' => $price_type,
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => (float) ($priced['base'] ?? $list_amount),
'list_amount' => $list_amount,
'rebate_discount' => $rebate_discount,
'coupon_discount' => $coupon_discount,
'discount' => $coupon_discount,
'coupon' => $coupon_meta,
'coupon_code' => $coupon_code_norm,
'amount' => $amount,
'payable' => $amount,
'referrer_id' => $referrer_id,
'rebate_price' => $rebate_price,
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
'guest' => $user_id <= 0 ? 1 : 0,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = AKSRT_Order_Repository::get_instance()->create([
'user_id' => max(0, $user_id),
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
'product_id' => $post_id,
'amount' => $amount,
'currency' => 'CNY',
'status' => AKSRT_Order::STATUS_PENDING,
'payment_method' => $payment_method,
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
}
$order = AKSRT_Order_Repository::get_instance()->get($order_number);
if (!$order) {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
}
}
$payment_method = sanitize_key($payment_method ?: (string) ($order['payment_method'] ?? 'balance'));
if ($payment_method === '') {
$payment_method = $user_id > 0 ? 'balance' : 'alipay';
}
$order['subject'] = sprintf(
/* translators: %s: post title */
__('购买《%s》', 'aksrt'),
get_the_title($post_id)
);
$order['payment_method'] = $payment_method;
// Ensure gateway uses discounted amount.
$order['amount'] = $amount;
if (!class_exists('AKSRT_Payment_Gateway')) {
return new WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]);
}
$gateway_manager = AKSRT_Payment_Gateway::get_instance();
$gateway = $gateway_manager->get_gateway($payment_method);
if (!$gateway) {
return new WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用。', 'aksrt'), [
'status' => 400,
'order_number' => $order_number,
]);
}
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
return new WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [
'status' => 400,
'order_number' => $order_number,
]);
}
$payment_result = $gateway->create_payment($order);
if (empty($payment_result['success'])) {
$msg = (string) ($payment_result['message'] ?? __('支付失败。', 'aksrt'));
$code = ($payment_method === 'balance' && (str_contains($msg, '余额') || str_contains($msg, '不足')))
? 'aksrt_balance_insufficient'
: 'aksrt_pay_failed';
return new WP_Error($code, $msg, [
'status' => 402,
'order_number' => $order_number,
]);
}
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
$payment_url = (string) ($payment_result['payment_url'] ?? '');
if ($payment_url !== '' || $code_url !== '') {
return [
'success' => true,
'data' => [
'redirect' => true,
'payment_url' => $payment_url,
'qr_content' => $code_url,
'order_number' => $order_number,
'amount' => $amount,
'pay_modo' => '0',
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
'reload' => false,
'poll' => true,
],
];
}
// Sync gateway (balance): mark paid.
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => $payment_method,
'payment_transaction_id' => sanitize_text_field((string) ($payment_result['transaction_id'] ?? '')),
'note' => __('用户完成内容购买支付。', 'aksrt'),
]);
if (!$paid) {
return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => $amount,
'pay_modo' => '0',
'message' => $payment_result['message'] ?? __('支付成功', 'aksrt'),
'reload' => true,
],
];
}
/**
* @return array<string,mixed>|null
*/
private function find_pending_content_order(int $user_id, int $post_id): ?array {
if ($user_id <= 0 || $post_id <= 0 || !class_exists('AKSRT_Order_Repository')) {
return null;
}
$orders = AKSRT_Order_Repository::get_instance()->get_user_orders($user_id, [
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
'status' => AKSRT_Domain_Model::ORDER_STATUS_PENDING,
'limit' => 20,
'offset' => 0,
]);
foreach ($orders as $order) {
if ((int) ($order['product_id'] ?? 0) === $post_id) {
return $order;
}
}
return null;
}
}
@@ -0,0 +1,207 @@
<?php
/**
* Extracted trait: AKSRT_Content_Pay_Rest
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Content_Pay_Rest {
public function register_routes(): void {
register_rest_route('aksrt/v1', '/pay/content/(?P<id>\\d+)', [
[
'methods' => 'GET',
'callback' => [$this, 'rest_preview'],
'permission_callback' => [$this, 'check_rest_nonce'],
],
[
'methods' => 'POST',
'callback' => [$this, 'rest_purchase'],
'permission_callback' => [$this, 'check_rest_nonce'],
],
]);
register_rest_route('aksrt/v1', '/pay/content/order/(?P<order_number>[A-Za-z0-9_-]+)', [
'methods' => 'GET',
'callback' => [$this, 'rest_order_status'],
'permission_callback' => [$this, 'check_rest_nonce'],
]);
}
/**
* REST nonce for logged-in users (wp_rest). Guests get 401 on purchase paths.
*
* @param WP_REST_Request $request
* @return true|WP_Error
*/
public function check_rest_nonce(WP_REST_Request $request) {
$nonce = $request->get_header('x-wp-nonce');
if (!$nonce) {
return new WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]);
}
if (!wp_verify_nonce($nonce, 'wp_rest')) {
return new WP_Error('aksrt_rest_bad_nonce', __('安全令牌已过期,请刷新页面', 'aksrt'), ['status' => 403]);
}
return true;
}
/**
* GET /pay/content/{id}
* query: coupon_code?, price_type?
*/
public function rest_preview(WP_REST_Request $request) {
$post_id = (int) $request['id'];
$uid = get_current_user_id();
$coupon_code = sanitize_text_field((string) ($request->get_param('coupon_code') ?? $request->get_param('coupon') ?? ''));
$price_type = sanitize_key((string) ($request->get_param('price_type') ?? ''));
if ($price_type !== 'download_limit_over') {
$price_type = '';
}
$preview = $this->preview($post_id, $uid, $coupon_code, $price_type);
if (is_wp_error($preview)) {
return $preview;
}
return new WP_REST_Response($preview, 200);
}
/**
* POST /pay/content/{id}
* body: { mode?: points|cash, payment_method?: balance|alipay|..., price_type?: ''|download_limit_over, coupon_code?: string }
*/
public function rest_purchase(WP_REST_Request $request) {
$post_id = (int) $request['id'];
$uid = get_current_user_id();
$params = $request->get_json_params();
if (!is_array($params)) {
$params = $request->get_params();
}
$price_type = sanitize_key((string) ($params['price_type'] ?? ''));
if ($price_type !== 'download_limit_over') {
$price_type = '';
}
$coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? $params['coupon'] ?? ''));
// Guest allowed only for cash + pay_limit=0 + setting on.
if ($uid <= 0) {
$post = get_post($post_id);
$allow_guest = function_exists('aksrt_allow_guest_content_pay') && aksrt_allow_guest_content_pay($post);
if (!$allow_guest) {
return new WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), [
'status' => 401,
'login_url' => wp_login_url($post ? get_permalink($post) : home_url('/')),
]);
}
}
$gate = $this->assert_purchasable($post_id, $uid, $price_type);
if (is_wp_error($gate)) {
return $gate;
}
$priced = $gate['priced'];
$pay_modo = (($priced['modo'] ?? '0') === 'points') ? 'points' : '0';
$mode = sanitize_key((string) ($params['mode'] ?? ''));
if ($mode === '') {
$mode = $pay_modo === 'points' ? 'points' : 'cash';
}
if ($mode === 'points' || $pay_modo === 'points') {
if ($uid <= 0) {
return new WP_Error('aksrt_need_login', __('积分支付请先登录。', 'aksrt'), ['status' => 401]);
}
// Points path: ignore coupon (zibll parity).
return $this->rest_wrap_result($this->create_and_pay_points($post_id, $uid, $price_type));
}
$payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance'));
if ($payment_method === '' || $payment_method === 'cash') {
$payment_method = 'balance';
}
// Guests cannot use balance/points gateways.
if ($uid <= 0 && in_array($payment_method, ['balance', 'points'], true)) {
$payment_method = 'alipay';
}
return $this->rest_wrap_result($this->create_cash_order($post_id, $uid, $payment_method, $price_type, $coupon_code));
}
/**
* GET /pay/content/order/{order_number}
*/
public function rest_order_status(WP_REST_Request $request) {
$uid = get_current_user_id();
$order_number = sanitize_text_field((string) $request['order_number']);
if ($order_number === '' || !class_exists('AKSRT_Order_Repository')) {
return new WP_Error('aksrt_invalid', __('订单不存在', 'aksrt'), ['status' => 404]);
}
$repo = AKSRT_Order_Repository::get_instance();
if ($uid > 0) {
$order = $repo->get_user_order($uid, $order_number);
} else {
$order = $repo->get($order_number);
// Guest may only poll own guest cash orders (user_id=0): content/topic resource or plate_follow.
$ptype = sanitize_key((string) ($order['product_type'] ?? ''));
$guest_types = [
AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
AKSRT_Domain_Model::ORDER_PRODUCT_PLATE_FOLLOW,
];
if ($order && ((int) ($order['user_id'] ?? -1) !== 0 || !in_array($ptype, $guest_types, true))) {
$order = null;
}
}
if (!$order) {
return new WP_Error('aksrt_invalid', __('订单不存在', 'aksrt'), ['status' => 404]);
}
$status = (string) ($order['status'] ?? '');
$paid = class_exists('AKSRT_Domain_Model') && AKSRT_Domain_Model::is_paid_order_status($status);
if ($paid && $uid <= 0 && function_exists('aksrt_set_guest_pay_cookies')) {
// Ensure cookie set even if notify raced ahead of browser cookie.
aksrt_set_guest_pay_cookies($order);
}
return new WP_REST_Response([
'order_number' => $order_number,
'status' => $status,
'paid' => $paid,
'amount' => (float) ($order['amount'] ?? 0),
'currency' => (string) ($order['currency'] ?? ''),
'product_id' => (int) ($order['product_id'] ?? 0),
'product_type' => (string) ($order['product_type'] ?? ''),
'reload' => $paid,
], 200);
}
/**
* @param array|WP_Error $result
* @return WP_REST_Response|WP_Error
*/
private function rest_wrap_result($result) {
if (is_wp_error($result)) {
return $result;
}
if (empty($result['success'])) {
$code = sanitize_key((string) ($result['code'] ?? 'aksrt_pay_failed'));
$status = (int) ($result['status'] ?? 400);
return new WP_Error($code !== '' ? $code : 'aksrt_pay_failed', (string) ($result['message'] ?? __('支付失败', 'aksrt')), array_merge(
['status' => $status > 0 ? $status : 400],
is_array($result['data'] ?? null) ? $result['data'] : []
));
}
return new WP_REST_Response($result, 200);
}
}
@@ -0,0 +1,491 @@
<?php
/**
* Extracted trait: AKSRT_Vip_Service_Packages
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Vip_Service_Packages {
/**
* Parse packages_raw lines: price|days|title
*
* @return array<int, array{price:float,days:int,title:string}>
*/
public function parse_packages(string $raw): array {
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
$out = [];
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '') {
continue;
}
$parts = array_map('trim', explode('|', $line));
$price = isset($parts[0]) ? (float) $parts[0] : 0.0;
$days = isset($parts[1]) ? (int) $parts[1] : 0;
$title = isset($parts[2]) ? sanitize_text_field($parts[2]) : '';
if ($price <= 0) {
continue;
}
if ($title === '') {
$title = $days > 0
? sprintf(__('%d天会员', 'aksrt'), $days)
: __('永久会员', 'aksrt');
}
$out[] = [
'price' => round($price, 2),
'days' => max(0, $days),
'title' => $title,
];
}
return $out;
}
/**
* Build upgrade package items (zibll vip_upgrade_product parity).
*
* upgrade.product multi-line formats:
* - permanent VIP1: permanent|price|title[|show_price]
* - fixed (alias): price|days|title
* - timed unit: unit|daily_price|title
* - timed jump permanent: jump|price|title[|show_price]
*
* Rules:
* - permanent user: only permanent/fixed (days=0 preferred)
* - timed user: unit (keep expiry) + optional jump (to permanent)
*
* package index: 901 permanent/fixed, 902 unit, 903 jump
*
* @return array<int, array<string,mixed>>
*/
private function build_upgrade_items(int $user_id, int $expires, bool $is_permanent, string $raw): array {
$raw = trim($raw);
if ($raw === '') {
return [];
}
$items = [];
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
$has_permanent = false;
$has_unit = false;
$has_jump = false;
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '') {
continue;
}
$parts = array_map('trim', explode('|', $line));
$mode = strtolower((string) ($parts[0] ?? ''));
// unit|daily|title — timed VIP1 only.
if ($mode === 'unit') {
if ($is_permanent || $expires <= time() || $has_unit) {
continue;
}
$daily = isset($parts[1]) ? (float) $parts[1] : 0.0;
if ($daily <= 0) {
continue;
}
$remaining = (int) ceil(max(1, ($expires - time()) / DAY_IN_SECONDS));
$price = round(max(0.01, $remaining * $daily), 2);
$title = sanitize_text_field((string) ($parts[2] ?? ''));
if ($title === '') {
$title = sprintf(__('按剩余 %d 天升级', 'aksrt'), $remaining);
}
$items[] = [
'package_id' => $this->encode_package_id(2, 902),
'level' => 2,
'index' => 902,
'action' => 'upgrade',
'price' => $price,
'base_price' => $price,
'show_price' => 0.0,
'discount_tag' => __('剩余天数', 'aksrt'),
'days' => $remaining,
'title' => $title,
'price_label' => $this->format_cash_label($price),
'show_price_label' => '',
'days_label' => sprintf(__('%d 天', 'aksrt'), $remaining),
'cta_label' => __('立即升级', 'aksrt'),
'is_upgrade_product' => true,
'upgrade_mode' => 'unit',
];
$has_unit = true;
continue;
}
// jump|price|title[|show_price] — timed VIP1 only → permanent VIP2.
if ($mode === 'jump') {
if ($is_permanent || $has_jump) {
continue;
}
$price = isset($parts[1]) ? (float) $parts[1] : 0.0;
if ($price <= 0) {
continue;
}
$title = sanitize_text_field((string) ($parts[2] ?? ''));
if ($title === '') {
$title = __('升级为永久会员', 'aksrt');
}
$show = isset($parts[3]) ? (float) $parts[3] : 0.0;
$items[] = [
'package_id' => $this->encode_package_id(2, 903),
'level' => 2,
'index' => 903,
'action' => 'upgrade',
'price' => round($price, 2),
'base_price' => round($price, 2),
'show_price' => max(0.0, $show),
'discount_tag' => __('永久升级', 'aksrt'),
'days' => 0,
'title' => $title,
'price_label' => $this->format_cash_label($price),
'show_price_label' => $show > 0 ? $this->format_cash_label($show) : '',
'days_label' => __('永久', 'aksrt'),
'cta_label' => __('立即升级', 'aksrt'),
'is_upgrade_product' => true,
'upgrade_mode' => 'jump',
];
$has_jump = true;
continue;
}
// permanent|price|title[|show_price]
if ($mode === 'permanent') {
if ($has_permanent) {
continue;
}
$price = isset($parts[1]) ? (float) $parts[1] : 0.0;
if ($price <= 0) {
continue;
}
$title = sanitize_text_field((string) ($parts[2] ?? ''));
if ($title === '') {
$title = __('永久会员升级', 'aksrt');
}
$show = isset($parts[3]) ? (float) $parts[3] : 0.0;
$items[] = [
'package_id' => $this->encode_package_id(2, 901),
'level' => 2,
'index' => 901,
'action' => 'upgrade',
'price' => round($price, 2),
'base_price' => round($price, 2),
'show_price' => max(0.0, $show),
'discount_tag' => __('升级专享', 'aksrt'),
'days' => 0,
'title' => $title,
'price_label' => $this->format_cash_label($price),
'show_price_label' => $show > 0 ? $this->format_cash_label($show) : '',
'days_label' => __('永久', 'aksrt'),
'cta_label' => __('立即升级', 'aksrt'),
'is_upgrade_product' => true,
'upgrade_mode' => 'permanent',
];
$has_permanent = true;
continue;
}
// fixed: price|days|title
$parsed = $this->parse_packages($line);
if (empty($parsed[0]) || $has_permanent) {
continue;
}
// For permanent VIP1, only accept days=0 fixed lines as permanent upgrade.
if ($is_permanent && (int) $parsed[0]['days'] > 0) {
continue;
}
$pkg = $parsed[0];
$items[] = [
'package_id' => $this->encode_package_id(2, 901),
'level' => 2,
'index' => 901,
'action' => 'upgrade',
'price' => (float) $pkg['price'],
'base_price' => (float) $pkg['price'],
'show_price' => 0.0,
'discount_tag' => __('升级专享', 'aksrt'),
'days' => (int) $pkg['days'],
'title' => (string) $pkg['title'],
'price_label' => $this->format_cash_label((float) $pkg['price']),
'show_price_label' => '',
'days_label' => ((int) $pkg['days'] <= 0)
? __('永久', 'aksrt')
: sprintf(__('%d 天', 'aksrt'), (int) $pkg['days']),
'cta_label' => __('立即升级', 'aksrt'),
'is_upgrade_product' => true,
'upgrade_mode' => ((int) $pkg['days'] <= 0) ? 'permanent' : 'fixed',
];
$has_permanent = true;
}
// Permanent VIP1 should not see unit/jump.
if ($is_permanent) {
$items = array_values(array_filter($items, static function ($it) {
$m = (string) ($it['upgrade_mode'] ?? '');
return in_array($m, ['permanent', 'fixed'], true);
}));
}
return $items;
}
/**
* Points exchange packages: level|points|days|title
*
* @param array<string,mixed> $vip
* @param array<int,mixed> $levels
* @return array<int, array<string,mixed>>
*/
public function get_points_exchange_packages(array $vip, array $levels): array {
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
$raw = (string) ($points_ex['packages_raw'] ?? '');
$currency = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
$out = [];
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
$i = 0;
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '') {
continue;
}
$parts = array_map('trim', explode('|', $line));
if (count($parts) < 3) {
continue;
}
$lv = (int) ($parts[0] ?? 0);
$pts = (int) ($parts[1] ?? 0);
$days = (int) ($parts[2] ?? 0);
$title = sanitize_text_field((string) ($parts[3] ?? ''));
if ($lv < 1 || $lv > 2 || $pts <= 0) {
continue;
}
if (empty($levels[$lv]['enabled'])) {
continue;
}
$i++;
if ($title === '') {
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
$title = $days > 0
? sprintf(__('%1$s · %2$d天', 'aksrt'), $lv_name, $days)
: sprintf(__('%s · 永久', 'aksrt'), $lv_name);
}
$out[] = [
'package_id' => 5000 + $i,
'level' => $lv,
'index' => $i,
'points' => $pts,
'days' => max(0, $days),
'title' => $title,
'price_label' => $pts . ' ' . $currency,
'days_label' => $days > 0 ? sprintf(__('%d 天', 'aksrt'), $days) : __('永久', 'aksrt'),
'cta_label' => __('立即兑换', 'aksrt'),
];
}
return $out;
}
/**
* Single upgrade product line: price|days|title
*
* @return array{price:float,days:int,title:string}|null
*/
public function parse_upgrade_product_line(string $raw): ?array {
$raw = trim($raw);
if ($raw === '') {
return null;
}
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '' || str_starts_with(strtolower($line), 'unit|')) {
continue;
}
$parsed = $this->parse_packages($line);
if (!empty($parsed[0])) {
return $parsed[0];
}
}
return null;
}
/**
* Apply renew discount/reduce to same-level packages.
*
* @param array<string,mixed> $renew
* @param array<string,mixed> $upgrade
* @return array{price:float,show_price:float,tag:string}|null
*/
private function apply_action_price(
float $base_price,
string $action,
array $renew,
array $upgrade,
int $target_level,
int $user_level,
int $expires,
bool $is_permanent,
int $days
): ?array {
if ($base_price <= 0) {
return null;
}
if ($action === 'renew' && $is_permanent) {
return null;
}
if ($action !== 'renew') {
return [
'price' => round($base_price, 2),
'show_price' => 0.0,
'tag' => '',
];
}
$type = sanitize_key((string) ($renew['price_type'] ?? 'discount'));
$price = $base_price;
$show = $base_price;
$tag = '';
if ($type === 'discount') {
$discount = (float) ($renew['discount'] ?? 8);
if ($discount <= 0) {
$discount = 8;
}
if ($discount <= 10) {
$price = round($base_price * ($discount / 10), 2);
$tag = sprintf(
/* translators: %s: discount like 8 */
__('%s折续费', 'aksrt'),
rtrim(rtrim(number_format($discount, 1, '.', ''), '0'), '.')
);
} else {
$price = round($base_price * (min(100, $discount) / 100), 2);
$tag = sprintf(__('%s%% 续费价', 'aksrt'), (string) (int) $discount);
}
} elseif ($type === 'reduce') {
$reduce = max(0, (float) ($renew['reduce'] ?? 20));
$price = round(max(0.01, $base_price - $reduce), 2);
$tag = sprintf(
/* translators: %s: amount */
__('续费立减%s', 'aksrt'),
$this->format_cash_label($reduce)
);
} else {
$show = 0.0;
$tag = __('续费同价', 'aksrt');
}
if ($price < 0.01) {
$price = 0.01;
}
if ($show > 0 && $price >= $show) {
$show = 0.0;
$tag = __('续费', 'aksrt');
}
return [
'price' => $price,
'show_price' => $show > $price ? $show : 0.0,
'tag' => $tag,
];
}
public function encode_package_id(int $level, int $index): int {
return (max(1, min(2, $level)) * 1000) + max(1, $index);
}
/**
* @return array{level:int,index:int}|null
*/
public function decode_package_id(int $package_id): ?array {
if ($package_id < 1001) {
return null;
}
$level = intdiv($package_id, 1000);
$index = $package_id % 1000;
if ($level < 1 || $level > 2 || $index < 1) {
return null;
}
return ['level' => $level, 'index' => $index];
}
/**
* @return array<int, array{id:string,name:string}>
*/
private function available_payment_methods(): array {
$methods = [];
if (!class_exists('AKSRT_Payment_Gateway')) {
return [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]];
}
$gw = AKSRT_Payment_Gateway::get_instance();
foreach (['balance', 'alipay', 'wechatpay', 'paypal', 'stripe'] as $id) {
$g = $gw->get_gateway($id);
if (!$g) {
continue;
}
if (method_exists($g, 'is_configured') && !$g->is_configured()) {
continue;
}
$methods[] = [
'id' => $id,
'name' => method_exists($g, 'get_name') ? (string) $g->get_name() : $id,
];
}
if ($methods === []) {
$methods[] = ['id' => 'balance', 'name' => __('站内余额', 'aksrt')];
}
return $methods;
}
private function format_cash_label(float $amount): string {
$symbol = '¥';
if (class_exists('AKSRT_Settings')) {
$paid = AKSRT_Settings::get_paid_content_settings();
$symbol = (string) ($paid['display']['currency_symbol'] ?? '¥');
}
$n = function_exists('aksrt_format_price') ? aksrt_format_price($amount) : (string) $amount;
return $symbol . $n;
}
private function format_expires_label(int $expires, int $level): string {
if ($level <= 0) {
return __('未开通', 'aksrt');
}
if ($expires <= 0) {
return __('永久有效', 'aksrt');
}
if ($expires < time()) {
return __('已过期', 'aksrt');
}
return sprintf(
/* translators: %s: date */
__('到期时间:%s', 'aksrt'),
wp_date('Y-m-d H:i', $expires)
);
}
}
@@ -0,0 +1,598 @@
<?php
/**
* Extracted trait: AKSRT_Vip_Service_Purchase
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Vip_Service_Purchase {
/**
* @return array<string,mixed>
*/
public function get_catalog(int $user_id = 0): array {
$vip = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_payment_vip_settings()
: ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => [], 'points_exchange' => []];
$level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0;
$expires = $user_id > 0 ? (int) get_user_meta($user_id, 'aksrt_vip_expires', true) : 0;
$is_permanent = ($level > 0 && $expires <= 0);
$packages = $this->parse_packages((string) ($vip['packages_raw'] ?? ''));
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
$renew = is_array($vip['renew'] ?? null) ? $vip['renew'] : [];
$upgrade = is_array($vip['upgrade'] ?? null) ? $vip['upgrade'] : [];
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
$renew_enabled = !isset($renew['enabled']) || !empty($renew['enabled']);
$upgrade_enabled = !isset($upgrade['enabled']) || !empty($upgrade['enabled']);
$points_enabled = !empty($points_ex['enabled'])
&& (!class_exists('AKSRT_Settings') || AKSRT_Settings::credits_enabled());
$level_packages = [];
foreach ([1, 2] as $lv) {
if (empty($levels[$lv]['enabled'])) {
continue;
}
if ($is_permanent && $lv <= $level) {
continue;
}
if ($level > 0 && $lv === $level && !$renew_enabled) {
continue;
}
if ($level > 0 && $lv > $level && !$upgrade_enabled) {
continue;
}
$name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
$items = [];
foreach ($packages as $idx => $pkg) {
$base_price = (float) $pkg['price'];
$days = (int) $pkg['days'];
$action = 'pay';
if ($level > 0 && $lv === $level) {
$action = 'renew';
} elseif ($level > 0 && $lv > $level) {
$action = 'upgrade';
}
$priced = $this->apply_action_price($base_price, $action, $renew, $upgrade, $lv, $level, $expires, $is_permanent, $days);
if ($priced === null) {
continue;
}
$items[] = [
'package_id' => $this->encode_package_id($lv, $idx + 1),
'level' => $lv,
'index' => $idx + 1,
'action' => $action,
'price' => (float) $priced['price'],
'base_price' => $base_price,
'show_price' => (float) ($priced['show_price'] ?? 0),
'discount_tag' => (string) ($priced['tag'] ?? ''),
'days' => $days,
'title' => (string) $pkg['title'],
'price_label' => $this->format_cash_label((float) $priced['price']),
'show_price_label' => !empty($priced['show_price'])
? $this->format_cash_label((float) $priced['show_price'])
: '',
'days_label' => ($days <= 0)
? __('永久', 'aksrt')
: sprintf(__('%d 天', 'aksrt'), $days),
'cta_label' => $action === 'renew'
? __('立即续费', 'aksrt')
: ($action === 'upgrade' ? __('立即升级', 'aksrt') : __('立即开通', 'aksrt')),
];
}
// Dedicated upgrade product: fixed or remaining-days unit price.
if ($level === 1 && $lv === 2 && $upgrade_enabled) {
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? ''));
foreach ($upgrade_items as $ui) {
$items[] = $ui;
}
}
if ($items === []) {
continue;
}
$level_packages[] = [
'level' => $lv,
'name' => $name,
'action' => ($level > 0 && $lv === $level) ? 'renew' : (($level > 0 && $lv > $level) ? 'upgrade' : 'pay'),
'items' => $items,
];
}
$points_packages = [];
if ($points_enabled && $level <= 0) {
$points_packages = $this->get_points_exchange_packages($vip, $levels);
}
$balance = ($user_id > 0 && class_exists('AKSRT_Credits')) ? AKSRT_Credits::balance($user_id) : 0;
$methods = $this->available_payment_methods();
$currency_name = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
return [
'enabled' => !empty($vip['enabled']),
'desc' => (string) ($vip['desc'] ?? ''),
'help_text' => (string) ($vip['help_text'] ?? ''),
'user_level' => $level,
'user_expires' => $expires,
'user_is_permanent' => $is_permanent,
'user_expires_label' => $this->format_expires_label($expires, $level),
'user_points' => $balance,
'currency_name' => $currency_name,
'renew_enabled' => $renew_enabled,
'upgrade_enabled' => $upgrade_enabled,
'renew_desc' => (string) ($renew['desc'] ?? ''),
'upgrade_desc' => (string) ($upgrade['desc'] ?? ''),
'level_packages' => $level_packages,
'points_exchange' => [
'enabled' => $points_enabled && $level <= 0,
'desc' => (string) ($points_ex['desc'] ?? ''),
'packages' => $points_packages,
],
'payment_methods' => $methods,
'vip_url' => function_exists('aksrt_get_user_center_tab_url')
? aksrt_get_user_center_tab_url('credits')
: (function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/')),
'logged_in' => $user_id > 0,
];
}
/**
* @return array|WP_Error
*/
public function purchase(int $user_id, int $package_id, string $payment_method = 'balance') {
if ($user_id <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录后再开通会员。', 'aksrt'), ['status' => 401]);
}
$decoded = $this->decode_package_id($package_id);
if (!$decoded) {
return new WP_Error('aksrt_invalid_package', __('套餐无效', 'aksrt'), ['status' => 400]);
}
$level = (int) $decoded['level'];
$index = (int) $decoded['index'];
$vip = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_payment_vip_settings()
: ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => []];
if (empty($vip['enabled'])) {
return new WP_Error('aksrt_vip_disabled', __('会员功能未启用', 'aksrt'), ['status' => 400]);
}
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
if (empty($levels[$level]['enabled'])) {
return new WP_Error('aksrt_vip_level_disabled', __('该会员等级未启用', 'aksrt'), ['status' => 400]);
}
$user_level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0;
$expires = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
$is_permanent = ($user_level > 0 && $expires <= 0);
$renew = is_array($vip['renew'] ?? null) ? $vip['renew'] : [];
$upgrade = is_array($vip['upgrade'] ?? null) ? $vip['upgrade'] : [];
$renew_enabled = !isset($renew['enabled']) || !empty($renew['enabled']);
$upgrade_enabled = !isset($upgrade['enabled']) || !empty($upgrade['enabled']);
if ($is_permanent && $level <= $user_level) {
return new WP_Error('aksrt_vip_permanent', __('您已是永久会员,无需续费。', 'aksrt'), ['status' => 400]);
}
if ($user_level > 0 && $level === $user_level && !$renew_enabled) {
return new WP_Error('aksrt_vip_renew_disabled', __('续费功能未开启。', 'aksrt'), ['status' => 400]);
}
if ($user_level > 0 && $level > $user_level && !$upgrade_enabled) {
return new WP_Error('aksrt_vip_upgrade_disabled', __('升级功能未开启。', 'aksrt'), ['status' => 400]);
}
if ($user_level > 0 && $level < $user_level) {
return new WP_Error('aksrt_vip_downgrade', __('不支持降级购买。', 'aksrt'), ['status' => 400]);
}
$action = 'pay';
if ($user_level > 0 && $level === $user_level) {
$action = 'renew';
} elseif ($user_level > 0 && $level > $user_level) {
$action = 'upgrade';
}
// Dedicated upgrade product:
// 901 permanent/fixed, 902 unit remaining days, 903 jump permanent.
$upgrade_mode = '';
if (in_array($index, [901, 902, 903], true)) {
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? ''));
$match = null;
foreach ($upgrade_items as $ui) {
if ((int) ($ui['package_id'] ?? 0) === $package_id) {
$match = $ui;
break;
}
}
if (!$match || $level !== 2) {
return new WP_Error('aksrt_invalid_package', __('升级套餐不存在', 'aksrt'), ['status' => 400]);
}
$pkg = [
'price' => (float) $match['price'],
'days' => (int) $match['days'],
'title' => (string) $match['title'],
];
$amount = round((float) $match['price'], 2);
$days = (int) $match['days'];
$base_price = (float) ($match['base_price'] ?? $amount);
$discount_tag = (string) ($match['discount_tag'] ?? __('升级专享', 'aksrt'));
$upgrade_mode = sanitize_key((string) ($match['upgrade_mode'] ?? ''));
$action = 'upgrade';
} else {
$packages = $this->parse_packages((string) ($vip['packages_raw'] ?? ''));
$pkg = $packages[$index - 1] ?? null;
if (!$pkg) {
return new WP_Error('aksrt_invalid_package', __('套餐不存在', 'aksrt'), ['status' => 400]);
}
$base_price = (float) $pkg['price'];
$days = (int) $pkg['days'];
$priced = $this->apply_action_price($base_price, $action, $renew, $upgrade, $level, $user_level, $expires, $is_permanent, $days);
if ($priced === null) {
return new WP_Error('aksrt_invalid_package', __('当前无法购买该套餐', 'aksrt'), ['status' => 400]);
}
$amount = round((float) $priced['price'], 2);
$discount_tag = (string) ($priced['tag'] ?? '');
}
if ($amount <= 0) {
return new WP_Error('aksrt_invalid_price', __('套餐价格无效', 'aksrt'), ['status' => 400]);
}
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用', 'aksrt'), ['status' => 500]);
}
$payment_method = sanitize_key($payment_method);
if ($payment_method === '') {
$payment_method = 'balance';
}
$note = wp_json_encode([
'vip_pay' => 1,
'level' => $level,
'days' => $days,
'package_index' => $index,
'package_title' => (string) ($pkg['title'] ?? ''),
'package_id' => $package_id,
'action' => $action,
'upgrade_mode' => $upgrade_mode,
'base_price' => $base_price,
'discount_tag' => $discount_tag,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = AKSRT_Order_Repository::get_instance()->create([
'user_id' => $user_id,
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_VIP,
'product_id' => $package_id,
'amount' => $amount,
'currency' => 'CNY',
'status' => AKSRT_Order::STATUS_PENDING,
'payment_method' => $payment_method,
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$order = AKSRT_Order_Repository::get_instance()->get($order_number);
if (!$order) {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
$action_label = $action === 'renew' ? __('续费', 'aksrt') : ($action === 'upgrade' ? __('升级', 'aksrt') : __('开通', 'aksrt'));
$order['subject'] = sprintf(
/* translators: 1: action 2: vip name 3: package title */
__('%1$s%2$s · %3$s', 'aksrt'),
$action_label,
$level_name,
(string) ($pkg['title'] ?? '')
);
$order['payment_method'] = $payment_method;
if (!class_exists('AKSRT_Payment_Gateway')) {
return new WP_Error('aksrt_pay_unavailable', __('支付网关不可用', 'aksrt'), ['status' => 500]);
}
$gateway = AKSRT_Payment_Gateway::get_instance()->get_gateway($payment_method);
if (!$gateway) {
return new WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用', 'aksrt'), [
'status' => 400,
'order_number' => $order_number,
]);
}
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
return new WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [
'status' => 400,
'order_number' => $order_number,
]);
}
$payment_result = $gateway->create_payment($order);
if (empty($payment_result['success'])) {
$msg = (string) ($payment_result['message'] ?? __('支付失败', 'aksrt'));
$code = ($payment_method === 'balance' && (str_contains($msg, '余额') || str_contains($msg, '不足')))
? 'aksrt_balance_insufficient'
: 'aksrt_pay_failed';
return new WP_Error($code, $msg, [
'status' => 402,
'order_number' => $order_number,
]);
}
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
$payment_url = (string) ($payment_result['payment_url'] ?? '');
if ($payment_url !== '' || $code_url !== '') {
return [
'success' => true,
'data' => [
'redirect' => true,
'payment_url' => $payment_url,
'qr_content' => $code_url,
'order_number' => $order_number,
'amount' => $amount,
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
'reload' => false,
'poll' => true,
],
];
}
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => $payment_method,
'payment_transaction_id' => sanitize_text_field((string) ($payment_result['transaction_id'] ?? '')),
'note' => __('用户完成会员开通支付。', 'aksrt'),
]);
if (!$paid) {
return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败', 'aksrt'), ['status' => 500]);
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => $amount,
'level' => $level,
'days' => $days,
'action' => $action,
'message' => $action === 'renew'
? __('会员续费成功', 'aksrt')
: ($action === 'upgrade' ? __('会员升级成功', 'aksrt') : __('会员开通成功', 'aksrt')),
'reload' => true,
],
];
}
/**
* Activate VIP on paid order.
*
* @param string $order_number
* @param array $paid_order
*/
public function on_order_paid(string $order_number, array $paid_order): void {
if (sanitize_key((string) ($paid_order['product_type'] ?? '')) !== AKSRT_Domain_Model::ORDER_PRODUCT_VIP) {
return;
}
$user_id = (int) ($paid_order['user_id'] ?? 0);
if ($user_id <= 0) {
return;
}
$meta = [];
if (!empty($paid_order['note']) && is_string($paid_order['note'])) {
$decoded = json_decode($paid_order['note'], true);
if (is_array($decoded)) {
$meta = $decoded;
}
}
if (empty($meta) && is_array($paid_order['meta_payload'] ?? null)) {
$meta = $paid_order['meta_payload'];
}
$level = (int) ($meta['level'] ?? 0);
$days = (int) ($meta['days'] ?? 0);
$upgrade_mode = sanitize_key((string) ($meta['upgrade_mode'] ?? ''));
if ($level <= 0) {
$decoded = $this->decode_package_id((int) ($paid_order['product_id'] ?? 0));
if ($decoded) {
$level = (int) $decoded['level'];
}
}
if ($level < 1) {
$level = 1;
}
if ($level > 2) {
$level = 2;
}
// unit upgrade: raise level only, keep current expiry (zibll).
if ($upgrade_mode === 'unit') {
$this->activate_vip($user_id, $level, 0, true);
} else {
$this->activate_vip($user_id, $level, $days, false);
}
}
/**
* Apply membership meta.
*
* @param bool $keep_expiry When true (unit upgrade), only raise level and do not change expiry.
* When days<=0 and keep_expiry=false, set permanent (expires=0).
*/
public function activate_vip(int $user_id, int $level, int $days = 0, bool $keep_expiry = false): void {
if ($user_id <= 0) {
return;
}
$level = max(1, min(2, $level));
$current_level = (int) get_user_meta($user_id, 'aksrt_vip_level', true);
$new_level = max($current_level, $level);
update_user_meta($user_id, 'aksrt_vip_level', $new_level);
// Unit upgrade (zibll): keep vip_exp_date, only raise level.
if ($keep_expiry) {
return;
}
if ($days <= 0) {
// Permanent.
update_user_meta($user_id, 'aksrt_vip_expires', 0);
return;
}
$current_exp = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
if ($current_exp === 0 && $current_level > 0) {
// Already permanent — keep permanent.
return;
}
$base = max($current_exp, time());
update_user_meta($user_id, 'aksrt_vip_expires', $base + ($days * DAY_IN_SECONDS));
}
/**
* Points exchange (non-VIP only, zibll pay_vip_points_exchange).
*
* @return array|WP_Error
*/
public function exchange_with_points(int $user_id, int $package_id) {
if ($user_id <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录后再兑换。', 'aksrt'), ['status' => 401]);
}
if (!class_exists('AKSRT_Credits')) {
return new WP_Error('aksrt_credits_unavailable', __('积分系统不可用', 'aksrt'), ['status' => 500]);
}
if (class_exists('AKSRT_Settings') && !AKSRT_Settings::credits_enabled()) {
return new WP_Error('aksrt_credits_disabled', __('积分功能未启用', 'aksrt'), ['status' => 400]);
}
$vip = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_payment_vip_settings()
: [];
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
if (empty($points_ex['enabled'])) {
return new WP_Error('aksrt_points_exchange_disabled', __('积分兑换会员未启用', 'aksrt'), ['status' => 400]);
}
$user_level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0;
if ($user_level > 0) {
return new WP_Error('aksrt_already_vip', __('您已是会员,无法使用积分兑换。', 'aksrt'), ['status' => 400]);
}
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
$packages = $this->get_points_exchange_packages($vip, $levels);
$match = null;
foreach ($packages as $pkg) {
if ((int) ($pkg['package_id'] ?? 0) === $package_id) {
$match = $pkg;
break;
}
}
if (!$match) {
return new WP_Error('aksrt_invalid_package', __('兑换套餐不存在', 'aksrt'), ['status' => 400]);
}
$cost = (int) ($match['points'] ?? 0);
$level = (int) ($match['level'] ?? 1);
$days = (int) ($match['days'] ?? 0);
if ($cost <= 0) {
return new WP_Error('aksrt_invalid_price', __('兑换积分无效', 'aksrt'), ['status' => 400]);
}
$balance = AKSRT_Credits::balance($user_id);
if ($balance < $cost) {
return new WP_Error('aksrt_credits_insufficient', __('积分不足,暂时无法兑换', 'aksrt'), [
'status' => 402,
'balance' => $balance,
'cost' => $cost,
]);
}
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
return new WP_Error('aksrt_pay_unavailable', __('订单系统不可用', 'aksrt'), ['status' => 500]);
}
$note = wp_json_encode([
'vip_pay' => 1,
'vip_pay_type' => 'points_exchange',
'level' => $level,
'days' => $days,
'points' => $cost,
'package_id' => $package_id,
'package_title' => (string) ($match['title'] ?? ''),
'action' => 'points_exchange',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = AKSRT_Order_Repository::get_instance()->create([
'user_id' => $user_id,
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_VIP,
'product_id' => $package_id,
'amount' => (float) $cost,
'currency' => 'points',
'status' => AKSRT_Order::STATUS_PENDING,
'payment_method' => 'points',
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$deducted = AKSRT_Credits::deduct(
$user_id,
$cost,
'vip_points_exchange',
['order' => $order_number, 'level' => $level, 'days' => $days],
sprintf(__('积分兑换会员 · %s', 'aksrt'), (string) ($match['title'] ?? ''))
);
if (is_wp_error($deducted) || $deducted === false) {
AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [
'note' => __('积分扣除失败,自动取消', 'aksrt'),
]);
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt');
return new WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败', 'aksrt'), ['status' => 400]);
}
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => 'points',
'payment_transaction_id' => 'points_' . $order_number,
'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost),
]);
if (!$paid) {
AKSRT_Credits::add(
$user_id,
$cost,
'vip_points_exchange_refund',
['order' => $order_number],
__('会员兑换失败退款', 'aksrt')
);
return new WP_Error('aksrt_order_confirm_failed', __('兑换确认失败,已尝试退回积分', 'aksrt'), ['status' => 500]);
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'points' => $cost,
'level' => $level,
'days' => $days,
'balance' => AKSRT_Credits::balance($user_id),
'message' => __('兑换成功', 'aksrt'),
'reload' => true,
],
];
}
}
@@ -0,0 +1,124 @@
<?php
/**
* Extracted trait: AKSRT_Vip_Service_Rest
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Vip_Service_Rest {
public function register_routes(): void {
register_rest_route('aksrt/v1', '/pay/vip', [
[
'methods' => 'GET',
'callback' => [$this, 'rest_catalog'],
'permission_callback' => [$this, 'check_rest_nonce'],
],
[
'methods' => 'POST',
'callback' => [$this, 'rest_purchase'],
'permission_callback' => [$this, 'check_rest_nonce'],
],
]);
register_rest_route('aksrt/v1', '/pay/vip/points-exchange', [
'methods' => 'POST',
'callback' => [$this, 'rest_points_exchange'],
'permission_callback' => [$this, 'check_rest_nonce'],
]);
}
/**
* @param WP_REST_Request $request
* @return true|WP_Error
*/
public function check_rest_nonce(WP_REST_Request $request) {
$nonce = $request->get_header('x-wp-nonce');
if (!$nonce) {
return new WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]);
}
if (!wp_verify_nonce($nonce, 'wp_rest')) {
return new WP_Error('aksrt_rest_bad_nonce', __('安全令牌已过期,请刷新页面', 'aksrt'), ['status' => 403]);
}
return true;
}
/**
* GET /pay/vip
*/
public function rest_catalog(WP_REST_Request $request) {
$uid = get_current_user_id();
return new WP_REST_Response($this->get_catalog($uid), 200);
}
/**
* POST /pay/vip
* body: { package_id: int, payment_method?: string }
*/
public function rest_purchase(WP_REST_Request $request) {
$uid = get_current_user_id();
if ($uid <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录后再开通会员。', 'aksrt'), [
'status' => 401,
'login_url' => function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(),
]);
}
$params = $request->get_json_params();
if (!is_array($params)) {
$params = $request->get_params();
}
$package_id = (int) ($params['package_id'] ?? 0);
$payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance'));
if ($payment_method === '' || $payment_method === 'cash') {
$payment_method = 'balance';
}
$result = $this->purchase($uid, $package_id, $payment_method);
if (is_wp_error($result)) {
return $result;
}
if (empty($result['success'])) {
return new WP_Error(
sanitize_key((string) ($result['code'] ?? 'aksrt_vip_pay_failed')),
(string) ($result['message'] ?? __('开通失败', 'aksrt')),
['status' => (int) ($result['status'] ?? 400)]
);
}
return new WP_REST_Response($result, 200);
}
/**
* POST /pay/vip/points-exchange
* body: { package_id: int } // points package id = 5000 + index
*/
public function rest_points_exchange(WP_REST_Request $request) {
$uid = get_current_user_id();
if ($uid <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录后再兑换。', 'aksrt'), ['status' => 401]);
}
$params = $request->get_json_params();
if (!is_array($params)) {
$params = $request->get_params();
}
$package_id = (int) ($params['package_id'] ?? 0);
$result = $this->exchange_with_points($uid, $package_id);
if (is_wp_error($result)) {
return $result;
}
return new WP_REST_Response($result, 200);
}
}
+1
View File
@@ -11,6 +11,7 @@ defined('ABSPATH') || exit;
class AKSRT_Scripts {
use AKSRT_Scripts_Part_1b;
use AKSRT_Scripts_Part_1c;
use AKSRT_Scripts_Part_2;
@@ -775,579 +775,8 @@ trait AKSRT_Scripts_Part_1b {
}
}
// ===== 5. 字体 =====
// 5a. 拉丁字体 - Inter (UI) + Source Serif 4 (正文)
wp_enqueue_style(
'aksrt-fonts',
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,500;0,8..60,600;0,8..60,700;1,8..60,400&display=swap',
[],
null
);
// 5b. 中文字体 - jsDelivr CDN (思源黑体 / 思源宋体)
// 仅 regular 字重,避免拖慢首屏;其它字重通过 system fallback (PingFang/微软雅黑)
// Fontsource v5 已按 unicode-range 自动分包,浏览器只会下载用到的简中子集
wp_enqueue_style(
'aksrt-fonts-cn',
'https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-sc@5.2.9/400.css',
[],
null
);
wp_enqueue_style(
'aksrt-fonts-cn-serif',
'https://cdn.jsdelivr.net/npm/@fontsource/noto-serif-sc@5.2.9/400.css',
[],
null
);
// ===== 6. 图标系统 =====
// Font Awesome 5 全站加载(评论回复、单篇互动栏、profile 联系方式等
// 多处模板使用 fa-* 类名,作为全主题图标包统一使用)
wp_enqueue_style(
'aksrt-font-awesome-5',
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css',
[],
'5.15.4'
);
// ===== 7. JS =====
// Swiper.js(首页/归档/CMS 模块/单篇文章页加载)
$needs_swiper = $is_home_or_archive || $is_single_post || $is_single_resource;
if ($needs_swiper) {
wp_enqueue_style(
'aksrt-swiper',
$base_uri . '/assets/css/frontend/swiper-bundle.min.css',
[],
'11.0'
);
wp_enqueue_script(
'aksrt-swiper',
$base_uri . '/assets/js/swiper-bundle.min.js',
[],
'11.0',
true
);
}
wp_enqueue_script(
'aksrt-qrcode',
'https://cdn.jsdelivr.net/npm/kjua@0.9.0/dist/kjua.min.js',
[],
'0.9.0',
true
);
// Rehydrate system — must load before aksrt-script so AksrtFrontend is available
wp_enqueue_script(
'aksrt-rehydrate',
$base_uri . '/assets/js/rehydrate.js',
[],
file_exists(get_template_directory() . '/assets/js/rehydrate.js')
? (string) filemtime(get_template_directory() . '/assets/js/rehydrate.js')
: $version,
true
);
// Runtime infrastructure: tbquire / notyf / confirm / refreshModal / scrollTopTo / [data-*] declarative
$runtime_js = get_template_directory() . '/assets/js/aksrt-runtime.js';
if (file_exists($runtime_js)) {
wp_enqueue_script(
'aksrt-runtime',
$base_uri . '/assets/js/aksrt-runtime.js',
['aksrt-rehydrate'],
(string) filemtime($runtime_js),
true
);
}
// Core modules (split from main.js) — register with AksrtFrontend
$core_modules = [
'aksrt-core-theme' => 'theme.js',
'aksrt-core-nav' => 'nav.js',
'aksrt-core-search' => 'search.js',
'aksrt-core-dropdown' => 'dropdown.js',
'aksrt-core-scroll' => 'scroll.js',
'aksrt-core-slider' => 'slider.js',
'aksrt-core-article' => 'article.js',
'aksrt-core-comments' => 'comments.js',
'aksrt-core-pagination' => 'pagination.js',
'aksrt-core-archive' => 'archive.js',
'aksrt-core-footer-tabbar' => 'footer-tabbar.js',
'aksrt-core-misc' => 'misc.js',
];
$core_handles = [];
foreach ($core_modules as $handle => $file) {
$path = get_template_directory() . '/assets/js/core/' . $file;
if (file_exists($path)) {
// Depend on aksrt-runtime so window.aksrtData (localized below) is available.
$deps = ['aksrt-rehydrate', 'aksrt-runtime'];
if ($handle === 'aksrt-core-slider') {
$deps = array_merge($deps, $needs_swiper ? ['aksrt-swiper'] : []);
}
wp_enqueue_script(
$handle,
$base_uri . '/assets/js/core/' . $file,
$deps,
(string) filemtime($path),
true
);
$core_handles[] = $handle;
}
}
wp_enqueue_script(
'aksrt-script',
$base_uri . '/assets/js/main.js',
array_merge(
['jquery', 'bootstrap5', 'aksrt-rehydrate', 'aksrt-runtime'],
$core_handles
),
file_exists(get_template_directory() . '/assets/js/main.js')
? (string) filemtime(get_template_directory() . '/assets/js/main.js')
: $version,
true
);
// SVG icon data + helper (must load before other scripts that use icons)
$svg_icon_js = get_template_directory() . '/assets/js/svg-icon.js';
if (file_exists($svg_icon_js)) {
wp_enqueue_script(
'aksrt-svg-icon',
$base_uri . '/assets/js/svg-icon.js',
[],
(string) filemtime($svg_icon_js),
false // load in <head> so icons are available early
);
}
$svg_helper_js = get_template_directory() . '/assets/js/svg-icon-helper.js';
if (file_exists($svg_helper_js)) {
wp_enqueue_script(
'aksrt-svg-icon-helper',
$base_uri . '/assets/js/svg-icon-helper.js',
['aksrt-svg-icon'],
(string) filemtime($svg_helper_js),
true
);
}
// Page loading indicator (top progress bar)
$loader_js = get_template_directory() . '/assets/js/loader.js';
if (file_exists($loader_js)) {
wp_enqueue_script(
'aksrt-loader',
$base_uri . '/assets/js/loader.js',
[],
(string) filemtime($loader_js),
true
);
}
// Code block enhancements (copy button + language label)
$precode_js = get_template_directory() . '/assets/js/precode.js';
if (file_exists($precode_js)) {
wp_enqueue_script(
'aksrt-precode',
$base_uri . '/assets/js/precode.js',
[],
(string) filemtime($precode_js),
true
);
}
// Input field enhancements (auto-resize, counter, validation)
$input_expand_js = get_template_directory() . '/assets/js/input-expand.js';
if (file_exists($input_expand_js)) {
wp_enqueue_script(
'aksrt-input-expand',
$base_uri . '/assets/js/input-expand.js',
[],
(string) filemtime($input_expand_js),
true
);
}
// Instant page prefetch (hover to preload)
$instant_page_js = get_template_directory() . '/assets/js/instant-page.js';
if (file_exists($instant_page_js)) {
wp_enqueue_script(
'aksrt-instant-page',
$base_uri . '/assets/js/instant-page.js',
[],
(string) filemtime($instant_page_js),
false // load in <head> for early prefetch capability
);
}
// Touch gesture support (swipe, pull-to-dismiss)
$mini_touch_js = get_template_directory() . '/assets/js/mini-touch.js';
if (file_exists($mini_touch_js)) {
wp_enqueue_script(
'aksrt-mini-touch',
$base_uri . '/assets/js/mini-touch.js',
[],
(string) filemtime($mini_touch_js),
true
);
}
// Section navs: article TOC / scrollspy
$section_navs_js = get_template_directory() . '/assets/js/section-navs.js';
if (file_exists($section_navs_js)) {
wp_enqueue_script(
'aksrt-section-navs',
$base_uri . '/assets/js/section-navs.js',
['aksrt-rehydrate', 'aksrt-runtime'],
(string) filemtime($section_navs_js),
true
);
}
// Video player (MP4/HLS/FLV)
$video_player_js = get_template_directory() . '/assets/js/video-player.js';
if (file_exists($video_player_js)) {
wp_enqueue_script(
'aksrt-video-player',
$base_uri . '/assets/js/video-player.js',
[],
(string) filemtime($video_player_js),
true
);
}
// Image color extraction
$color_extract_js = get_template_directory() . '/assets/js/color-extract.js';
if (file_exists($color_extract_js)) {
wp_enqueue_script(
'aksrt-color-extract',
$base_uri . '/assets/js/color-extract.js',
[],
(string) filemtime($color_extract_js),
true
);
}
$widget_set_js = get_template_directory() . '/assets/js/widget-set.js';
if (file_exists($widget_set_js)) {
wp_enqueue_script(
'aksrt-widget-set',
$base_uri . '/assets/js/widget-set.js',
['aksrt-script', 'aksrt-qrcode'],
(string) filemtime($widget_set_js),
true
);
}
// ===== 全局 UI 增强(Toast、骨架屏、悬停动效、灯箱、滚动感知)=====
$enhancements_css = get_template_directory() . '/assets/css/frontend/enhancements.css';
if (file_exists($enhancements_css)) {
wp_enqueue_style(
'aksrt-enhancements',
$base_uri . '/assets/css/frontend/enhancements.css',
[$core_dep],
(string) filemtime($enhancements_css)
);
}
$enhancements_js = get_template_directory() . '/assets/js/enhancements.js';
if (file_exists($enhancements_js)) {
wp_enqueue_script(
'aksrt-enhancements',
$base_uri . '/assets/js/enhancements.js',
['aksrt-script'],
(string) filemtime($enhancements_js),
true
);
}
$enhancements2_js = get_template_directory() . '/assets/js/enhancements2.js';
if (file_exists($enhancements2_js)) {
wp_enqueue_script(
'aksrt-enhancements2',
$base_uri . '/assets/js/enhancements2.js',
['aksrt-script'],
(string) filemtime($enhancements2_js),
true
);
}
// Reward (打赏)
$reward_js = get_template_directory() . '/assets/js/reward.js';
if (file_exists($reward_js)) {
wp_enqueue_script(
'aksrt-reward',
$base_uri . '/assets/js/reward.js',
['aksrt-script'],
(string) filemtime($reward_js),
true
);
}
// Effects (count-up, countdown, marquee, tilt, FPS detection)
$effects_js = get_template_directory() . '/assets/js/effects.js';
if (file_exists($effects_js)) {
wp_enqueue_script(
'aksrt-effects',
$base_uri . '/assets/js/effects.js',
['aksrt-script'],
(string) filemtime($effects_js),
true
);
}
// Yiyan (一言轮播)
$yiyan_js = get_template_directory() . '/assets/js/yiyan.js';
if (file_exists($yiyan_js)) {
wp_enqueue_script(
'aksrt-yiyan',
$base_uri . '/assets/js/yiyan.js',
[],
(string) filemtime($yiyan_js),
true
);
}
// Frontend translate (默认关)
$translate_js = get_template_directory() . '/assets/js/frontend-translate.js';
if (file_exists($translate_js)) {
wp_enqueue_script(
'aksrt-frontend-translate',
$base_uri . '/assets/js/frontend-translate.js',
[],
(string) filemtime($translate_js),
true
);
}
// Content protection (copy notice / right-click / F12)
$content_protect_js = get_template_directory() . '/assets/js/content-protect.js';
if (file_exists($content_protect_js)) {
wp_enqueue_script(
'aksrt-content-protect',
$base_uri . '/assets/js/content-protect.js',
[],
(string) filemtime($content_protect_js),
false // load in <head> for early binding
);
}
// Site notice popup (global, controlled by Settings)
$site_notice_css = get_template_directory() . '/assets/css/frontend/site-notice.css';
if (file_exists($site_notice_css)) {
wp_enqueue_style(
'aksrt-site-notice',
$base_uri . '/assets/css/frontend/site-notice.css',
[$core_dep],
(string) filemtime($site_notice_css)
);
}
$site_notice_js = get_template_directory() . '/assets/js/site-notice.js';
if (file_exists($site_notice_js)) {
wp_enqueue_script(
'aksrt-site-notice',
$base_uri . '/assets/js/site-notice.js',
[],
(string) filemtime($site_notice_js),
true
);
}
// 弹窗登录/注册(仅未登录用户)
if (!is_user_logged_in()) {
$modal_auth_js = get_template_directory() . '/assets/js/modal-auth.js';
if (file_exists($modal_auth_js)) {
wp_enqueue_script(
'aksrt-modal-auth',
$base_uri . '/assets/js/modal-auth.js',
['aksrt-script'],
(string) filemtime($modal_auth_js),
true
);
}
}
if ($is_fixed_homepage) {
$homepage_js_path = get_template_directory() . '/assets/js/homepage-fixed.js';
wp_enqueue_script(
'aksrt-homepage-fixed',
$base_uri . '/assets/js/homepage-fixed.js',
['aksrt-script'],
file_exists($homepage_js_path) ? (string) filemtime($homepage_js_path) : $version,
true
);
}
// Comment toolbar (emoji, code, bold, Ctrl+Enter) — only when the form is open.
if (is_singular() && comments_open()) {
$comment_tools_path = get_template_directory() . '/assets/js/comment-tools.js';
wp_enqueue_script(
'aksrt-comment-tools',
$base_uri . '/assets/js/comment-tools.js',
['aksrt-script'],
file_exists($comment_tools_path) ? (string) filemtime($comment_tools_path) : $version,
true
);
}
// Comment actions client (like / edit / delete / moderate / pin) — load
// wherever comments can exist, including closed-comment posts. Submission
// itself is handled by main.js initCommentSystem().
if (is_singular() && (comments_open() || (int) get_comments_number() > 0)) {
$comment_ajax_path = get_template_directory() . '/assets/js/comment-ajax.js';
if (file_exists($comment_ajax_path)) {
wp_enqueue_script(
'aksrt-comment-ajax',
$base_uri . '/assets/js/comment-ajax.js',
['aksrt-script'],
(string) filemtime($comment_ajax_path),
true
);
}
}
if (is_singular() || is_page_template('templates/pages/page-forums.php')) {
$page_template_path = get_template_directory() . '/assets/js/page-template.js';
if (file_exists($page_template_path)) {
wp_enqueue_script(
'aksrt-page-template',
$base_uri . '/assets/js/page-template.js',
['aksrt-script'],
(string) filemtime($page_template_path),
true
);
}
}
// Post/resource share functionality
if ($is_single_post || $is_single_resource) {
$weixin_share_path = get_template_directory() . '/assets/js/weixin-share.js';
if (file_exists($weixin_share_path)) {
wp_enqueue_script(
'aksrt-weixin-share',
$base_uri . '/assets/js/weixin-share.js',
['aksrt-script'],
(string) filemtime($weixin_share_path),
true
);
}
$post_share_path = get_template_directory() . '/assets/js/post-share.js';
if (file_exists($post_share_path)) {
wp_enqueue_script(
'aksrt-post-share',
$base_uri . '/assets/js/post-share.js',
['aksrt-script', 'aksrt-weixin-share', 'aksrt-share-data'],
(string) filemtime($post_share_path),
true
);
}
}
// Page navigation (TOC highlight, smooth scroll, doc search)
// 评论回复脚本
if (is_singular() && comments_open() && get_option('thread_comments')) {
wp_enqueue_script('comment-reply');
}
$runtime_routes = $this->resolve_frontend_routes();
// 数据注入 - 全站统一消费 window.aksrtData
// Attach to aksrt-runtime (loaded before all core modules) so ajaxUrl/nonce
// exist before pagination/search/article handlers run.
$aksrt_data_handle = wp_script_is('aksrt-runtime', 'registered') || wp_script_is('aksrt-runtime', 'enqueued')
? 'aksrt-runtime'
: 'aksrt-script';
wp_localize_script($aksrt_data_handle, 'aksrtData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('aksrt_public_nonce'),
'restNonce' => wp_create_nonce('wp_rest'),
'visibilityNonce' => wp_create_nonce('aksrt_visibility'),
'siteUrl' => home_url('/'),
'themeUrl' => AKSRT_URI,
'themeMode' => class_exists('AKSRT_Settings') ? AKSRT_Settings::theme_mode_js_value() : 'auto',
'debug' => defined('WP_DEBUG') && WP_DEBUG,
'debugLog' => defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG,
'postPaginationMode' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_pagination_mode() : 'load_more',
'commentPaginationMode' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_comment_pagination_mode() : 'numbered',
'homepagePostsPerPage' => function_exists('aksrt_homepage_get_tab_posts_per_page') ? aksrt_homepage_get_tab_posts_per_page() : (int) get_option('posts_per_page'),
'homepageDefaultSort' => class_exists('AKSRT_Settings') ? AKSRT_Settings::homepage_default_sort() : 'modified',
'homepageDefaultView' => class_exists('AKSRT_Settings') ? AKSRT_Settings::homepage_default_view() : 'list',
'homepageSortAjax' => class_exists('AKSRT_Settings') ? AKSRT_Settings::homepage_sort_ajax_enabled() : true,
'homepageStickyTabs' => class_exists('AKSRT_Settings') ? AKSRT_Settings::homepage_sticky_tabs_enabled() : true,
'homePostsListEnabled' => !class_exists('AKSRT_Settings') || AKSRT_Settings::home_posts_list_enabled(),
'homeListSortEnabled' => !class_exists('AKSRT_Settings') || AKSRT_Settings::home_list_sort_enabled(),
'homeListSortDropdown' => class_exists('AKSRT_Settings') && AKSRT_Settings::home_list_sort_dropdown(),
// AJAX 翻页文案
'ajaxTrigger' => (class_exists('AKSRT_Settings') && ($t = AKSRT_Settings::get_post_list_style_settings()['ajax_trigger_text'] ?? '') !== '') ? $t : __('加载更多', 'aksrt'),
'ajaxNomore' => (class_exists('AKSRT_Settings') && ($n = AKSRT_Settings::get_post_list_style_settings()['ajax_nomore_text'] ?? '') !== '') ? $n : __('没有更多了', 'aksrt'),
'ajaxLoading' => __('正在加载...', 'aksrt'),
'iasMax' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_list_style_settings()['ias_max'] : 3,
'commentIasAuto' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_comment_display_settings()['paging_ajax_ias'] : true,
'commentUploadEnabled' => class_exists('AKSRT_Settings') ? !empty((AKSRT_Settings::get_comment_display_settings()['toolbar']['upload_img'] ?? false)) : false,
'commentUploadUrl' => esc_url_raw(rest_url('aksrt/v1/comment/upload-image')),
'commentUploadMaxSize' => 2 * 1024 * 1024,
'commentIasMax' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_comment_display_settings()['paging_ajax_ias_max'] : 3,
'stickyEnabled' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_list_style_settings()['sticky_enabled'] : true,
'stickyPages' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_list_style_settings()['sticky_pages'] : ['home', 'cat'],
'badgeShow' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_list_style_settings()['badge_show'] : true,
'badgeTypes' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_list_style_settings()['badge_types'] : ['pay', 'tag', 'topics', 'cat'],
'search' => [
'mode' => class_exists('AKSRT_Settings') ? AKSRT_Settings::search_mode() : 'default',
'placeholder' => function_exists('aksrt_get_search_placeholder') ? aksrt_get_search_placeholder() : __('开启精彩搜索...', 'aksrt'),
'historyEnabled' => function_exists('aksrt_search_history_enabled') ? aksrt_search_history_enabled() : true,
'suggestionsEnabled' => function_exists('aksrt_search_suggestions_enabled') ? aksrt_search_suggestions_enabled() : false,
'hotKeywords' => function_exists('aksrt_get_search_hot_keywords') ? aksrt_get_search_hot_keywords() : [],
'popularTitle' => class_exists('AKSRT_Settings') ? AKSRT_Settings::search_popular_title() : __('热门搜索', 'aksrt'),
'contentTypes' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_search_content_types()['types'] : ['post', 'category', 'tag'],
'defaultType' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_search_content_types()['default'] : 'post',
],
// 用户状态
'isLoggedIn' => is_user_logged_in(),
'userId' => get_current_user_id(),
'cartUrl' => $runtime_routes['cart'],
'ordersUrl' => $runtime_routes['orders'],
'userCenterUrl' => function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/'),
'routes' => [
'home' => home_url('/'),
'login' => function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(),
'register' => function_exists('aksrt_get_register_url') ? aksrt_get_register_url() : wp_registration_url(),
'resetPassword' => function_exists('aksrt_get_reset_password_url') ? aksrt_get_reset_password_url() : wp_lostpassword_url(),
'profile' => function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/profile/'),
'cart' => $runtime_routes['cart'],
'orders' => $runtime_routes['orders'],
'resources' => $runtime_routes['resources'],
],
'shell' => [
'wrapperId' => '',
'navId' => 'site-nav',
'searchPanelId' => 'nav-search-panel',
'drawerId' => 'nav-drawer',
'userMenuId' => 'user-dropdown',
'floatActionsId' => 'float-right-btns',
'tabbarId' => 'footer-tabbar',
],
'themeUri' => get_template_directory_uri(),
'codeHighlight' => class_exists('AKSRT_Settings')
? AKSRT_Settings::get_code_highlight_config()
: ['enabled' => true, 'lightTheme' => 'enlighter', 'darkTheme' => 'monokai', 'lineNumbers' => false, 'showButtons' => true, 'maxHeight' => 500],
'lazyload' => class_exists('AKSRT_Settings')
? AKSRT_Settings::get_lazyload_config()
: ['enabled' => true, 'animation' => 'blur', 'placeholder_url' => '', 'blur' => true],
'systemNotice' => class_exists('AKSRT_Settings')
? AKSRT_Settings::get_system_notice_config()
: ['enabled' => false],
'contentProtect' => [
'enabled' => class_exists('AKSRT_Settings') && (bool) AKSRT_Settings::get_value('content_protect_enabled', false),
'copyNotice' => class_exists('AKSRT_Settings') && (bool) AKSRT_Settings::get_value('content_protect_copy_notice', false),
'copyNoticeText' => class_exists('AKSRT_Settings') ? (string) AKSRT_Settings::get_value('content_protect_copy_text', '来自本站,请尊重原创版权。') : '',
'disableRightClick'=> class_exists('AKSRT_Settings') && (bool) AKSRT_Settings::get_value('content_protect_right_click', false),
'detectDevTools' => class_exists('AKSRT_Settings') && (bool) AKSRT_Settings::get_value('content_protect_f12', false),
],
'translate' => class_exists('AKSRT_Settings')
? AKSRT_Settings::get_i18n_config()
: ['enabled' => false, 'autoDetect' => true, 'defaultLang' => 'zh-CN'],
]);
$this->enqueue_public_fonts_js_and_data($base_uri, $core_dep, $deps, $is_fixed_homepage, $is_home_or_archive, $is_single_post, $is_single_resource, $path, $version);
}
}
@@ -0,0 +1,591 @@
<?php
defined('ABSPATH') || exit;
/**
* Scripts public enqueue continuation: fonts, icons, global JS, localize.
*
* @package AKSRT
*/
trait AKSRT_Scripts_Part_1c {
/**
* Fonts / FA / global JS stack / aksrtData (split from enqueue_public).
*/
private function enqueue_public_fonts_js_and_data($base_uri, $core_dep, $deps, $is_fixed_homepage, $is_home_or_archive, $is_single_post, $is_single_resource, $path, $version): void {
// ===== 5. 字体 =====
// 5a. 拉丁字体 - Inter (UI) + Source Serif 4 (正文)
wp_enqueue_style(
'aksrt-fonts',
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,500;0,8..60,600;0,8..60,700;1,8..60,400&display=swap',
[],
null
);
// 5b. 中文字体 - jsDelivr CDN (思源黑体 / 思源宋体)
// 仅 regular 字重,避免拖慢首屏;其它字重通过 system fallback (PingFang/微软雅黑)
// Fontsource v5 已按 unicode-range 自动分包,浏览器只会下载用到的简中子集
wp_enqueue_style(
'aksrt-fonts-cn',
'https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-sc@5.2.9/400.css',
[],
null
);
wp_enqueue_style(
'aksrt-fonts-cn-serif',
'https://cdn.jsdelivr.net/npm/@fontsource/noto-serif-sc@5.2.9/400.css',
[],
null
);
// ===== 6. 图标系统 =====
// Font Awesome 5 全站加载(评论回复、单篇互动栏、profile 联系方式等
// 多处模板使用 fa-* 类名,作为全主题图标包统一使用)
wp_enqueue_style(
'aksrt-font-awesome-5',
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css',
[],
'5.15.4'
);
// ===== 7. JS =====
// Swiper.js(首页/归档/CMS 模块/单篇文章页加载)
$needs_swiper = $is_home_or_archive || $is_single_post || $is_single_resource;
if ($needs_swiper) {
wp_enqueue_style(
'aksrt-swiper',
$base_uri . '/assets/css/frontend/swiper-bundle.min.css',
[],
'11.0'
);
wp_enqueue_script(
'aksrt-swiper',
$base_uri . '/assets/js/swiper-bundle.min.js',
[],
'11.0',
true
);
}
wp_enqueue_script(
'aksrt-qrcode',
'https://cdn.jsdelivr.net/npm/kjua@0.9.0/dist/kjua.min.js',
[],
'0.9.0',
true
);
// Rehydrate system — must load before aksrt-script so AksrtFrontend is available
wp_enqueue_script(
'aksrt-rehydrate',
$base_uri . '/assets/js/rehydrate.js',
[],
file_exists(get_template_directory() . '/assets/js/rehydrate.js')
? (string) filemtime(get_template_directory() . '/assets/js/rehydrate.js')
: $version,
true
);
// Runtime infrastructure: tbquire / notyf / confirm / refreshModal / scrollTopTo / [data-*] declarative
$runtime_js = get_template_directory() . '/assets/js/aksrt-runtime.js';
if (file_exists($runtime_js)) {
wp_enqueue_script(
'aksrt-runtime',
$base_uri . '/assets/js/aksrt-runtime.js',
['aksrt-rehydrate'],
(string) filemtime($runtime_js),
true
);
}
// Core modules (split from main.js) — register with AksrtFrontend
$core_modules = [
'aksrt-core-theme' => 'theme.js',
'aksrt-core-nav' => 'nav.js',
'aksrt-core-search' => 'search.js',
'aksrt-core-dropdown' => 'dropdown.js',
'aksrt-core-scroll' => 'scroll.js',
'aksrt-core-slider' => 'slider.js',
'aksrt-core-article' => 'article.js',
'aksrt-core-comments' => 'comments.js',
'aksrt-core-pagination' => 'pagination.js',
'aksrt-core-archive' => 'archive.js',
'aksrt-core-footer-tabbar' => 'footer-tabbar.js',
'aksrt-core-misc' => 'misc.js',
];
$core_handles = [];
foreach ($core_modules as $handle => $file) {
$path = get_template_directory() . '/assets/js/core/' . $file;
if (file_exists($path)) {
// Depend on aksrt-runtime so window.aksrtData (localized below) is available.
$deps = ['aksrt-rehydrate', 'aksrt-runtime'];
if ($handle === 'aksrt-core-slider') {
$deps = array_merge($deps, $needs_swiper ? ['aksrt-swiper'] : []);
}
wp_enqueue_script(
$handle,
$base_uri . '/assets/js/core/' . $file,
$deps,
(string) filemtime($path),
true
);
$core_handles[] = $handle;
}
}
wp_enqueue_script(
'aksrt-script',
$base_uri . '/assets/js/main.js',
array_merge(
['jquery', 'bootstrap5', 'aksrt-rehydrate', 'aksrt-runtime'],
$core_handles
),
file_exists(get_template_directory() . '/assets/js/main.js')
? (string) filemtime(get_template_directory() . '/assets/js/main.js')
: $version,
true
);
// SVG icon data + helper (must load before other scripts that use icons)
$svg_icon_js = get_template_directory() . '/assets/js/svg-icon.js';
if (file_exists($svg_icon_js)) {
wp_enqueue_script(
'aksrt-svg-icon',
$base_uri . '/assets/js/svg-icon.js',
[],
(string) filemtime($svg_icon_js),
false // load in <head> so icons are available early
);
}
$svg_helper_js = get_template_directory() . '/assets/js/svg-icon-helper.js';
if (file_exists($svg_helper_js)) {
wp_enqueue_script(
'aksrt-svg-icon-helper',
$base_uri . '/assets/js/svg-icon-helper.js',
['aksrt-svg-icon'],
(string) filemtime($svg_helper_js),
true
);
}
// Page loading indicator (top progress bar)
$loader_js = get_template_directory() . '/assets/js/loader.js';
if (file_exists($loader_js)) {
wp_enqueue_script(
'aksrt-loader',
$base_uri . '/assets/js/loader.js',
[],
(string) filemtime($loader_js),
true
);
}
// Code block enhancements (copy button + language label)
$precode_js = get_template_directory() . '/assets/js/precode.js';
if (file_exists($precode_js)) {
wp_enqueue_script(
'aksrt-precode',
$base_uri . '/assets/js/precode.js',
[],
(string) filemtime($precode_js),
true
);
}
// Input field enhancements (auto-resize, counter, validation)
$input_expand_js = get_template_directory() . '/assets/js/input-expand.js';
if (file_exists($input_expand_js)) {
wp_enqueue_script(
'aksrt-input-expand',
$base_uri . '/assets/js/input-expand.js',
[],
(string) filemtime($input_expand_js),
true
);
}
// Instant page prefetch (hover to preload)
$instant_page_js = get_template_directory() . '/assets/js/instant-page.js';
if (file_exists($instant_page_js)) {
wp_enqueue_script(
'aksrt-instant-page',
$base_uri . '/assets/js/instant-page.js',
[],
(string) filemtime($instant_page_js),
false // load in <head> for early prefetch capability
);
}
// Touch gesture support (swipe, pull-to-dismiss)
$mini_touch_js = get_template_directory() . '/assets/js/mini-touch.js';
if (file_exists($mini_touch_js)) {
wp_enqueue_script(
'aksrt-mini-touch',
$base_uri . '/assets/js/mini-touch.js',
[],
(string) filemtime($mini_touch_js),
true
);
}
// Section navs: article TOC / scrollspy
$section_navs_js = get_template_directory() . '/assets/js/section-navs.js';
if (file_exists($section_navs_js)) {
wp_enqueue_script(
'aksrt-section-navs',
$base_uri . '/assets/js/section-navs.js',
['aksrt-rehydrate', 'aksrt-runtime'],
(string) filemtime($section_navs_js),
true
);
}
// Video player (MP4/HLS/FLV)
$video_player_js = get_template_directory() . '/assets/js/video-player.js';
if (file_exists($video_player_js)) {
wp_enqueue_script(
'aksrt-video-player',
$base_uri . '/assets/js/video-player.js',
[],
(string) filemtime($video_player_js),
true
);
}
// Image color extraction
$color_extract_js = get_template_directory() . '/assets/js/color-extract.js';
if (file_exists($color_extract_js)) {
wp_enqueue_script(
'aksrt-color-extract',
$base_uri . '/assets/js/color-extract.js',
[],
(string) filemtime($color_extract_js),
true
);
}
$widget_set_js = get_template_directory() . '/assets/js/widget-set.js';
if (file_exists($widget_set_js)) {
wp_enqueue_script(
'aksrt-widget-set',
$base_uri . '/assets/js/widget-set.js',
['aksrt-script', 'aksrt-qrcode'],
(string) filemtime($widget_set_js),
true
);
}
// ===== 全局 UI 增强(Toast、骨架屏、悬停动效、灯箱、滚动感知)=====
$enhancements_css = get_template_directory() . '/assets/css/frontend/enhancements.css';
if (file_exists($enhancements_css)) {
wp_enqueue_style(
'aksrt-enhancements',
$base_uri . '/assets/css/frontend/enhancements.css',
[$core_dep],
(string) filemtime($enhancements_css)
);
}
$enhancements_js = get_template_directory() . '/assets/js/enhancements.js';
if (file_exists($enhancements_js)) {
wp_enqueue_script(
'aksrt-enhancements',
$base_uri . '/assets/js/enhancements.js',
['aksrt-script'],
(string) filemtime($enhancements_js),
true
);
}
$enhancements2_js = get_template_directory() . '/assets/js/enhancements2.js';
if (file_exists($enhancements2_js)) {
wp_enqueue_script(
'aksrt-enhancements2',
$base_uri . '/assets/js/enhancements2.js',
['aksrt-script'],
(string) filemtime($enhancements2_js),
true
);
}
// Reward (打赏)
$reward_js = get_template_directory() . '/assets/js/reward.js';
if (file_exists($reward_js)) {
wp_enqueue_script(
'aksrt-reward',
$base_uri . '/assets/js/reward.js',
['aksrt-script'],
(string) filemtime($reward_js),
true
);
}
// Effects (count-up, countdown, marquee, tilt, FPS detection)
$effects_js = get_template_directory() . '/assets/js/effects.js';
if (file_exists($effects_js)) {
wp_enqueue_script(
'aksrt-effects',
$base_uri . '/assets/js/effects.js',
['aksrt-script'],
(string) filemtime($effects_js),
true
);
}
// Yiyan (一言轮播)
$yiyan_js = get_template_directory() . '/assets/js/yiyan.js';
if (file_exists($yiyan_js)) {
wp_enqueue_script(
'aksrt-yiyan',
$base_uri . '/assets/js/yiyan.js',
[],
(string) filemtime($yiyan_js),
true
);
}
// Frontend translate (默认关)
$translate_js = get_template_directory() . '/assets/js/frontend-translate.js';
if (file_exists($translate_js)) {
wp_enqueue_script(
'aksrt-frontend-translate',
$base_uri . '/assets/js/frontend-translate.js',
[],
(string) filemtime($translate_js),
true
);
}
// Content protection (copy notice / right-click / F12)
$content_protect_js = get_template_directory() . '/assets/js/content-protect.js';
if (file_exists($content_protect_js)) {
wp_enqueue_script(
'aksrt-content-protect',
$base_uri . '/assets/js/content-protect.js',
[],
(string) filemtime($content_protect_js),
false // load in <head> for early binding
);
}
// Site notice popup (global, controlled by Settings)
$site_notice_css = get_template_directory() . '/assets/css/frontend/site-notice.css';
if (file_exists($site_notice_css)) {
wp_enqueue_style(
'aksrt-site-notice',
$base_uri . '/assets/css/frontend/site-notice.css',
[$core_dep],
(string) filemtime($site_notice_css)
);
}
$site_notice_js = get_template_directory() . '/assets/js/site-notice.js';
if (file_exists($site_notice_js)) {
wp_enqueue_script(
'aksrt-site-notice',
$base_uri . '/assets/js/site-notice.js',
[],
(string) filemtime($site_notice_js),
true
);
}
// 弹窗登录/注册(仅未登录用户)
if (!is_user_logged_in()) {
$modal_auth_js = get_template_directory() . '/assets/js/modal-auth.js';
if (file_exists($modal_auth_js)) {
wp_enqueue_script(
'aksrt-modal-auth',
$base_uri . '/assets/js/modal-auth.js',
['aksrt-script'],
(string) filemtime($modal_auth_js),
true
);
}
}
if ($is_fixed_homepage) {
$homepage_js_path = get_template_directory() . '/assets/js/homepage-fixed.js';
wp_enqueue_script(
'aksrt-homepage-fixed',
$base_uri . '/assets/js/homepage-fixed.js',
['aksrt-script'],
file_exists($homepage_js_path) ? (string) filemtime($homepage_js_path) : $version,
true
);
}
// Comment toolbar (emoji, code, bold, Ctrl+Enter) — only when the form is open.
if (is_singular() && comments_open()) {
$comment_tools_path = get_template_directory() . '/assets/js/comment-tools.js';
wp_enqueue_script(
'aksrt-comment-tools',
$base_uri . '/assets/js/comment-tools.js',
['aksrt-script'],
file_exists($comment_tools_path) ? (string) filemtime($comment_tools_path) : $version,
true
);
}
// Comment actions client (like / edit / delete / moderate / pin) — load
// wherever comments can exist, including closed-comment posts. Submission
// itself is handled by main.js initCommentSystem().
if (is_singular() && (comments_open() || (int) get_comments_number() > 0)) {
$comment_ajax_path = get_template_directory() . '/assets/js/comment-ajax.js';
if (file_exists($comment_ajax_path)) {
wp_enqueue_script(
'aksrt-comment-ajax',
$base_uri . '/assets/js/comment-ajax.js',
['aksrt-script'],
(string) filemtime($comment_ajax_path),
true
);
}
}
if (is_singular() || is_page_template('templates/pages/page-forums.php')) {
$page_template_path = get_template_directory() . '/assets/js/page-template.js';
if (file_exists($page_template_path)) {
wp_enqueue_script(
'aksrt-page-template',
$base_uri . '/assets/js/page-template.js',
['aksrt-script'],
(string) filemtime($page_template_path),
true
);
}
}
// Post/resource share functionality
if ($is_single_post || $is_single_resource) {
$weixin_share_path = get_template_directory() . '/assets/js/weixin-share.js';
if (file_exists($weixin_share_path)) {
wp_enqueue_script(
'aksrt-weixin-share',
$base_uri . '/assets/js/weixin-share.js',
['aksrt-script'],
(string) filemtime($weixin_share_path),
true
);
}
$post_share_path = get_template_directory() . '/assets/js/post-share.js';
if (file_exists($post_share_path)) {
wp_enqueue_script(
'aksrt-post-share',
$base_uri . '/assets/js/post-share.js',
['aksrt-script', 'aksrt-weixin-share', 'aksrt-share-data'],
(string) filemtime($post_share_path),
true
);
}
}
// Page navigation (TOC highlight, smooth scroll, doc search)
// 评论回复脚本
if (is_singular() && comments_open() && get_option('thread_comments')) {
wp_enqueue_script('comment-reply');
}
$runtime_routes = $this->resolve_frontend_routes();
// 数据注入 - 全站统一消费 window.aksrtData
// Attach to aksrt-runtime (loaded before all core modules) so ajaxUrl/nonce
// exist before pagination/search/article handlers run.
$aksrt_data_handle = wp_script_is('aksrt-runtime', 'registered') || wp_script_is('aksrt-runtime', 'enqueued')
? 'aksrt-runtime'
: 'aksrt-script';
wp_localize_script($aksrt_data_handle, 'aksrtData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('aksrt_public_nonce'),
'restNonce' => wp_create_nonce('wp_rest'),
'visibilityNonce' => wp_create_nonce('aksrt_visibility'),
'siteUrl' => home_url('/'),
'themeUrl' => AKSRT_URI,
'themeMode' => class_exists('AKSRT_Settings') ? AKSRT_Settings::theme_mode_js_value() : 'auto',
'debug' => defined('WP_DEBUG') && WP_DEBUG,
'debugLog' => defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG,
'postPaginationMode' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_pagination_mode() : 'load_more',
'commentPaginationMode' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_comment_pagination_mode() : 'numbered',
'homepagePostsPerPage' => function_exists('aksrt_homepage_get_tab_posts_per_page') ? aksrt_homepage_get_tab_posts_per_page() : (int) get_option('posts_per_page'),
'homepageDefaultSort' => class_exists('AKSRT_Settings') ? AKSRT_Settings::homepage_default_sort() : 'modified',
'homepageDefaultView' => class_exists('AKSRT_Settings') ? AKSRT_Settings::homepage_default_view() : 'list',
'homepageSortAjax' => class_exists('AKSRT_Settings') ? AKSRT_Settings::homepage_sort_ajax_enabled() : true,
'homepageStickyTabs' => class_exists('AKSRT_Settings') ? AKSRT_Settings::homepage_sticky_tabs_enabled() : true,
'homePostsListEnabled' => !class_exists('AKSRT_Settings') || AKSRT_Settings::home_posts_list_enabled(),
'homeListSortEnabled' => !class_exists('AKSRT_Settings') || AKSRT_Settings::home_list_sort_enabled(),
'homeListSortDropdown' => class_exists('AKSRT_Settings') && AKSRT_Settings::home_list_sort_dropdown(),
// AJAX 翻页文案
'ajaxTrigger' => (class_exists('AKSRT_Settings') && ($t = AKSRT_Settings::get_post_list_style_settings()['ajax_trigger_text'] ?? '') !== '') ? $t : __('加载更多', 'aksrt'),
'ajaxNomore' => (class_exists('AKSRT_Settings') && ($n = AKSRT_Settings::get_post_list_style_settings()['ajax_nomore_text'] ?? '') !== '') ? $n : __('没有更多了', 'aksrt'),
'ajaxLoading' => __('正在加载...', 'aksrt'),
'iasMax' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_list_style_settings()['ias_max'] : 3,
'commentIasAuto' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_comment_display_settings()['paging_ajax_ias'] : true,
'commentUploadEnabled' => class_exists('AKSRT_Settings') ? !empty((AKSRT_Settings::get_comment_display_settings()['toolbar']['upload_img'] ?? false)) : false,
'commentUploadUrl' => esc_url_raw(rest_url('aksrt/v1/comment/upload-image')),
'commentUploadMaxSize' => 2 * 1024 * 1024,
'commentIasMax' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_comment_display_settings()['paging_ajax_ias_max'] : 3,
'stickyEnabled' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_list_style_settings()['sticky_enabled'] : true,
'stickyPages' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_list_style_settings()['sticky_pages'] : ['home', 'cat'],
'badgeShow' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_list_style_settings()['badge_show'] : true,
'badgeTypes' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_post_list_style_settings()['badge_types'] : ['pay', 'tag', 'topics', 'cat'],
'search' => [
'mode' => class_exists('AKSRT_Settings') ? AKSRT_Settings::search_mode() : 'default',
'placeholder' => function_exists('aksrt_get_search_placeholder') ? aksrt_get_search_placeholder() : __('开启精彩搜索...', 'aksrt'),
'historyEnabled' => function_exists('aksrt_search_history_enabled') ? aksrt_search_history_enabled() : true,
'suggestionsEnabled' => function_exists('aksrt_search_suggestions_enabled') ? aksrt_search_suggestions_enabled() : false,
'hotKeywords' => function_exists('aksrt_get_search_hot_keywords') ? aksrt_get_search_hot_keywords() : [],
'popularTitle' => class_exists('AKSRT_Settings') ? AKSRT_Settings::search_popular_title() : __('热门搜索', 'aksrt'),
'contentTypes' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_search_content_types()['types'] : ['post', 'category', 'tag'],
'defaultType' => class_exists('AKSRT_Settings') ? AKSRT_Settings::get_search_content_types()['default'] : 'post',
],
// 用户状态
'isLoggedIn' => is_user_logged_in(),
'userId' => get_current_user_id(),
'cartUrl' => $runtime_routes['cart'],
'ordersUrl' => $runtime_routes['orders'],
'userCenterUrl' => function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/'),
'routes' => [
'home' => home_url('/'),
'login' => function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(),
'register' => function_exists('aksrt_get_register_url') ? aksrt_get_register_url() : wp_registration_url(),
'resetPassword' => function_exists('aksrt_get_reset_password_url') ? aksrt_get_reset_password_url() : wp_lostpassword_url(),
'profile' => function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/profile/'),
'cart' => $runtime_routes['cart'],
'orders' => $runtime_routes['orders'],
'resources' => $runtime_routes['resources'],
],
'shell' => [
'wrapperId' => '',
'navId' => 'site-nav',
'searchPanelId' => 'nav-search-panel',
'drawerId' => 'nav-drawer',
'userMenuId' => 'user-dropdown',
'floatActionsId' => 'float-right-btns',
'tabbarId' => 'footer-tabbar',
],
'themeUri' => get_template_directory_uri(),
'codeHighlight' => class_exists('AKSRT_Settings')
? AKSRT_Settings::get_code_highlight_config()
: ['enabled' => true, 'lightTheme' => 'enlighter', 'darkTheme' => 'monokai', 'lineNumbers' => false, 'showButtons' => true, 'maxHeight' => 500],
'lazyload' => class_exists('AKSRT_Settings')
? AKSRT_Settings::get_lazyload_config()
: ['enabled' => true, 'animation' => 'blur', 'placeholder_url' => '', 'blur' => true],
'systemNotice' => class_exists('AKSRT_Settings')
? AKSRT_Settings::get_system_notice_config()
: ['enabled' => false],
'contentProtect' => [
'enabled' => class_exists('AKSRT_Settings') && (bool) AKSRT_Settings::get_value('content_protect_enabled', false),
'copyNotice' => class_exists('AKSRT_Settings') && (bool) AKSRT_Settings::get_value('content_protect_copy_notice', false),
'copyNoticeText' => class_exists('AKSRT_Settings') ? (string) AKSRT_Settings::get_value('content_protect_copy_text', '来自本站,请尊重原创版权。') : '',
'disableRightClick'=> class_exists('AKSRT_Settings') && (bool) AKSRT_Settings::get_value('content_protect_right_click', false),
'detectDevTools' => class_exists('AKSRT_Settings') && (bool) AKSRT_Settings::get_value('content_protect_f12', false),
],
'translate' => class_exists('AKSRT_Settings')
? AKSRT_Settings::get_i18n_config()
: ['enabled' => false, 'autoDetect' => true, 'defaultLang' => 'zh-CN'],
]);
}
}
@@ -0,0 +1,354 @@
<?php
/**
* Extracted trait: AKSRT_Balance_Service_Charge
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Balance_Service_Charge {
/**
* When a recharge order is paid, credit the wallet.
*
* @param string $order_number Order number.
* @param array<string,mixed> $order Order row.
*/
public function on_order_paid(string $order_number, array $order = []): void {
if ($order === [] && class_exists('AKSRT_Order_Repository')) {
$order = AKSRT_Order_Repository::get_instance()->get($order_number) ?: [];
}
if (!$order) {
return;
}
$type = sanitize_key((string) ($order['product_type'] ?? ''));
$user_id = (int) ($order['user_id'] ?? 0);
if ($user_id <= 0) {
return;
}
// Points purchase fulfill (zibll order_type=9).
if ($type === AKSRT_Domain_Model::ORDER_PRODUCT_POINTS_BUY) {
if (!class_exists('AKSRT_Credits')) {
return;
}
$note = json_decode((string) ($order['note'] ?? ''), true);
if (!is_array($note)) {
$note = [];
}
if (!empty($note['points_credited'])) {
return;
}
$rate = 100;
if (class_exists('AKSRT_Settings')) {
$rate = max(1, (int) (AKSRT_Settings::get_all()['points_rate'] ?? 100));
}
$points = isset($note['points_amount'])
? (int) $note['points_amount']
: (int) round((float) ($order['amount'] ?? 0) * $rate);
if ($points <= 0) {
return;
}
$add = AKSRT_Credits::add(
$user_id,
$points,
'points_buy',
['type' => 'order', 'order' => $order_number],
sprintf(__('购买积分订单 %s', 'aksrt'), $order_number)
);
if (is_wp_error($add)) {
return;
}
$note['points_credited'] = 1;
$note['credited_points'] = $points;
global $wpdb;
$wpdb->update(
$wpdb->prefix . 'aksrt_order',
['note' => wp_json_encode($note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)],
['order_number' => $order_number],
['%s'],
['%s']
);
return;
}
if ($type !== AKSRT_Domain_Model::ORDER_PRODUCT_RECHARGE) {
return;
}
if (!class_exists('AKSRT_Balance')) {
return;
}
$note = json_decode((string) ($order['note'] ?? ''), true);
if (!is_array($note)) {
$note = [];
}
if (!empty($note['balance_credited'])) {
return;
}
$credit_amount = isset($note['credit_amount'])
? round((float) $note['credit_amount'], 2)
: round((float) ($order['amount'] ?? 0), 2);
if ($credit_amount <= 0) {
return;
}
$result = AKSRT_Balance::add(
$user_id,
$credit_amount,
'recharge',
['type' => 'order', 'order' => $order_number],
sprintf(__('余额充值订单 %s', 'aksrt'), $order_number)
);
if (is_wp_error($result)) {
return;
}
$note['balance_credited'] = 1;
$note['credited_amount'] = $credit_amount;
global $wpdb;
$wpdb->update(
$wpdb->prefix . 'aksrt_order',
['note' => wp_json_encode($note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)],
['order_number' => $order_number],
['%s'],
['%s']
);
}
/**
* @return array<string,mixed>
*/
public function get_charge_view(int $user_id): array {
$cfg = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_payment_recharge_settings() : [];
$packages = $this->parse_packages((string) ($cfg['packages_raw'] ?? ''));
$custom_s = !isset($cfg['product']['custom_s']) || !empty($cfg['product']['custom_s']);
$custom_limit = max(1, (int) ($cfg['product']['custom_limit'] ?? 9999));
$methods = [];
if (function_exists('aksrt_list_cash_payment_methods')) {
// Exclude balance gateway for recharge (cannot pay wallet with wallet).
foreach (aksrt_list_cash_payment_methods(true) as $m) {
$id = is_array($m) ? (string) ($m['id'] ?? '') : (string) $m;
if ($id === '' || $id === 'balance') {
continue;
}
$methods[] = is_array($m) ? $m : ['id' => $id, 'name' => $id];
}
}
return [
'enabled' => !empty($cfg['enabled']),
'desc' => (string) ($cfg['desc'] ?? ''),
'wallet' => class_exists('AKSRT_Balance') ? AKSRT_Balance::balance($user_id) : 0.0,
'packages' => $packages,
'custom_s' => $custom_s,
'custom_limit' => $custom_limit,
'currency_mark' => (string) ($cfg['product']['tag'] ?? '元'),
'payment_methods' => $methods,
'transfer' => [
'enabled' => !empty($cfg['transfer']['enabled']),
'service_charge' => (int) ($cfg['transfer']['service_charge'] ?? 0),
'desc' => (string) ($cfg['transfer']['desc'] ?? ''),
],
'exchange' => [
'enabled' => !empty($cfg['exchange']['enabled']),
'rate' => (int) ($cfg['exchange']['rate'] ?? 100),
'desc' => (string) ($cfg['exchange']['desc'] ?? ''),
],
'pass_charge' => is_array($cfg['pass_charge'] ?? null) ? $cfg['pass_charge'] : [],
];
}
/**
* @return array<int,array{key:string,price:float,pay_price:float,title:string,tag:string}>
*/
public function parse_packages(string $raw): array {
$out = [];
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
$i = 0;
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '') {
continue;
}
// pay|credit|title OR credit|pay|title — Zibll uses price(credit)|pay_price|tag
// Admin help: 支付金额|到账金额|标题
$parts = array_map('trim', explode('|', $line));
if (count($parts) < 2) {
continue;
}
$pay = (float) $parts[0];
$credit = (float) $parts[1];
$title = $parts[2] ?? '';
if ($pay <= 0 || $credit <= 0) {
continue;
}
$out[] = [
'key' => (string) $i,
'pay_price' => round($pay, 2),
'price' => round($credit, 2),
'title' => $title !== '' ? $title : sprintf(__('充值 %s 元', 'aksrt'), (string) $credit),
'tag' => $title,
];
$i++;
}
return $out;
}
/**
* @return array<string,mixed>|WP_Error
*/
public function create_charge_order(int $user_id, string $package_key, float $custom_price, string $payment_method) {
if ($user_id <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录', 'aksrt'), ['status' => 401]);
}
if (!class_exists('AKSRT_Order_Repository') || !class_exists('AKSRT_Order')) {
return new WP_Error('aksrt_pay_unavailable', __('订单系统不可用', 'aksrt'), ['status' => 500]);
}
$view = $this->get_charge_view($user_id);
$credit_amount = 0.0;
$pay_amount = 0.0;
$title = __('余额充值', 'aksrt');
if ($package_key === 'custom' || $package_key === '') {
if (empty($view['custom_s'])) {
return new WP_Error('aksrt_custom_closed', __('未开放自定义金额', 'aksrt'), ['status' => 400]);
}
$custom_price = round($custom_price, 2);
$min = 1.0;
$max = (float) $view['custom_limit'];
if ($custom_price < $min || $custom_price > $max) {
return new WP_Error('aksrt_custom_limit', sprintf(__('充值金额需在 %s ~ %s 之间', 'aksrt'), (string) $min, (string) $max), ['status' => 400]);
}
$pay_amount = $custom_price;
$credit_amount = $custom_price;
$title = sprintf(__('自定义充值 %s 元', 'aksrt'), (string) $custom_price);
$package_key = 'custom';
} else {
$found = null;
foreach ((array) $view['packages'] as $pkg) {
if ((string) ($pkg['key'] ?? '') === (string) $package_key) {
$found = $pkg;
break;
}
}
if (!$found) {
return new WP_Error('aksrt_invalid_package', __('充值套餐无效', 'aksrt'), ['status' => 400]);
}
$pay_amount = (float) $found['pay_price'];
$credit_amount = (float) $found['price'];
$title = (string) $found['title'];
}
if ($pay_amount <= 0 || $credit_amount <= 0) {
return new WP_Error('aksrt_invalid_price', __('金额无效', 'aksrt'), ['status' => 400]);
}
$payment_method = sanitize_key($payment_method);
if ($payment_method === '' || $payment_method === 'balance') {
// Prefer first non-balance method.
$methods = $view['payment_methods'] ?? [];
$payment_method = !empty($methods[0]['id']) ? (string) $methods[0]['id'] : 'alipay';
}
$note = wp_json_encode([
'balance_recharge' => 1,
'package' => $package_key,
'credit_amount' => $credit_amount,
'pay_amount' => $pay_amount,
'title' => $title,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = AKSRT_Order_Repository::get_instance()->create([
'user_id' => $user_id,
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RECHARGE,
'product_id' => 0,
'amount' => $pay_amount,
'currency' => 'CNY',
'status' => AKSRT_Order::STATUS_PENDING,
'payment_method' => $payment_method,
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$order = AKSRT_Order_Repository::get_instance()->get($order_number);
if (!$order) {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$order['subject'] = $title;
$order['payment_method'] = $payment_method;
if (!class_exists('AKSRT_Payment_Gateway')) {
return new WP_Error('aksrt_pay_unavailable', __('支付网关不可用', 'aksrt'), ['status' => 500]);
}
$gateway = AKSRT_Payment_Gateway::get_instance()->get_gateway($payment_method);
if (!$gateway) {
return new WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用', 'aksrt'), [
'status' => 400,
'order_number' => $order_number,
]);
}
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
return new WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [
'status' => 400,
'order_number' => $order_number,
]);
}
$payment_result = $gateway->create_payment($order);
if (empty($payment_result['success'])) {
return new WP_Error(
'aksrt_pay_failed',
(string) ($payment_result['message'] ?? __('支付失败', 'aksrt')),
['status' => 402, 'order_number' => $order_number]
);
}
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
$payment_url = (string) ($payment_result['payment_url'] ?? '');
if ($payment_url !== '' || $code_url !== '') {
return [
'redirect' => true,
'payment_url' => $payment_url,
'qr_content' => $code_url,
'order_number' => $order_number,
'amount' => $pay_amount,
'credit_amount' => $credit_amount,
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
'reload' => false,
'poll' => true,
];
}
// Synchronous gateway (rare for recharge): mark paid → on_order_paid credits wallet.
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => $payment_method,
'payment_transaction_id' => sanitize_text_field((string) ($payment_result['transaction_id'] ?? '')),
'note' => __('余额充值支付完成', 'aksrt'),
]);
if (!$paid) {
return new WP_Error('aksrt_order_confirm_failed', __('支付确认失败', 'aksrt'), ['status' => 500]);
}
// Ensure credit even if status hook shape differs.
$this->on_order_paid($order_number, AKSRT_Order_Repository::get_instance()->get($order_number) ?: $order);
return [
'redirect' => false,
'order_number' => $order_number,
'amount' => $pay_amount,
'credit_amount' => $credit_amount,
'wallet' => AKSRT_Balance::balance($user_id),
'message' => __('充值成功', 'aksrt'),
'reload' => true,
];
}
}
@@ -0,0 +1,244 @@
<?php
/**
* Extracted trait: AKSRT_Balance_Service_Points
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Balance_Service_Points {
public function points_buy_enabled(): bool {
if (!class_exists('AKSRT_Settings') || !AKSRT_Settings::credits_enabled()) {
return false;
}
$all = AKSRT_Settings::get_all();
return !empty($all['points_pay_s']);
}
/**
* @return array<string,mixed>
*/
public function get_points_buy_view(int $user_id): array {
$all = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_all() : [];
$rate = max(1, (int) ($all['points_rate'] ?? 100));
$packages_raw = (string) ($all['pay_points_product'] ?? '');
$packages = $this->parse_points_packages($packages_raw);
$custom_s = !isset($all['pay_points_product_custom_s']) || !empty($all['pay_points_product_custom_s']);
$custom_limit = max(1, (int) ($all['pay_points_product_custom_limit'] ?? 99999));
$methods = [];
if (function_exists('aksrt_list_cash_payment_methods')) {
foreach (aksrt_list_cash_payment_methods(true) as $m) {
$id = is_array($m) ? (string) ($m['id'] ?? '') : (string) $m;
if ($id === '') {
continue;
}
if ($id === 'balance' && !(class_exists('AKSRT_Balance') && AKSRT_Balance::enabled())) {
continue;
}
$methods[] = is_array($m) ? $m : ['id' => $id, 'name' => $id];
}
}
if (class_exists('AKSRT_Balance') && AKSRT_Balance::enabled()) {
$has_bal = false;
foreach ($methods as $m) {
if (($m['id'] ?? '') === 'balance') {
$has_bal = true;
break;
}
}
if (!$has_bal) {
array_unshift($methods, ['id' => 'balance', 'name' => __('站内余额', 'aksrt')]);
}
}
return [
'enabled' => $this->points_buy_enabled(),
'desc' => (string) ($all['pay_points_desc'] ?? ''),
'rate' => $rate,
'points' => class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($user_id) : 0,
'packages' => $packages,
'custom_s' => $custom_s,
'custom_limit' => $custom_limit,
'payment_methods' => $methods,
];
}
/**
* @return array<int,array{key:string,points:int,pay_price:float,title:string}>
*/
public function parse_points_packages(string $raw): array {
$out = [];
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
$i = 0;
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '') {
continue;
}
// 支付金额|积分数量|标题
$parts = array_map('trim', explode('|', $line));
if (count($parts) < 2) {
continue;
}
$pay = (float) $parts[0];
$points = (int) $parts[1];
$title = $parts[2] ?? '';
if ($pay <= 0 || $points <= 0) {
continue;
}
$out[] = [
'key' => (string) $i,
'pay_price' => round($pay, 2),
'points' => $points,
'title' => $title !== '' ? $title : sprintf(__('%d 积分', 'aksrt'), $points),
];
$i++;
}
return $out;
}
/**
* @return array<string,mixed>|WP_Error
*/
public function create_points_buy_order(int $user_id, string $package_key, int $custom_points, string $payment_method) {
if ($user_id <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录', 'aksrt'), ['status' => 401]);
}
if (!class_exists('AKSRT_Order_Repository') || !class_exists('AKSRT_Order')) {
return new WP_Error('aksrt_pay_unavailable', __('订单系统不可用', 'aksrt'), ['status' => 500]);
}
$view = $this->get_points_buy_view($user_id);
$rate = max(1, (int) ($view['rate'] ?? 100));
$points_amount = 0;
$pay_amount = 0.0;
$title = __('购买积分', 'aksrt');
if ($package_key === 'custom' || $package_key === '') {
if (empty($view['custom_s'])) {
return new WP_Error('aksrt_custom_closed', __('未开放自定义数量', 'aksrt'), ['status' => 400]);
}
$custom_points = max(0, $custom_points);
$max = (int) ($view['custom_limit'] ?? 99999);
if ($custom_points < $rate || $custom_points > $max) {
return new WP_Error('aksrt_custom_limit', sprintf(__('购买积分需在 %d ~ %d 之间', 'aksrt'), $rate, $max), ['status' => 400]);
}
$points_amount = $custom_points;
$pay_amount = round($custom_points / $rate, 2);
if ($pay_amount <= 0) {
$pay_amount = 0.01;
}
$title = sprintf(__('购买 %d 积分', 'aksrt'), $points_amount);
$package_key = 'custom';
} else {
$found = null;
foreach ((array) $view['packages'] as $pkg) {
if ((string) ($pkg['key'] ?? '') === (string) $package_key) {
$found = $pkg;
break;
}
}
if (!$found) {
return new WP_Error('aksrt_invalid_package', __('积分套餐无效', 'aksrt'), ['status' => 400]);
}
$points_amount = (int) $found['points'];
$pay_amount = (float) $found['pay_price'];
$title = (string) $found['title'];
}
if ($points_amount <= 0 || $pay_amount <= 0) {
return new WP_Error('aksrt_invalid_price', __('金额无效', 'aksrt'), ['status' => 400]);
}
$payment_method = sanitize_key($payment_method);
if ($payment_method === '') {
$methods = $view['payment_methods'] ?? [];
$payment_method = !empty($methods[0]['id']) ? (string) $methods[0]['id'] : 'alipay';
}
$note = wp_json_encode([
'points_buy' => 1,
'package' => $package_key,
'points_amount' => $points_amount,
'pay_amount' => $pay_amount,
'title' => $title,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = AKSRT_Order_Repository::get_instance()->create([
'user_id' => $user_id,
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_POINTS_BUY,
'product_id' => 0,
'amount' => $pay_amount,
'currency' => 'CNY',
'status' => AKSRT_Order::STATUS_PENDING,
'payment_method' => $payment_method,
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$order = AKSRT_Order_Repository::get_instance()->get($order_number);
if (!$order) {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$order['subject'] = $title;
$order['payment_method'] = $payment_method;
if (!class_exists('AKSRT_Payment_Gateway')) {
return new WP_Error('aksrt_pay_unavailable', __('支付网关不可用', 'aksrt'), ['status' => 500]);
}
$gateway = AKSRT_Payment_Gateway::get_instance()->get_gateway($payment_method);
if (!$gateway) {
return new WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用', 'aksrt'), ['status' => 400, 'order_number' => $order_number]);
}
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
return new WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), ['status' => 400, 'order_number' => $order_number]);
}
$payment_result = $gateway->create_payment($order);
if (empty($payment_result['success'])) {
return new WP_Error('aksrt_pay_failed', (string) ($payment_result['message'] ?? __('支付失败', 'aksrt')), ['status' => 402, 'order_number' => $order_number]);
}
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
$payment_url = (string) ($payment_result['payment_url'] ?? '');
if ($payment_url !== '' || $code_url !== '') {
return [
'redirect' => true,
'payment_url' => $payment_url,
'qr_content' => $code_url,
'order_number' => $order_number,
'amount' => $pay_amount,
'points_amount' => $points_amount,
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
'reload' => false,
'poll' => true,
];
}
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => $payment_method,
'payment_transaction_id' => sanitize_text_field((string) ($payment_result['transaction_id'] ?? '')),
'note' => __('购买积分支付完成', 'aksrt'),
]);
if (!$paid) {
return new WP_Error('aksrt_order_confirm_failed', __('支付确认失败', 'aksrt'), ['status' => 500]);
}
$this->on_order_paid($order_number, AKSRT_Order_Repository::get_instance()->get($order_number) ?: $order);
return [
'redirect' => false,
'order_number' => $order_number,
'amount' => $pay_amount,
'points_amount' => $points_amount,
'points' => AKSRT_Credits::balance($user_id),
'message' => __('购买成功', 'aksrt'),
'reload' => true,
];
}
}
@@ -0,0 +1,449 @@
<?php
/**
* Extracted trait: AKSRT_Balance_Service_Rest
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Balance_Service_Rest {
public function register_routes(): void {
register_rest_route('aksrt/v1', '/pay/balance/charge', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'rest_charge_options'],
'permission_callback' => static function () {
return is_user_logged_in();
},
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'rest_charge_create'],
'permission_callback' => static function () {
return is_user_logged_in();
},
],
]);
register_rest_route('aksrt/v1', '/pay/transfer', [
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'rest_transfer'],
'permission_callback' => static function () {
return is_user_logged_in();
},
],
]);
register_rest_route('aksrt/v1', '/pay/transfer/search', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'rest_transfer_search'],
'permission_callback' => static function () {
return is_user_logged_in();
},
],
]);
register_rest_route('aksrt/v1', '/pay/balance/exchange', [
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'rest_exchange_from_credits'],
'permission_callback' => static function () {
return is_user_logged_in();
},
],
]);
register_rest_route('aksrt/v1', '/pay/points/buy', [
[
'methods' => WP_REST_Server::READABLE,
'callback' => [$this, 'rest_points_buy_options'],
'permission_callback' => static function () {
return is_user_logged_in();
},
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'rest_points_buy_create'],
'permission_callback' => static function () {
return is_user_logged_in();
},
],
]);
register_rest_route('aksrt/v1', '/pay/balance/withdraw', [
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [$this, 'rest_balance_withdraw'],
'permission_callback' => static function () {
return is_user_logged_in();
},
],
]);
}
/**
* GET charge options (packages + methods + limits).
*/
public function rest_charge_options(WP_REST_Request $request) {
if (!class_exists('AKSRT_Balance') || !AKSRT_Balance::enabled()) {
return new WP_Error('aksrt_balance_disabled', __('余额功能未启用', 'aksrt'), ['status' => 403]);
}
$user_id = get_current_user_id();
return rest_ensure_response([
'success' => true,
'data' => $this->get_charge_view($user_id),
]);
}
/**
* POST create recharge order and initiate payment.
*/
public function rest_charge_create(WP_REST_Request $request) {
if (!class_exists('AKSRT_Balance') || !AKSRT_Balance::enabled()) {
return new WP_Error('aksrt_balance_disabled', __('余额功能未启用', 'aksrt'), ['status' => 403]);
}
$user_id = get_current_user_id();
$params = $request->get_json_params();
if (!is_array($params)) {
$params = $request->get_params();
}
$package_key = sanitize_text_field((string) ($params['package'] ?? 'custom'));
$custom_price = isset($params['custom_price']) ? (float) $params['custom_price'] : 0.0;
$payment_method = sanitize_key((string) ($params['payment_method'] ?? 'alipay'));
$result = $this->create_charge_order($user_id, $package_key, $custom_price, $payment_method);
if (is_wp_error($result)) {
return $result;
}
return rest_ensure_response(['success' => true, 'data' => $result]);
}
/**
* Transfer balance or points between users.
*/
public function rest_transfer(WP_REST_Request $request) {
$params = $request->get_json_params();
if (!is_array($params)) {
$params = $request->get_params();
}
$type = sanitize_key((string) ($params['type'] ?? 'balance'));
$recipient = (int) ($params['recipient'] ?? 0);
$amount_raw = $params['amount'] ?? $params['price'] ?? 0;
$from_id = get_current_user_id();
if (!in_array($type, ['balance', 'points'], true)) {
return new WP_Error('aksrt_transfer_type', __('转账类型无效', 'aksrt'), ['status' => 400]);
}
if ($from_id <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录', 'aksrt'), ['status' => 401]);
}
if ($recipient <= 0 || $recipient === $from_id) {
return new WP_Error('aksrt_transfer_user', __('请选择有效的收款用户', 'aksrt'), ['status' => 400]);
}
if (!get_userdata($recipient)) {
return new WP_Error('aksrt_transfer_user', __('收款用户不存在', 'aksrt'), ['status' => 404]);
}
$cfg = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_payment_recharge_settings() : [];
if ($type === 'balance') {
if (empty($cfg['enabled']) || empty($cfg['transfer']['enabled'])) {
return new WP_Error('aksrt_transfer_closed', __('余额转账未开启', 'aksrt'), ['status' => 403]);
}
if (class_exists('AKSRT_Settings_Capabilities') && !AKSRT_Settings_Capabilities::user_can('balance_transfer', $from_id)) {
return new WP_Error('aksrt_transfer_forbidden', __('当前账号无权转账余额', 'aksrt'), ['status' => 403]);
}
$amount = round((float) $amount_raw, 2);
if ($amount <= 0) {
return new WP_Error('aksrt_transfer_amount', __('请输入有效金额', 'aksrt'), ['status' => 400]);
}
if (!class_exists('AKSRT_Balance')) {
return new WP_Error('aksrt_balance_unavailable', __('余额系统不可用', 'aksrt'), ['status' => 500]);
}
$from_bal = AKSRT_Balance::balance($from_id);
if ($from_bal < $amount) {
return new WP_Error('aksrt_balance_insufficient', __('余额不足', 'aksrt'), ['status' => 402]);
}
$fee_rate = max(0, min(50, (int) ($cfg['transfer']['service_charge'] ?? 0)));
$credit = $fee_rate > 0 ? round($amount * (100 - $fee_rate) / 100, 2) : $amount;
$from_user = wp_get_current_user();
$to_user = get_userdata($recipient);
$deduct = AKSRT_Balance::deduct(
$from_id,
$amount,
'transfer_out',
['type' => 'user', 'id' => $recipient],
sprintf(__('转账给用户[%s]', 'aksrt'), $to_user ? $to_user->display_name : (string) $recipient)
);
if (is_wp_error($deduct)) {
return $deduct;
}
AKSRT_Balance::add(
$recipient,
$credit,
'transfer_in',
['type' => 'user', 'id' => $from_id],
sprintf(__('来自用户[%s]的转账', 'aksrt'), $from_user->display_name ?: (string) $from_id)
);
return rest_ensure_response([
'success' => true,
'data' => [
'message' => __('转账成功', 'aksrt'),
'reload' => true,
'balance' => AKSRT_Balance::balance($from_id),
],
]);
}
// points transfer
$points_transfer_on = !empty(AKSRT_Settings::get_all()['points_transfer_s']);
if (!class_exists('AKSRT_Settings') || !AKSRT_Settings::credits_enabled() || !$points_transfer_on) {
return new WP_Error('aksrt_transfer_closed', __('积分转账未开启', 'aksrt'), ['status' => 403]);
}
if (class_exists('AKSRT_Settings_Capabilities') && !AKSRT_Settings_Capabilities::user_can('points_transfer', $from_id)) {
return new WP_Error('aksrt_transfer_forbidden', __('当前账号无权转账积分', 'aksrt'), ['status' => 403]);
}
$amount = (int) $amount_raw;
if ($amount <= 0) {
return new WP_Error('aksrt_transfer_amount', __('请输入有效积分', 'aksrt'), ['status' => 400]);
}
if (!class_exists('AKSRT_Credits')) {
return new WP_Error('aksrt_credits_unavailable', __('积分系统不可用', 'aksrt'), ['status' => 500]);
}
$fee_rate = max(0, min(50, (int) (AKSRT_Settings::get_all()['points_service_charge'] ?? 0)));
$credit = $fee_rate > 0 ? (int) floor($amount * (100 - $fee_rate) / 100) : $amount;
$from_user = wp_get_current_user();
$to_user = get_userdata($recipient);
$deduct = AKSRT_Credits::deduct(
$from_id,
$amount,
'transfer_out',
['type' => 'user', 'id' => $recipient],
sprintf(__('转账给用户[%s]', 'aksrt'), $to_user ? $to_user->display_name : (string) $recipient)
);
if (is_wp_error($deduct)) {
return $deduct;
}
AKSRT_Credits::add(
$recipient,
$credit,
'transfer_in',
['type' => 'user', 'id' => $from_id],
sprintf(__('来自用户[%s]的转账', 'aksrt'), $from_user->display_name ?: (string) $from_id)
);
return rest_ensure_response([
'success' => true,
'data' => [
'message' => __('转账成功', 'aksrt'),
'reload' => true,
'balance' => AKSRT_Credits::balance($from_id),
],
]);
}
public function rest_transfer_search(WP_REST_Request $request) {
$s = sanitize_text_field((string) $request->get_param('s'));
if (mb_strlen($s) < 1) {
return rest_ensure_response(['success' => true, 'data' => ['users' => []]]);
}
$exclude = [get_current_user_id()];
$q = new WP_User_Query([
'search' => '*' . $s . '*',
'search_columns' => ['user_login', 'user_nicename', 'display_name', 'user_email'],
'exclude' => $exclude,
'number' => 20,
'fields' => ['ID', 'display_name', 'user_login'],
]);
$users = [];
foreach ((array) $q->get_results() as $u) {
$users[] = [
'id' => (int) $u->ID,
'display_name' => (string) $u->display_name,
'user_login' => (string) $u->user_login,
'avatar' => get_avatar_url((int) $u->ID, ['size' => 48]),
];
}
return rest_ensure_response(['success' => true, 'data' => ['users' => $users]]);
}
/**
* Exchange points → cash wallet (pay_balance_exchange_*).
*/
public function rest_exchange_from_credits(WP_REST_Request $request) {
if (!class_exists('AKSRT_Balance') || !AKSRT_Balance::enabled()) {
return new WP_Error('aksrt_balance_disabled', __('余额功能未启用', 'aksrt'), ['status' => 403]);
}
$cfg = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_payment_recharge_settings() : [];
if (empty($cfg['exchange']['enabled'])) {
return new WP_Error('aksrt_exchange_closed', __('积分兑换余额未开启', 'aksrt'), ['status' => 403]);
}
if (!class_exists('AKSRT_Credits') || !AKSRT_Settings::credits_enabled()) {
return new WP_Error('aksrt_credits_disabled', __('积分功能未启用', 'aksrt'), ['status' => 403]);
}
$params = $request->get_json_params();
if (!is_array($params)) {
$params = $request->get_params();
}
$points = (int) ($params['points'] ?? 0);
$rate = max(1, (int) ($cfg['exchange']['rate'] ?? 100)); // N points = 1 yuan
if ($points < $rate) {
return new WP_Error('aksrt_exchange_min', sprintf(__('最少兑换 %d 积分', 'aksrt'), $rate), ['status' => 400]);
}
// Floor to whole yuan.
$yuan = (int) floor($points / $rate);
$cost = $yuan * $rate;
if ($yuan <= 0 || $cost <= 0) {
return new WP_Error('aksrt_exchange_amount', __('兑换数量无效', 'aksrt'), ['status' => 400]);
}
$user_id = get_current_user_id();
$deduct = AKSRT_Credits::deduct(
$user_id,
$cost,
'exchange_to_balance',
['type' => 'balance'],
sprintf(__('积分兑换余额 %d 元', 'aksrt'), $yuan)
);
if (is_wp_error($deduct)) {
return $deduct;
}
$add = AKSRT_Balance::add(
$user_id,
(float) $yuan,
'exchange_from_credits',
['type' => 'credits'],
sprintf(__('积分兑换:消耗 %d 积分', 'aksrt'), $cost)
);
if (is_wp_error($add)) {
// best-effort refund points
AKSRT_Credits::add($user_id, $cost, 'exchange_refund', [], __('兑换失败退回积分', 'aksrt'));
return $add;
}
return rest_ensure_response([
'success' => true,
'data' => [
'message' => sprintf(__('兑换成功:+%d 元', 'aksrt'), $yuan),
'wallet_balance' => AKSRT_Balance::balance($user_id),
'credit_balance' => AKSRT_Credits::balance($user_id),
'reload' => true,
],
]);
}
public function rest_points_buy_options(WP_REST_Request $request) {
if (!$this->points_buy_enabled()) {
return new WP_Error('aksrt_points_buy_disabled', __('购买积分未开启', 'aksrt'), ['status' => 403]);
}
return rest_ensure_response([
'success' => true,
'data' => $this->get_points_buy_view(get_current_user_id()),
]);
}
public function rest_points_buy_create(WP_REST_Request $request) {
if (!$this->points_buy_enabled()) {
return new WP_Error('aksrt_points_buy_disabled', __('购买积分未开启', 'aksrt'), ['status' => 403]);
}
$params = $request->get_json_params();
if (!is_array($params)) {
$params = $request->get_params();
}
$package_key = sanitize_text_field((string) ($params['package'] ?? 'custom'));
$custom_points = (int) ($params['custom_points'] ?? $params['points'] ?? 0);
$payment_method = sanitize_key((string) ($params['payment_method'] ?? 'alipay'));
$result = $this->create_points_buy_order(get_current_user_id(), $package_key, $custom_points, $payment_method);
if (is_wp_error($result)) {
return $result;
}
return rest_ensure_response(['success' => true, 'data' => $result]);
}
public function rest_balance_withdraw(WP_REST_Request $request) {
if (!class_exists('AKSRT_Balance') || !AKSRT_Balance::enabled()) {
return new WP_Error('aksrt_balance_disabled', __('余额功能未启用', 'aksrt'), ['status' => 403]);
}
$settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_all() : [];
if (empty($settings['pay_balance_withdraw_s'])) {
return new WP_Error('aksrt_withdraw_closed', __('余额提现未开启', 'aksrt'), ['status' => 403]);
}
if (!class_exists('AKSRT_Withdrawal')) {
return new WP_Error('aksrt_withdraw_unavailable', __('提现系统不可用', 'aksrt'), ['status' => 500]);
}
$params = $request->get_json_params();
if (!is_array($params)) {
$params = $request->get_params();
}
$user_id = get_current_user_id();
if (class_exists('AKSRT_Settings_Capabilities') && !AKSRT_Settings_Capabilities::user_can('balance_withdraw', $user_id)) {
return new WP_Error('aksrt_withdraw_forbidden', __('当前账号无权提现', 'aksrt'), ['status' => 403]);
}
$amount = round((float) ($params['amount'] ?? 0), 2);
$method = sanitize_key((string) ($params['method'] ?? 'alipay'));
$account = sanitize_textarea_field((string) ($params['account_info'] ?? $params['account'] ?? ''));
$lowest = max(1, (float) ($settings['pay_balance_withdraw_lowest_money'] ?? $settings['pay_rebate_withdraw_lowest_money'] ?? 50));
if ($amount < $lowest) {
return new WP_Error('aksrt_withdraw_min', sprintf(__('最低提现 %s 元', 'aksrt'), (string) $lowest), ['status' => 400]);
}
if ($account === '') {
return new WP_Error('aksrt_withdraw_account', __('请填写收款账号信息', 'aksrt'), ['status' => 400]);
}
// One open balance withdrawal at a time (Zibll parity).
if (AKSRT_Withdrawal::get_instance()->has_open_withdrawal($user_id)) {
return new WP_Error('aksrt_withdraw_pending', __('您有处理中的提现申请,请等待完成后再申请', 'aksrt'), ['status' => 409]);
}
$wallet = AKSRT_Balance::balance($user_id);
if ($wallet < $amount) {
return new WP_Error('aksrt_balance_insufficient', __('余额不足', 'aksrt'), ['status' => 402]);
}
$deduct = AKSRT_Balance::deduct($user_id, $amount, 'withdraw_hold', ['type' => 'withdraw'], __('余额提现申请冻结', 'aksrt'));
if (is_wp_error($deduct)) {
return $deduct;
}
$fee_rate = max(0, min(50, (int) ($settings['withdraw_service_charge'] ?? 0)));
$net = $fee_rate > 0 ? round($amount * (100 - $fee_rate) / 100, 2) : $amount;
$account_payload = wp_json_encode([
'account' => $account,
'gross_amount' => $amount,
'fee_rate' => $fee_rate,
'source' => 'balance',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: $account;
$wid = AKSRT_Withdrawal::get_instance()->create($user_id, $net, $method !== '' ? $method : 'alipay', $account_payload);
if ($wid <= 0) {
AKSRT_Balance::add($user_id, $amount, 'withdraw_refund', [], __('提现申请失败退回', 'aksrt'));
return new WP_Error('aksrt_withdraw_failed', __('提现申请失败', 'aksrt'), ['status' => 500]);
}
return rest_ensure_response([
'success' => true,
'data' => [
'message' => __('提现申请已提交,请等待处理', 'aksrt'),
'withdrawal_id' => $wid,
'amount' => $amount,
'net_amount' => $net,
'wallet_balance' => AKSRT_Balance::balance($user_id),
'reload' => true,
],
]);
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,603 @@
<?php
/**
* Extracted trait: AKSRT_Order_Repository_Admin
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Order_Repository_Admin {
/**
* Build a stable admin-facing order context payload for workbench pages.
*/
public function get_admin_order_context(string $order_number): array {
$order_number = sanitize_text_field($order_number);
$contexts = $this->get_admin_order_context_map([$order_number]);
return $contexts[$order_number] ?? $this->get_empty_admin_order_context($order_number);
}
/**
* Build admin-facing order context payloads in bulk to avoid per-row order queries.
*
* @param array<int, string> $order_numbers
* @return array<string, array<string, mixed>>
*/
public function get_admin_order_context_map(array $order_numbers): array {
global $wpdb;
$order_numbers = array_values(array_unique(array_filter(array_map(
static function ($order_number): string {
return sanitize_text_field((string) $order_number);
},
$order_numbers
))));
if (empty($order_numbers)) {
return [];
}
$contexts = [];
foreach ($order_numbers as $order_number) {
$contexts[$order_number] = $this->get_empty_admin_order_context($order_number);
}
$placeholders = implode(', ', array_fill(0, count($order_numbers), '%s'));
$sql = "SELECT * FROM {$this->table()} WHERE order_number IN ({$placeholders})";
$rows = $wpdb->get_results($wpdb->prepare($sql, $order_numbers), ARRAY_A);
if (empty($rows)) {
return $contexts;
}
$orders = [];
foreach ((array) $rows as $row) {
$order = $this->normalize_row($row);
$orders[(string) ($order['order_number'] ?? '')] = $order;
}
$resolved_order_numbers = array_keys($orders);
$shipping_map = class_exists('AKSRT_Shipping')
? AKSRT_Shipping::get_instance()->get_summary_map($resolved_order_numbers)
: [];
$after_sale_map = class_exists('AKSRT_After_Sale')
? AKSRT_After_Sale::get_instance()->get_summary_map($resolved_order_numbers)
: [];
foreach ($orders as $order_number => $order) {
$contexts[$order_number] = $this->build_admin_order_context($order, [
'shipping_summary' => $shipping_map[$order_number] ?? [],
'after_sale_summary' => $after_sale_map[$order_number] ?? [],
]);
}
return $contexts;
}
/**
* 更新订单状态(带状态机验证)
*/
/**
* Merge operator admin note into order note meta without destroying JSON payload.
*/
public function update_admin_note(string $order_number, string $admin_note): bool {
global $wpdb;
$order = $this->get($order_number);
if (!$order) {
return false;
}
$raw = isset($order['note']) && is_string($order['note']) ? $order['note'] : '';
$decoded = $raw !== '' ? json_decode($raw, true) : null;
$admin_note = sanitize_textarea_field($admin_note);
if (is_array($decoded)) {
$decoded['admin_note'] = $admin_note;
$new_note = wp_json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} elseif ($raw !== '') {
$new_note = wp_json_encode([
'customer_note' => $raw,
'admin_note' => $admin_note,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} else {
$new_note = wp_json_encode([
'admin_note' => $admin_note,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
$result = $wpdb->update(
$this->table(),
['note' => $new_note],
['order_number' => $order_number],
['%s'],
['%s']
);
return $result !== false;
}
/**
* 管理后台订单列表 — 支持 search(order_number / product_type 模糊匹配)
*/
/**
* Paid orders that still need shipping (no shipping row yet).
*
* @return array{items: array<int,array<string,mixed>>, total: int, page: int, max_pages: int}
*/
public function get_pending_shipment_orders(array $args = []): array {
global $wpdb;
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$search = sanitize_text_field((string) ($args['search'] ?? $args['s'] ?? ''));
$order_table = $this->table();
// Must match AKSRT_Shipping::table() key.
$shipping_table = class_exists('AKSRT_DB_Migrator')
? AKSRT_DB_Migrator::table('order_shipping')
: $wpdb->prefix . 'aksrt_order_shipping';
$where = ["o.status = %s"];
$params = ['paid'];
if ($search !== '') {
$where[] = '(o.order_number LIKE %s OR o.product_type LIKE %s)';
$like = '%' . $wpdb->esc_like($search) . '%';
$params[] = $like;
$params[] = $like;
}
// NOT EXISTS avoids ONLY_FULL_GROUP_BY issues from LEFT JOIN + GROUP BY.
$where[] = "NOT EXISTS (
SELECT 1 FROM {$shipping_table} s
WHERE s.order_number = o.order_number
)";
$where_sql = 'WHERE ' . implode(' AND ', $where);
$count_sql = "SELECT COUNT(*) FROM {$order_table} o {$where_sql}";
$total = (int) $wpdb->get_var($wpdb->prepare($count_sql, $params));
$sql = "SELECT o.*
FROM {$order_table} o
{$where_sql}
ORDER BY COALESCE(o.paid_at, o.created_at) DESC, o.id DESC
LIMIT %d OFFSET %d";
$query_params = $params;
$query_params[] = $limit;
$query_params[] = $offset;
$rows = $wpdb->get_results($wpdb->prepare($sql, $query_params), ARRAY_A);
$items = array_map([$this, 'normalize_row'], (array) $rows);
return [
'items' => $items,
'total' => $total,
'page' => (int) floor($offset / $limit) + 1,
'max_pages' => (int) ceil(max(1, $total) / $limit),
];
}
public function get_orders_admin(array $args = [], string $search = ''): array {
global $wpdb;
[$where, $params] = $this->build_admin_order_filters($args, $search);
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
$rows = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
return array_map([$this, 'normalize_row'], (array) $rows);
}
/**
* 管理后台订单计数 — 与 get_orders_admin 共用过滤条件
*/
public function get_orders_count(array $args = [], string $search = ''): int {
global $wpdb;
[$where, $params] = $this->build_admin_order_filters($args, $search);
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
if (!empty($params)) {
return (int) $wpdb->get_var($wpdb->prepare($sql, $params));
}
return (int) $wpdb->get_var($sql);
}
/**
* Aggregate paid content (resource) sales by product_id (post ID).
*
* @param array{
* product_type?: string,
* product_ids?: int[],
* status_in?: string[],
* limit?: int,
* offset?: int,
* orderby?: string,
* order?: string
* } $args
* @return array{items: list<array{product_id:int,product_type:string,sales_count:int,avg_amount:float,revenue:float}>, total: int}
*/
public function get_content_sales_summary(array $args = []): array {
global $wpdb;
$product_type = sanitize_key((string) ($args['product_type'] ?? AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE));
if ($product_type === '') {
$product_type = AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE;
}
$status_in = $args['status_in'] ?? AKSRT_Domain_Model::paid_order_statuses();
if (!is_array($status_in) || empty($status_in)) {
$status_in = AKSRT_Domain_Model::paid_order_statuses();
}
$status_in = array_values(array_filter(array_map('sanitize_key', $status_in)));
if (empty($status_in)) {
return ['items' => [], 'total' => 0];
}
$where = ['product_type = %s', 'product_id > 0'];
$params = [$product_type];
$status_placeholders = implode(', ', array_fill(0, count($status_in), '%s'));
$where[] = "status IN ({$status_placeholders})";
foreach ($status_in as $st) {
$params[] = $st;
}
$product_ids = $args['product_ids'] ?? [];
if (is_array($product_ids) && !empty($product_ids)) {
$ids = array_values(array_unique(array_filter(array_map('intval', $product_ids), static function ($id) {
return $id > 0;
})));
if (!empty($ids)) {
$id_ph = implode(', ', array_fill(0, count($ids), '%d'));
$where[] = "product_id IN ({$id_ph})";
foreach ($ids as $id) {
$params[] = $id;
}
}
}
$where_sql = 'WHERE ' . implode(' AND ', $where);
$table = $this->table();
$total_sql = "SELECT COUNT(DISTINCT product_id) FROM {$table} {$where_sql}";
$total = (int) $wpdb->get_var($wpdb->prepare($total_sql, $params));
$orderby_map = [
'sales_count' => 'sales_count',
'revenue' => 'revenue',
'avg_amount' => 'avg_amount',
'product_id' => 'product_id',
];
$orderby_key = sanitize_key((string) ($args['orderby'] ?? 'sales_count'));
$orderby_col = $orderby_map[$orderby_key] ?? 'sales_count';
$order_dir = strtoupper((string) ($args['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC';
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$list_sql = "SELECT product_id,
product_type,
COUNT(*) AS sales_count,
AVG(amount) AS avg_amount,
SUM(amount) AS revenue
FROM {$table}
{$where_sql}
GROUP BY product_id, product_type
ORDER BY {$orderby_col} {$order_dir}
LIMIT %d OFFSET %d";
$list_params = $params;
$list_params[] = $limit;
$list_params[] = $offset;
$rows = $wpdb->get_results($wpdb->prepare($list_sql, $list_params), ARRAY_A);
$items = [];
foreach ((array) $rows as $row) {
$items[] = [
'product_id' => (int) ($row['product_id'] ?? 0),
'product_type' => (string) ($row['product_type'] ?? $product_type),
'sales_count' => (int) ($row['sales_count'] ?? 0),
'avg_amount' => round((float) ($row['avg_amount'] ?? 0), 2),
'revenue' => round((float) ($row['revenue'] ?? 0), 2),
];
}
return [
'items' => $items,
'total' => $total,
];
}
/**
* Paid order totals for a set of product IDs (resource content).
*
* @param int[] $product_ids
* @return array<int, array{sales_count:int,avg_amount:float,revenue:float}>
*/
public function get_content_sales_map(array $product_ids, string $product_type = ''): array {
$ids = array_values(array_unique(array_filter(array_map('intval', $product_ids), static function ($id) {
return $id > 0;
})));
if (empty($ids)) {
return [];
}
if ($product_type === '') {
$product_type = AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE;
}
$summary = $this->get_content_sales_summary([
'product_type' => $product_type,
'product_ids' => $ids,
'limit' => max(count($ids), 1),
'offset' => 0,
'orderby' => 'product_id',
'order' => 'ASC',
]);
$map = [];
foreach ($summary['items'] as $item) {
$pid = (int) ($item['product_id'] ?? 0);
if ($pid <= 0) {
continue;
}
$map[$pid] = [
'sales_count' => (int) ($item['sales_count'] ?? 0),
'avg_amount' => (float) ($item['avg_amount'] ?? 0),
'revenue' => (float) ($item['revenue'] ?? 0),
];
}
return $map;
}
/**
* Shared WHERE builder for admin order list/count.
*
* @return array{0: string[], 1: array<int, mixed>}
*/
private function build_admin_order_filters(array $args, string $search = ''): array {
global $wpdb;
$where = [];
$params = [];
$status = sanitize_key((string) ($args['status'] ?? ''));
if ($status !== '' && $status !== 'all') {
$where[] = 'status = %s';
$params[] = $status;
}
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
if ($product_type !== '') {
$where[] = 'product_type = %s';
$params[] = $product_type;
}
$product_id = (int) ($args['product_id'] ?? 0);
if ($product_id > 0) {
$where[] = 'product_id = %d';
$params[] = $product_id;
}
$user_id = (int) ($args['user_id'] ?? 0);
if ($user_id > 0) {
$where[] = 'user_id = %d';
$params[] = $user_id;
}
$payment_method = sanitize_key((string) ($args['payment_method'] ?? ''));
if ($payment_method !== '') {
$where[] = 'payment_method = %s';
$params[] = $payment_method;
}
if ($search !== '') {
// Numeric search may mean user_id or product_id; also match order number / type.
if (ctype_digit($search)) {
$where[] = '(order_number LIKE %s OR product_type LIKE %s OR user_id = %d OR product_id = %d)';
$like = '%' . $wpdb->esc_like($search) . '%';
$params[] = $like;
$params[] = $like;
$params[] = (int) $search;
$params[] = (int) $search;
} else {
$where[] = '(order_number LIKE %s OR product_type LIKE %s OR payment_method LIKE %s)';
$like = '%' . $wpdb->esc_like($search) . '%';
$params[] = $like;
$params[] = $like;
$params[] = $like;
}
}
return [$where, $params];
}
private function get_empty_admin_order_context(string $order_number): array {
return [
'exists' => false,
'summary' => [
'order_number' => $order_number,
'status' => '',
'status_label' => '',
'amount' => 0.0,
'currency_symbol' => '¥',
'created_at' => '',
'paid_at' => '',
],
'buyer' => [],
'address' => [],
'payment_method' => [],
'shipping_method' => [],
'shipping' => [
'count' => 0,
'latest' => [],
],
'after_sale' => [
'count' => 0,
'latest' => [],
],
'coupon' => [],
'pricing' => [
'subtotal' => 0.0,
'shipping_fee' => 0.0,
'discount' => 0.0,
'amount' => 0.0,
],
'items' => [],
'customer_note' => '',
'raw_order' => null,
];
}
/**
* @param array<string, mixed> $order
* @param array<string, mixed> $related
* @return array<string, mixed>
*/
private function build_admin_order_context(array $order, array $related = []): array {
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
$user = get_userdata((int) ($order['user_id'] ?? 0));
$status = (string) ($order['status'] ?? '');
$address = is_array($meta['address'] ?? null) ? $meta['address'] : [];
$payment_method = is_array($meta['payment_method'] ?? null) ? $meta['payment_method'] : [];
$shipping_method = is_array($meta['shipping_method'] ?? null) ? $meta['shipping_method'] : [];
$coupon = is_array($meta['coupon'] ?? null) ? $meta['coupon'] : [];
$items = is_array($meta['items'] ?? null) ? $meta['items'] : [];
$shipping_fee = (float) ($meta['shipping_fee'] ?? 0);
$discount = (float) ($meta['discount'] ?? 0);
$amount = (float) ($order['amount'] ?? 0);
$subtotal = $this->calculate_order_subtotal($items, max(0, $amount + $discount - $shipping_fee));
$shipping_summary = is_array($related['shipping_summary'] ?? null) ? $related['shipping_summary'] : [];
$after_sale_summary = is_array($related['after_sale_summary'] ?? null) ? $related['after_sale_summary'] : [];
$address['full_text'] = $this->build_address_text($address);
return [
'exists' => true,
'summary' => [
'order_number' => (string) ($order['order_number'] ?? ''),
'status' => $status,
'status_label' => class_exists('AKSRT_Order') ? (AKSRT_Order::get_valid_statuses()[$status] ?? $status) : $status,
'amount' => $amount,
'currency_symbol' => '¥',
'created_at' => (string) ($order['created_at'] ?? ''),
'paid_at' => (string) ($order['paid_at'] ?? ''),
'product_type' => (string) ($order['product_type'] ?? ''),
'product_id' => (int) ($order['product_id'] ?? 0),
],
'buyer' => [
'id' => (int) ($order['user_id'] ?? 0),
'display_name' => $user ? (string) $user->display_name : '',
'user_login' => $user ? (string) $user->user_login : '',
'user_email' => $user ? (string) $user->user_email : '',
],
'address' => $address,
'payment_method' => [
'value' => (string) ($payment_method['value'] ?? $order['payment_method'] ?? ''),
'label' => (string) ($payment_method['label'] ?? $order['payment_method'] ?? ''),
],
'shipping_method' => [
'value' => (string) ($shipping_method['value'] ?? ''),
'label' => (string) ($shipping_method['label'] ?? ''),
'fee' => $shipping_fee,
],
'shipping' => [
'count' => (int) ($shipping_summary['count'] ?? 0),
'latest' => is_array($shipping_summary['latest'] ?? null) ? $shipping_summary['latest'] : [],
],
'after_sale' => [
'count' => (int) ($after_sale_summary['count'] ?? 0),
'latest' => is_array($after_sale_summary['latest'] ?? null) ? $after_sale_summary['latest'] : [],
],
'coupon' => $coupon,
'pricing' => [
'subtotal' => $subtotal,
'shipping_fee' => $shipping_fee,
'discount' => $discount,
'amount' => $amount,
],
'items' => $items,
'customer_note' => (string) ($meta['customer_note'] ?? $order['note_text'] ?? ''),
'raw_order' => $order,
];
}
/**
* @param array<int, array<string, mixed>> $items
*/
private function calculate_order_subtotal(array $items, float $fallback): float {
if (empty($items)) {
return round($fallback, 2);
}
$subtotal = 0.0;
foreach ($items as $item) {
if (isset($item['line_total'])) {
$subtotal += (float) $item['line_total'];
continue;
}
$subtotal += (float) ($item['price'] ?? 0) * max(1, (int) ($item['quantity'] ?? 1));
}
if ($subtotal <= 0) {
return round($fallback, 2);
}
return round($subtotal, 2);
}
private function build_address_text(array $address): string {
$segments = [];
foreach (['name', 'phone', 'province', 'city', 'district', 'address'] as $key) {
$value = trim((string) ($address[$key] ?? ''));
if ($value !== '') {
$segments[] = $value;
}
}
if (!empty($address['full_text']) && is_string($address['full_text'])) {
return (string) $address['full_text'];
}
return implode(' / ', $segments);
}
}
@@ -0,0 +1,481 @@
<?php
/**
* Extracted trait: AKSRT_Order_Repository_Core
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
trait AKSRT_Order_Repository_Core {
private function table(): string {
return AKSRT_DB_Migrator::table('order');
}
/* ------------------------------------------------------------------
* CRUD
* ----------------------------------------------------------------*/
/**
* 创建订单(写入表)
*/
public function create(array $data): string {
global $wpdb;
$order_number = $data['order_number'] ?? $this->generate_order_number();
$wpdb->insert($this->table(), [
'order_number' => $order_number,
'user_id' => (int) ($data['user_id'] ?? 0),
'product_type' => sanitize_key((string) ($data['product_type'] ?? '')),
'product_id' => (int) ($data['product_id'] ?? 0),
'amount' => (float) ($data['amount'] ?? 0),
'currency' => sanitize_key((string) ($data['currency'] ?? 'CNY')),
'status' => AKSRT_Domain_Model::normalize_order_status((string) ($data['status'] ?? AKSRT_Domain_Model::ORDER_STATUS_PENDING)),
'payment_method' => sanitize_key((string) ($data['payment_method'] ?? '')),
'payment_transaction_id' => sanitize_text_field((string) ($data['payment_transaction_id'] ?? '')),
'created_at' => $data['created_at'] ?? current_time('mysql'),
'paid_at' => $data['paid_at'] ?? null,
'expires_at' => $data['expires_at'] ?? null,
'note' => sanitize_textarea_field((string) ($data['note'] ?? '')),
], ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
if ($wpdb->insert_id) {
return $order_number;
}
return '';
}
/**
* 获取单个订单
*/
public function get(string $order_number): ?array {
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$this->table()} WHERE order_number = %s",
$order_number
), ARRAY_A);
if ($row) {
return $this->normalize_row($row);
}
return null;
}
/**
* 获取用户订单列表
*/
public function get_user_orders(int $user_id, array $args = []): array {
global $wpdb;
$where = [];
$params = [];
if ($user_id > 0) {
$where[] = 'user_id = %d';
$params[] = $user_id;
}
$status = sanitize_key((string) ($args['status'] ?? ''));
if ($status !== '') {
$where[] = 'status = %s';
$params[] = $status;
}
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
if ($product_type !== '') {
$where[] = 'product_type = %s';
$params[] = $product_type;
}
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
$rows = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
return array_map([$this, 'normalize_row'], (array) $rows);
}
/**
* 获取单个用户订单。
*/
public function get_user_order(int $user_id, string $order_number): ?array {
$order = $this->get($order_number);
if (!$order) {
return null;
}
if ($user_id > 0 && (int) ($order['user_id'] ?? 0) !== $user_id) {
return null;
}
return $order;
}
/**
* 获取用户订单总数。
*/
public function count_user_orders(int $user_id, array $args = []): int {
global $wpdb;
if ($user_id <= 0) {
return 0;
}
$where = ['user_id = %d'];
$params = [$user_id];
$status = sanitize_key((string) ($args['status'] ?? ''));
if ($status !== '') {
$where[] = 'status = %s';
$params[] = $status;
}
$product_type = sanitize_key((string) ($args['product_type'] ?? ''));
if ($product_type !== '') {
$where[] = 'product_type = %s';
$params[] = $product_type;
}
$where_sql = 'WHERE ' . implode(' AND ', $where);
$sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}";
return (int) $wpdb->get_var($wpdb->prepare($sql, $params));
}
public function update_status(string $order_number, string $new_status, array $extra = []): bool {
global $wpdb;
$valid = AKSRT_Domain_Model::order_status_labels();
if (!isset($valid[$new_status])) return false;
$order = $this->get($order_number);
if (!$order) return false;
$current_status = $order['status'] ?? 'created';
if (empty($extra['force']) && !AKSRT_Domain_Model::can_transition_order_status($current_status, $new_status)) {
return false;
}
$update_data = ['status' => $new_status];
$format = ['%s'];
$time_fields = [
'paid' => 'paid_at',
'delivered' => 'delivered_at',
'completed' => 'completed_at',
'cancelled' => 'cancelled_at',
'refunded' => 'refunded_at',
];
foreach ($time_fields as $status_key => $field) {
if ($new_status === $status_key) {
$update_data[$field] = current_time('mysql');
$format[] = '%s';
}
}
if (!empty($extra['payment_method'])) {
$update_data['payment_method'] = sanitize_key($extra['payment_method']);
$format[] = '%s';
}
if (!empty($extra['payment_transaction_id'])) {
$update_data['payment_transaction_id'] = sanitize_text_field($extra['payment_transaction_id']);
$format[] = '%s';
}
if (!empty($extra['note'])) {
// Preserve JSON pay meta (coupon/topic_pay/content_pay/…); stash human text as status_note.
$status_note = sanitize_textarea_field((string) $extra['note']);
$raw_note = isset($order['note']) && is_string($order['note']) ? $order['note'] : '';
$decoded_note = $raw_note !== '' ? json_decode($raw_note, true) : null;
if (is_array($decoded_note)) {
$decoded_note['status_note'] = $status_note;
$merged = wp_json_encode($decoded_note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$update_data['note'] = is_string($merged) ? $merged : $status_note;
} else {
$update_data['note'] = $status_note;
}
$format[] = '%s';
}
$result = $wpdb->update($this->table(), $update_data, ['order_number' => $order_number], $format, ['%s']);
if ($result !== false) {
// 到账事件:让充值/VIP/商品履约等挂接(单一履约入口)。
// 仅在「首次」转为 paid 时触发,避免回调多次到达重复履约。
if ($new_status === AKSRT_Domain_Model::ORDER_STATUS_PAID && $current_status !== AKSRT_Domain_Model::ORDER_STATUS_PAID) {
$paid_order = array_merge($order, [
'status' => $new_status,
'payment_method' => $update_data['payment_method'] ?? ($order['payment_method'] ?? ''),
'payment_transaction_id' => $update_data['payment_transaction_id'] ?? ($order['payment_transaction_id'] ?? ''),
]);
if (isset($update_data['note'])) {
// Keep pre-update JSON payload for listeners (coupon burn etc.);
// only attach status_note if callers mutated note.
$pre_meta = [];
$pre_raw = isset($order['note']) && is_string($order['note']) ? $order['note'] : '';
if ($pre_raw !== '') {
$pre_decoded = json_decode($pre_raw, true);
if (is_array($pre_decoded)) {
$pre_meta = $pre_decoded;
}
}
if ($pre_meta !== []) {
$paid_order['meta_payload'] = $pre_meta;
// Prefer merged DB note (JSON + status_note) for downstream reads.
$paid_order['note'] = $update_data['note'];
}
}
/**
* 订单到账。
*
* @param string $order_number 订单号
* @param array $paid_order 订单数据(status=paid)
*/
do_action('aksrt_order_paid', $order_number, $paid_order);
}
}
return $result !== false;
}
/**
* Delete closed (cancelled/expired) orders older than $days days.
* Mirrors zibll ZibPay::clear_order(14).
*
* @return int Number of rows deleted.
*/
public function clear_closed_orders(int $days = 14): int {
global $wpdb;
$days = max(1, min(3650, $days));
$table = $this->table();
$cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
// Prefer cancelled_at when set; fall back to created_at for older rows.
$sql = "DELETE FROM {$table}
WHERE status IN ('cancelled', 'expired')
AND COALESCE(NULLIF(cancelled_at, ''), created_at) < %s";
$deleted = $wpdb->query($wpdb->prepare($sql, $cutoff));
return $deleted === false ? 0 : (int) $deleted;
}
/**
* Hard-delete a single order by order_number (admin hygiene).
*/
public function delete_order(string $order_number): bool {
global $wpdb;
$order_number = sanitize_text_field($order_number);
if ($order_number === '') {
return false;
}
$ok = $wpdb->delete($this->table(), ['order_number' => $order_number], ['%s']);
return $ok !== false && $ok > 0;
}
/**
* 获取订单统计
*/
public function get_stats(int $user_id = 0): array {
global $wpdb;
$where = '';
$params = [];
if ($user_id > 0) {
$where = 'WHERE user_id = %d';
$params[] = $user_id;
}
$sql = "SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN status = 'paid' THEN 1 ELSE 0 END) as paid,
SUM(CASE WHEN status = 'delivered' THEN 1 ELSE 0 END) as delivered,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) as cancelled,
SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) as refunded,
SUM(CASE WHEN status IN ('paid','delivered','completed') THEN amount ELSE 0 END) as revenue
FROM {$this->table()} {$where}";
if (!empty($params)) {
$row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A);
} else {
$row = $wpdb->get_row($sql, ARRAY_A);
}
return [
'total' => (int) ($row['total'] ?? 0),
'pending' => (int) ($row['pending'] ?? 0),
'paid' => (int) ($row['paid'] ?? 0),
'delivered' => (int) ($row['delivered'] ?? 0),
'completed' => (int) ($row['completed'] ?? 0),
'cancelled' => (int) ($row['cancelled'] ?? 0),
'refunded' => (int) ($row['refunded'] ?? 0),
'revenue' => (float) ($row['revenue'] ?? 0),
];
}
public function has_paid_access(int $user_id, string $product_type, int $product_id): bool {
if ($user_id <= 0 || $product_id <= 0) {
return false;
}
return null !== $this->find_latest_success_order($user_id, $product_type, $product_id);
}
public function get_valid_vip_order(int $user_id): ?array {
return $this->find_latest_success_order($user_id, AKSRT_Domain_Model::ORDER_PRODUCT_VIP, 0, true);
}
/* ------------------------------------------------------------------
* Migration bridge: option → table
* ----------------------------------------------------------------*/
/**
* 一次性迁移:把 option 中的订单搬到表中。
* 幂等 — 已存在的 order_number 不会重复插入。
*/
public function migrate_from_option(): int {
global $wpdb;
$orders = get_option('aksrt_orders', []);
if (!is_array($orders) || empty($orders)) return 0;
$migrated = 0;
foreach ($orders as $order_number => $data) {
$existing = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$this->table()} WHERE order_number = %s",
$order_number
));
if ($existing) continue;
$wpdb->insert($this->table(), [
'order_number' => $order_number,
'user_id' => (int) ($data['user_id'] ?? 0),
'product_type' => sanitize_key((string) ($data['product_type'] ?? '')),
'product_id' => (int) ($data['product_id'] ?? 0),
'amount' => (float) ($data['amount'] ?? 0),
'currency' => sanitize_key((string) ($data['currency'] ?? 'CNY')),
'status' => AKSRT_Domain_Model::normalize_order_status((string) ($data['status'] ?? AKSRT_Domain_Model::ORDER_STATUS_PENDING)),
'payment_method' => sanitize_key((string) ($data['payment_method'] ?? '')),
'created_at' => $data['created_at'] ?? current_time('mysql'),
'paid_at' => $data['paid_at'] ?? null,
'delivered_at' => $data['delivered_at'] ?? null,
'completed_at' => $data['completed_at'] ?? null,
'cancelled_at' => $data['cancelled_at'] ?? null,
'refunded_at' => $data['refunded_at'] ?? null,
'expires_at' => $data['expires_at'] ?? null,
'note' => '',
], ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
if ($wpdb->insert_id) $migrated++;
}
update_option(AKSRT_Domain_Model::ORDER_MIGRATED_OPTION, time());
return $migrated;
}
/* ------------------------------------------------------------------
* Internal helpers
* ----------------------------------------------------------------*/
private function normalize_row(array $row): array {
$row['id'] = (int) $row['id'];
$row['user_id'] = (int) $row['user_id'];
$row['product_id'] = (int) $row['product_id'];
$row['amount'] = (float) $row['amount'];
$row['status'] = AKSRT_Domain_Model::normalize_order_status((string) ($row['status'] ?? ''));
$row['meta_payload'] = [];
if (!empty($row['note']) && is_string($row['note'])) {
$decoded = json_decode($row['note'], true);
if (is_array($decoded)) {
$row['meta_payload'] = $decoded;
if (isset($decoded['customer_note']) && is_string($decoded['customer_note'])) {
$row['note_text'] = $decoded['customer_note'];
}
} else {
$row['note_text'] = $row['note'];
}
}
return $row;
}
private function find_latest_success_order(int $user_id, string $product_type, int $product_id = 0, bool $ignore_product_id = false): ?array {
global $wpdb;
if ($user_id <= 0 || $product_type === '') {
return null;
}
$status_placeholders = implode(', ', array_fill(0, count(AKSRT_Domain_Model::paid_order_statuses()), '%s'));
$params = array_merge(
[$user_id, sanitize_key($product_type)],
$ignore_product_id ? [] : [$product_id],
AKSRT_Domain_Model::paid_order_statuses()
);
$product_sql = $ignore_product_id ? '' : ' AND product_id = %d';
$sql = "SELECT * FROM {$this->table()}
WHERE user_id = %d
AND product_type = %s{$product_sql}
AND status IN ({$status_placeholders})
ORDER BY COALESCE(paid_at, created_at) DESC, id DESC
LIMIT 1";
$row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A);
return is_array($row) ? $this->normalize_row($row) : null;
}
private function generate_order_number(): string {
return 'AKS' . date('YmdHis') . str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,559 @@
<?php
/**
* Settings facade bulk getters (header/page/brand/mail leftovers).
*
* @package AKSRT
*/
defined("ABSPATH") || exit;
trait AKSRT_Settings_Facade_Bulk {
/**
* Header navigation display settings.
*
* @return array{fixed:bool,search_enabled:bool,user_menu:bool,publish_btn:bool,mobile_drawer:bool,nav_style:string,max_width:int,scroll_down_hide:bool,layout:string,mobile_layout:string,mobile_align:string}
*/
public static function get_header_nav_settings(): array {
$settings = self::get_all();
$nav_style = sanitize_key((string) ($settings['header_nav_style'] ?? 'default'));
$max_width = (int) ($settings['header_max_width'] ?? 1200);
$layout = sanitize_key((string) ($settings['header_layout'] ?? '2'));
$ml = sanitize_key((string) ($settings['mobile_header_layout'] ?? 'center'));
$ma = sanitize_key((string) ($settings['mobile_navbar_align'] ?? 'left'));
if (!in_array($nav_style, ['default', 'transparent', 'blur'], true)) {
$nav_style = 'default';
}
if (!in_array($max_width, [0, 1080, 1200, 1320], true)) {
$max_width = 1200;
}
return [
'fixed' => !isset($settings['header_fixed']) || !empty($settings['header_fixed']),
'search_enabled' => !isset($settings['header_search_enabled']) || !empty($settings['header_search_enabled']),
'user_menu' => !isset($settings['header_user_menu']) || !empty($settings['header_user_menu']),
'publish_btn' => !isset($settings['header_publish_btn']) || !empty($settings['header_publish_btn']),
'mobile_drawer' => !isset($settings['header_mobile_drawer']) || !empty($settings['header_mobile_drawer']),
'nav_style' => $nav_style,
'max_width' => $max_width,
'scroll_down_hide'=> !empty($settings['header_scroll_down_hide']),
'layout' => in_array($layout, ['1', '2', '3'], true) ? $layout : '2',
'mobile_layout' => in_array($ml, ['center', 'left'], true) ? $ml : 'center',
'mobile_align' => in_array($ma, ['top', 'left', 'right'], true) ? $ma : 'left',
];
}
/**
* Header publish button configuration.
*
* @return array{enabled:bool,text:string,radius:int,class:string}
*/
public static function get_header_newposts_config(): array {
$s = self::get_all();
return [
'enabled' => !empty($s['header_newposts_enabled']),
'text' => trim((string) ($s['header_newposts_text'] ?? '<i class="fa fa-pencil"></i> 发布')),
'radius' => max(0, min(30, (int) ($s['header_newposts_radius'] ?? 20))),
'class' => sanitize_html_class((string) ($s['header_newposts_class'] ?? '')),
];
}
/**
* Header theme color configuration.
*
* @return array{custom:bool,bg_custom:string,bg:string,color_custom:string}
*/
public static function get_header_theme_config(): array {
$s = self::get_all();
return [
'custom' => !empty($s['header_theme_custom']),
'bg_custom' => sanitize_hex_color((string) ($s['header_theme_bg_custom'] ?? '')) ?: '',
'bg' => sanitize_hex_color((string) ($s['header_theme_bg'] ?? '')) ?: '',
'color_custom' => sanitize_hex_color((string) ($s['header_theme_color_custom'] ?? '')) ?: '',
];
}
/**
* Category archive display settings.
*
* @return array
*/
public static function get_category_page_settings(): array {
$settings = self::get_all();
$header_style = sanitize_key((string) ($settings['category_header_style'] ?? 'cover'));
$list_mode = sanitize_key((string) ($settings['category_list_mode'] ?? 'grid'));
$per_page = (int) ($settings['category_per_page'] ?? 12);
$orderby = sanitize_key((string) ($settings['category_orderby'] ?? 'modified'));
if (!in_array($header_style, ['cover', 'simple', 'none'], true)) {
$header_style = 'cover';
}
if (!in_array($list_mode, ['grid', 'list', 'compact'], true)) {
$list_mode = 'grid';
}
if (!in_array($orderby, ['modified', 'date', 'views', 'comments', 'likes'], true)) {
$orderby = 'modified';
}
return [
'header_style' => $header_style,
'cover_enabled' => !isset($settings['category_cover_enabled']) || !empty($settings['category_cover_enabled']),
'desc_enabled' => !isset($settings['category_desc_enabled']) || !empty($settings['category_desc_enabled']),
'default_cover' => esc_url_raw((string) ($settings['category_default_cover'] ?? '')),
'list_mode' => $list_mode,
'filter_bar' => !isset($settings['category_filter_bar']) || !empty($settings['category_filter_bar']),
'sidebar' => !isset($settings['category_sidebar']) || !empty($settings['category_sidebar']),
'per_page' => max(4, min(48, $per_page)),
'post_count' => !empty($settings['category_post_count']),
'orderby' => $orderby,
];
}
/**
* Tag archive display settings.
*
* @return array
*/
public static function get_tag_page_settings(): array {
$settings = self::get_all();
$header_style = sanitize_key((string) ($settings['tag_header_style'] ?? 'simple'));
$list_mode = sanitize_key((string) ($settings['tag_list_mode'] ?? 'grid'));
$per_page = (int) ($settings['tag_per_page'] ?? 12);
$orderby = sanitize_key((string) ($settings['tag_orderby'] ?? 'count'));
if (!in_array($header_style, ['cover', 'simple', 'none'], true)) {
$header_style = 'simple';
}
if (!in_array($list_mode, ['grid', 'list'], true)) {
$list_mode = 'grid';
}
if (!in_array($orderby, ['count', 'name', 'modified'], true)) {
$orderby = 'count';
}
return [
'header_style' => $header_style,
'default_cover' => esc_url_raw((string) ($settings['tag_default_cover'] ?? '')),
'list_mode' => $list_mode,
'sidebar' => !isset($settings['tag_sidebar']) || !empty($settings['tag_sidebar']),
'per_page' => max(4, min(48, $per_page)),
'post_count' => !empty($settings['tag_post_count']),
'orderby' => $orderby,
];
}
/**
* Misc page display settings (404, search, author).
*
* @return array
*/
public static function get_other_page_settings(): array {
$settings = self::get_all();
$message = trim((string) ($settings['page_404_message'] ?? ''));
$search_per_page = max(4, min(48, (int) ($settings['page_search_per_page'] ?? 12)));
$author_per_page = max(4, min(48, (int) ($settings['page_author_posts_per_page'] ?? 12)));
$header_style = sanitize_key((string) ($settings['page_header_style'] ?? '1'));
if (!in_array($header_style, ['', '1', '2', '3'], true)) {
$header_style = '1';
}
return [
'header_style' => $header_style,
'header_cover_img' => esc_url_raw((string) ($settings['page_header_cover_img'] ?? '')),
'page_404_message' => $message !== '' ? $message : __('页面未找到', 'aksrt'),
'page_404_search' => !isset($settings['page_404_search']) || !empty($settings['page_404_search']),
'page_404_popular' => !isset($settings['page_404_popular']) || !empty($settings['page_404_popular']),
'page_404_back' => !isset($settings['page_404_back']) || !empty($settings['page_404_back']),
'page_search_sidebar' => !isset($settings['page_search_sidebar']) || !empty($settings['page_search_sidebar']),
'page_search_per_page' => $search_per_page,
'page_author_cover' => !isset($settings['page_author_cover']) || !empty($settings['page_author_cover']),
'page_author_sidebar' => !isset($settings['page_author_sidebar']) || !empty($settings['page_author_sidebar']),
'page_author_posts_per_page' => $author_per_page,
];
}
/**
* Special/topic page display settings.
*
* @return array
*/
public static function get_special_page_settings(): array {
$s = self::get_all();
$layout = sanitize_key((string) ($s['special_layout'] ?? 'grid'));
$source_type = sanitize_key((string) ($s['special_source_type'] ?? 'category'));
$subpage_display = sanitize_key((string) ($s['special_subpage_display'] ?? 'cards'));
$pagination = sanitize_key((string) ($s['special_pagination'] ?? 'numeric'));
$orderby = sanitize_key((string) ($s['special_orderby'] ?? 'date'));
if (!in_array($layout, ['grid', 'list', 'masonry'], true)) {
$layout = 'grid';
}
if (!in_array($source_type, ['category', 'tag', 'custom'], true)) {
$source_type = 'category';
}
if (!in_array($subpage_display, ['cards', 'list', 'grid'], true)) {
$subpage_display = 'cards';
}
if (!in_array($pagination, ['numeric', 'load_more', 'infinite'], true)) {
$pagination = 'numeric';
}
if (!in_array($orderby, ['date', 'modified', 'views', 'comments'], true)) {
$orderby = 'date';
}
return [
'cover_enabled' => !empty($s['special_cover_enabled']),
'summary_enabled' => !empty($s['special_summary_enabled']),
'summary_length' => max(50, min(500, (int) ($s['special_summary_length'] ?? 200))),
'default_cover' => esc_url_raw((string) ($s['special_default_cover'] ?? '')),
'layout' => $layout,
'columns' => max(1, min(4, (int) ($s['special_columns'] ?? 2))),
'source_type' => $source_type,
'source_ids' => trim((string) ($s['special_source_ids'] ?? '')),
'subpage_display' => $subpage_display,
'subpage_count' => max(1, min(20, (int) ($s['special_subpage_count'] ?? 6))),
'sidebar' => !empty($s['special_sidebar']),
'pagination' => $pagination,
'per_page' => max(4, min(48, (int) ($s['special_per_page'] ?? 12))),
'post_count' => !empty($s['special_post_count']),
'orderby' => $orderby,
];
}
/**
* Share and poster settings.
*
* @return array{wechat_enabled:bool,wechat_appid:string,wechat_app_secret:string,only_logo:bool,compatible_s:bool,poster_img:string,poster_logo:string,share_img:string,share_desc:string}
*/
public static function get_share_settings(): array {
$settings = self::get_all();
return [
'wechat_enabled' => !empty($settings['share_wechat_s']),
'wechat_appid' => sanitize_text_field((string) ($settings['share_wechat_appid'] ?? '')),
'wechat_app_secret' => sanitize_text_field((string) ($settings['share_wechat_app_secret'] ?? '')),
'only_logo' => !empty($settings['share_only_logo']),
'compatible_s' => !empty($settings['share_img_compatible_s']),
'poster_img' => esc_url_raw((string) ($settings['share_img_byimg'] ?? '')),
'poster_logo' => esc_url_raw((string) ($settings['share_logo'] ?? '')),
'share_img' => esc_url_raw((string) ($settings['share_img'] ?? '')),
'share_desc' => sanitize_textarea_field((string) ($settings['share_desc'] ?? '')) ?: __('扫一扫 立即查看', 'aksrt'),
];
}
/**
* Single post/page display settings.
*
* @return array{toc_enabled:bool,toc_depth:int,reading_progress:bool,lightbox:bool,related_enabled:bool,related_count:int,like_enabled:bool,favorite_enabled:bool,share_enabled:bool,copyright_enabled:bool,print_enabled:bool,pdf_export_enabled:bool,word_count_enabled:bool,reading_time_enabled:bool}
*/
public static function get_post_page_settings(): array {
// Canonical assembly lives in the post-display helper (related style/source split, aliases).
if (class_exists('AKSRT_Settings_Post_Display')) {
return AKSRT_Settings_Post_Display::get_post_page_settings();
}
$settings = self::get_all();
return [
'toc_enabled' => !isset($settings['post_toc_enabled']) || !empty($settings['post_toc_enabled']),
'toc_depth' => max(1, min(6, (int) ($settings['post_toc_depth'] ?? 3))),
'reading_progress' => !isset($settings['post_reading_progress']) || !empty($settings['post_reading_progress']),
'breadcrumbs' => !isset($settings['breadcrumbs_single_s']) || !empty($settings['breadcrumbs_single_s']),
'breadcrumbs_home' => trim((string) ($settings['breadcrumbs_home_text'] ?? '')) ?: __('首页', 'aksrt'),
'related_enabled' => !isset($settings['post_related_enabled']) || !empty($settings['post_related_enabled']),
'related_count' => max(1, min(20, (int) ($settings['post_related_count'] ?? 6))),
'related_style' => 'img',
'related_source' => 'category',
'related_type' => 'category',
'related_orderby' => 'views',
'related_title' => sanitize_text_field((string) ($settings['post_related_title'] ?? '相关推荐')),
];
}
/**
* 获取懒加载配置
*
* 返回 schema 定义的全部字段:全局开关、动画、占位图、模糊预览、
* 以及 8 项粒度范围开关。
*
* @return array {enabled, animation, placeholder_url, blur, avatar, content, comment, private, slider, cover, other, widget}
*/
public static function get_lazyload_config(): array {
$s = self::get_all();
// placeholder 兼容:schema 存 lazyload_placeholder,REST/Vue 存 lazyload_placeholder_url
$placeholder = '';
if (!empty($s['lazyload_placeholder'])) {
$placeholder = $s['lazyload_placeholder'];
} elseif (!empty($s['lazyload_placeholder_url'])) {
$placeholder = $s['lazyload_placeholder_url'];
}
return [
'enabled' => !isset($s['lazyload_enabled']) || !empty($s['lazyload_enabled']),
'animation' => $s['lazyload_animation'] ?? 'blur',
'placeholder_url' => (string) $placeholder,
'blur' => !isset($s['lazyload_blur']) || !empty($s['lazyload_blur']),
'avatar' => !isset($s['lazyload_avatar']) || !empty($s['lazyload_avatar']),
'content' => !empty($s['lazyload_content']),
'comment' => !isset($s['lazyload_comment']) || !empty($s['lazyload_comment']),
'private' => !isset($s['lazyload_private']) || !empty($s['lazyload_private']),
'slider' => !isset($s['lazyload_slider']) || !empty($s['lazyload_slider']),
'cover' => !isset($s['lazyload_cover']) || !empty($s['lazyload_cover']),
'other' => !isset($s['lazyload_other']) || !empty($s['lazyload_other']),
'widget' => !isset($s['lazyload_widget']) || !empty($s['lazyload_widget']),
];
}
/**
* 获取备案配置(统一 page-display footer schema 和全局设置)
* @return array {icp_enabled, icp_number, police_enabled, police_number}
*/
public static function get_filing_settings(): array {
$s = self::get_all();
$icp_enabled = !isset($s['footer_icp']) || !empty($s['footer_icp']);
$police_enabled = !isset($s['footer_police']) || !empty($s['footer_police']);
// 优先读 footer schema 的 icp_number,回退到全局 icp_filing
$icp_number = trim((string) ($s['icp_number'] ?? $s['icp_filing'] ?? ''));
$police_number = trim((string) ($s['police_number'] ?? ''));
return [
'icp_enabled' => $icp_enabled,
'icp_number' => $icp_number,
'police_enabled' => $police_enabled,
'police_number' => $police_number,
];
}
/** 获取页脚附加文字 */
public static function footer_text(): string {
return trim((string) self::get_value('footer_text', ''));
}
/** 获取 ICP 备案号 */
public static function icp_filing(): string {
return trim((string) self::get_value('icp_filing', ''));
}
/** 获取公安备案号 */
public static function police_filing(): string {
return trim((string) self::get_value('police_number', ''));
}
/** 获取导航 Logo 宽度 */
public static function logo_width(): int {
$width = (int) self::get_value('logo_width', 150);
return max(50, min(400, $width));
}
/** 获取Favicon URL */
public static function custom_favicon_url(): string {
$favicon = (string) self::get_value('site_favicon', '');
return $favicon !== '' ? $favicon : '';
}
public static function favicon_url(): string {
$custom = self::custom_favicon_url();
if ($custom !== '') {
return $custom;
}
$site_icon = get_site_icon_url(512);
return $site_icon ? (string) $site_icon : self::default_asset_url('default-favicon.svg');
}
/** 获取About显示名 */
/**
* 让 SEO 设置中的 Favicon 覆盖 WordPress 原生 site_icon 输出链路。
*
* @param string $url
* @param int $size
* @param int $blog_id
* @return string
*/
public static function filter_site_icon_url($url, $size, $blog_id): string {
$custom = self::custom_favicon_url();
return $custom !== '' ? $custom : (string) $url;
}
/**
* 尝试把自定义 favicon 同步到 WordPress 原生 site_icon 选项。
*
* 仅当 URL 能映射到媒体库附件时才同步;外链仍通过 get_site_icon_url 过滤覆盖。
*/
public static function sync_native_site_icon(string $favicon_url): void {
$favicon_url = trim(esc_url_raw($favicon_url));
if ($favicon_url === '') {
return;
}
$attachment_id = attachment_url_to_postid($favicon_url);
if ($attachment_id > 0 && (int) get_option('site_icon', 0) !== $attachment_id) {
update_option('site_icon', $attachment_id);
}
}
/** 启动时对齐原生 site_icon,同步自定义 site_favicon 到原生 site_icon。 */
public static function maybe_sync_native_site_icon(): void {
$custom = self::custom_favicon_url();
if ($custom === '') {
return;
}
self::sync_native_site_icon($custom);
}
public static function about_display_name(): string {
$s = self::get_all();
return !empty($s['about_display_name']) ? $s['about_display_name'] : get_bloginfo('name');
}
/** 获取About简介 */
public static function about_bio(): string {
$s = self::get_all();
return !empty($s['about_bio']) ? $s['about_bio'] : get_bloginfo('description');
}
/** 获取About头像URL */
public static function about_avatar(): string {
$s = self::get_all();
return $s['about_avatar'] ?? ($s['admin_avatar_url'] ?? '');
}
/** 获取头像镜像源 */
public static function avatar_source(): string {
$source = sanitize_text_field((string) self::get_value('avatar_source', 'cn.cravatar.com'));
$allowed = ['cn.cravatar.com', 'cn.gravatar.com', 'gravatar.com'];
return in_array($source, $allowed, true) ? $source : 'cn.cravatar.com';
}
/** 获取SEO关键词 */
public static function seo_keywords(): string {
$s = self::get_all();
return $s['seo_keywords'] ?? '';
}
/** 获取自定义头部代码 */
public static function custom_head_code(): string {
$code = (string) self::get_value('head_code', '');
if ($code === '') {
$code = (string) self::get_value('custom_js_header', '');
}
return $code;
}
/** 获取自定义 CSS */
public static function custom_css(): string {
return (string) self::get_value('custom_css', '');
}
/** 获取自定义底部代码 */
public static function custom_footer_code(): string {
$code = (string) self::get_value('foot_code', '');
if ($code === '') {
$code = (string) self::get_value('custom_js_footer', '');
}
return $code;
}
/** 获取统计代码 */
public static function trackcode(): string {
return trim((string) self::get_value('trackcode', ''));
}
/** 获取SMTP配置 */
public static function get_smtp(): array {
return get_option('aksrt_mail_smtp', [
'enabled' => false,
'host' => '',
'port' => 465,
'username' => '',
'password' => '',
'encryption' => 'ssl',
'from_name' => '',
'from_address' => '',
]);
}
/** 获取邮件品牌设置(发件人昵称/标题前缀/底部内容) */
public static function get_mail_branding(): array {
$s = self::get_all();
return [
'showname' => trim((string) ($s['mail_showname'] ?? '')),
'title_prefix' => trim((string) ($s['mail_title_prefix'] ?? '')),
'description' => trim((string) ($s['mail_description'] ?? '')),
'more_content' => trim((string) ($s['mail_more_content'] ?? '')),
];
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,392 @@
<?php
/**
* User profile/password/email AJAX handlers
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
// ============================================================
add_action('wp_ajax_aksrt_save_profile', function () {
if (!isset($_POST['_profile_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_profile_nonce'])), 'aksrt_profile_save')) {
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
}
$user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
$result = function_exists('aksrt_save_user_center_profile')
? aksrt_save_user_center_profile($user_id, $_POST)
: new WP_Error('aksrt_profile_unavailable', __('保存失败', 'aksrt'));
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success(['message' => __('保存成功!', 'aksrt')]);
});
// ============================================================
// AJAX: 上传打赏收款码
// ============================================================
add_action('wp_ajax_aksrt_upload_reward_qr', function () {
if (!isset($_POST['_profile_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_profile_nonce'])), 'aksrt_profile_save')) {
wp_send_json_error(['message' => __('安全验证失败,请刷新页面重试', 'aksrt')]);
}
$user_id = get_current_user_id();
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
if (function_exists('aksrt_rewards_enabled') && !aksrt_rewards_enabled()) {
wp_send_json_error(['message' => __('打赏功能未启用', 'aksrt')]);
}
$kind = isset($_POST['kind']) ? sanitize_key((string) $_POST['kind']) : '';
if (!in_array($kind, ['weixin', 'alipay'], true)) {
wp_send_json_error(['message' => __('无效类型', 'aksrt')]);
}
if (empty($_FILES['file']['name'])) {
wp_send_json_error(['message' => __('请选择图片', 'aksrt')]);
}
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
$file = $_FILES['file'];
$check = wp_check_filetype_and_ext($file['tmp_name'], $file['name']);
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$mime = (string) ($check['type'] ?? $file['type'] ?? '');
if ($mime === '' || !in_array($mime, $allowed, true)) {
wp_send_json_error(['message' => __('仅支持 JPG/PNG/GIF/WebP 图片', 'aksrt')]);
}
if (!empty($file['size']) && (int) $file['size'] > 2 * 1024 * 1024) {
wp_send_json_error(['message' => __('图片不能超过 2MB', 'aksrt')]);
}
$attachment_id = media_handle_upload('file', 0);
if (is_wp_error($attachment_id)) {
wp_send_json_error(['message' => $attachment_id->get_error_message()]);
}
$meta = $kind === 'weixin' ? 'aksrt_rewards_weixin' : 'aksrt_rewards_alipay';
update_user_meta($user_id, $meta, (int) $attachment_id);
$url = wp_get_attachment_image_url((int) $attachment_id, 'full');
wp_send_json_success([
'message' => __('上传成功', 'aksrt'),
'id' => (int) $attachment_id,
'url' => is_string($url) ? $url : '',
'kind' => $kind,
]);
});
// ============================================================
// AJAX: 发送邮箱验证码(修改密码/更改邮箱通用)
// ============================================================
if (!function_exists('aksrt_get_email_code_key')) :
function aksrt_get_email_code_key(int $user_id, string $scene): string {
return 'aksrt_email_code_' . $scene . '_' . $user_id;
}
endif;
if (!function_exists('aksrt_get_email_code_rate_key')) :
function aksrt_get_email_code_rate_key(int $user_id, string $scene, string $target = ''): string {
$suffix = $target !== '' ? '_' . md5(strtolower($target)) : '';
return 'aksrt_email_code_rate_' . $scene . '_' . $user_id . $suffix;
}
endif;
if (!function_exists('aksrt_store_email_code')) :
function aksrt_store_email_code(int $user_id, string $scene, string $code, string $target = ''): void {
set_transient(
aksrt_get_email_code_key($user_id, $scene),
[
'hash' => wp_hash_password($code),
'target' => $target,
'issued_at' => time(),
],
5 * MINUTE_IN_SECONDS
);
}
endif;
if (!function_exists('aksrt_consume_email_code')) :
function aksrt_consume_email_code(int $user_id, string $scene, string $code, string $target = ''): bool {
$key = aksrt_get_email_code_key($user_id, $scene);
$stored = get_transient($key);
if (!is_array($stored) || empty($stored['hash']) || !wp_check_password($code, $stored['hash'])) {
return false;
}
if ($target !== '' && strtolower((string) ($stored['target'] ?? '')) !== strtolower($target)) {
return false;
}
delete_transient($key);
return true;
}
endif;
add_action('wp_ajax_aksrt_send_email_code', function () {
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
}
if ( ! is_user_logged_in() ) {
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
}
$scene = isset( $_POST['scene'] ) ? sanitize_key( (string) $_POST['scene'] ) : '';
if ( ! in_array( $scene, array( 'change_password', 'change_email' ), true ) ) {
wp_send_json_error( array( 'message' => __( '无效的验证场景', 'aksrt' ) ) );
}
if ( function_exists( 'aksrt_require_captcha' ) ) {
$captcha_result = aksrt_require_captcha( $scene, wp_unslash( $_POST ) );
if ( $captcha_result !== true ) {
wp_send_json_error( array( 'message' => $captcha_result ) );
}
}
$current_user = wp_get_current_user();
$target_email = '';
$success_message = '';
if ( $scene === 'change_password' ) {
$target_email = $current_user->user_email;
if ( empty( $target_email ) || ! is_email( $target_email ) ) {
wp_send_json_error( array( 'message' => __( '当前账号未绑定有效邮箱', 'aksrt' ) ) );
}
$success_message = __( '验证码已发送到您的注册邮箱', 'aksrt' );
} else {
$new_email = isset( $_POST['new_email'] ) ? trim( wp_unslash( $_POST['new_email'] ) ) : '';
$current_password = isset( $_POST['current_password'] ) ? trim( wp_unslash( $_POST['current_password'] ) ) : '';
if ( empty( $current_password ) || ! wp_check_password( $current_password, $current_user->user_pass, $current_user->ID ) ) {
wp_send_json_error( array( 'message' => __( '当前密码不正确', 'aksrt' ) ) );
}
if ( empty( $new_email ) || ! is_email( $new_email ) ) {
wp_send_json_error( array( 'message' => __( '请输入有效的新邮箱地址', 'aksrt' ) ) );
}
if ( strtolower( $new_email ) === strtolower( $current_user->user_email ) ) {
wp_send_json_error( array( 'message' => __( '新邮箱与当前邮箱相同', 'aksrt' ) ) );
}
if ( email_exists( $new_email ) ) {
wp_send_json_error( array( 'message' => __( '该邮箱已被其他账号使用', 'aksrt' ) ) );
}
$target_email = $new_email;
$success_message = __( '验证码已发送到新邮箱,请完成验证后再提交', 'aksrt' );
}
// 频率限制:60秒内不能重复发送
$rate_key = aksrt_get_email_code_rate_key( $current_user->ID, $scene, $target_email );
if ( get_transient( $rate_key ) ) {
wp_send_json_error( array( 'message' => __( '操作过于频繁,请稍后再试', 'aksrt' ) ) );
}
set_transient( $rate_key, true, 60 );
// 生成验证码并同步发送,只有真正发信成功后才写入验证码缓存。
$code = str_pad( (string) random_int( 0, 999999 ), 6, '0', STR_PAD_LEFT );
$site_name = get_bloginfo( 'name' );
$default_subject = '[' . $site_name . '] ' . __( '邮箱验证码', 'aksrt' );
$default_content = sprintf(
__( "您好 %s,\n\n您正在进行敏感操作验证,验证码为:\n\n【%s】\n\n验证码有效期为 5 分钟。如非本人操作,请忽略此邮件。", 'aksrt' ),
$current_user->display_name,
$code
);
$sent = class_exists( 'AKSRT_Settings' )
? AKSRT_Settings::send_event_email(
'change_code', $target_email, $default_subject, $default_content,
[ '{username}' => $current_user->display_name, '{site_name}' => $site_name, '{code}' => $code ]
)
: wp_mail( $target_email, $default_subject, $default_content, [ 'Content-Type: text/plain; charset=UTF-8' ] );
if ( ! $sent ) {
delete_transient( $rate_key );
error_log( '[AKSRT] 验证码邮件发送失败 user_id=' . $current_user->ID . ' email=' . $target_email . ' scene=' . $scene );
wp_send_json_error( array( 'message' => __( '验证码邮件发送失败,请稍后重试', 'aksrt' ) ) );
}
aksrt_store_email_code( $current_user->ID, $scene, $code, $target_email );
wp_send_json_success( array( 'message' => $success_message ) );
});
// ============================================================
// AJAX: 修改密码
// ============================================================
add_action('wp_ajax_aksrt_change_password', function () {
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
}
$old_password = isset( $_POST['old_password'] ) ? trim( wp_unslash( $_POST['old_password'] ) ) : '';
$new_password = isset( $_POST['new_password'] ) ? trim( wp_unslash( $_POST['new_password'] ) ) : '';
$new_password_confirm = isset( $_POST['new_password_confirm'] ) ? trim( wp_unslash( $_POST['new_password_confirm'] ) ) : '';
$email_code = isset( $_POST['email_code'] ) ? sanitize_text_field( $_POST['email_code'] ) : '';
// 验证原密码
$user = get_userdata( $user_id );
if ( ! $user || ! wp_check_password( $old_password, $user->data->user_pass, $user_id ) ) {
wp_send_json_error( array( 'message' => __( '原密码不正确', 'aksrt' ) ) );
}
// 验证新密码强度
if ( mb_strlen( $new_password ) < 6 ) {
wp_send_json_error( array( 'message' => __( '新密码长度至少6位', 'aksrt' ) ) );
}
if ( mb_strlen( $new_password ) > 128 ) {
wp_send_json_error( array( 'message' => __( '新密码过长', 'aksrt' ) ) );
}
if ( $new_password !== $new_password_confirm ) {
wp_send_json_error( array( 'message' => __( '两次输入的新密码不一致', 'aksrt' ) ) );
}
// 人机验证
if ( function_exists( 'aksrt_require_captcha' ) ) {
$captcha_result = aksrt_require_captcha( 'change_password', wp_unslash( $_POST ) );
if ( $captcha_result !== true ) {
wp_send_json_error( array( 'message' => $captcha_result ) );
}
}
// 验证邮箱验证码
if ( ! aksrt_consume_email_code( $user_id, 'change_password', $email_code, $user->user_email ) ) {
wp_send_json_error( array( 'message' => __( '邮箱验证码错误或已过期', 'aksrt' ) ) );
}
// 设置新密码
wp_set_password( $new_password, $user_id );
if ( class_exists( 'WP_Session_Tokens' ) ) {
WP_Session_Tokens::get_instance( $user_id )->destroy_all();
}
// 清除登录态
wp_clear_auth_cookie();
wp_send_json_success( array(
'message' => __( '密码修改成功,即将跳转到登录页…', 'aksrt' ),
'redirect_to' => aksrt_get_login_url(),
));
});
// ============================================================
// AJAX: 更改邮箱
// ============================================================
add_action('wp_ajax_aksrt_change_email', function () {
if ( ! isset( $_POST['_settings_nonce'] ) || ! wp_verify_nonce( $_POST['_settings_nonce'], 'aksrt_account_settings' ) ) {
wp_send_json_error( array( 'message' => __( '安全验证失败,请刷新页面重试', 'aksrt' ) ) );
}
$user_id = get_current_user_id();
if ( ! $user_id ) {
wp_send_json_error( array( 'message' => __( '请先登录', 'aksrt' ) ) );
}
$current_password = isset( $_POST['current_password'] ) ? trim( wp_unslash( $_POST['current_password'] ) ) : '';
$new_email = isset( $_POST['new_email'] ) ? trim( wp_unslash( $_POST['new_email'] ) ) : '';
$email_code = isset( $_POST['email_code'] ) ? sanitize_text_field( $_POST['email_code'] ) : '';
$current_user = wp_get_current_user();
if ( empty( $current_password ) || ! wp_check_password( $current_password, $current_user->user_pass, $current_user->ID ) ) {
wp_send_json_error( array( 'message' => __( '当前密码不正确', 'aksrt' ) ) );
}
// 验证新邮箱格式
if ( empty( $new_email ) || ! is_email( $new_email ) ) {
wp_send_json_error( array( 'message' => __( '请输入有效的邮箱地址', 'aksrt' ) ) );
}
if ( strtolower( $new_email ) === strtolower( $current_user->user_email ) ) {
wp_send_json_error( array( 'message' => __( '新邮箱与当前邮箱相同', 'aksrt' ) ) );
}
// 检查邮箱是否已被使用
if ( email_exists( $new_email ) ) {
wp_send_json_error( array( 'message' => __( '该邮箱已被其他账号使用', 'aksrt' ) ) );
}
// 人机验证
if ( function_exists( 'aksrt_require_captcha' ) ) {
$captcha_result = aksrt_require_captcha( 'change_email', wp_unslash( $_POST ) );
if ( $captcha_result !== true ) {
wp_send_json_error( array( 'message' => $captcha_result ) );
}
}
// 验证邮箱验证码
if ( ! aksrt_consume_email_code( $user_id, 'change_email', $email_code, $new_email ) ) {
wp_send_json_error( array( 'message' => __( '邮箱验证码错误或已过期', 'aksrt' ) ) );
}
// 更新邮箱
$old_email = $current_user->user_email;
$update_result = wp_update_user( [
'ID' => $user_id,
'user_email' => $new_email,
] );
if ( is_wp_error( $update_result ) ) {
wp_send_json_error( array( 'message' => $update_result->get_error_message() ?: __( '邮箱更新失败,请稍后重试', 'aksrt' ) ) );
}
if ( is_email( $old_email ) ) {
$site_name = get_bloginfo( 'name' );
$default_subject = '[' . $site_name . '] ' . __( '账号邮箱已变更', 'aksrt' );
$default_content = sprintf(
__( "您好 %s,\n\n您的账号邮箱已成功修改为:%s\n\n如果这不是您的操作,请立即联系站点管理员。", 'aksrt' ),
$current_user->display_name,
$new_email
);
if ( class_exists( 'AKSRT_Settings' ) ) {
AKSRT_Settings::send_event_email(
'change_notice', $old_email, $default_subject, $default_content,
[ '{username}' => $current_user->display_name, '{site_name}' => $site_name, '{new_email}' => $new_email ]
);
} else {
wp_mail( $old_email, $default_subject, $default_content, [ 'Content-Type: text/plain; charset=UTF-8' ] );
}
}
// ====== 邮箱变更后重置验证状态(阶段 5)======
// 1. 写入 14 天回滚窗口 meta(管理员可在此期内还原旧邮箱)
// 2. 把 aksrt_email_verified 重置为 0
// 3. 若功能启用 → 立即给新邮箱发送验证邮件
update_user_meta( $user_id, 'aksrt_email_previous', $old_email );
update_user_meta( $user_id, 'aksrt_email_changed_at', time() );
$verify_url = '';
$verify_message = '';
if ( class_exists( 'AKSRT_Settings' ) && class_exists( 'AKSRT_Auth_Service' )
&& AKSRT_Settings::registration_requires_email_verification() ) {
update_user_meta( $user_id, 'aksrt_email_verified', 0 );
// 用户对自身的 edit_user 权限映射到 'read',因此 admin_send_verification 会通过权限校验。
$verify_result = AKSRT_Auth_Service::admin_send_verification( $user_id, $user_id );
if ( ! empty( $verify_result['success'] ) ) {
$verify_url = function_exists('aksrt_get_verify_email_url') ? aksrt_get_verify_email_url( $user_id ) : '';
$verify_message = __( '请前往新邮箱完成验证,否则下次登录将被拦截。', 'aksrt' );
}
}
if ( class_exists( 'WP_Session_Tokens' ) ) {
WP_Session_Tokens::get_instance( $user_id )->destroy_all();
}
// 清除登录态
wp_clear_auth_cookie();
wp_send_json_success( array(
'message' => $verify_message !== ''
? __( '邮箱修改成功,请前往新邮箱完成验证后重新登录…', 'aksrt' )
: __( '邮箱修改成功,即将跳转到登录页…', 'aksrt' ),
'redirect_to' => $verify_url !== '' ? $verify_url : aksrt_get_login_url(),
'verify_url' => $verify_url,
));
});
@@ -0,0 +1,181 @@
<?php
/**
* User rewrite/routing/bootstrap flush + author redirects
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
// ============================================================
// 三、URL 路由与模板加载
// ============================================================
// --- 3a. /user/{昵称} 自定义重写 ---
add_filter('query_vars', function ($vars) {
$vars[] = 'aksrt_user_center';
$vars[] = 'user_nicename';
return $vars;
});
add_action('template_redirect', function () {
if (is_admin() || wp_doing_ajax() || is_feed() || is_embed() || is_preview()) {
return;
}
$resolved_path = aksrt_get_current_special_template_path();
if ($resolved_path === '') {
return;
}
status_header(200);
load_template($resolved_path, true);
exit;
}, 100);
add_action('init', function () {
add_rewrite_tag('%aksrt_user_center%', '([^&]+)');
add_rewrite_tag('%user_nicename%', '([^/]+)');
add_rewrite_rule('^profile/?$', 'index.php?aksrt_user_center=1', 'top');
add_rewrite_rule('^user/([^/]+)/?$', 'index.php?user_nicename=$matches[1]', 'top');
});
// --- 3b. 统一模板拦截器(核心)---
// 拦截所有使用主题特殊模板的页面请求,确保正确加载
add_filter('template_include', function ($template) {
$resolved_path = aksrt_get_current_special_template_path();
if ($resolved_path !== '') {
return $resolved_path;
}
return $template;
global $_aksrt_special_templates;
// A) /user/{昵称} 路由 → 加载公开主页模板
$nicename = get_query_var('user_nicename');
if (!empty($nicename)) {
$user = get_user_by('slug', $nicename);
if (!$user) return $template; // 不存在的用户 → 默认 404
$tpl = aksrt_locate_template_file('templates/pages/page-user-home.php');
if (!$tpl) {
// 兜底:如果新模板不存在,回退到旧 profile 模板
$tpl = aksrt_locate_template_file($_aksrt_special_templates['profile']['file']);
}
if (!$tpl) return $template;
global $aksrt_profile_user;
$aksrt_profile_user = $user;
return $tpl;
}
// B) 普通页面 → 检测是否使用了我们的特殊模板,确保加载正确的模板文件
// (防止 WP 在某些情况下未能正确解析自定义页面模板)
if (is_page()) {
$page_id = get_queried_object_id();
if ($page_id) {
$page_template = get_post_meta($page_id, '_wp_page_template', true);
if ($page_template && strpos($page_template, 'templates/pages/') === 0) {
$tpl = aksrt_locate_template_file($page_template);
if ($tpl) return $tpl;
}
}
}
return $template;
}, 99);
add_filter('template_include', function ($template) {
$resolved_path = aksrt_get_current_special_template_path();
if ($resolved_path !== '') {
return $resolved_path;
}
if (get_query_var('aksrt_user_center')) {
$profile_template = aksrt_locate_template_file('templates/pages/page-profile.php');
if ($profile_template) {
return $profile_template;
}
}
if (is_page()) {
$page_id = get_queried_object_id();
if ($page_id > 0) {
$page_template = get_post_meta($page_id, '_wp_page_template', true);
$resolved_template = aksrt_resolve_page_template_file(
is_string($page_template) ? $page_template : '',
(string) get_post_field('post_name', $page_id)
);
if ($resolved_template !== '') {
$resolved_path = aksrt_locate_template_file($resolved_template);
if ($resolved_path) {
return $resolved_path;
}
}
}
}
return $template;
}, 100);
// ============================================================
// 四、自动维护 + flush
// ============================================================
add_action('init', function () {
global $_aksrt_special_templates;
// 后台/AJAX:检查并创建缺失页面
$pages_bootstrapped = get_option('aksrt_template_pages_bootstrapped');
$requested_template_key = (!is_admin() && !wp_doing_ajax()) ? aksrt_get_requested_template_key() : '';
if (is_admin() || wp_doing_ajax() || $pages_bootstrapped !== AKSRT_VERSION) {
foreach ($_aksrt_special_templates as $key => $cfg) {
if ($key === 'profile') {
continue;
}
aksrt_ensure_template_page($key);
}
if ($pages_bootstrapped !== AKSRT_VERSION) {
update_option('aksrt_template_pages_bootstrapped', AKSRT_VERSION);
}
} elseif ($requested_template_key !== '' && $requested_template_key !== 'profile') {
aksrt_ensure_template_page($requested_template_key);
}
// 统一 flush 检查
$rewrite_version = AKSRT_VERSION . ':sign-profile-routing-v1';
$flushed = get_option('aksrt_user_rules_flushed');
if ($flushed !== $rewrite_version) {
flush_rewrite_rules(false);
update_option('aksrt_user_rules_flushed', $rewrite_version);
}
});
// 页面变更时清缓存
add_action('save_post', function ($pid) { if (get_post_type($pid) === 'page') aksrt_clear_template_page_cache(); });
add_action('delete_post', function ($pid) { if (get_post_type($pid) === 'page') aksrt_clear_template_page_cache(); });
// ============================================================
// 六、作者链接改写
// ============================================================
add_filter('author_link', function ($link, $author_id) {
$user = get_userdata($author_id);
$slug = $user ? $user->user_nicename : 'user-' . $author_id;
return home_url('/user/' . trailingslashit($slug));
}, 10, 2);
add_action('template_redirect', function () {
if (!is_author()) return;
$author_id = get_queried_object_id();
if ($author_id) {
$user = get_userdata($author_id);
$slug = $user ? $user->user_nicename : 'user-' . $author_id;
wp_safe_redirect(home_url('/user/' . trailingslashit($slug)), 301);
exit;
}
});
// ============================================================
// AJAX: 保存用户个人资料(显示名称、简介、联系方式)
@@ -0,0 +1,492 @@
<?php
/**
* User template registry + template utils
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
// ============================================================
// 一、模板注册表 — 所有特殊页面的"身份证"
// 修改此处即可增删模板页面,无需改动其他逻辑
// ============================================================
$_aksrt_special_templates = [
'login' => [
'file' => 'templates/pages/page-login.php',
'slug' => 'login',
'title' => '登录',
'fallback' => '/login/',
],
'register' => [
'file' => 'templates/pages/page-register.php',
'slug' => 'register',
'title' => '注册',
'fallback' => '/register/',
],
'bind' => [
'file' => 'templates/pages/page-bind-account.php',
'slug' => 'bind-account',
'title' => '绑定账号',
'fallback' => '/bind-account/',
],
'reset-password' => [
'file' => 'templates/pages/page-reset-password.php',
'slug' => 'reset-password',
'title' => '重置密码',
'fallback' => '/reset-password/',
],
'verify-email' => [
'file' => 'templates/pages/page-verify-email.php',
'slug' => 'verify-email',
'title' => '邮箱验证',
'fallback' => '/verify-email/',
],
'categories' => [
'file' => 'templates/pages/page-categories.php',
'slug' => 'categories',
'title' => '分类目录',
'fallback' => '/categories/',
],
'friends' => [
'file' => 'templates/pages/page-friends.php',
'slug' => 'friends',
'title' => '友情链接',
'fallback' => '/friends/',
],
'profile' => [
'file' => 'templates/pages/page-profile.php',
'slug' => 'profile',
'title' => '个人中心',
'fallback' => '/',
],
];
// Zibll-style v2 routing keeps only true fixed template pages in the registry.
$_aksrt_special_templates = [
'sign' => [
'file' => 'templates/pages/page-sign.php',
'slug' => 'sign',
'title' => '登录/注册',
'fallback' => '/sign/',
],
'bind' => [
'file' => 'templates/pages/page-bind-account.php',
'slug' => 'bind-account',
'title' => '绑定账号',
'fallback' => '/bind-account/',
],
'reset-password' => [
'file' => 'templates/pages/page-reset-password.php',
'slug' => 'reset-password',
'title' => '重置密码',
'fallback' => '/reset-password/',
],
'verify-email' => [
'file' => 'templates/pages/page-verify-email.php',
'slug' => 'verify-email',
'title' => '邮箱验证',
'fallback' => '/verify-email/',
],
'categories' => [
'file' => 'templates/pages/page-categories.php',
'slug' => 'categories',
'title' => '分类目录',
'fallback' => '/categories/',
],
'friends' => [
'file' => 'templates/pages/page-friends.php',
'slug' => 'friends',
'title' => '友情链接',
'fallback' => '/friends/',
],
'profile' => [
'file' => 'templates/pages/page-profile.php',
'slug' => 'profile',
'title' => '个人中心',
'fallback' => '/profile/',
],
];
/** @var array 缓存 key=模板文件路径 => value=WP_Post|null */
$_aksrt_template_page_cache = [];
// ============================================================
// 二、核心工具函数
// ============================================================
/**
* 查找使用指定模板文件的页面(按模板匹配,自适应任意 slug)
*
* @param string $template_file 相对于主题的模板路径
* @return WP_Post|null
*/
function aksrt_find_template_page(string $template_file) {
global $_aksrt_template_page_cache;
if (isset($_aksrt_template_page_cache[$template_file])) {
return $_aksrt_template_page_cache[$template_file];
}
$page = null;
$pages = get_pages([
'meta_key' => '_wp_page_template',
'meta_value' => $template_file,
'number' => 1,
'post_status' => 'publish',
]);
if (!empty($pages)) {
$page = $pages[0];
}
$_aksrt_template_page_cache[$template_file] = $page;
return $page;
}
/**
* 获取使用指定模板的页面 URL
*
* @param string $key 注册表 key(当前仅 profile 使用)
* @return string 页面 URL 或回退地址
*/
function aksrt_get_template_url(string $key): string {
global $_aksrt_special_templates;
if (!isset($_aksrt_special_templates[$key])) {
return home_url('/');
}
$cfg = $_aksrt_special_templates[$key];
$page = aksrt_find_template_page($cfg['file']);
if (!$page) {
aksrt_ensure_template_page($key);
$page = aksrt_find_template_page($cfg['file']);
}
if ($page) {
return get_permalink($page->ID);
}
return home_url($cfg['fallback']);
}
/**
* 定位模板文件的绝对路径
*
* @param string $template_file 相对于主题的路径
* @return string|false 文件绝对路径或 false
*/
function aksrt_locate_template_file(string $template_file) {
$path = locate_template([$template_file]);
if ($path) return $path;
$full = AKSRT_DIR . '/' . $template_file;
return file_exists($full) ? $full : false;
}
/**
* Resolve a public-facing page template file from stored meta or page slug.
*/
function aksrt_resolve_page_template_file(string $template_file = '', string $post_slug = ''): string {
$template_file = ltrim($template_file, '/\\');
$post_slug = sanitize_title($post_slug);
$legacy_auth_templates = [
'templates/pages/page-login.php' => 'login',
'templates/pages/page-register.php' => 'register',
'templates/pages/page-bind-account.php' => 'bind',
'templates/pages/page-reset-password.php' => 'reset-password',
'templates/pages/page-verify-email.php' => 'verify-email',
];
if ($template_file !== '') {
if ($template_file === 'templates/pages/page-sign.php') {
return aksrt_get_auth_template_file(aksrt_get_auth_tab(), true);
}
if (!empty($legacy_auth_templates[$template_file])) {
return aksrt_get_auth_template_file((string) $legacy_auth_templates[$template_file], true);
}
if (strpos($template_file, 'templates/pages/') === 0 && aksrt_locate_template_file($template_file)) {
return $template_file;
}
}
if ($post_slug !== '') {
$auth_slug_map = [
'sign' => aksrt_get_auth_tab(),
'login' => 'login',
'register' => 'register',
'bind-account' => 'bind',
'reset-password' => 'reset-password',
'verify-email' => 'verify-email',
];
if (!empty($auth_slug_map[$post_slug])) {
return aksrt_get_auth_template_file((string) $auth_slug_map[$post_slug], true);
}
$slug_map = [
'categories' => 'templates/pages/page-categories.php',
'friends' => 'templates/pages/page-friends.php',
'profile' => 'templates/pages/page-profile.php',
];
if (!empty($slug_map[$post_slug]) && aksrt_locate_template_file($slug_map[$post_slug])) {
return $slug_map[$post_slug];
}
}
return '';
}
/**
* Normalize auth view keys used by the unified sign page.
*/
function aksrt_normalize_auth_tab(string $tab): string {
$tab = sanitize_key($tab);
switch ($tab) {
case 'signin':
case 'login':
return 'login';
case 'signup':
case 'register':
return 'register';
case 'bind':
case 'bind-account':
return 'bind';
case 'reset':
case 'lostpassword':
case 'reset-password':
return 'reset-password';
case 'verify':
case 'verify-email':
return 'verify-email';
default:
return 'login';
}
}
/**
* Whether an auth tab should be rendered by the unified sign page.
*/
function aksrt_auth_tab_uses_unified_sign_page(string $tab): bool {
return in_array(
aksrt_normalize_auth_tab($tab),
['login', 'register'],
true
);
}
/**
* Resolve the auth template file, preferring the unified sign page when present.
*/
function aksrt_get_auth_template_file(string $tab = 'login', bool $prefer_sign_page = true): string {
$normalized_tab = aksrt_normalize_auth_tab($tab);
$sign_template = 'templates/pages/page-sign.php';
if (
$prefer_sign_page
&& aksrt_auth_tab_uses_unified_sign_page($normalized_tab)
&& aksrt_locate_template_file($sign_template)
) {
return $sign_template;
}
switch ($normalized_tab) {
case 'register':
return 'templates/pages/page-register.php';
case 'bind':
return 'templates/pages/page-bind-account.php';
case 'reset-password':
return 'templates/pages/page-reset-password.php';
case 'verify-email':
return 'templates/pages/page-verify-email.php';
default:
return 'templates/pages/page-login.php';
}
}
/**
* Return the active auth view for sign routes and the sign page template.
*/
function aksrt_get_auth_tab(): string {
$query_tab = get_query_var('aksrt_auth_tab');
if (is_string($query_tab) && $query_tab !== '') {
return aksrt_normalize_auth_tab($query_tab);
}
if (!empty($_GET['tab'])) {
return aksrt_normalize_auth_tab((string) wp_unslash($_GET['tab']));
}
$post = get_queried_object();
if ($post instanceof WP_Post) {
return aksrt_normalize_auth_tab((string) $post->post_name);
}
return 'login';
}
/**
* 清除模板页面缓存
*/
function aksrt_clear_template_page_cache(): void {
global $_aksrt_template_page_cache;
$_aksrt_template_page_cache = [];
}
/**
* Prepare the public user-home template context and return its template path.
*/
function aksrt_get_user_home_template_path(): string {
global $_aksrt_special_templates, $aksrt_profile_user;
$nicename = (string) get_query_var('user_nicename');
if ($nicename === '') {
return '';
}
$user = get_user_by('slug', $nicename);
if (!$user instanceof WP_User) {
return '';
}
$template_path = aksrt_locate_template_file('templates/pages/page-user-home.php');
if (!$template_path) {
return '';
}
$aksrt_profile_user = $user;
return $template_path;
}
/**
* Resolve the active special-page template path for the current request.
*/
function aksrt_get_current_special_template_path(): string {
if (get_query_var('user_nicename')) {
return aksrt_get_user_home_template_path();
}
if (get_query_var('aksrt_user_center')) {
$profile_template = aksrt_locate_template_file('templates/pages/page-profile.php');
return $profile_template ?: '';
}
if (get_query_var('aksrt_sign')) {
$auth_template = aksrt_get_auth_template_file((string) get_query_var('aksrt_auth_tab'), true);
$auth_template_path = aksrt_locate_template_file($auth_template);
return $auth_template_path ?: '';
}
if (is_page()) {
$page_id = get_queried_object_id();
if ($page_id > 0) {
$page_template = get_post_meta($page_id, '_wp_page_template', true);
$resolved_template = aksrt_resolve_page_template_file(
is_string($page_template) ? $page_template : '',
(string) get_post_field('post_name', $page_id)
);
if ($resolved_template !== '') {
$resolved_path = aksrt_locate_template_file($resolved_template);
if ($resolved_path) {
return $resolved_path;
}
}
}
}
return '';
}
/**
* 根据当前请求路径识别系统模板页 key。
*
* 用于前台直接访问固定 slug 时的自动补页。
*/
function aksrt_get_requested_template_key(): string {
global $_aksrt_special_templates;
$request_uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
if ($request_uri === '') {
return '';
}
$request_path = wp_parse_url($request_uri, PHP_URL_PATH);
if (!is_string($request_path) || $request_path === '') {
return '';
}
$home_path = (string) wp_parse_url(home_url('/'), PHP_URL_PATH);
if ($home_path !== '' && str_starts_with($request_path, $home_path)) {
$request_path = (string) substr($request_path, strlen($home_path));
}
$request_slug = trim($request_path, '/');
if ($request_slug === '') {
return '';
}
foreach ($_aksrt_special_templates as $key => $cfg) {
$slug = isset($cfg['slug']) ? sanitize_title((string) $cfg['slug']) : '';
if ($slug !== '' && $slug === $request_slug) {
return (string) $key;
}
}
return '';
}
/**
* 确保使用指定模板的页面存在,缺失则自动创建
*
* @param string $key 注册表 key
*/
function aksrt_ensure_template_page(string $key): void {
global $_aksrt_special_templates;
if (!isset($_aksrt_special_templates[$key])) return;
$cfg = $_aksrt_special_templates[$key];
$desired_slug = sanitize_title($cfg['slug'] ?? $key);
// 已有页面使用此模板?
if (aksrt_find_template_page($cfg['file'])) return;
if ($desired_slug !== '') {
$existing_page = get_page_by_path($desired_slug, OBJECT, 'page');
if ($existing_page instanceof WP_Post) {
if ($existing_page->post_status !== 'publish') {
wp_update_post([
'ID' => $existing_page->ID,
'post_status' => 'publish',
]);
}
update_post_meta($existing_page->ID, '_wp_page_template', $cfg['file']);
aksrt_clear_template_page_cache();
return;
}
}
// 自动创建
$page_id = wp_insert_post([
'post_title' => $cfg['title'],
'post_content' => '',
'post_status' => 'publish',
'post_type' => 'page',
'post_name' => $desired_slug !== '' ? $desired_slug : $key,
'comment_status' => 'closed',
'ping_status' => 'closed',
]);
if (!is_wp_error($page_id) && $page_id > 0) {
update_post_meta($page_id, '_wp_page_template', $cfg['file']);
aksrt_clear_template_page_cache();
}
}
@@ -0,0 +1,267 @@
<?php
/**
* User public URL/page API
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
// ============================================================
// 五、公开 API 函数(兼容旧代码调用)
// ============================================================
if (!function_exists('aksrt_get_user_center_url')) :
/**
* 获取本人中心 URL(私有空间入口)
*
* @param string $tab 可选 tab 参数
* @return string
*/
function aksrt_get_user_center_url(string $tab = ''): string {
if (get_option('permalink_structure')) {
$url = home_url('/profile/');
} else {
$url = add_query_arg('aksrt_user_center', '1', home_url('/'));
}
if ($tab !== '') {
$url = add_query_arg('tab', $tab, $url);
}
return $url;
}
endif;
if (!function_exists('aksrt_get_orders_page_url')) :
/**
* 订单中心页面 URL(模板 templates/pages/page-orders.php)。
* 未指派页面时返回空串,调用方应隐藏入口。
*/
function aksrt_get_orders_page_url(): string {
return function_exists('aksrt_get_page_url_by_template')
? aksrt_get_page_url_by_template('templates/pages/page-orders.php')
: '';
}
endif;
if (!function_exists('aksrt_get_cart_page_url')) :
/**
* 购物车页面 URL(模板 templates/pages/page-cart.php)。
*/
function aksrt_get_cart_page_url(): string {
return function_exists('aksrt_get_page_url_by_template')
? aksrt_get_page_url_by_template('templates/pages/page-cart.php')
: '';
}
endif;
if (!function_exists('aksrt_get_revenue_page_url')) :
/**
* 收益中心页面 URL(模板 templates/pages/page-revenue.php)。
*/
function aksrt_get_revenue_page_url(): string {
return function_exists('aksrt_get_page_url_by_template')
? aksrt_get_page_url_by_template('templates/pages/page-revenue.php')
: '';
}
endif;
if (!function_exists('aksrt_get_messages_page_url')) :
/**
* 消息中心页面 URL(模板 templates/pages/page-messages.php)。
*/
function aksrt_get_messages_page_url(): string {
return function_exists('aksrt_get_page_url_by_template')
? aksrt_get_page_url_by_template('templates/pages/page-messages.php')
: '';
}
endif;
if (!function_exists('aksrt_get_user_home_url')) :
/**
* 获取公开用户主页 URL(/user/{nicename}/)
*
* @param WP_User|int|string|null $user 用户对象、ID、nicename 或 null(当前用户)
* @return string
*/
function aksrt_get_user_home_url($user = null): string {
$slug = '';
if ($user instanceof WP_User) {
$slug = $user->user_nicename;
} elseif (is_int($user) && $user > 0) {
$u = get_userdata($user);
$slug = $u ? $u->user_nicename : '';
} elseif (is_string($user) && $user) {
$slug = $user;
} else {
$current = wp_get_current_user();
$slug = $current->ID ? $current->user_nicename : '';
}
if (!$slug) return home_url('/');
return home_url('/user/' . trailingslashit($slug));
}
endif;
if (!function_exists('aksrt_is_user_center_page')) :
/**
* 判断当前是否为本人中心页面
*/
function aksrt_is_user_center_page(): bool {
return (bool) get_query_var('aksrt_user_center')
|| (is_page_template('templates/pages/page-profile.php') && !(bool) get_query_var('user_nicename'));
}
endif;
if (!function_exists('aksrt_is_user_home_page')) :
/**
* 判断当前是否为公开用户主页
*/
function aksrt_is_user_home_page(): bool {
return (bool) get_query_var('user_nicename')
|| is_page_template('templates/pages/page-user-home.php');
}
endif;
if (!function_exists('aksrt_get_profile_url')) :
/**
* 兼容层:获取用户主页 URL
*
* @deprecated 使用 aksrt_get_user_center_url() 或 aksrt_get_user_home_url() 替代
* @param WP_User|int|string|null $user
* @return string
*/
function aksrt_get_profile_url($user = null): string {
// 无参调用 → 当前用户主页(公开页)
if ($user === null) {
return aksrt_get_user_home_url();
}
return aksrt_get_user_home_url($user);
}
endif;
if (!function_exists('aksrt_get_sign_page_url')) :
/**
* Get the unified auth page URL, with legacy route fallback while the page self-heals.
*
* @param string $tab
* @param array<string, scalar> $args
*/
function aksrt_get_sign_page_url(string $tab = 'login', array $args = []): string {
$tab = aksrt_normalize_auth_tab($tab);
$legacy_fallbacks = [
'login' => home_url('/login/'),
'register' => home_url('/register/'),
'bind' => home_url('/bind-account/'),
'reset-password' => home_url('/reset-password/'),
'verify-email' => home_url('/verify-email/'),
];
$page = aksrt_find_template_page('templates/pages/page-sign.php');
if (!$page) {
aksrt_ensure_template_page('sign');
$page = aksrt_find_template_page('templates/pages/page-sign.php');
}
if ($page instanceof WP_Post) {
$url = get_permalink($page->ID);
$args = ['tab' => $tab] + $args;
return add_query_arg($args, $url);
}
$url = $legacy_fallbacks[$tab] ?? home_url('/login/');
return !empty($args) ? add_query_arg($args, $url) : $url;
}
/**
* Determine whether the current request is served by the unified auth experience.
*/
function aksrt_is_auth_page_request(): bool {
return (bool) get_query_var('aksrt_sign')
|| is_page_template('templates/pages/page-sign.php')
|| is_page_template('templates/pages/page-login.php')
|| is_page_template('templates/pages/page-register.php')
|| is_page_template('templates/pages/page-bind-account.php')
|| is_page_template('templates/pages/page-reset-password.php')
|| is_page_template('templates/pages/page-verify-email.php');
}
endif;
if (!function_exists('aksrt_get_login_url')) :
/** 获取登录页 URL */
function aksrt_get_login_url($redirect = '') {
$url = aksrt_get_sign_page_url('login');
if ($redirect) {
$url = add_query_arg('redirect_to', urlencode($redirect), $url);
}
return $url;
}
endif;
if (!function_exists('aksrt_get_register_url')) :
/** 获取注册页 URL */
function aksrt_get_register_url() {
return aksrt_get_sign_page_url('register');
}
endif;
if (!function_exists('aksrt_get_bind_url')) :
/** 获取绑定账号页面 URL */
function aksrt_get_bind_url($transient_key = '') {
$url = aksrt_get_template_url('bind');
if ($transient_key) {
$url = add_query_arg('obk', $transient_key, $url);
}
return $url;
}
endif;
if (!function_exists('aksrt_get_logout_url')) :
/** 获取退出登录 URL */
function aksrt_get_logout_url($redirect = '') {
$target = $redirect ?: home_url('/');
$url = add_query_arg('redirect_to', rawurlencode($target), home_url('/logout/'));
return wp_nonce_url($url, 'aksrt-logout');
}
endif;
if (!function_exists('aksrt_get_reset_password_url')) :
/**
* 获取重置密码页面 URL
*
* @param string $token 主题重置 token(可选,设置密码时需要)
* @param int $user_id 用户 ID(可选,设置密码时需要)
* @return string
*/
function aksrt_get_reset_password_url(string $token = '', int $user_id = 0): string {
$url = aksrt_get_template_url('reset-password');
if ($token !== '' && $user_id > 0) {
$url = add_query_arg([
'token' => $token,
'uid' => $user_id,
], $url);
}
return $url;
}
endif;
if (!function_exists('aksrt_get_verify_email_url')) :
/** 获取邮箱验证页面 URL */
function aksrt_get_verify_email_url(int $user_id = 0, array $args = []): string {
$url = aksrt_get_template_url('verify-email');
if ($user_id > 0) {
$args['uid'] = $user_id;
}
if (!empty($args)) {
$url = add_query_arg($args, $url);
}
return $url;
}
endif;
// Notifications live in the same user/ domain; bootstrap also loads it.
if (!class_exists('AKSRT_User_Notifications', false) && file_exists(__DIR__ . '/class-aksrt-user-notifications.php')) {
require_once __DIR__ . '/class-aksrt-user-notifications.php';
}
+37
View File
@@ -0,0 +1,37 @@
# ADR: Domain folder placement for `inc/classes`
## Status
Accepted (2026-07-17)
## Context
After moving root class files into domain folders, freeze placement for boundary types so later moves are intentional.
## Decision
| Concern | Domain | Why |
|---------|--------|-----|
| Theme shell (setup, scripts, templates, SEO, meta boxes, DB migrator, front-submit, docs presenter, home modules, stats, redirects, update checker, comment actions) | `core/` | Shared CMS plumbing |
| SPA shell + controllers + schema + native ops | `admin/` | Dual-surface IA |
| Settings facade + runtime delegates | `settings/` | Single public settings API |
| Auth, OAuth, captcha, moderation, email rollback | `auth/` | Identity & access |
| User center/home/badges/level/metrics/notifications/follow | `user/` | Account surface |
| Credits, balance wallet, signin, invite, points-free | `credits/` | Points + cash wallet |
| Download tokens/state, content pay, VIP, visibility | `content/` | Post-centric paid content |
| Topic pay, plate follow pay | `forum/` | Forum SKUs (not generic content pay) |
| Orders, gateways, coupon, charge-card, revenue, creator-revenue, withdrawal, rebate, discount, after-sale, shipping, wechat-notify | `payment/` | Commerce + settlement |
| Shop services/meta/reviews | `shop/` | Product catalog / cart |
| SMS stack | `sms/` | Messaging channel |
| AI runtime (admin AI controller stays in `admin/`) | `ai/` | Generation helpers |
| Message, private message, site notice, report center | `messages/` | In-site messaging |
| MeiliSearch, search-push, ip-geo, content-audit, api-audit | `extensions/` | Optional integrations |
| Widgets + registry | `widgets/` | WP widgets |
| Inline style / custom code / loading overlay | `frontend/` | Asset render helpers |
### Rules
- Traits live next to owner: `{domain}/{slug}-traits/`.
- Loader: manual `require_once` in `inc/bootstrap.php` only; traits before shells.
- Do not move `topic-pay` / `plate-follow-pay` into `content/` without updating this ADR.
- `creator-revenue` and `wechat-notify` stay under `payment/`.
## Out of scope
PSR-4 class namespaces; class renames.