- 重构LanguageUtils为LanguageService,实现ILanguageService接口 - 移除自定义验证管道和装饰器,使用标准NestJS验证 - 集成框架ValidatorService进行业务验证 - 简化目录结构,移除不必要的子目录 - 支持模块化语言包加载(common、user、order等) - 统一API响应格式(code、msg、data、timestamp) - 添加ValidationExceptionFilter处理多语言验证错误 - 完善多语言示例和文档
412 lines
12 KiB
JavaScript
412 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const BaseGenerator = require('./base-generator');
|
||
|
||
/**
|
||
* 🏗️ 实体生成器
|
||
* 专门负责生成NestJS实体文件
|
||
*/
|
||
class EntityGenerator extends BaseGenerator {
|
||
constructor() {
|
||
super('EntityGenerator');
|
||
|
||
this.config = {
|
||
phpBasePath: '/Users/wanwu/Documents/wwjcloud/wwjcloud-nsetjs/niucloud-php/niucloud',
|
||
nestjsBasePath: '/Users/wanwu/Documents/wwjcloud/wwjcloud-nsetjs/wwjcloud-nest/src/core',
|
||
discoveryResultPath: '/Users/wanwu/Documents/wwjcloud/wwjcloud-nsetjs/tools/php-discovery-result.json'
|
||
};
|
||
|
||
this.discoveryData = null;
|
||
this.entityStats = {
|
||
entitiesCreated: 0,
|
||
entitiesSkipped: 0
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 运行实体生成
|
||
*/
|
||
async run() {
|
||
try {
|
||
console.log('🏗️ 启动实体生成器...');
|
||
console.log('目标:生成NestJS实体文件\n');
|
||
|
||
// 加载PHP文件发现结果
|
||
await this.loadDiscoveryData();
|
||
|
||
// 生成实体
|
||
await this.generateEntities();
|
||
|
||
// 输出统计报告
|
||
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 generateEntities() {
|
||
console.log(' 🔨 生成实体...');
|
||
|
||
// 检查是否有模型数据
|
||
if (!this.discoveryData.models || Object.keys(this.discoveryData.models).length === 0) {
|
||
console.log(' ⚠️ 未发现PHP模型,跳过生成');
|
||
return;
|
||
}
|
||
|
||
for (const [moduleName, models] of Object.entries(this.discoveryData.models)) {
|
||
// 检查PHP项目是否有对应的模型目录
|
||
if (!this.hasPHPModels(moduleName)) {
|
||
console.log(` ⚠️ 模块 ${moduleName} 在PHP项目中无模型,跳过`);
|
||
continue;
|
||
}
|
||
|
||
for (const [modelName, modelInfo] of Object.entries(models)) {
|
||
await this.createEntity(moduleName, modelName, modelInfo);
|
||
this.stats.entitiesCreated++;
|
||
}
|
||
}
|
||
|
||
console.log(` ✅ 生成了 ${this.stats.entitiesCreated} 个实体`);
|
||
}
|
||
|
||
/**
|
||
* 创建实体
|
||
*/
|
||
async createEntity(moduleName, modelName, modelInfo) {
|
||
const entityPath = path.join(
|
||
this.config.nestjsBasePath,
|
||
moduleName,
|
||
'entity',
|
||
`${this.toKebabCase(modelName)}.entity.ts`
|
||
);
|
||
|
||
// 基于真实PHP model文件生成实体
|
||
const content = await this.generateEntityFromPHP(moduleName, modelName, modelInfo);
|
||
if (content) {
|
||
this.writeFile(entityPath, content, `Entity for ${moduleName}/${modelName}`);
|
||
this.entityStats.entitiesCreated++;
|
||
} else {
|
||
this.log(`跳过实体生成: ${moduleName}/${this.toKebabCase(modelName)}.entity.ts (无PHP源码)`, 'warning');
|
||
this.entityStats.entitiesSkipped++;
|
||
this.stats.filesSkipped++;
|
||
}
|
||
}
|
||
|
||
toKebabCase(str) {
|
||
return String(str)
|
||
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||
.replace(/_/g, '-')
|
||
.toLowerCase();
|
||
}
|
||
|
||
/**
|
||
* 基于PHP model文件生成实体
|
||
*/
|
||
async generateEntityFromPHP(moduleName, modelName, modelInfo) {
|
||
const className = this.toPascalCase(modelName) + 'Entity';
|
||
// 表名必须从PHP模型解析,禁止假设
|
||
let tableName = '';
|
||
|
||
// 尝试读取真实的PHP model文件
|
||
let fields = '';
|
||
let primaryKey = 'id';
|
||
let hasCustomPrimaryKey = false;
|
||
|
||
try {
|
||
const phpModelPath = path.join(this.config.phpBasePath, 'app/model', moduleName, `${modelName}.php`);
|
||
if (fs.existsSync(phpModelPath)) {
|
||
const phpContent = fs.readFileSync(phpModelPath, 'utf-8');
|
||
|
||
// 提取主键信息
|
||
const pkMatch = phpContent.match(/protected\s+\$pk\s*=\s*['"]([^'"]+)['"]/);
|
||
if (pkMatch) {
|
||
primaryKey = pkMatch[1];
|
||
hasCustomPrimaryKey = true;
|
||
}
|
||
|
||
fields = this.extractEntityFieldsFromPHP(phpContent, modelName);
|
||
// 从PHP模型解析表名
|
||
const nameMatch = phpContent.match(/protected\s+\$name\s*=\s*['"]([^'"]*)['"]/);
|
||
tableName = nameMatch ? nameMatch[1] : '';
|
||
console.log(` 📖 基于真实PHP model: ${phpModelPath}, 表名: ${tableName}`);
|
||
} else {
|
||
// 禁止假设,如果找不到PHP文件,不生成实体
|
||
console.log(` ❌ 未找到PHP model文件,跳过生成: ${phpModelPath}`);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
// 禁止假设,如果读取失败,不生成实体
|
||
console.log(` ❌ 读取PHP model文件失败,跳过生成: ${error.message}`);
|
||
return null;
|
||
}
|
||
|
||
// 生成主键字段
|
||
let primaryKeyField = '';
|
||
if (hasCustomPrimaryKey) {
|
||
// 基于真实PHP主键定义生成,禁止假设类型
|
||
primaryKeyField = ` @PrimaryColumn({ name: '${primaryKey}', type: 'int' })
|
||
${this.toCamelCase(primaryKey)}: number;`;
|
||
} else {
|
||
// 禁止假设主键,如果没有找到PHP主键定义,不生成主键字段
|
||
primaryKeyField = '';
|
||
console.log(` ⚠️ 未找到PHP主键定义,不生成主键字段: ${modelName}`);
|
||
}
|
||
|
||
return `import { Entity, PrimaryGeneratedColumn, PrimaryColumn, Column, Index } from 'typeorm';
|
||
import { BaseEntity } from '@wwjCommon/base/base.entity';
|
||
|
||
/**
|
||
* ${className} - 数据库实体
|
||
* 继承Core层BaseEntity,包含site_id、create_time等通用字段 (对应PHP Model继承BaseModel)
|
||
* 使用Core层基础设施:索引管理、性能监控
|
||
*/
|
||
@Entity('${tableName}')
|
||
export class ${className} extends BaseEntity {
|
||
${primaryKeyField}
|
||
${fields}
|
||
}`;
|
||
}
|
||
|
||
/**
|
||
* 从PHP内容中提取实体字段 - 基于真实PHP模型
|
||
*/
|
||
extractEntityFieldsFromPHP(phpContent, modelName) {
|
||
// 提取表名
|
||
const nameMatch = phpContent.match(/protected\s+\$name\s*=\s*['"]([^'"]*)['"]/);
|
||
const tableName = nameMatch ? nameMatch[1] : this.getTableName(modelName);
|
||
|
||
// 提取字段类型定义
|
||
const typeMatch = phpContent.match(/protected\s+\$type\s*=\s*\[([\s\S]*?)\];/);
|
||
const typeMap = {};
|
||
if (typeMatch) {
|
||
const typeContent = typeMatch[1];
|
||
const typeMatches = typeContent.match(/(['"][^'"]*['"])\s*=>\s*(['"][^'"]*['"])/g);
|
||
if (typeMatches) {
|
||
typeMatches.forEach(match => {
|
||
const fieldTypeMatch = match.match(/(['"][^'"]*['"])\s*=>\s*(['"][^'"]*['"])/);
|
||
if (fieldTypeMatch) {
|
||
const fieldName = fieldTypeMatch[1].replace(/['"]/g, '');
|
||
const fieldType = fieldTypeMatch[2].replace(/['"]/g, '');
|
||
typeMap[fieldName] = fieldType;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 提取软删除字段
|
||
const deleteTimeMatch = phpContent.match(/protected\s+\$deleteTime\s*=\s*['"]([^'"]*)['"]/);
|
||
const deleteTimeField = deleteTimeMatch ? deleteTimeMatch[1] : 'delete_time';
|
||
|
||
// 基于真实PHP模型结构生成字段
|
||
console.log(` 📖 解析PHP模型字段: ${modelName}, 表名: ${tableName}`);
|
||
|
||
// 解析PHP模型字段定义
|
||
const fields = this.parsePHPModelFields(phpContent, typeMap);
|
||
|
||
return fields;
|
||
}
|
||
|
||
/**
|
||
* 解析PHP模型字段定义
|
||
*/
|
||
parsePHPModelFields(phpContent, typeMap) {
|
||
const fields = [];
|
||
|
||
// 提取所有getter方法,这些通常对应数据库字段
|
||
const getterMatches = phpContent.match(/public function get(\w+)Attr\([^)]*\)[\s\S]*?\{[\s\S]*?\n\s*\}/g);
|
||
|
||
if (getterMatches) {
|
||
getterMatches.forEach(match => {
|
||
const nameMatch = match.match(/public function get(\w+)Attr/);
|
||
if (nameMatch) {
|
||
const fieldName = this.toCamelCase(nameMatch[1]);
|
||
const fieldType = this.determineFieldType(fieldName, typeMap);
|
||
|
||
fields.push(` @Column({ name: '${this.toSnakeCase(fieldName)}', type: '${fieldType}' })
|
||
${fieldName}: ${this.getTypeScriptType(fieldType)};`);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 如果没有找到getter方法,尝试从注释或其他地方提取字段信息
|
||
if (fields.length === 0) {
|
||
// 基于常见的数据库字段生成基础字段
|
||
const commonFields = [
|
||
{ name: 'title', type: 'varchar' },
|
||
{ name: 'name', type: 'varchar' },
|
||
{ name: 'type', type: 'varchar' },
|
||
{ name: 'value', type: 'text' },
|
||
{ name: 'is_default', type: 'tinyint' },
|
||
{ name: 'sort', type: 'int' },
|
||
{ name: 'status', type: 'tinyint' }
|
||
];
|
||
|
||
commonFields.forEach(field => {
|
||
if (phpContent.includes(field.name) || phpContent.includes(`'${field.name}'`)) {
|
||
fields.push(` @Column({ name: '${field.name}', type: '${field.type}' })
|
||
${this.toCamelCase(field.name)}: ${this.getTypeScriptType(field.type)};`);
|
||
}
|
||
});
|
||
}
|
||
|
||
return fields.join('\n\n');
|
||
}
|
||
|
||
/**
|
||
* 确定字段类型
|
||
*/
|
||
determineFieldType(fieldName, typeMap) {
|
||
if (typeMap[fieldName]) {
|
||
return typeMap[fieldName];
|
||
}
|
||
|
||
// 基于字段名推断类型
|
||
if (fieldName.includes('time') || fieldName.includes('date')) {
|
||
return 'timestamp';
|
||
} else if (fieldName.includes('id')) {
|
||
return 'int';
|
||
} else if (fieldName.includes('status') || fieldName.includes('is_')) {
|
||
return 'tinyint';
|
||
} else if (fieldName.includes('sort') || fieldName.includes('order')) {
|
||
return 'int';
|
||
} else {
|
||
return 'varchar';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取TypeScript类型
|
||
*/
|
||
getTypeScriptType(phpType) {
|
||
const typeMap = {
|
||
'varchar': 'string',
|
||
'text': 'string',
|
||
'int': 'number',
|
||
'tinyint': 'number',
|
||
'timestamp': 'Date',
|
||
'datetime': 'Date',
|
||
'json': 'object'
|
||
};
|
||
|
||
return typeMap[phpType] || 'string';
|
||
}
|
||
|
||
/**
|
||
* 转换为camelCase
|
||
*/
|
||
toCamelCase(str) {
|
||
return str.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
|
||
}
|
||
|
||
/**
|
||
* 转换为snake_case
|
||
*/
|
||
toSnakeCase(str) {
|
||
return str.replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, '');
|
||
}
|
||
|
||
/**
|
||
* 生成默认实体字段 - 禁止假设,仅返回空
|
||
*/
|
||
generateEntityFields(modelName) {
|
||
// 禁止假设字段,返回空字符串
|
||
// 所有字段必须基于真实PHP模型解析
|
||
console.log(` ⚠️ 禁止假设字段,请基于真实PHP模型: ${modelName}`);
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 获取表名
|
||
*/
|
||
getTableName(modelName) {
|
||
// 禁止假设表名,表名必须从PHP模型的$name属性获取
|
||
// 这里返回空字符串,强制从PHP源码解析
|
||
console.log(` ⚠️ 禁止假设表名,必须从PHP模型解析: ${modelName}`);
|
||
return '';
|
||
}
|
||
|
||
/**
|
||
* 转换为PascalCase - 处理连字符
|
||
*/
|
||
toPascalCase(str) {
|
||
return str.replace(/(^|-)([a-z])/g, (match, p1, p2) => p2.toUpperCase());
|
||
}
|
||
|
||
/**
|
||
* 转换为camelCase
|
||
*/
|
||
toCamelCase(str) {
|
||
return str.charAt(0).toLowerCase() + str.slice(1);
|
||
}
|
||
|
||
toPascalCase(str) {
|
||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||
}
|
||
|
||
/**
|
||
* 检查模块是否有PHP模型
|
||
*/
|
||
hasPHPModels(moduleName) {
|
||
const phpProjectPath = path.join(__dirname, '../../niucloud-php/niucloud');
|
||
const modelPath = path.join(phpProjectPath, 'app/model', moduleName);
|
||
|
||
if (!fs.existsSync(modelPath)) return false;
|
||
|
||
// 检查目录内是否有PHP文件
|
||
try {
|
||
const files = fs.readdirSync(modelPath);
|
||
return files.some(file => file.endsWith('.php'));
|
||
} catch (error) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 确保目录存在
|
||
*/
|
||
ensureDir(dirPath) {
|
||
if (!fs.existsSync(dirPath)) {
|
||
fs.mkdirSync(dirPath, { recursive: true });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 输出统计报告
|
||
*/
|
||
printStats() {
|
||
super.printStats({
|
||
'Entities Created': this.entityStats.entitiesCreated,
|
||
'Entities Skipped': this.entityStats.entitiesSkipped
|
||
});
|
||
}
|
||
}
|
||
|
||
// 如果直接运行此文件
|
||
if (require.main === module) {
|
||
const generator = new EntityGenerator();
|
||
generator.run().catch(console.error);
|
||
}
|
||
|
||
module.exports = EntityGenerator;
|