fix(payment): wire balance wallet end-to-end when pay_balance_s is on

Opening 余额功能 alone now enables cash-wallet pay (unless 余额支付 is explicitly off). Drop points-as-balance fallback, stop fake method injection, expose real wallet in cashiers, and honor auto_to_balance on recharge fulfill.
This commit is contained in:
2026-07-25 12:46:38 +08:00
parent c85774f9a0
commit 3267bee71e
12 changed files with 109 additions and 105 deletions
@@ -12,7 +12,7 @@ export const gatewaySchema = {
payment_wechat_enabled: false,
payment_alipay_enabled: false,
payment_paypal_enabled: false,
payment_balance_enabled: false,
payment_balance_enabled: true,
payment_card_pass_enabled: false,
payment_xunhupay_enabled: false,
payment_xhpay_enabled: false,
@@ -73,7 +73,7 @@ export const gatewaySchema = {
{ key: 'payment_alipay_enabled', label: '支付宝', type: 'switch', default: false },
{ key: 'payment_paypal_enabled', label: 'PayPal', type: 'switch', default: false },
{ key: 'payment_stripe_enabled', label: 'Stripe', type: 'switch', default: false },
{ key: 'payment_balance_enabled', label: '余额支付', type: 'switch', default: false },
{ key: 'payment_balance_enabled', label: '余额支付', type: 'switch', default: true, help: '需同时在「余额充值」启用余额功能。关闭后用户中心仍可有钱包,但结账不可用余额支付。' },
{ key: 'payment_card_pass_enabled', label: '卡密充值', type: 'switch', default: false },
{ key: 'payment_xunhupay_enabled', label: '虎皮椒', type: 'switch', default: false },
{ key: 'payment_xhpay_enabled', label: '小火付', type: 'switch', default: false },
@@ -32,9 +32,9 @@ export const rechargeSchema = {
title: '充值开关',
icon: 'fas fa-wallet',
fields: [
{ key: 'pay_balance_s', label: '启用余额功能', type: 'switch', default: true },
{ key: 'pay_balance_s', label: '启用余额功能', type: 'switch', default: true, help: '开启后用户中心显示现金钱包,并默认允许余额支付(可在收款接口单独关闭)。充值需另配支付宝/微信等外部网关。' },
{ key: 'pay_balance_desc', label: '充值说明', type: 'textarea', default: '', dependsOn: { key: 'pay_balance_s', value: true } },
{ key: 'pay_auto_to_balance', label: '自动到账到余额', type: 'switch', default: true, dependsOn: { key: 'pay_balance_s', value: true } },
{ key: 'pay_auto_to_balance', label: '自动到账到余额', type: 'switch', default: true, dependsOn: { key: 'pay_balance_s', value: true }, help: '充值订单支付成功后自动入账钱包(卡密/管理员调整不受此开关限制)。' },
],
},
{
+29 -12
View File
@@ -548,18 +548,35 @@
}
body += '</div>';
}
let methodHtml = '';
methods.forEach(function (m, idx) {
const id = escapeAttr(m.id || '');
const name = escapeHtml(m.name || id);
methodHtml += '<label class="aksrt-cashier__method"><input type="radio" name="aksrt_pay_method" value="' + id + '"' + (idx === 0 ? ' checked' : '') + '> <span>' + name + '</span></label>';
});
if (methodHtml) {
body += '<div class="aksrt-cashier__methods"><div class="aksrt-cashier__methods-title">' + tt('pay_method', '支付方式') + '</div>' + methodHtml + '</div>';
}
body += '<div class="aksrt-dl-modal__actions">';
body += '<button type="button" class="btn-primary aksrt-dl-modal__download" data-aksrt-pay-submit data-mode="cash" data-price-type="' + escapeAttr(priceType) + '">' + tt('pay_now', '立即支付') + ' ' + amountLabel + '</button>';
body += '</div>';
const walletBal = typeof preview.wallet_balance === 'number'
? preview.wallet_balance
: (typeof preview.balance === 'number' ? preview.balance : 0);
const hasBalanceMethod = methods.some(function (m) { return (m && m.id) === 'balance'; });
if (hasBalanceMethod) {
body += '<div class="aksrt-cashier__box"><div class="aksrt-cashier__row"><span>' + tt('my_wallet', '我的余额') + '</span><b class="is-ok">¥' + Number(walletBal).toFixed(2) + '</b></div></div>';
if (preview.balance_enough === false) {
body += '<div class="aksrt-cashier__warn">' + tt('wallet_short', '余额不足,请先充值或选择其他支付方式') + '</div>';
if (preview.credits_url) {
body += '<div class="aksrt-dl-modal__actions"><a class="btn-sm" href="' + escapeAttr(preview.credits_url) + '">' + tt('go_recharge', '去充值') + '</a></div>';
}
}
}
let methodHtml = '';
methods.forEach(function (m, idx) {
const id = escapeAttr(m.id || '');
const name = escapeHtml(m.name || id);
methodHtml += '<label class="aksrt-cashier__method"><input type="radio" name="aksrt_pay_method" value="' + id + '"' + (idx === 0 ? ' checked' : '') + '> <span>' + name + '</span></label>';
});
if (methodHtml) {
body += '<div class="aksrt-cashier__methods"><div class="aksrt-cashier__methods-title">' + tt('pay_method', '支付方式') + '</div>' + methodHtml + '</div>';
} else {
body += '<div class="aksrt-cashier__warn">' + tt('no_pay_method', '暂无可用支付方式,请联系站长配置收款接口') + '</div>';
}
body += '<div class="aksrt-dl-modal__actions">';
if (methodHtml) {
body += '<button type="button" class="btn-primary aksrt-dl-modal__download" data-aksrt-pay-submit data-mode="cash" data-price-type="' + escapeAttr(priceType) + '">' + tt('pay_now', '立即支付') + ' ' + amountLabel + '</button>';
}
body += '</div>';
}
open('<h3 class="aksrt-dl-modal__title">' + tt('confirm_buy', '确认购买') + '</h3>' + body, returnFocusEl);
+2 -6
View File
@@ -107,12 +107,8 @@ if (!function_exists('aksrt_list_cash_payment_methods')) {
];
}
if ($methods === []) {
$methods[] = $logged_in
? ['id' => 'balance', 'name' => __('站内余额', 'aksrt')]
: ['id' => 'alipay', 'name' => __('支付宝', 'aksrt')];
}
// Do not inject a fake balance/alipay option when nothing is configured —
// callers should surface “暂无可用支付方式” instead of a dead method.
return $methods;
}
}
+5 -1
View File
@@ -95,6 +95,10 @@ class SchemaPayment {
['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' => 'withdraw', 'title' => '余额提现', '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]],
]],
],
],
'vip' => [
@@ -229,7 +233,7 @@ class SchemaPayment {
['key' => 'payment_wechat_enabled', 'label' => '微信支付', 'type' => 'switch', 'default' => false],
['key' => 'payment_alipay_enabled', 'label' => '支付宝', 'type' => 'switch', 'default' => false],
['key' => 'payment_paypal_enabled', 'label' => 'PayPal', 'type' => 'switch', 'default' => false],
['key' => 'payment_balance_enabled', 'label' => '余额支付', 'type' => 'switch', 'default' => false],
['key' => 'payment_balance_enabled', 'label' => '余额支付', 'type' => 'switch', 'default' => true],
['key' => 'payment_card_pass_enabled', 'label' => '卡密充值', 'type' => 'switch', 'default' => false],
['key' => 'payment_xunhupay_enabled', 'label' => '虎皮椒', 'type' => 'switch', 'default' => false],
['key' => 'payment_xhpay_enabled', 'label' => '小火付', 'type' => 'switch', 'default' => false],
+17 -3
View File
@@ -120,7 +120,14 @@ trait PayHelpers {
? (class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt'))
: '¥'));
$balance = ($uid > 0 && class_exists('\AKSRT\Credits\Credits')) ? \AKSRT\Credits\Credits::balance($uid) : 0;
// Points cashier uses Credits; cash cashier uses wallet (元). Keep both for UI.
$points_balance = ($uid > 0 && class_exists('\AKSRT\Credits\Credits'))
? (int) \AKSRT\Credits\Credits::balance($uid)
: 0;
$wallet_balance = ($uid > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled())
? (float) \AKSRT\Credits\Balance::balance($uid)
: 0.0;
$balance = $pay_modo === 'points' ? $points_balance : $wallet_balance;
$methods = $this->available_payment_methods($pay_modo, $uid > 0);
$amount_label = $this->format_amount_label($amount, $pay_modo, $currency);
@@ -140,6 +147,11 @@ trait PayHelpers {
? aksrt_get_user_free_downloaded_number($uid) : 0;
$quota_over = $free_limit > 0 && $free_used >= $free_limit;
$payable_amount = $pay_modo === 'points' ? (int) ceil($payable) : round($payable, 2);
$balance_enough = $pay_modo === 'points'
? ($points_balance >= (int) ceil($amount))
: ($wallet_balance + 0.0001 >= (float) $payable_amount);
return [
'post_id' => $post_id,
'title' => get_the_title($post),
@@ -153,7 +165,7 @@ trait PayHelpers {
'base' => $pay_modo === 'points' ? (int) ceil($base) : round($base, 2),
'list_amount' => $pay_modo === 'points' ? (int) ceil($list_amount) : round($list_amount, 2),
'list_amount_label' => $list_label,
'payable' => $pay_modo === 'points' ? (int) ceil($payable) : round($payable, 2),
'payable' => $payable_amount,
'original' => (float) ($priced['original'] ?? 0),
'vip1' => (float) ($priced['vip1'] ?? 0),
'vip2' => (float) ($priced['vip2'] ?? 0),
@@ -181,7 +193,9 @@ trait PayHelpers {
'need_purchase' => !$has_paid && !$need_vip && $amount > 0,
'logged_in' => $uid > 0,
'balance' => $balance,
'balance_enough' => $pay_modo !== 'points' || $balance >= (int) ceil($amount),
'wallet_balance' => $wallet_balance,
'points_balance' => $points_balance,
'balance_enough' => $balance_enough,
'payment_methods' => $methods,
'login_url' => wp_login_url(get_permalink($post)),
'vip_url' => $vip_page,
+4 -4
View File
@@ -506,9 +506,12 @@ trait VipServicePackages {
* @return array<int, array{id:string,name:string}>
*/
private function available_payment_methods(): array {
if (function_exists('aksrt_list_cash_payment_methods')) {
return aksrt_list_cash_payment_methods(true);
}
$methods = [];
if (!class_exists('\AKSRT\Payment\Gateway')) {
return [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]];
return $methods;
}
$gw = \AKSRT\Payment\Gateway::get_instance();
foreach (['balance', 'alipay', 'wechatpay', 'paypal', 'stripe'] as $id) {
@@ -524,9 +527,6 @@ trait VipServicePackages {
'name' => method_exists($g, 'get_name') ? (string) $g->get_name() : $id,
];
}
if ($methods === []) {
$methods[] = ['id' => 'balance', 'name' => __('站内余额', 'aksrt')];
}
return $methods;
}
+8 -2
View File
@@ -132,7 +132,12 @@ trait VipServicePurchase {
$points_packages = $this->get_points_exchange_packages($vip, $levels);
}
$balance = ($user_id > 0 && class_exists('\AKSRT\Credits\Credits')) ? \AKSRT\Credits\Credits::balance($user_id) : 0;
$points_balance = ($user_id > 0 && class_exists('\AKSRT\Credits\Credits'))
? (int) \AKSRT\Credits\Credits::balance($user_id)
: 0;
$wallet_balance = ($user_id > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled())
? (float) \AKSRT\Credits\Balance::balance($user_id)
: 0.0;
$methods = $this->available_payment_methods();
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
@@ -144,7 +149,8 @@ trait VipServicePurchase {
'user_expires' => $expires,
'user_is_permanent' => $is_permanent,
'user_expires_label' => $this->format_expires_label($expires, $level),
'user_points' => $balance,
'user_points' => $points_balance,
'wallet_balance' => $wallet_balance,
'currency_name' => $currency_name,
'renew_enabled' => $renew_enabled,
'upgrade_enabled' => $upgrade_enabled,
@@ -81,6 +81,13 @@ trait BalanceServiceCharge {
if (!class_exists('AKSRT\\Credits\\Balance')) {
return;
}
// Honor pay_auto_to_balance (default on). When off, leave order paid without wallet credit.
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_payment_recharge_settings')) {
$recharge_cfg = \AKSRT\Settings\Settings::get_payment_recharge_settings();
if (isset($recharge_cfg['auto_to_balance']) && empty($recharge_cfg['auto_to_balance'])) {
return;
}
}
$note = json_decode((string) ($order['note'] ?? ''), true);
if (!is_array($note)) {
$note = [];
+7 -8
View File
@@ -189,16 +189,14 @@ class TopicPay {
}
$has_paid = $this->user_has_paid($uid, $topic_id);
$balance = 0;
if ($uid > 0 && class_exists('\AKSRT\Credits\Credits')) {
$balance = (int) \AKSRT\Credits\Credits::balance($uid);
$wallet_balance = 0.0;
if ($uid > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled()) {
$wallet_balance = (float) \AKSRT\Credits\Balance::balance($uid);
}
$methods = function_exists('aksrt_list_cash_payment_methods')
? aksrt_list_cash_payment_methods($uid > 0)
: ($uid > 0
? [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]
: [['id' => 'alipay', 'name' => __('支付宝', 'aksrt')]]);
: [];
$coupon_allowed = $this->is_coupon_allowed($topic_id);
$coupon_error = '';
@@ -259,8 +257,9 @@ class TopicPay {
'need_vip' => false,
'need_purchase' => !$has_paid && !$is_free,
'logged_in' => $uid > 0,
'balance' => $balance,
'balance_enough' => true,
'balance' => $wallet_balance,
'wallet_balance' => $wallet_balance,
'balance_enough' => $is_free || ($wallet_balance + 0.0001 >= (float) $payable),
'payment_methods' => $methods,
'login_url' => wp_login_url(get_permalink($post)),
'vip_url' => home_url('/'),
+17 -64
View File
@@ -10,7 +10,7 @@ defined('ABSPATH') || exit;
/**
* 站内余额支付网关(内置实现)。
*
* 优先扣现金钱包(\AKSRT\Credits\Balance / pay_balance);未启用时回退到积分。
* 仅扣现金钱包(\AKSRT\Credits\Balance / pay_balance_s),不再回退到积分。
*/
class BalanceGateway implements GatewayInterface {
@@ -24,9 +24,8 @@ class BalanceGateway implements GatewayInterface {
}
public function is_configured(): bool {
$wallet_on = class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled();
$credits_on = class_exists('\AKSRT\Credits\Credits');
if (!$wallet_on && !$credits_on) {
// Cash wallet only — do not treat points as 余额.
if (!class_exists('\AKSRT\Credits\Balance') || !\AKSRT\Credits\Balance::enabled()) {
return false;
}
if (class_exists('\AKSRT\Settings\PaymentRuntime')) {
@@ -40,55 +39,26 @@ class BalanceGateway implements GatewayInterface {
$amount = round((float) ($order['amount'] ?? 0), 2);
$order_number = (string) ($order['order_number'] ?? '');
// Prefer cash wallet when pay_balance_s is on.
if (class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled()) {
if ($user_id <= 0) {
return ['success' => false, 'message' => __('请先登录', 'aksrt')];
}
$balance = \AKSRT\Credits\Balance::balance($user_id);
if ($balance < $amount) {
return ['success' => false, 'message' => __('余额不足', 'aksrt')];
}
$result = \AKSRT\Credits\Balance::deduct(
$user_id,
$amount,
'order_payment',
['type' => 'order', 'id' => 0, 'order' => $order_number],
sprintf(__('订单 %s 支付', 'aksrt'), $order_number)
);
if (is_wp_error($result)) {
return ['success' => false, 'message' => $result->get_error_message() ?: __('扣款失败', 'aksrt')];
}
return [
'success' => true,
'transaction_id' => 'BAL_' . ($order_number !== '' ? $order_number : (string) time()),
'message' => __('余额支付成功', 'aksrt'),
];
if (!class_exists('\AKSRT\Credits\Balance') || !\AKSRT\Credits\Balance::enabled()) {
return ['success' => false, 'message' => __('余额功能未启用', 'aksrt')];
}
// Fallback: legacy points-as-balance (1 元 = 100 积分).
if (!class_exists('\AKSRT\Credits\Credits')) {
return ['success' => false, 'message' => __('积分系统未启用', 'aksrt')];
if ($user_id <= 0) {
return ['success' => false, 'message' => __('请先登录', 'aksrt')];
}
$credits_cost = (int) ceil($amount * 100);
$balance = \AKSRT\Credits\Credits::balance($user_id);
if ($balance < $credits_cost) {
$balance = \AKSRT\Credits\Balance::balance($user_id);
if ($balance < $amount) {
return ['success' => false, 'message' => __('余额不足', 'aksrt')];
}
$result = \AKSRT\Credits\Credits::deduct(
$result = \AKSRT\Credits\Balance::deduct(
$user_id,
$credits_cost,
$amount,
'order_payment',
['order' => $order_number],
['type' => 'order', 'id' => 0, 'order' => $order_number],
sprintf(__('订单 %s 支付', 'aksrt'), $order_number)
);
if (is_wp_error($result)) {
return ['success' => false, 'message' => $result->get_error_message() ?: __('扣款失败', 'aksrt')];
}
return [
'success' => true,
'transaction_id' => 'BAL_' . ($order_number !== '' ? $order_number : (string) time()),
@@ -127,37 +97,20 @@ class BalanceGateway implements GatewayInterface {
$user_id = (int) $order['user_id'];
$amount = round((float) $amount, 2);
if (class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled()) {
$result = \AKSRT\Credits\Balance::add(
$user_id,
$amount,
'order_refund',
['type' => 'order', 'order' => $order_number],
sprintf(__('订单 %s 退款', 'aksrt'), $order_number)
);
if (is_wp_error($result)) {
return ['success' => false, 'message' => $result->get_error_message() ?: __('退款失败', 'aksrt')];
}
return ['success' => true, 'refund_id' => 'BAL_REF_' . $order_number, 'message' => __('退款成功', 'aksrt')];
if (!class_exists('\AKSRT\Credits\Balance') || !\AKSRT\Credits\Balance::enabled()) {
return ['success' => false, 'message' => __('余额功能未启用', 'aksrt')];
}
if (!class_exists('\AKSRT\Credits\Credits')) {
return ['success' => false, 'message' => __('积分系统未启用', 'aksrt')];
}
$credits_refund = (int) ceil($amount * 100);
$result = \AKSRT\Credits\Credits::add(
$result = \AKSRT\Credits\Balance::add(
$user_id,
$credits_refund,
$amount,
'order_refund',
['order' => $order_number],
['type' => 'order', 'order' => $order_number],
sprintf(__('订单 %s 退款', 'aksrt'), $order_number)
);
if (is_wp_error($result)) {
return ['success' => false, 'message' => $result->get_error_message() ?: __('退款失败', 'aksrt')];
}
return ['success' => true, 'refund_id' => 'BAL_REF_' . $order_number, 'message' => __('退款成功', 'aksrt')];
}
}
+9 -1
View File
@@ -467,7 +467,15 @@ class PaymentRuntime {
return false;
}
if ($gateway_id === 'balance') {
return !empty($settings['payment_balance_enabled']) && !empty($settings['pay_balance_s']);
// Master: 余额充值 → 启用余额功能. Optional kill-switch: payment_balance_enabled=false.
// Missing key (legacy sites) follows pay_balance_s so "open wallet" alone enables pay.
if (empty($settings['pay_balance_s'])) {
return false;
}
if (array_key_exists('payment_balance_enabled', $settings) && empty($settings['payment_balance_enabled'])) {
return false;
}
return true;
}
return !empty($settings[$key]);
}