Commit f3ae131f by renyizhao

分档计费与大屏

parent a87eb950
package com.luhu.computility.module.apihub.controller.admin.newapi;
import com.luhu.computility.framework.common.pojo.CommonResult;
import com.luhu.computility.module.apihub.service.newapi.AiTokenService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@Slf4j
@RestController
@Tag(name = "管理后台 - AI Token 统计")
@RequestMapping("/admin-api/apihub/ai-token-stats")
public class AiTokenStatsController {
@Resource
private AiTokenService aiTokenService;
@GetMapping("/user-stats")
@Operation(summary = "获取用户日志统计数据")
@PreAuthorize("@ss.hasPermission('admin:apihub:ai-token-stats:query')")
public CommonResult<AiTokenStatsVO> getUserStats(
@RequestParam(value = "startDate", required = false) String startDate,
@RequestParam(value = "endDate", required = false) String endDate) {
log.info("[getUserStats] startDate={}, endDate={}", startDate, endDate);
// 默认从 2026-06-11 到当前时间
long startTimestamp;
long endTimestamp;
if (startDate != null && !startDate.isEmpty()) {
startTimestamp = LocalDate.parse(startDate, DateTimeFormatter.ISO_DATE)
.atStartOfDay(ZoneId.systemDefault())
.toEpochSecond();
} else {
// 默认起始日期:2026-06-11
startTimestamp = LocalDate.of(2026, 6, 11)
.atStartOfDay(ZoneId.systemDefault())
.toEpochSecond();
}
if (endDate != null && !endDate.isEmpty()) {
endTimestamp = LocalDate.parse(endDate, DateTimeFormatter.ISO_DATE)
.atStartOfDay(ZoneId.systemDefault())
.toEpochSecond();
} else {
// 默认截止时间:当前时间
endTimestamp = Instant.now().getEpochSecond();
}
log.info("[getUserStats] 转换后时间戳: startTimestamp={}, endTimestamp={}", startTimestamp, endTimestamp);
AiTokenService.AiTokenStatsResult result = aiTokenService.getUserStats(startTimestamp, endTimestamp);
if (!Boolean.TRUE.equals(result.getSuccess())) {
return CommonResult.error(1, result.getMessage());
}
AiTokenStatsVO vo = new AiTokenStatsVO();
vo.setStatsCount(result.getStatsCount());
vo.setStatsQuota(result.getStatsQuota());
vo.setStatsTokens(result.getStatsTokens());
return CommonResult.success(vo);
}
@lombok.Data
public static class AiTokenStatsVO {
@io.swagger.v3.oas.annotations.media.Schema(description = "统计次数")
private Integer statsCount;
@io.swagger.v3.oas.annotations.media.Schema(description = "统计额度(元)")
private java.math.BigDecimal statsQuota;
@io.swagger.v3.oas.annotations.media.Schema(description = "统计Tokens")
private Long statsTokens;
}
}
...@@ -223,11 +223,26 @@ public class AppAiModelController { ...@@ -223,11 +223,26 @@ public class AppAiModelController {
vo.setModelRatio(toBigDecimal(pricing.get("model_ratio"))); vo.setModelRatio(toBigDecimal(pricing.get("model_ratio")));
vo.setModelPrice(toBigDecimal(pricing.get("model_price"))); vo.setModelPrice(toBigDecimal(pricing.get("model_price")));
vo.setCompletionRatio(toBigDecimal(pricing.get("completion_ratio"))); vo.setCompletionRatio(toBigDecimal(pricing.get("completion_ratio")));
vo.setCacheRatio(toBigDecimal(pricing.get("cache_ratio")));
vo.setCreateCacheRatio(toBigDecimal(pricing.get("create_cache_ratio")));
// 分档计费信息
String billingMode = (String) pricing.get("billing_mode");
String billingExpr = (String) pricing.get("billing_expr");
vo.setBillingMode(billingMode);
vo.setBillingExpr(billingExpr);
// 解析分档表达式
if ("tiered_expr".equals(billingMode) && billingExpr != null) {
List<AppModelPricingVO.TierVO> tiers = parseTieredExpr(billingExpr, groupRatio);
vo.setTiers(tiers);
}
// 计算价格 // 计算价格
BigDecimal modelPrice = vo.getModelPrice() != null ? vo.getModelPrice() : BigDecimal.ZERO; BigDecimal modelPrice = vo.getModelPrice() != null ? vo.getModelPrice() : BigDecimal.ZERO;
BigDecimal modelRatio = vo.getModelRatio() != null ? vo.getModelRatio() : BigDecimal.ZERO; BigDecimal modelRatio = vo.getModelRatio() != null ? vo.getModelRatio() : BigDecimal.ZERO;
BigDecimal completionRatio = vo.getCompletionRatio() != null ? vo.getCompletionRatio() : BigDecimal.ONE; BigDecimal completionRatio = vo.getCompletionRatio() != null ? vo.getCompletionRatio() : BigDecimal.ONE;
BigDecimal cacheRatio = vo.getCacheRatio() != null ? vo.getCacheRatio() : BigDecimal.ZERO;
BigDecimal createCacheRatio = vo.getCreateCacheRatio() != null ? vo.getCreateCacheRatio() : BigDecimal.ZERO;
if (vo.getQuotaType() != null && vo.getQuotaType() == 1) { if (vo.getQuotaType() != null && vo.getQuotaType() == 1) {
// 按次计费(quota_type=1):使用原始 group_ratio.default,不叠加 puhui 业务系数 // 按次计费(quota_type=1):使用原始 group_ratio.default,不叠加 puhui 业务系数
...@@ -237,8 +252,12 @@ public class AppAiModelController { ...@@ -237,8 +252,12 @@ public class AppAiModelController {
vo.setOutputPrice(perCallPrice); vo.setOutputPrice(perCallPrice);
} else { } else {
// 按量计费(quota_type=0):应用 groupRatio(含 BUSINESS_RATIO) // 按量计费(quota_type=0):应用 groupRatio(含 BUSINESS_RATIO)
vo.setInputPrice(modelPrice.add(modelRatio).multiply(groupRatio).setScale(2, RoundingMode.HALF_UP)); BigDecimal inputPrice = modelPrice.add(modelRatio).multiply(groupRatio).setScale(2, RoundingMode.HALF_UP);
vo.setInputPrice(inputPrice);
vo.setOutputPrice(modelPrice.add(modelRatio.multiply(completionRatio)).multiply(groupRatio).setScale(2, RoundingMode.HALF_UP)); vo.setOutputPrice(modelPrice.add(modelRatio.multiply(completionRatio)).multiply(groupRatio).setScale(2, RoundingMode.HALF_UP));
// 缓存价格 = cache_ratio × 输入价格(输入价格已包含 groupRatio)
vo.setCacheReadPrice(cacheRatio.multiply(inputPrice).setScale(2, RoundingMode.HALF_UP));
vo.setCacheCreatePrice(createCacheRatio.multiply(inputPrice).setScale(2, RoundingMode.HALF_UP));
} }
} }
...@@ -448,4 +467,71 @@ public class AppAiModelController { ...@@ -448,4 +467,71 @@ public class AppAiModelController {
} }
return null; return null;
} }
/**
* 解析分档计费表达式
* 格式: len <= 200000 && p > 200000 ? tier("base", p * 1 + c * 1.2) : c > 200000 && p > 200000 ? tier("第2档", p * 0.8 + c * 0.9) : tier("第3档", p * 0.5 + c * 0.6)
*/
private List<AppModelPricingVO.TierVO> parseTieredExpr(String billingExpr, BigDecimal groupRatio) {
List<AppModelPricingVO.TierVO> tiers = new java.util.ArrayList<>();
if (billingExpr == null || billingExpr.isEmpty()) {
return tiers;
}
try {
// 正则匹配 tier("名称", p * 输入倍率 + c * 输出倍率)
java.util.regex.Pattern tierPattern = java.util.regex.Pattern.compile(
"tier\\s*\\(\\s*\"([^\"]+)\"\\s*,\\s*p\\s*\\*\\s*([\\d.]+)\\s*\\+\\s*c\\s*\\*\\s*([\\d.]+)\\s*\\)"
);
java.util.regex.Matcher tierMatcher = tierPattern.matcher(billingExpr);
// 提取档位信息
while (tierMatcher.find()) {
AppModelPricingVO.TierVO tier = new AppModelPricingVO.TierVO();
tier.setName(tierMatcher.group(1));
// 动态计费的价格倍率已经是最终值,不需要乘以 groupRatio
BigDecimal inputRatio = new BigDecimal(tierMatcher.group(2)).setScale(4, RoundingMode.HALF_UP);
BigDecimal outputRatio = new BigDecimal(tierMatcher.group(3)).setScale(4, RoundingMode.HALF_UP);
tier.setInputRatio(inputRatio);
tier.setOutputRatio(outputRatio);
tiers.add(tier);
}
// 解析条件描述
if (tiers.isEmpty()) {
return tiers;
}
// 按 : 分割,每一段是 条件? tier(...) 的格式
String[] segments = billingExpr.split(":");
int tierIndex = 0;
for (String segment : segments) {
// 找 tier(...) 前的条件(?前面的部分)
int questionMarkIndex = segment.lastIndexOf('?');
if (questionMarkIndex > 0 && tierIndex < tiers.size()) {
String conditionDesc = segment.substring(0, questionMarkIndex).trim();
// 简化条件描述
conditionDesc = conditionDesc
.replaceAll("\\s+&&\\s+", " 且 ")
.replaceAll("\\s+\\|\\|\\s+", " 或 ")
.replace("len", "长度")
.replace(" p ", " 输入 ")
.replace(" c ", " 输出 ")
.replace("p >", "输入 >")
.replace("p <", "输入 <")
.replace("p >=", "输入 >=")
.replace("p <=", "输入 <=")
.replace("c >", "输出 >")
.replace("c <", "输出 <")
.replace("c >=", "输出 >=")
.replace("c <=", "输出 <=");
tiers.get(tierIndex).setConditionDesc(conditionDesc);
tierIndex++;
}
}
} catch (Exception e) {
log.warn("[parseTieredExpr] 解析分档表达式失败:{}", billingExpr, e);
}
return tiers;
}
} }
...@@ -41,6 +41,12 @@ public class AppModelPricingVO { ...@@ -41,6 +41,12 @@ public class AppModelPricingVO {
@Schema(description = "补全价格倍率") @Schema(description = "补全价格倍率")
private BigDecimal completionRatio; private BigDecimal completionRatio;
@Schema(description = "缓存读取价格倍率")
private BigDecimal cacheRatio;
@Schema(description = "缓存创建价格倍率")
private BigDecimal createCacheRatio;
@Schema(description = "当前用户分组倍率") @Schema(description = "当前用户分组倍率")
private BigDecimal groupRatio; private BigDecimal groupRatio;
...@@ -50,6 +56,12 @@ public class AppModelPricingVO { ...@@ -50,6 +56,12 @@ public class AppModelPricingVO {
@Schema(description = "输出价格(元/1k tokens),已乘 groupRatio") @Schema(description = "输出价格(元/1k tokens),已乘 groupRatio")
private BigDecimal outputPrice; private BigDecimal outputPrice;
@Schema(description = "缓存读取价格(元/1k tokens),已乘 groupRatio")
private BigDecimal cacheReadPrice;
@Schema(description = "缓存创建价格(元/1k tokens),已乘 groupRatio")
private BigDecimal cacheCreatePrice;
@Schema(description = "状态:1-启用,0-禁用") @Schema(description = "状态:1-启用,0-禁用")
private Integer status; private Integer status;
...@@ -59,4 +71,29 @@ public class AppModelPricingVO { ...@@ -59,4 +71,29 @@ public class AppModelPricingVO {
@Schema(description = "模型标签,多个用逗号分隔") @Schema(description = "模型标签,多个用逗号分隔")
private String tags; private String tags;
@Schema(description = "计费模式:tiered_expr-分档计费")
private String billingMode;
@Schema(description = "分档计费表达式")
private String billingExpr;
@Schema(description = "分档列表")
private List<TierVO> tiers;
/**
* 分档信息 VO
*/
@Data
@Schema(description = "分档信息")
public static class TierVO {
@Schema(description = "档位名称")
private String name;
@Schema(description = "输入倍率")
private BigDecimal inputRatio;
@Schema(description = "输出倍率")
private BigDecimal outputRatio;
@Schema(description = "档位条件描述")
private String conditionDesc;
}
} }
...@@ -10,6 +10,8 @@ import lombok.extern.slf4j.Slf4j; ...@@ -10,6 +10,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map; import java.util.Map;
@Slf4j @Slf4j
...@@ -213,4 +215,110 @@ public class AiTokenService { ...@@ -213,4 +215,110 @@ public class AiTokenService {
private String newApiPassword; private String newApiPassword;
} }
/**
* 获取用户日志统计数据
*
* @param startTimestamp 起始时间戳(秒)
* @param endTimestamp 截止时间戳(秒)
* @return 统计数据(统计次数、统计额度、统计Tokens)
*/
public AiTokenStatsResult getUserStats(Long startTimestamp, Long endTimestamp) {
log.info("[AiTokenService.getUserStats] startTimestamp={}, endTimestamp={}", startTimestamp, endTimestamp);
try {
NewApiResponse<Map<String, Object>> resp = newApiClient.getUserStats(startTimestamp, endTimestamp);
log.info("[AiTokenService.getUserStats] resp success={}, message={}", resp.getSuccess(), resp.getMessage());
if (!Boolean.TRUE.equals(resp.getSuccess()) || resp.getData() == null) {
return AiTokenStatsResult.builder()
.success(false)
.message("获取统计数据失败: " + resp.getMessage())
.build();
}
// 解析 data 字段(可能是列表)
Map<String, Object> data = resp.getData();
Object dataField = data.get("data");
if (dataField == null) {
return AiTokenStatsResult.builder()
.success(true)
.statsCount(0)
.statsQuota(BigDecimal.ZERO)
.statsTokens(0L)
.build();
}
// data 字段是列表
List<Map<String, Object>> items;
if (dataField instanceof List) {
@SuppressWarnings("unchecked")
List<Map<String, Object>> rawItems = (List<Map<String, Object>>) dataField;
items = rawItems;
} else {
return AiTokenStatsResult.builder()
.success(true)
.statsCount(0)
.statsQuota(BigDecimal.ZERO)
.statsTokens(0L)
.build();
}
// 汇总统计
long totalCount = 0;
long totalTokens = 0;
long totalQuota = 0;
for (Map<String, Object> item : items) {
// count 字段
Object countObj = item.get("count");
if (countObj instanceof Number) {
totalCount += ((Number) countObj).longValue();
}
// token_used 字段
Object tokensObj = item.get("token_used");
if (tokensObj instanceof Number) {
totalTokens += ((Number) tokensObj).longValue();
}
// quota 字段(单位是 1/500000 元)
Object quotaObj = item.get("quota");
if (quotaObj instanceof Number) {
totalQuota += ((Number) quotaObj).longValue();
}
}
// quota 转换为元(除以 500000)
BigDecimal statsQuota = BigDecimal.valueOf(totalQuota)
.divide(BigDecimal.valueOf(500000), 2, BigDecimal.ROUND_HALF_UP);
log.info("[AiTokenService.getUserStats] 汇总结果: count={}, tokens={}, quota={}", totalCount, totalTokens, statsQuota);
return AiTokenStatsResult.builder()
.success(true)
.statsCount((int) totalCount)
.statsTokens(totalTokens)
.statsQuota(statsQuota)
.build();
} catch (Exception e) {
log.error("[AiTokenService.getUserStats] 异常", e);
return AiTokenStatsResult.builder()
.success(false)
.message("获取统计数据异常: " + e.getMessage())
.build();
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class AiTokenStatsResult {
private Boolean success;
private String message;
private Integer statsCount; // 统计次数
private BigDecimal statsQuota; // 统计额度(元)
private Long statsTokens; // 统计Tokens
}
} }
\ No newline at end of file
...@@ -406,6 +406,43 @@ public class NewApiClient { ...@@ -406,6 +406,43 @@ public class NewApiClient {
} }
/** /**
* 获取用户日志统计(管理员接口)
* GET /api/data/users?start_timestamp=&end_timestamp=
*
* @param startTimestamp 起始时间戳
* @param endTimestamp 截止时间戳
*/
public NewApiResponse<Map<String, Object>> getUserStats(Long startTimestamp, Long endTimestamp) {
StringBuilder url = new StringBuilder(newApiProperties.getBaseUrl())
.append("/api/data/users?");
if (startTimestamp != null) {
url.append("start_timestamp=").append(startTimestamp).append("&");
}
if (endTimestamp != null) {
url.append("end_timestamp=").append(endTimestamp);
}
log.info("[NewApiClient.getUserStats] 请求URL: {}", url);
try {
HttpResponse response = HttpRequest.get(url.toString())
.header(HEADER_USER, newApiProperties.getAdminUserId())
.header(HEADER_AUTH, "Bearer " + newApiProperties.getAdminToken())
.timeout(30000)
.execute();
log.info("[NewApiClient.getUserStats] 响应状态: {}", response.getStatus());
return parseMapResponse(response);
} catch (Exception e) {
log.error("[NewApiClient.getUserStats] 请求异常", e);
return NewApiResponse.<Map<String, Object>>builder()
.success(false)
.message("请求异常: " + e.getMessage())
.httpResponse(null)
.build();
}
}
/**
* 获取个人日志 * 获取个人日志
* GET /api/log/self?p=1&page_size=10&type=0&model_name=&start_timestamp=&end_timestamp= * GET /api/log/self?p=1&page_size=10&type=0&model_name=&start_timestamp=&end_timestamp=
* *
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment