Newer
Older
DungeonShooting / DungeonShooting_ExcelTool / ExcelGenerator.cs
@lijincheng lijincheng on 21 Jun 2023 29 KB 修改导出excel表工具
using System.Text.Json;
using System.Text.RegularExpressions;
using NPOI.SS.UserModel;
using NPOI.XSSF.UserModel;

public static class ExcelGenerator
{
    private const string CodeOutPath = "src/config/";
    private const string JsonOutPath = "resource/config/";
    private const string ExcelFilePath = "excelFile";

    private static HashSet<string> _excelNames = new HashSet<string>();

    
    private enum CollectionsType
    {
        None,
        Array,
        Map
    }
    
    private class MappingData
    {

        public string TypeStr;
        public string TypeName;
        public CollectionsType CollectionsType;
        
        public bool IsRefExcel;
        public string RefTypeStr;
        public string RefTypeName;

        public MappingData(string typeStr, string typeName, CollectionsType collectionsType)
        {
            TypeStr = typeStr;
            TypeName = typeName;
            CollectionsType = collectionsType;
            IsRefExcel = false;
        }
        
        public MappingData(string typeStr, string typeName, CollectionsType collectionsType, string refTypeStr, string refTypeName)
        {
            TypeStr = typeStr;
            TypeName = typeName;
            CollectionsType = collectionsType;
            IsRefExcel = true;
            RefTypeStr = refTypeStr;
            RefTypeName = refTypeName;
        }
    }

    private class ExcelData
    {
        public string TableName;
        public string OutCode;
        public List<string> ColumnNames = new List<string>();
        public Dictionary<string, MappingData> ColumnMappingData = new Dictionary<string, MappingData>();
        public Dictionary<string, Type> ColumnType = new Dictionary<string, Type>();
        public List<Dictionary<string, object>> DataList = new List<Dictionary<string, object>>();
    }

    public static bool ExportExcel()
    {
        return ExportExcel(ExcelFilePath, JsonOutPath, CodeOutPath);
    }
    
    public static bool ExportExcel(string excelFilePath, string jsonOutPath, string codeOutPath)
    {
        Console.WriteLine("当前路径: " + Environment.CurrentDirectory);
        Console.WriteLine("excel路径: " + excelFilePath);
        Console.WriteLine("json输出路径: " + jsonOutPath);
        Console.WriteLine("cs代码输出路径: " + codeOutPath);
        try
        {
            var excelDataList = new List<ExcelData>();
            
            var directoryInfo = new DirectoryInfo(excelFilePath);
            if (directoryInfo.Exists)
            {
                var fileInfos = directoryInfo.GetFiles();
                //记录文件
                foreach (var fileInfo in fileInfos)
                {
                    var fileName = Path.GetFileNameWithoutExtension(fileInfo.Name).FirstToUpper();
                    _excelNames.Add(fileName);
                }
                //读取配置文件
                foreach (var fileInfo in fileInfos)
                {
                    if (fileInfo.Extension == ".xlsx")
                    {
                        if (fileInfo.Name == "ExcelConfig.xlsx")
                        {
                            throw new Exception("excel表文件名称不允许叫'ExcelConfig.xlsx'!");
                        }
                        Console.WriteLine("excel表: " + fileInfo.FullName);
                        excelDataList.Add(ReadExcel(fileInfo.FullName));
                    }
                }
            }

            Console.WriteLine($"一共检测到excel表共{excelDataList.Count}张.");
            if (excelDataList.Count == 0)
            {
                return true;
            }
            
            if (Directory.Exists(codeOutPath))
            {
                Directory.Delete(codeOutPath, true);
            }
            if (Directory.Exists(jsonOutPath))
            {
                Directory.Delete(jsonOutPath, true);
            }
            Directory.CreateDirectory(codeOutPath);
            Directory.CreateDirectory(jsonOutPath);
            
            //保存配置和代码
            foreach (var excelData in excelDataList)
            {
                File.WriteAllText(codeOutPath + "ExcelConfig_" + excelData.TableName + ".cs", excelData.OutCode);
                var config = new JsonSerializerOptions();
                config.WriteIndented = true;
                File.WriteAllText(jsonOutPath + excelData.TableName + ".json", JsonSerializer.Serialize(excelData.DataList, config));
            }
            
            //生成加载代码
            var code = GeneratorInitCode(excelDataList);
            File.WriteAllText(codeOutPath + "ExcelConfig.cs", code);
        }
        catch (Exception e)
        {
            PrintError(e.ToString());
            return false;
        }

        return true;
    }

