Skip to content

simcu/simapi-net

Repository files navigation

SimApi 完整 AI 编码参考

NuGet: Simcu.SimApi | 目标框架: net8.0 / net9.0 / net10.0 作者: xRain@SimcuTeam | 性质: ASP.NET Core API 基础辅助库

使用方式: 将本文档作为上下文提供给 AI,或粘贴到对话开头。


0. 快速开始

// 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 字段:

{ "code": 200, "message": "成功", "data": { ... } }
code 含义
200 成功
401 需要登录

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<T>(...)
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 命名空间

使用文件范围命名空间

// ✅
namespace MyApp.Controllers;

// ❌
namespace MyApp.Controllers { }

2.3 主构造函数(依赖注入)

// ✅ 主构造函数
public class OrderController(DataContext db) : SimApiBaseController { }

// ❌ 传统构造函数
public class OrderController : SimApiBaseController {
    private readonly DataContext _db;
    public OrderController(DataContext db) { _db = db; }
}

2.4 集合表达式

优先 []

string[] tags = [];                          // ✅
string[] roles = ["admin", "manager"];       // ✅
// var tags = new string[] { };               // ❌

2.5 Null 处理

var key = app?.Key;                         // 安全访问
var name = user?.Name ?? "匿名";            // 空合并
config ??= new Dictionary<string, string>(); // 空合并赋值

2.6 命名规范

类型 规则 示例
类、接口、枚举 PascalCase AdminControllerResPermission
方法名 PascalCase UserListApplicationEdit
属性名 PascalCase AccountIdLicenseTotal
私有字段 _camelCase(如有) _logger
局部变量、参数 camelCase var uservar appId
常量 PascalCase MaxRetryCountDefaultRole
路由路径 全小写 + 连字符 /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; 即可直接调用:

using static SimApi.Helpers.SimApiError;

业务项目可在根目录建 GlobalUsings.csglobal using static SimApi.Helpers.SimApiError; 实现全项目免 import。

4.2 方法签名

// 直接抛出错误
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<T>([NotNull] T? condition, int code = 404,
    string message = "请求的资源不存在") where T : class;

所有方法最终都 throw new SimApiException(code, message),由全局中间件捕获转为 {code, message} 响应。

4.3 使用示例

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

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 显式添加):

// 方法上写完整路径
[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 接口分组与文档

// 参数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 AccountDevice
列表查询 Entity[] 数组 不用 List<T>
分页查询 PageResponse<Entity[]> 含 Total/Page/Count/List
有状态响应 SimApiBaseResponse 自定义 code+message
复杂组合 对应 DTO 自定义结构

不使用 ActionResult<T>IActionResult(除非用了 [AesBody] 等框架 Attribute)。

5.6 参数规范

// 普通请求体
[FromBody] DeviceDto.SerialAddRequest request

// AES 加密请求体
[AesBody(KeyProvider = typeof(AesBodyProvider))] BotDto.BotChatRequest request

// 查询参数(直接写,不加 Attribute)
string appId

5.7 当前登录信息

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/ 目录下的独立类中。

// ✅ 控制器保持简洁,只有路由方法
[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 结构

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

6.2 构造方式

// 无数据
return new SimApiBaseResponse();                    // {"code":200,"message":"成功"}
return new SimApiBaseResponse(400, "参数错误");     // {"code":400,"message":"参数错误"}
return new SimApiBaseResponse(404);                // {"code":404,"message":"接口不存在"}

// 带数据
return new SimApiBaseResponse<User>(user);         // data = user

// 分页
return new PageResponse<Device[]>
{
    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 功能开关

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 子模块配置

options.ConfigureSimApiDoc(doc => { ... });        // Swagger
options.ConfigureSimApiStorage(s => { ... });      // S3 存储
options.ConfigureSimApiJob(job => { ... });        // 任务调度
options.ConfigureSimApiSynapse(s => { ... });      // MQTT
options.ConfigureCoceSdk(coce => { ... });         // Coce 身份

8. Attributes 完整参考

8.1 [SimApiAuth] — 身份认证

[SimApiAuth]                     // 仅检查登录
[SimApiAuth("admin")]            // Type 包含 "admin"
[SimApiAuth("admin,manager")]    // 逗号分隔 OR 关系
[SimApiAuth(new[]{"a", "b"})]    // 数组形式

8.2 [SimApiDoc] — Swagger 文档注解

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

8.3 [SynapseEvent] — MQTT 事件处理

// 参数规则: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 方法

[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) { }

8.5 [AesBody] — AES 解密请求体

[HttpPost]
public IActionResult Submit([AesBody(KeyProvider = typeof(MyAesKeyProvider))] MyRequest req)
{ /* request 已自动解密 */ }

// 客户端提交: {"data": "Base64(AES-256-CBC加密JSON)"}

8.6 [OriginResponse] — 跳过响应封装

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

8.7 [SimApiSign] — API 签名验证

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

9. 模块详解:认证系统(SimApiAuth)

9.1 配置

options.EnableSimApiAuth = true;   // 必须同时配置 RedisConfiguration
// Token 通过 Header 传入:Token: <value>

9.2 DI 注入与 API

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 结构

{ Id: string, Type: string[], Meta: Dictionary<string,string>, 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 配置

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<T>
SimApiAuthOperationFilter [SimApiAuth] 接口加 Token 认证要求
SimApiSignOperationFilter [SimApiSign] 接口注入签名参数说明
AesBodyOperationFilter [AesBody] 参数展示原始数据结构
GlobalDynamicObjectSchemaFilter object/Dictionary 类型生成 Schema 示例
RemoveEmptyTagsFilter 清除空分组 Tag

11. 模块详解:对象存储(EnableSimApiStorage)

基于 MinIO SDK(S3 兼容)。

11.1 配置

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

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:

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");                                  // 反序列化
cache.Remove("key");                                                  // 删除

13. 模块详解:任务调度(EnableJob)

Hangfire + Redis 存储。

13.1 配置

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 使用

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

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

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 无效");

14.4 处理器注册

[SynapseRpc]/[SynapseEvent] 的类会被自动扫描注册为 Scoped 服务,无需手动注册:

// 事件处理器
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<string, string> headers) { }
}

15. 模块详解:API 签名验证([SimApiSign])

15.1 实现密钥提供者

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<MyDbContext>();
        return db.Apps.Find(appId)?.SecretKey;
    }
}

