feat(users): normalize center routes and withdrawal limits
This commit is contained in:
+112
-2
@@ -30,7 +30,7 @@ class Withdrawal {
|
||||
/**
|
||||
* 创建提现申请
|
||||
*/
|
||||
public function create(int $user_id, float $amount, string $method = '', string $account_info = ''): int {
|
||||
public function create(int $user_id, float $amount, string $method = '', string $account_info = '', ?float $gross_amount = null): int {
|
||||
global $wpdb;
|
||||
|
||||
if ($user_id <= 0 || $amount <= 0) {
|
||||
@@ -40,15 +40,122 @@ class Withdrawal {
|
||||
$wpdb->insert($this->table(), [
|
||||
'user_id' => $user_id,
|
||||
'amount' => $amount,
|
||||
'gross_amount' => $gross_amount === null ? $amount : $gross_amount,
|
||||
'method' => sanitize_key($method),
|
||||
'account_info' => sanitize_textarea_field($account_info),
|
||||
'status' => self::STATUS_PENDING,
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%d', '%f', '%s', '%s', '%s', '%s']);
|
||||
], ['%d', '%f', '%f', '%s', '%s', '%s', '%s']);
|
||||
|
||||
return (int) $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Daily withdrawal window in the configured WordPress timezone.
|
||||
* @return array{start:string,end:string}
|
||||
*/
|
||||
public function daily_window(string $reset_time = '00:00', ?\DateTimeInterface $now = null): array {
|
||||
if (preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $reset_time) !== 1) {
|
||||
$reset_time = '00:00';
|
||||
}
|
||||
$tz = wp_timezone();
|
||||
$local_now = $now ? \DateTimeImmutable::createFromInterface($now)->setTimezone($tz) : new \DateTimeImmutable('now', $tz);
|
||||
$today_reset = new \DateTimeImmutable($local_now->format('Y-m-d') . ' ' . $reset_time . ':00', $tz);
|
||||
$start = $local_now < $today_reset ? $today_reset->modify('-1 day') : $today_reset;
|
||||
return ['start' => $start->format('Y-m-d H:i:s'), 'end' => $start->modify('+1 day')->format('Y-m-d H:i:s')];
|
||||
}
|
||||
|
||||
/** @return array{count:int,total:float,remaining_count:int,remaining_amount:float,window:array{start:string,end:string}} */
|
||||
public function daily_usage(int $user_id, array $settings): array {
|
||||
$window = $this->daily_window((string) ($settings['reset_time'] ?? '00:00'));
|
||||
return $this->daily_usage_in_window($user_id, $settings, $window);
|
||||
}
|
||||
|
||||
/** @return array{count:int,total:float,remaining_count:int,remaining_amount:float,window:array{start:string,end:string}} */
|
||||
private function daily_usage_in_window(int $user_id, array $settings, array $window): array {
|
||||
global $wpdb;
|
||||
$row = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT COUNT(*) AS count, COALESCE(SUM(CASE WHEN gross_amount > 0 THEN gross_amount ELSE amount END), 0) AS total FROM {$this->table()} WHERE user_id = %d AND created_at >= %s AND created_at < %s",
|
||||
$user_id, $window['start'], $window['end']
|
||||
), ARRAY_A);
|
||||
$count = (int) ($row['count'] ?? 0);
|
||||
$total = round((float) ($row['total'] ?? 0), 2);
|
||||
$max_count = max(0, (int) ($settings['daily_count'] ?? 3));
|
||||
$max_amount = max(0, (float) ($settings['daily_limit'] ?? 50));
|
||||
return [
|
||||
'count' => $count,
|
||||
'total' => $total,
|
||||
'remaining_count' => max(0, $max_count - $count),
|
||||
'remaining_amount' => max(0, round($max_amount - $total, 2)),
|
||||
'window' => $window,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically validates quotas, freezes wallet funds, writes the ledger and creates an order.
|
||||
* @return array{withdrawal_id:int,net_amount:float,wallet_balance:float,usage:array}|\WP_Error
|
||||
*/
|
||||
public function create_balance_request(int $user_id, int $gross_amount, string $method, string $account, array $settings) {
|
||||
global $wpdb;
|
||||
if ($user_id <= 0 || $gross_amount <= 0) {
|
||||
return new \WP_Error('aksrt_withdraw_amount', __('提现金额必须为正整数', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if (!class_exists('\AKSRT\\Credits\\Balance')) {
|
||||
return new \WP_Error('aksrt_withdraw_unavailable', __('提现服务不可用', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
$meta_table = $wpdb->usermeta;
|
||||
$meta_key = \AKSRT\Credits\Balance::META_KEY;
|
||||
if (!metadata_exists('user', $user_id, $meta_key)) {
|
||||
add_user_meta($user_id, $meta_key, '0', true);
|
||||
}
|
||||
$wpdb->query('START TRANSACTION');
|
||||
try {
|
||||
$meta_id = $wpdb->get_var($wpdb->prepare("SELECT umeta_id FROM {$meta_table} WHERE user_id = %d AND meta_key = %s LIMIT 1 FOR UPDATE", $user_id, $meta_key));
|
||||
if (!$meta_id) {
|
||||
throw new \RuntimeException('wallet row unavailable');
|
||||
}
|
||||
$window = $this->daily_window((string) ($settings['reset_time'] ?? '00:00'));
|
||||
$usage = $this->daily_usage_in_window($user_id, $settings, $window);
|
||||
if ($usage['remaining_count'] <= 0) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
return new \WP_Error('withdraw_count_limit', __('当日提现次数已用尽', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
if ($gross_amount > $usage['remaining_amount']) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
return new \WP_Error('withdraw_amount_limit', __('本次提现后将超出单日提现总额上限', 'aksrt'), ['status' => 409]);
|
||||
}
|
||||
$before = (float) $wpdb->get_var($wpdb->prepare("SELECT meta_value FROM {$meta_table} WHERE umeta_id = %d", $meta_id));
|
||||
if ($before < $gross_amount) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
return new \WP_Error('aksrt_balance_insufficient', __('余额不足', 'aksrt'), ['status' => 402]);
|
||||
}
|
||||
$after = round($before - $gross_amount, 2);
|
||||
if ($wpdb->update($meta_table, ['meta_value' => (string) $after], ['umeta_id' => $meta_id], ['%s'], ['%d']) !== 1) {
|
||||
throw new \RuntimeException('wallet update failed');
|
||||
}
|
||||
$fee_rate = max(0, min(50, (int) ($settings['service_charge_rate'] ?? 5)));
|
||||
$net = round($gross_amount * (100 - $fee_rate) / 100, 2);
|
||||
$payload = wp_json_encode(['account' => $account, 'gross_amount' => $gross_amount, 'fee_rate' => $fee_rate, 'source' => 'balance'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: $account;
|
||||
$ok = $wpdb->insert($this->table(), [
|
||||
'user_id' => $user_id, 'amount' => $net, 'gross_amount' => $gross_amount, 'method' => sanitize_key($method),
|
||||
'account_info' => $payload, 'status' => self::STATUS_PENDING, 'created_at' => current_time('mysql'),
|
||||
], ['%d', '%f', '%f', '%s', '%s', '%s', '%s']);
|
||||
if (!$ok) { throw new \RuntimeException('withdrawal insert failed'); }
|
||||
$withdrawal_id = (int) $wpdb->insert_id;
|
||||
\AKSRT\Credits\Balance::log($user_id, -$gross_amount, $after, 'withdraw_hold', ['type' => 'withdraw', 'id' => $withdrawal_id], __('余额提现申请冻结', 'aksrt'));
|
||||
$wpdb->query('COMMIT');
|
||||
wp_cache_delete($user_id, 'user_meta');
|
||||
$usage['count']++;
|
||||
$usage['total'] = round($usage['total'] + $gross_amount, 2);
|
||||
$usage['remaining_count']--;
|
||||
$usage['remaining_amount'] = max(0, round($usage['remaining_amount'] - $gross_amount, 2));
|
||||
return ['withdrawal_id' => $withdrawal_id, 'net_amount' => $net, 'wallet_balance' => $after, 'usage' => $usage];
|
||||
} catch (\Throwable $e) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
return new \WP_Error('aksrt_withdraw_failed', __('提现申请失败', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single row.
|
||||
*
|
||||
@@ -394,6 +501,9 @@ class Withdrawal {
|
||||
* @param array<string,mixed> $meta
|
||||
*/
|
||||
private function resolve_gross_amount(array $row, array $meta): float {
|
||||
if ((float) ($row['gross_amount'] ?? 0) > 0) {
|
||||
return max(0, round((float) $row['gross_amount'], 2));
|
||||
}
|
||||
if (isset($meta['gross_amount'])) {
|
||||
return max(0, round((float) $meta['gross_amount'], 2));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user