feat: complete V1 phases 0-7

This commit is contained in:
2026-08-12 14:35:12 +08:00
commit c3dd9be674
311 changed files with 38750 additions and 0 deletions
@@ -0,0 +1,169 @@
import { Type } from 'class-transformer';
import { OmitType } from '@nestjs/swagger';
import {
ArrayMaxSize,
IsArray,
IsBoolean,
IsIn,
IsInt,
IsISO8601,
IsOptional,
IsString,
IsUUID,
Length,
Matches,
Max,
MaxLength,
Min,
} from 'class-validator';
import {
ACTIVITY_STATUSES,
CHECK_IN_STATUSES,
PROJECT_STATUSES,
REGISTRATION_STATUSES,
SERVICE_CATEGORIES,
VOLUNTEER_SKILL_CODES,
VOLUNTEER_STATUSES,
} from '@lehuo/contracts';
import { PaginationDto } from '../../case-management/dto/case-management.dto';
const MOBILE_PATTERN = /^1[3-9]\d{9}$/;
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
export class ApplyVolunteerDto {
@IsUUID('4') clientRequestId!: string;
@IsOptional() @Type(() => Number) @IsInt() @Min(1) expectedRevision?: number;
@IsString() @Length(2, 50) realName!: string;
@Matches(MOBILE_PATTERN) mobile!: string;
@IsString() @Length(2, 100) regionCode!: string;
@IsString() @Length(2, 500) availabilityNote!: string;
@IsArray() @ArrayMaxSize(12) @IsIn(VOLUNTEER_SKILL_CODES, { each: true }) skillCodes!: string[];
@IsArray()
@ArrayMaxSize(12)
@IsIn(SERVICE_CATEGORIES, { each: true })
interestCategories!: string[];
@IsIn([true]) privacyConsent!: true;
}
export class VolunteerQueryDto extends PaginationDto {
@IsOptional() @IsIn(VOLUNTEER_STATUSES) status?: string;
@IsOptional() @IsString() @MaxLength(100) regionCode?: string;
}
export class ReviewVolunteerDto {
@IsUUID('4') clientRequestId!: string;
@IsIn(['APPROVED', 'REJECTED', 'SUSPENDED']) targetStatus!: 'APPROVED' | 'REJECTED' | 'SUSPENDED';
@IsString() @Length(2, 500) note!: string;
}
export class RestoreVolunteerDto {
@IsUUID('4') clientRequestId!: string;
@IsIn(['APPROVED']) targetStatus!: 'APPROVED';
@IsString() @Length(2, 500) note!: string;
}
export class ProjectQueryDto extends PaginationDto {
@IsOptional() @IsIn(PROJECT_STATUSES) status?: string;
@IsOptional() @IsIn(SERVICE_CATEGORIES) category?: string;
}
export class PublicProjectQueryDto extends PaginationDto {
@IsOptional() @IsIn(SERVICE_CATEGORIES) category?: string;
}
export class CreateProjectDto {
@IsUUID('4') clientRequestId!: string;
@IsString() @Length(2, 200) name!: string;
@IsIn(SERVICE_CATEGORIES) category!: string;
@IsString() @Length(10, 500) summary!: string;
@IsString() @Length(20, 20000) content!: string;
@IsOptional() @IsUUID('4') coverFilePublicId?: string;
@IsUUID('4') managerUserPublicId!: string;
@Matches(DATE_PATTERN) startDate!: string;
@Matches(DATE_PATTERN) endDate!: string;
@IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(1_000_000) targetPeople?: number;
@IsBoolean() isPublic!: boolean;
@Type(() => Number) @IsInt() @Min(-10_000) @Max(10_000) sortOrder!: number;
}
export class UpdateProjectDto extends OmitType(CreateProjectDto, ['clientRequestId'] as const) {
@Type(() => Number) @IsInt() @Min(1) expectedRevision!: number;
}
export class TransitionProjectDto {
@IsUUID('4') clientRequestId!: string;
@IsIn(['PUBLISHED', 'COMPLETED']) targetStatus!: 'PUBLISHED' | 'COMPLETED';
}
export class ReplaceProjectCasesDto {
@IsArray() @ArrayMaxSize(500) @IsUUID('4', { each: true }) serviceCasePublicIds!: string[];
}
export class ActivityQueryDto extends PaginationDto {
@IsOptional() @IsIn(ACTIVITY_STATUSES) status?: string;
@IsOptional() @IsUUID('4') projectPublicId?: string;
}
export class PublicActivityQueryDto extends PaginationDto {
@IsOptional() @IsUUID('4') projectPublicId?: string;
}
export class CreateActivityDto {
@IsUUID('4') clientRequestId!: string;
@IsOptional() @IsUUID('4') projectPublicId?: string;
@IsString() @Length(2, 200) title!: string;
@IsString() @Length(10, 500) summary!: string;
@IsString() @Length(20, 20000) content!: string;
@IsOptional() @IsUUID('4') coverFilePublicId?: string;
@IsISO8601({ strict: true }) startAt!: string;
@IsISO8601({ strict: true }) endAt!: string;
@IsISO8601({ strict: true }) registrationDeadline!: string;
@IsString() @Length(2, 300) locationText!: string;
@IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100_000) capacity?: number;
}
export class UpdateActivityDto extends OmitType(CreateActivityDto, ['clientRequestId'] as const) {
@Type(() => Number) @IsInt() @Min(1) expectedRevision!: number;
}
export class TransitionActivityDto {
@IsUUID('4') clientRequestId!: string;
@IsIn(ACTIVITY_STATUSES.filter((status) => status !== 'DRAFT')) targetStatus!: string;
}
export class RegisterActivityDto {
@IsUUID('4') clientRequestId!: string;
}
export class RegistrationQueryDto extends PaginationDto {
@IsOptional() @IsIn(REGISTRATION_STATUSES) status?: string;
@IsOptional() @IsUUID('4') activityPublicId?: string;
}
export class ReviewRegistrationDto {
@IsUUID('4') clientRequestId!: string;
@IsIn(['APPROVED', 'REJECTED']) targetStatus!: 'APPROVED' | 'REJECTED';
@IsString() @Length(2, 500) note!: string;
}
export class CheckInRegistrationDto {
@IsUUID('4') clientRequestId!: string;
@IsIn(CHECK_IN_STATUSES.filter((status) => status !== 'NOT_CHECKED_IN')) targetStatus!:
'CHECKED_IN' | 'ABSENT';
}
export class RecordServiceMinutesDto {
@IsUUID('4') clientRequestId!: string;
@Type(() => Number) @IsInt() @Min(1) @Max(100_800) serviceMinutes!: number;
}
export class CreateMediaUploadIntentDto {
@IsUUID('4') clientRequestId!: string;
@IsIn(['PROJECT_COVER', 'ACTIVITY_COVER']) businessType!: 'PROJECT_COVER' | 'ACTIVITY_COVER';
@IsString() @Length(1, 255) filename!: string;
@IsIn(['image/jpeg', 'image/png', 'image/webp']) mimeType!: string;
@Type(() => Number) @IsInt() @Min(1) @Max(10 * 1024 * 1024) size!: number;
@IsOptional() @Matches(/^[a-fA-F0-9]{64}$/) sha256?: string;
}