From d57fc6f934f45884a4066b41cb24879b46f91f58 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 16 Jul 2026 15:38:55 +0800 Subject: [PATCH] feat(user-center): points buy, balance withdraw, and asset record tabs Add cash purchase of points (points_pay_s), wallet withdraw requests via AKSRT_Withdrawal, Zibll-style dual-card layout with tabbed balance/points ledgers, and admin switches for buy-points packages and balance withdraw. --- .../src/schema/sections/payment/recharge.js | 11 + .../src/schema/sections/users/credits.js | 8 +- assets/css/routes/user-center/credits.css | 33 ++ assets/js/credits.js | 149 +++++++ inc/classes/class-aksrt-balance-service.php | 398 +++++++++++++++++- inc/classes/class-aksrt-balance.php | 2 + inc/helpers/aksrt-domain-model.php | 2 + .../user-center-renderers/credits-balance.php | 62 ++- .../user-center-renderers/credits-wallet.php | 34 ++ .../user-center-renderers/profile-credits.php | 97 ++++- 10 files changed, 780 insertions(+), 16 deletions(-) diff --git a/assets/admin/src/schema/sections/payment/recharge.js b/assets/admin/src/schema/sections/payment/recharge.js index 6c74626b..fd4a2223 100644 --- a/assets/admin/src/schema/sections/payment/recharge.js +++ b/assets/admin/src/schema/sections/payment/recharge.js @@ -23,6 +23,8 @@ export const rechargeSchema = { pay_balance_exchange_s: false, pay_balance_exchange_rate: 100, pay_balance_exchange_desc: '', + pay_balance_withdraw_s: false, + pay_balance_withdraw_lowest_money: 50, }, groups: [ { @@ -86,5 +88,14 @@ export const rechargeSchema = { { key: 'pay_balance_exchange_desc', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_balance_exchange_s', value: true } }, ], }, + { + key: 'withdraw', + title: '余额提现', + icon: 'fas fa-hand-holding-usd', + fields: [ + { key: 'pay_balance_withdraw_s', label: '启用余额提现', type: 'switch', default: false, dependsOn: { key: 'pay_balance_s', value: true } }, + { key: 'pay_balance_withdraw_lowest_money', label: '最低提现金额', type: 'number', default: 50, min: 1, max: 99999, dependsOn: { key: 'pay_balance_withdraw_s', value: true } }, + ], + }, ], } diff --git a/assets/admin/src/schema/sections/users/credits.js b/assets/admin/src/schema/sections/users/credits.js index e8659acb..b9da1ef7 100644 --- a/assets/admin/src/schema/sections/users/credits.js +++ b/assets/admin/src/schema/sections/users/credits.js @@ -38,8 +38,12 @@ export const creditsSchema = { icon: 'fas fa-coins', fields: [ { key: 'points_s', label: '启用积分系统', type: 'switch', default: true }, - { key: 'points_pay_s', label: '积分支付', type: 'switch', default: false }, - { key: 'points_rate', label: '积分兑换比例', type: 'number', default: 100, min: 1, max: 10000 }, + { key: 'points_pay_s', label: '启用购买积分', type: 'switch', default: false, help: '对齐 zibll points_pay_s:允许用户用现金/余额购买积分。' }, + { key: 'points_rate', label: '积分汇率(1元=N积分)', type: 'number', default: 100, min: 1, max: 10000, dependsOn: { key: 'points_pay_s', value: true } }, + { key: 'pay_points_desc', label: '购买积分说明', type: 'textarea', default: '', dependsOn: { key: 'points_pay_s', value: true } }, + { key: 'pay_points_product', label: '积分套餐', type: 'textarea', default: '', help: '每行:支付金额|积分数量|标题。留空则仅自定义数量。', dependsOn: { key: 'points_pay_s', value: true } }, + { key: 'pay_points_product_custom_s', label: '允许自定义积分数量', type: 'switch', default: true, dependsOn: { key: 'points_pay_s', value: true } }, + { key: 'pay_points_product_custom_limit', label: '自定义积分上限', type: 'number', default: 99999, min: 1, max: 999999, dependsOn: { key: 'points_pay_s', value: true } }, { key: 'points_name', label: '积分名称', type: 'input', default: '积分' }, { key: 'points_decimal', label: '积分小数位数', type: 'number', default: 2, min: 0, max: 4 }, ], diff --git a/assets/css/routes/user-center/credits.css b/assets/css/routes/user-center/credits.css index c36e4126..9f29d0c7 100644 --- a/assets/css/routes/user-center/credits.css +++ b/assets/css/routes/user-center/credits.css @@ -282,6 +282,39 @@ body.uc-modal-open { margin-top: 12px; } +/* Asset record tabs (Zibll tab-nav-theme) */ +.uc-asset-tabs { + margin-top: 8px; + padding: 12px 16px 16px; +} +.uc-asset-tabs__nav { + display: flex; + gap: 18px; + list-style: none; + margin: 0 0 12px; + padding: 0 0 10px; + border-bottom: 1px solid var(--color-line-soft, rgba(0,0,0,.06)); +} +.uc-asset-tabs__nav li a { + display: inline-block; + padding: 4px 0; + color: var(--muted-color, #777); + text-decoration: none; + font-weight: 700; + border-bottom: 2px solid transparent; +} +.uc-asset-tabs__nav li.active a, +.uc-asset-tabs__nav li a:hover { + color: var(--focus-color, #f04494); + border-bottom-color: var(--focus-color, #f04494); +} +.uc-asset-tabs__content .tab-pane[hidden] { + display: none !important; +} +.uc-asset-tabs__content .tab-pane.is-active { + display: block; +} + .uc-content #panel-credits .uc-transactions { background: transparent; border: 0; diff --git a/assets/js/credits.js b/assets/js/credits.js index 46ba85e7..36bc8fb8 100644 --- a/assets/js/credits.js +++ b/assets/js/credits.js @@ -775,6 +775,152 @@ }); } + function initPointsBuy() { + var modal = document.querySelector('[data-aksrt-points-buy-modal]'); + if (!modal) return; + var form = modal.querySelector('[data-aksrt-points-buy-form]'); + var tip = modal.querySelector('[data-aksrt-points-buy-tip]'); + var busy = false; + function setTip(text, isError) { + if (!tip) return; + tip.textContent = text || ''; + tip.classList.toggle('is-error', !!isError); + } + document.querySelectorAll('[data-aksrt-points-buy-open]').forEach(function (btn) { + btn.addEventListener('click', function () { openModal(modal); }); + }); + modal.querySelectorAll('[data-aksrt-points-buy-close]').forEach(function (btn) { + btn.addEventListener('click', function () { closeModal(modal); }); + }); + modal.querySelectorAll('.uc-charge-package input').forEach(function (inp) { + inp.addEventListener('change', function () { + modal.querySelectorAll('.uc-charge-package').forEach(function (el) { + el.classList.toggle('is-active', el.contains(inp) && inp.checked); + }); + }); + }); + if (!form) return; + form.addEventListener('submit', function (e) { + e.preventDefault(); + if (busy) return; + var pkgInput = form.querySelector('input[name="points_package"]:checked') || form.querySelector('input[name="points_package"]'); + var pkg = pkgInput ? pkgInput.value : 'custom'; + var customEl = form.querySelector('[data-aksrt-points-buy-custom]'); + var custom = customEl ? parseInt(customEl.value || '0', 10) : 0; + if (custom > 0) pkg = 'custom'; + if (pkg === 'custom' && !(custom > 0)) { + setTip('请输入购买积分数量', true); + return; + } + var methodEl = form.querySelector('input[name="points_pay_method"]:checked'); + var method = methodEl ? methodEl.value : 'alipay'; + busy = true; + setTip('正在创建订单…'); + api('/pay/points/buy', { + method: 'POST', + body: JSON.stringify({ + package: pkg, + custom_points: custom, + payment_method: method + }) + }).then(function (r) { + busy = false; + if (!r.ok || (r.json && r.json.code && !r.json.success)) { + setTip((r.json && r.json.message) || '购买失败', true); + return; + } + var data = (r.json && r.json.data) ? r.json.data : r.json; + if (data && data.redirect && (data.payment_url || data.qr_content)) { + if (data.payment_url) window.open(data.payment_url, '_blank'); + setTip(data.message || '请完成支付,支付成功后将自动刷新。'); + if (data.order_number) pollPaidOrder(data.order_number); + return; + } + setTip((data && data.message) || '购买成功,正在刷新…'); + window.location.reload(); + }).catch(function () { + busy = false; + setTip('网络错误,请重试', true); + }); + }); + } + + function initWithdraw() { + var modal = document.querySelector('[data-aksrt-withdraw-modal]'); + if (!modal) return; + var form = modal.querySelector('[data-aksrt-withdraw-form]'); + var tip = modal.querySelector('[data-aksrt-withdraw-tip]'); + var busy = false; + function setTip(text, isError) { + if (!tip) return; + tip.textContent = text || ''; + tip.classList.toggle('is-error', !!isError); + } + document.querySelectorAll('[data-aksrt-withdraw-open]').forEach(function (btn) { + btn.addEventListener('click', function () { openModal(modal); }); + }); + modal.querySelectorAll('[data-aksrt-withdraw-close]').forEach(function (btn) { + btn.addEventListener('click', function () { closeModal(modal); }); + }); + if (!form) return; + form.addEventListener('submit', function (e) { + e.preventDefault(); + if (busy) return; + var amountEl = form.querySelector('[data-aksrt-withdraw-amount]'); + var methodEl = form.querySelector('[data-aksrt-withdraw-method]'); + var accountEl = form.querySelector('[data-aksrt-withdraw-account]'); + var amount = amountEl ? parseFloat(amountEl.value || '0') : 0; + var method = methodEl ? methodEl.value : 'alipay'; + var account = accountEl ? String(accountEl.value || '').trim() : ''; + if (!(amount > 0)) { + setTip('请输入提现金额', true); + return; + } + if (!account) { + setTip('请填写收款账号信息', true); + return; + } + busy = true; + setTip('正在提交…'); + api('/pay/balance/withdraw', { + method: 'POST', + body: JSON.stringify({ amount: amount, method: method, account_info: account }) + }).then(function (r) { + busy = false; + if (!r.ok || (r.json && r.json.code && !r.json.success)) { + setTip((r.json && r.json.message) || '提现失败', true); + return; + } + var data = (r.json && r.json.data) ? r.json.data : r.json; + setTip((data && data.message) || '申请已提交,正在刷新…'); + window.location.reload(); + }).catch(function () { + busy = false; + setTip('网络错误,请重试', true); + }); + }); + } + + function initAssetTabs() { + var nav = document.querySelectorAll('[data-aksrt-asset-tab]'); + if (!nav.length) return; + nav.forEach(function (link) { + link.addEventListener('click', function (e) { + e.preventDefault(); + var key = link.getAttribute('data-aksrt-asset-tab'); + document.querySelectorAll('[data-aksrt-asset-tab]').forEach(function (a) { + var li = a.closest('li'); + if (li) li.classList.toggle('active', a === link); + }); + document.querySelectorAll('[data-aksrt-asset-pane]').forEach(function (pane) { + var on = pane.getAttribute('data-aksrt-asset-pane') === key; + pane.classList.toggle('is-active', on); + pane.hidden = !on; + }); + }); + }); + } + function bootAssets() { initSignin(); initInvite(); @@ -784,6 +930,9 @@ initBalanceCharge(); initTransfer(); initExchange(); + initPointsBuy(); + initWithdraw(); + initAssetTabs(); } if (document.readyState === 'loading') { diff --git a/inc/classes/class-aksrt-balance-service.php b/inc/classes/class-aksrt-balance-service.php index 3e6d0a3d..e7f22892 100644 --- a/inc/classes/class-aksrt-balance-service.php +++ b/inc/classes/class-aksrt-balance-service.php @@ -75,6 +75,33 @@ class AKSRT_Balance_Service { }, ], ]); + + 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(); + }, + ], + ]); } /** @@ -333,14 +360,62 @@ class AKSRT_Balance_Service { 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; } - $user_id = (int) ($order['user_id'] ?? 0); - if ($user_id <= 0 || !class_exists('AKSRT_Balance')) { + if (!class_exists('AKSRT_Balance')) { return; } - // Prevent double credit. $note = json_decode((string) ($order['note'] ?? ''), true); if (!is_array($note)) { $note = []; @@ -366,11 +441,9 @@ class AKSRT_Balance_Service { } $note['balance_credited'] = 1; $note['credited_amount'] = $credit_amount; - // Mark credited in note (best-effort; private table accessor). global $wpdb; - $tbl = $wpdb->prefix . 'aksrt_order'; $wpdb->update( - $tbl, + $wpdb->prefix . 'aksrt_order', ['note' => wp_json_encode($note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)], ['order_number' => $order_number], ['%s'], @@ -606,4 +679,317 @@ class AKSRT_Balance_Service { '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(); + $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]); + } + $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, + ], + ]); + } + + 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 + */ + 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 + */ + 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|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, + ]; + } + } diff --git a/inc/classes/class-aksrt-balance.php b/inc/classes/class-aksrt-balance.php index d8553d77..855e73f2 100644 --- a/inc/classes/class-aksrt-balance.php +++ b/inc/classes/class-aksrt-balance.php @@ -270,6 +270,8 @@ class AKSRT_Balance { 'transfer_out' => __('转出', 'aksrt'), 'admin' => __('管理员调整', 'aksrt'), 'exchange_from_credits' => __('积分兑换', 'aksrt'), + 'withdraw_hold' => __('提现冻结', 'aksrt'), + 'withdraw_refund' => __('提现退回', 'aksrt'), ]; $key = sanitize_key($reason); return $map[$key] ?? ($reason !== '' ? $reason : __('余额变动', 'aksrt')); diff --git a/inc/helpers/aksrt-domain-model.php b/inc/helpers/aksrt-domain-model.php index 898a408e..f040fd92 100644 --- a/inc/helpers/aksrt-domain-model.php +++ b/inc/helpers/aksrt-domain-model.php @@ -30,6 +30,8 @@ class AKSRT_Domain_Model { public const ORDER_PRODUCT_RESOURCE = 'resource'; public const ORDER_PRODUCT_VIP = 'vip'; public const ORDER_PRODUCT_RECHARGE = 'recharge'; + /** Cash purchase of points (zibll order_type=9). */ + public const ORDER_PRODUCT_POINTS_BUY = 'points_buy'; public const ORDER_PRODUCT_SHOP = 'shop_product'; /** Paid plate follow (zibll order_type=11). product_id = plate post ID. */ public const ORDER_PRODUCT_PLATE_FOLLOW = 'plate_follow'; diff --git a/inc/helpers/user-center-renderers/credits-balance.php b/inc/helpers/user-center-renderers/credits-balance.php index b4c21a75..83628e61 100644 --- a/inc/helpers/user-center-renderers/credits-balance.php +++ b/inc/helpers/user-center-renderers/credits-balance.php @@ -18,7 +18,10 @@ if (class_exists('AKSRT_Charge_Card')) { $opts = AKSRT_Charge_Card::get_instance()->get_public_options(); $card_pass_credits = is_array($opts['credits'] ?? null) ? $opts['credits'] : $card_pass_credits; } -$points_transfer_on = class_exists('AKSRT_Settings') && !empty(AKSRT_Settings::get_all()['points_transfer_s']); +$all_settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_all() : []; +$points_transfer_on = !empty($all_settings['points_transfer_s']); +$points_buy_on = class_exists('AKSRT_Balance_Service') && AKSRT_Balance_Service::get_instance()->points_buy_enabled(); +$points_buy_view = $points_buy_on ? AKSRT_Balance_Service::get_instance()->get_points_buy_view((int) ($user_id ?? 0)) : []; $currency = (string) ($currency_name ?? __('积分', 'aksrt')); ?>
@@ -41,11 +44,68 @@ $currency = (string) ($currency_name ?? __('积分', 'aksrt')); + + +

+ + + +
diff --git a/inc/helpers/user-center-renderers/credits-wallet.php b/inc/helpers/user-center-renderers/credits-wallet.php index c831e143..462fe5c7 100644 --- a/inc/helpers/user-center-renderers/credits-wallet.php +++ b/inc/helpers/user-center-renderers/credits-wallet.php @@ -23,6 +23,9 @@ $recharge = class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'g : []; $transfer_on = !empty($recharge['transfer']['enabled']); $exchange_on = !empty($recharge['exchange']['enabled']); +$all_settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_all() : []; +$withdraw_on = !empty($all_settings['pay_balance_withdraw_s']); +$withdraw_min = max(1, (float) ($all_settings['pay_balance_withdraw_lowest_money'] ?? $all_settings['pay_rebate_withdraw_lowest_money'] ?? 50)); $card_pass_balance = ['enabled' => false, 'desc' => '', 'only_password' => true]; if (class_exists('AKSRT_Charge_Card')) { $opts = AKSRT_Charge_Card::get_instance()->get_public_options(); @@ -56,6 +59,9 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元'); + + +
@@ -200,6 +206,32 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元'); + + + + +

@@ -259,4 +291,6 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元');

+ +
diff --git a/inc/helpers/user-center-renderers/profile-credits.php b/inc/helpers/user-center-renderers/profile-credits.php index 6d1d0460..410355fd 100644 --- a/inc/helpers/user-center-renderers/profile-credits.php +++ b/inc/helpers/user-center-renderers/profile-credits.php @@ -2,7 +2,7 @@ /** * User center assets panel orchestrator (wallet + points). * - * Zibll user-tab-balance parity layout. + * Zibll user-tab-balance parity: dual cards + tabbed ledgers. * * @package AKSRT */ @@ -22,15 +22,16 @@ if ($show_wallet || $show_credits) :
- +
- +
@@ -42,6 +43,88 @@ if (!empty($show_vip)) { } if ($show_credits) { require $dir . '/credits-signin.php'; - require $dir . '/credits-transactions.php'; } + +// Tabbed ledgers (Zibll record tabs). +if ($show_wallet || $show_credits) : + $active_record = isset($_GET['asset_tab']) ? sanitize_key(wp_unslash($_GET['asset_tab'])) : ''; + if ($active_record === '' || !in_array($active_record, ['balance', 'points'], true)) { + $active_record = $show_wallet ? 'balance' : 'points'; + } + ?> +
+
    + +
  • + +
  • + + +
  • + +
  • + +
+
+ +
> + $user_id, 'page' => $wallet_page, 'per_page' => 10]) + : ['items' => [], 'total' => 0]; + ?> +
+ +
+ = 0 ? 'is-positive' : 'is-negative'; + $delta_txt = ($delta > 0 ? '+' : '') . (class_exists('AKSRT_Balance') ? AKSRT_Balance::format($delta) : number_format_i18n($delta, 2)); + ?> +
+
+ +
+
+
+ + + () + +
+
+
+
+ +
+
+ +
+ 1 && function_exists('aksrt_render_profile_pagination')) { + aksrt_render_profile_pagination($total_pages, $wallet_page, 'wallet_page'); + } + ?> + +
+

+
+ +
+
+ + +
> + +
+ +
+
+