Compare commits

...

22 Commits

Author SHA1 Message Date
橙子
45736dfce9 feat: 完成多租户优化改造 2025-02-22 15:26:00 +08:00
chenchun
753b5b0a26 feat: 优化多租户配置 2025-02-21 18:00:06 +08:00
橙子
1fd4f2754a feat:优化文章性能 2025-02-12 22:25:27 +08:00
橙子
bedee3391e feat:聊天室支持公式,优化文章 2025-02-12 22:25:15 +08:00
橙子
176a672e86 update README-en.md.
Signed-off-by: 橙子 <454313500@qq.com>
2025-02-09 14:29:55 +00:00
橙子
9da6bcde41 update README.md.
Signed-off-by: 橙子 <454313500@qq.com>
2025-02-09 14:29:18 +00:00
橙子
3f8daa1d17 update README-Docker.md.
Signed-off-by: 橙子 <454313500@qq.com>
2025-02-09 14:25:52 +00:00
橙子
ed873da3b6 feat: 支持docker 2025-02-09 22:23:23 +08:00
橙子
c0dfa83828 doc: 完善docker文档 2025-02-09 01:49:07 +08:00
橙子
db08688968 Merge remote-tracking branch 'origin/abp' into abp 2025-02-09 01:28:22 +08:00
橙子
400a146a48 feat: 新增docker支持 2025-02-09 01:28:13 +08:00
chenchun
a645264da7 feat: 统一修改时区 2025-02-08 10:39:53 +08:00
chenchun
09d19d876f fix: 时区默认采用上海 2025-02-08 10:37:33 +08:00
橙子
373877cfcf feat: 支持hangfire内存模式 2025-02-07 17:52:38 +08:00
橙子
9e143c0a75 style: 修改job时间 2025-02-07 16:06:46 +08:00
橙子
37b16e8395 perf: 整体优化细节 2025-02-06 12:54:48 +08:00
橙子
9c94953e0e fix: 修复文件上传问题 2025-02-06 11:41:56 +08:00
橙子
2c48b8f881 feat:新增面试宝典 2025-02-05 11:58:50 +08:00
橙子
4c4b78dda7 perf: 优化包版本 2025-02-05 11:36:20 +08:00
橙子
4ba9a7917f feat:支持更新时间或者创建时间排序 2025-02-05 00:59:25 +08:00
橙子
5d286ebc9e feat: db操作支持不修改更新审计日志 2025-02-05 00:02:04 +08:00
橙子
e69bbb46b3 fix: 修复跳转刷新问题 2025-02-04 15:47:59 +08:00
55 changed files with 839 additions and 356 deletions

1
.gitignore vendored
View File