    private static string GeneratorInitCode(List<ExcelData> excelList)
    {
        var code = $"using System;\n";
        code += $"using System.Collections.Generic;\n";
        code += $"using System.Text.Json;\n";
        code += $"using Godot;\n";
        code += $"\n";
        code += $"namespace Config;\n";
        code += $"\n";
        code += $"public static partial class ExcelConfig\n";
        code += $"{{\n";

        var fieldCode = "";
        var callFuncCode = "";
        var callInitRefFuncCode = "";
        var funcCode = "";
        var initRefFuncCode = "";
        
        foreach (var excelData in excelList)
        {
            var idName = excelData.ColumnNames[0];
            var idTypeStr = excelData.ColumnMappingData[idName].TypeStr;
            
            //---------------------------- 引用其他表处理 ----------------------------
            var hasRefColumn = false;
            var refColumnNoneCode = "";
            foreach (var columnName in excelData.ColumnNames)
            {
                var mappingData = excelData.ColumnMappingData[columnName];
                if (mappingData.IsRefExcel)
                {
                    hasRefColumn = true;
                    if (mappingData.CollectionsType == CollectionsType.None)
                    {
                        refColumnNoneCode += $"                if (!string.IsNullOrEmpty(item.__{columnName}))\n";
                        refColumnNoneCode += $"                {{\n";
                        refColumnNoneCode += $"                    item.{columnName} = {mappingData.RefTypeName}_Map[item.__{columnName}];\n";
                        refColumnNoneCode += $"                }}\n";
                    }
                    else if (mappingData.CollectionsType == CollectionsType.Array)
                    {
                        refColumnNoneCode += $"                if (item.__{columnName} != null)\n";
                        refColumnNoneCode += $"                {{\n";
                        refColumnNoneCode += $"                    item.{columnName} = new {mappingData.RefTypeName}[item.__{columnName}.Length];\n";
                        refColumnNoneCode += $"                    for (var i = 0; i < item.__{columnName}.Length; i++)\n";
                        refColumnNoneCode += $"                    {{\n";
                        refColumnNoneCode += $"                        item.{columnName}[i] = {mappingData.RefTypeName}_Map[item.__{columnName}[i]];\n";
                        refColumnNoneCode += $"                    }}\n";
                        refColumnNoneCode += $"                }}\n";
                    }
                    else
                    {
                        refColumnNoneCode += $"                if (item.__{columnName} != null)\n";
                        refColumnNoneCode += $"                {{\n";
                        refColumnNoneCode += $"                    item.{columnName} = new {mappingData.RefTypeStr}();\n";
                        refColumnNoneCode += $"                    foreach (var pair in item.__{columnName})\n";
                        refColumnNoneCode += $"                    {{\n";
                        refColumnNoneCode += $"                        item.{columnName}.Add(pair.Key, {mappingData.RefTypeName}_Map[pair.Value]);\n";
                        refColumnNoneCode += $"                    }}\n";
                        refColumnNoneCode += $"                }}\n";
                    }
                    refColumnNoneCode += $"\n";
                }
            }
            
            //----------------------------- 数据集合 ------------------------------------
            fieldCode += $"    /// <summary>\n";
            fieldCode += $"    /// {excelData.TableName}.xlsx表数据集合, 以 List 形式存储, 数据顺序与 Excel 表相同\n";
            fieldCode += $"    /// </summary>\n";
            fieldCode += $"    public static List<{excelData.TableName}> {excelData.TableName}_List {{ get; private set; }}\n";
            fieldCode += $"    /// <summary>\n";
            fieldCode += $"    /// {excelData.TableName}.xlsx表数据集合, 里 Map 形式存储, key 为 {idName}\n";
            fieldCode += $"    /// </summary>\n";
            fieldCode += $"    public static Dictionary<{idTypeStr}, {excelData.TableName}> {excelData.TableName}_Map {{ get; private set; }}\n";
            fieldCode += $"\n";
            
            //------------------------------- 初始化函数 -------------------------------------
            callFuncCode += $"        _Init{excelData.TableName}Config();\n";
            
            funcCode += $"    private static void _Init{excelData.TableName}Config()\n";
            funcCode += $"    {{\n";
            funcCode += $"        try\n";
            funcCode += $"        {{\n";
            funcCode += $"            var text = _ReadConfigAsText(\"res://resource/config/{excelData.TableName}.json\");\n";
            if (hasRefColumn) //存在引用列
            {
                funcCode += $"            {excelData.TableName}_List = new List<{excelData.TableName}>(JsonSerializer.Deserialize<List<Ref_{excelData.TableName}>>(text));\n";
            }
            else
            {
                funcCode += $"            {excelData.TableName}_List = JsonSerializer.Deserialize<List<{excelData.TableName}>>(text);\n";
            }
            funcCode += $"            {excelData.TableName}_Map = new Dictionary<{idTypeStr}, {excelData.TableName}>();\n";
            funcCode += $"            foreach (var item in {excelData.TableName}_List)\n";
            funcCode += $"            {{\n";
            funcCode += $"                {excelData.TableName}_Map.Add(item.{idName}, item);\n";
            funcCode += $"            }}\n";
            funcCode += $"        }}\n";
            funcCode += $"        catch (Exception e)\n";
            funcCode += $"        {{\n";
            funcCode += $"            GD.PrintErr(e.ToString());\n";
            funcCode += $"            throw new Exception(\"初始化表'{excelData.TableName}'失败!\");\n";
            funcCode += $"        }}\n";
            funcCode += $"    }}\n";

            
            //------------------------------- 初始化引用 ---------------------------------
            if (hasRefColumn)
            {
                callInitRefFuncCode += $"        _Init{excelData.TableName}Ref();\n";

                initRefFuncCode += $"    private static void _Init{excelData.TableName}Ref()\n";
                initRefFuncCode += $"    {{\n";
                initRefFuncCode += $"        foreach (Ref_{excelData.TableName} item in {excelData.TableName}_List)\n";
                initRefFuncCode += $"        {{\n";
                initRefFuncCode += $"            try\n";
                initRefFuncCode += $"            {{\n";
                initRefFuncCode += refColumnNoneCode;
                initRefFuncCode += $"            }}\n";
                initRefFuncCode += $"            catch (Exception e)\n";
                initRefFuncCode += $"            {{\n";
                initRefFuncCode += $"                GD.PrintErr(e.ToString());\n";
                initRefFuncCode += $"                throw new Exception(\"初始化'{excelData.TableName}'引用其他表数据失败, 当前行id: \" + item.Id);\n";
                initRefFuncCode += $"            }}\n";
                initRefFuncCode += $"        }}\n";
                initRefFuncCode += $"    }}\n";
            }
        }

        code += fieldCode;
        code += $"\n";
        code += $"    private static bool _init = false;\n";
        code += $"    /// <summary>\n";
        code += $"    /// 初始化所有配置表数据\n";
        code += $"    /// </summary>\n";
        code += $"    public static void Init()\n";
        code += $"    {{\n";
        code += $"        if (_init) return;\n";
        code += $"        _init = true;\n";
        code += $"\n";
        code += callFuncCode;
        code += $"\n";
        code += callInitRefFuncCode;
        code += $"    }}\n";
        code += funcCode;
        code += $"\n";
        code += initRefFuncCode;
        code += $"    private static string _ReadConfigAsText(string path)\n";
        code += $"    {{\n";
        code += $"        var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);\n";
        code += $"        var asText = file.GetAsText();\n";
        code += $"        file.Dispose();\n";
        code += $"        return asText;\n";
        code += $"    }}\n";
        code += $"}}";
        return code;
    }
    
    private static ExcelData ReadExcel(string excelPath)
    {
        var excelData = new ExcelData();
        //文件名称
        var fileName = Path.GetFileNameWithoutExtension(excelPath).FirstToUpper();
        excelData.TableName = fileName;
        //输出代码
        var outStr = $"using System.Text.Json.Serialization;\n";
        outStr += $"using System.Collections.Generic;\n\n";
        outStr += $"namespace Config;\n\n";
        outStr += $"public static partial class ExcelConfig\n{{\n";
        outStr += $"    public class {fileName}\n";
        outStr += $"    {{\n";
        //继承的带有引用其他表的类代码
        var outRefStr = "";
        
        var cloneFuncStr = $"        /// <summary>\n";
        cloneFuncStr += $"        /// 返回浅拷贝出的新对象\n";
        cloneFuncStr += $"        /// </summary>\n";
        cloneFuncStr += $"        public {fileName} Clone()\n";
        cloneFuncStr += $"        {{\n";
        cloneFuncStr += $"            var inst = new {fileName}();\n";
        
        var sourceFile = excelPath;

        //行数
        var rowCount = -1;
        //列数
        var columnCount = -1;
        
        //加载表数据
        var workbook = new XSSFWorkbook(sourceFile);
        using (workbook)
        {
            var sheet1 = workbook.GetSheet("Sheet1");
            rowCount = sheet1.LastRowNum;
            //先解析表中的列名, 注释, 类型
            var names = sheet1.GetRow(0);
            var descriptions = sheet1.GetRow(1);
            var types = sheet1.GetRow(2);
            columnCount = names.LastCellNum;
            foreach (var cell in names)
            {
                //字段名称
                var field = GetCellStringValue(cell);
                if (string.IsNullOrEmpty(field))
                {
                    if (cell.ColumnIndex == 0)
                    {
                        throw new Exception($"表'{fileName}'的列数为0!");
                    }
                    //到达最后一列了
                    columnCount = cell.ColumnIndex;
                    break;
                }
                field = field.FirstToUpper();
                excelData.ColumnNames.Add(field);
                if (field == "Clone")
                {
                    throw new Exception($"表'{fileName}'中不允许有'Clone'字段!");
                }

                var descriptionCell = descriptions.GetCell(cell.ColumnIndex);
                //描述
                string description;
                if (descriptionCell != null)
                {
                    description = GetCellStringValue(descriptionCell).Replace("\n", " <br/>\n        /// ");
                }
                else
                {
                    description = "";
                }
                //类型
                var typeString = GetCellStringValue(types.GetCell(cell.ColumnIndex));
                if (string.IsNullOrEmpty(typeString))
                {
                    throw new Exception($"表'{fileName}'中'{field}'这一列类型为空!");
                }
                
                //尝试解析类型
                MappingData mappingData;
                try
                {
                    mappingData = ConvertToType(typeString.Replace(" ", ""));
                }
                catch (Exception e)
                {
                    PrintError(e.ToString());
                    throw new Exception($"表'{fileName}'中'{field}'这一列类型描述语法错误: {typeString}");
                }
                
                if (!excelData.ColumnMappingData.TryAdd(field, mappingData))
                {
                    throw new Exception($"表'{fileName}'中存在相同名称的列: '{field}'!");
                }
                outStr += $"        /// <summary>\n";
                outStr += $"        /// {description}\n";
                outStr += $"        /// </summary>\n";
                if (!mappingData.IsRefExcel) //没有引用其他表
                {
                    outStr += $"        [JsonInclude]\n";
                    outStr += $"        public {mappingData.TypeStr} {field};\n\n";
                }
                else
                {
                    outStr += $"        public {mappingData.RefTypeStr} {field};\n\n";
                }

                if (mappingData.IsRefExcel) //引用其他表
                {
                    if (string.IsNullOrEmpty(outRefStr))
                    {
                        outRefStr += $"    private class Ref_{fileName} : {fileName}\n";
                        outRefStr += $"    {{\n";
                    }
                    outRefStr += $"        [JsonInclude]\n";
                    outRefStr += $"        public {mappingData.TypeStr} __{field};\n\n";
                }
                
                cloneFuncStr += $"            inst.{field} = {field};\n";
            }
        
            cloneFuncStr += "            return inst;\n";
            cloneFuncStr += "        }\n";
            outStr += cloneFuncStr;
            outStr += "    }\n";

            if (!string.IsNullOrEmpty(outRefStr))
            {
                outRefStr += "    }\n";
                outStr += outRefStr;
            }
            
            outStr += "}";
            
            //解析字段类型
            foreach (var kv in excelData.ColumnMappingData)
            {
                var typeName = kv.Value.TypeName;
                var type = Type.GetType(typeName);
                if (type == null)
                {
                    throw new Exception($"表'{fileName}'中'{kv.Key}'这一列类型未知! " + kv.Value.TypeStr);
                }
                excelData.ColumnType.Add(kv.Key, type);
            }

            //解析数据
            for (int i = 3; i <= rowCount; i++)
            {
                Dictionary<string, object> data = null;
                var row = sheet1.GetRow(i);
                if (row == null)
                {
                    continue;
                }
                for (int j = 0; j < columnCount; j++)
                {
                    var cell = row.GetCell(j);
                    var strValue = GetCellStringValue(cell);
                    //如果这一行的第一列数据为空, 则跳过这一行
                    if (j == 0 && string.IsNullOrEmpty(strValue))
                    {
                        break;
                    }
                    else if (data == null)
                    {
                        data = new Dictionary<string, object>();
                        excelData.DataList.Add(data);
                    }

                    var fieldName = excelData.ColumnNames[j];
                    var mappingData = excelData.ColumnMappingData[fieldName];
                    var field = mappingData.IsRefExcel ? "__" + fieldName : fieldName;
                    try
                    {
                        switch (mappingData.TypeStr)
                        {
                            case "bool":
                            case "boolean":
                                data.Add(field, GetCellBooleanValue(cell));
                                break;
                            case "byte":
                                data.Add(field, Convert.ToByte(GetCellNumberValue(cell)));
                                break;
                            case "sbyte":
                                data.Add(field, Convert.ToSByte(GetCellNumberValue(cell)));
                                break;
                            case "short":
                                data.Add(field, Convert.ToInt16(GetCellNumberValue(cell)));
                                break;
                            case "ushort":
                                data.Add(field, Convert.ToUInt16(GetCellNumberValue(cell)));
                                break;
                            case "int":
                                data.Add(field, Convert.ToInt32(GetCellNumberValue(cell)));
                                break;
                            case "uint":
                                data.Add(field, Convert.ToUInt32(GetCellNumberValue(cell)));
                                break;
                            case "long":
                                data.Add(field, Convert.ToInt64(GetCellNumberValue(cell)));
                                break;
                            case "ulong":
                                data.Add(field, Convert.ToUInt64(GetCellNumberValue(cell)));
                                break;
                            case "float":
                                data.Add(field, Convert.ToSingle(GetCellNumberValue(cell)));
                                break;
                            case "double":
                                data.Add(field, GetCellNumberValue(cell));
                                break;
                            case "string":
                                data.Add(field, GetCellStringValue(cell));
                                break;
                            default:
                            {
                                var cellStringValue = GetCellStringValue(cell);
                                if (cellStringValue.Length == 0)
                                {
                                    data.Add(field, null);
                                }
                                else
                                {
                                    data.Add(field, JsonSerializer.Deserialize(cellStringValue, excelData.ColumnType[fieldName]));
                                }
                            }
                                break;
                        }
                    }
                    catch (Exception e)
                    {
                        PrintError(e.ToString());
                        throw new Exception($"解析表'{fileName}'第'{i + 1}'行第'{j + 1}'列数据时发生异常");
                    }
                }
            }
        }

        excelData.OutCode = outStr;
        return excelData;
    }

    private static string GetCellStringValue(ICell cell)
    {
        if (cell == null)
        {
            return "";
        }
        switch (cell.CellType)
        {
            case CellType.Numeric:
                return cell.NumericCellValue.ToString();
            case CellType.String:
                return cell.StringCellValue;
            case CellType.Formula:
                return cell.CellFormula;
            case CellType.Boolean:
                return cell.BooleanCellValue ? "true" : "false";
        }

        return "";
    }

    private static double GetCellNumberValue(ICell cell)
    {
        if (cell == null)
        {
            return 0;
        }

        return cell.NumericCellValue;
    }

    private static bool GetCellBooleanValue(ICell cell)
    {
        if (cell == null)
        {
            return false;
        }

        return cell.BooleanCellValue;
    }
    
    private static MappingData ConvertToType(string str, int depth = 0)
    {
        if (Regex.IsMatch(str, "^\\w+$"))
        {
            var typeStr = TypeStrMapping(str);
            var typeName = TypeNameMapping(str);
            return new MappingData(typeStr, typeName, CollectionsType.None);
        }
        else if (Regex.IsMatch(str, "^\\$\\w+$")) //引用其他表
        {
            var realName = str.Substring(1);
            if (!_excelNames.Contains(realName))
            {
                throw new Exception($"引用表数据失败, 未找到表: {realName}!");
            }

            if (depth > 1)
            {
                throw new Exception("引用表数据失败, 引用表数据仅支持放入第一层的数组和字典!");
            }

            return new MappingData(TypeStrMapping("string"), TypeNameMapping("string"), CollectionsType.None, realName, realName);
        }
        else if (str.StartsWith('{')) //字典
        {
            var tempStr = str.Substring(1, str.Length - 2);
            var index = tempStr.IndexOf(':');
            if (index == -1)
            {
                throw new Exception("类型描述语法错误!");
            }

            var keyStr = tempStr.Substring(0, index);
            if (!IsBaseType(keyStr))
            {
                throw new Exception($"字典key类型必须是基础类型!");
            }

            var type1 = ConvertToType(keyStr, depth + 1);
            var type2 = ConvertToType(tempStr.Substring(index + 1), depth + 1);

            var typeStr = $"Dictionary<{type1.TypeStr}, {type2.TypeStr}>";
            var typeName = $"System.Collections.Generic.Dictionary`2[[{type1.TypeName}],[{type2.TypeName}]]";

            if (type2.IsRefExcel) //引用过其他表
            {
                var refTypeStr = $"Dictionary<{type1.TypeStr}, {type2.RefTypeStr}>";
                return new MappingData(typeStr, typeName, CollectionsType.Map, refTypeStr, type2.RefTypeName);
            }

            return new MappingData(typeStr, typeName, CollectionsType.Map);
        }
        else if (str.StartsWith('[')) //数组
        {
            var tempStr = str.Substring(1, str.Length - 2);
            var typeData = ConvertToType(tempStr, depth + 1);
            var typeStr = typeData.TypeStr + "[]";
            var typeName = typeData.TypeName + "[]";

            if (typeData.IsRefExcel) //引用过其他表
            {
                var refTypeStr = typeData.RefTypeStr + "[]";
                return new MappingData(typeStr, typeName, CollectionsType.Array, refTypeStr, typeData.RefTypeName);
            }
            
            return new MappingData(typeStr, typeName, CollectionsType.Array);
        }
        throw new Exception("类型描述语法错误!");
    }
    
