积分与余额账务系统硬切重构
This commit is contained in:
@@ -41,13 +41,19 @@ trait OrderRepositoryCore {
|
||||
$note = sanitize_textarea_field($note_raw);
|
||||
}
|
||||
|
||||
$row = [
|
||||
$currency = sanitize_key((string) ($data['currency'] ?? 'CNY'));
|
||||
$amount = $this->normalize_order_amount($data['amount'] ?? null, $currency);
|
||||
if ($amount === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$row = [
|
||||
'order_number' => $order_number,
|
||||
'user_id' => (int) ($data['user_id'] ?? 0),
|
||||
'product_type' => sanitize_key((string) ($data['product_type'] ?? '')),
|
||||
'product_id' => (int) ($data['product_id'] ?? 0),
|
||||
'amount' => (float) ($data['amount'] ?? 0),
|
||||
'currency' => sanitize_key((string) ($data['currency'] ?? 'CNY')),
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'status' => \AKSRT\Core\DomainModel::normalize_order_status((string) ($data['status'] ?? \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING)),
|
||||
'payment_method' => sanitize_key((string) ($data['payment_method'] ?? '')),
|
||||
'payment_transaction_id' => sanitize_text_field((string) ($data['payment_transaction_id'] ?? '')),
|
||||
@@ -55,7 +61,7 @@ trait OrderRepositoryCore {
|
||||
'paid_at' => $data['paid_at'] ?? null,
|
||||
'note' => $note,
|
||||
];
|
||||
$formats = ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s'];
|
||||
$formats = ['%s', '%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s'];
|
||||
|
||||
// Pending orders always receive an expiry. Explicit caller values remain
|
||||
// supported for compatibility, while new flows use the shared runtime policy.
|
||||
@@ -67,6 +73,11 @@ trait OrderRepositoryCore {
|
||||
$formats[] = '%s';
|
||||
}
|
||||
|
||||
if (!empty($data['request_key'])) {
|
||||
$row['request_key'] = substr(sanitize_text_field((string) $data['request_key']), 0, 191);
|
||||
$formats[] = '%s';
|
||||
}
|
||||
|
||||
$wpdb->insert($this->table(), $row, $formats);
|
||||
|
||||
if ($wpdb->insert_id) {
|
||||
@@ -96,6 +107,19 @@ trait OrderRepositoryCore {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function get_by_request_key(string $request_key): ?array {
|
||||
global $wpdb;
|
||||
$request_key = substr(sanitize_text_field($request_key), 0, 191);
|
||||
if ($request_key === '') {
|
||||
return null;
|
||||
}
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE request_key = %s",
|
||||
$request_key
|
||||
), ARRAY_A);
|
||||
return is_array($row) ? $this->normalize_row($row) : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -264,12 +288,92 @@ trait OrderRepositoryCore {
|
||||
|
||||
|
||||
public function update_status(string $order_number, string $new_status, array $extra = []): bool {
|
||||
global $wpdb;
|
||||
|
||||
$preflight = $this->get($order_number);
|
||||
if (!$preflight) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
$new_status === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID
|
||||
&& sanitize_key((string) ($preflight['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE
|
||||
&& class_exists('\AKSRT\Credits\BalanceService')
|
||||
) {
|
||||
return \AKSRT\Credits\BalanceService::get_instance()->complete_recharge_payment(
|
||||
$preflight,
|
||||
sanitize_key((string) ($extra['payment_method'] ?? $preflight['payment_method'] ?? '')),
|
||||
sanitize_text_field((string) ($extra['payment_transaction_id'] ?? $preflight['payment_transaction_id'] ?? '')),
|
||||
(string) ($extra['note'] ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
$ledger_active = class_exists('\AKSRT\Ledger\LedgerService')
|
||||
&& \AKSRT\Ledger\LedgerService::get_instance()->transaction_active();
|
||||
$root_call = $this->status_transaction_depth === 0;
|
||||
if ($root_call) {
|
||||
$this->status_after_commit_events = [];
|
||||
}
|
||||
$owns_transaction = $root_call && !$ledger_active;
|
||||
if ($owns_transaction && $wpdb->query('START TRANSACTION') === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->status_transaction_depth++;
|
||||
try {
|
||||
$result = $this->update_status_in_transaction($order_number, $new_status, $extra);
|
||||
if (!$result) {
|
||||
if ($owns_transaction) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
}
|
||||
if ($root_call) {
|
||||
$this->status_after_commit_events = [];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if ($owns_transaction && $wpdb->query('COMMIT') === false) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
$this->status_after_commit_events = [];
|
||||
return false;
|
||||
}
|
||||
if ($root_call) {
|
||||
$events = $this->status_after_commit_events;
|
||||
$this->status_after_commit_events = [];
|
||||
if ($owns_transaction) {
|
||||
$this->dispatch_status_events($events);
|
||||
} elseif ($ledger_active) {
|
||||
\AKSRT\Ledger\LedgerService::get_instance()->after_commit(function () use ($events): void {
|
||||
$this->dispatch_status_events($events);
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
if ($owns_transaction) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
}
|
||||
if ($root_call) {
|
||||
$this->status_after_commit_events = [];
|
||||
}
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('[AKSRT Order] transactional status update failed: ' . $e->getMessage());
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
$this->status_transaction_depth--;
|
||||
}
|
||||
}
|
||||
|
||||
private function update_status_in_transaction(string $order_number, string $new_status, array $extra = []): bool {
|
||||
global $wpdb;
|
||||
|
||||
$valid = \AKSRT\Core\DomainModel::order_status_labels();
|
||||
if (!isset($valid[$new_status])) return false;
|
||||
|
||||
$order = $this->get($order_number);
|
||||
$order_row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE order_number = %s FOR UPDATE",
|
||||
$order_number
|
||||
), ARRAY_A);
|
||||
$order = is_array($order_row) ? $this->normalize_row($order_row) : null;
|
||||
if (!$order) return false;
|
||||
|
||||
$current_status = $order['status'] ?? 'created';
|
||||
@@ -370,8 +474,11 @@ trait OrderRepositoryCore {
|
||||
return is_array($latest) && ($latest['status'] ?? '') === $new_status;
|
||||
}
|
||||
|
||||
if ($result > 0) {
|
||||
do_action('aksrt_order_status_changed', $order_number, $new_status, array_merge($order, $update_data));
|
||||
if ($result > 0) {
|
||||
$this->status_after_commit_events[] = [
|
||||
'hook' => 'aksrt_order_status_changed',
|
||||
'args' => [$order_number, $new_status, array_merge($order, $update_data)],
|
||||
];
|
||||
// 到账事件:让充值/VIP/商品履约等挂接(单一履约入口)。
|
||||
// 仅在「首次」转为 paid 时触发,避免回调多次到达重复履约。
|
||||
if ($new_status === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID && $current_status !== \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) {
|
||||
@@ -398,19 +505,60 @@ trait OrderRepositoryCore {
|
||||
}
|
||||
}
|
||||
|
||||
if (class_exists('\AKSRT\Payment\CommissionLedger')) {
|
||||
$commission = \AKSRT\Payment\CommissionLedger::get_instance()->record_order_transition(
|
||||
$order_number,
|
||||
$new_status,
|
||||
$paid_order
|
||||
);
|
||||
if (is_wp_error($commission)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$product_type = sanitize_key((string) ($paid_order['product_type'] ?? ''));
|
||||
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE && class_exists('\AKSRT\Content\Pay')) {
|
||||
$fulfilled = \AKSRT\Content\Pay::get_instance()->fulfill_paid_order($order_number, $paid_order);
|
||||
if (is_wp_error($fulfilled)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP && class_exists('\AKSRT\Content\VipService')) {
|
||||
$fulfilled = \AKSRT\Content\VipService::get_instance()->fulfill_paid_order($order_number, $paid_order);
|
||||
if (is_wp_error($fulfilled)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单到账。
|
||||
*
|
||||
* @param string $order_number 订单号
|
||||
* @param array $paid_order 订单数据(status=paid)
|
||||
*/
|
||||
do_action('aksrt_order_paid', $order_number, $paid_order);
|
||||
$this->status_after_commit_events[] = [
|
||||
'hook' => 'aksrt_order_paid',
|
||||
'args' => [$order_number, $paid_order],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/** @param array<int,array{hook:string,args:array<int,mixed>}> $events */
|
||||
private function dispatch_status_events(array $events): void {
|
||||
foreach ($events as $event) {
|
||||
try {
|
||||
do_action((string) $event['hook'], ...$event['args']);
|
||||
} catch (\Throwable $e) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('[AKSRT Order Event] ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -494,7 +642,7 @@ trait OrderRepositoryCore {
|
||||
'refunding' => (int) ($row['refunding'] ?? 0),
|
||||
'cancelled' => (int) ($row['cancelled'] ?? 0),
|
||||
'refunded' => (int) ($row['refunded'] ?? 0),
|
||||
'revenue' => (float) ($row['revenue'] ?? 0),
|
||||
'revenue' => $this->cash_decimal((string) ($row['revenue'] ?? '0')),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -516,57 +664,6 @@ trait OrderRepositoryCore {
|
||||
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Migration bridge: option → table
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* 一次性迁移:把 option 中的订单搬到表中。
|
||||
* 幂等 — 已存在的 order_number 不会重复插入。
|
||||
*/
|
||||
public function migrate_from_option(): int {
|
||||
global $wpdb;
|
||||
|
||||
$orders = get_option('aksrt_orders', []);
|
||||
if (!is_array($orders) || empty($orders)) return 0;
|
||||
|
||||
$migrated = 0;
|
||||
foreach ($orders as $order_number => $data) {
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE order_number = %s",
|
||||
$order_number
|
||||
));
|
||||
if ($existing) continue;
|
||||
|
||||
$wpdb->insert($this->table(), [
|
||||
'order_number' => $order_number,
|
||||
'user_id' => (int) ($data['user_id'] ?? 0),
|
||||
'product_type' => sanitize_key((string) ($data['product_type'] ?? '')),
|
||||
'product_id' => (int) ($data['product_id'] ?? 0),
|
||||
'amount' => (float) ($data['amount'] ?? 0),
|
||||
'currency' => sanitize_key((string) ($data['currency'] ?? 'CNY')),
|
||||
'status' => \AKSRT\Core\DomainModel::normalize_order_status((string) ($data['status'] ?? \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING)),
|
||||
'payment_method' => sanitize_key((string) ($data['payment_method'] ?? '')),
|
||||
'created_at' => $data['created_at'] ?? current_time('mysql'),
|
||||
'paid_at' => $data['paid_at'] ?? null,
|
||||
'delivered_at' => $data['delivered_at'] ?? null,
|
||||
'completed_at' => $data['completed_at'] ?? null,
|
||||
'cancelled_at' => $data['cancelled_at'] ?? null,
|
||||
'refunded_at' => $data['refunded_at'] ?? null,
|
||||
'expires_at' => $data['expires_at'] ?? null,
|
||||
'note' => '',
|
||||
], ['%s', '%d', '%s', '%d', '%f', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s']);
|
||||
|
||||
if ($wpdb->insert_id) $migrated++;
|
||||
}
|
||||
|
||||
update_option(\AKSRT\Core\DomainModel::ORDER_MIGRATED_OPTION, time());
|
||||
|
||||
return $migrated;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Internal helpers
|
||||
* ----------------------------------------------------------------*/
|
||||
@@ -620,7 +717,10 @@ trait OrderRepositoryCore {
|
||||
$row['id'] = (int) $row['id'];
|
||||
$row['user_id'] = (int) $row['user_id'];
|
||||
$row['product_id'] = (int) $row['product_id'];
|
||||
$row['amount'] = (float) $row['amount'];
|
||||
$row['amount'] = $this->normalize_order_amount((string) ($row['amount'] ?? ''), (string) ($row['currency'] ?? ''), true);
|
||||
if ($row['amount'] === null) {
|
||||
$row['amount'] = (string) ($row['currency'] ?? '') === 'points' ? 0 : '0.00';
|
||||
}
|
||||
$row['status'] = \AKSRT\Core\DomainModel::normalize_order_status((string) ($row['status'] ?? ''));
|
||||
|
||||
$row['meta_payload'] = [];
|
||||
@@ -639,6 +739,39 @@ trait OrderRepositoryCore {
|
||||
return $row;
|
||||
}
|
||||
|
||||
private function normalize_order_amount(mixed $value, string $currency, bool $from_database = false): string|int|null {
|
||||
if ($currency === 'points') {
|
||||
$raw = is_int($value) ? (string) $value : (is_string($value) ? trim($value) : '');
|
||||
if ($from_database && preg_match('/^(0|[1-9]\d*)\.0+$/', $raw, $matches)) {
|
||||
$raw = $matches[1];
|
||||
}
|
||||
if (!preg_match('/^(0|[1-9]\d*)$/', $raw)) {
|
||||
return null;
|
||||
}
|
||||
$max = (string) PHP_INT_MAX;
|
||||
if (strlen($raw) > strlen($max) || (strlen($raw) === strlen($max) && strcmp($raw, $max) > 0)) {
|
||||
return null;
|
||||
}
|
||||
return (int) $raw;
|
||||
}
|
||||
if (!is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance(trim($value))->decimal();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function cash_decimal(string $value): string {
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance($value)->decimal();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return '0.00';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function find_latest_success_order(int $user_id, string $product_type, int $product_id = 0, bool $ignore_product_id = false): ?array {
|
||||
@@ -676,7 +809,11 @@ trait OrderRepositoryCore {
|
||||
* Column + major gateways (WeChat out_trade_no) cap at 32 chars, so the
|
||||
* prefix is trimmed to leave room for {YmdHis}{4 digits} (=18).
|
||||
*/
|
||||
private function generate_order_number(): string {
|
||||
public function allocate_order_number(): string {
|
||||
return $this->generate_order_number();
|
||||
}
|
||||
|
||||
private function generate_order_number(): string {
|
||||
$prefix = 'AKSRT';
|
||||
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_order_settings')) {
|
||||
$settings = \AKSRT\Settings\Settings::get_order_settings();
|
||||
|
||||
Reference in New Issue
Block a user