fix(polish): 订单购物车论坛粉边软化 + 论坛付费帖购链路

- 购物车/商品/论坛表面复用 #77 soft recipe(mix 边 + soft ring)
- 论坛 visibility=pay:订单 resource + 余额扣费闭环
- VIP 价/免费解锁 + 购买 CTA/AJAX,复用现有 Order/Credits

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-12 13:34:03 +08:00
co-authored by Claude Fable 5
parent de204c4794
commit 043ac178fe
21 changed files with 411 additions and 52 deletions
+2 -2
View File
@@ -224,7 +224,7 @@
.aksrt-forum-board__application-reason { .aksrt-forum-board__application-reason {
margin: 10px 0; margin: 10px 0;
padding: 10px 12px; padding: 10px 12px;
border-left: 3px solid var(--color-primary, #1890ff); border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent);
background: var(--surface-2, #f8fafc); background: var(--surface-2, #f8fafc);
color: var(--color-ink-3, #64748b); color: var(--color-ink-3, #64748b);
font-size: 13px; font-size: 13px;
@@ -240,7 +240,7 @@
padding: 12px 16px; padding: 12px 16px;
background: var(--surface-1, #fff); background: var(--surface-1, #fff);
border-radius: 8px; border-radius: 8px;
border-left: 3px solid var(--color-primary); border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent);
} }
.aksrt-forum-board__sticky h3 { .aksrt-forum-board__sticky h3 {
+3 -2
View File
@@ -489,8 +489,9 @@
} }
.aksrt-forum-compose__draft:hover { .aksrt-forum-compose__draft:hover {
border-color: var(--color-primary); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: var(--color-primary); color: var(--color-primary);
background: var(--color-primary-softer, color-mix(in srgb, var(--color-primary) 6%, transparent));
} }
/* ── topic cover (§7.3) ── */ /* ── topic cover (§7.3) ── */
@@ -646,7 +647,7 @@
} }
.aksrt-forum-compose__cover-btn:hover { .aksrt-forum-compose__cover-btn:hover {
border-color: var(--color-primary); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: var(--color-primary); color: var(--color-primary);
background: var(--color-primary-soft); background: var(--color-primary-soft);
} }
@@ -43,7 +43,7 @@
} }
.aksrt-forum-reply.is-pinned { .aksrt-forum-reply.is-pinned {
border-left: 3px solid #1890ff; border-left: 3px solid color-mix(in srgb, var(--color-primary) 55%, transparent);
} }
.aksrt-forum-reply.is-pinned.is-hot { .aksrt-forum-reply.is-pinned.is-hot {
+6 -6
View File
@@ -253,7 +253,7 @@
.aksrt-forum-topic__badge--question { .aksrt-forum-topic__badge--question {
background: #e6f7ff; background: #e6f7ff;
color: #1890ff; color: var(--color-primary);
} }
.aksrt-forum-topic__badge--solved { .aksrt-forum-topic__badge--solved {
@@ -263,7 +263,7 @@
.aksrt-forum-topic-item__badge--question { .aksrt-forum-topic-item__badge--question {
background: #e6f7ff; background: #e6f7ff;
color: #1890ff; color: var(--color-primary);
} }
.aksrt-forum-topic-item__badge--solved { .aksrt-forum-topic-item__badge--solved {
@@ -328,8 +328,8 @@
} }
.aksrt-forum-topic__score-btn:hover:not(:disabled) { .aksrt-forum-topic__score-btn:hover:not(:disabled) {
border-color: var(--color-primary, #1890ff); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: var(--color-primary, #1890ff); color: var(--color-primary);
} }
.aksrt-forum-topic__score-btn--up:hover:not(:disabled) { .aksrt-forum-topic__score-btn--up:hover:not(:disabled) {
@@ -372,7 +372,7 @@
.aksrt-forum-topic__vote-type-badge { .aksrt-forum-topic__vote-type-badge {
display: inline-block; display: inline-block;
padding: 1px 6px; padding: 1px 6px;
background: var(--color-primary, #1890ff); background: var(--color-primary);
color: #fff; color: #fff;
border-radius: var(--radius-pill, 999px); border-radius: var(--radius-pill, 999px);
font-size: 11px; font-size: 11px;
@@ -401,7 +401,7 @@
.aksrt-forum-topic__chip--topic-cat { .aksrt-forum-topic__chip--topic-cat {
background: var(--color-primary-bg, #e6f7ff); background: var(--color-primary-bg, #e6f7ff);
color: var(--color-primary, #1890ff); color: var(--color-primary);
font-weight: 600; font-weight: 600;
} }
@@ -14,11 +14,11 @@
} }
.aksrt-forum-reply__like-btn:hover { .aksrt-forum-reply__like-btn:hover {
color: var(--c-primary, #1890ff); color: var(--color-primary);
} }
.aksrt-forum-reply__like-btn.is-active { .aksrt-forum-reply__like-btn.is-active {
color: var(--c-primary, #1890ff); color: var(--color-primary);
} }
.aksrt-forum-reply__like-count { .aksrt-forum-reply__like-count {
@@ -17,7 +17,7 @@
} }
.aksrt-forum-replies__more-btn:hover { .aksrt-forum-replies__more-btn:hover {
border-color: var(--c-primary, #1890ff); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: var(--c-primary, #1890ff); color: var(--color-primary);
} }
@@ -19,8 +19,8 @@
} }
.aksrt-forum-quick-post__input:hover { .aksrt-forum-quick-post__input:hover {
border-color: var(--c-primary, #1890ff); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
box-shadow: 0 0 0 2px rgba(24, 144, 255, .1); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
} }
.aksrt-forum-quick-post__input img { .aksrt-forum-quick-post__input img {
@@ -16,7 +16,7 @@
.aksrt-forum-reply__reply-btn:hover, .aksrt-forum-reply__reply-btn:hover,
.aksrt-forum-reply__top-btn:hover { .aksrt-forum-reply__top-btn:hover {
color: var(--c-primary, #1890ff); color: var(--color-primary);
} }
/* ── §7.3 Hidden content block [aksrt_hide] ── */ /* ── §7.3 Hidden content block [aksrt_hide] ── */
@@ -80,13 +80,13 @@
.aksrt-forum-home-tabs__btn:hover { .aksrt-forum-home-tabs__btn:hover {
color: var(--c-text-1, #333); color: var(--c-text-1, #333);
border-color: var(--c-primary, #1890ff); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
} }
.aksrt-forum-home-tabs__btn.is-active { .aksrt-forum-home-tabs__btn.is-active {
background: var(--c-primary, #1890ff); background: var(--color-primary);
color: #fff; color: #fff;
border-color: var(--c-primary, #1890ff); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
} }
.aksrt-forum-home-tabs__badge { .aksrt-forum-home-tabs__badge {
@@ -30,13 +30,13 @@
.aksrt-forum-replies__tab:hover { .aksrt-forum-replies__tab:hover {
color: var(--c-text-1, #333); color: var(--c-text-1, #333);
border-color: var(--c-primary, #1890ff); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
} }
.aksrt-forum-replies__tab.is-active { .aksrt-forum-replies__tab.is-active {
background: var(--c-primary, #1890ff); background: var(--color-primary);
color: #fff; color: #fff;
border-color: var(--c-primary, #1890ff); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
} }
.aksrt-forum-replies__filter { .aksrt-forum-replies__filter {
+4 -1
View File
@@ -266,7 +266,10 @@
} }
.aksrt-forum-topic__vote-option.is-selected { .aksrt-forum-topic__vote-option.is-selected {
outline: 1px solid var(--color-primary); outline: none;
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
background: var(--color-primary-soft, color-mix(in srgb, var(--color-primary) 12%, transparent));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
} }
.aksrt-forum-topic__vote-count { .aksrt-forum-topic__vote-count {
+2 -1
View File
@@ -17,7 +17,8 @@
} }
.aksrt-forum-topic-row:hover { .aksrt-forum-topic-row:hover {
border-color: var(--color-primary); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
box-shadow: var(--shadow-sm);
} }
.aksrt-forum-topic-row__avatar img { .aksrt-forum-topic-row__avatar img {
+7 -6
View File
@@ -87,7 +87,7 @@
.aksrt-product-card:hover { .aksrt-product-card:hover {
transform: translateY(-3px); transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-color: var(--color-primary, var(--color-primary)); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
} }
.aksrt-product-card__link { .aksrt-product-card__link {
@@ -205,13 +205,13 @@
} }
.aksrt-product-archive__pagination .page-numbers:hover { .aksrt-product-archive__pagination .page-numbers:hover {
border-color: var(--color-primary, var(--color-primary)); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: var(--color-primary, var(--color-primary)); color: var(--color-primary, var(--color-primary));
} }
.aksrt-product-archive__pagination .page-numbers.current { .aksrt-product-archive__pagination .page-numbers.current {
background: var(--color-primary, var(--color-primary)); background: var(--color-primary, var(--color-primary));
border-color: var(--color-primary, var(--color-primary)); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: #fff; color: #fff;
} }
@@ -506,7 +506,7 @@
} }
.aksrt-cart__qty-btn:hover { .aksrt-cart__qty-btn:hover {
border-color: var(--color-primary, var(--color-primary)); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
} }
.aksrt-cart__item-subtotal { .aksrt-cart__item-subtotal {
@@ -675,7 +675,8 @@
.aksrt-product-edit__field input:focus, .aksrt-product-edit__field input:focus,
.aksrt-product-edit__field textarea:focus { .aksrt-product-edit__field textarea:focus {
outline: none; outline: none;
border-color: var(--color-primary, var(--color-primary)); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
} }
.aksrt-product-edit__field textarea { .aksrt-product-edit__field textarea {
@@ -717,7 +718,7 @@
} }
.aksrt-product-edit__actions .btn-ghost:hover { .aksrt-product-edit__actions .btn-ghost:hover {
border-color: var(--color-primary, var(--color-primary)); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
} }
.aksrt-product-archive__toolbar{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 16px} .aksrt-product-archive__toolbar{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 16px}
+9
View File
@@ -389,3 +389,12 @@
gap: 12px; gap: 12px;
padding-top: 8px; padding-top: 8px;
} }
/* Soft focus ring (#77 recipe) — avoid hard primary stroke */
.aksrt-cart-page__input:focus,
.aksrt-cart-page__textarea:focus,
.aksrt-cart-page__qty-btn:focus-visible {
outline: none;
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
}
+2 -2
View File
@@ -43,13 +43,13 @@
.aksrt-commerce-header__nav-item:hover { .aksrt-commerce-header__nav-item:hover {
color: var(--color-primary, #f04494); color: var(--color-primary, #f04494);
border-color: color-mix(in srgb, var(--color-primary, #f04494) 35%, transparent); border-color: color-mix(in srgb, var(--color-primary, #f04494) 45%, transparent);
} }
.aksrt-commerce-header__nav-item.is-active { .aksrt-commerce-header__nav-item.is-active {
color: var(--color-on-primary, #fff); color: var(--color-on-primary, #fff);
background: var(--color-primary, #f04494); background: var(--color-primary, #f04494);
border-color: var(--color-primary, #f04494); border-color: transparent;
} }
/* Soften revenue dashboard cards — task-first, not ops console */ /* Soften revenue dashboard cards — task-first, not ops console */
+6 -6
View File
@@ -100,7 +100,7 @@
.aksrt-product-page__thumb:hover, .aksrt-product-page__thumb:hover,
.aksrt-product-page__thumb.is-active { .aksrt-product-page__thumb.is-active {
border-color: var(--color-primary); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
} }
.aksrt-product-page__thumb img { .aksrt-product-page__thumb img {
@@ -227,8 +227,8 @@
.aksrt-product-editor__input:focus, .aksrt-product-editor__input:focus,
.aksrt-product-editor__textarea:focus { .aksrt-product-editor__textarea:focus {
outline: none; outline: none;
border-color: var(--color-primary); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
box-shadow: 0 0 0 3px var(--color-primary-soft); box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 18%, transparent);
} }
/* ===== Quantity controls ===== */ /* ===== Quantity controls ===== */
@@ -244,7 +244,7 @@
} }
.aksrt-product-page__qty-btn:hover { .aksrt-product-page__qty-btn:hover {
border-color: var(--color-primary); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
background: var(--color-primary-softer); background: var(--color-primary-softer);
} }
@@ -425,13 +425,13 @@ html.dark .aksrt-product-page__content img {
} }
.aksrt-product-page__spec-btn:hover { .aksrt-product-page__spec-btn:hover {
border-color: var(--color-primary); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: var(--color-primary); color: var(--color-primary);
} }
.aksrt-product-page__spec-btn.is-active { .aksrt-product-page__spec-btn.is-active {
background: var(--color-primary-soft); background: var(--color-primary-soft);
border-color: var(--color-primary); border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
color: var(--color-primary); color: var(--color-primary);
font-weight: 600; font-weight: 600;
} }
+44
View File
@@ -675,6 +675,49 @@
}); });
} }
function bindPurchaseButton(button) {
button.addEventListener('click', function () {
var topicId = parseInt(button.getAttribute('data-topic-id') || '0', 10);
var price = button.getAttribute('data-price') || '0';
var isFree = button.getAttribute('data-free') === '1';
var feedback = button.parentElement ? button.parentElement.querySelector('.js-aksrt-action-feedback') : null;
if (!topicId) {
return;
}
var confirmMsg = isFree
? '确认使用 VIP 免费解锁此主题?'
: ('确认使用余额支付 ¥' + price + ' 购买此主题?');
if (!window.confirm(confirmMsg)) {
return;
}
button.disabled = true;
setFeedback(feedback, isFree ? '正在解锁…' : '正在支付…', 'is-loading');
var data = new FormData();
data.append('action', 'aksrt_forum_purchase_topic');
data.append('topic_id', topicId);
data.append('_nonce', rootData.forumNonce || '');
fetch((rootData.ajaxUrl || '/wp-admin/admin-ajax.php'), {
method: 'POST',
body: data,
credentials: 'same-origin',
}).then(function (r) { return r.json(); }).then(function (payload) {
if (!payload.success) {
throw new Error((payload.data && payload.data.message) || '购买失败');
}
setFeedback(feedback, '购买成功,正在刷新…', 'is-success');
setTimeout(function () { window.location.reload(); }, 800);
}).catch(function (error) {
setFeedback(feedback, error.message || '购买失败', 'is-error');
button.disabled = false;
});
});
}
function bindComposeCoverToggle(form) { function bindComposeCoverToggle(form) {
var coverTypeSelect = form.querySelector('#compose-cover-type'); var coverTypeSelect = form.querySelector('#compose-cover-type');
var imageField = form.querySelector('#compose-cover-image-field'); var imageField = form.querySelector('#compose-cover-image-field');
@@ -1411,6 +1454,7 @@
document.querySelectorAll('.js-aksrt-topic-score').forEach(bindScoreButton); document.querySelectorAll('.js-aksrt-topic-score').forEach(bindScoreButton);
document.querySelectorAll('.js-aksrt-adopt-answer').forEach(bindAdoptButton); document.querySelectorAll('.js-aksrt-adopt-answer').forEach(bindAdoptButton);
document.querySelectorAll('.js-aksrt-unlock-topic').forEach(bindUnlockButton); document.querySelectorAll('.js-aksrt-unlock-topic').forEach(bindUnlockButton);
document.querySelectorAll('.js-aksrt-purchase-topic').forEach(bindPurchaseButton);
bindForumCommentEngine(); bindForumCommentEngine();
bindForumHomeTabs(); bindForumHomeTabs();
bindBoardSortTabs(); bindBoardSortTabs();
+7
View File
@@ -33,6 +33,13 @@ class AKSRT_Credits {
return is_numeric($val) ? (int) $val : 0; return is_numeric($val) ? (int) $val : 0;
} }
/**
* Alias used by forum unlock/purchase paths.
*/
public static function get_balance(int $user_id): int {
return self::balance($user_id);
}
/** /**
* 增加积分。 * 增加积分。
* *
+28
View File
@@ -144,6 +144,13 @@ class AKSRT_Forum {
return ['enabled' => false]; return ['enabled' => false];
} }
/**
* Alias kept for topic-service helpers that still call get_forum_config().
*/
public function get_forum_config(): array {
return $this->get_config();
}
public function get_boards(array $args = []): array { public function get_boards(array $args = []): array {
return $this->board_service()->get_boards($args); return $this->board_service()->get_boards($args);
} }
@@ -345,6 +352,7 @@ class AKSRT_Forum {
add_action('wp_ajax_aksrt_forum_create_topic', [$this, 'ajax_create_topic']); add_action('wp_ajax_aksrt_forum_create_topic', [$this, 'ajax_create_topic']);
add_action('wp_ajax_aksrt_forum_follow_plate', [$this, 'ajax_follow_plate']); add_action('wp_ajax_aksrt_forum_follow_plate', [$this, 'ajax_follow_plate']);
add_action('wp_ajax_aksrt_forum_unlock_topic', [$this, 'ajax_unlock_topic']); add_action('wp_ajax_aksrt_forum_unlock_topic', [$this, 'ajax_unlock_topic']);
add_action('wp_ajax_aksrt_forum_purchase_topic', [$this, 'ajax_purchase_topic']);
// §7.4 draft persistence // §7.4 draft persistence
add_action('wp_ajax_aksrt_forum_save_draft', [$this, 'ajax_save_draft']); add_action('wp_ajax_aksrt_forum_save_draft', [$this, 'ajax_save_draft']);
@@ -446,6 +454,26 @@ class AKSRT_Forum {
$this->topic_service()->ajax_unlock_topic(); $this->topic_service()->ajax_unlock_topic();
} }
public function ajax_purchase_topic(): void {
$this->topic_service()->ajax_purchase_topic();
}
/**
* Lightweight result helper for topic service actions.
*
* @return array{success:bool,message:string}
*/
public function success(string $message = '', array $extra = []): array {
return array_merge(['success' => true, 'message' => $message], $extra);
}
/**
* @return array{success:bool,message:string}
*/
public function error(string $message = '', array $extra = []): array {
return array_merge(['success' => false, 'message' => $message], $extra);
}
public function apply_moderator(int $user_id, int $board_id, string $reason) { public function apply_moderator(int $user_id, int $board_id, string $reason) {
return $this->moderation_service()->apply_moderator($user_id, $board_id, $reason); return $this->moderation_service()->apply_moderator($user_id, $board_id, $reason);
} }
@@ -188,7 +188,7 @@ trait AKSRT_Forum_Topic_Service_Part_3 {
if ($viewer_user_id <= 0) { if ($viewer_user_id <= 0) {
return false; return false;
} }
return $this->has_user_unlocked_topic($viewer_user_id, (int) $post->ID); return $this->has_user_paid_topic($viewer_user_id, (int) $post->ID);
} }
if ($visibility === AKSRT_Forum::VISIBILITY_MODERATOR) { if ($visibility === AKSRT_Forum::VISIBILITY_MODERATOR) {
@@ -324,16 +324,12 @@ trait AKSRT_Forum_Topic_Service_Part_3 {
'topic_id' => $topic_id, 'topic_id' => $topic_id,
], sprintf(__('解锁帖子「%s」', 'aksrt'), get_the_title($topic_id))); ], sprintf(__('解锁帖子「%s」', 'aksrt'), get_the_title($topic_id)));
if (!$deducted) { if (is_wp_error($deducted) || $deducted === false) {
return $this->forum->error(__('积分扣除失败', 'aksrt')); $msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt');
return $this->forum->error($msg ?: __('积分扣除失败', 'aksrt'));
} }
$unlocked = get_user_meta($user_id, 'aksrt_unlocked_topics', true); $this->mark_topic_unlocked($user_id, $topic_id);
if (!is_array($unlocked)) {
$unlocked = [];
}
$unlocked[] = $topic_id;
update_user_meta($user_id, 'aksrt_unlocked_topics', array_unique($unlocked));
$author_id = (int) $post->post_author; $author_id = (int) $post->post_author;
if ($author_id > 0 && $author_id !== $user_id) { if ($author_id > 0 && $author_id !== $user_id) {
@@ -350,12 +346,211 @@ trait AKSRT_Forum_Topic_Service_Part_3 {
return $this->forum->success(__('解锁成功', 'aksrt')); return $this->forum->success(__('解锁成功', 'aksrt'));
} }
/**
* Cash/balance purchase for VISIBILITY_PAY topics.
*
* Reuses AKSRT_Order product_type=resource + balance credits conversion
* (1 CNY = 100 credits, same as AKSRT_Balance_Gateway).
*
* @return array{success:bool,message:string}
*/
public function purchase_topic_with_balance(int $user_id, int $topic_id): array {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return $this->forum->error(__('主题不存在', 'aksrt'));
}
if ($user_id <= 0) {
return $this->forum->error(__('请先登录', 'aksrt'));
}
$visibility = (string) get_post_meta($topic_id, '_aksrt_visibility', true);
if ($visibility !== AKSRT_Forum::VISIBILITY_PAY) {
return $this->forum->error(__('该主题不是付费可见内容', 'aksrt'));
}
if ($this->has_user_paid_topic($user_id, $topic_id)) {
return $this->forum->success(__('已购买', 'aksrt'));
}
$price = $this->resolve_topic_pay_price($topic_id, $user_id);
if (!empty($price['is_free'])) {
$this->mark_topic_unlocked($user_id, $topic_id);
return $this->forum->success(__('VIP 免费解锁成功', 'aksrt'), [
'amount' => 0,
'free' => true,
]);
}
$amount = (float) ($price['amount'] ?? 0);
if ($amount <= 0) {
return $this->forum->error(__('价格未设置', 'aksrt'));
}
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Credits')) {
return $this->forum->error(__('支付系统不可用', 'aksrt'));
}
$product_type = class_exists('AKSRT_Domain_Model')
? AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE
: 'resource';
$order_number = AKSRT_Order::get_instance()->create($user_id, $product_type, $topic_id, $amount, 'CNY');
if ($order_number === '') {
return $this->forum->error(__('创建订单失败', 'aksrt'));
}
// Align with AKSRT_Balance_Gateway: 1 CNY = 100 credits.
$credits_cost = (int) ceil($amount * 100);
$balance = AKSRT_Credits::get_balance($user_id);
if ($balance < $credits_cost) {
AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [
'note' => __('余额不足,自动取消', 'aksrt'),
]);
return $this->forum->error(sprintf(
__('余额不足,需要 %d 积分(约 ¥%s),当前余额 %d', 'aksrt'),
$credits_cost,
number_format($amount, 2, '.', ''),
$balance
));
}
$deducted = AKSRT_Credits::deduct(
$user_id,
$credits_cost,
'forum_topic_pay',
[
'topic_id' => $topic_id,
'order' => $order_number,
],
sprintf(__('购买帖子「%s」', 'aksrt'), get_the_title($topic_id))
);
if (is_wp_error($deducted) || $deducted === false) {
AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [
'note' => __('扣款失败,自动取消', 'aksrt'),
]);
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('扣款失败', 'aksrt');
return $this->forum->error($msg ?: __('扣款失败', 'aksrt'));
}
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => 'balance',
'payment_transaction_id' => 'BAL_' . $order_number,
'note' => sprintf(__('论坛付费主题余额支付 %d 积分', 'aksrt'), $credits_cost),
]);
if (!$paid) {
// Best-effort refund so user is not charged without access.
AKSRT_Credits::add(
$user_id,
$credits_cost,
'forum_topic_pay_refund',
['topic_id' => $topic_id, 'order' => $order_number],
sprintf(__('购买失败退款:%s', 'aksrt'), get_the_title($topic_id))
);
return $this->forum->error(__('订单确认失败,已尝试退回积分', 'aksrt'));
}
$this->mark_topic_unlocked($user_id, $topic_id);
return $this->forum->success(__('购买成功', 'aksrt'), [
'order_number' => $order_number,
'amount' => $amount,
'credits_cost' => $credits_cost,
]);
}
/**
* Resolve cash price for a pay-visibility topic.
*
* @return array{amount:float,is_vip_price:bool,is_free:bool,label:string,base:float,vip_set:bool}
*/
public function resolve_topic_pay_price(int $topic_id, int $user_id = 0): array {
$base_raw = get_post_meta($topic_id, '_aksrt_pay_price', true);
$base = is_numeric($base_raw) ? (float) $base_raw : 0.0;
$vip_raw = get_post_meta($topic_id, '_aksrt_pay_vip_price', true);
$vip_set = ($vip_raw !== '' && $vip_raw !== null && $vip_raw !== false);
$vip_price = $vip_set && is_numeric($vip_raw) ? (float) $vip_raw : 0.0;
$is_vip = $user_id > 0 && function_exists('aksrt_is_vip') && aksrt_is_vip($user_id);
if (!$is_vip && $user_id > 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($user_id)) {
$is_vip = true;
}
if ($is_vip && $vip_set) {
return [
'amount' => max(0.0, $vip_price),
'is_vip_price' => true,
'is_free' => $vip_price <= 0,
'label' => $vip_price <= 0
? __('VIP 免费', 'aksrt')
: sprintf(__('VIP ¥%s', 'aksrt'), number_format($vip_price, 2, '.', '')),
'base' => $base,
'vip_set' => true,
];
}
return [
'amount' => max(0.0, $base),
'is_vip_price' => false,
'is_free' => false,
'label' => $base > 0
? sprintf(__('¥%s', 'aksrt'), number_format($base, 2, '.', ''))
: __('价格未设置', 'aksrt'),
'base' => $base,
'vip_set' => $vip_set,
];
}
public function mark_topic_unlocked(int $user_id, int $topic_id): void {
if ($user_id <= 0 || $topic_id <= 0) {
return;
}
$unlocked = get_user_meta($user_id, 'aksrt_unlocked_topics', true);
if (!is_array($unlocked)) {
$unlocked = [];
}
$unlocked[] = $topic_id;
update_user_meta($user_id, 'aksrt_unlocked_topics', array_values(array_unique(array_map('intval', $unlocked))));
}
public function has_user_unlocked_topic(int $user_id, int $post_id): bool { public function has_user_unlocked_topic(int $user_id, int $post_id): bool {
$unlocked = get_user_meta($user_id, 'aksrt_unlocked_topics', true); $unlocked = get_user_meta($user_id, 'aksrt_unlocked_topics', true);
if (!is_array($unlocked)) { if (!is_array($unlocked)) {
return false; return false;
} }
return in_array($post_id, $unlocked, true); return in_array($post_id, array_map('intval', $unlocked), true);
}
/**
* Paid access for VISIBILITY_PAY: unlock list OR paid resource order OR VIP free.
*/
public function has_user_paid_topic(int $user_id, int $topic_id): bool {
if ($user_id <= 0 || $topic_id <= 0) {
return false;
}
if ($this->has_user_unlocked_topic($user_id, $topic_id)) {
return true;
}
if (class_exists('AKSRT_Order')) {
$product_type = class_exists('AKSRT_Domain_Model')
? AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE
: 'resource';
if (AKSRT_Order::get_instance()->has_paid_access($user_id, $product_type, $topic_id)) {
return true;
}
}
$price = $this->resolve_topic_pay_price($topic_id, $user_id);
if (!empty($price['is_free'])) {
return true;
}
return false;
} }
public function has_user_commented_on(int $user_id, int $post_id): bool { public function has_user_commented_on(int $user_id, int $post_id): bool {
@@ -263,4 +263,25 @@ trait AKSRT_Forum_Topic_Service_Part_4 {
wp_send_json_error(['message' => $result['message'] ?? __('解锁失败', 'aksrt')]); wp_send_json_error(['message' => $result['message'] ?? __('解锁失败', 'aksrt')]);
} }
} }
public function ajax_purchase_topic(): void {
check_ajax_referer('aksrt_forum_action', '_nonce');
$user_id = get_current_user_id();
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$topic_id = (int) ($_POST['topic_id'] ?? 0);
if ($topic_id <= 0) {
wp_send_json_error(['message' => __('主题不存在', 'aksrt')]);
}
$result = $this->purchase_topic_with_balance($user_id, $topic_id);
if (!empty($result['success'])) {
wp_send_json_success($result);
} else {
wp_send_json_error(['message' => $result['message'] ?? __('购买失败', 'aksrt')]);
}
}
} }
+51 -2
View File
@@ -89,9 +89,58 @@ if (empty($topic['can_view'])) :
<a href="<?php echo esc_url(wp_login_url(get_permalink($topic_id))); ?>" class="aksrt-forum-topic__primary-btn"><?php esc_html_e('登录后解锁', 'aksrt'); ?></a> <a href="<?php echo esc_url(wp_login_url(get_permalink($topic_id))); ?>" class="aksrt-forum-topic__primary-btn"><?php esc_html_e('登录后解锁', 'aksrt'); ?></a>
<?php endif; ?> <?php endif; ?>
<?php elseif ($deny_vis === 'pay') : ?> <?php elseif ($deny_vis === 'pay') : ?>
<?php
$viewer_id = get_current_user_id();
$vip_price_raw = get_post_meta($topic_id, '_aksrt_pay_vip_price', true);
$vip_set = ($vip_price_raw !== '' && $vip_price_raw !== null && $vip_price_raw !== false);
$is_vip_user = $viewer_id > 0 && function_exists('aksrt_is_vip') && aksrt_is_vip($viewer_id);
if (!$is_vip_user && $viewer_id > 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($viewer_id)) {
$is_vip_user = true;
}
$charge_amount = (float) $pay_price;
$is_free_vip = false;
if ($is_vip_user && $vip_set) {
$charge_amount = max(0.0, (float) $vip_price_raw);
$is_free_vip = $charge_amount <= 0;
}
?>
<p><?php esc_html_e('该主题为付费内容,请购买后查看。', 'aksrt'); ?></p> <p><?php esc_html_e('该主题为付费内容,请购买后查看。', 'aksrt'); ?></p>
<?php if (!empty($pay_price)) : ?> <?php if ($is_vip_user && $vip_set) : ?>
<span class="aksrt-forum-topic__pay-price"><?php printf(esc_html__('价格:¥%s', 'aksrt'), esc_html((string) $pay_price)); ?></span> <span class="aksrt-forum-topic__pay-price">
<?php
if ($is_free_vip) {
esc_html_e('VIP 免费', 'aksrt');
} else {
printf(
esc_html__('VIP 价:¥%s', 'aksrt'),
esc_html(number_format($charge_amount, 2, '.', ''))
);
if (!empty($pay_price) && (float) $pay_price > $charge_amount) {
echo ' <del>' . esc_html(sprintf(__('原价 ¥%s', 'aksrt'), number_format((float) $pay_price, 2, '.', ''))) . '</del>';
}
}
?>
</span>
<?php elseif (!empty($pay_price)) : ?>
<span class="aksrt-forum-topic__pay-price"><?php printf(esc_html__('价格:¥%s', 'aksrt'), esc_html(number_format((float) $pay_price, 2, '.', ''))); ?></span>
<?php endif; ?>
<?php if (!is_user_logged_in()) : ?>
<a href="<?php echo esc_url(wp_login_url(get_permalink($topic_id))); ?>" class="aksrt-forum-topic__primary-btn"><?php esc_html_e('登录后购买', 'aksrt'); ?></a>
<?php else : ?>
<button type="button" class="aksrt-forum-topic__primary-btn js-aksrt-purchase-topic"
data-topic-id="<?php echo esc_attr((string) $topic_id); ?>"
data-price="<?php echo esc_attr(number_format($charge_amount, 2, '.', '')); ?>"
data-free="<?php echo $is_free_vip ? '1' : '0'; ?>">
<i class="fas fa-shopping-cart"></i>
<?php
if ($is_free_vip) {
esc_html_e('VIP 免费解锁', 'aksrt');
} else {
printf(esc_html__('余额支付 ¥%s', 'aksrt'), esc_html(number_format($charge_amount, 2, '.', '')));
}
?>
</button>
<span class="aksrt-forum-topic__unlock-feedback js-aksrt-action-feedback" aria-live="polite"></span>
<?php endif; ?> <?php endif; ?>
<?php else : ?> <?php else : ?>
<p><?php esc_html_e('该主题当前处于受限可见或审核状态。', 'aksrt'); ?></p> <p><?php esc_html_e('该主题当前处于受限可见或审核状态。', 'aksrt'); ?></p>