Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
A
an-plus-hr-ai
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
anjia-hr
an-plus-hr-ai
Commits
40305b50
Commit
40305b50
authored
Sep 21, 2026
by
刘斌
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: 完善消费记录为父子表结构
parent
6ceb318f
Show whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
162 additions
and
46 deletions
+162
-46
AiConsumeProjectCtrl.java
...ava/com/anplus/hr/ai/controller/AiConsumeProjectCtrl.java
+2
-2
AiConsumeProject.java
.../java/com/anplus/hr/ai/domain/model/AiConsumeProject.java
+5
-0
AiConsumeExportVo.java
...in/java/com/anplus/hr/ai/domain/vo/AiConsumeExportVo.java
+10
-3
AiConsumeProjectVo.java
...n/java/com/anplus/hr/ai/domain/vo/AiConsumeProjectVo.java
+5
-0
AiConsumeRecordSummaryVo.java
.../com/anplus/hr/ai/domain/vo/AiConsumeRecordSummaryVo.java
+5
-0
AiConsumeProjectServ.java
...n/java/com/anplus/hr/ai/service/AiConsumeProjectServ.java
+2
-2
AiConsumeProjectServImpl.java
...m/anplus/hr/ai/service/impl/AiConsumeProjectServImpl.java
+65
-24
index.ts
frontend/src/api/ai/consume/index.ts
+2
-2
model.d.ts
frontend/src/api/ai/consume/model.d.ts
+9
-1
consume-drawer.vue
frontend/src/views/ai/consume/consume-drawer.vue
+3
-0
data.ts
frontend/src/views/ai/consume/data.ts
+25
-6
index.vue
frontend/src/views/ai/consume/index.vue
+8
-3
data.ts
frontend/src/views/ai/recharge/data.ts
+15
-0
index.vue
frontend/src/views/ai/recharge/index.vue
+4
-3
recharge-drawer.vue
frontend/src/views/ai/recharge/recharge-drawer.vue
+2
-0
No files found.
admin/src/main/java/com/anplus/hr/ai/controller/AiConsumeProjectCtrl.java
View file @
40305b50
...
@@ -59,10 +59,10 @@ public class AiConsumeProjectCtrl {
...
@@ -59,10 +59,10 @@ public class AiConsumeProjectCtrl {
}
}
/**
/**
* 查询当前筛选条件下的
项目总金额
合计(跨所有分页的全量结果,口径与列表完全一致)。
* 查询当前筛选条件下的
命中明细金额合计与积分
合计(跨所有分页的全量结果,口径与列表完全一致)。
*
*
* @param param 查询条件(与分页查询共用同一条件对象)
* @param param 查询条件(与分页查询共用同一条件对象)
* @return 合计载体(amountCny
保留两位小数;无匹配数据时
为 0.00)
* @return 合计载体(amountCny
明细金额合计 + consumePoints 明细积分合计,均保留两位小数;无匹配数据时均
为 0.00)
*/
*/
@SaCheckPermission
(
"ai:consume:list"
)
@SaCheckPermission
(
"ai:consume:list"
)
@GetMapping
(
"/summary"
)
@GetMapping
(
"/summary"
)
...
...
admin/src/main/java/com/anplus/hr/ai/domain/model/AiConsumeProject.java
View file @
40305b50
...
@@ -59,6 +59,11 @@ public class AiConsumeProject extends TenantModel {
...
@@ -59,6 +59,11 @@ public class AiConsumeProject extends TenantModel {
*/
*/
private
BigDecimal
amountCny
;
private
BigDecimal
amountCny
;
/**
* 消费积分(= Σ 子表明细积分,随明细增删改事务聚合重算)
*/
private
BigDecimal
consumePoints
;
/**
/**
* 每分钟金额(= 项目总金额×60÷成片时长,2位小数HALF_UP;成片时长为0时为NULL;派生计算属 C2)
* 每分钟金额(= 项目总金额×60÷成片时长,2位小数HALF_UP;成片时长为0时为NULL;派生计算属 C2)
*/
*/
...
...
admin/src/main/java/com/anplus/hr/ai/domain/vo/AiConsumeExportVo.java
View file @
40305b50
...
@@ -13,11 +13,11 @@ import java.time.LocalDate;
...
@@ -13,11 +13,11 @@ import java.time.LocalDate;
* AI工具消费记录扁平导出行视图对象(导出专用,D8)
* AI工具消费记录扁平导出行视图对象(导出专用,D8)
*
*
* <p>导出({@code POST /ai/consume/export})将父子表 JOIN 展开为扁平明细行:每行 = 一条明细子行
* <p>导出({@code POST /ai/consume/export})将父子表 JOIN 展开为扁平明细行:每行 = 一条明细子行
* + 该项目父级汇总列(所属部门 / 项目名称 / 姓名 / 消费时间 / 项目总金额 / 成片时长 / 每分钟金额 /
* + 该项目父级汇总列(所属部门 / 项目名称 / 姓名 / 消费时间 / 项目总金额 /
消费积分 /
成片时长 / 每分钟金额 /
* 备注)重复展示,其后接明细列(AI工具名称 / 国内外 / 消耗积分 / 金额)。列序与标签对齐 spec
* 备注)重复展示,其后接明细列(AI工具名称 / 国内外 / 消耗积分 / 金额)。列序与标签对齐 spec
* 「导出末尾合计行」,并保持与既有导出列口径稳定;部门名由 ServiceImpl 批量回填(导出链不触发
* 「导出末尾合计行」,并保持与既有导出列口径稳定;部门名由 ServiceImpl 批量回填(导出链不触发
* JSON 翻译)。末尾合计行:所属部门 = 「合计」、项目总金额 = 全量命中明细金额合计
,其余列(含「金额(人民币)」
* JSON 翻译)。末尾合计行:所属部门 = 「合计」、项目总金额 = 全量命中明细金额合计
、消费积分 = 全量命中
* 明细列)留空。成片时长列以分秒文本输出(spec「导出成片时长格式化」)。</p>
* 明细
积分合计,其余列(含「金额(人民币)」明细
列)留空。成片时长列以分秒文本输出(spec「导出成片时长格式化」)。</p>
*
*
* <p>非导出辅助字段 {@code deptId} 无 {@code @ExcelProperty},被 {@code @ExcelIgnoreUnannotated}
* <p>非导出辅助字段 {@code deptId} 无 {@code @ExcelProperty},被 {@code @ExcelIgnoreUnannotated}
* 忽略,仅用于批量查询部门名回填 {@code deptName}。两个金额列语义不同:{@code amountCny} 为父级
* 忽略,仅用于批量查询部门名回填 {@code deptName}。两个金额列语义不同:{@code amountCny} 为父级
...
@@ -68,6 +68,13 @@ public class AiConsumeExportVo implements Serializable {
...
@@ -68,6 +68,13 @@ public class AiConsumeExportVo implements Serializable {
@ExcelProperty
(
value
=
"项目总金额"
)
@ExcelProperty
(
value
=
"项目总金额"
)
private
BigDecimal
amountCny
;
private
BigDecimal
amountCny
;
/**
* 消费积分(父级汇总列,= Σ 子表明细积分,每明细行重复;末尾合计行此列填全量命中明细积分合计;
* 与明细列 {@link #consumePoints}「消耗积分」配对,两者语义不同勿混)
*/
@ExcelProperty
(
value
=
"消费积分"
)
private
BigDecimal
projectConsumePoints
;
/**
/**
* 成片时长(父级汇总列,每明细行重复):导出层格式化为分秒文本({@code >60 秒→「X分Y秒」、
* 成片时长(父级汇总列,每明细行重复):导出层格式化为分秒文本({@code >60 秒→「X分Y秒」、
* 1..60 秒→「X秒」、0→「0秒」、NULL→留白}),对齐前端列表 durationText 约定(spec「导出成片时长格式化」)
* 1..60 秒→「X秒」、0→「0秒」、NULL→留白}),对齐前端列表 durationText 约定(spec「导出成片时长格式化」)
...
...
admin/src/main/java/com/anplus/hr/ai/domain/vo/AiConsumeProjectVo.java
View file @
40305b50
...
@@ -69,6 +69,11 @@ public class AiConsumeProjectVo implements Serializable {
...
@@ -69,6 +69,11 @@ public class AiConsumeProjectVo implements Serializable {
*/
*/
private
BigDecimal
amountCny
;
private
BigDecimal
amountCny
;
/**
* 消费积分(= Σ 子表明细积分,事务重算冗余存储)
*/
private
BigDecimal
consumePoints
;
/**
/**
* 成片时长(秒)
* 成片时长(秒)
*/
*/
...
...
admin/src/main/java/com/anplus/hr/ai/domain/vo/AiConsumeRecordSummaryVo.java
View file @
40305b50
...
@@ -24,4 +24,9 @@ public class AiConsumeRecordSummaryVo implements Serializable {
...
@@ -24,4 +24,9 @@ public class AiConsumeRecordSummaryVo implements Serializable {
*/
*/
private
BigDecimal
amountCny
;
private
BigDecimal
amountCny
;
/**
* 消费积分合计:当前查询条件筛选出的全部命中明细子行积分(跨所有分页)求和,保留两位小数(与 amountCny 同口径)
*/
private
BigDecimal
consumePoints
;
}
}
admin/src/main/java/com/anplus/hr/ai/service/AiConsumeProjectServ.java
View file @
40305b50
...
@@ -40,10 +40,10 @@ public interface AiConsumeProjectServ extends IService<AiConsumeProject> {
...
@@ -40,10 +40,10 @@ public interface AiConsumeProjectServ extends IService<AiConsumeProject> {
List
<
AiConsumeExportVo
>
queryList
(
AiConsumeProjectListParam
param
);
List
<
AiConsumeExportVo
>
queryList
(
AiConsumeProjectListParam
param
);
/**
/**
* 查询当前条件下的
项目总金额
合计(跨所有分页的全量筛选结果,口径与列表完全一致)
* 查询当前条件下的
命中明细金额合计与积分
合计(跨所有分页的全量筛选结果,口径与列表完全一致)
*
*
* @param param 查询条件(与分页查询共用同一条件对象 AiConsumeProjectListParam)
* @param param 查询条件(与分页查询共用同一条件对象 AiConsumeProjectListParam)
* @return 合计载体(amountCny
保留两位小数;无匹配数据时
为 0.00)
* @return 合计载体(amountCny
明细金额合计 + consumePoints 明细积分合计,均保留两位小数;无匹配数据时均
为 0.00)
*/
*/
AiConsumeRecordSummaryVo
querySummary
(
AiConsumeProjectListParam
param
);
AiConsumeRecordSummaryVo
querySummary
(
AiConsumeProjectListParam
param
);
...
...
admin/src/main/java/com/anplus/hr/ai/service/impl/AiConsumeProjectServImpl.java
View file @
40305b50
...
@@ -107,8 +107,9 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
...
@@ -107,8 +107,9 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
.
build
();
.
build
();
List
<
AiConsumeProject
>
parents
=
aiConsumeProjectMapper
.
selectList
(
lambdaQuery
);
List
<
AiConsumeProject
>
parents
=
aiConsumeProjectMapper
.
selectList
(
lambdaQuery
);
List
<
AiConsumeExportVo
>
rows
=
new
ArrayList
<>();
List
<
AiConsumeExportVo
>
rows
=
new
ArrayList
<>();
//合计 = Σ 命中明细子行金额
(口径与列表底部合计一致;无 toolName 时
等于父表项目总金额之和);无数据落 ZERO 仍追加合计行
//合计 = Σ 命中明细子行金额
/ 积分(口径与列表底部合计一致;无 toolName 时金额合计
等于父表项目总金额之和);无数据落 ZERO 仍追加合计行
BigDecimal
total
=
BigDecimal
.
ZERO
;
BigDecimal
total
=
BigDecimal
.
ZERO
;
BigDecimal
totalPoints
=
BigDecimal
.
ZERO
;
if
(
CollUtil
.
isNotEmpty
(
parents
))
{
if
(
CollUtil
.
isNotEmpty
(
parents
))
{
//② project_id IN 批量查明细(toolName 过滤),line_sort 升序
//② project_id IN 批量查明细(toolName 过滤),line_sort 升序
List
<
Long
>
projectIds
=
parents
.
stream
().
map
(
AiConsumeProject:
:
getId
).
collect
(
Collectors
.
toList
());
List
<
Long
>
projectIds
=
parents
.
stream
().
map
(
AiConsumeProject:
:
getId
).
collect
(
Collectors
.
toList
());
...
@@ -121,6 +122,11 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
...
@@ -121,6 +122,11 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
.
filter
(
ObjectUtil:
:
isNotNull
)
.
filter
(
ObjectUtil:
:
isNotNull
)
.
reduce
(
BigDecimal
.
ZERO
,
BigDecimal:
:
add
)
.
reduce
(
BigDecimal
.
ZERO
,
BigDecimal:
:
add
)
.
setScale
(
2
,
RoundingMode
.
HALF_UP
);
.
setScale
(
2
,
RoundingMode
.
HALF_UP
);
//合计 = Σ details.consumePoints(同一命中明细集;NULL 明细积分忽略,spec「导出末尾合计行」)
totalPoints
=
details
.
stream
().
map
(
AiConsumeRecord:
:
getConsumePoints
)
.
filter
(
ObjectUtil:
:
isNotNull
)
.
reduce
(
BigDecimal
.
ZERO
,
BigDecimal:
:
add
)
.
setScale
(
2
,
RoundingMode
.
HALF_UP
);
Map
<
Long
,
List
<
AiConsumeRecord
>>
detailsByProject
=
details
.
stream
()
Map
<
Long
,
List
<
AiConsumeRecord
>>
detailsByProject
=
details
.
stream
()
.
collect
(
Collectors
.
groupingBy
(
AiConsumeRecord:
:
getProjectId
));
.
collect
(
Collectors
.
groupingBy
(
AiConsumeRecord:
:
getProjectId
));
//③ 批量回填部门名(导出链不触发 JSON 翻译,spec「部门名称展示与导出翻译」)
//③ 批量回填部门名(导出链不触发 JSON 翻译,spec「部门名称展示与导出翻译」)
...
@@ -128,20 +134,27 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
...
@@ -128,20 +134,27 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
.
filter
(
ObjectUtil:
:
isNotNull
).
collect
(
Collectors
.
toSet
());
.
filter
(
ObjectUtil:
:
isNotNull
).
collect
(
Collectors
.
toSet
());
Map
<
Long
,
String
>
deptNames
=
CollUtil
.
isEmpty
(
deptIds
)
Map
<
Long
,
String
>
deptNames
=
CollUtil
.
isEmpty
(
deptIds
)
?
Collections
.
emptyMap
()
:
sysDeptServ
.
selectDeptNamesByIds
(
deptIds
);
?
Collections
.
emptyMap
()
:
sysDeptServ
.
selectDeptNamesByIds
(
deptIds
);
//④ JOIN 展开扁平行:每明细一行
+ 父级汇总列重复
//④ JOIN 展开扁平行:每明细一行
;父级汇总列仅该项目首条明细行(line_sort 升序的首条命中明细)写入,其余明细行留空(spec「消费记录导出末尾合计行」)
for
(
AiConsumeProject
parent
:
parents
)
{
for
(
AiConsumeProject
parent
:
parents
)
{
String
deptName
=
deptNames
.
get
(
parent
.
getDeptId
());
String
deptName
=
deptNames
.
get
(
parent
.
getDeptId
());
for
(
AiConsumeRecord
child
:
detailsByProject
.
getOrDefault
(
parent
.
getId
(),
Collections
.
emptyList
()))
{
List
<
AiConsumeRecord
>
projectDetails
=
detailsByProject
.
getOrDefault
(
parent
.
getId
(),
Collections
.
emptyList
());
for
(
int
i
=
0
;
i
<
projectDetails
.
size
();
i
++)
{
AiConsumeRecord
child
=
projectDetails
.
get
(
i
);
AiConsumeExportVo
row
=
new
AiConsumeExportVo
();
AiConsumeExportVo
row
=
new
AiConsumeExportVo
();
if
(
i
==
0
)
{
//首条明细行:写入父级汇总列(含部门名);非首行不写入,保持 null 由 Fesod 输出空单元格
row
.
setDeptId
(
parent
.
getDeptId
());
row
.
setDeptId
(
parent
.
getDeptId
());
row
.
setDeptName
(
deptName
);
row
.
setDeptName
(
deptName
);
row
.
setProjectName
(
parent
.
getProjectName
());
row
.
setProjectName
(
parent
.
getProjectName
());
row
.
setUserName
(
parent
.
getUserName
());
row
.
setUserName
(
parent
.
getUserName
());
row
.
setConsumeTime
(
parent
.
getConsumeTime
());
row
.
setConsumeTime
(
parent
.
getConsumeTime
());
row
.
setAmountCny
(
parent
.
getAmountCny
());
row
.
setAmountCny
(
parent
.
getAmountCny
());
row
.
setProjectConsumePoints
(
parent
.
getConsumePoints
());
row
.
setDurationText
(
formatDuration
(
parent
.
getDurationSeconds
()));
row
.
setDurationText
(
formatDuration
(
parent
.
getDurationSeconds
()));
row
.
setAmountPerMinute
(
parent
.
getAmountPerMinute
());
row
.
setAmountPerMinute
(
parent
.
getAmountPerMinute
());
row
.
setRemark
(
parent
.
getRemark
());
row
.
setRemark
(
parent
.
getRemark
());
}
//明细列逐行照写
row
.
setToolName
(
child
.
getToolName
());
row
.
setToolName
(
child
.
getToolName
());
row
.
setRegion
(
child
.
getRegion
());
row
.
setRegion
(
child
.
getRegion
());
row
.
setConsumePoints
(
child
.
getConsumePoints
());
row
.
setConsumePoints
(
child
.
getConsumePoints
());
...
@@ -151,28 +164,30 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
...
@@ -151,28 +164,30 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
}
}
}
}
//⑤ 末尾追加合计行(无数据时 total=0.00 仍追加,spec「导出文件末尾含合计行」)
//⑤ 末尾追加合计行(无数据时 total=0.00 仍追加,spec「导出文件末尾含合计行」)
rows
.
add
(
buildTotalRow
(
total
));
rows
.
add
(
buildTotalRow
(
total
,
totalPoints
));
return
rows
;
return
rows
;
}
}
/**
/**
* 查询当前条件下的命中明细金额合计(跨所有分页的全量筛选结果,口径与列表/导出展示明细逐一等价)
* 查询当前条件下的命中明细金额合计
与积分合计
(跨所有分页的全量筛选结果,口径与列表/导出展示明细逐一等价)
*
*
* <p>合计 = Σ 命中明细子行 {@code ai_consume_record.amount_cny}:先复用 {@link #buildProjectCondition}
* <p>合计 = Σ 命中明细子行 {@code ai_consume_record.amount_cny} 与 {@code consume_points}:先复用
* 取命中父行 id 集合(含 toolName EXISTS 命中),再对子表按 {@code project_id IN + toolName LIKE} 做 SQL SUM。
* {@link #buildProjectCondition} 取命中父行 id 集合(含 toolName EXISTS 命中),再对子表按
* 按 toolName 过滤时合计仅覆盖匹配明细,不回退父行全项目总金额(spec「消费记录合计查询接口」)。</p>
* {@code project_id IN + toolName LIKE} 做 SQL SUM。按 toolName 过滤时合计仅覆盖匹配明细,
* 不回退父行全项目汇总值(spec「消费记录合计查询接口」双字段)。</p>
*
*
* @param param 查询条件(与分页查询共用同一条件对象)
* @param param 查询条件(与分页查询共用同一条件对象)
* @return 合计载体(amountCny
为明细金额合计,保留两位小数;无匹配数据时
为 0.00)
* @return 合计载体(amountCny
明细金额合计 + consumePoints 明细积分合计,均保留两位小数;无匹配数据时均
为 0.00)
*/
*/
@Override
@Override
public
AiConsumeRecordSummaryVo
querySummary
(
AiConsumeProjectListParam
param
)
{
public
AiConsumeRecordSummaryVo
querySummary
(
AiConsumeProjectListParam
param
)
{
AiConsumeRecordSummaryVo
summary
=
new
AiConsumeRecordSummaryVo
();
AiConsumeRecordSummaryVo
summary
=
new
AiConsumeRecordSummaryVo
();
//① 复用 buildProjectCondition 取命中父行(口径与列表/导出完全一致,含 toolName EXISTS 命中)
//① 复用 buildProjectCondition 取命中父行(口径与列表/导出完全一致,含 toolName EXISTS 命中)
List
<
AiConsumeProject
>
parents
=
aiConsumeProjectMapper
.
selectList
(
buildProjectCondition
(
param
).
build
());
List
<
AiConsumeProject
>
parents
=
aiConsumeProjectMapper
.
selectList
(
buildProjectCondition
(
param
).
build
());
//② 命中父行为空 → 合计 0.00,避免 in(空集合) 退化并省去无谓子查询(design D6)
//② 命中父行为空 →
双
合计 0.00,避免 in(空集合) 退化并省去无谓子查询(design D6)
if
(
CollUtil
.
isEmpty
(
parents
))
{
if
(
CollUtil
.
isEmpty
(
parents
))
{
summary
.
setAmountCny
(
BigDecimal
.
ZERO
.
setScale
(
2
,
RoundingMode
.
HALF_UP
));
summary
.
setAmountCny
(
BigDecimal
.
ZERO
.
setScale
(
2
,
RoundingMode
.
HALF_UP
));
summary
.
setConsumePoints
(
BigDecimal
.
ZERO
.
setScale
(
2
,
RoundingMode
.
HALF_UP
));
return
summary
;
return
summary
;
}
}
List
<
Long
>
projectIds
=
parents
.
stream
().
map
(
AiConsumeProject:
:
getId
).
collect
(
Collectors
.
toList
());
List
<
Long
>
projectIds
=
parents
.
stream
().
map
(
AiConsumeProject:
:
getId
).
collect
(
Collectors
.
toList
());
...
@@ -189,6 +204,17 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
...
@@ -189,6 +204,17 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
?
BigDecimal
.
ZERO
?
BigDecimal
.
ZERO
:
Convert
.
toBigDecimal
(
objs
.
get
(
0
),
BigDecimal
.
ZERO
);
:
Convert
.
toBigDecimal
(
objs
.
get
(
0
),
BigDecimal
.
ZERO
);
summary
.
setAmountCny
(
amountCny
.
setScale
(
2
,
RoundingMode
.
HALF_UP
));
summary
.
setAmountCny
(
amountCny
.
setScale
(
2
,
RoundingMode
.
HALF_UP
));
//④ 同口径 SQL SUM 命中明细积分(与金额共用命中范围;NULL 由 SUM 忽略,spec「消费记录合计查询接口」双字段)
LambdaQueryWrapper
<
AiConsumeRecord
>
pointsSumWrapper
=
QueryBuilder
.
lambda
(
AiConsumeRecord
.
class
)
.
in
(
AiConsumeRecord:
:
getProjectId
,
projectIds
)
.
likeIfText
(
AiConsumeRecord:
:
getToolName
,
param
.
getToolName
())
.
selectSum
(
AiConsumeRecord:
:
getConsumePoints
)
.
build
();
List
<
Object
>
pointsObjs
=
aiConsumeRecordMapper
.
selectObjs
(
pointsSumWrapper
);
BigDecimal
consumePoints
=
CollUtil
.
isEmpty
(
pointsObjs
)
?
BigDecimal
.
ZERO
:
Convert
.
toBigDecimal
(
pointsObjs
.
get
(
0
),
BigDecimal
.
ZERO
);
summary
.
setConsumePoints
(
consumePoints
.
setScale
(
2
,
RoundingMode
.
HALF_UP
));
return
summary
;
return
summary
;
}
}
...
@@ -321,11 +347,12 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
...
@@ -321,11 +347,12 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
}
}
/**
/**
* 全量重算项目总金额
+ 派生每分钟金额并回写父表(design D4,Clarify Q2
全量 SUM 非增量)。
* 全量重算项目总金额
/ 消费积分 + 派生每分钟金额并回写父表(design D2/D4,
全量 SUM 非增量)。
*
*
* <p>项目总金额 = SUM(子表 amount_cny WHERE project_id AND deleted=0)(租户由拦截器注入,NULL 落 ZERO);
* <p>项目总金额 = SUM(子表 amount_cny WHERE project_id AND deleted=0)(租户由拦截器注入,NULL 落 ZERO);
* 消费积分 = SUM(子表 consume_points WHERE project_id AND deleted=0)(同口径,NULL 由 SUM 忽略);
* 每分钟金额 = 总金额×60÷成片时长(2 位 HALF_UP),成片时长为 0/null 时置 NULL(避免除零,spec「每分钟金额派生」)。
* 每分钟金额 = 总金额×60÷成片时长(2 位 HALF_UP),成片时长为 0/null 时置 NULL(避免除零,spec「每分钟金额派生」)。
* 用 LambdaUpdateWrapper 显式 set 以强制写入 NULL(绕过 updateById 的 NOT_NULL 策略)。</p>
*
金额 / 积分 / 每分钟金额以同一条 UPDATE 回写(spec「消费项目消费积分事务重算」);
用 LambdaUpdateWrapper 显式 set 以强制写入 NULL(绕过 updateById 的 NOT_NULL 策略)。</p>
*
*
* @param projectId 项目id
* @param projectId 项目id
* @param durationSeconds 成片时长(秒)
* @param durationSeconds 成片时长(秒)
...
@@ -340,6 +367,16 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
...
@@ -340,6 +367,16 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
?
BigDecimal
.
ZERO
?
BigDecimal
.
ZERO
:
Convert
.
toBigDecimal
(
objs
.
get
(
0
),
BigDecimal
.
ZERO
))
:
Convert
.
toBigDecimal
(
objs
.
get
(
0
),
BigDecimal
.
ZERO
))
.
setScale
(
2
,
RoundingMode
.
HALF_UP
);
.
setScale
(
2
,
RoundingMode
.
HALF_UP
);
//消费积分 = SUM(consume_points)(与金额同模式独立 SUM;NULL 由 SQL SUM 忽略,空落 ZERO)
LambdaQueryWrapper
<
AiConsumeRecord
>
pointsSumWrapper
=
QueryBuilder
.
lambda
(
AiConsumeRecord
.
class
)
.
eq
(
AiConsumeRecord:
:
getProjectId
,
projectId
)
.
selectSum
(
AiConsumeRecord:
:
getConsumePoints
)
.
build
();
List
<
Object
>
pointsObjs
=
aiConsumeRecordMapper
.
selectObjs
(
pointsSumWrapper
);
BigDecimal
points
=
(
CollUtil
.
isEmpty
(
pointsObjs
)
?
BigDecimal
.
ZERO
:
Convert
.
toBigDecimal
(
pointsObjs
.
get
(
0
),
BigDecimal
.
ZERO
))
.
setScale
(
2
,
RoundingMode
.
HALF_UP
);
BigDecimal
amountPerMinute
=
null
;
BigDecimal
amountPerMinute
=
null
;
if
(
durationSeconds
!=
null
&&
durationSeconds
>
0
)
{
if
(
durationSeconds
!=
null
&&
durationSeconds
>
0
)
{
amountPerMinute
=
total
.
multiply
(
BigDecimal
.
valueOf
(
60
))
amountPerMinute
=
total
.
multiply
(
BigDecimal
.
valueOf
(
60
))
...
@@ -348,19 +385,23 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
...
@@ -348,19 +385,23 @@ public class AiConsumeProjectServImpl extends ServiceImpl<AiConsumeProjectMapper
aiConsumeProjectMapper
.
update
(
null
,
new
LambdaUpdateWrapper
<
AiConsumeProject
>()
aiConsumeProjectMapper
.
update
(
null
,
new
LambdaUpdateWrapper
<
AiConsumeProject
>()
.
eq
(
AiConsumeProject:
:
getId
,
projectId
)
.
eq
(
AiConsumeProject:
:
getId
,
projectId
)
.
set
(
AiConsumeProject:
:
getAmountCny
,
total
)
.
set
(
AiConsumeProject:
:
getAmountCny
,
total
)
.
set
(
AiConsumeProject:
:
getConsumePoints
,
points
)
.
set
(
AiConsumeProject:
:
getAmountPerMinute
,
amountPerMinute
));
.
set
(
AiConsumeProject:
:
getAmountPerMinute
,
amountPerMinute
));
}
}
/**
/**
* 构建导出末尾合计行:所属部门列固定「合计」、项目总金额列填全量命中明细金额合计,其余列(含金额(人民币)明细列)留空(spec「导出末尾合计行」,design D5)。
* 构建导出末尾合计行:所属部门列固定「合计」、项目总金额列填全量命中明细金额合计、消费积分列填全量命中明细积分合计,
* 其余列(含金额(人民币)明细列)留空(spec「导出末尾合计行」,design D4)。
*
*
* @param total 全量命中明细金额合计(两位小数)
* @param total 全量命中明细金额合计(两位小数)
* @param totalPoints 全量命中明细积分合计(两位小数)
* @return 合计行
* @return 合计行
*/
*/
private
AiConsumeExportVo
buildTotalRow
(
BigDecimal
total
)
{
private
AiConsumeExportVo
buildTotalRow
(
BigDecimal
total
,
BigDecimal
totalPoints
)
{
AiConsumeExportVo
totalRow
=
new
AiConsumeExportVo
();
AiConsumeExportVo
totalRow
=
new
AiConsumeExportVo
();
totalRow
.
setDeptName
(
"合计"
);
totalRow
.
setDeptName
(
"合计"
);
totalRow
.
setAmountCny
(
total
);
totalRow
.
setAmountCny
(
total
);
totalRow
.
setProjectConsumePoints
(
totalPoints
);
return
totalRow
;
return
totalRow
;
}
}
...
...
frontend/src/api/ai/consume/index.ts
View file @
40305b50
...
@@ -23,9 +23,9 @@ export function consumeList(params?: PageQuery) {
...
@@ -23,9 +23,9 @@ export function consumeList(params?: PageQuery) {
}
}
/**
/**
* 查询消费记录金额合计(跨所有分页的全量筛选结果,口径与列表一致)
* 查询消费记录金额
与积分
合计(跨所有分页的全量筛选结果,口径与列表一致)
* @param params 请求参数(与列表查询共用同一条件)
* @param params 请求参数(与列表查询共用同一条件)
* @returns 合计载体(amountCny)
* @returns 合计载体(amountCny
明细金额合计 + consumePoints 明细积分合计
)
*/
*/
export
function
consumeSummary
(
params
?:
PageQuery
)
{
export
function
consumeSummary
(
params
?:
PageQuery
)
{
return
alovaInstance
.
get
<
ConsumeSummary
>
(
Api
.
consumeSummary
,
{
params
});
return
alovaInstance
.
get
<
ConsumeSummary
>
(
Api
.
consumeSummary
,
{
params
});
...
...
frontend/src/api/ai/consume/model.d.ts
View file @
40305b50
...
@@ -30,6 +30,10 @@ export interface Consume {
...
@@ -30,6 +30,10 @@ export interface Consume {
* 项目总金额(= Σ 明细金额,人民币;列表「项目总金额」列绑定字段)
* 项目总金额(= Σ 明细金额,人民币;列表「项目总金额」列绑定字段)
*/
*/
amountCny
:
number
;
amountCny
:
number
;
/**
* 消费积分(= Σ 明细积分,列表「消费积分」列绑定字段;存量未回填行为 null 时列留白)
*/
consumePoints
?:
number
;
/**
/**
* 成片时长(秒,非负整数)
* 成片时长(秒,非负整数)
*/
*/
...
@@ -144,7 +148,11 @@ export interface ConsumeForm {
...
@@ -144,7 +148,11 @@ export interface ConsumeForm {
*/
*/
export
interface
ConsumeSummary
{
export
interface
ConsumeSummary
{
/**
/**
*
项目总金额全量合计(
两位小数)
*
明细金额合计(命中明细子行金额之和,
两位小数)
*/
*/
amountCny
:
number
;
amountCny
:
number
;
/**
* 明细积分合计(命中明细子行积分之和,两位小数;与 amountCny 同口径)
*/
consumePoints
:
number
;
}
}
frontend/src/views/ai/consume/consume-drawer.vue
View file @
40305b50
...
@@ -30,6 +30,7 @@ import {
...
@@ -30,6 +30,7 @@ import {
TimePicker
,
TimePicker
,
}
from
'antdv-next'
;
}
from
'antdv-next'
;
import
{
VxeGrid
}
from
'vxe-table'
;
import
{
VxeGrid
}
from
'vxe-table'
;
import
{
cnyText
}
from
'./data.ts'
const
emit
=
defineEmits
<
{
reload
:
[]
}
>
();
const
emit
=
defineEmits
<
{
reload
:
[]
}
>
();
...
@@ -186,12 +187,14 @@ const detailColumns: VxeGridProps['columns'] = [
...
@@ -186,12 +187,14 @@ const detailColumns: VxeGridProps['columns'] = [
title
:
'金额(人民币)'
,
title
:
'金额(人民币)'
,
minWidth
:
130
,
minWidth
:
130
,
align
:
'center'
,
align
:
'center'
,
formatter
:
({
cellValue
})
=>
cnyText
(
cellValue
),
editRender
:
{},
editRender
:
{},
slots
:
{
slots
:
{
edit
:
(
props
)
=>
{
edit
:
(
props
)
=>
{
const
{
row
,
$grid
}
=
props
;
const
{
row
,
$grid
}
=
props
;
return
(
return
(
<
InputNumber
<
InputNumber
prefix
=
"¥"
class
=
"w-full"
class
=
"w-full"
min
=
{
0
}
min
=
{
0
}
onChange
=
{()
=>
$grid
?.
updateStatus
(
props
)}
onChange
=
{()
=>
$grid
?.
updateStatus
(
props
)}
...
...
frontend/src/views/ai/consume/data.ts
View file @
40305b50
import
type
{
VxeGridProps
}
from
'vxe-table'
;
import
type
{
VxeGridProps
}
from
'vxe-table'
;
/**
/**
* 金额格式化:空值留白(合并列中父/子行在对方列天然为 null),有值保留 2 位小数。
* 数值格式化:空值/空串留白,有值保留 2 位小数。用于父行「消费积分」列(design D8/D5)
* 同时用于父行「项目总金额」与子行「金额(人民币)」列(design D8)
*/
*/
function
amountText
(
value
?:
null
|
number
|
string
):
string
{
function
amountText
(
value
?:
null
|
number
|
string
):
string
{
return
value
==
null
||
value
===
''
?
''
:
Number
(
value
).
toFixed
(
2
);
return
value
==
null
||
value
===
''
?
''
:
Number
(
value
).
toFixed
(
2
);
}
}
/**
* 人民币金额格式化:空值/空串留白;已带「¥」标识的值直通(幂等,兼容 footer 单元格复用列 formatter),
* 否则补「¥ + 2 位小数」。用于父行「项目总金额」/「每分钟金额」与子行「金额(人民币)」列
* (spec「消费记录列表金额列币种标识」)
*/
export
function
cnyText
(
value
?:
null
|
number
|
string
):
string
{
if
(
value
==
null
||
value
===
''
)
{
return
''
;
}
const
text
=
String
(
value
);
return
text
.
startsWith
(
'¥'
)
?
text
:
`¥
${
Number
(
value
).
toFixed
(
2
)}
`
;
}
/**
/**
* 成片时长格式化:>60 秒「X分X秒」、≤60 秒「X秒」、0「0秒」;空值(子行)留白(design D8)
* 成片时长格式化:>60 秒「X分X秒」、≤60 秒「X秒」、0「0秒」;空值(子行)留白(design D8)
*/
*/
...
@@ -25,7 +37,7 @@ function durationText(value?: null | number): string {
...
@@ -25,7 +37,7 @@ function durationText(value?: null | number): string {
/**
/**
* 主网格列集(design D7):仅项目父级列 + 展开列。
* 主网格列集(design D7):仅项目父级列 + 展开列。
* - 展开列 type:'expand':点击在该行下方内嵌明细子表(列见 detailColumns,数据取 row.children)
* - 展开列 type:'expand':点击在该行下方内嵌明细子表(列见 detailColumns,数据取 row.children)
* - 父级列:所属部门 / 项目名称 / 姓名 / 项目总金额(amountCny) / 成片时长 / 每分钟金额 / 消费时间 / 备注 / 操作
* - 父级列:所属部门 / 项目名称 / 姓名 / 项目总金额(amountCny) /
消费积分(consumePoints) /
成片时长 / 每分钟金额 / 消费时间 / 备注 / 操作
* - 明细字段(AI工具名称 / 国内外 / 消耗积分 / 金额)不再占主网格列,移入展开子表
* - 明细字段(AI工具名称 / 国内外 / 消耗积分 / 金额)不再占主网格列,移入展开子表
*/
*/
export
const
columns
:
VxeGridProps
[
'columns'
]
=
[
export
const
columns
:
VxeGridProps
[
'columns'
]
=
[
...
@@ -51,6 +63,13 @@ export const columns: VxeGridProps['columns'] = [
...
@@ -51,6 +63,13 @@ export const columns: VxeGridProps['columns'] = [
field
:
'amountCny'
,
field
:
'amountCny'
,
minWidth
:
100
,
minWidth
:
100
,
align
:
'right'
,
align
:
'right'
,
formatter
:
({
cellValue
})
=>
cnyText
(
cellValue
),
},
{
title
:
'消费积分'
,
field
:
'consumePoints'
,
minWidth
:
100
,
align
:
'right'
,
formatter
:
({
cellValue
})
=>
amountText
(
cellValue
),
formatter
:
({
cellValue
})
=>
amountText
(
cellValue
),
},
},
{
{
...
@@ -65,7 +84,7 @@ export const columns: VxeGridProps['columns'] = [
...
@@ -65,7 +84,7 @@ export const columns: VxeGridProps['columns'] = [
field
:
'amountPerMinute'
,
field
:
'amountPerMinute'
,
minWidth
:
90
,
minWidth
:
90
,
align
:
'right'
,
align
:
'right'
,
formatter
:
({
cellValue
})
=>
amount
Text
(
cellValue
),
formatter
:
({
cellValue
})
=>
cny
Text
(
cellValue
),
},
},
{
{
title
:
'消费时间'
,
title
:
'消费时间'
,
...
@@ -90,7 +109,7 @@ export const columns: VxeGridProps['columns'] = [
...
@@ -90,7 +109,7 @@ export const columns: VxeGridProps['columns'] = [
/**
/**
* 展开行明细子表列集(design D7 / spec「消费记录列表展开明细子表前端展示」):
* 展开行明细子表列集(design D7 / spec「消费记录列表展开明细子表前端展示」):
* AI工具名称 / 国内外 / 消耗积分 / 金额(人民币)。数据源为父行内嵌 children(ConsumeDetail,
* AI工具名称 / 国内外 / 消耗积分 / 金额(人民币)。数据源为父行内嵌 children(ConsumeDetail,
* 金额字段 amountCny);金额列
复用 amountText
保留 2 位、空值留白。仅供主网格展开区嵌套只读子表使用。
* 金额字段 amountCny);金额列
用 cnyText 补「¥」标识
保留 2 位、空值留白。仅供主网格展开区嵌套只读子表使用。
*/
*/
export
const
detailColumns
:
VxeGridProps
[
'columns'
]
=
[
export
const
detailColumns
:
VxeGridProps
[
'columns'
]
=
[
{
{
...
@@ -116,6 +135,6 @@ export const detailColumns: VxeGridProps['columns'] = [
...
@@ -116,6 +135,6 @@ export const detailColumns: VxeGridProps['columns'] = [
field
:
'amountCny'
,
field
:
'amountCny'
,
minWidth
:
120
,
minWidth
:
120
,
align
:
'center'
,
align
:
'center'
,
formatter
:
({
cellValue
})
=>
amount
Text
(
cellValue
),
formatter
:
({
cellValue
})
=>
cny
Text
(
cellValue
),
},
},
];
];
frontend/src/views/ai/consume/index.vue
View file @
40305b50
...
@@ -75,10 +75,15 @@ const gridOptions = withDefaultVxeGridOptions<Consume>({
...
@@ -75,10 +75,15 @@ const gridOptions = withDefaultVxeGridOptions<Consume>({
}),
}),
consumeSummary
(
values
),
consumeSummary
(
values
),
]);
]);
// 合计写入独立 footerData;amountCny 键对齐 data.ts「项目总金额」列 field——
// 合计写入独立 footerData;amountCny / consumePoints 键对齐 data.ts「项目总金额」/「消费积分」列 field——
// 合计口径为命中明细金额之和,落「项目总金额」列、其余列留空(design D5,req1);值来源仍是 summary 的 amountCny 单值
// 合计口径为命中明细金额 / 积分之和,落「项目总金额」与「消费积分」两列、其余列留空(design D3/D5);值来源为 summary 双字段。
// 金额值补「¥」前缀(spec「消费记录列表底部合计行」);积分非金额列不加标识
footerData
.
value
=
[
footerData
.
value
=
[
{
deptName
:
'合计'
,
amountCny
:
Number
(
sum
.
amountCny
).
toFixed
(
2
)
},
{
deptName
:
'合计'
,
amountCny
:
`¥
${
Number
(
sum
.
amountCny
).
toFixed
(
2
)}
`
,
consumePoints
:
Number
(
sum
.
consumePoints
??
0
).
toFixed
(
2
),
},
];
];
// 主网格每行 = 一个项目父行,children 原样保留供展开子表直接渲染(design D7);
// 主网格每行 = 一个项目父行,children 原样保留供展开子表直接渲染(design D7);
// 明细移入独立子表实例,父子不再同表格 → 无需子行字段改名 / 复合键 _rid / rowType 判别
// 明细移入独立子表实例,父子不再同表格 → 无需子行字段改名 / 复合键 _rid / rowType 判别
...
...
frontend/src/views/ai/recharge/data.ts
View file @
40305b50
...
@@ -3,6 +3,19 @@ import type { VxeGridProps } from 'vxe-table';
...
@@ -3,6 +3,19 @@ import type { VxeGridProps } from 'vxe-table';
import
{
DictEnum
}
from
'@/constants'
;
import
{
DictEnum
}
from
'@/constants'
;
import
{
renderDict
}
from
'@/utils/render'
;
import
{
renderDict
}
from
'@/utils/render'
;
/**
* 币种金额格式化:空值/空串留白;已带对应币种标识的值直通(幂等,兼容 footer 单元格复用列 formatter),
* 否则补「标识 + 原始数值」前缀(不强制补位小数,保持后端返回展示精度)。
* 用于「充值金额(美元)」「充值金额(人民币)」列(spec「充值记录列表金额列币种标识」)
*/
function
currencyText
(
value
:
null
|
number
|
string
|
undefined
,
symbol
:
string
):
string
{
if
(
value
==
null
||
value
===
''
)
{
return
''
;
}
const
text
=
String
(
value
);
return
text
.
startsWith
(
symbol
)
?
text
:
`
${
symbol
}${
text
}
`
;
}
export
const
columns
:
VxeGridProps
[
'columns'
]
=
[
export
const
columns
:
VxeGridProps
[
'columns'
]
=
[
{
type
:
'checkbox'
,
width
:
60
},
{
type
:
'checkbox'
,
width
:
60
},
{
{
...
@@ -41,10 +54,12 @@ export const columns: VxeGridProps['columns'] = [
...
@@ -41,10 +54,12 @@ export const columns: VxeGridProps['columns'] = [
{
{
title
:
'充值金额(美元)'
,
title
:
'充值金额(美元)'
,
field
:
'amountUsd'
,
field
:
'amountUsd'
,
formatter
:
({
cellValue
})
=>
currencyText
(
cellValue
,
'$'
),
},
},
{
{
title
:
'充值金额(人民币)'
,
title
:
'充值金额(人民币)'
,
field
:
'amountCny'
,
field
:
'amountCny'
,
formatter
:
({
cellValue
})
=>
currencyText
(
cellValue
,
'¥'
),
},
},
{
{
title
:
'开通账号数量'
,
title
:
'开通账号数量'
,
...
...
frontend/src/views/ai/recharge/index.vue
View file @
40305b50
...
@@ -73,12 +73,13 @@ const gridOptions = withDefaultVxeGridOptions<Recharge>({
...
@@ -73,12 +73,13 @@ const gridOptions = withDefaultVxeGridOptions<Recharge>({
}),
}),
rechargeSummary
(
values
),
rechargeSummary
(
values
),
]);
]);
// 合计写入独立 footerData;两金额键按 data.ts 列 field 对齐;toFixed(2) 规避 JSON 数值退化(design D6)
// 合计写入独立 footerData;两金额键按 data.ts 列 field 对齐;toFixed(2) 规避 JSON 数值退化(design D6)。
// 合计值补币种标识($ / ¥,spec「充值记录列表底部合计行」)
footerData
.
value
=
[
footerData
.
value
=
[
{
{
deptName
:
'合计'
,
deptName
:
'合计'
,
amountUsd
:
Number
(
sum
.
amountUsd
).
toFixed
(
2
)
,
amountUsd
:
`$
${
Number
(
sum
.
amountUsd
).
toFixed
(
2
)}
`
,
amountCny
:
Number
(
sum
.
amountCny
).
toFixed
(
2
)
,
amountCny
:
`¥
${
Number
(
sum
.
amountCny
).
toFixed
(
2
)}
`
,
},
},
];
];
return
listRes
;
return
listRes
;
...
...
frontend/src/views/ai/recharge/recharge-drawer.vue
View file @
40305b50
...
@@ -224,6 +224,7 @@ async function handleClosed() {
...
@@ -224,6 +224,7 @@ async function handleClosed() {
</FormItem>
</FormItem>
<FormItem
label=
"充值金额(美元)"
name=
"amountUsd"
>
<FormItem
label=
"充值金额(美元)"
name=
"amountUsd"
>
<InputNumber
<InputNumber
prefix=
"$"
class=
"w-full"
class=
"w-full"
:style=
"
{ width: '50%' }"
:style=
"
{ width: '50%' }"
:min="0"
:min="0"
...
@@ -233,6 +234,7 @@ async function handleClosed() {
...
@@ -233,6 +234,7 @@ async function handleClosed() {
</FormItem>
</FormItem>
<FormItem
label=
"充值金额(人民币)"
name=
"amountCny"
:rules=
"formRules.amountCny"
>
<FormItem
label=
"充值金额(人民币)"
name=
"amountCny"
:rules=
"formRules.amountCny"
>
<InputNumber
<InputNumber
prefix=
"¥"
class=
"w-full"
class=
"w-full"
:style=
"
{ width: '50%' }"
:style=
"
{ width: '50%' }"
:min="0"
:min="0"
...
...
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