feat(plaza-ops): add post management API for admin console

Expose categories listing, category filter on review queue, and invalidate
feed caches when ops hides a published post.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-05 19:27:09 +08:00
parent dd091e6e2c
commit 0c2cb9e283
4 changed files with 114 additions and 4 deletions
+72 -3
View File
@@ -251,7 +251,69 @@ export function createPlazaPostService(
};
};
const createPost = async (userId, input) => {
const findPostByPublicationId = async (publicationId, { userId = null } = {}) => {
const params = [publicationId];
let userClause = '';
if (userId) {
userClause = ' AND pp.user_id = ?';
params.push(userId);
}
const [rows] = await pool.query(
`SELECT pp.id, pp.status
FROM plaza_posts pp
WHERE pp.publication_id = ?${userClause} AND pp.status != 'hidden'
LIMIT 1`,
params,
);
return rows[0] ?? null;
};
const republishExistingPost = async (userId, postId, options = {}) => {
const now = Date.now();
const status = autoApprove || options.forcePublished ? 'published' : 'pending_review';
const hotScore = status === 'published' ? await resolveHotScore(now) : 0;
const [result] = await pool.query(
`UPDATE plaza_posts
SET status = ?, hot_score = ?, hot_updated_at = ?, updated_at = ?, published_at = ?
WHERE id = ? AND user_id = ? AND status IN ('hidden', 'rejected')`,
[status, hotScore, status === 'published' ? now : null, now, now, postId, userId],
);
if (result.affectedRows === 0) {
throw plazaError('帖子不存在或无法重新发布', 'POST_NOT_FOUND');
}
if (status === 'published') {
await plazaRedis?.invalidateFeedCaches?.();
if (options.deferPostPublishedHooks) {
void onPostPublished?.(postId);
} else {
await onPostPublished?.(postId);
}
}
return { id: postId, status };
};
const publishPostForPublication = async (userId, input, options = {}) => {
const publicationId = String(input?.publication_id ?? '').trim();
if (!publicationId) throw plazaError('publication_id 不能为空', 'invalid_input');
const [existingRows] = await pool.query(
`SELECT id, status FROM plaza_posts WHERE publication_id = ? AND user_id = ? LIMIT 1`,
[publicationId, userId],
);
const existing = existingRows[0];
if (existing) {
if (existing.status === 'published' || existing.status === 'pending_review') {
throw plazaError('该内容已发布到广场', 'ALREADY_PUBLISHED', { post_id: existing.id });
}
if (existing.status === 'hidden' || existing.status === 'rejected') {
return republishExistingPost(userId, existing.id, options);
}
}
return createPost(userId, input, options);
};
const createPost = async (userId, input, options = {}) => {
const publicationId = String(input?.publication_id ?? '').trim();
if (!publicationId) throw plazaError('publication_id 不能为空', 'invalid_input');
@@ -269,7 +331,7 @@ export function createPlazaPostService(
const allowComment = input?.allow_comment == null ? true : Boolean(input.allow_comment);
const now = Date.now();
const postId = idFactory();
const status = autoApprove ? 'published' : 'pending_review';
const status = autoApprove || options.forcePublished ? 'published' : 'pending_review';
const hotScore = status === 'published' ? await resolveHotScore(now) : 0;
const conn = await pool.getConnection();
@@ -317,7 +379,11 @@ export function createPlazaPostService(
if (status === 'published') {
await plazaRedis?.invalidateFeedCaches?.();
await onPostPublished?.(postId);
if (options.deferPostPublishedHooks) {
void onPostPublished?.(postId);
} else {
await onPostPublished?.(postId);
}
}
return { id: postId, status };
@@ -591,6 +657,7 @@ export function createPlazaPostService(
`UPDATE plaza_posts SET status = 'hidden', updated_at = ? WHERE id = ?`,
[now, postId],
);
await plazaRedis?.invalidateFeedCaches?.();
return { id: postId, status: 'hidden' };
}
throw plazaError('不支持的审核操作', 'invalid_input');
@@ -619,6 +686,8 @@ export function createPlazaPostService(
ensureCategories,
listCategories,
createPost,
publishPostForPublication,
findPostByPublicationId,
updatePost,
hidePost,
hidePostsByPublicationId,