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;
|