# SimApi AI Context > NuGet: `Simcu.SimApi` | .NET 8/9/10 | ASP.NET Core API 基础库 --- ## SETUP ```csharp // Program.cs builder.Services.AddSimApi(options => { ... }); var app = builder.Build(); app.UseSimApi(); app.Run(); ``` --- ## CORE 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` 字段 | | MQTT 用 RabbitMQ | **用 MQTTnet v5,通过 WebSocket 连接** | | `SimApiStorageOptions = Configuration.GetSection(...)` | 用 `options.ConfigureSimApiStorage(s => {...})` | --- ## SimApiOptions(AddSimApi 配置) ```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; options.EnableSimApiException = true; options.EnableSimApiResponseFilter = true; options.EnableForwardHeaders = true; options.EnableLowerUrl = true; options.EnableVersionUrl = true; options.EnableLogger = true; ``` --- ## 响应格式 所有接口统一输出: ```json { "code": 200, "message": "成功", "data": { ... } } ``` HTTP 状态码**始终 200**,错误信息在 `code` 字段。 ```csharp // 无数据 return new SimApiBaseResponse(); return new SimApiBaseResponse(400, "参数错误"); return new SimApiBaseResponse(404); // 自动映射消息 // 带数据 return new SimApiBaseResponse(data); // 分页 return new SimApiBaseResponse>>(new PageResponse> { List = items, Page = 1, Count = 20, Total = 100 }); ``` | 控制器返回值 | JSON 输出 | |-------------|----------| | `null` | `{"code":200,"message":"成功"}` | | 普通对象 | `{"code":200,"message":"成功","data":对象}` | | `SimApiBaseResponse` | 原样 | | `[OriginResponse]` 方法 | 完全不封装 | --- ## SimApiBaseController ```csharp [ApiController] [Route("[controller]")] public class XxxController : SimApiBaseController { } // 错误方法(protected static) Error(code, message) // 直接抛出,默认(500,"") ErrorWhen(condition, code, message) // condition==true 抛出,默认(400,"") ErrorWhenTrue(condition, code, message)// 同上别名 ErrorWhenFalse(condition, code, message)// condition==false 抛出,默认(400,"") ErrorWhenNull(obj, code, message) // obj==null 抛出,默认(404,"请求的资源不存在") // 当前登录信息(需 EnableSimApiAuth) SimApiLoginItem? loginInfo = LoginInfo; // 从 HttpContext.Items["LoginInfo"] 取 ``` --- ## Attributes ### [SimApiAuth] — 认证 ```csharp [SimApiAuth] // 仅检查登录 [SimApiAuth("admin")] // Type 包含 "admin" [SimApiAuth("admin,manager")] // 逗号分隔,OR 关系 ``` ### [SimApiDoc] — Swagger 注解 ```csharp [SimApiDoc("分组名", "接口名")] [SimApiDoc("分组名", "接口名", "接口描述")] [SimApiDoc(new[]{"tag1","tag2"}, "接口名")] ``` ### [SynapseEvent] — MQTT 事件处理 ```csharp [SynapseEvent("order/created")] public void OnOrderCreated(string eventName) { } [SynapseEvent("order/+/status")] // 支持 + 和 # 通配符 public void OnOrderStatus(string eventName, MyDto data) { } // 参数规则:0个、1个(string eventName)、2个(string eventName, T data) ``` ### [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) { } ``` ### [AesBody] — AES 解密请求体 ```csharp [HttpPost] public IActionResult Submit([AesBody(KeyProvider = typeof(MyAesKeyProvider))] MyRequest req) { } // 客户端提交: {"data": "Base64(AES-256-CBC 加密 JSON)"} ``` ### [OriginResponse] — 跳过响应封装 ```csharp [HttpGet][OriginResponse] public string GetRaw() => "raw string"; ``` ### [SimApiSign] — API 签名验证 ```csharp [SimApiSign(KeyProvider = typeof(MySignProvider))] public IActionResult SecureApi(...) { } // 签名算法:MD5(field1=v1&...&appId=xxx×tamp=ts&nonce=nnn&密钥) ``` --- ## Auth 配置 & SimApiAuth 服务 ```csharp // Program.cs options.EnableSimApiAuth = true; options.RedisConfiguration = "..."; // 必须 // Token 通过 Header 传入:Token: ``` ```csharp // SimApiLoginItem 结构 { Id: string, Type: string[], Meta: Dictionary, Extra: object? } // DI 注入使用 public MyController(SimApiAuth auth) { } string token = auth.Login(loginItem); // 自动生成 GUID token string token = auth.Login(loginItem, "custom-token"); auth.Update(loginItem, token); SimApiLoginItem? info = auth.GetLogin(token); auth.Logout(token); ``` 自动路由(`EnableSimApiAuth` 开启后): - `POST /auth/check` — 检测登录状态 - `POST /auth/logout` — 退出登录 - `POST /user/info` — 获取用户信息(需登录) --- ## 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! }); // 分组方式:控制器或方法加 [ApiExplorerSettings(GroupName = "admin")] // 不加则默认归入 Id="api" 的分组 ``` --- ## 对象存储(EnableSimApiStorage) ```csharp 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"; // 不能以 / 结尾 }); ``` ```csharp // DI 注入 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 客户端 ``` --- ## 任务调度(EnableJob) ```csharp 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 编号,null 用默认 job.Servers = [ new SimApiJobServerConfig { Queues = ["default"], WorkerNum = 5 }, // 默认 WorkerNum=5 new SimApiJobServerConfig { Queues = ["email"], WorkerNum = 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()); ``` --- ## MQTT 通信(EnableSynapse) ```csharp options.ConfigureSimApiSynapse(s => { s.Websocket = "ws://mqtt:8083/mqtt"; // WebSocket 连接 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; }); ``` Topic 规则: ``` 事件发布: {SysName}/event/{AppName}/{eventName} 事件订阅: {SysName}/event/{eventName}(或 $queue/... 启用负载均衡) RPC 请求: {SysName}/{targetApp}/rpc/server/{method} RPC 响应: {SysName}/{callerApp}/rpc/client/{AppId}/{messageId} 配置存储: {SysName}/synapse-config-store/{key}(Retain) ``` ```csharp // DI 注入 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 无效"); ``` 处理器类(含 `[SynapseRpc]`/`[SynapseEvent]` 的类无需手动注册,自动扫描为 Scoped): ```csharp public class OrderEventHandler { [SynapseEvent("order/+/status")] public void OnOrderStatus(string eventName, OrderStatusDto data) { } } public class UserRpcService { [SynapseRpc] // 注册为 "UserRpcService.GetUserInfo" public UserDto GetUserInfo(GetUserRequest req) { return ...; } [SynapseRpc("customName")] public ResultDto DoSomething(RequestDto req, Dictionary headers) { } } ``` --- ## API 签名验证([SimApiSign]) ```csharp // 1. 实现密钥提供者 public class MySignProvider : SimApiSignProviderBase { public override string? AppIdName { get; set; } = "appId"; public override string TimestampName { get; set; } = "timestamp"; public override string NonceName { get; set; } = "nonce"; public override string SignName { get; set; } = "sign"; public override int QueryExpires { get; set; } = 5; public override bool DuplicateRequestProtection { get; set; } = true; public override string[] SignFields { get; set; } = ["userId"]; // 额外签名字段 public override string? GetKey(string? appId) { // 根据 appId 返回密钥 return db.Apps.Find(appId)?.SecretKey; } } services.AddScoped(); // 注册 // 2. 使用 [SimApiSign(KeyProvider = typeof(MySignProvider))] public IActionResult SecureApi(...) { } ``` --- ## AES 加密传输([AesBody]) 算法:**AES-256-CBC + PKCS7**,IV 随机生成附在密文前,整体 Base64 编码。 ```csharp // 1. 实现密钥提供者 public class MyAesKeyProvider : AesBodyProviderBase { public override string? AppIdName { get; set; } = "appId"; // 从 Query/Header 取 public override string? GetKey(string? appId) => db.Apps.Find(appId)?.SecretKey; } services.AddScoped(); // 2. 使用(客户端提交 {"data": "Base64密文"}) [HttpPost] public IActionResult Submit([AesBody(KeyProvider = typeof(MyAesKeyProvider))] MyRequest req) { } // 工具类(静态,无需注入) string cipher = SimApiAesUtil.Encrypt("明文", "任意长度密钥"); // SHA256 处理为 32 字节 string plain = SimApiAesUtil.Decrypt(cipher, "任意长度密钥"); ``` --- ## 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"); // 反序列化 ``` --- ## HTTP 客户端(SimApiHttpClient) ```csharp var client = new SimApiHttpClient(appId: "myapp", appKey: "secret") { Server = "https://api.example.com", AppIdName = "appId", TimestampName = "timestamp", NonceName = "nonce", SignName = "sign", SignFields = ["field1"] }; var r = client.SignQuery("/api/user", body, queries); // 仅签名 var r = client.AesQuery("/api/user", body); // 仅 AES 加密 var r = client.AesSignQuery("/api/user", body, queries);// AES + 签名 ``` --- ## 工具类(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); ``` --- ## 数据模型基类(SimApiBaseModel) ```csharp public class UserEntity : SimApiBaseModel { public string Name { get; set; } // 自动字段:Id(GUID string)、CreatedAt、UpdatedAt } entity.MapData(dto); // 跳过 Id/CreatedAt/UpdatedAt entity.MapData(dto, mapAll: true); // 映射所有字段 entity.MapData(dto, new[]{"Name","Email"}); // 只映射指定字段 entity.UpdateTime(); // 手动更新 UpdatedAt // 注意:只映射同名+同类型+源值不为null 的属性 ``` --- ## Coce 统一身份(EnableCoceSdk) > 同时需要 `EnableSimApiAuth = true` ```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"; }); ``` ```csharp // DI 注入 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.ProxyQuery(uri, token, json) coce.ProxyQueue(uri, token, data) ``` 自动路由: - `POST /auth/login` — Coce 一键登录(前端传 `{"data":"lv1Token"}`) - `POST /user/groups` — 获取用户群组(需登录) - `GET /auth/config` — 获取 AppId 和授权 URL 自定义登录逻辑: ```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; } } services.AddScoped(); ``` --- ## 内置路由汇总 | 路由 | 方法 | 条件 | |------|------|------| | `/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` | --- ## 异常处理流程 ``` 请求进入 └─ SimApiExceptionMiddleware(捕获所有异常 → HTTP 200 + code 字段) └─ SimApiAuthMiddleware(解析 Token) └─ [SimApiSign] Filter └─ [SimApiAuth] Filter └─ OnActionExecuting(模型验证 → code 400) └─ Action 执行 └─ SimApiResponseFilter(封装响应) ```