123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- #!/usr/bin/env node
- const fs = require('fs');
- 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:
- // 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.
- // Note: Only .xls format is supported for Excel files, not .xlsx
- const [,, jsonDir, xlsDir, packageName, outputDir, ...rest] = process.argv;
- if (!jsonDir || !xlsDir || !packageName || !outputDir) {
- console.log('Usage: node gen_pojo_from_xls_json.js <sourceJsonDir> <sourceXlsDir> <packageName> <outputDir> [Tables...] [--ignore=a,b,c]');
- process.exit(1);
- }
- function normalizeName(n) {
- return String(n).toLowerCase().replace(/\.[^.]+$/, '');
- }
- function parseArgs(args) {
- const whitelist = [];
- const blacklist = [];
- for (let i = 0; i < args.length; i++) {
- const a = String(args[i]);
- if (a.startsWith('--ignore=')) {
- const v = a.substring('--ignore='.length);
- v.split(',').map(s => s.trim()).filter(Boolean).forEach(x => blacklist.push(normalizeName(x)));
- continue;
- }
- if (a === '--ignore' && i + 1 < args.length) {
- const v = String(args[++i]);
- v.split(',').map(s => s.trim()).filter(Boolean).forEach(x => blacklist.push(normalizeName(x)));
- continue;
- }
- whitelist.push(normalizeName(a));
- }
- return { whitelist: new Set(whitelist.filter(Boolean)), blacklist: new Set(blacklist) };
- }
- const { whitelist: whiteSet, blacklist: blackSet } = parseArgs(rest);
- function listJsonFiles(dir) {
- return fs.readdirSync(dir)
- .filter(name => name.toLowerCase().endsWith('.json'))
- .map(name => path.join(dir, name));
- }
- function readJson(filePath) {
- let content = fs.readFileSync(filePath, 'utf8');
- if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
- return JSON.parse(content);
- }
- function pickSample(root) {
- if (Array.isArray(root)) return root.length > 0 ? root[0] : null;
- if (root && typeof root === 'object') {
- const keys = Object.keys(root);
- return keys.length > 0 ? root[keys[0]] : 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) {
- if (!str) return str;
- return str.charAt(0).toUpperCase() + str.slice(1);
- }
- // Convert filename like M_item.json -> MItem
- function toClassName(fileName) {
- const base = fileName.replace(/\.[^.]+$/, '');
- return base.split(/[_\-\s]+/).filter(Boolean).map(upperFirst).join('');
- }
- function 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) {
- if (value === null || value === undefined) return 'String';
- 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 === 'string') return 'String';
- if (Array.isArray(value)) {
- if (value.length === 0) return 'List<Object>';
- return `List<${inferJavaType(value[0])}>`;
- }
- if (typeof value === 'object') return 'Object';
- 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([
- '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'
- ]);
- function sanitizeFieldName(name) {
- let safe = name.replace(/[^A-Za-z0-9_]/g, '_');
- if (!/^[A-Za-z_]/.test(safe)) safe = '_' + safe;
- if (JAVA_KEYWORDS.has(safe)) safe = safe + '_';
- return safe;
- }
- function readXlsComments(xlsFile) {
- try {
- const wb = xlsx.readFile(xlsFile);
- const sheetName = wb.SheetNames[0];
- const ws = wb.Sheets[sheetName];
- const rows = xlsx.utils.sheet_to_json(ws, { header: 1, raw: false });
- // Heuristics: within first 5 rows, pick field row as the one with max ASCII ratio and contains 'ID' or many word tokens
- const maxCheck = Math.min(5, rows.length);
- let fieldRowIdx = -1, commentRowIdx = -1;
- let bestScore = -1;
- for (let i = 0; i < maxCheck; i++) {
- const row = rows[i] || [];
- const joined = row.join(' ').trim();
- if (!joined) continue;
- const asciiCount = (joined.match(/[\x20-\x7E]/g) || []).length;
- const ratio = asciiCount / Math.max(1, joined.length);
- const wordCount = (joined.match(/[A-Za-z_][A-Za-z0-9_]*/g) || []).length;
- const score = ratio * 0.7 + Math.min(wordCount / Math.max(1, row.length), 1) * 0.3 + (joined.includes('ID') ? 0.2 : 0);
- if (score > bestScore) { bestScore = score; fieldRowIdx = i; }
- }
- // Comment row: prefer a different row with lower ASCII ratio (more likely Chinese)
- let bestCR = -1;
- for (let i = 0; i < maxCheck; i++) {
- if (i === fieldRowIdx) continue;
- const row = rows[i] || [];
- const joined = row.join(' ').trim();
- if (!joined) continue;
- const asciiCount = (joined.match(/[\x20-\x7E]/g) || []).length;
- const ratio = asciiCount / Math.max(1, joined.length);
- const score = 1 - ratio; // prefer non-ASCII
- if (score > bestCR) { bestCR = score; commentRowIdx = i; }
- }
- const fields = rows[fieldRowIdx] || [];
- const comments = rows[commentRowIdx] || [];
- const map = new Map();
- for (let i = 0; i < fields.length; i++) {
- const k = String(fields[i] || '').trim();
- if (!k) continue;
- const v = String(comments[i] || '').trim();
- if (k) map.set(k, v);
- }
- return { sheetName, comments: map };
- } catch (e) {
- return { sheetName: '', comments: new Map() };
- }
- }
- function findXlsForBase(xlsDir, baseName) {
- // 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) {
- if (normalizeName(n) === normalizeName(baseName)) return path.join(xlsDir, n);
- }
- return null;
- }
- function generateJavaClass(pkg, className, fieldsOrder, fieldTypes, fieldComments, classComment, jsonFileName) {
- const lines = [];
- lines.push(`package ${pkg};`);
- lines.push('');
- lines.push('import lombok.Data;');
-
- // Only add imports that are actually used
- const usedTypes = new Set();
- for (const fname of fieldsOrder) {
- const type = fieldTypes.get(fname) || 'String';
- if (type === 'List<Integer>') {
- usedTypes.add('java.util.List');
- } else if (type === 'List<String>') {
- usedTypes.add('java.util.List');
- } else if (type === 'List<Object>') {
- usedTypes.add('java.util.List');
- } else if (type === 'List<BigDecimal>') {
- usedTypes.add('java.util.List');
- usedTypes.add('java.math.BigDecimal');
- } else if (type === 'List<Long>') {
- usedTypes.add('java.util.List');
- } else if (type === 'BigDecimal') {
- usedTypes.add('java.math.BigDecimal');
- }
- }
-
- // Add only the imports that are actually used
- usedTypes.forEach(importType => {
- lines.push(`import ${importType};`);
- });
-
- if (usedTypes.size > 0) {
- lines.push('');
- }
-
- if (classComment) {
- lines.push('/**');
- lines.push(` * 配置表:${classComment}`);
- lines.push(' */');
- }
- lines.push('@Data');
- lines.push(`public class ${className} {`);
-
- // Add path field using the actual JSON filename
- lines.push('\t/** JSON 配置文件路径 */');
- lines.push('\tpublic static final String path = "' + jsonFileName + '";');
- lines.push('');
-
- // Add fields
- for (const fname of fieldsOrder) {
- const type = fieldTypes.get(fname) || 'String';
- const cmt = fieldComments.get(fname);
- const safe = sanitizeFieldName(fname);
- if (cmt) {
- lines.push(`\t/** ${cmt} */`);
- }
- if (safe !== fname) {
- lines.push(`\t@JsonProperty("${fname}")`);
- }
- lines.push(`\tpublic ${type} ${safe};`);
- }
-
- lines.push('');
- lines.push('}');
- return lines.join('\n');
- }
- (function main() {
- if (!fs.existsSync(jsonDir)) {
- console.error('JSON dir not found:', jsonDir);
- process.exit(2);
- }
- if (!fs.existsSync(xlsDir)) {
- console.error('XLS dir not found:', xlsDir);
- process.exit(2);
- }
- const outPkgDir = path.join(outputDir, packageName.replace(/\./g, path.sep));
- fs.mkdirSync(outPkgDir, { recursive: true });
- let files = listJsonFiles(jsonDir);
- // Apply whitelist filter if provided
- if (whiteSet.size > 0) {
- files = files.filter(f => whiteSet.has(normalizeName(path.basename(f))));
- }
- // Apply blacklist filter if provided
- if (blackSet.size > 0) {
- files = files.filter(f => !blackSet.has(normalizeName(path.basename(f))));
- }
- if (files.length === 0) {
- console.log('No JSON files to process.');
- process.exit(0);
- }
- let generated = 0;
- for (const jf of files) {
- try {
- const base = path.basename(jf).replace(/\.[^.]+$/, '');
- const jsonRoot = readJson(jf);
- const sample = pickSample(jsonRoot);
- 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 { sheetName, comments } = xlsPath ? readXlsComments(xlsPath) : { sheetName: '', comments: new Map() };
- const className = toClassName(base) + 'Pojo';
- const fieldsOrder = Object.keys(sample);
- const fieldTypes = new Map();
-
- // 使用多样本分析进行类型推断
- 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 jsonFileName = path.basename(jf);
- const content = generateJavaClass(packageName, className, fieldsOrder, fieldTypes, comments, classComment, jsonFileName);
- const outPath = path.join(outPkgDir, `${className}.java`);
- fs.writeFileSync(outPath, content, 'utf8');
- generated++;
- console.log('Generated', outPath);
- } catch (e) {
- console.warn('Failed for', jf, e.message);
- }
- }
- console.log(`Done. Generated ${generated} classes.`);
- })();
|