mirror of
https://gitee.com/ccnetcore/Yi
synced 2026-03-03 08:10:51 +08:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb894e14a4 | ||
|
|
b492d82442 | ||
|
|
5eaffe2ec2 | ||
|
|
d7bcad9da7 | ||
|
|
04e11d15e2 | ||
|
|
97e3dc5eed | ||
|
|
695bd56a27 | ||
|
|
7919383be3 | ||
|
|
d6cc3c5d96 | ||
|
|
19f0d05a69 | ||
|
|
3c5e575e9b | ||
|
|
f875617de1 | ||
|
|
9976e8a6e2 | ||
|
|
38e8cbc5ca | ||
|
|
d95c14c903 | ||
|
|
ffb2f2fb4c | ||
|
|
4bd2fc357d | ||
|
|
da23d17af8 | ||
|
|
c1a6046107 | ||
|
|
2ec7b5f4fd | ||
|
|
d21f61646a | ||
|
|
eecdf442fb | ||
|
|
1b4c3cbb8d | ||
|
|
b7756e2112 | ||
|
|
cb49059e84 | ||
|
|
690cabfd96 | ||
|
|
4521212a90 | ||
|
|
771ecd9d81 | ||
|
|
94834f45c3 | ||
|
|
a9b2979a21 | ||
|
|
22ac150acd | ||
|
|
17337b8d78 | ||
|
|
09fb43ee14 | ||
|
|
477c0e3f2c | ||
|
|
2e4f520dac | ||
|
|
067b25b9af | ||
|
|
36370c215d | ||
|
|
e24731acfe | ||
|
|
927e9df7de | ||
|
|
114b41144e | ||
|
|
5019a36138 | ||
|
|
e15eb6149b | ||
|
|
9d401a9c93 | ||
|
|
eacf86e118 | ||
|
|
c4b631c815 | ||
|
|
fb25e75a3a | ||
|
|
e9099bbe04 | ||
|
|
f02fb91175 | ||
|
|
5beef22269 | ||
|
|
933cbb91d8 | ||
|
|
efd917d184 | ||
|
|
e906208f4a | ||
|
|
cf137f6307 | ||
|
|
e6b991fe86 | ||
|
|
3e75792e43 | ||
|
|
dd3f6325bb | ||
|
|
108ba348f6 | ||
|
|
bcdcec40e0 | ||
|
|
2ce8baea42 | ||
|
|
c6425ca206 | ||
|
|
acb359ec33 | ||
|
|
a1395d9a33 | ||
|
|
609de29e71 | ||
|
|
2efed4f4a5 | ||
|
|
aec90ec9d6 | ||
|
|
1aaff2942d | ||
|
|
cdbfc5383d | ||
|
|
f302555e0c | ||
|
|
86c5890476 | ||
|
|
a13ee395c7 | ||
|
|
9abcd72aca | ||
|
|
4ddea6d468 | ||
|
|
867a2dc861 | ||
|
|
4a72e3fa0d | ||
|
|
8b4371aabb | ||
|
|
799dd08ec0 | ||
|
|
c5c22224cf | ||
|
|
2dae47e85c | ||
|
|
375dd4f797 | ||
|
|
acb2db8397 | ||
|
|
b7a3e76d0b | ||
|
|
48150b712a | ||
|
|
6db9dfc308 | ||
|
|
2d6c1f3c46 | ||
|
|
161e10d2d1 | ||
|
|
a9a2a91183 | ||
|
|
1c9a6f108e | ||
|
|
d6adf9b736 | ||
|
|
959eb3f782 | ||
|
|
7a53e0c90c | ||
|
|
533b87fc5b | ||
|
|
15713cf7fe | ||
|
|
31dc756868 | ||
|
|
52f6b6130f | ||
|
|
16945b3d5b | ||
|
|
bdc664fc44 | ||
|
|
9555ef10e0 | ||
|
|
49e6cb26fc | ||
|
|
3ace29e692 | ||
|
|
aa9dd0129b | ||
|
|
1464271fbd | ||
|
|
754f145559 | ||
|
|
6afd0cb955 | ||
|
|
d32906702a | ||
|
|
9bcdaf6bd8 | ||
|
|
db82a8cf08 | ||
|
|
85bd1ce8d6 | ||
|
|
1cc5f2a14f | ||
|
|
d9997eeb28 | ||
|
|
06e77aa8fd | ||
|
|
e9e2228f6e | ||
|
|
d516a381d0 | ||
|
|
4e792ba976 | ||
|
|
f90d3871fa | ||
|
|
6005b9329d | ||
|
|
9d4b3e7d0c | ||
|
|
72795382a1 | ||
|
|
35cdff2afa | ||
|
|
dcf547f513 | ||
|
|
8660d45f36 | ||
|
|
63dd55e7a4 | ||
|
|
40cd89f90c |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -265,6 +265,7 @@ src/Acme.BookStore.Blazor.Server.Tiered/Logs/*
|
||||
**/wwwroot/libs/*
|
||||
public
|
||||
dist
|
||||
dist - 副本
|
||||
.vscode
|
||||
/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.Development.json
|
||||
/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.Production.json
|
||||
|
||||
10
README.md
10
README.md
@@ -7,6 +7,12 @@
|
||||
[](https://gitee.com/ccnetcore/Yi)
|
||||
[](https://gitee.com/ccnetcore/Yi)
|
||||
|
||||
本项目 CDN 加速及安全防护由 Tencent EdgeOne 赞助
|
||||
|
||||
[亚洲最佳CDN、边缘和安全解决方案 - Tencent EdgeOne](https://edgeone.ai/zh?from=github)
|
||||
|
||||
<img src="readme/edgeone.png"/>
|
||||
|
||||
[English](README-en.md) | 简体中文
|
||||
****
|
||||
## 🍍 简介:
|
||||
@@ -60,9 +66,9 @@ bbs前端:`docker run -d --name yi.bbs -p 18001:18001 -v /home/Yi/Yi.Bbs.Vue3/
|
||||
|
||||
Yi社区官网网址(Bbs社区正式):[ccnetcore.com](https://ccnetcore.com) (已上线,欢迎加入)
|
||||
|
||||
Rbac后台演示地址:https://ccnetcore.com:1000 (用户cc、密码123456)
|
||||
Rbac后台演示地址:https://data.ccnetcore.com:1000 (用户cc、密码123456)
|
||||
|
||||
Pure后台演示地址:https://ccnetcore.com:1001 (用户cc、密码123456)
|
||||
Pure后台演示地址:https://data.ccnetcore.com:1001 (用户cc、密码123456)
|
||||
|
||||
## 🍏 支持:
|
||||
|
||||
|
||||
10
Yi.Abp.Net8/.claude/settings.local.json
Normal file
10
Yi.Abp.Net8/.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dotnet build \"E:\\code\\github\\Yi\\Yi.Abp.Net8\\module\\ai-hub\\Yi.Framework.AiHub.Application\\Yi.Framework.AiHub.Application.csproj\" --no-restore)",
|
||||
"Read(//e/code/github/Yi/Yi.Ai.Vue3/**)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// MIT 许可证
|
||||
// MIT 许可证
|
||||
//
|
||||
// 版权 © 2020-present 百小僧, 百签科技(广东)有限公司 和所有贡献者
|
||||
//
|
||||
@@ -17,25 +17,25 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Volo.Abp.AspNetCore.Mvc;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Volo.Abp.Validation;
|
||||
using Yi.Framework.Core.Extensions;
|
||||
|
||||
namespace Yi.Framework.AspNetCore.UnifyResult.Fiters;
|
||||
|
||||
/// <summary>
|
||||
/// 友好异常拦截器
|
||||
/// 友好异常拦截器
|
||||
/// </summary>
|
||||
public sealed class FriendlyExceptionFilter : IAsyncExceptionFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 异常拦截
|
||||
/// 异常拦截
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
public async Task OnExceptionAsync(ExceptionContext context)
|
||||
{
|
||||
|
||||
// 排除 WebSocket 请求处理
|
||||
if (context.HttpContext.IsWebSocketRequest()) return;
|
||||
|
||||
@@ -44,20 +44,23 @@ public sealed class FriendlyExceptionFilter : IAsyncExceptionFilter
|
||||
|
||||
// 解析异常信息
|
||||
var exceptionMetadata = GetExceptionMetadata(context);
|
||||
|
||||
IUnifyResultProvider unifyResult = context.GetRequiredService<IUnifyResultProvider>();
|
||||
var unifyResult = context.GetRequiredService<IUnifyResultProvider>();
|
||||
// 执行规范化异常处理
|
||||
context.Result = unifyResult.OnException(context, exceptionMetadata);
|
||||
|
||||
|
||||
// 创建日志记录器
|
||||
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<FriendlyExceptionFilter>>();
|
||||
|
||||
var errorMsg = "";
|
||||
if (exceptionMetadata.Errors != null) errorMsg = "\n" + JsonConvert.SerializeObject(exceptionMetadata.Errors);
|
||||
|
||||
|
||||
// 记录拦截日常
|
||||
logger.LogError(context.Exception, context.Exception.Message);
|
||||
logger.LogError(context.Exception, context.Exception.Message + errorMsg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取异常元数据
|
||||
/// 获取异常元数据
|
||||
/// </summary>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
@@ -74,22 +77,25 @@ public sealed class FriendlyExceptionFilter : IAsyncExceptionFilter
|
||||
// 判断是否是 ExceptionContext 或者 ActionExecutedContext
|
||||
var exception = context is ExceptionContext exContext
|
||||
? exContext.Exception
|
||||
: (
|
||||
context is ActionExecutedContext edContext
|
||||
? edContext.Exception
|
||||
: default
|
||||
);
|
||||
: context is ActionExecutedContext edContext
|
||||
? edContext.Exception
|
||||
: default;
|
||||
|
||||
if (exception is AbpValidationException validationException)
|
||||
{
|
||||
errors = validationException.ValidationErrors;
|
||||
isValidationException = true;
|
||||
}
|
||||
|
||||
// 判断是否是友好异常
|
||||
if (exception is UserFriendlyException friendlyException)
|
||||
{
|
||||
int statusCode2 = 500;
|
||||
var statusCode2 = 500;
|
||||
int.TryParse(friendlyException.Code, out statusCode2);
|
||||
isFriendlyException = true;
|
||||
errorCode = friendlyException.Code;
|
||||
originErrorCode = friendlyException.Code;
|
||||
statusCode = statusCode2==0?403:statusCode2;
|
||||
isValidationException = false;
|
||||
statusCode = statusCode2 == 0 ? 403 : statusCode2;
|
||||
errors = friendlyException.Message;
|
||||
data = friendlyException.Data;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Volo.Abp.AspNetCore.Mvc.ExceptionHandling;
|
||||
using Volo.Abp.AspNetCore.Mvc.Response;
|
||||
using Yi.Framework.AspNetCore.UnifyResult.Fiters;
|
||||
|
||||
namespace Yi.Framework.AspNetCore.UnifyResult;
|
||||
@@ -20,9 +21,10 @@ public static class UnifyResultExtensions
|
||||
services.AddTransient<FriendlyExceptionFilter>();
|
||||
services.AddMvc(options =>
|
||||
{
|
||||
options.Filters.RemoveAll(x => (x as ServiceFilterAttribute)?.ServiceType == typeof(AbpExceptionFilter));
|
||||
options.Filters.RemoveAll(x => (x as ServiceFilterAttribute)?.ServiceType == typeof(AbpNoContentActionFilter));
|
||||
options.Filters.AddService<SucceededUnifyResultFilter>(99);
|
||||
options.Filters.AddService<FriendlyExceptionFilter>(100);
|
||||
options.Filters.RemoveAll(x => (x as ServiceFilterAttribute)?.ServiceType == typeof(AbpExceptionFilter));
|
||||
});
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -199,11 +199,11 @@ namespace Yi.Framework.Ddd.Application
|
||||
/// <summary>
|
||||
/// 批量删除实体
|
||||
/// </summary>
|
||||
/// <param name="ids">实体ID集合</param>
|
||||
/// <param name="id">实体ID集合</param>
|
||||
[RemoteService(isEnabled: true)]
|
||||
public virtual async Task DeleteAsync(IEnumerable<TKey> ids)
|
||||
public virtual async Task DeleteAsync(IEnumerable<TKey> id)
|
||||
{
|
||||
await Repository.DeleteManyAsync(ids);
|
||||
await Repository.DeleteManyAsync(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Volo.Abp.Modularity;
|
||||
using Volo.Abp.ObjectMapping;
|
||||
using Yi.Framework.Core;
|
||||
using Mapster;
|
||||
|
||||
namespace Yi.Framework.Mapster
|
||||
{
|
||||
@@ -22,7 +23,8 @@ namespace Yi.Framework.Mapster
|
||||
public override void ConfigureServices(ServiceConfigurationContext context)
|
||||
{
|
||||
var services = context.Services;
|
||||
|
||||
// 扫描并注册所有映射配置
|
||||
TypeAdapterConfig.GlobalSettings.Scan(AppDomain.CurrentDomain.GetAssemblies());
|
||||
// 注册Mapster相关服务
|
||||
services.AddTransient<IAutoObjectMappingProvider, MapsterAutoObjectMappingProvider>();
|
||||
services.AddTransient<IObjectMapper, MapsterObjectMapper>();
|
||||
|
||||
@@ -60,8 +60,8 @@ namespace Yi.Framework.SqlSugarCore.Abstractions
|
||||
public bool EnabledSaasMultiTenancy { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 并发乐观锁异常,否则不处理
|
||||
/// 是否开启更新并发乐观锁
|
||||
/// </summary>
|
||||
public bool EnabledConcurrencyException { get; set; } = true;
|
||||
public bool EnabledConcurrencyException { get;set; } = false;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Nito.AsyncEx;
|
||||
using SqlSugar;
|
||||
using Volo.Abp;
|
||||
using Volo.Abp.Auditing;
|
||||
using Volo.Abp.Data;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Volo.Abp.Domain.Entities;
|
||||
@@ -17,18 +12,17 @@ using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
{
|
||||
public class SqlSugarRepository<TEntity> : ISqlSugarRepository<TEntity>, IRepository<TEntity>
|
||||
where TEntity : class, IEntity, new()
|
||||
public class SqlSugarRepository<TEntity> : ISqlSugarRepository<TEntity>, IRepository<TEntity> where TEntity : class, IEntity, new()
|
||||
{
|
||||
public ISqlSugarClient _Db => AsyncContext.Run(async () => await GetDbContextAsync());
|
||||
|
||||
public ISugarQueryable<TEntity> _DbQueryable => _Db.Queryable<TEntity>();
|
||||
|
||||
private readonly ISugarDbContextProvider<ISqlSugarDbContext> _dbContextProvider;
|
||||
|
||||
|
||||
public IAbpLazyServiceProvider LazyServiceProvider { get; set; }
|
||||
|
||||
protected DbConnOptions? Options => LazyServiceProvider?.LazyGetService<IOptions<DbConnOptions>>().Value;
|
||||
|
||||
/// <summary>
|
||||
/// 异步查询执行器
|
||||
/// </summary>
|
||||
@@ -64,26 +58,22 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
|
||||
#region Abp模块
|
||||
|
||||
public virtual async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate,
|
||||
bool includeDetails = true, CancellationToken cancellationToken = default)
|
||||
public virtual async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate, bool includeDetails = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetFirstAsync(predicate);
|
||||
}
|
||||
|
||||
public virtual async Task<TEntity> GetAsync(Expression<Func<TEntity, bool>> predicate,
|
||||
bool includeDetails = true, CancellationToken cancellationToken = default)
|
||||
public virtual async Task<TEntity> GetAsync(Expression<Func<TEntity, bool>> predicate, bool includeDetails = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetFirstAsync(predicate);
|
||||
}
|
||||
|
||||
public virtual async Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this.DeleteAsync(predicate);
|
||||
}
|
||||
|
||||
public virtual async Task DeleteDirectAsync(Expression<Func<TEntity, bool>> predicate,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task DeleteDirectAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this.DeleteAsync(predicate);
|
||||
}
|
||||
@@ -113,71 +103,60 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public virtual async Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate,
|
||||
bool includeDetails = false, CancellationToken cancellationToken = default)
|
||||
public virtual async Task<List<TEntity>> GetListAsync(Expression<Func<TEntity, bool>> predicate, bool includeDetails = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetListAsync(predicate);
|
||||
}
|
||||
|
||||
public virtual async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await InsertReturnEntityAsync(entity);
|
||||
}
|
||||
|
||||
public virtual async Task InsertManyAsync(IEnumerable<TEntity> entities, bool autoSave = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task InsertManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await InsertRangeAsync(entities.ToList());
|
||||
}
|
||||
|
||||
public virtual async Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await UpdateAsync(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
public virtual async Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await UpdateRangeAsync(entities.ToList());
|
||||
}
|
||||
|
||||
public virtual async Task DeleteAsync(TEntity entity, bool autoSave = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await DeleteAsync(entity);
|
||||
}
|
||||
|
||||
public virtual async Task DeleteManyAsync(IEnumerable<TEntity> entities, bool autoSave = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task DeleteManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await DeleteAsync(entities.ToList());
|
||||
}
|
||||
|
||||
public virtual async Task<List<TEntity>> GetListAsync(bool includeDetails = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetListAsync();
|
||||
}
|
||||
|
||||
public virtual async Task<long> GetCountAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await this.CountAsync(_ => true);
|
||||
return await this.CountAsync(_=>true);
|
||||
}
|
||||
|
||||
public virtual async Task<List<TEntity>> GetPagedListAsync(int skipCount, int maxResultCount, string sorting,
|
||||
bool includeDetails = false, CancellationToken cancellationToken = default)
|
||||
public virtual async Task<List<TEntity>> GetPagedListAsync(int skipCount, int maxResultCount, string sorting, bool includeDetails = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetPageListAsync(_ => true, skipCount, maxResultCount);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
#region 内置DB快捷操作
|
||||
|
||||
public virtual async Task<IDeleteable<TEntity>> AsDeleteable()
|
||||
{
|
||||
return (await GetDbSimpleClientAsync()).AsDeleteable();
|
||||
@@ -192,7 +171,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
{
|
||||
return (await GetDbSimpleClientAsync()).AsInsertable(insertObj);
|
||||
}
|
||||
|
||||
|
||||
public virtual async Task<IInsertable<TEntity>> AsInsertable(TEntity[] insertObjs)
|
||||
{
|
||||
return (await GetDbSimpleClientAsync()).AsInsertable(insertObjs);
|
||||
@@ -232,11 +211,9 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
{
|
||||
return (await GetDbSimpleClientAsync()).AsUpdateable(updateObjs);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SimpleClient模块
|
||||
|
||||
public virtual async Task<int> CountAsync(Expression<Func<TEntity, bool>> whereExpression)
|
||||
{
|
||||
return await (await GetDbSimpleClientAsync()).CountAsync(whereExpression);
|
||||
@@ -253,6 +230,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
{
|
||||
return await (await GetDbSimpleClientAsync()).DeleteAsync(deleteObj);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public virtual async Task<bool> DeleteAsync(List<TEntity> deleteObjs)
|
||||
@@ -272,13 +250,13 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
{
|
||||
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
|
||||
{
|
||||
return await (await GetDbSimpleClientAsync()).AsUpdateable()
|
||||
.SetColumns(nameof(ISoftDelete.IsDeleted), true).Where(whereExpression).ExecuteCommandAsync() > 0;
|
||||
return await (await GetDbSimpleClientAsync()).AsUpdateable().SetColumns(nameof(ISoftDelete.IsDeleted), true).Where(whereExpression).ExecuteCommandAsync() > 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return await (await GetDbSimpleClientAsync()).DeleteAsync(whereExpression);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public virtual async Task<bool> DeleteByIdAsync(dynamic id)
|
||||
@@ -286,11 +264,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
|
||||
{
|
||||
var entity = await GetByIdAsync(id);
|
||||
if (entity is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (entity == null) return false;
|
||||
//反射赋值
|
||||
ReflexHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, entity);
|
||||
return await UpdateAsync(entity);
|
||||
@@ -311,7 +285,6 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//反射赋值
|
||||
entities.ForEach(e => ReflexHelper.SetModelValue(nameof(ISoftDelete.IsDeleted), true, e));
|
||||
return await UpdateRangeAsync(entities);
|
||||
@@ -320,6 +293,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
{
|
||||
return await (await GetDbSimpleClientAsync()).DeleteByIdAsync(ids);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public virtual async Task<TEntity> GetByIdAsync(dynamic id)
|
||||
@@ -328,6 +302,7 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
}
|
||||
|
||||
|
||||
|
||||
public virtual async Task<TEntity> GetFirstAsync(Expression<Func<TEntity, bool>> whereExpression)
|
||||
{
|
||||
return await (await GetDbSimpleClientAsync()).GetFirstAsync(whereExpression);
|
||||
@@ -343,19 +318,14 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
return await (await GetDbSimpleClientAsync()).GetListAsync(whereExpression);
|
||||
}
|
||||
|
||||
public virtual async Task<List<TEntity>> GetPageListAsync(Expression<Func<TEntity, bool>> whereExpression,
|
||||
int pageNum, int pageSize)
|
||||
public virtual async Task<List<TEntity>> GetPageListAsync(Expression<Func<TEntity, bool>> whereExpression, int pageNum, int pageSize)
|
||||
{
|
||||
return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression,
|
||||
new PageModel() { PageIndex = pageNum, PageSize = pageSize });
|
||||
return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression, new PageModel() { PageIndex = pageNum, PageSize = pageSize });
|
||||
}
|
||||
|
||||
public virtual async Task<List<TEntity>> GetPageListAsync(Expression<Func<TEntity, bool>> whereExpression,
|
||||
int pageNum, int pageSize, Expression<Func<TEntity, object>>? orderByExpression = null,
|
||||
OrderByType orderByType = OrderByType.Asc)
|
||||
public virtual async Task<List<TEntity>> GetPageListAsync(Expression<Func<TEntity, bool>> whereExpression, int pageNum, int pageSize, Expression<Func<TEntity, object>>? orderByExpression = null, OrderByType orderByType = OrderByType.Asc)
|
||||
{
|
||||
return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression,
|
||||
new PageModel { PageIndex = pageNum, PageSize = pageSize }, orderByExpression, orderByType);
|
||||
return await (await GetDbSimpleClientAsync()).GetPageListAsync(whereExpression, new PageModel { PageIndex = pageNum, PageSize = pageSize }, orderByExpression, orderByType);
|
||||
}
|
||||
|
||||
public virtual async Task<TEntity> GetSingleAsync(Expression<Func<TEntity, bool>> whereExpression)
|
||||
@@ -410,9 +380,9 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
|
||||
public virtual async Task<bool> UpdateAsync(TEntity updateObj)
|
||||
{
|
||||
if (typeof(TEntity).IsAssignableTo<IHasConcurrencyStamp>()) //带版本号乐观锁更新
|
||||
if (Options is not null && Options.EnabledConcurrencyException)
|
||||
{
|
||||
if (Options is not null && Options.EnabledConcurrencyException)
|
||||
if (typeof(TEntity).IsAssignableTo<IHasConcurrencyStamp>()) //带版本号乐观锁更新
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -426,24 +396,18 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
$"{ex.Message}[更新失败:ConcurrencyStamp不是最新版本],entityInfo:{updateObj}", ex);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int num = await (await GetDbSimpleClientAsync())
|
||||
.Context.Updateable(updateObj).ExecuteCommandAsync();
|
||||
return num > 0;
|
||||
}
|
||||
}
|
||||
|
||||
return await (await GetDbSimpleClientAsync()).UpdateAsync(updateObj);
|
||||
}
|
||||
|
||||
public virtual async Task<bool> UpdateAsync(Expression<Func<TEntity, TEntity>> columns,
|
||||
Expression<Func<TEntity, bool>> whereExpression)
|
||||
public virtual async Task<bool> UpdateAsync(Expression<Func<TEntity, TEntity>> columns, Expression<Func<TEntity, bool>> whereExpression)
|
||||
{
|
||||
return await (await GetDbSimpleClientAsync()).UpdateAsync(columns, whereExpression);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public virtual async Task<bool> UpdateRangeAsync(List<TEntity> updateObjs)
|
||||
{
|
||||
return await (await GetDbSimpleClientAsync()).UpdateRangeAsync(updateObjs);
|
||||
@@ -452,36 +416,30 @@ namespace Yi.Framework.SqlSugarCore.Repositories
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class SqlSugarRepository<TEntity, TKey> : SqlSugarRepository<TEntity>, ISqlSugarRepository<TEntity, TKey>,
|
||||
IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>, new()
|
||||
public class SqlSugarRepository<TEntity, TKey> : SqlSugarRepository<TEntity>, ISqlSugarRepository<TEntity, TKey>, IRepository<TEntity, TKey> where TEntity : class, IEntity<TKey>, new()
|
||||
{
|
||||
public SqlSugarRepository(ISugarDbContextProvider<ISqlSugarDbContext> dbContextProvider) : base(
|
||||
dbContextProvider)
|
||||
public SqlSugarRepository(ISugarDbContextProvider<ISqlSugarDbContext> sugarDbContextProvider) : base(sugarDbContextProvider)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual async Task DeleteAsync(TKey id, bool autoSave = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await DeleteByIdAsync(id);
|
||||
}
|
||||
|
||||
public virtual async Task DeleteManyAsync(IEnumerable<TKey> ids, bool autoSave = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task DeleteManyAsync(IEnumerable<TKey> ids, bool autoSave = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await DeleteByIdsAsync(ids.Select(x => (object)x).ToArray());
|
||||
}
|
||||
|
||||
public virtual async Task<TEntity?> FindAsync(TKey id, bool includeDetails = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task<TEntity?> FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetByIdAsync(id);
|
||||
}
|
||||
|
||||
public virtual async Task<TEntity> GetAsync(TKey id, bool includeDetails = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
public virtual async Task<TEntity> GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await GetByIdAsync(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,19 +18,4 @@ public class AiUserRoleMenuDto:UserRoleMenuDto
|
||||
/// VIP到期时间
|
||||
/// </summary>
|
||||
public DateTime? VipExpireTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包总Token数
|
||||
/// </summary>
|
||||
public long PremiumTotalTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包已使用Token数
|
||||
/// </summary>
|
||||
public long PremiumUsedTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包剩余Token数
|
||||
/// </summary>
|
||||
public long PremiumRemainingTokens { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
||||
|
||||
/// <summary>
|
||||
/// 公告缓存 DTO
|
||||
/// </summary>
|
||||
public class AnnouncementCacheDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 版本号
|
||||
/// </summary>
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 公告日志列表
|
||||
/// </summary>
|
||||
public List<AnnouncementLogDto> Logs { get; set; } = new List<AnnouncementLogDto>();
|
||||
|
||||
/// <summary>
|
||||
/// 跳转链接
|
||||
/// </summary>
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
||||
|
||||
/// <summary>
|
||||
/// 公告日志 DTO
|
||||
/// </summary>
|
||||
public class AnnouncementLogDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 内容列表
|
||||
/// </summary>
|
||||
public List<string> Content { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 图片url
|
||||
/// </summary>
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间(系统公告时间、活动开始时间)
|
||||
/// </summary>
|
||||
public DateTime StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 活动结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 公告类型(系统、活动)
|
||||
/// </summary>
|
||||
public AnnouncementTypeEnum Type{ get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跳转链接
|
||||
/// </summary>
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
||||
|
||||
/// <summary>
|
||||
/// 公告输出 DTO
|
||||
/// </summary>
|
||||
public class AnnouncementOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 版本号
|
||||
/// </summary>
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 公告日志列表
|
||||
/// </summary>
|
||||
public List<AnnouncementLogDto> Logs { get; set; } = new List<AnnouncementLogDto>();
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌任务状态输出
|
||||
/// </summary>
|
||||
public class CardFlipStatusOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 本周总翻牌次数
|
||||
/// </summary>
|
||||
public int TotalFlips { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 剩余免费次数
|
||||
/// </summary>
|
||||
public int RemainingFreeFlips { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 剩余赠送次数
|
||||
/// </summary>
|
||||
public int RemainingBonusFlips { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 剩余邀请解锁次数
|
||||
/// </summary>
|
||||
public int RemainingInviteFlips { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否可以翻牌
|
||||
/// </summary>
|
||||
public bool CanFlip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户的邀请码
|
||||
/// </summary>
|
||||
public string? MyInviteCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本周邀请人数
|
||||
/// </summary>
|
||||
public int InvitedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌记录
|
||||
/// </summary>
|
||||
public List<CardFlipRecord> FlipRecords { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 下次可翻牌提示
|
||||
/// </summary>
|
||||
public string? NextFlipTip { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前用户是否已经填写过邀请码
|
||||
/// </summary>
|
||||
public bool IsFilledInviteCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌记录
|
||||
/// </summary>
|
||||
public class CardFlipRecord
|
||||
{
|
||||
/// <summary>
|
||||
/// 翻牌序号(1-10)
|
||||
/// </summary>
|
||||
public int FlipNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已翻
|
||||
/// </summary>
|
||||
public bool IsFlipped { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否中奖
|
||||
/// </summary>
|
||||
public bool IsWin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 奖励金额(token数)
|
||||
/// </summary>
|
||||
public long? RewardAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌类型描述
|
||||
/// </summary>
|
||||
public string? FlipTypeDesc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 在翻牌顺序中的位置(1-10,表示第几个翻)
|
||||
/// </summary>
|
||||
public int FlipOrderIndex { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌输入
|
||||
/// </summary>
|
||||
public class FlipCardInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 翻牌序号(1-10)
|
||||
/// </summary>
|
||||
public int FlipNumber { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌输出
|
||||
/// </summary>
|
||||
public class FlipCardOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 翻牌序号(1-10)
|
||||
/// </summary>
|
||||
public int FlipNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否中奖
|
||||
/// </summary>
|
||||
public bool IsWin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 奖励金额(token数)
|
||||
/// </summary>
|
||||
public long? RewardAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 奖励描述
|
||||
/// </summary>
|
||||
public string? RewardDesc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 剩余可翻次数
|
||||
/// </summary>
|
||||
public int RemainingFlips { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
|
||||
|
||||
/// <summary>
|
||||
/// 邀请码信息输出
|
||||
/// </summary>
|
||||
public class InviteCodeOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 我的邀请码
|
||||
/// </summary>
|
||||
public string? MyInviteCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本周邀请人数
|
||||
/// </summary>
|
||||
public int InvitedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已被邀请
|
||||
/// </summary>
|
||||
public bool IsInvited { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 邀请历史记录
|
||||
/// </summary>
|
||||
public List<InvitationHistoryItem> InvitationHistory { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 邀请历史记录项
|
||||
/// </summary>
|
||||
public class InvitationHistoryItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 被邀请人昵称(脱敏)
|
||||
/// </summary>
|
||||
public string InvitedUserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 邀请时间
|
||||
/// </summary>
|
||||
public DateTime InvitationTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本周所在
|
||||
/// </summary>
|
||||
public string WeekDescription { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
|
||||
|
||||
/// <summary>
|
||||
/// 使用邀请码输入
|
||||
/// </summary>
|
||||
public class UseInviteCodeInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 邀请码
|
||||
/// </summary>
|
||||
public string InviteCode { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.DailyTask;
|
||||
|
||||
/// <summary>
|
||||
/// 领取任务奖励输入
|
||||
/// </summary>
|
||||
public class ClaimTaskRewardInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务等级(1=1000w任务,2=3000w任务)
|
||||
/// </summary>
|
||||
public int TaskLevel { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.DailyTask;
|
||||
|
||||
/// <summary>
|
||||
/// 每日任务状态输出
|
||||
/// </summary>
|
||||
public class DailyTaskStatusOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 今日消耗的尊享包Token数
|
||||
/// </summary>
|
||||
public long TodayConsumedTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务列表
|
||||
/// </summary>
|
||||
public List<DailyTaskItem> Tasks { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 每日任务项
|
||||
/// </summary>
|
||||
public class DailyTaskItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务等级(1=1000w任务,2=3000w任务)
|
||||
/// </summary>
|
||||
public int Level { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务名称
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 任务描述
|
||||
/// </summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 任务要求的Token消耗量
|
||||
/// </summary>
|
||||
public long RequiredTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 奖励的Token数量
|
||||
/// </summary>
|
||||
public long RewardTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务状态:0=未完成,1=可领取,2=已领取
|
||||
/// </summary>
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务进度百分比(0-100)
|
||||
/// </summary>
|
||||
public decimal Progress { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品列表输入DTO
|
||||
/// </summary>
|
||||
public class GetGoodsListInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 商品类别(可选)
|
||||
/// 如果不传,则返回所有商品
|
||||
/// 如果传了,则只返回指定类别的商品(VIP服务或尊享包)
|
||||
/// </summary>
|
||||
public GoodsCategoryType? GoodsCategoryType { get; set; }
|
||||
}
|
||||
@@ -17,28 +17,39 @@ public class GoodsListOutput
|
||||
/// </summary>
|
||||
public decimal OriginalPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品参考价格
|
||||
/// </summary>
|
||||
public decimal ReferencePrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品实际价格(折扣后的价格)
|
||||
/// </summary>
|
||||
public decimal GoodsPrice { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品类型
|
||||
/// </summary>
|
||||
public GoodsTypeEnum GoodsType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品备注
|
||||
/// </summary>
|
||||
public string Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 折扣金额(仅尊享包)
|
||||
/// </summary>
|
||||
public decimal? DiscountAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品类别
|
||||
/// </summary>
|
||||
public string GoodsCategory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 商品备注
|
||||
/// </summary>
|
||||
public string Remark { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 折扣说明(仅尊享包)
|
||||
/// </summary>
|
||||
public string? DiscountDescription { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 商品类型
|
||||
/// </summary>
|
||||
public GoodsTypeEnum GoodsType { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
|
||||
|
||||
/// <summary>
|
||||
/// 尊享服务Token用量统计DTO
|
||||
/// </summary>
|
||||
public class PremiumTokenUsageDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 总Token数
|
||||
/// </summary>
|
||||
public long PremiumTotalTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已使用Token数
|
||||
/// </summary>
|
||||
public long PremiumUsedTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 剩余Token数
|
||||
/// </summary>
|
||||
public long PremiumRemainingTokens { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Yi.Framework.Ddd.Application.Contracts;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
|
||||
|
||||
public class PremiumTokenUsageGetListInput : PagedAllResultRequestDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否免费
|
||||
/// </summary>
|
||||
public bool? IsFree { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Yi.Framework.Ddd.Application.Contracts;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
|
||||
|
||||
public class PremiumTokenUsageGetListOutput : CreationAuditedEntityDto
|
||||
{
|
||||
/// <summary>
|
||||
/// id
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户ID
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 包名称
|
||||
/// </summary>
|
||||
public string PackageName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总用量(总token数)
|
||||
/// </summary>
|
||||
public long TotalTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 剩余用量(剩余token数)
|
||||
/// </summary>
|
||||
public long RemainingTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已使用token数
|
||||
/// </summary>
|
||||
public long UsedTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 到期时间
|
||||
/// </summary>
|
||||
public DateTime? ExpireDateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否激活
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 购买金额
|
||||
/// </summary>
|
||||
public decimal PurchaseAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||
|
||||
/// <summary>
|
||||
/// 公告服务接口
|
||||
/// </summary>
|
||||
public interface IAnnouncementService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取公告信息
|
||||
/// </summary>
|
||||
/// <returns>公告信息</returns>
|
||||
Task<List<AnnouncementLogDto>> GetAsync();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌服务接口
|
||||
/// </summary>
|
||||
public interface ICardFlipService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取本周翻牌任务状态
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<CardFlipStatusOutput> GetWeeklyTaskStatusAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌
|
||||
/// </summary>
|
||||
/// <param name="input">翻牌输入</param>
|
||||
/// <returns></returns>
|
||||
Task<FlipCardOutput> FlipCardAsync(FlipCardInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 使用邀请码解锁翻牌次数
|
||||
/// </summary>
|
||||
/// <param name="input">邀请码输入</param>
|
||||
/// <returns></returns>
|
||||
Task UseInviteCodeAsync(UseInviteCodeInput input);
|
||||
|
||||
/// <summary>
|
||||
/// 获取我的邀请码信息
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<InviteCodeOutput> GetMyInviteCodeAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 生成我的邀请码(如果没有)
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<string> GenerateMyInviteCodeAsync();
|
||||
}
|
||||
@@ -34,6 +34,7 @@ public interface IPayService : IApplicationService
|
||||
/// <summary>
|
||||
/// 获取商品列表
|
||||
/// </summary>
|
||||
/// <param name="input">获取商品列表输入</param>
|
||||
/// <returns>商品列表</returns>
|
||||
Task<List<GoodsListOutput>> GetGoodsListAsync();
|
||||
Task<List<GoodsListOutput>> GetGoodsListAsync([FromQuery] GetGoodsListInput input);
|
||||
}
|
||||
@@ -18,4 +18,10 @@ public interface IUsageStatisticsService
|
||||
/// </summary>
|
||||
/// <returns>模型Token使用量列表</returns>
|
||||
Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户尊享服务Token用量统计
|
||||
/// </summary>
|
||||
/// <returns>尊享服务Token用量统计</returns>
|
||||
Task<PremiumTokenUsageDto> GetPremiumTokenUsageAsync();
|
||||
}
|
||||
@@ -9,7 +9,6 @@ using Yi.Framework.Rbac.Application.Contracts.IServices;
|
||||
using Yi.Framework.Rbac.Domain.Shared.Dtos;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Services;
|
||||
|
||||
@@ -74,23 +73,79 @@ public class AiAccountService : ApplicationService
|
||||
}
|
||||
}
|
||||
|
||||
// 获取尊享包Token信息
|
||||
var premiumPackages = await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && x.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
if (premiumPackages.Any())
|
||||
{
|
||||
// 过滤掉已过期的包
|
||||
var validPackages = premiumPackages
|
||||
.Where(p => p.IsAvailable())
|
||||
.ToList();
|
||||
|
||||
output.PremiumTotalTokens = validPackages.Sum(x => x.TotalTokens);
|
||||
output.PremiumUsedTokens = validPackages.Sum(x => x.UsedTokens);
|
||||
output.PremiumRemainingTokens = validPackages.Sum(x => x.RemainingTokens);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取利润统计数据
|
||||
/// </summary>
|
||||
/// <param name="currentCost">当前成本(RMB)</param>
|
||||
/// <returns></returns>
|
||||
[Authorize]
|
||||
[HttpGet("account/profit-statistics")]
|
||||
public async Task<string> GetProfitStatisticsAsync([FromQuery] decimal currentCost)
|
||||
{
|
||||
if (CurrentUser.UserName != "Guo" && CurrentUser.UserName != "cc")
|
||||
{
|
||||
throw new UserFriendlyException("您暂无权限访问");
|
||||
}
|
||||
|
||||
// 1. 获取尊享包总消耗和剩余库存
|
||||
var premiumPackages = await _premiumPackageRepository._DbQueryable.ToListAsync();
|
||||
long totalUsedTokens = premiumPackages.Sum(p => p.UsedTokens);
|
||||
long totalRemainingTokens = premiumPackages.Sum(p => p.RemainingTokens);
|
||||
|
||||
// 2. 计算1亿Token成本
|
||||
decimal costPerHundredMillion = totalUsedTokens > 0
|
||||
? currentCost / (totalUsedTokens / 100000000m)
|
||||
: 0;
|
||||
|
||||
// 3. 计算总成本(剩余+已使用的总成本)
|
||||
long totalTokens = totalUsedTokens + totalRemainingTokens;
|
||||
decimal totalCost = totalTokens > 0
|
||||
? (totalTokens / 100000000m) * costPerHundredMillion
|
||||
: 0;
|
||||
|
||||
// 4. 获取总收益(RechargeType=PremiumPackage的充值金额总和)
|
||||
decimal totalRevenue = await _rechargeRepository._DbQueryable
|
||||
.Where(x => x.RechargeType == Domain.Shared.Enums.RechargeTypeEnum.PremiumPackage)
|
||||
.SumAsync(x => x.RechargeAmount);
|
||||
|
||||
// 5. 计算利润率
|
||||
decimal profitRate = totalCost > 0
|
||||
? (totalRevenue / totalCost - 1) * 100
|
||||
: 0;
|
||||
|
||||
// 6. 按200售价计算成本
|
||||
decimal costAt200Price = totalRevenue > 0
|
||||
? (totalCost / totalRevenue) * 200
|
||||
: 0;
|
||||
|
||||
// 7. 格式化输出
|
||||
var today = DateTime.Now;
|
||||
string dayOfWeek = today.ToString("dddd", new System.Globalization.CultureInfo("zh-CN"));
|
||||
string weekDay = dayOfWeek switch
|
||||
{
|
||||
"星期一" => "周1",
|
||||
"星期二" => "周2",
|
||||
"星期三" => "周3",
|
||||
"星期四" => "周4",
|
||||
"星期五" => "周5",
|
||||
"星期六" => "周6",
|
||||
"星期日" => "周日",
|
||||
_ => dayOfWeek
|
||||
};
|
||||
|
||||
var result = $@"{today:M月d日} {weekDay}
|
||||
尊享包已消耗({totalUsedTokens / 100000000m:F2}亿){totalUsedTokens}
|
||||
尊享包剩余库存({totalRemainingTokens / 100000000m:F2}亿){totalRemainingTokens}
|
||||
当前成本:{currentCost:F2}RMB
|
||||
1亿Token成本:{costPerHundredMillion:F2} RMB=1亿 Token
|
||||
总成本:{totalCost:F2} RMB
|
||||
总收益:{totalRevenue:F2}RMB
|
||||
利润率: {profitRate:F1}%
|
||||
按200售价来算,成本在{costAt200Price:F2}";
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Mapster;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.Caching;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.Announcement;
|
||||
using Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 公告服务
|
||||
/// </summary>
|
||||
public class AnnouncementService : ApplicationService, IAnnouncementService
|
||||
{
|
||||
private readonly ISqlSugarRepository<AnnouncementAggregateRoot> _announcementRepository;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IDistributedCache<AnnouncementCacheDto> _announcementCache;
|
||||
private const string AnnouncementCacheKey = "AiHub:Announcement";
|
||||
|
||||
public AnnouncementService(
|
||||
ISqlSugarRepository<AnnouncementAggregateRoot> announcementRepository,
|
||||
IConfiguration configuration,
|
||||
IDistributedCache<AnnouncementCacheDto> announcementCache)
|
||||
{
|
||||
_announcementRepository = announcementRepository;
|
||||
_configuration = configuration;
|
||||
_announcementCache = announcementCache;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取公告信息
|
||||
/// </summary>
|
||||
public async Task<List<AnnouncementLogDto>> GetAsync()
|
||||
{
|
||||
// 使用 GetOrAddAsync 从缓存获取或添加数据,缓存1小时
|
||||
var cacheData = await _announcementCache.GetOrAddAsync(
|
||||
AnnouncementCacheKey,
|
||||
async () => await LoadAnnouncementDataAsync(),
|
||||
() => new DistributedCacheEntryOptions
|
||||
{
|
||||
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
|
||||
}
|
||||
);
|
||||
|
||||
return cacheData?.Logs ?? new List<AnnouncementLogDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从数据库加载公告数据
|
||||
/// </summary>
|
||||
private async Task<AnnouncementCacheDto> LoadAnnouncementDataAsync()
|
||||
{
|
||||
// 查询所有公告日志,按日期降序排列
|
||||
var logs = await _announcementRepository._DbQueryable
|
||||
.OrderByDescending(x => x.StartTime)
|
||||
.ToListAsync();
|
||||
|
||||
// 转换为 DTO
|
||||
var logDtos = logs.Adapt<List<AnnouncementLogDto>>();
|
||||
return new AnnouncementCacheDto
|
||||
{
|
||||
Logs = logDtos
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.Users;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.CardFlip;
|
||||
using Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.AiHub.Domain.Managers;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌服务 - 应用层组合服务
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class CardFlipService : ApplicationService, ICardFlipService
|
||||
{
|
||||
private readonly CardFlipManager _cardFlipManager;
|
||||
private readonly InviteCodeManager _inviteCodeManager;
|
||||
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
|
||||
private readonly ILogger<CardFlipService> _logger;
|
||||
|
||||
public CardFlipService(
|
||||
CardFlipManager cardFlipManager,
|
||||
InviteCodeManager inviteCodeManager,
|
||||
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
|
||||
ILogger<CardFlipService> logger)
|
||||
{
|
||||
_cardFlipManager = cardFlipManager;
|
||||
_inviteCodeManager = inviteCodeManager;
|
||||
_premiumPackageRepository = premiumPackageRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本周翻牌任务状态
|
||||
/// </summary>
|
||||
public async Task<CardFlipStatusOutput> GetWeeklyTaskStatusAsync()
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
|
||||
|
||||
// 获取本周任务
|
||||
var task = await _cardFlipManager.GetOrCreateWeeklyTaskAsync(userId, weekStart, createIfNotExists: false);
|
||||
|
||||
// 获取邀请码信息
|
||||
var inviteCode = await _inviteCodeManager.GetUserInviteCodeAsync(userId);
|
||||
|
||||
// 统计本周邀请人数
|
||||
var invitedCount = await _inviteCodeManager.GetWeeklyInvitationCountAsync(userId, weekStart);
|
||||
|
||||
//当前用户是否已填写过邀请码
|
||||
var isFilledInviteCode = await _inviteCodeManager.IsFilledInviteCodeAsync(userId);
|
||||
var output = new CardFlipStatusOutput
|
||||
{
|
||||
TotalFlips = task?.TotalFlips ?? 0,
|
||||
RemainingFreeFlips = CardFlipManager.MAX_FREE_FLIPS - (task?.FreeFlipsUsed ?? 0),
|
||||
RemainingBonusFlips = 0, // 已废弃
|
||||
RemainingInviteFlips = CardFlipManager.MAX_INVITE_FLIPS - (task?.InviteFlipsUsed ?? 0),
|
||||
CanFlip = _cardFlipManager.CanFlipCard(task),
|
||||
MyInviteCode = inviteCode?.Code,
|
||||
InvitedCount = invitedCount,
|
||||
FlipRecords = BuildFlipRecords(task),
|
||||
IsFilledInviteCode = isFilledInviteCode
|
||||
};
|
||||
|
||||
// 生成提示信息
|
||||
output.NextFlipTip = GenerateNextFlipTip(output);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌
|
||||
/// </summary>
|
||||
public async Task<FlipCardOutput> FlipCardAsync(FlipCardInput input)
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
|
||||
|
||||
// 执行翻牌逻辑(由Manager处理验证和翻牌)
|
||||
var result = await _cardFlipManager.ExecuteFlipAsync(userId, input.FlipNumber, weekStart);
|
||||
|
||||
// 如果中奖,发放奖励
|
||||
if (result.IsWin)
|
||||
{
|
||||
await GrantRewardAsync(userId, result.RewardAmount, $"翻牌活动-序号{input.FlipNumber}中奖");
|
||||
}
|
||||
|
||||
// 构建输出
|
||||
var output = new FlipCardOutput
|
||||
{
|
||||
FlipNumber = result.FlipNumber,
|
||||
IsWin = result.IsWin,
|
||||
RewardAmount = result.RewardAmount,
|
||||
RewardDesc = result.RewardDesc,
|
||||
RemainingFlips = CardFlipManager.TOTAL_MAX_FLIPS - input.FlipNumber
|
||||
};
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用邀请码解锁翻牌次数
|
||||
/// </summary>
|
||||
public async Task UseInviteCodeAsync(UseInviteCodeInput input)
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
|
||||
|
||||
// 获取本周任务
|
||||
var task = await _cardFlipManager.GetOrCreateWeeklyTaskAsync(userId, weekStart, createIfNotExists: true);
|
||||
|
||||
// 验证是否已经使用了所有邀请解锁次数
|
||||
if (task.InviteFlipsUsed >= CardFlipManager.MAX_INVITE_FLIPS)
|
||||
{
|
||||
throw new UserFriendlyException("本周邀请解锁次数已用完");
|
||||
}
|
||||
|
||||
// 使用邀请码(由Manager处理验证和邀请逻辑)
|
||||
await _inviteCodeManager.UseInviteCodeAsync(userId, input.InviteCode);
|
||||
|
||||
_logger.LogInformation($"用户 {userId} 使用邀请码 {input.InviteCode} 解锁翻牌次数成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取我的邀请码信息
|
||||
/// </summary>
|
||||
public async Task<InviteCodeOutput> GetMyInviteCodeAsync()
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
var weekStart = CardFlipManager.GetWeekStartDate(DateTime.Now);
|
||||
|
||||
// 获取我的邀请码
|
||||
var inviteCode = await _inviteCodeManager.GetUserInviteCodeAsync(userId);
|
||||
|
||||
// 统计本周邀请人数
|
||||
var invitedCount = await _inviteCodeManager.GetWeeklyInvitationCountAsync(userId, weekStart);
|
||||
|
||||
// 获取邀请历史
|
||||
var invitationHistory = await _inviteCodeManager.GetInvitationHistoryAsync(userId, 10);
|
||||
|
||||
return new InviteCodeOutput
|
||||
{
|
||||
MyInviteCode = inviteCode?.Code,
|
||||
InvitedCount = invitedCount,
|
||||
InvitationHistory = invitationHistory.Select(x => new InvitationHistoryItem
|
||||
{
|
||||
InvitedUserName = x.InvitedUserName,
|
||||
InvitationTime = x.InvitationTime,
|
||||
WeekDescription = GetWeekDescription(x.InvitationTime)
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成我的邀请码
|
||||
/// </summary>
|
||||
public async Task<string> GenerateMyInviteCodeAsync()
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
|
||||
// 生成邀请码(由Manager处理)
|
||||
var code = await _inviteCodeManager.GenerateInviteCodeForUserAsync(userId);
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
#region 私有辅助方法
|
||||
|
||||
/// <summary>
|
||||
/// 构建翻牌记录列表
|
||||
/// </summary>
|
||||
private List<CardFlipRecord> BuildFlipRecords(CardFlipTaskAggregateRoot? task)
|
||||
{
|
||||
var records = new List<CardFlipRecord>();
|
||||
|
||||
// 获取已翻牌的顺序
|
||||
var flippedOrder = task != null ? _cardFlipManager.GetFlippedOrder(task) : new List<int>();
|
||||
var flippedNumbers = new HashSet<int>(flippedOrder);
|
||||
|
||||
// 获取中奖记录
|
||||
var winRecords = task?.WinRecords ?? new Dictionary<int, long>();
|
||||
|
||||
// 构建记录,按照原始序号1-10排列
|
||||
for (int i = 1; i <= CardFlipManager.TOTAL_MAX_FLIPS; i++)
|
||||
{
|
||||
var record = new CardFlipRecord
|
||||
{
|
||||
FlipNumber = i,
|
||||
IsFlipped = flippedNumbers.Contains(i),
|
||||
IsWin = false,
|
||||
FlipTypeDesc = CardFlipManager.GetFlipTypeDesc(i),
|
||||
// 设置在翻牌顺序中的位置(0表示未翻,>0表示第几个翻的)
|
||||
FlipOrderIndex = flippedOrder.IndexOf(i) >= 0 ? flippedOrder.IndexOf(i) + 1 : 0
|
||||
};
|
||||
|
||||
// 设置中奖信息
|
||||
// 判断这张卡是第几次翻的
|
||||
if (task != null && flippedNumbers.Contains(i))
|
||||
{
|
||||
var flipOrderIndex = flippedOrder.IndexOf(i) + 1; // 第几次翻的(1-based)
|
||||
|
||||
// 检查这次翻牌是否中奖
|
||||
if (winRecords.TryGetValue(flipOrderIndex, out var rewardAmount))
|
||||
{
|
||||
record.IsWin = true;
|
||||
record.RewardAmount = rewardAmount;
|
||||
}
|
||||
}
|
||||
|
||||
records.Add(record);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成下次翻牌提示
|
||||
/// </summary>
|
||||
private string GenerateNextFlipTip(CardFlipStatusOutput status)
|
||||
{
|
||||
if (status.TotalFlips >= CardFlipManager.TOTAL_MAX_FLIPS)
|
||||
{
|
||||
return "本周翻牌次数已用完,请下周再来!";
|
||||
}
|
||||
|
||||
if (status.RemainingFreeFlips > 0)
|
||||
{
|
||||
return $"本周您还有{status.RemainingFreeFlips}次免费翻牌机会";
|
||||
}
|
||||
else if (status.RemainingInviteFlips > 0)
|
||||
{
|
||||
if (status.TotalFlips >= 7)
|
||||
{
|
||||
return $"本周使用他人邀请码或他人使用你的邀请码,可解锁{status.RemainingInviteFlips}次翻牌,且必中大奖!每次中奖最大额度将翻倍!";
|
||||
}
|
||||
|
||||
return $"本周使用他人邀请码或他人使用你的邀请码,可解锁{status.RemainingInviteFlips}次翻牌,必中大奖!每次中奖最大额度将翻倍!";
|
||||
}
|
||||
|
||||
return "继续加油!";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发放奖励
|
||||
/// </summary>
|
||||
private async Task GrantRewardAsync(Guid userId, long amount, string description)
|
||||
{
|
||||
var premiumPackage = new PremiumPackageAggregateRoot(userId, amount, description)
|
||||
{
|
||||
PurchaseAmount = 0, // 奖励不需要付费
|
||||
Remark = $"翻牌活动奖励:{amount / 10000}w tokens"
|
||||
};
|
||||
|
||||
await _premiumPackageRepository.InsertAsync(premiumPackage);
|
||||
|
||||
_logger.LogInformation($"用户 {userId} 获得翻牌奖励 {amount / 10000}w tokens");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取周描述
|
||||
/// </summary>
|
||||
private string GetWeekDescription(DateTime date)
|
||||
{
|
||||
var weekStart = CardFlipManager.GetWeekStartDate(date);
|
||||
var weekEnd = weekStart.AddDays(6);
|
||||
|
||||
return $"{weekStart:MM-dd} 至 {weekEnd:MM-dd}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.AiHub.Domain.Managers;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
@@ -35,17 +36,19 @@ public class AiChatService : ApplicationService
|
||||
private readonly AiBlacklistManager _aiBlacklistManager;
|
||||
private readonly ILogger<AiChatService> _logger;
|
||||
private readonly AiGateWayManager _aiGateWayManager;
|
||||
private readonly PremiumPackageManager _premiumPackageManager;
|
||||
|
||||
public AiChatService(IHttpContextAccessor httpContextAccessor,
|
||||
AiBlacklistManager aiBlacklistManager,
|
||||
ISqlSugarRepository<AiModelEntity> aiModelRepository,
|
||||
ILogger<AiChatService> logger, AiGateWayManager aiGateWayManager)
|
||||
ILogger<AiChatService> logger, AiGateWayManager aiGateWayManager, PremiumPackageManager premiumPackageManager)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_aiBlacklistManager = aiBlacklistManager;
|
||||
_aiModelRepository = aiModelRepository;
|
||||
_logger = logger;
|
||||
_aiGateWayManager = aiGateWayManager;
|
||||
_premiumPackageManager = premiumPackageManager;
|
||||
}
|
||||
|
||||
|
||||
@@ -118,6 +121,17 @@ public class AiChatService : ApplicationService
|
||||
}
|
||||
}
|
||||
|
||||
//如果是尊享包服务,需要校验是是否尊享包足够
|
||||
if (CurrentUser.IsAuthenticated && PremiumPackageConst.ModeIds.Contains(input.Model))
|
||||
{
|
||||
// 检查尊享token包用量
|
||||
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(CurrentUser.GetId());
|
||||
if (availableTokens <= 0)
|
||||
{
|
||||
throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包");
|
||||
}
|
||||
}
|
||||
|
||||
//ai网关代理httpcontext
|
||||
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
|
||||
CurrentUser.Id, sessionId, cancellationToken);
|
||||
@@ -154,7 +168,7 @@ public class AiChatService : ApplicationService
|
||||
{
|
||||
input.Model = "DeepSeek-R1-0528";
|
||||
}
|
||||
|
||||
|
||||
//ai网关代理httpcontext
|
||||
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
|
||||
CurrentUser.Id, null, cancellationToken);
|
||||
@@ -0,0 +1,185 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.Users;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.DailyTask;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.AiHub.Domain.Managers;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 每日任务服务
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class DailyTaskService : ApplicationService
|
||||
{
|
||||
private readonly ISqlSugarRepository<DailyTaskRewardRecordAggregateRoot> _dailyTaskRepository;
|
||||
private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository;
|
||||
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
|
||||
private readonly ILogger<DailyTaskService> _logger;
|
||||
|
||||
// 任务配置
|
||||
private readonly Dictionary<int, (long RequiredTokens, long RewardTokens, string Name, string Description)>
|
||||
_taskConfigs = new()
|
||||
{
|
||||
{ 1, (10000000, 1000000, "尊享包1000w token任务", "累积使用尊享包 1000w token") }, // 1000w消耗 -> 100w奖励
|
||||
{ 2, (30000000, 2000000, "尊享包3000w token任务", "累积使用尊享包 3000w token") } // 3000w消耗 -> 200w奖励
|
||||
};
|
||||
|
||||
public DailyTaskService(
|
||||
ISqlSugarRepository<DailyTaskRewardRecordAggregateRoot> dailyTaskRepository,
|
||||
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
|
||||
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
|
||||
ILogger<DailyTaskService> logger)
|
||||
{
|
||||
_dailyTaskRepository = dailyTaskRepository;
|
||||
_messageRepository = messageRepository;
|
||||
_premiumPackageRepository = premiumPackageRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取今日任务状态
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task<DailyTaskStatusOutput> GetTodayTaskStatusAsync()
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
var today = DateTime.Today;
|
||||
|
||||
// 1. 统计今日尊享包Token消耗量
|
||||
var todayConsumed = await GetTodayPremiumTokenConsumptionAsync(userId, today);
|
||||
|
||||
// 2. 查询今日已领取的任务
|
||||
var claimedTasks = await _dailyTaskRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && x.TaskDate == today)
|
||||
.Select(x => new { x.TaskLevel, x.IsRewarded })
|
||||
.ToListAsync();
|
||||
|
||||
// 3. 构建任务列表
|
||||
var tasks = new List<DailyTaskItem>();
|
||||
foreach (var (level, config) in _taskConfigs)
|
||||
{
|
||||
var claimed = claimedTasks.FirstOrDefault(x => x.TaskLevel == level);
|
||||
int status;
|
||||
|
||||
if (claimed != null && claimed.IsRewarded)
|
||||
{
|
||||
status = 2; // 已领取
|
||||
}
|
||||
else if (todayConsumed >= config.RequiredTokens)
|
||||
{
|
||||
status = 1; // 可领取
|
||||
}
|
||||
else
|
||||
{
|
||||
status = 0; // 未完成
|
||||
}
|
||||
|
||||
var progress = todayConsumed >= config.RequiredTokens
|
||||
? 100
|
||||
: Math.Round((decimal)todayConsumed / config.RequiredTokens * 100, 2);
|
||||
|
||||
tasks.Add(new DailyTaskItem
|
||||
{
|
||||
Level = level,
|
||||
Name = config.Name,
|
||||
Description = config.Description,
|
||||
RequiredTokens = config.RequiredTokens,
|
||||
RewardTokens = config.RewardTokens,
|
||||
Status = status,
|
||||
Progress = progress
|
||||
});
|
||||
}
|
||||
|
||||
return new DailyTaskStatusOutput
|
||||
{
|
||||
TodayConsumedTokens = todayConsumed,
|
||||
Tasks = tasks
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 领取任务奖励
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public async Task ClaimTaskRewardAsync(ClaimTaskRewardInput input)
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
var today = DateTime.Today;
|
||||
|
||||
// 1. 验证任务等级
|
||||
if (!_taskConfigs.TryGetValue(input.TaskLevel, out var taskConfig))
|
||||
{
|
||||
throw new UserFriendlyException($"无效的任务等级: {input.TaskLevel}");
|
||||
}
|
||||
|
||||
// 2. 检查是否已领取
|
||||
var existingRecord = await _dailyTaskRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && x.TaskDate == today && x.TaskLevel == input.TaskLevel)
|
||||
.FirstAsync();
|
||||
|
||||
if (existingRecord != null)
|
||||
{
|
||||
throw new UserFriendlyException("今日该任务奖励已领取,请明天再来!");
|
||||
}
|
||||
|
||||
// 3. 验证今日Token消耗是否达标
|
||||
var todayConsumed = await GetTodayPremiumTokenConsumptionAsync(userId, today);
|
||||
if (todayConsumed < taskConfig.RequiredTokens)
|
||||
{
|
||||
throw new UserFriendlyException(
|
||||
$"Token消耗未达标!需要 {taskConfig.RequiredTokens / 10000}w,当前 {todayConsumed / 10000}w");
|
||||
}
|
||||
|
||||
// 4. 创建奖励包(使用 PremiumPackageManager)
|
||||
|
||||
var premiumPackage =
|
||||
new PremiumPackageAggregateRoot(userId, taskConfig.RewardTokens, $"每日任务:{taskConfig.Name}")
|
||||
{
|
||||
PurchaseAmount = 0, // 奖励不需要付费
|
||||
Remark = $"{today:yyyy-MM-dd} 每日任务奖励"
|
||||
};
|
||||
|
||||
await _premiumPackageRepository.InsertAsync(premiumPackage);
|
||||
|
||||
// 5. 记录领取记录
|
||||
var record = new DailyTaskRewardRecordAggregateRoot(userId, input.TaskLevel, today, taskConfig.RewardTokens)
|
||||
{
|
||||
Remark = $"完成任务{input.TaskLevel},名称:{taskConfig.Name},消耗 {todayConsumed / 10000}w token"
|
||||
};
|
||||
|
||||
await _dailyTaskRepository.InsertAsync(record);
|
||||
|
||||
_logger.LogInformation(
|
||||
$"用户 {userId} 领取每日任务 {input.TaskLevel} 奖励成功,获得 {taskConfig.RewardTokens / 10000}w tokens");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取今日尊享包Token消耗量
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="today">今日日期</param>
|
||||
/// <returns>消耗的Token总数</returns>
|
||||
private async Task<long> GetTodayPremiumTokenConsumptionAsync(Guid userId, DateTime today)
|
||||
{
|
||||
var tomorrow = today.AddDays(1);
|
||||
|
||||
// 查询今日所有使用尊享包模型的消息(role=system 表示消耗)
|
||||
var totalTokens = await _messageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId)
|
||||
.Where(x => x.Role == "system") // system角色表示实际消耗
|
||||
.Where(x => PremiumPackageConst.ModeIds.Contains(x.ModelId)) // 尊享包模型
|
||||
.Where(x => x.CreationTime >= today && x.CreationTime < tomorrow)
|
||||
.SumAsync(x => x.TokenUsage.TotalTokenCount);
|
||||
|
||||
return totalTokens;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Xml.Serialization;
|
||||
using Medallion.Threading;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -18,7 +17,7 @@ using Yi.Framework.Rbac.Application.Contracts.Dtos.Account;
|
||||
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Services;
|
||||
namespace Yi.Framework.AiHub.Application.Services.Fuwuhao;
|
||||
|
||||
/// <summary>
|
||||
/// 服务号服务
|
||||
@@ -57,6 +57,18 @@ public class OpenApiService : ApplicationService
|
||||
var httpContext = this._httpContextAccessor.HttpContext;
|
||||
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
|
||||
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
|
||||
|
||||
//如果是尊享包服务,需要校验是是否尊享包足够
|
||||
if (PremiumPackageConst.ModeIds.Contains(input.Model))
|
||||
{
|
||||
// 检查尊享token包用量
|
||||
var availableTokens = await _premiumPackageManager.GetAvailableTokensAsync(userId);
|
||||
if (availableTokens <= 0)
|
||||
{
|
||||
throw new UserFriendlyException("尊享token包用量不足,请先购买尊享token包");
|
||||
}
|
||||
}
|
||||
|
||||
//ai网关代理httpcontext
|
||||
if (input.Stream == true)
|
||||
{
|
||||
@@ -181,11 +193,16 @@ public class OpenApiService : ApplicationService
|
||||
|
||||
private string? GetTokenByHttpContext(HttpContext httpContext)
|
||||
{
|
||||
// 获取Authorization头
|
||||
string authHeader = httpContext.Request.Headers["Authorization"];
|
||||
// 优先从 x-api-key 获取
|
||||
string apiKeyHeader = httpContext.Request.Headers["x-api-key"];
|
||||
if (!string.IsNullOrWhiteSpace(apiKeyHeader))
|
||||
{
|
||||
return apiKeyHeader.Trim();
|
||||
}
|
||||
|
||||
// 检查是否有Bearer token
|
||||
if (authHeader != null && authHeader.StartsWith("Bearer "))
|
||||
// 再检查 Authorization 头
|
||||
string authHeader = httpContext.Request.Headers["Authorization"];
|
||||
if (!string.IsNullOrWhiteSpace(authHeader) && authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return authHeader.Substring("Bearer ".Length).Trim();
|
||||
}
|
||||
|
||||
@@ -112,6 +112,13 @@ public class PayService : ApplicationService, IPayService
|
||||
|
||||
_logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus);
|
||||
|
||||
// 验证交易状态,只有交易成功才执行充值逻辑
|
||||
if (status != TradeStatusEnum.TRADE_SUCCESS)
|
||||
{
|
||||
_logger.LogError($"订单 {outTradeNo} 状态为 {tradeStatus},不执行充值逻辑");
|
||||
return "success";
|
||||
}
|
||||
|
||||
// 5. 根据商品类型进行不同的处理
|
||||
if (order.GoodsType.IsPremiumPackage())
|
||||
{
|
||||
@@ -189,9 +196,10 @@ public class PayService : ApplicationService, IPayService
|
||||
/// <summary>
|
||||
/// 获取商品列表
|
||||
/// </summary>
|
||||
/// <param name="input">获取商品列表输入</param>
|
||||
/// <returns>商品列表</returns>
|
||||
[HttpGet("pay/GoodsList")]
|
||||
public async Task<List<GoodsListOutput>> GetGoodsListAsync()
|
||||
public async Task<List<GoodsListOutput>> GetGoodsListAsync([FromQuery] GetGoodsListInput input)
|
||||
{
|
||||
var goodsList = new List<GoodsListOutput>();
|
||||
|
||||
@@ -205,36 +213,56 @@ public class PayService : ApplicationService, IPayService
|
||||
// 遍历所有商品枚举
|
||||
foreach (GoodsTypeEnum goodsType in Enum.GetValues(typeof(GoodsTypeEnum)))
|
||||
{
|
||||
// 如果指定了商品类别,则过滤
|
||||
if (input.GoodsCategoryType.HasValue)
|
||||
{
|
||||
var goodsCategory = goodsType.GetGoodsCategory();
|
||||
if (goodsCategory != input.GoodsCategoryType.Value)
|
||||
{
|
||||
continue; // 跳过不匹配的商品
|
||||
}
|
||||
}
|
||||
|
||||
var originalPrice = goodsType.GetTotalAmount();
|
||||
decimal actualPrice = originalPrice;
|
||||
decimal? discountAmount = null;
|
||||
string? discountDescription = null;
|
||||
|
||||
// 如果是尊享包商品,计算折扣
|
||||
if (goodsType.IsPremiumPackage() && CurrentUser.IsAuthenticated)
|
||||
if (goodsType.IsPremiumPackage())
|
||||
{
|
||||
discountAmount = goodsType.CalculateDiscount(totalRechargeAmount);
|
||||
actualPrice = goodsType.GetDiscountedPrice(totalRechargeAmount);
|
||||
|
||||
if (discountAmount > 0)
|
||||
|
||||
if (CurrentUser.IsAuthenticated)
|
||||
{
|
||||
discountDescription = $"已优惠 ¥{discountAmount:F2}(累计充值每10元减1元,最多减20元)";
|
||||
discountAmount = goodsType.CalculateDiscount(totalRechargeAmount);
|
||||
actualPrice = goodsType.GetDiscountedPrice(totalRechargeAmount);
|
||||
if (discountAmount > 0)
|
||||
{
|
||||
discountDescription = $"根据累积充值已优惠 ¥{discountAmount:F2}";
|
||||
}
|
||||
else
|
||||
{
|
||||
discountDescription = $"累积充值过低,暂无优惠";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
discountDescription = "累计充值每10元可减1元,最多减20元";
|
||||
discountDescription = $"登录后查看优惠";
|
||||
}
|
||||
}
|
||||
|
||||
var goodsItem = new GoodsListOutput
|
||||
{
|
||||
GoodsName = goodsType.GetDisplayName(),
|
||||
GoodsName = goodsType.GetChineseName(),
|
||||
OriginalPrice = originalPrice,
|
||||
ReferencePrice = goodsType.GetReferencePrice(),
|
||||
GoodsPrice = actualPrice,
|
||||
GoodsType = goodsType,
|
||||
Remark = GetGoodsRemark(goodsType),
|
||||
GoodsCategory = goodsType.GetGoodsCategory().ToString(),
|
||||
Remark = goodsType.GetRemark(),
|
||||
DiscountAmount = discountAmount,
|
||||
DiscountDescription = discountDescription
|
||||
DiscountDescription = discountDescription,
|
||||
GoodsType = goodsType
|
||||
};
|
||||
|
||||
goodsList.Add(goodsItem);
|
||||
@@ -243,27 +271,6 @@ public class PayService : ApplicationService, IPayService
|
||||
return goodsList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品备注信息
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <returns>商品备注</returns>
|
||||
private string GetGoodsRemark(GoodsTypeEnum goodsType)
|
||||
{
|
||||
if (goodsType.IsPremiumPackage())
|
||||
{
|
||||
var tokenAmount = goodsType.GetTokenAmount();
|
||||
return $"尊享包服务,提供 {tokenAmount:N0} Tokens(需要VIP资格)";
|
||||
}
|
||||
else if (goodsType.IsVipService())
|
||||
{
|
||||
var validMonths = goodsType.GetValidMonths();
|
||||
var monthlyPrice = goodsType.GetMonthlyPrice();
|
||||
return $"VIP服务,有效期 {validMonths} 个月,月均价 ¥{monthlyPrice:F2}";
|
||||
}
|
||||
|
||||
return "未知商品类型";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取交易状态描述
|
||||
@@ -288,6 +295,7 @@ public class PayService : ApplicationService, IPayService
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return TradeStatusEnum.WAIT_TRADE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Managers;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
@@ -64,6 +65,7 @@ namespace Yi.Framework.AiHub.Application.Services
|
||||
{
|
||||
// 直接查询该用户最大的过期时间
|
||||
var maxExpireTime = await _repository._DbQueryable
|
||||
.Where(x => x.RechargeType == RechargeTypeEnum.Vip)
|
||||
.Where(x => x.UserId == input.UserId && x.ExpireDateTime.HasValue)
|
||||
.MaxAsync(x => x.ExpireDateTime);
|
||||
|
||||
@@ -85,7 +87,8 @@ namespace Yi.Framework.AiHub.Application.Services
|
||||
Content = input.Content,
|
||||
ExpireDateTime = expireDateTime,
|
||||
Remark = input.Remark,
|
||||
ContactInfo = input.ContactInfo
|
||||
ContactInfo = input.ContactInfo,
|
||||
RechargeType = RechargeTypeEnum.Vip
|
||||
};
|
||||
|
||||
// 保存充值记录到数据库
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
using Mapster;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Volo.Abp.Application.Services;
|
||||
using Volo.Abp.Users;
|
||||
using Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
|
||||
using Yi.Framework.AiHub.Application.Contracts.IServices;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||
using Yi.Framework.AiHub.Domain.Extensions;
|
||||
using Yi.Framework.Ddd.Application.Contracts;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Services;
|
||||
@@ -18,13 +23,16 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
|
||||
{
|
||||
private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository;
|
||||
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
|
||||
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
|
||||
|
||||
public UsageStatisticsService(
|
||||
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
|
||||
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
|
||||
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository,
|
||||
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository)
|
||||
{
|
||||
_messageRepository = messageRepository;
|
||||
_usageStatisticsRepository = usageStatisticsRepository;
|
||||
_premiumPackageRepository = premiumPackageRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,6 +48,7 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
|
||||
// 从Message表统计近7天的token消耗
|
||||
var dailyUsage = await _messageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId)
|
||||
.Where(x => x.Role == "assistant" || x.Role == "system")
|
||||
.Where(x => x.CreationTime >= startDate && x.CreationTime < endDate.AddDays(1))
|
||||
.GroupBy(x => x.CreationTime.Date)
|
||||
.Select(g => new
|
||||
@@ -102,4 +111,57 @@ public class UsageStatisticsService : ApplicationService, IUsageStatisticsServic
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户尊享服务Token用量统计
|
||||
/// </summary>
|
||||
/// <returns>尊享服务Token用量统计</returns>
|
||||
public async Task<PremiumTokenUsageDto> GetPremiumTokenUsageAsync()
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
|
||||
// 获取尊享包Token信息
|
||||
var premiumPackages = await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && x.IsActive)
|
||||
.ToListAsync();
|
||||
|
||||
var result = new PremiumTokenUsageDto();
|
||||
|
||||
if (premiumPackages.Any())
|
||||
{
|
||||
// 过滤掉已过期、禁用的包,不过滤用量负数的包
|
||||
var validPackages = premiumPackages
|
||||
.Where(p => p.IsAvailable(false))
|
||||
.ToList();
|
||||
|
||||
result.PremiumTotalTokens = validPackages.Sum(x => x.TotalTokens);
|
||||
result.PremiumUsedTokens = validPackages.Sum(x => x.UsedTokens);
|
||||
result.PremiumRemainingTokens = validPackages.Sum(x => x.RemainingTokens);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户尊享服务token用量统计列表
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet("usage-statistics/premium-token-usage/list")]
|
||||
public async Task<PagedResultDto<PremiumTokenUsageGetListOutput>> GetPremiumTokenUsageListAsync(
|
||||
PremiumTokenUsageGetListInput input)
|
||||
{
|
||||
var userId = CurrentUser.GetId();
|
||||
RefAsync<int> total = 0;
|
||||
// 获取尊享包Token信息
|
||||
var entities = await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId)
|
||||
.WhereIF(input.IsFree == false, x => x.PurchaseAmount > 0)
|
||||
.WhereIF(input.IsFree == true, x => x.PurchaseAmount == 0)
|
||||
.WhereIF(input.StartTime is not null && input.EndTime is not null,
|
||||
x => x.CreationTime >= input.StartTime && x.CreationTime <= input.EndTime)
|
||||
.OrderByDescending(x => x.CreationTime)
|
||||
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
|
||||
return new PagedResultDto<PremiumTokenUsageGetListOutput>(total,
|
||||
entities.Adapt<List<PremiumTokenUsageGetListOutput>>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
|
||||
public class PremiumPackageConst
|
||||
{
|
||||
public static List<string> ModeIds = ["claude-sonnet-4-5-20250929"];
|
||||
}
|
||||
@@ -32,6 +32,25 @@ public class AnthropicStreamDto
|
||||
PromptTokensDetails = null,
|
||||
CompletionTokensDetails = null
|
||||
};
|
||||
|
||||
|
||||
public void SupplementalMultiplier(double multiplier)
|
||||
{
|
||||
if (this.Usage is not null)
|
||||
{
|
||||
this.Usage.CacheCreationInputTokens =
|
||||
(int)Math.Round((this.Usage.CacheCreationInputTokens ?? 0) * multiplier);
|
||||
|
||||
this.Usage.CacheReadInputTokens =
|
||||
(int)Math.Round((this.Usage.CacheReadInputTokens ?? 0) * multiplier);
|
||||
|
||||
this.Usage.InputTokens =
|
||||
(int)Math.Round((this.Usage.InputTokens ?? 0) * multiplier);
|
||||
|
||||
this.Usage.OutputTokens =
|
||||
(int)Math.Round((this.Usage.OutputTokens ?? 0) * multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AnthropicStreamErrorDto
|
||||
@@ -75,6 +94,8 @@ public class AnthropicChatCompletionDtoContentBlock
|
||||
[JsonPropertyName("tool_use_id")] public string? ToolUseId { get; set; }
|
||||
|
||||
[JsonPropertyName("content")] public object? Content { get; set; }
|
||||
|
||||
[JsonPropertyName("text")] public string? Text { get; set; }
|
||||
}
|
||||
|
||||
public class AnthropicChatCompletionDto
|
||||
@@ -93,7 +114,7 @@ public class AnthropicChatCompletionDto
|
||||
|
||||
public object stop_sequence { get; set; }
|
||||
|
||||
public AnthropicCompletionDtoUsage Usage { get; set; }
|
||||
public AnthropicCompletionDtoUsage? Usage { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public ThorUsageResponse TokenUsage => new ThorUsageResponse
|
||||
@@ -108,6 +129,24 @@ public class AnthropicChatCompletionDto
|
||||
PromptTokensDetails = null,
|
||||
CompletionTokensDetails = null
|
||||
};
|
||||
|
||||
public void SupplementalMultiplier(double multiplier)
|
||||
{
|
||||
if (this.Usage is not null)
|
||||
{
|
||||
this.Usage.CacheCreationInputTokens =
|
||||
(int)Math.Round((this.Usage?.CacheCreationInputTokens ?? 0) * multiplier);
|
||||
|
||||
this.Usage.CacheReadInputTokens =
|
||||
(int)Math.Round((this.Usage?.CacheReadInputTokens ?? 0) * multiplier);
|
||||
|
||||
this.Usage.InputTokens =
|
||||
(int)Math.Round((this.Usage?.InputTokens ?? 0) * multiplier);
|
||||
|
||||
this.Usage.OutputTokens =
|
||||
(int)Math.Round((this.Usage?.OutputTokens ?? 0) * multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class AnthropicChatCompletionDtoContent
|
||||
|
||||
@@ -108,9 +108,16 @@ public sealed class AnthropicInput
|
||||
|
||||
public class AnthropicThinkingInput
|
||||
{
|
||||
[JsonPropertyName("type")] public string Type { get; set; }
|
||||
|
||||
[JsonPropertyName("budget_tokens")] public int BudgetTokens { get; set; }
|
||||
[JsonPropertyName("type")] public string? Type { get; set; }
|
||||
[JsonPropertyName("budget_tokens")] public int? BudgetTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("signature")] public string? Signature { get; set; }
|
||||
|
||||
[JsonPropertyName("thinking")] public string? Thinking { get; set; }
|
||||
|
||||
[JsonPropertyName("data")] public string? Data { get; set; }
|
||||
|
||||
[JsonPropertyName("text")] public string? Text { get; set; }
|
||||
}
|
||||
|
||||
public class AnthropicTooChoiceInput
|
||||
@@ -122,16 +129,16 @@ public class AnthropicTooChoiceInput
|
||||
|
||||
public class AnthropicMessageTool
|
||||
{
|
||||
[JsonPropertyName("name")] public string name { get; set; }
|
||||
[JsonPropertyName("name")] public string? name { get; set; }
|
||||
|
||||
[JsonPropertyName("description")] public string? Description { get; set; }
|
||||
|
||||
[JsonPropertyName("input_schema")] public Input_schema InputSchema { get; set; }
|
||||
[JsonPropertyName("input_schema")] public Input_schema? InputSchema { get; set; }
|
||||
}
|
||||
|
||||
public class Input_schema
|
||||
{
|
||||
[JsonPropertyName("type")] public string Type { get; set; }
|
||||
[JsonPropertyName("type")] public string? Type { get; set; }
|
||||
|
||||
[JsonPropertyName("properties")] public Dictionary<string, InputSchemaValue>? Properties { get; set; }
|
||||
|
||||
@@ -140,9 +147,9 @@ public class Input_schema
|
||||
|
||||
public class InputSchemaValue
|
||||
{
|
||||
public string type { get; set; }
|
||||
public string? type { get; set; }
|
||||
|
||||
public string description { get; set; }
|
||||
public string? description { get; set; }
|
||||
|
||||
public InputSchemaValueItems? items { get; set; }
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ public class AnthropicMessageContent
|
||||
|
||||
[JsonPropertyName("thinking")] public string? Thinking { get; set; }
|
||||
|
||||
[JsonPropertyName("signature")] public string? Signature { get; set; }
|
||||
|
||||
[JsonPropertyName("input")] public object? Input { get; set; }
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
|
||||
@@ -22,11 +22,15 @@ public class AnthropicMessageInput
|
||||
throw new ValidationException("Messages 中 Content 和 Contents 字段不能同时有值");
|
||||
}
|
||||
|
||||
if (Content is not null)
|
||||
if (!string.IsNullOrEmpty(Content))
|
||||
{
|
||||
return Content;
|
||||
}
|
||||
|
||||
// 如果 Contents 为空或 null,返回空字符串而不是 null
|
||||
if (Contents == null || Contents.Count == 0)
|
||||
{
|
||||
return "_"; // 兼容客户端空值问题
|
||||
}
|
||||
return Contents!;
|
||||
}
|
||||
set
|
||||
|
||||
@@ -60,4 +60,19 @@ public record ThorChatCompletionsResponse
|
||||
/// </summary>
|
||||
[JsonPropertyName("error")]
|
||||
public ThorError? Error { get; set; }
|
||||
|
||||
public void SupplementalMultiplier(double multiplier)
|
||||
{
|
||||
if (this.Usage is not null)
|
||||
{
|
||||
this.Usage.InputTokens =
|
||||
(int)Math.Round((this.Usage.InputTokens ?? 0) * multiplier);
|
||||
this.Usage.OutputTokens =
|
||||
(int)Math.Round((this.Usage.OutputTokens ?? 0) * multiplier);
|
||||
this.Usage.CompletionTokens =
|
||||
(int)Math.Round((this.Usage.CompletionTokens ?? 0) * multiplier);
|
||||
this.Usage.PromptTokens =
|
||||
(int)Math.Round((this.Usage.PromptTokens ?? 0) * multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
public enum AnnouncementTypeEnum
|
||||
{
|
||||
Activity=1,
|
||||
System=2
|
||||
}
|
||||
@@ -10,12 +10,16 @@ namespace Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
public class PriceAttribute : Attribute
|
||||
{
|
||||
public decimal Price { get; }
|
||||
|
||||
public decimal ReferencePrice { get; }
|
||||
|
||||
public int ValidMonths { get; }
|
||||
|
||||
public PriceAttribute(double price, int validMonths)
|
||||
public PriceAttribute(double price, int validMonths, double referencePrice)
|
||||
{
|
||||
Price = (decimal)price;
|
||||
ValidMonths = validMonths;
|
||||
ReferencePrice = (decimal)referencePrice;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +30,14 @@ public class PriceAttribute : Attribute
|
||||
public class DisplayNameAttribute : Attribute
|
||||
{
|
||||
public string DisplayName { get; }
|
||||
public string ChineseName { get; }
|
||||
public string Remark { get; }
|
||||
|
||||
public DisplayNameAttribute(string displayName)
|
||||
public DisplayNameAttribute(string displayName, string chineseName = "", string remark = "")
|
||||
{
|
||||
DisplayName = displayName;
|
||||
ChineseName = chineseName;
|
||||
Remark = remark;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +64,7 @@ public enum GoodsCategoryType
|
||||
/// <summary>
|
||||
/// VIP服务
|
||||
/// </summary>
|
||||
VipService = 1,
|
||||
Vip = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 尊享包服务
|
||||
@@ -85,39 +93,32 @@ public class TokenAmountAttribute : Attribute
|
||||
public enum GoodsTypeEnum
|
||||
{
|
||||
// VIP服务
|
||||
[Price(29.9, 1)]
|
||||
[DisplayName("YiXinVip 1 month")]
|
||||
[GoodsCategory(GoodsCategoryType.VipService)]
|
||||
[Price(29.9, 1, 29.9)] [DisplayName("YiXinVip 1 month", "1个月", "灵活选择")] [GoodsCategory(GoodsCategoryType.Vip)]
|
||||
YiXinVip1 = 1,
|
||||
|
||||
[Price(83.7, 3)]
|
||||
[DisplayName("YiXinVip 3 month")]
|
||||
[GoodsCategory(GoodsCategoryType.VipService)]
|
||||
[Price(83.7, 3, 27.9)] [DisplayName("YiXinVip 3 month", "3个月", "短期体验")] [GoodsCategory(GoodsCategoryType.Vip)]
|
||||
YiXinVip3 = 3,
|
||||
|
||||
[Price(155.4, 6)]
|
||||
[DisplayName("YiXinVip 6 month")]
|
||||
[GoodsCategory(GoodsCategoryType.VipService)]
|
||||
[Price(155.4, 6, 25.9)] [DisplayName("YiXinVip 6 month", "6个月", "年度热销")] [GoodsCategory(GoodsCategoryType.Vip)]
|
||||
YiXinVip6 = 6,
|
||||
|
||||
[Price(183.2, 8)]
|
||||
[DisplayName("YiXinVip 8 month")]
|
||||
[GoodsCategory(GoodsCategoryType.VipService)]
|
||||
[Price(183.2, 8, 22.9)]
|
||||
[DisplayName("YiXinVip 8 month", "8个月(推荐)", "限时活动,超高性价比")]
|
||||
[GoodsCategory(GoodsCategoryType.Vip)]
|
||||
YiXinVip8 = 8,
|
||||
|
||||
// 尊享包服务 - 需要VIP资格才能购买
|
||||
[Price(188.9, 0)]
|
||||
[DisplayName("Premium Package 5000W Tokens")]
|
||||
[Price(188.9, 0, 1750)]
|
||||
[DisplayName("YiXinPremiumPackage 5000W Tokens", "5000万Tokens", "简单尝试")]
|
||||
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
|
||||
[TokenAmount(5000)]
|
||||
[TokenAmount(50000000)]
|
||||
PremiumPackage5000W = 101,
|
||||
|
||||
[Price(248.9, 0)]
|
||||
[DisplayName("Premium Package 10000W Tokens")]
|
||||
[Price(248.9, 0, 3500)]
|
||||
[DisplayName("YiXinPremiumPackage 10000W Tokens", "1亿Tokens(推荐)", "极致性价比")]
|
||||
[GoodsCategory(GoodsCategoryType.PremiumPackage)]
|
||||
[TokenAmount(10000)]
|
||||
[TokenAmount(100000000)]
|
||||
PremiumPackage10000W = 102,
|
||||
|
||||
}
|
||||
|
||||
public static class GoodsTypeEnumExtensions
|
||||
@@ -181,6 +182,18 @@ public static class GoodsTypeEnumExtensions
|
||||
return validMonths > 0 ? totalPrice / validMonths : 0m;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品参考价格
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <returns>参考价格</returns>
|
||||
public static decimal GetReferencePrice(this GoodsTypeEnum goodsType)
|
||||
{
|
||||
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
|
||||
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
|
||||
return priceAttribute?.ReferencePrice ?? 0m;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品类别
|
||||
/// </summary>
|
||||
@@ -190,7 +203,7 @@ public static class GoodsTypeEnumExtensions
|
||||
{
|
||||
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
|
||||
var categoryAttribute = fieldInfo?.GetCustomAttribute<GoodsCategoryAttribute>();
|
||||
return categoryAttribute?.Category ?? GoodsCategoryType.VipService;
|
||||
return categoryAttribute?.Category ?? GoodsCategoryType.Vip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -210,7 +223,7 @@ public static class GoodsTypeEnumExtensions
|
||||
/// <returns>是否为VIP服务</returns>
|
||||
public static bool IsVipService(this GoodsTypeEnum goodsType)
|
||||
{
|
||||
return goodsType.GetGoodsCategory() == GoodsCategoryType.VipService;
|
||||
return goodsType.GetGoodsCategory() == GoodsCategoryType.Vip;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -225,9 +238,33 @@ public static class GoodsTypeEnumExtensions
|
||||
return tokenAttribute?.TokenAmount ?? 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品中文名称
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <returns>中文名称</returns>
|
||||
public static string GetChineseName(this GoodsTypeEnum goodsType)
|
||||
{
|
||||
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
|
||||
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
|
||||
return displayNameAttribute?.ChineseName ?? goodsType.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取商品备注
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <returns>备注信息</returns>
|
||||
public static string GetRemark(this GoodsTypeEnum goodsType)
|
||||
{
|
||||
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
|
||||
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
|
||||
return displayNameAttribute?.Remark ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算折扣金额(仅用于尊享包)
|
||||
/// 规则:每累加充值10元,减少1元,最多减少20元
|
||||
/// 规则:每累加充值10元,减少2.5元,最多减少50元
|
||||
/// </summary>
|
||||
/// <param name="goodsType">商品类型</param>
|
||||
/// <param name="totalRechargeAmount">用户累加充值金额</param>
|
||||
@@ -240,11 +277,11 @@ public static class GoodsTypeEnumExtensions
|
||||
return 0m;
|
||||
}
|
||||
|
||||
// 每10元减1元
|
||||
var discountAmount = Math.Floor(totalRechargeAmount / 10m);
|
||||
// 每10元减2.5元
|
||||
var discountAmount = Math.Floor(totalRechargeAmount / 2.5m);
|
||||
|
||||
// 最多减少20元
|
||||
return Math.Min(discountAmount, 20m);
|
||||
// 最多减少50元
|
||||
return Math.Min(discountAmount, 50m);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -260,6 +297,6 @@ public static class GoodsTypeEnumExtensions
|
||||
var discountedPrice = originalPrice - discount;
|
||||
|
||||
// 确保价格不为负数,至少为0.01元
|
||||
return Math.Max(discountedPrice, 0.01m);
|
||||
return Math.Round(discountedPrice, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
public enum ModelApiTypeEnum
|
||||
{
|
||||
OpenAi,
|
||||
Claude
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
public enum RechargeTypeEnum
|
||||
{
|
||||
Vip = 10,
|
||||
PremiumPackage = 20
|
||||
}
|
||||
@@ -9,19 +9,24 @@ using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorClaude.Chats;
|
||||
|
||||
public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactory,ILogger<AnthropicChatCompletionsService> logger)
|
||||
public class AnthropicChatCompletionsService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<AnthropicChatCompletionsService> logger)
|
||||
: IAnthropicChatCompletionService
|
||||
{
|
||||
public const double ClaudeMultiplier = 1.3d;
|
||||
|
||||
public async Task<AnthropicChatCompletionDto> ChatCompletionsAsync(AiModelDescribe options, AnthropicInput input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var openai =
|
||||
using var openai =
|
||||
Activity.Current?.Source.StartActivity("Claudia 对话补全");
|
||||
|
||||
if (string.IsNullOrEmpty(options.Endpoint))
|
||||
{
|
||||
options.Endpoint = "https://api.anthropic.com/";
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient();
|
||||
|
||||
var headers = new Dictionary<string, string>
|
||||
@@ -71,30 +76,33 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor
|
||||
if (response.StatusCode >= HttpStatusCode.BadRequest)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", options.Endpoint,
|
||||
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
|
||||
options.Endpoint,
|
||||
response.StatusCode, error);
|
||||
|
||||
throw new Exception("OpenAI对话异常" + response.StatusCode.ToString());
|
||||
throw new Exception( $"恭喜你运气爆棚遇到了错误,尊享包对话异常:StatusCode【{response.StatusCode}】,Response【{error}】");
|
||||
}
|
||||
|
||||
|
||||
var value =
|
||||
await response.Content.ReadFromJsonAsync<AnthropicChatCompletionDto>(ThorJsonSerializer.DefaultOptions,
|
||||
cancellationToken: cancellationToken);
|
||||
value.SupplementalMultiplier(AnthropicChatCompletionsService.ClaudeMultiplier);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<(string, AnthropicStreamDto?)> StreamChatCompletionsAsync(AiModelDescribe options, AnthropicInput input,
|
||||
public async IAsyncEnumerable<(string, AnthropicStreamDto?)> StreamChatCompletionsAsync(AiModelDescribe options,
|
||||
AnthropicInput input,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var openai =
|
||||
using var openai =
|
||||
Activity.Current?.Source.StartActivity("Claudia 对话补全");
|
||||
|
||||
if (string.IsNullOrEmpty(options.Endpoint))
|
||||
{
|
||||
options.Endpoint = "https://api.anthropic.com/";
|
||||
}
|
||||
|
||||
|
||||
var client = httpClientFactory.CreateClient();
|
||||
|
||||
var headers = new Dictionary<string, string>
|
||||
@@ -117,7 +125,8 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor
|
||||
if (response.StatusCode >= HttpStatusCode.BadRequest)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}", options.Endpoint,
|
||||
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
|
||||
options.Endpoint,
|
||||
response.StatusCode, error);
|
||||
|
||||
throw new Exception("OpenAI对话异常" + response.StatusCode);
|
||||
@@ -160,7 +169,8 @@ public class AnthropicChatCompletionsService(IHttpClientFactory httpClientFactor
|
||||
|
||||
var result = JsonSerializer.Deserialize<AnthropicStreamDto>(data,
|
||||
ThorJsonSerializer.DefaultOptions);
|
||||
|
||||
|
||||
result.SupplementalMultiplier(AnthropicChatCompletionsService.ClaudeMultiplier);
|
||||
yield return (eventType, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,879 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorClaude.Chats;
|
||||
|
||||
public sealed class ClaudiaChatCompletionsService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<ClaudiaChatCompletionsService> logger)
|
||||
: IChatCompletionService
|
||||
{
|
||||
public List<ThorChatChoiceResponse> CreateResponse(AnthropicChatCompletionDto completionDto)
|
||||
{
|
||||
var response = new ThorChatChoiceResponse();
|
||||
var chatMessage = new ThorChatMessage();
|
||||
if (completionDto == null)
|
||||
{
|
||||
return new List<ThorChatChoiceResponse>();
|
||||
}
|
||||
|
||||
if (completionDto.content.Any(x => x.type.Equals("thinking", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// 将推理字段合并到返回对象去
|
||||
chatMessage.ReasoningContent = completionDto.content
|
||||
.First(x => x.type.Equals("thinking", StringComparison.OrdinalIgnoreCase)).Thinking;
|
||||
|
||||
chatMessage.Role = completionDto.role;
|
||||
chatMessage.Content = completionDto.content
|
||||
.First(x => x.type.Equals("text", StringComparison.OrdinalIgnoreCase)).text;
|
||||
}
|
||||
else
|
||||
{
|
||||
chatMessage.Role = completionDto.role;
|
||||
chatMessage.Content = completionDto.content
|
||||
.FirstOrDefault()?.text;
|
||||
}
|
||||
|
||||
response.Delta = chatMessage;
|
||||
response.Message = chatMessage;
|
||||
|
||||
if (completionDto.content.Any(x => x.type.Equals("tool_use", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var toolUse = completionDto.content
|
||||
.First(x => x.type.Equals("tool_use", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
chatMessage.ToolCalls =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Id = toolUse.id,
|
||||
Function = new ThorChatMessageFunction()
|
||||
{
|
||||
Name = toolUse.name,
|
||||
Arguments = JsonSerializer.Serialize(toolUse.input,
|
||||
ThorJsonSerializer.DefaultOptions),
|
||||
},
|
||||
Index = 0,
|
||||
}
|
||||
];
|
||||
|
||||
return
|
||||
[
|
||||
response
|
||||
];
|
||||
}
|
||||
|
||||
return new List<ThorChatChoiceResponse> { response };
|
||||
}
|
||||
|
||||
private object CreateMessage(List<ThorChatMessage> messages, AiModelDescribe options)
|
||||
{
|
||||
var list = new List<object>();
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
// 如果是图片
|
||||
if (message.ContentCalculated is IList<ThorChatMessageContent> contentCalculated)
|
||||
{
|
||||
list.Add(new
|
||||
{
|
||||
role = message.Role,
|
||||
content = (List<object>)contentCalculated.Select<ThorChatMessageContent, object>(x =>
|
||||
{
|
||||
if (x.Type == "text")
|
||||
{
|
||||
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new
|
||||
{
|
||||
type = "text",
|
||||
text = x.Text,
|
||||
cache_control = new
|
||||
{
|
||||
type = "ephemeral"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
type = "text",
|
||||
text = x.Text
|
||||
};
|
||||
}
|
||||
|
||||
var isBase64 = x.ImageUrl?.Url.StartsWith("http") == true;
|
||||
|
||||
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new
|
||||
{
|
||||
type = "image",
|
||||
source = new
|
||||
{
|
||||
type = isBase64 ? "base64" : "url",
|
||||
media_type = "image/png",
|
||||
data = x.ImageUrl?.Url,
|
||||
},
|
||||
cache_control = new
|
||||
{
|
||||
type = "ephemeral"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return new
|
||||
{
|
||||
type = "image",
|
||||
source = new
|
||||
{
|
||||
type = isBase64 ? "base64" : "url",
|
||||
media_type = "image/png",
|
||||
data = x.ImageUrl?.Url,
|
||||
}
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if ("true".Equals(options.ModelExtraInfo, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (message.Role == "system")
|
||||
{
|
||||
list.Add(new
|
||||
{
|
||||
type = "text",
|
||||
text = message.Content,
|
||||
cache_control = new
|
||||
{
|
||||
type = "ephemeral"
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(new
|
||||
{
|
||||
role = message.Role,
|
||||
content = message.Content
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (message.Role == "system")
|
||||
{
|
||||
list.Add(new
|
||||
{
|
||||
type = "text",
|
||||
text = message.Content
|
||||
});
|
||||
}
|
||||
else if (message.Role == "tool")
|
||||
{
|
||||
list.Add(new
|
||||
{
|
||||
role = "user",
|
||||
content = new List<object>
|
||||
{
|
||||
new
|
||||
{
|
||||
type = "tool_result",
|
||||
tool_use_id = message.ToolCallId,
|
||||
content = message.Content
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (message.Role == "assistant")
|
||||
{
|
||||
// {
|
||||
// "role": "assistant",
|
||||
// "content": [
|
||||
// {
|
||||
// "type": "text",
|
||||
// "text": "<thinking>I need to use get_weather, and the user wants SF, which is likely San Francisco, CA.</thinking>"
|
||||
// },
|
||||
// {
|
||||
// "type": "tool_use",
|
||||
// "id": "toolu_01A09q90qw90lq917835lq9",
|
||||
// "name": "get_weather",
|
||||
// "input": {
|
||||
// "location": "San Francisco, CA",
|
||||
// "unit": "celsius"
|
||||
// }
|
||||
// }
|
||||
// ]
|
||||
// },
|
||||
if (message.ToolCalls?.Count > 0)
|
||||
{
|
||||
var content = new List<object>();
|
||||
if (!string.IsNullOrEmpty(message.Content))
|
||||
{
|
||||
content.Add(new
|
||||
{
|
||||
type = "text",
|
||||
text = message.Content
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var toolCall in message.ToolCalls)
|
||||
{
|
||||
content.Add(new
|
||||
{
|
||||
type = "tool_use",
|
||||
id = toolCall.Id,
|
||||
name = toolCall.Function?.Name,
|
||||
input = JsonSerializer.Deserialize<Dictionary<string, object>>(
|
||||
toolCall.Function?.Arguments, ThorJsonSerializer.DefaultOptions)
|
||||
});
|
||||
}
|
||||
|
||||
list.Add(new
|
||||
{
|
||||
role = "assistant",
|
||||
content
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(new
|
||||
{
|
||||
role = "assistant",
|
||||
content = new List<object>
|
||||
{
|
||||
new
|
||||
{
|
||||
type = "text",
|
||||
text = message.Content
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(new
|
||||
{
|
||||
role = message.Role,
|
||||
content = message.Content
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
|
||||
ThorChatCompletionsRequest input,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var openai =
|
||||
Activity.Current?.Source.StartActivity("Claudia 对话补全");
|
||||
|
||||
if (string.IsNullOrEmpty(options.Endpoint))
|
||||
{
|
||||
options.Endpoint = "https://api.anthropic.com/";
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient();
|
||||
|
||||
var headers = new Dictionary<string, string>
|
||||
{
|
||||
{ "x-api-key", options.ApiKey },
|
||||
{ "anthropic-version", "2023-06-01" }
|
||||
};
|
||||
|
||||
var isThinking = input.Model.EndsWith("thinking");
|
||||
input.Model = input.Model.Replace("-thinking", string.Empty);
|
||||
var budgetTokens = 1024;
|
||||
|
||||
if (input.MaxTokens is < 2048)
|
||||
{
|
||||
input.MaxTokens = 2048;
|
||||
}
|
||||
|
||||
if (input.MaxTokens != null && input.MaxTokens / 2 < 1024)
|
||||
{
|
||||
budgetTokens = input.MaxTokens.Value / (4 * 3);
|
||||
}
|
||||
|
||||
// budgetTokens最大4096
|
||||
budgetTokens = Math.Min(budgetTokens, 4096);
|
||||
|
||||
object tool_choice;
|
||||
if (input.ToolChoice is not null && input.ToolChoice.Type == "auto")
|
||||
{
|
||||
tool_choice = new
|
||||
{
|
||||
type = "auto",
|
||||
disable_parallel_tool_use = false,
|
||||
};
|
||||
}
|
||||
else if (input.ToolChoice is not null && input.ToolChoice.Type == "any")
|
||||
{
|
||||
tool_choice = new
|
||||
{
|
||||
type = "any",
|
||||
disable_parallel_tool_use = false,
|
||||
};
|
||||
}
|
||||
else if (input.ToolChoice is not null && input.ToolChoice.Type == "tool")
|
||||
{
|
||||
tool_choice = new
|
||||
{
|
||||
type = "tool",
|
||||
name = input.ToolChoice.Function?.Name,
|
||||
disable_parallel_tool_use = false,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
tool_choice = null;
|
||||
}
|
||||
|
||||
var response = await client.HttpRequestRaw(options.Endpoint.TrimEnd('/') + "/v1/messages", new
|
||||
{
|
||||
model = input.Model,
|
||||
max_tokens = input.MaxTokens ?? 2048,
|
||||
stream = true,
|
||||
tool_choice,
|
||||
system = CreateMessage(input.Messages.Where(x => x.Role == "system").ToList(), options),
|
||||
messages = CreateMessage(input.Messages.Where(x => x.Role != "system").ToList(), options),
|
||||
top_p = isThinking ? null : input.TopP,
|
||||
thinking = isThinking
|
||||
? new
|
||||
{
|
||||
type = "enabled",
|
||||
budget_tokens = budgetTokens,
|
||||
}
|
||||
: null,
|
||||
tools = input.Tools?.Select(x => new
|
||||
{
|
||||
name = x.Function?.Name,
|
||||
description = x.Function?.Description,
|
||||
input_schema = new
|
||||
{
|
||||
type = x.Function?.Parameters?.Type,
|
||||
required = x.Function?.Parameters?.Required,
|
||||
properties = x.Function?.Parameters?.Properties?.ToDictionary(y => y.Key, y => new
|
||||
{
|
||||
description = y.Value.Description,
|
||||
type = y.Value.Type,
|
||||
@enum = y.Value.Enum
|
||||
})
|
||||
}
|
||||
}).ToArray(),
|
||||
temperature = isThinking ? null : input.Temperature
|
||||
}, string.Empty, headers);
|
||||
|
||||
openai?.SetTag("Model", input.Model);
|
||||
openai?.SetTag("Response", response.StatusCode.ToString());
|
||||
|
||||
// 大于等于400的状态码都认为是异常
|
||||
if (response.StatusCode >= HttpStatusCode.BadRequest)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
|
||||
options.Endpoint,
|
||||
response.StatusCode, error);
|
||||
|
||||
throw new Exception("OpenAI对话异常" + response.StatusCode);
|
||||
}
|
||||
|
||||
using var stream = new StreamReader(await response.Content.ReadAsStreamAsync(cancellationToken));
|
||||
|
||||
using StreamReader reader = new(await response.Content.ReadAsStreamAsync(cancellationToken));
|
||||
string? line = string.Empty;
|
||||
var first = true;
|
||||
var isThink = false;
|
||||
|
||||
string? toolId = null;
|
||||
string? toolName = null;
|
||||
while ((line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false)) != null)
|
||||
{
|
||||
line += Environment.NewLine;
|
||||
|
||||
if (line.StartsWith('{'))
|
||||
{
|
||||
logger.LogInformation("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode,
|
||||
line);
|
||||
|
||||
throw new Exception("OpenAI对话异常" + line);
|
||||
}
|
||||
|
||||
if (line.StartsWith(OpenAIConstant.Data))
|
||||
line = line[OpenAIConstant.Data.Length..];
|
||||
|
||||
line = line.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(line)) continue;
|
||||
|
||||
if (line == OpenAIConstant.Done)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (line.StartsWith(':'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("event: ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = JsonSerializer.Deserialize<AnthropicStreamDto>(line,
|
||||
ThorJsonSerializer.DefaultOptions);
|
||||
|
||||
if (result?.Type == "content_block_delta")
|
||||
{
|
||||
if (result.Delta.Type is "text" or "text_delta")
|
||||
{
|
||||
yield return new ThorChatCompletionsResponse()
|
||||
{
|
||||
Choices =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Message = new ThorChatMessage()
|
||||
{
|
||||
Content = result.Delta.Text,
|
||||
Role = "assistant",
|
||||
}
|
||||
}
|
||||
],
|
||||
Model = input.Model,
|
||||
Id = result?.Message?.id,
|
||||
Usage = new ThorUsageResponse()
|
||||
{
|
||||
CompletionTokens = result?.Message?.Usage?.OutputTokens,
|
||||
PromptTokens = result?.Message?.Usage?.InputTokens,
|
||||
}
|
||||
};
|
||||
}
|
||||
else if (result.Delta.Type == "input_json_delta")
|
||||
{
|
||||
yield return new ThorChatCompletionsResponse()
|
||||
{
|
||||
Choices =
|
||||
[
|
||||
new ThorChatChoiceResponse()
|
||||
{
|
||||
Message = new ThorChatMessage()
|
||||
{
|
||||
ToolCalls =
|
||||
[
|
||||
new ThorToolCall()
|
||||
{
|
||||
Id = toolId,
|
||||
Function = new ThorChatMessageFunction()
|
||||
{
|
||||
Name = toolName,
|
||||
Arguments = result.Delta.PartialJson
|
||||
}
|
||||
}
|
||||
],
|
||||
Role = "tool",
|
||||
}
|
||||
}
|
||||
],
|
||||
Model = input.Model,
|
||||
Usage = new ThorUsageResponse()
|
||||
{
|
||||
PromptTokens = result?.Message?.Usage?.InputTokens,
|
||||
}
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new ThorChatCompletionsResponse()
|
||||
{
|
||||
Choices = new List<ThorChatChoiceResponse>()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Message = new ThorChatMessage()
|
||||
{
|
||||
ReasoningContent = result.Delta.Thinking,
|
||||
Role = "assistant",
|
||||
}
|
||||
}
|
||||
},
|
||||
Model = input.Model,
|
||||
Id = result?.Message?.id,
|
||||
Usage = new ThorUsageResponse()
|
||||
{
|
||||
CompletionTokens = result?.Message?.Usage?.OutputTokens,
|
||||
PromptTokens = result?.Message?.Usage?.InputTokens
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result?.Type == "content_block_start")
|
||||
{
|
||||
if (result?.ContentBlock?.Id is not null)
|
||||
{
|
||||
toolId = result.ContentBlock.Id;
|
||||
}
|
||||
|
||||
if (result?.ContentBlock?.Name is not null)
|
||||
{
|
||||
toolName = result.ContentBlock.Name;
|
||||
}
|
||||
|
||||
if (toolId is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
yield return new ThorChatCompletionsResponse()
|
||||
{
|
||||
Choices =
|
||||
[
|
||||
new ThorChatChoiceResponse()
|
||||
{
|
||||
Message = new ThorChatMessage()
|
||||
{
|
||||
ToolCalls =
|
||||
[
|
||||
new ThorToolCall()
|
||||
{
|
||||
Id = toolId,
|
||||
Function = new ThorChatMessageFunction()
|
||||
{
|
||||
Name = toolName
|
||||
}
|
||||
}
|
||||
],
|
||||
Role = "tool",
|
||||
}
|
||||
}
|
||||
],
|
||||
Model = input.Model,
|
||||
Usage = new ThorUsageResponse()
|
||||
{
|
||||
PromptTokens = result?.Message?.Usage?.InputTokens,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (result.Type == "content_block_delta")
|
||||
{
|
||||
yield return new ThorChatCompletionsResponse()
|
||||
{
|
||||
Choices =
|
||||
[
|
||||
new ThorChatChoiceResponse()
|
||||
{
|
||||
Message = new ThorChatMessage()
|
||||
{
|
||||
ToolCallId = result?.ContentBlock?.Id,
|
||||
FunctionCall = new ThorChatMessageFunction()
|
||||
{
|
||||
Name = result?.ContentBlock?.Name,
|
||||
Arguments = result?.Delta?.PartialJson
|
||||
},
|
||||
Role = "tool",
|
||||
}
|
||||
}
|
||||
],
|
||||
Model = input.Model,
|
||||
Usage = new ThorUsageResponse()
|
||||
{
|
||||
PromptTokens = result?.Message?.Usage?.InputTokens
|
||||
}
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.Type == "message_start")
|
||||
{
|
||||
yield return new ThorChatCompletionsResponse()
|
||||
{
|
||||
Choices =
|
||||
[
|
||||
new ThorChatChoiceResponse()
|
||||
{
|
||||
Message = new ThorChatMessage()
|
||||
{
|
||||
Content = result?.Delta?.Text,
|
||||
Role = "assistant",
|
||||
}
|
||||
}
|
||||
],
|
||||
Model = input.Model,
|
||||
Usage = new ThorUsageResponse()
|
||||
{
|
||||
PromptTokens = result?.Message?.Usage?.InputTokens,
|
||||
}
|
||||
};
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.Type == "message_delta")
|
||||
{
|
||||
var deltaOutput = new ThorChatCompletionsResponse()
|
||||
{
|
||||
Choices =
|
||||
[
|
||||
new ThorChatChoiceResponse()
|
||||
{
|
||||
Message = new ThorChatMessage()
|
||||
{
|
||||
Content = result.Delta?.Text,
|
||||
Role = "assistant",
|
||||
}
|
||||
}
|
||||
],
|
||||
Model = input.Model,
|
||||
Usage = new ThorUsageResponse
|
||||
{
|
||||
InputTokens = result.Usage?.InputTokens + result.Usage?.CacheCreationInputTokens +
|
||||
result.Usage?.CacheReadInputTokens,
|
||||
OutputTokens = result.Usage?.OutputTokens,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
deltaOutput.Usage.PromptTokens = deltaOutput.Usage.InputTokens;
|
||||
deltaOutput.Usage.CompletionTokens = deltaOutput.Usage.OutputTokens;
|
||||
|
||||
deltaOutput.Usage.TotalTokens = deltaOutput.Usage.InputTokens + deltaOutput.Usage.OutputTokens;
|
||||
|
||||
yield return deltaOutput;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.Message == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var chat = CreateResponse(result.Message);
|
||||
|
||||
var content = chat?.FirstOrDefault()?.Delta;
|
||||
|
||||
if (first && string.IsNullOrWhiteSpace(content?.Content) && string.IsNullOrEmpty(content?.ReasoningContent))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (first && content.Content == OpenAIConstant.ThinkStart)
|
||||
{
|
||||
isThink = true;
|
||||
continue;
|
||||
// 需要将content的内容转换到其他字段
|
||||
}
|
||||
|
||||
if (isThink && content.Content.Contains(OpenAIConstant.ThinkEnd))
|
||||
{
|
||||
isThink = false;
|
||||
// 需要将content的内容转换到其他字段
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isThink)
|
||||
{
|
||||
// 需要将content的内容转换到其他字段
|
||||
foreach (var choice in chat)
|
||||
{
|
||||
choice.Delta.ReasoningContent = choice.Delta.Content;
|
||||
choice.Delta.Content = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
first = false;
|
||||
|
||||
var output = new ThorChatCompletionsResponse()
|
||||
{
|
||||
Choices = chat,
|
||||
Model = input.Model,
|
||||
Id = result.Message.id,
|
||||
Usage = new ThorUsageResponse()
|
||||
{
|
||||
InputTokens = result.Message.Usage?.InputTokens + result.Message.Usage?.CacheCreationInputTokens +
|
||||
result.Message.Usage?.CacheReadInputTokens,
|
||||
OutputTokens = result.Message.Usage?.OutputTokens,
|
||||
}
|
||||
};
|
||||
output.Usage.PromptTokens = output.Usage.InputTokens;
|
||||
output.Usage.CompletionTokens = output.Usage.OutputTokens;
|
||||
output.Usage.TotalTokens = output.Usage.InputTokens + output.Usage.OutputTokens;
|
||||
output.SupplementalMultiplier(AnthropicChatCompletionsService.ClaudeMultiplier);
|
||||
yield return output;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ThorChatCompletionsResponse> CompleteChatAsync(AiModelDescribe options,
|
||||
ThorChatCompletionsRequest input,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var openai =
|
||||
Activity.Current?.Source.StartActivity("Claudia 对话补全");
|
||||
|
||||
if (string.IsNullOrEmpty(options.Endpoint))
|
||||
{
|
||||
options.Endpoint = "https://api.anthropic.com/";
|
||||
}
|
||||
|
||||
var client = httpClientFactory.CreateClient();
|
||||
|
||||
var headers = new Dictionary<string, string>
|
||||
{
|
||||
{ "x-api-key", options.ApiKey },
|
||||
{ "anthropic-version", "2023-06-01" }
|
||||
};
|
||||
|
||||
bool isThink = input.Model.EndsWith("-thinking");
|
||||
input.Model = input.Model.Replace("-thinking", string.Empty);
|
||||
|
||||
var budgetTokens = 1024;
|
||||
if (input.MaxTokens is < 2048)
|
||||
{
|
||||
input.MaxTokens = 2048;
|
||||
}
|
||||
|
||||
if (input.MaxTokens != null && input.MaxTokens / 2 < 1024)
|
||||
{
|
||||
budgetTokens = input.MaxTokens.Value / (4 * 3);
|
||||
}
|
||||
|
||||
object tool_choice;
|
||||
if (input.ToolChoice is not null && input.ToolChoice.Type == "auto")
|
||||
{
|
||||
tool_choice = new
|
||||
{
|
||||
type = "auto",
|
||||
disable_parallel_tool_use = false,
|
||||
};
|
||||
}
|
||||
else if (input.ToolChoice is not null && input.ToolChoice.Type == "any")
|
||||
{
|
||||
tool_choice = new
|
||||
{
|
||||
type = "any",
|
||||
disable_parallel_tool_use = false,
|
||||
};
|
||||
}
|
||||
else if (input.ToolChoice is not null && input.ToolChoice.Type == "tool")
|
||||
{
|
||||
tool_choice = new
|
||||
{
|
||||
type = "tool",
|
||||
name = input.ToolChoice.Function?.Name,
|
||||
disable_parallel_tool_use = false,
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
tool_choice = null;
|
||||
}
|
||||
|
||||
// budgetTokens最大4096
|
||||
budgetTokens = Math.Min(budgetTokens, 4096);
|
||||
|
||||
var response = await client.PostJsonAsync(options.Endpoint.TrimEnd('/') + "/v1/messages", new
|
||||
{
|
||||
model = input.Model,
|
||||
max_tokens = input.MaxTokens ?? 2000,
|
||||
system = CreateMessage(input.Messages.Where(x => x.Role == "system").ToList(), options),
|
||||
messages = CreateMessage(input.Messages.Where(x => x.Role != "system").ToList(), options),
|
||||
top_p = isThink ? null : input.TopP,
|
||||
tool_choice,
|
||||
thinking = isThink
|
||||
? new
|
||||
{
|
||||
type = "enabled",
|
||||
budget_tokens = budgetTokens,
|
||||
}
|
||||
: null,
|
||||
tools = input.Tools?.Select(x => new
|
||||
{
|
||||
name = x.Function?.Name,
|
||||
description = x.Function?.Description,
|
||||
input_schema = new
|
||||
{
|
||||
type = x.Function?.Parameters?.Type,
|
||||
required = x.Function?.Parameters?.Required,
|
||||
properties = x.Function?.Parameters?.Properties?.ToDictionary(y => y.Key, y => new
|
||||
{
|
||||
description = y.Value.Description,
|
||||
type = y.Value.Type,
|
||||
@enum = y.Value.Enum
|
||||
})
|
||||
}
|
||||
}).ToArray(),
|
||||
temperature = isThink ? null : input.Temperature
|
||||
}, string.Empty, headers);
|
||||
|
||||
openai?.SetTag("Model", input.Model);
|
||||
openai?.SetTag("Response", response.StatusCode.ToString());
|
||||
|
||||
// 大于等于400的状态码都认为是异常
|
||||
if (response.StatusCode >= HttpStatusCode.BadRequest)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
logger.LogError("OpenAI对话异常 请求地址:{Address}, StatusCode: {StatusCode} Response: {Response}",
|
||||
options.Endpoint,
|
||||
response.StatusCode, error);
|
||||
|
||||
throw new Exception("OpenAI对话异常" + response.StatusCode.ToString());
|
||||
}
|
||||
|
||||
var value =
|
||||
await response.Content.ReadFromJsonAsync<AnthropicChatCompletionDto>(ThorJsonSerializer.DefaultOptions,
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
var thor = new ThorChatCompletionsResponse()
|
||||
{
|
||||
Choices = CreateResponse(value),
|
||||
Model = input.Model,
|
||||
Id = value.id,
|
||||
Usage = new ThorUsageResponse()
|
||||
{
|
||||
CompletionTokens = value.Usage.OutputTokens,
|
||||
PromptTokens = value.Usage.InputTokens
|
||||
}
|
||||
};
|
||||
|
||||
if (value.Usage.CacheReadInputTokens != null)
|
||||
{
|
||||
thor.Usage.PromptTokensDetails ??= new ThorUsageResponsePromptTokensDetails()
|
||||
{
|
||||
CachedTokens = value.Usage.CacheReadInputTokens.Value,
|
||||
};
|
||||
|
||||
if (value.Usage.InputTokens > 0)
|
||||
{
|
||||
thor.Usage.InputTokens = value.Usage.InputTokens;
|
||||
}
|
||||
|
||||
if (value.Usage.OutputTokens > 0)
|
||||
{
|
||||
thor.Usage.CompletionTokens = value.Usage.OutputTokens;
|
||||
thor.Usage.OutputTokens = value.Usage.OutputTokens;
|
||||
}
|
||||
}
|
||||
|
||||
thor.Usage.TotalTokens = thor.Usage.InputTokens + thor.Usage.OutputTokens;
|
||||
thor.SupplementalMultiplier(AnthropicChatCompletionsService.ClaudeMultiplier);
|
||||
return thor;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Entities;
|
||||
|
||||
@@ -13,7 +14,7 @@ public class AiRechargeAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
/// 充值金额
|
||||
/// </summary>
|
||||
public decimal RechargeAmount { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 用户
|
||||
/// </summary>
|
||||
@@ -33,9 +34,14 @@ public class AiRechargeAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
/// 到期时间
|
||||
/// </summary>
|
||||
public DateTime? ExpireDateTime { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 联系方式
|
||||
/// </summary>
|
||||
public string? ContactInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单类型
|
||||
/// </summary>
|
||||
public RechargeTypeEnum RechargeType { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 公告日志
|
||||
/// </summary>
|
||||
[SugarTable("Ai_Announcement")]
|
||||
public class AnnouncementAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
{
|
||||
public AnnouncementAggregateRoot()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标题
|
||||
/// </summary>
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 内容列表(JSON格式存储)
|
||||
/// </summary>
|
||||
[SugarColumn(IsJson = true, IsNullable = false)]
|
||||
public List<string> Content { get; set; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图片url
|
||||
/// </summary>
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 开始时间(系统公告时间、活动开始时间)
|
||||
/// </summary>
|
||||
public DateTime StartTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 活动结束时间
|
||||
/// </summary>
|
||||
public DateTime? EndTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 公告类型(系统、活动)
|
||||
/// </summary>
|
||||
public AnnouncementTypeEnum Type{ get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跳转链接
|
||||
/// </summary>
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌任务记录
|
||||
/// </summary>
|
||||
[SugarTable("Ai_CardFlipTask")]
|
||||
[SugarIndex($"index_{nameof(UserId)}_{nameof(WeekStartDate)}",
|
||||
nameof(UserId), OrderByType.Asc,
|
||||
nameof(WeekStartDate), OrderByType.Desc)]
|
||||
public class CardFlipTaskAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
{
|
||||
public CardFlipTaskAggregateRoot()
|
||||
{
|
||||
}
|
||||
|
||||
public CardFlipTaskAggregateRoot(Guid userId, DateTime weekStartDate)
|
||||
{
|
||||
UserId = userId;
|
||||
WeekStartDate = weekStartDate.Date; // 确保只存储日期部分
|
||||
TotalFlips = 0;
|
||||
FreeFlipsUsed = 0;
|
||||
BonusFlipsUsed = 0;
|
||||
InviteFlipsUsed = 0;
|
||||
IsFirstFlipDone = false;
|
||||
FlippedOrder = new List<int>();
|
||||
WinRecords = new Dictionary<int, long>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户ID
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本周开始日期(每周一)
|
||||
/// </summary>
|
||||
public DateTime WeekStartDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总共已翻牌次数
|
||||
/// </summary>
|
||||
public int TotalFlips { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已使用的免费次数(最多7次)
|
||||
/// </summary>
|
||||
public int FreeFlipsUsed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已使用的赠送次数(已废弃,保持为0)
|
||||
/// </summary>
|
||||
public int BonusFlipsUsed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已使用的邀请解锁次数(最多3次)
|
||||
/// </summary>
|
||||
public int InviteFlipsUsed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已完成首次翻牌(用于判断是否创建任务)
|
||||
/// </summary>
|
||||
public bool IsFirstFlipDone { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 中奖记录(以翻牌顺序为key,例如第1次翻牌中奖则key为1,奖励金额为value)
|
||||
/// </summary>
|
||||
[SugarColumn(IsJson = true, IsNullable = true)]
|
||||
public Dictionary<int, long>? WinRecords { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 500, IsNullable = true)]
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已翻牌的顺序(存储用户实际翻牌的序号列表,如[3,7,1,5]表示依次翻了3号、7号、1号、5号牌)
|
||||
/// </summary>
|
||||
[SugarColumn(IsJson = true, IsNullable = true)]
|
||||
public List<int>? FlippedOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 增加翻牌次数
|
||||
/// </summary>
|
||||
/// <param name="flipType">翻牌类型</param>
|
||||
public void IncrementFlip(FlipType flipType)
|
||||
{
|
||||
TotalFlips++;
|
||||
|
||||
switch (flipType)
|
||||
{
|
||||
case FlipType.Free:
|
||||
FreeFlipsUsed++;
|
||||
break;
|
||||
case FlipType.Bonus:
|
||||
BonusFlipsUsed++;
|
||||
break;
|
||||
case FlipType.Invite:
|
||||
InviteFlipsUsed++;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!IsFirstFlipDone)
|
||||
{
|
||||
IsFirstFlipDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录中奖
|
||||
/// </summary>
|
||||
/// <param name="flipCount">第几次翻牌(1-10)</param>
|
||||
/// <param name="amount">奖励金额</param>
|
||||
public void SetWinReward(int flipCount, long amount)
|
||||
{
|
||||
if (WinRecords == null)
|
||||
{
|
||||
WinRecords = new Dictionary<int, long>();
|
||||
}
|
||||
WinRecords[flipCount] = amount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌类型枚举
|
||||
/// </summary>
|
||||
public enum FlipType
|
||||
{
|
||||
/// <summary>
|
||||
/// 免费翻牌(1-7次)
|
||||
/// </summary>
|
||||
Free = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 赠送翻牌(已废弃)
|
||||
/// </summary>
|
||||
Bonus = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 邀请解锁翻牌(8-10次)
|
||||
/// </summary>
|
||||
Invite = 2
|
||||
}
|
||||
@@ -24,7 +24,8 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
{
|
||||
UserId = userId;
|
||||
SessionId = sessionId;
|
||||
Content = content;
|
||||
//如果没有会话,不存储对话内容
|
||||
Content = sessionId is null ? null : content;
|
||||
Role = role;
|
||||
ModelId = modelId;
|
||||
if (tokenUsage is not null)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 每日任务奖励领取记录
|
||||
/// </summary>
|
||||
[SugarTable("Ai_DailyTaskRewardRecord")]
|
||||
[SugarIndex($"index_{nameof(UserId)}_{nameof(TaskDate)}",
|
||||
nameof(UserId), OrderByType.Asc,
|
||||
nameof(TaskDate), OrderByType.Desc)]
|
||||
public class DailyTaskRewardRecordAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
{
|
||||
public DailyTaskRewardRecordAggregateRoot()
|
||||
{
|
||||
}
|
||||
|
||||
public DailyTaskRewardRecordAggregateRoot(Guid userId, int taskLevel, DateTime taskDate, long rewardTokens)
|
||||
{
|
||||
UserId = userId;
|
||||
TaskLevel = taskLevel;
|
||||
TaskDate = taskDate.Date; // 确保只存储日期部分
|
||||
RewardTokens = rewardTokens;
|
||||
IsRewarded = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户ID
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务等级(1=1000w任务,2=3000w任务)
|
||||
/// </summary>
|
||||
public int TaskLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 任务日期(只包含日期,不包含时间)
|
||||
/// </summary>
|
||||
public DateTime TaskDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 奖励的Token数量
|
||||
/// </summary>
|
||||
public long RewardTokens { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否已发放奖励
|
||||
/// </summary>
|
||||
public bool IsRewarded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息
|
||||
/// </summary>
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 邀请记录
|
||||
/// </summary>
|
||||
[SugarTable("Ai_InvitationRecord")]
|
||||
[SugarIndex($"index_{nameof(InviterId)}_{nameof(InvitedUserId)}",
|
||||
nameof(InviterId), OrderByType.Asc,
|
||||
nameof(InvitedUserId), OrderByType.Asc)]
|
||||
[SugarIndex($"index_{nameof(InvitedUserId)}", nameof(InvitedUserId), OrderByType.Asc)]
|
||||
public class InvitationRecordAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
{
|
||||
public InvitationRecordAggregateRoot()
|
||||
{
|
||||
}
|
||||
|
||||
public InvitationRecordAggregateRoot(Guid inviterId, Guid invitedUserId, string inviteCode)
|
||||
{
|
||||
InviterId = inviterId;
|
||||
InvitedUserId = invitedUserId;
|
||||
InviteCode = inviteCode;
|
||||
InvitationTime = DateTime.Now;
|
||||
Status = InvitationStatus.Valid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 邀请人ID
|
||||
/// </summary>
|
||||
public Guid InviterId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 被邀请人ID
|
||||
/// </summary>
|
||||
public Guid InvitedUserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 使用的邀请码
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 50)]
|
||||
public string InviteCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 邀请时间
|
||||
/// </summary>
|
||||
public DateTime InvitationTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 邀请状态(0=有效,1=已撤销)
|
||||
/// </summary>
|
||||
public InvitationStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 500, IsNullable = true)]
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 邀请状态枚举
|
||||
/// </summary>
|
||||
public enum InvitationStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 有效
|
||||
/// </summary>
|
||||
Valid = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 已撤销
|
||||
/// </summary>
|
||||
Revoked = 1
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 用户邀请码
|
||||
/// </summary>
|
||||
[SugarTable("Ai_InviteCode")]
|
||||
[SugarIndex($"index_{nameof(UserId)}", nameof(UserId), OrderByType.Asc, true)]
|
||||
[SugarIndex($"index_{nameof(Code)}", nameof(Code), OrderByType.Asc, true)]
|
||||
public class InviteCodeAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
{
|
||||
public InviteCodeAggregateRoot()
|
||||
{
|
||||
}
|
||||
|
||||
public InviteCodeAggregateRoot(Guid userId, string code)
|
||||
{
|
||||
UserId = userId;
|
||||
Code = code;
|
||||
UsedCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 用户ID(邀请码拥有者)
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 邀请码(唯一)
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 50)]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 被使用次数(统计用,一个邀请码可以被多人使用)
|
||||
/// </summary>
|
||||
public int UsedCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 备注信息
|
||||
/// </summary>
|
||||
[SugarColumn(Length = 500, IsNullable = true)]
|
||||
public string? Remark { get; set; }
|
||||
|
||||
}
|
||||
@@ -52,7 +52,12 @@ public class AiModelEntity : Entity<Guid>, IOrderNum, ISoftDelete
|
||||
public string? ExtraInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型类型
|
||||
/// 模型类型(聊天/图片等)
|
||||
/// </summary>
|
||||
public ModelTypeEnum ModelType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 模型Api类型,现支持同一个模型id,多种接口格式
|
||||
/// </summary>
|
||||
public ModelApiTypeEnum ModelApiType { get; set; }
|
||||
}
|
||||
@@ -76,23 +76,9 @@ public class PremiumPackageAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
/// <returns>是否消耗成功</returns>
|
||||
public bool ConsumeTokens(long tokenCount)
|
||||
{
|
||||
if (RemainingTokens < tokenCount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ExpireDateTime.HasValue && ExpireDateTime.Value < DateTime.Now)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
RemainingTokens -= tokenCount;
|
||||
UsedTokens += tokenCount;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -100,14 +86,14 @@ public class PremiumPackageAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
/// 检查是否可用
|
||||
/// </summary>
|
||||
/// <returns>是否可用</returns>
|
||||
public bool IsAvailable()
|
||||
public bool IsAvailable(bool isVerifyRemainingToken=true)
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (RemainingTokens <= 0)
|
||||
if (isVerifyRemainingToken&&RemainingTokens <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,11 +12,13 @@ using Volo.Abp.Domain.Services;
|
||||
using Yi.Framework.AiHub.Domain.AiGateWay;
|
||||
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
|
||||
using Yi.Framework.AiHub.Domain.Entities.Model;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.Anthropic;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
using Yi.Framework.Core.Extensions;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
@@ -27,21 +29,24 @@ namespace Yi.Framework.AiHub.Domain.Managers;
|
||||
public class AiGateWayManager : DomainService
|
||||
{
|
||||
private readonly ISqlSugarRepository<AiAppAggregateRoot> _aiAppRepository;
|
||||
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
|
||||
private readonly ILogger<AiGateWayManager> _logger;
|
||||
private readonly AiMessageManager _aiMessageManager;
|
||||
private readonly UsageStatisticsManager _usageStatisticsManager;
|
||||
private readonly ISpecialCompatible _specialCompatible;
|
||||
private PremiumPackageManager? _premiumPackageManager;
|
||||
|
||||
|
||||
public AiGateWayManager(ISqlSugarRepository<AiAppAggregateRoot> aiAppRepository, ILogger<AiGateWayManager> logger,
|
||||
AiMessageManager aiMessageManager, UsageStatisticsManager usageStatisticsManager,
|
||||
ISpecialCompatible specialCompatible)
|
||||
ISpecialCompatible specialCompatible, ISqlSugarRepository<AiModelEntity> aiModelRepository)
|
||||
{
|
||||
_aiAppRepository = aiAppRepository;
|
||||
_logger = logger;
|
||||
_aiMessageManager = aiMessageManager;
|
||||
_usageStatisticsManager = usageStatisticsManager;
|
||||
_specialCompatible = specialCompatible;
|
||||
_aiModelRepository = aiModelRepository;
|
||||
}
|
||||
|
||||
private PremiumPackageManager PremiumPackageManager =>
|
||||
@@ -50,17 +55,17 @@ public class AiGateWayManager : DomainService
|
||||
/// <summary>
|
||||
/// 获取模型
|
||||
/// </summary>
|
||||
/// <param name="modelApiType"></param>
|
||||
/// <param name="modelId"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<AiModelDescribe> GetModelAsync(string modelId)
|
||||
private async Task<AiModelDescribe> GetModelAsync(ModelApiTypeEnum modelApiType, string modelId)
|
||||
{
|
||||
var allApp = await _aiAppRepository._DbQueryable.Includes(x => x.AiModels).ToListAsync();
|
||||
foreach (var app in allApp)
|
||||
{
|
||||
var model = app.AiModels.FirstOrDefault(x => x.ModelId == modelId);
|
||||
if (model is not null)
|
||||
{
|
||||
return new AiModelDescribe
|
||||
var aiModelDescribe = await _aiModelRepository._DbQueryable
|
||||
.LeftJoin<AiAppAggregateRoot>((model, app) => model.AiAppId == app.Id)
|
||||
.Where((model, app) => model.ModelId == modelId)
|
||||
.Where((model, app) => model.ModelApiType == modelApiType)
|
||||
.Select((model, app) =>
|
||||
new AiModelDescribe
|
||||
{
|
||||
AppId = app.Id,
|
||||
AppName = app.Name,
|
||||
@@ -73,11 +78,14 @@ public class AiGateWayManager : DomainService
|
||||
Description = model.Description,
|
||||
AppExtraUrl = app.ExtraUrl,
|
||||
ModelExtraInfo = model.ExtraInfo
|
||||
};
|
||||
}
|
||||
})
|
||||
.FirstAsync();
|
||||
if (aiModelDescribe is null)
|
||||
{
|
||||
throw new UserFriendlyException($"【{modelId}】模型当前版本【{modelApiType}】格式不支持");
|
||||
}
|
||||
|
||||
throw new UserFriendlyException($"{modelId}模型当前版本不支持");
|
||||
return aiModelDescribe;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +100,7 @@ public class AiGateWayManager : DomainService
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
_specialCompatible.Compatible(request);
|
||||
var modelDescribe = await GetModelAsync(request.Model);
|
||||
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, request.Model);
|
||||
var chatService =
|
||||
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
|
||||
|
||||
@@ -122,7 +130,7 @@ public class AiGateWayManager : DomainService
|
||||
var response = httpContext.Response;
|
||||
// 设置响应头,声明是 json
|
||||
//response.ContentType = "application/json; charset=UTF-8";
|
||||
var modelDescribe = await GetModelAsync(request.Model);
|
||||
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, request.Model);
|
||||
var chatService =
|
||||
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
|
||||
var data = await chatService.CompleteChatAsync(modelDescribe, request, cancellationToken);
|
||||
@@ -131,7 +139,7 @@ public class AiGateWayManager : DomainService
|
||||
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
|
||||
new MessageInputDto
|
||||
{
|
||||
Content = request.Messages?.LastOrDefault().Content ?? string.Empty,
|
||||
Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault().Content ?? string.Empty,
|
||||
ModelId = request.Model,
|
||||
TokenUsage = data.Usage,
|
||||
});
|
||||
@@ -139,12 +147,23 @@ public class AiGateWayManager : DomainService
|
||||
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
|
||||
new MessageInputDto
|
||||
{
|
||||
Content = data.Choices?.FirstOrDefault()?.Delta.Content,
|
||||
Content =
|
||||
sessionId is null ? "不予存储" : data.Choices?.FirstOrDefault()?.Delta.Content ?? string.Empty,
|
||||
ModelId = request.Model,
|
||||
TokenUsage = data.Usage
|
||||
});
|
||||
|
||||
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage);
|
||||
|
||||
// 扣减尊享token包用量
|
||||
if (PremiumPackageConst.ModeIds.Contains(request.Model))
|
||||
{
|
||||
var totalTokens = data.Usage?.TotalTokens ?? 0;
|
||||
if (totalTokens > 0)
|
||||
{
|
||||
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await response.WriteAsJsonAsync(data, cancellationToken);
|
||||
@@ -216,7 +235,7 @@ public class AiGateWayManager : DomainService
|
||||
{
|
||||
await foreach (var data in completeChatResponse)
|
||||
{
|
||||
if (data.Usage is not null)
|
||||
if (data.Usage is not null && (data.Usage.CompletionTokens > 0 || data.Usage.OutputTokens > 0))
|
||||
{
|
||||
tokenUsage = data.Usage;
|
||||
}
|
||||
@@ -263,7 +282,7 @@ public class AiGateWayManager : DomainService
|
||||
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
|
||||
new MessageInputDto
|
||||
{
|
||||
Content = request.Messages?.LastOrDefault()?.Content ?? string.Empty,
|
||||
Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault()?.Content ?? string.Empty,
|
||||
ModelId = request.Model,
|
||||
TokenUsage = tokenUsage,
|
||||
});
|
||||
@@ -271,12 +290,22 @@ public class AiGateWayManager : DomainService
|
||||
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
|
||||
new MessageInputDto
|
||||
{
|
||||
Content = backupSystemContent.ToString(),
|
||||
Content = sessionId is null ? "不予存储" : backupSystemContent.ToString(),
|
||||
ModelId = request.Model,
|
||||
TokenUsage = tokenUsage
|
||||
});
|
||||
|
||||
await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage);
|
||||
|
||||
// 扣减尊享token包用量
|
||||
if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model))
|
||||
{
|
||||
var totalTokens = tokenUsage.TotalTokens ?? 0;
|
||||
if (totalTokens > 0)
|
||||
{
|
||||
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -297,7 +326,7 @@ public class AiGateWayManager : DomainService
|
||||
var model = request.Model;
|
||||
if (string.IsNullOrEmpty(model)) model = "dall-e-2";
|
||||
|
||||
var modelDescribe = await GetModelAsync(model);
|
||||
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, model);
|
||||
|
||||
// 获取渠道指定的实现类型的服务
|
||||
var imageService =
|
||||
@@ -315,7 +344,7 @@ public class AiGateWayManager : DomainService
|
||||
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
|
||||
new MessageInputDto
|
||||
{
|
||||
Content = request.Prompt,
|
||||
Content = sessionId is null ? "不予存储" : request.Prompt,
|
||||
ModelId = model,
|
||||
TokenUsage = response.Usage,
|
||||
});
|
||||
@@ -323,12 +352,22 @@ public class AiGateWayManager : DomainService
|
||||
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
|
||||
new MessageInputDto
|
||||
{
|
||||
Content = response.Results?.FirstOrDefault()?.Url,
|
||||
Content = sessionId is null ? "不予存储" : response.Results?.FirstOrDefault()?.Url,
|
||||
ModelId = model,
|
||||
TokenUsage = response.Usage
|
||||
});
|
||||
|
||||
await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage);
|
||||
|
||||
// 扣减尊享token包用量
|
||||
if (userId is not null && PremiumPackageConst.ModeIds.Contains(request.Model))
|
||||
{
|
||||
var totalTokens = response.Usage.TotalTokens ?? 0;
|
||||
if (totalTokens > 0)
|
||||
{
|
||||
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -357,7 +396,7 @@ public class AiGateWayManager : DomainService
|
||||
using var embedding =
|
||||
Activity.Current?.Source.StartActivity("向量模型调用");
|
||||
|
||||
var modelDescribe = await GetModelAsync(input.Model);
|
||||
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.OpenAi, input.Model);
|
||||
|
||||
// 获取渠道指定的实现类型的服务
|
||||
var embeddingService =
|
||||
@@ -461,7 +500,7 @@ public class AiGateWayManager : DomainService
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||
{
|
||||
_specialCompatible.AnthropicCompatible(request);
|
||||
var modelDescribe = await GetModelAsync(request.Model);
|
||||
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Claude, request.Model);
|
||||
var chatService =
|
||||
LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName);
|
||||
|
||||
@@ -491,7 +530,7 @@ public class AiGateWayManager : DomainService
|
||||
var response = httpContext.Response;
|
||||
// 设置响应头,声明是 json
|
||||
//response.ContentType = "application/json; charset=UTF-8";
|
||||
var modelDescribe = await GetModelAsync(request.Model);
|
||||
var modelDescribe = await GetModelAsync(ModelApiTypeEnum.Claude, request.Model);
|
||||
var chatService =
|
||||
LazyServiceProvider.GetRequiredKeyedService<IAnthropicChatCompletionService>(modelDescribe.HandlerName);
|
||||
var data = await chatService.ChatCompletionsAsync(modelDescribe, request, cancellationToken);
|
||||
@@ -500,7 +539,7 @@ public class AiGateWayManager : DomainService
|
||||
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
|
||||
new MessageInputDto
|
||||
{
|
||||
Content = request.Messages?.FirstOrDefault()?.Content ?? string.Empty,
|
||||
Content = sessionId is null ? "不予存储" : request.Messages?.FirstOrDefault()?.Content ?? string.Empty,
|
||||
ModelId = request.Model,
|
||||
TokenUsage = data.TokenUsage,
|
||||
});
|
||||
@@ -508,7 +547,7 @@ public class AiGateWayManager : DomainService
|
||||
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
|
||||
new MessageInputDto
|
||||
{
|
||||
Content = data.content?.FirstOrDefault()?.text,
|
||||
Content = sessionId is null ? "不予存储" : data.content?.FirstOrDefault()?.text,
|
||||
ModelId = request.Model,
|
||||
TokenUsage = data.TokenUsage
|
||||
});
|
||||
@@ -516,14 +555,10 @@ public class AiGateWayManager : DomainService
|
||||
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.TokenUsage);
|
||||
|
||||
// 扣减尊享token包用量
|
||||
var totalTokens = (data.TokenUsage?.InputTokens ?? 0) + (data.TokenUsage?.OutputTokens ?? 0);
|
||||
var totalTokens = data.TokenUsage.TotalTokens ?? 0;
|
||||
if (totalTokens > 0)
|
||||
{
|
||||
var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens);
|
||||
if (!consumeSuccess)
|
||||
{
|
||||
_logger.LogWarning($"用户 {userId.Value} 尊享token包扣减失败,消耗token数: {totalTokens}");
|
||||
}
|
||||
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,10 +597,11 @@ public class AiGateWayManager : DomainService
|
||||
await foreach (var responseResult in completeChatResponse)
|
||||
{
|
||||
//message_start是为了保底机制
|
||||
if (responseResult.Item1.Contains("message_delta")||responseResult.Item1.Contains("message_start"))
|
||||
if (responseResult.Item1.Contains("message_delta") || responseResult.Item1.Contains("message_start"))
|
||||
{
|
||||
tokenUsage = responseResult.Item2?.TokenUsage;
|
||||
}
|
||||
|
||||
backupSystemContent.Append(responseResult.Item2?.Delta?.Text);
|
||||
await WriteAsEventStreamDataAsync(httpContext, responseResult.Item1, responseResult.Item2,
|
||||
cancellationToken);
|
||||
@@ -576,35 +612,12 @@ public class AiGateWayManager : DomainService
|
||||
_logger.LogError(e, $"Ai对话异常");
|
||||
var errorContent = $"对话Ai异常,异常信息:\n当前Ai模型:{request.Model}\n异常信息:{e.Message}\n异常堆栈:{e}";
|
||||
throw new UserFriendlyException(errorContent);
|
||||
// var model = new AnthropicStreamDto
|
||||
// {
|
||||
// Message = new AnthropicChatCompletionDto
|
||||
// {
|
||||
// content =
|
||||
// [
|
||||
// new AnthropicChatCompletionDtoContent
|
||||
// {
|
||||
// text = errorContent,
|
||||
// }
|
||||
// ],
|
||||
// },
|
||||
// Error = new AnthropicStreamErrorDto
|
||||
// {
|
||||
// Type = null,
|
||||
// Message = errorContent
|
||||
// }
|
||||
// };
|
||||
// var message = JsonConvert.SerializeObject(model, new JsonSerializerSettings
|
||||
// {
|
||||
// ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
// });
|
||||
// await response.WriteAsJsonAsync(message, ThorJsonSerializer.DefaultOptions);
|
||||
}
|
||||
|
||||
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
|
||||
new MessageInputDto
|
||||
{
|
||||
Content = request.Messages?.LastOrDefault()?.Content ?? string.Empty,
|
||||
Content = sessionId is null ? "不予存储" : request.Messages?.LastOrDefault()?.Content ?? string.Empty,
|
||||
ModelId = request.Model,
|
||||
TokenUsage = tokenUsage,
|
||||
});
|
||||
@@ -612,7 +625,7 @@ public class AiGateWayManager : DomainService
|
||||
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
|
||||
new MessageInputDto
|
||||
{
|
||||
Content = backupSystemContent.ToString(),
|
||||
Content = sessionId is null ? "不予存储" : backupSystemContent.ToString(),
|
||||
ModelId = request.Model,
|
||||
TokenUsage = tokenUsage
|
||||
});
|
||||
@@ -622,14 +635,10 @@ public class AiGateWayManager : DomainService
|
||||
// 扣减尊享token包用量
|
||||
if (userId.HasValue && tokenUsage is not null)
|
||||
{
|
||||
var totalTokens = tokenUsage.TotalTokens??0;
|
||||
if (totalTokens > 0)
|
||||
var totalTokens = tokenUsage.TotalTokens ?? 0;
|
||||
if (tokenUsage.TotalTokens > 0)
|
||||
{
|
||||
var consumeSuccess = await PremiumPackageManager.ConsumeTokensAsync(userId.Value, totalTokens);
|
||||
if (!consumeSuccess)
|
||||
{
|
||||
_logger.LogWarning($"用户 {userId.Value} 尊享token包扣减失败,消耗token数: {totalTokens}");
|
||||
}
|
||||
await PremiumPackageManager.TryConsumeTokensAsync(userId.Value, totalTokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Volo.Abp.Domain.Services;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||
@@ -28,7 +29,7 @@ public class AiRechargeManager : DomainService
|
||||
var currentTime = DateTime.Now;
|
||||
|
||||
// 查找所有充值记录,按用户分组
|
||||
var allRecharges = await _rechargeRepository._DbQueryable
|
||||
var allRecharges = await _rechargeRepository._DbQueryable.Where(x => x.RechargeType == RechargeTypeEnum.Vip)
|
||||
.ToListAsync();
|
||||
|
||||
if (!allRecharges.Any())
|
||||
@@ -48,7 +49,7 @@ public class AiRechargeManager : DomainService
|
||||
|
||||
// 找到用户最大的过期时间
|
||||
var maxExpireTime = group.Max(x => x.ExpireDateTime);
|
||||
|
||||
|
||||
// 如果最大过期时间小于当前时间,说明用户已过期(比较日期,满足用户最后一天)
|
||||
return maxExpireTime.HasValue && maxExpireTime.Value.Date < currentTime.Date;
|
||||
})
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Volo.Abp.Domain.Services;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌管理器 - 负责翻牌核心业务逻辑
|
||||
/// </summary>
|
||||
public class CardFlipManager : DomainService
|
||||
{
|
||||
private readonly ISqlSugarRepository<CardFlipTaskAggregateRoot> _cardFlipTaskRepository;
|
||||
private readonly ISqlSugarRepository<InvitationRecordAggregateRoot> _invitationRecordRepository;
|
||||
private readonly InviteCodeManager _inviteCodeManager;
|
||||
private readonly ILogger<CardFlipManager> _logger;
|
||||
|
||||
// 翻牌规则配置
|
||||
public const int MAX_FREE_FLIPS = 7; // 免费翻牌次数
|
||||
public const int MAX_INVITE_FLIPS = 3; // 邀请解锁翻牌次数
|
||||
public const int TOTAL_MAX_FLIPS = 10; // 总最大翻牌次数
|
||||
|
||||
private const int EIGHTH_FLIP = 8; // 第8次翻牌
|
||||
private const int NINTH_FLIP = 9; // 第9次翻牌
|
||||
private const int TENTH_FLIP = 10; // 第10次翻牌
|
||||
|
||||
// 前7次免费翻牌奖励配置
|
||||
private const long FREE_MIN_REWARD = 10000; // 前7次最小奖励 1w
|
||||
private const long FREE_MAX_REWARD = 30000; // 前7次最大奖励 3w
|
||||
private const double FREE_WIN_RATE = 0.5; // 前7次中奖概率 50%
|
||||
|
||||
private const long EIGHTH_MIN_REWARD = 1000000; // 第8次最小奖励 100w
|
||||
private const long EIGHTH_MAX_REWARD = 3000000; // 第8次最大奖励 300w
|
||||
private const long NINTH_MIN_REWARD = 1000000; // 第9次最小奖励 100w
|
||||
private const long NINTH_MAX_REWARD = 5000000; // 第9次最大奖励 500w
|
||||
private const long TENTH_MIN_REWARD = 1000000; // 第10次最小奖励 100w
|
||||
private const long TENTH_MAX_REWARD = 10000000; // 第10次最大奖励 1000w
|
||||
|
||||
public CardFlipManager(
|
||||
ISqlSugarRepository<CardFlipTaskAggregateRoot> cardFlipTaskRepository,
|
||||
ISqlSugarRepository<InvitationRecordAggregateRoot> invitationRecordRepository,
|
||||
InviteCodeManager inviteCodeManager,
|
||||
ILogger<CardFlipManager> logger)
|
||||
{
|
||||
_cardFlipTaskRepository = cardFlipTaskRepository;
|
||||
_invitationRecordRepository = invitationRecordRepository;
|
||||
_inviteCodeManager = inviteCodeManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建本周任务
|
||||
/// </summary>
|
||||
public async Task<CardFlipTaskAggregateRoot?> GetOrCreateWeeklyTaskAsync(
|
||||
Guid userId,
|
||||
DateTime weekStart,
|
||||
bool createIfNotExists)
|
||||
{
|
||||
var task = await _cardFlipTaskRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && x.WeekStartDate == weekStart)
|
||||
.FirstAsync();
|
||||
|
||||
if (task == null && createIfNotExists)
|
||||
{
|
||||
task = new CardFlipTaskAggregateRoot(userId, weekStart);
|
||||
await _cardFlipTaskRepository.InsertAsync(task);
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取已翻牌的顺序列表
|
||||
/// </summary>
|
||||
public List<int> GetFlippedOrder(CardFlipTaskAggregateRoot task)
|
||||
{
|
||||
return task.FlippedOrder ?? new List<int>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行翻牌逻辑
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="flipNumber">翻牌序号</param>
|
||||
/// <param name="weekStart">本周开始日期</param>
|
||||
/// <returns>翻牌结果</returns>
|
||||
public async Task<FlipResult> ExecuteFlipAsync(Guid userId, int flipNumber, DateTime weekStart)
|
||||
{
|
||||
// 验证翻牌序号
|
||||
if (flipNumber < 1 || flipNumber > TOTAL_MAX_FLIPS)
|
||||
{
|
||||
throw new UserFriendlyException($"翻牌序号必须在1-{TOTAL_MAX_FLIPS}之间");
|
||||
}
|
||||
|
||||
// 获取或创建本周任务
|
||||
var task = await GetOrCreateWeeklyTaskAsync(userId, weekStart, createIfNotExists: true);
|
||||
|
||||
// 验证翻牌次数
|
||||
if (task.TotalFlips >= TOTAL_MAX_FLIPS)
|
||||
{
|
||||
throw new UserFriendlyException("本周翻牌次数已用完,请下周再来!");
|
||||
}
|
||||
|
||||
// 验证该牌是否已经翻过
|
||||
var flippedOrder = GetFlippedOrder(task);
|
||||
if (flippedOrder.Contains(flipNumber))
|
||||
{
|
||||
throw new UserFriendlyException($"第 {flipNumber} 号牌已经翻过了!");
|
||||
}
|
||||
|
||||
// 判断翻牌类型
|
||||
var flipType = DetermineFlipType(task);
|
||||
|
||||
// 验证是否有足够的次数
|
||||
if (!CanUseFlipType(task, flipType))
|
||||
{
|
||||
throw new UserFriendlyException(GetFlipTypeErrorMessage(flipType));
|
||||
}
|
||||
|
||||
// 如果是邀请类型翻牌,必须验证用户本周的邀请记录数量足够(包括填写别人的邀请码和别人填写我的邀请码)
|
||||
if (flipType == FlipType.Invite)
|
||||
{
|
||||
// 查询本周作为邀请人或被邀请人的记录数量(双方都会增加翻牌次数)
|
||||
var weeklyInviteRecordCount = await _invitationRecordRepository._DbQueryable
|
||||
.Where(x => x.InviterId == userId || x.InvitedUserId == userId)
|
||||
.Where(x => x.InvitationTime >= weekStart)
|
||||
.CountAsync();
|
||||
|
||||
// 本周邀请记录数量必须 >= 即将使用的邀请翻牌次数
|
||||
// 例如: 要翻第8次(InviteFlipsUsed=0->1), 需要至少有1条邀请记录(我邀请别人或别人邀请我)
|
||||
// 要翻第9次(InviteFlipsUsed=1->2), 需要至少有2条邀请记录
|
||||
// 要翻第10次(InviteFlipsUsed=2->3), 需要至少有3条邀请记录
|
||||
var requiredInviteRecordCount = task.InviteFlipsUsed + 1;
|
||||
if (weeklyInviteRecordCount < requiredInviteRecordCount)
|
||||
{
|
||||
throw new UserFriendlyException($"需本周累积{requiredInviteRecordCount}次邀请记录(填写别人的邀请码或别人填写你的邀请码)才能解锁第{task.TotalFlips + 1}次翻牌");
|
||||
}
|
||||
}
|
||||
|
||||
// 计算翻牌结果(基于当前是第几次翻牌,而不是卡片序号)
|
||||
var flipCount = task.TotalFlips + 1; // 当前这次翻牌是第几次
|
||||
var result = CalculateFlipResult(flipCount);
|
||||
|
||||
// 将卡片序号信息也返回
|
||||
result.FlipNumber = flipNumber;
|
||||
|
||||
// 更新翻牌次数(必须在记录奖励之前,因为需要先确定是第几次)
|
||||
task.IncrementFlip(flipType);
|
||||
|
||||
// 记录翻牌顺序
|
||||
if (task.FlippedOrder == null)
|
||||
{
|
||||
task.FlippedOrder = new List<int>();
|
||||
}
|
||||
task.FlippedOrder.Add(flipNumber);
|
||||
|
||||
// 如果中奖,记录奖励金额(用于后续查询显示)
|
||||
if (result.IsWin)
|
||||
{
|
||||
task.SetWinReward(flipCount, result.RewardAmount);
|
||||
}
|
||||
|
||||
await _cardFlipTaskRepository.UpdateAsync(task);
|
||||
|
||||
_logger.LogInformation($"用户 {userId} 完成第 {flipNumber} 次翻牌,中奖:{result.IsWin}");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否可以翻牌
|
||||
/// </summary>
|
||||
public bool CanFlipCard(CardFlipTaskAggregateRoot? task)
|
||||
{
|
||||
if (task == null) return true; // 没有任务记录,可以开始翻牌
|
||||
|
||||
return task.TotalFlips < TOTAL_MAX_FLIPS;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断翻牌类型
|
||||
/// </summary>
|
||||
public FlipType DetermineFlipType(CardFlipTaskAggregateRoot task)
|
||||
{
|
||||
if (task.FreeFlipsUsed < MAX_FREE_FLIPS)
|
||||
{
|
||||
return FlipType.Free;
|
||||
}
|
||||
else
|
||||
{
|
||||
return FlipType.Invite;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否可以使用该翻牌类型
|
||||
/// </summary>
|
||||
public bool CanUseFlipType(CardFlipTaskAggregateRoot task, FlipType flipType)
|
||||
{
|
||||
return flipType switch
|
||||
{
|
||||
FlipType.Free => task.FreeFlipsUsed < MAX_FREE_FLIPS,
|
||||
FlipType.Invite => task.InviteFlipsUsed < MAX_INVITE_FLIPS,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算翻牌结果
|
||||
/// </summary>
|
||||
/// <param name="flipCount">第几次翻牌(1-10)</param>
|
||||
private FlipResult CalculateFlipResult(int flipCount)
|
||||
{
|
||||
var result = new FlipResult
|
||||
{
|
||||
FlipNumber = 0, // 稍后会被设置为实际的卡片序号
|
||||
IsWin = false
|
||||
};
|
||||
|
||||
var random = new Random();
|
||||
|
||||
// 前7次: 50%概率中奖,奖励1w-3w
|
||||
if (flipCount <= 7)
|
||||
{
|
||||
// 50%概率中奖
|
||||
if (random.NextDouble() < FREE_WIN_RATE)
|
||||
{
|
||||
var rewardAmount = GenerateRandomReward(FREE_MIN_REWARD, FREE_MAX_REWARD);
|
||||
result.IsWin = true;
|
||||
result.RewardAmount = rewardAmount;
|
||||
result.RewardDesc = $"恭喜获得尊享包 {rewardAmount / 10000m:0.##}w tokens!";
|
||||
}
|
||||
else
|
||||
{
|
||||
result.IsWin = false;
|
||||
result.RewardDesc = "很遗憾,未中奖";
|
||||
}
|
||||
}
|
||||
// 第8次中奖 (邀请码解锁)
|
||||
else if (flipCount == EIGHTH_FLIP)
|
||||
{
|
||||
var rewardAmount = GenerateRandomReward(EIGHTH_MIN_REWARD, EIGHTH_MAX_REWARD);
|
||||
result.IsWin = true;
|
||||
result.RewardAmount = rewardAmount;
|
||||
result.RewardDesc = $"恭喜获得尊享包 {rewardAmount / 10000}w tokens!";
|
||||
}
|
||||
// 第9次中奖 (邀请码解锁)
|
||||
else if (flipCount == NINTH_FLIP)
|
||||
{
|
||||
var rewardAmount = GenerateRandomReward(NINTH_MIN_REWARD, NINTH_MAX_REWARD);
|
||||
result.IsWin = true;
|
||||
result.RewardAmount = rewardAmount;
|
||||
result.RewardDesc = $"恭喜获得尊享包 {rewardAmount / 10000}w tokens!";
|
||||
}
|
||||
// 第10次中奖 (邀请码解锁)
|
||||
else if (flipCount == TENTH_FLIP)
|
||||
{
|
||||
var rewardAmount = GenerateRandomReward(TENTH_MIN_REWARD, TENTH_MAX_REWARD);
|
||||
result.IsWin = true;
|
||||
result.RewardAmount = rewardAmount;
|
||||
result.RewardDesc = $"恭喜获得尊享包 {rewardAmount / 10000}w tokens!";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取翻牌类型错误提示
|
||||
/// </summary>
|
||||
private string GetFlipTypeErrorMessage(FlipType flipType)
|
||||
{
|
||||
return flipType switch
|
||||
{
|
||||
FlipType.Free => "免费翻牌次数已用完",
|
||||
FlipType.Invite => "需要使用邀请码解锁更多次数",
|
||||
_ => "无法翻牌"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成随机奖励金额
|
||||
/// </summary>
|
||||
private long GenerateRandomReward(long min, long max)
|
||||
{
|
||||
var random = new Random();
|
||||
|
||||
// 根据最小值判断单位
|
||||
// 如果min小于100000,则使用1w(10000)作为单位;否则使用100w(1000000)作为单位
|
||||
long unit = min < 100000 ? 10000 : 1000000;
|
||||
|
||||
// 将min和max转换为单位的倍数
|
||||
long minUnits = min / unit;
|
||||
long maxUnits = max / unit;
|
||||
|
||||
// 在倍数范围内随机
|
||||
long randomUnits = random.Next((int)minUnits, (int)maxUnits + 1);
|
||||
|
||||
// 返回单位的倍数
|
||||
return randomUnits * unit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本周开始日期(周一)
|
||||
/// </summary>
|
||||
public static DateTime GetWeekStartDate(DateTime date)
|
||||
{
|
||||
var dayOfWeek = (int)date.DayOfWeek;
|
||||
// 将周日(0)转换为7
|
||||
if (dayOfWeek == 0) dayOfWeek = 7;
|
||||
|
||||
// 计算本周一的日期
|
||||
var monday = date.Date.AddDays(-(dayOfWeek - 1));
|
||||
return monday;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取翻牌类型描述
|
||||
/// </summary>
|
||||
public static string GetFlipTypeDesc(int flipNumber)
|
||||
{
|
||||
if (flipNumber <= MAX_FREE_FLIPS)
|
||||
{
|
||||
return "免费";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "邀请解锁";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 翻牌结果
|
||||
/// </summary>
|
||||
public class FlipResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 翻牌序号
|
||||
/// </summary>
|
||||
public int FlipNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否中奖
|
||||
/// </summary>
|
||||
public bool IsWin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 奖励金额
|
||||
/// </summary>
|
||||
public long RewardAmount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 奖励描述
|
||||
/// </summary>
|
||||
public string RewardDesc { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public class FuwuhaoManager : DomainService
|
||||
private IDistributedCache<AccessTokenResponse> _accessTokenCache;
|
||||
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
|
||||
private readonly ILogger<FuwuhaoManager> _logger;
|
||||
|
||||
public FuwuhaoManager(IOptions<FuwuhaoOptions> options, IHttpClientFactory httpClientFactory,
|
||||
ISqlSugarRepository<AiUserExtraInfoEntity> userRepository,
|
||||
IDistributedCache<AccessTokenResponse> accessTokenCache, ILogger<FuwuhaoManager> logger)
|
||||
@@ -49,6 +50,11 @@ public class FuwuhaoManager : DomainService
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
});
|
||||
if (result is null || string.IsNullOrEmpty(result.AccessToken))
|
||||
{
|
||||
throw new UserFriendlyException("微信服务号AccessToken为空");
|
||||
}
|
||||
|
||||
return result;
|
||||
}, () => new DistributedCacheEntryOptions()
|
||||
{
|
||||
@@ -107,7 +113,7 @@ public class FuwuhaoManager : DomainService
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var jsonContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
|
||||
_logger.LogInformation($"服务号code获取用户基础信息:{jsonContent}");
|
||||
var result = JsonSerializer.Deserialize<UserBaseInfoResponse>(jsonContent);
|
||||
|
||||
@@ -175,7 +181,8 @@ public class FuwuhaoManager : DomainService
|
||||
/// <param name="title">图文消息标题</param>
|
||||
/// <param name="description">图文消息描述</param>
|
||||
/// <returns>XML格式的图文消息体</returns>
|
||||
public string BuildRegisterMessage(string toUser, string title="意社区点击一键注册账号", string description="来自意社区SSO统一注册安全中心")
|
||||
public string BuildRegisterMessage(string toUser, string title = "意社区点击一键注册账号",
|
||||
string description = "来自意社区SSO统一注册安全中心")
|
||||
{
|
||||
var createTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var fromUser = _options.FromUser;
|
||||
@@ -207,7 +214,8 @@ public class FuwuhaoManager : DomainService
|
||||
/// <param name="openId"></param>
|
||||
/// <param name="bindUserId"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<(SceneResultEnum SceneResult,Guid? UserId)> CallBackHandlerAsync(SceneTypeEnum sceneType, string openId, Guid? bindUserId)
|
||||
public async Task<(SceneResultEnum SceneResult, Guid? UserId)> CallBackHandlerAsync(SceneTypeEnum sceneType,
|
||||
string openId, Guid? bindUserId)
|
||||
{
|
||||
var aiUserInfo = await _userRepository._DbQueryable.Where(x => x.FuwuhaoOpenId == openId).FirstAsync();
|
||||
switch (sceneType)
|
||||
@@ -216,12 +224,12 @@ public class FuwuhaoManager : DomainService
|
||||
//有openid,说明登录成功
|
||||
if (aiUserInfo is not null)
|
||||
{
|
||||
return (SceneResultEnum.Login,aiUserInfo.UserId);
|
||||
return (SceneResultEnum.Login, aiUserInfo.UserId);
|
||||
}
|
||||
//无openid,说明需要进行注册
|
||||
else
|
||||
{
|
||||
return (SceneResultEnum.Register,null);
|
||||
return (SceneResultEnum.Register, null);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -240,7 +248,7 @@ public class FuwuhaoManager : DomainService
|
||||
|
||||
//说明没有绑定过,直接绑定
|
||||
await _userRepository.InsertAsync(new AiUserExtraInfoEntity(bindUserId.Value, openId));
|
||||
return (SceneResultEnum.Bind,bindUserId);
|
||||
return (SceneResultEnum.Bind, bindUserId);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(sceneType), sceneType, null);
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Domain.Services;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// 邀请码管理器 - 负责邀请码核心业务逻辑
|
||||
/// </summary>
|
||||
public class InviteCodeManager : DomainService
|
||||
{
|
||||
private readonly ISqlSugarRepository<InviteCodeAggregateRoot> _inviteCodeRepository;
|
||||
private readonly ISqlSugarRepository<InvitationRecordAggregateRoot> _invitationRecordRepository;
|
||||
private readonly ILogger<InviteCodeManager> _logger;
|
||||
|
||||
public InviteCodeManager(
|
||||
ISqlSugarRepository<InviteCodeAggregateRoot> inviteCodeRepository,
|
||||
ISqlSugarRepository<InvitationRecordAggregateRoot> invitationRecordRepository,
|
||||
ILogger<InviteCodeManager> logger)
|
||||
{
|
||||
_inviteCodeRepository = inviteCodeRepository;
|
||||
_invitationRecordRepository = invitationRecordRepository;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成用户的邀请码
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>邀请码</returns>
|
||||
public async Task<string> GenerateInviteCodeForUserAsync(Guid userId)
|
||||
{
|
||||
// 检查是否已有邀请码
|
||||
var existingCode = await _inviteCodeRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId)
|
||||
.FirstAsync();
|
||||
|
||||
if (existingCode != null)
|
||||
{
|
||||
return existingCode.Code;
|
||||
}
|
||||
|
||||
// 生成新邀请码
|
||||
var code = GenerateUniqueInviteCode();
|
||||
var inviteCode = new InviteCodeAggregateRoot(userId, code);
|
||||
await _inviteCodeRepository.InsertAsync(inviteCode);
|
||||
|
||||
_logger.LogInformation($"用户 {userId} 生成邀请码 {code}");
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户的邀请码信息
|
||||
/// </summary>
|
||||
public async Task<InviteCodeAggregateRoot?> GetUserInviteCodeAsync(Guid userId)
|
||||
{
|
||||
return await _inviteCodeRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId)
|
||||
.FirstAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统计用户本周邀请人数(别人填写我的邀请码的次数/或者我填写别人邀请码)
|
||||
/// </summary>
|
||||
public async Task<int> GetWeeklyInvitationCountAsync(Guid userId, DateTime weekStart)
|
||||
{
|
||||
var inviterCount= await _invitationRecordRepository._DbQueryable
|
||||
.Where(x => x.InviterId == userId)
|
||||
.Where(x => x.InvitationTime >= weekStart)
|
||||
.CountAsync();
|
||||
var invitedUserIdCount= await _invitationRecordRepository._DbQueryable
|
||||
.Where(x => x.InvitedUserId == userId)
|
||||
.Where(x => x.InvitationTime >= weekStart)
|
||||
.CountAsync();
|
||||
return inviterCount + invitedUserIdCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取邀请历史记录
|
||||
/// </summary>
|
||||
public async Task<List<InvitationHistoryDto>> GetInvitationHistoryAsync(Guid userId, int limit = 10)
|
||||
{
|
||||
return await _invitationRecordRepository._DbQueryable
|
||||
.Where(x => x.InviterId == userId)
|
||||
.OrderBy(x => x.InvitationTime, OrderByType.Desc)
|
||||
.Take(limit)
|
||||
.Select(x => new InvitationHistoryDto
|
||||
{
|
||||
InvitedUserName = "用户***", // 脱敏处理
|
||||
InvitationTime = x.InvitationTime
|
||||
})
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用邀请码(双方都增加翻牌次数)
|
||||
/// </summary>
|
||||
/// <param name="userId">使用者ID</param>
|
||||
/// <param name="inviteCode">邀请码</param>
|
||||
/// <returns>邀请人ID</returns>
|
||||
public async Task<Guid> UseInviteCodeAsync(Guid userId, string inviteCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(inviteCode))
|
||||
{
|
||||
throw new UserFriendlyException("邀请码不能为空");
|
||||
}
|
||||
|
||||
// 查找邀请码
|
||||
var inviteCodeEntity = await _inviteCodeRepository._DbQueryable
|
||||
.Where(x => x.Code == inviteCode)
|
||||
.FirstAsync();
|
||||
|
||||
if (inviteCodeEntity == null)
|
||||
{
|
||||
throw new UserFriendlyException("邀请码不存在");
|
||||
}
|
||||
|
||||
// 验证不能使用自己的邀请码
|
||||
if (inviteCodeEntity.UserId == userId)
|
||||
{
|
||||
throw new UserFriendlyException("不能使用自己的邀请码");
|
||||
}
|
||||
|
||||
// 检查当前用户是否已经填写过别人的邀请码(一辈子只能填写一次)
|
||||
var hasUsedOthersCode = await IsFilledInviteCodeAsync(userId);
|
||||
|
||||
if (hasUsedOthersCode)
|
||||
{
|
||||
throw new UserFriendlyException("您已经填写过别人的邀请码了,每个账号只能填写一次");
|
||||
}
|
||||
|
||||
// 增加邀请码被使用次数
|
||||
inviteCodeEntity.UsedCount++;
|
||||
await _inviteCodeRepository.UpdateAsync(inviteCodeEntity);
|
||||
|
||||
// 创建邀请记录(双方都会因为这条记录增加一次翻牌机会)
|
||||
var invitationRecord = new InvitationRecordAggregateRoot(
|
||||
inviteCodeEntity.UserId,
|
||||
userId,
|
||||
inviteCode);
|
||||
await _invitationRecordRepository.InsertAsync(invitationRecord);
|
||||
|
||||
_logger.LogInformation($"用户 {userId} 使用邀请码 {inviteCode} 成功,邀请人 {inviteCodeEntity.UserId} 和被邀请人 {userId} 都增加一次翻牌机会");
|
||||
|
||||
return inviteCodeEntity.UserId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查用户是否已填写过邀请码
|
||||
/// </summary>
|
||||
public async Task<bool> IsFilledInviteCodeAsync(Guid userId)
|
||||
{
|
||||
// 检查当前用户是否已经填写过别人的邀请码(一辈子只能填写一次)
|
||||
return await _invitationRecordRepository._DbQueryable
|
||||
.Where(x => x.InvitedUserId == userId)
|
||||
.AnyAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成唯一邀请码
|
||||
/// </summary>
|
||||
private string GenerateUniqueInviteCode()
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
var random = new Random();
|
||||
var code = new char[8];
|
||||
|
||||
for (int i = 0; i < code.Length; i++)
|
||||
{
|
||||
code[i] = chars[random.Next(chars.Length)];
|
||||
}
|
||||
|
||||
return new string(code);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 邀请历史记录DTO
|
||||
/// </summary>
|
||||
public class InvitationHistoryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 被邀请人名称(脱敏)
|
||||
/// </summary>
|
||||
public string InvitedUserName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 邀请时间
|
||||
/// </summary>
|
||||
public DateTime InvitationTime { get; set; }
|
||||
}
|
||||
@@ -13,13 +13,14 @@ public class PremiumPackageManager : DomainService
|
||||
{
|
||||
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot, Guid> _premiumPackageRepository;
|
||||
private readonly ILogger<PremiumPackageManager> _logger;
|
||||
|
||||
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _rechargeRepository;
|
||||
public PremiumPackageManager(
|
||||
ISqlSugarRepository<PremiumPackageAggregateRoot, Guid> premiumPackageRepository,
|
||||
ILogger<PremiumPackageManager> logger)
|
||||
ILogger<PremiumPackageManager> logger, ISqlSugarRepository<AiRechargeAggregateRoot> rechargeRepository)
|
||||
{
|
||||
_premiumPackageRepository = premiumPackageRepository;
|
||||
_logger = logger;
|
||||
_rechargeRepository = rechargeRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,6 +58,21 @@ public class PremiumPackageManager : DomainService
|
||||
|
||||
await _premiumPackageRepository.InsertAsync(premiumPackage);
|
||||
|
||||
// 创建充值记录
|
||||
var rechargeRecord = new AiRechargeAggregateRoot
|
||||
{
|
||||
UserId = userId,
|
||||
RechargeAmount = totalAmount,
|
||||
Content = packageName,
|
||||
ExpireDateTime = premiumPackage.ExpireDateTime,
|
||||
Remark = "自助充值",
|
||||
ContactInfo = null,
|
||||
RechargeType = RechargeTypeEnum.PremiumPackage
|
||||
};
|
||||
|
||||
// 保存充值记录到数据库
|
||||
await _rechargeRepository.InsertAsync(rechargeRecord);
|
||||
|
||||
_logger.LogInformation(
|
||||
$"用户 {userId} 购买尊享包成功: {packageName}, Token数量: {tokenAmount}, 金额: {totalAmount}");
|
||||
|
||||
@@ -69,12 +85,12 @@ public class PremiumPackageManager : DomainService
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <param name="tokenCount">需要消耗的Token数量</param>
|
||||
/// <returns>是否消耗成功</returns>
|
||||
public async Task<bool> ConsumeTokensAsync(Guid userId, long tokenCount)
|
||||
public async Task<bool> TryConsumeTokensAsync(Guid userId, long tokenCount)
|
||||
{
|
||||
// 获取用户所有可用的尊享包,按剩余token升序排列(优先消耗快用完的)
|
||||
var availablePackages = await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && x.IsActive && x.RemainingTokens > 0)
|
||||
.OrderBy(x => x.RemainingTokens)
|
||||
.Where(x => x.UserId == userId && x.IsActive)
|
||||
.OrderBy(x => x.CreationTime)
|
||||
.ToListAsync();
|
||||
|
||||
if (!availablePackages.Any())
|
||||
@@ -94,34 +110,12 @@ public class PremiumPackageManager : DomainService
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算总可用Token
|
||||
var totalAvailableTokens = validPackages.Sum(p => p.RemainingTokens);
|
||||
if (totalAvailableTokens < tokenCount)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
$"用户 {userId} 尊享包Token不足,需要: {tokenCount}, 可用: {totalAvailableTokens}");
|
||||
return false;
|
||||
}
|
||||
var firstPackage = validPackages.First();
|
||||
// 直接扣除最早的token包需要消耗的token,允许扣减到负数
|
||||
firstPackage.ConsumeTokens(tokenCount);
|
||||
await _premiumPackageRepository.UpdateAsync(firstPackage);
|
||||
|
||||
// 从可用的包中逐个扣除Token
|
||||
var remainingToConsume = tokenCount;
|
||||
foreach (var package in validPackages)
|
||||
{
|
||||
if (remainingToConsume <= 0)
|
||||
break;
|
||||
|
||||
var toConsume = Math.Min(remainingToConsume, package.RemainingTokens);
|
||||
if (package.ConsumeTokens(toConsume))
|
||||
{
|
||||
await _premiumPackageRepository.UpdateAsync(package);
|
||||
remainingToConsume -= toConsume;
|
||||
|
||||
_logger.LogInformation(
|
||||
$"用户 {userId} 从尊享包 {package.Id} 消耗 {toConsume} tokens, 剩余: {package.RemainingTokens}");
|
||||
}
|
||||
}
|
||||
|
||||
return remainingToConsume == 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -130,55 +124,11 @@ public class PremiumPackageManager : DomainService
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>可用Token总数</returns>
|
||||
public async Task<long> GetAvailableTokensAsync(Guid userId)
|
||||
{
|
||||
var packages = await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId && x.IsActive && x.RemainingTokens > 0)
|
||||
.ToListAsync();
|
||||
|
||||
return packages
|
||||
.Where(p => p.IsAvailable())
|
||||
.Sum(p => p.RemainingTokens);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取用户的所有尊享包
|
||||
/// </summary>
|
||||
/// <param name="userId">用户ID</param>
|
||||
/// <returns>尊享包列表</returns>
|
||||
public async Task<List<PremiumPackageAggregateRoot>> GetUserPremiumPackagesAsync(Guid userId)
|
||||
{
|
||||
return await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.UserId == userId)
|
||||
.OrderByDescending(x => x.CreationTime)
|
||||
.ToListAsync();
|
||||
.Where(x => x.UserId == userId && x.IsActive && x.RemainingTokens > 0)
|
||||
.Where(p => p.IsActive)
|
||||
.Where(p => !p.ExpireDateTime.HasValue || p.ExpireDateTime.Value >= DateTime.Now)
|
||||
.SumAsync(p => p.RemainingTokens);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停用过期的尊享包
|
||||
/// </summary>
|
||||
/// <returns>停用的包数量</returns>
|
||||
public async Task<int> DeactivateExpiredPackagesAsync()
|
||||
{
|
||||
_logger.LogInformation("开始执行尊享包过期自动停用任务");
|
||||
|
||||
var now = DateTime.Now;
|
||||
var expiredPackages = await _premiumPackageRepository._DbQueryable
|
||||
.Where(x => x.IsActive && x.ExpireDateTime.HasValue && x.ExpireDateTime.Value < now)
|
||||
.ToListAsync();
|
||||
|
||||
if (!expiredPackages.Any())
|
||||
{
|
||||
_logger.LogInformation("没有找到过期的尊享包");
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach (var package in expiredPackages)
|
||||
{
|
||||
package.Deactivate();
|
||||
await _premiumPackageRepository.UpdateAsync(package);
|
||||
}
|
||||
|
||||
_logger.LogInformation($"成功停用 {expiredPackages.Count} 个过期的尊享包");
|
||||
return expiredPackages.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,8 @@ namespace Yi.Framework.AiHub.Domain
|
||||
nameof(DeepSeekChatCompletionsService));
|
||||
services.AddKeyedTransient<IChatCompletionService, OpenAiChatCompletionsService>(
|
||||
nameof(OpenAiChatCompletionsService));
|
||||
|
||||
services.AddKeyedTransient<IChatCompletionService, ClaudiaChatCompletionsService>(
|
||||
nameof(ClaudiaChatCompletionsService));
|
||||
#endregion
|
||||
|
||||
#region Anthropic ChatCompletion
|
||||
@@ -127,6 +128,7 @@ namespace Yi.Framework.AiHub.Domain
|
||||
{
|
||||
builder.ConfigureHttpClient(client =>
|
||||
{
|
||||
client.DefaultRequestHeaders.Add("User-Agent","Apifox/1.0.0 (https://apifox.com)");
|
||||
client.Timeout = TimeSpan.FromMinutes(10);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace Yi.Framework.Bbs.Domain.Entities.Integral
|
||||
[SugarTable("SignIn")]
|
||||
|
||||
[SugarIndex($"index_{nameof(CreatorId)}", nameof(CreatorId), OrderByType.Asc)]
|
||||
public class SignInAggregateRoot : AggregateRoot<Guid>, ICreationAuditedObject
|
||||
public class
|
||||
SignInAggregateRoot : AggregateRoot<Guid>, ICreationAuditedObject
|
||||
{
|
||||
|
||||
[SugarColumn(IsPrimaryKey = true)]
|
||||
|
||||
@@ -55,11 +55,11 @@ namespace Yi.Framework.Rbac.Application.Services
|
||||
{
|
||||
return new NotFoundResult();
|
||||
}
|
||||
|
||||
var steam = await File.ReadAllBytesAsync(path);
|
||||
return new FileContentResult(steam, file.GetMimeMapping());
|
||||
|
||||
var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
return new FileStreamResult(stream, file!.GetMimeMapping());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 上传文件
|
||||
/// </summary>
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Yi.Framework.Rbac.Application.Services.System
|
||||
.WhereIF(!string.IsNullOrEmpty(input.DeptName), u => u.DeptName.Contains(input.DeptName!))
|
||||
.WhereIF(input.State is not null, u => u.State == input.State)
|
||||
.OrderBy(u => u.OrderNum, OrderByType.Asc)
|
||||
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
|
||||
.ToListAsync();
|
||||
return new PagedResultDto<DeptGetListOutputDto>
|
||||
{
|
||||
Items = await MapToGetListOutputDtosAsync(entities),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Yi.Framework.Ddd.Application;
|
||||
@@ -54,5 +55,25 @@ namespace Yi.Framework.Rbac.Application.Services.System
|
||||
throw new UserFriendlyException(RoleConst.Exist);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新状态
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="state"></param>
|
||||
/// <returns></returns>
|
||||
[Route("post/{id}/{state}")]
|
||||
public async Task<PostGetOutputDto> UpdateStateAsync([FromRoute] Guid id, [FromRoute] bool state)
|
||||
{
|
||||
var entity = await _repository.GetByIdAsync(id);
|
||||
if (entity is null)
|
||||
{
|
||||
throw new ApplicationException("岗位未存在");
|
||||
}
|
||||
|
||||
entity.State = state;
|
||||
await _repository.UpdateAsync(entity);
|
||||
return await MapToGetOutputDtoAsync(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Yi.Framework.Rbac.Domain.Shared.Caches;
|
||||
namespace Yi.Framework.Rbac.Domain.Shared.Caches;
|
||||
|
||||
public class FileCacheItem
|
||||
{
|
||||
|
||||
@@ -34,8 +34,7 @@ namespace Yi.Framework.Rbac.Domain.Entities
|
||||
var type = GetFileType();
|
||||
|
||||
var savePath = GetSaveFilePath();
|
||||
var filePath = Path.Combine(savePath, this.FileName);
|
||||
this.FilePath = filePath;
|
||||
this.FilePath = savePath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Yi.Abp.Web;
|
||||
|
||||
@@ -29,12 +29,15 @@ using Volo.Abp.Swashbuckle;
|
||||
using Yi.Abp.Application;
|
||||
using Yi.Abp.SqlsugarCore;
|
||||
using Yi.Framework.AiHub.Application;
|
||||
using Yi.Framework.AiHub.Application.Services;
|
||||
using Yi.Framework.AiHub.Domain.Entities;
|
||||
using Yi.Framework.AspNetCore;
|
||||
using Yi.Framework.AspNetCore.Authentication.OAuth;
|
||||
using Yi.Framework.AspNetCore.Authentication.OAuth.Gitee;
|
||||
using Yi.Framework.AspNetCore.Authentication.OAuth.QQ;
|
||||
using Yi.Framework.AspNetCore.Microsoft.AspNetCore.Builder;
|
||||
using Yi.Framework.AspNetCore.Microsoft.Extensions.DependencyInjection;
|
||||
using Yi.Framework.AspNetCore.UnifyResult;
|
||||
using Yi.Framework.BackgroundWorkers.Hangfire;
|
||||
using Yi.Framework.Bbs.Application;
|
||||
using Yi.Framework.Bbs.Application.Extensions;
|
||||
@@ -46,6 +49,7 @@ using Yi.Framework.Rbac.Application;
|
||||
using Yi.Framework.Rbac.Domain.Authorization;
|
||||
using Yi.Framework.Rbac.Domain.Shared.Consts;
|
||||
using Yi.Framework.Rbac.Domain.Shared.Options;
|
||||
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||
using Yi.Framework.Stock.Application;
|
||||
using Yi.Framework.TenantManagement.Application;
|
||||
|
||||
@@ -123,6 +127,7 @@ namespace Yi.Abp.Web
|
||||
});
|
||||
|
||||
//采用furion格式的规范化api,默认不开启,使用abp优雅的方式
|
||||
//前置:需要将管道工作单元前加上app.Properties.Add("_AbpExceptionHandlingMiddleware_Added",false);
|
||||
//你没看错。。。
|
||||
//service.AddFurionUnifyResultApi();
|
||||
|
||||
@@ -350,6 +355,11 @@ namespace Yi.Abp.Web
|
||||
var app = context.GetApplicationBuilder();
|
||||
app.UseRouting();
|
||||
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<AnnouncementAggregateRoot>();
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<CardFlipTaskAggregateRoot>();
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<InviteCodeAggregateRoot>();
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<InvitationRecordAggregateRoot>();
|
||||
|
||||
//跨域
|
||||
app.UseCors(DefaultCorsPolicyName);
|
||||
|
||||
@@ -394,8 +404,7 @@ namespace Yi.Abp.Web
|
||||
app.UseDefaultFiles();
|
||||
app.UseDirectoryBrowser("/api/app/wwwroot");
|
||||
|
||||
|
||||
// app.Properties.Add("_AbpExceptionHandlingMiddleware_Added",false);
|
||||
//app.Properties.Add("_AbpExceptionHandlingMiddleware_Added",false);
|
||||
//工作单元
|
||||
app.UseUnitOfWork();
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "sk-lVcGFMOQfXYtZWp1rD4SaBNDNpM270UX2wDqWh",
|
||||
"ANTHROPIC_BASE_URL": "https://api.token-ai.cn",
|
||||
"ANTHROPIC_MODEL": "gpt-4o-mini"
|
||||
}
|
||||
}
|
||||
9
Yi.Ai.Vue3/.claude/settings.local.json
Normal file
9
Yi.Ai.Vue3/.claude/settings.local.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(npx vue-tsc --noEmit)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,13 @@ pnpm lint:stylelint # 样式格式化
|
||||
pnpm cz # 规范提交(自动执行lint)
|
||||
```
|
||||
|
||||
|
||||
### 服务端启动
|
||||
目录:E:\devDemo\Yi\Yi.Abp.Net8\src\Yi.Abp.Web
|
||||
```bash
|
||||
dotnet run
|
||||
```
|
||||
|
||||
## 🧸 即将推出 (含 接口联调)
|
||||
- [x] 会话管理
|
||||
- [x] 发送消息
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
<link rel="icon" href="/favicon.ico"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<meta name="baidu-site-verification" content="codeva-mkVpSFmYJm"/>
|
||||
<meta name="description" content="意心AI:一站式多模型 AI 平台,提供 GPT-4o、DeepSeek 等服务"/>
|
||||
<meta name="description" content="意心AI:一站式多模型 AI 平台,提供 AI 服务"/>
|
||||
<meta name="description" content="各大主流AI无限制使用,直连,AI,claude ,DeepSeek,open-ai"/>
|
||||
<meta name="keywords" content="意心AI, GPT-4.5, 多模型AI, AI工具"/>
|
||||
<meta name="keywords" content="意心AI, 多模型AI, AI工具"/>
|
||||
<meta name="keywords" content="橙子,chengzi,橙子老哥,ccnetcore,意社区"/>
|
||||
<meta name="author" content="橙子,chengzi,橙子老哥,ccnetcore"/>
|
||||
<meta name="version" content="%VITE_APP_VERSION%"/>
|
||||
@@ -112,7 +112,7 @@
|
||||
<body>
|
||||
<!-- 加载动画容器 -->
|
||||
<div id="yixinai-loader" class="loader-container">
|
||||
<div class="loader-title">意心Ai</div>
|
||||
<div class="loader-title">意心Ai 2.3</div>
|
||||
<div class="loader-subtitle">海外地址,仅首次访问预计加载约10秒</div>
|
||||
<div class="loader-logo">
|
||||
<div class="pulse-box"></div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
12
Yi.Ai.Vue3/src/api/announcement/index.ts
Normal file
12
Yi.Ai.Vue3/src/api/announcement/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { AnnouncementLogDto } from './types';
|
||||
import { get } from '@/utils/request';
|
||||
|
||||
/**
|
||||
* 获取系统公告和活动数据
|
||||
* 后端接口: GET /api/app/announcement
|
||||
* 返回格式: AnnouncementLogDto[]
|
||||
*/
|
||||
export function getSystemAnnouncements() {
|
||||
return get<AnnouncementLogDto[]>('/announcement').json();
|
||||
}
|
||||
export * from './types';
|
||||
18
Yi.Ai.Vue3/src/api/announcement/types.ts
Normal file
18
Yi.Ai.Vue3/src/api/announcement/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// 公告类型(对应后端 AnnouncementTypeEnum)
|
||||
export type AnnouncementType = 'Activity' | 'System'
|
||||
|
||||
// 公告DTO(对应后端 AnnouncementLogDto)
|
||||
export interface AnnouncementLogDto {
|
||||
/** 标题 */
|
||||
title: string
|
||||
/** 内容列表 */
|
||||
content: string[]
|
||||
/** 图片url */
|
||||
imageUrl?: string | null
|
||||
/** 开始时间(系统公告时间、活动开始时间) */
|
||||
startTime: string
|
||||
/** 活动结束时间 */
|
||||
endTime?: string | null
|
||||
/** 公告类型(系统、活动) */
|
||||
type: AnnouncementType
|
||||
}
|
||||
33
Yi.Ai.Vue3/src/api/cardFlip/index.ts
Normal file
33
Yi.Ai.Vue3/src/api/cardFlip/index.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { get, post } from '@/utils/request';
|
||||
import type {
|
||||
CardFlipStatusOutput,
|
||||
FlipCardInput,
|
||||
FlipCardOutput,
|
||||
UseInviteCodeInput,
|
||||
InviteCodeOutput
|
||||
} from './types';
|
||||
|
||||
// 获取本周翻牌任务状态
|
||||
export function getWeeklyTaskStatus() {
|
||||
return get<CardFlipStatusOutput>('/card-flip/weekly-task-status').json();
|
||||
}
|
||||
|
||||
// 翻牌
|
||||
export function flipCard(data: FlipCardInput) {
|
||||
return post<FlipCardOutput>('/card-flip/flip-card', data).json();
|
||||
}
|
||||
|
||||
// 使用邀请码解锁翻牌次数
|
||||
export function useInviteCode(data: UseInviteCodeInput) {
|
||||
return post<void>('/card-flip/use-invite-code', data).json();
|
||||
}
|
||||
|
||||
// 获取我的邀请码信息
|
||||
export function getMyInviteCode() {
|
||||
return get<InviteCodeOutput>('/card-flip/my-invite-code').json();
|
||||
}
|
||||
|
||||
// 生成我的邀请码(如果没有)
|
||||
export function generateMyInviteCode() {
|
||||
return post<string>('/card-flip/generate-my-invite-code').json();
|
||||
}
|
||||
59
Yi.Ai.Vue3/src/api/cardFlip/types.ts
Normal file
59
Yi.Ai.Vue3/src/api/cardFlip/types.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
// 翻牌任务状态输出
|
||||
export interface CardFlipStatusOutput {
|
||||
totalFlips: number; // 本周总翻牌次数
|
||||
remainingFreeFlips: number; // 剩余免费次数
|
||||
remainingBonusFlips: number; // 剩余赠送次数
|
||||
remainingInviteFlips: number; // 剩余邀请解锁次数
|
||||
canFlip: boolean; // 是否可以翻牌
|
||||
myInviteCode?: string; // 用户的邀请码
|
||||
invitedCount: number; // 本周邀请人数
|
||||
// isInvited: boolean; // 是否已被邀请
|
||||
flipRecords: CardFlipRecord[]; // 翻牌记录
|
||||
nextFlipTip?: string; // 下次可翻牌提示
|
||||
isFilledInviteCode: boolean;// 当前用户是否已经填写过邀请码
|
||||
}
|
||||
|
||||
// 翻牌记录
|
||||
export interface CardFlipRecord {
|
||||
flipNumber: number; // 翻牌序号(1-10)
|
||||
isFlipped: boolean; // 是否已翻
|
||||
isWin: boolean; // 是否中奖
|
||||
rewardAmount?: number; // 奖励金额(token数)
|
||||
flipTypeDesc?: string; // 翻牌类型描述
|
||||
flipOrderIndex: number; // 在翻牌顺序中的位置(1-10,表示第几个翻)
|
||||
}
|
||||
|
||||
// 翻牌输入
|
||||
export interface FlipCardInput {
|
||||
flipNumber: number; // 翻牌序号(1-10)
|
||||
}
|
||||
|
||||
// 翻牌输出
|
||||
export interface FlipCardOutput {
|
||||
flipNumber: number; // 翻牌序号(1-10)
|
||||
isWin: boolean; // 是否中奖
|
||||
rewardAmount?: number; // 奖励金额(token数)
|
||||
rewardDesc?: string; // 奖励描述
|
||||
showDoubleRewardTip: boolean; // 是否显示翻倍包提示
|
||||
remainingFlips: number; // 剩余可翻次数
|
||||
}
|
||||
|
||||
// 使用邀请码输入
|
||||
export interface UseInviteCodeInput {
|
||||
inviteCode: string; // 邀请码
|
||||
}
|
||||
|
||||
// 邀请码信息输出
|
||||
export interface InviteCodeOutput {
|
||||
myInviteCode?: string; // 我的邀请码
|
||||
invitedCount: number; // 本周邀请人数
|
||||
isInvited: boolean; // 是否已被邀请
|
||||
invitationHistory: InvitationHistoryItem[]; // 邀请历史记录
|
||||
}
|
||||
|
||||
// 邀请历史记录项
|
||||
export interface InvitationHistoryItem {
|
||||
invitedUserName: string; // 被邀请人昵称(脱敏)
|
||||
invitationTime: string; // 邀请时间
|
||||
weekDescription: string; // 本周所在
|
||||
}
|
||||
12
Yi.Ai.Vue3/src/api/dailyTask/index.ts
Normal file
12
Yi.Ai.Vue3/src/api/dailyTask/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { get, post } from '@/utils/request';
|
||||
import type { DailyTaskStatusOutput, ClaimTaskRewardInput } from './types';
|
||||
|
||||
// 获取今日任务状态
|
||||
export function getTodayTaskStatus() {
|
||||
return get<DailyTaskStatusOutput>('/daily-task/today-task-status').json();
|
||||
}
|
||||
|
||||
// 领取任务奖励
|
||||
export function claimTaskReward(data: ClaimTaskRewardInput) {
|
||||
return post<void>('/daily-task/claim-task-reward', data).json();
|
||||
}
|
||||
21
Yi.Ai.Vue3/src/api/dailyTask/types.ts
Normal file
21
Yi.Ai.Vue3/src/api/dailyTask/types.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// 每日任务状态
|
||||
export interface DailyTaskStatusOutput {
|
||||
todayConsumedTokens: number; // 今日消耗的尊享包Token数
|
||||
tasks: DailyTaskItem[]; // 任务列表
|
||||
}
|
||||
|
||||
// 每日任务项
|
||||
export interface DailyTaskItem {
|
||||
level: number; // 任务等级(1=1000w任务,2=3000w任务)
|
||||
name: string; // 任务名称
|
||||
description: string; // 任务描述
|
||||
requiredTokens: number; // 任务要求的Token消耗量
|
||||
rewardTokens: number; // 奖励的Token数量
|
||||
status: number; // 任务状态:0=未完成,1=可领取,2=已领取
|
||||
progress: number; // 任务进度百分比(0-100)
|
||||
}
|
||||
|
||||
// 领取任务奖励输入
|
||||
export interface ClaimTaskRewardInput {
|
||||
taskLevel: number; // 任务等级(1=1000w任务,2=3000w任务)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './announcement'
|
||||
export * from './auth';
|
||||
export * from './chat';
|
||||
export * from './model';
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
import { get, post } from '@/utils/request.ts';
|
||||
|
||||
// 商品分类类型
|
||||
export enum GoodsCategoryType {
|
||||
Vip = 'Vip',
|
||||
PremiumPackage = 'PremiumPackage',
|
||||
}
|
||||
|
||||
// 商品信息接口
|
||||
export interface GoodsItem {
|
||||
goodsName: string; // 商品名称
|
||||
originalPrice: number; // 原价
|
||||
referencePrice: number; // 参考价格(月均价)
|
||||
goodsPrice: number; // 实际价格
|
||||
discountAmount: number | null; // 折扣金额
|
||||
goodsCategory: string; // 商品分类
|
||||
remark: string | null; // 备注(标签)
|
||||
discountDescription: string | null; // 折扣描述
|
||||
goodsType: string | null; // 折扣描述
|
||||
}
|
||||
|
||||
// 获取商品列表
|
||||
export function getGoodsList(categoryType: GoodsCategoryType) {
|
||||
return get<GoodsItem[]>(`/pay/GoodsList?GoodsCategoryType=${categoryType}`).json();
|
||||
}
|
||||
|
||||
// 创建订单并发起支付
|
||||
export function createOrder(params: any) {
|
||||
return post<any>(`/pay/Order`, params).json();
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
import { get, post } from '@/utils/request';
|
||||
|
||||
// 尊享包用量明细DTO
|
||||
export interface PremiumTokenUsageDto {
|
||||
/** id */
|
||||
id: string;
|
||||
/** 用户ID */
|
||||
userId: string;
|
||||
/** 包名称 */
|
||||
packageName: string;
|
||||
/** 总用量(总token数) */
|
||||
totalTokens: number;
|
||||
/** 剩余用量(剩余token数) */
|
||||
remainingTokens: number;
|
||||
/** 已使用token数 */
|
||||
usedTokens: number;
|
||||
/** 到期时间 */
|
||||
expireDateTime?: string;
|
||||
/** 是否激活 */
|
||||
isActive: boolean;
|
||||
/** 购买金额 */
|
||||
purchaseAmount: number;
|
||||
/** 备注 */
|
||||
remark?: string;
|
||||
/** 创建时间 */
|
||||
creationTime?: string;
|
||||
/** 创建者ID */
|
||||
creatorId?: string;
|
||||
}
|
||||
|
||||
// 查询参数接口 - 匹配后端 PagedAllResultRequestDto
|
||||
export interface PremiumTokenUsageQueryParams {
|
||||
/** 查询开始时间 */
|
||||
startTime?: string;
|
||||
/** 查询结束时间 */
|
||||
endTime?: string;
|
||||
/** 排序列名 */
|
||||
orderByColumn?: string;
|
||||
/** 排序方向(ascending/descending) */
|
||||
isAsc?: string;
|
||||
/** 跳过数量(分页) */
|
||||
skipCount?: number;
|
||||
/** 最大返回数量(分页) */
|
||||
maxResultCount?: number;
|
||||
/** 是否免费 */
|
||||
isFree?: boolean;
|
||||
// 是否为升序排序
|
||||
isAscending?: boolean;
|
||||
}
|
||||
|
||||
// 分页响应接口
|
||||
export interface PagedResult<T> {
|
||||
/** 数据列表 */
|
||||
items: T[];
|
||||
/** 总数量 */
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export function getUserInfo() {
|
||||
return get<any>('/account/ai').json();
|
||||
@@ -19,3 +75,68 @@ export function getQrCodeResult(data: any) {
|
||||
export function getWechatAuth(data: any) {
|
||||
return post<any>('/fuwuhao/register', data).json();
|
||||
}
|
||||
|
||||
// 获取尊享服务Token包额度
|
||||
export function getPremiumTokenPackage() {
|
||||
return get<any>('/usage-statistics/premium-token-usage').json();
|
||||
}
|
||||
|
||||
// 获取尊享包用量明细列表
|
||||
export function getPremiumTokenUsageList(params?: PremiumTokenUsageQueryParams) {
|
||||
return get<PagedResult<PremiumTokenUsageDto>>('/usage-statistics/premium-token-usage/list', params).json();
|
||||
}
|
||||
|
||||
// 查询条件的后端dto,其他查询或者排序由前端自己实现:
|
||||
// using Volo.Abp.Application.Dtos;
|
||||
//
|
||||
// namespace Yi.Framework.Ddd.Application.Contracts
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// 分页查询请求DTO,包含时间范围和自定义排序功能
|
||||
// /// </summary>
|
||||
// public class PagedAllResultRequestDto : PagedAndSortedResultRequestDto, IPagedAllResultRequestDto
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// 查询开始时间
|
||||
// /// </summary>
|
||||
// public DateTime? StartTime { get; set; }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 查询结束时间
|
||||
// /// </summary>
|
||||
// public DateTime? EndTime { get; set; }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 排序列名
|
||||
// /// </summary>
|
||||
// public string? OrderByColumn { get; set; }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 排序方向(ascending/descending)
|
||||
// /// </summary>
|
||||
// public string? IsAsc { get; set; }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 是否为升序排序
|
||||
// /// </summary>
|
||||
// public bool IsAscending => string.Equals(IsAsc, "ascending", StringComparison.OrdinalIgnoreCase);
|
||||
//
|
||||
// private string? _sorting;
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 排序表达式
|
||||
// /// </summary>
|
||||
// public override string? Sorting
|
||||
// {
|
||||
// get
|
||||
// {
|
||||
// if (!string.IsNullOrWhiteSpace(OrderByColumn))
|
||||
// {
|
||||
// return $"{OrderByColumn} {(IsAscending ? "ASC" : "DESC")}";
|
||||
// }
|
||||
// return _sorting;
|
||||
// }
|
||||
// set => _sorting = value;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getQrCode, getQrCodeResult, getUserInfo } from '@/api';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useSessionStore } from '@/stores/modules/session.ts';
|
||||
import { WECHAT_QRCODE_TYPE } from '@/utils/user.ts';
|
||||
import { useGuideTour } from '@/hooks/useGuideTour';
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
@@ -29,6 +30,7 @@ const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const sessionStore = useSessionStore();
|
||||
const isQrCodeError = ref(false);
|
||||
const { startFullTour } = useGuideTour();
|
||||
// 二维码倒计时实例
|
||||
const { start: qrStart, stop: qrStop } = useCountdown(shallowRef(600), {
|
||||
interval: 1000,
|
||||
@@ -126,6 +128,11 @@ async function handleLoginSuccess(token: string, refreshToken: string) {
|
||||
stopPolling();
|
||||
userStore.setToken(token, refreshToken);
|
||||
const resUserInfo = await getUserInfo();
|
||||
|
||||
// 判断是否为新用户(注册时间小于1小时)
|
||||
const creationTime = resUserInfo.data.user.creationTime; // 格式: "2024-11-01 12:01:34"
|
||||
const isNewUser = checkIsNewUser(creationTime);
|
||||
|
||||
userStore.setUserInfo(resUserInfo.data);
|
||||
// 提示用户
|
||||
ElMessage.success('登录成功');
|
||||
@@ -133,6 +140,34 @@ async function handleLoginSuccess(token: string, refreshToken: string) {
|
||||
await router.replace('/');
|
||||
await sessionStore.requestSessionList(1, true);
|
||||
userStore.closeLoginDialog();
|
||||
|
||||
// 如果是新用户,延迟500ms后自动触发新手引导
|
||||
if (isNewUser) {
|
||||
setTimeout(() => {
|
||||
startFullTour();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为新用户(注册时间距离当前时间小于1小时)
|
||||
function checkIsNewUser(creationTimeStr: string): boolean {
|
||||
try {
|
||||
// 解析注册时间字符串 "2024-11-01 12:01:34"
|
||||
const creationTime = new Date(creationTimeStr.replace(' ', 'T'));
|
||||
const currentTime = new Date();
|
||||
|
||||
// 计算时间差(毫秒)
|
||||
const timeDiff = currentTime.getTime() - creationTime.getTime();
|
||||
|
||||
// 1小时 = 60分钟 * 60秒 * 1000毫秒 = 3600000毫秒
|
||||
const oneHourInMs = 60 * 60 * 1000;
|
||||
|
||||
return timeDiff < oneHourInMs;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('解析注册时间失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理注册授权
|
||||
|
||||
@@ -77,7 +77,6 @@ async function onReLogin() {
|
||||
}
|
||||
function handleThirdPartyLogin(type: any) {
|
||||
const redirectUri = encodeURIComponent(`${window.location.origin}/chat`);
|
||||
console.log('cccc', type);
|
||||
const popup = window.open(
|
||||
`${SSO_SEVER_URL}/login?client_id=${type}&redirect_uri=${redirectUri}`,
|
||||
'SSOLogin',
|
||||
@@ -149,7 +148,6 @@ function handleLoginAgainYi() {
|
||||
&& event.data.type === 'SSO_LOGIN_SUCCESS'
|
||||
&& !isHandled) {
|
||||
isHandled = true;
|
||||
console.log('111');
|
||||
try {
|
||||
// 清理监听
|
||||
window.removeEventListener('message', messageHandler);
|
||||
@@ -362,7 +360,20 @@ function openContact() {
|
||||
联系我们
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a
|
||||
href="https://ccnetcore.com/login"
|
||||
target="_blank"
|
||||
class="mt5 flex items-center gap-2 group"
|
||||
style="color: #101d2c;"
|
||||
title="点击跳转YiXinAI玩法指南专栏"
|
||||
>
|
||||
<span class="pc-text">前往 意社区 👉</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loginFormType === 'RegistrationForm'" class="form-container">
|
||||
<span class="content-title"> 登录后免费使用完整功能 </span>
|
||||
|
||||
|
||||
@@ -2,22 +2,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { GetSessionListVO } from '@/api/model/types';
|
||||
import { Lock } from '@element-plus/icons-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import Popover from '@/components/Popover/index.vue';
|
||||
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useModelStore } from '@/stores/modules/model';
|
||||
import { showProductPackage } from '@/utils/product-package.ts';
|
||||
import { isUserVip } from '@/utils/user';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const userStore = useUserStore();
|
||||
const modelStore = useModelStore();
|
||||
// 检查模型是否可用
|
||||
function isModelAvailable(item: GetSessionListVO) {
|
||||
return isUserVip() || item.modelId?.includes('DeepSeek-R1-0528') || userStore.userInfo?.user?.userName === 'cc';
|
||||
return isUserVip() || item.modelId?.includes('DeepSeek-R1-0528');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -57,7 +52,6 @@ async function showPopover() {
|
||||
// 点击
|
||||
// 处理模型点击
|
||||
function handleModelClick(item: GetSessionListVO) {
|
||||
console.log('modelStore.modelList', modelStore.modelList);
|
||||
if (!isModelAvailable(item)) {
|
||||
ElMessageBox.confirm(
|
||||
`
|
||||
@@ -89,11 +83,6 @@ function handleModelClick(item: GetSessionListVO) {
|
||||
)
|
||||
.then(() => {
|
||||
showProductPackage();
|
||||
|
||||
// router.push({
|
||||
// name: 'products', // 使用命名路由
|
||||
// query: { from: isUserVip() ? 'vip' : 'user' }, // 可选:添加来源标识
|
||||
// });
|
||||
})
|
||||
.catch(() => {
|
||||
// 点击右上角关闭或“关闭”按钮,不执行任何操作
|
||||
@@ -103,10 +92,64 @@ function handleModelClick(item: GetSessionListVO) {
|
||||
modelStore.setCurrentModelInfo(item);
|
||||
popoverRef.value?.hide?.();
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
模型样式规则
|
||||
规则1:普通灰色(免费模型)
|
||||
规则2:金色光泽(VIP/付费)
|
||||
规则3:彩色流光(尊享/高级)
|
||||
-------------------------------- */
|
||||
function getModelStyleClass(modelName: any) {
|
||||
if (!modelName) {
|
||||
return;
|
||||
}
|
||||
const name = modelName.toLowerCase();
|
||||
|
||||
// 规则3:彩色流光
|
||||
if (name.includes('claude-sonnet-4-5-20250929')) {
|
||||
return `
|
||||
text-transparent bg-clip-text
|
||||
bg-[linear-gradient(45deg,#ff0000,#ff8000,#ffff00,#00ff00,#00ffff,#0000ff,#8000ff,#ff0080)]
|
||||
bg-[length:400%_400%] animate-gradientFlow
|
||||
`;
|
||||
}
|
||||
|
||||
// 规则2:普通灰
|
||||
if (name.includes('deepseek-r1')) {
|
||||
return 'text-gray-700';
|
||||
}
|
||||
|
||||
// 规则1:金色光泽
|
||||
return `
|
||||
text-[#B38728] font-semibold relative overflow-hidden
|
||||
before:content-[''] before:absolute before:-inset-2 before:-z-10
|
||||
before:animate-goldShine
|
||||
`;
|
||||
// 金色背景
|
||||
// before:bg-[linear-gradient(135deg,#BF953F,#FCF6BA,#B38728,#FBF5B7,#AA771C)]
|
||||
}
|
||||
|
||||
/* -------------------------------
|
||||
外层卡片样式(选中态 + hover 动效)
|
||||
-------------------------------- */
|
||||
function getWrapperClass(item: GetSessionListVO) {
|
||||
const isSelected = item.modelName === currentModelName.value;
|
||||
const available = isModelAvailable(item);
|
||||
|
||||
return [
|
||||
'p-2 rounded-md text-sm transition-all duration-300 relative select-none flex items-center justify-between',
|
||||
available
|
||||
? 'hover:scale-[1.03] hover:shadow-[0_0_8px_rgba(0,0,0,0.1)] hover:border-gray-300'
|
||||
: 'opacity-60 cursor-not-allowed',
|
||||
isSelected
|
||||
? 'border-2 border-blue-700 shadow-[0_0_10px_rgba(29,78,216,1)]'
|
||||
: 'border border-transparent cursor-pointer',
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="model-select">
|
||||
<div class="model-select" data-tour="model-select">
|
||||
<Popover
|
||||
ref="popoverRef"
|
||||
placement="top-start"
|
||||
@@ -119,12 +162,12 @@ function handleModelClick(item: GetSessionListVO) {
|
||||
<!-- 触发元素插槽 -->
|
||||
<template #trigger>
|
||||
<div
|
||||
class="model-select-box select-none flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-12px border-[rgba()]"
|
||||
class="model-select-box select-none flex items-center gap-4px p-10px rounded-10px cursor-pointer font-size-12px border-[rgba()] leading-snug"
|
||||
>
|
||||
<div class="model-select-box-icon">
|
||||
<SvgIcon name="models" size="12" />
|
||||
</div>
|
||||
<div class="model-select-box-text font-size-12px">
|
||||
<div :class="getModelStyleClass(currentModelName)" class="model-select-box-text font-size-12px">
|
||||
{{ currentModelName }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,7 +177,8 @@ function handleModelClick(item: GetSessionListVO) {
|
||||
<div
|
||||
v-for="item in popoverList"
|
||||
:key="item.id"
|
||||
class="popover-content-box-items w-full rounded-8px select-none transition-all transition-duration-300 flex items-center hover:cursor-pointer hover:bg-[rgba(0,0,0,.04)]"
|
||||
:class="getWrapperClass(item)"
|
||||
@click="handleModelClick(item)"
|
||||
>
|
||||
<Popover
|
||||
trigger-class="popover-trigger-item-text"
|
||||
@@ -144,23 +188,9 @@ function handleModelClick(item: GetSessionListVO) {
|
||||
:offset="[12, 0]"
|
||||
>
|
||||
<template #trigger>
|
||||
<div
|
||||
class="popover-content-box-item p-4px font-size-12px text-overflow line-height-16px relative"
|
||||
:class="[
|
||||
{ 'bg-[rgba(0,0,0,.04)] is-select': item.modelName === currentModelName },
|
||||
{ 'cursor-not-allowed opacity-60': !isModelAvailable(item) },
|
||||
]"
|
||||
@click="handleModelClick(item)"
|
||||
>
|
||||
<span :class="getModelStyleClass(item.modelName)">
|
||||
{{ item.modelName }}
|
||||
<!-- VIP锁定图标 -->
|
||||
<el-icon
|
||||
v-if="!isModelAvailable(item)"
|
||||
class="absolute right-1 top-1/2 transform -translate-y-1/2"
|
||||
>
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
<div
|
||||
class="popover-content-box-item-text text-wrap max-w-200px rounded-lg p-8px font-size-12px line-height-tight"
|
||||
@@ -168,6 +198,14 @@ function handleModelClick(item: GetSessionListVO) {
|
||||
{{ item.remark }}
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<!-- VIP锁定图标 -->
|
||||
<el-icon
|
||||
v-if="!isModelAvailable(item)"
|
||||
class="absolute right-1 top-1/2 transform -translate-y-1/2"
|
||||
>
|
||||
<Lock />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
@@ -181,23 +219,18 @@ function handleModelClick(item: GetSessionListVO) {
|
||||
border: 1px solid var(--el-color-primary, #409eff);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.popover-content-box-item.is-select {
|
||||
font-weight: 700;
|
||||
color: var(--el-color-primary, #409eff);
|
||||
}
|
||||
|
||||
.popover-content-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
height: 200px;
|
||||
height: 300px;
|
||||
overflow: hidden auto;
|
||||
.popover-content-box-items {
|
||||
:deep() {
|
||||
.popover-trigger-item-text {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.popover-trigger-item-text) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.popover-content-box-item-text {
|
||||
color: white;
|
||||
background-color: black;
|
||||
@@ -215,4 +248,32 @@ function handleModelClick(item: GetSessionListVO) {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 彩色流光动画 */
|
||||
@keyframes gradientFlow {
|
||||
0%, 100% { background-position: 0 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
|
||||
/* 金色光泽动画 */
|
||||
@keyframes goldShine {
|
||||
0% { transform: translateX(-100%) translateY(-100%); }
|
||||
100% { transform: translateX(100%) translateY(100%); }
|
||||
}
|
||||
|
||||
/* 柔光 hover 动效 */
|
||||
@keyframes glowPulse {
|
||||
0%, 100% { box-shadow: 0 0 6px rgba(37,99,235,0.2); }
|
||||
50% { box-shadow: 0 0 10px rgba(37,99,235,0.5); }
|
||||
}
|
||||
|
||||
.animate-gradientFlow {
|
||||
animation: gradientFlow 3s ease infinite;
|
||||
}
|
||||
.animate-goldShine {
|
||||
animation: goldShine 4s linear infinite;
|
||||
}
|
||||
.animate-glowPulse {
|
||||
animation: glowPulse 2s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1261
Yi.Ai.Vue3/src/components/SystemAnnouncementDialog/index.vue
Normal file
1261
Yi.Ai.Vue3/src/components/SystemAnnouncementDialog/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import { FullScreen } from '@element-plus/icons-vue';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
interface NavItem {
|
||||
@@ -17,7 +18,7 @@ interface Props {
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: '弹窗标题',
|
||||
width: '1000px',
|
||||
width: '75%',
|
||||
defaultActive: '',
|
||||
});
|
||||
|
||||
@@ -25,9 +26,13 @@ const emit = defineEmits(['update:modelValue', 'confirm', 'close', 'nav-change']
|
||||
|
||||
const visible = ref(false);
|
||||
const activeNav = ref(props.defaultActive || (props.navItems.length > 0 ? props.navItems[0].name : ''));
|
||||
const isFullscreen = ref(false);
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val;
|
||||
if (!val) {
|
||||
isFullscreen.value = false; // 关闭时重置全屏状态
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.defaultActive, (val) => {
|
||||
@@ -51,20 +56,49 @@ function handleConfirm() {
|
||||
emit('confirm', activeNav.value);
|
||||
handleClose();
|
||||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
isFullscreen.value = !isFullscreen.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
:width="width"
|
||||
:width="isFullscreen ? '100%' : width"
|
||||
:before-close="handleClose"
|
||||
:fullscreen="isFullscreen"
|
||||
:top="isFullscreen ? '0' : '5vh'"
|
||||
class="nav-dialog"
|
||||
>
|
||||
<template #header="{ titleId, titleClass }">
|
||||
<div class="dialog-header">
|
||||
<h4 :id="titleId" :class="titleClass">
|
||||
{{ title }}
|
||||
</h4>
|
||||
<!-- 全屏按钮暂不做 -->
|
||||
<div v-if="false" class="header-actions">
|
||||
<slot name="extra-actions" />
|
||||
<el-button
|
||||
circle
|
||||
plain
|
||||
size="small"
|
||||
class="fullscreen-btn"
|
||||
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||
@click="toggleFullscreen"
|
||||
>
|
||||
<el-icon>
|
||||
<FullScreen />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="dialog-container">
|
||||
<!-- 左侧导航 -->
|
||||
<div class="nav-side">
|
||||
<div class="nav-side" data-tour="user-nav-menu">
|
||||
<el-menu
|
||||
:default-active="activeNav"
|
||||
class="nav-menu"
|
||||
@@ -74,6 +108,7 @@ function handleConfirm() {
|
||||
v-for="item in navItems"
|
||||
:key="item.name"
|
||||
:index="item.name"
|
||||
:data-tour="`nav-${item.name}`"
|
||||
>
|
||||
<template #title>
|
||||
<el-icon v-if="item.icon">
|
||||
@@ -104,24 +139,52 @@ function handleConfirm() {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fullscreen-btn {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.fullscreen-btn:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.dialog-container {
|
||||
display: flex;
|
||||
height: 500px;
|
||||
height: 70vh;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
:deep(.el-dialog.is-fullscreen) .dialog-container {
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.nav-side {
|
||||
width: 200px;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
border-right: none;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-main {
|
||||
flex: 1;
|
||||
padding: 0 20px;
|
||||
overflow: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
|
||||
@@ -262,7 +262,7 @@ onMounted(async () => {
|
||||
<!-- 自适应缩放 iframe -->
|
||||
<iframe
|
||||
src="https://ccnetcore.com/article/3a1bc4d1-6a7d-751d-91cc-2817eb2ddcde"
|
||||
class="min-w-full h-[700px] scale-100 duration-300"
|
||||
class="min-w-full iframe-responsive scale-100 duration-300"
|
||||
loading="lazy"
|
||||
sandbox="allow-scripts allow-same-origin allow-popups"
|
||||
@load="document.querySelector('.iframe-loading')?.remove()"
|
||||
@@ -315,9 +315,17 @@ onMounted(async () => {
|
||||
|
||||
<style scoped>
|
||||
.api-key-management {
|
||||
padding: 20px;
|
||||
max-width: 600px;
|
||||
padding:5px 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
/* iframe 响应式高度 */
|
||||
.iframe-responsive {
|
||||
height: 60vh;
|
||||
min-height: 400px;
|
||||
max-height: 800px;
|
||||
}
|
||||
|
||||
/* 未领取状态样式 */
|
||||
@@ -389,20 +397,37 @@ onMounted(async () => {
|
||||
/* 已领取状态样式 */
|
||||
.claimed-state {
|
||||
margin: 30px 0;
|
||||
padding: 30px;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.key-title {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.key-display {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.key-input {
|
||||
font-family: monospace;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.key-input :deep(.el-input__inner) {
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* 添加按钮间距 */
|
||||
@@ -414,71 +439,89 @@ onMounted(async () => {
|
||||
.key-input :deep(.el-input-group__append .el-button + .el-button) {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.key-actions {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.key-hint {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
margin-top: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 使用说明样式 */
|
||||
.usage-guide {
|
||||
margin-top: 40px;
|
||||
margin-top: 10px;
|
||||
padding: 10px 20px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.guide-content {
|
||||
background: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
line-height: 1.8;
|
||||
border-left: 4px solid #409eff;
|
||||
}
|
||||
|
||||
/* 成功弹窗样式 */
|
||||
.success-dialog {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
font-size: 18px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 15px 0 5px;
|
||||
margin: 20px 0 10px;
|
||||
}
|
||||
|
||||
.success-tip {
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
font-size: 15px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 未领取状态样式 */
|
||||
.unclaimed-state {
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
margin: 40px 0;
|
||||
padding: 40px;
|
||||
perspective: 600px;
|
||||
background: linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.gift-container {
|
||||
position: relative;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
margin: 0 auto;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
margin: 0 auto 30px;
|
||||
cursor: pointer;
|
||||
filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
|
||||
.gift-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: all 0.3s;
|
||||
transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
|
||||
.gift-box:hover:not(.opening) {
|
||||
transform: translateY(-5px);
|
||||
transform: translateY(-10px) scale(1.05);
|
||||
}
|
||||
|
||||
.gift-box.opening .gift-lid {
|
||||
@@ -592,16 +635,23 @@ onMounted(async () => {
|
||||
|
||||
.claim-text {
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.claim-text h3 {
|
||||
color: #e74c3c;
|
||||
margin-bottom: 10px;
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.claim-text p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user