fix:部门交易记录查询慢的问题

This commit is contained in:
dzc committed 2026-09-01 19:47:00 +08:00
1 parent 0c19eff578
commit a3c89be742
11 files changed
+295 -68

No files matched your search

@@ -32,6 +32,7 @@ import com.bsd.pjcloud.hard.mapper.order.model.HardOrderDepartmentStatisticsDeta
import com.bsd.pjcloud.hard.service.HardManagerService;
import com.bsd.pjcloud.hard.service.HardOrderTotalService;
import com.bsd.pjcloud.wallet.api.constant.WalletBizEnum;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotBatchDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionSumAndCountDto;
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
@@ -432,10 +433,19 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
List<WalletTransactionSumAndCountVo> walletOnceReduceList = queryWalletStatistics(
buildWalletStatisticsDto(dto, WalletBizEnum.UPDATE.getCode(), WalletTransactionSumAndCountDto.TYPE_EXPENSE));
List<HardOrderSumAndCountVo> payList = aggregateByTransactionDepartment(
hardOrderMapper.listPayStatisticsDetails(buildOrderStatisticsDto(dto, false)), WalletBizEnum.PAYMENT, dto);
List<HardOrderSumAndCountVo> refundList = aggregateByTransactionDepartment(
hardOrderMapper.listRefundStatisticsDetails(buildOrderStatisticsDto(dto, true)), WalletBizEnum.REFUND, dto);
List<HardOrderDepartmentStatisticsDetail> payDetails =
hardOrderMapper.listPayStatisticsDetails(buildOrderStatisticsDto(dto, false));
List<HardOrderDepartmentStatisticsDetail> refundDetails =
hardOrderMapper.listRefundStatisticsDetails(buildOrderStatisticsDto(dto, true));
Map<String, List<HardOrderDepartmentStatisticsDetail>> detailsByBizType = new LinkedHashMap<>();
detailsByBizType.put(WalletBizEnum.PAYMENT.getCode(), payDetails);
detailsByBizType.put(WalletBizEnum.REFUND.getCode(), refundDetails);
Map<String, Map<String, WalletTransactionDepartmentSnapshotVo>> transactionSnapshotMaps =
queryTransactionDepartmentSnapshotMaps(detailsByBizType);
List<HardOrderSumAndCountVo> payList = aggregateByTransactionDepartment(payDetails,
transactionSnapshotMaps.getOrDefault(WalletBizEnum.PAYMENT.getCode(), Map.of()), dto, false);
List<HardOrderSumAndCountVo> refundList = aggregateByTransactionDepartment(refundDetails,
transactionSnapshotMaps.getOrDefault(WalletBizEnum.REFUND.getCode(), Map.of()), dto, true);
Map<Long, WalletTransactionSumAndCountVo> walletBatchMap = indexWalletStatistics(walletBatchList);
Map<Long, WalletTransactionSumAndCountVo> walletReduceMap = indexWalletStatistics(walletReduceList);
@@ -569,20 +579,23 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
/**
* 将订单或售后明细按钱包流水发生时保存的部门重新归集,并在重新归属后应用页面部门条件。
*
* <p>现金、第三方支付或历史异常记录没有对应钱包流水时,继续使用订单创建时保存的部门快照,
* 避免为了统一钱包口径而丢失原有非钱包交易能力。</p>
* <p>支付统计中,现金、第三方支付或历史异常记录没有对应钱包流水时,继续使用订单创建时保存的
* 部门快照,避免丢失原有非钱包交易能力。退款统计则与退款记录页面一致,只接受真实 REFUND
* 钱包流水,不允许把没有资金流水的已完成售后回退计入。</p>
*
* @param details 符合订单状态、商户和时间条件的支付或售后明细
* @param bizType 支付或退款的钱包流水业务类型
* @param transactionSnapshotMap 当前业务类型以业务号为键的钱包流水快照
* @param dto 页面部门包含或排除条件
* @param transactionOnly 是否只统计真实存在的钱包流水,并以钱包流水金额为准
* @return 按最终历史部门归集后的金额和次数
*/
private List<HardOrderSumAndCountVo> aggregateByTransactionDepartment(
List<HardOrderDepartmentStatisticsDetail> details, WalletBizEnum bizType, HardDeptTotalDto dto) {
List<HardOrderDepartmentStatisticsDetail> details,
Map<String, WalletTransactionDepartmentSnapshotVo> transactionSnapshotMap,
HardDeptTotalDto dto, boolean transactionOnly) {
if (ObjectUtil.isEmpty(details)) {
return List.of();
}
Map<String, Long> transactionDepartmentMap = queryTransactionDepartmentMap(details, bizType);
Set<Long> departmentFilter = ObjectUtil.isEmpty(dto.getDeptIds())
? Set.of() : new HashSet<>(dto.getDeptIds());
Map<Long, HardOrderSumAndCountVo> aggregateMap = new LinkedHashMap<>();
@@ -590,7 +603,12 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
if (detail == null) {
continue;
}
Long departmentId = transactionDepartmentMap.getOrDefault(detail.getBizId(), detail.getDeptId());
WalletTransactionDepartmentSnapshotVo snapshot = transactionSnapshotMap.get(detail.getBizId());
if (transactionOnly && (snapshot == null || snapshot.getPrice() == null)) {
// 退款记录以真实 REFUND 钱包流水为准,已完成但没有资金流水的售后单不能回退计入。
continue;
}
Long departmentId = snapshot == null ? detail.getDeptId() : snapshot.getDeptId();
if (departmentId == null || !matchesDepartmentFilter(departmentId, departmentFilter, dto.getIsInclude())) {
continue;
}
@@ -601,43 +619,69 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
value.setCount(0L);
return value;
});
aggregate.setSum(Math.addExact(aggregate.getSum(), ObjectUtil.defaultIfNull(detail.getSum(), 0L)));
long amount = transactionOnly
? Math.abs(snapshot.getPrice()) : ObjectUtil.defaultIfNull(detail.getSum(), 0L);
aggregate.setSum(Math.addExact(aggregate.getSum(), amount));
aggregate.setCount(Math.addExact(aggregate.getCount(), ObjectUtil.defaultIfNull(detail.getCount(), 0L)));
}
return new ArrayList<>(aggregateMap.values());
}
/**
* 分批查询订单或售后业务号对应的钱包流水历史部门;远程失败时终止统计,禁止静默回退造成混合口径。
* 将支付和退款明细整理成受控查询组,并用一次或极少数几次 Wallet 调用查询全部流水历史部门。
*
* @param details 支付订单或退款售后明细
* @param bizType 钱包支付或退款业务类型
* @return 以订单 ID 或售后单 ID 字符串为键的流水历史部门
* <p>单个 SQL 查询组最多一万条,单次远程请求最多十组。常规报表只产生一次远程调用,
* 极端大数据量会自动拆分远程请求,避免请求体或 SQL 参数失控。远程失败时终止统计,
* 禁止静默回退造成历史部门和订单部门混合口径。</p>
*
* @param detailsByBizType 以钱包业务类型编码为键的支付订单或退款售后明细
* @return 第一层以业务类型编码为键、第二层以订单或售后单 ID 为键的钱包流水快照
* @throws IllegalStateException 钱包历史部门查询失败时抛出
*/
private Map<String, Long> queryTransactionDepartmentMap(
List<HardOrderDepartmentStatisticsDetail> details, WalletBizEnum bizType) {
List<HardOrderDepartmentStatisticsDetail> walletDetails = details.stream()
.filter(ObjectUtil::isNotEmpty)
.filter(detail -> ObjectUtil.isAllNotEmpty(detail.getBizId(), detail.getWalletId()))
.collect(Collectors.toMap(HardOrderDepartmentStatisticsDetail::getBizId,
detail -> detail, (first, ignored) -> first, LinkedHashMap::new))
.values().stream()
.toList();
Map<String, Long> expectedWalletMap = walletDetails.stream().collect(Collectors.toMap(
HardOrderDepartmentStatisticsDetail::getBizId,
HardOrderDepartmentStatisticsDetail::getWalletId));
Map<String, Long> resultMap = new LinkedHashMap<>();
for (int start = 0; start < walletDetails.size(); start += WalletTransactionDepartmentSnapshotDto.MAX_QUERY_BATCH_SIZE) {
int end = Math.min(start + WalletTransactionDepartmentSnapshotDto.MAX_QUERY_BATCH_SIZE, walletDetails.size());
List<HardOrderDepartmentStatisticsDetail> batchDetails = walletDetails.subList(start, end);
WalletTransactionDepartmentSnapshotDto snapshotDto = new WalletTransactionDepartmentSnapshotDto();
snapshotDto.setBizType(bizType.getCode());
snapshotDto.setBizIds(batchDetails.stream().map(HardOrderDepartmentStatisticsDetail::getBizId).toList());
snapshotDto.setWalletIds(batchDetails.stream().map(HardOrderDepartmentStatisticsDetail::getWalletId)
.distinct().toList());
private Map<String, Map<String, WalletTransactionDepartmentSnapshotVo>> queryTransactionDepartmentSnapshotMaps(
Map<String, List<HardOrderDepartmentStatisticsDetail>> detailsByBizType) {
Map<String, Map<String, Long>> expectedWalletMaps = new LinkedHashMap<>();
Map<String, Map<String, WalletTransactionDepartmentSnapshotVo>> resultMaps = new LinkedHashMap<>();
List<WalletTransactionDepartmentSnapshotDto> queryGroups = new ArrayList<>();
for (Map.Entry<String, List<HardOrderDepartmentStatisticsDetail>> entry : detailsByBizType.entrySet()) {
List<HardOrderDepartmentStatisticsDetail> sourceDetails =
entry.getValue() == null ? List.of() : entry.getValue();
List<HardOrderDepartmentStatisticsDetail> walletDetails = sourceDetails.stream()
.filter(ObjectUtil::isNotEmpty)
.filter(detail -> ObjectUtil.isAllNotEmpty(detail.getBizId(), detail.getWalletId()))
.collect(Collectors.toMap(HardOrderDepartmentStatisticsDetail::getBizId,
detail -> detail, (first, ignored) -> first, LinkedHashMap::new))
.values().stream()
.toList();
Map<String, Long> expectedWalletMap = walletDetails.stream().collect(Collectors.toMap(
HardOrderDepartmentStatisticsDetail::getBizId,
HardOrderDepartmentStatisticsDetail::getWalletId,
(first, ignored) -> first, LinkedHashMap::new));
expectedWalletMaps.put(entry.getKey(), expectedWalletMap);
resultMaps.put(entry.getKey(), new LinkedHashMap<>());
for (int start = 0; start < walletDetails.size();
start += WalletTransactionDepartmentSnapshotDto.MAX_QUERY_BATCH_SIZE) {
int end = Math.min(start + WalletTransactionDepartmentSnapshotDto.MAX_QUERY_BATCH_SIZE,
walletDetails.size());
List<HardOrderDepartmentStatisticsDetail> batchDetails = walletDetails.subList(start, end);
WalletTransactionDepartmentSnapshotDto snapshotDto = new WalletTransactionDepartmentSnapshotDto();
snapshotDto.setBizType(entry.getKey());
snapshotDto.setBizIds(batchDetails.stream()
.map(HardOrderDepartmentStatisticsDetail::getBizId).toList());
snapshotDto.setWalletIds(batchDetails.stream()
.map(HardOrderDepartmentStatisticsDetail::getWalletId).distinct().toList());
queryGroups.add(snapshotDto);
}
}
for (int start = 0; start < queryGroups.size();
start += WalletTransactionDepartmentSnapshotBatchDto.MAX_QUERY_GROUP_SIZE) {
int end = Math.min(start + WalletTransactionDepartmentSnapshotBatchDto.MAX_QUERY_GROUP_SIZE,
queryGroups.size());
WalletTransactionDepartmentSnapshotBatchDto batchDto =
new WalletTransactionDepartmentSnapshotBatchDto();
batchDto.setQueries(new ArrayList<>(queryGroups.subList(start, end)));
R<List<WalletTransactionDepartmentSnapshotVo>> response =
remoteWalletTransactionService.listDepartmentSnapshots(snapshotDto);
remoteWalletTransactionService.listDepartmentSnapshotsBatch(batchDto);
if (response == null || !response.isOk()) {
throw new IllegalStateException("查询钱包流水历史部门失败");
}
@@ -646,14 +690,19 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
}
for (WalletTransactionDepartmentSnapshotVo snapshot : response.getData()) {
if (snapshot != null && ObjectUtil.isAllNotEmpty(
snapshot.getBizId(), snapshot.getWalletId(), snapshot.getDeptId())
&& ObjectUtil.equals(expectedWalletMap.get(snapshot.getBizId()), snapshot.getWalletId())) {
snapshot.getBizType(), snapshot.getBizId(), snapshot.getWalletId(), snapshot.getDeptId())) {
Map<String, Long> expectedWalletMap = expectedWalletMaps.get(snapshot.getBizType());
Map<String, WalletTransactionDepartmentSnapshotVo> resultMap = resultMaps.get(snapshot.getBizType());
if (expectedWalletMap == null || resultMap == null
|| !ObjectUtil.equals(expectedWalletMap.get(snapshot.getBizId()), snapshot.getWalletId())) {
continue;
}
// 钱包服务按流水主键倒序返回,异常重复业务号保留最新一笔历史快照。
resultMap.putIfAbsent(snapshot.getBizId(), snapshot.getDeptId());
resultMap.putIfAbsent(snapshot.getBizId(), snapshot);
}
}
}
return resultMap;
return resultMaps;
}
/**
@@ -11,6 +11,7 @@ import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper;
import com.bsd.pjcloud.hard.mapper.order.model.HardOrderDepartmentStatisticsDetail;
import com.bsd.pjcloud.hard.service.HardManagerService;
import com.bsd.pjcloud.wallet.api.constant.WalletBizEnum;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotBatchDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotDto;
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
import com.bsd.pjcloud.wallet.api.feign.RemoteWalletTransactionService;
@@ -27,6 +28,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -74,7 +76,7 @@ class HardOrderTotalServiceImplTest {
}
/**
* 验证排除测试部门时以钱包流水部门为准,并保留没有钱包流水的非钱包交易订单部门快照。
* 验证排除测试部门时以钱包流水部门为准:支付保留非钱包订单快照,退款只统计真实 REFUND 流水。
*/
@Test
void pageListDeptFiltersAfterApplyingTransactionDepartmentSnapshot() {
@@ -84,16 +86,17 @@ class HardOrderTotalServiceImplTest {
detail("102", 12L, 1L, 200L),
detail("103", null, 1L, 50L)));
when(hardOrderMapper.listRefundStatisticsDetails(any(HardOrderSumAndCountDto.class))).thenReturn(List.of(
detail("201", 21L, 9L, 30L),
detail("202", 22L, 1L, 40L),
detail("101", 21L, 9L, 35L),
// 同一订单分批退款会产生不同售后单 ID,必须各自计入金额和次数。
detail("104", 21L, 9L, 25L),
detail("102", 22L, 1L, 40L),
detail("203", null, 1L, 10L)));
when(remoteWalletTransactionService.listDepartmentSnapshots(any())).thenAnswer(invocation -> {
WalletTransactionDepartmentSnapshotDto query = invocation.getArgument(0);
if (WalletBizEnum.PAYMENT.getCode().equals(query.getBizType())) {
return R.ok(List.of(snapshot("101", 11L, 1L), snapshot("102", 12L, 9L)));
}
return R.ok(List.of(snapshot("201", 21L, 1L), snapshot("202", 22L, 9L)));
});
when(remoteWalletTransactionService.listDepartmentSnapshotsBatch(any())).thenReturn(R.ok(List.of(
snapshot(WalletBizEnum.PAYMENT.getCode(), "101", 11L, 1L, -100L),
snapshot(WalletBizEnum.PAYMENT.getCode(), "102", 12L, 9L, -200L),
snapshot(WalletBizEnum.REFUND.getCode(), "101", 21L, 1L, 30L),
snapshot(WalletBizEnum.REFUND.getCode(), "104", 21L, 1L, 20L),
snapshot(WalletBizEnum.REFUND.getCode(), "102", 22L, 9L, 40L))));
HardDeptTotalDto query = new HardDeptTotalDto();
query.setDeptIds(List.of(9L));
@@ -105,18 +108,23 @@ class HardOrderTotalServiceImplTest {
HardDeptTotalVo normalDepartment = result.get(0);
assertEquals(BigDecimal.valueOf(150L), normalDepartment.getOrderPayPrice());
assertEquals(2L, normalDepartment.getOrderPayCount());
assertEquals(BigDecimal.valueOf(40L), normalDepartment.getOrderRefundPrice());
assertEquals(BigDecimal.valueOf(50L), normalDepartment.getOrderRefundPrice());
assertEquals(2L, normalDepartment.getOrderRefundCont());
assertEquals(BigDecimal.valueOf(110L), normalDepartment.getOrderProfitPrice());
assertEquals(BigDecimal.valueOf(100L), normalDepartment.getOrderProfitPrice());
assertEquals(0L, normalDepartment.getOrderProfitCont());
ArgumentCaptor<WalletTransactionDepartmentSnapshotDto> captor =
ArgumentCaptor.forClass(WalletTransactionDepartmentSnapshotDto.class);
verify(remoteWalletTransactionService, org.mockito.Mockito.times(2)).listDepartmentSnapshots(captor.capture());
assertEquals(List.of("101", "102"), captor.getAllValues().get(0).getBizIds());
assertEquals(List.of(11L, 12L), captor.getAllValues().get(0).getWalletIds());
assertEquals(List.of("201", "202"), captor.getAllValues().get(1).getBizIds());
assertEquals(List.of(21L, 22L), captor.getAllValues().get(1).getWalletIds());
ArgumentCaptor<WalletTransactionDepartmentSnapshotBatchDto> captor =
ArgumentCaptor.forClass(WalletTransactionDepartmentSnapshotBatchDto.class);
verify(remoteWalletTransactionService).listDepartmentSnapshotsBatch(captor.capture());
verify(remoteWalletTransactionService, never()).listDepartmentSnapshots(any());
List<WalletTransactionDepartmentSnapshotDto> queryGroups = captor.getValue().getQueries();
assertEquals(2, queryGroups.size());
assertEquals(WalletBizEnum.PAYMENT.getCode(), queryGroups.get(0).getBizType());
assertEquals(List.of("101", "102"), queryGroups.get(0).getBizIds());
assertEquals(List.of(11L, 12L), queryGroups.get(0).getWalletIds());
assertEquals(WalletBizEnum.REFUND.getCode(), queryGroups.get(1).getBizType());
assertEquals(List.of("101", "104", "102"), queryGroups.get(1).getBizIds());
assertEquals(List.of(21L, 22L), queryGroups.get(1).getWalletIds());
}
/**
@@ -141,16 +149,21 @@ class HardOrderTotalServiceImplTest {
/**
* 创建钱包流水发生时固化的部门快照。
*
* @param bizType 钱包流水业务类型持久化编码
* @param bizId 订单 ID 或售后单 ID 字符串
* @param walletId 产生流水的钱包 ID
* @param departmentId 流水创建时部门 ID
* @param price 钱包流水实际发生金额,单位分
* @return 钱包流水历史部门快照
*/
private WalletTransactionDepartmentSnapshotVo snapshot(String bizId, Long walletId, Long departmentId) {
private WalletTransactionDepartmentSnapshotVo snapshot(
String bizType, String bizId, Long walletId, Long departmentId, Long price) {
WalletTransactionDepartmentSnapshotVo snapshot = new WalletTransactionDepartmentSnapshotVo();
snapshot.setBizType(bizType);
snapshot.setBizId(bizId);
snapshot.setWalletId(walletId);
snapshot.setDeptId(departmentId);
snapshot.setPrice(price);
return snapshot;
}
@@ -0,0 +1,28 @@
package com.bsd.pjcloud.wallet.api.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.List;
/**
* 钱包流水历史部门快照批量查询条件,用一次跨服务请求承载多个受控 SQL 查询组。
*/
@Data
public class WalletTransactionDepartmentSnapshotBatchDto {
/**
* 单次远程请求允许携带的最大查询组数量,限制请求体和钱包服务单线程数据库占用时长。
*/
public static final int MAX_QUERY_GROUP_SIZE = 10;
/**
* 按业务类型和 SQL 参数容量拆分后的查询组,组内条件继续沿用单组校验规则。
*/
@Valid
@NotEmpty(message = "钱包流水历史部门查询组不能为空")
@Size(max = MAX_QUERY_GROUP_SIZE, message = "单次查询的钱包流水历史部门查询组不能超过10个")
private List<WalletTransactionDepartmentSnapshotDto> queries;
}
@@ -13,20 +13,22 @@ import java.util.List;
@Data
public class WalletTransactionDepartmentSnapshotDto {
/** 单次内部查询允许携带的最大业务号或钱包数量。 */
public static final int MAX_QUERY_BATCH_SIZE = 500;
/**
* 单个 SQL 查询组允许携带的最大业务号或钱包数量;两组 IN 参数合计仍低于 MySQL 预编译参数上限。
*/
public static final int MAX_QUERY_BATCH_SIZE = 10_000;
/** 钱包流水业务类型持久化编码,例如消费 PAYMENT 或退款 REFUND。 */
@NotBlank(message = "钱包流水业务类型不能为空")
private String bizType;
/** 待查询的钱包流水业务号集合;调用方应分批传入,避免生成超长 SQL。 */
/** 待查询的钱包流水业务号集合;调用方应按单组上限拆分,避免超过数据库参数容量。 */
@NotEmpty(message = "钱包流水业务号不能为空")
@Size(max = MAX_QUERY_BATCH_SIZE, message = "单次查询的钱包流水业务号不能超过500个")
@Size(max = MAX_QUERY_BATCH_SIZE, message = "单组查询的钱包流水业务号不能超过10000个")
private List<String> bizIds;
/** 业务号对应的候选钱包 ID 集合,用于命中钱包流水现有索引并缩小扫描范围。 */
@NotEmpty(message = "钱包ID不能为空")
@Size(max = MAX_QUERY_BATCH_SIZE, message = "单次查询的钱包ID不能超过500个")
@Size(max = MAX_QUERY_BATCH_SIZE, message = "单组查询的钱包ID不能超过10000个")
private List<Long> walletIds;
}
@@ -4,6 +4,7 @@ import com.bsd.pjcloud.common.core.constant.ServiceNameConstants;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.feign.annotation.NoToken;
import com.bsd.pjcloud.wallet.api.dto.WalletBalanceTransactionDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotBatchDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionSumAndCountDto;
import com.bsd.pjcloud.wallet.api.entity.WalletTransactionEntity;
@@ -76,4 +77,15 @@ public interface RemoteWalletTransactionService {
R<List<WalletTransactionDepartmentSnapshotVo>> listDepartmentSnapshots(
@RequestBody WalletTransactionDepartmentSnapshotDto dto);
/**
* 用一次远程请求查询多个支付或退款业务号分组的流水历史部门快照。
*
* @param dto 按业务类型和数据库参数容量拆分后的查询组
* @return 所有查询组命中的历史部门快照,业务类型用于调用方隔离同值业务号
*/
@NoToken
@PostMapping("/wallet-transaction/department-snapshots-batch")
R<List<WalletTransactionDepartmentSnapshotVo>> listDepartmentSnapshotsBatch(
@RequestBody WalletTransactionDepartmentSnapshotBatchDto dto);
}
@@ -8,6 +8,9 @@ import lombok.Data;
@Data
public class WalletTransactionDepartmentSnapshotVo {
/** 钱包流水业务类型持久化编码,用于批量响应区分支付与退款业务号。 */
private String bizType;
/** 钱包流水关联的稳定业务号;消费对应订单 ID,订单退款对应售后单 ID。 */
private String bizId;
@@ -16,4 +19,7 @@ public class WalletTransactionDepartmentSnapshotVo {
/** 流水创建时的钱包所属部门 ID;历史异常流水可能为空。 */
private Long deptId;
/** 钱包流水实际发生金额,单位分;退款统计以该值为准,避免混入没有资金流水的售后记录。 */
private Long price;
}
@@ -15,6 +15,7 @@ import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.wallet.api.constant.WalletBizEnum;
import com.bsd.pjcloud.wallet.api.dto.WalletBalanceTransactionDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotBatchDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionRemarkDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionSumAndCountDto;
@@ -146,6 +147,19 @@ public class WalletTransactionController {
@Valid @RequestBody WalletTransactionDepartmentSnapshotDto dto) {
return R.ok(walletTransactionMapper.listDepartmentSnapshots(dto));
}
/**
* 用一次内部服务请求查询多个支付或退款分组的流水历史部门快照。
*
* @param dto 已按业务类型和数据库参数容量拆分的查询组
* @return 所有查询组命中的历史部门快照
*/
@Inner
@PostMapping("/department-snapshots-batch")
public R<List<WalletTransactionDepartmentSnapshotVo>> listDepartmentSnapshotsBatch(
@Valid @RequestBody WalletTransactionDepartmentSnapshotBatchDto dto) {
return R.ok(walletTransactionService.listDepartmentSnapshotsBatch(dto));
}
/* ************************************* end 远程调用 end ************************************* */
/* ************************************* start app start ************************************* */
@@ -5,10 +5,12 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.common.data.datascope.DataScope;
import com.bsd.pjcloud.wallet.api.constant.WalletBizEnum;
import com.bsd.pjcloud.wallet.api.dto.WalletBalanceTransactionDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotBatchDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionRemarkDto;
import com.bsd.pjcloud.wallet.api.entity.WalletTransactionEntity;
import com.bsd.pjcloud.wallet.api.vo.WalletCzTotalVo;
import com.bsd.pjcloud.wallet.api.vo.WalletTransactionDepartmentSnapshotVo;
import com.bsd.pjcloud.wallet.api.vo.WalletTypeTotalVo;
import com.bsd.pjcloud.wallet.api.vo.transaction.WalletTransactionVo;
import jakarta.servlet.http.HttpServletResponse;
@@ -17,6 +19,15 @@ import java.util.List;
public interface WalletTransactionService extends IService<WalletTransactionEntity> {
/**
* 在钱包服务内部依次执行受控查询组并合并历史部门快照,避免调用方逐组发起远程请求。
*
* @param dto 按业务类型和数据库参数容量拆分后的查询组
* @return 所有查询组命中的历史部门快照,保留组内流水主键倒序
*/
List<WalletTransactionDepartmentSnapshotVo> listDepartmentSnapshotsBatch(
WalletTransactionDepartmentSnapshotBatchDto dto);
/**
* 将钱包流水身份条件转换为最终用户主键集合,供统计、分页和导出共用。
*
@@ -35,6 +35,8 @@ import com.bsd.pjcloud.wallet.api.constant.WalletTypeEnum;
import com.bsd.pjcloud.wallet.api.dto.MchManageDto;
import com.bsd.pjcloud.wallet.api.dto.WalletBalanceTransactionDto;
import com.bsd.pjcloud.wallet.api.dto.WalletManageDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotBatchDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionRemarkDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTypeDto;
@@ -155,6 +157,29 @@ public class WalletTransactionServiceImpl extends ServiceImpl<WalletTransactionM
private final Redisson redisson;
/**
* 在一次钱包服务调用中依次执行受控历史部门查询组,并合并支付、退款快照结果。
*
* <p>分组边界留在钱包服务内部,Hardware 不再为每一万条明细承担一次串行网络往返;
* 每组仍使用现有 wallet_id 索引缩小扫描范围,且不会生成超过数据库参数容量的 SQL。</p>
*
* @param dto 按业务类型和数据库参数容量拆分后的查询组
* @return 所有查询组命中的流水历史部门快照
*/
@Override
public List<WalletTransactionDepartmentSnapshotVo> listDepartmentSnapshotsBatch(
WalletTransactionDepartmentSnapshotBatchDto dto) {
List<WalletTransactionDepartmentSnapshotVo> result = new ArrayList<>();
for (WalletTransactionDepartmentSnapshotDto query : dto.getQueries()) {
List<WalletTransactionDepartmentSnapshotVo> snapshots = baseMapper.listDepartmentSnapshots(query);
if (ObjectUtil.isNotEmpty(snapshots)) {
result.addAll(snapshots);
}
}
return result;
}
/**
* 保存新钱包流水并在同一本地事务内追加业务消息Outbox;幂等查询旧流水不会进入本方法。
*
@@ -216,9 +216,11 @@
<select id="listDepartmentSnapshots"
resultType="com.bsd.pjcloud.wallet.api.vo.WalletTransactionDepartmentSnapshotVo">
select a.biz_id as bizId,
select a.biz_type as bizType,
a.biz_id as bizId,
a.wallet_id as walletId,
a.dept_id as deptId
a.dept_id as deptId,
a.price as price
from wallet_transaction a
where a.del_flag = '0'
and a.dept_id is not null
@@ -6,10 +6,13 @@ import com.bsd.pjcloud.admin.api.feign.RemoteUserRelationService;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.wallet.api.constant.WalletBizEnum;
import com.bsd.pjcloud.wallet.api.constant.WalletTypeEnum;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotBatchDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDto;
import com.bsd.pjcloud.wallet.api.entity.WalletManageEntity;
import com.bsd.pjcloud.wallet.api.entity.WalletMessageOutboxEntity;
import com.bsd.pjcloud.wallet.api.entity.WalletTransactionEntity;
import com.bsd.pjcloud.wallet.api.vo.WalletTransactionDepartmentSnapshotVo;
import com.bsd.pjcloud.wallet.api.vo.transaction.WalletTransactionVo;
import com.bsd.pjcloud.wallet.mapper.WalletManageMapper;
import com.bsd.pjcloud.wallet.mapper.WalletMessageOutboxMapper;
@@ -149,6 +152,31 @@ class WalletTransactionServiceImplTest {
verify(walletManageService, never()).getWallet(any(), any(WalletTypeEnum.class));
}
/**
* 批量历史部门查询应在 Wallet 内依次执行各业务类型查询组并合并响应。
*/
@Test
void departmentSnapshotBatchShouldMergePaymentAndRefundGroups() {
WalletTransactionDepartmentSnapshotDto paymentQuery = snapshotQuery(
WalletBizEnum.PAYMENT.getCode(), "101", 11L);
WalletTransactionDepartmentSnapshotDto refundQuery = snapshotQuery(
WalletBizEnum.REFUND.getCode(), "101", 21L);
WalletTransactionDepartmentSnapshotVo paymentSnapshot = snapshot(
WalletBizEnum.PAYMENT.getCode(), "101", 11L, 1L, -100L);
WalletTransactionDepartmentSnapshotVo refundSnapshot = snapshot(
WalletBizEnum.REFUND.getCode(), "101", 21L, 9L, 100L);
when(walletTransactionMapper.listDepartmentSnapshots(paymentQuery)).thenReturn(List.of(paymentSnapshot));
when(walletTransactionMapper.listDepartmentSnapshots(refundQuery)).thenReturn(List.of(refundSnapshot));
WalletTransactionDepartmentSnapshotBatchDto batchDto = new WalletTransactionDepartmentSnapshotBatchDto();
batchDto.setQueries(List.of(paymentQuery, refundQuery));
List<WalletTransactionDepartmentSnapshotVo> result = service.listDepartmentSnapshotsBatch(batchDto);
assertEquals(List.of(paymentSnapshot, refundSnapshot), result);
verify(walletTransactionMapper).listDepartmentSnapshots(paymentQuery);
verify(walletTransactionMapper).listDepartmentSnapshots(refundQuery);
}
/**
* 首次幂等入账应锁定钱包、增加一次余额并保存稳定业务号流水。
*/
@@ -214,6 +242,43 @@ class WalletTransactionServiceImplTest {
return wallet;
}
/**
* 创建一组按业务类型、业务号和钱包 ID 查询的历史部门条件。
*
* @param bizType 钱包流水业务类型持久化编码
* @param bizId 订单或售后单业务号
* @param walletId 产生流水的钱包 ID
* @return 单个受控 SQL 查询组
*/
private WalletTransactionDepartmentSnapshotDto snapshotQuery(String bizType, String bizId, Long walletId) {
WalletTransactionDepartmentSnapshotDto query = new WalletTransactionDepartmentSnapshotDto();
query.setBizType(bizType);
query.setBizIds(List.of(bizId));
query.setWalletIds(List.of(walletId));
return query;
}
/**
* 创建一条钱包流水发生时固化的部门快照。
*
* @param bizType 钱包流水业务类型持久化编码
* @param bizId 订单或售后单业务号
* @param walletId 产生流水的钱包 ID
* @param departmentId 流水发生时部门 ID
* @param price 钱包流水实际发生金额,单位分
* @return 钱包流水历史部门快照
*/
private WalletTransactionDepartmentSnapshotVo snapshot(
String bizType, String bizId, Long walletId, Long departmentId, Long price) {
WalletTransactionDepartmentSnapshotVo snapshot = new WalletTransactionDepartmentSnapshotVo();
snapshot.setBizType(bizType);
snapshot.setBizId(bizId);
snapshot.setWalletId(walletId);
snapshot.setDeptId(departmentId);
snapshot.setPrice(price);
return snapshot;
}
/**
* 构造带越权用户与钱包筛选的请求,用于验证服务端会覆盖敏感范围。
*