Merge pull request 'Worktree creator income rework' (#406) from worktree-creator-income-rework into main
Reviewed-on: CYL/wordpress-theme-aksrt#406
This commit is contained in:
@@ -63,11 +63,11 @@ export const paymentSection = {
|
||||
schemaPage({
|
||||
key: 'payment-revenue',
|
||||
name: 'PaymentRevenue',
|
||||
label: '创作分成',
|
||||
label: '创作中心',
|
||||
route: '/payment/revenue',
|
||||
status: 'available',
|
||||
title: '创作分成',
|
||||
desc: '作者分成比例、结算模式、结算延迟和收益说明。',
|
||||
title: '创作中心',
|
||||
desc: '独立配置现金与积分分成比例、结算周期和结算备注文案。',
|
||||
canonicalDomain: 'payment',
|
||||
section: 'revenue',
|
||||
}),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<PaymentSectionPage
|
||||
section="revenue"
|
||||
title="收益中心"
|
||||
desc="查看现金与积分收入流水、收益统计和提现记录。"
|
||||
title="创作中心"
|
||||
desc="查看现金与积分收益统计及结算运营入口。"
|
||||
:links="[
|
||||
{ label: 'VIP 会员', path: '/payment/vip' },
|
||||
{ label: '推广返佣', path: '/payment/affiliate' },
|
||||
{ label: '创作分成运营', path: '/payment/income' },
|
||||
]"
|
||||
>
|
||||
<template #before-form>
|
||||
|
||||
@@ -77,7 +77,7 @@ function createSectionRedirectRoutes() {
|
||||
*/
|
||||
const NATIVE_OPS_REDIRECTS = [
|
||||
{ path: '/payment/charge-card', title: '充值卡', nativeAdminPath: 'admin.php?page=aksrt_charge_cards' },
|
||||
{ path: '/payment/revenue', title: '创作分成', nativeAdminPath: 'admin.php?page=aksrt_income' },
|
||||
{ path: '/payment/income', title: '创作分成运营', nativeAdminPath: 'admin.php?page=aksrt_income' },
|
||||
{ path: '/payment/rebate', title: '返佣管理', nativeAdminPath: 'admin.php?page=aksrt_rebate' },
|
||||
{ path: '/users/messages', title: '消息中心', nativeAdminPath: 'users.php?page=aksrt_messages' },
|
||||
{ path: '/users/invite-codes', title: '邀请码运营', nativeAdminPath: 'users.php?page=aksrt_invite_codes' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const revenueSchema = {
|
||||
key: 'payment.revenue',
|
||||
title: '创作分成',
|
||||
title: '创作中心',
|
||||
icon: 'fas fa-chart-line',
|
||||
description: '分别配置现金收益和积分收益的会员分成比例,结算周期结束后自动入账。',
|
||||
defaults: {
|
||||
@@ -17,7 +17,7 @@ export const revenueSchema = {
|
||||
groups: [
|
||||
{
|
||||
key: 'basic',
|
||||
title: '创作分成设置',
|
||||
title: '创作中心设置',
|
||||
icon: 'fas fa-cog',
|
||||
fields: [
|
||||
{ key: 'pay_income_s', label: '启用创作分成', type: 'switch', default: false },
|
||||
|
||||
@@ -12,7 +12,7 @@ export const MEDAL_GET_TYPES = [
|
||||
{ label: '文章获赞', value: 'like' },
|
||||
{ label: '评论获赞', value: 'comment_like' },
|
||||
{ label: '内容阅读量', value: 'views' },
|
||||
{ label: '创作分成累计(元)', value: 'pay_income' },
|
||||
{ label: '现金创作分成累计(元)', value: 'pay_income' },
|
||||
{ label: '推广佣金累计(元)', value: 'pay_rebate' },
|
||||
{ label: '仅手动授予', value: 'manually_add' },
|
||||
]
|
||||
|
||||
@@ -24,7 +24,7 @@ export const featuresSchema = {
|
||||
title: '用户中心',
|
||||
icon: 'fas fa-user-shield',
|
||||
fields: [
|
||||
{ key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,level,posts,comments,favorites,vip,credits,revenue,auth', help: '逗号分隔。level=我的等级;revenue=收益中心;auth=官方认证;等级、VIP、订单、收益中心和认证会按功能开关自动注入' },
|
||||
{ key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,level,comments,favorites,vip,credits,revenue,auth', help: '逗号分隔。level=我的等级;creator=创作中心;revenue=推广收益中心;auth=官方认证;等级、VIP、订单、创作中心、收益中心和认证会按功能开关自动注入' },
|
||||
{ key: 'user_cover_upload', label: '封面上传', type: 'switch', default: true },
|
||||
{ key: 'user_social_links', label: '社交链接', type: 'switch', default: true },
|
||||
{ key: 'user_desc_std', label: '默认签名', type: 'input', default: '这家伙很懒…', help: '用户未设置个人简介时显示的默认文案' },
|
||||
|
||||
@@ -721,6 +721,7 @@
|
||||
.creator-income-card__metrics small { display: block; }
|
||||
.creator-income-card__metrics strong { color: var(--color-ink); font-size: 18px; font-variant-numeric: tabular-nums; }
|
||||
.creator-income-card__metrics small { margin-top: 4px; color: var(--color-ink-4); font-size: 11px; }
|
||||
.creator-income-card__hint { margin: 12px 0 0; color: var(--color-ink-4); font-size: 12px; line-height: 1.5; }
|
||||
.creator-income__info { padding: 20px; }
|
||||
.creator-income__info h2,
|
||||
.creator-income__toolbar h2 { margin: 0; font-size: 16px; }
|
||||
|
||||
@@ -70,17 +70,17 @@
|
||||
closeDrawer();
|
||||
}
|
||||
|
||||
function loadPanel(name) {
|
||||
function loadPanel(name, forceLoad, queryParams) {
|
||||
if (!panels[name]) return Promise.reject(new Error('panel-not-found'));
|
||||
var target = panels[name].panel;
|
||||
if (target.dataset.panelLoaded === 'true') return Promise.resolve();
|
||||
if (!forceLoad && target.dataset.panelLoaded === 'true') return Promise.resolve();
|
||||
if (target._aksrtPanelRequest) return target._aksrtPanelRequest;
|
||||
var restUrl = (window.aksrtData && (window.aksrtData.restUrl || window.aksrtData.apiUrl)) || cfg.restUrl || '';
|
||||
if (!restUrl) return Promise.reject(new Error('rest-unavailable'));
|
||||
target.classList.add('is-loading');
|
||||
var url = restUrl.replace(/\/$/, '') + '/user-center/panel/' + encodeURIComponent(name);
|
||||
if (name === 'creator') {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var params = queryParams || new URLSearchParams(window.location.search);
|
||||
['income_page'].forEach(function(key) {
|
||||
if (params.get(key)) url += (url.indexOf('?') === -1 ? '?' : '&') + key + '=' + encodeURIComponent(params.get(key));
|
||||
});
|
||||
@@ -101,9 +101,9 @@
|
||||
return target._aksrtPanelRequest;
|
||||
}
|
||||
|
||||
function activate(name, updateHistory) {
|
||||
function activate(name, updateHistory, forceLoad, queryParams) {
|
||||
if (!panels[name]) name = defaultTab;
|
||||
return loadPanel(name).then(function() {
|
||||
return loadPanel(name, forceLoad, queryParams).then(function() {
|
||||
switchTo(name);
|
||||
if (updateHistory) updateUrl(name);
|
||||
if (typeof window.AKSRTRevenueCenterInit === 'function') window.AKSRTRevenueCenterInit(panels[name].panel);
|
||||
@@ -138,7 +138,23 @@
|
||||
activate(this.getAttribute('data-tab'), true);
|
||||
});
|
||||
});
|
||||
window.addEventListener('popstate', function() { activate(tabFromUrl(), false); });
|
||||
var creatorPanel = panels.creator ? panels.creator.panel : null;
|
||||
if (creatorPanel) {
|
||||
creatorPanel.addEventListener('click', function(e) {
|
||||
var link = e.target && e.target.closest ? e.target.closest('.creator-income__pagination a') : null;
|
||||
if (!link || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
||||
e.preventDefault();
|
||||
var pageUrl = new URL(link.href, window.location.href);
|
||||
loadPanel('creator', true, pageUrl.searchParams).then(function() {
|
||||
switchTo('creator');
|
||||
if (window.history.pushState) window.history.pushState({}, '', pageUrl.toString());
|
||||
if (typeof window.AKSRTRevenueCenterInit === 'function') window.AKSRTRevenueCenterInit(creatorPanel);
|
||||
}).catch(function() {
|
||||
if (typeof showToast === 'function') showToast(i18n.networkError || '加载失败,请重试');
|
||||
});
|
||||
});
|
||||
}
|
||||
window.addEventListener('popstate', function() { activate(tabFromUrl(), false, true); });
|
||||
if (drawerTrigger) drawerTrigger.addEventListener('click', openDrawer);
|
||||
if (drawerOverlay) drawerOverlay.addEventListener('click', closeDrawer);
|
||||
activate(tabFromUrl(), false);
|
||||
|
||||
@@ -147,7 +147,6 @@ if (!function_exists('aksrt_capture_user_center_view')) {
|
||||
$map = [
|
||||
'level' => 'level',
|
||||
'profile' => 'form',
|
||||
'posts' => 'posts',
|
||||
'creator' => 'creator',
|
||||
'comments' => 'comments',
|
||||
'favorites' => 'favorites',
|
||||
@@ -180,7 +179,7 @@ if (!function_exists('aksrt_capture_user_center_panel')) {
|
||||
* Capture a built-in or registered user-center panel body.
|
||||
*/
|
||||
function aksrt_capture_user_center_panel(string $tab_key, array $tab, array $context): string {
|
||||
$built_in_tabs = ['profile', 'level', 'posts', 'creator', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'revenue', 'settings'];
|
||||
$built_in_tabs = ['profile', 'level', 'creator', 'comments', 'favorites', 'credits', 'vip', 'orders', 'auth', 'revenue', 'settings'];
|
||||
if (in_array($tab_key, $built_in_tabs, true)) {
|
||||
return aksrt_capture_user_center_view($tab_key, $context);
|
||||
}
|
||||
@@ -274,7 +273,6 @@ if (!function_exists('aksrt_user_center_resolve_tabs')) {
|
||||
? \AKSRT\User\Center::get_instance()->get_dashboard_tabs()
|
||||
: [
|
||||
'profile' => ['label' => __('个人信息', 'aksrt'), 'icon' => 'user'],
|
||||
'posts' => ['label' => __('我的文章', 'aksrt'), 'icon' => 'file-alt'],
|
||||
'comments' => ['label' => __('我的评论', 'aksrt'), 'icon' => 'comment'],
|
||||
'favorites' => ['label' => __('我的收藏', 'aksrt'), 'icon' => 'star'],
|
||||
'settings' => ['label' => __('账号设置', 'aksrt'), 'icon' => 'cog'],
|
||||
@@ -310,29 +308,6 @@ if (!function_exists('aksrt_user_center_resolve_tabs')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_user_center_post_status_counts')) {
|
||||
/**
|
||||
* Count the current user's posts by status (posts tab only).
|
||||
*
|
||||
* @return array{counts:array<string,int>,total:int}
|
||||
*/
|
||||
function aksrt_user_center_post_status_counts(int $user_id): array {
|
||||
global $wpdb;
|
||||
$counts = ['publish' => 0, 'draft' => 0, 'pending' => 0, 'private' => 0];
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT post_status, COUNT(*) AS num FROM {$wpdb->posts} WHERE post_author = %d AND post_type = 'post' AND post_status IN ('publish','draft','pending','private') GROUP BY post_status",
|
||||
$user_id
|
||||
));
|
||||
foreach ((array) $rows as $row) {
|
||||
$counts[$row->post_status] = (int) $row->num;
|
||||
}
|
||||
return [
|
||||
'counts' => $counts,
|
||||
'total' => (int) array_sum($counts),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_user_center_runtime_config')) {
|
||||
/**
|
||||
* Frontend runtime config injected into the private user center page.
|
||||
@@ -444,7 +419,7 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
|
||||
$user_center_settings = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_user_center_settings()
|
||||
: [
|
||||
'tabs' => ['profile', 'posts', 'comments', 'favorites'],
|
||||
'tabs' => ['profile', 'comments', 'favorites'],
|
||||
'avatar_upload' => true,
|
||||
'cover_upload' => true,
|
||||
'social_links' => true,
|
||||
@@ -475,17 +450,6 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
|
||||
? \AKSRT\User\Follow::get_instance()->get_follower_count($user_id)
|
||||
: 0;
|
||||
|
||||
$post_status_filter = isset($_GET['post_status']) ? sanitize_key(wp_unslash($_GET['post_status'])) : 'publish';
|
||||
if (!in_array($post_status_filter, ['all', 'publish', 'draft', 'pending', 'private'], true)) {
|
||||
$post_status_filter = 'publish';
|
||||
}
|
||||
$user_status_counts = ['publish' => 0, 'draft' => 0, 'pending' => 0, 'private' => 0];
|
||||
if ('posts' === $active_tab) {
|
||||
$status = aksrt_user_center_post_status_counts($user_id);
|
||||
$user_status_counts = $status['counts'];
|
||||
$post_count = $status['total'];
|
||||
}
|
||||
|
||||
$svgs = aksrt_user_center_empty_svgs();
|
||||
require_once AKSRT_DIR . '/templates/parts/user-shared-helpers.php';
|
||||
|
||||
@@ -499,7 +463,6 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
|
||||
$runtime_config['avatarUpload'] = aksrt_user_center_avatar_upload_config($allow_avatar_upload);
|
||||
$revenue_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_revenue_settings() : [];
|
||||
$show_revenue_section = !empty($revenue_settings['enabled']) || !empty($affiliate_settings['enabled']);
|
||||
$new_post_url = function_exists('aksrt_get_new_post_url') ? aksrt_get_new_post_url() : '';
|
||||
$revenue_url = isset($tabs['tabs']['revenue'])
|
||||
? aksrt_get_user_center_tab_url('revenue')
|
||||
: '';
|
||||
@@ -525,15 +488,10 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
|
||||
'show_avatar_edit' => $allow_avatar_upload,
|
||||
'notification_unread_count' => function_exists('aksrt_get_user_unread_notification_count') ? aksrt_get_user_unread_notification_count($user_id) : 0,
|
||||
'messages_url' => $messages_enabled && function_exists('aksrt_get_messages_page_url') ? aksrt_get_messages_page_url() : '',
|
||||
'new_post_url' => $new_post_url,
|
||||
'revenue_url' => $revenue_url,
|
||||
'per_page' => 10,
|
||||
'posts_page' => max(1, isset($_GET['posts_page']) ? (int) $_GET['posts_page'] : 1),
|
||||
'comments_page' => max(1, isset($_GET['comments_page']) ? (int) $_GET['comments_page'] : 1),
|
||||
'favorites_page' => max(1, isset($_GET['favorites_page']) ? (int) $_GET['favorites_page'] : 1),
|
||||
'post_status_filter' => $post_status_filter,
|
||||
'user_status_counts' => $user_status_counts,
|
||||
'svg_empty_post' => $svgs['post'],
|
||||
'svg_empty_comment' => $svgs['comment'],
|
||||
'svg_empty_favorite' => $svgs['favorite'],
|
||||
'orders_url' => function_exists('aksrt_get_user_center_tab_url')
|
||||
|
||||
@@ -67,6 +67,9 @@ $currency_labels = [
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if ($unit === 'CNY') : ?>
|
||||
<p class="creator-income-card__hint"><?php esc_html_e('超过 50 元可申请现金提现。', 'aksrt'); ?></p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</section>
|
||||
|
||||
@@ -18,7 +18,7 @@ if ($creation_url === '' && $revenue_url === '') {
|
||||
<?php if ($creation_url !== '') : ?>
|
||||
<a class="uc-center-card uc-center-card--creation" href="<?php echo esc_url($creation_url); ?>">
|
||||
<span class="uc-center-card__icon"><i class="fas fa-feather-alt" aria-hidden="true"></i></span>
|
||||
<span><strong><?php esc_html_e('创作中心', 'aksrt'); ?></strong><small><?php esc_html_e('发布与管理内容', 'aksrt'); ?></small></span>
|
||||
<span><strong><?php esc_html_e('创作中心', 'aksrt'); ?></strong><small><?php esc_html_e('查看作者现金与积分收益', 'aksrt'); ?></small></span>
|
||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -40,14 +40,21 @@ class PayIncome {
|
||||
}
|
||||
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||
$order_status = isset($_GET['order_status']) ? sanitize_key((string) wp_unslash($_GET['order_status'])) : '';
|
||||
$user_id = isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : 0;
|
||||
$post_id = isset($_GET['post_id']) ? max(0, (int) $_GET['post_id']) : 0;
|
||||
$currency = isset($_GET['currency']) ? sanitize_key((string) wp_unslash($_GET['currency'])) : '';
|
||||
$currency = isset($_GET['currency']) ? strtolower(sanitize_key((string) wp_unslash($_GET['currency']))) : '';
|
||||
if ($currency === 'cny') {
|
||||
$currency = 'CNY';
|
||||
}
|
||||
$order_number = isset($_GET['order_number']) ? sanitize_text_field((string) wp_unslash($_GET['order_number'])) : '';
|
||||
$date_from = isset($_GET['date_from']) ? sanitize_text_field((string) wp_unslash($_GET['date_from'])) : '';
|
||||
$date_to = isset($_GET['date_to']) ? sanitize_text_field((string) wp_unslash($_GET['date_to'])) : '';
|
||||
$page_num = max(1, (int) ($_GET['paged'] ?? 1));
|
||||
$per_page = 20;
|
||||
$order_statuses = class_exists('\\AKSRT\\Payment\\Order')
|
||||
? \AKSRT\Payment\Order::get_valid_statuses()
|
||||
: [];
|
||||
$svc = \AKSRT\Payment\Revenue::get_instance();
|
||||
$stats = $user_id > 0 ? $svc->get_stats($user_id) : $svc->get_stats();
|
||||
|
||||
@@ -76,6 +83,11 @@ class PayIncome {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($status, $key, false), esc_html($label));
|
||||
}
|
||||
echo '</select> ';
|
||||
echo '<select name="order_status"><option value="">' . esc_html__('全部订单状态', 'aksrt') . '</option>';
|
||||
foreach ((array) $order_statuses as $key => $label) {
|
||||
printf('<option value="%s"%s>%s</option>', esc_attr($key), selected($order_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" /> ';
|
||||
echo '<input type="number" min="0" name="post_id" value="' . esc_attr($post_id > 0 ? (string) $post_id : '') . '" placeholder="' . esc_attr__('文章 ID', 'aksrt') . '" class="small-text" /> ';
|
||||
echo '<select name="currency"><option value="">' . esc_html__('全部货币', 'aksrt') . '</option><option value="CNY"' . selected($currency, 'CNY', false) . '>CNY</option><option value="points"' . selected($currency, 'points', false) . '>' . esc_html__('积分', 'aksrt') . '</option></select> ';
|
||||
@@ -89,6 +101,7 @@ class PayIncome {
|
||||
|
||||
$list_args = [
|
||||
'status' => $status,
|
||||
'order_status' => $order_status,
|
||||
'limit' => $per_page,
|
||||
'offset' => ($page_num - 1) * $per_page,
|
||||
'post_id' => $post_id,
|
||||
@@ -174,10 +187,13 @@ class PayIncome {
|
||||
}
|
||||
}
|
||||
$ids = array_values(array_unique($ids));
|
||||
$ok = !empty($ids)
|
||||
&& class_exists('\\AKSRT\\Payment\\Revenue')
|
||||
&& \AKSRT\Payment\Revenue::get_instance()->settle($ids, get_current_user_id());
|
||||
wp_safe_redirect($this->page_url('aksrt_income', ['aksrt_pay_msg' => $ok ? 'income_batch_settled' : 'income_failed']));
|
||||
$batch_result = !empty($ids) && class_exists('\\AKSRT\\Payment\\Revenue')
|
||||
? \AKSRT\Payment\Revenue::get_instance()->settle_batch($ids, get_current_user_id())
|
||||
: ['settled' => [], 'failed' => $ids];
|
||||
$message = empty($batch_result['settled'])
|
||||
? 'income_failed'
|
||||
: (!empty($batch_result['failed']) ? 'income_batch_partial' : 'income_batch_settled');
|
||||
wp_safe_redirect($this->page_url('aksrt_income', ['aksrt_pay_msg' => $message]));
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -205,12 +221,15 @@ class PayIncome {
|
||||
$map = [
|
||||
'income_settled' => __('分成已结算。', 'aksrt'),
|
||||
'income_batch_settled' => __('选中分成已批量结算。', 'aksrt'),
|
||||
'income_batch_partial' => __('部分分成已结算,失败记录可稍后重试。', 'aksrt'),
|
||||
'income_failed' => __('分成结算失败。', 'aksrt'),
|
||||
];
|
||||
if (!isset($map[$code])) {
|
||||
return;
|
||||
}
|
||||
$class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success';
|
||||
$class = $code === 'income_batch_partial'
|
||||
? 'notice-warning'
|
||||
: (str_contains($code, 'failed') ? 'notice-error' : 'notice-success');
|
||||
echo '<div class="notice ' . esc_attr($class) . ' is-dismissible"><p>' . esc_html($map[$code]) . '</p></div>';
|
||||
}
|
||||
|
||||
|
||||
@@ -254,6 +254,7 @@ class PaymentController extends ControllerBase {
|
||||
'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')),
|
||||
|
||||
@@ -163,11 +163,11 @@ class SchemaPayment {
|
||||
],
|
||||
'revenue' => [
|
||||
'key' => 'revenue',
|
||||
'title' => '创作分成',
|
||||
'title' => '创作中心',
|
||||
'version' => '2.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'basic', 'title' => '创作分成设置', 'fields' => [
|
||||
['key' => 'basic', 'title' => '创作中心设置', 'fields' => [
|
||||
['key' => 'pay_income_s', 'label' => '启用创作分成', 'type' => 'switch', 'default' => false],
|
||||
['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]],
|
||||
|
||||
@@ -601,7 +601,7 @@ class SchemaUsers {
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'center', 'title' => '用户中心', 'fields' => [
|
||||
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,level,posts,comments,favorites,vip,credits,revenue,auth', 'help' => '逗号分隔。level=我的等级;revenue=收益中心;auth=官方认证;等级、VIP、订单、收益中心和认证会按功能开关自动注入'],
|
||||
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,level,comments,favorites,vip,credits,revenue,auth', 'help' => '逗号分隔。level=我的等级;creator=创作中心;revenue=推广收益中心;auth=官方认证;等级、VIP、订单、创作中心、收益中心和认证会按功能开关自动注入'],
|
||||
['key' => 'user_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'],
|
||||
|
||||
+47
-29
@@ -30,10 +30,26 @@ class Revenue {
|
||||
public function create_from_paid_order(array $order): int {
|
||||
global $wpdb;
|
||||
|
||||
if (!$this->is_enabled()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$order_number = sanitize_text_field((string) ($order['order_number'] ?? ''));
|
||||
if ($order_number === '' || (string) ($order['status'] ?? '') !== 'completed') {
|
||||
return 0;
|
||||
}
|
||||
$product_type = sanitize_key((string) ($order['product_type'] ?? ''));
|
||||
if ($product_type !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE order_number = %s ORDER BY id ASC LIMIT 1",
|
||||
$order_number
|
||||
));
|
||||
if ((int) $existing > 0) {
|
||||
return (int) $existing;
|
||||
}
|
||||
|
||||
$meta = $this->decode_order_meta($order);
|
||||
if (empty($meta['content_pay'])) {
|
||||
@@ -95,8 +111,7 @@ class Revenue {
|
||||
if ($inserted === false) {
|
||||
// Duplicate callbacks are a successful no-op by contract.
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE author_id = %d AND order_number = %s LIMIT 1",
|
||||
$author_id,
|
||||
"SELECT id FROM {$this->table()} WHERE order_number = %s ORDER BY id ASC LIMIT 1",
|
||||
$order_number
|
||||
));
|
||||
return (int) $existing;
|
||||
@@ -113,28 +128,6 @@ class Revenue {
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy generic insertion retained as the native income page entry point.
|
||||
*/
|
||||
public function add(int $user_id, float $amount, string $order_number = '', string $type = 'income'): int {
|
||||
global $wpdb;
|
||||
if ($user_id <= 0 || $amount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
$order_number = sanitize_text_field($order_number);
|
||||
$wpdb->insert($this->table(), [
|
||||
'author_id' => $user_id,
|
||||
'order_number' => $order_number,
|
||||
'amount' => round($amount, 2),
|
||||
'original_amount' => round($amount, 2),
|
||||
'currency' => 'CNY',
|
||||
'type' => sanitize_key($type),
|
||||
'status' => 'pending',
|
||||
'created_at' => current_time('mysql'),
|
||||
], ['%d', '%s', '%f', '%f', '%s', '%s', '%s', '%s']);
|
||||
return (int) $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settle one or more pending records through matching atomic balance services.
|
||||
*
|
||||
@@ -142,18 +135,34 @@ class Revenue {
|
||||
* @param int $actor_id
|
||||
*/
|
||||
public function settle($record_ids, int $actor_id = 0): bool {
|
||||
$result = $this->settle_batch($record_ids, $actor_id);
|
||||
return !empty($result['settled']) && empty($result['failed']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Settle a batch while preserving per-record outcomes for operational feedback.
|
||||
*
|
||||
* @param int|array<int,int> $record_ids
|
||||
* @param int $actor_id
|
||||
* @return array{settled:array<int,int>,failed:array<int,int>}
|
||||
*/
|
||||
public function settle_batch($record_ids, int $actor_id = 0): array {
|
||||
$ids = is_array($record_ids) ? $record_ids : [$record_ids];
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), static fn (int $id): bool => $id > 0)));
|
||||
if (!$ids || !$this->is_enabled()) {
|
||||
return false;
|
||||
return ['settled' => [], 'failed' => $ids];
|
||||
}
|
||||
|
||||
$settled = [];
|
||||
$failed = [];
|
||||
foreach ($ids as $revenue_id) {
|
||||
if (!$this->settle_one($revenue_id, $actor_id)) {
|
||||
return false;
|
||||
if ($this->settle_one($revenue_id, $actor_id)) {
|
||||
$settled[] = $revenue_id;
|
||||
} else {
|
||||
$failed[] = $revenue_id;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return ['settled' => $settled, 'failed' => $failed];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,7 +185,7 @@ class Revenue {
|
||||
|
||||
$limit = max(1, min(500, $limit));
|
||||
$ids = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE status = %s AND created_at < %s ORDER BY id ASC LIMIT %d",
|
||||
"SELECT id FROM {$this->table()} WHERE status = %s AND created_at <= %s ORDER BY id ASC LIMIT %d",
|
||||
'pending',
|
||||
$cutoff,
|
||||
$limit
|
||||
@@ -396,6 +405,15 @@ class Revenue {
|
||||
$where[] = 'currency = %s';
|
||||
$params[] = $currency;
|
||||
}
|
||||
$order_status = sanitize_key((string) ($args['order_status'] ?? ''));
|
||||
$valid_order_statuses = class_exists('\\AKSRT\\Core\\DomainModel')
|
||||
? \AKSRT\Core\DomainModel::order_status_labels()
|
||||
: [];
|
||||
$orders_table = \AKSRT\Core\DBMigrator::table('order');
|
||||
if ($orders_table !== '' && isset($valid_order_statuses[$order_status])) {
|
||||
$where[] = "EXISTS (SELECT 1 FROM {$orders_table} AS income_order WHERE income_order.order_number = {$this->table()}.order_number AND income_order.status = %s)";
|
||||
$params[] = $order_status;
|
||||
}
|
||||
$order_number = sanitize_text_field((string) ($args['order_number'] ?? ''));
|
||||
if ($order_number !== '') {
|
||||
$where[] = 'order_number LIKE %s';
|
||||
|
||||
@@ -83,14 +83,14 @@ class UserRuntime {
|
||||
*/
|
||||
public static function get_user_center_settings(): array {
|
||||
$settings = Settings::get_all();
|
||||
$raw_tabs = $settings['user_center_tabs'] ?? 'profile,level,posts,comments,credits,revenue,favorites,auth';
|
||||
$raw_tabs = $settings['user_center_tabs'] ?? 'profile,level,comments,credits,revenue,favorites,auth';
|
||||
$tabs = is_array($raw_tabs)
|
||||
? $raw_tabs
|
||||
: preg_split('/[\s,]+/', (string) $raw_tabs, -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
// Built-in switchable panels. VIP is always auto-injected when enabled
|
||||
// by AKSRT\User\Center — keep it allowed for explicit config too.
|
||||
$allowed_tabs = ['profile', 'level', 'posts', 'comments', 'favorites', 'credits', 'revenue', 'vip', 'orders', 'auth', 'settings'];
|
||||
$allowed_tabs = ['profile', 'level', 'comments', 'favorites', 'credits', 'revenue', 'vip', 'orders', 'auth', 'settings'];
|
||||
$normalized_tabs = ['profile'];
|
||||
|
||||
foreach ((array) $tabs as $tab) {
|
||||
@@ -104,8 +104,8 @@ class UserRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_array('posts', $normalized_tabs, true) && !in_array('comments', $normalized_tabs, true)) {
|
||||
$normalized_tabs[] = 'posts';
|
||||
if (!in_array('comments', $normalized_tabs, true)) {
|
||||
$normalized_tabs[] = 'comments';
|
||||
}
|
||||
|
||||
// Ensure VIP appears in configured tabs when membership is on, even if
|
||||
|
||||
+8
-11
@@ -116,7 +116,7 @@ class Center {
|
||||
$settings = class_exists('\AKSRT\Settings\Settings')
|
||||
? \AKSRT\Settings\Settings::get_user_center_settings()
|
||||
: [
|
||||
'tabs' => ['profile', 'posts', 'comments', 'favorites'],
|
||||
'tabs' => ['profile', 'comments', 'favorites'],
|
||||
'avatar_upload' => true,
|
||||
'cover_upload' => true,
|
||||
'social_links' => true,
|
||||
@@ -135,12 +135,6 @@ class Center {
|
||||
'priority' => 15,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'posts' => [
|
||||
'label' => __('我的文章', 'aksrt'),
|
||||
'icon' => 'file-alt',
|
||||
'priority' => 20,
|
||||
'type' => 'panel',
|
||||
],
|
||||
'creator' => [
|
||||
'label' => __('创作中心', 'aksrt'),
|
||||
'icon' => 'feather-alt',
|
||||
@@ -206,9 +200,12 @@ class Center {
|
||||
];
|
||||
|
||||
$this->panels = [];
|
||||
$configured = array_values(array_filter(array_map('sanitize_key', (array) ($settings['tabs'] ?? []))));
|
||||
$configured = array_values(array_filter(
|
||||
array_map('sanitize_key', (array) ($settings['tabs'] ?? [])),
|
||||
static fn(string $key): bool => $key !== 'posts'
|
||||
));
|
||||
if ($configured === []) {
|
||||
$configured = ['profile', 'posts', 'comments', 'favorites'];
|
||||
$configured = ['profile', 'comments', 'favorites'];
|
||||
}
|
||||
|
||||
$level_on = class_exists('\AKSRT\User\Level')
|
||||
@@ -246,7 +243,7 @@ class Center {
|
||||
|
||||
if ($vip_on && !in_array('vip', $order, true)) {
|
||||
$insert_at = count($order);
|
||||
foreach (['credits', 'favorites', 'posts'] as $anchor) {
|
||||
foreach (['credits', 'favorites'] as $anchor) {
|
||||
$idx = array_search($anchor, $order, true);
|
||||
if ($idx !== false) {
|
||||
$insert_at = (int) $idx + 1;
|
||||
@@ -278,7 +275,7 @@ class Center {
|
||||
|
||||
if ($revenue_tab_on && !in_array('revenue', $order, true)) {
|
||||
$insert_at = count($order);
|
||||
foreach (['credits', 'favorites', 'posts'] as $anchor) {
|
||||
foreach (['credits', 'favorites'] as $anchor) {
|
||||
$idx = array_search($anchor, $order, true);
|
||||
if ($idx !== false) {
|
||||
$insert_at = (int) $idx + 1;
|
||||
|
||||
@@ -13,6 +13,7 @@ class Metrics {
|
||||
public const META_COMMENT_LIKE_SUM = 'aksrt_comment_like_sum';
|
||||
public const META_POSTS_VIEWS_SUM = 'aksrt_posts_views_sum';
|
||||
public const META_PAY_INCOME = 'aksrt_pay_income_sum';
|
||||
public const META_PAY_INCOME_POINTS = 'aksrt_pay_income_points_sum';
|
||||
public const META_PAY_REBATE = 'aksrt_pay_rebate_sum';
|
||||
public const META_CHECKIN_ALL_DAY = 'aksrt_checkin_all_day';
|
||||
|
||||
@@ -132,13 +133,14 @@ class Metrics {
|
||||
* @param array $context
|
||||
*/
|
||||
public function on_creator_income_added($user_id, $amount, $context = []): void {
|
||||
unset($context);
|
||||
$currency = sanitize_key((string) ($context['currency'] ?? 'CNY'));
|
||||
$user_id = (int) $user_id;
|
||||
$amount = (float) $amount;
|
||||
if ($user_id <= 0 || $amount <= 0) {
|
||||
return;
|
||||
}
|
||||
self::bump_user_meta($user_id, self::META_PAY_INCOME, $amount);
|
||||
$meta_key = $currency === 'points' ? self::META_PAY_INCOME_POINTS : self::META_PAY_INCOME;
|
||||
self::bump_user_meta($user_id, $meta_key, $amount);
|
||||
self::reevaluate_medals($user_id);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user