feat: 兼容claude格式

This commit is contained in:
chenchun
2026-01-05 15:54:14 +08:00
parent b4a97e8b09
commit 29c1768ded
11 changed files with 645 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics;
/// <summary>
/// 模型Token统计DTO
/// </summary>
public class ModelTokenStatisticsDto
{
/// <summary>
/// 模型ID
/// </summary>
public string ModelId { get; set; }
/// <summary>
/// 模型名称
/// </summary>
public string ModelName { get; set; }
/// <summary>
/// Token消耗量
/// </summary>
public long Tokens { get; set; }
/// <summary>
/// Token消耗量(万)
/// </summary>
public decimal TokensInWan { get; set; }
/// <summary>
/// 使用次数
/// </summary>
public long Count { get; set; }
/// <summary>
/// 成本(RMB)
/// </summary>
public decimal Cost { get; set; }
/// <summary>
/// 1亿Token成本(RMB)
/// </summary>
public decimal CostPerHundredMillion { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics;
/// <summary>
/// 利润统计输入
/// </summary>
public class ProfitStatisticsInput
{
/// <summary>
/// 当前成本(RMB)
/// </summary>
public decimal CurrentCost { get; set; }
}

View File

@@ -0,0 +1,62 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics;
/// <summary>
/// 利润统计输出
/// </summary>
public class ProfitStatisticsOutput
{
/// <summary>
/// 日期
/// </summary>
public string Date { get; set; }
/// <summary>
/// 尊享包已消耗Token数(单位:个)
/// </summary>
public long TotalUsedTokens { get; set; }
/// <summary>
/// 尊享包已消耗Token数(单位:亿)
/// </summary>
public decimal TotalUsedTokensInHundredMillion { get; set; }
/// <summary>
/// 尊享包剩余库存Token数(单位:个)
/// </summary>
public long TotalRemainingTokens { get; set; }
/// <summary>
/// 尊享包剩余库存Token数(单位:亿)
/// </summary>
public decimal TotalRemainingTokensInHundredMillion { get; set; }
/// <summary>
/// 当前成本(RMB)
/// </summary>
public decimal CurrentCost { get; set; }
/// <summary>
/// 1亿Token成本(RMB)
/// </summary>
public decimal CostPerHundredMillion { get; set; }
/// <summary>
/// 总成本(RMB)
/// </summary>
public decimal TotalCost { get; set; }
/// <summary>
/// 总收益(RMB)
/// </summary>
public decimal TotalRevenue { get; set; }
/// <summary>
/// 利润率(%)
/// </summary>
public decimal ProfitRate { get; set; }
/// <summary>
/// 按200售价计算的成本(RMB)
/// </summary>
public decimal CostAt200Price { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics;
/// <summary>
/// Token统计输入
/// </summary>
public class TokenStatisticsInput
{
/// <summary>
/// 指定日期(当天零点)
/// </summary>
public DateTime Date { get; set; }
}

View File

@@ -0,0 +1,17 @@
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics;
/// <summary>
/// Token统计输出
/// </summary>
public class TokenStatisticsOutput
{
/// <summary>
/// 日期
/// </summary>
public string Date { get; set; }
/// <summary>
/// 模型统计列表
/// </summary>
public List<ModelTokenStatisticsDto> ModelStatistics { get; set; } = new();
}

View File

@@ -0,0 +1,19 @@
using Yi.Framework.AiHub.Application.Contracts.Dtos.SystemStatistics;
namespace Yi.Framework.AiHub.Application.Contracts.IServices;
/// <summary>
/// 系统使用量统计服务接口
/// </summary>
public interface ISystemUsageStatisticsService
{
/// <summary>
/// 获取利润统计数据
/// </summary>
Task<ProfitStatisticsOutput> GetProfitStatisticsAsync(ProfitStatisticsInput input);
/// <summary>
/// 获取指定日期各模型Token统计
/// </summary>
Task<TokenStatisticsOutput> GetTokenStatisticsAsync(TokenStatisticsInput input);
}

View File

@@ -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;
/// <summary>
/// 系统使用量统计服务实现
/// </summary>
[Authorize(Roles = "admin")]
public class SystemUsageStatisticsService : ApplicationService, ISystemUsageStatisticsService
{
private readonly ISqlSugarRepository<PremiumPackageAggregateRoot> _premiumPackageRepository;
private readonly ISqlSugarRepository<AiRechargeAggregateRoot> _rechargeRepository;
private readonly ISqlSugarRepository<MessageAggregateRoot> _messageRepository;
private readonly ISqlSugarRepository<AiModelEntity, Guid> _modelRepository;
public SystemUsageStatisticsService(
ISqlSugarRepository<PremiumPackageAggregateRoot> premiumPackageRepository,
ISqlSugarRepository<AiRechargeAggregateRoot> rechargeRepository,
ISqlSugarRepository<MessageAggregateRoot> messageRepository,
ISqlSugarRepository<AiModelEntity, Guid> modelRepository)
{
_premiumPackageRepository = premiumPackageRepository;
_rechargeRepository = rechargeRepository;
_messageRepository = messageRepository;
_modelRepository = modelRepository;
}
/// <summary>
/// 获取利润统计数据
/// </summary>
[HttpPost("system-statistics/profit")]
public async Task<ProfitStatisticsOutput> 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
};
}
/// <summary>
/// 获取指定日期各模型Token统计
/// </summary>
[HttpPost("system-statistics/token")]
public async Task<TokenStatisticsOutput> 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<ModelTokenStatisticsDto>()
};
}
// 按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<ModelTokenStatisticsDto>();
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}";
}
}

