add AiContext

This commit is contained in:
2026-04-07 10:55:46 +08:00
parent eb8c9732bd
commit 566b1d190b
+574
View File
@@ -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<T>(...)` |
| HTTP 状态码 4xx/5xx 表示错误 | **所有错误均 HTTP 200**,错误在 JSON `code` 字段 |
| MQTT 用 RabbitMQ | **用 MQTTnet v5,通过 WebSocket 连接** |
| `SimApiStorageOptions = Configuration.GetSection(...)` | 用 `options.ConfigureSimApiStorage(s => {...})` |
---
## SimApiOptionsAddSimApi 配置)
```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<T>(data);
// 分页
return new SimApiBaseResponse<PageResponse<List<T>>>(new PageResponse<List<T>>
{
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<string,string>headers
public UserDto GetUserInfo(GetUserRequest req) { }
public UserDto GetUserInfo(GetUserRequest req, Dictionary<string, string> 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&timestamp=ts&nonce=nnn&密钥)
```
---
## Auth 配置 & SimApiAuth 服务
```csharp
// Program.cs
options.EnableSimApiAuth = true;
options.RedisConfiguration = "..."; // 必须
// Token 通过 Header 传入:Token: <value>
```
```csharp
// SimApiLoginItem 结构
{ Id: string, Type: string[], Meta: Dictionary<string,string>, 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 → 公开访问 URLr.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<T>
var res = synapse.Rpc<UserDto>("user-service", "GetUserInfo", new { Id = 1 });
var res = synapse.Rpc<UserDto>("user-service", "GetUserInfo", param,
headers: new Dictionary<string, string> { { "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<string, string> 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<MySignProvider>(); // 注册
// 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<MyAesKeyProvider>();
// 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<int>("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<T>("/api/user", body, queries); // 仅签名
var r = client.AesQuery<T>("/api/user", body); // 仅 AES 加密
var r = client.AesSignQuery<T>("/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<T>(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<T>(uri, token)
coce.ProxyQuery<T>(uri, token, json)
coce.ProxyQueue<T>(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<ICoceLoginProcessor, MyLoginProcessor>();
```
---
## 内置路由汇总
| 路由 | 方法 | 条件 |
|------|------|------|
| `/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(封装响应)
```