diff --git a/.specs/plans/user-creator-income-center.md b/.specs/plans/user-creator-income-center.md index 36858e1c..821101a0 100644 --- a/.specs/plans/user-creator-income-center.md +++ b/.specs/plans/user-creator-income-center.md @@ -28,12 +28,12 @@ ### Done when -- [ ] `src/Core/DBMigratorPart2.php` 或最终负责收益表 schema 的文件包含作者、订单、文章、购买者、货币、原始支付金额、比例快照、会员等级、作者收益金额、状态和时间字段,并同步提升 schema 版本。 -- [ ] `src/Payment/Revenue.php` 或拆分后的作者收益服务实现 `create_from_paid_order(order)`,且同一订单重复调用只保留一条作者收益记录。 -- [ ] 现金订单仅创建现金收益记录,积分订单仅创建积分收益记录;作者购买自己的付费文章不会创建收益记录。 -- [ ] 普通用户、一级会员、二级会员分别按 60%、70%、80% 计算,并把生成时比例保存到收益记录。 -- [ ] 支付成功但权益未完成时不创建收益;权益完成后的支付流程能够创建待结算记录。 -- [ ] 创作分成关闭时,收益生成入口被跳过;相关收益服务测试覆盖上述场景。 +- [x] `src/Core/DBMigratorPart2.php` 或最终负责收益表 schema 的文件包含作者、订单、文章、购买者、货币、原始支付金额、比例快照、会员等级、作者收益金额、状态和时间字段,并同步提升 schema 版本。 +- [x] `src/Payment/Revenue.php` 或拆分后的作者收益服务实现 `create_from_paid_order(order)`,且同一订单重复调用只保留一条作者收益记录。 +- [x] 现金订单仅创建现金收益记录,积分订单仅创建积分收益记录;作者购买自己的付费文章不会创建收益记录。 +- [x] 普通用户、一级会员、二级会员分别按 60%、70%、80% 计算,并把生成时比例保存到收益记录。 +- [x] 支付成功但权益未完成时不创建收益;权益完成后的支付流程能够创建待结算记录。 +- [x] 创作分成关闭时,收益生成入口被跳过;相关收益服务测试覆盖上述场景。 --- @@ -47,11 +47,11 @@ ### Done when -- [ ] `src/Payment/Revenue.php` 或拆分后的结算服务实现 `settle(record_ids, actor)`,现金和积分分别调用对应原子余额服务并写入对应流水。 -- [ ] `settlement scheduler` 仅在自动模式下处理达到配置延迟时间的待结算记录,默认延迟 24 小时;手动模式下定时任务不入账。 -- [ ] 同一收益被重复结算、并发结算或重复定时扫描时,只能成功入账一次。 -- [ ] `Refund reversal handler` 实现 `cancel_or_reverse(order_id, reason)`:未入钱包记录变为已取消/退款,已入钱包记录按货币执行冲正。 -- [ ] 冲正时余额不足不会产生负余额,收益记录转为待追缴,并保存应冲正金额、错误信息和审计信息。 +- [x] `src/Payment/Revenue.php` 或拆分后的结算服务实现 `settle(record_ids, actor)`,现金和积分分别调用对应原子余额服务并写入对应流水。 +- [x] `settlement scheduler` 仅在自动模式下处理达到配置延迟时间的待结算记录,默认延迟 24 小时;手动模式下定时任务不入账。 +- [x] 同一收益被重复结算、并发结算或重复定时扫描时,只能成功入账一次。 +- [x] `Refund reversal handler` 实现 `cancel_or_reverse(order_id, reason)`:未入钱包记录变为已取消/退款,已入钱包记录按货币执行冲正。 +- [x] 冲正时余额不足不会产生负余额,收益记录转为待追缴,并保存应冲正金额、错误信息和审计信息。 - [ ] 重复退款通知和重复冲正请求保持幂等;服务测试覆盖自动结算、手动结算、退款前结算、退款后结算和余额不足场景。 --- diff --git a/assets/admin/src/schema/sections/payment/revenue.js b/assets/admin/src/schema/sections/payment/revenue.js index fb43c3d1..22ac339d 100644 --- a/assets/admin/src/schema/sections/payment/revenue.js +++ b/assets/admin/src/schema/sections/payment/revenue.js @@ -10,6 +10,8 @@ export const revenueSchema = { ratio_vip_2: 0, payment_revenue_settlement_threshold: 100, payment_revenue_cycle_days: 30, + payment_revenue_settlement_mode: 'automatic', + payment_revenue_settlement_delay_hours: 24, payment_revenue_points_rule: '', payment_revenue_note: '分成规则已就绪,订单入账后自动按比例结算。', pay_income_desc_details: '', @@ -27,6 +29,8 @@ export const revenueSchema = { { key: 'ratio_vip_2', label: 'VIP2分成比例(%)', type: 'number', default: 0, min: 0, max: 100, dependsOn: { key: 'pay_income_s', value: true } }, { key: 'payment_revenue_settlement_threshold', label: '最小结算金额', type: 'number', default: 100, min: 0, max: 100000, dependsOn: { key: 'pay_income_s', value: true } }, { key: 'payment_revenue_cycle_days', label: '结算周期(天)', type: 'number', default: 30, min: 1, max: 365, dependsOn: { key: 'pay_income_s', value: true } }, + { key: 'payment_revenue_settlement_mode', label: '结算模式', type: 'select', default: 'automatic', options: [{ value: 'automatic', label: '自动结算' }, { value: 'manual', label: '手动结算' }], dependsOn: { key: 'pay_income_s', value: true } }, + { key: 'payment_revenue_settlement_delay_hours', label: '结算延迟(小时)', type: 'number', default: 24, min: 0, max: 8760, dependsOn: { key: 'pay_income_s', value: true } }, { key: 'payment_revenue_points_rule', label: '积分规则', type: 'textarea', default: '', dependsOn: { key: 'pay_income_s', value: true } }, { key: 'payment_revenue_note', label: '结算备注', type: 'textarea', default: '分成策略和台账结构已就绪,订单入账后自动按比例结算。', dependsOn: { key: 'pay_income_s', value: true } }, ], diff --git a/assets/css/routes/profile/lists.css b/assets/css/routes/profile/lists.css index 73adffe3..70a1b809 100644 --- a/assets/css/routes/profile/lists.css +++ b/assets/css/routes/profile/lists.css @@ -709,6 +709,10 @@ border-bottom: 0; } +/* ==================== 作者收益中心 ==================== */ +.creator-income { display:flex; flex-direction:column; gap:16px; }.creator-income__hero { padding:24px 26px; border-radius:var(--radius-lg); color:#fff; background:linear-gradient(135deg,var(--color-primary),#7c3aed); }.creator-income__hero p { margin:0 0 6px; opacity:.82; font-size:13px; }.creator-income__hero h2 { margin:0; color:inherit; }.creator-income__tabs { display:flex; gap:8px; flex-wrap:wrap; }.creator-income__tab { padding:7px 12px; border-radius:var(--radius-pill); color:var(--color-ink-3); text-decoration:none; }.creator-income__tab.is-active,.creator-income__tab:hover { color:var(--color-primary); background:var(--color-primary-soft); }.creator-income__cards { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:12px; }.creator-income-card { padding:18px; border:1px solid var(--color-line); border-radius:var(--radius-lg); background:var(--surface-1); }.creator-income-card h3 { margin:0 0 14px; }.creator-income-card__metrics { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:10px; }.creator-income-card__metrics strong,.creator-income-card__metrics small { display:block; }.creator-income-card__metrics strong { color:var(--color-ink); font-size:18px; }.creator-income-card__metrics small { margin-top:4px; color:var(--color-ink-4); font-size:11px; }.creator-income__info,.creator-income__details { padding:20px; }.creator-income__info h3,.creator-income__toolbar h3 { margin:0; }.creator-income__info div,.creator-income__toolbar p { margin:6px 0 0; color:var(--color-ink-4); font-size:13px; }.creator-income__toolbar { display:flex; justify-content:space-between; align-items:flex-end; gap:12px; margin-bottom:14px; }.creator-income__table-wrap { overflow-x:auto; }.creator-income__table { width:100%; border-collapse:collapse; font-size:12px; }.creator-income__table th,.creator-income__table td { padding:10px 8px; border-bottom:1px solid var(--color-line); text-align:left; white-space:nowrap; }.creator-income-status { padding:3px 7px; border-radius:var(--radius-pill); color:var(--color-primary); background:var(--color-primary-soft); }.creator-income-status--cancelled,.creator-income-status--reversed { color:var(--color-ink-4); background:var(--surface-2); }.creator-income-status--pending_recovery { color:var(--color-error); background:rgba(220,38,38,.08); }.creator-income__empty { padding:40px 20px; text-align:center; color:var(--color-ink-4); }.creator-income__empty i { font-size:28px; color:var(--color-primary); } .creator-income__details select { margin-left:6px; } +@media (max-width:767px) { .creator-income__cards { grid-template-columns:1fr; }.creator-income-card__metrics { grid-template-columns:repeat(2,minmax(0,1fr)); }.creator-income__toolbar { align-items:flex-start; flex-direction:column; } } + /* ==================== 独立创作中心工作台 ==================== */ .creator-workspace { display: flex; flex-direction: column; gap: 16px; } .creator-workspace__hero { display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 24px 26px; border-radius: var(--radius-lg); color: #fff; background: linear-gradient(135deg, var(--color-primary), #7c3aed); } diff --git a/assets/js/routes/user-center.js b/assets/js/routes/user-center.js index ebef7126..5a48424e 100644 --- a/assets/js/routes/user-center.js +++ b/assets/js/routes/user-center.js @@ -79,6 +79,12 @@ 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); + ['income_currency', 'income_status', 'income_page'].forEach(function(key) { + if (params.get(key)) url += (url.indexOf('?') === -1 ? '?' : '&') + key + '=' + encodeURIComponent(params.get(key)); + }); + } target._aksrtPanelRequest = fetch(url, { credentials: 'same-origin', headers: { 'X-WP-Nonce': (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || '' } @@ -699,36 +705,6 @@ }); } - /* ==================== 创作中心文章操作 ==================== */ - function initCreatorPostActions() { - document.addEventListener('click', function(e) { - var btn = e.target.closest('[data-action="delete-post"]'); - if (!btn || btn.disabled) return; - if (!confirm(i18n.confirmDeletePost || '确定删除这篇文章?')) return; - var postId = btn.getAttribute('data-post-id'); - var nonce = btn.getAttribute('data-nonce') || submitNonce; - if (!postId || !nonce) return; - btn.disabled = true; - var body = new URLSearchParams(); - body.set('action', 'aksrt_delete_post'); - body.set('id', postId); - body.set('_nonce', nonce); - fetch(ajaxUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, credentials: 'same-origin', body: body.toString() }) - .then(function(r) { return r.json(); }) - .then(function(res) { - if (res && res.success) { - var item = btn.closest('.profile-list-item, .creator-post-row'); - if (item) item.remove(); - showToast((res.data && res.data.message) || '文章已经删除'); - } else { - btn.disabled = false; - showToast((res && res.data && res.data.message) || i18n.operationFailed || '操作失败'); - } - }) - .catch(function() { btn.disabled = false; showToast(i18n.networkError || '网络错误'); }); - }); - } - if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function() { @@ -736,7 +712,6 @@ initProfileForm(); initAuthForm(); initUnfavorite(); - initCreatorPostActions(); initNotifications(); initSettings(); }); @@ -745,8 +720,7 @@ initProfileForm(); initAuthForm(); initUnfavorite(); - initCreatorPostActions(); - initNotifications(); + initNotifications(); initSettings(); } diff --git a/functions.php b/functions.php index d5b0c8df..a97eb615 100644 --- a/functions.php +++ b/functions.php @@ -111,7 +111,7 @@ add_filter('query_vars', function ($vars) { // 主题切换离开时:清理本主题注册的 cron 任务 add_action('switch_theme', function () { - $hooks = ['aksrt_dl_token_cleanup', 'aksrt_email_verification_backfill_batch', 'aksrt_notification_cleanup']; + $hooks = ['aksrt_dl_token_cleanup', 'aksrt_email_verification_backfill_batch', 'aksrt_notification_cleanup', 'aksrt_creator_income_settlement']; foreach ($hooks as $hook) { $next = wp_next_scheduled($hook); while ($next) { diff --git a/inc/helpers/user-center-renderers/profile-creator.php b/inc/helpers/user-center-renderers/profile-creator.php index 3583f49b..d109796b 100644 --- a/inc/helpers/user-center-renderers/profile-creator.php +++ b/inc/helpers/user-center-renderers/profile-creator.php @@ -1,6 +1,6 @@ can_submit((int) $user_id); -$counts = isset($user_status_counts) && is_array($user_status_counts) ? $user_status_counts : []; -$total = array_sum(array_map('intval', $counts)); -$creator_filter = isset($_GET['creator_status']) ? sanitize_key(wp_unslash($_GET['creator_status'])) : 'all'; -if (!in_array($creator_filter, ['all', 'publish', 'draft', 'pending', 'private'], true)) { - $creator_filter = 'all'; +$user_id = (int) ($context['user_id'] ?? get_current_user_id()); +if ($user_id <= 0) { + echo '