Files
simapi-net/AICONTEXT.md
T
2026-04-07 10:55:46 +08:00

17 KiB
Raw Blame History

SimApi AI Context

NuGet: Simcu.SimApi | .NET 8/9/10 | ASP.NET Core API 基础库


SETUP

// 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 配置)

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;

响应格式

所有接口统一输出:

{ "code": 200, "message": "成功", "data": { ... } }

HTTP 状态码始终 200,错误信息在 code 字段。

// 无数据
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

[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] — 认证

[SimApiAuth]                     // 仅检查登录
[SimApiAuth("admin")]            // Type 包含 "admin"
[SimApiAuth("admin,manager")]    // 逗号分隔,OR 关系

[SimApiDoc] — Swagger 注解

[SimApiDoc("分组名", "接口名")]
[SimApiDoc("分组名", "接口名", "接口描述")]
[SimApiDoc(new[]{"tag1","tag2"}, "接口名")]

[SynapseEvent] — MQTT 事件处理

[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 方法

[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 解密请求体

[HttpPost]
public IActionResult Submit([AesBody(KeyProvider = typeof(MyAesKeyProvider))] MyRequest req) { }
// 客户端提交: {"data": "Base64(AES-256-CBC 加密 JSON)"}

[OriginResponse] — 跳过响应封装

[HttpGet][OriginResponse]
public string GetRaw() => "raw string";

[SimApiSign] — API 签名验证

[SimApiSign(KeyProvider = typeof(MySignProvider))]
public IActionResult SecureApi(...) { }
// 签名算法:MD5(field1=v1&...&appId=xxx&timestamp=ts&nonce=nnn&密钥)

Auth 配置 & SimApiAuth 服务

// Program.cs
options.EnableSimApiAuth = true;
options.RedisConfiguration = "..."; // 必须

// Token 通过 Header 传入:Token: <value>
// 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

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

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"; // 不能以 / 结尾
});
// 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

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 }
    ];
});
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

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
// 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):

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]

// 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 编码。

// 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

依赖 RedisConfigurationkey 自动加前缀 SimApi:Cache:

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

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,全部静态)

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

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

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";
});
// 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

自定义登录逻辑:

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(封装响应)