feat: 完成系统监控页面

This commit is contained in:
chenchun
2026-01-05 15:44:48 +08:00
parent 6101ea46d3
commit b4a97e8b09
7 changed files with 43 additions and 178 deletions

View File

@@ -81,162 +81,4 @@ public class AiAccountService : ApplicationService
return output;
}
/// <summary>
/// 获取利润统计数据
/// </summary>
/// <param name="currentCost">当前成本(RMB)</param>
/// <returns></returns>
[Authorize]
[HttpGet("account/profit-statistics")]
public async Task<string> GetProfitStatisticsAsync([FromQuery] decimal currentCost)
{
if (CurrentUser.UserName != "Guo" && CurrentUser.UserName != "cc")
{
throw new UserFriendlyException("您暂无权限访问");
}
// 1. 获取尊享包总消耗和剩余库存
var premiumPackages = await _premiumPackageRepository._DbQueryable.ToListAsync();
long totalUsedTokens = premiumPackages.Sum(p => p.UsedTokens);
long totalRemainingTokens = premiumPackages.Sum(p => p.RemainingTokens);
// 2. 计算1亿Token成本
decimal costPerHundredMillion = totalUsedTokens > 0
? currentCost / (totalUsedTokens / 100000000m)
: 0;
// 3. 计算总成本(剩余+已使用的总成本)
long totalTokens = totalUsedTokens + totalRemainingTokens;
decimal totalCost = totalTokens > 0
? (totalTokens / 100000000m) * costPerHundredMillion
: 0;
// 4. 获取总收益(RechargeType=PremiumPackage的充值金额总和)
decimal totalRevenue = await _rechargeRepository._DbQueryable
.Where(x => x.RechargeType == Domain.Shared.Enums.RechargeTypeEnum.PremiumPackage)
.SumAsync(x => x.RechargeAmount);
// 5. 计算利润率
decimal profitRate = totalCost > 0
? (totalRevenue / totalCost - 1) * 100
: 0;
// 6. 按200售价计算成本
decimal costAt200Price = totalRevenue > 0
? (totalCost / totalRevenue) * 200
: 0;
// 7. 格式化输出
var today = DateTime.Now;
string dayOfWeek = today.ToString("dddd", new System.Globalization.CultureInfo("zh-CN"));
string weekDay = dayOfWeek switch
{
"星期一" => "周1",
"星期二" => "周2",
"星期三" => "周3",
"星期四" => "周4",
"星期五" => "周5",
"星期六" => "周6",
"星期日" => "周日",
_ => dayOfWeek
};
var result = $@"{today:M月d日} {weekDay}
尊享包已消耗({totalUsedTokens / 100000000m:F2}亿){totalUsedTokens}
尊享包剩余库存({totalRemainingTokens / 100000000m:F2}亿){totalRemainingTokens}
当前成本:{currentCost:F2}RMB
1亿Token成本:{costPerHundredMillion:F2} RMB=1亿 Token
总成本:{totalCost:F2} RMB
总收益:{totalRevenue:F2}RMB
利润率: {profitRate:F1}%
按200售价来算,成本在{costAt200Price:F2}";
return result;
}
public class TokenStatisticsInput
{
/// <summary>
/// 指定日期(当天零点)
/// </summary>
public DateTime Date { get; set; }
/// <summary>
/// 模型Id -> 1亿Token成本(RMB)
/// </summary>
public Dictionary<string, decimal> ModelCosts { get; set; } = new();
}
/// <summary>
/// 获取指定日期各模型Token统计
/// </summary>
[Authorize]
[HttpPost("account/token-statistics")]
public async Task<string> GetTokenStatisticsAsync([FromBody] TokenStatisticsInput input)
{
if (CurrentUser.UserName != "Guo" && CurrentUser.UserName != "cc")
{
throw new UserFriendlyException("您暂无权限访问");
}
if (input.ModelCosts is null || input.ModelCosts.Count == 0)
{
throw new UserFriendlyException("请提供模型成本配置");
}
var day = input.Date.Date;
var nextDay = day.AddDays(1);
var modelIds = input.ModelCosts.Keys.ToList();
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);
string weekDay = day.ToString("dddd", new CultureInfo("zh-CN")) switch
{
"星期一" => "周1",
"星期二" => "周2",
"星期三" => "周3",
"星期四" => "周4",
"星期五" => "周5",
"星期六" => "周6",
"星期日" => "周日",
_ => day.ToString("dddd", new CultureInfo("zh-CN"))
};
var sb = new StringBuilder();
sb.AppendLine($"{day:M月d日} {weekDay}");
foreach (var kvp in input.ModelCosts)
{
var modelId = kvp.Key;
var cost = kvp.Value;
modelStatDict.TryGetValue(modelId, out var stat);
long tokens = stat?.Tokens ?? 0;
long count = stat?.Count ?? 0;
decimal costPerHundredMillion = tokens > 0
? cost / (tokens / 100000000m)
: 0;
decimal tokensInWan = tokens / 10000m;
sb.AppendLine();
sb.AppendLine($"{modelId} 成本:【{cost:F2}RMB】 次数:【{count}次】 token【{tokensInWan:F0}w】 1亿token成本【{costPerHundredMillion:F2}RMB】");
}
return sb.ToString().TrimEnd();
}
}

View File

@@ -22,13 +22,13 @@ public class AnthropicStreamDto
[JsonIgnore]
public ThorUsageResponse TokenUsage => new ThorUsageResponse
{
PromptTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens,
InputTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens,
OutputTokens = Usage?.OutputTokens,
PromptTokens = Usage?.InputTokens??0 + Usage?.CacheCreationInputTokens??0 + Usage?.CacheReadInputTokens??0,
InputTokens = Usage?.InputTokens??0 + Usage?.CacheCreationInputTokens??0 + Usage?.CacheReadInputTokens??0,
OutputTokens = Usage?.OutputTokens??0,
InputTokensDetails = null,
CompletionTokens = Usage?.OutputTokens,
TotalTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens +
Usage?.OutputTokens,
CompletionTokens = Usage?.OutputTokens??0,
TotalTokens = Usage?.InputTokens??0 + Usage?.CacheCreationInputTokens??0 + Usage?.CacheReadInputTokens??0 +
Usage?.OutputTokens??0,
PromptTokensDetails = null,
CompletionTokensDetails = null
};
@@ -119,13 +119,13 @@ public class AnthropicChatCompletionDto
[JsonIgnore]
public ThorUsageResponse TokenUsage => new ThorUsageResponse
{
PromptTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens,
InputTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens,
OutputTokens = Usage?.OutputTokens,
PromptTokens = Usage?.InputTokens??0 + Usage?.CacheCreationInputTokens??0 + Usage?.CacheReadInputTokens??0,
InputTokens = Usage?.InputTokens??0 + Usage?.CacheCreationInputTokens??0 + Usage?.CacheReadInputTokens??0,
OutputTokens = Usage?.OutputTokens??0,
InputTokensDetails = null,
CompletionTokens = Usage?.OutputTokens,
TotalTokens = Usage?.InputTokens + Usage?.CacheCreationInputTokens + Usage?.CacheReadInputTokens +
Usage?.OutputTokens,
CompletionTokens = Usage?.OutputTokens??0,
TotalTokens = Usage?.InputTokens??0 + Usage?.CacheCreationInputTokens??0 + Usage?.CacheReadInputTokens??0 +
Usage?.OutputTokens??0,
PromptTokensDetails = null,
CompletionTokensDetails = null
};

View File

@@ -628,9 +628,13 @@ public class AiGateWayManager : DomainService
{
responseResult.Item2.SupplementalMultiplier(modelDescribe.Multiplier);
//message_start是为了保底机制
if (responseResult.Item1.Contains("message_delta") || responseResult.Item1.Contains("message_start"))
if (responseResult.Item1.Contains("message_delta") || responseResult.Item1.Contains("message_start")||responseResult.Item1.Contains("message_stop"))
{
tokenUsage = responseResult.Item2?.TokenUsage;
if (responseResult.Item2?.TokenUsage is not null||responseResult.Item2?.TokenUsage.TotalTokens>0)
{
tokenUsage = responseResult.Item2?.TokenUsage;
}
}
backupSystemContent.Append(responseResult.Item2?.Delta?.Text);