@@ -263,6 +263,7 @@ src/Acme.BookStore.Blazor.Server.Tiered/Logs/*
# Use abp install-libs to restore. # Use abp install-libs to restore.
**/wwwroot/libs/* **/wwwroot/libs/*
public
dist dist
.vscode .vscode
/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.Development.json /Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.Development.json

37
README-Docker.md Normal file
View 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 .

View File

@@ -35,6 +35,18 @@ A Comprehensive Solution, Ultimately Just Another Wheel.
- Yi.RuoYi.Vue3RuoYi JS Backend Frontend - Yi.RuoYi.Vue3RuoYi JS Backend Frontend
**** ****
## 🍉 docker
Full contentREADME-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 ## 🍊 Official website and demo link

View File

@@ -41,6 +41,17 @@ Yi框架-一套与SqlSugar一样爽的.Net8开源框架。
- Yi.Pure.Vue3Pure ts后台前端 - Yi.Pure.Vue3Pure ts后台前端
- Yi.RuoYi.Vue3RuoYi js后台前端 - Yi.RuoYi.Vue3RuoYi 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] 完全支持微服务架构 - [x] 完全支持微服务架构
**** ****
## 🍇 详细到爆炸的Yi框架教程导航 ## 🍇 详细到爆炸的Yi框架教程导航
1. [框架快速开始教程](https://ccnetcore.com/article/aaa00329-7f35-d3fe-d258-3a0f8380b742)(已完成) 1. [框架快速开始教程](https://ccnetcore.com/article/aaa00329-7f35-d3fe-d258-3a0f8380b742)(已完成)

View File

@@ -27,4 +27,7 @@ README.md
!.git/HEAD !.git/HEAD
!.git/config !.git/config
!.git/packed-refs !.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
View 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"]

View 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"]

View File

@@ -36,6 +36,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
version.props = version.props version.props = version.props
publish.bat = publish.bat publish.bat = publish.bat
publish_Demo.bat = publish_Demo.bat publish_Demo.bat = publish_Demo.bat
Dockerfile = Dockerfile
DockerfileFast = DockerfileFast
EndProjectSection EndProjectSection
EndProject 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}" 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}"

View File

@@ -1,7 +1,10 @@
using Hangfire; using System.Linq.Expressions;
using Hangfire;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.BackgroundWorkers; using Volo.Abp.BackgroundWorkers;
using Volo.Abp.BackgroundWorkers.Hangfire; using Volo.Abp.BackgroundWorkers.Hangfire;
using Volo.Abp.DynamicProxy;
namespace Yi.Framework.BackgroundWorkers.Hangfire; namespace Yi.Framework.BackgroundWorkers.Hangfire;
@@ -19,11 +22,28 @@ public class YiFrameworkBackgroundWorkersHangfireModule : AbpModule
var backgroundWorkerManager = context.ServiceProvider.GetRequiredService<IBackgroundWorkerManager>(); var backgroundWorkerManager = context.ServiceProvider.GetRequiredService<IBackgroundWorkerManager>();
var works = context.ServiceProvider.GetServices<IHangfireBackgroundWorker>(); 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) foreach (var work in works)
{ {
//如果为空,默认使用服务器本地utc时间 //如果为空,默认使用服务器本地上海时间
work.TimeZone ??= TimeZoneInfo.Local; work.TimeZone = TimeZoneInfo.Local;
await backgroundWorkerManager.AddAsync(work); 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
});
}
} }
} }

View File

@@ -3,7 +3,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="SqlSugarCoreNoDrive" Version="$(SqlSugarVersion)" /> <!-- <PackageReference Include="SqlSugarCoreNoDrive" Version="$(SqlSugarVersion)" />-->
<PackageReference Include="SqlSugarCore" Version="$(SqlSugarVersion)" />
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" /> <PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
</ItemGroup> </ItemGroup>

View File

@@ -61,7 +61,12 @@ public class DefaultSqlSugarDbContext : SqlSugarDbContext
if (entityInfo.PropertyName.Equals(nameof(IAuditedObject.LastModificationTime))) 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); entityInfo.SetValue(DateTime.Now);
} }
@@ -70,7 +75,12 @@ public class DefaultSqlSugarDbContext : SqlSugarDbContext
{ {
if (typeof(Guid?) == entityInfo.EntityColumnInfo.PropertyInfo.PropertyType) 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); entityInfo.SetValue(CurrentUser.Id);
} }

View File

@@ -22,8 +22,10 @@ namespace Yi.Framework.SqlSugarCore
private IAbpLazyServiceProvider LazyServiceProvider { get; } private IAbpLazyServiceProvider LazyServiceProvider { get; }
private TenantConfigurationWrapper TenantConfigurationWrapper=> LazyServiceProvider.LazyGetRequiredService<TenantConfigurationWrapper>();
private ICurrentTenant CurrentTenant => LazyServiceProvider.LazyGetRequiredService<ICurrentTenant>(); 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>(); private ISerializeService SerializeService => LazyServiceProvider.LazyGetRequiredService<ISerializeService>();
@@ -36,19 +38,13 @@ namespace Yi.Framework.SqlSugarCore
{ {
LazyServiceProvider = lazyServiceProvider; LazyServiceProvider = lazyServiceProvider;
var connectionString = GetCurrentConnectionString(); var tenantConfiguration= AsyncHelper.RunSync(async () =>await TenantConfigurationWrapper.GetAsync());
var connectionConfig =BuildConnectionConfig(action: options => var connectionConfig =BuildConnectionConfig(action: options =>
{ {
options.ConnectionString = connectionString; options.ConnectionString =tenantConfiguration.GetCurrentConnectionString();
options.DbType = GetCurrentDbType(); options.DbType = GetCurrentDbType(tenantConfiguration.GetCurrentConnectionName());
}); });
// var connectionConfig = ConnectionConfigCache.GetOrAdd(connectionString, (_) =>
// BuildConnectionConfig(action: options =>
// {
// options.ConnectionString = connectionString;
// options.DbType = GetCurrentDbType();
// }));
SqlSugarClient = new SqlSugarClient(connectionConfig); SqlSugarClient = new SqlSugarClient(connectionConfig);
//生命周期以下都可以直接使用sqlsugardb了 //生命周期以下都可以直接使用sqlsugardb了
@@ -188,41 +184,18 @@ namespace Yi.Framework.SqlSugarCore
return connectionConfig; return connectionConfig;
} }
/// <summary> protected virtual DbType GetCurrentDbType(string tenantName)
/// db切换多库支持
/// </summary>
/// <returns></returns>
protected virtual string GetCurrentConnectionString()
{ {
var connectionStringResolver = LazyServiceProvider.LazyGetRequiredService<IConnectionStringResolver>(); if (tenantName == ConnectionStrings.DefaultConnectionStringName)
var connectionString =
AsyncHelper.RunSync(() => connectionStringResolver.ResolveAsync());
if (string.IsNullOrWhiteSpace(connectionString))
{ {
Check.NotNull(Options.Url, "dbUrl未配置"); return Options.DbType!.Value;
} }
var dbTypeFromTenantName = GetDbTypeFromTenantName(tenantName);
return connectionString!; return dbTypeFromTenantName!.Value;
} }
protected virtual DbType GetCurrentDbType() //根据租户name进行匹配db类型: Test@Sqlite[form:AI]
{
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]
private DbType? GetDbTypeFromTenantName(string name) private DbType? GetDbTypeFromTenantName(string name)
{ {
if (string.IsNullOrWhiteSpace(name)) if (string.IsNullOrWhiteSpace(name))
@@ -230,25 +203,26 @@ namespace Yi.Framework.SqlSugarCore
return null; 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; return null;
} }
// 提取 枚举 部分 // 提取 枚举 部分
string enumString = name.Substring(underscoreIndex + 1); string enumString = name.Substring(atIndex + 1);
// 尝试将 尾缀 转换为枚举 // 尝试将 尾缀 转换为枚举
if (Enum.TryParse<DbType>(enumString, out DbType result)) if (Enum.TryParse<DbType>(enumString, out DbType result))
{ {
return result; return result;
} }
else
// 条件不满足时返回 null {
return null; throw new ArgumentException($"数据库{name}db类型错误或不支持无法匹配{enumString}数据库类型");
}
} }
public virtual void BackupDataBase() public virtual void BackupDataBase()

View File

@@ -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;
}
}

View File

@@ -15,8 +15,8 @@ namespace Yi.Framework.SqlSugarCore.Uow
{ {
public ILogger<UnitOfWorkSqlsugarDbContextProvider<TDbContext>> Logger { get; set; } public ILogger<UnitOfWorkSqlsugarDbContextProvider<TDbContext>> Logger { get; set; }
public IServiceProvider ServiceProvider { get; set; } public IServiceProvider ServiceProvider { get; set; }
private static AsyncLocalDbContextAccessor ContextInstance => AsyncLocalDbContextAccessor.Instance; private static AsyncLocalDbContextAccessor ContextInstance => AsyncLocalDbContextAccessor.Instance;
protected readonly TenantConfigurationWrapper _tenantConfigurationWrapper;
protected readonly IUnitOfWorkManager UnitOfWorkManager; protected readonly IUnitOfWorkManager UnitOfWorkManager;
protected readonly IConnectionStringResolver ConnectionStringResolver; protected readonly IConnectionStringResolver ConnectionStringResolver;
protected readonly ICancellationTokenProvider CancellationTokenProvider; protected readonly ICancellationTokenProvider CancellationTokenProvider;
@@ -26,26 +26,25 @@ namespace Yi.Framework.SqlSugarCore.Uow
IUnitOfWorkManager unitOfWorkManager, IUnitOfWorkManager unitOfWorkManager,
IConnectionStringResolver connectionStringResolver, IConnectionStringResolver connectionStringResolver,
ICancellationTokenProvider cancellationTokenProvider, ICancellationTokenProvider cancellationTokenProvider,
ICurrentTenant currentTenant ICurrentTenant currentTenant, TenantConfigurationWrapper tenantConfigurationWrapper)
)
{ {
UnitOfWorkManager = unitOfWorkManager; UnitOfWorkManager = unitOfWorkManager;
ConnectionStringResolver = connectionStringResolver; ConnectionStringResolver = connectionStringResolver;
CancellationTokenProvider = cancellationTokenProvider; CancellationTokenProvider = cancellationTokenProvider;
CurrentTenant = currentTenant; CurrentTenant = currentTenant;
_tenantConfigurationWrapper = tenantConfigurationWrapper;
Logger = NullLogger<UnitOfWorkSqlsugarDbContextProvider<TDbContext>>.Instance; Logger = NullLogger<UnitOfWorkSqlsugarDbContextProvider<TDbContext>>.Instance;
} }
public virtual async Task<TDbContext> GetDbContextAsync() 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 dbContextKey = $"{this.GetType().Name}_{connectionString}";
var unitOfWork = UnitOfWorkManager.Current; var unitOfWork = UnitOfWorkManager.Current;
if (unitOfWork == null ) if (unitOfWork == null )
{ {
@@ -67,8 +66,6 @@ namespace Yi.Framework.SqlSugarCore.Uow
databaseApi = new SqlSugarDatabaseApi( databaseApi = new SqlSugarDatabaseApi(
await CreateDbContextAsync(unitOfWork, connectionStringName, connectionString) await CreateDbContextAsync(unitOfWork, connectionStringName, connectionString)
); );
//await Console.Out.WriteLineAsync(">>>----------------实例化了db"+ ((SqlSugarDatabaseApi)databaseApi).DbContext.SqlSugarClient.ContextID.ToString());
//创建的db加入到当前工作单元中 //创建的db加入到当前工作单元中
unitOfWork.AddDatabaseApi(dbContextKey, databaseApi); unitOfWork.AddDatabaseApi(dbContextKey, databaseApi);
@@ -98,7 +95,7 @@ namespace Yi.Framework.SqlSugarCore.Uow
protected virtual async Task<TDbContext> CreateDbContextWithTransactionAsync(IUnitOfWork unitOfWork) protected virtual async Task<TDbContext> CreateDbContextWithTransactionAsync(IUnitOfWork unitOfWork)
{ {
//事务key //事务key
var transactionApiKey = $"SqlsugarCore_{SqlSugarDbContextCreationContext.Current.ConnectionString}"; var transactionApiKey = $"SqlSugarCore_{SqlSugarDbContextCreationContext.Current.ConnectionString}";
//尝试查找事务 //尝试查找事务
var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as SqlSugarTransactionApi; 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);
}
} }
} }

View File

@@ -10,6 +10,8 @@ using Volo.Abp.Data;
using Volo.Abp.Domain; using Volo.Abp.Domain;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
using Volo.Abp.Guids; using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.MultiTenancy.ConfigurationStore;
using Yi.Framework.SqlSugarCore.Abstractions; using Yi.Framework.SqlSugarCore.Abstractions;
using Yi.Framework.SqlSugarCore.Repositories; using Yi.Framework.SqlSugarCore.Repositories;
using Yi.Framework.SqlSugarCore.Uow; using Yi.Framework.SqlSugarCore.Uow;
@@ -51,7 +53,7 @@ namespace Yi.Framework.SqlSugarCore
options.DefaultSequentialGuidType = guidType; options.DefaultSequentialGuidType = guidType;
}); });
service.TryAddScoped<ISqlSugarDbContext, SqlSugarDbContextFactory>(); service.TryAddTransient<ISqlSugarDbContext, SqlSugarDbContextFactory>();
//不开放sqlsugarClient //不开放sqlsugarClient
//service.AddTransient<ISqlSugarClient>(x => x.GetRequiredService<ISqlsugarDbContext>().SqlSugarClient); //service.AddTransient<ISqlSugarClient>(x => x.GetRequiredService<ISqlsugarDbContext>().SqlSugarClient);
@@ -69,7 +71,25 @@ namespace Yi.Framework.SqlSugarCore
var dbConfig = section.Get<DbConnOptions>(); var dbConfig = section.Get<DbConnOptions>();
//将默认db传递给abp连接字符串模块 //将默认db传递给abp连接字符串模块
Configure<AbpDbConnectionOptions>(x => { x.ConnectionStrings.Default = dbConfig.Url; }); 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>(); context.Services.AddYiDbContext<DefaultSqlSugarDbContext>();
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -23,8 +23,8 @@ public class AccessLogCacheJob : HangfireBackgroundWorkerBase
{ {
_localEventBus = localEventBus; _localEventBus = localEventBus;
RecurringJobId = "访问日志写入缓存"; RecurringJobId = "访问日志写入缓存";
//每10秒执行一次将本地缓存转入redis防止丢数据 //每分钟执行一次将本地缓存转入redis防止丢数据
CronExpression = "*/10 * * * * *"; CronExpression = "0 * * * * ?";
// //
// JobDetail = JobBuilder.Create<AccessLogCacheJob>().WithIdentity(nameof(AccessLogCacheJob)) // JobDetail = JobBuilder.Create<AccessLogCacheJob>().WithIdentity(nameof(AccessLogCacheJob))
// .Build(); // .Build();

