Compare commits

..

63 Commits

Author SHA1 Message Date
chenchun
3a60bcc174 refactor: 优化交易状态枚举处理方式
- 为TradeStatusEnum枚举添加Description特性标注
- 重构GetTradeStatusDescription方法,使用反射获取Description特性值
- 简化ParseTradeStatus方法,使用Enum.TryParse替代switch表达式
- 提高代码可维护性,避免硬编码状态描述
2025-08-13 18:30:56 +08:00
chenchun
2b3fad16fd feat: 优化支付宝回调通知记录功能
- 新增SignStr字段记录支付宝回调的原始签名字符串
- 修改日志记录格式,使用键值对形式记录回调通知数据
- 更新PayManager.RecordPayNoticeAsync方法支持记录原始签名字符串
- 移除AlipayManager中冗余的注释说明
2025-08-13 18:21:05 +08:00
chenchun
f0cf6bf5c8 fix: 修复支付宝支付功能相关问题
- 修复支付接口参数顺序错误,调整商品名称和订单号参数位置
- 修复支付页面HTML返回格式,直接返回Body内容而非序列化字符串
- 添加支付相关接口的权限控制,支付回调接口允许匿名访问
- 优化支付宝回调验签逻辑,保持原始参数顺序避免验签失败
- 增加回调格式错误的异常处理
- 修复商品类型枚举显示名称为英文,新增测试商品类型
- 修正Token服务提示文案中的错别字
- 移除订单更新时不必要的时间字段设置
2025-08-13 17:42:13 +08:00
chenchun
0ba4e3240b feat: 完成支付宝接入 2025-08-13 12:07:35 +08:00
ccnetcore
9332b17fc1 feat: 集成支付宝支付SDK并添加当面付测试调用,更新CORS配置支持capacitor 2025-08-13 08:26:45 +08:00
ccnetcore
4ec4023f40 feat: 增加EmbeddingResponse的object字段并完善AiGateWayManager的Usage统计,更新CORS配置 2025-08-11 20:24:48 +08:00
chenchun
d9971541f2 feat: 支持字符串类型的embedding输入参数
在AiGateWayManager中新增对JsonElement字符串类型的处理,确保embedding请求能够正确处理单个字符串输入参数。
2025-08-11 18:10:11 +08:00
chenchun
7b0e4fcc73 fix: 修复Embedding输入处理逻辑和字段可空性
- 优化Embedding输入类型判断逻辑,支持string和JsonElement数组类型
- 将EncodingFormat字段设置为可空类型,提高兼容性
- 注释知识库场景下的消息统计功能,避免不必要的数据记录
2025-08-11 18:05:33 +08:00
chenchun
cfde73d13a fix: 修复输出为空问题 2025-08-11 16:53:33 +08:00
chenchun
c17c9000a8 refactor: 移除AiHub Domain层对Application.Contracts的循环依赖
移除Yi.Framework.AiHub.Domain项目中对Yi.Framework.AiHub.Application.Contracts的项目引用,解决领域层和应用层之间的循环依赖问题,符合DDD架构分层原则。
2025-08-11 15:51:59 +08:00
chenchun
42d537a68b style: 调整架构引用 2025-08-11 15:31:11 +08:00
chenchun
25eebec8f7 feat: 新增向量嵌入服务支持
新增SiliconFlow向量嵌入服务实现,支持文本向量化功能:
- 新增ITextEmbeddingService接口和SiliconFlowTextEmbeddingService实现
- 新增EmbeddingCreateRequest/Response等向量相关DTO
- 在AiGateWayManager中新增EmbeddingForStatisticsAsync方法
- 在OpenApiService中新增向量生成API接口
- 扩展ModelTypeEnum枚举支持Embedding类型
- 优化ThorChatMessage的Content属性处理逻辑
2025-08-11 15:29:24 +08:00
Gsh
bbe5b01872 fix: 优化token图表,增加全屏显示 2025-08-10 15:34:53 +08:00
ccnetcore
6b31536de5 fix: 修复用户过期判断逻辑,按日期比较避免当天误判 2025-08-10 12:07:09 +08:00
ccnetcore
2e5db5500f Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-08-10 11:53:52 +08:00
ccnetcore
7038d31c53 feat: 新增VIP充值接口并支持通过角色代码为用户分配角色 2025-08-10 11:53:28 +08:00
Gsh
3eb27c3d35 fix: 增加对话token显示,token消耗统计 2025-08-10 00:56:44 +08:00
ccnetcore
a9c3a1bcec fix: 修复统计中 Token 数量计算错误,将计数改为求和 2025-08-09 23:38:56 +08:00
ccnetcore
384926e73a feat: 新增用户数据导出功能 2025-08-09 22:55:26 +08:00
ccnetcore
4335c12659 chore: 注释掉生成新闻和股票价格的异步调用 2025-08-09 13:58:26 +08:00
ccnetcore
e6e4829164 feat: 新增VIP过期自动卸载功能
- 新增`AiRechargeManager`类,实现VIP过期用户的自动卸载逻辑。
- 新增`AiHubConst`常量类,统一管理角色名称。
- 在`IRoleService`中添加`RemoveUserRoleByRoleCodeAsync`方法,用于移除指定用户的角色。
- 在`RoleManager`中实现`RemoveUserRoleByRoleCodeAsync`方法。
- 优化`CurrentExtensions`中VIP角色判断逻辑,使用常量替代硬编码。
- 调整`YiAbpWebModule`中部分代码格式,提升可读性。
2025-08-09 13:14:15 +08:00
ccnetcore
f3c67cf598 fix: 修复统计数量偶发问题 2025-08-09 12:20:28 +08:00
ccnetcore
4681d468ce style: 优化验证码样式 2025-08-05 22:41:20 +08:00
chenchun
63e7d3d5f5 style: 更新主题2.2 2025-08-05 18:23:33 +08:00
chenchun
f47d8c8ce3 style: 优化2.1样式 2025-08-05 17:19:03 +08:00
chenchun
6f69f45ddc Merge branch 'bbs-sharpdance' into ai-hub 2025-08-05 14:11:16 +08:00
chenchun
e73678c788 style: 全部样式更新2.0 2025-08-05 14:09:39 +08:00
ccnetcore
09a2f91cbf style: 优化样式1.1 2025-08-04 23:55:48 +08:00
ccnetcore
29da7499a4 Merge branch 'bbs-sharpdance' into ai-hub 2025-08-04 23:37:11 +08:00
ccnetcore
5b024e9443 style: 重写ele 2025-08-04 23:34:13 +08:00
ccnetcore
225932eff1 style: 上线全局样式 2025-08-04 23:29:25 +08:00
Gsh
65d5f5ae86 fix: 加载优化、vip状态优化、apikey优化 2025-08-04 23:11:42 +08:00
ccnetcore
3e647ef14d style: 全局修改样式主题 2025-08-04 22:35:45 +08:00
chenchun
7cb3aea2e6 style: 调整样式 2025-08-04 18:27:18 +08:00
chenchun
7f4b8f1c8a feat: 添加暗色主题支持
- 在HTML根元素添加dark类名以启用暗色模式
- 引入Element Plus暗色主题CSS变量文件
- 格式化代码缩进和结构,提升代码可读性
2025-08-04 17:07:01 +08:00
ccnetcore
0a2710b865 feat: 支持图片生成 2025-08-04 01:03:47 +08:00
ccnetcore
2a301c4983 feat: 支持图片生成 2025-08-03 23:23:32 +08:00
Gsh
faa8131a1b fix: 未登录对话id逻辑玩优化 2025-08-03 21:56:51 +08:00
ccnetcore
71bd885bd0 fix: 支持router参数 2025-08-03 21:47:22 +08:00
ccnetcore
691a1e50f0 feat: 支持未登录用户统计 2025-08-03 21:32:54 +08:00
ccnetcore
ef6e9fd16d style: 优化提示词 2025-08-02 22:04:22 +08:00
chenchun
17f9ac6d54 style: 优化防抖样式 2025-08-01 17:58:07 +08:00
chenchun
3f8e6e48c0 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-07-28 14:39:09 +08:00
chenchun
bda4fdf69d feat: 兼容代码补全功能 2025-07-28 14:39:02 +08:00
Gsh
5c85ed13fd fix: 加载进度优化与登录弹窗优化 2025-07-28 13:43:46 +08:00
chenchun
1986901031 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-07-28 13:15:49 +08:00
chenchun
e1d3ec21e5 feat: 支持错误处理 2025-07-28 13:15:42 +08:00
Gsh
f45283dade Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-07-28 12:59:42 +08:00
Gsh
31c44d8df7 fix: 登录弹窗超时功能取消 2025-07-28 12:59:07 +08:00
chenchun
bf443963c8 fix: 修复ThorChatCompletionsRequest中Messages属性的可空类型问题 2025-07-28 12:50:48 +08:00
chenchun
a0eb234539 feat: 兼容了用量使用显示 2025-07-22 10:40:23 +08:00
ccnetcore
b6d670c240 perf: 兼容deepseek格式 2025-07-21 22:03:55 +08:00
ccnetcore
b5fb2c42c6 feat: 兼容deepseek协议 2025-07-21 21:57:14 +08:00
ccnetcore
d72cc529ba perf: 优化流式输出 2025-07-21 21:15:02 +08:00
Gsh
660bd00cae fix: apikey加载状态 2025-07-20 22:12:48 +08:00
Gsh
b5489711ec fix: 加载优化 2025-07-20 21:01:41 +08:00
Gsh
76717c4f8a Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2025-07-20 17:23:33 +08:00
ccnetcore
3d53d0bcd6 style: 完成进度条加载 2025-07-20 17:14:05 +08:00
ccnetcore
c7c9428b68 style: 完成进度条加载 2025-07-20 17:01:17 +08:00
ccnetcore
991a970d6a style: 完成进度条加载 2025-07-20 16:40:54 +08:00
ccnetcore
cbe93b9f7e style: 完成进度条加载 2025-07-20 15:15:05 +08:00
ccnetcore
5d7217b775 feat: 完成支持functioncall功能 2025-07-18 23:12:20 +08:00
ccnetcore
d6836b8bcf feat: 提交cicd产物 2025-07-18 21:08:51 +08:00
172 changed files with 4030 additions and 763 deletions

View File

@@ -0,0 +1,14 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
/// <summary>
/// 创建订单输入DTO
/// </summary>
public class CreateOrderInput
{
/// <summary>
/// 商品类型
/// </summary>
public GoodsTypeEnum GoodsType { get; set; }
}

View File