builder.Services.AddScoped<MySignProvider>();

15.2 使用

[SimApiSign(KeyProvider = typeof(MySignProvider))]
public IActionResult SecureApi(...) { }

// 签名算法: MD5(field1=v1&field2=v2&...&appId=xxx&timestamp=ts&nonce=nnn&密钥)
// 支持通过 Query 或 Header 传入签名参数

16. 模块详解:AES 加密传输([AesBody])

算法:AES-256-CBC + PKCS7,IV 随机生成附在密文前,整体 Base64 编码。

16.1 实现密钥提供者

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<MyDbContext>();
        return db.Apps.Find(appId)?.SecretKey;
    }
}
builder.Services.AddScoped<MyAesKeyProvider>();

16.2 使用

[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 构造函数与属性

// 构造函数(必填参数)
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 调用方法

// 仅签名(自动计算 MD5 签名附加到 Query/Header)
var r = client.SignQuery<T>("/api/user", body, queries);

// 仅 AES 加密(body 自动加密为 {"data":"Base64密文"})
var r = client.AesQuery<T>("/api/user", body);

// AES 加密 + 签名
var r = client.AesSignQuery<T>("/api/user", body, queries);

18. 模块详解:Coce 统一身份平台(EnableCoceSdk)

同时需要 EnableSimApiAuth = true

18.1 配置

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

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.ProxyQueue<T>(uri, token, data)

18.3 自动路由

路由 方法 说明
POST /auth/login 无需登录 Coce 一键登录(前端传 {"data":"lv1Token"}
POST /user/groups 需登录 获取群组列表
GET /auth/config 无需登录 获取 AppId 和授权 URL

18.4 自定义登录处理器

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<ICoceLoginProcessor, MyLoginProcessor>();

19. 工具类(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);

20. 数据模型基类(SimApiBaseModel)

ORM 实体基类,提供通用字段和轻量映射能力:

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/ 下,嵌套容器类:

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 UserEditRequestDeviceSerialAddRequest
响应 DTO [动作]Response GenerateResponseTokenResponse
数据载体 [含义]Data / [含义]Item GenerateDataAgentItem

引用时用全限定名:AdminDto.UserEditRequest

21.3 属性规则

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<T> 只有 Data 字段
SimApiBasePageRequest 分页请求基类(Page + Count)
SimApiBaseResponse 通用响应(可带 code + message)
SimApiBaseResponse<T> 带数据的响应
PageResponse<T> 分页响应(Total + Page + Count + List)

22. Entity 与 DataContext 规范

22.1 Entity

所有实体继承 SimApiBaseModel

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,如 AccountIdAppId
  • 不配导航属性不写 Fluent API,依赖 Convention 自动映射

22.2 DataContext

只定义 DbSet,不做任何配置:

public class DataContext(DbContextOptions<DataContext> options) : DbContext(options)
{
    public required DbSet<Account> Accounts { get; set; }
    public required DbSet<Application> Applications { get; set; }
}

23. EF Core 查询风格

// 列表查询(排序 + 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 完整模板

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<DataContext>(opt =>
    opt.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

// 3. 框架扩展点(接口注册)
builder.Services.AddScoped<AesBodyProviderBase, AesBodyProvider>();
builder.Services.AddScoped<SimApiSignProviderBase, SimApiSignProvider>();

// 4. 项目自定义服务
builder.Services.AddScoped<ResPermission>();
builder.Services.AddSingleton<JsonSchemaHelper>();

var app = builder.Build();

// 5. 启动时自动迁移
app.Services.CreateScope().ServiceProvider
    .GetRequiredService<DataContext>().Database.Migrate();

// 6. 框架中间件
app.UseSimApi();
app.Run();

25. 配置文件规范

appsettings.json 只保留框架默认值:

{
  "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } },
  "AllowedHosts": "*"
}

appsettings.Development.json 存放开发环境实际配置(不提交 Git):

{
  "ConnectionStrings": {
    "Default": "Host=...;Database=...;Username=...;Password=...",
    "Redis": "host:port,defaultDatabase=N"
  },
  "Sms": { "Account": "...", "Password": "..." }
}

读取方式:

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 文档注释
  • 私有方法:简单可不写;复杂逻辑写行内注释说为什么
  • 不要废话注释
/// <summary>
/// 根据邮箱查用户,不存在返回 null。
/// </summary>
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<T> / 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<T>() / new string[] {} [] 集合表达式
Count() > 0 判断存在 Any()
ToList() 再转数组 直接 ToArray()
string.IsNullOrEmpty 判断必填入参 required + 模型验证
全局 catch 吞异常 让异常冒泡到 SimApiExceptionMiddleware
SimApiStorageOptions = Configuration.GetSection(...) ConfigureSimApiStorage(s => {...})

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages