生成记录同时包含模型内部 ID 和上游模型 ID,名称显示和筛选统一关联模型目录。图片代理处理源响应体超时与传输中断,返回明确的 504/502;浏览器取消请求同步停止上游下载。小尺寸预览允许 45 秒读取窗口,前端保留 4 路并发和可见区域加载,失败提示展示接口原因。原图下载保持字节不变。 验证:模型 ID 筛选的生产只读查询返回正确记录,浏览器夹具验证内部 ID 名称和峰值并发 4;15 项相关测试覆盖取消、响应体超时、原图字节与 ICC 保留。TypeScript 与改动文件 lint 通过。
84 lines
5.1 KiB
TypeScript
84 lines
5.1 KiB
TypeScript
import { sql, type SQL } from "drizzle-orm";
|
|
|
|
export function parseMediaQuery(params: URLSearchParams) {
|
|
const integer = (key: string, fallback: number, max: number) => {
|
|
const value = Number(params.get(key));
|
|
return Number.isInteger(value) && value > 0 ? Math.min(value, max) : fallback;
|
|
};
|
|
const date = (key: string) => {
|
|
const value = params.get(key) || "";
|
|
return /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) ? value : "";
|
|
};
|
|
return {
|
|
page: integer("page", 1, 100000),
|
|
pageSize: integer("pageSize", 40, 100),
|
|
search: (params.get("search") || "").trim().slice(0, 128),
|
|
type: ["image", "video"].includes(params.get("type") || "") ? params.get("type")! : "all",
|
|
model: params.get("model") || "",
|
|
featured: params.get("featured") === "true",
|
|
startDate: date("startDate"),
|
|
endDate: date("endDate"),
|
|
};
|
|
}
|
|
|
|
export function mediaRecordsQuery(params: ReturnType<typeof parseMediaQuery>) {
|
|
const conditions: SQL[] = [sql`true`];
|
|
if (params.search)
|
|
conditions.push(
|
|
sql`exists (select 1 from "User" u where u.id=m."userId" and (u.name ilike ${`%${params.search}%`} or u.email ilike ${`%${params.search}%`}))`,
|
|
);
|
|
if (params.startDate) conditions.push(sql`m."createdAt" >= ${params.startDate}::date`);
|
|
if (params.endDate)
|
|
conditions.push(sql`m."createdAt" < ${params.endDate}::date + interval '1 day'`);
|
|
if (params.model) conditions.push(sql`(m."modelId" = ${params.model} or m."modelId" in (select id::text from "AiModel" where "modelId"=${params.model}))`);
|
|
const where = sql.join(conditions, sql` and `);
|
|
const videoFilters: SQL[] = [sql`true`];
|
|
if (params.startDate)
|
|
videoFilters.push(sql`v."createdAt">=${params.startDate}::date - interval '8 hours'`);
|
|
if (params.endDate)
|
|
videoFilters.push(sql`v."createdAt"<${params.endDate}::date+interval '16 hours'`);
|
|
if (params.model) videoFilters.push(sql`(v."video_metadata"->>'model'=${params.model} or v."video_metadata"->>'model' in (select id::text from "AiModel" where "modelId"=${params.model}))`);
|
|
if (params.search)
|
|
videoFilters.push(
|
|
sql`exists (select 1 from "User" u where u.id=c."userId" and (u.name ilike ${`%${params.search}%`} or u.email ilike ${`%${params.search}%`}))`,
|
|
);
|
|
return sql`
|
|
with video_candidates as materialized (
|
|
select v.id, v."chatId", v."createdAt" + interval '8 hours' as "createdAt", v."video_metadata", c."userId",
|
|
coalesce(nullif(v."permanent_video_url", ''), nullif(v."temporary_video_url", ''),
|
|
(regexp_match(t.body, '<video[^>]+src="([^"]+)"', 'i'))[1],
|
|
(regexp_match(t.body, '(https?://[^[:space:]"<>]+\\.mp4[^[:space:]"<>)]*)', 'i'))[1]) as url
|
|
from "Message_v2" v join "Chat" c on c.id=v."chatId"
|
|
cross join lateral (select case when jsonb_typeof(v.parts::jsonb)='array'
|
|
then (select string_agg(p->>'text', E'\\n') from jsonb_array_elements(v.parts::jsonb) p)
|
|
else v.parts::jsonb #>> '{}' end as body) t
|
|
where ${params.type !== "image" && !params.featured} and v.role='assistant'
|
|
and ${sql.join(videoFilters, sql` and `)}
|
|
and (v."permanent_video_url" is not null or v."temporary_video_url" is not null or v.parts::text ilike '%<video%' or v.parts::text ilike '%.mp4%')
|
|
), media as not materialized (
|
|
select g.id, 'image'::text as kind, g."createdAt", g."userId", g."modelId", null::text as url
|
|
from "GeneratedImage" g where ${params.type !== "video"} and (${!params.featured} or g."isFeatured")
|
|
union all
|
|
select id, 'video', "createdAt", "userId", "video_metadata"->>'model', url
|
|
from video_candidates where url is not null
|
|
), filtered as not materialized (select * from media m where ${where}),
|
|
totals as (select count(*)::int as total, count(*) filter(where kind='image')::int as images, count(*) filter(where kind='video')::int as videos from filtered),
|
|
page_rows as (
|
|
select * from filtered order by "createdAt" desc, kind, id desc
|
|
limit ${params.pageSize} offset ${(params.page - 1) * params.pageSize}
|
|
)
|
|
select t.total, t.images as "totalImages", t.videos as "totalVideos",
|
|
coalesce((select jsonb_agg(jsonb_build_object(
|
|
'id', case when p.kind='video' then 'video-'||p.id::text else p.id::text end,
|
|
'recordType',p.kind,'userId',p."userId",'userName',u.name,'userEmail',u.email,'userDepartment',u.department,
|
|
'createdAt',p."createdAt",'modelId',p."modelId",'chatId',coalesce(g."chatId",v."chatId"),
|
|
'thumbnailUrl',coalesce(p.url,g."thumbnailUrl"),'originalUrl',coalesce(p.url,g."originalUrl"),'videoUrl',p.url,
|
|
'prompt',coalesce(g.prompt,v."video_metadata"->>'prompt'),'imageSize',coalesce(g."imageSize",v."video_metadata"->>'resolution'),
|
|
'isFeatured',coalesce(g."isFeatured",false),'featuredCategory',g."featuredCategory",'featuredAt',g."featuredAt" + interval '8 hours'
|
|
) order by p."createdAt" desc,p.kind,p.id desc)
|
|
from page_rows p left join "User" u on u.id=p."userId"
|
|
left join "GeneratedImage" g on p.kind='image' and g.id=p.id
|
|
left join "Message_v2" v on p.kind='video' and v.id=p.id),'[]'::jsonb) as records
|
|
from totals t`;
|
|
}
|