mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-04-16 22:26:37 +08:00
Compare commits
22 Commits
digital-co
...
TenantConf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45736dfce9 | ||
|
|
753b5b0a26 | ||
|
|
1fd4f2754a | ||
|
|
bedee3391e | ||
|
|
176a672e86 | ||
|
|
9da6bcde41 | ||
|
|
3f8daa1d17 | ||
|
|
ed873da3b6 | ||
|
|
c0dfa83828 | ||
|
|
db08688968 | ||
|
|
400a146a48 | ||
|
|
a645264da7 | ||
|
|
09d19d876f | ||
|
|
373877cfcf | ||
|
|
9e143c0a75 | ||
|
|
37b16e8395 | ||
|
|
9c94953e0e | ||
|
|
2c48b8f881 | ||
|
|
4c4b78dda7 | ||
|
|
4ba9a7917f | ||
|
|
5d286ebc9e | ||
|
|
e69bbb46b3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -263,6 +263,7 @@ src/Acme.BookStore.Blazor.Server.Tiered/Logs/*
|
||||
|
||||
# Use abp install-libs to restore.
|
||||
**/wwwroot/libs/*
|
||||
public
|
||||
dist
|
||||
.vscode
|
||||
/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.Development.json
|
||||
|
||||
37
README-Docker.md
Normal file
37
README-Docker.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 🍉Docker 构建说明
|
||||
|
||||
## 🍊后端
|
||||
执行目录:Yi\Yi.Abp.Net8
|
||||
|
||||
#### 🍊启动
|
||||
D:/code/csharp/source/Yi/Yi.Bbs.Vue3/yi-bbs.conf 为我的配置文件,内部带了默认的配置文件,根据自己配置进行更改
|
||||
|
||||
//不带配置文件
|
||||
docker run -d --name yi.admin -p 19001:19001 jiftcc/yi.admin:1.0.0
|
||||
|
||||
//带配置文件
|
||||
docker run -d --name yi.admin -p 19001:19001 -v D:/code/csharp/source/Yi/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json:/app/appsettings.json jiftcc/yi.admin:1.0.0
|
||||
|
||||
|
||||
#### 🍊完整代码编译
|
||||
docker build -t jiftcc/yi.admin:1.0.0 -f Dockerfile .
|
||||
|
||||
#### 🍊快速产物编译
|
||||
docker build -t jiftcc/yi.admin:1.0.0 -f DockerfileFast .
|
||||
|
||||
****
|
||||
|
||||
## 🍇前端
|
||||
执行目录:Yi\Yi.Bbs.Vue3
|
||||
|
||||
#### 🍇启动
|
||||
D:/code/csharp/source/Yi/Yi.Bbs.Vue3/yi-bbs.conf 为我的conf配置目录,默认反向代理到ccnetcore.com,根据自己后端地址进行修改配置
|
||||
|
||||
docker run -d --name yi.bbs -p 18001:18001 -v D:/code/csharp/source/Yi/Yi.Bbs.Vue3/yi-bbs.conf:/etc/nginx/conf.d/yi-bbs.conf jiftcc/yi.bbs:1.0.0
|
||||
|
||||
#### 🍇完整代码编译
|
||||
docker build -t jiftcc/yi.bbs:1.0.0 -f Dockerfile .
|
||||
|
||||
#### 🍇快速产物编译
|
||||
docker build -t jiftcc/yi.bbs:1.0.0 -f DockerfileFast .
|
||||
|
||||
12
README-en.md
12
README-en.md
@@ -35,6 +35,18 @@ A Comprehensive Solution, Ultimately Just Another Wheel.
|
||||
- Yi.RuoYi.Vue3:RuoYi JS Backend Frontend
|
||||
|
||||
****
|
||||
## 🍉 docker
|
||||
|
||||
Full content:README-Docker.md
|
||||
|
||||
backend:`docker run -d --name yi.admin -p 19001:19001 jiftcc/yi.admin:last`
|
||||
|
||||
bbs frontend:`docker run -d --name yi.bbs -p 18001:18001 -v /home/Yi/Yi.Bbs.Vue3/yi-bbs.conf:/etc/nginx/conf.d/yi-bbs.conf jiftcc/yi.bbs:last`
|
||||
|
||||
> In addition, we provide Docker build operation, and we hope that you can build your own image through this method
|
||||
|
||||
****
|
||||
|
||||
|
||||
## 🍊 Official website and demo link:
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -41,6 +41,17 @@ Yi框架-一套与SqlSugar一样爽的.Net8开源框架。
|
||||
- Yi.Pure.Vue3:Pure ts后台前端
|
||||
- Yi.RuoYi.Vue3:RuoYi js后台前端
|
||||
|
||||
****
|
||||
## 🍉 docker 一键启动
|
||||
|
||||
完整内容在:README-Docker.md
|
||||
|
||||
后端:`docker run -d --name yi.admin -p 19001:19001 jiftcc/yi.admin:last`
|
||||
|
||||
bbs前端:`docker run -d --name yi.bbs -p 18001:18001 -v /home/Yi/Yi.Bbs.Vue3/yi-bbs.conf:/etc/nginx/conf.d/yi-bbs.conf jiftcc/yi.bbs:last`
|
||||
|
||||
> 另外我们提供docker的build操作,我们更希望你能通过此种方式二开构建属于自己的镜像
|
||||
|
||||
****
|
||||
|
||||
## 🍊 官网及演示地址:
|
||||
@@ -60,6 +71,7 @@ Pure后台演示地址:https://ccnetcore.com:1001 (用户cc、密码123456
|
||||
- [x] 完全支持微服务架构
|
||||
|
||||
****
|
||||
|
||||
## 🍇 详细到爆炸的Yi框架教程导航:
|
||||
|
||||
1. [框架快速开始教程](https://ccnetcore.com/article/aaa00329-7f35-d3fe-d258-3a0f8380b742)(已完成)
|
||||
|
||||
@@ -27,4 +27,7 @@ README.md
|
||||
!.git/HEAD
|
||||
!.git/config
|
||||
!.git/packed-refs
|
||||
!.git/refs/heads/**
|
||||
!.git/refs/heads/**
|
||||
appsettings.Development.json
|
||||
appsettings.Production.json
|
||||
appsettings.Staging.json
|
||||
22
Yi.Abp.Net8/Dockerfile
Normal file
22
Yi.Abp.Net8/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
USER root
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
RUN echo "Asia/Shanghai" > /etc/timezone
|
||||
WORKDIR /app
|
||||
EXPOSE 19001
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /main
|
||||
COPY . .
|
||||
WORKDIR "/main/src/Yi.Abp.Web"
|
||||
RUN dotnet restore "Yi.Abp.Web.csproj"
|
||||
|
||||
FROM build AS publish
|
||||
WORKDIR "/main/src/Yi.Abp.Web"
|
||||
RUN dotnet publish "Yi.Abp.Web.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Yi.Abp.Web.dll"]
|
||||
11
Yi.Abp.Net8/DockerfileFast
Normal file
11
Yi.Abp.Net8/DockerfileFast
Normal file
@@ -0,0 +1,11 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
USER root
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
RUN echo "Asia/Shanghai" > /etc/timezone
|
||||
WORKDIR /app
|
||||
EXPOSE 19001
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY ["./publish","."]
|
||||
ENTRYPOINT ["dotnet", "Yi.Abp.Web.dll"]
|
||||
@@ -36,6 +36,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
version.props = version.props
|
||||
publish.bat = publish.bat
|
||||
publish_Demo.bat = publish_Demo.bat
|
||||
Dockerfile = Dockerfile
|
||||
DockerfileFast = DockerfileFast
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Yi.Framework.SqlSugarCore.Abstractions", "framework\Yi.Framework.SqlSugarCore.Abstractions\Yi.Framework.SqlSugarCore.Abstractions.csproj", "{FD6D6860-3753-4747-8A26-977E4A3001F9}"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using Hangfire;
|
||||
using System.Linq.Expressions;
|
||||
using Hangfire;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Volo.Abp.BackgroundWorkers;
|
||||
using Volo.Abp.BackgroundWorkers.Hangfire;
|
||||
using Volo.Abp.DynamicProxy;
|
||||
|
||||
namespace Yi.Framework.BackgroundWorkers.Hangfire;
|
||||
|
||||
@@ -19,11 +22,28 @@ public class YiFrameworkBackgroundWorkersHangfireModule : AbpModule
|
||||
var backgroundWorkerManager = context.ServiceProvider.GetRequiredService<IBackgroundWorkerManager>();
|
||||
var works = context.ServiceProvider.GetServices<IHangfireBackgroundWorker>();
|
||||
|
||||
var configuration = context.ServiceProvider.GetRequiredService<IConfiguration>();
|
||||
//【特殊,为了兼容内存模式,由于内存模式任务,不能使用队列】
|
||||
bool.TryParse(configuration["Redis:IsEnabled"], out var redisEnabled);
|
||||
foreach (var work in works)
|
||||
{
|
||||
//如果为空,默认使用服务器本地utc时间
|
||||
work.TimeZone ??= TimeZoneInfo.Local;
|
||||
await backgroundWorkerManager.AddAsync(work);
|
||||
//如果为空,默认使用服务器本地上海时间
|
||||
work.TimeZone = TimeZoneInfo.Local;
|
||||
if (redisEnabled)
|
||||
{
|
||||
await backgroundWorkerManager.AddAsync(work);
|
||||
}
|
||||
else
|
||||
{
|
||||
object unProxyWorker = ProxyHelper.UnProxy((object)work);
|
||||
RecurringJob.AddOrUpdate(work.RecurringJobId,
|
||||
(Expression<Func<Task>>)(() =>
|
||||
((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(default(CancellationToken))),
|
||||
work.CronExpression, new RecurringJobOptions()
|
||||
{
|
||||
TimeZone = work.TimeZone
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SqlSugarCoreNoDrive" Version="$(SqlSugarVersion)" />
|
||||
<!-- <PackageReference Include="SqlSugarCoreNoDrive" Version="$(SqlSugarVersion)" />-->
|
||||
|
||||
<PackageReference Include="SqlSugarCore" Version="$(SqlSugarVersion)" />
|
||||
|
||||
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -61,7 +61,12 @@ public class DefaultSqlSugarDbContext : SqlSugarDbContext
|
||||
|
||||
if (entityInfo.PropertyName.Equals(nameof(IAuditedObject.LastModificationTime)))
|
||||
{
|
||||
if (!DateTime.MinValue.Equals(oldValue))
|
||||
//最后更新时间,已经是最小值,忽略
|
||||
if (DateTime.MinValue.Equals(oldValue))
|
||||
{
|
||||
entityInfo.SetValue(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
entityInfo.SetValue(DateTime.Now);
|
||||
}
|
||||
@@ -70,7 +75,12 @@ public class DefaultSqlSugarDbContext : SqlSugarDbContext
|
||||
{
|
||||
if (typeof(Guid?) == entityInfo.EntityColumnInfo.PropertyInfo.PropertyType)
|
||||
{
|
||||
if (CurrentUser.Id != null)
|
||||
//最后更新者,已经是空guid,忽略
|
||||
if (Guid.Empty.Equals(oldValue))
|
||||
{
|
||||
entityInfo.SetValue(null);
|
||||
}
|
||||
else if (CurrentUser.Id != null)
|
||||
{
|
||||
entityInfo.SetValue(CurrentUser.Id);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,10 @@ namespace Yi.Framework.SqlSugarCore
|
||||
|
||||
private IAbpLazyServiceProvider LazyServiceProvider { get; }
|
||||
|
||||
private TenantConfigurationWrapper TenantConfigurationWrapper=> LazyServiceProvider.LazyGetRequiredService<TenantConfigurationWrapper>();
|
||||
private ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService<ICurrentTenant>();
|
||||
public DbConnOptions Options => LazyServiceProvider.LazyGetRequiredService<IOptions<DbConnOptions>>().Value;
|
||||
|
||||
private DbConnOptions Options => LazyServiceProvider.LazyGetRequiredService<IOptions<DbConnOptions>>().Value;
|
||||
|
||||
private ISerializeService SerializeService => LazyServiceProvider.LazyGetRequiredService<ISerializeService>();
|
||||
|
||||
@@ -36,19 +38,13 @@ namespace Yi.Framework.SqlSugarCore
|
||||
{
|
||||
LazyServiceProvider = lazyServiceProvider;
|
||||
|
||||
var connectionString = GetCurrentConnectionString();
|
||||
|
||||
var tenantConfiguration= AsyncHelper.RunSync(async () =>await TenantConfigurationWrapper.GetAsync());
|
||||
|
||||
var connectionConfig =BuildConnectionConfig(action: options =>
|
||||
{
|
||||
options.ConnectionString = connectionString;
|
||||
options.DbType = GetCurrentDbType();
|
||||
options.ConnectionString =tenantConfiguration.GetCurrentConnectionString();
|
||||
options.DbType = GetCurrentDbType(tenantConfiguration.GetCurrentConnectionName());
|
||||
});
|
||||
// var connectionConfig = ConnectionConfigCache.GetOrAdd(connectionString, (_) =>
|
||||
// BuildConnectionConfig(action: options =>
|
||||
// {
|
||||
// options.ConnectionString = connectionString;
|
||||
// options.DbType = GetCurrentDbType();
|
||||
// }));
|
||||
SqlSugarClient = new SqlSugarClient(connectionConfig);
|
||||
//生命周期,以下都可以直接使用sqlsugardb了
|
||||
|
||||
@@ -188,41 +184,18 @@ namespace Yi.Framework.SqlSugarCore
|
||||
|
||||
return connectionConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// db切换多库支持
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual string GetCurrentConnectionString()
|
||||
|
||||
protected virtual DbType GetCurrentDbType(string tenantName)
|
||||
{
|
||||
var connectionStringResolver = LazyServiceProvider.LazyGetRequiredService<IConnectionStringResolver>();
|
||||
var connectionString =
|
||||
AsyncHelper.RunSync(() => connectionStringResolver.ResolveAsync());
|
||||
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
if (tenantName == ConnectionStrings.DefaultConnectionStringName)
|
||||
{
|
||||
Check.NotNull(Options.Url, "dbUrl未配置");
|
||||
return Options.DbType!.Value;
|
||||
}
|
||||
|
||||
return connectionString!;
|
||||
var dbTypeFromTenantName = GetDbTypeFromTenantName(tenantName);
|
||||
return dbTypeFromTenantName!.Value;
|
||||
}
|
||||
|
||||
protected virtual DbType GetCurrentDbType()
|
||||
{
|
||||
if (CurrentTenant.Name is not null)
|
||||
{
|
||||
var dbTypeFromTenantName = GetDbTypeFromTenantName(CurrentTenant.Name);
|
||||
if (dbTypeFromTenantName is not null)
|
||||
{
|
||||
return dbTypeFromTenantName.Value;
|
||||
}
|
||||
}
|
||||
|
||||
Check.NotNull(Options.DbType, "默认DbType未配置!");
|
||||
return Options.DbType!.Value;
|
||||
}
|
||||
|
||||
//根据租户name进行匹配db类型: Test_Sqlite,[来自AI]
|
||||
//根据租户name进行匹配db类型: Test@Sqlite,[form:AI]
|
||||
private DbType? GetDbTypeFromTenantName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
@@ -230,25 +203,26 @@ namespace Yi.Framework.SqlSugarCore
|
||||
return null;
|
||||
}
|
||||
|
||||
// 查找下划线的位置
|
||||
int underscoreIndex = name.LastIndexOf('_');
|
||||
// 查找@符号的位置
|
||||
int atIndex = name.LastIndexOf('@');
|
||||
|
||||
if (underscoreIndex == -1 || underscoreIndex == name.Length - 1)
|
||||
if (atIndex == -1 || atIndex == name.Length - 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 提取 枚举 部分
|
||||
string enumString = name.Substring(underscoreIndex + 1);
|
||||
string enumString = name.Substring(atIndex + 1);
|
||||
|
||||
// 尝试将 尾缀 转换为枚举
|
||||
if (Enum.TryParse<DbType>(enumString, out DbType result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
// 条件不满足时返回 null
|
||||
return null;
|
||||
else
|
||||
{
|
||||
throw new ArgumentException($"数据库{name}db类型错误或不支持:无法匹配{enumString}数据库类型");
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void BackupDataBase()
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Volo.Abp.Data;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.SqlSugarCore;
|
||||
|
||||
/// <summary>
|
||||
/// 租户配置
|
||||
/// </summary>
|
||||
public class TenantConfigurationWrapper : ITransientDependency
|
||||
{
|
||||
private readonly IAbpLazyServiceProvider _serviceProvider;
|
||||
private ICurrentTenant CurrentTenant => _serviceProvider.LazyGetRequiredService<ICurrentTenant>();
|
||||
private ITenantStore TenantStore => _serviceProvider.LazyGetRequiredService<ITenantStore>();
|
||||
private DbConnOptions DbConnOptions => _serviceProvider.LazyGetRequiredService<IOptions<DbConnOptions>>().Value;
|
||||
|
||||
public TenantConfigurationWrapper(IAbpLazyServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取租户信息
|
||||
/// [from:ai]
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<TenantConfiguration?> GetAsync()
|
||||
{
|
||||
//未开启多租户
|
||||
if (!DbConnOptions.EnabledSaasMultiTenancy)
|
||||
{
|
||||
return await TenantStore.FindAsync(ConnectionStrings.DefaultConnectionStringName);
|
||||
}
|
||||
|
||||
TenantConfiguration? tenantConfiguration = null;
|
||||
|
||||
if (CurrentTenant.Id is not null)
|
||||
{
|
||||
tenantConfiguration = await TenantStore.FindAsync(CurrentTenant.Id.Value);
|
||||
if (tenantConfiguration == null)
|
||||
{
|
||||
throw new ApplicationException($"未找到租户信息,租户Id:{CurrentTenant.Id}");
|
||||
}
|
||||
return tenantConfiguration;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(CurrentTenant.Name))
|
||||
{
|
||||
tenantConfiguration = await TenantStore.FindAsync(CurrentTenant.Name);
|
||||
if (tenantConfiguration == null)
|
||||
{
|
||||
throw new ApplicationException($"未找到租户信息,租户名称:{CurrentTenant.Name}");
|
||||
}
|
||||
return tenantConfiguration;
|
||||
}
|
||||
|
||||
return await TenantStore.FindAsync(ConnectionStrings.DefaultConnectionStringName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前连接字符串
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<string> GetCurrentConnectionStringAsync()
|
||||
{
|
||||
return (await GetAsync()).ConnectionStrings.Default!;
|
||||
}
|
||||
/// <summary>
|
||||
/// 获取当前连接名
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<string> GetCurrentConnectionNameAsync()
|
||||
{
|
||||
return (await GetAsync()).Name;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TenantConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前连接字符串
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string GetCurrentConnectionString(this TenantConfiguration tenantConfiguration)
|
||||
{
|
||||
return tenantConfiguration.ConnectionStrings.Default!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前连接名
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static string GetCurrentConnectionName(this TenantConfiguration tenantConfiguration)
|
||||
{
|
||||
return tenantConfiguration.Name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ namespace Yi.Framework.SqlSugarCore.Uow
|
||||
{
|
||||
public ILogger<UnitOfWorkSqlsugarDbContextProvider<TDbContext>> Logger { get; set; }
|
||||
public IServiceProvider ServiceProvider { get; set; }
|
||||
|
||||
private static AsyncLocalDbContextAccessor ContextInstance => AsyncLocalDbContextAccessor.Instance;
|
||||
protected readonly TenantConfigurationWrapper _tenantConfigurationWrapper;
|
||||
protected readonly IUnitOfWorkManager UnitOfWorkManager;
|
||||
protected readonly IConnectionStringResolver ConnectionStringResolver;
|
||||
protected readonly ICancellationTokenProvider CancellationTokenProvider;
|
||||
@@ -26,26 +26,25 @@ namespace Yi.Framework.SqlSugarCore.Uow
|
||||
IUnitOfWorkManager unitOfWorkManager,
|
||||
IConnectionStringResolver connectionStringResolver,
|
||||
ICancellationTokenProvider cancellationTokenProvider,
|
||||
ICurrentTenant currentTenant
|
||||
)
|
||||
ICurrentTenant currentTenant, TenantConfigurationWrapper tenantConfigurationWrapper)
|
||||
{
|
||||
UnitOfWorkManager = unitOfWorkManager;
|
||||
ConnectionStringResolver = connectionStringResolver;
|
||||
CancellationTokenProvider = cancellationTokenProvider;
|
||||
CurrentTenant = currentTenant;
|
||||
_tenantConfigurationWrapper = tenantConfigurationWrapper;
|
||||
Logger = NullLogger<UnitOfWorkSqlsugarDbContextProvider<TDbContext>>.Instance;
|
||||
}
|
||||
|
||||
public virtual async Task<TDbContext> GetDbContextAsync()
|
||||
{
|
||||
|
||||
var connectionStringName = ConnectionStrings.DefaultConnectionStringName;
|
||||
|
||||
//获取当前连接字符串,未多租户时,默认为空
|
||||
var connectionString = await ResolveConnectionStringAsync(connectionStringName);
|
||||
var tenantConfiguration= await _tenantConfigurationWrapper.GetAsync();
|
||||
//由于sqlsugar的特殊性,没有db区分,不再使用连接字符串解析器
|
||||
var connectionStringName = tenantConfiguration.GetCurrentConnectionName();
|
||||
var connectionString = tenantConfiguration.GetCurrentConnectionString();
|
||||
var dbContextKey = $"{this.GetType().Name}_{connectionString}";
|
||||
|
||||
|
||||
|
||||
var unitOfWork = UnitOfWorkManager.Current;
|
||||
if (unitOfWork == null )
|
||||
{
|
||||
@@ -67,8 +66,6 @@ namespace Yi.Framework.SqlSugarCore.Uow
|
||||
databaseApi = new SqlSugarDatabaseApi(
|
||||
await CreateDbContextAsync(unitOfWork, connectionStringName, connectionString)
|
||||
);
|
||||
|
||||
//await Console.Out.WriteLineAsync(">>>----------------实例化了db"+ ((SqlSugarDatabaseApi)databaseApi).DbContext.SqlSugarClient.ContextID.ToString());
|
||||
//创建的db加入到当前工作单元中
|
||||
unitOfWork.AddDatabaseApi(dbContextKey, databaseApi);
|
||||
|
||||
@@ -98,7 +95,7 @@ namespace Yi.Framework.SqlSugarCore.Uow
|
||||
protected virtual async Task<TDbContext> CreateDbContextWithTransactionAsync(IUnitOfWork unitOfWork)
|
||||
{
|
||||
//事务key
|
||||
var transactionApiKey = $"SqlsugarCore_{SqlSugarDbContextCreationContext.Current.ConnectionString}";
|
||||
var transactionApiKey = $"SqlSugarCore_{SqlSugarDbContextCreationContext.Current.ConnectionString}";
|
||||
|
||||
//尝试查找事务
|
||||
var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as SqlSugarTransactionApi;
|
||||
@@ -123,20 +120,5 @@ namespace Yi.Framework.SqlSugarCore.Uow
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
protected virtual async Task<string> ResolveConnectionStringAsync(string connectionStringName)
|
||||
{
|
||||
if (typeof(TDbContext).IsDefined(typeof(IgnoreMultiTenancyAttribute), false))
|
||||
{
|
||||
using (CurrentTenant.Change(null))
|
||||
{
|
||||
return await ConnectionStringResolver.ResolveAsync(connectionStringName);
|
||||
}
|
||||
}
|
||||
|
||||
return await ConnectionStringResolver.ResolveAsync(connectionStringName);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ using Volo.Abp.Data;
|
||||
using Volo.Abp.Domain;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
using Volo.Abp.Guids;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
using Volo.Abp.MultiTenancy.ConfigurationStore;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
using Yi.Framework.SqlSugarCore.Repositories;
|
||||
using Yi.Framework.SqlSugarCore.Uow;
|
||||
@@ -51,7 +53,7 @@ namespace Yi.Framework.SqlSugarCore
|
||||
options.DefaultSequentialGuidType = guidType;
|
||||
});
|
||||
|
||||
service.TryAddScoped<ISqlSugarDbContext, SqlSugarDbContextFactory>();
|
||||
service.TryAddTransient<ISqlSugarDbContext, SqlSugarDbContextFactory>();
|
||||
|
||||
//不开放sqlsugarClient
|
||||
//service.AddTransient<ISqlSugarClient>(x => x.GetRequiredService<ISqlsugarDbContext>().SqlSugarClient);
|
||||
@@ -69,7 +71,25 @@ namespace Yi.Framework.SqlSugarCore
|
||||
var dbConfig = section.Get<DbConnOptions>();
|
||||
//将默认db传递给abp连接字符串模块
|
||||
Configure<AbpDbConnectionOptions>(x => { x.ConnectionStrings.Default = dbConfig.Url; });
|
||||
|
||||
//配置abp默认租户,对接abp模块
|
||||
Configure<AbpDefaultTenantStoreOptions>(x => {
|
||||
var tenantList = x.Tenants.ToList();
|
||||
foreach(var tenant in tenantList)
|
||||
{
|
||||
tenant.NormalizedName = tenant.Name.Contains("@") ?
|
||||
tenant.Name.Substring(0, tenant.Name.LastIndexOf("@")) :
|
||||
tenant.Name;
|
||||
}
|
||||
tenantList.Insert(0, new TenantConfiguration
|
||||
{
|
||||
Id = Guid.Empty,
|
||||
Name = $"{ConnectionStrings.DefaultConnectionStringName}",
|
||||
NormalizedName = ConnectionStrings.DefaultConnectionStringName,
|
||||
ConnectionStrings = new ConnectionStrings() { { ConnectionStrings.DefaultConnectionStringName, dbConfig.Url } },
|
||||
IsActive = true
|
||||
});
|
||||
x.Tenants = tenantList.ToArray();
|
||||
});
|
||||
context.Services.AddYiDbContext<DefaultSqlSugarDbContext>();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ public class AccessLogCacheJob : HangfireBackgroundWorkerBase
|
||||
{
|
||||
_localEventBus = localEventBus;
|
||||
RecurringJobId = "访问日志写入缓存";
|
||||
//每10秒执行一次,将本地缓存转入redis,防止丢数据
|
||||
CronExpression = "*/10 * * * * *";
|
||||
//每分钟执行一次,将本地缓存转入redis,防止丢数据
|
||||
CronExpression = "0 * * * * ?";
|
||||
//
|
||||
// JobDetail = JobBuilder.Create<AccessLogCacheJob>().WithIdentity(nameof(AccessLogCacheJob))
|
||||
// .Build();
|
||||
|
||||
@@ -46,8 +46,8 @@ public class AccessLogStoreJob : HangfireBackgroundWorkerBase
|
||||
|
||||
|
||||
RecurringJobId = "访问日志写入数据库";
|
||||
//每分钟执行一次
|
||||
CronExpression = "0 * * * * ?";
|
||||
//每小时执行一次
|
||||
CronExpression = "0 0 * * * ?";
|
||||
// JobDetail = JobBuilder.Create<AccessLogStoreJob>().WithIdentity(nameof(AccessLogStoreJob))
|
||||
// .Build();
|
||||
// //每分钟执行一次
|
||||
|
||||
@@ -136,8 +136,6 @@ namespace Yi.Framework.Bbs.Application.Services.Forum
|
||||
{ DiscussId = output.Id, OldSeeNum = output.SeeNum });
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 查询
|
||||
/// </summary>
|
||||
@@ -157,7 +155,11 @@ namespace Yi.Framework.Bbs.Application.Services.Forum
|
||||
.WhereIF(input.UserName is not null, (discuss, user) => user.UserName == input.UserName!)
|
||||
.LeftJoin<BbsUserExtraInfoEntity>((discuss, user, info) => user.Id == info.UserId)
|
||||
.OrderByDescending(discuss => discuss.OrderNum)
|
||||
.OrderByIF(input.Type == QueryDiscussTypeEnum.New, discuss => discuss.CreationTime, OrderByType.Desc)
|
||||
//已提示杰哥新增表达式
|
||||
// .OrderByIF(input.Type == QueryDiscussTypeEnum.New,
|
||||
// @"COALESCE(discuss.LastModificationTime, discuss.CreationTime) DESC")
|
||||
//采用上方写法
|
||||
.OrderByIF(input.Type == QueryDiscussTypeEnum.New,discuss=>SqlFunc.Coalesce(discuss.LastModificationTime,discuss.CreationTime),OrderByType.Desc)
|
||||
.OrderByIF(input.Type == QueryDiscussTypeEnum.Host, discuss => discuss.SeeNum, OrderByType.Desc)
|
||||
.OrderByIF(input.Type == QueryDiscussTypeEnum.Suggest, discuss => discuss.AgreeNum, OrderByType.Desc)
|
||||
.Select((discuss, user, info) => new DiscussGetListOutputDto
|
||||
|
||||
@@ -24,6 +24,15 @@ namespace Yi.Framework.Bbs.Domain.Entities.Forum
|
||||
PlateId = plateId;
|
||||
}
|
||||
|
||||
public void AddSeeNumber()
|
||||
{
|
||||
this.SeeNum += 1;
|
||||
//设置最小值,不更新
|
||||
this.LastModificationTime = DateTime.MinValue;
|
||||
//设置空值,不更新
|
||||
this.LastModifierId = Guid.Empty;
|
||||
}
|
||||
|
||||
[SugarColumn(ColumnName = "Id", IsPrimaryKey = true)]
|
||||
public override Guid Id { get; protected set; }
|
||||
public string? Title { get; set; }
|
||||
|
||||
@@ -8,28 +8,24 @@ using Volo.Abp.Domain.Repositories;
|
||||
using Volo.Abp.EventBus;
|
||||
using Yi.Framework.Bbs.Domain.Entities.Forum;
|
||||
using Yi.Framework.Bbs.Domain.Shared.Etos;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.Bbs.Domain.EventHandlers
|
||||
{
|
||||
public class SeeDiscussEventHandler : ILocalEventHandler<SeeDiscussEventArgs>, ITransientDependency
|
||||
{
|
||||
private IRepository<DiscussAggregateRoot, Guid> _repository;
|
||||
public SeeDiscussEventHandler(IRepository<DiscussAggregateRoot, Guid> repository)
|
||||
private ISqlSugarRepository<DiscussAggregateRoot, Guid> _repository;
|
||||
|
||||
public SeeDiscussEventHandler(ISqlSugarRepository<DiscussAggregateRoot, Guid> repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task HandleEventAsync(SeeDiscussEventArgs eventData)
|
||||
{
|
||||
var entity = await _repository.GetAsync(eventData.DiscussId);
|
||||
if (entity is not null)
|
||||
{
|
||||
entity.SeeNum += 1;
|
||||
await _repository.UpdateAsync(entity);
|
||||
}
|
||||
await _repository._Db.Updateable<DiscussAggregateRoot>()
|
||||
.SetColumns(x => new DiscussAggregateRoot { SeeNum = x.SeeNum + 1 })
|
||||
.Where(x => x.Id == eventData.DiscussId).ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Mapster;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Volo.Abp.Caching;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Yi.Framework.Bbs.Domain.Entities.Forum;
|
||||
@@ -28,10 +29,13 @@ public class DiscussLableRepository : SqlSugarRepository<DiscussLableAggregateRo
|
||||
public async Task<Dictionary<Guid, DiscussLableCacheItem>> GetDiscussLableCacheMapAsync()
|
||||
{
|
||||
var cahce = await _lableCache.GetOrAddAsync(DiscussLableConst.DiscussLableCacheKey, async () =>
|
||||
{
|
||||
var entities = await _DbQueryable.ToListAsync();
|
||||
return entities.Adapt<List<DiscussLableCacheItem>>();
|
||||
});
|
||||
{
|
||||
var entities = await _DbQueryable.ToListAsync();
|
||||
return entities.Adapt<List<DiscussLableCacheItem>>();
|
||||
}, () =>
|
||||
new DistributedCacheEntryOptions()
|
||||
{ AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2) }
|
||||
);
|
||||
return cahce.ToDictionary(x => x.Id);
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ namespace Yi.Framework.ChatHub.Domain.Managers
|
||||
{
|
||||
if (result.Successful)
|
||||
{
|
||||
yield return result.Choices.FirstOrDefault()?.Message.Content ?? string.Empty;
|
||||
yield return result.Choices.FirstOrDefault()?.Message.Content ?? null;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -99,10 +99,14 @@ public class FileManager : DomainService, IFileManager
|
||||
this.LoggerFactory.CreateLogger<FileManager>().LogInformation(exception, exception.Message);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
|
||||
this.LoggerFactory.CreateLogger<FileManager>().LogError(exception, exception.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
//如果失败了,直接复制一份到缩略图上即可
|
||||
compressImageStream = fileStream;
|
||||
this.LoggerFactory.CreateLogger<FileManager>().LogError(exception, exception.Message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -53,10 +53,8 @@ public class YiMultiTenantConnectionStringResolver : DefaultConnectionStringReso
|
||||
: Options.ConnectionStrings.Default!;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//Requesting specific connection string...
|
||||
var connString = tenant.ConnectionStrings?.FirstOrDefault().Value;
|
||||
var connString = tenant.ConnectionStrings?.GetOrDefault(connectionStringName);
|
||||
if (!connString.IsNullOrWhiteSpace())
|
||||
{
|
||||
//Found for the tenant
|
||||
|
||||
@@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.DistributedLocking;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
using Volo.Abp.MultiTenancy;
|
||||
using Volo.Abp.Settings;
|
||||
using Volo.Abp.Uow;
|
||||
using Yi.Framework.Bbs.Application.Contracts.Dtos.Banner;
|
||||
@@ -215,5 +217,53 @@ namespace Yi.Abp.Application.Services
|
||||
});
|
||||
return $"加锁结果:{number},不加锁结果:{number2}";
|
||||
}
|
||||
|
||||
public ICurrentTenant CurrentTenant { get; set; }
|
||||
public IRepository<BannerAggregateRoot> repository { get; set; }
|
||||
/// <summary>
|
||||
/// 多租户
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<string> GetMultiTenantAsync()
|
||||
{
|
||||
using (var uow=UnitOfWorkManager.Begin())
|
||||
{
|
||||
//此处会实例化一个db,连接默认库
|
||||
var defautTenantData1= await repository.GetListAsync();
|
||||
using (CurrentTenant.Change(null,"Default"))
|
||||
{
|
||||
var defautTenantData2= await repository.GetListAsync();
|
||||
await repository.InsertAsync(new BannerAggregateRoot
|
||||
{
|
||||
Name = "default",
|
||||
});
|
||||
var defautTenantData3= await repository.GetListAsync(x=>x.Name=="default");
|
||||
}
|
||||
//此处会实例化一个新的db连接MES
|
||||
using (CurrentTenant.Change(null,"Mes"))
|
||||
{
|
||||
var otherTenantData1= await repository.GetListAsync();
|
||||
await repository.InsertAsync(new BannerAggregateRoot
|
||||
{
|
||||
Name = "Mes1",
|
||||
});
|
||||
var otherTenantData2= await repository.GetListAsync(x=>x.Name=="Mes1");
|
||||
}
|
||||
//此处会复用Mesdb,不会实例化新的db
|
||||
using (CurrentTenant.Change(Guid.Parse("33333333-3d72-4339-9adc-845151f8ada0")))
|
||||
{
|
||||
var otherTenantData1= await repository.GetListAsync();
|
||||
await repository.InsertAsync(new BannerAggregateRoot
|
||||
{
|
||||
Name = "Mes2",
|
||||
});
|
||||
var otherTenantData2= await repository.GetListAsync(x=>x.Name=="Mes2");
|
||||
}
|
||||
//此处会将多库进行一起提交,前面的操作有报错,全部回滚
|
||||
await uow.CompleteAsync();
|
||||
return "根据租户切换不同的数据库,并管理db实例连接,涉及多库事务统一到最后提交";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
USER app
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
|
||||
COPY ./common.props ./
|
||||
COPY ["src/Yi.Abp.Web/Yi.Abp.Web.csproj", "src/Yi.Abp.Web/"]
|
||||
COPY ["framework/Yi.Framework.AspNetCore.Authentication.OAuth/Yi.Framework.AspNetCore.Authentication.OAuth.csproj", "framework/Yi.Framework.AspNetCore.Authentication.OAuth/"]
|
||||
COPY ["framework/Yi.Framework.AspNetCore/Yi.Framework.AspNetCore.csproj", "framework/Yi.Framework.AspNetCore/"]
|
||||
COPY ["framework/Yi.Framework.Core/Yi.Framework.Core.csproj", "framework/Yi.Framework.Core/"]
|
||||
COPY ["src/Yi.Abp.Application/Yi.Abp.Application.csproj", "src/Yi.Abp.Application/"]
|
||||
COPY ["framework/Yi.Framework.Ddd.Application/Yi.Framework.Ddd.Application.csproj", "framework/Yi.Framework.Ddd.Application/"]
|
||||
COPY ["framework/Yi.Framework.Ddd.Application.Contracts/Yi.Framework.Ddd.Application.Contracts.csproj", "framework/Yi.Framework.Ddd.Application.Contracts/"]
|
||||
COPY ["module/bbs/Yi.Framework.Bbs.Application/Yi.Framework.Bbs.Application.csproj", "module/bbs/Yi.Framework.Bbs.Application/"]
|
||||
COPY ["module/rbac/Yi.Framework.Rbac.Application/Yi.Framework.Rbac.Application.csproj", "module/rbac/Yi.Framework.Rbac.Application/"]
|
||||
COPY ["module/rbac/Yi.Framework.Rbac.Application.Contracts/Yi.Framework.Rbac.Application.Contracts.csproj", "module/rbac/Yi.Framework.Rbac.Application.Contracts/"]
|
||||
COPY ["module/rbac/Yi.Framework.Rbac.Domain.Shared/Yi.Framework.Rbac.Domain.Shared.csproj", "module/rbac/Yi.Framework.Rbac.Domain.Shared/"]
|
||||
COPY ["framework/Yi.Framework.Mapster/Yi.Framework.Mapster.csproj", "framework/Yi.Framework.Mapster/"]
|
||||
COPY ["framework/Yi.Framework.SqlSugarCore.Abstractions/Yi.Framework.SqlSugarCore.Abstractions.csproj", "framework/Yi.Framework.SqlSugarCore.Abstractions/"]
|
||||
COPY ["module/rbac/Yi.Framework.Rbac.Domain/Yi.Framework.Rbac.Domain.csproj", "module/rbac/Yi.Framework.Rbac.Domain/"]
|
||||
COPY ["module/bbs/Yi.Framework.Bbs.Application.Contracts/Yi.Framework.Bbs.Application.Contracts.csproj", "module/bbs/Yi.Framework.Bbs.Application.Contracts/"]
|
||||
COPY ["module/bbs/Yi.Framework.Bbs.Domain.Shared/Yi.Framework.Bbs.Domain.Shared.csproj", "module/bbs/Yi.Framework.Bbs.Domain.Shared/"]
|
||||
COPY ["module/bbs/Yi.Framework.Bbs.Domain/Yi.Framework.Bbs.Domain.csproj", "module/bbs/Yi.Framework.Bbs.Domain/"]
|
||||
COPY ["src/Yi.Abp.Application.Contracts/Yi.Abp.Application.Contracts.csproj", "src/Yi.Abp.Application.Contracts/"]
|
||||
COPY ["src/Yi.Abp.Domain.Shared/Yi.Abp.Domain.Shared.csproj", "src/Yi.Abp.Domain.Shared/"]
|
||||
COPY ["src/Yi.Abp.Domain/Yi.Abp.Domain.csproj", "src/Yi.Abp.Domain/"]
|
||||
COPY ["src/Yi.Abp.SqlSugarCore/Yi.Abp.SqlSugarCore.csproj", "src/Yi.Abp.SqlSugarCore/"]
|
||||
COPY ["framework/Yi.Framework.SqlSugarCore/Yi.Framework.SqlSugarCore.csproj", "framework/Yi.Framework.SqlSugarCore/"]
|
||||
COPY ["module/bbs/Yi.Framework.Bbs.SqlSugarCore/Yi.Framework.Bbs.SqlSugarCore.csproj", "module/bbs/Yi.Framework.Bbs.SqlSugarCore/"]
|
||||
COPY ["module/rbac/Yi.Framework.Rbac.SqlSugarCore/Yi.Framework.Rbac.SqlSugarCore.csproj", "module/rbac/Yi.Framework.Rbac.SqlSugarCore/"]
|
||||
RUN dotnet restore "./src/Yi.Abp.Web/./Yi.Abp.Web.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/src/Yi.Abp.Web"
|
||||
RUN dotnet build "./Yi.Abp.Web.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./Yi.Abp.Web.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "Yi.Abp.Web.dll"]
|
||||
@@ -1,22 +0,0 @@
|
||||
# Docker 构建说明
|
||||
|
||||
## 执行命令
|
||||
|
||||
```shell
|
||||
# 在Yi.Abp.Net8 目录下执行
|
||||
docker build -t admin-server:${BUILD_NUMBER} -f ./src/Yi.Abp.Web/Dockerfile .
|
||||
|
||||
```
|
||||
|
||||
## 注意
|
||||
|
||||
NuGet 源国内访问有时候会报错,可以考虑切换成华为源,加上参数
|
||||
|
||||
```shell
|
||||
RUN dotnet restore --source https://repo.huaweicloud.com/repository/nuget/v3/index.json "./src/Yi.Abp.Web/./Yi.Abp.Web.csproj"
|
||||
|
||||
RUN dotnet build --source https://repo.huaweicloud.com/repository/nuget/v3/index.json "./Yi.Abp.Web.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
RUN dotnet publish --source https://repo.huaweicloud.com/repository/nuget/v3/index.json "./Yi.Abp.Web.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
```
|
||||
@@ -188,10 +188,10 @@ namespace Yi.Abp.Web
|
||||
|
||||
//配置Hangfire定时任务存储,开启redis后,优先使用redis
|
||||
var redisConfiguration = configuration["Redis:Configuration"];
|
||||
var redisEnabled = configuration["Redis:IsEnabled"];
|
||||
context.Services.AddHangfire(config=>
|
||||
{
|
||||
if (redisEnabled.IsNullOrEmpty() || bool.Parse(redisEnabled))
|
||||
bool.TryParse( configuration["Redis:IsEnabled"], out var redisEnabled);
|
||||
if (redisEnabled)
|
||||
{
|
||||
config.UseRedisStorage(
|
||||
ConnectionMultiplexer.Connect(redisConfiguration),
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
{
|
||||
//多租户,支持多库,DbConnOptions会自动创建到默认租户,支持配置文件方式+数据库方式,AbpDefaultTenantStoreOptions
|
||||
// "Tenants": [
|
||||
// {
|
||||
// "Id": "33333333-3d72-4339-9adc-845151f8ada0",
|
||||
// "Name": "Mes@MySql",
|
||||
// "ConnectionStrings": {
|
||||
// "Default": "DataSource=mes-dev.db"
|
||||
// },
|
||||
// "IsActive": false
|
||||
// }
|
||||
// ],
|
||||
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
//"Default": "Information",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<AbpVersion>8.2.0</AbpVersion>
|
||||
<SqlSugarVersion>5.1.4.166</SqlSugarVersion>
|
||||
<AbpVersion>8.3.4</AbpVersion>
|
||||
<SqlSugarVersion>5.1.4.176-preview16</SqlSugarVersion>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
33
Yi.Bbs.Vue3/.dockerignore
Normal file
33
Yi.Bbs.Vue3/.dockerignore
Normal file
@@ -0,0 +1,33 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/azds.yaml
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
LICENSE
|
||||
README.md
|
||||
!**/.gitignore
|
||||
!.git/HEAD
|
||||
!.git/config
|
||||
!.git/packed-refs
|
||||
!.git/refs/heads/**
|
||||
appsettings.Development.json
|
||||
appsettings.Production.json
|
||||
appsettings.Staging.json
|
||||
13
Yi.Bbs.Vue3/Dockerfile
Normal file
13
Yi.Bbs.Vue3/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM nginx:stable-alpine3.20-perl as base
|
||||
EXPOSE 18001
|
||||
|
||||
FROM node:20.18 as publish
|
||||
WORKDIR /main
|
||||
COPY . .
|
||||
RUN npm i
|
||||
RUN npm run build
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /main/dist/ .
|
||||
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
||||
8
Yi.Bbs.Vue3/DockerfileFast
Normal file
8
Yi.Bbs.Vue3/DockerfileFast
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM nginx as base
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 18001
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY ["./dist/","."]
|
||||
ENTRYPOINT ["nginx", "-g", "daemon off;"]
|
||||
@@ -22,6 +22,7 @@
|
||||
"i": "^0.3.7",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^4.2.12",
|
||||
"marked-katex-extension": "^5.1.4",
|
||||
"mavon-editor": "^3.0.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"path-to-regexp": "^6.2.1",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, defineProps } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { compile } from "path-to-regexp";
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, defineProps } from "vue";
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getListByDiscussId, add, del } from "@/apis/commentApi.js";
|
||||
import AvatarInfo from "./AvatarInfo.vue";
|
||||
|
||||
@@ -11,7 +11,7 @@ const onClickText=()=>{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card class="box-card" shadow="never" :body-style="{padding: isPadding===false?'0px 20px':'20px 20px'}">
|
||||
<el-card class="box-card" shadow="never" :body-style="{padding: props.isPadding===false?'0px 0px':'20px 20px'}">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ props.header }}</span>
|
||||
@@ -37,7 +37,8 @@ const onClickText=()=>{
|
||||
.el-divider {
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
.VisitsLineChart /deep/ .el-card__body{
|
||||
|
||||
::v-deep(.VisitsLineChart .el-card__body){
|
||||
padding: 0 20px;
|
||||
}
|
||||
.box-card-info {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps } from "vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
isBorder: {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<!-- @node-click="handleNodeClick"-->
|
||||
<el-tree
|
||||
empty-text="无子文章"
|
||||
:data="props.data == '' ? [] : props.data"
|
||||
:props="defaultProps"
|
||||
@node-click="handleNodeClick"
|
||||
|
||||
:expand-on-click-node="false"
|
||||
node-key="id"
|
||||
:default-expand-all="true"
|
||||
@@ -18,7 +19,7 @@
|
||||
:content="data.name"
|
||||
placement="right"
|
||||
>
|
||||
<span class="title-name">{{ data.name }}</span>
|
||||
<span @click="handleNodeClick(data)" class="title-name">{{ data.name }}</span>
|
||||
</el-tooltip>
|
||||
|
||||
<span>
|
||||
@@ -44,6 +45,7 @@
|
||||
删除
|
||||
</a>
|
||||
</span>
|
||||
|
||||
</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
@@ -79,7 +81,7 @@ const { isHasPermission: isRemoveArticle } = getPermission("bbs:article:del");
|
||||
</script>
|
||||
<style scoped>
|
||||
.custom-tree-node {
|
||||
width: 100%;
|
||||
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="UserInfoCard">
|
||||
import { computed, defineProps } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import UserLimitTag from "../UserLimitTag.vue";
|
||||
const props = defineProps({
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
<el-menu-item index="2" @click="enterStart"
|
||||
>开始</el-menu-item>
|
||||
<el-menu-item index="3" @click="enterWatermelon" style="color: red;font-weight: bolder;font-size: large;"
|
||||
>知识宝典</el-menu-item>
|
||||
<el-menu-item index="3" @click="enterBook" style="color: red;font-weight: bolder;font-size: large;"
|
||||
>面试宝典</el-menu-item>
|
||||
<el-menu-item index="4" @click="enterShop"
|
||||
>商城</el-menu-item>
|
||||
<!-- <el-sub-menu index="4">-->
|
||||
@@ -233,9 +233,8 @@ const enterStart = () => {
|
||||
router.push("/start");
|
||||
}
|
||||
|
||||
const enterWatermelon=()=>{
|
||||
// router.push("/dc");
|
||||
alert("即将上线,敬请期待!")
|
||||
const enterBook=()=>{
|
||||
router.push("/book");
|
||||
}
|
||||
const enterShop=()=>{
|
||||
router.push("/shop");
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
<el-icon><Trophy /></el-icon>
|
||||
<span>数字藏品</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="9" :route="{ path: '/book' }">
|
||||
<el-icon><Memo /></el-icon>
|
||||
<span>面试宝典</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -136,6 +136,14 @@ const router = createRouter({
|
||||
title: "数字藏品",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "book",
|
||||
path: "/book",
|
||||
component: () => import("../views/book/Index.vue"),
|
||||
meta: {
|
||||
title: "面试宝典",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,82 +4,84 @@
|
||||
<el-col :span="5">
|
||||
<el-row class="art-info-left">
|
||||
<el-col :span="24">
|
||||
<InfoCard header="文章信息" text="展开" hideDivider="true">
|
||||
<InfoCard header="文章信息" text="展开" hideDivider="true" :isPadding="false" style="padding: 10px">
|
||||
<template #content>
|
||||
<el-button
|
||||
style="width: 100%; margin-bottom: 0.8rem"
|
||||
@click="loadDiscuss(true)"
|
||||
>主题首页</el-button
|
||||
style="width: 100%; margin-bottom: 0.8rem"
|
||||
@click="loadDiscuss(true)"
|
||||
>主题首页
|
||||
</el-button
|
||||
>
|
||||
<el-button
|
||||
v-if="isAddArticle && isArticleUser"
|
||||
@click="addArticle('00000000-0000-0000-0000-000000000000')"
|
||||
type="primary"
|
||||
style="width: 100%; margin-bottom: 0.8rem; margin-left: 0"
|
||||
>添加子文章</el-button
|
||||
v-if="isAddArticle && isArticleUser"
|
||||
@click="addArticle('00000000-0000-0000-0000-000000000000')"
|
||||
type="primary"
|
||||
style="width: 100%; margin-bottom: 0.8rem; margin-left: 0"
|
||||
>添加子文章
|
||||
</el-button
|
||||
>
|
||||
<!--目录在这里 -->
|
||||
<el-scrollbar style="min-height: 410px">
|
||||
<el-scrollbar style="height:600px;overflow-y: auto;">
|
||||
<TreeArticleInfo
|
||||
:data="articleData"
|
||||
@remove="delArticle"
|
||||
@update="updateArticle"
|
||||
@create="addNextArticle"
|
||||
@handleNodeClick="handleNodeClick"
|
||||
:currentNodeKey="currentNodeKey"
|
||||
:isArticleUser="isArticleUser"
|
||||
:data="articleData"
|
||||
@remove="delArticle"
|
||||
@update="updateArticle"
|
||||
@create="addNextArticle"
|
||||
@handleNodeClick="handleNodeClick"
|
||||
:currentNodeKey="currentNodeKey"
|
||||
:isArticleUser="isArticleUser"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<InfoCard :items="authorList" :isPadding="false" header="作者分享" height="410" text="更多">
|
||||
<InfoCard :items="authorList" :isPadding="false" header="作者分享" height="410" text="更多" style="padding:0 20px">
|
||||
<template #item="temp">
|
||||
<ThemeData :themeData="temp"/>
|
||||
</template>
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
<!-- <el-col :span="24">-->
|
||||
<!-- <InfoCard :items="items" header="内容推荐" text="更多">-->
|
||||
<!-- <template #item="temp">-->
|
||||
<!-- <AvatarInfo />-->
|
||||
<!-- </template>-->
|
||||
<!-- </InfoCard>-->
|
||||
<!-- </el-col>-->
|
||||
<!-- <el-col :span="24">-->
|
||||
<!-- <InfoCard :items="items" header="内容推荐" text="更多">-->
|
||||
<!-- <template #item="temp">-->
|
||||
<!-- <AvatarInfo />-->
|
||||
<!-- </template>-->
|
||||
<!-- </InfoCard>-->
|
||||
<!-- </el-col>-->
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="14">
|
||||
<el-row class="left-div">
|
||||
<el-col :span="24">
|
||||
<Breadcrumb :breadcrumbsList="breadcrumbsList" class="breadcrumb" />
|
||||
<Breadcrumb :breadcrumbsList="breadcrumbsList" class="breadcrumb"/>
|
||||
<!-- {{ discuss.user }} -->
|
||||
<AvatarInfo
|
||||
:size="50"
|
||||
:showWatching="true"
|
||||
:time="discuss.creationTime"
|
||||
:userInfo="discuss.user"
|
||||
:size="50"
|
||||
:showWatching="true"
|
||||
:time="discuss.creationTime"
|
||||
:userInfo="discuss.user"
|
||||
></AvatarInfo>
|
||||
<!-- :userInfo="{nick:'qwe'} -->
|
||||
|
||||
|
||||
<h2>{{ discuss.title }}</h2>
|
||||
<h5 class="subtitle">{{discuss.introduction }}</h5>
|
||||
<h5 class="subtitle">{{ discuss.introduction }}</h5>
|
||||
<el-image
|
||||
:preview-src-list="[getUrl(discuss.cover)]"
|
||||
v-if="discuss.cover"
|
||||
:src="getUrl(discuss.cover)"
|
||||
style="width: 150px; height: 150px"
|
||||
:preview-src-list="[getUrl(discuss.cover)]"
|
||||
v-if="discuss.cover"
|
||||
:src="getUrl(discuss.cover)"
|
||||
style="width: 150px; height: 150px"
|
||||
/>
|
||||
<el-divider />
|
||||
<el-divider/>
|
||||
<ArticleContentInfo
|
||||
:code="discuss.content ?? ''"
|
||||
:code="discuss.content ?? ''"
|
||||
></ArticleContentInfo>
|
||||
|
||||
<el-divider class="tab-divider" />
|
||||
<el-divider class="tab-divider"/>
|
||||
|
||||
<el-space :size="10" :spacer="spacer">
|
||||
<AgreeInfo :data="discuss" />
|
||||
<AgreeInfo :data="discuss"/>
|
||||
<el-button icon="Star" text> 0</el-button>
|
||||
<el-button icon="Share" text> 分享</el-button>
|
||||
<el-button icon="Operation" text> 操作</el-button>
|
||||
@@ -87,38 +89,40 @@
|
||||
</el-col>
|
||||
|
||||
<el-col :span="24" class="comment">
|
||||
<CommentInfo :isComment="isDisabledCreateComment" />
|
||||
<CommentInfo :isComment="isDisabledCreateComment"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<BottomInfo />
|
||||
<BottomInfo/>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="5">
|
||||
<el-row class="right-div">
|
||||
<el-col :span="24">
|
||||
<InfoCard
|
||||
class="art-info-right"
|
||||
header="主题信息"
|
||||
text="更多"
|
||||
hideDivider="true"
|
||||
class="art-info-right"
|
||||
header="主题信息"
|
||||
text="更多"
|
||||
hideDivider="true"
|
||||
>
|
||||
<template #content>
|
||||
<div>
|
||||
<ul class="art-info-ul">
|
||||
<li>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="default"
|
||||
v-if="isEditTheme && isArticleUser"
|
||||
@click="updateHander(route.params.discussId)"
|
||||
>编辑</el-button
|
||||
type="primary"
|
||||
size="default"
|
||||
v-if="isEditTheme && isArticleUser"
|
||||
@click="updateHander(route.params.discussId)"
|
||||
>编辑
|
||||
</el-button
|
||||
>
|
||||
<el-button
|
||||
style="margin-left: 1rem"
|
||||
type="danger"
|
||||
v-if="isRemoveTheme && isArticleUser"
|
||||
@click="delHander(route.params.discussId)"
|
||||
>删除</el-button
|
||||
style="margin-left: 1rem"
|
||||
type="danger"
|
||||
v-if="isRemoveTheme && isArticleUser"
|
||||
@click="delHander(route.params.discussId)"
|
||||
>删除
|
||||
</el-button
|
||||
>
|
||||
</li>
|
||||
<li>分类: <span>主题</span></li>
|
||||
@@ -135,18 +139,19 @@
|
||||
<template #content>
|
||||
<div>
|
||||
<el-empty
|
||||
:image-size="100"
|
||||
style="padding: 20px 0"
|
||||
v-if="catalogueData.length == 0"
|
||||
description="无目录"
|
||||
:image-size="100"
|
||||
style="padding: 20px 0"
|
||||
v-if="catalogueData.length == 0"
|
||||
description="无目录"
|
||||
/>
|
||||
<ul v-else class="art-info-ul">
|
||||
<li v-for="(item, i) in catalogueData" :key="i">
|
||||
<el-button
|
||||
style="width: 100%; justify-content: left"
|
||||
type="primary"
|
||||
text
|
||||
>{{ `${i + 1} : ${item}` }}</el-button
|
||||
style="width: 100%; justify-content: left"
|
||||
type="primary"
|
||||
text
|
||||
>{{ `${i + 1} : ${item}` }}
|
||||
</el-button
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -155,62 +160,62 @@
|
||||
</InfoCard>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<InfoCard :items="themeList" :isPadding="false" header="推荐主题" text="更多" height="500">
|
||||
<InfoCard :items="themeList" :isPadding="false" header="推荐主题" text="更多" height="500" style="padding:0 20px">
|
||||
<template #item="temp">
|
||||
<ThemeData :themeData="temp"/>
|
||||
</template>
|
||||
</InfoCard>
|
||||
<!-- <InfoCard :items="items" header="其他" text="更多">-->
|
||||
<!-- <template #item="temp">-->
|
||||
<!-- <AvatarInfo />-->
|
||||
<!-- </template>-->
|
||||
<!-- </InfoCard>-->
|
||||
<!-- <InfoCard :items="items" header="其他" text="更多">-->
|
||||
<!-- <template #item="temp">-->
|
||||
<!-- <AvatarInfo />-->
|
||||
<!-- </template>-->
|
||||
<!-- </InfoCard>-->
|
||||
</el-col>
|
||||
<!-- <el-col :span="24">-->
|
||||
<!-- <InfoCard :items="items" header="其他" text="更多">-->
|
||||
<!-- <template #item="temp">-->
|
||||
<!-- <AvatarInfo />-->
|
||||
<!-- </template>-->
|
||||
<!-- </InfoCard>-->
|
||||
<!-- </el-col>-->
|
||||
<!-- <el-col :span="24">-->
|
||||
<!-- <InfoCard :items="items" header="其他" text="更多">-->
|
||||
<!-- <template #item="temp">-->
|
||||
<!-- <AvatarInfo />-->
|
||||
<!-- </template>-->
|
||||
<!-- </InfoCard>-->
|
||||
<!-- </el-col>-->
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { h, ref, onMounted, watch, computed } from "vue";
|
||||
import {h, ref, onMounted, watch, computed} from "vue";
|
||||
import AvatarInfo from "@/components/AvatarInfo.vue";
|
||||
import InfoCard from "@/components/InfoCard.vue";
|
||||
import ArticleContentInfo from "@/components/ArticleContentInfo.vue";
|
||||
import CommentInfo from "@/components/CommentInfo.vue";
|
||||
import BottomInfo from "@/components/BottomInfo.vue";
|
||||
import TreeArticleInfo from "@/components/TreeArticleInfo.vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import AgreeInfo from "@/components/AgreeInfo.vue";
|
||||
import { get as discussGet, del as discussDel } from "@/apis/discussApi.js";
|
||||
import {get as discussGet, del as discussDel} from "@/apis/discussApi.js";
|
||||
import {
|
||||
all as articleall,
|
||||
del as articleDel,
|
||||
get as articleGet,
|
||||
} from "@/apis/articleApi.js";
|
||||
import Breadcrumb from "@/components/Breadcrumb/index.vue";
|
||||
import { getPermission } from "@/utils/auth";
|
||||
import {getPermission} from "@/utils/auth";
|
||||
import useUserStore from "@/stores/user.js";
|
||||
import ThemeData from "@/views/home/components/RecommendTheme/index.vue";
|
||||
import {getRecommendedTopic,getAuthorTopic} from "@/apis/analyseApi";
|
||||
import {getRecommendedTopic, getAuthorTopic} from "@/apis/analyseApi";
|
||||
//数据定义
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const spacer = h(ElDivider, { direction: "vertical" });
|
||||
const spacer = h(ElDivider, {direction: "vertical"});
|
||||
//子文章数据
|
||||
const articleData = ref([]);
|
||||
//主题内容
|
||||
const discuss = ref({});
|
||||
//推荐主题
|
||||
const themeList=ref([]);
|
||||
const themeList = ref([]);
|
||||
//作者主题
|
||||
const authorList=ref([]);
|
||||
const authorList = ref([]);
|
||||
//封面url
|
||||
const getUrl = (str) => {
|
||||
return `${import.meta.env.VITE_APP_BASEAPI}/file/${str}`;
|
||||
@@ -225,10 +230,10 @@ const catalogueData = ref([]);
|
||||
const breadcrumbsList = ref([]);
|
||||
const resultRouters = ["index", "discuss", "themeCover"];
|
||||
breadcrumbsList.value = route.matched[0].children
|
||||
.filter((item) => resultRouters.includes(item.name))
|
||||
.sort((a, b) => {
|
||||
return resultRouters.indexOf(a.name) - resultRouters.indexOf(b.name);
|
||||
});
|
||||
.filter((item) => resultRouters.includes(item.name))
|
||||
.sort((a, b) => {
|
||||
return resultRouters.indexOf(a.name) - resultRouters.indexOf(b.name);
|
||||
});
|
||||
|
||||
// 当前文章名称
|
||||
const currentArticle = ref("");
|
||||
@@ -242,9 +247,9 @@ const loadArticleData = async () => {
|
||||
//主题初始化
|
||||
const isDisabledCreateComment = ref(false);
|
||||
const isArticleUser = ref(false);
|
||||
const { isHasPermission: isAddArticle } = getPermission("bbs:article:add");
|
||||
const { isHasPermission: isEditTheme } = getPermission("bbs:discuss:update");
|
||||
const { isHasPermission: isRemoveTheme } = getPermission("bbs:discuss:del");
|
||||
const {isHasPermission: isAddArticle} = getPermission("bbs:article:add");
|
||||
const {isHasPermission: isEditTheme} = getPermission("bbs:discuss:update");
|
||||
const {isHasPermission: isRemoveTheme} = getPermission("bbs:discuss:del");
|
||||
const loadDiscuss = async (isRewrite) => {
|
||||
if (isRewrite) {
|
||||
//跳转路由
|
||||
@@ -357,9 +362,7 @@ const handleNodeClick = async (data) => {
|
||||
|
||||
router.push(`/article/${route.params.discussId}/${data.id}`);
|
||||
|
||||
const response = await articleGet(data.id);
|
||||
discuss.value.content = response.data.content;
|
||||
ContentHander();
|
||||
|
||||
};
|
||||
//删除子文章
|
||||
const delArticle = (node, data) => {
|
||||
@@ -377,11 +380,11 @@ const delArticle = (node, data) => {
|
||||
});
|
||||
});
|
||||
};
|
||||
const loadThemeData =async () => {
|
||||
const loadThemeData = async () => {
|
||||
const {data: themeData} = await getRecommendedTopic();
|
||||
themeList.value = themeData;
|
||||
}
|
||||
const loadAuthorData=async () => {
|
||||
const loadAuthorData = async () => {
|
||||
const {data: authorData} = await getAuthorTopic(discuss.value.user.id);
|
||||
authorList.value = authorData;
|
||||
}
|
||||
@@ -394,39 +397,53 @@ onMounted(async () => {
|
||||
|
||||
|
||||
watch(
|
||||
() => currentArticle.value,
|
||||
(val) => {
|
||||
if (val !== "") {
|
||||
breadcrumbsList.value[3] = {
|
||||
name: "article",
|
||||
path: "/article/:discussId",
|
||||
meta: {
|
||||
title: currentArticle.value,
|
||||
},
|
||||
};
|
||||
() => currentArticle.value,
|
||||
(val) => {
|
||||
if (val !== "") {
|
||||
breadcrumbsList.value[3] = {
|
||||
name: "article",
|
||||
path: "/article/:discussId",
|
||||
meta: {
|
||||
title: currentArticle.value,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => route.params.articleId,
|
||||
async (val) => {
|
||||
if (val === "") {
|
||||
discuss.value = (await discussGet(route.params.discussId)).data;
|
||||
() => route.params,
|
||||
async (val) => {
|
||||
if (val.articleId !=="")
|
||||
{
|
||||
const response = await articleGet(route.params.articleId);
|
||||
discuss.value.content = response.data.content;
|
||||
ContentHander();
|
||||
}
|
||||
else if (val.discussId !== "") {
|
||||
discuss.value = (await discussGet(route.params.discussId)).data;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
//路由发送变化,重新加载
|
||||
// watch(() => route.params, async () => {
|
||||
// await loadDiscuss();
|
||||
// await loadArticleData();
|
||||
// await loadAuthorData();
|
||||
// await loadThemeData();
|
||||
// }
|
||||
// )
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
.subtitle
|
||||
{
|
||||
color:#999AAA;
|
||||
.subtitle {
|
||||
color: #999AAA;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.article-box {
|
||||
width: 1500px;
|
||||
height: 100%;
|
||||
|
||||
.comment {
|
||||
min-height: 40rem;
|
||||
}
|
||||
|
||||
53
Yi.Bbs.Vue3/src/views/book/Index.vue
Normal file
53
Yi.Bbs.Vue3/src/views/book/Index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
<script setup>
|
||||
import BookCard from './components/BookCard.vue'
|
||||
import {getList} from '@/apis/discussApi'
|
||||
import { ref, onMounted } from "vue";
|
||||
import {useRouter} from "vue-router";
|
||||
const discussList=ref([]);
|
||||
|
||||
//面试宝典板块id
|
||||
const bookPlateId="d940818d-90ec-9dbe-b7af-3a0f935dac0a";
|
||||
onMounted(async ()=>{
|
||||
const {data}=await getList({plateId:bookPlateId, skipCount:1,maxResultCount:100});
|
||||
discussList.value=data.items;
|
||||
})
|
||||
const router = useRouter();
|
||||
const enterDiscuss = (id) => {
|
||||
router.push(`/article/${id}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="body">
|
||||
<p class="title">面试宝典</p>
|
||||
<div class="content">
|
||||
<el-row :gutter="20">
|
||||
<el-col @click="enterDiscuss(discussInfo.id)" v-for="discussInfo in discussList" :xs="12" :sm="8" :md="8" :lg="6" :xl="6">
|
||||
<BookCard :discuss="discussInfo"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<style scoped>
|
||||
.el-col{
|
||||
cursor: pointer; /* 显示手型光标 */
|
||||
margin: 10px 0;
|
||||
}
|
||||
.body{
|
||||
width: 1200px;
|
||||
.title{
|
||||
margin-top: 0.5em;
|
||||
font-size: 24px;
|
||||
line-height: 1.2667;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
71
Yi.Bbs.Vue3/src/views/book/components/BookCard.vue
Normal file
71
Yi.Bbs.Vue3/src/views/book/components/BookCard.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
const props = defineProps([
|
||||
"discuss",
|
||||
]);
|
||||
const discuss=ref(props.discuss)
|
||||
onMounted(()=>{
|
||||
|
||||
})
|
||||
const getUrl = (str) => {
|
||||
return `${import.meta.env.VITE_APP_BASEAPI}/file/${str}`;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="card-body">
|
||||
<div class="left-logo">
|
||||
<el-image style="width: 40px; height: 40px"
|
||||
:src="getUrl(discuss.cover)"
|
||||
fit="fill">
|
||||
<template #error>
|
||||
<el-icon size="40px" color="#F0F2F5"><Picture /></el-icon>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
</div>
|
||||
<div class="right-content">
|
||||
|
||||
<div class="content-top">
|
||||
{{discuss.title}}
|
||||
</div>
|
||||
<div class="content-bottom">
|
||||
{{discuss.introduction}}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.card-body {
|
||||
width: 100%;
|
||||
background-color: #FFFFFF;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
.left-logo{
|
||||
|
||||
}
|
||||
.right-content{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: 15px;
|
||||
overflow: hidden;
|
||||
.content-top{
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.content-bottom{
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,6 +18,7 @@ import {getUrl} from '@/utils/icon'
|
||||
|
||||
//markdown ai显示
|
||||
import {marked} from 'marked';
|
||||
import markedKatex from "marked-katex-extension";
|
||||
import '@/assets/atom-one-dark.css';
|
||||
import '@/assets/github-markdown.css';
|
||||
import hljs from "highlight.js";
|
||||
@@ -39,11 +40,11 @@ const currentInputValue = ref("");
|
||||
//临时存储的输入框,根据用户id及组name、all组为key,data为value
|
||||
const inputListDataStore = ref([
|
||||
{key: "all", name: "官方学习交流群", titleName: "官方学习交流群", logo: "yilogo.png", value: ""},
|
||||
{key: "ai@deepseek-chat", name: "DeepSeek聊天", titleName: "DeepSeek-聊天模式", logo: "deepSeekAi.png", value: ""},
|
||||
{key: "ai@deepseek-chat", name: "DeepSeek聊天", titleName: "满血!DeepSeek-聊天模式", logo: "deepSeekAi.png", value: ""},
|
||||
{
|
||||
key: "ai@DeepSeek-R1",
|
||||
key: "ai@deepseek-ai/deepseek-r1",
|
||||
name: "DeepSeek思索",
|
||||
titleName: "DeepSeek-思索模式",
|
||||
titleName: "满血!DeepSeek-思索模式",
|
||||
logo: "deepSeekAi.png",
|
||||
value: ""
|
||||
},
|
||||
@@ -66,6 +67,25 @@ onMounted(async () => {
|
||||
onclickClose();
|
||||
}, 3000);
|
||||
}
|
||||
marked.use(markedKatex({
|
||||
throwOnError: false,
|
||||
nonStandard: true
|
||||
}));
|
||||
marked.setOptions({
|
||||
renderer: new marked.Renderer(),
|
||||
highlight: function (code, language) {
|
||||
return codeHandler(code, language);
|
||||
},
|
||||
pedantic: false,
|
||||
gfm: true,//允许 Git Hub标准的markdown
|
||||
tables: true,//支持表格
|
||||
breaks: true,
|
||||
sanitize: false,
|
||||
smartypants: false,
|
||||
xhtml: false,
|
||||
smartLists: true,
|
||||
}
|
||||
);
|
||||
//all的聊天消息
|
||||
chatStore.setMsgList((await getChatAccountMessageList()).data);
|
||||
//在线用户列表
|
||||
@@ -117,7 +137,7 @@ const currentMsgContext = computed(() => {
|
||||
});
|
||||
//获取聊天内容的头像
|
||||
const getChatUrl = (url, position) => {
|
||||
if (position === "left" && (selectIsAi()||selectIsAll())) {
|
||||
if (position === "left" && selectIsAi()) {
|
||||
return imageSrc(inputListDataStore.value.find(x=>x.key===currentSelectUser.value).logo)
|
||||
}
|
||||
return getUrl(url);
|
||||
@@ -125,7 +145,7 @@ const getChatUrl = (url, position) => {
|
||||
//当前聊天框显示的名称
|
||||
const currentHeaderName = computed(() => {
|
||||
if (selectIsAll()||selectIsAi()) {
|
||||
return inputListDataStore.value.find(x=>x.key===currentSelectUser.value).name;
|
||||
return inputListDataStore.value.find(x=>x.key===currentSelectUser.value).titleName;
|
||||
} else {
|
||||
return currentSelectUser.value.userName;
|
||||
}
|
||||
@@ -333,27 +353,14 @@ const clearAiMsg = () => {
|
||||
|
||||
//转换markdown
|
||||
const toMarkDownHtml = (text) => {
|
||||
marked.setOptions({
|
||||
renderer: new marked.Renderer(),
|
||||
highlight: function (code, language) {
|
||||
return codeHandler(code, language);
|
||||
},
|
||||
pedantic: false,
|
||||
gfm: true,//允许 Git Hub标准的markdown
|
||||
tables: true,//支持表格
|
||||
breaks: true,
|
||||
sanitize: false,
|
||||
smartypants: false,
|
||||
xhtml: false,
|
||||
smartLists: true,
|
||||
}
|
||||
);
|
||||
//处理数学公式
|
||||
let soureMd=text.replace(/\\\[/g, '$').replace(/\\\]/g, '$');
|
||||
//需要注意代码块样式
|
||||
const soureHtml = marked(text);
|
||||
let soureHtml = marked(soureMd);
|
||||
nextTick(() => {
|
||||
addCopyEvent();
|
||||
})
|
||||
return soureHtml;
|
||||
return soureHtml;
|
||||
}
|
||||
//code部分处理、高亮
|
||||
const codeHandler = (code, language) => {
|
||||
@@ -421,7 +428,7 @@ const clickCopyEvent = async function (event) {
|
||||
const spanId = event.target.id;
|
||||
console.log(codeCopyDic, "codeCopyDic")
|
||||
console.log(spanId, "spanId")
|
||||
await navigator.clipboard.writeText(codeCopyDic.filter(x => x.id === spanId)[0].code);
|
||||
await navigator.clipboard.writeText(codeCopyDic.filter(x => x.id == spanId)[0].code);
|
||||
ElMessage({
|
||||
message: "代码块复制成功",
|
||||
type: "success",
|
||||
@@ -433,7 +440,7 @@ const clickCopyEvent = async function (event) {
|
||||
<template>
|
||||
|
||||
<div style="position: absolute; top: 0;left: 0;" v-show="isShowTipNumber>0">
|
||||
<p>当前版本:2.0.0</p>
|
||||
<p>当前版本:2.1.0</p>
|
||||
<p>tip:官方学习交流群每次发送消息消耗 1 钱钱</p>
|
||||
<p>tip:点击聊天窗口右上角“X”可退出</p>
|
||||
<p>tip:多人同时在聊天室时,左侧可显示其他成员</p>
|
||||
@@ -1122,17 +1129,39 @@ ul {
|
||||
color: red;
|
||||
cursor: pointer; /* 设置鼠标悬浮为手型 */
|
||||
}
|
||||
|
||||
::v-deep(.katex-html)
|
||||
{
|
||||
color: #7B7C7C;
|
||||
font-size: smaller;
|
||||
}
|
||||
::v-deep(.nav-ul) {
|
||||
border-right: 1px solid #FFFFFF;
|
||||
margin-top: 12px;
|
||||
margin-left: 0 !important;
|
||||
padding-left: 10px;
|
||||
padding-right: 2px;
|
||||
|
||||
.nav-li {
|
||||
margin: 1.0px 0;
|
||||
text-align: right;
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
.content-msg-common ::v-deep(ul){
|
||||
margin-left: 20px;
|
||||
}
|
||||
.content-msg-common ::v-deep(ol){
|
||||
margin-left: 20px;
|
||||
}
|
||||
::v-deep(.katex){
|
||||
margin: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
@@ -27,7 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, defineProps, defineEmits } from "vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
|
||||
@@ -162,6 +162,7 @@ margin: 10px auto;">
|
||||
<el-col v-if="!isIcp" :span="24">
|
||||
<template v-if="isPointFinished">
|
||||
<InfoCard :isPadding="false" :items="pointList" header="财富排行榜" text="查看我的位置" height="410"
|
||||
style="padding:0 20px"
|
||||
@onClickText="onClickMoneyTop">
|
||||
<template #item="temp">
|
||||
<PointsRanking :pointsData="temp"/>
|
||||
@@ -179,7 +180,8 @@ margin: 10px auto;">
|
||||
|
||||
<el-col v-if="!isIcp" :span="24">
|
||||
<template v-if="isFriendFinished">
|
||||
<InfoCard :isPadding="false" :items="friendList" header="推荐好友" text="更多" height="400">
|
||||
<InfoCard :isPadding="false" :items="friendList" header="推荐好友" text="更多" height="400"
|
||||
style="padding:0 20px">
|
||||
<template #item="temp">
|
||||
<RecommendFriend :friendData="temp"/>
|
||||
</template>
|
||||
@@ -195,7 +197,9 @@ margin: 10px auto;">
|
||||
</el-col>
|
||||
<el-col v-if="!isIcp" :span="24">
|
||||
<template v-if="isThemeFinished">
|
||||
<InfoCard :isPadding="false" :items="themeList" header="推荐主题" text="更多" height="400">
|
||||
<InfoCard :isPadding="false" :items="themeList" header="推荐主题" text="更多" height="400"
|
||||
style="padding:0 20px"
|
||||
>
|
||||
<template #item="temp">
|
||||
<ThemeData :themeData="temp"/>
|
||||
</template>
|
||||
@@ -307,10 +311,10 @@ const activeList = [
|
||||
{name: "排行榜", path: "/money", icon: "Money"},
|
||||
{name: "开始", path: "/start", icon: "Position"},
|
||||
{name: "聊天室", path: "/chat", icon: "ChatRound"},
|
||||
|
||||
|
||||
|
||||
{name: "商城", path: "/shop", icon: "ShoppingCart"},
|
||||
{name: "数字藏品", path: "/dc", icon: "Trophy"},
|
||||
{name: "面试宝典", path: "/book", icon: "Memo"},
|
||||
// {name: "小程序", path: "/", icon: "Position"},
|
||||
// {name: "公众号", path: "/", icon: "ChatRound"},
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="AccessLogChart">
|
||||
import { ref, defineEmits, defineProps, defineExpose } from "vue";
|
||||
import { ref } from "vue";
|
||||
import useEcharts from "@/hooks/useEcharts";
|
||||
import { accessLogEchartsConfig } from "../../hooks/accessLogEchartsConfig";
|
||||
const props = defineProps({
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="PointsRanking">
|
||||
import { defineProps, computed } from "vue";
|
||||
import { computed } from "vue";
|
||||
import UserInfoCard from "@/components/UserInfoCard/index.vue";
|
||||
import UserLimitTag from "@/components/UserLimitTag.vue";
|
||||
const props = defineProps({
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="RecommendFriend">
|
||||
import { defineProps, computed } from "vue";
|
||||
import { computed } from "vue";
|
||||
import UserInfoCard from "@/components/UserInfoCard/index.vue";
|
||||
import UserLimitTag from "@/components/UserLimitTag.vue";
|
||||
const props = defineProps({
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="RecommendFriend">
|
||||
import { defineProps, ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const props = defineProps({
|
||||
@@ -57,7 +57,6 @@ const seeNumLength = ref(props.themeData.seeNum.toString().length);
|
||||
|
||||
const handleClickTheme = (id) => {
|
||||
router.push(`/article/${id}`);
|
||||
router.go(0);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="VisitsLineChart">
|
||||
import { ref, defineEmits, defineProps, defineExpose } from "vue";
|
||||
import { ref } from "vue";
|
||||
import useEcharts from "@/hooks/useEcharts";
|
||||
import { statisticsEcharts } from "../../hooks/echartsConfig";
|
||||
const props = defineProps({
|
||||
|
||||
28
Yi.Bbs.Vue3/yi-bbs.conf
Normal file
28
Yi.Bbs.Vue3/yi-bbs.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
server {
|
||||
client_header_buffer_size 10k;
|
||||
large_client_header_buffers 40 40k;
|
||||
listen 18001;
|
||||
server_name _;
|
||||
client_max_body_size 100m;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
location /{
|
||||
root /app;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
|
||||
location /prod-api/ {
|
||||
proxy_pass http://ccnetcore.com:19001/api/app/;
|
||||
}
|
||||
|
||||
location /prod-ws/ {
|
||||
proxy_pass http://ccnetcore.com:19001/hub/;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user