NewLife/NewLife.Skills

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
d4e2b53
Tree
1 Parent(s) 734e741
Summary: 5 changed files with 1896 additions and 0 deletions.
Added +271 -0
Added +421 -0
Added +411 -0
Added +318 -0
Added +475 -0
Added +271 -0
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 容器
Added +421 -0
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)
Added +411 -0
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` 等写操作相关的字段定制。
Added +318 -0
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` 决定)
Added +475 -0
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 端点是否支持自定义字段投影(返回部分字段)。