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:
@@ -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
@@ -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]);
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user