feat: 完成客户拜访模块全功能开发

本次提交实现了完整的客户拜访业务系统:
1.  新增对象存储上传服务,支持S3兼容协议的RustFS存储
2.  完善客户拜访实体与数据库表结构,补充全业务字段
3.  开发拜访台账与记录的前后端接口,包含增删改查、批量创建、状态流转等能力
4.  新增拜访表单页面与路由,支持父级菜单补全面包屑
5.  新增勘察列表页预填客户ID跳转逻辑
6.  补充UI设计文档与页面截图
7.  调整日志中间件跳过上传路由,优化RBAC权限配置
This commit is contained in:
andyesfly committed 2026-09-14 11:42:10 +08:00
1 parent 68af363ca9
commit 1c83e09fa1
33 files changed
+6920 -587

No files matched your search

+68 -10
View File
@@ -1,4 +1,16 @@
import { IsNotEmpty, IsOptional, IsString, IsInt, IsNumber, Matches, Min, Max, MaxLength } from "class-validator";
import {
IsNotEmpty,
IsOptional,
IsString,
IsInt,
IsArray,
IsDateString,
ArrayNotEmpty,
Matches,
Min,
Max,
MaxLength
} from "class-validator";
export class CustomerVisitQueryDTO {
@IsOptional() @IsInt() @Min(1) page?: number = 1;
@@ -6,24 +18,70 @@ export class CustomerVisitQueryDTO {
@IsOptional() @IsString() @MaxLength(64) keyword?: string;
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsInt() salesmanId?: number;
@IsOptional() @IsInt() @Min(1) @Max(5) visitType?: number;
@IsOptional() @IsInt() @Min(0) @Max(5) status?: number;
@IsOptional() @IsInt() @Min(1) @Max(3) priority?: number;
// 计划拜访日期窗口(YYYY-MM-DD)
@IsOptional() @IsString() @MaxLength(10) planStartDate?: string;
@IsOptional() @IsString() @MaxLength(10) planEndDate?: string;
}
export class CustomerVisitCreateDTO {
@IsNotEmpty({ message: '所属客户不能为空' }) @IsInt() customerId!: number;
@IsNotEmpty({ message: '拜访人不能为空' }) @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitPerson!: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitPerson?: string;
@IsOptional() @Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' }) visitPhone?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) visitContent?: string;
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
@IsOptional() @IsInt() salesmanId?: number;
@IsOptional() @IsInt() @Min(0) @Max(1) willPlan?: number;
@IsOptional() @IsInt() @Min(1) @Max(5) visitType?: number;
@IsOptional() @IsInt() @Min(1) @Max(4) visitMode?: number;
@IsOptional() @IsInt() @Min(1) @Max(3) priority?: number;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) subject?: string;
@IsOptional() @IsDateString() planTime?: string;
@IsOptional() @IsDateString() planEndTime?: string;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) spaceDesc?: string;
@IsOptional() @IsInt() @Min(1) @Max(3) intentLevel?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) intentDesc?: string;
@IsOptional() @IsString() @MaxLength(512) @Matches(/^[^<>]*$/) demands?: string;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) companion?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) receptionPerson?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) receptionTitle?: string;
@IsOptional() @Matches(/^1[3-9]\d{9}$/, { message: '接待人联系方式格式不正确' }) receptionPhone?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) resultRating?: string;
@IsOptional() @IsInt() @Min(0) @Max(5) status?: number;
@IsOptional() @IsString() @MaxLength(10) nextVisitTime?: string;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) nextAction?: string;
// 现场照片对象 key 列表(对象存储),最多 2 张
@IsOptional() @IsArray() @MaxLength(2) photos?: string[];
}
export class CustomerVisitUpdateDTO {
@IsOptional() @IsInt() customerId?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) visitPerson?: string;
@IsOptional() @Matches(/^1[3-9]\d{9}$/) visitPhone?: string;
@IsOptional() @IsString() @MaxLength(2000) @Matches(/^[^<>]*$/) visitContent?: string;
@IsOptional() @IsString() @MaxLength(10) visitTime?: string;
export class CustomerVisitUpdateDTO extends CustomerVisitCreateDTO {
@IsOptional() @IsInt() declare customerId: number;
@IsOptional() @IsInt() @Min(0) @Max(5) declare status?: number;
@IsOptional() @IsInt() @Min(0) @Max(1) declare checkinStatus?: number;
@IsOptional() @IsDateString() declare checkinTime?: string;
@IsOptional() declare checkinLng?: number;
@IsOptional() declare checkinLat?: number;
}
// 批量安排拜访:多选客户 + 统一计划信息,批量生成待执行(status=0)记录
export class CustomerVisitBatchCreateDTO {
@IsArray() @ArrayNotEmpty({ message: '请至少选择一个客户' }) @IsInt({ each: true }) customerIds!: number[];
@IsNotEmpty({ message: '计划拜访时间不能为空' }) @IsDateString() planTime!: string;
@IsOptional() @IsDateString() planEndTime?: string;
@IsOptional() @IsInt() salesmanId?: number;
@IsOptional() @IsInt() @Min(0) @Max(1) willPlan?: number;
}
@IsOptional() @IsInt() @Min(1) @Max(5) visitType?: number;
@IsOptional() @IsInt() @Min(1) @Max(4) visitMode?: number;
@IsOptional() @IsInt() @Min(1) @Max(3) priority?: number;
@IsOptional() @IsString() @MaxLength(128) @Matches(/^[^<>]*$/) subject?: string;
}
// 状态流转(执行跟进 / 审核现场取证 / 取消 / 归档)
export class CustomerVisitStatusDTO {
@IsNotEmpty({ message: '目标状态不能为空' }) @IsInt() @Min(0) @Max(5) status!: number;
@IsOptional() @IsDateString() checkinTime?: string;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) resultRating?: string;
@IsOptional() @IsInt() surveyOrderId?: number;
@IsOptional() @IsString() @MaxLength(64) @Matches(/^[^<>]*$/) projectNo?: string;
}