# SimApi 完整 AI 编码参考 > **NuGet**: `Simcu.SimApi` | **目标框架**: `net8.0` / `net9.0` / `net10.0` > **作者**: xRain@SimcuTeam | **性质**: ASP.NET Core API 基础辅助库 > > **使用方式**: 将本文档作为上下文提供给 AI,或粘贴到对话开头。 --- ## 0. 快速开始 ```csharp // Program.cs — 两步启动 var builder = WebApplication.CreateBuilder(args); builder.Services.AddSimApi(options => { options.RedisConfiguration = "localhost:6379"; options.EnableSimApiAuth = true; options.EnableSimApiDoc = true; options.ConfigureSimApiDoc(doc => { doc.DocumentTitle = "我的API文档"; doc.ApiGroups = [ new("api", "公共接口"), new("admin", "管理接口", "需要管理员Token") ]; }); }); var app = builder.Build(); app.UseSimApi(); app.Run(); ``` --- ## 1. 核心概念(必读) ### 1.1 统一响应格式 **所有接口统一输出 JSON,HTTP 状态码始终 `200`,错误信息在 `code` 字段:** ```json { "code": 200, "message": "成功", "data": { ... } } ``` | 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(封装响应) ``` ### 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 ``` ### 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. 项目目录结构 推荐**极简扁平化**: ``` 项目名/ ├── Controllers/ # 控制器(含业务逻辑) │ └── Dtos/ # 请求/响应 DTO ├── Models/ # EF Core 实体 + DataContext ├── Helpers/ # 工具类 / 框架扩展点 ├── Migrations/ # EF Core 迁移(自动生成,勿手改) └── Program.cs # 入口 + DI + 中间件(无 Startup.cs) ``` 业务逻辑直接在 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; ``` > 业务项目可在根目录建 `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, "数据已存在"); ErrorWhenFalse(hasPermission, 403, "无权操作"); ``` --- ## 5. 控制器规范 ### 5.1 基类 所有业务控制器继承 `SimApiBaseController`: ```csharp using static SimApi.Helpers.SimApiError; [ApiController] [Route("[controller]")] public class UserController(DataContext db) : SimApiBaseController { // 获取当前登录用户(需 EnableSimApiAuth) protected SimApiLoginItem LoginInfo => (SimApiLoginItem)HttpContext.Items["LoginInfo"]!; } ``` `SimApiBaseController` 已内置: - `[Consumes("application/json")]` + `[Produces("application/json")]` - `OnActionExecuting` 自动验证 ModelState,无效时抛 `code 400` - `LoginInfo` 属性获取当前登录信息 ### 5.2 路由规则 **默认全部 POST**(除非在 `SupportedMethod` 显式添加): ```csharp // 方法上写完整路径 [HttpPost("/device/list")] [HttpPost("/application/refresh-key")] // 类上前缀 + 方法相对路径 [Route("/platform")] public class PlatformController(DataContext db) : SimApiBaseController { [HttpPost("device/detail")] // 最终路由:/platform/device/detail [HttpPost("bot/generate")] // 最终路由:/platform/bot/generate } ``` 路由路径全小写,多词用连字符 `-` 分隔。 ### 5.3 鉴权 Attribute | Attribute | 用途 | |-----------|------| | `[SimApiAuth]` | 要求登录 | | `[SimApiAuth("admin")]` | 要求 admin 角色 | | `[SimApiAuth("admin,manager")]` | OR 关系 | | `[SimApiSign(KeyProvider = typeof(Xxx))]` | 签名验证 | > 鉴权 Attribute 加在 **Controller 类** 上,不加在方法上。 ### 5.4 接口分组与文档 ```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, "无权操作此资源"); } } ``` --- ## 6. 响应格式详解 ### 6.1 结构 ```json { "code": 200, "message": "成功", "data": { ... } } ``` ### 6.2 构造方式 ```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 { List = devices, Page = 1, Count = 20, Total = 100 }; ``` ### 6.3 响应过滤器行为 | 控制器返回值 | 最终 JSON 输出 | |-------------|--------------| | `null` | `{"code":200,"message":"成功"}` | | 普通对象 | `{"code":200,"message":"成功","data":对象}` | | `SimApiBaseResponse` | 原样输出 | | `[OriginResponse]` 方法 | 完全不封装,原始输出 | --- ## 7. SimApiOptions 完整配置 通过 `AddSimApi(options => { ... })` 设置: ### 7.1 功能开关 ```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; // 彩色控制台日志 ``` ### 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 文档注解 ```csharp [SimApiDoc("分组名", "接口名称")] [SimApiDoc("分组名", "接口名称", "详细描述")] [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 签名验证 ```csharp [SimApiSign(KeyProvider = typeof(MySignProvider))] public IActionResult SecureApi(...) { } // 签名算法: MD5(field1=v1&...&appId=xxx×tamp=ts&nonce=nnn&密钥) ``` --- ## 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 配置 ```csharp options.ConfigureSimApiDoc(doc => { doc.DocumentTitle = "接口文档"; doc.ApiGroups = [ new("api", "公共接口"), new("admin", "管理接口", "描述可选") ]; doc.ApiAuth = new SimApiAuthOption { Type = ["SimApiAuth"] }; doc.SupportedMethod = [SubmitMethod.Post]; // 默认仅 Post!按需添加其他 }); // 分组方式 [ApiExplorerSettings(GroupName = "admin")] // 归入 admin 组 // 不标注则默认归入 "api" 组 ``` ### 10.2 访问 启动后访问 `/swagger`。 ### 10.3 自动 Swagger 过滤器(无需手动配置) | 过滤器 | 效果 | |--------|------| | `SimApiResponseOperationFilter` | 返回类型包装为 `SimApiBaseResponse` | | `SimApiAuthOperationFilter` | `[SimApiAuth]` 接口加 Token 认证要求 | | `SimApiSignOperationFilter` | `[SimApiSign]` 接口注入签名参数说明 | | `AesBodyOperationFilter` | `[AesBody]` 参数展示原始数据结构 | | `GlobalDynamicObjectSchemaFilter` | `object`/`Dictionary` 类型生成 Schema 示例 | | `RemoveEmptyTagsFilter` | 清除空分组 Tag | --- ## 11. 模块详解:对象存储(EnableSimApiStorage) 基于 MinIO SDK(S3 兼容)。 ### 11.1 配置 ```csharp options.EnableSimApiStorage = true; options.ConfigureSimApiStorage(s => { s.Endpoint = "http://minio:9000"; // 不能以 / 结尾 s.AccessKey = "admin"; s.SecretKey = "pass"; s.Bucket = "my-bucket"; s.ServeUrl = "http://cdn.example.com/my-bucket"; // 不能以 / 结尾 }); ``` ### 11.2 API ```csharp public MyController(SimApiStorage storage) { } // 路径必须以 / 开头 GetUploadUrlResponse r = storage.GetUploadUrl("/avatars/user1.jpg"); // r.UploadUrl → PUT 上传地址(前端直接用) // r.DownloadUrl → 公开访问 URL // r.Path → 相对路径 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→路径 IMinioClient mc = storage.Client; // 底层 MinIO 客户端 ``` --- ## 12. 模块详解:Redis 缓存(SimApiCache) > 依赖 `RedisConfiguration`,key 自动加前缀 `SimApi:Cache:` ```csharp public MyService(SimApiCache cache) { } 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"); // 删除 ``` --- ## 13. 模块详解:任务调度(EnableJob) Hangfire + Redis 存储。 ### 13.1 配置 ```csharp options.EnableJob = true; options.ConfigureSimApiJob(job => { job.DashboardUrl = "/jobs"; // null 则不开启 Dashboard job.DashboardAuthUser = "admin"; job.DashboardAuthPass = "Admin@123!"; job.RedisConfiguration = null; // null 则用全局 RedisConfiguration job.Database = 1; // Redis DB 编号 job.Servers = [ new SimApiJobServerConfig { Queues = ["default"], WorkerNum = 5 }, new SimApiJobServerConfig { 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()); // 依赖链 ``` Dashboard 访问 `/jobs`,Basic Auth 登录。 --- ## 14. 模块详解:MQTT 通信(EnableSynapse) 基于 MQTTnet v5,WebSocket 连接。 ### 14.1 配置 ```csharp options.EnableSynapse = true; 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; }); ``` ### 14.2 Topic 规则 | 用途 | Topic 格式 | |------|-----------| | 事件发布 | `{SysName}/event/{AppName}/{eventName}` | | 事件订阅(无负载均衡) | `{SysName}/event/{eventName}` | | 事件订阅(有负载均衡) | `$queue/{SysName}/event/{eventName}` | | RPC 请求 | `{SysName}/{targetApp}/rpc/server/{method}`($queue 天然负载均衡) | | RPC 响应 | `{SysName}/{callerApp}/rpc/client/{AppId}/{messageId}` | | 配置存储 | `{SysName}/synapse-config-store/{key}`(Retain 消息) | ### 14.3 Synapse API ```csharp public MyService(Synapse synapse) { } synapse.Event("order/created", new { OrderId = 1 }); // 发布事件 // 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 超时 // 分布式配置 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 无效"); ``` ### 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) { } } ``` --- ## 15. 模块详解:API 签名验证([SimApiSign]) ### 15.1 实现密钥提供者 ```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 构造函数与属性 ```csharp // 构造函数(必填参数) var client = new SimApiHttpClient( appId: "myapp", // 必填:应用 ID appKey: "secret" // 必填:应用密钥 // debug: false // 可选:是否打印请求/响应日志(默认 false) ); // 以下属性均有默认值,按需覆盖即可: 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"]; // 默认 [](空) ``` > **只有 `Server` 是必须设置的**,其他属性都有合理默认值。 ### 17.2 调用方法 ```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); ``` --- ## 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 实体基类,提供通用字段和轻量映射能力: ```csharp public class UserEntity : SimApiBaseModel { public string Name { get; set; } // 自动拥有: Id(GUID string)、CreatedAt、UpdatedAt } entity.MapData(dto); // 跳过 Id/CreatedAt/UpdatedAt,同名同类型非null属性 entity.MapData(dto, mapAll: true); // 映射所有字段 entity.MapData(dto, new[]{"Name"}); // 只映射指定字段 entity.UpdateTime(); // 手动更新 UpdatedAt // MapData 只映射: 同名 + 同类型 + 源值不为null ``` --- ## 21. 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` | 引用时用全限定名:`AdminDto.UserEditRequest`。 ### 21.3 属性规则 ```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; } } ``` --- ## 23. EF Core 查询风格 ```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.ConfigureSimApiDoc(doc => { doc.DocumentTitle = "接口文档"; doc.ApiGroups = [new("api", "公共接口"), new("admin", "管理接口")]; doc.SupportedMethod = [SubmitMethod.Post]; }); }); // 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"] ``` --- ## 26. 内置路由汇总 | 路由 | 方法 | 启用条件 | |------|------|----------| | `/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 => {...})` |