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
4f0c132c
authored
Sep 14, 2026
by
renyizhao
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
赠送金額相关修改
parent
913503d3
Hide whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
281 additions
and
6 deletions
+281
-6
computility-module-apihub/computility-module-apihub-api/src/main/java/com/luhu/computility/module/apihub/api/newapi/NewApiUserApi.java
+25
-0
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/api/NewApiUserApiServiceImpl.java
+58
-0
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/admin/newapi/AiTokenStatsController.java
+4
-0
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/service/newapi/AiTokenService.java
+97
-4
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/service/newapi/NewApiClient.java
+48
-0
computility-module-member/src/main/java/com/luhu/computility/module/member/service/recharge/RechargeAgreementPdfService.java
+46
-1
computility-module-member/src/main/resources/fonts/NotoSansSC-Regular.ttf
+0
-0
computility-module-member/src/main/resources/templates/recharge-agreement.html
+3
-1
No files found.
computility-module-apihub/computility-module-apihub-api/src/main/java/com/luhu/computility/module/apihub/api/newapi/NewApiUserApi.java
0 → 100644
View file @
4f0c132c
package
com
.
luhu
.
computility
.
module
.
apihub
.
api
.
newapi
;
import
java.math.BigDecimal
;
/**
* New API 用户相关 API 接口
*
* <p>供其他模块(member 等)通过 RPC 调用 apihub 模块查询 New API 网关用户信息。
*
* @author luhu
*/
public
interface
NewApiUserApi
{
/**
* 获取 New API 网关上指定用户的当前余额(单位:元,保留 2 位小数)。
*
* <p>失败时(如 token 失效、网络异常)返回 {@code null},由调用方自行决定 fallback。
*
* @param accessToken New API 网关用户 access_token
* @param newApiUserId New API 网关用户 ID
* @return 余额(元),失败返回 {@code null}
*/
BigDecimal
getQuotaYuan
(
String
accessToken
,
Integer
newApiUserId
);
}
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/api/NewApiUserApiServiceImpl.java
0 → 100644
View file @
4f0c132c
package
com
.
luhu
.
computility
.
module
.
apihub
.
api
;
import
com.luhu.computility.module.apihub.api.newapi.NewApiUserApi
;
import
com.luhu.computility.module.apihub.controller.admin.newapi.NewApiResponse
;
import
com.luhu.computility.module.apihub.service.newapi.NewApiClient
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Service
;
import
javax.annotation.Resource
;
import
java.math.BigDecimal
;
import
java.math.RoundingMode
;
import
java.util.Map
;
/**
* {@link NewApiUserApi} 实现,封装 New API 网关用户余额查询。
*
* <p>复用现有 {@link NewApiClient#getUserInfo}(quota 单位 1元 = 500000),
* 在此层把 quota 转换成元(保留 2 位小数)。
*
* @author luhu
*/
@Slf4j
@Service
public
class
NewApiUserApiServiceImpl
implements
NewApiUserApi
{
/** New API 配额单位:1 元 = 500000 quota */
private
static
final
BigDecimal
QUOTA_PER_YUAN
=
new
BigDecimal
(
"500000"
);
@Resource
private
NewApiClient
newApiClient
;
@Override
public
BigDecimal
getQuotaYuan
(
String
accessToken
,
Integer
newApiUserId
)
{
if
(
accessToken
==
null
||
accessToken
.
isEmpty
()
||
newApiUserId
==
null
)
{
return
null
;
}
try
{
NewApiResponse
<
Map
<
String
,
Object
>>
resp
=
newApiClient
.
getUserInfo
(
accessToken
,
newApiUserId
);
if
(
resp
==
null
||
!
Boolean
.
TRUE
.
equals
(
resp
.
getSuccess
())
||
resp
.
getData
()
==
null
)
{
log
.
warn
(
"[NewApiUserApi.getQuotaYuan] New API 返回失败 newApiUserId={} message={}"
,
newApiUserId
,
resp
==
null
?
"null"
:
resp
.
getMessage
());
return
null
;
}
Object
quotaObj
=
resp
.
getData
().
get
(
"quota"
);
if
(!(
quotaObj
instanceof
Number
))
{
log
.
warn
(
"[NewApiUserApi.getQuotaYuan] quota 字段缺失或非数值 newApiUserId={} quota={}"
,
newApiUserId
,
quotaObj
);
return
null
;
}
long
quota
=
((
Number
)
quotaObj
).
longValue
();
return
BigDecimal
.
valueOf
(
quota
)
.
divide
(
QUOTA_PER_YUAN
,
2
,
RoundingMode
.
HALF_UP
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[NewApiUserApi.getQuotaYuan] 查询 New API 余额异常 newApiUserId={}"
,
newApiUserId
,
e
);
return
null
;
}
}
}
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/controller/admin/newapi/AiTokenStatsController.java
View file @
4f0c132c
...
...
@@ -71,6 +71,7 @@ public class AiTokenStatsController {
vo
.
setStatsCount
(
result
.
getStatsCount
());
vo
.
setStatsQuota
(
result
.
getStatsQuota
());
vo
.
setStatsTokens
(
result
.
getStatsTokens
());
vo
.
setStatsGiftAmount
(
result
.
getStatsGiftAmount
());
return
CommonResult
.
success
(
vo
);
}
...
...
@@ -85,5 +86,8 @@ public class AiTokenStatsController {
@io
.
swagger
.
v3
.
oas
.
annotations
.
media
.
Schema
(
description
=
"统计Tokens"
)
private
Long
statsTokens
;
@io
.
swagger
.
v3
.
oas
.
annotations
.
media
.
Schema
(
description
=
"赠送金额(元)"
)
private
java
.
math
.
BigDecimal
statsGiftAmount
;
}
}
computility-module-apihub/computility-module-apihub-biz/src/main/java/com/luhu/computility/module/apihub/service/newapi/AiTokenService.java
View file @
4f0c132c
...
...
@@ -12,8 +12,11 @@ import org.springframework.stereotype.Service;
import
javax.annotation.Resource
;
import
java.math.BigDecimal
;
import
java.math.RoundingMode
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.regex.Matcher
;
import
java.util.regex.Pattern
;
@Slf4j
@Service
...
...
@@ -248,6 +251,7 @@ public class AiTokenService {
.
statsCount
(
0
)
.
statsQuota
(
BigDecimal
.
ZERO
)
.
statsTokens
(
0L
)
.
statsGiftAmount
(
safeGetGiftAmount
(
startTimestamp
,
endTimestamp
))
.
build
();
}
...
...
@@ -263,6 +267,7 @@ public class AiTokenService {
.
statsCount
(
0
)
.
statsQuota
(
BigDecimal
.
ZERO
)
.
statsTokens
(
0L
)
.
statsGiftAmount
(
safeGetGiftAmount
(
startTimestamp
,
endTimestamp
))
.
build
();
}
...
...
@@ -293,7 +298,7 @@ public class AiTokenService {
// quota 转换为元(除以 500000)
BigDecimal
statsQuota
=
BigDecimal
.
valueOf
(
totalQuota
)
.
divide
(
BigDecimal
.
valueOf
(
500000
),
2
,
BigDecimal
.
ROUND_
HALF_UP
);
.
divide
(
BigDecimal
.
valueOf
(
500000
),
2
,
RoundingMode
.
HALF_UP
);
log
.
info
(
"[AiTokenService.getUserStats] 汇总结果: count={}, tokens={}, quota={}"
,
totalCount
,
totalTokens
,
statsQuota
);
...
...
@@ -302,6 +307,7 @@ public class AiTokenService {
.
statsCount
((
int
)
totalCount
)
.
statsTokens
(
totalTokens
)
.
statsQuota
(
statsQuota
)
.
statsGiftAmount
(
safeGetGiftAmount
(
startTimestamp
,
endTimestamp
))
.
build
();
}
catch
(
Exception
e
)
{
...
...
@@ -313,6 +319,19 @@ public class AiTokenService {
}
}
/**
* 安全调用 getAdminGiftAmount,失败时返回 0
* 主统计失败时不应被 gift 调用拖垮
*/
private
BigDecimal
safeGetGiftAmount
(
Long
startTimestamp
,
Long
endTimestamp
)
{
try
{
return
getAdminGiftAmount
(
startTimestamp
,
endTimestamp
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[safeGetGiftAmount] 赠送金额查询失败,降级返回 0"
,
e
);
return
BigDecimal
.
ZERO
;
}
}
@Data
@Builder
@NoArgsConstructor
...
...
@@ -320,8 +339,81 @@ public class AiTokenService {
public
static
class
AiTokenStatsResult
{
private
Boolean
success
;
private
String
message
;
private
Integer
statsCount
;
// 统计次数
private
BigDecimal
statsQuota
;
// 统计额度(元)
private
Long
statsTokens
;
// 统计Tokens
private
Integer
statsCount
;
// 统计次数
private
BigDecimal
statsQuota
;
// 统计额度(元)
private
Long
statsTokens
;
// 统计Tokens
private
BigDecimal
statsGiftAmount
;
// 赠送金额(元,管理员增加的用户额度)
}
/**
* 统计全平台"管理员增加用户额度"的总金额(元)
* 通过 admin 调 New API /api/log?type=3,按 content 匹配 "管理员增加用户额度 ¥xxx 额度" 累加
* 注意:type=3 的记录 quota 字段为 0,金额必须从 content 提取
*
* @param startTimestamp 起始时间戳(秒)
* @param endTimestamp 截止时间戳(秒)
* @return 赠送总额(元,保留 2 位小数)
*/
public
BigDecimal
getAdminGiftAmount
(
Long
startTimestamp
,
Long
endTimestamp
)
{
final
int
pageSize
=
100
;
final
int
maxPages
=
200
;
// 安全上限,防止 total 异常时死循环
// 严格匹配 "管理员增加用户额度 ¥金额 额度",避免误匹配其他 type=3 记录
final
Pattern
pattern
=
Pattern
.
compile
(
"^管理员增加用户额度\\s+¥(\\d+(?:\\.\\d+)?)\\s+额度$"
);
BigDecimal
totalGiftYuan
=
BigDecimal
.
ZERO
;
for
(
int
page
=
1
;
page
<=
maxPages
;
page
++)
{
NewApiResponse
<
Map
<
String
,
Object
>>
resp
=
newApiClient
.
getAdminLog
(
page
,
pageSize
,
3
,
startTimestamp
,
endTimestamp
);
if
(!
Boolean
.
TRUE
.
equals
(
resp
.
getSuccess
())
||
resp
.
getData
()
==
null
)
{
log
.
warn
(
"[getAdminGiftAmount] 第 {} 页请求失败: {}"
,
page
,
resp
.
getMessage
());
break
;
}
// /api/log 响应 data 结构:{page, page_size, total, items: [...]}
Map
<
String
,
Object
>
data
=
resp
.
getData
();
@SuppressWarnings
(
"unchecked"
)
List
<
Map
<
String
,
Object
>>
items
=
(
List
<
Map
<
String
,
Object
>>)
data
.
get
(
"items"
);
int
total
=
0
;
Object
totalObj
=
data
.
get
(
"total"
);
if
(
totalObj
instanceof
Number
)
{
total
=
((
Number
)
totalObj
).
intValue
();
}
if
(
items
==
null
||
items
.
isEmpty
())
{
log
.
info
(
"[getAdminGiftAmount] 第 {} 页无数据,结束"
,
page
);
break
;
}
for
(
Map
<
String
,
Object
>
item
:
items
)
{
Object
contentObj
=
item
.
get
(
"content"
);
if
(!(
contentObj
instanceof
String
))
continue
;
Matcher
matcher
=
pattern
.
matcher
((
String
)
contentObj
);
if
(
matcher
.
matches
())
{
try
{
BigDecimal
amount
=
new
BigDecimal
(
matcher
.
group
(
1
));
totalGiftYuan
=
totalGiftYuan
.
add
(
amount
);
}
catch
(
NumberFormatException
nfe
)
{
log
.
warn
(
"[getAdminGiftAmount] 金额解析失败: {}"
,
matcher
.
group
(
1
));
}
}
}
log
.
info
(
"[getAdminGiftAmount] 第 {} 页: items={}, total={}, 当前累加={} 元"
,
page
,
items
.
size
(),
total
,
totalGiftYuan
);
// 终止条件 1:已拉满 total
if
(
total
>
0
&&
page
*
pageSize
>=
total
)
{
break
;
}
// 终止条件 2:本次返回不足一页(最后一页)
if
(
items
.
size
()
<
pageSize
)
{
break
;
}
}
log
.
info
(
"[getAdminGiftAmount] 赠送总额: {} 元"
,
totalGiftYuan
);
return
totalGiftYuan
.
setScale
(
2
,
RoundingMode
.
HALF_UP
);
}
}
\ 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 @
4f0c132c
...
...
@@ -609,6 +609,54 @@ public class NewApiClient {
}
}
/**
* 获取全平台日志(管理员专用)
* GET /api/log?p=1&page_size=10&type=0&model_name=&start_timestamp=&end_timestamp=
*
* 响应 data 结构:{page, page_size, total, items: [...]}
* 与 /api/log/self 的区别:用 admin token + admin userId 头;可查看所有用户的日志
*
* @param page 页数
* @param pageSize 每页条数
* @param type 0: 全部 1: 充值 2: 消费 3: 管理
* @param startTimestamp 起始时间戳(可空)
* @param endTimestamp 截止时间戳(可空)
*/
public
NewApiResponse
<
Map
<
String
,
Object
>>
getAdminLog
(
Integer
page
,
Integer
pageSize
,
Integer
type
,
Long
startTimestamp
,
Long
endTimestamp
)
{
StringBuilder
url
=
new
StringBuilder
(
newApiProperties
.
getBaseUrl
())
.
append
(
"/api/log/?"
)
.
append
(
"p="
).
append
(
page
==
null
?
1
:
page
)
.
append
(
"&page_size="
).
append
(
pageSize
==
null
?
10
:
pageSize
)
.
append
(
"&type="
).
append
(
type
==
null
?
0
:
type
)
.
append
(
"&model_name="
);
if
(
startTimestamp
!=
null
)
{
url
.
append
(
"&start_timestamp="
).
append
(
startTimestamp
);
}
if
(
endTimestamp
!=
null
)
{
url
.
append
(
"&end_timestamp="
).
append
(
endTimestamp
);
}
log
.
info
(
"[NewApiClient.getAdminLog] 请求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.getAdminLog] 响应状态: {}"
,
response
.
getStatus
());
return
parseMapResponse
(
response
);
}
catch
(
Exception
e
)
{
log
.
error
(
"[NewApiClient.getAdminLog] 请求异常"
,
e
);
return
NewApiResponse
.<
Map
<
String
,
Object
>>
builder
()
.
success
(
false
)
.
message
(
"请求异常: "
+
e
.
getMessage
())
.
httpResponse
(
null
)
.
build
();
}
}
@SuppressWarnings
(
"unchecked"
)
private
NewApiResponse
<
Map
<
String
,
Object
>>
parseMapResponse
(
HttpResponse
response
)
{
try
{
...
...
computility-module-member/src/main/java/com/luhu/computility/module/member/service/recharge/RechargeAgreementPdfService.java
View file @
4f0c132c
package
com
.
luhu
.
computility
.
module
.
member
.
service
.
recharge
;
import
com.luhu.computility.module.apihub.api.newapi.NewApiUserApi
;
import
com.luhu.computility.module.infra.api.file.FileApi
;
import
com.luhu.computility.module.member.dal.dataobject.recharge.MemberRechargeDO
;
import
com.luhu.computility.module.member.dal.dataobject.user.MemberUserDO
;
...
...
@@ -17,6 +18,8 @@ import java.io.ByteArrayOutputStream;
import
java.io.File
;
import
java.io.FileOutputStream
;
import
java.io.InputStream
;
import
java.math.BigDecimal
;
import
java.math.RoundingMode
;
import
java.nio.charset.StandardCharsets
;
import
java.time.LocalDate
;
import
java.time.format.DateTimeFormatter
;
...
...
@@ -43,6 +46,10 @@ public class RechargeAgreementPdfService {
private
static
final
String
PLACEHOLDER_AGREEMENT_DATE
=
"${agreementDate}"
;
/** HTML 模板里字体栈占位符(实际加载的字体 family name 在首位) */
private
static
final
String
PLACEHOLDER_FONT_FAMILY
=
"${fontFamilyStack}"
;
/** HTML 模板里本次充值金额占位符(单位:元,保留 2 位小数) */
private
static
final
String
PLACEHOLDER_RECHARGE_AMOUNT
=
"${rechargeAmount}"
;
/** HTML 模板里余额合计占位符(单位:元,保留 2 位小数) */
private
static
final
String
PLACEHOLDER_TOTAL_BALANCE
=
"${totalBalance}"
;
/** 兜底字体栈(实际加载的字体 family name 会被插入到第一位) */
private
static
final
String
FALLBACK_FONT_STACK
=
...
...
@@ -53,6 +60,7 @@ public class RechargeAgreementPdfService {
private
final
MemberRechargeMapper
rechargeMapper
;
private
final
MemberUserService
memberUserService
;
private
final
FileApi
fileApi
;
private
final
NewApiUserApi
newApiUserApi
;
/**
* 生成并保存协议 PDF。
...
...
@@ -88,6 +96,20 @@ public class RechargeAgreementPdfService {
?
recharge
.
getCreateTime
().
format
(
DATE_FORMATTER
)
:
LocalDate
.
now
().
format
(
DATE_FORMATTER
);
// 本次充值金额(元,保留 2 位小数)
BigDecimal
rechargeAmount
=
recharge
.
getAmount
()
!=
null
?
recharge
.
getAmount
().
setScale
(
2
,
RoundingMode
.
HALF_UP
)
:
BigDecimal
.
ZERO
.
setScale
(
2
);
// 余额合计 = 充值前 New API 余额 + 本次充值金额
// 协议是在点击"确认充值"那一刻签署的(此时支付未完成、New API 余额也未到账),
// 这里的"余额合计"按用户口径取预期到账后总额。
// New API 余额查询失败时降级为只显示本次充值金额。
BigDecimal
currentBalance
=
queryNewApiBalanceYuan
(
user
);
BigDecimal
totalBalance
=
(
currentBalance
!=
null
?
currentBalance
:
BigDecimal
.
ZERO
)
.
add
(
rechargeAmount
)
.
setScale
(
2
,
RoundingMode
.
HALF_UP
);
// 2. 尝试加载中文字体,读出真实 family name 用于 HTML font-family
LoadedFont
loadedFont
=
loadChineseFont
();
String
fontFamilyStack
=
loadedFont
==
null
...
...
@@ -98,7 +120,9 @@ public class RechargeAgreementPdfService {
String
html
=
loadTemplate
()
.
replace
(
PLACEHOLDER_USER_NICKNAME
,
escapeHtml
(
userNickname
))
.
replace
(
PLACEHOLDER_AGREEMENT_DATE
,
escapeHtml
(
agreementDate
))
.
replace
(
PLACEHOLDER_FONT_FAMILY
,
fontFamilyStack
);
.
replace
(
PLACEHOLDER_FONT_FAMILY
,
fontFamilyStack
)
.
replace
(
PLACEHOLDER_RECHARGE_AMOUNT
,
rechargeAmount
.
toPlainString
())
.
replace
(
PLACEHOLDER_TOTAL_BALANCE
,
totalBalance
.
toPlainString
());
// 4. HTML → PDF
byte
[]
pdfBytes
=
renderHtmlToPdf
(
html
,
loadedFont
);
...
...
@@ -126,6 +150,27 @@ public class RechargeAgreementPdfService {
// ============ 私有工具方法 ============
/**
* 查询用户在 New API 网关上的当前余额(元)。
*
* <p>用户未拿到 New API access_token(如 newapi_user_id / newapi_access_token 为空)
* 或查询失败时返回 {@code null},由调用方降级处理。
*/
private
BigDecimal
queryNewApiBalanceYuan
(
MemberUserDO
user
)
{
if
(
user
==
null
||
user
.
getNewapiAccessToken
()
==
null
||
user
.
getNewapiAccessToken
().
isEmpty
()
||
user
.
getNewapiUserId
()
==
null
)
{
return
null
;
}
try
{
Integer
newApiUserId
=
Integer
.
parseInt
(
user
.
getNewapiUserId
().
toString
());
return
newApiUserApi
.
getQuotaYuan
(
user
.
getNewapiAccessToken
(),
newApiUserId
);
}
catch
(
NumberFormatException
e
)
{
log
.
warn
(
"[充值协议PDF] newapiUserId 不是合法数字: {}"
,
user
.
getNewapiUserId
());
return
null
;
}
}
/** 读取 classpath 下的 HTML 协议模板 */
private
String
loadTemplate
()
{
ClassPathResource
resource
=
new
ClassPathResource
(
"templates/recharge-agreement.html"
);
...
...
computility-module-member/src/main/resources/fonts/NotoSansSC-Regular.ttf
0 → 100644
View file @
4f0c132c
File added
computility-module-member/src/main/resources/templates/recharge-agreement.html
View file @
4f0c132c
...
...
@@ -163,7 +163,9 @@
<p>
12.5 甲方确认:在点击"同意"前,已仔细阅读并充分理解本协议全部条款,特别是涉及甲方权利义务限制、责任免除及争议解决的条款,乙方已对相关条款进行了充分提示和说明。
</p>
<div
class=
"signature"
>
<p>
甲方(客户)确认:我已仔细阅读并完全同意本协议全部条款,自愿充值并使用平台服务。
</p>
<p>
甲方(客户)确认:
</p>
<p>
本次充值金額${rechargeAmount}元,余额合计${totalBalance}元
</p>
<p>
我已仔细阅读并完全同意本协议全部条款,自愿充值并使用平台服务。
</p>
<p>
乙方(平台运营方):面向科创园区的普惠算力公共服务平台
</p>
<p>
协议生效日期:${agreementDate}
</p>
</div>
...
...
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