View File

@@ -46,8 +46,8 @@ public class AccessLogStoreJob : HangfireBackgroundWorkerBase
RecurringJobId = "访问日志写入数据库"; RecurringJobId = "访问日志写入数据库";
//每分钟执行一次 //每小时执行一次
CronExpression = "0 * * * * ?"; CronExpression = "0 0 * * * ?";
// JobDetail = JobBuilder.Create<AccessLogStoreJob>().WithIdentity(nameof(AccessLogStoreJob)) // JobDetail = JobBuilder.Create<AccessLogStoreJob>().WithIdentity(nameof(AccessLogStoreJob))
// .Build(); // .Build();
// //每分钟执行一次 // //每分钟执行一次

View File

@@ -136,8 +136,6 @@ namespace Yi.Framework.Bbs.Application.Services.Forum
{ DiscussId = output.Id, OldSeeNum = output.SeeNum }); { DiscussId = output.Id, OldSeeNum = output.SeeNum });
return output; return output;
} }
/// <summary> /// <summary>
/// 查询 /// 查询
/// </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!) .WhereIF(input.UserName is not null, (discuss, user) => user.UserName == input.UserName!)
.LeftJoin<BbsUserExtraInfoEntity>((discuss, user, info) => user.Id == info.UserId) .LeftJoin<BbsUserExtraInfoEntity>((discuss, user, info) => user.Id == info.UserId)
.OrderByDescending(discuss => discuss.OrderNum) .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.Host, discuss => discuss.SeeNum, OrderByType.Desc)
.OrderByIF(input.Type == QueryDiscussTypeEnum.Suggest, discuss => discuss.AgreeNum, OrderByType.Desc) .OrderByIF(input.Type == QueryDiscussTypeEnum.Suggest, discuss => discuss.AgreeNum, OrderByType.Desc)
.Select((discuss, user, info) => new DiscussGetListOutputDto .Select((discuss, user, info) => new DiscussGetListOutputDto

View File

@@ -24,6 +24,15 @@ namespace Yi.Framework.Bbs.Domain.Entities.Forum
PlateId = plateId; PlateId = plateId;
} }
public void AddSeeNumber()
{
this.SeeNum += 1;
//设置最小值,不更新
this.LastModificationTime = DateTime.MinValue;
//设置空值,不更新
this.LastModifierId = Guid.Empty;
}
[SugarColumn(ColumnName = "Id", IsPrimaryKey = true)] [SugarColumn(ColumnName = "Id", IsPrimaryKey = true)]
public override Guid Id { get; protected set; } public override Guid Id { get; protected set; }
public string? Title { get; set; } public string? Title { get; set; }

View File

@@ -8,28 +8,24 @@ using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus; using Volo.Abp.EventBus;
using Yi.Framework.Bbs.Domain.Entities.Forum; using Yi.Framework.Bbs.Domain.Entities.Forum;
using Yi.Framework.Bbs.Domain.Shared.Etos; using Yi.Framework.Bbs.Domain.Shared.Etos;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.Bbs.Domain.EventHandlers namespace Yi.Framework.Bbs.Domain.EventHandlers
{ {
public class SeeDiscussEventHandler : ILocalEventHandler<SeeDiscussEventArgs>, ITransientDependency public class SeeDiscussEventHandler : ILocalEventHandler<SeeDiscussEventArgs>, ITransientDependency
{ {
private IRepository<DiscussAggregateRoot, Guid> _repository; private ISqlSugarRepository<DiscussAggregateRoot, Guid> _repository;
public SeeDiscussEventHandler(IRepository<DiscussAggregateRoot, Guid> repository)
public SeeDiscussEventHandler(ISqlSugarRepository<DiscussAggregateRoot, Guid> repository)
{ {
_repository = repository; _repository = repository;
} }
public async Task HandleEventAsync(SeeDiscussEventArgs eventData) public async Task HandleEventAsync(SeeDiscussEventArgs eventData)
{ {
var entity = await _repository.GetAsync(eventData.DiscussId); await _repository._Db.Updateable<DiscussAggregateRoot>()
if (entity is not null) .SetColumns(x => new DiscussAggregateRoot { SeeNum = x.SeeNum + 1 })
{ .Where(x => x.Id == eventData.DiscussId).ExecuteCommandAsync();
entity.SeeNum += 1;
await _repository.UpdateAsync(entity);
}
} }
} }
} }

