NewLife/NewLife.Skills

Add XCode skills for entity caching, ORM, and sharding ETL

- Introduced `xcode-entity-caching` skill to enhance entity data retrieval using a multi-level caching system, detailing four cache types: EntityCache, SingleCache, FieldCache, and DbCache.
- Added `xcode-entity-orm` skill covering CRUD operations with NewLife.XCode ORM, including Biz file conventions, dirty field tracking, validation hooks, built-in interceptors, and transaction management.
- Created `xcode-sharding-etl` skill for implementing sharding and ETL processes, featuring TimeShardPolicy configuration, cross-shard querying, and a comprehensive ETL framework for data extraction and synchronization.
大石头 authored at 2026-04-02 18:30:07
734e741
Tree
1 Parent(s) 13b0f1a
Summary: 5 changed files with 1703 additions and 0 deletions.
Added +308 -0
Added +349 -0
Added +161 -0
Added +576 -0
Added +309 -0
Added +308 -0
diff --git a/.github/skills/xcode-data-access-layer/SKILL.md b/.github/skills/xcode-data-access-layer/SKILL.md
new file mode 100644
index 0000000..a64b1d5
--- /dev/null
+++ b/.github/skills/xcode-data-access-layer/SKILL.md
@@ -0,0 +1,308 @@
+---
+name: xcode-data-access-layer
+description: >
+  使用 NewLife.XCode 数据访问层(DAL)进行直接 SQL 操作和高级查询构建,
+  涵盖 DAL.Create() 全局实例、SelectBuilder SQL 构建与解析、
+  WhereExpression 类型安全条件运算符(防 SQL 注入)、
+  ReadWriteStrategy 读写分离,以及 BatchFinder 解决 N+1 查询问题。
+  适用于复杂查询构建、跨表统计、读写分离配置、批量关联数据加载等任务。
+argument-hint: >
+  说明查询场景:是否需要跨表原生 SQL;条件是否复杂(多字段组合/模糊/区间);
+  是否有读写分离需求;是否存在循环逐条查询的 N+1 性能问题。
+---
+
+# XCode 数据访问层(DAL)
+
+## 适用场景
+
+- 使用 `WhereExpression` 构建类型安全的查询条件,防止 SQL 注入。
+- 直接通过 `DAL` 执行原生 SQL(跨表查询、自定义统计)。
+- 用 `SelectBuilder` 构建/解析/二次修改复杂查询语句。
+- 配置读写分离,将查询压力分散到只读副本。
+- 用 `BatchFinder` 消除关联数据加载中的 N+1 查询问题。
+
+## WhereExpression — 查询条件表达式
+
+`WhereExpression` 是 XCode 防 SQL 注入的核心工具。通过 `ClassName._` 获取字段定义,使用运算符重载拼接条件。
+
+### 基础运算符
+
+```csharp
+// 等值 / 不等
+User._.Status == 1
+User._.Name != "admin"
+
+// 比较
+User._.Age > 18
+User._.CreateTime >= DateTime.Today
+User._.Score <= 100.0
+
+// 模糊匹配
+User._.Name.Contains("alice")     // LIKE '%alice%'
+User._.Name.StartsWith("A")       // LIKE 'A%'
+User._.Email.EndsWith(".cn")      // LIKE '%.cn'
+
+// IN 查询
+User._.Id.In(new[] { 1, 2, 3 })
+User._.Status.In(new[] { 1, 2 })
+User._.Id.NotIn(blockedIds)
+
+// NULL 判断
+User._.DeleteTime.IsNull()
+User._.DeleteTime.NotNull()
+
+// 排序(用于 FindAll 的 order 参数)
+User._.CreateTime.Desc()
+User._.Name.Asc()
+```
+
+### 组合条件
+
+```csharp
+// AND 组合(& 运算符)
+var exp = User._.Status == 1 & User._.Age >= 18;
+
+// OR 组合(| 运算符)
+var exp2 = User._.Type == 1 | User._.Type == 2;
+
+// 动态拼接(推荐模式)
+var exp3 = new WhereExpression();
+exp3 &= _.UserId == userId;                                 // 必须条件
+if (start > DateTime.MinValue) exp3 &= _.CreateTime >= start;
+if (end > DateTime.MinValue)   exp3 &= _.CreateTime < end;
+if (!key.IsNullOrEmpty())      exp3 &= _.Name.Contains(key.Trim());
+
+var list = User.FindAll(exp3, page);
+```
+
+### 在 FindAll 系列中使用
+
+```csharp
+// 完整签名:FindAll(where, orderBy, select, startRowIndex, maximumRows)
+var list = User.FindAll(
+    User._.Status == 1,
+    User._.CreateTime.Desc(),
+    null,   // select *
+    0,      // offset
+    20      // limit
+);
+
+// 带分页参数(推荐)
+var page = new PageParameter { PageIndex = 1, PageSize = 20 };
+var list2 = User.FindAll(User._.Status == 1, page);
+
+// 统计
+var count = User.FindCount(User._.Status == 1);
+```
+
+## SelectBuilder — SQL 查询构建器
+
+用于构建或解析 SELECT 语句,支持二次修改。
+
+```csharp
+// 从零构建
+var sb = new SelectBuilder
+{
+    Table = "User",
+    Column = "Id, Name, CreateTime",
+    Where = "Status = 1",
+    OrderBy = "CreateTime Desc",
+};
+var sql = sb.ToString();
+// SELECT Id, Name, CreateTime FROM User WHERE Status = 1 ORDER BY CreateTime Desc
+
+// 解析已有 SQL 再修改
+var sb2 = new SelectBuilder("SELECT * FROM User WHERE Status = 1");
+sb2.Where += " AND RoleId = 2";
+sb2.OrderBy = "Id Desc";
+sb2.Limit = "LIMIT 0, 20";
+
+// 通过 DAL 执行
+var ds = DAL.Create("MyDb").Select(sb2.ToString());
+```
+
+## DAL — 数据访问层
+
+```csharp
+// 按连接名获取全局唯一实例
+var dal = DAL.Create("MyDb");
+
+// 核心属性
+dal.ConnName    // 连接名
+dal.DbType      // 数据库类型(DatabaseType 枚举)
+dal.ShowSQL     // 是否输出 SQL 日志
+
+// 查询 DataSet
+var ds = dal.Select("SELECT * FROM User WHERE Status = 1");
+
+// 查询 DataTable(第一个表)
+var dt = dal.SelectTable("SELECT Id, Name FROM User");
+
+// 执行非查询语句(返回受影响行数)
+var rows = dal.Execute("UPDATE User SET Status = 0 WHERE Id = @Id",
+    CommandType.Text,
+    new DbParameter[] { new SqlParameter("@Id", 1) });
+
+// 查询标量(COUNT / SUM 等)
+var count = dal.SelectCount("SELECT COUNT(*) FROM User WHERE Status = 1");
+
+// 查询实体列表(通过 SQL 映射到实体)
+var users = User.FindAllBySQL("SELECT * FROM User WHERE Status = 1");
+```
+
+## ReadWriteStrategy — 读写分离
+
+在连接字符串中配置主库和只读副本:
+
+```json
+{
+  "ConnectionStrings": {
+    "Order": "Server=master;Database=Order;Uid=sa;Pwd=xxx",
+    "OrderSlave0": "Server=slave1;Database=Order;Uid=sa;Pwd=xxx;readonly=true",
+    "OrderSlave1": "Server=slave2;Database=Order;Uid=sa;Pwd=xxx;readonly=true"
+  }
+}
+```
+
+**规则**:
+- 写操作(Insert/Update/Delete)→ 自动路由到主库
+- 读操作(FindAll/FindCount)→ 按策略路由到只读副本
+- 连接名规则:`{主连接名}Slave{序号}`
+
+代码层面也可显式指定:
+
+```csharp
+// 临时指定读连接(强制从主库读,如写后立即读)
+using (EntitySplit.Begin(null, User.Meta.ConnName))
+{
+    var user = User.FindByKey(newUserId);
+}
+```
+
+## BatchFinder — 解决 N+1 查询
+
+**问题**:批量处理数据时,关联实体逐条查询导致 N 次数据库往返。
+
+```csharp
+// ❌ 反例:N+1 查询(100 条记录 = 100 次查询)
+foreach (var log in logs)
+{
+    var user = User.FindById(log.UserId);   // 每次都查 DB
+    Console.WriteLine($"{user?.Name}: {log.Action}");
+}
+
+// ✅ 正例:BatchFinder(一次 IN 查询,多次内存取用)
+var finder = new BatchFinder<Int32, User>(logs.Select(e => e.UserId));
+foreach (var log in logs)
+{
+    var user = finder.FindByKey(log.UserId);  // 内存命中
+    Console.WriteLine($"{user?.Name}: {log.Action}");
+}
+```
+
+**核心配置**:
+
+| 属性 | 默认 | 说明 |
+|------|------|------|
+| `BatchSize` | `500` | 每次 IN 查询最大条数(自动分批)|
+| `Callback` | `null` | 自定义查询函数(追加过滤或指定从库)|
+| `Cache` | `ConcurrentDictionary` | 可预填充已有实体,只查未命中的 |
+
+```csharp
+// 预填充已有数据(避免重复查询)
+var finder = new BatchFinder<Int32, User>(allUserIds);
+foreach (var cached in onlineUsers)
+    finder.Cache[cached.Id] = cached;
+
+// FindByKey 只会查询 Cache 中不存在的 ID
+var user4 = finder.FindByKey(someId);
+```
+
+## DAL.Query\<T\> — 泛型结果映射
+
+`DAL_Mapper` 提供了类似 Dapper 的泛型映射能力,可将 SQL 查询结果直接映射到任意类型:
+
+```csharp
+var dal = DAL.Create("MyDb");
+
+// 映射至自定义 DTO 或实体类
+IEnumerable<UserDto> users = dal.Query<UserDto>(
+    "SELECT Id, Name, Status FROM User WHERE Status = @s",
+    new { s = 1 });
+
+// 映射单行
+UserDto? user = dal.QuerySingle<UserDto>(
+    "SELECT * FROM User WHERE Id = @id",
+    new { id = 42 });
+
+// 映射基础类型(返回第一列)
+IEnumerable<Int32> ids = dal.Query<Int32>("SELECT Id FROM User WHERE Status = 1");
+
+// 异步版本
+IEnumerable<UserDto> users2 = await dal.QueryAsync<UserDto>(
+    "SELECT * FROM User WHERE Status = @s",
+    new { s = 1 });
+UserDto? user3 = await dal.QuerySingleAsync<UserDto>(
+    "SELECT * FROM User WHERE Id = @id",
+    new { id = 42 });
+
+// 支持分页
+IEnumerable<UserDto> paged = dal.Query<UserDto>(
+    "SELECT * FROM User ORDER BY Id",
+    null, startRowIndex: 20, maximumRows: 10);
+
+// 带 PageParameter 分页
+var page = new PageParameter { PageIndex = 2, PageSize = 20, RetrieveTotalCount = true };
+IEnumerable<UserDto> paged2 = dal.Query<UserDto>(
+    "SELECT * FROM User ORDER BY Id",
+    null, page);
+// page.TotalCount 已自动设置
+```
+
+**参数传递规则**:
+- `param` 接受匿名对象(`new { s = 1 }`)或字典(`Dictionary<String, Object>`)
+- 参数名与 SQL 中 `@param` 对应(不区分大小写)
+- **不支持 `ValueTuple` 作为泛型类型**
+
+### DAL 原生执行(参数化)
+
+```csharp
+// Execute 也支持匿名对象参数——比 CommandType 重载更简洁
+Int32 rows = dal.Execute(
+    "UPDATE User SET Status=@s WHERE Id=@id",
+    new { s = 0, id = 42 });
+
+// ExecuteScalar 获取标量
+Int32? total = dal.ExecuteScalar<Int32>(
+    "SELECT COUNT(*) FROM User WHERE Status = @s",
+    new { s = 1 });
+
+// ExecuteReader — 低层只读流(记得用 using 关闭)
+using var reader = dal.ExecuteReader("SELECT Id, Name FROM User");
+while (reader.Read())
+    Console.WriteLine(reader["Name"]);
+```
+
+### ReadWriteStrategy 精细控制
+
+除基础读写分离外,可按时段/表名强制走主库:
+
+```csharp
+var dal = DAL.Create("Order");
+var strategy = dal.Strategy;
+
+// 配置在 00:00-01:00 期间忽略读写分离(走主库,避免主从延迟导致统计误差)
+strategy.IgnoreTimes.Add(new TimeRegion { Start = TimeSpan.FromHours(0), End = TimeSpan.FromHours(1) });
+
+// 特定表名始终走主库(如 OrderStat 统计表写后立即读)
+strategy.IgnoreTables.Add("OrderStat");
+```
+
+## 注意事项
+
+- `WhereExpression` 所有条件均参数化,**永远不要拼接用户输入到 SQL 字符串**——这是防止 SQL 注入的根本。
+- `SelectBuilder` 适合需要动态构建分页/子查询的复杂场景;简单查询优先使用 `FindAll + WhereExpression`。
+- 读写分离场景中,**写后立即读**若业务不允许主从延迟,需显式绑定主库(使用 `EntitySplit.Begin`)。
+- `BatchFinder` 适合一次性批量处理(如报表导出);若需长期高频查单条,用 `SingleCache` 更合适。
+- `DAL.Query<T>` **不支持 ValueTuple** 映射,会抛出 `InvalidOperationException`。
+- `ExecuteReader` 返回的 `IDataReader` 必须在 `using` 块内使用,底层连接随 reader close 而释放。
Added +349 -0
diff --git a/.github/skills/xcode-data-modeling/SKILL.md b/.github/skills/xcode-data-modeling/SKILL.md
new file mode 100644
index 0000000..f69971e
--- /dev/null
+++ b/.github/skills/xcode-data-modeling/SKILL.md
@@ -0,0 +1,349 @@
+---
+name: xcode-data-modeling
+description: >
+  使用 NewLife.XCode 进行数据建模,涵盖 Model.xml 完整属性体系(Option/Table/Column/Index)、
+  主键设计约定(Int32 自增 vs Int64 雪花 ID)、Map 外键关联、ShowIn 显示控制、DataScale 分表字段、
+  xcode 命令生成实体类,以及多模块项目目录结构。
+  适用于新建数据层项目、设计表结构、修改 *.xml 模型文件等任务。
+argument-hint: >
+  说明业务场景:需要建哪些表、主要字段类型、外键关系、是否有分表需求、是否需要生成魔方控制器。
+---
+
+# XCode 数据建模
+
+## 适用场景
+
+- 新建 XCode 数据类库,从零开始创建 `Model.xml` 进行数据建模。
+- 向已有模型文件添加新表或新字段。
+- 设计合理的主键策略(普通表 vs 大数据表)。
+- 配置字段外键关联(Map)、界面显示控制(ShowIn)、分表标记(DataScale)。
+- 执行 `xcode` 命令生成实体类、模型接口、数据字典、魔方控制器。
+
+## 环境准备
+
+```powershell
+# 安装 XCode NuGet 包
+dotnet add package NewLife.XCode
+
+# 安装 xcodetool 代码生成工具
+dotnet tool install xcodetool -g
+
+# 安装项目模板(发布日期须 > 2025-08-01)
+dotnet new details NewLife.Templates
+dotnet new install NewLife.Templates   # 未安装或版本过旧时执行
+
+# 创建数据类库项目
+dotnet new xcode -n Zero.Data
+
+# 创建 Web 管理后台(可选)
+dotnet new cube -n ZeroWeb
+
+# 创建控制台应用(可选)
+dotnet new nconsole -n ZeroApp
+```
+
+## Model.xml 文件结构
+
+```xml
+<?xml version="1.0" encoding="utf-8"?>
+<EntityModel xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
+             xs:schemaLocation="https://newlifex.com https://newlifex.com/Model202509.xsd"
+             xmlns="https://newlifex.com/Model202509.xsd">
+  <Option>
+    <!-- 全局配置 -->
+  </Option>
+  <Tables>
+    <Table>
+      <Columns>
+        <Column />
+      </Columns>
+      <Indexes>
+        <Index />
+      </Indexes>
+    </Table>
+  </Tables>
+</EntityModel>
+```
+
+## Option 配置项
+
+| 配置项 | 说明 | 示例 |
+|--------|------|------|
+| `Namespace` | 命名空间 | `Zero.Data` |
+| `ConnName` | 数据库连接名 | `Zero` |
+| `Output` | 实体类输出目录 | `.\` |
+| `BaseClass` | 实体基类 | `Entity` |
+| `ChineseFileName` | 使用中文文件名 | `True` |
+| `Nullable` | 生成可空引用类型 | `True` |
+| `HasIModel` | 实现 IModel 接口 | `True` |
+| `ModelClass` | 模型类模板 | `{name}Model` |
+| `ModelsOutput` | 模型类输出目录 | `.\Models\` |
+| `ModelInterface` | 模型接口模板 | `I{name}` |
+| `InterfacesOutput` | 接口输出目录 | `.\Interfaces\` |
+| `DisplayName` | 魔方区域显示名 | `订单管理` |
+| `CubeOutput` | 魔方控制器输出目录 | `../../OrderWeb/Areas/Order` |
+| `NameFormat` | 命名格式 | `Default`/`Upper`/`Lower`/`Underline` |
+| `ExtendNameSpace` | 额外引用命名空间(逗号分隔)| `System.Xml.Serialization` |
+
+## Table 属性
+
+| 属性 | 说明 | 示例 |
+|------|------|------|
+| `Name` | 实体类名 | `Order` |
+| `TableName` | 数据库表名(可选,默认同 Name)| `sys_order` |
+| `Description` | 表说明(`。`后为注释)| `订单。电商订单主表` |
+| `ConnName` | 独立连接名(覆盖全局)| `Log` |
+| `BaseType` | 基类(支持实体继承)| `EntityBase` |
+| `InsertOnly` | 仅插入模式(日志表优化)| `True` |
+| `IsView` | 视图标识 | `True` |
+
+## Column 属性完整参考
+
+### 基础属性
+
+| 属性 | 说明 | 示例 |
+|------|------|------|
+| `Name` | 属性名 | `UserName` |
+| `ColumnName` | 数据库列名(可选)| `user_name` |
+| `DataType` | 数据类型 | `Int32`/`Int64`/`String`/`DateTime`/`Boolean`/`Double`/`Decimal` |
+| `Description` | 字段说明 | `用户名。登录账号` |
+| `Length` | 字符串长度 | `50`/`200`/`-1`(大文本)|
+| `Precision` | 数值精度 | `18` |
+| `Scale` | 小数位数 | `2` |
+| `Nullable` | 允许空 | `False` |
+| `DefaultValue` | 默认值 | `0`/`''`/`getdate()` |
+
+### 主键设计约定
+
+| 场景 | 数据类型 | 必填属性 | 说明 |
+|------|---------|---------|------|
+| 普通表 | `Int32` | `PrimaryKey="True" Identity="True"` | 自增整数 |
+| 大数据表 | `Int64` | `PrimaryKey="True" DataScale="time"` | 雪花 ID,不设 Identity |
+
+```xml
+<!-- 普通表主键 -->
+<Column Name="Id" DataType="Int32" PrimaryKey="True" Identity="True" Description="编号" />
+
+<!-- 大数据表主键(雪花 ID) -->
+<Column Name="Id" DataType="Int64" PrimaryKey="True" DataScale="time" Description="编号" />
+```
+
+### 主字段(Master)
+
+业务主要字段(通常是名称、编号等,在列表中突出显示):
+
+```xml
+<Column Name="Name" DataType="String" Master="True" Length="50" Nullable="False" Description="名称" />
+```
+
+### Map 外键关联
+
+格式:`表名@主键字段@显示字段` 或 `表名@主键@显示字段@属性名`
+
+| 格式 | 说明 | 示例 |
+|------|------|------|
+| `Table@Id@Name` | 三段(属性名自动推导)| `Role@Id@Name` |
+| `Table@Id@Name@RoleName` | 四段(指定属性名)| `Role@Id@Name@RoleName` |
+| `NS.Table@Id@Path@AreaPath` | 完整命名空间 | `XCode.Membership.Area@Id@Path@AreaPath` |
+
+```xml
+<Column Name="UserId" DataType="Int32" Map="User@Id@Name" Description="用户" />
+<Column Name="RoleId" DataType="Int32" Map="Role@Id@Name@RoleName" Description="角色" />
+```
+
+### 字段类型(DataType)与 ItemType
+
+`DataType` 决定 C# 属性类型;`ItemType` 用于魔方前端渲染指定 UI 组件:
+
+| ItemType 值 | 用途 |
+|------------|------|
+| `image` | 图片 URL,预览缩略图 |
+| `file` | 文件路径,显示下载链接 |
+| `url` | 超链接 |
+| `mail` | 电子邮件 |
+| `mobile` | 手机号 |
+| `html` | 富文本 HTML |
+| `code` | 代码块 |
+| `json` | JSON 内容 |
+| `TimeSpan` | 时间间隔(毫秒转为可读格式)|
+| `GMK` | 字节数转为 GB/MB/KB 显示 |
+
+### ShowIn 显示控制
+
+控制字段在各区域(列表/详情/新增/编辑/搜索)的显示。
+
+**区域别名**:`List(L)` / `Detail(D)` / `AddForm(Add/A)` / `EditForm(Edit/E)` / `Search(S)` / `Form(F)`(同时控制 Add 和 Edit)
+
+```xml
+<!-- 具名列表语法(推荐) -->
+<Column ShowIn="List,Search" ... />        <!-- 仅 List 和 Search 显示 -->
+<Column ShowIn="-EditForm,-Detail" ... />  <!-- 编辑表单和详情隐藏 -->
+<Column ShowIn="All,-Detail" ... />        <!-- 全部显示,详情隐藏 -->
+
+<!-- 管道分隔语法 -->
+<Column ShowIn="Y|Y|N||A" ... />           <!-- List=显示|Detail=显示|Add=隐藏|Edit=自动|Search=自动 -->
+
+<!-- 5字符掩码语法 -->
+<Column ShowIn="11110" ... />              <!-- 1=显示, 0=隐藏, A/?/-=自动 -->
+```
+
+### DataScale 分表字段
+
+| 值 | 说明 |
+|----|------|
+| `time` | 大数据单表的时间字段(雪花 ID 内嵌时间)|
+| `timeShard:yyMMdd` | 分表字段,按日期格式自动分表 |
+| `timeShard:yyyyMM` | 分表字段,按月分表 |
+
+### 其他常用属性
+
+| 属性 | 说明 | 示例 |
+|------|------|------|
+| `Type` | 枚举类型 | `Zero.Data.OrderStatus` |
+| `Category` | 表单分组 | `登录信息`/`扩展` |
+| `Model` | 是否包含在模型类中 | `False` |
+| `Attribute` | 额外特性 | `XmlIgnore, IgnoreDataMember` |
+| `RawType` | 原始数据库类型 | `varchar(50)` |
+
+## Index 属性
+
+| 属性 | 说明 | 示例 |
+|------|------|------|
+| `Columns` | 索引列(逗号分隔)| `Name`/`Category,CreateTime` |
+| `Unique` | 唯一索引 | `True` |
+
+## 自动拦截器扩展字段约定
+
+以下字段由内置拦截器自动填充,**无需业务代码手动赋值**,建议统一设置 `Model="False" Category="扩展"`:
+
+```xml
+<Column Name="CreateUser"   DataType="String"   Description="创建者"   Model="False" Category="扩展" />
+<Column Name="CreateUserID" DataType="Int32"    Description="创建者"   Model="False" Category="扩展" />
+<Column Name="CreateTime"   DataType="DateTime" Description="创建时间" Model="False" Category="扩展" />
+<Column Name="CreateIP"     DataType="String"   Description="创建地址" Model="False" Category="扩展" />
+<Column Name="UpdateUser"   DataType="String"   Description="更新者"   Model="False" Category="扩展" />
+<Column Name="UpdateUserID" DataType="Int32"    Description="更新者"   Model="False" Category="扩展" />
+<Column Name="UpdateTime"   DataType="DateTime" Description="更新时间" Model="False" Category="扩展" />
+<Column Name="UpdateIP"     DataType="String"   Description="更新地址" Model="False" Category="扩展" />
+<Column Name="TraceId"      DataType="String"   Description="链路追踪" Model="False" Category="扩展" />
+```
+
+## 完整 Model.xml 示例
+
+```xml
+<?xml version="1.0" encoding="utf-8"?>
+<EntityModel xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
+             xs:schemaLocation="https://newlifex.com https://newlifex.com/Model202509.xsd"
+             xmlns="https://newlifex.com/Model202509.xsd">
+  <Option>
+    <Namespace>Order.Data</Namespace>
+    <ConnName>Order</ConnName>
+    <Output>.\</Output>
+    <ChineseFileName>True</ChineseFileName>
+    <Nullable>True</Nullable>
+    <HasIModel>True</HasIModel>
+    <DisplayName>订单管理</DisplayName>
+    <CubeOutput>../../OrderWeb/Areas/Order</CubeOutput>
+  </Option>
+  <Tables>
+    <Table Name="Order" Description="订单。电商订单主表">
+      <Columns>
+        <Column Name="Id"          DataType="Int64"    PrimaryKey="True" DataScale="time"        Description="编号" />
+        <Column Name="OrderNo"     DataType="String"   Master="True"     Length="50" Nullable="False" Description="订单号" />
+        <Column Name="UserId"      DataType="Int32"    Map="User@Id@Name"                         Description="用户" />
+        <Column Name="Status"      DataType="Int32"    Type="Order.Data.OrderStatus"              Description="状态" />
+        <Column Name="TotalAmount" DataType="Decimal"  Precision="18"    Scale="2"               Description="总金额" />
+        <Column Name="Remark"      DataType="String"   Length="500"      Category="扩展"         Description="备注" />
+        <Column Name="CreateUser"  DataType="String"   Model="False"     Category="扩展"         Description="创建者" />
+        <Column Name="CreateTime"  DataType="DateTime" Nullable="False"  Category="扩展"         Description="创建时间" />
+        <Column Name="UpdateTime"  DataType="DateTime" Model="False"     Category="扩展"         Description="更新时间" />
+      </Columns>
+      <Indexes>
+        <Index Columns="OrderNo" Unique="True" />
+        <Index Columns="UserId" />
+        <Index Columns="Status,CreateTime" />
+      </Indexes>
+    </Table>
+  </Tables>
+</EntityModel>
+```
+
+## xcode 命令
+
+```powershell
+# 在模型文件所在目录执行(自动查找所有 *.xml)
+xcode
+
+# 指定模型文件
+xcode Model.xml
+xcode Order.xml
+```
+
+**生成物**:
+1. `实体名.cs` — 自动生成的数据映射代码,**每次 xcode 会覆盖,禁止手动修改**
+2. `实体名.Biz.cs` — 业务扩展代码,仅首次生成,**可自由修改**
+3. `实体名.htm` — 数据字典(字段说明文档)
+4. 模型类(配置了 `ModelClass` 时)
+5. 接口(配置了 `ModelInterface` 时)
+6. 魔方控制器(配置了 `CubeOutput` 时)
+
+## 多模块项目结构
+
+```
+Zero.Data/
+├── Order/           # 订单模块
+│   ├── Order.xml    # 订单模型
+│   ├── 订单.cs
+│   ├── 订单.Biz.cs
+│   └── 订单明细.cs
+├── Product/         # 商品模块
+│   ├── Product.xml
+│   ├── 商品.cs
+│   └── 分类.cs
+└── Member/          # 会员模块
+    ├── Member.xml
+    └── 会员.cs
+```
+
+每个模块目录内有独立的 `*.xml`,在各自目录执行 `xcode` 生成。
+
+## 数据库连接配置
+
+连接名对应 Model.xml 的 `ConnName`。未配置时,默认创建同名 SQLite 数据库文件。
+
+```json
+{
+  "ConnectionStrings": {
+    "Order": "Server=.;Database=Order;Uid=sa;Pwd=xxx"
+  }
+}
+```
+
+## 反向工程(自动建表)模式
+
+通过 `XCode.json` 或 `appsettings.json` 的 `XCode` 节配置:
+
+| 模式 | 说明 | 适用场景 |
+|------|------|---------|
+| `Off` | 关闭,不检查不执行 | 生产环境(表结构由 DBA 管理)|
+| `ReadOnly` | 只读,检查差异但不执行 DDL | 生产环境排查 |
+| `On` | 仅新建表/列(**默认值**)| 开发/测试环境 |
+| `Full` | 可修改列类型、删除列/索引 | 开发初期快速迭代 |
+
+```json
+{
+  "XCode": {
+    "Migration": "Off",
+    "ShowSQL": false,
+    "SQLPath": "../SqlLog",
+    "TraceSQLTime": 500
+  }
+}
+```
+
+## 注意事项
+
+- `实体名.cs` 每次执行 `xcode` 会覆盖;字段调整始终在 `Model.xml` 中进行,再重新生成。
+- `String` 类型字段必须指定合理的 `Length`;大文本用 `-1`。
+- `Master="True"` 最多设置一个字段,作为列表主显示字段。
+- 同一个系统的不同模块可使用不同 `ConnName`,分别连接不同数据库。
Added +161 -0
diff --git a/.github/skills/xcode-entity-caching/SKILL.md b/.github/skills/xcode-entity-caching/SKILL.md
new file mode 100644
index 0000000..28c7520
--- /dev/null
+++ b/.github/skills/xcode-entity-caching/SKILL.md
@@ -0,0 +1,161 @@
+---
+name: xcode-entity-caching
+description: >
+  使用 NewLife.XCode 内置多级缓存体系加速实体数据读取,涵盖四层缓存选型矩阵:
+  EntityCache(整表列表缓存)、SingleCache(按键单对象字典缓存)、
+  FieldCache(聚合统计/下拉枚举缓存)、DbCache(跨进程数据库键值缓存)。
+  适用于字典表加速、用户高频读、统计下拉、跨服务缓存共享等场景。
+argument-hint: >
+  说明数据规模(小表字典 / 高频单对象读 / 统计聚合 / 跨进程共享);
+  数据更新频率(读多写少 vs 频繁变化);是否有跨进程共享需求。
+---
+
+# XCode 实体缓存
+
+## 四层缓存选型矩阵
+
+| 缓存层 | 类 | 典型场景 | 数据量上限 | 更新方式 |
+|--------|---|---------|-----------|---------|
+| **EntityCache** | `EntityCache<T>` | 字典表、配置表、状态表 | ≤1000 条 | 过期异步刷新 |
+| **SingleCache** | `SingleEntityCache<TK,TV>` | 按主键高频查单条 | 无硬限制 | TTL + 定时清理 |
+| **FieldCache** | `FieldCache<T>` | 下拉选项、TopN 统计、分类汇总 | 聚合结果行 | 过期重查 |
+| **DbCache** | `DbCache` | 跨进程共享、无外部缓存服务 | 受数据库限制 | Set/Get |
+
+## EntityCache — 整表列表缓存
+
+适用于**小表、读多写少**(如字典、角色、配置)。首次访问时阻塞加载,过期后异步刷新(不阻塞读取)。
+
+```csharp
+// 读取所有缓存实体(自动管理缓存生命周期)
+var roles = Role.FindAllWithCache();
+
+// 从缓存列表中过滤(无数据库访问)
+var enabled = Role.FindAllWithCache().Where(r => r.Enable).ToList();
+
+// 从缓存按主键查找
+var role = Role.FindByKeyWithCache(roleId);
+
+// 缓存内过滤(Biz 文件约定命名)
+public static IList<Role> FindAllCachedEnabled() =>
+    Meta.Cache.FindAll(_.Enable == true);
+
+public static Role? FindCachedByName(String name) =>
+    Meta.Cache.Find(_.Name == name);
+```
+
+**过期时间**:由 `XCodeSetting.EntityCacheExpire`(默认 10 秒)控制。
+
+**写操作后自动失效**:Insert/Update/Delete 后框架自动调用 `ClearCache()`,下次读取重新加载。
+
+## SingleCache — 单对象字典缓存
+
+适用于**按主键或业务键高频读单条记录**(如用户信息、订单详情)。以字典形式存储,支持主键和从键两种索引。
+
+```csharp
+// 按主键读取(命中则从内存返回,未命中则查 DB 并缓存)
+var user = User.Meta.SingleCache[userId];
+
+// 按从键读取(如按用户名查用户)
+var user2 = User.Meta.SingleCache.GetItemWithSlaveKey("alice");
+
+// 手动配置(在实体静态构造函数中)
+static User()
+{
+    // 配置从键(业务唯一键,如 Name/No)
+    Meta.SingleCache.SlaveKeyName = nameof(Name);
+    Meta.SingleCache.GetSlaveKey = e => e.Name;
+
+    // 过期时间(秒),默认由 XCodeSetting.SingleCacheExpire 控制
+    // Meta.SingleCache.Expire = 60;
+}
+```
+
+**过期时间**:由 `XCodeSetting.SingleCacheExpire`(默认 10 秒)控制,空闲超时自动清理。
+
+## FieldCache — 聚合统计缓存
+
+适用于**下拉选项、TopN 统计、Group By 汇总**,避免频繁执行 GROUP BY 查询。
+
+```csharp
+// 字段枚举值(如品类名称列表)
+var categories = new FieldCache<Product>("Category").FindAllName();
+
+// 按某字段统计数量(下拉选项 + 数量)
+var fieldCache = new FieldCache<Order>("Status")
+{
+    // 自定义显示格式:名称(数量)
+    DisplayFormat = "{name}({count})",
+    // 数量阈值(只显示 ≥5 条的项)
+    // WhereExpression = _.CreateTime >= DateTime.Today,
+};
+var items = fieldCache.DataSource;  // 返回 IDictionary<Object, String>
+
+// 在 Biz 文件中封装为属性(魔方自动识别用于下拉)
+public static FieldCache<Order> StatusCache { get; } =
+    new FieldCache<Order>(nameof(Status));
+```
+
+## DbCache — 跨进程数据库键值缓存
+
+适用于**无外部 Redis 但需要跨进程共享缓存**的场景,以数据库表存储键值对。
+
+```csharp
+// 获取默认实例(使用默认连接)
+var cache = DbCache.Default;
+
+// 写入(整数秒过期,0 = 永不过期)
+cache.Set("config:maxRetry", 3, 600);
+cache.Set("user:token:42", tokenObj, 3600);
+
+// 读取
+var retries = cache.Get<Int32>("config:maxRetry");
+var token = cache.Get<MyToken>("user:token:42");
+
+// 判断是否存在
+if (cache.ContainsKey("feature:darkmode")) { ... }
+
+// 删除
+cache.Remove("user:token:42");
+```
+
+**特点**:JSON 序列化、内置热点(高频读取时进程内二级缓存加速)、自动清理过期条目。
+
+## 缓存失效触发
+
+```csharp
+// 手动失效(如外部修改了数据需要刷新)
+User.Meta.Session.ClearCache("数据已更新", force: true);
+
+// 强制清空单对象缓存
+User.Meta.SingleCache.Clear();
+
+// 框架自动触发(无需手动调用):
+// - Entity.Insert() / Update() / Delete() 后自动调用 ClearCache
+// - EntityTransaction 回滚后强制清空
+```
+
+## 选型建议
+
+```
+数据量 ≤ 1000 条,读多写少(字典/配置/角色)
+    → EntityCache(FindAllWithCache)
+
+按 ID/Key 高频查单条(用户首页、订单详情)
+    → SingleCache(Meta.SingleCache[key])
+
+下拉列表 / Group By 统计 / TopN
+    → FieldCache(避免频繁聚合查询)
+
+无 Redis,跨进程共享少量键值
+    → DbCache(Set/Get)
+
+超大表 + 分布式共享
+    → NewLife.Redis(见 cache-provider-architecture 技能)
+```
+
+## 注意事项
+
+- `EntityCache` 适合**千条以内**的字典表;超过 1 万条建议改用 `SingleCache` 或直接查询。
+- `EntityCacheExpire`/`SingleCacheExpire` 是全局配置;单个实体需要不同过期时间时,在静态构造函数里单独设置 `Meta.Cache.Expire` / `Meta.SingleCache.Expire`。
+- `FieldCache` 返回的 `DataSource` 是快照,多次调用会共享同一结果直到过期。
+- 缓存读出的实体**不要修改后不 Save**;修改必须调用 `Update()`,否则下次缓存刷新后修改丢失。
Added +576 -0
diff --git a/.github/skills/xcode-entity-orm/SKILL.md b/.github/skills/xcode-entity-orm/SKILL.md
new file mode 100644
index 0000000..68823e9
--- /dev/null
+++ b/.github/skills/xcode-entity-orm/SKILL.md
@@ -0,0 +1,576 @@
+---
+name: xcode-entity-orm
+description: >
+  使用 NewLife.XCode 实体 ORM 进行增删改查操作,涵盖 Entity<T> 基类 CRUD API、
+  Biz 文件约定(FindByXxx/FindAllByXxx/Search 命名与参数顺序)、脏字段追踪、
+  Valid 验证钩子、内置拦截器(Time/User/IP/Trace 自动填充)、实体事务、
+  InitData 种子数据、EntityQueue 异步批写,以及 XCodeSetting 关键配置。
+  适用于数据库 CRUD 业务逻辑开发、实体类使用、代码审查任务。
+argument-hint: >
+  说明业务场景:需要做哪类数据操作(查询/写入/批量/异步);
+  是否需要事务;对延迟写入(日志/统计)是否有要求。
+---
+
+# XCode 实体 ORM
+
+## 适用场景
+
+- 使用实体类进行增删改查(CRUD)操作。
+- 在 `*.Biz.cs` 文件中封装数据层查询方法,规范命名和参数顺序。
+- 配置 `Valid` 钩子做字段校验和自动填充。
+- 使用内置拦截器自动填充审计字段(创建时间、操作人、IP、TraceId)。
+- 日志/统计场景使用 `SaveAsync` 异步批量写入,减少 DB 压力。
+- 审查实体类代码,确保遵循 Biz 文件约定。
+
+## Entity<T> 基础 CRUD
+
+```csharp
+// 新增
+var entity = new User { Name = "test", Password = "123456" };
+entity.Insert();
+
+// 查询单个(按主键)
+var user = User.FindByKey(1);
+// 查询单个(按条件)
+var user2 = User.Find(User._.Name == "test");
+
+// 查询列表
+var list = User.FindAll();
+var list2 = User.FindAll(User._.Status == 1, User._.Id.Desc(), null, 0, 10);
+
+// 分页查询
+var page = new PageParameter { PageIndex = 1, PageSize = 20 };
+var list3 = User.FindAll(User._.Status == 1, page);
+
+// 统计数量
+var count = User.FindCount(User._.Status == 1);
+
+// 更新
+user.Name = "newName";
+user.Update();
+
+// 删除
+user.Delete();
+
+// 保存(自动判断 Insert/Update)
+entity.Save();
+
+// 异步保存(适合日志/高频写入,延迟 3 秒批量写入)
+await entity.SaveAsync(3000);
+
+// 批量插入
+var list4 = new List<User>();
+for (var i = 0; i < 100; i++)
+    list4.Add(new User { Name = $"user{i}" });
+list4.Insert();
+
+// 批量更新(将 Status==1 改为 Status==2)
+User.Update(User._.Status == 2, User._.Status == 1);
+
+// 批量删除
+User.Delete(User._.Status == 0);
+
+// 异步查询(可选)
+var user3 = await User.FindAsync(User._.Id == 1);
+var list5 = await User.FindAllAsync(User._.Status == 1, page);
+```
+
+## Biz 文件约定
+
+所有数据层逻辑一律放在 `*.Biz.cs` 文件的 `#region 高级查询` 中。**外部调用方只传语义化参数**,不在外部拼接 `WhereExpression`。
+
+### 方法形式选择
+
+| 场景 | 方法形式 | 返回类型 | 说明 |
+|------|---------|---------|------|
+| 返回单个对象,参数 ≤2 | `FindByXxx` | `TEntity?` | 未找到返回 `null` |
+| 返回列表,参数 ≤2,无模糊查询/分页 | `FindAllByXxx` | `IList<TEntity>` | 未找到返回空列表 |
+| 参数较多,含模糊查询或分页 | `Search(...)` | `IList<TEntity>` | 未找到返回空列表 |
+| 实体缓存内过滤 | `FindAllCachedXxx` / `FindCachedXxx` | — | 调 `Meta.Cache.FindAll(...)` |
+
+**命名边界**:
+- `FindByXxx` → 单条记录(`TEntity?`)
+- `FindAllByXxx` 和 `Search` → 列表(`IList<TEntity>`,**绝不返回 null**)
+
+### Search 方法参数顺序
+
+```
+Search(业务过滤字段..., DateTime start, DateTime end, String? key, PageParameter page)
+```
+
+- 时间区间 `(start, end)` 在 key/page 左边
+- 模糊关键词 `String? key` 倒数第二
+- `PageParameter page` 始终最后
+
+### Biz 文件内的表达式简写
+
+在 Biz 类内部,可省略类名前缀直接用 `_`:
+
+```csharp
+// ✅ Biz 文件内部(推荐)
+var exp = _.UserId == userId;
+if (!keyword.IsNullOrEmpty()) exp &= _.Title.Contains(keyword.Trim());
+return FindAll(exp, page);
+
+// ❌ 外部业务代码(避免在外部拼接表达式)
+var exp = Conversation._.UserId == userId;
+```
+
+### 完整示例
+
+```csharp
+partial class Conversation
+{
+    #region 高级查询
+
+    /// <summary>根据用户编号查找最新一条会话</summary>
+    /// <param name="userId">用户编号</param>
+    /// <returns>会话对象,不存在时返回 null</returns>
+    public static Conversation? FindByUserId(Int32 userId) => Find(_.UserId == userId);
+
+    /// <summary>根据用户编号查找所有会话</summary>
+    /// <param name="userId">用户编号</param>
+    /// <returns>会话列表,不存在时返回空列表</returns>
+    public static IList<Conversation> FindAllByUserId(Int32 userId) =>
+        FindAll(_.UserId == userId);
+
+    /// <summary>分页搜索会话列表</summary>
+    /// <param name="userId">用户编号,0 不过滤</param>
+    /// <param name="start">创建时间起始</param>
+    /// <param name="end">创建时间截止</param>
+    /// <param name="key">标题关键字,空时不过滤</param>
+    /// <param name="page">分页参数</param>
+    /// <returns>会话列表,不存在时返回空列表</returns>
+    public static IList<Conversation> Search(Int32 userId, DateTime start, DateTime end,
+        String? key, PageParameter page)
+    {
+        var exp = new WhereExpression();
+        if (userId > 0) exp &= _.UserId == userId;
+        if (start > DateTime.MinValue) exp &= _.CreateTime >= start;
+        if (end > DateTime.MinValue) exp &= _.CreateTime < end;
+        if (!key.IsNullOrEmpty()) exp &= _.Title.Contains(key.Trim());
+
+        return FindAll(exp, page);
+    }
+
+    #endregion
+}
+```
+
+## 脏字段追踪
+
+实体从数据库加载后,自动追踪哪些字段被修改过,Update 时**仅更新脏字段**(减少 SQL 字段数):
+
+```csharp
+var user = User.FindByKey(1);
+user.HasDirty;              // 是否有未保存的修改
+user.Dirtys["Name"];        // Name 字段是否被修改
+user.IsDirty(nameof(Name)); // 同上
+```
+
+## Valid 验证钩子
+
+Insert/Update/Delete 前自动调用,**返回 false 阻止操作**:
+
+```csharp
+public override Boolean Valid(DataMethod method)
+{
+    // Delete 通常只做必要检查
+    if (method == DataMethod.Delete) return true;
+
+    // 未修改时跳过(提升 Update 性能)
+    if (!HasDirty) return true;
+
+    // 必填校验
+    if (Name.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Name), "名称不能为空");
+
+    // 长度校验
+    if (Name.Length > 50) throw new ArgumentException("名称不能超过50个字符", nameof(Name));
+
+    // 新增时特有逻辑(拦截器未覆盖时手动设置)
+    if (method == DataMethod.Insert)
+        Enable = true;
+
+    return base.Valid(method);  // 调用基类:触发拦截器链 + 字符串长度自动截断
+}
+```
+
+**`DataMethod` 枚举**:`None` / `Insert` / `Update` / `Delete` / `Upsert`
+
+## 内置拦截器(按字段名自动激活)
+
+实体包含特定命名字段时,**无需代码手动赋值**,拦截器在 Insert/Update 时自动填充:
+
+| 拦截器 | 匹配字段名 | 类型 | 触发时机 |
+|--------|-----------|------|---------|
+| `TimeInterceptor` | `CreateTime` | `DateTime` | Insert |
+| | `UpdateTime` | `DateTime` | Insert + Update |
+| `UserInterceptor` | `CreateUserID` | `Int32`/`Int64` | Insert |
+| | `CreateUser` | `String` | Insert |
+| | `UpdateUserID` | `Int32`/`Int64` | Insert + Update |
+| | `UpdateUser` | `String` | Insert + Update |
+| `IPInterceptor` | `CreateIP` | `String` | Insert |
+| | `UpdateIP` | `String` | Insert + Update |
+| `TraceInterceptor` | `TraceId` | `String` | Insert + Update |
+
+**关键细节**:
+- `UserInterceptor.AllowEmpty = false`(默认)— 无登录用户时不清空已记录的操作人
+- `TraceInterceptor.AllowMerge = true` — 多个 Trace 修改同一记录时联接所有 TraceId
+
+**Model.xml 建模约定**(对应字段设 `Model="False" Category="扩展"`,不暴露到模型类):
+
+```xml
+<Column Name="CreateTime"   DataType="DateTime" Nullable="False" Model="False" Category="扩展" Description="创建时间" />
+<Column Name="CreateUser"   DataType="String"                    Model="False" Category="扩展" Description="创建者" />
+<Column Name="CreateUserID" DataType="Int32"                     Model="False" Category="扩展" Description="创建者ID" />
+<Column Name="CreateIP"     DataType="String"                    Model="False" Category="扩展" Description="创建IP" />
+<Column Name="UpdateTime"   DataType="DateTime"                  Model="False" Category="扩展" Description="更新时间" />
+<Column Name="UpdateUser"   DataType="String"                    Model="False" Category="扩展" Description="更新者" />
+<Column Name="UpdateUserID" DataType="Int32"                     Model="False" Category="扩展" Description="更新者ID" />
+<Column Name="UpdateIP"     DataType="String"                    Model="False" Category="扩展" Description="更新IP" />
+<Column Name="TraceId"      DataType="String"                    Model="False" Category="扩展" Description="链路追踪" />
+```
+
+## EntityTransaction 实体事务
+
+```csharp
+// 推荐:强类型事务(自动绑定对应实体的数据库连接)
+using var et = new EntityTransaction<Order>();
+try
+{
+    var order = new Order { ... };
+    order.Insert();
+
+    var detail = new OrderDetail { OrderId = order.Id, ... };
+    detail.Insert();
+
+    et.Commit();  // 显式提交;未 Commit 则离开 using 时自动回滚
+}
+catch
+{
+    // using 块结束自动回滚
+    throw;
+}
+
+// 指定连接名事务
+using var et2 = new EntityTransaction(DAL.Create("Order"));
+```
+
+**缓存联动**:
+- 事务内裸 SQL → 强制清空缓存
+- 事务提交成功 → 按正常逻辑更新缓存
+- 事务回滚 → 强制清空所有缓存
+
+## InitData 种子数据
+
+**执行时机**:应用启动 → 首次访问实体 → 建表完成 → `InitData()` 执行一次。
+
+```csharp
+partial class Role
+{
+    protected override void InitData()
+    {
+        if (Meta.Count > 0) return;  // 幂等:已有数据直接返回
+
+        var roles = new[]
+        {
+            new Role { Name = "管理员", Enable = true, Sort = 1 },
+            new Role { Name = "普通用户", Enable = true, Sort = 2 },
+        };
+        roles.Insert();
+    }
+}
+```
+
+**特点**:线程安全(Monitor 保证只执行一次);`InitData` 内查询其他表时会自动触发对应表的 `WaitForInitData()`。
+
+## EntityQueue 异步批写
+
+高频写入场景(日志、访问统计等):将实体操作批量化、异步化降低 DB 压力。
+
+```csharp
+// 最简用法:SaveAsync 隐式使用队列
+await log.SaveAsync(3000);   // 延迟 3 秒批量写入
+
+// 直接使用队列(如批量 Insert Only)
+var queue = new EntityQueue(AccessLog.Meta.Session)
+{
+    Method = DataMethod.Insert,
+    InsertOnly = true,
+};
+foreach (var log2 in logs)
+    queue.Add(log2, 0);
+
+queue.Dispose();  // Dispose 等待最多 3s 完成剩余刷出
+```
+
+**关键配置**:
+
+| 属性 | 默认 | 说明 |
+|------|------|------|
+| `Method` | 自动 | 强制写入方式(Insert/Update/Upsert)|
+| `InsertOnly` | `false` | true 时仅批量 Insert,跳过 Upsert |
+| `MaxEntity` | 100 万 | 队列深度上限,超出时写入线程阻塞 15s |
+| `Speed` | 只读 | 上次刷出速度(TPS)|
+
+## XCodeSetting 关键配置
+
+通过 `XCode.json` 或 `appsettings.json` 的 `"XCode"` 节配置:
+
+| 配置项 | 默认值 | 说明 |
+|--------|--------|------|
+| `Debug` | `true` | 调试日志 |
+| `ShowSQL` | `true` | 输出 SQL 语句 |
+| `SQLPath` | `""` | SQL 日志独立目录(生产环境建议设置)|
+| `TraceSQLTime` | `1000` | 慢查询阈值(毫秒)|
+| `Migration` | `On` | 反向工程模式(Off/ReadOnly/On/Full)|
+| `BatchSize` | `5000` | 批量操作数据量 |
+| `EntityCacheExpire` | `10` | 实体缓存过期时间(秒)|
+| `SingleCacheExpire` | `10` | 单对象缓存过期时间(秒)|
+
+```json
+{
+  "XCode": {
+    "Migration": "Off",
+    "ShowSQL": false,
+    "SQLPath": "../SqlLog",
+    "TraceSQLTime": 500
+  }
+}
+```
+
+## EntityFactory 初始化(应用启动)
+
+```csharp
+// 初始化所有连接(建表 + 初始化种子数据)
+EntityFactory.InitAll();
+
+// 只初始化指定连接
+EntityFactory.InitConnection("Order");
+
+// 只初始化单个实体
+EntityFactory.InitEntity(typeof(User));
+
+// 异步并行初始化(多连接时推荐)
+await EntityFactory.InitAllAsync();
+```
+
+## 查询方法完整签名
+
+### 单对象查询(返回 TEntity?)
+
+| 方法 | 说明 |
+|------|------|
+| `FindByKey(Object key)` | 按主键查找,未找到返回 null |
+| `FindByKey(Object key, String? selects)` | 按主键查找,仅返回指定列 |
+| `FindByKeyForEdit(Object key)` | 按主键查找;**主键为空或记录不存在时返回新实例**,用于编辑页 |
+| `FindByKeyWithCache(Object key)` | 从 SingleCache 查找,适合只读高频访问 |
+| `FindBySlaveWithCache(String slaveKey)` | 按从键从 SingleCache 查找 |
+| `Find(Expression where)` | 按 WhereExpression 查找单条 |
+| `Find(Expression where, String? selects)` | 按表达式查找,指定返回列 |
+| `Find(String whereClause)` | 按字符串条件查找(无类型安全) |
+| `Find(String name, Object value)` | 按单字段名-值查找 |
+| `Find(String[] names, Object[] values)` | 按多字段名-值查找 |
+| `FindAsync(Expression where)` | 异步查找单条 |
+
+```csharp
+// 编辑页标准用法:id==0 或记录不存在时都返回空实体
+var user = User.FindByKeyForEdit(id);  // 不会返回 null
+user.Name = "new name";
+user.Save();  // 自动判断 Insert 还是 Update
+
+// 只读高频场景:先走单对象缓存
+var user2 = User.FindByKeyWithCache(1);
+```
+
+### FindAll 分页查询全集
+
+| 方法 | 说明 |
+|------|------|
+| `FindAll()` | 获取全表(大表慎用) |
+| `FindAll(Expression? where, String? order, String? selects, Int64 startRow, Int64 maxRows)` | 标准分页 |
+| `FindAll(Expression? where, PageParameter? page, String? selects)` | PageParameter 分页(推荐) |
+| `FindAll(String sql)` | 直接执行 SQL 返回实体列表 |
+| `FindAllWithCache()` | 从 EntityCache 查找全部 |
+| `FindAllAsync(Expression where, PageParameter? page, String? selects)` | 异步分页 |
+
+```csharp
+// PageParameter 标准分页
+var page = new PageParameter { PageIndex = 1, PageSize = 20 };
+var list = User.FindAll(User._.Status == 1, page);
+// 查询后 page.TotalCount 已被自动赋值
+
+// 带统计(同时返回 totalCount 与 state 合计值)
+page.RetrieveTotalCount = true;
+page.RetrieveState = true;
+```
+
+### 统计与聚合
+
+| 方法 | 返回 | 说明 |
+|------|------|------|
+| `FindCount()` | `Int64` | 全表计数 |
+| `FindCount(Expression where, ...)` | `Int64` | 条件计数 |
+| `FindCount(String where, ...)` | `Int32` | 字符串条件计数(遗留) |
+| `FindMin(String field, Expression? where)` | `Decimal` | 查字段最小值 |
+| `FindMax(String field, Expression? where)` | `Decimal` | 查字段最大值 |
+| `FindData(Expression, String, String, Int64, Int64)` | `DbTable` | 返回内存数据表(非实体列表) |
+
+```csharp
+var count = Order.FindCount(Order._.Status == 1);
+var minPrice = Order.FindMin("TotalAmount", Order._.Status == 1);
+var maxPrice = Order.FindMax("TotalAmount", Order._.Status == 1);
+
+// DbTable 适合统计汇总场景(避免创建大量实体对象)
+var dt = Order.FindData(Order._.Status == 1, "CreateTime", "Id,Status,TotalAmount", 0, 100);
+```
+
+### SQL 构建与子查询
+
+```csharp
+// 构造 SelectBuilder 用于外层包装
+var builder = User.CreateBuilder(User._.Status == 1, "Id desc", "Id,Name");
+
+// FindSQL 得到 SelectBuilder,用于子查询
+var subSql = User.FindSQL(null, null, "Id");
+// 外层可用 WHERE Id IN (subSql)
+```
+
+## 存在性检查
+
+```csharp
+partial class User
+{
+    public override void Valid(Boolean isNew)
+    {
+        base.Valid(isNew);
+        // 检查 Name 是否已存在(会自动排除当前记录)
+        CheckExist(isNew, nameof(Name));
+
+        // 组合唯一:Name+Email
+        CheckExist(isNew, nameof(Name), nameof(Email));
+    }
+}
+
+// 非 Valid 内部也可使用
+var exists = user.Exist(nameof(Name));        // 返回 bool
+var exists2 = user.Exist(true, nameof(Name)); // 显式指定 isNew
+```
+
+## 高并发 GetOrAdd 模式
+
+适合统计场景:按某键"查则返回,无则新建",避免并发重复插入:
+
+```csharp
+// 统计每天每用户的访问数,并发安全
+var stat = DailyStat.GetOrAdd(
+    userId,
+    (k, isNew) => DailyStat.FindByUserDate(k, today),   // 查找函数
+    k => new DailyStat { UserId = k, Date = today, Count = 0 }  // 创建函数
+);
+stat.Count++;
+stat.Update();
+```
+
+等效于 `Find → 若null则Create + Insert → 返回`,内部加锁防止并发重建。
+
+## 跨类型复制 CopyFrom
+
+```csharp
+// 将 UserModel(非实体类)的字段复制到 User 实体
+var user = new User();
+user.CopyFrom(model, setDirty: true, getDirty: false);
+user.Save();
+
+// 两个同类实体:仅复制有脏数据的字段(差量复制)
+var user2 = User.FindByKey(1);
+user2.CopyFrom(user, setDirty: true, getDirty: true);
+```
+
+## 静态批量操作
+
+### 批量更新(不加载实体)
+
+```csharp
+// UPDATE User SET Status=2 WHERE Status=1
+User.Update(User._.Status.SetValue(2), User._.Status == 1);
+
+// 累加字段(AdditionalFields 字段)
+User.Update("LoginCount=LoginCount+1,LastLoginTime=Now()", "Id=1");
+```
+
+### 批量删除(分批安全删除)
+
+```csharp
+// 删除全部满足条件的记录
+User.Delete(User._.Status == 0);
+
+// 分批删除:每批最多 1000 行,防止大批量删除锁表
+User.Delete(User._.Status == 0, maximumRows: 1000);
+```
+
+### 批量Insert(高性能)
+
+```csharp
+// BatchInsert:一次提交多行,比逐条 Insert 快 5-10x
+var list = new List<User> { ... };
+list.BatchInsert();
+
+// 忽略重复冲突的批量插入(MySQL 的 INSERT IGNORE)
+list.BatchInsertIgnore();
+
+// 自定义 BatchOption
+list.BatchInsert(new BatchOption { BatchSize = 500 });
+
+// 验证后批量插入(会触发 Valid 钩子)
+var validated = list.Valid(isNew: true);
+validated.BatchInsert();
+```
+
+## EntityTransaction 事务详解
+
+```csharp
+// 1. 强类型(最常用):自动绑定 TEntity 对应的数据库连接
+using var et = new EntityTransaction<Order>();
+try
+{
+    var order = new Order { ... };
+    order.Insert();                                // 同一事务内
+    new OrderDetail { OrderId = order.Id }.Insert();
+    et.Commit();
+}
+catch { throw; }  // using 块结束时自动 Rollback
+
+// 2. 指定隔离级别
+using var et2 = new EntityTransaction(DAL.Create("MyDb"), IsolationLevel.Serializable);
+
+// 3. 多个实体同一数据库(用 DAL 共享连接)
+var dal = DAL.Create("Order");
+using var et3 = new EntityTransaction(dal);
+// Order / OrderDetail 只要 ConnName 相同,都在同一事务内
+```
+
+**IsolationLevel 可选值**:`ReadUncommitted` / `ReadCommitted`(默认)/ `RepeatableRead` / `Serializable`
+
+## PageParameter 完整属性
+
+| 属性 | 类型 | 默认 | 说明 |
+|------|------|------|------|
+| `PageIndex` | `Int32` | 1 | 页码(1-based) |
+| `PageSize` | `Int32` | 20 | 每页行数 |
+| `StartRow` | `Int64` | -1 | 直接指定起始行(0-based,与 PageIndex 二选一) |
+| `TotalCount` | `Int64` | — | **查询后自动写入**总记录数 |
+| `RetrieveTotalCount` | `Boolean` | false | 是否统计总数 |
+| `State` | `Object?` | — | 统计附加数据(如合计金额)|
+| `RetrieveState` | `Boolean` | false | 是否检索 State |
+
+## 注意事项
+
+- **禁止在 `Valid` 外部手动赋值审计字段**(CreateTime/UpdateTime 等);拦截器自动处理。
+- `FindAll` 返回的是引用,避免在缓存列表上直接修改实体后不 `Update()`。
+- `SaveAsync` 有延迟,不适合需要立即可查的场景(用普通 `Save()`)。
+- `BatchFinder` 解决表关联的 N+1 查询,参见 `xcode-data-access-layer` 技能。
+- `FindByKeyForEdit` 解决编辑页 key==0 空对象问题,**不要手动判断 null**后再 `new`。
+- `Delete(Expression, maximumRows)` 支持分表;`Delete(String)` 不支持,大表批量删除必须用前者。
+- `BatchInsert` 不触发 `Valid`,需手动调用 `list.Valid(true)` 后再批量插入。
+- `GetOrAdd` 内部用 `Monitor.TryEnter` 加锁,高并发统计场景务必用此方法代替手写判断。
Added +309 -0
diff --git a/.github/skills/xcode-sharding-etl/SKILL.md b/.github/skills/xcode-sharding-etl/SKILL.md
new file mode 100644
index 0000000..2defa43
--- /dev/null
+++ b/.github/skills/xcode-sharding-etl/SKILL.md
@@ -0,0 +1,309 @@
+---
+name: xcode-sharding-etl
+description: >
+  使用 NewLife.XCode 实现分库分表和数据 ETL/同步,涵盖 TimeShardPolicy 时间分片策略配置
+  (TablePolicy/ConnPolicy/Step)、Meta.ShardPolicy 挂载、AutoShard 跨分片查询、
+  EntitySplit 手动切换分片,以及 ETL<TSource> 数据抽取框架(IExtracter 五种实现)
+  和 SyncManager 四阶段数据同步。
+  适用于大数据分表存储、跨分片聚合查询、数据迁移、增量同步等场景。
+argument-hint: >
+  说明数据规模和分表策略(按天/月/年);是否需要雪花 ID 路由;
+  是否需要跨分片聚合查询;ETL 抽取是增量还是全量;数据同步的源端和目标端类型。
+---
+
+# XCode 分表分库与 ETL
+
+## 分表分库(Shards)
+
+### 适用场景
+
+- 单表数据量超过千万,需要按时间维度(天/月/年)分表。
+- 需要跨分片查询历史数据并汇总结果。
+- 使用雪花 ID(Int64)作为主键,通过 ID 内嵌的时间偏移路由到正确分片。
+
+### TimeShardPolicy 配置
+
+```csharp
+// 实体静态构造函数中挂载分片策略
+static AccessLog()
+{
+    // 按天分表(单库)
+    Meta.ShardPolicy = new TimeShardPolicy(nameof(CreateTime), Meta.Factory)
+    {
+        TablePolicy = "{0}_{1:yyyyMMdd}",   // 表名格式:AccessLog_20250101
+        Step = TimeSpan.FromDays(1),         // 每 1 天一张表
+    };
+}
+
+static Order()
+{
+    // 按月分库分表(分库 + 分表)
+    Meta.ShardPolicy = new TimeShardPolicy(nameof(Id), Meta.Factory)
+    {
+        ConnPolicy  = "{0}_{1:yyyy}",        // 库名格式:Order_2025
+        TablePolicy = "{0}_{1:yyyyMM}",      // 表名格式:Order_202501
+        Step = TimeSpan.FromDays(30),        // 每 30 天(约一月)一张表
+    };
+}
+
+static EventLog()
+{
+    // 按年分表
+    Meta.ShardPolicy = new TimeShardPolicy(nameof(CreateTime), Meta.Factory)
+    {
+        TablePolicy = "{0}_{1:yyyy}",
+        Step = TimeSpan.FromDays(365),
+    };
+}
+```
+
+**`TimeShardPolicy` 参数说明**:
+
+| 参数 | 说明 |
+|------|------|
+| 第一参数(字段名)| 分片依据字段:`DateTime` 字段名 或 雪花 `Int64` 字段名 |
+| `Factory` | 实体工厂(`Meta.Factory`)|
+| `TablePolicy` | 表名格式,`{0}` = 实体表名,`{1}` = 时间 |
+| `ConnPolicy` | 库名格式(分库时配置),`{0}` = 连接名,`{1}` = 时间 |
+| `Step` | 每个分片的时间跨度 |
+
+### 数据写入(自动路由)
+
+框架根据实体的分片字段值自动路由到正确的表/库:
+
+```csharp
+// 自动路由写入对应分片(无需手动指定)
+var log = new AccessLog { CreateTime = DateTime.Now, ... };
+log.Insert();   // 自动写入 AccessLog_20250402 表
+
+// 批量 Insert 自动按分片分组写入
+var logs = batchLogs;
+logs.Insert();  // 自动分组,每组写入对应分片
+```
+
+### AutoShard — 跨分片查询
+
+跨多个分片自动执行查询并汇总结果:
+
+```csharp
+// 按时间区间跨分片查询(自动推导涉及的分片)
+var start = new DateTime(2025, 1, 1);
+var end = new DateTime(2025, 3, 31);
+
+var list = AccessLog.Meta.AutoShard(start, end, (session) =>
+{
+    // 在每个分片 session 中执行的查询
+    return AccessLog.FindAll(AccessLog._.CreateTime.Between(start, end));
+});
+
+// 跨分片统计
+var total = AccessLog.Meta.AutoShard(start, end, session =>
+    (Int64)AccessLog.FindCount()
+).Sum();
+```
+
+**自动条件裁剪**:单分片命中时自动移除冗余时间条件,减少 SQL 扫描范围。
+
+### EntitySplit — 手动切换分片
+
+精准指定连接和表名(运维迁移、手动归档场景):
+
+```csharp
+// 手动切换到指定分片(using 块内的查询都针对该分片)
+using (Order.Meta.CreateSplit("Order_2024", "Order_202412"))
+{
+    var orders = Order.FindAll(Order._.Status == 1);
+}
+
+// 按策略路由(输入时间,让策略计算目标分片)
+using (Order.Meta.CreateShard(new DateTime(2024, 12, 15)))
+{
+    var order = Order.FindByKey(orderId);
+}
+```
+
+---
+
+## ETL 数据抽取框架
+
+### 适用场景
+
+- 大表增量同步(按时间字段滑动窗口分批抽取)。
+- 历史数据全量迁移(分页或按 ID 区间)。
+- 跨库数据转换处理(源端 ≠ 目标端格式)。
+
+### ETL<TSource> 基类
+
+```csharp
+// 继承 ETL 实现自定义处理
+public class OrderETL : ETL<Order>
+{
+    /// <summary>处理一批数据</summary>
+    protected override Int32 Process(IList<Order> list, DataContext ctx)
+    {
+        foreach (var order in list)
+        {
+            // 转换并写入目标
+            var stat = BuildStat(order);
+            stat.Save();
+        }
+        return list.Count;
+    }
+}
+
+// 配置并启动
+var etl = new OrderETL
+{
+    Setting = new ExtractSetting
+    {
+        Start = new DateTime(2025, 1, 1),  // 抽取起始时间
+        End = DateTime.Today,              // 抽取截止时间
+        BatchSize = 1000,                  // 每批数量
+        Step = TimeSpan.FromMinutes(10),   // 时间窗口步长(TimeExtracter)
+    }
+};
+etl.Start();
+```
+
+### IExtracter 抽取器族谱
+
+| 抽取器 | 场景 | 特点 |
+|-------|------|------|
+| `TimeExtracter` | **增量同步**(默认)| 按时间字段递增,游标滑动 |
+| `TimeSpanExtracter` | 补跑历史 | 固定时间步长循环,可重复执行 |
+| `PagingExtracter` | 全量表(无时间字段)| Row 偏移分页 |
+| `IdExtracter` | 自增主键 | 按连续 ID 区间分批 |
+| `EntityIdExtracter` | 复合主键 | 按实体主键分批 |
+
+```csharp
+// 默认使用 TimeExtracter(推荐增量场景)
+var etl = new OrderETL();
+
+// 指定抽取器
+var etl2 = new OrderETL
+{
+    Extracter = new PagingExtracter { BatchSize = 500 }
+};
+
+// IdExtracter(适合自增 ID 表)
+var etl3 = new OrderETL
+{
+    Extracter = new IdExtracter { BatchSize = 1000 }
+};
+```
+
+### ExtractSetting 配置
+
+```csharp
+var setting = new ExtractSetting
+{
+    Start = new DateTime(2024, 1, 1),    // 抽取起始时间/ID
+    End = DateTime.Now,                  // 抽取截止
+    Offset = TimeSpan.FromMinutes(-5),   // 安全偏移(避免抽取未完成写入的数据)
+    BatchSize = 1000,                    // 每批记录数
+    Step = TimeSpan.FromHours(1),        // 每步时间跨度(TimeExtracter)
+};
+```
+
+### IETLModule 生命周期钩子
+
+```csharp
+public class MyModule : IETLModule
+{
+    public void OnInit(ETL etl) { /* 初始化资源 */ }
+
+    public void OnProcessing(ETL etl, DataContext ctx)
+    {
+        // 每批数据处理前后的逻辑(统计/日志/报警)
+        ctx.TotalCount  // 累计处理总数
+        ctx.Speed       // 当前处理速度(条/秒)
+    }
+
+    public void OnStop(ETL etl) { /* 释放资源 */ }
+}
+
+etl.Modules.Add(new MyModule());
+```
+
+---
+
+## SyncManager — 数据同步框架
+
+### 适用场景
+
+- 主从库数据双向同步(如主数据中心 → 分支机构)。
+- 业务系统间数据同步(保持多个数据库中某张表的一致性)。
+- 需要冲突检测和解决策略的场景。
+
+### 四阶段同步流程
+
+```
+ProcessNew     从方新增处理(避免主键冲突,先同步主方还没有的新数据)
+    ↓
+ProcessDelete  从方删除处理(清理已在主方删除的数据)
+    ↓
+ProcessItems   主方增量数据(分批拉取,更新从方)
+    ↓
+ProcessOthers  检查本地未涉及的数据在主方是否仍存在
+```
+
+### ISyncMaster / ISyncSlave
+
+```csharp
+// 主方:实现 ISyncMaster(数据提供者)
+public class OrderMaster : ISyncMaster
+{
+    // 获取指定时间后修改的主键集合
+    public IList<Object> GetAdd(DateTime last, Int32 count) =>
+        Order.FindAll(Order._.UpdateTime >= last, Order._.Id, "Id", 0, count)
+             .Select(e => (Object)e.Id).ToList();
+
+    // 获取已删除的主键集合(需要额外的删除日志表)
+    public IList<Object> GetDelete(DateTime last) => [];
+
+    // 获取一批完整数据
+    public IList<IEntity> GetItems(IList<Object> keys) =>
+        Order.FindAll(Order._.Id.In(keys)).Cast<IEntity>().ToList();
+}
+
+// 从方:实现 ISyncSlave(数据消费者)
+// 通常用泛型适配器,处理 LastSync / SyncStatus 字段
+```
+
+### SyncManager 配置与启动
+
+```csharp
+var sync = new SyncManager
+{
+    Master = new OrderMaster(),         // 主方
+    BatchSize = 200,                    // 每批处理数
+    UpdateConflictByLastUpdate = true,  // 冲突时比较 UpdateTime(true=本地时间新则不覆盖)
+    Names = new[] { "Status", "Amount", "UpdateTime" },  // 参与同步的字段
+};
+
+sync.Start();   // 开始同步
+// sync.Stop(); // 停止
+```
+
+### 冲突解决策略
+
+| 场景 | `UpdateConflictByLastUpdate=false`(默认主覆盖)| `=true`(按时间戳)|
+|------|------|------|
+| 从方改、主方不变 | 主方覆盖从方 | 从方推送到主方 |
+| 从方不变、主方改 | 主方更新从方 | 主方更新从方 |
+| 双方同时修改 | 主方覆盖从方 | 比较时间戳,较新者胜 |
+
+### 从方辅助字段
+
+```xml
+<!-- 同步框架使用的辅助字段(在 Model.xml 中添加) -->
+<Column Name="LastSync"   DataType="DateTime" Description="最近同步时间" />
+<Column Name="SyncStatus" DataType="Int32"    Description="同步状态。1=新增待同步 2=删除待同步" />
+```
+
+## 注意事项
+
+- 分表实体的 `InitData` 不执行建表检查(分片表由运行时按策略创建),不要在分片实体中做数据初始化。
+- `AutoShard` 跨分片查询是串行执行的(默认),大量分片时性能随分片数线性增加;适合查询范围确定的场景。
+- `TimeExtracter` 使用滑动游标,断点续跑时从 `ExtractSetting.Start` 的上次位置继续,注意持久化游标位置。
+- ETL `Process` 方法应做幂等处理(同一批数据重复处理不产生副作用),以应对失败重试。