积分与余额账务系统硬切重构
This commit is contained in:
+372
-199
@@ -45,6 +45,11 @@ class ChargeCard {
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_redeem'],
|
||||
'permission_callback' => [$this, 'check_logged_in_rest'],
|
||||
'args' => [
|
||||
'password' => ['type' => 'string', 'required' => true, 'minLength' => 1, 'maxLength' => 80, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'card' => ['type' => 'string', 'default' => '', 'maxLength' => 60, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'type' => ['type' => 'string', 'default' => '', 'enum' => ['', self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS]],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -90,14 +95,19 @@ class ChargeCard {
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$password = sanitize_text_field((string) ($params['password'] ?? $params['code'] ?? ''));
|
||||
$password = sanitize_text_field((string) ($params['password'] ?? ''));
|
||||
$card = sanitize_text_field((string) ($params['card'] ?? ''));
|
||||
$type = sanitize_key((string) ($params['type'] ?? ''));
|
||||
if ($type !== '' && !in_array($type, [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS], true)) {
|
||||
$type = '';
|
||||
}
|
||||
|
||||
$result = $this->redeem_for_user(get_current_user_id(), $password, $card, $type);
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::from_request($request, 'card-redeem', get_current_user_id());
|
||||
if (is_wp_error($operation_key)) {
|
||||
return $operation_key;
|
||||
}
|
||||
|
||||
$result = $this->redeem_for_user(get_current_user_id(), $password, $card, $type, $operation_key);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
@@ -159,18 +169,17 @@ class ChargeCard {
|
||||
}
|
||||
}
|
||||
|
||||
// Points card-pass: legacy points_pass_exchange_* (stored in aksrt_settings).
|
||||
$credits = ['enabled' => false, 'desc' => '', 'only_password' => true];
|
||||
$all = class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_all')
|
||||
? \AKSRT\Settings\Settings::get_all()
|
||||
: (array) get_option('aksrt_settings', []);
|
||||
if (!empty($all['points_pass_exchange_s'])) {
|
||||
if (!empty($all['asset_points_card_enabled'])) {
|
||||
// Require credits system on when available.
|
||||
$credits_on = !class_exists('\AKSRT\Settings\Settings') || !method_exists('\AKSRT\Settings\Settings', 'credits_enabled') || \AKSRT\Settings\Settings::credits_enabled();
|
||||
$credits = [
|
||||
'enabled' => $credits_on,
|
||||
'desc' => sanitize_textarea_field((string) ($all['points_pass_exchange_desc'] ?? '')),
|
||||
'only_password' => !isset($all['points_pass_exchange_only_password']) || !empty($all['points_pass_exchange_only_password']),
|
||||
'desc' => sanitize_textarea_field((string) ($all['asset_points_card_description'] ?? '')),
|
||||
'only_password' => !isset($all['asset_points_card_password_only']) || !empty($all['asset_points_card_password_only']),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -186,7 +195,7 @@ class ChargeCard {
|
||||
* Create one card.
|
||||
*
|
||||
* Accepted $data keys:
|
||||
* - password|code (required unless auto)
|
||||
* - password (required)
|
||||
* - card (optional)
|
||||
* - type balance|vip|credits
|
||||
* - amount (balance)
|
||||
@@ -203,7 +212,7 @@ class ChargeCard {
|
||||
$type = self::TYPE_BALANCE;
|
||||
}
|
||||
|
||||
$password = sanitize_text_field((string) ($data['password'] ?? $data['code'] ?? ''));
|
||||
$password = sanitize_text_field((string) ($data['password'] ?? ''));
|
||||
if ($password === '') {
|
||||
return 0;
|
||||
}
|
||||
@@ -225,7 +234,7 @@ class ChargeCard {
|
||||
|
||||
$expires_at = sanitize_text_field((string) ($data['expires_at'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($data['note'] ?? ''));
|
||||
$amount_col = (float) ($meta['amount'] ?? $meta['points'] ?? 0);
|
||||
$amount_col = (string) ($meta['amount'] ?? $meta['points'] ?? '0');
|
||||
|
||||
$result = $wpdb->insert($this->table(), [
|
||||
'card' => $card,
|
||||
@@ -276,9 +285,9 @@ class ChargeCard {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{success:bool,message:string,type?:string,amount?:float,level?:int,days?:int,order_number?:string,balance?:int}|\WP_Error
|
||||
* @return array{success:bool,message:string,type?:string,amount?:string|int,level?:int,days?:int,order_number?:string,balance?:string|int}|\WP_Error
|
||||
*/
|
||||
public function redeem_for_user(int $user_id, string $password, string $card = '', string $expected_type = '') {
|
||||
public function redeem_for_user(int $user_id, string $password, string $card = '', string $expected_type = '', string $operation_key = '') {
|
||||
$user_id = max(0, $user_id);
|
||||
$password = sanitize_text_field($password);
|
||||
$card = sanitize_text_field($card);
|
||||
@@ -328,7 +337,7 @@ class ChargeCard {
|
||||
return new \WP_Error('aksrt_card_invalid', __('卡号或卡密错误', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
if (!empty($row['expires_at']) && strtotime((string) $row['expires_at']) < time()) {
|
||||
if ((string) ($row['status'] ?? '') === self::STATUS_ACTIVE && !empty($row['expires_at']) && strtotime((string) $row['expires_at']) < time()) {
|
||||
global $wpdb;
|
||||
$wpdb->update($this->table(), ['status' => self::STATUS_EXPIRED], ['id' => (int) $row['id']], ['%s'], ['%d']);
|
||||
return new \WP_Error('aksrt_card_expired', __('卡密已过期', 'aksrt'), ['status' => 400]);
|
||||
@@ -337,47 +346,16 @@ class ChargeCard {
|
||||
$meta = $this->decode_meta($row);
|
||||
$card_id = (int) $row['id'];
|
||||
|
||||
// Create audit order first (pending), then CAS mark used, then fulfill + mark paid.
|
||||
$order_number = $this->create_redeem_order($user_id, $type, $meta, $card_id, $password);
|
||||
if ($order_number === '') {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
if (!$this->mark_used_cas($card_id, $user_id, $order_number)) {
|
||||
// Race lost.
|
||||
if (class_exists('AKSRT\\Payment\\Order')) {
|
||||
Order::get_instance()->update_status($order_number, Order::STATUS_CANCELLED, [
|
||||
'note' => __('卡密已被使用,订单取消', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
return new \WP_Error('aksrt_card_used', __('该卡密已使用', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
$fulfill = $this->fulfill($user_id, $type, $meta, $order_number, $card_id);
|
||||
if (is_wp_error($fulfill)) {
|
||||
return $fulfill;
|
||||
}
|
||||
|
||||
if (class_exists('AKSRT\\Payment\\Order')) {
|
||||
Order::get_instance()->update_status($order_number, Order::STATUS_PAID, [
|
||||
'payment_method' => 'card_pass',
|
||||
'payment_transaction_id' => 'card_pass_' . $card_id,
|
||||
'note' => (string) ($fulfill['message'] ?? __('卡密兑换成功', 'aksrt')),
|
||||
]);
|
||||
}
|
||||
|
||||
$out = array_merge([
|
||||
'success' => true,
|
||||
'type' => $type,
|
||||
'order_number' => $order_number,
|
||||
'reload' => true,
|
||||
], $fulfill);
|
||||
|
||||
if (class_exists('\AKSRT\Credits\Credits')) {
|
||||
$out['balance'] = \AKSRT\Credits\Credits::balance($user_id);
|
||||
}
|
||||
|
||||
return $out;
|
||||
if (in_array($type, [self::TYPE_BALANCE, self::TYPE_CREDITS], true)) {
|
||||
if ($operation_key === '') {
|
||||
return new \WP_Error('aksrt_idempotency_key', __('缺少操作标识', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
return $this->redeem_asset_card($user_id, $row, $meta, $card, $operation_key);
|
||||
}
|
||||
if ($operation_key === '') {
|
||||
return new \WP_Error('aksrt_idempotency_key', __('缺少操作标识', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
return $this->redeem_vip_card($user_id, $row, $meta, $card, $operation_key);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -401,139 +379,342 @@ class ChargeCard {
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
if ((string) ($row['status'] ?? '') !== self::STATUS_ACTIVE) {
|
||||
return null;
|
||||
}
|
||||
return $row;
|
||||
}
|
||||
|
||||
public function mark_used_cas(int $card_id, int $user_id, string $order_number): bool {
|
||||
global $wpdb;
|
||||
if ($card_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->table()} SET status = %s, used_by = %d, used_at = %s, order_number = %s, balance = 0
|
||||
WHERE id = %d AND status = %s",
|
||||
self::STATUS_USED,
|
||||
$user_id,
|
||||
current_time('mysql'),
|
||||
$order_number,
|
||||
$card_id,
|
||||
self::STATUS_ACTIVE
|
||||
));
|
||||
return $updated === 1;
|
||||
}
|
||||
/**
|
||||
* @param array<string,mixed> $row
|
||||
* @param array<string,mixed> $meta
|
||||
* @return array<string,mixed>|\WP_Error
|
||||
*/
|
||||
private function redeem_asset_card(int $user_id, array $row, array $meta, string $submitted_card, string $operation_key) {
|
||||
$type = sanitize_key((string) ($row['type'] ?? ''));
|
||||
$card_id = (int) ($row['id'] ?? 0);
|
||||
$asset = $type === self::TYPE_BALANCE ? \AKSRT\Ledger\AssetType::BALANCE : \AKSRT\Ledger\AssetType::POINTS;
|
||||
if ($asset === \AKSRT\Ledger\AssetType::BALANCE) {
|
||||
try {
|
||||
$amount_minor = \AKSRT\Ledger\AssetAmount::balance(is_string($meta['amount'] ?? null) ? $meta['amount'] : '')->minor();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new \WP_Error('aksrt_card_amount', __('当前卡密充值金额无效', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$reason = 'card_pass_balance';
|
||||
$remark = sprintf(__('卡密充值 %s 元', 'aksrt'), \AKSRT\Ledger\AssetAmount::from_minor($asset, $amount_minor)->decimal());
|
||||
} else {
|
||||
$amount_minor = (int) ($meta['points'] ?? 0);
|
||||
$reason = 'card_pass_credits';
|
||||
$remark = sprintf(__('卡密兑换积分 %d', 'aksrt'), $amount_minor);
|
||||
}
|
||||
if ($card_id <= 0 || $amount_minor <= 0) {
|
||||
return new \WP_Error('aksrt_card_amount', __('当前卡密可兑换资产为 0', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$order_number = 'CARD' . $card_id;
|
||||
$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);
|
||||
if (!is_array($current)
|
||||
|| (string) ($current['status'] ?? '') !== self::STATUS_ACTIVE
|
||||
|| sanitize_key((string) ($current['type'] ?? '')) !== $type
|
||||
) {
|
||||
return new \WP_Error('aksrt_card_used', __('该卡密已使用或不可用', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
if ((string) ($current['card'] ?? '') !== '' && $submitted_card !== '' && !hash_equals((string) $current['card'], $submitted_card)) {
|
||||
return new \WP_Error('aksrt_card_invalid', __('卡号或卡密错误', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if (!empty($current['expires_at']) && strtotime((string) $current['expires_at']) < time()) {
|
||||
return new \WP_Error('aksrt_card_expired', __('卡密已过期', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if ((int) ($current['used_by'] ?? 0) > 0 && (int) $current['used_by'] !== $user_id) {
|
||||
return new \WP_Error('aksrt_card_used', __('该卡密已使用', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
$mutation = function () use ($user_id, $type, $meta, $card_id, $order_number, $operation_key) {
|
||||
global $wpdb;
|
||||
$created = $this->create_redeem_order($user_id, $type, $meta, $card_id, '', $operation_key, $order_number, Order::STATUS_PAID);
|
||||
if ($created !== $order_number) {
|
||||
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$used = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->table()} SET status = %s, used_by = %d, used_at = %s, order_number = %s, balance = 0 WHERE id = %d AND status = %s",
|
||||
self::STATUS_USED,
|
||||
$user_id,
|
||||
current_time('mysql'),
|
||||
$order_number,
|
||||
$card_id,
|
||||
self::STATUS_ACTIVE
|
||||
));
|
||||
return $used === 1 ? true : new \WP_Error('aksrt_card_used', __('该卡密已使用', 'aksrt'), ['status' => 409]);
|
||||
};
|
||||
$result = \AKSRT\Ledger\LedgerService::get_instance()->credit(
|
||||
$user_id,
|
||||
$asset,
|
||||
$amount_minor,
|
||||
$operation_key,
|
||||
$reason,
|
||||
['type' => 'charge_card', 'id' => $card_id],
|
||||
$remark,
|
||||
$user_id,
|
||||
['card_type' => $type],
|
||||
$mutation,
|
||||
$guard
|
||||
);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
$balance = $result->balance($user_id, $asset);
|
||||
return [
|
||||
'success' => true,
|
||||
'type' => $type,
|
||||
'order_number' => $order_number,
|
||||
'transaction_id' => $result->transaction_id,
|
||||
'replayed' => $result->replayed,
|
||||
'message' => $asset === \AKSRT\Ledger\AssetType::BALANCE
|
||||
? sprintf(__('卡密充值成功,充值金额:%s 元', 'aksrt'), \AKSRT\Ledger\AssetAmount::from_minor($asset, $amount_minor)->decimal())
|
||||
: sprintf(__('卡密兑换成功,积分:%d', 'aksrt'), $amount_minor),
|
||||
'amount' => $asset === \AKSRT\Ledger\AssetType::BALANCE
|
||||
? \AKSRT\Ledger\AssetAmount::from_minor($asset, $amount_minor)->decimal()
|
||||
: $amount_minor,
|
||||
'balance' => $asset === \AKSRT\Ledger\AssetType::BALANCE
|
||||
? \AKSRT\Ledger\AssetAmount::from_minor($asset, $balance)->decimal()
|
||||
: $balance,
|
||||
'reload' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $meta
|
||||
* @return array{message:string,amount?:float,level?:int,days?:int}|\WP_Error
|
||||
*/
|
||||
private function fulfill(int $user_id, string $type, array $meta, string $order_number, int $card_id) {
|
||||
if ($type === self::TYPE_BALANCE) {
|
||||
$amount = round((float) ($meta['amount'] ?? 0), 2);
|
||||
if ($amount <= 0) {
|
||||
return new \WP_Error('aksrt_card_amount', __('当前卡密可充值金额为0', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
// Cash wallet first (legacy pay_balance); fall back to points if wallet class missing.
|
||||
if (class_exists('\AKSRT\Credits\Balance')) {
|
||||
$new = \AKSRT\Credits\Balance::add($user_id, $amount, 'card_pass_balance', [
|
||||
'type' => 'charge_card',
|
||||
'id' => $card_id,
|
||||
'order' => $order_number,
|
||||
], sprintf(__('卡密充值 %s 元', 'aksrt'), (string) $amount));
|
||||
if (is_wp_error($new)) {
|
||||
return new \WP_Error('aksrt_balance_fail', $new->get_error_message() ?: __('余额入账失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
return [
|
||||
'message' => sprintf(__('卡密充值成功,充值金额:%s 元', 'aksrt'), (string) $amount),
|
||||
'amount' => (float) $amount,
|
||||
];
|
||||
}
|
||||
if (!class_exists('\AKSRT\Credits\Credits')) {
|
||||
return new \WP_Error('aksrt_card_amount', __('余额系统不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$pts = (int) round($amount);
|
||||
$new = \AKSRT\Credits\Credits::add($user_id, $pts, 'card_pass_balance', [
|
||||
'ref_type' => 'charge_card',
|
||||
'ref_id' => $card_id,
|
||||
'order' => $order_number,
|
||||
], sprintf(__('卡密充值 %d', 'aksrt'), $pts));
|
||||
if ($new === null || $new === false || is_wp_error($new)) {
|
||||
return new \WP_Error('aksrt_credit_fail', __('余额入账失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
return [
|
||||
'message' => sprintf(__('卡密充值成功,充值金额:%s', 'aksrt'), (string) $pts),
|
||||
'amount' => (float) $pts,
|
||||
];
|
||||
}
|
||||
/**
|
||||
* Redeem a VIP card with the card row, order row, and membership metadata in one transaction.
|
||||
*
|
||||
* @param array<string,mixed> $row
|
||||
* @param array<string,mixed> $meta
|
||||
* @return array<string,mixed>|\WP_Error
|
||||
*/
|
||||
private function redeem_vip_card(int $user_id, array $row, array $meta, string $submitted_card, string $operation_key) {
|
||||
global $wpdb;
|
||||
if (!class_exists('\AKSRT\Content\VipService') || !class_exists(OrderRepository::class)) {
|
||||
return new \WP_Error('aksrt_vip_unavailable', __('会员系统不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$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]);
|
||||
}
|
||||
|
||||
if ($type === self::TYPE_VIP) {
|
||||
$level = max(1, min(2, (int) ($meta['level'] ?? 1)));
|
||||
$days = max(0, (int) ($meta['days'] ?? 0));
|
||||
if (!class_exists('\AKSRT\Content\VipService')) {
|
||||
return new \WP_Error('aksrt_vip_unavailable', __('会员系统不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
\AKSRT\Content\VipService::get_instance()->activate_vip($user_id, $level, $days);
|
||||
$msg = $days > 0
|
||||
? sprintf(
|
||||
/* translators: 1: level 2: days */
|
||||
__('卡密兑换成功:VIP%s %d 天', 'aksrt'),
|
||||
(string) $level,
|
||||
$days
|
||||
)
|
||||
: sprintf(
|
||||
/* translators: %s: level */
|
||||
__('卡密兑换成功:VIP%s 永久', 'aksrt'),
|
||||
(string) $level
|
||||
);
|
||||
return [
|
||||
'message' => $msg,
|
||||
'level' => $level,
|
||||
'days' => $days,
|
||||
];
|
||||
}
|
||||
for ($attempt = 1; $attempt <= 3; $attempt++) {
|
||||
if ($wpdb->query('START TRANSACTION') === false) {
|
||||
return new \WP_Error('aksrt_card_db', __('无法开启卡密兑换事务', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
try {
|
||||
$current = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE id = %d FOR UPDATE",
|
||||
$card_id
|
||||
), ARRAY_A);
|
||||
$existing = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM {$order_table} WHERE request_key = %s FOR UPDATE",
|
||||
$operation_key
|
||||
), ARRAY_A);
|
||||
if (is_array($existing)) {
|
||||
$replay = $this->vip_card_replay_result($existing, $current, $user_id, $card_id, $level, $days, $operation_key);
|
||||
if (is_wp_error($replay)) {
|
||||
throw new \AKSRT\Ledger\LedgerAbort($replay);
|
||||
}
|
||||
if ($wpdb->query('COMMIT') === false) {
|
||||
throw new \RuntimeException('vip_card_replay_commit_failed');
|
||||
}
|
||||
return $replay;
|
||||
}
|
||||
|
||||
if ($type === self::TYPE_CREDITS) {
|
||||
$points = (int) ($meta['points'] ?? 0);
|
||||
if ($points <= 0 || !class_exists('\AKSRT\Credits\Credits')) {
|
||||
return new \WP_Error('aksrt_card_points', __('当前卡密可兑换积分为0', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
\AKSRT\Credits\Credits::add($user_id, $points, 'card_pass_credits', [
|
||||
'ref_type' => 'charge_card',
|
||||
'ref_id' => $card_id,
|
||||
'order' => $order_number,
|
||||
], sprintf(__('卡密兑换积分 %d', 'aksrt'), $points));
|
||||
return [
|
||||
'message' => sprintf(__('卡密兑换成功,积分:%d', 'aksrt'), $points),
|
||||
'amount' => (float) $points,
|
||||
];
|
||||
}
|
||||
if (!is_array($current)
|
||||
|| (string) ($current['status'] ?? '') !== self::STATUS_ACTIVE
|
||||
|| sanitize_key((string) ($current['type'] ?? '')) !== self::TYPE_VIP
|
||||
|| !hash_equals((string) ($row['password'] ?? ''), (string) ($current['password'] ?? ''))
|
||||
) {
|
||||
throw new \AKSRT\Ledger\LedgerAbort(new \WP_Error('aksrt_card_used', __('该卡密已使用或不可用', 'aksrt'), ['status' => 409]));
|
||||
}
|
||||
if ((string) ($current['card'] ?? '') !== '' && $submitted_card !== '' && !hash_equals((string) $current['card'], $submitted_card)) {
|
||||
throw new \AKSRT\Ledger\LedgerAbort(new \WP_Error('aksrt_card_invalid', __('卡号或卡密错误', 'aksrt'), ['status' => 400]));
|
||||
}
|
||||
if (!empty($current['expires_at']) && strtotime((string) $current['expires_at']) < time()) {
|
||||
throw new \AKSRT\Ledger\LedgerAbort(new \WP_Error('aksrt_card_expired', __('卡密已过期', 'aksrt'), ['status' => 400]));
|
||||
}
|
||||
|
||||
return new \WP_Error('aksrt_card_type', __('不支持的卡密类型', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$vip_meta_keys = ['aksrt_vip_level', 'aksrt_vip_expires', 'aksrt_vip_base_level', 'aksrt_vip_base_expires', 'aksrt_vip_level_expired'];
|
||||
$placeholders = implode(',', array_fill(0, count($vip_meta_keys), '%s'));
|
||||
$wpdb->get_results($wpdb->prepare(
|
||||
"SELECT umeta_id FROM {$wpdb->usermeta} WHERE user_id = %d AND meta_key IN ({$placeholders}) FOR UPDATE",
|
||||
array_merge([$user_id], $vip_meta_keys)
|
||||
));
|
||||
|
||||
$current_level = max(0, (int) get_user_meta($user_id, 'aksrt_vip_level', true));
|
||||
$current_expiry = max(0, (int) get_user_meta($user_id, 'aksrt_vip_expires', true));
|
||||
$activation_base = max($current_expiry, time());
|
||||
$created = $this->create_redeem_order(
|
||||
$user_id,
|
||||
self::TYPE_VIP,
|
||||
['level' => $level, 'days' => $days],
|
||||
$card_id,
|
||||
(string) ($current['password'] ?? ''),
|
||||
$operation_key,
|
||||
$order_number,
|
||||
Order::STATUS_PAID
|
||||
);
|
||||
if ($created !== $order_number) {
|
||||
throw new \RuntimeException('vip_card_order_create_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));
|
||||
$permanent_preserved = $days > 0 && $current_level > 0 && $level <= $current_level && $current_expiry === 0;
|
||||
$expiry_valid = $days <= 0
|
||||
? $actual_expiry === 0
|
||||
: ($permanent_preserved ? $actual_expiry === 0 : $actual_expiry >= $activation_base + ($days * DAY_IN_SECONDS));
|
||||
if ($actual_level < max($current_level, $level) || !$expiry_valid) {
|
||||
throw new \RuntimeException('vip_card_fulfillment_failed');
|
||||
}
|
||||
|
||||
$used = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->table()} SET status = %s, used_by = %d, used_at = %s, order_number = %s, balance = 0 WHERE id = %d AND status = %s",
|
||||
self::STATUS_USED,
|
||||
$user_id,
|
||||
current_time('mysql'),
|
||||
$order_number,
|
||||
$card_id,
|
||||
self::STATUS_ACTIVE
|
||||
));
|
||||
if ($used !== 1) {
|
||||
throw new \AKSRT\Ledger\LedgerAbort(new \WP_Error('aksrt_card_used', __('该卡密已使用', 'aksrt'), ['status' => 409]));
|
||||
}
|
||||
if ($wpdb->query('COMMIT') === false) {
|
||||
throw new \RuntimeException('vip_card_commit_failed');
|
||||
}
|
||||
|
||||
$message = $this->vip_card_message($level, $days);
|
||||
$paid_order = OrderRepository::get_instance()->get($order_number);
|
||||
if (is_array($paid_order)) {
|
||||
try {
|
||||
do_action('aksrt_order_status_changed', $order_number, Order::STATUS_PAID, $paid_order);
|
||||
do_action('aksrt_order_paid', $order_number, $paid_order);
|
||||
} catch (\Throwable $event_error) {
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('[AKSRT VIP Card Event] ' . $event_error->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'type' => self::TYPE_VIP,
|
||||
'order_number' => $order_number,
|
||||
'transaction_id' => 0,
|
||||
'replayed' => false,
|
||||
'message' => $message,
|
||||
'level' => $level,
|
||||
'days' => $days,
|
||||
'reload' => true,
|
||||
];
|
||||
} catch (\AKSRT\Ledger\LedgerAbort $e) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
clean_user_cache($user_id);
|
||||
return $e->error;
|
||||
} catch (\Throwable $e) {
|
||||
$errno = $this->last_db_errno();
|
||||
$wpdb->query('ROLLBACK');
|
||||
clean_user_cache($user_id);
|
||||
$replay_order = OrderRepository::get_instance()->get_by_request_key($operation_key);
|
||||
if (is_array($replay_order)) {
|
||||
$replay_card = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table()} WHERE id = %d", $card_id), ARRAY_A);
|
||||
return $this->vip_card_replay_result($replay_order, $replay_card, $user_id, $card_id, $level, $days, $operation_key);
|
||||
}
|
||||
if (in_array($errno, [1205, 1213], true) && $attempt < 3) {
|
||||
usleep(random_int(10000, 50000));
|
||||
continue;
|
||||
}
|
||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log('[AKSRT VIP Card] ' . $e->getMessage());
|
||||
}
|
||||
return new \WP_Error('aksrt_card_db', __('卡密兑换失败,请稍后重试', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
}
|
||||
return new \WP_Error('aksrt_card_db', __('卡密兑换失败,请稍后重试', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|\WP_Error */
|
||||
private function vip_card_replay_result($order, $card, int $user_id, int $card_id, int $level, int $days, string $operation_key) {
|
||||
$order = is_array($order) ? $order : [];
|
||||
$card = is_array($card) ? $card : [];
|
||||
$note = json_decode((string) ($order['note'] ?? ''), true);
|
||||
$note = is_array($note) ? $note : [];
|
||||
$stored_meta = is_array($note['meta'] ?? null) ? $note['meta'] : [];
|
||||
$same = (string) ($order['request_key'] ?? '') === $operation_key
|
||||
&& (int) ($order['user_id'] ?? 0) === $user_id
|
||||
&& sanitize_key((string) ($order['product_type'] ?? '')) === \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP
|
||||
&& (int) ($order['product_id'] ?? 0) === $card_id
|
||||
&& sanitize_key((string) ($order['payment_method'] ?? '')) === 'card_pass'
|
||||
&& in_array((string) ($order['status'] ?? ''), [Order::STATUS_PAID, Order::STATUS_COMPLETED], true)
|
||||
&& (int) ($note['card_id'] ?? 0) === $card_id
|
||||
&& sanitize_key((string) ($note['card_type'] ?? '')) === self::TYPE_VIP
|
||||
&& (int) ($stored_meta['level'] ?? 0) === $level
|
||||
&& (int) ($stored_meta['days'] ?? -1) === $days
|
||||
&& (string) ($card['status'] ?? '') === self::STATUS_USED
|
||||
&& (int) ($card['used_by'] ?? 0) === $user_id
|
||||
&& (string) ($card['order_number'] ?? '') === (string) ($order['order_number'] ?? '');
|
||||
if (!$same) {
|
||||
return new \WP_Error('aksrt_ledger_idempotency_conflict', __('相同操作标识已用于其他卡密兑换请求', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'type' => self::TYPE_VIP,
|
||||
'order_number' => (string) $order['order_number'],
|
||||
'transaction_id' => 0,
|
||||
'replayed' => true,
|
||||
'message' => $this->vip_card_message($level, $days),
|
||||
'level' => $level,
|
||||
'days' => $days,
|
||||
'reload' => true,
|
||||
];
|
||||
}
|
||||
|
||||
private function vip_card_message(int $level, int $days): string {
|
||||
return $days > 0
|
||||
? sprintf(__('卡密兑换成功:VIP%s %d 天', 'aksrt'), (string) $level, $days)
|
||||
: sprintf(__('卡密兑换成功:VIP%s 永久', 'aksrt'), (string) $level);
|
||||
}
|
||||
|
||||
private function last_db_errno(): int {
|
||||
global $wpdb;
|
||||
if (isset($wpdb->dbh) && $wpdb->dbh instanceof \mysqli) {
|
||||
return mysqli_errno($wpdb->dbh);
|
||||
}
|
||||
if (isset($wpdb->last_errno)) {
|
||||
return (int) $wpdb->last_errno;
|
||||
}
|
||||
return preg_match('/\\b(1205|1213)\\b/', (string) ($wpdb->last_error ?? ''), $matches) ? (int) $matches[1] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $meta
|
||||
*/
|
||||
private function create_redeem_order(int $user_id, string $type, array $meta, int $card_id, string $password): 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 {
|
||||
if (!class_exists('AKSRT\\Payment\\OrderRepository') || !class_exists('\AKSRT\Core\DomainModel')) {
|
||||
return 'CARD' . $card_id . '_' . time();
|
||||
return '';
|
||||
}
|
||||
|
||||
$product_type = \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE;
|
||||
$product_id = $card_id;
|
||||
$amount = 0.0;
|
||||
$amount = '0.00';
|
||||
$currency = 'CNY';
|
||||
|
||||
if ($type === self::TYPE_VIP) {
|
||||
$product_type = \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP;
|
||||
$amount = 0.0;
|
||||
$amount = '0.00';
|
||||
} elseif ($type === self::TYPE_BALANCE) {
|
||||
$amount = round((float) ($meta['amount'] ?? 0), 2);
|
||||
$parsed_amount = \AKSRT\Ledger\AssetAmount::balance(is_string($meta['amount'] ?? null) ? $meta['amount'] : '');
|
||||
$amount = $parsed_amount->decimal();
|
||||
$currency = 'CNY';
|
||||
} elseif ($type === self::TYPE_CREDITS) {
|
||||
$amount = (float) ($meta['points'] ?? 0);
|
||||
$amount = (int) ($meta['points'] ?? 0);
|
||||
$currency = 'points';
|
||||
}
|
||||
|
||||
@@ -545,17 +726,25 @@ class ChargeCard {
|
||||
'password' => substr(md5($password), 0, 8),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return OrderRepository::get_instance()->create([
|
||||
$order_status = $status !== '' ? $status : Order::STATUS_PENDING;
|
||||
$order_data = [
|
||||
'order_number' => $order_number !== '' ? $order_number : null,
|
||||
'user_id' => $user_id,
|
||||
'product_type' => $product_type,
|
||||
'product_id' => $product_id,
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'status' => class_exists('AKSRT\\Payment\\Order') ? Order::STATUS_PENDING : 'pending',
|
||||
'status' => $order_status,
|
||||
'payment_method' => 'card_pass',
|
||||
'payment_transaction_id' => $order_status === Order::STATUS_PAID ? 'card_pass_' . $card_id : '',
|
||||
'paid_at' => $order_status === Order::STATUS_PAID ? current_time('mysql') : null,
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => \AKSRT\Settings\PaymentRuntime::get_order_expiry_at(),
|
||||
]);
|
||||
'request_key' => $request_key,
|
||||
];
|
||||
if ($order_status === Order::STATUS_PENDING) {
|
||||
$order_data['expires_at'] = \AKSRT\Settings\PaymentRuntime::get_order_expiry_at();
|
||||
}
|
||||
return OrderRepository::get_instance()->create($order_data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -587,19 +776,27 @@ class ChargeCard {
|
||||
}
|
||||
|
||||
if ($type === self::TYPE_BALANCE) {
|
||||
$amount = round((float) ($raw['amount'] ?? $data['amount'] ?? 0), 2);
|
||||
if ($amount <= 0) {
|
||||
$value = $raw['amount'] ?? $data['amount'] ?? '';
|
||||
if (!is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$amount = \AKSRT\Ledger\AssetAmount::balance($value);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return null;
|
||||
}
|
||||
if ($amount->minor() <= 0) {
|
||||
return null;
|
||||
}
|
||||
return ['amount' => $amount];
|
||||
return ['amount' => $amount->decimal()];
|
||||
}
|
||||
if ($type === self::TYPE_VIP) {
|
||||
$level = max(1, min(2, (int) ($raw['level'] ?? $data['level'] ?? 1)));
|
||||
$days = max(0, (int) ($raw['days'] ?? $data['days'] ?? $data['vip_days'] ?? 0));
|
||||
$days = max(0, (int) ($raw['days'] ?? $data['days'] ?? 0));
|
||||
return ['level' => $level, 'days' => $days];
|
||||
}
|
||||
if ($type === self::TYPE_CREDITS) {
|
||||
$points = max(0, (int) ($raw['points'] ?? $data['points'] ?? $data['credits'] ?? 0));
|
||||
$points = max(0, (int) ($raw['points'] ?? $data['points'] ?? 0));
|
||||
if ($points <= 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -624,15 +821,11 @@ class ChargeCard {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Legacy amount-only rows.
|
||||
if ($meta === [] && isset($row['amount'])) {
|
||||
$meta = ['amount' => (float) $row['amount']];
|
||||
}
|
||||
return $meta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin list helper (supports new + legacy columns).
|
||||
* Admin list helper.
|
||||
*
|
||||
* @return array{items:array,total:int,page:int,max_pages:int}
|
||||
*/
|
||||
@@ -652,9 +845,6 @@ class ChargeCard {
|
||||
$params[] = $status;
|
||||
}
|
||||
if ($type !== '') {
|
||||
if ($type === 'points') {
|
||||
$type = self::TYPE_CREDITS;
|
||||
}
|
||||
$where[] = 'type = %s';
|
||||
$params[] = $type;
|
||||
}
|
||||
@@ -715,25 +905,8 @@ class ChargeCard {
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'active' => (int) ($row['active'] ?? 0),
|
||||
'used' => (int) ($row['used'] ?? 0),
|
||||
'total_amount' => (float) ($row['total_amount'] ?? 0),
|
||||
'used_amount' => (float) ($row['used_amount'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy alias used by older admin code.
|
||||
*
|
||||
* @return array{success:bool,message:string,amount?:float}
|
||||
*/
|
||||
public function redeem(string $code, int $user_id): array {
|
||||
$result = $this->redeem_for_user($user_id, $code, '', self::TYPE_BALANCE);
|
||||
if (is_wp_error($result)) {
|
||||
return ['success' => false, 'message' => $result->get_error_message()];
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => (string) ($result['message'] ?? __('兑换成功', 'aksrt')),
|
||||
'amount' => (float) ($result['amount'] ?? 0),
|
||||
'total_amount' => (string) ($row['total_amount'] ?? '0.00'),
|
||||
'used_amount' => (string) ($row['used_amount'] ?? '0.00'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user