feat: support full community post editing

This commit is contained in:
hajimi
2026-08-02 20:38:47 +08:00
parent f78580b2b9
commit 2358603eda
9 changed files with 638 additions and 72 deletions
+3 -3
View File
@@ -10,9 +10,9 @@ export function communityPostDetail(params: any) {
return request.get({ url: '/community.communityPost/detail', params })
}
// 编辑帖子内容
export function communityPostEditContent(params: any) {
return request.post({ url: '/community.communityPost/editContent', params })
// 编辑帖子
export function communityPostEdit(params: any) {
return request.post({ url: '/community.communityPost/edit', params })
}
// 帖子审核状态
+319 -26
View File
@@ -68,6 +68,7 @@
<el-table-column label="类型" min-width="80">
<template #default="{ row }">
<el-tag v-if="row.post_type === 1" type="warning">赛事分析</el-tag>
<el-tag v-else-if="row.post_type === 2" type="success">战绩分享</el-tag>
<el-tag v-else>普通</el-tag>
</template>
</el-table-column>
@@ -135,8 +136,8 @@
@click="handleDetail(row.id)">
详情
</el-button>
<el-button v-perms="['community.communityPost/editContent']" type="primary" link
@click="handleEdit(row)">
<el-button v-perms="['community.communityPost/edit']" type="primary" link
@click="handleEdit(row.id)">
编辑
</el-button>
<el-button v-if="row.status === 0" v-perms="['community.communityPost/updateStatus']"
@@ -203,16 +204,161 @@
<el-button @click="showDetail = false">关闭</el-button>
</template>
</el-dialog>
<el-dialog v-model="showEdit" title="编辑帖子内容" width="700px" destroy-on-close>
<el-form label-position="top">
<el-form-item label="帖子内容">
<el-input v-model="editData.content" type="textarea" :rows="14" :maxlength="16000"
show-word-limit resize="vertical" placeholder="请输入帖子内容" />
</el-form-item>
</el-form>
<el-dialog v-model="showEdit" title="编辑帖子" width="920px" destroy-on-close>
<el-scrollbar max-height="68vh">
<el-form ref="editFormRef" v-loading="editLoading" :model="editData" :rules="editRules"
label-width="104px" class="pr-5">
<el-divider content-position="left">基础信息</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="帖子ID">
<el-input :model-value="String(editData.id || '')" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="用户ID" prop="user_id">
<el-input-number v-model="editData.user_id" :min="1" :precision="0"
controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="发布时间" prop="create_time">
<el-date-picker v-model="editData.create_time" type="datetime"
value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择发布时间" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="来源标识" prop="origin_id">
<el-input v-model="editData.origin_id" maxlength="100" clearable
placeholder="例如 truthsocial:123456" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="更新时间">
<el-input :model-value="editData.update_time || '-'" disabled />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="帖子类型" prop="post_type">
<el-select v-model="editData.post_type" class="w-full">
<el-option label="普通帖子" :value="0" />
<el-option label="赛事分析" :value="1" />
<el-option label="战绩分享" :value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="帖子分类" prop="category_id">
<el-select v-model="editData.category_id" filterable class="w-full">
<el-option label="未分类" :value="0" />
<el-option v-for="item in categoryList" :key="item.id"
:label="item.status === 1 ? item.name : `${item.name}(禁用)`"
:value="item.id" :disabled="item.status !== 1 && item.id !== editData.category_id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="关联赛事ID" prop="match_id">
<el-input-number v-model="editData.match_id" :min="0" :precision="0"
controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="帖子标签" prop="tag_ids">
<el-select v-model="editData.tag_ids" multiple filterable :multiple-limit="3"
collapse-tags collapse-tags-tooltip class="w-full" placeholder="最多选择3个标签">
<el-option v-for="item in tagList" :key="item.id"
:label="item.status === 1 ? item.name : `${item.name}(禁用)`"
:value="item.id" :disabled="item.status !== 1 && !editData.tag_ids.includes(item.id)" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">正文与素材</el-divider>
<el-form-item label="帖子内容" prop="content">
<el-input v-model="editData.content" type="textarea" :rows="8" :maxlength="16000"
show-word-limit resize="vertical" placeholder="帖子内容和图片不能同时为空" />
</el-form-item>
<el-form-item label="帖子图片">
<material-picker v-model="editData.images" :limit="18" />
</el-form-item>
<el-divider content-position="left">付费设置</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="收费阅读" prop="is_paid">
<el-switch v-model="editData.is_paid" :active-value="1" :inactive-value="0"
@change="handlePaidChange" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="积分价格" prop="price_points">
<el-input-number v-model="editData.price_points" :min="0" :max="9999" :precision="0"
:disabled="editData.is_paid !== 1" controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="免费预览" prop="free_content_len">
<el-input-number v-model="editData.free_content_len" :min="20" :max="500"
:precision="0" :disabled="editData.is_paid !== 1" controls-position="right"
class="!w-full" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">统计数据手动修正</el-divider>
<el-row :gutter="20">
<el-col v-for="item in countFields" :key="item.key" :span="8">
<el-form-item :label="item.label" :prop="item.key">
<el-input-number v-model="editData[item.key]" :min="0" :precision="0"
controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">展示与状态</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="审核状态" prop="status">
<el-select v-model="editData.status" class="w-full">
<el-option label="待审核" :value="0" />
<el-option label="已通过" :value="1" />
<el-option label="已拒绝/隐藏" :value="2" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="推荐排序" prop="sort">
<el-input-number v-model="editData.sort" :min="0" :max="999999" :precision="0"
controls-position="right" class="!w-full" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="展示开关">
<div class="flex flex-wrap gap-x-4 gap-y-2">
<el-checkbox v-model="editData.is_top" :true-value="1" :false-value="0">置顶</el-checkbox>
<el-checkbox v-model="editData.is_recommend" :true-value="1"
:false-value="0">推荐</el-checkbox>
<el-checkbox v-model="editData.is_hot" :true-value="1" :false-value="0">热门</el-checkbox>
<el-checkbox v-model="editData.is_topic" :true-value="1"
:false-value="0">话题</el-checkbox>
</div>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">扩展数据</el-divider>
<el-form-item label="扩展JSON" prop="ext">
<el-input v-model="editData.ext" type="textarea" :rows="6" resize="vertical"
placeholder='请输入合法 JSON,例如 {"source":"manual"}' />
</el-form-item>
</el-form>
</el-scrollbar>
<template #footer>
<el-button @click="showEdit = false">取消</el-button>
<el-button type="primary" :loading="editSubmitLoading" @click="handleSubmitEdit">
<el-button type="primary" :loading="editSubmitLoading" :disabled="editLoading"
@click="handleSubmitEdit">
保存
</el-button>
</template>
@@ -223,7 +369,7 @@
import {
communityPostLists,
communityPostDetail,
communityPostEditContent,
communityPostEdit,
communityPostStatus,
communityPostSetTop,
communityPostSetHot,
@@ -231,11 +377,13 @@ import {
communityPostSetSort,
communityPostSetTopic,
communityPostSetCategory,
communityTagAll,
communityCategoryAll,
communityPostDelete
} from '@/api/community'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import type { FormInstance, FormItemRule, FormRules } from 'element-plus'
const queryParams = reactive({
status: '',
@@ -247,6 +395,7 @@ const queryParams = reactive({
})
const categoryList = ref<any[]>([])
const tagList = ref<any[]>([])
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: communityPostLists,
@@ -335,33 +484,167 @@ const handleDetail = async (id: number) => {
}
const showEdit = ref(false)
const editLoading = ref(false)
const editSubmitLoading = ref(false)
const editData = reactive({
const createEditData = () => ({
id: 0,
content: ''
origin_id: '',
user_id: 0,
content: '',
images: [] as string[],
post_type: 0,
category_id: 0,
match_id: 0,
tag_ids: [] as number[],
is_paid: 0,
price_points: 0,
free_content_len: 100,
paid_count: 0,
view_count: 0,
like_count: 0,
comment_count: 0,
share_count: 0,
ext: '',
is_top: 0,
is_hot: 0,
is_recommend: 0,
is_topic: 0,
sort: 0,
status: 1,
create_time: '',
update_time: ''
})
type EditData = ReturnType<typeof createEditData>
type CountFieldKey = 'paid_count' | 'view_count' | 'like_count' | 'comment_count' | 'share_count'
const handleEdit = (post: any) => {
const editData = reactive<EditData>(createEditData())
const editFormRef = ref<FormInstance>()
const countFields: Array<{ key: CountFieldKey; label: string }> = [
{ key: 'paid_count', label: '购买人数' },
{ key: 'view_count', label: '浏览数' },
{ key: 'like_count', label: '点赞数' },
{ key: 'comment_count', label: '评论数' },
{ key: 'share_count', label: '分享数' }
]
const validatePostBody = (_rule: any, _value: any, callback: (error?: Error) => void) => {
if (!editData.content.trim() && editData.images.length === 0) {
callback(new Error('帖子内容和图片不能同时为空'))
return
}
callback()
}
const validatePaidPrice = (_rule: any, value: number, callback: (error?: Error) => void) => {
if (editData.is_paid === 1 && (!value || value < 1 || value > 9999)) {
callback(new Error('付费帖子积分价格范围为1~9999'))
return
}
callback()
}
const validateExt = (_rule: any, value: string, callback: (error?: Error) => void) => {
if (!value.trim()) {
callback()
return
}
try {
JSON.parse(value)
callback()
} catch {
callback(new Error('请输入合法的 JSON'))
}
}
const nonNegativeRule: FormItemRule[] = [
{ required: true, type: 'number', min: 0, message: '请输入不小于0的整数' }
]
const editRules: FormRules = {
user_id: [{ required: true, type: 'number', min: 1, message: '请输入有效的用户ID', trigger: 'blur' }],
content: [{ validator: validatePostBody, trigger: 'blur' }],
post_type: [{ required: true, type: 'number', message: '请选择帖子类型', trigger: 'change' }],
category_id: nonNegativeRule,
match_id: nonNegativeRule,
tag_ids: [{ type: 'array', max: 3, message: '帖子标签最多选择3个', trigger: 'change' }],
price_points: [{ validator: validatePaidPrice, trigger: 'blur' }],
free_content_len: [{ required: true, type: 'number', min: 20, max: 500, message: '免费预览字数为20~500' }],
paid_count: nonNegativeRule,
view_count: nonNegativeRule,
like_count: nonNegativeRule,
comment_count: nonNegativeRule,
share_count: nonNegativeRule,
sort: nonNegativeRule,
status: [{ required: true, type: 'number', message: '请选择审核状态', trigger: 'change' }],
create_time: [{ required: true, message: '请选择发布时间', trigger: 'change' }],
ext: [{ validator: validateExt, trigger: 'blur' }]
}
const normalizeExt = (value: any) => {
if (value === null || value === undefined || value === '') return ''
if (typeof value === 'string') return value
return JSON.stringify(value, null, 2)
}
const handleEdit = async (id: number) => {
showEdit.value = true
editData.id = post.id
editData.content = post.content || ''
editLoading.value = true
Object.assign(editData, createEditData())
try {
const res = await communityPostDetail({ id })
Object.assign(editData, createEditData(), res, {
id: Number(res.id || id),
origin_id: String(res.origin_id || ''),
user_id: Number(res.user_id || 0),
content: String(res.content || ''),
images: Array.isArray(res.images) ? res.images : [],
post_type: Number(res.post_type || 0),
category_id: Number(res.category_id || 0),
match_id: Number(res.match_id || 0),
tag_ids: Array.isArray(res.tag_ids) ? res.tag_ids.map(Number) : [],
is_paid: Number(res.is_paid || 0),
ext: normalizeExt(res.ext),
price_points: Number(res.is_paid) === 1 ? Number(res.price_points || 0) : 0,
free_content_len: Number(res.is_paid) === 1 ? Number(res.free_content_len || 100) : 100,
paid_count: Number(res.paid_count || 0),
view_count: Number(res.view_count || 0),
like_count: Number(res.like_count || 0),
comment_count: Number(res.comment_count || 0),
share_count: Number(res.share_count || 0),
is_top: Number(res.is_top || 0),
is_hot: Number(res.is_hot || 0),
is_recommend: Number(res.is_recommend || 0),
is_topic: Number(res.is_topic || 0),
sort: Number(res.sort || 0),
status: Number(res.status || 0),
create_time: String(res.create_time || ''),
update_time: String(res.update_time || '')
})
} finally {
editLoading.value = false
}
}
const handlePaidChange = (value: any) => {
if (Number(value) !== 1) {
editData.price_points = 0
editData.free_content_len = 100
}
}
const handleSubmitEdit = async () => {
if (!editData.content.trim()) {
feedback.msgError('请输入帖子内容')
return
}
const valid = await editFormRef.value?.validate().catch(() => false)
if (!valid) return
editSubmitLoading.value = true
try {
await communityPostEditContent({
id: editData.id,
content: editData.content
await communityPostEdit({
...editData,
origin_id: editData.origin_id.trim(),
content: editData.content,
images: [...editData.images],
tag_ids: [...editData.tag_ids],
ext: editData.ext.trim()
})
showEdit.value = false
if (detailData.value.id === editData.id) {
detailData.value.content = editData.content
}
getLists()
} finally {
editSubmitLoading.value = false
@@ -377,6 +660,16 @@ const loadCategories = async () => {
}
}
const loadTags = async () => {
try {
const tags = await communityTagAll()
tagList.value = Array.isArray(tags) ? tags : []
} catch {
tagList.value = []
}
}
loadCategories()
loadTags()
getLists()
</script>
@@ -204,7 +204,7 @@ FROM (
SELECT '设置推荐' AS `name`, 'community.communityPost/setRecommend' AS `perms`
UNION ALL SELECT '设置话题', 'community.communityPost/setTopic'
UNION ALL SELECT '设置分类', 'community.communityPost/setCategory'
UNION ALL SELECT '编辑内容', 'community.communityPost/editContent'
UNION ALL SELECT '编辑帖子', 'community.communityPost/edit'
) permission
WHERE @post_menu_id IS NOT NULL
AND NOT EXISTS (
@@ -224,7 +224,7 @@ WHERE menu.`perms` IN (
'community.communityPost/setRecommend',
'community.communityPost/setTopic',
'community.communityPost/setCategory',
'community.communityPost/editContent'
'community.communityPost/edit'
)
AND NOT EXISTS (
SELECT 1
@@ -1,24 +0,0 @@
SET @post_menu_id = COALESCE(
(SELECT `id` FROM `la_system_menu` WHERE `perms` = 'community.communityPost/lists' LIMIT 1),
(SELECT `id` FROM `la_system_menu` WHERE `component` = 'community/post/index' LIMIT 1)
);
INSERT INTO `la_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
@post_menu_id, 'A', '编辑内容', '', 0, 'community.communityPost/editContent', '', '', '', '', 0, 0, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
WHERE @post_menu_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM `la_system_menu` WHERE `perms` = 'community.communityPost/editContent'
);
INSERT INTO `la_system_role_menu` (`role_id`, `menu_id`)
SELECT 1, menu.`id`
FROM `la_system_menu` menu
WHERE menu.`perms` = 'community.communityPost/editContent'
AND NOT EXISTS (
SELECT 1
FROM `la_system_role_menu` role_menu
WHERE role_menu.`role_id` = 1
AND role_menu.`menu_id` = menu.`id`
);
@@ -0,0 +1,71 @@
SET @post_menu_id = COALESCE(
(SELECT `id` FROM `la_system_menu` WHERE `perms` = 'community.communityPost/lists' LIMIT 1),
(SELECT `id` FROM `la_system_menu` WHERE `component` = 'community/post/index' LIMIT 1)
);
SET @old_edit_menu_id = (
SELECT `id` FROM `la_system_menu`
WHERE `perms` = 'community.communityPost/editContent'
LIMIT 1
);
SET @edit_menu_id = (
SELECT `id` FROM `la_system_menu`
WHERE `perms` = 'community.communityPost/edit'
LIMIT 1
);
UPDATE `la_system_menu`
SET `name` = '编辑帖子',
`perms` = 'community.communityPost/edit',
`update_time` = UNIX_TIMESTAMP()
WHERE `id` = @old_edit_menu_id
AND @edit_menu_id IS NULL;
INSERT INTO `la_system_menu`
(`pid`, `type`, `name`, `icon`, `sort`, `perms`, `paths`, `component`, `selected`, `params`, `is_cache`, `is_show`, `is_disable`, `create_time`, `update_time`)
SELECT
@post_menu_id, 'A', '编辑帖子', '', 0, 'community.communityPost/edit', '', '', '', '', 0, 0, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
WHERE @post_menu_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM `la_system_menu` WHERE `perms` = 'community.communityPost/edit'
);
SET @edit_menu_id = (
SELECT `id` FROM `la_system_menu`
WHERE `perms` = 'community.communityPost/edit'
LIMIT 1
);
INSERT INTO `la_system_role_menu` (`role_id`, `menu_id`)
SELECT 1, @edit_menu_id
WHERE @edit_menu_id IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM `la_system_role_menu`
WHERE `role_id` = 1
AND `menu_id` = @edit_menu_id
);
INSERT INTO `la_system_role_menu` (`role_id`, `menu_id`)
SELECT old_role_menu.`role_id`, @edit_menu_id
FROM `la_system_role_menu` old_role_menu
WHERE old_role_menu.`menu_id` = @old_edit_menu_id
AND @old_edit_menu_id IS NOT NULL
AND @old_edit_menu_id <> @edit_menu_id
AND NOT EXISTS (
SELECT 1
FROM `la_system_role_menu` current_role_menu
WHERE current_role_menu.`role_id` = old_role_menu.`role_id`
AND current_role_menu.`menu_id` = @edit_menu_id
);
DELETE FROM `la_system_role_menu`
WHERE `menu_id` = @old_edit_menu_id
AND @old_edit_menu_id IS NOT NULL
AND @old_edit_menu_id <> @edit_menu_id;
DELETE FROM `la_system_menu`
WHERE `id` = @old_edit_menu_id
AND @old_edit_menu_id IS NOT NULL
AND @old_edit_menu_id <> @edit_menu_id;
@@ -21,10 +21,10 @@ class CommunityPostController extends BaseAdminController
return $this->data($result);
}
public function editContent()
public function edit()
{
$params = (new CommunityPostValidate())->post()->goCheck('editContent');
$result = CommunityPostLogic::editContent($params);
$params = (new CommunityPostValidate())->post()->goCheck('edit');
$result = CommunityPostLogic::edit($params);
if (true === $result) {
return $this->success('保存成功', [], 1, 1);
}
@@ -5,6 +5,7 @@ namespace app\adminapi\logic\community;
use app\common\logic\BaseLogic;
use app\common\model\community\CommunityCategory;
use app\common\model\community\CommunityPost;
use app\common\model\community\CommunityTag;
use app\common\model\user\User;
use app\common\service\ai\KbSyncService;
use think\facade\Db;
@@ -26,6 +27,19 @@ class CommunityPostLogic extends BaseLogic
$post['category_name'] = (int) ($post['category_id'] ?? 0) > 0
? (string) CommunityCategory::where('id', $post['category_id'])->value('name')
: '';
$post['tag_ids'] = array_map('intval', Db::name('community_post_tag')
->where('post_id', $params['id'])
->column('tag_id'));
if (is_array($post['ext'] ?? null)) {
$post['ext'] = json_encode($post['ext'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
} elseif (is_string($post['ext'] ?? null) && trim($post['ext']) !== '') {
$decodedExt = json_decode($post['ext'], true);
$post['ext'] = json_last_error() === JSON_ERROR_NONE
? json_encode($decodedExt, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)
: $post['ext'];
} else {
$post['ext'] = '';
}
return $post;
}
@@ -50,7 +64,7 @@ class CommunityPostLogic extends BaseLogic
}
}
public static function editContent(array $params): bool
public static function edit(array $params): bool
{
$post = CommunityPost::findOrEmpty($params['id']);
if ($post->isEmpty()) {
@@ -58,17 +72,79 @@ class CommunityPostLogic extends BaseLogic
return false;
}
$oldCategoryId = (int) $post->category_id;
$oldTagIds = array_map('intval', Db::name('community_post_tag')
->where('post_id', $params['id'])
->column('tag_id'));
$newCategoryId = (int) $params['category_id'];
$newTagIds = array_values(array_unique(array_filter(
array_map('intval', $params['tag_ids'] ?? []),
static fn (int $tagId): bool => $tagId > 0
)));
$isPaid = (int) $params['is_paid'] === 1;
$extValue = $params['ext'] ?? '';
$ext = is_array($extValue)
? json_encode($extValue, JSON_UNESCAPED_UNICODE)
: trim((string) $extValue);
Db::startTrans();
try {
$post->content = (string) $params['content'];
$post->save();
if ((int) $post->status === 1) {
KbSyncService::enqueue('post', 'post', (int) $post->id, 'upsert', 30);
$post->save([
'origin_id' => trim((string) ($params['origin_id'] ?? '')) ?: null,
'user_id' => (int) $params['user_id'],
'content' => (string) ($params['content'] ?? ''),
'images' => array_values($params['images'] ?? []),
'post_type' => (int) $params['post_type'],
'category_id' => $newCategoryId,
'match_id' => (int) $params['match_id'],
'is_paid' => $isPaid ? 1 : 0,
'price_points' => $isPaid ? (int) $params['price_points'] : 0,
'free_content_len' => $isPaid ? (int) $params['free_content_len'] : 100,
'paid_count' => (int) $params['paid_count'],
'view_count' => (int) $params['view_count'],
'like_count' => (int) $params['like_count'],
'comment_count' => (int) $params['comment_count'],
'share_count' => (int) $params['share_count'],
'ext' => $ext === '' ? null : $ext,
'is_top' => (int) $params['is_top'],
'is_hot' => (int) $params['is_hot'],
'is_recommend' => (int) $params['is_recommend'],
'is_topic' => (int) $params['is_topic'],
'sort' => (int) $params['sort'],
'status' => (int) $params['status'],
'create_time' => strtotime((string) $params['create_time']),
'update_time' => time(),
]);
Db::name('community_post_tag')->where('post_id', $post->id)->delete();
if ($newTagIds) {
Db::name('community_post_tag')->insertAll(array_map(
static fn (int $tagId): array => [
'post_id' => (int) $post->id,
'tag_id' => $tagId,
],
$newTagIds
));
}
return true;
self::refreshCategoryCount($oldCategoryId);
self::refreshCategoryCount($newCategoryId);
self::refreshTagCounts(array_merge($oldTagIds, $newTagIds));
Db::commit();
} catch (\Exception $e) {
Db::rollback();
self::setError($e->getMessage());
return false;
}
KbSyncService::enqueue(
'post',
'post',
(int) $post->id,
(int) $post->status === 1 ? 'upsert' : 'delete',
30
);
return true;
}
public static function setTop(array $params): bool
@@ -188,4 +264,24 @@ class CommunityPostLogic extends BaseLogic
'update_time' => time(),
]);
}
private static function refreshTagCounts(array $tagIds): void
{
$tagIds = array_values(array_unique(array_filter(
array_map('intval', $tagIds),
static fn (int $tagId): bool => $tagId > 0
)));
foreach ($tagIds as $tagId) {
$postCount = Db::name('community_post_tag')
->alias('post_tag')
->join('community_post post', 'post.id = post_tag.post_id')
->where('post_tag.tag_id', $tagId)
->whereNull('post.delete_time')
->count();
CommunityTag::where('id', $tagId)->update([
'post_count' => $postCount,
'update_time' => time(),
]);
}
}
}
@@ -48,6 +48,6 @@ class CommunityTagLogic extends BaseLogic
public static function all(): array
{
return CommunityTag::where('status', 1)->order('sort', 'desc')->order('id', 'desc')->select()->toArray();
return CommunityTag::order('sort', 'desc')->order('id', 'desc')->select()->toArray();
}
}
@@ -5,11 +5,30 @@ namespace app\adminapi\validate\community;
use app\common\validate\BaseValidate;
use app\common\model\community\CommunityCategory;
use app\common\model\community\CommunityPost;
use app\common\model\community\CommunityTag;
use app\common\model\user\User;
use think\facade\Db;
class CommunityPostValidate extends BaseValidate
{
protected $rule = [
'id' => 'require|checkPost',
'origin_id' => 'max:100|checkOriginId',
'user_id' => 'require|integer|gt:0|checkUser|checkBody',
'content' => 'max:16000',
'images' => 'array|checkImages',
'post_type' => 'require|in:0,1,2',
'match_id' => 'require|integer|egt:0|checkMatch',
'tag_ids' => 'array|checkTags',
'is_paid' => 'require|in:0,1|checkPayment',
'price_points' => 'require|integer|egt:0|elt:10000',
'free_content_len' => 'require|integer|egt:0|elt:501|checkFreeContentLen',
'paid_count' => 'require|integer|egt:0|elt:4294967296',
'view_count' => 'require|integer|egt:0|elt:4294967296',
'like_count' => 'require|integer|egt:0|elt:4294967296',
'comment_count' => 'require|integer|egt:0|elt:4294967296',
'share_count' => 'require|integer|egt:0|elt:4294967296',
'ext' => 'checkExt',
'status' => 'require|in:0,1,2',
'is_top' => 'require|in:0,1',
'is_hot' => 'require|in:0,1',
@@ -17,14 +36,22 @@ class CommunityPostValidate extends BaseValidate
'sort' => 'require|integer|egt:0|elt:1000000',
'is_topic' => 'require|in:0,1',
'category_id' => 'require|integer|egt:0|checkCategory',
'content' => 'require|max:16000',
'create_time' => 'require|checkDateTime',
];
protected $message = [
'id.require' => '帖子id不能为空',
'status.require' => '状态不能为空',
'content.require' => '帖子内容不能为空',
'content.max' => '帖子内容不能超过16000个字符',
'origin_id.max' => '来源标识不能超过100个字符',
'user_id.require' => '用户ID不能为空',
'images.array' => '帖子图片格式错误',
'post_type.in' => '帖子类型错误',
'match_id.egt' => '赛事ID不能小于0',
'tag_ids.array' => '帖子标签格式错误',
'price_points.elt' => '积分价格范围为0~9999',
'free_content_len.elt' => '免费预览字数不能超过500',
'create_time.require' => '发布时间不能为空',
];
public function sceneDetail()
@@ -42,9 +69,15 @@ class CommunityPostValidate extends BaseValidate
return $this->only(['id', 'status']);
}
public function sceneEditContent()
public function sceneEdit()
{
return $this->only(['id', 'content']);
return $this->only([
'id', 'origin_id', 'user_id', 'content', 'images', 'post_type', 'category_id',
'match_id', 'tag_ids', 'is_paid', 'price_points', 'free_content_len',
'paid_count', 'view_count', 'like_count', 'comment_count', 'share_count',
'ext', 'is_top', 'is_hot', 'is_recommend', 'is_topic', 'sort', 'status',
'create_time',
]);
}
public function sceneTop()
@@ -86,14 +119,111 @@ class CommunityPostValidate extends BaseValidate
return true;
}
public function checkCategory($value)
public function checkOriginId($value, $rule, $data)
{
$originId = trim((string) $value);
if ($originId === '') {
return true;
}
$exists = Db::name('community_post')
->where('origin_id', $originId)
->where('id', '<>', (int) ($data['id'] ?? 0))
->count();
return $exists > 0 ? '来源标识已存在' : true;
}
public function checkUser($value)
{
return User::where('id', $value)->count() > 0 ? true : '用户不存在';
}
public function checkBody($value, $rule, $data)
{
if (trim((string) ($data['content'] ?? '')) === '' && empty($data['images'])) {
return '帖子内容和图片不能同时为空';
}
return true;
}
public function checkImages($value)
{
if (count($value) > 18) {
return '帖子图片最多18张';
}
foreach ($value as $image) {
if (!is_string($image) || trim($image) === '') {
return '帖子图片地址格式错误';
}
}
return true;
}
public function checkMatch($value)
{
if ((int) $value === 0) {
return true;
}
$category = CommunityCategory::where('id', $value)->where('status', 1)->findOrEmpty();
return Db::name('match')->where('id', $value)->count() > 0 ? true : '关联赛事不存在';
}
public function checkTags($value)
{
$tagIds = array_values(array_unique(array_map('intval', $value)));
if (count($tagIds) > 3) {
return '帖子标签最多选择3个';
}
if (!$tagIds) {
return true;
}
return CommunityTag::whereIn('id', $tagIds)->count() === count($tagIds)
? true
: '帖子标签不存在';
}
public function checkPayment($value, $rule, $data)
{
if ((int) $value === 1 && (int) ($data['price_points'] ?? 0) <= 0) {
return '付费帖子必须设置积分价格';
}
return true;
}
public function checkFreeContentLen($value, $rule, $data)
{
if ((int) ($data['is_paid'] ?? 0) === 1 && (int) $value < 20) {
return '付费帖子免费预览字数不能少于20';
}
return true;
}
public function checkExt($value)
{
if (is_array($value) || trim((string) $value) === '') {
return true;
}
json_decode((string) $value, true);
return json_last_error() === JSON_ERROR_NONE ? true : '扩展数据必须是有效的JSON';
}
public function checkDateTime($value)
{
return strtotime((string) $value) !== false ? true : '发布时间格式错误';
}
public function checkCategory($value, $rule, $data)
{
if ((int) $value === 0) {
return true;
}
$category = CommunityCategory::where('id', $value)->findOrEmpty();
if ($category->isEmpty()) {
return '帖子分类不存在或已禁用';
return '帖子分类不存在';
}
if ((int) $category->status !== 1) {
$currentCategoryId = (int) CommunityPost::where('id', $data['id'] ?? 0)->value('category_id');
if ($currentCategoryId !== (int) $value) {
return '帖子分类已禁用';
}
}
return true;
}