@@ -0,0 +1,22 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
/// <summary>
/// 创建订单输出DTO
/// </summary>
public class CreateOrderOutput
{
/// <summary>
/// 订单ID
/// </summary>
public Guid OrderId { get; set; }
/// <summary>
/// 商家订单号
/// </summary>
public string OutTradeNo { get; set; }
/// <summary>
/// 支付页面HTML内容
/// </summary>
public object PaymentPageHtml { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
/// <summary>
/// 查询订单状态输入DTO
/// </summary>
public class QueryOrderStatusInput
{
/// <summary>
/// 商家订单号
/// </summary>
public string OutTradeNo { get; set; }
}

View File

@@ -0,0 +1,59 @@
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
/// <summary>
/// 查询订单状态输出DTO
/// </summary>
public class QueryOrderStatusOutput
{
/// <summary>
/// 订单ID
/// </summary>
public Guid OrderId { get; set; }
/// <summary>
/// 商家订单号
/// </summary>
public string OutTradeNo { get; set; }
/// <summary>
/// 支付宝交易号
/// </summary>
public string? TradeNo { get; set; }
/// <summary>
/// 交易状态
/// </summary>
public TradeStatusEnum TradeStatus { get; set; }
/// <summary>
/// 交易状态描述
/// </summary>
public string TradeStatusDescription { get; set; }
/// <summary>
/// 订单金额
/// </summary>
public decimal TotalAmount { get; set; }
/// <summary>
/// 商品名称
/// </summary>
public string GoodsName { get; set; }
/// <summary>
/// 商品类型
/// </summary>
public GoodsTypeEnum GoodsType { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreationTime { get; set; }
/// <summary>
/// 最后修改时间
/// </summary>
public DateTime? LastModificationTime { get; set; }
}

View File

@@ -0,0 +1,43 @@
using System.ComponentModel.DataAnnotations;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
public class RechargeCreateInput
{
/// <summary>
/// 用户ID
/// </summary>
[Required]
public Guid UserId { get; set; }
/// <summary>
/// 充值金额
/// </summary>
[Required]
[Range(0.01, double.MaxValue, ErrorMessage = "充值金额必须大于0")]
public decimal RechargeAmount { get; set; }
/// <summary>
/// 充值内容
/// </summary>
[Required]
[StringLength(500, ErrorMessage = "充值内容不能超过500个字符")]
public string Content { get; set; } = string.Empty;
/// <summary>
/// 到期时间为空表示永久VIP
/// </summary>
public DateTime? ExpireDateTime { get; set; }
/// <summary>
/// 备注
/// </summary>
[StringLength(1000, ErrorMessage = "备注不能超过1000个字符")]
public string? Remark { get; set; }
/// <summary>
/// 联系方式
/// </summary>
[StringLength(200, ErrorMessage = "联系方式不能超过200个字符")]
public string? ContactInfo { get; set; }
}

View File

@@ -0,0 +1,17 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
/// <summary>
/// 每日Token使用量统计DTO
/// </summary>
public class DailyTokenUsageDto
{
/// <summary>
/// 日期
/// </summary>
public DateTime Date { get; set; }
/// <summary>
/// Token消耗量
/// </summary>
public long Tokens { get; set; }
}

View File

@@ -0,0 +1,22 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
/// <summary>
/// 模型Token使用量统计DTO
/// </summary>
public class ModelTokenUsageDto
{
/// <summary>
/// 模型ID
/// </summary>
public string Model { get; set; }
/// <summary>
/// 总消耗量
/// </summary>
public long Tokens { get; set; }
/// <summary>
/// 占比(百分比)
/// </summary>
public decimal Percentage { get; set; }
}

View File

@@ -0,0 +1,33 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
/// <summary>
/// 支付服务接口
/// </summary>
public interface IPayService : IApplicationService
{
/// <summary>
/// 创建订单并发起支付
/// </summary>
/// <param name="input">创建订单输入</param>
/// <returns>订单创建结果</returns>
Task<CreateOrderOutput> CreateOrderAsync(CreateOrderInput input);
/// <summary>
/// 支付宝异步通知处理
/// </summary>
/// <param name="form">表单数据</param>
/// <returns></returns>
Task<string> AlipayNotifyAsync([FromForm] IFormCollection form);
/// <summary>
/// 查询订单状态
/// </summary>
/// <param name="input">查询订单状态输入</param>
/// <returns>订单状态信息</returns>
Task<QueryOrderStatusOutput> QueryOrderStatusAsync([FromQuery] QueryOrderStatusInput input);
}

View File

@@ -0,0 +1,9 @@
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
public interface IRechargeService
{
/// <summary>
/// 移除用户vip及角色
/// </summary>
Task RemoveVipRoleByExpireAsync();
}

View File

@@ -0,0 +1,21 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.UsageStatistics;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
/// <summary>
/// 使用量统计服务接口
/// </summary>
public interface IUsageStatisticsService
{
/// <summary>
/// 获取当前用户近7天的Token消耗统计
/// </summary>
/// <returns>每日Token使用量列表</returns>
Task<List<DailyTokenUsageDto>> GetLast7DaysTokenUsageAsync();
/// <summary>
/// 获取当前用户各个模型的Token消耗量及占比
/// </summary>
/// <returns>模型Token使用量列表</returns>
Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync();
}

View File

@@ -6,8 +6,5 @@
<ProjectReference Include="..\..\rbac\Yi.Framework.Rbac.Application.Contracts\Yi.Framework.Rbac.Application.Contracts.csproj" />
<ProjectReference Include="..\Yi.Framework.AiHub.Domain.Shared\Yi.Framework.AiHub.Domain.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="IServices\" />
</ItemGroup>
</Project>

View File

@@ -12,12 +12,13 @@ using OpenAI.Chat;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
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.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.Rbac.Application.Contracts.IServices;
using Yi.Framework.Rbac.Domain.Shared.Dtos;
using Yi.Framework.SqlSugarCore.Abstractions;
@@ -68,6 +69,7 @@ public class AiChatService : ApplicationService
public async Task<List<ModelGetListOutput>> GetModelAsync()
{
var output = await _aiModelRepository._DbQueryable
.Where(x => x.ModelType == ModelTypeEnum.Chat)
.OrderByDescending(x => x.OrderNum)
.Select(x => new ModelGetListOutput
{
@@ -94,7 +96,8 @@ public class AiChatService : ApplicationService
/// <param name="input"></param>
/// <param name="sessionId"></param>
/// <param name="cancellationToken"></param>
public async Task PostSendAsync([FromBody] ThorChatCompletionsRequest input, [FromRoute] Guid sessionId,
[HttpPost("ai-chat/send")]
public async Task PostSendAsync([FromBody] ThorChatCompletionsRequest input, [FromQuery] Guid? sessionId,
CancellationToken cancellationToken)
{
//除了免费模型,其他的模型都要校验
@@ -114,6 +117,7 @@ public class AiChatService : ApplicationService
throw new UserFriendlyException("未登录用户只能使用未加速的DeepSeek-R1请登录后重试");
}
}
//ai网关代理httpcontext
await _aiGateWayManager.CompleteChatStreamForStatisticsAsync(_httpContextAccessor.HttpContext, input,
CurrentUser.Id, sessionId, cancellationToken);

View File

@@ -2,10 +2,13 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
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.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.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
@@ -17,16 +20,17 @@ public class OpenApiService : ApplicationService
private readonly TokenManager _tokenManager;
private readonly AiGateWayManager _aiGateWayManager;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
private readonly AiBlacklistManager _aiBlacklistManager;
public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger<OpenApiService> logger,
TokenManager tokenManager, AiGateWayManager aiGateWayManager,
ISqlSugarRepository<AiModelEntity> aiModelRepository)
ISqlSugarRepository<AiModelEntity> aiModelRepository, AiBlacklistManager aiBlacklistManager)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_tokenManager = tokenManager;
_aiGateWayManager = aiGateWayManager;
_aiModelRepository = aiModelRepository;
_aiBlacklistManager = aiBlacklistManager;
}
/// <summary>
@@ -41,6 +45,7 @@ public class OpenApiService : ApplicationService
//前面都是校验,后面才是真正的调用
var httpContext = this._httpContextAccessor.HttpContext;
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
//ai网关代理httpcontext
if (input.Stream == true)
{
@@ -55,6 +60,35 @@ public class OpenApiService : ApplicationService
}
}
/// <summary>
/// 图片生成
/// </summary>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
[HttpPost("openApi/v1/images/generations")]
public async Task ImagesGenerationsAsync([FromBody] ImageCreateRequest input, CancellationToken cancellationToken)
{
var httpContext = this._httpContextAccessor.HttpContext;
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
await _aiGateWayManager.CreateImageForStatisticsAsync(httpContext, userId, null, input);
}
/// <summary>
/// 向量生成
/// </summary>
/// <param name="input"></param>
/// <param name="cancellationToken"></param>
[HttpPost("openApi/v1/embeddings")]
public async Task EmbeddingAsync([FromBody] ThorEmbeddingInput input, CancellationToken cancellationToken)
{
var httpContext = this._httpContextAccessor.HttpContext;
var userId = await _tokenManager.GetUserIdAsync(GetTokenByHttpContext(httpContext));
await _aiBlacklistManager.VerifiyAiBlacklist(userId);
await _aiGateWayManager.EmbeddingForStatisticsAsync(httpContext, userId, null, input);
}
/// <summary>
/// 获取模型列表
/// </summary>
@@ -63,6 +97,7 @@ public class OpenApiService : ApplicationService
public async Task<ModelsListDto> ModelsAsync()
{
var data = await _aiModelRepository._DbQueryable
.Where(x => x.ModelType == ModelTypeEnum.Chat)
.OrderByDescending(x => x.OrderNum)
.Select(x => new ModelsDataDto
{

View File

@@ -0,0 +1,170 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Application.Services;
using Yi.Framework.AiHub.Domain.Alipay;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Pay;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Volo.Abp;
using Yi.Framework.AiHub.Domain.Entities.Pay;
using Yi.Framework.SqlSugarCore.Abstractions;
using System.ComponentModel;
using System.Reflection;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// 支付服务
/// </summary>
public class PayService : ApplicationService, IPayService
{
private readonly AlipayManager _alipayManager;
private readonly PayManager _payManager;
private readonly ILogger<PayService> _logger;
private readonly ISqlSugarRepository<PayOrderAggregateRoot, Guid> _payOrderRepository;
public PayService(
AlipayManager alipayManager,
PayManager payManager,
ILogger<PayService> logger, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository)
{
_alipayManager = alipayManager;
_payManager = payManager;
_logger = logger;
_payOrderRepository = payOrderRepository;
}
/// <summary>
/// 创建订单并发起支付
/// </summary>
/// <param name="input">创建订单输入</param>
/// <returns>订单创建结果</returns>
[Authorize]
[HttpPost("pay/Order")]
public async Task<CreateOrderOutput> CreateOrderAsync(CreateOrderInput input)
{
// 1. 通过PayManager创建订单
var order = await _payManager.CreateOrderAsync(input.GoodsType);
// 2. 通过AlipayManager发起页面支付
var paymentPageHtml = await _alipayManager.PaymentPageAsync(
order.GoodsName,
order.OutTradeNo,
order.TotalAmount);
// 3. 返回结果
return new CreateOrderOutput
{
OrderId = order.Id,
OutTradeNo = order.OutTradeNo,
PaymentPageHtml = paymentPageHtml.Body
};
}
/// <summary>
/// 支付宝异步通知处理
/// </summary>
/// <param name="form">表单数据</param>
/// <returns></returns>
[HttpPost("pay/AlipayNotify")]
[AllowAnonymous]
public async Task<string> AlipayNotifyAsync([FromForm] IFormCollection form)
{
// 1. 将表单数据转换为字典,保持原始顺序
var notifyData = new Dictionary<string, string>();
foreach (var item in form)
{
notifyData[item.Key] = item.Value.ToString();
}
var signStr = string.Join("&", notifyData.Select(kv => $"{kv.Key}={kv.Value}"));
_logger.LogInformation($"收到支付宝回调通知:{signStr}");
// 2. 验证签名
await _alipayManager.VerifyNotifyAsync(notifyData);
// 3. 记录支付通知
await _payManager.RecordPayNoticeAsync(notifyData,signStr);
// 4. 更新订单状态
var outTradeNo = notifyData.GetValueOrDefault("out_trade_no", string.Empty);
var tradeStatus = notifyData.GetValueOrDefault("trade_status", string.Empty);
var tradeNo = notifyData.GetValueOrDefault("trade_no", string.Empty);
if (!string.IsNullOrEmpty(outTradeNo) && !string.IsNullOrEmpty(tradeStatus))
{
var status = ParseTradeStatus(tradeStatus);
await _payManager.UpdateOrderStatusAsync(outTradeNo, status, tradeNo);
_logger.LogInformation("订单状态更新成功,订单号:{OutTradeNo},状态:{TradeStatus}", outTradeNo, tradeStatus);
}
else
{
throw new AlipayException($"回调格式错误");
}
return "success";
}
/// <summary>
/// 查询订单状态
/// </summary>
/// <param name="input">查询订单状态输入</param>
/// <returns>订单状态信息</returns>
[HttpGet("pay/OrderStatus")]
[Authorize]
public async Task<QueryOrderStatusOutput> QueryOrderStatusAsync([FromQuery] QueryOrderStatusInput input)
{
// 通过PayManager查询订单
var order = await _payOrderRepository.GetFirstAsync(x => x.OutTradeNo == input.OutTradeNo);
if (order == null)
{
throw new UserFriendlyException($"订单不存在:{input.OutTradeNo}");
}
return new QueryOrderStatusOutput
{
OrderId = order.Id,
OutTradeNo = order.OutTradeNo,
TradeNo = order.TradeNo,
TradeStatus = order.TradeStatus,
TradeStatusDescription = GetTradeStatusDescription(order.TradeStatus),
TotalAmount = order.TotalAmount,
GoodsName = order.GoodsName,
GoodsType = order.GoodsType,
CreationTime = order.CreationTime,
LastModificationTime = order.LastModificationTime
};
}
/// <summary>
/// 获取交易状态描述
/// </summary>
/// <param name="tradeStatus">交易状态</param>
/// <returns>状态描述</returns>
private string GetTradeStatusDescription(TradeStatusEnum tradeStatus)
{
var fieldInfo = tradeStatus.GetType().GetField(tradeStatus.ToString());
var descriptionAttribute = fieldInfo?.GetCustomAttribute<DescriptionAttribute>();
return descriptionAttribute?.Description ?? "未知状态";
}
/// <summary>
/// 解析交易状态
/// </summary>
/// <param name="tradeStatus">状态字符串</param>
/// <returns></returns>
private TradeStatusEnum ParseTradeStatus(string tradeStatus)
{
if (Enum.TryParse<TradeStatusEnum>(tradeStatus, out var result))
{
return result;
}
return TradeStatusEnum.WAIT_TRADE;
}
}

View File

@@ -1,41 +1,92 @@
using Mapster;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Application.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos.Recharge;
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.Rbac.Application.Contracts.IServices;
using Yi.Framework.Rbac.Domain.Shared.Dtos;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// ai 充值表
/// </summary>
public class RechargeService : ApplicationService
namespace Yi.Framework.AiHub.Application.Services
{
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _repository;
public RechargeService(ISqlSugarRepository<AiRechargeAggregateRoot> repository)
public class RechargeService : ApplicationService,IRechargeService
{
_repository = repository;
}
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _repository;
private readonly ICurrentUser _currentUser;
private readonly IUserService _userService;
private readonly IRoleService _roleService;
private readonly AiRechargeManager _aiMessageManager;
/// <summary>
/// 查询已登录的账户充值记录
/// </summary>
/// <returns></returns>
[Route("recharge/account")]
[Authorize]
public async Task<List<RechargeGetListOutput>> GetListByAccountAsync()
{
var userId = CurrentUser.Id;
var entities = await _repository._DbQueryable.Where(x => x.UserId == userId)
.OrderByDescending(x => x.CreationTime)
.ToListAsync();
var output = entities.Adapt<List<RechargeGetListOutput>>();
return output;
public RechargeService(
ISqlSugarRepository<AiRechargeAggregateRoot> repository,
ICurrentUser currentUser,
IUserService userService, IRoleService roleService, AiRechargeManager aiMessageManager)
{
_repository = repository;
_currentUser = currentUser;
_userService = userService;
_roleService = roleService;
_aiMessageManager = aiMessageManager;
}
/// <summary>
/// 查询已登录的账户充值记录
/// </summary>
/// <returns></returns>
[Route("recharge/account")]
[Authorize]
public async Task<List<RechargeGetListOutput>> GetListByAccountAsync()
{
var userId = CurrentUser.Id;
var entities = await _repository._DbQueryable.Where(x => x.UserId == userId)
.OrderByDescending(x => x.CreationTime)
.ToListAsync();
var output = entities.Adapt<List<RechargeGetListOutput>>();
return output;
}
/// <summary>
/// 给用户充值VIP
/// </summary>
/// <param name="input">充值输入参数</param>
/// <returns></returns>
[HttpPost("recharge/vip")]
public async Task RechargeVipAsync(RechargeCreateInput input)
{
// 创建充值记录
var rechargeRecord = new AiRechargeAggregateRoot
{
UserId = input.UserId,
RechargeAmount = input.RechargeAmount,
Content = input.Content,
ExpireDateTime = input.ExpireDateTime,
Remark = input.Remark,
ContactInfo = input.ContactInfo
};
// 保存充值记录到数据库
await _repository.InsertAsync(rechargeRecord);
// 使用UserService给用户添加VIP角色
await _userService.AddUserRoleByRoleCodeAsync(input.UserId,
new List<string>() { AiHubConst.VipRole, "default" });
}
/// <summary>
/// 移除用户vip及角色
/// </summary>
[RemoteService(isEnabled: false)]
public async Task RemoveVipRoleByExpireAsync()
{
var expiredUserIds = await _aiMessageManager.RemoveVipByExpireAsync();
if (expiredUserIds is not null)
{
await _roleService.RemoveUserRoleByRoleCodeAsync(expiredUserIds, AiHubConst.VipRole);
}
}
}
}

View File

@@ -47,7 +47,7 @@ public class TokenService : ApplicationService
{
if (!CurrentUser.IsAiVip())
{
throw new UserFriendlyException("充值成为Vip第三方token服务");
throw new UserFriendlyException("充值成为Vip第三方token服务");
}
await _tokenManager.CreateAsync(CurrentUser.GetId());

View File

@@ -0,0 +1,105 @@
using Microsoft.AspNetCore.Authorization;
using SqlSugar;
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.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Application.Services;
/// <summary>
/// 使用量统计服务
/// </summary>
[Authorize]
public class UsageStatisticsService : ApplicationService, IUsageStatisticsService
{
private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository;
private readonly ISqlSugarRepository<UsageStatisticsAggregateRoot> _usageStatisticsRepository;
public UsageStatisticsService(
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<UsageStatisticsAggregateRoot> usageStatisticsRepository)
{
_messageRepository = messageRepository;
_usageStatisticsRepository = usageStatisticsRepository;
}
/// <summary>
/// 获取当前用户近7天的Token消耗统计
/// </summary>
/// <returns>每日Token使用量列表</returns>
public async Task<List<DailyTokenUsageDto>> GetLast7DaysTokenUsageAsync()
{
var userId = CurrentUser.GetId();
var endDate = DateTime.Today;
var startDate = endDate.AddDays(-6); // 近7天
// 从Message表统计近7天的token消耗
var dailyUsage = await _messageRepository._DbQueryable
.Where(x => x.UserId == userId)
.Where(x => x.CreationTime >= startDate && x.CreationTime < endDate.AddDays(1))
.GroupBy(x => x.CreationTime.Date)
.Select(g => new
{
Date = g.CreationTime.Date,
Tokens = SqlFunc.AggregateSum(g.TokenUsage.TotalTokenCount)
})
.ToListAsync();
// 生成完整的7天数据包括没有使用记录的日期
var result = new List<DailyTokenUsageDto>();
for (int i = 0; i < 7; i++)
{
var date = startDate.AddDays(i);
var usage = dailyUsage.FirstOrDefault(x => x.Date == date);
result.Add(new DailyTokenUsageDto
{
Date = date,
Tokens = usage?.Tokens ?? 0
});
}
return result.OrderBy(x => x.Date).ToList();
}
/// <summary>
/// 获取当前用户各个模型的Token消耗量及占比
/// </summary>
/// <returns>模型Token使用量列表</returns>
public async Task<List<ModelTokenUsageDto>> GetModelTokenUsageAsync()
{
var userId = CurrentUser.GetId();
// 从UsageStatistics表获取各模型的token消耗统计
var modelUsages = await _usageStatisticsRepository._DbQueryable
.Where(x => x.UserId == userId)
.Select(x => new
{
x.ModelId,
x.TotalTokenCount
})
.ToListAsync();
if (!modelUsages.Any())
{
return new List<ModelTokenUsageDto>();
}
// 计算总token数
var totalTokens = modelUsages.Sum(x => x.TotalTokenCount);
// 计算各模型占比
var result = modelUsages.Select(x => new ModelTokenUsageDto
{
Model = x.ModelId,
Tokens = x.TotalTokenCount,
Percentage = totalTokens > 0 ? Math.Round((decimal)x.TotalTokenCount / totalTokens * 100, 2) : 0
}).OrderByDescending(x => x.Tokens).ToList();
return result;
}
}

View File

@@ -0,0 +1,6 @@
namespace Yi.Framework.AiHub.Domain.Shared.Consts;
public class AiHubConst
{
public const string VipRole = "YiXinAi-Vip";
}

View File

@@ -1,8 +1,6 @@
using SqlSugar;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos;
public class MessageInputDto
{

View File

@@ -0,0 +1,79 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
//TODO add model validation
//TODO check what is string or array for prompt,..
public record EmbeddingCreateRequest
{
/// <summary>
/// Input text to get embeddings for, encoded as a string or array of tokens. To get embeddings for multiple inputs
/// in a single request, pass an array of strings or array of token arrays. Each input must not exceed 2048 tokens in
/// length.
/// Unless your are embedding code, we suggest replacing newlines (`\n`) in your input with a single space, as we have
/// observed inferior results when newlines are present.
/// </summary>
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings/create-input" />
[JsonIgnore]
public List<string>? InputAsList { get; set; }
/// <summary>
/// Input text to get embeddings for, encoded as a string or array of tokens. To get embeddings for multiple inputs
/// in a single request, pass an array of strings or array of token arrays. Each input must not exceed 2048 tokens in
/// length.
/// Unless your are embedding code, we suggest replacing newlines (`\n`) in your input with a single space, as we have
/// observed inferior results when newlines are present.
/// </summary>
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings/create-input" />
[JsonIgnore]
public string? Input { get; set; }
[JsonPropertyName("input")]
public IList<string>? InputCalculated
{
get
{
if (Input != null && InputAsList != null)
{
throw new ValidationException(
"Input and InputAsList can not be assigned at the same time. One of them is should be null.");
}
if (Input != null)
{
return new List<string> { Input };
}
return InputAsList;
}
}
/// <summary>
/// ID of the model to use. You can use the [List models](/docs/api-reference/models/list) API to see all of your
/// available models, or see our [Model overview](/docs/models/overview) for descriptions of them.
/// </summary>
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings/create-model" />
[JsonPropertyName("model")]
public string? Model { get; set; }
/// <summary>
/// The number of dimensions the resulting output embeddings should have. Only supported in text-embedding-3 and later models.
/// </summary>
/// <see href="https://platform.openai.com/docs/api-reference/embeddings/create#embeddings-create-dimensions" />
[JsonPropertyName("dimensions")]
public int? Dimensions { get; set; }
/// <summary>
/// The format to return the embeddings in. Can be either float or base64.
/// </summary>
/// <returns></returns>
[JsonPropertyName("encoding_format")]
public string? EncodingFormat { get; set; }
public IEnumerable<ValidationResult> Validate()
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,113 @@
using System.Buffers;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
public record EmbeddingCreateResponse : ThorBaseResponse
{
[JsonPropertyName("model")] public string Model { get; set; }
[JsonPropertyName("data")] public List<EmbeddingResponse> Data { get; set; } = [];
/// <summary>
/// 类型转换如果类型是base64,则将float[]转换为base64,如果是空或是float和原始类型一样则不转换
/// </summary>
public void ConvertEmbeddingData(string? encodingFormat)
{
if (Data.Count == 0)
{
return;
}
switch (encodingFormat)
{
// 判断第一个是否是float[],如果是则不转换
case null or "float" when Data[0].Embedding is float[]:
return;
// 否则转换成float[]
case null or "float":
{
foreach (var embeddingResponse in Data)
{
if (embeddingResponse.Embedding is string base64)
{
embeddingResponse.Embedding = Convert.FromBase64String(base64);
}
}
return;
}
// 判断第一个是否是string如果是则不转换
case "base64" when Data[0].Embedding is string:
return;
// 否则转换成base64
case "base64":
{
foreach (var embeddingResponse in Data)
{
if (embeddingResponse.Embedding is JsonElement str)
{
if (str.ValueKind == JsonValueKind.Array)
{
var floats = str.EnumerateArray().Select(element => element.GetSingle()).ToArray();
embeddingResponse.Embedding = ConvertFloatArrayToBase64(floats);
}
}
else if (embeddingResponse.Embedding is IList<double> doubles)
{
embeddingResponse.Embedding = ConvertFloatArrayToBase64(doubles.ToArray());
}
}
break;
}
}
}
public static string ConvertFloatArrayToBase64(double[] floatArray)
{
// 将 float[] 转换成 byte[]
byte[] byteArray = ArrayPool<byte>.Shared.Rent(floatArray.Length * sizeof(float));
try
{
Buffer.BlockCopy(floatArray, 0, byteArray, 0, byteArray.Length);
// 将 byte[] 转换成 base64 字符串
return Convert.ToBase64String(byteArray);
}
finally
{
ArrayPool<byte>.Shared.Return(byteArray);
}
}
public static string ConvertFloatArrayToBase64(float[] floatArray)
{
// 将 float[] 转换成 byte[]
byte[] byteArray = ArrayPool<byte>.Shared.Rent(floatArray.Length * sizeof(float));
try
{
Buffer.BlockCopy(floatArray, 0, byteArray, 0, floatArray.Length);
// 将 byte[] 转换成 base64 字符串
return Convert.ToBase64String(byteArray);
}
finally
{
ArrayPool<byte>.Shared.Return(byteArray);
}
}
[JsonPropertyName("usage")] public ThorUsageResponse? Usage { get; set; }
}
public record EmbeddingResponse
{
[JsonPropertyName("object")] public string Object { get; set; } = "embedding";
[JsonPropertyName("index")] public int? Index { get; set; }
[JsonPropertyName("embedding")] public object Embedding { get; set; }
}

View File

@@ -0,0 +1,22 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
public sealed class ThorEmbeddingInput
{
[JsonPropertyName("model")]
public string Model { get; set; }
[JsonPropertyName("input")]
public object Input { get; set; }
[JsonPropertyName("encoding_format")]
public string? EncodingFormat { get; set; }
[JsonPropertyName("dimensions")]
public int? Dimensions { get; set; }
[JsonPropertyName("user")]
public string? User { get; set; }
}

View File

@@ -0,0 +1,54 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
/// <summary>
/// Image Create Request Model
/// </summary>
public record ImageCreateRequest : SharedImageRequestBaseModel
{
public ImageCreateRequest()
{
}
public ImageCreateRequest(string prompt)
{
Prompt = prompt;
}
/// <summary>
/// A text description of the desired image(s). The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3
/// </summary>
[JsonPropertyName("prompt")]
public string Prompt { get; set; }
/// <summary>
/// The quality of the image that will be generated. Possible values are 'standard' or 'hd' (default is 'standard').
/// Hd creates images with finer details and greater consistency across the image.
/// This param is only supported for dall-e-3 model.
/// <br /><br />Check <see cref="StaticValues.ImageStatics.Quality"/> for possible values
/// </summary>
[JsonPropertyName("quality")]
public string? Quality { get; set; }
/// <summary>
/// The style of the generated images. Must be one of vivid or natural.
/// Vivid causes the model to lean towards generating hyper-real and dramatic images.
/// Natural causes the model to produce more natural, less hyper-real looking images. This param is only supported for dall-e-3.
/// <br /><br />Check <see cref="StaticValues.ImageStatics.Style"/> for possible values
/// </summary>
[JsonPropertyName("style")]
public string? Style { get; set; }
[JsonPropertyName("background")]
public string? Background { get; set; }
[JsonPropertyName("moderation")]
public string? Moderation { get; set; }
[JsonPropertyName("output_compression")]
public string? OutputCompression { get; set; }
[JsonPropertyName("output_format")]
public string? OutputFormat { get; set; }
}

View File

@@ -0,0 +1,19 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
public record ImageCreateResponse : ThorBaseResponse
{
[JsonPropertyName("data")] public List<ImageDataResult> Results { get; set; }
[JsonPropertyName("usage")] public ThorUsageResponse? Usage { get; set; } = new();
public record ImageDataResult
{
[JsonPropertyName("url")] public string Url { get; set; }
[JsonPropertyName("b64_json")] public string B64 { get; set; }
[JsonPropertyName("revised_prompt")] public string RevisedPrompt { get; set; }
}
}

View File

@@ -0,0 +1,51 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
public record ImageEditCreateRequest : SharedImageRequestBaseModel
{
/// <summary>
/// The image to edit. Must be a valid PNG file, less than 4MB, and square.
/// </summary>
public byte[]? Image { get; set; }
/// <summary>
/// Image file name
/// </summary>
public string ImageName { get; set; }
/// <summary>
/// An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where image should be edited.
/// Must be a valid PNG file, less than 4MB, and have the same dimensions as image.
/// </summary>
public byte[]? Mask { get; set; }
/// <summary>
/// Mask file name
/// </summary>
public string? MaskName { get; set; }
[JsonPropertyName("quality")]
public string Quality { get; set; }
/// <summary>
/// A text description of the desired image(s). The maximum length is 1000 characters.
/// </summary>
[JsonPropertyName("prompt")]
public string Prompt { get; set; }
[JsonPropertyName("background")]
public string? Background { get; set; }
[JsonPropertyName("moderation")]
public string? Moderation { get; set; }
[JsonPropertyName("output_compression")]
public string? OutputCompression { get; set; }
[JsonPropertyName("output_format")]
public string? OutputFormat { get; set; }
[JsonPropertyName("style")]
public string? Style { get; set; }
}

View File

@@ -0,0 +1,14 @@
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
public record ImageVariationCreateRequest : SharedImageRequestBaseModel
{
/// <summary>
/// The image to edit. Must be a valid PNG file, less than 4MB, and square.
/// </summary>
public byte[] Image { get; set; }
/// <summary>
/// Image file name
/// </summary>
public string ImageName { get; set; }
}

View File

@@ -0,0 +1,42 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
public record SharedImageRequestBaseModel
{
/// <summary>
/// The number of images to generate. Must be between 1 and 10.
/// For dall-e-3 model, only n=1 is supported.
/// </summary>
[JsonPropertyName("n")]
public int? N { get; set; }
/// <summary>
/// The size of the generated images.
/// Must be one of 256x256, 512x512, or 1024x1024 for dall-e-2.
/// Must be one of 1024x1024, 1792x1024, or 1024x1792 for dall-e-3 models.
/// <br /><br />Check <see cref="StaticValues.ImageStatics.Size"/> for possible values
/// </summary>
[JsonPropertyName("size")]
public string? Size { get; set; }
/// <summary>
/// The format in which the generated images are returned. Must be one of url or b64_json
/// </summary>
[JsonPropertyName("response_format")]
public string? ResponseFormat { get; set; }
/// <summary>
/// A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse.
/// <a href="https://platform.openai.com/docs/usage-policies/end-user-ids">Learn more</a>.
/// </summary>
[JsonPropertyName("user")]
public string? User { get; set; }
/// <summary>
/// The model to use for image generation. Must be one of dall-e-2 or dall-e-3
/// For ImageEditCreateRequest and for ImageVariationCreateRequest only dall-e-2 modell is supported at this time.
/// </summary>
[JsonPropertyName("model")]
public string? Model { get; set; }
}

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
public class ModelsListDto
{

View File

@@ -1,4 +1,4 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// OpenAI常量

View File

@@ -1,7 +1,6 @@
using System.Text.Json.Serialization;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
public record ThorBaseResponse
{

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
public sealed class ThorChatAudioRequest
{

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 聊天完成选项列

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
public class ThorChatClaudeThinking
{

View File

@@ -2,20 +2,14 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 对话补全请求参数对象
/// </summary>
public class ThorChatCompletionsRequest
{
public ThorChatCompletionsRequest()
{
Messages = new List<ThorChatMessage>();
}
[JsonPropertyName("store")]
public bool? Store { get; set; }
[JsonPropertyName("store")] public bool? Store { get; set; }
/// <summary>
/// 表示对话中支持的模态类型数组。可以为 null。
@@ -26,14 +20,72 @@ public class ThorChatCompletionsRequest
/// <summary>
/// 表示对话中的音频请求参数。可以为 null。
/// </summary>
[JsonPropertyName("audio")] public ThorChatAudioRequest? Audio { get; set; }
[JsonPropertyName("audio")]
public ThorChatAudioRequest? Audio { get; set; }
/// <summary>
/// 包含迄今为止对话的消息列表
/// </summary>
[JsonPropertyName("messages")]
public List<ThorChatMessage> Messages { get; set; }
public List<ThorChatMessage>? Messages { get; set; }
/// <summary>
/// 兼容-代码补全
/// </summary>
[JsonPropertyName("suffix")]
public string? Suffix { get; set; }
/// <summary>
/// 兼容-代码补全
/// </summary>
[JsonPropertyName("prompt")]
public string? Prompt { get; set; }
private const string CodeCompletionPrompt = """
You are a code modification assistant. Your task is to modify the provided code based on the user's instructions.
Rules:
1. Return only the modified code, with no additional text or explanations.
2. The first character of your response must be the first character of the code.
3. The last character of your response must be the last character of the code.
4. NEVER use triple backticks (```) or any other markdown formatting in your response.
5. Do not use any code block indicators, syntax highlighting markers, or any other formatting characters.
6. Present the code exactly as it would appear in a plain text editor, preserving all whitespace, indentation, and line breaks.
7. Maintain the original code structure and only make changes as specified by the user's instructions.
8. Ensure that the modified code is syntactically and semantically correct for the given programming language.
9. Use consistent indentation and follow language-specific style guidelines.
10. If the user's request cannot be translated into code changes, respond only with the word NULL (without quotes or any formatting).
11. Do not include any comments or explanations within the code unless specifically requested.
12. Assume that any necessary dependencies or libraries are already imported or available.
IMPORTANT: Your response must NEVER begin or end with triple backticks, single backticks, or any other formatting characters.
The relevant context before the current editing content is: {0}.
After the current editing content is: {1}.
""";
/// <summary>
/// 兼容代码补全
/// </summary>
public void CompatibleCodeCompletion()
{
if (Messages is null || !Messages.Any())
{
//兼容代码补全模式Prompt为当前代码前内容Suffix为当前代码后内容
Messages = new List<ThorChatMessage>()
{
new ThorChatMessage
{
Role = "system",
Content = string.Format(CodeCompletionPrompt, Prompt, Suffix)
}
};
}
Suffix = null;
Prompt = null;
}
/// <summary>
/// 模型唯一编码值,如 gpt-4gpt-3.5-turbo,moonshot-v1-8k看底层具体平台定义
/// </summary>
@@ -229,18 +281,25 @@ public class ThorChatCompletionsRequest
{
if (value is JsonElement jsonElement)
{
if (jsonElement.ValueKind == JsonValueKind.String)
{
ToolChoice = new ThorToolChoice
{
Type = jsonElement.GetString()
};
}
else if (jsonElement.ValueKind == JsonValueKind.Object)
// if (jsonElement.ValueKind == JsonValueKind.String)
// {
// ToolChoice = new ThorToolChoice
// {
// Type = jsonElement.GetString()
// };
// }
if (jsonElement.ValueKind == JsonValueKind.Object)
{
ToolChoice = jsonElement.Deserialize<ThorToolChoice>();
}
}
else if (value is string text)
{
ToolChoice = new ThorToolChoice
{
Type = text
};
}
else
{
ToolChoice = (ThorToolChoice)value;

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 对话补全服务返回结果

View File

@@ -2,7 +2,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 聊天消息体建议使用CreeateXXX系列方法构建内容
@@ -14,7 +14,6 @@ public class ThorChatMessage
/// </summary>
public ThorChatMessage()
{
}
/// <summary>
@@ -87,7 +86,6 @@ public class ThorChatMessage
{
Content = value?.ToString();
}
}
}
@@ -108,15 +106,14 @@ public class ThorChatMessage
/// </summary>
[JsonPropertyName("function_call")]
public ThorChatMessageFunction? FunctionCall { get; set; }
/// <summary>
/// 【可选】推理内容
/// </summary>
[JsonPropertyName("reasoning_content")]
public string? ReasoningContent { get; set; }
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("id")] public string? Id { get; set; }
/// <summary>
/// 工具调用列表,模型生成的工具调用,例如函数调用。<br/>
@@ -164,14 +161,15 @@ public class ThorChatMessage
/// <param name="name">参与者的可选名称。提供模型信息以区分同一角色的参与者。</param>
/// <param name="toolCalls">工具调用参数列表</param>
/// <returns></returns>
public static ThorChatMessage CreateAssistantMessage(string content, string? name = null, List<ThorToolCall> toolCalls = null)
public static ThorChatMessage CreateAssistantMessage(string content, string? name = null,
List<ThorToolCall> toolCalls = null)
{
return new()
{
Role = ThorChatMessageRoleConst.Assistant,
Content = content,
Name = name,
ToolCalls=toolCalls,
ToolCalls = toolCalls,
};
}
@@ -187,7 +185,7 @@ public class ThorChatMessage
{
Role = ThorChatMessageRoleConst.Tool,
Content = content,
ToolCallId= toolCallId
ToolCallId = toolCallId
};
}
}

View File

@@ -1,6 +1,8 @@

using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
public sealed class ThorChatMessageAudioContent
{
[JsonPropertyName("data")]

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 发出的消息内容包含图文一般是一文一图一文多图两种情况请使用CreeateXXX系列方法构建内容

View File

@@ -1,7 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
{
/// <summary>
/// 模型调用的函数。

View File

@@ -1,4 +1,4 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
{
/// <summary>
/// 对话消息角色定义

View File

@@ -1,7 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
{
public class ThorError
{

View File

@@ -1,4 +1,4 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
{
/// <summary>
/// 支持图片识别的消息体内容类型

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 指定模型必须输出的格式的对象。用于启用JSON模式。

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
public class ThorResponseJsonSchema
{

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
{
/// <summary>
/// 流响应选项。仅当您设置 stream: true 时才设置此项。

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 工具调用对象定义

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 工具

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
{
public class ThorToolChoiceFunctionTool
{

View File

@@ -1,4 +1,4 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
{
public class ThorToolChoiceTypeConst
{

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 有效工具的定义。

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 有效函数调用的定义。

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 函数参数是JSON格式对象

View File

@@ -1,4 +1,4 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi
{
/// <summary>
/// 工具类型定义

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 统计信息模型

View File

@@ -1,6 +1,6 @@
using System.Text.Json.Serialization;
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
/// <summary>
/// 图片消息内容对象

View File

@@ -0,0 +1,96 @@
using System;
using System.Reflection;
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
/// <summary>
/// 价格特性
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class PriceAttribute : Attribute
{
public decimal Price { get; }
public PriceAttribute(double price)
{
Price = (decimal)price;
}
}
/// <summary>
/// 显示名称特性
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class DisplayNameAttribute : Attribute
{
public string DisplayName { get; }
public DisplayNameAttribute(string displayName)
{
DisplayName = displayName;
}
}
/// <summary>
/// 商品枚举
/// </summary>
public enum GoodsTypeEnum
{
[Price(0.01)]
[DisplayName("YiXinVip Test")]
YiXinVipTest = 0,
[Price(29.9)]
[DisplayName("YiXinVip 1 month")]
YiXinVip1 = 1,
[Price(80.7)]
[DisplayName("YiXinVip 3 month")]
YiXinVip3 = 3,
[Price(143.9)]
[DisplayName("YiXinVip 6 month")]
YiXinVip6 = 6,
[Price(199.9)]
[DisplayName("YiXinVip 10 month")]
YiXinVip10 = 10
}
public static class GoodsTypeEnumExtensions
{
/// <summary>
/// 获取商品总金额
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>总金额</returns>
public static decimal GetTotalAmount(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var priceAttribute = fieldInfo?.GetCustomAttribute<PriceAttribute>();
return priceAttribute?.Price ?? 0m;
}
/// <summary>
/// 获取商品价格描述
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>价格描述</returns>
public static string GetPriceDescription(this GoodsTypeEnum goodsType)
{
var price = goodsType.GetTotalAmount();
return $"¥{price:F1}";
}
/// <summary>
/// 获取商品名称
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>商品名称</returns>
public static string GetDisplayName(this GoodsTypeEnum goodsType)
{
var fieldInfo = goodsType.GetType().GetField(goodsType.ToString());
var displayNameAttribute = fieldInfo?.GetCustomAttribute<DisplayNameAttribute>();
return displayNameAttribute?.DisplayName ?? goodsType.ToString();
}
}

View File

@@ -0,0 +1,8 @@
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum ModelTypeEnum
{
Chat = 0,
Image = 1,
Embedding = 2
}

View File

@@ -0,0 +1,36 @@
using System.ComponentModel;
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
public enum TradeStatusEnum
{
/// <summary>
/// 准备发起
/// </summary>
[Description("准备发起")]
WAIT_TRADE = 0,
/// <summary>
/// 交易创建
/// </summary>
[Description("等待买家付款")]
WAIT_BUYER_PAY = 10,
/// <summary>
/// 交易关闭
/// </summary>
[Description("交易关闭")]
TRADE_CLOSED = 20,
/// <summary>
/// 交易成功
/// </summary>
[Description("交易成功")]
TRADE_SUCCESS = 100,
/// <summary>
/// 交易结束
/// </summary>
[Description("交易结束")]
TRADE_FINISHED = -10
}

View File

@@ -6,7 +6,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Consts\" />
<Folder Include="Etos\" />
</ItemGroup>

View File

@@ -2,6 +2,7 @@ using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;

View File

@@ -1,5 +1,5 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;

View File

@@ -0,0 +1,39 @@
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public interface IImageService
{
/// <summary>Creates an image given a prompt.</summary>
/// <param name="imageCreate"></param>
/// <param name="aiModelDescribe"></param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns></returns>
Task<ImageCreateResponse> CreateImage(
ImageCreateRequest imageCreate,
AiModelDescribe? aiModelDescribe = null,
CancellationToken cancellationToken = default);
/// <summary>
/// Creates an edited or extended image given an original image and a prompt.
/// </summary>
/// <param name="imageEditCreateRequest"></param>
/// <param name="aiModelDescribe"></param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns></returns>
Task<ImageCreateResponse> CreateImageEdit(
ImageEditCreateRequest imageEditCreateRequest,
AiModelDescribe? aiModelDescribe = null,
CancellationToken cancellationToken = default);
/// <summary>Creates a variation of a given image.</summary>
/// <param name="imageEditCreateRequest"></param>
/// <param name="aiModelDescribe"></param>
/// <param name="cancellationToken">Propagates notification that operations should be canceled.</param>
/// <returns></returns>
Task<ImageCreateResponse> CreateImageVariation(
ImageVariationCreateRequest imageEditCreateRequest,
AiModelDescribe? aiModelDescribe = null,
CancellationToken cancellationToken = default);
}

View File

@@ -1,4 +1,4 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;

View File

@@ -0,0 +1,19 @@
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
namespace Yi.Framework.AiHub.Domain.AiGateWay;
public interface ITextEmbeddingService
{
/// <summary>
///
/// </summary>
/// <param name="createEmbeddingModel"></param>
/// <param name="aiModelDescribe"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<EmbeddingCreateResponse> EmbeddingAsync(
EmbeddingCreateRequest createEmbeddingModel,
AiModelDescribe? aiModelDescribe = null,
CancellationToken cancellationToken = default);
}

View File

@@ -4,9 +4,9 @@ using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats;
@@ -108,33 +108,33 @@ public class AzureDatabricksChatCompletionsService(ILogger<AzureDatabricksChatCo
continue;
}
var content = result?.Choices?.FirstOrDefault()?.Delta;
if (first && content?.Content == OpenAIConstant.ThinkStart)
{
isThink = true;
continue;
// 需要将content的内容转换到其他字段
}
if (isThink && content?.Content?.Contains(OpenAIConstant.ThinkEnd) == true)
{
isThink = false;
// 需要将content的内容转换到其他字段
continue;
}
if (isThink && result?.Choices != null)
{
// 需要将content的内容转换到其他字段
foreach (var choice in result.Choices)
{
choice.Delta.ReasoningContent = choice.Delta.Content;
choice.Delta.Content = string.Empty;
}
}
first = false;
// var content = result?.Choices?.FirstOrDefault()?.Delta;
//
// if (first && content?.Content == OpenAIConstant.ThinkStart)
// {
// isThink = true;
// continue;
// // 需要将content的内容转换到其他字段
// }
//
// if (isThink && content?.Content?.Contains(OpenAIConstant.ThinkEnd) == true)
// {
// isThink = false;
// // 需要将content的内容转换到其他字段
// continue;
// }
//
// if (isThink && result?.Choices != null)
// {
// // 需要将content的内容转换到其他字段
// foreach (var choice in result.Choices)
// {
// choice.Delta.ReasoningContent = choice.Delta.Content;
// choice.Delta.Content = string.Empty;
// }
// }
//
// first = false;
yield return result;
}

View File

@@ -4,9 +4,9 @@ using System.Net.Http.Json;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
@@ -30,6 +30,7 @@ public class AzureOpenAiChatCompletionCompletionsService(ILogger<AzureOpenAiChat
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
var error = await response.Content.ReadAsStringAsync();
logger.LogError("Azure对话异常 , StatusCode: {StatusCode} 错误响应内容:{Content}", response.StatusCode,
error);

View File

@@ -0,0 +1,112 @@
using OpenAI.Images;
using Yi.Framework.AiHub.Domain.AiGateWay;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Images;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images;
public class AzureOpenAIServiceImageService : IImageService
{
public async Task<ImageCreateResponse> CreateImage(ImageCreateRequest imageCreate, AiModelDescribe? options = null,
CancellationToken cancellationToken = default(CancellationToken))
{
var createClient = AzureOpenAIFactory.CreateClient(options);
var client = createClient.GetImageClient(imageCreate.Model);
// 将size字符串拆分为宽度和高度
var size = imageCreate.Size.Split('x');
if (size.Length != 2)
{
throw new ArgumentException("Size must be in the format of 'width x height'");
}
var response = await client.GenerateImageAsync(imageCreate.Prompt, new ImageGenerationOptions()
{
Quality = imageCreate.Quality == "standard" ? GeneratedImageQuality.Standard : GeneratedImageQuality.High,
Size = new GeneratedImageSize(Convert.ToInt32(size[0]), Convert.ToInt32(size[1])),
Style = imageCreate.Style == "vivid" ? GeneratedImageStyle.Vivid : GeneratedImageStyle.Natural,
ResponseFormat =
imageCreate.ResponseFormat == "url" ? GeneratedImageFormat.Uri : GeneratedImageFormat.Bytes,
// User = imageCreate.User
EndUserId = imageCreate.User
}, cancellationToken);
var ret = new ImageCreateResponse()
{
Results = new List<ImageCreateResponse.ImageDataResult>()
};
if (response.Value.ImageUri != null)
{
ret.Results.Add(new ImageCreateResponse.ImageDataResult()
{
Url = response.Value.ImageUri.ToString()
});
}
else
{
ret.Results.Add(new ImageCreateResponse.ImageDataResult()
{
B64 = Convert.ToBase64String(response.Value.ImageBytes.ToArray())
});
}
return ret;
}
public async Task<ImageCreateResponse> CreateImageEdit(ImageEditCreateRequest imageEditCreateRequest,
AiModelDescribe? options = null,
CancellationToken cancellationToken = default(CancellationToken))
{
var url = AzureOpenAIFactory.GetEditImageAddress(options, imageEditCreateRequest.Model);
var multipartContent = new MultipartFormDataContent();
if (imageEditCreateRequest.User != null)
{
multipartContent.Add(new StringContent(imageEditCreateRequest.User), "user");
}
if (imageEditCreateRequest.ResponseFormat != null)
{
multipartContent.Add(new StringContent(imageEditCreateRequest.ResponseFormat), "response_format");
}
if (imageEditCreateRequest.Size != null)
{
multipartContent.Add(new StringContent(imageEditCreateRequest.Size), "size");
}
if (imageEditCreateRequest.N != null)
{
multipartContent.Add(new StringContent(imageEditCreateRequest.N.ToString()!), "n");
}
if (imageEditCreateRequest.Model != null)
{
multipartContent.Add(new StringContent(imageEditCreateRequest.Model!), "model");
}
if (imageEditCreateRequest.Mask != null)
{
multipartContent.Add(new ByteArrayContent(imageEditCreateRequest.Mask), "mask",
imageEditCreateRequest.MaskName);
}
multipartContent.Add(new StringContent(imageEditCreateRequest.Prompt), "prompt");
multipartContent.Add(new ByteArrayContent(imageEditCreateRequest.Image), "image",
imageEditCreateRequest.ImageName);
return await HttpClientFactory.GetHttpClient(url).PostFileAndReadAsAsync<ImageCreateResponse>(
url,
multipartContent, cancellationToken);
}
public Task<ImageCreateResponse> CreateImageVariation(ImageVariationCreateRequest imageEditCreateRequest,
AiModelDescribe? options = null,
CancellationToken cancellationToken = default(CancellationToken))
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,178 @@
using System.Diagnostics;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Yi.Framework.AiHub.Domain.AiGateWay.Exceptions;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats;
public sealed class DeepSeekChatCompletionsService(ILogger<DeepSeekChatCompletionsService> logger)
: IChatCompletionService
{
public async IAsyncEnumerable<ThorChatCompletionsResponse> CompleteChatStreamAsync(AiModelDescribe options,
ThorChatCompletionsRequest chatCompletionCreate,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(options.Endpoint))
{
options.Endpoint = "https://api.deepseek.com/v1";
}
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 对话流式补全");
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).HttpRequestRaw(
options?.Endpoint.TrimEnd('/') + "/chat/completions",
chatCompletionCreate, options.ApiKey);
openai?.SetTag("Model", chatCompletionCreate.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new UnauthorizedAccessException();
}
if (response.StatusCode == HttpStatusCode.PaymentRequired)
{
throw new PaymentRequiredException();
}
// 如果限流则抛出限流异常
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new ThorRateLimitException();
}
// 大于等于400的状态码都认为是异常
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
logger.LogError("OpenAI对话异常 , StatusCode: {StatusCode} ", response.StatusCode);
throw new BusinessException("OpenAI对话异常", response.StatusCode.ToString());
}
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;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
{
line += Environment.NewLine;
if (line.StartsWith('{'))
{
logger.LogInformation("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode,
line);
throw new BusinessException("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;
}
var result = JsonSerializer.Deserialize<ThorChatCompletionsResponse>(line,
ThorJsonSerializer.DefaultOptions);
// var content = result?.Choices?.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 result.Choices)
// {
// //choice.Delta.ReasoningContent = choice.Delta.Content;
// //choice.Delta.Content = string.Empty;
// }
// }
// first = false;
yield return result;
}
}
public async Task<ThorChatCompletionsResponse> CompleteChatAsync(AiModelDescribe options,
ThorChatCompletionsRequest chatCompletionCreate,
CancellationToken cancellationToken)
{
using var openai =
Activity.Current?.Source.StartActivity("OpenAI 对话补全");
if (string.IsNullOrWhiteSpace(options.Endpoint))
{
options.Endpoint = "https://api.deepseek.com/v1";
}
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).PostJsonAsync(
options?.Endpoint.TrimEnd('/') + "/chat/completions",
chatCompletionCreate, options.ApiKey).ConfigureAwait(false);
openai?.SetTag("Model", chatCompletionCreate.Model);
openai?.SetTag("Response", response.StatusCode.ToString());
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
throw new BusinessException("渠道未登录,请联系管理人员", "401");
}
// 如果限流则抛出限流异常
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
throw new ThorRateLimitException();
}
// 大于等于400的状态码都认为是异常
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
logger.LogError("OpenAI对话异常 , StatusCode: {StatusCode} Response: {Response}", response.StatusCode, error);
throw new BusinessException("OpenAI对话异常", response.StatusCode.ToString());
}
var result =
await response.Content.ReadFromJsonAsync<ThorChatCompletionsResponse>(
cancellationToken: cancellationToken).ConfigureAwait(false);
return result;
}
}

View File

@@ -0,0 +1,24 @@
using System.Net.Http.Json;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi.Embeddings;
namespace Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings;
public sealed class SiliconFlowTextEmbeddingService
: ITextEmbeddingService
{
public async Task<EmbeddingCreateResponse> EmbeddingAsync(
EmbeddingCreateRequest createEmbeddingModel,
AiModelDescribe? options = null,
CancellationToken cancellationToken = default)
{
var response = await HttpClientFactory.GetHttpClient(options.Endpoint).PostJsonAsync(
options?.Endpoint.TrimEnd('/') + "/v1/embeddings",
createEmbeddingModel, options!.ApiKey);
var result =
await response.Content.ReadFromJsonAsync<EmbeddingCreateResponse>(cancellationToken: cancellationToken);
return result;
}
}

View File

@@ -1,6 +1,6 @@
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;

View File

@@ -1,4 +1,4 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
namespace Yi.Framework.AiHub.Domain.AiGateWay;

View File

@@ -0,0 +1,10 @@
using Microsoft.Extensions.Logging;
namespace Yi.Framework.AiHub.Domain.Alipay;
public class AlipayException:UserFriendlyException
{
public AlipayException(string message, string? code = null, string? details = null, Exception? innerException = null, LogLevel logLevel = LogLevel.Warning) : base(message, code, details, innerException, logLevel)
{
}
}

View File

@@ -0,0 +1,69 @@
using Alipay.EasySDK.Factory;
using Alipay.EasySDK.Kernel.Util;
using Alipay.EasySDK.Payment.Page.Models;
using Microsoft.Extensions.Logging;
using Volo.Abp.Domain.Services;
namespace Yi.Framework.AiHub.Domain.Alipay;
public class AlipayManager : DomainService
{
private readonly ILogger<AlipayManager> _logger;
public AlipayManager(ILogger<AlipayManager> logger)
{
_logger = logger;
}
/// <summary>
/// 统一Page支付
/// </summary>
/// <returns></returns>
/// <exception cref="AlipayException"></exception>
public Task<AlipayTradePagePayResponse> PaymentPageAsync(string productName, string orderNumber,
decimal totalAmount)
{
try
{
// 2. 发起API调用以创建当面付收款二维码为例
var response = Factory.Payment.Page()
.Pay(productName, orderNumber, totalAmount.ToString(), "https://ccnetcore.com/pay/sucess");
// 3. 处理响应或异常
if (ResponseChecker.Success(response))
{
_logger.LogInformation($"支付宝PaymentPage发起调用成功返回内容{response.Body}");
//插入数据库
return Task.FromResult(response);
}
else
{
throw new AlipayException($"支付宝PaymentPage发起调用失败原因{response.Body}");
}
}
catch (Exception ex)
{
throw new AlipayException($"支付宝PaymentPage发起调用错误原因{ex.Message}", innerException: ex);
}
}
/// <summary>
/// 通知验签
/// </summary>
/// <param name="form"></param>
/// <returns></returns>
/// <exception cref="AlipayException"></exception>
public Task VerifyNotifyAsync(Dictionary<string, string> form)
{
// 支付宝的验签需要保持原始参数顺序,排序会导致验签失败
var result = Factory.Payment.Common().VerifyNotify(form);
if (result == false)
{
_logger.LogError($"支付宝支付验签失败,回调参数:{System.Text.Json.JsonSerializer.Serialize(form)}");
throw new AlipayException($"支付宝支付,验签失败");
}
_logger.LogInformation("支付宝回调验签成功");
return Task.CompletedTask;
}
}

View File

@@ -1,9 +1,9 @@
using Mapster;
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Entities.ValueObjects;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Domain.Entities.Chat;
@@ -19,7 +19,7 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
{
}
public MessageAggregateRoot(Guid userId, Guid? sessionId, string content, string role, string modelId,
public MessageAggregateRoot(Guid? userId, Guid? sessionId, string content, string role, string modelId,
ThorUsageResponse? tokenUsage)
{
UserId = userId;
@@ -29,10 +29,18 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
ModelId = modelId;
if (tokenUsage is not null)
{
long inputTokenCount = tokenUsage.PromptTokens
?? tokenUsage.InputTokens
?? 0;
long outputTokenCount = tokenUsage.CompletionTokens
?? tokenUsage.OutputTokens
?? 0;
this.TokenUsage = new TokenUsageValueObject
{
OutputTokenCount = tokenUsage.OutputTokens ?? 0,
InputTokenCount = tokenUsage.InputTokens ?? 0,
OutputTokenCount = outputTokenCount,
InputTokenCount = inputTokenCount,
TotalTokenCount = tokenUsage.TotalTokens ?? 0
};
}
@@ -40,11 +48,11 @@ public class MessageAggregateRoot : FullAuditedAggregateRoot<Guid>
this.MessageType = sessionId is null ? MessageTypeEnum.Api : MessageTypeEnum.Web;
}
public Guid UserId { get; set; }
public Guid? UserId { get; set; }
public Guid? SessionId { get; set; }
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string Content { get; set; }
public string? Content { get; set; }
public string Role { get; set; }
public string ModelId { get; set; }

View File

@@ -1,5 +1,6 @@
using SqlSugar;
using Volo.Abp.Domain.Entities;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.Core.Data;
namespace Yi.Framework.AiHub.Domain.Entities.Model;
@@ -8,7 +9,7 @@ namespace Yi.Framework.AiHub.Domain.Entities.Model;
/// ai模型定义
/// </summary>
[SugarTable("Ai_Model")]
public class AiModelEntity : Entity<Guid>, IOrderNum,ISoftDelete
public class AiModelEntity : Entity<Guid>, IOrderNum, ISoftDelete
{
/// <summary>
/// 处理名
@@ -44,9 +45,14 @@ public class AiModelEntity : Entity<Guid>, IOrderNum,ISoftDelete
/// ai应用id
/// </summary>
public Guid AiAppId { get; set; }
/// <summary>
/// 额外信息
/// </summary>
public string? ExtraInfo { get; set; }
/// <summary>
/// 模型类型
/// </summary>
public ModelTypeEnum ModelType { get; set; }
}

View File

@@ -0,0 +1,64 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Domain.Entities.Pay;
/// <summary>
/// 支付通知记录
/// </summary>
[SugarTable("Ai_PayNoticeRecord")]
public class PayNoticeRecordAggregateRoot: FullAuditedAggregateRoot<Guid>
{
/// <summary>
/// 通知时间
/// </summary>
public DateTime NotifyTime { get; set; }
/// <summary>
/// 支付宝交易号
/// </summary>
public string TradeNo { get; set; }
/// <summary>
/// 商家订单号
/// </summary>
public string OutTradeNo { get; set; }
/// <summary>
/// 买家openId
/// </summary>
public string BuyerId { get; set; }
/// <summary>
/// 订单状态
/// </summary>
public TradeStatusEnum TradeStatus { get; set; }
/// <summary>
/// 订单金额
/// </summary>
public decimal TotalAmount { get; set; }
/// <summary>
/// 实收金额
/// </summary>
public decimal ReceiptAmount { get; set; }
/// <summary>
/// 用户支付金额
/// </summary>
public decimal BuyerPayAmount { get; set; }
/// <summary>
/// 通知原始数据
/// </summary>
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string NotifyData { get; set; } = string.Empty;
/// <summary>
/// 原始signstr
/// </summary>
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
public string SignStr { get; set; }
}

View File

@@ -0,0 +1,52 @@
using SqlSugar;
using Volo.Abp.Domain.Entities.Auditing;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Domain.Entities.Pay;
/// <summary>
/// 支付订单
/// </summary>
[SugarTable("Ai_PayOrder")]
public class PayOrderAggregateRoot : FullAuditedAggregateRoot<Guid>
{
/// <summary>
/// 商家订单号
/// </summary>
public string OutTradeNo { get; set; }
/// <summary>
/// 下单用户
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// 下单用户名称
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 订单状态
/// </summary>
public TradeStatusEnum TradeStatus { get; set; } = TradeStatusEnum.WAIT_TRADE;
/// <summary>
/// 订单金额
/// </summary>
public decimal TotalAmount { get; set; }
/// <summary>
/// 商品类型
/// </summary>
public GoodsTypeEnum GoodsType { get; set; }
/// <summary>
/// 商品名称
/// </summary>
public string GoodsName { get; set; }
/// <summary>
/// 支付宝交易号
/// </summary>
public string? TradeNo { get; set; }
}

View File

@@ -13,7 +13,7 @@ public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
{
}
public UsageStatisticsAggregateRoot(Guid userId, string modelId)
public UsageStatisticsAggregateRoot(Guid? userId, string modelId)
{
UserId = userId;
ModelId = modelId;
@@ -22,7 +22,7 @@ public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
/// <summary>
/// 用户id
/// </summary>
public Guid UserId { get; set; }
public Guid? UserId { get; set; }
/// <summary>
/// 哪个模型
@@ -37,22 +37,22 @@ public class UsageStatisticsAggregateRoot : FullAuditedAggregateRoot<Guid>
/// <summary>
/// 使用输出token总数
/// </summary>
public int UsageOutputTokenCount { get; set; }
public long UsageOutputTokenCount { get; set; }
/// <summary>
/// 使用输入总数
/// </summary>
public int UsageInputTokenCount { get; set; }
public long UsageInputTokenCount { get; set; }
/// <summary>
/// 总token使用数量
/// </summary>
public int TotalTokenCount { get; set; }
public long TotalTokenCount { get; set; }
/// <summary>
/// 新增一次聊天统计
/// </summary>
public void AddOnceChat(int inputTokenCount, int outputTokenCount)
public void AddOnceChat(long inputTokenCount, long outputTokenCount)
{
UsageTotalNumber += 1;
UsageOutputTokenCount += outputTokenCount;

View File

@@ -2,9 +2,9 @@
public class TokenUsageValueObject
{
public int OutputTokenCount { get; set; }
public long OutputTokenCount { get; set; }
public int InputTokenCount { get; set; }
public long InputTokenCount { get; set; }
public long TotalTokenCount { get; set; }
}

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Http;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Domain.Shared.Consts;
namespace Yi.Framework.AiHub.Domain.Extensions;
@@ -7,6 +8,6 @@ public static class CurrentExtensions
{
public static bool IsAiVip(this ICurrentUser currentUser)
{
return currentUser.Roles.Contains("YiXinAi-Vip") || currentUser.UserName == "cc";
return currentUser.Roles.Contains(AiHubConst.VipRole) || currentUser.UserName == "cc";
}
}

View File

@@ -1,17 +1,22 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Application.Contracts.Dtos;
using Yi.Framework.AiHub.Application.Contracts.Dtos.OpenAi;
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.Dtos;
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.Core.Extensions;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
@@ -83,7 +88,7 @@ public class AiGateWayManager : DomainService
var modelDescribe = await GetModelAsync(request.Model);
var chatService =
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
await foreach (var result in chatService.CompleteChatStreamAsync(modelDescribe, request, cancellationToken))
{
yield return result;
@@ -109,8 +114,7 @@ public class AiGateWayManager : DomainService
_specialCompatible.Compatible(request);
var response = httpContext.Response;
// 设置响应头,声明是 json
response.ContentType = "application/json; charset=UTF-8";
await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true);
//response.ContentType = "application/json; charset=UTF-8";
var modelDescribe = await GetModelAsync(request.Model);
var chatService =
LazyServiceProvider.GetRequiredKeyedService<IChatCompletionService>(modelDescribe.HandlerName);
@@ -120,7 +124,7 @@ public class AiGateWayManager : DomainService
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
new MessageInputDto
{
Content = request.Messages.LastOrDefault().Content ?? string.Empty,
Content = request.Messages?.LastOrDefault().Content ?? string.Empty,
ModelId = request.Model,
TokenUsage = data.Usage,
});
@@ -133,16 +137,10 @@ public class AiGateWayManager : DomainService
TokenUsage = data.Usage
});
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage.InputTokens ?? 0,
data.Usage.OutputTokens ?? 0);
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, data.Usage);
}
var body = JsonConvert.SerializeObject(data, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
await writer.WriteLineAsync(body);
await writer.FlushAsync(cancellationToken);
await response.WriteAsJsonAsync(data, cancellationToken);
}
/// <summary>
@@ -163,15 +161,14 @@ public class AiGateWayManager : DomainService
{
var response = httpContext.Response;
// 设置响应头,声明是 SSE 流
response.ContentType = "text/event-stream";
response.Headers.Append("Cache-Control", "no-cache");
response.Headers.Append("Connection", "keep-alive");
response.ContentType = "text/event-stream;charset=utf-8;";
response.Headers.TryAdd("Cache-Control", "no-cache");
response.Headers.TryAdd("Connection", "keep-alive");
var gateWay = LazyServiceProvider.GetRequiredService<AiGateWayManager>();
var completeChatResponse = gateWay.CompleteChatStreamAsync(request, cancellationToken);
var tokenUsage = new ThorUsageResponse();
await using var writer = new StreamWriter(response.Body, Encoding.UTF8, leaveOpen: true);
//缓存队列算法
// 创建一个队列来缓存消息
@@ -189,14 +186,14 @@ public class AiGateWayManager : DomainService
{
if (messageQueue.TryDequeue(out var message))
{
await writer.WriteLineAsync(message);
await writer.FlushAsync(cancellationToken);
await response.WriteAsync(message, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
await response.Body.FlushAsync(cancellationToken).ConfigureAwait(false);
}
if (!isComplete)
{
// 如果没有完成,才等待,已完成,全部输出
await Task.Delay(outputInterval, cancellationToken);
await Task.Delay(outputInterval, cancellationToken).ConfigureAwait(false);
}
}
}, cancellationToken);
@@ -207,24 +204,21 @@ public class AiGateWayManager : DomainService
{
await foreach (var data in completeChatResponse)
{
if (data.Usage is not null && data.Usage.TotalTokens is not null)
if (data.Usage is not null)
{
tokenUsage = data.Usage;
}
var message = JsonConvert.SerializeObject(data, new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
var message = System.Text.Json.JsonSerializer.Serialize(data, ThorJsonSerializer.DefaultOptions);
backupSystemContent.Append(data.Choices.FirstOrDefault()?.Delta.Content);
// 将消息加入队列而不是直接写入
messageQueue.Enqueue($"data: {message}\n");
messageQueue.Enqueue($"data: {message}\n\n");
}
}
catch (Exception e)
{
_logger.LogError(e, $"Ai对话异常");
var errorContent = $"Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}";
var errorContent = $"对话Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}\n异常堆栈:{e}";
var model = new ThorChatCompletionsResponse()
{
Choices = new List<ThorChatChoiceResponse>()
@@ -243,36 +237,203 @@ public class AiGateWayManager : DomainService
ContractResolver = new CamelCasePropertyNamesContractResolver()
});
backupSystemContent.Append(errorContent);
messageQueue.Enqueue($"data: {message}\n");
messageQueue.Enqueue($"data: {message}\n\n");
}
//断开连接
messageQueue.Enqueue("data: [DONE]\n");
messageQueue.Enqueue("data: [DONE]\n\n");
// 标记完成并发送结束标记
isComplete = true;
await outputTask;
if (userId is not null)
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
new MessageInputDto
{
Content = request.Messages?.LastOrDefault()?.Content ?? string.Empty,
ModelId = request.Model,
TokenUsage = tokenUsage,
});
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
new MessageInputDto
{
Content = backupSystemContent.ToString(),
ModelId = request.Model,
TokenUsage = tokenUsage
});
await _usageStatisticsManager.SetUsageAsync(userId, request.Model, tokenUsage);
}
/// <summary>
/// 图片生成
/// </summary>
/// <param name="context"></param>
/// <param name="userId"></param>
/// <param name="sessionId"></param>
/// <param name="request"></param>
/// <exception cref="BusinessException"></exception>
/// <exception cref="Exception"></exception>
public async Task CreateImageForStatisticsAsync(HttpContext context, Guid? userId, Guid? sessionId,
ImageCreateRequest request)
{
try
{
await _aiMessageManager.CreateUserMessageAsync(userId.Value, sessionId,
var model = request.Model;
if (string.IsNullOrEmpty(model)) model = "dall-e-2";
var modelDescribe = await GetModelAsync(model);
// 获取渠道指定的实现类型的服务
var imageService =
LazyServiceProvider.GetRequiredKeyedService<IImageService>(modelDescribe.HandlerName);
var response = await imageService.CreateImage(request, modelDescribe);
if (response.Error != null || response.Results.Count == 0)
{
throw new BusinessException(response.Error?.Message ?? "图片生成失败", response.Error?.Code?.ToString());
}
await context.Response.WriteAsJsonAsync(response);
await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
new MessageInputDto
{
Content = request.Messages.LastOrDefault()?.Content ?? string.Empty,
ModelId = request.Model,
TokenUsage = tokenUsage,
Content = request.Prompt,
ModelId = model,
TokenUsage = response.Usage,
});
await _aiMessageManager.CreateSystemMessageAsync(userId.Value, sessionId,
await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
new MessageInputDto
{
Content = backupSystemContent.ToString(),
ModelId = request.Model,
TokenUsage = tokenUsage
Content = response.Results?.FirstOrDefault()?.Url,
ModelId = model,
TokenUsage = response.Usage
});
await _usageStatisticsManager.SetUsageAsync(userId.Value, request.Model, tokenUsage.InputTokens ?? 0,
tokenUsage.OutputTokens ?? 0);
await _usageStatisticsManager.SetUsageAsync(userId, model, response.Usage);
}
catch (Exception e)
{
var errorContent = $"图片生成Ai异常异常信息\n当前Ai模型{request.Model}\n异常信息{e.Message}\n异常堆栈:{e}";
throw new UserFriendlyException(errorContent);
}
}
/// <summary>
/// 向量生成
/// </summary>
/// <param name="context"></param>
/// <param name="sessionId"></param>
/// <param name="input"></param>
/// <param name="userId"></param>
/// <exception cref="Exception"></exception>
/// <exception cref="BusinessException"></exception>
public async Task EmbeddingForStatisticsAsync(HttpContext context, Guid? userId, Guid? sessionId,
ThorEmbeddingInput input)
{
try
{
if (input == null) throw new Exception("模型校验异常");
using var embedding =
Activity.Current?.Source.StartActivity("向量模型调用");
var modelDescribe = await GetModelAsync(input.Model);
// 获取渠道指定的实现类型的服务
var embeddingService =
LazyServiceProvider.GetRequiredKeyedService<ITextEmbeddingService>(modelDescribe.HandlerName);
var embeddingCreateRequest = new EmbeddingCreateRequest
{
Model = input.Model,
EncodingFormat = input.EncodingFormat
};
//dto进行转换支持多种格式
if (input.Input is JsonElement str)
{
if (str.ValueKind == JsonValueKind.String)
{
embeddingCreateRequest.Input = str.ToString();
}
else if (str.ValueKind == JsonValueKind.Array)
{
var inputString = str.EnumerateArray().Select(x => x.ToString()).ToArray();
embeddingCreateRequest.InputAsList = inputString.ToList();
}
else
{
throw new Exception("Input输入格式错误非string或Array类型");
}
}
else if (input.Input is string strInput)
{
embeddingCreateRequest.Input = strInput;
}
else
{
throw new Exception("Input输入格式错误未找到类型");
}
var stream =
await embeddingService.EmbeddingAsync(embeddingCreateRequest, modelDescribe, context.RequestAborted);
var usage = new ThorUsageResponse()
{
PromptTokens = stream.Usage?.PromptTokens??0,
InputTokens = stream.Usage?.InputTokens ?? 0,
CompletionTokens = 0,
TotalTokens = stream.Usage?.InputTokens ?? 0
};
await context.Response.WriteAsJsonAsync(new
{
input.Model,
stream.Data,
stream.Error,
Object = stream.ObjectTypeName,
Usage = usage
});
//知识库暂不使用message统计
// await _aiMessageManager.CreateUserMessageAsync(userId, sessionId,
// new MessageInputDto
// {
// Content = string.Empty,
// ModelId = input.Model,
// TokenUsage = usage,
// });
//
// await _aiMessageManager.CreateSystemMessageAsync(userId, sessionId,
// new MessageInputDto
// {
// Content = string.Empty,
// ModelId = input.Model,
// TokenUsage = usage
// });
await _usageStatisticsManager.SetUsageAsync(userId, input.Model, usage);
}
catch (ThorRateLimitException)
{
context.Response.StatusCode = 429;
}
catch (UnauthorizedAccessException e)
{
context.Response.StatusCode = 401;
}
catch (Exception e)
{
var errorContent = $"嵌入Ai异常异常信息\n当前Ai模型{input.Model}\n异常信息{e.Message}\n异常堆栈:{e}";
throw new UserFriendlyException(errorContent);
}
}
}

View File

@@ -1,8 +1,8 @@
using Volo.Abp.Domain.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Application.Contracts.Dtos;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Chat;
using Yi.Framework.AiHub.Domain.Shared.Dtos;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
@@ -23,7 +23,7 @@ public class AiMessageManager : DomainService
/// <param name="userId"></param>
/// <param name="input"></param>
/// <returns></returns>
public async Task CreateSystemMessageAsync(Guid userId, Guid? sessionId, MessageInputDto input)
public async Task CreateSystemMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input)
{
input.Role = "system";
var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage);
@@ -37,7 +37,7 @@ public class AiMessageManager : DomainService
/// <param name="userId"></param>
/// <param name="input"></param>
/// <returns></returns>
public async Task CreateUserMessageAsync(Guid userId, Guid? sessionId, MessageInputDto input)
public async Task CreateUserMessageAsync(Guid? userId, Guid? sessionId, MessageInputDto input)
{
input.Role = "user";
var message = new MessageAggregateRoot(userId, sessionId, input.Content, input.Role, input.ModelId,input.TokenUsage);

View File

@@ -0,0 +1,75 @@
using Microsoft.Extensions.Logging;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
public class AiRechargeManager : DomainService
{
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _rechargeRepository;
private readonly ISqlSugarRepository<TokenAggregateRoot> _tokenRepository;
private readonly ILogger<AiRechargeManager> _logger;
public AiRechargeManager(ISqlSugarRepository<AiRechargeAggregateRoot> rechargeRepository,
ISqlSugarRepository<TokenAggregateRoot> tokenRepository, ILogger<AiRechargeManager> logger)
{
_rechargeRepository = rechargeRepository;
_tokenRepository = tokenRepository;
_logger = logger;
}
public async Task<List<Guid>?> RemoveVipByExpireAsync()
{
_logger.LogInformation("开始执行VIP过期自动卸载任务");
// 获取当前时间
var currentTime = DateTime.Now;
// 查找所有充值记录,按用户分组
var allRecharges = await _rechargeRepository._DbQueryable
.ToListAsync();
if (!allRecharges.Any())
{
_logger.LogInformation("没有找到任何充值记录");
return null;
}
// 按用户分组,找出真正过期的用户
var expiredUserIds = allRecharges
.GroupBy(x => x.UserId)
.Where(group =>
{
// 如果用户有任何一个过期时间为空的记录说明是永久VIP不过期
if (group.Any(x => !x.ExpireDateTime.HasValue))
return false;
// 找到用户最大的过期时间
var maxExpireTime = group.Max(x => x.ExpireDateTime);
// 如果最大过期时间小于当前时间,说明用户已过期(比较日期,满足用户最后一天)
return maxExpireTime.HasValue && maxExpireTime.Value.Date < currentTime.Date;
})
.Select(group => group.Key)
.ToList();
if (!expiredUserIds.Any())
{
_logger.LogInformation("没有找到过期的VIP用户");
return null;
}
_logger.LogInformation($"找到 {expiredUserIds.Count} 个过期的VIP用户");
// 删除过期用户的Token密钥
var removedTokenCount = await _tokenRepository.DeleteAsync(x => expiredUserIds.Contains(x.UserId));
_logger.LogInformation($"成功删除 {removedTokenCount} 个用户的Token密钥");
_logger.LogInformation($"VIP过期自动卸载任务执行完成共处理 {expiredUserIds.Count} 个过期用户");
return expiredUserIds;
}
}

View File

@@ -0,0 +1,140 @@
using System.Text.Json;
using Volo.Abp.Domain.Services;
using Volo.Abp.Users;
using Yi.Framework.AiHub.Domain.Entities.Pay;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
/// <summary>
/// 支付管理器
/// </summary>
public class PayManager : DomainService
{
private readonly ISqlSugarRepository<PayNoticeRecordAggregateRoot, Guid> _payNoticeRepository;
private readonly ICurrentUser _currentUser;
private readonly ISqlSugarRepository<PayOrderAggregateRoot, Guid> _payOrderRepository;
public PayManager(
ISqlSugarRepository<PayNoticeRecordAggregateRoot, Guid> payNoticeRepository,
ICurrentUser currentUser, ISqlSugarRepository<PayOrderAggregateRoot, Guid> payOrderRepository)
{
_payNoticeRepository = payNoticeRepository;
_currentUser = currentUser;
_payOrderRepository = payOrderRepository;
}
/// <summary>
/// 创建订单
/// </summary>
/// <param name="goodsType">商品类型</param>
/// <returns>订单信息</returns>
public async Task<PayOrderAggregateRoot> CreateOrderAsync(GoodsTypeEnum goodsType)
{
// 验证用户是否登录
if (!_currentUser.IsAuthenticated)
{
throw new UserFriendlyException("用户未登录");
}
// 生成订单号
var outTradeNo = GenerateOutTradeNo();
// 获取商品信息
var goodsName = goodsType.GetDisplayName();
var totalAmount = goodsType.GetTotalAmount();
// 创建订单实体
var payOrder = new PayOrderAggregateRoot
{
OutTradeNo = outTradeNo,
UserId = _currentUser.GetId(),
UserName = _currentUser.UserName ?? string.Empty,
TotalAmount = totalAmount,
GoodsName = goodsName,
GoodsType = goodsType
};
// 保存订单
await _payOrderRepository.InsertAsync(payOrder);
return payOrder;
}
/// <summary>
/// 更新订单状态
/// </summary>
/// <param name="outTradeNo">商户订单号</param>
/// <param name="tradeStatus">交易状态</param>
/// <param name="tradeNo">支付宝交易号</param>
/// <returns></returns>
public async Task UpdateOrderStatusAsync(string outTradeNo, TradeStatusEnum tradeStatus, string? tradeNo = null)
{
var order = await _payOrderRepository.GetFirstAsync(x => x.OutTradeNo == outTradeNo);
if (order == null)
{
throw new UserFriendlyException($"订单不存在:{outTradeNo}");
}
order.TradeStatus = tradeStatus;
if (!string.IsNullOrEmpty(tradeNo))
{
order.TradeNo = tradeNo;
}
await _payOrderRepository.UpdateAsync(order);
}
/// <summary>
/// 记录支付通知
/// </summary>
/// <param name="notifyData">通知数据</param>
/// <param name="signStr"></param>
/// <returns></returns>
public async Task RecordPayNoticeAsync(Dictionary<string, string> notifyData, string signStr)
{
var payNotice = new PayNoticeRecordAggregateRoot
{
NotifyTime = DateTime.Parse(notifyData.GetValueOrDefault("notify_time", string.Empty)),
TradeNo = notifyData.GetValueOrDefault("trade_no", string.Empty),
OutTradeNo = notifyData.GetValueOrDefault("out_trade_no", string.Empty),
BuyerId = notifyData.GetValueOrDefault("buyer_id", string.Empty),
TradeStatus = ParseTradeStatus(notifyData.GetValueOrDefault("trade_status", string.Empty)),
TotalAmount = decimal.TryParse(notifyData.GetValueOrDefault("total_amount", "-1"), out var amount)
? amount
: 0,
NotifyData = JsonSerializer.Serialize(notifyData),
SignStr = signStr
};
await _payNoticeRepository.InsertAsync(payNotice);
}
/// <summary>
/// 生成商户订单号
/// </summary>
/// <returns></returns>
private string GenerateOutTradeNo()
{
return $"YI_{DateTime.Now:yyyyMMddHHmmss}_{Random.Shared.Next(1000, 9999)}";
}
/// <summary>
/// 解析交易状态
/// </summary>
/// <param name="tradeStatus">状态字符串</param>
/// <returns></returns>
private TradeStatusEnum ParseTradeStatus(string tradeStatus)
{
return tradeStatus switch
{
"WAIT_BUYER_PAY" => TradeStatusEnum.WAIT_BUYER_PAY,
"TRADE_SUCCESS" => TradeStatusEnum.TRADE_SUCCESS,
"TRADE_FINISHED" => TradeStatusEnum.TRADE_FINISHED,
"TRADE_CLOSED" => TradeStatusEnum.TRADE_CLOSED,
_ => TradeStatusEnum.WAIT_TRADE
};
}
}

View File

@@ -1,6 +1,7 @@
using Medallion.Threading;
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
@@ -17,9 +18,17 @@ public class UsageStatisticsManager : DomainService
private IDistributedLockProvider DistributedLock =>
LazyServiceProvider.LazyGetRequiredService<IDistributedLockProvider>();
public async Task SetUsageAsync(Guid userId, string modelId, int inputTokenCount, int outputTokenCount)
public async Task SetUsageAsync(Guid? userId, string modelId, ThorUsageResponse? tokenUsage)
{
await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId.ToString()}"))
long inputTokenCount = tokenUsage?.PromptTokens
?? tokenUsage.InputTokens
?? 0;
long outputTokenCount = tokenUsage?.CompletionTokens
?? tokenUsage.OutputTokens
?? 0;
await using (await DistributedLock.AcquireLockAsync($"UsageStatistics:{userId?.ToString()}"))
{
var entity = await _repository._DbQueryable.FirstAsync(x => x.UserId == userId && x.ModelId == modelId);
//存在数据,更细
@@ -37,8 +46,4 @@ public class UsageStatisticsManager : DomainService
}
}
}
}
internal class LazyServiceProvider
{
}

View File

@@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.props" />
<ItemGroup>
<PackageReference Include="AlipayEasySDK" Version="2.1.3" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.2.0-beta.4" />
<PackageReference Include="Volo.Abp.Ddd.Domain" Version="$(AbpVersion)" />
<PackageReference Include="Volo.Abp.DistributedLocking" Version="$(AbpVersion)" />
@@ -9,8 +10,11 @@
<ItemGroup>
<ProjectReference Include="..\..\..\framework\Yi.Framework.Mapster\Yi.Framework.Mapster.csproj" />
<ProjectReference Include="..\..\..\framework\Yi.Framework.SqlSugarCore.Abstractions\Yi.Framework.SqlSugarCore.Abstractions.csproj" />
<ProjectReference Include="..\Yi.Framework.AiHub.Application.Contracts\Yi.Framework.AiHub.Application.Contracts.csproj" />
<ProjectReference Include="..\Yi.Framework.AiHub.Domain.Shared\Yi.Framework.AiHub.Domain.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="AiGateWay\Impl\ThorSiliconFlow\" />
</ItemGroup>
</Project>

View File

@@ -1,9 +1,19 @@
using Alipay.EasySDK.Factory;
using Alipay.EasySDK.Kernel;
using Alipay.EasySDK.Kernel.Util;
using Alipay.EasySDK.Payment.FaceToFace.Models;
using Dm.util;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Domain;
using Yi.Framework.AiHub.Domain.AiGateWay;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureDatabricks.Chats;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats;
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings;
using Yi.Framework.AiHub.Domain.Shared;
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
using Yi.Framework.Mapster;
namespace Yi.Framework.AiHub.Domain
@@ -25,10 +35,21 @@ namespace Yi.Framework.AiHub.Domain
nameof(AzureOpenAiChatCompletionCompletionsService));
services.AddKeyedTransient<IChatCompletionService, AzureDatabricksChatCompletionsService>(
nameof(AzureDatabricksChatCompletionsService));
services.AddKeyedTransient<IChatCompletionService, DeepSeekChatCompletionsService>(
nameof(DeepSeekChatCompletionsService));
services.AddKeyedTransient<IImageService, AzureOpenAIServiceImageService>(
nameof(AzureOpenAIServiceImageService));
services.AddKeyedTransient<ITextEmbeddingService, SiliconFlowTextEmbeddingService>(
nameof(SiliconFlowTextEmbeddingService));
//ai模型特殊性兼容处理
Configure<SpecialCompatibleOptions>(options =>
{
options.Handles.add(request => { request.CompatibleCodeCompletion(); });
options.Handles.Add(request =>
{
if (request.Model == "o1")
@@ -36,12 +57,34 @@ namespace Yi.Framework.AiHub.Domain
request.Temperature = null;
}
});
options.Handles.Add(request =>
{
if (request.Model.StartsWith("o3-mini") || request.Model.StartsWith("o4-mini"))
{
request.MaxCompletionTokens = request.MaxTokens;
request.MaxTokens = null;
request.Temperature = null;
}
});
options.Handles.Add(request =>
{
if (request.Stream == true)
{
request.StreamOptions = new ThorStreamOptions()
{
IncludeUsage = true
};
}
});
});
//配置支付宝支付
var config = configuration.GetSection("Alipay").Get<Config>();
Factory.SetOptions(config);
}
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
{
var service = context.ServiceProvider;
}
}
}

View File

@@ -23,12 +23,13 @@ public class AccessLogMiddleware : IMiddleware, ITransientDependency
{
_accessLogNumber = 0;
}
internal static int GetAccessLogNumber()
{
return _accessLogNumber;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
await next(context);
@@ -64,28 +65,25 @@ public class AccessLogResetEventHandler : ILocalEventHandler<AccessLogResetArgs>
return redisEnabled.IsNullOrEmpty() || bool.Parse(redisEnabled);
}
}
//该事件由job定时10秒触发
public async Task HandleEventAsync(AccessLogResetArgs eventData)
{
if (EnableRedisCache)
{
//分布式锁
if (await RedisClient.SetNxAsync("AccessLogLock",true,TimeSpan.FromSeconds(5)))
if (await RedisClient.SetNxAsync("AccessLogLock", true, TimeSpan.FromSeconds(5)))
{
//自增长数
var incrNumber= AccessLogMiddleware.GetAccessLogNumber();
var incrNumber = AccessLogMiddleware.GetAccessLogNumber();
//立即重置,开始计算,方式丢失
AccessLogMiddleware.ResetAccessLogNumber();
if (incrNumber>0)
if (incrNumber > 0)
{
await RedisClient.IncrByAsync(
$"{CacheKeyPrefix}{AccessLogCacheConst.Key}:{DateTime.Now.Date:yyyyMMdd}", incrNumber);
}
}
}
}
}

