Abp|Abp Vnext Vue3 的版本实现

基于ABP Vnext的二次开发,前端 vue3.0,Typescript,Ant Design Vue ,Vben Admin 的后台管理框架.
技术点

  • Net Core5.0
  • ABP Vnext
  • Vben Admin
  • Redis
  • MySql
  • RabbitMq
  • Hangfire
  • DDD
  • IdentityServer4
  • dotnetcore.CAP
  • Ocelot
  • Consul
开始 项目结构
后端
. ├── Directory.Build.props nuget 版本控制 ├── frameworks # 公共模块 │├── CAP # dotnetcore.cap │└── Extensions # 自定义扩展 ├── gateways # 网关 ├── modules # 模块 │├── DataDictionaryManagement # 数据字典 │└── NotificationManagement # 通知服务 ├── services # 公共静态资源目录 │├── host # 启动模块 │├── CompanyName.ProjectName.HttpApi.Host # admin ui host │└── CompanyName.ProjectName.IdentityServer # IdentityServer host │├── src# 源码 │└── CompanyName.ProjectName.DbMigrator # 迁移控制台程序 │└── test # 单元测试

前端
. ├── _nginx # docker 打包 ├── build # 打包脚本相关 │├── config # 配置文件 │├── generate # 生成器 │├── script # 脚本 │└── vite # vite配置 ├── mock # mock文件夹 ├── public # 公共静态资源目录 ├── src # 主目录 │├── api # 接口文件 │├── assets # 资源文件 ││├── icons # icon sprite 图标文件夹 ││├── images # 项目存放图片的文件夹 ││└── svg # 项目存放svg图片的文件夹 │├── components # 公共组件 │├── design # 样式文件 │├── directives # 指令 │├── enums # 枚举/常量 │├── hooks # hook ││├── component # 组件相关hook ││├── core # 基础hook ││├── event # 事件相关hook ││├── setting # 配置相关hook ││└── web # web相关hook │├── layouts # 布局文件 ││├── default # 默认布局 ││├── iframe # iframe布局 ││└── page # 页面布局 │├── locales # 多语言 │├── logics # 逻辑 │├── main.ts # 主入口 │├── router # 路由配置 │├── services # Nswag生成的代理 ││├── ServiceProxies.ts # Nswag生成的代理 ││├── ServiceProxyBase.ts # Nswag生成的代理拦截器 │├── settings # 项目配置 ││├── componentSetting.ts # 组件配置 ││├── designSetting.ts # 样式配置 ││├── encryptionSetting.ts # 加密配置 ││├── localeSetting.ts # 多语言配置 ││├── projectSetting.ts # 项目配置 ││└── siteSetting.ts # 站点配置 │├── store # 数据仓库 │├── utils # 工具类 │└── views # 页面 ├── test # 测试 │└── server # 测试用到的服务 │├── api # 测试服务器 │├── upload # 测试上传服务器 │└── websocket # 测试ws服务器 ├── types # 类型文件 ├── vite.config.ts # vite配置文件 └── windi.config.ts # windcss配置文件

运行项目前提
  • Mysql
    docker run --name mymysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=1q2w3E* -d mysql:5.7 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci

  • Redis
    docker run --name myredis -p 6379:6379 -d redis:latest redis-server

  • RabbitMq 非必须
  • appsetting.development.json-> CAP:Enabled 设置为 false
    docker run -d --name myrabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 rabbitmq:management

  • ELK 非必须
  • appsetting.development.json-> LogToElasticSearch:Enabled 设置为 false
  • 安装 Node.js, Npm Or Yarn
获取项目
  • 直接 clone 项目
git clone https://github.com/WangJunZzz/abp-vnext-pro.git

OR
  • 下载代码生成器
git clone https://github.com/WangJunZzz/abp-vnext-pro-gui.git

  • 下载代码生成生成器之后,输入自己想要的项目名称生成代码即可
启动
  • 修改 HttpApi.Host-> appsettings.development.json 的数据库连接字符串,Redis, RabbitMq,Es 地址即可(如果没有 es 也可以运行,只是前端 es 日志页面无法使用而已,不影响后端项目启动)
  • 修改 IdentityServer-> appsettings.development.json 数据库连接字符串
  • 修改 DbMigrator-> appsettings.json 数据库连接字符串
  • 运行 DbMigrator 生成数据库
  • 启动 HttpApi.Host 和 IdentityServer
  • 前端 yarn 之后,执行 npm run dev 启动
