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:
2026-09-12 02:37:53 +08:00
co-authored by Claude
parent 01a1891988
commit 0a3c8e834d
11 changed files with 406 additions and 29 deletions
+56 -6
View File
@@ -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 = 一条后端 DesignListitems 固定 1 个元素;
* 决策#3productIcon 不入库,由客户端按 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[];
}