View File

@@ -9,6 +9,12 @@ namespace Yi.Framework.Rbac.Application.Contracts.IServices
/// </summary>
public interface IRoleService : IYiCrudAppService<RoleGetOutputDto, RoleGetListOutputDto, Guid, RoleGetListInputVo, RoleCreateInputVo, RoleUpdateInputVo>
{
/// <summary>
/// 根据角色名称移除指定用户的角色
/// </summary>
/// <param name="userIds"></param>
/// <param name="roleName"></param>
/// <returns></returns>
Task<int> RemoveUserRoleByRoleCodeAsync(List<Guid> userIds, string roleName);
}
}

View File

@@ -8,5 +8,12 @@ namespace Yi.Framework.Rbac.Application.Contracts.IServices
/// </summary>
public interface IUserService : IYiCrudAppService<UserGetOutputDto, UserGetListOutputDto, Guid, UserGetListInputVo, UserCreateInputVo, UserUpdateInputVo>
{
/// <summary>
/// 通过角色代码给用户添加角色
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="roleCodes">角色代码</param>
/// <returns></returns>
Task AddUserRoleByRoleCodeAsync(Guid userId, List<string> roleCodes);
}
}

View File

@@ -2,9 +2,7 @@ using Mapster;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Uow;
using Yi.Framework.Ddd.Application;
using Yi.Framework.Rbac.Application.Contracts.Dtos.Role;
using Yi.Framework.Rbac.Application.Contracts.Dtos.User;
@@ -98,7 +96,8 @@ namespace Yi.Framework.Rbac.Application.Services.System
{
var entity = await _repository.GetByIdAsync(id);
var isExist = await _repository._DbQueryable.Where(x => x.Id != entity.Id).AnyAsync(x => x.RoleCode == input.RoleCode || x.RoleName == input.RoleName);
var isExist = await _repository._DbQueryable.Where(x => x.Id != entity.Id)
.AnyAsync(x => x.RoleCode == input.RoleCode || x.RoleName == input.RoleName);
if (isExist)
{
throw new UserFriendlyException(RoleConst.Exist);
@@ -213,7 +212,18 @@ namespace Yi.Framework.Rbac.Application.Services.System
await _userRoleRepository._Db.Deleteable<UserRoleEntity>().Where(x => x.RoleId == input.RoleId)
.Where(x => input.UserIds.Contains(x.UserId))
.ExecuteCommandAsync();
;
}
/// <summary>
/// 根据角色名称移除指定用户的角色
/// </summary>
/// <param name="userIds"></param>
/// <param name="roleCode"></param>
/// <returns></returns>
[RemoteService(isEnabled: false)]
public Task<int> RemoveUserRoleByRoleCodeAsync(List<Guid> userIds, string roleCode)
{
return _roleManager.RemoveUserRoleByRoleCodeAsync(userIds, roleCode);
}
}
}

