diff --git a/AuthSDK/SimApiAuthCenterMiddleware.cs b/AuthSDK/SimApiAuthCenterMiddleware.cs index 3da6c1d..e32d76b 100644 --- a/AuthSDK/SimApiAuthCenterMiddleware.cs +++ b/AuthSDK/SimApiAuthCenterMiddleware.cs @@ -16,7 +16,7 @@ public class SimApiAuthCenterMiddleware(RequestDelegate next, ILogger {signStr}"); if (SimApiUtil.Md5(signStr) == sign && !string.IsNullOrEmpty(auth)) { diff --git a/AuthSDK/SimApiAuthClient.cs b/AuthSDK/SimApiAuthClient.cs index a7dc563..579086d 100644 --- a/AuthSDK/SimApiAuthClient.cs +++ b/AuthSDK/SimApiAuthClient.cs @@ -7,7 +7,7 @@ namespace SimApi.AuthSDK; public class SimApiAuthClient(SimApiOptions apiOptions, ILogger logger) : SimApiHttpClient(apiOptions, logger) { - public override string Server { get; init; } = apiOptions.SimApiAuthGateOptions.Server ?? string.Empty; - public override string AppId { get; init; } = apiOptions.SimApiAuthGateOptions.AppId ?? string.Empty; - public override string AppKey { get; init; } = apiOptions.SimApiAuthGateOptions.AppKey ?? string.Empty; + public override string Server { get; init; } = apiOptions.SimApiAuthCenterOptions.Server ?? string.Empty; + public override string AppId { get; init; } = apiOptions.SimApiAuthCenterOptions.AppId ?? string.Empty; + public override string AppKey { get; init; } = apiOptions.SimApiAuthCenterOptions.AppKey ?? string.Empty; } \ No newline at end of file diff --git a/Configurations/SimApiAuthGateOptions.cs b/Configurations/SimApiAuthCenterOptions.cs similarity index 92% rename from Configurations/SimApiAuthGateOptions.cs rename to Configurations/SimApiAuthCenterOptions.cs index 2997dd9..9bbf23d 100644 --- a/Configurations/SimApiAuthGateOptions.cs +++ b/Configurations/SimApiAuthCenterOptions.cs @@ -1,6 +1,6 @@ namespace SimApi.Configurations; -public class SimApiAuthGateOptions +public class SimApiAuthCenterOptions { public string? Server { get; set; } public string? AppId { get; set; } diff --git a/Configurations/SimApiOptions.cs b/Configurations/SimApiOptions.cs index 27993b0..b7611aa 100644 --- a/Configurations/SimApiOptions.cs +++ b/Configurations/SimApiOptions.cs @@ -96,7 +96,7 @@ public class SimApiOptions public SimApiSynapseOptions SimApiSynapseOptions { get; set; } = new(); - public SimApiAuthGateOptions SimApiAuthGateOptions { get; set; } = new(); + public SimApiAuthCenterOptions SimApiAuthCenterOptions { get; set; } = new(); public SimApiHttpClientOptions SimApiHttpClientOptions { get; set; } = new(); @@ -139,8 +139,8 @@ public class SimApiOptions options?.Invoke(SimApiJobOptions); } - public void ConfigureSimApiAuthGate(Action? options = null) + public void ConfigureSimApiAuthCenter(Action? options = null) { - options?.Invoke(SimApiAuthGateOptions); + options?.Invoke(SimApiAuthCenterOptions); } } \ No newline at end of file diff --git a/README.md b/README.md index 2556d79..5facb0c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@ -# SimApi 完整 AI 编码参考 +# SimApi for .NET -> **NuGet**: `Simcu.SimApi` | **目标框架**: `net8.0` / `net9.0` / `net10.0` -> **作者**: xRain@SimcuTeam | **性质**: ASP.NET Core API 基础辅助库 -> -> **使用方式**: 将本文档作为上下文提供给 AI,或粘贴到对话开头。 +> NuGet: `Simcu.SimApi` | 目标框架: `net8.0` / `net9.0` / `net10.0` +> 作者: xRain@SimcuTeam + +ASP.NET Core API 基础框架库,提供统一异常拦截、响应封装、Token 认证、Swagger 文档、S3 存储、MQTT 通信、Hangfire 任务调度、Auth Center 网关鉴权与 IAM 权限管理。 --- -## 0. 快速开始 +## 快速开始 ```csharp -// Program.cs — 两步启动 var builder = WebApplication.CreateBuilder(args); - builder.Services.AddSimApi(options => { options.RedisConfiguration = "localhost:6379"; @@ -21,12 +19,8 @@ builder.Services.AddSimApi(options => options.ConfigureSimApiDoc(doc => { - doc.DocumentTitle = "我的API文档"; - doc.ApiGroups = - [ - new("api", "公共接口"), - new("admin", "管理接口", "需要管理员Token") - ]; + doc.DocumentTitle = "我的API"; + doc.ApiGroups = [new("api", "公共接口"), new("admin", "管理接口")]; }); }); @@ -37,430 +31,260 @@ app.Run(); --- -## 1. 核心概念(必读) +## 核心概念 -### 1.1 统一响应格式 +### 统一响应格式 -**所有接口统一输出 JSON,HTTP 状态码始终 `200`,错误信息在 `code` 字段:** +所有接口输出 JSON,HTTP 状态码始终 `200`,错误信息在 `code` 字段: -```json -{ "code": 200, "message": "成功", "data": { ... } } -``` +| code | 含义 | +|------|------| +| 200 | 成功 | +| 204 | 无数据 | +| 400 | 参数错误 | +| 401 | 需要登录 | +| 403 | 无权访问 | +| 404 | 资源不存在 | +| 500 | 服务器错误 | -| code 含义 | -|-----------| -| 200 成功 | 204 无数据 | 400 参数错误 | -| 401 需要登录 | 403 无权访问 | 404 不存在 | 500 服务器错误 | - -### 1.2 异常处理流程 +### 异常处理流程 ``` -请求进入 - └─ SimApiExceptionMiddleware(捕获所有异常 → HTTP 200 + code) - └─ SimApiAuthMiddleware(解析 Token → HttpContext.Items["LoginInfo"]) - └─ [SimApiSign] Filter(签名验证) - └─ [SimApiAuth] Filter(登录/角色检查) - └─ OnActionExecuting(模型验证 → code 400) - └─ Action 执行 - └─ SimApiResponseFilter(封装响应) +请求 → SimApiExceptionMiddleware(全异常捕获→HTTP 200+JSON) + → SimApiAuthMiddleware(Token→LoginInfo) + → [SimApiSign] Filter(签名) + → [SimApiAuth] Filter(登录检查) + → OnActionExecuting(模型验证→code 400) + → Action + → SimApiResponseFilter(封装响应) ``` -### 1.3 GOTCHAS(AI 最容易犯的错) - -| ❌ 错误 | ✅ 正确 | -|---------|---------| -| `SupportedMethod` 写多种方法 | 默认仅 **POST**,按需显式添加 | -| `WorkerNum = 50` | 默认是 **5** | -| 存储路径 `/avatars/file.jpg`(无前导斜杠) | 路径必须以 **`/`** 开头 | -| `s.Endpoint = "http://minio:9000/"` | `ServeUrl`/`Endpoint` **不能以 `/` 结尾** | -| `synapse.PublishEvent(...)` | 方法名是 **`synapse.Event(...)`** | -| `synapse.CallRpcAsync(...)` | 方法名是 **`synapse.Rpc(...)`** | -| HTTP 状态码 4xx/5xx 表示错误 | 所有错误均 **HTTP 200**,错误在 JSON `code` | -| `SimApiStorageOptions = Configuration.GetSection(...)` | 用 **`options.ConfigureSimApiStorage(s => {...})`** | -| MQTT 用 RabbitMQ | **MQTTnet v5,通过 WebSocket 连接** | - --- -## 2. C# 语言与编码规范 - -### 2.1 技术栈基础 +## 项目结构 ``` -NuGet: Simcu.SimApi -.NET 8 / 9 / 10 C# 12 / 14 -Nullable: enable ImplicitUsings: enable +SimApi/ +├── Attributes/ # [SimApiAuth] [SimApiDoc] [SimApiSign] [AesBody] [SynapseEvent] [SynapseRpc] [OriginResponse] +├── AuthSDK/ # Auth Center & IAM SDK +│ ├── SimApiAuthClient.cs HTTP 客户端(签名调用 Auth Center) +│ ├── SimApiAuthCenter.cs Auth Center API 封装 +│ ├── SimApiAuthCenterDto.cs 数据模型 +│ ├── SimApiAuthCenterMiddleware.cs 网关鉴权中间件 +│ ├── SimApiAuthIam.cs IAM 权限管理 API +│ └── SimApiAuthIamDto.cs IAM 数据模型 +├── Communications/ # SimApiBaseResponse, PageResponse, SimApiLoginItem, SimApiBaseRequest +├── Configurations/ # SimApiOptions + 各模块 Option 类 +├── Controllers/ # SimApiBaseController, SimApiCommonController, SimApiAuthController +├── Helpers/ # SimApiError, SimApiAuth, SimApiCache, SimApiHttpClient, SimApiStorage, SimApiUtil, SimApiAesUtil +├── Interfaces/ # ISimApiAuthChecker +├── Middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware +├── Synapse/ # MQTT Pub/Sub + RPC + Config Store +├── Exceptions/ # SimApiException +├── Models/ # SimApiBaseModel +├── SwaggerFilters/ # 文档自动过滤器 +├── ModelBinders/ # AesBody, SimApiSign +├── Logger/ # 彩色控制台日志 +└── SimApiExtensions.cs # AddSimApi + UseSimApi ``` -### 2.2 命名空间 - -使用**文件范围命名空间**: - -```csharp -// ✅ -namespace MyApp.Controllers; - -// ❌ -namespace MyApp.Controllers { } -``` - -### 2.3 主构造函数(依赖注入) - -```csharp -// ✅ 主构造函数 -public class OrderController(DataContext db) : SimApiBaseController { } - -// ❌ 传统构造函数 -public class OrderController : SimApiBaseController { - private readonly DataContext _db; - public OrderController(DataContext db) { _db = db; } -} -``` - -### 2.4 集合表达式 - -优先 `[]`: -```csharp -string[] tags = []; // ✅ -string[] roles = ["admin", "manager"]; // ✅ -// var tags = new string[] { }; // ❌ -``` - -### 2.5 Null 处理 - -```csharp -var key = app?.Key; // 安全访问 -var name = user?.Name ?? "匿名"; // 空合并 -config ??= new Dictionary(); // 空合并赋值 -``` - -### 2.6 命名规范 - -| 类型 | 规则 | 示例 | -|------|------|------| -| 类、接口、枚举 | PascalCase | `AdminController`、`ResPermission` | -| 方法名 | PascalCase | `UserList`、`ApplicationEdit` | -| 属性名 | PascalCase | `AccountId`、`LicenseTotal` | -| 私有字段 | `_camelCase`(如有) | `_logger` | -| 局部变量、参数 | camelCase | `var user`、`var appId` | -| 常量 | PascalCase | `MaxRetryCount`、`DefaultRole` | -| 路由路径 | 全小写 + 连字符 | `/device/refresh-context` | -| 配置键 | PascalCase:PascalCase | `"Sms:Templates:Verify"` | -| Redis 缓存 Key | `模块:子类型:标识` | `"Sms:Verify:登陆:手机号"` | - --- -## 3. 项目目录结构 +## 1. 错误处理 — SimApiError -推荐**极简扁平化**: +独立静态类 `SimApi.Helpers.SimApiError`。所有错误最终 → `throw new SimApiException(code, message)` → 中间件捕获。 -``` -项目名/ -├── Controllers/ # 控制器(含业务逻辑) -│ └── Dtos/ # 请求/响应 DTO -├── Models/ # EF Core 实体 + DataContext -├── Helpers/ # 工具类 / 框架扩展点 -├── Migrations/ # EF Core 迁移(自动生成,勿手改) -└── Program.cs # 入口 + DI + 中间件(无 Startup.cs) -``` +**使用**: 文件顶部加 `using static SimApi.Helpers.SimApiError;` -业务逻辑直接在 Controller 中通过 `db`(EF Core DbContext)操作数据库。可复用横切逻辑抽取到 `Helpers/`。 - -> 如果项目较复杂也可选标准分层(Controllers → Services → Models),但需项目内保持一致不混用。 - ---- - -## 4. 错误处理系统(SimApiError) - -### 4.1 概述 - -报错方法已从 BaseController 移出,独立为静态类 `SimApi.Helpers.SimApiError`。可在任何地方使用。 - -**使用方式**:每个需要用的文件加一行 `using static SimApiHelpers.SimApiError;` 即可直接调用: +### 完整方法签名 ```csharp -using static SimApi.Helpers.SimApiError; +// 直接抛错 +void Error(int code = 500, string message = ""); + +// condition 为 true 时抛错 (code 默认 400) +void ErrorWhen([DoesNotReturnIf(true)] bool condition, int code = 400, string message = ""); + +// 同上(别名) +void ErrorWhenTrue([DoesNotReturnIf(true)] bool condition, int code = 400, string message = ""); + +// condition 为 false 时抛错 +void ErrorWhenFalse([DoesNotReturnIf(false)] bool condition, int code = 400, string message = ""); + +// obj 为 null 时抛错 (code 默认 404) +void ErrorWhenNull([NotNull] object? condition, int code = 404, string message = ""); ``` -> 业务项目可在根目录建 `GlobalUsings.cs` 加 `global using static SimApi.Helpers.SimApiError;` 实现全项目免 import。 - -### 4.2 方法签名 - -```csharp -// 直接抛出错误 -Error(int code = 500, string message = ""); - -// condition 为 true 时抛出(默认 code=400) -ErrorWhen([DoesNotReturnIf(true)] bool condition, int code = 400, string message = ""); - -// ErrorWhen 的别名 -ErrorWhenTrue(bool condition, int code = 400, string message = ""); - -// condition 为 false 时抛出 -ErrorWhenFalse([DoesNotReturnIf(false)] bool condition, int code = 400, string message = ""); - -// obj 为 null 时抛出(默认 code=404) -ErrorWhenNull([NotNull] object? condition, int code = 404, - string message = "请求的资源不存在"); - -// 泛型版本(编译器 null 流分析更精准) -ErrorWhenNull([NotNull] T? condition, int code = 404, - string message = "请求的资源不存在") where T : class; -``` - -所有方法最终都 `throw new SimApiException(code, message)`,由全局中间件捕获转为 `{code, message}` 响应。 - -### 4.3 使用示例 +### 示例 ```csharp using static SimApi.Helpers.SimApiError; -// Controller 内 -[HttpPost] -public UserDto GetUser(string id) -{ - var user = db.Users.Find(id); - ErrorWhenNull(user, 404, "用户不存在"); // null → 404 - ErrorWhen(user.Status == 0, 403, "账号已被禁用"); // 条件成立 → 报错 - return user; -} - -// Helper / Service 中同样可用 -Error(500, "内部错误"); -ErrorWhen(isDuplicated, 400, "数据已存在"); +var user = db.Users.Find(id); +ErrorWhenNull(user, 404, "用户不存在"); +ErrorWhen(amount <= 0, 400, "金额无效"); ErrorWhenFalse(hasPermission, 403, "无权操作"); +Error(500, "服务器内部错误"); ``` --- -## 5. 控制器规范 - -### 5.1 基类 - -所有业务控制器继承 `SimApiBaseController`: +## 2. 控制器 — SimApiBaseController ```csharp -using static SimApi.Helpers.SimApiError; - -[ApiController] -[Route("[controller]")] -public class UserController(DataContext db) : SimApiBaseController +public class SimApiBaseController : Controller { - // 获取当前登录用户(需 EnableSimApiAuth) + // 当前登录信息(需 EnableSimApiAuth) protected SimApiLoginItem LoginInfo => (SimApiLoginItem)HttpContext.Items["LoginInfo"]!; + protected string LoginToken => (string)HttpContext.Items["LoginToken"]!; + + // OnActionExecuting 自动验证 ModelState → 无效时抛 code 400 } ``` -`SimApiBaseController` 已内置: -- `[Consumes("application/json")]` + `[Produces("application/json")]` -- `OnActionExecuting` 自动验证 ModelState,无效时抛 `code 400` -- `LoginInfo` 属性获取当前登录信息 +**内建**: `[Consumes("application/json")]` + `[Produces("application/json")]` -### 5.2 路由规则 +### 自动路由 -**默认全部 POST**(除非在 `SupportedMethod` 显式添加): +| 路由 | 方法 | 条件 | 说明 | +|------|------|------|------| +| `/versions` | GET/POST | `EnableVersionUrl`(默认) | 返回 SimApi/App 版本 | +| `/user/info` | POST | `EnableSimApiAuth` | 需登录,返回 LoginInfo | +| `/logout` | POST | `EnableSimApiAuth` | 退出登录(可自定义路由) | +| `/swagger` | GET | `EnableSimApiDoc` | Swagger UI | +| `/jobs` | GET | `EnableJob` + DashboardUrl | Hangfire 控制台 | +| `/exception/{code:int}` | GET | 始终 | 错误反馈页面 | + +### 返回值规范 + +| 场景 | 返回类型 | +|------|----------| +| 写操作 | `void` | +| 单条查询 | 直接 Entity | +| 列表查询 | `Entity[]` | +| 分页 | `PageResponse` | +| 自定义状态 | `SimApiBaseResponse` | +| 跳封装 | 方法加 `[OriginResponse]` | + +--- + +## 3. 认证系统 — SimApiAuth ```csharp -// 方法上写完整路径 -[HttpPost("/device/list")] -[HttpPost("/application/refresh-key")] +string Login(SimApiLoginItem loginItem, TimeSpan? expireTime = null, string? token = null); // 默认7天 +string Update(SimApiLoginItem loginItem, string token); +SimApiLoginItem? GetLogin(string token); +SimApiLoginItem[] GetAllLogins(string userId); +void Logout(string token); +void LogoutAll(string userId); +``` -// 类上前缀 + 方法相对路径 -[Route("/platform")] -public class PlatformController(DataContext db) : SimApiBaseController -{ - [HttpPost("device/detail")] // 最终路由:/platform/device/detail - [HttpPost("bot/generate")] // 最终路由:/platform/bot/generate +```csharp +// LoginItem 结构 +public class SimApiLoginItem { + required string Id; + string[] Type = ["user"]; + Dictionary Meta = []; + Dictionary Extra = []; } ``` -路由路径全小写,多词用连字符 `-` 分隔。 +**Token 传参**: Header `Token: ` -### 5.3 鉴权 Attribute - -| Attribute | 用途 | -|-----------|------| -| `[SimApiAuth]` | 要求登录 | -| `[SimApiAuth("admin")]` | 要求 admin 角色 | -| `[SimApiAuth("admin,manager")]` | OR 关系 | -| `[SimApiSign(KeyProvider = typeof(Xxx))]` | 签名验证 | - -> 鉴权 Attribute 加在 **Controller 类** 上,不加在方法上。 - -### 5.4 接口分组与文档 +### 认证后处理 Hook — ISimApiAuthChecker ```csharp -// 参数1: 分组Tag(对应 ApiExplorerSettings.GroupName) -// 参数2: 接口名称(必填) -// 参数3: 接口详细描述(可选) -[ApiExplorerSettings(GroupName = "platform")] -[SimApiDoc("设备", "获取设备详情")] -[SimApiDoc("设备", "获取设备详情", "根据设备ID查询设备的完整信息,含关联应用列表")] -[HttpPost("device/detail")] -public Device DeviceDetail(...) { ... } -``` - -> 每个接口都应标注 `[SimApiDoc]`,至少填写前两个参数。复杂业务接口建议补充第三参数。 - -### 5.5 返回值规范 - -| 场景 | 返回类型 | 说明 | -|------|----------|------| -| 写操作(增删改) | `void` | 框架自动返回 `{"code":200}` | -| 单条查询 | 直接返回 Entity | 如 `Account`、`Device` | -| 列表查询 | `Entity[]` 数组 | 不用 `List` | -| 分页查询 | `PageResponse` | 含 Total/Page/Count/List | -| 有状态响应 | `SimApiBaseResponse` | 自定义 code+message | -| 复杂组合 | 对应 DTO | 自定义结构 | - -> **不使用** `ActionResult` 或 `IActionResult`(除非用了 `[AesBody]` 等框架 Attribute)。 - -### 5.6 参数规范 - -```csharp -// 普通请求体 -[FromBody] DeviceDto.SerialAddRequest request - -// AES 加密请求体 -[AesBody(KeyProvider = typeof(AesBodyProvider))] BotDto.BotChatRequest request - -// 查询参数(直接写,不加 Attribute) -string appId -``` - -### 5.7 当前登录信息 - -```csharp -var userId = LoginInfo?.Id; // 用户唯一标识 -var userRole = LoginInfo?.Type; // string[] 角色列表 -``` - -> **⚠️ 权限检查一律使用 `[SimApiAuth]` Attribute,禁止在代码中手动判断 `LoginInfo.Type.Contains(...)` 来控制权限。** -> -> 需要角色权限的 Controller 直接标注:`[SimApiAuth("admin")]` -> 需要数据归属校验(如"只能操作自己的资源")在路由方法中直接写条件判断即可。 - -### 5.8 控制器方法规范 - -**Controller 中只保留路由方法(即带 `[HttpPost]`/`[HttpGet]` 等路由 Attribute 的 public 方法)。** - -不要在 Controller 里写非路由的 private 辅助方法。可复用的横切逻辑应抽取到 `Helpers/` 目录下的独立类中。 - -```csharp -// ✅ 控制器保持简洁,只有路由方法 -[SimApiAuth] -public class UserController(DataContext db) : SimApiBaseController -{ - [HttpPost("list")] - public User[] GetUserList() => db.Users.OrderBy(x => x.CreatedAt).ToArray(); - - [HttpPost("detail")] - public User GetUser([FromBody] StringIdOnlyRequest req) - { - var user = db.Users.Find(req.Id); - ErrorWhenNull(user); - return user; - } -} - -// ✅ 复杂逻辑抽到 Helper -// Helpers/UserHelper.cs -public static class UserHelper -{ - public static void ValidateOwnership(DataContext db, string loginId, string resourceId) - { - ErrorWhen(!db.Resources.Any(x => x.Id == resourceId && x.OwnerId == loginId), - 403, "无权操作此资源"); - } +public interface ISimApiAuthChecker { + void Run(SimApiLoginItem loginItem, string token); } +// 实现后自动注册为 Scoped,每次认证后调用 ``` --- -## 6. 响应格式详解 +## 4. AuthSDK — Auth Center 网关 & IAM -### 6.1 结构 +启用: `EnableSimApiAuthGate = true` -```json -{ "code": 200, "message": "成功", "data": { ... } } -``` - -### 6.2 构造方式 +### 4.1 配置 ```csharp -// 无数据 -return new SimApiBaseResponse(); // {"code":200,"message":"成功"} -return new SimApiBaseResponse(400, "参数错误"); // {"code":400,"message":"参数错误"} -return new SimApiBaseResponse(404); // {"code":404,"message":"接口不存在"} - -// 带数据 -return new SimApiBaseResponse(user); // data = user - -// 分页 -return new PageResponse +options.EnableSimApiAuthGate = true; +options.ConfigureSimApiAuthCenter(gate => { - List = devices, Page = 1, Count = 20, Total = 100 -}; + gate.Server = "https://auth.coce.cc"; + gate.AppId = "your-app-id"; + gate.AppKey = "your-app-key"; + gate.UseMiddleware = true; // 内部应用启用,解析 X-SimApi-Gate-Auth Header + gate.UseIam = true; // 启用 IAM +}); ``` -### 6.3 响应过滤器行为 +### 4.2 SimApiAuthClient -| 控制器返回值 | 最终 JSON 输出 | -|-------------|--------------| -| `null` | `{"code":200,"message":"成功"}` | -| 普通对象 | `{"code":200,"message":"成功","data":对象}` | -| `SimApiBaseResponse` | 原样输出 | -| `[OriginResponse]` 方法 | 完全不封装,原始输出 | +继承 `SimApiHttpClient`,自动配置 Server/AppId/AppKey,对 Auth Center 发起签名请求。 + +### 4.3 SimApiAuthCenter — Auth Center API + +```csharp +// === 签名验证 === +void VerifySign(string appId, string timestamp, string nonce, string sign); + +// === 群组 === +GroupRelatedItem[]? GroupRelated(string profileId); +AppAndProfileItem[]? GroupSearch(string keyword, int skip = 0, int take = 20); +GroupDetailTreeNode? GroupDetail(string groupId, string profileId); +string[]? GroupRelatedIndex(string groupId, string profileId); + +// === Profile === +AppAndProfileItem[]? ProfileSearch(string keyword, int skip = 0, int take = 20); +AppAndProfileItem[]? ProfileList(string[] ids); + +// === 内部应用专用 === +bool CheckIsAppOwner(string profileId, string applicationId); +AppAndProfileItem[]? GetAppList(string profileId, IEnumerable appIds); + +// === 登录 === +GetCodeResponse GetLoginCode(string? scene = null, Dictionary? data = null, string? backUrl = null); +LoginInfoResponse GetLoginInfo(string code, string? scene = null); + +// === 安全验证(二次确认) === +GetCodeResponse GetConfirmCode(string scene, string userId, Dictionary? data = null, string? backUrl = null); +ConfirmResponse Confirm(string code, string scene, string? userId = null); +``` + +**GetCodeResponse**: `{ Code, Server, FullUrl }` + +### 4.4 SimApiAuthIam — IAM 权限管理 + +```csharp +void RegisterPermissions(PermissionItem[] permissions); +string[] GetPermissionOwned(string profileId); +void CheckPermission(string profileId, string permission); +``` + +**PermissionItem**: `{ Identifier, Name, Group, Description }` + +### 4.5 SimApiAuthCenterMiddleware — 网关鉴权 + +内部应用专用。解析请求头 `X-SimApi-Gate-Auth` / `X-SimApi-Gate-Time` / `X-SimApi-Gate-Sign`,MD5 验签后将 Base64 解码的用户信息注入 `HttpContext.Items["LoginInfo"]`。 --- -## 7. SimApiOptions 完整配置 +## 5. Attributes 完整参考 -通过 `AddSimApi(options => { ... })` 设置: - -### 7.1 功能开关 +### [SimApiAuth] — 身份认证 ```csharp -options.RedisConfiguration = "localhost:6379"; // 多模块共用 -options.EnableSimApiAuth = false; // Token 认证 -options.EnableSimApiDoc = false; // Swagger 文档 -options.EnableSimApiStorage = false; // S3 对象存储 -options.EnableJob = false; // Hangfire 任务调度 -options.EnableSynapse = false; // MQTT 通信 -options.EnableCoceSdk = false; // Coce 统一身份 -// 以下默认 true,通常不改: -options.EnableCors = true; // 全量 CORS -options.EnableSimApiException = true; // 全局异常拦截 -options.EnableSimApiResponseFilter = true; // 响应统一封装 -options.EnableForwardHeaders = true; // 反向代理 Header -options.EnableLowerUrl = true; // URL 小写化 -options.EnableVersionUrl = true; // /versions 接口 -options.EnableLogger = true; // 彩色控制台日志 +[SimApiAuth] // 仅检查登录 +[SimApiAuth("admin")] // 单角色 +[SimApiAuth("admin,manager")] // 逗号分隔 OR 关系 +[SimApiAuth(new[]{"a","b"})] // 数组形式 ``` +- 加在 **Controller 类** 或 **方法** 上 +- **禁止** 代码中手动判断 `LoginInfo.Type.Contains(...)` 做权限控制 -### 7.2 子模块配置 - -```csharp -options.ConfigureSimApiDoc(doc => { ... }); // Swagger -options.ConfigureSimApiStorage(s => { ... }); // S3 存储 -options.ConfigureSimApiJob(job => { ... }); // 任务调度 -options.ConfigureSimApiSynapse(s => { ... }); // MQTT -options.ConfigureCoceSdk(coce => { ... }); // Coce 身份 -``` - ---- - -## 8. Attributes 完整参考 - -### 8.1 [SimApiAuth] — 身份认证 - -```csharp -[SimApiAuth] // 仅检查登录 -[SimApiAuth("admin")] // Type 包含 "admin" -[SimApiAuth("admin,manager")] // 逗号分隔 OR 关系 -[SimApiAuth(new[]{"a", "b"})] // 数组形式 -``` - -### 8.2 [SimApiDoc] — Swagger 文档注解 +### [SimApiDoc] — Swagger 文档注解 ```csharp [SimApiDoc("分组名", "接口名称")] @@ -468,241 +292,193 @@ options.ConfigureCoceSdk(coce => { ... }); // Coce 身份 [SimApiDoc(new[]{"tag1","tag2"}, "接口名称")] ``` -### 8.3 [SynapseEvent] — MQTT 事件处理 - -```csharp -// 参数规则:0个 / 1个(eventName string) / 2个(eventName string, T data) -[SynapseEvent("order/created")] -public void OnOrderCreated(string eventName) { } - -[SynapseEvent("order/+/status")] // 支持 MQTT 通配符 + 和 # -public void OnOrderStatus(string eventName, OrderStatusDto data) { } -``` - -### 8.4 [SynapseRpc] — MQTT RPC 方法 - -```csharp -[SynapseRpc] // 注册名为 "ClassName.MethodName" -[SynapseRpc("customRpcName")] // 自定义名 - -// 支持 0~2 个参数,第2个固定为 Dictionary(headers) -public UserDto GetUserInfo(GetUserRequest req) { } -public UserDto GetUserInfo(GetUserRequest req, Dictionary headers) { } -``` - -### 8.5 [AesBody] — AES 解密请求体 - -```csharp -[HttpPost] -public IActionResult Submit([AesBody(KeyProvider = typeof(MyAesKeyProvider))] MyRequest req) -{ /* request 已自动解密 */ } - -// 客户端提交: {"data": "Base64(AES-256-CBC加密JSON)"} -``` - -### 8.6 [OriginResponse] — 跳过响应封装 - -```csharp -[HttpGet][OriginResponse] -public string GetRaw() => "raw string"; -``` - -### 8.7 [SimApiSign] — API 签名验证 +### [SimApiSign] — API 签名验证 ```csharp [SimApiSign(KeyProvider = typeof(MySignProvider))] -public IActionResult SecureApi(...) { } -// 签名算法: MD5(field1=v1&...&appId=xxx×tamp=ts&nonce=nnn&密钥) +// 签名: MD5(field1=v1&...&appId=xxx×tamp=ts&nonce=nnn&密钥) +``` + +实现 `SimApiSignProviderBase`: +```csharp +public class MySignProvider : SimApiSignProviderBase +{ + // 可覆盖: AppIdName, TimestampName, NonceName, SignName, QueryExpires(秒), DuplicateRequestProtection, SignFields + public override string? GetKey(string? appId) { ... } +} +``` + +### [AesBody] — AES 解密请求体 + +```csharp +[AesBody(KeyProvider = typeof(MyAesKeyProvider))] MyRequest req +// 客户端提交: {"data": "Base64(AES-256-CBC 密文)"} +``` + +实现 `AesBodyProviderBase`,覆盖 `AppIdName` 和 `GetKey(appId)`。 + +### [SynapseEvent] — MQTT 事件处理 + +```csharp +[SynapseEvent("order/created")] // 指定 eventName +[SynapseEvent] // 不指定 = 方法名 +// 参数: 0个 / 1个(string eventName) / 2个(string eventName, T data) +``` + +### [SynapseRpc] — MQTT RPC 方法 + +```csharp +[SynapseRpc] // 注册名 "ClassName.MethodName" +[SynapseRpc("customName")] // 自定义名 +// 参数: 0~2个,第2个固定 Dictionary(headers) +``` + +### [OriginResponse] — 跳过响应封装 + +```csharp +[HttpGet][OriginResponse] +public string GetRaw() => "raw"; ``` --- -## 9. 模块详解:认证系统(SimApiAuth) - -### 9.1 配置 - -```csharp -options.EnableSimApiAuth = true; // 必须同时配置 RedisConfiguration -// Token 通过 Header 传入:Token: -``` - -### 9.2 DI 注入与 API - -```csharp -public MyController(SimApiAuth auth) { } - -string token = auth.Login(loginItem); // 自动 GUID token -string token = auth.Login(loginItem, "custom-token"); -auth.Update(loginItem, token); // 更新不换 token -SimApiLoginItem? info = auth.GetLogin(token); // 查询登录态 -auth.Logout(token); // 退出 -``` - -### 9.3 SimApiLoginItem 结构 - -```csharp -{ Id: string, Type: string[], Meta: Dictionary, Extra: object? } -``` - -- `Id`: 用户唯一标识 -- `Type`: 角色数组,如 `["user", "admin"]` -- `Meta`: 附加元数据字典 -- `Extra`: 扩展对象 - -### 9.4 自动路由 - -启用后自动生成: - -| 路由 | 方法 | 说明 | -|------|------|------| -| `POST /auth/check` | 无需登录 | 检测登录状态,返回用户 ID | -| `POST /auth/logout` | 无需登录 | 退出登录 | -| `POST /user/info` | 需要登录 | 获取当前用户完整信息 | - ---- - -## 10. 模块详解:Swagger 文档(EnableSimApiDoc) - -### 10.1 配置 +## 6. Swagger 文档 — EnableSimApiDoc ```csharp options.ConfigureSimApiDoc(doc => { doc.DocumentTitle = "接口文档"; - - doc.ApiGroups = [ - new("api", "公共接口"), - new("admin", "管理接口", "描述可选") - ]; - - doc.ApiAuth = new SimApiAuthOption { Type = ["SimApiAuth"] }; - doc.SupportedMethod = [SubmitMethod.Post]; // 默认仅 Post!按需添加其他 + doc.ApiGroups = [new("api", "公共"), new("admin", "管理", "描述可选")]; + doc.SupportedMethod = [SubmitMethod.Post]; // 默认仅POST + doc.ApiAuth = new SimApiAuthOption { Type = ["SimApiAuth"] }; // 认证方式 }); - -// 分组方式 -[ApiExplorerSettings(GroupName = "admin")] // 归入 admin 组 -// 不标注则默认归入 "api" 组 ``` -### 10.2 访问 +每组通过 `[ApiExplorerSettings(GroupName = "admin")]` 分类。 -启动后访问 `/swagger`。 - -### 10.3 自动 Swagger 过滤器(无需手动配置) +### 自动过滤器 | 过滤器 | 效果 | |--------|------| -| `SimApiResponseOperationFilter` | 返回类型包装为 `SimApiBaseResponse` | -| `SimApiAuthOperationFilter` | `[SimApiAuth]` 接口加 Token 认证要求 | -| `SimApiSignOperationFilter` | `[SimApiSign]` 接口注入签名参数说明 | -| `AesBodyOperationFilter` | `[AesBody]` 参数展示原始数据结构 | -| `GlobalDynamicObjectSchemaFilter` | `object`/`Dictionary` 类型生成 Schema 示例 | -| `RemoveEmptyTagsFilter` | 清除空分组 Tag | +| `SimApiResponseOperationFilter` | 返回值包装为 `SimApiBaseResponse` | +| `SimApiAuthOperationFilter` | 鉴权接口 + Token Header | +| `SimApiSignOperationFilter` | 签名接口注入签名参数 | +| `AesBodyOperationFilter` | AES 接口展示原始结构 | +| `GlobalDynamicObjectSchemaFilter` | object/Dictionary → Schema | +| `RemoveEmptyTagsFilter` | 清除空分组 | --- -## 11. 模块详解:对象存储(EnableSimApiStorage) +## 7. 对象存储 — EnableSimApiStorage 基于 MinIO SDK(S3 兼容)。 -### 11.1 配置 +### 配置 ```csharp options.EnableSimApiStorage = true; options.ConfigureSimApiStorage(s => { - s.Endpoint = "http://minio:9000"; // 不能以 / 结尾 + s.Endpoint = "http://minio:9000"; // 不能以 / 结尾 s.AccessKey = "admin"; s.SecretKey = "pass"; - s.Bucket = "my-bucket"; - s.ServeUrl = "http://cdn.example.com/my-bucket"; // 不能以 / 结尾 + s.Bucket = "bucket"; + s.ServeUrl = "http://cdn.example.com/bucket"; // 不能以 / 结尾 }); ``` -### 11.2 API +### API ```csharp -public MyController(SimApiStorage storage) { } +GetUploadUrlResponse GetUploadUrl(string path, int expire = 7200); +// 返回: { UploadUrl, DownloadUrl, Path } -// 路径必须以 / 开头 -GetUploadUrlResponse r = storage.GetUploadUrl("/avatars/user1.jpg"); -// r.UploadUrl → PUT 上传地址(前端直接用) -// r.DownloadUrl → 公开访问 URL -// r.Path → 相对路径 +string GetDownloadUrl(string path, int expire = 600); +void UploadFile(string path, Stream stream, string contentType = "image/png"); +string? FullUrl(string? path); // 路径→完整URL +string? GetUrl(string? path); // 同上 +string? GetPath(string? url); // URL→相对路径 +IMinioClient Client { get; } // 底层 MinIO 客户端 +``` -string url = storage.GetDownloadUrl("/files/doc.pdf"); // 默认 10 分钟 -string url = storage.GetDownloadUrl("/files/doc.pdf", expire: 3600); +> **路径必须以 `/` 开头** -storage.UploadFile("/path/file.jpg", stream, "image/jpeg"); +--- -string? url = storage.FullUrl("/path/file"); // 路径转完整 URL -string? url = storage.GetUrl("/path/file"); // 同上 -string? path = storage.GetPath("http://cdn.../my-bucket/path/file"); // URL→路径 +## 8. Redis 缓存 — SimApiCache -IMinioClient mc = storage.Client; // 底层 MinIO 客户端 +依赖 `RedisConfiguration`,Key 自动加前缀 `SimApi:Cache:`。 + +```csharp +void Set(string key, object value, DistributedCacheEntryOptions? options = null); +T? Get(string key); +string? Get(string key); +bool HasKey(string key); +void Remove(string key); ``` --- -## 12. 模块详解:Redis 缓存(SimApiCache) +## 9. HTTP 客户端 — SimApiHttpClient -> 依赖 `RedisConfiguration`,key 自动加前缀 `SimApi:Cache:` +用于调用其他带签名/AES 的 SimApi 服务。 + +### 属性 ```csharp -public MyService(SimApiCache cache) { } +virtual string Server { get; init; } +virtual string AppId { get; init; } +virtual string AppKey { get; init; } +virtual string SignName { get; init; } = "sign"; +virtual string TimestampName { get; init; } = "timestamp"; +virtual string NonceName { get; init; } = "nonce"; +virtual string? AppIdName { get; init; } = "appId"; +virtual string[] SignFields { get; init; } = []; +``` -cache.Set("key", value); // 永不过期 -cache.Set("key", value, new DistributedCacheEntryOptions { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) -}); -string? raw = cache.Get("key"); // 原始字符串 -int? val = cache.Get("key"); // 反序列化 -cache.Remove("key"); // 删除 +### 调用方法 + +```csharp +T? SignQuery(string url, object? body = null, Dictionary? queries = null); +T? AesQuery(string url, object body); +T? AesSignQuery(string url, object body, Dictionary? queries = null); ``` --- -## 13. 模块详解:任务调度(EnableJob) +## 10. 任务调度 — EnableJob -Hangfire + Redis 存储。 - -### 13.1 配置 +Hangfire + Redis。 ```csharp options.EnableJob = true; options.ConfigureSimApiJob(job => { - job.DashboardUrl = "/jobs"; // null 则不开启 Dashboard + job.DashboardUrl = "/jobs"; // null = 不开启 job.DashboardAuthUser = "admin"; - job.DashboardAuthPass = "Admin@123!"; - job.RedisConfiguration = null; // null 则用全局 RedisConfiguration - job.Database = 1; // Redis DB 编号 + job.DashboardAuthPass = "pass"; + job.Database = 1; // Redis DB 编号 job.Servers = [ - new SimApiJobServerConfig { Queues = ["default"], WorkerNum = 5 }, - new SimApiJobServerConfig { Queues = ["email"], WorkerNum = 2 } + new() { Queues = ["default"], WorkerNum = 5 }, + new() { Queues = ["email"], WorkerNum = 2 } ]; }); ``` -### 13.2 使用 - ```csharp -BackgroundJob.Enqueue(() => myService.DoWork()); // 立即执行 -BackgroundJob.Schedule(() => myService.DoWork(), TimeSpan.FromMinutes(5)); // 延迟 -RecurringJob.AddOrUpdate("job-id", () => myService.DoWork(), Cron.Daily); // 定时 -var id = BackgroundJob.Enqueue(() => Step1()); -BackgroundJob.ContinueJobWith(id, () => Step2()); // 依赖链 +BackgroundJob.Enqueue(() => DoWork()); +BackgroundJob.Schedule(() => DoWork(), TimeSpan.FromMinutes(5)); +RecurringJob.AddOrUpdate("id", () => DoWork(), Cron.Daily); +BackgroundJob.ContinueJobWith(id, () => Step2()); ``` -Dashboard 访问 `/jobs`,Basic Auth 登录。 - --- -## 14. 模块详解:MQTT 通信(EnableSynapse) +## 11. MQTT 通信 — EnableSynapse 基于 MQTTnet v5,WebSocket 连接。 -### 14.1 配置 +### 配置 ```csharp options.EnableSynapse = true; @@ -711,579 +487,190 @@ options.ConfigureSimApiSynapse(s => s.Websocket = "ws://mqtt:8083/mqtt"; s.Username = "user"; s.Password = "pass"; - s.SysName = "my-system"; // Topic 命名空间前缀 - s.AppName = "order-service"; // 服务名 - s.AppId = "instance-001"; // 实例ID(不填自动GUID) - s.RpcTimeout = 3; // RPC 超时秒数 - s.EventLoadBalancing = false; // $queue 订阅负载均衡 - s.EnableConfigStore = true; // 分布式配置中心 - s.DisableEventClient = false; - s.DisableRpcClient = false; + s.SysName = "my-system"; + s.AppName = "order-service"; + s.AppId = "instance-001"; // 不填自动GUID + s.RpcTimeout = 3; // 秒 + s.EventLoadBalancing = false; // $queue 负载均衡 + s.EnableConfigStore = true; // 分布式配置中心 }); ``` -### 14.2 Topic 规则 +### Topic 规则 | 用途 | Topic 格式 | |------|-----------| | 事件发布 | `{SysName}/event/{AppName}/{eventName}` | -| 事件订阅(无负载均衡) | `{SysName}/event/{eventName}` | -| 事件订阅(有负载均衡) | `$queue/{SysName}/event/{eventName}` | -| RPC 请求 | `{SysName}/{targetApp}/rpc/server/{method}`($queue 天然负载均衡) | +| 事件订阅 | `{SysName}/event/{eventName}` (或 `$queue/` 前缀) | +| RPC 请求 | `{SysName}/{targetApp}/rpc/server/{method}` | | RPC 响应 | `{SysName}/{callerApp}/rpc/client/{AppId}/{messageId}` | -| 配置存储 | `{SysName}/synapse-config-store/{key}`(Retain 消息) | +| 配置 | `{SysName}/synapse-config-store/{key}` (Retain) | -### 14.3 Synapse API +### API ```csharp -public MyService(Synapse synapse) { } +// 事件 +bool Event(string eventName, dynamic? param = null); -synapse.Event("order/created", new { OrderId = 1 }); // 发布事件 +// RPC (同步) +SimApiBaseResponse Rpc(string appName, string method, dynamic? param = null, + Dictionary? headers = null, int? timeout = null); -// RPC 同步调用,返回 SimApiBaseResponse -var res = synapse.Rpc("user-service", "GetUserInfo", new { Id = 1 }); -var res = synapse.Rpc("user-service", "GetUserInfo", param, - headers: new Dictionary { { "traceId", "xxx" } }); - -// code=502 表示 RPC 超时 +// RPC 内报错 +void RpcError(int code, string message = ""); +void RpcErrorWhen(bool condition, int code, string message = ""); // 分布式配置 -synapse.SetConfig("key", "value"); -string? val = synapse.GetConfig("key"); -synapse.OnConfigChanged += (sender, item) => Console.WriteLine($"{item.Key}={item.Value}"); - -// RPC 方法内部抛错 -synapse.RpcError(400, "参数错误"); -synapse.RpcErrorWhen(id <= 0, 400, "ID 无效"); +bool SetConfig(string key, string value); +string? GetConfig(string key); ``` -### 14.4 处理器注册 +### 处理器扫描(自动注册) -含 `[SynapseRpc]`/`[SynapseEvent]` 的类会被**自动扫描注册为 Scoped 服务**,无需手动注册: - -```csharp -// 事件处理器 -public class OrderEventHandler -{ - [SynapseEvent("order/+/status")] - public void OnOrderStatus(string eventName, OrderStatusDto data) { } -} - -// RPC 服务 -public class UserRpcService -{ - [SynapseRpc] // 注册为 "UserRpcService.GetUserInfo" - public UserDto GetUserInfo(GetUserRequest req) { return ...; } - - [SynapseRpc("customName")] - public ResultDto DoSomething(RequestDto req, Dictionary headers) { } -} -``` +带 `[SynapseRpc]` / `[SynapseEvent]` 的类自动扫描注册为 Scoped 服务。 --- -## 15. 模块详解:API 签名验证([SimApiSign]) +## 12. AES 加解密 — SimApiAesUtil -### 15.1 实现密钥提供者 +AES-256-CBC + PKCS7,密钥经 SHA256 处理。 ```csharp -public class MySignProvider : SimApiSignProviderBase -{ - private readonly IServiceScopeFactory _scopeFactory; - public MySignProvider(IServiceScopeFactory sf) => _scopeFactory = sf; - - // ── 以下参数均有默认值,按需覆盖即可(不写则用默认值)── - public override string? AppIdName { get; set; } = "appId"; // Query/Header 参数名:应用ID - public override string TimestampName { get; set; } = "timestamp"; // Query/Header 参数名:时间戳 - public override string NonceName { get; set; } = "nonce"; // Query/Header 参数名:随机串 - public override string SignName { get; set; } = "sign"; // Query/Header 参数名:签名值 - public override int QueryExpires { get; set; } = 5; // 签名有效期(秒) - public override bool DuplicateRequestProtection { get; set; } = true;// 防重放攻击 - public override string[] SignFields { get; set; } = []; // 额外参与签名的业务字段(默认无) - - // ── 必须实现:根据 appId 返回对应密钥 ── - public override string? GetKey(string? appId) - { - using var scope = _scopeFactory.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - return db.Apps.Find(appId)?.SecretKey; - } -} - -builder.Services.AddScoped(); -``` - -### 15.2 使用 - -```csharp -[SimApiSign(KeyProvider = typeof(MySignProvider))] -public IActionResult SecureApi(...) { } - -// 签名算法: MD5(field1=v1&field2=v2&...&appId=xxx×tamp=ts&nonce=nnn&密钥) -// 支持通过 Query 或 Header 传入签名参数 -``` - ---- - -## 16. 模块详解:AES 加密传输([AesBody]) - -算法:**AES-256-CBC + PKCS7**,IV 随机生成附在密文前,整体 Base64 编码。 - -### 16.1 实现密钥提供者 - -```csharp -public class MyAesKeyProvider : AesBodyProviderBase -{ - private readonly IServiceScopeFactory _scopeFactory; - public MyAesKeyProvider(IServiceScopeFactory sf) => _scopeFactory = sf; - - public override string? AppIdName { get; set; } = "appId"; - - public override string? GetKey(string? appId) - { - using var scope = _scopeFactory.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); - return db.Apps.Find(appId)?.SecretKey; - } -} -builder.Services.AddScoped(); -``` - -### 16.2 使用 - -```csharp -[HttpPost] -public IActionResult Submit([AesBody(KeyProvider = typeof(MyAesKeyProvider))] MyRequest req) -{ /* request 已解密反序列化 */ } - -// 客户端提交: {"data": "Base64(AES-256-CBC 密文)"} - -// 静态工具类(无需注入,任意长度密钥会经 SHA256 处理为32字节) string cipher = SimApiAesUtil.Encrypt("明文", "任意长度密钥"); string plain = SimApiAesUtil.Decrypt(cipher, "任意长度密钥"); ``` --- -## 17. 模块详解:HTTP 客户端(SimApiHttpClient) - -用于调用其他带签名/AES 加密的 SimApi 服务。 - -### 17.1 构造函数与属性 +## 13. 工具集 — SimApiUtil ```csharp -// 构造函数(必填参数) -var client = new SimApiHttpClient( - appId: "myapp", // 必填:应用 ID - appKey: "secret" // 必填:应用密钥 - // debug: false // 可选:是否打印请求/响应日志(默认 false) -); +DateTime CstNow; // UTC+8 +double TimestampNow; // 秒级 Unix +string SimApiVersion; // NuGet 包版本 +string AppVersion; // 宿主应用版本 -// 以下属性均有默认值,按需覆盖即可: -client.Server = "https://api.example.com"; // 必须设置!目标服务地址(无默认值) -client.AppIdName = "appId"; // 默认 "appId" -client.TimestampName = "timestamp"; // 默认 "timestamp" -client.NonceName = "nonce"; // 默认 "nonce" -client.SignName = "sign"; // 默认 "sign" -client.SignFields = ["field1", "field2"]; // 默认 [](空) -``` +string Md5(string src, string mode = "x2"); // x2=32位, x3=48位, x4=64位 +string Sha1(string src, string mode = "x2"); +string Base64Encode(string str); +string Base64Decode(string base64Str); +string Base64Encode(object obj); +T? Base64Decode(string base64Str); -> **只有 `Server` 是必须设置的**,其他属性都有合理默认值。 +string Json(object? obj); // camelCase,中文不转义 +T? FromJson(string json); +T XmlDeserialize(string xml); +JsonSerializerOptions JsonOption; // 可复用 -### 17.2 调用方法 +bool CheckCell(string cell); // 手机号验证 +bool CheckEmail(string email); -```csharp -// 仅签名(自动计算 MD5 签名附加到 Query/Header) -var r = client.SignQuery("/api/user", body, queries); - -// 仅 AES 加密(body 自动加密为 {"data":"Base64密文"}) -var r = client.AesQuery("/api/user", body); - -// AES 加密 + 签名 -var r = client.AesSignQuery("/api/user", body, queries); +IQueryable Paginate(this IQueryable query, int page, int count); ``` --- -## 18. 模块详解:Coce 统一身份平台(EnableCoceSdk) - -> 同时需要 `EnableSimApiAuth = true` - -### 18.1 配置 - -```csharp -options.ConfigureCoceSdk(coce => -{ - coce.ApiEndpoint = "https://api.coce.cc"; // 默认 - coce.AuthEndpoint = "https://home.coce.cc"; // 默认 - coce.AppId = "your-app-id"; - coce.AppKey = "your-app-key"; -}); -``` - -### 18.2 CoceApp API - -```csharp -public MyService(CoceApp coce) { } - -// 用户 -coce.GetUserInfo(levelToken) -coce.GetUserGroups(levelToken) -coce.SearchUserByPhone("13800138000") -coce.SearchUserByIds(new[]{"uid1","uid2"}) - -// 消息 -coce.SendUserMessage(userId, "标题", "内容") - -// 支付 -string? tradeNo = coce.TradeCreate("商品名", 100, "扩展数据") -coce.TradeCheck(tradeNo) -coce.TradeRefund(tradeNo) - -// Token -coce.GetLevelToken(lv1Token, level: 5) -coce.SaveToken(userId, levelToken) -coce.GetToken(userId) - -// 代理请求 -coce.ProxyQuery(uri, token) -coce.ProxyQueue(uri, token, data) -``` - -### 18.3 自动路由 - -| 路由 | 方法 | 说明 | -|------|------|------| -| `POST /auth/login` | 无需登录 | Coce 一键登录(前端传 `{"data":"lv1Token"}`) | -| `POST /user/groups` | 需登录 | 获取群组列表 | -| `GET /auth/config` | 无需登录 | 获取 AppId 和授权 URL | - -### 18.4 自定义登录处理器 - -```csharp -public class MyLoginProcessor : ICoceLoginProcessor -{ - public SimApiLoginItem Process(SimApiLoginItem item, GroupInfo[] groups) - { - if (groups.Any(g => g.Role == "owner")) - item.Type = ["user", "admin"]; - return item; - } -} -builder.Services.AddScoped(); -``` - ---- - -## 19. 工具类(SimApiUtil,全部静态) - -```csharp -DateTime cst = SimApiUtil.CstNow; // UTC+8 当前时间 -double ts = SimApiUtil.TimestampNow; // 秒级 Unix 时间戳 -string simVer = SimApiUtil.SimApiVersion; // SimApi 包版本 -string appVer = SimApiUtil.AppVersion; // 宿主应用版本 - -string md5 = SimApiUtil.Md5("src"); // 32位 MD5 -string md5 = SimApiUtil.Md5("src", "x3"); // 48位 -string sha1 = SimApiUtil.Sha1("src"); - -string json = SimApiUtil.Json(obj); // camelCase,中文不转义 -T obj = SimApiUtil.XmlDeserialize(xml); -JsonSerializerOptions opts = SimApiUtil.JsonOption; // 可复用配置 - -bool ok = SimApiUtil.CheckCell("13800138000"); // 手机号验证 - -// IQueryable 分页扩展 -var paged = dbContext.Users.AsQueryable().Paginate(page: 1, count: 20); -``` - ---- - -## 20. 数据模型基类(SimApiBaseModel) - -ORM 实体基类,提供通用字段和轻量映射能力: +## 14. 数据模型 — SimApiBaseModel ```csharp public class UserEntity : SimApiBaseModel { - public string Name { get; set; } - // 自动拥有: Id(GUID string)、CreatedAt、UpdatedAt + // 自动: Id(GUID string), CreatedAt, UpdatedAt + // 默认忽略映射: Id, CreatedAt, UpdatedAt } -entity.MapData(dto); // 跳过 Id/CreatedAt/UpdatedAt,同名同类型非null属性 -entity.MapData(dto, mapAll: true); // 映射所有字段 -entity.MapData(dto, new[]{"Name"}); // 只映射指定字段 -entity.UpdateTime(); // 手动更新 UpdatedAt - -// MapData 只映射: 同名 + 同类型 + 源值不为null +void MapData(TS source, bool mapAll = false); // 同名同类型非null属性 +void MapData(TS source, string[] mapFields); // 指定字段 +void UpdateTime(); ``` --- -## 21. DTO 规范 +## 15. DTO 规范 -### 21.1 组织方式 - -DTO 在 `Controllers/Dtos/` 下,嵌套容器类: - -```csharp -namespace MyApp.Controllers.Dtos; - -public abstract class AdminDto -{ - public class UserEditRequest - { - public required string Id { get; set; } - public required string Name { get; set; } - } - - public class ApplicationListRequest : SimApiBasePageRequest - { - public string? Keyword { get; set; } - } -} -``` - -### 21.2 命名规则 - -| 类型 | 格式 | 示例 | +| 类型 | 命名 | 示例 | |------|------|------| -| 请求 DTO | `[动作]Request` | `UserEditRequest`、`DeviceSerialAddRequest` | -| 响应 DTO | `[动作]Response` | `GenerateResponse`、`TokenResponse` | -| 数据载体 | `[含义]Data` / `[含义]Item` | `GenerateData`、`AgentItem` | +| 请求 | `[动作]Request` | `UserEditRequest` | +| 响应 | `[动作]Response` | `TokenResponse` | +| 载体 | `[含义]Data/Item` | `GenerateData` | -引用时用全限定名:`AdminDto.UserEditRequest`。 - -### 21.3 属性规则 +### 框架内置 DTO ```csharp -public class RequestDto -{ - public required string Verify { get; set; } // 必填 - public required string AppId { get; set; } - [Range(1, 10000)] public required int Num { get; set; } // 范围校验 - public string? Remark { get; set; } // 可选 - public int Status { get; set; } = 1; // 有默认值 -} -``` - -### 21.4 框架内置 DTO(优先复用) - -| DTO | 用途 | -|-----|------| -| `SimApiStringIdOnlyRequest` | 只有 `Id` 字段 | -| `SimApiOneFieldRequest` | 只有 `Data` 字段 | -| `SimApiBasePageRequest` | 分页请求基类(Page + Count) | -| `SimApiBaseResponse` | 通用响应(可带 code + message) | -| `SimApiBaseResponse` | 带数据的响应 | -| `PageResponse` | 分页响应(Total + Page + Count + List) | - ---- - -## 22. Entity 与 DataContext 规范 - -### 22.1 Entity - -所有实体继承 `SimApiBaseModel`: - -```csharp -public class Account : SimApiBaseModel -{ - public required string Name { get; set; } - public required string Username { get; set; } - public required string Role { get; set; } = "user"; - public int Status { get; set; } = 1; -} -``` - -- 必填用 `required`,可选用 `?`,有默认值直接赋值 -- 外键命名:`[关联实体]Id`,如 `AccountId`、`AppId` -- **不配导航属性**,**不写 Fluent API**,依赖 Convention 自动映射 - -### 22.2 DataContext - -只定义 DbSet,不做任何配置: - -```csharp -public class DataContext(DbContextOptions options) : DbContext(options) -{ - public required DbSet Accounts { get; set; } - public required DbSet Applications { get; set; } -} +class SimApiStringIdOnlyRequest { required string Id; } +class SimApiOneFieldRequest { T? Data; } +class SimApiBasePageRequest { int Page = 1; int Count = 20; } +class SimApiBaseResponse { int Code; string Message; } // code=200 默认 +class SimApiBaseResponse : SimApiBaseResponse { T? Data; } +class PageResponse { T? List; int Page; int Count; int Total; } ``` --- -## 23. EF Core 查询风格 +## 16. SimApiOptions 完整配置 ```csharp -// 列表查询(排序 + ToArray) -db.Accounts.OrderBy(x => x.CreatedAt).ToArray(); - -// 动态条件查询 -var query = db.Devices.Where(x => x.ApplicationId == appId).OrderBy(x => x.CreatedAt).AsQueryable(); -if (!string.IsNullOrEmpty(request.Serial)) - query = query.Where(x => x.Serial == request.Serial); - -// 分页 -var list = query.Paginate(request.Page, request.Count).ToArray(); -var total = query.Count(); - -// 单条查询 -db.Accounts.Find(id); // 主键用 Find(命中缓存) -db.Accounts.FirstOrDefault(x => x.Username == username); // 其他用 FirstOrDefault - -// 写操作 -db.Add(entity); // 新增 -db.Update(entity); // 修改 -db.Remove(entity); // 删除 -db.SaveChanges(); // 最后统一 SaveChanges 一次 - -// 存在性判断(不用 Count > 0) -db.AppServices.Any(x => x.ServiceId == request.Id) -``` - ---- - -## 24. Program.cs 完整模板 - -```csharp -var builder = WebApplication.CreateBuilder(args); - -// 1. SimApi 框架 builder.Services.AddSimApi(options => { - options.RedisConfiguration = builder.Configuration.GetConnectionString("Redis"); - options.EnableSimApiAuth = true; - options.EnableSimApiDoc = true; - options.EnableSimApiStorage = false; - options.EnableJob = false; - options.EnableSynapse = false; + options.RedisConfiguration = "localhost:6379"; - options.ConfigureSimApiDoc(doc => - { - doc.DocumentTitle = "接口文档"; - doc.ApiGroups = [new("api", "公共接口"), new("admin", "管理接口")]; - doc.SupportedMethod = [SubmitMethod.Post]; - }); + // 功能开关 + options.EnableSimApiAuth = false; // Token 认证 + options.EnableSimApiAuthGate = false; // Auth Center 网关鉴权 + options.EnableSimApiDoc = false; // Swagger 文档 + options.EnableSimApiStorage = false; // S3 存储 + options.EnableJob = false; // Hangfire + options.EnableSynapse = false; // MQTT + options.EnableSimApiHttpClient = false; // 外部 HTTP 调用 + options.EnableLogger = true; // 控制台日志 + options.EnableCors = true; // 全量 CORS + options.EnableSimApiException = true; // 全局异常拦截 + options.EnableSimApiResponseFilter = true; // 响应统一封装 + options.EnableForwardHeaders = true; // 反向代理 Header + options.EnableLowerUrl = true; // URL 小写 + options.EnableVersionUrl = true; // /versions 接口 + + // 子模块配置 + options.ConfigureSimApiDoc(doc => { ... }); + options.ConfigureSimApiStorage(s => { ... }); + options.ConfigureSimApiJob(job => { ... }); + options.ConfigureSimApiSynapse(s => { ... }); + options.ConfigureSimApiAuthCenter(gate => { ... }); + options.ConfigureSimApiHttpClient(http => { ... }); + options.ConfigureSimApiRoute(route => { ... }); + options.ConfigureSimApiException(ex => { ... }); }); - -// 2. 数据库 -builder.Services.AddDbContext(opt => - opt.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); - -// 3. 框架扩展点(接口注册) -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// 4. 项目自定义服务 -builder.Services.AddScoped(); -builder.Services.AddSingleton(); - -var app = builder.Build(); - -// 5. 启动时自动迁移 -app.Services.CreateScope().ServiceProvider - .GetRequiredService().Database.Migrate(); - -// 6. 框架中间件 -app.UseSimApi(); -app.Run(); ``` --- -## 25. 配置文件规范 - -`appsettings.json` 只保留框架默认值: - -```json -{ - "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" -} -``` - -`appsettings.Development.json` 存放开发环境实际配置(不提交 Git): - -```json -{ - "ConnectionStrings": { - "Default": "Host=...;Database=...;Username=...;Password=...", - "Redis": "host:port,defaultDatabase=N" - }, - "Sms": { "Account": "...", "Password": "..." } -} -``` - -读取方式: -```csharp -builder.Configuration.GetConnectionString("Default") -config["Gateway:Key"] -config.GetSection("Sms").GetSection("Templates")["verify"] -``` +## 17. GOTCHAS — 常见错误 +| ❌ 错误 | ✅ 正确 | +|---------|---------| +| 存储路径 `avatars/file.jpg`(无前导 `/`) | 必须以 **`/`** 开头 | +| `s.Endpoint = "http://x:9000/"` | **不能以 `/` 结尾** | +| `synapse.PublishEvent(...)` | 方法名是 **`synapse.Event(...)`** | +| `synapse.CallRpcAsync(...)` | 方法名是 **`synapse.Rpc(...)`** | +| HTTP 4xx/5xx 状态码 | 永远 **HTTP 200**,错误在 JSON code | +| `SupportedMethod` 写多种方法 | 默认仅 **POST** | +| `SimApiStorageOptions = Configuration.GetSection(...)` | 用 **`ConfigureSimApiStorage(s => {...})`** | +| 代码中 `LoginInfo.Type.Contains("admin")` | 用 `[SimApiAuth("admin")]` | +| `return ActionResult` | 直接返回 Entity / void | --- -## 26. 内置路由汇总 +## 18. 禁止事项 -| 路由 | 方法 | 启用条件 | -|------|------|----------| -| `/swagger` | GET | `EnableSimApiDoc` | -| `/versions` | GET/POST | `EnableVersionUrl`(默认开) | -| `/auth/check` | POST | `EnableSimApiAuth` | -| `/auth/logout` | POST | `EnableSimApiAuth` | -| `/user/info` | POST | `EnableSimApiAuth`(需登录) | -| `/auth/login` | POST | `EnableCoceSdk` | -| `/user/groups` | POST | `EnableCoceSdk`(需登录) | -| `/auth/config` | GET | `EnableCoceSdk` | -| `/jobs` | GET | `EnableJob` | - ---- - -## 27. 注释规范 - -- **公有 API/方法**:XML 文档注释 -- **私有方法**:简单可不写;复杂逻辑写行内注释说**为什么** -- **不要废话注释** - -```csharp -/// -/// 根据邮箱查用户,不存在返回 null。 -/// -public Account? FindByEmail(string email) => db.Accounts.FirstOrDefault(x => x.Email == email); - -// ✅ 有意义的注释(解释原因) -// EF Core Find 优先命中一级缓存 -var user = db.Accounts.Find(id); - -// ❌ 废话注释 -// 查询用户 -var user = db.Accounts.Find(id); -``` - ---- - -## 28. 禁止事项 - -以下模式在使用 SimApi 框架时**明确禁止**: - -| ❌ 禁止 | ✅ 正确做法 | -|---------|------------| -| HTTP 4xx/5xx 表达业务错误 | HTTP 200 + JSON `code` 字段 | -| `throw new Exception(message)` | `ErrorWhen` 系列 或 `throw new SimApiException(code, msg)` | -| 新建 Service / Repository 层(除非项目明确需要) | Controller 直接操作 DbContext | -| 使用 `ActionResult` / `IActionResult` | 直接返回 Entity / void / SimApiBaseResponse | -| 鉴权 Attribute 加在方法上 | 加在 Controller **类** 上 | -| **手动判断 `LoginInfo.Type.Contains(...)` 做权限控制** | **一律用 `[SimApiAuth("role")]` Attribute** | -| **Controller 里写非路由的 private 辅助方法** | **抽到 `Helpers/` 独立类** | -| Entity 配导航属性 / EF Fluent API | 依赖 Convention 自动映射 | -| DbContext 中写 `OnModelCreating`(除非必要) | 只定义 DbSet | -| 花括号块命名空间 | 文件范围命名空间 | -| 传统构造函数注入 | 主构造函数 | -| `new List()` / `new string[] {}` | `[]` 集合表达式 | -| `Count() > 0` 判断存在 | `Any()` | -| `ToList()` 再转数组 | 直接 `ToArray()` | -| `string.IsNullOrEmpty` 判断必填入参 | `required` + 模型验证 | -| 全局 catch 吞异常 | 让异常冒泡到 SimApiExceptionMiddleware | -| `SimApiStorageOptions = Configuration.GetSection(...)` | `ConfigureSimApiStorage(s => {...})` | +| ❌ 禁止 | ✅ 正确 | +|---------|------------------------------------------------------| +| HTTP 4xx/5xx 表达业务错误 | HTTP 200 + JSON `code` | +| `throw new Exception(msg)` | `ErrorWhen` 或 `throw new SimApiException(code, msg)` | +| 鉴权 Attribute 只放方法 | 可以放 Controller **类**上 | +| 手动判断 `LoginInfo.Type.Contains(...)` | `[SimApiAuth("role")]` | +| Entity 配导航属性 / Fluent API | Convention 自动映射 | +| 花括号块命名空间 | 文件范围 `namespace X;` | +| 传统构造函数注入 | 主构造函数 | +| `new List()` / `new string[]{}` | `[]` 集合表达式 | +| `Count() > 0` | `Any()` | +| `ToList()` → 数组 | 直接 `ToArray()` | +| 全局 catch 吞异常 | 让异常冒泡到 SimApiExceptionMiddleware | diff --git a/SimApiExtensions.cs b/SimApiExtensions.cs index d4a33e4..aad2d5c 100644 --- a/SimApiExtensions.cs +++ b/SimApiExtensions.cs @@ -333,7 +333,7 @@ public static class SimApiExtensions { builder.AddSingleton(); builder.AddSingleton(); - if (simApiOptions.SimApiAuthGateOptions.UseIam) + if (simApiOptions.SimApiAuthCenterOptions.UseIam) { builder.AddSingleton(); } @@ -425,14 +425,14 @@ public static class SimApiExtensions if (options.EnableSimApiAuthGate) { logger.LogInformation("开始配置SimApiAuthGate..."); - if (string.IsNullOrEmpty(options.SimApiAuthGateOptions.AppId) || - string.IsNullOrEmpty(options.SimApiAuthGateOptions.AppKey)) + if (string.IsNullOrEmpty(options.SimApiAuthCenterOptions.AppId) || + string.IsNullOrEmpty(options.SimApiAuthCenterOptions.AppKey)) { logger.LogCritical("必须配置AuthGate的AppId和AppKey才能启用SimApiAuthGate"); } else { - if (options.SimApiAuthGateOptions.UseMiddleware) + if (options.SimApiAuthCenterOptions.UseMiddleware) { builder.UseMiddleware(); }