feat(api): batch hide with per-item results and owner transfer
P4a Task 2(A16/A17):
- POST /albums/{albumId}/photos/hide-batch:逐项结果(部分成功)——
不存在/他册 → ok:false PHOTO_NOT_FOUND;非 published(unpublished/
deleted)→ ok:false PHOTO_NOT_PUBLIC;已 hidden → ok:true 幂等 no-op
(同值不产生第二次事件/审计/版本,P1 语义);published → 逐照片事务
(条件更新 + outbox photo.hidden 高优先级 + audit photo.hide)。
results 与请求同序;权限同管理照片列表(仅创建者或管理员,404 防枚举);
单批 1..200(DTO 层 400)。
- POST /admin/albums/{albumId}/transfer:admin-only owner 移交——事务内
条件更新 ownerId+entityVersion+1(乐观重试)+ 成员互换(目标 upsert 为
owner,原 owner 按 keepAsCollaborator 降级 collaborator 或删行)+
outbox album.updated(标准载荷,版本推进)+ audit album.transfer
{from,to,kept}。目标校验同成员邀请资格谓词(active/未锁定/未停用);
自交接 400;photos.uploaderId 不动(A17 历史上传人不变)。
- e2e(batch-ops.e2e-spec,双入口 harness):8 用例覆盖逐项结果/幂等/
入参权限矩阵/ov 传播(真实发布链→隐藏→photos_ov=hidden+公开列表剔除)/
keep 两分支/目标校验/交接后权限(新 owner 管理、旧 owner 保留协作者
可上传)。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
// services/api/src/photos/album-photos.controller.ts —— /albums/{albumId}/photos
|
||||
// 管理列表端点(P1b Task 4)。方法名与契约 operationId 一一对应
|
||||
// (listAlbumPhotos,tag [photos]——路由挂在相册子路径下,域仍属照片管理)。
|
||||
// 控制器级 SessionGuard:端点需认证。控制器只做认证用户透传 + 查询参数规整
|
||||
// (visibility 本域 dto / categoryId 空串归一 / cursor+limit 走 shared/pagination,
|
||||
// 同契约 bounds);权限与 HTTP 映射由 PhotosService 抛 ApiException
|
||||
// (404 ALBUM_NOT_FOUND / 403 AUTH_FORBIDDEN / 400 VALIDATION_FAILED)。
|
||||
// 管理列表端点(P1b Task 4)+ 批量隐藏(P4a Task 2)。方法名与契约
|
||||
// operationId 一一对应(listAlbumPhotos/hidePhotosBatch,tag [photos]——路由
|
||||
// 挂在相册子路径下,域仍属照片管理)。控制器级 SessionGuard:端点需认证。
|
||||
// 控制器只做认证用户透传 + 查询参数规整(visibility 本域 dto / categoryId
|
||||
// 空串归一 / cursor+limit 走 shared/pagination,同契约 bounds);权限与 HTTP
|
||||
// 映射由 PhotosService 抛 ApiException(404 ALBUM_NOT_FOUND / 403
|
||||
// AUTH_FORBIDDEN / 400 VALIDATION_FAILED)。注意 DTO 值导入(非 import type):
|
||||
// @Body 参数类型经 emitDecoratorMetadata 进入设计期元数据。
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
@@ -16,8 +22,12 @@ import {
|
||||
import { userOf } from '../albums/albums.controller';
|
||||
import { AuthenticatedRequest, SessionGuard } from '../auth/session.guard';
|
||||
import { parseCursor, parseLimit } from '../shared/pagination';
|
||||
import { parseVisibilityFilter } from './dto';
|
||||
import { PhotosService, type AdminPhotoListResponse } from './photos.service';
|
||||
import { PhotoHideBatchDto, parseVisibilityFilter } from './dto';
|
||||
import {
|
||||
PhotosService,
|
||||
type AdminPhotoListResponse,
|
||||
type BatchHideResult,
|
||||
} from './photos.service';
|
||||
|
||||
@Controller('albums/:albumId/photos')
|
||||
@UseGuards(SessionGuard)
|
||||
@@ -45,4 +55,18 @@ export class AlbumPhotosController {
|
||||
limit: parseLimit(limit),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量隐藏(A16):逐项结果(部分成功);单批 1..200(DTO 层 400)。
|
||||
* 契约 200(非 201:批量结果不是新建资源的表述)。
|
||||
*/
|
||||
@Post('hide-batch')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
hidePhotosBatch(
|
||||
@Param('albumId') albumId: string,
|
||||
@Body() dto: PhotoHideBatchDto,
|
||||
@Req() req: AuthenticatedRequest,
|
||||
): Promise<BatchHideResult> {
|
||||
return this.photosService.hideBatch(userOf(req), albumId, dto.photoIds);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user