feat(assets): album asset status — show current banner/cover/logo in admin branding tab
- 契约 listAlbumAssetStatus(GET /albums/{albumId}/assets):每 (kind,locale)
最新 effective 行 + 现签 URL;权限同详情(可见即读);deleted → 404
- 服务端 AssetsService.status(createdAt,id DESC 组内取首;presignGet 现签)
- Admin 品牌资产页签改造:进入即加载现状——三种 kind 卡片(预览图/未设置
空态/设置时间),banner 分全局/zh/en 槽位;上传成功自动刷新(替换语义)
- API 56/56、admin 87/87 + build 全绿
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,13 @@ import {
|
||||
Controller,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import type { components } from '@photo/contracts';
|
||||
import { AuthenticatedRequest, SessionGuard } from '../auth/session.guard';
|
||||
import { userOf } from './albums.controller';
|
||||
import { AssetInitRequestDto } from './dto';
|
||||
@@ -29,6 +31,14 @@ export class AssetsController {
|
||||
constructor(private readonly assetsService: AssetsService) {}
|
||||
|
||||
/** 契约 201:会话行已建(每次 init 新行新键,无幂等键)。 */
|
||||
@Get()
|
||||
listStatus(
|
||||
@Param('albumId') albumId: string,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
): Promise<components['schemas']['AssetStatusResponse']> {
|
||||
return this.assetsService.status(userOf(req), albumId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
initAlbumAssetUpload(
|
||||
@Param('albumId') albumId: string,
|
||||
|
||||
@@ -63,6 +63,50 @@ export class AssetsService {
|
||||
@Inject(STORAGE_PORT) private readonly storage: StoragePort,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 各 (kind, locale) 组合最新生效资产(契约 listAlbumAssetStatus):管理端
|
||||
* 现状展示。权限同相册详情(可见即可读);deleted → 404。签名现签
|
||||
* (SigV4 本地计算);同组取 createdAt,id DESC 首行(替换语义)。
|
||||
*/
|
||||
async status(
|
||||
user: AuthUser,
|
||||
albumId: string,
|
||||
): Promise<components['schemas']['AssetStatusResponse']> {
|
||||
if (!(await this.permissions.canViewAlbum(user, albumId))) {
|
||||
throw albumNotFound();
|
||||
}
|
||||
const album = await this.prisma.albums.findUnique({
|
||||
where: { id: albumId },
|
||||
select: { id: true, status: true },
|
||||
});
|
||||
if (album === null || album.status === 'deleted') {
|
||||
throw albumNotFound();
|
||||
}
|
||||
const rows = await this.prisma.album_assets.findMany({
|
||||
where: { albumId, state: 'effective' },
|
||||
orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
|
||||
});
|
||||
// 每 (kind, locale|null) 组合取首行(排序已保证最新在前)
|
||||
const seen = new Set<string>();
|
||||
const ttl = signedUrlTtlSeconds();
|
||||
const items: components['schemas']['AssetStatusItem'][] = [];
|
||||
for (const row of rows) {
|
||||
const key = `${row.kind}|${row.locale ?? ''}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
const url = await this.storage.presignGet(row.objectKey, ttl);
|
||||
items.push({
|
||||
kind: row.kind as components['schemas']['AssetStatusItem']['kind'],
|
||||
locale: (row.locale ?? null) as string | null,
|
||||
url,
|
||||
expiresAt: new Date(Date.now() + ttl * 1000).toISOString(),
|
||||
contentType: row.contentType,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
});
|
||||
}
|
||||
return { items };
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化:权限/参数校验 → 建行(state=pending)→ presign。
|
||||
* 每次 init 都是新行新键——无幂等裁决,天然无并发冲突锚。
|
||||
|
||||
Reference in New Issue
Block a user