View File

@@ -1,4 +1,5 @@
using Mapster; using Mapster;
using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.Caching; using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
using Yi.Framework.Bbs.Domain.Entities.Forum; using Yi.Framework.Bbs.Domain.Entities.Forum;
@@ -28,10 +29,13 @@ public class DiscussLableRepository : SqlSugarRepository<DiscussLableAggregateRo
public async Task<Dictionary<Guid, DiscussLableCacheItem>> GetDiscussLableCacheMapAsync() public async Task<Dictionary<Guid, DiscussLableCacheItem>> GetDiscussLableCacheMapAsync()
{ {
var cahce = await _lableCache.GetOrAddAsync(DiscussLableConst.DiscussLableCacheKey, async () => var cahce = await _lableCache.GetOrAddAsync(DiscussLableConst.DiscussLableCacheKey, async () =>
{ {
var entities = await _DbQueryable.ToListAsync(); var entities = await _DbQueryable.ToListAsync();
return entities.Adapt<List<DiscussLableCacheItem>>(); return entities.Adapt<List<DiscussLableCacheItem>>();
}); }, () =>
new DistributedCacheEntryOptions()
{ AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(2) }
);
return cahce.ToDictionary(x => x.Id); return cahce.ToDictionary(x => x.Id);
} }
} }

View File

@@ -53,7 +53,7 @@ namespace Yi.Framework.ChatHub.Domain.Managers
{ {
if (result.Successful) if (result.Successful)
{ {
yield return result.Choices.FirstOrDefault()?.Message.Content ?? string.Empty; yield return result.Choices.FirstOrDefault()?.Message.Content ?? null;
} }
else else
{ {

View File

@@ -99,10 +99,14 @@ public class FileManager : DomainService, IFileManager
this.LoggerFactory.CreateLogger<FileManager>().LogInformation(exception, exception.Message); this.LoggerFactory.CreateLogger<FileManager>().LogInformation(exception, exception.Message);
} }
catch (Exception exception) catch (Exception exception)
{
this.LoggerFactory.CreateLogger<FileManager>().LogError(exception, exception.Message);
}
finally
{ {
//如果失败了,直接复制一份到缩略图上即可 //如果失败了,直接复制一份到缩略图上即可
compressImageStream = fileStream; compressImageStream = fileStream;
this.LoggerFactory.CreateLogger<FileManager>().LogError(exception, exception.Message);
} }

View File

@@ -53,10 +53,8 @@ public class YiMultiTenantConnectionStringResolver : DefaultConnectionStringReso
: Options.ConnectionStrings.Default!; : Options.ConnectionStrings.Default!;
} }
//Requesting specific connection string... //Requesting specific connection string...
var connString = tenant.ConnectionStrings?.FirstOrDefault().Value; var connString = tenant.ConnectionStrings?.GetOrDefault(connectionStringName);
if (!connString.IsNullOrWhiteSpace()) if (!connString.IsNullOrWhiteSpace())
{ {
//Found for the tenant //Found for the tenant

View File

@@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.DistributedLocking; using Volo.Abp.DistributedLocking;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings; using Volo.Abp.Settings;
using Volo.Abp.Uow; using Volo.Abp.Uow;
using Yi.Framework.Bbs.Application.Contracts.Dtos.Banner; using Yi.Framework.Bbs.Application.Contracts.Dtos.Banner;
@@ -215,5 +217,53 @@ namespace Yi.Abp.Application.Services
}); });
return $"加锁结果:{number},不加锁结果:{number2}"; 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实例连接涉及多库事务统一到最后提交";
}
}
} }
} }

View File

@@ -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"]

View File

