feat(payment): 支付&付费菜单按参考主题重构并新增提现策略页

- 子菜单重组为知识付费/余额充值/VIP会员/创作分成/推广返佣/收款接口/提现策略
- 收款接口页 1:1 复刻 Zibll 双下拉结构,仅保留支付宝/微信/PayPal/Stripe/易支付
- 订单前缀/流水号前缀/提现单号前缀配置保留并移入收款接口页
- 知识付费/推广返佣按参考主题全字段复刻,余额充值并入充值卡配置
- 新增 tabbed/between_number/page_select 表单控件及后端 pages 搜索源
- 清理旧 SPA 组件、无消费者 REST 端点与废弃配置键(不写双路兼容)
- 更新存量契约测试至新 schema 文件
This commit is contained in:
2026-09-16 19:18:28 +08:00
parent fb5cdf247d
commit a48007a44d
36 changed files with 2042 additions and 2273 deletions
-224
View File
@@ -19,49 +19,6 @@ class PaymentController extends ControllerBase {
public function register_routes(): void {
$this->register_section_routes('payment', 'payment');
register_rest_route(self::API_NAMESPACE, '/payment/overview', [
'methods' => 'GET',
'callback' => [$this, 'rest_payment_overview'],
'permission_callback' => [$this, 'check_permission'],
]);
// Revenue stats (with optional time-series)
register_rest_route(self::API_NAMESPACE, '/payment/revenue', [
'methods' => 'GET',
'callback' => [$this, 'get_revenue'],
'permission_callback' => [$this, 'check_permission'],
'args' => [
'date_from' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
'date_to' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'],
'granularity' => ['default' => 'day', 'sanitize_callback' => 'sanitize_text_field'],
'time_series' => ['default' => false, 'sanitize_callback' => 'rest_sanitize_boolean'],
],
]);
register_rest_route(self::API_NAMESPACE, '/payment/creator-income', [
'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'],
@@ -154,187 +111,6 @@ class PaymentController extends ControllerBase {
}
public function rest_payment_overview(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Settings\Settings')) {
return $this->error('设置模块未初始化', 500);
}
$paid = \AKSRT\Settings\Settings::get_paid_content_settings();
$recharge = \AKSRT\Settings\Settings::get_payment_recharge_settings();
$vip = \AKSRT\Settings\Settings::get_payment_vip_settings();
$revenue = \AKSRT\Settings\Settings::get_payment_revenue_settings();
$affiliate = \AKSRT\Settings\Settings::get_payment_affiliate_settings();
$gateway = \AKSRT\Settings\Settings::get_payment_gateway_settings();
return $this->success([
'data' => [
'paid' => $paid,
'recharge' => $recharge,
'vip' => $vip,
'revenue' => $revenue,
'affiliate' => $affiliate,
'gateway' => $gateway,
'currency_name' => \AKSRT\Settings\Settings::get_currency_name(),
],
]);
}
/**
* GET /payment/revenue — revenue stats with optional time-series
*/
public function get_revenue(\WP_REST_Request $request): \WP_REST_Response {
$revenue_stats = [
'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.00'];
if (class_exists('\AKSRT\Payment\Withdrawal')) {
$withdrawal_stats = \AKSRT\Payment\Withdrawal::get_instance()->get_stats();
}
$response = [
'revenue' => $revenue_stats,
'withdrawal' => $withdrawal_stats,
];
// Time-series data
if ($request->get_param('time_series')) {
$response['time_series'] = $this->get_revenue_time_series($request);
}
return $this->success(['data' => $response]);
}
/**
* Build revenue time-series data.
*/
private function get_revenue_time_series(\WP_REST_Request $request): array {
global $wpdb;
$prefix = $wpdb->prefix;
$date_from = sanitize_text_field((string) $request->get_param('date_from'));
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
$granularity = sanitize_text_field((string) $request->get_param('granularity'));
// Default: last 30 days in the WordPress site timezone.
$timezone = function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC');
$now = new \DateTimeImmutable('now', $timezone);
if (!$date_from) {
$date_from = $now->modify('-30 days')->format('Y-m-d');
}
if (!$date_to) {
$date_to = $now->format('Y-m-d');
}
// Granularity: day, week, month
$date_format = '%Y-%m-%d';
if ($granularity === 'week') {
$date_format = '%x-W%v';
} elseif ($granularity === 'month') {
$date_format = '%Y-%m';
}
$table = $prefix . 'aksrt_revenue';
$results = $wpdb->get_results($wpdb->prepare(
"SELECT currency, DATE_FORMAT(created_at, %s) AS period,
SUM(amount) AS total_amount,
COUNT(*) AS order_count
FROM {$table}
WHERE status IN ('pending', 'settled')
AND created_at >= %s AND created_at < DATE_ADD(%s, INTERVAL 1 DAY)
GROUP BY currency, period
ORDER BY period ASC",
$date_format,
$date_from,
$date_to
));
$series = ['cash' => [], 'points' => []];
foreach ((array) $results as $row) {
$key = (string) ($row->currency ?? '') === 'points' ? 'points' : 'cash';
$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' => $amount,
'count' => (int) $row->order_count,
];
}
return $series;
}
/**
* GET /payment/creator-income — creator income operations list.
*/
public function get_creator_income(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Revenue')) {
return $this->error(__('创作分成模块未启用', 'aksrt'), 500);
}
$page = max(1, (int) $request->get_param('page'));
$per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20)));
$result = \AKSRT\Payment\Revenue::get_instance()->get_list([
'limit' => $per_page,
'offset' => ($page - 1) * $per_page,
'status' => sanitize_key((string) $request->get_param('status')),
'user_id' => max(0, (int) $request->get_param('user_id')),
'post_id' => max(0, (int) $request->get_param('post_id')),
'currency' => sanitize_key((string) $request->get_param('currency')),
'order_status' => sanitize_key((string) $request->get_param('order_status')),
'order_number' => sanitize_text_field((string) $request->get_param('order_number')),
'date_from' => sanitize_text_field((string) $request->get_param('date_from')),
'date_to' => sanitize_text_field((string) $request->get_param('date_to')),
]);
$result['page'] = $page;
$result['per_page'] = $per_page;
return $this->success(['data' => $result]);
}
/**
* POST /payment/creator-income/{id}/settle — settle one income record.
*/
public function settle_creator_income(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Revenue')) {
return $this->error(__('创作分成模块未启用', 'aksrt'), 500);
}
$id = (int) $request->get_param('id');
$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 {
if (!class_exists('\AKSRT\Payment\Withdrawal')) {
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1, 'stats' => []]]);