卡密充值与兑换接入统一订单号与支付流水号
此前卡密核销虽已建订单,但订单号硬编码 CARD{卡ID}、渠道交易号硬编码
card_pass_{卡ID},均不受后台前缀控制;且核销产生的资金流水没有挂到
订单上(order_number 为 NULL),订单详情页的「支付流水号」恒为「—」。
ChargeCard:
- 新增 allocate_redeem_order_number():走 OrderRepository 统一分配器,
消费后台「订单前缀」(payment_order_prefix)。
- 新增 card_pass_transaction_number():BusinessNumber::candidate(flow_prefix),
消费后台「流水号前缀」(payment_flow_prefix)。
- 两条兑换路径(redeem_asset_card / redeem_vip_card)替换硬编码取号;
create_redeem_order 接收调用方传入的交易号。
- 账本重放(replayed)时回取已落库订单号,不再把本次新分配的号回给前端。
FinancialFlowService:
- record_card_pass_payment():会员卡(0 元、无账本参与)直接落一条
与订单关联、零分录的 order_payment 流水;record() 新增
allow_empty_entries 通道(prepare_entries 拒绝 delta=0,占位分录不可行)。
- record_order_event() 新增凭证结算模式:豁免「金额必须 > 0」与
「资产类订单不得记现金流水」两条门槛(卡密核销既无第三方现金流入,
订单还可能为 0 元或以积分计价)。
- attach_order_to_ledger_flow():账本提交后把流水补挂到订单上,
event_type 归一为 order_payment,使「支付流水号」可反查。
- is_successful_order_payment() 认可 card_pass 凭证结算;
is_external_recharge() 把 card_pass 与 balance/points 一并列为平台内部
结算方式,不再为卡密充值伪造现金出账分录(现金在售卡时已确认)。
关键取舍:随机订单号不得进入 LedgerCommand 的 metadata —— payload_hash
覆盖 metadata,会破坏账本幂等重放(实测撞 uniq_operation_key 后报
aksrt_ledger_idempotency_conflict),因此订单关联改在账本提交后补挂。
测试:新增 tests/admin/card-pass-order-numbers.test.cjs 钉住上述约束;
容器内三类卡密实测通过(订单号/渠道交易号/支付流水号三项均带后台前缀,
流水行分别携带 balance:in / points:in / 零分录),重放幂等恢复正常。
This commit is contained in:
@@ -444,7 +444,11 @@ class ChargeCard {
|
||||
if ($card_id <= 0 || $amount_minor <= 0) {
|
||||
return new \WP_Error('aksrt_card_amount', __('当前卡密可兑换资产为 0', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$order_number = 'CARD' . $card_id;
|
||||
$order_number = $this->allocate_redeem_order_number();
|
||||
if (is_wp_error($order_number)) {
|
||||
return $order_number;
|
||||
}
|
||||
$transaction_number = $this->card_pass_transaction_number();
|
||||
$guard = function () use ($card_id, $user_id, $submitted_card, $type) {
|
||||
global $wpdb;
|
||||
$current = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table()} WHERE id = %d FOR UPDATE", $card_id), ARRAY_A);
|
||||
@@ -465,9 +469,9 @@ class ChargeCard {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
$mutation = function () use ($user_id, $type, $meta, $card_id, $order_number, $operation_key) {
|
||||
$mutation = function () use ($user_id, $type, $meta, $card_id, $order_number, $operation_key, $transaction_number) {
|
||||
global $wpdb;
|
||||
$created = $this->create_redeem_order($user_id, $type, $meta, $card_id, '', $operation_key, $order_number, Order::STATUS_PAID);
|
||||
$created = $this->create_redeem_order($user_id, $type, $meta, $card_id, '', $operation_key, $order_number, Order::STATUS_PAID, $transaction_number);
|
||||
if ($created !== $order_number) {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
@@ -498,6 +502,22 @@ class ChargeCard {
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
if ($result->replayed) {
|
||||
// 重放不会重新建单,上面分配的订单号并未落库;这里改从既有订单取回真实号,
|
||||
// 避免把一个新号回给前端。
|
||||
$existing_order = OrderRepository::get_instance()->get_by_request_key($operation_key);
|
||||
if (is_array($existing_order) && (string) ($existing_order['order_number'] ?? '') !== '') {
|
||||
$order_number = (string) $existing_order['order_number'];
|
||||
}
|
||||
} else {
|
||||
// 账本命令必须保持确定性(payload_hash 含 metadata,塞入随机订单号会破坏幂等重放),
|
||||
// 因此在账本提交之后再把流水行挂到订单上。
|
||||
\AKSRT\Finance\FinancialFlowService::get_instance()->attach_order_to_ledger_flow(
|
||||
(int) $result->transaction_id,
|
||||
$order_number,
|
||||
$transaction_number
|
||||
);
|
||||
}
|
||||
$balance = $result->balance($user_id, $asset);
|
||||
return [
|
||||
'success' => true,
|
||||
@@ -533,11 +553,15 @@ class ChargeCard {
|
||||
$card_id = (int) ($row['id'] ?? 0);
|
||||
$level = max(1, min(2, (int) ($meta['level'] ?? 1)));
|
||||
$days = max(0, (int) ($meta['days'] ?? 0));
|
||||
$order_number = 'CARD' . $card_id;
|
||||
$order_table = \AKSRT\Core\DBMigrator::table('order');
|
||||
if ($card_id <= 0 || sanitize_key((string) ($row['type'] ?? '')) !== self::TYPE_VIP) {
|
||||
return new \WP_Error('aksrt_card_type', __('当前卡密不是会员卡', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$order_number = $this->allocate_redeem_order_number();
|
||||
if (is_wp_error($order_number)) {
|
||||
return $order_number;
|
||||
}
|
||||
$transaction_number = $this->card_pass_transaction_number();
|
||||
|
||||
for ($attempt = 1; $attempt <= 3; $attempt++) {
|
||||
if ($wpdb->query('START TRANSACTION') === false) {
|
||||
@@ -595,12 +619,28 @@ class ChargeCard {
|
||||
(string) ($current['password'] ?? ''),
|
||||
$operation_key,
|
||||
$order_number,
|
||||
Order::STATUS_PAID
|
||||
Order::STATUS_PAID,
|
||||
$transaction_number
|
||||
);
|
||||
if ($created !== $order_number) {
|
||||
throw new \RuntimeException('vip_card_order_create_failed');
|
||||
}
|
||||
|
||||
// 会员卡没有账本参与(入账的是 usermeta 而非账本资产),流水要单独补一条,
|
||||
// 且它不产生任何现金/资产分录(0 元凭证)。同一事务内写入,失败即整体回滚。
|
||||
$flow_number = \AKSRT\Finance\FinancialFlowService::get_instance()->record_card_pass_payment([
|
||||
'order_number' => $order_number,
|
||||
'user_id' => $user_id,
|
||||
'amount' => '0.00',
|
||||
'currency' => 'CNY',
|
||||
'status' => Order::STATUS_PAID,
|
||||
'payment_method' => 'card_pass',
|
||||
'payment_transaction_id' => $transaction_number,
|
||||
], true);
|
||||
if (is_wp_error($flow_number)) {
|
||||
throw new \RuntimeException('vip_card_flow_failed');
|
||||
}
|
||||
|
||||
\AKSRT\Content\VipService::get_instance()->activate_vip($user_id, $level, $days);
|
||||
$actual_level = max(0, (int) get_user_meta($user_id, 'aksrt_vip_level', true));
|
||||
$actual_expiry = max(0, (int) get_user_meta($user_id, 'aksrt_vip_expires', true));
|
||||
@@ -730,10 +770,40 @@ class ChargeCard {
|
||||
return preg_match('/\\b(1205|1213)\\b/', (string) ($wpdb->last_error ?? ''), $matches) ? (int) $matches[1] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡密核销订单号:走统一的订单号分配器,受后台「订单前缀」控制
|
||||
* (Settings::get_order_settings()['prefix'],默认 AKSRT)。
|
||||
*
|
||||
* @return string|\WP_Error
|
||||
*/
|
||||
private function allocate_redeem_order_number() {
|
||||
if (!class_exists('AKSRT\\Payment\\OrderRepository')) {
|
||||
return new \WP_Error('aksrt_order_unavailable', __('订单系统不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$order_number = OrderRepository::get_instance()->allocate_order_number();
|
||||
if (!is_string($order_number) || $order_number === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
return $order_number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡密核销订单的渠道交易号:与资金流水同源,受后台「流水号前缀」控制
|
||||
* (Settings::get_order_settings()['flow_prefix'],默认 AK)。
|
||||
*/
|
||||
private function card_pass_transaction_number(): string {
|
||||
$prefix = 'AK';
|
||||
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_order_settings')) {
|
||||
$settings = \AKSRT\Settings\Settings::get_order_settings();
|
||||
$prefix = \AKSRT\Finance\BusinessNumber::normalize_prefix($settings['flow_prefix'] ?? 'AK', 'AK');
|
||||
}
|
||||
return \AKSRT\Finance\BusinessNumber::candidate($prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $meta
|
||||
*/
|
||||
private function create_redeem_order(int $user_id, string $type, array $meta, int $card_id, string $password, string $request_key = '', string $order_number = '', string $status = ''): string {
|
||||
private function create_redeem_order(int $user_id, string $type, array $meta, int $card_id, string $password, string $request_key = '', string $order_number = '', string $status = '', string $transaction_number = ''): string {
|
||||
if (!class_exists('AKSRT\\Payment\\OrderRepository') || !class_exists('\AKSRT\Core\DomainModel')) {
|
||||
return '';
|
||||
}
|
||||
@@ -774,7 +844,7 @@ class ChargeCard {
|
||||
'currency' => $currency,
|
||||
'status' => $order_status,
|
||||
'payment_method' => 'card_pass',
|
||||
'payment_transaction_id' => $order_status === Order::STATUS_PAID ? 'card_pass_' . $card_id : '',
|
||||
'payment_transaction_id' => $order_status === Order::STATUS_PAID ? $transaction_number : '',
|
||||
'paid_at' => $order_status === Order::STATUS_PAID ? current_time('mysql') : null,
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'request_key' => $request_key,
|
||||
|
||||
Reference in New Issue
Block a user