feat:增加学生家长导入。

feat:钱包流水、批量充值增加tag。
feat:优化表格区域的搜索逻辑。
This commit is contained in:
dzc committed 2026-08-13 15:41:29 +08:00
1 parent 2a617c9460
commit 4816f151d6
33 files changed
+418 -47

No files matched your search

@@ -0,0 +1,6 @@
-- 为充值导入记录及最终钱包流水补充财务统计标签;历史数据保持为空。
ALTER TABLE wallet_recharge_import
ADD COLUMN tags varchar(512) DEFAULT NULL COMMENT '财务统计标签,多个wallet_transaction_tag字典值使用英文逗号分隔' AFTER price;
ALTER TABLE wallet_transaction
ADD COLUMN tags varchar(512) DEFAULT NULL COMMENT '财务统计标签,多个wallet_transaction_tag字典值使用英文逗号分隔' AFTER balance;
+2
View File
@@ -455,6 +455,7 @@ CREATE TABLE wallet_transaction
title varchar(128) NOT NULL COMMENT '流水标题',
price int NOT NULL COMMENT '交易金额, 单位分',
balance int NOT NULL COMMENT '余额, 单位分',
tags varchar(512) DEFAULT NULL COMMENT '财务统计标签,多个wallet_transaction_tag字典值使用英文逗号分隔',
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
create_time datetime DEFAULT NULL COMMENT '创建时间',
update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
@@ -536,6 +537,7 @@ CREATE TABLE wallet_recharge_import
apply_id bigint NOT NULL COMMENT '申请id',
before_price int DEFAULT NULL COMMENT '导入前的金额',
price int NOT NULL COMMENT '交易金额, 单位分',
tags varchar(512) DEFAULT NULL COMMENT '财务统计标签,多个wallet_transaction_tag字典值使用英文逗号分隔',
create_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
create_time datetime DEFAULT NULL COMMENT '创建时间',
update_by varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '修改人',
@@ -9,6 +9,7 @@ import java.lang.annotation.Target;
/**
* Excel 字典字段。
* <p>导入时校验单元格内容是否为字典标签,导出模板时将同一字典标签写入下拉选项。</p>
* <p>多选字段在普通 Excel 中使用英文逗号分隔多个标签;原生下拉仅负责提供选项,无法自动追加多次选择。</p>
*/
@Documented
@Target(ElementType.FIELD)
@@ -20,4 +21,11 @@ public @interface ExcelDict {
*/
String value();
/**
* 是否允许一个单元格填写多个字典标签。
*
* @return {@code true} 时按英文逗号拆分并逐项校验,{@code false} 时整个单元格只能是一个字典标签
*/
boolean multiple() default false;
}
@@ -8,11 +8,18 @@ import java.lang.annotation.Target;
/**
* Excel 模板增强。
* <p>切面自动收集 {@link RequestExcel} 导入参数中的模型,下载接口无需感知文件名或具体业务模型。</p>
* <p>切面默认自动收集当前服务 {@link RequestExcel} 导入参数中的模型;跨服务模型可通过注解参数显式补充。</p>
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelTemplate {
/**
* 当前下载接口需要额外处理的 Excel 模型,用于补充当前服务无法自动扫描到的跨服务导入模型。
*
* @return 需要参与模板字典下拉增强的模型类型;默认不额外补充模型
*/
Class<?>[] value() default {};
}
@@ -63,7 +63,7 @@ public class ExcelTemplateAspect {
Object result = joinPoint.proceed(wrappedArgs);
wrapper.flushBuffer();
byte[] content = wrapper.getContentAsByteArray();
byte[] enhancedContent = enhance(content, getModelClasses());
byte[] enhancedContent = enhance(content, resolveModelClasses(excelTemplate));
response.setContentLength(enhancedContent.length);
response.getOutputStream().write(enhancedContent);
response.getOutputStream().flush();
@@ -72,6 +72,18 @@ public class ExcelTemplateAspect {
throw new IllegalStateException("@ExcelTemplate 方法必须包含 HttpServletResponse 参数");
}
/**
* 合并注解显式模型与当前服务自动扫描模型,保证跨服务模板可增强且保留原有自动发现能力。
*
* @param excelTemplate 当前下载方法的模板增强注解
* @return 去重后的模板模型集合
*/
private Class<?>[] resolveModelClasses(ExcelTemplate excelTemplate) {
return java.util.stream.Stream.concat(Arrays.stream(excelTemplate.value()), Arrays.stream(getModelClasses()))
.distinct()
.toArray(Class<?>[]::new);
}
private boolean isExcelTemplateRequest(Object[] args) {
for (Object arg : args) {
if (arg instanceof String value) {
@@ -85,7 +97,7 @@ public class ExcelTemplateAspect {
}
/**
* 导入接口的 {@code @RequestExcel List<T>} 是模板模型的唯一来源,新增导入模型无需再修改下载接口或注册表。
* 自动收集当前服务导入接口中的 {@code @RequestExcel List<T>} 模型,供未显式注册的同服务模板继续使用。
*/
private Class<?>[] getModelClasses() {
Class<?>[] cachedModelClasses = modelClasses;
@@ -162,7 +174,7 @@ public class ExcelTemplateAspect {
HeaderCell headerCell = findHeaderCell(sheet, headerName);
if (headerCell != null) {
ExcelDictDataValidationWriter.write(sheet, headerCell.columnIndex(), headerCell.rowIndex() + 1,
excelDict.value());
excelDict.value(), excelDict.multiple());
enhanced = true;
}
}
@@ -31,9 +31,15 @@ public final class ExcelDictDataValidationWriter {
}
/**
* 将字典标签写入隐藏页,并为目标列创建引用式下拉。
* 将字典标签写入隐藏页,并为目标列创建引用式下拉;多选模式允许按英文逗号手工组合多个选项。
*
* @param dataSheet 数据页
* @param columnIndex 字典字段所在列下标
* @param firstDataRowIndex 首行数据的下标
* @param dictType 字典类型
* @param multiple 是否允许一个单元格包含多个字典标签
*/
public static void write(Sheet dataSheet, int columnIndex, int firstDataRowIndex, String dictType) {
public static void write(Sheet dataSheet, int columnIndex, int firstDataRowIndex, String dictType, boolean multiple) {
List<String> labels = ExcelDictHelper.getLabels(dictType);
if (labels.isEmpty()) {
throw new IllegalStateException("Excel 字典未配置或没有可用项: " + dictType);
@@ -41,7 +47,7 @@ public final class ExcelDictDataValidationWriter {
Workbook workbook = dataSheet.getWorkbook();
String formulaName = writeDictColumn(workbook, dictType, labels);
removeColumnValidations(dataSheet, columnIndex);
addValidation(dataSheet, columnIndex, firstDataRowIndex, formulaName);
addValidation(dataSheet, columnIndex, firstDataRowIndex, formulaName, multiple);
}
/**
@@ -123,15 +129,30 @@ public final class ExcelDictDataValidationWriter {
return false;
}
private static void addValidation(Sheet sheet, int columnIndex, int firstDataRowIndex, String formulaName) {
/**
* 为目标列添加字典下拉。多选时关闭 Excel 原生错误拦截,否则逗号组合值会被单选列表拒绝。
*
* @param sheet 数据页
* @param columnIndex 字典字段所在列下标
* @param firstDataRowIndex 首行数据的下标
* @param formulaName 隐藏字典区域的名称
* @param multiple 是否允许按英文逗号组合多个标签
*/
private static void addValidation(Sheet sheet, int columnIndex, int firstDataRowIndex, String formulaName, boolean multiple) {
DataValidationHelper helper = sheet.getDataValidationHelper();
DataValidationConstraint constraint = helper.createFormulaListConstraint(formulaName);
CellRangeAddressList range = new CellRangeAddressList(firstDataRowIndex,
sheet.getWorkbook().getSpreadsheetVersion().getLastRowIndex(), columnIndex, columnIndex);
DataValidation validation = helper.createValidation(constraint, range);
validation.setEmptyCellAllowed(true);
validation.setShowErrorBox(true);
validation.createErrorBox("填写错误", "请选择下拉中的有效字典项");
validation.setShowErrorBox(!multiple);
if (multiple) {
validation.setShowPromptBox(true);
validation.createPromptBox("填写说明", "可填写多个选项,请使用英文逗号分隔");
}
else {
validation.createErrorBox("填写错误", "请选择下拉中的有效字典项");
}
sheet.addValidationData(validation);
}
@@ -12,6 +12,7 @@ import org.springframework.util.StringUtils;
import jakarta.validation.ConstraintViolation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -67,7 +68,10 @@ public class DefaultAnalysisEventListener extends ListAnalysisEventListener<Obje
}
/**
* 字典字段统一按 Excel 中展示的标签校验,避免业务 Service 为每个字段重复编写字典判断。
* 字典字段统一按 Excel 中展示的标签校验;多选字段按英文逗号拆分,避免业务 Service 重复编写字典判断。
*
* @param data 当前 Excel 行转换出的模型
* @return 当前行字典字段的校验错误;没有错误时返回空集合
*/
private Set<String> validateExcelDict(Object data) {
Set<String> messages = new LinkedHashSet<>();
@@ -82,14 +86,24 @@ public class DefaultAnalysisEventListener extends ListAnalysisEventListener<Obje
if (fieldValue == null || !StringUtils.hasText(fieldValue.toString())) {
continue;
}
String label = fieldValue.toString().trim();
String fieldText = fieldValue.toString().trim();
List<String> labels = dictLabelCache.computeIfAbsent(excelDict.value(), ExcelDictHelper::getLabels);
String fieldName = getExcelFieldName(field);
if (labels.isEmpty()) {
messages.add(fieldName + "字典未配置或没有可用项");
}
else if (!labels.contains(label)) {
messages.add(fieldName + ":" + label + ",不在可选项中");
else {
List<String> selectedLabels = excelDict.multiple()
? Arrays.stream(fieldText.split(",", -1)).map(String::trim).toList()
: List.of(fieldText);
if (selectedLabels.stream().anyMatch(label -> !StringUtils.hasText(label))) {
messages.add(fieldName + "包含空选项,请使用英文逗号正确分隔");
continue;
}
List<String> invalidLabels = selectedLabels.stream().filter(label -> !labels.contains(label)).distinct().toList();
if (!invalidLabels.isEmpty()) {
messages.add(fieldName + ":" + String.join(",", invalidLabels) + ",不在可选项中");
}
}
}
catch (IllegalAccessException exception) {
+7
View File
@@ -146,6 +146,13 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Sentinel BOM 会把 mockito-core 降至 2.28.2,此处与 Spring Boot 3.3.6 的 Mockito 5 测试套件保持一致。 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<!--引入分布式事务-->
<dependency>
<groupId>com.bsd</groupId>
@@ -19,7 +19,7 @@ import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.core.notification.RequestParam;
import com.wechat.pay.java.service.partnerpayments.app.AppServiceExtension;
import com.wechat.pay.java.service.partnerpayments.app.model.Transaction;
import com.wechat.pay.java.service.partnerpayments.app.model.TransactionAmount;
import com.wechat.pay.java.service.partnerpayments.model.TransactionAmount;
import com.wechat.pay.java.service.refund.RefundService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
@@ -43,11 +43,6 @@ public class StudentUserExcelVO {
*/
private Long guardianUserId;
/**
* 监护人部门id
*/
private Long guardianDeptId;
/**
* 学号
*/
@@ -55,6 +50,19 @@ public class StudentUserExcelVO {
@ExcelProperty("学号")
private String studentNo;
/**
* 学生所属部门名称或路径,必须匹配当前租户已有部门。
*/
@NotBlank(message = "学生部门")
@ExcelProperty("学生部门")
private String studentDeptName;
/**
* 同步校验阶段解析出的学生部门 ID,仅供异步写入使用,不直接读取 Excel。
*/
@ExcelIgnore
private Long studentDeptId;
/**
* 学校名称
*/
@@ -31,6 +31,7 @@ import com.bsd.pjcloud.common.excel.annotation.ExcelTemplate;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.common.security.annotation.HasPermission;
import com.bsd.pjcloud.common.security.annotation.Inner;
import com.bsd.pjcloud.wallet.api.dto.recharge.excel.WalletRechargeExcelDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -152,7 +153,7 @@ public class SysFileController {
* @param response 本地文件
*/
@SneakyThrows
@ExcelTemplate
@ExcelTemplate(WalletRechargeExcelDto.class)
@GetMapping("/local/file/{fileName}")
public void localFile(@PathVariable String fileName, HttpServletResponse response) {
ClassPathResource resource = new ClassPathResource("file/" + fileName);
@@ -421,7 +421,7 @@ public class SysUserController {
* @return 校验失败时包含对应行号和错误原因;校验通过时包含异步任务 ID 和初始进度
*/
@PostMapping("/import")
@HasPermission("sys_user_export")
@HasPermission("sys_user_import")
public R<?> importUser(@RequestExcel List<UserExcelVO> excelVOList, BindingResult bindingResult) {
return userImportExportService.importUser(excelVOList, bindingResult);
}
@@ -479,7 +479,7 @@ public class SysUserController {
* @return 校验失败时包含校验或图片处理错误;校验通过时包含异步任务 ID 和初始进度
*/
@PostMapping("/import-face")
@HasPermission("sys_user_export")
@HasPermission("sys_user_import")
@Idempotent(expireTime = 5, key = "'/user/import-face:' + #file.getSize() + ':' + #excelList.hashCode() + ':' + T(com.bsd.pjcloud.common.security.util.SecurityUtils).getUser().getId()", info = "正在导入中,请勿重复操作")
public R<?> importUserFace(@RequestParam("file") MultipartFile file, @RequestExcel List<UserExcelFaceVo> excelList, BindingResult bindingResult) {
return userImportExportService.importUserFace(file, excelList, bindingResult);
@@ -254,10 +254,13 @@ public class SysUserImportExportServiceImpl implements SysUserImportExportServic
excel.setUsername(StrUtil.trim(excel.getUsername()));
excel.setGuardianPhone(StrUtil.trim(excel.getGuardianPhone()));
excel.setStudentNo(StrUtil.trim(excel.getStudentNo()));
excel.setStudentDeptName(StrUtil.trim(excel.getStudentDeptName()));
excel.setSchoolName(StrUtil.trim(excel.getSchoolName()));
excel.setClassName(StrUtil.trim(excel.getClassName()));
});
DepartmentIndex departmentIndex = new DepartmentIndex(sysDeptService.list());
List<String> guardianPhones = studentUserExcelList.stream().map(StudentUserExcelVO::getGuardianPhone)
.filter(StrUtil::isNotBlank).distinct().toList();
Map<String, SysUser> guardianUserMap = guardianPhones.isEmpty() ? Map.of() : sysUserService.list(
@@ -324,9 +327,12 @@ public class SysUserImportExportServiceImpl implements SysUserImportExportServic
errorMessages.add("监护人手机号:" + excel.getGuardianPhone() + ",对应的用户未添加或未审批通过");
} else {
excel.setGuardianUserId(guardian.getUserId());
excel.setGuardianDeptId(guardian.getDeptId());
}
}
SysDept studentDepartment = resolveDepartment(excel.getStudentDeptName(), departmentIndex, errorMessages);
if (studentDepartment != null) {
excel.setStudentDeptId(studentDepartment.getDeptId());
}
if (CollUtil.isNotEmpty(errorMessages)) {
errorMessageList.add(new ErrorMessage(excel.getLineNum(), errorMessages));
}
@@ -740,7 +746,7 @@ public class SysUserImportExportServiceImpl implements SysUserImportExportServic
}
/**
* 创建学生用户及监护关系,学生扩展信息只属于 STUDENT 身份。
* 创建学生用户及监护关系,学生扩展信息只属于 STUDENT 身份,用户部门使用模板中已校验的学生部门。
*
* @param excel 当前校验通过的学生行
* @param guardianWardRelationTypeId 监护关系类型 ID
@@ -757,7 +763,7 @@ public class SysUserImportExportServiceImpl implements SysUserImportExportServic
relationUser.setStaffingType(UserStaffingTypeEnum.NONE_STAFFING.getType());
relationUser.setIdentityTypes(List.of(UserIdentityTypeEnum.STUDENT.getType()));
relationUser.setRole(studentRoleIds);
relationUser.setDeptId(excel.getGuardianDeptId());
relationUser.setDeptId(excel.getStudentDeptId());
SysUserStudentDto studentInfo = new SysUserStudentDto();
studentInfo.setStudentNo(excel.getStudentNo());
studentInfo.setSchoolName(excel.getSchoolName());
@@ -219,6 +219,18 @@
<if test="query.staffingType != null">
AND u.staffing_type = #{query.staffingType}
</if>
<!-- 多身份查询按任一身份命中处理,EXISTS 可避免身份表的一对多关系放大用户结果集。 -->
<if test="query.identityTypes != null and query.identityTypes.size() != 0">
AND EXISTS (
SELECT 1
FROM sys_user_identity user_identity
WHERE user_identity.user_id = u.user_id
AND user_identity.identity_type IN
<foreach collection="query.identityTypes" item="identityType" open="(" close=")" separator=",">
#{identityType}
</foreach>
)
</if>
<if test="query.role != null and query.role.size() != 0">
and urole.role_id IN
<foreach collection="query.role" item="v" open="(" close=")" separator=",">
@@ -307,6 +319,18 @@
<if test="query.deptId != null and query.deptId != ''">
AND u.dept_id = #{query.deptId}
</if>
<!-- 导出沿用列表页的身份筛选语义,确保导出结果与当前查询一致。 -->
<if test="query.identityTypes != null and query.identityTypes.size() != 0">
AND EXISTS (
SELECT 1
FROM sys_user_identity user_identity
WHERE user_identity.user_id = u.user_id
AND user_identity.identity_type IN
<foreach collection="query.identityTypes" item="identityType" open="(" close=")" separator=",">
#{identityType}
</foreach>
)
</if>
<if test="query.role != null and query.role.size() != 0">
and urole.role_id IN
<foreach collection="query.role" item="v" open="(" close=")" separator=",">
@@ -0,0 +1,34 @@
package com.bsd.pjcloud.wallet.api.constant;
import cn.hutool.core.util.ObjectUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 充值流水tag
*/
@Getter
@AllArgsConstructor
public enum WalletTransactionTag {
BJZ_CZ("1", "不记账充值"),
;
private final String code;
private final String desc;
public static WalletTransactionTag valueOfByCode(String code) {
if (ObjectUtil.isEmpty(code)) return null;
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(code, v.getCode())).findFirst().orElse(null);
}
public static WalletTransactionTag descOf(String description) {
return Arrays.stream(WalletTransactionTag.values())
.filter(v -> ObjectUtil.equal(v.getDesc(), description))
.findFirst().orElse(null);
}
}
@@ -23,12 +23,12 @@ public enum WalletTypeEnum {
public static WalletTypeEnum valueOfByCode(String code) {
if (ObjectUtil.isEmpty(code)) return null;
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(code, v.getCode())).findFirst().get();
return Arrays.stream(values()).filter(v -> ObjectUtil.equals(code, v.getCode())).findFirst().orElse(null);
}
public static WalletTypeEnum descOf(String description) {
return Arrays.stream(WalletTypeEnum.values())
.filter(v -> ObjectUtil.equal(v.getDesc(), description))
.findFirst().get();
.findFirst().orElse(null);
}
}
@@ -32,4 +32,9 @@ public class WalletBalanceTransactionDto {
@NotNull(message = "是否暴力消费")
private Boolean force = false;
/**
* 财务统计标签字典值,多个值使用英文逗号分隔;未传时不为流水设置标签。
*/
private String tags;
}
@@ -39,6 +39,11 @@ public class WalletTransactionDto extends WalletTransactionEntity implements Bas
private List<String> bizTypes;
/**
* 财务统计标签筛选值;传入多个字典值时匹配包含任一标签的流水。
*/
private List<String> tagList;
private LocalDate[] createTimeList;
/**
@@ -18,6 +18,11 @@ public class WalletRechargeImportDto extends WalletRechargeImportEntity implemen
private List<Long> userUniqueIds;
/**
* 财务统计标签筛选值;传入多个字典值时匹配包含任一标签的导入记录。
*/
private List<String> tagList;
private Long userDeptId;
@@ -6,6 +6,7 @@ import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
import com.alibaba.excel.annotation.write.style.HeadRowHeight;
import com.bsd.pjcloud.common.excel.annotation.ExcelDict;
import com.bsd.pjcloud.common.excel.annotation.ExcelLine;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -42,6 +43,20 @@ public class WalletRechargeExcelDto {
@ExcelProperty("操作金额")
private String price;
/**
* Excel 中展示和填写的财务标签名称;支持使用英文逗号分隔多个 {@code wallet_transaction_tag} 字典标签。
*/
@ColumnWidth(100)
@ExcelProperty("操作标签")
@ExcelDict(value = "wallet_transaction_tag", multiple = true)
private String tag;
/**
* 导入校验后解析出的财务标签字典值,多个值使用英文逗号分隔,不直接读取 Excel。
*/
@ExcelIgnore
private String tags;
// 操作余额
private Integer priceNumber;
@@ -78,6 +78,12 @@ public class WalletTransactionEntity extends Model<WalletTransactionEntity> {
@Schema(description = "余额, 单位分")
private Integer balance;
/**
* 财务统计标签,保存 {@code wallet_transaction_tag} 字典值;多个值使用英文逗号分隔,历史流水可为空。
*/
@Schema(description = "财务统计标签,多个字典值使用英文逗号分隔")
private String tags;
/**
* 创建人
*/
@@ -119,4 +125,4 @@ public class WalletTransactionEntity extends Model<WalletTransactionEntity> {
*/
@Schema(description = "租户ID")
private Long tenantId;
}
}
@@ -67,6 +67,12 @@ public class WalletRechargeImportEntity extends Model<WalletRechargeImportEntity
@Schema(description = "交易金额, 单位分")
private Integer price;
/**
* 财务统计标签,保存 {@code wallet_transaction_tag} 字典值;多个值使用英文逗号分隔,未选择时为空。
*/
@Schema(description = "财务统计标签,多个字典值使用英文逗号分隔")
private String tags;
/**
* 创建人
*/
@@ -108,4 +114,4 @@ public class WalletRechargeImportEntity extends Model<WalletRechargeImportEntity
*/
@Schema(description = "租户ID")
private Long tenantId;
}
}
@@ -0,0 +1,36 @@
package com.bsd.pjcloud.wallet.config;
import com.bsd.pjcloud.admin.api.entity.SysDictItem;
import com.bsd.pjcloud.admin.api.feign.RemoteDictService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.excel.dict.ExcelDictProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Collections;
import java.util.List;
/**
* 钱包服务 Excel 字典配置。
*/
@Configuration
public class ExcelDictConfiguration {
/**
* 通过 UPMS 远程字典接口提供当前租户的 Excel 下拉标签。
*
* @param remoteDictService UPMS 字典远程服务
* @return 钱包服务使用的 Excel 字典数据提供者;远程响应异常或无数据时返回空标签列表
*/
@Bean
public ExcelDictProvider excelDictProvider(RemoteDictService remoteDictService) {
return dictType -> {
R<List<SysDictItem>> result = remoteDictService.getDictByType(dictType);
if (result == null || !result.isOk() || result.getData() == null) {
return Collections.emptyList();
}
return result.getData().stream().map(SysDictItem::getLabel).toList();
};
}
}
@@ -56,7 +56,7 @@ public class WalletTransactionController {
@Inner
@PostMapping("/add-balance")
public R<WalletTransactionEntity> addWalletBalance(@Valid @RequestBody WalletBalanceTransactionDto dto) {
WalletTransactionEntity walletTransaction = walletTransactionService.addWalletBalance(dto.getWalletId(), dto.getPrice(), dto.getBizId(), dto.getBizType());
WalletTransactionEntity walletTransaction = walletTransactionService.addWalletBalance(dto.getWalletId(), dto.getPrice(), dto.getBizId(), dto.getBizType(), dto.getTags());
if (ObjectUtil.isEmpty(walletTransaction)) return R.failed("增加钱包余额错误");
return R.ok(walletTransaction);
}
@@ -61,6 +61,20 @@ public interface WalletTransactionMapper extends PjcloudBaseMapper<WalletTransac
} else if (Objects.equals(dto.getType(), WalletTransactionDto.TYPE_EXPENSE)) {
wrapper.lt(WalletTransactionEntity::getPrice, 0);
}
if (ObjectUtil.isNotEmpty(dto.getTagList())) {
wrapper.and(nested -> {
boolean first = true;
for (String tag : dto.getTagList()) {
if (first) {
nested.apply("FIND_IN_SET({0}, tags) > 0", tag);
first = false;
}
else {
nested.or().apply("FIND_IN_SET({0}, tags) > 0", tag);
}
}
});
}
return wrapper;
}
@@ -98,5 +112,11 @@ public interface WalletTransactionMapper extends PjcloudBaseMapper<WalletTransac
List<WalletTransactionSumAndCountVo> sumAndCount(@Param("dto") WalletTransactionSumAndCountDto dto);
List<WalletExportCzTotalPerson> exportCzTotalPerson();
}
/**
* 查询固定金额范围内的个人充值统计明细,并按财务标签筛选。
*
* @param dto 充值统计筛选条件;标签多选时匹配包含任一标签的流水
* @return 个人充值统计导出明细
*/
List<WalletExportCzTotalPerson> exportCzTotalPerson(@Param("dto") WalletTransactionDto dto);
}
@@ -69,6 +69,18 @@ public interface WalletTransactionService extends IService<WalletTransactionEnti
*/
WalletTransactionEntity addWalletBalance(Long walletId, Integer price, String bizId, WalletBizEnum bizType);
/**
* 增加钱包余额并记录财务统计标签。
*
* @param walletId 钱包 id
* @param price 增加金额,单位分
* @param bizId 业务关联 id
* @param bizType 业务关联分类
* @param tags 财务标签字典值,多个值使用英文逗号分隔,允许为空
* @return 已保存的钱包流水
*/
WalletTransactionEntity addWalletBalance(Long walletId, Integer price, String bizId, WalletBizEnum bizType, String tags);
/**
* 更新会员用户余额
*
@@ -85,6 +97,16 @@ public interface WalletTransactionService extends IService<WalletTransactionEnti
*/
Boolean resetWalletBalance(Long walletId, String bizId);
/**
* 重置钱包余额并记录财务统计标签。
*
* @param walletId 钱包 id
* @param bizId 业务关联 id
* @param tags 财务标签字典值,多个值使用英文逗号分隔,允许为空
* @return 重置成功返回 {@code true}
*/
Boolean resetWalletBalance(Long walletId, String bizId, String tags);
void exportByTemplate(HttpServletResponse response, WalletTransactionDto dto, String fileName, List<WalletTransactionVo> list);
/**
@@ -212,6 +212,22 @@ public class WalletTransactionServiceImpl extends ServiceImpl<WalletTransactionM
@Transactional(rollbackFor = Exception.class)
// @CacheEvict(value = CacheConstants.WALLET_DETAIL, key = "#walletId")
public WalletTransactionEntity addWalletBalance(Long walletId, Integer price, String bizId, WalletBizEnum bizType) {
return getSelf().addWalletBalance(walletId, price, bizId, bizType, null);
}
/**
* 增加钱包余额并将来源业务的财务标签同步到最终流水,供后续列表筛选和金额统计。
*
* @param walletId 钱包 id
* @param price 增加金额,单位分
* @param bizId 业务关联 id
* @param bizType 业务关联分类
* @param tags 财务标签字典值,多个值使用英文逗号分隔,允许为空
* @return 已保存的钱包流水
*/
@Override
@Transactional(rollbackFor = Exception.class)
public WalletTransactionEntity addWalletBalance(Long walletId, Integer price, String bizId, WalletBizEnum bizType, String tags) {
WalletManageEntity wallet = walletManageService.getById(walletId);
if (ObjectUtil.isEmpty(wallet)) {
log.error("|{}|,用户钱包id({})不存在.", TAG, walletId);
@@ -241,6 +257,7 @@ public class WalletTransactionServiceImpl extends ServiceImpl<WalletTransactionM
insetObj.setBizId(String.valueOf(bizId));
insetObj.setBizType(bizType.getCode());
insetObj.setTitle(bizType.getDesc());
insetObj.setTags(tags);
save(insetObj);
return insetObj;
}
@@ -257,7 +274,7 @@ public class WalletTransactionServiceImpl extends ServiceImpl<WalletTransactionM
throw new RuntimeException("用户钱包不存在");
}
// 更新钱包余额
WalletTransactionEntity result = getSelf().addWalletBalance(wallet.getId(), dto.getBalance(), String.valueOf(dto.getUserId()), WalletBizEnum.UPDATE);
WalletTransactionEntity result = getSelf().addWalletBalance(wallet.getId(), dto.getBalance(), String.valueOf(dto.getUserId()), WalletBizEnum.UPDATE, dto.getTags());
// 查询钱包类型
WalletTypeEntity walletType = walletTypeMapper.selectById(wallet.getWalletTypeId());
if (ObjectUtil.isNotEmpty(walletType) && ObjectUtil.equals(walletType.getUniqueCode(), WalletTypeEnum.EAT.getCode())) {
@@ -276,6 +293,20 @@ public class WalletTransactionServiceImpl extends ServiceImpl<WalletTransactionM
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean resetWalletBalance(Long walletId, String bizId) {
return getSelf().resetWalletBalance(walletId, bizId, null);
}
/**
* 重置钱包余额并将来源导入记录的财务标签同步到重置流水。
*
* @param walletId 钱包 id
* @param bizId 业务关联 id
* @param tags 财务标签字典值,多个值使用英文逗号分隔,允许为空
* @return 重置成功返回 {@code true}
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean resetWalletBalance(Long walletId, String bizId, String tags) {
WalletManageEntity wallet = walletManageService.getById(walletId);
if (ObjectUtil.isEmpty(wallet)) {
log.error("|{}|重置余额,用户钱包id({})不存在.", TAG, walletId);
@@ -298,6 +329,7 @@ public class WalletTransactionServiceImpl extends ServiceImpl<WalletTransactionM
insetObj.setBizId(String.valueOf(bizId));
insetObj.setBizType(WalletBizEnum.RESET.getCode());
insetObj.setTitle(WalletBizEnum.RESET.getDesc());
insetObj.setTags(tags);
save(insetObj);
return true;
}
@@ -535,7 +567,7 @@ public class WalletTransactionServiceImpl extends ServiceImpl<WalletTransactionM
String fileName = "file/个人充值统计800、1600、2400.xlsx";
try (ExcelWriter excelWriter = EasyExcel.write(response.getOutputStream()).withTemplate(ResourceUtil.getStream(fileName)).build()) {
// 查询导出数据
List<WalletExportCzTotalPerson> list = baseMapper.exportCzTotalPerson();
List<WalletExportCzTotalPerson> list = baseMapper.exportCzTotalPerson(dto);
WriteSheet writeSheet = EasyExcel.writerSheet().build();
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
@@ -12,8 +12,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
import com.bsd.pjcloud.admin.api.entity.SysDept;
import com.bsd.pjcloud.admin.api.entity.SysDictItem;
import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.admin.api.feign.RemoteDeptService;
import com.bsd.pjcloud.admin.api.feign.RemoteDictService;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
import com.bsd.pjcloud.admin.api.utils.DataUtil;
import com.bsd.pjcloud.admin.api.vo.UserVO;
@@ -90,6 +92,11 @@ public class WalletRechargeApplyServiceImpl extends ServiceImpl<WalletRechargeAp
static private final String TAG = "充值/扣款申请";
/**
* 充值流水财务标签使用的 UPMS 字典类型,Excel 展示标签而数据库保存对应字典值。
*/
private static final String TRANSACTION_TAG_DICT_TYPE = "wallet_transaction_tag";
private final WalletRechargeImportServiceImpl rechargeImportService;
private final WalletManageService walletManageService;
private final WalletTransactionService walletTransactionService;
@@ -103,6 +110,7 @@ public class WalletRechargeApplyServiceImpl extends ServiceImpl<WalletRechargeAp
private final RemoteUserService userService;
private final RemoteDeptService deptService;
private final RemoteDictService remoteDictService;
private final RemoteFlowEngineService flowEngineService;
private final FlowUtil flowUtil;
@@ -328,7 +336,7 @@ public class WalletRechargeApplyServiceImpl extends ServiceImpl<WalletRechargeAp
if (i <= 0) {
throw new RuntimeException("导入失败");
}
List<WalletRechargeImportEntity> batchSaveList = list.stream().map(v -> WalletRechargeImportEntity.builder().price(v.getPriceNumber()).userId(v.getUserId()).userDeptId(v.getUserDeptId()).build()).toList();
List<WalletRechargeImportEntity> batchSaveList = list.stream().map(v -> WalletRechargeImportEntity.builder().price(v.getPriceNumber()).userId(v.getUserId()).userDeptId(v.getUserDeptId()).tags(v.getTags()).build()).toList();
batchSaveList.forEach(v -> v.setApplyId(insertObj.getId()));
rechargeImportService.saveBatch(batchSaveList);
return R.ok(null, "导入成功");
@@ -349,7 +357,7 @@ public class WalletRechargeApplyServiceImpl extends ServiceImpl<WalletRechargeAp
if (i <= 0) {
throw new RuntimeException("导入失败");
}
List<WalletRechargeImportEntity> batchSaveList = list.stream().map(v -> WalletRechargeImportEntity.builder().price(0).userId(v.getUserId()).userDeptId(v.getUserDeptId()).build()).toList();
List<WalletRechargeImportEntity> batchSaveList = list.stream().map(v -> WalletRechargeImportEntity.builder().price(0).userId(v.getUserId()).userDeptId(v.getUserDeptId()).tags(v.getTags()).build()).toList();
batchSaveList.forEach(v -> v.setApplyId(insertObj.getId()));
rechargeImportService.saveBatch(batchSaveList);
return R.ok(null, "导入成功");
@@ -371,7 +379,7 @@ public class WalletRechargeApplyServiceImpl extends ServiceImpl<WalletRechargeAp
WalletRechargeInvoiceEntity invoice = walletRechargeInvoiceService.createBaseInvoice(loginUser, totalPrice);
if (ObjectUtil.isEmpty(invoice)) throw new RuntimeException("导入失败");
// 导入数据
List<WalletRechargeImportEntity> batchSaveList = list.stream().map(v -> WalletRechargeImportEntity.builder().price(v.getPriceNumber()).userId(v.getUserId()).userDeptId(v.getUserDeptId()).invoiceId(invoice.getId()).build()).toList();
List<WalletRechargeImportEntity> batchSaveList = list.stream().map(v -> WalletRechargeImportEntity.builder().price(v.getPriceNumber()).userId(v.getUserId()).userDeptId(v.getUserDeptId()).invoiceId(invoice.getId()).tags(v.getTags()).build()).toList();
rechargeImportService.saveBatch(batchSaveList);
return R.ok(null, "导入成功");
}
@@ -388,6 +396,7 @@ public class WalletRechargeApplyServiceImpl extends ServiceImpl<WalletRechargeAp
throw new RuntimeException("读取excel数据错误");
}
List<ErrorMessage> errorMessageList = (List<ErrorMessage>) bindingResult.getTarget();
resolveTransactionTagValues(list, errorMessageList);
List<String> usernameList = list.stream().map(WalletRechargeExcelDto::getUsername).toList();
List<String> userPoneList = list.stream().map(WalletRechargeExcelDto::getUserMobile).toList();
for (WalletRechargeExcelDto v : list) {
@@ -442,6 +451,41 @@ public class WalletRechargeApplyServiceImpl extends ServiceImpl<WalletRechargeAp
return ImmutablePair.of(null, list);
}
/**
* 将 Excel 中展示的财务标签转换为稳定的字典值,供导入记录和最终钱包流水持久化及统计查询。
*
* @param list 已通过公共 Excel 字典标签校验的数据行
* @param errorMessageList 当前导入任务的行级错误集合,字典服务异常或标签映射缺失时追加错误
*/
private void resolveTransactionTagValues(List<WalletRechargeExcelDto> list, List<ErrorMessage> errorMessageList) {
List<WalletRechargeExcelDto> taggedRows = list.stream().filter(item -> ObjectUtil.isNotEmpty(item.getTag())).toList();
if (taggedRows.isEmpty()) {
return;
}
R<List<SysDictItem>> result = remoteDictService.getDictByType(TRANSACTION_TAG_DICT_TYPE);
if (result == null || !result.isOk() || ObjectUtil.isEmpty(result.getData())) {
taggedRows.forEach(item -> errorMessageList.add(new ErrorMessage(item.getLineNum(), Set.of("操作标签字典查询失败"))));
return;
}
Map<String, String> valueByLabel = result.getData().stream()
.filter(item -> ObjectUtil.isNotEmpty(item.getLabel()) && ObjectUtil.isNotEmpty(item.getItemValue()))
.collect(java.util.stream.Collectors.toMap(SysDictItem::getLabel, SysDictItem::getItemValue, (first, ignored) -> first));
for (WalletRechargeExcelDto item : taggedRows) {
List<String> tagValues = Arrays.stream(item.getTag().split(","))
.map(String::trim)
.distinct()
.map(valueByLabel::get)
.filter(Objects::nonNull)
.toList();
long selectedTagCount = Arrays.stream(item.getTag().split(",")).map(String::trim).distinct().count();
if (tagValues.size() != selectedTagCount) {
errorMessageList.add(new ErrorMessage(item.getLineNum(), Set.of("操作标签包含无法映射的字典项")));
continue;
}
item.setTags(String.join(",", tagValues));
}
}
/* ************************************* end excel end ************************************* */
@Override
@@ -497,7 +541,7 @@ public class WalletRechargeApplyServiceImpl extends ServiceImpl<WalletRechargeAp
}
// 判断是`退还各自食堂`还是`批量更新余额`
WalletBizEnum walletBiz = ObjectUtil.equal(WalletBizRechargeEnum.RETURN_CANTEEN.getCode(), detail.getBizType()) ? WalletBizEnum.RETURN_CANTEEN : WalletBizEnum.BATCH;
walletTransactionService.addWalletBalance(wallet.getId(), v.getPrice(), String.valueOf(v.getId()), walletBiz);
walletTransactionService.addWalletBalance(wallet.getId(), v.getPrice(), String.valueOf(v.getId()), walletBiz, v.getTags());
}
// 同步到正元钱包状态
List<Long> userIds = importList.stream().map(WalletRechargeImportEntity::getUserId).distinct().toList();
@@ -548,7 +592,7 @@ public class WalletRechargeApplyServiceImpl extends ServiceImpl<WalletRechargeAp
if (ObjectUtil.isEmpty(wallet)) {
continue;
}
walletTransactionService.resetWalletBalance(wallet.getId(), String.valueOf(v.getId()));
walletTransactionService.resetWalletBalance(wallet.getId(), String.valueOf(v.getId()), v.getTags());
}
log.info("|{}|重置余额审批回调,dto:|{}", TAG, JSON.toJSONString(detail));
} catch (Exception e) {
@@ -766,9 +810,9 @@ public class WalletRechargeApplyServiceImpl extends ServiceImpl<WalletRechargeAp
if (ObjectUtil.isEmpty(cardWalletVo)) continue;
// 先判断消费钱包是不是够用
int discountPrice = payWalletVo.getBalance() - i.getPrice();
walletTransactionService.addWalletBalance(payWalletVo.getId(), payWalletVo.getBalance() * -1, String.valueOf(i.getId()), WalletBizEnum.BATCH);
walletTransactionService.addWalletBalance(payWalletVo.getId(), payWalletVo.getBalance() * -1, String.valueOf(i.getId()), WalletBizEnum.BATCH, i.getTags());
if (discountPrice < 0) {
walletTransactionService.addWalletBalance(cardWalletVo.getId(), discountPrice, String.valueOf(i.getId()), WalletBizEnum.BATCH);
walletTransactionService.addWalletBalance(cardWalletVo.getId(), discountPrice, String.valueOf(i.getId()), WalletBizEnum.BATCH, i.getTags());
}
}
}
@@ -776,4 +820,4 @@ public class WalletRechargeApplyServiceImpl extends ServiceImpl<WalletRechargeAp
private WalletRechargeApplyServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
}
}
@@ -124,7 +124,7 @@ public class WalletRechargeImportServiceImpl extends ServiceImpl<WalletRechargeI
if (ObjectUtil.isNotEmpty(walletTransaction)) throw new RuntimeException("钱包已经充值过");
WalletManageEntity wallet = walletManageService.getOrCreateWallet(rechargeImport.getUserId(), walletTypeEnum);
if (ObjectUtil.isEmpty(wallet)) throw new RuntimeException("钱包错误");
walletTransactionService.addWalletBalance(wallet.getId(), rechargeImport.getPrice(), String.valueOf(rechargeImport.getId()), WalletBizEnum.BATCH);
walletTransactionService.addWalletBalance(wallet.getId(), rechargeImport.getPrice(), String.valueOf(rechargeImport.getId()), WalletBizEnum.BATCH, rechargeImport.getTags());
return true;
}
@@ -149,4 +149,4 @@ public class WalletRechargeImportServiceImpl extends ServiceImpl<WalletRechargeI
}
}
}
}
}
@@ -10,6 +10,7 @@
<result property="title" column="title"/>
<result property="price" column="price"/>
<result property="balance" column="balance"/>
<result property="tags" column="tags"/>
</resultMap>
<sql id="selectA">
@@ -22,6 +23,7 @@
a.title,
a.price,
a.balance,
a.tags,
a.create_by,
a.create_time,
a.update_by,
@@ -44,6 +46,13 @@
<if test="dto.bizType != null">
and a.biz_type = #{dto.bizType}
</if>
<if test="dto.tagList != null and dto.tagList.size() != 0">
and (
<foreach collection="dto.tagList" item="tag" separator=" or ">
FIND_IN_SET(#{tag}, a.tags) &gt; 0
</foreach>
)
</if>
<if test="dto.deptId != null">
and a.dept_id = #{dto.deptId}
</if>
@@ -286,6 +295,13 @@
and t.biz_type in ('1', '2', '3')
and t.price in (80000, 160000, 240000)
and t.dept_id not in (1875013738914156546, 1910218102049226753, 1910218128624336898, 1942046316044144642)
<if test="dto.tagList != null and dto.tagList.size() != 0">
and (
<foreach collection="dto.tagList" item="tag" separator=" or ">
FIND_IN_SET(#{tag}, t.tags) &gt; 0
</foreach>
)
</if>
order by v.mchId, v.deptId
</select>
@@ -303,4 +319,4 @@
</where>
)
</select>
</mapper>
</mapper>
@@ -9,6 +9,7 @@
<result property="applyId" column="apply_id"/>
<result property="beforePrice" column="before_price"/>
<result property="price" column="price"/>
<result property="tags" column="tags"/>
<result property="createBy" column="create_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="update_by"/>
@@ -24,6 +25,7 @@
a.apply_id,
a.before_price,
a.price,
a.tags,
a.create_by,
a.create_time,
a.update_by,
@@ -71,6 +73,13 @@
<if test="dto.createTimeList != null and dto.createTimeList.length == 2">
and a.create_time between #{dto.createTimeList[0]} and #{dto.createTimeList[1]}
</if>
<if test="dto.tagList != null and dto.tagList.size() != 0">
and (
<foreach collection="dto.tagList" item="tag" separator=" or ">
FIND_IN_SET(#{tag}, a.tags) &gt; 0
</foreach>
)
</if>
</sql>
<sql id="selectJoin">
@@ -134,4 +143,4 @@
</if>
</where>
</select>
</mapper>
</mapper>