1、前言
为什么说是伪微服务框架,常见微服务框架可能还包括服务容错、服务间的通信、服务追踪和监控、服务注册和发现等等,而我这里为了在使用中的更简单,将很多东西进行了简化或者省略了。
年前到现在在开发一个新的小项目,刚好项目最初的很多功能是比较通用的,所以就想着将这些功能抽离出来,然后做成一个通用的基础服务,然后其他项目可以直接引用这个基础服务,这样就可以减少很多重复的工作了。
我在做的过程中也是参考了公司原有的一个项目,目标是尽量的简单,但是项目搞着搞着就越来越大了,所以我也是在不断的进行简化和优化。
当然我的思考和架构能力还存在很大的问题,另外还由于时间比较仓促,很多东西还没有经过我的深思熟虑,而且现在项目还在初期的开发阶段,问题肯定是有很多的,这里也是希望自己通过整理出来,加深对项目的理解,也希望如果大家能够给我一点指导和建议那就更好了。
总之,后期会慢慢优化和完善这个项目,也会在这里记录下来。后端如果差不多了,就会进行前端项目的开发,然后再进行整合。
直接上github链接:https://github.com/aehyok/NET8.0
现阶段部署的一个单节点的服务:http://101.200.243.192:8080/docs/index.html
2、全文思维导航图
其中列举了我觉得比较重点的一些知识点吧,当然其实还有很多知识点,可能我忽略掉了,后期有时间看到了还会加进来。
3、简单整体框架
首先我将sun.Core作为了中转,其他外部或者自己封装的类库,在引用的时候都是在sun.Core中进行的引用, 算是间接引用,来简化项目中的依赖关系。同时在sun.Core也封装了一些核心组件和服务。
sun.Infrastructure 其中主要封装一些通用的方法,以及基础设施组件,供外部使用。
4、已实现业务功能
这里通常建议使用构造函数注入的方式,而且在.NET8.0中新增加了主构造函数的语法糖,使声明构造函数的参数更加简洁
没有使用主构造函数的方式
public classDictController : BasicControllerBase
{
privatereadonly IDictionaryGroupService dictionaryGroupService;
privatereadonly IDictionaryItemService dictionaryItemService;
publicDictController(IDictionaryGroupService dictionaryGroupService, IDictionaryItemService dictionaryItemService)
{
this.dictionaryGroupService = dictionaryGroupService;
this.dictionaryItemService = dictionaryItemService;
}
使用主构造函数之后的方法,看上去代码就简洁了很多
public class DictionaryController(
IDictionaryGroupService dictionaryGroupService,
IDictionaryItemService dictionaryItemService) : BasicControllerBase
{
}
var token = new UserToken()
{
ExpirationDate = DateTime.Now.AddHours(10),
IpAddress = ipAddress.ToString(),
PlatformType = platform,
UserAgent = userAgent,
UserId = user.Id,
LoginType = LoginType.Login,
RefreshTokenIsAvailable = true
};
token.Token = StringExtensions.GenerateToken(user.Id.ToString(), token.ExpirationDate);
token.TokenHash = StringExtensions.EncodeMD5(token.Token);
token.RefreshToken = StringExtensions.GenerateToken(token.Token, token.ExpirationDate.AddMonths(1));
if (code === ResultEnum.NOT_LOGIN && !res.config.url?.includes("/basic/Token/Refresh")) {
if (!isRefreshing) {
isRefreshing = true;
try {
const { code, data } = await refreshTokenApi({
userId: storage.get(UserEnum.ACCESS_TOKEN_INFO).userId,
refreshToken: storage.get(UserEnum.ACCESS_TOKEN_INFO).refreshToken
});
if (code === ResultEnum.SUCCESS) {
storage.set(UserEnum.ACCESS_TOKEN_INFO, data);
res.config.headers.Authorization = `${data?.token}`;
res.config.url = res.config.url?.replace("/api", "");
// token 刷新后将数组的方法重新执行
requests.forEach((cb) => cb(data?.token));
requests = []; // 重新请求完清空
// @ts-ignore
return http.request(res.config, res.config.requestOptions);
}
} catch (err) {
return Promise.reject(err);
} finally {
isRefreshing = false;
}
}
后端方法的实现则是通过RefreshToken进行确认身份,然后重新生成登录的token和refreshToken,以及重新设置token的过期时间,跟登录时的逻辑是一样的。
7、实现Authentication安全授权
首先在初始化应用程序的时候注册授权认证的中间件
builder.Services.AddAuthentication("Authorization-Token").AddScheme<RequestAuthenticationSchemeOptions, RequestAuthenticationHandler>("Authorization-Token", options => { });
然后来看一下我的RequestAuthenticationHandler具体实现如下
处理认证流程中的一个核心方法,这个方法返回
AuthenticateResult
来标记是否认证成功以及返回认证过后的票据(AuthenticationTicket)。
这样后续便可以通过context.HttpContext.User.Identity.IsAuthenticated 来判断是否已经认证
// 其他需要登录验证的,则通过AuthenticationHandler进行用户认证
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = new RequestJsonResult(new RequestResultModel(StatusCodes.Status401Unauthorized, "请先登录", null));
return;
}
同时通过配置文件的方式,添加多个微服务项目进行切换测试
直接通过以下代码
统一在Header中添加一个Menu-Code的参数
builder.ConfigureAppConfiguration((context, options) =>
{
// 正式环境配置文件路径
options.AddJsonFile(Path.Combine(AppContext.BaseDirectory, $"../etc/appsettings.json"), true, true);
options.AddJsonFile(Path.Combine(AppContext.BaseDirectory, $"../etc/{moduleKey}-appsettings.json"), true, true);
// 本地开发环境配置文件路径
options.AddJsonFile(Path.Combine(AppContext.BaseDirectory, $"../../../../../../etc/appsettings.json"), true, true);
options.AddJsonFile(Path.Combine(AppContext.BaseDirectory, $"../../../../../../etc/{moduleKey}-appsettings.json"), true, true);
});
其中还可以对日志封装一些特殊字段,方便查看日志,定位问题的字段。例如下面我封装了三个特殊字段
IpAddressEnricher 在日志中记录请求的 IP 地址
TokenEnricher 将TokenId写入日志
WorkerEnricher 将配置文件中的WorkId写入日志
然后遍可以在seq可视化平台进行查看定位问题
实现IAsyncExceptionFilter接口,统一记录错误日志,以及统一返回前端错误
11、 通过实现过滤器IAsyncActionFilter结合反射来记录操作日志,并通过请求头中的Menu-Code来辨别具体接口
直接看一下对过滤器IAsyncActionFilter的实现
姑且有关RabbitMQ的内容我下面会继续记录,这里暂时就点到为止。
12、通过实现IAsyncAuthorizationFilter来验证用户身份,并判断接口访问的权限
先看一下对IAsyncAuthorizationFilter接口的实现
///<summary>
/// 请求接口权限过滤器而AuthenticationHandler则是用户认证,token认证
///</summary>
public class RequestAuthorizeFilter(IPermissionService permissionService) : IAsyncAuthorizationFilter
{
publicasync Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
// 接口标记了[AllowAnonymous],则不需要进行权限验证
if (context.ActionDescriptor.EndpointMetadata.Any(a => a.GetType() == typeof(AllowAnonymousAttribute)))
{
return;
}
// 其他需要登录验证的,则通过AuthenticationHandler进行用户认证
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = new RequestJsonResult(new RequestResultModel(StatusCodes.Status401Unauthorized, "请先登录", null));
return;
}
if (context.ActionDescriptor is not null && context.ActionDescriptor is ControllerActionDescriptor descriptor)
{
var namespaceStr = descriptor.ControllerTypeInfo.Namespace;
var controllerName = descriptor.ControllerName;
var actionName = descriptor.ActionName;
var code = $"{namespaceStr}.{controllerName}.{actionName}";
var menuCode = string.Empty;
if (context.HttpContext.Request.Headers.ContainsKey("Menu-Code") && !string.IsNullOrEmpty(context.HttpContext.Request.Headers["Menu-Code"]))
{
menuCode = context.HttpContext.Request.Headers["Menu-Code"].ToString();
}
// 通过menuCode找到菜单Id,通过code找到接口Id
var hasPermission = false;
//有些操作是不在菜单下面的,则默认有访问接口的权限
if (string.IsNullOrEmpty(menuCode))
{
hasPermission = true;
}
hasPermission = await permissionService.JudgeHasPermissionAsync(code, menuCode);
if (hasPermission)
{
return;
}
context.Result = new RequestJsonResult(new RequestResultModel(StatusCodes.Status403Forbidden, "暂无权限", null));
await Task.CompletedTask;
}
}
}
}
// 比如直接return Ok();
elseif(context.Result is StatusCodeResult statusCodeResult)
{
var resultModel = new RequestResultModel
{
Code = statusCodeResult.StatusCode,
Message = statusCodeResult.StatusCode == 200 ? "Success" : "请求发生错误",
Data = statusCodeResult.StatusCode == 200
};
context.Result = new RequestJsonResult(resultModel);
}
elseif(context.Result is ObjectResult result)
{
if(result.Value isnull)
{
var resultModel = new RequestResultModel
{
Code = result.StatusCode ?? context.HttpContext.Response.StatusCode,
Message = "未请求到数据"
};
context.Result = new RequestJsonResult(resultModel);
}
elseif(result.Value is not RequestJsonResult)
{
if (result.Value is IPagedList pagedList)
{
var resultModel = new RequestPagedResultModel
{
Message = "Success",
Data = result.Value,
Total = pagedList.TotalItemCount,
Page = pagedList.PageNumber,
TotalPage = pagedList.PageCount,
Limit = pagedList.PageSize,
Code = result.StatusCode ?? context.HttpContext.Response.StatusCode
};
context.Result = new RequestJsonResult(resultModel);
}
else
{
var resultModel = new RequestResultModel
{
Code = result.StatusCode ?? context.HttpContext.Response.StatusCode,
Message = "Success",
Data = result.Value
};
context.Result = new RequestJsonResult(resultModel);
}
}
}
await next();
}
}
主要就是三种情况
请求参数验证错误的返回提示
正常返回例如详情的结果数据
单独针对分页数据的返回
这样前端也可以更好的根据情况进行封装统一,便于维护的代码
14、初始化EFCore,并实现Repository仓储模式
16、引入Redis统一封装实现分布式缓存和分布式锁
所使用的开源类库:https://github.com/2881099/csredis
目前主要封装了几个常用的接口方法
https://www.redis.net.cn/order/3552.html
17、引入RabbitMQ统一封装实现异步任务,例如上传和下载文件等
暂时只使用了direct模式,根据routingKey和exchange决定的那个唯一的queue可以接收消息。
定义好要传输的消息实体,发布消息,然后RabbitMQ通用方法收到消息后会进行处理,然后交给指定的处理器
直接实现IEventHandler,这个T便是AsyncTaskEventData,根据需要进行定义就好了。
// 发布任务
publisher.Publish(new AsyncTaskEventData(task));
后面搞前端的时候顺便加上定时任务的是否启用,以及可以在线修改表达式,也就是修改定时任务的执行时间。
19、通过BackgroundService实现数据的初始化服务,例如字典数据等
上面是通用的定时任务执行。这里主要就是根据BackgroundService来初始化或更新一些数据,例如 字典项、初始化区域、初始化角色等等
这是一个通用的初始化数据的执行器,然后可以单独进行实现每个想要初始化的数据执行器
可以对执行进行设置顺序,因为有些数据是有依赖的。
这里可以看到上面的定时任务列表,我就是通过这里实现的初始化数据
其中里面用到了反射来读取类的信息。
20、通过BackgroundService和反射实现所有接口的写入数据库
程序中所有的接口列表,我也是在这里进行单独初始化的,通过类似反射来读取项目中的所有接口,来初始化到数据库中,然后在程序中进行使用的。
21、引入EPPlus实现Excel的导入和导出
所使用的开源类库:https://github.com/EPPlusSoftware/EPPlus
统一封装关于Excel导入导出中的通用方法。
22、goploy一键部署前后端项目
所使用的开源类库:https://github.com/zhenorzz/goploy 部署其实也非常简单的,能通过脚本使用的,便可以在工具上进行设置,然后点一下就可以进行一键部署,当然了还需要服务器的支持了。
同时我也将.net8的后端部署为本地宿主的服务也是没问题的
这是部署后进行查看服务状态的,通过一个命令便可以查看三个服务的状态
systemctl status sun-*,同样也可以一起重启和关闭服务
24、docker一键部署后端项目
写了个脚本和Dockerfile文件,可单独更新某个服务,也可以三个服务一起更新。
同样我现在开发使用的Mysql、Redis、RabbitMQ、Seq、等等也可以通过docker进行运行,很湿方便啊。
25、总结
经过这段时间的项目实践,也学到了非常多的知识,同时也发现了一些自身的问题。同时也发现现有项目中方方面面如果再有一个月的时间,很多代码可以做一波新的优化和重写。
后面有时间我还会整理一套简易的微前端框架,同时要将后端的大部分接口进行实现, pnpm + vue3 + vite5 + wujie 微前端。
转自:aehyok
链接:cnblogs.com/aehyok/p/18058032
- EOF -
推荐阅读 点击标题可跳转
看完本文有收获?请转发分享给更多人
推荐关注「DotNet」,提升.Net技能
点赞和在看就是最大的支持❤️