Wire paid plate follow (admin meta + points/balance purchase + AJAX), re-check plate gate on topics, post list sales column, and order type subtabs; update gap matrix and admin IA map.
790 lines
31 KiB
PHP
790 lines
31 KiB
PHP
<?php
|
|
defined('ABSPATH') || exit;
|
|
|
|
trait AKSRT_Forum_Board_Service_Part_2 {
|
|
|
|
|
|
/**
|
|
* Get board page view (统一入口 CPT/taxonomy).
|
|
*/
|
|
public function get_board_view(int $board_id, array $args = []): array {
|
|
$user_id = (int) ($args['user_id'] ?? get_current_user_id());
|
|
$page = max(1, (int) ($args['paged'] ?? $args['page'] ?? 1));
|
|
$orderby = sanitize_key((string) ($args['orderby'] ?? $args['sort'] ?? ''));
|
|
|
|
$board_page = class_exists('AKSRT_Settings')
|
|
? (AKSRT_Settings::get_forum_settings()['board_page'] ?? [])
|
|
: [];
|
|
$tabs = is_array($board_page['tabs'] ?? null) ? $board_page['tabs'] : [
|
|
['label' => '全部', 'sort' => 'date'],
|
|
['label' => '推荐', 'sort' => 'featured'],
|
|
['label' => '最新回复', 'sort' => 'last_reply'],
|
|
['label' => '热门', 'sort' => 'hot'],
|
|
['label' => '精华', 'sort' => 'essence'],
|
|
];
|
|
$allowed_sorts = [];
|
|
foreach ($tabs as $tab) {
|
|
$sort = sanitize_key((string) ($tab['sort'] ?? ''));
|
|
if ($sort !== '') {
|
|
$allowed_sorts[] = $sort;
|
|
}
|
|
}
|
|
if ($orderby === '' || !in_array($orderby, $allowed_sorts, true)) {
|
|
$active_index = max(0, (int) ($board_page['tab_active_index'] ?? 0));
|
|
$orderby = sanitize_key((string) ($tabs[$active_index]['sort'] ?? 'date'));
|
|
if ($orderby === '') {
|
|
$orderby = 'date';
|
|
}
|
|
}
|
|
|
|
$compose_url = function_exists('aksrt_get_forum_compose_url')
|
|
? aksrt_get_forum_compose_url(['board' => $board_id])
|
|
: home_url('/');
|
|
$top_info = !isset($board_page['top_info']) || !empty($board_page['top_info']);
|
|
|
|
if ($this->forum->is_plate_cpt_active()) {
|
|
$view = $this->get_plate_view_compat($board_id, $user_id, $page, $orderby);
|
|
$view['tabs'] = $tabs;
|
|
$view['active_sort'] = $orderby;
|
|
$view['top_info'] = $top_info;
|
|
$view['compose_url'] = $compose_url;
|
|
return $view;
|
|
}
|
|
|
|
$board = get_term($board_id, 'aksrt_forum_board');
|
|
|
|
if (!$board || is_wp_error($board)) {
|
|
return [
|
|
'board' => null,
|
|
'topics' => [],
|
|
'sticky_topics' => [],
|
|
'moderators' => [],
|
|
'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0],
|
|
'permissions' => [],
|
|
'application' => null,
|
|
'board_options' => [],
|
|
'tabs' => $tabs,
|
|
'active_sort' => $orderby,
|
|
'top_info' => $top_info,
|
|
'compose_url' => function_exists('aksrt_get_forum_compose_url') ? aksrt_get_forum_compose_url() : home_url('/'),
|
|
];
|
|
}
|
|
|
|
$topics = $this->forum->get_topic_list([
|
|
'board_id' => $board_id,
|
|
'page' => $page,
|
|
'limit' => 20,
|
|
'status' => 'publish',
|
|
'viewer_user_id' => $user_id,
|
|
'orderby' => $orderby,
|
|
]);
|
|
|
|
$sticky_topics = $this->forum->get_sticky_topics_for_board($board_id);
|
|
$board_link = get_term_link($board);
|
|
|
|
return [
|
|
'board' => [
|
|
'id' => $board_id,
|
|
'name' => (string) $board->name,
|
|
'description' => (string) $board->description,
|
|
'content' => '',
|
|
'icon' => (string) get_term_meta($board_id, '_aksrt_icon', true),
|
|
'topic_count' => (int) $board->count,
|
|
'reply_count' => 0,
|
|
'today_reply_count' => 0,
|
|
'views' => 0,
|
|
'follow_count' => 0,
|
|
'last_reply' => '',
|
|
'last_post' => '',
|
|
'url' => !is_wp_error($board_link) ? (string) $board_link : '',
|
|
'thumbnail_url' => '',
|
|
'author_id' => 0,
|
|
'allow_view' => 'public',
|
|
],
|
|
'topics' => $topics['items'],
|
|
'sticky_topics' => array_slice($sticky_topics, 0, 5),
|
|
'moderators' => $this->forum->get_board_moderator_profiles($board_id),
|
|
'pagination' => [
|
|
'page' => $topics['page'],
|
|
'max_pages' => $topics['max_pages'],
|
|
'total' => $topics['total'],
|
|
],
|
|
'permissions' => [
|
|
'can_compose' => $user_id > 0,
|
|
'can_apply_moderator' => $user_id > 0 && !$this->forum->is_board_moderator($user_id, $board_id),
|
|
'can_moderate' => $this->forum->is_board_moderator($user_id, $board_id),
|
|
],
|
|
'application' => $user_id > 0 ? $this->forum->get_board_application_for_user($user_id, $board_id) : null,
|
|
'board_options' => $this->get_boards(),
|
|
'tabs' => $tabs,
|
|
'active_sort' => $orderby,
|
|
'top_info' => $top_info,
|
|
'compose_url' => $compose_url,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Plate CPT board view (backwards-compatible with get_board_view return shape).
|
|
*/
|
|
private function get_plate_view_compat(int $plate_id, int $user_id, int $page, string $orderby = 'date'): array {
|
|
$plate = $this->get_plate($plate_id);
|
|
if (!$plate) {
|
|
return [
|
|
'board' => null,
|
|
'topics' => [],
|
|
'sticky_topics' => [],
|
|
'moderators' => [],
|
|
'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0],
|
|
'permissions' => [],
|
|
'application' => null,
|
|
'board_options' => [],
|
|
];
|
|
}
|
|
|
|
// Check access permission
|
|
$allow_view = $plate['allow_view'] ?? 'public';
|
|
if (!$this->can_view_plate($user_id, $plate_id, $allow_view)) {
|
|
return [
|
|
'board' => $plate,
|
|
'topics' => [],
|
|
'sticky_topics' => [],
|
|
'moderators' => $this->get_plate_moderator_profiles($plate_id),
|
|
'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0],
|
|
'permissions' => [
|
|
'can_compose' => false,
|
|
'can_apply_moderator' => false,
|
|
'can_moderate' => false,
|
|
'access_denied' => true,
|
|
'access_reason' => $allow_view,
|
|
],
|
|
'application' => null,
|
|
'board_options' => $this->get_boards(),
|
|
];
|
|
}
|
|
|
|
$topics = $this->forum->get_topic_list([
|
|
'plate_id' => $plate_id,
|
|
'page' => $page,
|
|
'limit' => 20,
|
|
'status' => 'publish',
|
|
'viewer_user_id' => $user_id,
|
|
'orderby' => $orderby,
|
|
]);
|
|
|
|
$sticky_topics = $this->get_sticky_topics_for_plate($plate_id);
|
|
|
|
return [
|
|
'board' => $plate,
|
|
'topics' => $topics['items'],
|
|
'sticky_topics' => array_slice($sticky_topics, 0, 5),
|
|
'moderators' => $this->get_plate_moderator_profiles($plate_id),
|
|
'pagination' => [
|
|
'page' => $topics['page'],
|
|
'max_pages' => $topics['max_pages'],
|
|
'total' => $topics['total'],
|
|
],
|
|
'permissions' => [
|
|
'can_compose' => $user_id > 0 && $this->can_compose_in_plate($user_id, $plate_id),
|
|
'can_apply_moderator' => $user_id > 0 && $this->forum->forum_user_can($user_id, 'moderator_apply'),
|
|
'can_moderate' => $this->forum->forum_user_can($user_id, 'topic_moderate', $plate_id),
|
|
'can_manage_plate' => $this->forum->forum_user_can($user_id, 'plate_manage', $plate_id),
|
|
'can_manage_mods' => $this->forum->forum_user_can($user_id, 'moderator_manage', $plate_id),
|
|
'user_tier' => $this->forum->get_user_plate_tier($user_id, $plate_id),
|
|
],
|
|
'application' => $user_id > 0 ? $this->forum->get_board_application_for_user($user_id, $plate_id) : null,
|
|
'board_options' => $this->get_boards(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Check if a user can view a plate (§7.2 板块阅读权限).
|
|
*/
|
|
public function can_view_plate(int $user_id, int $plate_id, string $allow_view = ''): bool {
|
|
if ($allow_view === '') {
|
|
$allow_view = (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public');
|
|
}
|
|
|
|
if ($allow_view === 'public') {
|
|
return true;
|
|
}
|
|
|
|
if ($user_id <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->is_plate_moderator($user_id, $plate_id)) {
|
|
return true;
|
|
}
|
|
|
|
if ($allow_view === 'login') {
|
|
return true;
|
|
}
|
|
|
|
if ($allow_view === 'follow') {
|
|
return $this->is_user_following_plate($user_id, $plate_id);
|
|
}
|
|
|
|
if ($allow_view === 'role') {
|
|
$allowed_roles = get_post_meta($plate_id, '_aksrt_allow_view_roles', true);
|
|
if (!is_array($allowed_roles) || empty($allowed_roles)) {
|
|
return true;
|
|
}
|
|
$user = get_userdata($user_id);
|
|
if (!$user) {
|
|
return false;
|
|
}
|
|
return !empty(array_intersect($user->roles, $allowed_roles));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if a user can compose in a plate (§7.2 板块发帖限制).
|
|
*/
|
|
public function can_compose_in_plate(int $user_id, int $plate_id): bool {
|
|
if ($user_id <= 0) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->is_plate_moderator($user_id, $plate_id)) {
|
|
return true;
|
|
}
|
|
|
|
$add_limit = (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all');
|
|
if ($add_limit === 'all' || $add_limit === 'login') {
|
|
return true;
|
|
}
|
|
|
|
// Moderators already returned true above.
|
|
if ($add_limit === 'moderator') {
|
|
return false;
|
|
}
|
|
|
|
$user = get_userdata($user_id);
|
|
if (!$user) {
|
|
return false;
|
|
}
|
|
|
|
if ($add_limit === 'role') {
|
|
$allowed_roles = get_post_meta($plate_id, '_aksrt_add_limit_roles', true);
|
|
if (!is_array($allowed_roles) || empty($allowed_roles)) {
|
|
return true;
|
|
}
|
|
return !empty(array_intersect($user->roles, $allowed_roles));
|
|
}
|
|
|
|
// Unknown contract → deny (no legacy fallthrough).
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if a user is following a plate (§7.2 关注).
|
|
*/
|
|
public function is_user_following_plate(int $user_id, int $plate_id): bool {
|
|
if ($user_id <= 0) {
|
|
return false;
|
|
}
|
|
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
|
|
if (!is_array($follow_list)) {
|
|
return false;
|
|
}
|
|
return in_array($plate_id, $follow_list, true);
|
|
}
|
|
|
|
/**
|
|
* Toggle user follow on a plate (§7.2 关注/取消关注).
|
|
*
|
|
* When plate has paid follow enabled and user is not following, returns
|
|
* require_pay payload instead of free-following.
|
|
*
|
|
* @return array{followed?:bool,follow_count?:int,require_pay?:bool,pay?:array,message?:string,success?:bool}
|
|
*/
|
|
public function toggle_follow_plate(int $user_id, int $plate_id): array {
|
|
if ($user_id <= 0) {
|
|
return ['followed' => false, 'follow_count' => 0, 'success' => false, 'message' => __('请先登录', 'aksrt')];
|
|
}
|
|
|
|
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
|
|
if (!is_array($follow_list)) {
|
|
$follow_list = [];
|
|
}
|
|
|
|
$followed = in_array($plate_id, $follow_list, true);
|
|
if ($followed) {
|
|
// Unfollow is always free (zibll warns but still allows cancel).
|
|
$follow_list = array_values(array_diff($follow_list, [$plate_id]));
|
|
update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list))));
|
|
$count = $this->count_plate_followers($plate_id);
|
|
update_post_meta($plate_id, '_aksrt_follow_count', $count);
|
|
return [
|
|
'followed' => false,
|
|
'follow_count' => $count,
|
|
'success' => true,
|
|
];
|
|
}
|
|
|
|
// Follow path — paid gate.
|
|
if ($this->is_plate_follow_pay_required($plate_id, $user_id)) {
|
|
$price = $this->resolve_plate_follow_pay_price($plate_id, $user_id);
|
|
return [
|
|
'followed' => false,
|
|
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
|
|
'require_pay' => true,
|
|
'success' => false,
|
|
'message' => __('关注该版块需要付费', 'aksrt'),
|
|
'pay' => $price,
|
|
];
|
|
}
|
|
|
|
$follow_list[] = $plate_id;
|
|
update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list))));
|
|
|
|
$count = $this->count_plate_followers($plate_id);
|
|
update_post_meta($plate_id, '_aksrt_follow_count', $count);
|
|
|
|
return [
|
|
'followed' => true,
|
|
'follow_count' => $count,
|
|
'success' => true,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Whether global+plate paid follow is on and user still needs to pay.
|
|
*/
|
|
public function is_plate_follow_pay_required(int $plate_id, int $user_id): bool {
|
|
if ($user_id <= 0 || $plate_id <= 0) {
|
|
return false;
|
|
}
|
|
if ($this->is_user_following_plate($user_id, $plate_id)) {
|
|
return false;
|
|
}
|
|
if ($this->is_plate_moderator($user_id, $plate_id) || user_can($user_id, 'manage_options')) {
|
|
return false;
|
|
}
|
|
if (!$this->is_plate_follow_pay_enabled($plate_id)) {
|
|
return false;
|
|
}
|
|
// Already purchased plate_follow order.
|
|
if (class_exists('AKSRT_Order_Repository') && class_exists('AKSRT_Domain_Model')) {
|
|
if (AKSRT_Order_Repository::get_instance()->has_paid_access(
|
|
$user_id,
|
|
AKSRT_Domain_Model::ORDER_PRODUCT_PLATE_FOLLOW,
|
|
$plate_id
|
|
)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public function is_plate_follow_pay_enabled(int $plate_id): bool {
|
|
if ($plate_id <= 0) {
|
|
return false;
|
|
}
|
|
// Global switch from forum settings (SPA).
|
|
$global_on = true;
|
|
if (class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_forum_settings')) {
|
|
$cfg = AKSRT_Settings::get_forum_settings();
|
|
// Nested permissions.plate_follow_pay or flat.
|
|
if (isset($cfg['permissions']['plate_follow_pay'])) {
|
|
$global_on = !empty($cfg['permissions']['plate_follow_pay']);
|
|
} elseif (isset($cfg['plate_follow_pay'])) {
|
|
$global_on = !empty($cfg['plate_follow_pay']);
|
|
}
|
|
}
|
|
if (!$global_on) {
|
|
return false;
|
|
}
|
|
if ((int) get_post_meta($plate_id, '_aksrt_follow_pay', true) !== 1) {
|
|
return false;
|
|
}
|
|
$modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money'));
|
|
if ($modo === 'points') {
|
|
return (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true) > 0;
|
|
}
|
|
return (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true) > 0;
|
|
}
|
|
|
|
/**
|
|
* @return array{amount:float|int,modo:string,is_vip_price:bool,is_free:bool,label:string,base:float|int,currency:string}
|
|
*/
|
|
public function resolve_plate_follow_pay_price(int $plate_id, int $user_id = 0): array {
|
|
$modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money'));
|
|
if (!in_array($modo, ['money', 'points'], true)) {
|
|
$modo = 'money';
|
|
}
|
|
|
|
$is_points = $modo === 'points';
|
|
$base = $is_points
|
|
? max(0, (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true))
|
|
: max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true));
|
|
|
|
$vip_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip_price', true);
|
|
$vip_set = ($vip_raw !== '' && $vip_raw !== null && $vip_raw !== false);
|
|
$vip_price = $vip_set
|
|
? ($is_points ? max(0, (int) $vip_raw) : max(0.0, (float) $vip_raw))
|
|
: 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) {
|
|
$amount = $vip_price;
|
|
return [
|
|
'amount' => $amount,
|
|
'modo' => $modo,
|
|
'is_vip_price' => true,
|
|
'is_free' => $amount <= 0,
|
|
'label' => $amount <= 0
|
|
? __('VIP 免费关注', 'aksrt')
|
|
: ($is_points
|
|
? sprintf(__('VIP %d 积分', 'aksrt'), (int) $amount)
|
|
: sprintf(__('VIP ¥%s', 'aksrt'), number_format((float) $amount, 2, '.', ''))),
|
|
'base' => $base,
|
|
'currency' => $is_points ? 'points' : 'CNY',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'amount' => $base,
|
|
'modo' => $modo,
|
|
'is_vip_price' => false,
|
|
'is_free' => false,
|
|
'label' => $is_points
|
|
? sprintf(__('%d 积分', 'aksrt'), (int) $base)
|
|
: sprintf(__('¥%s', 'aksrt'), number_format((float) $base, 2, '.', '')),
|
|
'base' => $base,
|
|
'currency' => $is_points ? 'points' : 'CNY',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Force-add plate to user follow list (after paid success).
|
|
*/
|
|
public function mark_plate_followed(int $user_id, int $plate_id): void {
|
|
if ($user_id <= 0 || $plate_id <= 0) {
|
|
return;
|
|
}
|
|
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
|
|
if (!is_array($follow_list)) {
|
|
$follow_list = [];
|
|
}
|
|
$follow_list[] = $plate_id;
|
|
update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list))));
|
|
$count = $this->count_plate_followers($plate_id);
|
|
update_post_meta($plate_id, '_aksrt_follow_count', $count);
|
|
}
|
|
|
|
/**
|
|
* Purchase plate follow with points or balance (creates plate_follow order when cash).
|
|
*
|
|
* @return array{success:bool,message:string,followed?:bool,follow_count?:int,order_number?:string}
|
|
*/
|
|
public function purchase_plate_follow(int $user_id, int $plate_id): array {
|
|
if ($user_id <= 0) {
|
|
return $this->forum->error(__('请先登录', 'aksrt'));
|
|
}
|
|
$post = get_post($plate_id);
|
|
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
|
|
return $this->forum->error(__('版块不存在', 'aksrt'));
|
|
}
|
|
if ($this->is_user_following_plate($user_id, $plate_id)) {
|
|
return $this->forum->success(__('已关注', 'aksrt'), [
|
|
'followed' => true,
|
|
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
|
|
]);
|
|
}
|
|
if (!$this->is_plate_follow_pay_enabled($plate_id)) {
|
|
// Free follow fallback.
|
|
$this->mark_plate_followed($user_id, $plate_id);
|
|
return $this->forum->success(__('关注成功', 'aksrt'), [
|
|
'followed' => true,
|
|
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
|
|
]);
|
|
}
|
|
|
|
$price = $this->resolve_plate_follow_pay_price($plate_id, $user_id);
|
|
if (!empty($price['is_free'])) {
|
|
$this->mark_plate_followed($user_id, $plate_id);
|
|
return $this->forum->success(__('VIP 免费关注成功', 'aksrt'), [
|
|
'followed' => true,
|
|
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
|
|
'free' => true,
|
|
]);
|
|
}
|
|
|
|
if (!class_exists('AKSRT_Credits')) {
|
|
return $this->forum->error(__('积分系统不可用', 'aksrt'));
|
|
}
|
|
|
|
$modo = (string) ($price['modo'] ?? 'money');
|
|
$amount = $price['amount'] ?? 0;
|
|
|
|
if ($modo === 'points') {
|
|
$cost = max(0, (int) $amount);
|
|
if ($cost <= 0) {
|
|
return $this->forum->error(__('积分价格未设置', 'aksrt'));
|
|
}
|
|
$balance = AKSRT_Credits::get_balance($user_id);
|
|
if ($balance < $cost) {
|
|
return $this->forum->error(sprintf(__('积分不足,需要 %d 积分', 'aksrt'), $cost));
|
|
}
|
|
$deducted = AKSRT_Credits::deduct($user_id, $cost, 'forum_plate_follow', [
|
|
'plate_id' => $plate_id,
|
|
], sprintf(__('付费关注版块「%s」', 'aksrt'), get_the_title($plate_id)));
|
|
if (is_wp_error($deducted) || $deducted === false) {
|
|
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt');
|
|
return $this->forum->error($msg ?: __('积分扣除失败', 'aksrt'));
|
|
}
|
|
$this->mark_plate_followed($user_id, $plate_id);
|
|
$this->grant_plate_follow_author_share($user_id, $plate_id, $cost, true);
|
|
return $this->forum->success(__('关注成功', 'aksrt'), [
|
|
'followed' => true,
|
|
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
|
|
'credits_cost' => $cost,
|
|
]);
|
|
}
|
|
|
|
// Cash via balance credits (same as topic pay).
|
|
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Domain_Model')) {
|
|
return $this->forum->error(__('支付系统不可用', 'aksrt'));
|
|
}
|
|
$cash = max(0.0, (float) $amount);
|
|
if ($cash <= 0) {
|
|
return $this->forum->error(__('价格未设置', 'aksrt'));
|
|
}
|
|
$product_type = AKSRT_Domain_Model::ORDER_PRODUCT_PLATE_FOLLOW;
|
|
$order_number = AKSRT_Order::get_instance()->create($user_id, $product_type, $plate_id, $cash, 'CNY');
|
|
if ($order_number === '') {
|
|
return $this->forum->error(__('创建订单失败', 'aksrt'));
|
|
}
|
|
$credits_cost = (int) ceil($cash * 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($cash, 2, '.', ''),
|
|
$balance
|
|
));
|
|
}
|
|
$deducted = AKSRT_Credits::deduct(
|
|
$user_id,
|
|
$credits_cost,
|
|
'forum_plate_follow_pay',
|
|
['plate_id' => $plate_id, 'order' => $order_number],
|
|
sprintf(__('付费关注版块「%s」', 'aksrt'), get_the_title($plate_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) {
|
|
AKSRT_Credits::add(
|
|
$user_id,
|
|
$credits_cost,
|
|
'forum_plate_follow_refund',
|
|
['plate_id' => $plate_id, 'order' => $order_number],
|
|
sprintf(__('关注失败退款:%s', 'aksrt'), get_the_title($plate_id))
|
|
);
|
|
return $this->forum->error(__('订单确认失败,已尝试退回积分', 'aksrt'));
|
|
}
|
|
|
|
$this->mark_plate_followed($user_id, $plate_id);
|
|
return $this->forum->success(__('关注成功', 'aksrt'), [
|
|
'followed' => true,
|
|
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
|
|
'order_number' => $order_number,
|
|
'amount' => $cash,
|
|
'credits_cost' => $credits_cost,
|
|
]);
|
|
}
|
|
|
|
private function grant_plate_follow_author_share(int $buyer_id, int $plate_id, int $cost, bool $is_points): void {
|
|
if (!$is_points || $cost <= 0 || !class_exists('AKSRT_Credits')) {
|
|
return;
|
|
}
|
|
$post = get_post($plate_id);
|
|
if (!$post) {
|
|
return;
|
|
}
|
|
$author_id = (int) $post->post_author;
|
|
if ($author_id <= 0 || $author_id === $buyer_id) {
|
|
return;
|
|
}
|
|
$author_share = max(0, (int) ($this->forum->get_forum_config()['credits']['author_share_percent'] ?? 70));
|
|
$author_amount = (int) floor($cost * $author_share / 100);
|
|
if ($author_amount <= 0) {
|
|
return;
|
|
}
|
|
AKSRT_Credits::add($author_id, $author_amount, 'forum_plate_follow_income', [
|
|
'plate_id' => $plate_id,
|
|
'buyer_id' => $buyer_id,
|
|
], sprintf(__('版块「%s」被付费关注', 'aksrt'), get_the_title($plate_id)));
|
|
}
|
|
|
|
/**
|
|
* Count followers for a plate.
|
|
*/
|
|
private function count_plate_followers(int $plate_id): int {
|
|
global $wpdb;
|
|
return (int) $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$wpdb->usermeta}
|
|
WHERE meta_key = 'aksrt_follow_plate'
|
|
AND meta_value LIKE %s",
|
|
'%i:' . $plate_id . ';%'
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Get plates a user follows.
|
|
*/
|
|
public function get_user_followed_plates(int $user_id): array {
|
|
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
|
|
if (!is_array($follow_list) || empty($follow_list)) {
|
|
return [];
|
|
}
|
|
|
|
$plates = get_posts([
|
|
'post_type' => 'aksrt_forum_plate',
|
|
'post__in' => array_map('intval', $follow_list),
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => 100,
|
|
'orderby' => 'post__in',
|
|
]);
|
|
|
|
$result = [];
|
|
foreach ($plates as $plate) {
|
|
$result[] = $this->build_plate_payload($plate);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Get a user's managed plates (plates where user is moderator/author).
|
|
*
|
|
* @return array{owned:array,moderating:array}
|
|
*/
|
|
public function get_user_managed_plates(int $user_id): array {
|
|
$owned = [];
|
|
$moderating = [];
|
|
|
|
if (!$this->forum->is_plate_cpt_active()) {
|
|
return ['owned' => $owned, 'moderating' => $moderating];
|
|
}
|
|
|
|
// Plates where user is author (超级版主)
|
|
$own_plates = get_posts([
|
|
'post_type' => 'aksrt_forum_plate',
|
|
'author' => $user_id,
|
|
'post_status' => 'publish',
|
|
'posts_per_page' => 50,
|
|
'orderby' => 'title',
|
|
'order' => 'ASC',
|
|
]);
|
|
foreach ($own_plates as $plate) {
|
|
$owned[] = $this->build_plate_payload($plate);
|
|
}
|
|
|
|
// Plates where user is a moderator (in _aksrt_moderators list)
|
|
global $wpdb;
|
|
$mod_plate_ids = $wpdb->get_col($wpdb->prepare(
|
|
"SELECT pm.post_id FROM {$wpdb->postmeta} pm
|
|
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id AND p.post_type = 'aksrt_forum_plate' AND p.post_status = 'publish'
|
|
WHERE pm.meta_key = '_aksrt_moderators'",
|
|
));
|
|
|
|
foreach ($mod_plate_ids as $plate_id) {
|
|
$plate_id = (int) $plate_id;
|
|
// Skip plates already in owned
|
|
if (in_array($plate_id, wp_list_pluck($own_plates, 'ID'), true)) {
|
|
continue;
|
|
}
|
|
$mods = get_post_meta($plate_id, '_aksrt_moderators', true);
|
|
if (is_array($mods) && in_array($user_id, $mods, true)) {
|
|
$plate = get_post($plate_id);
|
|
if ($plate) {
|
|
$moderating[] = $this->build_plate_payload($plate);
|
|
}
|
|
}
|
|
}
|
|
|
|
return ['owned' => $owned, 'moderating' => $moderating];
|
|
}
|
|
|
|
// ── Plate utilities (extracted from facade) ──
|
|
|
|
public function increment_plate_views(int $plate_id): void {
|
|
if ($plate_id <= 0 || !$this->forum->is_plate_cpt_active()) {
|
|
return;
|
|
}
|
|
$views = (int) get_post_meta($plate_id, '_aksrt_views', true);
|
|
update_post_meta($plate_id, '_aksrt_views', $views + 1);
|
|
}
|
|
|
|
/**
|
|
* Check if a plate qualifies as "hot" (§7.2 热门版块).
|
|
*/
|
|
public function is_hot_plate(int $plate_id): bool {
|
|
$config = $this->forum->get_config();
|
|
$threshold = (int) ($config['other']['hot_plate_threshold'] ?? 100);
|
|
$posts_count = (int) get_post_meta($plate_id, '_aksrt_posts_count', true);
|
|
$follow_count = (int) get_post_meta($plate_id, '_aksrt_follow_count', true);
|
|
return ($posts_count + $follow_count * 2) >= $threshold;
|
|
}
|
|
|
|
/**
|
|
* Get plate categories (分区) with moderators.
|
|
*/
|
|
public function get_plate_categories(): array {
|
|
$terms = get_terms([
|
|
'taxonomy' => 'aksrt_plate_cat',
|
|
'hide_empty' => false,
|
|
'orderby' => 'name',
|
|
'order' => 'ASC',
|
|
]);
|
|
|
|
if (is_wp_error($terms) || empty($terms)) {
|
|
return [];
|
|
}
|
|
|
|
$categories = [];
|
|
foreach ($terms as $term) {
|
|
$term_id = (int) $term->term_id;
|
|
$moderator_ids = get_term_meta($term_id, '_aksrt_cat_moderator', true);
|
|
if (!is_array($moderator_ids)) {
|
|
$moderator_ids = [];
|
|
}
|
|
|
|
$categories[] = [
|
|
'id' => $term_id,
|
|
'name' => (string) $term->name,
|
|
'description' => (string) $term->description,
|
|
'slug' => (string) $term->slug,
|
|
'count' => (int) $term->count,
|
|
'parent_id' => (int) $term->parent,
|
|
'moderators' => array_values(array_unique(array_filter(array_map('intval', $moderator_ids)))),
|
|
'add_limit' => (string) (get_term_meta($term_id, '_aksrt_add_limit', true) ?: 'all'),
|
|
'url' => (string) get_term_link($term),
|
|
];
|
|
}
|
|
|
|
return $categories;
|
|
}
|
|
}
|