Compare commits

..
37 Commits
Author SHA1 Message Date
xrain e53048a980 update option 2024-12-23 22:11:00 +08:00
xrain bbc2806995 add origin response attribute 2024-12-23 20:34:38 +08:00
xrain 404a638d5e add filter 2024-12-23 20:26:21 +08:00
xrain e27d5e1c77 fix data 2024-09-08 14:42:51 +08:00
xrain 6af68da3eb timeout rpc supported 2024-09-05 15:06:44 +08:00
xrain 7dc2ac6a86 add headers 2024-09-05 12:59:57 +08:00
xrain 8f6b853b6c fix rpc ex , add RpcError Method 2024-08-25 00:11:00 +08:00
xrain e876486c00 fix rpc ex , add RpcError Method 2024-08-25 00:10:00 +08:00
xrain e71226be4a fix rpc ex , add RpcError Method 2024-08-25 00:08:06 +08:00
xrain bd674dcc31 add cocesdk 2024-08-19 03:39:35 +08:00
xrain 141846d79d 1. RpcServer can return not found , 2. Event server process all parllels 2024-08-05 19:55:14 +08:00
xrain a4bae0226e fix bug 2024-08-03 19:37:40 +08:00
xrain 15f74b6570 fix warning 2024-08-03 19:21:23 +08:00
xrain ac4619a789 fix warning 2024-08-03 19:19:42 +08:00
xrain 4d0f801f21 now mqtt reconnect will resub topic 2024-08-03 18:35:01 +08:00
xrain 683cf9176a add event loadbalancing 2024-07-27 01:33:17 +08:00
xrain 1ee8d941e9 fix eventserver bug 2024-07-26 19:18:59 +08:00
xrain 1d474f596a config fix 2024-07-26 08:56:01 +08:00
xrain 929ddaced3 add config store 2024-07-26 08:11:21 +08:00
xrain 82f7e0fc00 use Task insteadof while improve performance 2024-07-26 05:20:27 +08:00
xrain f4bc6dc080 fix synapse use mqtt 2024-07-26 05:03:28 +08:00
xrain 56b0490d1e fix log color 2024-07-24 21:50:46 +08:00
xrain 45e19c3561 fix log color 2024-07-24 21:43:31 +08:00
xrain 04b2be1bb7 fix bug 2024-07-21 13:50:12 +08:00
xrain f78c3a0e0d fix fullurl 不抛出异常了 2024-07-10 06:35:19 +08:00
xrain 506704fd90 fix fullurl 不抛出异常了 2024-07-10 06:29:36 +08:00
xrain 786051ddbf update pkg 2024-07-10 03:30:09 +08:00
xrain d9ff4ae422 fix upload url 2024-07-10 01:18:18 +08:00
xrain 986b4e74e6 new upload 2024-07-10 00:52:51 +08:00
xrain 2257ba5b2a new feature 2024-06-27 04:38:04 +08:00
xrain c40f8d664d default exception edit 2024-04-23 12:27:36 +08:00
xrain a1f10b9c09 class 添加默认方法,支持老的代码 2024-04-16 06:57:56 +08:00
xrain 6ef7aca8ae 将record切换回class, 不再使用CST时间,使用DateTime.Now 2024-04-16 06:29:21 +08:00
xrain 4171756c54 新增 ErrorWhenTrue和ErrorWhenFalse 2024-03-26 10:06:17 +08:00
xrain c68cfa6192 fix simapiauth to singleton 2024-03-24 17:40:48 +08:00
xrain 8344567ac6 新增了一个获取登陆信息的方法 2024-03-24 17:31:22 +08:00
xrain 99b26d99f6 fix code 2024-03-23 07:30:24 +08:00
34 changed files with 1335 additions and 485 deletions
+8
View File
@@ -0,0 +1,8 @@
using System;
namespace SimApi.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public class OriginResponseAttribute : Attribute
{
}
+1 -1
View File
@@ -43,7 +43,7 @@ public class SimApiAuthAttribute : ActionFilterAttribute
public override void OnActionExecuting(ActionExecutingContext context)
{
var loginInfo = (SimApiLoginItem)context.HttpContext.Items["LoginInfo"];
var loginInfo = (SimApiLoginItem)context.HttpContext.Items["LoginInfo"]!;
//检测是否登录
if (loginInfo == null)
{
+2 -7
View File
@@ -3,12 +3,7 @@ using System;
namespace SimApi.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public class SynapseEventAttribute : Attribute
public class SynapseEventAttribute(string? name = null) : Attribute
{
public string Name { get; }
public SynapseEventAttribute(string name)
{
Name = name;
}
public string? Name { get; } = name;
}
+1 -1
View File
@@ -5,7 +5,7 @@ namespace SimApi.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public class SynapseRpcAttribute : Attribute
{
public string Name { get; }
public string? Name { get; }
public SynapseRpcAttribute()
{
+235
View File
@@ -0,0 +1,235 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using SimApi.Communications;
using SimApi.Configurations;
using SimApi.Helpers;
namespace SimApi.CoceSdk;
public class CoceApp(SimApiOptions simApiOptions, ILogger<CoceApp> logger, IDistributedCache cache)
{
/// <summary>
/// 获取Level Token
/// </summary>
/// <param name="lv1Token"></param>
/// <param name="level"></param>
/// <returns></returns>
public LevelTokenResponse? GetLevelToken(string lv1Token, int level = 5)
{
var dict = new Dictionary<string, object>
{
{ "lv1Token", lv1Token },
{ "time", (int)SimApiUtil.TimestampNow },
{ "appId", simApiOptions.CoceSdkOptions.AppId! },
{ "level", level }
};
return QueryAppApi<LevelTokenResponse>("/api/app/token", dict);
}
/// <summary>
/// 通过用户手机号搜索用户
/// </summary>
/// <param name="phone"></param>
/// <returns></returns>
public UserInfo? SearchUserByPhone(string phone)
{
var dict = new Dictionary<string, object>
{
{ "cell", phone },
{ "time", (int)SimApiUtil.TimestampNow },
{ "appId", simApiOptions.CoceSdkOptions.AppId! },
};
return QueryAppApi<UserInfo>("/api/app/user/search-by-phone", dict);
}
/// <summary>
/// 通过给出的UserId列表获取用户信息
/// </summary>
/// <param name="userIds"></param>
/// <returns></returns>
public UserInfo[]? SearchUserByIds(IEnumerable<string> userIds)
{
var dict = new Dictionary<string, object>
{
{ "ids", string.Join(",", userIds) },
{ "time", (int)SimApiUtil.TimestampNow },
{ "appId", simApiOptions.CoceSdkOptions.AppId! },
};
return QueryAppApi<UserInfo[]>("/api/app/user/search-by-ids", dict);
}
/// <summary>
/// 向用户发送消息
/// </summary>
/// <param name="userId"></param>
/// <param name="title"></param>
/// <param name="content"></param>
/// <returns></returns>
public bool SendUserMessage(string userId, string title, string content)
{
var dict = new Dictionary<string, object>
{
{ "time", (int)SimApiUtil.TimestampNow },
{ "appId", simApiOptions.CoceSdkOptions.AppId! },
{ "userId", userId },
{ "type", "text" },
{ "title", title },
{ "text", content }
};
return QueryAppApiNoResp("/api/app/message", dict);
}
/// <summary>
/// 创建交易订单号
/// </summary>
/// <param name="name"></param>
/// <param name="amount"></param>
/// <param name="ext"></param>
/// <returns></returns>
public string? TradeCreate(string name, int amount, string ext)
{
var dict = new Dictionary<string, object>
{
{ "time", (int)SimApiUtil.TimestampNow },
{ "appId", simApiOptions.CoceSdkOptions.AppId! },
{ "amount", amount },
{ "ext", ext },
{ "name", name }
};
return QueryAppApi<string>("/api/app/trade/create", dict);
}
/// <summary>
/// 查询订单状态
/// </summary>
/// <param name="tradeNo"></param>
/// <returns></returns>
public CheckTradeResponse? TradeCheck(string tradeNo)
{
var dict = new Dictionary<string, object>
{
{ "time", (int)SimApiUtil.TimestampNow },
{ "appId", simApiOptions.CoceSdkOptions.AppId! },
{ "tradeNo", tradeNo }
};
return QueryAppApi<CheckTradeResponse>("/api/app/trade/result", dict);
}
/// <summary>
/// 对订单进行退款
/// </summary>
/// <param name="tradeNo"></param>
/// <returns></returns>
public bool TradeRefund(string tradeNo)
{
var dict = new Dictionary<string, object>
{
{ "time", (int)SimApiUtil.TimestampNow },
{ "appId", simApiOptions.CoceSdkOptions.AppId! },
{ "tradeNo", tradeNo }
};
return QueryAppApiNoResp("/api/app/trade/refund", dict);
}
private T? QueryAppApi<T>(string endpoint, Dictionary<string, object> request)
{
var response = QueryAppApi(endpoint, request);
var result = response.Content.ReadFromJsonAsync<SimApiBaseResponse<T>>().Result!;
if (result.Code == 200) return result.Data;
logger.LogDebug("发生错误: {Code} => {Message}", result.Code, result.Message);
return default;
}
private bool QueryAppApiNoResp(string endpoint, Dictionary<string, object> request)
{
var response = QueryAppApi(endpoint, request);
var result = response.Content.ReadFromJsonAsync<SimApiBaseResponse>().Result!;
return result.Code == 200;
}
private HttpResponseMessage QueryAppApi(string endpoint, Dictionary<string, object> request)
{
var platUrl = simApiOptions.CoceSdkOptions.ApiEndpoint + endpoint;
var sorted = request.OrderBy(x => x.Key);
var signStr = sorted.Aggregate("", (current, item) => current + $"{item.Key}={item.Value}&").TrimEnd('&');
logger.LogDebug("签名的字符串: {SignStr}", signStr);
var sign = SimApiUtil.Md5(signStr + simApiOptions.CoceSdkOptions.AppKey);
logger.LogDebug("签名: {Sign}", sign);
request.Add("sign", sign);
logger.LogDebug("请求地址: {PlatUrl} => {Data}", platUrl, JsonSerializer.Serialize(request));
var http = new HttpClient();
return http.PostAsJsonAsync(platUrl, request).Result;
}
/// <summary>
/// 获取用户的群组信息
/// </summary>
/// <param name="token">Level >=2 的Token</param>
/// <returns></returns>
public IEnumerable<GroupInfo>? GetUserGroups(string token)
{
const string uri = "/api/lv2/user/groups";
var resp = ProxyQuery<GroupInfo[]>(uri, token,"{}");
return resp;
}
/// <summary>
/// 获取用户信息
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public UserInfo? GetUserInfo(string token)
{
const string uri = "/api/lv1/user/info";
return ProxyQuery<UserInfo>(uri, token);
}
public void SaveToken(string userId, string levelToken)
{
cache.SetString($"LEVEL:TOKEN:{userId}", levelToken);
}
public string? GetToken(string userId)
{
return cache.GetString($"LEVEL:TOKEN:{userId}");
}
public dynamic? ProxyQuery(string uri, string token, string json) => ProxyQuery<dynamic>(uri, token, json);
public dynamic? ProxyQueue(string uri, string token, object data) =>
ProxyQuery<dynamic>(uri, token, JsonSerializer.Serialize(data));
public T? ProxyQueue<T>(string uri, string token, object data) =>
ProxyQuery<T>(uri, token, JsonSerializer.Serialize(data));
public T? ProxyQuery<T>(string uri, string token, string json = "{}")
{
var http = new HttpClient();
http.DefaultRequestHeaders.Add("Token", token);
var realUrl = simApiOptions.CoceSdkOptions.ApiEndpoint + uri;
var response = http.PostAsync(realUrl, new StringContent(json, Encoding.UTF8, "application/json"))
.Result;
var resp = response.Content.ReadFromJsonAsync<SimApiBaseResponse<T>>().Result!;
if (resp.Code != 200)
{
logger.LogDebug("请求发生错误: {RespCode} => {RespMessage}", resp.Code, resp.Message);
}
return resp.Code == 200 ? resp.Data : default;
}
public ConfigResponse GetConfig()
{
return new ConfigResponse(simApiOptions.CoceSdkOptions.AppId!, simApiOptions.CoceSdkOptions.AuthEndpoint);
}
}
+15
View File
@@ -0,0 +1,15 @@
namespace SimApi.CoceSdk;
public class CoceAppSdkOption
{
/**
* 中心API服务器地址
*/
public string ApiEndpoint { get; set; } = "https://api.coce.cc";
public string AuthEndpoint { get; set; } = "https://home.coce.cc";
public string? AppId { get; set; }
public string? AppKey { get; set; }
}
+43
View File
@@ -0,0 +1,43 @@
using System;
namespace SimApi.CoceSdk;
public record ConfigResponse(string AppId, string AuthUrl);
public record LevelTokenResponse(string Token, string UserId, int TokenLevel);
public record GroupInfo(string Id, string Name, string Image, string Description, string Role);
public record UserInfo(string UserId, string Name, string Image);
public record CheckTradeResponse(
string TradeNo,
int Amount,
int Fee,
string Name,
string? Ext,
string Status,
DateTime CreatedAt,
DateTime? FinishedAt,
DateTime? RefundAt,
DateTime? CloseAt);
public class UserInfoWithGroup
{
public string? UserId { get; set; }
public string? Name { get; set; }
public string? LevelToken { get; set; }
public UserGroupItem[]? UserGroupItems { get; set; }
}
public class UserGroupItem
{
public string? GroupId { get; set; }
public string? GroupName { get; set; }
public string? GroupRole { get; set; }
}
+20 -5
View File
@@ -1,22 +1,37 @@
namespace SimApi.Communications;
using System.ComponentModel.DataAnnotations;
namespace SimApi.Communications;
/// <summary>
/// 只有ID的请求
/// </summary>
public record SimApiIdOnlyRequest(int Id);
public class SimApiIdOnlyRequest
{
[Required] public int Id { get; set; }
}
/// <summary>
/// 只有ID的请求(字符串)
/// </summary>
public record SimApiStringIdOnlyRequest(string Id);
public class SimApiStringIdOnlyRequest
{
[Required] public string? Id { get; set; }
}
/// <summary>
/// 动态类型单字段请求
/// </summary>
/// <typeparam name="T"></typeparam>
public record SimApiOneFieldRequest<T>(T Data);
public class SimApiOneFieldRequest<T>
{
[Required] public T? Data { get; set; }
}
/// <summary>
/// 基础分页请求
/// </summary>
public record SimApiBasePageRequest(int Page, int Count);
public class SimApiBasePageRequest
{
[Required] public int Page { get; set; }
[Required] public int Count { get; set; }
}
+31 -20
View File
@@ -1,14 +1,18 @@
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using SimApi.Helpers;
namespace SimApi.Communications;
/// <summary>
/// 基础相应
/// </summary>
public record SimApiBaseResponse(int Code = 200, string Message = "成功")
public class SimApiBaseResponse(int code = 200, string message = "成功")
{
public int Code { get; set; } = code;
public string Message { get; set; } = message;
/// <summary>
/// 默认错误代码对应提示信息
/// </summary>
@@ -23,7 +27,7 @@ public record SimApiBaseResponse(int Code = 200, string Message = "成功")
{ 500, "服务器错误" }
};
public SimApiBaseResponse(int code) : this(code, MsgBox.ContainsKey(code) ? MsgBox[code] : "未知错误")
public SimApiBaseResponse(int code) : this(code, MsgBox.GetValueOrDefault(code, "未知错误"))
{
}
@@ -33,15 +37,7 @@ public record SimApiBaseResponse(int Code = 200, string Message = "成功")
/// <returns></returns>
public override string ToString()
{
return JsonSerializer.Serialize(this, new JsonSerializerOptions
{
IgnoreReadOnlyProperties = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters =
{
new JsonStringEnumConverter()
}
});
return SimApiUtil.Json(this);
}
}
@@ -49,17 +45,32 @@ public record SimApiBaseResponse(int Code = 200, string Message = "成功")
/// 动态内容分页
/// </summary>
/// <typeparam name="T"></typeparam>
public record SimApiBasePageResponse<T>(
T List,
int Page = 1,
int Count = 1,
int Total = 1,
int Code = 200,
string Message = "成功") : SimApiBaseResponse(Code, Message);
public class SimApiBasePageResponse<T>() : SimApiBaseResponse
{
public T? List { get; set; }
public int Page { get; set; } = 1;
public int Count { get; set; } = 20;
public int Total { get; set; }
public SimApiBasePageResponse(T list, int page, int count, int total) : this()
{
List = list;
Page = page;
Count = count;
Total = total;
}
}
/// <summary>
/// 动态Data返回
/// </summary>
/// <typeparam name="T"></typeparam>
public record SimApiBaseResponse<T>(T Data, int Code = 200, string Message = "成功") : SimApiBaseResponse(Code,
Message);
public class SimApiBaseResponse<T>() : SimApiBaseResponse
{
public T? Data { get; set; }
public SimApiBaseResponse(T data) : this()
{
Data = data;
}
}
+10 -2
View File
@@ -1,6 +1,14 @@
namespace SimApi.Communications;
using System.Collections.Generic;
namespace SimApi.Communications;
/// <summary>
/// 登录信息中间件
/// </summary>
public record SimApiLoginItem(string Id, string[] Type);
public class SimApiLoginItem
{
public string? Id { get; set; }
public string[] Type { get; set; } = new[] { "user" };
public Dictionary<string, string>? Meta { get; set; } = null;
public object? Extra { get; set; } = null;
};
+7 -7
View File
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using Swashbuckle.AspNetCore.SwaggerUI;
namespace SimApi.Configs;
namespace SimApi.Configurations;
/// <summary>
/// 文档组配置
@@ -11,17 +11,17 @@ public class SimApiDocGroupOption
/// <summary>
/// 文档标识
/// </summary>
public string Id { get; set; }
public string Id { get; set; } = null!;
/// <summary>
/// 文档名称
/// </summary>
public string Name { get; set; }
public string Name { get; set; } = null!;
/// <summary>
/// 文档描述
/// </summary>
public string Description { get; set; }
public string Description { get; set; } = null!;
}
/// <summary>
@@ -33,11 +33,11 @@ public class SimApiAuthOption
public string Description { get; set; } = "认证服务器颁发的AccessToken";
public string AuthorizationUrl { get; set; }
public string AuthorizationUrl { get; set; } = null!;
public string TokenUrl { get; set; }
public string TokenUrl { get; set; } = null!;
public Dictionary<string, string> Scopes { get; set; }
public Dictionary<string, string> Scopes { get; set; } = null!;
}
/// <summary>
+26 -12
View File
@@ -1,6 +1,7 @@
using System;
using SimApi.CoceSdk;
namespace SimApi.Configs;
namespace SimApi.Configurations;
public class SimApiOptions
{
@@ -11,28 +12,41 @@ public class SimApiOptions
public bool EnableCors { get; set; } = true;
/// <summary>
/// 启用SimapiAuth,一个简单的基于Header Token的认证方式。
/// 启用SimApiAuth,一个简单的基于Header Token的认证方式。
/// default: false
/// </summary>
public bool EnableSimApiAuth { get; set; } = false;
public bool EnableSimApiAuth { get; set; }
/// <summary>
/// 是否使用CoceSdk
/// </summary>
public bool EnableCoceSdk { get; set; }
public CoceAppSdkOption CoceSdkOptions { get; set; } = new();
/// <summary>
/// 启用在线文档,启用后 访问 /swagger 可以查看对应的api文档。
/// default: false
/// </summary>
public bool EnableSimApiDoc { get; set; } = false;
public bool EnableSimApiDoc { get; set; }
/// <summary>
/// 启用异常拦截,启用后,所有的异常将被通过json反馈。
/// default: true
/// </summary>
public bool EnableSimApiException { get; set; } = true;
/// <summary>
/// 启用返回结果拦截
/// </summary>
public bool EnableSimApiResponseFilter { get; set; } = true;
/// <summary>
/// 开启S3兼容的存储系统。
/// default: false
/// </summary>
public bool EnableSimApiStorage { get; set; } = false;
public bool EnableSimApiStorage { get; set; }
/// <summary>
/// 开启ForwardHeaders,开启后可以透传负载均衡的Headers
@@ -50,12 +64,12 @@ public class SimApiOptions
/// 启用格式化的 Console Logger
/// default: false
/// </summary>
public bool EnableLogger { get; set; } = false;
public bool EnableLogger { get; set; }
/// <summary>
/// 是否启用Synapse
/// </summary>
public bool EnableSynapse { get; set; } = false;
public bool EnableSynapse { get; set; }
/// <summary>
@@ -70,22 +84,22 @@ public class SimApiOptions
public SimApiSynapseOptions SimApiSynapseOptions { get; set; } = new();
public void ConfigureSimApiSynapse(Action<SimApiSynapseOptions> options = null)
public void ConfigureSimApiSynapse(Action<SimApiSynapseOptions>? options = null)
{
options?.Invoke(SimApiSynapseOptions);
}
public void ConfigureSimApiSynapse(SimApiSynapseOptions options)
public void ConfigureCoceSdk(Action<CoceAppSdkOption>? options = null)
{
SimApiSynapseOptions = options;
options?.Invoke(CoceSdkOptions);
}
public void ConfigureSimApiDoc(Action<SimApiDocOptions> options = null)
public void ConfigureSimApiDoc(Action<SimApiDocOptions>? options = null)
{
options?.Invoke(SimApiDocOptions);
}
public void ConfigureSimApiStorage(Action<SimApiStorageOptions> options = null)
public void ConfigureSimApiStorage(Action<SimApiStorageOptions>? options = null)
{
options?.Invoke(SimApiStorageOptions);
}
+6 -6
View File
@@ -1,23 +1,23 @@
namespace SimApi.Configs;
namespace SimApi.Configurations;
public class SimApiStorageOptions
{
/// <summary>
/// S3 服务器入口地址
/// </summary>
public string Endpoint { get; set; }
public string? Endpoint { get; set; }
/// <summary>
/// S3 服务器文件访问地址
/// </summary>
public string ServeUrl { get; set; }
public string? ServeUrl { get; set; }
/// <summary>
/// S3服务 Bucket
/// </summary>
public string Bucket { get; set; }
public string? Bucket { get; set; }
public string AccessKey { get; set; }
public string? AccessKey { get; set; }
public string SecretKey { get; set; }
public string? SecretKey { get; set; }
}
+17 -22
View File
@@ -1,30 +1,25 @@
namespace SimApi.Configs;
namespace SimApi.Configurations;
public class SimApiSynapseOptions
{
public string MqHost { get; set; }
public int MqPort { get; set; }
public string MqUser { get; set; }
public string MqPass { get; set; }
public string MqVHost { get; set; } = "/";
public string SysName { get; set; }
public string AppName { get; set; }
public string AppId { get; set; }
/// <summary>
/// Mqtt服务器的Websocket地址
/// </summary>
public string? Websocket { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
public string? SysName { get; set; }
public string? AppName { get; set; }
public string? AppId { get; set; }
public int RpcTimeout { get; set; } = 3;
public ushort EventProcessorNum { get; set; } = 20;
public ushort RpcProcessorNum { get; set; } = 20;
/// <summary>
/// Event是否使用负载均衡
/// 也就是订阅$queue主题,消息会分发给不同的AppId
/// 如果false,多个AppId都可以同时收到消息
/// </summary>
public bool EventLoadBalancing { get; set; } = false;
public bool EnableConfigStore { get; set; } = true;
public bool DisableEventClient { get; set; } = false;
public bool DisableRpcClient { get; set; } = false;
}
+34 -12
View File
@@ -18,7 +18,7 @@ public class SimApiBaseController : Controller
/// <summary>
/// 当前登录用户的ID
/// </summary>
protected SimApiLoginItem LoginInfo => (SimApiLoginItem)HttpContext.Items["LoginInfo"];
protected SimApiLoginItem LoginInfo => (SimApiLoginItem)HttpContext.Items["LoginInfo"]!;
/// <summary>
/// 验证请求参数
@@ -26,14 +26,12 @@ public class SimApiBaseController : Controller
/// <param name="context"></param>
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
if (context.ModelState.IsValid) return;
foreach (var item in context.ModelState.Values.Where(item =>
item.ValidationState == ModelValidationState.Invalid))
{
foreach (var item in context.ModelState.Values.Where(item =>
item.ValidationState == ModelValidationState.Invalid))
{
Error(400, item.Errors.First().ErrorMessage);
break;
}
Error(400, item.Errors.First().ErrorMessage);
break;
}
}
@@ -50,12 +48,12 @@ public class SimApiBaseController : Controller
}
/// <summary>
/// 检测条件,根据条件返回报错
/// 检测条件,根据条件返回报错 如果condition是ture报错
/// </summary>
/// <param name="condition">检测条件</param>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述</param>
protected static void ErrorWhen(bool condition, int code = 500, string message = "")
protected static void ErrorWhen(bool condition, int code = 400, string message = "")
{
if (condition)
{
@@ -63,13 +61,37 @@ public class SimApiBaseController : Controller
}
}
/// <summary>
/// 检测条件,根据条件返回报错 如果condition是ture报错
/// </summary>
/// <param name="condition">检测条件</param>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述</param>
protected static void ErrorWhenTrue(bool condition, int code = 400, string message = "")
{
ErrorWhen(condition, code, message);
}
/// <summary>
/// 如果condition是false 报错
/// </summary>
/// <param name="condition"></param>
/// <param name="code"></param>
/// <param name="message"></param>
protected static void ErrorWhenFalse(bool condition, int code = 400, string message = "")
{
ErrorWhen(!condition, code, message);
}
/// <summary>
/// 检测给定的变量是否为NUll
/// </summary>
/// <param name="condition">检测条件</param>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述</param>
protected static void ErrorWhenNull(object condition, int code = 404, string message = "")
protected static void ErrorWhenNull(object? condition, int code = 404, string message = "请求的资源不存在")
{
ErrorWhen(condition == null, code, message);
}
@@ -80,6 +102,6 @@ public class SimApiBaseController : Controller
/// <returns></returns>
protected SimApiBaseResponse<string> UploadFile()
{
return new SimApiBaseResponse<string>(null);
return new SimApiBaseResponse<string>();
}
}
+48
View File
@@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using SimApi.Attributes;
using SimApi.CoceSdk;
using SimApi.Communications;
using SimApi.Helpers;
namespace SimApi.Controllers;
public class SimApiCoceController(CoceApp coce,SimApiAuth auth) : SimApiBaseController
{
[HttpPost]
public SimApiBaseResponse<ConfigResponse> GetConfig()
{
return new SimApiBaseResponse<ConfigResponse>(coce.GetConfig());
}
[HttpPost]
public SimApiBaseResponse<string> Login([FromBody] SimApiOneFieldRequest<string> request)
{
var data = coce.GetLevelToken(request.Data!);
ErrorWhenNull(data, 400);
coce.SaveToken(data!.UserId, data.Token);
var userinfo = coce.GetUserInfo(data!.Token);
var meta = new Dictionary<string, string>
{
{ "name", userinfo!.Name },
{ "image", userinfo.Image }
};
var groups = coce.GetUserGroups(data.Token!)!;
var loginItem = new SimApiLoginItem
{
Id = data.UserId,
Meta = meta,
Extra = groups
};
return new SimApiBaseResponse<string>(auth.Login(loginItem));
}
[HttpPost, SimApiAuth]
public SimApiBaseResponse<GroupInfo[]> ListGroups()
{
var levelToken = coce.GetToken(LoginInfo.Id!);
var groups = coce.GetUserGroups(levelToken!)!;
return new SimApiBaseResponse<GroupInfo[]>(groups.ToArray());
}
}
+17 -10
View File
@@ -4,8 +4,6 @@ using SimApi.Communications;
using SimApi.Helpers;
namespace SimApi.Controllers;
[ApiExplorerSettings(GroupName = "api")]
public class SimApiCommonController(SimApiAuth auth) : SimApiBaseController
{
/// <summary>
@@ -24,28 +22,37 @@ public class SimApiCommonController(SimApiAuth auth) : SimApiBaseController
/// 检测用户登陆的控制器
/// </summary>
/// <returns></returns>
[HttpPost("/auth/check"), SimApiDoc("认证", "检测登陆")]
[HttpPost, SimApiDoc("认证", "检测登陆")]
public SimApiBaseResponse<string> CheckLogin()
{
ErrorWhenNull(LoginInfo, 401);
return new SimApiBaseResponse<string>(LoginInfo.Id);
ErrorWhenNull(LoginInfo, 401,"未登录");
return new SimApiBaseResponse<string>
{
Data = LoginInfo.Id
};
}
/// <summary>
/// 退出登陆
/// </summary>
/// <returns></returns>
[HttpPost("/auth/logout"), SimApiDoc("认证", "退出登陆")]
[HttpPost, SimApiDoc("认证", "退出登陆")]
public SimApiBaseResponse Logout()
{
string token = null;
string? token = null;
if (Request.Headers.ContainsKey("Token"))
if (Request.Headers.TryGetValue("Token", out var value))
{
token = Request.Headers["Token"];
token = value;
}
auth.Logout(token);
auth.Logout(token!);
return new SimApiBaseResponse();
}
[HttpPost,SimApiAuth]
public SimApiBaseResponse<SimApiLoginItem> UserInfo()
{
return new SimApiBaseResponse<SimApiLoginItem>(LoginInfo);
}
}
+14 -12
View File
@@ -1,4 +1,6 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.Extensions.Caching.Distributed;
using SimApi.Communications;
@@ -11,27 +13,27 @@ namespace SimApi.Helpers;
public class SimApiAuth(IDistributedCache cache)
{
/// <summary>
/// 产生一个Token记录并返回Token
/// 登录信息
/// </summary>
/// <param name="id"></param>
/// <param name="loginItem"></param>
/// <param name="token"></param>
/// <returns></returns>
public string Login(string id, string type = "user", string token = null)
public string Login(SimApiLoginItem loginItem, string? token = null)
{
return Login(id, new[] { type }, token);
token ??= Guid.NewGuid().ToString();
cache.SetString(token, JsonSerializer.Serialize(loginItem));
return token;
}
/// <summary>
/// 产生一个Token并记录用户ID角色[多角色]
/// 获取登陆信息
/// </summary>
/// <param name="id"></param>
/// <param name="type"></param>
/// <param name="token"></param>
/// <returns></returns>
public string Login(string id, string[] type, string uuid = null)
public SimApiLoginItem? GetLogin(string token)
{
uuid ??= Guid.NewGuid().ToString();
var loginItem = new SimApiLoginItem(id, type);
cache.SetString(uuid, JsonSerializer.Serialize(loginItem));
return uuid;
var login = cache.GetString(token);
return login != null ? JsonSerializer.Deserialize<SimApiLoginItem>(login) : default;
}
/// <summary>
+34
View File
@@ -0,0 +1,34 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using SimApi.Attributes;
using SimApi.Communications;
namespace SimApi.Helpers;
public class SimApiResponseFilter : IResultFilter
{
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.ActionDescriptor.EndpointMetadata.Any(meta => meta is OriginResponseAttribute))
{
return;
}
context.Result = context.Result switch
{
// 检查结果是否为 null
null => new OkObjectResult(new SimApiBaseResponse()),
ObjectResult { Value: SimApiBaseResponse simApiBaseResponse } =>
new OkObjectResult(simApiBaseResponse),
ObjectResult objectResult => new OkObjectResult(new SimApiBaseResponse<object>(objectResult.Value!)),
EmptyResult => new OkObjectResult(new SimApiBaseResponse()),
_ => context.Result
};
}
public void OnResultExecuted(ResultExecutedContext context)
{
}
}
+80 -14
View File
@@ -1,19 +1,23 @@
using System;
#nullable enable
using System;
using System.IO;
using Microsoft.AspNetCore.Http;
using Minio;
using SimApi.Configs;
using Minio.DataModel.Args;
using SimApi.Configurations;
namespace SimApi.Helpers;
public class SimApiStorage
{
private MinioClient Mc { get; }
private IMinioClient Mc { get; }
public MinioClient Client => Mc;
public IMinioClient Client => Mc;
private string ServeUrl { get; }
private string Endpoint { get; }
public string Bucket { get; }
private IHttpContextAccessor HttpContextAccessor { get; }
@@ -22,9 +26,10 @@ public class SimApiStorage
{
var options = apiOptions.SimApiStorageOptions;
HttpContextAccessor = httpContextAccessor;
Endpoint = options.Endpoint!;
var useSsl = false;
string endpoint;
if (options.Endpoint.StartsWith("http://"))
if (options.Endpoint!.StartsWith("http://"))
{
endpoint = options.Endpoint.Replace("http://", string.Empty);
}
@@ -38,14 +43,16 @@ public class SimApiStorage
throw new Exception("SimApiStorage: Error Endpoint");
}
ServeUrl = options.ServeUrl;
Bucket = options.Bucket;
ServeUrl = options.ServeUrl!;
if (ServeUrl.EndsWith('/')) throw new Exception("SimApiStorage: ServeUrl must not end with /");
Bucket = options.Bucket!;
var mcb = new MinioClient().WithEndpoint(endpoint)
.WithCredentials(options.AccessKey, options.SecretKey);
if (useSsl)
{
mcb = mcb.WithSSL();
}
Mc = mcb.Build();
var found = Mc.BucketExistsAsync(new BucketExistsArgs().WithBucket(Bucket)).Result;
@@ -61,10 +68,13 @@ public class SimApiStorage
/// <param name="path"></param>
/// <param name="expire"></param>
/// <returns></returns>
public string GetUploadUrl(string path, int expire = 7200)
public GetUploadUrlResponse GetUploadUrl(string path, int expire = 7200)
{
return Mc.PresignedPutObjectAsync(new PresignedPutObjectArgs().WithBucket(Bucket)
.WithObject(path).WithExpiry(expire)).Result;
CheckPath(path);
var obj = path.TrimStart('/');
var uploadUrl = Mc.PresignedPutObjectAsync(new PresignedPutObjectArgs().WithBucket(Bucket)
.WithObject(obj).WithExpiry(expire)).Result;
return new GetUploadUrlResponse(uploadUrl, $"{ServeUrl}{path}", path);
}
/// <summary>
@@ -75,20 +85,38 @@ public class SimApiStorage
/// <returns></returns>
public string GetDownloadUrl(string path, int expire = 600)
{
CheckPath(path);
path = path.TrimStart('/');
return Mc.PresignedGetObjectAsync(new PresignedGetObjectArgs().WithBucket(Bucket).WithObject(path)
.WithExpiry(expire)).Result;
}
public string UploadFile(string path, Stream stream, string contentType = "image/png")
/// <summary>
/// 直接上传文件
/// </summary>
/// <param name="path"></param>
/// <param name="stream"></param>
/// <param name="contentType"></param>
public void UploadFile(string path, Stream stream, string contentType = "image/png")
{
CheckPath(path);
path = path.TrimStart('/');
Mc.PutObjectAsync(new PutObjectArgs().WithBucket(Bucket).WithObject(path).WithObjectSize(stream.Length)
.WithStreamData(stream).WithContentType(contentType)).Wait();
return null;
}
public string FullUrl(string path)
/// <summary>
/// 使用path获取完整的访问URL
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public string? FullUrl(string? path)
{
if (string.IsNullOrEmpty(path)) return path;
if(path.StartsWith("http://") || path.StartsWith("https://")) return path;
if (!(path.StartsWith('/') || path.StartsWith("~/"))) return path;
var httpRequest = HttpContextAccessor.HttpContext?.Request;
var url = $"{httpRequest?.Scheme}://{httpRequest?.Host}";
if (string.IsNullOrEmpty(path))
@@ -98,4 +126,42 @@ public class SimApiStorage
return path.StartsWith('~') ? string.Concat(url, path.AsSpan(1, path.Length - 1)) : $"{ServeUrl}{path}";
}
}
/// <summary>
/// 获取一个Path得访问URL
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public string? GetUrl(string? path)
{
if (string.IsNullOrEmpty(path)) return path;
if (path.StartsWith("~/"))
{
var httpRequest = HttpContextAccessor.HttpContext?.Request;
var url = $"{httpRequest?.Scheme}://{httpRequest?.Host}";
return string.Concat(url, path.AsSpan(1, path.Length - 1));
}
if (path.StartsWith('/'))
{
return $"{ServeUrl}{path}";
}
return path;
}
/// <summary>
/// 从URL中获取相对路径 (如果url不是当前服务器的url,则原样返回)
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public string? GetPath(string? url)
{
return url?.Replace($"{Endpoint}/{Bucket}", string.Empty).Replace(ServeUrl,string.Empty);
}
private void CheckPath(string path)
{
if (!path.StartsWith('/')) throw new Exception("path must start with /");
}
}
public record GetUploadUrlResponse(string UploadUrl, string DownloadUrl, string Path);
+23 -6
View File
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
@@ -50,16 +51,15 @@ public static class SimApiUtil
/// <returns></returns>
public static string Md5(string source, string mode = "x2")
{
var sor = Encoding.UTF8.GetBytes(source);
var md5 = MD5.Create();
var result = md5.ComputeHash(sor);
var strbul = new StringBuilder(40);
var sourceBytes = Encoding.UTF8.GetBytes(source);
var result = MD5.HashData(sourceBytes);
var stringBuilder = new StringBuilder(40);
foreach (var t in result)
{
strbul.Append(t.ToString(mode));
stringBuilder.Append(t.ToString(mode));
}
return strbul.ToString();
return stringBuilder.ToString();
}
/// <summary>
@@ -71,4 +71,21 @@ public static class SimApiUtil
{
return JsonSerializer.Serialize(obj, JsonOption);
}
/// <summary>
/// 分页
/// </summary>
/// <param name="query"></param>
/// <param name="page">页码</param>
/// <param name="count">每页数量</param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static IQueryable<T> Paginate<T>(this IQueryable<T> query, int page, int count)
{
if (page < 1)
page = 1;
if (count <= 0)
count = 10;
return query.Skip((page - 1) * count).Take(count);
}
}
+14 -5
View File
@@ -1,16 +1,14 @@
using System;
using Microsoft.Extensions.Logging;
using SimApi.Helpers;
namespace SimApi.Logger;
public class SimApiLogger(string name) : ILogger
{
public IDisposable BeginScope<TState>(TState state) => default!;
public IDisposable BeginScope<TState>(TState state) where TState : notnull => default!;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception, string> formatter)
{
Console.ForegroundColor = logLevel switch
@@ -23,11 +21,22 @@ public class SimApiLogger(string name) : ILogger
_ => ConsoleColor.White
};
var message =
$"[ {name} ][ {SimApiUtil.CstNow.ToString("yyyy-MM-dd HH:mm:ss:ffff")} ][ {logLevel.ToString()} ]\n{state}\n";
$"[ {name} ][ {DateTime.Now:yyyy-MM-dd HH:mm:ss:ffff} ][ {logLevel.ToString()} ]\n{state}\n";
if (exception != null)
{
message += $"{exception}\n";
}
Console.WriteLine(message);
Console.ResetColor();
}
}
public class EmptyDisposable : IDisposable
{
public static EmptyDisposable Instance { get; } = new EmptyDisposable();
public void Dispose()
{
}
}
-15
View File
@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
namespace SimApi.Logger;
public class SimApiLoggerConfiguration
{
public int EventId { get; set; }
public Dictionary<LogLevel, ConsoleColor> LogLevels { get; set; } = new()
{
[LogLevel.Information] = ConsoleColor.Green
};
}
+7 -9
View File
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using SimApi.Communications;
using SimApi.Helpers;
namespace SimApi.Middlewares;
@@ -11,23 +12,20 @@ namespace SimApi.Middlewares;
/// </summary>
public class SimApiAuthMiddleware(RequestDelegate next)
{
public Task Invoke(HttpContext httpContext, IDistributedCache cache)
public Task Invoke(HttpContext httpContext, IDistributedCache cache, SimApiAuth auth)
{
string token = null;
string? token = null;
if (httpContext.Request.Headers.TryGetValue("Token", out var header))
{
token = header;
}
if (!string.IsNullOrEmpty(token))
if (string.IsNullOrEmpty(token)) return next(httpContext);
var login = auth.GetLogin(token);
if (login != null)
{
var login = cache.GetString(token);
if (login != null)
{
httpContext.Items.Add("LoginInfo", JsonSerializer.Deserialize<SimApiLoginItem>(login));
}
httpContext.Items.Add("LoginInfo", login);
}
return next(httpContext);
}
}
+15 -15
View File
@@ -1,5 +1,5 @@
using System;
using System.Linq;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using SimApi.Communications;
@@ -24,12 +24,16 @@ public class SimApiExceptionMiddleware(RequestDelegate next, ILogger<SimApiExcep
try
{
await next(context);
if (context.Response.StatusCode != 200)
switch (context.Response.StatusCode)
{
if (!new[] { 301, 302 }.Contains(context.Response.StatusCode))
{
case 200:
case 301:
case 302:
break;
case 404:
throw new SimApiException(context.Response.StatusCode, "请求的接口不存在");
default:
throw new SimApiException(context.Response.StatusCode);
}
}
}
catch (SimApiException ex)
@@ -37,13 +41,12 @@ public class SimApiExceptionMiddleware(RequestDelegate next, ILogger<SimApiExcep
response = string.IsNullOrEmpty(ex.Message)
? new SimApiBaseResponse(ex.Code)
: new SimApiBaseResponse(ex.Code, ex.Message);
ErrorResponse(context, response);
}
catch (Exception ex)
{
log.LogError(ex.Message);
log.LogError(ex.StackTrace);
log.LogError("{Msg}", ex.Message);
log.LogError("{Msg}", ex.StackTrace);
response = new SimApiBaseResponse(500, ex.Message);
ErrorResponse(context, response);
}
@@ -54,13 +57,10 @@ public class SimApiExceptionMiddleware(RequestDelegate next, ILogger<SimApiExcep
/// </summary>
/// <param name="context"></param>
/// <param name="response"></param>
private void ErrorResponse(HttpContext context, SimApiBaseResponse response)
private static void ErrorResponse(HttpContext context, SimApiBaseResponse response)
{
if (!context.Response.HasStarted)
{
context.Response.StatusCode = 200;
context.Response.Headers.Append("Content-Type", "application/json");
context.Response.WriteAsync(response.ToString());
}
context.Response.StatusCode = 200;
context.Response.Headers.Append("Content-Type", "application/json");
context.Response.WriteAsync(response.ToString()).Wait();
}
}
+6 -9
View File
@@ -7,14 +7,11 @@ namespace SimApi.Models;
public class SimApiBaseModel
{
[Column(Order = 1)]
public string Id { get; set; } = Guid.NewGuid().ToString();
[Column(Order = 1)] public string Id { get; set; } = Guid.NewGuid().ToString();
[Column(Order = 9998)]
public DateTime UpdatedAt { get; set; } = SimApiUtil.CstNow;
[Column(Order = 9998)] public DateTime UpdatedAt { get; set; } = DateTime.Now;
[Column(Order = 9999)]
public DateTime CreatedAt { get; set; } = SimApiUtil.CstNow;
[Column(Order = 9999)] public DateTime CreatedAt { get; set; } = DateTime.Now;
protected virtual string[] MapperIgnoreField { get; set; } = { "Id", "CreatedAt", "UpdatedAt" };
@@ -23,7 +20,7 @@ public class SimApiBaseModel
public void MapData<TS>(TS source, bool mapAll = false)
{
//获取要赋值的源数据不为null的项目
var sourceProps = source.GetType().GetProperties().Where(x => x.GetValue(source) != null)
var sourceProps = source!.GetType().GetProperties().Where(x => x.GetValue(source) != null)
.Select(x => new
{
x.Name,
@@ -52,7 +49,7 @@ public class SimApiBaseModel
public void MapData<TS>(TS source, string[] mapFields)
{
//获取要赋值的源数据不为null的项目
var sourceProps = source.GetType().GetProperties().Where(x => x.GetValue(source) != null)
var sourceProps = source!.GetType().GetProperties().Where(x => x.GetValue(source) != null)
.Select(x => new
{
x.Name,
@@ -84,6 +81,6 @@ public class SimApiBaseModel
/// <returns></returns>
public void UpdateTime()
{
GetType().GetProperty(UpdatedTimeField)?.SetValue(this, SimApiUtil.CstNow);
GetType().GetProperty(UpdatedTimeField)?.SetValue(this, DateTime.Now);
}
}
+5 -7
View File
@@ -15,20 +15,18 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageVersion>5.0.2</PackageVersion>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Helpers\"/>
<Folder Include="Communications\"/>
<Folder Include="Controllers\"/>
<Folder Include="Middlewares\"/>
<Folder Include="Exceptions\"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Minio" Version="4.0.7"/>
<PackageReference Include="RabbitMQ.Client" Version="6.6.0"/>
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0"/>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.5.0"/>
<PackageReference Include="Minio" Version="6.0.3" />
<PackageReference Include="MQTTnet" Version="4.3.6.1152" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.7.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.7.1" />
</ItemGroup>
<ProjectExtensions>
<MonoDevelop>
+101 -9
View File
@@ -1,12 +1,19 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using SimApi.Helpers;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using SimApi.Middlewares;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using SimApi.Configs;
using SimApi.Attributes;
using SimApi.CoceSdk;
using SimApi.Configurations;
using SimApi.Logger;
namespace SimApi;
@@ -18,7 +25,7 @@ public static class SimApiExtensions
{
//**********快捷添加**************
public static IServiceCollection AddSimApi(this IServiceCollection builder,
Action<SimApiOptions> options = null)
Action<SimApiOptions>? options = null)
{
var simApiOptions = new SimApiOptions();
options?.Invoke(simApiOptions);
@@ -30,10 +37,16 @@ public static class SimApiExtensions
logger.AddProvider(new SimApiLoggerProvider());
});
}
// 是否使用 AUTH
if (simApiOptions.EnableSimApiAuth)
{
builder.AddScoped<SimApiAuth>();
builder.AddSingleton<SimApiAuth>();
}
if (simApiOptions.EnableCoceSdk)
{
builder.AddSingleton<CoceApp>();
}
if (simApiOptions.EnableCors)
@@ -45,6 +58,19 @@ public static class SimApiExtensions
if (simApiOptions.EnableSynapse)
{
builder.AddSingleton<Synapse>();
//自动依赖注入
var stackTrace = new StackTrace();
var callingMethod = stackTrace.GetFrame(stackTrace.FrameCount - 1)?.GetMethod();
var assembly = callingMethod?.DeclaringType?.Assembly;
var types = assembly?.GetTypes() ?? [];
foreach (var type in types)
{
var methodsWithSynapse = type.GetMethods()
.Where(m => m.GetCustomAttribute<SynapseRpcAttribute>() != null ||
m.GetCustomAttribute<SynapseEventAttribute>() != null);
if (!methodsWithSynapse.Any()) continue;
builder.AddScoped(type);
}
}
// 使用SimApiDoc
@@ -145,7 +171,7 @@ public static class SimApiExtensions
Id = "oauth2"
}
},
new[] { "SimApiAuth" }
["SimApiAuth"]
}
});
}
@@ -184,21 +210,69 @@ public static class SimApiExtensions
builder.AddSingleton<SimApiStorage>();
}
if (simApiOptions.EnableSimApiResponseFilter)
{
builder.AddControllers(opt => opt.Filters.Add<SimApiResponseFilter>())
.AddJsonOptions(opt => opt.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase);
}
builder.AddSingleton(simApiOptions);
return builder;
}
public static IHost UseSimApi(this IHost builder)
{
var options = builder.Services.GetRequiredService<SimApiOptions>();
var logger = builder.Services.GetRequiredService<ILogger<SimApiOptions>>();
logger.LogInformation("当前时区: {LocalId}", TimeZoneInfo.Local.Id);
//请求一下检测存储错误
if (options.EnableSimApiStorage)
{
logger.LogInformation("开始配置SimApiStorage...");
builder.Services.GetService<SimApiStorage>();
}
if (options.EnableSimApiResponseFilter)
{
logger.LogInformation("开始配置SimApiResponseFilter...");
}
if (options.EnableCoceSdk)
{
logger.LogInformation("开始配置CoceAppSdk...\nApi入口: {ApiUrl}\nAuth入口:{AuthUrl}n\nAppId: {AppId}",
options.CoceSdkOptions.ApiEndpoint, options.CoceSdkOptions.AuthEndpoint,
options.CoceSdkOptions.AppId);
}
if (options.EnableLowerUrl)
{
logger.LogInformation("开始配置使用URL小写...");
}
if (options.EnableSynapse)
{
var synapse = builder.Services.GetRequiredService<Synapse>();
synapse.Init();
}
return builder;
}
/// <summary>
/// 使用所有SimApi自定义中间件
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IApplicationBuilder UseSimApi(this IApplicationBuilder builder)
public static WebApplication UseSimApi(this WebApplication builder)
{
var options = builder.ApplicationServices.GetRequiredService<SimApiOptions>();
var options = builder.Services.GetRequiredService<SimApiOptions>();
var logger = builder.ApplicationServices.GetRequiredService<ILogger<SimApiOptions>>();
var logger = builder.Services.GetRequiredService<ILogger<SimApiOptions>>();
logger.LogInformation("当前时区: {LocalId}", TimeZoneInfo.Local.Id);
if (options.EnableForwardHeaders)
{
logger.LogInformation("开始配置ForwardedHeaders...");
@@ -215,6 +289,24 @@ public static class SimApiExtensions
{
logger.LogInformation("开始配置SimApiAuth...");
builder.UseMiddleware<SimApiAuthMiddleware>();
builder.MapControllerRoute(name: "GetUserInfo", pattern: "/user/info",
defaults: new { controller = "SimApiCommon", action = "UserInfo" });
builder.MapControllerRoute(name: "CheckLogin", pattern: "/auth/check",
defaults: new { controller = "SimApiCommon", action = "CheckLogin" });
builder.MapControllerRoute(name: "Logout", pattern: "/auth/logout",
defaults: new { controller = "SimApiCommon", action = "Logout" });
if (options.EnableCoceSdk)
{
logger.LogInformation("开始配置CoceAppSdk...\nApi入口: {ApiUrl}\nAuth入口:{AuthUrl}n\nAppId: {AppId}",
options.CoceSdkOptions.ApiEndpoint, options.CoceSdkOptions.AuthEndpoint,
options.CoceSdkOptions.AppId);
builder.MapControllerRoute(name: "LoginUseCoce", pattern: "/auth/login",
defaults: new { controller = "SimApiCoce", action = "Login" });
builder.MapControllerRoute(name: "LoginUseCoce", pattern: "/user/groups",
defaults: new { controller = "SimApiCoce", action = "ListGroups" });
builder.MapControllerRoute(name: "LoginUseCoce", pattern: "/auth/config",
defaults: new { controller = "SimApiCoce", action = "GetConfig" });
}
}
if (options.EnableSimApiDoc)
@@ -245,7 +337,7 @@ public static class SimApiExtensions
if (options.EnableSimApiStorage)
{
logger.LogInformation("开始配置SimApiStorage...");
builder.ApplicationServices.GetService<SimApiStorage>();
builder.Services.GetService<SimApiStorage>();
}
if (options.EnableLowerUrl)
@@ -255,7 +347,7 @@ public static class SimApiExtensions
if (options.EnableSynapse)
{
var synapse = builder.ApplicationServices.GetRequiredService<Synapse>();
var synapse = builder.Services.GetRequiredService<Synapse>();
synapse.Init();
}
+63
View File
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using MQTTnet;
using MQTTnet.Protocol;
namespace SimApi;
public partial class Synapse
{
public event EventHandler<ConfigStoreItem>? OnConfigChanged;
private Dictionary<string, string> CurrentConfig { get; } = new();
private string ConfigStoreTopicPrefix => $"{Options.SysName}/synapse-config-store/";
private bool FireSetConfig(string key, string value)
{
if (key.Contains('#') || key.Contains('+')) return false;
var topic = $"{Options.SysName}/synapse-config-store/{key}";
var message = new MqttApplicationMessageBuilder()
.WithTopic(topic)
.WithPayload(value)
.WithRetainFlag()
.Build();
if (!Client!.IsConnected) return false;
Client!.PublishAsync(message, CancellationToken.None).Wait();
logger.LogDebug("Synapse Config Set: {Config} => {Data}", key, value);
return true;
}
private string? FireGetConfig(string key)
{
return CurrentConfig.GetValueOrDefault(key);
}
private void RunConfigStoreServer()
{
Client!.ApplicationMessageReceivedAsync += e =>
{
if (!e.ApplicationMessage.Topic.StartsWith(ConfigStoreTopicPrefix)) return Task.CompletedTask;
var reqBody = e.ApplicationMessage.ConvertPayloadToString();
var eventName = e.ApplicationMessage.Topic.Replace(ConfigStoreTopicPrefix, string.Empty);
CurrentConfig[eventName] = reqBody;
OnConfigChanged?.Invoke(this, new ConfigStoreItem(eventName, reqBody));
logger.LogDebug("Synapse Config Changed: {Config} => {Data}", eventName, reqBody);
return Task.CompletedTask;
};
SubConfigStoreServerTopic();
}
private void SubConfigStoreServerTopic()
{
var csSubOpts = MqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter(o =>
o.WithTopic($"{ConfigStoreTopicPrefix}#").WithRetainHandling(MqttRetainHandling.SendAtSubscribe))
.Build();
Client!.SubscribeAsync(csSubOpts).Wait();
}
public record ConfigStoreItem(string Key, string Value);
}
+22 -20
View File
@@ -1,31 +1,33 @@
using System;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
using System.Threading;
using Microsoft.Extensions.Logging;
using MQTTnet;
using SimApi.Helpers;
namespace SimApi;
public partial class Synapse
{
private void RunEventClient()
private bool FireEvent(string eventName, object? param)
{
EventClientChannel = CreateChannel(0, "EventClient");
}
private void FireEvent(string eventName, object param)
{
var paramJson = JsonSerializer.Serialize(param, SimApiUtil.JsonOption);
var router = $"event.{Options.AppName}.{eventName}";
var props = EventClientChannel.CreateBasicProperties();
props.AppId = Options.AppId;
props.MessageId = Guid.NewGuid().ToString();
props.ReplyTo = Options.AppName;
props.Type = eventName;
EventClientChannel.BasicPublish(Options.SysName, router, false, props, Encoding.UTF8.GetBytes(paramJson));
Logger.LogDebug("Event Publish: {OptionsAppName}.{EventName}\n{ParamJson}", Options.AppName, eventName,
paramJson);
string paramJson;
if (param is string strParam)
{
paramJson = strParam;
}
else
{
paramJson = JsonSerializer.Serialize(param, SimApiUtil.JsonOption);
}
var topic = $"{Options.SysName}/event/{Options.AppName}/{eventName}";
var message = new MqttApplicationMessageBuilder()
.WithTopic(topic)
.WithPayload(paramJson)
.WithRetainFlag(false)
.Build();
if (!Client!.IsConnected) return false;
Client.PublishAsync(message, CancellationToken.None).Wait();
logger.LogDebug("Synapse Event Publish: {Event}@{App} {Json}", eventName, Options.AppName, paramJson);
return true;
}
}
+58 -33
View File
@@ -1,50 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client.Events;
using MQTTnet;
using SimApi.Helpers;
namespace SimApi;
public partial class Synapse
{
private string EventServerTopicPrefix => $"{Options.SysName}/event/";
private void RunEventServer()
{
EventServerChannel = CreateChannel(Options.EventProcessorNum, "EventServer");
var queue = $"{Options.SysName}_{Options.AppName}_event";
EventServerChannel.QueueDeclare(queue, true, false, true, null);
foreach (var ev in EventRegistry.Where(ev => !ev.Key.Contains('*') && !ev.Key.Contains('#')))
Client!.ApplicationMessageReceivedAsync += async e =>
{
EventServerChannel.QueueBind(queue, Options.SysName, $"event.{ev.Key}", null);
}
var consumer = new EventingBasicConsumer(EventServerChannel);
consumer.Received += (ch, ea) =>
{
var reqBody = Encoding.UTF8.GetString(ea.Body.ToArray());
Logger.LogDebug("Event Receive: {BasicPropertiesReplyTo}.{BasicPropertiesType}\n{S}",
ea.BasicProperties.ReplyTo, ea.BasicProperties.Type, reqBody);
var key = ea.RoutingKey.Replace("event.", string.Empty);
var method = EventRegistry.FirstOrDefault(x => x.Key == key);
var callClass = Sp.CreateScope().ServiceProvider.GetRequiredService(method.Class);
var mt = callClass.GetType().GetMethod(method.Method);
var pt = mt.GetParameters()[0].ParameterType;
try
{
mt.Invoke(callClass, pt == typeof(string)
? new object[] { reqBody }
: new[] { JsonSerializer.Deserialize(reqBody, pt, SimApiUtil.JsonOption) });
EventServerChannel.BasicAck(ea.DeliveryTag, false);
}
catch (Exception e)
{
Logger.LogError("Event Processor Error: {Err}", e.InnerException);
EventServerChannel.BasicNack(ea.DeliveryTag, false, false);
}
if (!e.ApplicationMessage.Topic.StartsWith(EventServerTopicPrefix)) return;
var reqBody = e.ApplicationMessage.ConvertPayloadToString();
var eventName = e.ApplicationMessage.Topic.Replace(EventServerTopicPrefix, string.Empty);
logger.LogDebug("Synapse Event Receive: {EventName}\n{Body}", eventName, reqBody);
var methods = EventRegistry
.Where(x => Regex.IsMatch(eventName,
"^" + Regex.Escape(x.Key!).Replace("\\+", "[^/]+").Replace("\\#", ".*") + "$"))
.ToArray();
var tasks = methods.Select(method => Task.Run(() =>
{
var callClass = sp.CreateScope().ServiceProvider.GetRequiredService(method.Class!);
var mt = callClass.GetType().GetMethod(method.Method!);
try
{
if (mt!.GetParameters().Length == 2)
{
var pt = mt.GetParameters()[1].ParameterType;
mt.Invoke(callClass, pt == typeof(string)
? new object?[] { eventName, reqBody }
: new object?[] { eventName, JsonSerializer.Deserialize(reqBody, pt, SimApiUtil.JsonOption) });
}
else
{
mt.Invoke(callClass, new object[] { eventName });
}
}
catch (Exception ex)
{
logger.LogError("Synapse Event Processor Error: {Err}\n{Stack}", ex.Message, ex.StackTrace);
}
}))
.ToList();
await Task.WhenAll(tasks);
};
EventServerChannel.BasicConsume(queue, false, "", false, false, null, consumer);
SubEventServerTopic();
}
private void SubEventServerTopic()
{
foreach (var ev in EventRegistry)
{
var topic = $"{EventServerTopicPrefix}{ev.Key}";
if (Options.EventLoadBalancing)
{
topic = "$queue/" + topic;
}
var evSubOpts = MqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter(o => o.WithTopic(topic)).Build();
Client!.SubscribeAsync(evSubOpts).Wait();
logger.LogDebug("Synapse Event Register Event Success: {EventName}\nFull Topic: {Topic}", ev.Key, topic);
}
}
}
+75 -43
View File
@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Encodings.Web;
using System.Linq;
using System.Text.Json;
using System.Text.Unicode;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Serialization;
using RabbitMQ.Client.Events;
using MQTTnet;
using MQTTnet.Packets;
using SimApi.Communications;
using SimApi.Helpers;
@@ -14,57 +14,89 @@ namespace SimApi;
public partial class Synapse
{
private Dictionary<string, byte[]> ResponseCache { get; } = new();
private Dictionary<string, TaskCompletionSource<string>> ResponseCompletionSources { get; } = new();
private string EventClientTopicPrefix => $"{Options.SysName}/{Options.AppName}/rpc/client/{Options.AppId}/";
private void RunRpcClient()
{
RpcClientChannel = CreateChannel(0, "RpcClient");
var queue = $"{Options.SysName}_{Options.AppName}_client_{Options.AppId}";
var router = $"client.{Options.AppName}.{Options.AppId}";
RpcClientChannel.QueueDeclare(queue, true, false, true, null);
RpcClientChannel.QueueBind(queue, Options.SysName, router, null);
var consumer = new EventingBasicConsumer(RpcClientChannel);
consumer.Received += (ch, ea) =>
Client!.ApplicationMessageReceivedAsync += e =>
{
ResponseCache.Add(ea.BasicProperties.CorrelationId, ea.Body.ToArray());
RpcClientChannel.BasicAck(ea.DeliveryTag, false);
Logger.LogDebug(
"RPC Response: ({BasicPropertiesCorrelationId}) {BasicPropertiesType}@{BasicPropertiesReplyTo} -> {OptionsAppName}\n{S}",
ea.BasicProperties.CorrelationId, ea.BasicProperties.Type, ea.BasicProperties.ReplyTo, Options.AppName,
Encoding.UTF8.GetString(ea.Body.ToArray()));
if (!e.ApplicationMessage.Topic.StartsWith(EventClientTopicPrefix)) return Task.CompletedTask;
var reqBody = e.ApplicationMessage.ConvertPayloadToString();
var messageId = e.ApplicationMessage.Topic.Replace(EventClientTopicPrefix, string.Empty);
if (!ResponseCompletionSources.TryGetValue(messageId, out var tcs)) return Task.CompletedTask;
tcs.SetResult(reqBody);
ResponseCompletionSources.Remove(messageId);
return Task.CompletedTask;
};
RpcClientChannel.BasicConsume(queue, false, "", false, false, null, consumer);
SubRpcClientTopic();
}
private string FireRpc(string app, string action, object param)
private void SubRpcClientTopic()
{
var paramJson = JsonSerializer.Serialize(param, SimApiUtil.JsonOption);
var router = $"server.{app}";
string response;
var props = RpcClientChannel.CreateBasicProperties();
props.AppId = Options.AppId;
props.MessageId = Guid.NewGuid().ToString();
props.Type = action;
props.ReplyTo = Options.AppName;
RpcClientChannel.BasicPublish(Options.SysName, router, false, props, Encoding.UTF8.GetBytes(paramJson));
Logger.LogDebug("RPC Request: ({PropsMessageId}) {OptionsAppName} -> {Action}@{App}\n{ParamJson}",
props.MessageId,
Options.AppName, action, app, paramJson);
var ts = SimApiUtil.TimestampNow;
while (true)
var rcSubOpts = MqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter(o => o.WithTopic($"{EventClientTopicPrefix}+")).Build();
Client!.SubscribeAsync(rcSubOpts).Wait();
}
private string? FireRpc(string app, string action, object? param, Dictionary<string, string>? headers = null,
int? timeout = null)
{
string paramJson;
if (param is string strParam)
{
if (SimApiUtil.TimestampNow - ts > Options.RpcTimeout)
paramJson = strParam;
}
else
{
paramJson = JsonSerializer.Serialize(param, SimApiUtil.JsonOption);
}
var topic = $"{Options.SysName}/{app}/rpc/server/{action}";
var messageId = Guid.NewGuid().ToString();
var tcs = new TaskCompletionSource<string>();
ResponseCompletionSources.Add(messageId, tcs);
var messageBuilder = new MqttApplicationMessageBuilder()
.WithTopic(topic)
.WithPayload(paramJson)
.WithResponseTopic($"{Options.AppName},{Options.AppId},{messageId}")
.WithContentType("application/json")
.WithRetainFlag(false);
foreach (var h in headers ?? new Dictionary<string, string>())
{
messageBuilder.WithUserProperty(h.Key, h.Value);
}
var message = messageBuilder.Build();
if (!Client!.IsConnected) return null;
Client.PublishAsync(message, CancellationToken.None).Wait();
logger.LogDebug(
"Synapse RPC Client Request: ({PropsMessageId}) {OptionsAppName} -> {Action}@{App}\n{ParamJson}\nHeaders: {Headers}",
messageId, Options.AppName, action, app, paramJson, JsonSerializer.Serialize(headers));
string response;
try
{
timeout ??= Options.RpcTimeout;
if (tcs.Task.Wait(timeout.Value * 1000))
{
response = tcs.Task.Result;
logger.LogDebug(
"Synapse RPC Client Response: ({BasicPropertiesCorrelationId}) {BasicPropertiesType}@{BasicPropertiesReplyTo} -> {OptionsAppName}\n{S}",
messageId, action, app, Options.AppName, response);
}
else
{
response = JsonSerializer.Serialize(new SimApiBaseResponse(502, "timeout"), SimApiUtil.JsonOption);
break;
}
if (ResponseCache.TryGetValue(props.MessageId, out var value))
{
response = Encoding.UTF8.GetString(value);
ResponseCache.Remove(props.MessageId);
break;
}
}
catch
{
response = JsonSerializer.Serialize(new SimApiBaseResponse(500, "Synapse RPC Client Error"),
SimApiUtil.JsonOption);
}
return response;
}
}
+80 -53
View File
@@ -1,14 +1,12 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Unicode;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client.Events;
using MQTTnet;
using MQTTnet.Protocol;
using SimApi.Communications;
using SimApi.Exceptions;
using SimApi.Helpers;
@@ -18,76 +16,105 @@ namespace SimApi;
public partial class Synapse
{
private string RpcServerTopicPrefix => $"{Options.SysName}/{Options.AppName}/rpc/server/";
private void RunRpcServer()
{
RpcServerChannel = CreateChannel(Options.RpcProcessorNum, "RpcServer");
var queue = $"{Options.SysName}_{Options.AppName}_server";
var router = $"server.{Options.AppName}";
RpcServerChannel.QueueDeclare(queue, true, false, true, null);
RpcServerChannel.QueueBind(queue, Options.SysName, router, null);
var consumer = new EventingBasicConsumer(RpcServerChannel);
consumer.Received += (ch, ea) =>
Client!.ApplicationMessageReceivedAsync += e =>
{
var reqBody = Encoding.UTF8.GetString(ea.Body.ToArray());
Logger.LogDebug(
"RPC Receive: ({BasicPropertiesMessageId}) {BasicPropertiesReplyTo} -> {BasicPropertiesType}@{OptionsAppName}\n{S}",
ea.BasicProperties.MessageId, ea.BasicProperties.ReplyTo, ea.BasicProperties.Type, Options.AppName,
reqBody);
var res = new SimApiBaseResponse(404, "method not found");
var method = RpcRegistry.FirstOrDefault(x => x.Key == ea.BasicProperties.Type);
if (method != null)
if (!e.ApplicationMessage.Topic.StartsWith(RpcServerTopicPrefix)) return Task.CompletedTask;
var reqBody = e.ApplicationMessage.ConvertPayloadToString();
var action = e.ApplicationMessage.Topic.Replace(RpcServerTopicPrefix, string.Empty);
var appInfo = e.ApplicationMessage.ResponseTopic.Split(",");
logger.LogDebug(
"Synapse RPC Server Receive: ({BasicPropertiesMessageId}) {BasicPropertiesReplyTo} -> {BasicPropertiesType}@{OptionsAppName}\n{S}",
appInfo[2], appInfo[0], action, Options.AppName, reqBody);
SimApiBaseResponse res;
var method = RpcRegistry.FirstOrDefault(x => x.Key == action);
if (method == null)
{
var callClass = Sp.CreateScope().ServiceProvider.GetRequiredService(method.Class);
var mt = callClass.GetType().GetMethod(method.Method);
var param = Array.Empty<object>();
res = new SimApiBaseResponse(404, "method not found");
}
else
{
var callClass = sp.CreateScope().ServiceProvider.GetRequiredService(method.Class!);
var mt = callClass.GetType().GetMethod(method.Method!);
try
{
var pt = mt.GetParameters()[0].ParameterType;
if (pt == typeof(string))
var methodParams = mt!.GetParameters();
object? ret;
switch (methodParams.Length)
{
param = new[] { reqBody };
case 1:
var pt = mt.GetParameters()[0].ParameterType;
var param = pt == typeof(string)
? [reqBody]
: new[] { JsonSerializer.Deserialize(reqBody, pt, SimApiUtil.JsonOption) };
ret = mt.Invoke(callClass, param);
break;
case 2:
var headerData =
e.ApplicationMessage.UserProperties.ToDictionary(x => x.Name, x => x.Value);
var pt2 = mt.GetParameters()[0].ParameterType;
var param2 = pt2 == typeof(string)
? [reqBody]
: new[] { JsonSerializer.Deserialize(reqBody, pt2, SimApiUtil.JsonOption), headerData };
ret = mt.Invoke(callClass, param2);
break;
default:
ret = mt.Invoke(callClass, []);
break;
}
else
res = new SimApiBaseResponse<object?>
{
var paramObj = JsonSerializer.Deserialize(reqBody, pt, SimApiUtil.JsonOption);
param = new[] { paramObj };
}
var ret = mt.Invoke(callClass, param);
res = new SimApiBaseResponse<object>(ret);
Data = ret
};
}
catch (TargetInvocationException e)
catch (TargetInvocationException ex)
{
if (e.InnerException is SimApiException ie)
if (ex.InnerException is SimApiException ie)
{
Logger.LogDebug("RPC调用错误: {Err}", ie.Message);
logger.LogDebug("Synapse RPC调用错误: {Err}", ie.Message);
res = new SimApiBaseResponse(ie.Code, ie.Message);
}
else
{
res = new SimApiBaseResponse(500, e.Message);
logger.LogError("Synapse RPC 方法异常: {Err}\n{Stack}", ex.Message, ex.StackTrace);
res = new SimApiBaseResponse(500, ex.Message);
}
}
catch (Exception e)
catch (Exception ex)
{
Logger.LogDebug("RPC调用失败: {Err}", e.Message);
res = new SimApiBaseResponse(500, e.Message);
logger.LogDebug("Synapse RPC调用失败: {Err}", ex.Message);
res = new SimApiBaseResponse(500, ex.Message);
}
}
var returnJson = JsonSerializer.Serialize((object)res, SimApiUtil.JsonOption);
var reply = $"client.{ea.BasicProperties.ReplyTo}.{ea.BasicProperties.AppId}";
var props = RpcServerChannel.CreateBasicProperties();
props.AppId = Options.AppId;
props.CorrelationId = ea.BasicProperties.MessageId;
props.MessageId = Guid.NewGuid().ToString();
props.ReplyTo = Options.AppName;
props.Type = ea.BasicProperties.Type;
RpcServerChannel.BasicPublish(Options.SysName, reply, false, props, Encoding.UTF8.GetBytes(returnJson));
Logger.LogDebug(
"Rpc Return: ({BasicPropertiesMessageId}) {BasicPropertiesType}@{OptionsAppName} -> {BasicPropertiesReplyTo}\n{ReturnJson}",
ea.BasicProperties.MessageId, ea.BasicProperties.Type, Options.AppName, ea.BasicProperties.ReplyTo,
returnJson);
var reply = $"{Options.SysName}/{appInfo[0]}/rpc/client/{appInfo[1]}/{appInfo[2]}";
var message = new MqttApplicationMessageBuilder()
.WithTopic(reply)
.WithPayload(returnJson)
.WithRetainFlag(false)
.Build();
if (!Client.IsConnected) return Task.CompletedTask;
Client.PublishAsync(message, CancellationToken.None).Wait();
logger.LogDebug(
"Synapse Rpc Server Return: ({BasicPropertiesMessageId}) {BasicPropertiesType}@{OptionsAppName} -> {BasicPropertiesReplyTo}\n{ReturnJson}",
appInfo[2], action, Options.AppName, appInfo[0], returnJson);
return Task.CompletedTask;
};
RpcServerChannel.BasicConsume(queue, true, "", false, false, null, consumer);
SubRpcServerTopic();
}
private void SubRpcServerTopic()
{
var rsSubOpts = MqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter(o =>
o.WithTopic($"$queue/{RpcServerTopicPrefix}+").WithRetainHandling(MqttRetainHandling.SendAtSubscribe))
.Build();
Client!.SubscribeAsync(rsSubOpts).Wait();
}
}
+217 -130
View File
@@ -3,188 +3,228 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Exceptions;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Formatter;
using SimApi.Attributes;
using SimApi.Communications;
using SimApi.Configs;
using SimApi.Configurations;
using SimApi.Exceptions;
using SimApi.Helpers;
namespace SimApi;
public partial class Synapse
public partial class Synapse(SimApiOptions simApiOptions, ILogger<Synapse> logger, IServiceProvider sp)
{
private IServiceProvider Sp { get; }
private SimApiSynapseOptions Options { get; } = simApiOptions.SimApiSynapseOptions;
private SimApiSynapseOptions Options { get; }
private MqttFactory MqttFactory { get; } = new();
public IMqttClient? Client { get; set; }
private ILogger<Synapse> Logger { get; }
private List<RegisterItem> EventRegistry { get; set; } = new();
private IConnection Connection { get; set; }
private IModel EventClientChannel { get; set; }
private IModel EventServerChannel { get; set; }
private IModel RpcClientChannel { get; set; }
private IModel RpcServerChannel { get; set; }
private List<RegisterItem> EventRegistry { get; set; }
private List<RegisterItem> RpcRegistry { get; set; }
public Synapse(SimApiOptions simApiOptions, ILogger<Synapse> logger, IServiceProvider sp)
{
Logger = logger;
Sp = sp;
Options = simApiOptions.SimApiSynapseOptions;
}
private List<RegisterItem> RpcRegistry { get; set; } = new();
public void Init()
{
ProcessAttribute();
Logger.LogDebug("Synapse初始化配置信息: {Json}", SimApiUtil.Json(Options));
logger.LogDebug("Synapse初始化配置信息: {Json}", SimApiUtil.Json(Options));
if (string.IsNullOrEmpty(Options.AppName) || string.IsNullOrEmpty(Options.SysName))
{
Logger.LogCritical("Synapse初始化失败: AppName or SysName 错误");
logger.LogCritical("Synapse初始化失败: AppName SysName 不能为空");
}
Options.AppId ??= Guid.NewGuid().ToString();
Logger.LogInformation("System Name: {SysName}\nApp Name: {AppName}\nAppId: {AppId}", Options.SysName,
Options.AppName, Options.AppId);
logger.LogInformation("Synapse Sys Name: {SysName}\nSynapse App Name: {AppName}\nSynapse App Id: {AppId}",
Options.SysName, Options.AppName, Options.AppId);
CreateConnection();
CheckAndCreateExchange();
ProcessAttribute();
//事件客户端
if (Options.DisableEventClient)
{
Logger.LogWarning("Event Client Disabled: DisableEventClient set true");
logger.LogWarning("Synapse Event Client Disabled: DisableEventClient set true");
}
else
{
RunEventClient();
Logger.LogInformation("Event Client Ready");
logger.LogInformation("Synapse Event Client Ready");
}
//RPC客户端
if (Options.DisableRpcClient)
{
Logger.LogWarning("Rpc Client Disabled: DisableEventClient set true");
logger.LogWarning("Synapse Rpc Client Disabled: DisableEventClient set true");
}
else
{
RunRpcClient();
Logger.LogInformation("Rpc Client Ready, Client Timeout: {OptionsRpcTimeout}s", Options.RpcTimeout);
logger.LogInformation("Synapse Rpc Client Ready, Client Timeout: {OptionsRpcTimeout}s", Options.RpcTimeout);
}
if (RpcRegistry.Count > 0)
{
RunRpcServer();
}
if (EventRegistry.Count > 0)
{
RunEventServer();
}
if (Options.EnableConfigStore)
{
RunConfigStoreServer();
logger.LogInformation("Synapse Config Store Ready [{SysName}] ...", Options.SysName);
}
}
public SimApiBaseResponse<T> Rpc<T>(string appName, string method, dynamic param)
/// <summary>
/// 调用RPC使用明确的返回值类型
/// </summary>
/// <param name="appName"></param>
/// <param name="method"></param>
/// <param name="param"></param>
/// <param name="headers"></param>
/// <param name="timeout"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public SimApiBaseResponse<T> Rpc<T>(string appName, string method, dynamic? param = null,
Dictionary<string, string>? headers = null, int? timeout = null)
{
var res = new SimApiBaseResponse(500, "Rpc Client Disabled!");
var res = new SimApiBaseResponse(500, "Synapse Rpc Client Disabled!");
if (Options.DisableRpcClient)
{
Logger.LogError("Rpc Client Disabled!");
logger.LogError("Synapse Rpc Client Disabled!");
}
else
{
var data = FireRpc(appName, method, param);
var data = FireRpc(appName, method, param, headers, timeout);
res = JsonSerializer.Deserialize<SimApiBaseResponse<T>>(data, SimApiUtil.JsonOption);
}
return res as SimApiBaseResponse<T>;
return (res as SimApiBaseResponse<T>)!;
}
public SimApiBaseResponse<object> Rpc(string appName, string method, dynamic param)
/// <summary>
/// 调用RPC使用object作为返回值类型
/// </summary>
/// <param name="appName"></param>
/// <param name="method"></param>
/// <param name="param"></param>
/// <param name="headers"></param>
/// <param name="timeout"></param>
/// <returns></returns>
public SimApiBaseResponse<object> Rpc(string appName, string method, dynamic? param = null,
Dictionary<string, string>? headers = null, int? timeout = null)
{
return Rpc<object>(appName, method, param);
return Rpc<object>(appName, method, param, headers, timeout);
}
public void Event(string eventName, dynamic param)
/// <summary>
/// 只能在Rpc方法中使用,快捷抛出异常返回
/// </summary>
/// <param name="code"></param>
/// <param name="message"></param>
/// <exception cref="SimApiException"></exception>
public void RpcError(int code, string message = "")
{
if (Options.DisableEventClient)
{
Logger.LogError("Event Client Disabled!");
}
else
{
FireEvent(eventName, param);
}
throw new SimApiException(code, message);
}
/// <summary>
/// 如果条件成立,则爆出错误
/// </summary>
/// <param name="condition"></param>
/// <param name="code"></param>
/// <param name="message"></param>
public void RpcErrorWhen(bool condition, int code, string message = "")
{
if (condition) RpcError(code, message);
}
/// <summary>
/// 发送一个事件x
/// </summary>
/// <param name="eventName"></param>
/// <param name="param"></param>
public bool Event(string eventName, dynamic? param = null)
{
if (!Options.DisableEventClient) return FireEvent(eventName, param);
logger.LogError("Synapse Event Client Disabled!");
return false;
}
/// <summary>
/// 设置一个系统配置项
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <returns></returns>
public bool SetConfig(string key, string value)
{
if (Options.EnableConfigStore) return FireSetConfig(key, value);
logger.LogError("Synapse Config Store Disabled!");
return false;
}
/// <summary>
/// 读取一个配置项,如果没有则为空
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public string? GetConfig(string key)
{
if (Options.EnableConfigStore) return FireGetConfig(key);
logger.LogError("Synapse Config Store Disabled!");
return null;
}
private void CreateConnection()
{
var factory = new ConnectionFactory
Client = MqttFactory.CreateMqttClient();
var clientOpts = new MqttClientOptionsBuilder().WithProtocolVersion(MqttProtocolVersion.V500)
.WithWebSocketServer(o => o.WithUri(Options.Websocket))
.WithCredentials(Options.Username, Options.Password)
.WithClientId($"{Options.AppName}:{Options.AppId}")
.Build();
Client!.ConnectAsync(clientOpts, CancellationToken.None).Wait();
Client.ConnectedAsync += _ =>
{
HostName = Options.MqHost,
Port = Options.MqPort,
VirtualHost = Options.MqVHost,
UserName = Options.MqUser,
Password = Options.MqPass
logger.LogInformation("Synapse MQTT[{AppName}:{AppId}] 连接成功...", Options.AppName, Options.AppId);
//RPC客户端
if (!Options.DisableRpcClient) SubRpcClientTopic();
if (RpcRegistry.Count > 0) SubRpcServerTopic();
if (EventRegistry.Count > 0) SubEventServerTopic();
if (Options.EnableConfigStore) SubConfigStoreServerTopic();
return Task.CompletedTask;
};
try
//重连
Client.DisconnectedAsync += async _ =>
{
Connection = factory.CreateConnection();
Logger.LogInformation("连接RabbitMQ服务器成功");
}
catch (BrokerUnreachableException e)
{
Logger.LogError("连接RabbitMQ失败: \n{Err}", e);
}
}
private IModel CreateChannel(ushort processNum = 0, string desc = "unknow")
{
IModel channel = null;
try
{
var log = $"Channel [{desc}] 创建成功...";
channel = Connection.CreateModel();
if (processNum != 0)
logger.LogError("Synapse MQTT[{AppName}:{AppId}] 断开连接,开始重连...", Options.AppName, Options.AppId);
await Task.Delay(TimeSpan.FromSeconds(5));
try
{
channel.BasicQos(0, processNum, false);
log += $"最大处理器数量: {processNum}";
logger.LogInformation("Synapse MQTT[{AppName}:{AppId}] 开始连接MQTT服务器...", Options.AppName, Options.AppId);
Client.ConnectAsync(clientOpts).Wait();
}
Logger.LogInformation(log);
}
catch (ConnectFailureException e)
{
Logger.LogError("Channel [{{Desc}}] 创建失败...\n {0}", e);
}
return channel;
}
private void CheckAndCreateExchange()
{
var channel = CreateChannel(0, "Exchange");
try
{
channel.ExchangeDeclare(Options.SysName, ExchangeType.Topic, true, true, null);
Logger.LogDebug("Register Exchange Success");
}
catch (ConnectFailureException e)
{
Logger.LogError("Failed to declare Exchange.\n {Err}", e);
}
channel.Close();
Logger.LogDebug("Exchange Channel Closed");
catch
{
logger.LogError("Synapse MQTT[{AppName}:{AppId}] 重连失败...", Options.AppName, Options.AppId);
}
};
}
private void ProcessAttribute()
{
EventRegistry = new List<RegisterItem>();
RpcRegistry = new List<RegisterItem>();
var stackTrace = new StackTrace();
var callingMethod = stackTrace.GetFrame(stackTrace.FrameCount - 1).GetMethod();
var assembly = callingMethod.DeclaringType.Assembly;
var types = assembly.GetTypes(); // 获取程序集中的所有类型
var callingMethod = stackTrace.GetFrame(stackTrace.FrameCount - 1)?.GetMethod();
var assembly = callingMethod?.DeclaringType?.Assembly;
var types = assembly!.GetTypes(); // 获取程序集中的所有类型
foreach (var type in types)
{
var methods = type.GetMethods(); // 获取类型中的所有方法
@@ -193,46 +233,93 @@ public partial class Synapse
if (method.IsDefined(typeof(SynapseEventAttribute), false))
{
var attribute =
(SynapseEventAttribute)Attribute.GetCustomAttribute(method, typeof(SynapseEventAttribute));
if (attribute != null)
(SynapseEventAttribute)Attribute.GetCustomAttribute(method, typeof(SynapseEventAttribute))!;
var tmp = new RegisterItem
{
EventRegistry.Add(new RegisterItem
{
Key = attribute.Name ?? method.Name,
Class = type,
Method = method.Name
});
Key = attribute.Name ?? method.Name,
Class = type,
Method = method.Name
};
if (tmp.Key.StartsWith('/') || tmp.Key.EndsWith('/'))
{
logger.LogError("Synapse Event Register Error: {Key} Can't start or end of '/'", tmp.Key);
continue;
}
var callClass = sp.CreateScope().ServiceProvider.GetRequiredService(tmp.Class!);
var mt = callClass.GetType().GetMethod(tmp.Method);
if (mt!.GetParameters().Length > 2 || mt.GetParameters().Length < 1)
{
logger.LogError(
"Synapse Event Register Error: Only one or two parameter supported. {Key} -> {Method}@{Class}",
tmp.Key, tmp.Method, tmp.Class.Name);
continue;
}
EventRegistry.Add(tmp);
}
if (method.IsDefined(typeof(SynapseRpcAttribute), false))
{
var attribute =
(SynapseRpcAttribute)Attribute.GetCustomAttribute(method, typeof(SynapseRpcAttribute));
if (attribute != null)
(SynapseRpcAttribute)Attribute.GetCustomAttribute(method, typeof(SynapseRpcAttribute))!;
var tmp = new RegisterItem
{
RpcRegistry.Add(new RegisterItem
{
Key = attribute.Name ?? $"{type.Name}.{method.Name}",
Class = type,
Method = method.Name
});
Key = attribute.Name ?? $"{type.Name}.{method.Name}",
Class = type,
Method = method.Name
};
if (tmp.Key.Contains('/') || tmp.Key.Contains('#') || tmp.Key.Contains('+'))
{
logger.LogError("Synapse Rpc Register Error: {Key} contains '/' , '#' , '+'", tmp.Key);
continue;
}
var callClass = sp.CreateScope().ServiceProvider.GetRequiredService(tmp.Class!);
var mt = callClass.GetType().GetMethod(tmp.Method);
if (mt!.GetParameters().Length > 2)
{
logger.LogError(
"Synapse Rpc Register Error: Only 1,2 or none parameter supported. {Key} -> {Method}@{Class}",
tmp.Key, tmp.Method, tmp.Class.Name);
continue;
}
if (mt.GetParameters().Length == 2 &&
mt.GetParameters()[1].ParameterType != typeof(Dictionary<string, string>))
{
logger.LogError(
"Synapse Rpc Register Error: RpcMethod Parameter 2 must be Dictionary<string, string>. {Key} -> {Method}@{Class}",
tmp.Key, tmp.Method, tmp.Class.Name);
continue;
}
if (RpcRegistry.Any(x => x.Key == tmp.Key))
{
logger.LogError("Synapse Rpc Register Error: {Key} Already Exists -> {Method}@{Class}", tmp.Key,
tmp.Method, tmp.Class.Name);
continue;
}
RpcRegistry.Add(tmp);
}
}
}
var events = EventRegistry.Aggregate(string.Empty,
(current, ev) => current + $"\n |- {ev.Key} -> {ev.Method}@{ev.Class.Name}");
(current, ev) => current + $"\n |- {ev.Key} -> {ev.Method}@{ev.Class!.Name}");
var rpcList = RpcRegistry.Aggregate(string.Empty,
(current, ev) => current + $"\n |- {ev.Key} -> {ev.Method}@{ev.Class.Name}");
Logger.LogInformation(">>> Synapse System 读取Event方法:{Event}\n>>>Synapse System 读取RPC方法:{Rpc}", events, rpcList);
(current, ev) => current + $"\n |- {ev.Key} -> {ev.Method}@{ev.Class!.Name}");
if (EventRegistry.Count > 0) logger.LogInformation(" >> Synapse System 读取Event方法:{Event}", events);
if (RpcRegistry.Count > 0) logger.LogInformation(" >> Synapse System 读取RPC方法:{Rpc}", rpcList);
}
}
public class RegisterItem
{
public string Key { get; set; }
public string? Key { get; init; }
public Type Class { get; set; }
public Type? Class { get; init; }
public string Method { get; set; }
public string? Method { get; init; }
}