feat: remove forum feature (Phase 1)

This commit is contained in:
kate522 committed 2026-08-02 20:02:44 +08:00
1 parent f3e4cdd9cf
commit 2bbd7c01a3
173 files changed
+51 -27360

No files matched your search

-2
View File
@@ -182,8 +182,6 @@ class Admin {
PaymentController::class,
ToolsController::class,
ImportExportController::class,
ShopController::class,
ForumController::class,
ExtensionController::class,
CommentController::class,
MessageController::class,
-411
View File
@@ -1,411 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
class ForumController extends ControllerBase {
use ForumControllerPart1;
use ForumControllerPart2;
private static $instance = null;
private $admin;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function set_admin(Admin $admin): void {
$this->admin = $admin;
}
public function register_routes(): void {
register_rest_route(self::API_NAMESPACE, '/forum/overview', [
'methods' => 'GET',
'callback' => [$this, 'get_overview'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/boards', [
'methods' => 'GET',
'callback' => [$this, 'get_boards'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/boards', [
'methods' => 'POST',
'callback' => [$this, 'create_board'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/boards/(?P<id>\d+)', [
'methods' => 'POST',
'callback' => [$this, 'update_board'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/boards/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [$this, 'delete_board'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/boards/reorder', [
'methods' => 'POST',
'callback' => [$this, 'reorder_boards'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/topics', [
'methods' => 'GET',
'callback' => [$this, 'get_topics'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_topic_detail'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)/moderate', [
'methods' => 'POST',
'callback' => [$this, 'moderate_topic'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)/vote', [
'methods' => 'POST',
'callback' => [$this, 'vote_topic'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/update-topic', [
'methods' => 'POST',
'callback' => [$this, 'update_topic_legacy'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/moderators', [
'methods' => 'GET',
'callback' => [$this, 'get_moderators'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/moderators', [
'methods' => 'POST',
'callback' => [$this, 'update_moderators'],
'permission_callback' => [$this, 'check_permission'],
]);
// Change moderator role
register_rest_route(self::API_NAMESPACE, '/forum/moderators/role', [
'methods' => 'POST',
'callback' => [$this, 'change_moderator_role'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/moderator/apply', [
'methods' => 'POST',
'callback' => [$this, 'apply_moderator'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/moderator/applications', [
'methods' => 'GET',
'callback' => [$this, 'get_moderator_applications'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/moderator/applications/(?P<id>[A-Za-z0-9-]+)/review', [
'methods' => 'POST',
'callback' => [$this, 'review_moderator_application'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)/favorite', [
'methods' => 'POST',
'callback' => [$this, 'toggle_favorite'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)/score', [
'methods' => 'POST',
'callback' => [$this, 'score_topic'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/topics/(?P<id>\d+)/adopt', [
'methods' => 'POST',
'callback' => [$this, 'adopt_answer'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
// §7.0 Plate CPT endpoints
register_rest_route(self::API_NAMESPACE, '/forum/plates', [
'methods' => 'GET',
'callback' => [$this, 'get_plates'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/plates', [
'methods' => 'POST',
'callback' => [$this, 'create_plate'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [$this, 'get_plate'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P<id>\d+)', [
'methods' => 'POST',
'callback' => [$this, 'update_plate'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [$this, 'delete_plate'],
'permission_callback' => [$this, 'check_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/plates/reorder', [
'methods' => 'POST',
'callback' => [$this, 'reorder_plates'],
'permission_callback' => [$this, 'check_permission'],
]);
// §7.2 Follow plate
register_rest_route(self::API_NAMESPACE, '/forum/plates/(?P<id>\d+)/follow', [
'methods' => 'POST',
'callback' => [$this, 'toggle_follow_plate'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
// §7.2 Plate categories (分区)
register_rest_route(self::API_NAMESPACE, '/forum/plate-categories', [
'methods' => 'GET',
'callback' => [$this, 'get_plate_categories'],
'permission_callback' => [$this, 'check_permission'],
]);
// §7.2 Sorted plates
register_rest_route(self::API_NAMESPACE, '/forum/plates/sorted', [
'methods' => 'GET',
'callback' => [$this, 'get_sorted_plates'],
'permission_callback' => '__return_true',
]);
// §7.2 Front-end plate CRUD (forum admin/moderator)
register_rest_route(self::API_NAMESPACE, '/forum/front/plates', [
'methods' => 'POST',
'callback' => [$this, 'frontend_create_plate'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/front/plates/(?P<id>\d+)', [
'methods' => 'POST',
'callback' => [$this, 'frontend_update_plate'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
register_rest_route(self::API_NAMESPACE, '/forum/front/plates/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [$this, 'frontend_delete_plate'],
'permission_callback' => [$this, 'check_logged_in_permission'],
]);
}
public function get_overview(): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->success(['data' => ['stats' => [], 'boards' => [], 'recent_topics' => [], 'pending_applications' => []]]);
}
$pending_topics = $forum->get_topic_list([
'limit' => 6,
'status' => 'pending',
'review_status' => \AKSRT\Forum\Forum::REVIEW_PENDING,
'viewer_user_id' => get_current_user_id(),
'include_hidden' => true,
]);
return $this->success([
'data' => [
'stats' => $forum->get_stats(),
'boards' => array_slice($forum->get_boards(), 0, 6),
'pending_topics' => $pending_topics['items'],
'pending_applications' => array_slice($forum->get_moderator_application_list(['status' => \AKSRT\Forum\Forum::APPLICATION_PENDING]), 0, 6),
],
]);
}
public function get_boards(): \WP_REST_Response {
$forum = $this->get_forum_service();
$items = $forum ? $forum->get_boards() : [];
return $this->success([
'data' => [
'items' => $items,
'total' => count($items),
],
]);
}
public function create_board(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$params = $this->get_json_params($request);
$name = sanitize_text_field((string) ($params['name'] ?? ''));
if ($name === '') {
return $this->error(__('版块名称不能为空', 'aksrt'));
}
$term_id = $forum->create_board($params);
if ($term_id <= 0) {
return $this->error(__('创建版块失败', 'aksrt'));
}
return $this->success([
'message' => __('版块已创建', 'aksrt'),
'data' => ['id' => $term_id],
], 201);
}
public function update_board(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$board_id = (int) $request->get_param('id');
if ($board_id <= 0) {
return $this->error(__('参数无效', 'aksrt'));
}
$success = $forum->update_board($board_id, $this->get_json_params($request));
if (!$success) {
return $this->error(__('版块更新失败', 'aksrt'));
}
return $this->success(['message' => __('版块已更新', 'aksrt')]);
}
public function delete_board(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$board_id = (int) $request->get_param('id');
if ($board_id <= 0) {
return $this->error(__('参数无效', 'aksrt'));
}
$success = $forum->delete_board($board_id);
if (!$success) {
return $this->error(__('版块删除失败', 'aksrt'));
}
return $this->success(['message' => __('版块已删除', 'aksrt')]);
}
public function reorder_boards(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$params = $this->get_json_params($request);
$order = $params['order'] ?? [];
if (!is_array($order) || empty($order)) {
return $this->error(__('排序参数无效', 'aksrt'));
}
$forum->reorder_boards($order);
return $this->success(['message' => __('排序已更新', 'aksrt')]);
}
// ================================================================
// §7.0 Plate CPT handlers
// ================================================================
public function get_plates(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->success(['data' => ['items' => [], 'total' => 0]]);
}
$args = [];
$parent = $request->get_param('parent');
if ($parent !== null) {
$args['parent'] = (int) $parent;
}
$plate_cat = $request->get_param('plate_cat');
if ($plate_cat) {
$args['plate_cat'] = (int) $plate_cat;
}
$items = $forum->get_plates($args);
return $this->success([
'data' => [
'items' => $items,
'total' => count($items),
],
]);
}
public function get_plate(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$plate_id = (int) $request->get_param('id');
$plate = $forum->get_plate($plate_id);
if (!$plate) {
return $this->error(__('版块不存在', 'aksrt'), 404);
}
return $this->success(['data' => $plate]);
}
public function create_plate(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$params = $this->get_json_params($request);
$name = sanitize_text_field((string) ($params['name'] ?? ''));
if ($name === '') {
return $this->error(__('版块名称不能为空', 'aksrt'));
}
$plate_id = $forum->create_plate($params);
if ($plate_id <= 0) {
return $this->error(__('创建版块失败', 'aksrt'));
}
return $this->success([
'message' => __('版块已创建', 'aksrt'),
'data' => ['id' => $plate_id],
], 201);
}
}
-390
View File
@@ -1,390 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ForumControllerPart1 {
public function update_plate(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$plate_id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$result = $forum->update_plate($plate_id, $params);
if (!$result) {
return $this->error(__('更新版块失败', 'aksrt'));
}
return $this->success(['message' => __('版块已更新', 'aksrt')]);
}
public function delete_plate(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$plate_id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$cascade = sanitize_key((string) ($params['cascade'] ?? 'trash'));
$move_to = (int) ($params['move_to'] ?? 0);
$result = $forum->delete_plate($plate_id, $cascade, $move_to);
if (!$result) {
return $this->error(__('删除版块失败', 'aksrt'));
}
return $this->success(['message' => __('版块已删除', 'aksrt')]);
}
public function reorder_plates(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$params = $this->get_json_params($request);
$order = $params['order'] ?? [];
if (!is_array($order) || empty($order)) {
return $this->error(__('排序参数无效', 'aksrt'));
}
$forum->reorder_plates($order);
return $this->success(['message' => __('排序已更新', 'aksrt')]);
}
public function toggle_follow_plate(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$plate_id = (int) $request->get_param('id');
$user_id = get_current_user_id();
$result = $forum->toggle_follow_plate($user_id, $plate_id);
return $this->success($result);
}
public function get_plate_categories(): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->success(['data' => []]);
}
return $this->success(['data' => $forum->get_plate_categories()]);
}
public function get_sorted_plates(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->success(['data' => []]);
}
$orderby = sanitize_key((string) ($request->get_param('orderby') ?: 'menu_order'));
$order = sanitize_key((string) ($request->get_param('order') ?: 'ASC'));
$limit = max(1, min(100, (int) ($request->get_param('limit') ?: 50)));
return $this->success(['data' => $forum->get_sorted_plates($orderby, $order, $limit)]);
}
public function get_topics(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'total_pages' => 1]]);
}
$args = [
'page' => max(1, (int) ($request->get_param('page') ?: 1)),
'per_page' => max(1, min(100, (int) ($request->get_param('per_page') ?: 20))),
'status' => sanitize_key((string) ($request->get_param('status') ?: 'all')),
'board_id' => (int) ($request->get_param('board_id') ?: 0),
'visibility' => sanitize_key((string) ($request->get_param('visibility') ?: '')),
'review_status' => sanitize_key((string) ($request->get_param('review_status') ?: '')),
'keyword' => sanitize_text_field((string) ($request->get_param('keyword') ?: '')),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?: 'date')),
'viewer_user_id' => get_current_user_id(),
'include_hidden' => true,
];
$view = $forum->get_topic_list($args);
return $this->success([
'data' => [
'items' => $view['items'],
'total' => $view['total'],
'page' => $view['page'],
'total_pages' => $view['max_pages'],
'options' => [
'boards' => $forum->get_boards(),
'visibility' => $forum->get_visibility_options(),
'review' => $forum->get_review_options(),
'status' => [
['label' => __('全部', 'aksrt'), 'value' => 'all'],
['label' => __('已发布', 'aksrt'), 'value' => 'publish'],
['label' => __('待审核', 'aksrt'), 'value' => 'pending'],
['label' => __('草稿/拒绝', 'aksrt'), 'value' => 'draft'],
],
],
],
]);
}
public function get_topic_detail(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$topic_id = (int) $request->get_param('id');
if ($topic_id <= 0) {
return $this->error(__('参数无效', 'aksrt'));
}
return $this->success([
'data' => $forum->get_topic_detail($topic_id, get_current_user_id()),
]);
}
public function moderate_topic(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$topic_id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$action = sanitize_key((string) ($params['action'] ?? ''));
if ($topic_id <= 0 || $action === '') {
return $this->error(__('参数无效', 'aksrt'));
}
$result = $forum->moderate_topic($topic_id, $action, $params, get_current_user_id());
if (is_wp_error($result)) {
return $this->error($result->get_error_message(), $result->get_error_code() === 'forbidden' ? 403 : 400);
}
return $this->success([
'message' => $result['message'] ?? __('治理动作已执行', 'aksrt'),
'data' => $result,
]);
}
public function vote_topic(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$topic_id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$option_ids = $params['option_ids'] ?? [];
if (!is_array($option_ids)) {
$option_ids = [];
}
$result = $forum->submit_vote($topic_id, get_current_user_id(), $option_ids);
if (is_wp_error($result)) {
$status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
return $this->error($result->get_error_message(), $status);
}
return $this->success([
'message' => $result['message'] ?? __('投票成功', 'aksrt'),
'data' => $result,
]);
}
/**
* Legacy admin moderation endpoint kept for compatibility.
*/
public function update_topic_legacy(\WP_REST_Request $request): \WP_REST_Response {
$params = $this->get_json_params($request);
$topic_id = (int) ($params['topic_id'] ?? 0);
if ($topic_id <= 0) {
return $this->error(__('参数无效', 'aksrt'));
}
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$actions = [];
if (isset($params['sticky'])) {
$actions[] = ['action' => 'sticky', 'value' => !empty($params['sticky'])];
}
if (isset($params['featured'])) {
$actions[] = ['action' => 'featured', 'value' => !empty($params['featured'])];
}
if (isset($params['locked'])) {
$actions[] = ['action' => 'locked', 'value' => !empty($params['locked'])];
}
foreach ($actions as $action) {
$result = $forum->moderate_topic($topic_id, $action['action'], ['value' => $action['value']], get_current_user_id());
if (is_wp_error($result)) {
return $this->error($result->get_error_message());
}
}
return $this->success(['message' => __('治理状态已更新', 'aksrt')]);
}
public function get_moderators(): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->success(['data' => ['boards' => []]]);
}
$board_id = isset($_GET['board_id']) ? (int) $_GET['board_id'] : 0;
$result = [];
foreach ($forum->get_boards() as $board) {
$current_board_id = (int) ($board['id'] ?? 0);
if ($board_id > 0 && $current_board_id !== $board_id) {
continue;
}
$result[] = [
'board_id' => $current_board_id,
'board_name' => (string) ($board['name'] ?? ''),
'topic_count' => (int) ($board['post_count'] ?? 0),
'pending_application_count' => (int) ($board['pending_application_count'] ?? 0),
'moderators' => $forum->get_board_moderator_profiles_with_roles($current_board_id),
];
}
return $this->success(['data' => ['boards' => $result]]);
}
public function update_moderators(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$params = $this->get_json_params($request);
$board_id = (int) ($params['board_id'] ?? 0);
$user_id = (int) ($params['user_id'] ?? 0);
$action = sanitize_key((string) ($params['action'] ?? ''));
$role = sanitize_key((string) ($params['role'] ?? 'moderator'));
if ($board_id <= 0 || $user_id <= 0 || !in_array($action, ['add', 'remove'], true)) {
return $this->error(__('参数无效', 'aksrt'));
}
$valid_roles = ['super_moderator', 'moderator', 'junior_moderator'];
if (!in_array($role, $valid_roles, true)) {
$role = 'moderator';
}
$board = get_term($board_id, 'aksrt_forum_board');
if (!$board || is_wp_error($board)) {
return $this->error(__('版块不存在', 'aksrt'), 404);
}
$user = get_userdata($user_id);
if (!$user) {
return $this->error(__('用户不存在', 'aksrt'), 404);
}
if ($action === 'add') {
$forum->add_moderator($user_id, $board_id, $role);
} else {
$forum->remove_moderator($user_id, $board_id);
}
return $this->success([
'message' => $action === 'add' ? __('版主已添加', 'aksrt') : __('版主已移除', 'aksrt'),
]);
}
public function change_moderator_role(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$params = $this->get_json_params($request);
$board_id = (int) ($params['board_id'] ?? 0);
$user_id = (int) ($params['user_id'] ?? 0);
$role = sanitize_key((string) ($params['role'] ?? ''));
if ($board_id <= 0 || $user_id <= 0 || !$role) {
return $this->error(__('参数无效', 'aksrt'));
}
$valid_roles = ['super_moderator', 'moderator', 'junior_moderator'];
if (!in_array($role, $valid_roles, true)) {
return $this->error(__('无效的角色', 'aksrt'));
}
$result = $forum->set_moderator_role($user_id, $board_id, $role);
if (!$result) {
return $this->error(__('设置角色失败,用户可能不是该版块版主', 'aksrt'));
}
return $this->success(['message' => __('角色已更新', 'aksrt')]);
}
public function apply_moderator(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$params = $this->get_json_params($request);
$board_id = (int) ($params['board_id'] ?? 0);
$reason = sanitize_textarea_field((string) ($params['reason'] ?? ''));
$result = $forum->apply_moderator(get_current_user_id(), $board_id, $reason);
if (is_wp_error($result)) {
$status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
return $this->error($result->get_error_message(), $status);
}
return $this->success([
'message' => $result['message'] ?? __('申请已提交', 'aksrt'),
'data' => $result,
]);
}
public function get_moderator_applications(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->success(['data' => ['items' => [], 'total' => 0]]);
}
$items = $forum->get_moderator_application_list([
'status' => sanitize_key((string) ($request->get_param('status') ?: '')),
'board_id' => (int) ($request->get_param('board_id') ?: 0),
'keyword' => sanitize_text_field((string) ($request->get_param('keyword') ?: '')),
]);
return $this->success([
'data' => [
'items' => $items,
'total' => count($items),
'options' => [
'boards' => $forum->get_boards(),
'status' => [
['label' => __('全部', 'aksrt'), 'value' => ''],
['label' => __('待处理', 'aksrt'), 'value' => \AKSRT\Forum\Forum::APPLICATION_PENDING],
['label' => __('已通过', 'aksrt'), 'value' => \AKSRT\Forum\Forum::APPLICATION_APPROVED],
['label' => __('已拒绝', 'aksrt'), 'value' => \AKSRT\Forum\Forum::APPLICATION_REJECTED],
],
],
],
]);
}
}
-188
View File
@@ -1,188 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin;
defined('ABSPATH') || exit;
trait ForumControllerPart2 {
public function review_moderator_application(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$application_id = (string) $request->get_param('id');
$params = $this->get_json_params($request);
$status = sanitize_key((string) ($params['status'] ?? ''));
$note = sanitize_textarea_field((string) ($params['note'] ?? ''));
$result = $forum->review_moderator_application($application_id, $status, $note, get_current_user_id());
if (is_wp_error($result)) {
return $this->error($result->get_error_message());
}
return $this->success([
'message' => $result['message'] ?? __('申请状态已更新', 'aksrt'),
'data' => $result,
]);
}
public function toggle_favorite(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$topic_id = (int) $request->get_param('id');
$result = $forum->toggle_favorite(get_current_user_id(), $topic_id);
if (is_wp_error($result)) {
$status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
return $this->error($result->get_error_message(), $status);
}
return $this->success($result);
}
public function score_topic(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$topic_id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$amount = (int) ($params['amount'] ?? 0);
$result = $forum->score_topic(get_current_user_id(), $topic_id, $amount);
if (is_wp_error($result)) {
$status = $result->get_error_code() === 'not_logged_in' ? 401 : 400;
return $this->error($result->get_error_message(), $status);
}
return $this->success($result);
}
public function adopt_answer(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$topic_id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$comment_id = (int) ($params['comment_id'] ?? 0);
$result = $forum->adopt_answer($topic_id, $comment_id, get_current_user_id());
if (is_wp_error($result)) {
$status = $result->get_error_code() === 'forbidden' ? 403 : 400;
return $this->error($result->get_error_message(), $status);
}
return $this->success($result);
}
public function check_logged_in_permission(): bool {
return is_user_logged_in();
}
private function get_forum_service(): ?\AKSRT\Forum\Forum {
return class_exists('\AKSRT\Forum\Forum') ? \AKSRT\Forum\Forum::get_instance() : null;
}
public function frontend_create_plate(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$user_id = get_current_user_id();
if (!$forum->is_forum_admin($user_id)) {
return $this->error(__('仅论坛管理员可创建版块', 'aksrt'), 403);
}
$params = $this->get_json_params($request);
$data = [
'name' => sanitize_text_field((string) ($params['name'] ?? '')),
'description' => sanitize_textarea_field((string) ($params['description'] ?? '')),
'icon' => sanitize_text_field((string) ($params['icon'] ?? '')),
'cover_url' => esc_url_raw((string) ($params['cover_url'] ?? '')),
'parent_id' => (int) ($params['parent_id'] ?? 0),
'plate_cat' => (int) ($params['plate_cat'] ?? 0),
];
if ($data['name'] === '') {
return $this->error(__('版块名称不能为空', 'aksrt'));
}
$result = $forum->create_plate($data);
if (is_wp_error($result)) {
return $this->error($result->get_error_message());
}
return $this->success($result);
}
public function frontend_update_plate(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$user_id = get_current_user_id();
$plate_id = (int) $request->get_param('id');
$post = get_post($plate_id);
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
return $this->error(__('版块不存在', 'aksrt'));
}
$is_author = (int) $post->post_author === $user_id;
if (!$is_author && !$forum->is_forum_admin($user_id)) {
return $this->error(__('无权编辑此版块', 'aksrt'), 403);
}
$params = $this->get_json_params($request);
$data = [
'name' => sanitize_text_field((string) ($params['name'] ?? '')),
'description' => sanitize_textarea_field((string) ($params['description'] ?? '')),
'icon' => sanitize_text_field((string) ($params['icon'] ?? '')),
'cover_url' => esc_url_raw((string) ($params['cover_url'] ?? '')),
];
$result = $forum->update_plate($plate_id, $data);
if (is_wp_error($result)) {
return $this->error($result->get_error_message());
}
return $this->success($result);
}
public function frontend_delete_plate(\WP_REST_Request $request): \WP_REST_Response {
$forum = $this->get_forum_service();
if (!$forum) {
return $this->error(__('论坛系统未启用', 'aksrt'));
}
$user_id = get_current_user_id();
if (!$forum->is_forum_admin($user_id)) {
return $this->error(__('仅论坛管理员可删除版块', 'aksrt'), 403);
}
$plate_id = (int) $request->get_param('id');
$params = $this->get_json_params($request);
$cascade = sanitize_key((string) ($params['cascade'] ?? 'trash'));
$move_to = (int) ($params['move_to'] ?? 0);
$result = $forum->delete_plate($plate_id, $cascade, $move_to);
if (!$result) {
return $this->error(__('删除版块失败', 'aksrt'));
}
return $this->success(['message' => __('版块已删除', 'aksrt')]);
}
}
+1 -1
View File
@@ -185,7 +185,7 @@ trait GlobalControllerPart1 {
}
private function sanitize_floating_button_item(array $item): array {
$valid_types = ['back_top', 'theme_mode', 'qrcode', 'service_qq', 'service_wechat', 'pay_vip', 'custom', 'add', 'shop_cart'];
$valid_types = ['back_top', 'theme_mode', 'qrcode', 'service_qq', 'service_wechat', 'pay_vip', 'custom', 'add'];
$type = sanitize_text_field($item['type'] ?? 'custom');
if (!in_array($type, $valid_types, true)) {
$type = 'custom';
-1
View File
@@ -92,7 +92,6 @@ class PayOrders {
'vip' => __('VIP', 'aksrt'),
'recharge' => __('充值', 'aksrt'),
'shop_product' => __('商城商品', 'aksrt'),
'plate_follow' => __('版块关注', 'aksrt'),
];
echo '<ul class="subsubsub" style="float:none;margin:8px 0 12px;">';
$tab_bits = [];
+1 -1
View File
@@ -573,7 +573,7 @@ class UsersMenus {
}
echo '</select></label></p>';
echo '<p><label>' . esc_html__('范围', 'aksrt') . '<br><select name="scope">';
foreach (['global' => __('全局', 'aksrt'), 'forum' => __('论坛', 'aksrt'), 'shop' => __('商城', 'aksrt'), 'comment' => __('评论', 'aksrt')] as $key => $label) {
foreach (['global' => __('全局', 'aksrt'), 'comment' => __('评论', 'aksrt')] as $key => $label) {
printf('<option value="%s">%s</option>', esc_attr($key), esc_html($label));
}
echo '</select></label></p>';
@@ -28,7 +28,6 @@ class SchemaExtensionsAudit {
['key' => 'scope', 'title' => '审核范围', 'fields' => [
['key' => 'content_audit_text_enabled', 'label' => '文本审核', 'type' => 'switch', 'default' => true],
['key' => 'content_audit_image_enabled', 'label' => '图片审核', 'type' => 'switch', 'default' => false],
['key' => 'audit_bbs_term', 'label' => '审核论坛分类', 'type' => 'switch', 'default' => false],
]],
['key' => 'fallback', 'title' => '失败策略', 'fields' => [
['key' => 'content_audit_on_failure', 'label' => '审核失败时', 'type' => 'radio', 'default' => 'allow', 'options' => [
@@ -81,9 +81,6 @@ class SchemaExtensionsSecurity {
['key' => 'audit_msg_private', 'label' => '审核私信', 'type' => 'switch', 'default' => false],
['key' => 'audit_be_like_text', 'label' => '文本允许疑似合规', 'type' => 'switch', 'default' => false],
['key' => 'audit_be_like_img', 'label' => '图片允许疑似合规', 'type' => 'switch', 'default' => false],
['key' => 'audit_bbs_posts', 'label' => '审核论坛帖子', 'type' => 'switch', 'default' => false],
['key' => 'audit_bbs_plate', 'label' => '审核论坛板块', 'type' => 'switch', 'default' => false],
['key' => 'audit_bbs_term', 'label' => '审核论坛分类', 'type' => 'switch', 'default' => false],
]],
],
];
-126
View File
@@ -1,126 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaForum {
public static function build(): array {
return [
'domain' => 'forum',
'title' => '社区&论坛',
'sections' => [
'basic' => SchemaForumBasic::build(),
'names' => SchemaForumNames::build(),
'homepage' => SchemaForumHomepage::build(),
'board_page' => SchemaForumBoardPage::build(),
'post_page' => SchemaForumPostPage::build(),
'reply' => SchemaForumReply::build(),
'permissions' => SchemaForumPermissions::build(),
'upload' => SchemaForumUpload::build(),
'credits' => SchemaForumCredits::build(),
'other' => SchemaForumOther::build(),
],
];
}
// ================================================================
// §8.1 Capability matrix schema builder
// ================================================================
/**
* Build groups for the capabilities section from cap_definitions().
*
* Each capability becomes a group with role columns as fields.
* Grouped by prefix: user_, new_post_, comment_.
*/
private static function build_cap_matrix_groups(): array {
if (!class_exists('\AKSRT\Settings\Settings')) {
return [];
}
$defs = \AKSRT\Settings\Settings::cap_definitions();
$cols = \AKSRT\Settings\Settings::cap_role_columns();
$col_labels = [
'all' => __('所有人', 'aksrt'),
'logged' => __('已登录', 'aksrt'),
'level' => __('等级≥', 'aksrt'),
'vip' => __('VIP≥', 'aksrt'),
'auth' => __('已认证', 'aksrt'),
'moderator' => __('版主', 'aksrt'),
'plate_author' => __('超级版主', 'aksrt'),
'cat_moderator' => __('分区版主', 'aksrt'),
];
// Group caps by prefix
$buckets = [
'user' => ['title' => __('用户管理', 'aksrt'), 'caps' => []],
'post' => ['title' => __('投稿权限', 'aksrt'), 'caps' => []],
'comment' => ['title' => __('评论权限', 'aksrt'), 'caps' => []],
];
foreach ($defs as $cap_id => $def) {
if (str_starts_with($cap_id, 'comment_')) {
$buckets['comment']['caps'][$cap_id] = $def;
} elseif (str_starts_with($cap_id, 'new_post_')) {
$buckets['post']['caps'][$cap_id] = $def;
} else {
$buckets['user']['caps'][$cap_id] = $def;
}
}
$groups = [];
foreach ($buckets as $bucket_key => $bucket) {
if (empty($bucket['caps'])) continue;
$fields = [];
foreach ($bucket['caps'] as $cap_id => $def) {
$excludes = $def['exclude_roles'] ?? [];
$only_roles = $def['roles'] ?? null;
$defaults = $def['default'] ?? [];
// Each cap gets one field per role column
foreach ($cols as $col) {
if (in_array($col, $excludes, true)) continue;
if ($only_roles !== null && !in_array($col, $only_roles, true)
&& !in_array($col, ['all', 'logged', 'level', 'vip', 'auth'], true)) {
continue;
}
$field_key = 'user_caps.' . $cap_id . '.' . $col;
$is_spinner = in_array($col, ['level', 'vip'], true);
$field = [
'key' => $field_key,
'label' => $def['label'] . ' — ' . ($col_labels[$col] ?? $col),
'type' => $is_spinner ? 'number' : 'switch',
'default' => $defaults[$col] ?? ($is_spinner ? -1 : false),
'cap_id' => $cap_id,
'cap_col' => $col,
];
if ($is_spinner) {
$field['min'] = -1;
$field['max'] = $col === 'level' ? 10 : 2;
$field['help'] = __('-1 = 不限', 'aksrt');
}
$fields[] = $field;
}
}
$groups[] = [
'key' => 'cap_' . $bucket_key,
'title' => $bucket['title'],
'fields' => $fields,
];
}
return $groups;
}
}
@@ -1,28 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaForumBasic {
public static function build(): array {
return [
'key' => 'basic',
'title' => '基础设置',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'switch', 'title' => '功能开关', 'fields' => [
['key' => 'bbs_s', 'label' => '启用论坛', 'type' => 'switch', 'default' => true, 'help' => '关闭后论坛页面将返回 404'],
]],
['key' => 'admin', 'title' => '论坛管理', 'fields' => [
['key' => 'bbs_admin_users', 'label' => '论坛管理员', 'type' => 'user_select', 'multiple' => true, 'default' => [], 'help' => '拥有跨版块全部管理权限,绕过 ban'],
]],
],
];
}
}
@@ -1,34 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaForumBoardPage {
public static function build(): array {
return [
'key' => 'board_page',
'title' => '版块页面',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'layout', 'title' => '版块页布局', 'fields' => [
['key' => 'bbs_plate_top_info_s', 'label' => '版块信息卡', 'type' => 'switch', 'default' => true, 'help' => '版块页顶部显示版块详情卡片'],
['key' => 'bbs_plate_tab', 'label' => '版块栏目', 'type' => 'group', 'default' => [
['label' => '全部', 'sort' => 'date'],
['label' => '推荐', 'sort' => 'featured'],
['label' => '最新回复', 'sort' => 'last_reply'],
['label' => '热门', 'sort' => 'hot'],
['label' => '精华', 'sort' => 'essence'],
]],
['key' => 'bbs_plate_tab_active_index', 'label' => '默认选中 Tab', 'type' => 'number', 'default' => 0, 'min' => 0],
['key' => 'bbs_plate_tab_swiper', 'label' => '移动端滑动切换', 'type' => 'switch', 'default' => true],
]],
],
];
}
}
@@ -1,34 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaForumCredits {
public static function build(): array {
return [
'key' => 'credits',
'title' => '积分设置',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'earn', 'title' => '积分奖励', 'fields' => [
['key' => 'bbs_credits_post_new', 'label' => '发帖奖励', 'type' => 'number', 'default' => 5, 'min' => 0],
['key' => 'bbs_credits_plate_new', 'label' => '建版奖励', 'type' => 'number', 'default' => 10, 'min' => 0],
['key' => 'bbs_credits_comment_new', 'label' => '回复奖励', 'type' => 'number', 'default' => 2, 'min' => 0],
['key' => 'bbs_credits_comment_like', 'label' => '评论被赞', 'type' => 'number', 'default' => 1, 'min' => 0],
['key' => 'bbs_credits_score_extra', 'label' => '被加分', 'type' => 'number', 'default' => 3, 'min' => 0],
['key' => 'bbs_credits_essence', 'label' => '被设精华', 'type' => 'number', 'default' => 10, 'min' => 0],
['key' => 'bbs_credits_posts_hot', 'label' => '成为热帖', 'type' => 'number', 'default' => 5, 'min' => 0],
['key' => 'bbs_hot_recommend_threshold', 'label' => '热帖推荐分阈值', 'type' => 'number', 'default' => 500, 'min' => 1, 'help' => '推荐分达到该值触发热帖奖励与勋章累计'],
['key' => 'bbs_credits_adopted', 'label' => '被采纳', 'type' => 'number', 'default' => 20, 'min' => 0],
['key' => 'bbs_credits_daily_max', 'label' => '每日积分上限', 'type' => 'number', 'default' => 50, 'min' => 0, 'help' => '0 = 不限'],
]],
],
];
}
}
@@ -1,32 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaForumHomepage {
public static function build(): array {
return [
'key' => 'homepage',
'title' => '首页设置',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'tabs', 'title' => '首页 Tab', 'fields' => [
['key' => 'bbs_home_tab', 'label' => '首页栏目', 'type' => 'group', 'default' => [
['type' => 'follow', 'label' => '关注'],
['type' => 'synthesis', 'label' => '综合'],
['type' => 'plate', 'label' => '版块'],
], 'help' => '可拖拽排序,支持 follow / synthesis / plate / tabs 类型'],
['key' => 'bbs_home_tab_active_index', 'label' => '默认选中 Tab', 'type' => 'number', 'default' => 1, 'min' => 0],
['key' => 'bbs_home_tab_quick_posts_s', 'label' => '快速发帖', 'type' => 'switch', 'default' => true, 'help' => '首页顶部显示快速发帖入口'],
['key' => 'bbs_home_tab_swiper', 'label' => '移动端滑动切换', 'type' => 'switch', 'default' => true],
]],
],
];
}
}
@@ -1,36 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaForumNames {
public static function build(): array {
return [
'key' => 'names',
'title' => '名称定义',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'display_names', 'title' => '名词自定义', 'fields' => [
['key' => 'bbs_forum_display_name', 'label' => '论坛', 'type' => 'text', 'default' => '论坛'],
['key' => 'bbs_plate_display_name', 'label' => '版块', 'type' => 'text', 'default' => '版块'],
['key' => 'bbs_posts_display_name', 'label' => '帖子', 'type' => 'text', 'default' => '帖子'],
['key' => 'bbs_topic_display_name', 'label' => '话题', 'type' => 'text', 'default' => '话题'],
['key' => 'bbs_tag_display_name', 'label' => '标签', 'type' => 'text', 'default' => '标签'],
['key' => 'bbs_comment_display_name', 'label' => '回复', 'type' => 'text', 'default' => '回复'],
['key' => 'bbs_question_badge_name', 'label' => '提问徽章', 'type' => 'text', 'default' => '提问'],
['key' => 'bbs_question_ok_badge_name', 'label' => '已解决徽章', 'type' => 'text', 'default' => '已解决'],
['key' => 'bbs_cat_moderator_name', 'label' => '分区版主', 'type' => 'text', 'default' => '分区版主'],
['key' => 'bbs_plate_author_name', 'label' => '超级版主', 'type' => 'text', 'default' => '超级版主'],
['key' => 'bbs_plate_moderator_name', 'label' => '版主', 'type' => 'text', 'default' => '版主'],
['key' => 'bbs_plate_follow_name', 'label' => '关注', 'type' => 'text', 'default' => '关注'],
]],
],
];
}
}
@@ -1,46 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaForumOther {
public static function build(): array {
return [
'key' => 'other',
'title' => '其它设置',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'vote', 'title' => '投票', 'fields' => [
['key' => 'bbs_vote_number_type', 'label' => '投票显示', 'type' => 'select', 'default' => 'percent', 'options' => [
['label' => '百分比', 'value' => 'percent'],
['label' => '票数', 'value' => 'number'],
]],
['key' => 'bbs_vote_max', 'label' => '选项上限', 'type' => 'number', 'default' => 8, 'min' => 2, 'max' => 20],
]],
['key' => 'moderator_apply', 'title' => '版主申请', 'fields' => [
['key' => 'bbs_apply_moderator_desc', 'label' => '申请说明', 'type' => 'textarea', 'default' => '欢迎有热情的用户申请成为版主,协助管理社区秩序。'],
]],
['key' => 'misc', 'title' => '杂项', 'fields' => [
['key' => 'bbs_term_thumb', 'label' => '标签默认缩略图', 'type' => 'media', 'default' => ''],
['key' => 'bbs_post_lists_hide_pay_s', 'label' => '列表隐藏付费标记', 'type' => 'switch', 'default' => false],
['key' => 'bbs_post_title_strlen_limit', 'label' => '标题字数限制', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 100, 'help' => '最小 5,最大 100'],
['key' => 'bbs_t_placeholder', 'label' => '标题占位符', 'type' => 'text', 'default' => '请输入标题'],
['key' => 'bbs_c_placeholder', 'label' => '内容占位符', 'type' => 'text', 'default' => '请输入内容'],
]],
['key' => 'url', 'title' => 'URL / 别名', 'fields' => [
['key' => 'bbs_rewrite_suffix_html_s', 'label' => '.html 后缀', 'type' => 'switch', 'default' => true],
['key' => 'bbs_plate_rewrite_slug', 'label' => '版块别名', 'type' => 'text', 'default' => 'board'],
['key' => 'bbs_posts_rewrite_slug', 'label' => '帖子别名', 'type' => 'text', 'default' => 'topic'],
['key' => 'bbs_posts_edit_rewrite_slug', 'label' => '编辑别名', 'type' => 'text', 'default' => 'topic-edit'],
['key' => 'bbs_posts_show_in_rest_s', 'label' => '古腾堡编辑器', 'type' => 'switch', 'default' => false, 'help' => '开启后帖子可使用 Gutenberg'],
]],
],
];
}
}
@@ -1,46 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaForumPermissions {
public static function build(): array {
return [
'key' => 'permissions',
'title' => '权限配置',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'posting', 'title' => '发帖限制', 'fields' => [
['key' => 'bbs_posts_add_limit_opt_max', 'label' => '每日发帖上限', 'type' => 'number', 'default' => 10, 'min' => 0, 'help' => '0 = 不限'],
['key' => 'bbs_plate_add_limit_opt_max', 'label' => '建版数量上限', 'type' => 'number', 'default' => 3, 'min' => 0],
]],
['key' => 'display_btns', 'title' => '按钮显示', 'fields' => [
['key' => 'bbs_show_new_plate', 'label' => '始终显示建版按钮', 'type' => 'switch', 'default' => true],
['key' => 'bbs_show_new_topic', 'label' => '始终显示发帖按钮', 'type' => 'switch', 'default' => true],
['key' => 'bbs_show_apply_moderator', 'label' => '始终显示申请版主', 'type' => 'switch', 'default' => true],
]],
['key' => 'paid_content', 'title' => '帖子付费', 'fields' => [
['key' => 'bbs_post_pay_hide_type_s', 'label' => '启用付费内容', 'type' => 'switch', 'default' => true],
['key' => 'bbs_post_pay_vip_price_s', 'label' => 'VIP 特价', 'type' => 'switch', 'default' => true],
['key' => 'bbs_post_pay_vip_1_discount', 'label' => '一级 VIP 折扣 (%)', 'type' => 'number', 'default' => 80, 'min' => 0, 'max' => 100],
['key' => 'bbs_post_pay_vip_2_discount', 'label' => '二级 VIP 折扣 (%)', 'type' => 'number', 'default' => 50, 'min' => 0, 'max' => 100],
['key' => 'bbs_post_pay_price_limit', 'label' => '最高定价', 'type' => 'number', 'default' => 1000, 'min' => 1],
]],
['key' => 'plate_follow', 'title' => '版块付费关注', 'fields' => [
['key' => 'bbs_plate_follow_pay_s', 'label' => '启用付费关注', 'type' => 'switch', 'default' => false],
['key' => 'bbs_plate_follow_pay_price', 'label' => '关注价格', 'type' => 'number', 'default' => 10, 'min' => 0],
['key' => 'bbs_plate_follow_pay_type', 'label' => '支付方式', 'type' => 'select', 'default' => 'money', 'options' => [
['label' => '现金', 'value' => 'money'],
['label' => '积分', 'value' => 'points'],
]],
]],
],
];
}
}
@@ -1,46 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaForumPostPage {
public static function build(): array {
return [
'key' => 'post_page',
'title' => '帖子页面',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'breadcrumbs', 'title' => '面包屑', 'fields' => [
['key' => 'bbs_breadcrumbs_s', 'label' => '显示面包屑', 'type' => 'switch', 'default' => true],
['key' => 'bbs_breadcrumbs_home', 'label' => '显示首页', 'type' => 'switch', 'default' => true],
['key' => 'bbs_breadcrumbs_bbs_home', 'label' => '显示论坛首页', 'type' => 'switch', 'default' => true],
['key' => 'bbs_breadcrumbs_bbs_home_name', 'label' => '论坛首页名称', 'type' => 'text', 'default' => '论坛'],
['key' => 'bbs_breadcrumbs_plate_cat', 'label' => '显示分区', 'type' => 'switch', 'default' => true],
]],
['key' => 'score', 'title' => '评分设置', 'fields' => [
['key' => 'bbs_score_extra_max', 'label' => '加分上限', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 50],
['key' => 'bbs_score_deduct_max', 'label' => '扣分上限', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 50],
]],
['key' => 'display', 'title' => '显示', 'fields' => [
['key' => 'bbs_posts_maxheight_s', 'label' => '内容高度限制', 'type' => 'switch', 'default' => false],
['key' => 'bbs_posts_maxheight', 'label' => '最大高度 (px)', 'type' => 'number', 'default' => 800, 'min' => 300, 'max' => 3000, 'condition' => ['bbs_posts_maxheight_s' => true]],
]],
['key' => 'cover', 'title' => '封面设置', 'fields' => [
['key' => 'bbs_posts_cover_ratio', 'label' => '封面比例', 'type' => 'select', 'default' => '16:9', 'options' => [
['label' => '16:9', 'value' => '16:9'],
['label' => '4:3', 'value' => '4:3'],
['label' => '1:1', 'value' => '1:1'],
['label' => '3:4', 'value' => '3:4'],
]],
['key' => 'bbs_posts_cover_video_mute', 'label' => '视频封面静音', 'type' => 'switch', 'default' => true],
]],
],
];
}
}
@@ -1,39 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaForumReply {
public static function build(): array {
return [
'key' => 'reply',
'title' => '回复评论',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'paginate', 'title' => '翻页', 'fields' => [
['key' => 'bbs_reply_paginate_type', 'label' => '翻页方式', 'type' => 'select', 'default' => 'ajax', 'options' => [
['label' => 'AJAX 加载', 'value' => 'ajax'],
['label' => '传统翻页', 'value' => 'page'],
]],
['key' => 'bbs_reply_paginate_ias_s', 'label' => '无限滚动', 'type' => 'switch', 'default' => true],
['key' => 'bbs_reply_paginate_ias_max', 'label' => '自动加载页数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 10],
]],
['key' => 'toolbar', 'title' => '回复工具栏', 'fields' => [
['key' => 'bbs_comment_smilie', 'label' => '表情', 'type' => 'switch', 'default' => true],
['key' => 'bbs_comment_code', 'label' => '代码', 'type' => 'switch', 'default' => true],
['key' => 'bbs_comment_img', 'label' => '插图', 'type' => 'switch', 'default' => true],
['key' => 'bbs_comment_upload_img', 'label' => '上传图片', 'type' => 'switch', 'default' => true],
['key' => 'bbs_comment_quick_s', 'label' => '快捷回复', 'type' => 'switch', 'default' => true],
['key' => 'bbs_comment_quick_often', 'label' => '预设短语', 'type' => 'textarea', 'default' => "不错,学习了\n感谢分享\n期待更新", 'help' => '每行一条'],
['key' => 'bbs_comment_placeholder', 'label' => '回复占位符', 'type' => 'text', 'default' => '写下你的回复…'],
]],
],
];
}
}
@@ -1,29 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Admin\Schema;
defined('ABSPATH') || exit;
class SchemaForumUpload {
public static function build(): array {
return [
'key' => 'upload',
'title' => '上传权限',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'limits', 'title' => '上传限制', 'fields' => [
['key' => 'bbs_upload_img_size', 'label' => '图片大小 (KB)', 'type' => 'number', 'default' => 2048, 'min' => 100],
['key' => 'bbs_image_upload_multiple', 'label' => '批量上传数', 'type' => 'number', 'default' => 9, 'min' => 1, 'max' => 20],
['key' => 'bbs_upload_video_size', 'label' => '视频大小 (KB)', 'type' => 'number', 'default' => 20480, 'min' => 1024],
['key' => 'bbs_upload_file_size', 'label' => '文件大小 (KB)', 'type' => 'number', 'default' => 10240, 'min' => 100],
['key' => 'bbs_upload_file_mimes', 'label' => '允许格式', 'type' => 'text', 'default' => 'jpg,jpeg,png,gif,mp4,zip,rar,pdf', 'help' => '逗号分隔'],
]],
],
];
}
}
+1 -4
View File
@@ -98,9 +98,6 @@ class SchemaPageDisplay {
['label' => '用户页', 'value' => 'author'],
['label' => '文章页', 'value' => 'single'],
['label' => '搜索页', 'value' => 'search'],
['label' => '[论坛]首页', 'value' => 'forum_home'],
['label' => '[论坛]板块页面', 'value' => 'forum_plate'],
['label' => '[论坛]帖子页面', 'value' => 'forum_post'],
['label' => '其它页面', 'value' => 'page'],
]],
['key' => 'header_components_device_visibility', 'label' => '设备显示规则', 'type' => 'radio', 'default' => '', 'options' => [
@@ -255,7 +252,7 @@ class SchemaPageDisplay {
['key' => 'user_home_paging_ajax', 'label' => 'AJAX 翻页', 'type' => 'switch', 'default' => true],
]],
['key' => 'tabs', 'title' => 'Tab 配置', 'fields' => [
['key' => 'user_home_tabs', 'label' => '显示的 Tab', 'type' => 'input', 'default' => 'posts,favorites,comments,follow', 'help' => '逗号分隔;默认对齐 Zibll:posts,favorites,comments,follow(粉丝)。可选 credits,product,plate,forum'],
['key' => 'user_home_tabs', 'label' => '显示的 Tab', 'type' => 'input', 'default' => 'posts,favorites,comments,follow', 'help' => '逗号分隔;默认对齐 Zibll:posts,favorites,comments,follow(粉丝)。可选 credits'],
['key' => 'user_home_tabs_order', 'label' => 'Tab 显示顺序', 'type' => 'input', 'default' => 'posts,favorites,comments,follow'],
['key' => 'user_home_tabs_pc_hide', 'label' => 'PC 端隐藏', 'type' => 'input', 'default' => ''],
['key' => 'user_home_tabs_mobile_hide', 'label' => '移动端隐藏', 'type' => 'input', 'default' => ''],
+1 -11
View File
@@ -23,10 +23,7 @@ class SchemaUsers {
return [
'day_max' => 100, 'sign_up' => 20, 'sign_in' => 5, 'followed' => 5,
'post_new' => 5, 'post_like' => 1, 'post_favorite' => 2,
'comment_new' => 2, 'comment_like' => 1, 'bbs_posts_new' => 3,
'bbs_score_extra' => 1, 'bbs_essence' => 2, 'bbs_posts_hot' => 2,
'bbs_plate_new' => 2, 'bbs_plate_hot' => 2, 'bbs_adopt' => 2,
'bbs_comment_hot' => 2,
'comment_new' => 2, 'comment_like' => 1,
];
}
@@ -459,13 +456,6 @@ class SchemaUsers {
['key' => 'points_free_comment_new', 'label' => '发表评论', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
['key' => 'points_free_comment_like', 'label' => '评论被赞', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
['key' => 'points_free_followed', 'label' => '被关注', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100],
['key' => 'points_free_bbs_posts_new', 'label' => '论坛发帖', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 1000],
['key' => 'points_free_bbs_reply', 'label' => '论坛回复', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 1000],
['key' => 'points_free_bbs_essence', 'label' => '帖子加精', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1000],
['key' => 'points_free_bbs_adopt', 'label' => '回复被采纳', 'type' => 'number', 'default' => 20, 'min' => 0, 'max' => 1000],
['key' => 'points_free_bbs_posts_hot', 'label' => '热帖奖励', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 1000],
['key' => 'points_free_bbs_comment_hot', 'label' => '神评奖励', 'type' => 'number', 'default' => 5, 'min' => 0, 'max' => 1000],
['key' => 'points_free_bbs_score_extra', 'label' => '帖子加分', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 1000],
['key' => 'points_earn_invite', 'label' => '邀请注册奖励', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1000, 'help' => '邀请奖励不走免费日上限'],
]],
['key' => 'spend', 'title' => '积分消费', 'fields' => [
-6
View File
@@ -43,8 +43,6 @@ class SchemaProvider {
'payment',
'extensions',
'tools',
'shop',
'forum',
];
}
@@ -92,8 +90,6 @@ class SchemaProvider {
'payment' => 'aksrt_settings',
'extensions' => 'aksrt_settings',
'tools' => 'aksrt_settings',
'shop' => 'aksrt_settings',
'forum' => 'aksrt_settings',
];
return $primary[$this->normalize_domain($domain)] ?? 'aksrt_settings';
@@ -275,8 +271,6 @@ class SchemaProvider {
'payment' => Schema\SchemaPayment::class,
'extensions' => Schema\SchemaExtensions::class,
'tools' => Schema\SchemaTools::class,
'shop' => Schema\SchemaShop::class,
'forum' => Schema\SchemaForum::class,
];
}
-7
View File
@@ -155,13 +155,6 @@ class ToolsController extends ControllerBase {
}
return $this->success(['message' => sprintf(__('已优化 %d 个数据表', 'aksrt'), $optimized)]);
case 'rebuild_index':
// Rebuild search index / recount stats
if (class_exists('\AKSRT\Forum\Forum') && method_exists(\AKSRT\Forum\Forum::get_instance(), 'recount_stats')) {
\AKSRT\Forum\Forum::get_instance()->recount_stats();
}
return $this->success(['message' => __('索引重建完成', 'aksrt')]);
default:
return $this->error('未知的维护操作: ' . esc_html($action));
}
+1 -2
View File
@@ -152,11 +152,10 @@ trait PayRest {
$order = $repo->get_user_order($uid, $order_number);
} else {
$order = $repo->get($order_number);
// Guest may only poll own guest cash orders (user_id=0): content/topic resource or plate_follow.
// Guest may only poll own guest cash orders (user_id=0): content resource.
$ptype = sanitize_key((string) ($order['product_type'] ?? ''));
$guest_types = [
\AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW,
];
if ($order && ((int) ($order['user_id'] ?? -1) !== 0 || !in_array($ptype, $guest_types, true))) {
$order = null;
-6
View File
@@ -17,12 +17,6 @@ class DBMigrator {
const OPT_KEY = 'aksrt_db_schema_version';
/**
* Historical flag still readable by older code paths.
* Plate CPT is now native when registered; prefer post_type_exists().
*/
const OPT_PLATE_CPT_MIGRATED = 'aksrt_plate_cpt_migrated';
/**
* Register hooks.
*/
-3
View File
@@ -31,8 +31,6 @@ class DomainModel {
/** Cash purchase of points (zibll order_type=9). */
public const ORDER_PRODUCT_POINTS_BUY = 'points_buy';
public const ORDER_PRODUCT_SHOP = 'shop_product';
/** Paid plate follow (zibll order_type=11). product_id = plate post ID. */
public const ORDER_PRODUCT_PLATE_FOLLOW = 'plate_follow';
/**
* Human labels for aksrt_order.product_type (front-end order center).
@@ -46,7 +44,6 @@ class DomainModel {
self::ORDER_PRODUCT_VIP => __('VIP 会员', 'aksrt'),
self::ORDER_PRODUCT_RECHARGE => __('余额充值', 'aksrt'),
self::ORDER_PRODUCT_POINTS_BUY => __('购买积分', 'aksrt'),
self::ORDER_PRODUCT_PLATE_FOLLOW => __('关注版块', 'aksrt'),
];
}
+3 -78
View File
@@ -735,24 +735,6 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
}
}
// Forum pages (home / plate / board tax / tag / topic / compose / plate-cat)
$is_forum_surface = is_page_template('templates/pages/page-forums.php')
|| is_page_template('templates/forum/forum-compose.php')
|| is_tax('aksrt_forum_board')
|| is_tax('aksrt_forum_tag')
|| is_tax('aksrt_plate_cat')
|| is_singular('aksrt_forum_topic')
|| is_singular('aksrt_forum_plate');
if ($is_forum_surface) {
wp_enqueue_style(
'aksrt-forum',
$base_uri . '/assets/css/frontend/forum.css',
['aksrt-tokens'],
$version
);
}
// Product archive pages (category / tag)
if (is_tax('aksrt_product_cat') || is_tax('aksrt_product_tag')) {
wp_enqueue_style(
@@ -826,51 +808,6 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
);
}
// Forum edit page
if (is_singular('aksrt_forum_topic') && isset($_GET['edit']) && $_GET['edit'] === '1') {
$forum_edit_js = get_template_directory() . '/assets/js/forum-edit.js';
if (file_exists($forum_edit_js)) {
wp_enqueue_script(
'aksrt-forum-edit',
$base_uri . '/assets/js/forum-edit.js',
['aksrt-script'],
(string) filemtime($forum_edit_js),
true
);
}
}
if ($is_forum_surface) {
$forum_governance_js = get_template_directory() . '/assets/js/forum-governance.js';
if (file_exists($forum_governance_js)) {
wp_enqueue_script(
'aksrt-forum-governance',
$base_uri . '/assets/js/forum-governance.js',
['aksrt-script'],
(string) filemtime($forum_governance_js),
true
);
wp_localize_script('aksrt-forum-governance', 'aksrtForumGovernance', [
'ajaxUrl' => admin_url('admin-ajax.php'),
'restUrl' => esc_url_raw(rest_url()),
'restNonce' => wp_create_nonce('wp_rest'),
'publicNonce' => wp_create_nonce('aksrt_public_nonce'),
'i18n' => [
'empty' => __('内容空空如也', 'aksrt'),
'loadMore' => __('加载更多', 'aksrt'),
'loading' => __('加载中…', 'aksrt'),
],
]);
}
// §7.4: enqueue media on compose and edit pages
$is_compose = is_page_template('templates/forum/forum-compose.php');
$is_edit = is_singular('aksrt_forum_topic') && isset($_GET['edit']) && $_GET['edit'] === '1';
if ($is_compose || $is_edit) {
wp_enqueue_media();
}
}
// 商品编辑页共用 page-edit.js
if (is_page_template('templates/pages/page-product-edit.php')) {
$page_edit_js = get_template_directory() . '/assets/js/page-edit.js';
@@ -901,7 +838,7 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
}
// 单篇文章 — poster-share.js(海报分享按钮)
if (is_singular('post') || is_singular('aksrt_forum_topic')) {
if (is_singular('post')) {
$poster_js = get_template_directory() . '/assets/js/poster-share.js';
if (file_exists($poster_js)) {
wp_enqueue_script(
@@ -977,20 +914,8 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
$enqueue_module('resource-archive');
}
// 付费论坛主题:多网关收银台(与内容共用 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';
// 版块现金付费关注:多网关收银台
$is_pay_forum_plate = false;
if (is_singular('aksrt_forum_plate')) {
$plate_qid = get_queried_object_id();
$is_pay_forum_plate = (int) get_post_meta($plate_qid, '_aksrt_follow_pay', true) === 1
&& sanitize_key((string) (get_post_meta($plate_qid, '_aksrt_follow_pay_modo', true) ?: 'money')) === 'money';
}
// 单篇资源 / 付费内容 / 独立下载页 / 付费主题 / 付费关注版块:下载 + 收银台脚本
if ($is_single_resource || $is_paid_content_single || $is_download_file_page || $is_pay_forum_topic || $is_pay_forum_plate) {
// 单篇资源 / 付费内容 / 独立下载页:下载 + 收银台脚本
if ($is_single_resource || $is_paid_content_single || $is_download_file_page) {
$cms_download_js_path = get_template_directory() . '/assets/js/cms-download.js';
wp_enqueue_script(
'aksrt-cms-download',
+3 -4
View File
@@ -436,9 +436,8 @@ trait ScriptsPart1c {
}
// Comment toolbar (smilie/code/image/quick shortcodes) — post form or forum topic reply.
$load_comment_tools = (is_singular() && comments_open())
|| (is_singular('aksrt_forum_topic') || is_singular('forum_topic'));
// Comment toolbar (smilie/code/image/quick shortcodes) — post form.
$load_comment_tools = (is_singular() && comments_open());
if ($load_comment_tools) {
$comment_tools_path = get_template_directory() . '/assets/js/comment-tools.js';
wp_enqueue_script(
@@ -474,7 +473,7 @@ trait ScriptsPart1c {
}
}
if (is_singular() || is_page_template('templates/pages/page-forums.php')) {
if (is_singular()) {
$page_template_path = get_template_directory() . '/assets/js/page-template.js';
if (file_exists($page_template_path)) {
wp_enqueue_script(
-30
View File
@@ -222,36 +222,6 @@ trait ScriptsPart2 {
}
}
} elseif ($type === 'topic' && post_type_exists('aksrt_topic')) {
$posts = get_posts([
'post_type' => 'aksrt_topic',
'post_status' => 'publish',
's' => $q,
'posts_per_page' => 6,
'no_found_rows' => true,
]);
foreach ($posts as $p) {
$items[] = [
'title' => $p->post_title,
'url' => get_permalink($p),
'meta' => get_the_date('', $p),
];
}
} elseif ($type === 'product' && post_type_exists('aksrt_product')) {
$posts = get_posts([
'post_type' => 'aksrt_product',
'post_status' => 'publish',
's' => $q,
'posts_per_page' => 6,
'no_found_rows' => true,
]);
foreach ($posts as $p) {
$items[] = [
'title' => $p->post_title,
'url' => get_permalink($p),
'meta' => get_the_date('', $p),
];
}
} elseif ($type === 'tag') {
$terms = get_terms([
'taxonomy' => 'post_tag',
-23
View File
@@ -382,29 +382,6 @@ trait SetupPart1 {
'after_title' => $title_after,
]);
// ── Forum / BBS widget areas (zibll bbs_* parity lite) ──
$forum_widgets = [
'bbs_home_sidebar' => [__('论坛首页 · 侧栏', 'aksrt'), __('论坛首页侧栏区域。', 'aksrt')],
'bbs_home_top_fluid' => [__('论坛首页 · 顶部全宽', 'aksrt'), __('论坛首页顶部全宽。', 'aksrt')],
'bbs_plate_sidebar' => [__('论坛版块 · 侧栏', 'aksrt'), __('版块页侧栏。', 'aksrt')],
'bbs_plate_top_fluid' => [__('论坛版块 · 顶部全宽', 'aksrt'), __('版块页顶部全宽。', 'aksrt')],
'bbs_posts_sidebar' => [__('论坛帖子 · 侧栏', 'aksrt'), __('帖子页侧栏挂件区。', 'aksrt')],
'bbs_posts_top_fluid' => [__('论坛帖子 · 顶部全宽', 'aksrt'), __('帖子页顶部全宽。', 'aksrt')],
'bbs_new_posts_sidebar_top' => [__('论坛发帖 · 侧栏顶部', 'aksrt'), __('发帖页侧栏顶部。', 'aksrt')],
'bbs_new_posts_sidebar_bottom' => [__('论坛发帖 · 侧栏底部', 'aksrt'), __('发帖页侧栏底部。', 'aksrt')],
];
foreach ($forum_widgets as $sid => $meta) {
register_sidebar([
'id' => $sid,
'name' => $meta[0],
'description' => $meta[1],
'before_widget' => $card_wrapper_before,
'after_widget' => '</div>',
'before_title' => $title_before,
'after_title' => $title_after,
]);
}
// ── Shop widget areas (§6.9) ──
$shop_widgets = [
'shop_home_top_fluid' => ['商城首页 · 顶部通栏', '商城首页顶部全宽区域。'],
-3
View File
@@ -32,9 +32,6 @@ trait SetupPart2 {
if (!isset($menu[34])) {
$menu[34] = ['', 'read', 'separator-aksrt-shop', '', 'wp-menu-separator'];
}
if (!isset($menu[39])) {
$menu[39] = ['', 'read', 'separator-aksrt-forum', '', 'wp-menu-separator'];
}
}
/**
-23
View File
@@ -178,29 +178,6 @@ class Shortcodes {
return true;
}
// Forum topics: paid/credits unlock list or orders.
if ($post->post_type === 'aksrt_forum_topic' && class_exists('\AKSRT\Forum\Forum')) {
$forum = \AKSRT\Forum\Forum::get_instance();
$visibility = sanitize_key((string) get_post_meta($pid, '_aksrt_visibility', true));
if ($visibility === 'public' || $visibility === '') {
return true;
}
if (method_exists($forum, 'has_user_unlocked_topic') && $forum->has_user_unlocked_topic($uid, $pid)) {
return true;
}
if (method_exists($forum, 'has_user_paid_topic') && $forum->has_user_paid_topic($uid, $pid)) {
return true;
}
// Free VIP price counts as paid for cash topics.
if ($visibility === 'pay' && method_exists($forum, 'resolve_topic_pay_price')) {
$price = $forum->resolve_topic_pay_price($pid, $uid);
if (!empty($price['is_free'])) {
return true;
}
}
return false;
}
if (class_exists('\AKSRT\Content\Visibility')) {
return \AKSRT\Content\Visibility::can_view_paid_content($pid);
}
+1 -21
View File
@@ -24,7 +24,6 @@ class Templates {
public function init(): void {
add_filter('theme_page_templates', [$this, 'register_page_templates']);
add_filter('template_include', [$this, 'load_page_template']);
add_filter('template_include', [$this, 'load_taxonomy_template']);
add_filter('page_template_hierarchy', [$this, 'template_hierarchy']);
add_filter('aksrt_page_template_hierarchy', [$this, 'template_hierarchy']);
@@ -51,12 +50,7 @@ class Templates {
'templates/pages/page-ban-appeal.php' => __('AKSRT - 禁封申诉', 'aksrt'),
'templates/pages/page-orders.php' => __('AKSRT - 订单中心', 'aksrt'),
'templates/pages/page-revenue.php' => __('AKSRT - 收益中心', 'aksrt'),
'templates/pages/page-cart.php' => __('AKSRT - 购物车', 'aksrt'),
'templates/pages/page-messages.php' => __('AKSRT - 消息中心', 'aksrt'),
'templates/pages/page-product.php' => __('AKSRT - 商品详情', 'aksrt'),
'templates/pages/page-product-edit.php' => __('AKSRT - 商品编辑', 'aksrt'),
'templates/pages/page-forums.php' => __('AKSRT - 论坛', 'aksrt'),
'templates/forum/forum-compose.php' => __('AKSRT - 发帖', 'aksrt'),
];
}
@@ -104,23 +98,9 @@ class Templates {
}
/**
* Load dedicated product taxonomy templates.
* Load dedicated taxonomy templates.
*/
public function load_taxonomy_template(string $template): string {
if (is_tax('aksrt_product_cat')) {
$cat_template = AKSRT_DIR . '/templates/shop/product-cat.php';
if (file_exists($cat_template)) {
return $cat_template;
}
}
if (is_tax('aksrt_product_tag')) {
$tag_template = AKSRT_DIR . '/templates/shop/product-tag.php';
if (file_exists($tag_template)) {
return $tag_template;
}
}
return $template;
}
-53
View File
@@ -35,9 +35,6 @@ class WidgetAjax {
'/widgets/oneline-posts' => 'rest_oneline_posts',
'/widgets/recent-comments' => 'rest_recent_comments',
'/widgets/user-lists' => 'rest_user_lists',
'/widgets/forum-topics' => 'rest_forum_topics',
'/widgets/forum-terms' => 'rest_forum_terms',
'/widgets/forum-boards' => 'rest_forum_boards',
'/widgets/tag-cloud' => 'rest_tag_cloud',
'/widgets/categories' => 'rest_categories',
'/widgets/term-cards' => 'rest_term_cards',
@@ -264,42 +261,6 @@ class WidgetAjax {
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_forum_topics(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\WidgetForumHotTopics')) {
return $this->fail();
}
$style = sanitize_key((string) ($request->get_param('style') ?? 'mini'));
if (!in_array($style, ['mini', 'rank'], true)) {
$style = 'mini';
}
$instance = [
'count' => max(1, min(20, (int) ($request->get_param('count') ?? 5))),
'period' => sanitize_key((string) ($request->get_param('period') ?? 'week')),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'views')),
'style' => $style,
'plate' => max(0, (int) ($request->get_param('plate') ?? $request->get_param('plate_id') ?? 0)),
'paginate' => in_array(sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')) : 'ajax',
];
$page = max(1, (int) ($request->get_param('page') ?? 1));
return $this->payload(\AKSRT\Widgets\WidgetForumHotTopics::query_payload($instance, $page), $page);
}
public function rest_forum_terms(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\WidgetForumTopicTerms')) {
return $this->fail();
}
$instance = [
'number' => max(1, min(40, (int) ($request->get_param('number') ?? 12))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'count')),
'hide_empty' => $request->get_param('hide_empty') === null ? 1 : (int) $request->get_param('hide_empty'),
'include' => sanitize_text_field((string) ($request->get_param('include') ?? '')),
'style' => sanitize_key((string) ($request->get_param('style') ?? 'mini')),
];
$html = \AKSRT\Widgets\WidgetForumTopicTerms::render_html($instance);
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_category_block(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\CategoryBlockWidget')) {
return $this->fail();
@@ -337,20 +298,6 @@ class WidgetAjax {
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_forum_boards(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\WidgetForumBoardList')) {
return $this->fail();
}
$instance = [
'count' => max(1, min(50, (int) ($request->get_param('count') ?? 30))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'menu_order')),
'style' => sanitize_key((string) ($request->get_param('style') ?? 'list')),
'hide_empty' => $request->get_param('hide_empty') === null ? 1 : (int) $request->get_param('hide_empty'),
];
$html = \AKSRT\Widgets\WidgetForumBoardList::render_html($instance);
return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
}
public function rest_tag_cloud(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\TagCloudWidget')) {
return $this->fail();
-53
View File
@@ -32,10 +32,6 @@ class Earn {
add_action('aksrt_post_favorited', [$this, 'on_post_favorited'], 10, 3);
add_action('aksrt_comment_liked', [$this, 'on_comment_liked'], 10, 3);
add_action('aksrt_user_followed', [$this, 'on_user_followed'], 10, 2);
add_action('aksrt_forum_topic_created', [$this, 'on_forum_topic_created'], 10, 2);
add_action('comment_post', [$this, 'on_forum_reply_posted'], 20, 3);
add_action('aksrt_forum_topic_featured', [$this, 'on_forum_topic_featured'], 10, 2);
}
public function on_user_register(int $user_id): void {
@@ -124,11 +120,6 @@ class Earn {
if (get_comment_meta($comment->comment_ID, 'aksrt_credit_rewarded', true)) {
return;
}
// Skip forum topic replies (handled separately).
$post = get_post((int) $comment->comment_post_ID);
if ($post && $post->post_type === 'aksrt_forum_topic') {
return;
}
$cfg = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_credit_settings() : [];
$reward = is_array($cfg['comment_reward'] ?? null) ? $cfg['comment_reward'] : [];
$min_length = (int) ($reward['min_length'] ?? 0);
@@ -200,48 +191,4 @@ class Earn {
}
PointsFree::grant($following_id, 'followed', 0, ['type' => 'user', 'id' => $follower_id]);
}
public function on_forum_topic_created(int $user_id, int $post_id): void {
if ($user_id <= 0 || !class_exists('AKSRT\\Credits\\PointsFree')) {
return;
}
PointsFree::grant($user_id, 'bbs_posts_new', 0, ['type' => 'post', 'id' => $post_id]);
}
public function on_forum_reply_posted(int $comment_id, $approved, array $commentdata = []): void {
unset($commentdata);
if ((int) $approved !== 1 && $approved !== '1' && $approved !== true) {
// Still load comment and check.
}
$comment = get_comment($comment_id);
if (!$comment || (int) $comment->comment_approved !== 1) {
return;
}
$post = get_post((int) $comment->comment_post_ID);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return;
}
$user_id = (int) $comment->user_id;
if ($user_id <= 0 || !class_exists('AKSRT\\Credits\\PointsFree')) {
return;
}
if (get_comment_meta($comment_id, 'aksrt_credit_forum_reply', true)) {
return;
}
$granted = PointsFree::grant($user_id, 'bbs_reply', 0, ['type' => 'comment', 'id' => $comment_id]);
if (!is_wp_error($granted)) {
update_comment_meta($comment_id, 'aksrt_credit_forum_reply', 1);
}
}
/**
* Essence reward is owned by \AKSRT\Forum\Forum::reward_essence() (free-day-cap).
* Keep hook for external integrations only — do not double-grant.
*
* @param int $topic_id
* @param int $author_or_operator Author id from forum (2nd arg) or operator.
*/
public function on_forum_topic_featured(int $topic_id, int $author_or_operator = 0): void {
unset($topic_id, $author_or_operator);
}
}
+2 -51
View File
@@ -30,13 +30,6 @@ class PointsFree {
'post_favorite' => [__('文章被收藏', 'aksrt'), __('你的文章被收藏', 'aksrt'), 1],
'comment_new' => [__('发表评论', 'aksrt'), __('发表并通过审核的评论', 'aksrt'), 1],
'comment_like' => [__('评论被赞', 'aksrt'), __('你的评论被点赞', 'aksrt'), 1],
'bbs_posts_new' => [__('论坛发帖', 'aksrt'), __('发布论坛主题', 'aksrt'), 5],
'bbs_reply' => [__('论坛回复', 'aksrt'), __('回复论坛主题', 'aksrt'), 2],
'bbs_essence' => [__('帖子加精', 'aksrt'), __('主题被设为精华', 'aksrt'), 10],
'bbs_adopt' => [__('回复被采纳', 'aksrt'), __('问答帖回复被采纳', 'aksrt'), 20],
'bbs_posts_hot' => [__('热帖奖励', 'aksrt'), __('主题成为热帖', 'aksrt'), 5],
'bbs_comment_hot'=> [__('神评奖励', 'aksrt'), __('回复成为神评', 'aksrt'), 5],
'bbs_score_extra'=> [__('帖子加分', 'aksrt'), __('主题被加分时奖励', 'aksrt'), 3],
];
}
@@ -194,8 +187,8 @@ class PointsFree {
$task_dates = is_array($raw) ? $raw : [];
}
$passive = ['post_like', 'post_favorite', 'followed', 'comment_like', 'bbs_essence', 'bbs_adopt', 'bbs_posts_hot', 'bbs_comment_hot', 'bbs_score_extra'];
$active_once = ['post_new', 'comment_new', 'bbs_posts_new', 'bbs_reply'];
$passive = ['post_like', 'post_favorite', 'followed', 'comment_like'];
$active_once = ['post_new', 'comment_new'];
foreach (self::catalog() as $key => $meta) {
$pts = (int) ($cfg['tasks'][$key] ?? 0);
@@ -333,48 +326,6 @@ class PointsFree {
return 0;
}
return max(0, (int) ($cr['amount'] ?? $default));
case 'bbs_posts_new':
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) {
$f = \AKSRT\Settings\Settings::get_forum_settings();
return max(0, (int) ($f['credits']['post_new'] ?? $f['credits']['topic_new'] ?? $all['bbs_credits_post_new'] ?? $default));
}
return max(0, (int) ($all['bbs_credits_post_new'] ?? $default));
case 'bbs_reply':
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) {
$f = \AKSRT\Settings\Settings::get_forum_settings();
return max(0, (int) ($f['credits']['comment_new'] ?? $default));
}
return $default;
case 'bbs_essence':
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) {
$f = \AKSRT\Settings\Settings::get_forum_settings();
return max(0, (int) ($f['credits']['essence'] ?? $default));
}
return $default;
case 'bbs_adopt':
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) {
$f = \AKSRT\Settings\Settings::get_forum_settings();
return max(0, (int) ($f['credits']['adopted'] ?? $default));
}
return $default;
case 'bbs_posts_hot':
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) {
$f = \AKSRT\Settings\Settings::get_forum_settings();
return max(0, (int) ($f['credits']['posts_hot'] ?? $default));
}
return $default;
case 'bbs_comment_hot':
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) {
$f = \AKSRT\Settings\Settings::get_forum_settings();
return max(0, (int) ($f['credits']['comment_hot'] ?? $f['credits']['comment_like'] ?? $default));
}
return $default;
case 'bbs_score_extra':
if (class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) {
$f = \AKSRT\Settings\Settings::get_forum_settings();
return max(0, (int) ($f['credits']['score_extra'] ?? $default));
}
return $default;
default:
return $default;
}
-396
View File
@@ -1,396 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class BoardService {
use BoardServicePart1;
use BoardServicePart2;
use BoardServicePart3;
/** @var Forum */
private $forum;
public function __construct(Forum $forum) {
$this->forum = $forum;
}
/**
* Get board list (plate CPT).
*
* 读取 aksrt_forum_plate CPT,支持层级查询。
* Fallback:若 plate CPT 不存在或迁移未完成,降级读取 taxonomy。
*/
public function get_boards(array $args = []): array {
// 优先使用 plate CPT
if ($this->forum->is_plate_cpt_active()) {
return $this->get_boards_from_cpt($args);
}
// Fallback: 读取旧 taxonomy(向后兼容)
return $this->get_boards_from_taxonomy();
}
/**
* 从 plate CPT 读取板块列表(新实现)。
*/
private function get_boards_from_cpt(array $args = []): array {
$defaults = [
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'menu_order',
'order' => 'ASC',
'hierarchical' => true,
];
$query_args = wp_parse_args($args, $defaults);
$plates = get_posts($query_args);
if (empty($plates)) {
return [];
}
$boards = [];
foreach ($plates as $plate) {
$plate_id = (int) $plate->ID;
$moderators = $this->forum->get_plate_moderators($plate_id);
$applications = $this->forum->get_plate_moderator_applications($plate_id);
$boards[] = [
'id' => $plate_id,
'name' => (string) $plate->post_title,
'description' => (string) $plate->post_excerpt,
'content' => (string) $plate->post_content,
'icon' => (string) get_post_meta($plate_id, '_aksrt_icon', true),
'sort_order' => (int) $plate->menu_order,
'post_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true),
'reply_count' => (int) get_post_meta($plate_id, '_aksrt_reply_count', true),
'today_reply_count' => (int) get_post_meta($plate_id, '_aksrt_today_reply_count', true),
'views' => (int) get_post_meta($plate_id, '_aksrt_views', true),
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
'last_reply' => (string) get_post_meta($plate_id, '_aksrt_last_reply', true),
'last_post' => (string) get_post_meta($plate_id, '_aksrt_last_post', true),
'moderator_count' => count($moderators),
'pending_application_count' => count(array_filter($applications, static function ($item): bool {
return (string) ($item['status'] ?? '') === Forum::APPLICATION_PENDING;
})),
'url' => (string) get_permalink($plate),
'author_id' => (int) $plate->post_author,
'parent_id' => (int) $plate->post_parent,
'created_at' => (string) $plate->post_date,
'thumbnail_url' => (string) get_the_post_thumbnail_url($plate, 'thumbnail'),
];
}
return $this->build_hierarchical_boards($boards);
}
/**
* 从 taxonomy 读取板块(向后兼容)。
*/
private function get_boards_from_taxonomy(): array {
$terms = get_terms([
'taxonomy' => 'aksrt_forum_board',
'hide_empty' => false,
'orderby' => 'meta_value_num',
'meta_key' => '_aksrt_sort_order',
'order' => 'ASC',
]);
if (is_wp_error($terms) || empty($terms)) {
return [];
}
$boards = [];
foreach ($terms as $term) {
$board_id = (int) $term->term_id;
$moderators = $this->forum->get_board_moderators($board_id);
$applications = $this->forum->get_board_moderator_applications($board_id);
$board_link = get_term_link($term);
$boards[] = [
'id' => $board_id,
'name' => (string) $term->name,
'description' => (string) $term->description,
'content' => '',
'icon' => (string) get_term_meta($board_id, '_aksrt_icon', true),
'sort_order' => (int) get_term_meta($board_id, '_aksrt_sort_order', true),
'post_count' => (int) $term->count,
'reply_count' => 0,
'today_reply_count' => 0,
'views' => 0,
'follow_count' => 0,
'last_reply' => '',
'last_post' => '',
'moderator_count' => count($moderators),
'pending_application_count' => count(array_filter($applications, static function ($item): bool {
return (string) ($item['status'] ?? '') === Forum::APPLICATION_PENDING;
})),
'url' => !is_wp_error($board_link) ? (string) $board_link : '',
'author_id' => 0,
'parent_id' => (int) $term->parent,
'created_at' => '',
'thumbnail_url' => '',
];
}
return $boards;
}
/**
* 构建层级板块树(子板块嵌套)。
*/
private function build_hierarchical_boards(array $boards): array {
$by_id = [];
foreach ($boards as $board) {
$by_id[$board['id']] = $board;
$by_id[$board['id']]['children'] = [];
}
$roots = [];
foreach ($boards as $board) {
if ($board['parent_id'] > 0 && isset($by_id[$board['parent_id']])) {
$by_id[$board['parent_id']]['children'][] = &$by_id[$board['id']];
} else {
$roots[] = &$by_id[$board['id']];
}
}
return $roots;
}
/**
* Create board (统一入口,自动选择 CPT/taxonomy).
*/
public function create_board(array $data): int {
if ($this->forum->is_plate_cpt_active()) {
return $this->create_plate($data);
}
// Fallback: taxonomy
$name = sanitize_text_field((string) ($data['name'] ?? ''));
if ($name === '') {
return 0;
}
$result = wp_insert_term($name, 'aksrt_forum_board', [
'description' => sanitize_textarea_field((string) ($data['description'] ?? '')),
]);
if (is_wp_error($result)) {
return 0;
}
$term_id = (int) $result['term_id'];
$icon = sanitize_text_field((string) ($data['icon'] ?? ''));
$sort_order = (int) ($data['sort_order'] ?? $term_id);
if ($icon !== '') {
update_term_meta($term_id, '_aksrt_icon', $icon);
}
update_term_meta($term_id, '_aksrt_sort_order', $sort_order);
return $term_id;
}
/**
* Update board (统一入口,自动选择 CPT/taxonomy).
*/
public function update_board(int $board_id, array $data): bool {
if ($this->forum->is_plate_cpt_active()) {
return $this->update_plate($board_id, $data);
}
// Fallback: taxonomy
$term = get_term($board_id, 'aksrt_forum_board');
if (!$term || is_wp_error($term)) {
return false;
}
$update = ['taxonomy' => 'aksrt_forum_board'];
if (isset($data['name'])) {
$name = sanitize_text_field((string) $data['name']);
if ($name !== '') {
$update['name'] = $name;
}
}
if (isset($data['description'])) {
$update['description'] = sanitize_textarea_field((string) $data['description']);
}
$result = wp_update_term($board_id, 'aksrt_forum_board', $update);
if (is_wp_error($result)) {
return false;
}
if (isset($data['icon'])) {
update_term_meta($board_id, '_aksrt_icon', sanitize_text_field((string) $data['icon']));
}
if (isset($data['sort_order'])) {
update_term_meta($board_id, '_aksrt_sort_order', (int) $data['sort_order']);
}
return true;
}
/**
* Delete board (统一入口,自动选择 CPT/taxonomy).
*/
public function delete_board(int $board_id): bool {
if ($this->forum->is_plate_cpt_active()) {
return $this->delete_plate($board_id);
}
// Fallback: taxonomy
$term = get_term($board_id, 'aksrt_forum_board');
if (!$term || is_wp_error($term)) {
return false;
}
$result = wp_delete_term($board_id, 'aksrt_forum_board');
return !is_wp_error($result) && !empty($result);
}
/**
* Reorder boards (统一入口).
*/
public function reorder_boards(array $order_map): bool {
if ($this->forum->is_plate_cpt_active()) {
return $this->reorder_plates($order_map);
}
foreach ($order_map as $term_id => $sort_order) {
$term_id = (int) $term_id;
if ($term_id <= 0) {
continue;
}
update_term_meta($term_id, '_aksrt_sort_order', (int) $sort_order);
}
return true;
}
// ================================================================
// §7.0 Plate CPT methods —板块 CPT 化 CRUD
// ================================================================
/**
* Get all plates (板块 CPT posts).
*
* @return array<int, array<string, mixed>>
*/
public function get_plates(array $args = []): array {
$query_args = [
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => (int) ($args['limit'] ?? 100),
'orderby' => 'menu_order',
'order' => 'ASC',
];
if (!empty($args['parent'])) {
$query_args['post_parent'] = (int) $args['parent'];
}
if (!empty($args['plate_cat'])) {
$query_args['tax_query'] = [[
'taxonomy' => 'aksrt_plate_cat',
'field' => 'term_id',
'terms' => (int) $args['plate_cat'],
]];
}
$posts = get_posts($query_args);
$plates = [];
foreach ($posts as $post) {
$plates[] = $this->build_plate_payload($post);
}
return $plates;
}
/**
* Get single plate by ID.
*/
public function get_plate(int $plate_id): ?array {
$post = get_post($plate_id);
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
return null;
}
return $this->build_plate_payload($post);
}
/**
* Build plate data payload from \WP_Post.
*/
public function build_plate_payload(\WP_Post $post): array {
$plate_id = (int) $post->ID;
$moderators = get_post_meta($plate_id, '_aksrt_moderators', true);
if (!is_array($moderators)) {
$moderators = [];
}
$cover_url = (string) get_the_post_thumbnail_url($plate_id, 'medium');
if ($cover_url === '') {
$config = $this->forum->get_config();
$default_thumbs = $config['other']['bbs_term_thumb'] ?? [];
if (!empty($default_thumbs) && is_array($default_thumbs)) {
$cover_url = $default_thumbs[array_rand($default_thumbs)];
}
}
$plate_cats = wp_get_object_terms($plate_id, 'aksrt_plate_cat', ['fields' => 'all']);
$plate_cat = (!is_wp_error($plate_cats) && !empty($plate_cats)) ? [
'id' => (int) $plate_cats[0]->term_id,
'name' => (string) $plate_cats[0]->name,
'slug' => (string) $plate_cats[0]->slug,
] : null;
return [
'id' => $plate_id,
'name' => (string) $post->post_title,
'description' => (string) $post->post_excerpt,
'content' => (string) $post->post_content,
'icon' => (string) get_post_meta($plate_id, '_aksrt_icon', true),
'cover_url' => $cover_url,
'sort_order' => (int) $post->menu_order,
'parent_id' => (int) $post->post_parent,
'author_id' => (int) $post->post_author,
'post_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true),
'reply_count' => (int) get_post_meta($plate_id, '_aksrt_reply_count', true),
'today_reply_count' => (int) get_post_meta($plate_id, '_aksrt_today_reply_count', true),
'views' => (int) get_post_meta($plate_id, '_aksrt_views', true),
'follow_count' => (int) get_post_meta($plate_id, '_aksrt_follow_count', true),
'last_reply' => (string) get_post_meta($plate_id, '_aksrt_last_reply', true),
'last_post' => (string) get_post_meta($plate_id, '_aksrt_last_post', true),
'moderator_count' => count($moderators),
'moderators' => $moderators,
'url' => (string) get_permalink($plate_id),
'created_at' => (string) $post->post_date,
'allow_view' => (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public'),
'add_limit' => (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all'),
'is_hot' => $this->forum->is_hot_plate($plate_id),
'plate_cat' => $plate_cat,
// Alias fields for backward compat with get_board_view consumer
'topic_count' => (int) get_post_meta($plate_id, '_aksrt_posts_count', true),
'thumbnail_url' => $cover_url,
];
}
/**
* Create plate (CPT post).
*/
}
-425
View File
@@ -1,425 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
trait BoardServicePart1 {
public function create_plate(array $data): int {
$title = sanitize_text_field((string) ($data['name'] ?? ''));
if ($title === '') {
return 0;
}
$plate_id = wp_insert_post([
'post_type' => 'aksrt_forum_plate',
'post_title' => $title,
'post_content' => wp_kses_post((string) ($data['content'] ?? '')),
'post_excerpt' => sanitize_textarea_field((string) ($data['description'] ?? '')),
'post_status' => 'publish',
'post_parent' => (int) ($data['parent_id'] ?? 0),
'post_author' => (int) ($data['author_id'] ?? get_current_user_id()),
'menu_order' => (int) ($data['sort_order'] ?? 0),
], true);
if (is_wp_error($plate_id)) {
return 0;
}
$icon = sanitize_text_field((string) ($data['icon'] ?? ''));
if ($icon !== '') {
update_post_meta($plate_id, '_aksrt_icon', $icon);
}
update_post_meta($plate_id, '_aksrt_sort_order', (int) ($data['sort_order'] ?? 0));
update_post_meta($plate_id, '_aksrt_posts_count', 0);
update_post_meta($plate_id, '_aksrt_reply_count', 0);
update_post_meta($plate_id, '_aksrt_follow_count', 0);
update_post_meta($plate_id, '_aksrt_plate_views', 0);
if (!empty($data['plate_cat'])) {
wp_set_object_terms($plate_id, [(int) $data['plate_cat']], 'aksrt_plate_cat');
}
// Also create a corresponding taxonomy term for backward compat
$term_result = wp_insert_term($title, 'aksrt_forum_board', [
'description' => sanitize_textarea_field((string) ($data['description'] ?? '')),
]);
if (!is_wp_error($term_result)) {
$term_id = (int) $term_result['term_id'];
update_term_meta($term_id, '_aksrt_sort_order', (int) ($data['sort_order'] ?? 0));
if ($icon !== '') {
update_term_meta($term_id, '_aksrt_icon', $icon);
}
update_post_meta($plate_id, '_aksrt_migrated_from_term', $term_id);
}
return $plate_id;
}
/**
* Update plate.
*/
public function update_plate(int $plate_id, array $data): bool {
$post = get_post($plate_id);
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
return false;
}
$update = ['ID' => $plate_id];
if (isset($data['name'])) {
$name = sanitize_text_field((string) $data['name']);
if ($name !== '') {
$update['post_title'] = $name;
}
}
if (isset($data['description'])) {
$update['post_excerpt'] = sanitize_textarea_field((string) $data['description']);
}
if (isset($data['content'])) {
$update['post_content'] = wp_kses_post((string) $data['content']);
}
if (isset($data['parent_id'])) {
$update['post_parent'] = (int) $data['parent_id'];
}
if (isset($data['sort_order'])) {
$update['menu_order'] = (int) $data['sort_order'];
update_post_meta($plate_id, '_aksrt_sort_order', (int) $data['sort_order']);
}
if (isset($data['author_id'])) {
$update['post_author'] = (int) $data['author_id'];
}
$result = wp_update_post($update, true);
if (is_wp_error($result)) {
return false;
}
if (isset($data['icon'])) {
update_post_meta($plate_id, '_aksrt_icon', sanitize_text_field((string) $data['icon']));
}
if (isset($data['allow_view'])) {
$allowed_views = ['public', 'login', 'role', 'follow'];
$view = in_array($data['allow_view'], $allowed_views, true) ? $data['allow_view'] : 'public';
update_post_meta($plate_id, '_aksrt_allow_view', $view);
}
// Sync to legacy taxonomy term if exists
$term_id = (int) get_post_meta($plate_id, '_aksrt_migrated_from_term', true);
if ($term_id > 0) {
$term_update = [];
if (isset($update['post_title'])) {
$term_update['name'] = $update['post_title'];
}
if (isset($update['post_excerpt'])) {
$term_update['description'] = $update['post_excerpt'];
}
if (!empty($term_update)) {
wp_update_term($term_id, 'aksrt_forum_board', $term_update);
}
if (isset($data['icon'])) {
update_term_meta($term_id, '_aksrt_icon', sanitize_text_field((string) $data['icon']));
}
if (isset($data['sort_order'])) {
update_term_meta($term_id, '_aksrt_sort_order', (int) $data['sort_order']);
}
}
return true;
}
/**
* Delete plate (with cascade option).
*/
public function delete_plate(int $plate_id, string $cascade = 'trash', int $move_to = 0): bool {
$post = get_post($plate_id);
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
return false;
}
$topic_ids = get_posts([
'post_type' => 'aksrt_forum_topic',
'meta_key' => '_aksrt_plate_id',
'meta_value' => $plate_id,
'posts_per_page' => -1,
'fields' => 'ids',
'post_status' => 'any',
]);
if (!empty($topic_ids)) {
if ($cascade === 'move' && $move_to > 0 && $move_to !== $plate_id) {
$target = get_post($move_to);
if ($target && $target->post_type === 'aksrt_forum_plate') {
foreach ($topic_ids as $tid) {
update_post_meta((int) $tid, '_aksrt_plate_id', $move_to);
}
$this->forum->sync_plate_counts($move_to);
}
} else {
foreach ($topic_ids as $tid) {
wp_trash_post((int) $tid);
}
}
}
// Handle child plates (move to top-level)
$children = get_posts([
'post_type' => 'aksrt_forum_plate',
'post_parent' => $plate_id,
'posts_per_page' => -1,
'fields' => 'ids',
'post_status' => 'any',
]);
foreach ($children as $child_id) {
wp_update_post(['ID' => (int) $child_id, 'post_parent' => 0]);
}
// Remove legacy taxonomy term if linked
$term_id = (int) get_post_meta($plate_id, '_aksrt_migrated_from_term', true);
if ($term_id > 0) {
wp_delete_term($term_id, 'aksrt_forum_board');
}
wp_delete_post($plate_id, true);
return true;
}
/**
* Reorder plates.
*/
public function reorder_plates(array $order_map): bool {
foreach ($order_map as $plate_id => $sort_order) {
$plate_id = (int) $plate_id;
if ($plate_id <= 0) continue;
wp_update_post(['ID' => $plate_id, 'menu_order' => (int) $sort_order]);
update_post_meta($plate_id, '_aksrt_sort_order', (int) $sort_order);
}
return true;
}
/**
* Resolve plate_id for a topic (reads _aksrt_plate_id meta, falls back to taxonomy).
*/
public function get_topic_plate_id(int $topic_id): int {
$plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true);
if ($plate_id > 0) {
return $plate_id;
}
// Fallback: find plate via legacy taxonomy term
$terms = get_the_terms($topic_id, 'aksrt_forum_board');
if (!$terms || is_wp_error($terms)) {
return 0;
}
$term = $terms[0];
// Look up plate post by migrated_from_term meta
$plates = get_posts([
'post_type' => 'aksrt_forum_plate',
'meta_key' => '_aksrt_migrated_from_term',
'meta_value' => (string) $term->term_id,
'numberposts' => 1,
'fields' => 'ids',
'post_status' => 'publish',
]);
if (!empty($plates)) {
$resolved = (int) $plates[0];
update_post_meta($topic_id, '_aksrt_plate_id', $resolved);
return $resolved;
}
return 0;
}
/**
* Check if a user is a plate moderator (CPT-based).
*/
public function is_plate_moderator(int $user_id, int $plate_id): bool {
if ($user_id <= 0 || $plate_id <= 0) {
return false;
}
if ($this->forum->is_forum_admin($user_id)) {
return true;
}
$post = get_post($plate_id);
if ($post && (int) $post->post_author === $user_id) {
return true;
}
$moderators = get_post_meta($plate_id, '_aksrt_moderators', true);
if (is_array($moderators) && in_array($user_id, $moderators, true)) {
return true;
}
return false;
}
/**
* Get plate view (topics + moderators + pagination) for frontend.
*/
public function get_plate_view(int $plate_id, array $args = []): array {
$plate = $this->get_plate($plate_id);
if (!$plate) {
return [
'plate' => null,
'topics' => [],
'sticky_topics' => [],
'moderators' => [],
'pagination' => ['page' => 1, 'max_pages' => 1, 'total' => 0],
'permissions' => [],
'children' => [],
];
}
$user_id = (int) ($args['user_id'] ?? get_current_user_id());
$paged = max(1, (int) ($args['paged'] ?? 1));
$topics = $this->forum->get_topic_list([
'plate_id' => $plate_id,
'page' => $paged,
'limit' => 20,
]);
// Global + super + plate sticky
$sticky_topics = $this->get_sticky_topics_for_plate($plate_id);
$children = get_posts([
'post_type' => 'aksrt_forum_plate',
'post_parent' => $plate_id,
'post_status' => 'publish',
'posts_per_page' => 50,
'orderby' => 'menu_order',
'order' => 'ASC',
]);
$child_plates = [];
foreach ($children as $child) {
$child_plates[] = $this->build_plate_payload($child);
}
return [
'plate' => $plate,
'topics' => $topics['items'],
'sticky_topics' => $sticky_topics,
'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),
],
'children' => $child_plates,
];
}
/**
* Get sticky topics for a plate (global + super + plate-level).
*/
private function get_sticky_topics_for_plate(int $plate_id): array {
$global_sticky = get_posts([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'meta_key' => '_aksrt_sticky',
'meta_value' => Forum::STICKY_GLOBAL,
'posts_per_page' => 5,
'orderby' => 'date',
'order' => 'DESC',
]);
$plate_sticky = get_posts([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'posts_per_page' => 10,
'orderby' => 'date',
'order' => 'DESC',
'meta_query' => [
'relation' => 'AND',
['key' => '_aksrt_plate_id', 'value' => $plate_id, 'type' => 'NUMERIC'],
['key' => '_aksrt_sticky', 'value' => 0, 'compare' => '>', 'type' => 'NUMERIC'],
['key' => '_aksrt_sticky', 'value' => Forum::STICKY_GLOBAL, 'compare' => '<', 'type' => 'NUMERIC'],
],
]);
$items = [];
$seen = [];
foreach (array_merge($global_sticky, $plate_sticky) as $post) {
if (isset($seen[$post->ID])) continue;
$seen[$post->ID] = true;
$items[] = $this->forum->build_topic_payload($post);
}
return array_slice($items, 0, 10);
}
/**
* Get moderator user profiles for a plate.
*/
private function get_plate_moderator_profiles(int $plate_id): array {
$post = get_post($plate_id);
$profiles = [];
// Author = super moderator
if ($post && $post->post_author > 0) {
$user = get_userdata((int) $post->post_author);
if ($user) {
$profiles[] = [
'id' => (int) $user->ID,
'name' => $user->display_name,
'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user->ID, 40) : get_avatar_url($user->ID, ['size' => 40]),
'role' => 'plate_author',
];
}
}
$moderators = get_post_meta($plate_id, '_aksrt_moderators', true);
if (!is_array($moderators)) {
return $profiles;
}
$roles = get_post_meta($plate_id, '_aksrt_moderator_roles', true);
if (!is_array($roles)) {
$roles = [];
}
foreach ($moderators as $uid) {
$uid = (int) $uid;
if ($post && $uid === (int) $post->post_author) continue;
$user = get_userdata($uid);
if (!$user) continue;
$profiles[] = [
'id' => $uid,
'name' => $user->display_name,
'avatar' => function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($uid, 40) : get_avatar_url($uid, ['size' => 40]),
'role' => (string) ($roles[$uid] ?? 'moderator'),
];
}
return $profiles;
}
}
-796
View File
@@ -1,796 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
trait BoardServicePart2 {
/**
* 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\Settings')
? (\AKSRT\Settings\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) {
// Guest cash follow unlock via signed cookie (money follow pay only).
if ($allow_view === 'follow'
&& function_exists('aksrt_guest_has_plate_follow_access')
&& aksrt_guest_has_plate_follow_access($plate_id)
) {
return true;
}
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 ($plate_id <= 0) {
return false;
}
if ($user_id <= 0) {
return function_exists('aksrt_guest_has_plate_follow_access')
&& aksrt_guest_has_plate_follow_access($plate_id);
}
$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\Payment\OrderRepository') && class_exists('\AKSRT\Core\DomainModel')) {
if (\AKSRT\Payment\OrderRepository::get_instance()->has_paid_access(
$user_id,
\AKSRT\Core\DomainModel::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\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_forum_settings')) {
$cfg = \AKSRT\Settings\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,vip_level:int,vip_set:bool}
*/
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_level = 0;
if ($user_id > 0) {
if (function_exists('aksrt_get_user_vip_level')) {
$vip_level = (int) aksrt_get_user_vip_level($user_id);
} elseif (function_exists('aksrt_is_vip2') && aksrt_is_vip2($user_id)) {
$vip_level = 2;
} elseif (function_exists('aksrt_is_vip') && aksrt_is_vip($user_id)) {
$vip_level = 1;
}
if ($vip_level <= 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($user_id)) {
$vip_level = 1;
}
}
$pick = static function ($raw) use ($is_points) {
if ($raw === '' || $raw === null || $raw === false) {
return null;
}
if (!is_numeric($raw)) {
return null;
}
return $is_points ? max(0, (int) $raw) : max(0.0, (float) $raw);
};
$vip2 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip2_price', true));
$vip1 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip1_price', true));
if ($vip1 === null) {
// Legacy single VIP key → vip1.
$vip1 = $pick(get_post_meta($plate_id, '_aksrt_follow_pay_vip_price', true));
}
$amount = $base;
$is_vip_price = false;
$vip_set = ($vip1 !== null || $vip2 !== null);
if ($vip_level >= 2 && $vip2 !== null) {
$amount = $vip2;
$is_vip_price = true;
} elseif ($vip_level >= 1 && $vip1 !== null) {
$amount = $vip1;
$is_vip_price = true;
} elseif ($vip_level >= 2 && $vip1 !== null) {
// VIP2 user falls back to VIP1 when VIP2 unset.
$amount = $vip1;
$is_vip_price = true;
}
if ($is_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',
'vip_level' => $vip_level,
'vip_set' => true,
];
}
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',
'vip_level' => $vip_level,
'vip_set' => $vip_set,
];
}
/**
* 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\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\Credits::get_balance($user_id);
if ($balance < $cost) {
return $this->forum->error(sprintf(__('积分不足,需要 %d 积分', 'aksrt'), $cost));
}
$deducted = \AKSRT\Credits\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: multi-gateway cashier only (PlateFollowPay REST). No Credits×100 bypass.
return $this->forum->error(__('请通过收银台支付关注费用。', 'aksrt'), [
'require_cashier' => true,
'pay' => $price,
'modo' => 'money',
'plate_id' => $plate_id,
]);
}
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\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\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 = %s",
'_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);
$is_hot = ($posts_count + $follow_count * 2) >= $threshold;
if ($is_hot && get_post_meta($plate_id, '_aksrt_level_hot_awarded', true) !== '1') {
$plate = get_post($plate_id);
$author_id = $plate instanceof \WP_Post ? (int) $plate->post_author : 0;
update_post_meta($plate_id, '_aksrt_level_hot_awarded', '1');
do_action('aksrt_forum_plate_hot', $plate_id, $author_id);
}
return $is_hot;
}
/**
* 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;
}
}
-187
View File
@@ -1,187 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
trait BoardServicePart3 {
/**
* Check if a user is a plate_cat moderator (分区版主).
*/
public function is_cat_moderator(int $user_id, int $cat_id): bool {
if ($user_id <= 0) {
return false;
}
if ($this->forum->is_forum_admin($user_id)) {
return true;
}
$moderator_ids = get_term_meta($cat_id, '_aksrt_cat_moderator', true);
if (!is_array($moderator_ids)) {
return false;
}
return in_array($user_id, array_map('intval', $moderator_ids), true);
}
/**
* Get plates sorted by a specified dimension (§7.2 多维排序).
*/
public function get_sorted_plates(string $orderby = 'menu_order', string $order = 'ASC', int $limit = 50): array {
$allowed = [
'name' => 'title',
'date' => 'date',
'modified' => 'modified',
'menu_order' => 'menu_order',
'last_post' => '_aksrt_last_post',
'last_reply' => '_aksrt_last_reply',
'posts_count' => '_aksrt_posts_count',
'reply_count' => '_aksrt_reply_count',
'today_reply_count' => '_aksrt_today_reply_count',
'views' => '_aksrt_views',
'follow_count' => '_aksrt_follow_count',
'rand' => 'rand',
];
$query_args = [
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => $limit,
'order' => strtoupper($order) === 'ASC' ? 'ASC' : 'DESC',
];
if (isset($allowed[$orderby])) {
$meta_key = $allowed[$orderby];
if (str_starts_with($meta_key, '_aksrt_')) {
$query_args['orderby'] = 'meta_value_num';
$query_args['meta_key'] = $meta_key;
} else {
$query_args['orderby'] = $meta_key;
}
} else {
$query_args['orderby'] = 'menu_order';
}
$posts = get_posts($query_args);
$plates = [];
foreach ($posts as $post) {
$plates[] = $this->build_plate_payload($post);
}
return $plates;
}
public function maybe_migrate_boards(): void {
if (get_option('aksrt_forum_boards_migrated')) {
return;
}
$old_boards = get_option('aksrt_forum_boards', []);
if (!is_array($old_boards) || empty($old_boards)) {
update_option('aksrt_forum_boards_migrated', time());
return;
}
foreach ($old_boards as $board) {
$name = sanitize_text_field((string) ($board['name'] ?? ''));
if ($name === '') {
continue;
}
$existing = get_terms([
'taxonomy' => 'aksrt_forum_board',
'name' => $name,
'hide_empty' => false,
'fields' => 'ids',
]);
if (!is_wp_error($existing) && !empty($existing)) {
$term_id = (int) $existing[0];
} else {
$result = wp_insert_term($name, 'aksrt_forum_board', [
'description' => sanitize_textarea_field((string) ($board['description'] ?? '')),
]);
if (is_wp_error($result)) {
continue;
}
$term_id = (int) $result['term_id'];
}
$icon = sanitize_text_field((string) ($board['icon'] ?? ''));
if ($icon !== '') {
update_term_meta($term_id, '_aksrt_icon', $icon);
}
update_term_meta($term_id, '_aksrt_sort_order', (int) ($board['sort_order'] ?? $term_id));
if (!empty($board['moderators']) && is_array($board['moderators'])) {
update_term_meta($term_id, '_aksrt_moderators', array_map('intval', $board['moderators']));
}
}
update_option('aksrt_forum_boards_migrated', time());
}
public function ajax_follow_plate(): void {
check_ajax_referer('aksrt_forum_action', '_nonce');
$user_id = get_current_user_id();
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$plate_id = (int) ($_POST['plate_id'] ?? 0);
if ($plate_id <= 0) {
wp_send_json_error(['message' => __('版块不存在', 'aksrt')]);
}
$result = $this->toggle_follow_plate($user_id, $plate_id);
if (!empty($result['require_pay'])) {
wp_send_json_error([
'code' => 'require_pay',
'message' => (string) ($result['message'] ?? __('需要付费关注', 'aksrt')),
'require_pay' => true,
'pay' => $result['pay'] ?? [],
'followed' => false,
'follow_count' => (int) ($result['follow_count'] ?? 0),
]);
}
if (isset($result['success']) && $result['success'] === false) {
wp_send_json_error([
'message' => (string) ($result['message'] ?? __('操作失败', 'aksrt')),
]);
}
wp_send_json_success($result);
}
public function ajax_purchase_plate_follow(): void {
check_ajax_referer('aksrt_forum_action', '_nonce');
$user_id = get_current_user_id();
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$plate_id = (int) ($_POST['plate_id'] ?? 0);
if ($plate_id <= 0) {
wp_send_json_error(['message' => __('版块不存在', 'aksrt')]);
}
$result = $this->purchase_plate_follow($user_id, $plate_id);
if (empty($result['success'])) {
$err = [
'message' => (string) ($result['message'] ?? __('支付失败', 'aksrt')),
];
if (!empty($result['require_cashier'])) {
$err['require_cashier'] = true;
$err['pay'] = $result['pay'] ?? [];
$err['modo'] = (string) ($result['modo'] ?? 'money');
$err['plate_id'] = (int) ($result['plate_id'] ?? $plate_id);
}
wp_send_json_error($err);
}
wp_send_json_success($result);
}
}
-604
View File
@@ -1,604 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class CommentService {
const COMMENT_TOP_NONE = 0;
const COMMENT_TOP_NORMAL = 1;
const COMMENT_TOP_HOT = 2;
const COMMENT_TOP_BEST = 3;
/** @var Forum */
private $forum;
public function __construct($forum) {
$this->forum = $forum;
}
// ── Hook: forum comment posted ───────────────────────────────────
public function on_forum_comment_posted(int $comment_id, $approved, array $commentdata = []): void {
$comment = get_comment($comment_id);
if (!$comment) return;
$post = get_post((int) $comment->comment_post_ID);
if (!$post || $post->post_type !== 'aksrt_forum_topic') return;
if ((int) $comment->comment_approved !== 1) return;
$commenter_id = (int) $comment->user_id;
if ($commenter_id <= 0) return;
// Topic-level last_reply for board sort tabs.
update_post_meta((int) $post->ID, '_aksrt_last_reply', (string) $comment->comment_date);
$this->forum->notify_topic_reply((int) $post->ID, $commenter_id);
}
// ── AJAX: get forum comments ─────────────────────────────────────
/**
* AJAX: Get forum comments with sort/filter/pagination.
*/
public function ajax_forum_comments(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$topic_id = (int) ($_POST['topic_id'] ?? 0);
$sort = sanitize_key((string) ($_POST['sort'] ?? 'newest'));
$sort = in_array($sort, ['newest', 'oldest', 'hottest'], true) ? $sort : 'newest';
$author_only = !empty($_POST['author_only']);
$page = max(1, (int) ($_POST['page'] ?? 1));
$per_page = max(1, min(100, (int) ($_POST['per_page'] ?? 30)));
if ($topic_id <= 0) {
wp_send_json_error(['message' => __('参数无效', 'aksrt')]);
}
$result = $this->get_forum_comments($topic_id, $sort, $author_only, $page, $per_page);
ob_start();
$this->render_forum_comments($result['comments'], $topic_id, $result['adopted_id']);
$html = ob_get_clean();
wp_send_json_success([
'html' => $html,
'total' => $result['total'],
'max_pages' => $result['max_pages'],
'page' => $result['page'],
'has_more' => $result['page'] < $result['max_pages'],
]);
}
// ── AJAX: comment topping ────────────────────────────────────────
/**
* AJAX: Set comment topping level (moderator action).
*/
public function ajax_comment_topping(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$comment_id = (int) ($_POST['comment_id'] ?? 0);
$level = max(0, min(3, (int) ($_POST['level'] ?? 0)));
$user_id = get_current_user_id();
$comment = get_comment($comment_id);
if (!$comment) {
wp_send_json_error(['message' => __('评论不存在', 'aksrt')]);
}
$topic_id = (int) $comment->comment_post_ID;
if (!$this->forum->forum_user_can($user_id, 'reply_top', $topic_id)) {
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
}
if ($this->set_comment_topping($comment_id, $level)) {
wp_send_json_success([
'message' => $level > 0 ? __('已置顶', 'aksrt') : __('已取消置顶', 'aksrt'),
'level' => $level,
]);
}
wp_send_json_error(['message' => __('操作失败', 'aksrt')]);
}
// ── Render forum comments ────────────────────────────────────────
/**
* Render forum comments HTML for AJAX and initial load.
*
* @param \WP_Comment[] $comments Comments (parents + children mixed).
* @param int $topic_id Topic post ID.
* @param int $adopted_id Adopted comment ID (0 if none).
*/
public function render_forum_comments(array $comments, int $topic_id, int $adopted_id = 0): void {
$post = get_post($topic_id);
$post_author_id = $post ? (int) $post->post_author : 0;
$current_user_id = get_current_user_id();
$is_question = 'question' === (string) get_post_meta($topic_id, '_aksrt_bbs_type', true);
$can_adopt = $is_question && $current_user_id > 0
&& ($current_user_id === $post_author_id || $this->forum->forum_user_can($current_user_id, 'reply_adopt_other', $topic_id));
$can_moderate = $this->forum->forum_user_can($current_user_id, 'topic_moderate', $topic_id);
if (empty($comments)) {
echo '<div class="aksrt-forum-replies__empty"><i class="far fa-comment-dots"></i> ' . esc_html__('暂无回复,来抢沙发吧!', 'aksrt') . '</div>';
return;
}
foreach ($comments as $comment) {
if (!($comment instanceof \WP_Comment)) continue;
if ((int) $comment->comment_parent !== 0) continue;
$comment_id = (int) $comment->comment_ID;
$commenter_uid = (int) $comment->user_id;
$is_adopted = $comment_id === $adopted_id;
$is_hot = $this->is_hot_comment($comment_id);
$is_post_author = $commenter_uid > 0 && $commenter_uid === $post_author_id;
$topping = (int) get_comment_meta($comment_id, '_aksrt_comment_topping', true);
$badge = $this->get_commenter_badge($commenter_uid, $topic_id);
$like_count = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($comment_id) : 0;
$is_liked = function_exists('aksrt_is_comment_liked') ? aksrt_is_comment_liked($comment_id) : false;
$classes = ['aksrt-forum-reply'];
if ($is_adopted) $classes[] = 'is-adopted';
if ($is_hot) $classes[] = 'is-hot';
if ($topping > 0) $classes[] = 'is-pinned';
?>
<div class="<?php echo esc_attr(implode(' ', $classes)); ?>" id="comment-<?php echo esc_attr((string) $comment_id); ?>" data-comment-id="<?php echo esc_attr((string) $comment_id); ?>">
<div class="aksrt-forum-reply__avatar"><?php echo get_avatar($comment, 40); ?></div>
<div class="aksrt-forum-reply__body">
<div class="aksrt-forum-reply__meta">
<strong><?php comment_author($comment_id); ?></strong>
<?php if ($is_post_author) : ?>
<span class="aksrt-forum-reply__role-badge aksrt-forum-reply__role-badge--author"><?php esc_html_e('楼主', 'aksrt'); ?></span>
<?php endif; ?>
<?php if ($badge) : ?>
<span class="aksrt-forum-reply__role-badge <?php echo esc_attr($badge['class']); ?>"><?php echo esc_html($badge['label']); ?></span>
<?php endif; ?>
<?php if ($topping > 0) : ?>
<span class="aksrt-forum-reply__badge aksrt-forum-reply__badge--pinned"><i class="fas fa-thumbtack"></i> <?php esc_html_e('置顶', 'aksrt'); ?></span>
<?php endif; ?>
<?php if ($is_hot) : ?>
<span class="aksrt-forum-reply__badge aksrt-forum-reply__badge--hot"><i class="fas fa-fire"></i> <?php esc_html_e('神评', 'aksrt'); ?></span>
<?php endif; ?>
<?php if ($is_adopted) : ?>
<span class="aksrt-forum-reply__adopted-badge"><i class="fas fa-check-circle"></i> <?php esc_html_e('最佳答案', 'aksrt'); ?></span>
<?php endif; ?>
<time datetime="<?php echo esc_attr(get_comment_date('c', $comment)); ?>"><?php echo esc_html(get_comment_date('Y-m-d H:i', $comment)); ?></time>
</div>
<div class="aksrt-forum-reply__content"><?php echo function_exists('aksrt_render_comment_content') ? aksrt_render_comment_content((string) get_comment_text($comment_id)) : wp_kses_post(get_comment_text($comment_id)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
<div class="aksrt-forum-reply__actions">
<button type="button" class="aksrt-forum-reply__like-btn js-aksrt-comment-like<?php echo $is_liked ? ' is-active' : ''; ?>"
data-comment-id="<?php echo esc_attr((string) $comment_id); ?>"
data-active="<?php echo $is_liked ? '1' : '0'; ?>">
<i class="<?php echo $is_liked ? 'fas' : 'far'; ?> fa-thumbs-up"></i>
<span class="aksrt-forum-reply__like-count"><?php echo $like_count > 0 ? esc_html((string) $like_count) : ''; ?></span>
</button>
<?php if (is_user_logged_in()) : ?>
<button type="button" class="aksrt-forum-reply__report-btn js-aksrt-report-trigger"
data-target-type="comment"
data-target-id="<?php echo esc_attr((string) $comment_id); ?>"
data-target-title="<?php echo esc_attr(wp_strip_all_tags(get_comment_excerpt($comment))); ?>">
<i class="far fa-flag"></i> <?php esc_html_e('举报', 'aksrt'); ?>
</button>
<?php endif; ?>
<?php if (comments_open($topic_id)) : ?>
<button type="button" class="aksrt-forum-reply__reply-btn js-aksrt-comment-reply"
data-comment-id="<?php echo esc_attr((string) $comment_id); ?>"
data-author="<?php echo esc_attr(get_comment_author($comment_id)); ?>">
<i class="fas fa-reply"></i> <?php esc_html_e('回复', 'aksrt'); ?>
</button>
<?php endif; ?>
<?php if ($can_adopt && !$is_adopted) : ?>
<button type="button" class="aksrt-forum-reply__adopt-btn js-aksrt-adopt-answer"
data-topic-id="<?php echo esc_attr((string) $topic_id); ?>"
data-comment-id="<?php echo esc_attr((string) $comment_id); ?>">
<i class="fas fa-check"></i> <?php esc_html_e('采纳', 'aksrt'); ?>
</button>
<?php endif; ?>
<?php if ($can_moderate) : ?>
<button type="button" class="aksrt-forum-reply__top-btn js-aksrt-comment-top"
data-comment-id="<?php echo esc_attr((string) $comment_id); ?>"
data-level="<?php echo esc_attr((string) $topping); ?>">
<i class="fas fa-thumbtack"></i> <?php echo $topping > 0 ? esc_html__('取消置顶', 'aksrt') : esc_html__('置顶', 'aksrt'); ?>
</button>
<?php endif; ?>
</div>
<?php
// Render nested children (楼中楼)
$child_comments = array_filter($comments, function ($c) use ($comment_id) {
return ($c instanceof \WP_Comment) && (int) $c->comment_parent === $comment_id;
});
if (!empty($child_comments)) : ?>
<div class="aksrt-forum-reply__children">
<?php foreach ($child_comments as $child) :
$child_id = (int) $child->comment_ID;
$child_uid = (int) $child->user_id;
$child_badge = $this->get_commenter_badge($child_uid, $topic_id);
$child_is_author = $child_uid > 0 && $child_uid === $post_author_id;
$child_like_count = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($child_id) : 0;
$child_is_liked = function_exists('aksrt_is_comment_liked') ? aksrt_is_comment_liked($child_id) : false;
$parent_comment = get_comment((int) $child->comment_parent);
$parent_name = $parent_comment ? get_comment_author((int) $parent_comment->comment_ID) : '';
?>
<div class="aksrt-forum-reply aksrt-forum-reply--nested" id="comment-<?php echo esc_attr((string) $child_id); ?>" data-comment-id="<?php echo esc_attr((string) $child_id); ?>">
<div class="aksrt-forum-reply__avatar aksrt-forum-reply__avatar--sm"><?php echo get_avatar($child, 28); ?></div>
<div class="aksrt-forum-reply__body">
<div class="aksrt-forum-reply__meta">
<strong><?php comment_author($child_id); ?></strong>
<?php if ($child_is_author) : ?>
<span class="aksrt-forum-reply__role-badge aksrt-forum-reply__role-badge--author"><?php esc_html_e('楼主', 'aksrt'); ?></span>
<?php endif; ?>
<?php if ($child_badge) : ?>
<span class="aksrt-forum-reply__role-badge <?php echo esc_attr($child_badge['class']); ?>"><?php echo esc_html($child_badge['label']); ?></span>
<?php endif; ?>
<?php if ($parent_name && (int) $child->comment_parent !== $comment_id) : ?>
<span class="aksrt-forum-reply__reply-to"><?php printf(esc_html__('回复 %s', 'aksrt'), esc_html($parent_name)); ?></span>
<?php endif; ?>
<time datetime="<?php echo esc_attr(get_comment_date('c', $child)); ?>"><?php echo esc_html(get_comment_date('Y-m-d H:i', $child)); ?></time>
</div>
<div class="aksrt-forum-reply__content"><?php echo function_exists('aksrt_render_comment_content') ? aksrt_render_comment_content((string) get_comment_text($child_id)) : wp_kses_post(get_comment_text($child_id)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
<div class="aksrt-forum-reply__actions">
<button type="button" class="aksrt-forum-reply__like-btn js-aksrt-comment-like<?php echo $child_is_liked ? ' is-active' : ''; ?>"
data-comment-id="<?php echo esc_attr((string) $child_id); ?>"
data-active="<?php echo $child_is_liked ? '1' : '0'; ?>">
<i class="<?php echo $child_is_liked ? 'fas' : 'far'; ?> fa-thumbs-up"></i>
<span class="aksrt-forum-reply__like-count"><?php echo $child_like_count > 0 ? esc_html((string) $child_like_count) : ''; ?></span>
</button>
<?php if (comments_open($topic_id)) : ?>
<button type="button" class="aksrt-forum-reply__reply-btn js-aksrt-comment-reply"
data-comment-id="<?php echo esc_attr((string) $child_id); ?>"
data-author="<?php echo esc_attr(get_comment_author($child_id)); ?>">
<i class="fas fa-reply"></i> <?php esc_html_e('回复', 'aksrt'); ?>
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<?php
}
}
// ── Adopt answer (best answer) ──────────────────────────────────
public function adopt_answer(int $topic_id, int $comment_id, int $operator_id) {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt'));
}
$bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true));
if ($bbs_type !== 'question') {
return new \WP_Error('not_question', __('该帖不是提问类型', 'aksrt'));
}
$author_id = (int) $post->post_author;
if ($operator_id !== $author_id && !$this->forum->can_moderate_topic($operator_id, $topic_id)) {
return new \WP_Error('forbidden', __('仅提问者或版主可采纳答案', 'aksrt'));
}
$comment = get_comment($comment_id);
if (!$comment || (int) $comment->comment_post_ID !== $topic_id) {
return new \WP_Error('invalid_comment', __('回复不存在', 'aksrt'));
}
$existing = get_post_meta($topic_id, '_aksrt_adopted_comment', true);
if ($existing && (int) $existing === $comment_id) {
return new \WP_Error('already_adopted', __('该回复已被采纳', 'aksrt'));
}
if ($existing) {
delete_comment_meta((int) $existing, '_aksrt_adopted');
}
update_comment_meta($comment_id, '_aksrt_adopted', '1');
update_post_meta($topic_id, '_aksrt_adopted_comment', $comment_id);
do_action('aksrt_forum_reply_adopted', $topic_id, $comment_id, (int) $comment->user_id);
update_post_meta($topic_id, '_aksrt_question_status', 1); // QUESTION_SOLVED
$adoptee_id = (int) $comment->user_id;
if ($adoptee_id > 0) {
$config = $this->forum->get_config();
$amount = (int) ($config['credits']['adopted'] ?? 20);
if ($amount > 0) {
$already = get_comment_meta($comment_id, '_aksrt_adopt_credited', true);
if (!$already) {
if (class_exists('\AKSRT\Credits\PointsFree')) {
$granted = \AKSRT\Credits\PointsFree::grant($adoptee_id, 'bbs_adopt', $amount, [
'type' => 'comment',
'id' => $comment_id,
]);
if (!is_wp_error($granted) && (int) $granted > 0) {
update_comment_meta($comment_id, '_aksrt_adopt_credited', '1');
}
} elseif (class_exists('\AKSRT\Credits\Credits')) {
\AKSRT\Credits\Credits::add($adoptee_id, $amount, 'forum_adopted', [
'type' => 'comment',
'id' => $comment_id,
]);
update_comment_meta($comment_id, '_aksrt_adopt_credited', '1');
}
}
}
$this->forum->notify($adoptee_id, 'system', sprintf(
__('你的回复被采纳为最佳答案(帖子「%s」)', 'aksrt'),
get_the_title($topic_id)
), [
'title' => __('回复被采纳', 'aksrt'),
'target_url' => get_permalink($topic_id) . '#comment-' . $comment_id,
]);
}
return [
'message' => __('已采纳该回复为最佳答案', 'aksrt'),
'comment_id' => $comment_id,
'solved' => true,
];
}
public function get_adopted_comment_id(int $topic_id): int {
return (int) get_post_meta($topic_id, '_aksrt_adopted_comment', true);
}
// ── Get forum comments (query) ──────────────────────────────────
/**
* Get forum comments with sort/filter support.
*
* @param int $topic_id Topic post ID.
* @param string $sort 'newest'|'oldest'|'hottest'
* @param bool $author_only Only show post-author replies.
* @param int $page Page number.
* @param int $per_page Items per page.
* @return array{comments:\WP_Comment[],total:int,max_pages:int,page:int,adopted_id:int}
*/
public function get_forum_comments(int $topic_id, string $sort = 'newest', bool $author_only = false, int $page = 1, int $per_page = 30): array {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return ['comments' => [], 'total' => 0, 'max_pages' => 1, 'page' => 1, 'adopted_id' => 0];
}
$args = [
'post_id' => $topic_id,
'status' => 'approve',
'parent' => 0,
'number' => $per_page,
'offset' => ($page - 1) * $per_page,
];
if ($author_only) {
$args['user_id'] = (int) $post->post_author;
}
if ($sort === 'hottest') {
$args['meta_key'] = 'aksrt_comment_likes';
$args['orderby'] = 'meta_value_num';
$args['order'] = 'DESC';
} elseif ($sort === 'oldest') {
$args['orderby'] = 'comment_date_gmt';
$args['order'] = 'ASC';
} else {
$args['orderby'] = 'comment_date_gmt';
$args['order'] = 'DESC';
}
// Total count for pagination
$count_args = $args;
unset($count_args['number'], $count_args['offset']);
$count_args['count'] = true;
$total = (int) get_comments($count_args);
$top_level = get_comments($args);
// Fetch child comments for these parents
if (!empty($top_level)) {
$parent_ids = wp_list_pluck($top_level, 'comment_ID');
$children = get_comments([
'post_id' => $topic_id,
'status' => 'approve',
'parent__in' => $parent_ids,
'orderby' => 'comment_date_gmt',
'order' => 'ASC',
'number' => 200,
]);
} else {
$children = [];
}
// Float pinned top-level comments
$pinned = [];
$normal = [];
foreach ($top_level as $c) {
$topping = (int) get_comment_meta((int) $c->comment_ID, '_aksrt_comment_topping', true);
if ($topping > 0) {
$c->aksrt_topping = $topping;
$pinned[] = $c;
} else {
$normal[] = $c;
}
}
usort($pinned, function ($a, $b) {
return ($b->aksrt_topping ?? 0) <=> ($a->aksrt_topping ?? 0);
});
$comments = array_merge($pinned, $normal, $children);
return [
'comments' => $comments,
'total' => $total,
'max_pages' => max(1, (int) ceil($total / $per_page)),
'page' => $page,
'adopted_id' => $this->get_adopted_comment_id($topic_id),
];
}
// ── Set comment topping ─────────────────────────────────────────
/**
* Set comment topping level.
*
* @param int $comment_id Comment ID.
* @param int $level 0=unpin, 1=normal pin, 2=hot pin, 3=best answer pin.
*/
public function set_comment_topping(int $comment_id, int $level): bool {
$comment = get_comment($comment_id);
if (!$comment) {
return false;
}
if ($level <= 0) {
delete_comment_meta($comment_id, '_aksrt_comment_topping');
delete_comment_meta($comment_id, '_aksrt_comment_pinned');
} else {
update_comment_meta($comment_id, '_aksrt_comment_topping', $level);
update_comment_meta($comment_id, '_aksrt_comment_pinned', '1');
}
return true;
}
// ── Maybe mark hot comment ──────────────────────────────────────
/**
* Check and mark a comment as hot ("神评") if it crosses the like threshold.
*
* @return bool True if newly marked as hot.
*/
public function maybe_mark_hot_comment(int $comment_id): bool {
$comment = get_comment($comment_id);
if (!$comment || '1' !== (string) $comment->comment_approved) {
return false;
}
$post = get_post((int) $comment->comment_post_ID);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return false;
}
$already_hot = '1' === (string) get_comment_meta($comment_id, '_aksrt_is_hot', true);
if ($already_hot) {
return false;
}
$config = $this->forum->get_config();
$threshold = max(1, (int) ($config['reply']['hot_threshold'] ?? 10));
$likes = max(0, (int) get_comment_meta($comment_id, 'aksrt_comment_likes', true));
if ($likes < $threshold) {
return false;
}
update_comment_meta($comment_id, '_aksrt_is_hot', '1');
$hot_author = (int) (get_comment($comment_id)->user_id ?? 0);
do_action('aksrt_forum_comment_hot', $comment_id, $hot_author);
// Credit the comment author (free-task day cap).
$author_id = (int) $comment->user_id;
if ($author_id > 0) {
$amount = (int) ($config['credits']['comment_hot'] ?? $config['credits']['comment_like'] ?? 5);
if ($amount > 0) {
if (class_exists('\AKSRT\Credits\PointsFree')) {
\AKSRT\Credits\PointsFree::grant($author_id, 'bbs_comment_hot', $amount, [
'type' => 'comment',
'id' => $comment_id,
]);
} elseif (class_exists('\AKSRT\Credits\Credits')) {
\AKSRT\Credits\Credits::add($author_id, $amount, 'forum_hot_comment', [
'type' => 'comment',
'id' => $comment_id,
]);
}
}
}
// Notify the author
if ($author_id > 0) {
$this->forum->notify($author_id, 'system', sprintf(
__('你的回复成为了神评(帖子「%s」)', 'aksrt'),
get_the_title((int) $comment->comment_post_ID)
), [
'title' => __('恭喜获得神评', 'aksrt'),
'target_url' => get_permalink((int) $comment->comment_post_ID) . '#comment-' . $comment_id,
]);
}
return true;
}
// ── Is hot comment ──────────────────────────────────────────────
/**
* Check if a comment is a hot comment ("神评").
*/
public function is_hot_comment(int $comment_id): bool {
return '1' === (string) get_comment_meta($comment_id, '_aksrt_is_hot', true);
}
// ── Get commenter badge ─────────────────────────────────────────
/**
* Get the user's moderator tier label for badge rendering in comments.
*
* @return array{tier:int,label:string,class:string}|null
*/
public function get_commenter_badge(int $user_id, int $topic_id): ?array {
if ($user_id <= 0) {
return null;
}
if ($this->forum->is_forum_admin($user_id)) {
return [
'tier' => Forum::TIER_FORUM_ADMIN,
'label' => __('管理员', 'aksrt'),
'class' => 'aksrt-forum-reply__role-badge--admin',
];
}
$plate_id = $this->forum->get_topic_plate_id($topic_id);
if ($plate_id > 0) {
$tier = $this->forum->get_user_plate_tier($user_id, $plate_id);
if ($tier >= Forum::TIER_CAT_MOD) {
$names = $this->forum->get_config()['names'] ?? [];
return [
'tier' => $tier,
'label' => $names['cat_moderator'] ?? __('分区版主', 'aksrt'),
'class' => 'aksrt-forum-reply__role-badge--catmod',
];
}
if ($tier >= Forum::TIER_PLATE_AUTHOR) {
$names = $this->forum->get_config()['names'] ?? [];
return [
'tier' => $tier,
'label' => $names['plate_author'] ?? __('超级版主', 'aksrt'),
'class' => 'aksrt-forum-reply__role-badge--super',
];
}
if ($tier >= Forum::TIER_MODERATOR) {
$names = $this->forum->get_config()['names'] ?? [];
return [
'tier' => $tier,
'label' => $names['plate_moderator'] ?? __('版主', 'aksrt'),
'class' => 'aksrt-forum-reply__role-badge--mod',
];
}
}
return null;
}
}
-441
View File
@@ -1,441 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class DraftService {
/** @var Forum */
private $forum;
public function __construct(Forum $forum) {
$this->forum = $forum;
}
public function save_draft(int $user_id, array $data) {
if ($user_id <= 0) {
return new \WP_Error('not_logged_in', __('请先登录', 'aksrt'));
}
$draft_id = (int) ($data['draft_id'] ?? 0);
$post_arr = [
'post_type' => 'aksrt_forum_topic',
'post_status' => 'draft',
'post_author' => $user_id,
'post_title' => sanitize_text_field((string) ($data['title'] ?? '')),
'post_content' => wp_kses_post((string) ($data['content'] ?? '')),
];
if ($draft_id > 0) {
$existing = get_post($draft_id);
if (!$existing || $existing->post_type !== 'aksrt_forum_topic'
|| (int) $existing->post_author !== $user_id
|| $existing->post_status !== 'draft') {
return new \WP_Error('invalid_draft', __('草稿不存在或无权编辑', 'aksrt'));
}
$post_arr['ID'] = $draft_id;
$result = wp_update_post($post_arr, true);
} else {
$result = wp_insert_post($post_arr, true);
}
if (is_wp_error($result)) {
return $result;
}
$post_id = (int) $result;
$board_id = (int) ($data['board_id'] ?? 0);
if ($board_id > 0) {
if ($this->forum->is_plate_cpt_active()) {
update_post_meta($post_id, '_aksrt_plate_id', $board_id);
} else {
wp_set_object_terms($post_id, [$board_id], 'aksrt_forum_board');
}
}
$tags = is_array($data['tags'] ?? null) ? $data['tags'] : [];
if (!empty($tags)) {
$clean_tags = array_slice(array_values(array_filter(array_map('sanitize_text_field', $tags))), 0, 5);
wp_set_object_terms($post_id, $clean_tags, 'aksrt_forum_tag');
}
$meta_fields = [
'visibility' => '_aksrt_visibility',
'bbs_type' => '_aksrt_bbs_type',
'special_topic' => '_aksrt_special_topic',
'cover_type' => '_aksrt_cover_type',
];
foreach ($meta_fields as $key => $meta_key) {
if (isset($data[$key])) {
update_post_meta($post_id, $meta_key, sanitize_key((string) $data[$key]));
}
}
if (isset($data['credits_cost'])) {
$cost = max(0, (int) $data['credits_cost']);
update_post_meta($post_id, '_aksrt_credits_cost', $cost);
update_post_meta($post_id, '_aksrt_pay_credits', $cost);
}
if (isset($data['pay_price'])) {
update_post_meta($post_id, '_aksrt_pay_price', sanitize_text_field((string) $data['pay_price']));
}
if (isset($data['credits_vip1']) && (string) $data['credits_vip1'] !== '') {
update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) $data['credits_vip1']));
}
if (isset($data['credits_vip2']) && (string) $data['credits_vip2'] !== '') {
update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) $data['credits_vip2']));
}
if (isset($data['pay_vip1_price']) && (string) $data['pay_vip1_price'] !== '') {
$v1 = max(0, (float) $data['pay_vip1_price']);
update_post_meta($post_id, '_aksrt_pay_vip1_price', $v1);
update_post_meta($post_id, '_aksrt_pay_vip_price', $v1);
}
if (isset($data['pay_vip2_price']) && (string) $data['pay_vip2_price'] !== '') {
update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) $data['pay_vip2_price']));
}
if (array_key_exists('pay_hide_part', $data)) {
update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0);
}
if (array_key_exists('topic_coupon_s', $data)) {
update_post_meta($post_id, '_aksrt_topic_coupon_s', !empty($data['topic_coupon_s']) ? 1 : 0);
}
if (isset($data['visibility_roles']) && is_array($data['visibility_roles'])) {
update_post_meta($post_id, '_aksrt_visibility_roles', array_map('sanitize_key', $data['visibility_roles']));
}
$this->forum->save_cover_meta($post_id, $data);
if (!empty($data['vote_enabled'])) {
$vote_meta = $this->forum->prepare_vote_meta($data);
$this->forum->save_vote_meta($post_id, $vote_meta);
}
if (isset($data['topic_cat'])) {
$topic_cat = sanitize_text_field((string) $data['topic_cat']);
if ($topic_cat !== '' && taxonomy_exists('aksrt_forum_topic_cat')) {
wp_set_object_terms($post_id, [$topic_cat], 'aksrt_forum_topic_cat');
}
}
return [
'post_id' => $post_id,
'message' => __('草稿已保存', 'aksrt'),
];
}
public function get_user_draft_count(int $user_id): int {
if ($user_id <= 0) {
return 0;
}
global $wpdb;
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'aksrt_forum_topic' AND post_status = 'draft' AND post_author = %d",
$user_id
));
}
/**
* @return array<int,array{id:int,title:string,date:string,board_name:string}>
*/
public function get_user_drafts(int $user_id, int $limit = 20): array {
if ($user_id <= 0) {
return [];
}
$query = new \WP_Query([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'draft',
'author' => $user_id,
'posts_per_page' => $limit,
'orderby' => 'modified',
'order' => 'DESC',
'no_found_rows' => true,
]);
$drafts = [];
while ($query->have_posts()) {
$query->the_post();
$post = get_post();
if (!$post) {
continue;
}
$board_name = '';
if ($this->forum->is_plate_cpt_active()) {
$plate_id = (int) get_post_meta($post->ID, '_aksrt_plate_id', true);
if ($plate_id > 0) {
$board_name = get_the_title($plate_id);
}
} else {
$terms = wp_get_post_terms($post->ID, 'aksrt_forum_board', ['fields' => 'names']);
if (!is_wp_error($terms) && !empty($terms)) {
$board_name = $terms[0];
}
}
$drafts[] = [
'id' => $post->ID,
'title' => $post->post_title ?: __('(无标题)', 'aksrt'),
'date' => $post->post_modified,
'board_name' => $board_name,
];
}
wp_reset_postdata();
return $drafts;
}
public function delete_draft(int $user_id, int $draft_id): bool {
if ($user_id <= 0 || $draft_id <= 0) {
return false;
}
$post = get_post($draft_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic'
|| (int) $post->post_author !== $user_id
|| $post->post_status !== 'draft') {
return false;
}
return (bool) wp_delete_post($draft_id, true);
}
/**
* Load a draft's full data for the compose form.
*/
public function load_draft_data(int $user_id, int $draft_id): ?array {
if ($user_id <= 0 || $draft_id <= 0) {
return null;
}
$post = get_post($draft_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic'
|| (int) $post->post_author !== $user_id
|| $post->post_status !== 'draft') {
return null;
}
$board_id = 0;
if ($this->forum->is_plate_cpt_active()) {
$board_id = (int) get_post_meta($draft_id, '_aksrt_plate_id', true);
} else {
$terms = wp_get_post_terms($draft_id, 'aksrt_forum_board', ['fields' => 'ids']);
if (!is_wp_error($terms) && !empty($terms)) {
$board_id = (int) $terms[0];
}
}
$tags = wp_get_post_terms($draft_id, 'aksrt_forum_tag', ['fields' => 'names']);
if (is_wp_error($tags)) {
$tags = [];
}
$topic_cat = '';
if (taxonomy_exists('aksrt_forum_topic_cat')) {
$cats = wp_get_post_terms($draft_id, 'aksrt_forum_topic_cat', ['fields' => 'names']);
if (!is_wp_error($cats) && !empty($cats)) {
$topic_cat = $cats[0];
}
}
$slide_raw = get_post_meta($draft_id, '_aksrt_featured_slide', true);
return [
'draft_id' => $draft_id,
'title' => $post->post_title,
'content' => $post->post_content,
'board_id' => $board_id,
'tags' => implode(', ', $tags),
'visibility' => get_post_meta($draft_id, '_aksrt_visibility', true) ?: Forum::VISIBILITY_PUBLIC,
'bbs_type' => get_post_meta($draft_id, '_aksrt_bbs_type', true) ?: Forum::BBS_TYPE_STANDARD,
'special_topic' => get_post_meta($draft_id, '_aksrt_special_topic', true) ?: '',
'cover_type' => get_post_meta($draft_id, '_aksrt_cover_type', true) ?: '',
'cover_image' => get_post_meta($draft_id, '_aksrt_cover_image', true) ?: '',
'featured_slide' => is_array($slide_raw) ? $slide_raw : [],
'featured_video' => get_post_meta($draft_id, '_aksrt_featured_video', true) ?: '',
'credits_cost' => (int) get_post_meta($draft_id, '_aksrt_credits_cost', true),
'pay_price' => get_post_meta($draft_id, '_aksrt_pay_price', true) ?: '',
'credits_vip1' => get_post_meta($draft_id, '_aksrt_credits_vip1', true),
'credits_vip2' => get_post_meta($draft_id, '_aksrt_credits_vip2', true),
'pay_vip1_price' => get_post_meta($draft_id, '_aksrt_pay_vip1_price', true),
'pay_vip2_price' => get_post_meta($draft_id, '_aksrt_pay_vip2_price', true),
'pay_hide_part' => (int) get_post_meta($draft_id, '_aksrt_pay_hide_part', true) === 1,
'topic_coupon_s' => (int) get_post_meta($draft_id, '_aksrt_topic_coupon_s', true) === 1,
'visibility_roles' => get_post_meta($draft_id, '_aksrt_visibility_roles', true) ?: [],
'topic_cat' => $topic_cat,
'vote_enabled' => get_post_meta($draft_id, '_aksrt_vote_enabled', true) === '1',
'vote_title' => get_post_meta($draft_id, '_aksrt_vote_title', true) ?: '',
'vote_multiple' => get_post_meta($draft_id, '_aksrt_vote_multiple', true) === '1',
'vote_type' => get_post_meta($draft_id, '_aksrt_vote_type', true) ?: 'standard',
'vote_expiry_days' => (int) get_post_meta($draft_id, '_aksrt_vote_expiry_days', true),
'vote_options' => $this->get_vote_options_text($draft_id),
];
}
private function get_vote_options_text(int $post_id): string {
$options = get_post_meta($post_id, '_aksrt_vote_options', true);
if (!is_array($options)) {
return '';
}
return implode("\n", array_column($options, 'label'));
}
// ──────────────────────────────────────────────────────────
// AJAX handlers
// ──────────────────────────────────────────────────────────
public function ajax_save_draft(): void {
check_ajax_referer('aksrt_forum_compose', '_nonce');
$user_id = get_current_user_id();
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$data = [
'draft_id' => (int) ($_POST['draft_id'] ?? 0),
'board_id' => (int) ($_POST['board_id'] ?? 0),
'title' => sanitize_text_field((string) ($_POST['title'] ?? '')),
'content' => wp_kses_post((string) ($_POST['content'] ?? '')),
'tags' => $this->forum->split_tags((string) ($_POST['tags'] ?? '')),
'special_topic' => sanitize_text_field((string) ($_POST['special_topic'] ?? '')),
'visibility' => sanitize_key((string) ($_POST['visibility'] ?? Forum::VISIBILITY_PUBLIC)),
'bbs_type' => sanitize_key((string) ($_POST['bbs_type'] ?? Forum::BBS_TYPE_STANDARD)),
'topic_cat' => sanitize_text_field((string) ($_POST['topic_cat'] ?? '')),
'vote_enabled' => !empty($_POST['vote_enabled']),
'vote_title' => sanitize_text_field((string) ($_POST['vote_title'] ?? '')),
'vote_options_text' => sanitize_textarea_field((string) ($_POST['vote_options'] ?? '')),
'vote_multiple' => !empty($_POST['vote_multiple']),
'vote_type' => sanitize_key((string) ($_POST['vote_type'] ?? 'standard')),
'vote_expiry_days' => (int) ($_POST['vote_expiry_days'] ?? 0),
'cover_type' => sanitize_key((string) ($_POST['cover_type'] ?? '')),
'cover_image' => esc_url_raw((string) ($_POST['cover_image'] ?? '')),
'featured_slide' => (string) ($_POST['featured_slide'] ?? ''),
'featured_video' => esc_url_raw((string) ($_POST['featured_video'] ?? '')),
'credits_cost' => (int) ($_POST['credits_cost'] ?? 0),
'pay_price' => sanitize_text_field((string) ($_POST['pay_price'] ?? '')),
'credits_vip1' => isset($_POST['credits_vip1']) ? sanitize_text_field((string) $_POST['credits_vip1']) : '',
'credits_vip2' => isset($_POST['credits_vip2']) ? sanitize_text_field((string) $_POST['credits_vip2']) : '',
'pay_vip1_price' => isset($_POST['pay_vip1_price']) ? sanitize_text_field((string) $_POST['pay_vip1_price']) : '',
'pay_vip2_price' => isset($_POST['pay_vip2_price']) ? sanitize_text_field((string) $_POST['pay_vip2_price']) : '',
'pay_hide_part' => !empty($_POST['pay_hide_part']),
'topic_coupon_s' => !empty($_POST['topic_coupon_s']),
'visibility_roles' => isset($_POST['visibility_roles']) && is_array($_POST['visibility_roles'])
? array_map('sanitize_key', $_POST['visibility_roles']) : [],
];
$result = $this->save_draft($user_id, $data);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success($result);
}
public function ajax_load_draft(): void {
check_ajax_referer('aksrt_forum_compose', '_nonce');
$user_id = get_current_user_id();
$draft_id = (int) ($_GET['draft_id'] ?? $_POST['draft_id'] ?? 0);
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$data = $this->load_draft_data($user_id, $draft_id);
if (!$data) {
wp_send_json_error(['message' => __('草稿不存在', 'aksrt')]);
}
wp_send_json_success($data);
}
public function ajax_delete_draft(): void {
check_ajax_referer('aksrt_forum_compose', '_nonce');
$user_id = get_current_user_id();
$draft_id = (int) ($_POST['draft_id'] ?? 0);
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
if (!$this->delete_draft($user_id, $draft_id)) {
wp_send_json_error(['message' => __('删除失败', 'aksrt')]);
}
wp_send_json_success(['message' => __('草稿已删除', 'aksrt')]);
}
// ──────────────────────────────────────────────────────────
// Publish draft (transition from draft → publish/pending)
// ──────────────────────────────────────────────────────────
public function publish_draft(int $user_id, int $draft_id, array $data) {
if ($user_id <= 0) {
return new \WP_Error('not_logged_in', __('请先登录', 'aksrt'));
}
$post = get_post($draft_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic'
|| (int) $post->post_author !== $user_id
|| $post->post_status !== 'draft') {
return new \WP_Error('invalid_draft', __('草稿不存在', 'aksrt'));
}
$data['draft_id'] = $draft_id;
$save_result = $this->save_draft($user_id, $data);
if (is_wp_error($save_result)) {
return $save_result;
}
$board_id = (int) ($data['board_id'] ?? 0);
$title = sanitize_text_field((string) ($data['title'] ?? ''));
$content = wp_kses_post((string) ($data['content'] ?? ''));
if ($board_id <= 0 || $title === '' || $content === '') {
return new \WP_Error('invalid_payload', __('请填写版块、标题和内容', 'aksrt'));
}
$can_publish = $this->forum->can_user_publish_directly($user_id, $board_id);
$post_status = $can_publish ? 'publish' : 'pending';
$review_status = $can_publish ? Forum::REVIEW_APPROVED : Forum::REVIEW_PENDING;
wp_update_post([
'ID' => $draft_id,
'post_status' => $post_status,
]);
update_post_meta($draft_id, '_aksrt_review_status', $review_status);
update_post_meta($draft_id, '_aksrt_sticky', Forum::STICKY_NONE);
update_post_meta($draft_id, '_aksrt_featured', '0');
update_post_meta($draft_id, '_aksrt_locked', '0');
update_post_meta($draft_id, '_aksrt_views', 0);
update_post_meta($draft_id, '_aksrt_last_reply', (string) get_post_field('post_date', $draft_id));
$bbs_type = get_post_meta($draft_id, '_aksrt_bbs_type', true) ?: Forum::BBS_TYPE_STANDARD;
if ($bbs_type === Forum::BBS_TYPE_QUESTION) {
update_post_meta($draft_id, '_aksrt_question_status', Forum::QUESTION_UNSOLVED);
}
if ($post_status === 'publish') {
$this->forum->sync_board_count($board_id);
if (class_exists('\AKSRT\Credits\Earn')) {
do_action('aksrt_forum_topic_created', $user_id, $draft_id);
}
}
if ($post_status === 'pending') {
$this->forum->notify_topic_pending($draft_id, $user_id);
}
return [
'post_id' => $draft_id,
'message' => $post_status === 'publish'
? __('主题已发布', 'aksrt')
: __('主题已提交审核,审核通过后对外展示', 'aksrt'),
'redirect' => $post_status === 'publish'
? get_permalink($draft_id)
: add_query_arg('compose_status', 'pending', aksrt_get_forum_home_url()),
];
}
}
-959
View File
@@ -1,959 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class Forum {
private static $instance = null;
const TIER_NONE = 0; // 游客/无权
const TIER_MODERATOR = 1; // 版主(plate meta _aksrt_moderators)
const TIER_PLATE_AUTHOR = 2; // 超级版主(plate post_author)
const TIER_CAT_MOD = 3; // 分区版主(plate_cat term meta moderator)
const TIER_FORUM_ADMIN = 4; // 论坛管理员(bbs_admin_users + manage_options)
private static array $cap_cache = [];
private static array $tier_cache = [];
private ?PermissionService $perm_service = null;
private ?ScoreService $score_service = null;
private ?CommentService $comment_service = null;
private ?DraftService $draft_svc = null;
private ?BoardService $board_svc = null;
const VISIBILITY_PUBLIC = 'public';
const VISIBILITY_LOGIN = 'login';
const VISIBILITY_COMMENT = 'comment';
const VISIBILITY_ROLE = 'role';
const VISIBILITY_CREDITS = 'credits';
const VISIBILITY_PAY = 'pay';
const VISIBILITY_MODERATOR = 'moderator';
const REVIEW_APPROVED = 'approved';
const REVIEW_PENDING = 'pending';
const REVIEW_REJECTED = 'rejected';
const APPLICATION_PENDING = 'pending';
const APPLICATION_APPROVED = 'approved';
const APPLICATION_REJECTED = 'rejected';
const STICKY_NONE = 0;
const STICKY_PLATE = 1;
const STICKY_SUPER = 2;
const STICKY_GLOBAL = 3;
const BBS_TYPE_STANDARD = 'standard';
const BBS_TYPE_QUESTION = 'question';
const QUESTION_UNSOLVED = 0;
const QUESTION_SOLVED = 1;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function permission_service(): PermissionService {
if (!$this->perm_service) {
$this->perm_service = new PermissionService($this);
}
return $this->perm_service;
}
private function score_service(): ScoreService {
if (!$this->score_service) {
$this->score_service = new ScoreService($this);
}
return $this->score_service;
}
private function comment_service(): CommentService {
if (!$this->comment_service) {
$this->comment_service = new CommentService($this);
}
return $this->comment_service;
}
private function draft_service(): DraftService {
if (!$this->draft_svc) {
// Composer PSR-4: AKSRT\Forum\DraftService → src/AKSRT/Forum/DraftService.php
// Never require legacy class-aksrt-forum-draft-service.php.
if (!class_exists(DraftService::class, true)) {
throw new \RuntimeException(
'AKSRT\\Forum\\DraftService missing; deploy src/AKSRT/Forum/DraftService.php and run composer dump-autoload if needed'
);
}
$this->draft_svc = new DraftService($this);
}
return $this->draft_svc;
}
private ?TopicService $topic_svc = null;
private ?ModerationService $mod_svc = null;
private function moderation_service(): ModerationService {
if ($this->mod_svc === null) {
$this->mod_svc = new ModerationService($this);
}
return $this->mod_svc;
}
private ?ViewBuilder $view_builder = null;
private function view_builder(): ViewBuilder {
if ($this->view_builder === null) {
$this->view_builder = new ViewBuilder($this);
}
return $this->view_builder;
}
private function topic_service(): TopicService {
if ($this->topic_svc === null) {
$this->topic_svc = new TopicService($this);
}
return $this->topic_svc;
}
private function board_service(): BoardService {
if (!$this->board_svc) {
$this->board_svc = new BoardService($this);
}
return $this->board_svc;
}
public function is_plate_cpt_active(): bool {
// Plate CPT is the native board model; no option migration gate.
return post_type_exists('aksrt_forum_plate');
}
public function get_config(): array {
if (class_exists('\AKSRT\Settings\Settings')) {
return \AKSRT\Settings\Settings::get_forum_settings();
}
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);
}
public function create_board(array $data): int {
return $this->board_service()->create_board($data);
}
public function update_board(int $board_id, array $data): bool {
return $this->board_service()->update_board($board_id, $data);
}
public function delete_board(int $board_id): bool {
return $this->board_service()->delete_board($board_id);
}
public function reorder_boards(array $order_map): bool {
return $this->board_service()->reorder_boards($order_map);
}
// §7.0 Plate CPT methods —板块 CPT 化 CRUD
public function get_plates(array $args = []): array {
return $this->board_service()->get_plates($args);
}
public function get_plate(int $plate_id): ?array {
return $this->board_service()->get_plate($plate_id);
}
public function build_plate_payload(\WP_Post $post): array {
return $this->board_service()->build_plate_payload($post);
}
public function create_plate(array $data): int {
return $this->board_service()->create_plate($data);
}
public function update_plate(int $plate_id, array $data): bool {
return $this->board_service()->update_plate($plate_id, $data);
}
public function delete_plate(int $plate_id, string $cascade = 'trash', int $move_to = 0): bool {
return $this->board_service()->delete_plate($plate_id, $cascade, $move_to);
}
public function reorder_plates(array $order_map): bool {
return $this->board_service()->reorder_plates($order_map);
}
public function get_topic_plate_id(int $topic_id): int {
return $this->board_service()->get_topic_plate_id($topic_id);
}
public function is_plate_moderator(int $user_id, int $plate_id): bool {
return $this->board_service()->is_plate_moderator($user_id, $plate_id);
}
public function get_plate_view(int $plate_id, array $args = []): array {
return $this->board_service()->get_plate_view($plate_id, $args);
}
public function get_topics(array $args = []): array {
$view = $this->get_topic_list($args);
return $view['items'];
}
public function get_topic_list(array $args = []): array {
return $this->view_builder()->get_topic_list($args);
}
public function get_home_view(int $user_id = 0): array {
return $this->view_builder()->get_home_view($user_id);
}
public function get_follow_stream(int $user_id, int $page = 1, int $limit = 15): array {
return $this->view_builder()->get_follow_stream($user_id, $page, $limit);
}
public function get_synthesis_stream(int $page = 1, int $limit = 15, int $viewer = 0): array {
return $this->view_builder()->get_synthesis_stream($page, $limit, $viewer);
}
public function get_pending_stream(int $user_id, int $page = 1, int $limit = 15): array {
return $this->view_builder()->get_pending_stream($user_id, $page, $limit);
}
public function ajax_home_tab(): void {
$this->view_builder()->ajax_home_tab();
}
public function ajax_board_topics(): void {
$this->view_builder()->ajax_board_topics();
}
public function register_widget_routes(): void {
register_rest_route('aksrt/v1', '/forum/frontend/widgets/topics', [
'methods' => 'GET',
'callback' => [$this, 'rest_widget_topics'],
'permission_callback' => '__return_true',
]);
register_rest_route('aksrt/v1', '/forum/frontend/widgets/plates', [
'methods' => 'GET',
'callback' => [$this, 'rest_widget_plates'],
'permission_callback' => '__return_true',
]);
}
public function rest_widget_topics(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\WidgetForumTopicLists')) {
return new \WP_REST_Response(['success' => false, 'message' => 'widget unavailable'], 500);
}
$instance = [
'count' => max(1, min(30, (int) ($request->get_param('count') ?? 8))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'date')),
'style' => sanitize_key((string) ($request->get_param('style') ?? 'mini')),
'plate_id' => max(0, (int) ($request->get_param('plate_id') ?? 0)),
'paginate' => sanitize_key((string) ($request->get_param('paginate') ?? 'ajax')),
];
$page = max(1, (int) ($request->get_param('page') ?? $request->get_param('paged') ?? 1));
$payload = \AKSRT\Widgets\WidgetForumTopicLists::query_payload($instance, $page);
return new \WP_REST_Response([
'success' => true,
'html' => (string) ($payload['html'] ?? ''),
'paginate_html' => (string) ($payload['paginate_html'] ?? ''),
'has_more' => !empty($payload['has_more']),
'page' => (int) ($payload['page'] ?? $page),
'max_pages' => (int) ($payload['max_pages'] ?? 1),
'total' => (int) ($payload['total'] ?? 0),
], 200);
}
public function rest_widget_plates(\WP_REST_Request $request) {
if (!class_exists('\AKSRT\Widgets\WidgetForumPlateLists')) {
return new \WP_REST_Response(['success' => false, 'message' => 'widget unavailable'], 500);
}
$instance = [
'showposts' => max(1, min(30, (int) ($request->get_param('showposts') ?? $request->get_param('count') ?? 8))),
'orderby' => sanitize_key((string) ($request->get_param('orderby') ?? 'menu_order')),
'style' => sanitize_key((string) ($request->get_param('style') ?? 'default')),
'filter' => sanitize_key((string) ($request->get_param('filter') ?? '')),
'cat' => sanitize_text_field((string) ($request->get_param('cat') ?? '')),
];
$payload = \AKSRT\Widgets\WidgetForumPlateLists::query_payload($instance);
return new \WP_REST_Response([
'success' => true,
'html' => (string) ($payload['html'] ?? ''),
], 200);
}
public function render_board_cards(array $boards): void {
$this->view_builder()->render_board_cards($boards);
}
public function render_topic_row(array $topic, string $style = 'detail'): void {
$this->view_builder()->render_topic_row($topic, $style);
}
public function get_board_view(int $board_id, array $args = []): array {
return $this->board_service()->get_board_view($board_id, $args);
}
public function can_view_plate(int $user_id, int $plate_id, string $allow_view = ''): bool {
return $this->board_service()->can_view_plate($user_id, $plate_id, $allow_view);
}
public function can_compose_in_plate(int $user_id, int $plate_id): bool {
return $this->board_service()->can_compose_in_plate($user_id, $plate_id);
}
public function is_user_following_plate(int $user_id, int $plate_id): bool {
return $this->board_service()->is_user_following_plate($user_id, $plate_id);
}
public function is_plate_follow_pay_enabled(int $plate_id): bool {
return $this->board_service()->is_plate_follow_pay_enabled($plate_id);
}
public function resolve_plate_follow_pay_price(int $plate_id, int $user_id = 0): array {
return $this->board_service()->resolve_plate_follow_pay_price($plate_id, $user_id);
}
public function mark_plate_followed(int $user_id, int $plate_id): void {
$this->board_service()->mark_plate_followed($user_id, $plate_id);
}
public function toggle_follow_plate(int $user_id, int $plate_id): array {
return $this->board_service()->toggle_follow_plate($user_id, $plate_id);
}
public function get_user_followed_plates(int $user_id): array {
return $this->board_service()->get_user_followed_plates($user_id);
}
public function get_user_managed_plates(int $user_id): array {
return $this->board_service()->get_user_managed_plates($user_id);
}
public function get_user_forum_topics(int $user_id, array $args = []): array {
return $this->view_builder()->get_user_forum_topics($user_id, $args);
}
public function get_user_favorite_topics(int $user_id, int $page = 1, int $limit = 10): array {
return $this->view_builder()->get_user_favorite_topics($user_id, $page, $limit);
}
public function get_user_forum_stats(int $user_id): array {
return $this->view_builder()->get_user_forum_stats($user_id);
}
public function increment_plate_views(int $plate_id): void {
$this->board_service()->increment_plate_views($plate_id);
}
public function is_hot_plate(int $plate_id): bool {
return $this->board_service()->is_hot_plate($plate_id);
}
public function get_plate_categories(): array {
return $this->board_service()->get_plate_categories();
}
public function is_cat_moderator(int $user_id, int $cat_id): bool {
return $this->board_service()->is_cat_moderator($user_id, $cat_id);
}
public function get_sorted_plates(string $orderby = 'menu_order', string $order = 'ASC', int $limit = 50): array {
return $this->board_service()->get_sorted_plates($orderby, $order, $limit);
}
public function get_topic_view(int $topic_id, int $user_id = 0): array {
return $this->view_builder()->get_topic_view($topic_id, $user_id);
}
public function get_compose_view(int $user_id, array $args = []): array {
return $this->view_builder()->get_compose_view($user_id, $args);
}
public function get_stats(): array {
return $this->view_builder()->get_stats();
}
public function can_edit_topic(int $user_id, int $topic_id): bool {
return $this->topic_service()->can_edit_topic($user_id, $topic_id);
}
public function can_moderate_topic(int $user_id, int $topic_id): bool {
return $this->forum_user_can($user_id, 'topic_moderate', $topic_id);
}
public function update_topic(int $topic_id, array $data) {
return $this->topic_service()->update_topic($topic_id, $data);
}
public function get_tags(): array {
return $this->view_builder()->get_tags();
}
public function get_tag_archive_view(array $args = []): array {
return $this->view_builder()->get_tag_archive_view($args);
}
public function init(): void {
add_action('wp_ajax_aksrt_forum_update_topic', [$this, 'ajax_update_topic']);
add_action('wp_ajax_aksrt_forum_topic_list', [$this, 'ajax_topic_list']);
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_purchase_plate_follow', [$this, 'ajax_purchase_plate_follow']);
add_action('wp_ajax_aksrt_forum_unlock_topic', [$this, 'ajax_unlock_topic']);
// §7.4 draft persistence
add_action('wp_ajax_aksrt_forum_save_draft', [$this, 'ajax_save_draft']);
add_action('wp_ajax_aksrt_forum_load_draft', [$this, 'ajax_load_draft']);
add_action('wp_ajax_aksrt_forum_delete_draft', [$this, 'ajax_delete_draft']);
// §7.4 remote search selectors
add_action('wp_ajax_aksrt_forum_search_boards', [$this, 'ajax_search_boards']);
add_action('wp_ajax_nopriv_aksrt_forum_search_boards', [$this, 'ajax_search_boards']);
add_action('wp_ajax_aksrt_forum_search_tags', [$this, 'ajax_search_tags']);
add_action('wp_ajax_nopriv_aksrt_forum_search_tags', [$this, 'ajax_search_tags']);
add_action('wp_ajax_aksrt_forum_search_topics', [$this, 'ajax_search_topics']);
add_action('wp_ajax_nopriv_aksrt_forum_search_topics', [$this, 'ajax_search_topics']);
// §7.4 AJAX-based topic editing
add_action('wp_ajax_aksrt_forum_edit_topic', [$this, 'ajax_edit_topic']);
// §7.6 forum comment engine
add_action('wp_ajax_aksrt_forum_comments', [$this, 'ajax_forum_comments']);
add_action('wp_ajax_nopriv_aksrt_forum_comments', [$this, 'ajax_forum_comments']);
add_action('wp_ajax_aksrt_forum_comment_top', [$this, 'ajax_comment_topping']);
self::register_hide_shortcode();
// §7.10 forum home tab AJAX
add_action('wp_ajax_aksrt_forum_home_tab', [$this, 'ajax_home_tab']);
add_action('wp_ajax_nopriv_aksrt_forum_home_tab', [$this, 'ajax_home_tab']);
// Public plate/board topic list (sort tabs)
add_action('wp_ajax_aksrt_forum_board_topics', [$this, 'ajax_board_topics']);
add_action('wp_ajax_nopriv_aksrt_forum_board_topics', [$this, 'ajax_board_topics']);
add_action('rest_api_init', [$this, 'register_widget_routes']);
add_action('admin_init', [$this, 'maybe_migrate_boards'], 25);
add_filter('template_include', [$this, 'template_routing']);
add_action('wp', [$this, 'maybe_refresh_recommend_on_view']);
add_action('comment_post', [$this, 'on_forum_comment_posted'], 10, 3);
// §7.2 板块多维统计 hooks
add_action('save_post_aksrt_forum_topic', [$this, 'on_topic_saved_sync_plate'], 20, 2);
add_action('comment_post', [$this, 'on_comment_sync_plate_reply'], 25, 3);
add_action('wp_login', [$this, 'cap_cache_on_login'], 10, 2);
add_action('updated_post_meta', [$this, 'cap_cache_on_plate_meta'], 10, 3);
add_action('added_post_meta', [$this, 'cap_cache_on_plate_meta'], 10, 3);
add_action('deleted_post_meta', [$this, 'cap_cache_on_plate_meta'], 10, 3);
add_action('updated_term_meta', [$this, 'cap_cache_on_cat_meta'], 10, 3);
add_action('save_post_aksrt_forum_plate', [$this, 'cap_cache_on_plate_save'], 10, 1);
}
public function on_topic_saved_sync_plate(int $post_id, \WP_Post $post): void {
$this->moderation_service()->on_topic_saved_sync_plate($post_id, $post);
}
public function on_comment_sync_plate_reply(int $comment_id, $approved, array $commentdata): void {
$this->moderation_service()->on_comment_sync_plate_reply($comment_id, $approved, $commentdata);
}
public function template_routing(string $template): string {
return $this->view_builder()->template_routing($template);
}
public function update_topic_meta(int $topic_id, array $meta): bool {
return $this->topic_service()->update_topic_meta($topic_id, $meta);
}
public function is_moderator(int $user_id, int $topic_id): bool {
return $this->moderation_service()->is_moderator($user_id, $topic_id);
}
public function ajax_update_topic(): void {
$this->topic_service()->ajax_update_topic();
}
public function ajax_topic_list(): void {
$this->view_builder()->ajax_topic_list();
}
public function ajax_create_topic(): void {
$this->topic_service()->ajax_create_topic();
}
public function create_topic(int $user_id, array $data) {
return $this->topic_service()->create_topic($user_id, $data);
}
public function moderate_topic(int $topic_id, string $action, array $payload, int $operator_id = 0) {
return $this->topic_service()->moderate_topic($topic_id, $action, $payload, $operator_id);
}
public function submit_vote(int $topic_id, int $user_id, array $option_ids) {
return $this->topic_service()->submit_vote($topic_id, $user_id, $option_ids);
}
public function ajax_follow_plate(): void {
$this->board_service()->ajax_follow_plate();
}
public function ajax_purchase_plate_follow(): void {
$this->board_service()->ajax_purchase_plate_follow();
}
public function ajax_unlock_topic(): void {
$this->topic_service()->ajax_unlock_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);
}
public function review_moderator_application(string $application_id, string $status, string $note, int $reviewer_id) {
return $this->moderation_service()->review_moderator_application($application_id, $status, $note, $reviewer_id);
}
public function get_moderator_application_list(array $args = []): array {
return $this->moderation_service()->get_moderator_application_list($args);
}
public function sync_board_count(int $board_id): void {
$this->moderation_service()->sync_board_count($board_id);
}
public function sync_plate_counts(int $plate_id): void {
$this->moderation_service()->sync_plate_counts($plate_id);
}
public function get_board_moderators(int $board_id): array {
return $this->moderation_service()->get_board_moderators($board_id);
}
public function get_plate_moderators(int $plate_id): array {
return $this->moderation_service()->get_plate_moderators($plate_id);
}
public function is_forum_admin(int $user_id): bool {
return $this->moderation_service()->is_forum_admin($user_id);
}
public function is_board_moderator(int $user_id, int $board_id): bool {
return $this->moderation_service()->is_board_moderator($user_id, $board_id);
}
public function get_board_moderator_profiles(int $board_id): array {
return $this->moderation_service()->get_board_moderator_profiles($board_id);
}
public function get_board_moderator_profiles_with_roles(int $board_id): array {
return $this->moderation_service()->get_board_moderator_profiles_with_roles($board_id);
}
public function get_moderator_roles(int $board_id): array {
return $this->moderation_service()->get_moderator_roles($board_id);
}
public function set_moderator_role(int $user_id, int $board_id, string $role): bool {
return $this->moderation_service()->set_moderator_role($user_id, $board_id, $role);
}
public function add_moderator(int $user_id, int $board_id, string $role = 'moderator'): bool {
return $this->moderation_service()->add_moderator($user_id, $board_id, $role);
}
public function remove_moderator(int $user_id, int $board_id): bool {
return $this->moderation_service()->remove_moderator($user_id, $board_id);
}
public function get_user_moderator_boards(int $user_id): array {
return $this->moderation_service()->get_user_moderator_boards($user_id);
}
public function has_moderator_role(int $user_id, int $board_id, string $min_role = 'moderator'): bool {
return $this->moderation_service()->has_moderator_role($user_id, $board_id, $min_role);
}
public function get_board_moderator_applications(int $board_id): array {
return $this->moderation_service()->get_board_moderator_applications($board_id);
}
public function get_plate_moderator_applications(int $plate_id): array {
return $this->moderation_service()->get_plate_moderator_applications($plate_id);
}
public function get_board_application_for_user(int $user_id, int $board_id): ?array {
return $this->moderation_service()->get_board_application_for_user($user_id, $board_id);
}
public function get_visibility_options(): array {
return $this->topic_service()->get_visibility_options();
}
public function get_review_options(): array {
return $this->topic_service()->get_review_options();
}
public function can_user_publish_directly(int $user_id, int $board_id = 0): bool {
return $this->topic_service()->can_user_publish_directly($user_id, $board_id);
}
public function get_sticky_topics_for_board(int $board_id): array {
return $this->view_builder()->get_sticky_topics_for_board($board_id);
}
public function get_topic_detail(int $topic_id, int $viewer_user_id = 0): array {
return $this->view_builder()->get_topic_detail($topic_id, $viewer_user_id);
}
public function increment_view_count(int $topic_id): void {
$this->view_builder()->increment_view_count($topic_id);
}
public function maybe_migrate_boards(): void {
$this->board_service()->maybe_migrate_boards();
}
public function build_topic_payload(\WP_Post $post, int $viewer_user_id = 0): array {
return $this->topic_service()->build_topic_payload($post, $viewer_user_id);
}
public function unlock_topic_with_credits(int $user_id, int $topic_id): array {
return $this->topic_service()->unlock_topic_with_credits($user_id, $topic_id);
}
public function has_user_unlocked_topic(int $user_id, int $topic_id): bool {
return $this->topic_service()->has_user_unlocked_topic($user_id, $topic_id);
}
public function has_user_paid_topic(int $user_id, int $topic_id): bool {
return $this->topic_service()->has_user_paid_topic($user_id, $topic_id);
}
public function resolve_topic_pay_price(int $topic_id, int $user_id = 0): array {
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);
}
public function save_vote_meta(int $topic_id, array $vote_meta): void {
$this->topic_service()->save_vote_meta($topic_id, $vote_meta);
}
public function normalize_post_status_query(string $status) {
return $this->topic_service()->normalize_post_status_query($status);
}
private function build_user_summary(int $user_id): array {
return $this->topic_service()->build_user_summary($user_id);
}
private function get_user_avatar_url(int $user_id): string {
return $this->topic_service()->get_user_avatar_url($user_id);
}
public function save_cover_meta(int $post_id, array $data): void {
$this->topic_service()->save_cover_meta($post_id, $data);
}
public function split_tags(string $tags): array {
return $this->topic_service()->split_tags($tags);
}
private function get_visibility_map(): array {
return $this->topic_service()->get_visibility_map();
}
private function get_review_map(): array {
return $this->topic_service()->get_review_map();
}
private function get_visibility_label(string $visibility): string {
return $this->topic_service()->get_visibility_label($visibility);
}
private function get_review_label(string $review_status): string {
return $this->topic_service()->get_review_label($review_status);
}
public function is_favorited(int $user_id, int $topic_id): bool {
return $this->score_service()->is_favorited($user_id, $topic_id);
}
public function toggle_favorite(int $user_id, int $topic_id) {
return $this->score_service()->toggle_favorite($user_id, $topic_id);
}
public function score_topic(int $user_id, int $topic_id, int $amount) {
return $this->score_service()->score_topic($user_id, $topic_id, $amount);
}
public function get_score_detail(int $topic_id): array {
return $this->score_service()->get_score_detail($topic_id);
}
public function calculate_recommend_score(int $topic_id): float {
return $this->score_service()->calculate_recommend_score($topic_id);
}
public function refresh_recommend_score(int $topic_id): float {
return $this->score_service()->refresh_recommend_score($topic_id);
}
public function batch_refresh_recommend_scores(int $limit = 100): int {
return $this->score_service()->batch_refresh_recommend_scores($limit);
}
public function maybe_refresh_recommend_on_view(): void {
$this->score_service()->maybe_refresh_recommend_on_view();
}
public function notify(int $user_id, string $type, string $content, array $data = []): void {
$this->score_service()->notify($user_id, $type, $content, $data);
}
public function notify_topic_pending(int $topic_id, int $author_id): void {
$this->score_service()->notify_topic_pending($topic_id, $author_id);
}
public function notify_moderation_result(int $topic_id, string $action, int $operator_id): void {
$this->score_service()->notify_moderation_result($topic_id, $action, $operator_id);
}
public function notify_topic_reply(int $topic_id, int $commenter_id): void {
$this->score_service()->notify_topic_reply($topic_id, $commenter_id);
}
public function notify_topic_scored(int $topic_id, int $scorer_id, int $amount): void {
$this->score_service()->notify_topic_scored($topic_id, $scorer_id, $amount);
}
const COMMENT_TOP_NONE = 0;
const COMMENT_TOP_NORMAL = 1;
const COMMENT_TOP_HOT = 2;
const COMMENT_TOP_BEST = 3;
public function on_forum_comment_posted(int $comment_id, $approved, array $commentdata = []): void {
$this->score_service()->on_forum_comment_posted($comment_id, $approved, $commentdata);
}
public function ajax_forum_comments(): void {
$this->comment_service()->ajax_forum_comments();
}
public function ajax_comment_topping(): void {
$this->comment_service()->ajax_comment_topping();
}
public function render_forum_comments(array $comments, int $topic_id, int $adopted_id = 0): void {
$this->comment_service()->render_forum_comments($comments, $topic_id, $adopted_id);
}
public function adopt_answer(int $topic_id, int $comment_id, int $operator_id) {
return $this->comment_service()->adopt_answer($topic_id, $comment_id, $operator_id);
}
public function get_adopted_comment_id(int $topic_id): int {
return $this->comment_service()->get_adopted_comment_id($topic_id);
}
public function get_forum_comments(int $topic_id, string $sort = 'newest', bool $author_only = false, int $page = 1, int $per_page = 30): array {
return $this->comment_service()->get_forum_comments($topic_id, $sort, $author_only, $page, $per_page);
}
public function set_comment_topping(int $comment_id, int $level): bool {
return $this->comment_service()->set_comment_topping($comment_id, $level);
}
public function maybe_mark_hot_comment(int $comment_id): bool {
return $this->comment_service()->maybe_mark_hot_comment($comment_id);
}
public function is_hot_comment(int $comment_id): bool {
return $this->comment_service()->is_hot_comment($comment_id);
}
public function get_commenter_badge(int $user_id, int $topic_id): ?array {
return $this->comment_service()->get_commenter_badge($user_id, $topic_id);
}
public function save_draft(int $user_id, array $data) {
return $this->draft_service()->save_draft($user_id, $data);
}
public function get_user_draft_count(int $user_id): int {
return $this->draft_service()->get_user_draft_count($user_id);
}
public function get_user_drafts(int $user_id, int $limit = 20): array {
return $this->draft_service()->get_user_drafts($user_id, $limit);
}
public function delete_draft(int $user_id, int $draft_id): bool {
return $this->draft_service()->delete_draft($user_id, $draft_id);
}
public function load_draft_data(int $user_id, int $draft_id): ?array {
return $this->draft_service()->load_draft_data($user_id, $draft_id);
}
public function ajax_save_draft(): void {
$this->run_draft_ajax(static function (self $forum): void {
$forum->draft_service()->ajax_save_draft();
});
}
public function ajax_load_draft(): void {
$this->run_draft_ajax(static function (self $forum): void {
$forum->draft_service()->ajax_load_draft();
});
}
public function ajax_delete_draft(): void {
$this->run_draft_ajax(static function (self $forum): void {
$forum->draft_service()->ajax_delete_draft();
});
}
/**
* Draft AJAX must never leak WP recovery HTML — return JSON on Throwable.
*/
private function run_draft_ajax(callable $callback): void {
try {
$callback($this);
} catch (\Throwable $e) {
if (defined('WP_DEBUG') && WP_DEBUG) {
error_log('[AKSRT forum draft ajax] ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
}
// Prefer JSON even when autoload/require still broken on a partial deploy.
if (!headers_sent()) {
status_header(500);
}
wp_send_json_error([
'message' => sprintf(
/* translators: %s: exception message */
__('草稿操作失败:%s', 'aksrt'),
$e->getMessage()
),
'file' => basename($e->getFile()),
'line' => $e->getLine(),
], 500);
}
}
public function ajax_search_boards(): void {
$this->view_builder()->ajax_search_boards();
}
public function ajax_search_tags(): void {
$this->view_builder()->ajax_search_tags();
}
public function ajax_search_topics(): void {
$this->view_builder()->ajax_search_topics();
}
public function ajax_edit_topic(): void {
$this->topic_service()->ajax_edit_topic();
}
public function get_compose_capabilities(int $user_id): array {
return $this->permission_service()->get_compose_capabilities($user_id);
}
public function sanitize_topic_content(int $user_id, string $content): string {
return $this->topic_service()->sanitize_topic_content($user_id, $content);
}
public static function register_hide_shortcode(): void {
add_shortcode('aksrt_hide', [TopicService::class, 'render_hide_shortcode']);
}
public function publish_draft(int $user_id, int $draft_id, array $data) {
return $this->draft_service()->publish_draft($user_id, $draft_id, $data);
}
//
// 四级层级(对标 zibll):
// TIER_FORUM_ADMIN (4) — bbs_admin_users + manage_options
// TIER_CAT_MOD (3) — aksrt_plate_cat term meta 'moderator'
// TIER_PLATE_AUTHOR(2) — plate CPT post_author(超级版主)
// TIER_MODERATOR (1) — plate CPT post meta '_aksrt_moderators' list
//
// 公开 API:
// forum_user_can( $uid, $cap, $context_id = 0 ): bool
// get_user_plate_tier( $uid, $plate_id ): int
// flush_cap_cache( $uid = 0 ): void
public function get_user_plate_tier(int $user_id, int $plate_id): int {
return $this->permission_service()->get_user_plate_tier($user_id, $plate_id);
}
public function forum_user_can(int $user_id, string $cap, int $context_id = 0): bool {
return $this->permission_service()->forum_user_can($user_id, $cap, $context_id);
}
public function flush_cap_cache(int $user_id = 0): void {
$this->permission_service()->flush_cap_cache($user_id);
}
public function cap_cache_on_login(string $user_login, \WP_User $user): void {
$this->permission_service()->cap_cache_on_login($user_login, $user);
}
public function cap_cache_on_plate_meta($meta_id, $object_id = 0, $meta_key = ''): void {
$this->permission_service()->cap_cache_on_plate_meta($meta_id, $object_id, $meta_key);
}
public function cap_cache_on_cat_meta($meta_id, $object_id = 0, $meta_key = ''): void {
$this->permission_service()->cap_cache_on_cat_meta($meta_id, $object_id, $meta_key);
}
public function cap_cache_on_plate_save($post_id): void {
$this->permission_service()->cap_cache_on_plate_save($post_id);
}
}
-717
View File
@@ -1,717 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class ModerationService {
private Forum $forum;
public function __construct(Forum $forum) {
$this->forum = $forum;
}
public function apply_moderator(int $user_id, int $board_id, string $reason) {
if ($user_id <= 0) {
return new \WP_Error('not_logged_in', __('请先登录', 'aksrt'));
}
if ($this->forum->is_plate_cpt_active()) {
$post = get_post($board_id);
if (!$post || $post->post_type !== 'aksrt_forum_plate') {
return new \WP_Error('invalid_board', __('版块不存在', 'aksrt'));
}
} else {
$board = get_term($board_id, 'aksrt_forum_board');
if (!$board || is_wp_error($board)) {
return new \WP_Error('invalid_board', __('版块不存在', 'aksrt'));
}
}
if ($this->is_board_moderator($user_id, $board_id)) {
return new \WP_Error('already_moderator', __('你已经是该版块版主', 'aksrt'));
}
$applications = $this->get_board_moderator_applications($board_id);
foreach ($applications as $application) {
if ((int) ($application['user_id'] ?? 0) === $user_id && (string) ($application['status'] ?? '') === Forum::APPLICATION_PENDING) {
return new \WP_Error('application_exists', __('你已经提交过申请,等待处理即可', 'aksrt'));
}
}
$reason = trim($reason);
if ($reason === '') {
return new \WP_Error('empty_reason', __('请填写申请理由', 'aksrt'));
}
$application = [
'id' => wp_generate_uuid4(),
'board_id' => $board_id,
'user_id' => $user_id,
'reason' => sanitize_textarea_field($reason),
'status' => Forum::APPLICATION_PENDING,
'created_at' => current_time('mysql'),
'reviewed_at' => '',
'review_note' => '',
'reviewer_id' => 0,
];
$applications[] = $application;
if ($this->forum->is_plate_cpt_active()) {
update_post_meta($board_id, '_aksrt_moderator_applications', $applications);
} else {
update_term_meta($board_id, '_aksrt_moderator_applications', $applications);
}
return [
'message' => __('版主申请已提交', 'aksrt'),
'application' => $application,
];
}
/**
* Review a moderator application.
*
* @return array<string,mixed>|\WP_Error
*/
public function review_moderator_application(string $application_id, string $status, string $note, int $reviewer_id) {
if ($reviewer_id <= 0 || !$this->forum->forum_user_can($reviewer_id, 'moderator_review')) {
return new \WP_Error('forbidden', __('权限不足', 'aksrt'));
}
if (!in_array($status, [Forum::APPLICATION_APPROVED, Forum::APPLICATION_REJECTED], true)) {
return new \WP_Error('invalid_status', __('申请处理状态无效', 'aksrt'));
}
$use_cpt = $this->forum->is_plate_cpt_active();
$boards = $this->forum->get_boards();
$flat_boards = $this->flatten_boards($boards);
foreach ($flat_boards as $board) {
$board_id = (int) ($board['id'] ?? 0);
$applications = $this->get_board_moderator_applications($board_id);
$updated = false;
foreach ($applications as &$application) {
if ((string) ($application['id'] ?? '') !== $application_id) {
continue;
}
$application['status'] = $status;
$application['review_note'] = sanitize_textarea_field($note);
$application['reviewed_at'] = current_time('mysql');
$application['reviewer_id'] = $reviewer_id;
$updated = true;
if ($status === Forum::APPLICATION_APPROVED) {
$this->add_moderator((int) ($application['user_id'] ?? 0), $board_id);
}
break;
}
unset($application);
if ($updated) {
if ($use_cpt) {
update_post_meta($board_id, '_aksrt_moderator_applications', $applications);
} else {
update_term_meta($board_id, '_aksrt_moderator_applications', $applications);
}
$reviewed_application = $this->find_application_by_id($application_id);
if ($reviewed_application) {
$reviewed_user_id = (int) ($reviewed_application['user_id'] ?? 0);
if ($reviewed_user_id > 0 && $reviewed_user_id !== $reviewer_id) {
$board_name = (string) ($board['name'] ?? __('当前版块', 'aksrt'));
$status_label = $status === Forum::APPLICATION_APPROVED
? __('已通过', 'aksrt')
: __('已拒绝', 'aksrt');
$payload = [
'title' => __('版主申请结果', 'aksrt'),
'target_url' => isset($board['url']) ? (string) $board['url'] : aksrt_get_forum_home_url(),
];
$clean_note = sanitize_textarea_field($note);
if ($clean_note !== '') {
$payload['reason'] = $clean_note;
}
$this->forum->notify(
$reviewed_user_id,
'system',
sprintf(__('你在「%1$s」提交的版主申请%2$s', 'aksrt'), $board_name, $status_label),
$payload
);
}
}
return [
'message' => __('申请状态已更新', 'aksrt'),
'application' => $reviewed_application,
];
}
}
return new \WP_Error('application_not_found', __('申请记录不存在', 'aksrt'));
}
/**
* Flatten hierarchical boards array (collapse children into flat list).
*/
private function flatten_boards(array $boards): array {
$flat = [];
foreach ($boards as $board) {
$children = $board['children'] ?? [];
unset($board['children']);
$flat[] = $board;
if (!empty($children)) {
$flat = array_merge($flat, $this->flatten_boards($children));
}
}
return $flat;
}
/**
* Get flattened moderator application list.
*/
public function get_moderator_application_list(array $args = []): array {
$status = sanitize_key((string) ($args['status'] ?? ''));
$board_id = (int) ($args['board_id'] ?? 0);
$keyword = sanitize_text_field((string) ($args['keyword'] ?? ''));
$items = [];
foreach ($this->forum->get_boards() as $board) {
$current_board_id = (int) ($board['id'] ?? 0);
if ($board_id > 0 && $current_board_id !== $board_id) {
continue;
}
foreach ($this->get_board_moderator_applications($current_board_id) as $application) {
if ($status !== '' && (string) ($application['status'] ?? '') !== $status) {
continue;
}
$user = get_userdata((int) ($application['user_id'] ?? 0));
$row = [
'id' => (string) ($application['id'] ?? ''),
'board_id' => $current_board_id,
'board_name' => (string) ($board['name'] ?? ''),
'user_id' => (int) ($application['user_id'] ?? 0),
'display_name' => $user ? $user->display_name : '',
'user_email' => $user ? $user->user_email : '',
'avatar_url' => $user ? $this->forum->get_user_avatar_url((int) $user->ID) : '',
'reason' => (string) ($application['reason'] ?? ''),
'status' => (string) ($application['status'] ?? ''),
'status_label' => $this->get_application_status_label((string) ($application['status'] ?? '')),
'created_at' => (string) ($application['created_at'] ?? ''),
'reviewed_at' => (string) ($application['reviewed_at'] ?? ''),
'review_note' => (string) ($application['review_note'] ?? ''),
'reviewer' => $this->forum->build_user_summary((int) ($application['reviewer_id'] ?? 0)),
];
if ($keyword !== '') {
$haystack = strtolower(implode(' ', [
(string) $row['board_name'],
(string) $row['display_name'],
(string) $row['user_email'],
(string) $row['reason'],
]));
if (strpos($haystack, strtolower($keyword)) === false) {
continue;
}
}
$items[] = $row;
}
}
usort($items, static function ($left, $right): int {
return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? ''));
});
return $items;
}
/**
* Sync board/plate topic count + reply count.
*/
public function sync_board_count(int $board_id): void {
if ($board_id <= 0) {
return;
}
if ($this->forum->is_plate_cpt_active()) {
$this->sync_plate_counts($board_id);
return;
}
$count = new \WP_Query([
'post_type' => 'aksrt_forum_topic',
'posts_per_page' => 1,
'fields' => 'ids',
'tax_query' => [[
'taxonomy' => 'aksrt_forum_board',
'field' => 'term_id',
'terms' => $board_id,
]],
'post_status' => 'publish',
]);
update_term_meta($board_id, '_aksrt_topic_count', (int) $count->found_posts);
}
/**
* Sync plate multi-dimensional counters (§7.2 去规范化统计).
*/
public function sync_plate_counts(int $plate_id): void {
global $wpdb;
$posts_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s
AND p.post_type = 'aksrt_forum_topic' AND p.post_status = 'publish'",
(string) $plate_id
));
$reply_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID
WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s
AND c.comment_approved = '1'",
(string) $plate_id
));
$today_start = current_time('Y-m-d') . ' 00:00:00';
$today_reply_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID
WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s
AND c.comment_approved = '1' AND c.comment_date >= %s",
(string) $plate_id,
$today_start
));
$last_post = $wpdb->get_var($wpdb->prepare(
"SELECT p.post_date FROM {$wpdb->posts} p
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s
AND p.post_type = 'aksrt_forum_topic' AND p.post_status = 'publish'
ORDER BY p.post_date DESC LIMIT 1",
(string) $plate_id
));
$last_reply = $wpdb->get_var($wpdb->prepare(
"SELECT c.comment_date FROM {$wpdb->comments} c
INNER JOIN {$wpdb->postmeta} pm ON pm.post_id = c.comment_post_ID
WHERE pm.meta_key = '_aksrt_plate_id' AND pm.meta_value = %s
AND c.comment_approved = '1'
ORDER BY c.comment_date DESC LIMIT 1",
(string) $plate_id
));
update_post_meta($plate_id, '_aksrt_posts_count', $posts_count);
update_post_meta($plate_id, '_aksrt_reply_count', $reply_count);
update_post_meta($plate_id, '_aksrt_today_reply_count', $today_reply_count);
update_post_meta($plate_id, '_aksrt_last_post', (string) ($last_post ?: ''));
update_post_meta($plate_id, '_aksrt_last_reply', (string) ($last_reply ?: ''));
}
/**
* Get board moderator IDs (统一入口,自动检测 CPT/taxonomy).
*/
public function get_board_moderators(int $board_id): array {
if ($this->forum->is_plate_cpt_active()) {
return $this->get_plate_moderators($board_id);
}
// Fallback: taxonomy
$moderators = get_term_meta($board_id, '_aksrt_moderators', true);
if (!is_array($moderators)) {
return [];
}
return array_values(array_unique(array_filter(array_map('intval', $moderators))));
}
/**
* Get plate moderators from CPT meta.
*/
public function get_plate_moderators(int $plate_id): array {
if (!post_type_exists('aksrt_forum_plate')) {
return [];
}
$moderators = get_post_meta($plate_id, '_aksrt_moderators', true);
if (!is_array($moderators)) {
return [];
}
return array_values(array_unique(array_filter(array_map('intval', $moderators))));
}
/**
* Check if a user is a forum-level admin (bbs_admin_users setting).
*
* Forum admins have cross-board governance privileges and bypass bans.
*/
public function is_forum_admin(int $user_id): bool {
if ($user_id <= 0) {
return false;
}
if (user_can($user_id, 'manage_options')) {
return true;
}
$forum = \AKSRT\Settings\Settings::get_forum_settings();
$admin_users = $forum['admin_users'] ?? [];
return in_array($user_id, $admin_users, true);
}
/**
* Check if a user is a board moderator.
*/
public function is_board_moderator(int $user_id, int $board_id): bool {
if ($user_id <= 0) {
return false;
}
if ($this->is_forum_admin($user_id)) {
return true;
}
return in_array($user_id, $this->get_board_moderators($board_id), true);
}
/**
* Get board moderator profile rows.
*/
public function get_board_moderator_profiles(int $board_id): array {
$profiles = [];
foreach ($this->get_board_moderators($board_id) as $user_id) {
$user = get_userdata($user_id);
if (!$user) {
continue;
}
$profiles[] = [
'id' => $user_id,
'display_name' => $user->display_name,
'user_email' => $user->user_email,
'avatar_url' => $this->forum->get_user_avatar_url($user_id),
];
}
return $profiles;
}
/**
* Get board moderator profiles with role information.
*/
public function get_board_moderator_profiles_with_roles(int $board_id): array {
$roles_map = $this->get_moderator_roles($board_id);
$profiles = [];
foreach ($this->get_board_moderators($board_id) as $user_id) {
$user = get_userdata($user_id);
if (!$user) continue;
$profiles[] = [
'id' => $user_id,
'display_name' => $user->display_name,
'user_email' => $user->user_email,
'avatar_url' => $this->forum->get_user_avatar_url($user_id),
'role' => $roles_map[$user_id] ?? 'moderator',
];
}
return $profiles;
}
/**
* Get moderator role map for a board: [user_id => role].
*
* In CPT mode, also maps plate author → 'plate_author' (超级版主).
*/
public function get_moderator_roles(int $board_id): array {
if ($this->forum->is_plate_cpt_active()) {
$roles = get_post_meta($board_id, '_aksrt_moderator_roles', true);
$roles = is_array($roles) ? $roles : [];
$post = get_post($board_id);
if ($post && $post->post_author > 0) {
$roles[(int) $post->post_author] = 'plate_author';
}
return $roles;
}
$roles = get_term_meta($board_id, '_aksrt_moderator_roles', true);
return is_array($roles) ? $roles : [];
}
/**
* Set a moderator's role on a board.
*/
public function set_moderator_role(int $user_id, int $board_id, string $role): bool {
$allowed = ['super_moderator', 'moderator', 'junior_moderator', 'plate_author', 'cat_moderator'];
if (!in_array($role, $allowed, true)) {
return false;
}
if (!in_array($user_id, $this->get_board_moderators($board_id), true)) {
return false;
}
$roles = $this->get_moderator_roles($board_id);
$roles[$user_id] = $role;
if ($this->forum->is_plate_cpt_active()) {
return (bool) update_post_meta($board_id, '_aksrt_moderator_roles', $roles);
}
return (bool) update_term_meta($board_id, '_aksrt_moderator_roles', $roles);
}
/**
* Add a moderator to a board with optional role.
*/
public function add_moderator(int $user_id, int $board_id, string $role = 'moderator'): bool {
$current = $this->get_board_moderators($board_id);
if (in_array($user_id, $current, true)) {
$this->set_moderator_role($user_id, $board_id, $role);
return true;
}
$current[] = $user_id;
$new_list = array_values(array_unique(array_map('intval', $current)));
if ($this->forum->is_plate_cpt_active()) {
$result = update_post_meta($board_id, '_aksrt_moderators', $new_list);
} else {
$result = update_term_meta($board_id, '_aksrt_moderators', $new_list);
}
if ($result !== false) {
$this->set_moderator_role($user_id, $board_id, $role);
}
return $result !== false;
}
/**
* Remove a moderator from a board.
*/
public function remove_moderator(int $user_id, int $board_id): bool {
$current = $this->get_board_moderators($board_id);
$current = array_values(array_diff($current, [$user_id]));
$roles = $this->get_moderator_roles($board_id);
unset($roles[$user_id]);
if ($this->forum->is_plate_cpt_active()) {
$result = update_post_meta($board_id, '_aksrt_moderators', $current);
update_post_meta($board_id, '_aksrt_moderator_roles', $roles);
} else {
$result = update_term_meta($board_id, '_aksrt_moderators', $current);
update_term_meta($board_id, '_aksrt_moderator_roles', $roles);
}
return $result !== false;
}
/**
* Get all boards a user moderates (with roles).
*/
public function get_user_moderator_boards(int $user_id): array {
$boards = [];
foreach ($this->forum->get_boards() as $board) {
$board_id = (int) ($board['id'] ?? 0);
if ($board_id <= 0) continue;
$moderators = $this->get_board_moderators($board_id);
if (in_array($user_id, $moderators, true)) {
$roles = $this->get_moderator_roles($board_id);
$boards[] = [
'board_id' => $board_id,
'board_name' => (string) ($board['name'] ?? ''),
'role' => $roles[$user_id] ?? 'moderator',
];
}
}
return $boards;
}
/**
* Check if a user has a specific minimum tier on a board/plate.
*
* Tier hierarchy (aligned with zibll 4-level):
* forum_admin(4) > cat_moderator(3) > plate_author(2) > moderator(1)
*
* @param string $min_role 'moderator'|'plate_author'|'cat_moderator'|'forum_admin'
*/
public function has_moderator_role(int $user_id, int $board_id, string $min_role = 'moderator'): bool {
$tier_map = [
'junior_moderator' => Forum::TIER_MODERATOR, // backward-compat alias
'moderator' => Forum::TIER_MODERATOR,
'plate_author' => Forum::TIER_PLATE_AUTHOR,
'super_moderator' => Forum::TIER_PLATE_AUTHOR, // backward-compat alias
'cat_moderator' => Forum::TIER_CAT_MOD,
'forum_admin' => Forum::TIER_FORUM_ADMIN,
];
$required = $tier_map[$min_role] ?? Forum::TIER_MODERATOR;
return $this->forum->get_user_plate_tier($user_id, $board_id) >= $required;
}
/**
* Get board moderator applications (统一入口,自动检测 CPT/taxonomy).
*/
public function get_board_moderator_applications(int $board_id): array {
if ($this->forum->is_plate_cpt_active()) {
return $this->get_plate_moderator_applications($board_id);
}
// Fallback: taxonomy
$applications = get_term_meta($board_id, '_aksrt_moderator_applications', true);
if (!is_array($applications)) {
return [];
}
return array_values(array_filter(array_map(function ($application) use ($board_id): array {
if (!is_array($application)) {
$application = [];
}
return [
'id' => (string) ($application['id'] ?? wp_generate_uuid4()),
'board_id' => $board_id,
'user_id' => (int) ($application['user_id'] ?? 0),
'reason' => (string) ($application['reason'] ?? ''),
'status' => (string) ($application['status'] ?? Forum::APPLICATION_PENDING),
'created_at' => (string) ($application['created_at'] ?? ''),
'reviewed_at' => (string) ($application['reviewed_at'] ?? ''),
'review_note' => (string) ($application['review_note'] ?? ''),
'reviewer_id' => (int) ($application['reviewer_id'] ?? 0),
];
}, $applications)));
}
/**
* Get plate moderator applications from CPT meta.
*/
public function get_plate_moderator_applications(int $plate_id): array {
if (!post_type_exists('aksrt_forum_plate')) {
return [];
}
$applications = get_post_meta($plate_id, '_aksrt_moderator_applications', true);
if (!is_array($applications)) {
return [];
}
return array_values(array_filter(array_map(function ($application) use ($plate_id): array {
if (!is_array($application)) {
$application = [];
}
return [
'id' => (string) ($application['id'] ?? wp_generate_uuid4()),
'board_id' => $plate_id,
'user_id' => (int) ($application['user_id'] ?? 0),
'reason' => (string) ($application['reason'] ?? ''),
'status' => (string) ($application['status'] ?? Forum::APPLICATION_PENDING),
'created_at' => (string) ($application['created_at'] ?? ''),
'reviewed_at' => (string) ($application['reviewed_at'] ?? ''),
'review_note' => (string) ($application['review_note'] ?? ''),
'reviewer_id' => (int) ($application['reviewer_id'] ?? 0),
];
}, $applications)));
}
/**
* Get current user's board application.
*/
public function get_board_application_for_user(int $user_id, int $board_id): ?array {
$applications = $this->get_board_moderator_applications($board_id);
$matched = array_values(array_filter($applications, static function ($application) use ($user_id): bool {
return (int) ($application['user_id'] ?? 0) === $user_id;
}));
if (empty($matched)) {
return null;
}
usort($matched, static function ($left, $right): int {
return strcmp((string) ($right['created_at'] ?? ''), (string) ($left['created_at'] ?? ''));
});
$application = $matched[0];
$application['status_label'] = $this->get_application_status_label((string) ($application['status'] ?? ''));
return $application;
}
public function find_application_by_id(string $application_id): ?array {
foreach ($this->get_moderator_application_list() as $application) {
if ((string) ($application['id'] ?? '') === $application_id) {
return $application;
}
}
return null;
}
public function get_application_status_label(string $status): string {
$map = [
Forum::APPLICATION_PENDING => __('待处理', 'aksrt'),
Forum::APPLICATION_APPROVED => __('已通过', 'aksrt'),
Forum::APPLICATION_REJECTED => __('已拒绝', 'aksrt'),
];
return $map[$status] ?? $map[Forum::APPLICATION_PENDING];
}
public function on_topic_saved_sync_plate(int $post_id, \WP_Post $post): void {
if (!$this->forum->is_plate_cpt_active()) {
return;
}
$plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true);
if ($plate_id > 0) {
$this->sync_plate_counts($plate_id);
}
}
public function on_comment_sync_plate_reply(int $comment_id, $approved, array $commentdata): void {
if (!$this->forum->is_plate_cpt_active()) {
return;
}
$post_id = (int) ($commentdata['comment_post_ID'] ?? 0);
if ($post_id <= 0) {
return;
}
$post = get_post($post_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return;
}
$plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true);
if ($plate_id > 0) {
$this->sync_plate_counts($plate_id);
}
}
public function is_moderator(int $user_id, int $topic_id): bool {
if ($user_id <= 0) {
return false;
}
if ($this->is_forum_admin($user_id)) {
return true;
}
// CPT mode: resolve plate from topic meta
if ($this->forum->is_plate_cpt_active()) {
$plate_id = $this->forum->get_topic_plate_id($topic_id);
if ($plate_id > 0) {
return $this->forum->get_user_plate_tier($user_id, $plate_id) >= self::TIER_MODERATOR;
}
}
// Fallback: taxonomy-based boards
$boards = get_the_terms($topic_id, 'aksrt_forum_board');
if ($boards && !is_wp_error($boards)) {
foreach ($boards as $board) {
if ($this->is_board_moderator($user_id, (int) $board->term_id)) {
return true;
}
}
}
return false;
}
}
-420
View File
@@ -1,420 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class PermissionService {
// ── Tier constants (mirrored from Forum) ───────────────
const TIER_NONE = 0;
const TIER_MODERATOR = 1;
const TIER_PLATE_AUTHOR = 2;
const TIER_CAT_MOD = 3;
const TIER_FORUM_ADMIN = 4;
/** @var array<string,bool> cap_key => bool */
private static array $cap_cache = [];
/** @var array<string,int> "uid:plate_id" => tier */
private static array $tier_cache = [];
/** @var Forum */
private $forum;
public function __construct($forum) {
$this->forum = $forum;
}
// ── Capability matrix ─────────────────────────────────────────
/**
* Capability definitions matrix.
*
* Keys:
* 'min_tier' — minimum TIER_* constant required
* 'context' — 'topic'|'plate'|'global'|'any'
* 'self' — true = only applies to own content
* 'logged' — shorthand: any logged-in user
* 'settings_key' — optional forum settings permissions key
*/
private function cap_defs(): array {
return [
// ── Topic viewing ─────────────────────────────────────────────
'topic_view_pending' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_view_hidden' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_view_deleted' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'],
// ── Topic editing ─────────────────────────────────────────────
'topic_edit' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'],
'topic_edit_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_delete' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'],
'topic_delete_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
// ── Topic governance ──────────────────────────────────────────
'topic_sticky_plate' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_sticky_super' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate'],
'topic_sticky_global' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'],
'topic_featured' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_lock' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'],
'topic_lock_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_highlight' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_score_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_move' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'topic_moderate' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
// ── Topic interaction (logged users) ──────────────────────────
'topic_vote' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'topic_favorite' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'topic_score' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'topic_report' => ['min_tier' => self::TIER_NONE, 'logged' => true],
// ── Reply governance ──────────────────────────────────────────
'reply_adopt' => ['min_tier' => self::TIER_NONE, 'self' => true, 'context' => 'topic'],
'reply_adopt_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'reply_delete_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'reply_edit_other' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'reply_top' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
// ── Reply interaction ─────────────────────────────────────────
'reply_like' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'reply_score' => ['min_tier' => self::TIER_NONE, 'logged' => true],
// ── Plate management ──────────────────────────────────────────
'plate_follow' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'plate_create' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'],
'plate_manage' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate'],
'plate_delete' => ['min_tier' => self::TIER_FORUM_ADMIN, 'context' => 'global'],
// ── Moderator system ──────────────────────────────────────────
'moderator_manage' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'plate'],
'moderator_apply' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'moderator_review' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'plate'],
// ── User governance ───────────────────────────────────────────
'user_ban' => ['min_tier' => self::TIER_CAT_MOD, 'context' => 'global'],
'user_report' => ['min_tier' => self::TIER_NONE, 'logged' => true],
// ── Compose capabilities ──────────────────────────────────────
'compose_post' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => ''],
'compose_post_no_review' => ['min_tier' => self::TIER_MODERATOR, 'context' => 'plate'],
'compose_upload_image' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_img'],
'compose_upload_video' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_video'],
'compose_upload_file' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_file'],
'compose_iframe_video' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_iframe_video'],
'compose_set_type' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'compose_set_visibility' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'compose_set_cover' => ['min_tier' => self::TIER_NONE, 'logged' => true, 'settings_key' => 'posts_upload_img'],
'compose_set_vote' => ['min_tier' => self::TIER_NONE, 'logged' => true],
'compose_set_paid' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate', 'settings_key' => 'paid_content'],
'compose_set_hide' => ['min_tier' => self::TIER_PLATE_AUTHOR, 'context' => 'plate', 'settings_key' => 'paid_content'],
];
}
// ── Tier resolution ───────────────────────────────────────────
/**
* Return the user's highest tier for a given plate.
*
* Result is cached per request on "$user_id:$plate_id".
*/
public function get_user_plate_tier(int $user_id, int $plate_id): int {
if ($user_id <= 0) {
return self::TIER_NONE;
}
$cache_key = "$user_id:$plate_id";
if (isset(self::$tier_cache[$cache_key])) {
return self::$tier_cache[$cache_key];
}
$tier = $this->compute_user_plate_tier($user_id, $plate_id);
self::$tier_cache[$cache_key] = $tier;
return $tier;
}
private function compute_user_plate_tier(int $user_id, int $plate_id): int {
// Tier 4: WP admin or bbs_admin_users list
if ($this->forum->is_forum_admin($user_id)) {
return self::TIER_FORUM_ADMIN;
}
// Tier 3: category moderator (plate_cat term meta 'moderator')
$cats = get_the_terms($plate_id, 'aksrt_plate_cat');
if ($cats && !is_wp_error($cats)) {
foreach ($cats as $cat) {
if ($this->forum->is_cat_moderator($user_id, (int) $cat->term_id)) {
return self::TIER_CAT_MOD;
}
}
}
// Tier 2: plate_author (plate CPT post_author)
if ($this->forum->is_plate_cpt_active()) {
$post = get_post($plate_id);
if ($post && (int) $post->post_author === $user_id) {
return self::TIER_PLATE_AUTHOR;
}
// Tier 1: in plate _aksrt_moderators list
$mods = get_post_meta($plate_id, '_aksrt_moderators', true);
if (is_array($mods) && in_array($user_id, $mods, true)) {
return self::TIER_MODERATOR;
}
return self::TIER_NONE;
}
// Fallback: taxonomy board
$roles = get_term_meta($plate_id, '_aksrt_moderator_roles', true);
$roles = is_array($roles) ? $roles : [];
$role = $roles[$user_id] ?? '';
if ($role === 'super_moderator' || $role === 'plate_author') {
return self::TIER_PLATE_AUTHOR;
}
$mods = get_term_meta($plate_id, '_aksrt_moderators', true);
if (is_array($mods) && in_array($user_id, $mods, true)) {
return self::TIER_MODERATOR;
}
return self::TIER_NONE;
}
// ── Capability check ──────────────────────────────────────────
/**
* Unified capability resolver.
*
* @param int $user_id User to check (0 = guest).
* @param string $cap Capability id from cap_defs().
* @param int $context_id topic_id for 'topic' context, plate_id for 'plate' context.
* @return bool
*/
public function forum_user_can(int $user_id, string $cap, int $context_id = 0): bool {
$cache_key = "$user_id:$cap:$context_id";
if (array_key_exists($cache_key, self::$cap_cache)) {
return self::$cap_cache[$cache_key];
}
$result = $this->compute_forum_cap($user_id, $cap, $context_id);
self::$cap_cache[$cache_key] = $result;
return $result;
}
private function compute_forum_cap(int $user_id, string $cap, int $context_id): bool {
$defs = $this->cap_defs();
if (!isset($defs[$cap])) {
return false;
}
$def = $defs[$cap];
$logged_only = !empty($def['logged']) && ($def['min_tier'] ?? self::TIER_NONE) === self::TIER_NONE;
$self_only = !empty($def['self']);
$context = $def['context'] ?? 'any';
$min_tier = (int) ($def['min_tier'] ?? self::TIER_NONE);
$settings_key = $def['settings_key'] ?? '';
// Guest short-circuit
if ($user_id <= 0) {
return false;
}
// Forum-wide admin bypasses everything
if ($this->forum->is_forum_admin($user_id)) {
// Honour settings-gated compose caps even for admins when the feature is globally off
if ($settings_key !== '') {
$perms = $this->forum->get_config()['permissions'] ?? [];
if (isset($perms[$settings_key]) && $perms[$settings_key] === false) {
return false;
}
}
return true;
}
// 'logged' / any-user caps: just need to be logged in
if ($logged_only || ($min_tier === self::TIER_NONE && $context === 'any')) {
if ($settings_key !== '') {
$perms = $this->forum->get_config()['permissions'] ?? [];
if (array_key_exists($settings_key, $perms) && empty($perms[$settings_key])) {
return false;
}
}
return true;
}
// 'self' caps: caller passes topic_id as context; check authorship
if ($self_only) {
if ($context_id <= 0) {
return false;
}
$post = get_post($context_id);
return $post && (int) $post->post_author === $user_id;
}
// Global caps (no specific plate/topic context)
if ($context === 'global') {
if ($min_tier <= self::TIER_CAT_MOD) {
return $this->is_cat_moderator_anywhere($user_id);
}
return false;
}
// Plate-context caps
if ($context === 'plate') {
$plate_id = $this->resolve_plate_id($context_id, $cap);
if ($plate_id <= 0) {
return false;
}
$tier = $this->get_user_plate_tier($user_id, $plate_id);
// Check optional settings gate
if ($settings_key !== '') {
$perms = $this->forum->get_config()['permissions'] ?? [];
if (array_key_exists($settings_key, $perms) && empty($perms[$settings_key])) {
return $tier >= self::TIER_PLATE_AUTHOR;
}
}
return $tier >= $min_tier;
}
return false;
}
// ── Helper methods ────────────────────────────────────────────
/**
* Resolve a plate_id from either a plate_id or topic_id, depending on the cap context.
*/
private function resolve_plate_id(int $context_id, string $cap): int {
if ($context_id <= 0) {
return 0;
}
$post = get_post($context_id);
if (!$post) {
return 0;
}
if ($post->post_type === 'aksrt_forum_plate') {
return $context_id;
}
if ($post->post_type === 'aksrt_forum_topic') {
return $this->forum->get_topic_plate_id($context_id);
}
return 0;
}
/**
* Check if the user is a cat_moderator on ANY plate category (for global caps).
*/
private function is_cat_moderator_anywhere(int $user_id): bool {
if ($user_id <= 0) {
return false;
}
if ($this->forum->is_forum_admin($user_id)) {
return true;
}
$cats = get_terms(['taxonomy' => 'aksrt_plate_cat', 'hide_empty' => false, 'fields' => 'ids']);
if (is_wp_error($cats) || empty($cats)) {
return false;
}
foreach ((array) $cats as $cat_id) {
if ($this->forum->is_cat_moderator($user_id, (int) $cat_id)) {
return true;
}
}
return false;
}
// ── Compose capabilities ──────────────────────────────────────
/**
* Get per-feature capabilities for the compose/edit form.
*
* @return array<string,bool>
*/
public function get_compose_capabilities(int $user_id): array {
$keys = [
'can_post', 'can_set_type', 'can_set_visibility',
'can_set_cover', 'can_set_cover_image', 'can_set_cover_slide', 'can_set_cover_video',
'can_set_vote', 'can_set_paid', 'can_set_hide',
'can_upload_image', 'can_upload_video', 'can_upload_file',
'can_iframe_video',
];
if ($user_id <= 0) {
return array_fill_keys($keys, false);
}
return [
'can_post' => $this->forum_user_can($user_id, 'compose_post'),
'can_set_type' => $this->forum_user_can($user_id, 'compose_set_type'),
'can_set_visibility' => $this->forum_user_can($user_id, 'compose_set_visibility'),
'can_set_cover' => $this->forum_user_can($user_id, 'compose_set_cover'),
'can_set_cover_image' => $this->forum_user_can($user_id, 'compose_set_cover'),
'can_set_cover_slide' => $this->forum_user_can($user_id, 'compose_set_cover'),
'can_set_cover_video' => $this->forum_user_can($user_id, 'compose_upload_video'),
'can_set_vote' => $this->forum_user_can($user_id, 'compose_set_vote'),
'can_set_paid' => $this->forum_user_can($user_id, 'compose_set_paid'),
'can_set_hide' => $this->forum_user_can($user_id, 'compose_set_hide'),
'can_upload_image' => $this->forum_user_can($user_id, 'compose_upload_image'),
'can_upload_video' => $this->forum_user_can($user_id, 'compose_upload_video'),
'can_upload_file' => $this->forum_user_can($user_id, 'compose_upload_file'),
'can_iframe_video' => $this->forum_user_can($user_id, 'compose_iframe_video'),
];
}
// ── Cache invalidation ────────────────────────────────────────
public function flush_cap_cache(int $user_id = 0): void {
if ($user_id > 0) {
foreach (array_keys(self::$cap_cache) as $k) {
if (strncmp($k, "$user_id:", strlen("$user_id:")) === 0) {
unset(self::$cap_cache[$k]);
}
}
foreach (array_keys(self::$tier_cache) as $k) {
if (strncmp($k, "$user_id:", strlen("$user_id:")) === 0) {
unset(self::$tier_cache[$k]);
}
}
} else {
self::$cap_cache = [];
self::$tier_cache = [];
}
}
public function cap_cache_on_login(string $user_login, \WP_User $user): void {
$this->flush_cap_cache((int) $user->ID);
}
/** Flush cache when plate moderator meta changes. */
public function cap_cache_on_plate_meta($meta_id, $object_id = 0, $meta_key = ''): void {
// deleted_post_meta passes $meta_ids as array; other hooks pass int meta_id.
if (is_array($meta_key) || (is_array($meta_id) && !is_string($meta_key))) {
// Still accept call; $meta_key should be string on WP hooks.
}
$meta_key = is_string($meta_key) ? $meta_key : '';
if (in_array($meta_key, ['_aksrt_moderators', '_aksrt_moderator_roles'], true)) {
self::$cap_cache = [];
self::$tier_cache = [];
}
}
/** Flush cache when plate_cat moderator term meta changes. */
public function cap_cache_on_cat_meta($meta_id, $object_id = 0, $meta_key = ''): void {
$meta_key = is_string($meta_key) ? $meta_key : '';
if ($meta_key === 'moderator') {
self::$cap_cache = [];
self::$tier_cache = [];
}
}
/** Flush cache when a plate CPT post is saved (plate_author may change). */
public function cap_cache_on_plate_save($post_id): void {
$post_id = (int) $post_id;
foreach (array_keys(self::$tier_cache) as $k) {
if (substr($k, -strlen(":$post_id")) === ":$post_id") {
unset(self::$tier_cache[$k]);
}
}
self::$cap_cache = [];
}
}
-107
View File
@@ -1,107 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class PlateAdminList {
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 {
if (!is_admin()) {
return;
}
add_filter('manage_edit-aksrt_forum_plate_columns', [$this, 'columns']);
add_action('manage_aksrt_forum_plate_posts_custom_column', [$this, 'render_column'], 10, 2);
add_filter('manage_edit-aksrt_forum_plate_sortable_columns', [$this, 'sortable_columns']);
add_action('pre_get_posts', [$this, 'apply_orderby']);
}
public function columns(array $columns): array {
$new = [];
foreach ($columns as $key => $label) {
$new[$key] = $label;
if ($key === 'title') {
$new['aksrt_plate_mods'] = __('版主数', 'aksrt');
$new['aksrt_plate_posts'] = __('帖子数', 'aksrt');
$new['aksrt_plate_reply'] = __('回复数', 'aksrt');
$new['aksrt_plate_views'] = __('浏览', 'aksrt');
$new['aksrt_plate_follow'] = __('关注', 'aksrt');
$new['aksrt_plate_pending_apps'] = __('待审申请', 'aksrt');
}
}
return $new;
}
public function sortable_columns(array $columns): array {
$columns['aksrt_plate_posts'] = 'aksrt_posts_count';
$columns['aksrt_plate_views'] = 'aksrt_plate_views';
return $columns;
}
public function render_column(string $column, int $post_id): void {
switch ($column) {
case 'aksrt_plate_mods':
$mods = get_post_meta($post_id, '_aksrt_moderators', true);
$count = is_array($mods) ? count(array_filter(array_map('intval', $mods))) : 0;
echo esc_html((string) $count);
break;
case 'aksrt_plate_posts':
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_posts_count', true));
break;
case 'aksrt_plate_reply':
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_reply_count', true));
break;
case 'aksrt_plate_views':
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_plate_views', true));
break;
case 'aksrt_plate_follow':
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_follow_count', true));
break;
case 'aksrt_plate_pending_apps':
$apps = get_post_meta($post_id, '_aksrt_moderator_applications', true);
$pending = 0;
if (is_array($apps)) {
foreach ($apps as $app) {
if (is_array($app) && (($app['status'] ?? '') === 'pending')) {
$pending++;
}
}
}
if ($pending > 0) {
echo '<strong style="color:#b32d2e;">' . esc_html((string) $pending) . '</strong>';
} else {
echo '0';
}
break;
}
}
public function apply_orderby(\WP_Query $query): void {
if (!is_admin() || !$query->is_main_query()) {
return;
}
if ($query->get('post_type') !== 'aksrt_forum_plate') {
return;
}
$orderby = $query->get('orderby');
if ($orderby === 'aksrt_posts_count') {
$query->set('meta_key', '_aksrt_posts_count');
$query->set('orderby', 'meta_value_num');
} elseif ($orderby === 'aksrt_plate_views') {
$query->set('meta_key', '_aksrt_plate_views');
$query->set('orderby', 'meta_value_num');
}
}
}
-852
View File
@@ -1,852 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class PlateFollowPay {
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/plate-follow/(?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'],
],
]);
}
/**
* @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) {
$plate_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($plate_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) {
$plate_id = (int) $request['id'];
$uid = get_current_user_id();
$post = get_post($plate_id);
$allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post);
if ($uid <= 0 && !$allow_guest) {
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';
}
if ($uid <= 0 && in_array($payment_method, ['balance', 'points'], true)) {
$payment_method = 'alipay';
}
$coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? $params['coupon'] ?? ''));
return $this->rest_wrap_result(
$this->create_cash_order($plate_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);
}
/**
* @return array|\WP_Error
*/
public function preview(int $plate_id, int $user_id = 0, string $coupon_code = '') {
$assert = $this->assert_plate($plate_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();
$coupon_code = sanitize_text_field($coupon_code);
$allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post);
if ($uid <= 0 && !$allow_guest) {
return [
'post_id' => $plate_id,
'title' => get_the_title($post),
'permalink' => get_permalink($post),
'pay_modo' => '0',
'pay_type' => 'plate_follow',
'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($plate_id, $uid);
if (($priced['modo'] ?? 'money') === 'points') {
return new \WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]);
}
$list_amount = round((float) ($priced['amount'] ?? 0), 2);
$base = round((float) ($priced['base'] ?? $list_amount), 2);
$vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price']));
if ($uid <= 0) {
$vip_free = false;
}
$has_followed = $this->user_has_followed($uid, $plate_id);
$wallet_balance = 0.0;
if ($uid > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled()) {
$wallet_balance = (float) \AKSRT\Credits\Balance::balance($uid);
}
$methods = function_exists('aksrt_list_cash_payment_methods')
? aksrt_list_cash_payment_methods($uid > 0)
: [];
$coupon_allowed = $this->is_coupon_allowed($plate_id);
$coupon_error = '';
$coupon_discount = 0.0;
$coupon_meta = null;
$applied_code = '';
$payable = $list_amount;
if (!$vip_free && $list_amount > 0) {
$discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $uid, $coupon_code, true);
if (is_wp_error($discounted)) {
$coupon_error = $discounted->get_error_message();
$discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $uid, '', true);
}
if (!is_wp_error($discounted)) {
$list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2);
$payable = round((float) ($discounted['payable'] ?? $list_amount), 2);
$coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2);
$coupon_meta = !empty($discounted['coupon']) ? $discounted['coupon'] : null;
$applied_code = (string) ($discounted['coupon_code'] ?? ($coupon_meta['code'] ?? ''));
if ($coupon_error === '' && !empty($discounted['coupon_error'])) {
$coupon_error = (string) $discounted['coupon_error'];
}
}
} elseif ($vip_free) {
$payable = 0.0;
$list_amount = 0.0;
}
$is_free = $vip_free || $payable <= 0;
$amount_label = $is_free
? __('免费', 'aksrt')
: sprintf('¥%s', number_format($payable, 2, '.', ''));
$list_label = sprintf('¥%s', number_format($list_amount > 0 ? $list_amount : $base, 2, '.', ''));
$base_label = sprintf('¥%s', number_format($base, 2, '.', ''));
return [
'post_id' => $plate_id,
'title' => get_the_title($post),
'permalink' => get_permalink($post),
'pay_modo' => '0',
'pay_type' => 'plate_follow',
'pay_type_label' => __('付费关注', 'aksrt'),
'amount' => $payable,
'base' => $base,
'list_amount' => $list_amount > 0 ? $list_amount : $base,
'list_amount_label' => $list_label,
'payable' => $payable,
'currency' => '¥',
'amount_label' => $amount_label,
'base_label' => $base_label,
'is_free' => $is_free,
'has_paid_access' => $has_followed,
'paid_order' => null,
'pending_order' => null,
'need_login' => $uid <= 0 && !$allow_guest,
'allow_guest' => $allow_guest,
'need_vip' => false,
'need_purchase' => !$has_followed && !$is_free,
'logged_in' => $uid > 0,
'balance' => $wallet_balance,
'wallet_balance' => $wallet_balance,
'balance_enough' => $is_free || ($wallet_balance + 0.0001 >= (float) $payable),
'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_allowed ? __('支持优惠码', 'aksrt') : '',
'coupon_code' => $applied_code,
'coupon' => $coupon_meta,
'coupon_error' => $coupon_error,
'coupon_discount' => $coupon_discount,
'coupon_discount_label' => $coupon_discount > 0
? ('-' . sprintf('¥%s', number_format($coupon_discount, 2, '.', '')))
: '',
'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']),
];
}
/**
* @return array|\WP_Error
*/
public function create_cash_order(int $plate_id, int $user_id, string $payment_method = 'balance', string $coupon_code = '') {
$assert = $this->assert_plate($plate_id);
if (is_wp_error($assert)) {
return $assert;
}
/** @var \WP_Post $post */
$post = $assert['post'];
$allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post);
if ($user_id <= 0 && !$allow_guest) {
return new \WP_Error('aksrt_need_login', __('请先登录后再关注。', 'aksrt'), [
'status' => 401,
'login_url' => wp_login_url(get_permalink($post)),
]);
}
if ($this->user_has_followed($user_id, $plate_id)) {
return new \WP_Error('aksrt_already_followed', __('已关注该版块。', 'aksrt'), [
'status' => 400,
'reload' => true,
]);
}
$priced = $this->resolve_price($plate_id, $user_id);
if (($priced['modo'] ?? 'money') === 'points') {
return new \WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]);
}
$list_amount = round((float) ($priced['amount'] ?? 0), 2);
$base = round((float) ($priced['base'] ?? $list_amount), 2);
$vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price']));
if ($user_id <= 0) {
$vip_free = false;
}
$coupon_code = sanitize_text_field($coupon_code);
// VIP free — no coupon stack. Guests never hit this.
if ($vip_free) {
return $this->create_free_paid_order($plate_id, $user_id, $post, $priced, [
'list_amount' => 0.0,
'payable' => 0.0,
'coupon_code' => '',
'coupon_discount' => 0.0,
'coupon' => [],
'message' => __('VIP 免费关注成功', 'aksrt'),
'paid_note' => __('VIP 免费关注版块。', 'aksrt'),
]);
}
if ($list_amount <= 0) {
return new \WP_Error('aksrt_price_unset', __('价格未设置', 'aksrt'), ['status' => 400]);
}
$discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $user_id, $coupon_code, false);
if (is_wp_error($discounted)) {
return $discounted;
}
$list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2);
$amount = round((float) ($discounted['payable'] ?? $list_amount), 2);
$coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2);
$coupon_meta = is_array($discounted['coupon'] ?? null) ? $discounted['coupon'] : [];
$coupon_code_norm = (string) ($coupon_meta['code'] ?? $discounted['coupon_code'] ?? $coupon_code);
// Free after coupon.
if ($amount <= 0) {
return $this->create_free_paid_order($plate_id, $user_id, $post, $priced, [
'list_amount' => $list_amount,
'payable' => 0.0,
'coupon_code' => $coupon_code_norm,
'coupon_discount' => $coupon_discount,
'coupon' => $coupon_meta,
'message' => __('无需支付,已关注。', 'aksrt'),
'paid_note' => __('优惠后无需支付。', 'aksrt'),
]);
}
if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) {
return new \WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
}
$payment_method = sanitize_key($payment_method);
if ($payment_method === '') {
$payment_method = $user_id > 0 ? 'balance' : 'alipay';
}
if ($user_id <= 0 && in_array($payment_method, ['balance', 'points'], true)) {
$payment_method = 'alipay';
}
$pending = $this->find_pending_order($user_id, $plate_id);
$can_resume = false;
if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) {
$pending_meta = $this->decode_order_note_meta($pending);
$pending_coupon = (string) ($pending_meta['coupon_code'] ?? '');
if (is_array($pending_meta['coupon'] ?? null)) {
$pending_coupon = (string) ($pending_meta['coupon']['code'] ?? $pending_coupon);
}
if ($pending_coupon === $coupon_code_norm) {
$can_resume = true;
}
}
if ($can_resume) {
$order_number = (string) ($pending['order_number'] ?? '');
$order = $pending;
} else {
$note = wp_json_encode([
'plate_follow_pay' => 1,
'plate_id' => $plate_id,
'pay_modo' => '0',
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => $base,
'list_amount' => $list_amount,
'amount' => $amount,
'payable' => $amount,
'coupon_code' => $coupon_code_norm,
'coupon_discount' => $coupon_discount,
'coupon' => $coupon_meta,
'guest' => $user_id <= 0 ? 1 : 0,
'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\Payment\OrderRepository::get_instance()->create([
'user_id' => max(0, $user_id),
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW,
'product_id' => $plate_id,
'amount' => $amount,
'currency' => 'CNY',
'status' => \AKSRT\Payment\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\Payment\OrderRepository::get_instance()->get($order_number);
if (!$order) {
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败。', 'aksrt'), ['status' => 500]);
}
}
$order['subject'] = sprintf(
/* translators: %s: plate 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]);
}
$resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method);
if (empty($resolved['gateway'])) {
return new \WP_Error(
(string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'),
(string) ($resolved['message'] ?? __('当前支付方式不可用。', 'aksrt')),
['status' => 400, 'order_number' => $order_number]
);
}
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [
'paid_note' => __('用户完成版块付费关注支付。', 'aksrt'),
]);
if (empty($payment_result['success'])) {
return new \WP_Error(
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
(string) ($payment_result['message'] ?? __('支付失败。', 'aksrt')),
['status' => 402, 'order_number' => $order_number]
);
}
if (!empty($payment_result['redirect'])) {
return [
'success' => true,
'data' => [
'redirect' => true,
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
'order_number' => $order_number,
'amount' => $amount,
'pay_modo' => '0',
'message' => $payment_result['message'],
'reload' => false,
'poll' => true,
],
];
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => $amount,
'pay_modo' => '0',
'message' => $payment_result['message'],
'reload' => true,
],
];
}
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\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW) {
return;
}
$plate_id = (int) ($paid_order['product_id'] ?? 0);
$user_id = (int) ($paid_order['user_id'] ?? 0);
if ($plate_id <= 0) {
return;
}
$meta = $this->decode_order_note_meta($paid_order);
if (empty($meta['plate_follow_pay'])) {
$post = get_post($plate_id);
if (!($post instanceof \WP_Post) || $post->post_type !== 'aksrt_forum_plate') {
return;
}
}
$fulfilled = $user_id <= 0;
if ($user_id > 0) {
$this->mark_followed($user_id, $plate_id);
$fulfilled = class_exists('AKSRT\\Forum\\Forum')
? Forum::get_instance()->is_user_following_plate($user_id, $plate_id)
: in_array($plate_id, array_map('intval', (array) get_user_meta($user_id, 'aksrt_follow_plate', true)), true);
} elseif (function_exists('aksrt_set_guest_pay_cookies')) {
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$full = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (is_array($full)) {
$paid_order = array_merge($full, $paid_order);
}
}
aksrt_set_guest_pay_cookies($paid_order);
}
$this->finalize_plate_order_side_effects($order_number, $paid_order);
if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) {
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
$order_number,
\AKSRT\Payment\Order::STATUS_COMPLETED,
['note' => __('付费关注权益已发放。', 'aksrt')]
);
}
}
/**
* @return array{post:\WP_Post}|\WP_Error
*/
private function assert_plate(int $plate_id) {
$post = get_post($plate_id);
if (!($post instanceof \WP_Post) || $post->post_type !== 'aksrt_forum_plate') {
return new \WP_Error('aksrt_invalid', __('版块不存在', 'aksrt'), ['status' => 404]);
}
if ($post->post_status !== 'publish') {
return new \WP_Error('aksrt_invalid', __('版块不可关注', 'aksrt'), ['status' => 404]);
}
if (!$this->is_follow_pay_enabled($plate_id)) {
return new \WP_Error('aksrt_not_paid', __('该版块未开启付费关注', 'aksrt'), ['status' => 400]);
}
$modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money'));
if ($modo === 'points') {
return new \WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]);
}
return ['post' => $post];
}
private function is_follow_pay_enabled(int $plate_id): bool {
if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'is_plate_follow_pay_enabled')) {
return (bool) Forum::get_instance()->is_plate_follow_pay_enabled($plate_id);
}
return (int) get_post_meta($plate_id, '_aksrt_follow_pay', true) === 1;
}
/**
* @return array{amount:float,modo:string,is_vip_price:bool,is_free:bool,label:string,base:float,vip_level:int}
*/
private function resolve_price(int $plate_id, int $user_id): array {
if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'resolve_plate_follow_pay_price')) {
return Forum::get_instance()->resolve_plate_follow_pay_price($plate_id, $user_id);
}
return [
'amount' => max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true)),
'modo' => 'money',
'is_vip_price' => false,
'is_free' => false,
'label' => '',
'base' => max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true)),
'vip_level' => 0,
];
}
private function user_has_followed(int $user_id, int $plate_id): bool {
if ($plate_id <= 0) {
return false;
}
if ($user_id <= 0) {
return function_exists('aksrt_guest_has_plate_follow_access')
&& aksrt_guest_has_plate_follow_access($plate_id);
}
if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'is_user_following_plate')) {
return (bool) Forum::get_instance()->is_user_following_plate($user_id, $plate_id);
}
$follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true);
if (!is_array($follow_list)) {
return false;
}
return in_array($plate_id, array_map('intval', $follow_list), true);
}
private function mark_followed(int $user_id, int $plate_id): void {
if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'mark_plate_followed')) {
Forum::get_instance()->mark_plate_followed($user_id, $plate_id);
return;
}
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))));
}
/**
* Free VIP or free-after-discount paid order.
*
* @param array{
* list_amount?:float,
* payable?:float,
* coupon_code?:string,
* coupon_discount?:float,
* coupon?:array,
* message?:string,
* paid_note?:string
* } $opts
* @return array|\WP_Error
*/
private function create_free_paid_order(int $plate_id, int $user_id, \WP_Post $post, array $priced, array $opts = []) {
$list_amount = round((float) ($opts['list_amount'] ?? 0), 2);
$payable = round((float) ($opts['payable'] ?? 0), 2);
$coupon_code = sanitize_text_field((string) ($opts['coupon_code'] ?? ''));
$coupon_discount = round((float) ($opts['coupon_discount'] ?? 0), 2);
$coupon_meta = is_array($opts['coupon'] ?? null) ? $opts['coupon'] : [];
$message = (string) ($opts['message'] ?? __('VIP 免费关注成功', 'aksrt'));
$paid_note = (string) ($opts['paid_note'] ?? __('VIP 免费关注版块。', 'aksrt'));
if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) {
$this->mark_followed($user_id, $plate_id);
return [
'success' => true,
'data' => [
'order_number' => '',
'amount' => 0,
'pay_modo' => '0',
'free' => true,
'message' => $message,
'reload' => true,
],
];
}
$note = wp_json_encode([
'plate_follow_pay' => 1,
'plate_id' => $plate_id,
'pay_modo' => '0',
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => (float) ($priced['base'] ?? 0),
'list_amount' => $list_amount,
'amount' => $payable,
'payable' => $payable,
'coupon_code' => $coupon_code,
'coupon_discount' => $coupon_discount,
'coupon' => $coupon_meta,
'free' => 1,
'guest' => $user_id <= 0 ? 1 : 0,
'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\Payment\OrderRepository::get_instance()->create([
'user_id' => max(0, $user_id),
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW,
'product_id' => $plate_id,
'amount' => 0.0,
'currency' => 'CNY',
'status' => \AKSRT\Payment\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_followed($user_id, $plate_id);
return [
'success' => true,
'data' => [
'order_number' => '',
'amount' => 0,
'free' => true,
'message' => $message,
'reload' => true,
],
];
}
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
'payment_method' => 'free',
'payment_transaction_id' => 'free_' . $order_number,
'note' => $paid_note,
]);
if (!$paid) {
return new \WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
}
$this->mark_followed($user_id, $plate_id);
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => 0,
'pay_modo' => '0',
'free' => true,
'message' => $message,
'reload' => true,
],
];
}
private function is_coupon_allowed(int $plate_id): bool {
return (int) get_post_meta($plate_id, '_aksrt_follow_pay_coupon_s', true) === 1;
}
/**
* VIP list price → coupon → payable.
*
* @param array<string,mixed> $priced
* @return array|\WP_Error
*/
private function apply_plate_cash_discounts(array $priced, int $plate_id, int $user_id, string $coupon_code = '', bool $soft_coupon = false) {
$list_amount = round(max(0.0, (float) ($priced['amount'] ?? 0)), 2);
$pay = $list_amount;
$coupon_discount = 0.0;
$coupon_meta = [];
$coupon_error = '';
$coupon_code = sanitize_text_field($coupon_code);
if ($coupon_code !== '') {
if (!$this->is_coupon_allowed($plate_id)) {
$err = new \WP_Error('aksrt_coupon_not_allowed', __('当前版块不支持优惠码。', 'aksrt'), ['status' => 400]);
if (!$soft_coupon) {
return $err;
}
$coupon_error = $err->get_error_message();
$coupon_code = '';
} elseif (!class_exists('\AKSRT\Payment\Coupon')) {
$err = new \WP_Error('aksrt_coupon_unavailable', __('优惠券系统不可用。', 'aksrt'), ['status' => 500]);
if (!$soft_coupon) {
return $err;
}
$coupon_error = $err->get_error_message();
$coupon_code = '';
} else {
$result = \AKSRT\Payment\Coupon::get_instance()->validate($coupon_code, $pay, [
'user_id' => max(0, $user_id),
'product_ids' => [$plate_id],
'context' => 'plate_follow',
]);
if (empty($result['valid'])) {
$err = new \WP_Error(
'aksrt_coupon_error',
(string) ($result['message'] ?? __('优惠码不可用。', 'aksrt')),
['status' => 400]
);
if (!$soft_coupon) {
return $err;
}
$coupon_error = $err->get_error_message();
$coupon_code = '';
} else {
$coupon_discount = round(max(0.0, (float) ($result['discount'] ?? 0)), 2);
if ($coupon_discount > $pay) {
$coupon_discount = $pay;
}
$pay = round(max(0.0, $pay - $coupon_discount), 2);
$c = is_array($result['coupon'] ?? null) ? $result['coupon'] : [];
$coupon_meta = [
'id' => (int) ($c['id'] ?? 0),
'code' => (string) ($c['code'] ?? $coupon_code),
'type' => (string) ($c['type'] ?? ''),
'value' => (float) ($c['value'] ?? 0),
'discount' => $coupon_discount,
];
$coupon_code = (string) $coupon_meta['code'];
}
}
}
return [
'list_amount' => $list_amount,
'coupon_discount' => $coupon_discount,
'coupon' => $coupon_meta,
'coupon_code' => $coupon_code,
'coupon_error' => $coupon_error,
'payable' => round(max(0.0, $pay), 2),
];
}
private function finalize_plate_order_side_effects(string $order_number, array $order): void {
$meta = $this->decode_order_note_meta($order);
if (empty($meta['plate_follow_pay'])) {
return;
}
if (($meta['pay_modo'] ?? '') === 'points') {
return;
}
$buyer_id = (int) ($order['user_id'] ?? 0);
if (function_exists('aksrt_burn_order_coupon')) {
aksrt_burn_order_coupon($order_number, $meta, $buyer_id);
}
}
private function find_pending_order(int $user_id, int $plate_id): ?array {
if ($user_id <= 0 || $plate_id <= 0 || !class_exists('\AKSRT\Payment\OrderRepository')) {
return null;
}
$orders = \AKSRT\Payment\OrderRepository::get_instance()->get_user_orders($user_id, [
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW,
'status' => \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING,
'limit' => 20,
'offset' => 0,
]);
foreach ($orders as $order) {
if ((int) ($order['product_id'] ?? 0) !== $plate_id) {
continue;
}
$meta = $this->decode_order_note_meta($order);
if (!empty($meta['plate_follow_pay'])) {
return $order;
}
return $order;
}
return null;
}
private function decode_order_note_meta(array $order): array {
if (isset($order['meta_payload']) && is_array($order['meta_payload']) && $order['meta_payload'] !== []) {
return $order['meta_payload'];
}
$note = (string) ($order['note'] ?? '');
if ($note === '') {
return [];
}
$decoded = json_decode($note, true);
return is_array($decoded) ? $decoded : [];
}
}
-581
View File
@@ -1,581 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class PlateMetaBox {
private static $instance = null;
/** @var array<string,string> */
private const ASSIGNABLE_ROLES = [
'super_moderator' => '超级版主',
'moderator' => '版主',
'junior_moderator' => '初级版主',
];
/** @var list<string> */
private const ALLOW_VIEW = ['public', 'login', 'follow', 'role'];
/** @var list<string> */
private const ADD_LIMIT = ['all', 'login', 'moderator', 'role'];
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
if (!is_admin()) {
return;
}
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_aksrt_forum_plate', [$this, 'save'], 20, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_plate_moderators',
__('版主与权限', 'aksrt'),
[$this, 'render'],
'aksrt_forum_plate',
'side',
'high'
);
}
public function render(\WP_Post $post): void {
wp_nonce_field('aksrt_plate_meta_save', 'aksrt_plate_meta_nonce');
$plate_id = (int) $post->ID;
$author_id = (int) $post->post_author;
$moderator_ids = [];
$roles_map = [];
$applications = [];
if (class_exists('AKSRT\\Forum\\Forum')) {
$forum = Forum::get_instance();
$moderator_ids = $forum->get_plate_moderators($plate_id);
$roles_map = $forum->get_moderator_roles($plate_id);
$applications = $forum->get_plate_moderator_applications($plate_id);
} else {
$raw = get_post_meta($plate_id, '_aksrt_moderators', true);
$moderator_ids = is_array($raw) ? array_map('intval', $raw) : [];
$roles_raw = get_post_meta($plate_id, '_aksrt_moderator_roles', true);
$roles_map = is_array($roles_raw) ? $roles_raw : [];
$applications = get_post_meta($plate_id, '_aksrt_moderator_applications', true);
$applications = is_array($applications) ? $applications : [];
}
$allow_view = (string) (get_post_meta($plate_id, '_aksrt_allow_view', true) ?: 'public');
if (!in_array($allow_view, self::ALLOW_VIEW, true)) {
// Rewrite legacy values (e.g. moderator) on display; save will persist new contract.
$allow_view = 'login';
}
$add_limit = (string) (get_post_meta($plate_id, '_aksrt_add_limit', true) ?: 'all');
if (!in_array($add_limit, self::ADD_LIMIT, true)) {
$add_limit = 'all';
}
$view_roles = get_post_meta($plate_id, '_aksrt_allow_view_roles', true);
$view_roles = is_array($view_roles) ? array_map('sanitize_key', $view_roles) : [];
$add_roles = get_post_meta($plate_id, '_aksrt_add_limit_roles', true);
$add_roles = is_array($add_roles) ? array_map('sanitize_key', $add_roles) : [];
$wp_roles = $this->wp_role_options();
$icon = (string) get_post_meta($plate_id, '_aksrt_icon', true);
$sort_order = (int) get_post_meta($plate_id, '_aksrt_sort_order', true);
$follow_pay = $this->get_follow_pay_meta($plate_id);
$this->render_access_fields($allow_view, $add_limit, $view_roles, $add_roles, $wp_roles);
$this->render_follow_pay_fields($follow_pay);
$this->render_display_fields($icon, $sort_order);
$this->render_moderator_fields($moderator_ids, $roles_map, $author_id);
$this->render_applications($applications);
}
/**
* @param list<string> $view_roles
* @param list<string> $add_roles
* @param array<string,string> $wp_roles
*/
private function render_access_fields(string $allow_view, string $add_limit, array $view_roles, array $add_roles, array $wp_roles): void {
?>
<p>
<label for="aksrt_plate_allow_view"><strong><?php esc_html_e('浏览权限', 'aksrt'); ?></strong></label>
<select id="aksrt_plate_allow_view" name="aksrt_plate_allow_view" class="widefat">
<?php foreach ([
'public' => __('公开', 'aksrt'),
'login' => __('登录可见', 'aksrt'),
'follow' => __('关注可见', 'aksrt'),
'role' => __('指定角色', 'aksrt'),
] as $k => $label) : ?>
<option value="<?php echo esc_attr($k); ?>" <?php selected($allow_view, $k); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</p>
<p>
<label for="aksrt_plate_allow_view_roles"><strong><?php esc_html_e('可浏览角色', 'aksrt'); ?></strong></label>
<select id="aksrt_plate_allow_view_roles" name="aksrt_plate_allow_view_roles[]" class="widefat" multiple size="4">
<?php foreach ($wp_roles as $slug => $label) : ?>
<option value="<?php echo esc_attr($slug); ?>" <?php selected(in_array($slug, $view_roles, true)); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
<span class="description"><?php esc_html_e('仅「指定角色」时生效。', 'aksrt'); ?></span>
</p>
<p>
<label for="aksrt_plate_add_limit"><strong><?php esc_html_e('发帖权限', 'aksrt'); ?></strong></label>
<select id="aksrt_plate_add_limit" name="aksrt_plate_add_limit" class="widefat">
<?php foreach ([
'all' => __('所有人', 'aksrt'),
'login' => __('登录用户', 'aksrt'),
'moderator' => __('仅版主', 'aksrt'),
'role' => __('指定角色', 'aksrt'),
] as $k => $label) : ?>
<option value="<?php echo esc_attr($k); ?>" <?php selected($add_limit, $k); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</p>
<p>
<label for="aksrt_plate_add_limit_roles"><strong><?php esc_html_e('可发帖角色', 'aksrt'); ?></strong></label>
<select id="aksrt_plate_add_limit_roles" name="aksrt_plate_add_limit_roles[]" class="widefat" multiple size="4">
<?php foreach ($wp_roles as $slug => $label) : ?>
<option value="<?php echo esc_attr($slug); ?>" <?php selected(in_array($slug, $add_roles, true)); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
<span class="description"><?php esc_html_e('仅「指定角色」时生效。', 'aksrt'); ?></span>
</p>
<hr>
<?php
}
/**
* @param array{enabled:bool,modo:string,price:float,points:int,vip_price:string} $follow_pay
*/
private function render_follow_pay_fields(array $follow_pay): void {
?>
<p>
<label>
<input type="checkbox" name="aksrt_plate_follow_pay_enabled" value="1" <?php checked(!empty($follow_pay['enabled'])); ?>>
<strong><?php esc_html_e('付费关注', 'aksrt'); ?></strong>
</label>
<span class="description" style="display:block;"><?php esc_html_e('开启后,未关注用户需支付才能关注(与「关注可见」配合使用)。', 'aksrt'); ?></span>
</p>
<p>
<label for="aksrt_plate_follow_pay_modo"><strong><?php esc_html_e('关注支付方式', 'aksrt'); ?></strong></label>
<select id="aksrt_plate_follow_pay_modo" name="aksrt_plate_follow_pay_modo" class="widefat">
<option value="money" <?php selected(($follow_pay['modo'] ?? 'money'), 'money'); ?>><?php esc_html_e('现金', 'aksrt'); ?></option>
<option value="points" <?php selected(($follow_pay['modo'] ?? ''), 'points'); ?>><?php esc_html_e('积分', 'aksrt'); ?></option>
</select>
<span class="description" style="display:block;"><?php esc_html_e('现金走多网关收银台;积分仍即时扣点。', 'aksrt'); ?></span>
</p>
<p>
<label for="aksrt_plate_follow_pay_price"><strong><?php esc_html_e('现金价格(元)', 'aksrt'); ?></strong></label>
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_plate_follow_pay_price" id="aksrt_plate_follow_pay_price" value="<?php echo esc_attr((string) ($follow_pay['price'] ?? 0)); ?>">
</p>
<p>
<label for="aksrt_plate_follow_pay_points"><strong><?php esc_html_e('积分价格', 'aksrt'); ?></strong></label>
<input type="number" step="1" min="0" class="widefat" name="aksrt_plate_follow_pay_points" id="aksrt_plate_follow_pay_points" value="<?php echo esc_attr((string) (int) ($follow_pay['points'] ?? 0)); ?>">
</p>
<p>
<label for="aksrt_plate_follow_pay_vip1_price"><strong><?php esc_html_e('VIP1 价格(可选)', 'aksrt'); ?></strong></label>
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_plate_follow_pay_vip1_price" id="aksrt_plate_follow_pay_vip1_price" value="<?php echo esc_attr((string) ($follow_pay['vip1_price'] ?? '')); ?>" placeholder="<?php esc_attr_e('留空=不设 VIP1 价;0=VIP1 免费', 'aksrt'); ?>">
</p>
<p>
<label for="aksrt_plate_follow_pay_vip2_price"><strong><?php esc_html_e('VIP2 价格(可选)', 'aksrt'); ?></strong></label>
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_plate_follow_pay_vip2_price" id="aksrt_plate_follow_pay_vip2_price" value="<?php echo esc_attr((string) ($follow_pay['vip2_price'] ?? '')); ?>" placeholder="<?php esc_attr_e('留空=不设 VIP2 价;0=VIP2 免费', 'aksrt'); ?>">
<span class="description"><?php esc_html_e('现金模式用元;积分模式填积分数。未填 VIP1 时仍可读旧「VIP 价格」字段。', 'aksrt'); ?></span>
</p>
<p>
<label>
<input type="checkbox" name="aksrt_plate_follow_pay_coupon_s" value="1" <?php checked(!empty($follow_pay['coupon_s'])); ?>>
<?php esc_html_e('允许优惠码(仅现金关注)', 'aksrt'); ?>
</label>
<span class="description" style="display:block;"><?php esc_html_e('开启后收银台可输入优惠码;积分关注不吃券。', 'aksrt'); ?></span>
</p>
<hr>
<?php
}
private function render_display_fields(string $icon, int $sort_order): void {
?>
<p>
<label for="aksrt_plate_icon"><strong><?php esc_html_e('图标', 'aksrt'); ?></strong></label>
<input type="text" id="aksrt_plate_icon" name="aksrt_plate_icon" class="widefat" value="<?php echo esc_attr($icon); ?>" placeholder="fa-comments / dashicons-admin-site" />
<span class="description"><?php esc_html_e('图标 class 或标识,保存即重写。', 'aksrt'); ?></span>
</p>
<p>
<label for="aksrt_plate_sort_order"><strong><?php esc_html_e('排序', 'aksrt'); ?></strong></label>
<input type="number" id="aksrt_plate_sort_order" name="aksrt_plate_sort_order" class="widefat" value="<?php echo esc_attr((string) $sort_order); ?>" step="1" />
<span class="description"><?php esc_html_e('数字越小越靠前。', 'aksrt'); ?></span>
</p>
<hr>
<?php
}
/**
* @param list<int> $moderator_ids
* @param array<int|string,string> $roles_map
*/
private function render_moderator_fields(array $moderator_ids, array $roles_map, int $author_id): void {
if ($author_id > 0) {
$author = get_userdata($author_id);
echo '<p><strong>' . esc_html__('超级版主(版块作者)', 'aksrt') . '</strong><br>';
echo '<code>' . esc_html($author ? $author->user_login . ' (#' . $author_id . ')' : (string) $author_id) . '</code>';
echo '<br><span class="description">' . esc_html__('由版块作者决定,不可在此移除。', 'aksrt') . '</span></p>';
}
echo '<p><strong>' . esc_html__('版主列表', 'aksrt') . '</strong></p>';
$rows = 0;
foreach ($moderator_ids as $uid) {
$uid = (int) $uid;
if ($uid <= 0 || $uid === $author_id) {
continue;
}
$user = get_userdata($uid);
$role = sanitize_key((string) ($roles_map[$uid] ?? 'moderator'));
if (!isset(self::ASSIGNABLE_ROLES[$role])) {
$role = 'moderator';
}
$rows++;
echo '<div style="margin:0 0 10px;padding:8px;border:1px solid #dcdcde;border-radius:4px;">';
echo '<div><code>' . esc_html($user ? $user->user_login . ' (#' . $uid . ')' : (string) $uid) . '</code></div>';
echo '<label style="display:block;margin-top:4px;">' . esc_html__('角色', 'aksrt') . ' ';
echo '<select name="aksrt_plate_mod_role[' . esc_attr((string) $uid) . ']" class="widefat">';
foreach (self::ASSIGNABLE_ROLES as $rk => $rl) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr($rk),
selected($role, $rk, false),
esc_html__($rl, 'aksrt')
);
}
echo '</select></label>';
echo '<label style="display:block;margin-top:4px;"><input type="checkbox" name="aksrt_plate_mod_remove[]" value="' . esc_attr((string) $uid) . '"> ' . esc_html__('移除', 'aksrt') . '</label>';
echo '</div>';
}
if ($rows === 0) {
echo '<p class="description">' . esc_html__('暂无版主。', 'aksrt') . '</p>';
}
echo '<p><strong>' . esc_html__('添加版主', 'aksrt') . '</strong></p>';
echo '<p><label>' . esc_html__('用户 ID', 'aksrt') . '<br>';
echo '<input type="number" min="1" name="aksrt_plate_mod_add_id" class="widefat" value="" placeholder="123"></label></p>';
echo '<p><label>' . esc_html__('角色', 'aksrt') . '<br><select name="aksrt_plate_mod_add_role" class="widefat">';
foreach (self::ASSIGNABLE_ROLES as $rk => $rl) {
printf(
'<option value="%s"%s>%s</option>',
esc_attr($rk),
selected($rk, 'moderator', false),
esc_html__($rl, 'aksrt')
);
}
echo '</select></label></p>';
echo '<p class="description">' . esc_html__('保存后生效。勿将版块作者重复加为版主。', 'aksrt') . '</p>';
echo '<hr>';
}
/**
* @param array<int,array<string,mixed>> $applications
*/
private function render_applications(array $applications): void {
echo '<p><strong>' . esc_html__('版主申请', 'aksrt') . '</strong></p>';
$pending = array_values(array_filter($applications, static function ($app) {
if (!is_array($app)) {
return false;
}
$status = (string) ($app['status'] ?? '');
if ($status === 'pending') {
return true;
}
return class_exists('AKSRT\\Forum\\Forum') && $status === Forum::APPLICATION_PENDING;
}));
if (!$pending) {
echo '<p class="description">' . esc_html__('暂无待处理申请。', 'aksrt') . '</p>';
return;
}
echo '<ul style="margin:0;padding-left:16px;">';
foreach ($pending as $app) {
$uid = (int) ($app['user_id'] ?? 0);
$user = $uid > 0 ? get_userdata($uid) : null;
$aid = (string) ($app['id'] ?? '');
$reason = (string) ($app['reason'] ?? '');
echo '<li style="margin-bottom:10px;">';
echo '<div><code>' . esc_html($user ? $user->display_name . ' (#' . $uid . ')' : (string) $uid) . '</code></div>';
if ($reason !== '') {
echo '<div class="description">' . esc_html($reason) . '</div>';
}
echo '<label style="display:block;margin-top:4px;"><input type="radio" name="aksrt_plate_app[' . esc_attr($aid) . ']" value="keep" checked> ' . esc_html__('保留', 'aksrt') . '</label>';
echo '<label style="display:block;"><input type="radio" name="aksrt_plate_app[' . esc_attr($aid) . ']" value="approved"> ' . esc_html__('通过', 'aksrt') . '</label>';
echo '<label style="display:block;"><input type="radio" name="aksrt_plate_app[' . esc_attr($aid) . ']" value="rejected"> ' . esc_html__('拒绝', 'aksrt') . '</label>';
echo '</li>';
}
echo '</ul>';
}
public function save(int $post_id, \WP_Post $post): void {
if (!isset($_POST['aksrt_plate_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_plate_meta_nonce'])), 'aksrt_plate_meta_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type !== 'aksrt_forum_plate' || !current_user_can('edit_post', $post_id)) {
return;
}
$this->save_access_meta($post_id);
$this->save_follow_pay_meta($post_id);
$this->save_display_meta($post_id);
$this->save_moderators($post_id, (int) $post->post_author);
$this->save_applications();
}
private function save_display_meta(int $post_id): void {
$icon = isset($_POST['aksrt_plate_icon'])
? sanitize_text_field((string) wp_unslash($_POST['aksrt_plate_icon']))
: '';
update_post_meta($post_id, '_aksrt_icon', $icon);
$sort = isset($_POST['aksrt_plate_sort_order'])
? (int) wp_unslash($_POST['aksrt_plate_sort_order'])
: 0;
update_post_meta($post_id, '_aksrt_sort_order', $sort);
}
private function save_access_meta(int $post_id): void {
$allow_view = isset($_POST['aksrt_plate_allow_view']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_allow_view'])) : 'public';
if (!in_array($allow_view, self::ALLOW_VIEW, true)) {
$allow_view = 'public';
}
update_post_meta($post_id, '_aksrt_allow_view', $allow_view);
if ($allow_view === 'role') {
$roles = $this->sanitize_role_list($_POST['aksrt_plate_allow_view_roles'] ?? []);
update_post_meta($post_id, '_aksrt_allow_view_roles', $roles);
} else {
delete_post_meta($post_id, '_aksrt_allow_view_roles');
}
$add_limit = isset($_POST['aksrt_plate_add_limit']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_add_limit'])) : 'all';
if (!in_array($add_limit, self::ADD_LIMIT, true)) {
$add_limit = 'all';
}
update_post_meta($post_id, '_aksrt_add_limit', $add_limit);
if ($add_limit === 'role') {
$roles = $this->sanitize_role_list($_POST['aksrt_plate_add_limit_roles'] ?? []);
update_post_meta($post_id, '_aksrt_add_limit_roles', $roles);
} else {
delete_post_meta($post_id, '_aksrt_add_limit_roles');
}
}
private function save_follow_pay_meta(int $post_id): void {
$enabled = !empty($_POST['aksrt_plate_follow_pay_enabled']);
$modo = isset($_POST['aksrt_plate_follow_pay_modo'])
? sanitize_key((string) wp_unslash($_POST['aksrt_plate_follow_pay_modo']))
: 'money';
if (!in_array($modo, ['money', 'points'], true)) {
$modo = 'money';
}
$price = isset($_POST['aksrt_plate_follow_pay_price']) ? max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_price'])) : 0;
$points = isset($_POST['aksrt_plate_follow_pay_points']) ? max(0, (int) wp_unslash($_POST['aksrt_plate_follow_pay_points'])) : 0;
// Invalid price while enabled → force off (normalize, no migration).
if ($enabled) {
if ($modo === 'points' && $points <= 0) {
$enabled = false;
}
if ($modo === 'money' && $price <= 0) {
$enabled = false;
}
}
if (!$enabled) {
delete_post_meta($post_id, '_aksrt_follow_pay');
delete_post_meta($post_id, '_aksrt_follow_pay_modo');
delete_post_meta($post_id, '_aksrt_follow_pay_price');
delete_post_meta($post_id, '_aksrt_follow_pay_points');
delete_post_meta($post_id, '_aksrt_follow_pay_vip_price');
delete_post_meta($post_id, '_aksrt_follow_pay_vip1_price');
delete_post_meta($post_id, '_aksrt_follow_pay_vip2_price');
delete_post_meta($post_id, '_aksrt_follow_pay_coupon_s');
return;
}
update_post_meta($post_id, '_aksrt_follow_pay', 1);
update_post_meta($post_id, '_aksrt_follow_pay_modo', $modo);
update_post_meta($post_id, '_aksrt_follow_pay_price', $price);
update_post_meta($post_id, '_aksrt_follow_pay_points', $points);
update_post_meta($post_id, '_aksrt_follow_pay_coupon_s', !empty($_POST['aksrt_plate_follow_pay_coupon_s']) ? 1 : 0);
if (isset($_POST['aksrt_plate_follow_pay_vip1_price']) && (string) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price']) !== '') {
$vip1 = max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_vip1_price']));
update_post_meta($post_id, '_aksrt_follow_pay_vip1_price', $vip1);
// Keep legacy single VIP key in sync with VIP1 for old readers.
update_post_meta($post_id, '_aksrt_follow_pay_vip_price', $vip1);
} else {
delete_post_meta($post_id, '_aksrt_follow_pay_vip1_price');
delete_post_meta($post_id, '_aksrt_follow_pay_vip_price');
}
if (isset($_POST['aksrt_plate_follow_pay_vip2_price']) && (string) wp_unslash($_POST['aksrt_plate_follow_pay_vip2_price']) !== '') {
update_post_meta($post_id, '_aksrt_follow_pay_vip2_price', max(0, (float) wp_unslash($_POST['aksrt_plate_follow_pay_vip2_price'])));
} else {
delete_post_meta($post_id, '_aksrt_follow_pay_vip2_price');
}
}
/**
* @return array{enabled:bool,modo:string,price:float,points:int,vip1_price:string,vip2_price:string,coupon_s:bool}
*/
private function get_follow_pay_meta(int $plate_id): array {
$enabled = (int) get_post_meta($plate_id, '_aksrt_follow_pay', true) === 1;
$modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money'));
if (!in_array($modo, ['money', 'points'], true)) {
$modo = 'money';
}
$vip1_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip1_price', true);
$vip1_set = ($vip1_raw !== '' && $vip1_raw !== null && $vip1_raw !== false);
if (!$vip1_set) {
// Legacy single VIP key → vip1.
$vip1_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip_price', true);
$vip1_set = ($vip1_raw !== '' && $vip1_raw !== null && $vip1_raw !== false);
}
$vip2_raw = get_post_meta($plate_id, '_aksrt_follow_pay_vip2_price', true);
$vip2_set = ($vip2_raw !== '' && $vip2_raw !== null && $vip2_raw !== false);
return [
'enabled' => $enabled,
'modo' => $modo,
'price' => (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true),
'points' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true),
'vip1_price' => $vip1_set ? (string) (float) $vip1_raw : '',
'vip2_price' => $vip2_set ? (string) (float) $vip2_raw : '',
'coupon_s' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_coupon_s', true) === 1,
];
}
private function save_moderators(int $post_id, int $author_id): void {
$role_posted = isset($_POST['aksrt_plate_mod_role']) && is_array($_POST['aksrt_plate_mod_role'])
? wp_unslash($_POST['aksrt_plate_mod_role'])
: [];
$remove = isset($_POST['aksrt_plate_mod_remove']) && is_array($_POST['aksrt_plate_mod_remove'])
? array_map('intval', wp_unslash($_POST['aksrt_plate_mod_remove']))
: [];
$remove_set = array_fill_keys($remove, true);
$desired = [];
foreach ($role_posted as $uid => $role) {
$uid = (int) $uid;
$role = sanitize_key((string) $role);
if ($uid <= 0 || $uid === $author_id || isset($remove_set[$uid])) {
continue;
}
if (!isset(self::ASSIGNABLE_ROLES[$role])) {
$role = 'moderator';
}
$desired[$uid] = $role;
}
$add_id = isset($_POST['aksrt_plate_mod_add_id']) ? (int) wp_unslash($_POST['aksrt_plate_mod_add_id']) : 0;
$add_role = isset($_POST['aksrt_plate_mod_add_role']) ? sanitize_key((string) wp_unslash($_POST['aksrt_plate_mod_add_role'])) : 'moderator';
if ($add_id > 0 && $add_id !== $author_id) {
if (!isset(self::ASSIGNABLE_ROLES[$add_role])) {
$add_role = 'moderator';
}
$desired[$add_id] = $add_role;
}
if (class_exists('AKSRT\\Forum\\Forum')) {
$forum = Forum::get_instance();
$current = $forum->get_plate_moderators($post_id);
foreach ($current as $uid) {
$uid = (int) $uid;
if ($uid === $author_id) {
continue;
}
if (!isset($desired[$uid])) {
$forum->remove_moderator($uid, $post_id);
}
}
foreach ($desired as $uid => $role) {
if (in_array($uid, $current, true)) {
$forum->set_moderator_role($uid, $post_id, $role);
} else {
$forum->add_moderator($uid, $post_id, $role);
}
}
return;
}
// Fallback without forum service: rewrite meta directly (no legacy keep).
$ids = array_values(array_unique(array_map('intval', array_keys($desired))));
update_post_meta($post_id, '_aksrt_moderators', $ids);
update_post_meta($post_id, '_aksrt_moderator_roles', $desired);
}
private function save_applications(): void {
$actions = isset($_POST['aksrt_plate_app']) && is_array($_POST['aksrt_plate_app'])
? wp_unslash($_POST['aksrt_plate_app'])
: [];
if (!$actions || !class_exists('AKSRT\\Forum\\Forum')) {
return;
}
$forum = Forum::get_instance();
foreach ($actions as $application_id => $action) {
$application_id = sanitize_text_field((string) $application_id);
$action = sanitize_key((string) $action);
if ($application_id === '' || $action === 'keep') {
continue;
}
if ($action === 'approved') {
$forum->review_moderator_application($application_id, Forum::APPLICATION_APPROVED, '', get_current_user_id());
} elseif ($action === 'rejected') {
$forum->review_moderator_application($application_id, Forum::APPLICATION_REJECTED, '', get_current_user_id());
}
}
}
/**
* @param mixed $raw
* @return list<string>
*/
private function sanitize_role_list($raw): array {
if (!is_array($raw)) {
return [];
}
$valid = array_keys($this->wp_role_options());
$out = [];
foreach ($raw as $role) {
$role = sanitize_key((string) $role);
if ($role !== '' && in_array($role, $valid, true)) {
$out[] = $role;
}
}
return array_values(array_unique($out));
}
/**
* @return array<string,string>
*/
private function wp_role_options(): array {
$out = [];
if (!function_exists('wp_roles')) {
return $out;
}
foreach (wp_roles()->roles as $slug => $data) {
$out[sanitize_key((string) $slug)] = translate_user_role((string) ($data['name'] ?? $slug));
}
return $out;
}
}
-447
View File
@@ -1,447 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class ScoreService {
/** @var Forum */
private $forum;
public function __construct($forum) {
$this->forum = $forum;
}
// ================================================================
// Favorite (§7.3)
// ================================================================
public function is_favorited(int $user_id, int $topic_id): bool {
if ($user_id <= 0) {
return false;
}
$favorites = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true);
$favorites = is_array($favorites) ? $favorites : [];
return in_array($topic_id, $favorites, true);
}
public function toggle_favorite(int $user_id, int $topic_id) {
if ($user_id <= 0) {
return new \WP_Error('not_logged_in', __('请先登录', 'aksrt'));
}
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt'));
}
$favorites = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true);
$favorites = is_array($favorites) ? $favorites : [];
$was_favorited = in_array($topic_id, $favorites, true);
if ($was_favorited) {
$favorites = array_values(array_diff($favorites, [$topic_id]));
} else {
$favorites[] = $topic_id;
}
update_user_meta($user_id, 'aksrt_favorite_forum_posts', $favorites);
$count = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true);
$count = $was_favorited ? max(0, $count - 1) : $count + 1;
update_post_meta($topic_id, '_aksrt_favorite_count', $count);
$this->refresh_recommend_score($topic_id);
return [
'favorited' => !$was_favorited,
'count' => $count,
'message' => $was_favorited
? __('已取消收藏', 'aksrt')
: __('收藏成功', 'aksrt'),
];
}
// ================================================================
// Score (§7.3)
// ================================================================
public function score_topic(int $user_id, int $topic_id, int $amount) {
if ($user_id <= 0) {
return new \WP_Error('not_logged_in', __('请先登录', 'aksrt'));
}
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt'));
}
if ($amount === 0) {
return new \WP_Error('invalid_amount', __('评分值不能为零', 'aksrt'));
}
$author_id = (int) $post->post_author;
if ($author_id === $user_id) {
return new \WP_Error('self_score', __('不能给自己的帖子评分', 'aksrt'));
}
$config = $this->forum->get_config();
$max_extra = (int) ($config['post_page']['score_extra_max'] ?? 5);
$max_deduct = (int) ($config['post_page']['score_deduct_max'] ?? 3);
if ($amount > 0 && $amount > $max_extra) {
$amount = $max_extra;
}
if ($amount < 0 && abs($amount) > $max_deduct) {
$amount = -$max_deduct;
}
$detail = get_post_meta($topic_id, '_aksrt_score_detail', true);
$detail = is_array($detail) ? $detail : [];
$user_key = (string) $user_id;
if (isset($detail[$user_key])) {
return new \WP_Error('already_scored', __('你已经对该帖评过分', 'aksrt'));
}
$detail[$user_key] = [
'amount' => $amount,
'time' => current_time('mysql'),
];
update_post_meta($topic_id, '_aksrt_score_detail', $detail);
$total = (int) get_post_meta($topic_id, '_aksrt_score', true);
$total += $amount;
update_post_meta($topic_id, '_aksrt_score', $total);
do_action('aksrt_forum_topic_scored', $topic_id, (int) $author_id, (float) $amount);
$reputation = (int) get_user_meta($author_id, 'aksrt_reputation', true);
$reputation += $amount;
update_user_meta($author_id, 'aksrt_reputation', $reputation);
if ($amount > 0) {
$credits_amount = (int) ($config['credits']['score_extra'] ?? 3);
if ($credits_amount > 0) {
if (class_exists('\AKSRT\Credits\PointsFree')) {
\AKSRT\Credits\PointsFree::grant($author_id, 'bbs_score_extra', $credits_amount, [
'type' => 'post',
'id' => $topic_id,
]);
} elseif (class_exists('\AKSRT\Credits\Credits')) {
$daily = $this->today_credits_count($author_id, 'forum_score_extra');
$daily_max = (int) ($config['credits']['daily_max'] ?? 50);
if ($daily < $daily_max) {
\AKSRT\Credits\Credits::add($author_id, $credits_amount, 'forum_score_extra', sprintf(
__('帖子「%s」被加分', 'aksrt'),
get_the_title($topic_id)
));
}
}
}
}
$this->refresh_recommend_score($topic_id);
$this->notify_topic_scored($topic_id, $user_id, $amount);
return [
'score' => $total,
'message' => $amount > 0
? __('加分成功', 'aksrt')
: __('减分成功', 'aksrt'),
];
}
public function get_score_detail(int $topic_id): array {
$detail = get_post_meta($topic_id, '_aksrt_score_detail', true);
$detail = is_array($detail) ? $detail : [];
$entries = [];
foreach ($detail as $uid => $entry) {
$user = get_userdata((int) $uid);
$entries[] = [
'user_id' => (int) $uid,
'display_name' => $user ? $user->display_name : '',
'amount' => (int) ($entry['amount'] ?? 0),
'time' => (string) ($entry['time'] ?? ''),
];
}
return $entries;
}
// ================================================================
// Recommend score (§7.3)
// ================================================================
public function calculate_recommend_score(int $topic_id): float {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return 0.0;
}
$views = (int) get_post_meta($topic_id, '_aksrt_views', true);
$favorites = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true);
$comments = (int) get_comments([
'post_id' => $topic_id,
'count' => true,
'status' => 'approve',
]);
$score = (int) get_post_meta($topic_id, '_aksrt_score', true);
$w_views = 1.0;
$w_favorites = 5.0;
$w_comments = 3.0;
$w_score = 2.0;
$raw = ($views * $w_views)
+ ($favorites * $w_favorites)
+ ($comments * $w_comments)
+ (max(0, $score) * $w_score);
$post_time = strtotime($post->post_date_gmt);
$age_hours = max(1, (time() - $post_time) / 3600);
$decay = pow($age_hours, 1.5);
$recommend = ($raw + 1) / $decay * 10000;
return round($recommend, 2);
}
public function refresh_recommend_score(int $topic_id): float {
$score = $this->calculate_recommend_score($topic_id);
update_post_meta($topic_id, '_aksrt_recommend_score', $score);
$this->maybe_reward_hot_topic($topic_id, $score);
return $score;
}
private function maybe_reward_hot_topic(int $topic_id, float $score): void {
$config = $this->forum->get_config();
$hot_threshold = (float) ($config['post_page']['hot_recommend_threshold']
?? $config['credits']['hot_recommend_threshold']
?? $config['other']['hot_topic_threshold']
?? 500);
if ($hot_threshold <= 0) {
$hot_threshold = 500.0;
}
if ($score < $hot_threshold) {
return;
}
if (get_post_meta($topic_id, '_aksrt_hot_rewarded', true)) {
return;
}
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return;
}
$author_id = (int) $post->post_author;
if ($author_id <= 0) {
return;
}
do_action('aksrt_forum_topic_hot', $topic_id, (int) $author_id);
update_post_meta($topic_id, '_aksrt_hot_rewarded', '1');
$amount = (int) ($config['credits']['posts_hot'] ?? 5);
if ($amount > 0) {
if (class_exists('\AKSRT\Credits\PointsFree')) {
\AKSRT\Credits\PointsFree::grant($author_id, 'bbs_posts_hot', $amount, [
'type' => 'post',
'id' => $topic_id,
]);
} elseif (class_exists('\AKSRT\Credits\Credits')) {
\AKSRT\Credits\Credits::add($author_id, $amount, 'forum_hot_topic', [
'type' => 'post', 'id' => $topic_id,
]);
}
}
$this->notify($author_id, 'hot', sprintf(
__('你的帖子「%s」成为热帖!', 'aksrt'), get_the_title($topic_id)
), [
'title' => __('热帖通知', 'aksrt'),
'target_url' => get_permalink($topic_id),
]);
}
public function batch_refresh_recommend_scores(int $limit = 100): int {
$query = new \WP_Query([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'posts_per_page' => $limit,
'orderby' => 'modified',
'order' => 'DESC',
'fields' => 'ids',
'no_found_rows' => true,
]);
$count = 0;
foreach ($query->posts as $post_id) {
$this->refresh_recommend_score((int) $post_id);
$count++;
}
return $count;
}
public function maybe_refresh_recommend_on_view(): void {
if (!is_singular('aksrt_forum_topic')) {
return;
}
$topic_id = get_the_ID();
if (!$topic_id) {
return;
}
$last = (int) get_post_meta($topic_id, '_aksrt_recommend_refreshed', true);
if ((time() - $last) > 300) {
$this->refresh_recommend_score($topic_id);
update_post_meta($topic_id, '_aksrt_recommend_refreshed', time());
}
}
private function get_bbs_type_label(string $bbs_type): string {
$config = $this->forum->get_config();
$names = $config['names'] ?? [];
$map = [
Forum::BBS_TYPE_STANDARD => $names['posts'] ?? __('帖子', 'aksrt'),
Forum::BBS_TYPE_QUESTION => $names['question_badge'] ?? __('提问', 'aksrt'),
];
return $map[$bbs_type] ?? $map[Forum::BBS_TYPE_STANDARD];
}
private function get_question_status_label(int $status): string {
$config = $this->forum->get_config();
$names = $config['names'] ?? [];
return $status === Forum::QUESTION_SOLVED
? ($names['question_ok_badge'] ?? __('已解决', 'aksrt'))
: ($names['question_badge'] ?? __('提问', 'aksrt'));
}
// ================================================================
// Notifications (§7.8)
// ================================================================
public function notify(int $user_id, string $type, string $content, array $data = []): void {
if ($user_id <= 0 || !class_exists('\AKSRT\Messages\Message')) {
return;
}
\AKSRT\Messages\Message::get_instance()->send($user_id, $type, $content, $data);
}
public function notify_topic_pending(int $topic_id, int $author_id): void {
$title = get_the_title($topic_id);
$config = $this->forum->get_config();
$admin_users = $config['admin_users'] ?? [];
foreach ($admin_users as $admin_id) {
$this->notify((int) $admin_id, 'posts', sprintf(
__('帖子「%s」待审核', 'aksrt'), $title
), [
'title' => __('待审核帖子', 'aksrt'),
'target_url' => get_permalink($topic_id),
]);
}
}
public function notify_moderation_result(int $topic_id, string $action, int $operator_id): void {
$post = get_post($topic_id);
if (!$post) return;
$author_id = (int) $post->post_author;
if ($author_id <= 0 || $author_id === $operator_id) return;
$title = get_the_title($topic_id);
$action_labels = [
'review' => __('审核状态已变更', 'aksrt'),
'sticky' => __('置顶状态已变更', 'aksrt'),
'featured' => __('精华状态已变更', 'aksrt'),
'locked' => __('锁定状态已变更', 'aksrt'),
'close' => __('帖子已关闭', 'aksrt'),
'hide' => __('帖子已隐藏', 'aksrt'),
'delete' => __('帖子已删除', 'aksrt'),
'undelete' => __('帖子已恢复', 'aksrt'),
];
if ($action === 'review') {
$review_status = (string) get_post_meta($topic_id, '_aksrt_review_status', true);
if ($review_status === Forum::REVIEW_REJECTED) {
$action_labels['review'] = __('审核未通过', 'aksrt');
} elseif ($review_status === Forum::REVIEW_PENDING) {
$action_labels['review'] = __('已转为待审核', 'aksrt');
} elseif ($review_status === Forum::REVIEW_APPROVED) {
$action_labels['review'] = __('审核已通过', 'aksrt');
}
}
$label = $action_labels[$action] ?? __('治理动作已执行', 'aksrt');
$target_url = $action === 'delete' ? aksrt_get_forum_home_url() : get_permalink($topic_id);
$payload = [
'title' => $label,
'target_url' => $target_url,
];
$note = (string) get_post_meta($topic_id, '_aksrt_moderation_note', true);
if ($note !== '') {
$payload['reason'] = $note;
}
$this->notify($author_id, 'system', sprintf(
__('你的帖子「%1$s」%2$s', 'aksrt'), $title, $label
), $payload);
}
public function notify_topic_reply(int $topic_id, int $commenter_id): void {
$post = get_post($topic_id);
if (!$post) return;
$author_id = (int) $post->post_author;
if ($author_id <= 0 || $author_id === $commenter_id) return;
$commenter = get_userdata($commenter_id);
$name = $commenter ? $commenter->display_name : __('有人', 'aksrt');
$title = get_the_title($topic_id);
$this->notify($author_id, 'comment', sprintf(
__('%1$s 回复了你的帖子「%2$s」', 'aksrt'), $name, $title
), [
'title' => __('新回复', 'aksrt'),
'target_url' => get_permalink($topic_id),
]);
}
public function notify_topic_scored(int $topic_id, int $scorer_id, int $amount): void {
$post = get_post($topic_id);
if (!$post) return;
$author_id = (int) $post->post_author;
if ($author_id <= 0 || $author_id === $scorer_id) return;
$title = get_the_title($topic_id);
$this->notify($author_id, 'system', sprintf(
__('你的帖子「%1$s」被%2$s %3$d 分', 'aksrt'),
$title,
$amount > 0 ? __('加', 'aksrt') : __('减', 'aksrt'),
abs($amount)
), [
'title' => $amount > 0 ? __('帖子被加分', 'aksrt') : __('帖子被减分', 'aksrt'),
'target_url' => get_permalink($topic_id),
]);
}
// ================================================================
// Private helpers
// ================================================================
private function today_credits_count(int $user_id, string $reason): int {
if (!class_exists('\AKSRT\Core\DBMigrator') || $user_id <= 0) {
return 0;
}
$table = \AKSRT\Core\DBMigrator::table('credit_log');
if (!$table) {
return 0;
}
global $wpdb;
$today = current_time('Y-m-d');
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND reason = %s AND DATE(created_at) = %s",
$user_id, $reason, $today
));
}
}
-424
View File
@@ -1,424 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class TopicAdminList {
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 {
if (!is_admin()) {
return;
}
add_filter('manage_edit-aksrt_forum_topic_columns', [$this, 'columns']);
add_action('manage_aksrt_forum_topic_posts_custom_column', [$this, 'render_column'], 10, 2);
add_filter('manage_edit-aksrt_forum_topic_sortable_columns', [$this, 'sortable_columns']);
add_action('restrict_manage_posts', [$this, 'filters']);
add_action('pre_get_posts', [$this, 'apply_filters']);
add_filter('bulk_actions-edit-aksrt_forum_topic', [$this, 'bulk_actions']);
add_filter('handle_bulk_actions-edit-aksrt_forum_topic', [$this, 'handle_bulk_actions'], 10, 3);
add_action('quick_edit_custom_box', [$this, 'quick_edit_box'], 10, 2);
add_action('save_post_aksrt_forum_topic', [$this, 'save_quick_edit'], 20, 2);
add_action('admin_notices', [$this, 'bulk_notices']);
add_action('admin_footer-edit.php', [$this, 'quick_edit_script']);
}
public function columns(array $columns): array {
$new = [];
foreach ($columns as $key => $label) {
$new[$key] = $label;
if ($key === 'title') {
$new['aksrt_plate'] = __('版块', 'aksrt');
$new['aksrt_sticky'] = __('置顶', 'aksrt');
$new['aksrt_locked'] = __('锁定', 'aksrt');
$new['aksrt_review'] = __('审核', 'aksrt');
$new['aksrt_bbs_type']= __('类型', 'aksrt');
$new['aksrt_visibility'] = __('可见性', 'aksrt');
$new['aksrt_views'] = __('阅读', 'aksrt');
}
}
return $new;
}
public function sortable_columns(array $columns): array {
$columns['aksrt_sticky'] = 'aksrt_sticky';
$columns['aksrt_views'] = 'aksrt_views';
return $columns;
}
public function render_column(string $column, int $post_id): void {
switch ($column) {
case 'aksrt_plate':
$plate_id = (int) get_post_meta($post_id, '_aksrt_plate_id', true);
if ($plate_id > 0) {
$plate = get_post($plate_id);
echo $plate ? esc_html(get_the_title($plate)) : esc_html('#' . $plate_id);
} else {
// fallback board taxonomy
$terms = get_the_terms($post_id, 'aksrt_forum_board');
if (is_array($terms) && $terms) {
echo esc_html($terms[0]->name);
} else {
echo '—';
}
}
break;
case 'aksrt_sticky':
$sticky = (int) get_post_meta($post_id, '_aksrt_sticky', true);
$map = [
0 => __('否', 'aksrt'),
1 => __('版块', 'aksrt'),
2 => __('超级', 'aksrt'),
3 => __('全局', 'aksrt'),
];
echo esc_html($map[$sticky] ?? (string) $sticky);
echo '<div class="hidden" id="aksrt-inline-' . esc_attr((string) $post_id) . '"';
echo ' data-sticky="' . esc_attr((string) $sticky) . '"';
echo ' data-locked="' . esc_attr((string) (int) get_post_meta($post_id, '_aksrt_locked', true)) . '"';
echo ' data-review="' . esc_attr((string) get_post_meta($post_id, '_aksrt_review_status', true)) . '"';
echo ' data-plate="' . esc_attr((string) (int) get_post_meta($post_id, '_aksrt_plate_id', true)) . '"';
echo '></div>';
break;
case 'aksrt_locked':
$locked = (int) get_post_meta($post_id, '_aksrt_locked', true);
echo $locked ? esc_html__('是', 'aksrt') : esc_html__('否', 'aksrt');
break;
case 'aksrt_review':
$status = (string) get_post_meta($post_id, '_aksrt_review_status', true);
if ($status === '') {
$status = 'approved';
}
$map = [
'approved' => __('已通过', 'aksrt'),
'pending' => __('待审核', 'aksrt'),
'rejected' => __('已拒绝', 'aksrt'),
];
echo esc_html($map[$status] ?? $status);
break;
case 'aksrt_bbs_type':
$type = (string) (get_post_meta($post_id, '_aksrt_bbs_type', true) ?: 'standard');
echo esc_html($type === 'question' ? __('提问', 'aksrt') : __('标准', 'aksrt'));
break;
case 'aksrt_visibility':
$visibility = (string) (get_post_meta($post_id, '_aksrt_visibility', true) ?: 'public');
$map = [
'public' => __('公开', 'aksrt'),
'login' => __('登录', 'aksrt'),
'comment' => __('评论', 'aksrt'),
'role' => __('角色', 'aksrt'),
'credits' => __('积分', 'aksrt'),
'pay' => __('付费', 'aksrt'),
'moderator' => __('版主', 'aksrt'),
];
echo esc_html($map[$visibility] ?? $visibility);
break;
case 'aksrt_views':
echo esc_html((string) (int) get_post_meta($post_id, '_aksrt_views', true));
break;
}
}
public function filters(string $post_type): void {
if ($post_type !== 'aksrt_forum_topic') {
return;
}
$sticky = isset($_GET['aksrt_sticky']) ? sanitize_key((string) wp_unslash($_GET['aksrt_sticky'])) : '';
$locked = isset($_GET['aksrt_locked']) ? sanitize_key((string) wp_unslash($_GET['aksrt_locked'])) : '';
$review = isset($_GET['aksrt_review']) ? sanitize_key((string) wp_unslash($_GET['aksrt_review'])) : '';
$plate = isset($_GET['aksrt_plate']) ? (int) $_GET['aksrt_plate'] : 0;
echo '<select name="aksrt_sticky">';
echo '<option value="">' . esc_html__('置顶筛选', 'aksrt') . '</option>';
foreach ([
'0' => __('未置顶', 'aksrt'),
'1' => __('版块置顶', 'aksrt'),
'2' => __('超级置顶', 'aksrt'),
'3' => __('全局置顶', 'aksrt'),
] as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($sticky, $k, false), esc_html($label));
}
echo '</select> ';
echo '<select name="aksrt_locked">';
echo '<option value="">' . esc_html__('锁定筛选', 'aksrt') . '</option>';
printf('<option value="1"%s>%s</option>', selected($locked, '1', false), esc_html__('已锁定', 'aksrt'));
printf('<option value="0"%s>%s</option>', selected($locked, '0', false), esc_html__('未锁定', 'aksrt'));
echo '</select> ';
echo '<select name="aksrt_review">';
echo '<option value="">' . esc_html__('审核筛选', 'aksrt') . '</option>';
foreach ([
'pending' => __('待审核', 'aksrt'),
'approved' => __('已通过', 'aksrt'),
'rejected' => __('已拒绝', 'aksrt'),
] as $k => $label) {
printf('<option value="%s"%s>%s</option>', esc_attr($k), selected($review, $k, false), esc_html($label));
}
echo '</select> ';
$plates = get_posts([
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => 200,
'orderby' => 'title',
'order' => 'ASC',
]);
if ($plates) {
echo '<select name="aksrt_plate">';
echo '<option value="0">' . esc_html__('全部版块', 'aksrt') . '</option>';
foreach ($plates as $p) {
printf(
'<option value="%d"%s>%s</option>',
(int) $p->ID,
selected($plate, (int) $p->ID, false),
esc_html(get_the_title($p))
);
}
echo '</select> ';
}
}
public function apply_filters(\WP_Query $query): void {
if (!is_admin() || !$query->is_main_query()) {
return;
}
$post_type = $query->get('post_type');
if ($post_type !== 'aksrt_forum_topic') {
return;
}
$meta_query = (array) $query->get('meta_query');
if (isset($_GET['aksrt_sticky']) && $_GET['aksrt_sticky'] !== '') {
$meta_query[] = [
'key' => '_aksrt_sticky',
'value' => (int) $_GET['aksrt_sticky'],
];
}
if (isset($_GET['aksrt_locked']) && $_GET['aksrt_locked'] !== '') {
$meta_query[] = [
'key' => '_aksrt_locked',
'value' => (int) $_GET['aksrt_locked'],
];
}
if (!empty($_GET['aksrt_review'])) {
$review = sanitize_key((string) wp_unslash($_GET['aksrt_review']));
if ($review === 'approved') {
$meta_query[] = [
'relation' => 'OR',
[
'key' => '_aksrt_review_status',
'value' => 'approved',
],
[
'key' => '_aksrt_review_status',
'compare' => 'NOT EXISTS',
],
[
'key' => '_aksrt_review_status',
'value' => '',
],
];
} else {
$meta_query[] = [
'key' => '_aksrt_review_status',
'value' => $review,
];
}
}
if (!empty($_GET['aksrt_plate'])) {
$meta_query[] = [
'key' => '_aksrt_plate_id',
'value' => (int) $_GET['aksrt_plate'],
];
}
if ($meta_query) {
$query->set('meta_query', $meta_query);
}
$orderby = $query->get('orderby');
if ($orderby === 'aksrt_sticky') {
$query->set('meta_key', '_aksrt_sticky');
$query->set('orderby', 'meta_value_num');
} elseif ($orderby === 'aksrt_views') {
$query->set('meta_key', '_aksrt_views');
$query->set('orderby', 'meta_value_num');
}
}
public function bulk_actions(array $actions): array {
$actions['aksrt_sticky_1'] = __('设为版块置顶', 'aksrt');
$actions['aksrt_sticky_0'] = __('取消置顶', 'aksrt');
$actions['aksrt_lock'] = __('锁定', 'aksrt');
$actions['aksrt_unlock'] = __('解锁', 'aksrt');
$actions['aksrt_approve'] = __('审核通过', 'aksrt');
$actions['aksrt_reject'] = __('审核拒绝', 'aksrt');
return $actions;
}
public function handle_bulk_actions(string $redirect_to, string $action, array $post_ids): string {
$count = 0;
foreach ($post_ids as $post_id) {
$post_id = (int) $post_id;
if ($post_id <= 0 || get_post_type($post_id) !== 'aksrt_forum_topic') {
continue;
}
switch ($action) {
case 'aksrt_sticky_1':
update_post_meta($post_id, '_aksrt_sticky', 1);
$count++;
break;
case 'aksrt_sticky_0':
update_post_meta($post_id, '_aksrt_sticky', 0);
$count++;
break;
case 'aksrt_lock':
update_post_meta($post_id, '_aksrt_locked', 1);
$count++;
break;
case 'aksrt_unlock':
update_post_meta($post_id, '_aksrt_locked', 0);
$count++;
break;
case 'aksrt_approve':
update_post_meta($post_id, '_aksrt_review_status', 'approved');
$count++;
break;
case 'aksrt_reject':
update_post_meta($post_id, '_aksrt_review_status', 'rejected');
$count++;
break;
}
}
return add_query_arg('aksrt_topic_bulk', $count, $redirect_to);
}
public function bulk_notices(): void {
if (empty($_GET['aksrt_topic_bulk'])) {
return;
}
$count = (int) $_GET['aksrt_topic_bulk'];
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html(sprintf(
/* translators: %d count */
__('已更新 %d 个帖子。', 'aksrt'),
$count
)) . '</p></div>';
}
public function quick_edit_box(string $column_name, string $post_type): void {
if ($post_type !== 'aksrt_forum_topic' || $column_name !== 'aksrt_sticky') {
return;
}
?>
<fieldset class="inline-edit-col-right">
<div class="inline-edit-col">
<label>
<span class="title"><?php esc_html_e('置顶', 'aksrt'); ?></span>
<select name="aksrt_sticky">
<option value="0"><?php esc_html_e('否', 'aksrt'); ?></option>
<option value="1"><?php esc_html_e('版块', 'aksrt'); ?></option>
<option value="2"><?php esc_html_e('超级', 'aksrt'); ?></option>
<option value="3"><?php esc_html_e('全局', 'aksrt'); ?></option>
</select>
</label>
<label>
<span class="title"><?php esc_html_e('锁定', 'aksrt'); ?></span>
<select name="aksrt_locked">
<option value="0"><?php esc_html_e('否', 'aksrt'); ?></option>
<option value="1"><?php esc_html_e('是', 'aksrt'); ?></option>
</select>
</label>
<label>
<span class="title"><?php esc_html_e('审核', 'aksrt'); ?></span>
<select name="aksrt_review_status">
<option value="approved"><?php esc_html_e('已通过', 'aksrt'); ?></option>
<option value="pending"><?php esc_html_e('待审核', 'aksrt'); ?></option>
<option value="rejected"><?php esc_html_e('已拒绝', 'aksrt'); ?></option>
</select>
</label>
<?php wp_nonce_field('aksrt_topic_quick_edit', 'aksrt_topic_qe_nonce'); ?>
<input type="hidden" name="aksrt_topic_qe_flag" value="1">
</div>
</fieldset>
<?php
}
public function save_quick_edit(int $post_id, \WP_Post $post): void {
if (!isset($_POST['aksrt_topic_qe_flag'])) {
return;
}
if (!isset($_POST['aksrt_topic_qe_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_topic_qe_nonce'])), 'aksrt_topic_quick_edit')) {
return;
}
if (!current_user_can('edit_post', $post_id)) {
return;
}
if (isset($_POST['aksrt_sticky'])) {
update_post_meta($post_id, '_aksrt_sticky', max(0, min(3, (int) $_POST['aksrt_sticky'])));
}
if (isset($_POST['aksrt_locked'])) {
update_post_meta($post_id, '_aksrt_locked', (int) $_POST['aksrt_locked'] ? 1 : 0);
}
if (isset($_POST['aksrt_review_status'])) {
$status = sanitize_key((string) wp_unslash($_POST['aksrt_review_status']));
if (in_array($status, ['approved', 'pending', 'rejected'], true)) {
update_post_meta($post_id, '_aksrt_review_status', $status);
}
}
}
public function quick_edit_script(): void {
$screen = function_exists('get_current_screen') ? get_current_screen() : null;
if (!$screen || $screen->post_type !== 'aksrt_forum_topic') {
return;
}
?>
<script>
(function($){
var $wp_inline_edit = inlineEditPost.edit;
inlineEditPost.edit = function(id){
$wp_inline_edit.apply(this, arguments);
var postId = 0;
if (typeof(id) === 'object') postId = parseInt(this.getId(id));
if (postId > 0) {
var $row = $('#post-' + postId);
var $data = $('#aksrt-inline-' + postId);
if ($data.length) {
var sticky = $data.data('sticky');
var locked = $data.data('locked');
var review = $data.data('review') || 'approved';
var $edit = $('#edit-' + postId);
$edit.find('select[name="aksrt_sticky"]').val(String(sticky));
$edit.find('select[name="aksrt_locked"]').val(String(locked));
$edit.find('select[name="aksrt_review_status"]').val(String(review));
}
}
};
})(jQuery);
</script>
<?php
}
}
-423
View File
@@ -1,423 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class TopicMetaBox {
private const VISIBILITY = [
'public',
'login',
'comment',
'role',
'credits',
'pay',
'moderator',
];
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 {
if (!is_admin()) {
return;
}
add_action('add_meta_boxes', [$this, 'register_meta_box']);
add_action('save_post_aksrt_forum_topic', [$this, 'save'], 20, 2);
}
public function register_meta_box(): void {
add_meta_box(
'aksrt_topic_governance',
__('帖子治理', 'aksrt'),
[$this, 'render'],
'aksrt_forum_topic',
'side',
'high'
);
}
public function render(\WP_Post $post): void {
wp_nonce_field('aksrt_topic_meta_save', 'aksrt_topic_meta_nonce');
$plate_id = (int) get_post_meta($post->ID, '_aksrt_plate_id', true);
$sticky = (int) get_post_meta($post->ID, '_aksrt_sticky', true);
$locked = (int) get_post_meta($post->ID, '_aksrt_locked', true);
$featured = (int) get_post_meta($post->ID, '_aksrt_featured', true);
$review = (string) get_post_meta($post->ID, '_aksrt_review_status', true);
if ($review === '') {
$review = 'approved';
}
$bbs_type = (string) (get_post_meta($post->ID, '_aksrt_bbs_type', true) ?: 'standard');
$visibility = sanitize_key((string) (get_post_meta($post->ID, '_aksrt_visibility', true) ?: 'public'));
if (!in_array($visibility, self::VISIBILITY, true)) {
$visibility = 'public';
}
$credits_cost = (int) get_post_meta($post->ID, '_aksrt_credits_cost', true);
if ($credits_cost <= 0) {
// Legacy dual key.
$credits_cost = max(0, (int) get_post_meta($post->ID, '_aksrt_pay_credits', true));
}
$pay_price = (float) get_post_meta($post->ID, '_aksrt_pay_price', true);
$pay_vip_price = get_post_meta($post->ID, '_aksrt_pay_vip_price', true);
$vip_set = ($pay_vip_price !== '' && $pay_vip_price !== null && $pay_vip_price !== false);
$vip_price_val = $vip_set ? (float) $pay_vip_price : '';
$pay_vip1_price = get_post_meta($post->ID, '_aksrt_pay_vip1_price', true);
$pay_vip2_price = get_post_meta($post->ID, '_aksrt_pay_vip2_price', true);
$vip1_set = ($pay_vip1_price !== '' && $pay_vip1_price !== null && $pay_vip1_price !== false);
$vip2_set = ($pay_vip2_price !== '' && $pay_vip2_price !== null && $pay_vip2_price !== false);
// Migrate single VIP price into vip1 display if vip1 empty.
if (!$vip1_set && $vip_set) {
$vip1_set = true;
$pay_vip1_price = $pay_vip_price;
}
$credits_vip1 = get_post_meta($post->ID, '_aksrt_credits_vip1', true);
$credits_vip2 = get_post_meta($post->ID, '_aksrt_credits_vip2', true);
$credits_vip1_set = ($credits_vip1 !== '' && $credits_vip1 !== null && $credits_vip1 !== false);
$credits_vip2_set = ($credits_vip2 !== '' && $credits_vip2 !== null && $credits_vip2 !== false);
$virtual_sales = max(0, (int) get_post_meta($post->ID, '_aksrt_virtual_sales', true));
$coupon_s = (int) get_post_meta($post->ID, '_aksrt_topic_coupon_s', true) === 1;
$pay_hide_part = (int) get_post_meta($post->ID, '_aksrt_pay_hide_part', true) === 1;
$view_roles = get_post_meta($post->ID, '_aksrt_visibility_roles', true);
if (!is_array($view_roles)) {
$view_roles = [];
}
$view_roles = array_values(array_filter(array_map('sanitize_key', $view_roles)));
$wp_roles = $this->wp_role_options();
$plates = get_posts([
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => 200,
'orderby' => 'title',
'order' => 'ASC',
]);
$visibility_labels = [
'public' => __('公开', 'aksrt'),
'login' => __('登录可见', 'aksrt'),
'comment' => __('评论可见', 'aksrt'),
'role' => __('指定角色', 'aksrt'),
'credits' => __('支付积分后可见', 'aksrt'),
'pay' => __('付费后可见', 'aksrt'),
'moderator' => __('仅版主', 'aksrt'),
];
?>
<p>
<label for="aksrt_plate_id"><strong><?php esc_html_e('所属版块', 'aksrt'); ?></strong></label>
<select name="aksrt_plate_id" id="aksrt_plate_id" class="widefat">
<option value="0"><?php esc_html_e('未指定', 'aksrt'); ?></option>
<?php foreach ($plates as $plate) : ?>
<option value="<?php echo esc_attr((string) $plate->ID); ?>" <?php selected($plate_id, (int) $plate->ID); ?>>
<?php echo esc_html(get_the_title($plate)); ?>
</option>
<?php endforeach; ?>
</select>
</p>
<p>
<label for="aksrt_sticky"><strong><?php esc_html_e('置顶', 'aksrt'); ?></strong></label>
<select name="aksrt_sticky" id="aksrt_sticky" class="widefat">
<option value="0" <?php selected($sticky, 0); ?>><?php esc_html_e('否', 'aksrt'); ?></option>
<option value="1" <?php selected($sticky, 1); ?>><?php esc_html_e('版块置顶', 'aksrt'); ?></option>
<option value="2" <?php selected($sticky, 2); ?>><?php esc_html_e('超级置顶', 'aksrt'); ?></option>
<option value="3" <?php selected($sticky, 3); ?>><?php esc_html_e('全局置顶', 'aksrt'); ?></option>
</select>
</p>
<p>
<label><input type="checkbox" name="aksrt_locked" value="1" <?php checked($locked, 1); ?>> <?php esc_html_e('锁定回复', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="aksrt_featured" value="1" <?php checked($featured, 1); ?>> <?php esc_html_e('精华', 'aksrt'); ?></label>
</p>
<p>
<label for="aksrt_review_status"><strong><?php esc_html_e('审核状态', 'aksrt'); ?></strong></label>
<select name="aksrt_review_status" id="aksrt_review_status" class="widefat">
<option value="approved" <?php selected($review, 'approved'); ?>><?php esc_html_e('已通过', 'aksrt'); ?></option>
<option value="pending" <?php selected($review, 'pending'); ?>><?php esc_html_e('待审核', 'aksrt'); ?></option>
<option value="rejected" <?php selected($review, 'rejected'); ?>><?php esc_html_e('已拒绝', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="aksrt_bbs_type"><strong><?php esc_html_e('帖子类型', 'aksrt'); ?></strong></label>
<select name="aksrt_bbs_type" id="aksrt_bbs_type" class="widefat">
<option value="standard" <?php selected($bbs_type, 'standard'); ?>><?php esc_html_e('标准', 'aksrt'); ?></option>
<option value="question" <?php selected($bbs_type, 'question'); ?>><?php esc_html_e('提问', 'aksrt'); ?></option>
</select>
</p>
<p>
<label for="aksrt_visibility"><strong><?php esc_html_e('可见性', 'aksrt'); ?></strong></label>
<select name="aksrt_visibility" id="aksrt_visibility" class="widefat">
<?php foreach ($visibility_labels as $k => $label) : ?>
<option value="<?php echo esc_attr($k); ?>" <?php selected($visibility, $k); ?>><?php echo esc_html($label); ?></option>
<?php endforeach; ?>
</select>
</p>
<p class="aksrt-topic-meta-role-fields">
<label for="aksrt_visibility_roles"><strong><?php esc_html_e('可查看角色', 'aksrt'); ?></strong></label>
<select id="aksrt_visibility_roles" name="aksrt_visibility_roles[]" class="widefat" multiple size="4">
<?php foreach ($wp_roles as $role_key => $role_label) : ?>
<option value="<?php echo esc_attr($role_key); ?>" <?php selected(in_array($role_key, $view_roles, true)); ?>>
<?php echo esc_html($role_label); ?>
</option>
<?php endforeach; ?>
</select>
<span class="description"><?php esc_html_e('仅「指定角色」时生效。', 'aksrt'); ?></span>
</p>
<p class="aksrt-topic-meta-credits-fields">
<label for="aksrt_credits_cost"><strong><?php esc_html_e('积分价格', 'aksrt'); ?></strong></label>
<input type="number" step="1" min="0" class="widefat" name="aksrt_credits_cost" id="aksrt_credits_cost" value="<?php echo esc_attr((string) $credits_cost); ?>">
<span class="description"><?php esc_html_e('可见性为「支付积分后可见」时使用。', 'aksrt'); ?></span>
</p>
<p class="aksrt-topic-meta-credits-fields">
<label for="aksrt_credits_vip1"><strong><?php esc_html_e('VIP1 积分价', 'aksrt'); ?></strong></label>
<input type="number" step="1" min="0" class="widefat" name="aksrt_credits_vip1" id="aksrt_credits_vip1" value="<?php echo esc_attr($credits_vip1_set ? (string) (int) $credits_vip1 : ''); ?>" placeholder="<?php esc_attr_e('留空=不设', 'aksrt'); ?>">
</p>
<p class="aksrt-topic-meta-credits-fields">
<label for="aksrt_credits_vip2"><strong><?php esc_html_e('VIP2 积分价', 'aksrt'); ?></strong></label>
<input type="number" step="1" min="0" class="widefat" name="aksrt_credits_vip2" id="aksrt_credits_vip2" value="<?php echo esc_attr($credits_vip2_set ? (string) (int) $credits_vip2 : ''); ?>" placeholder="<?php esc_attr_e('留空=不设;0=免费', 'aksrt'); ?>">
</p>
<p class="aksrt-topic-meta-pay-fields">
<label for="aksrt_pay_price"><strong><?php esc_html_e('付费价格(元)', 'aksrt'); ?></strong></label>
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_pay_price" id="aksrt_pay_price" value="<?php echo esc_attr((string) $pay_price); ?>">
</p>
<p class="aksrt-topic-meta-pay-fields">
<label for="aksrt_pay_vip1_price"><strong><?php esc_html_e('VIP1 价格(元)', 'aksrt'); ?></strong></label>
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_pay_vip1_price" id="aksrt_pay_vip1_price" value="<?php echo esc_attr($vip1_set ? (string) (float) $pay_vip1_price : ''); ?>" placeholder="<?php esc_attr_e('留空=不设 VIP1 价', 'aksrt'); ?>">
</p>
<p class="aksrt-topic-meta-pay-fields">
<label for="aksrt_pay_vip2_price"><strong><?php esc_html_e('VIP2 价格(元)', 'aksrt'); ?></strong></label>
<input type="number" step="0.01" min="0" class="widefat" name="aksrt_pay_vip2_price" id="aksrt_pay_vip2_price" value="<?php echo esc_attr($vip2_set ? (string) (float) $pay_vip2_price : ''); ?>" placeholder="<?php esc_attr_e('留空=不设;0=VIP2 免费', 'aksrt'); ?>">
<span class="description"><?php esc_html_e('兼容旧「VIP 价格」字段:未填 VIP1 时仍可读 _aksrt_pay_vip_price。', 'aksrt'); ?></span>
</p>
<p class="aksrt-topic-meta-pay-hide-fields">
<label>
<input type="checkbox" name="aksrt_pay_hide_part" value="1" <?php checked($pay_hide_part); ?>>
<?php esc_html_e('仅隐藏部分内容', 'aksrt'); ?>
</label>
<span class="description" style="display:block;"><?php esc_html_e('开启后正文仍输出,付费块用 [payshow] 或 [hidecontent type="payshow"] 包裹。未开启则全文门控。', 'aksrt'); ?></span>
</p>
<p class="aksrt-topic-meta-pay-fields aksrt-topic-meta-credits-fields">
<label for="aksrt_virtual_sales"><strong><?php esc_html_e('虚拟销量', 'aksrt'); ?></strong></label>
<input type="number" step="1" min="0" class="widefat" name="aksrt_virtual_sales" id="aksrt_virtual_sales" value="<?php echo esc_attr((string) $virtual_sales); ?>">
<span class="description"><?php esc_html_e('展示用浮动销量,不计入真实订单统计。', 'aksrt'); ?></span>
</p>
<p class="aksrt-topic-meta-pay-fields">
<label>
<input type="checkbox" name="aksrt_topic_coupon_s" value="1" <?php checked($coupon_s); ?>>
<?php esc_html_e('允许优惠码', 'aksrt'); ?>
</label>
<span class="description" style="display:block;"><?php esc_html_e('开启后现金收银台可输入优惠码;积分解锁不吃券。', 'aksrt'); ?></span>
</p>
<?php
}
public function save(int $post_id, \WP_Post $post): void {
if (!isset($_POST['aksrt_topic_meta_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_topic_meta_nonce'])), 'aksrt_topic_meta_save')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if ($post->post_type !== 'aksrt_forum_topic' || !current_user_can('edit_post', $post_id)) {
return;
}
$plate_id = isset($_POST['aksrt_plate_id']) ? max(0, (int) $_POST['aksrt_plate_id']) : 0;
update_post_meta($post_id, '_aksrt_plate_id', $plate_id);
$sticky = isset($_POST['aksrt_sticky']) ? max(0, min(3, (int) $_POST['aksrt_sticky'])) : 0;
update_post_meta($post_id, '_aksrt_sticky', $sticky);
update_post_meta($post_id, '_aksrt_locked', !empty($_POST['aksrt_locked']) ? 1 : 0);
update_post_meta($post_id, '_aksrt_featured', !empty($_POST['aksrt_featured']) ? 1 : 0);
$review = isset($_POST['aksrt_review_status']) ? sanitize_key((string) wp_unslash($_POST['aksrt_review_status'])) : 'approved';
if (!in_array($review, ['approved', 'pending', 'rejected'], true)) {
$review = 'approved';
}
update_post_meta($post_id, '_aksrt_review_status', $review);
$bbs_type = isset($_POST['aksrt_bbs_type']) ? sanitize_key((string) wp_unslash($_POST['aksrt_bbs_type'])) : 'standard';
if (!in_array($bbs_type, ['standard', 'question'], true)) {
$bbs_type = 'standard';
}
update_post_meta($post_id, '_aksrt_bbs_type', $bbs_type);
$this->save_visibility_meta($post_id);
// Keep board taxonomy bridge roughly in sync when plate selected.
if ($plate_id > 0 && taxonomy_exists('aksrt_forum_board')) {
$slug = 'plate-' . $plate_id;
$term = get_term_by('slug', $slug, 'aksrt_forum_board');
if (!$term) {
$title = get_the_title($plate_id) ?: $slug;
$created = wp_insert_term($title, 'aksrt_forum_board', ['slug' => $slug]);
if (!is_wp_error($created) && !empty($created['term_id'])) {
$term_id = (int) $created['term_id'];
} else {
$term_id = 0;
}
} else {
$term_id = (int) $term->term_id;
}
if ($term_id > 0) {
wp_set_object_terms($post_id, [$term_id], 'aksrt_forum_board', false);
}
}
}
private function save_visibility_meta(int $post_id): void {
$visibility = isset($_POST['aksrt_visibility']) ? sanitize_key((string) wp_unslash($_POST['aksrt_visibility'])) : 'public';
if (!in_array($visibility, self::VISIBILITY, true)) {
$visibility = 'public';
}
update_post_meta($post_id, '_aksrt_visibility', $visibility);
// Rewrite legacy dual allow_view so admin/runtime stay on one contract.
$allow_view_map = [
'public' => 'public',
'login' => 'login',
'comment' => 'login',
'role' => 'public',
'credits' => 'pay',
'pay' => 'pay',
'moderator' => 'moderator',
];
update_post_meta($post_id, '_aksrt_allow_view', $allow_view_map[$visibility] ?? 'public');
// Roles.
if ($visibility === 'role') {
$roles = $this->sanitize_role_list($_POST['aksrt_visibility_roles'] ?? []);
update_post_meta($post_id, '_aksrt_visibility_roles', $roles);
} else {
delete_post_meta($post_id, '_aksrt_visibility_roles');
}
// Credits (canonical + legacy dual-write).
if ($visibility === 'credits') {
$cost = isset($_POST['aksrt_credits_cost']) ? max(0, (int) wp_unslash($_POST['aksrt_credits_cost'])) : 0;
// Accept legacy field name if UI still posts it.
if ($cost <= 0 && isset($_POST['aksrt_pay_credits'])) {
$cost = max(0, (int) wp_unslash($_POST['aksrt_pay_credits']));
}
update_post_meta($post_id, '_aksrt_credits_cost', $cost);
update_post_meta($post_id, '_aksrt_pay_credits', $cost);
if (isset($_POST['aksrt_credits_vip1']) && (string) wp_unslash($_POST['aksrt_credits_vip1']) !== '') {
update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) wp_unslash($_POST['aksrt_credits_vip1'])));
} else {
delete_post_meta($post_id, '_aksrt_credits_vip1');
}
if (isset($_POST['aksrt_credits_vip2']) && (string) wp_unslash($_POST['aksrt_credits_vip2']) !== '') {
update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) wp_unslash($_POST['aksrt_credits_vip2'])));
} else {
delete_post_meta($post_id, '_aksrt_credits_vip2');
}
} else {
delete_post_meta($post_id, '_aksrt_credits_cost');
delete_post_meta($post_id, '_aksrt_pay_credits');
delete_post_meta($post_id, '_aksrt_credits_vip1');
delete_post_meta($post_id, '_aksrt_credits_vip2');
}
// Cash pay.
if ($visibility === 'pay') {
$price = isset($_POST['aksrt_pay_price']) ? max(0, (float) wp_unslash($_POST['aksrt_pay_price'])) : 0;
update_post_meta($post_id, '_aksrt_pay_price', $price);
$vip1_written = false;
if (isset($_POST['aksrt_pay_vip1_price']) && (string) wp_unslash($_POST['aksrt_pay_vip1_price']) !== '') {
$vip1 = max(0, (float) wp_unslash($_POST['aksrt_pay_vip1_price']));
update_post_meta($post_id, '_aksrt_pay_vip1_price', $vip1);
// Keep legacy single VIP key in sync with VIP1 for older readers.
update_post_meta($post_id, '_aksrt_pay_vip_price', $vip1);
$vip1_written = true;
} else {
delete_post_meta($post_id, '_aksrt_pay_vip1_price');
}
if (isset($_POST['aksrt_pay_vip2_price']) && (string) wp_unslash($_POST['aksrt_pay_vip2_price']) !== '') {
update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) wp_unslash($_POST['aksrt_pay_vip2_price'])));
} else {
delete_post_meta($post_id, '_aksrt_pay_vip2_price');
}
// Legacy field if posted without vip1.
if (!$vip1_written && isset($_POST['aksrt_pay_vip_price']) && (string) wp_unslash($_POST['aksrt_pay_vip_price']) !== '') {
$vip = max(0, (float) wp_unslash($_POST['aksrt_pay_vip_price']));
update_post_meta($post_id, '_aksrt_pay_vip_price', $vip);
update_post_meta($post_id, '_aksrt_pay_vip1_price', $vip);
} elseif (!$vip1_written) {
delete_post_meta($post_id, '_aksrt_pay_vip_price');
}
} else {
delete_post_meta($post_id, '_aksrt_pay_price');
delete_post_meta($post_id, '_aksrt_pay_vip_price');
delete_post_meta($post_id, '_aksrt_pay_vip1_price');
delete_post_meta($post_id, '_aksrt_pay_vip2_price');
}
// Partial hide only meaningful for pay/credits; otherwise clear.
if (in_array($visibility, ['pay', 'credits'], true)) {
update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($_POST['aksrt_pay_hide_part']) ? 1 : 0);
$virtual = isset($_POST['aksrt_virtual_sales']) ? max(0, (int) wp_unslash($_POST['aksrt_virtual_sales'])) : 0;
if ($virtual > 0) {
update_post_meta($post_id, '_aksrt_virtual_sales', $virtual);
} else {
delete_post_meta($post_id, '_aksrt_virtual_sales');
}
if ($visibility === 'pay') {
update_post_meta($post_id, '_aksrt_topic_coupon_s', !empty($_POST['aksrt_topic_coupon_s']) ? 1 : 0);
} else {
delete_post_meta($post_id, '_aksrt_topic_coupon_s');
}
} else {
delete_post_meta($post_id, '_aksrt_pay_hide_part');
delete_post_meta($post_id, '_aksrt_virtual_sales');
delete_post_meta($post_id, '_aksrt_topic_coupon_s');
}
}
/**
* @param mixed $raw
* @return string[]
*/
private function sanitize_role_list($raw): array {
if (!is_array($raw)) {
return [];
}
$allowed = array_keys($this->wp_role_options());
$out = [];
foreach ($raw as $role) {
$role = sanitize_key((string) $role);
if ($role !== '' && in_array($role, $allowed, true)) {
$out[] = $role;
}
}
return array_values(array_unique($out));
}
/**
* @return array<string, string>
*/
private function wp_role_options(): array {
if (!function_exists('wp_roles')) {
return [];
}
$roles = wp_roles();
if (!$roles || !is_array($roles->roles)) {
return [];
}
$out = [];
foreach ($roles->roles as $key => $data) {
$out[sanitize_key((string) $key)] = (string) ($data['name'] ?? $key);
}
return $out;
}
}
-853
View File
@@ -1,853 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class TopicPay {
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();
$post = get_post($topic_id);
$allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post);
if ($uid <= 0 && !$allow_guest) {
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';
}
// Guests cannot use balance.
if ($uid <= 0 && in_array($payment_method, ['balance', 'points'], true)) {
$payment_method = 'alipay';
}
$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();
$coupon_code = sanitize_text_field($coupon_code);
$allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post);
if ($uid <= 0 && !$allow_guest) {
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);
$list_amount = round((float) ($priced['amount'] ?? 0), 2);
$base = round((float) ($priced['base'] ?? $list_amount), 2);
$vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price']));
// Guests never get VIP free (vip_level=0).
if ($uid <= 0) {
$vip_free = false;
}
$has_paid = $this->user_has_paid($uid, $topic_id);
$wallet_balance = 0.0;
if ($uid > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled()) {
$wallet_balance = (float) \AKSRT\Credits\Balance::balance($uid);
}
$methods = function_exists('aksrt_list_cash_payment_methods')
? aksrt_list_cash_payment_methods($uid > 0)
: [];
$coupon_allowed = $this->is_coupon_allowed($topic_id);
$coupon_error = '';
$coupon_discount = 0.0;
$coupon_meta = null;
$applied_code = '';
$payable = $list_amount;
if (!$vip_free && $list_amount > 0) {
$discounted = $this->apply_topic_cash_discounts($priced, $topic_id, $uid, $coupon_code, true);
if (is_wp_error($discounted)) {
$coupon_error = $discounted->get_error_message();
$discounted = $this->apply_topic_cash_discounts($priced, $topic_id, $uid, '', true);
}
if (!is_wp_error($discounted)) {
$list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2);
$payable = round((float) ($discounted['payable'] ?? $list_amount), 2);
$coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2);
$coupon_meta = !empty($discounted['coupon']) ? $discounted['coupon'] : null;
$applied_code = (string) ($discounted['coupon_code'] ?? ($coupon_meta['code'] ?? ''));
if ($coupon_error === '' && !empty($discounted['coupon_error'])) {
$coupon_error = (string) $discounted['coupon_error'];
}
}
} elseif ($vip_free) {
$payable = 0.0;
$list_amount = 0.0;
}
$is_free = $vip_free || $payable <= 0;
$amount_label = $is_free
? __('免费', 'aksrt')
: sprintf('¥%s', number_format($payable, 2, '.', ''));
$list_label = sprintf('¥%s', number_format($list_amount > 0 ? $list_amount : $base, 2, '.', ''));
$base_label = sprintf('¥%s', number_format($base, 2, '.', ''));
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' => $payable,
'base' => $base,
'list_amount' => $list_amount > 0 ? $list_amount : $base,
'list_amount_label' => $list_label,
'payable' => $payable,
'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' => $uid <= 0 && !$allow_guest,
'allow_guest' => $allow_guest,
'need_vip' => false,
'need_purchase' => !$has_paid && !$is_free,
'logged_in' => $uid > 0,
'balance' => $wallet_balance,
'wallet_balance' => $wallet_balance,
'balance_enough' => $is_free || ($wallet_balance + 0.0001 >= (float) $payable),
'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_allowed ? __('支持优惠码', 'aksrt') : '',
'coupon_code' => $applied_code,
'coupon' => $coupon_meta,
'coupon_error' => $coupon_error,
'coupon_discount' => $coupon_discount,
'coupon_discount_label' => $coupon_discount > 0
? ('-' . sprintf('¥%s', number_format($coupon_discount, 2, '.', '')))
: '',
'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 = '') {
$assert = $this->assert_topic($topic_id);
if (is_wp_error($assert)) {
return $assert;
}
/** @var \WP_Post $post */
$post = $assert['post'];
$allow_guest = function_exists('aksrt_allow_guest_forum_pay') && aksrt_allow_guest_forum_pay($post);
if ($user_id <= 0 && !$allow_guest) {
return new \WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), [
'status' => 401,
'login_url' => wp_login_url(get_permalink($post)),
]);
}
if ($this->user_has_paid($user_id, $topic_id)) {
return new \WP_Error('aksrt_already_paid', __('已购买,可直接查看。', 'aksrt'), [
'status' => 400,
'has_paid_access' => true,
'reload' => true,
]);
}
$priced = $this->resolve_price($topic_id, $user_id);
$list_amount = round((float) ($priced['amount'] ?? 0), 2);
$base = round((float) ($priced['base'] ?? $list_amount), 2);
$vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price']));
if ($user_id <= 0) {
$vip_free = false;
}
$coupon_code = sanitize_text_field($coupon_code);
// VIP free (no purchase price) — no coupon stack. Guests never hit this.
if ($vip_free) {
return $this->create_free_paid_order($topic_id, $user_id, $post, $priced, [
'list_amount' => 0.0,
'payable' => 0.0,
'coupon_code' => '',
'coupon_discount' => 0.0,
'coupon' => [],
'message' => __('VIP 免费解锁成功', 'aksrt'),
'paid_note' => __('VIP 免费解锁主题。', 'aksrt'),
]);
}
if ($list_amount <= 0) {
return new \WP_Error('aksrt_price_unset', __('价格未设置', 'aksrt'), ['status' => 400]);
}
$discounted = $this->apply_topic_cash_discounts($priced, $topic_id, $user_id, $coupon_code, false);
if (is_wp_error($discounted)) {
return $discounted;
}
$list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2);
$amount = round((float) ($discounted['payable'] ?? $list_amount), 2);
$coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2);
$coupon_meta = is_array($discounted['coupon'] ?? null) ? $discounted['coupon'] : [];
$coupon_code_norm = (string) ($coupon_meta['code'] ?? $discounted['coupon_code'] ?? $coupon_code);
// Free after coupon discount.
if ($amount <= 0) {
return $this->create_free_paid_order($topic_id, $user_id, $post, $priced, [
'list_amount' => $list_amount,
'payable' => 0.0,
'coupon_code' => $coupon_code_norm,
'coupon_discount' => $coupon_discount,
'coupon' => $coupon_meta,
'message' => __('无需支付,已可访问。', 'aksrt'),
'paid_note' => __('优惠后无需支付。', 'aksrt'),
]);
}
if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) {
return new \WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
}
$payment_method = sanitize_key($payment_method);
if ($payment_method === '') {
$payment_method = $user_id > 0 ? 'balance' : 'alipay';
}
if ($user_id <= 0 && in_array($payment_method, ['balance', 'points'], true)) {
$payment_method = 'alipay';
}
$pending = $this->find_pending_topic_order($user_id, $topic_id);
$can_resume = false;
if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) {
$pending_meta = $this->decode_order_note_meta($pending);
$pending_coupon = (string) ($pending_meta['coupon_code'] ?? '');
if (is_array($pending_meta['coupon'] ?? null)) {
$pending_coupon = (string) ($pending_meta['coupon']['code'] ?? $pending_coupon);
}
if ($pending_coupon === $coupon_code_norm) {
$can_resume = true;
}
}
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' => $list_amount,
'amount' => $amount,
'payable' => $amount,
'coupon_code' => $coupon_code_norm,
'coupon_discount' => $coupon_discount,
'coupon' => $coupon_meta,
'guest' => $user_id <= 0 ? 1 : 0,
'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\Payment\OrderRepository::get_instance()->create([
'user_id' => max(0, $user_id),
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
'product_id' => $topic_id,
'amount' => $amount,
'currency' => 'CNY',
'status' => \AKSRT\Payment\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\Payment\OrderRepository::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]);
}
$resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method);
if (empty($resolved['gateway'])) {
return new \WP_Error(
(string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'),
(string) ($resolved['message'] ?? __('当前支付方式不可用。', 'aksrt')),
['status' => 400, 'order_number' => $order_number]
);
}
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [
'paid_note' => __('用户完成论坛主题购买支付。', 'aksrt'),
]);
if (empty($payment_result['success'])) {
return new \WP_Error(
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
(string) ($payment_result['message'] ?? __('支付失败。', 'aksrt')),
['status' => 402, 'order_number' => $order_number]
);
}
if (!empty($payment_result['redirect'])) {
return [
'success' => true,
'data' => [
'redirect' => true,
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
'order_number' => $order_number,
'amount' => $amount,
'pay_modo' => '0',
'message' => $payment_result['message'],
'reload' => false,
'poll' => true,
],
];
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => $amount,
'pay_modo' => '0',
'message' => $payment_result['message'],
'reload' => true,
],
];
}
/**
* On paid: unlock topic (or guest cookie) + burn coupon (idempotent).
*/
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\Core\DomainModel::ORDER_PRODUCT_RESOURCE) {
return;
}
$topic_id = (int) ($paid_order['product_id'] ?? 0);
$user_id = (int) ($paid_order['user_id'] ?? 0);
if ($topic_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;
}
}
$fulfilled = $user_id <= 0;
if ($user_id > 0) {
$this->mark_unlocked($user_id, $topic_id);
$fulfilled = class_exists('AKSRT\\Forum\\Forum')
? Forum::get_instance()->has_user_unlocked_topic($user_id, $topic_id)
: in_array($topic_id, array_map('intval', (array) get_user_meta($user_id, 'aksrt_unlocked_topics', true)), true);
} elseif (function_exists('aksrt_set_guest_pay_cookies')) {
if (class_exists('\AKSRT\Payment\OrderRepository')) {
$full = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (is_array($full)) {
$paid_order = array_merge($full, $paid_order);
}
}
aksrt_set_guest_pay_cookies($paid_order);
}
$this->finalize_topic_order_side_effects($order_number, $paid_order);
if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) {
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
$order_number,
\AKSRT\Payment\Order::STATUS_COMPLETED,
['note' => __('付费主题权益已发放。', 'aksrt')]
);
}
}
/**
* @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 !== 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\\Forum')) {
return 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 ($user_id <= 0) {
return function_exists('aksrt_guest_has_paid_access') && aksrt_guest_has_paid_access($topic_id);
}
if (class_exists('AKSRT\\Forum\\Forum')) {
return Forum::get_instance()->has_user_paid_topic($user_id, $topic_id);
}
if (class_exists('\AKSRT\Payment\Order')) {
return \AKSRT\Payment\Order::get_instance()->has_paid_access(
$user_id,
\AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
$topic_id
);
}
return false;
}
private function mark_unlocked(int $user_id, int $topic_id): void {
if (class_exists('AKSRT\\Forum\\Forum') && method_exists('AKSRT\\Forum\\Forum', 'mark_topic_unlocked')) {
Forum::get_instance()->mark_topic_unlocked($user_id, $topic_id);
return;
}
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 {
return (int) get_post_meta($topic_id, '_aksrt_topic_coupon_s', true) === 1;
}
/**
* VIP list price → coupon → payable. No affiliate rebate on topics.
*
* @param array<string,mixed> $priced
* @return array|\WP_Error
*/
private function apply_topic_cash_discounts(array $priced, int $topic_id, int $user_id, string $coupon_code = '', bool $soft_coupon = false) {
$list_amount = round(max(0.0, (float) ($priced['amount'] ?? 0)), 2);
$pay = $list_amount;
$coupon_discount = 0.0;
$coupon_meta = [];
$coupon_error = '';
$coupon_code = sanitize_text_field($coupon_code);
if ($coupon_code !== '') {
if (!$this->is_coupon_allowed($topic_id)) {
$err = new \WP_Error('aksrt_coupon_not_allowed', __('当前主题不支持优惠码。', 'aksrt'), ['status' => 400]);
if (!$soft_coupon) {
return $err;
}
$coupon_error = $err->get_error_message();
$coupon_code = '';
} elseif (!class_exists('\AKSRT\Payment\Coupon')) {
$err = new \WP_Error('aksrt_coupon_unavailable', __('优惠券系统不可用。', 'aksrt'), ['status' => 500]);
if (!$soft_coupon) {
return $err;
}
$coupon_error = $err->get_error_message();
$coupon_code = '';
} else {
$result = \AKSRT\Payment\Coupon::get_instance()->validate($coupon_code, $pay, [
'user_id' => max(0, $user_id),
'product_ids' => [$topic_id],
'context' => 'topic',
]);
if (empty($result['valid'])) {
$err = new \WP_Error(
'aksrt_coupon_error',
(string) ($result['message'] ?? __('优惠码不可用。', 'aksrt')),
['status' => 400]
);
if (!$soft_coupon) {
return $err;
}
$coupon_error = $err->get_error_message();
$coupon_code = '';
} else {
$coupon_discount = round(max(0.0, (float) ($result['discount'] ?? 0)), 2);
if ($coupon_discount > $pay) {
$coupon_discount = $pay;
}
$pay = round(max(0.0, $pay - $coupon_discount), 2);
$c = is_array($result['coupon'] ?? null) ? $result['coupon'] : [];
$coupon_meta = [
'id' => (int) ($c['id'] ?? 0),
'code' => (string) ($c['code'] ?? $coupon_code),
'type' => (string) ($c['type'] ?? ''),
'value' => (float) ($c['value'] ?? 0),
'discount' => $coupon_discount,
];
$coupon_code = (string) $coupon_meta['code'];
}
}
}
return [
'list_amount' => $list_amount,
'coupon_discount' => $coupon_discount,
'coupon' => $coupon_meta,
'coupon_code' => $coupon_code,
'coupon_error' => $coupon_error,
'payable' => round(max(0.0, $pay), 2),
];
}
/**
* Burn coupon after paid (idempotent via coupon_usage.order_number).
*/
private function finalize_topic_order_side_effects(string $order_number, array $order): void {
$meta = $this->decode_order_note_meta($order);
if (empty($meta['topic_pay'])) {
return;
}
if (($meta['pay_modo'] ?? '') === 'points') {
return;
}
$buyer_id = (int) ($order['user_id'] ?? 0);
if (function_exists('aksrt_burn_order_coupon')) {
aksrt_burn_order_coupon($order_number, $meta, $buyer_id);
}
}
/**
* Free VIP or free-after-discount paid order.
*
* @param array{
* list_amount?:float,
* payable?:float,
* coupon_code?:string,
* coupon_discount?:float,
* coupon?:array,
* message?:string,
* paid_note?:string
* } $opts
* @return array|\WP_Error
*/
private function create_free_paid_order(int $topic_id, int $user_id, \WP_Post $post, array $priced, array $opts = []) {
$list_amount = round((float) ($opts['list_amount'] ?? 0), 2);
$payable = round((float) ($opts['payable'] ?? 0), 2);
$coupon_code = sanitize_text_field((string) ($opts['coupon_code'] ?? ''));
$coupon_discount = round((float) ($opts['coupon_discount'] ?? 0), 2);
$coupon_meta = is_array($opts['coupon'] ?? null) ? $opts['coupon'] : [];
$message = (string) ($opts['message'] ?? __('VIP 免费解锁成功', 'aksrt'));
$paid_note = (string) ($opts['paid_note'] ?? __('VIP 免费解锁主题。', 'aksrt'));
if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) {
$this->mark_unlocked($user_id, $topic_id);
return [
'success' => true,
'data' => [
'order_number' => '',
'amount' => 0,
'pay_modo' => '0',
'free' => true,
'message' => $message,
'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' => $list_amount,
'amount' => $payable,
'payable' => $payable,
'coupon_code' => $coupon_code,
'coupon_discount' => $coupon_discount,
'coupon' => $coupon_meta,
'free' => 1,
'guest' => $user_id <= 0 ? 1 : 0,
'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\Payment\OrderRepository::get_instance()->create([
'user_id' => max(0, $user_id),
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
'product_id' => $topic_id,
'amount' => 0.0,
'currency' => 'CNY',
'status' => \AKSRT\Payment\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' => $message,
'reload' => true,
],
];
}
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
'payment_method' => 'free',
'payment_transaction_id' => 'free_' . $order_number,
'note' => $paid_note,
]);
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' => 0,
'pay_modo' => '0',
'free' => true,
'message' => $message,
'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\Payment\OrderRepository')) {
return null;
}
$orders = \AKSRT\Payment\OrderRepository::get_instance()->get_user_orders($user_id, [
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE,
'status' => \AKSRT\Core\DomainModel::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 {
if (isset($order['meta_payload']) && is_array($order['meta_payload']) && $order['meta_payload'] !== []) {
return $order['meta_payload'];
}
$note = (string) ($order['note'] ?? '');
if ($note === '') {
return [];
}
$decoded = json_decode($note, true);
return is_array($decoded) ? $decoded : [];
}
}
-275
View File
@@ -1,275 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class TopicService {
use TopicServicePart1;
use TopicServicePart2;
use TopicServicePart3;
use TopicServicePart4;
private Forum $forum;
public function __construct(Forum $forum) {
$this->forum = $forum;
}
// ================================================================
// Topic CRUD
// ================================================================
/**
* Update topic content and editable governance metadata.
*
* @return bool|\WP_Error
*/
public function update_topic(int $topic_id, array $data) {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt'));
}
$update = ['ID' => $topic_id];
if (isset($data['title'])) {
$title = sanitize_text_field((string) $data['title']);
if ($title === '') {
return new \WP_Error('empty_title', __('标题不能为空', 'aksrt'));
}
$update['post_title'] = $title;
}
if (isset($data['content'])) {
$content = wp_kses_post((string) $data['content']);
if ($content === '') {
return new \WP_Error('empty_content', __('内容不能为空', 'aksrt'));
}
$update['post_content'] = $content;
}
if (isset($data['excerpt'])) {
$update['post_excerpt'] = sanitize_textarea_field((string) $data['excerpt']);
}
$result = wp_update_post($update, true);
if (is_wp_error($result)) {
return $result;
}
if (isset($data['tags']) && is_array($data['tags'])) {
$tags = array_values(array_filter(array_map('sanitize_text_field', $data['tags'])));
wp_set_object_terms($topic_id, $tags, 'aksrt_forum_tag');
}
$this->save_topic_governance_meta($topic_id, $data);
return true;
}
/**
* Create topic.
*
* @return array<string,mixed>|\WP_Error
*/
public function create_topic(int $user_id, array $data) {
if ($user_id <= 0) {
return new \WP_Error('not_logged_in', __('请先登录', 'aksrt'));
}
$board_id = (int) ($data['board_id'] ?? 0);
$title = sanitize_text_field((string) ($data['title'] ?? ''));
$content = wp_kses_post((string) ($data['content'] ?? ''));
$visibility = sanitize_key((string) ($data['visibility'] ?? Forum::VISIBILITY_PUBLIC));
$special_topic = sanitize_text_field((string) ($data['special_topic'] ?? ''));
$tags = is_array($data['tags'] ?? null) ? $data['tags'] : [];
$bbs_type = sanitize_key((string) ($data['bbs_type'] ?? Forum::BBS_TYPE_STANDARD));
if (!in_array($bbs_type, [Forum::BBS_TYPE_STANDARD, Forum::BBS_TYPE_QUESTION], true)) {
$bbs_type = Forum::BBS_TYPE_STANDARD;
}
if ($board_id <= 0 || $title === '' || $content === '') {
return new \WP_Error('invalid_payload', __('请填写所有必填项', 'aksrt'));
}
// §7.3: Strip unauthorized media from content based on compose capabilities
$content = $this->sanitize_topic_content($user_id, $content);
$board_ok = $this->assert_board($board_id);
if (is_wp_error($board_ok)) {
return $board_ok;
}
if (!isset($this->get_visibility_map()[$visibility])) {
$visibility = Forum::VISIBILITY_PUBLIC;
}
$can_publish = $this->can_user_publish_directly($user_id, $board_id);
$post_status = $can_publish ? 'publish' : 'pending';
$review_status = $can_publish ? Forum::REVIEW_APPROVED : Forum::REVIEW_PENDING;
$post_id = wp_insert_post([
'post_title' => $title,
'post_content' => $content,
'post_status' => $post_status,
'post_author' => $user_id,
'post_type' => 'aksrt_forum_topic',
], true);
if (is_wp_error($post_id)) {
return $post_id;
}
$this->attach_topic_to_board((int) $post_id, $board_id);
$clean_tags = array_values(array_filter(array_map('sanitize_text_field', $tags)));
if (count($clean_tags) > 5) {
$clean_tags = array_slice($clean_tags, 0, 5);
}
if (!empty($clean_tags)) {
wp_set_object_terms($post_id, $clean_tags, 'aksrt_forum_tag');
}
$topic_cat = sanitize_text_field((string) ($data['topic_cat'] ?? ''));
if ($topic_cat !== '' && taxonomy_exists('aksrt_forum_topic_cat')) {
wp_set_object_terms($post_id, [$topic_cat], 'aksrt_forum_topic_cat');
}
update_post_meta($post_id, '_aksrt_visibility', $visibility);
update_post_meta($post_id, '_aksrt_review_status', $review_status);
if ($visibility === Forum::VISIBILITY_CREDITS) {
$credits_cost = max(0, (int) ($data['credits_cost'] ?? 0));
update_post_meta($post_id, '_aksrt_credits_cost', $credits_cost);
update_post_meta($post_id, '_aksrt_pay_credits', $credits_cost);
if (isset($data['credits_vip1']) && (string) $data['credits_vip1'] !== '') {
update_post_meta($post_id, '_aksrt_credits_vip1', max(0, (int) $data['credits_vip1']));
}
if (isset($data['credits_vip2']) && (string) $data['credits_vip2'] !== '') {
update_post_meta($post_id, '_aksrt_credits_vip2', max(0, (int) $data['credits_vip2']));
}
update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0);
}
if ($visibility === Forum::VISIBILITY_PAY) {
$pay_price = sanitize_text_field((string) ($data['pay_price'] ?? ''));
update_post_meta($post_id, '_aksrt_pay_price', $pay_price);
if (isset($data['pay_vip1_price']) && (string) $data['pay_vip1_price'] !== '') {
$v1 = max(0, (float) $data['pay_vip1_price']);
update_post_meta($post_id, '_aksrt_pay_vip1_price', $v1);
update_post_meta($post_id, '_aksrt_pay_vip_price', $v1);
}
if (isset($data['pay_vip2_price']) && (string) $data['pay_vip2_price'] !== '') {
update_post_meta($post_id, '_aksrt_pay_vip2_price', max(0, (float) $data['pay_vip2_price']));
}
update_post_meta($post_id, '_aksrt_pay_hide_part', !empty($data['pay_hide_part']) ? 1 : 0);
update_post_meta($post_id, '_aksrt_topic_coupon_s', !empty($data['topic_coupon_s']) ? 1 : 0);
}
if ($visibility === Forum::VISIBILITY_ROLE) {
$roles = isset($data['visibility_roles']) && is_array($data['visibility_roles'])
? array_map('sanitize_key', $data['visibility_roles']) : [];
update_post_meta($post_id, '_aksrt_visibility_roles', $roles);
}
update_post_meta($post_id, '_aksrt_special_topic', $special_topic);
// 置顶分级 0~3(§7.3):新建主题默认不置顶。
update_post_meta($post_id, '_aksrt_sticky', Forum::STICKY_NONE);
update_post_meta($post_id, '_aksrt_featured', '0');
update_post_meta($post_id, '_aksrt_locked', '0');
update_post_meta($post_id, '_aksrt_views', 0);
update_post_meta($post_id, '_aksrt_last_reply', (string) get_post_field('post_date', $post_id));
update_post_meta($post_id, '_aksrt_bbs_type', $bbs_type);
if ($bbs_type === Forum::BBS_TYPE_QUESTION) {
update_post_meta($post_id, '_aksrt_question_status', Forum::QUESTION_UNSOLVED);
}
$this->save_cover_meta($post_id, $data);
$vote_meta = $this->prepare_vote_meta($data);
$this->save_vote_meta($post_id, $vote_meta);
if ($post_status === 'publish') {
$this->forum->sync_board_count($board_id);
}
if (class_exists('\AKSRT\Credits\Earn') && $post_status === 'publish') {
do_action('aksrt_forum_topic_created', $user_id, $post_id);
}
if ($post_status === 'pending') {
$this->forum->notify_topic_pending($post_id, $user_id);
}
return [
'post_id' => $post_id,
'message' => $post_status === 'publish'
? __('主题已发布', 'aksrt')
: __('主题已提交审核,审核通过后对外展示', 'aksrt'),
'redirect' => $post_status === 'publish'
? get_permalink($post_id)
: add_query_arg('compose_status', 'pending', aksrt_get_forum_home_url()),
];
}
// ================================================================
// Board / plate attachment helpers
// ================================================================
/**
* Validate board_id as plate CPT post ID when plate is active, else taxonomy term.
*
* @return true|\WP_Error
*/
private function assert_board(int $board_id, string $error_message = '') {
if ($error_message === '') {
$error_message = __('版块不存在', 'aksrt');
}
if ($board_id <= 0) {
return new \WP_Error('invalid_board', $error_message);
}
if ($this->forum->is_plate_cpt_active()) {
$post = get_post($board_id);
if (!$post || $post->post_type !== 'aksrt_forum_plate' || $post->post_status !== 'publish') {
return new \WP_Error('invalid_board', $error_message);
}
return true;
}
$board = get_term($board_id, 'aksrt_forum_board');
if (!$board || is_wp_error($board)) {
return new \WP_Error('invalid_board', $error_message);
}
return true;
}
/**
* Attach a topic to a board.
* Plate CPT active: store `_aksrt_plate_id` only (native association).
* Otherwise: taxonomy term on aksrt_forum_board.
*/
private function attach_topic_to_board(int $topic_id, int $board_id): void {
if ($topic_id <= 0 || $board_id <= 0) {
return;
}
if ($this->forum->is_plate_cpt_active()) {
update_post_meta($topic_id, '_aksrt_plate_id', $board_id);
return;
}
if (taxonomy_exists('aksrt_forum_board')) {
wp_set_object_terms($topic_id, [$board_id], 'aksrt_forum_board');
}
}
// ================================================================
// Topic moderation
// ================================================================
}
-441
View File
@@ -1,441 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
trait TopicServicePart1 {
public function moderate_topic(int $topic_id, string $action, array $payload, int $operator_id = 0) {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt'));
}
if (!$this->forum->can_moderate_topic($operator_id, $topic_id)) {
return new \WP_Error('forbidden', __('无权执行该治理动作', 'aksrt'));
}
$action = sanitize_key($action);
$note = sanitize_textarea_field((string) ($payload['note'] ?? ''));
switch ($action) {
case 'move_board':
$target_board_id = (int) ($payload['target_board_id'] ?? 0);
$target_ok = $this->assert_board($target_board_id, __('目标版块不存在', 'aksrt'));
if (is_wp_error($target_ok)) {
return $target_ok;
}
if ($this->forum->is_plate_cpt_active()) {
$previous_plate_id = (int) $this->forum->get_topic_plate_id($topic_id);
$this->attach_topic_to_board($topic_id, $target_board_id);
if ($previous_plate_id > 0 && $previous_plate_id !== $target_board_id) {
$this->forum->sync_board_count($previous_plate_id);
}
$this->forum->sync_board_count($target_board_id);
} else {
$current_board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']);
if (is_wp_error($current_board_ids)) {
$current_board_ids = [];
}
$this->attach_topic_to_board($topic_id, $target_board_id);
foreach ($current_board_ids as $current_board_id) {
$this->forum->sync_board_count((int) $current_board_id);
}
$this->forum->sync_board_count($target_board_id);
}
break;
case 'visibility':
$visibility = sanitize_key((string) ($payload['visibility'] ?? ''));
if (!isset($this->get_visibility_map()[$visibility])) {
return new \WP_Error('invalid_visibility', __('可见性参数无效', 'aksrt'));
}
update_post_meta($topic_id, '_aksrt_visibility', $visibility);
if ($visibility === Forum::VISIBILITY_CREDITS) {
update_post_meta($topic_id, '_aksrt_credits_cost', max(0, (int) ($payload['credits_cost'] ?? 0)));
delete_post_meta($topic_id, '_aksrt_pay_price');
delete_post_meta($topic_id, '_aksrt_visibility_roles');
} elseif ($visibility === Forum::VISIBILITY_PAY) {
update_post_meta($topic_id, '_aksrt_pay_price', sanitize_text_field((string) ($payload['pay_price'] ?? '')));
delete_post_meta($topic_id, '_aksrt_credits_cost');
delete_post_meta($topic_id, '_aksrt_visibility_roles');
} elseif ($visibility === Forum::VISIBILITY_ROLE) {
$roles = isset($payload['visibility_roles']) && is_array($payload['visibility_roles'])
? array_values(array_filter(array_map('sanitize_key', $payload['visibility_roles']))) : [];
update_post_meta($topic_id, '_aksrt_visibility_roles', $roles);
delete_post_meta($topic_id, '_aksrt_credits_cost');
delete_post_meta($topic_id, '_aksrt_pay_price');
} else {
delete_post_meta($topic_id, '_aksrt_credits_cost');
delete_post_meta($topic_id, '_aksrt_pay_price');
delete_post_meta($topic_id, '_aksrt_visibility_roles');
}
break;
case 'review':
$review_status = sanitize_key((string) ($payload['review_status'] ?? ''));
if (!isset($this->get_review_map()[$review_status])) {
return new \WP_Error('invalid_review_status', __('审核状态参数无效', 'aksrt'));
}
$board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']);
update_post_meta($topic_id, '_aksrt_review_status', $review_status);
if ($review_status === Forum::REVIEW_APPROVED) {
wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']);
} elseif ($review_status === Forum::REVIEW_PENDING) {
wp_update_post(['ID' => $topic_id, 'post_status' => 'pending']);
} else {
wp_update_post(['ID' => $topic_id, 'post_status' => 'draft']);
}
foreach ($board_ids as $board_id) {
$this->forum->sync_board_count((int) $board_id);
}
break;
case 'sticky': {
$raw = isset($payload['value']) ? $payload['value'] : null;
if ($raw === false || $raw === null) {
$level = Forum::STICKY_NONE;
} elseif (is_numeric($raw)) {
$level = (int) $raw;
} else {
$level = !empty($raw) ? Forum::STICKY_PLATE : Forum::STICKY_NONE;
}
$level = max(Forum::STICKY_NONE, min(Forum::STICKY_GLOBAL, $level));
update_post_meta($topic_id, '_aksrt_sticky', $level);
break;
}
case 'featured': {
$prev = get_post_meta($topic_id, '_aksrt_featured', true) === '1';
$next = !empty($payload['value']);
update_post_meta($topic_id, '_aksrt_featured', $next ? '1' : '0');
// 精华联动:从「未设精华」→「设为精华」时触发积分奖励 + 站内信通知(§7.3/§7.7/§7.8)。
// 从「精华」→「取消」时不回收积分,仅不重复发放。
if ($next && !$prev) {
$this->reward_essence($topic_id, $operator_id);
}
break;
}
case 'locked': {
$locked = !empty($payload['value']);
update_post_meta($topic_id, '_aksrt_locked', $locked ? '1' : '0');
wp_update_post([
'ID' => $topic_id,
'comment_status' => $locked ? 'closed' : 'open',
]);
break;
}
case 'question_status': {
$bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true));
if ($bbs_type !== Forum::BBS_TYPE_QUESTION) {
return new \WP_Error('not_question', __('该帖不是提问类型', 'aksrt'));
}
$status = (int) ($payload['value'] ?? Forum::QUESTION_UNSOLVED);
$status = in_array($status, [Forum::QUESTION_UNSOLVED, Forum::QUESTION_SOLVED], true)
? $status : Forum::QUESTION_UNSOLVED;
update_post_meta($topic_id, '_aksrt_question_status', $status);
break;
}
case 'close': {
$closed = !empty($payload['value']);
if ($closed) {
update_post_meta($topic_id, '_aksrt_closed', '1');
wp_update_post(['ID' => $topic_id, 'comment_status' => 'closed']);
} else {
delete_post_meta($topic_id, '_aksrt_closed');
$locked = get_post_meta($topic_id, '_aksrt_locked', true) === '1';
if (!$locked) {
wp_update_post(['ID' => $topic_id, 'comment_status' => 'open']);
}
}
break;
}
case 'hide': {
$hidden = !empty($payload['value']);
if ($hidden) {
update_post_meta($topic_id, '_aksrt_hidden', '1');
$board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']);
wp_update_post(['ID' => $topic_id, 'post_status' => 'private']);
foreach ($board_ids as $bid) {
$this->forum->sync_board_count((int) $bid);
}
} else {
delete_post_meta($topic_id, '_aksrt_hidden');
$board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']);
wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']);
foreach ($board_ids as $bid) {
$this->forum->sync_board_count((int) $bid);
}
}
break;
}
case 'delete': {
$board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']);
wp_trash_post($topic_id);
foreach ($board_ids as $bid) {
$this->forum->sync_board_count((int) $bid);
}
$author_id = (int) $post->post_author;
if ($author_id > 0 && $author_id !== $operator_id) {
$this->forum->notify($author_id, 'system', sprintf(
__('你的帖子「%s」已被删除', 'aksrt'),
$post->post_title
), [
'title' => __('帖子已删除', 'aksrt'),
'target_url' => aksrt_get_forum_home_url(),
'reason' => $note,
]);
}
break;
}
case 'undelete': {
if ($post->post_status !== 'trash') {
return new \WP_Error('not_trashed', __('该帖不在回收站中', 'aksrt'));
}
wp_untrash_post($topic_id);
$review_status = (string) get_post_meta($topic_id, '_aksrt_review_status', true);
if ($review_status === Forum::REVIEW_APPROVED) {
wp_update_post(['ID' => $topic_id, 'post_status' => 'publish']);
} else {
wp_update_post(['ID' => $topic_id, 'post_status' => 'pending']);
}
$board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']);
foreach ($board_ids as $bid) {
$this->forum->sync_board_count((int) $bid);
}
break;
}
default:
return new \WP_Error('invalid_action', __('不支持的治理动作', 'aksrt'));
}
if ($note !== '') {
update_post_meta($topic_id, '_aksrt_moderation_note', $note);
}
update_post_meta($topic_id, '_aksrt_moderated_by', $operator_id);
update_post_meta($topic_id, '_aksrt_moderated_at', current_time('mysql'));
$this->forum->notify_moderation_result($topic_id, $action, $operator_id);
return [
'message' => __('治理动作已执行', 'aksrt'),
'topic' => $this->build_topic_payload(get_post($topic_id), $operator_id),
];
}
/**
* 精华联动奖励(§7.3 联动 + §7.7 被设精华奖励 + §7.8 精华通知)。
*
* - 触发积分奖励:读 `credits.essence` 配置,有每日上限与去重 meta(防止重复发放)。
* - 触发站内信通知作者(依赖 \AKSRT\Messages\Message inbox 子系统)。
* - 缺失依赖(积分模块未启用/未配置)时静默跳过,不阻塞治理动作。
*/
public function reward_essence(int $topic_id, int $operator_id): void {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return;
}
$author_id = (int) $post->post_author;
if ($author_id <= 0) {
return;
}
// 去重:同一帖已发过精华奖励则跳过(取消再加不重复发)。
if (get_post_meta($topic_id, '_aksrt_essence_rewarded', true) === '1') {
return;
}
// 积分奖励(§7.7):统一走免费积分日上限。
if (class_exists('\AKSRT\Settings\Settings')) {
$cfg = \AKSRT\Settings\Settings::get_forum_settings();
$amount = (int) ($cfg['credits']['essence'] ?? 0);
if ($amount > 0) {
$granted = 0;
if (class_exists('\AKSRT\Credits\PointsFree')) {
$result = \AKSRT\Credits\PointsFree::grant($author_id, 'bbs_essence', $amount, [
'type' => 'post',
'id' => $topic_id,
]);
$granted = is_wp_error($result) ? 0 : (int) $result;
} elseif (class_exists('\AKSRT\Credits\Credits')) {
$daily_max = (int) ($cfg['credits']['daily_max'] ?? 50);
if ($daily_max > 0 && $this->today_credits_count($author_id, 'forum_essence') >= $daily_max) {
$amount = 0;
}
if ($amount > 0) {
\AKSRT\Credits\Credits::add($author_id, $amount, 'forum_essence', [
'type' => 'post',
'id' => $topic_id,
], __('主题被设为精华', 'aksrt'));
$granted = $amount;
}
}
if ($granted > 0) {
update_post_meta($topic_id, '_aksrt_essence_rewarded', '1');
update_post_meta($topic_id, '_aksrt_essence_rewarded_at', current_time('mysql'));
}
}
}
// 站内信通知作者(§7.8)。
if (class_exists('\AKSRT\Messages\Message')) {
$title = get_the_title($topic_id);
$permalink = (string) get_permalink($topic_id);
\AKSRT\Messages\Message::get_instance()->send(
$author_id,
'forum_essence',
sprintf(__('你的主题「%s」被设为精华', 'aksrt'), $title),
[
'title' => __('主题被设为精华', 'aksrt'),
'target_url' => $permalink,
]
);
}
do_action('aksrt_forum_topic_featured', $topic_id, $author_id, $operator_id);
}
/**
* 今日某 reason 的积分发放次数(用于精华/被加分等每日上限判断)。
*/
public function today_credits_count(int $user_id, string $reason): int {
if (!class_exists('\AKSRT\Core\DBMigrator') || $user_id <= 0) {
return 0;
}
$table = \AKSRT\Core\DBMigrator::table('credit_log');
if (!$table) {
return 0;
}
global $wpdb;
$today = current_time('Y-m-d');
return (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND reason = %s AND DATE(created_at) = %s",
$user_id, $reason, $today
));
}
/**
* 置顶分级 label(对标 zibll zib_bbs_get_topping_badge)。
*/
public function get_sticky_label(int $level): string {
switch ($level) {
case Forum::STICKY_GLOBAL:
return __('全局置顶', 'aksrt');
case Forum::STICKY_SUPER:
return __('超级置顶', 'aksrt');
case Forum::STICKY_PLATE:
return __('置顶', 'aksrt');
default:
return '';
}
}
/**
* 读取并规范化置顶分级 meta。
*
* 兼容历史 0/1 二态 meta("1" → STICKY_PLATE),新数据存 0~3 整数。
*/
public function resolve_sticky_level(int $topic_id): int {
$raw = get_post_meta($topic_id, '_aksrt_sticky', true);
if ($raw === '' || $raw === null) {
return Forum::STICKY_NONE;
}
if (is_numeric($raw)) {
return max(Forum::STICKY_NONE, min(Forum::STICKY_GLOBAL, (int) $raw));
}
return $raw === '1' ? Forum::STICKY_PLATE : Forum::STICKY_NONE;
}
// ================================================================
// Voting system
// ================================================================
/**
* Submit vote.
*
* @return array<string,mixed>|\WP_Error
*/
public function submit_vote(int $topic_id, int $user_id, array $option_ids) {
if ($user_id <= 0) {
return new \WP_Error('not_logged_in', __('请先登录后投票', 'aksrt'));
}
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt'));
}
$vote = $this->get_vote_payload($topic_id, $user_id);
if (empty($vote['enabled'])) {
return new \WP_Error('vote_disabled', __('当前主题未开启投票', 'aksrt'));
}
if (!empty($vote['expired'])) {
return new \WP_Error('vote_expired', __('投票已结束', 'aksrt'));
}
if (!empty($vote['has_voted'])) {
return new \WP_Error('already_voted', __('你已经参与过本次投票', 'aksrt'));
}
$selected = array_values(array_unique(array_filter(array_map('sanitize_text_field', $option_ids))));
if (empty($selected)) {
return new \WP_Error('empty_selection', __('请选择至少一个投票选项', 'aksrt'));
}
if (empty($vote['multiple']) && count($selected) > 1) {
return new \WP_Error('too_many_selection', __('当前投票仅支持单选', 'aksrt'));
}
$options = get_post_meta($topic_id, '_aksrt_vote_options', true);
$options = is_array($options) ? $options : [];
$valid_option_ids = wp_list_pluck($options, 'id');
foreach ($selected as $selected_id) {
if (!in_array($selected_id, $valid_option_ids, true)) {
return new \WP_Error('invalid_option', __('投票选项不存在', 'aksrt'));
}
}
$records = get_post_meta($topic_id, '_aksrt_vote_records', true);
$records = is_array($records) ? $records : [];
$records[(string) $user_id] = $selected;
update_post_meta($topic_id, '_aksrt_vote_records', $records);
foreach ($options as &$option) {
if (in_array((string) ($option['id'] ?? ''), $selected, true)) {
$option['count'] = max(0, (int) ($option['count'] ?? 0)) + 1;
}
}
unset($option);
update_post_meta($topic_id, '_aksrt_vote_options', $options);
return [
'message' => __('投票成功', 'aksrt'),
'vote' => $this->get_vote_payload($topic_id, $user_id),
];
}
}
-382
View File
@@ -1,382 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
trait TopicServicePart2 {
/**
* Prepare vote meta from request data.
*/
public function prepare_vote_meta(array $data): array {
$enabled = !empty($data['vote_enabled']);
$title = sanitize_text_field((string) ($data['vote_title'] ?? ''));
$multiple = !empty($data['vote_multiple']);
$raw_options = $data['vote_options'] ?? $data['vote_options_text'] ?? [];
$labels = [];
if (is_array($raw_options)) {
foreach ($raw_options as $option) {
$option = sanitize_text_field((string) $option);
if ($option !== '') {
$labels[] = $option;
}
}
} else {
$lines = preg_split('/\r\n|\r|\n/', (string) $raw_options) ?: [];
foreach ($lines as $line) {
$line = sanitize_text_field((string) $line);
if ($line !== '') {
$labels[] = $line;
}
}
}
$labels = array_values(array_unique($labels));
$config = $this->forum->get_config();
$vote_max = (int) ($config['other']['vote_max'] ?? 8);
if ($vote_max > 0 && count($labels) > $vote_max) {
$labels = array_slice($labels, 0, $vote_max);
}
$vote_type = sanitize_key((string) ($data['vote_type'] ?? 'standard'));
if ($vote_type === 'pk' && count($labels) !== 2) {
$vote_type = 'standard';
}
if ($vote_type === 'pk') {
$multiple = false;
}
if (count($labels) < 2) {
$enabled = false;
}
$expiry_days = (int) ($data['vote_expiry_days'] ?? 0);
$options = [];
foreach ($labels as $index => $label) {
$options[] = [
'id' => 'option_' . ($index + 1),
'label' => $label,
'count' => 0,
];
}
return [
'enabled' => $enabled,
'title' => $title,
'multiple' => $multiple,
'vote_type' => $vote_type,
'expiry_days' => $expiry_days,
'options' => $options,
];
}
/**
* Save vote meta while preserving counts when possible.
*/
public function save_vote_meta(int $topic_id, array $vote_meta): void {
$existing_options = get_post_meta($topic_id, '_aksrt_vote_options', true);
$existing_options = is_array($existing_options) ? $existing_options : [];
$existing_by_label = [];
foreach ($existing_options as $existing_option) {
if (!empty($existing_option['label'])) {
$existing_by_label[(string) $existing_option['label']] = $existing_option;
}
}
$options = [];
foreach ($vote_meta['options'] as $index => $option) {
$label = (string) ($option['label'] ?? '');
if ($label === '') {
continue;
}
$existing = $existing_by_label[$label] ?? null;
$options[] = [
'id' => $existing['id'] ?? ('option_' . ($index + 1)),
'label' => $label,
'count' => (int) ($existing['count'] ?? 0),
];
}
update_post_meta($topic_id, '_aksrt_vote_enabled', !empty($vote_meta['enabled']) ? '1' : '0');
update_post_meta($topic_id, '_aksrt_vote_title', sanitize_text_field((string) ($vote_meta['title'] ?? '')));
update_post_meta($topic_id, '_aksrt_vote_multiple', !empty($vote_meta['multiple']) ? '1' : '0');
update_post_meta($topic_id, '_aksrt_vote_options', $options);
$vote_type = in_array($vote_meta['vote_type'] ?? '', ['standard', 'pk'], true) ? $vote_meta['vote_type'] : 'standard';
update_post_meta($topic_id, '_aksrt_vote_type', $vote_type);
$expiry_days = (int) ($vote_meta['expiry_days'] ?? 0);
if (!in_array($expiry_days, [0, 1, 7, 30], true)) {
$expiry_days = 0;
}
update_post_meta($topic_id, '_aksrt_vote_expiry_days', $expiry_days);
if ($expiry_days > 0 && !get_post_meta($topic_id, '_aksrt_vote_started_at', true)) {
update_post_meta($topic_id, '_aksrt_vote_started_at', current_time('mysql'));
}
if (empty($vote_meta['enabled'])) {
delete_post_meta($topic_id, '_aksrt_vote_records');
}
}
/**
* Get vote payload for a topic.
*/
public function get_vote_payload(int $topic_id, int $user_id = 0): array {
$enabled = get_post_meta($topic_id, '_aksrt_vote_enabled', true) === '1';
$options = get_post_meta($topic_id, '_aksrt_vote_options', true);
$options = is_array($options) ? $options : [];
$records = get_post_meta($topic_id, '_aksrt_vote_records', true);
$records = is_array($records) ? $records : [];
$vote_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_vote_type', true));
if (!in_array($vote_type, ['standard', 'pk'], true)) {
$vote_type = 'standard';
}
$expiry_days = (int) get_post_meta($topic_id, '_aksrt_vote_expiry_days', true);
$started_at = (string) get_post_meta($topic_id, '_aksrt_vote_started_at', true);
$expired = false;
if ($expiry_days > 0 && $started_at !== '') {
$expire_time = strtotime($started_at) + ($expiry_days * DAY_IN_SECONDS);
$expired = time() >= $expire_time;
}
$config = $this->forum->get_config();
$display_mode = $config['other']['vote_number_type'] ?? 'percent';
$selected = $user_id > 0 && isset($records[(string) $user_id]) && is_array($records[(string) $user_id])
? array_values(array_map('strval', $records[(string) $user_id]))
: [];
$total_votes = 0;
foreach ($options as &$option) {
$count = max(0, (int) ($option['count'] ?? 0));
$option['count'] = $count;
$option['selected'] = in_array((string) ($option['id'] ?? ''), $selected, true);
$total_votes += $count;
}
unset($option);
return [
'enabled' => $enabled && !empty($options),
'title' => (string) get_post_meta($topic_id, '_aksrt_vote_title', true),
'multiple' => get_post_meta($topic_id, '_aksrt_vote_multiple', true) === '1',
'vote_type' => $vote_type,
'expiry_days' => $expiry_days,
'expired' => $expired,
'display_mode' => $display_mode,
'options' => $options,
'total_votes' => $total_votes,
'has_voted' => !empty($selected),
'selected' => $selected,
];
}
// ================================================================
// Content processing
// ================================================================
/**
* Save governance meta fields from an editable payload.
*/
public function save_topic_governance_meta(int $topic_id, array $data): void {
if (isset($data['visibility'])) {
$visibility = sanitize_key((string) $data['visibility']);
if (isset($this->get_visibility_map()[$visibility])) {
update_post_meta($topic_id, '_aksrt_visibility', $visibility);
}
}
if (array_key_exists('special_topic', $data)) {
update_post_meta($topic_id, '_aksrt_special_topic', sanitize_text_field((string) $data['special_topic']));
}
if (array_key_exists('vote_enabled', $data) || array_key_exists('vote_options', $data) || array_key_exists('vote_options_text', $data)) {
$this->save_vote_meta($topic_id, $this->prepare_vote_meta($data));
}
}
public function save_cover_meta(int $post_id, array $data): void {
$cover_type = sanitize_key((string) ($data['cover_type'] ?? ''));
if (!in_array($cover_type, ['image', 'slide', 'video'], true)) {
delete_post_meta($post_id, '_aksrt_cover_type');
return;
}
update_post_meta($post_id, '_aksrt_cover_type', $cover_type);
if ($cover_type === 'image') {
$url = esc_url_raw((string) ($data['cover_image'] ?? ''));
update_post_meta($post_id, '_aksrt_cover_image', $url);
} elseif ($cover_type === 'slide') {
$raw = $data['featured_slide'] ?? '';
$slides = is_array($raw) ? $raw : json_decode((string) $raw, true);
if (!is_array($slides)) {
$slides = [];
}
$slides = array_values(array_filter(array_map('esc_url_raw', array_slice($slides, 0, 9))));
update_post_meta($post_id, '_aksrt_featured_slide', $slides);
} elseif ($cover_type === 'video') {
$url = esc_url_raw((string) ($data['featured_video'] ?? ''));
update_post_meta($post_id, '_aksrt_featured_video', $url);
}
}
/**
* Sanitize topic content based on user's compose capabilities.
*
* Strips <img>, <video>, <audio>, <iframe>, [aksrt_hide] blocks
* when the author lacks the corresponding permission.
*/
public function sanitize_topic_content(int $user_id, string $content): string {
$caps = $this->forum->get_compose_capabilities($user_id);
if (empty($caps['can_upload_image'])) {
$content = preg_replace('/<img[^>]*>/i', '', $content);
}
if (empty($caps['can_upload_video'])) {
$content = preg_replace('/<video[^>]*>.*?<\/video>/is', '', $content);
$content = preg_replace('/<audio[^>]*>.*?<\/audio>/is', '', $content);
}
if (empty($caps['can_iframe_video'])) {
$content = preg_replace('/<iframe[^>]*>.*?<\/iframe>/is', '', $content);
}
if (empty($caps['can_set_hide'])) {
$content = preg_replace('/\[aksrt_hide[^\]]*\](.*?)\[\/aksrt_hide\]/s', '$1', $content);
}
return $content;
}
/**
* Register the [aksrt_hide] shortcode for hidden content blocks.
*/
public static function register_hide_shortcode(): void {
add_shortcode('aksrt_hide', [self::class, 'render_hide_shortcode']);
}
/**
* Render [aksrt_hide] shortcode.
*
* Attributes:
* type — 'login' (default) | 'comment' | 'role' | 'credits'
* roles — comma-separated role slugs (for type=role)
* cost — int credits cost (for type=credits)
*/
public static function render_hide_shortcode($atts, $content = ''): string {
$atts = shortcode_atts([
'type' => 'login',
'roles' => '',
'cost' => 0,
], $atts, 'aksrt_hide');
$type = sanitize_key($atts['type']);
$user_id = get_current_user_id();
$visible = false;
$message = '';
switch ($type) {
case 'login':
$visible = $user_id > 0;
if (!$visible) {
$message = __('此内容需要登录后才能查看。', 'aksrt');
}
break;
case 'comment':
if ($user_id <= 0) {
$message = __('登录并评论后可查看隐藏内容。', 'aksrt');
} else {
$post_id = get_the_ID();
if ($post_id && function_exists('get_comments')) {
$has_commented = (int) get_comments([
'post_id' => $post_id,
'user_id' => $user_id,
'count' => true,
'status' => 'approve',
]) > 0;
$visible = $has_commented;
}
if (!$visible) {
$message = __('评论后可查看隐藏内容。', 'aksrt');
}
}
break;
case 'role':
if ($user_id <= 0) {
$message = __('此内容仅特定角色可见。', 'aksrt');
} else {
$allowed = array_filter(array_map('trim', explode(',', (string) $atts['roles'])));
if (empty($allowed)) {
$visible = true;
} else {
$user = get_userdata($user_id);
$visible = $user && !empty(array_intersect($user->roles, $allowed));
}
if (!$visible) {
$message = __('此内容仅限特定用户角色查看。', 'aksrt');
}
}
break;
case 'credits':
$cost = max(0, (int) $atts['cost']);
if ($user_id <= 0) {
$message = sprintf(__('此内容需要 %d 积分解锁。请先登录。', 'aksrt'), $cost);
} elseif ($cost <= 0) {
$visible = true;
} else {
$balance = class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::get_balance($user_id) : 0;
$visible = $balance >= $cost;
if (!$visible) {
$message = sprintf(__('此内容需要 %d 积分解锁(当前余额:%d)。', 'aksrt'), $cost, (int) $balance);
}
}
break;
default:
$visible = $user_id > 0;
if (!$visible) {
$message = __('此内容需要登录后才能查看。', 'aksrt');
}
}
if ($visible) {
return '<div class="aksrt-forum-hide aksrt-forum-hide--revealed">' . do_shortcode($content) . '</div>';
}
return '<div class="aksrt-forum-hide aksrt-forum-hide--locked">'
. '<div class="aksrt-forum-hide__message">'
. '<i class="fas fa-lock"></i> '
. esc_html($message)
. '</div></div>';
}
/**
* Split a tag string.
*/
public function split_tags(string $tags): array {
$parts = preg_split('/[,,]/', $tags) ?: [];
$clean = [];
foreach ($parts as $part) {
$part = sanitize_text_field(trim($part));
if ($part !== '') {
$clean[] = $part;
}
}
return array_values(array_unique($clean));
}
}
-631
View File
@@ -1,631 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
trait TopicServicePart3 {
// ================================================================
/**
* Build a topic payload.
*/
public function build_topic_payload(\WP_Post $post, int $viewer_user_id = 0): array {
$topic_id = (int) $post->ID;
$author_id = (int) $post->post_author;
$board_terms = get_the_terms($topic_id, 'aksrt_forum_board');
$tag_terms = get_the_terms($topic_id, 'aksrt_forum_tag');
$boards = [];
if ($board_terms && !is_wp_error($board_terms)) {
foreach ($board_terms as $board_term) {
$board_link = get_term_link($board_term);
$boards[] = [
'id' => (int) $board_term->term_id,
'name' => (string) $board_term->name,
'url' => !is_wp_error($board_link) ? (string) $board_link : '',
];
}
}
$tags = [];
if ($tag_terms && !is_wp_error($tag_terms)) {
foreach ($tag_terms as $tag_term) {
$tag_link = get_term_link($tag_term);
$tags[] = [
'id' => (int) $tag_term->term_id,
'name' => (string) $tag_term->name,
'url' => !is_wp_error($tag_link) ? (string) $tag_link : '',
];
}
}
$topic_cat_terms = get_the_terms($topic_id, 'aksrt_forum_topic_cat');
$topic_cat = null;
if ($topic_cat_terms && !is_wp_error($topic_cat_terms)) {
$first = $topic_cat_terms[0];
$tc_link = get_term_link($first);
$topic_cat = [
'id' => (int) $first->term_id,
'name' => (string) $first->name,
'url' => !is_wp_error($tc_link) ? (string) $tc_link : '',
];
}
$visibility = sanitize_key((string) get_post_meta($topic_id, '_aksrt_visibility', true));
if (!isset($this->get_visibility_map()[$visibility])) {
$visibility = Forum::VISIBILITY_PUBLIC;
}
$review_status = sanitize_key((string) get_post_meta($topic_id, '_aksrt_review_status', true));
if (!isset($this->get_review_map()[$review_status])) {
$review_status = $post->post_status === 'publish' ? Forum::REVIEW_APPROVED : Forum::REVIEW_PENDING;
}
$can_view = $this->can_view_topic($post, $viewer_user_id, $visibility, $review_status);
$excerpt = $post->post_excerpt !== ''
? $post->post_excerpt
: wp_trim_words(wp_strip_all_tags((string) $post->post_content), 40);
$pay_hide_part = (int) get_post_meta($topic_id, '_aksrt_pay_hide_part', true) === 1;
$content = (string) $post->post_content;
if (!$can_view) {
// Partial hide: keep body for shortcode processing; strip locked blocks at render.
// Full hide (default): blank content to avoid API/template leaks.
if ($pay_hide_part && in_array($visibility, [Forum::VISIBILITY_CREDITS, Forum::VISIBILITY_PAY], true)) {
// leave content; payshow/hidecontent shortcodes gate paid parts
$excerpt = wp_trim_words(wp_strip_all_tags(
preg_replace('/\[(payshow|hidecontent)[^\]]*\].*?\[\/\1\]/is', '', $content) ?? $content
), 40);
} else {
$content = '';
if (in_array($visibility, [Forum::VISIBILITY_CREDITS, Forum::VISIBILITY_PAY, Forum::VISIBILITY_LOGIN, Forum::VISIBILITY_COMMENT, Forum::VISIBILITY_ROLE, Forum::VISIBILITY_MODERATOR], true)) {
$excerpt = '';
}
}
}
$sticky_level = $this->resolve_sticky_level($topic_id);
$featured = get_post_meta($topic_id, '_aksrt_featured', true) === '1';
$locked = get_post_meta($topic_id, '_aksrt_locked', true) === '1';
$bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true));
if (!in_array($bbs_type, [Forum::BBS_TYPE_STANDARD, Forum::BBS_TYPE_QUESTION], true)) {
$bbs_type = Forum::BBS_TYPE_STANDARD;
}
$question_status = $bbs_type === Forum::BBS_TYPE_QUESTION
? (int) get_post_meta($topic_id, '_aksrt_question_status', true)
: null;
$favorite_count = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true);
$is_favorited = $viewer_user_id > 0 && $this->forum->is_favorited($viewer_user_id, $topic_id);
$score = (int) get_post_meta($topic_id, '_aksrt_score', true);
$cover = $this->build_cover_payload($topic_id);
$credits_cost = max(0, (int) get_post_meta($topic_id, '_aksrt_credits_cost', true));
if ($credits_cost <= 0) {
$credits_cost = max(0, (int) get_post_meta($topic_id, '_aksrt_pay_credits', true));
}
return [
'id' => $topic_id,
'title' => (string) get_the_title($topic_id),
'excerpt' => (string) $excerpt,
'content' => $content,
'status' => (string) $post->post_status,
'author_id' => $author_id,
'author' => $this->build_user_summary($author_id),
'date' => get_the_date('Y-m-d H:i', $topic_id),
'modified' => get_the_modified_date('Y-m-d H:i', $topic_id),
'comment_count' => (int) get_comments([
'post_id' => $topic_id,
'count' => true,
'status' => 'approve',
]),
'views' => (int) get_post_meta($topic_id, '_aksrt_views', true),
'permalink' => (string) get_permalink($topic_id),
'sticky' => $sticky_level > Forum::STICKY_NONE,
'sticky_level' => $sticky_level,
'sticky_label' => $this->get_sticky_label($sticky_level),
'featured' => $featured,
'locked' => $locked,
'special_topic' => (string) get_post_meta($topic_id, '_aksrt_special_topic', true),
'bbs_type' => $bbs_type,
'question_status' => $question_status,
'favorite_count' => $favorite_count,
'is_favorited' => $is_favorited,
'score' => $score,
'cover' => $cover,
'visibility' => [
'key' => $visibility,
'label' => $this->get_visibility_label($visibility),
],
'review_status' => [
'key' => $review_status,
'label' => $this->get_review_label($review_status),
],
'moderation_note' => (string) get_post_meta($topic_id, '_aksrt_moderation_note', true),
'plate_id' => (int) get_post_meta($topic_id, '_aksrt_plate_id', true),
'boards' => $boards,
'tags' => $tags,
'topic_cat' => $topic_cat,
'vote' => $this->get_vote_payload($topic_id, $viewer_user_id),
'can_view' => $can_view,
'can_edit' => $this->forum->can_edit_topic($viewer_user_id, $topic_id),
'can_moderate' => $this->forum->can_moderate_topic($viewer_user_id, $topic_id),
'pay_hide_part' => $pay_hide_part,
'credits_cost' => $credits_cost,
'pay_price' => (float) get_post_meta($topic_id, '_aksrt_pay_price', true),
];
}
/**
* Determine whether current viewer can read a topic.
*/
public function can_view_topic(\WP_Post $post, int $viewer_user_id, string $visibility, string $review_status): bool {
if (user_can($viewer_user_id, 'manage_options')) {
return true;
}
if ($post->post_status !== 'publish' || $review_status !== Forum::REVIEW_APPROVED) {
return $viewer_user_id > 0
&& ((int) $post->post_author === $viewer_user_id || $this->forum->can_moderate_topic($viewer_user_id, (int) $post->ID));
}
if ((int) $post->post_author === $viewer_user_id) {
return true;
}
// Plate gate first (zibll plate → post). Moderators of plate already pass can_view_plate.
$plate_id = (int) get_post_meta((int) $post->ID, '_aksrt_plate_id', true);
if ($plate_id > 0 && method_exists($this->forum, 'can_view_plate')) {
if (!$this->forum->can_view_plate($viewer_user_id, $plate_id)) {
return false;
}
}
if ($visibility === Forum::VISIBILITY_PUBLIC) {
return true;
}
if ($visibility === Forum::VISIBILITY_LOGIN) {
return $viewer_user_id > 0;
}
if ($visibility === Forum::VISIBILITY_COMMENT) {
if ($viewer_user_id <= 0) {
return false;
}
return $this->has_user_commented_on($viewer_user_id, (int) $post->ID);
}
if ($visibility === Forum::VISIBILITY_ROLE) {
if ($viewer_user_id <= 0) {
return false;
}
$allowed_roles = (array) get_post_meta((int) $post->ID, '_aksrt_visibility_roles', true);
if (empty($allowed_roles)) {
return true;
}
$user = get_userdata($viewer_user_id);
if (!$user) {
return false;
}
return !empty(array_intersect($user->roles, $allowed_roles));
}
if ($visibility === Forum::VISIBILITY_CREDITS) {
if ($viewer_user_id <= 0) {
return false;
}
return $this->has_user_unlocked_topic($viewer_user_id, (int) $post->ID);
}
if ($visibility === Forum::VISIBILITY_PAY) {
if ($viewer_user_id <= 0) {
return function_exists('aksrt_guest_has_paid_access')
&& aksrt_guest_has_paid_access((int) $post->ID);
}
return $this->has_user_paid_topic($viewer_user_id, (int) $post->ID);
}
if ($visibility === Forum::VISIBILITY_MODERATOR) {
if ($viewer_user_id <= 0) {
return false;
}
return $this->forum->can_moderate_topic($viewer_user_id, (int) $post->ID);
}
return true;
}
/**
* Build cover payload for a topic.
*
* Supports three cover types: image (single), slide (multiple images), video.
* Falls back to WP featured image (thumbnail) when no explicit cover_type is set.
*/
public function build_cover_payload(int $topic_id): ?array {
$cover_type = (string) get_post_meta($topic_id, '_aksrt_cover_type', true);
if ($cover_type === 'image') {
$url = (string) get_post_meta($topic_id, '_aksrt_cover_image', true);
if ($url === '') {
$url = (string) get_the_post_thumbnail_url($topic_id, 'medium_large');
}
if ($url === '') return null;
return [
'type' => 'image',
'url' => $url,
'ratio' => (string) (get_post_meta($topic_id, '_aksrt_cover_ratio', true) ?: '16:9'),
];
}
if ($cover_type === 'slide') {
$slides = get_post_meta($topic_id, '_aksrt_featured_slide', true);
if (!is_array($slides) || empty($slides)) return null;
return [
'type' => 'slide',
'images' => array_map('esc_url', array_slice($slides, 0, 9)),
'ratio' => (string) (get_post_meta($topic_id, '_aksrt_cover_ratio', true) ?: '16:9'),
];
}
if ($cover_type === 'video') {
$video_url = (string) get_post_meta($topic_id, '_aksrt_featured_video', true);
if ($video_url === '') return null;
$poster = (string) get_the_post_thumbnail_url($topic_id, 'medium_large');
return [
'type' => 'video',
'url' => $video_url,
'poster' => $poster,
'ratio' => (string) (get_post_meta($topic_id, '_aksrt_cover_ratio', true) ?: '16:9'),
];
}
// Fallback: use WP featured image if set
$thumb_url = (string) get_the_post_thumbnail_url($topic_id, 'medium_large');
if ($thumb_url !== '') {
return [
'type' => 'image',
'url' => $thumb_url,
'ratio' => '16:9',
];
}
return null;
}
/**
* Build user summary payload.
*/
public function build_user_summary(int $user_id): array {
$user = $user_id > 0 ? get_userdata($user_id) : false;
return [
'id' => $user_id,
'display_name' => $user ? $user->display_name : '',
'user_email' => $user ? $user->user_email : '',
'avatar_url' => $user ? $this->get_user_avatar_url($user_id) : '',
];
}
/**
* Get avatar url.
*/
public function get_user_avatar_url(int $user_id): string {
if (function_exists('aksrt_get_avatar_url')) {
return (string) aksrt_get_avatar_url($user_id);
}
return (string) get_avatar_url($user_id);
}
// ================================================================
// Topic unlock
// ================================================================
public function unlock_topic_with_credits(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'));
}
$visibility = (string) get_post_meta($topic_id, '_aksrt_visibility', true);
if ($visibility !== Forum::VISIBILITY_CREDITS) {
return $this->forum->error(__('该主题不需要积分解锁', 'aksrt'));
}
if ($this->has_user_unlocked_topic($user_id, $topic_id)) {
return $this->forum->success(__('已解锁', 'aksrt'));
}
$cost = max(0, (int) get_post_meta($topic_id, '_aksrt_credits_cost', true));
if ($cost <= 0) {
$cost = max(0, (int) get_post_meta($topic_id, '_aksrt_pay_credits', true));
}
// VIP tier discount for credits.
$vip_level = 0;
if (function_exists('aksrt_get_user_vip_level')) {
$vip_level = (int) aksrt_get_user_vip_level($user_id);
} elseif (function_exists('aksrt_is_vip2') && aksrt_is_vip2($user_id)) {
$vip_level = 2;
} elseif (function_exists('aksrt_is_vip') && aksrt_is_vip($user_id)) {
$vip_level = 1;
}
if ($vip_level >= 2) {
$v2 = get_post_meta($topic_id, '_aksrt_credits_vip2', true);
if ($v2 !== '' && $v2 !== null && $v2 !== false) {
$cost = max(0, (int) $v2);
}
} elseif ($vip_level >= 1) {
$v1 = get_post_meta($topic_id, '_aksrt_credits_vip1', true);
if ($v1 !== '' && $v1 !== null && $v1 !== false) {
$cost = max(0, (int) $v1);
}
}
if ($cost <= 0 && $vip_level > 0) {
// VIP free credits unlock.
$this->mark_topic_unlocked($user_id, $topic_id);
return $this->forum->success(__('VIP 免费解锁成功', 'aksrt'), ['free' => true]);
}
if ($cost <= 0) {
return $this->forum->error(__('积分价格未设置', 'aksrt'));
}
if (!class_exists('\AKSRT\Credits\Credits')) {
return $this->forum->error(__('积分系统不可用', 'aksrt'));
}
$balance = \AKSRT\Credits\Credits::get_balance($user_id);
if ($balance < $cost) {
return $this->forum->error(sprintf(__('积分不足,需要 %d 积分', 'aksrt'), $cost));
}
$deducted = \AKSRT\Credits\Credits::deduct($user_id, $cost, 'forum_topic_unlock', [
'topic_id' => $topic_id,
], sprintf(__('解锁帖子「%s」', 'aksrt'), get_the_title($topic_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_topic_unlocked($user_id, $topic_id);
$author_id = (int) $post->post_author;
if ($author_id > 0 && $author_id !== $user_id) {
$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) {
\AKSRT\Credits\Credits::add($author_id, $author_amount, 'forum_content_income', [
'topic_id' => $topic_id,
'buyer_id' => $user_id,
], sprintf(__('帖子「%s」被付费查看', 'aksrt'), get_the_title($topic_id)));
}
}
return $this->forum->success(__('解锁成功', 'aksrt'));
}
/**
* Resolve cash price for a pay-visibility topic.
*
* VIP resolution: VIP2 price → VIP1/legacy VIP price → base.
*
* @return array{amount:float,is_vip_price:bool,is_free:bool,label:string,base:float,vip_set:bool,vip_level:int}
*/
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_level = 0;
if ($user_id > 0) {
if (function_exists('aksrt_get_user_vip_level')) {
$vip_level = (int) aksrt_get_user_vip_level($user_id);
} elseif (function_exists('aksrt_is_vip2') && aksrt_is_vip2($user_id)) {
$vip_level = 2;
} elseif (function_exists('aksrt_is_vip') && aksrt_is_vip($user_id)) {
$vip_level = 1;
}
if ($vip_level <= 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($user_id)) {
$vip_level = 1;
}
}
$pick = static function ($raw) {
if ($raw === '' || $raw === null || $raw === false) {
return null;
}
return is_numeric($raw) ? (float) $raw : null;
};
$vip2 = $pick(get_post_meta($topic_id, '_aksrt_pay_vip2_price', true));
$vip1 = $pick(get_post_meta($topic_id, '_aksrt_pay_vip1_price', true));
if ($vip1 === null) {
// Legacy single VIP key.
$vip1 = $pick(get_post_meta($topic_id, '_aksrt_pay_vip_price', true));
}
$amount = $base;
$is_vip_price = false;
$vip_set = ($vip1 !== null || $vip2 !== null);
if ($vip_level >= 2 && $vip2 !== null) {
$amount = max(0.0, $vip2);
$is_vip_price = true;
} elseif ($vip_level >= 1 && $vip1 !== null) {
$amount = max(0.0, $vip1);
$is_vip_price = true;
} elseif ($vip_level >= 2 && $vip1 !== null) {
// VIP2 user falls back to VIP1 price when VIP2 unset.
$amount = max(0.0, $vip1);
$is_vip_price = true;
}
if ($is_vip_price) {
return [
'amount' => $amount,
'is_vip_price' => true,
'is_free' => $amount <= 0,
'label' => $amount <= 0
? __('VIP 免费', 'aksrt')
: sprintf(__('VIP ¥%s', 'aksrt'), number_format($amount, 2, '.', '')),
'base' => $base,
'vip_set' => true,
'vip_level' => $vip_level,
];
}
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,
'vip_level' => $vip_level,
];
}
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, 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 ($topic_id <= 0) {
return false;
}
if ($user_id <= 0) {
return function_exists('aksrt_guest_has_paid_access') && aksrt_guest_has_paid_access($topic_id);
}
if ($this->has_user_unlocked_topic($user_id, $topic_id)) {
return true;
}
if (class_exists('\AKSRT\Payment\Order')) {
$product_type = class_exists('\AKSRT\Core\DomainModel')
? \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE
: 'resource';
if (\AKSRT\Payment\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 {
global $wpdb;
$count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_post_ID = %d AND user_id = %d AND comment_approved = '1' LIMIT 1",
$post_id,
$user_id
));
return $count > 0;
}
// ================================================================
// Status helpers
// ================================================================
/**
* Normalize post status filter.
*
* @return array<int,string>|string
*/
public function normalize_post_status_query(string $status) {
if ($status === 'all') {
return ['publish', 'pending', 'draft'];
}
return $status !== '' ? $status : 'publish';
}
/**
* Visibility labels.
*/
public function get_visibility_map(): array {
return [
Forum::VISIBILITY_PUBLIC => __('公开可见', 'aksrt'),
Forum::VISIBILITY_LOGIN => __('登录可见', 'aksrt'),
Forum::VISIBILITY_COMMENT => __('评论可见', 'aksrt'),
Forum::VISIBILITY_ROLE => __('角色可见', 'aksrt'),
Forum::VISIBILITY_CREDITS => __('积分查看', 'aksrt'),
Forum::VISIBILITY_PAY => __('付费查看', 'aksrt'),
Forum::VISIBILITY_MODERATOR => __('版主可见', 'aksrt'),
];
}
/**
* Review labels.
*/
public function get_review_map(): array {
return [
Forum::REVIEW_APPROVED => __('已通过', 'aksrt'),
Forum::REVIEW_PENDING => __('待审核', 'aksrt'),
Forum::REVIEW_REJECTED => __('已拒绝', 'aksrt'),
];
}
/**
* Get visibility label.
*/
public function get_visibility_label(string $visibility): string {
$map = $this->get_visibility_map();
return $map[$visibility] ?? $map[Forum::VISIBILITY_PUBLIC];
}
/**
* Get review label.
*/
public function get_review_label(string $review_status): string {
$map = $this->get_review_map();
return $map[$review_status] ?? $map[Forum::REVIEW_PENDING];
}
/**
* Get visibility options.
*/
public function get_visibility_options(): array {
$options = [];
foreach ($this->get_visibility_map() as $value => $label) {
$options[] = ['label' => $label, 'value' => $value];
}
return $options;
}
}
-306
View File
@@ -1,306 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
trait TopicServicePart4 {
/**
* Get review options.
*/
public function get_review_options(): array {
$options = [];
foreach ($this->get_review_map() as $value => $label) {
$options[] = ['label' => $label, 'value' => $value];
}
return $options;
}
/**
* Check whether a user can publish directly.
*/
public function can_user_publish_directly(int $user_id, int $board_id = 0): bool {
if ($user_id <= 0) {
return false;
}
if (user_can($user_id, 'manage_options')) {
return true;
}
if ($this->forum->is_forum_admin($user_id)) {
return true;
}
if ($board_id > 0 && $this->forum->is_board_moderator($user_id, $board_id)) {
return true;
}
return false;
}
// ── AJAX edit (extracted from facade) ──
public function ajax_edit_topic(): void {
check_ajax_referer('aksrt_forum_compose', '_nonce');
$user_id = get_current_user_id();
$topic_id = (int) ($_POST['topic_id'] ?? 0);
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
if (!$this->forum->can_edit_topic($user_id, $topic_id)) {
wp_send_json_error(['message' => __('无编辑权限', 'aksrt')]);
}
$title = sanitize_text_field((string) ($_POST['title'] ?? ''));
$content = wp_kses_post((string) ($_POST['content'] ?? ''));
if ($title === '' || $content === '') {
wp_send_json_error(['message' => __('标题和内容不能为空', 'aksrt')]);
}
$result = wp_update_post([
'ID' => $topic_id,
'post_title' => $title,
'post_content' => $content,
], true);
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
$tags = $this->split_tags((string) ($_POST['tags'] ?? ''));
$clean_tags = array_slice(array_values(array_filter(array_map('sanitize_text_field', $tags))), 0, 5);
wp_set_object_terms($topic_id, $clean_tags, 'aksrt_forum_tag');
if (isset($_POST['special_topic'])) {
update_post_meta($topic_id, '_aksrt_special_topic', sanitize_text_field((string) $_POST['special_topic']));
}
if (isset($_POST['topic_cat']) && taxonomy_exists('aksrt_forum_topic_cat')) {
$topic_cat = sanitize_text_field((string) $_POST['topic_cat']);
wp_set_object_terms($topic_id, $topic_cat !== '' ? [$topic_cat] : [], 'aksrt_forum_topic_cat');
}
$caps = $this->forum->get_compose_capabilities($user_id);
if ($caps['can_set_visibility'] && isset($_POST['visibility'])) {
$visibility = sanitize_key((string) $_POST['visibility']);
if (isset($this->get_visibility_map()[$visibility])) {
update_post_meta($topic_id, '_aksrt_visibility', $visibility);
if ($visibility === self::VISIBILITY_CREDITS) {
$cost = max(0, (int) ($_POST['credits_cost'] ?? 0));
update_post_meta($topic_id, '_aksrt_credits_cost', $cost);
update_post_meta($topic_id, '_aksrt_pay_credits', $cost);
if (isset($_POST['credits_vip1']) && (string) $_POST['credits_vip1'] !== '') {
update_post_meta($topic_id, '_aksrt_credits_vip1', max(0, (int) $_POST['credits_vip1']));
} else {
delete_post_meta($topic_id, '_aksrt_credits_vip1');
}
if (isset($_POST['credits_vip2']) && (string) $_POST['credits_vip2'] !== '') {
update_post_meta($topic_id, '_aksrt_credits_vip2', max(0, (int) $_POST['credits_vip2']));
} else {
delete_post_meta($topic_id, '_aksrt_credits_vip2');
}
update_post_meta($topic_id, '_aksrt_pay_hide_part', !empty($_POST['pay_hide_part']) ? 1 : 0);
}
if ($visibility === self::VISIBILITY_PAY) {
update_post_meta($topic_id, '_aksrt_pay_price', sanitize_text_field((string) ($_POST['pay_price'] ?? '')));
if (isset($_POST['pay_vip1_price']) && (string) $_POST['pay_vip1_price'] !== '') {
$v1 = max(0, (float) $_POST['pay_vip1_price']);
update_post_meta($topic_id, '_aksrt_pay_vip1_price', $v1);
update_post_meta($topic_id, '_aksrt_pay_vip_price', $v1);
} else {
delete_post_meta($topic_id, '_aksrt_pay_vip1_price');
delete_post_meta($topic_id, '_aksrt_pay_vip_price');
}
if (isset($_POST['pay_vip2_price']) && (string) $_POST['pay_vip2_price'] !== '') {
update_post_meta($topic_id, '_aksrt_pay_vip2_price', max(0, (float) $_POST['pay_vip2_price']));
} else {
delete_post_meta($topic_id, '_aksrt_pay_vip2_price');
}
update_post_meta($topic_id, '_aksrt_pay_hide_part', !empty($_POST['pay_hide_part']) ? 1 : 0);
update_post_meta($topic_id, '_aksrt_topic_coupon_s', !empty($_POST['topic_coupon_s']) ? 1 : 0);
}
if ($visibility === self::VISIBILITY_ROLE) {
$roles = isset($_POST['visibility_roles']) && is_array($_POST['visibility_roles'])
? array_map('sanitize_key', $_POST['visibility_roles']) : [];
update_post_meta($topic_id, '_aksrt_visibility_roles', $roles);
}
}
}
if ($caps['can_set_cover']) {
$this->save_cover_meta($topic_id, [
'cover_type' => sanitize_key((string) ($_POST['cover_type'] ?? '')),
'cover_image' => esc_url_raw((string) ($_POST['cover_image'] ?? '')),
'featured_slide' => (string) ($_POST['featured_slide'] ?? ''),
'featured_video' => esc_url_raw((string) ($_POST['featured_video'] ?? '')),
]);
}
if ($caps['can_set_vote'] && isset($_POST['vote_enabled'])) {
$vote_data = [
'vote_enabled' => !empty($_POST['vote_enabled']),
'vote_title' => sanitize_text_field((string) ($_POST['vote_title'] ?? '')),
'vote_options_text' => sanitize_textarea_field((string) ($_POST['vote_options'] ?? '')),
'vote_multiple' => !empty($_POST['vote_multiple']),
'vote_type' => sanitize_key((string) ($_POST['vote_type'] ?? 'standard')),
'vote_expiry_days' => (int) ($_POST['vote_expiry_days'] ?? 0),
];
$vote_meta = $this->prepare_vote_meta($vote_data);
$this->save_vote_meta($topic_id, $vote_meta);
}
wp_send_json_success([
'message' => __('帖子已更新', 'aksrt'),
'redirect' => get_permalink($topic_id),
]);
}
public function can_edit_topic(int $user_id, int $topic_id): bool {
if ($user_id <= 0 || $topic_id <= 0) {
return false;
}
// Moderators and above can always edit
if ($this->forum->forum_user_can($user_id, 'topic_edit_other', $topic_id)) {
return true;
}
// Authors can edit their own unlocked topics
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return false;
}
if ((int) $post->post_author !== $user_id) {
return false;
}
return get_post_meta($topic_id, '_aksrt_locked', true) !== '1';
}
public function update_topic_meta(int $topic_id, array $meta): bool {
if ($topic_id <= 0) {
return false;
}
if (!$this->forum->can_moderate_topic(get_current_user_id(), $topic_id) && !current_user_can('manage_options')) {
return false;
}
$allowed_keys = ['_aksrt_sticky', '_aksrt_featured', '_aksrt_locked', '_aksrt_highlight'];
foreach ($meta as $key => $value) {
if (!in_array($key, $allowed_keys, true)) {
continue;
}
update_post_meta($topic_id, $key, !empty($value) ? '1' : '0');
}
return true;
}
public function ajax_update_topic(): void {
$user_id = get_current_user_id();
$topic_id = (int) ($_POST['topic_id'] ?? 0);
if ($topic_id <= 0 || !$this->forum->forum_user_can($user_id, 'topic_moderate', $topic_id)) {
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
}
$meta = [];
if (isset($_POST['sticky'])) {
$meta['_aksrt_sticky'] = !empty($_POST['sticky']);
}
if (isset($_POST['featured'])) {
$meta['_aksrt_featured'] = !empty($_POST['featured']);
}
if (isset($_POST['locked'])) {
$meta['_aksrt_locked'] = !empty($_POST['locked']);
}
if ($this->update_topic_meta($topic_id, $meta)) {
wp_send_json_success(['message' => __('已更新', 'aksrt')]);
}
wp_send_json_error(['message' => __('更新失败', 'aksrt')]);
}
public function ajax_create_topic(): void {
check_ajax_referer('aksrt_forum_compose', '_nonce');
$user_id = get_current_user_id();
if ($user_id <= 0) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')]);
}
$data = [
'board_id' => (int) ($_POST['board_id'] ?? 0),
'title' => sanitize_text_field((string) ($_POST['title'] ?? '')),
'content' => wp_kses_post((string) ($_POST['content'] ?? '')),
'tags' => $this->split_tags((string) ($_POST['tags'] ?? '')),
'special_topic' => sanitize_text_field((string) ($_POST['special_topic'] ?? '')),
'visibility' => sanitize_key((string) ($_POST['visibility'] ?? self::VISIBILITY_PUBLIC)),
'bbs_type' => sanitize_key((string) ($_POST['bbs_type'] ?? self::BBS_TYPE_STANDARD)),
'topic_cat' => sanitize_text_field((string) ($_POST['topic_cat'] ?? '')),
'vote_enabled' => !empty($_POST['vote_enabled']),
'vote_title' => sanitize_text_field((string) ($_POST['vote_title'] ?? '')),
'vote_options_text' => sanitize_textarea_field((string) ($_POST['vote_options'] ?? '')),
'vote_multiple' => !empty($_POST['vote_multiple']),
'cover_type' => sanitize_key((string) ($_POST['cover_type'] ?? '')),
'cover_image' => esc_url_raw((string) ($_POST['cover_image'] ?? '')),
'featured_slide' => (string) ($_POST['featured_slide'] ?? ''),
'featured_video' => esc_url_raw((string) ($_POST['featured_video'] ?? '')),
'credits_cost' => (int) ($_POST['credits_cost'] ?? 0),
'pay_price' => sanitize_text_field((string) ($_POST['pay_price'] ?? '')),
'credits_vip1' => isset($_POST['credits_vip1']) ? sanitize_text_field((string) $_POST['credits_vip1']) : '',
'credits_vip2' => isset($_POST['credits_vip2']) ? sanitize_text_field((string) $_POST['credits_vip2']) : '',
'pay_vip1_price' => isset($_POST['pay_vip1_price']) ? sanitize_text_field((string) $_POST['pay_vip1_price']) : '',
'pay_vip2_price' => isset($_POST['pay_vip2_price']) ? sanitize_text_field((string) $_POST['pay_vip2_price']) : '',
'pay_hide_part' => !empty($_POST['pay_hide_part']),
'topic_coupon_s' => !empty($_POST['topic_coupon_s']),
'visibility_roles' => isset($_POST['visibility_roles']) && is_array($_POST['visibility_roles'])
? array_map('sanitize_key', $_POST['visibility_roles']) : [],
];
$draft_id = (int) ($_POST['draft_id'] ?? 0);
if ($draft_id > 0) {
$result = $this->forum->publish_draft($user_id, $draft_id, $data);
} else {
$result = $this->create_topic($user_id, $data);
}
if (is_wp_error($result)) {
wp_send_json_error(['message' => $result->get_error_message()]);
}
wp_send_json_success([
'message' => (string) ($result['message'] ?? __('主题已提交', 'aksrt')),
'post_id' => (int) ($result['post_id'] ?? 0),
'redirect' => (string) ($result['redirect'] ?? aksrt_get_forum_home_url()),
]);
}
public function ajax_unlock_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->unlock_topic_with_credits($user_id, $topic_id);
if (!empty($result['success'])) {
wp_send_json_success($result);
} else {
wp_send_json_error(['message' => $result['message'] ?? __('解锁失败', 'aksrt')]);
}
}
}
-718
View File
@@ -1,718 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
class ViewBuilder {
use ViewBuilderPart1;
use ViewBuilderPart2;
private Forum $forum;
public function __construct(Forum $forum) {
$this->forum = $forum;
}
public function get_topic_list(array $args = []): array {
$board_id = (int) ($args['board_id'] ?? 0);
$limit = max(1, min(100, (int) ($args['limit'] ?? ($args['per_page'] ?? 20))));
$page = max(1, (int) ($args['page'] ?? $args['paged'] ?? 1));
$offset = array_key_exists('offset', $args)
? max(0, (int) $args['offset'])
: ($page - 1) * $limit;
$status = sanitize_key((string) ($args['status'] ?? 'publish'));
$keyword = sanitize_text_field((string) ($args['keyword'] ?? ''));
$visibility = sanitize_key((string) ($args['visibility'] ?? ''));
$review_status = sanitize_key((string) ($args['review_status'] ?? ''));
$author_id = (int) ($args['author_id'] ?? 0);
$orderby = sanitize_key((string) ($args['orderby'] ?? ($args['sort'] ?? 'date')));
$viewer_user_id = (int) ($args['viewer_user_id'] ?? get_current_user_id());
$include_hidden = !empty($args['include_hidden']);
// Settings tab aliases: hot→views, essence/featured→featured filter.
if ($orderby === 'hot') {
$orderby = 'views';
}
$query_args = [
'post_type' => 'aksrt_forum_topic',
'posts_per_page' => $limit,
'offset' => $offset,
'post_status' => $this->forum->normalize_post_status_query($status),
'order' => 'DESC',
'ignore_sticky_posts'=> true,
];
$meta_query = [];
if ($orderby === 'views') {
$query_args['orderby'] = 'meta_value_num';
$query_args['meta_key'] = '_aksrt_views';
} elseif ($orderby === 'recommend') {
$query_args['orderby'] = 'meta_value_num';
$query_args['meta_key'] = '_aksrt_recommend_score';
} elseif ($orderby === 'comment_count') {
$query_args['orderby'] = 'comment_count';
} elseif ($orderby === 'score') {
$query_args['orderby'] = 'meta_value_num';
$query_args['meta_key'] = '_aksrt_score';
} elseif ($orderby === 'favorites') {
$query_args['orderby'] = 'meta_value_num';
$query_args['meta_key'] = '_aksrt_favorite_count';
} elseif ($orderby === 'last_reply') {
$query_args['orderby'] = 'meta_value';
$query_args['meta_key'] = '_aksrt_last_reply';
$query_args['meta_type']= 'DATETIME';
} elseif ($orderby === 'featured' || $orderby === 'essence') {
// 推荐 / 精华 share _aksrt_featured (Zibll essence = featured).
$meta_query[] = [
'key' => '_aksrt_featured',
'value' => '1',
'compare' => '=',
];
$query_args['orderby'] = 'date';
} else {
$query_args['orderby'] = 'date';
$orderby = 'date';
}
if ($keyword !== '') {
$query_args['s'] = $keyword;
}
if ($author_id > 0) {
$query_args['author'] = $author_id;
}
$plate_id = (int) ($args['plate_id'] ?? 0);
$plate_ids = $args['plate_ids'] ?? [];
if (!empty($plate_ids) && is_array($plate_ids)) {
$meta_query[] = [
'key' => '_aksrt_plate_id',
'value' => array_map('intval', $plate_ids),
'type' => 'NUMERIC',
'compare' => 'IN',
];
} elseif ($plate_id > 0) {
$meta_query[] = [
'key' => '_aksrt_plate_id',
'value' => $plate_id,
'type' => 'NUMERIC',
'compare' => '=',
];
} elseif ($board_id > 0) {
$query_args['tax_query'] = [[
'taxonomy' => 'aksrt_forum_board',
'field' => 'term_id',
'terms' => $board_id,
]];
}
if ($visibility !== '') {
$meta_query[] = [
'key' => '_aksrt_visibility',
'value' => $visibility,
'compare' => '=',
];
}
if ($review_status !== '') {
$meta_query[] = [
'key' => '_aksrt_review_status',
'value' => $review_status,
'compare' => '=',
];
}
if (!empty($meta_query)) {
if (count($meta_query) > 1) {
$meta_query['relation'] = 'AND';
}
$query_args['meta_query'] = $meta_query;
}
$query = new \WP_Query($query_args);
$items = [];
while ($query->have_posts()) {
$query->the_post();
$post = get_post(get_the_ID());
if (!$post) {
continue;
}
$topic = $this->forum->build_topic_payload($post, $viewer_user_id);
if (!$include_hidden && empty($topic['can_view'])) {
continue;
}
$items[] = $topic;
}
wp_reset_postdata();
return [
'items' => $items,
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
'page' => $page,
'orderby' => $orderby,
];
}
/**
* Get forum homepage view.
*/
public function get_home_view(int $user_id = 0): array {
$boards = $this->forum->get_boards();
$stats = $this->get_stats();
$synthesis = $this->get_synthesis_stream(1, 15, $user_id);
$hot = $this->get_topic_list([
'limit' => 8,
'status' => 'publish',
'viewer_user_id' => $user_id,
'orderby' => 'views',
]);
$applications = [];
if ($user_id > 0) {
foreach ($boards as $board) {
$applications[(int) $board['id']] = $this->forum->get_board_application_for_user($user_id, (int) $board['id']);
}
}
$config = $this->forum->get_config();
$home_tabs = $config['homepage']['tabs'] ?? [
['type' => 'follow', 'label' => '关注'],
['type' => 'synthesis', 'label' => '综合'],
['type' => 'plate', 'label' => '版块'],
];
$active_index = (int) ($config['homepage']['tab_active_index'] ?? 1);
$quick_posts = !empty($config['homepage']['quick_posts']);
return [
'boards' => $boards,
'recent_topics' => $synthesis['items'],
'synthesis' => $synthesis,
'hot_topics' => $hot['items'],
'stats' => $stats,
'permissions' => [
'can_compose' => $user_id > 0 && $this->forum->forum_user_can($user_id, 'compose_post'),
'can_manage' => $this->forum->is_forum_admin($user_id),
'can_create_plate'=> $this->forum->forum_user_can($user_id, 'plate_create'),
'user_tier' => $user_id > 0 ? $this->forum->get_user_plate_tier($user_id, 0) : Forum::TIER_NONE,
],
'applications' => $applications,
'home_tabs' => $home_tabs,
'active_index' => $active_index,
'quick_posts' => $quick_posts,
'compose_url' => function_exists('aksrt_get_forum_compose_url') ? aksrt_get_forum_compose_url() : home_url('/'),
'home_url' => function_exists('aksrt_get_forum_home_url') ? aksrt_get_forum_home_url() : home_url('/'),
];
}
// ── §7.10 Tab data streams ──────────────────────────────────────────
/**
* Get topics from plates the user follows.
*
* @return array{items:array,total:int,max_pages:int}
*/
public function get_follow_stream(int $user_id, int $page = 1, int $limit = 15): array {
if ($user_id <= 0) {
return ['items' => [], 'total' => 0, 'max_pages' => 1];
}
$follow_ids = get_user_meta($user_id, 'aksrt_follow_plate', true);
$follow_ids = is_array($follow_ids) ? array_filter(array_map('intval', $follow_ids)) : [];
if (empty($follow_ids)) {
return ['items' => [], 'total' => 0, 'max_pages' => 1];
}
return $this->get_topic_list([
'plate_ids' => $follow_ids,
'page' => $page,
'limit' => $limit,
'status' => 'publish',
'viewer_user_id' => $user_id,
]);
}
/**
* Get synthesis (all topics) stream sorted by date.
*
* @return array{items:array,total:int,max_pages:int}
*/
public function get_synthesis_stream(int $page = 1, int $limit = 15, int $viewer = 0): array {
return $this->get_topic_list([
'page' => $page,
'limit' => $limit,
'status' => 'publish',
'viewer_user_id' => $viewer,
]);
}
/**
* Get pending-review topics for moderators.
*
* @return array{items:array,total:int,max_pages:int}
*/
public function get_pending_stream(int $user_id, int $page = 1, int $limit = 15): array {
if ($user_id <= 0 || !$this->forum->is_forum_admin($user_id)) {
return ['items' => [], 'total' => 0, 'max_pages' => 1];
}
return $this->get_topic_list([
'page' => $page,
'limit' => $limit,
'status' => 'pending',
'viewer_user_id' => $user_id,
'include_hidden' => true,
]);
}
/**
* AJAX: Load a forum home tab stream.
*/
public function ajax_home_tab(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$tab_type = sanitize_key((string) ($_POST['tab_type'] ?? 'synthesis'));
$page = max(1, (int) ($_POST['page'] ?? 1));
$user_id = get_current_user_id();
switch ($tab_type) {
case 'follow':
$result = $this->get_follow_stream($user_id, $page);
break;
case 'pending':
$result = $this->get_pending_stream($user_id, $page);
break;
case 'plate':
ob_start();
$this->render_board_cards($this->forum->get_boards());
$html = ob_get_clean();
wp_send_json_success([
'html' => $html,
'type' => 'plate',
'boards' => $this->forum->get_boards(),
'total' => count($this->forum->get_boards()),
'max_pages' => 1,
'page' => 1,
'has_more' => false,
]);
return;
default:
$result = $this->get_synthesis_stream($page, 15, $user_id);
}
ob_start();
if (!empty($result['items'])) {
foreach ($result['items'] as $topic) {
$this->render_topic_row($topic);
}
} else {
echo '<div class="aksrt-forum__empty"><i class="far fa-comment-dots"></i><p>' . esc_html__('内容空空如也', 'aksrt') . '</p></div>';
}
$html = ob_get_clean();
wp_send_json_success([
'html' => $html,
'type' => $tab_type,
'total' => $result['total'],
'max_pages' => $result['max_pages'],
'page' => $page,
'has_more' => $page < $result['max_pages'],
]);
}
/**
* Public AJAX: plate/board topic list with sort tabs.
*/
public function ajax_board_topics(): void {
check_ajax_referer('aksrt_public_nonce', 'nonce');
$plate_id = (int) ($_POST['plate_id'] ?? $_GET['plate_id'] ?? 0);
$board_id = (int) ($_POST['board_id'] ?? $_GET['board_id'] ?? 0);
$page = max(1, (int) ($_POST['page'] ?? $_GET['page'] ?? 1));
$orderby = sanitize_key((string) ($_POST['orderby'] ?? $_POST['sort'] ?? $_GET['orderby'] ?? $_GET['sort'] ?? 'date'));
$limit = max(1, min(50, (int) ($_POST['limit'] ?? $_GET['limit'] ?? 20)));
$user_id = get_current_user_id();
$allowed = ['date', 'featured', 'essence', 'last_reply', 'hot', 'views', 'recommend', 'comment_count', 'score', 'favorites'];
if (!in_array($orderby, $allowed, true)) {
$orderby = 'date';
}
$list_args = [
'page' => $page,
'limit' => $limit,
'status' => 'publish',
'viewer_user_id' => $user_id,
'orderby' => $orderby,
];
if ($plate_id > 0) {
$list_args['plate_id'] = $plate_id;
} elseif ($board_id > 0) {
$list_args['board_id'] = $board_id;
}
$result = $this->get_topic_list($list_args);
ob_start();
if (!empty($result['items'])) {
foreach ($result['items'] as $topic) {
$this->render_topic_row($topic);
}
} else {
$compose = function_exists('aksrt_get_forum_compose_url')
? aksrt_get_forum_compose_url($plate_id > 0 ? ['board' => $plate_id] : ($board_id > 0 ? ['board' => $board_id] : []))
: '';
echo '<div class="aksrt-forum-board__empty aksrt-forum__empty">';
echo '<i class="fas fa-comments"></i>';
echo '<p>' . esc_html__('内容空空如也', 'aksrt') . '</p>';
if ($compose !== '') {
echo '<a href="' . esc_url($compose) . '">' . esc_html__('发布第一个主题', 'aksrt') . '</a>';
}
echo '</div>';
}
$html = ob_get_clean();
wp_send_json_success([
'html' => $html,
'total' => $result['total'],
'max_pages' => $result['max_pages'],
'page' => $page,
'orderby' => $orderby,
'has_more' => $page < $result['max_pages'],
]);
}
/**
* Render plate/board cards grid (home plate tab).
*
* @param array<int, array<string, mixed>> $boards
*/
public function render_board_cards(array $boards): void {
$names = $this->forum->get_config()['names'] ?? [];
$name_topic = $names['posts'] ?? __('主题', 'aksrt');
if (empty($boards)) {
echo '<div class="aksrt-forum__empty"><i class="far fa-folder-open"></i><p>' . esc_html__('内容空空如也', 'aksrt') . '</p></div>';
return;
}
echo '<div class="aksrt-forum-board-card__grid">';
foreach ($boards as $board) {
$url = (string) ($board['url'] ?? '#');
$name = (string) ($board['name'] ?? '');
$desc = (string) ($board['description'] ?? '');
$count = (int) ($board['post_count'] ?? $board['topic_count'] ?? 0);
$cover = (string) ($board['cover_url'] ?? $board['thumbnail_url'] ?? '');
?>
<a class="aksrt-forum-board-card" href="<?php echo esc_url($url); ?>">
<?php if ($cover !== '') : ?>
<span class="aksrt-forum-board-card__cover" style="background-image:url(<?php echo esc_url($cover); ?>)"></span>
<?php else : ?>
<span class="aksrt-forum-board-card__cover aksrt-forum-board-card__cover--empty"><i class="fas fa-comments"></i></span>
<?php endif; ?>
<span class="aksrt-forum-board-card__body">
<span class="aksrt-forum-board-card__title"><?php echo esc_html($name); ?></span>
<?php if ($desc !== '') : ?>
<span class="aksrt-forum-board-card__desc"><?php echo esc_html(wp_trim_words($desc, 18, '…')); ?></span>
<?php endif; ?>
<span class="aksrt-forum-board-card__meta">
<i class="fas fa-list-alt"></i>
<?php printf(esc_html__('%1$d 个%2$s', 'aksrt'), $count, esc_html($name_topic)); ?>
</span>
</span>
</a>
<?php
}
echo '</div>';
}
/**
* Render a single topic row (reusable for AJAX and initial render).
*
* @param array $topic Topic payload.
* @param string $style detail|mini (zibll forum-posts parity).
*/
public function render_topic_row(array $topic, string $style = 'detail'): void {
$style = $style === 'mini' ? 'mini' : 'detail';
$board = !empty($topic['boards'][0]) ? $topic['boards'][0] : null;
$names = $this->forum->get_config()['names'] ?? [];
$sticky_level = (int) ($topic['sticky_level'] ?? (!empty($topic['sticky']) ? 1 : 0));
$permalink = (string) ($topic['permalink'] ?? '#');
$cover = is_array($topic['cover'] ?? null) ? $topic['cover'] : null;
$vote = is_array($topic['vote'] ?? null) ? $topic['vote'] : null;
$excerpt = trim((string) ($topic['excerpt'] ?? ''));
$topic_cat = is_array($topic['topic_cat'] ?? null) ? $topic['topic_cat'] : null;
$row_class = 'aksrt-forum-topic-row aksrt-forum-posts forum-posts ajax-item ' . $style;
if ($sticky_level > 0) {
$row_class .= ' is-sticky is-sticky-' . $sticky_level;
}
if (!empty($topic['featured'])) {
$row_class .= ' is-featured';
}
$badges = '';
if ($sticky_level > 0) {
$badges .= '<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--sticky badge-topping">'
. esc_html((string) ($topic['sticky_label'] ?? __('置顶', 'aksrt'))) . '</span>';
}
if (!empty($topic['featured'])) {
$badges .= '<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--featured badge-essence">'
. esc_html__('精华', 'aksrt') . '</span>';
}
if (($topic['bbs_type'] ?? '') === 'question') {
$q_status = (int) ($topic['question_status'] ?? 0);
$badges .= '<span class="aksrt-forum-topic-row__badge aksrt-forum-topic-row__badge--question'
. ($q_status === 1 ? ' is-solved' : '') . '">'
. esc_html($q_status === 1
? (string) ($names['question_ok_badge'] ?? __('已解决', 'aksrt'))
: (string) ($names['question_badge'] ?? __('提问', 'aksrt')))
. '</span>';
}
$title_html = $badges
. '<h3 class="aksrt-forum-topic-row__title forum-title">'
. '<a href="' . esc_url($permalink) . '">' . esc_html((string) ($topic['title'] ?? '')) . '</a></h3>';
if ($style === 'mini') {
?>
<article class="<?php echo esc_attr($row_class); ?>" data-topic-id="<?php echo esc_attr((string) ($topic['id'] ?? 0)); ?>">
<div class="aksrt-forum-topic-row__avatar forum-user">
<?php if (!empty($topic['author']['avatar_url'])) : ?>
<img src="<?php echo esc_url((string) $topic['author']['avatar_url']); ?>" alt="" width="40" height="40" loading="lazy" class="forum-avatar">
<?php endif; ?>
</div>
<div class="aksrt-forum-topic-row__body entry-info">
<div class="aksrt-forum-topic-row__title-wrap"><?php echo $title_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
<div class="aksrt-forum-topic-row__meta item-meta flex ac jsb">
<div class="meta-left">
<span class="aksrt-forum-topic-row__author"><?php echo esc_html((string) ($topic['author']['display_name'] ?? '')); ?></span>
<span class="aksrt-forum-topic-row__time icon-circle"><?php echo esc_html((string) ($topic['date'] ?? '')); ?></span>
</div>
<div class="meta-right action-meta">
<span title="<?php esc_attr_e('浏览', 'aksrt'); ?>"><i class="fas fa-eye"></i> <?php echo esc_html(number_format_i18n((int) ($topic['views'] ?? 0))); ?></span>
<span title="<?php esc_attr_e('回复', 'aksrt'); ?>"><i class="fas fa-comment"></i> <?php echo esc_html(number_format_i18n((int) ($topic['comment_count'] ?? 0))); ?></span>
<span title="<?php esc_attr_e('评分', 'aksrt'); ?>"><i class="fas fa-thumbs-up"></i> <?php echo esc_html(number_format_i18n((int) ($topic['score'] ?? 0))); ?></span>
</div>
</div>
</div>
</article>
<?php
return;
}
// detail
$list_imgs = $this->extract_topic_list_images($topic, 4);
?>
<article class="<?php echo esc_attr($row_class); ?>" data-topic-id="<?php echo esc_attr((string) ($topic['id'] ?? 0)); ?>">
<div class="aksrt-forum-topic-row__author-card mb10 flex ac">
<div class="aksrt-forum-topic-row__avatar">
<?php if (!empty($topic['author']['avatar_url'])) : ?>
<img src="<?php echo esc_url((string) $topic['author']['avatar_url']); ?>" alt="" width="44" height="44" loading="lazy">
<?php endif; ?>
</div>
<div class="aksrt-forum-topic-row__author-meta">
<div class="aksrt-forum-topic-row__author-name"><?php echo esc_html((string) ($topic['author']['display_name'] ?? '')); ?></div>
<div class="aksrt-forum-topic-row__time muted-2-color em09"><?php echo esc_html((string) ($topic['date'] ?? '')); ?></div>
</div>
</div>
<?php if (is_array($cover) && !empty($cover['type'])) : ?>
<a class="aksrt-forum-topic-row__cover forum-lists-cover" href="<?php echo esc_url($permalink); ?>">
<?php if (($cover['type'] ?? '') === 'video') : ?>
<?php if (!empty($cover['poster'])) : ?>
<img src="<?php echo esc_url((string) $cover['poster']); ?>" alt="" loading="lazy">
<?php endif; ?>
<i class="fas fa-play-circle"></i>
<?php elseif (($cover['type'] ?? '') === 'slide' && !empty($cover['images'][0])) : ?>
<img src="<?php echo esc_url((string) $cover['images'][0]); ?>" alt="" loading="lazy">
<span class="aksrt-forum-topic-row__cover-count"><i class="fas fa-images"></i></span>
<?php elseif (!empty($cover['url'])) : ?>
<img src="<?php echo esc_url((string) $cover['url']); ?>" alt="" loading="lazy">
<?php endif; ?>
</a>
<?php endif; ?>
<div class="aksrt-forum-topic-row__body">
<div class="aksrt-forum-topic-row__title-wrap"><?php echo $title_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
<div class="content mt6">
<?php if ($topic_cat && !empty($topic_cat['name'])) : ?>
<a class="aksrt-forum-topic-row__topic-link focus-color mr3" href="<?php echo esc_url((string) ($topic_cat['url'] ?? '#')); ?>">
<i class="fas fa-hashtag"></i><?php echo esc_html((string) $topic_cat['name']); ?>
</a>
<?php endif; ?>
<?php if ($excerpt !== '') : ?>
<div class="excerpt"><?php echo esc_html($excerpt); ?></div>
<?php endif; ?>
</div>
<?php if (!empty($list_imgs) && empty($cover)) : ?>
<div class="imgbox-container lists-imgs mt6 count-<?php echo esc_attr((string) count($list_imgs)); ?>">
<?php foreach ($list_imgs as $img_url) : ?>
<a class="lists-imgs__item cursor-zoom-in" href="<?php echo esc_url($permalink); ?>">
<img src="<?php echo esc_url($img_url); ?>" alt="" loading="lazy">
</a>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($vote['enabled']) && !empty($vote['options']) && is_array($vote['options'])) : ?>
<div class="aksrt-forum-topic-row__vote mt10">
<?php
$total_votes = 0;
foreach ($vote['options'] as $opt) {
$total_votes += (int) ($opt['count'] ?? 0);
}
foreach (array_slice($vote['options'], 0, 4) as $opt) :
$cnt = (int) ($opt['count'] ?? 0);
$pct = $total_votes > 0 ? round($cnt * 100 / $total_votes) : 0;
?>
<div class="aksrt-forum-topic-row__vote-opt">
<span class="aksrt-forum-topic-row__vote-label"><?php echo esc_html((string) ($opt['label'] ?? $opt['text'] ?? '')); ?></span>
<span class="aksrt-forum-topic-row__vote-bar"><i style="width:<?php echo esc_attr((string) $pct); ?>%"></i></span>
<span class="aksrt-forum-topic-row__vote-pct"><?php echo esc_html((string) $pct); ?>%</span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="flex ac jsb mt10 aksrt-forum-topic-row__footer">
<?php if ($board) : ?>
<a class="but but-plate aksrt-forum-topic-row__board" href="<?php echo esc_url((string) ($board['url'] ?? '#')); ?>">
<?php echo esc_html((string) ($board['name'] ?? '')); ?>
<i class="fa fa-angle-right ml6"></i>
</a>
<?php else : ?>
<span class="plate-null"></span>
<?php endif; ?>
<div class="action-meta flex0 flex jsb aksrt-forum-topic-row__actions">
<span title="<?php esc_attr_e('评分', 'aksrt'); ?>"><i class="fas fa-thumbs-up"></i> <?php echo esc_html(number_format_i18n((int) ($topic['score'] ?? 0))); ?></span>
<a href="<?php echo esc_url($permalink); ?>#comments" title="<?php esc_attr_e('回复', 'aksrt'); ?>"><i class="fas fa-comment"></i> <?php echo esc_html(number_format_i18n((int) ($topic['comment_count'] ?? 0))); ?></a>
<span title="<?php esc_attr_e('浏览', 'aksrt'); ?>"><i class="fas fa-eye"></i> <?php echo esc_html(number_format_i18n((int) ($topic['views'] ?? 0))); ?></span>
<span title="<?php esc_attr_e('收藏', 'aksrt'); ?>"><i class="fas fa-star"></i> <?php echo esc_html(number_format_i18n((int) ($topic['favorite_count'] ?? 0))); ?></span>
</div>
</div>
</div>
</article>
<?php
}
/**
* Pull up to N image URLs from cover slides or content HTML for list grids.
*
* @return array<int,string>
*/
private function extract_topic_list_images(array $topic, int $max = 4): array {
$max = max(1, min(6, $max));
$out = [];
$cover = is_array($topic['cover'] ?? null) ? $topic['cover'] : null;
if ($cover) {
if (($cover['type'] ?? '') === 'slide' && !empty($cover['images']) && is_array($cover['images'])) {
foreach ($cover['images'] as $u) {
$u = esc_url_raw((string) $u);
if ($u !== '') {
$out[] = $u;
}
if (count($out) >= $max) {
return $out;
}
}
} elseif (!empty($cover['url'])) {
$out[] = esc_url_raw((string) $cover['url']);
}
}
if (count($out) >= $max) {
return $out;
}
$content = (string) ($topic['content'] ?? '');
if ($content !== '' && preg_match_all('/<img[^>]+src=["\']([^"\']+)["\']/i', $content, $m)) {
foreach ($m[1] as $src) {
$src = esc_url_raw((string) $src);
if ($src === '' || in_array($src, $out, true)) {
continue;
}
$out[] = $src;
if (count($out) >= $max) {
break;
}
}
}
return $out;
}
public function get_user_forum_topics(int $user_id, array $args = []): array {
$status = $args['status'] ?? 'publish';
$page = max(1, (int) ($args['page'] ?? 1));
$limit = max(1, min(50, (int) ($args['limit'] ?? 10)));
$query_args = [
'post_type' => 'aksrt_forum_topic',
'author' => $user_id,
'post_status' => $status,
'posts_per_page' => $limit,
'paged' => $page,
'orderby' => 'date',
'order' => 'DESC',
];
$query = new \WP_Query($query_args);
$items = [];
foreach ($query->posts as $post) {
$items[] = $this->forum->build_topic_payload($post, $user_id);
}
return [
'items' => $items,
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
];
}
/**
* Get a user's favorited forum topics.
*
* @return array{items:array,total:int,max_pages:int}
*/
public function get_user_favorite_topics(int $user_id, int $page = 1, int $limit = 10): array {
$fav_ids = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true);
$fav_ids = is_array($fav_ids) ? array_map('intval', array_filter($fav_ids)) : [];
if (empty($fav_ids)) {
return ['items' => [], 'total' => 0, 'max_pages' => 1];
}
$query = new \WP_Query([
'post_type' => 'aksrt_forum_topic',
'post__in' => $fav_ids,
'post_status' => 'publish',
'posts_per_page' => $limit,
'paged' => $page,
'orderby' => 'post__in',
]);
$items = [];
foreach ($query->posts as $post) {
$items[] = $this->forum->build_topic_payload($post, $user_id);
}
return [
'items' => $items,
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
];
}
/**
* Get a user's managed plates (plates where user is moderator/author).
*
* @return array{owned:array,moderating:array}
*/
}
-416
View File
@@ -1,416 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
trait ViewBuilderPart1 {
public function get_user_managed_plates(int $user_id): array {
return $this->forum->get_user_managed_plates($user_id);
}
/**
* Forum stats for a user (counts, cached per request).
*
* @return array{topic_count:int,reply_count:int,favorite_count:int,followed_plate_count:int,managed_plate_count:int}
*/
public function get_user_forum_stats(int $user_id): array {
$topic_count = (int) count_user_posts($user_id, 'aksrt_forum_topic', true);
global $wpdb;
$reply_count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments} c
INNER JOIN {$wpdb->posts} p ON p.ID = c.comment_post_ID
WHERE c.user_id = %d AND p.post_type = 'aksrt_forum_topic' AND c.comment_approved = '1'",
$user_id
));
$fav_ids = get_user_meta($user_id, 'aksrt_favorite_forum_posts', true);
$favorite_count = is_array($fav_ids) ? count(array_filter($fav_ids)) : 0;
$follow_ids = get_user_meta($user_id, 'aksrt_follow_plate', true);
$followed_plate_count = is_array($follow_ids) ? count(array_filter($follow_ids)) : 0;
$managed = $this->forum->get_user_managed_plates($user_id);
$managed_count = count($managed['owned']) + count($managed['moderating']);
return [
'topic_count' => $topic_count,
'reply_count' => $reply_count,
'favorite_count' => $favorite_count,
'followed_plate_count' => $followed_plate_count,
'managed_plate_count' => $managed_count,
];
}
public function get_topic_view(int $topic_id, int $user_id = 0): array {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return [
'topic' => null,
'board' => null,
'permissions' => [],
'board_options' => [],
'visibility_options' => $this->forum->get_visibility_options(),
'review_options' => $this->forum->get_review_options(),
];
}
$topic = $this->forum->build_topic_payload($post, $user_id);
$boards = !empty($topic['boards']) ? $topic['boards'] : [];
$board = !empty($boards[0]) ? $boards[0] : null;
return [
'topic' => $topic,
'board' => $board,
'permissions' => [
'can_edit' => $this->forum->can_edit_topic($user_id, $topic_id),
'can_moderate' => $this->forum->can_moderate_topic($user_id, $topic_id),
// 加锁语义统一到原生 comment_status(§7.3):comment_status=closed 或 _aksrt_locked=1 均不可回复。
'can_reply' => comments_open($topic_id) && empty($topic['locked']),
],
'board_options' => $this->forum->get_boards(),
'visibility_options' => $this->forum->get_visibility_options(),
'review_options' => $this->forum->get_review_options(),
];
}
/**
* Get compose page view.
*/
public function get_compose_view(int $user_id, array $args = []): array {
$board_id = (int) ($args['board_id'] ?? 0);
return [
'boards' => $this->forum->get_boards(),
'selected_board_id' => $board_id,
'visibility_options' => $this->forum->get_visibility_options(),
'publish_mode' => $this->forum->can_user_publish_directly($user_id, $board_id) ? 'publish' : 'pending',
'draft_count' => $this->forum->get_user_draft_count($user_id),
'drafts' => $this->forum->get_user_drafts($user_id, 10),
'capabilities' => $this->forum->get_compose_capabilities($user_id),
];
}
/**
* Get forum stats.
*/
public function get_stats(): array {
$boards = $this->forum->get_boards();
$topic_count = post_type_exists('aksrt_forum_topic')
? (int) (wp_count_posts('aksrt_forum_topic')->publish ?? 0)
: 0;
$pending_topic_count = post_type_exists('aksrt_forum_topic')
? (int) (wp_count_posts('aksrt_forum_topic')->pending ?? 0)
: 0;
global $wpdb;
$post_count = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->comments} c
INNER JOIN {$wpdb->posts} p ON c.comment_post_ID = p.ID
WHERE p.post_type = 'aksrt_forum_topic' AND c.comment_approved = '1'"
);
$moderator_count = 0;
$pending_application_count = 0;
foreach ($boards as $board) {
$moderator_count += (int) ($board['moderator_count'] ?? 0);
$pending_application_count += (int) ($board['pending_application_count'] ?? 0);
}
return [
'board_count' => count($boards),
'topic_count' => $topic_count,
'post_count' => $post_count,
'pending_topic_count' => $pending_topic_count,
'moderator_count' => $moderator_count,
'pending_application_count' => $pending_application_count,
];
}
public function get_sticky_topics_for_board(int $board_id): array {
$viewer_user_id = get_current_user_id();
// 先取本版块内的置顶帖(plate/super),再取全局置顶帖(跨版块)。
$query = new \WP_Query([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'posts_per_page' => 20,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'orderby' => 'meta_value_num date',
'order' => 'DESC DESC',
'meta_query' => [
'relation' => 'OR',
[
'key' => '_aksrt_sticky',
'value' => [Forum::STICKY_PLATE, Forum::STICKY_SUPER],
'compare' => 'IN',
'type' => 'NUMERIC',
],
[
'key' => '_aksrt_sticky',
'value' => Forum::STICKY_GLOBAL,
'compare' => '=',
'type' => 'NUMERIC',
],
],
'tax_query' => [[
'taxonomy' => 'aksrt_forum_board',
'field' => 'term_id',
'terms' => $board_id,
]],
]);
$items = [];
while ($query->have_posts()) {
$query->the_post();
$post = get_post(get_the_ID());
if (!$post) {
continue;
}
$topic = $this->forum->build_topic_payload($post, $viewer_user_id);
if (!$topic['can_view']) {
continue;
}
$items[] = $topic;
}
wp_reset_postdata();
// 全局置顶帖可能不归属本版块,单独查询后合并去重。
$global = new \WP_Query([
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'posts_per_page' => 5,
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'meta_query' => [[
'key' => '_aksrt_sticky',
'value' => Forum::STICKY_GLOBAL,
'compare' => '=',
'type' => 'NUMERIC',
]],
]);
$existing_ids = array_map(static fn($t) => (int) $t['id'], $items);
while ($global->have_posts()) {
$global->the_post();
$post = get_post(get_the_ID());
if (!$post || in_array((int) $post->ID, $existing_ids, true)) {
continue;
}
$topic = $this->forum->build_topic_payload($post, $viewer_user_id);
if (!$topic['can_view']) {
continue;
}
$items[] = $topic;
}
wp_reset_postdata();
// 按 sticky_level DESC, date DESC 排序(全局 > 超级 > 版块)。
usort($items, function (array $a, array $b): int {
$la = (int) ($a['sticky_level'] ?? 0);
$lb = (int) ($b['sticky_level'] ?? 0);
if ($la !== $lb) {
return $lb <=> $la;
}
return strcmp((string) $b['date'], (string) $a['date']);
});
return array_slice($items, 0, 5);
}
/**
* Get topic detail payload for admin.
*/
public function get_topic_detail(int $topic_id, int $viewer_user_id = 0): array {
$post = get_post($topic_id);
if (!$post || $post->post_type !== 'aksrt_forum_topic') {
return [
'topic' => null,
'options' => [
'boards' => $this->forum->get_boards(),
'visibility' => $this->forum->get_visibility_options(),
'review' => $this->forum->get_review_options(),
],
];
}
return [
'topic' => $this->forum->build_topic_payload($post, $viewer_user_id),
'options' => [
'boards' => $this->forum->get_boards(),
'visibility' => $this->forum->get_visibility_options(),
'review' => $this->forum->get_review_options(),
],
];
}
// ── Tags + Search (extracted from facade) ──
public function get_tags(): array {
$terms = get_terms([
'taxonomy' => 'aksrt_forum_tag',
'hide_empty' => true,
'orderby' => 'count',
'order' => 'DESC',
]);
if (is_wp_error($terms)) {
return [];
}
return array_map(static function ($term): array {
$url = get_term_link($term);
return [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'count' => (int) $term->count,
'url' => !is_wp_error($url) ? (string) $url : '',
];
}, $terms);
}
/**
* Get tag archive page payload.
*/
public function get_tag_archive_view(array $args = []): array {
$term_id = (int) ($args['term_id'] ?? 0);
$paged = max(1, (int) ($args['paged'] ?? 1));
$per_page = 20;
$term = get_term($term_id, 'aksrt_forum_tag');
if (is_wp_error($term) || !$term) {
return ['topics' => [], 'term' => null, 'total' => 0, 'max_pages' => 0];
}
$query = new \WP_Query([
'post_type' => 'aksrt_forum_topic',
'posts_per_page' => $per_page,
'paged' => $paged,
'post_status' => 'publish',
'tax_query' => [[
'taxonomy' => 'aksrt_forum_tag',
'field' => 'term_id',
'terms' => $term_id,
]],
]);
$topics = [];
while ($query->have_posts()) {
$query->the_post();
$post = get_post(get_the_ID());
if ($post) {
$topics[] = $this->forum->build_topic_payload($post, get_current_user_id());
}
}
wp_reset_postdata();
return [
'topics' => $topics,
'term' => [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'description' => (string) $term->description,
'count' => (int) $term->count,
],
'total' => (int) $query->found_posts,
'max_pages' => (int) $query->max_num_pages,
];
}
public function increment_view_count(int $topic_id): void {
if ($topic_id <= 0) {
return;
}
$views = (int) get_post_meta($topic_id, '_aksrt_views', true);
update_post_meta($topic_id, '_aksrt_views', $views + 1);
}
public function ajax_search_boards(): void {
$search = sanitize_text_field((string) ($_GET['q'] ?? $_POST['q'] ?? ''));
$limit = min(50, max(5, (int) ($_GET['limit'] ?? 20)));
if ($this->forum->is_plate_cpt_active()) {
$args = [
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => $limit,
'no_found_rows' => true,
'orderby' => 'title',
'order' => 'ASC',
];
if ($search !== '') {
$args['s'] = $search;
}
$query = new \WP_Query($args);
$results = [];
while ($query->have_posts()) {
$query->the_post();
$p = get_post();
if ($p) {
$results[] = ['id' => $p->ID, 'name' => $p->post_title];
}
}
wp_reset_postdata();
} else {
$args = [
'taxonomy' => 'aksrt_forum_board',
'hide_empty' => false,
'number' => $limit,
'orderby' => 'name',
'order' => 'ASC',
];
if ($search !== '') {
$args['search'] = $search;
}
$terms = get_terms($args);
$results = [];
if (!is_wp_error($terms)) {
foreach ($terms as $term) {
$results[] = ['id' => (int) $term->term_id, 'name' => (string) $term->name];
}
}
}
wp_send_json_success($results);
}
public function ajax_search_tags(): void {
$search = sanitize_text_field((string) ($_GET['q'] ?? $_POST['q'] ?? ''));
$limit = min(50, max(5, (int) ($_GET['limit'] ?? 20)));
$args = [
'taxonomy' => 'aksrt_forum_tag',
'hide_empty' => false,
'number' => $limit,
'orderby' => 'count',
'order' => 'DESC',
];
if ($search !== '') {
$args['search'] = $search;
}
$terms = get_terms($args);
$results = [];
if (!is_wp_error($terms)) {
foreach ($terms as $term) {
$results[] = [
'id' => (int) $term->term_id,
'name' => (string) $term->name,
'count' => (int) $term->count,
];
}
}
wp_send_json_success($results);
}
}
-116
View File
@@ -1,116 +0,0 @@
<?php
/**
* @package AKSRT
*/
namespace AKSRT\Forum;
defined('ABSPATH') || exit;
trait ViewBuilderPart2 {
public function ajax_search_topics(): void {
$search = sanitize_text_field((string) ($_GET['q'] ?? $_POST['q'] ?? ''));
$limit = min(50, max(5, (int) ($_GET['limit'] ?? 20)));
if (!taxonomy_exists('aksrt_forum_topic_cat')) {
wp_send_json_success([]);
}
$args = [
'taxonomy' => 'aksrt_forum_topic_cat',
'hide_empty' => false,
'number' => $limit,
'orderby' => 'name',
'order' => 'ASC',
];
if ($search !== '') {
$args['search'] = $search;
}
$terms = get_terms($args);
$results = [];
if (!is_wp_error($terms)) {
foreach ($terms as $term) {
$results[] = ['id' => (int) $term->term_id, 'name' => (string) $term->name];
}
}
wp_send_json_success($results);
}
public function template_routing(string $template): string {
// Plate CPT single → board template
if (is_singular('aksrt_forum_plate')) {
$plate_template = AKSRT_DIR . '/templates/forum/forum-plate.php';
if (file_exists($plate_template)) {
return $plate_template;
}
$board_template = AKSRT_DIR . '/templates/forum/forum-board.php';
if (file_exists($board_template)) {
return $board_template;
}
}
// Plate category (分区) archive
if (is_tax('aksrt_plate_cat')) {
$cat_template = AKSRT_DIR . '/templates/forum/forum-plate-cat.php';
if (file_exists($cat_template)) {
return $cat_template;
}
}
if (is_tax('aksrt_forum_board')) {
$board_template = AKSRT_DIR . '/templates/forum/forum-board.php';
if (file_exists($board_template)) {
return $board_template;
}
}
if (is_tax('aksrt_forum_tag')) {
$tag_template = AKSRT_DIR . '/templates/forum/forum-tag.php';
if (file_exists($tag_template)) {
return $tag_template;
}
}
if (is_singular('aksrt_forum_topic')) {
if (isset($_GET['edit']) && $_GET['edit'] === '1') {
$edit_template = AKSRT_DIR . '/templates/forum/forum-edit.php';
if (file_exists($edit_template)) {
return $edit_template;
}
}
$topic_template = AKSRT_DIR . '/templates/forum/forum-topic.php';
if (file_exists($topic_template)) {
return $topic_template;
}
}
return $template;
}
public function ajax_topic_list(): void {
$user_id = get_current_user_id();
if (!$this->forum->is_forum_admin($user_id)) {
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
}
$board_id = (int) ($_GET['board_id'] ?? 0);
$limit = max(1, min(100, (int) ($_GET['limit'] ?? 20)));
$view = $this->get_topic_list([
'board_id' => $board_id,
'limit' => $limit,
'status' => 'all',
'viewer_user_id' => get_current_user_id(),
'include_hidden' => true,
]);
wp_send_json_success([
'topics' => $view['items'],
'stats' => $this->get_stats(),
]);
}
}
+3 -61
View File
@@ -89,56 +89,11 @@ class CreatorRevenue {
$buyer_id = (int) ($order['user_id'] ?? 0);
$items = [];
if (class_exists('\AKSRT\Shop\Shop')) {
$shop = \AKSRT\Shop\Shop::get_instance();
if (method_exists($shop, 'get_order_items')) {
$items = (array) $shop->get_order_items($order_number);
}
}
if (!empty($items)) {
$this->settle_shop_items($order_number, $buyer_id, $items);
return;
}
$this->settle_non_shop_cash_order($order_number, $order, $buyer_id);
}
/**
* @param list<array<string,mixed>> $items
*/
private function settle_shop_items(string $order_number, int $buyer_id, array $items): void {
$revenue = Revenue::get_instance();
foreach ($items as $item) {
$product_id = (int) ($item['product_id'] ?? 0);
$pay_price = (float) ($item['pay_price'] ?? $item['total_price'] ?? 0);
if ($product_id <= 0 || $pay_price <= 0) {
continue;
}
$author_id = (int) get_post_field('post_author', $product_id);
if ($author_id <= 0 || $author_id === $buyer_id) {
continue;
}
$ratio = $this->resolve_share_ratio($author_id);
if ($ratio <= 0) {
continue;
}
$amount = round($pay_price * $ratio / 100, 2);
if ($amount <= 0) {
continue;
}
$revenue->add($author_id, $amount, $order_number, 'order');
}
}
/**
* Content / topic / plate_follow cash orders (no shop line items).
* Content / topic cash orders (no shop line items).
*/
private function settle_non_shop_cash_order(string $order_number, array $order, int $buyer_id): void {
$meta = $this->decode_note($order);
@@ -168,24 +123,11 @@ class CreatorRevenue {
return;
}
$is_plate = !empty($meta['plate_follow_pay'])
|| $product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_PLATE_FOLLOW;
$is_topic = !empty($meta['topic_pay'])
|| ($product_type === \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE
&& $post->post_type === 'aksrt_forum_topic');
$is_content = !empty($meta['content_pay'])
&& $post->post_type === 'post';
// Resource without content_pay/topic_pay and not a known cash kind: skip.
if (!$is_plate && !$is_topic && !$is_content) {
return;
}
// Guard: plate must be plate CPT; topic must be topic CPT.
if ($is_plate && $post->post_type !== 'aksrt_forum_plate') {
return;
}
if ($is_topic && $post->post_type !== 'aksrt_forum_topic') {
// Resource without content_pay and not a known cash kind: skip.
if (!$is_content) {
return;
}
-26
View File
@@ -152,32 +152,6 @@ class Capabilities {
}
}
$forum = class_exists('\AKSRT\Forum\Forum') ? \AKSRT\Forum\Forum::get_instance() : null;
if (!empty($row['moderator']) && $forum && $context > 0) {
if ($forum->get_user_plate_tier($user_id, $context) >= \AKSRT\Forum\Forum::TIER_MODERATOR) {
return true;
}
}
if (!empty($row['plate_author']) && $forum && $context > 0) {
if ($forum->get_user_plate_tier($user_id, $context) >= \AKSRT\Forum\Forum::TIER_PLATE_AUTHOR) {
return true;
}
}
if (!empty($row['cat_moderator']) && $forum) {
if ($context > 0) {
if ($forum->get_user_plate_tier($user_id, $context) >= \AKSRT\Forum\Forum::TIER_CAT_MOD) {
return true;
}
} else {
if (method_exists($forum, 'is_cat_moderator_anywhere') && $forum->is_cat_moderator_anywhere($user_id)) {
return true;
}
}
}
return false;
}
-3
View File
@@ -59,9 +59,6 @@ trait DisplayPageGroups {
'author',
'single',
'search',
'forum_home',
'forum_plate',
'forum_post',
'page',
];
$show_on = isset($s['header_components_show_on']) ? (array) $s['header_components_show_on'] : ['home'];
-5
View File
@@ -124,11 +124,6 @@ class DisplayRuntime {
$labels = [
'post' => __('文章', 'aksrt'),
'user' => __('用户', 'aksrt'),
'product' => __('商品', 'aksrt'),
'forum' => __('帖子', 'aksrt'),
'forum_topic' => __('帖子', 'aksrt'),
'plate' => __('版块', 'aksrt'),
'forum_plate' => __('版块', 'aksrt'),
'page' => __('页面', 'aksrt'),
];
$enabled = $types['enabled'] ?? $types;
-10
View File
@@ -36,16 +36,6 @@ trait DomainGroups {
return ForumShop::get_shop_settings();
}
/**
* 获取论坛设置
*
* 对标 zibll 论坛后台 12 section,提供一站式读取。
* 所有 key 与 schema_forum() 一一对应。
*/
public static function get_forum_settings(): array {
return ForumShop::get_forum_settings();
}
/**
* 获取文档模式设置
*/
+1 -163
View File
@@ -182,166 +182,4 @@ class ForumShop {
'author_show' => $author_show,
];
}
/**
* 获取论坛设置
*
* 对标 zibll 论坛后台 12 section,提供一站式读取。
* 所有 key 与 schema_forum() 一一对应。
*/
public static function get_forum_settings(): array {
$s = Settings::get_all();
// helper: bool with default true
$bool = static fn(string $k, bool $d = false): bool =>
isset($s[$k]) ? !empty($s[$k]) : $d;
// helper: int
$int = static fn(string $k, int $d): int =>
(int) ($s[$k] ?? $d);
// helper: string
$str = static fn(string $k, string $d): string =>
sanitize_text_field((string) ($s[$k] ?? $d));
// helper: select with allowlist
$sel = static fn(string $k, string $d, array $allow): string =>
in_array($s[$k] ?? '', $allow, true) ? $s[$k] : $d;
return [
// ── basic ──
'enabled' => $bool('bbs_s', true),
'admin_users' => array_map('absint', (array) ($s['bbs_admin_users'] ?? [])),
// ── names ──
'names' => [
'forum' => $str('bbs_forum_display_name', '论坛'),
'plate' => $str('bbs_plate_display_name', '版块'),
'posts' => $str('bbs_posts_display_name', '帖子'),
'topic' => $str('bbs_topic_display_name', '话题'),
'tag' => $str('bbs_tag_display_name', '标签'),
'comment' => $str('bbs_comment_display_name', '回复'),
'question_badge' => $str('bbs_question_badge_name', '提问'),
'question_ok_badge' => $str('bbs_question_ok_badge_name', '已解决'),
'cat_moderator' => $str('bbs_cat_moderator_name', '分区版主'),
'plate_author' => $str('bbs_plate_author_name', '超级版主'),
'plate_moderator' => $str('bbs_plate_moderator_name', '版主'),
'plate_follow' => $str('bbs_plate_follow_name', '关注'),
],
// ── homepage ──
'homepage' => [
'tabs' => (array) ($s['bbs_home_tab'] ?? [
['type' => 'follow', 'label' => '关注'],
['type' => 'synthesis', 'label' => '综合'],
['type' => 'plate', 'label' => '版块'],
]),
'tab_active_index' => $int('bbs_home_tab_active_index', 1),
'quick_posts' => $bool('bbs_home_tab_quick_posts_s', true),
'tab_swiper' => $bool('bbs_home_tab_swiper', true),
],
// ── board_page ──
'board_page' => [
'top_info' => $bool('bbs_plate_top_info_s', true),
'tabs' => (array) ($s['bbs_plate_tab'] ?? [
['label' => '全部', 'sort' => 'date'],
['label' => '推荐', 'sort' => 'featured'],
['label' => '最新回复', 'sort' => 'last_reply'],
['label' => '热门', 'sort' => 'hot'],
['label' => '精华', 'sort' => 'essence'],
]),
'tab_active_index' => $int('bbs_plate_tab_active_index', 0),
'tab_swiper' => $bool('bbs_plate_tab_swiper', true),
],
// ── post_page ──
'post_page' => [
'breadcrumbs' => $bool('bbs_breadcrumbs_s', true),
'breadcrumbs_home' => $bool('bbs_breadcrumbs_home', true),
'breadcrumbs_bbs_home' => $bool('bbs_breadcrumbs_bbs_home', true),
'breadcrumbs_bbs_home_name' => $str('bbs_breadcrumbs_bbs_home_name', '论坛'),
'breadcrumbs_plate_cat' => $bool('bbs_breadcrumbs_plate_cat', true),
'score_extra_max' => $int('bbs_score_extra_max', 5),
'hot_recommend_threshold' => $int('bbs_hot_recommend_threshold', 500),
'score_deduct_max' => $int('bbs_score_deduct_max', 3),
'maxheight_enabled' => $bool('bbs_posts_maxheight_s'),
'maxheight' => $int('bbs_posts_maxheight', 800),
'cover_ratio' => $sel('bbs_posts_cover_ratio', '16:9', ['16:9', '4:3', '1:1', '3:4']),
'cover_video_mute' => $bool('bbs_posts_cover_video_mute', true),
],
// ── reply ──
'reply' => [
'paginate_type' => $sel('bbs_reply_paginate_type', 'ajax', ['ajax', 'page']),
'paginate_ias' => $bool('bbs_reply_paginate_ias_s'),
'paginate_ias_max' => $int('bbs_reply_paginate_ias_max', 3),
'smilie' => $bool('bbs_comment_smilie', true),
'code' => $bool('bbs_comment_code', true),
'img' => $bool('bbs_comment_img', true),
'upload_img' => $bool('bbs_comment_upload_img'),
'quick_reply' => $bool('bbs_comment_quick_s', true),
'quick_phrases' => $str('bbs_comment_quick_often', "不错,学习了\n感谢分享\n期待更新"),
'placeholder' => $str('bbs_comment_placeholder', '写下你的回复…'),
'hot_threshold' => $int('bbs_comment_hot_threshold', 10),
],
// ── permissions ──
'permissions' => [
'post_add_limit' => $int('bbs_posts_add_limit_opt_max', 10),
'plate_add_limit' => $int('bbs_plate_add_limit_opt_max', 3),
'show_new_plate' => $bool('bbs_show_new_plate'),
'show_new_topic' => $bool('bbs_show_new_topic', true),
'show_apply_moderator' => $bool('bbs_show_apply_moderator', true),
'paid_content' => $bool('bbs_post_pay_hide_type_s'),
'vip_price' => $bool('bbs_post_pay_vip_price_s'),
'vip_1_discount' => $int('bbs_post_pay_vip_1_discount', 80),
'vip_2_discount' => $int('bbs_post_pay_vip_2_discount', 50),
'pay_price_limit' => $int('bbs_post_pay_price_limit', 1000),
'plate_follow_pay' => $bool('bbs_plate_follow_pay_s'),
'plate_follow_price' => $int('bbs_plate_follow_pay_price', 10),
'plate_follow_pay_type' => $sel('bbs_plate_follow_pay_type', 'money', ['money', 'points']),
],
// ── other ──
'other' => [
'vote_number_type' => $sel('bbs_vote_number_type', 'percent', ['percent', 'number']),
'vote_max' => $int('bbs_vote_max', 8),
'apply_moderator_desc' => $str('bbs_apply_moderator_desc', '欢迎有热情的用户申请成为版主,协助管理社区秩序。'),
'term_thumb' => esc_url_raw((string) ($s['bbs_term_thumb'] ?? '')),
'hide_pay_in_list' => $bool('bbs_post_lists_hide_pay_s'),
'title_strlen_limit' => $int('bbs_post_title_strlen_limit', 30),
't_placeholder' => $str('bbs_t_placeholder', '请输入标题'),
'c_placeholder' => $str('bbs_c_placeholder', '请输入内容'),
'html_suffix' => $bool('bbs_rewrite_suffix_html_s'),
'plate_slug' => $str('bbs_plate_rewrite_slug', 'board'),
'posts_slug' => $str('bbs_posts_rewrite_slug', 'topic'),
'edit_slug' => $str('bbs_posts_edit_rewrite_slug', 'topic-edit'),
'gutenberg' => $bool('bbs_posts_show_in_rest_s'),
],
// ── upload ──
'upload' => [
'img_size' => $int('bbs_upload_img_size', 2048),
'img_multiple' => $int('bbs_image_upload_multiple', 9),
'video_size' => $int('bbs_upload_video_size', 20480),
'file_size' => $int('bbs_upload_file_size', 10240),
'file_mimes' => $str('bbs_upload_file_mimes', 'jpg,jpeg,png,gif,mp4,zip,rar,pdf'),
],
// ── credits ──
'credits' => [
'post_new' => $int('bbs_credits_post_new', 5),
'plate_new' => $int('bbs_credits_plate_new', 10),
'comment_new' => $int('bbs_credits_comment_new', 2),
'comment_like' => $int('bbs_credits_comment_like', 1),
'score_extra' => $int('bbs_credits_score_extra', 3),
'essence' => $int('bbs_credits_essence', 10),
'posts_hot' => $int('bbs_credits_posts_hot', 5),
'hot_recommend_threshold' => $int('bbs_hot_recommend_threshold', 500),
'adopted' => $int('bbs_credits_adopted', 20),
'daily_max' => $int('bbs_credits_daily_max', 50),
],
];
}
}
}
-2
View File
@@ -226,8 +226,6 @@ class ThemeSearch {
'user' => 'search_type_user',
'category' => 'search_type_category',
'tag' => 'search_type_tag',
'product' => 'search_type_product',
'topic' => 'search_type_topic',
];
$enabled = [];
+1 -7
View File
@@ -16,7 +16,7 @@ class UserRuntime {
*/
public static function get_user_home_settings(): array {
$settings = Settings::get_all();
$allowed_tabs = ['posts', 'comments', 'favorites', 'credits', 'product', 'plate', 'forum', 'follow'];
$allowed_tabs = ['posts', 'comments', 'favorites', 'credits', 'follow'];
$parse_csv = function (string $key, string $fallback) use ($settings, $allowed_tabs): array {
$raw = $settings[$key] ?? $fallback;
@@ -276,16 +276,10 @@ class UserRuntime {
'registration_time',
'checkin_all_day',
'new_post',
'bbs_post',
'comment',
'followed',
'like',
'comment_like',
'bbs_adopt',
'bbs_comment_hot',
'bbs_post_hot',
'bbs_essence',
'bbs_score',
'views',
'pay_income',
'pay_rebate',
-19
View File
@@ -673,10 +673,6 @@ class Badges {
case 'new_post':
return (float) count_user_posts($user_id, 'post', true);
case 'bbs_post':
$types = post_type_exists('forum_topic') ? 'forum_topic' : (post_type_exists('topic') ? 'topic' : '');
return $types !== '' ? (float) count_user_posts($user_id, $types, true) : 0.0;
case 'comment':
$counts = get_comments(['user_id' => $user_id, 'count' => true, 'status' => 'approve']);
return (float) $counts;
@@ -693,21 +689,6 @@ class Badges {
case 'comment_like':
return (float) get_user_meta($user_id, 'aksrt_comment_like_sum', true);
case 'bbs_adopt':
return (float) get_user_meta($user_id, 'aksrt_bbs_adopt_count', true);
case 'bbs_comment_hot':
return (float) get_user_meta($user_id, 'aksrt_bbs_comment_hot_count', true);
case 'bbs_post_hot':
return (float) get_user_meta($user_id, 'aksrt_bbs_post_hot_count', true);
case 'bbs_essence':
return (float) get_user_meta($user_id, 'aksrt_bbs_essence_count', true);
case 'bbs_score':
return (float) get_user_meta($user_id, 'aksrt_bbs_score_sum', true);
case 'views':
$sum = (float) get_user_meta($user_id, 'aksrt_posts_views_sum', true);
if ($sum > 0) {
+3 -4
View File
@@ -124,7 +124,7 @@ class Center {
$vip_on = class_exists('\AKSRT\Settings\Settings')
&& !empty(\AKSRT\Settings\Settings::get_payment_vip_settings()['enabled']);
// Follow configured order; always auto-include enabled product surfaces.
// Follow configured order; always auto-include enabled surfaces.
$order = $configured;
// Growth level is a dedicated user-center surface, separate from spendable points.
@@ -147,10 +147,9 @@ class Center {
}
array_splice($order, $insert_at, 0, ['vip']);
}
// Orders panel: always available when payment/shop stack exists (UserOrders or Shop).
// Orders panel: always available when payment stack exists (UserOrders or Order).
$orders_on = class_exists('\AKSRT\Payment\UserOrders')
|| class_exists('\AKSRT\Payment\Order')
|| class_exists('\AKSRT\Shop\Shop');
|| class_exists('\AKSRT\Payment\Order');
if ($orders_on && !in_array('orders', $order, true)) {
$insert_at = count($order);
foreach (['vip', 'credits', 'favorites'] as $anchor) {
-15
View File
@@ -54,21 +54,6 @@ class Home {
'icon' => 'wallet',
'priority' => 40,
],
'product' => [
'label' => __('商品', 'aksrt'),
'icon' => 'shopping-bag',
'priority' => 50,
],
'plate' => [
'label' => __('圈子', 'aksrt'),
'icon' => 'users',
'priority' => 60,
],
'forum' => [
'label' => __('论坛', 'aksrt'),
'icon' => 'comments',
'priority' => 70,
],
'follow' => [
'label' => __('粉丝', 'aksrt'),
'icon' => 'user-plus',
-61
View File
@@ -39,12 +39,6 @@ class Level {
add_action('aksrt_post_liked', [$this, 'on_post_like'], 20, 3);
add_action('aksrt_comment_liked', [$this, 'on_comment_like'], 20, 3);
add_action('aksrt_post_favorited', [$this, 'on_post_favorite'], 20, 3);
add_action('aksrt_forum_topic_scored', [$this, 'on_forum_scored'], 20, 3);
add_action('aksrt_forum_topic_featured', [$this, 'on_forum_featured'], 20, 3);
add_action('aksrt_forum_topic_hot', [$this, 'on_forum_hot'], 20, 2);
add_action('aksrt_forum_plate_hot', [$this, 'on_forum_plate_hot'], 20, 2);
add_action('aksrt_forum_reply_adopted', [$this, 'on_forum_adopted'], 20, 3);
add_action('aksrt_forum_comment_hot', [$this, 'on_forum_comment_hot'], 20, 2);
add_action('aksrt_user_level_changed', [$this, 'on_level_changed'], 20, 3);
}
@@ -85,14 +79,6 @@ class Level {
'post_favorite' => 2,
'comment_new' => 2,
'comment_like' => 1,
'bbs_posts_new' => 3,
'bbs_score_extra' => 1,
'bbs_essence' => 2,
'bbs_posts_hot' => 2,
'bbs_plate_new' => 2,
'bbs_plate_hot' => 2,
'bbs_adopt' => 2,
'bbs_comment_hot' => 2,
];
}
@@ -110,14 +96,6 @@ class Level {
'post_favorite' => __('文章被收藏', 'aksrt'),
'comment_new' => __('发表评论', 'aksrt'),
'comment_like' => __('评论获赞', 'aksrt'),
'bbs_posts_new' => __('发布帖子', 'aksrt'),
'bbs_score_extra' => __('帖子被加分', 'aksrt'),
'bbs_essence' => __('帖子评为精华', 'aksrt'),
'bbs_posts_hot' => __('帖子成为热门', 'aksrt'),
'bbs_plate_new' => __('创建版块', 'aksrt'),
'bbs_plate_hot' => __('版块成为热门', 'aksrt'),
'bbs_adopt' => __('回答被采纳', 'aksrt'),
'bbs_comment_hot' => __('评论成为神评', 'aksrt'),
];
}
@@ -394,10 +372,6 @@ class Level {
$key = '';
if ($post->post_type === 'post') {
$key = 'post_new';
} elseif ($post->post_type === 'aksrt_forum_topic') {
$key = 'bbs_posts_new';
} elseif ($post->post_type === 'aksrt_forum_plate') {
$key = 'bbs_plate_new';
}
if ($key !== '') {
self::award_once((int) $post->post_author, $key, 'post', (int) $post->ID, '_aksrt_level_' . $key);
@@ -480,41 +454,6 @@ class Level {
}
}
public function on_forum_scored($topic_id, $author_id, $amount): void {
if ((float) $amount > 0) {
$score_detail = get_post_meta((int) $topic_id, '_aksrt_score_detail', true);
$score_detail = is_array($score_detail) ? $score_detail : [];
$actor_id = 0;
foreach (array_reverse($score_detail, true) as $candidate => $entry) {
if ((float) ($entry['amount'] ?? 0) === (float) $amount) {
$actor_id = (int) $candidate;
break;
}
}
self::award_once((int) $author_id, 'bbs_score_extra', 'post', (int) $topic_id, '_aksrt_level_bbs_score_extra', 5, $actor_id);
}
}
public function on_forum_featured($topic_id, $author_id): void {
self::award_once((int) $author_id, 'bbs_essence', 'post', (int) $topic_id, '_aksrt_level_bbs_essence');
}
public function on_forum_hot($topic_id, $author_id): void {
self::award_once((int) $author_id, 'bbs_posts_hot', 'post', (int) $topic_id, '_aksrt_level_bbs_posts_hot');
}
public function on_forum_plate_hot($plate_id, $author_id): void {
self::award_once((int) $author_id, 'bbs_plate_hot', 'post', (int) $plate_id, '_aksrt_level_bbs_plate_hot');
}
public function on_forum_adopted($topic_id, $comment_id, $author_id): void {
self::award_once((int) $author_id, 'bbs_adopt', 'comment', (int) $comment_id, '_aksrt_level_bbs_adopt');
}
public function on_forum_comment_hot($comment_id, $author_id): void {
self::award_once((int) $author_id, 'bbs_comment_hot', 'comment', (int) $comment_id, '_aksrt_level_bbs_comment_hot');
}
public function on_level_changed(int $user_id, int $new_level, int $old_level): void {
$all = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
if ($new_level <= $old_level || isset($all['user_level_notify']) && empty($all['user_level_notify'])) {
-89
View File
@@ -12,11 +12,6 @@ class Metrics {
public const META_POST_LIKE_SUM = 'aksrt_posts_like_sum';
public const META_COMMENT_LIKE_SUM = 'aksrt_comment_like_sum';
public const META_POSTS_VIEWS_SUM = 'aksrt_posts_views_sum';
public const META_BBS_ADOPT = 'aksrt_bbs_adopt_count';
public const META_BBS_COMMENT_HOT = 'aksrt_bbs_comment_hot_count';
public const META_BBS_POST_HOT = 'aksrt_bbs_post_hot_count';
public const META_BBS_ESSENCE = 'aksrt_bbs_essence_count';
public const META_BBS_SCORE = 'aksrt_bbs_score_sum';
public const META_PAY_INCOME = 'aksrt_pay_income_sum';
public const META_PAY_REBATE = 'aksrt_pay_rebate_sum';
public const META_CHECKIN_ALL_DAY = 'aksrt_checkin_all_day';
@@ -38,12 +33,6 @@ class Metrics {
add_action('aksrt_user_followed', [$this, 'on_user_followed'], 20, 2);
add_action('aksrt_post_viewed', [$this, 'on_post_viewed'], 20, 2);
add_action('aksrt_forum_topic_featured', [$this, 'on_forum_topic_featured'], 20, 2);
add_action('aksrt_forum_reply_adopted', [$this, 'on_forum_reply_adopted'], 20, 3);
add_action('aksrt_forum_comment_hot', [$this, 'on_forum_comment_hot'], 20, 2);
add_action('aksrt_forum_topic_hot', [$this, 'on_forum_topic_hot'], 20, 2);
add_action('aksrt_forum_topic_scored', [$this, 'on_forum_topic_scored'], 20, 3);
add_action('aksrt_user_signed_in', [$this, 'on_user_signed_in'], 20, 1);
add_action('aksrt_creator_income_added', [$this, 'on_creator_income_added'], 20, 3);
add_action('aksrt_pay_rebate_added', [$this, 'on_pay_rebate_added'], 20, 3);
@@ -129,84 +118,6 @@ class Metrics {
* @param int $topic_id
* @param int $author_id
*/
public function on_forum_topic_featured($topic_id, $author_id = 0): void {
$author_id = (int) $author_id;
if ($author_id <= 0) {
$post = get_post((int) $topic_id);
$author_id = $post ? (int) $post->post_author : 0;
}
if ($author_id <= 0) {
return;
}
self::bump_user_meta($author_id, self::META_BBS_ESSENCE, 1.0);
self::reevaluate_medals($author_id);
}
/**
* @param int $topic_id
* @param int $comment_id
* @param int $adoptee_id
*/
public function on_forum_reply_adopted($topic_id, $comment_id, $adoptee_id): void {
unset($topic_id, $comment_id);
$adoptee_id = (int) $adoptee_id;
if ($adoptee_id <= 0) {
return;
}
self::bump_user_meta($adoptee_id, self::META_BBS_ADOPT, 1.0);
self::reevaluate_medals($adoptee_id);
}
/**
* @param int $comment_id
* @param int $author_id
*/
public function on_forum_comment_hot($comment_id, $author_id = 0): void {
$author_id = (int) $author_id;
if ($author_id <= 0) {
$c = get_comment((int) $comment_id);
$author_id = $c ? (int) $c->user_id : 0;
}
if ($author_id <= 0) {
return;
}
self::bump_user_meta($author_id, self::META_BBS_COMMENT_HOT, 1.0);
self::reevaluate_medals($author_id);
}
/**
* @param int $topic_id
* @param int $author_id
*/
public function on_forum_topic_hot($topic_id, $author_id = 0): void {
$author_id = (int) $author_id;
if ($author_id <= 0) {
$post = get_post((int) $topic_id);
$author_id = $post ? (int) $post->post_author : 0;
}
if ($author_id <= 0) {
return;
}
self::bump_user_meta($author_id, self::META_BBS_POST_HOT, 1.0);
self::reevaluate_medals($author_id);
}
/**
* @param int $topic_id
* @param int $author_id
* @param float $delta
*/
public function on_forum_topic_scored($topic_id, $author_id, $delta = 0): void {
unset($topic_id);
$author_id = (int) $author_id;
$delta = (float) $delta;
if ($author_id <= 0 || $delta == 0.0) {
return;
}
self::bump_user_meta($author_id, self::META_BBS_SCORE, $delta);
self::reevaluate_medals($author_id);
}
public function on_user_signed_in(int $user_id): void {
if ($user_id <= 0) {
return;
+1 -16
View File
@@ -17,12 +17,10 @@ class Restriction {
const TYPE_BAN = 'ban';
const SCOPE_GLOBAL = 'global';
const SCOPE_FORUM = 'forum';
const SCOPE_SHOP = 'shop';
const SCOPE_COMMENT = 'comment';
const TYPES = [self::TYPE_WARN, self::TYPE_MUTE, self::TYPE_SUSPEND, self::TYPE_BAN];
const SCOPES = [self::SCOPE_GLOBAL, self::SCOPE_FORUM, self::SCOPE_SHOP, self::SCOPE_COMMENT];
const SCOPES = [self::SCOPE_GLOBAL, self::SCOPE_COMMENT];
public static function get_instance(): self {
if (null === self::$instance) {
@@ -36,8 +34,6 @@ class Restriction {
add_filter('authenticate', [$this, 'enforce_login_ban'], 999, 3);
// Enforce comment mute
add_filter('pre_comment_on_post', [$this, 'enforce_comment_mute']);
// Enforce forum mute
add_filter('aksrt_before_create_topic', [$this, 'enforce_forum_mute']);
}
/**
@@ -196,17 +192,6 @@ class Restriction {
return $post_id;
}
/**
* Enforce forum mute.
*/
public function enforce_forum_mute($data) {
$user_id = get_current_user_id();
if ($user_id > 0 && $this->is_restricted($user_id, self::TYPE_MUTE, self::SCOPE_FORUM)) {
return new \WP_Error('forum_muted', '您已被限制在论坛发言。');
}
return $data;
}
/**
* Sync restriction meta for quick lookup.
*/
@@ -59,16 +59,6 @@ class RecentCommentsWidget extends \WP_Widget {
'orderby' => 'comment_date_gmt',
'order' => 'DESC',
];
// Include forum topics / products when active.
if (post_type_exists('aksrt_forum_topic')) {
$query['post_type'][] = 'aksrt_forum_topic';
} elseif (post_type_exists('forum_topic') || post_type_exists('forum_post')) {
// Legacy CPT names.
$query['post_type'][] = post_type_exists('forum_topic') ? 'forum_topic' : 'forum_post';
}
if (post_type_exists('aksrt_product')) {
$query['post_type'][] = 'aksrt_product';
}
if ($post_not_in) {
$query['post__not_in'] = $post_not_in;
}
-6
View File
@@ -82,9 +82,6 @@ class WidgetAuthorCard extends \WP_Widget {
}
$post_count = count_user_posts($author_id, 'post', true);
$topic_count = post_type_exists('aksrt_forum_topic')
? count_user_posts($author_id, 'aksrt_forum_topic', true)
: 0;
$followers = 0;
if (class_exists('\AKSRT\User\Follow')) {
$followers = \AKSRT\User\Follow::get_instance()->get_follower_count($author_id);
@@ -139,9 +136,6 @@ class WidgetAuthorCard extends \WP_Widget {
if ($show_stats) {
echo '<div class="aksrt-author-card__stats">';
echo '<span><b>' . esc_html((string) $post_count) . '</b>' . esc_html__('文章', 'aksrt') . '</span>';
if ($topic_count > 0) {
echo '<span><b>' . esc_html((string) $topic_count) . '</b>' . esc_html__('帖子', 'aksrt') . '</span>';
}
echo '<span><b>' . esc_html((string) $followers) . '</b>' . esc_html__('粉丝', 'aksrt') . '</span>';
echo '</div>';
}
-204
View File
@@ -1,204 +0,0 @@
<?php
/**
* Forum Board List Widget (board taxonomy + plate CPT fallback).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
class WidgetForumBoardList extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_forum_board_list',
__('论坛 - 版块列表', 'aksrt'),
['description' => __('显示论坛版块列表及话题数量。', 'aksrt')]
);
}
public static function render_html(array $instance): string {
$limit = max(1, min(50, (int) ($instance['count'] ?? $instance['showposts'] ?? 30)));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'menu_order'));
$style = sanitize_key((string) ($instance['style'] ?? 'list'));
if (!in_array($style, ['list', 'mini'], true)) {
$style = 'list';
}
// Prefer plate CPT (same as plate_lists lite).
if (post_type_exists('aksrt_forum_plate')) {
$query_args = [
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => $limit,
'order' => 'ASC',
'no_found_rows' => true,
];
switch ($orderby) {
case 'posts':
$query_args['meta_key'] = '_aksrt_posts_count';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
break;
case 'views':
$query_args['meta_key'] = '_aksrt_views';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
break;
case 'date':
$query_args['orderby'] = 'date';
$query_args['order'] = 'DESC';
break;
default:
$query_args['orderby'] = 'menu_order title';
}
$plates = get_posts($query_args);
if ($plates) {
ob_start();
if ($style === 'mini' && function_exists('aksrt_render_plate_row')) {
// Compact mini still uses shared plate row for consistent DOM/lazy.
echo '<div class="plate-lists aksrt-forum-widget__boards style-mini ajax-item">';
foreach ($plates as $plate) {
echo aksrt_render_plate_row($plate); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '</div>';
} elseif (function_exists('aksrt_render_plate_row')) {
echo '<div class="plate-lists aksrt-forum-widget__boards style-list ajax-item">';
foreach ($plates as $plate) {
echo aksrt_render_plate_row($plate); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '</div>';
} else {
echo '<ul class="aksrt-forum-widget__boards style-' . esc_attr($style) . ' ajax-item">';
foreach ($plates as $plate) {
$icon = (string) get_post_meta($plate->ID, '_aksrt_icon', true);
$count = (int) get_post_meta($plate->ID, '_aksrt_posts_count', true);
$thumb = get_the_post_thumbnail_url($plate->ID, 'thumbnail');
$ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
echo '<li class="aksrt-forum-widget__board">';
echo '<a href="' . esc_url(get_permalink($plate)) . '">';
if ($style === 'mini' && $thumb) {
echo '<img class="aksrt-forum-widget__thumb aksrt-lazy lazyload" data-src="' . esc_url($thumb) . '" src="' . esc_url($ph) . '" alt="" width="28" height="28" loading="lazy" />';
} elseif ($icon !== '') {
echo '<i class="' . esc_attr($icon) . '"></i> ';
}
echo '<span class="aksrt-forum-widget__name">' . esc_html(get_the_title($plate)) . '</span>';
echo '<span class="aksrt-forum-widget__count">' . esc_html((string) $count) . '</span>';
echo '</a></li>';
}
echo '</ul>';
}
return (string) ob_get_clean();
}
}
// Taxonomy fallback.
$term_args = [
'taxonomy' => 'aksrt_forum_board',
'hide_empty' => !isset($instance['hide_empty']) || !empty($instance['hide_empty']),
'number' => $limit,
'order' => 'ASC',
];
if ($orderby === 'count') {
$term_args['orderby'] = 'count';
$term_args['order'] = 'DESC';
} elseif ($orderby === 'name') {
$term_args['orderby'] = 'name';
} else {
$term_args['orderby'] = 'meta_value_num';
$term_args['meta_key'] = '_aksrt_sort_order';
}
$boards = get_terms($term_args);
if (is_wp_error($boards) || empty($boards)) {
return function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无版块', 'aksrt'))
: '<p class="aksrt-forum-widget__empty ajax-item">' . esc_html__('暂无版块', 'aksrt') . '</p>';
}
ob_start();
echo '<ul class="aksrt-forum-widget__boards style-' . esc_attr($style) . ' ajax-item">';
foreach ($boards as $board) {
$icon = get_term_meta($board->term_id, '_aksrt_icon', true);
echo '<li class="aksrt-forum-widget__board">';
echo '<a href="' . esc_url(get_term_link($board)) . '">';
if ($icon) {
echo '<i class="' . esc_attr($icon) . '"></i> ';
}
echo '<span class="aksrt-forum-widget__name">' . esc_html($board->name) . '</span>';
echo '<span class="aksrt-forum-widget__count">' . esc_html((string) (int) $board->count) . '</span>';
echo '</a></li>';
}
echo '</ul>';
return (string) ob_get_clean();
}
public function widget($args, $instance): void {
$title = !empty($instance['title']) ? $instance['title'] : __('版块列表', 'aksrt');
$html = self::render_html($instance);
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
$title = !empty($instance['title']) ? $instance['title'] : __('版块列表', 'aksrt');
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
</p>
<p>
<label><?php esc_html_e('数量', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="50" name="<?php echo esc_attr($this->get_field_name('count')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['count'] ?? 30))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('排序', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
<option value="menu_order" <?php selected(($instance['orderby'] ?? 'menu_order'), 'menu_order'); ?>><?php esc_html_e('菜单顺序', 'aksrt'); ?></option>
<option value="posts" <?php selected(($instance['orderby'] ?? ''), 'posts'); ?>><?php esc_html_e('帖子数', 'aksrt'); ?></option>
<option value="views" <?php selected(($instance['orderby'] ?? ''), 'views'); ?>><?php esc_html_e('浏览量', 'aksrt'); ?></option>
<option value="date" <?php selected(($instance['orderby'] ?? ''), 'date'); ?>><?php esc_html_e('创建时间', 'aksrt'); ?></option>
<option value="name" <?php selected(($instance['orderby'] ?? ''), 'name'); ?>><?php esc_html_e('名称', 'aksrt'); ?></option>
<option value="count" <?php selected(($instance['orderby'] ?? ''), 'count'); ?>><?php esc_html_e('分类计数', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('样式', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="list" <?php selected(($instance['style'] ?? 'list'), 'list'); ?>><?php esc_html_e('列表', 'aksrt'); ?></option>
<option value="mini" <?php selected(($instance['style'] ?? ''), 'mini'); ?>><?php esc_html_e('迷你图', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('hide_empty')); ?>" value="1" <?php checked(!isset($instance['hide_empty']) || !empty($instance['hide_empty'])); ?>> <?php esc_html_e('隐藏空版块(分类模式)', 'aksrt'); ?></label>
</p>
<?php
$this->render_common_form_section($instance);
}
public function update($new_instance, $old_instance): array {
$orderby = sanitize_key((string) ($new_instance['orderby'] ?? 'menu_order'));
$allowed = ['menu_order', 'posts', 'views', 'date', 'name', 'count'];
if (!in_array($orderby, $allowed, true)) {
$orderby = 'menu_order';
}
$business = [
'title' => sanitize_text_field($new_instance['title'] ?? ''),
'count' => max(1, min(50, (int) ($new_instance['count'] ?? 30))),
'orderby' => $orderby,
'style' => ($new_instance['style'] ?? '') === 'mini' ? 'mini' : 'list',
'hide_empty' => !empty($new_instance['hide_empty']) ? 1 : 0,
]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
-274
View File
@@ -1,274 +0,0 @@
<?php
/**
* Forum hot topics with shared mini renderer + ajax pager.
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
class WidgetForumHotTopics extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_forum_hot_topics',
__('论坛 - 热门话题', 'aksrt'),
[
'description' => __('显示论坛热门话题列表(头像/身份/阅读)。', 'aksrt'),
'classname' => 'widget_aksrt_forum_hot_topics',
]
);
}
public static function build_query_args(array $instance, int $paged = 1): array {
$count = max(1, min(20, (int) ($instance['count'] ?? 5)));
$period = sanitize_key((string) ($instance['period'] ?? 'week'));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'views'));
$plate = max(0, (int) ($instance['plate'] ?? $instance['plate_id'] ?? 0));
$date_query = [];
if ($period === 'week') {
$date_query = [['after' => '7 days ago']];
} elseif ($period === 'month') {
$date_query = [['after' => '30 days ago']];
}
$query = [
'post_type' => 'aksrt_forum_topic',
'posts_per_page' => $count,
'paged' => max(1, $paged),
'post_status' => 'publish',
'date_query' => $date_query,
];
if ($orderby === 'comments') {
$query['orderby'] = 'comment_count';
$query['order'] = 'DESC';
} elseif ($orderby === 'date') {
$query['orderby'] = 'date';
$query['order'] = 'DESC';
} elseif ($orderby === 'essence') {
$query['meta_query'] = [[
'key' => '_aksrt_essence',
'value' => '1',
]];
$query['orderby'] = 'date';
$query['order'] = 'DESC';
} else {
$query['meta_key'] = '_aksrt_views';
$query['orderby'] = 'meta_value_num';
$query['order'] = 'DESC';
}
if ($plate > 0) {
$mq = isset($query['meta_query']) ? $query['meta_query'] : [];
$mq[] = [
'key' => '_aksrt_plate_id',
'value' => $plate,
];
$query['meta_query'] = array_values($mq);
}
return $query;
}
public static function render_items_html(\WP_Query $query, int $start = 0, string $style = 'mini'): string {
if (!$query->have_posts()) {
return '';
}
$style = $style === 'rank' ? 'rank' : 'mini';
ob_start();
if ($style === 'rank') {
echo '<div class="forum-posts-lists aksrt-forum-widget__list is-rank">';
} else {
echo '<div class="forum-posts-lists aksrt-forum-widget__list is-mini">';
}
$i = $start;
while ($query->have_posts()) {
$query->the_post();
$i++;
if ($style === 'rank') {
$tone = $i === 1 ? 'jb-red' : ($i === 2 ? 'jb-yellow' : ($i === 3 ? 'b-gray' : ''));
echo '<div class="aksrt-forum-widget__rank-row relative ajax-item">';
if ($i <= 3 && $tone !== '') {
echo '<badge class="img-badge left hot ' . esc_attr($tone) . '"><i>TOP' . (int) $i . '</i></badge>';
} else {
echo '<span class="aksrt-forum-widget__rank">' . esc_html((string) $i) . '</span>';
}
if (function_exists('aksrt_render_forum_topic_mini')) {
echo aksrt_render_forum_topic_mini(null, 'ajax-item is-rank-item', true); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
$views = (int) get_post_meta(get_the_ID(), '_aksrt_views', true);
echo '<a href="' . esc_url(get_permalink()) . '" class="aksrt-forum-widget__link">' . esc_html(get_the_title()) . '</a>';
echo '<span class="aksrt-forum-widget__views"><i class="fa fa-eye" aria-hidden="true"></i> ' . esc_html((string) $views) . '</span>';
}
echo '</div>';
} elseif (function_exists('aksrt_render_forum_topic_mini')) {
echo aksrt_render_forum_topic_mini(null, 'ajax-item', true); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
$views = (int) get_post_meta(get_the_ID(), '_aksrt_views', true);
echo '<div class="aksrt-forum-widget__item ajax-item">';
echo '<a href="' . esc_url(get_permalink()) . '" class="aksrt-forum-widget__link">' . esc_html(get_the_title()) . '</a>';
echo '<span class="aksrt-forum-widget__views"><i class="fa fa-eye" aria-hidden="true"></i> ' . esc_html((string) $views) . '</span>';
echo '</div>';
}
}
echo '</div>';
wp_reset_postdata();
return (string) ob_get_clean();
}
/**
* @return array{html:string,paginate_html:string,has_more:bool,page:int,max_pages:int,total:int}
*/
public static function query_payload(array $instance, int $page = 1): array {
$page = max(1, $page);
$count = max(1, min(20, (int) ($instance['count'] ?? 5)));
$paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
$style = sanitize_key((string) ($instance['style'] ?? 'mini'));
if (!in_array($style, ['mini', 'rank'], true)) {
$style = 'mini';
}
$q = new \WP_Query(self::build_query_args($instance, $page));
$html = self::render_items_html($q, ($page - 1) * $count, $style);
if ($html === '' && $page === 1) {
$html = function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无话题', 'aksrt'))
: '<p class="aksrt-forum-widget__empty ajax-item">' . esc_html__('暂无话题', 'aksrt') . '</p>';
}
$max_pages = max(1, (int) $q->max_num_pages);
$total = (int) $q->found_posts;
$has_more = $page < $max_pages;
$base_qs = array_filter([
'count' => $count,
'period' => sanitize_key((string) ($instance['period'] ?? 'week')),
'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'views')),
'style' => $style,
'plate' => max(0, (int) ($instance['plate'] ?? 0)),
'paginate' => $paginate,
], static function ($v) {
return $v !== '' && $v !== null;
});
$rest_base = add_query_arg($base_qs, rest_url('aksrt/v1/widgets/forum-topics'));
$paginate_html = '';
if ($paginate === 'ajax' && $has_more) {
$next = add_query_arg(['page' => $page + 1], $rest_base);
$paginate_html = function_exists('aksrt_get_ajax_next_paginate')
? aksrt_get_ajax_next_paginate($total, $page, $count, $next)
: '<div class="text-center theme-pagination ajax-pag"><div class="next-page ajax-next"><a href="' . esc_url($next) . '">' . esc_html__('加载更多', 'aksrt') . '</a></div></div>';
} elseif ($paginate === 'number' && $max_pages > 1) {
$paginate_html = function_exists('aksrt_get_ajax_number_paginate')
? aksrt_get_ajax_number_paginate($total, $page, $count, $rest_base)
: '<div class="ajax-pag hide"><div class="next-page ajax-next"><a href="#"></a></div></div>';
} else {
$paginate_html = '<div class="ajax-pag hide"><div class="next-page ajax-next"><a href="#"></a></div></div>';
}
return compact('html', 'paginate_html', 'has_more', 'page', 'max_pages', 'total');
}
public function widget($args, $instance): void {
if (!post_type_exists('aksrt_forum_topic')) {
return;
}
$title = !empty($instance['title']) ? $instance['title'] : __('热门话题', 'aksrt');
$instance['paginate'] = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
$payload = self::query_payload($instance, 1);
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$rest = rest_url('aksrt/v1/widgets/forum-topics');
$affix = function_exists('aksrt_widget_affix_attrs') ? aksrt_widget_affix_attrs($instance) : ['class' => '', 'attr' => ''];
$wrap = 'ajaxpager' . $affix['class'];
echo '<div class="' . esc_attr($wrap) . '" data-aksrt-ajaxpager data-aksrt-ajax-url="' . esc_url($rest) . '"' . $affix['attr'] . '>';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader data-aksrt-ajax-loader-hide hidden></div>';
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
$title = !empty($instance['title']) ? $instance['title'] : __('热门话题', 'aksrt');
$count = !empty($instance['count']) ? (int) $instance['count'] : 5;
$period = !empty($instance['period']) ? $instance['period'] : 'week';
$orderby = !empty($instance['orderby']) ? $instance['orderby'] : 'views';
$style = !empty($instance['style']) ? $instance['style'] : 'mini';
$plate = (int) ($instance['plate'] ?? 0);
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('count')); ?>"><?php esc_html_e('数量:', 'aksrt'); ?></label>
<input class="tiny-text" id="<?php echo esc_attr($this->get_field_id('count')); ?>" name="<?php echo esc_attr($this->get_field_name('count')); ?>" type="number" value="<?php echo esc_attr((string) $count); ?>" min="1" max="20">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('period')); ?>"><?php esc_html_e('时间范围:', 'aksrt'); ?></label>
<select id="<?php echo esc_attr($this->get_field_id('period')); ?>" name="<?php echo esc_attr($this->get_field_name('period')); ?>">
<option value="all" <?php selected($period, 'all'); ?>><?php esc_html_e('全部', 'aksrt'); ?></option>
<option value="week" <?php selected($period, 'week'); ?>><?php esc_html_e('最近一周', 'aksrt'); ?></option>
<option value="month" <?php selected($period, 'month'); ?>><?php esc_html_e('最近一月', 'aksrt'); ?></option>
</select>
</p>
<p>
<label><?php esc_html_e('排序', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
<option value="views" <?php selected($orderby, 'views'); ?>><?php esc_html_e('热门浏览', 'aksrt'); ?></option>
<option value="comments" <?php selected($orderby, 'comments'); ?>><?php esc_html_e('评论数', 'aksrt'); ?></option>
<option value="date" <?php selected($orderby, 'date'); ?>><?php esc_html_e('最新', 'aksrt'); ?></option>
<option value="essence" <?php selected($orderby, 'essence'); ?>><?php esc_html_e('精华', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('样式', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="mini" <?php selected($style, 'mini'); ?>><?php esc_html_e('迷你信息行', 'aksrt'); ?></option>
<option value="rank" <?php selected($style, 'rank'); ?>><?php esc_html_e('排行榜', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('版块 ID(可选)', 'aksrt'); ?>
<input class="tiny-text" type="number" min="0" name="<?php echo esc_attr($this->get_field_name('plate')); ?>" value="<?php echo esc_attr((string) $plate); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('分页', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('paginate')); ?>">
<option value="ajax" <?php selected(($instance['paginate'] ?? 'ajax'), 'ajax'); ?>><?php esc_html_e('AJAX', 'aksrt'); ?></option>
<option value="number" <?php selected(($instance['paginate'] ?? ''), 'number'); ?>><?php esc_html_e('数字分页', 'aksrt'); ?></option>
<option value="none" <?php selected(($instance['paginate'] ?? ''), 'none'); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
</select>
</label>
</p>
<?php $this->render_common_form_section($instance); ?>
<?php
}
public function update($new_instance, $old_instance): array {
$orderby = sanitize_key((string) ($new_instance['orderby'] ?? 'views'));
if (!in_array($orderby, ['views', 'comments', 'date', 'essence'], true)) {
$orderby = 'views';
}
$style = sanitize_key((string) ($new_instance['style'] ?? 'mini'));
if (!in_array($style, ['mini', 'rank'], true)) {
$style = 'mini';
}
return $this->update_common_fields($new_instance, $old_instance, [
'title' => sanitize_text_field($new_instance['title'] ?? ''),
'count' => max(1, min(20, (int) ($new_instance['count'] ?? 5))),
'period' => in_array($new_instance['period'] ?? '', ['all', 'week', 'month'], true) ? $new_instance['period'] : 'week',
'orderby' => $orderby,
'style' => $style,
'plate' => max(0, (int) ($new_instance['plate'] ?? 0)),
'paginate' => in_array(sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')) : 'ajax',
]);
}
}
-174
View File
@@ -1,174 +0,0 @@
<?php
/**
* Current plate info mini-box (zibll zib_bbs_widget_ui_plate_info).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
class WidgetForumPlateInfo extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_forum_plate_info',
esc_html__('论坛 - 当前版块信息', 'aksrt'),
[
'description' => esc_html__('在版块/帖子页侧栏显示当前 plate 信息。', 'aksrt'),
'classname' => 'widget_aksrt_forum_plate_info',
]
);
}
/**
* @return array{html:string}
*/
public static function render_payload(array $instance, ?\WP_Post $plate = null): array {
$plate = $plate ?: self::resolve_current_plate_static();
if (!$plate) {
return ['html' => ''];
}
$show_follow = !isset($instance['show_follow']) || !empty($instance['show_follow']);
$show_compose = !isset($instance['show_compose']) || !empty($instance['show_compose']);
$show_stats = !isset($instance['show_stats']) || !empty($instance['show_stats']);
$id = (int) $plate->ID;
$url = get_permalink($id);
$thumb = get_the_post_thumbnail_url($id, 'medium');
$posts = (int) get_post_meta($id, '_aksrt_posts_count', true);
$reply = (int) get_post_meta($id, '_aksrt_reply_count', true);
$follow = (int) get_post_meta($id, '_aksrt_follow_count', true);
$views = (int) get_post_meta($id, '_aksrt_views', true);
$desc = $plate->post_excerpt !== '' ? $plate->post_excerpt : wp_trim_words(wp_strip_all_tags($plate->post_content), 30);
$icon = (string) get_post_meta($id, '_aksrt_icon', true);
$forum = class_exists('\AKSRT\Forum\Forum') ? \AKSRT\Forum\Forum::get_instance() : null;
$uid = get_current_user_id();
$is_followed = $forum && $uid > 0 ? $forum->is_user_following_plate($uid, $id) : false;
$compose_url = function_exists('aksrt_get_forum_compose_url')
? aksrt_get_forum_compose_url(['board' => $id])
: home_url('/');
ob_start();
echo '<div class="forum-header plate-info-sidebar aksrt-plate-info ajax-item">';
if ($thumb) {
$ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
echo '<div class="aksrt-plate-info__cover plate-thumb"><img class="aksrt-lazy lazyload fit-cover" data-src="' . esc_url($thumb) . '" src="' . esc_url($ph) . '" alt="" loading="lazy" /></div>';
} elseif ($icon !== '') {
echo '<div class="aksrt-plate-info__cover plate-thumb is-icon"><i class="' . esc_attr($icon) . '" aria-hidden="true"></i></div>';
}
echo '<div class="aksrt-plate-info__body">';
echo '<a class="aksrt-plate-info__title" href="' . esc_url($url) . '">' . esc_html(get_the_title($plate)) . '</a>';
if ($desc !== '') {
echo '<div class="aksrt-plate-info__desc">' . esc_html($desc) . '</div>';
}
if ($show_stats) {
// zibll count-mates: label above big number.
echo '<div class="aksrt-plate-info__stats count-mates text-center flex jsb">';
echo '<item class="mate-views"><div class="em09 opacity5 mb6">' . esc_html__('帖子', 'aksrt') . '</div><div class="em14">' . esc_html((string) $posts) . '</div></item>';
echo '<item class="mate-reply"><div class="em09 opacity5 mb6">' . esc_html__('回复', 'aksrt') . '</div><div class="em14">' . esc_html((string) $reply) . '</div></item>';
echo '<item class="mate-views"><div class="em09 opacity5 mb6">' . esc_html__('阅读', 'aksrt') . '</div><div class="em14">' . esc_html((string) $views) . '</div></item>';
echo '<item class="mate-follow"><div class="em09 opacity5 mb6">' . esc_html__('关注', 'aksrt') . '</div><div class="em14">' . esc_html((string) $follow) . '</div></item>';
echo '</div>';
}
$actions = [];
if ($show_follow && $uid > 0) {
$actions[] = sprintf(
'<button type="button" class="aksrt-plate-info__btn aksrt-plate-info__follow js-aksrt-follow-plate%s" data-plate-id="%d" data-followed="%s"><i class="fas fa-heart" aria-hidden="true"></i> %s</button>',
$is_followed ? ' is-followed' : '',
$id,
$is_followed ? '1' : '0',
esc_html($is_followed ? __('已关注', 'aksrt') : __('关注', 'aksrt'))
);
}
if ($show_compose) {
$actions[] = '<a class="aksrt-plate-info__btn aksrt-plate-info__compose" href="' . esc_url($compose_url) . '"><i class="fas fa-pen" aria-hidden="true"></i> ' . esc_html__('发帖', 'aksrt') . '</a>';
}
if ($actions) {
echo '<div class="aksrt-plate-info__actions">' . implode('', $actions) . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '</div></div>';
return ['html' => (string) ob_get_clean()];
}
public function widget($args, $instance): void {
$payload = self::render_payload($instance);
if ($payload['html'] === '') {
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public static function resolve_current_plate_static(): ?\WP_Post {
if (is_singular('aksrt_forum_plate')) {
$p = get_queried_object();
return $p instanceof \WP_Post ? $p : null;
}
if (is_singular('aksrt_forum_topic')) {
$topic_id = (int) get_queried_object_id();
$plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true);
if ($plate_id <= 0) {
$boards = get_the_terms($topic_id, 'aksrt_forum_board');
if (!is_wp_error($boards) && !empty($boards[0])) {
$linked = (int) get_term_meta((int) $boards[0]->term_id, '_aksrt_plate_id', true);
if ($linked > 0) {
$plate_id = $linked;
}
}
}
if ($plate_id > 0) {
$p = get_post($plate_id);
if ($p instanceof \WP_Post && $p->post_type === 'aksrt_forum_plate') {
return $p;
}
}
}
return null;
}
private function resolve_current_plate(): ?\WP_Post {
return self::resolve_current_plate_static();
}
public function form($instance): void {
?>
<p>
<label><?php esc_html_e('标题(可选)', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) ($instance['title'] ?? '')); ?>">
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('show_stats')); ?>" value="1" <?php checked(!isset($instance['show_stats']) || !empty($instance['show_stats'])); ?>> <?php esc_html_e('显示统计', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('show_follow')); ?>" value="1" <?php checked(!isset($instance['show_follow']) || !empty($instance['show_follow'])); ?>> <?php esc_html_e('显示关注按钮', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('show_compose')); ?>" value="1" <?php checked(!isset($instance['show_compose']) || !empty($instance['show_compose'])); ?>> <?php esc_html_e('显示发帖按钮', 'aksrt'); ?></label>
</p>
<p class="description"><?php esc_html_e('仅在版块页或帖子页显示对应版块信息。', 'aksrt'); ?></p>
<?php
$this->render_common_form_section($instance);
}
public function update($new_instance, $old_instance): array {
$business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'show_stats' => !empty($new_instance['show_stats']) ? 1 : 0,
'show_follow' => !empty($new_instance['show_follow']) ? 1 : 0,
'show_compose' => !empty($new_instance['show_compose']) ? 1 : 0,
]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
-284
View File
@@ -1,284 +0,0 @@
<?php
/**
* Forum plate list (zibll zib_bbs_widget_ui_plate_lists + ajax).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
class WidgetForumPlateLists extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_forum_plate_lists',
esc_html__('论坛 - 版块列表(Plate)', 'aksrt'),
[
'description' => esc_html__('展示论坛 plate 版块列表,支持排序/卡片/AJAX。', 'aksrt'),
'classname' => 'widget_aksrt_forum_plate_lists',
]
);
}
/**
* @return array
*/
public static function build_query_args(array $instance): array {
$limit = max(1, min(30, (int) ($instance['showposts'] ?? 8)));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'menu_order'));
$filter = sanitize_key((string) ($instance['filter'] ?? ''));
$cat_raw = (string) ($instance['cat'] ?? '');
$cat_ids = array_values(array_filter(array_map('intval', preg_split('/[\s,]+/', $cat_raw) ?: [])));
$query_args = [
'post_type' => 'aksrt_forum_plate',
'post_status' => 'publish',
'posts_per_page' => $limit,
'order' => 'ASC',
'no_found_rows' => true,
];
switch ($orderby) {
case 'date':
$query_args['orderby'] = 'date';
$query_args['order'] = 'DESC';
break;
case 'posts':
$query_args['meta_key'] = '_aksrt_posts_count';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
break;
case 'views':
$query_args['meta_key'] = '_aksrt_views';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
break;
case 'follow':
$query_args['meta_key'] = '_aksrt_follow_count';
$query_args['orderby'] = 'meta_value_num';
$query_args['order'] = 'DESC';
break;
default:
$query_args['orderby'] = 'menu_order title';
}
if ($cat_ids) {
$query_args['tax_query'] = [[
'taxonomy' => 'aksrt_plate_cat',
'field' => 'term_id',
'terms' => $cat_ids,
]];
}
if ($filter === 'is_hot') {
$query_args['meta_query'] = [[
'key' => '_aksrt_is_hot',
'value' => '1',
'compare' => '=',
]];
}
return $query_args;
}
public static function render_items_html(array $plates, string $style = 'default'): string {
if (!$plates) {
return '';
}
ob_start();
if ($style === 'card') {
echo '<div class="panel-plate scroll-plate aksrt-plate-cards">';
foreach ($plates as $post) {
if ($post instanceof \WP_Post) {
if (function_exists('aksrt_render_plate_card')) {
echo aksrt_render_plate_card($post); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
(new self())->render_card($post);
}
}
}
echo '</div>';
} else {
echo '<div class="plate-lists aksrt-plate-lists">';
foreach ($plates as $post) {
if ($post instanceof \WP_Post) {
if (function_exists('aksrt_render_plate_row')) {
echo aksrt_render_plate_row($post); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
(new self())->render_row($post);
}
}
}
echo '</div>';
}
return (string) ob_get_clean();
}
/**
* @return array{html:string}
*/
public static function query_payload(array $instance): array {
if (!post_type_exists('aksrt_forum_plate')) {
return ['html' => ''];
}
$style = sanitize_key((string) ($instance['style'] ?? 'default'));
$plates = get_posts(self::build_query_args($instance));
$html = self::render_items_html($plates ?: [], $style === 'card' ? 'card' : 'default');
if ($html === '') {
$html = function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无版块', 'aksrt'))
: '<p class="aksrt-plate-empty ajax-item">' . esc_html__('暂无版块', 'aksrt') . '</p>';
}
return ['html' => $html];
}
public function widget($args, $instance): void {
if (!post_type_exists('aksrt_forum_plate')) {
return;
}
$style = sanitize_key((string) ($instance['style'] ?? 'default'));
$payload = self::query_payload($instance);
if ($payload['html'] === '') {
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '<div class="widget-plate-lists mb20">';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function render_row(\WP_Post $post): void {
if (function_exists('aksrt_render_plate_row')) {
echo aksrt_render_plate_row($post); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
}
$id = (int) $post->ID;
$url = get_permalink($id);
$thumb = get_the_post_thumbnail_url($id, 'thumbnail');
$icon = (string) get_post_meta($id, '_aksrt_icon', true);
$count = (int) get_post_meta($id, '_aksrt_posts_count', true);
$desc = $post->post_excerpt !== '' ? $post->post_excerpt : wp_trim_words(wp_strip_all_tags($post->post_content), 18);
$ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
echo '<a class="plate-item flex aksrt-plate-item" href="' . esc_url($url) . '">';
echo '<div class="plate-thumb aksrt-plate-item__thumb">';
if ($thumb) {
echo '<img class="aksrt-lazy lazyload" data-src="' . esc_url($thumb) . '" src="' . esc_url($ph) . '" alt="" loading="lazy" />';
} elseif ($icon !== '') {
echo '<i class="' . esc_attr($icon) . '" aria-hidden="true"></i>';
} else {
echo '<i class="fas fa-comments" aria-hidden="true"></i>';
}
echo '</div>';
echo '<div class="item-info flex1 aksrt-plate-item__body">';
echo '<div class="info-header"><div class="forum-title aksrt-plate-item__title">' . esc_html(get_the_title($post)) . '</div></div>';
if ($desc !== '') {
echo '<div class="aksrt-plate-item__desc muted-2-color em09">' . esc_html($desc) . '</div>';
}
echo '<div class="aksrt-plate-item__meta"><span>' . esc_html(sprintf(/* translators: %d topic count */ __('%d 帖', 'aksrt'), $count)) . '</span></div>';
echo '</div></a>';
}
public function render_card(\WP_Post $post): void {
if (function_exists('aksrt_render_plate_card')) {
echo aksrt_render_plate_card($post); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
return;
}
$id = (int) $post->ID;
$url = get_permalink($id);
$thumb = get_the_post_thumbnail_url($id, 'medium');
$count = (int) get_post_meta($id, '_aksrt_posts_count', true);
$follow = (int) get_post_meta($id, '_aksrt_follow_count', true);
$ph = get_template_directory_uri() . '/assets/img/placeholder.svg';
echo '<a class="plate-card aksrt-plate-card" href="' . esc_url($url) . '">';
echo '<div class="plate-thumb aksrt-plate-card__cover">';
if ($thumb) {
echo '<img class="aksrt-lazy lazyload fit-cover" data-src="' . esc_url($thumb) . '" src="' . esc_url($ph) . '" alt="" loading="lazy" />';
}
echo '</div>';
echo '<div class="aksrt-plate-card__title forum-title">' . esc_html(get_the_title($post)) . '</div>';
echo '<div class="aksrt-plate-card__meta">';
echo '<span>' . esc_html(sprintf(__('%d 帖', 'aksrt'), $count)) . '</span>';
echo '<span>' . esc_html(sprintf(__('%d 关注', 'aksrt'), $follow)) . '</span>';
echo '</div></a>';
}
public function form($instance): void {
?>
<p>
<label><?php esc_html_e('标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) ($instance['title'] ?? __('版块列表', 'aksrt'))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('数量', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="30" name="<?php echo esc_attr($this->get_field_name('showposts')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['showposts'] ?? 8))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('排序', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
<option value="menu_order" <?php selected(($instance['orderby'] ?? 'menu_order'), 'menu_order'); ?>><?php esc_html_e('菜单顺序', 'aksrt'); ?></option>
<option value="date" <?php selected(($instance['orderby'] ?? ''), 'date'); ?>><?php esc_html_e('创建时间', 'aksrt'); ?></option>
<option value="posts" <?php selected(($instance['orderby'] ?? ''), 'posts'); ?>><?php esc_html_e('帖子数', 'aksrt'); ?></option>
<option value="views" <?php selected(($instance['orderby'] ?? ''), 'views'); ?>><?php esc_html_e('浏览量', 'aksrt'); ?></option>
<option value="follow" <?php selected(($instance['orderby'] ?? ''), 'follow'); ?>><?php esc_html_e('关注数', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('样式', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="default" <?php selected(($instance['style'] ?? 'default'), 'default'); ?>><?php esc_html_e('列表', 'aksrt'); ?></option>
<option value="card" <?php selected(($instance['style'] ?? ''), 'card'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('筛选', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('filter')); ?>">
<option value="" <?php selected(($instance['filter'] ?? ''), ''); ?>><?php esc_html_e('全部', 'aksrt'); ?></option>
<option value="is_hot" <?php selected(($instance['filter'] ?? ''), 'is_hot'); ?>><?php esc_html_e('仅热门', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('版块分类 term ID(逗号)', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('cat')); ?>" value="<?php echo esc_attr((string) ($instance['cat'] ?? '')); ?>">
</label>
</p>
<?php
$this->render_common_form_section($instance);
}
public function update($new_instance, $old_instance): array {
$orderby = sanitize_key((string) ($new_instance['orderby'] ?? 'menu_order'));
if (!in_array($orderby, ['menu_order', 'date', 'posts', 'views', 'follow'], true)) {
$orderby = 'menu_order';
}
$business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'showposts' => max(1, min(30, (int) ($new_instance['showposts'] ?? 8))),
'orderby' => $orderby,
'style' => ($new_instance['style'] ?? '') === 'card' ? 'card' : 'default',
'filter' => ($new_instance['filter'] ?? '') === 'is_hot' ? 'is_hot' : '',
'cat' => sanitize_text_field((string) ($new_instance['cat'] ?? '')),
]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
@@ -1,248 +0,0 @@
<?php
/**
* Current plate moderators (zibll zib_bbs_widget_ui_plate_moderator).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
class WidgetForumPlateModerator extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_forum_plate_moderator',
esc_html__('论坛 - 版主列表', 'aksrt'),
[
'description' => esc_html__('显示当前版块的版主与超级版主。', 'aksrt'),
'classname' => 'widget_aksrt_forum_plate_moderator',
]
);
}
/**
* Build apply entry HTML (modal form).
*/
public static function render_apply_html(int $plate_id, bool $show_apply = true): string {
if (!$show_apply || $plate_id <= 0) {
return '';
}
$forum = class_exists('\AKSRT\Forum\Forum') ? \AKSRT\Forum\Forum::get_instance() : null;
$cuid = get_current_user_id();
$show_always = true;
if (function_exists('aksrt_get_setting')) {
$show_always = (bool) aksrt_get_setting('bbs_show_apply_moderator', true);
}
$can_apply = $cuid > 0 && $forum && method_exists($forum, 'forum_user_can')
? $forum->forum_user_can($cuid, 'moderator_apply')
: ($cuid > 0);
if (!$show_always && !$can_apply) {
return '';
}
$is_mod = false;
if ($cuid > 0 && $forum && method_exists($forum, 'is_board_moderator')) {
$is_mod = $forum->is_board_moderator($cuid, $plate_id);
}
if ($is_mod || ($cuid > 0 && user_can($cuid, 'manage_options'))) {
return '';
}
if ($cuid <= 0) {
$login = function_exists('aksrt_get_login_url') ? aksrt_get_login_url(get_permalink($plate_id)) : wp_login_url(get_permalink($plate_id));
return '<a class="but hollow c-blue p2-10 em09" href="' . esc_url($login) . '">' . esc_html__('登录后申请版主', 'aksrt') . '</a>';
}
$pending = false;
if ($forum && method_exists($forum, 'get_board_moderator_applications')) {
foreach ((array) $forum->get_board_moderator_applications($plate_id) as $app) {
if ((int) ($app['user_id'] ?? 0) === $cuid && (string) ($app['status'] ?? '') === 'pending') {
$pending = true;
break;
}
}
}
if ($pending) {
return '<span class="badg c-yellow hollow em09">' . esc_html__('申请审核中', 'aksrt') . '</span>';
}
$uid = 'aksrt-mod-apply-' . $plate_id . '-' . wp_rand(1000, 9999);
$desc = function_exists('aksrt_get_setting') ? (string) aksrt_get_setting('bbs_apply_moderator_desc', '') : '';
if ($desc === '') {
$desc = __('欢迎有热情的用户申请成为版主,协助管理社区秩序。', 'aksrt');
}
ob_start();
echo '<button type="button" class="but hollow c-blue p2-10 em09" data-aksrt-mod-apply-open="' . esc_attr((string) $uid) . '">' . esc_html__('申请版主', 'aksrt') . '</button>';
echo '<div class="aksrt-mod-apply-modal" id="' . esc_attr((string) $uid) . '" hidden>';
echo '<div class="aksrt-mod-apply-modal__backdrop" data-aksrt-mod-apply-close></div>';
echo '<div class="aksrt-mod-apply-modal__dialog zib-widget" role="dialog" aria-modal="true">';
echo '<div class="flex jsb ac mb10"><div class="title-theme">' . esc_html__('申请版主', 'aksrt') . '</div>';
echo '<button type="button" class="but hollow p2-10 em09" data-aksrt-mod-apply-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">&times;</button></div>';
echo '<div class="muted-2-color em09 mb10">' . esc_html($desc) . '</div>';
echo '<form class="js-aksrt-moderator-apply-form" data-board-id="' . esc_attr((string) $plate_id) . '">';
echo '<label class="mb6 block">' . esc_html__('申请理由', 'aksrt') . '</label>';
echo '<textarea name="reason" rows="4" class="mb10" required placeholder="' . esc_attr__('请简要说明你的社区经验、擅长领域或管理计划。', 'aksrt') . '"></textarea>';
echo '<div class="js-aksrt-action-feedback mb10" aria-live="polite"></div>';
echo '<button type="submit" class="but c-blue padding-lg btn-block">' . esc_html__('提交版主申请', 'aksrt') . '</button>';
echo '</form></div></div>';
return (string) ob_get_clean();
}
/**
* @return array{html:string}
*/
public static function render_payload(array $instance, ?\WP_Post $plate = null): array {
$plate = $plate ?: WidgetForumPlateInfo::resolve_current_plate_static();
if (!$plate) {
return ['html' => ''];
}
$id = (int) $plate->ID;
$author_id = (int) $plate->post_author;
$mods = get_post_meta($id, '_aksrt_moderators', true);
if (!is_array($mods)) {
$mods = [];
}
$mods = array_values(array_unique(array_filter(array_map('intval', $mods))));
// Super-mods meta (zibll-like).
$super = get_post_meta($id, '_aksrt_super_moderators', true);
if (!is_array($super)) {
$super = [];
}
$super = array_values(array_unique(array_filter(array_map('intval', $super))));
$people = [];
if ($author_id > 0) {
$people[] = ['id' => $author_id, 'role' => 'author'];
}
foreach ($super as $sid) {
if ($sid === $author_id) {
continue;
}
$people[] = ['id' => $sid, 'role' => 'super'];
}
foreach ($mods as $mid) {
if ($mid === $author_id || in_array($mid, $super, true)) {
continue;
}
$people[] = ['id' => $mid, 'role' => 'moderator'];
}
$title_text = (string) ($instance['title_text'] ?? __('版主', 'aksrt'));
$show_avatar = !isset($instance['show_avatar']) || !empty($instance['show_avatar']);
$style = sanitize_key((string) ($instance['style'] ?? 'list'));
if (!in_array($style, ['list', 'grid'], true)) {
$style = 'list';
}
$show_apply = !isset($instance['apply_btn']) || !empty($instance['apply_btn']);
$apply_html = self::render_apply_html($id, $show_apply);
if (!$people && $apply_html === '') {
if (current_user_can('manage_options')) {
return [
'html' => function_exists('aksrt_get_widget_null')
? aksrt_get_widget_null(__('当前版块暂无版主', 'aksrt'))
: '<p class="widget-empty">' . esc_html__('当前版块暂无版主', 'aksrt') . '</p>',
];
}
return ['html' => ''];
}
ob_start();
echo '<div class="zib-widget moderator-box aksrt-plate-mods style-' . esc_attr($style) . ' ajax-item" data-aksrt-plate-mods="1">';
if ($title_text !== '' || $apply_html !== '') {
echo '<div class="mb10 flex jsb ac aksrt-plate-mods__head">';
echo $title_text !== '' ? '<div class="title-theme">' . esc_html($title_text) . '</div>' : '<div></div>';
echo $apply_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
}
if ($people) {
echo '<div class="aksrt-plate-mods__list">';
foreach ($people as $row) {
$uid = (int) $row['id'];
if (function_exists('aksrt_render_moderator_row')) {
echo aksrt_render_moderator_row($uid, (string) $row['role'], $show_avatar, $style); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
continue;
}
$user = get_userdata($uid);
if (!$user) {
continue;
}
$avatar = function_exists('aksrt_get_avatar_url')
? aksrt_get_avatar_url($uid, ['size' => 64])
: get_avatar_url($uid, ['size' => 64]);
$home = function_exists('aksrt_get_user_home_url')
? aksrt_get_user_home_url($uid)
: get_author_posts_url($uid);
if ($row['role'] === 'author' || $row['role'] === 'super') {
$role_label = __('超级版主', 'aksrt');
} else {
$role_label = __('版主', 'aksrt');
}
echo '<a class="aksrt-plate-mods__item role-' . esc_attr($row['role']) . '" href="' . esc_url($home) . '">';
if ($show_avatar) {
echo '<img class="aksrt-plate-mods__avatar aksrt-lazy lazyload" data-src="' . esc_url($avatar) . '" src="' . esc_url(get_template_directory_uri() . '/assets/img/placeholder.svg') . '" alt="" loading="lazy" width="40" height="40" />';
}
echo '<span class="aksrt-plate-mods__name">' . esc_html($user->display_name) . '</span>';
echo '<span class="aksrt-plate-mods__role">' . esc_html($role_label) . '</span>';
echo '</a>';
}
echo '</div>';
} else {
echo '<div class="muted-2-color em09 mb10">' . esc_html__('暂无版主,欢迎申请加入管理。', 'aksrt') . '</div>';
}
echo '</div>';
return ['html' => (string) ob_get_clean()];
}
public function widget($args, $instance): void {
$payload = self::render_payload($instance);
if ($payload['html'] === '') {
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
// title_text is rendered inside payload head (zibll-like); optional WP title still allowed.
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
?>
<p>
<label><?php esc_html_e('小标题文案', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title_text')); ?>" value="<?php echo esc_attr((string) ($instance['title_text'] ?? __('版主', 'aksrt'))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('样式', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="list" <?php selected(($instance['style'] ?? 'list'), 'list'); ?>><?php esc_html_e('列表', 'aksrt'); ?></option>
<option value="grid" <?php selected(($instance['style'] ?? ''), 'grid'); ?>><?php esc_html_e('头像网格', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('show_avatar')); ?>" value="1" <?php checked(!isset($instance['show_avatar']) || !empty($instance['show_avatar'])); ?>> <?php esc_html_e('显示头像', 'aksrt'); ?></label><br>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('apply_btn')); ?>" value="1" <?php checked(!isset($instance['apply_btn']) || !empty($instance['apply_btn'])); ?>> <?php esc_html_e('显示申请版主入口', 'aksrt'); ?></label>
</p>
<p class="description"><?php esc_html_e('仅在版块/帖子页显示。', 'aksrt'); ?></p>
<?php $this->render_common_form_section($instance); ?>
<?php
}
public function update($new_instance, $old_instance): array {
return $this->update_common_fields($new_instance, $old_instance, [
'title_text' => sanitize_text_field((string) ($new_instance['title_text'] ?? '')),
'style' => ($new_instance['style'] ?? '') === 'grid' ? 'grid' : 'list',
'show_avatar' => !empty($new_instance['show_avatar']) ? 1 : 0,
'apply_btn' => !empty($new_instance['apply_btn']) ? 1 : 0,
]);
}
}
@@ -1,109 +0,0 @@
<?php
/**
* Forum recent topics — thin wrapper over hot-topics payload with date order.
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
class WidgetForumRecentTopics extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_forum_recent_topics',
__('论坛 - 最新话题', 'aksrt'),
[
'description' => __('显示论坛最新话题列表(共享迷你行渲染)。', 'aksrt'),
'classname' => 'widget_aksrt_forum_recent_topics',
]
);
}
public function widget($args, $instance): void {
if (!post_type_exists('aksrt_forum_topic')) {
return;
}
$title = !empty($instance['title']) ? $instance['title'] : __('最新话题', 'aksrt');
$instance['orderby'] = 'date';
$instance['period'] = 'all';
$instance['style'] = sanitize_key((string) ($instance['style'] ?? 'mini'));
if (!in_array($instance['style'], ['mini', 'rank'], true)) {
$instance['style'] = 'mini';
}
$instance['paginate'] = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
$instance['plate'] = max(0, (int) ($instance['plate'] ?? 0));
$payload = WidgetForumHotTopics::query_payload($instance, 1);
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$rest = rest_url('aksrt/v1/widgets/forum-topics');
$affix = function_exists('aksrt_widget_affix_attrs') ? aksrt_widget_affix_attrs($instance) : ['class' => '', 'attr' => ''];
$wrap = 'ajaxpager' . $affix['class'];
echo '<div class="' . esc_attr($wrap) . '" data-aksrt-ajaxpager data-aksrt-ajax-url="' . esc_url($rest) . '"' . $affix['attr'] . '>';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader data-aksrt-ajax-loader-hide hidden></div>';
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
public function form($instance): void {
$title = !empty($instance['title']) ? $instance['title'] : __('最新话题', 'aksrt');
$count = !empty($instance['count']) ? (int) $instance['count'] : 5;
$plate = (int) ($instance['plate'] ?? 0);
$style = !empty($instance['style']) ? $instance['style'] : 'mini';
?>
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php esc_html_e('标题:', 'aksrt'); ?></label>
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>">
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('count')); ?>"><?php esc_html_e('数量:', 'aksrt'); ?></label>
<input class="tiny-text" id="<?php echo esc_attr($this->get_field_id('count')); ?>" name="<?php echo esc_attr($this->get_field_name('count')); ?>" type="number" value="<?php echo esc_attr((string) $count); ?>" min="1" max="20">
</p>
<p>
<label><?php esc_html_e('版块 ID(可选)', 'aksrt'); ?>
<input class="tiny-text" type="number" min="0" name="<?php echo esc_attr($this->get_field_name('plate')); ?>" value="<?php echo esc_attr((string) $plate); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('样式', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="mini" <?php selected($style, 'mini'); ?>><?php esc_html_e('迷你信息行', 'aksrt'); ?></option>
<option value="rank" <?php selected($style, 'rank'); ?>><?php esc_html_e('排行榜', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('分页', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('paginate')); ?>">
<option value="ajax" <?php selected(($instance['paginate'] ?? 'ajax'), 'ajax'); ?>><?php esc_html_e('AJAX', 'aksrt'); ?></option>
<option value="number" <?php selected(($instance['paginate'] ?? ''), 'number'); ?>><?php esc_html_e('数字分页', 'aksrt'); ?></option>
<option value="none" <?php selected(($instance['paginate'] ?? ''), 'none'); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
</select>
</label>
</p>
<?php $this->render_common_form_section($instance); ?>
<?php
}
public function update($new_instance, $old_instance): array {
$style = sanitize_key((string) ($new_instance['style'] ?? 'mini'));
if (!in_array($style, ['mini', 'rank'], true)) {
$style = 'mini';
}
return $this->update_common_fields($new_instance, $old_instance, [
'title' => sanitize_text_field($new_instance['title'] ?? ''),
'count' => max(1, min(20, (int) ($new_instance['count'] ?? 5))),
'plate' => max(0, (int) ($new_instance['plate'] ?? 0)),
'style' => $style,
'paginate' => in_array(sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')) : 'ajax',
]);
}
}
-188
View File
@@ -1,188 +0,0 @@
<?php
/**
* Multi-tab forum topic lists (zibll zib_bbs_widget_ui_tab_posts + shared mini rows).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
class WidgetForumTabTopics extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_forum_tab_topics',
esc_html__('论坛 - 多栏目帖子', 'aksrt'),
[
'description' => esc_html__('热门/最新/精华等栏目切换的帖子列表(共享迷你行 + 懒加载)。', 'aksrt'),
'classname' => 'widget_aksrt_forum_tab_topics',
]
);
}
public function widget($args, $instance): void {
if (!post_type_exists('aksrt_forum_topic')) {
return;
}
$tabs = $this->normalize_tabs($instance['tabs'] ?? []);
if (!$tabs) {
$tabs = [
['title' => __('最新', 'aksrt'), 'orderby' => 'date'],
['title' => __('热门', 'aksrt'), 'orderby' => 'views'],
['title' => __('精华', 'aksrt'), 'orderby' => 'essence'],
];
}
$count = max(1, min(20, (int) ($instance['count'] ?? 6)));
$style = sanitize_key((string) ($instance['style'] ?? 'mini'));
if (!in_array($style, ['mini', 'rank'], true)) {
$style = 'mini';
}
$plate = max(0, (int) ($instance['plate'] ?? 0));
$uid = 'aksrt-ftab-' . esc_attr($this->id);
// Prefer WidgetAjax forum-topics (HotTopics payload with style/plate).
$rest = rest_url('aksrt/v1/widgets/forum-topics');
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '<div class="aksrt-ftab widget-tab-posts" id="' . $uid . '" data-aksrt-tabs="1" data-aksrt-ftab="1" data-rest="' . esc_url($rest) . '" data-count="' . esc_attr((string) $count) . '" data-style="' . esc_attr($style) . '" data-plate="' . esc_attr((string) $plate) . '">';
echo '<ul class="aksrt-ftab__nav widget-tab-posts__nav" role="tablist">';
foreach ($tabs as $i => $tab) {
$cls = $i === 0 ? ' is-active' : '';
echo '<li role="presentation"><button type="button" role="tab" class="aksrt-ftab__btn widget-tab-posts__tab' . esc_attr($cls) . '" data-index="' . (int) $i . '" data-orderby="' . esc_attr($tab['orderby']) . '" aria-selected="' . ($i === 0 ? 'true' : 'false') . '">' . esc_html($tab['title']) . '</button></li>';
}
echo '</ul>';
foreach ($tabs as $i => $tab) {
$cls = $i === 0 ? ' is-active' : '';
$loaded = $i === 0 ? '1' : '0';
echo '<div class="aksrt-ftab__panel widget-tab-posts__panel' . esc_attr($cls) . '" data-index="' . (int) $i . '" data-loaded="' . esc_attr($loaded) . '"' . ($i === 0 ? '' : ' hidden') . ' role="tabpanel">';
if ($i === 0) {
$payload = WidgetForumHotTopics::query_payload([
'count' => $count,
'orderby' => $tab['orderby'],
'period' => 'all',
'style' => $style,
'plate' => $plate,
'paginate' => 'none',
], 1);
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader></div>';
}
echo '</div>';
}
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
/**
* @param mixed $raw
* @return list<array{title:string,orderby:string}>
*/
private function normalize_tabs($raw): array {
$out = [];
if (is_string($raw)) {
foreach (array_filter(explode("\n", $raw)) as $line) {
$parts = array_map('trim', explode('|', $line));
if ($parts[0] === '') {
continue;
}
$out[] = [
'title' => $parts[0],
'orderby' => sanitize_key($parts[1] ?? 'date'),
];
}
return $out;
}
if (is_array($raw)) {
foreach ($raw as $tab) {
if (!is_array($tab)) {
continue;
}
$title = trim((string) ($tab['title'] ?? ''));
if ($title === '') {
continue;
}
$out[] = [
'title' => $title,
'orderby' => sanitize_key((string) ($tab['orderby'] ?? 'date')),
];
}
}
return $out;
}
public function form($instance): void {
$defaults = [
'title' => __('论坛栏目', 'aksrt'),
'count' => 6,
'style' => 'mini',
'plate' => 0,
'tabs' => "最新|date\n热门|views\n精华|essence",
];
$instance = wp_parse_args($instance, $defaults);
$tabs_val = $instance['tabs'];
if (is_array($tabs_val)) {
$lines = [];
foreach ($tabs_val as $t) {
$lines[] = ($t['title'] ?? '') . '|' . ($t['orderby'] ?? 'date');
}
$tabs_val = implode("\n", $lines);
}
?>
<p>
<label><?php esc_html_e('标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) $instance['title']); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('每栏数量', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="20" name="<?php echo esc_attr($this->get_field_name('count')); ?>" value="<?php echo esc_attr((string) $instance['count']); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('样式', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="mini" <?php selected($instance['style'], 'mini'); ?>><?php esc_html_e('迷你信息行', 'aksrt'); ?></option>
<option value="rank" <?php selected($instance['style'], 'rank'); ?>><?php esc_html_e('排行榜', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('版块 ID(可选)', 'aksrt'); ?>
<input class="tiny-text" type="number" min="0" name="<?php echo esc_attr($this->get_field_name('plate')); ?>" value="<?php echo esc_attr((string) ((int) $instance['plate'])); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('栏目(每行: 标题|排序 date/views/essence/comments)', 'aksrt'); ?>
<textarea class="widefat" rows="4" name="<?php echo esc_attr($this->get_field_name('tabs')); ?>"><?php echo esc_textarea((string) $tabs_val); ?></textarea>
</label>
</p>
<?php
$this->render_common_form_section($instance);
}
public function update($new_instance, $old_instance): array {
$style = sanitize_key((string) ($new_instance['style'] ?? 'mini'));
if (!in_array($style, ['mini', 'rank'], true)) {
$style = 'mini';
}
$business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'count' => max(1, min(20, (int) ($new_instance['count'] ?? 6))),
'style' => $style,
'plate' => max(0, (int) ($new_instance['plate'] ?? 0)),
'tabs' => sanitize_textarea_field((string) ($new_instance['tabs'] ?? '')),
]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
-292
View File
@@ -1,292 +0,0 @@
<?php
/**
* Forum topic list widget (zibll zib_bbs_widget_ui_posts_lists lite + ajax pager).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
class WidgetForumTopicLists extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_forum_topic_lists',
esc_html__('论坛 - 帖子列表', 'aksrt'),
[
'description' => esc_html__('按排序/版块筛选展示论坛帖子(支持 AJAX 加载更多)。', 'aksrt'),
'classname' => 'widget_aksrt_forum_topic_lists',
]
);
}
/**
* @param array $instance
* @param int $paged
* @return array
*/
public static function build_query_args(array $instance, int $paged = 1): array {
$count = max(1, min(30, (int) ($instance['count'] ?? 8)));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'date'));
$plate_id = (int) ($instance['plate_id'] ?? 0);
$query = [
'post_type' => 'aksrt_forum_topic',
'post_status' => 'publish',
'posts_per_page' => $count,
'paged' => max(1, $paged),
'order' => 'DESC',
];
switch ($orderby) {
case 'views':
$query['meta_key'] = '_aksrt_views';
$query['orderby'] = 'meta_value_num';
break;
case 'comments':
$query['orderby'] = 'comment_count';
break;
case 'modified':
$query['orderby'] = 'modified';
break;
case 'essence':
$query['meta_query'] = [[
'key' => '_aksrt_essence',
'value' => '1',
]];
$query['orderby'] = 'date';
break;
default:
$query['orderby'] = 'date';
}
if ($plate_id > 0) {
$query['meta_query'] = array_values(array_filter(array_merge(
isset($query['meta_query']) ? $query['meta_query'] : [],
[[
'key' => '_aksrt_plate_id',
'value' => $plate_id,
]]
)));
}
return $query;
}
public static function render_items_html(\WP_Query $q, string $style = 'mini'): string {
if (!$q->have_posts()) {
return '';
}
ob_start();
echo $style === 'detail'
? '<div class="forum-posts-lists aksrt-ftopic-list is-detail">'
: '<div class="forum-posts-lists aksrt-ftopic-list is-mini">';
while ($q->have_posts()) {
$q->the_post();
if ($style === 'detail' && function_exists('aksrt_render_forum_topic_detail')) {
echo aksrt_render_forum_topic_detail(null, 'ajax-item', true); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} elseif (function_exists('aksrt_render_forum_topic_mini')) {
echo aksrt_render_forum_topic_mini(null, 'ajax-item', true); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} else {
$views = (int) get_post_meta(get_the_ID(), '_aksrt_views', true);
echo '<div class="aksrt-ftopic-list__item ajax-item">';
echo '<a class="aksrt-ftopic-list__link" href="' . esc_url(get_permalink()) . '">' . esc_html(get_the_title()) . '</a>';
echo '<span class="aksrt-ftopic-list__views">' . esc_html((string) $views) . '</span>';
echo '</div>';
}
}
echo '</div>';
wp_reset_postdata();
return (string) ob_get_clean();
}
/**
* @return array{html:string,paginate_html:string,has_more:bool,page:int,max_pages:int,total:int}
*/
public static function query_payload(array $instance, int $page = 1): array {
$page = max(1, $page);
$style = sanitize_key((string) ($instance['style'] ?? 'mini'));
$paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
if (!in_array($paginate, ['none', 'ajax', 'number'], true)) {
$paginate = 'ajax';
}
$q = new \WP_Query(self::build_query_args($instance, $page));
$html = self::render_items_html($q, $style);
if ($html === '' && $page === 1) {
$html = function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无内容', 'aksrt'))
: '<p class="aksrt-ftopic-list__empty ajax-item">' . esc_html__('暂无内容', 'aksrt') . '</p>';
}
$max_pages = max(1, (int) $q->max_num_pages);
$total = (int) $q->found_posts;
$has_more = $page < $max_pages;
$base_qs = array_filter([
'count' => max(1, min(30, (int) ($instance['count'] ?? 8))),
'orderby' => sanitize_key((string) ($instance['orderby'] ?? 'date')),
'style' => $style,
'plate_id' => (int) ($instance['plate_id'] ?? 0),
'paginate' => $paginate,
], static function ($v) {
return $v !== '' && $v !== null;
});
$rest_base = add_query_arg($base_qs, rest_url('aksrt/v1/forum/frontend/widgets/topics'));
$paginate_html = '';
if ($paginate === 'ajax' && $has_more) {
$next = add_query_arg(['page' => $page + 1], $rest_base);
$paginate_html = function_exists('aksrt_get_ajax_next_paginate')
? aksrt_get_ajax_next_paginate($total, $page, max(1, (int) ($instance['count'] ?? 8)), $next)
: '<div class="text-center theme-pagination ajax-pag"><div class="next-page ajax-next"><a href="' . esc_url($next) . '">' . esc_html__('加载更多', 'aksrt') . '</a></div></div>';
} elseif ($paginate === 'number' && $max_pages > 1) {
$paginate_html = function_exists('aksrt_get_ajax_number_paginate')
? aksrt_get_ajax_number_paginate($total, $page, max(1, (int) ($instance['count'] ?? 8)), $rest_base)
: '<div class="ajax-pag hide"><div class="next-page ajax-next"><a href="#"></a></div></div>';
} else {
$paginate_html = '<div class="ajax-pag hide"><div class="next-page ajax-next"><a href="#"></a></div></div>';
}
return [
'html' => $html,
'paginate_html' => $paginate_html,
'has_more' => $has_more,
'page' => $page,
'max_pages' => $max_pages,
'total' => $total,
];
}
public function widget($args, $instance): void {
if (!post_type_exists('aksrt_forum_topic')) {
return;
}
$count = max(1, min(30, (int) ($instance['count'] ?? 8)));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'date'));
$style = sanitize_key((string) ($instance['style'] ?? 'mini'));
$plate_id = (int) ($instance['plate_id'] ?? 0);
$current_plate = !empty($instance['current_plate']);
$paginate = sanitize_key((string) ($instance['paginate'] ?? 'ajax'));
if ($current_plate) {
$resolved = $this->resolve_current_plate_id();
if ($resolved <= 0) {
return;
}
$plate_id = $resolved;
}
$instance['count'] = $count;
$instance['orderby'] = $orderby;
$instance['style'] = $style;
$instance['plate_id'] = $plate_id;
$instance['paginate'] = $paginate;
$payload = self::query_payload($instance, 1);
$rest = rest_url('aksrt/v1/forum/frontend/widgets/topics');
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '<div class="aksrt-forum-topic-widget" data-aksrt-forum-topic-widget>';
echo '<div class="ajaxpager" data-aksrt-ajaxpager data-aksrt-ajax-url="' . esc_url($rest) . '">';
echo $payload['html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $payload['paginate_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="post_ajax_loader" data-aksrt-ajax-loader data-aksrt-ajax-loader-hide hidden></div>';
echo '</div></div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
private function resolve_current_plate_id(): int {
if (is_singular('aksrt_forum_plate')) {
return (int) get_queried_object_id();
}
if (is_singular('aksrt_forum_topic')) {
$topic_id = (int) get_queried_object_id();
$plate_id = (int) get_post_meta($topic_id, '_aksrt_plate_id', true);
return max(0, $plate_id);
}
return 0;
}
public function form($instance): void {
$defaults = [
'title' => __('帖子列表', 'aksrt'),
'count' => 8,
'orderby' => 'date',
'style' => 'mini',
'plate_id' => 0,
'current_plate' => 0,
'paginate' => 'ajax',
];
$instance = wp_parse_args($instance, $defaults);
?>
<p>
<label><?php esc_html_e('标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) $instance['title']); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('数量', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="30" name="<?php echo esc_attr($this->get_field_name('count')); ?>" value="<?php echo esc_attr((string) $instance['count']); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('排序', 'aksrt'); ?>
<select class="widefat" name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
<option value="date" <?php selected($instance['orderby'], 'date'); ?>><?php esc_html_e('最新', 'aksrt'); ?></option>
<option value="views" <?php selected($instance['orderby'], 'views'); ?>><?php esc_html_e('热门', 'aksrt'); ?></option>
<option value="comments" <?php selected($instance['orderby'], 'comments'); ?>><?php esc_html_e('评论', 'aksrt'); ?></option>
<option value="essence" <?php selected($instance['orderby'], 'essence'); ?>><?php esc_html_e('精华', 'aksrt'); ?></option>
<option value="modified" <?php selected($instance['orderby'], 'modified'); ?>><?php esc_html_e('更新', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('样式', 'aksrt'); ?>
<select class="widefat" name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="mini" <?php selected($instance['style'], 'mini'); ?>><?php esc_html_e('迷你', 'aksrt'); ?></option>
<option value="detail" <?php selected($instance['style'], 'detail'); ?>><?php esc_html_e('详情', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('版块 ID', 'aksrt'); ?>
<input class="tiny-text" type="number" min="0" name="<?php echo esc_attr($this->get_field_name('plate_id')); ?>" value="<?php echo esc_attr((string) $instance['plate_id']); ?>">
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('current_plate')); ?>" value="1" <?php checked(!empty($instance['current_plate'])); ?>> <?php esc_html_e('跟随当前版块', 'aksrt'); ?></label>
</p>
<p>
<label><?php esc_html_e('分页', 'aksrt'); ?>
<select class="widefat" name="<?php echo esc_attr($this->get_field_name('paginate')); ?>">
<option value="ajax" <?php selected($instance['paginate'], 'ajax'); ?>><?php esc_html_e('AJAX', 'aksrt'); ?></option>
<option value="number" <?php selected(($instance['paginate'] ?? ''), 'number'); ?>><?php esc_html_e('数字分页', 'aksrt'); ?></option>
<option value="none" <?php selected($instance['paginate'], 'none'); ?>><?php esc_html_e('无', 'aksrt'); ?></option>
</select>
</label>
</p>
<?php
$this->render_common_form_section($instance);
}
public function update($new_instance, $old_instance): array {
$business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'count' => max(1, min(30, (int) ($new_instance['count'] ?? 8))),
'orderby' => sanitize_key((string) ($new_instance['orderby'] ?? 'date')),
'style' => sanitize_key((string) ($new_instance['style'] ?? 'mini')),
'plate_id' => max(0, (int) ($new_instance['plate_id'] ?? 0)),
'current_plate' => !empty($new_instance['current_plate']) ? 1 : 0,
'paginate' => in_array(sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')), ['none', 'ajax', 'number'], true) ? sanitize_key((string) ($new_instance['paginate'] ?? 'ajax')) : 'ajax',
]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
-177
View File
@@ -1,177 +0,0 @@
<?php
/**
* Forum topic tags list (zibll topic_lists + remote HTML).
*
* @package AKSRT
*/
namespace AKSRT\Widgets;
use AKSRT\Widgets\Traits\CommonWidgetFields;
defined('ABSPATH') || exit;
class WidgetForumTopicTerms extends \WP_Widget {
use CommonWidgetFields;
public function __construct() {
parent::__construct(
'aksrt_forum_topic_terms',
esc_html__('论坛 - 话题标签列表', 'aksrt'),
[
'description' => esc_html__('展示论坛话题标签(aksrt_forum_tag)。', 'aksrt'),
'classname' => 'widget_aksrt_forum_topic_terms',
]
);
}
/** @return array<int,int> */
public static function parse_ids_static(string $raw): array {
if ($raw === '') {
return [];
}
$ids = [];
foreach (preg_split('/[\s,]+/', $raw) ?: [] as $p) {
$id = (int) $p;
if ($id > 0) {
$ids[] = $id;
}
}
return array_values(array_unique($ids));
}
public static function render_html(array $instance): string {
$tax = taxonomy_exists('aksrt_forum_tag') ? 'aksrt_forum_tag' : (taxonomy_exists('aksrt_forum_topic_cat') ? 'aksrt_forum_topic_cat' : '');
if ($tax === '') {
return '';
}
$number = max(1, min(40, (int) ($instance['number'] ?? 12)));
$orderby = sanitize_key((string) ($instance['orderby'] ?? 'count'));
$hide_empty = !isset($instance['hide_empty']) || !empty($instance['hide_empty']);
$include = self::parse_ids_static((string) ($instance['include'] ?? ''));
$style = sanitize_key((string) ($instance['style'] ?? 'mini'));
$term_args = [
'taxonomy' => $tax,
'number' => $number,
'hide_empty' => (bool) $hide_empty,
];
if ($include) {
$term_args['include'] = $include;
$term_args['orderby'] = 'include';
} elseif ($orderby === 'name') {
$term_args['orderby'] = 'name';
$term_args['order'] = 'ASC';
} elseif ($orderby === 'views') {
$term_args['meta_key'] = '_aksrt_views';
$term_args['orderby'] = 'meta_value_num';
$term_args['order'] = 'DESC';
} else {
$term_args['orderby'] = 'count';
$term_args['order'] = 'DESC';
}
$terms = get_terms($term_args);
if (is_wp_error($terms) || !$terms) {
return function_exists('aksrt_get_ajax_null')
? aksrt_get_ajax_null(__('暂无话题', 'aksrt'))
: '<p class="widget-empty ajax-item">' . esc_html__('暂无话题', 'aksrt') . '</p>';
}
ob_start();
$list_cls = $style === 'card' ? 'aksrt-ftag-list is-card ajax-item' : 'aksrt-ftag-list is-mini ajax-item';
echo '<div class="' . esc_attr($list_cls) . '">';
foreach ($terms as $term) {
$link = get_term_link($term);
if (is_wp_error($link)) {
continue;
}
$icon = (string) get_term_meta($term->term_id, '_aksrt_icon', true);
echo '<a class="aksrt-ftag-item but p2-10' . ($style === 'card' ? ' is-card' : '') . '" href="' . esc_url($link) . '">';
if ($icon !== '') {
echo '<i class="' . esc_attr($icon) . '" aria-hidden="true"></i> ';
}
echo '<span class="aksrt-ftag-item__name">' . esc_html($term->name) . '</span>';
echo '<span class="aksrt-ftag-item__count">' . esc_html((string) (int) $term->count) . '</span>';
echo '</a>';
}
echo '</div>';
return (string) ob_get_clean();
}
public function widget($args, $instance): void {
$html = self::render_html($instance);
if ($html === '') {
return;
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$title = apply_filters('widget_title', (string) ($instance['title'] ?? ''), $instance, $this->id_base);
if ($title !== '') {
echo $args['before_title'] . esc_html($title) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
echo '<div class="zib-widget padding-6 aksrt-ftag-wrap">';
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
private function parse_ids(string $raw): array {
return self::parse_ids_static($raw);
}
public function form($instance): void {
?>
<p>
<label><?php esc_html_e('标题', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('title')); ?>" value="<?php echo esc_attr((string) ($instance['title'] ?? __('话题', 'aksrt'))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('数量', 'aksrt'); ?>
<input class="tiny-text" type="number" min="1" max="40" name="<?php echo esc_attr($this->get_field_name('number')); ?>" value="<?php echo esc_attr((string) ((int) ($instance['number'] ?? 12))); ?>">
</label>
</p>
<p>
<label><?php esc_html_e('排序', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
<option value="count" <?php selected(($instance['orderby'] ?? 'count'), 'count'); ?>><?php esc_html_e('帖子数', 'aksrt'); ?></option>
<option value="name" <?php selected(($instance['orderby'] ?? ''), 'name'); ?>><?php esc_html_e('名称', 'aksrt'); ?></option>
<option value="views" <?php selected(($instance['orderby'] ?? ''), 'views'); ?>><?php esc_html_e('浏览', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('样式', 'aksrt'); ?>
<select name="<?php echo esc_attr($this->get_field_name('style')); ?>">
<option value="mini" <?php selected(($instance['style'] ?? 'mini'), 'mini'); ?>><?php esc_html_e('列表', 'aksrt'); ?></option>
<option value="card" <?php selected(($instance['style'] ?? ''), 'card'); ?>><?php esc_html_e('卡片', 'aksrt'); ?></option>
</select>
</label>
</p>
<p>
<label><?php esc_html_e('包含 term ID', 'aksrt'); ?>
<input class="widefat" type="text" name="<?php echo esc_attr($this->get_field_name('include')); ?>" value="<?php echo esc_attr((string) ($instance['include'] ?? '')); ?>">
</label>
</p>
<p>
<label><input type="checkbox" name="<?php echo esc_attr($this->get_field_name('hide_empty')); ?>" value="1" <?php checked(!isset($instance['hide_empty']) || !empty($instance['hide_empty'])); ?>> <?php esc_html_e('隐藏空标签', 'aksrt'); ?></label>
</p>
<?php
$this->render_common_form_section($instance);
}
public function update($new_instance, $old_instance): array {
$orderby = sanitize_key((string) ($new_instance['orderby'] ?? 'count'));
if (!in_array($orderby, ['count', 'name', 'views'], true)) {
$orderby = 'count';
}
$business = [
'title' => sanitize_text_field((string) ($new_instance['title'] ?? '')),
'number' => max(1, min(40, (int) ($new_instance['number'] ?? 12))),
'orderby' => $orderby,
'style' => ($new_instance['style'] ?? '') === 'card' ? 'card' : 'mini',
'include' => sanitize_text_field((string) ($new_instance['include'] ?? '')),
'hide_empty' => !empty($new_instance['hide_empty']) ? 1 : 0,
]; return $this->update_common_fields($new_instance, $old_instance, $business);
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ class WidgetPostPayBox extends \WP_Widget {
* @param array<string,mixed> $instance
*/
public static function render_html(array $instance = []): string {
if (!is_singular(['post', 'page', 'aksrt_forum_topic', 'aksrt_product'])) {
if (!is_singular(['post', 'page'])) {
return '';
}
$post = get_queried_object();
+2 -2
View File
@@ -29,11 +29,11 @@ class WidgetPostsNavs extends \WP_Widget {
}
public function widget($args, $instance): void {
if (!is_singular(['post', 'page', 'aksrt_forum_topic', 'aksrt_product'])) {
if (!is_singular(['post', 'page'])) {
if (current_user_can('manage_options')) {
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo function_exists('aksrt_get_widget_null')
? aksrt_get_widget_null(__('目录树仅在文章/帖子/商品详情页显示', 'aksrt')) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
? aksrt_get_widget_null(__('目录树仅在文章页面显示', 'aksrt')) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
: '<p class="widget-empty">' . esc_html__('目录树仅在详情页显示', 'aksrt') . '</p>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}