View File

@@ -241,5 +241,29 @@ namespace Yi.Framework.Rbac.Application.Services.System
{
return base.PostImportExcelAsync(input);
}
/// <summary>
/// 通过角色代码给用户添加角色
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="roleCodes"></param>
/// <returns></returns>
public async Task AddUserRoleByRoleCodeAsync(Guid userId, List<string> roleCodes)
{
// 根据角色代码查找角色ID
var roleRepository = LazyServiceProvider.LazyGetRequiredService<ISqlSugarRepository<RoleAggregateRoot>>();
var roleIds = await roleRepository._DbQueryable
.Where(r => roleCodes.Contains(r.RoleCode) && r.State == true)
.Select(r=>r.Id)
.ToListAsync();
if (!roleIds.Any())
{
return;
}
// 使用UserManager给用户设置角色
await _userManager.GiveUserSetRoleAsync(new List<Guid> { userId }, roleIds);
}
}
}

View File

@@ -8,8 +8,8 @@
<ItemGroup>
<PackageReference Include="Lazy.Captcha.Core" Version="2.0.7" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.7" />
<PackageReference Include="Lazy.Captcha.Core" Version="2.2.2" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.88.9" />
<PackageReference Include="Volo.Abp.BackgroundJobs.Hangfire" Version="$(AbpVersion)" />
</ItemGroup>

