修复两个问题
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
// 只允许从未支付态完成
|
||||
|
||||
@@ -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*''\)/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user