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
17beaa7b
authored
Sep 17, 2026
by
renyizhao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
价格展示修改
parent
763e8dce
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
168 additions
and
82 deletions
+168
-82
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/app/aimodel/AppAiModelController.java
+157
-80
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/app/aimodel/vo/AppModelPricingVO.java
+7
-0
computility-server/src/main/resources/application-local.yaml
+4
-2
No files found.
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/app/aimodel/AppAiModelController.java
View file @
17beaa7b
...
...
@@ -200,19 +200,6 @@ public class AppAiModelController {
vo
.
setDescription
((
String
)
item
.
get
(
"description"
));
vo
.
setTags
((
String
)
item
.
get
(
"tags"
));
// endpoints
Object
ep
=
item
.
get
(
"endpoints"
);
if
(
ep
instanceof
String
)
{
try
{
vo
.
setSupportedEndpoints
(
com
.
luhu
.
computility
.
framework
.
common
.
util
.
json
.
JsonUtils
.
parseArray
((
String
)
ep
,
String
.
class
));
}
catch
(
Exception
ignored
)
{
}
}
else
if
(
ep
instanceof
List
)
{
@SuppressWarnings
(
"unchecked"
)
List
<
String
>
eps
=
(
List
<
String
>)
ep
;
vo
.
setSupportedEndpoints
(
eps
);
}
// enable_groups
Object
eg
=
item
.
get
(
"enable_groups"
);
if
(
eg
instanceof
List
)
{
...
...
@@ -224,6 +211,14 @@ public class AppAiModelController {
// 合并 pricing
Map
<
String
,
Object
>
pricing
=
pricingIndex
.
get
(
modelName
);
if
(
pricing
!=
null
)
{
// supportedEndpoints:从 pricing 数据的 supported_endpoint_types 读取
// (projects memory 硬约束:不可为 null,来源于 NewAPI 的 supported_endpoint_types)
Object
pricingEps
=
pricing
.
get
(
"supported_endpoint_types"
);
if
(
pricingEps
instanceof
List
)
{
@SuppressWarnings
(
"unchecked"
)
List
<
String
>
pEps
=
(
List
<
String
>)
pricingEps
;
vo
.
setSupportedEndpoints
(
pEps
);
}
vo
.
setQuotaType
(
toInteger
(
pricing
.
get
(
"quota_type"
)));
vo
.
setModelRatio
(
toBigDecimal
(
pricing
.
get
(
"model_ratio"
)));
vo
.
setModelPrice
(
toBigDecimal
(
pricing
.
get
(
"model_price"
)));
...
...
@@ -549,6 +544,12 @@ public class AppAiModelController {
/**
* 解析分档计费表达式
* 格式: (条件1 ? tier("档位1", p*x + c*y + cr*a + cc*b + ...) : 条件2 ? tier("档位2", ...) : tier("档位3", ...)) * (条件 ? 倍率 : 1) * ...
*
* 使用括号深度匹配(而非简单正则)来正确支持参数内嵌的 u("field") 嵌套调用,
* 否则旧实现会把 u("tokens") 的内层 ) 误识别为外层 tier(...) 的闭合,导致:
* 1. params 截断为 u("tokens"
* 2. 后续档位 conditionDesc 包含前一档 params 的残骸
* 3. 最后一档(无 ? 的兜底档)被错误地给 null
*/
private
List
<
AppModelPricingVO
.
TierVO
>
parseTieredExpr
(
String
billingExpr
,
BigDecimal
groupRatio
)
{
List
<
AppModelPricingVO
.
TierVO
>
tiers
=
new
java
.
util
.
ArrayList
<>();
...
...
@@ -557,87 +558,63 @@ public class AppAiModelController {
}
try
{
// 找到最后一个 tier(...) 结束的位置(用于截取分档表达式部分)
int
lastTierEnd
=
-
1
;
int
searchStart
=
0
;
while
(
true
)
{
int
tierStart
=
billingExpr
.
indexOf
(
"tier("
,
searchStart
);
if
(
tierStart
==
-
1
)
break
;
int
depth
=
0
;
int
i
=
tierStart
;
for
(;
i
<
billingExpr
.
length
();
i
++)
{
char
ch
=
billingExpr
.
charAt
(
i
);
if
(
ch
==
'('
)
depth
++;
else
if
(
ch
==
')'
)
{
depth
--;
if
(
depth
==
0
)
{
lastTierEnd
=
i
+
1
;
break
;
}
}
}
searchStart
=
i
+
1
;
}
if
(
lastTierEnd
==
-
1
)
{
// 1. 用括号深度匹配找出所有 tier(...) 的精确 [start, end] 范围
List
<
int
[]>
tierRanges
=
findAllTierRanges
(
billingExpr
);
if
(
tierRanges
.
isEmpty
())
{
return
tiers
;
}
// 提取分档表达式部分(从开头到最后一个 tier(...) 结束)
// 截取到最后一个 tier(...) 结束,去掉尾部的条件乘数
int
lastTierEnd
=
tierRanges
.
get
(
tierRanges
.
size
()
-
1
)[
1
];
String
tierExpr
=
billingExpr
.
substring
(
0
,
lastTierEnd
);
// 匹配 tier("名称", 参数部分) 的正则
java
.
util
.
regex
.
Pattern
tierPattern
=
java
.
util
.
regex
.
Pattern
.
compile
(
"tier\\s*\\(\\s*\"([^\"]+)\"\\s*,\\s*((?:[^)]+))\\s*\\)"
);
java
.
util
.
regex
.
Matcher
tierMatcher
=
tierPattern
.
matcher
(
tierExpr
);
// 收集所有 tier 的位置和内容
java
.
util
.
List
<
java
.
util
.
regex
.
MatchResult
>
tierMatches
=
new
java
.
util
.
ArrayList
<>();
while
(
tierMatcher
.
find
())
{
tierMatches
.
add
(
tierMatcher
.
toMatchResult
());
}
if
(
tierMatches
.
isEmpty
())
{
// 2. 解析每个 tier 的标签和参数
List
<
TierInfo
>
tierInfos
=
new
java
.
util
.
ArrayList
<>();
for
(
int
[]
range
:
tierRanges
)
{
int
start
=
range
[
0
];
int
end
=
range
[
1
];
// tier(...) 内部的全部内容(去掉首尾括号)
String
inner
=
billingExpr
.
substring
(
start
+
"tier("
.
length
(),
end
).
trim
();
// 找顶层逗号(depth=0,不在字符串里)来切分 label 和 params
int
commaIdx
=
findTopLevelComma
(
inner
);
if
(
commaIdx
<
0
)
continue
;
String
label
=
inner
.
substring
(
0
,
commaIdx
).
trim
();
if
(
label
.
startsWith
(
"\""
)
&&
label
.
endsWith
(
"\""
))
{
label
=
label
.
substring
(
1
,
label
.
length
()
-
1
);
}
String
params
=
inner
.
substring
(
commaIdx
+
1
).
trim
();
tierInfos
.
add
(
new
TierInfo
(
start
,
end
,
label
,
params
));
}
if
(
tierInfos
.
isEmpty
())
{
return
tiers
;
}
// 遍历每个 tier,提取其对应的条件
for
(
int
i
=
0
;
i
<
tierMatches
.
size
();
i
++)
{
// 3. 遍历每个 tier,提取其条件描述
for
(
int
i
=
0
;
i
<
tierInfos
.
size
();
i
++)
{
TierInfo
info
=
tierInfos
.
get
(
i
);
AppModelPricingVO
.
TierVO
tier
=
new
AppModelPricingVO
.
TierVO
();
java
.
util
.
regex
.
MatchResult
mr
=
tierMatches
.
get
(
i
);
tier
.
setName
(
mr
.
group
(
1
));
tier
.
setName
(
info
.
label
);
// 解析参数部分,提取各倍率
String
params
=
mr
.
group
(
2
);
parseTierParams
(
tier
,
params
);
// 提取条件:从上一个 tier 结束(或表达式开头)到当前 tier 开始的 ? 之前
int
tierStart
=
mr
.
start
();
String
segment
;
if
(
i
==
0
)
{
// 第一个 tier:条件在表达式开头到 tier 之前
segment
=
tierExpr
.
substring
(
0
,
tierStart
);
}
else
{
// 后续 tier:条件在上一个 tier 结束后到当前 tier 之前
int
prevTierEnd
=
tierMatches
.
get
(
i
-
1
).
end
();
segment
=
tierExpr
.
substring
(
prevTierEnd
,
tierStart
);
}
// 找到 ? 的位置,? 之前是条件
int
questionMarkIndex
=
segment
.
lastIndexOf
(
'?'
);
if
(
questionMarkIndex
>
0
)
{
String
condition
=
segment
.
substring
(
0
,
questionMarkIndex
).
trim
();
// 清理:去掉开头的 ( : 和结尾的空格
condition
=
condition
.
replaceAll
(
"^[\\s(:)]+"
,
""
);
// 去掉首尾括号对
parseTierParams
(
tier
,
info
.
params
);
// 提取条件:在 (上一个 tier 结束 + 1) 到当前 tier 开始的段落里找顶层 ?
int
segStart
=
(
i
==
0
)
?
0
:
tierInfos
.
get
(
i
-
1
).
end
+
1
;
int
segEnd
=
info
.
start
;
String
segment
=
tierExpr
.
substring
(
segStart
,
segEnd
);
int
qIdx
=
findTopLevelQuestionMark
(
segment
);
if
(
qIdx
>=
0
)
{
String
condition
=
segment
.
substring
(
0
,
qIdx
).
trim
();
// 清理前导 ( : 和空白
condition
=
condition
.
replaceAll
(
"^[\\s(:]+"
,
""
);
// 去掉首尾成对的括号
while
(
condition
.
startsWith
(
"("
)
&&
condition
.
endsWith
(
")"
))
{
condition
=
condition
.
substring
(
1
,
condition
.
length
()
-
1
).
trim
();
}
tier
.
setConditionDesc
(
simplifyCondition
(
condition
));
}
else
if
(
i
==
tier
Matche
s
.
size
()
-
1
)
{
// 最后一个 tier 没有 ?
,是默认
档位
tier
.
setConditionDesc
(
null
);
}
else
if
(
i
==
tier
Info
s
.
size
()
-
1
)
{
// 最后一个 tier 没有 ?
: 出现,是默认兜底
档位
tier
.
setConditionDesc
(
"默认"
);
}
tiers
.
add
(
tier
);
...
...
@@ -649,14 +626,114 @@ public class AppAiModelController {
}
/**
* tier 解析过程的中间结果
*/
private
static
class
TierInfo
{
final
int
start
;
final
int
end
;
final
String
label
;
final
String
params
;
TierInfo
(
int
start
,
int
end
,
String
label
,
String
params
)
{
this
.
start
=
start
;
this
.
end
=
end
;
this
.
label
=
label
;
this
.
params
=
params
;
}
}
/**
* 用括号深度匹配找出表达式中所有 tier(...) 的精确起止位置。
* 兼容参数里嵌套的 u("field") 调用,避免被内层 ) 误截断。
*/
private
List
<
int
[]>
findAllTierRanges
(
String
expr
)
{
List
<
int
[]>
ranges
=
new
java
.
util
.
ArrayList
<>();
int
searchFrom
=
0
;
while
(
true
)
{
int
idx
=
expr
.
indexOf
(
"tier("
,
searchFrom
);
if
(
idx
<
0
)
break
;
// depth 初始为 1(外层 tier( 的左括号)
int
depth
=
1
;
int
end
=
-
1
;
for
(
int
i
=
idx
+
"tier("
.
length
();
i
<
expr
.
length
();
i
++)
{
char
ch
=
expr
.
charAt
(
i
);
if
(
ch
==
'('
)
depth
++;
else
if
(
ch
==
')'
)
{
depth
--;
if
(
depth
==
0
)
{
end
=
i
;
break
;
}
}
}
if
(
end
<
0
)
break
;
ranges
.
add
(
new
int
[]{
idx
,
end
});
searchFrom
=
end
+
1
;
}
return
ranges
;
}
/**
* 在字符串中找到深度=0 且不在引号内的第一个逗号位置(用于切分 tier(label, params))
*/
private
int
findTopLevelComma
(
String
inner
)
{
int
depth
=
0
;
boolean
inQuote
=
false
;
for
(
int
i
=
0
;
i
<
inner
.
length
();
i
++)
{
char
ch
=
inner
.
charAt
(
i
);
if
(
ch
==
'"'
)
{
inQuote
=
!
inQuote
;
}
else
if
(!
inQuote
)
{
if
(
ch
==
'('
)
depth
++;
else
if
(
ch
==
')'
)
depth
--;
else
if
(
ch
==
','
&&
depth
==
0
)
return
i
;
}
}
return
-
1
;
}
/**
* 在段落里找顶层 ? 的位置(深度=0、未在引号内)。
* 返回最右侧的顶层 ?,因为三元表达式段落里通常只有一个 ? 在末尾。
*/
private
int
findTopLevelQuestionMark
(
String
segment
)
{
int
depth
=
0
;
int
lastQ
=
-
1
;
boolean
inQuote
=
false
;
for
(
int
i
=
0
;
i
<
segment
.
length
();
i
++)
{
char
ch
=
segment
.
charAt
(
i
);
if
(
ch
==
'"'
)
{
inQuote
=
!
inQuote
;
}
else
if
(!
inQuote
)
{
if
(
ch
==
'('
)
depth
++;
else
if
(
ch
==
')'
)
depth
--;
else
if
(
ch
==
'?'
&&
depth
==
0
)
{
lastQ
=
i
;
}
}
}
return
lastQ
;
}
/**
* 解析 tier 参数部分,提取各扩展价格倍率
* 格式: p * x + c * y + cr * a + cc * b + cc1h * c1 + img * d + img_o * e + ai * f + ao * g
* 或 u("field") * X [/ 1000000] (seedance 风格按 token 计费,X 即每百万 token 价格)
*/
private
void
parseTierParams
(
AppModelPricingVO
.
TierVO
tier
,
String
params
)
{
if
(
params
==
null
||
params
.
isEmpty
())
{
return
;
}
// 提取 u("field") * X [/ 1000000](seedance 风格按 token 计费)
// 例如 u("tokens") * 29.14 / 1000000 => tokensMultiplier = 29.14
java
.
util
.
regex
.
Pattern
tokensPattern
=
java
.
util
.
regex
.
Pattern
.
compile
(
"u\\s*\\(\\s*\"(\\w+)\"\\s*\\)\\s*\\*\\s*([\\d.]+)\\s*(?:/\\s*1000000)?"
);
java
.
util
.
regex
.
Matcher
tokensMatcher
=
tokensPattern
.
matcher
(
params
);
if
(
tokensMatcher
.
find
())
{
tier
.
setTokensMultiplier
(
new
BigDecimal
(
tokensMatcher
.
group
(
2
)).
setScale
(
4
,
RoundingMode
.
HALF_UP
));
}
// 提取 p * x
java
.
util
.
regex
.
Pattern
pPattern
=
java
.
util
.
regex
.
Pattern
.
compile
(
"p\\s*\\*\\s*([\\d.]+)"
);
java
.
util
.
regex
.
Matcher
pMatcher
=
pPattern
.
matcher
(
params
);
...
...
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/app/aimodel/vo/AppModelPricingVO.java
View file @
17beaa7b
...
...
@@ -151,6 +151,13 @@ public class AppModelPricingVO {
private
BigDecimal
audioInputRatio
;
@Schema
(
description
=
"音频输出倍率"
)
private
BigDecimal
audioOutputRatio
;
/**
* 按 token 计费的倍率(seedance 风格 u("tokens") * X / 1000000 中的 X)。
* 适用于 openai-video 等按 token 数动态计费的档位,
* 单位 = 元/百万 token。前端展示时优先于 inputRatio/outputRatio。
*/
@Schema
(
description
=
"按 token 计费的倍率(seedance 风格,每百万 token 价格)"
)
private
BigDecimal
tokensMultiplier
;
}
/**
...
...
computility-server/src/main/resources/application-local.yaml
View file @
17beaa7b
...
...
@@ -212,8 +212,10 @@ wx:
# 芋道配置项,设置当前项目所有自定义的配置
computility
:
new-api
:
base-url
:
http://172.25.164.0:3000
admin-token
:
nfcJyVtYb6xtREHA2MyL4f4o5TDg/qU=
# 线上 NewAPI 地址(用于获取 /api/pricing 数据)
base-url
:
http://phslgld.hnluchuan.com:3000
# 线上 NewAPI 管理员 token(在 NewAPI 后台 → 用户管理 → 管理员账号详情获取)
admin-token
:
V33oMN4fI5H0wKSs0gXipXZ4WvU6DQ==
admin-user-id
:
1
captcha
:
enable
:
false
# 本地环境,暂时关闭图片验证码,方便登录等接口的测试;
...
...
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