Net8微服务实战

前言

学习杨中科老师开源项目在线英语网站微服务

1.需求

服务拆分

image-20240413213531990

image-20240419234736153

2.项目源码

项目 说明
Peng.ASPNETCore DistributedCacheHelper 分布式缓存帮助类
MemoryCacheHelper 内存缓存帮助类
UnitOfWorkFilter 工作单元筛选器
Peng.Commons Validators文件夹 FluentValidation的扩展类
LoggerExtensions 使用FormattableString简化日志的代码
ModuleInitializerExtensions 把服务注册代码放到各自的项目中
Peng.DomainCommons IAggregateRoot 聚合根标识接口
BaseEntity、AggregateRootEntity 领域事件的发布
MultilingualString 多语言值对象
Peng.EventBus 集成事件总线
Peng.Infrastructure BaseDbContext 领域事件的发布
EFCoreInitializerHelper 上下文的自动化注册
ExpressionHelper 简化值对象的相等性比较
MediatorExtensions 领域事件的注册
MultilingualStringEFCoreExtensions 多语言值对象的配置
Peng.JWT 使用JWT实现登录令牌

3.运行

3.1环境搭建

需要什么就装什么

https://www.cnblogs.com/pengboke/p/18156610

3.2升级Net8

所有程序都升级为Net8

创建Net8框架程序,然后把代码复制粘贴,升级对应的NuGet包

image-20240415224859985

	<PropertyGroup>
		<TargetFramework>net8.0</TargetFramework>
		<ImplicitUsings>enable</ImplicitUsings>
		<Nullable>enable</Nullable>
	</PropertyGroup>

3.3appsettings

这里的配置文件我都换成了本地配置

image-20240415221406651

 "Cors": {
   "Origins": [ "http://localhost:3000", "http://localhost:3001" ]
 },
 "FileService": {
   "SMB": {
     "WorkingDir": "e:/temp/upload"
   },
   "UpYun": {

   },
   "EndPoint": {

   }
 },
 "ConnectionStrings": {
   "Connection": "Server=localhost;Database=Peng-VNext;charset=utf8;uid=root;pwd=root;port=3306;"
 },
 "Redis": {
   "ConnStr": "localhost"
 },
 "JWT": {
   "Issuer": "myIssuer",
   "Audience": "myAudience",
   "Key": "pengpeng@123456789abcdefghijklmnopq",
   "ExpireSeconds": "31536000"
 },
 "RabbitMQ": {
   "HostName": "127.0.0.1",
   "ExchangeName": "peng_event_bus",
   "UserName": "guest",
   "Password": "guest"
 },
 "ElasticSearch": { "Url": "http://elastic:OXanY7JrEElR--N0Fx4z@localhost:9200" }

3.4数据库冲突

注释一下代码:

WebApplicationBuilderExtensions

image-20240415221547291

添加数据库配置

builder.Services.AddDbContext<IdDbContext>(options =>
{
    string connstr = builder.Configuration.GetValue<string>("ConnectionStrings:Connection");
    options.UseMySql(connstr, ServerVersion.AutoDetect(connstr));
});

image-20240415221645001

3.5IMediatR冲突

注释WebApplicationBuilderExtensions的IMediatR注入

image-20240415221838013

IMediatR升级Net8后,没有这个方法了。

image-20240415222032222

3.6Nginx配置

我这里用的是8000端口

主要是配置Server

image-20240415222340101

server {
        listen       8000;
        server_name  localhost;

        location /FileService/ {
			proxy_pass http://localhost:50401/;
			proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Real-PORT $remote_port;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto  $scheme;
            client_max_body_size 100m;
		}
        
        location /IdentityService/ {
			proxy_pass  http://localhost:50402/;
			proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Real-PORT $remote_port;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto  $scheme;
		}
		
		location /Listening.Admin/ {
			proxy_pass http://localhost:50403/;
			proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Real-PORT $remote_port;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto  $scheme;
			proxy_http_version 1.1;
			proxy_set_header Upgrade $http_upgrade;
			proxy_set_header Connection "upgrade";
		}

		location /Listening.Main/ {
			proxy_pass http://localhost:50404/;
			proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Real-PORT $remote_port;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;			
            proxy_set_header X-Forwarded-Proto  $scheme;
		}			
		
		location /MediaEncoder/ {
			proxy_pass http://localhost:50405/;
			proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Real-PORT $remote_port;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;	
            proxy_set_header X-Forwarded-Proto  $scheme;		
		}

		location /SearchService/ {
			proxy_pass http://localhost:50406/;
			proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Real-PORT $remote_port;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto  $scheme;
		}

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

        location / {
            root   html;
            index  index.html index.htm;
        }   
    }

3.7数据迁移

3.7.1IdentityService

IdentityService.WebAPI安装

<ItemGroup>
	<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4">
		<PrivateAssets>all</PrivateAssets>
		<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
	</PackageReference>
	<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
	<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
</ItemGroup>

image-20240415231829436

IdentityService.Infrastructure安装8.0的Mysql包

  <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />

image-20240415223057854

IdentityService.Domain

image-20240416001519950

DesignTimeDbContextFactory,有些代码没看明白,先固定,保证运行

image-20240416001411817

没有报错运行一下代码,正常跑起来可以用迁移命令

迁移命令

Add-Migration init
Update-Database init

image-20240415222932044

迁移成功,数据库已更新

image-20240415222915328

创建一个管理员

image-20240415223316003

登录成功,认证服务可以正常运行了。

image-20240415224736448

3.7.2FileService

FileService.WebAPI安装包并且注释全球化

<ItemGroup>
	<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.4">
		<PrivateAssets>all</PrivateAssets>
		<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
	</PackageReference>
	<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
	<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
</ItemGroup>

image-20240415232544073

FileService.Infrastructure

	<ItemGroup>
		<FrameworkReference Include="Microsoft.AspNetCore.App" />
		<ProjectReference Include="..\FileService.Domain\FileService.Domain.csproj" />
		<ProjectReference Include="..\Peng.Infrastructure\Peng.Infrastructure.csproj" />
	</ItemGroup>

	<ItemGroup>	
		<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
		<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
		<PackageReference Include="UpYun.NETCore" Version="1.1.0" />
	</ItemGroup>

image-20240415231102806

注入DB服务

//db
builder.Services.AddDbContext<FSDbContext>(options =>
{
    string connstr = builder.Configuration.GetValue<string>("ConnectionStrings:Connection");
    options.UseMySql(connstr, ServerVersion.AutoDetect(connstr));
});

image-20240415232111642

appsettings

image-20240415232233756

数据迁移

Add-Migration init
Update-Database init

image-20240415232653848

3.7.3ListeningService

Listening.Domain

image-20240415235659905

Listening.Infrastructure

image-20240415235726508

Listening.Main.WebAPI

image-20240415235755730

Listening.Admin.WebAPI

image-20240415235823978

注入DB服务

image-20240415235932066

appsettings

image-20240416000016612

没有报错运行一下代码,正常跑起来使用迁移命令

Add-Migration init
Update-Database init

image-20240416000122892

3.7.4MediaEncoderService

MediaEncoder.Domain

image-20240416002921841

MediaEncoder.Infrastructure

image-20240416002951116

MediaEncoder.Infrastructure

image-20240416003013945

DesignTimeDbContextFactory,有些代码没看明白,先固定,保证运行

image-20240416003038312

注入DB服务,然后注释掉builder.Services.AddHostedService,等数据库迁移完成再放开即可

image-20240416153951872

appsettings

image-20240416003203417

没有报错运行一下代码,正常跑起来使用迁移命令

Add-Migration init
Update-Database init

image-20240416003245150

3.7.5SearchService.WebAPI

SearchService.Domain

image-20240416004615476

SearchService.Infrastructure

image-20240416004632762

SearchService.WebAPI

image-20240416004641628

appsettings

image-20240416004705451

3.7.6前端

淘宝镜像过期

报错:

npm ERR! request to https://registry.npm.taobao.org/XXX failed, reason: certificate has expired

更新淘宝镜像

# 清楚缓存
npm cache clean --force
# 获取镜像地址
npm config get registry
# 设置淘宝镜像地址
npm config set registry https://registry.npmmirror.com

yarn

# 设置yarn的strict-ssl
yarn config set "strict-ssl" false -g
# 安装yarn
npm install -g yarn
# 运行
yarn dev

image-20240416175635070

修改前端端口号

端口号为nginx所配置的

image-20240416203930145

3.7.8运行后端

修改后端启动端口

FileService.WebAPI:http://localhost:50401/
IdentityService.WebAPI:http://localhost:50402/
Listening.Admin.WebAPI:http://localhost:50403/
Listening.Main.WebAPI:http://localhost:50404/
MediaEncoder.WebAPI:http://localhost:50405/
SearchService.WebAPI:http://localhost:50406/

image-20240416204122770

3.7.9完成

到此所有服务基本可以跑起来了,但是基于Net8有很多不兼容,后面慢慢学习慢慢优化。

4.文件服务

清空表数据

truncate table T_Episodes;
truncate table T_FS_UploadedItems;

添加专辑AlbumAddRequestValidator

 RuleFor(x => x.CategoryId).Must((cId, ct) => dbCtx.Query<Category>().Any(c => c.Id == cId.CategoryId)).WithMessage(c => $"CategoryId={c.CategoryId}不存在");

image-20240416215239459

添加EpisodeAddRequestValidator

  RuleFor(x => x.AlbumId).Must((cId, ct) => ctx.Query<Album>().Any(c => c.Id == cId.AlbumId))
      .WithMessage(c => $"AlbumId={c.AlbumId}不存在");

image-20240416221235074

修改端口号,我没搞明白为什么http://localhost可以访问,这里需要加上nginx端口号进行转发,实际的文件服务地址端口号应该也可以。

image-20240416222153405

上传

image-20240416222005546

这样我的文件服务可以正常上传了,下面梳理下逻辑

上传主要是是上传到云服务器和备份服务器

image-20240416223009912

MockCloudStorageClient是模拟云服务器,文件保存在wwwroot文件夹下。

image-20240416223111128

SMBStorageClient是备份服务器,保存在配置的e:/temp/upload

image-20240416223205830

5.认证服务

使用IdentityServer4组件,User和Role

image-20240416225726604

IIdRepository认证服务仓储

image-20240416225838415

实现手机和邮箱发送验证码

image-20240416225918162

创建用户和重置密码的时候发布领域事件

可以同时或者选择性的把新增用户的密码短信/邮件/打印给用户

体现了领域事件对于代码“高内聚、低耦合”的追求

image-20240416230329023

认证服务主要分为

  • LoginController:登录功能

  • UserAdminController:用户的基本CRUD功能

image-20240417191925209

6.听力服务

分类(Category)、专辑(Album)、音频(Episode)3个实体。由于我们可以直接访问某个专辑或者某个音频,因此我们这里把这3个实体放到3个聚合之中。

image-20240417200416576

建造者模式使用链式调用保证创建出来的Episode都是合法的。

把许多构造函数复制变成方法赋值看起来更清晰。

参数的合法性校验也放到Build中。

var builder = new Episode.Builder();
builder.Id(id).SequenceNumber(maxSeq + 1).Name(name).AlbumId(albumId)
     .AudioUrl(audioUrl).DurationInSecond(durationInSecond)
     .SubtitleType(subtitleType).Subtitle(subtitle);

image-20240417200544626

字幕解析防腐层

ISubtitleParser:字幕解析接口。所有的字幕解析实现继承该接口。
JsonParser:Json字幕解析方式。
LrcParser:Lrc字幕解析方式。
SrtParser:Srt字幕解析方式。
SubtitleParserFactory:创建解析对象,通过传入名称生成解析对象。

image-20240417200951849

听力服务的仓储接口IListeningRepository

image-20240417201306616

听力服务的领域服务ListeningDomainService

image-20240417201341414

Listening.Admin.WebAPI(听力后台服务):CategoryController、AlbumController、EpisodeController分别提供了对分类、专辑、音频进行管理的接口。

image-20240417204238972

DDD原则:一个实体不应该有处于不完整状态的时刻,这样就能减少程序中对于不完整状态进行处理的代码。因此:一个没有完成转码的Episode对象就不应该存在于数据库中。

所以不通过IsReady来保存音频状态。

保存音频的时候,先把音频实体放到Redis缓存,然后发布集成事件通知转码服务进行转码。

image-20240417204337209

需要转码的音频保存到Redis,转码完成后,程序再把Redis中暂存的音频数据保存到数据库中。

Episode对象一直处于完整状态,也就是数据库中的音频记录一定是可用的,代码就简单很多了。这就是DDD给系统设计带来的一个很大的好处。

image-20240417204716679

转码服务会在转码开始、转码失败、转码完成等事件出现的时候,发布名字分别为MediaEncoding.Started、MediaEncoding.Failed、MediaEncoding.Completed等集成事件,因此我们只要监听这些集成事件,然后把转码状态的变化推送到前端页面即可,然后在更新Redis。

image-20240417205130700

领域事件转换为集成事件:

EpisodeCreatedEventHandler:音频添加,发布事件。
EpisodeDeletedEventHandler:音频删除,发布事件。
EpisodeUpdatedEventHandler:音频修改,发布事件。

image-20240417205513418

查询分类、专辑、音频的时候使用了MemoryCache缓存。

Redis分布式缓存都缓存在Redis服务器上,而MemoryCache缓存在内存上,这里没有多少服务器而且数据量不大更适合MemoryCache缓存。

还使用了local函数(本地函数),本地函数是一种嵌套在另一成员中的类型的方法。 仅能从其包含成员中调用它们。使代码结构更清晰。

image-20240417212925009

前端显示字幕:根据audio标签的timeupdate事件获取标签进度的时间,然后再去sentences(字幕列表)根据startTime和endTime过滤。

 const querySentence=(position)=>
    {
        const sentences = state.episode.sentences;
        for (var i = 0; i < sentences.length; i++)
        {
            var sentence = sentences[i];
            if (position >= sentence.startTime && position <= sentence.endTime)
            {
                return sentence;
            }
        }              
    };
 const updateCurrentSentence=()=>{
        var position = mainPlayer.value.currentTime;
        var foundSentence = querySentence(position);
        if (foundSentence && !sentenceEqual(foundSentence,state.currentSentence))
        {
            state.currentSentence = foundSentence;
        }    
    };

音频标签

image-20240417220523370

字幕列表

image-20240417220920019

7.转码服务

目前只需要m4a转码,这和听力服务的字幕解析模块类似。

IMediaEncoder:转码接口。所有的转码服务实现继承该接口。
ToM4AEncoder:M4A转码服务。
MediaEncoderFactory:创建转码服务对象,通过传入名称生成解析对象。

image-20240417221713342

IMediaEncoderRepository:转码服务仓储

image-20240417221333284

M4A转码服务实现

使用第三方FFmpeg.NET包(也可以通过命令行(cmd)直接调用ffmpeg.exe)

var ffmpeg = new Engine(ffmpegPath);
string? errorMsg = null;
ffmpeg.Error += (s, e) =>
{
    errorMsg = e.Exception.Message;
};
await ffmpeg.ConvertAsync(inputFile, outputFile, ct);//进行转码
if (errorMsg != null)
{
    throw new Exception(errorMsg);
}

image-20240417221816327

启动后台服务,并创建一个死循环,5s一次获取Redis中需要执行的转码任务。

image-20240417222035432

首先使用RedLockNet.SERedis实现Redis分布式锁,保证任务同一个任务不会被多个转码服务执行。

获取到任务后开始执行,更改并保存任务状态为开始。

然后下载到转码服务器临时文件夹。

image-20240417222403735

声明转码后的文件信息。

计算下载后的文件散列值(相同文件的散列值一样,不一样的概率很低),然后根据文件散列值个文件大小查询数据库是否存在对应的M4A文件,存在就不转码,更新到音频表。

然后使用EncoderFactory创建ToM4AEncoder转码对象进行转码,转码成功发布集成事件和领域事件。

image-20240417223026154

8.搜索服务

搜索服务没什么要说的,本来想通过最新的Elastic.Clients.Elasticsearch实现,但是查询高亮会有一些问题,还是退回NEST。还是留一下Elasticsearch的安装,不过有坑的我都记录下来了。

image-20240419235041500

前端

image-20240419235304900

posted @ 2024-04-24 23:34  peng_boke  阅读(1183)  评论(1编辑  收藏  举报