feat(forum): add complete forum workflows

This commit is contained in:
kate522 committed 2026-11-11 00:09:43 +08:00
1 parent a254cbd3e9
commit ee65fc7fd2
39 files changed
+3425 -4

No files matched your search

+251
View File
@@ -0,0 +1,251 @@
<?php
namespace Jio\Forum;
defined('ABSPATH') || exit;
trait RestPosts {
public static function create_post(\WP_REST_Request $request) {
return self::save_post($request, 0);
}
public static function update_post(\WP_REST_Request $request) {
return self::save_post($request, absint($request['id']));
}
private static function save_post(\WP_REST_Request $request, int $id) {
$user_id = self::actor();
$current = $id > 0 ? self::post($id) : null;
if ($id > 0 && (!$current || !Access::can_manage_post($user_id, $id))) {
return self::fail('无权编辑此帖子', 403);
}
$input = self::input($request);
$plate_id = absint($input['plate_id'] ?? ($current ? get_post_meta($id, '_jio_forum_plate_id', true) : 0));
if (!self::plate($plate_id) || !Access::can_publish($user_id, $plate_id)) {
return self::fail('没有在此版块发布的权限', 403);
}
if ($id === 0 && !Access::is_moderator($user_id, $plate_id)) {
$limit = max(0, (int) jio_get_setting('jio_forum_daily_post_limit', 20));
if ($limit > 0) {
$recent = get_posts(['post_type' => Core::POST, 'post_status' => ['publish', 'pending'], 'author' => $user_id, 'date_query' => [['after' => 'today']], 'posts_per_page' => $limit, 'fields' => 'ids']);
if (count($recent) >= $limit) {
return self::fail('已达到今日发帖上限', 429);
}
}
}
$title = sanitize_text_field((string) ($input['title'] ?? ($current->post_title ?? '')));
$content = wp_kses_post((string) ($input['content'] ?? ($current->post_content ?? '')));
if (mb_strlen($title) < 2 || mb_strlen($title) > 120 || trim(wp_strip_all_tags($content)) === '') {
return self::fail('请填写标题和正文');
}
$type = sanitize_key((string) ($input['type'] ?? ($current ? get_post_meta($id, '_jio_forum_type', true) : 'normal')));
if (!in_array($type, ['normal', 'question', 'vote'], true)) {
return self::fail('帖子类型无效');
}
if (($type === 'vote' && !jio_get_setting('jio_forum_vote_enabled', true)) || ($type === 'question' && !jio_get_setting('jio_forum_question_enabled', true))) {
return self::fail('当前未启用此帖子类型');
}
$votes = [];
$vote_type = 'single';
$vote_title = '';
$vote_days = 0;
if ($type === 'vote') {
$vote_type = sanitize_key((string) ($input['vote_type'] ?? ($current ? get_post_meta($id, '_jio_forum_vote_type', true) : 'single')));
if ($vote_type === '') $vote_type = 'single';
if (!in_array($vote_type, ['single', 'multiple', 'pk'], true)) return self::fail('投票方式无效');
$vote_title = sanitize_text_field((string) ($input['vote_title'] ?? ($current ? get_post_meta($id, '_jio_forum_vote_title', true) : '')));
if (mb_strlen($vote_title) > 60) return self::fail('投票标题过长');
$vote_days = absint($input['vote_days'] ?? 0);
if (!in_array($vote_days, [0, 1, 7, 30], true)) return self::fail('投票有效期无效');
$raw = (string) ($input['vote_options'] ?? '');
if ($raw === '' && $current) {
$votes = (array) get_post_meta($id, '_jio_forum_vote_options', true);
} else {
$votes = array_values(array_unique(array_filter(array_map('sanitize_text_field', preg_split('/\R/', $raw)))));
}
$max_votes = max(2, min(20, (int) jio_get_setting('jio_forum_vote_max', 10)));
if (count($votes) < 2 || count($votes) > $max_votes) {
return self::fail('投票选项数量无效');
}
if ($vote_type === 'pk' && count($votes) !== 2) return self::fail('双选 PK 只能有两个选项');
if ($current && get_post_meta($id, '_jio_forum_vote_total', true) && $votes !== (array) get_post_meta($id, '_jio_forum_vote_options', true)) {
return self::fail('已有投票后不能更改选项');
}
if ($current && get_post_meta($id, '_jio_forum_vote_total', true) && $vote_type !== (string) get_post_meta($id, '_jio_forum_vote_type', true)) {
return self::fail('已有投票后不能更改投票方式');
}
}
$view = sanitize_key((string) ($input['view_limit'] ?? ($current ? get_post_meta($id, '_jio_forum_view_limit', true) : 'all')));
if (!in_array($view, ['all', 'login', 'follow', 'points', 'pay'], true)) {
return self::fail('访问权限无效');
}
if (($view === 'points' && !jio_get_setting('jio_forum_points_enabled', true)) || ($view === 'pay' && !jio_get_setting('jio_forum_paid_content', true))) {
return self::fail('当前未启用此解锁方式');
}
$price = (string) ($input['price'] ?? ($current ? get_post_meta($id, '_jio_forum_price', true) : '0'));
if (($view === 'points' && (!ctype_digit($price) || (int) $price <= 0)) || ($view === 'pay' && !preg_match('/^(0|[1-9]\d*)(?:\.\d{1,2})?$/', $price))) {
return self::fail('解锁价格无效');
}
if (in_array($view, ['points', 'pay'], true)) {
$valid_price = self::validate_price($price, $view);
if (is_wp_error($valid_price)) return $valid_price;
foreach (['vip1', 'vip2'] as $vip) {
if (!isset($input['price_' . $vip]) || $input['price_' . $vip] === '') continue;
$valid_price = self::validate_price((string) $input['price_' . $vip], $view);
if (is_wp_error($valid_price)) return $valid_price;
}
}
if (isset($input['topics']) && !jio_get_setting('jio_forum_user_topics', true) && !Access::is_admin($user_id)) {
foreach (array_filter(array_map('sanitize_text_field', explode(',', (string) $input['topics']))) as $name) {
if (!term_exists($name, Core::TOPIC)) return self::fail('无权创建新话题', 403);
}
}
if ($current && (int) get_post_meta($id, '_jio_forum_vote_total', true) > 0 && $type !== 'vote') {
return self::fail('已有投票后不能更改帖子类型');
}
if ($current && get_post_meta($id, '_jio_forum_adopted_comment', true) && $type !== 'question') {
return self::fail('已有采纳回复后不能更改帖子类型');
}
$status = Access::is_moderator($user_id, $plate_id) || !jio_get_setting('jio_forum_post_review', true) ? 'publish' : 'pending';
$post_data = [
'ID' => $id, 'post_type' => Core::POST, 'post_title' => $title, 'post_content' => $content,
'post_status' => $status, 'post_author' => $current ? (int) $current->post_author : $user_id,
'comment_status' => $current ? $current->comment_status : 'open', 'ping_status' => 'closed',
];
$result = $id ? wp_update_post($post_data, true) : wp_insert_post($post_data, true);
if (is_wp_error($result) || !$result) {
return self::fail('保存失败', 500);
}
$id = (int) $result;
if (class_exists('\\Jio\\Attachment\\PendingUploads')) {
\Jio\Attachment\PendingUploads::claim_text($content, $user_id);
}
update_post_meta($id, '_jio_forum_plate_id', $plate_id);
update_post_meta($id, '_jio_forum_type', $type);
update_post_meta($id, '_jio_forum_view_limit', $view);
update_post_meta($id, '_jio_forum_price', $price);
foreach (['vip1', 'vip2'] as $vip) {
if (!isset($input['price_' . $vip])) continue;
$vip_price = (string) $input['price_' . $vip];
if ($vip_price === '' || preg_match($view === 'points' ? '/^\d+$/' : '/^(0|[1-9]\d*)(?:\.\d{1,2})?$/', $vip_price)) {
update_post_meta($id, '_jio_forum_price_' . $vip, $vip_price);
}
}
if ($type === 'vote') {
update_post_meta($id, '_jio_forum_vote_options', $votes);
update_post_meta($id, '_jio_forum_vote_type', $vote_type);
update_post_meta($id, '_jio_forum_vote_title', $vote_title);
if (!$current || !get_post_meta($id, '_jio_forum_vote_started', true)) {
$started = current_time('timestamp');
update_post_meta($id, '_jio_forum_vote_started', $started);
update_post_meta($id, '_jio_forum_vote_deadline', $vote_days ? $started + $vote_days * DAY_IN_SECONDS : 0);
}
}
foreach ([Core::TOPIC => 'topics', Core::TAG => 'tags'] as $taxonomy => $key) {
if (isset($input[$key])) {
$names = array_values(array_filter(array_map('sanitize_text_field', explode(',', (string) $input[$key]))));
wp_set_object_terms($id, $names, $taxonomy);
}
}
do_action($current ? 'jio_forum_post_updated' : 'jio_forum_post_created', $id, $user_id, $status);
return self::success(['id' => $id, 'url' => $status === 'pending' ? Core::edit_url($id) : get_permalink($id), 'status' => $status], $status === 'pending' ? '已提交审核' : '帖子已发布');
}
public static function create_plate(\WP_REST_Request $request) {
return self::save_plate($request, 0);
}
public static function update_plate(\WP_REST_Request $request) {
return self::save_plate($request, absint($request['id']));
}
private static function save_plate(\WP_REST_Request $request, int $id) {
$user_id = self::actor();
$current = $id > 0 ? self::plate($id) : null;
if ($id > 0 && (!$current || !Access::is_moderator($user_id, $id))) {
return self::fail('无权编辑版块', 403);
}
if ($id === 0 && !Access::is_admin($user_id) && !jio_get_setting('jio_forum_user_plates', true)) {
return self::fail('当前不允许创建版块', 403);
}
$input = self::input($request);
$title = sanitize_text_field((string) ($input['title'] ?? ($current->post_title ?? '')));
if (mb_strlen($title) < 2 || mb_strlen($title) > 80) {
return self::fail('版块名称需为 2 至 80 个字符');
}
$follow_mode = sanitize_key((string) ($input['follow_mode'] ?? ($current ? get_post_meta($id, '_jio_forum_follow_mode', true) : 'all')));
$view_limit = sanitize_key((string) ($input['view_limit'] ?? ($current ? get_post_meta($id, '_jio_forum_view_limit', true) : 'all')));
if (!in_array($follow_mode, ['all', 'points', 'pay'], true) || !in_array($view_limit, ['all', 'login', 'follow', 'points', 'pay'], true)) {
return self::fail('版块权限无效');
}
if (in_array($follow_mode, ['points', 'pay'], true) && !jio_get_setting('jio_forum_paid_follow', true)) {
return self::fail('付费关注未启用');
}
if (($follow_mode === 'points' || $view_limit === 'points') && !jio_get_setting('jio_forum_points_enabled', true)) {
return self::fail('积分解锁未启用');
}
$follow_price = (string) ($input['follow_price'] ?? ($current ? get_post_meta($id, '_jio_forum_follow_price', true) : '0'));
if ($follow_price === '') $follow_price = '0';
if (!preg_match($follow_mode === 'points' ? '/^\d+$/' : '/^(0|[1-9]\d*)(?:\.\d{1,2})?$/', $follow_price)) {
return self::fail('关注价格无效');
}
if (in_array($follow_mode, ['points', 'pay'], true)) {
$valid_price = self::validate_price($follow_price, $follow_mode);
if (is_wp_error($valid_price)) return $valid_price;
foreach (['vip1', 'vip2'] as $vip) {
if (!isset($input['follow_price_' . $vip]) || $input['follow_price_' . $vip] === '') continue;
$valid_price = self::validate_price((string) $input['follow_price_' . $vip], $follow_mode);
if (is_wp_error($valid_price)) return $valid_price;
}
}
$status = Access::is_admin($user_id) || !jio_get_setting('jio_forum_plate_review', true) ? 'publish' : 'pending';
$result = $id ? wp_update_post([
'ID' => $id, 'post_title' => $title,
'post_content' => wp_kses_post((string) ($input['content'] ?? $current->post_content)),
], true) : wp_insert_post([
'post_type' => Core::PLATE, 'post_title' => $title, 'post_status' => $status,
'post_author' => $user_id, 'post_content' => wp_kses_post((string) ($input['content'] ?? '')),
], true);
if (is_wp_error($result) || !$result) {
return self::fail('版块保存失败', 500);
}
$id = (int) $result;
if (isset($input['category_id'])) {
$term_id = absint($input['category_id']);
if ($term_id && term_exists($term_id, Core::CATEGORY)) {
wp_set_object_terms($id, [$term_id], Core::CATEGORY);
}
}
if (Access::is_moderator($user_id, $id) && isset($input['publish_limit'])) {
$limit = sanitize_key((string) $input['publish_limit']);
if (in_array($limit, ['all', 'follow'], true)) {
update_post_meta($id, '_jio_forum_publish_limit', $limit);
}
}
update_post_meta($id, '_jio_forum_follow_mode', $follow_mode);
update_post_meta($id, '_jio_forum_follow_price', $follow_price);
update_post_meta($id, '_jio_forum_view_limit', $view_limit);
foreach (['vip1', 'vip2'] as $vip) {
if (!isset($input['follow_price_' . $vip])) continue;
$vip_price = (string) $input['follow_price_' . $vip];
if ($vip_price === '' || preg_match($follow_mode === 'points' ? '/^\d+$/' : '/^(0|[1-9]\d*)(?:\.\d{1,2})?$/', $vip_price)) {
update_post_meta($id, '_jio_forum_follow_price_' . $vip, $vip_price);
}
}
return self::success(['id' => $id, 'url' => get_post_status($id) === 'publish' ? get_permalink($id) : Core::home_url(), 'status' => get_post_status($id)], get_post_status($id) === 'pending' ? '版块已提交审核' : '版块已保存');
}
private static function validate_price(string $value, string $mode) {
if ($mode === 'points') {
$max = (string) PHP_INT_MAX;
return preg_match('/^(0|[1-9]\d*)$/', $value) && (strlen($value) < strlen($max) || strlen($value) === strlen($max) && strcmp($value, $max) <= 0)
? true : self::fail('积分价格超出范围');
}
try {
\Jio\Ledger\AssetAmount::balance($value);
return true;
} catch (\InvalidArgumentException $exception) {
return self::fail('付费价格超出范围');
}
}
}