WuYongJie c431a574c8 避免同名模型导致精选图片重复和计数偏差
同一 modelId 配置多个上游时,精选接口的模型关联会把一张图片返回多次,导致数量和分页偏大。改为为每张图片选择一个模型名称,保证图片记录和总数一致。

验证:生产只读数据包含同名模型两条,修正查询返回 6 个不同图片 ID、总数 6;TypeScript 检查通过。
2026-11-03 13:51:58 +08:00

107 lines
3.6 KiB
TypeScript

import { NextRequest } from "next/server";
import { db } from "@/lib/db";
import { generatedImage, user, aiModel } from "@/lib/db/schema";
import { and, eq, desc, sql } from "drizzle-orm";
import { isRetiredStorageUrl, RETIRED_STORAGE_URL_PATTERN } from "@/lib/storage/retired-storage";
import { cachedPublicJson } from "@/lib/public-api/performance";
const FEATURED_TIMEOUT_MS = 1800;
const FEATURED_DEFAULT_PAGE_SIZE = 24;
const FEATURED_MAX_PAGE_SIZE = 48;
function clampPositiveInt(value: string | null, fallback: number, max: number) {
const parsed = Number.parseInt(value || "", 10);
if (!Number.isFinite(parsed) || parsed < 1) {
return fallback;
}
return Math.min(parsed, max);
}
// GET: 获取精选图片列表
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = clampPositiveInt(searchParams.get("page"), 1, 500);
const pageSize = clampPositiveInt(
searchParams.get("pageSize"),
FEATURED_DEFAULT_PAGE_SIZE,
FEATURED_MAX_PAGE_SIZE
);
const offset = (page - 1) * pageSize;
const fallbackData = {
images: [],
pagination: {
page,
pageSize,
total: 0,
totalPages: 0,
},
};
return cachedPublicJson({
cacheKey: `featured:${page}:${pageSize}`,
fallbackData,
timeoutMs: FEATURED_TIMEOUT_MS,
load: async () => {
// 查询精选图片,关联用户和模型信息
const featuredImages = await db
.select({
id: generatedImage.id,
thumbnailUrl: generatedImage.thumbnailUrl,
originalUrl: generatedImage.originalUrl,
imageSize: generatedImage.imageSize,
prompt: generatedImage.prompt,
modelId: generatedImage.modelId,
featuredCategory: generatedImage.featuredCategory,
createdAt: generatedImage.createdAt,
featuredAt: generatedImage.featuredAt,
userId: generatedImage.userId,
userName: user.name,
userEmail: user.email,
userDepartment: user.department,
modelName: sql<string | null>`(select ${aiModel.displayName} from ${aiModel}
where ${aiModel.modelId} = ${generatedImage.modelId}
order by ${aiModel.isEnabled} desc, ${aiModel.createdAt} desc, ${aiModel.id} asc limit 1)`,
totalCount: sql<number>`count(*) over()::int`,
})
.from(generatedImage)
.leftJoin(user, eq(generatedImage.userId, user.id))
.where(and(
eq(generatedImage.isFeatured, true),
sql`${generatedImage.originalUrl} !~* ${RETIRED_STORAGE_URL_PATTERN}`,
))
.orderBy(desc(generatedImage.featuredAt))
.limit(pageSize)
.offset(offset);
const total = featuredImages[0]?.totalCount || 0;
const totalPages = Math.ceil(total / pageSize);
return {
images: featuredImages.map((img) => ({
id: img.id,
thumbnailUrl: isRetiredStorageUrl(img.thumbnailUrl) ? img.originalUrl : img.thumbnailUrl,
originalUrl: img.originalUrl,
imageSize: img.imageSize,
prompt: img.prompt,
modelId: img.modelId,
modelName: img.modelName || img.modelId || "未知模型",
featuredCategory: img.featuredCategory,
createdAt: img.createdAt,
featuredAt: img.featuredAt,
user: {
id: img.userId,
name: img.userName || img.userEmail?.split("@")[0] || "未知用户",
department: img.userDepartment || "未设置部门",
},
})),
pagination: {
page,
pageSize,
total,
totalPages,
},
};
},
});
}