diff --git a/assets/admin/src/schema/sections/payment/recharge.js b/assets/admin/src/schema/sections/payment/recharge.js index 5759c6ba..980a6ef4 100644 --- a/assets/admin/src/schema/sections/payment/recharge.js +++ b/assets/admin/src/schema/sections/payment/recharge.js @@ -20,9 +20,11 @@ export const rechargeSchema = { pay_balance_pass_charge_s: false, pay_balance_pass_charge_desc: '', pay_balance_pass_charge_only_password: false, - pay_balance_exchange_s: false, - pay_balance_exchange_rate: 100, - pay_balance_exchange_desc: '', + pay_balance_points_exchange_s: false, + pay_balance_points_exchange_rate: 100, + pay_balance_points_exchange_min_points: 100, + pay_balance_points_exchange_daily_max_points: 100000, + pay_balance_points_exchange_desc: '', pay_balance_withdraw_s: false, }, groups: [ @@ -79,12 +81,14 @@ export const rechargeSchema = { }, { key: 'exchange', - title: '积分兑换', + title: '余额兑换积分', icon: 'fas fa-coins', fields: [ - { key: 'pay_balance_exchange_s', label: '启用积分兑换', type: 'switch', default: false }, - { key: 'pay_balance_exchange_rate', label: '兑换比例', type: 'number', default: 100, min: 1, max: 10000, help: 'N 积分 = 1 元', dependsOn: { key: 'pay_balance_exchange_s', value: true } }, - { key: 'pay_balance_exchange_desc', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_balance_exchange_s', value: true } }, + { key: 'pay_balance_points_exchange_s', label: '启用余额兑换积分', type: 'switch', default: false, dependsOn: { key: 'pay_balance_s', value: true } }, + { key: 'pay_balance_points_exchange_rate', label: '兑换比例', type: 'number', default: 100, min: 1, max: 10000, help: '1 余额 = N 积分', dependsOn: { key: 'pay_balance_points_exchange_s', value: true } }, + { key: 'pay_balance_points_exchange_min_points', label: '单笔最低购买积分', type: 'number', default: 100, min: 1, max: 10000000, dependsOn: { key: 'pay_balance_points_exchange_s', value: true } }, + { key: 'pay_balance_points_exchange_daily_max_points', label: '每日最高购买积分', type: 'number', default: 100000, min: 1, max: 10000000, dependsOn: { key: 'pay_balance_points_exchange_s', value: true } }, + { key: 'pay_balance_points_exchange_desc', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_balance_points_exchange_s', value: true } }, ], }, { diff --git a/assets/admin/src/schema/sections/users/credits.js b/assets/admin/src/schema/sections/users/credits.js index 637c9a81..49b1a963 100644 --- a/assets/admin/src/schema/sections/users/credits.js +++ b/assets/admin/src/schema/sections/users/credits.js @@ -5,8 +5,6 @@ export const creditsSchema = { description: '积分规则、积分日志、用户积分管理。', defaults: { points_s: true, - points_pay_s: false, - points_rate: 100, points_name: '积分', points_decimal: 2, points_earn_daily_limit: 50, @@ -43,12 +41,6 @@ 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, 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/js/credits.js b/assets/js/credits.js index 1bbe3efb..866aea27 100644 --- a/assets/js/credits.js +++ b/assets/js/credits.js @@ -692,6 +692,10 @@ 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 amountLabel = modal.querySelector('[data-aksrt-transfer-amount-label]'); + var balanceInfo = modal.querySelector('[data-aksrt-transfer-balance-info]'); + var pointsInfo = modal.querySelector('[data-aksrt-transfer-points-info]'); + var pointsTransferMin = 1; var searchTimer = null; var busy = false; function setTip(text, isError) { @@ -702,8 +706,21 @@ document.querySelectorAll('[data-aksrt-transfer-open]').forEach(function (btn) { btn.addEventListener('click', function () { var type = btn.getAttribute('data-aksrt-transfer-open') || 'balance'; + pointsTransferMin = type === 'points' + ? Math.max(1, parseInt(btn.getAttribute('data-aksrt-transfer-min') || '1', 10) || 1) + : 1; if (typeInput) typeInput.value = type; if (title) title.textContent = type === 'points' ? '积分转账' : '余额转账'; + var amountEl = form ? form.querySelector('[data-aksrt-transfer-amount]') : null; + if (amountEl) { + amountEl.min = type === 'points' ? String(pointsTransferMin) : '0.01'; + amountEl.step = type === 'points' ? '1' : '0.01'; + amountEl.inputMode = type === 'points' ? 'numeric' : 'decimal'; + amountEl.value = ''; + } + if (amountLabel) amountLabel.textContent = type === 'points' ? '积分' : '金额'; + if (balanceInfo) balanceInfo.hidden = type === 'points'; + if (pointsInfo) pointsInfo.hidden = type !== 'points'; if (results) results.innerHTML = ''; if (selected) selected.textContent = ''; if (recipientInput) recipientInput.value = ''; @@ -762,7 +779,7 @@ setTip('请选择收款用户', true); return; } - if (!(parseFloat(amount) > 0)) { + if (!(parseFloat(amount) > 0) || (type === 'points' && (!Number.isInteger(Number(amount)) || Number(amount) < pointsTransferMin))) { setTip('请输入有效金额', true); return; } @@ -787,102 +804,49 @@ }); } - function initExchange() { - var modal = document.querySelector('[data-aksrt-exchange-modal]'); + function initPointsExchange() { + var modal = document.querySelector('[data-aksrt-points-exchange-modal]'); if (!modal) return; - var form = modal.querySelector('[data-aksrt-exchange-form]'); - var tip = modal.querySelector('[data-aksrt-exchange-tip]'); + var form = modal.querySelector('[data-aksrt-points-exchange-form]'); + var tip = modal.querySelector('[data-aksrt-points-exchange-tip]'); + var amountEl = form ? form.querySelector('[data-aksrt-points-exchange-amount]') : null; + var preview = form ? form.querySelector('[data-aksrt-points-exchange-preview]') : null; 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) { + document.querySelectorAll('[data-aksrt-points-exchange-open]').forEach(function (btn) { btn.addEventListener('click', function () { openModal(modal); }); }); - modal.querySelectorAll('[data-aksrt-exchange-close]').forEach(function (btn) { + modal.querySelectorAll('[data-aksrt-points-exchange-close]').forEach(function (btn) { btn.addEventListener('click', function () { closeModal(modal); }); }); if (!form) return; + function updatePreview() { + if (!preview || !amountEl) return; + var amount = Number(amountEl.value || '0'); + var rate = Number(amountEl.getAttribute('data-aksrt-points-exchange-rate') || '0'); + preview.textContent = Number.isInteger(amount) && amount > 0 && rate > 0 + ? '预计获得 ' + (amount * rate) + ' 积分' + : ''; + } + amountEl.addEventListener('input', updatePreview); + updatePreview(); 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); + var amount = amountEl ? Number(amountEl.value || '0') : 0; + if (!Number.isInteger(amount) || !(amount > 0)) { + setTip('请输入正整数余额', true); return; } busy = true; setTip('正在兑换…'); - api('/pay/balance/exchange', { + api('/pay/balance/points-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 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 - }) + body: JSON.stringify({ amount: amount }) }).then(function (r) { busy = false; if (!r.ok || (r.json && r.json.code && !r.json.success)) { @@ -890,12 +854,6 @@ 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) activePoll = pollPaidOrder(data.order_number, setTip, ['credits', 'orders']); - return; - } setTip((data && data.message) || '购买成功,正在刷新…'); window.location.reload(); }).catch(function () { @@ -1044,8 +1002,7 @@ initCardPass(); initBalanceCharge(); initTransfer(); - initExchange(); - initPointsBuy(); + initPointsExchange(); initWithdraw(); initWithdrawHistory(); initAssetTabs(); diff --git a/inc/helpers/aksrt-credits.php b/inc/helpers/aksrt-credits.php index a74c33fc..187b71aa 100644 --- a/inc/helpers/aksrt-credits.php +++ b/inc/helpers/aksrt-credits.php @@ -56,6 +56,10 @@ if (!function_exists('aksrt_credits_label')) { 'free_post_favorite'=> __('文章被收藏', 'aksrt'), 'free_comment_new'=> __('发表评论', 'aksrt'), 'free_comment_like'=> __('评论被赞', 'aksrt'), + 'points_exchange_in' => __('余额兑换积分', 'aksrt'), + 'transfer_in' => __('积分转入', 'aksrt'), + 'transfer_out' => __('积分转出', 'aksrt'), + 'transfer_refund' => __('积分转账退款', 'aksrt'), ]; if (isset($map[$reason])) { return $map[$reason]; diff --git a/inc/helpers/user-center-renderers/credits-balance.php b/inc/helpers/user-center-renderers/credits-balance.php index ed0260d5..45b72823 100644 --- a/inc/helpers/user-center-renderers/credits-balance.php +++ b/inc/helpers/user-center-renderers/credits-balance.php @@ -24,8 +24,9 @@ $points_transfer_on = !empty($all_settings['points_transfer_s']); if ($points_transfer_on && class_exists('\AKSRT\Settings\Capabilities') && !\AKSRT\Settings\Capabilities::user_can('points_transfer', $points_uid)) { $points_transfer_on = false; } -$points_buy_on = class_exists('\AKSRT\Credits\BalanceService') && \AKSRT\Credits\BalanceService::get_instance()->points_buy_enabled(); -$points_buy_view = $points_buy_on ? \AKSRT\Credits\BalanceService::get_instance()->get_points_buy_view((int) ($user_id ?? 0)) : []; +$points_transfer_min = max(1, (int) ($all_settings['points_transfer_min'] ?? 1)); +$points_exchange = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_recharge_settings()['points_exchange'] ?? [] : []; +$points_exchange_on = !empty($points_exchange['enabled']) && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled(); $currency = (string) ($currency_name ?? __('积分', 'aksrt')); ?>
@@ -46,10 +47,10 @@ $currency = (string) ($currency_name ?? __('积分', 'aksrt'));
- + - - + +
@@ -57,54 +58,25 @@ $currency = (string) ($currency_name ?? __('积分', 'aksrt')); - - - - -
- +
@@ -181,60 +171,6 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元'); - - - - - - - - + diff --git a/src/Admin/Schema/SchemaPayment.php b/src/Admin/Schema/SchemaPayment.php index fe2e34af..e50ae4a4 100644 --- a/src/Admin/Schema/SchemaPayment.php +++ b/src/Admin/Schema/SchemaPayment.php @@ -46,10 +46,12 @@ class SchemaPayment { ['key' => 'pay_balance_pass_charge_desc', 'label' => '卡密兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]], ['key' => 'pay_balance_pass_charge_only_password', 'label' => '仅密码兑换', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]], ]], - ['key' => 'exchange', 'title' => '积分兑换', 'fields' => [ - ['key' => 'pay_balance_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false], - ['key' => 'pay_balance_exchange_rate', 'label' => '兑换比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000, 'dependsOn' => ['key' => 'pay_balance_exchange_s', 'value' => true]], - ['key' => 'pay_balance_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_exchange_s', 'value' => true]], + ['key' => 'points-exchange', 'title' => '余额兑换积分', 'fields' => [ + ['key' => 'pay_balance_points_exchange_s', 'label' => '启用余额兑换积分', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]], + ['key' => 'pay_balance_points_exchange_rate', 'label' => '兑换比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000, 'help' => '1 余额 = N 积分', 'dependsOn' => ['key' => 'pay_balance_points_exchange_s', 'value' => true]], + ['key' => 'pay_balance_points_exchange_min_points', 'label' => '单笔最低购买积分', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000000, 'dependsOn' => ['key' => 'pay_balance_points_exchange_s', 'value' => true]], + ['key' => 'pay_balance_points_exchange_daily_max_points', 'label' => '每日最高购买积分', 'type' => 'number', 'default' => 100000, 'min' => 1, 'max' => 10000000, 'dependsOn' => ['key' => 'pay_balance_points_exchange_s', 'value' => true]], + ['key' => 'pay_balance_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_points_exchange_s', 'value' => true]], ]], ['key' => 'withdraw', 'title' => '余额提现', 'fields' => [ ['key' => 'pay_balance_withdraw_s', 'label' => '启用余额提现', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]], diff --git a/src/Admin/Schema/SchemaUsers.php b/src/Admin/Schema/SchemaUsers.php index c2ffecac..d05822a6 100644 --- a/src/Admin/Schema/SchemaUsers.php +++ b/src/Admin/Schema/SchemaUsers.php @@ -515,8 +515,6 @@ class SchemaUsers { 'groups' => [ ['key' => 'basic', 'title' => '积分基础', '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_name', 'label' => '积分名称', 'type' => 'input', 'default' => '积分'], ['key' => 'points_decimal', 'label' => '积分小数位数', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 4], ]], diff --git a/src/Core/DomainModel.php b/src/Core/DomainModel.php index 55391b4b..545cf651 100644 --- a/src/Core/DomainModel.php +++ b/src/Core/DomainModel.php @@ -28,8 +28,6 @@ class DomainModel { 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'; /** * Human labels for aksrt_order.product_type (front-end order center). @@ -41,7 +39,6 @@ class DomainModel { self::ORDER_PRODUCT_RESOURCE => __('付费内容', 'aksrt'), self::ORDER_PRODUCT_VIP => __('VIP 会员', 'aksrt'), self::ORDER_PRODUCT_RECHARGE => __('余额充值', 'aksrt'), - self::ORDER_PRODUCT_POINTS_BUY => __('购买积分', 'aksrt'), ]; } diff --git a/src/Credits/Balance.php b/src/Credits/Balance.php index c8bdae00..37a02c6f 100644 --- a/src/Credits/Balance.php +++ b/src/Credits/Balance.php @@ -366,7 +366,8 @@ class Balance { 'tip_sent' => __('打赏支出', 'aksrt'), 'tip_received' => __('收到打赏', 'aksrt'), 'admin' => __('管理员调整', 'aksrt'), - 'exchange_from_credits' => __('积分兑换', 'aksrt'), + 'points_exchange_out' => __('余额兑换积分', 'aksrt'), + 'points_exchange_refund' => __('余额兑换积分退款', 'aksrt'), 'creator_income' => __('创作收益结算', 'aksrt'), 'rebate' => __('推广返佣结算', 'aksrt'), 'withdraw_hold' => __('提现冻结', 'aksrt'), diff --git a/src/Credits/BalanceService.php b/src/Credits/BalanceService.php index d94dc1c6..9e3198a7 100644 --- a/src/Credits/BalanceService.php +++ b/src/Credits/BalanceService.php @@ -10,7 +10,6 @@ defined('ABSPATH') || exit; class BalanceService { use BalanceServiceRest; use BalanceServiceCharge; - use BalanceServicePoints; private static $instance = null; diff --git a/src/Credits/BalanceServiceCharge.php b/src/Credits/BalanceServiceCharge.php index a27c048c..d24ddd60 100644 --- a/src/Credits/BalanceServiceCharge.php +++ b/src/Credits/BalanceServiceCharge.php @@ -30,74 +30,6 @@ trait BalanceServiceCharge { return; } - // Points purchase fulfill (zibll order_type=9). - if ($type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY) { - if (!class_exists('AKSRT\\Credits\\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\Settings')) { - $rate = max(1, (int) (\AKSRT\Settings\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; - } - global $wpdb; - if ($wpdb->query('START TRANSACTION') === false) { - return; - } - $add = Credits::add( - $user_id, - $points, - 'points_buy', - ['type' => 'order', 'order' => $order_number], - sprintf(__('购买积分订单 %s', 'aksrt'), $order_number), - false - ); - if (is_wp_error($add)) { - $wpdb->query('ROLLBACK'); - return; - } - $note['points_credited'] = 1; - $note['credited_points'] = $points; - $updated = $wpdb->update( - $wpdb->prefix . 'aksrt_order', - ['note' => wp_json_encode($note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)], - ['order_number' => $order_number], - ['%s'], - ['%s'] - ); - if ($updated === false) { - $wpdb->query('ROLLBACK'); - wp_cache_delete($user_id, 'user_meta'); - return; - } - if ($wpdb->query('COMMIT') === false) { - $wpdb->query('ROLLBACK'); - wp_cache_delete($user_id, 'user_meta'); - return; - } - do_action('aksrt_credits_added', $user_id, $points, 'points_buy', (int) $add); - if (class_exists('\AKSRT\Payment\OrderRepository')) { - \AKSRT\Payment\OrderRepository::get_instance()->update_status( - $order_number, - \AKSRT\Payment\Order::STATUS_COMPLETED, - ['note' => __('购买积分已到账。', 'aksrt')] - ); - } - return; - } - if ($type !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) { return; } @@ -204,11 +136,6 @@ trait BalanceServiceCharge { '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'] : [], ]; } diff --git a/src/Credits/BalanceServicePoints.php b/src/Credits/BalanceServicePoints.php deleted file mode 100644 index 955b7a16..00000000 --- a/src/Credits/BalanceServicePoints.php +++ /dev/null @@ -1,241 +0,0 @@ - - */ - public function get_points_buy_view(int $user_id): array { - $all = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\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\\Credits\\Balance') && Balance::enabled())) { - continue; - } - $methods[] = is_array($m) ? $m : ['id' => $id, 'name' => $id]; - } - } - if (class_exists('AKSRT\\Credits\\Balance') && 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\\Credits') ? 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\Payment\OrderRepository') || !class_exists('\AKSRT\Payment\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\Payment\OrderRepository::get_instance()->create([ - 'user_id' => $user_id, - 'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS_BUY, - 'product_id' => 0, - 'amount' => $pay_amount, - 'currency' => 'CNY', - 'status' => \AKSRT\Payment\Order::STATUS_PENDING, - 'payment_method' => $payment_method, - 'note' => is_string($note) ? $note : '', - 'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(), - ]); - if ($order_number === '') { - return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]); - } - $order = \AKSRT\Payment\OrderRepository::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]); - } - $resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method); - if (empty($resolved['gateway'])) { - return new \WP_Error( - (string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'), - (string) ($resolved['message'] ?? __('当前支付方式不可用', 'aksrt')), - ['status' => 400, 'order_number' => $order_number] - ); - } - $payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [ - 'paid_note' => __('购买积分支付完成', 'aksrt'), - ]); - if (empty($payment_result['success'])) { - return new \WP_Error( - (string) ($payment_result['code'] ?? 'aksrt_pay_failed'), - (string) ($payment_result['message'] ?? __('支付失败', 'aksrt')), - ['status' => 402, 'order_number' => $order_number] - ); - } - if (!empty($payment_result['redirect'])) { - return [ - 'redirect' => true, - 'payment_url' => (string) ($payment_result['payment_url'] ?? ''), - 'qr_content' => (string) ($payment_result['qr_content'] ?? ''), - 'order_number' => $order_number, - 'amount' => $pay_amount, - 'points_amount' => $points_amount, - 'message' => $payment_result['message'], - 'reload' => false, - 'poll' => true, - ]; - } - // PaymentOrchestrator's paid transition already dispatched the fulfillment hook. - return [ - 'redirect' => false, - 'order_number' => $order_number, - 'amount' => $pay_amount, - 'points_amount' => $points_amount, - 'points' => Credits::balance($user_id), - 'message' => __('购买成功', 'aksrt'), - 'reload' => true, - ]; - } - -} diff --git a/src/Credits/BalanceServiceRest.php b/src/Credits/BalanceServiceRest.php index c98167da..7fa708b1 100644 --- a/src/Credits/BalanceServiceRest.php +++ b/src/Credits/BalanceServiceRest.php @@ -49,27 +49,10 @@ trait BalanceServiceRest { ], ]); - register_rest_route('aksrt/v1', '/pay/balance/exchange', [ + register_rest_route('aksrt/v1', '/pay/balance/points-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'], + 'callback' => [$this, 'rest_exchange_balance_for_points'], 'permission_callback' => static function () { return is_user_logged_in(); }, @@ -208,21 +191,28 @@ trait BalanceServiceRest { } // points transfer - $points_transfer_on = !empty(\AKSRT\Settings\Settings::get_all()['points_transfer_s']); - if (!class_exists('\AKSRT\Settings\Settings') || !\AKSRT\Settings\Settings::credits_enabled() || !$points_transfer_on) { + if (!class_exists('\AKSRT\Settings\Settings') || !\AKSRT\Settings\Settings::credits_enabled()) { + return new \WP_Error('aksrt_transfer_closed', __('积分转账未开启', 'aksrt'), ['status' => 403]); + } + $points_settings = \AKSRT\Settings\Settings::get_all(); + if (empty($points_settings['points_transfer_s'])) { 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) { + if (filter_var($amount_raw, FILTER_VALIDATE_INT) === false || (int) $amount_raw <= 0) { return new \WP_Error('aksrt_transfer_amount', __('请输入有效积分', 'aksrt'), ['status' => 400]); } + $amount = (int) $amount_raw; + $min_amount = max(1, (int) ($points_settings['points_transfer_min'] ?? 1)); + if ($amount < $min_amount) { + return new \WP_Error('aksrt_transfer_minimum', sprintf(__('单笔转账至少需要 %d 积分', 'aksrt'), $min_amount), ['status' => 400]); + } if (!class_exists('AKSRT\\Credits\\Credits')) { return new \WP_Error('aksrt_credits_unavailable', __('积分系统不可用', 'aksrt'), ['status' => 500]); } - $fee_rate = max(0, min(50, (int) (\AKSRT\Settings\Settings::get_all()['points_service_charge'] ?? 0))); + $fee_rate = max(0, min(50, (int) ($points_settings['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); @@ -236,13 +226,26 @@ trait BalanceServiceRest { if (is_wp_error($deduct)) { return $deduct; } - Credits::add( + $add = Credits::add( $recipient, $credit, 'transfer_in', ['type' => 'user', 'id' => $from_id], sprintf(__('来自用户[%s]的转账', 'aksrt'), $from_user->display_name ?: (string) $from_id) ); + if (is_wp_error($add)) { + $refund = Credits::add( + $from_id, + $amount, + 'transfer_refund', + ['type' => 'user', 'id' => $recipient], + __('收款积分入账失败,已退回转账积分', 'aksrt') + ); + if (is_wp_error($refund)) { + return new \WP_Error('aksrt_transfer_recovery_failed', __('收款积分入账失败,转账积分退回失败,请联系管理员', 'aksrt'), ['status' => 500]); + } + return $add; + } return rest_ensure_response([ 'success' => true, 'data' => [ @@ -283,102 +286,82 @@ trait BalanceServiceRest { /** - * Exchange points → cash wallet (pay_balance_exchange_*). + * Exchange integer cash-wallet balance for points. */ - public function rest_exchange_from_credits(\WP_REST_Request $request) { + public function rest_exchange_balance_for_points(\WP_REST_Request $request) { if (!class_exists('AKSRT\\Credits\\Balance') || !Balance::enabled()) { return new \WP_Error('aksrt_balance_disabled', __('余额功能未启用', 'aksrt'), ['status' => 403]); } - $cfg = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\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\\Credits') || !\AKSRT\Settings\Settings::credits_enabled()) { + if (!class_exists('AKSRT\\Credits\\Credits') || !class_exists('AKSRT\\Settings\\Settings') || !\AKSRT\Settings\Settings::credits_enabled()) { return new \WP_Error('aksrt_credits_disabled', __('积分功能未启用', 'aksrt'), ['status' => 403]); } + $cfg = \AKSRT\Settings\Settings::get_payment_recharge_settings(); + $exchange = is_array($cfg['points_exchange'] ?? null) ? $cfg['points_exchange'] : []; + if (empty($exchange['enabled'])) { + return new \WP_Error('aksrt_exchange_closed', __('余额兑换积分未开启', '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]); + $raw_amount = $params['amount'] ?? 0; + if (filter_var($raw_amount, FILTER_VALIDATE_INT) === false || (int) $raw_amount <= 0) { + return new \WP_Error('aksrt_exchange_amount', __('兑换余额必须为正整数', 'aksrt'), ['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]); + $amount = (int) $raw_amount; + $rate = max(1, (int) ($exchange['rate'] ?? 100)); + $daily_max = max(1, (int) ($exchange['daily_max_points'] ?? 100000)); + if ($amount > intdiv($daily_max, $rate)) { + return new \WP_Error('aksrt_exchange_daily_limit', sprintf(__('今日购买积分上限为 %d', 'aksrt'), $daily_max), ['status' => 400]); + } + $points = $amount * $rate; + $min_points = max(1, (int) ($exchange['min_points'] ?? 100)); + if ($points < $min_points) { + return new \WP_Error('aksrt_exchange_min', sprintf(__('单笔至少购买 %d 积分', 'aksrt'), $min_points), ['status' => 400]); } $user_id = get_current_user_id(); - $deduct = Credits::deduct( - $user_id, - $cost, - 'exchange_to_balance', - ['type' => 'balance'], - sprintf(__('积分兑换余额 %d 元', 'aksrt'), $yuan) - ); - if (is_wp_error($deduct)) { - return $deduct; + $today_points = $this->today_points_exchange_total($user_id); + if ($today_points + $points > $daily_max) { + return new \WP_Error('aksrt_exchange_daily_limit', sprintf(__('今日购买积分上限为 %d,当前还可购买 %d', 'aksrt'), $daily_max, max(0, $daily_max - $today_points)), ['status' => 400]); } - $add = Balance::add( - $user_id, - (float) $yuan, - 'exchange_from_credits', - ['type' => 'credits'], - sprintf(__('积分兑换:消耗 %d 积分', 'aksrt'), $cost) - ); + try { + $exchange_ref = random_int(1, PHP_INT_MAX); + } catch (\Throwable $e) { + $exchange_ref = (int) round(microtime(true) * 1000000); + } + $ref = ['type' => 'points_exchange', 'id' => $exchange_ref]; + $deduct = Balance::deduct($user_id, (float) $amount, 'points_exchange_out', $ref, sprintf(__('余额兑换 %d 积分', 'aksrt'), $points)); + if (is_wp_error($deduct)) return $deduct; + $add = Credits::add($user_id, $points, 'points_exchange_in', $ref, sprintf(__('余额兑换:扣除 %d 余额', 'aksrt'), $amount)); if (is_wp_error($add)) { - // best-effort refund points - Credits::add($user_id, $cost, 'exchange_refund', [], __('兑换失败退回积分', 'aksrt')); + $refund = Balance::add($user_id, (float) $amount, 'points_exchange_refund', $ref, __('积分入账失败,已退回兑换余额', 'aksrt')); + if (is_wp_error($refund)) return new \WP_Error('aksrt_exchange_recovery_failed', __('积分入账失败,余额退回失败,请联系管理员', 'aksrt'), ['status' => 500]); return $add; } - return rest_ensure_response([ - 'success' => true, - 'data' => [ - 'message' => sprintf(__('兑换成功:+%d 元', 'aksrt'), $yuan), - 'wallet_balance' => Balance::balance($user_id), - 'credit_balance' => Credits::balance($user_id), - 'reload' => true, - ], - ]); + return rest_ensure_response(['success' => true, 'data' => [ + 'message' => sprintf(__('兑换成功,获得 %d 积分', 'aksrt'), $points), + 'wallet_balance' => Balance::balance($user_id), + 'credit_balance' => Credits::balance($user_id), + 'points' => $points, + '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()), - ]); + private function today_points_exchange_total(int $user_id): int { + if ($user_id <= 0 || !class_exists('AKSRT\\Core\\DBMigrator')) return 0; + global $wpdb; + $table = \AKSRT\Core\DBMigrator::table('credit_log'); + if ($table === '') return 0; + $site_now = current_datetime(); + $start = (clone $site_now)->setTime(0, 0, 0); + $end = (clone $start)->modify('+1 day'); + $utc = new \DateTimeZone('UTC'); + $sql = "SELECT COALESCE(SUM(delta), 0) FROM {$table} WHERE user_id = %d AND reason = %s AND created_at >= %s AND created_at < %s"; + $start_gmt = $start->setTimezone($utc)->format('Y-m-d H:i:s'); + $end_gmt = $end->setTimezone($utc)->format('Y-m-d H:i:s'); + return max(0, (int) $wpdb->get_var($wpdb->prepare($sql, $user_id, 'points_exchange_in', $start_gmt, $end_gmt))); } - - - 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\\Credits\\Balance') || !Balance::enabled()) { return new \WP_Error('aksrt_balance_disabled', __('余额功能未启用', 'aksrt'), ['status' => 403]); diff --git a/src/Payment/UserOrders.php b/src/Payment/UserOrders.php index d80517fc..97690063 100644 --- a/src/Payment/UserOrders.php +++ b/src/Payment/UserOrders.php @@ -313,9 +313,6 @@ class UserOrders { if ($type === DomainModel::ORDER_PRODUCT_RECHARGE) { return __('余额充值', 'aksrt'); } - if ($type === DomainModel::ORDER_PRODUCT_POINTS_BUY) { - return __('购买积分', 'aksrt'); - } if ($product_id > 0) { $cache_key = (string) $product_id; if (!array_key_exists($cache_key, $this->post_title_cache)) { @@ -357,7 +354,7 @@ class UserOrders { if ($type === DomainModel::ORDER_PRODUCT_VIP) { $url = function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('vip') : ''; - } elseif (in_array($type, [DomainModel::ORDER_PRODUCT_RECHARGE, DomainModel::ORDER_PRODUCT_POINTS_BUY], true)) { + } elseif ($type === DomainModel::ORDER_PRODUCT_RECHARGE) { $url = function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('credits') : ''; } else { $url = $post_id > 0 ? get_permalink($post_id) : ''; @@ -371,7 +368,6 @@ class UserOrders { return match ($type) { DomainModel::ORDER_PRODUCT_VIP => __('查看会员权益', 'aksrt'), DomainModel::ORDER_PRODUCT_RECHARGE => __('查看余额', 'aksrt'), - DomainModel::ORDER_PRODUCT_POINTS_BUY => __('查看积分', 'aksrt'), default => __('打开已购内容', 'aksrt'), }; } diff --git a/src/Settings/PaymentRuntime.php b/src/Settings/PaymentRuntime.php index ff926dac..05b65a6f 100644 --- a/src/Settings/PaymentRuntime.php +++ b/src/Settings/PaymentRuntime.php @@ -110,10 +110,12 @@ class PaymentRuntime { 'desc' => sanitize_textarea_field((string) ($settings['pay_balance_pass_charge_desc'] ?? '')), 'only_password' => !empty($settings['pay_balance_pass_charge_only_password']), ], - 'exchange' => [ - 'enabled' => !empty($settings['pay_balance_exchange_s']), - 'rate' => max(1, min(10000, (int) ($settings['pay_balance_exchange_rate'] ?? 100))), - 'desc' => sanitize_textarea_field((string) ($settings['pay_balance_exchange_desc'] ?? '')), + 'points_exchange' => [ + 'enabled' => !empty($settings['pay_balance_points_exchange_s']), + 'rate' => max(1, min(10000, (int) ($settings['pay_balance_points_exchange_rate'] ?? 100))), + 'min_points' => max(1, min(10000000, (int) ($settings['pay_balance_points_exchange_min_points'] ?? 100))), + 'daily_max_points' => max(1, min(10000000, (int) ($settings['pay_balance_points_exchange_daily_max_points'] ?? 100000))), + 'desc' => sanitize_textarea_field((string) ($settings['pay_balance_points_exchange_desc'] ?? '')), ], ]; }