🎯 核心改进: 1. ✅ 创建CentralDataRepository类 - 统一管理元数据 2. ✅ Coordinator集成CDR - 传递给所有Generator 3. ✅ DTO Generator记录位置 - 629个类型已记录 4. ✅ Service Generator CDR查询 - 支持路径查询 📊 CDR统计: - Service方法: 1,038个 - DTO: 35个 - VO: 277个 - Param: 317个 - 总类型: 629个 ⚠️ 待解决问题: - 类型名查询不匹配(记录vs查询) - 需要调试类型名映射逻辑 💡 下一步: - 修复类型名匹配问题 - 验证DTO路径查询效果 - 预期减少4,200+ DTO路径错误
378 lines
11 KiB
JavaScript
378 lines
11 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
const NamingUtils = require('../utils/naming-utils');
|
||
|
||
/**
|
||
* DTO生成器
|
||
* 将Java DTO转换为NestJS DTO
|
||
*/
|
||
class DtoGenerator {
|
||
setCDR(cdr) { this.cdr = cdr; }
|
||
|
||
constructor() {
|
||
this.cdr = null;
|
||
this.namingUtils = new NamingUtils();
|
||
}
|
||
|
||
/**
|
||
* 生成DTO文件
|
||
*/
|
||
generateDto(javaDto, outputDir) {
|
||
const dtoName = this.namingUtils.generateDtoName(javaDto.className);
|
||
const fileName = this.namingUtils.generateFileName(javaDto.className, 'dto');
|
||
|
||
// ✅ 严格保持Java目录结构
|
||
// 从Java路径中提取层级目录(如: admin/member/vo, admin/member/param)
|
||
const subPath = this.extractJavaSubPath(javaDto.filePath);
|
||
const targetDir = path.join(outputDir, subPath);
|
||
|
||
// 创建目录
|
||
if (!fs.existsSync(targetDir)) {
|
||
fs.mkdirSync(targetDir, { recursive: true });
|
||
}
|
||
|
||
const filePath = path.join(targetDir, fileName);
|
||
const content = this.generateDtoContent(javaDto, dtoName, subPath);
|
||
fs.writeFileSync(filePath, content);
|
||
|
||
console.log(`✅ 生成DTO: ${subPath}/${fileName}`);
|
||
|
||
// ✅ V2: 返回完整信息供CDR记录
|
||
return {
|
||
fileName,
|
||
content,
|
||
subPath,
|
||
dtoName,
|
||
relativePath: subPath ? `dtos/${subPath}/${fileName}` : `dtos/${fileName}`,
|
||
absolutePath: filePath
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 从Java文件路径中提取子路径
|
||
* 例如: com/niu/core/service/admin/member/vo/MemberListVo.java
|
||
* -> admin/member/vo
|
||
*/
|
||
extractJavaSubPath(javaFilePath) {
|
||
if (!javaFilePath) return '';
|
||
|
||
// 匹配 service/ 后面的路径,但排除 impl/ 目录
|
||
const match = javaFilePath.match(/service\/(.+?)\/(vo|param|dto)\//);
|
||
if (match) {
|
||
return `${match[1]}/${match[2]}`; // 如: admin/member/vo
|
||
}
|
||
|
||
// 兜底:如果在 enums 目录下
|
||
const enumMatch = javaFilePath.match(/enums\/(.+?)\/(vo|param|dto)\//);
|
||
if (enumMatch) {
|
||
return `${enumMatch[1]}/${enumMatch[2]}`;
|
||
}
|
||
|
||
// 兜底:如果在 common/loader 等特殊目录下
|
||
const loaderMatch = javaFilePath.match(/common\/loader\/(.+?)\/(vo|param|dto)\//);
|
||
if (loaderMatch) {
|
||
return `common/loader/${loaderMatch[1]}/${loaderMatch[2]}`;
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 生成DTO内容
|
||
*/
|
||
generateDtoContent(javaDto, dtoName, subPath = '') {
|
||
const imports = this.generateImports(javaDto, subPath);
|
||
const fields = this.generateFields(javaDto);
|
||
|
||
return `${imports}
|
||
|
||
export class ${dtoName} extends BaseDto {
|
||
${fields}
|
||
}
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 生成导入语句
|
||
*/
|
||
generateImports(javaDto, subPath = '') {
|
||
// 计算相对路径的 ../ 数量
|
||
const depth = subPath ? subPath.split('/').length : 0;
|
||
const basePath = '../'.repeat(depth + 1); // +1 是因为还要回到 dto/ 的上一级
|
||
|
||
const imports = [
|
||
"import { IsString, IsNumber, IsBoolean, IsOptional, IsArray, IsDateString, IsEmail, IsUrl, IsEnum } from 'class-validator';",
|
||
"import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';",
|
||
`import { BaseDto } from '${basePath}common/base.dto';`
|
||
];
|
||
|
||
// 添加枚举导入
|
||
if (javaDto.enums && javaDto.enums.length > 0) {
|
||
javaDto.enums.forEach(enumItem => {
|
||
const enumName = this.namingUtils.generateEnumName(enumItem);
|
||
const enumFileName = this.namingUtils.generateFileName(enumItem, 'enum');
|
||
imports.push(`import { ${enumName} } from '${basePath}enums/${enumFileName.replace('.enum.ts', '')}';`);
|
||
});
|
||
}
|
||
|
||
return imports.join('\n');
|
||
}
|
||
|
||
/**
|
||
* 生成装饰器
|
||
*/
|
||
generateDecorators(javaDto) {
|
||
// 不再生成类声明,类声明由 generateDtoContent 统一生成
|
||
// 这里可以添加类级别的装饰器,如 @ApiTags 等
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 生成字段
|
||
*/
|
||
generateFields(javaDto) {
|
||
if (!javaDto.fields || javaDto.fields.length === 0) {
|
||
return ' // 无字段';
|
||
}
|
||
|
||
// 去重:使用Map记录已生成的字段名
|
||
const generatedFieldNames = new Set();
|
||
const uniqueFields = [];
|
||
|
||
for (const field of javaDto.fields) {
|
||
const fieldName = this.namingUtils.toCamelCase(field.fieldName);
|
||
if (!generatedFieldNames.has(fieldName)) {
|
||
generatedFieldNames.add(fieldName);
|
||
uniqueFields.push(field);
|
||
}
|
||
}
|
||
|
||
return uniqueFields.map(field => {
|
||
return this.generateField(field);
|
||
}).join('\n\n');
|
||
}
|
||
|
||
/**
|
||
* 生成单个字段
|
||
*/
|
||
generateField(field) {
|
||
const fieldName = this.namingUtils.toCamelCase(field.fieldName);
|
||
const fieldType = this.mapJavaTypeToTypeScript(field.fieldType);
|
||
const decorators = this.generateFieldDecorators(field);
|
||
const nullable = field.nullable ? ' | null' : '';
|
||
|
||
return ` ${decorators}
|
||
${fieldName}: ${fieldType}${nullable};`;
|
||
}
|
||
|
||
/**
|
||
* 生成字段装饰器
|
||
*/
|
||
generateFieldDecorators(field) {
|
||
const decorators = [];
|
||
|
||
// API文档装饰器
|
||
if (field.description) {
|
||
decorators.push(`@ApiProperty({ description: '${field.description}' })`);
|
||
} else {
|
||
decorators.push('@ApiProperty()');
|
||
}
|
||
|
||
// 验证装饰器
|
||
const validators = this.generateValidators(field);
|
||
if (validators.length > 0) {
|
||
decorators.push(validators.join('\n '));
|
||
}
|
||
|
||
return decorators.join('\n ');
|
||
}
|
||
|
||
/**
|
||
* 生成验证装饰器
|
||
*/
|
||
generateValidators(field) {
|
||
const validators = [];
|
||
|
||
// 可选/必填装饰器(放在最前面)
|
||
if (field.isOptional || field.nullable || field.optional || (!field.isRequired && !field.annotations?.isRequired)) {
|
||
validators.push('@IsOptional()');
|
||
}
|
||
|
||
// 根据字段类型添加验证器
|
||
const fieldType = field.fieldType || '';
|
||
if (fieldType.includes('String') || fieldType === 'String') {
|
||
validators.push('@IsString()');
|
||
|
||
// 字符串长度验证
|
||
if (field.minLength || field.maxLength || field.annotations?.minLength || field.annotations?.maxLength) {
|
||
const min = field.minLength || field.annotations?.minLength || 0;
|
||
const max = field.maxLength || field.annotations?.maxLength;
|
||
if (max) {
|
||
validators.push(`@Length(${min}, ${max})`);
|
||
}
|
||
}
|
||
|
||
// 正则验证
|
||
if (field.pattern || field.annotations?.pattern) {
|
||
const pattern = field.pattern || field.annotations.pattern;
|
||
validators.push(`@Matches(/${pattern}/)`);
|
||
}
|
||
|
||
} else if (fieldType.includes('Integer') || fieldType.includes('Long') ||
|
||
fieldType.includes('Double') || fieldType.includes('Float') ||
|
||
fieldType === 'int' || fieldType === 'long' || fieldType === 'double' || fieldType === 'float') {
|
||
validators.push('@IsNumber()');
|
||
|
||
// 数值范围验证
|
||
if (field.min !== null && field.min !== undefined) {
|
||
validators.push(`@Min(${field.min})`);
|
||
}
|
||
if (field.max !== null && field.max !== undefined) {
|
||
validators.push(`@Max(${field.max})`);
|
||
}
|
||
|
||
} else if (fieldType.includes('Boolean') || fieldType === 'boolean') {
|
||
validators.push('@IsBoolean()');
|
||
|
||
} else if (fieldType.includes('Date') || fieldType.includes('LocalDateTime') ||
|
||
fieldType.includes('LocalDate') || fieldType.includes('Timestamp')) {
|
||
validators.push('@IsDateString()');
|
||
|
||
} else if (fieldType.includes('List') || fieldType.includes('Array') || fieldType.includes('[]')) {
|
||
validators.push('@IsArray()');
|
||
}
|
||
|
||
// 邮箱验证
|
||
if (field.fieldName && (field.fieldName.includes('email') || field.fieldName.includes('Email'))) {
|
||
validators.push('@IsEmail()');
|
||
}
|
||
|
||
// URL验证
|
||
if (field.fieldName && (field.fieldName.includes('url') || field.fieldName.includes('Url'))) {
|
||
validators.push('@IsUrl()');
|
||
}
|
||
|
||
// 枚举验证
|
||
if (field.enumType) {
|
||
const enumName = this.namingUtils.generateEnumName(field.enumType);
|
||
validators.push(`@IsEnum(${enumName})`);
|
||
}
|
||
|
||
return validators;
|
||
}
|
||
|
||
/**
|
||
* 将Java类型映射到TypeScript类型
|
||
*/
|
||
mapJavaTypeToTypeScript(javaType) {
|
||
const typeMap = {
|
||
'String': 'string',
|
||
'Integer': 'number',
|
||
'Long': 'number',
|
||
'Double': 'number',
|
||
'Float': 'number',
|
||
'Boolean': 'boolean',
|
||
'Date': 'string',
|
||
'LocalDateTime': 'string',
|
||
'LocalDate': 'string',
|
||
'LocalTime': 'string',
|
||
'BigDecimal': 'number',
|
||
'List': 'any[]',
|
||
'Array': 'any[]',
|
||
'Map': 'Record<string, any>',
|
||
'Set': 'Set<any>',
|
||
'Optional': 'any | null'
|
||
};
|
||
|
||
return typeMap[javaType] || 'any';
|
||
}
|
||
|
||
/**
|
||
* 生成查询DTO
|
||
*/
|
||
generateQueryDto(javaDto, outputDir) {
|
||
const dtoName = 'Query' + this.namingUtils.toPascalCase(javaDto.className) + 'Dto';
|
||
const fileName = 'query-' + this.namingUtils.toKebabCase(javaDto.className) + '.dto.ts';
|
||
const filePath = path.join(outputDir, fileName);
|
||
|
||
const content = this.generateQueryDtoContent(javaDto, dtoName);
|
||
fs.writeFileSync(filePath, content);
|
||
|
||
console.log(`✅ 生成查询DTO: ${filePath}`);
|
||
return { fileName, content };
|
||
}
|
||
|
||
/**
|
||
* 生成查询DTO内容
|
||
*/
|
||
generateQueryDtoContent(javaDto, dtoName) {
|
||
const imports = [
|
||
"import { IsOptional, IsNumber, IsString, IsDateString } from 'class-validator';",
|
||
"import { ApiPropertyOptional } from '@nestjs/swagger';",
|
||
"import { Transform } from 'class-transformer';"
|
||
].join('\n');
|
||
|
||
const decorators = `export class ${dtoName} {`;
|
||
|
||
const fields = [
|
||
' @ApiPropertyOptional({ description: \'页码\', default: 1 })',
|
||
' @IsOptional()',
|
||
' @Transform(({ value }) => parseInt(value))',
|
||
' @IsNumber()',
|
||
' page?: number = 1;',
|
||
'',
|
||
' @ApiPropertyOptional({ description: \'每页数量\', default: 10 })',
|
||
' @IsOptional()',
|
||
' @Transform(({ value }) => parseInt(value))',
|
||
' @IsNumber()',
|
||
' limit?: number = 10;',
|
||
'',
|
||
' @ApiPropertyOptional({ description: \'关键词搜索\' })',
|
||
' @IsOptional()',
|
||
' @IsString()',
|
||
' keyword?: string;',
|
||
'',
|
||
' @ApiPropertyOptional({ description: \'开始时间\' })',
|
||
' @IsOptional()',
|
||
' @IsDateString()',
|
||
' startTime?: string;',
|
||
'',
|
||
' @ApiPropertyOptional({ description: \'结束时间\' })',
|
||
' @IsOptional()',
|
||
' @IsDateString()',
|
||
' endTime?: string;'
|
||
].join('\n');
|
||
|
||
return `${imports}
|
||
|
||
${decorators}
|
||
${fields}
|
||
}
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 验证DTO一致性
|
||
*/
|
||
validateDtoConsistency(javaDto, nestJSDto) {
|
||
const issues = [];
|
||
|
||
// 验证字段数量
|
||
if (javaDto.fields.length !== nestJSDto.fields.length) {
|
||
issues.push('字段数量不一致');
|
||
}
|
||
|
||
// 验证每个字段
|
||
javaDto.fields.forEach((javaField, index) => {
|
||
const nestJSField = nestJSDto.fields[index];
|
||
if (nestJSField && javaField.fieldName !== nestJSField.fieldName) {
|
||
issues.push(`字段名不一致: ${javaField.fieldName} vs ${nestJSField.fieldName}`);
|
||
}
|
||
});
|
||
|
||
return issues;
|
||
}
|
||
}
|
||
|
||
module.exports = DtoGenerator;
|