refactor(autoload): migrate theme classes to Composer PSR-4

Move 288 types from global AKSRT_* + inc/classes manual requires into
namespace AKSRT\{Domain} under src/AKSRT/, wire composer.json psr-4,
and slim bootstrap to helpers/modules/hooks only. Split multi-class
widget/gateway files, update admin contract audit paths, and document
the layout in ADR/README.
This commit is contained in:
2026-07-17 13:29:22 +08:00
parent 8f349b40fb
commit aaf458af1c
404 changed files with 13602 additions and 7705 deletions
+429
View File
@@ -0,0 +1,429 @@
<?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_board = get_term($target_board_id, 'aksrt_forum_board');
if (!$target_board || is_wp_error($target_board)) {
return new \WP_Error('invalid_board', __('目标版块不存在', 'aksrt'));
}
$current_board_ids = wp_get_post_terms($topic_id, 'aksrt_forum_board', ['fields' => 'ids']);
wp_set_object_terms($topic_id, [$target_board_id], 'aksrt_forum_board');
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),
];
}
}