feat(api): members and categories management

成员(/v1/albums/{albumId}/members):owner/管理员邀请已开通同事为协作者
(重复 409 MEMBER_CONFLICT、owner 目标 400、未开通/停用账号 400)、成员列表
((addedAt,id) DESC 键集分页)、移除协作者(幂等 204;成员行是唯一权限来源,
删除行即失去可见性 A02;owner 不可移除 403)。分类(/albums/{albumId}/categories
与 /categories/{categoryId}):创建/部分更新/删除,translations 全量替换,
相册详情按 sortOrder 升序带出;404 统一 ALBUM_NOT_FOUND(分类不存在与相册
不可见同码)。审计 album.member.add/remove、album.category.add/update/remove
(resourceId=albumId,metadata 记目标 id/字段)。共用错误/P2002 判定抽至
support.ts(修复 meta.target 为 DB 约束名时的误判)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-17 22:16:16 +08:00
parent 1faefa4227
commit e04e1f3bdf
9 changed files with 1360 additions and 68 deletions
@@ -0,0 +1,68 @@
// services/api/src/albums/members.controller.ts —— /albums/{albumId}/members
// 端点(Task 17)。方法名与契约 operationId 一一对应(addAlbumMember/
// listAlbumMembers/removeAlbumMember)。控制器级 SessionGuard:三端点全需
// 认证。控制器只做认证用户透传 + 查询参数规整(复用 albums.controller 的
// parseLimit/parseCursor,同一契约 bounds/游标语义);权限与 HTTP 映射由
// MembersService 抛 ApiException(404 ALBUM_NOT_FOUND / 403 AUTH_FORBIDDEN /
// 409 MEMBER_CONFLICT)。
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { AuthenticatedRequest, SessionGuard } from '../auth/session.guard';
import { parseCursor, parseLimit, userOf } from './albums.controller';
import { MemberAddRequestDto } from './dto';
import {
type MemberListResponse,
type MemberResponse,
MembersService,
} from './members.service';
@Controller('albums/:albumId/members')
@UseGuards(SessionGuard)
export class MembersController {
constructor(private readonly membersService: MembersService) {}
@Post()
addAlbumMember(
@Param('albumId') albumId: string,
@Body() dto: MemberAddRequestDto,
@Req() req: AuthenticatedRequest,
): Promise<MemberResponse> {
return this.membersService.add(userOf(req), albumId, dto);
}
@Get()
listAlbumMembers(
@Param('albumId') albumId: string,
@Query('cursor') cursor: string | undefined,
@Query('limit') limit: string | undefined,
@Req() req: AuthenticatedRequest,
): Promise<MemberListResponse> {
return this.membersService.list(
userOf(req),
albumId,
parseCursor(cursor),
parseLimit(limit),
);
}
@Delete(':userId')
@HttpCode(HttpStatus.NO_CONTENT)
removeAlbumMember(
@Param('albumId') albumId: string,
@Param('userId') userId: string,
@Req() req: AuthenticatedRequest,
): Promise<void> {
return this.membersService.remove(userOf(req), albumId, userId);
}
}