feat(api): listUploads 会话条目内嵌照片预览(现签 thumb/browse)
- 契约 UploadSessionPhoto 增可选 preview(PhotoPreview;旧客户端零 break) - list() 批量查 photo_variants,uploader 视角签发:thumb+browse 齐备且 未隐藏/删除才签(「隐藏是运营动作」口径从严;无变体→字段缺省) - presign 本地 SigV4 无网络开销,TTL 同全局 900s - 顺手修 batch-ops ⑦:交接后上传 bytes 撞查重改 71007(登记旧账清偿) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -511,6 +511,18 @@ export class UploadsService {
|
||||
});
|
||||
const photoById = new Map(photos.map((photo) => [photo.id, photo]));
|
||||
|
||||
// 预览变体批量查询(会话历史缩略图,2026-09-29):thumb+browse 齐备才签。
|
||||
const variantRows =
|
||||
page.length === 0
|
||||
? []
|
||||
: await this.prisma.photo_variants.findMany({
|
||||
where: { photoId: { in: page.map((row) => row.photoId) } },
|
||||
select: { photoId: true, variant: true, objectKey: true },
|
||||
});
|
||||
const variantKeyOf = new Map(
|
||||
variantRows.map((v) => [`${v.photoId}:${v.variant}`, v.objectKey]),
|
||||
);
|
||||
|
||||
const last = page.at(-1);
|
||||
const nextCursor =
|
||||
hasMore && last !== undefined
|
||||
@@ -520,31 +532,60 @@ export class UploadsService {
|
||||
})
|
||||
: null;
|
||||
return {
|
||||
items: page.map((row) => {
|
||||
const photo = photoById.get(row.photoId);
|
||||
const record: UploadSessionRecord = {
|
||||
uploadId: row.id,
|
||||
albumId: row.albumId,
|
||||
clientTaskId: row.clientTaskId,
|
||||
state: row.state as UploadSessionRecord['state'],
|
||||
expectedBytes: row.expectedBytes,
|
||||
contentType: row.contentType,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
sourceDeviceId: row.sourceDeviceId,
|
||||
photo: {
|
||||
photoId: row.photoId,
|
||||
processingState:
|
||||
(photo?.processingState ?? 'pending_upload') as UploadSessionRecord['photo']['processingState'],
|
||||
visibility: (photo?.visibility ?? 'unpublished') as UploadSessionRecord['photo']['visibility'],
|
||||
},
|
||||
};
|
||||
return record;
|
||||
}),
|
||||
items: await Promise.all(
|
||||
page.map(async (row) => {
|
||||
const photo = photoById.get(row.photoId);
|
||||
const record: UploadSessionRecord = {
|
||||
uploadId: row.id,
|
||||
albumId: row.albumId,
|
||||
clientTaskId: row.clientTaskId,
|
||||
state: row.state as UploadSessionRecord['state'],
|
||||
expectedBytes: row.expectedBytes,
|
||||
contentType: row.contentType,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
sourceDeviceId: row.sourceDeviceId,
|
||||
photo: {
|
||||
photoId: row.photoId,
|
||||
processingState:
|
||||
(photo?.processingState ?? 'pending_upload') as UploadSessionRecord['photo']['processingState'],
|
||||
visibility: (photo?.visibility ?? 'unpublished') as UploadSessionRecord['photo']['visibility'],
|
||||
},
|
||||
};
|
||||
// 预览签发(uploader 本人视角):thumb/browse 变体齐备且未隐藏/删除
|
||||
// 才签(对齐「隐藏是运营动作」;管理端 manager 可签的口径在此从严——
|
||||
// 避免按相册批量鉴权扇出)。处理未完成(无变体)→ 字段缺省,App 回退
|
||||
// 占位图标。SigV4 本地计算,无网络副作用。
|
||||
const thumbKey = variantKeyOf.get(`${row.photoId}:thumb`);
|
||||
const browseKey = variantKeyOf.get(`${row.photoId}:browse`);
|
||||
if (
|
||||
thumbKey !== undefined &&
|
||||
browseKey !== undefined &&
|
||||
photo?.visibility !== 'hidden' &&
|
||||
photo?.visibility !== 'deleted'
|
||||
) {
|
||||
record.photo.preview = {
|
||||
thumb: await this.presignVariantUrl(thumbKey),
|
||||
browse: await this.presignVariantUrl(browseKey),
|
||||
};
|
||||
}
|
||||
return record;
|
||||
}),
|
||||
),
|
||||
nextCursor,
|
||||
hasMore: nextCursor !== null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 现签变体短期 GET 凭证(TTL 惰性读取;同 photos.service 预览模式)。 */
|
||||
private async presignVariantUrl(objectKey: string): Promise<{
|
||||
url: string;
|
||||
expiresAt: string;
|
||||
}> {
|
||||
const ttl = signedUrlTtlSeconds();
|
||||
const url = await this.storage.presignGet(objectKey, ttl);
|
||||
return { url, expiresAt: new Date(Date.now() + ttl * 1000).toISOString() };
|
||||
}
|
||||
|
||||
/**
|
||||
* 查重预检(「上传查重前置到入队前」Task 1):只读批量——用户点上传(确认
|
||||
* 入队)时刻预判服务端历史重复,跨设备/重装/改名均可查到。判定谓词与 init
|
||||
|
||||
Reference in New Issue
Block a user