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
b9f1ac5d
Commit
b9f1ac5d
authored
Sep 09, 2026
by
刘斌
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: 完善消费记录和充值记录
parent
b54c4d35
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
41 changed files
with
1440 additions
and
482 deletions
+1440
-482
pom.xml
admin/pom.xml
+8
-0
AiConsumeRecordCtrl.java
...java/com/anplus/hr/ai/controller/AiConsumeRecordCtrl.java
+42
-0
AiRechargeRecordCtrl.java
...ava/com/anplus/hr/ai/controller/AiRechargeRecordCtrl.java
+30
-0
AiConsumeRecord.java
...n/java/com/anplus/hr/ai/domain/model/AiConsumeRecord.java
+19
-4
AiRechargeRecord.java
.../java/com/anplus/hr/ai/domain/model/AiRechargeRecord.java
+7
-3
AiConsumeDetailParam.java
.../com/anplus/hr/ai/domain/params/AiConsumeDetailParam.java
+48
-0
AiConsumeRecordListParam.java
.../anplus/hr/ai/domain/params/AiConsumeRecordListParam.java
+7
-2
AiConsumeRecordParam.java
.../com/anplus/hr/ai/domain/params/AiConsumeRecordParam.java
+26
-37
AiRechargeRecordListParam.java
...anplus/hr/ai/domain/params/AiRechargeRecordListParam.java
+6
-1
AiRechargeRecordParam.java
...com/anplus/hr/ai/domain/params/AiRechargeRecordParam.java
+8
-3
AiConsumeRecordSummaryVo.java
.../com/anplus/hr/ai/domain/vo/AiConsumeRecordSummaryVo.java
+27
-0
AiConsumeRecordVo.java
...in/java/com/anplus/hr/ai/domain/vo/AiConsumeRecordVo.java
+39
-9
AiRechargeRecordSummaryVo.java
...com/anplus/hr/ai/domain/vo/AiRechargeRecordSummaryVo.java
+32
-0
AiRechargeRecordVo.java
...n/java/com/anplus/hr/ai/domain/vo/AiRechargeRecordVo.java
+17
-4
AiConsumeRecordMapper.java
...n/java/com/anplus/hr/ai/mapper/AiConsumeRecordMapper.java
+29
-0
AiConsumeRecordServ.java
...in/java/com/anplus/hr/ai/service/AiConsumeRecordServ.java
+17
-0
AiRechargeRecordServ.java
...n/java/com/anplus/hr/ai/service/AiRechargeRecordServ.java
+9
-0
AiConsumeRecordServImpl.java
...om/anplus/hr/ai/service/impl/AiConsumeRecordServImpl.java
+148
-13
AiRechargeRecordServImpl.java
...m/anplus/hr/ai/service/impl/AiRechargeRecordServImpl.java
+80
-5
AiConsumeRecordMapper.xml
admin/src/main/resources/mapper/AiConsumeRecordMapper.xml
+9
-0
index.ts
frontend/src/api/ai/consume/index.ts
+36
-8
model.d.ts
frontend/src/api/ai/consume/model.d.ts
+92
-2
index.ts
frontend/src/api/ai/recharge/index.ts
+20
-1
model.d.ts
frontend/src/api/ai/recharge/model.d.ts
+27
-1
accessible.ts
frontend/src/components/access/accessible.ts
+6
-71
dict-enum.ts
frontend/src/constants/dict-enum.ts
+0
-2
access.ts
frontend/src/router/access.ts
+17
-15
dashboard.ts
frontend/src/router/routes/modules/dashboard.ts
+3
-0
profile.ts
frontend/src/router/routes/modules/profile.ts
+1
-0
login.vue
frontend/src/views/_core/authentication/login.vue
+2
-2
consume-drawer.vue
frontend/src/views/ai/consume/consume-drawer.vue
+300
-90
consume-search.vue
frontend/src/views/ai/consume/consume-search.vue
+7
-20
data.ts
frontend/src/views/ai/consume/data.ts
+4
-13
index.vue
frontend/src/views/ai/consume/index.vue
+130
-73
data.ts
frontend/src/views/ai/recharge/data.ts
+4
-5
index.vue
frontend/src/views/ai/recharge/index.vue
+132
-71
recharge-drawer.vue
frontend/src/views/ai/recharge/recharge-drawer.vue
+41
-12
recharge-search.vue
frontend/src/views/ai/recharge/recharge-search.vue
+6
-11
menu-drawer.vue
frontend/src/views/system/menu/menu-drawer.vue
+1
-1
role-auth-modal.vue
frontend/src/views/system/role/role-auth-modal.vue
+2
-2
pom.xml
pom.xml
+1
-1
No files found.
admin/pom.xml
View file @
b9f1ac5d
...
...
@@ -20,6 +20,12 @@
<artifactId>
admin
</artifactId>
<version>
${binfast.version}
</version>
</dependency>
<!-- 用户密码加密 如数据库连接等 -->
<dependency>
<groupId>
com.github.ulisesbocchio
</groupId>
<artifactId>
jasypt-spring-boot-starter
</artifactId>
<version>
3.0.5
</version>
</dependency>
<dependency>
<groupId>
top.binfast
</groupId>
<artifactId>
daemon-codegen
</artifactId>
...
...
@@ -36,6 +42,8 @@
</dependencies>
<build>
<!-- 打包产物名(不含扩展名),mvn package 后生成 admin-ai.${project.version}.jar -->
<finalName>
admin-ai.${project.version}
</finalName>
<plugins>
<!-- 跳过单元测试 -->
<plugin>
...
...
admin/src/main/java/com/anplus/hr/ai/controller/AiConsumeRecordCtrl.java
View file @
b9f1ac5d
package
com
.
anplus
.
hr
.
ai
.
controller
;
import
cn.dev33.satoken.annotation.SaCheckPermission
;
import
cn.hutool.core.lang.tree.Tree
;
import
com.alibaba.cola.dto.MultiResponse
;
import
com.alibaba.cola.dto.PageResponse
;
import
com.alibaba.cola.dto.Response
;
import
com.alibaba.cola.dto.SingleResponse
;
...
...
@@ -11,8 +13,11 @@ import org.springframework.validation.annotation.Validated;
import
org.springframework.web.bind.annotation.*
;
import
com.anplus.hr.ai.domain.params.AiConsumeRecordListParam
;
import
com.anplus.hr.ai.domain.params.AiConsumeRecordParam
;
import
com.anplus.hr.ai.domain.vo.AiConsumeRecordSummaryVo
;
import
com.anplus.hr.ai.domain.vo.AiConsumeRecordVo
;
import
com.anplus.hr.ai.service.AiConsumeRecordServ
;
import
top.binfast.app.biz.sysapi.bean.params.sysDept.SysDeptParam
;
import
top.binfast.app.biz.sysbiz.service.SysDeptServ
;
import
top.binfast.common.core.constant.BusinessType
;
import
top.binfast.common.core.util.ResponseUtils
;
import
top.binfast.common.excel.annotion.ExcelExport
;
...
...
@@ -33,6 +38,7 @@ import java.util.List;
public
class
AiConsumeRecordCtrl
{
private
final
AiConsumeRecordServ
aiConsumeRecordServ
;
private
final
SysDeptServ
sysDeptServ
;
/**
* 分页查询AI工具消费记录列表。
...
...
@@ -46,6 +52,18 @@ public class AiConsumeRecordCtrl {
return
aiConsumeRecordServ
.
queryPageList
(
param
);
}
/**
* 查询当前筛选条件下的金额合计(跨所有分页的全量结果,口径与列表完全一致)。
*
* @param param 查询条件(与分页查询共用同一条件对象)
* @return 合计载体(amountCny 保留两位小数;无匹配数据时为 0.00)
*/
@SaCheckPermission
(
"ai:consume:list"
)
@GetMapping
(
"/summary"
)
public
SingleResponse
<
AiConsumeRecordSummaryVo
>
summary
(
AiConsumeRecordListParam
param
)
{
return
SingleResponse
.
of
(
aiConsumeRecordServ
.
querySummary
(
param
));
}
/**
* 导出AI工具消费记录列表。
*
...
...
@@ -71,6 +89,18 @@ public class AiConsumeRecordCtrl {
return
SingleResponse
.
of
(
aiConsumeRecordServ
.
queryById
(
id
));
}
/**
* 按批次号获取整批消费记录详情(主从批量编辑回填用)。
*
* @param batchNo 批次号
* @return 整批详情(公共字段 + details 明细列表,line_sort 升序)
*/
@SaCheckPermission
(
"ai:consume:query"
)
@GetMapping
(
"/batch/{batchNo}"
)
public
SingleResponse
<
AiConsumeRecordVo
>
getBatchDetail
(
@PathVariable
String
batchNo
)
{
return
SingleResponse
.
of
(
aiConsumeRecordServ
.
queryByBatchNo
(
batchNo
));
}
/**
* 新增AI工具消费记录。
*
...
...
@@ -109,4 +139,16 @@ public class AiConsumeRecordCtrl {
@PathVariable
Long
[]
ids
)
{
return
ResponseUtils
.
ofResult
(
aiConsumeRecordServ
.
delByIds
(
List
.
of
(
ids
)));
}
/**
* 获取消费记录筛选用的部门树。
*
* @param dept 部门查询条件
* @return 部门树列表
*/
@SaCheckPermission
(
"ai:consume:list"
)
@GetMapping
(
"/deptTree"
)
public
MultiResponse
<
Tree
<
Long
>>
deptTree
(
SysDeptParam
dept
)
{
return
MultiResponse
.
of
(
sysDeptServ
.
selectDeptTreeList
(
dept
));
}
}
admin/src/main/java/com/anplus/hr/ai/controller/AiRechargeRecordCtrl.java
View file @
b9f1ac5d
package
com
.
anplus
.
hr
.
ai
.
controller
;
import
cn.dev33.satoken.annotation.SaCheckPermission
;
import
cn.hutool.core.lang.tree.Tree
;
import
com.alibaba.cola.dto.MultiResponse
;
import
com.alibaba.cola.dto.PageResponse
;
import
com.alibaba.cola.dto.Response
;
import
com.alibaba.cola.dto.SingleResponse
;
...
...
@@ -11,8 +13,11 @@ import org.springframework.validation.annotation.Validated;
import
org.springframework.web.bind.annotation.*
;
import
com.anplus.hr.ai.domain.params.AiRechargeRecordListParam
;
import
com.anplus.hr.ai.domain.params.AiRechargeRecordParam
;
import
com.anplus.hr.ai.domain.vo.AiRechargeRecordSummaryVo
;
import
com.anplus.hr.ai.domain.vo.AiRechargeRecordVo
;
import
com.anplus.hr.ai.service.AiRechargeRecordServ
;
import
top.binfast.app.biz.sysapi.bean.params.sysDept.SysDeptParam
;
import
top.binfast.app.biz.sysbiz.service.SysDeptServ
;
import
top.binfast.common.core.constant.BusinessType
;
import
top.binfast.common.core.util.ResponseUtils
;
import
top.binfast.common.excel.annotion.ExcelExport
;
...
...
@@ -33,6 +38,7 @@ import java.util.List;
public
class
AiRechargeRecordCtrl
{
private
final
AiRechargeRecordServ
aiRechargeRecordServ
;
private
final
SysDeptServ
sysDeptServ
;
/**
* 分页查询AI工具充值记录列表。
...
...
@@ -46,6 +52,18 @@ public class AiRechargeRecordCtrl {
return
aiRechargeRecordServ
.
queryPageList
(
param
);
}
/**
* 查询当前筛选条件下的双金额合计(跨所有分页的全量结果,口径与列表完全一致)。
*
* @param param 查询条件(与分页查询共用同一条件对象)
* @return 合计载体(amountUsd、amountCny 各保留两位小数;无匹配数据时为 0.00)
*/
@SaCheckPermission
(
"ai:recharge:list"
)
@GetMapping
(
"/summary"
)
public
SingleResponse
<
AiRechargeRecordSummaryVo
>
summary
(
AiRechargeRecordListParam
param
)
{
return
SingleResponse
.
of
(
aiRechargeRecordServ
.
querySummary
(
param
));
}
/**
* 导出AI工具充值记录列表。
*
...
...
@@ -109,4 +127,16 @@ public class AiRechargeRecordCtrl {
@PathVariable
Long
[]
ids
)
{
return
ResponseUtils
.
ofResult
(
aiRechargeRecordServ
.
delByIds
(
List
.
of
(
ids
)));
}
/**
* 获取充值记录筛选用的部门树。
*
* @param dept 部门查询条件
* @return 部门树列表
*/
@SaCheckPermission
(
"ai:recharge:list"
)
@GetMapping
(
"/deptTree"
)
public
MultiResponse
<
Tree
<
Long
>>
deptTree
(
SysDeptParam
dept
)
{
return
MultiResponse
.
of
(
sysDeptServ
.
selectDeptTreeList
(
dept
));
}
}
admin/src/main/java/com/anplus/hr/ai/domain/model/AiConsumeRecord.java
View file @
b9f1ac5d
...
...
@@ -8,7 +8,7 @@ import lombok.EqualsAndHashCode;
import
java.io.Serial
;
import
java.math.BigDecimal
;
import
java.time.LocalDate
Time
;
import
java.time.LocalDate
;
/**
* AI工具消费记录对象 ai_consume_record
...
...
@@ -26,7 +26,12 @@ public class AiConsumeRecord extends TenantModel {
private
static
final
long
serialVersionUID
=
1L
;
/**
* 项目名称(字典ai_project_name的dict_value)
* 所属部门ID(关联sys_dept.id)
*/
private
Long
deptId
;
/**
* 项目名称(用户直接输入的自由文本)
*/
private
String
projectName
;
...
...
@@ -36,7 +41,7 @@ public class AiConsumeRecord extends TenantModel {
private
String
userName
;
/**
* AI工具/平台名称(
字典ai_tool_name的dict_value
)
* AI工具/平台名称(
用户直接输入的自由文本
)
*/
private
String
toolName
;
...
...
@@ -58,11 +63,21 @@ public class AiConsumeRecord extends TenantModel {
/**
* 消费发生时间
*/
private
LocalDate
Time
consumeTime
;
private
LocalDate
consumeTime
;
/**
* 备注
*/
private
String
remark
;
/**
* 批次号(主从批量录入:同批多行共享同一 batch_no,全局唯一)
*/
private
String
batchNo
;
/**
* 行序号(批次内从 1 连续自增)
*/
private
Integer
lineSort
;
}
admin/src/main/java/com/anplus/hr/ai/domain/model/AiRechargeRecord.java
View file @
b9f1ac5d
...
...
@@ -9,7 +9,6 @@ import lombok.EqualsAndHashCode;
import
java.io.Serial
;
import
java.math.BigDecimal
;
import
java.time.LocalDate
;
import
java.time.LocalDateTime
;
/**
* AI工具充值记录对象 ai_recharge_record
...
...
@@ -27,7 +26,12 @@ public class AiRechargeRecord extends TenantModel {
private
static
final
long
serialVersionUID
=
1L
;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
* 所属部门ID(关联sys_dept.id)
*/
private
Long
deptId
;
/**
* AI工具/平台名称(用户直接输入的自由文本)
*/
private
String
toolName
;
...
...
@@ -44,7 +48,7 @@ public class AiRechargeRecord extends TenantModel {
/**
* 充值完成时间
*/
private
LocalDate
Time
rechargeTime
;
private
LocalDate
rechargeTime
;
/**
* 到期时间(纯字段,无联动)
...
...
admin/src/main/java/com/anplus/hr/ai/domain/params/AiConsumeDetailParam.java
0 → 100644
View file @
b9f1ac5d
package
com
.
anplus
.
hr
.
ai
.
domain
.
params
;
import
jakarta.validation.constraints.Digits
;
import
jakarta.validation.constraints.NotBlank
;
import
jakarta.validation.constraints.Pattern
;
import
lombok.Data
;
import
java.math.BigDecimal
;
/**
* AI工具消费记录明细行业务对象(主从批量录入的子表行)
*
* <p>对应 {@code ai_consume_record} 单行的工具维度字段。消耗积分与金额维持非必填
* (Clarify Q4:对齐既有 baseline 与现有代码口径,放宽需求文档「必填」措辞),
* 仅约束最多 2 位小数。校验注解均不带 groups,由父对象 {@code @Valid} 级联触发。</p>
*
* @author LiuBin
* @date 2026-09-08
*/
@Data
public
class
AiConsumeDetailParam
{
/**
* AI工具/平台名称(用户直接输入的自由文本)
*/
@NotBlank
(
message
=
"AI工具/平台名称不能为空"
)
private
String
toolName
;
/**
* 国内/国外(中文原文,固定二值)
*/
@NotBlank
(
message
=
"国内/国外不能为空"
)
@Pattern
(
regexp
=
"国内|国外"
,
message
=
"国内/国外仅接受「国内」或「国外」"
)
private
String
region
;
/**
* 消耗积分(非必填,最多2位小数)
*/
@Digits
(
integer
=
16
,
fraction
=
2
,
message
=
"消耗积分最多2位小数"
)
private
BigDecimal
consumePoints
;
/**
* 金额(人民币)(非必填,最多2位小数)
*/
@Digits
(
integer
=
16
,
fraction
=
2
,
message
=
"金额(人民币)最多2位小数"
)
private
BigDecimal
amountCny
;
}
admin/src/main/java/com/anplus/hr/ai/domain/params/AiConsumeRecordListParam.java
View file @
b9f1ac5d
...
...
@@ -18,7 +18,7 @@ import java.util.Map;
public
class
AiConsumeRecordListParam
extends
PageQueryParam
{
/**
* 项目名称(
字典ai_project_name的dict_value
)
* 项目名称(
用户直接输入的自由文本
)
*/
private
String
projectName
;
...
...
@@ -28,10 +28,15 @@ public class AiConsumeRecordListParam extends PageQueryParam {
private
String
userName
;
/**
* AI工具/平台名称(
字典ai_tool_name的dict_value
)
* AI工具/平台名称(
用户直接输入的自由文本
)
*/
private
String
toolName
;
/**
* 归属部门id(部门树,含子部门)
*/
private
Long
belongDeptId
;
private
Map
<
String
,
Object
>
params
=
new
HashMap
<>();
}
admin/src/main/java/com/anplus/hr/ai/domain/params/AiConsumeRecordParam.java
View file @
b9f1ac5d
package
com
.
anplus
.
hr
.
ai
.
domain
.
params
;
import
io.github.linpeilie.annotations.AutoMapper
;
import
jakarta.validation.constraints.Digits
;
import
jakarta.validation.Valid
;
import
jakarta.validation.constraints.NotBlank
;
import
jakarta.validation.constraints.NotEmpty
;
import
jakarta.validation.constraints.NotNull
;
import
jakarta.validation.constraints.Pattern
;
import
jakarta.validation.constraints.Size
;
import
lombok.Data
;
import
com.anplus.hr.ai.domain.model.AiConsumeRecord
;
import
java.
math.BigDecimal
;
import
java.
time.LocalDateTime
;
import
java.
time.LocalDate
;
import
java.
util.List
;
/**
* AI工具消费记录业务对象 ai_consume_record
* AI工具消费记录业务对象(主从批量录入)ai_consume_record
*
* <p>上部公共字段对整批生效,下部 {@code details} 为明细行列表;新增时 {@code batchNo}
* 为空由服务端生成,编辑时携带 {@code batchNo} 定位原批(整批全删重插)。校验注解均不带
* groups,配合 Controller 的 {@code @Validated} 默认组生效(对齐既有 Param 风格)。</p>
*
* @author LiuBin
* @date 2026-09-04
*/
@Data
@AutoMapper
(
target
=
AiConsumeRecord
.
class
,
reverseConvertGenerate
=
false
)
public
class
AiConsumeRecordParam
{
/**
*
主键ID(新增为空,修改必填
)
*
批次号(编辑时定位原批;新增时为空、由服务端生成
)
*/
private
Long
id
;
private
String
batchNo
;
/**
* 项目名称(字典ai_project_name的dict_value)
* 所属部门ID(关联sys_dept.id)
*/
@NotNull
(
message
=
"所属部门不能为空"
)
private
Long
deptId
;
/**
* 项目名称(用户直接输入的自由文本)
*/
@NotBlank
(
message
=
"项目名称不能为空"
)
private
String
projectName
;
...
...
@@ -39,36 +46,11 @@ public class AiConsumeRecordParam {
@NotBlank
(
message
=
"使用人姓名不能为空"
)
private
String
userName
;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
*/
@NotBlank
(
message
=
"AI工具/平台名称不能为空"
)
private
String
toolName
;
/**
* 国内/国外(中文原文,固定二值)
*/
@NotBlank
(
message
=
"国内/国外不能为空"
)
@Pattern
(
regexp
=
"国内|国外"
,
message
=
"国内/国外仅接受「国内」或「国外」"
)
private
String
region
;
/**
* 消耗积分
*/
@Digits
(
integer
=
16
,
fraction
=
2
,
message
=
"消耗积分最多2位小数"
)
private
BigDecimal
consumePoints
;
/**
* 金额(人民币)
*/
@Digits
(
integer
=
16
,
fraction
=
2
,
message
=
"金额(人民币)最多2位小数"
)
private
BigDecimal
amountCny
;
/**
* 消费发生时间
*/
@NotNull
(
message
=
"消费发生时间不能为空"
)
private
LocalDate
Time
consumeTime
;
private
LocalDate
consumeTime
;
/**
* 备注
...
...
@@ -76,4 +58,11 @@ public class AiConsumeRecordParam {
@Size
(
max
=
200
,
message
=
"备注最长200字符"
)
private
String
remark
;
/**
* 消费明细行列表(至少一行,级联校验每行)
*/
@Valid
@NotEmpty
(
message
=
"消费明细不能为空"
)
private
List
<
AiConsumeDetailParam
>
details
;
}
admin/src/main/java/com/anplus/hr/ai/domain/params/AiRechargeRecordListParam.java
View file @
b9f1ac5d
...
...
@@ -18,7 +18,7 @@ import java.util.Map;
public
class
AiRechargeRecordListParam
extends
PageQueryParam
{
/**
* AI工具/平台名称(
字典ai_tool_name的dict_value
)
* AI工具/平台名称(
用户直接输入的自由文本
)
*/
private
String
toolName
;
...
...
@@ -32,6 +32,11 @@ public class AiRechargeRecordListParam extends PageQueryParam {
*/
private
String
rechargeCategory
;
/**
* 归属部门id(部门树,含子部门)
*/
private
Long
belongDeptId
;
private
Map
<
String
,
Object
>
params
=
new
HashMap
<>();
}
admin/src/main/java/com/anplus/hr/ai/domain/params/AiRechargeRecordParam.java
View file @
b9f1ac5d
...
...
@@ -12,7 +12,6 @@ import com.anplus.hr.ai.domain.model.AiRechargeRecord;
import
java.math.BigDecimal
;
import
java.time.LocalDate
;
import
java.time.LocalDateTime
;
/**
* AI工具充值记录业务对象 ai_recharge_record
...
...
@@ -30,7 +29,13 @@ public class AiRechargeRecordParam {
private
Long
id
;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
* 所属部门ID(关联sys_dept.id)
*/
@NotNull
(
message
=
"所属部门不能为空"
)
private
Long
deptId
;
/**
* AI工具/平台名称(用户直接输入的自由文本)
*/
@NotBlank
(
message
=
"AI工具/平台名称不能为空"
)
private
String
toolName
;
...
...
@@ -52,7 +57,7 @@ public class AiRechargeRecordParam {
* 充值完成时间
*/
@NotNull
(
message
=
"充值完成时间不能为空"
)
private
LocalDate
Time
rechargeTime
;
private
LocalDate
rechargeTime
;
/**
* 到期时间(纯字段,无联动)
...
...
admin/src/main/java/com/anplus/hr/ai/domain/vo/AiConsumeRecordSummaryVo.java
0 → 100644
View file @
b9f1ac5d
package
com
.
anplus
.
hr
.
ai
.
domain
.
vo
;
import
lombok.Data
;
import
java.io.Serial
;
import
java.io.Serializable
;
import
java.math.BigDecimal
;
/**
* AI工具消费记录合计视图对象(列表底部合计行 / GET /ai/consume/summary 载体)
*
* @author LiuBin
* @date 2026-09-08
*/
@Data
public
class
AiConsumeRecordSummaryVo
implements
Serializable
{
@Serial
private
static
final
long
serialVersionUID
=
1L
;
/**
* 金额(人民币)合计:当前查询条件筛选出的全部结果(跨所有分页)amountCny 求和,保留两位小数
*/
private
BigDecimal
amountCny
;
}
admin/src/main/java/com/anplus/hr/ai/domain/vo/AiConsumeRecordVo.java
View file @
b9f1ac5d
package
com
.
anplus
.
hr
.
ai
.
domain
.
vo
;
import
com.fasterxml.jackson.annotation.JsonInclude
;
import
io.github.linpeilie.annotations.AutoMapper
;
import
lombok.Data
;
import
org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated
;
import
org.apache.fesod.sheet.annotation.ExcelProperty
;
import
com.anplus.hr.ai.domain.model.AiConsumeRecord
;
import
top.binfast.common.
excel.annotion.ExcelDictFormat
;
import
top.binfast.common.
excel.converters.ExcelDictConver
t
;
import
top.binfast.common.
translation.annotation.Translation
;
import
top.binfast.common.
translation.constant.TransConstan
t
;
import
java.io.Serial
;
import
java.io.Serializable
;
import
java.math.BigDecimal
;
import
java.time.LocalDate
;
import
java.time.LocalDateTime
;
import
java.util.List
;
/**
...
...
@@ -34,10 +37,31 @@ public class AiConsumeRecordVo implements Serializable {
private
Long
id
;
/**
*
项目名称(字典ai_project_name的dict_value
)
*
批次号(主从批量录入元数据;非展示、非导出,前端编辑流按此定位整批
)
*/
@ExcelProperty
(
value
=
"项目名称"
,
converter
=
ExcelDictConvert
.
class
)
@ExcelDictFormat
(
dictType
=
"ai_project_name"
)
private
String
batchNo
;
/**
* 行序号(批次内从 1 连续;非展示、非导出)
*/
private
Integer
lineSort
;
/**
* 所属部门ID(关联sys_dept.id)
*/
private
Long
deptId
;
/**
* 所属部门名称(@Translation 列表自动翻译;导出由 ServiceImpl 批量回填)
*/
@Translation
(
type
=
TransConstant
.
DEPT_ID_TO_NAME
,
mapper
=
"deptId"
)
@ExcelProperty
(
value
=
"所属部门"
)
private
String
deptName
;
/**
* 项目名称(用户直接输入的自由文本)
*/
@ExcelProperty
(
value
=
"项目名称"
)
private
String
projectName
;
/**
...
...
@@ -47,10 +71,9 @@ public class AiConsumeRecordVo implements Serializable {
private
String
userName
;
/**
* AI工具/平台名称(
字典ai_tool_name的dict_value
)
* AI工具/平台名称(
用户直接输入的自由文本
)
*/
@ExcelProperty
(
value
=
"AI工具/平台名称"
,
converter
=
ExcelDictConvert
.
class
)
@ExcelDictFormat
(
dictType
=
"ai_tool_name"
)
@ExcelProperty
(
value
=
"AI工具/平台名称"
)
private
String
toolName
;
/**
...
...
@@ -75,7 +98,7 @@ public class AiConsumeRecordVo implements Serializable {
* 消费发生时间
*/
@ExcelProperty
(
value
=
"消费时间"
)
private
LocalDate
Time
consumeTime
;
private
LocalDate
consumeTime
;
/**
* 备注
...
...
@@ -85,4 +108,11 @@ public class AiConsumeRecordVo implements Serializable {
private
LocalDateTime
createTime
;
/**
* 明细行列表(仅整批详情接口 GET /ai/consume/batch/{batchNo} 返回;
* 列表与单条详情为 null 且不参与 JSON 序列化;非展示、非导出)
*/
@JsonInclude
(
JsonInclude
.
Include
.
NON_NULL
)
private
List
<
AiConsumeRecordVo
>
details
;
}
admin/src/main/java/com/anplus/hr/ai/domain/vo/AiRechargeRecordSummaryVo.java
0 → 100644
View file @
b9f1ac5d
package
com
.
anplus
.
hr
.
ai
.
domain
.
vo
;
import
lombok.Data
;
import
java.io.Serial
;
import
java.io.Serializable
;
import
java.math.BigDecimal
;
/**
* AI工具充值记录合计视图对象(列表底部合计行 / GET /ai/recharge/summary 载体)
*
* @author LiuBin
* @date 2026-09-08
*/
@Data
public
class
AiRechargeRecordSummaryVo
implements
Serializable
{
@Serial
private
static
final
long
serialVersionUID
=
1L
;
/**
* 充值金额(美元)合计:当前查询条件筛选出的全部结果(跨所有分页)amountUsd 求和,保留两位小数
*/
private
BigDecimal
amountUsd
;
/**
* 充值金额(人民币)合计:当前查询条件筛选出的全部结果(跨所有分页)amountCny 求和,保留两位小数
*/
private
BigDecimal
amountCny
;
}
admin/src/main/java/com/anplus/hr/ai/domain/vo/AiRechargeRecordVo.java
View file @
b9f1ac5d
...
...
@@ -7,6 +7,8 @@ import org.apache.fesod.sheet.annotation.ExcelProperty;
import
com.anplus.hr.ai.domain.model.AiRechargeRecord
;
import
top.binfast.common.excel.annotion.ExcelDictFormat
;
import
top.binfast.common.excel.converters.ExcelDictConvert
;
import
top.binfast.common.translation.annotation.Translation
;
import
top.binfast.common.translation.constant.TransConstant
;
import
java.io.Serial
;
import
java.io.Serializable
;
...
...
@@ -35,10 +37,21 @@ public class AiRechargeRecordVo implements Serializable {
private
Long
id
;
/**
*
AI工具/平台名称(字典ai_tool_name的dict_value
)
*
所属部门ID(关联sys_dept.id
)
*/
@ExcelProperty
(
value
=
"AI工具/平台名称"
,
converter
=
ExcelDictConvert
.
class
)
@ExcelDictFormat
(
dictType
=
"ai_tool_name"
)
private
Long
deptId
;
/**
* 所属部门名称(@Translation 列表自动翻译;导出由 ServiceImpl 批量回填)
*/
@Translation
(
type
=
TransConstant
.
DEPT_ID_TO_NAME
,
mapper
=
"deptId"
)
@ExcelProperty
(
value
=
"所属部门"
)
private
String
deptName
;
/**
* AI工具/平台名称(用户直接输入的自由文本)
*/
@ExcelProperty
(
value
=
"AI工具/平台名称"
)
private
String
toolName
;
/**
...
...
@@ -58,7 +71,7 @@ public class AiRechargeRecordVo implements Serializable {
* 充值完成时间
*/
@ExcelProperty
(
value
=
"充值完成时间"
)
private
LocalDate
Time
rechargeTime
;
private
LocalDate
rechargeTime
;
/**
* 到期时间(纯字段,无联动)
...
...
admin/src/main/java/com/anplus/hr/ai/mapper/AiConsumeRecordMapper.java
View file @
b9f1ac5d
package
com
.
anplus
.
hr
.
ai
.
mapper
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.github.yulichang.base.MPJBaseMapper
;
import
org.apache.ibatis.annotations.Mapper
;
import
org.apache.ibatis.annotations.Param
;
import
com.anplus.hr.ai.domain.model.AiConsumeRecord
;
import
top.binfast.common.mybatis.mapper.BinBaseMapper
;
import
java.util.List
;
/**
* AI工具消费记录Mapper接口
*
...
...
@@ -14,4 +18,29 @@ import top.binfast.common.mybatis.mapper.BinBaseMapper;
@Mapper
public
interface
AiConsumeRecordMapper
extends
BinBaseMapper
<
AiConsumeRecord
>,
MPJBaseMapper
<
AiConsumeRecord
>
{
/**
* 按批次号查询整批记录(line_sort 升序)。
*
* <p>租户过滤由全局 TenantLineInnerInterceptor 自动注入,逻辑删除行由 @TableLogic 自动排除。</p>
*
* @param batchNo 批次号
* @return 该批次下的全部有效记录,按行序号升序
*/
default
List
<
AiConsumeRecord
>
selectByBatchNo
(
String
batchNo
)
{
return
this
.
selectList
(
new
LambdaQueryWrapper
<
AiConsumeRecord
>()
.
eq
(
AiConsumeRecord:
:
getBatchNo
,
batchNo
)
.
orderByAsc
(
AiConsumeRecord:
:
getLineSort
));
}
/**
* 按批次号物理删除整批记录(编辑「整批全删重插」用,绕过 @TableLogic 逻辑删除)。
*
* <p>SQL 显式带 tenant_id 条件(design.md D3 / R1),杜绝跨租户误删同 batch_no 行。</p>
*
* @param batchNo 批次号
* @param tenantId 当前租户ID
* @return 删除的行数
*/
int
deleteByBatchNo
(
@Param
(
"batchNo"
)
String
batchNo
,
@Param
(
"tenantId"
)
Long
tenantId
);
}
admin/src/main/java/com/anplus/hr/ai/service/AiConsumeRecordServ.java
View file @
b9f1ac5d
...
...
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.spring.service.IService;
import
com.anplus.hr.ai.domain.model.AiConsumeRecord
;
import
com.anplus.hr.ai.domain.params.AiConsumeRecordListParam
;
import
com.anplus.hr.ai.domain.params.AiConsumeRecordParam
;
import
com.anplus.hr.ai.domain.vo.AiConsumeRecordSummaryVo
;
import
com.anplus.hr.ai.domain.vo.AiConsumeRecordVo
;
import
java.util.List
;
...
...
@@ -33,6 +34,14 @@ public interface AiConsumeRecordServ extends IService<AiConsumeRecord> {
*/
List
<
AiConsumeRecordVo
>
queryList
(
AiConsumeRecordListParam
param
);
/**
* 查询当前条件下的金额合计(跨所有分页的全量筛选结果,口径与列表完全一致)
*
* @param param 查询条件(与分页查询共用同一条件对象 AiConsumeRecordListParam)
* @return 合计载体(amountCny 保留两位小数;无匹配数据时为 0.00)
*/
AiConsumeRecordSummaryVo
querySummary
(
AiConsumeRecordListParam
param
);
/**
* 查询AI工具消费记录
*
...
...
@@ -41,6 +50,14 @@ public interface AiConsumeRecordServ extends IService<AiConsumeRecord> {
*/
AiConsumeRecordVo
queryById
(
Long
id
);
/**
* 按批次号查询整批详情(公共字段 + 明细列表,line_sort 升序)
*
* @param batchNo 批次号
* @return 整批详情;批次不存在或跨租户返回 null
*/
AiConsumeRecordVo
queryByBatchNo
(
String
batchNo
);
/**
* 新增AI工具消费记录
*
...
...
admin/src/main/java/com/anplus/hr/ai/service/AiRechargeRecordServ.java
View file @
b9f1ac5d
...
...
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.spring.service.IService;
import
com.anplus.hr.ai.domain.model.AiRechargeRecord
;
import
com.anplus.hr.ai.domain.params.AiRechargeRecordListParam
;
import
com.anplus.hr.ai.domain.params.AiRechargeRecordParam
;
import
com.anplus.hr.ai.domain.vo.AiRechargeRecordSummaryVo
;
import
com.anplus.hr.ai.domain.vo.AiRechargeRecordVo
;
import
java.util.List
;
...
...
@@ -33,6 +34,14 @@ public interface AiRechargeRecordServ extends IService<AiRechargeRecord> {
*/
List
<
AiRechargeRecordVo
>
queryList
(
AiRechargeRecordListParam
param
);
/**
* 查询当前条件下的双金额合计(跨所有分页的全量筛选结果,口径与列表完全一致)
*
* @param param 查询条件(与分页查询共用同一条件对象 AiRechargeRecordListParam)
* @return 合计载体(amountUsd、amountCny 各保留两位小数;无匹配数据时为 0.00)
*/
AiRechargeRecordSummaryVo
querySummary
(
AiRechargeRecordListParam
param
);
/**
* 查询AI工具充值记录
*
...
...
admin/src/main/java/com/anplus/hr/ai/service/impl/AiConsumeRecordServImpl.java
View file @
b9f1ac5d
This diff is collapsed.
Click to expand it.
admin/src/main/java/com/anplus/hr/ai/service/impl/AiRechargeRecordServImpl.java
View file @
b9f1ac5d
package
com
.
anplus
.
hr
.
ai
.
service
.
impl
;
import
cn.hutool.core.collection.CollUtil
;
import
cn.hutool.core.convert.Convert
;
import
cn.hutool.core.util.ObjectUtil
;
import
com.alibaba.cola.dto.PageResponse
;
import
com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper
;
import
com.baomidou.mybatisplus.extension.plugins.pagination.Page
;
...
...
@@ -9,16 +12,23 @@ import org.springframework.stereotype.Service;
import
com.anplus.hr.ai.domain.model.AiRechargeRecord
;
import
com.anplus.hr.ai.domain.params.AiRechargeRecordListParam
;
import
com.anplus.hr.ai.domain.params.AiRechargeRecordParam
;
import
com.anplus.hr.ai.domain.vo.AiRechargeRecordSummaryVo
;
import
com.anplus.hr.ai.domain.vo.AiRechargeRecordVo
;
import
com.anplus.hr.ai.mapper.AiRechargeRecordMapper
;
import
com.anplus.hr.ai.service.AiRechargeRecordServ
;
import
top.binfast.app.biz.sysapi.dao.auth.SysDeptMapper
;
import
top.binfast.app.biz.sysbiz.service.SysDeptServ
;
import
top.binfast.common.core.util.MapstructUtils
;
import
top.binfast.common.mybatis.query.LambdaQueryBuilder
;
import
top.binfast.common.mybatis.query.QueryBuilder
;
import
top.binfast.common.mybatis.util.QueryUtil
;
import
java.math.BigDecimal
;
import
java.math.RoundingMode
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Set
;
import
java.util.stream.Collectors
;
/**
* AI工具充值记录Service业务层处理
...
...
@@ -31,6 +41,8 @@ import java.util.Map;
public
class
AiRechargeRecordServImpl
extends
ServiceImpl
<
AiRechargeRecordMapper
,
AiRechargeRecord
>
implements
AiRechargeRecordServ
{
private
final
AiRechargeRecordMapper
aiRechargeRecordMapper
;
private
final
SysDeptMapper
deptMapper
;
private
final
SysDeptServ
sysDeptServ
;
/**
* 分页查询AI工具充值记录列表
...
...
@@ -55,18 +67,81 @@ public class AiRechargeRecordServImpl extends ServiceImpl<AiRechargeRecordMapper
@Override
public
List
<
AiRechargeRecordVo
>
queryList
(
AiRechargeRecordListParam
param
)
{
LambdaQueryWrapper
<
AiRechargeRecord
>
lambdaQuery
=
this
.
buildQueryWrapper
(
param
);
return
MapstructUtils
.
convert
(
aiRechargeRecordMapper
.
selectList
(
lambdaQuery
),
AiRechargeRecordVo
.
class
);
List
<
AiRechargeRecordVo
>
list
=
MapstructUtils
.
convert
(
aiRechargeRecordMapper
.
selectList
(
lambdaQuery
),
AiRechargeRecordVo
.
class
);
//导出路径批量回填部门名:@Translation 仅在 JSON 响应增强管线生效,Excel 导出走 fesod 直接反射读字段,需手动回填
Set
<
Long
>
deptIds
=
list
.
stream
()
.
map
(
AiRechargeRecordVo:
:
getDeptId
)
.
filter
(
ObjectUtil:
:
isNotNull
)
.
collect
(
Collectors
.
toSet
());
if
(
CollUtil
.
isNotEmpty
(
deptIds
))
{
Map
<
Long
,
String
>
deptNames
=
sysDeptServ
.
selectDeptNamesByIds
(
deptIds
);
list
.
forEach
(
vo
->
vo
.
setDeptName
(
deptNames
.
get
(
vo
.
getDeptId
())));
}
//导出末尾追加合计行:全量数据已在内存,两金额列各流式求和(filter nonNull 与 SQL SUM 忽略 NULL 语义一致),不发第二次 SQL(design D7)
BigDecimal
amountUsdTotal
=
list
.
stream
()
.
map
(
AiRechargeRecordVo:
:
getAmountUsd
)
.
filter
(
ObjectUtil:
:
isNotNull
)
.
reduce
(
BigDecimal
.
ZERO
,
BigDecimal:
:
add
)
.
setScale
(
2
,
RoundingMode
.
HALF_UP
);
BigDecimal
amountCnyTotal
=
list
.
stream
()
.
map
(
AiRechargeRecordVo:
:
getAmountCny
)
.
filter
(
ObjectUtil:
:
isNotNull
)
.
reduce
(
BigDecimal
.
ZERO
,
BigDecimal:
:
add
)
.
setScale
(
2
,
RoundingMode
.
HALF_UP
);
//合计行仅 deptName + 两金额有值,其余字段(含 rechargeCategory)保持 null:
//rechargeCategory=null 经 ExcelDictConvert null 分支落空单元格,不触发字典转换异常(design D7/Risks)
AiRechargeRecordVo
totalRow
=
new
AiRechargeRecordVo
();
totalRow
.
setDeptName
(
"合计"
);
totalRow
.
setAmountUsd
(
amountUsdTotal
);
totalRow
.
setAmountCny
(
amountCnyTotal
);
list
.
add
(
totalRow
);
return
list
;
}
private
LambdaQueryWrapper
<
AiRechargeRecord
>
buildQueryWrapper
(
AiRechargeRecordListParam
param
)
{
/**
* 查询当前条件下的双金额合计(跨所有分页的全量筛选结果,口径与列表完全一致)。
*
* @param param 查询条件(与分页查询共用同一条件对象)
* @return 合计载体(amountUsd、amountCny 各保留两位小数;无匹配数据时为 0.00)
*/
@Override
public
AiRechargeRecordSummaryVo
querySummary
(
AiRechargeRecordListParam
param
)
{
//amountUsd、amountCny 各构造一次聚合查询(均复用 buildCondition,不含 orderBy,规避 ONLY_FULL_GROUP_BY,design D3/D4)
List
<
Object
>
usdObjs
=
aiRechargeRecordMapper
.
selectObjs
(
buildCondition
(
param
).
selectSum
(
AiRechargeRecord:
:
getAmountUsd
).
build
());
List
<
Object
>
cnyObjs
=
aiRechargeRecordMapper
.
selectObjs
(
buildCondition
(
param
).
selectSum
(
AiRechargeRecord:
:
getAmountCny
).
build
());
//SUM 无 GROUP BY 恒返回单行;空结果集时该行为 null,落 ZERO(design D5)
BigDecimal
amountUsd
=
CollUtil
.
isEmpty
(
usdObjs
)
?
BigDecimal
.
ZERO
:
Convert
.
toBigDecimal
(
usdObjs
.
get
(
0
),
BigDecimal
.
ZERO
);
BigDecimal
amountCny
=
CollUtil
.
isEmpty
(
cnyObjs
)
?
BigDecimal
.
ZERO
:
Convert
.
toBigDecimal
(
cnyObjs
.
get
(
0
),
BigDecimal
.
ZERO
);
AiRechargeRecordSummaryVo
summary
=
new
AiRechargeRecordSummaryVo
();
summary
.
setAmountUsd
(
amountUsd
.
setScale
(
2
,
RoundingMode
.
HALF_UP
));
summary
.
setAmountCny
(
amountCny
.
setScale
(
2
,
RoundingMode
.
HALF_UP
));
return
summary
;
}
/**
* 构造查询条件(不含排序),供列表查询与合计聚合共用,确保两者筛选口径完全一致(design D3)。
*
* @param param 查询条件
* @return 承载全部筛选条件(多租户 + 部门树子树 + 搜索条件 + 时间范围)的 builder,不含 orderBy
*/
private
LambdaQueryBuilder
<
AiRechargeRecord
>
buildCondition
(
AiRechargeRecordListParam
param
)
{
Map
<
String
,
Object
>
params
=
param
.
getParams
();
LambdaQueryBuilder
<
AiRechargeRecord
>
builder
=
QueryBuilder
.
lambda
(
AiRechargeRecord
.
class
)
return
QueryBuilder
.
lambda
(
AiRechargeRecord
.
class
)
.
eqIfText
(
AiRechargeRecord:
:
getToolName
,
param
.
getToolName
())
.
eqIfText
(
AiRechargeRecord:
:
getRegion
,
param
.
getRegion
())
.
eqIfText
(
AiRechargeRecord:
:
getRechargeCategory
,
param
.
getRechargeCategory
())
.
betweenParams
(
AiRechargeRecord:
:
getRechargeTime
,
params
,
"beginTime"
,
"endTime"
)
.
orderByDesc
(
AiRechargeRecord:
:
getRechargeTime
);
return
builder
.
build
();
//部门树筛选(含子部门子树,对齐 SysUserServImpl.buildQueryWrapper)
.
and
(
ObjectUtil
.
isNotNull
(
param
.
getBelongDeptId
()),
x
->
{
List
<
Long
>
deptIds
=
deptMapper
.
selectDeptAndChildById
(
param
.
getBelongDeptId
());
x
.
in
(
AiRechargeRecord:
:
getDeptId
,
deptIds
);
});
}
private
LambdaQueryWrapper
<
AiRechargeRecord
>
buildQueryWrapper
(
AiRechargeRecordListParam
param
)
{
return
buildCondition
(
param
).
orderByDesc
(
AiRechargeRecord:
:
getRechargeTime
).
build
();
}
/**
...
...
admin/src/main/resources/mapper/AiConsumeRecordMapper.xml
View file @
b9f1ac5d
...
...
@@ -4,4 +4,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper
namespace=
"com.anplus.hr.ai.mapper.AiConsumeRecordMapper"
>
<!--
按批次号物理删除整批(编辑「整批全删重插」用,绕过 @TableLogic 逻辑删除)。
显式带 tenant_id 条件(design.md D3 / R1),杜绝跨租户误删同 batch_no 行。
-->
<delete
id=
"deleteByBatchNo"
>
delete from ai_consume_record
where batch_no = #{batchNo} and tenant_id = #{tenantId}
</delete>
</mapper>
frontend/src/api/ai/consume/index.ts
View file @
b9f1ac5d
import
type
{
ID
,
IDS
,
PageQuery
,
PageResult
}
from
'@/api/common'
;
import
type
{
DeptTree
}
from
'@/api/system/user/model'
;
import
type
{
Consume
}
from
'./model'
;
import
type
{
Consume
,
ConsumeForm
,
ConsumeSummary
}
from
'./model'
;
import
{
commonExport
}
from
'@/api/helper'
;
import
{
alovaInstance
}
from
'@/utils/http'
;
...
...
@@ -8,6 +9,7 @@ import { alovaInstance } from '@/utils/http';
enum
Api
{
consumeExport
=
'/ai/consume/export'
,
consumeList
=
'/ai/consume/list'
,
consumeSummary
=
'/ai/consume/summary'
,
root
=
'/ai/consume'
,
}
...
...
@@ -20,6 +22,15 @@ export function consumeList(params?: PageQuery) {
return
alovaInstance
.
get
<
PageResult
<
Consume
>>
(
Api
.
consumeList
,
{
params
});
}
/**
* 查询消费记录金额合计(跨所有分页的全量筛选结果,口径与列表一致)
* @param params 请求参数(与列表查询共用同一条件)
* @returns 合计载体(amountCny)
*/
export
function
consumeSummary
(
params
?:
PageQuery
)
{
return
alovaInstance
.
get
<
ConsumeSummary
>
(
Api
.
consumeSummary
,
{
params
});
}
/**
* 导出消费记录excel
* @param data 请求参数
...
...
@@ -29,7 +40,7 @@ export function consumeExport(data: Partial<Consume>) {
}
/**
* 消费记录详情
* 消费记录详情
(单条)
* @param id id
* @returns 详情
*/
...
...
@@ -38,18 +49,27 @@ export function consumeInfo(id: ID) {
}
/**
* 消费记录新增
* @param data 参数
* 消费记录整批详情(主从批量录入编辑回填用)
* @param batchNo 批次号
* @returns 整批详情(公共字段 + details 明细列表)
*/
export
function
consumeBatchDetail
(
batchNo
:
string
)
{
return
alovaInstance
.
get
<
Consume
>
(
`
${
Api
.
root
}
/batch/
${
batchNo
}
`
);
}
/**
* 消费记录新增(主从批量:公共字段 + details 明细数组)
* @param data 批量入参
*/
export
function
consumeAdd
(
data
:
Partial
<
Consume
>
)
{
export
function
consumeAdd
(
data
:
ConsumeForm
)
{
return
alovaInstance
.
postWithMsg
<
void
>
(
Api
.
root
,
data
);
}
/**
* 消费记录修改
* @param data
参数
* 消费记录修改
(主从批量:整批全删重插)
* @param data
批量入参
*/
export
function
consumeUpdate
(
data
:
Partial
<
Consume
>
)
{
export
function
consumeUpdate
(
data
:
ConsumeForm
)
{
return
alovaInstance
.
putWithMsg
<
void
>
(
Api
.
root
,
data
);
}
...
...
@@ -60,3 +80,11 @@ export function consumeUpdate(data: Partial<Consume>) {
export
function
consumeRemove
(
ids
:
IDS
)
{
return
alovaInstance
.
deleteWithMsg
<
void
>
(
`
${
Api
.
root
}
/
${
ids
}
`
);
}
/**
* 消费记录专用 - 获取部门树
* @returns 部门树
*/
export
function
consumeDeptTreeSelect
()
{
return
alovaInstance
.
get
<
DeptTree
[]
>
(
'/ai/consume/deptTree'
);
}
frontend/src/api/ai/consume/model.d.ts
View file @
b9f1ac5d
...
...
@@ -4,7 +4,27 @@
export
interface
Consume
{
id
:
number
;
/**
* 项目名称(字典ai_project_name的dict_value)
* 批次号(主从批量录入元数据;非展示、非导出,编辑流按此定位整批)
*/
batchNo
?:
string
;
/**
* 行序号(批次内从 1 连续;非展示、非导出)
*/
lineSort
?:
number
;
/**
* 所属部门ID
*/
deptId
?:
number
;
/**
* 所属部门名称(后端 @Translation 翻译 / 导出回填)
*/
deptName
?:
string
;
/**
* 归属部门id(部门树查询用,含子部门)
*/
belongDeptId
?:
number
|
string
;
/**
* 项目名称(用户直接输入的自由文本)
*/
projectName
:
string
;
/**
...
...
@@ -12,7 +32,7 @@ export interface Consume {
*/
userName
:
string
;
/**
* AI工具/平台名称(
字典ai_tool_name的dict_value
)
* AI工具/平台名称(
用户直接输入的自由文本
)
*/
toolName
:
string
;
/**
...
...
@@ -36,4 +56,74 @@ export interface Consume {
*/
remark
:
string
;
createTime
:
string
;
/**
* 明细行列表(仅整批详情接口 GET /batch/{batchNo} 返回;列表与单条详情为 undefined)
*/
details
?:
Consume
[];
}
/**
* @description: 消费明细行入参(对应后端 AiConsumeDetailParam)
*/
export
interface
ConsumeDetailForm
{
/**
* AI工具/平台名称
*/
toolName
?:
string
;
/**
* 国内/国外(固定二值:国内 | 国外)
*/
region
?:
string
;
/**
* 消耗积分(非必填,最多 2 位小数)
*/
consumePoints
?:
number
;
/**
* 金额(人民币,非必填,最多 2 位小数)
*/
amountCny
?:
number
;
}
/**
* @description: 消费记录批量入参(主从结构,对应后端 AiConsumeRecordParam)
*/
export
interface
ConsumeForm
{
/**
* 批次号(编辑时定位原批;新增时为空、由服务端生成)
*/
batchNo
?:
string
;
/**
* 所属部门ID
*/
deptId
?:
number
;
/**
* 项目名称(用户直接输入的自由文本)
*/
projectName
?:
string
;
/**
* 使用人姓名
*/
userName
?:
string
;
/**
* 消费发生时间(日期类型,落库 date 当天日期)
*/
consumeTime
?:
string
;
/**
* 备注
*/
remark
?:
string
;
/**
* 消费明细行列表(至少一行)
*/
details
:
ConsumeDetailForm
[];
}
/**
* @description: 消费记录金额合计载体(对应后端 AiConsumeRecordSummaryVo)
*/
export
interface
ConsumeSummary
{
/**
* 金额(人民币)全量合计(两位小数)
*/
amountCny
:
number
;
}
frontend/src/api/ai/recharge/index.ts
View file @
b9f1ac5d
import
type
{
ID
,
IDS
,
PageQuery
,
PageResult
}
from
'@/api/common'
;
import
type
{
DeptTree
}
from
'@/api/system/user/model'
;
import
type
{
Recharge
}
from
'./model'
;
import
type
{
Recharge
,
RechargeSummary
}
from
'./model'
;
import
{
commonExport
}
from
'@/api/helper'
;
import
{
alovaInstance
}
from
'@/utils/http'
;
...
...
@@ -8,6 +9,7 @@ import { alovaInstance } from '@/utils/http';
enum
Api
{
rechargeExport
=
'/ai/recharge/export'
,
rechargeList
=
'/ai/recharge/list'
,
rechargeSummary
=
'/ai/recharge/summary'
,
root
=
'/ai/recharge'
,
}
...
...
@@ -20,6 +22,15 @@ export function rechargeList(params?: PageQuery) {
return
alovaInstance
.
get
<
PageResult
<
Recharge
>>
(
Api
.
rechargeList
,
{
params
});
}
/**
* 查询充值记录双金额合计(跨所有分页的全量筛选结果,口径与列表一致)
* @param params 请求参数(与列表查询共用同一条件)
* @returns 合计载体(amountUsd、amountCny)
*/
export
function
rechargeSummary
(
params
?:
PageQuery
)
{
return
alovaInstance
.
get
<
RechargeSummary
>
(
Api
.
rechargeSummary
,
{
params
});
}
/**
* 导出充值记录excel
* @param data 请求参数
...
...
@@ -60,3 +71,11 @@ export function rechargeUpdate(data: Partial<Recharge>) {
export
function
rechargeRemove
(
ids
:
IDS
)
{
return
alovaInstance
.
deleteWithMsg
<
void
>
(
`
${
Api
.
root
}
/
${
ids
}
`
);
}
/**
* 充值记录专用 - 获取部门树
* @returns 部门树
*/
export
function
rechargeDeptTreeSelect
()
{
return
alovaInstance
.
get
<
DeptTree
[]
>
(
'/ai/recharge/deptTree'
);
}
frontend/src/api/ai/recharge/model.d.ts
View file @
b9f1ac5d
...
...
@@ -4,7 +4,19 @@
export
interface
Recharge
{
id
:
number
;
/**
* AI工具/平台名称(字典ai_tool_name的dict_value)
* 所属部门ID
*/
deptId
?:
number
;
/**
* 所属部门名称(后端 @Translation 翻译 / 导出回填)
*/
deptName
?:
string
;
/**
* 归属部门id(部门树查询用,含子部门)
*/
belongDeptId
?:
number
|
string
;
/**
* AI工具/平台名称(用户直接输入的自由文本)
*/
toolName
:
string
;
/**
...
...
@@ -49,3 +61,17 @@ export interface Recharge {
remark
:
string
;
createTime
:
string
;
}
/**
* @description: 充值记录双金额合计载体(对应后端 AiRechargeRecordSummaryVo)
*/
export
interface
RechargeSummary
{
/**
* 充值金额(美元)全量合计(两位小数)
*/
amountUsd
:
number
;
/**
* 充值金额(人民币)全量合计(两位小数)
*/
amountCny
:
number
;
}
frontend/src/components/access/accessible.ts
View file @
b9f1ac5d
import
type
{
GenerateMenuAndRoutesOptions
,
RouteRecordRaw
}
from
'@/types'
;
import
type
{
GenerateMenuAndRoutesOptions
}
from
'@/types'
;
import
type
{
Component
,
DefineComponent
}
from
'vue'
;
...
...
@@ -8,7 +8,6 @@ import {
cloneDeep
,
generateMenus
,
generateRoutesByBackend
,
generateRoutesByFrontend
,
isFunction
,
isString
,
mapTree
,
...
...
@@ -80,16 +79,11 @@ async function generateAccessible(options: GenerateMenuAndRoutesOptions) {
* @param options
*/
async
function
generateRoutes
(
options
:
GenerateMenuAndRoutesOptions
)
{
const
{
forbiddenComponent
,
roles
,
routes
}
=
options
;
const
[
frontendResultRoutes
,
backendResultRoutes
]
=
await
Promise
.
all
([
generateRoutesByFrontend
(
routes
,
roles
||
[],
forbiddenComponent
),
generateRoutesByBackend
(
options
),
]);
let
resultRoutes
=
mergeRoutesByName
(
backendResultRoutes
,
frontendResultRoutes
,
);
// 本项目采用「后端权限模式」:后端 /auth/menu/getRouters 返回当前用户授权的路由
// name 集合,fetchMenuListAsync 已基于前端路由表完成过滤与组件解析,故直接以后端
// 结果为唯一真相源,不再并入前端模式。(roles 为空时 generateRoutesByFrontend 会
// 返回全量路由表,经合并会把后端已过滤掉的无权限路由又加回来,导致越权显示全部菜单。)
let
resultRoutes
=
await
generateRoutesByBackend
(
options
);
/**
* 调整路由树,做以下处理:
...
...
@@ -146,63 +140,4 @@ async function generateRoutes(options: GenerateMenuAndRoutesOptions) {
return
resultRoutes
;
}
/**
* 根据 name 合并前后端路由
* @param baseRoutes 后端路由
* @param extraRoutes 前端路由
*/
function
mergeRoutesByName
(
baseRoutes
:
RouteRecordRaw
[],
extraRoutes
:
RouteRecordRaw
[],
):
RouteRecordRaw
[]
{
const
result
:
RouteRecordRaw
[]
=
[];
const
routeMap
=
new
Map
<
string
,
RouteRecordRaw
>
();
for
(
const
route
of
baseRoutes
)
{
const
clone
=
{
...
route
}
as
RouteRecordRaw
;
result
.
push
(
clone
);
if
(
clone
.
name
&&
isString
(
clone
.
name
))
{
routeMap
.
set
(
clone
.
name
as
string
,
clone
);
}
}
for
(
const
route
of
extraRoutes
)
{
if
(
route
.
name
&&
isString
(
route
.
name
)
&&
routeMap
.
has
(
route
.
name
as
string
)
)
{
const
existing
=
routeMap
.
get
(
route
.
name
as
string
);
if
(
!
existing
)
{
continue
;
}
const
existingChildren
=
existing
.
children
??
[];
const
routeChildren
=
route
.
children
??
[];
const
merged
=
{
...
route
,
...
existing
,
// keep backend as base
meta
:
{
...
route
.
meta
,
...
existing
.
meta
,
// backend meta wins on conflicts
},
}
as
RouteRecordRaw
;
if
(
existingChildren
.
length
>
0
||
routeChildren
.
length
>
0
)
{
merged
.
children
=
mergeRoutesByName
(
existingChildren
,
routeChildren
);
}
Object
.
assign
(
existing
,
merged
);
}
else
{
const
clone
=
{
...
route
}
as
RouteRecordRaw
;
result
.
push
(
clone
);
if
(
clone
.
name
&&
isString
(
clone
.
name
))
{
routeMap
.
set
(
clone
.
name
as
string
,
clone
);
}
}
}
return
result
;
}
export
{
generateAccessible
};
frontend/src/constants/dict-enum.ts
View file @
b9f1ac5d
export
const
DictEnum
=
{
AI_PROJECT_NAME
:
'ai_project_name'
,
// 项目名称
AI_RECHARGE_CATEGORY
:
'ai_recharge_category'
,
// 充值分类
AI_TOOL_NAME
:
'ai_tool_name'
,
// AI工具/平台名称
SYS_COMMON_STATUS
:
'sys_common_status'
,
SYS_DEVICE_TYPE
:
'sys_device_type'
,
// 设备类型
SYS_GRANT_TYPE
:
'sys_grant_type'
,
// 授权类型
...
...
frontend/src/router/access.ts
View file @
b9f1ac5d
...
...
@@ -219,23 +219,25 @@ function backMenuToVbenMenuV2(
let
componentPath
=
''
;
// 如果是动态组件或异步加载函数,可以提取路径字符串
if
(
typeof
component
===
'function'
)
{
try
{
// 获取 () => import('xxx') 中的路径字符串
// componentPath =
// component.toString().match(/import\("(.*)"\)/)?.[1] || '';
const
match
=
component
.
toString
().
match
(
/import
\(
"
(
.*
)
"
\)
/
);
if
(
match
&&
match
[
1
])
{
// 替换 '/src' 为 '../views'
// componentPath = match[1].replace('/src', '..');
componentPath
=
meta
?.
componentPath
?
(
meta
.
componentPath
as
string
).
replace
(
'@/'
,
'../'
)
:
match
[
1
].
replace
(
'/src'
,
'..'
);
if
(
componentPath
.
includes
(
'?t='
))
{
componentPath
=
componentPath
.
split
(
'?'
)[
0
]
||
''
;
// 优先使用 meta.componentPath:路由静态声明,dev/生产一致可靠,
// 转换后(@/ → ../)正好匹配 import.meta.glob('../views/**/*.vue') 的 key。
if
(
meta
?.
componentPath
)
{
componentPath
=
(
meta
.
componentPath
as
string
).
replace
(
'@/'
,
'../'
);
}
else
{
// 回退:解析 () => import('xxx') 的源码字符串。
// 生产环境经 Rollup 转译后 component.toString() 不再是源码形式,
// 该回退仅在 dev 可靠,故动态路由务必显式配置 meta.componentPath。
try
{
const
match
=
component
.
toString
().
match
(
/import
\(
"
(
.*
)
"
\)
/
);
if
(
match
&&
match
[
1
])
{
componentPath
=
match
[
1
].
replace
(
'/src'
,
'..'
);
}
}
catch
{
console
.
warn
(
`无法解析组件路径:
${
String
(
route
.
name
)}
`
);
}
}
catch
{
console
.
warn
(
`无法解析组件路径:
${
String
(
route
.
name
)}
`
);
}
if
(
componentPath
.
includes
(
'?t='
))
{
componentPath
=
componentPath
.
split
(
'?'
)[
0
]
||
''
;
}
}
...
...
frontend/src/router/routes/modules/dashboard.ts
View file @
b9f1ac5d
...
...
@@ -25,6 +25,7 @@ const routes: RouteRecordRaw[] = [
meta
:
{
affixTab
:
true
,
title
:
$t
(
'page.dashboard.analytics'
),
componentPath
:
'@/views/dashboard/analytics/index.vue'
,
},
},
{
...
...
@@ -48,6 +49,7 @@ const routes: RouteRecordRaw[] = [
title
:
'更新日志'
,
badge
:
`当前:
${
version
}
`
,
badgeVariants
:
'bg-primary'
,
componentPath
:
'@/views/_core/changelog/index.vue'
,
},
},
],
...
...
@@ -58,6 +60,7 @@ const routes: RouteRecordRaw[] = [
icon
:
'lucide:copyright'
,
order
:
9999
,
title
:
$t
(
'demos.vben.about'
),
componentPath
:
'@/views/_core/about/index.vue'
,
},
name
:
'About'
,
path
:
'/vben-admin/about'
,
...
...
frontend/src/router/routes/modules/profile.ts
View file @
b9f1ac5d
...
...
@@ -9,6 +9,7 @@ const routes: RouteRecordRaw[] = [
title
:
$t
(
'ui.widgets.profile'
),
hideInMenu
:
true
,
requireHomeRedirect
:
true
,
componentPath
:
'@/views/_core/profile/index.vue'
,
},
name
:
'Profile'
,
path
:
'/profile'
,
...
...
frontend/src/views/_core/authentication/login.vue
View file @
b9f1ac5d
...
...
@@ -43,8 +43,8 @@ const rememberMe = ref(!!localUsername);
const
formState
=
reactive
({
tenantId
:
DEFAULT_TENANT_ID
,
username
:
localUsername
||
'
admin
'
,
password
:
'
111111
'
,
username
:
localUsername
||
''
,
password
:
''
,
code
:
''
,
});
...
...
frontend/src/views/ai/consume/consume-drawer.vue
View file @
b9f1ac5d
This diff is collapsed.
Click to expand it.
frontend/src/views/ai/consume/consume-search.vue
View file @
b9f1ac5d
...
...
@@ -4,12 +4,10 @@ import type { Dayjs } from 'dayjs';
import
{
ref
}
from
'vue'
;
import
{
FormInput
,
FormSelect
}
from
'@/components/global/form'
;
import
{
FormInput
}
from
'@/components/global/form'
;
import
{
SearchButtonGroup
}
from
'@/components/table'
;
import
{
tableSeachClass
}
from
'@/components/vxe-table'
;
import
{
DictEnum
}
from
'@/constants'
;
import
{
formatDateTime
}
from
'@/utils'
;
import
{
getDictOptions
}
from
'@/utils/dict'
;
import
{
formatDate
}
from
'@/utils'
;
import
{
Card
,
DateRangePicker
,
Form
,
FormItem
}
from
'antdv-next'
;
const
emit
=
defineEmits
<
{
...
...
@@ -38,8 +36,9 @@ function buildSearchParams(values: ConsumeSearchFormParams) {
const
params
:
Record
<
string
,
any
>
=
{
...
values
};
if
(
params
.
consumeTime
)
{
params
.
params
=
{
beginTime
:
formatDateTime
(
params
.
consumeTime
[
0
]),
endTime
:
formatDateTime
(
params
.
consumeTime
[
1
]),
// date 列 BETWEEN 两端均为纯日期,天然含结束日当天(无需再补 00:00:00/23:59:59)
beginTime
:
formatDate
(
params
.
consumeTime
[
0
],
'YYYY-MM-DD'
),
endTime
:
formatDate
(
params
.
consumeTime
[
1
],
'YYYY-MM-DD'
),
};
Reflect
.
deleteProperty
(
params
,
'consumeTime'
);
}
...
...
@@ -77,25 +76,13 @@ defineExpose({
<div
:class=
"tableSeachClass"
>
<template
v-if=
"!searchCollapsed"
>
<FormItem
label=
"项目名称"
name=
"projectName"
>
<FormSelect
allow-clear
option-filter-prop=
"label"
show-search
v-model:value=
"model.projectName"
:options=
"getDictOptions(DictEnum.AI_PROJECT_NAME)"
/>
<FormInput
v-model:value=
"model.projectName"
allow-clear
/>
</FormItem>
<FormItem
label=
"姓名"
name=
"userName"
>
<FormInput
v-model:value=
"model.userName"
allow-clear
/>
</FormItem>
<FormItem
label=
"AI工具/平台"
name=
"toolName"
>
<FormSelect
allow-clear
option-filter-prop=
"label"
show-search
v-model:value=
"model.toolName"
:options=
"getDictOptions(DictEnum.AI_TOOL_NAME)"
/>
<FormInput
v-model:value=
"model.toolName"
allow-clear
/>
</FormItem>
<FormItem
label=
"消费时间"
name=
"consumeTime"
>
<DateRangePicker
v-model:value=
"model.consumeTime"
allow-clear
/>
...
...
frontend/src/views/ai/consume/data.ts
View file @
b9f1ac5d
import
type
{
VxeGridProps
}
from
'vxe-table'
;
import
{
DictEnum
}
from
'@/constants'
;
import
{
renderDict
}
from
'@/utils/render'
;
export
const
columns
:
VxeGridProps
[
'columns'
]
=
[
{
type
:
'checkbox'
,
width
:
60
},
{
title
:
'所属部门'
,
field
:
'deptName'
,
},
{
title
:
'项目名称'
,
field
:
'projectName'
,
slots
:
{
default
:
({
row
})
=>
{
return
renderDict
(
row
.
projectName
,
DictEnum
.
AI_PROJECT_NAME
);
},
},
},
{
title
:
'姓名'
,
...
...
@@ -21,11 +17,6 @@ export const columns: VxeGridProps['columns'] = [
{
title
:
'AI工具/平台名称'
,
field
:
'toolName'
,
slots
:
{
default
:
({
row
})
=>
{
return
renderDict
(
row
.
toolName
,
DictEnum
.
AI_TOOL_NAME
);
},
},
},
{
title
:
'国内/国外'
,
...
...
frontend/src/views/ai/consume/index.vue
View file @
b9f1ac5d
...
...
@@ -4,7 +4,13 @@ import type { VxeGridInstance, VxeGridListeners } from 'vxe-table';
import
{
ref
,
useTemplateRef
}
from
'vue'
;
import
{
consumeExport
,
consumeList
,
consumeRemove
}
from
'@/api/ai/consume'
;
import
{
consumeDeptTreeSelect
,
consumeExport
,
consumeList
,
consumeRemove
,
consumeSummary
,
}
from
'@/api/ai/consume'
;
import
{
Page
,
useVbenDrawer
}
from
'@/components'
;
import
{
resolveQueryFormValues
,
...
...
@@ -12,17 +18,26 @@ import {
withDefaultVxeGridOptions
,
}
from
'@/components/vxe-table'
;
import
{
useBlobExport
}
from
'@/utils/file/export'
;
import
DeptTree
from
'@/views/system/user/dept-tree.vue'
;
import
{
Popconfirm
,
Space
,
Spin
}
from
'antdv-next'
;
import
{
VxeGrid
}
from
'vxe-table'
;
import
{
columns
}
from
'./data'
;
import
consumeDrawer
from
'./consume-drawer.vue'
;
import
ConsumeSearchForm
from
'./consume-search.vue'
;
import
{
columns
}
from
'./data'
;
// 左边部门用
const
selectDeptId
=
ref
<
string
[]
>
([]);
const
searchFormRef
=
ref
<
InstanceType
<
typeof
ConsumeSearchForm
>>
();
// 缓存最近一次搜索参数,部门树切换时重新查询用
const
currentSearchParams
=
ref
<
Record
<
string
,
any
>>
({});
const
tableLoading
=
ref
(
false
);
// 列表底部合计行数据(独立 ref,不写回 gridOptions,规避 TS 自引用循环类型推断,design D6)
const
footerData
=
ref
<
Record
<
string
,
any
>
[]
>
([]);
const
gridOptions
=
withDefaultVxeGridOptions
<
Consume
>
({
checkboxConfig
:
{
// 高亮
...
...
@@ -42,11 +57,27 @@ const gridOptions = withDefaultVxeGridOptions<Consume>({
const
values
=
await
resolveQueryFormValues
(
searchFormRef
,
formValues
);
tableLoading
.
value
=
true
;
try
{
return
await
consumeList
({
pageNum
:
page
.
currentPage
,
pageSize
:
page
.
pageSize
,
...
values
,
});
// 部门树选择处理
if
(
selectDeptId
.
value
.
length
===
1
)
{
values
.
belongDeptId
=
selectDeptId
.
value
[
0
];
}
else
{
Reflect
.
deleteProperty
(
values
,
'belongDeptId'
);
}
// 列表与合计并行拉取:合计走独立接口、跨页全量,口径与列表完全一致(design D1/D6)
const
[
listRes
,
sum
]
=
await
Promise
.
all
([
consumeList
({
pageNum
:
page
.
currentPage
,
pageSize
:
page
.
pageSize
,
...
values
,
}),
consumeSummary
(
values
),
]);
// 合计写入独立 footerData;键名按 data.ts 列 field 对齐;toFixed(2) 规避 JSON 数值退化(design D6)
footerData
.
value
=
[
{
deptName
:
'合计'
,
amountCny
:
Number
(
sum
.
amountCny
).
toFixed
(
2
)
},
];
return
listRes
;
}
finally
{
tableLoading
.
value
=
false
;
}
...
...
@@ -56,6 +87,8 @@ const gridOptions = withDefaultVxeGridOptions<Consume>({
rowConfig
:
{
keyField
:
'id'
,
},
// footer 页面局部开启(不改全局 vxe 配置,design D2)
showFooter
:
true
,
toolbarConfig
:
{
slots
:
{
buttons
:
'toolbar-left'
,
...
...
@@ -88,7 +121,7 @@ function handleAdd() {
}
async
function
handleEdit
(
record
:
Consume
)
{
drawerApi
.
setData
({
id
:
record
.
id
});
drawerApi
.
setData
({
batchNo
:
record
.
batchNo
,
id
:
record
.
id
});
drawerApi
.
open
();
}
...
...
@@ -121,19 +154,33 @@ const { exportBlob, exportLoading, buildExportFileName } =
async
function
handleExport
()
{
// 构建表单请求参数
const
formValues
=
(
await
searchFormRef
.
value
?.
getValues
())
??
{};
// 部门树选择处理(与 query 口径一致,导出携带 belongDeptId)
if
(
selectDeptId
.
value
.
length
===
1
)
{
formValues
.
belongDeptId
=
selectDeptId
.
value
[
0
];
}
else
{
Reflect
.
deleteProperty
(
formValues
,
'belongDeptId'
);
}
// 文件名
const
fileName
=
buildExportFileName
(
'消费记录数据'
);
exportBlob
({
data
:
formValues
,
fileName
});
}
function
handleSearchSubmit
(
data
:
Record
<
string
,
any
>
)
{
currentSearchParams
.
value
=
data
;
reload
(
data
);
}
function
handleSearchReset
()
{
currentSearchParams
.
value
=
{};
selectDeptId
.
value
=
[];
reload
();
}
function
handleDeptSelect
(
keys
:
string
[])
{
selectDeptId
.
value
=
keys
;
reload
(
currentSearchParams
.
value
);
}
function
getCheckedRows
()
{
const
table
=
tableRef
.
value
;
if
(
!
table
)
{
...
...
@@ -158,77 +205,87 @@ function syncCheckedRows() {
size="large"
:delay="300"
>
<div
class=
"flex h-full flex-col gap-2"
>
<ConsumeSearchForm
ref=
"searchFormRef"
@
submit=
"handleSearchSubmit"
@
reset=
"handleSearchReset"
<div
class=
"flex h-full gap-[8px]"
>
<DeptTree
:api=
"consumeDeptTreeSelect"
v-model:select-dept-id=
"selectDeptId"
class=
"w-[260px]"
@
reload=
"() => reload()"
@
select=
"handleDeptSelect"
/>
<div
class=
"bg-card flex-1 overflow-hidden rounded-lg"
>
<VxeGrid
ref=
"tableRef"
class=
"p-2 pt-0"
v-bind=
"gridOptions"
v-on=
"gridEvents"
>
<template
#
toolbar-left
>
<div
class=
"text-[16px] font-medium"
>
消费记录
</div>
</
template
>
<
template
#
toolbar-right
>
<Space>
<a-button
v-access:code=
"['ai:consume:export']"
:loading=
"exportLoading"
:disabled=
"exportLoading"
@
click=
"handleExport"
>
{{
$t
(
'pages.common.export'
)
}}
</a-button>
<a-button
:disabled=
"checkedRows.length === 0"
danger
type=
"primary"
v-access:code=
"['ai:consume:remove']"
@
click=
"handleMultiDelete"
>
{{
$t
(
'pages.common.delete'
)
}}
</a-button>
<a-button
type=
"primary"
v-access:code=
"['ai:consume:add']"
@
click=
"handleAdd"
>
{{
$t
(
'pages.common.add'
)
}}
</a-button>
</Space>
</
template
>
<
template
#
action=
"{ row }"
>
<Space>
<action-button
v-access:code=
"['ai:consume:edit']"
@
click=
"handleEdit(row)"
>
{{
$t
(
'pages.common.edit'
)
}}
</action-button>
<Popconfirm
placement=
"left"
title=
"确认删除?"
@
confirm=
"handleDelete(row)"
>
<action-button
<div
class=
"flex flex-1 flex-col gap-4 overflow-hidden"
>
<ConsumeSearchForm
ref=
"searchFormRef"
@
submit=
"handleSearchSubmit"
@
reset=
"handleSearchReset"
/>
<div
class=
"bg-card flex-1 overflow-hidden rounded-lg"
>
<VxeGrid
ref=
"tableRef"
class=
"p-2 pt-0"
v-bind=
"gridOptions"
:footer-data=
"footerData"
v-on=
"gridEvents"
>
<template
#
toolbar-left
>
<div
class=
"text-[16px] font-medium"
>
消费记录
</div>
</
template
>
<
template
#
toolbar-right
>
<Space>
<a-button
v-access:code=
"['ai:consume:export']"
:loading=
"exportLoading"
:disabled=
"exportLoading"
@
click=
"handleExport"
>
{{
$t
(
'pages.common.export'
)
}}
</a-button>
<a-button
:disabled=
"checkedRows.length === 0"
danger
type=
"primary"
v-access:code=
"['ai:consume:remove']"
@
click
.
stop=
"
"
@
click
=
"handleMultiDelete
"
>
{{
$t
(
'pages.common.delete'
)
}}
</a-button>
<a-button
type=
"primary"
v-access:code=
"['ai:consume:add']"
@
click=
"handleAdd"
>
{{
$t
(
'pages.common.add'
)
}}
</a-button>
</Space>
</
template
>
<
template
#
action=
"{ row }"
>
<Space>
<action-button
v-access:code=
"['ai:consume:edit']"
@
click=
"handleEdit(row)"
>
{{
$t
(
'pages.common.edit'
)
}}
</action-button>
</Popconfirm>
</Space>
</
template
>
<
template
#
loading
>
<Spin
:spinning=
"true"
size=
"large"
/>
</
template
>
</VxeGrid>
<Popconfirm
placement=
"left"
title=
"确认删除?"
@
confirm=
"handleDelete(row)"
>
<action-button
danger
v-access:code=
"['ai:consume:remove']"
@
click
.
stop=
""
>
{{
$t
(
'pages.common.delete'
)
}}
</action-button>
</Popconfirm>
</Space>
</
template
>
<
template
#
loading
>
<Spin
:spinning=
"true"
size=
"large"
/>
</
template
>
</VxeGrid>
</div>
</div>
</div>
</Spin>
...
...
frontend/src/views/ai/recharge/data.ts
View file @
b9f1ac5d
...
...
@@ -5,14 +5,13 @@ import { renderDict } from '@/utils/render';
export
const
columns
:
VxeGridProps
[
'columns'
]
=
[
{
type
:
'checkbox'
,
width
:
60
},
{
title
:
'所属部门'
,
field
:
'deptName'
,
},
{
title
:
'AI工具/平台名称'
,
field
:
'toolName'
,
slots
:
{
default
:
({
row
})
=>
{
return
renderDict
(
row
.
toolName
,
DictEnum
.
AI_TOOL_NAME
);
},
},
},
{
title
:
'国内/国外'
,
...
...
frontend/src/views/ai/recharge/index.vue
View file @
b9f1ac5d
...
...
@@ -4,7 +4,13 @@ import type { VxeGridInstance, VxeGridListeners } from 'vxe-table';
import
{
ref
,
useTemplateRef
}
from
'vue'
;
import
{
rechargeExport
,
rechargeList
,
rechargeRemove
}
from
'@/api/ai/recharge'
;
import
{
rechargeDeptTreeSelect
,
rechargeExport
,
rechargeList
,
rechargeRemove
,
rechargeSummary
,
}
from
'@/api/ai/recharge'
;
import
{
Page
,
useVbenDrawer
}
from
'@/components'
;
import
{
resolveQueryFormValues
,
...
...
@@ -12,6 +18,7 @@ import {
withDefaultVxeGridOptions
,
}
from
'@/components/vxe-table'
;
import
{
useBlobExport
}
from
'@/utils/file/export'
;
import
DeptTree
from
'@/views/system/user/dept-tree.vue'
;
import
{
Popconfirm
,
Space
,
Spin
}
from
'antdv-next'
;
import
{
VxeGrid
}
from
'vxe-table'
;
...
...
@@ -19,10 +26,18 @@ import { columns } from './data';
import
rechargeDrawer
from
'./recharge-drawer.vue'
;
import
RechargeSearchForm
from
'./recharge-search.vue'
;
// 左边部门用
const
selectDeptId
=
ref
<
string
[]
>
([]);
const
searchFormRef
=
ref
<
InstanceType
<
typeof
RechargeSearchForm
>>
();
// 缓存最近一次搜索参数,部门树切换时重新查询用
const
currentSearchParams
=
ref
<
Record
<
string
,
any
>>
({});
const
tableLoading
=
ref
(
false
);
// 列表底部合计行数据(独立 ref,不写回 gridOptions,规避 TS 自引用循环类型推断,design D6)
const
footerData
=
ref
<
Record
<
string
,
any
>
[]
>
([]);
const
gridOptions
=
withDefaultVxeGridOptions
<
Recharge
>
({
checkboxConfig
:
{
// 高亮
...
...
@@ -42,11 +57,31 @@ const gridOptions = withDefaultVxeGridOptions<Recharge>({
const
values
=
await
resolveQueryFormValues
(
searchFormRef
,
formValues
);
tableLoading
.
value
=
true
;
try
{
return
await
rechargeList
({
pageNum
:
page
.
currentPage
,
pageSize
:
page
.
pageSize
,
...
values
,
});
// 部门树选择处理
if
(
selectDeptId
.
value
.
length
===
1
)
{
values
.
belongDeptId
=
selectDeptId
.
value
[
0
];
}
else
{
Reflect
.
deleteProperty
(
values
,
'belongDeptId'
);
}
// 列表与合计并行拉取:合计走独立接口、跨页全量,口径与列表完全一致(design D1/D6)
const
[
listRes
,
sum
]
=
await
Promise
.
all
([
rechargeList
({
pageNum
:
page
.
currentPage
,
pageSize
:
page
.
pageSize
,
...
values
,
}),
rechargeSummary
(
values
),
]);
// 合计写入独立 footerData;两金额键按 data.ts 列 field 对齐;toFixed(2) 规避 JSON 数值退化(design D6)
footerData
.
value
=
[
{
deptName
:
'合计'
,
amountUsd
:
Number
(
sum
.
amountUsd
).
toFixed
(
2
),
amountCny
:
Number
(
sum
.
amountCny
).
toFixed
(
2
),
},
];
return
listRes
;
}
finally
{
tableLoading
.
value
=
false
;
}
...
...
@@ -56,6 +91,8 @@ const gridOptions = withDefaultVxeGridOptions<Recharge>({
rowConfig
:
{
keyField
:
'id'
,
},
// footer 页面局部开启(不改全局 vxe 配置,design D2)
showFooter
:
true
,
toolbarConfig
:
{
slots
:
{
buttons
:
'toolbar-left'
,
...
...
@@ -121,19 +158,33 @@ const { exportBlob, exportLoading, buildExportFileName } =
async
function
handleExport
()
{
// 构建表单请求参数
const
formValues
=
(
await
searchFormRef
.
value
?.
getValues
())
??
{};
// 部门树选择处理(与 query 口径一致,导出携带 belongDeptId)
if
(
selectDeptId
.
value
.
length
===
1
)
{
formValues
.
belongDeptId
=
selectDeptId
.
value
[
0
];
}
else
{
Reflect
.
deleteProperty
(
formValues
,
'belongDeptId'
);
}
// 文件名
const
fileName
=
buildExportFileName
(
'充值记录数据'
);
exportBlob
({
data
:
formValues
,
fileName
});
}
function
handleSearchSubmit
(
data
:
Record
<
string
,
any
>
)
{
currentSearchParams
.
value
=
data
;
reload
(
data
);
}
function
handleSearchReset
()
{
currentSearchParams
.
value
=
{};
selectDeptId
.
value
=
[];
reload
();
}
function
handleDeptSelect
(
keys
:
string
[])
{
selectDeptId
.
value
=
keys
;
reload
(
currentSearchParams
.
value
);
}
function
getCheckedRows
()
{
const
table
=
tableRef
.
value
;
if
(
!
table
)
{
...
...
@@ -158,77 +209,87 @@ function syncCheckedRows() {
size="large"
:delay="300"
>
<div
class=
"flex h-full flex-col gap-2"
>
<RechargeSearchForm
ref=
"searchFormRef"
@
submit=
"handleSearchSubmit"
@
reset=
"handleSearchReset"
<div
class=
"flex h-full gap-[8px]"
>
<DeptTree
:api=
"rechargeDeptTreeSelect"
v-model:select-dept-id=
"selectDeptId"
class=
"w-[260px]"
@
reload=
"() => reload()"
@
select=
"handleDeptSelect"
/>
<div
class=
"bg-card flex-1 overflow-hidden rounded-lg"
>
<VxeGrid
ref=
"tableRef"
class=
"p-2 pt-0"
v-bind=
"gridOptions"
v-on=
"gridEvents"
>
<template
#
toolbar-left
>
<div
class=
"text-[16px] font-medium"
>
充值记录
</div>
</
template
>
<
template
#
toolbar-right
>
<Space>
<a-button
v-access:code=
"['ai:recharge:export']"
:loading=
"exportLoading"
:disabled=
"exportLoading"
@
click=
"handleExport"
>
{{
$t
(
'pages.common.export'
)
}}
</a-button>
<a-button
:disabled=
"checkedRows.length === 0"
danger
type=
"primary"
v-access:code=
"['ai:recharge:remove']"
@
click=
"handleMultiDelete"
>
{{
$t
(
'pages.common.delete'
)
}}
</a-button>
<a-button
type=
"primary"
v-access:code=
"['ai:recharge:add']"
@
click=
"handleAdd"
>
{{
$t
(
'pages.common.add'
)
}}
</a-button>
</Space>
</
template
>
<
template
#
action=
"{ row }"
>
<Space>
<action-button
v-access:code=
"['ai:recharge:edit']"
@
click=
"handleEdit(row)"
>
{{
$t
(
'pages.common.edit'
)
}}
</action-button>
<Popconfirm
placement=
"left"
title=
"确认删除?"
@
confirm=
"handleDelete(row)"
>
<action-button
<div
class=
"flex flex-1 flex-col gap-4 overflow-hidden"
>
<RechargeSearchForm
ref=
"searchFormRef"
@
submit=
"handleSearchSubmit"
@
reset=
"handleSearchReset"
/>
<div
class=
"bg-card flex-1 overflow-hidden rounded-lg"
>
<VxeGrid
ref=
"tableRef"
class=
"p-2 pt-0"
v-bind=
"gridOptions"
:footer-data=
"footerData"
v-on=
"gridEvents"
>
<template
#
toolbar-left
>
<div
class=
"text-[16px] font-medium"
>
充值记录
</div>
</
template
>
<
template
#
toolbar-right
>
<Space>
<a-button
v-access:code=
"['ai:recharge:export']"
:loading=
"exportLoading"
:disabled=
"exportLoading"
@
click=
"handleExport"
>
{{
$t
(
'pages.common.export'
)
}}
</a-button>
<a-button
:disabled=
"checkedRows.length === 0"
danger
type=
"primary"
v-access:code=
"['ai:recharge:remove']"
@
click
.
stop=
"
"
@
click
=
"handleMultiDelete
"
>
{{
$t
(
'pages.common.delete'
)
}}
</a-button>
<a-button
type=
"primary"
v-access:code=
"['ai:recharge:add']"
@
click=
"handleAdd"
>
{{
$t
(
'pages.common.add'
)
}}
</a-button>
</Space>
</
template
>
<
template
#
action=
"{ row }"
>
<Space>
<action-button
v-access:code=
"['ai:recharge:edit']"
@
click=
"handleEdit(row)"
>
{{
$t
(
'pages.common.edit'
)
}}
</action-button>
</Popconfirm>
</Space>
</
template
>
<
template
#
loading
>
<Spin
:spinning=
"true"
size=
"large"
/>
</
template
>
</VxeGrid>
<Popconfirm
placement=
"left"
title=
"确认删除?"
@
confirm=
"handleDelete(row)"
>
<action-button
danger
v-access:code=
"['ai:recharge:remove']"
@
click
.
stop=
""
>
{{
$t
(
'pages.common.delete'
)
}}
</action-button>
</Popconfirm>
</Space>
</
template
>
<
template
#
loading
>
<Spin
:spinning=
"true"
size=
"large"
/>
</
template
>
</VxeGrid>
</div>
</div>
</div>
</Spin>
...
...
frontend/src/views/ai/recharge/recharge-drawer.vue
View file @
b9f1ac5d
<
script
setup
lang=
"ts"
>
import
type
{
Recharge
}
from
'@/api/ai/recharge/model'
;
import
type
{
DeptTree
}
from
'@/api/system/user/model'
;
import
type
{
AntdFormRules
}
from
'@/types/form'
;
import
type
{
FormInstance
}
from
'antdv-next'
;
import
{
computed
,
ref
}
from
'vue'
;
import
{
rechargeAdd
,
rechargeInfo
,
rechargeUpdate
}
from
'@/api/ai/recharge'
;
import
{
getDeptTree
}
from
'@/api/system/user'
;
import
{
useVbenDrawer
}
from
'@/components'
;
import
{
FormInput
as
Input
,
FormInputNumber
as
InputNumber
,
FormSelect
as
Select
,
FormTextArea
as
TextArea
,
FormTreeSelect
as
TreeSelect
,
}
from
'@/components/global/form'
;
import
{
DictEnum
}
from
'@/constants'
;
import
{
$t
}
from
'@/locales'
;
import
{
cloneDeep
,
getPopupContainer
}
from
'@/utils'
;
import
{
addFullName
,
cloneDeep
,
getPopupContainer
}
from
'@/utils'
;
import
{
getDictOptions
}
from
'@/utils/dict'
;
import
{
useBeforeCloseDiff
}
from
'@/utils/popup'
;
import
{
DatePicker
,
Form
,
FormItem
}
from
'antdv-next'
;
...
...
@@ -26,7 +30,9 @@ const title = computed(() => {
return
isUpdate
.
value
?
$t
(
'pages.common.edit'
)
:
$t
(
'pages.common.add'
);
});
type
FormData
=
Partial
<
Recharge
>
;
type
FormData
=
Partial
<
Recharge
>
&
{
deptId
?:
number
|
string
;
};
// 国内/国外为固定二值(非字典,Clarify C2)
const
regionOptions
=
[
...
...
@@ -37,6 +43,7 @@ const regionOptions = [
function
getDefaultValues
():
FormData
{
return
{
id
:
undefined
,
deptId
:
undefined
,
toolName
:
undefined
,
region
:
undefined
,
rechargeCategory
:
undefined
,
...
...
@@ -53,9 +60,11 @@ function getDefaultValues(): FormData {
const
formData
=
ref
<
FormData
>
(
getDefaultValues
());
const
formInstance
=
ref
<
FormInstance
>
();
const
deptTreeData
=
ref
<
DeptTree
[]
>
([]);
const
formRules
=
ref
<
AntdFormRules
<
FormData
>>
({
toolName
:
[{
required
:
true
,
message
:
$t
(
'ui.formRules.selectRequired'
)
}],
deptId
:
[{
required
:
true
,
message
:
$t
(
'ui.formRules.selectRequired'
)
}],
toolName
:
[{
required
:
true
,
message
:
$t
(
'ui.formRules.required'
)
}],
region
:
[{
required
:
true
,
message
:
$t
(
'ui.formRules.selectRequired'
)
}],
rechargeCategory
:
[
{
required
:
true
,
message
:
$t
(
'ui.formRules.selectRequired'
)
},
...
...
@@ -64,6 +73,13 @@ const formRules = ref<AntdFormRules<FormData>>({
amountCny
:
[{
required
:
true
,
message
:
$t
(
'ui.formRules.required'
)
}],
});
async
function
setupDeptSelect
()
{
const
deptTree
=
await
getDeptTree
();
// 选中后显示在输入框的值 即父节点 / 子节点
addFullName
(
deptTree
,
'label'
,
' / '
);
deptTreeData
.
value
=
deptTree
;
}
function
customFormValueGetter
()
{
return
JSON
.
stringify
(
formData
.
value
);
}
...
...
@@ -86,6 +102,8 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
drawerApi
.
drawerLoading
(
true
);
const
{
id
}
=
drawerApi
.
getData
()
as
{
id
?:
number
|
string
};
isUpdate
.
value
=
!!
id
;
// 初始化部门树
await
setupDeptSelect
();
// 更新 && 赋值
if
(
isUpdate
.
value
&&
id
)
{
const
record
=
await
rechargeInfo
(
id
);
...
...
@@ -119,6 +137,7 @@ async function handleConfirm() {
async
function
handleClosed
()
{
formData
.
value
=
getDefaultValues
();
formInstance
.
value
?.
resetFields
();
deptTreeData
.
value
=
[];
resetInitialized
();
}
</
script
>
...
...
@@ -130,14 +149,25 @@ async function handleClosed() {
:model=
"formData"
:label-col=
"
{ style: { width: '140px' } }"
>
<FormItem
label=
"AI工具/平台名称"
name=
"toolName"
:rules=
"formRules.toolName"
>
<Select
<FormItem
label=
"所属部门"
name=
"deptId"
:rules=
"formRules.deptId"
>
<TreeSelect
allow-clear
class=
"w-full"
:allow-clear=
"false"
option-filter-prop=
"label"
show-search
:field-names=
"
{ label: 'label', value: 'id', children: 'children' }"
:get-popup-container="getPopupContainer"
:options=
"getDictOptions(DictEnum.AI_TOOL_NAME)"
show-search
:tree-data="deptTreeData"
tree-default-expand-all
:tree-line="{ showLeafIcon: false }"
tree-node-filter-prop="label"
tree-node-label-prop="fullName"
v-model:value="formData.deptId"
/>
</FormItem>
<FormItem
label=
"AI工具/平台名称"
name=
"toolName"
:rules=
"formRules.toolName"
>
<Input
class=
"w-full"
allow-clear
v-model:value=
"formData.toolName"
/>
</FormItem>
...
...
@@ -170,9 +200,8 @@ async function handleClosed() {
>
<DatePicker
class=
"w-full"
show-time
format=
"YYYY-MM-DD HH:mm:ss"
value-format=
"YYYY-MM-DD HH:mm:ss"
format=
"YYYY-MM-DD"
value-format=
"YYYY-MM-DD"
v-model:value=
"formData.rechargeTime"
/>
</FormItem>
...
...
frontend/src/views/ai/recharge/recharge-search.vue
View file @
b9f1ac5d
...
...
@@ -4,11 +4,11 @@ import type { Dayjs } from 'dayjs';
import
{
ref
}
from
'vue'
;
import
{
FormSelect
}
from
'@/components/global/form'
;
import
{
Form
Input
,
Form
Select
}
from
'@/components/global/form'
;
import
{
SearchButtonGroup
}
from
'@/components/table'
;
import
{
tableSeachClass
}
from
'@/components/vxe-table'
;
import
{
DictEnum
}
from
'@/constants'
;
import
{
formatDate
Time
}
from
'@/utils'
;
import
{
formatDate
}
from
'@/utils'
;
import
{
getDictOptions
}
from
'@/utils/dict'
;
import
{
Card
,
DateRangePicker
,
Form
,
FormItem
}
from
'antdv-next'
;
...
...
@@ -44,8 +44,9 @@ function buildSearchParams(values: RechargeSearchFormParams) {
const
params
:
Record
<
string
,
any
>
=
{
...
values
};
if
(
params
.
rechargeTime
)
{
params
.
params
=
{
beginTime
:
formatDateTime
(
params
.
rechargeTime
[
0
]),
endTime
:
formatDateTime
(
params
.
rechargeTime
[
1
]),
// date 列 BETWEEN 两端均为纯日期,天然含结束日当天(无需再补 00:00:00/23:59:59)
beginTime
:
formatDate
(
params
.
rechargeTime
[
0
],
'YYYY-MM-DD'
),
endTime
:
formatDate
(
params
.
rechargeTime
[
1
],
'YYYY-MM-DD'
),
};
Reflect
.
deleteProperty
(
params
,
'rechargeTime'
);
}
...
...
@@ -86,13 +87,7 @@ defineExpose({
<DateRangePicker
v-model:value=
"model.rechargeTime"
allow-clear
/>
</FormItem>
<FormItem
label=
"AI工具/平台"
name=
"toolName"
>
<FormSelect
allow-clear
option-filter-prop=
"label"
show-search
v-model:value=
"model.toolName"
:options=
"getDictOptions(DictEnum.AI_TOOL_NAME)"
/>
<FormInput
v-model:value=
"model.toolName"
allow-clear
/>
</FormItem>
<FormItem
label=
"国内/国外"
name=
"region"
>
<FormSelect
...
...
frontend/src/views/system/menu/menu-drawer.vue
View file @
b9f1ac5d
...
...
@@ -410,7 +410,7 @@ async function handleCopyConfig() {
/>
</FormItem>
<FormItem
v-if=
"show
Menu
"
v-if=
"show
NotButton
"
label=
"页面名称"
name=
"pageName"
:rules=
"formRules.pageName"
...
...
frontend/src/views/system/role/role-auth-modal.vue
View file @
b9f1ac5d
...
...
@@ -25,7 +25,7 @@ const tabOptions = [
];
interface
RolePermissionData
{
roleI
d
?:
number
|
string
;
i
d
?:
number
|
string
;
roleKey
?:
string
;
roleName
?:
string
;
// 菜单权限
...
...
@@ -91,7 +91,7 @@ const [BasicModal, modalApi] = useVbenModal({
]);
formData
.
value
=
{
roleI
d
:
record
.
id
,
i
d
:
record
.
id
,
roleName
:
record
.
roleName
,
roleKey
:
record
.
roleKey
,
menuCheckStrictly
:
record
.
menuCheckStrictly
,
...
...
pom.xml
View file @
b9f1ac5d
...
...
@@ -19,7 +19,7 @@
<mapstruct-plus.version>
1.5.1
</mapstruct-plus.version>
<lombok-mapstruct-binding.version>
0.2.0
</lombok-mapstruct-binding.version>
<therapi-javadoc.version>
0.15.0
</therapi-javadoc.version>
<binfast.version>
2.0.
2
</binfast.version>
<binfast.version>
2.0.
3
</binfast.version>
<lombok.version>
1.18.46
</lombok.version>
</properties>
...
...
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