Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
f3ae131f
authored
Jun 18, 2026
by
renyizhao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
分档计费与大屏
parent
a87eb950
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
359 additions
and
1 deletions
+359
-1
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/admin/newapi/AiTokenStatsController.java
+89
-0
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/app/aimodel/AppAiModelController.java
+87
-1
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/app/aimodel/vo/AppModelPricingVO.java
+37
-0
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/service/newapi/AiTokenService.java
+109
-0
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/service/newapi/NewApiClient.java
+37
-0
No files found.
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/admin/newapi/AiTokenStatsController.java
0 → 100644
View file @
f3ae131f
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
;
}
}
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/app/aimodel/AppAiModelController.java
View file @
f3ae131f
...
...
@@ -223,11 +223,26 @@ public class AppAiModelController {
vo
.
setModelRatio
(
toBigDecimal
(
pricing
.
get
(
"model_ratio"
)));
vo
.
setModelPrice
(
toBigDecimal
(
pricing
.
get
(
"model_price"
)));
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
modelRatio
=
vo
.
getModelRatio
()
!=
null
?
vo
.
getModelRatio
()
:
BigDecimal
.
ZERO
;
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
)
{
// 按次计费(quota_type=1):使用原始 group_ratio.default,不叠加 puhui 业务系数
...
...
@@ -237,8 +252,12 @@ public class AppAiModelController {
vo
.
setOutputPrice
(
perCallPrice
);
}
else
{
// 按量计费(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
));
// 缓存价格 = 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 {
}
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
;
}
}
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/app/aimodel/vo/AppModelPricingVO.java
View file @
f3ae131f
...
...
@@ -41,6 +41,12 @@ public class AppModelPricingVO {
@Schema
(
description
=
"补全价格倍率"
)
private
BigDecimal
completionRatio
;
@Schema
(
description
=
"缓存读取价格倍率"
)
private
BigDecimal
cacheRatio
;
@Schema
(
description
=
"缓存创建价格倍率"
)
private
BigDecimal
createCacheRatio
;
@Schema
(
description
=
"当前用户分组倍率"
)
private
BigDecimal
groupRatio
;
...
...
@@ -50,6 +56,12 @@ public class AppModelPricingVO {
@Schema
(
description
=
"输出价格(元/1k tokens),已乘 groupRatio"
)
private
BigDecimal
outputPrice
;
@Schema
(
description
=
"缓存读取价格(元/1k tokens),已乘 groupRatio"
)
private
BigDecimal
cacheReadPrice
;
@Schema
(
description
=
"缓存创建价格(元/1k tokens),已乘 groupRatio"
)
private
BigDecimal
cacheCreatePrice
;
@Schema
(
description
=
"状态:1-启用,0-禁用"
)
private
Integer
status
;
...
...
@@ -59,4 +71,29 @@ public class AppModelPricingVO {
@Schema
(
description
=
"模型标签,多个用逗号分隔"
)
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
;
}
}
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/service/newapi/AiTokenService.java
View file @
f3ae131f
...
...
@@ -10,6 +10,8 @@ import lombok.extern.slf4j.Slf4j;
import
org.springframework.stereotype.Service
;
import
javax.annotation.Resource
;
import
java.math.BigDecimal
;
import
java.util.List
;
import
java.util.Map
;
@Slf4j
...
...
@@ -213,4 +215,110 @@ public class AiTokenService {
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
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/service/newapi/NewApiClient.java
View file @
f3ae131f
...
...
@@ -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=
*
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment