diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/ModelTokenStatisticsDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/ModelTokenStatisticsDto.cs new file mode 100644 index 00000000..df8ce5a0 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/ModelTokenStatisticsDto.cs @@ -0,0 +1,42 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics; + +/// +/// 模型Token统计DTO +/// +public class ModelTokenStatisticsDto +{ + /// + /// 模型ID + /// + public string ModelId { get; set; } + + /// + /// 模型名称 + /// + public string ModelName { get; set; } + + /// + /// Token消耗量 + /// + public long Tokens { get; set; } + + /// + /// Token消耗量(万) + /// + public decimal TokensInWan { get; set; } + + /// + /// 使用次数 + /// + public long Count { get; set; } + + /// + /// 成本(RMB) + /// + public decimal Cost { get; set; } + + /// + /// 1亿Token成本(RMB) + /// + public decimal CostPerHundredMillion { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/ProfitStatisticsInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/ProfitStatisticsInput.cs new file mode 100644 index 00000000..becae629 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/ProfitStatisticsInput.cs @@ -0,0 +1,12 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics; + +/// +/// 利润统计输入 +/// +public class ProfitStatisticsInput +{ + /// + /// 当前成本(RMB) + /// + public decimal CurrentCost { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/ProfitStatisticsOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/ProfitStatisticsOutput.cs new file mode 100644 index 00000000..e0061b76 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/ProfitStatisticsOutput.cs @@ -0,0 +1,62 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics; + +/// +/// 利润统计输出 +/// +public class ProfitStatisticsOutput +{ + /// + /// 日期 + /// + public string Date { get; set; } + + /// + /// 尊享包已消耗Token数(单位:个) + /// + public long TotalUsedTokens { get; set; } + + /// + /// 尊享包已消耗Token数(单位:亿) + /// + public decimal TotalUsedTokensInHundredMillion { get; set; } + + /// + /// 尊享包剩余库存Token数(单位:个) + /// + public long TotalRemainingTokens { get; set; } + + /// + /// 尊享包剩余库存Token数(单位:亿) + /// + public decimal TotalRemainingTokensInHundredMillion { get; set; } + + /// + /// 当前成本(RMB) + /// + public decimal CurrentCost { get; set; } + + /// + /// 1亿Token成本(RMB) + /// + public decimal CostPerHundredMillion { get; set; } + + /// + /// 总成本(RMB) + /// + public decimal TotalCost { get; set; } + + /// + /// 总收益(RMB) + /// + public decimal TotalRevenue { get; set; } + + /// + /// 利润率(%) + /// + public decimal ProfitRate { get; set; } + + /// + /// 按200售价计算的成本(RMB) + /// + public decimal CostAt200Price { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/TokenStatisticsInput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/TokenStatisticsInput.cs new file mode 100644 index 00000000..8b8d8ba6 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/TokenStatisticsInput.cs @@ -0,0 +1,12 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics; + +/// +/// Token统计输入 +/// +public class TokenStatisticsInput +{ + /// + /// 指定日期(当天零点) + /// + public DateTime Date { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/TokenStatisticsOutput.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/TokenStatisticsOutput.cs new file mode 100644 index 00000000..ad8cb76f --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/SystemStatistics/TokenStatisticsOutput.cs @@ -0,0 +1,17 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics; + +/// +/// Token统计输出 +/// +public class TokenStatisticsOutput +{ + /// + /// 日期 + /// + public string Date { get; set; } + + /// + /// 模型统计列表 + /// + public List ModelStatistics { get; set; } = new(); +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/ISystemUsageStatisticsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/ISystemUsageStatisticsService.cs new file mode 100644 index 00000000..1e1dceb0 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/ISystemUsageStatisticsService.cs @@ -0,0 +1,19 @@ +using Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics; + +namespace Yi.Framework.AiHub.Application.Contracts.IServices; + +/// +/// 系统使用量统计服务接口 +/// +public interface ISystemUsageStatisticsService +{ + /// + /// 获取利润统计数据 + /// + Task GetProfitStatisticsAsync(ProfitStatisticsInput input); + + /// + /// 获取指定日期各模型Token统计 + /// + Task GetTokenStatisticsAsync(TokenStatisticsInput input); +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/SystemUsageStatisticsService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/SystemUsageStatisticsService.cs new file mode 100644 index 00000000..49e99f77 --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/SystemUsageStatisticsService.cs @@ -0,0 +1,203 @@ +using Mapster; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using SqlSugar; +using System.Globalization; +using Volo.Abp.Application.Services; +using Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics; +using Yi.Framework.AiHub.Application.Contracts.IServices; +using Yi.Framework.AiHub.Domain.Entities; +using Yi.Framework.AiHub.Domain.Entities.Chat; +using Yi.Framework.AiHub.Domain.Entities.Model; +using Yi.Framework.SqlSugarCore.Abstractions; + +namespace Yi.Framework.AiHub.Application.Services; + +/// +/// 系统使用量统计服务实现 +/// +[Authorize(Roles = "admin")] +public class SystemUsageStatisticsService : ApplicationService, ISystemUsageStatisticsService +{ + private readonly ISqlSugarRepository _premiumPackageRepository; + private readonly ISqlSugarRepository _rechargeRepository; + private readonly ISqlSugarRepository _messageRepository; + private readonly ISqlSugarRepository _modelRepository; + + public SystemUsageStatisticsService( + ISqlSugarRepository premiumPackageRepository, + ISqlSugarRepository rechargeRepository, + ISqlSugarRepository messageRepository, + ISqlSugarRepository modelRepository) + { + _premiumPackageRepository = premiumPackageRepository; + _rechargeRepository = rechargeRepository; + _messageRepository = messageRepository; + _modelRepository = modelRepository; + } + + /// + /// 获取利润统计数据 + /// + [HttpPost("system-statistics/profit")] + public async Task GetProfitStatisticsAsync(ProfitStatisticsInput input) + { + // 1. 获取尊享包总消耗和剩余库存 + var premiumPackages = await _premiumPackageRepository._DbQueryable.ToListAsync(); + long totalUsedTokens = premiumPackages.Sum(p => p.UsedTokens); + long totalRemainingTokens = premiumPackages.Sum(p => p.RemainingTokens); + + // 2. 计算1亿Token成本 + decimal costPerHundredMillion = totalUsedTokens > 0 + ? input.CurrentCost / (totalUsedTokens / 100000000m) + : 0; + + // 3. 计算总成本(剩余+已使用的总成本) + long totalTokens = totalUsedTokens + totalRemainingTokens; + decimal totalCost = totalTokens > 0 + ? (totalTokens / 100000000m) * costPerHundredMillion + : 0; + + // 4. 获取总收益(RechargeType=PremiumPackage的充值金额总和) + decimal totalRevenue = await _rechargeRepository._DbQueryable + .Where(x => x.RechargeType == Domain.Shared.Enums.RechargeTypeEnum.PremiumPackage) + .SumAsync(x => x.RechargeAmount); + + // 5. 计算利润率 + decimal profitRate = totalCost > 0 + ? (totalRevenue / totalCost - 1) * 100 + : 0; + + // 6. 按200售价计算成本 + decimal costAt200Price = totalRevenue > 0 + ? (totalCost / totalRevenue) * 200 + : 0; + + // 7. 格式化日期 + var today = DateTime.Now; + string dayOfWeek = today.ToString("dddd", new CultureInfo("zh-CN")); + string weekDay = dayOfWeek switch + { + "星期一" => "周1", + "星期二" => "周2", + "星期三" => "周3", + "星期四" => "周4", + "星期五" => "周5", + "星期六" => "周6", + "星期日" => "周日", + _ => dayOfWeek + }; + + return new ProfitStatisticsOutput + { + Date = $"{today:M月d日} {weekDay}", + TotalUsedTokens = totalUsedTokens, + TotalUsedTokensInHundredMillion = totalUsedTokens / 100000000m, + TotalRemainingTokens = totalRemainingTokens, + TotalRemainingTokensInHundredMillion = totalRemainingTokens / 100000000m, + CurrentCost = input.CurrentCost, + CostPerHundredMillion = costPerHundredMillion, + TotalCost = totalCost, + TotalRevenue = totalRevenue, + ProfitRate = profitRate, + CostAt200Price = costAt200Price + }; + } + + /// + /// 获取指定日期各模型Token统计 + /// + [HttpPost("system-statistics/token")] + public async Task GetTokenStatisticsAsync(TokenStatisticsInput input) + { + var day = input.Date.Date; + var nextDay = day.AddDays(1); + + // 1. 获取所有尊享模型(包含被禁用的),按ModelId去重 + var premiumModels = await _modelRepository._DbQueryable + .Where(x => x.IsPremium) + .ToListAsync(); + + if (premiumModels.Count == 0) + { + return new TokenStatisticsOutput + { + Date = FormatDate(day), + ModelStatistics = new List() + }; + } + + // 按ModelId去重,保留第一个模型的名称 + var distinctModels = premiumModels + .GroupBy(x => x.ModelId) + .Select(g => g.First()) + .ToList(); + + var modelIds = distinctModels.Select(x => x.ModelId).ToList(); + + // 2. 查询指定日期内各模型的Token使用统计 + var modelStats = await _messageRepository._DbQueryable + .Where(x => modelIds.Contains(x.ModelId)) + .Where(x => x.CreationTime >= day && x.CreationTime < nextDay) + .Where(x => x.Role == "system") + .GroupBy(x => x.ModelId) + .Select(x => new + { + ModelId = x.ModelId, + Tokens = SqlFunc.AggregateSum(x.TokenUsage.TotalTokenCount), + Count = SqlFunc.AggregateCount(x.Id) + }) + .ToListAsync(); + + var modelStatDict = modelStats.ToDictionary(x => x.ModelId, x => x); + + // 3. 构建结果列表,使用去重后的模型列表 + var result = new List(); + foreach (var model in distinctModels) + { + modelStatDict.TryGetValue(model.ModelId, out var stat); + long tokens = stat?.Tokens ?? 0; + long count = stat?.Count ?? 0; + + // 这里成本设为0,因为需要前端传入或者从配置中获取 + decimal cost = 0; + decimal costPerHundredMillion = tokens > 0 && cost > 0 + ? cost / (tokens / 100000000m) + : 0; + + result.Add(new ModelTokenStatisticsDto + { + ModelId = model.ModelId, + ModelName = model.Name, + Tokens = tokens, + TokensInWan = tokens / 10000m, + Count = count, + Cost = cost, + CostPerHundredMillion = costPerHundredMillion + }); + } + + return new TokenStatisticsOutput + { + Date = FormatDate(day), + ModelStatistics = result + }; + } + + private string FormatDate(DateTime date) + { + string dayOfWeek = date.ToString("dddd", new CultureInfo("zh-CN")); + string weekDay = dayOfWeek switch + { + "星期一" => "周1", + "星期二" => "周2", + "星期三" => "周3", + "星期四" => "周4", + "星期五" => "周5", + "星期六" => "周6", + "星期日" => "周日", + _ => dayOfWeek + }; + return $"{date:M月d日} {weekDay}"; + } +} \ No newline at end of file diff --git a/Yi.Ai.Vue3/src/api/systemStatistics/index.ts b/Yi.Ai.Vue3/src/api/systemStatistics/index.ts new file mode 100644 index 00000000..ce731c77 --- /dev/null +++ b/Yi.Ai.Vue3/src/api/systemStatistics/index.ts @@ -0,0 +1,17 @@ +import { post } from '@/utils/request'; +import type { + ProfitStatisticsInput, + ProfitStatisticsOutput, + TokenStatisticsInput, + TokenStatisticsOutput, +} from './types'; + +// 获取利润统计数据 +export function getProfitStatistics(data: ProfitStatisticsInput) { + return post('/system-statistics/profit', data).json(); +} + +// 获取指定日期各模型Token统计 +export function getTokenStatistics(data: TokenStatisticsInput) { + return post('/system-statistics/token', data).json(); +} diff --git a/Yi.Ai.Vue3/src/api/systemStatistics/types.ts b/Yi.Ai.Vue3/src/api/systemStatistics/types.ts new file mode 100644 index 00000000..fcbe5eb6 --- /dev/null +++ b/Yi.Ai.Vue3/src/api/systemStatistics/types.ts @@ -0,0 +1,41 @@ +// 利润统计输入 +export interface ProfitStatisticsInput { + currentCost: number; +} + +// 利润统计输出 +export interface ProfitStatisticsOutput { + date: string; + totalUsedTokens: number; + totalUsedTokensInHundredMillion: number; + totalRemainingTokens: number; + totalRemainingTokensInHundredMillion: number; + currentCost: number; + costPerHundredMillion: number; + totalCost: number; + totalRevenue: number; + profitRate: number; + costAt200Price: number; +} + +// Token统计输入 +export interface TokenStatisticsInput { + date: string; +} + +// 模型Token统计DTO +export interface ModelTokenStatisticsDto { + modelId: string; + modelName: string; + tokens: number; + tokensInWan: number; + count: number; + cost: number; + costPerHundredMillion: number; +} + +// Token统计输出 +export interface TokenStatisticsOutput { + date: string; + modelStatistics: ModelTokenStatisticsDto[]; +} diff --git a/Yi.Ai.Vue3/src/pages/console/system-statistics/index.vue b/Yi.Ai.Vue3/src/pages/console/system-statistics/index.vue new file mode 100644 index 00000000..19c2c2a6 --- /dev/null +++ b/Yi.Ai.Vue3/src/pages/console/system-statistics/index.vue @@ -0,0 +1,219 @@ + + + + + + diff --git a/Yi.Ai.Vue3/types/import_meta.d.ts b/Yi.Ai.Vue3/types/import_meta.d.ts index 8f2a798b..c98d612e 100644 --- a/Yi.Ai.Vue3/types/import_meta.d.ts +++ b/Yi.Ai.Vue3/types/import_meta.d.ts @@ -7,6 +7,7 @@ interface ImportMetaEnv { readonly VITE_WEB_BASE_API: string; readonly VITE_API_URL: string; readonly VITE_FILE_UPLOAD_API: string; + readonly VITE_BUILD_COMPRESS: string; readonly VITE_SSO_SEVER_URL: string; readonly VITE_APP_VERSION: string; }