    private static string TypeStrMapping(string typeName)
    {
        switch (typeName)
        {
            case "boolean": return "bool";
            case "vector2": return "SerializeVector2";
            case "vector3": return "SerializeVector3";
            case "color": return "SerializeColor";
        }

        return typeName;
    }

    private static string TypeNameMapping(string typeName)
    {
        switch (typeName)
        {
            case "bool":
            case "boolean": return typeof(bool).FullName;
            case "byte": return typeof(byte).FullName;
            case "sbyte": return typeof(sbyte).FullName;
            case "short": return typeof(short).FullName;
            case "ushort": return typeof(ushort).FullName;
            case "int": return typeof(int).FullName;
            case "uint": return typeof(uint).FullName;
            case "long": return typeof(long).FullName;
            case "ulong": return typeof(ulong).FullName;
            case "string": return typeof(string).FullName;
            case "float": return typeof(float).FullName;
            case "double": return typeof(double).FullName;
            case "vector2": return "SerializeVector2";
            case "vector3": return "SerializeVector3";
            case "color": return "SerializeColor";
        }

        return typeName;
    }

    private static bool IsBaseType(string typeName)
    {
        switch (typeName)
        {
            case "bool":
            case "boolean":
            case "byte":
            case "sbyte":
            case "short":
            case "ushort":
            case "int":
            case "uint":
            case "long":
            case "ulong":
            case "string":
            case "float":
            case "double":
                return true;
        }

        return false;
    }
    
    private static void PrintError(string message)
    {
        Console.ForegroundColor = ConsoleColor.Red;
        Console.WriteLine(message);
        Console.ResetColor();
    }
    
    /// <summary>
    /// 字符串首字母小写
    /// </summary>
    public static string FirstToLower(this string str)
    {
        return str.Substring(0, 1).ToLower() + str.Substring(1);
    }
    
    /// <summary>
    /// 字符串首字母大写
    /// </summary>
    public static string FirstToUpper(this string str)
    {
        return str.Substring(0, 1).ToUpper() + str.Substring(1);
    }
}