积分与余额账务系统硬切重构

This commit is contained in:
2026-08-24 15:59:11 +08:00
parent ca12226b39
commit 5cae5b5c93
159 changed files with 7275 additions and 4718 deletions
+175 -93
View File
@@ -143,7 +143,7 @@ trait VipServicePurchase {
? $this->parse_level_products((string) ($levels[$lv]['products_raw'] ?? ''))
: [];
foreach ($packages as $idx => $pkg) {
$base_price = (float) $pkg['price'];
$base_price = (string) $pkg['price'];
$days = (int) $pkg['days'];
$action = 'pay';
if ($level > 0 && $lv === $level) {
@@ -157,9 +157,11 @@ trait VipServicePurchase {
continue;
}
$show_price = (float) ($priced['show_price'] ?? 0);
if ($show_price <= 0 && !empty($pkg['show_price']) && $action === 'pay') {
$show_price = (float) $pkg['show_price'];
$show_price = (string) ($priced['show_price'] ?? '0.00');
$show_minor = $this->parse_vip_money_minor($show_price);
if ($show_minor <= 0 && $this->parse_vip_money_minor($pkg['show_price'] ?? '0') > 0 && $action === 'pay') {
$show_price = (string) $pkg['show_price'];
$show_minor = $this->parse_vip_money_minor($show_price);
}
$discount_tag = (string) ($priced['tag'] ?? '');
if ($discount_tag === '' && $action === 'pay' && !empty($pkg['tag'])) {
@@ -178,7 +180,7 @@ trait VipServicePurchase {
'level' => $lv,
'index' => $idx + 1,
'action' => $action,
'price' => (float) $priced['price'],
'price' => (string) $priced['price'],
'base_price' => $base_price,
'show_price' => $show_price,
'discount_tag' => $discount_tag,
@@ -186,8 +188,8 @@ trait VipServicePurchase {
'time' => (int) ($pkg['time'] ?? 0),
'unit' => (string) ($pkg['unit'] ?? 'day'),
'title' => (string) $pkg['title'],
'price_label' => $this->format_cash_label((float) $priced['price']),
'show_price_label' => $show_price > 0
'price_label' => $this->format_cash_label((string) $priced['price']),
'show_price_label' => $show_minor > 0
? $this->format_cash_label($show_price)
: '',
'days_label' => $days_label,
@@ -230,12 +232,8 @@ trait VipServicePurchase {
}
}
$points_balance = ($user_id > 0 && class_exists('\AKSRT\Credits\Credits'))
? (int) \AKSRT\Credits\Credits::balance($user_id)
: 0;
$wallet_balance = ($user_id > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled())
? (float) \AKSRT\Credits\Balance::balance($user_id)
: 0.0;
$points_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS) : 0;
$wallet_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : '0.00';
$methods = $this->available_payment_methods();
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
@@ -304,7 +302,7 @@ trait VipServicePurchase {
/**
* @return array|\WP_Error
*/
public function purchase(int $user_id, int $package_id, string $payment_method = 'balance') {
public function purchase(int $user_id, int $package_id, string $payment_method = 'balance', string $operation_key = '') {
if ($user_id <= 0) {
return new \WP_Error('aksrt_need_login', __('请先登录后再开通会员。', 'aksrt'), ['status' => 401]);
}
@@ -322,8 +320,9 @@ trait VipServicePurchase {
$level = (int) ($pkg['level'] ?? 0);
$index = (int) ($pkg['index'] ?? 0);
$action = sanitize_key((string) ($pkg['action'] ?? 'pay'));
$amount = round((float) ($pkg['price'] ?? 0), 2);
$base_price = (float) ($pkg['base_price'] ?? $amount);
$amount = is_string($pkg['price'] ?? null) ? (string) $pkg['price'] : '';
$amount_minor = $this->parse_vip_money_minor($amount);
$base_price = is_string($pkg['base_price'] ?? null) ? (string) $pkg['base_price'] : $amount;
$days = (int) ($pkg['days'] ?? 0);
$pkg_time = array_key_exists('time', $pkg) ? (int) $pkg['time'] : $days;
$pkg_unit = sanitize_key((string) ($pkg['unit'] ?? 'day'));
@@ -341,9 +340,10 @@ trait VipServicePurchase {
return new \WP_Error('aksrt_invalid_package', __('套餐无效', 'aksrt'), ['status' => 400]);
}
if ($amount <= 0) {
if ($amount_minor <= 0) {
return new \WP_Error('aksrt_invalid_price', __('套餐价格无效', 'aksrt'), ['status' => 400]);
}
$amount_decimal = \AKSRT\Ledger\AssetAmount::decimal_from_minor($amount_minor);
if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) {
return new \WP_Error('aksrt_pay_unavailable', __('支付系统不可用', 'aksrt'), ['status' => 500]);
@@ -370,21 +370,63 @@ trait VipServicePurchase {
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$expires_at = \AKSRT\Settings\PaymentRuntime::get_order_expiry_at();
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$existing = $operation_key !== '' ? $repo->get_by_request_key($operation_key) : null;
$is_new_order = !is_array($existing);
if (is_array($existing)) {
$existing_meta = json_decode((string) ($existing['note'] ?? ''), true);
$existing_meta = is_array($existing_meta) ? $existing_meta : [];
if ((int) ($existing['user_id'] ?? 0) !== $user_id
|| sanitize_key((string) ($existing['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP
|| (int) ($existing['product_id'] ?? 0) !== $package_id
|| (string) ($existing['amount'] ?? '') !== $amount_decimal
|| strtoupper((string) ($existing['currency'] ?? '')) !== 'CNY'
|| sanitize_key((string) ($existing['payment_method'] ?? '')) !== $payment_method
|| (int) ($existing_meta['level'] ?? 0) !== $level
|| (int) ($existing_meta['days'] ?? 0) !== $days
|| (int) ($existing_meta['time'] ?? 0) !== $pkg_time
|| sanitize_key((string) ($existing_meta['unit'] ?? '')) !== $pkg_unit
|| sanitize_key((string) ($existing_meta['action'] ?? '')) !== $action
) {
return new \WP_Error('aksrt_ledger_idempotency_conflict', __('相同操作标识已用于其他会员购买请求', 'aksrt'), ['status' => 409]);
}
$order_number = (string) $existing['order_number'];
} else {
$order_number = $repo->create([
'user_id' => $user_id,
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
'product_id' => $package_id,
'amount' => $amount,
'amount' => $amount_decimal,
'currency' => 'CNY',
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
'payment_method' => $payment_method,
'note' => is_string($note) ? $note : '',
'expires_at' => $expires_at,
]);
'request_key' => $operation_key,
]);
}
if ($order_number === '') {
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
if (is_array($existing) && in_array((string) ($existing['status'] ?? ''), [\AKSRT\Payment\Order::STATUS_PAID, \AKSRT\Payment\Order::STATUS_DELIVERED, \AKSRT\Payment\Order::STATUS_COMPLETED], true)) {
$ledger_key = \AKSRT\Ledger\OperationKey::deterministic('order-payment', [$order_number]);
$history = \AKSRT\Ledger\LedgerQuery::get_instance()->history(['operation_key' => $ledger_key, 'page' => 1, 'per_page' => 1]);
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => $amount_decimal,
'level' => $level,
'days' => $days,
'action' => $action,
'transaction_id' => (int) ($history['items'][0]['id'] ?? 0),
'replayed' => true,
'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
'message' => __('会员购买已完成', 'aksrt'),
'reload' => true,
],
];
}
// Build the request-local order context from the same server-derived
// package used for validation. PaymentOrchestrator still reloads the
// persisted row under its attempt lock before touching the gateway.
@@ -395,7 +437,7 @@ trait VipServicePurchase {
'user_id' => $user_id,
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
'product_id' => $package_id,
'amount' => $amount,
'amount' => $amount_decimal,
'currency' => 'CNY',
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
'payment_method' => $payment_method,
@@ -430,7 +472,7 @@ trait VipServicePurchase {
return new \WP_Error(
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
(string) ($payment_result['message'] ?? __('支付失败', 'aksrt')),
['status' => 402, 'order_number' => $order_number]
['status' => (int) ($payment_result['status'] ?? 400), 'order_number' => $order_number]
);
}
@@ -442,10 +484,12 @@ trait VipServicePurchase {
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
'order_number' => $order_number,
'amount' => $amount,
'amount' => $amount_decimal,
'message' => $payment_result['message'],
'reload' => false,
'poll' => true,
'transaction_id' => 0,
'replayed' => false,
],
];
}
@@ -454,10 +498,13 @@ trait VipServicePurchase {
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => $amount,
'amount' => $amount_decimal,
'level' => $level,
'days' => $days,
'action' => $action,
'transaction_id' => (int) ($payment_result['ledger_transaction_id'] ?? 0),
'replayed' => !empty($payment_result['replayed']),
'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
'message' => $action === 'renew'
? __('会员续费成功', 'aksrt')
: ($action === 'upgrade' ? __('会员升级成功', 'aksrt') : __('会员开通成功', 'aksrt')),
@@ -474,16 +521,16 @@ trait VipServicePurchase {
* @param string $order_number
* @param array $paid_order
*/
public function on_order_paid(string $order_number, array $paid_order): void {
public function fulfill_paid_order(string $order_number, array $paid_order) {
if (sanitize_key((string) ($paid_order['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) {
return;
return true;
}
if (sanitize_key((string) ($paid_order['payment_method'] ?? '')) === 'card_pass') {
return;
return true;
}
$user_id = (int) ($paid_order['user_id'] ?? 0);
if ($user_id <= 0) {
return;
return true;
}
$meta = [];
@@ -497,7 +544,7 @@ trait VipServicePurchase {
$meta = $paid_order['meta_payload'];
}
if (!empty($meta['card_pass']) || sanitize_key((string) ($meta['vip_pay_type'] ?? '')) === 'card_pass') {
return;
return true;
}
$level = (int) ($meta['level'] ?? 0);
@@ -561,13 +608,20 @@ trait VipServicePurchase {
$fulfilled = (int) get_user_meta($user_id, 'aksrt_vip_level', true) >= max($previous_level, $level)
&& ($new_expiry === 0 || $days <= 0 || $new_expiry >= max($previous_expiry, time()) + ($days * DAY_IN_SECONDS));
}
if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) {
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
if (!$fulfilled) {
return new \WP_Error('aksrt_vip_fulfillment_failed', __('会员权益发放失败。', 'aksrt'), ['status' => 500]);
}
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$completed = \AKSRT\Payment\OrderRepository::get_instance()->update_status(
$order_number,
\AKSRT\Payment\Order::STATUS_COMPLETED,
['note' => __('会员权益已发放。', 'aksrt')]
);
if (!$completed) {
return new \WP_Error('aksrt_vip_fulfillment_failed', __('会员订单完成失败。', 'aksrt'), ['status' => 500]);
}
}
return true;
}
@@ -673,16 +727,13 @@ trait VipServicePurchase {
*
* @return array|\WP_Error
*/
public function exchange_with_points(int $user_id, int $package_id) {
public function exchange_with_points(int $user_id, int $package_id, string $operation_key = '') {
if ($user_id <= 0) {
return new \WP_Error('aksrt_need_login', __('请先登录后再兑换。', 'aksrt'), ['status' => 401]);
}
if (!class_exists('\AKSRT\Credits\Credits')) {
return new \WP_Error('aksrt_credits_unavailable', __('积分系统不可用', 'aksrt'), ['status' => 500]);
}
$catalog = $this->get_catalog($user_id);
if (array_key_exists('credits_enabled', $catalog) && empty($catalog['credits_enabled'])) {
return new \WP_Error('aksrt_credits_disabled', __('积分功能未启用', 'aksrt'), ['status' => 400]);
return new \WP_Error('aksrt_points_disabled', __('积分功能未启用', 'aksrt'), ['status' => 400]);
}
$points_ex = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : [];
if (empty($points_ex['enabled'])) {
@@ -719,7 +770,7 @@ trait VipServicePurchase {
$balance = (int) ($catalog['user_points'] ?? 0);
if ($balance < $cost) {
return new \WP_Error('aksrt_credits_insufficient', __('积分不足,暂时无法兑换', 'aksrt'), [
return new \WP_Error('aksrt_points_insufficient', __('积分不足,暂时无法兑换', 'aksrt'), [
'status' => 402,
'balance' => $balance,
'cost' => $cost,
@@ -741,62 +792,91 @@ trait VipServicePurchase {
'action' => 'points_exchange',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
'user_id' => $user_id,
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
'product_id' => $package_id,
'amount' => (float) $cost,
'currency' => 'points',
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
'payment_method' => 'points',
'note' => is_string($note) ? $note : '',
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
]);
if ($order_number === '') {
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$repo = \AKSRT\Payment\OrderRepository::get_instance();
$existing = $operation_key !== '' ? $repo->get_by_request_key($operation_key) : null;
$is_new_order = !is_array($existing);
if (is_array($existing)) {
$existing_meta = json_decode((string) ($existing['note'] ?? ''), true);
$existing_meta = is_array($existing_meta) ? $existing_meta : [];
if ((int) ($existing['user_id'] ?? 0) !== $user_id
|| sanitize_key((string) ($existing['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP
|| (int) ($existing['product_id'] ?? 0) !== $package_id
|| (string) ($existing['currency'] ?? '') !== 'points'
|| (int) ($existing['amount'] ?? 0) !== $cost
|| sanitize_key((string) ($existing['payment_method'] ?? '')) !== 'points'
|| (int) ($existing_meta['level'] ?? 0) !== $level
|| (int) ($existing_meta['days'] ?? 0) !== $days
) {
return new \WP_Error('aksrt_ledger_idempotency_conflict', __('相同操作标识已用于其他会员兑换请求', 'aksrt'), ['status' => 409]);
}
$order_number = (string) $existing['order_number'];
} else {
$order_number = $repo->allocate_order_number();
}
if ($order_number === '') {
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$guard = static function () use ($is_new_order, $order_number, $user_id, $package_id, $cost) {
if ($is_new_order) {
return true;
}
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('order');
$current = $wpdb->get_row($wpdb->prepare("SELECT user_id, product_id, amount, currency, status FROM {$table} WHERE order_number = %s FOR UPDATE", $order_number), ARRAY_A);
if (!is_array($current)
|| (int) ($current['user_id'] ?? 0) !== $user_id
|| (int) ($current['product_id'] ?? 0) !== $package_id
|| (int) ($current['amount'] ?? 0) !== $cost
|| (string) ($current['currency'] ?? '') !== 'points'
|| !in_array((string) ($current['status'] ?? ''), [\AKSRT\Payment\Order::STATUS_CREATED, \AKSRT\Payment\Order::STATUS_PENDING], true)
) {
return new \WP_Error('aksrt_order_confirm_failed', __('订单状态或金额已变化', 'aksrt'), ['status' => 409]);
}
return true;
};
$lock = class_exists('\AKSRT\Payment\PaymentAttemptLock')
? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number)
: false;
if ($lock === false) {
return new \WP_Error('aksrt_pay_locked', __('该订单正在发起支付,请稍后重试。', 'aksrt'), ['status' => 409]);
}
try {
$deducted = \AKSRT\Credits\Credits::deduct(
$user_id,
$cost,
'vip_points_exchange',
['order' => $order_number, 'level' => $level, 'days' => $days],
sprintf(__('积分兑换会员 · %s', 'aksrt'), (string) ($match['title'] ?? ''))
);
if (is_wp_error($deducted) || $deducted === false) {
\AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_CANCELLED, [
'note' => __('积分扣除失败,自动取消', 'aksrt'),
$ledger = \AKSRT\Ledger\LedgerService::get_instance();
$deducted = $ledger->debit(
$user_id,
\AKSRT\Ledger\AssetType::POINTS,
$cost,
$operation_key,
'vip_points_exchange',
['type' => 'order', 'key' => $order_number],
sprintf(__('积分兑换会员 · %s', 'aksrt'), (string) ($match['title'] ?? '')),
$user_id,
['level' => $level, 'days' => $days, 'package_id' => $package_id],
static function () use ($is_new_order, $repo, $order_number, $operation_key, $user_id, $package_id, $cost, $note) {
if ($is_new_order) {
$created = $repo->create([
'order_number' => $order_number,
'user_id' => $user_id,
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
'product_id' => $package_id,
'amount' => $cost,
'currency' => 'points',
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
'payment_method' => 'points',
'note' => is_string($note) ? $note : '',
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
'request_key' => $operation_key,
]);
if ($created !== $order_number) {
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
}
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
'payment_method' => 'points',
'payment_transaction_id' => 'points_' . $order_number,
'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost),
'require_transition' => true,
]);
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt');
return new \WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败', 'aksrt'), ['status' => 400]);
}
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
'payment_method' => 'points',
'payment_transaction_id' => 'points_' . $order_number,
'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost),
'require_transition' => true,
]);
if (!$paid) {
\AKSRT\Credits\Credits::add(
$user_id,
$cost,
'vip_points_exchange_refund',
['order' => $order_number],
__('会员兑换失败退款', 'aksrt')
);
return new \WP_Error('aksrt_order_confirm_failed', __('兑换确认失败,已尝试退回积分', 'aksrt'), ['status' => 500]);
}
} finally {
\AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock);
return $paid ? true : new \WP_Error('aksrt_order_confirm_failed', __('兑换确认失败', 'aksrt'), ['status' => 500]);
},
$guard
);
if (is_wp_error($deducted)) {
return $deducted;
}
return [
@@ -806,7 +886,9 @@ trait VipServicePurchase {
'points' => $cost,
'level' => $level,
'days' => $days,
'balance' => \AKSRT\Credits\Credits::balance($user_id),
'balance' => $ledger->balance($user_id, \AKSRT\Ledger\AssetType::POINTS),
'transaction_id' => $deducted->transaction_id,
'replayed' => $deducted->replayed,
'message' => __('兑换成功', 'aksrt'),
'reload' => true,
],