这是一个功能强大的Excel配置文件导出工具,支持将Excel数据转换为高效的C#代码,包含字典映射、嵌套字典映射、常量定义等多种格式。
- Dict模式:将Excel数据导出为C#字典结构
- Const模式:将Excel数据导出为C#常量定义
- MulDict模式:将Excel数据导出为嵌套字典结构(多层键值映射)
- 多端支持:分别导出到客户端(c)、服务端(s)或两者(cs)
- 多文件合并:支持从多个Excel文件中读取数据并合并到一个字典
- 字段常量优化:自动生成字段名常量,减少内存占用
- 空值跳过:自动跳过空值字段,生成更简洁的代码
- 列过滤:以#开头的列将被自动跳过
- PostProcess后处理系统:支持自定义数据转换和类成员添加
int:整数类型float:单精度浮点数double:双精度浮点数bool:布尔值(支持true/false和0/1)string:字符串类型
list:字符串列表(向后兼容)list(int):整数列表list(float):浮点数列表list(double):双精度列表list(bool):布尔值列表list(string):字符串列表
注意:所有列表类型生成为完全不可修改的ImmutableArray<T>,保证数据完全安全性
dict(string:int):字符串到整数的映射dict(string:float):字符串到浮点数的映射dict(string:bool):字符串到布尔值的映射dict(string:string):字符串到字符串的映射dict(keyType:valueType):支持任意类型组合- 嵌套字典:
dict(string:dict(int:int))等复杂结构
refer(filename.sheetname.columnname):引用其他表格中的数据- 示例:
refer(npc.NPC.type_id)- 引用npc.xlsx文件中NPC工作表的type_id列 - 自动类型检测:自动推断被引用列的实际数据类型(int、string等)
- 引用验证:导入时自动验证引用值的有效性,无效值会立即报错
- 子目录支持:支持在ExcelRootDirectory的子目录中查找引用文件
- 跨文件引用:可以引用其他Excel文件中的任意工作表和列
- 同文件引用:也可以引用当前文件中的其他工作表
- 示例:
text或text(language):本地化字符串类型- 示例:
text、text(zh)、text(en) - TextId模式:生成基于哈希的文本ID,运行时从翻译文件加载
- 后缀标识支持:
"apple#company"vs"apple#fruit"产生不同翻译 - 多语言支持:支持任意数量的目标语言
- 翻译文件生成:自动生成翻译模板JSON文件
- 运行时切换:支持动态语言切换,无需重新编译
- 示例:
#unique:唯一性修饰符,确保该列中的所有数据都是唯一的- 示例:
int#unique、string#unique - 如果发现重复值,会抛出异常并指明具体的行号和重复值
- 支持所有基础数据类型的唯一性验证
- 示例:
| 列 | 说明 | 示例 |
|---|---|---|
| 第1列 | 导出端 | c(客户端), s(服务端), cs(两者) |
| 第2列 | 导出模式 | dict, const, mul_dict |
| 第3列 | Sheet映射 | camera=CAMERA,item=ITEM (dict模式) constants (const模式) monsterdata=MONSTER_DATA (mul_dict模式) |
| 第4列 | 输出文件名 | Config.cs |
| 第5列 | 额外参数 | (预留) |
| 第6列 | 多源文件 | npc_1.xlsx,npc_2.xlsx,npc_3.xlsx |
| 第7列 | 本地化模式 | textid, direct, embedded (留空使用全局配置) |
| 行 | 说明 | 示例 |
|---|---|---|
| 第1行 | 说明文字 | ID, 名称, #隐藏列, 数量, 价格, 类型引用, 唯一编码 |
| 第2行 | 字段名 | id, name, hidden, count, price, type_ref, code |
| 第3行 | 数据类型 | int, string, string, list(int), dict(string:float), refer(types.ItemType.type_id), string#unique |
| 第4行 | 导出端 | cs, c, s, cs, c, cs, cs |
| 第5行+ | 数据行 | 1, "道具1", "", "10,20,30", "{"buy":9.99,"sell":4.99}", 1001, "ITEM_001" |
说明:
- 以#开头的列(如"#隐藏列")将被自动跳过
- 空值字段不会出现在生成的代码中
- list类型用逗号分隔:
"10,20,30" - dict类型使用JSON格式:
"{\"key\":value}" - refer类型自动检测被引用列的数据类型,在上例中type_ref字段会变成int类型
- 带
#unique修饰符的列将进行唯一性验证,如发现重复值会立即抛出异常
| 列 | 说明 | 示例 |
|---|---|---|
| 第1列 | 常量名 | MAX_LEVEL |
| 第2列 | 导出端 | cs |
| 第3列 | 数据类型 | int |
| 第4列 | 常量值 | 100 |
MulDict模式用于创建嵌套字典结构,其中第一列作为主键,第二列作为子键:
| 行 | 说明 | 示例 |
|---|---|---|
| 第1行 | 说明文字 | 怪物ID, 等级, 名称, 血量, 伤害 |
| 第2行 | 字段名 | monsterId, level, name, health, damage |
| 第3行 | 数据类型 | int, int, string, int, int |
| 第4行 | 导出端 | cs, cs, c, cs, c |
| 第5行+ | 数据行 | 1, 1, "哥布林", 50, 10 |
关键特点:
- 第一列(主键):作为外层字典的键值
- 第二列(子键):作为内层字典的键值
- 其余列:作为最内层字典的数据字段
- 嵌套结构:生成
Dictionary<主键类型, Dictionary<子键类型, Dictionary<string, object>>>
使用场景:
- 游戏怪物数据:
MONSTER_DATA[怪物ID][等级] - 物品升级数据:
ITEM_UPGRADE[物品ID][等级] - 技能配置数据:
SKILL_CONFIG[技能ID][等级]
# 编译项目
dotnet build
# 基本用法
dotnet run config.xlsx
# 指定输出目录
dotnet run config.xlsx ./output
# 自定义客户端和服务端子目录
dotnet run config.xlsx ./output frontend backend
# 完整用法
dotnet run <Excel文件路径> [输出目录] [客户端子目录] [服务端子目录]# 启动交互界面
dotnet run
# 然后选择:
# 1. 处理单个Excel文件
# 2. 处理多个Excel文件
# 3. 处理目录中的所有Excel文件交互模式会提示你配置:
- 输出根目录 (默认: ./output)
- 客户端子目录 (默认: client)
- 服务端子目录 (默认: server)
output/
├── client/ # 客户端代码
│ └── InfoNpc.cs
└── server/ # 服务端代码
└── InfoNpcConst.cs
# 使用自定义目录名
dotnet run config.xlsx ./game_config frontend backendgame_config/
├── frontend/ # 自定义客户端目录
│ └── InfoNpc.cs
└── backend/ # 自定义服务端目录
└── InfoNpcConst.cs
using System.Collections.Generic;
using System.Collections.Immutable;
namespace GameConfig
{
public static class ItemConfig
{
// 字段名常量
public const string NAME = "name";
public const string STATS = "stats";
public const string PRICES = "prices";
public const string CONFIG = "config";
public static readonly Dictionary<int, Dictionary<string, object>> ITEMS = new()
{
[1001] = new Dictionary<string, object>
{
[NAME] = "魔剑",
[STATS] = ImmutableArray.Create(new int[] { 100, 200, 50 }),
[PRICES] = new Dictionary<string, float> { ["buy"] = 99.99f, ["sell"] = 49.99f },
[CONFIG] = new Dictionary<string, bool> { ["tradeable"] = true, ["droppable"] = false },
},
[1002] = new Dictionary<string, object>
{
[NAME] = "盾牌",
[STATS] = ImmutableArray.Create(new int[] { 50, 500, 10 }),
[PRICES] = new Dictionary<string, float> { ["buy"] = 150f, ["sell"] = 75f },
// 注意:空的config字段被自动跳过
},
};
}
}namespace GameConfig
{
public static class Constants
{
public static readonly int MAX_LEVEL = 100;
public static readonly float CRITICAL_RATE = 0.25f;
public static readonly bool DEBUG_MODE = false;
public static readonly string GAME_VERSION = "1.0.0";
}
}using System.Collections.Generic;
namespace GameConfig
{
public static class MonsterData
{
// 字段名常量
public const string NAME = "name";
public const string HEALTH = "health";
public const string DAMAGE = "damage";
public static readonly Dictionary<int, Dictionary<int, Dictionary<string, object>>> MONSTER_DATA = new()
{
[1] = new Dictionary<int, Dictionary<string, object>>
{
[1] = new Dictionary<string, object>
{
[NAME] = "哥布林",
[HEALTH] = 50,
[DAMAGE] = 10,
},
[5] = new Dictionary<string, object>
{
[NAME] = "哥布林战士",
[HEALTH] = 120,
[DAMAGE] = 25,
},
[10] = new Dictionary<string, object>
{
[NAME] = "哥布林精英",
[HEALTH] = 250,
[DAMAGE] = 45,
},
},
[2] = new Dictionary<int, Dictionary<string, object>>
{
[1] = new Dictionary<string, object>
{
[NAME] = "兽人",
[HEALTH] = 80,
[DAMAGE] = 15,
},
[5] = new Dictionary<string, object>
{
[NAME] = "兽人狂战士",
[HEALTH] = 180,
[DAMAGE] = 35,
},
},
};
}
}使用方式:
// 访问1号怪物5级的数据
var goblinLevel5 = MonsterData.MONSTER_DATA[1][5];
string name = (string)goblinLevel5[MonsterData.NAME]; // "哥布林战士"
int health = (int)goblinLevel5[MonsterData.HEALTH]; // 120
int damage = (int)goblinLevel5[MonsterData.DAMAGE]; // 25
// 检查某个怪物是否存在特定等级
if (MonsterData.MONSTER_DATA.ContainsKey(1) &&
MonsterData.MONSTER_DATA[1].ContainsKey(10))
{
// 处理1号怪物10级数据
}支持多种布尔值表示方式:
- 文本形式:
true,false(不区分大小写) - 数字形式:
1=true,0=false - 列表形式:
"1,0,true,false"→ImmutableArray.Create(new bool[] { true, false, true, false })
Excel列类型: list(int)
Excel单元格值: "100,200,50"
生成C#代码: ImmutableArray.Create(new int[] { 100, 200, 50 })
完全不可修改性:
- 使用
ImmutableArray<T>类型,完全不可变 - 支持索引访问:
array[0]、array.Length等读取操作 - 无法修改元素:
array[0] = newValue编译错误 - 无修改方法:没有
Add()、Remove()、Clear()等修改方法 - 修改操作返回新数组:
array.Add(item)返回新的ImmutableArray,原数组不变 - 保证配置数据的绝对安全性和不变性
Excel列类型: dict(string:int)
Excel单元格值: {"attack":100,"defense":50}
生成C#代码: new Dictionary<string, int> { ["attack"] = 100, ["defense"] = 50 }
Excel列类型: dict(string:dict(int:int))
Excel单元格值: {"bonus":{"1":10,"2":20}}
生成C#代码: new Dictionary<string, Dictionary<int, int>> {
["bonus"] = new Dictionary<int, int> { [1] = 10, [2] = 20 }
}
Excel列类型: refer(filename.sheetname.columnname)
1. 跨文件引用
文件结构:
├── config.xlsx # 主配置文件
├── Excel/
│ ├── npc/
│ │ └── npc_data.xlsx # NPC数据文件
│ └── items.xlsx # 物品数据文件
config.xlsx 中的配置:
字段名: npc_type
数据类型: refer(npc_data.NPC.type_id)
数据值: 1001
系统会:
1. 在Excel/子目录中找到 npc_data.xlsx
2. 读取NPC工作表的type_id列
3. 检测type_id列的数据类型(如int)
4. 验证1001是否存在于type_id列中
5. 生成时使用int类型:[NPC_TYPE] = 1001
2. 同文件引用
在同一个Excel文件中引用其他工作表:
数据类型: refer(config.ItemTypes.id)
// 引用当前文件中ItemTypes工作表的id列
系统会按以下优先级查找引用文件:
-
ExcelRootDirectory递归搜索 (默认./Excel)
- 在Excel目录及其所有子目录中查找
- 支持任意深度的嵌套目录结构
-
当前Excel文件目录
- 在当前处理的Excel文件所在目录查找
-
当前工作目录
- 在程序运行的根目录查找
引用文件不存在:
警告: 无法找到引用的工作表或数据 npc_data.xlsx#NPC.type_id
引用值无效:
错误: 字段 'npc_type' 在第 5 行存在无效引用值: 9999,
请检查 npc_data.xlsx#NPC.type_id
- 外键关系:物品引用物品类型、NPC引用NPC类型
- 配置层级:子配置引用主配置的ID
- 数据完整性:确保引用关系的有效性
- 分表管理:大型配置分散在多个文件中管理
当需要处理大量数据时(如NPC、物品等),可以将数据分散到多个Excel文件中,然后在导出时自动合并到一个字典中。
在#Setting工作表的第6列(sources列)配置需要合并的源文件:
target | mode | key | filename | extra | sources
both | dict | NpcId | InfoNpc | | npc_1.xlsx,npc_2.xlsx,npc_3.xlsx
数据读取顺序:
- 当前文件优先:首先读取配置文件本身的对应sheet数据
- 源文件依次读取:按sources配置的顺序读取每个文件
- 数据自动合并:所有数据合并到同一个字典中
- 唯一性验证:跨所有文件维护#unique约束
列结构定义:
- 从第一个源文件读取列定义(字段名、类型、导出端等)
- 所有文件必须具有相同的列结构
- 支持当前文件没有对应sheet,仅从源文件读取
文件结构:
config.xlsx # 主配置文件,包含#Setting
├── npc_1.xlsx # NPC数据文件1:基础NPC
├── npc_2.xlsx # NPC数据文件2:精英NPC
└── npc_3.xlsx # NPC数据文件3:BOSS NPC
#Setting配置:
target | mode | mappings | filename | extra | sources
both | dict | npcs | InfoNpc | | npc_1.xlsx,npc_2.xlsx,npc_3.xlsx
执行结果:
- 读取config.xlsx中的npcs工作表(如果存在)
- 读取npc_1.xlsx中的npcs工作表
- 读取npc_2.xlsx中的npcs工作表
- 读取npc_3.xlsx中的npcs工作表
- 所有NPC数据合并生成InfoNpc.cs
- Dict模式:多文件数据合并到单个字典
- Const模式:多文件常量合并到单个常量类
- MulDict模式:多文件嵌套字典合并
- 文件不存在:显示警告但继续处理其他文件
- 工作表缺失:显示警告但继续处理其他文件
- 唯一性冲突:立即抛出异常,显示重复值的文件和行号
- 列结构不匹配:使用第一个源文件的列定义为准
- 大型数据集:NPC数据、物品数据、技能数据等
- 分类管理:按类别、等级、区域分散到不同文件
- 团队协作:多人维护不同类型的配置数据
- 版本管理:便于Git等版本控制系统管理
- 内存优化:逐个文件读取,避免同时加载所有文件
- 唯一性高效验证:使用HashSet进行O(1)重复检查
- 错误快速定位:精确显示问题文件和行号
本地化系统提供高效的多语言支持,采用文本ID方式。您只需在Excel中填写主要语言的文本,系统会自动生成文本ID,运行时从翻译文件中动态加载对应语言的翻译。
- ✅ 简化Excel编辑:Excel中只需填写主语言文本,无需复杂的多语言配置
- ✅ 运行时翻译:支持动态语言切换,无需重新编译代码
- ✅ 独立翻译管理:翻译人员可以独立处理JSON翻译文件
- ✅ 基于哈希的文本ID:一致性强、抗冲突的文本标识系统
- ✅ 上下文消歧:使用后缀标记支持同文本不同翻译
- ✅ 优雅降级:缺失翻译时的后备机制
本地化设置在全局级别配置,确保所有导出的一致性:
var globalConfig = new GlobalConfig
{
SupportedLanguages = "en,zh,ja,fr", // 支持的语言代码
DefaultLocalizationMode = LocalizationMode.TextId, // 默认本地化模式
PrimaryLanguage = "en", // 主语言(Excel中使用)
LocalizationOutputPath = "Localization" // 翻译文件输出目录
};- TextId模式(推荐):生成文本ID,翻译在运行时加载
- Direct模式:直接使用原始文本,不进行本地化
- Embedded模式:传统的内嵌翻译模式(不推荐新项目使用)
在Excel列类型定义中使用text类型:
// 列类型定义
text // 使用全局默认主语言
text(zh) // 指定中文为主语言
text(en) // 指定英文为主语言
// Excel单元格数据(简单文本格式)
"Hello World"
"你好世界"
// 上下文消歧(后缀标记)
"apple" // 将翻译为 "苹果"
"apple#company" // 将翻译为 "苹果公司"
"apple#fruit" // 明确标识为水果(如需要区分)
系统为每个文本生成基于哈希的稳定ID:
- "Hello World" → T8B7C4D2F (Hello World的哈希)
- "apple" → TA1B2C3D4 (apple的哈希)
- "apple#company" → T5E6F7A8B (apple#company的哈希)
这确保了:
- ✅ 相同文本=相同ID:相同文本在不同表格中共享翻译
- ✅ 不同上下文=不同ID:后缀标记创建独特翻译
- ✅ 抗冲突性:基于SHA-256的哈希防止ID冲突
| Target | Mode | Mappings | FileName | ExtraParams | Sources | LocalizationMode |
|--------|------|----------|----------|-------------|---------|------------------|
| cs | dict | Items | Items.cs | | | |
说明: LocalizationMode列为可选,留空则使用全局默认模式。
| Description | Field Name | Type | Target | Row 1 Data | Row 2 Data |
|-------------|------------|----------|--------|---------------------|-----------------------|
| Item ID | id | int | cs | 1 | 2 |
| Item Name | name | text | cs | Sword | Shield |
| Description | desc | text | cs | A sharp blade | Protective gear |
| Price | price | int | cs | 100 | 150 |
关键特点:Excel中只需直接填写主语言文本,无需复杂的JSON格式!
生成的C#代码 (Items.cs):
using System.Collections.Generic;
using System.Collections.Immutable;
using CSExcelConverter.Models;
namespace GameConfig
{
public static class Items
{
public static readonly Dictionary<int, Dictionary<string, object>> ITEMS = new()
{
[1] = new Dictionary<string, object>
{
["name"] = "T8B7C4D2F", // "Sword"的哈希ID
["desc"] = "TA1B2C3D4", // "A sharp blade"的哈希ID
["price"] = 100,
},
[2] = new Dictionary<string, object>
{
["name"] = "T5E6F7A8B", // "Shield"的哈希ID
["desc"] = "T9F8E7D6C", // "Protective gear"的哈希ID
["price"] = 150,
},
};
/// <summary>
/// 获取本地化文本,如果找不到翻译则返回文本ID
/// </summary>
public static string GetText(string textId, params object[] args)
{
return LocalizationService.Instance.GetText(textId, textId, args);
}
/// <summary>
/// 获取本地化文本,指定默认文本
/// </summary>
public static string GetText(string textId, string defaultText, params object[] args)
{
return LocalizationService.Instance.GetText(textId, defaultText, args);
}
}
}TextIds.json(文本ID映射表):
{
"GeneratedTime": "2025-09-06 20:30:00",
"TotalEntries": 4,
"Entries": [
{
"TextId": "T8B7C4D2F",
"OriginalText": "Sword",
"Context": "Items.name",
"SheetName": "Items",
"ColumnName": "name",
"RowIndex": 5
},
{
"TextId": "TA1B2C3D4",
"OriginalText": "A sharp blade",
"Context": "Items.desc",
"SheetName": "Items",
"ColumnName": "desc",
"RowIndex": 5
}
// ... 更多条目
]
}Translations_ZH.json(翻译模板):
{
"$meta": {
"Language": "zh",
"GeneratedTime": "2025-09-06 20:30:00",
"TotalEntries": 4,
"Instructions": [
"请将右侧的原文翻译为对应的目标语言",
"保持占位符 {0}, {1} 等不变",
"如果不需要翻译某个条目,可以删除该行或设置为空字符串"
]
},
"T8B7C4D2F": {
"original": "Sword",
"translation": "剑",
"context": "Items.name",
"note": "Row 5"
},
"TA1B2C3D4": {
"original": "A sharp blade",
"translation": "锋利的刀刃",
"context": "Items.desc",
"note": "Row 5"
},
"T5E6F7A8B": {
"original": "apple#company",
"translation": "苹果公司",
"context": "Items.name (suffix: company)",
"note": "Row 6 - 上下文特定翻译"
}
// ... 更多翻译
}using CSExcelConverter.Models;
// 游戏启动时初始化本地化服务
LocalizationService.Instance.Initialize(
localizationPath: "./Localization", // 翻译文件目录
defaultLanguage: "en", // 默认语言
fallbackLanguage: "en" // 后备语言
);
// 动态切换语言
LocalizationService.Instance.CurrentLanguage = "zh";// 方法1:使用生成的帮助方法
var itemName = Items.GetText((string)Items.ITEMS[1]["name"]);
// 方法2:直接使用LocalizationService
var textId = (string)Items.ITEMS[1]["name"];
var itemName = LocalizationService.Instance.GetText(textId, "Default Text");
// 方法3:使用扩展方法
var itemName = textId.Localize("Default Text");
// 带格式参数
var message = Items.GetText("ITEMS_LEVEL_UP_MSG", "Level Up!", playerLevel, newSkill);// 检查可用语言
var languages = LocalizationService.Instance.SupportedLanguages;
// 检查翻译是否存在
bool hasTranslation = LocalizationService.Instance.HasTranslation("T8B7C4D2F", "zh");
// 获取翻译统计
var stats = LocalizationService.Instance.GetStats();
Console.WriteLine($"总文本ID数量: {stats.TotalTextIds}");
Console.WriteLine($"当前语言: {stats.CurrentLanguage}");
// 重新加载翻译(开发时热重载很有用)
LocalizationService.Instance.Reload("./Localization");-
全局配置:在全局配置中设定支持的语言
var globalConfig = new GlobalConfig { SupportedLanguages = "en,zh,ja,fr", DefaultLocalizationMode = LocalizationMode.TextId, PrimaryLanguage = "en" };
-
Excel设置:在数据工作表中使用
text类型列 -
生成代码:运行CSExcelConverter生成C#代码和翻译模板
-
翻译:将
Translations_*.json文件发送给翻译人员 -
部署:将完成的翻译文件包含在游戏构建中
-
运行时:初始化LocalizationService并根据需要切换语言
- 基于哈希的文本ID:由文本内容生成,确保构建间的一致性
- 上下文消歧:当同一文本需要不同翻译时使用后缀标记(
apple#company) - 共享翻译:相同文本在不同表格中自动共享同一翻译
- 后缀使用示例:
OKvsOK#buttonvsOK#dialogapplevsapple#fruitvsapple#companyclosevsclose#doorvsclose#application
- 翻译管理:基于哈希的ID保持稳定,便于翻译管理
- 性能:哈希查找在运行时翻译查询中极其快速
// TextIdManager支持自定义前缀和上下文处理
var textId = textIdManager.GetOrCreateTextId(
originalText: "Hello World",
context: "MainMenu.Welcome",
sheetName: "UI",
columnName: "text",
rowIndex: 10
);// 内置翻译完整性验证
var validator = new TranslationValidator();
var issues = validator.ValidateTranslations("./Localization", requiredLanguages);
foreach (var issue in issues)
{
Console.WriteLine($"缺失: {issue.TextId} 在 {issue.Language}");
}在Excel第一行(说明行)以#开头的列将被自动跳过:
第1行: ID | 名称 | #调试信息 | 价格
第2行: id | name | debug | price
结果:#调试信息列不会出现在生成的代码中。
空的单元格或空字符串字段不会出现在生成的字典中:
// 如果某个道具的description字段为空,生成的代码中不会包含该字段
[1001] = new Dictionary<string, object>
{
[NAME] = "道具名",
// [DESCRIPTION] = "", // 空值字段被自动跳过
[PRICE] = 99,
}自动生成字段名常量,避免重复的字符串字面值:
// 传统方式:多次使用字符串字面值
["name"] = "道具1",
["name"] = "道具2",
// 优化后:使用常量
public const string NAME = "name";
[NAME] = "道具1",
[NAME] = "道具2",使用 #unique 修饰符确保列数据的唯一性:
基础类型#unique
int#unique:整数唯一性string#unique:字符串唯一性float#unique:浮点数唯一性double#unique:双精度数唯一性bool#unique:布尔值唯一性
Excel表格示例:
行1: 道具ID | 道具名称 | 唯一编码
行2: id | name | code
行3: int | string | string#unique
行4: cs | cs | cs
行5: 1001 | "剑" | "SWORD_001"
行6: 1002 | "盾" | "SHIELD_001"
行7: 1003 | "弓" | "SWORD_001" ← 重复值!
当检测到重复值时,程序会立即抛出异常:
字段 'code' 在第 7 行存在重复值: SWORD_001
- ID字段:确保每个实体的ID都是唯一的
- 编码字段:确保业务编码不重复
- 键值字段:确保作为字典键的字段值唯一
- 配置名称:确保配置项名称不冲突
PostProcess系统允许用户通过自定义后处理文件对导出的数据进行二次处理,实现数据转换、验证和增强功能。
在项目根目录下的PostProcess/目录中创建后处理文件,命名格式为:{导出文件名}_PostProcess.cs
示例:
- 导出文件
InfoNpc.cs→ 后处理文件InfoNpc_PostProcess.cs - 导出文件
ItemConfig.cs→ 后处理文件ItemConfig_PostProcess.cs
using CSExcelConverter.Models;
namespace CSExcelConverter.PostProcess
{
public class InfoNpc_PostProcess : IPostProcessor
{
public PostProcessResult ProcessDictData(string className,
Dictionary<string, List<Dictionary<string, object?>>> data,
ExportTarget target)
{
var result = new PostProcessResult();
// 克隆数据避免修改原始数据
var processedData = CloneData(data);
// 数据处理逻辑...
return result;
}
// 实现其他接口方法...
}
}// 修改特定记录的值
if (processedData.ContainsKey("NPC"))
{
foreach (var row in processedData["NPC"])
{
if (row["id"]?.ToString() == "1002")
{
row["name"] = "李四"; // 修改1002号NPC的名称
}
}
}// 为所有记录添加计算字段
foreach (var sheetData in processedData.Values)
{
foreach (var row in sheetData)
{
// 基于年龄添加是否成年字段
if (row.ContainsKey("age") && row["age"] is int age)
{
row["isAdult"] = age >= 18;
}
// 添加处理标记
row["processedBy"] = "PostProcessor";
}
}方式1:字典方式(推荐)
// 简洁易用的字典方式
result.AdditionalFields["MAX_LEVEL"] = new AdditionalField
{
Type = "int",
Value = 100,
IsConstant = true,
Comment = "最大等级限制"
};
result.AdditionalFields["NPC_NAMES"] = new AdditionalField
{
Type = "ImmutableArray<string>",
Value = new string[] { "张三", "李四" },
IsReadOnly = true,
Comment = "NPC名称列表"
};生成结果:
/// <summary>
/// 最大等级限制
/// </summary>
public const int MAX_LEVEL = 100;
/// <summary>
/// NPC名称列表
/// </summary>
public static readonly ImmutableArray<string> NPC_NAMES = ImmutableArray.Create("张三", "李四");方式2:传统逐行方式
// 适合复杂方法和自定义逻辑
result.AdditionalMembers.Add("/// <summary>");
result.AdditionalMembers.Add("/// 检查NPC是否有效");
result.AdditionalMembers.Add("/// </summary>");
result.AdditionalMembers.Add("public static bool IsValidNpc(int npcId)");
result.AdditionalMembers.Add("{");
result.AdditionalMembers.Add(" return npcId > 0 && NPC.ContainsKey(npcId);");
result.AdditionalMembers.Add("}");- 基础类型:
int,string,bool,float,double - 集合类型:
ImmutableArray<T>(自动格式化) - 字段修饰符:
const(IsConstant = true)static readonly(IsReadOnly = true, 默认)static(IsReadOnly = false)
- 数据转换: 修改特定字段值,格式转换
- 计算字段: 基于现有数据生成新字段
- 数据验证: 添加数据校验逻辑
- 辅助方法: 生成便捷的查询和操作方法
- 常量定义: 添加业务相关的常量和配置
- 集合预处理: 预计算常用的数据集合
当导出工具检测到对应的后处理文件时,会自动:
- 动态编译: 使用Roslyn编译器编译后处理文件
- 数据处理: 对每个导出目标(客户端/服务端)分别执行后处理
- 代码生成: 将处理后的数据和额外成员整合到最终的C#代码中
- 错误处理: 如果后处理失败,会显示详细错误信息但不影响基础导出
- .NET 9.0: 运行时环境
- EPPlus: 用于读取Excel文件
- Microsoft.CodeAnalysis.CSharp: Roslyn编译器,用于PostProcess动态编译
- 跨平台支持: Windows, macOS, Linux
- ✅ 本地化系统:支持基于哈希TextId的多语言文本管理
- ✅ text类型支持:
text和text(language)列类型用于本地化文本 - ✅ TextId模式:生成基于哈希的文本ID,运行时动态加载翻译
- ✅ 后缀消歧支持:
"apple#company"vs"apple#fruit"支持上下文特定翻译 - ✅ 全局本地化配置:支持的语言、默认模式、主语言等全局设置
- ✅ 翻译文件生成:自动生成TextIds.json映射表和Translations_*.json翻译模板
- ✅ LocalizationService:完整的运行时本地化服务,支持动态语言切换
- ✅ 基于哈希的文本ID:使用SHA-256确保ID一致性和抗冲突性
- ✅ TextIdManager:高效的文本ID管理,支持冲突检测和统计
- ✅ 多种本地化模式:TextId(推荐)、Direct、Embedded模式支持
- ✅ 简化Excel编辑:Excel中只需填写主语言文本,无需复杂多语言配置
- ✅ 翻译验证工具:内置翻译完整性检查和统计功能
- ✅ refer类型支持:
refer(filename.sheetname.columnname)语法引用其他表格数据 - ✅ 自动类型推断:自动检测被引用列的实际数据类型(int/string/float等)
- ✅ 引用验证:导入时验证引用值的有效性,无效值立即报错并指明位置
- ✅ 子目录支持:支持在ExcelRootDirectory的任意子目录中搜索引用文件
- ✅ 跨文件引用:可以引用其他Excel文件中的任意工作表和列
- ✅ 同文件引用:支持引用当前文件中的其他工作表
- ✅ 多级搜索策略:ExcelRootDirectory递归搜索 → 当前文件目录 → 工作目录
- ✅ 智能缓存:构建引用数据缓存,提高验证效率
- ✅ 详细错误信息:引用失败时提供精确的文件、工作表、列信息
- ✅ 多文件合并功能:支持从多个Excel文件读取数据并合并到一个字典
- ✅ 当前文件优先读取:自动读取配置文件本身的对应sheet数据
- ✅ 灵活的源文件配置:通过#Setting表第6列配置多个源文件
- ✅ 跨文件唯一性验证:#unique约束在所有文件间生效
- ✅ 智能错误处理:文件缺失或工作表缺失时显示警告但继续处理
- ✅ 全模式支持:Dict、Const、MulDict三种模式均支持多文件合并
- ✅ 向后兼容:不配置sources时保持原有单文件模式
- ✅ 性能优化:逐文件读取,内存占用优化,高效重复检查
- ✅ PostProcess后处理系统:支持自定义数据转换和类成员添加
- ✅ 动态编译:使用Roslyn编译器动态编译后处理文件
- ✅ 数据修改:修改特定记录值,添加计算字段
- ✅ 字典式类成员添加:通过AdditionalFields简洁添加常量和字段
- ✅ 传统式类成员添加:通过AdditionalMembers添加复杂方法和逻辑
- ✅ 自动字段常量生成:自动为后处理添加的字段生成常量
- ✅ 多种字段类型支持:const、static readonly、static修饰符
- ✅ ImmutableArray自动格式化:智能处理集合类型格式化
- ✅ 错误处理机制:后处理失败不影响基础导出功能
- ✅ 唯一性约束支持 (#unique):类型后缀加#unique修饰符
- ✅ 实时唯一性验证:导出时检查数据唯一性,发现重复立即报错
- ✅ 精确错误定位:准确显示重复数据的行号和具体值
- ✅ 全类型支持:int#unique, string#unique, float#unique, double#unique, bool#unique
- ✅ 多模式兼容:Dict模式和MulDict模式均支持唯一性约束
- ✅ MulDict模式:多层嵌套字典支持
- ✅ 双重键值映射:支持主键+子键的嵌套结构
- ✅ 复杂数据关系:适用于等级系统、分类数据等场景
- ✅ List类型完全不可修改:所有列表类型生成为ImmutableArray形式
- ✅ 数据完全安全性:配置数据无法在运行时被意外修改,元素值也无法更改
- ✅ ImmutableArray支持:自动导入System.Collections.Immutable命名空间
- ✅ 字段常量优化,减少内存占用
- ✅ 空值自动跳过功能
- ✅ 列过滤功能(#开头列)
- ✅ Bool类型0/1支持
- ✅ List类型泛型支持
- ✅ Dict类型完整支持
- ✅ 嵌套数据结构支持
- ✅ JSON格式数据解析
- ✅ Dict/Const双模式导出
- ✅ 多端分离导出
- ✅ 交互式用户界面
- ✅ 基础数据类型支持