积分与余额账务系统硬切重构
This commit is contained in:
@@ -23,8 +23,20 @@ abstract class ControllerBase {
|
||||
/**
|
||||
* Permission callback — requires manage_options capability.
|
||||
*/
|
||||
public function check_permission(): bool {
|
||||
return current_user_can('manage_options');
|
||||
public function check_permission(?\WP_REST_Request $request = null) {
|
||||
if (!current_user_can('manage_options')) {
|
||||
if ($request === null) {
|
||||
return false;
|
||||
}
|
||||
return new \WP_Error('aksrt_admin_forbidden', __('无权执行此操作', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
if ($request !== null) {
|
||||
$nonce = (string) $request->get_header('X-WP-Nonce');
|
||||
if ($nonce === '' || !wp_verify_nonce($nonce, 'wp_rest')) {
|
||||
return new \WP_Error('aksrt_rest_nonce', __('安全验证失败,请刷新页面后重试', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -307,7 +307,9 @@ class ImportExportController extends ControllerBase {
|
||||
|
||||
// Database tables
|
||||
$expected_tables = [
|
||||
$wpdb->prefix . 'aksrt_credit_log',
|
||||
$wpdb->prefix . 'aksrt_asset_account',
|
||||
$wpdb->prefix . 'aksrt_asset_transaction',
|
||||
$wpdb->prefix . 'aksrt_asset_entry',
|
||||
$wpdb->prefix . 'aksrt_signin_log',
|
||||
$wpdb->prefix . 'aksrt_dl_token',
|
||||
$wpdb->prefix . 'aksrt_notification',
|
||||
|
||||
@@ -50,7 +50,6 @@ class PayChargeCards {
|
||||
'balance' => __('余额', 'aksrt'),
|
||||
'vip' => __('VIP', 'aksrt'),
|
||||
'credits' => __('积分', 'aksrt'),
|
||||
'points' => __('积分', 'aksrt'),
|
||||
];
|
||||
$status_labels = [
|
||||
'active' => __('可用', 'aksrt'),
|
||||
@@ -81,7 +80,7 @@ class PayChargeCards {
|
||||
echo '<p><label>' . esc_html__('前缀', 'aksrt') . '<br><input type="text" name="prefix" value="" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('面额/积分', 'aksrt') . '<br><input type="number" step="0.01" name="amount" value="10" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('VIP等级', 'aksrt') . '<br><input type="number" name="vip_level" min="1" max="2" value="1" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('VIP天数', 'aksrt') . '<br><input type="number" name="vip_days" min="1" value="30" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('VIP天数', 'aksrt') . '<br><input type="number" name="days" min="1" value="30" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('过期时间', 'aksrt') . '<br><input type="datetime-local" name="expires_at" /></label></p>';
|
||||
echo '<p><label>' . esc_html__('备注', 'aksrt') . '<br><input type="text" name="note" value="" /></label></p>';
|
||||
submit_button(__('生成', 'aksrt'), 'primary', '', false);
|
||||
@@ -179,7 +178,7 @@ class PayChargeCards {
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td>';
|
||||
echo '<td>' . esc_html($type_labels[$row_type] ?? $row_type) . '</td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['password'] ?? $row['code'] ?? '')) . '</code></td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['password'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . esc_html((string) ($row['amount'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html($status_labels[$st] ?? $st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['expires_at'] ?? '—')) . '</td>';
|
||||
@@ -252,10 +251,10 @@ class PayChargeCards {
|
||||
$count = isset($_POST['count']) ? (int) $_POST['count'] : 10;
|
||||
$length = isset($_POST['length']) ? (int) $_POST['length'] : 16;
|
||||
$prefix = isset($_POST['prefix']) ? sanitize_text_field((string) wp_unslash($_POST['prefix'])) : '';
|
||||
$amount = isset($_POST['amount']) ? (float) wp_unslash($_POST['amount']) : 0;
|
||||
$amount = isset($_POST['amount']) ? trim(sanitize_text_field((string) wp_unslash($_POST['amount']))) : '';
|
||||
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
|
||||
$level = isset($_POST['vip_level']) ? max(1, min(2, (int) $_POST['vip_level'])) : 1;
|
||||
$days = isset($_POST['vip_days']) ? max(1, (int) $_POST['vip_days']) : 30;
|
||||
$days = isset($_POST['days']) ? max(1, (int) $_POST['days']) : 30;
|
||||
$expires = isset($_POST['expires_at']) ? sanitize_text_field((string) wp_unslash($_POST['expires_at'])) : '';
|
||||
if ($expires !== '') {
|
||||
$expires = str_replace('T', ' ', $expires);
|
||||
@@ -264,15 +263,30 @@ class PayChargeCards {
|
||||
}
|
||||
}
|
||||
|
||||
if ($type === 'points') {
|
||||
$type = 'credits';
|
||||
if (!in_array($type, ['balance', 'vip', 'credits'], true)) {
|
||||
wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed']));
|
||||
exit;
|
||||
}
|
||||
|
||||
$meta = [];
|
||||
if ($type === 'balance') {
|
||||
$meta['amount'] = max(0, $amount);
|
||||
try {
|
||||
$balance_amount = \AKSRT\Ledger\AssetAmount::balance($amount);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed']));
|
||||
exit;
|
||||
}
|
||||
if ($balance_amount->minor() <= 0) {
|
||||
wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed']));
|
||||
exit;
|
||||
}
|
||||
$meta['amount'] = $balance_amount->decimal();
|
||||
} elseif ($type === 'credits') {
|
||||
$meta['points'] = max(0, (int) $amount);
|
||||
if (!preg_match('/^[1-9]\d*$/', $amount)) {
|
||||
wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed']));
|
||||
exit;
|
||||
}
|
||||
$meta['points'] = (int) $amount;
|
||||
} elseif ($type === 'vip') {
|
||||
$meta['level'] = $level;
|
||||
$meta['days'] = $days;
|
||||
|
||||
@@ -255,9 +255,9 @@ class PayCoupons {
|
||||
$id = \AKSRT\Payment\Coupon::get_instance()->create([
|
||||
'code' => sanitize_text_field((string) wp_unslash($_POST['code'] ?? '')),
|
||||
'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'fixed')),
|
||||
'value' => (float) ($_POST['value'] ?? 0),
|
||||
'min_amount' => (float) ($_POST['min_amount'] ?? 0),
|
||||
'max_discount' => (float) ($_POST['max_discount'] ?? 0),
|
||||
'value' => trim((string) wp_unslash($_POST['value'] ?? '0')),
|
||||
'min_amount' => trim((string) wp_unslash($_POST['min_amount'] ?? '0')),
|
||||
'max_discount' => trim((string) wp_unslash($_POST['max_discount'] ?? '0')),
|
||||
'usage_limit' => (int) ($_POST['usage_limit'] ?? 0),
|
||||
'per_user_limit' => (int) ($_POST['per_user_limit'] ?? 0),
|
||||
'product_scope' => $this->parse_product_scope_from_request(),
|
||||
@@ -281,9 +281,9 @@ class PayCoupons {
|
||||
if (class_exists('\AKSRT\Payment\Coupon')) {
|
||||
$result = \AKSRT\Payment\Coupon::get_instance()->batch_create([
|
||||
'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'fixed')),
|
||||
'value' => (float) ($_POST['value'] ?? 0),
|
||||
'min_amount' => (float) ($_POST['min_amount'] ?? 0),
|
||||
'max_discount' => (float) ($_POST['max_discount'] ?? 0),
|
||||
'value' => trim((string) wp_unslash($_POST['value'] ?? '0')),
|
||||
'min_amount' => trim((string) wp_unslash($_POST['min_amount'] ?? '0')),
|
||||
'max_discount' => trim((string) wp_unslash($_POST['max_discount'] ?? '0')),
|
||||
'usage_limit' => (int) ($_POST['usage_limit'] ?? 0),
|
||||
'per_user_limit' => (int) ($_POST['per_user_limit'] ?? 0),
|
||||
'product_scope' => $this->parse_product_scope_from_request(),
|
||||
@@ -315,9 +315,9 @@ class PayCoupons {
|
||||
if ($id > 0 && class_exists('\AKSRT\Payment\Coupon')) {
|
||||
$ok = \AKSRT\Payment\Coupon::get_instance()->update($id, [
|
||||
'type' => sanitize_key((string) wp_unslash($_POST['type'] ?? 'fixed')),
|
||||
'value' => (float) ($_POST['value'] ?? 0),
|
||||
'min_amount' => (float) ($_POST['min_amount'] ?? 0),
|
||||
'max_discount' => (float) ($_POST['max_discount'] ?? 0),
|
||||
'value' => trim((string) wp_unslash($_POST['value'] ?? '0')),
|
||||
'min_amount' => trim((string) wp_unslash($_POST['min_amount'] ?? '0')),
|
||||
'max_discount' => trim((string) wp_unslash($_POST['max_discount'] ?? '0')),
|
||||
'usage_limit' => (int) ($_POST['usage_limit'] ?? 0),
|
||||
'per_user_limit' => (int) ($_POST['per_user_limit'] ?? 0),
|
||||
'product_scope' => $this->parse_product_scope_from_request(),
|
||||
|
||||
@@ -60,18 +60,26 @@ class PayIncome {
|
||||
|
||||
$cash_stats = (array) ($stats['cash'] ?? []);
|
||||
$points_stats = (array) ($stats['points'] ?? []);
|
||||
$cash_amount = static function ($value): string {
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance(is_string($value) ? $value : '')->formatted();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return '0.00';
|
||||
}
|
||||
};
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
__('现金:合计 ¥%1$s · 待结算 ¥%2$s · 已结算 ¥%3$s;积分:合计 %4$s · 待结算 %5$s · 已结算 %6$s', 'aksrt'),
|
||||
number_format((float) ($cash_stats['total'] ?? 0), 2),
|
||||
number_format((float) ($cash_stats['pending'] ?? 0), 2),
|
||||
number_format((float) ($cash_stats['settled'] ?? 0), 2),
|
||||
number_format((float) ($points_stats['total'] ?? 0), 0),
|
||||
number_format((float) ($points_stats['pending'] ?? 0), 0),
|
||||
number_format((float) ($points_stats['settled'] ?? 0), 0)
|
||||
$cash_amount($cash_stats['total'] ?? '0'),
|
||||
$cash_amount($cash_stats['pending'] ?? '0'),
|
||||
$cash_amount($cash_stats['settled'] ?? '0'),
|
||||
number_format_i18n(max(0, (int) ($points_stats['total'] ?? 0))),
|
||||
number_format_i18n(max(0, (int) ($points_stats['pending'] ?? 0))),
|
||||
number_format_i18n(max(0, (int) ($points_stats['settled'] ?? 0)))
|
||||
)) . '</p>';
|
||||
|
||||
echo '<form id="aksrt-pay-batch-settle-income" method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_batch_settle_income" />';
|
||||
echo '<input type="hidden" name="operation_key" value="' . esc_attr(wp_generate_uuid4()) . '" />';
|
||||
wp_nonce_field('aksrt_pay_batch_settle_income');
|
||||
submit_button(__('批量结算选中记录', 'aksrt'), 'secondary', '', false);
|
||||
echo '</form>';
|
||||
@@ -135,8 +143,10 @@ class PayIncome {
|
||||
echo '<td><code>' . esc_html((string) ($row['order_number'] ?? '')) . '</code></td>';
|
||||
$is_points = (string) ($row['currency'] ?? 'CNY') === 'points';
|
||||
$row_currency = $is_points ? __('积分', 'aksrt') : '¥';
|
||||
$row_decimals = $is_points ? 0 : 2;
|
||||
echo '<td>' . esc_html(number_format((float) ($row['amount'] ?? 0), $row_decimals) . ' ' . $row_currency) . '</td>';
|
||||
$row_amount = $is_points
|
||||
? number_format_i18n(max(0, (int) ($row['amount'] ?? 0)))
|
||||
: $cash_amount($row['amount'] ?? '0');
|
||||
echo '<td>' . esc_html($row_amount . ' ' . $row_currency) . '</td>';
|
||||
echo '<td>' . esc_html($st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td>';
|
||||
@@ -144,6 +154,7 @@ class PayIncome {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_settle_income" />';
|
||||
echo '<input type="hidden" name="revenue_id" value="' . esc_attr((string) $id) . '" />';
|
||||
echo '<input type="hidden" name="operation_key" value="' . esc_attr(wp_generate_uuid4()) . '" />';
|
||||
wp_nonce_field('aksrt_pay_settle_income_' . $id);
|
||||
submit_button(__('结算', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
@@ -163,7 +174,11 @@ class PayIncome {
|
||||
}
|
||||
$id = isset($_POST['revenue_id']) ? (int) $_POST['revenue_id'] : 0;
|
||||
check_admin_referer('aksrt_pay_settle_income_' . $id);
|
||||
$ok = $id > 0 && class_exists('\AKSRT\Payment\Revenue') && \AKSRT\Payment\Revenue::get_instance()->settle($id, get_current_user_id());
|
||||
$request_key = sanitize_text_field((string) wp_unslash($_POST['operation_key'] ?? ''));
|
||||
$operation_key = wp_is_uuid($request_key)
|
||||
? \AKSRT\Ledger\OperationKey::deterministic('admin-revenue-settle', [get_current_user_id(), strtolower($request_key)])
|
||||
: '';
|
||||
$ok = $id > 0 && $operation_key !== '' && class_exists('\AKSRT\Payment\Revenue') && \AKSRT\Payment\Revenue::get_instance()->settle($id, get_current_user_id(), $operation_key);
|
||||
wp_safe_redirect($this->page_url('aksrt_income', ['aksrt_pay_msg' => $ok ? 'income_settled' : 'income_failed']));
|
||||
exit;
|
||||
}
|
||||
@@ -187,8 +202,12 @@ class PayIncome {
|
||||
}
|
||||
}
|
||||
$ids = array_values(array_unique($ids));
|
||||
$batch_result = !empty($ids) && class_exists('\\AKSRT\\Payment\\Revenue')
|
||||
? \AKSRT\Payment\Revenue::get_instance()->settle_batch($ids, get_current_user_id())
|
||||
$request_key = sanitize_text_field((string) wp_unslash($_POST['operation_key'] ?? ''));
|
||||
$operation_key = wp_is_uuid($request_key)
|
||||
? \AKSRT\Ledger\OperationKey::deterministic('admin-revenue-settle-batch', [get_current_user_id(), strtolower($request_key)])
|
||||
: '';
|
||||
$batch_result = !empty($ids) && $operation_key !== '' && class_exists('\\AKSRT\\Payment\\Revenue')
|
||||
? \AKSRT\Payment\Revenue::get_instance()->settle_batch($ids, get_current_user_id(), $operation_key)
|
||||
: ['settled' => [], 'failed' => $ids];
|
||||
$message = empty($batch_result['settled'])
|
||||
? 'income_failed'
|
||||
|
||||
@@ -107,17 +107,6 @@ class PayMenus {
|
||||
wp_die(esc_html__('分成模块未初始化。', 'aksrt'));
|
||||
},
|
||||
],
|
||||
'rebate' => [
|
||||
'slug' => 'aksrt_rebate',
|
||||
'title' => __('推广返佣', 'aksrt'),
|
||||
'callback' => static function (): void {
|
||||
if (class_exists('AKSRT\\Admin\\Native\\PayRebate')) {
|
||||
PayRebate::get_instance()->render_rebate_page();
|
||||
return;
|
||||
}
|
||||
wp_die(esc_html__('返佣模块未初始化。', 'aksrt'));
|
||||
},
|
||||
],
|
||||
'withdraw' => [
|
||||
'slug' => 'aksrt_withdraw',
|
||||
'title' => __('提现审核', 'aksrt'),
|
||||
@@ -224,7 +213,7 @@ class PayMenus {
|
||||
$this->stat_card(__('订单总数', 'aksrt'), (string) (int) ($order_stats['total'] ?? 0), $this->page_url('aksrt_orders'));
|
||||
$this->stat_card(__('待支付', 'aksrt'), (string) (int) ($order_stats['pending'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'pending']));
|
||||
$this->stat_card(__('已支付', 'aksrt'), (string) (int) ($order_stats['paid'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'paid']));
|
||||
$this->stat_card(__('营收', 'aksrt'), number_format((float) ($order_stats['revenue'] ?? 0), 2), $this->page_url('aksrt_orders'));
|
||||
$this->stat_card(__('营收', 'aksrt'), (string) ($order_stats['revenue'] ?? '0.00'), $this->page_url('aksrt_orders'));
|
||||
$this->stat_card(__('待审提现', 'aksrt'), (string) (int) ($withdraw_stats['pending'] ?? 0), $this->page_url('aksrt_withdraw', ['status' => 'pending']));
|
||||
$this->stat_card(__('可用卡密', 'aksrt'), (string) (int) ($card_stats['active'] ?? 0), $this->page_url('aksrt_charge_cards', ['status' => 'active']));
|
||||
echo '</div>';
|
||||
|
||||
@@ -213,7 +213,7 @@ class PayOrders {
|
||||
echo '<td><code>' . esc_html($order_number) . '</code></td>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['product_type'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . ' ' . esc_html((string) ($row['currency'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html($this->cash_decimal((string) ($row['amount'] ?? '0'))) . ' ' . esc_html((string) ($row['currency'] ?? '')) . '</td>';
|
||||
echo '<td>' . esc_html($status_label) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;">';
|
||||
@@ -296,7 +296,7 @@ class PayOrders {
|
||||
__('用户', 'aksrt') => $user ? $user->display_name . ' (#' . $user_id . ')' : (string) $user_id,
|
||||
__('商品类型', 'aksrt') => (string) ($order['product_type'] ?? ''),
|
||||
__('商品 ID', 'aksrt') => (string) ($order['product_id'] ?? ''),
|
||||
__('金额', 'aksrt') => number_format((float) ($order['amount'] ?? 0), 2) . ' ' . (string) ($order['currency'] ?? ''),
|
||||
__('金额', 'aksrt') => $this->cash_decimal((string) ($order['amount'] ?? '0')) . ' ' . (string) ($order['currency'] ?? ''),
|
||||
__('状态', 'aksrt') => $labels[$status] ?? $status,
|
||||
__('支付方式', 'aksrt') => (string) ($order['payment_method'] ?? ''),
|
||||
__('交易号', 'aksrt') => (string) ($order['payment_transaction_id'] ?? ''),
|
||||
@@ -465,6 +465,14 @@ class PayOrders {
|
||||
}
|
||||
}
|
||||
|
||||
private function cash_decimal(string $value): string {
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance($value)->decimal();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return '0.00';
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
|
||||
@@ -94,17 +94,16 @@ class PayProducts {
|
||||
$modo_label = $modo === 'points' ? __('积分', 'aksrt') : __('现金', 'aksrt');
|
||||
|
||||
if ($modo === 'points') {
|
||||
$list_price = (int) ceil((float) ($meta['points_price'] ?? $meta['price'] ?? 0));
|
||||
$list_price = max(0, (int) ($meta['points_price'] ?? 0));
|
||||
$price_text = sprintf(__('%d 积分', 'aksrt'), $list_price);
|
||||
} else {
|
||||
$list_price = round((float) ($meta['price'] ?? 0), 2);
|
||||
$price_text = number_format($list_price, 2);
|
||||
$price_text = $this->cash_decimal((string) ($meta['price'] ?? '0'));
|
||||
}
|
||||
|
||||
$sales = $sales_map[$post_id] ?? ['sales_count' => 0, 'avg_amount' => 0.0, 'revenue' => 0.0];
|
||||
$sales = $sales_map[$post_id] ?? ['sales_count' => 0, 'avg_amount' => '0.00', 'revenue' => '0.00'];
|
||||
$db_sales = (int) ($sales['sales_count'] ?? 0);
|
||||
$avg = (float) ($sales['avg_amount'] ?? 0);
|
||||
$revenue = (float) ($sales['revenue'] ?? 0);
|
||||
$avg = $this->cash_decimal((string) ($sales['avg_amount'] ?? '0'));
|
||||
$revenue = $this->cash_decimal((string) ($sales['revenue'] ?? '0'));
|
||||
$meta_sales = (int) get_post_meta($post_id, \AKSRT\Core\DomainModel::RESOURCE_SALES_VOLUME_KEY, true);
|
||||
|
||||
$edit_url = get_edit_post_link($post_id, 'raw');
|
||||
@@ -130,10 +129,10 @@ class PayProducts {
|
||||
echo '<td>' . esc_html((string) $pt_label) . '</td>';
|
||||
echo '<td>' . esc_html($modo_label) . '</td>';
|
||||
echo '<td>' . esc_html($price_text) . '</td>';
|
||||
echo '<td>' . esc_html(number_format($avg, 2)) . '</td>';
|
||||
echo '<td>' . esc_html($avg) . '</td>';
|
||||
echo '<td>' . esc_html((string) $db_sales) . '</td>';
|
||||
echo '<td>' . esc_html((string) max(0, $meta_sales)) . '</td>';
|
||||
echo '<td>' . esc_html(number_format($revenue, 2)) . '</td>';
|
||||
echo '<td>' . esc_html($revenue) . '</td>';
|
||||
echo '<td style="display:flex;gap:6px;flex-wrap:wrap;">';
|
||||
if ($edit_url) {
|
||||
echo '<a class="button button-small" href="' . esc_url($edit_url) . '">' . esc_html__('编辑', 'aksrt') . '</a>';
|
||||
@@ -337,6 +336,14 @@ class PayProducts {
|
||||
}
|
||||
}
|
||||
|
||||
private function cash_decimal(string $value): string {
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance($value)->decimal();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return '0.00';
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
namespace AKSRT\Admin\Native;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class PayRebate {
|
||||
|
||||
public const CAPABILITY = 'manage_options';
|
||||
public const PAGE_SLUG = 'aksrt_rebate';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_action('admin_post_aksrt_pay_settle_rebate', [$this, 'handle_settle_rebate']);
|
||||
add_action('admin_post_aksrt_pay_cancel_rebate', [$this, 'handle_cancel_rebate']);
|
||||
}
|
||||
|
||||
public function render_rebate_page(): void {
|
||||
$this->ensure_cap();
|
||||
$this->render_header(__('推广返佣', 'aksrt'));
|
||||
|
||||
if (!class_exists('\AKSRT\Payment\Rebate')) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__('返佣模块未初始化。', 'aksrt') . '</p></div>';
|
||||
$this->render_footer();
|
||||
return;
|
||||
}
|
||||
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||
$user_id = isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : (isset($_GET['referrer_id']) ? max(0, (int) $_GET['referrer_id']) : 0);
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
$svc = \AKSRT\Payment\Rebate::get_instance();
|
||||
$stats = $svc->get_stats();
|
||||
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: 1: total 2: pending 3: settled amount */
|
||||
__('记录 %1$d · 待结算 %2$d · 已结算金额 %3$s', 'aksrt'),
|
||||
(int) ($stats['total'] ?? 0),
|
||||
(int) ($stats['pending'] ?? 0),
|
||||
number_format((float) ($stats['settled_amount'] ?? 0), 2)
|
||||
)) . '</p>';
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
echo '<input type="hidden" name="page" value="aksrt_rebate" />';
|
||||
echo '<select name="status">';
|
||||
foreach (['' => __('全部状态', 'aksrt'), 'pending' => __('待结算', 'aksrt'), 'settled' => __('已结算', 'aksrt'), 'cancelled' => __('已取消', 'aksrt')] as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select> ';
|
||||
echo '<input type="number" min="0" name="user_id" value="' . esc_attr($user_id > 0 ? (string) $user_id : '') . '" placeholder="' . esc_attr__('推广用户 ID', 'aksrt') . '" class="small-text" /> ';
|
||||
submit_button(__('筛选', 'aksrt'), 'secondary', '', false);
|
||||
if ($user_id > 0) {
|
||||
echo ' <a class="button button-small" href="' . esc_url($this->page_url('aksrt_rebate')) . '">' . esc_html__('清除用户过滤', 'aksrt') . '</a>';
|
||||
}
|
||||
echo '</form>';
|
||||
|
||||
$list_args = [
|
||||
'status' => $status,
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page_num - 1) * $per_page,
|
||||
];
|
||||
if ($user_id > 0) {
|
||||
$list_args['user_id'] = $user_id;
|
||||
}
|
||||
$result = $svc->get_list($list_args);
|
||||
$items = $result['items'] ?? [];
|
||||
$total = (int) ($result['total'] ?? 0);
|
||||
$pages = max(1, (int) ceil(max(1, $total) / $per_page));
|
||||
|
||||
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
|
||||
echo '<th>ID</th><th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('订单号', 'aksrt') . '</th><th>' . esc_html__('金额', 'aksrt') . '</th><th>' . esc_html__('状态', 'aksrt') . '</th><th>' . esc_html__('时间', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
|
||||
echo '</tr></thead><tbody>';
|
||||
if (!$items) {
|
||||
echo '<tr><td colspan="7">' . esc_html__('暂无返佣记录', 'aksrt') . '</td></tr>';
|
||||
}
|
||||
foreach ($items as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
$user_id = (int) ($row['user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
$st = (string) ($row['status'] ?? '');
|
||||
echo '<tr>';
|
||||
echo '<td>' . esc_html((string) $id) . '</td>';
|
||||
echo '<td>' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '</td>';
|
||||
echo '<td><code>' . esc_html((string) ($row['order_number'] ?? '')) . '</code></td>';
|
||||
echo '<td>' . esc_html(number_format((float) ($row['amount'] ?? 0), 2)) . '</td>';
|
||||
echo '<td>' . esc_html($st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td style="display:flex;gap:4px;">';
|
||||
if ($st === 'pending') {
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_settle_rebate" />';
|
||||
echo '<input type="hidden" name="rebate_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_settle_rebate_' . $id);
|
||||
submit_button(__('结算', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_cancel_rebate" />';
|
||||
echo '<input type="hidden" name="rebate_id" value="' . esc_attr((string) $id) . '" />';
|
||||
wp_nonce_field('aksrt_pay_cancel_rebate_' . $id);
|
||||
submit_button(__('取消', 'aksrt'), 'small', '', false);
|
||||
echo '</form>';
|
||||
} else {
|
||||
echo '—';
|
||||
}
|
||||
echo '</td></tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
$this->render_pagination($pages, $page_num);
|
||||
$this->render_footer();
|
||||
}
|
||||
|
||||
public function handle_settle_rebate(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$id = isset($_POST['rebate_id']) ? (int) $_POST['rebate_id'] : 0;
|
||||
check_admin_referer('aksrt_pay_settle_rebate_' . $id);
|
||||
$ok = $id > 0 && class_exists('\AKSRT\Payment\Rebate') && \AKSRT\Payment\Rebate::get_instance()->settle($id);
|
||||
wp_safe_redirect($this->page_url('aksrt_rebate', ['aksrt_pay_msg' => $ok ? 'rebate_settled' : 'rebate_failed']));
|
||||
exit;
|
||||
}
|
||||
|
||||
public function handle_cancel_rebate(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('权限不足', 'aksrt'));
|
||||
}
|
||||
$id = isset($_POST['rebate_id']) ? (int) $_POST['rebate_id'] : 0;
|
||||
check_admin_referer('aksrt_pay_cancel_rebate_' . $id);
|
||||
$ok = $id > 0 && class_exists('\AKSRT\Payment\Rebate') && \AKSRT\Payment\Rebate::get_instance()->cancel($id);
|
||||
wp_safe_redirect($this->page_url('aksrt_rebate', ['aksrt_pay_msg' => $ok ? 'rebate_cancelled' : 'rebate_failed']));
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function ensure_cap(): void {
|
||||
if (!current_user_can(self::CAPABILITY)) {
|
||||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||||
}
|
||||
}
|
||||
|
||||
private function render_header(string $title): void {
|
||||
echo '<div class="wrap">';
|
||||
echo '<h1>' . esc_html($title) . '</h1>';
|
||||
$this->render_flash();
|
||||
}
|
||||
|
||||
private function render_footer(): void {
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
private function render_flash(): void {
|
||||
if (empty($_GET['aksrt_pay_msg'])) {
|
||||
return;
|
||||
}
|
||||
$code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg']));
|
||||
$map = [
|
||||
'rebate_settled' => __('返佣已结算。', 'aksrt'),
|
||||
'rebate_cancelled' => __('返佣已取消。', 'aksrt'),
|
||||
'rebate_failed' => __('返佣操作失败。', 'aksrt'),
|
||||
];
|
||||
if (!isset($map[$code])) {
|
||||
return;
|
||||
}
|
||||
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
||||
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
||||
}
|
||||
|
||||
private function page_url(string $slug, array $args = []): string {
|
||||
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
|
||||
}
|
||||
|
||||
private function render_pagination(int $pages, int $page_num): void {
|
||||
if ($pages <= 1) {
|
||||
return;
|
||||
}
|
||||
echo '<div class="tablenav"><div class="tablenav-pages">';
|
||||
echo paginate_links([
|
||||
'base' => add_query_arg('paged', '%#%'),
|
||||
'format' => '',
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
'total' => $pages,
|
||||
'current' => $page_num,
|
||||
]);
|
||||
echo '</div></div>';
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,11 @@ class PayWithdrawals {
|
||||
$per_page = 20;
|
||||
$svc = \AKSRT\Payment\Withdrawal::get_instance();
|
||||
$stats = $svc->get_stats();
|
||||
$retry_id = isset($_GET['retry_id']) ? max(0, (int) $_GET['retry_id']) : 0;
|
||||
$retry_key = isset($_GET['retry_key']) ? sanitize_text_field((string) wp_unslash($_GET['retry_key'])) : '';
|
||||
if (!wp_is_uuid($retry_key)) {
|
||||
$retry_key = '';
|
||||
}
|
||||
|
||||
echo '<p>' . esc_html(sprintf(
|
||||
/* translators: 1: total 2: pending 3: completed 4: paid */
|
||||
@@ -52,7 +57,7 @@ class PayWithdrawals {
|
||||
(int) ($stats['processing'] ?? 0),
|
||||
(int) ($stats['completed'] ?? 0),
|
||||
(int) ($stats['rejected'] ?? 0),
|
||||
number_format((float) ($stats['total_paid'] ?? 0), 2)
|
||||
(string) ($stats['total_paid'] ?? '0.00')
|
||||
)) . '</p>';
|
||||
|
||||
echo '<form method="get" style="margin:12px 0;">';
|
||||
@@ -106,17 +111,17 @@ class PayWithdrawals {
|
||||
$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(
|
||||
$gross = (string) ($row['gross_amount'] ?? '0.00');
|
||||
$net = (string) ($row['amount'] ?? '0.00');
|
||||
$fee = (int) ($row['fee_rate'] ?? 0);
|
||||
$acct = (string) ($row['display_account'] ?? $row['account_info'] ?? '');
|
||||
$amount_txt = $net;
|
||||
if ($gross !== $net) {
|
||||
$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),
|
||||
$gross,
|
||||
$net,
|
||||
$fee
|
||||
);
|
||||
}
|
||||
@@ -130,15 +135,22 @@ class PayWithdrawals {
|
||||
echo '<td>' . esc_html($status_options[$st] ?? $st) . '</td>';
|
||||
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
|
||||
echo '<td>';
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;flex-wrap:wrap;align-items:center;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_withdrawal" />';
|
||||
echo '<input type="hidden" name="withdrawal_id" value="' . esc_attr((string) $id) . '" />';
|
||||
$next_statuses = $st === 'pending' ? ['processing', 'rejected'] : ($st === 'processing' ? ['completed', 'rejected'] : []);
|
||||
if ($next_statuses === []) {
|
||||
echo '—';
|
||||
echo '</td>';
|
||||
echo '</tr>';
|
||||
continue;
|
||||
}
|
||||
$request_key = $retry_id === $id && $retry_key !== '' ? $retry_key : wp_generate_uuid4();
|
||||
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="display:flex;gap:4px;flex-wrap:wrap;align-items:center;">';
|
||||
echo '<input type="hidden" name="action" value="aksrt_pay_update_withdrawal" />';
|
||||
echo '<input type="hidden" name="withdrawal_id" value="' . esc_attr((string) $id) . '" />';
|
||||
echo '<input type="hidden" name="idempotency_key" value="' . esc_attr($request_key) . '" />';
|
||||
wp_nonce_field('aksrt_pay_update_withdrawal_' . $id);
|
||||
echo '<select name="new_status">';
|
||||
foreach ($status_options as $key => $label) {
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
foreach ($next_statuses as $key) {
|
||||
$label = $status_options[$key];
|
||||
printf(
|
||||
'<option value="%s"%s>%s</option>',
|
||||
esc_attr($key),
|
||||
@@ -180,26 +192,31 @@ class PayWithdrawals {
|
||||
$id = isset($_POST['withdrawal_id']) ? (int) $_POST['withdrawal_id'] : 0;
|
||||
$new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
|
||||
$note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
|
||||
$request_key = isset($_POST['idempotency_key']) ? sanitize_text_field((string) wp_unslash($_POST['idempotency_key'])) : '';
|
||||
check_admin_referer('aksrt_pay_update_withdrawal_' . $id);
|
||||
|
||||
$msg = 'withdraw_failed';
|
||||
if ($id > 0 && $new_status !== '' && class_exists('\AKSRT\Payment\Withdrawal')) {
|
||||
$result = \AKSRT\Payment\Withdrawal::get_instance()->process_status_change($id, $new_status, $note);
|
||||
if ($id > 0 && $new_status !== '' && wp_is_uuid($request_key) && class_exists('\AKSRT\Payment\Withdrawal')) {
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::deterministic('admin-withdraw-transition', [get_current_user_id(), strtolower($request_key)]);
|
||||
$result = \AKSRT\Payment\Withdrawal::get_instance()->process_status_change($id, $new_status, $note, $operation_key);
|
||||
if (!is_wp_error($result) && $result) {
|
||||
$msg = 'withdraw_updated';
|
||||
} elseif (is_wp_error($result) && $result->get_error_code() === 'aksrt_balance') {
|
||||
} elseif (is_wp_error($result) && $result->get_error_code() === 'aksrt_balance_insufficient') {
|
||||
$msg = 'withdraw_refund_failed';
|
||||
}
|
||||
}
|
||||
|
||||
wp_safe_redirect($this->page_url('aksrt_withdraw', [
|
||||
'aksrt_pay_msg' => $msg,
|
||||
]));
|
||||
$redirect_args = ['aksrt_pay_msg' => $msg];
|
||||
if ($msg !== 'withdraw_updated' && wp_is_uuid($request_key)) {
|
||||
$redirect_args['retry_id'] = $id;
|
||||
$redirect_args['retry_key'] = $request_key;
|
||||
}
|
||||
wp_safe_redirect($this->page_url('aksrt_withdraw', $redirect_args));
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Income / rebate
|
||||
* Income
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
||||
+125
-260
@@ -42,17 +42,35 @@ class PaymentController extends ControllerBase {
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_creator_income'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
|
||||
'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20],
|
||||
'status' => ['type' => 'string', 'enum' => ['', 'pending', 'settled']],
|
||||
'user_id' => ['type' => 'integer', 'minimum' => 0, 'default' => 0],
|
||||
'post_id' => ['type' => 'integer', 'minimum' => 0, 'default' => 0],
|
||||
'currency' => ['type' => 'string', 'enum' => ['', 'CNY', 'cny', 'points']],
|
||||
'order_status' => ['type' => 'string', 'sanitize_callback' => 'sanitize_key'],
|
||||
'order_number' => ['type' => 'string', 'maxLength' => 64, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'date_from' => ['type' => 'string', 'format' => 'date'],
|
||||
'date_to' => ['type' => 'string', 'format' => 'date'],
|
||||
],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/creator-income/(?P<id>\d+)/settle', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'settle_creator_income'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => ['id' => ['type' => 'integer', 'minimum' => 1, 'required' => true]],
|
||||
]);
|
||||
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/withdrawals', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_withdrawals'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
|
||||
'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 50, 'default' => 20],
|
||||
'status' => ['type' => 'string', 'enum' => ['', 'pending', 'processing', 'completed', 'rejected']],
|
||||
],
|
||||
]);
|
||||
|
||||
// Withdrawal status update
|
||||
@@ -60,6 +78,11 @@ class PaymentController extends ControllerBase {
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'update_withdrawal_status'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'id' => ['type' => 'integer', 'minimum' => 1, 'required' => true],
|
||||
'status' => ['type' => 'string', 'required' => true, 'enum' => ['processing', 'completed', 'rejected']],
|
||||
'note' => ['type' => 'string', 'default' => '', 'maxLength' => 1000, 'sanitize_callback' => 'sanitize_textarea_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// ── Charge Card ──
|
||||
@@ -69,11 +92,20 @@ class PaymentController extends ControllerBase {
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_charge_cards'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
|
||||
'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20],
|
||||
'status' => ['type' => 'string', 'enum' => ['', 'active', 'used', 'expired', 'disabled']],
|
||||
'keyword' => ['type' => 'string', 'maxLength' => 191, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'date_from' => ['type' => 'string', 'format' => 'date'],
|
||||
'date_to' => ['type' => 'string', 'format' => 'date'],
|
||||
],
|
||||
]);
|
||||
register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/(?P<id>\d+)', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'get_charge_card_detail'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => ['id' => ['type' => 'integer', 'minimum' => 1, 'required' => true]],
|
||||
]);
|
||||
|
||||
// Charge card create
|
||||
@@ -81,6 +113,16 @@ class PaymentController extends ControllerBase {
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'create_charge_card'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'type' => ['type' => 'string', 'required' => true, 'enum' => ['balance', 'vip', 'credits']],
|
||||
'password' => ['type' => 'string', 'required' => true, 'minLength' => 1, 'maxLength' => 80, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'amount' => ['type' => 'string', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
|
||||
'points' => ['type' => 'integer', 'minimum' => 1],
|
||||
'level' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 2],
|
||||
'days' => ['type' => 'integer', 'minimum' => 0],
|
||||
'expires_at' => ['type' => 'string', 'maxLength' => 32, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'note' => ['type' => 'string', 'maxLength' => 1000, 'sanitize_callback' => 'sanitize_textarea_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Charge card batch create
|
||||
@@ -88,6 +130,18 @@ class PaymentController extends ControllerBase {
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'batch_create_charge_cards'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'type' => ['type' => 'string', 'required' => true, 'enum' => ['balance', 'vip', 'credits']],
|
||||
'count' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'required' => true],
|
||||
'amount' => ['type' => 'string', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
|
||||
'points' => ['type' => 'integer', 'minimum' => 1],
|
||||
'level' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 2],
|
||||
'days' => ['type' => 'integer', 'minimum' => 0],
|
||||
'prefix' => ['type' => 'string', 'maxLength' => 20, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'length' => ['type' => 'integer', 'minimum' => 8, 'maximum' => 32, 'default' => 16],
|
||||
'expires_at' => ['type' => 'string', 'maxLength' => 32, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'note' => ['type' => 'string', 'maxLength' => 1000, 'sanitize_callback' => 'sanitize_textarea_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Charge card disable
|
||||
@@ -95,6 +149,7 @@ class PaymentController extends ControllerBase {
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'disable_charge_card'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => ['id' => ['type' => 'integer', 'minimum' => 1, 'required' => true]],
|
||||
]);
|
||||
|
||||
}
|
||||
@@ -129,14 +184,14 @@ class PaymentController extends ControllerBase {
|
||||
*/
|
||||
public function get_revenue(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$revenue_stats = [
|
||||
'cash' => ['total' => 0, 'pending' => 0, 'settled' => 0, 'count' => 0],
|
||||
'cash' => ['total' => '0.00', 'pending' => '0.00', 'settled' => '0.00', 'count' => 0],
|
||||
'points' => ['total' => 0, 'pending' => 0, 'settled' => 0, 'count' => 0],
|
||||
];
|
||||
if (class_exists('\AKSRT\Payment\Revenue')) {
|
||||
$revenue_stats = \AKSRT\Payment\Revenue::get_instance()->get_stats();
|
||||
}
|
||||
|
||||
$withdrawal_stats = ['total' => 0, 'pending' => 0, 'processing' => 0, 'completed' => 0, 'rejected' => 0, 'total_paid' => 0];
|
||||
$withdrawal_stats = ['total' => 0, 'pending' => 0, 'processing' => 0, 'completed' => 0, 'rejected' => 0, 'total_paid' => '0.00'];
|
||||
if (class_exists('\AKSRT\Payment\Withdrawal')) {
|
||||
$withdrawal_stats = \AKSRT\Payment\Withdrawal::get_instance()->get_stats();
|
||||
}
|
||||
@@ -201,9 +256,19 @@ class PaymentController extends ControllerBase {
|
||||
$series = ['cash' => [], 'points' => []];
|
||||
foreach ((array) $results as $row) {
|
||||
$key = (string) ($row->currency ?? '') === 'points' ? 'points' : 'cash';
|
||||
$series[$key][] = [
|
||||
$amount = (string) ($row->total_amount ?? '0');
|
||||
if ($key === 'points') {
|
||||
$amount = preg_match('/^(0|[1-9]\d*)(?:\.0+)?$/', $amount, $matches) ? (int) $matches[1] : 0;
|
||||
} else {
|
||||
try {
|
||||
$amount = \AKSRT\Ledger\AssetAmount::balance($amount)->decimal();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$amount = '0.00';
|
||||
}
|
||||
}
|
||||
$series[$key][] = [
|
||||
'date' => (string) $row->period,
|
||||
'amount' => (float) $row->total_amount,
|
||||
'amount' => $amount,
|
||||
'count' => (int) $row->order_count,
|
||||
];
|
||||
}
|
||||
@@ -245,10 +310,29 @@ class PaymentController extends ControllerBase {
|
||||
return $this->error(__('创作分成模块未启用', 'aksrt'), 500);
|
||||
}
|
||||
$id = (int) $request->get_param('id');
|
||||
if ($id <= 0 || !\AKSRT\Payment\Revenue::get_instance()->settle($id, get_current_user_id())) {
|
||||
return $this->error(__('结算失败', 'aksrt'));
|
||||
}
|
||||
return $this->success(['message' => __('创作分成已结算', 'aksrt')]);
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::from_request($request, 'admin-revenue-settle', get_current_user_id());
|
||||
if (is_wp_error($operation_key)) {
|
||||
return $this->error($operation_key->get_error_message(), (int) ($operation_key->get_error_data()['status'] ?? 400));
|
||||
}
|
||||
$result = \AKSRT\Payment\Revenue::get_instance()->settle_result($id, get_current_user_id(), $operation_key);
|
||||
if (is_wp_error($result)) {
|
||||
$data = $result->get_error_data();
|
||||
return $this->error($result->get_error_message(), is_array($data) ? (int) ($data['status'] ?? 400) : 400);
|
||||
}
|
||||
$entry = (array) ($result->entries[0] ?? []);
|
||||
$asset = (string) ($entry['asset'] ?? \AKSRT\Ledger\AssetType::BALANCE->value);
|
||||
$author_id = (int) ($entry['user_id'] ?? 0);
|
||||
$balance_minor = $result->balance($author_id, $asset);
|
||||
$balance = $asset === \AKSRT\Ledger\AssetType::BALANCE->value
|
||||
? \AKSRT\Ledger\AssetAmount::decimal_from_minor($balance_minor)
|
||||
: $balance_minor;
|
||||
return $this->success([
|
||||
'message' => __('创作分成已结算', 'aksrt'),
|
||||
'transaction_id' => $result->transaction_id,
|
||||
'replayed' => $result->replayed,
|
||||
'asset' => $asset,
|
||||
'balance' => $balance,
|
||||
]);
|
||||
}
|
||||
|
||||
public function get_withdrawals(\WP_REST_Request $request): \WP_REST_Response {
|
||||
@@ -293,7 +377,7 @@ class PaymentController extends ControllerBase {
|
||||
$new_status = sanitize_key((string) ($params['status'] ?? ''));
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
|
||||
$valid_statuses = ['pending', 'processing', 'completed', 'rejected'];
|
||||
$valid_statuses = ['processing', 'completed', 'rejected'];
|
||||
if (!in_array($new_status, $valid_statuses, true)) {
|
||||
return $this->error('无效的提现状态');
|
||||
}
|
||||
@@ -302,13 +386,18 @@ class PaymentController extends ControllerBase {
|
||||
return $this->error('无效的提现 ID');
|
||||
}
|
||||
|
||||
$result = \AKSRT\Payment\Withdrawal::get_instance()->process_status_change($id, $new_status, $note);
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::from_request($request, 'admin-withdraw-transition', get_current_user_id());
|
||||
if (is_wp_error($operation_key)) {
|
||||
return $this->error($operation_key->get_error_message(), (int) ($operation_key->get_error_data()['status'] ?? 400));
|
||||
}
|
||||
$result = \AKSRT\Payment\Withdrawal::get_instance()->process_status_change($id, $new_status, $note, $operation_key);
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error($result->get_error_message());
|
||||
$data = $result->get_error_data();
|
||||
return $this->error($result->get_error_message(), is_array($data) ? (int) ($data['status'] ?? 400) : 400);
|
||||
}
|
||||
if ($result) {
|
||||
return $this->success(['message' => __('提现状态已更新', 'aksrt')]);
|
||||
if (is_array($result)) {
|
||||
return $this->success(array_merge(['message' => __('提现状态已更新', 'aksrt')], $result));
|
||||
}
|
||||
return $this->error(__('更新失败', 'aksrt'));
|
||||
}
|
||||
@@ -359,7 +448,6 @@ class PaymentController extends ControllerBase {
|
||||
foreach ((array) $rows as $row) {
|
||||
$item = $this->build_charge_card_admin_item($row);
|
||||
if ($keyword !== '' && mb_stripos(implode(' ', array_filter([
|
||||
(string) ($item['code'] ?? ''),
|
||||
(string) ($item['password'] ?? ''),
|
||||
(string) ($item['card'] ?? ''),
|
||||
(string) ($item['type_label'] ?? ''),
|
||||
@@ -410,7 +498,7 @@ class PaymentController extends ControllerBase {
|
||||
if (!in_array($type, ['balance', 'vip', 'credits'], true)) {
|
||||
$type = 'balance';
|
||||
}
|
||||
$password = sanitize_text_field((string) ($params['password'] ?? $params['code'] ?? ''));
|
||||
$password = sanitize_text_field((string) ($params['password'] ?? ''));
|
||||
if ($password === '') {
|
||||
return $this->error('卡密不能为空');
|
||||
}
|
||||
@@ -449,15 +537,22 @@ class PaymentController extends ControllerBase {
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
$expires = sanitize_text_field((string) ($params['expires_at'] ?? ''));
|
||||
|
||||
$raw_amount = $params['amount'] ?? '';
|
||||
$meta_fields = [
|
||||
'amount' => (float) ($params['amount'] ?? 0),
|
||||
'amount' => is_string($raw_amount) ? $raw_amount : '',
|
||||
'level' => (int) ($params['level'] ?? 1),
|
||||
'days' => (int) ($params['days'] ?? $params['vip_days'] ?? 30),
|
||||
'points' => (int) ($params['points'] ?? $params['credits'] ?? 0),
|
||||
'days' => (int) ($params['days'] ?? 30),
|
||||
'points' => (int) ($params['points'] ?? 0),
|
||||
];
|
||||
|
||||
if ($type === 'balance' && $meta_fields['amount'] <= 0) {
|
||||
return $this->error('余额卡金额必须大于 0');
|
||||
if ($type === 'balance') {
|
||||
try {
|
||||
$card_amount = \AKSRT\Ledger\AssetAmount::balance($meta_fields['amount']);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error('余额卡金额格式无效');
|
||||
}
|
||||
if ($card_amount->minor() <= 0) return $this->error('余额卡金额必须大于 0');
|
||||
$meta_fields['amount'] = $card_amount->decimal();
|
||||
}
|
||||
if ($type === 'credits' && $meta_fields['points'] <= 0) {
|
||||
return $this->error('积分卡积分必须大于 0');
|
||||
@@ -522,171 +617,6 @@ class PaymentController extends ControllerBase {
|
||||
]);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Rebate
|
||||
* ----------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* GET /payment/rebate/log — 返佣记录列表
|
||||
*/
|
||||
public function get_rebate_list(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\Rebate')) {
|
||||
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]);
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$table = \AKSRT\Core\DBMigrator::table('rebate_log');
|
||||
$page = max(1, (int) ($request->get_param('page') ?: 1));
|
||||
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
|
||||
$status = sanitize_key((string) $request->get_param('status'));
|
||||
$type = sanitize_key((string) $request->get_param('type'));
|
||||
$keyword = trim(sanitize_text_field((string) $request->get_param('keyword')));
|
||||
$user_id = (int) $request->get_param('user_id');
|
||||
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
|
||||
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($status !== '') {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
if ($type !== '') {
|
||||
$where[] = 'type = %s';
|
||||
$params[] = $type;
|
||||
}
|
||||
if ($user_id > 0) {
|
||||
$where[] = 'user_id = %d';
|
||||
$params[] = $user_id;
|
||||
}
|
||||
if ($date_from !== '') {
|
||||
$where[] = 'created_at >= %s';
|
||||
$params[] = $date_from . ' 00:00:00';
|
||||
}
|
||||
if ($date_to !== '') {
|
||||
$where[] = 'created_at <= %s';
|
||||
$params[] = $date_to . ' 23:59:59';
|
||||
}
|
||||
|
||||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
$sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500";
|
||||
$rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A);
|
||||
|
||||
$items = [];
|
||||
foreach ((array) $rows as $row) {
|
||||
$item = $this->build_rebate_admin_item($row);
|
||||
if ($keyword !== '' && mb_stripos(implode(' ', array_filter([
|
||||
(string) ($item['order_number'] ?? ''),
|
||||
(string) ($item['note'] ?? ''),
|
||||
(string) ($item['user']['display_name'] ?? ''),
|
||||
(string) ($item['source_user']['display_name'] ?? ''),
|
||||
])), $keyword) === false) {
|
||||
continue;
|
||||
}
|
||||
$items[] = $item;
|
||||
}
|
||||
|
||||
$total = count($items);
|
||||
$offset = ($page - 1) * $per_page;
|
||||
$stats = \AKSRT\Payment\Rebate::get_instance()->get_stats();
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'items' => array_values(array_slice($items, $offset, $per_page)),
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $per_page,
|
||||
'max_pages' => max(1, (int) ceil(max(1, $total) / $per_page)),
|
||||
'stats' => $stats,
|
||||
'options' => [
|
||||
'statuses' => [
|
||||
['label' => __('全部状态', 'aksrt'), 'value' => ''],
|
||||
['label' => __('待结算', 'aksrt'), 'value' => 'pending'],
|
||||
['label' => __('已结算', 'aksrt'), 'value' => 'settled'],
|
||||
['label' => __('已取消', 'aksrt'), 'value' => 'cancelled'],
|
||||
],
|
||||
'types' => [
|
||||
['label' => __('全部类型', 'aksrt'), 'value' => ''],
|
||||
['label' => __('订单返佣', 'aksrt'), 'value' => 'order'],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/rebate/{id}/settle — 结算返佣
|
||||
*/
|
||||
public function settle_rebate(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\Rebate')) {
|
||||
return $this->error('返佣系统未启用', 500);
|
||||
}
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
if ($id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$success = \AKSRT\Payment\Rebate::get_instance()->settle($id);
|
||||
|
||||
if ($success) {
|
||||
$this->append_rebate_process_log($id, 'settled', $note);
|
||||
return $this->success(['message' => __('返佣已结算', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('结算失败', 'aksrt'));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /payment/rebate/{id}/cancel — 取消返佣
|
||||
*/
|
||||
public function cancel_rebate(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Payment\Rebate')) {
|
||||
return $this->error('返佣系统未启用', 500);
|
||||
}
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
|
||||
if ($id <= 0) {
|
||||
return $this->error('参数无效');
|
||||
}
|
||||
|
||||
$success = \AKSRT\Payment\Rebate::get_instance()->cancel($id);
|
||||
|
||||
if ($success) {
|
||||
$this->append_rebate_process_log($id, 'cancelled', $note);
|
||||
return $this->success(['message' => __('返佣已取消', 'aksrt')]);
|
||||
}
|
||||
return $this->error(__('操作失败', 'aksrt'));
|
||||
}
|
||||
|
||||
public function get_rebate_detail(\WP_REST_Request $request): \WP_REST_Response {
|
||||
global $wpdb;
|
||||
|
||||
$id = (int) $request->get_param('id');
|
||||
$table = \AKSRT\Core\DBMigrator::table('rebate_log');
|
||||
|
||||
if ($id <= 0 || !$table) {
|
||||
return $this->error(__('返佣记录不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
$row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A);
|
||||
if (!$row) {
|
||||
return $this->error(__('返佣记录不存在', 'aksrt'), 404);
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'data' => [
|
||||
'record' => $this->build_rebate_admin_item($row),
|
||||
'history' => $this->get_rebate_process_log($id),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function build_charge_card_admin_item(array $row): array {
|
||||
$used_by = (int) ($row['used_by'] ?? 0);
|
||||
$user = $used_by > 0 ? get_userdata($used_by) : null;
|
||||
@@ -716,18 +646,17 @@ class PaymentController extends ControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
$password = (string) ($row['password'] ?? $row['code'] ?? '');
|
||||
$password = (string) ($row['password'] ?? '');
|
||||
$card_no = (string) ($row['card'] ?? '');
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'code' => $password,
|
||||
'password' => $password,
|
||||
'card' => $card_no,
|
||||
'type' => $type,
|
||||
'type_label' => $typeMap[$type] ?? $type,
|
||||
'amount' => (float) ($row['amount'] ?? 0),
|
||||
'balance' => (float) ($row['balance'] ?? 0),
|
||||
'amount' => $this->cash_decimal((string) ($meta['amount'] ?? $row['amount'] ?? '0')),
|
||||
'balance' => $this->cash_decimal((string) ($row['balance'] ?? '0')),
|
||||
'meta' => $meta,
|
||||
'level' => (int) ($meta['level'] ?? 0),
|
||||
'days' => (int) ($meta['days'] ?? 0),
|
||||
@@ -748,76 +677,12 @@ class PaymentController extends ControllerBase {
|
||||
];
|
||||
}
|
||||
|
||||
private function build_rebate_admin_item(array $row): array {
|
||||
$user_id = (int) ($row['user_id'] ?? 0);
|
||||
$source_user_id = (int) ($row['source_user_id'] ?? 0);
|
||||
$user = $user_id > 0 ? get_userdata($user_id) : null;
|
||||
$source_user = $source_user_id > 0 ? get_userdata($source_user_id) : null;
|
||||
$status = sanitize_key((string) ($row['status'] ?? 'pending'));
|
||||
$type = sanitize_key((string) ($row['type'] ?? 'order'));
|
||||
private function cash_decimal(string $value): string {
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance($value)->decimal();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return '0.00';
|
||||
}
|
||||
}
|
||||
|
||||
$statusMap = [
|
||||
'pending' => __('待结算', 'aksrt'),
|
||||
'settled' => __('已结算', 'aksrt'),
|
||||
'cancelled' => __('已取消', 'aksrt'),
|
||||
];
|
||||
$typeMap = [
|
||||
'order' => __('订单返佣', 'aksrt'),
|
||||
];
|
||||
|
||||
return [
|
||||
'id' => (int) ($row['id'] ?? 0),
|
||||
'order_number' => (string) ($row['order_number'] ?? ''),
|
||||
'amount' => (float) ($row['amount'] ?? 0),
|
||||
'status' => $status,
|
||||
'status_label' => $statusMap[$status] ?? $status,
|
||||
'type' => $type,
|
||||
'type_label' => $typeMap[$type] ?? $type,
|
||||
'created_at' => (string) ($row['created_at'] ?? ''),
|
||||
'settled_at' => (string) ($row['settled_at'] ?? ''),
|
||||
'note' => (string) ($row['note'] ?? ''),
|
||||
'history_count' => count($this->get_rebate_process_log((int) ($row['id'] ?? 0))),
|
||||
'user' => $user instanceof \WP_User ? [
|
||||
'id' => (int) $user->ID,
|
||||
'display_name' => $user->display_name,
|
||||
'email' => $user->user_email,
|
||||
] : null,
|
||||
'source_user' => $source_user instanceof \WP_User ? [
|
||||
'id' => (int) $source_user->ID,
|
||||
'display_name' => $source_user->display_name,
|
||||
'email' => $source_user->user_email,
|
||||
] : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function append_rebate_process_log(int $rebate_id, string $event, string $note = ''): void {
|
||||
$log = get_option('aksrt_rebate_process_log', []);
|
||||
if (!is_array($log)) {
|
||||
$log = [];
|
||||
}
|
||||
|
||||
$rebate_id = (int) $rebate_id;
|
||||
if (!isset($log[$rebate_id]) || !is_array($log[$rebate_id])) {
|
||||
$log[$rebate_id] = [];
|
||||
}
|
||||
|
||||
$log[$rebate_id][] = [
|
||||
'event' => sanitize_key($event),
|
||||
'note' => sanitize_textarea_field($note),
|
||||
'operator' => get_current_user_id(),
|
||||
'operator_name' => ($user = wp_get_current_user()) ? $user->display_name : '',
|
||||
'time' => current_time('mysql'),
|
||||
];
|
||||
|
||||
update_option('aksrt_rebate_process_log', $log, false);
|
||||
}
|
||||
|
||||
private function get_rebate_process_log(int $rebate_id): array {
|
||||
$log = get_option('aksrt_rebate_process_log', []);
|
||||
if (!is_array($log) || !isset($log[$rebate_id]) || !is_array($log[$rebate_id])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values($log[$rebate_id]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ class SchemaPayment {
|
||||
['key' => 'basic', 'title' => '充值开关', 'fields' => [
|
||||
['key' => 'pay_balance_s', 'label' => '启用余额功能', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_balance_desc', 'label' => '充值说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
['key' => 'pay_auto_to_balance', 'label' => '自动到账到余额', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'packages', 'title' => '充值套餐', 'fields' => [
|
||||
['key' => 'pay_balance_product', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|入门包\n98|108|推荐包", 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
@@ -105,7 +104,7 @@ class SchemaPayment {
|
||||
['label' => '优惠立减', 'value' => 'reduce'],
|
||||
]],
|
||||
['key' => 'vip_renew_discount', 'label' => '续费折扣(折,如 8=八折)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount']],
|
||||
['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'number', 'default' => 20, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce']],
|
||||
['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '20.00', 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce']],
|
||||
]],
|
||||
['key' => 'upgrade', 'title' => '升级设置', 'fields' => [
|
||||
['key' => 'vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true],
|
||||
@@ -297,7 +296,7 @@ class SchemaPayment {
|
||||
['key' => 'redeem', 'title' => '兑换设置', 'fields' => [
|
||||
['key' => 'card_pass_max_redeem', 'label' => '单卡最大兑换次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_expiry', 'label' => '有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_amount', 'label' => '面额(元)', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 9999, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_amount', 'label' => '面额(元)', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '10.00', 'min' => 1, 'max' => 9999, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_vip_days', 'label' => 'VIP天数', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 3650, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
['key' => 'card_pass_credits', 'label' => '赠送积分', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]],
|
||||
]],
|
||||
@@ -315,10 +314,10 @@ class SchemaPayment {
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '提现设置', 'fields' => [
|
||||
['key' => 'affiliate_min_withdrawal', 'label' => '最低提现金额', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 99999],
|
||||
['key' => 'affiliate_min_withdrawal', 'label' => '最低提现金额', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '50.00', 'desc' => '仅接受最多两位小数的十进制字符串。'],
|
||||
['key' => 'affiliate_withdrawal_fee', 'label' => '提现手续费(%)', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 100],
|
||||
['key' => 'affiliate_daily_withdrawal_count', 'label' => '每日可提现次数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 99999],
|
||||
['key' => 'affiliate_daily_withdrawal_limit', 'label' => '每日提现上限', 'type' => 'number', 'default' => 500, 'min' => 0, 'max' => 999999],
|
||||
['key' => 'affiliate_daily_withdrawal_limit', 'label' => '每日提现上限', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '500.00', 'desc' => '仅接受最多两位小数的十进制字符串。'],
|
||||
['key' => 'affiliate_withdrawal_reset_time', 'label' => '重置时间', 'type' => 'input', 'default' => '00:00', 'help' => '按站点 WordPress 时区重置,格式 HH:MM。'],
|
||||
['key' => 'affiliate_payout_channels', 'label' => '可用收款渠道', 'type' => 'checkbox', 'options' => [['value' => 'alipay', 'label' => '支付宝收款码'], ['value' => 'wechat', 'label' => '微信收款码']], 'default' => ['alipay', 'wechat']],
|
||||
]],
|
||||
|
||||
@@ -114,8 +114,8 @@ class SchemaPostList {
|
||||
['key' => 'post_rewards_text', 'label' => '打赏按钮文案', 'type' => 'input', 'default' => '赞赏'],
|
||||
['key' => 'post_rewards_amounts', 'label' => '快捷金额', 'type' => 'input', 'default' => '5,10,20,50,100', 'description' => '英文逗号分隔,赞赏直接进入作者站内余额'],
|
||||
['key' => 'post_rewards_custom', 'label' => '允许自定义金额', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_rewards_min', 'label' => '最低金额', 'type' => 'number', 'default' => 1, 'min' => 0.01, 'max' => 1000, 'step' => 0.01],
|
||||
['key' => 'post_rewards_max', 'label' => '最高金额', 'type' => 'number', 'default' => 1000, 'min' => 1, 'max' => 100000, 'step' => 0.01],
|
||||
['key' => 'post_rewards_min', 'label' => '最低金额', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '1.00', 'min' => 0.01, 'max' => 1000, 'step' => 0.01],
|
||||
['key' => 'post_rewards_max', 'label' => '最高金额', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '1000.00', 'min' => 1, 'max' => 100000, 'step' => 0.01],
|
||||
['key' => 'post_button_toptext', 'label' => '按钮上方文案', 'type' => 'input', 'default' => '喜欢就支持一下吧'],
|
||||
]],
|
||||
['key' => 'content', 'title' => '内容处理', 'fields' => [
|
||||
@@ -346,8 +346,8 @@ class SchemaPostList {
|
||||
]],
|
||||
['key' => 'paid', 'title' => '付费投稿权限', 'fields' => [
|
||||
['key' => 'post_article_pay_free_s', 'label' => '允许设置免费', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_article_pay_price_min', 'label' => '最低价格', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'help' => '投稿可设置的最低付费金额'],
|
||||
['key' => 'post_article_pay_price_max', 'label' => '最高价格', 'type' => 'number', 'default' => 100, 'min' => 0, 'max' => 9999, 'help' => '投稿可设置的最高付费金额'],
|
||||
['key' => 'post_article_pay_price_min', 'label' => '最低价格', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '0.00', 'min' => 0, 'max' => 9999, 'help' => '投稿可设置的最低付费金额'],
|
||||
['key' => 'post_article_pay_price_max', 'label' => '最高价格', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '100.00', 'min' => 0, 'max' => 9999, 'help' => '投稿可设置的最高付费金额'],
|
||||
['key' => 'post_article_pay_vip_price_s', 'label' => '允许设置会员价', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_article_pay_vip_1_discount', 'label' => 'VIP1 折扣(%)', 'type' => 'number', 'default' => 80, 'min' => 0, 'max' => 100, 'help' => 'VIP1 会员折扣百分比,80 即 8 折'],
|
||||
['key' => 'post_article_pay_vip_2_discount', 'label' => 'VIP2 折扣(%)', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 100, 'help' => 'VIP2 会员折扣百分比,50 即 5 折'],
|
||||
|
||||
@@ -514,9 +514,9 @@ class SchemaUsers {
|
||||
['key' => 'points_deduction_max', 'label' => '最大抵扣比例(%)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 100, 'help' => '预留:最多可抵订单金额百分比。当前未参与下单。'],
|
||||
]],
|
||||
['key' => 'card_pass', 'title' => '积分卡密兑换', 'fields' => [
|
||||
['key' => 'points_pass_exchange_s', 'label' => '启用积分卡密兑换', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'points_pass_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true]],
|
||||
['key' => 'points_pass_exchange_only_password', 'label' => '仅密码兑换', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'points_pass_exchange_s', 'value' => true]],
|
||||
['key' => 'asset_points_card_enabled', 'label' => '启用积分卡密兑换', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'asset_points_card_description', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'asset_points_card_enabled', 'value' => true]],
|
||||
['key' => 'asset_points_card_password_only', 'label' => '仅密码兑换', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'asset_points_card_enabled', 'value' => true]],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'points_show_balance', 'label' => '导航栏显示余额', 'type' => 'switch', 'default' => true],
|
||||
@@ -591,9 +591,9 @@ class SchemaUsers {
|
||||
['key' => 'apply', 'title' => '申请设置', 'fields' => [
|
||||
['key' => 'user_verify_allow_apply', 'label' => '允许用户前台申请', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_verify_disable_desc', 'label' => '停用申请说明', 'type' => 'textarea', 'default' => "由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员"],
|
||||
['key' => 'user_verify_personal_fee', 'label' => '个人认证每次扣余额(元)', 'type' => 'number', 'sanitize_type' => 'float', 'default' => 0, 'min' => 0, 'step' => 0.01],
|
||||
['key' => 'user_verify_personal_fee', 'label' => '个人认证每次扣余额(元)', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '0.00', 'desc' => '仅接受最多两位小数的十进制字符串。'],
|
||||
['key' => 'user_verify_personal_free_applications', 'label' => '个人认证赠送初始次数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'desc' => '每位用户前 N 次个人认证申请免费,之后按单次费用扣余额。'],
|
||||
['key' => 'user_verify_enterprise_fee', 'label' => '企业认证每次扣余额(元)', 'type' => 'number', 'sanitize_type' => 'float', 'default' => 0, 'min' => 0, 'step' => 0.01],
|
||||
['key' => 'user_verify_enterprise_fee', 'label' => '企业认证每次扣余额(元)', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '0.00', 'desc' => '仅接受最多两位小数的十进制字符串。'],
|
||||
['key' => 'user_verify_enterprise_free_applications', 'label' => '企业认证赠送初始次数', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'desc' => '每位用户前 N 次企业认证申请免费,之后按单次费用扣余额。'],
|
||||
['key' => 'user_verify_desc', 'label' => '申请介绍', 'type' => 'textarea', 'default' => '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。'],
|
||||
]],
|
||||
|
||||
@@ -170,6 +170,17 @@ class SchemaProvider {
|
||||
$errors[] = sprintf('%s is required', $field['label'] ?? $key);
|
||||
continue;
|
||||
}
|
||||
if (($field['sanitize_type'] ?? '') === 'money_decimal') {
|
||||
try {
|
||||
if (!is_string($value)) {
|
||||
throw new \InvalidArgumentException('money_decimal_must_be_string');
|
||||
}
|
||||
\AKSRT\Ledger\AssetAmount::balance(trim($value));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$errors[] = sprintf('%s must be a decimal string with at most two fractional digits', $field['label'] ?? $key);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$type = $field['type'] ?? 'input';
|
||||
if (($type === 'number' || $type === 'slider') && !is_numeric($value)) {
|
||||
@@ -259,13 +270,20 @@ class SchemaProvider {
|
||||
$stored = get_option('aksrt_settings', []);
|
||||
$merged = is_array($stored) ? array_merge($stored, $data) : $data;
|
||||
$count = $merged['affiliate_daily_withdrawal_count'] ?? 3;
|
||||
$min = (float) ($merged['affiliate_min_withdrawal'] ?? 50);
|
||||
$limit = (float) ($merged['affiliate_daily_withdrawal_limit'] ?? 500);
|
||||
$min_raw = $merged['affiliate_min_withdrawal'] ?? '50.00';
|
||||
$limit_raw = $merged['affiliate_daily_withdrawal_limit'] ?? '500.00';
|
||||
$time = (string) ($merged['affiliate_withdrawal_reset_time'] ?? '00:00');
|
||||
if (filter_var($count, FILTER_VALIDATE_INT) === false || (int) $count < 0) {
|
||||
$errors[] = __('每日可提现次数必须是非负整数', 'aksrt');
|
||||
}
|
||||
if ($limit < $min) {
|
||||
try {
|
||||
$min_minor = \AKSRT\Ledger\AssetAmount::balance(is_string($min_raw) ? $min_raw : '')->minor();
|
||||
$limit_minor = \AKSRT\Ledger\AssetAmount::balance(is_string($limit_raw) ? $limit_raw : '')->minor();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$min_minor = 0;
|
||||
$limit_minor = 0;
|
||||
}
|
||||
if ($limit_minor < $min_minor) {
|
||||
$errors[] = __('每日提现上限不得低于最低提现金额', 'aksrt');
|
||||
}
|
||||
if (preg_match('/^(?:[01]\\d|2[0-3]):[0-5]\\d$/', $time) !== 1) {
|
||||
@@ -463,6 +481,15 @@ class SchemaProvider {
|
||||
return !empty($value);
|
||||
case 'float':
|
||||
return (float) $value;
|
||||
case 'money_decimal':
|
||||
if (!is_string($value)) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return \AKSRT\Ledger\AssetAmount::balance(trim($value))->decimal();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return '';
|
||||
}
|
||||
case 'array':
|
||||
// 深层字符串净化,保留嵌套服务商配置(如 geetest.captcha_id)
|
||||
if (!is_array($value)) {
|
||||
|
||||
+139
-33
@@ -108,39 +108,60 @@ class UserController extends ControllerBase {
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Credits settings (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/credits/settings', [
|
||||
register_rest_route(self::API_NAMESPACE, '/assets/settings', [
|
||||
'methods' => ['GET', 'POST'],
|
||||
'callback' => [$this, 'rest_handle_credit_settings'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Credits — current user balance (frontend)
|
||||
register_rest_route(self::API_NAMESPACE, '/credits/me', [
|
||||
register_rest_route(self::API_NAMESPACE, '/assets/me', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_get_credits_me'],
|
||||
'permission_callback' => function () { return is_user_logged_in(); },
|
||||
'permission_callback' => [$this, 'check_asset_self_permission'],
|
||||
]);
|
||||
|
||||
// Credits — user list (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/credits/users', [
|
||||
register_rest_route(self::API_NAMESPACE, '/assets/accounts', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_list_credit_users'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
]);
|
||||
|
||||
// Credits — adjust user balance (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/credits/users/(?P<id>\d+)/adjust', [
|
||||
register_rest_route(self::API_NAMESPACE, '/assets/accounts/(?P<id>\d+)/adjust', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_adjust_user_credits'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'id' => ['type' => 'integer', 'minimum' => 1, 'required' => true],
|
||||
'asset' => ['type' => 'string', 'enum' => ['points', 'balance'], 'required' => true],
|
||||
'delta' => [
|
||||
'required' => true,
|
||||
'oneOf' => [
|
||||
['type' => 'integer'],
|
||||
['type' => 'string', 'pattern' => '^-?(?:0|[1-9]\\d*)(?:\\.\\d{1,2})?$'],
|
||||
],
|
||||
],
|
||||
'remark' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Credits — transaction log (admin)
|
||||
register_rest_route(self::API_NAMESPACE, '/credits/log', [
|
||||
register_rest_route(self::API_NAMESPACE, '/assets/transactions', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_get_credits_log'],
|
||||
'permission_callback' => [$this, 'check_permission'],
|
||||
'args' => [
|
||||
'transaction_id' => ['type' => 'integer', 'minimum' => 1],
|
||||
'operation_key' => ['type' => 'string', 'maxLength' => 191, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'user_id' => ['type' => 'integer', 'minimum' => 1],
|
||||
'actor_user_id' => ['type' => 'integer', 'minimum' => 1],
|
||||
'asset' => ['type' => 'string', 'enum' => ['points', 'balance']],
|
||||
'reason' => ['type' => 'string', 'maxLength' => 64, 'sanitize_callback' => 'sanitize_key'],
|
||||
'ref_type' => ['type' => 'string', 'maxLength' => 32, 'sanitize_callback' => 'sanitize_key'],
|
||||
'ref_key' => ['type' => 'string', 'maxLength' => 191, 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1],
|
||||
'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20],
|
||||
'date_from' => ['type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
'date_to' => ['type' => 'string', 'sanitize_callback' => 'sanitize_text_field'],
|
||||
],
|
||||
]);
|
||||
|
||||
// Report ticket list (admin)
|
||||
@@ -639,60 +660,145 @@ class UserController extends ControllerBase {
|
||||
$user_id = get_current_user_id();
|
||||
return $this->success(['data' => [
|
||||
'user_id' => $user_id,
|
||||
'balance' => class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::balance($user_id) : 0,
|
||||
'points' => \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS),
|
||||
'balance' => \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id),
|
||||
]]);
|
||||
}
|
||||
|
||||
public function check_asset_self_permission(\WP_REST_Request $request) {
|
||||
if (!is_user_logged_in()) {
|
||||
return new \WP_Error('aksrt_auth_required', __('请先登录', 'aksrt'), ['status' => 401]);
|
||||
}
|
||||
$nonce = (string) $request->get_header('X-WP-Nonce');
|
||||
if ($nonce === '' || !wp_verify_nonce($nonce, 'wp_rest')) {
|
||||
return new \WP_Error('aksrt_rest_nonce', __('安全验证失败,请刷新页面后重试', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rest_list_credit_users(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$args = [
|
||||
'search' => (string) $request->get_param('search'),
|
||||
'page' => (int) $request->get_param('page'),
|
||||
'per_page' => (int) $request->get_param('per_page'),
|
||||
];
|
||||
$data = class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::list_users($args) : ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20];
|
||||
$asset = sanitize_key((string) $request->get_param('asset'));
|
||||
$asset = in_array($asset, ['points', 'balance'], true) ? $asset : 'points';
|
||||
$data = \AKSRT\Ledger\LedgerQuery::get_instance()->list_accounts($asset, $args);
|
||||
foreach ($data['items'] as &$item) {
|
||||
$item['asset'] = $asset;
|
||||
$item['balance'] = $asset === 'balance'
|
||||
? \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, (int) $item['balance_minor'])->decimal()
|
||||
: (int) $item['balance_minor'];
|
||||
unset($item['balance_minor']);
|
||||
}
|
||||
unset($item);
|
||||
return $this->success(['data' => $data]);
|
||||
}
|
||||
|
||||
public function rest_adjust_user_credits(\WP_REST_Request $request): \WP_REST_Response {
|
||||
if (!class_exists('\AKSRT\Credits\Credits')) {
|
||||
return $this->error(__('积分系统不可用', 'aksrt'), 500);
|
||||
}
|
||||
|
||||
$user_id = (int) $request->get_param('id');
|
||||
$params = $this->get_json_params($request);
|
||||
$delta = (int) ($params['delta'] ?? 0);
|
||||
$reason = sanitize_text_field((string) ($params['reason'] ?? 'admin'));
|
||||
|
||||
if ($user_id <= 0 || $delta === 0) {
|
||||
return $this->error(__('用户或积分变动值无效', 'aksrt'), 422);
|
||||
}
|
||||
|
||||
if ($delta > 0) {
|
||||
$result = \AKSRT\Credits\Credits::add($user_id, $delta, $reason, ['type' => 'user', 'id' => $user_id], 'Admin adjustment');
|
||||
} else {
|
||||
$result = \AKSRT\Credits\Credits::deduct($user_id, abs($delta), $reason, ['type' => 'user', 'id' => $user_id], 'Admin adjustment');
|
||||
}
|
||||
$asset = sanitize_key((string) ($params['asset'] ?? ''));
|
||||
$raw_delta = $params['delta'] ?? null;
|
||||
$remark = sanitize_text_field((string) ($params['remark'] ?? ''));
|
||||
if ($user_id <= 0 || !in_array($asset, ['points', 'balance'], true) || !get_userdata($user_id)) {
|
||||
return $this->error(__('用户或资产类型无效', 'aksrt'), 400);
|
||||
}
|
||||
if (!current_user_can('edit_user', $user_id)) {
|
||||
return $this->error(__('无权调整该用户资产', 'aksrt'), 403);
|
||||
}
|
||||
$negative = is_string($raw_delta) && str_starts_with($raw_delta, '-');
|
||||
$unsigned = $negative ? substr($raw_delta, 1) : $raw_delta;
|
||||
try {
|
||||
if ($asset === 'points') {
|
||||
if (!is_int($raw_delta) || $raw_delta === PHP_INT_MIN) throw new \InvalidArgumentException();
|
||||
$minor = abs($raw_delta);
|
||||
$negative = $raw_delta < 0;
|
||||
} else {
|
||||
if (!is_string($unsigned)) throw new \InvalidArgumentException();
|
||||
$minor = \AKSRT\Ledger\AssetAmount::balance($unsigned)->minor();
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error(__('资产变动金额格式无效', 'aksrt'), 400);
|
||||
}
|
||||
if ($minor <= 0) return $this->error(__('资产变动金额必须非零', 'aksrt'), 400);
|
||||
$operation_key = \AKSRT\Ledger\OperationKey::from_request($request, 'admin-adjustment', get_current_user_id());
|
||||
if (is_wp_error($operation_key)) return new \WP_REST_Response(['success' => false, 'message' => $operation_key->get_error_message()], 400);
|
||||
$service = \AKSRT\Ledger\LedgerService::get_instance();
|
||||
$result = $negative
|
||||
? $service->debit($user_id, $asset, $minor, $operation_key, 'admin_adjustment', ['type' => 'user', 'id' => $user_id], $remark, get_current_user_id())
|
||||
: $service->credit($user_id, $asset, $minor, $operation_key, 'admin_adjustment', ['type' => 'user', 'id' => $user_id], $remark, get_current_user_id());
|
||||
|
||||
if (is_wp_error($result)) {
|
||||
return $this->error($result->get_error_message(), 422);
|
||||
$status = (int) (($result->get_error_data()['status'] ?? 400));
|
||||
return $this->error($result->get_error_message(), $status);
|
||||
}
|
||||
|
||||
return $this->success(['data' => [
|
||||
'user_id' => $user_id,
|
||||
'balance' => (int) $result,
|
||||
], 'message' => __('积分调整已保存', 'aksrt')]);
|
||||
'asset' => $asset,
|
||||
'balance' => $asset === 'balance'
|
||||
? \AKSRT\Ledger\AssetAmount::from_minor(\AKSRT\Ledger\AssetType::BALANCE, $result->balance($user_id, $asset))->decimal()
|
||||
: $result->balance($user_id, $asset),
|
||||
'transaction_id' => $result->transaction_id,
|
||||
'replayed' => $result->replayed,
|
||||
], 'message' => __('资产调整已保存', 'aksrt')]);
|
||||
}
|
||||
|
||||
public function rest_get_credits_log(\WP_REST_Request $request): \WP_REST_Response {
|
||||
$args = [
|
||||
'transaction_id' => (int) $request->get_param('transaction_id'),
|
||||
'operation_key' => (string) $request->get_param('operation_key'),
|
||||
'user_id' => (int) $request->get_param('user_id'),
|
||||
'actor_user_id' => (int) $request->get_param('actor_user_id'),
|
||||
'reason' => (string) $request->get_param('reason'),
|
||||
'ref_type' => (string) $request->get_param('ref_type'),
|
||||
'ref_key' => (string) $request->get_param('ref_key'),
|
||||
'page' => (int) $request->get_param('page'),
|
||||
'per_page' => (int) $request->get_param('per_page'),
|
||||
'date_from' => (string) $request->get_param('date_from'),
|
||||
'date_to' => (string) $request->get_param('date_to'),
|
||||
];
|
||||
$data = class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::get_log($args) : ['items' => [], 'total' => 0, 'page' => 1, 'per_page' => 20];
|
||||
$args['asset'] = sanitize_key((string) $request->get_param('asset'));
|
||||
$data = \AKSRT\Ledger\LedgerQuery::get_instance()->history($args);
|
||||
foreach ($data['items'] as &$item) {
|
||||
$assets = array_values(array_unique(array_map('strval', (array) ($item['assets'] ?? []))));
|
||||
$single_asset = count($assets) === 1 ? $assets[0] : '';
|
||||
if ($single_asset !== '') {
|
||||
$item['delta'] = $this->asset_minor_for_response($single_asset, (int) ($item['delta_minor'] ?? 0), true);
|
||||
$item['fee'] = $this->asset_minor_for_response($single_asset, (int) ($item['fee_minor'] ?? 0));
|
||||
$item['balance_after'] = $this->asset_minor_for_response($single_asset, (int) ($item['balance_after_minor'] ?? 0));
|
||||
} else {
|
||||
$item['delta'] = null;
|
||||
$item['fee'] = null;
|
||||
$item['balance_after'] = null;
|
||||
}
|
||||
unset($item['delta_minor'], $item['fee_minor'], $item['balance_after_minor']);
|
||||
foreach ($item['entries'] as &$entry) {
|
||||
$entry_asset = (string) ($entry['asset'] ?? '');
|
||||
$entry['delta'] = $this->asset_minor_for_response($entry_asset, (int) ($entry['delta_minor'] ?? 0), true);
|
||||
$entry['balance_after'] = $this->asset_minor_for_response($entry_asset, (int) ($entry['balance_after_minor'] ?? 0));
|
||||
unset($entry['delta_minor'], $entry['balance_after_minor']);
|
||||
}
|
||||
unset($entry);
|
||||
}
|
||||
unset($item);
|
||||
return $this->success(['data' => $data]);
|
||||
}
|
||||
|
||||
private function asset_minor_for_response(string $asset, int $minor, bool $signed = false) {
|
||||
if ($asset === \AKSRT\Ledger\AssetType::POINTS->value) {
|
||||
return $minor;
|
||||
}
|
||||
if ($asset !== \AKSRT\Ledger\AssetType::BALANCE->value) {
|
||||
return null;
|
||||
}
|
||||
$negative = $minor < 0;
|
||||
$value = \AKSRT\Ledger\AssetAmount::decimal_from_minor(abs($minor));
|
||||
if ($negative) {
|
||||
return '-' . $value;
|
||||
}
|
||||
return $signed && $minor > 0 ? '+' . $value : $value;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user