View File

@@ -1,5 +1,8 @@
using Lazy.Captcha.Core.Generator;
using Lazy.Captcha.Core;
using Lazy.Captcha.Core.Generator;
using Lazy.Captcha.Core.Generator.Image.Option;
using Microsoft.Extensions.DependencyInjection;
using SkiaSharp;
using Yi.Framework.Ddd.Application;
using Yi.Framework.Rbac.Application.Contracts;
using Yi.Framework.Rbac.Domain;
@@ -21,7 +24,9 @@ namespace Yi.Framework.Rbac.Application
service.AddCaptcha(options =>
{
options.CaptchaType = CaptchaType.ARITHMETIC;
options.CaptchaType = CaptchaType.NUMBER;
options.ImageOption.BackgroundColor = SkiaSharp.SKColors.Transparent;
options.ImageOption.FontFamily = DefaultFontFamilies.Prefix;
});
}

View File

@@ -32,14 +32,15 @@ public class FileManager : DomainService, IFileManager
/// <exception cref="ArgumentException"></exception>
public async Task<List<FileAggregateRoot>> CreateAsync(IEnumerable<IFormFile> files)
{
if (files.Count() == 0)
var formFiles = files as IFormFile[] ?? files.ToArray();
if (!formFiles.Any())
{
throw new ArgumentException("文件上传为空!");
}
//批量插入
List<FileAggregateRoot> entities = new();
foreach (var file in files)
foreach (var file in formFiles)
{
FileAggregateRoot data = new(_guidGenerator.Create(), file.FileName, (decimal)file.Length / 1024);
data.CheckDirectoryOrCreate();

View File

@@ -8,10 +8,12 @@ namespace Yi.Framework.Rbac.Domain.Managers
{
private ISqlSugarRepository<RoleAggregateRoot> _repository;
private ISqlSugarRepository<RoleMenuEntity> _roleMenuRepository;
public RoleManager(ISqlSugarRepository<RoleAggregateRoot> repository, ISqlSugarRepository<RoleMenuEntity> roleMenuRepository)
private ISqlSugarRepository<UserRoleEntity> _userRoleRepository;
public RoleManager(ISqlSugarRepository<RoleAggregateRoot> repository, ISqlSugarRepository<RoleMenuEntity> roleMenuRepository, ISqlSugarRepository<UserRoleEntity> userRoleRepository)
{
_repository = repository;
_roleMenuRepository = roleMenuRepository;
_userRoleRepository = userRoleRepository;
}
/// <summary>
@@ -24,19 +26,45 @@ namespace Yi.Framework.Rbac.Domain.Managers
{
//这个是需要事务的在service中进行工作单元
await _roleMenuRepository.DeleteAsync(u => roleIds.Contains(u.RoleId));
//添加新的关系
List<RoleMenuEntity> roleMenuEntity = new();
//遍历用户
foreach (var roleId in roleIds)
{
//添加新的关系
List<RoleMenuEntity> roleMenuEntity = new();
foreach (var menu in menuIds)
{
roleMenuEntity.Add(new RoleMenuEntity() { RoleId = roleId, MenuId = menu });
}
//一次性批量添加
await _roleMenuRepository.InsertRangeAsync(roleMenuEntity);
}
//一次性批量添加
await _roleMenuRepository.InsertRangeAsync(roleMenuEntity);
}
/// <summary>
/// 根据角色名称移除指定用户的角色
/// </summary>
/// <param name="userIds">用户ID列表</param>
/// <param name="roleName">角色名称</param>
/// <returns>移除的角色关系数量</returns>
public async Task<int> RemoveUserRoleByRoleCodeAsync(List<Guid> userIds, string roleName)
{
// 获取角色ID
var role = await _repository._DbQueryable
.Where(x => x.RoleCode == roleName)
.FirstAsync();
if (role == null)
{
return 0;
}
// 移除用户角色关系
var removedCount = await _userRoleRepository._Db.Deleteable<UserRoleEntity>()
.Where(x => userIds.Contains(x.UserId) && x.RoleId == role.Id)
.ExecuteCommandAsync();
return removedCount;
}
}
}

View File

@@ -46,27 +46,25 @@ namespace Yi.Framework.Rbac.Domain.Managers
/// <param name="userIds"></param>
/// <param name="roleIds"></param>
/// <returns></returns>
public async Task GiveUserSetRoleAsync(List<Guid> userIds, List<Guid> roleIds)
public async Task GiveUserSetRoleAsync(List<Guid> userIds, List<Guid>? roleIds)
{
//删除用户之前所有的用户角色关系(物理删除,没有恢复的必要)
//删除用户之前所有的用户角色关系
await _repositoryUserRole.DeleteAsync(u => userIds.Contains(u.UserId));
if (roleIds is not null)
{
//添加新的关系
List<UserRoleEntity> userRoleEntities = new();
//遍历用户
foreach (var userId in userIds)
{
//添加新的关系
List<UserRoleEntity> userRoleEntities = new();
foreach (var roleId in roleIds)
{
userRoleEntities.Add(new UserRoleEntity() { UserId = userId, RoleId = roleId });
}
//一次性批量添加
await _repositoryUserRole.InsertRangeAsync(userRoleEntities);
}
//一次性批量添加
await _repositoryUserRole.InsertRangeAsync(userRoleEntities);
}
}
@@ -77,25 +75,24 @@ namespace Yi.Framework.Rbac.Domain.Managers
/// <param name="userIds"></param>
/// <param name="postIds"></param>
/// <returns></returns>
public async Task GiveUserSetPostAsync(List<Guid> userIds, List<Guid> postIds)
public async Task GiveUserSetPostAsync(List<Guid> userIds, List<Guid>? postIds)
{
//删除用户之前所有的用户角色关系(物理删除,没有恢复的必要)
await _repositoryUserPost.DeleteAsync(u => userIds.Contains(u.UserId));
if (postIds is not null)
{
//添加新的关系
List<UserPostEntity> userPostEntities = new();
//遍历用户
foreach (var userId in userIds)
{
//添加新的关系
List<UserPostEntity> userPostEntities = new();
foreach (var post in postIds)
{
userPostEntities.Add(new UserPostEntity() { UserId = userId, PostId = post });
}
//一次性批量添加
await _repositoryUserPost.InsertRangeAsync(userPostEntities);
}
//一次性批量添加
await _repositoryUserPost.InsertRangeAsync(userPostEntities);
}
}
@@ -137,10 +134,7 @@ namespace Yi.Framework.Rbac.Domain.Managers
public async Task SetDefautRoleAsync(Guid userId)
{
var role = await _roleRepository.GetFirstAsync(x => x.RoleCode == UserConst.DefaultRoleCode);
if (role is not null)
{
await GiveUserSetRoleAsync(new List<Guid> { userId }, new List<Guid> { role.Id });
}
await GiveUserSetRoleAsync(new List<Guid> { userId }, new List<Guid> { role.Id });
}
private void ValidateUserName(UserAggregateRoot input)

View File

@@ -1,6 +1,6 @@
@echo on
set SERVER_USER=ccnetcore
set SERVER_USER=root
set SERVER_IP=yxai.chat
set FILE_PATH=publish_02.zip
set REMOTE_PATH=/home/yi/build/publish_02.zip

View File

@@ -0,0 +1,32 @@
using Microsoft.Extensions.Logging;
using SqlSugar;
using Volo.Abp.BackgroundWorkers.Hangfire;
using Yi.Framework.AiHub.Application.Contracts.IServices;
using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.Rbac.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Abp.Web.Jobs.ai_hub;
/// <summary>
/// VIP过期自动卸载任务
/// </summary>
public class VipExpireJob : HangfireBackgroundWorkerBase
{
private readonly IRechargeService _rechargeService;
public VipExpireJob(IRechargeService rechargeService)
{
_rechargeService = rechargeService;
RecurringJobId = "VIP过期自动卸载";
// 每天凌晨0点执行一次
CronExpression = "0 0 0 * * ?";
}
public override async Task DoWorkAsync(CancellationToken cancellationToken = new CancellationToken())
{
await _rechargeService.RemoveVipRoleByExpireAsync();
}
}

View File

@@ -27,7 +27,7 @@ namespace Yi.Abp.Web.Jobs.ai_stock
if (probability < 2)
{
await _newsManager.GenerateNewsAsync();
// await _newsManager.GenerateNewsAsync();
}
}
}

Some files were not shown because too many files have changed in this diff Show More