@@ -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
```

View File

@@ -188,10 +188,10 @@ namespace Yi.Abp.Web
//配置Hangfire定时任务存储开启redis后优先使用redis //配置Hangfire定时任务存储开启redis后优先使用redis
var redisConfiguration = configuration["Redis:Configuration"]; var redisConfiguration = configuration["Redis:Configuration"];
var redisEnabled = configuration["Redis:IsEnabled"];
context.Services.AddHangfire(config=> context.Services.AddHangfire(config=>
{ {
if (redisEnabled.IsNullOrEmpty() || bool.Parse(redisEnabled)) bool.TryParse( configuration["Redis:IsEnabled"], out var redisEnabled);
if (redisEnabled)
{ {
config.UseRedisStorage( config.UseRedisStorage(
ConnectionMultiplexer.Connect(redisConfiguration), ConnectionMultiplexer.Connect(redisConfiguration),

View File

@@ -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": { "Logging": {
"LogLevel": { "LogLevel": {
//"Default": "Information", //"Default": "Information",

View File

@@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<AbpVersion>8.2.0</AbpVersion> <AbpVersion>8.3.4</AbpVersion>
<SqlSugarVersion>5.1.4.166</SqlSugarVersion> <SqlSugarVersion>5.1.4.176-preview16</SqlSugarVersion>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

33
Yi.Bbs.Vue3/.dockerignore Normal file
View 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
View 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;"]

View 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;"]

View File

@@ -22,6 +22,7 @@
"i": "^0.3.7", "i": "^0.3.7",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"marked": "^4.2.12", "marked": "^4.2.12",
"marked-katex-extension": "^5.1.4",
"mavon-editor": "^3.0.0", "mavon-editor": "^3.0.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"path-to-regexp": "^6.2.1", "path-to-regexp": "^6.2.1",

View File

@@ -17,7 +17,7 @@
</template> </template>
<script setup> <script setup>
import { ref, watch, defineProps } from "vue"; import { ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { compile } from "path-to-regexp"; import { compile } from "path-to-regexp";

View File

@@ -131,7 +131,7 @@
/> />
</template> </template>
<script setup> <script setup>
import { onMounted, reactive, ref, defineProps } from "vue"; import { onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { getListByDiscussId, add, del } from "@/apis/commentApi.js"; import { getListByDiscussId, add, del } from "@/apis/commentApi.js";
import AvatarInfo from "./AvatarInfo.vue"; import AvatarInfo from "./AvatarInfo.vue";

View File

@@ -11,7 +11,7 @@ const onClickText=()=>{
</script> </script>
<template> <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> <template #header>
<div class="card-header"> <div class="card-header">
<span>{{ props.header }}</span> <span>{{ props.header }}</span>
@@ -37,7 +37,8 @@ const onClickText=()=>{
.el-divider { .el-divider {
margin: 0.2rem 0; margin: 0.2rem 0;
} }
.VisitsLineChart /deep/ .el-card__body{
::v-deep(.VisitsLineChart .el-card__body){
padding: 0 20px; padding: 0 20px;
} }
.box-card-info { .box-card-info {

View File

@@ -5,7 +5,7 @@
</template> </template>
<script setup> <script setup>
import { ref, defineProps } from "vue"; import { ref } from "vue";
const props = defineProps({ const props = defineProps({
isBorder: { isBorder: {

View File

@@ -1,9 +1,10 @@
<template> <template>
<!-- @node-click="handleNodeClick"-->
<el-tree <el-tree
empty-text="无子文章" empty-text="无子文章"
:data="props.data == '' ? [] : props.data" :data="props.data == '' ? [] : props.data"
:props="defaultProps" :props="defaultProps"
@node-click="handleNodeClick"
:expand-on-click-node="false" :expand-on-click-node="false"
node-key="id" node-key="id"
:default-expand-all="true" :default-expand-all="true"
@@ -18,7 +19,7 @@
:content="data.name" :content="data.name"
placement="right" placement="right"
> >
<span class="title-name">{{ data.name }}</span> <span @click="handleNodeClick(data)" class="title-name">{{ data.name }}</span>
</el-tooltip> </el-tooltip>
<span> <span>
@@ -44,6 +45,7 @@
删除 删除
</a> </a>
</span> </span>
</span> </span>
</template> </template>
</el-tree> </el-tree>
@@ -79,7 +81,7 @@ const { isHasPermission: isRemoveArticle } = getPermission("bbs:article:del");
</script> </script>
<style scoped> <style scoped>
.custom-tree-node { .custom-tree-node {
width: 100%;
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -63,7 +63,7 @@
</template> </template>
<script setup name="UserInfoCard"> <script setup name="UserInfoCard">
import { computed, defineProps } from "vue"; import { computed } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import UserLimitTag from "../UserLimitTag.vue"; import UserLimitTag from "../UserLimitTag.vue";
const props = defineProps({ const props = defineProps({

View File

@@ -13,8 +13,8 @@
<el-menu-item index="2" @click="enterStart" <el-menu-item index="2" @click="enterStart"
>开始</el-menu-item> >开始</el-menu-item>
<el-menu-item index="3" @click="enterWatermelon" style="color: red;font-weight: bolder;font-size: large;" <el-menu-item index="3" @click="enterBook" style="color: red;font-weight: bolder;font-size: large;"
>知识宝典</el-menu-item> >面试宝典</el-menu-item>
<el-menu-item index="4" @click="enterShop" <el-menu-item index="4" @click="enterShop"
>商城</el-menu-item> >商城</el-menu-item>
<!-- <el-sub-menu index="4">--> <!-- <el-sub-menu index="4">-->
@@ -233,9 +233,8 @@ const enterStart = () => {
router.push("/start"); router.push("/start");
} }
const enterWatermelon=()=>{ const enterBook=()=>{
// router.push("/dc"); router.push("/book");
alert("即将上线,敬请期待!")
} }
const enterShop=()=>{ const enterShop=()=>{
router.push("/shop"); router.push("/shop");

View File

@@ -45,6 +45,10 @@
<el-icon><Trophy /></el-icon> <el-icon><Trophy /></el-icon>
<span>数字藏品</span> <span>数字藏品</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="9" :route="{ path: '/book' }">
<el-icon><Memo /></el-icon>
<span>面试宝典</span>
</el-menu-item>
</el-menu> </el-menu>
</template> </template>

View File

@@ -136,6 +136,14 @@ const router = createRouter({
title: "数字藏品", title: "数字藏品",
}, },
}, },
{
name: "book",
path: "/book",
component: () => import("../views/book/Index.vue"),
meta: {
title: "面试宝典",
},
},
], ],
}, },
{ {

View File

@@ -4,82 +4,84 @@
<el-col :span="5"> <el-col :span="5">
<el-row class="art-info-left"> <el-row class="art-info-left">
<el-col :span="24"> <el-col :span="24">
<InfoCard header="文章信息" text="展开" hideDivider="true"> <InfoCard header="文章信息" text="展开" hideDivider="true" :isPadding="false" style="padding: 10px">
<template #content> <template #content>
<el-button <el-button
style="width: 100%; margin-bottom: 0.8rem" style="width: 100%; margin-bottom: 0.8rem"
@click="loadDiscuss(true)" @click="loadDiscuss(true)"
>主题首页</el-button >主题首页
</el-button
> >
<el-button <el-button
v-if="isAddArticle && isArticleUser" v-if="isAddArticle && isArticleUser"
@click="addArticle('00000000-0000-0000-0000-000000000000')" @click="addArticle('00000000-0000-0000-0000-000000000000')"
type="primary" type="primary"
style="width: 100%; margin-bottom: 0.8rem; margin-left: 0" style="width: 100%; margin-bottom: 0.8rem; margin-left: 0"
>添加子文章</el-button >添加子文章
</el-button
> >
<!--目录在这里 --> <!--目录在这里 -->
<el-scrollbar style="min-height: 410px"> <el-scrollbar style="height:600px;overflow-y: auto;">
<TreeArticleInfo <TreeArticleInfo
:data="articleData" :data="articleData"
@remove="delArticle" @remove="delArticle"
@update="updateArticle" @update="updateArticle"
@create="addNextArticle" @create="addNextArticle"
@handleNodeClick="handleNodeClick" @handleNodeClick="handleNodeClick"
:currentNodeKey="currentNodeKey" :currentNodeKey="currentNodeKey"
:isArticleUser="isArticleUser" :isArticleUser="isArticleUser"
/> />
</el-scrollbar> </el-scrollbar>
</template> </template>
</InfoCard> </InfoCard>
</el-col> </el-col>
<el-col :span="24"> <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"> <template #item="temp">
<ThemeData :themeData="temp"/> <ThemeData :themeData="temp"/>
</template> </template>
</InfoCard> </InfoCard>
</el-col> </el-col>
<!-- <el-col :span="24">--> <!-- <el-col :span="24">-->
<!-- <InfoCard :items="items" header="内容推荐" text="更多">--> <!-- <InfoCard :items="items" header="内容推荐" text="更多">-->
<!-- <template #item="temp">--> <!-- <template #item="temp">-->
<!-- <AvatarInfo />--> <!-- <AvatarInfo />-->
<!-- </template>--> <!-- </template>-->
<!-- </InfoCard>--> <!-- </InfoCard>-->
<!-- </el-col>--> <!-- </el-col>-->
</el-row> </el-row>
</el-col> </el-col>
<el-col :span="14"> <el-col :span="14">
<el-row class="left-div"> <el-row class="left-div">
<el-col :span="24"> <el-col :span="24">
<Breadcrumb :breadcrumbsList="breadcrumbsList" class="breadcrumb" /> <Breadcrumb :breadcrumbsList="breadcrumbsList" class="breadcrumb"/>
<!-- {{ discuss.user }} --> <!-- {{ discuss.user }} -->
<AvatarInfo <AvatarInfo
:size="50" :size="50"
:showWatching="true" :showWatching="true"
:time="discuss.creationTime" :time="discuss.creationTime"
:userInfo="discuss.user" :userInfo="discuss.user"
></AvatarInfo> ></AvatarInfo>
<!-- :userInfo="{nick:'qwe'} --> <!-- :userInfo="{nick:'qwe'} -->
<h2>{{ discuss.title }}</h2> <h2>{{ discuss.title }}</h2>
<h5 class="subtitle">{{discuss.introduction }}</h5> <h5 class="subtitle">{{ discuss.introduction }}</h5>
<el-image <el-image
:preview-src-list="[getUrl(discuss.cover)]" :preview-src-list="[getUrl(discuss.cover)]"
v-if="discuss.cover" v-if="discuss.cover"
:src="getUrl(discuss.cover)" :src="getUrl(discuss.cover)"
style="width: 150px; height: 150px" style="width: 150px; height: 150px"
/> />
<el-divider /> <el-divider/>
<ArticleContentInfo <ArticleContentInfo
:code="discuss.content ?? ''" :code="discuss.content ?? ''"
></ArticleContentInfo> ></ArticleContentInfo>
<el-divider class="tab-divider" /> <el-divider class="tab-divider"/>
<el-space :size="10" :spacer="spacer"> <el-space :size="10" :spacer="spacer">
<AgreeInfo :data="discuss" /> <AgreeInfo :data="discuss"/>
<el-button icon="Star" text> 0</el-button> <el-button icon="Star" text> 0</el-button>
<el-button icon="Share" text> 分享</el-button> <el-button icon="Share" text> 分享</el-button>
<el-button icon="Operation" text> 操作</el-button> <el-button icon="Operation" text> 操作</el-button>
@@ -87,38 +89,40 @@
</el-col> </el-col>
<el-col :span="24" class="comment"> <el-col :span="24" class="comment">
<CommentInfo :isComment="isDisabledCreateComment" /> <CommentInfo :isComment="isDisabledCreateComment"/>
</el-col> </el-col>
</el-row> </el-row>
<BottomInfo /> <BottomInfo/>
</el-col> </el-col>
<el-col :span="5"> <el-col :span="5">
<el-row class="right-div"> <el-row class="right-div">
<el-col :span="24"> <el-col :span="24">
<InfoCard <InfoCard
class="art-info-right" class="art-info-right"
header="主题信息" header="主题信息"
text="更多" text="更多"
hideDivider="true" hideDivider="true"
> >
<template #content> <template #content>
<div> <div>
<ul class="art-info-ul"> <ul class="art-info-ul">
<li> <li>
<el-button <el-button
type="primary" type="primary"
size="default" size="default"
v-if="isEditTheme && isArticleUser" v-if="isEditTheme && isArticleUser"
@click="updateHander(route.params.discussId)" @click="updateHander(route.params.discussId)"
>编辑</el-button >编辑
</el-button
> >
<el-button <el-button
style="margin-left: 1rem" style="margin-left: 1rem"
type="danger" type="danger"
v-if="isRemoveTheme && isArticleUser" v-if="isRemoveTheme && isArticleUser"
@click="delHander(route.params.discussId)" @click="delHander(route.params.discussId)"
>删除</el-button >删除
</el-button
> >
</li> </li>
<li>分类: <span>主题</span></li> <li>分类: <span>主题</span></li>
@@ -135,18 +139,19 @@
<template #content> <template #content>
<div> <div>
<el-empty <el-empty
:image-size="100" :image-size="100"
style="padding: 20px 0" style="padding: 20px 0"
v-if="catalogueData.length == 0" v-if="catalogueData.length == 0"
description="无目录" description="无目录"
/> />
<ul v-else class="art-info-ul"> <ul v-else class="art-info-ul">
<li v-for="(item, i) in catalogueData" :key="i"> <li v-for="(item, i) in catalogueData" :key="i">
<el-button <el-button
style="width: 100%; justify-content: left" style="width: 100%; justify-content: left"
type="primary" type="primary"
text text
>{{ `${i + 1} ${item}` }}</el-button >{{ `${i + 1} ${item}` }}
</el-button
> >
</li> </li>
</ul> </ul>
@@ -155,62 +160,62 @@
</InfoCard> </InfoCard>
</el-col> </el-col>
<el-col :span="24"> <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"> <template #item="temp">
<ThemeData :themeData="temp"/> <ThemeData :themeData="temp"/>
</template> </template>
</InfoCard> </InfoCard>
<!-- <InfoCard :items="items" header="其他" text="更多">--> <!-- <InfoCard :items="items" header="其他" text="更多">-->
<!-- <template #item="temp">--> <!-- <template #item="temp">-->
<!-- <AvatarInfo />--> <!-- <AvatarInfo />-->
<!-- </template>--> <!-- </template>-->
<!-- </InfoCard>--> <!-- </InfoCard>-->
</el-col> </el-col>
<!-- <el-col :span="24">--> <!-- <el-col :span="24">-->
<!-- <InfoCard :items="items" header="其他" text="更多">--> <!-- <InfoCard :items="items" header="其他" text="更多">-->
<!-- <template #item="temp">--> <!-- <template #item="temp">-->
<!-- <AvatarInfo />--> <!-- <AvatarInfo />-->
<!-- </template>--> <!-- </template>-->
<!-- </InfoCard>--> <!-- </InfoCard>-->
<!-- </el-col>--> <!-- </el-col>-->
</el-row> </el-row>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
</template> </template>
<script setup> <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 AvatarInfo from "@/components/AvatarInfo.vue";
import InfoCard from "@/components/InfoCard.vue"; import InfoCard from "@/components/InfoCard.vue";
import ArticleContentInfo from "@/components/ArticleContentInfo.vue"; import ArticleContentInfo from "@/components/ArticleContentInfo.vue";
import CommentInfo from "@/components/CommentInfo.vue"; import CommentInfo from "@/components/CommentInfo.vue";
import BottomInfo from "@/components/BottomInfo.vue"; import BottomInfo from "@/components/BottomInfo.vue";
import TreeArticleInfo from "@/components/TreeArticleInfo.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 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 { import {
all as articleall, all as articleall,
del as articleDel, del as articleDel,
get as articleGet, get as articleGet,
} from "@/apis/articleApi.js"; } from "@/apis/articleApi.js";
import Breadcrumb from "@/components/Breadcrumb/index.vue"; import Breadcrumb from "@/components/Breadcrumb/index.vue";
import { getPermission } from "@/utils/auth"; import {getPermission} from "@/utils/auth";
import useUserStore from "@/stores/user.js"; import useUserStore from "@/stores/user.js";
import ThemeData from "@/views/home/components/RecommendTheme/index.vue"; 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 route = useRoute();
const router = useRouter(); const router = useRouter();
const spacer = h(ElDivider, { direction: "vertical" }); const spacer = h(ElDivider, {direction: "vertical"});
//子文章数据 //子文章数据
const articleData = ref([]); const articleData = ref([]);
//主题内容 //主题内容
const discuss = ref({}); const discuss = ref({});
//推荐主题 //推荐主题
const themeList=ref([]); const themeList = ref([]);
//作者主题 //作者主题
const authorList=ref([]); const authorList = ref([]);
//封面url //封面url
const getUrl = (str) => { const getUrl = (str) => {
return `${import.meta.env.VITE_APP_BASEAPI}/file/${str}`; return `${import.meta.env.VITE_APP_BASEAPI}/file/${str}`;
@@ -225,10 +230,10 @@ const catalogueData = ref([]);
const breadcrumbsList = ref([]); const breadcrumbsList = ref([]);
const resultRouters = ["index", "discuss", "themeCover"]; const resultRouters = ["index", "discuss", "themeCover"];
breadcrumbsList.value = route.matched[0].children breadcrumbsList.value = route.matched[0].children
.filter((item) => resultRouters.includes(item.name)) .filter((item) => resultRouters.includes(item.name))
.sort((a, b) => { .sort((a, b) => {
return resultRouters.indexOf(a.name) - resultRouters.indexOf(b.name); return resultRouters.indexOf(a.name) - resultRouters.indexOf(b.name);
}); });
// 当前文章名称 // 当前文章名称
const currentArticle = ref(""); const currentArticle = ref("");
@@ -242,9 +247,9 @@ const loadArticleData = async () => {
//主题初始化 //主题初始化
const isDisabledCreateComment = ref(false); const isDisabledCreateComment = ref(false);
const isArticleUser = ref(false); const isArticleUser = ref(false);
const { isHasPermission: isAddArticle } = getPermission("bbs:article:add"); const {isHasPermission: isAddArticle} = getPermission("bbs:article:add");
const { isHasPermission: isEditTheme } = getPermission("bbs:discuss:update"); const {isHasPermission: isEditTheme} = getPermission("bbs:discuss:update");
const { isHasPermission: isRemoveTheme } = getPermission("bbs:discuss:del"); const {isHasPermission: isRemoveTheme} = getPermission("bbs:discuss:del");
const loadDiscuss = async (isRewrite) => { const loadDiscuss = async (isRewrite) => {
if (isRewrite) { if (isRewrite) {
//跳转路由 //跳转路由
@@ -357,9 +362,7 @@ const handleNodeClick = async (data) => {
router.push(`/article/${route.params.discussId}/${data.id}`); 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) => { const delArticle = (node, data) => {
@@ -377,11 +380,11 @@ const delArticle = (node, data) => {
}); });
}); });
}; };
const loadThemeData =async () => { const loadThemeData = async () => {
const {data: themeData} = await getRecommendedTopic(); const {data: themeData} = await getRecommendedTopic();
themeList.value = themeData; themeList.value = themeData;
} }
const loadAuthorData=async () => { const loadAuthorData = async () => {
const {data: authorData} = await getAuthorTopic(discuss.value.user.id); const {data: authorData} = await getAuthorTopic(discuss.value.user.id);
authorList.value = authorData; authorList.value = authorData;
} }
@@ -394,39 +397,53 @@ onMounted(async () => {
watch( watch(
() => currentArticle.value, () => currentArticle.value,
(val) => { (val) => {
if (val !== "") { if (val !== "") {
breadcrumbsList.value[3] = { breadcrumbsList.value[3] = {
name: "article", name: "article",
path: "/article/:discussId", path: "/article/:discussId",
meta: { meta: {
title: currentArticle.value, title: currentArticle.value,
}, },
}; };
}
} }
}
); );
watch( watch(
() => route.params.articleId, () => route.params,
async (val) => { async (val) => {
if (val === "") { if (val.articleId !=="")
discuss.value = (await discussGet(route.params.discussId)).data; {
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> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.subtitle .subtitle {
{ color: #999AAA;
color:#999AAA;
margin: 0; margin: 0;
} }
.article-box { .article-box {
width: 1500px; width: 1500px;
height: 100%; height: 100%;
.comment { .comment {
min-height: 40rem; min-height: 40rem;
} }

View 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>

View 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>

View File

@@ -18,6 +18,7 @@ import {getUrl} from '@/utils/icon'
//markdown ai显示 //markdown ai显示
import {marked} from 'marked'; import {marked} from 'marked';
import markedKatex from "marked-katex-extension";
import '@/assets/atom-one-dark.css'; import '@/assets/atom-one-dark.css';
import '@/assets/github-markdown.css'; import '@/assets/github-markdown.css';
import hljs from "highlight.js"; import hljs from "highlight.js";
@@ -39,11 +40,11 @@ const currentInputValue = ref("");
//临时存储的输入框根据用户id及组name、all组为keydata为value //临时存储的输入框根据用户id及组name、all组为keydata为value
const inputListDataStore = ref([ const inputListDataStore = ref([
{key: "all", name: "官方学习交流群", titleName: "官方学习交流群", logo: "yilogo.png", value: ""}, {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思索", name: "DeepSeek思索",
titleName: "DeepSeek-思索模式", titleName: "满血!DeepSeek-思索模式",
logo: "deepSeekAi.png", logo: "deepSeekAi.png",
value: "" value: ""
}, },
@@ -66,6 +67,25 @@ onMounted(async () => {
onclickClose(); onclickClose();
}, 3000); }, 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的聊天消息 //all的聊天消息
chatStore.setMsgList((await getChatAccountMessageList()).data); chatStore.setMsgList((await getChatAccountMessageList()).data);
//在线用户列表 //在线用户列表
@@ -117,7 +137,7 @@ const currentMsgContext = computed(() => {
}); });
//获取聊天内容的头像 //获取聊天内容的头像
const getChatUrl = (url, position) => { 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 imageSrc(inputListDataStore.value.find(x=>x.key===currentSelectUser.value).logo)
} }
return getUrl(url); return getUrl(url);
@@ -125,7 +145,7 @@ const getChatUrl = (url, position) => {
//当前聊天框显示的名称 //当前聊天框显示的名称
const currentHeaderName = computed(() => { const currentHeaderName = computed(() => {
if (selectIsAll()||selectIsAi()) { if (selectIsAll()||selectIsAi()) {
return inputListDataStore.value.find(x=>x.key===currentSelectUser.value).name; return inputListDataStore.value.find(x=>x.key===currentSelectUser.value).titleName;
} else { } else {
return currentSelectUser.value.userName; return currentSelectUser.value.userName;
} }
@@ -333,27 +353,14 @@ const clearAiMsg = () => {
//转换markdown //转换markdown
const toMarkDownHtml = (text) => { const toMarkDownHtml = (text) => {
marked.setOptions({ //处理数学公式
renderer: new marked.Renderer(), let soureMd=text.replace(/\\\[/g, '$').replace(/\\\]/g, '$');
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,
}
);
//需要注意代码块样式 //需要注意代码块样式
const soureHtml = marked(text); let soureHtml = marked(soureMd);
nextTick(() => { nextTick(() => {
addCopyEvent(); addCopyEvent();
}) })
return soureHtml; return soureHtml;
} }
//code部分处理、高亮 //code部分处理、高亮
const codeHandler = (code, language) => { const codeHandler = (code, language) => {
@@ -421,7 +428,7 @@ const clickCopyEvent = async function (event) {
const spanId = event.target.id; const spanId = event.target.id;
console.log(codeCopyDic, "codeCopyDic") console.log(codeCopyDic, "codeCopyDic")
console.log(spanId, "spanId") 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({ ElMessage({
message: "代码块复制成功", message: "代码块复制成功",
type: "success", type: "success",
@@ -433,7 +440,7 @@ const clickCopyEvent = async function (event) {
<template> <template>
<div style="position: absolute; top: 0;left: 0;" v-show="isShowTipNumber>0"> <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:官方学习交流群每次发送消息消耗 1 钱钱</p>
<p>tip:点击聊天窗口右上角X可退出</p> <p>tip:点击聊天窗口右上角X可退出</p>
<p>tip:多人同时在聊天室时左侧可显示其他成员</p> <p>tip:多人同时在聊天室时左侧可显示其他成员</p>
@@ -1122,17 +1129,39 @@ ul {
color: red; color: red;
cursor: pointer; /* 设置鼠标悬浮为手型 */ cursor: pointer; /* 设置鼠标悬浮为手型 */
} }
::v-deep(.katex-html)
{
color: #7B7C7C;
font-size: smaller;
}
::v-deep(.nav-ul) { ::v-deep(.nav-ul) {
border-right: 1px solid #FFFFFF; border-right: 1px solid #FFFFFF;
margin-top: 12px; margin-top: 12px;
margin-left: 0 !important;
padding-left: 10px; padding-left: 10px;
padding-right: 2px; padding-right: 2px;
.nav-li { .nav-li {
margin: 1.0px 0; margin: 1.0px 0;
text-align: right; text-align: right;
margin-right: 3px; 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> </style>

View File

@@ -27,7 +27,7 @@
</template> </template>
<script setup> <script setup>
import { computed, defineProps, defineEmits } from "vue"; import { computed } from "vue";
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {

View File

@@ -162,6 +162,7 @@ margin: 10px auto;">
<el-col v-if="!isIcp" :span="24"> <el-col v-if="!isIcp" :span="24">
<template v-if="isPointFinished"> <template v-if="isPointFinished">
<InfoCard :isPadding="false" :items="pointList" header="财富排行榜" text="查看我的位置" height="410" <InfoCard :isPadding="false" :items="pointList" header="财富排行榜" text="查看我的位置" height="410"
style="padding:0 20px"
@onClickText="onClickMoneyTop"> @onClickText="onClickMoneyTop">
<template #item="temp"> <template #item="temp">
<PointsRanking :pointsData="temp"/> <PointsRanking :pointsData="temp"/>
@@ -179,7 +180,8 @@ margin: 10px auto;">
<el-col v-if="!isIcp" :span="24"> <el-col v-if="!isIcp" :span="24">
<template v-if="isFriendFinished"> <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"> <template #item="temp">
<RecommendFriend :friendData="temp"/> <RecommendFriend :friendData="temp"/>
</template> </template>
@@ -195,7 +197,9 @@ margin: 10px auto;">
</el-col> </el-col>
<el-col v-if="!isIcp" :span="24"> <el-col v-if="!isIcp" :span="24">
<template v-if="isThemeFinished"> <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"> <template #item="temp">
<ThemeData :themeData="temp"/> <ThemeData :themeData="temp"/>
</template> </template>
@@ -307,10 +311,10 @@ const activeList = [
{name: "排行榜", path: "/money", icon: "Money"}, {name: "排行榜", path: "/money", icon: "Money"},
{name: "开始", path: "/start", icon: "Position"}, {name: "开始", path: "/start", icon: "Position"},
{name: "聊天室", path: "/chat", icon: "ChatRound"}, {name: "聊天室", path: "/chat", icon: "ChatRound"},
{name: "商城", path: "/shop", icon: "ShoppingCart"}, {name: "商城", path: "/shop", icon: "ShoppingCart"},
{name: "数字藏品", path: "/dc", icon: "Trophy"}, {name: "数字藏品", path: "/dc", icon: "Trophy"},
{name: "面试宝典", path: "/book", icon: "Memo"},
// {name: "小程序", path: "/", icon: "Position"}, // {name: "小程序", path: "/", icon: "Position"},
// {name: "公众号", path: "/", icon: "ChatRound"}, // {name: "公众号", path: "/", icon: "ChatRound"},
]; ];

View File

@@ -3,7 +3,7 @@
</template> </template>
<script setup name="AccessLogChart"> <script setup name="AccessLogChart">
import { ref, defineEmits, defineProps, defineExpose } from "vue"; import { ref } from "vue";
import useEcharts from "@/hooks/useEcharts"; import useEcharts from "@/hooks/useEcharts";
import { accessLogEchartsConfig } from "../../hooks/accessLogEchartsConfig"; import { accessLogEchartsConfig } from "../../hooks/accessLogEchartsConfig";
const props = defineProps({ const props = defineProps({

View File

@@ -36,7 +36,7 @@
</template> </template>
<script setup name="PointsRanking"> <script setup name="PointsRanking">
import { defineProps, computed } from "vue"; import { computed } from "vue";
import UserInfoCard from "@/components/UserInfoCard/index.vue"; import UserInfoCard from "@/components/UserInfoCard/index.vue";
import UserLimitTag from "@/components/UserLimitTag.vue"; import UserLimitTag from "@/components/UserLimitTag.vue";
const props = defineProps({ const props = defineProps({

View File

@@ -36,7 +36,7 @@
</template> </template>
<script setup name="RecommendFriend"> <script setup name="RecommendFriend">
import { defineProps, computed } from "vue"; import { computed } from "vue";
import UserInfoCard from "@/components/UserInfoCard/index.vue"; import UserInfoCard from "@/components/UserInfoCard/index.vue";
import UserLimitTag from "@/components/UserLimitTag.vue"; import UserLimitTag from "@/components/UserLimitTag.vue";
const props = defineProps({ const props = defineProps({

View File

@@ -42,7 +42,7 @@
</template> </template>
<script setup name="RecommendFriend"> <script setup name="RecommendFriend">
import { defineProps, ref } from "vue"; import { ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const props = defineProps({ const props = defineProps({
@@ -57,7 +57,6 @@ const seeNumLength = ref(props.themeData.seeNum.toString().length);
const handleClickTheme = (id) => { const handleClickTheme = (id) => {
router.push(`/article/${id}`); router.push(`/article/${id}`);
router.go(0);
}; };
</script> </script>

View File

@@ -3,7 +3,7 @@
</template> </template>
<script setup name="VisitsLineChart"> <script setup name="VisitsLineChart">
import { ref, defineEmits, defineProps, defineExpose } from "vue"; import { ref } from "vue";
import useEcharts from "@/hooks/useEcharts"; import useEcharts from "@/hooks/useEcharts";
import { statisticsEcharts } from "../../hooks/echartsConfig"; import { statisticsEcharts } from "../../hooks/echartsConfig";
const props = defineProps({ const props = defineProps({

28
Yi.Bbs.Vue3/yi-bbs.conf Normal file
View 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/;
}
}