|
@@ -2,9 +2,21 @@
|
|
const fs = require('fs');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const path = require('path');
|
|
|
|
|
|
|
|
+// 尝试加载xlsx模块,如果不存在则提供友好的错误信息
|
|
|
|
+// 注意:xlsx模块支持读取.xls格式文件
|
|
|
|
+let xlsx;
|
|
|
|
+try {
|
|
|
|
+ xlsx = require('xlsx');
|
|
|
|
+} catch (e) {
|
|
|
|
+ console.error('Error: xlsx module not found. Please install it with: npm install xlsx');
|
|
|
|
+ console.error('Note: This script only supports .xls format files, not .xlsx');
|
|
|
|
+ process.exit(1);
|
|
|
|
+}
|
|
|
|
+
|
|
// Usage:
|
|
// Usage:
|
|
// node gen_pojo_from_xls_json.js <sourceJsonDir> <sourceXlsDir> <packageName> <outputDir> [Tables...] [--ignore=a,b,c]
|
|
// node gen_pojo_from_xls_json.js <sourceJsonDir> <sourceXlsDir> <packageName> <outputDir> [Tables...] [--ignore=a,b,c]
|
|
// Tables can be names with or without .json/.xls, case-insensitive. Multiple can be space- or comma-separated.
|
|
// Tables can be names with or without .json/.xls, case-insensitive. Multiple can be space- or comma-separated.
|
|
|
|
+// Note: Only .xls format is supported for Excel files, not .xlsx
|
|
const [,, jsonDir, xlsDir, packageName, outputDir, ...rest] = process.argv;
|
|
const [,, jsonDir, xlsDir, packageName, outputDir, ...rest] = process.argv;
|
|
|
|
|
|
if (!jsonDir || !xlsDir || !packageName || !outputDir) {
|
|
if (!jsonDir || !xlsDir || !packageName || !outputDir) {
|
|
@@ -59,6 +71,17 @@ function pickSample(root) {
|
|
return null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+function analyzeAllSamples(root) {
|
|
|
|
+ if (Array.isArray(root)) {
|
|
|
|
+ return root; // 分析所有样本
|
|
|
|
+ }
|
|
|
|
+ if (root && typeof root === 'object') {
|
|
|
|
+ const keys = Object.keys(root);
|
|
|
|
+ return keys.map(k => root[k]); // 分析所有样本
|
|
|
|
+ }
|
|
|
|
+ return [];
|
|
|
|
+}
|
|
|
|
+
|
|
function upperFirst(str) {
|
|
function upperFirst(str) {
|
|
if (!str) return str;
|
|
if (!str) return str;
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
@@ -74,9 +97,35 @@ function isInteger(num) {
|
|
return typeof num === 'number' && Number.isInteger(num);
|
|
return typeof num === 'number' && Number.isInteger(num);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+function isLong(num) {
|
|
|
|
+ return typeof num === 'number' && Number.isInteger(num) && (num > 2147483647 || num < -2147483648);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+function isFloat(num) {
|
|
|
|
+ return typeof num === 'number' && !Number.isInteger(num);
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 检查字符串是否包含小数点(用于识别BigDecimal类型)
|
|
|
|
+function hasDecimalPoint(str) {
|
|
|
|
+ return typeof str === 'string' && str.includes('.') && !isNaN(Number(str));
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 检查字符串是否为有效的数字格式
|
|
|
|
+function isValidNumberString(str) {
|
|
|
|
+ if (typeof str !== 'string') return false;
|
|
|
|
+ const trimmed = str.trim();
|
|
|
|
+ if (trimmed === '') return false;
|
|
|
|
+ return !isNaN(Number(trimmed)) && isFinite(Number(trimmed));
|
|
|
|
+}
|
|
|
|
+
|
|
function inferJavaType(value) {
|
|
function inferJavaType(value) {
|
|
if (value === null || value === undefined) return 'String';
|
|
if (value === null || value === undefined) return 'String';
|
|
- if (typeof value === 'number') return isInteger(value) ? 'Integer' : 'BigDecimal';
|
|
|
|
|
|
+ if (typeof value === 'number') {
|
|
|
|
+ if (isLong(value)) return 'Long';
|
|
|
|
+ if (isInteger(value)) return 'Integer';
|
|
|
|
+ if (isFloat(value)) return 'BigDecimal';
|
|
|
|
+ return 'BigDecimal';
|
|
|
|
+ }
|
|
if (typeof value === 'boolean') return 'Boolean';
|
|
if (typeof value === 'boolean') return 'Boolean';
|
|
if (typeof value === 'string') return 'String';
|
|
if (typeof value === 'string') return 'String';
|
|
if (Array.isArray(value)) {
|
|
if (Array.isArray(value)) {
|
|
@@ -87,6 +136,129 @@ function inferJavaType(value) {
|
|
return 'String';
|
|
return 'String';
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+function inferJavaTypeFromSamples(samples, fieldName) {
|
|
|
|
+ if (samples.length === 0) return 'String';
|
|
|
|
+
|
|
|
|
+ // 统计所有样本的类型
|
|
|
|
+ const typeStats = {
|
|
|
|
+ null: 0,
|
|
|
|
+ undefined: 0,
|
|
|
|
+ number: 0,
|
|
|
|
+ boolean: 0,
|
|
|
|
+ string: 0,
|
|
|
|
+ array: 0,
|
|
|
|
+ object: 0
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ const numberValues = [];
|
|
|
|
+ const stringValues = [];
|
|
|
|
+ const arrayValues = [];
|
|
|
|
+
|
|
|
|
+ for (const sample of samples) {
|
|
|
|
+ if (sample === null) {
|
|
|
|
+ typeStats.null++;
|
|
|
|
+ } else if (sample === undefined) {
|
|
|
|
+ typeStats.undefined++;
|
|
|
|
+ } else if (typeof sample === 'number') {
|
|
|
|
+ typeStats.number++;
|
|
|
|
+ numberValues.push(sample);
|
|
|
|
+ } else if (typeof sample === 'boolean') {
|
|
|
|
+ typeStats.boolean++;
|
|
|
|
+ } else if (typeof sample === 'string') {
|
|
|
|
+ typeStats.string++;
|
|
|
|
+ stringValues.push(sample);
|
|
|
|
+ } else if (Array.isArray(sample)) {
|
|
|
|
+ typeStats.array++;
|
|
|
|
+ arrayValues.push(sample);
|
|
|
|
+ } else if (typeof sample === 'object') {
|
|
|
|
+ typeStats.object++;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 找出主要类型
|
|
|
|
+ const total = samples.length;
|
|
|
|
+ const maxType = Object.keys(typeStats).reduce((a, b) => typeStats[a] > typeStats[b] ? a : b);
|
|
|
|
+ const maxRatio = typeStats[maxType] / total;
|
|
|
|
+
|
|
|
|
+ // 如果主要类型占比超过80%,使用该类型
|
|
|
|
+ if (maxRatio >= 0.8) {
|
|
|
|
+ switch (maxType) {
|
|
|
|
+ case 'number':
|
|
|
|
+ if (numberValues.length === 0) return 'Integer';
|
|
|
|
+ // 检查是否需要Long类型
|
|
|
|
+ const hasLong = numberValues.some(n => isLong(n));
|
|
|
|
+ const hasFloat = numberValues.some(n => isFloat(n));
|
|
|
|
+ if (hasLong) return 'Long';
|
|
|
|
+ if (hasFloat) return 'BigDecimal';
|
|
|
|
+ return 'Integer';
|
|
|
|
+ case 'boolean':
|
|
|
|
+ return 'Boolean';
|
|
|
|
+ case 'string':
|
|
|
|
+ // 检查字符串是否都是数字格式
|
|
|
|
+ const allNumeric = stringValues.every(s => isValidNumberString(s));
|
|
|
|
+ if (allNumeric && stringValues.length > 0) {
|
|
|
|
+ const allNumbers = stringValues.map(s => Number(s));
|
|
|
|
+ const hasLong = allNumbers.some(n => isLong(n));
|
|
|
|
+ const hasFloat = allNumbers.some(n => isFloat(n));
|
|
|
|
+ // 检查原始字符串是否包含小数点
|
|
|
|
+ const hasDecimalPointInStrings = stringValues.some(s => hasDecimalPoint(s));
|
|
|
|
+ if (hasLong) return 'Long';
|
|
|
|
+ if (hasFloat || hasDecimalPointInStrings) return 'BigDecimal';
|
|
|
|
+ return 'Integer';
|
|
|
|
+ }
|
|
|
|
+ return 'String';
|
|
|
|
+ case 'array':
|
|
|
|
+ if (arrayValues.length === 0) return 'List<Object>';
|
|
|
|
+ // 分析数组元素类型
|
|
|
|
+ const allArrayElements = [];
|
|
|
|
+ for (const arr of arrayValues) {
|
|
|
|
+ allArrayElements.push(...arr.slice(0, 3)); // 取前3个元素
|
|
|
|
+ }
|
|
|
|
+ if (allArrayElements.length === 0) return 'List<Object>';
|
|
|
|
+ const elementType = inferJavaTypeFromSamples(allArrayElements, fieldName + '_element');
|
|
|
|
+ return `List<${elementType}>`;
|
|
|
|
+ case 'object':
|
|
|
|
+ return 'Object';
|
|
|
|
+ default:
|
|
|
|
+ return 'String';
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 混合类型,优先选择数字类型(可能是字符串形式的数字)
|
|
|
|
+ if (typeStats.number > 0 && typeStats.string > 0) {
|
|
|
|
+ // 检查字符串是否都是有效数字
|
|
|
|
+ const allNumeric = stringValues.every(s => isValidNumberString(s));
|
|
|
|
+ if (allNumeric) {
|
|
|
|
+ const allNumbers = [...numberValues, ...stringValues.map(s => Number(s))];
|
|
|
|
+ const hasLong = allNumbers.some(n => isLong(n));
|
|
|
|
+ const hasFloat = allNumbers.some(n => isFloat(n));
|
|
|
|
+ // 检查原始字符串是否包含小数点
|
|
|
|
+ const hasDecimalPointInStrings = stringValues.some(s => hasDecimalPoint(s));
|
|
|
|
+ if (hasLong) return 'Long';
|
|
|
|
+ if (hasFloat || hasDecimalPointInStrings) return 'BigDecimal';
|
|
|
|
+ return 'Integer';
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 如果只有字符串类型,检查是否都是数字格式
|
|
|
|
+ if (typeStats.string > 0 && typeStats.number === 0) {
|
|
|
|
+ const allNumeric = stringValues.every(s => isValidNumberString(s));
|
|
|
|
+ if (allNumeric && stringValues.length > 0) {
|
|
|
|
+ const allNumbers = stringValues.map(s => Number(s));
|
|
|
|
+ const hasLong = allNumbers.some(n => isLong(n));
|
|
|
|
+ const hasFloat = allNumbers.some(n => isFloat(n));
|
|
|
|
+ // 检查原始字符串是否包含小数点
|
|
|
|
+ const hasDecimalPointInStrings = stringValues.some(s => hasDecimalPoint(s));
|
|
|
|
+ if (hasLong) return 'Long';
|
|
|
|
+ if (hasFloat || hasDecimalPointInStrings) return 'BigDecimal';
|
|
|
|
+ return 'Integer';
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 默认返回String
|
|
|
|
+ return 'String';
|
|
|
|
+}
|
|
|
|
+
|
|
const JAVA_KEYWORDS = new Set([
|
|
const JAVA_KEYWORDS = new Set([
|
|
'abstract','assert','boolean','break','byte','case','catch','char','class','const','continue','default','do','double','else','enum','extends','final','finally','float','for','goto','if','implements','import','instanceof','int','interface','long','native','new','package','private','protected','public','return','short','static','strictfp','super','switch','synchronized','this','throw','throws','transient','try','void','volatile','while','null','true','false'
|
|
'abstract','assert','boolean','break','byte','case','catch','char','class','const','continue','default','do','double','else','enum','extends','final','finally','float','for','goto','if','implements','import','instanceof','int','interface','long','native','new','package','private','protected','public','return','short','static','strictfp','super','switch','synchronized','this','throw','throws','transient','try','void','volatile','while','null','true','false'
|
|
]);
|
|
]);
|
|
@@ -146,13 +318,12 @@ function readXlsComments(xlsFile) {
|
|
}
|
|
}
|
|
|
|
|
|
function findXlsForBase(xlsDir, baseName) {
|
|
function findXlsForBase(xlsDir, baseName) {
|
|
- // Try .xls then .xlsx
|
|
|
|
- const cands = [path.join(xlsDir, baseName + '.xls'), path.join(xlsDir, baseName + '.xlsx')];
|
|
|
|
- for (const f of cands) {
|
|
|
|
- if (fs.existsSync(f)) return f;
|
|
|
|
- }
|
|
|
|
- // Case-insensitive search fallback
|
|
|
|
- const files = fs.readdirSync(xlsDir).filter(n => /\.xlsx?$/.test(n));
|
|
|
|
|
|
+ // Only try .xls format
|
|
|
|
+ const xlsFile = path.join(xlsDir, baseName + '.xls');
|
|
|
|
+ if (fs.existsSync(xlsFile)) return xlsFile;
|
|
|
|
+
|
|
|
|
+ // Case-insensitive search fallback for .xls files only
|
|
|
|
+ const files = fs.readdirSync(xlsDir).filter(n => /\.xls$/i.test(n));
|
|
for (const n of files) {
|
|
for (const n of files) {
|
|
if (normalizeName(n) === normalizeName(baseName)) return path.join(xlsDir, n);
|
|
if (normalizeName(n) === normalizeName(baseName)) return path.join(xlsDir, n);
|
|
}
|
|
}
|
|
@@ -178,6 +349,8 @@ function generateJavaClass(pkg, className, fieldsOrder, fieldTypes, fieldComment
|
|
} else if (type === 'List<BigDecimal>') {
|
|
} else if (type === 'List<BigDecimal>') {
|
|
usedTypes.add('java.util.List');
|
|
usedTypes.add('java.util.List');
|
|
usedTypes.add('java.math.BigDecimal');
|
|
usedTypes.add('java.math.BigDecimal');
|
|
|
|
+ } else if (type === 'List<Long>') {
|
|
|
|
+ usedTypes.add('java.util.List');
|
|
} else if (type === 'BigDecimal') {
|
|
} else if (type === 'BigDecimal') {
|
|
usedTypes.add('java.math.BigDecimal');
|
|
usedTypes.add('java.math.BigDecimal');
|
|
}
|
|
}
|
|
@@ -255,12 +428,29 @@ function generateJavaClass(pkg, className, fieldsOrder, fieldTypes, fieldComment
|
|
const jsonRoot = readJson(jf);
|
|
const jsonRoot = readJson(jf);
|
|
const sample = pickSample(jsonRoot);
|
|
const sample = pickSample(jsonRoot);
|
|
if (!sample || typeof sample !== 'object') { console.warn('Skip (no object sample):', jf); continue; }
|
|
if (!sample || typeof sample !== 'object') { console.warn('Skip (no object sample):', jf); continue; }
|
|
|
|
+
|
|
|
|
+ // 分析所有样本以获得更准确的类型推断
|
|
|
|
+ const allSamples = analyzeAllSamples(jsonRoot);
|
|
|
|
+ if (allSamples.length === 0) { console.warn('Skip (no samples):', jf); continue; }
|
|
|
|
+
|
|
const xlsPath = findXlsForBase(xlsDir, base);
|
|
const xlsPath = findXlsForBase(xlsDir, base);
|
|
const { sheetName, comments } = xlsPath ? readXlsComments(xlsPath) : { sheetName: '', comments: new Map() };
|
|
const { sheetName, comments } = xlsPath ? readXlsComments(xlsPath) : { sheetName: '', comments: new Map() };
|
|
const className = toClassName(base) + 'Pojo';
|
|
const className = toClassName(base) + 'Pojo';
|
|
const fieldsOrder = Object.keys(sample);
|
|
const fieldsOrder = Object.keys(sample);
|
|
const fieldTypes = new Map();
|
|
const fieldTypes = new Map();
|
|
- for (const k of fieldsOrder) fieldTypes.set(k, inferJavaType(sample[k]));
|
|
|
|
|
|
+
|
|
|
|
+ // 使用多样本分析进行类型推断
|
|
|
|
+ for (const fieldName of fieldsOrder) {
|
|
|
|
+ const fieldSamples = allSamples.map(s => s[fieldName]).filter(v => v !== undefined);
|
|
|
|
+ const inferredType = inferJavaTypeFromSamples(fieldSamples, fieldName);
|
|
|
|
+ fieldTypes.set(fieldName, inferredType);
|
|
|
|
+
|
|
|
|
+ // 调试输出
|
|
|
|
+ if (fieldSamples.length > 0) {
|
|
|
|
+ console.log(` ${fieldName}: ${inferredType} (samples: ${fieldSamples.length}, types: ${[...new Set(fieldSamples.map(v => typeof v))].join(', ')})`);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
const classComment = sheetName ? `${base}(${sheetName})` : `${base}`;
|
|
const classComment = sheetName ? `${base}(${sheetName})` : `${base}`;
|
|
const jsonFileName = path.basename(jf);
|
|
const jsonFileName = path.basename(jf);
|
|
const content = generateJavaClass(packageName, className, fieldsOrder, fieldTypes, comments, classComment, jsonFileName);
|
|
const content = generateJavaClass(packageName, className, fieldsOrder, fieldTypes, comments, classComment, jsonFileName);
|