feat(payment): separate creator income settlement

This commit is contained in:
kate522 committed 2026-08-07 12:39:32 +08:00
1 parent cd93ea1656
commit e20418e909
17 files changed
+451 -445

No files matched your search

+14 -7
View File
@@ -51,12 +51,16 @@ class PayIncome {
$svc = \AKSRT\Payment\Revenue::get_instance();
$stats = $user_id > 0 ? $svc->get_stats($user_id) : $svc->get_stats();
$cash_stats = (array) ($stats['cash'] ?? []);
$points_stats = (array) ($stats['points'] ?? []);
echo '<p>' . esc_html(sprintf(
/* translators: 1: total 2: pending 3: settled */
__('合计 %1$s · 待结算 %2$s · 已结算 %3$s', 'aksrt'),
number_format((float) ($stats['total'] ?? 0), 2),
number_format((float) ($stats['pending'] ?? 0), 2),
number_format((float) ($stats['settled'] ?? 0), 2)
__('现金:合计 ¥%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)
)) . '</p>';
echo '<form id="aksrt-pay-batch-settle-income" method="post" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin:12px 0;">';
@@ -68,7 +72,7 @@ class PayIncome {
echo '<form method="get" style="margin:12px 0;">';
echo '<input type="hidden" name="page" value="aksrt_income" />';
echo '<select name="status">';
foreach (['' => __('全部状态', 'aksrt'), 'pending' => __('待结算', 'aksrt'), 'settled' => __('已结算', 'aksrt'), 'cancelled' => __('已取消', 'aksrt'), 'reversed' => __('已冲正', 'aksrt'), 'pending_recovery' => __('待追缴', 'aksrt')] as $key => $label) {
foreach (['' => __('全部状态', 'aksrt'), 'pending' => __('待结算', 'aksrt'), 'settled' => __('已结算', 'aksrt')] as $key => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
}
echo '</select> ';
@@ -116,7 +120,10 @@ class PayIncome {
echo '<td>' . ($st === 'pending' ? '<input type="checkbox" form="aksrt-pay-batch-settle-income" name="revenue_ids[]" value="' . esc_attr((string) $id) . '" />' : '') . '</td>';
echo '<td>' . esc_html((string) $id) . '</td><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>';
$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>';
echo '<td>' . esc_html($st) . '</td>';
echo '<td>' . esc_html((string) ($row['created_at'] ?? '')) . '</td>';
echo '<td>';
+21 -37
View File
@@ -48,11 +48,6 @@ class PaymentController extends ControllerBase {
'callback' => [$this, 'settle_creator_income'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/payment/creator-income/order/(?P<order_id>[a-zA-Z0-9_-]+)/reverse', [
'methods' => 'POST',
'callback' => [$this, 'reverse_creator_income'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/payment/withdrawals', [
'methods' => 'GET',
@@ -160,7 +155,10 @@ class PaymentController extends ControllerBase {
* GET /payment/revenue — revenue stats with optional time-series
*/
public function get_revenue(\WP_REST_Request $request): \WP_REST_Response {
$revenue_stats = ['total' => 0, 'pending' => 0, 'settled' => 0, 'count' => 0];
$revenue_stats = [
'cash' => ['total' => 0, 'pending' => 0, 'settled' => 0, '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();
}
@@ -194,12 +192,14 @@ class PaymentController extends ControllerBase {
$date_to = sanitize_text_field((string) $request->get_param('date_to'));
$granularity = sanitize_text_field((string) $request->get_param('granularity'));
// Default: last 30 days
// 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 = gmdate('Y-m-d', strtotime('-30 days'));
$date_from = $now->modify('-30 days')->format('Y-m-d');
}
if (!$date_to) {
$date_to = gmdate('Y-m-d');
$date_to = $now->format('Y-m-d');
}
// Granularity: day, week, month
@@ -212,27 +212,27 @@ class PaymentController extends ControllerBase {
$table = $prefix . 'aksrt_revenue';
$results = $wpdb->get_results($wpdb->prepare(
"SELECT DATE_FORMAT(created_at, %s) AS period,
"SELECT currency, DATE_FORMAT(created_at, %s) AS period,
SUM(amount) AS total_amount,
COUNT(*) AS order_count
FROM {$table}
WHERE created_at >= %s AND created_at < DATE_ADD(%s, INTERVAL 1 DAY)
GROUP BY period
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 = [];
if ($results) {
foreach ($results as $row) {
$series[] = [
'date' => (string) $row->period,
'amount' => (float) $row->total_amount,
'count' => (int) $row->order_count,
];
}
$series = ['cash' => [], 'points' => []];
foreach ((array) $results as $row) {
$key = (string) ($row->currency ?? '') === 'points' ? 'points' : 'cash';
$series[$key][] = [
'date' => (string) $row->period,
'amount' => (float) $row->total_amount,
'count' => (int) $row->order_count,
];
}
return $series;
@@ -277,22 +277,6 @@ class PaymentController extends ControllerBase {
return $this->success(['message' => __('创作分成已结算', 'aksrt')]);
}
/**
* POST /payment/creator-income/order/{order_id}/reverse — cancel or reverse order income.
*/
public function reverse_creator_income(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Payment\Revenue')) {
return $this->error(__('创作分成模块未启用', 'aksrt'), 500);
}
$order_id = sanitize_text_field((string) $request->get_param('order_id'));
$params = $this->get_json_params($request);
$reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
if ($order_id === '' || !\AKSRT\Payment\Revenue::get_instance()->cancel_or_reverse($order_id, $reason)) {
return $this->error(__('冲正失败', 'aksrt'));
}
return $this->success(['message' => __('创作分成已处理退款/冲正', 'aksrt')]);
}
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' => []]]);
+13 -14
View File
@@ -164,24 +164,23 @@ class SchemaPayment {
'revenue' => [
'key' => 'revenue',
'title' => '创作分成',
'version' => '1.0.0',
'version' => '2.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'policy', 'title' => '分成策略', 'fields' => [
['key' => 'basic', 'title' => '创作分成设置', 'fields' => [
['key' => 'pay_income_s', 'label' => '启用创作分成', 'type' => 'switch', 'default' => false],
['key' => 'payment_revenue_share_ratio', 'label' => '作者分成比例(%)', 'type' => 'number', 'default' => 70, 'min' => 1, 'max' => 100],
['key' => 'ratio_vip_1', 'label' => 'VIP1分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
['key' => 'ratio_vip_2', 'label' => 'VIP2分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
['key' => 'payment_revenue_settlement_threshold', 'label' => '最小结算金额', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 99999],
['key' => 'payment_revenue_cycle_days', 'label' => '结算周期(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365],
['key' => 'payment_revenue_settlement_mode', 'label' => '结算模式', 'type' => 'select', 'default' => 'automatic', 'options' => [['value' => 'automatic', 'label' => '自动结算'], ['value' => 'manual', 'label' => '手动结算']]],
['key' => 'payment_revenue_settlement_delay_hours', 'label' => '结算延迟(小时)', 'type' => 'number', 'default' => 24, 'min' => 0, 'max' => 8760],
['key' => 'payment_revenue_points_rule', 'label' => '积分规则', 'type' => 'textarea', 'default' => ''],
['key' => 'payment_revenue_note', 'label' => '结算备注', 'type' => 'textarea', 'default' => ''],
['key' => 'payment_revenue_cycle_days', 'label' => '结算周期(天)', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 365, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
['key' => 'payment_revenue_note', 'label' => '结算备注文案', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<p>订单完成并经过结算周期后,作者分成会自动转入对应的钱包。</p>', 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
]],
['key' => 'display', 'title' => '展示设置', 'fields' => [
['key' => 'pay_income_desc_details', 'label' => '收益明细文案', 'type' => 'textarea', 'default' => ''],
['key' => 'pay_income_desc_page', 'label' => '收益说明页', 'type' => 'input', 'default' => ''],
['key' => 'cash', 'title' => '现金收益分成', 'fields' => [
['key' => 'payment_revenue_cash_ratio_user', 'label' => '普通用户分成比例(%)', 'type' => 'number', 'default' => 60, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
['key' => 'payment_revenue_cash_ratio_vip_1', 'label' => '一级会员分成比例(%)', 'type' => 'number', 'default' => 70, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
['key' => 'payment_revenue_cash_ratio_vip_2', 'label' => '二级会员分成比例(%)', 'type' => 'number', 'default' => 80, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
]],
['key' => 'points', 'title' => '积分收益分成', 'fields' => [
['key' => 'payment_revenue_points_ratio_user', 'label' => '普通用户分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
['key' => 'payment_revenue_points_ratio_vip_1', 'label' => '一级会员分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
['key' => 'payment_revenue_points_ratio_vip_2', 'label' => '二级会员分成比例(%)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'pay_income_s', 'value' => true]],
]],
],
],