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:
@@ -224,7 +224,7 @@
|
||||
.aksrt-forum-board__application-reason {
|
||||
margin: 10px 0;
|
||||
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);
|
||||
color: var(--color-ink-3, #64748b);
|
||||
font-size: 13px;
|
||||
@@ -240,7 +240,7 @@
|
||||
padding: 12px 16px;
|
||||
background: var(--surface-1, #fff);
|
||||
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 {
|
||||
|
||||
@@ -489,8 +489,9 @@
|
||||
}
|
||||
|
||||
.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);
|
||||
background: var(--color-primary-softer, color-mix(in srgb, var(--color-primary) 6%, transparent));
|
||||
}
|
||||
|
||||
/* ── topic cover (§7.3) ── */
|
||||
@@ -646,7 +647,7 @@
|
||||
}
|
||||
|
||||
.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);
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
|
||||
.aksrt-forum-topic__badge--question {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__badge--solved {
|
||||
@@ -263,7 +263,7 @@
|
||||
|
||||
.aksrt-forum-topic-item__badge--question {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic-item__badge--solved {
|
||||
@@ -328,8 +328,8 @@
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__score-btn:hover:not(:disabled) {
|
||||
border-color: var(--color-primary, #1890ff);
|
||||
color: var(--color-primary, #1890ff);
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-topic__score-btn--up:hover:not(:disabled) {
|
||||
@@ -372,7 +372,7 @@
|
||||
.aksrt-forum-topic__vote-type-badge {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
background: var(--color-primary, #1890ff);
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-pill, 999px);
|
||||
font-size: 11px;
|
||||
@@ -401,7 +401,7 @@
|
||||
|
||||
.aksrt-forum-topic__chip--topic-cat {
|
||||
background: var(--color-primary-bg, #e6f7ff);
|
||||
color: var(--color-primary, #1890ff);
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__like-btn:hover {
|
||||
color: var(--c-primary, #1890ff);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__like-btn.is-active {
|
||||
color: var(--c-primary, #1890ff);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.aksrt-forum-reply__like-count {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.aksrt-forum-replies__more-btn:hover {
|
||||
border-color: var(--c-primary, #1890ff);
|
||||
color: var(--c-primary, #1890ff);
|
||||
border-color: color-mix(in srgb, var(--color-primary) 45%, var(--color-line, #e5e7eb));
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
}
|
||||
|
||||
.aksrt-forum-quick-post__input:hover {
|
||||
border-color: var(--c-primary, #1890ff);
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, .1);
|
||||
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-forum-quick-post__input img {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
.aksrt-forum-reply__reply-btn:hover,
|
||||
.aksrt-forum-reply__top-btn:hover {
|
||||
color: var(--c-primary, #1890ff);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* ── §7.3 Hidden content block [aksrt_hide] ── */
|
||||
@@ -80,13 +80,13 @@
|
||||
|
||||
.aksrt-forum-home-tabs__btn:hover {
|
||||
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 {
|
||||
background: var(--c-primary, #1890ff);
|
||||
background: var(--color-primary);
|
||||
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 {
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
|
||||
.aksrt-forum-replies__tab:hover {
|
||||
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 {
|
||||
background: var(--c-primary, #1890ff);
|
||||
background: var(--color-primary);
|
||||
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 {
|
||||
|
||||
@@ -266,7 +266,10 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
.aksrt-product-card:hover {
|
||||
transform: translateY(-3px);
|
||||
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 {
|
||||
@@ -205,13 +205,13 @@
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
.aksrt-product-archive__pagination .page-numbers.current {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -506,7 +506,7 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -675,7 +675,8 @@
|
||||
.aksrt-product-edit__field input:focus,
|
||||
.aksrt-product-edit__field textarea:focus {
|
||||
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 {
|
||||
@@ -717,7 +718,7 @@
|
||||
}
|
||||
|
||||
.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}
|
||||
|
||||
@@ -389,3 +389,12 @@
|
||||
gap: 12px;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -43,13 +43,13 @@
|
||||
|
||||
.aksrt-commerce-header__nav-item:hover {
|
||||
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 {
|
||||
color: var(--color-on-primary, #fff);
|
||||
background: var(--color-primary, #f04494);
|
||||
border-color: var(--color-primary, #f04494);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* Soften revenue dashboard cards — task-first, not ops console */
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
.aksrt-product-page__thumb:hover,
|
||||
.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 {
|
||||
@@ -227,8 +227,8 @@
|
||||
.aksrt-product-editor__input:focus,
|
||||
.aksrt-product-editor__textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px var(--color-primary-soft);
|
||||
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);
|
||||
}
|
||||
|
||||
/* ===== Quantity controls ===== */
|
||||
@@ -244,7 +244,7 @@
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
@@ -425,13 +425,13 @@ html.dark .aksrt-product-page__content img {
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.aksrt-product-page__spec-btn.is-active {
|
||||
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);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
var coverTypeSelect = form.querySelector('#compose-cover-type');
|
||||
var imageField = form.querySelector('#compose-cover-image-field');
|
||||
@@ -1411,6 +1454,7 @@
|
||||
document.querySelectorAll('.js-aksrt-topic-score').forEach(bindScoreButton);
|
||||
document.querySelectorAll('.js-aksrt-adopt-answer').forEach(bindAdoptButton);
|
||||
document.querySelectorAll('.js-aksrt-unlock-topic').forEach(bindUnlockButton);
|
||||
document.querySelectorAll('.js-aksrt-purchase-topic').forEach(bindPurchaseButton);
|
||||
bindForumCommentEngine();
|
||||
bindForumHomeTabs();
|
||||
bindBoardSortTabs();
|
||||
|
||||
@@ -33,6 +33,13 @@ class AKSRT_Credits {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加积分。
|
||||
*
|
||||
|
||||
@@ -144,6 +144,13 @@ class AKSRT_Forum {
|
||||
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 {
|
||||
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_follow_plate', [$this, 'ajax_follow_plate']);
|
||||
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
|
||||
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();
|
||||
}
|
||||
|
||||
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) {
|
||||
return $this->moderation_service()->apply_moderator($user_id, $board_id, $reason);
|
||||
}
|
||||
|
||||
+205
-10
@@ -188,7 +188,7 @@ trait AKSRT_Forum_Topic_Service_Part_3 {
|
||||
if ($viewer_user_id <= 0) {
|
||||
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) {
|
||||
@@ -324,16 +324,12 @@ trait AKSRT_Forum_Topic_Service_Part_3 {
|
||||
'topic_id' => $topic_id,
|
||||
], sprintf(__('解锁帖子「%s」', 'aksrt'), get_the_title($topic_id)));
|
||||
|
||||
if (!$deducted) {
|
||||
return $this->forum->error(__('积分扣除失败', 'aksrt'));
|
||||
if (is_wp_error($deducted) || $deducted === false) {
|
||||
$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);
|
||||
if (!is_array($unlocked)) {
|
||||
$unlocked = [];
|
||||
}
|
||||
$unlocked[] = $topic_id;
|
||||
update_user_meta($user_id, 'aksrt_unlocked_topics', array_unique($unlocked));
|
||||
$this->mark_topic_unlocked($user_id, $topic_id);
|
||||
|
||||
$author_id = (int) $post->post_author;
|
||||
if ($author_id > 0 && $author_id !== $user_id) {
|
||||
@@ -350,12 +346,211 @@ trait AKSRT_Forum_Topic_Service_Part_3 {
|
||||
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 {
|
||||
$unlocked = get_user_meta($user_id, 'aksrt_unlocked_topics', true);
|
||||
if (!is_array($unlocked)) {
|
||||
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 {
|
||||
|
||||
@@ -263,4 +263,25 @@ trait AKSRT_Forum_Topic_Service_Part_4 {
|
||||
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')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
<?php endif; ?>
|
||||
<?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>
|
||||
<?php if (!empty($pay_price)) : ?>
|
||||
<span class="aksrt-forum-topic__pay-price"><?php printf(esc_html__('价格:¥%s', 'aksrt'), esc_html((string) $pay_price)); ?></span>
|
||||
<?php if ($is_vip_user && $vip_set) : ?>
|
||||
<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 else : ?>
|
||||
<p><?php esc_html_e('该主题当前处于受限可见或审核状态。', 'aksrt'); ?></p>
|
||||
|
||||
Reference in New Issue
Block a user