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.
448 lines
16 KiB
PHP
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
|
|
));
|
|
}
|
|
}
|