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.
**/wwwroot/libs/*
public
dist
.vscode
/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
****
## 🍉 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

View File

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

View File

@@ -28,3 +28,6 @@ README.md
!.git/config
!.git/packed-refs
!.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
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}"

View File

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

View File

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

View File

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

View File

@@ -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了
@@ -189,40 +185,17 @@ 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()

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

View File

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

View File

@@ -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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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实例连接涉及多库事务统一到最后提交";
}
}
}
}

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
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),

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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({

View File

@@ -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");

View File

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

View File

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

View File

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

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显示
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组为keydata为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>

View File

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

View File

@@ -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>
@@ -308,9 +312,9 @@ const activeList = [
{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"},
];

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

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

View File

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