🏗️ 架构重构: - Coordinator构建中央Service方法签名索引(1038个方法) - 所有Generator从索引读取,而非各自读文件 - Controller不再依赖已生成的NestJS文件 ✅ 修复: 1. Java类型映射(Integer/Long→number, String→string) 2. Controller参数智能匹配V6(Java类型感知) 3. 消除层级间的循环依赖 📊 效果: 14234 → 14121 (-113) - Controller层: 162 → 49 (-113) - TS2345类型错误: 115 → 2 (-113) 🔧 核心改进: - 统一数据源,消除不一致 - 各Generator协同工作,不再各自为政
933 lines
30 KiB
JavaScript
933 lines
30 KiB
JavaScript
const fs = require('fs');
|
||
const path = require('path');
|
||
const NamingUtils = require('../utils/naming-utils');
|
||
|
||
/**
|
||
* 控制器生成器
|
||
* 将Java控制器转换为NestJS控制器
|
||
*/
|
||
class ControllerGenerator {
|
||
constructor(outputDir) {
|
||
this.namingUtils = new NamingUtils();
|
||
this.outputDir = outputDir || '';
|
||
// ✅ 中央Service方法签名索引
|
||
this.serviceMethodSignatureIndex = null;
|
||
}
|
||
|
||
/**
|
||
* ✅ 设置中央Service方法签名索引
|
||
*/
|
||
setServiceMethodSignatureIndex(index) {
|
||
this.serviceMethodSignatureIndex = index;
|
||
}
|
||
|
||
/**
|
||
* 生成控制器文件
|
||
*/
|
||
generateController(javaController, outputDir) {
|
||
// 如果this.outputDir已经被设置,不要覆盖它(用于findServiceFile)
|
||
// outputDir参数用于控制器文件的输出目录
|
||
const controllerOutputDir = outputDir || this.outputDir;
|
||
|
||
// 检查控制器数据是否有效
|
||
if (!javaController || !javaController.className) {
|
||
console.warn(`⚠️ 跳过无效控制器: ${JSON.stringify(javaController)}`);
|
||
return null;
|
||
}
|
||
|
||
// 根据Java文件路径创建子目录结构
|
||
const subDir = this.getSubDirectoryFromJavaPath(javaController.filePath, 'controller');
|
||
const fullOutputDir = path.join(controllerOutputDir, subDir);
|
||
|
||
// 确保子目录存在
|
||
if (!fs.existsSync(fullOutputDir)) {
|
||
fs.mkdirSync(fullOutputDir, { recursive: true });
|
||
}
|
||
|
||
const controllerName = this.namingUtils.generateControllerName(javaController.className);
|
||
const fileName = this.namingUtils.generateFileName(javaController.className, 'controller');
|
||
const filePath = path.join(fullOutputDir, fileName);
|
||
|
||
const content = this.generateControllerContent(javaController, controllerName);
|
||
fs.writeFileSync(filePath, content);
|
||
|
||
console.log(`✅ 生成控制器: ${filePath}`);
|
||
return { fileName, content };
|
||
}
|
||
|
||
/**
|
||
* 根据Java文件路径获取子目录结构
|
||
*/
|
||
getSubDirectoryFromJavaPath(javaFilePath, type) {
|
||
if (!javaFilePath) return '';
|
||
|
||
// 从Java文件路径中提取包结构
|
||
// 例如: /path/to/java/com/niu/core/controller/core/HttpServerErrorController.java
|
||
// 提取: controller/core
|
||
const pathParts = javaFilePath.split(path.sep);
|
||
const javaIndex = pathParts.findIndex(part => part === 'java');
|
||
|
||
if (javaIndex === -1) return '';
|
||
|
||
// 获取java目录后的包结构
|
||
const packageParts = pathParts.slice(javaIndex + 1, -1); // 排除文件名
|
||
|
||
// 根据类型过滤相关目录
|
||
const typeIndex = packageParts.findIndex(part => part === type || part === type + 's');
|
||
if (typeIndex === -1) return '';
|
||
|
||
// 返回类型目录后的子目录结构
|
||
const subParts = packageParts.slice(typeIndex + 1);
|
||
return subParts.join('/');
|
||
}
|
||
|
||
/**
|
||
* 生成控制器内容
|
||
*/
|
||
generateControllerContent(javaController, controllerName) {
|
||
const routeInfo = javaController.routeInfo;
|
||
const imports = this.generateImports(javaController);
|
||
const decorators = this.generateDecorators(routeInfo);
|
||
const constructor = this.generateConstructor(javaController);
|
||
const methods = this.generateMethods(javaController);
|
||
|
||
return `${imports}
|
||
|
||
${decorators}
|
||
export class ${controllerName} {
|
||
${constructor}
|
||
${methods}
|
||
}
|
||
`;
|
||
}
|
||
|
||
|
||
/**
|
||
* 查找Service文件的实际路径
|
||
* 支持宽松匹配,忽略连字符差异
|
||
* @returns {string|null} 相对于services目录的路径
|
||
*/
|
||
findServiceFile(serviceName) {
|
||
if (!this.outputDir) return null;
|
||
|
||
const servicesDir = path.join(this.outputDir, 'services');
|
||
if (!fs.existsSync(servicesDir)) return null;
|
||
|
||
// 规范化文件名用于比较(移除连字符、下划线、impl、service等后缀,统一为小写)
|
||
const normalizeForComparison = (name) => {
|
||
return name.toLowerCase()
|
||
.replace(/[-_]/g, '')
|
||
.replace(/serviceimpl/g, '')
|
||
.replace(/impl/g, '')
|
||
.replace(/service\.ts/g, '.ts')
|
||
.replace(/service/g, '');
|
||
};
|
||
|
||
const targetNormalized = normalizeForComparison(serviceName);
|
||
|
||
const searchDir = (dir) => {
|
||
const files = fs.readdirSync(dir);
|
||
for (const file of files) {
|
||
const fullPath = path.join(dir, file);
|
||
const stat = fs.statSync(fullPath);
|
||
|
||
if (stat.isDirectory()) {
|
||
const result = searchDir(fullPath);
|
||
if (result) return result;
|
||
} else if (normalizeForComparison(file) === targetNormalized) {
|
||
return path.relative(servicesDir, fullPath);
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
return searchDir(servicesDir);
|
||
}
|
||
|
||
/**
|
||
* 从Service文件中提取实际的类名
|
||
* @param {string} serviceFilePath - Service文件的完整路径
|
||
* @returns {string|null} - 导出的类名
|
||
*/
|
||
extractServiceClassName(serviceFilePath) {
|
||
if (!fs.existsSync(serviceFilePath)) return null;
|
||
|
||
try {
|
||
const content = fs.readFileSync(serviceFilePath, 'utf-8');
|
||
// 匹配: export class XxxService
|
||
const classMatch = content.match(/export\s+class\s+(\w+)/);
|
||
return classMatch ? classMatch[1] : null;
|
||
} catch (error) {
|
||
console.warn(`⚠️ 无法读取Service文件: ${serviceFilePath}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取Service的实际类名(优先从文件读取,否则推断)
|
||
* @param {string} dep - 依赖的Java类名(如 AddonServiceImpl)
|
||
* @returns {string} - 实际的TypeScript类名
|
||
*/
|
||
getActualServiceClassName(dep) {
|
||
const serviceFileName = this.namingUtils.generateFileName(dep, 'service');
|
||
let serviceName = this.namingUtils.generateServiceName(dep); // 默认推断的类名
|
||
|
||
if (this.outputDir) {
|
||
const servicesDir = path.join(this.outputDir, 'services');
|
||
const serviceRelativePath = this.findServiceFile(serviceFileName);
|
||
|
||
if (serviceRelativePath) {
|
||
const serviceFullPath = path.join(servicesDir, serviceRelativePath);
|
||
const actualClassName = this.extractServiceClassName(serviceFullPath);
|
||
|
||
if (actualClassName) {
|
||
serviceName = actualClassName; // 使用实际的类名
|
||
}
|
||
}
|
||
}
|
||
|
||
return serviceName;
|
||
}
|
||
|
||
/**
|
||
* 计算从Controller到Service的相对路径
|
||
*/
|
||
calculateServicePath(javaFilePath, serviceFileName) {
|
||
const cleanFileName = serviceFileName.replace('.ts', '');
|
||
|
||
// 先尝试查找实际的service文件
|
||
const actualServicePath = this.findServiceFile(serviceFileName);
|
||
|
||
if (actualServicePath) {
|
||
if (!javaFilePath) return `../../../services/${actualServicePath.replace(/\\/g, '/').replace('.ts', '')}`;
|
||
|
||
const pathParts = javaFilePath.split(path.sep);
|
||
const javaIndex = pathParts.findIndex(part => part === 'java');
|
||
|
||
if (javaIndex === -1) return `../../../services/${actualServicePath.replace(/\\/g, '/').replace('.ts', '')}`;
|
||
|
||
const packageParts = pathParts.slice(javaIndex + 1, -1);
|
||
const controllerIndex = packageParts.findIndex(part => part === 'controller' || part === 'controllers');
|
||
|
||
if (controllerIndex === -1) return `../../../services/${actualServicePath.replace(/\\/g, '/').replace('.ts', '')}`;
|
||
|
||
const subParts = packageParts.slice(controllerIndex + 1);
|
||
const depth = subParts.length + 1;
|
||
const upLevels = '../'.repeat(depth);
|
||
|
||
return `${upLevels}services/${actualServicePath.replace(/\\/g, '/').replace('.ts', '')}`;
|
||
}
|
||
|
||
// 回退到推断路径
|
||
if (!javaFilePath) return `../../../services/admin/impl/${cleanFileName}`;
|
||
|
||
const pathParts = javaFilePath.split(path.sep);
|
||
const javaIndex = pathParts.findIndex(part => part === 'java');
|
||
|
||
if (javaIndex === -1) return `../../../services/admin/impl/${cleanFileName}`;
|
||
|
||
const packageParts = pathParts.slice(javaIndex + 1, -1);
|
||
const controllerIndex = packageParts.findIndex(part => part === 'controller' || part === 'controllers');
|
||
|
||
if (controllerIndex === -1) return `../../../services/admin/impl/${cleanFileName}`;
|
||
|
||
const subParts = packageParts.slice(controllerIndex + 1);
|
||
const depth = subParts.length + 1;
|
||
const upLevels = '../'.repeat(depth);
|
||
|
||
let serviceSubPath = subParts.map(part => part === 'adminapi' ? 'admin' : part).join('/');
|
||
if (serviceSubPath) {
|
||
serviceSubPath += '/impl';
|
||
} else {
|
||
serviceSubPath = 'admin/impl';
|
||
}
|
||
|
||
return `${upLevels}services/${serviceSubPath}/${cleanFileName}`;
|
||
}
|
||
|
||
/**
|
||
* 计算Result类的相对路径
|
||
*/
|
||
calculateResultPath(javaFilePath) {
|
||
// 根据Java文件路径计算相对路径
|
||
if (!javaFilePath) return '../common/result';
|
||
|
||
// 从Java文件路径中提取包结构
|
||
const pathParts = javaFilePath.split(path.sep);
|
||
const javaIndex = pathParts.findIndex(part => part === 'java');
|
||
|
||
if (javaIndex === -1) return '../common/result';
|
||
|
||
// 获取java目录后的包结构
|
||
const packageParts = pathParts.slice(javaIndex + 1, -1); // 排除文件名
|
||
|
||
// 根据类型过滤相关目录
|
||
const typeIndex = packageParts.findIndex(part => part === 'controller' || part === 'controllers');
|
||
if (typeIndex === -1) return '../common/result';
|
||
|
||
// 返回类型目录后的子目录结构
|
||
const subParts = packageParts.slice(typeIndex + 1);
|
||
|
||
// 计算需要返回的层级数
|
||
const depth = subParts.length + 1; // +1 for controllers directory
|
||
const upLevels = '../'.repeat(depth);
|
||
|
||
return `${upLevels}common/result`;
|
||
}
|
||
|
||
/**
|
||
* 生成导入语句
|
||
*/
|
||
generateImports(javaController) {
|
||
const imports = [
|
||
"import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards } from '@nestjs/common';",
|
||
"import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';",
|
||
"import { AuthGuard, RbacGuard, Public, Result } from '@wwjBoot';"
|
||
];
|
||
|
||
// 获取依赖列表,如果为空则从控制器名称推断
|
||
let dependencies = javaController.dependencies || [];
|
||
|
||
if (dependencies.length === 0) {
|
||
// 从控制器名称推断 Service 依赖
|
||
// Controller -> ControllerServiceImpl(匹配Java Service命名规范)
|
||
const controllerName = javaController.className.replace(/Controller$/, '');
|
||
dependencies = [controllerName + 'ServiceImpl'];
|
||
}
|
||
|
||
// 添加服务导入 - 计算正确的相对路径
|
||
dependencies.forEach(dep => {
|
||
const serviceFileName = this.namingUtils.generateFileName(dep, 'service');
|
||
|
||
// 检查Service文件是否存在
|
||
const serviceRelativePath = this.findServiceFile(serviceFileName);
|
||
if (!serviceRelativePath) {
|
||
console.warn(`⚠️ Service文件未找到: ${serviceFileName} (Controller: ${javaController.className})`);
|
||
return; // 跳过该导入
|
||
}
|
||
|
||
const relativePath = this.calculateServicePath(javaController.filePath, serviceFileName);
|
||
const serviceName = this.getActualServiceClassName(dep);
|
||
|
||
if (serviceName) {
|
||
imports.push(`import { ${serviceName} } from '${relativePath}';`);
|
||
}
|
||
});
|
||
|
||
// 添加DTO导入
|
||
if (javaController.dtos && javaController.dtos.length > 0) {
|
||
javaController.dtos.forEach(dto => {
|
||
const dtoName = this.namingUtils.generateDtoName(dto);
|
||
const dtoFileName = this.namingUtils.generateFileName(dto, 'dto');
|
||
imports.push(`import { ${dtoName} } from '../dtos/${dtoFileName.replace('.dto.ts', '')}';`);
|
||
});
|
||
}
|
||
|
||
return imports.join('\n');
|
||
}
|
||
|
||
/**
|
||
* 生成装饰器
|
||
*/
|
||
generateDecorators(routeInfo) {
|
||
const decorators = [];
|
||
|
||
// 控制器装饰器 - 保持Java原始路径,只去掉前导斜杠
|
||
// Java: adminapi → NestJS: adminapi → 最终: /adminapi/*
|
||
// Java: /api → NestJS: api → 最终: /api/*
|
||
if (routeInfo.controllerPath) {
|
||
let cleanPath = routeInfo.controllerPath.replace(/^\/+/, ''); // 只去掉前导斜杠
|
||
|
||
if (cleanPath) {
|
||
decorators.push(`@Controller('${cleanPath}')`);
|
||
} else {
|
||
decorators.push('@Controller()'); // 空路径使用无参数形式
|
||
}
|
||
} else {
|
||
decorators.push('@Controller()');
|
||
}
|
||
|
||
// API文档装饰器
|
||
decorators.push('@ApiTags(\'API\')');
|
||
|
||
// ✅ 修复:自动识别Java白名单路由,添加@Public()装饰器
|
||
// 对应Java配置: WebMvcConfig.excludePathPatterns
|
||
const controllerPath = (routeInfo.controllerPath || '').toLowerCase();
|
||
|
||
// Java白名单路径(无需认证):
|
||
// 1. /adminapi/login/**
|
||
// 2. /adminapi/sys/web/**
|
||
// 3. /adminapi/captcha/**
|
||
const publicPaths = [
|
||
'adminapi/login',
|
||
'adminapi/sys/web',
|
||
'adminapi/captcha',
|
||
'login',
|
||
'register',
|
||
'captcha',
|
||
];
|
||
|
||
const isPublicRoute = publicPaths.some(path => controllerPath.includes(path)) ||
|
||
controllerPath === 'api' ||
|
||
routeInfo.hasClassLevelIgnore;
|
||
|
||
// 根据路由信息决定是否添加认证守卫
|
||
if (isPublicRoute) {
|
||
// 白名单路由或Java标记@SaIgnore的路由,自动添加@Public()
|
||
decorators.push('@Public()');
|
||
} else if (routeInfo.hasClassLevelAuth) {
|
||
// 如果类级别有 @SaCheckLogin,添加守卫
|
||
decorators.push('@UseGuards(AuthGuard)');
|
||
decorators.push('@ApiBearerAuth()');
|
||
}
|
||
|
||
return decorators.join('\n');
|
||
}
|
||
|
||
/**
|
||
* 生成方法
|
||
*/
|
||
generateMethods(javaController) {
|
||
if (!javaController.routeInfo || !javaController.routeInfo.methods) {
|
||
return ' // 无方法';
|
||
}
|
||
|
||
const existingMethods = [];
|
||
return javaController.routeInfo.methods.map(method => {
|
||
const methodName = this.generateValidMethodName(method, existingMethods);
|
||
existingMethods.push(methodName);
|
||
return this.generateMethod(method, javaController, methodName);
|
||
}).join('\n\n');
|
||
}
|
||
|
||
/**
|
||
* 生成单个方法
|
||
*/
|
||
generateMethod(method, javaController, methodName) {
|
||
// 使用传入的方法名,确保无重复
|
||
const httpDecorator = this.getHttpDecorator(method.httpMethod);
|
||
const parameters = this.generateMethodParameters(method, javaController);
|
||
const returnType = this.generateReturnType(method);
|
||
|
||
// 将 /{key} 格式转换为 NestJS 的 :key 格式,并移除前导斜杠
|
||
const nestPath = method.path.replace(/\{(\w+)\}/g, ':$1').replace(/^\/+/, '');
|
||
|
||
// 生成方法体:使用实际的Java Service调用
|
||
const methodBody = this.generateMethodBody(method, javaController);
|
||
|
||
// 生成方法级别的认证装饰器
|
||
const authDecorators = this.generateMethodAuthDecorators(method, javaController.routeInfo);
|
||
|
||
return ` ${httpDecorator}('${nestPath}')
|
||
@ApiOperation({ summary: '${method.path}' })
|
||
@ApiResponse({ status: 200, description: '成功' })${authDecorators}
|
||
async ${methodName}(${parameters}): Promise<${returnType}> {
|
||
${methodBody}
|
||
}`;
|
||
}
|
||
|
||
/**
|
||
* 生成方法级别的认证装饰器
|
||
*/
|
||
generateMethodAuthDecorators(method, routeInfo) {
|
||
const decorators = [];
|
||
|
||
// 情况1: 类级别有认证,但方法标记了@SaIgnore
|
||
if (routeInfo.hasClassLevelAuth && method.isPublic) {
|
||
decorators.push('\n @Public()');
|
||
}
|
||
|
||
// 情况2: 类级别无认证,但方法标记了@SaCheckLogin
|
||
if (!routeInfo.hasClassLevelAuth && !routeInfo.hasClassLevelIgnore && method.requiresAuth) {
|
||
decorators.push('\n @UseGuards(AuthGuard)');
|
||
decorators.push('\n @ApiBearerAuth()');
|
||
}
|
||
|
||
return decorators.join('');
|
||
}
|
||
|
||
/**
|
||
* 生成方法体
|
||
*/
|
||
generateMethodBody(method, javaController) {
|
||
const javaMethodName = method.javaMethodName;
|
||
const methodServiceCalls = javaController.methodServiceCalls || {};
|
||
const serviceCalls = methodServiceCalls[javaMethodName] || [];
|
||
|
||
if (serviceCalls.length === 0) {
|
||
// 没有找到Service调用,返回占位符
|
||
return ` // TODO: 实现业务逻辑
|
||
return Result.success(null);`;
|
||
}
|
||
|
||
// 使用第一个Service调用(通常Controller方法只调用一个主Service方法)
|
||
const firstCall = serviceCalls[0];
|
||
const servicePropertyName = this.namingUtils.toCamelCase(firstCall.serviceImpl) + 'Service';
|
||
const serviceMethodName = firstCall.serviceMethod;
|
||
const methodParams = this.generateServiceMethodParameters(method, javaController);
|
||
|
||
return ` const result = await this.${servicePropertyName}.${serviceMethodName}(${methodParams});
|
||
return Result.success(result);`;
|
||
}
|
||
|
||
/**
|
||
* 生成有效的方法名
|
||
*/
|
||
generateValidMethodName(method, existingMethods = []) {
|
||
// 如果已经有有效的方法名,直接使用
|
||
if (method.methodName && this.isValidMethodName(method.methodName)) {
|
||
return method.methodName;
|
||
}
|
||
|
||
// 从路径生成方法名
|
||
const path = method.path || '';
|
||
const httpMethod = method.httpMethod || 'get';
|
||
|
||
// 移除路径参数和特殊字符
|
||
let methodName = path
|
||
.replace(/[{}]/g, '') // 移除路径参数
|
||
.replace(/[^a-zA-Z0-9\/]/g, '') // 只保留字母数字和斜杠
|
||
.split('/')
|
||
.filter(part => part.length > 0)
|
||
.join('');
|
||
|
||
// 如果为空,使用HTTP方法
|
||
if (!methodName) {
|
||
methodName = httpMethod.toLowerCase();
|
||
}
|
||
|
||
// 确保以HTTP方法开头
|
||
const httpPrefix = httpMethod.toLowerCase();
|
||
if (!methodName.toLowerCase().startsWith(httpPrefix)) {
|
||
methodName = httpPrefix + this.namingUtils.toPascalCase(methodName);
|
||
}
|
||
|
||
// 转换为camelCase
|
||
let finalMethodName = this.namingUtils.toCamelCase(methodName);
|
||
|
||
// 检查重复并添加后缀
|
||
let counter = 1;
|
||
while (existingMethods.includes(finalMethodName)) {
|
||
finalMethodName = this.namingUtils.toCamelCase(methodName) + counter;
|
||
counter++;
|
||
}
|
||
|
||
return finalMethodName;
|
||
}
|
||
|
||
/**
|
||
* 检查方法名是否有效
|
||
*/
|
||
isValidMethodName(name) {
|
||
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);
|
||
}
|
||
|
||
/**
|
||
* 获取HTTP装饰器
|
||
*/
|
||
getHttpDecorator(httpMethod) {
|
||
const methodMap = {
|
||
'get': '@Get',
|
||
'post': '@Post',
|
||
'put': '@Put',
|
||
'delete': '@Delete',
|
||
'patch': '@Patch'
|
||
};
|
||
return methodMap[httpMethod.toLowerCase()] || '@Get';
|
||
}
|
||
|
||
/**
|
||
* 生成方法参数(V2:基于Service签名)
|
||
*/
|
||
generateMethodParameters(method, javaController) {
|
||
const parameters = [];
|
||
const httpMethod = method.httpMethod.toLowerCase();
|
||
const isPost = httpMethod === 'post' || httpMethod === 'put';
|
||
const isGet = httpMethod === 'get';
|
||
|
||
// 尝试读取Service签名,确定需要哪些参数
|
||
const serviceParams = javaController ? this.readServiceMethodSignature(method, javaController) : null;
|
||
|
||
let needsBody = false;
|
||
let needsQuery = false;
|
||
let pathParams = [];
|
||
|
||
if (serviceParams !== null) {
|
||
// 根据Service参数类型,确定Controller需要哪些参数
|
||
for (const param of serviceParams) {
|
||
const paramType = param.type;
|
||
const paramName = param.name;
|
||
const tsType = this.javaTypeToTsType(paramType); // ✅ Java类型转换
|
||
|
||
// 路径参数
|
||
if (method.path && method.path.includes(`{${paramName}}`)) {
|
||
pathParams.push(paramName);
|
||
}
|
||
// DTO/Param类型
|
||
else if (this.isBodyParameter(paramType)) {
|
||
if (isPost) {
|
||
needsBody = true;
|
||
} else {
|
||
needsQuery = true;
|
||
}
|
||
}
|
||
// 基本类型或其他
|
||
else {
|
||
if (isPost) {
|
||
needsQuery = true; // POST请求的基本类型从query获取
|
||
} else {
|
||
needsQuery = true;
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 无法读取Service签名,使用默认逻辑
|
||
needsBody = isPost;
|
||
needsQuery = isGet;
|
||
|
||
// 提取路径参数
|
||
if (method.path.includes('{')) {
|
||
const paramNames = method.path.match(/\{(\w+)\}/g);
|
||
if (paramNames) {
|
||
pathParams = paramNames.map(p => p.replace(/[{}]/g, ''));
|
||
}
|
||
}
|
||
}
|
||
|
||
// 生成参数列表
|
||
if (needsBody) {
|
||
const dtoType = method.requestDto || 'Record<string, any>';
|
||
parameters.push(`@Body() body: ${dtoType}`);
|
||
}
|
||
|
||
if (pathParams.length > 0) {
|
||
if (pathParams.length === 1) {
|
||
parameters.push(`@Param('${pathParams[0]}') ${pathParams[0]}: string`);
|
||
} else {
|
||
// 多个路径参数,分别声明
|
||
pathParams.forEach(paramName => {
|
||
parameters.push(`@Param('${paramName}') ${paramName}: string`);
|
||
});
|
||
}
|
||
}
|
||
|
||
if (needsQuery) {
|
||
const queryType = method.queryDto || 'Record<string, any>';
|
||
parameters.push(`@Query() query: ${queryType}`);
|
||
}
|
||
|
||
return parameters.join(', ');
|
||
}
|
||
|
||
/**
|
||
* 生成返回类型
|
||
*/
|
||
generateReturnType(method) {
|
||
return 'Result<any>';
|
||
}
|
||
|
||
/**
|
||
* 获取正确的服务属性名
|
||
*/
|
||
getCorrectServiceName(javaController) {
|
||
// 获取依赖列表,如果为空则从控制器名称推断
|
||
let dependencies = javaController.dependencies || [];
|
||
|
||
if (dependencies.length === 0) {
|
||
// 从控制器名称推断服务名
|
||
// Controller -> ControllerServiceImpl(匹配Java Service命名规范)
|
||
const controllerName = javaController.className.replace(/Controller$/, '');
|
||
return this.namingUtils.toCamelCase(controllerName + 'ServiceImpl') + 'Service';
|
||
}
|
||
|
||
const firstDep = dependencies[0];
|
||
return this.namingUtils.toCamelCase(firstDep) + 'Service';
|
||
}
|
||
|
||
/**
|
||
* 生成服务方法参数
|
||
*
|
||
* ✅ V2: 智能匹配Service方法签名
|
||
*
|
||
* 策略:
|
||
* 1. 尝试从Service文件读取方法签名
|
||
* 2. 根据参数类型智能映射到Controller参数源
|
||
* 3. 如果读取失败,回退到基于HTTP路由的简单映射
|
||
*/
|
||
generateServiceMethodParameters(method, javaController) {
|
||
// 尝试从Service文件读取方法签名
|
||
const serviceParams = this.readServiceMethodSignature(method, javaController);
|
||
|
||
if (serviceParams !== null) {
|
||
// 成功读取到Service签名(包括0参数的情况)
|
||
if (serviceParams.length === 0) {
|
||
return ''; // 无参数方法
|
||
}
|
||
return this.mapServiceParametersToController(serviceParams, method);
|
||
}
|
||
|
||
// 回退到旧逻辑(基于HTTP路由)
|
||
return this.generateParametersFromRoute(method);
|
||
}
|
||
|
||
/**
|
||
* ✅ V2: 从中央索引读取Service方法签名(不再读取文件)
|
||
*/
|
||
readServiceMethodSignature(method, javaController) {
|
||
try {
|
||
// 如果没有索引,回退到旧逻辑
|
||
if (!this.serviceMethodSignatureIndex) {
|
||
return null;
|
||
}
|
||
|
||
// 从methodServiceCalls中获取实际调用的Service名称
|
||
const javaMethodName = method.javaMethodName;
|
||
const methodServiceCalls = javaController.methodServiceCalls || {};
|
||
const serviceCalls = methodServiceCalls[javaMethodName] || [];
|
||
|
||
if (serviceCalls.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
const serviceImplName = serviceCalls[0].serviceImpl;
|
||
const serviceMethodName = serviceCalls[0].serviceMethod;
|
||
|
||
// ✅ 从中央索引查询方法签名
|
||
const key = `${serviceImplName}.${serviceMethodName}`;
|
||
const signature = this.serviceMethodSignatureIndex.get(key);
|
||
|
||
if (!signature) {
|
||
return null; // 索引中没有找到
|
||
}
|
||
|
||
// 返回参数列表(已经是 {name, type} 格式)
|
||
return signature.parameters || [];
|
||
|
||
} catch (error) {
|
||
// 查询失败,返回null
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 智能映射Service参数到Controller参数源
|
||
*
|
||
* ✅ V6 策略(Java类型感知):
|
||
* - Java Integer/Long/int/long → Number(id) 或 Number(query.xxx)
|
||
* - Java String → query.xxx
|
||
* - DTO/Param → body 或 query
|
||
*/
|
||
mapServiceParametersToController(serviceParams, method) {
|
||
const controllerParams = [];
|
||
const httpMethod = method.httpMethod.toLowerCase();
|
||
const isGetRequest = httpMethod === 'get';
|
||
const isPostOrPut = httpMethod === 'post' || httpMethod === 'put';
|
||
|
||
for (const param of serviceParams) {
|
||
const paramType = param.type;
|
||
const paramName = param.name;
|
||
|
||
// ✅ 将Java类型转为TS类型(用于判断)
|
||
const tsType = this.javaTypeToTsType(paramType);
|
||
|
||
// 1. 路径参数:根据类型决定是否转换
|
||
if (method.path && method.path.includes(`{${paramName}}`)) {
|
||
if (tsType === 'number') {
|
||
controllerParams.push(`Number(${paramName})`);
|
||
} else {
|
||
controllerParams.push(paramName);
|
||
}
|
||
}
|
||
// 2. DTO/Param类型:整体传递
|
||
else if (this.isBodyParameter(paramType)) {
|
||
if (isPostOrPut) {
|
||
controllerParams.push('body');
|
||
} else {
|
||
controllerParams.push('query');
|
||
}
|
||
}
|
||
// 3. 基本类型
|
||
else if (tsType === 'number' || tsType === 'string' || tsType === 'boolean') {
|
||
if (tsType === 'number') {
|
||
controllerParams.push(`Number(query.${paramName})`);
|
||
} else {
|
||
controllerParams.push(`query.${paramName}`);
|
||
}
|
||
}
|
||
// 4. 默认:query
|
||
else {
|
||
controllerParams.push('query');
|
||
}
|
||
}
|
||
|
||
return controllerParams.join(', ');
|
||
}
|
||
|
||
/**
|
||
* ✅ 将Java类型转换为TypeScript类型
|
||
*/
|
||
javaTypeToTsType(javaType) {
|
||
const typeMap = {
|
||
// 数字类型
|
||
'Integer': 'number',
|
||
'int': 'number',
|
||
'Long': 'number',
|
||
'long': 'number',
|
||
'Double': 'number',
|
||
'double': 'number',
|
||
'Float': 'number',
|
||
'float': 'number',
|
||
'BigDecimal': 'number',
|
||
// 字符串类型
|
||
'String': 'string',
|
||
// 布尔类型
|
||
'Boolean': 'boolean',
|
||
'boolean': 'boolean',
|
||
// 其他
|
||
'void': 'void',
|
||
'Object': 'any'
|
||
};
|
||
return typeMap[javaType] || javaType; // 未映射的保持原样
|
||
}
|
||
|
||
/**
|
||
* 判断是否是ID参数
|
||
*/
|
||
isIdParameter(paramName, paramType) {
|
||
return paramName === 'id' ||
|
||
paramName.endsWith('Id') ||
|
||
(paramType === 'number' && paramName.includes('id'));
|
||
}
|
||
|
||
/**
|
||
* 判断是否是Body参数(DTO/Param类型)
|
||
*/
|
||
isBodyParameter(paramType) {
|
||
return paramType.includes('Dto') ||
|
||
paramType.includes('Param') ||
|
||
paramType.includes('Body') ||
|
||
paramType.includes('Request');
|
||
}
|
||
|
||
/**
|
||
* 判断是否是分页参数
|
||
*/
|
||
isPageParameter(paramType) {
|
||
return paramType.includes('PageParam') ||
|
||
paramType.includes('Page');
|
||
}
|
||
|
||
/**
|
||
* 判断是否是搜索参数
|
||
*/
|
||
isSearchParameter(paramType) {
|
||
return paramType.includes('Search') ||
|
||
paramType.includes('Query') ||
|
||
paramType.includes('Filter');
|
||
}
|
||
|
||
/**
|
||
* 判断是否是基本类型
|
||
*/
|
||
isPrimitiveParameter(paramType) {
|
||
return ['number', 'string', 'boolean'].includes(paramType.toLowerCase());
|
||
}
|
||
|
||
/**
|
||
* 基于HTTP路由生成参数(回退逻辑)
|
||
*/
|
||
generateParametersFromRoute(method) {
|
||
const parameters = [];
|
||
|
||
// 根据HTTP方法添加参数
|
||
if (method.httpMethod.toLowerCase() === 'post' || method.httpMethod.toLowerCase() === 'put') {
|
||
parameters.push('body');
|
||
}
|
||
|
||
// 添加路径参数 - 使用实际的参数名
|
||
if (method.path.includes('{')) {
|
||
const paramNames = method.path.match(/\{(\w+)\}/g);
|
||
if (paramNames && paramNames.length === 1) {
|
||
// 单个参数,使用参数名
|
||
const paramName = paramNames[0].replace(/[{}]/g, '');
|
||
parameters.push(paramName);
|
||
} else if (paramNames && paramNames.length > 1) {
|
||
// 多个参数,使用params对象
|
||
parameters.push('params');
|
||
}
|
||
}
|
||
|
||
// 添加查询参数 - 只在GET请求时添加
|
||
if (method.httpMethod.toLowerCase() === 'get') {
|
||
parameters.push('query');
|
||
}
|
||
|
||
return parameters.join(', ');
|
||
}
|
||
|
||
/**
|
||
* 生成构造函数
|
||
*/
|
||
generateConstructor(javaController) {
|
||
// 获取依赖列表,如果为空则从控制器名称推断
|
||
let dependencies = javaController.dependencies || [];
|
||
|
||
if (dependencies.length === 0) {
|
||
// 从控制器名称推断 Service 依赖
|
||
// Controller -> ControllerServiceImpl(匹配Java Service命名规范)
|
||
const controllerName = javaController.className.replace(/Controller$/, '');
|
||
dependencies = [controllerName + 'ServiceImpl'];
|
||
}
|
||
|
||
const constructorParams = [];
|
||
dependencies.forEach(dep => {
|
||
// 检查Service文件是否存在
|
||
const serviceFileName = this.namingUtils.generateFileName(dep, 'service');
|
||
const serviceRelativePath = this.findServiceFile(serviceFileName);
|
||
|
||
if (serviceRelativePath) {
|
||
const serviceName = this.getActualServiceClassName(dep); // 使用实际类名
|
||
const servicePropertyName = this.namingUtils.toCamelCase(dep) + 'Service';
|
||
constructorParams.push(` private readonly ${servicePropertyName}: ${serviceName}`);
|
||
}
|
||
});
|
||
|
||
if (constructorParams.length === 0) {
|
||
return ' constructor() {}';
|
||
}
|
||
|
||
return ` constructor(
|
||
${constructorParams.join(',\n')}
|
||
) {}`;
|
||
}
|
||
|
||
/**
|
||
* 验证控制器一致性
|
||
*/
|
||
validateControllerConsistency(javaController, nestJSController) {
|
||
const issues = [];
|
||
|
||
// 验证路由路径
|
||
if (javaController.routeInfo.controllerPath !== nestJSController.routeInfo.controllerPath) {
|
||
issues.push('控制器路径不一致');
|
||
}
|
||
|
||
// 验证方法数量
|
||
if (javaController.routeInfo.methods.length !== nestJSController.routeInfo.methods.length) {
|
||
issues.push('方法数量不一致');
|
||
}
|
||
|
||
// 验证每个方法
|
||
javaController.routeInfo.methods.forEach((javaMethod, index) => {
|
||
const nestJSMethod = nestJSController.routeInfo.methods[index];
|
||
if (nestJSMethod && javaMethod.path !== nestJSMethod.path) {
|
||
issues.push(`方法路径不一致: ${javaMethod.path} vs ${nestJSMethod.path}`);
|
||
}
|
||
});
|
||
|
||
return issues;
|
||
}
|
||
}
|
||
|
||
module.exports = ControllerGenerator;
|