修复两个问题

This commit is contained in:
2026-08-24 16:52:26 +08:00
parent 58f3f42bf8
commit 406fb8d6a6
4 changed files with 63 additions and 11 deletions
+35 -8
View File
@@ -40,23 +40,50 @@ trait BalanceServiceCharge {
$provider_transaction_id = sanitize_text_field($provider_transaction_id);
$locked_status = '';
$locked_note = [];
$guard = function () use ($order_number, $user_id, &$locked_status, &$locked_note) {
$locked_payment_method = '';
$locked_transaction_id = '';
$locked_paid_at = '';
$locked_completed_at = '';
$guard = function () use ($order_number, $user_id, $credit, $provider_transaction_id, &$locked_status, &$locked_note, &$locked_payment_method, &$locked_transaction_id, &$locked_paid_at, &$locked_completed_at) {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('order');
$current = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE order_number = %s FOR UPDATE", $order_number), ARRAY_A);
if (!is_array($current)
|| (int) ($current['user_id'] ?? 0) !== $user_id
|| sanitize_key((string) ($current['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE
|| !in_array((string) ($current['status'] ?? ''), [\AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_PENDING], true)
|| !in_array((string) ($current['status'] ?? ''), [
\AKSRT\Payment\Order::STATUS_CREATED,
\AKSRT\Payment\Order::STATUS_PENDING,
\AKSRT\Payment\Order::STATUS_PAID,
\AKSRT\Payment\Order::STATUS_COMPLETED,
], true)
) {
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态确认失败', 'aksrt'), ['status' => 409]);
}
$locked_status = (string) $current['status'];
$decoded = json_decode((string) ($current['note'] ?? ''), true);
$locked_note = is_array($decoded) ? $decoded : [];
$current_credit_raw = $locked_note['credit_amount'] ?? null;
try {
$current_credit = is_string($current_credit_raw)
? \AKSRT\Ledger\AssetAmount::balance($current_credit_raw)
: null;
} catch (\InvalidArgumentException $e) {
$current_credit = null;
}
if (!$current_credit || $current_credit->minor() !== $credit->minor()) {
return new \WP_Error('aksrt_recharge_amount_changed', __('充值到账金额已变化', 'aksrt'), ['status' => 409]);
}
$locked_payment_method = sanitize_key((string) ($current['payment_method'] ?? ''));
$locked_transaction_id = sanitize_text_field((string) ($current['payment_transaction_id'] ?? ''));
$locked_paid_at = trim((string) ($current['paid_at'] ?? ''));
$locked_completed_at = trim((string) ($current['completed_at'] ?? ''));
if ($provider_transaction_id !== '' && $locked_transaction_id !== '' && !hash_equals($locked_transaction_id, $provider_transaction_id)) {
return new \WP_Error('aksrt_recharge_transaction_conflict', __('充值交易号不一致', 'aksrt'), ['status' => 409]);
}
return true;
};
$mutation = function () use ($order_number, $payment_method, $provider_transaction_id, $status_note, $credit, &$locked_status, &$locked_note) {
$mutation = function () use ($order_number, $payment_method, $provider_transaction_id, $status_note, $credit, &$locked_status, &$locked_note, &$locked_payment_method, &$locked_transaction_id, &$locked_paid_at, &$locked_completed_at) {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('order');
$locked_note['ledger_credited'] = 1;
@@ -67,13 +94,13 @@ trait BalanceServiceCharge {
$now = current_time('mysql');
$updated = $wpdb->update($table, [
'status' => \AKSRT\Payment\Order::STATUS_COMPLETED,
'payment_method' => $payment_method,
'payment_transaction_id' => $provider_transaction_id,
'paid_at' => $now,
'completed_at' => $now,
'payment_method' => $payment_method !== '' ? $payment_method : $locked_payment_method,
'payment_transaction_id' => $provider_transaction_id !== '' ? $provider_transaction_id : $locked_transaction_id,
'paid_at' => $locked_paid_at !== '' ? $locked_paid_at : $now,
'completed_at' => $locked_completed_at !== '' ? $locked_completed_at : $now,
'note' => wp_json_encode($locked_note, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
], ['order_number' => $order_number, 'status' => $locked_status], ['%s', '%s', '%s', '%s', '%s', '%s'], ['%s', '%s']);
return $updated === 1 ? true : new \WP_Error('aksrt_order_confirm_failed', __('订单状态确认失败', 'aksrt'), ['status' => 409]);
return $updated !== false ? true : new \WP_Error('aksrt_order_confirm_failed', __('订单状态确认失败', 'aksrt'), ['status' => 409]);
};
$result = \AKSRT\Ledger\LedgerService::get_instance()->credit(
$user_id,
+10
View File
@@ -386,6 +386,16 @@ class Gateway {
// 用户可能重复付款,不能静默 ACK 为已正确同步。
if (in_array($current, ['paid', 'delivered', 'completed'], true)) {
$stored_transaction = sanitize_text_field((string) ($order['payment_transaction_id'] ?? ''));
if (sanitize_key((string) ($order['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE) {
if ($stored_transaction !== '' && !hash_equals($stored_transaction, $transaction_id)) {
return false;
}
return $repo->update_status($order_number, Order::STATUS_PAID, [
'payment_method' => $gateway_id,
'payment_transaction_id' => $transaction_id,
'note' => __('支付网关回调补充确认到账。', 'aksrt'),
]);
}
return $stored_transaction !== '' && hash_equals($stored_transaction, $transaction_id);
}
// 只允许从未支付态完成
+6 -3
View File
@@ -244,7 +244,7 @@ trait OrderRepositoryCore {
public function expire_pending_orders(?int $user_id = null): int {
global $wpdb;
$where = "status IN ('created', 'pending') AND expires_at IS NOT NULL AND expires_at <> '' AND expires_at <= %s";
$where = "status IN ('created', 'pending') AND expires_at IS NOT NULL AND expires_at <= %s";
$params = [gmdate('Y-m-d H:i:s')];
if ($user_id !== null && $user_id > 0) {
$where .= ' AND user_id = %d';
@@ -298,7 +298,10 @@ trait OrderRepositoryCore {
return false;
}
if (
$new_status === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID
in_array($new_status, [
\AKSRT\Core\DomainModel::ORDER_STATUS_PAID,
\AKSRT\Core\DomainModel::ORDER_STATUS_COMPLETED,
], true)
&& sanitize_key((string) ($preflight['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE
&& class_exists('\AKSRT\Credits\BalanceService')
) {
@@ -579,7 +582,7 @@ trait OrderRepositoryCore {
// Prefer cancelled_at when set; fall back to created_at for older rows.
$sql = "DELETE FROM {$table}
WHERE status IN ('cancelled', 'expired')
AND COALESCE(NULLIF(cancelled_at, ''), created_at) < %s";
AND COALESCE(cancelled_at, created_at) < %s";
$deleted = $wpdb->query($wpdb->prepare($sql, $cutoff));
return $deleted === false ? 0 : (int) $deleted;
}
@@ -204,3 +204,15 @@ test('asset purchases share the order center and refresh every wallet surface',
assert.match(header, /data-aksrt-wallet-value/)
assert.match(header, /data-aksrt-points-value/)
})
test('recharge completion always credits the ledger and expiry SQL is strict-mode safe', () => {
const repository = fs.readFileSync(path.join(root, 'src', 'Payment', 'OrderRepositoryCore.php'), 'utf8')
const charge = fs.readFileSync(path.join(root, 'src', 'Credits', 'BalanceServiceCharge.php'), 'utf8')
const gateway = fs.readFileSync(path.join(root, 'src', 'Payment', 'Gateway.php'), 'utf8')
assert.match(repository, /in_array\(\$new_status,\s*\[\s*\\AKSRT\\Core\\DomainModel::ORDER_STATUS_PAID,\s*\\AKSRT\\Core\\DomainModel::ORDER_STATUS_COMPLETED,\s*\],\s*true\)/)
assert.match(charge, /Order::STATUS_CREATED,\s*\\AKSRT\\Payment\\Order::STATUS_PENDING,\s*\\AKSRT\\Payment\\Order::STATUS_PAID,\s*\\AKSRT\\Payment\\Order::STATUS_COMPLETED/)
assert.match(gateway, /ORDER_PRODUCT_RECHARGE[\s\S]*?update_status\(\$order_number, Order::STATUS_PAID/)
assert.doesNotMatch(repository, /expires_at\s*<>\s*''/)
assert.doesNotMatch(repository, /NULLIF\(cancelled_at,\s*''\)/)
})