Add cube-oauth-sso and cube-webapi skills documentation - Introduced cube-oauth-sso skill for implementing OAuth/SSO with NewLife.Cube, detailing configuration, built-in providers, and usage scenarios. - Added cube-webapi skill for developing backend API services using NewLife.Cube, covering service registration, controller types, JWT authentication, and Swagger integration.大石头 authored at 2026-04-02 19:19:00
diff --git a/.github/skills/cube-jobs/SKILL.md b/.github/skills/cube-jobs/SKILL.md
new file mode 100644
index 0000000..b5de3ad
--- /dev/null
+++ b/.github/skills/cube-jobs/SKILL.md
@@ -0,0 +1,271 @@
+---
+name: cube-jobs
+description: >
+ 使用 NewLife.Cube 内置定时作业体系(ICubeJob/CubeJobBase/CronJobAttribute)
+ 开发和管理周期性后台任务,涵盖接口定义、强类型参数、Cron 表达式、
+ 依赖注入构造、分布式锁防重、Job.Data 状态持久化、内置作业(HttpService/SqlService/BackupDbService),
+ 以及 AddCubeJob() 注册与管理后台热修改。
+ 适用于定时数据统计、定时同步、HTTP 回调、SQL 批处理、数据库备份等后台任务场景。
+argument-hint: >
+ 说明任务类型和触发频率:是纯计算任务、HTTP 请求、SQL 操作还是 XCode 数据库读写;
+ 是否需要注入其他服务;是否需要跨执行持久化状态(时间指针等)。
+---
+
+# Cube 定时作业
+
+## 适用场景
+
+- 定期执行数据统计、日志清理、数据同步、消息推送等后台任务。
+- 利用内置 HTTP 作业定时调用外部接口(如 Webhook、健康检查)。
+- 利用内置 SQL 作业定时执行数据库维护语句。
+- 在管理后台界面热修改 Cron 表达式、参数或启停状态,无需重启服务。
+- 分布式部署时通过魔方内置锁机制防止多节点重复执行。
+
+---
+
+## 核心接口
+
+### ICubeJob — 作业接口
+
+```csharp
+public interface ICubeJob
+{
+ /// <summary>执行定时作业</summary>
+ /// <param name="argument">JSON 格式参数字符串</param>
+ /// <returns>执行结果日志消息</returns>
+ Task<String> Execute(String argument);
+}
+```
+
+### CubeJobBase / CubeJobBase<TArgument>
+
+```csharp
+// 基础基类(手动解析参数)
+public abstract class CubeJobBase : ICubeJob
+{
+ public CronJob Job { get; set; } // 当前作业实体,包含 Data/NextTime 等字段
+ public abstract Task<String> Execute(String argument);
+}
+
+// 泛型基类(自动 JSON 反序列化参数,推荐使用)
+public abstract class CubeJobBase<TArgument> : CubeJobBase where TArgument : class, new()
+{
+ public override async Task<String> Execute(String argument)
+ {
+ var arg = argument.IsNullOrEmpty() ? new TArgument()
+ : argument.ToJsonEntity<TArgument>();
+ return await OnExecute(arg);
+ }
+
+ protected abstract Task<String> OnExecute(TArgument argument);
+}
+```
+
+### CronJobAttribute — 声明式注册
+
+```csharp
+[AttributeUsage(AttributeTargets.Class)]
+public class CronJobAttribute(String name, String cron) : Attribute
+{
+ public String Name { get; set; } = name; // 数据库唯一名,修改后视为新作业
+ public String Cron { get; set; } = cron; // 初始 Cron,后续以数据库为准
+ public Boolean Enable { get; set; } // 首次创建时是否自动启用
+}
+```
+
+---
+
+## 快速自定义作业(推荐模式)
+
+```csharp
+using NewLife.Cube.Jobs;
+
+/// <summary>用户活跃度统计</summary>
+[DisplayName("用户活跃度统计")]
+[Description("每小时统计近30分钟的活跃用户数")]
+[CronJob("StatActiveUsers", "0 0 * * * ? *", Enable = true)] // 每小时整点
+public class StatActiveUsersJob : CubeJobBase<StatArgument>
+{
+ private readonly ITracer _tracer;
+
+ // 支持构造函数依赖注入
+ public StatActiveUsersJob(ITracer tracer)
+ {
+ _tracer = tracer;
+ }
+
+ protected override async Task<String> OnExecute(StatArgument arg)
+ {
+ using var span = _tracer?.NewSpan(nameof(StatActiveUsersJob), arg);
+
+ // 从 Job.Data 读取上次时间指针(跨执行持久化)
+ var lastTime = Job.Data.IsNullOrEmpty()
+ ? DateTime.Now.AddMinutes(-arg.MinutesBack ?? -30)
+ : Job.Data.ToDateTime();
+
+ var count = User.FindCount(User._.LastLoginTime > lastTime);
+
+ // 更新时间指针供下次执行使用
+ Job.Data = DateTime.Now.ToString("O");
+ Job.Update();
+
+ return $"统计完成,活跃用户数:{count},时间范围:{lastTime:t}~{DateTime.Now:t}";
+ }
+}
+
+public class StatArgument
+{
+ /// <summary>统计时间窗口(分钟),默认 30</summary>
+ public Int32? MinutesBack { get; set; } = 30;
+}
+```
+
+---
+
+## 注册作业
+
+### AddCubeJob() — 服务注册
+
+```csharp
+// Program.cs
+builder.Services.AddCube();
+builder.Services.AddCubeJob(); // 注册作业调度后台服务 + 自动扫描 ICubeJob 实现类
+```
+
+`AddCubeJob()` 完成:
+1. 注册 `JobService` 为 `IHostedService`(在后台调度作业)。
+2. 启动时调用 `BackupDbService.Init()`(注册内置备份作业)。
+3. 调用 `JobService.ScanJobs()` 反射扫描所有 `[CronJob]` 特性类,自动创建/更新数据库记录。
+
+### 扫描机制
+
+- 扫描范围:当前 AppDomain 中所有实现 `ICubeJob` 的类型。
+- 若数据库中已存在同名(`Name`)作业,**仅更新 DisplayName/Method/Remark**,不重置 Cron/参数/启用状态。
+- 若不存在,则新增记录,使用 `CronJobAttribute` 中的初始值。
+
+### 传统静态方法注册(兼容)
+
+```csharp
+// 在 Program.cs 或 BackupDbService.Init() 中
+CronJob.Add(null, MyClass.MyStaticMethod, "5 0 0 * * ? *", enable: false);
+// 参数1:DisplayName(null 取方法 DisplayName 特性)
+// 参数2:静态方法委托
+// 参数3:Cron 表达式
+// 参数4:是否启用
+```
+
+---
+
+## 内置作业
+
+### HttpService — HTTP 请求作业
+
+```csharp
+// 注册名:"RunHttp",默认 Cron:"25 0 0 * * ? *"(每天 00:00:25),默认禁用
+
+// 参数(在管理后台的"参数"字段填 JSON):
+{
+ "Method": "GET", // GET 或 POST
+ "Url": "https://api.example.com/trigger",
+ "Body": "{\"key\":\"value\"}" // POST 时的请求体
+}
+```
+
+### SqlService — SQL 执行作业
+
+```csharp
+// 注册名:"RunSql",默认 Cron:"15 * * * * ? *"(每分钟第15秒),默认禁用
+
+// 参数:
+{
+ "ConnName": "Master", // DAL 连接名
+ "Sql": "DELETE FROM AccessLog WHERE CreateTime < DATEADD(DAY,-30,GETDATE())"
+}
+```
+
+### BackupDbService — SQLite 备份作业
+
+```csharp
+// 注册名:"BackupDb",默认 Cron:"5 0 0 * * ? *"(每天 00:00:05),默认禁用
+
+// 参数(字符串,逗号分隔连接名):
+"Cube,Log" // 备份 Cube 库和 Log 库(仅支持 SQLite)
+```
+
+---
+
+## CronJob 实体字段
+
+```csharp
+public class CronJob
+{
+ public Int32 Id { get; set; }
+ public String Name { get; set; } // 唯一名(代码级固定)
+ public String DisplayName { get; set; } // 显示名(管理后台可编辑)
+ public String Cron { get; set; } // Cron 表达式(管理后台可修改)
+ public String Method { get; set; } // ICubeJob 实现类全名 或 静态方法全名
+ public String Argument { get; set; } // JSON 参数(管理后台可修改)
+ public String Data { get; set; } // 跨执行持久化数据(由 Job 自写)
+ public Boolean Enable { get; set; } // 启停(管理后台可切换)
+ public Boolean EnableLog { get; set; } // 是否记录每次执行日志
+ public DateTime LastTime { get; set; } // 上次执行时间
+ public DateTime NextTime { get; set; } // 下次执行时间(由调度服务计算)
+}
+```
+
+---
+
+## Cron 表达式速查
+
+魔方使用 Quartz 风格 7 段 Cron(秒 分 时 日 月 周 年):
+
+| 表达式 | 含义 |
+|--------|------|
+| `0 0 * * * ? *` | 每小时整点 |
+| `0 0 0 * * ? *` | 每天 00:00:00 |
+| `0 0 2 * * ? *` | 每天凌晨 02:00 |
+| `0 0 0 1 * ? *` | 每月1日 00:00 |
+| `0 0 0 * * SUN ? *` | 每周日 00:00 |
+| `0/30 * * * * ? *` | 每30秒一次 |
+| `0 0/5 * * * ? *` | 每5分钟一次 |
+| `0 30 8-18 * * ? *` | 工作时间每小时30分 |
+| `5 0 0 * * ? *` | 每天 00:00:05(避开整点) |
+| `15 * * * * ? *` | 每分钟第15秒 |
+
+---
+
+## 分布式执行
+
+- `JobService` 在执行前检查分布式锁(基于数据库 `CronJob.LastTime` 或 Redis)。
+- 若检测到另一节点正在执行同名作业(`CheckRunning` 返回 true),**本次跳过**,写入调试日志。
+- 无需额外配置,内置自动防重复执行。
+
+---
+
+## 管理后台操作
+
+| 功能 | 路径 |
+|------|------|
+| 查看所有作业列表 | 系统管理 → 定时作业 |
+| 修改 Cron 表达式 | 编辑作业 → Cron 字段 |
+| 修改参数 | 编辑作业 → 参数字段(JSON) |
+| 手动立即执行 | 列表 → 执行按钮 |
+| 查看执行日志 | 列表 → 日志明细 |
+
+---
+
+## 常见例外与注意事项
+
+- `CronJobAttribute.Name` 是数据库唯一键,修改后视为新作业,旧记录不会自动删除(需手动处理)。
+- `CronJobAttribute.Cron` 仅在**首次创建**记录时有效,之后以数据库中的值为准(避免代码更新覆盖管理员的手动配置)。
+- `Job.Data` 在 `OnExecute` 中写入后需手动调用 `Job.Update()` 才能持久化。
+- 依赖注入作用域:`JobService` 从 `IServiceProvider` 使用 `GetService` 获取 Job 实例,默认以**单例**方式解析(非 Scoped),如需 Scoped 服务需手动创建 Scope。
+- 静态方法方式不支持依赖注入,推荐新代码全部使用 `CubeJobBase<TArgument>`。
+
+## 推荐检查项
+
+- [ ] 是否已调用 `AddCubeJob()`(否则 Job 不会被扫描和调度)
+- [ ] `CronJobAttribute.Name` 是否全局唯一(同名作业在同一应用中只会保留一条记录)
+- [ ] 需要跨执行持久化的数据是否通过 `Job.Data + Job.Update()` 保存
+- [ ] 首次启动后是否在管理后台启用所需作业(内置作业默认 `Enable = false`)
+- [ ] 需注入服务的 Job 是否已将服务注册到 DI 容器
diff --git a/.github/skills/cube-membership/SKILL.md b/.github/skills/cube-membership/SKILL.md
new file mode 100644
index 0000000..072652d
--- /dev/null
+++ b/.github/skills/cube-membership/SKILL.md
@@ -0,0 +1,421 @@
+---
+name: cube-membership
+description: >
+ 使用 NewLife.Cube 的用户认证与权限管理体系,涵盖 ManageProvider 用户上下文管理、
+ UserService 登录/注册/验证码(密码/短信/邮件三模式)、PasswordService 密码强度验证、
+ TokenService JWT 颁发与验证(HS256/RS256 算法格式)、AccessService 访问日志,
+ 以及 User/Role/UserToken/UserOnline/UserConnect 核心实体操作。
+ 适用于用户登录、注册、密码策略、令牌颁发、在线会话统计、权限检查等场景。
+argument-hint: >
+ 说明场景:是登录鉴权(密码/短信/邮件)、注册流程、密码策略、
+ 还是 JWT 颁发与验证?是否需要在线会话或访问日志?
+---
+
+# Cube Membership 用户认证与权限
+
+## 适用场景
+
+- 实现用户密码登录、短信验证码登录、邮件验证码登录。
+- 发送短信/邮件验证码(含防刷限流机制)。
+- 配置密码强度正则策略,强制密码复杂度要求。
+- 颁发和验证 JWT 令牌(应用级 Token)。
+- 获取当前登录用户、当前租户,管理会话生命周期。
+- 记录和查询用户访问日志与在线状态统计。
+
+---
+
+## ManageProvider — 用户上下文
+
+### 获取当前用户
+
+```csharp
+// 方式1:通过静态属性(推荐,已绑定到当前请求上下文)
+var user = ManageProvider.User as User;
+
+// 方式2:从控制器基类属性
+// ControllerBaseX 中直接可用:
+var user = CurrentUser as User;
+
+// 方式3:通过提供者实例
+var user = ManageProvider.Provider.GetCurrent();
+```
+
+### 登录与注销
+
+```csharp
+// 密码登录(基础接口,推荐通过 UserService.Login())
+var provider = ManageProvider.Provider;
+provider.Login(username, password, remember: true);
+
+// 注销(清理 Session + Cookie)
+provider.Logout();
+
+// 手动设置当前用户(如 OAuth 回调后)
+provider.SetCurrent(user);
+```
+
+### 委托代理(Identity Delegation)
+
+```csharp
+// 检查是否有可用的身份委托(用于代理另一用户身份)
+// Login() 内部自动调用,无需手动触发
+provider.CheckAgent(user); // 若存在有效代理,返回被代理用户
+
+// 查询某用户的所有有效代理
+var agents = PrincipalAgent.GetAllValidByAgentId(userId);
+```
+
+### ManageProvider 关键属性
+
+| 属性 | 说明 |
+|------|------|
+| `ManageProvider.User` | 当前请求的登录用户(静态,绑定到 AsyncLocal) |
+| `ManageProvider.Provider` | 当前提供者实例(根据租户上下文切换) |
+| `ManageProvider.Menu` | 菜单管理器(用于菜单权限查找) |
+| `TenantContext.CurrentId` | 当前租户 ID |
+
+---
+
+## UserService — 统一登录服务
+
+### 三种登录模式
+
+```csharp
+// 注入方式
+[Inject] public UserService UserSvc { get; set; }
+
+// 构建登录请求模型
+var model = new LoginModel
+{
+ Username = "alice",
+ Password = "MyPassword123",
+ LoginCategory = LoginCategory.Password, // 必填:密码/手机/邮件
+ Remember = true,
+ Pkey = "rsa-key-id", // 用于解密前端 RSA 加密密码
+};
+
+// 统一登录入口(自动分发到对应模式)
+ServiceResult<IToken> result = UserSvc.Login(model, HttpContext);
+if (result.IsSuccess)
+{
+ var token = result.Data; // IToken: AccessToken + RefreshToken + ExpireIn
+ return Json(0, "登录成功", token);
+}
+else
+{
+ return Json(401, result.Message);
+}
+```
+
+**LoginCategory 枚举:**
+
+| 值 | 说明 |
+|----|------|
+| `Password` | 账号密码登录(支持前端 RSA 加密密码) |
+| `Phone` | 手机验证码登录(自动注册新用户) |
+| `Email` | 邮箱验证码登录(自动注册新用户) |
+
+### 发送验证码
+
+```csharp
+// 发送短信验证码(自动防刷:60秒间隔 + IP限5次/10分)
+var record = await UserSvc.SendVerifyCode(new VerifyCodeModel
+{
+ Username = "13800138000", // 手机号
+ Channel = "Sms", // "Sms" 或 "Mail"
+ Action = "login", // "login" / "bind" / "reset" / "notify"
+}, ip: UserHost);
+
+// 发送邮件验证码
+var record = await UserSvc.SendVerifyCode(new VerifyCodeModel
+{
+ Username = "alice@example.com",
+ Channel = "Mail",
+ Action = "reset", // 重置密码场景
+}, ip: UserHost);
+```
+
+**Action 类型说明:**
+
+| Action | 场景 | 缓存前缀隔离 |
+|--------|------|-------------|
+| `login` | 验证码登录 | 独立计数器 |
+| `bind` | 绑定手机/邮件 | 独立计数器 |
+| `reset` | 重置密码 | 独立计数器 |
+| `notify` | 通知(默认) | 独立计数器 |
+
+### 在线会话管理
+
+```csharp
+// 更新用户在线记录(RunTimeMiddleware 自动调用)
+var online = UserSvc.SetWebStatus(
+ online: existing,
+ sessionId: HttpContext.Session?.Id,
+ deviceId: Request.Cookies["deviceId"],
+ page: Request.Path,
+ status: "ok",
+ userAgent: new UserAgentParser(Request.Headers["User-Agent"]),
+ user: ManageProvider.User,
+ ip: UserHost
+);
+
+// 清理20分钟无活动的过期会话(DataRetentionService 自动调用)
+var expired = UserSvc.ClearExpire(secTimeout: 20 * 60);
+```
+
+---
+
+## PasswordService — 密码强度验证
+
+```csharp
+// 注入或通过 DI 获取
+[Inject] public PasswordService PwdSvc { get; set; }
+
+// 验证密码是否符合强度要求(基于 CubeSetting.PaswordStrength 正则)
+var valid = PwdSvc.Valid("MyPassword123");
+if (!valid)
+ throw new Exception("密码不符合要求:需包含大小写字母和数字,至少8位");
+```
+
+**配置密码强度(appsettings.json):**
+
+```json
+{
+ "Cube": {
+ "PaswordStrength": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$"
+ }
+}
+```
+
+| 正则示例 | 要求 |
+|---------|------|
+| `*` 或空 | 无要求(默认) |
+| `^.{6,}$` | 至少6位 |
+| `^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,}$` | 大小写+数字+8位 |
+| `^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^\\w]).{10,}$` | 大小写+数字+特殊字符+10位 |
+
+---
+
+## TokenService — JWT 令牌
+
+### 颁发令牌
+
+```csharp
+// 注入
+[Inject] public TokenService TokenSvc { get; set; }
+
+// 颁发(name=用户名或应用名,secret 格式:"算法:密钥")
+var token = TokenSvc.IssueToken(
+ name: user.Name,
+ secret: CubeSetting.Current.JwtSecret, // "HS256:MySecretKey..."
+ expire: 7200, // 有效期(秒)
+ id: Rand.NextString(8) // 可选:令牌 ID
+);
+// 返回 IToken { AccessToken, TokenType="JWT", ExpireIn=7200, RefreshToken }
+```
+
+### 验证令牌
+
+```csharp
+// 验证并解码(含异常)
+var (jwt, app) = TokenSvc.DecodeToken(token, CubeSetting.Current.JwtSecret);
+var username = jwt.Subject; // 令牌中的用户名
+var expireAt = jwt.Expire; // 过期时间
+
+// 验证并返回异常对象(不抛出)
+var (jwt, ex) = TokenSvc.DecodeTokenWithError(token, jwtSecret);
+if (ex != null) return Json(403, "令牌无效:" + ex.Message);
+```
+
+### 自动续签
+
+```csharp
+// 过期前 10 分钟内返回新令牌,其余时间返回 null
+var newToken = TokenSvc.ValidAndIssueToken(user.Name, oldToken, jwtSecret, expire: 7200);
+if (newToken != null) Response.Headers["X-Token"] = newToken.AccessToken;
+```
+
+### JWT 密钥格式
+
+```
+"算法:密钥"
+```
+
+| 格式示例 | 算法 | 说明 |
+|---------|------|------|
+| `HS256:MySecretKey123456` | HMAC-SHA256 | 对称密钥,推荐长度 ≥ 32 字符 |
+| `RS256:-----BEGIN RSA PRIVATE KEY-----...` | RSA-SHA256 | 非对称,验证时用公钥 |
+
+---
+
+## 应用授权(TokenService.Authorize)
+
+用于验证接入应用(App 实体)的身份,支持 IP 白名单。
+
+```csharp
+// 验证应用凭证(不存在时根据 autoRegister 决定是否自动创建)
+var app = TokenSvc.Authorize(
+ username: "OrderWeb", // 应用名
+ password: "app-secret-key", // 应用密钥
+ autoRegister: false, // 是否允许自动注册新应用
+ ip: UserHost // 请求来源 IP(用于白名单检查)
+);
+```
+
+---
+
+## 核心实体速查
+
+### User(用户)
+
+```csharp
+// 常用查找方法
+User.FindByName(username)
+User.FindByMobile(mobile)
+User.FindByMail(mail)
+User.FindByKey(id)
+User.Find(User._.Code == code)
+
+// 密码验证(基于 Membership.User 基类)
+var user = User.FindByName(username);
+if (user.Password != PasswordService.Hash(password, user.Name))
+ throw new Exception("密码错误");
+
+// 常用字段
+user.Name // 登录名
+user.DisplayName // 显示名/昵称
+user.Mail // 邮箱
+user.Mobile // 手机号
+user.Avatar // 头像 URL
+user.Enable // 是否启用
+user.Roles // 角色列表(IRole[])
+user.IsAdmin // 是否超级管理员
+```
+
+### Role(角色)
+
+```csharp
+Role.FindByName(roleName)
+Role.FindAllByNames(roleNames)
+
+// 判断用户是否有某角色
+user.Roles.Any(r => r.Name == "Admin");
+
+// 检查权限
+user.Has(menu, PermissionFlags.Update)
+```
+
+### UserToken(用户令牌记录)
+
+```csharp
+// 查找有效 Token 记录(可用于实现 Token 黑名单/刷新)
+UserToken.FindByToken(accessToken)
+UserToken.FindAllByUserId(userId)
+
+// 核心字段
+token.Token // AccessToken 字符串
+token.RefreshToken // 刷新令牌
+token.Expire // 过期时间
+token.DeviceId // 设备 ID
+token.Enable // 是否有效
+```
+
+### UserConnect(第三方账号绑定)
+
+```csharp
+// 通过第三方账号查找binding
+UserConnect.FindByProviderAndOpenID("Weixin", openID)
+UserConnect.FindAllByUserId(userId)
+
+// 核心字段
+uc.Provider // "Weixin" / "DingTalk" / "QQ" 等
+uc.OpenID // 第三方 OpenID
+uc.UnionID // 第三方 UnionID(跨应用)
+uc.UserID // 本地用户 ID
+uc.NickName // 第三方昵称
+uc.Avatar // 第三方头像 URL
+```
+
+### UserOnline(在线会话)
+
+```csharp
+UserOnline.FindBySessionID(sessionId)
+UserOnline.FindAllByUserId(userId)
+
+// 核心字段
+online.SessionID // Session 标识
+online.DeviceId // 设备 ID
+online.Page // 当前页面路径
+online.Platform // 操作系统平台
+online.Brower // 浏览器
+online.NetType // 网络类型(Wifi/4G 等)
+online.OnlineTime // 在线时长(秒)
+online.Address // IP 归属地
+```
+
+---
+
+## AccessService — 访问日志
+
+```csharp
+// AccessService 由 RunTimeMiddleware 自动调用,无需手动使用。
+// 管理后台查看日志:系统管理 → 访问日志
+
+// 若需手动记录
+var access = new UserVisit
+{
+ UserId = ManageProvider.User?.ID ?? 0,
+ Page = Request.Path,
+ Action = "API调用",
+ Ip = UserHost,
+ TraceId = DefaultSpan.Current?.TraceId,
+};
+access.Insert();
+```
+
+---
+
+## 限流与安全配置(CubeSetting)
+
+```csharp
+var set = CubeSetting.Current;
+
+// 登录安全
+set.MaxLoginError // 最大错误次数(默认5),超出后封禁
+set.LoginForbiddenTime // 封禁时长(秒,默认300)
+
+// 密码策略
+set.PaswordStrength // 正则表达式,空或"*"=不限制
+
+// 令牌配置
+set.JwtSecret // "算法:密钥" 格式(必须配置!)
+set.TokenExpire // Token 有效期(秒,默认7200)
+
+// 注册策略
+set.AllowRegister // 是否允许新用户注册
+set.AutoRegister // OAuth 登录后是否自动注册
+set.DefaultRole // 新注册用户的默认角色名(默认"普通用户")
+
+// 会话策略
+set.SessionTimeout // 会话超时(秒,0=浏览器关闭时过期)
+set.RefreshUserPeriod // 刷新用户信息周期(秒,默认600)
+```
+
+---
+
+## 常见例外与注意事项
+
+- `CubeSetting.JwtSecret` **不能为空**,否则 `TokenService.IssueToken()` 会抛出解析异常;生产环境应通过 Secret 管理工具注入,不要写入版本控制。
+- `ManageProvider.User` 基于 `AsyncLocal<T>` 实现,跨线程传递时需注意 `AsyncLocal` 的值捕获行为。
+- `UserService.Login()` 会自动调用 `ManageProvider.SaveCookie()`,无需在控制器层再次写入 Cookie。
+- `MaxLoginError` 的错误计数存储在 **缓存(ICache)** 中,服务重启后会重置;若需持久化可自定义 `ICacheProvider` 使用 Redis。
+- `SendVerifyCode()` 中的 IP 限流(5次/10分钟)基于请求 IP,内容分发网络(CDN)场景需确保传入真实客户端 IP(`X-Forwarded-For`)。
+
+## 推荐检查项
+
+- [ ] `CubeSetting.JwtSecret` 是否已配置(非空、非默认值)
+- [ ] 短信/邮件验证码功能是否已在 `CubeSetting` 中启用,对应服务商配置是否完整
+- [ ] `MaxLoginError` 和 `LoginForbiddenTime` 是否根据业务安全要求调整
+- [ ] `PasswordService.Valid()` 在用户注册/重置密码时是否已调用
+- [ ] Token 响应中的 `ExpireIn` 是否已告知前端用于实现自动续签逻辑
+- [ ] `UserConnect` 表是否为第三方登录提供了唯一索引(Provider + OpenID)
diff --git a/.github/skills/cube-mvc-backend/SKILL.md b/.github/skills/cube-mvc-backend/SKILL.md
new file mode 100644
index 0000000..7a4b5c3
--- /dev/null
+++ b/.github/skills/cube-mvc-backend/SKILL.md
@@ -0,0 +1,411 @@
+---
+name: cube-mvc-backend
+description: >
+ 使用 NewLife.Cube MVC 版本(NewLife.CubeNC)开发后台管理系统,涵盖 Area 区域注册、
+ EntityController<T> 实体控制器CRUD、字段定制机制(ListFields/FormFields/SearchFields/DetailFields)、
+ 视图重载覆盖机制(ThemeViewLocationExpander + CubeEmbeddedFileProvider)、
+ 菜单与权限体系(MenuAttribute/EntityAuthorizeAttribute/PermissionFlags),
+ 以及 AddCube()/UseCube() 启动配置入口。
+ 适用于基于 Cube MVC 框架开发后台管理系统、控制器扩展、视图覆盖、字段定制等任务。
+argument-hint: >
+ 说明业务场景:是新建 Area 区域、扩展控制器、还是定制字段/视图;
+ 如涉及权限,说明需要哪类权限检查(查看/新增/编辑/删除)。
+---
+
+# Cube MVC 后台管理系统
+
+## 适用场景
+
+- 基于 NewLife.CubeNC(含 `#if MVC` 编译符的项目)开发管理后台。
+- 新建 Area 区域并自动注册菜单与权限。
+- 继承 `EntityController<T>` 开发实体 CRUD 页面并定制字段显示。
+- 覆盖魔方内置视图(通过本地物理文件优先机制)。
+- 配置菜单特性、权限过滤器、数据权限中间件。
+
+---
+
+## 快速启动
+
+### 1. 服务注册(Program.cs / Startup.cs)
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+// 添加魔方核心服务(包含认证、模型绑定、菜单、权限、缓存等)
+builder.Services.AddCube();
+
+// 添加 Razor Pages 和 MVC
+builder.Services.AddControllersWithViews();
+
+var app = builder.Build();
+
+// 激活魔方中间件管道(认证、静态文件、路由、数据权限等)
+app.UseCube(builder.Environment);
+
+// 映射控制器路由
+app.MapControllerRoute(
+ name: "default",
+ pattern: "{area:exists}/{controller=Index}/{action=Index}/{id?}");
+
+app.Run();
+```
+
+### 2. 项目模板方式
+
+```powershell
+# 安装模板
+dotnet new install NewLife.Templates
+
+# 创建带魔方的 Web 项目
+dotnet new cube -n MyCompanyWeb
+
+# 添加数据层引用
+dotnet add reference ../MyCompany.Data/MyCompany.Data.csproj
+```
+
+---
+
+## Area 区域注册
+
+每个业务模块对应一个 Area,Area 注册时会自动扫描控制器、创建菜单树、绑定权限。
+
+### AreaBase 继承规范
+
+```csharp
+// Areas/Order/OrderAreaRegistration.cs
+[DisplayName("订单管理")] // 菜单显示名
+[Menu(50, true, Icon = "fa-shopping-cart", // Order=50,可见=true
+ LastUpdate = "20240601")] // 有新菜单项时触发重建
+public class OrderArea : AreaBase
+{
+ public OrderArea() : base("Order") { } // 必须传入与文件夹同名的区域名
+}
+```
+
+**命名约定**:类名必须以 `Area` 结尾,去掉 `Area` 后缀即为区域名(`OrderArea` → `Order`)。
+
+**`AreaBase` 做了什么**:
+1. 自动调用 `MenuHelper.ScanController()` 反射扫描本区域所有控制器。
+2. 依据 `[MenuAttribute]` 和 `[DisplayName]` 创建或更新数据库菜单记录。
+3. 将菜单ID写入 `CubeService.AreaNames`,供权限过滤器判断。
+
+### Menu 特性参数
+
+| 参数 | 说明 |
+|------|------|
+| `Order`(第1参数) | 排序值,**越大越靠前**(Admin=-1,Cube=-2) |
+| `Visible`(第2参数) | 是否在导航中显示 |
+| `Icon` | FontAwesome 图标名,如 `fa-table` |
+| `LastUpdate` | 日期字符串,内置菜单有改动时需更新此值以触发重建 |
+
+---
+
+## EntityController<T> 实体控制器
+
+### 继承与装饰
+
+```csharp
+// Areas/Order/Controllers/OrderController.cs
+[Menu(100, true, Icon = "fa-list")] // 控制器在菜单中的位置与可见性
+[OrderArea] // 声明所属区域(AreaBase 子类特性)
+public class OrderController : EntityController<Order>
+{
+ // 若实体与视图模型不同,使用泛型二参数形式
+ // public class OrderController : EntityController<Order, OrderModel>
+}
+```
+
+### 静态构造器字段配置(核心模式)
+
+所有字段定制均在 **静态构造器** 中完成,仅执行一次:
+
+```csharp
+static OrderController()
+{
+ // ===== 列表字段 =====
+ ListFields.RemoveCreateField() // 删除 CreateUser/CreateTime
+ .RemoveUpdateField() // 删除 UpdateUser/UpdateTime
+ .RemoveRemarkField(); // 删除 Remark
+
+ ListFields.RemoveField("Secret"); // 删除指定字段(支持逗号分隔多个)
+
+ // 在 Remark 前插入自定义链接列
+ var df = ListFields.AddListField("detail", null, "Remark");
+ df.DisplayName = "查看详情";
+ df.Url = "OrderDetail?orderId={Id}"; // {Id} 自动替换为当前行实体的属性值
+ df.Target = "_blank"; // 在新标签打开
+ df.DataAction = "action"; // 使用 ajax 方式(不跳转,执行动作)
+
+ // 自定义单元格值(委托)
+ var sf = ListFields.GetField("Status") as ListField;
+ sf.GetValue = (entity) => ((Order)entity).StatusName; // 返回值覆盖默认显示
+
+ // ===== 添加表单字段 =====
+ AddFormFields.RemoveField("Id,CreateTime,CreateUser,UpdateTime,UpdateUser");
+ AddFormFields.GetField("Title").Required = true; // 设置必填
+
+ // ===== 编辑表单字段 =====
+ EditFormFields.RemoveField("CreateTime,CreateUser");
+ EditFormFields.AddField("AuditTime"); // 从 AllFields 中补充字段
+
+ // ===== 搜索字段 =====
+ SearchFields.AddField("Status").Multiple = true; // 多选下拉
+ SearchFields.AddField("CreateTime");
+
+ // ===== 详情字段 =====
+ DetailFields.AddField("Remark");
+}
+```
+
+### 常用字段操作方法速查
+
+| 方法 | 说明 |
+|------|------|
+| `RemoveField(params string[])` | 删除字段,支持 `*` 模糊匹配 |
+| `RemoveCreateField()` | 批量删除 CreateUser/CreateTime |
+| `RemoveUpdateField()` | 批量删除 UpdateUser/UpdateTime |
+| `RemoveRemarkField()` | 批量删除 Remark |
+| `AddField(name)` | 从实体 AllFields 添加指定字段 |
+| `AddListField(name, before, after)` | 插入自定义列表列(ListField 类型) |
+| `AddFormField(name, before, after)` | 插入自定义表单项(FormField 类型) |
+| `GetField(name)` | 获取字段对象(可强转为 ListField/FormField 等) |
+| `Replace(oriName, newName)` | 替换字段 |
+
+### ListField 扩展属性
+
+```csharp
+var lf = ListFields.GetField("Name") as ListField;
+
+lf.Header = "订单编号"; // 列头文字(覆盖 DisplayName)
+lf.Url = "/Order/Detail/{Id}"; // 单元格链接,{属性名} 自动插值
+lf.Target = "_blank"; // 链接目标
+lf.TextAlign = TextAligns.Center; // 对齐方式
+lf.Class = "text-warning"; // 单元格 CSS 类
+lf.MaxWidth = 200; // 超出宽度折叠
+lf.DataAction = "action"; // "action" = ajax调用,null = 普通跳转
+lf.GetValue = e => ((Order)e).FormatAmount(); // 自定义显示值
+lf.GetClass = e => ((Order)e).IsOverdue ? "text-red" : ""; // 动态样式
+```
+
+### 搜索与数据加载钩子
+
+```csharp
+// 重载搜索逻辑(调用实体 Search 方法)
+protected override IEnumerable<Order> Search(Pager p)
+{
+ return Order.Search(p["key"], p["status"].ToInt(-1), p);
+}
+
+// 重载单条查询
+protected override Order FindData(String id)
+{
+ return Order.FindByKey(id.ToInt());
+}
+```
+
+### CRUD 钩子方法
+
+```csharp
+// 保存前验证(新增与编辑均触发)
+protected override Boolean Valid(Order entity, DataObjectMethodType type, Boolean post)
+{
+ if (post && entity.Amount <= 0)
+ throw new Exception("金额必须大于0");
+ return base.Valid(entity, type, post);
+}
+
+// 新增前处理
+protected override void OnInsert(Order entity)
+{
+ entity.CreateUserId = ManageProvider.User?.ID ?? 0;
+ base.OnInsert(entity);
+}
+
+// 更新前处理
+protected override void OnUpdate(Order entity)
+{
+ entity.UpdateTime = DateTime.Now;
+ base.OnUpdate(entity);
+}
+
+// 删除前处理
+protected override void OnDelete(Order entity)
+{
+ if (entity.Status == 2) throw new Exception("已完成订单不允许删除");
+ base.OnDelete(entity);
+}
+```
+
+---
+
+## 视图覆盖机制
+
+### 覆盖优先级(从高到低)
+
+应用程序本地物理文件 **始终优于** 魔方嵌入式资源:
+
+```
+1. ~/Areas/{Area}/Views/{Controller}_{Theme}/{Action}.cshtml ← 主题特定覆盖
+2. ~/Areas/{Area}/Views/{Controller}/{Action}.cshtml ← 控制器覆盖
+3. ~/Areas/{Area}/Views/{Theme}/{Action}.cshtml ← 主题共享覆盖
+4. ~/Areas/{Area}/Views/Shared/{Action}.cshtml ← 区域共享覆盖
+5. ~/Views/{Theme}/{Action}.cshtml ← 应用级主题覆盖
+6. ~/Views/Shared/{Action}.cshtml ← 应用级共享覆盖
+7. 魔方程序集嵌入资源(fallback)
+```
+
+**`{Theme}`** 由 `CubeSetting.Current.Theme` 决定(默认 `ACE`),首页使用 `CubeSetting.Current.Skin`。
+
+### 常见视图覆盖场景
+
+```
+# 覆盖列表页(Order 区域 Product 控制器)
+Areas/Order/Views/Product/Index.cshtml
+
+# 覆盖编辑表单(共享给该区域所有控制器)
+Areas/Order/Views/Shared/EditForm.cshtml
+
+# 覆盖特定主题下的列表页
+Areas/Order/Views/Product_ACE/Index.cshtml
+
+# 覆盖全局共享视图(影响所有区域)
+Views/ACE/_LayoutAdmin.cshtml
+```
+
+### 静态资源覆盖
+
+通过物理文件覆盖嵌入资源中的 JS/CSS/图片:
+
+```
+# 项目 wwwroot 中同路径文件会优先于魔方嵌入资源
+wwwroot/js/jquery.min.js → 覆盖嵌入的同名文件
+wwwroot/css/custom.css → 新增自定义样式
+```
+
+---
+
+## 菜单与权限
+
+### PermissionFlags 枚举
+
+| 值 | 含义 | 适用 Action |
+|----|------|------------|
+| `Detail` (0x01) | 查看 | Index、Detail |
+| `Insert` (0x02) | 新增 | Add (GET/POST) |
+| `Update` (0x04) | 编辑 | Edit (GET/POST) |
+| `Delete` (0x08) | 删除 | Delete |
+| `Approve` (0x10) | 审批 | 自定义 Action |
+| `Export` (0x20) | 导出 | Export |
+
+### EntityAuthorize 用法
+
+```csharp
+// 方法级权限(自定义 Action)
+[EntityAuthorize(PermissionFlags.Approve)]
+public ActionResult Approve(Int32 id)
+{
+ // ...
+}
+
+// 允许匿名访问(跳过权限检查)
+[AllowAnonymous]
+public ActionResult Public()
+{
+ // ...
+}
+```
+
+`EntityController<T>` 内置 5 个标准 Action 已自动配置对应权限,无需手动标注。
+
+### 权限检查流程
+
+```
+请求到达 → EntityAuthorizeAttribute.OnAuthorization()
+ ↓
+检查 [AllowAnonymous] → 有则跳过
+ ↓
+根据控制器命名空间映射到菜单节点
+ ↓
+user.Has(menu, PermissionFlags) → XCode Membership 实现
+ ↓
+无权限 → JSON 请求返回 401/403;浏览器请求跳转登录页
+```
+
+---
+
+## 数据权限(多租户隔离)
+
+`DataScopeMiddleware` 在每次请求时自动设置当前用户的数据权限上下文,XCode 查询会自动附加过滤条件。
+
+```csharp
+// 中间件自动注入,无需手动调用
+// UseCube() 内部已注册 UseMiddleware<DataScopeMiddleware>()
+
+// 控制器内可读取当前租户
+var tenantId = TenantContext.CurrentId;
+
+// 若需要跳过数据权限过滤(超级管理员场景)
+using (DataScopeContext.Disable())
+{
+ var all = Order.FindAll(); // 此处查询不带数据权限过滤
+}
+```
+
+---
+
+## CubeSetting 关键配置
+
+`CubeSetting` 通过 `[Config("Cube")]` 绑定,会自动从 `appsettings.json` 或 Cube 配置文件读取。
+
+```csharp
+var set = CubeSetting.Current;
+
+// 读取常用配置
+var theme = set.Theme; // 主题名(默认 ACE)
+var skin = set.Skin; // 首页皮肤
+var uploadDir = set.UploadPath; // 上传目录(默认 Uploads)
+var avatarDir = set.AvatarPath; // 头像目录(默认 Avatars)
+var jwtSecret = set.JwtSecret; // JWT 签名密钥
+var expire = set.TokenExpire; // Token 有效期(秒,默认 7200)
+var maxErr = set.MaxLoginError; // 登录失败上限(默认 5)
+```
+
+**配置文件示例(appsettings.json):**
+
+```json
+{
+ "Cube": {
+ "Theme": "Tabler",
+ "TokenExpire": 86400,
+ "CorsOrigins": "https://app.example.com",
+ "MaxLoginError": 3,
+ "JwtSecret": "your-secret-key-here"
+ }
+}
+```
+
+---
+
+## 常见例外与注意事项
+
+- `ListFields.AddListField()` 返回的是 `ListField` 类型,但存在 `GetField()` 返回 `DataField`,**需要强转**才能访问 `Url`/`GetValue` 等扩展属性。
+- 静态构造器中对字段集合的修改是**全局一次性**操作,不能在实例方法中修改(否则线程不安全)。
+- 视图文件名含 `-` 或 `@` 时,嵌入资源中对应名称会被映射为 `_`(`CubeEmbeddedFileProvider` 自动反向映射)。
+- `[Menu]` 的 `LastUpdate` 字段若不更新,新添加的控制器菜单项不会触发菜单重建;建议每次改动菜单结构时更新此值。
+- 区域名大小写需与文件夹名**严格一致**(`base("Order")` vs 文件夹 `Areas/Order/`)。
+
+---
+
+## 推荐检查项
+
+- [ ] `AreaBase` 子类静态构造器中是否已调用 `RegisterArea(typeof(XxxArea))`(或通过 `base()` 自动完成)
+- [ ] 控制器是否标注了所属区域的特性(如 `[OrderArea]`)
+- [ ] 静态构造器字段配置是否放在 `static XxxController(){}` 中(非实例构造器)
+- [ ] 覆盖视图路径是否与 `ThemeViewLocationExpander` 查找顺序严格一致
+- [ ] 自定义 Action 是否标注了 `[EntityAuthorize]` 或 `[AllowAnonymous]`
+- [ ] `CubeSetting.JwtSecret` 在生产环境中是否已配置为强密钥
+
+## 待确认问题
+
+- `EntityTreeController`(树形控制器)与 `EntityController` 的字段配置方式是否完全相同。
+- `ReadOnlyEntityController` 是否支持 `AddListField` 等写操作相关的字段定制。
diff --git a/.github/skills/cube-oauth-sso/SKILL.md b/.github/skills/cube-oauth-sso/SKILL.md
new file mode 100644
index 0000000..7b915df
--- /dev/null
+++ b/.github/skills/cube-oauth-sso/SKILL.md
@@ -0,0 +1,318 @@
+---
+name: cube-oauth-sso
+description: >
+ 使用 NewLife.Cube 的 OAuth/SSO 体系实现第三方登录和单点登录,
+ 涵盖 OAuthClient 基类(14+ 内置提供者)、OAuthConfig 数据库配置、
+ SsoController 回调处理、OAuthServer 作为 SSO 服务端(Authorize/Token/UserInfo 端点),
+ 以及 SsoClient 作为 SSO 客户端(密码模式/客户端凭证模式)。
+ 适用于微信/钉钉/企业微信等第三方登录接入、自建 SSO 中心、跨系统统一认证等场景。
+argument-hint: >
+ 说明场景:是接入第三方平台(微信/钉钉等)、使用魔方作为 SSO 服务端,
+ 还是作为 SSO 客户端接入外部 SSO?需要的授权模式(授权码/密码/客户端凭证)?
+---
+
+# Cube OAuth / SSO 单点登录
+
+## 适用场景
+
+- 为应用添加微信公众号、微信小程序、企业微信、钉钉、QQ、GitHub 等第三方登录。
+- 将魔方作为 OAuth2 SSO 服务端,向子系统统一提供用户认证。
+- 通过 `SsoClient` 在服务间以密码模式或客户端凭证模式进行用户身份验证。
+- 管理后台配置第三方 OAuth 应用,无需修改代码。
+
+---
+
+## 内置 OAuth 提供者
+
+| 名称(Name) | 类 | 说明 |
+|----|----|----|
+| `Weixin` | `WeixinClient` | 微信公众号网页授权 |
+| `WxApp` | `WxAppClient` | 微信小程序 code 换 openid |
+| `WxOpen` | `WxOpenClient` | 微信开放平台 |
+| `QyWeixin` / `QyWeiXin` | `QyWeiXin` | 企业微信应用授权 |
+| `DingTalk` | `DingTalkClient` | 钉钉企业应用免登 |
+| `Alipay` | `AlipayClient` | 支付宝授权 |
+| `QQ` | `QQClient` | QQ 互联 |
+| `Github` | `GithubClient` | GitHub Developer OAuth |
+| `Weibo` | `WeiboClient` | 微博登录 |
+| `Baidu` | `BaiduClient` | 百度账号 |
+| `Microsoft` | `MicrosoftClient` | 微软账号 |
+| `Id4` | `Id4Client` | IdentityServer4 自建 OAuth |
+| `Taobao` | `TaobaoClient` | 淘宝开放平台 |
+| `NewLife` | `OAuthClient`(基类) | 魔方自身 SSO 服务端 |
+
+---
+
+## OAuthConfig — 数据库配置
+
+所有 OAuth 配置存储在 `OAuthConfig` 表,通过管理后台(**系统管理 → OAuth配置**)维护,无需硬编码。
+
+### 核心字段
+
+```csharp
+public class OAuthConfig
+{
+ public String Name { get; set; } // 提供者唯一名,与 OAuthClient 子类绑定
+ public String NickName { get; set; } // 登录页显示名(如"微信公众号")
+ public String Logo { get; set; } // 登录按钮图标 URL
+
+ // 应用凭证
+ public String AppId { get; set; } // 第三方 AppId / ClientId
+ public String Secret { get; set; } // 第三方 AppSecret / ClientSecret
+ public String Scope { get; set; } // 授权范围(snsapi_userinfo/user_info 等)
+
+ // 服务端点(使用内置客户端时无需填写,已内置)
+ public String Server { get; set; } // OAuth 服务基地址
+ public String AuthUrl { get; set; } // 授权端点 URL
+ public String AccessUrl { get; set; } // 令牌端点 URL
+ public String UserUrl { get; set; } // 用户信息端点 URL
+ public String AppUrl { get; set; } // 本应用外部地址(反向代理时使用)
+
+ // 授权类型
+ public GrantTypes GrantType { get; set; } // AuthorizationCode/Password/ClientCredentials
+ public String FieldMap { get; set; } // 字段映射 JSON:{"openid":"user_id"}
+
+ // 功能开关
+ public Boolean Enable { get; set; } // 是否启用
+ public Boolean Visible { get; set; } // 登录页是否展示此方式
+ public Boolean AutoRegister { get; set; } // 是否自动注册新用户
+ public Boolean FetchAvatar { get; set; } // 是否下载第三方头像到本地
+ public Boolean Debug { get; set; } // 输出调试日志
+}
+```
+
+### 枚举值
+
+```csharp
+public enum GrantTypes
+{
+ AuthorizationCode = 0, // 授权码模式(网页登录,最常用)
+ Implicit, // 隐式模式(已弃用)
+ Password, // 密码模式(服务端直接验证)
+ ClientCredentials, // 客户端凭证(机器对机器)
+}
+```
+
+---
+
+## 第三方登录流程(以微信为例)
+
+### 1. 配置 OAuthConfig(管理后台一次性操作)
+
+```
+系统管理 → OAuth配置 → 添加
+ Name: Weixin
+ NickName: 微信公众号
+ AppId: wx1234567890abcdef
+ Secret: 你的-app-secret
+ Scope: snsapi_userinfo
+ Enable: ✓
+ Visible: ✓
+ AutoRegister:✓
+```
+
+### 2. 用户点击"微信登录"
+
+浏览器访问 `/Sso/Login?name=Weixin&r=/dashboard`(其中 `r` 是登录成功后跳转地址)。
+
+### 3. SsoController 处理回调
+
+魔方内置 `SsoController` 自动处理整个 OAuth 流程:
+
+```
+1. GET /Sso/Login?name=Weixin&r=/dashboard
+ → 创建 WeixinClient,Authorize() 构建授权 URL
+ → 重定向到微信授权页
+
+2. 微信回调 GET /Sso/LoginInfo/Weixin?code=xxx&state=yyy
+ → GetAccessToken(code) 换 access_token
+ → GetUserInfo() 获取 openid/nickname/avatar
+ → 查找或自动注册本地 User(通过 UserConnect 绑定)
+ → ManageProvider.Login(userId) 写入 Session + Cookie
+ → 重定向到 /dashboard
+```
+
+### 4. OAuthClient API(自定义流程时使用)
+
+```csharp
+// 在自定义控制器中手动控制 OAuth 流程
+public class CustomSsoController : ControllerBaseX
+{
+ [AllowAnonymous]
+ public IActionResult LoginByWeixin()
+ {
+ // 1. 创建客户端(自动从 OAuthConfig 加载配置)
+ var client = OAuthHelper.Create(TenantContext.CurrentId, "Weixin");
+
+ // 2. 构建授权 URL(state 存到 Session 用于防 CSRF)
+ var redirectUri = $"{Request.Scheme}://{Request.Host}/callback/Weixin";
+ var authUrl = client.Authorize(redirectUri, state: "random-state", Request.GetUri()....);
+ return Redirect(authUrl);
+ }
+
+ [AllowAnonymous]
+ public async Task<IActionResult> CallbackWeixin(String code, String state)
+ {
+ var client = OAuthHelper.Create(TenantContext.CurrentId, "Weixin");
+
+ // 3. 用 code 换 access_token
+ await client.GetAccessToken(code); // 填充 AccessToken
+
+ // 4. 用 access_token 获取用户信息
+ await client.GetUserInfo(); // 填充 OpenID/NickName/Avatar/Mail 等
+
+ // 5. 通过 OpenID 查找绑定关系
+ var uc = UserConnect.FindByProviderAndOpenID("Weixin", client.OpenID);
+ if (uc == null)
+ {
+ // 首次登录:自动注册本地用户
+ uc = new UserConnect { Provider = "Weixin", OpenID = client.OpenID };
+ var user = new User { Name = client.NickName, DisplayName = client.NickName };
+ user.Insert();
+ uc.UserID = user.ID;
+ uc.Insert();
+ }
+
+ // 6. 登录
+ ManageProvider.Provider.SetCurrent(User.FindByKey(uc.UserID));
+ return Redirect("/");
+ }
+}
+```
+
+---
+
+## 魔方作为 SSO 服务端(OAuthServer)
+
+魔方可对外提供标准 OAuth2 授权码模式,让子系统通过魔方统一登录。
+
+### 端点列表(SsoController 提供)
+
+| 端点 | 方法 | 说明 |
+|------|------|------|
+| `/Sso/Authorize` | GET | 子系统重定向到此处,魔方显示登录界面 |
+| `/Sso/Auth2` | GET | 用户登录后内部跳转,生成 code |
+| `/Sso/Access_Token` | GET/POST | 子系统用 code 换 access_token |
+| `/Sso/Token` | GET/POST | 密码模式/客户端凭证模式 |
+| `/Sso/UserInfo` | GET/POST | 子系统用 token 获取用户信息 |
+| `/Sso/Logout` | GET | 注销(可传 `redirect_uri` 参数) |
+
+### 子系统接入步骤
+
+**第一步:在魔方管理后台注册子系统应用**
+
+```
+系统管理 → 应用系统(App)→ 添加
+ Name: OrderWeb
+ Secret: app-secret-key
+ Enable: ✓
+ 允许IP: (留空=不限,或填写子系统 IP)
+```
+
+**第二步:子系统配置 SsoClient**
+
+```csharp
+// 子系统 appsettings.json(或 Cube 配置文件)
+{
+ "SsoServer": "https://sso.company.com",
+ "AppId": "OrderWeb",
+ "AppSecret": "app-secret-key",
+ "JwtKey": "main$-----BEGIN PUBLIC KEY-----\nMIIBIjAN...\n-----END PUBLIC KEY-----"
+}
+
+// 子系统代码中创建 SsoClient
+var sso = SsoClient.Create("NewLife"); // Name 对应 OAuthConfig 中 NewLife 提供者
+
+// 也可以直接实例化
+var sso = new SsoClient
+{
+ Server = "https://sso.company.com",
+ AppId = "OrderWeb",
+ Secret = "app-secret-key",
+ SecurityKey = "main$-----BEGIN PUBLIC KEY-----\nMII..."
+};
+```
+
+**第三步:子系统密码模式登录(服务端直连)**
+
+```csharp
+// 密码模式(密码会被 RSA 公钥加密后传输)
+var token = await sso.GetToken("alice@company.com", "user-password");
+var user = await sso.GetUser(token.AccessToken);
+Console.WriteLine($"用户 {user.Name},角色:{user.Roles}");
+
+// 刷新令牌
+var newToken = await sso.RefreshToken(token.AccessToken);
+
+// 一体化验证(直接返回用户信息)
+var userInfo = await sso.UserAuth("alice@company.com", "user-password");
+```
+
+**第四步:客户端凭证模式(应用间 API 调用)**
+
+```csharp
+// 用应用凭证换取应用级令牌
+var appToken = await sso.GetToken(deviceId: "server-app-001");
+// 用于机器间 API 调用,不关联最终用户
+```
+
+---
+
+## SsoClient API 速查
+
+| 方法 | 说明 |
+|------|------|
+| `GetToken(username, password)` | 密码模式:用户名密码换令牌(密码 RSA 公钥加密) |
+| `GetToken(deviceId)` | 客户端凭证模式:设备 ID 换令牌 |
+| `RefreshToken(accessToken)` | 刷新已有令牌,获取新 AccessToken |
+| `GetUserInfo(accessToken)` | 用令牌获取用户信息(原始字典) |
+| `GetUser(accessToken)` | 用令牌获取强类型用户对象 |
+| `UserAuth(username, password)` | 一体化:验证并直接返回用户信息 |
+| `GetKey(client_id, client_secret)` | 获取 JWT 验证公钥 |
+
+---
+
+## OAuthClient 基类 — 可扩展属性
+
+自定义 OAuth 提供者时继承 `OAuthClient`:
+
+```csharp
+public class MyOAuthClient : OAuthClient
+{
+ public MyOAuthClient()
+ {
+ Name = "MyProvider";
+ Server = "https://auth.example.com";
+ AuthUrl = "/oauth/authorize";
+ AccessUrl = "/oauth/token";
+ UserUrl = "/api/userinfo";
+ Scope = "read:user";
+
+ // 字段映射(第三方字段名 → 魔方标准字段名)
+ FieldMap = new Dictionary<String, Object>
+ {
+ ["login"] = "UserName", // GitHub 的 login 映射到 UserName
+ ["avatar_url"] = "Avatar",
+ ["email"] = "Mail",
+ };
+ }
+}
+```
+
+---
+
+## 常见例外与注意事项
+
+- 微信公众号在**非微信内置浏览器**中无法使用 `snsapi_userinfo` scope,需改为 `snsapi_base`(仅获取 OpenID)。
+- `OAuthConfig.AppUrl` 在反向代理(Nginx/网关)场景下必须填写应用的**外网地址**,否则 `redirect_uri` 会被构建为内网地址导致回调失败。
+- `SsoClient.SecurityKey` 格式为 `"keyName$PEM-PUBLIC-KEY"`,其中 `keyName` 为公钥名(固定 `"main"`),`$` 后为 PEM 格式 RSA 公钥(Base64 encoded)。
+- `OAuthConfig.FieldMap` 是 JSON 字符串,格式为 `{"第三方字段名":"标准字段名"}`,用于适配非标准第三方接口的字段命名。
+- 同一 `Provider`(如 `"Weixin"`)在同一租户内只能有一条 `OAuthConfig`,多租户按 `TenantId` 隔离。
+
+## 推荐检查项
+
+- [ ] `OAuthConfig.Enable` 和 `OAuthConfig.Visible` 是否均已启用(二者独立控制)
+- [ ] 第三方平台上 OAuth 应用回调地址是否与应用实际地址一致(`AppUrl` 配置正确)
+- [ ] `SsoClient.SecurityKey` 是否配置了 RSA 公钥(密码模式必须,避免明文传输密码)
+- [ ] 子系统(App 实体)是否在魔方管理后台注册并启用
+- [ ] `AutoRegister = true` 时是否已考虑自动注册用户的默认角色(由 `CubeSetting.DefaultRole` 决定)
diff --git a/.github/skills/cube-webapi/SKILL.md b/.github/skills/cube-webapi/SKILL.md
new file mode 100644
index 0000000..d84123c
--- /dev/null
+++ b/.github/skills/cube-webapi/SKILL.md
@@ -0,0 +1,475 @@
+---
+name: cube-webapi
+description: >
+ 使用 NewLife.Cube WebAPI 版本(NewLife.Cube)开发后端 API 服务,涵盖三层控制器体系
+ (ControllerBaseX/BaseController/AppControllerBase)、EntityController<T> 实体 CRUD 端点、
+ 统一响应格式(ApiResponse/ApiListResponse)、JWT/Token 令牌认证(TokenService)、
+ 中间件服务注册入口(AddCube/UseCube),以及 Swagger/OAuth/SSO 集成。
+ 适用于基于 Cube 框架开发 REST API、第三方应用接入、Token 认证、API 权限控制等任务。
+argument-hint: >
+ 说明业务场景:是开发管理端 API(ControllerBaseX)、业务 API(BaseController)
+ 还是第三方应用 API(AppControllerBase);是否需要 JWT 认证;是否需要 Swagger。
+---
+
+# Cube WebAPI 后端 API 服务
+
+## 适用场景
+
+- 基于 NewLife.Cube(WebAPI 版,无视图依赖)开发 REST API 接口。
+- 为前端 SPA / 移动端 App / 第三方系统提供标准 JSON 接口。
+- 使用 `EntityController<TEntity>` 快速生成标准实体 CRUD 端点。
+- 使用 `TokenService` 签发和验证 JWT 令牌,实现无状态认证。
+- 集成 Swagger UI 进行 API 文档展示与调试。
+
+---
+
+## 快速启动
+
+### 服务注册与中间件
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+// 注册 Cube 核心服务(含认证、JWT、用户管理、缓存等)
+builder.Services.AddCube();
+
+// 可选:Swagger 文档(需引用 NewLife.Cube.Swagger 包)
+builder.Services.AddCubeSwagger();
+
+// 可选:主题 UI 模块(无视图时可省略)
+// builder.Services.AddCubeUI();
+
+var app = builder.Build();
+
+// 激活 Cube 中间件管道(顺序固定,请勿调整)
+app.UseCube(builder.Environment);
+
+// 可选:Swagger UI
+app.UseSwagger();
+app.UseSwaggerUI();
+
+app.MapControllers();
+app.Run();
+```
+
+### AddCube() 注册内容速查
+
+| 内容 | 说明 |
+|------|------|
+| `ManageProvider` | 用户管理提供者(当前用户、权限检查) |
+| `TokenService` | JWT 令牌颁发与验证 |
+| `PasswordService` | 密码加密/校验(BCrypt) |
+| `UserService` | 用户 CRUD、登录、绑定 |
+| `AccessService` | 访问日志记录 |
+| `PageService` | 页面渲染(管理后台用) |
+| `SmsService` / `MailService` | 短信/邮件服务(扩展接口) |
+| `DataProtection` | 数据保护密钥(存储到数据库) |
+| `DataRetentionService` | 后台服务:定期清理过期日志/令牌 |
+| `JobService` | 后台服务:定时作业调度(可选) |
+| `ModuleManager` | 功能插件管理(IModule 实现自动加载) |
+| CORS | 根据 `CubeSetting.CorsOrigins` 自动配置 |
+| Stardust | 星尘链路追踪集成(如项目引用) |
+
+### UseCube() 中间件顺序
+
+```
+UseCube()
+ ├── UseManagerProvider() ← 用户主体解析
+ ├── UseExceptionHandler() ← 全局异常捕获
+ ├── UseCors("cube_cors") ← 跨域
+ ├── UseStaticHttpContext() ← 静态 HttpContext 访问
+ ├── UseStaticFiles() ← 头像/上传文件静态访问
+ ├── UseCookiePolicy() ← Cookie 策略
+ ├── UseAuthentication() ← 认证中间件
+ ├── UseStardust() ← 链路追踪
+ ├── UseMiddleware<RunTimeMiddleware>()
+ ├── UseMiddleware<DataScopeMiddleware>() ← 数据权限上下文
+ └── UseRouter() ← 区域路由注册
+```
+
+---
+
+## 三层控制器体系
+
+### 选型矩阵
+
+| 基类 | 适用场景 | 认证方式 |
+|------|---------|---------|
+| `ControllerBaseX` | 管理后台 API(后台操作,有会话) | Cookie / Session |
+| `BaseController` | 业务 REST API(子类实现令牌验证) | 自定义 Token |
+| `AppControllerBase` | 第三方应用接入(JWT 标准认证) | JWT Bearer |
+
+### ControllerBaseX — 管理后台 API 基类
+
+提供当前登录用户、菜单、租户等上下文,适合管理后台接口。
+
+```csharp
+[ApiController]
+[Route("[area]/[controller]/[action]")]
+public class ProductController : ControllerBaseX
+{
+ [HttpGet]
+ [EntityAuthorize(PermissionFlags.Detail)]
+ public IActionResult List(Pager p)
+ {
+ var list = Product.Search(p["key"], p);
+ return Json(0, null, list);
+ }
+
+ // 可访问的上下文属性
+ // ManageProvider.User → IManageUser 当前用户
+ // TenantContext.CurrentId → 当前租户 ID
+ // PageSetting → 页面渲染配置(本请求专用)
+}
+```
+
+### BaseController — 业务 API 基类
+
+子类必须实现 `OnAuthorize(token)` 验证令牌并返回用户对象。
+
+```csharp
+public class BizController : BaseController
+{
+ // 子类实现令牌验证
+ protected override IManageUser OnAuthorize(String token)
+ {
+ // 自定义令牌验证逻辑
+ var user = UserToken.FindByToken(token);
+ return user;
+ }
+
+ // 未认证时自动返回 403,无需手动检查
+ [HttpGet]
+ public IActionResult GetProfile()
+ {
+ var user = Session["user"] as User;
+ return Ok(user);
+ }
+}
+```
+
+### AppControllerBase — 第三方应用 API 基类
+
+继承自 `BaseController`,令牌验证已内置(解码 JWT → 获取应用),专用于应用级接入。
+
+```csharp
+[ApiController]
+[Route("api/[controller]/[action]")]
+public class DataController : AppControllerBase
+{
+ [HttpPost]
+ public IActionResult Push([FromBody] DataModel data)
+ {
+ // App 属性 = 当前接入应用(已通过 JWT 验证)
+ var appName = App.Name;
+
+ // WriteLog 自动关联应用和 TraceId
+ WriteLog("Push", true, $"收到数据 {data.Count} 条");
+
+ return Ok(new { count = data.Count });
+ }
+}
+```
+
+---
+
+## 统一响应格式
+
+所有接口返回均使用 `ApiResponse<T>` 结构,`ControllerBaseX` 的 `OnActionExecuted` 自动包装。
+
+### ApiResponse<T> 结构
+
+```csharp
+// 基础响应
+public class ApiResponse<T>
+{
+ public Int32 Code { get; set; } // 0=成功,其他=错误
+ public String Message { get; set; } // 提示信息
+ public T Data { get; set; } // 业务数据
+ public String TraceId { get; set; } // 链路追踪 ID
+}
+
+// 列表响应(含分页)
+public class ApiListResponse<T> : ApiResponse<IList<T>>
+{
+ public PageModel Page { get; set; } // 分页信息(PageIndex/PageSize/TotalCount)
+ public T Stat { get; set; } // 统计行(合计/平均等)
+}
+```
+
+### 标准状态码
+
+| code | 含义 | 触发场景 |
+|------|------|---------|
+| `0` | 成功 | 正常返回 |
+| `400` | 请求参数错误 | 模型验证失败 |
+| `401` | 未登录 / Token 过期 | 无有效 Token |
+| `403` | 无权限 | 认证通过但权限不足 |
+| `500` | 服务器错误 | 未捕获异常 |
+
+### 手动返回响应
+
+```csharp
+// 成功(有数据)
+return Json(0, null, data);
+
+// 成功(有提示信息)
+return Json(0, "操作成功!", entity);
+
+// 业务错误
+return Json(500, "库存不足,无法下单");
+
+// 抛出异常(自动被 OnActionExecuted 捕获并包装为 JSON)
+throw new ApiException(400, "参数不合法:金额必须大于0");
+throw new NoPermissionException("没有审批权限");
+```
+
+---
+
+## JWT 令牌认证
+
+### 颁发令牌(TokenService)
+
+```csharp
+// 注入 TokenService(已在 AddCube() 中注册为单例)
+public class AuthController : ControllerBaseX
+{
+ [Inject]
+ public TokenService TokenSvc { get; set; }
+
+ [HttpPost, AllowAnonymous]
+ public IActionResult Login([FromBody] LoginModel model)
+ {
+ var user = User.Auth(model.Username, model.Password);
+ if (user == null) return Json(401, "用户名或密码错误");
+
+ // 颁发 JWT 令牌
+ var tokenModel = TokenSvc.IssueToken(user.Name, user.Secret, expire: 7200);
+ return Json(0, "登录成功", tokenModel);
+ }
+}
+```
+
+颁发结果:
+
+```json
+{
+ "code": 0,
+ "data": {
+ "accessToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
+ "tokenType": "Bearer",
+ "expireIn": 7200
+ }
+}
+```
+
+### 验证令牌
+
+```csharp
+// TokenService.DecodeToken() 验证 JWT 签名并返回应用信息
+var (jwt, app) = TokenSvc.DecodeToken(token, CubeSetting.Current.JwtSecret);
+
+// 获取 JWT 负载中的声明
+var name = jwt.Subject;
+var expire = jwt.Expire;
+```
+
+### 客户端传递令牌(三种方式,任选其一)
+
+```http
+# 方式1:Authorization Header(推荐)
+Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
+
+# 方式2:自定义 Header
+X-Token: eyJhbGciOiJIUzI1NiJ9...
+
+# 方式3:Query String
+GET /api/data?token=eyJhbGciOiJIUzI1NiJ9...
+```
+
+---
+
+## EntityController<TEntity> — 实体 CRUD 端点
+
+继承后自动获得标准 REST 端点,无需手写 Action。
+
+### 路由映射
+
+```
+GET /[area]/[controller]/Search?key=&page=1&pageSize=20 → 分页查询
+GET /[area]/[controller]/Detail/{id} → 查询单条
+POST /[area]/[controller] → 新增(JSON Body)
+PUT /[area]/[controller] → 更新(JSON Body,含 Id)
+DELETE /[area]/[controller]?id=1 → 删除
+```
+
+### 继承示例
+
+```csharp
+[ApiController]
+[Route("[area]/[controller]/[action]")]
+[OrderArea]
+public class ProductController : EntityController<Product>
+{
+ // 泛型二参数:实体与视图模型不同时使用
+ // public class ProductController : EntityController<Product, ProductModel>
+}
+```
+
+### 钩子方法
+
+```csharp
+// 字段配置(静态构造器)
+static ProductController()
+{
+ // WebAPI 版同样支持 ListFields/SearchFields 等字段集合操作
+ // 用于影响 Search 接口返回的字段范围
+ ListFields.RemoveField("Remark,Secret");
+}
+
+// 查询钩子
+protected override IEnumerable<Product> Search(Pager p)
+{
+ return Product.Search(p["key"], p["categoryId"].ToInt(), p);
+}
+
+// CRUD 钩子(同 MVC 版)
+protected override void OnInsert(Product entity) { ... }
+protected override void OnUpdate(Product entity) { ... }
+protected override void OnDelete(Product entity) { ... }
+
+// 验证钩子
+protected override Boolean Valid(Product entity, DataObjectMethodType type, Boolean post)
+{
+ if (post && entity.Price < 0)
+ throw new ApiException(400, "价格不能为负数");
+ return base.Valid(entity, type, post);
+}
+```
+
+---
+
+## Swagger 集成
+
+```csharp
+// 安装包:dotnet add package NewLife.Cube.Swagger
+
+// Program.cs
+builder.Services.AddCube();
+builder.Services.AddCubeSwagger(); // 一键集成(含 XML 注释、认证配置)
+
+var app = builder.Build();
+app.UseCube(env);
+app.UseSwagger();
+app.UseSwaggerUI(c =>
+{
+ c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
+ c.RoutePrefix = "swagger"; // 访问地址:/swagger
+});
+```
+
+`AddCubeSwagger()` 自动完成:
+- 加载 `NewLife.Cube.xml` XML 文档注释
+- 根据 `OAuthConfig` 配置优先使用 OAuth2,否则使用 JwtBearer
+- 自定义 SchemaId(避免同名类冲突)
+
+---
+
+## OAuth / SSO 集成
+
+### 内置 OAuth 提供者(14+)
+
+魔方内置以下第三方 OAuth 登录提供者,通过管理后台 **系统管理 → OAuth配置** 启用:
+
+| 提供者 | 类 | 说明 |
+|--------|----|----|
+| 微信公众号 | `WeixinClient` | 网页授权登录 |
+| 微信小程序 | `WxAppClient` / `WxOpenClient` | code 换 openid |
+| 企业微信 | `QyWeiXin` | 企业应用登录 |
+| 钉钉 | `DingTalkClient` | 企业应用授权 |
+| 支付宝 | `AlipayClient` | 支付宝授权 |
+| QQ | `QQClient` | QQ 互联 |
+| GitHub | `GithubClient` | 开发者登录 |
+| 微博 | `WeiboClient` | 微博登录 |
+| 百度 | `BaiduClient` | 百度账号 |
+| Microsoft | `MicrosoftClient` | 微软账号 |
+| IdentityServer4 | `Id4Client` | 自建 OAuth 服务器 |
+
+### 作为 SSO 服务端(OAuthServer)
+
+魔方自身可作为 SSO 中心,为其他系统提供授权:
+
+```
+其他系统 → 携带 client_id 重定向到 /Cube/OAuth/Authorize
+ ← 魔方验证用户并返回 code
+其他系统 → POST /Cube/OAuth/Token(code 换 token)
+ ← 返回 access_token
+其他系统 → GET /Cube/OAuth/UserInfo(token 获取用户信息)
+```
+
+`SsoClient` 封装了标准 OAuth2 Authorization Code 流程,用于接入外部 SSO:
+
+```csharp
+// 在 SsoController 中处理回调
+public class SsoController : ControllerBaseX
+{
+ [AllowAnonymous]
+ public async Task<IActionResult> Callback(String code, String state)
+ {
+ var client = SsoClient.Create(provider); // 根据 provider 名创建客户端
+ var info = await client.GetUserInfo(code); // code 换取用户信息
+ // 自动创建/绑定用户,写入会话
+ return Redirect("/");
+ }
+}
+```
+
+---
+
+## 菜单与权限(WebAPI 版)
+
+WebAPI 版权限机制与 MVC 版一致,同样使用 `[EntityAuthorize]` 和 `PermissionFlags`。
+
+```csharp
+[EntityAuthorize(PermissionFlags.Insert)]
+[HttpPost]
+public IActionResult Create([FromBody] Product model) { ... }
+
+[EntityAuthorize(PermissionFlags.Delete)]
+[HttpDelete]
+public IActionResult Remove(Int32 id) { ... }
+```
+
+权限检查失败(用户已登录但无权限)返回:
+
+```json
+{ "code": 403, "message": "没有权限", "traceId": "xxx" }
+```
+
+---
+
+## 常见例外与注意事项
+
+- `BaseController` 子类**必须重写** `OnAuthorize(token)` 方法,否则所有请求均返回 403。
+- `AppControllerBase` 使用 `App.Secret` 作为 JWT 签名密钥,不使用 `CubeSetting.JwtSecret`,两者需区分。
+- `ApiListResponse<T>.Stat` 统计行仅在实体 Search 方法支持时才有数据,默认为 `null`。
+- 跨域配置 `CubeSetting.CorsOrigins = "*"` 允许所有来源,生产环境应改为具体域名。
+- `UseCube()` 必须在 `UseAuthentication()` 之前调用(内部已处理顺序),不要在 `app.UseAuthentication()` 后再调用 `UseCube()`。
+- `AddCubeSwagger()` 自动启用 `CustomSchemaIds`,若有同名但不同命名空间的类,需确保 `FullName` 不冲突。
+
+---
+
+## 推荐检查项
+
+- [ ] `AddCube()` 是否在 `AddControllers()` 之前调用(确保模型绑定器注册生效)
+- [ ] `UseCube()` 是否在 `UseEndpoints()` / `MapControllers()` 之前调用
+- [ ] JWT 相关接口是否标注了 `[AllowAnonymous]`(登录、获取Token 接口必须允许匿名)
+- [ ] `BaseController` 子类是否实现了 `OnAuthorize()` 方法
+- [ ] `CubeSetting.CorsOrigins` 在生产环境不为 `"*"`
+- [ ] `CubeSetting.JwtSecret` 是否已配置为随机强密钥(不使用默认空值)
+- [ ] `AddCubeSwagger()` 是否只在开发/测试环境启用(避免线上暴露 API 文档)
+
+## 待确认问题
+
+- `ControllerBaseX` 与 `BaseController` 的响应包装逻辑是否共用同一个 `OnActionExecuted` 过滤器。
+- `EntityController<TEntity>` 的 Search 端点是否支持自定义字段投影(返回部分字段)。