feat: add admin gallery prompt APIs
This commit is contained in:
102
src/app/api/admin/gallery/prompt/route.ts
Normal file
102
src/app/api/admin/gallery/prompt/route.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import type { PoolClient } from 'pg';
|
||||
import {
|
||||
AdminGalleryPromptError,
|
||||
updateAdminGalleryPrompt,
|
||||
type AdminGalleryPromptEmailMessage,
|
||||
type AdminGalleryPromptWorkRow,
|
||||
} from '@/lib/admin-gallery-prompt-service';
|
||||
import { getRequestBaseUrl, sendTemplatedEmail } from '@/lib/email-service';
|
||||
import { writePlatformLog } from '@/lib/platform-logs';
|
||||
import { requireAdminUser } from '@/lib/session-auth';
|
||||
import { getDbClient } from '@/storage/database/local-db';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
async function loadPublicGalleryWork(client: PoolClient, workId: string): Promise<AdminGalleryPromptWorkRow | null> {
|
||||
const result = await client.query(
|
||||
`SELECT w.id, w.user_id, w.type, w.title, w.prompt, w.negative_prompt,
|
||||
w.result_url, w.thumbnail_url, w.likes_count, w.is_public, w.status, w.created_at,
|
||||
p.email AS author_email,
|
||||
p.nickname AS author_nickname,
|
||||
p.display_nickname AS author_display_nickname,
|
||||
p.avatar_url AS author_avatar_url
|
||||
FROM works w
|
||||
LEFT JOIN profiles p ON p.id = w.user_id
|
||||
WHERE w.id = $1
|
||||
LIMIT 1`,
|
||||
[workId],
|
||||
);
|
||||
return (result.rows[0] as AdminGalleryPromptWorkRow | undefined) || null;
|
||||
}
|
||||
|
||||
export async function PUT(request: NextRequest) {
|
||||
const admin = await requireAdminUser(request);
|
||||
if (admin instanceof NextResponse) return admin;
|
||||
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const client = await getDbClient();
|
||||
try {
|
||||
const assetBaseUrl = getRequestBaseUrl(request) || undefined;
|
||||
const result = await updateAdminGalleryPrompt(body, {
|
||||
admin,
|
||||
loadWork: workId => loadPublicGalleryWork(client, workId),
|
||||
updatePrompt: async (workId, prompt) => {
|
||||
const updateResult = await client.query(
|
||||
'UPDATE works SET prompt = $2, updated_at = NOW() WHERE id = $1 RETURNING id',
|
||||
[workId, prompt],
|
||||
);
|
||||
if ((updateResult.rowCount || 0) === 0) {
|
||||
throw new AdminGalleryPromptError('作品更新失败', 500);
|
||||
}
|
||||
const updated = await loadPublicGalleryWork(client, workId);
|
||||
if (!updated) throw new AdminGalleryPromptError('作品更新后读取失败', 500);
|
||||
return updated;
|
||||
},
|
||||
sendEmail: async (message: AdminGalleryPromptEmailMessage) => {
|
||||
try {
|
||||
await sendTemplatedEmail(client, {
|
||||
to: message.to,
|
||||
type: 'business',
|
||||
subject: message.subject,
|
||||
title: message.subject,
|
||||
body: message.body,
|
||||
note: '这是一封公开作品内容调整通知,请勿直接回复。',
|
||||
templateKind: 'admin',
|
||||
ipAddress: 'admin-gallery-prompt',
|
||||
assetBaseUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
const text = error instanceof Error ? error.message : String(error);
|
||||
throw new AdminGalleryPromptError(`邮件发送失败:${text}`, 502);
|
||||
}
|
||||
},
|
||||
writeLog: async entry => {
|
||||
await writePlatformLog({
|
||||
type: entry.type === 'admin' ? 'admin' : 'admin',
|
||||
level: entry.level === 'warning' || entry.level === 'error' ? entry.level : 'info',
|
||||
action: String(entry.action || 'admin_gallery_prompt_update'),
|
||||
message: String(entry.message || '管理员修改公开画廊作品提示词并发送邮件通知'),
|
||||
userId: typeof entry.userId === 'string' ? entry.userId : admin.userId,
|
||||
targetType: typeof entry.targetType === 'string' ? entry.targetType : 'work',
|
||||
targetId: typeof entry.targetId === 'string' ? entry.targetId : null,
|
||||
metadata: (entry.metadata && typeof entry.metadata === 'object' ? entry.metadata : {}) as Record<string, unknown>,
|
||||
request,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
work: result.work,
|
||||
notificationSent: result.notificationSent,
|
||||
});
|
||||
} catch (error) {
|
||||
const status = error instanceof AdminGalleryPromptError ? error.status : 500;
|
||||
const message = error instanceof Error ? error.message : '修改画廊作品提示词失败';
|
||||
if (status >= 500) console.error('[admin/gallery/prompt] PUT error:', error);
|
||||
return NextResponse.json({ error: message }, { status });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
105
src/app/api/admin/gallery/works/route.ts
Normal file
105
src/app/api/admin/gallery/works/route.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { requireAdmin } from '@/lib/admin-auth';
|
||||
import { toAdminGalleryPromptWork, type AdminGalleryPromptWorkRow } from '@/lib/admin-gallery-prompt-service';
|
||||
import { getDbClient } from '@/storage/database/local-db';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
const WORK_TYPES = new Set(['text2img', 'img2img', 'text2video', 'img2video']);
|
||||
|
||||
function intParam(value: string | null, fallback: number, min: number, max: number) {
|
||||
const parsed = Number.parseInt(value || '', 10);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return Math.min(max, Math.max(min, parsed));
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authError = await requireAdmin(request);
|
||||
if (authError) return authError;
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const q = (searchParams.get('q') || searchParams.get('search') || '').trim().toLowerCase();
|
||||
const type = searchParams.get('type') || 'all';
|
||||
const sort = searchParams.get('sort') || 'newest';
|
||||
const limit = intParam(searchParams.get('limit'), 20, 1, 100);
|
||||
const offset = intParam(searchParams.get('offset'), 0, 0, 1000000);
|
||||
|
||||
const where: string[] = [
|
||||
'w.is_public = true',
|
||||
"w.status = 'completed'",
|
||||
"COALESCE(w.result_url, '') <> ''",
|
||||
];
|
||||
const params: unknown[] = [];
|
||||
|
||||
if (type === 'image') {
|
||||
params.push('text2img', 'img2img');
|
||||
where.push(`w.type IN ($${params.length - 1}, $${params.length})`);
|
||||
} else if (type === 'video') {
|
||||
params.push('text2video', 'img2video');
|
||||
where.push(`w.type IN ($${params.length - 1}, $${params.length})`);
|
||||
} else if (WORK_TYPES.has(type)) {
|
||||
params.push(type);
|
||||
where.push(`w.type = $${params.length}`);
|
||||
} else if (type !== 'all') {
|
||||
return NextResponse.json({ error: '作品类型无效' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (q) {
|
||||
params.push(`%${q}%`);
|
||||
where.push(`(
|
||||
LOWER(w.id::text) LIKE $${params.length}
|
||||
OR LOWER(COALESCE(w.title, '')) LIKE $${params.length}
|
||||
OR LOWER(COALESCE(w.prompt, '')) LIKE $${params.length}
|
||||
OR LOWER(COALESCE(w.negative_prompt, '')) LIKE $${params.length}
|
||||
OR LOWER(COALESCE(p.email, '')) LIKE $${params.length}
|
||||
OR LOWER(COALESCE(p.display_nickname, p.nickname, '')) LIKE $${params.length}
|
||||
OR LOWER(COALESCE(p.nickname, '')) LIKE $${params.length}
|
||||
)`);
|
||||
}
|
||||
|
||||
const whereSql = `WHERE ${where.join(' AND ')}`;
|
||||
const orderSql = sort === 'popular'
|
||||
? 'ORDER BY w.likes_count DESC, w.created_at DESC'
|
||||
: 'ORDER BY w.created_at DESC';
|
||||
|
||||
const client = await getDbClient();
|
||||
try {
|
||||
const countResult = await client.query(
|
||||
`SELECT COUNT(*)::int AS total
|
||||
FROM works w
|
||||
LEFT JOIN profiles p ON p.id = w.user_id
|
||||
${whereSql}`,
|
||||
params,
|
||||
);
|
||||
const result = await client.query(
|
||||
`SELECT w.id, w.user_id, w.type, w.title, w.prompt, w.negative_prompt,
|
||||
w.result_url, w.thumbnail_url, w.likes_count, w.is_public, w.status, w.created_at,
|
||||
p.email AS author_email,
|
||||
p.nickname AS author_nickname,
|
||||
p.display_nickname AS author_display_nickname,
|
||||
p.avatar_url AS author_avatar_url
|
||||
FROM works w
|
||||
LEFT JOIN profiles p ON p.id = w.user_id
|
||||
${whereSql}
|
||||
${orderSql}
|
||||
LIMIT $${params.length + 1}
|
||||
OFFSET $${params.length + 2}`,
|
||||
[...params, limit, offset],
|
||||
);
|
||||
|
||||
const works = (result.rows as AdminGalleryPromptWorkRow[]).map(row => toAdminGalleryPromptWork(row));
|
||||
const total = Number(countResult.rows[0]?.total || 0);
|
||||
const nextOffset = offset + works.length;
|
||||
return NextResponse.json({
|
||||
works,
|
||||
total,
|
||||
nextOffset,
|
||||
hasMore: nextOffset < total,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[admin/gallery/works] GET error:', error);
|
||||
return NextResponse.json({ error: '加载画廊作品失败' }, { status: 500 });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user