积分与余额账务系统硬切重构
This commit is contained in:
+193
-54
@@ -10,7 +10,7 @@ defined('ABSPATH') || exit;
|
||||
/**
|
||||
* 站内余额支付网关(内置实现)。
|
||||
*
|
||||
* 仅扣现金钱包(\AKSRT\Credits\Balance / pay_balance_s),不再回退到积分。
|
||||
* 仅扣统一资产账本中的现金余额。
|
||||
*/
|
||||
|
||||
class BalanceGateway implements GatewayInterface {
|
||||
@@ -24,8 +24,7 @@ class BalanceGateway implements GatewayInterface {
|
||||
}
|
||||
|
||||
public function is_configured(): bool {
|
||||
// Cash wallet only — do not treat points as 余额.
|
||||
if (!class_exists('\AKSRT\Credits\Balance') || !\AKSRT\Credits\Balance::enabled()) {
|
||||
if (!$this->balance_enabled()) {
|
||||
return false;
|
||||
}
|
||||
if (class_exists('\AKSRT\Settings\PaymentRuntime')) {
|
||||
@@ -36,32 +35,85 @@ class BalanceGateway implements GatewayInterface {
|
||||
|
||||
public function create_payment(array $order): array {
|
||||
$user_id = (int) ($order['user_id'] ?? 0);
|
||||
$amount = round((float) ($order['amount'] ?? 0), 2);
|
||||
$order_number = (string) ($order['order_number'] ?? '');
|
||||
|
||||
if (!class_exists('\AKSRT\Credits\Balance') || !\AKSRT\Credits\Balance::enabled()) {
|
||||
if (!$this->balance_enabled()) {
|
||||
return ['success' => false, 'message' => __('余额功能未启用', 'aksrt')];
|
||||
}
|
||||
if ($user_id <= 0) {
|
||||
return ['success' => false, 'message' => __('请先登录', 'aksrt')];
|
||||
}
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('order');
|
||||
$amount = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT amount FROM {$table} WHERE order_number = %s",
|
||||
$order_number
|
||||
));
|
||||
try {
|
||||
$amount_minor = \AKSRT\Ledger\AssetAmount::balance(is_string($amount) ? $amount : '')->minor();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
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(
|
||||
$transaction_code = 'BAL_' . $order_number;
|
||||
$guard = static function () use ($order_number, $user_id, $amount_minor) {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('order');
|
||||
$current = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT user_id, amount, status FROM {$table} WHERE order_number = %s FOR UPDATE",
|
||||
$order_number
|
||||
), ARRAY_A);
|
||||
try {
|
||||
$current_minor = is_array($current)
|
||||
? \AKSRT\Ledger\AssetAmount::balance((string) ($current['amount'] ?? ''))->minor()
|
||||
: -1;
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$current_minor = -1;
|
||||
}
|
||||
if (!is_array($current)
|
||||
|| (int) ($current['user_id'] ?? 0) !== $user_id
|
||||
|| $current_minor !== $amount_minor
|
||||
|| !in_array((string) ($current['status'] ?? ''), [Order::STATUS_CREATED, Order::STATUS_PENDING], true)
|
||||
) {
|
||||
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态或金额已变化', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
$result = \AKSRT\Ledger\LedgerService::get_instance()->debit(
|
||||
$user_id,
|
||||
$amount,
|
||||
\AKSRT\Ledger\AssetType::BALANCE,
|
||||
$amount_minor,
|
||||
\AKSRT\Ledger\OperationKey::deterministic('order-payment', [$order_number]),
|
||||
'order_payment',
|
||||
['type' => 'order', 'id' => 0, 'order' => $order_number],
|
||||
sprintf(__('订单 %s 支付', 'aksrt'), $order_number)
|
||||
['type' => 'order', 'key' => $order_number],
|
||||
sprintf(__('订单 %s 支付', 'aksrt'), $order_number),
|
||||
$user_id,
|
||||
['order_number' => $order_number],
|
||||
static function () use ($order_number, $transaction_code, $order) {
|
||||
$paid = OrderRepository::get_instance()->update_status($order_number, Order::STATUS_PAID, [
|
||||
'payment_method' => 'balance',
|
||||
'payment_transaction_id' => $transaction_code,
|
||||
'note' => (string) ($order['_ledger_paid_note'] ?? __('余额支付成功', 'aksrt')),
|
||||
'require_transition' => true,
|
||||
]);
|
||||
return $paid ? true : new \WP_Error('aksrt_order_confirm_failed', __('订单状态确认失败', 'aksrt'), ['status' => 409]);
|
||||
},
|
||||
$guard
|
||||
);
|
||||
if (is_wp_error($result)) {
|
||||
return ['success' => false, 'message' => $result->get_error_message() ?: __('扣款失败', 'aksrt')];
|
||||
$error_data = $result->get_error_data();
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => $result->get_error_code(),
|
||||
'status' => is_array($error_data) ? (int) ($error_data['status'] ?? 500) : 500,
|
||||
'message' => $result->get_error_message() ?: __('扣款失败', 'aksrt'),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'transaction_id' => 'BAL_' . ($order_number !== '' ? $order_number : (string) time()),
|
||||
'transaction_id' => $transaction_code,
|
||||
'ledger_transaction_id' => $result->transaction_id,
|
||||
'replayed' => $result->replayed,
|
||||
'order_committed' => true,
|
||||
'message' => __('余额支付成功', 'aksrt'),
|
||||
];
|
||||
}
|
||||
@@ -86,7 +138,7 @@ class BalanceGateway implements GatewayInterface {
|
||||
];
|
||||
}
|
||||
|
||||
public function refund(string $order_number, float $amount, string $request_id = ''): array {
|
||||
public function refund(string $order_number, string $amount, string $request_id = ''): array {
|
||||
$order_repo = OrderRepository::get_instance();
|
||||
$order = $order_repo->get($order_number);
|
||||
|
||||
@@ -95,53 +147,140 @@ 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()) {
|
||||
if (!$this->balance_enabled()) {
|
||||
return ['success' => false, 'message' => __('余额功能未启用', 'aksrt')];
|
||||
}
|
||||
|
||||
$request_id = sanitize_text_field($request_id);
|
||||
$refund_key = $request_id !== '' ? $request_id : 'order:' . $order_number;
|
||||
$lock_key = 'aksrt_balance_refund_' . hash('sha256', $refund_key);
|
||||
$existing = get_option($lock_key, []);
|
||||
if (is_array($existing) && ($existing['status'] ?? '') === 'succeeded') {
|
||||
return [
|
||||
'success' => true,
|
||||
'refund_id' => 'BAL_REF_' . $order_number,
|
||||
'message' => __('退款已处理(幂等)', 'aksrt'),
|
||||
];
|
||||
}
|
||||
if (is_array($existing) && ($existing['status'] ?? '') === 'pending') {
|
||||
return ['success' => false, 'status' => 'pending', 'message' => __('退款正在处理中,请稍后查询', 'aksrt')];
|
||||
}
|
||||
|
||||
$marker = ['status' => 'pending', 'token' => wp_generate_uuid4(), 'created_at' => time()];
|
||||
if (!add_option($lock_key, $marker, '', false)) {
|
||||
return ['success' => false, 'status' => 'pending', 'message' => __('退款正在处理中,请稍后查询', 'aksrt')];
|
||||
}
|
||||
|
||||
try {
|
||||
$result = \AKSRT\Credits\Balance::add(
|
||||
$user_id,
|
||||
$amount,
|
||||
'order_refund',
|
||||
['type' => 'order', 'order' => $order_number, 'request_id' => $request_id],
|
||||
sprintf(__('订单 %s 退款', 'aksrt'), $order_number)
|
||||
);
|
||||
try {
|
||||
$amount_minor = \AKSRT\Ledger\AssetAmount::balance($amount)->minor();
|
||||
if ($amount_minor <= 0) {
|
||||
return ['success' => false, 'message' => __('退款金额无效', 'aksrt')];
|
||||
}
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::deterministic('order-refund', [$request_id !== '' ? $request_id : $order_number]);
|
||||
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $operation_key, 'page' => 1, 'per_page' => 1]);
|
||||
if (!empty($history['items'][0])) {
|
||||
$committed = $history['items'][0];
|
||||
$refund_entry = null;
|
||||
foreach ((array) ($committed['entries'] ?? []) as $entry) {
|
||||
if ((int) ($entry['user_id'] ?? 0) === $user_id
|
||||
&& (string) ($entry['asset'] ?? '') === \AKSRT\Ledger\AssetType::BALANCE->value
|
||||
&& (string) ($entry['entry_type'] ?? '') === 'refund'
|
||||
) {
|
||||
$refund_entry = $entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((string) ($committed['reason'] ?? '') !== 'order_refund'
|
||||
|| (string) ($committed['ref_type'] ?? '') !== 'order'
|
||||
|| (string) ($committed['ref_key'] ?? '') !== $order_number
|
||||
|| !is_array($refund_entry)
|
||||
|| (int) ($refund_entry['delta_minor'] ?? 0) !== $amount_minor
|
||||
) {
|
||||
return ['success' => false, 'message' => __('相同退款标识已用于其他请求', 'aksrt')];
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'refund_id' => 'BAL_REF_' . $order_number,
|
||||
'transaction_id' => (int) ($committed['id'] ?? 0),
|
||||
'ledger_transaction_id' => (int) ($committed['id'] ?? 0),
|
||||
'replayed' => true,
|
||||
'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||
'message' => __('退款成功', 'aksrt'),
|
||||
];
|
||||
}
|
||||
$commission = CommissionLedger::get_instance();
|
||||
$commission_plan = $commission->prepare_full_refund($order_number);
|
||||
if (is_wp_error($commission_plan)) {
|
||||
return ['success' => false, 'message' => $commission_plan->get_error_message()];
|
||||
}
|
||||
$guard = static function () use ($order_number, $user_id, $amount_minor, $commission, $commission_plan) {
|
||||
global $wpdb;
|
||||
$table = \AKSRT\Core\DBMigrator::table('order');
|
||||
$current = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT user_id, amount, payment_method, status FROM {$table} WHERE order_number = %s FOR UPDATE",
|
||||
$order_number
|
||||
), ARRAY_A);
|
||||
try {
|
||||
$order_minor = is_array($current)
|
||||
? \AKSRT\Ledger\AssetAmount::balance((string) ($current['amount'] ?? ''))->minor()
|
||||
: -1;
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$order_minor = -1;
|
||||
}
|
||||
if (!is_array($current)
|
||||
|| (int) ($current['user_id'] ?? 0) !== $user_id
|
||||
|| sanitize_key((string) ($current['payment_method'] ?? '')) !== 'balance'
|
||||
|| $order_minor !== $amount_minor
|
||||
|| !in_array((string) ($current['status'] ?? ''), [Order::STATUS_PAID, Order::STATUS_DELIVERED, Order::STATUS_COMPLETED, Order::STATUS_REFUNDING], true)
|
||||
) {
|
||||
return new \WP_Error('aksrt_order_refund_conflict', __('退款订单状态或金额已变化', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
return $commission->guard_full_refund($commission_plan);
|
||||
};
|
||||
$mutation = static function () use ($order_number, $commission, $commission_plan) {
|
||||
$repo = OrderRepository::get_instance();
|
||||
$current = $repo->get($order_number);
|
||||
if (!is_array($current)) {
|
||||
return new \WP_Error('aksrt_order_refund_failed', __('退款订单不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
if ((string) ($current['status'] ?? '') !== Order::STATUS_REFUNDING
|
||||
&& !$repo->update_status($order_number, Order::STATUS_REFUNDING, ['note' => __('余额退款处理中', 'aksrt')])) {
|
||||
return new \WP_Error('aksrt_order_refund_failed', __('退款状态更新失败', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
if (!$repo->update_status($order_number, Order::STATUS_REFUNDED, ['note' => __('余额退款已完成', 'aksrt')])) {
|
||||
return new \WP_Error('aksrt_order_refund_failed', __('退款状态更新失败', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
return $commission->apply_full_refund($commission_plan);
|
||||
};
|
||||
$entries = [[
|
||||
'user_id' => $user_id,
|
||||
'asset' => \AKSRT\Ledger\AssetType::BALANCE->value,
|
||||
'delta_minor' => $amount_minor,
|
||||
'entry_type' => 'refund',
|
||||
'remark' => sprintf(__('订单 %s 退款', 'aksrt'), $order_number),
|
||||
]];
|
||||
if (!empty($commission_plan['active']) && (string) ($commission_plan['status'] ?? '') === 'settled') {
|
||||
$entries[] = [
|
||||
'user_id' => (int) $commission_plan['promoter_id'],
|
||||
'asset' => \AKSRT\Ledger\AssetType::BALANCE->value,
|
||||
'delta_minor' => -(int) $commission_plan['amount_minor'],
|
||||
'entry_type' => 'commission_reversal',
|
||||
'counterparty_user_id' => $user_id,
|
||||
'remark' => sprintf(__('订单 %s 返佣退款冲正', 'aksrt'), $order_number),
|
||||
];
|
||||
}
|
||||
$result = \AKSRT\Ledger\LedgerService::get_instance()->post(new \AKSRT\Ledger\LedgerCommand(
|
||||
$operation_key,
|
||||
'order_refund',
|
||||
$entries,
|
||||
'order',
|
||||
$order_number,
|
||||
0,
|
||||
['request_id' => $request_id, 'commission_id' => (int) ($commission_plan['id'] ?? 0)]
|
||||
), $guard, $mutation);
|
||||
if (is_wp_error($result)) {
|
||||
delete_option($lock_key);
|
||||
return ['success' => false, 'message' => $result->get_error_message() ?: __('退款失败', 'aksrt')];
|
||||
}
|
||||
update_option($lock_key, [
|
||||
'status' => 'succeeded',
|
||||
'refund_id' => 'BAL_REF_' . $order_number,
|
||||
'completed_at' => time(),
|
||||
], false);
|
||||
return ['success' => true, 'refund_id' => 'BAL_REF_' . $order_number, 'message' => __('退款成功', 'aksrt')];
|
||||
return [
|
||||
'success' => true,
|
||||
'refund_id' => 'BAL_REF_' . $order_number,
|
||||
'transaction_id' => $result->transaction_id,
|
||||
'ledger_transaction_id' => $result->transaction_id,
|
||||
'replayed' => $result->replayed,
|
||||
'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||
'message' => __('退款成功', 'aksrt'),
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
delete_option($lock_key);
|
||||
return ['success' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
private function balance_enabled(): bool {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
return false;
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_payment_recharge_settings();
|
||||
return !empty($settings['enabled']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user