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:
@@ -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')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user