- ✅ 成功运行迁移工具,生成28个模块的完整NestJS代码 - ✅ 生成所有实体、服务、控制器、验证器等组件 - ✅ 修复npm依赖冲突,更新package-lock.json - ✅ 添加Docker测试脚本和配置文件 - ✅ 完善迁移工具的调试日志和错误处理 - 🔧 包含增量更新工具和质量检查工具 - 📊 迁移统计:28个模块,数千个文件,耗时26.47秒 主要变更: - wwjcloud-nest/src/core/* - 生成的业务模块代码 - tools/* - 迁移工具和辅助脚本 - wwjcloud-nest/package.json - 依赖更新 - docker/* - 容器化配置和测试脚本
881 lines
30 KiB
JavaScript
881 lines
30 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
/**
|
||
* 🎮 控制器生成器
|
||
* 专门负责生成NestJS控制器
|
||
*/
|
||
class ControllerGenerator {
|
||
constructor() {
|
||
this.config = {
|
||
phpBasePath: '/Users/wanwu/Documents/wwjcloud/wwjcloud-nsetjs/niucloud-php/niucloud',
|
||
nestjsBasePath: '/Users/wanwu/Documents/wwjcloud/wwjcloud-nsetjs/wwjcloud-nest-v1/libs/wwjcloud-core/src',
|
||
discoveryResultPath: '/Users/wanwu/Documents/wwjcloud/wwjcloud-nsetjs/tools-v1/php-tools/php-discovery-result.json',
|
||
// 前端API模块优先级列表(基于PHP和Java前端API一致性)
|
||
frontendApiModules: [
|
||
'addon', 'aliapp', 'auth', 'cloud', 'dict', 'diy', 'diy_form', 'h5',
|
||
'home', 'member', 'module', 'notice', 'pay', 'pc', 'personal',
|
||
'poster', 'printer', 'site', 'stat', 'sys', 'tools', 'upgrade',
|
||
'user', 'verify', 'weapp', 'wechat', 'wxoplatform'
|
||
]
|
||
};
|
||
|
||
this.discoveryData = null;
|
||
this.stats = {
|
||
controllersCreated: 0,
|
||
errors: 0
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 运行控制器生成
|
||
*/
|
||
async run() {
|
||
try {
|
||
console.log('🎮 启动控制器生成器...');
|
||
console.log('目标:生成NestJS控制器文件\n');
|
||
|
||
// 加载PHP文件发现结果
|
||
await this.loadDiscoveryData();
|
||
|
||
// 生成控制器
|
||
await this.generateControllers();
|
||
|
||
// 输出统计报告
|
||
this.printStats();
|
||
|
||
} catch (error) {
|
||
console.error('❌ 控制器生成失败:', error);
|
||
this.stats.errors++;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载PHP文件发现结果
|
||
*/
|
||
async loadDiscoveryData() {
|
||
try {
|
||
const data = fs.readFileSync(this.config.discoveryResultPath, 'utf8');
|
||
this.discoveryData = JSON.parse(data);
|
||
console.log(' ✅ 成功加载PHP文件发现结果');
|
||
} catch (error) {
|
||
console.error('❌ 加载发现结果失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成控制器
|
||
*/
|
||
async generateControllers() {
|
||
console.log(' 🔨 生成控制器...');
|
||
|
||
// 检查是否有控制器数据
|
||
if (!this.discoveryData.controllers || Object.keys(this.discoveryData.controllers).length === 0) {
|
||
console.log(' ⚠️ 未发现PHP控制器,跳过生成');
|
||
return;
|
||
}
|
||
|
||
// 优先处理前端API模块
|
||
console.log(' 🎯 优先处理前端API模块(27个相同模块)...');
|
||
|
||
// 先处理前端API优先级模块
|
||
for (const moduleName of this.config.frontendApiModules) {
|
||
if (this.discoveryData.controllers[moduleName]) {
|
||
console.log(` 📋 处理前端API优先级模块: ${moduleName}`);
|
||
for (const [controllerName, controllerInfo] of Object.entries(this.discoveryData.controllers[moduleName])) {
|
||
const layer = controllerInfo.layer || 'adminapi';
|
||
await this.createController(moduleName, controllerName, controllerInfo, layer);
|
||
}
|
||
} else {
|
||
console.log(` ⚠️ 前端API模块 ${moduleName} 在后端控制器中不存在`);
|
||
}
|
||
}
|
||
|
||
// 再处理其他模块
|
||
for (const [moduleName, controllers] of Object.entries(this.discoveryData.controllers)) {
|
||
if (!this.isFrontendApiPriorityModule(moduleName)) {
|
||
console.log(` 📋 处理其他模块: ${moduleName}`);
|
||
for (const [controllerName, controllerInfo] of Object.entries(controllers)) {
|
||
const layer = controllerInfo.layer || 'adminapi';
|
||
await this.createController(moduleName, controllerName, controllerInfo, layer);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(` ✅ 生成了 ${this.stats.controllersCreated} 个控制器`);
|
||
}
|
||
|
||
/**
|
||
* 创建控制器
|
||
*/
|
||
async createController(moduleName, controllerName, controllerInfo, layer) {
|
||
const controllerPath = path.join(
|
||
this.config.nestjsBasePath,
|
||
moduleName,
|
||
'controllers',
|
||
layer,
|
||
`${this.toKebabCase(controllerName)}.controller.ts`
|
||
);
|
||
|
||
// 确保目录存在
|
||
this.ensureDir(path.dirname(controllerPath));
|
||
|
||
// 检查是否有对应的PHP控制器文件
|
||
const phpControllerPath = path.join(this.config.phpBasePath, 'app', layer, 'controller', moduleName, `${this.toPascalCase(controllerName)}.php`);
|
||
if (!fs.existsSync(phpControllerPath)) {
|
||
console.log(` ❌ 未找到PHP控制器文件,跳过生成: ${phpControllerPath}`);
|
||
return;
|
||
}
|
||
|
||
const content = this.generateControllerContent(moduleName, controllerName, layer);
|
||
fs.writeFileSync(controllerPath, content);
|
||
this.stats.controllersCreated += 1;
|
||
console.log(` ✅ 创建控制器: ${moduleName}/${layer}/${this.toKebabCase(controllerName)}.controller.ts`);
|
||
}
|
||
|
||
/**
|
||
* 生成控制器内容
|
||
*/
|
||
generateControllerContent(moduleName, controllerName, layer) {
|
||
const className = `${this.toPascalCase(controllerName)}Controller`;
|
||
|
||
// 基础设施导入和服务导入
|
||
const infraImports = this.getInfrastructureImports(layer);
|
||
const serviceImports = this.getServiceImports(moduleName, controllerName, layer);
|
||
|
||
// 过滤掉空的导入语句
|
||
const filteredServiceImports = serviceImports.split('\n').filter(line => {
|
||
const trimmed = line.trim();
|
||
return trimmed !== '' && !trimmed.includes("} from '';" );
|
||
}).join('\n');
|
||
|
||
const constructorServices = this.getConstructorServices(
|
||
moduleName,
|
||
this.toPascalCase(controllerName),
|
||
this.getCorrectServiceLayer(layer)
|
||
);
|
||
const routePrefix = this.getRoutePath(layer);
|
||
const guardsDecorator = this.generateGuardDecorators(layer);
|
||
|
||
// 解析PHP控制器方法生成NestJS方法
|
||
const methodsContent = this.parsePHPControllerMethods(
|
||
moduleName,
|
||
this.toPascalCase(controllerName),
|
||
layer
|
||
);
|
||
|
||
return `import { Controller, Get, Post, Put, Delete, Body, Param, Query, UseGuards, UploadedFile, UploadedFiles, UseInterceptors } from '@nestjs/common';
|
||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||
${infraImports}
|
||
${filteredServiceImports}
|
||
|
||
@ApiTags('${moduleName}')
|
||
${guardsDecorator}
|
||
@Controller('${routePrefix}/${this.toKebabCase(moduleName)}')
|
||
export class ${className} {
|
||
constructor(
|
||
${constructorServices}
|
||
) {}
|
||
|
||
${methodsContent}
|
||
}
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* 检查模块是否为前端API优先级模块
|
||
*/
|
||
isFrontendApiPriorityModule(moduleName) {
|
||
return this.config.frontendApiModules.includes(moduleName);
|
||
}
|
||
|
||
/**
|
||
* 获取正确的服务层路径
|
||
*/
|
||
getCorrectServiceLayer(layer) {
|
||
if (layer === 'adminapi') return 'admin';
|
||
if (layer === 'api') return 'api';
|
||
if (layer === 'core') return 'core';
|
||
return layer;
|
||
}
|
||
|
||
/**
|
||
* 获取路由路径
|
||
*/
|
||
getRoutePath(layer) {
|
||
if (layer === 'adminapi') return 'adminapi';
|
||
if (layer === 'api') return 'api';
|
||
return layer;
|
||
}
|
||
|
||
/**
|
||
* 获取基础设施导入
|
||
*/
|
||
getInfrastructureImports(layer) {
|
||
const imports = [];
|
||
|
||
// 根据层类型选择合适的基础设施
|
||
if (layer === 'adminapi') {
|
||
imports.push("import { AuthGuard } from '@wwjCommon/auth/auth.guard';");
|
||
imports.push("import { RbacGuard } from '@wwjCommon/auth/rbac.guard';");
|
||
imports.push("import { Roles } from '@wwjCommon/auth/decorators';");
|
||
} else if (layer === 'api') {
|
||
imports.push("import { AuthGuard } from '@wwjCommon/auth/auth.guard';");
|
||
}
|
||
|
||
// 通用基础设施
|
||
imports.push("import { Public } from '@wwjCommon/auth/decorators';");
|
||
imports.push("import { BadRequestException } from '@nestjs/common';");
|
||
|
||
// 常用装饰器说明(已在主import中引入)
|
||
imports.push("// @UploadedFile() - 单文件上传,配合 @UseInterceptors(FileInterceptor('file'))");
|
||
imports.push("// @UploadedFiles() - 多文件上传,配合 @UseInterceptors(FilesInterceptor('files'))");
|
||
imports.push("// @Session() - 获取会话对象,对应PHP Session::get()");
|
||
imports.push("// @Req() - 获取Request对象,对应PHP Request");
|
||
|
||
return imports.join('\n');
|
||
}
|
||
|
||
/**
|
||
* 获取基础设施装饰器
|
||
*/
|
||
getInfrastructureDecorators(layer) {
|
||
if (layer === 'adminapi') {
|
||
} else if (layer === 'api') {
|
||
return '@UseGuards(ApiCheckTokenGuard)';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 生成守卫装饰器
|
||
*/
|
||
generateGuardDecorators(layer) {
|
||
if (layer === 'adminapi') {
|
||
return '@UseGuards(AuthGuard, RbacGuard)';
|
||
} else if (layer === 'api') {
|
||
return '@UseGuards(AuthGuard)';
|
||
}
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 生成返回类型
|
||
*/
|
||
generateReturnType(serviceMethodName) {
|
||
if (serviceMethodName) {
|
||
return ': Promise<ApiResponse>';
|
||
}
|
||
return ': Promise<any>';
|
||
}
|
||
|
||
/**
|
||
* 从PHP路由文件提取API信息(HTTP方法 + 路径)
|
||
*/
|
||
extractRoutesFromPHP(moduleName, controllerName, layer) {
|
||
const routeFilePath = path.join(this.config.phpBasePath, 'app', layer, 'route', `${moduleName}.php`);
|
||
|
||
if (!fs.existsSync(routeFilePath)) {
|
||
console.log(` ⚠️ 未找到路由文件: ${routeFilePath}`);
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
const routeContent = fs.readFileSync(routeFilePath, 'utf-8');
|
||
const routes = [];
|
||
|
||
// 提取Route::group内的内容
|
||
const groupMatch = routeContent.match(/Route::group\(['"]([^'"]+)['"],\s*function\s*\(\)\s*\{([\s\S]*?)\}\)/);
|
||
if (groupMatch) {
|
||
const groupPrefix = groupMatch[1];
|
||
const groupContent = groupMatch[2];
|
||
|
||
// 匹配所有路由定义: Route::方法('路径', '控制器.方法')
|
||
const routePattern = /Route::(\w+)\(['"]([^'"]+)['"],\s*['"]([^'"]+)['"]\)/g;
|
||
let match;
|
||
|
||
while ((match = routePattern.exec(groupContent)) !== null) {
|
||
const httpMethod = match[1]; // get, post, put, delete
|
||
const routePath = match[2]; // 如 'site/:id'
|
||
const controllerMethod = match[3]; // 如 'site.Site/lists'
|
||
|
||
// 解析控制器方法: 'site.Site/lists' -> moduleName='site', controller='Site', method='lists'
|
||
const methodParts = controllerMethod.split('/');
|
||
if (methodParts.length === 2) {
|
||
const controllerPart = methodParts[0]; // 'site.Site'
|
||
const methodName = methodParts[1]; // 'lists'
|
||
const controllerParts = controllerPart.split('.');
|
||
|
||
if (controllerParts.length === 2) {
|
||
const routeModule = controllerParts[0]; // 'site'
|
||
const routeController = controllerParts[1]; // 'Site'
|
||
|
||
// 只提取当前控制器的路由
|
||
if (routeModule === moduleName && routeController === controllerName) {
|
||
routes.push({
|
||
httpMethod: httpMethod.toLowerCase(),
|
||
routePath: routePath,
|
||
methodName: methodName
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return routes;
|
||
|
||
} catch (error) {
|
||
console.log(` ❌ 解析路由文件失败: ${error.message}`);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析PHP控制器方法
|
||
* 结合路由信息和控制器方法体
|
||
*/
|
||
parsePHPControllerMethods(moduleName, controllerName, layer) {
|
||
// 1. 先从路由文件提取API信息(HTTP方法 + 路径)
|
||
const routes = this.extractRoutesFromPHP(moduleName, controllerName, layer);
|
||
|
||
if (routes.length > 0) {
|
||
console.log(` ✅ 从路由文件提取到 ${routes.length} 个API`);
|
||
}
|
||
|
||
// 2. 读取PHP控制器文件
|
||
const phpControllerPath = path.join(this.config.phpBasePath, 'app', layer, 'controller', moduleName, `${controllerName}.php`);
|
||
|
||
if (!fs.existsSync(phpControllerPath)) {
|
||
return ' // 未找到PHP控制器文件,无法解析方法';
|
||
}
|
||
|
||
try {
|
||
const phpContent = fs.readFileSync(phpControllerPath, 'utf-8');
|
||
|
||
// 3. 提取所有public方法
|
||
const methodMatches = phpContent.match(/public function (\w+)\([^)]*\)\s*\{[\s\S]*?\n\s*\}/g);
|
||
|
||
if (!methodMatches || methodMatches.length === 0) {
|
||
return ' // 未找到PHP控制器方法';
|
||
}
|
||
|
||
// 4. 为每个方法匹配路由信息
|
||
const methods = methodMatches.map(match => {
|
||
// 提取方法名
|
||
const nameMatch = match.match(/public function (\w+)\(/);
|
||
if (!nameMatch) return null;
|
||
|
||
const methodName = nameMatch[1];
|
||
|
||
// 从路由信息中查找该方法的路由配置
|
||
const routeInfo = routes.find(r => r.methodName === methodName);
|
||
|
||
// 如果没有路由信息,跳过此方法(说明没有对外暴露)
|
||
if (!routeInfo) {
|
||
console.log(` ⚠️ 方法 ${methodName} 没有对应的路由,跳过生成`);
|
||
return null;
|
||
}
|
||
|
||
// 提取方法注释
|
||
const commentMatch = phpContent.match(new RegExp(`/\\*\\*[\\s\\S]*?\\*/\\s*public function ${methodName}`));
|
||
const description = commentMatch ? this.extractDescription(commentMatch[0]) : methodName;
|
||
|
||
// 使用路由信息中的HTTP方法
|
||
const httpMethod = routeInfo.httpMethod.charAt(0).toUpperCase() + routeInfo.httpMethod.slice(1);
|
||
|
||
// 生成NestJS方法(传入路由信息)
|
||
return this.generateNestJSControllerMethod(methodName, description, httpMethod, match, moduleName, controllerName, layer, routeInfo);
|
||
}).filter(method => method !== null);
|
||
|
||
return methods.join('\n\n');
|
||
|
||
} catch (error) {
|
||
console.log(` ❌ 解析PHP控制器失败: ${error.message}`);
|
||
return ' // 解析PHP控制器失败';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 提取方法描述
|
||
*/
|
||
extractDescription(comment) {
|
||
const descMatch = comment.match(/@description\s+(.+)/);
|
||
return descMatch ? descMatch[1].trim() : '';
|
||
}
|
||
|
||
/**
|
||
* 确定HTTP方法
|
||
*/
|
||
determineHttpMethod(methodName) {
|
||
if (methodName.startsWith('get') || methodName.startsWith('list') || methodName.startsWith('info')) {
|
||
return 'Get';
|
||
} else if (methodName.startsWith('add') || methodName.startsWith('create') || methodName.startsWith('set')) {
|
||
return 'Post';
|
||
} else if (methodName.startsWith('edit') || methodName.startsWith('update') || methodName.startsWith('modify')) {
|
||
return 'Put';
|
||
} else if (methodName.startsWith('del') || methodName.startsWith('delete') || methodName.startsWith('remove')) {
|
||
return 'Delete';
|
||
} else {
|
||
return 'Post'; // 默认使用Post
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成NestJS控制器方法(严格适配V1,不解析PHP方法体、不做兼容推断)
|
||
*/
|
||
generateNestJSControllerMethod(methodName, description, httpMethod, phpMethod, moduleName, controllerName, layer, routeInfo = null) {
|
||
// 路由以发现结果为准,缺失则使用方法名的kebab-case
|
||
const routePath = routeInfo ? routeInfo.routePath : this.toKebabCase(methodName);
|
||
|
||
// 路径参数(如 :id, :uid)
|
||
const pathParams = routePath.match(/:(\w+)/g) || [];
|
||
const hasPathParams = pathParams.length > 0;
|
||
|
||
// 服务实例名仅由控制器名派生,保持V1注入一致性
|
||
const serviceInstanceName = this.getServiceInstanceName('', controllerName);
|
||
|
||
// 服务方法名与控制器方法名保持一致(严格映射)
|
||
const serviceMethodName = methodName;
|
||
|
||
// 生成参数列表(路径参数 + Query/Body)
|
||
const paramsList = [];
|
||
|
||
// 1) 路径参数
|
||
if (hasPathParams) {
|
||
pathParams.forEach(param => {
|
||
const paramName = param.substring(1);
|
||
paramsList.push(`@Param('${paramName}') ${paramName}: string`);
|
||
});
|
||
}
|
||
|
||
// 2) Query 或 Body(按HTTP方法严格区分)
|
||
if (httpMethod === 'Post' || httpMethod === 'Put') {
|
||
paramsList.push(`@Body() data: any`);
|
||
} else {
|
||
paramsList.push(`@Query() query: any`);
|
||
}
|
||
|
||
const methodParams = paramsList.join(', ');
|
||
|
||
// 路由信息注释
|
||
const routeComment = routeInfo
|
||
? ` * 路由: ${routeInfo.httpMethod.toUpperCase()} ${routePath}\n * PHP路由: Route::${routeInfo.httpMethod}('${routePath}', '${moduleName}.${controllerName}/${methodName}')`
|
||
: ` * 基于PHP控制器方法: ${methodName}`;
|
||
|
||
// 守卫装饰器与返回类型
|
||
const guardDecorators = this.generateGuardDecorators(layer);
|
||
const returnType = this.generateReturnType(serviceMethodName);
|
||
|
||
// 生成服务调用参数(严格:路径参数 + query/data)
|
||
const paramNames = pathParams.map(p => p.substring(1));
|
||
const callParams = (httpMethod === 'Post' || httpMethod === 'Put')
|
||
? [...paramNames, 'data'].join(', ')
|
||
: [...paramNames, 'query'].join(', ');
|
||
|
||
return ` /**
|
||
* ${description || methodName}
|
||
${routeComment}
|
||
*/
|
||
@${httpMethod}('${routePath}')
|
||
${guardDecorators}
|
||
@ApiOperation({ summary: ${JSON.stringify(description || methodName)} })
|
||
${methodName}(${methodParams})${returnType} {
|
||
try {
|
||
return this.${serviceInstanceName}.${serviceMethodName}(${callParams});
|
||
} catch (error) {
|
||
throw new BadRequestException('${methodName}操作失败', error);
|
||
}
|
||
}`;
|
||
}
|
||
|
||
/**
|
||
* 解析PHP方法调用
|
||
* 提取服务方法名和参数
|
||
*/
|
||
parsePHPMethodCall(phpMethod) {
|
||
// 安全默认值,避免 undefined 导致异常
|
||
let serviceMethodName = '';
|
||
let params = [];
|
||
let isDirectReturn = false;
|
||
|
||
try {
|
||
// 判断是否直接 return(非 success 包装)
|
||
isDirectReturn = /return\s+(?!success\()/i.test(phpMethod);
|
||
|
||
// 提取服务调用(支持 new XxxService()->method(...) 或 XxxService::method(...))
|
||
const callMatches = [...phpMethod.matchAll(/(?:new\s+\w+Service\s*\([^)]*\)\s*->|\w+Service::)\s*(\w+)\s*\(([^)]*)\)/g)];
|
||
if (callMatches.length > 0) {
|
||
const lastCall = callMatches[callMatches.length - 1];
|
||
serviceMethodName = lastCall[1] || '';
|
||
const rawParams = (lastCall[2] || '').trim();
|
||
if (rawParams) {
|
||
const tokens = rawParams.split(',').map(p => p.trim()).filter(Boolean);
|
||
params = tokens.map(token => {
|
||
// 支持 $data['search']、$id、'constant' 等形式
|
||
const arrMatch = token.match(/\$(\w+)\['([^']+)'\]/);
|
||
if (arrMatch) {
|
||
return { name: arrMatch[2], type: 'any' };
|
||
}
|
||
const varMatch = token.match(/\$(\w+)/);
|
||
if (varMatch) {
|
||
return { name: varMatch[1], type: 'any' };
|
||
}
|
||
const strMatch = token.match(/['"]([^'\"]+)['"]/);
|
||
if (strMatch) {
|
||
return { name: strMatch[1], type: 'any' };
|
||
}
|
||
return { name: token, type: 'any' };
|
||
});
|
||
}
|
||
}
|
||
|
||
// 额外:解析 $this->request->params([['field', default], ...]) 获得查询字段
|
||
const paramsBlockMatch = phpMethod.match(/params\(\[([\s\S]*?)\]\)/);
|
||
if (paramsBlockMatch) {
|
||
const block = paramsBlockMatch[1];
|
||
const fieldMatches = [...block.matchAll(/\[\s*['"]([^'\"]+)['"]\s*,\s*([^\]]*)\]/g)];
|
||
if (fieldMatches.length > 0) {
|
||
const fields = fieldMatches.map(m => {
|
||
const name = m[1];
|
||
const defaultRaw = (m[2] || '').trim();
|
||
const defaultValue = defaultRaw.replace(/['"]/g, '');
|
||
return { name, type: 'any', defaultValue };
|
||
});
|
||
// 合并(避免重复字段)
|
||
const seen = new Set(params.map(p => p.name));
|
||
fields.forEach(f => { if (!seen.has(f.name)) params.push(f); });
|
||
}
|
||
}
|
||
|
||
} catch (e) {
|
||
// 保持安全默认值
|
||
}
|
||
|
||
return { serviceMethodName, params, isDirectReturn };
|
||
}
|
||
|
||
/**
|
||
* 获取默认服务实例名
|
||
*/
|
||
getDefaultServiceInstanceName(controllerName) {
|
||
const baseName = controllerName.replace(/Controller$/, '');
|
||
const serviceInstanceName = this.toCamelCase(baseName);
|
||
// 这里不需要硬编码方法名,因为会在getConstructorServices中动态获取
|
||
return serviceInstanceName;
|
||
}
|
||
|
||
/**
|
||
* 获取第一个服务实例名
|
||
*/
|
||
getFirstServiceInstanceName(moduleName, controllerName, layer) {
|
||
// 解析PHP控制器中实际注入的服务,获取第一个服务的实例名
|
||
const phpProjectPath = path.join(__dirname, '../../niucloud-php/niucloud');
|
||
const controllerPath = path.join(phpProjectPath, 'app', layer, 'controller', moduleName, `${this.toPascalCase(controllerName)}.php`);
|
||
|
||
if (!fs.existsSync(controllerPath)) {
|
||
// 默认使用控制器名,避免与方法名重复
|
||
return this.getDefaultServiceInstanceName(controllerName);
|
||
}
|
||
|
||
try {
|
||
const content = fs.readFileSync(controllerPath, 'utf8');
|
||
|
||
// 解析 use 语句中的第一个服务
|
||
const useMatches = content.match(/use\s+app\\service\\([^;]+);/g);
|
||
if (useMatches && useMatches.length > 0) {
|
||
const firstUseMatch = useMatches[0];
|
||
const servicePath = firstUseMatch.match(/use\s+app\\service\\([^;]+);/)[1];
|
||
const parts = servicePath.split('\\');
|
||
const serviceName = parts[parts.length - 1];
|
||
|
||
// 生成服务实例名:CoreAddonService -> coreAddon
|
||
return this.toCamelCase(serviceName.replace('Service', ''));
|
||
}
|
||
|
||
// 如果没有找到服务,使用控制器名,避免与方法名重复
|
||
return this.getDefaultServiceInstanceName(controllerName);
|
||
} catch (error) {
|
||
// 默认使用控制器名,避免与方法名重复
|
||
return this.getDefaultServiceInstanceName(controllerName);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取PHP服务文件路径
|
||
*/
|
||
getPHPServicePath(moduleName, serviceMethodName) {
|
||
// 这里需要根据实际的服务文件结构来解析
|
||
// 暂时返回null,避免复杂的路径解析
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 检查PHP服务方法是否需要参数
|
||
*/
|
||
checkPHPServiceMethodNeedsParams(phpServicePath, serviceMethodName) {
|
||
// 这里需要解析PHP服务文件来检查方法签名
|
||
// 暂时返回false,避免复杂的PHP解析
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 生成数据提取代码
|
||
* 解析PHP中的 $this->request->params() 调用
|
||
*/
|
||
generateDataExtraction(phpMethod) {
|
||
// 转换PHP语法到TypeScript
|
||
let convertedMethod = phpMethod
|
||
.replace(/::/g, '.') // PHP静态调用 -> TypeScript属性访问
|
||
.replace(/\$this->/g, 'this.') // PHP实例调用 -> TypeScript实例调用
|
||
.replace(/\$(\w+)/g, '$1') // PHP变量 -> TypeScript变量
|
||
.replace(/new\s+(\w+Service)\([^)]*\)/g, (match, serviceName) => {
|
||
// 将ServiceName转换为serviceName (camelCase)
|
||
const camelCaseName = serviceName.charAt(0).toLowerCase() + serviceName.slice(1).replace('Service', '');
|
||
return `this.${camelCaseName}`;
|
||
})
|
||
.replace(/success\(/g, 'return ') // PHP成功返回 -> TypeScript返回
|
||
.replace(/error\(/g, 'throw new BadRequestException(') // PHP错误 -> TypeScript异常
|
||
.replace(/\[([^\]]+)\]/g, '{$1}') // PHP数组 -> TypeScript对象
|
||
.replace(/'([^']+)'\s*=>/g, '$1:') // PHP关联数组 -> TypeScript对象属性
|
||
.replace(/,\s*}/g, '}') // 移除尾随逗号
|
||
.replace(/,\s*]/g, ']'); // 移除尾随逗号
|
||
|
||
// 查找 $this->request->params() 调用
|
||
const paramsMatch = convertedMethod.match(/params\(\[([\s\S]*?)\]/);
|
||
if (paramsMatch) {
|
||
const paramsContent = paramsMatch[1];
|
||
// 解析参数列表
|
||
const paramLines = paramsContent.split('\n').map(line => line.trim()).filter(line => line);
|
||
const dataFields = [];
|
||
|
||
paramLines.forEach(line => {
|
||
// 匹配 [ 'field', default ] 格式,支持字符串和数字
|
||
const fieldMatch = line.match(/\[\s*['"]([^'"]+)['"]\s*,\s*([^'"]*)\s*\]/);
|
||
if (fieldMatch) {
|
||
const fieldName = fieldMatch[1];
|
||
const defaultValue = fieldMatch[2].trim();
|
||
// 避免重复字段
|
||
if (!dataFields.some(field => field.name === fieldName)) {
|
||
dataFields.push({ name: fieldName, default: defaultValue });
|
||
}
|
||
}
|
||
});
|
||
|
||
if (dataFields.length > 0) {
|
||
// 生成数据对象
|
||
const dataObject = dataFields.map(field => {
|
||
if (field.default === '0' || field.default === 'false') {
|
||
return `${field.name}: ${field.default}`;
|
||
} else if (field.default === 'true') {
|
||
return `${field.name}: ${field.default}`;
|
||
} else if (field.default === '') {
|
||
return `${field.name}: ''`;
|
||
} else if (field.default && !isNaN(field.default)) {
|
||
return `${field.name}: ${field.default}`;
|
||
} else {
|
||
return `${field.name}: ${field.default ? `'${field.default}'` : 'undefined'}`;
|
||
}
|
||
}).join(', ');
|
||
|
||
return `const data = { ${dataObject} };`;
|
||
}
|
||
}
|
||
|
||
// 如果没有找到params调用,但方法中使用了data变量,生成默认的data对象
|
||
if (convertedMethod.includes('$data[') || convertedMethod.includes('data[')) {
|
||
// 尝试从方法体中提取使用的字段
|
||
const fieldMatches = [...convertedMethod.matchAll(/data\[([^\]]+)\]/g)];
|
||
if (fieldMatches.length > 0) {
|
||
const fields = fieldMatches.map(match => {
|
||
const field = match[1].trim().replace(/['"]/g, '');
|
||
return field;
|
||
});
|
||
|
||
if (fields.length > 0) {
|
||
const dataObject = fields.map(field => `${field}: ''`).join(', ');
|
||
return `const data = { ${dataObject} };`;
|
||
}
|
||
}
|
||
return `const data = { search: '' };`;
|
||
}
|
||
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 转换为PascalCase
|
||
*/
|
||
toPascalCase(str) {
|
||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||
}
|
||
|
||
/**
|
||
* 转换为camelCase
|
||
*/
|
||
toCamelCase(str) {
|
||
return str.charAt(0).toLowerCase() + str.slice(1);
|
||
}
|
||
|
||
/**
|
||
* 转换为kebab-case(我们框架的标准命名格式)
|
||
*/
|
||
toKebabCase(str) {
|
||
return str
|
||
.replace(/([A-Z])/g, '-$1')
|
||
.replace(/^-/, '')
|
||
.toLowerCase();
|
||
}
|
||
|
||
/**
|
||
* 检查模块是否有PHP控制器
|
||
*/
|
||
hasPHPControllers(moduleName, layer) {
|
||
const phpProjectPath = path.join(__dirname, '../../niucloud-php/niucloud');
|
||
const controllerPath = path.join(phpProjectPath, 'app', layer, 'controller', moduleName);
|
||
|
||
if (!fs.existsSync(controllerPath)) return false;
|
||
|
||
// 检查目录内是否有PHP文件
|
||
try {
|
||
const files = fs.readdirSync(controllerPath);
|
||
return files.some(file => file.endsWith('.php'));
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 确保目录存在
|
||
*/
|
||
ensureDir(dirPath) {
|
||
if (!fs.existsSync(dirPath)) {
|
||
fs.mkdirSync(dirPath, { recursive: true });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 输出统计报告
|
||
*/
|
||
printStats() {
|
||
console.log('\n📊 控制器生成统计报告');
|
||
console.log('==================================================');
|
||
console.log(`✅ 创建控制器数量: ${this.stats.controllersCreated}`);
|
||
console.log(`❌ 错误数量: ${this.stats.errors}`);
|
||
console.log(`📈 成功率: ${this.stats.controllersCreated > 0 ? '100.00%' : '0.00%'}`);
|
||
}
|
||
|
||
/**
|
||
* 获取模块可用的服务层 - 基于发现结果
|
||
*/
|
||
getAvailableServiceLayers(moduleName) {
|
||
if (!this.discoveryData || !this.discoveryData.services) {
|
||
return [];
|
||
}
|
||
|
||
const layers = [];
|
||
const moduleServices = this.discoveryData.services[moduleName];
|
||
|
||
if (moduleServices) {
|
||
// 从发现结果中提取所有层级
|
||
const layerSet = new Set();
|
||
Object.values(moduleServices).forEach(service => {
|
||
if (service.layer) {
|
||
layerSet.add(service.layer);
|
||
}
|
||
});
|
||
layers.push(...layerSet);
|
||
}
|
||
|
||
return layers;
|
||
}
|
||
|
||
/**
|
||
* 智能选择最佳服务层
|
||
*/
|
||
selectBestServiceLayer(moduleName, controllerLayer) {
|
||
const availableLayers = this.getAvailableServiceLayers(moduleName);
|
||
|
||
// adminapi控制器:优先admin,其次core
|
||
if (controllerLayer === 'adminapi') {
|
||
if (availableLayers.includes('admin')) return 'admin';
|
||
if (availableLayers.includes('core')) return 'core';
|
||
}
|
||
|
||
// api控制器:优先api,其次core
|
||
if (controllerLayer === 'api') {
|
||
if (availableLayers.includes('api')) return 'api';
|
||
if (availableLayers.includes('core')) return 'core';
|
||
}
|
||
|
||
return null; // 没有可用的服务层
|
||
}
|
||
|
||
/**
|
||
* 查找服务文件 - 支持多层级查找
|
||
*/
|
||
findServiceFile(moduleName, serviceName, preferredLayers) {
|
||
const layers = preferredLayers || ['admin', 'api', 'core'];
|
||
|
||
for (const layer of layers) {
|
||
const servicePath = path.join(this.config.nestjsBasePath, moduleName, 'services', layer, `${this.toKebabCase(serviceName)}.service.ts`);
|
||
if (fs.existsSync(servicePath)) {
|
||
return { layer, path: servicePath };
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 生成服务导入语句
|
||
*/
|
||
getServiceImports(moduleName, controllerName, layer) {
|
||
try {
|
||
const pascal = this.toPascalCase(controllerName);
|
||
const kebab = this.toKebabCase(controllerName);
|
||
const serviceLayer = this.getCorrectServiceLayer(layer);
|
||
if (!serviceLayer) return '';
|
||
const importPath = `../services/${serviceLayer}/${kebab}.service`;
|
||
return `import { ${pascal}Service } from '${importPath}';`;
|
||
} catch (e) {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成构造函数注入的服务
|
||
*/
|
||
getConstructorServices(moduleName, controllerNamePascal, serviceLayer) {
|
||
const prop = this.toCamelCase(controllerNamePascal) + 'Service';
|
||
const type = `${controllerNamePascal}Service`;
|
||
return `private readonly ${prop}: ${type}`;
|
||
}
|
||
|
||
/**
|
||
* 获取服务实例名(与构造函数属性一致)
|
||
*/
|
||
getServiceInstanceName(phpMethod, controllerName) {
|
||
try {
|
||
const serviceMatch = phpMethod && phpMethod.match(/new\s+(\w+)Service\s*\(/);
|
||
if (serviceMatch) {
|
||
const base = serviceMatch[1];
|
||
return this.toCamelCase(base) + 'Service';
|
||
}
|
||
} catch {}
|
||
const baseName = (controllerName || '').replace(/Controller$/, '');
|
||
return this.toCamelCase(baseName) + 'Service';
|
||
}
|
||
}
|
||
|
||
// 如果直接运行此文件
|
||
if (require.main === module) {
|
||
const generator = new ControllerGenerator();
|
||
generator.run().catch(console.error);
|
||
}
|
||
|
||
module.exports = ControllerGenerator;
|