View File

@@ -0,0 +1,17 @@
import { post } from '@/utils/request';
import type {
ProfitStatisticsInput,
ProfitStatisticsOutput,
TokenStatisticsInput,
TokenStatisticsOutput,
} from './types';
// 获取利润统计数据
export function getProfitStatistics(data: ProfitStatisticsInput) {
return post<ProfitStatisticsOutput>('/system-statistics/profit', data).json();
}
// 获取指定日期各模型Token统计
export function getTokenStatistics(data: TokenStatisticsInput) {
return post<TokenStatisticsOutput>('/system-statistics/token', data).json();
}

View File

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

View File

@@ -0,0 +1,219 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { ElMessage } from 'element-plus';
import { Refresh } from '@element-plus/icons-vue';
import { getProfitStatistics, getTokenStatistics } from '@/api/systemStatistics';
import type { ProfitStatisticsOutput, TokenStatisticsOutput } from '@/api/systemStatistics/types';
// ==================== 利润统计 ====================
const profitLoading = ref(false);
const profitData = ref<ProfitStatisticsOutput | null>(null);
const currentCost = ref<number>(0);
// 获取利润统计
async function fetchProfitStatistics() {
if (!currentCost.value || currentCost.value <= 0) {
ElMessage.warning('请输入当前成本');
return;
}
profitLoading.value = true;
try {
const res = await getProfitStatistics({
currentCost: currentCost.value,
});
profitData.value = res.data;
}
catch (error: any) {
ElMessage.error(error.message || '获取利润统计失败');
}
finally {
profitLoading.value = false;
}
}
// ==================== Token统计 ====================
const tokenLoading = ref(false);
const tokenData = ref<TokenStatisticsOutput | null>(null);
const selectedDate = ref<string>(new Date().toISOString().split('T')[0]);
// 获取Token统计
async function fetchTokenStatistics() {
if (!selectedDate.value) {
ElMessage.warning('请选择日期');
return;
}
tokenLoading.value = true;
try {
const res = await getTokenStatistics({
date: selectedDate.value,
});
tokenData.value = res.data;
}
catch (error: any) {
ElMessage.error(error.message || '获取Token统计失败');
}
finally {
tokenLoading.value = false;
}
}
// 初始化
onMounted(() => {
// 页面加载时不自动获取数据,等待用户输入参数后手动获取
});
</script>
<template>
<div class="system-statistics">
<div class="statistics-container">
<!-- 利润统计卡片 -->
<el-card class="statistics-card">
<template #header>
<div class="card-header">
<h3>利润统计</h3>
<el-button type="primary" size="small" :icon="Refresh" :loading="profitLoading" @click="fetchProfitStatistics">
查询
</el-button>
</div>
</template>
<div class="input-section">
<el-form :inline="true">
<el-form-item label="当前成本(RMB)">
<el-input-number
v-model="currentCost"
:min="0"
:precision="2"
placeholder="请输入当前成本"
style="width: 200px"
/>
</el-form-item>
</el-form>
</div>
<div v-if="profitData" v-loading="profitLoading" class="data-display">
<el-descriptions :column="2" border>
<el-descriptions-item label="日期">{{ profitData.date }}</el-descriptions-item>
<el-descriptions-item label="当前成本">{{ profitData.currentCost.toFixed(2) }} RMB</el-descriptions-item>
<el-descriptions-item label="尊享包已消耗">
{{ profitData.totalUsedTokensInHundredMillion.toFixed(2) }}亿 ({{ profitData.totalUsedTokens }})
</el-descriptions-item>
<el-descriptions-item label="尊享包剩余库存">
{{ profitData.totalRemainingTokensInHundredMillion.toFixed(2) }}亿 ({{ profitData.totalRemainingTokens }})
</el-descriptions-item>
<el-descriptions-item label="1亿Token成本">
{{ profitData.costPerHundredMillion.toFixed(2) }} RMB
</el-descriptions-item>
<el-descriptions-item label="总成本">
{{ profitData.totalCost.toFixed(2) }} RMB
</el-descriptions-item>
<el-descriptions-item label="总收益">
{{ profitData.totalRevenue.toFixed(2) }} RMB
</el-descriptions-item>
<el-descriptions-item label="利润率">
<el-tag :type="profitData.profitRate > 0 ? 'success' : 'danger'">
{{ profitData.profitRate.toFixed(1) }}%
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="按200售价计算成本" :span="2">
{{ profitData.costAt200Price.toFixed(2) }} RMB
</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
<!-- Token统计卡片 -->
<el-card class="statistics-card">
<template #header>
<div class="card-header">
<h3>Token统计</h3>
<el-button type="primary" size="small" :icon="Refresh" :loading="tokenLoading" @click="fetchTokenStatistics">
查询
</el-button>
</div>
</template>
<div class="input-section">
<el-form :inline="true">
<el-form-item label="选择日期">
<el-date-picker
v-model="selectedDate"
type="date"
placeholder="选择日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 200px"
/>
</el-form-item>
</el-form>
</div>
<div v-if="tokenData" v-loading="tokenLoading" class="data-display">
<div class="date-info">
<h4>{{ tokenData.date }}</h4>
</div>
<el-table :data="tokenData.modelStatistics" border stripe>
<el-table-column prop="modelName" label="模型名称" min-width="150" />
<el-table-column prop="modelId" label="模型ID" min-width="200" show-overflow-tooltip />
<el-table-column label="Token消耗" min-width="120">
<template #default="{ row }">
{{ row.tokensInWan.toFixed(0) }}w
</template>
</el-table-column>
<el-table-column prop="count" label="使用次数" width="100" />
</el-table>
</div>
</el-card>
</div>
</div>
</template>
<style scoped lang="scss">
.system-statistics {
padding: 20px;
height: 100vh;
overflow-y: auto;
.statistics-container {
max-width: 1400px;
margin: 0 auto;
}
.statistics-card {
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
}
.input-section {
margin-bottom: 20px;
}
.data-display {
.date-info {
margin-bottom: 16px;
h4 {
margin: 0;
font-size: 14px;
color: #606266;
}
}
}
}
}
</style>

View File

@@ -7,6 +7,7 @@ interface ImportMetaEnv {
readonly VITE_WEB_BASE_API: string; readonly VITE_WEB_BASE_API: string;
readonly VITE_API_URL: string; readonly VITE_API_URL: string;
readonly VITE_FILE_UPLOAD_API: string; readonly VITE_FILE_UPLOAD_API: string;
readonly VITE_BUILD_COMPRESS: string;
readonly VITE_SSO_SEVER_URL: string; readonly VITE_SSO_SEVER_URL: string;
readonly VITE_APP_VERSION: string; readonly VITE_APP_VERSION: string;
} }