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技能
點贊和在看就是最大的支持❤️