fix(pay): refund wallet on withdraw reject and show pending history

Close the freeze/refund loop when admin rejects a balance withdrawal,
block concurrent open applications, and surface processing state plus
server-rendered withdraw history on the wallet card.
This commit is contained in:
2026-07-16 16:28:56 +08:00
parent d57fc6f934
commit 4da003663d
6 changed files with 514 additions and 136 deletions
+38
View File
@@ -928,3 +928,41 @@ body.uc-modal-open {
}
.uc-card-pass__tip.is-error { color: #b91c1c; }
/* Withdraw history + pending foot */
.uc-asset-card__foot {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--muted-border-color, rgba(0,0,0,.08));
}
.uc-asset-card__foot button {
background: none;
border: 0;
padding: 0;
cursor: pointer;
}
.uc-asset-card__head-action {
background: none;
border: 0;
cursor: pointer;
}
.uc-withdraw-history__item:last-child {
border-bottom: 0 !important;
}
/* Points tasks tab */
.uc-points-tasks .border-bottom,
.uc-points-date .border-bottom,
.uc-withdraw-history__item.border-bottom {
border-bottom: 1px solid var(--muted-border-color, rgba(0,0,0,.08));
}
.uc-points-tasks button,
.uc-points-date button {
background: none;
border: 0;
cursor: pointer;
padding: 0;
}
.focus-color { color: var(--focus-color, #f59e0b); }
+24 -2
View File
@@ -908,10 +908,17 @@
link.addEventListener('click', function (e) {
e.preventDefault();
var key = link.getAttribute('data-aksrt-asset-tab');
document.querySelectorAll('[data-aksrt-asset-tab]').forEach(function (a) {
document.querySelectorAll('.uc-asset-tabs__nav [data-aksrt-asset-tab]').forEach(function (a) {
var li = a.closest('li');
if (li) li.classList.toggle('active', a === link);
if (!li) return;
var aKey = a.getAttribute('data-aksrt-asset-tab');
var on = aKey === key || (key === 'points_date' && aKey === 'points_free');
li.classList.toggle('active', on);
});
var tabs = document.querySelector('.uc-asset-tabs');
if (tabs && typeof tabs.scrollIntoView === 'function') {
tabs.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
document.querySelectorAll('[data-aksrt-asset-pane]').forEach(function (pane) {
var on = pane.getAttribute('data-aksrt-asset-pane') === key;
pane.classList.toggle('is-active', on);
@@ -921,6 +928,20 @@
});
}
function initWithdrawHistory() {
var modal = document.querySelector('[data-aksrt-withdraw-history-modal]');
if (!modal) return;
function open() { modal.hidden = false; document.body.classList.add('uc-modal-open'); }
function close() { modal.hidden = true; document.body.classList.remove('uc-modal-open'); }
document.querySelectorAll('[data-aksrt-withdraw-history-open]').forEach(function (btn) {
btn.addEventListener('click', function (e) { e.preventDefault(); open(); });
});
modal.querySelectorAll('[data-aksrt-withdraw-history-close]').forEach(function (btn) {
btn.addEventListener('click', function (e) { e.preventDefault(); close(); });
});
}
function bootAssets() {
initSignin();
initInvite();
@@ -932,6 +953,7 @@
initExchange();
initPointsBuy();
initWithdraw();
initWithdrawHistory();
initAssetTabs();
}
@@ -107,13 +107,27 @@ class AKSRT_Admin_Pay_Withdrawals {
$user_id = (int) ($row['user_id'] ?? 0);
$user = $user_id > 0 ? get_userdata($user_id) : null;
$st = (string) ($row['status'] ?? '');
$gross = (float) ($row['gross_amount'] ?? $row['amount'] ?? 0);
$net = (float) ($row['amount'] ?? 0);
$fee = (int) ($row['fee_rate'] ?? 0);
$acct = (string) ($row['display_account'] ?? $row['account_info'] ?? '');
$amount_txt = number_format($net, 2);
if ($gross > 0 && abs($gross - $net) > 0.001) {
$amount_txt = sprintf(
/* translators: 1: gross 2: net 3: fee percent */
__('申请 %1$s / 到账 %2$s(手续费 %3$d%%)', 'aksrt'),
number_format($gross, 2),
number_format($net, 2),
$fee
);
}
echo '<tr>';
echo '<td>' . esc_html((string) $id) . '</td>';
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
echo '<td>' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . '</td>';
echo '<td>' . esc_html($amount_txt) . '</td>';
echo '<td>' . esc_html((string) ($row['method'] ?? '')) . '</td>';
echo '<td>' . esc_html((string) ($row['account_info'] ?? '')) . '</td>';
echo '<td>' . esc_html($acct) . '</td>';
echo '<td>' . esc_html($status_options[$st] ?? $st) . '</td>';
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
echo '<td>';
@@ -169,13 +183,18 @@ class AKSRT_Admin_Pay_Withdrawals {
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
check_admin_referer('aksrt_pay_update_withdrawal_' . $id);
$ok = false;
$msg = 'withdraw_failed';
if ($id > 0 && $new_status !== '' && class_exists('AKSRT_Withdrawal')) {
$ok = AKSRT_Withdrawal::get_instance()->update_status($id, $new_status, $note);
$result = AKSRT_Withdrawal::get_instance()->process_status_change($id, $new_status, $note);
if (!is_wp_error($result) && $result) {
$msg = 'withdraw_updated';
} elseif (is_wp_error($result) && $result->get_error_code() === 'aksrt_balance') {
$msg = 'withdraw_refund_failed';
}
}
wp_safe_redirect($this->page_url('aksrt_withdraw', [
'aksrt_pay_msg' => $ok ? 'withdraw_updated' : 'withdraw_failed',
'aksrt_pay_msg' => $msg,
]));
exit;
}
@@ -208,8 +227,9 @@ class AKSRT_Admin_Pay_Withdrawals {
}
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
$map = [
'withdraw_updated' => __('提现状态已更新。', 'aksrt'),
'withdraw_failed' => __('提现状态更新失败。', 'aksrt'),
'withdraw_updated' => __('提现状态已更新。', 'aksrt'),
'withdraw_failed' => __('提现状态更新失败。', 'aksrt'),
'withdraw_refund_failed' => __('状态未更新:余额退回失败,请检查钱包后重试。', 'aksrt'),
];
if (!isset($map[$code])) {
return;
@@ -734,6 +734,10 @@ class AKSRT_Balance_Service {
if ($account === '') {
return new WP_Error('aksrt_withdraw_account', __('请填写收款账号信息', 'aksrt'), ['status' => 400]);
}
// One open balance withdrawal at a time (Zibll parity).
if (AKSRT_Withdrawal::get_instance()->has_open_withdrawal($user_id)) {
return new WP_Error('aksrt_withdraw_pending', __('您有处理中的提现申请,请等待完成后再申请', 'aksrt'), ['status' => 409]);
}
$wallet = AKSRT_Balance::balance($user_id);
if ($wallet < $amount) {
return new WP_Error('aksrt_balance_insufficient', __('余额不足', 'aksrt'), ['status' => 402]);
+349 -126
View File
@@ -1,8 +1,6 @@
<?php
/**
* AKSRT Withdrawal — 提现台账
*
* 管理用户提现申请、审核和处理。
* AKSRT Withdrawal — 提现台账 + 状态副作用(冻结退回)
*
* @package AKSRT
*/
@@ -11,156 +9,381 @@ defined('ABSPATH') || exit;
class AKSRT_Withdrawal {
const STATUS_PENDING = 'pending';
const STATUS_APPROVED = 'approved';
const STATUS_PROCESSING = 'processing';
const STATUS_COMPLETED = 'completed';
const STATUS_REJECTED = 'rejected';
public const STATUS_PENDING = 'pending';
public const STATUS_APPROVED = 'approved';
public const STATUS_PROCESSING = 'processing';
public const STATUS_COMPLETED = 'completed';
public const STATUS_REJECTED = 'rejected';
private static $instance = null;
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function table(): string {
return AKSRT_DB_Migrator::table('withdrawal');
}
private function table(): string {
return AKSRT_DB_Migrator::table('withdrawal');
}
/**
* 创建提现申请
*/
public function create(int $user_id, float $amount, string $method = '', string $account_info = ''): int {
global $wpdb;
/**
* 创建提现申请
*/
public function create(int $user_id, float $amount, string $method = '', string $account_info = ''): int {
global $wpdb;
if ($user_id <= 0 || $amount <= 0) return 0;
if ($user_id <= 0 || $amount <= 0) {
return 0;
}
$wpdb->insert($this->table(), [
'user_id' => $user_id,
'amount' => $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']);
$wpdb->insert($this->table(), [
'user_id' => $user_id,
'amount' => $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']);
return (int) $wpdb->insert_id;
}
return (int) $wpdb->insert_id;
}
/**
* 更新提现状态
*/
public function update_status(int $withdrawal_id, string $new_status, string $note = ''): bool {
global $wpdb;
/**
* Get single row.
*
* @return array<string,mixed>|null
*/
public function get(int $withdrawal_id): ?array {
global $wpdb;
if ($withdrawal_id <= 0) {
return null;
}
$row = $wpdb->get_row(
$wpdb->prepare("SELECT * FROM {$this->table()} WHERE id = %d LIMIT 1", $withdrawal_id),
ARRAY_A
);
return is_array($row) ? $row : null;
}
$update = ['status' => sanitize_key($new_status)];
$format = ['%s'];
/**
* 更新提现状态(无余额副作用)。优先使用 process_status_change()。
*/
public function update_status(int $withdrawal_id, string $new_status, string $note = ''): bool {
global $wpdb;
if ($new_status === self::STATUS_COMPLETED || $new_status === self::STATUS_REJECTED) {
$update['processed_at'] = current_time('mysql');
$format[] = '%s';
}
$update = ['status' => sanitize_key($new_status)];
$format = ['%s'];
if ($note !== '') {
$update['note'] = sanitize_textarea_field($note);
$format[] = '%s';
}
if ($new_status === self::STATUS_COMPLETED || $new_status === self::STATUS_REJECTED) {
$update['processed_at'] = current_time('mysql');
$format[] = '%s';
}
return $wpdb->update($this->table(), $update, ['id' => $withdrawal_id], $format, ['%d']) !== false;
}
if ($note !== '') {
$update['note'] = sanitize_textarea_field($note);
$format[] = '%s';
}
/**
* 获取用户提现列表
*/
public function get_user_withdrawals(int $user_id, array $args = []): array {
global $wpdb;
return $wpdb->update($this->table(), $update, ['id' => $withdrawal_id], $format, ['%d']) !== false;
}
$where = ['user_id = %d'];
$params = [$user_id];
/**
* Status transition with balance side-effects.
* Reject of balance-source holds refunds gross_amount once (idempotent).
*
* @return true|WP_Error
*/
public function process_status_change(int $withdrawal_id, string $new_status, string $note = '') {
$new_status = sanitize_key($new_status);
$allowed = [
self::STATUS_PENDING,
self::STATUS_APPROVED,
self::STATUS_PROCESSING,
self::STATUS_COMPLETED,
self::STATUS_REJECTED,
];
if ($withdrawal_id <= 0 || !in_array($new_status, $allowed, true)) {
return new WP_Error('aksrt_withdraw_status', __('提现状态无效', 'aksrt'));
}
$status = sanitize_key((string) ($args['status'] ?? ''));
if ($status !== '') {
$where[] = 'status = %s';
$params[] = $status;
}
$row = $this->get($withdrawal_id);
if (!$row) {
return new WP_Error('aksrt_withdraw_missing', __('提现记录不存在', 'aksrt'));
}
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$old_status = sanitize_key((string) ($row['status'] ?? ''));
if ($old_status === $new_status && $note === '') {
return true;
}
$sql = "SELECT * FROM {$this->table()} WHERE " . implode(' AND ', $where) . " ORDER BY created_at DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
$meta = $this->parse_account_meta((string) ($row['account_info'] ?? ''));
$user_id = (int) ($row['user_id'] ?? 0);
return (array) $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
}
// Refund frozen balance when moving into rejected (once).
if (
$new_status === self::STATUS_REJECTED
&& $old_status !== self::STATUS_REJECTED
&& empty($meta['balance_refunded'])
&& $this->is_balance_source($meta)
&& class_exists('AKSRT_Balance')
&& $user_id > 0
) {
$gross = $this->resolve_gross_amount($row, $meta);
if ($gross > 0) {
$refund = AKSRT_Balance::add(
$user_id,
$gross,
'withdraw_refund',
['type' => 'withdraw', 'id' => $withdrawal_id],
sprintf(__('提现拒绝退回 #%d', 'aksrt'), $withdrawal_id)
);
if (is_wp_error($refund)) {
return $refund;
}
$meta['balance_refunded'] = true;
$meta['refunded_at'] = current_time('mysql');
}
}
/**
* 获取所有提现列表(管理后台)
*/
public function get_all(array $args = []): array {
global $wpdb;
$account_info = $this->encode_account_meta($meta, (string) ($row['account_info'] ?? ''));
$merged_note = $this->merge_note((string) ($row['note'] ?? ''), $note);
$where = [];
$params = [];
global $wpdb;
$update = [
'status' => $new_status,
'account_info' => $account_info,
];
$format = ['%s', '%s'];
$status = sanitize_key((string) ($args['status'] ?? ''));
if ($status !== '') {
$where[] = 'status = %s';
$params[] = $status;
}
if ($new_status === self::STATUS_COMPLETED || $new_status === self::STATUS_REJECTED) {
$update['processed_at'] = current_time('mysql');
$format[] = '%s';
}
if ($merged_note !== '') {
$update['note'] = $merged_note;
$format[] = '%s';
}
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$ok = $wpdb->update($this->table(), $update, ['id' => $withdrawal_id], $format, ['%d']) !== false;
return $ok ? true : new WP_Error('aksrt_withdraw_update', __('提现状态更新失败', 'aksrt'));
}
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
/**
* Whether user has open (pending/processing/approved) withdrawals.
*/
public function has_open_withdrawal(int $user_id): bool {
return $this->count_open($user_id) > 0;
}
if (!empty($params)) {
return (array) $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
}
return (array) $wpdb->get_results($sql, ARRAY_A);
}
/**
* Count open withdrawals.
*/
public function count_open(int $user_id): int {
global $wpdb;
if ($user_id <= 0) {
return 0;
}
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$this->table()}
WHERE user_id = %d AND status IN ('pending','approved','processing')",
$user_id
));
}
/**
* 获取提现总数(支持状态过滤)
*/
public function get_count(array $args = []): int {
global $wpdb;
/**
* Sum of frozen gross amounts still open (balance source).
*/
public function sum_pending_hold(int $user_id): float {
global $wpdb;
if ($user_id <= 0) {
return 0.0;
}
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT amount, account_info FROM {$this->table()}
WHERE user_id = %d AND status IN ('pending','approved','processing')",
$user_id
), ARRAY_A);
$total = 0.0;
foreach ((array) $rows as $row) {
$meta = $this->parse_account_meta((string) ($row['account_info'] ?? ''));
if (!$this->is_balance_source($meta) || !empty($meta['balance_refunded'])) {
continue;
}
$total += $this->resolve_gross_amount($row, $meta);
}
return round($total, 2);
}
$status = sanitize_key((string) ($args['status'] ?? ''));
if ($status !== '') {
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$this->table()} WHERE status = %s",
$status
));
}
return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$this->table()}");
}
/**
* 获取用户提现列表
*
* @param array{status?:string,limit?:int,offset?:int,open_only?:bool} $args
* @return list<array<string,mixed>>
*/
public function get_user_withdrawals(int $user_id, array $args = []): array {
global $wpdb;
/**
* 获取提现统计
*/
public function get_stats(): array {
global $wpdb;
$where = ['user_id = %d'];
$params = [$user_id];
$row = $wpdb->get_row(
"SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
COALESCE(SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END), 0) as total_paid
FROM {$this->table()}",
ARRAY_A
);
if (!empty($args['open_only'])) {
$where[] = "status IN ('pending','approved','processing')";
} else {
$status = sanitize_key((string) ($args['status'] ?? ''));
if ($status !== '') {
$where[] = 'status = %s';
$params[] = $status;
}
}
return [
'total' => (int) ($row['total'] ?? 0),
'pending' => (int) ($row['pending'] ?? 0),
'completed' => (int) ($row['completed'] ?? 0),
'total_paid' => (float) ($row['total_paid'] ?? 0),
];
}
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$sql = "SELECT * FROM {$this->table()} WHERE " . implode(' AND ', $where) . ' ORDER BY created_at DESC LIMIT %d OFFSET %d';
$params[] = $limit;
$params[] = $offset;
$rows = (array) $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
return array_map([$this, 'normalize_row'], $rows);
}
/**
* 获取所有提现列表(管理后台)
*
* @return list<array<string,mixed>>
*/
public function get_all(array $args = []): array {
global $wpdb;
$where = [];
$params = [];
$status = sanitize_key((string) ($args['status'] ?? ''));
if ($status !== '') {
$where[] = 'status = %s';
$params[] = $status;
}
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
$offset = max(0, (int) ($args['offset'] ?? 0));
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
$sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
$params[] = $limit;
$params[] = $offset;
if (!empty($params)) {
$rows = (array) $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
} else {
$rows = (array) $wpdb->get_results($sql, ARRAY_A);
}
return array_map([$this, 'normalize_row'], $rows);
}
public function get_count(array $args = []): int {
global $wpdb;
$status = sanitize_key((string) ($args['status'] ?? ''));
if ($status !== '') {
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$this->table()} WHERE status = %s",
$status
));
}
return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$this->table()}");
}
public function get_stats(): array {
global $wpdb;
$row = $wpdb->get_row(
"SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
COALESCE(SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END), 0) as total_paid
FROM {$this->table()}",
ARRAY_A
);
return [
'total' => (int) ($row['total'] ?? 0),
'pending' => (int) ($row['pending'] ?? 0),
'completed' => (int) ($row['completed'] ?? 0),
'total_paid' => (float) ($row['total_paid'] ?? 0),
];
}
/**
* @param array<string,mixed> $row
* @return array<string,mixed>
*/
private function normalize_row(array $row): array {
$meta = $this->parse_account_meta((string) ($row['account_info'] ?? ''));
$gross = $this->resolve_gross_amount($row, $meta);
$row['meta'] = $meta;
$row['gross_amount'] = $gross;
$row['fee_rate'] = (int) ($meta['fee_rate'] ?? 0);
$row['display_account'] = (string) ($meta['account'] ?? $row['account_info'] ?? '');
$row['source'] = (string) ($meta['source'] ?? 'balance');
return $row;
}
/**
* @return array<string,mixed>
*/
private function parse_account_meta(string $raw): array {
$raw = trim($raw);
if ($raw === '') {
return [];
}
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : ['account' => $raw];
}
/**
* @param array<string,mixed> $meta
*/
private function encode_account_meta(array $meta, string $fallback_raw): string {
if ($meta === []) {
return $fallback_raw;
}
$json = wp_json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
return is_string($json) ? $json : $fallback_raw;
}
/**
* @param array<string,mixed> $meta
*/
private function is_balance_source(array $meta): bool {
$source = sanitize_key((string) ($meta['source'] ?? 'balance'));
// Empty / balance = cash wallet freeze path.
return $source === '' || $source === 'balance';
}
/**
* @param array<string,mixed> $row
* @param array<string,mixed> $meta
*/
private function resolve_gross_amount(array $row, array $meta): float {
if (isset($meta['gross_amount'])) {
return max(0, round((float) $meta['gross_amount'], 2));
}
// Legacy rows: amount was net or gross without meta.
return max(0, round((float) ($row['amount'] ?? 0), 2));
}
private function merge_note(string $old, string $new): string {
$new = sanitize_textarea_field($new);
if ($new === '') {
return sanitize_textarea_field($old);
}
$old = sanitize_textarea_field($old);
if ($old === '') {
return $new;
}
return $old . "\n" . $new;
}
}
@@ -26,6 +26,14 @@ $exchange_on = !empty($recharge['exchange']['enabled']);
$all_settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_all() : [];
$withdraw_on = !empty($all_settings['pay_balance_withdraw_s']);
$withdraw_min = max(1, (float) ($all_settings['pay_balance_withdraw_lowest_money'] ?? $all_settings['pay_rebate_withdraw_lowest_money'] ?? 50));
$withdraw_pending_hold = 0.0;
$withdraw_history = [];
if ($withdraw_on && class_exists('AKSRT_Withdrawal')) {
$wd_svc = AKSRT_Withdrawal::get_instance();
$withdraw_pending_hold = $wd_svc->sum_pending_hold((int) ($user_id ?? 0));
$withdraw_history = $wd_svc->get_user_withdrawals((int) ($user_id ?? 0), ['limit' => 30]);
}
$withdraw_processing = $withdraw_pending_hold > 0;
$card_pass_balance = ['enabled' => false, 'desc' => '', 'only_password' => true];
if (class_exists('AKSRT_Charge_Card')) {
$opts = AKSRT_Charge_Card::get_instance()->get_public_options();
@@ -49,6 +57,12 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元');
<div class="uc-asset-card__title">
<i class="fas fa-wallet em12 mr6" aria-hidden="true"></i><?php esc_html_e('余额', 'aksrt'); ?>
</div>
<?php if ($withdraw_on) : ?>
<button type="button" class="uc-asset-card__head-action muted-color px12" data-aksrt-withdraw-history-open>
<?php echo $withdraw_processing ? esc_html__('提现处理中', 'aksrt') : esc_html__('提现记录', 'aksrt'); ?>
<i class="fa fa-angle-right em12 ml3" aria-hidden="true"></i>
</button>
<?php endif; ?>
</div>
<div class="uc-asset-card__body flex jsb ac">
<div class="uc-asset-card__amount c-blue-2">
@@ -60,7 +74,11 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元');
<button type="button" class="but c-blue-2 mr6 hollow" data-aksrt-transfer-open="balance"><?php esc_html_e('转账', 'aksrt'); ?></button>
<?php endif; ?>
<?php if ($withdraw_on) : ?>
<button type="button" class="but c-blue-2 mr6 hollow" data-aksrt-withdraw-open><?php esc_html_e('提现', 'aksrt'); ?></button>
<?php if ($withdraw_processing) : ?>
<button type="button" class="but c-blue-2 mr6 hollow" data-aksrt-withdraw-history-open><?php esc_html_e('提现处理中', 'aksrt'); ?></button>
<?php else : ?>
<button type="button" class="but c-blue-2 mr6 hollow" data-aksrt-withdraw-open><?php esc_html_e('提现', 'aksrt'); ?></button>
<?php endif; ?>
<?php endif; ?>
<button type="button" class="but b-blue-2" data-aksrt-charge-open><?php esc_html_e('充值', 'aksrt'); ?></button>
</div>
@@ -74,6 +92,12 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元');
<i class="fa fa-angle-right em12 ml3" aria-hidden="true"></i>
</button>
<?php endif; ?>
<?php if ($withdraw_on) : ?>
<div class="uc-asset-card__foot muted-2-color em09 flex jsb ac mt6">
<span><?php printf(esc_html__('处理中冻结 %s', 'aksrt'), esc_html(class_exists('AKSRT_Balance') ? AKSRT_Balance::format((float) $withdraw_pending_hold) : number_format_i18n((float) $withdraw_pending_hold, 2))); ?></span>
<button type="button" class="muted-color" data-aksrt-withdraw-history-open><?php esc_html_e('提现记录', 'aksrt'); ?></button>
</div>
<?php endif; ?>
</div>
<?php if (!empty($card_pass_balance['enabled'])) : ?>
@@ -231,6 +255,53 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元');
</div>
<?php endif; ?>
<?php if ($withdraw_on) :
$status_labels = [
'pending' => __('待处理', 'aksrt'),
'approved' => __('已通过', 'aksrt'),
'processing' => __('处理中', 'aksrt'),
'completed' => __('已完成', 'aksrt'),
'rejected' => __('已拒绝', 'aksrt'),
];
?>
<div class="uc-modal" id="aksrt-withdraw-history-modal" hidden data-aksrt-withdraw-history-modal>
<div class="uc-modal__overlay" data-aksrt-withdraw-history-close></div>
<div class="uc-modal__dialog" role="dialog" aria-modal="true">
<button type="button" class="uc-modal__close" data-aksrt-withdraw-history-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fas fa-times" aria-hidden="true"></i></button>
<h3 class="uc-modal__title"><i class="fas fa-file-invoice-dollar mr6" aria-hidden="true"></i><?php esc_html_e('提现记录', 'aksrt'); ?></h3>
<div class="uc-withdraw-history">
<?php if (!empty($withdraw_history)) : ?>
<?php foreach ($withdraw_history as $wd) :
$st = sanitize_key((string) ($wd['status'] ?? ''));
$gross = (float) ($wd['gross_amount'] ?? $wd['amount'] ?? 0);
$net = (float) ($wd['amount'] ?? 0);
$st_class = $st === 'rejected' ? 'c-red' : ($st === 'completed' ? 'c-green' : 'c-blue');
?>
<div class="uc-withdraw-history__item border-bottom padding-h10">
<div class="flex jsb ac">
<span class="badg badg-sm <?php echo esc_attr($st_class); ?>"><?php echo esc_html($status_labels[$st] ?? $st); ?></span>
<b class="em12"><?php echo esc_html(class_exists('AKSRT_Balance') ? AKSRT_Balance::format($gross) : number_format_i18n($gross, 2)); ?></b>
</div>
<div class="muted-2-color em09 mt6">
<?php echo esc_html((string) ($wd['created_at'] ?? '')); ?>
<?php if (abs($gross - $net) > 0.001) : ?>
· <?php printf(esc_html__('到账 %s', 'aksrt'), esc_html(class_exists('AKSRT_Balance') ? AKSRT_Balance::format($net) : number_format_i18n($net, 2))); ?>
<?php endif; ?>
</div>
<?php if (!empty($wd['note'])) : ?>
<div class="muted-2-color em09 mt3"><?php echo esc_html((string) $wd['note']); ?></div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php else : ?>
<div class="uc-transactions__empty"><p><?php esc_html_e('暂无提现记录', 'aksrt'); ?></p></div>
<?php endif; ?>
</div>
</div>
</div>
<?php endif; ?>
<?php if (empty($skip_wallet_ledger)) : ?>
<div class="uc-transactions uc-transactions--wallet">
<div class="uc-transactions__header">