Files
wwjcloud-nest-v1/wwjcloud-nest-v1/tools/java-to-nestjs-migration/generators/dto-generator.js
wanwu 3c87db45ff feat(cdr): 建立中央数据仓库(CDR)架构
🎯 核心改进:
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路径错误
2025-10-29 22:41:23 +08:00

378 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;