Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c2cb9e283 | |||
| dd091e6e2c |
@@ -576,6 +576,14 @@ export function createOpsApi({ jsonBody, plazaOps }) {
|
|||||||
opsApi.use(jsonBody);
|
opsApi.use(jsonBody);
|
||||||
opsApi.use(makeRequireOps('reviewer'));
|
opsApi.use(makeRequireOps('reviewer'));
|
||||||
|
|
||||||
|
opsApi.get('/categories', async (req, res) => {
|
||||||
|
try {
|
||||||
|
return sendData(res, req, { categories: await plazaOps.listCategories() });
|
||||||
|
} catch (error) {
|
||||||
|
return plazaRouteError(res, req, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
opsApi.get('/review/queue', async (req, res) => {
|
opsApi.get('/review/queue', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const queue = await plazaOps.listReviewQueue({
|
const queue = await plazaOps.listReviewQueue({
|
||||||
@@ -583,6 +591,7 @@ export function createOpsApi({ jsonBody, plazaOps }) {
|
|||||||
cursor: req.query.cursor ?? null,
|
cursor: req.query.cursor ?? null,
|
||||||
limit: req.query.limit,
|
limit: req.query.limit,
|
||||||
keyword: req.query.keyword ?? null,
|
keyword: req.query.keyword ?? null,
|
||||||
|
category: req.query.category ?? null,
|
||||||
});
|
});
|
||||||
return sendData(res, req, queue);
|
return sendData(res, req, queue);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -434,6 +434,14 @@ export function attachPlazaApiRoutes(api, plaza, { sendData, sendError }) {
|
|||||||
opsApi.use(express.json({ limit: '1mb' }));
|
opsApi.use(express.json({ limit: '1mb' }));
|
||||||
opsApi.use(makeRequireOps('reviewer'));
|
opsApi.use(makeRequireOps('reviewer'));
|
||||||
|
|
||||||
|
opsApi.get('/categories', async (req, res) => {
|
||||||
|
try {
|
||||||
|
return sendData(res, req, { categories: await ops.listCategories() });
|
||||||
|
} catch (error) {
|
||||||
|
return routeError(res, req, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
opsApi.get('/review/queue', async (req, res) => {
|
opsApi.get('/review/queue', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const queue = await ops.listReviewQueue({
|
const queue = await ops.listReviewQueue({
|
||||||
@@ -441,6 +449,7 @@ export function attachPlazaApiRoutes(api, plaza, { sendData, sendError }) {
|
|||||||
cursor: req.query.cursor ?? null,
|
cursor: req.query.cursor ?? null,
|
||||||
limit: req.query.limit,
|
limit: req.query.limit,
|
||||||
keyword: req.query.keyword ?? null,
|
keyword: req.query.keyword ?? null,
|
||||||
|
category: req.query.category ?? null,
|
||||||
});
|
});
|
||||||
return sendData(res, req, queue);
|
return sendData(res, req, queue);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+24
-1
@@ -46,11 +46,27 @@ export function createPlazaOpsService(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const listCategories = async () => {
|
||||||
|
const [rows] = await pool.query(
|
||||||
|
`SELECT id, name, slug, icon
|
||||||
|
FROM plaza_categories
|
||||||
|
WHERE is_active = 1
|
||||||
|
ORDER BY sort_order ASC, name ASC`,
|
||||||
|
);
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
slug: row.slug,
|
||||||
|
icon: row.icon ?? '',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const listReviewQueue = async ({
|
const listReviewQueue = async ({
|
||||||
status = 'pending_review',
|
status = 'pending_review',
|
||||||
cursor = null,
|
cursor = null,
|
||||||
limit = 20,
|
limit = 20,
|
||||||
keyword = null,
|
keyword = null,
|
||||||
|
category = null,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const pageLimit = clampLimit(limit, 20, 100);
|
const pageLimit = clampLimit(limit, 20, 100);
|
||||||
const params = [status];
|
const params = [status];
|
||||||
@@ -65,13 +81,19 @@ export function createPlazaOpsService(
|
|||||||
const pattern = `%${String(keyword).trim()}%`;
|
const pattern = `%${String(keyword).trim()}%`;
|
||||||
params.push(pattern, pattern, pattern);
|
params.push(pattern, pattern, pattern);
|
||||||
}
|
}
|
||||||
|
let categoryClause = '';
|
||||||
|
const categorySlug = String(category ?? '').trim();
|
||||||
|
if (categorySlug) {
|
||||||
|
categoryClause = 'AND c.slug = ?';
|
||||||
|
params.push(categorySlug);
|
||||||
|
}
|
||||||
params.push(pageLimit + 1);
|
params.push(pageLimit + 1);
|
||||||
const [rows] = await pool.query(
|
const [rows] = await pool.query(
|
||||||
`SELECT pp.id, pp.title, pp.summary, pp.cover_url, pp.status, pp.user_display_name, pp.user_slug,
|
`SELECT pp.id, pp.title, pp.summary, pp.cover_url, pp.status, pp.user_display_name, pp.user_slug,
|
||||||
pp.published_at, pp.created_at, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon
|
pp.published_at, pp.created_at, c.name AS category_name, c.slug AS category_slug, c.icon AS category_icon
|
||||||
FROM plaza_posts pp
|
FROM plaza_posts pp
|
||||||
JOIN plaza_categories c ON c.id = pp.category_id
|
JOIN plaza_categories c ON c.id = pp.category_id
|
||||||
WHERE pp.status = ? ${cursorClause} ${keywordClause}
|
WHERE pp.status = ? ${cursorClause} ${keywordClause} ${categoryClause}
|
||||||
ORDER BY pp.published_at DESC, pp.id DESC
|
ORDER BY pp.published_at DESC, pp.id DESC
|
||||||
LIMIT ?`,
|
LIMIT ?`,
|
||||||
params,
|
params,
|
||||||
@@ -419,6 +441,7 @@ export function createPlazaOpsService(
|
|||||||
return {
|
return {
|
||||||
loadOperatorRole,
|
loadOperatorRole,
|
||||||
hasOpsRole,
|
hasOpsRole,
|
||||||
|
listCategories,
|
||||||
listReviewQueue,
|
listReviewQueue,
|
||||||
reviewPostAsOps,
|
reviewPostAsOps,
|
||||||
batchReviewPosts,
|
batchReviewPosts,
|
||||||
|
|||||||
+72
-3
@@ -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();
|
const publicationId = String(input?.publication_id ?? '').trim();
|
||||||
if (!publicationId) throw plazaError('publication_id 不能为空', 'invalid_input');
|
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 allowComment = input?.allow_comment == null ? true : Boolean(input.allow_comment);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const postId = idFactory();
|
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 hotScore = status === 'published' ? await resolveHotScore(now) : 0;
|
||||||
|
|
||||||
const conn = await pool.getConnection();
|
const conn = await pool.getConnection();
|
||||||
@@ -317,7 +379,11 @@ export function createPlazaPostService(
|
|||||||
|
|
||||||
if (status === 'published') {
|
if (status === 'published') {
|
||||||
await plazaRedis?.invalidateFeedCaches?.();
|
await plazaRedis?.invalidateFeedCaches?.();
|
||||||
await onPostPublished?.(postId);
|
if (options.deferPostPublishedHooks) {
|
||||||
|
void onPostPublished?.(postId);
|
||||||
|
} else {
|
||||||
|
await onPostPublished?.(postId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { id: postId, status };
|
return { id: postId, status };
|
||||||
@@ -591,6 +657,7 @@ export function createPlazaPostService(
|
|||||||
`UPDATE plaza_posts SET status = 'hidden', updated_at = ? WHERE id = ?`,
|
`UPDATE plaza_posts SET status = 'hidden', updated_at = ? WHERE id = ?`,
|
||||||
[now, postId],
|
[now, postId],
|
||||||
);
|
);
|
||||||
|
await plazaRedis?.invalidateFeedCaches?.();
|
||||||
return { id: postId, status: 'hidden' };
|
return { id: postId, status: 'hidden' };
|
||||||
}
|
}
|
||||||
throw plazaError('不支持的审核操作', 'invalid_input');
|
throw plazaError('不支持的审核操作', 'invalid_input');
|
||||||
@@ -619,6 +686,8 @@ export function createPlazaPostService(
|
|||||||
ensureCategories,
|
ensureCategories,
|
||||||
listCategories,
|
listCategories,
|
||||||
createPost,
|
createPost,
|
||||||
|
publishPostForPublication,
|
||||||
|
findPostByPublicationId,
|
||||||
updatePost,
|
updatePost,
|
||||||
hidePost,
|
hidePost,
|
||||||
hidePostsByPublicationId,
|
hidePostsByPublicationId,
|
||||||
|
|||||||
Reference in New Issue
Block a user