配置说明
  • HttpApi.Host-> appsettings.development.json
{ // Serilog 日志配置,生成环境修改日志级别 "Serilog": { "MinimumLevel": { "Default": "Information", "Override": { "Microsoft": "Information", "Volo.Abp": "Information", "Hangfire": "Information", "DotNetCore.CAP": "Information", "Serilog.AspNetCore": "Information" } } }, // 跨域设置 "App": { "CorsOrigins": "https://*.ProjectName.com,http://localhost:4200,http://localhost:3100" }, // 数据库连接字符串,修改为你本地的mysql地址 "ConnectionStrings": { "Default": "Data Source=localhost; Database=CompanyNameProjectNameDB; uid=root; pwd=1q2w3E*; charset=utf8mb4; Allow User Variables=true; AllowLoadLocalInfile=true" }, // Redis缓存 "Cache": { "Redis": { "ConnectionString": "localhost", "Password": "mypassword", "DatabaseId": 0 } }, // Jwt配置 "Jwt": { "Audience": "CompanyNameProjectName", //客户端标识 "SecurityKey": "dzehzRz9a8asdfasfdadfasdfasdfafsdadfasbasdf=", "Issuer": "CompanyNameProjectName", //签发者 "ExpirationTime": 24 //过期时间 hour }, // 使用了Dotnetcore.cap的rabbitmq,false的情况基于内存 "Cap": { "Enabled": "false", "RabbitMq": { "HostName": "localhost", "UserName": "admin", "Password": "admin" } }, // es日志地址配置 "LogToElasticSearch": { "Enabled": "true", "ElasticSearch": { "Url": "http://es.cn", "IndexFormat": "companyname.projectname.development", "UserName": "elastic", "Password": "aVVhjQ95RP7nbwNy", "DashboardIndex": "companyname.projectname" } }, // identityserver地址 "HttpClient": { "Sts": { "Url": "http://localhost:44354" } }, // Consul 服务发现和治理 "Consul": { "Enabled": false, "Host": "http://localhost:8500", "Service": "Project-Service" } }

  • IdentityServer-> appsettings.development.json
{ "App": { "SelfUrl": "https://localhost:44354", "ClientUrl": "http://localhost:4200", "CorsOrigins": "https://*.ProjectName.com,http://localhost:4200,https://localhost:44307,https://localhost:44315", "RedirectAllowedUrls": "http://localhost:4200,https://localhost:44307" }, // mysql连接字符串 "ConnectionStrings": { "Default": "Data Source=localhost; Database=CompanyNameProjectNameDB; uid=root; pwd=1q2w3E*; charset=utf8mb4; Allow User Variables=true; AllowLoadLocalInfile=true" }, // Redis "Redis": { "Configuration": "localhost,password=mypassword" } }

  • DbMigrator-> appsettings.json
// 迁移数据库 "ConnectionStrings": { "Default": "Data Source=localhost; Database=CompanyNameProjectNameDB; uid=root; pwd=1q2w3E*; charset=utf8mb4; Allow User Variables=true; AllowLoadLocalInfile=true" }

前端
  • 前端采用 TypeScript,所有的类型动态生成 NSwag
  • 后端 api 统一使用 Post
  • 定义 api 格式
// 一定要打Tags,因为前端会根据这个生成代理类 // 建议参数都封装为一个Input [SwaggerOperation(summary: "登录", Tags = new[] {"Account"})] public Task LoginAsync(LoginInput input) { return _loginAppService.LoginAsync(input); }

  • 在前端目录下配置代理的地址
    • nswag->nswag.json
"documentGenerator": { "fromDocument": { "url": "http://localhost:44315/swagger/v1/swagger.json", // 代理地址,只有生成的时候用,不区分环境 } }

  • 如果接口参数或者返回值有改变,需要重新生成代理,执行:
npm run nswag

  • 前端多环境,.env.development 和.env.production
    • 接口地址配置 VITE_API_URL
    • IdentityServer 地址配置 VITE_AUTH_URL
  • 【Abp|Abp Vnext Vue3 的版本实现】权限配置
  • 菜单权限
    • src/router/routes
      policy 字段匹配后端的权限名称
    • 按钮权限
      v-auth="'AbpIdentity.Users.Delete'"
健康检查 模块 用户管理
  • 提供原始登录和第三方登录(IdentityServer4),默认用户名密码:admin 1q2w3*
角色管理
  • 权限定义(Application.Contracts 层)
  • Abp 会自动扫描继承 PermissionDefinitionProvider
  • 文档 Abp 官方
  • 在 Http.Api 的 Controller 打上 Authorize
设置管理
  • 集成Abp.SettingUi
消息通知
  • 消息类型,发送给指定人和广播消息
  • 发送消息到前端,通过集成事件和 RabbitMq
  • 注入 NotificationManager 发送消息,
/// /// 发送普通文本消息 /// /// /// public async Task SendCommonTextAsync(string title, string content, List receiveIds) { if (receiveIds is {Count: 0}) { throw new NotificationManagementDomainException("消息接收人不能为空"); var senderId = Guid.Empty; if (_currentUser?.Id != null) { senderId = _currentUser.Id.Value; var entity = new Notification(GuidGenerator.Create(), title, content, MessageType.Text, senderId); foreach (var item in receiveIds) { entity.AddNotificationSubscription(GuidGenerator.Create(), item); var notificationEto = ObjectMapper.Map(entity); // 发送集成事件 entity.AddCreatedNotificationDistributedEvent(new CreatedNotificationDistributedEvent(notificationEto)); return entity = await _notificationRepository.InsertAsync(entity); }

  • Handler 当前事件:NotificationCreatedDistributedEventHandler
/// /// 发送消息 /// public async Task SendMessageAsync(string title, string content, MessageType messageType, List users) { switch (messageType) { case MessageType.Text: await SendMessageToClientByUserIdAsync(new SendNotificationDto(title, content, messageType), users); break; case MessageType.BroadCast: await SendMessageToAllClientAsync(new SendNotificationDto(title, content, messageType)); break; default: throw new UserFriendlyException("未知的消息类型"); } }

  • 前端接受 SignalR 消息
// src/hooks/web/useSignalR.js import * as signalR from "@microsoft/signalr"; import { useMessage } from "/@/hooks/web/useMessage"; import { useUserStoreWithOut } from "/@/store/modules/user"; export function useSignalR() { /** * 开始连接SignalR */ function startConnect(): void { let connection = connectionsignalR(); //接收普通文本消息 connection.on("ReceiveTextMessageAsync", ReceiveTextMessageHandlerAsync); //接收广播消息 connection.on("ReceiveBroadCastMessageAsync", ReceiveBroadCastMessageHandlerAsync); //开始连接 connection.start(); }/** * 连接signalr */ function connectionsignalR(): signalR.HubConnection { const userStore = useUserStoreWithOut(); const token = userStore.getToken; const url = (import.meta.env.VITE_WEBSOCKE_URL as string) + "/ws/signalr/notification"; const connection = new signalR.HubConnectionBuilder() .withUrl(url, { accessTokenFactory: () => token, skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets, }) .withAutomaticReconnect({ nextRetryDelayInMilliseconds: (retryContext) => { //重连规则:重连次数<300:间隔1s; 重试次数<3000:间隔3s; 重试次数>3000:间隔30s let count = retryContext.previousRetryCount / 300; if (count < 1) { //重试次数<300,间隔1s return 1000; } else if (count < 10) { //重试次数>300:间隔5s return 1000 * 5; } //重试次数>3000:间隔30s else { return 1000 * 30; } }, }) .configureLogging(signalR.LogLevel.Debug) .build(); return connection; }/** * 接收文本消息 * @param message 消息体 */ function ReceiveTextMessageHandlerAsync(message: any) { console.log(message); const { notification } = useMessage(); notification.open({ message: message.title, description: message.content, }); }/** * 接收广播消息 * @param message 消息体 */ function ReceiveBroadCastMessageHandlerAsync(message: any) { const { notification } = useMessage(); notification.open({ message: message.title, description: message.content, }); }return { startConnect }; }

审计日志
  • 参考 Abp 官方文档即可
ES 日志
  • 在 appsetting.development.json 设置是否开启
"LogToElasticSearch": { "Enabled": "false", // 如果为fasel,日志也会写入到本地,安装ELK,参考上面的docker-compose "ElasticSearch": { "Url": "http://es.cn", "IndexFormat": "companyname.projectname.development", "UserName": "elastic", "Password": "aVVhjQ95RP7nbwNy", "DashboardIndex": "companyname.projectname" } },

后台任务
  • 定时任务
public override void OnPostApplicationInitialization(ApplicationInitializationContext context) { context.CreateRecurringJob(); base.OnPostApplicationInitialization(context); }

  • 延迟任务: 官方文档
集成事件
  • 集成 dotnetcore.CAP
  • 在 appsetting.development.json 设置是否开启
"Cap": { "Enabled": "false", //如果为false 默认使用内存级别的队列,否则请安装rabbitmq "RabbitMq": { "HostName": "localhost", "UserName": "admin", "Password": "admin" } },

private void ConfigurationCap(ServiceConfigurationContext context) { var configuration = context.Services.GetConfiguration(); var enabled = configuration.GetValue("Cap:Enabled", false); if (enabled) { context.AddAbpCap(capOptions => { capOptions.UseEntityFramework(); capOptions.UseRabbitMQ(option => { option.HostName = configuration.GetValue("Cap:RabbitMq:HostName"); option.UserName = configuration.GetValue("Cap:RabbitMq:UserName"); option.Password = configuration.GetValue("Cap:RabbitMq:Password"); }); var hostingEnvironment = context.Services.GetHostingEnvironment(); bool auth = !hostingEnvironment.IsDevelopment(); capOptions.UseDashboard(options => { options.UseAuth = auth; }); }); } else { context.AddAbpCap(capOptions => { capOptions.UseInMemoryStorage(); capOptions.UseInMemoryMessageQueue(); var hostingEnvironment = context.Services.GetHostingEnvironment(); bool auth = !hostingEnvironment.IsDevelopment(); capOptions.UseDashboard(options => { options.UseAuth = auth; }); }); } }

  • 发布事件
    • 可参考通知模块
// 发送集成事件 entity.AddCreatedNotificationDistributedEvent(new CreatedNotificationDistributedEvent(notificationEto));

  • 订阅事件
    • 可参考通知模块
/// /// 创建消息事件处理 /// public class CreatedNotificationDistributedEventHandler : IDistributedEventHandler, ITransientDependency { private readonly INotificationAppService _hubAppService; public CreatedNotificationDistributedEventHandler(INotificationAppService hubAppService) { _hubAppService = hubAppService; } public Task HandleEventAsync(CreatedNotificationDistributedEvent eventData) { return _hubAppService.SendMessageAsync( eventData.NotificationEto.Title, eventData.NotificationEto.Content, eventData.NotificationEto.MessageType, eventData.NotificationEto.NotificationSubscriptions.Select(e => e.ReceiveId.ToString()).ToList()); } }

身份认证中心
  • IdentityServer4
  • 可重写登录界面 UI
租户管理
  • 提供租户登录和 IdentityServer4 租户登录方式
Ocelot 网关(可选)
  • 集成 Ocelot 和 Consul
部署 Docker 方式
HttpApi.Host
  • 发布 HttpApi.Host 到和 Dockerfile 同级目录
    -- publish -- Dockerfile

  • Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:5.0# 创建目录 RUN mkdir /appCOPY publish /app# 设置工作目录 WORKDIR /app# 暴露80端口 EXPOSE 80# 设置环境变量 ENV ASPNETCORE_ENVIRONMENT=ProductionENTRYPOINT ["dotnet", "CompanyName.ProjectName.HttpApi.Host.dll"]

  • 生成 Docker 镜像
docker build -t abp-vnext-pro-admin .

  • 运行容器
docker run -itd --name abp-vnext-pro-admin -p 8011:80 abp-vnext-pro-admin

IdentityServer.Host
  • 步骤同上
前端
  • 打包
npm run build

  • Dockerfile
FROM nginx:1.17.3-alpine as base EXPOSE 80 COPY /_nginx/nginx.conf /etc/nginx/nginx.conf COPY /_nginx/env.js /etc/nginx/env.js COPY /_nginx/default.conf /etc/nginx/conf.d/default.conf COPY /dist/ /usr/share/nginx/html CMD ["nginx", "-g", "daemon off; "]

  • 生成 Docker 镜像
docker build -t abp-vnext-pro-ui .

  • 运行容器
docker run -itd --name abp-vnext-pro-ui -p 8012:80 abp-vnext-pro-ui

常见问题 VS 编译项目字符串超过 256 个字符
  • 把项目拷贝到磁盘根目录 OR 使用 Rider 开发
Hangfire 和 Cap 界面加载不出来
  • 这 2 个界面开启了权限认证,由于前端路由的异步加载,导致路由在渲染的时候 access_token 没有加载出来,Ctrl+F5 刷新即可

    推荐阅读