Merge pull request 'feat(forum): multi-gateway cashier for pay-visibility topics' (#161) from worktree-forum-topic-gateway into main
Reviewed-on: CYL/wordpress-theme-aksrt#161
This commit is contained in:
+36
-13
@@ -366,8 +366,13 @@
|
||||
};
|
||||
}
|
||||
|
||||
function buildPreviewUrl(postId, priceType, couponCode) {
|
||||
var previewUrl = REST_BASE + '/pay/content/' + postId;
|
||||
function payApiBase(apiKind) {
|
||||
return apiKind === 'topic' ? '/pay/topic/' : '/pay/content/';
|
||||
}
|
||||
|
||||
function buildPreviewUrl(postId, priceType, couponCode, apiKind) {
|
||||
apiKind = apiKind || 'content';
|
||||
var previewUrl = REST_BASE + payApiBase(apiKind) + postId;
|
||||
var qs = [];
|
||||
if (priceType) qs.push('price_type=' + encodeURIComponent(priceType));
|
||||
if (couponCode) qs.push('coupon_code=' + encodeURIComponent(couponCode));
|
||||
@@ -375,8 +380,8 @@
|
||||
return previewUrl;
|
||||
}
|
||||
|
||||
function fetchPreview(postId, priceType, couponCode) {
|
||||
return fetch(buildPreviewUrl(postId, priceType, couponCode), {
|
||||
function fetchPreview(postId, priceType, couponCode, apiKind) {
|
||||
return fetch(buildPreviewUrl(postId, priceType, couponCode, apiKind), {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
headers: restHeaders(),
|
||||
@@ -385,17 +390,18 @@
|
||||
});
|
||||
}
|
||||
|
||||
function openCashier(postId, returnFocusEl, priceType, couponCode) {
|
||||
function openCashier(postId, returnFocusEl, priceType, couponCode, apiKind) {
|
||||
if (!postId) return;
|
||||
priceType = priceType || '';
|
||||
couponCode = couponCode || '';
|
||||
apiKind = apiKind || 'content';
|
||||
open(
|
||||
'<h3 class="aksrt-dl-modal__title">' + tt('cashier_loading', '确认购买') + '</h3>' +
|
||||
'<p class="aksrt-dl-modal__msg">' + tt('loading', '加载中…') + '</p>',
|
||||
returnFocusEl
|
||||
);
|
||||
|
||||
fetchPreview(postId, priceType, couponCode)
|
||||
fetchPreview(postId, priceType, couponCode, apiKind)
|
||||
.then(function (r) {
|
||||
if (!r.ok || (r.json && r.json.code)) {
|
||||
const code = r.json && r.json.code ? r.json.code : '';
|
||||
@@ -418,6 +424,7 @@
|
||||
var prev = r.json || {};
|
||||
if (priceType) prev._price_type = priceType;
|
||||
if (couponCode) prev._coupon_code = couponCode;
|
||||
prev._api_kind = apiKind;
|
||||
renderCashier(prev, returnFocusEl);
|
||||
})
|
||||
.catch(function () {
|
||||
@@ -435,13 +442,19 @@
|
||||
renderError('', { vip_url: preview.vip_url }, returnFocusEl, 'aksrt_need_vip');
|
||||
return;
|
||||
}
|
||||
if (preview.has_paid_access || preview.is_free) {
|
||||
if (preview.has_paid_access) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
// Free VIP topics still need a free-order unlock POST; content free can reload.
|
||||
if (preview.is_free && preview.pay_type !== 'topic') {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const isPoints = preview.pay_modo === 'points';
|
||||
const priceType = (preview._price_type || preview.price_type || '');
|
||||
const apiKind = preview._api_kind || (preview.pay_type === 'topic' ? 'topic' : 'content');
|
||||
let amountLabel = escapeHtml(preview.amount_label || '');
|
||||
let baseLabel = escapeHtml(preview.base_label || '');
|
||||
const listLabel = escapeHtml(preview.list_amount_label || preview.amount_label || amountLabel);
|
||||
@@ -541,7 +554,7 @@
|
||||
checkBtn.disabled = true;
|
||||
const old = checkBtn.innerHTML;
|
||||
checkBtn.innerHTML = tt('checking', '检查中…');
|
||||
fetchPreview(preview.post_id, priceType, code)
|
||||
fetchPreview(preview.post_id, priceType, code, apiKind)
|
||||
.then(function (r) {
|
||||
checkBtn.disabled = false;
|
||||
checkBtn.innerHTML = old;
|
||||
@@ -552,6 +565,7 @@
|
||||
var prev = r.json || {};
|
||||
if (priceType) prev._price_type = priceType;
|
||||
prev._coupon_code = code;
|
||||
prev._api_kind = apiKind;
|
||||
renderCashier(prev, returnFocusEl);
|
||||
})
|
||||
.catch(function () {
|
||||
@@ -571,13 +585,14 @@
|
||||
if (checked) paymentMethod = checked.value;
|
||||
const input = document.querySelector('input[name="aksrt_coupon_code"]');
|
||||
const code = input ? String(input.value || '').trim() : (preview.coupon_code || preview._coupon_code || '');
|
||||
submitPurchase(preview.post_id, mode, paymentMethod, submitBtn, returnFocusEl, code);
|
||||
submitPurchase(preview.post_id, mode, paymentMethod, submitBtn, returnFocusEl, code, apiKind);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function submitPurchase(postId, mode, paymentMethod, button, returnFocusEl, couponCode) {
|
||||
function submitPurchase(postId, mode, paymentMethod, button, returnFocusEl, couponCode, apiKind) {
|
||||
if (!postId) return;
|
||||
apiKind = apiKind || 'content';
|
||||
const original = button.innerHTML;
|
||||
button.disabled = true;
|
||||
button.innerHTML = tt('processing', '处理中…');
|
||||
@@ -588,7 +603,7 @@
|
||||
if (pt) payload.price_type = pt;
|
||||
if (mode === 'cash' && couponCode) payload.coupon_code = couponCode;
|
||||
|
||||
fetch(REST_BASE + '/pay/content/' + postId, {
|
||||
fetch(REST_BASE + payApiBase(apiKind) + postId, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: restHeaders(),
|
||||
@@ -643,6 +658,14 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const topicBuyBtn = e.target.closest('[data-aksrt-pay-topic]');
|
||||
if (topicBuyBtn) {
|
||||
e.preventDefault();
|
||||
const topicId = parseInt(topicBuyBtn.getAttribute('data-aksrt-pay-topic') || '0', 10);
|
||||
if (topicId) openCashier(topicId, topicBuyBtn, '', '', 'topic');
|
||||
return;
|
||||
}
|
||||
|
||||
const buyBtn = e.target.closest('[data-action="aksrt-need-purchase"], [data-aksrt-pay-content]');
|
||||
if (buyBtn) {
|
||||
e.preventDefault();
|
||||
@@ -654,7 +677,7 @@
|
||||
10
|
||||
);
|
||||
const priceType = buyBtn.getAttribute('data-price-type') || '';
|
||||
if (postId) openCashier(postId, buyBtn, priceType);
|
||||
if (postId) openCashier(postId, buyBtn, priceType, '', 'content');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -662,7 +685,7 @@
|
||||
if (cashierLink && cashierLink.hasAttribute('data-aksrt-pay-content')) {
|
||||
e.preventDefault();
|
||||
const postId = parseInt(cashierLink.getAttribute('data-aksrt-pay-content') || '0', 10);
|
||||
if (postId) openCashier(postId, cashierLink);
|
||||
if (postId) openCashier(postId, cashierLink, '', '', 'content');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -675,49 +675,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
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 formatFollowPayLabel(pay) {
|
||||
pay = pay || {};
|
||||
if (pay.is_free) {
|
||||
@@ -1558,7 +1515,6 @@
|
||||
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);
|
||||
document.querySelectorAll('.js-aksrt-follow-plate').forEach(bindFollowPlateButton);
|
||||
bindForumCommentEngine();
|
||||
bindForumHomeTabs();
|
||||
|
||||
@@ -99,8 +99,8 @@ Note: zibll「商品管理」`zibpay_product_page` is **paid article sales**, no
|
||||
| 2b Shipping/after-sale/coupons/income/rebate | done |
|
||||
| 3 users.php submenus | done |
|
||||
| 4 SPA strip | done (ops routes redirect to native) |
|
||||
| 5+ behavior parity | done — native ops shell + plate follow-pay + topic pay matrix + sales report/download logs + hidecontent + users commerce + order filters/clear-closed + post pay_type filter + bulk content-pay edit + income/rebate user filter |
|
||||
| 5 remaining | optional: multi-gateway cashier for forum topics (alipay/wechat) |
|
||||
| 5+ behavior parity | done — native ops shell + plate follow-pay + topic pay matrix + multi-gateway topic cashier (`AKSRT_Topic_Pay` /pay/topic) + sales report/download logs + hidecontent + users commerce + order filters/clear-closed + post pay_type filter + bulk content-pay edit + income/rebate user filter |
|
||||
| 5 remaining | optional: topic coupon burn (P1), plate follow VIP1/2 dual tier + multi-gateway (P2) |
|
||||
|
||||
## Dual-UI rule
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ if (class_exists('AKSRT_Download')) {
|
||||
if (class_exists('AKSRT_Content_Pay')) {
|
||||
AKSRT_Content_Pay::get_instance()->init();
|
||||
}
|
||||
if (class_exists('AKSRT_Topic_Pay')) {
|
||||
AKSRT_Topic_Pay::get_instance()->init();
|
||||
}
|
||||
if (class_exists('AKSRT_Vip_Service')) {
|
||||
AKSRT_Vip_Service::get_instance()->init();
|
||||
}
|
||||
|
||||
@@ -232,6 +232,9 @@ require_once AKSRT_DIR . '/inc/classes/class-aksrt-download.php';
|
||||
// 付费内容收银台(post/_aksrt_pay → resource 订单)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-content-pay.php';
|
||||
|
||||
// 论坛主题收银台(visibility=pay → resource 订单 + 多网关)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-topic-pay.php';
|
||||
|
||||
// VIP 开通服务(package → ORDER_PRODUCT_VIP)
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-vip-service.php';
|
||||
|
||||
|
||||
@@ -1056,36 +1056,14 @@ class AKSRT_Content_Pay {
|
||||
];
|
||||
}
|
||||
|
||||
$methods = [];
|
||||
$ids = $logged_in
|
||||
? ['balance', 'alipay', 'wechatpay', 'paypal', 'stripe']
|
||||
: ['alipay', 'wechatpay', 'paypal', 'stripe'];
|
||||
if (!class_exists('AKSRT_Payment_Gateway')) {
|
||||
if (function_exists('aksrt_list_cash_payment_methods')) {
|
||||
return aksrt_list_cash_payment_methods($logged_in);
|
||||
}
|
||||
|
||||
return $logged_in
|
||||
? [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]
|
||||
: [['id' => 'alipay', 'name' => __('支付宝', 'aksrt')]];
|
||||
}
|
||||
$gw = AKSRT_Payment_Gateway::get_instance();
|
||||
foreach ($ids as $id) {
|
||||
$g = $gw->get_gateway($id);
|
||||
if (!$g) {
|
||||
continue;
|
||||
}
|
||||
if (method_exists($g, 'is_configured') && !$g->is_configured()) {
|
||||
continue;
|
||||
}
|
||||
$methods[] = [
|
||||
'id' => $id,
|
||||
'name' => method_exists($g, 'get_name') ? (string) $g->get_name() : $id,
|
||||
];
|
||||
}
|
||||
if ($methods === []) {
|
||||
$methods[] = $logged_in
|
||||
? ['id' => 'balance', 'name' => __('站内余额', 'aksrt')]
|
||||
: ['id' => 'alipay', 'name' => __('支付宝', 'aksrt')];
|
||||
}
|
||||
return $methods;
|
||||
}
|
||||
|
||||
|
||||
private function is_coupon_allowed(int $post_id, array $meta): bool {
|
||||
|
||||
@@ -353,7 +353,6 @@ class AKSRT_Forum {
|
||||
add_action('wp_ajax_aksrt_forum_follow_plate', [$this, 'ajax_follow_plate']);
|
||||
add_action('wp_ajax_aksrt_forum_purchase_plate_follow', [$this, 'ajax_purchase_plate_follow']);
|
||||
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']);
|
||||
@@ -459,10 +458,6 @@ 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.
|
||||
*
|
||||
@@ -607,6 +602,10 @@ class AKSRT_Forum {
|
||||
return $this->topic_service()->resolve_topic_pay_price($topic_id, $user_id);
|
||||
}
|
||||
|
||||
public function mark_topic_unlocked(int $user_id, int $topic_id): void {
|
||||
$this->topic_service()->mark_topic_unlocked($user_id, $topic_id);
|
||||
}
|
||||
|
||||
public function prepare_vote_meta(array $data): array {
|
||||
return $this->topic_service()->prepare_vote_meta($data);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,642 @@
|
||||
<?php
|
||||
/**
|
||||
* AKSRT Topic Pay — forum topic multi-gateway cashier.
|
||||
*
|
||||
* Contract:
|
||||
* - visibility = pay topics only (credits path stays on unlock_topic_with_credits)
|
||||
* - product_type = resource, product_id = topic_id
|
||||
* - order note.topic_pay = 1
|
||||
* - unlock: has_user_paid_topic (order OR unlock list OR VIP free)
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_Topic_Pay {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
add_action('aksrt_order_paid', [$this, 'on_order_paid'], 16, 2);
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
register_rest_route('aksrt/v1', '/pay/topic/(?P<id>\\d+)', [
|
||||
[
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_preview'],
|
||||
'permission_callback' => [$this, 'check_rest_nonce'],
|
||||
],
|
||||
[
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_purchase'],
|
||||
'permission_callback' => [$this, 'check_rest_nonce'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param WP_REST_Request $request
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public function check_rest_nonce(WP_REST_Request $request) {
|
||||
$nonce = $request->get_header('x-wp-nonce');
|
||||
if (!$nonce) {
|
||||
return new WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
if (!wp_verify_nonce($nonce, 'wp_rest')) {
|
||||
return new WP_Error('aksrt_rest_bad_nonce', __('安全令牌已过期,请刷新页面', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rest_preview(WP_REST_Request $request) {
|
||||
$topic_id = (int) $request['id'];
|
||||
$uid = get_current_user_id();
|
||||
$coupon_code = sanitize_text_field((string) ($request->get_param('coupon_code') ?? $request->get_param('coupon') ?? ''));
|
||||
$preview = $this->preview($topic_id, $uid, $coupon_code);
|
||||
if (is_wp_error($preview)) {
|
||||
return $preview;
|
||||
}
|
||||
return new WP_REST_Response($preview, 200);
|
||||
}
|
||||
|
||||
public function rest_purchase(WP_REST_Request $request) {
|
||||
$topic_id = (int) $request['id'];
|
||||
$uid = get_current_user_id();
|
||||
if ($uid <= 0) {
|
||||
$post = get_post($topic_id);
|
||||
return new WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), [
|
||||
'status' => 401,
|
||||
'login_url' => wp_login_url($post ? get_permalink($post) : home_url('/')),
|
||||
]);
|
||||
}
|
||||
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
|
||||
$payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance'));
|
||||
if ($payment_method === '' || $payment_method === 'cash') {
|
||||
$payment_method = 'balance';
|
||||
}
|
||||
$coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? $params['coupon'] ?? ''));
|
||||
|
||||
return $this->rest_wrap_result(
|
||||
$this->create_cash_order($topic_id, $uid, $payment_method, $coupon_code)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $result
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
private function rest_wrap_result($result) {
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
if (empty($result['success'])) {
|
||||
$code = sanitize_key((string) ($result['code'] ?? 'aksrt_pay_failed'));
|
||||
$status = (int) ($result['status'] ?? 400);
|
||||
return new WP_Error(
|
||||
$code !== '' ? $code : 'aksrt_pay_failed',
|
||||
(string) ($result['message'] ?? __('支付失败', 'aksrt')),
|
||||
array_merge(
|
||||
['status' => $status > 0 ? $status : 400],
|
||||
is_array($result['data'] ?? null) ? $result['data'] : []
|
||||
)
|
||||
);
|
||||
}
|
||||
return new WP_REST_Response($result, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cashier preview payload (content-pay field subset for cms-download).
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function preview(int $topic_id, int $user_id = 0, string $coupon_code = '') {
|
||||
$assert = $this->assert_topic($topic_id);
|
||||
if (is_wp_error($assert)) {
|
||||
return $assert;
|
||||
}
|
||||
/** @var WP_Post $post */
|
||||
$post = $assert['post'];
|
||||
$uid = $user_id > 0 ? $user_id : get_current_user_id();
|
||||
|
||||
if ($uid <= 0) {
|
||||
return [
|
||||
'post_id' => $topic_id,
|
||||
'title' => get_the_title($post),
|
||||
'permalink' => get_permalink($post),
|
||||
'pay_modo' => '0',
|
||||
'pay_type' => 'topic',
|
||||
'pay_type_label' => __('付费主题', 'aksrt'),
|
||||
'amount' => 0,
|
||||
'base' => 0,
|
||||
'list_amount' => 0,
|
||||
'payable' => 0,
|
||||
'currency' => '¥',
|
||||
'amount_label' => '',
|
||||
'base_label' => '',
|
||||
'list_amount_label' => '',
|
||||
'is_free' => false,
|
||||
'has_paid_access' => false,
|
||||
'need_login' => true,
|
||||
'allow_guest' => false,
|
||||
'need_vip' => false,
|
||||
'need_purchase' => true,
|
||||
'logged_in' => false,
|
||||
'balance' => 0,
|
||||
'balance_enough' => false,
|
||||
'payment_methods' => [],
|
||||
'login_url' => wp_login_url(get_permalink($post)),
|
||||
'vip_url' => home_url('/'),
|
||||
'credits_url' => home_url('/'),
|
||||
'coupon_allowed' => false,
|
||||
'coupon_desc' => '',
|
||||
'coupon_code' => '',
|
||||
'coupon' => null,
|
||||
'coupon_error' => '',
|
||||
'coupon_discount' => 0,
|
||||
'coupon_discount_label' => '',
|
||||
'rebate_discount' => 0,
|
||||
'rebate_discount_label' => '',
|
||||
'points_deduction_allowed' => false,
|
||||
'card_pass_allowed' => false,
|
||||
'price_type' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$priced = $this->resolve_price($topic_id, $uid);
|
||||
$amount = round((float) ($priced['amount'] ?? 0), 2);
|
||||
$base = round((float) ($priced['base'] ?? $amount), 2);
|
||||
$is_free = !empty($priced['is_free']) || $amount <= 0;
|
||||
$has_paid = $this->user_has_paid($uid, $topic_id);
|
||||
|
||||
$balance = 0;
|
||||
if (class_exists('AKSRT_Credits')) {
|
||||
$balance = (int) AKSRT_Credits::balance($uid);
|
||||
}
|
||||
|
||||
$methods = function_exists('aksrt_list_cash_payment_methods')
|
||||
? aksrt_list_cash_payment_methods(true)
|
||||
: [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]];
|
||||
|
||||
$amount_label = $is_free
|
||||
? __('免费', 'aksrt')
|
||||
: sprintf('¥%s', number_format($amount, 2, '.', ''));
|
||||
$base_label = sprintf('¥%s', number_format($base, 2, '.', ''));
|
||||
|
||||
$coupon_allowed = $this->is_coupon_allowed($topic_id);
|
||||
// P0: coupon applied on purchase path only when allowed; preview soft field.
|
||||
unset($coupon_code);
|
||||
|
||||
return [
|
||||
'post_id' => $topic_id,
|
||||
'title' => get_the_title($post),
|
||||
'permalink' => get_permalink($post),
|
||||
'pay_modo' => '0',
|
||||
'pay_type' => 'topic',
|
||||
'pay_type_label' => __('付费主题', 'aksrt'),
|
||||
'amount' => $amount,
|
||||
'base' => $base,
|
||||
'list_amount' => $amount,
|
||||
'list_amount_label' => $amount_label,
|
||||
'payable' => $amount,
|
||||
'currency' => '¥',
|
||||
'amount_label' => $amount_label,
|
||||
'base_label' => $base_label,
|
||||
'is_free' => $is_free,
|
||||
'has_paid_access' => $has_paid,
|
||||
'paid_order' => null,
|
||||
'pending_order' => null,
|
||||
'need_login' => false,
|
||||
'allow_guest' => false,
|
||||
'need_vip' => false,
|
||||
'need_purchase' => !$has_paid && !$is_free,
|
||||
'logged_in' => true,
|
||||
'balance' => $balance,
|
||||
'balance_enough' => true,
|
||||
'payment_methods' => $methods,
|
||||
'login_url' => wp_login_url(get_permalink($post)),
|
||||
'vip_url' => home_url('/'),
|
||||
'credits_url' => home_url('/'),
|
||||
'coupon_allowed' => $coupon_allowed,
|
||||
'coupon_desc' => '',
|
||||
'coupon_code' => '',
|
||||
'coupon' => null,
|
||||
'coupon_error' => '',
|
||||
'coupon_discount' => 0,
|
||||
'coupon_discount_label' => '',
|
||||
'rebate_discount' => 0,
|
||||
'rebate_discount_label' => '',
|
||||
'points_deduction_allowed' => false,
|
||||
'card_pass_allowed' => false,
|
||||
'price_type' => '',
|
||||
'vip_level' => (int) ($priced['vip_level'] ?? 0),
|
||||
'is_vip_price' => !empty($priced['is_vip_price']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create cash order and invoke gateway.
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function create_cash_order(int $topic_id, int $user_id, string $payment_method = 'balance', string $coupon_code = '') {
|
||||
if ($user_id <= 0) {
|
||||
return new WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), ['status' => 401]);
|
||||
}
|
||||
|
||||
$assert = $this->assert_topic($topic_id);
|
||||
if (is_wp_error($assert)) {
|
||||
return $assert;
|
||||
}
|
||||
/** @var WP_Post $post */
|
||||
$post = $assert['post'];
|
||||
|
||||
if ($this->user_has_paid($user_id, $topic_id)) {
|
||||
return new WP_Error('aksrt_already_paid', __('已购买,可直接查看。', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
$priced = $this->resolve_price($topic_id, $user_id);
|
||||
$amount = round((float) ($priced['amount'] ?? 0), 2);
|
||||
$base = round((float) ($priced['base'] ?? $amount), 2);
|
||||
$is_free = !empty($priced['is_free']) || $amount <= 0;
|
||||
|
||||
// Coupon reserved (P1): ignore invalid empty codes; no discount stack yet beyond VIP.
|
||||
$coupon_code = sanitize_text_field($coupon_code);
|
||||
if (!$this->is_coupon_allowed($topic_id)) {
|
||||
$coupon_code = '';
|
||||
}
|
||||
|
||||
if ($is_free) {
|
||||
return $this->create_free_paid_order($topic_id, $user_id, $post, $priced);
|
||||
}
|
||||
|
||||
if ($amount <= 0) {
|
||||
return new WP_Error('aksrt_price_unset', __('价格未设置', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
|
||||
return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$payment_method = sanitize_key($payment_method);
|
||||
if ($payment_method === '') {
|
||||
$payment_method = 'balance';
|
||||
}
|
||||
|
||||
$pending = $this->find_pending_topic_order($user_id, $topic_id);
|
||||
$can_resume = $pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001;
|
||||
|
||||
if ($can_resume) {
|
||||
$order_number = (string) ($pending['order_number'] ?? '');
|
||||
$order = $pending;
|
||||
} else {
|
||||
$note = wp_json_encode([
|
||||
'topic_pay' => 1,
|
||||
'topic_id' => $topic_id,
|
||||
'pay_modo' => '0',
|
||||
'vip_level' => (int) ($priced['vip_level'] ?? 0),
|
||||
'base' => $base,
|
||||
'list_amount' => $amount,
|
||||
'amount' => $amount,
|
||||
'payable' => $amount,
|
||||
'coupon_code' => $coupon_code,
|
||||
'coupon_discount' => 0,
|
||||
'coupon' => [],
|
||||
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$order_number = AKSRT_Order_Repository::get_instance()->create([
|
||||
'user_id' => $user_id,
|
||||
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
|
||||
'product_id' => $topic_id,
|
||||
'amount' => $amount,
|
||||
'currency' => 'CNY',
|
||||
'status' => AKSRT_Order::STATUS_PENDING,
|
||||
'payment_method' => $payment_method,
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
|
||||
]);
|
||||
|
||||
if ($order_number === '') {
|
||||
return new WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$order = AKSRT_Order_Repository::get_instance()->get($order_number);
|
||||
if (!$order) {
|
||||
return new WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
}
|
||||
|
||||
$order['subject'] = sprintf(
|
||||
/* translators: %s: topic title */
|
||||
__('购买论坛主题「%s」', 'aksrt'),
|
||||
get_the_title($post)
|
||||
);
|
||||
$order['payment_method'] = $payment_method;
|
||||
$order['amount'] = $amount;
|
||||
|
||||
if (!class_exists('AKSRT_Payment_Gateway')) {
|
||||
return new WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$gateway_manager = AKSRT_Payment_Gateway::get_instance();
|
||||
$gateway = $gateway_manager->get_gateway($payment_method);
|
||||
if (!$gateway) {
|
||||
return new WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用。', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
}
|
||||
if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) {
|
||||
return new WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [
|
||||
'status' => 400,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
}
|
||||
|
||||
$payment_result = $gateway->create_payment($order);
|
||||
if (empty($payment_result['success'])) {
|
||||
$msg = (string) ($payment_result['message'] ?? __('支付失败。', 'aksrt'));
|
||||
$code = ($payment_method === 'balance' && (str_contains($msg, '余额') || str_contains($msg, '不足')))
|
||||
? 'aksrt_balance_insufficient'
|
||||
: 'aksrt_pay_failed';
|
||||
return new WP_Error($code, $msg, [
|
||||
'status' => 402,
|
||||
'order_number' => $order_number,
|
||||
]);
|
||||
}
|
||||
|
||||
$code_url = (string) ($payment_result['payment_data']['code_url'] ?? '');
|
||||
$payment_url = (string) ($payment_result['payment_url'] ?? '');
|
||||
if ($payment_url !== '' || $code_url !== '') {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'redirect' => true,
|
||||
'payment_url' => $payment_url,
|
||||
'qr_content' => $code_url,
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'pay_modo' => '0',
|
||||
'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'),
|
||||
'reload' => false,
|
||||
'poll' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// Sync gateway (balance): mark paid.
|
||||
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
|
||||
'payment_method' => $payment_method,
|
||||
'payment_transaction_id' => sanitize_text_field((string) ($payment_result['transaction_id'] ?? '')),
|
||||
'note' => __('用户完成论坛主题购买支付。', 'aksrt'),
|
||||
]);
|
||||
if (!$paid) {
|
||||
return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
|
||||
}
|
||||
|
||||
$this->mark_unlocked($user_id, $topic_id);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order_number' => $order_number,
|
||||
'amount' => $amount,
|
||||
'pay_modo' => '0',
|
||||
'message' => $payment_result['message'] ?? __('支付成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* On paid: write unlock list for forum topics.
|
||||
*/
|
||||
public function on_order_paid(string $order_number, array $paid_order): void {
|
||||
$product_type = sanitize_key((string) ($paid_order['product_type'] ?? ''));
|
||||
if ($product_type !== AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$topic_id = (int) ($paid_order['product_id'] ?? 0);
|
||||
$user_id = (int) ($paid_order['user_id'] ?? 0);
|
||||
if ($topic_id <= 0 || $user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$meta = $this->decode_order_note_meta($paid_order);
|
||||
$is_topic_pay = !empty($meta['topic_pay']);
|
||||
if (!$is_topic_pay) {
|
||||
$post = get_post($topic_id);
|
||||
if (!($post instanceof WP_Post) || $post->post_type !== 'aksrt_forum_topic') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->mark_unlocked($user_id, $topic_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{post:WP_Post}|WP_Error
|
||||
*/
|
||||
private function assert_topic(int $topic_id) {
|
||||
$post = get_post($topic_id);
|
||||
if (!($post instanceof WP_Post) || $post->post_type !== 'aksrt_forum_topic') {
|
||||
return new WP_Error('aksrt_invalid', __('主题不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
if ($post->post_status !== 'publish') {
|
||||
return new WP_Error('aksrt_invalid', __('主题不可购买', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
|
||||
$visibility = sanitize_key((string) get_post_meta($topic_id, '_aksrt_visibility', true));
|
||||
if ($visibility !== AKSRT_Forum::VISIBILITY_PAY) {
|
||||
return new WP_Error('aksrt_not_paid', __('该主题不是付费可见内容', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
|
||||
return ['post' => $post];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{amount:float,is_vip_price:bool,is_free:bool,label:string,base:float,vip_set:bool,vip_level:int}
|
||||
*/
|
||||
private function resolve_price(int $topic_id, int $user_id): array {
|
||||
if (class_exists('AKSRT_Forum')) {
|
||||
return AKSRT_Forum::get_instance()->resolve_topic_pay_price($topic_id, $user_id);
|
||||
}
|
||||
return [
|
||||
'amount' => max(0.0, (float) get_post_meta($topic_id, '_aksrt_pay_price', true)),
|
||||
'is_vip_price' => false,
|
||||
'is_free' => false,
|
||||
'label' => '',
|
||||
'base' => max(0.0, (float) get_post_meta($topic_id, '_aksrt_pay_price', true)),
|
||||
'vip_set' => false,
|
||||
'vip_level' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
private function user_has_paid(int $user_id, int $topic_id): bool {
|
||||
if (class_exists('AKSRT_Forum')) {
|
||||
return AKSRT_Forum::get_instance()->has_user_paid_topic($user_id, $topic_id);
|
||||
}
|
||||
if (class_exists('AKSRT_Order')) {
|
||||
return AKSRT_Order::get_instance()->has_paid_access(
|
||||
$user_id,
|
||||
AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
|
||||
$topic_id
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function mark_unlocked(int $user_id, int $topic_id): void {
|
||||
if (class_exists('AKSRT_Forum') && method_exists('AKSRT_Forum', 'mark_topic_unlocked')) {
|
||||
AKSRT_Forum::get_instance()->mark_topic_unlocked($user_id, $topic_id);
|
||||
return;
|
||||
}
|
||||
// Fallback user meta write.
|
||||
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))));
|
||||
}
|
||||
|
||||
private function is_coupon_allowed(int $topic_id): bool {
|
||||
// P0/P1: honor reserved meta; coupon discount stack deferred to P1.
|
||||
return (int) get_post_meta($topic_id, '_aksrt_topic_coupon_s', true) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
private function create_free_paid_order(int $topic_id, int $user_id, WP_Post $post, array $priced) {
|
||||
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
|
||||
$this->mark_unlocked($user_id, $topic_id);
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order_number' => '',
|
||||
'amount' => 0,
|
||||
'pay_modo' => '0',
|
||||
'free' => true,
|
||||
'message' => __('VIP 免费解锁成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$note = wp_json_encode([
|
||||
'topic_pay' => 1,
|
||||
'topic_id' => $topic_id,
|
||||
'pay_modo' => '0',
|
||||
'vip_level' => (int) ($priced['vip_level'] ?? 0),
|
||||
'base' => (float) ($priced['base'] ?? 0),
|
||||
'list_amount' => 0,
|
||||
'amount' => 0,
|
||||
'payable' => 0,
|
||||
'coupon_code' => '',
|
||||
'coupon_discount' => 0,
|
||||
'free' => 1,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$order_number = AKSRT_Order_Repository::get_instance()->create([
|
||||
'user_id' => $user_id,
|
||||
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
|
||||
'product_id' => $topic_id,
|
||||
'amount' => 0.0,
|
||||
'currency' => 'CNY',
|
||||
'status' => AKSRT_Order::STATUS_PENDING,
|
||||
'payment_method' => 'free',
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
|
||||
]);
|
||||
|
||||
if ($order_number === '') {
|
||||
$this->mark_unlocked($user_id, $topic_id);
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order_number' => '',
|
||||
'amount' => 0,
|
||||
'free' => true,
|
||||
'message' => __('VIP 免费解锁成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
|
||||
'payment_method' => 'free',
|
||||
'payment_transaction_id' => 'free_' . $order_number,
|
||||
'note' => __('VIP 免费解锁主题。', 'aksrt'),
|
||||
]);
|
||||
$this->mark_unlocked($user_id, $topic_id);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'order_number' => $order_number,
|
||||
'amount' => 0,
|
||||
'pay_modo' => '0',
|
||||
'free' => true,
|
||||
'message' => __('VIP 免费解锁成功', 'aksrt'),
|
||||
'reload' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function find_pending_topic_order(int $user_id, int $topic_id): ?array {
|
||||
if ($user_id <= 0 || $topic_id <= 0 || !class_exists('AKSRT_Order_Repository')) {
|
||||
return null;
|
||||
}
|
||||
$orders = AKSRT_Order_Repository::get_instance()->get_user_orders($user_id, [
|
||||
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
|
||||
'status' => AKSRT_Domain_Model::ORDER_STATUS_PENDING,
|
||||
'limit' => 20,
|
||||
'offset' => 0,
|
||||
]);
|
||||
foreach ($orders as $order) {
|
||||
if ((int) ($order['product_id'] ?? 0) !== $topic_id) {
|
||||
continue;
|
||||
}
|
||||
$meta = $this->decode_order_note_meta($order);
|
||||
if (!empty($meta['topic_pay']) || $this->is_forum_topic_id($topic_id)) {
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function is_forum_topic_id(int $topic_id): bool {
|
||||
$post = get_post($topic_id);
|
||||
return $post instanceof WP_Post && $post->post_type === 'aksrt_forum_topic';
|
||||
}
|
||||
|
||||
private function decode_order_note_meta(array $order): array {
|
||||
$note = (string) ($order['note'] ?? '');
|
||||
if ($note === '') {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($note, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
}
|
||||
@@ -408,121 +408,6 @@ 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.
|
||||
*
|
||||
|
||||
@@ -263,25 +263,4 @@ 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')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,8 +661,12 @@ trait AKSRT_Scripts_Part_1b {
|
||||
$enqueue_module('resource-archive');
|
||||
}
|
||||
|
||||
// 单篇资源 / 付费内容 / 独立下载页:下载 + 收银台脚本
|
||||
if ($is_single_resource || $is_paid_content_single || $is_download_file_page) {
|
||||
// 付费论坛主题:多网关收银台(与内容共用 cms-download.js)
|
||||
$is_pay_forum_topic = is_singular('aksrt_forum_topic')
|
||||
&& sanitize_key((string) get_post_meta(get_queried_object_id(), '_aksrt_visibility', true)) === 'pay';
|
||||
|
||||
// 单篇资源 / 付费内容 / 独立下载页 / 付费主题:下载 + 收银台脚本
|
||||
if ($is_single_resource || $is_paid_content_single || $is_download_file_page || $is_pay_forum_topic) {
|
||||
$cms_download_js_path = get_template_directory() . '/assets/js/cms-download.js';
|
||||
wp_enqueue_script(
|
||||
'aksrt-cms-download',
|
||||
|
||||
@@ -74,6 +74,49 @@ function aksrt_get_pay_types(): array {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Cash payment methods available for content/topic cashiers.
|
||||
*
|
||||
* @return array<int, array{id:string,name:string}>
|
||||
*/
|
||||
if (!function_exists('aksrt_list_cash_payment_methods')) {
|
||||
function aksrt_list_cash_payment_methods(bool $logged_in = true): array {
|
||||
$ids = $logged_in
|
||||
? ['balance', 'alipay', 'wechatpay', 'paypal', 'stripe']
|
||||
: ['alipay', 'wechatpay', 'paypal', 'stripe'];
|
||||
|
||||
if (!class_exists('AKSRT_Payment_Gateway')) {
|
||||
return $logged_in
|
||||
? [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]
|
||||
: [['id' => 'alipay', 'name' => __('支付宝', 'aksrt')]];
|
||||
}
|
||||
|
||||
$methods = [];
|
||||
$gw = AKSRT_Payment_Gateway::get_instance();
|
||||
foreach ($ids as $id) {
|
||||
$g = $gw->get_gateway($id);
|
||||
if (!$g) {
|
||||
continue;
|
||||
}
|
||||
if (method_exists($g, 'is_configured') && !$g->is_configured()) {
|
||||
continue;
|
||||
}
|
||||
$methods[] = [
|
||||
'id' => $id,
|
||||
'name' => method_exists($g, 'get_name') ? (string) $g->get_name() : $id,
|
||||
];
|
||||
}
|
||||
|
||||
if ($methods === []) {
|
||||
$methods[] = $logged_in
|
||||
? ['id' => 'balance', 'name' => __('站内余额', 'aksrt')]
|
||||
: ['id' => 'alipay', 'name' => __('支付宝', 'aksrt')];
|
||||
}
|
||||
|
||||
return $methods;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 元数据读取
|
||||
// ============================================================
|
||||
|
||||
@@ -85,19 +85,15 @@ if (empty($topic['can_view'])) :
|
||||
<span class="aksrt-forum-topic__unlock-feedback js-aksrt-action-feedback" aria-live="polite"></span>
|
||||
<?php elseif ($deny_vis === 'pay' && is_user_logged_in()) : ?>
|
||||
<?php
|
||||
$charge_amount = (float) $pay_price;
|
||||
$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);
|
||||
$viewer_id = get_current_user_id();
|
||||
$is_vip_user = $viewer_id > 0 && function_exists('aksrt_is_vip') && aksrt_is_vip($viewer_id);
|
||||
$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;
|
||||
}
|
||||
$price_info = class_exists('AKSRT_Forum')
|
||||
? AKSRT_Forum::get_instance()->resolve_topic_pay_price($topic_id, $viewer_id)
|
||||
: ['amount' => (float) $pay_price, 'is_free' => false, 'label' => '', 'base' => (float) $pay_price];
|
||||
$charge_amount = (float) ($price_info['amount'] ?? 0);
|
||||
$is_free_vip = !empty($price_info['is_free']);
|
||||
?>
|
||||
<button type="button" class="aksrt-forum-topic__primary-btn js-aksrt-purchase-topic"
|
||||
data-topic-id="<?php echo esc_attr((string) $topic_id); ?>"
|
||||
<button type="button" class="aksrt-forum-topic__primary-btn"
|
||||
data-aksrt-pay-topic="<?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>
|
||||
@@ -105,7 +101,7 @@ if (empty($topic['can_view'])) :
|
||||
if ($is_free_vip) {
|
||||
esc_html_e('VIP 免费解锁', 'aksrt');
|
||||
} else {
|
||||
printf(esc_html__('余额支付 ¥%s', 'aksrt'), esc_html(number_format($charge_amount, 2, '.', '')));
|
||||
printf(esc_html__('购买解锁 ¥%s', 'aksrt'), esc_html(number_format($charge_amount, 2, '.', '')));
|
||||
}
|
||||
?>
|
||||
</button>
|
||||
@@ -168,21 +164,16 @@ if (empty($topic['can_view'])) :
|
||||
<?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;
|
||||
}
|
||||
$price_info = class_exists('AKSRT_Forum')
|
||||
? AKSRT_Forum::get_instance()->resolve_topic_pay_price($topic_id, $viewer_id)
|
||||
: ['amount' => (float) $pay_price, 'is_free' => false, 'is_vip_price' => false, 'base' => (float) $pay_price, 'label' => ''];
|
||||
$charge_amount = (float) ($price_info['amount'] ?? 0);
|
||||
$base_amount = (float) ($price_info['base'] ?? $charge_amount);
|
||||
$is_free_vip = !empty($price_info['is_free']);
|
||||
$is_vip_price = !empty($price_info['is_vip_price']);
|
||||
?>
|
||||
<p><?php esc_html_e('该主题为付费内容,请购买后查看。', 'aksrt'); ?></p>
|
||||
<?php if ($is_vip_user && $vip_set) : ?>
|
||||
<?php if ($is_vip_price) : ?>
|
||||
<span class="aksrt-forum-topic__pay-price">
|
||||
<?php
|
||||
if ($is_free_vip) {
|
||||
@@ -192,20 +183,20 @@ if (empty($topic['can_view'])) :
|
||||
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>';
|
||||
if ($base_amount > $charge_amount) {
|
||||
echo ' <del>' . esc_html(sprintf(__('原价 ¥%s', 'aksrt'), number_format($base_amount, 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 elseif ($charge_amount > 0 || $base_amount > 0) : ?>
|
||||
<span class="aksrt-forum-topic__pay-price"><?php printf(esc_html__('价格:¥%s', 'aksrt'), esc_html(number_format(max($charge_amount, $base_amount), 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); ?>"
|
||||
<button type="button" class="aksrt-forum-topic__primary-btn"
|
||||
data-aksrt-pay-topic="<?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>
|
||||
@@ -213,7 +204,7 @@ if (empty($topic['can_view'])) :
|
||||
if ($is_free_vip) {
|
||||
esc_html_e('VIP 免费解锁', 'aksrt');
|
||||
} else {
|
||||
printf(esc_html__('余额支付 ¥%s', 'aksrt'), esc_html(number_format($charge_amount, 2, '.', '')));
|
||||
printf(esc_html__('购买解锁 ¥%s', 'aksrt'), esc_html(number_format($charge_amount, 2, '.', '')));
|
||||
}
|
||||
?>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user