feat(payment): rebuild order details and asset records

This commit is contained in:
kate522 committed 2026-10-05 16:42:25 +08:00
1 parent 476e2f961d
commit 7b55cfb190
57 files changed
+2080 -1294

No files matched your search

+62 -7
View File
@@ -133,6 +133,46 @@ class Withdrawal {
if ($net_minor < 1) {
return new \WP_Error('aksrt_withdraw_net', __('扣除手续费后到账金额必须至少为 0.01 元', 'aksrt'), ['status' => 400]);
}
$request_hash = hash('sha256', (string) wp_json_encode([
'user_id' => $user_id,
'gross_minor' => $gross_minor,
'net_minor' => $net_minor,
'fee_rate' => $fee_rate,
'method' => sanitize_key($method),
'account' => sanitize_text_field($account),
'source' => $source,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
$existing_request = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$this->table()} WHERE operation_key = %s LIMIT 1",
$operation_key
), ARRAY_A);
if (is_array($existing_request)) {
$existing_meta = $this->parse_account_meta((string) ($existing_request['account_info'] ?? ''));
if ((int) ($existing_request['user_id'] ?? 0) !== $user_id
|| !hash_equals((string) ($existing_meta['request_hash'] ?? ''), $request_hash)) {
return new \WP_Error('aksrt_withdraw_idempotency_conflict', __('相同提现操作标识已用于其他请求', 'aksrt'), ['status' => 409]);
}
$window = $this->daily_window((string) ($settings['reset_time'] ?? '00:00'));
$usage = $this->daily_usage_minor_in_window($user_id, $settings, $window, false, $source);
return [
'withdrawal_id' => (int) $existing_request['id'],
'withdrawal_number' => (string) ($existing_request['withdrawal_number'] ?? ''),
'net_amount' => $this->database_balance_decimal((string) ($existing_request['amount'] ?? '0.00')),
'source' => $source,
'wallet_balance' => $this->source_balance_decimal($user_id, $source),
'transaction_id' => max(0, (int) ($existing_request['ledger_transaction_id'] ?? 0)),
'flow_number' => $source === self::SOURCE_COMMISSION
? \AKSRT\Finance\FinancialFlowService::get_instance()->number_for_operation($operation_key)
: '',
'replayed' => true,
'usage' => [
'count' => $usage['count'],
'total' => $this->balance_decimal($usage['total_minor']),
'remaining_count' => $usage['remaining_count'],
'remaining_amount' => $this->balance_decimal($usage['remaining_minor']),
],
];
}
$window = $this->daily_window((string) ($settings['reset_time'] ?? '00:00'));
$usage = null;
$withdrawal_id = 0;
@@ -163,7 +203,7 @@ class Withdrawal {
}
return true;
};
$mutation = function (int $transaction_id) use ($user_id, $gross_minor, $net_minor, $fee_rate, $method, $account, $operation_key, $source, &$withdrawal_id, &$withdrawal_number) {
$mutation = function (int $transaction_id) use ($user_id, $gross_minor, $net_minor, $fee_rate, $method, $account, $operation_key, $request_hash, $source, &$withdrawal_id, &$withdrawal_number) {
global $wpdb;
$payload = wp_json_encode([
'account' => $account,
@@ -171,11 +211,12 @@ class Withdrawal {
'fee_rate' => $fee_rate,
'source' => $source,
'operation_key' => $operation_key,
'request_hash' => $request_hash,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: $account;
$ok = false;
for ($attempt = 1; $attempt <= 5; $attempt++) {
$withdrawal_number = \AKSRT\Finance\BusinessNumber::allocate();
$ok = $wpdb->insert($this->table(), [
$insert_data = [
'withdrawal_number' => $withdrawal_number,
'user_id' => $user_id,
'amount' => \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, $net_minor)->decimal(),
@@ -183,9 +224,15 @@ class Withdrawal {
'method' => sanitize_key($method),
'account_info' => $payload,
'status' => self::STATUS_PENDING,
'ledger_transaction_id' => $transaction_id,
'operation_key' => $operation_key,
'created_at' => current_time('mysql'),
], ['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%d', '%s']);
];
$insert_formats = ['%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s'];
if ($transaction_id > 0) {
$insert_data['ledger_transaction_id'] = $transaction_id;
$insert_formats[] = '%d';
}
$ok = $wpdb->insert($this->table(), $insert_data, $insert_formats);
if ($ok === 1) {
break;
}
@@ -214,7 +261,9 @@ class Withdrawal {
}
if ($withdrawal_id <= 0) {
$replayed_row = $wpdb->get_row($wpdb->prepare(
"SELECT id, withdrawal_number FROM {$this->table()} WHERE ledger_transaction_id = %d",
"SELECT id, withdrawal_number FROM {$this->table()} WHERE operation_key = %s OR (%d > 0 AND ledger_transaction_id = %d) ORDER BY id DESC LIMIT 1",
$operation_key,
$result->transaction_id,
$result->transaction_id
), ARRAY_A);
$withdrawal_id = (int) ($replayed_row['id'] ?? 0);
@@ -329,7 +378,7 @@ class Withdrawal {
return $preflight_replay;
}
if (is_array($preflight_replay)) {
$preflight_replay['wallet_balance'] = \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal((int) ($row['user_id'] ?? 0));
$preflight_replay['wallet_balance'] = $this->source_balance_decimal((int) ($row['user_id'] ?? 0), $this->normalize_source((string) ($meta['source'] ?? self::SOURCE_BALANCE)));
$preflight_replay['withdrawal_number'] = (string) ($row['withdrawal_number'] ?? '');
$preflight_replay['flow_number'] = \AKSRT\Finance\FinancialFlowService::get_instance()->number_for_operation($operation_key);
return $preflight_replay;
@@ -497,10 +546,16 @@ class Withdrawal {
'flow_number' => $ledger_result instanceof \AKSRT\Ledger\LedgerResult ? $ledger_result->flow_number : $completed_flow_number,
'withdrawal_number' => (string) ($row['withdrawal_number'] ?? ''),
'replayed' => $replayed,
'wallet_balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
'wallet_balance' => $this->source_balance_decimal($user_id, $refund_source),
];
}
private function source_balance_decimal(int $user_id, string $source): string {
$asset = $this->source_asset($source);
$minor = \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, $asset);
return \AKSRT\Ledger\AssetAmount::from_minor($asset, $minor)->decimal();
}
/**
* Whether user has open (pending/processing) withdrawals.
*/