feat(r2): 地址与设计清单完整 CRUD(事务/归属校验/状态机)
addresses: - 补 PATCH /:id、DELETE /:id(契约 §4 全路由齐备) - 默认地址唯一性事务:create/update/setDefault 先清旧默认再写入 - 首个地址自动设为默认;删除默认地址事务内补偿最新一条 - 归属校验统一:不存在 404,非本人 403 design-list: - 补 PATCH /:id、DELETE /:id、POST /batch-delete(宽松语义返回 deleted 数) - items 强制恰好 1 个元素(一条设计=一条清单,阶段0 决策#1) - designData 白名单 7 键放行、单条 ≤1MB 校验(400) - 单向状态机 DRAFT→SUBMITTED→PROCESSING→DONE,回退/跳级 400 - PATCH 部分更新语义,不传 items 不清 designData(R4 wordcloud 保护) infra: - main.ts: JSON body 上限 100KB→2MB,使契约 1MB 设计数据可达(>2MB 返回 413) - exceptions filter: body-parser entity.too.large 映射 413 - api-contract-v1.md §5 补实现说明(非契约变更) 验证:nest build 通过;本地 3091 实例 + mock 登录实测 35 项全过 (CRUD/默认地址补偿/越权 403/404/状态机/1MB 边界/413/未登录 401) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -179,6 +179,11 @@
|
||||
> 贴纸图 `src` 必须为 COS 持久 URL(禁止 `wxfile://`/`tmp`)、保留 `wordcloud` 分组、
|
||||
> 保留 `category.mask`;后端该 JSON 白名单须放行 `version/background/wordcloud/rotation/zIndex`。
|
||||
>`items` 为服务端 JSON,需做结构白名单与大小校验(单条 ≤ 1MB)。
|
||||
>
|
||||
> **实现补充(R2,非契约变更)**:服务端 JSON body 传输上限为 **2MB**(`main.ts`,Nest 默认 100KB
|
||||
> 会使 1MB 业务限制不可达)。三层边界:≤1MB 正常受理;1MB~2MB 由 design-list service 返回
|
||||
> 400「单条设计数据超过 1MB 上限」;>2MB 返回 413「请求体过大」。文件上传(R4 multipart)
|
||||
> 不走此限制,沿用各模块独立校验(如底图 ≤10MB)。
|
||||
|
||||
### PATCH /api/design-list/:id
|
||||
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentUser, JwtPayload } from '../common/decorators/current-user.decorator';
|
||||
import { AddressesService } from './addresses.service';
|
||||
import { CreateAddressDto } from './dto/create-address.dto';
|
||||
import { UpdateAddressDto } from './dto/update-address.dto';
|
||||
|
||||
@ApiTags('收货地址')
|
||||
@ApiBearerAuth()
|
||||
@@ -11,13 +20,13 @@ export class AddressesController {
|
||||
constructor(private readonly addressesService: AddressesService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '我的收货地址列表' })
|
||||
@ApiOperation({ summary: '我的收货地址列表(默认地址在前)' })
|
||||
list(@CurrentUser() user: JwtPayload) {
|
||||
return this.addressesService.listByUser(user.sub);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: '新增收货地址' })
|
||||
@ApiOperation({ summary: '新增收货地址(首个地址自动设为默认)' })
|
||||
create(@CurrentUser() user: JwtPayload, @Body() dto: CreateAddressDto) {
|
||||
return this.addressesService.create(user.sub, dto);
|
||||
}
|
||||
@@ -27,4 +36,20 @@ export class AddressesController {
|
||||
setDefault(@CurrentUser() user: JwtPayload, @Param('id') id: string) {
|
||||
return this.addressesService.setDefault(user.sub, id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: '更新本人地址(isDefault 走默认地址事务)' })
|
||||
update(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateAddressDto,
|
||||
) {
|
||||
return this.addressesService.update(user.sub, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: '删除本人地址(删默认地址时自动补偿新默认)' })
|
||||
remove(@CurrentUser() user: JwtPayload, @Param('id') id: string) {
|
||||
return this.addressesService.remove(user.sub, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Address, Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { CreateAddressDto } from './dto/create-address.dto';
|
||||
import { UpdateAddressDto } from './dto/update-address.dto';
|
||||
|
||||
@Injectable()
|
||||
export class AddressesService {
|
||||
@@ -14,12 +16,76 @@ export class AddressesService {
|
||||
}
|
||||
|
||||
async create(userId: string, dto: CreateAddressDto) {
|
||||
// TODO: 若 isDefault,需先把该用户其它地址置为非默认(事务)
|
||||
return this.prisma.address.create({ data: { ...dto, userId } });
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
// 首个地址强制为默认(列表非空时默认地址始终存在)
|
||||
const count = await tx.address.count({ where: { userId } });
|
||||
const isDefault = count === 0 ? true : (dto.isDefault ?? false);
|
||||
if (isDefault) {
|
||||
// 事务内先清旧默认再写入,保证默认地址唯一
|
||||
await tx.address.updateMany({
|
||||
where: { userId, isDefault: true },
|
||||
data: { isDefault: false },
|
||||
});
|
||||
}
|
||||
return tx.address.create({
|
||||
data: { ...dto, isDefault, userId },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async update(userId: string, id: string, dto: UpdateAddressDto) {
|
||||
const existing = await this.getOwnedAddress(userId, id);
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
if (dto.isDefault === true) {
|
||||
await tx.address.updateMany({
|
||||
where: { userId, isDefault: true },
|
||||
data: { isDefault: false },
|
||||
});
|
||||
}
|
||||
return tx.address.update({ where: { id: existing.id }, data: dto });
|
||||
});
|
||||
}
|
||||
|
||||
async setDefault(userId: string, id: string) {
|
||||
// TODO: 事务内先清旧默认再设新默认
|
||||
return this.prisma.address.update({ where: { id }, data: { isDefault: true } });
|
||||
const existing = await this.getOwnedAddress(userId, id);
|
||||
return this.prisma.$transaction(async (tx) => {
|
||||
await tx.address.updateMany({
|
||||
where: { userId, isDefault: true },
|
||||
data: { isDefault: false },
|
||||
});
|
||||
return tx.address.update({
|
||||
where: { id: existing.id },
|
||||
data: { isDefault: true },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async remove(userId: string, id: string) {
|
||||
const existing = await this.getOwnedAddress(userId, id);
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.address.delete({ where: { id: existing.id } });
|
||||
// 删除的是默认地址:补偿把最新一条设为默认,避免列表无默认
|
||||
if (existing.isDefault) {
|
||||
const next = await tx.address.findFirst({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
if (next) {
|
||||
await tx.address.update({
|
||||
where: { id: next.id },
|
||||
data: { isDefault: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 归属校验:不存在 404,存在但非本人 403(api-contract-v1 §4) */
|
||||
private async getOwnedAddress(userId: string, id: string): Promise<Address> {
|
||||
const address = await this.prisma.address.findUnique({ where: { id } });
|
||||
if (!address) throw new NotFoundException('地址不存在');
|
||||
if (address.userId !== userId) throw new ForbiddenException('无权操作该地址');
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
/** PATCH /api/addresses/:id:全字段可选,至少传一个字段才有意义 */
|
||||
export class UpdateAddressDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
phone?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
province?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
city?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
district?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
detail?: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isDefault?: boolean;
|
||||
}
|
||||
@@ -34,6 +34,14 @@ export class AllExceptionsFilter implements ExceptionFilter {
|
||||
? res
|
||||
: ((res as Record<string, unknown>).message as string | string[] | undefined)?.toString() ??
|
||||
exception.message;
|
||||
} else if (
|
||||
exception instanceof Error &&
|
||||
(exception as Error & { type?: string }).type === 'entity.too.large'
|
||||
) {
|
||||
// body-parser 请求体超限(main.ts 限制 2MB)
|
||||
status = HttpStatus.PAYLOAD_TOO_LARGE;
|
||||
code = 413;
|
||||
message = '请求体过大';
|
||||
} else if (exception instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
// 常见 Prisma 错误码映射
|
||||
switch (exception.code) {
|
||||
|
||||
@@ -1,30 +1,55 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { CurrentUser, JwtPayload } from '../common/decorators/current-user.decorator';
|
||||
import { DesignListService } from './design-list.service';
|
||||
import { BatchDeleteDesignListDto } from './dto/batch-delete-design-list.dto';
|
||||
import { CreateDesignListDto } from './dto/create-design-list.dto';
|
||||
import { UpdateDesignListDto } from './dto/update-design-list.dto';
|
||||
|
||||
@ApiTags('设计清单')
|
||||
@ApiBearerAuth()
|
||||
@Controller('design-lists')
|
||||
@Controller('design-list')
|
||||
export class DesignListController {
|
||||
constructor(private readonly designListService: DesignListService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '我的设计清单列表' })
|
||||
@ApiOperation({ summary: '我的设计清单(createdAt 倒序,不分页)' })
|
||||
list(@CurrentUser() user: JwtPayload) {
|
||||
return this.designListService.listByUser(user.sub);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '设计清单详情' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.designListService.findOne(id);
|
||||
@ApiOperation({ summary: '查询单条本人清单' })
|
||||
findOne(@CurrentUser() user: JwtPayload, @Param('id') id: string) {
|
||||
return this.designListService.findOne(user.sub, id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: '创建设计清单' })
|
||||
@ApiOperation({ summary: '创建清单(一条设计一条清单,items 固定 1 个元素)' })
|
||||
create(@CurrentUser() user: JwtPayload, @Body() dto: CreateDesignListDto) {
|
||||
return this.designListService.create(user.sub, dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: '更新本人清单(title/items/状态迁移,单向状态机)' })
|
||||
update(
|
||||
@CurrentUser() user: JwtPayload,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateDesignListDto,
|
||||
) {
|
||||
return this.designListService.update(user.sub, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: '删除本人清单' })
|
||||
remove(@CurrentUser() user: JwtPayload, @Param('id') id: string) {
|
||||
return this.designListService.remove(user.sub, id);
|
||||
}
|
||||
|
||||
@Post('batch-delete')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({ summary: '批量删除本人清单,返回实际删除数' })
|
||||
batchDelete(@CurrentUser() user: JwtPayload, @Body() dto: BatchDeleteDesignListDto) {
|
||||
return this.designListService.batchDelete(user.sub, dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,32 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { DesignList, DesignListStatus, Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { BatchDeleteDesignListDto } from './dto/batch-delete-design-list.dto';
|
||||
import { CreateDesignListDto } from './dto/create-design-list.dto';
|
||||
import { UpdateDesignListDto } from './dto/update-design-list.dto';
|
||||
|
||||
/** designData 结构白名单(wechat_wc/docs/design-data-contract-v1.md §2 冻结字段) */
|
||||
const DESIGN_DATA_ALLOWED_KEYS = new Set([
|
||||
'version',
|
||||
'category',
|
||||
'background',
|
||||
'wordcloud',
|
||||
'stickers',
|
||||
// 兼容旧版字段(契约明确保留)
|
||||
'imageSrc',
|
||||
'imagePos',
|
||||
]);
|
||||
|
||||
/** 单条设计数据上限(api-contract-v1 §5:1MB) */
|
||||
const DESIGN_DATA_MAX_BYTES = 1024 * 1024;
|
||||
|
||||
/** 状态机:单向推进(阶段0 决策#6),前端只允许提交 DRAFT→SUBMITTED */
|
||||
const STATUS_TRANSITIONS: Record<DesignListStatus, DesignListStatus[]> = {
|
||||
DRAFT: [DesignListStatus.SUBMITTED],
|
||||
SUBMITTED: [DesignListStatus.PROCESSING],
|
||||
PROCESSING: [DesignListStatus.DONE],
|
||||
DONE: [],
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class DesignListService {
|
||||
@@ -14,19 +39,89 @@ export class DesignListService {
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
// TODO: 权限校验(仅本人可查)
|
||||
return this.prisma.designList.findUnique({ where: { id } });
|
||||
async findOne(userId: string, id: string) {
|
||||
const list = await this.getOwnedList(userId, id);
|
||||
return list;
|
||||
}
|
||||
|
||||
async create(userId: string, dto: CreateDesignListDto) {
|
||||
// TODO: 校验 items 结构、关联商品 SKU
|
||||
const items = dto.items.map((it) => this.validateDesignData(it));
|
||||
return this.prisma.designList.create({
|
||||
data: {
|
||||
title: dto.title,
|
||||
items: (dto.items ?? []) as Prisma.InputJsonValue,
|
||||
items: this.toJson(items),
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async update(userId: string, id: string, dto: UpdateDesignListDto) {
|
||||
const existing = await this.getOwnedList(userId, id);
|
||||
|
||||
// 部分更新语义:只覆盖显式传入的字段,不传 items 不清 designData(R4 wordcloud 分组保护)
|
||||
const data: Prisma.DesignListUpdateInput = {};
|
||||
if (dto.title !== undefined) data.title = dto.title;
|
||||
if (dto.items !== undefined) {
|
||||
data.items = this.toJson(dto.items.map((it) => this.validateDesignData(it)));
|
||||
}
|
||||
if (dto.status !== undefined && dto.status !== existing.status) {
|
||||
this.assertTransition(existing.status, dto.status);
|
||||
data.status = dto.status;
|
||||
}
|
||||
|
||||
return this.prisma.designList.update({ where: { id: existing.id }, data });
|
||||
}
|
||||
|
||||
async remove(userId: string, id: string) {
|
||||
const existing = await this.getOwnedList(userId, id);
|
||||
await this.prisma.designList.delete({ where: { id: existing.id } });
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 批量删除:一次事务只删本人清单,返回实际删除数(部分 id 无效不报错) */
|
||||
async batchDelete(userId: string, dto: BatchDeleteDesignListDto) {
|
||||
const result = await this.prisma.designList.deleteMany({
|
||||
where: { id: { in: dto.ids }, userId },
|
||||
});
|
||||
return { deleted: result.count };
|
||||
}
|
||||
|
||||
/** DTO 实例 → 纯 JSON(剥离 class 元数据与 undefined 字段,满足 Prisma InputJsonValue) */
|
||||
private toJson(items: unknown): Prisma.InputJsonValue {
|
||||
return JSON.parse(JSON.stringify(items)) as Prisma.InputJsonValue;
|
||||
}
|
||||
|
||||
/** 归属校验:不存在 404,存在但非本人 403(与 addresses 模块一致) */
|
||||
private async getOwnedList(userId: string, id: string): Promise<DesignList> {
|
||||
const list = await this.prisma.designList.findUnique({ where: { id } });
|
||||
if (!list) throw new NotFoundException('设计清单不存在');
|
||||
if (list.userId !== userId) throw new ForbiddenException('无权操作该设计清单');
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 白名单 + 大小校验;只校验不修改业务 JSON(契约:服务端不改内容) */
|
||||
private validateDesignData<T extends { designData?: Record<string, unknown> }>(item: T): T {
|
||||
if (item.designData === undefined) return item;
|
||||
|
||||
const unknownKeys = Object.keys(item.designData).filter(
|
||||
(k) => !DESIGN_DATA_ALLOWED_KEYS.has(k),
|
||||
);
|
||||
if (unknownKeys.length > 0) {
|
||||
throw new BadRequestException(
|
||||
`designData 包含不支持的字段: ${unknownKeys.join(', ')}`,
|
||||
);
|
||||
}
|
||||
|
||||
const size = Buffer.byteLength(JSON.stringify(item.designData), 'utf8');
|
||||
if (size > DESIGN_DATA_MAX_BYTES) {
|
||||
throw new BadRequestException('单条设计数据超过 1MB 上限');
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private assertTransition(from: DesignListStatus, to: DesignListStatus) {
|
||||
if (!STATUS_TRANSITIONS[from].includes(to)) {
|
||||
throw new BadRequestException(`状态不允许从 ${from} 迁移到 ${to}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ArrayMaxSize, ArrayNotEmpty, IsArray, IsString, IsNotEmpty } from 'class-validator';
|
||||
|
||||
/** POST /api/design-list/batch-delete 请求体(api-contract-v1 §5) */
|
||||
export class BatchDeleteDesignListDto {
|
||||
@ApiProperty({ type: [String] })
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@ArrayMaxSize(50)
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
ids!: string[];
|
||||
}
|
||||
@@ -1,14 +1,64 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsArray, IsObject, IsOptional, IsString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Max,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
/**
|
||||
* 清单条目(api-contract-v1 §5)。
|
||||
* 阶段0 决策#1:一条前端 DesignItem = 一条后端 DesignList,items 固定 1 个元素;
|
||||
* 决策#3:productIcon 不入库,由客户端按 productId 推导。
|
||||
*/
|
||||
export class DesignListEntryDto {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
productId!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
productName!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
unitPrice!: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(999)
|
||||
count!: number;
|
||||
|
||||
/** 结构白名单与大小校验在 service 层做(设计数据契约 v1) */
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
designData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export class CreateDesignListDto {
|
||||
@ApiProperty({ description: '清单标题' })
|
||||
@ApiProperty({ description: '清单标题;前端传 productName(阶段0 决策#4)' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '定制项(结构化 JSON)', example: [{ sku: 'tshirt', color: 'black' }] })
|
||||
@IsOptional()
|
||||
@ApiProperty({ type: [DesignListEntryDto] })
|
||||
@IsArray()
|
||||
@IsObject({ each: true })
|
||||
items?: Record<string, unknown>[];
|
||||
@ArrayMinSize(1)
|
||||
@ArrayMaxSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DesignListEntryDto)
|
||||
items!: DesignListEntryDto[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMaxSize,
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { DesignListStatus } from '@prisma/client';
|
||||
import { DesignListEntryDto } from './create-design-list.dto';
|
||||
|
||||
export class UpdateDesignListDto {
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title?: string;
|
||||
|
||||
@ApiPropertyOptional({ type: [DesignListEntryDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayMinSize(1)
|
||||
@ArrayMaxSize(1)
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DesignListEntryDto)
|
||||
items?: DesignListEntryDto[];
|
||||
|
||||
/** 状态迁移由 service 层做单向状态机校验(DRAFT→SUBMITTED→PROCESSING→DONE) */
|
||||
@ApiPropertyOptional({ enum: DesignListStatus })
|
||||
@IsOptional()
|
||||
@IsEnum(DesignListStatus)
|
||||
@MaxLength(20)
|
||||
status?: DesignListStatus;
|
||||
}
|
||||
+6
-1
@@ -2,12 +2,17 @@ import { ValidationPipe } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import * as express from 'express';
|
||||
import { AppModule } from './app.module';
|
||||
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
|
||||
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
||||
|
||||
async function bootstrap(): Promise<void> {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
// 设计清单 designData 契约上限 1MB(api-contract-v1 §5),
|
||||
// body 限制放宽到 2MB,超 1MB 的业务校验在 design-list service 返回 400
|
||||
const app = await NestFactory.create(AppModule, { bodyParser: false });
|
||||
app.use(express.json({ limit: '2mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '2mb' }));
|
||||
const config = app.get(ConfigService);
|
||||
|
||||
app.setGlobalPrefix('api', { exclude: ['health'] });
|
||||
|
||||
Reference in New Issue
Block a user