积分与余额账务系统硬切重构

This commit is contained in:
2026-08-24 15:59:11 +08:00
parent ca12226b39
commit 5cae5b5c93
159 changed files with 7275 additions and 4718 deletions
+125 -260
View File
@@ -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]);
}
}