diff --git a/assets/css/routes/user-center/credits.css b/assets/css/routes/user-center/credits.css
index 4f4890eb..c36e4126 100644
--- a/assets/css/routes/user-center/credits.css
+++ b/assets/css/routes/user-center/credits.css
@@ -1,27 +1,285 @@
/**
- * User Center — credits balance + transactions
+ * User Center — assets (wallet + points) Zibll parity
*/
.uc-balance-card {
box-shadow: none;
}
-/* Dual assets: wallet + points cards stack with gap */
-.uc-wallet-grid {
+/* Dual asset row */
+.uc-assets-row {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 12px;
margin-bottom: 16px;
}
-
-.uc-balance-card--wallet .uc-balance-card__bg--wallet,
-.uc-balance-card__bg--wallet {
- background: linear-gradient(135deg, #0f766e 0%, #14b8a6 45%, #2dd4bf 100%);
+.uc-assets-row--dual {
+ grid-template-columns: 1fr 1fr;
+}
+@media (max-width: 767px) {
+ .uc-assets-row--dual {
+ grid-template-columns: 1fr;
+ }
}
-.uc-balance-card + .uc-balance-card {
- margin-top: 12px;
+.uc-asset-card {
+ background: var(--main-bg-color, var(--surface-1, #fff));
+ border-radius: var(--main-radius, 8px);
+ box-shadow: 0 0 10px var(--main-shadow, rgba(116, 116, 116, 0.08));
+ padding: 16px 20px;
+ margin-bottom: 0;
+}
+.uc-asset-card__head {
+ margin-bottom: 8px;
+}
+.uc-asset-card__title {
+ font-weight: 600;
+ color: var(--main-color, #4e5358);
+}
+.uc-asset-card__amount {
+ font-size: 1.75rem;
+ line-height: 1.2;
+}
+.uc-asset-card__mark {
+ font-size: 12px;
+ margin-right: 2px;
+}
+.uc-asset-card__actions .but {
+ min-width: 4.2em;
+}
+.uc-asset-card__link {
+ display: inline-flex;
+ align-items: center;
+ margin-top: 8px;
+ background: none;
+ border: 0;
+ padding: 0;
+ cursor: pointer;
+}
+.uc-asset-card__link:hover {
+ color: var(--focus-color, #f04494);
+}
+
+/* Sidebar mini dual cards (Zibll) */
+.uc-asset-mini {
+ gap: 8px;
+}
+.uc-asset-mini__card {
+ display: block;
+ background: var(--muted-bg-color, #f3f4f6);
+ border-radius: 6px;
+ padding: 10px 12px;
+ text-decoration: none;
+ color: inherit;
+ transition: background 0.15s;
+}
+.uc-asset-mini__card:hover {
+ background: var(--color-line-soft, rgba(0, 0, 0, 0.06));
+}
+.uc-asset-mini__label {
+ margin-bottom: 6px;
+}
+.c-blue-2 {
+ color: #3b82f6;
+}
+.c-yellow {
+ color: #eab308;
+}
+.c-green {
+ color: #16a34a;
+}
+.col-ml6 {
+ margin-left: -6px;
+}
+.col-ml6 > * {
+ margin-left: 6px;
+}
+
+/* Charge packages */
+.uc-charge-packages {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+ margin-bottom: 8px;
+}
+.uc-charge-package {
+ position: relative;
+ display: block;
+ padding: 12px;
+ border: 1px solid var(--color-line, rgba(0, 0, 0, 0.08));
+ border-radius: 8px;
+ cursor: pointer;
+ background: var(--surface-1, #fff);
+}
+.uc-charge-package input {
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+}
+.uc-charge-package.is-active,
+.uc-charge-package:has(input:checked) {
+ border-color: var(--focus-color, #f04494);
+ box-shadow: 0 0 0 1px var(--focus-color, #f04494);
+}
+.uc-charge-package__credit {
+ font-size: 1.25rem;
+ font-weight: 700;
+}
+.uc-charge-package__tag {
+ position: absolute;
+ top: 0;
+ right: 0;
+ font-size: 11px;
+ padding: 2px 8px;
+ border-radius: 0 8px 0 8px;
+ background: #fbbf24;
+ color: #333;
+}
+.uc-charge-custom__input {
+ flex: 1;
+ min-width: 0;
+ border: 0;
+ border-bottom: 1px solid var(--color-line, #ddd);
+ padding: 6px 0;
+ font-size: 1.1rem;
+ outline: none;
+}
+.uc-charge-methods {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+.uc-charge-method {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ cursor: pointer;
+}
+
+/* Modal shell */
+.uc-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 1100;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ padding: 16px;
+}
+.uc-modal.is-open,
+.uc-modal:not([hidden]) {
+ display: flex;
+}
+.uc-modal[hidden] {
+ display: none !important;
+}
+.uc-modal__overlay {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.45);
+}
+.uc-modal__dialog {
+ position: relative;
+ z-index: 1;
+ width: 100%;
+ max-width: 420px;
+ max-height: 85vh;
+ overflow: auto;
+ background: var(--main-bg-color, #fff);
+ border-radius: 12px;
+ padding: 18px 18px 20px;
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
+}
+.uc-modal__close {
+ position: absolute;
+ top: 10px;
+ right: 12px;
+ border: 0;
+ background: transparent;
+ font-size: 18px;
+ cursor: pointer;
+ color: #999;
+}
+.uc-modal__title {
+ margin: 0 0 14px;
+ font-size: 17px;
+ font-weight: 700;
+}
+.uc-modal__desc {
+ margin-bottom: 12px;
+}
+.uc-modal__tip {
+ min-height: 1.4em;
+ margin: 10px 0 0;
+ font-size: 13px;
+ color: var(--muted-color, #777);
+}
+.uc-modal__tip.is-error {
+ color: #ef4444;
+}
+body.uc-modal-open {
+ overflow: hidden;
+}
+.btn-block {
+ display: block;
+ width: 100%;
+}
+.but.jb-blue,
+.but.b-blue-2 {
+ background: #3b82f6;
+ color: #fff;
+ border: 0;
+ border-radius: 4px;
+ padding: 0.45em 1em;
+ cursor: pointer;
+}
+.but.c-blue-2.hollow,
+.but.c-green.hollow {
+ background: transparent;
+ border: 1px solid currentColor;
+ border-radius: 4px;
+ padding: 0.4em 0.9em;
+ cursor: pointer;
+}
+.but.c-blue-2 {
+ color: #3b82f6;
+}
+.but.c-green {
+ color: #16a34a;
+}
+
+/* Transfer search results */
+.uc-transfer-results {
+ margin-top: 8px;
+ max-height: 200px;
+ overflow: auto;
+}
+.uc-transfer-user {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ padding: 8px;
+ border: 0;
+ background: var(--muted-bg-color, #f5f5f5);
+ border-radius: 6px;
+ margin-bottom: 6px;
+ cursor: pointer;
+ text-align: left;
+}
+.uc-transfer-user:hover {
+ background: var(--color-primary-soft, rgba(240, 68, 148, 0.08));
+}
+.uc-transfer-user__avatar {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ object-fit: cover;
}
.uc-transactions--wallet {
margin-bottom: 20px;
+ margin-top: 12px;
}
.uc-content #panel-credits .uc-transactions {
diff --git a/assets/js/credits.js b/assets/js/credits.js
index 40a4d3ed..46ba85e7 100644
--- a/assets/js/credits.js
+++ b/assets/js/credits.js
@@ -515,19 +515,280 @@
});
}
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', function () {
- initSignin();
- initInvite();
- initVipShop();
- initVipPointsExchange();
- initCardPass();
+ // ===== 余额充值 / 转账 / 积分兑余额(Zibll 资产中心) =====
+ function openModal(el) {
+ if (!el) return;
+ el.hidden = false;
+ el.classList.add('is-open');
+ document.body.classList.add('uc-modal-open');
+ }
+ function closeModal(el) {
+ if (!el) return;
+ el.hidden = true;
+ el.classList.remove('is-open');
+ if (!document.querySelector('.uc-modal.is-open')) {
+ document.body.classList.remove('uc-modal-open');
+ }
+ }
+ function pollPaidOrder(orderNumber) {
+ if (!orderNumber) return;
+ var tries = 0;
+ var maxTries = 40;
+ var timer = setInterval(function () {
+ tries += 1;
+ api('/pay/content/order/' + encodeURIComponent(orderNumber)).then(function (r) {
+ if (r.ok && r.json && r.json.paid) {
+ clearInterval(timer);
+ window.location.reload();
+ }
+ }).catch(function () {});
+ if (tries >= maxTries) clearInterval(timer);
+ }, 3000);
+ }
+
+ function initBalanceCharge() {
+ var modal = document.querySelector('[data-aksrt-charge-modal]');
+ if (!modal) return;
+ var form = modal.querySelector('[data-aksrt-charge-form]');
+ var tip = modal.querySelector('[data-aksrt-charge-tip]');
+ var busy = false;
+ function setTip(text, isError) {
+ if (!tip) return;
+ tip.textContent = text || '';
+ tip.classList.toggle('is-error', !!isError);
+ }
+ document.querySelectorAll('[data-aksrt-charge-open]').forEach(function (btn) {
+ btn.addEventListener('click', function () { openModal(modal); });
});
- } else {
+ modal.querySelectorAll('[data-aksrt-charge-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="balance_package"]:checked') || form.querySelector('input[name="balance_package"]');
+ var pkg = pkgInput ? pkgInput.value : 'custom';
+ var customEl = form.querySelector('input[name="custom_price"]');
+ var custom = customEl ? parseFloat(customEl.value || '0') : 0;
+ if (custom > 0) {
+ pkg = 'custom';
+ }
+ if ((!pkg || pkg === 'custom') && !(custom > 0) && !pkgInput) {
+ setTip('请选择或输入充值金额', true);
+ return;
+ }
+ if (pkg === 'custom' && !(custom > 0)) {
+ setTip('请输入有效充值金额', true);
+ return;
+ }
+ var methodEl = form.querySelector('input[name="payment_method"]:checked');
+ var method = methodEl ? methodEl.value : 'alipay';
+ busy = true;
+ setTip('正在创建订单…');
+ api('/pay/balance/charge', {
+ method: 'POST',
+ body: JSON.stringify({
+ package: pkg,
+ custom_price: 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 initTransfer() {
+ var modal = document.querySelector('[data-aksrt-transfer-modal]');
+ if (!modal) return;
+ var form = modal.querySelector('[data-aksrt-transfer-form]');
+ var tip = modal.querySelector('[data-aksrt-transfer-tip]');
+ var results = modal.querySelector('[data-aksrt-transfer-results]');
+ var selected = modal.querySelector('[data-aksrt-transfer-selected]');
+ var recipientInput = modal.querySelector('[data-aksrt-transfer-recipient]');
+ var typeInput = modal.querySelector('[data-aksrt-transfer-type]');
+ var title = modal.querySelector('[data-aksrt-transfer-title]');
+ var searchTimer = null;
+ var busy = false;
+ function setTip(text, isError) {
+ if (!tip) return;
+ tip.textContent = text || '';
+ tip.classList.toggle('is-error', !!isError);
+ }
+ document.querySelectorAll('[data-aksrt-transfer-open]').forEach(function (btn) {
+ btn.addEventListener('click', function () {
+ var type = btn.getAttribute('data-aksrt-transfer-open') || 'balance';
+ if (typeInput) typeInput.value = type;
+ if (title) title.textContent = type === 'points' ? '积分转账' : '余额转账';
+ if (results) results.innerHTML = '';
+ if (selected) selected.textContent = '';
+ if (recipientInput) recipientInput.value = '';
+ setTip('');
+ openModal(modal);
+ });
+ });
+ modal.querySelectorAll('[data-aksrt-transfer-close]').forEach(function (btn) {
+ btn.addEventListener('click', function () { closeModal(modal); });
+ });
+ var searchEl = modal.querySelector('[data-aksrt-transfer-search]');
+ if (searchEl) {
+ searchEl.addEventListener('input', function () {
+ var q = String(searchEl.value || '').trim();
+ if (searchTimer) clearTimeout(searchTimer);
+ if (q.length < 1) {
+ if (results) results.innerHTML = '';
+ return;
+ }
+ searchTimer = setTimeout(function () {
+ api('/pay/transfer/search?s=' + encodeURIComponent(q)).then(function (r) {
+ if (!results) return;
+ results.innerHTML = '';
+ if (!r.ok) return;
+ var data = (r.json && r.json.data) ? r.json.data : r.json;
+ var users = (data && data.users) || [];
+ if (!users.length) {
+ results.innerHTML = '
未找到相关用户
';
+ return;
+ }
+ users.forEach(function (u) {
+ var item = document.createElement('button');
+ item.type = 'button';
+ item.className = 'uc-transfer-user';
+ item.innerHTML = '
' + (u.display_name || u.user_login) + '@' + (u.user_login || '') + '';
+ item.addEventListener('click', function () {
+ if (recipientInput) recipientInput.value = String(u.id);
+ if (selected) selected.textContent = '已选择:' + (u.display_name || u.user_login);
+ results.innerHTML = '';
+ });
+ results.appendChild(item);
+ });
+ });
+ }, 280);
+ });
+ }
+ if (!form) return;
+ form.addEventListener('submit', function (e) {
+ e.preventDefault();
+ if (busy) return;
+ var type = typeInput ? typeInput.value : 'balance';
+ var recipient = recipientInput ? parseInt(recipientInput.value || '0', 10) : 0;
+ var amountEl = form.querySelector('[data-aksrt-transfer-amount]');
+ var amount = amountEl ? amountEl.value : 0;
+ if (!recipient) {
+ setTip('请选择收款用户', true);
+ return;
+ }
+ if (!(parseFloat(amount) > 0)) {
+ setTip('请输入有效金额', true);
+ return;
+ }
+ busy = true;
+ setTip('正在转账…');
+ api('/pay/transfer', {
+ method: 'POST',
+ body: JSON.stringify({ type: type, recipient: recipient, amount: amount })
+ }).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 initExchange() {
+ var modal = document.querySelector('[data-aksrt-exchange-modal]');
+ if (!modal) return;
+ var form = modal.querySelector('[data-aksrt-exchange-form]');
+ var tip = modal.querySelector('[data-aksrt-exchange-tip]');
+ var busy = false;
+ function setTip(text, isError) {
+ if (!tip) return;
+ tip.textContent = text || '';
+ tip.classList.toggle('is-error', !!isError);
+ }
+ document.querySelectorAll('[data-aksrt-exchange-open]').forEach(function (btn) {
+ btn.addEventListener('click', function () { openModal(modal); });
+ });
+ modal.querySelectorAll('[data-aksrt-exchange-close]').forEach(function (btn) {
+ btn.addEventListener('click', function () { closeModal(modal); });
+ });
+ if (!form) return;
+ form.addEventListener('submit', function (e) {
+ e.preventDefault();
+ if (busy) return;
+ var pointsEl = form.querySelector('[data-aksrt-exchange-points]');
+ var points = pointsEl ? parseInt(pointsEl.value || '0', 10) : 0;
+ if (!(points > 0)) {
+ setTip('请输入兑换积分数量', true);
+ return;
+ }
+ busy = true;
+ setTip('正在兑换…');
+ api('/pay/balance/exchange', {
+ method: 'POST',
+ body: JSON.stringify({ points: points })
+ }).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 bootAssets() {
initSignin();
initInvite();
initVipShop();
initVipPointsExchange();
initCardPass();
+ initBalanceCharge();
+ initTransfer();
+ initExchange();
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', bootAssets);
+ } else {
+ bootAssets();
}
})();
diff --git a/functions.php b/functions.php
index 8ae4fd4c..c1b8e16f 100644
--- a/functions.php
+++ b/functions.php
@@ -189,6 +189,9 @@ if (class_exists('AKSRT_Vip_Service')) {
if (class_exists('AKSRT_Charge_Card')) {
AKSRT_Charge_Card::get_instance()->init();
}
+if (class_exists('AKSRT_Balance_Service')) {
+ AKSRT_Balance_Service::get_instance()->init();
+}
// Download State & Docs Presenter are pure service singletons (no hooks)
if (class_exists('AKSRT_Download_State')) {
AKSRT_Download_State::get_instance();
diff --git a/inc/bootstrap.php b/inc/bootstrap.php
index 2381d0f5..f74a3a50 100644
--- a/inc/bootstrap.php
+++ b/inc/bootstrap.php
@@ -226,6 +226,7 @@ require_once AKSRT_DIR . '/inc/classes/class-aksrt-db-migrator.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-credits.php';
// 站内现金钱包(与积分分离)
require_once AKSRT_DIR . '/inc/classes/class-aksrt-balance.php';
+require_once AKSRT_DIR . '/inc/classes/class-aksrt-balance-service.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-credits-earn.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-signin.php';
require_once AKSRT_DIR . '/inc/classes/class-aksrt-invite.php';
diff --git a/inc/classes/class-aksrt-balance-service.php b/inc/classes/class-aksrt-balance-service.php
new file mode 100644
index 00000000..3e6d0a3d
--- /dev/null
+++ b/inc/classes/class-aksrt-balance-service.php
@@ -0,0 +1,609 @@
+ 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();
+ },
+ ],
+ ]);
+ }
+
+ /**
+ * 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]);
+ }
+ $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]);
+ }
+ $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,
+ ],
+ ]);
+ }
+
+ /**
+ * When a recharge order is paid, credit the wallet.
+ *
+ * @param string $order_number Order number.
+ * @param array $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'] ?? ''));
+ if ($type !== AKSRT_Domain_Model::ORDER_PRODUCT_RECHARGE) {
+ return;
+ }
+ $user_id = (int) ($order['user_id'] ?? 0);
+ if ($user_id <= 0 || !class_exists('AKSRT_Balance')) {
+ return;
+ }
+ // Prevent double credit.
+ $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;
+ // Mark credited in note (best-effort; private table accessor).
+ global $wpdb;
+ $tbl = $wpdb->prefix . 'aksrt_order';
+ $wpdb->update(
+ $tbl,
+ ['note' => wp_json_encode($note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)],
+ ['order_number' => $order_number],
+ ['%s'],
+ ['%s']
+ );
+ }
+
+ /**
+ * @return array
+ */
+ 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
+ */
+ 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|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,
+ ];
+ }
+}
diff --git a/inc/helpers/user-center-renderers/credits-balance.php b/inc/helpers/user-center-renderers/credits-balance.php
index 60ff07f2..b4c21a75 100644
--- a/inc/helpers/user-center-renderers/credits-balance.php
+++ b/inc/helpers/user-center-renderers/credits-balance.php
@@ -1,8 +1,6 @@
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']);
+$currency = (string) ($currency_name ?? __('积分', 'aksrt'));
?>
-