Files
wordpress-theme-aksrt/src/AKSRT/Forum/ScoreService.php
T
kate522 aaf458af1c 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.
2026-07-17 13:29:22 +08:00

448 lines
16 KiB
PHP

<?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
));
}
}