From 566b1d190bcbf70e2fbae7f448ca4e6cb3678dce Mon Sep 17 00:00:00 2001 From: xRain Date: Tue, 7 Apr 2026 10:55:46 +0800 Subject: [PATCH] add AiContext --- AICONTEXT.md | 574 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 574 insertions(+) create mode 100644 AICONTEXT.md diff --git a/AICONTEXT.md b/AICONTEXT.md new file mode 100644 index 0000000..8d3d4a1 --- /dev/null +++ b/AICONTEXT.md @@ -0,0 +1,574 @@ +# 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(封装响应) +```