feat:余额排序
This commit is contained in:
1 parent
94a0cd5f92
commit
886d23e3db
6 files changed
+564
-53
No files matched your search
+25
-2
@@ -4,6 +4,7 @@ import cn.hutool.core.util.ObjectUtil;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.core.util.BeanUtils;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
@@ -164,6 +165,27 @@ public interface WalletManageMapper extends PjcloudBaseMapper<WalletManageEntity
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 复用钱包筛选与数据权限,查询全量月末余额排序后的当前页;调用方独立统计总数。 */
|
||||
default Page<WalletManageVo> scopeQbBalancePage(Page<WalletManageVo> page, WalletManageDto dto, LocalDateTime monthEndExclusive, DataScope dataScope) {
|
||||
LambdaQueryWrapper<WalletManageEntity> wrapper = commonQuery(dto);
|
||||
// 排序由分页插件在权限查询外层追加,避免默认钱包顺序覆盖余额顺序。
|
||||
wrapper.getExpression().getOrderBy().clear();
|
||||
return selectQbBalancePage(page, dto, monthEndExclusive, wrapper, dataScope);
|
||||
}
|
||||
|
||||
/** 查询每个钱包唯一的月末余额;仅接收服务端生成的筛选包装器及可信分页排序。 */
|
||||
Page<WalletManageVo> selectQbBalancePage(Page<WalletManageVo> page, @Param("dto") WalletManageDto dto, @Param("monthEndExclusive") LocalDateTime monthEndExclusive, @Param(Constants.WRAPPER) LambdaQueryWrapper<WalletManageEntity> wrapper, @Param("dataScope") DataScope dataScope);
|
||||
|
||||
/** 钱包数量独立计数:仅查钱包字段供权限拦截器包装 COUNT,不执行流水计算或排序。 */
|
||||
default Long countQbWallets(WalletManageDto dto, DataScope dataScope) {
|
||||
LambdaQueryWrapper<WalletManageEntity> wrapper = commonQuery(dto).eq(WalletManageEntity::getDelFlag, "0");
|
||||
wrapper.getExpression().getOrderBy().clear();
|
||||
return selectQbWalletCount(wrapper, dataScope);
|
||||
}
|
||||
|
||||
/** 返回钱包筛选及 COUNT 数据权限范围内的总数;dataScope 必须使用 COUNT 模式。 */
|
||||
Long selectQbWalletCount(@Param(Constants.WRAPPER) LambdaQueryWrapper<WalletManageEntity> wrapper, @Param("dataScope") DataScope dataScope);
|
||||
|
||||
default Long count(WalletManageDto dto, DataScope dataScope) {
|
||||
return selectCountByScope(commonQuery(dto), dataScope);
|
||||
}
|
||||
@@ -177,10 +199,11 @@ public interface WalletManageMapper extends PjcloudBaseMapper<WalletManageEntity
|
||||
|
||||
List<Long> scopeUserList(@Param("dto") WalletManageDto dto, @Param("dataScope") DataScope dataScope);
|
||||
|
||||
List<WalletManageBalanceVo> selectBalance(@Param("dto") WalletManageBalanceDto walletDto);
|
||||
/** 查询月界之前最新未删除流水,同秒取最大主键;每个用户与钱包类型最多返回一行。 */
|
||||
List<WalletManageBalanceVo> selectBalance(@Param("dto") WalletManageBalanceDto walletDto, @Param("monthEndExclusive") LocalDateTime monthEndExclusive);
|
||||
|
||||
/** 复用历史余额查询口径,流水结果也按固定批大小读取。 */
|
||||
Page<WalletManageBalanceVo> selectBalanceExportPage(Page page, @Param("dto") WalletManageBalanceDto walletDto);
|
||||
Page<WalletManageBalanceVo> selectBalanceExportPage(Page page, @Param("dto") WalletManageBalanceDto walletDto, @Param("monthEndExclusive") LocalDateTime monthEndExclusive);
|
||||
|
||||
Page<WalletManageVo> activeBalancePage(Page page, @Param("dto") WalletTopupActionDto dto, @Param("unactiveDayNumberStr") String unactiveDayNumberStr);
|
||||
|
||||
|
||||
+70
-5
@@ -7,6 +7,7 @@ import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.admin.api.dto.UserInfo;
|
||||
@@ -24,6 +25,7 @@ import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.asynctask.service.AsyncTaskException;
|
||||
import com.bsd.pjcloud.common.asynctask.service.BatchExportSupport;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScopeFuncEnum;
|
||||
import com.bsd.pjcloud.common.data.resolver.ParamResolver;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
|
||||
import com.bsd.pjcloud.wallet.api.constant.WalletTypeEnum;
|
||||
@@ -309,11 +311,18 @@ public class WalletManageServiceImpl extends ServiceImpl<WalletManageMapper, Wal
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 按月末余额全量排序时先在数据库排序分页,普通查询仍只计算当前页余额。 */
|
||||
@Override
|
||||
public Page<WalletManageVo> scopeQbPage(Page page, WalletManageBalanceDto dto, DataScope dataScope) {
|
||||
LocalDateTime monthEndExclusive = walletRecordMonthEnd(dto.getWalletRecordMonth());
|
||||
List<OrderItem> orders = page.orders();
|
||||
Optional<OrderItem> balanceOrder = orders.stream().filter(order -> "balance".equals(order.getColumn())).findFirst();
|
||||
if (balanceOrder.isPresent()) {
|
||||
return queryWalletRecordBalancePage(page, dto, dataScope, monthEndExclusive, balanceOrder.get().isAsc());
|
||||
}
|
||||
WalletManageDto walletDto = BeanUtils.toBean(dto, WalletManageDto.class);
|
||||
Page<WalletManageVo> walletPage = baseMapper.scopePage(page, walletDto, dataScope);
|
||||
if (walletPage.getTotal() == 0) return walletPage;
|
||||
if (ObjectUtil.isEmpty(walletPage.getRecords())) return walletPage;
|
||||
walletPage.getRecords().forEach(v -> {
|
||||
v.setBalance(0);
|
||||
v.setTotalRecharge(0);
|
||||
@@ -332,10 +341,10 @@ public class WalletManageServiceImpl extends ServiceImpl<WalletManageMapper, Wal
|
||||
if (ObjectUtil.isEmpty(walletBalanceDto.getUserId())) {
|
||||
walletBalanceDto.setUserIds(userIds);
|
||||
}
|
||||
List<WalletManageBalanceVo> itemList = baseMapper.selectBalance(walletBalanceDto);
|
||||
List<WalletManageBalanceVo> itemList = baseMapper.selectBalance(walletBalanceDto, monthEndExclusive);
|
||||
if (ObjectUtil.isEmpty(itemList)) continue;
|
||||
for (WalletManageBalanceVo j : itemList) {
|
||||
walletPage.getRecords().stream().filter(v -> ObjectUtil.equals(i.getId(), v.getWalletTypeId()) && ObjectUtil.equals(v.getUserId(), j.getUserId())).findFirst().ifPresent(v -> {
|
||||
walletPage.getRecords().stream().filter(v -> ObjectUtil.equals(i.getId(), v.getWalletTypeId()) && ObjectUtil.equals(v.getUserId(), j.getUserId())).forEach(v -> {
|
||||
if (ObjectUtil.isNotEmpty(j.getBalance())) {
|
||||
v.setBalance(j.getBalance());
|
||||
v.setLastTopUpTime(j.getCreateTime());
|
||||
@@ -347,8 +356,63 @@ public class WalletManageServiceImpl extends ServiceImpl<WalletManageMapper, Wal
|
||||
return walletPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 只对白名单余额字段排序,独立统计钱包数,避免分页插件再次计算全部历史流水。
|
||||
* 同额按钱包主键倒序;远程用户、部门资料仅补充数据库返回的当前页。
|
||||
*/
|
||||
private Page<WalletManageVo> queryWalletRecordBalancePage(Page<WalletManageVo> page, WalletManageBalanceDto dto, DataScope dataScope, LocalDateTime monthEndExclusive, boolean ascending) {
|
||||
if (page.getCurrent() < 1 || page.getSize() <= 0) {
|
||||
throw new IllegalArgumentException("余额排序必须使用有效页码和正数分页大小");
|
||||
}
|
||||
WalletManageDto walletDto = BeanUtils.toBean(dto, WalletManageDto.class);
|
||||
page.setOrders(new ArrayList<>(List.of(ascending ? OrderItem.asc("balance") : OrderItem.desc("balance"), OrderItem.desc("id"))));
|
||||
boolean searchCount = page.searchCount();
|
||||
DataScope queryScope = dataScope == null ? DataScope.of() : dataScope;
|
||||
if (searchCount) {
|
||||
// 权限计算可能扩展部门列表,计数使用独立副本,避免污染随后列表的数据范围。
|
||||
DataScope countScope = new DataScope();
|
||||
countScope.setScopeDeptName(queryScope.getScopeDeptName());
|
||||
countScope.setScopeUserName(queryScope.getScopeUserName());
|
||||
countScope.setDeptList(new ArrayList<>(queryScope.getDeptList()));
|
||||
countScope.setUsername(queryScope.getUsername());
|
||||
countScope.setIsOnly(queryScope.getIsOnly());
|
||||
countScope.setFunc(DataScopeFuncEnum.COUNT);
|
||||
page.setTotal(baseMapper.countQbWallets(walletDto, countScope));
|
||||
if (page.getTotal() == 0) {
|
||||
page.setRecords(Collections.emptyList());
|
||||
return page;
|
||||
}
|
||||
}
|
||||
try {
|
||||
page.setSearchCount(false);
|
||||
Page<WalletManageVo> result = baseMapper.scopeQbBalancePage(page, walletDto, monthEndExclusive, queryScope);
|
||||
disposeList(result.getRecords());
|
||||
return result;
|
||||
} finally {
|
||||
page.setSearchCount(searchCount);
|
||||
}
|
||||
}
|
||||
|
||||
/** 将有效 YYYY-MM 月份转为下月零点排他上界,让流水时间条件能够使用索引。 */
|
||||
private LocalDateTime walletRecordMonthEnd(String month) {
|
||||
if (month == null || !month.matches("[0-9]{4}-[0-9]{2}")) {
|
||||
throw new IllegalArgumentException("请选择有效月份(YYYY-MM)");
|
||||
}
|
||||
try {
|
||||
YearMonth recordMonth = YearMonth.parse(month);
|
||||
YearMonth nextMonth = recordMonth.plusMonths(1);
|
||||
if (recordMonth.getYear() < 1000 || nextMonth.getYear() > 9999) {
|
||||
throw new IllegalArgumentException("统计月份超出支持范围");
|
||||
}
|
||||
return nextMonth.atDay(1).atStartOfDay();
|
||||
} catch (DateTimeException exception) {
|
||||
throw new IllegalArgumentException("请选择有效月份(YYYY-MM)", exception);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Page<WalletManageVo> scopeQbExportPage(Page page, WalletManageBalanceDto dto, DataScope dataScope) {
|
||||
LocalDateTime monthEndExclusive = walletRecordMonthEnd(dto.getWalletRecordMonth());
|
||||
// 长导出任务的事务会保留 MyBatis 一级缓存,每批读取前释放上一批结果。
|
||||
sqlSession.clearCache();
|
||||
WalletManageDto walletQuery = BeanUtils.toBean(dto, WalletManageDto.class);
|
||||
@@ -376,7 +440,7 @@ public class WalletManageServiceImpl extends ServiceImpl<WalletManageMapper, Wal
|
||||
sqlSession.clearCache();
|
||||
Page<WalletManageBalanceVo> historyPage = BatchExportSupport.page(current);
|
||||
historyPage.setSearchCount(false);
|
||||
List<WalletManageBalanceVo> history = baseMapper.selectBalanceExportPage(historyPage, historyQuery).getRecords();
|
||||
List<WalletManageBalanceVo> history = baseMapper.selectBalanceExportPage(historyPage, historyQuery, monthEndExclusive).getRecords();
|
||||
for (WalletManageBalanceVo balance : history) {
|
||||
WalletManageVo target = targets.get(balance.getUserId());
|
||||
if (target != null && balance.getBalance() != null) {
|
||||
@@ -464,6 +528,7 @@ public class WalletManageServiceImpl extends ServiceImpl<WalletManageMapper, Wal
|
||||
|
||||
@Override
|
||||
public List<WalletManageVo> scopeQbList(WalletManageBalanceDto dto, DataScope dataScope) {
|
||||
LocalDateTime monthEndExclusive = walletRecordMonthEnd(dto.getWalletRecordMonth());
|
||||
WalletManageDto walletDto = BeanUtils.toBean(dto, WalletManageDto.class);
|
||||
List<WalletManageVo> walletList = baseMapper.scopeList(walletDto, dataScope);
|
||||
if (ObjectUtil.isEmpty(walletList)) return walletList;
|
||||
@@ -482,7 +547,7 @@ public class WalletManageServiceImpl extends ServiceImpl<WalletManageMapper, Wal
|
||||
if (ObjectUtil.isEmpty(dto.getUserId())) {
|
||||
dto.setUserIds(userIds);
|
||||
}
|
||||
List<WalletManageBalanceVo> itemList = baseMapper.selectBalance(dto);
|
||||
List<WalletManageBalanceVo> itemList = baseMapper.selectBalance(dto, monthEndExclusive);
|
||||
if (ObjectUtil.isEmpty(itemList)) continue;
|
||||
for (WalletManageBalanceVo j : itemList) {
|
||||
walletList.stream().filter(v -> ObjectUtil.equals(i.getId(), v.getWalletTypeId()) && ObjectUtil.equals(v.getUserId(), j.getUserId())).findFirst().ifPresent(v -> {
|
||||
|
||||
+85
-38
@@ -28,60 +28,107 @@
|
||||
<sql id="whereA">
|
||||
</sql>
|
||||
|
||||
<sql id="selectBalanceQuery">
|
||||
SELECT a.user_id,
|
||||
a.balance,
|
||||
a.wallet_id,
|
||||
m.wallet_type_id,
|
||||
a.create_time
|
||||
FROM wallet_transaction a
|
||||
LEFT JOIN wallet_manage m on a.wallet_id = m.id
|
||||
<where>
|
||||
a.del_flag = 0
|
||||
<if test="dto.walletRecordMonth != null">
|
||||
AND a.create_time = (
|
||||
SELECT MAX(sub.create_time)
|
||||
FROM wallet_transaction sub
|
||||
LEFT JOIN wallet_manage wallet on wallet.id = sub.wallet_id
|
||||
WHERE sub.user_id = a.user_id
|
||||
AND DATE_FORMAT(sub.create_time, '%Y-%m') <= #{dto.walletRecordMonth}
|
||||
<if test="dto.userId != null">
|
||||
AND sub.user_id = #{dto.userId}
|
||||
</if>
|
||||
<if test="dto.userIds != null and dto.userIds.size() != 0">
|
||||
and sub.user_id in
|
||||
<foreach collection="dto.userIds" item="v" open="(" close=")" separator=",">
|
||||
#{v}
|
||||
</foreach>
|
||||
</if>
|
||||
<if test="dto.walletTypeId != null">
|
||||
AND wallet.wallet_type_id = #{dto.walletTypeId}
|
||||
</if>
|
||||
)
|
||||
</if>
|
||||
<!--
|
||||
复用余额统计的候选收缩方案:先按钱包和历史所属用户取月界之前最大时间,
|
||||
同秒取最大 ID,再仅对候选行按用户、钱包类型去重,避免排序全部历史流水。
|
||||
历史钱包不限制删除状态,部门筛选留给外层当前钱包,沿用原列表取值口径。
|
||||
-->
|
||||
<sql id="qbLatestBalanceRows">
|
||||
SELECT a.user_id, a.wallet_id, m.wallet_type_id, a.balance, a.create_time,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY a.user_id, m.wallet_type_id
|
||||
ORDER BY a.create_time DESC, a.id DESC
|
||||
) AS row_num
|
||||
FROM (
|
||||
SELECT h.tenant_id, h.del_flag, h.wallet_id, h.user_id,
|
||||
MAX(h.create_time) AS latest_time
|
||||
FROM wallet_transaction h
|
||||
WHERE h.del_flag = '0'
|
||||
AND h.create_time < #{monthEndExclusive}
|
||||
<if test="dto.userId != null">
|
||||
AND a.user_id = #{dto.userId}
|
||||
AND h.user_id = #{dto.userId}
|
||||
</if>
|
||||
<if test="dto.userIds != null and dto.userIds.size() != 0">
|
||||
AND h.user_id IN
|
||||
<foreach collection="dto.userIds" item="userId" open="(" close=")" separator=",">
|
||||
#{userId}
|
||||
</foreach>
|
||||
</if>
|
||||
GROUP BY h.tenant_id, h.del_flag, h.wallet_id, h.user_id
|
||||
) latest
|
||||
INNER JOIN wallet_manage m ON m.id = latest.wallet_id
|
||||
INNER JOIN wallet_transaction a ON a.id = (
|
||||
SELECT MAX(last.id)
|
||||
FROM wallet_transaction last
|
||||
WHERE (last.tenant_id = latest.tenant_id
|
||||
OR (last.tenant_id IS NULL AND latest.tenant_id IS NULL))
|
||||
AND last.del_flag = latest.del_flag
|
||||
AND last.wallet_id = latest.wallet_id
|
||||
AND last.user_id = latest.user_id
|
||||
AND last.create_time = latest.latest_time
|
||||
)
|
||||
<where>
|
||||
<if test="dto.walletTypeId != null">
|
||||
AND m.wallet_type_id = #{dto.walletTypeId}
|
||||
</if>
|
||||
<if test="dto.userIds != null and dto.userIds.size() != 0">
|
||||
and a.user_id in
|
||||
<foreach collection="dto.userIds" item="v" open="(" close=")" separator=",">
|
||||
#{v}
|
||||
<if test="dto.walletTypeIdList != null and dto.walletTypeIdList.size() != 0">
|
||||
AND m.wallet_type_id IN
|
||||
<foreach collection="dto.walletTypeIdList" item="walletTypeId" open="(" close=")" separator=",">
|
||||
#{walletTypeId}
|
||||
</foreach>
|
||||
</if>
|
||||
</where>
|
||||
</sql>
|
||||
|
||||
<!-- 权限拦截器在外层统一 COUNT,内层须保留部门和创建人,不能先执行一次 COUNT。 -->
|
||||
<select id="selectQbWalletCount" resultType="java.lang.Long">
|
||||
SELECT wallet.id, wallet.dept_id, wallet.create_by
|
||||
FROM wallet_manage wallet
|
||||
${ew.customSqlSegment}
|
||||
</select>
|
||||
|
||||
<!-- 月末余额先连接到全部匹配钱包,分页插件在租户及数据权限处理之后排序、分页。 -->
|
||||
<select id="selectQbBalancePage" resultType="com.bsd.pjcloud.wallet.api.vo.WalletManageVo">
|
||||
SELECT monthly_wallet.*
|
||||
FROM (
|
||||
SELECT wallet.id, wallet.user_id, wallet.wallet_type_id, wallet.dept_id,
|
||||
wallet.card_number, COALESCE(latest.balance, 0) AS balance,
|
||||
0 AS total_expense, 0 AS total_recharge,
|
||||
wallet.freeze_price, wallet.is_freeze, wallet.active_bind_time,
|
||||
wallet.status, wallet.version, wallet.create_by, wallet.create_time,
|
||||
wallet.update_by, wallet.update_time, wallet.del_flag, wallet.tenant_id,
|
||||
CASE WHEN latest.balance IS NOT NULL THEN latest.create_time END AS last_top_up_time
|
||||
FROM wallet_manage wallet
|
||||
<!-- 普通列表只计算有效类型的余额;缺失或已删除类型的钱包同样保留零余额。 -->
|
||||
LEFT JOIN wallet_type available_type ON available_type.id = wallet.wallet_type_id
|
||||
AND available_type.del_flag = '0'
|
||||
LEFT JOIN (
|
||||
<include refid="qbLatestBalanceRows"/>
|
||||
) latest ON latest.user_id = wallet.user_id
|
||||
AND latest.wallet_type_id = available_type.id
|
||||
AND latest.row_num = 1
|
||||
WHERE wallet.del_flag = '0'
|
||||
) monthly_wallet
|
||||
${ew.customSqlSegment}
|
||||
</select>
|
||||
|
||||
<!-- 列表与导出共享唯一的最新有效流水,防止切换排序时同一钱包的显示余额变化。 -->
|
||||
<sql id="selectBalanceQuery">
|
||||
SELECT latest.user_id, latest.balance, latest.wallet_id, latest.wallet_type_id, latest.create_time
|
||||
FROM (
|
||||
<include refid="qbLatestBalanceRows"/>
|
||||
) latest
|
||||
WHERE latest.row_num = 1
|
||||
</sql>
|
||||
|
||||
<select id="selectBalance" resultType="com.bsd.pjcloud.wallet.api.vo.WalletManageBalanceVo">
|
||||
<include refid="selectBalanceQuery"/>
|
||||
ORDER BY a.user_id;
|
||||
ORDER BY latest.user_id, latest.wallet_type_id
|
||||
</select>
|
||||
|
||||
<select id="selectBalanceExportPage" resultType="com.bsd.pjcloud.wallet.api.vo.WalletManageBalanceVo">
|
||||
<include refid="selectBalanceQuery"/>
|
||||
ORDER BY a.user_id, a.id
|
||||
ORDER BY latest.user_id, latest.wallet_type_id
|
||||
</select>
|
||||
|
||||
<sql id="scopeUser">
|
||||
|
||||
+8
-8
@@ -165,7 +165,7 @@ class WalletLedgerBatchExportTest {
|
||||
List<WalletManageVo> rows = LongStream.range(from, Math.min(from + page.getSize(), 202)).mapToObj(WalletLedgerBatchExportTest::currentWallet).toList();
|
||||
return new Page<WalletManageVo>(page.getCurrent(), page.getSize()).setRecords(rows).setTotal(page.getCurrent() == 1 ? 202 : 0);
|
||||
});
|
||||
when(mapper.selectBalanceExportPage(any(), any())).thenAnswer(call -> {
|
||||
when(mapper.selectBalanceExportPage(any(), any(), any())).thenAnswer(call -> {
|
||||
Page<WalletManageBalanceVo> page = call.getArgument(0); checkPage(page, false);
|
||||
WalletManageBalanceDto query = call.getArgument(1);
|
||||
assertEquals("2026-09", query.getWalletRecordMonth()); assertEquals(1L, query.getWalletTypeId());
|
||||
@@ -181,12 +181,12 @@ class WalletLedgerBatchExportTest {
|
||||
real.exportQb(response, new Page<>(8, 10), dto);
|
||||
assertNull(dto.getUserIds()); assertNull(dto.getWalletTypeId()); assertEquals("2026-09", dto.getWalletRecordMonth());
|
||||
assertEquals(List.of(1L), dto.getWalletTypeIdList()); assertEquals(List.of(10L), dto.getDeptIds());
|
||||
verify(mapper, times(4)).selectBalanceExportPage(any(), any());
|
||||
verify(mapper, times(4)).selectBalanceExportPage(any(), any(), any());
|
||||
// 三个钱包查询(含结束检查)和四个历史流水查询均释放上一批会话缓存。
|
||||
verify(context.getBean(SqlSessionTemplate.class), times(7)).clearCache();
|
||||
// 复用原业务服务生成固定参考样本,当前余额 99999 不应进入历史导出。
|
||||
when(mapper.scopeList(any(), any())).thenAnswer(call -> IntStream.range(0, 202).mapToObj(WalletLedgerBatchExportTest::currentWallet).toList());
|
||||
when(mapper.selectBalance(any())).thenReturn(balances);
|
||||
when(mapper.selectBalance(any(), any())).thenReturn(balances);
|
||||
List<WalletManageVo> oldRows = real.scopeQbList(BeanUtil.toBean(dto, WalletManageBalanceDto.class), DataScope.of());
|
||||
long total = 0;
|
||||
for (int i = 0; i < oldRows.size(); i++) {
|
||||
@@ -225,21 +225,21 @@ class WalletLedgerBatchExportTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void historySqlSharesLegacyMonthAndDeletionFiltersWithStablePageOrdering() throws Exception {
|
||||
void historySqlSharesLatestValidBalanceAndStablePageOrdering() throws Exception {
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
try (InputStream input = new ClassPathResource("mapper/WalletManageMapper.xml").getInputStream()) {
|
||||
new XMLMapperBuilder(input, configuration, "mapper/WalletManageMapper.xml", configuration.getSqlFragments()).parse();
|
||||
}
|
||||
for (String month : Arrays.asList("2026-09", null)) {
|
||||
for (String month : List.of("2026-09", "2026-10")) {
|
||||
WalletManageBalanceDto dto = new WalletManageBalanceDto(); dto.setWalletRecordMonth(month);
|
||||
dto.setWalletTypeId(1L); dto.setUserId(1000L); dto.setUserIds(List.of(1000L, 999L));
|
||||
String prefix = WalletManageMapper.class.getName() + ".";
|
||||
var before = configuration.getMappedStatement(prefix + "selectBalance").getBoundSql(Map.of("dto", dto));
|
||||
var after = configuration.getMappedStatement(prefix + "selectBalanceExportPage").getBoundSql(Map.of("dto", dto));
|
||||
var before = configuration.getMappedStatement(prefix + "selectBalance").getBoundSql(Map.of("dto", dto, "monthEndExclusive", java.time.YearMonth.parse(month).plusMonths(1).atDay(1).atStartOfDay()));
|
||||
var after = configuration.getMappedStatement(prefix + "selectBalanceExportPage").getBoundSql(Map.of("dto", dto, "monthEndExclusive", java.time.YearMonth.parse(month).plusMonths(1).atDay(1).atStartOfDay()));
|
||||
String original = before.getSql().replaceAll("\\s+", " ").trim();
|
||||
String paged = after.getSql().replaceAll("\\s+", " ").trim();
|
||||
assertEquals(original.replaceFirst("ORDER BY.*$", ""), paged.replaceFirst("ORDER BY.*$", ""));
|
||||
assertTrue(paged.endsWith("ORDER BY a.user_id, a.id"));
|
||||
assertTrue(paged.endsWith("ORDER BY latest.user_id, latest.wallet_type_id"));
|
||||
assertEquals(before.getParameterMappings().stream().map(item -> item.getProperty()).toList(),
|
||||
after.getParameterMappings().stream().map(item -> item.getProperty()).toList());
|
||||
}
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package com.bsd.pjcloud.wallet.total;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteDeptService;
|
||||
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScopeFuncEnum;
|
||||
import com.bsd.pjcloud.wallet.api.dto.WalletManageBalanceDto;
|
||||
import com.bsd.pjcloud.wallet.api.entity.WalletTypeEntity;
|
||||
import com.bsd.pjcloud.wallet.api.vo.WalletManageBalanceVo;
|
||||
import com.bsd.pjcloud.wallet.api.vo.WalletManageVo;
|
||||
import com.bsd.pjcloud.wallet.mapper.WalletManageMapper;
|
||||
import com.bsd.pjcloud.wallet.mapper.WalletTypeMapper;
|
||||
import com.bsd.pjcloud.wallet.service.WalletTypeService;
|
||||
import com.bsd.pjcloud.wallet.service.impl.WalletManageServiceImpl;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** 验证全量排序查询边界、权限隔离及仅补充当前页资料,查询结果由 SQL 测试验证。 */
|
||||
class WalletQbBalanceSortServiceTest {
|
||||
private final WalletManageMapper mapper = mock(WalletManageMapper.class);
|
||||
private final WalletTypeMapper types = mock(WalletTypeMapper.class);
|
||||
private final WalletTypeService typeService = mock(WalletTypeService.class);
|
||||
private final RemoteUserService users = mock(RemoteUserService.class);
|
||||
private final RemoteDeptService departments = mock(RemoteDeptService.class);
|
||||
private WalletManageServiceImpl service;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
Constructor<?> constructor = WalletManageServiceImpl.class.getConstructors()[0];
|
||||
Map<Class<?>, Object> supplied = Map.of(WalletTypeMapper.class, types, WalletTypeService.class, typeService,
|
||||
RemoteUserService.class, users, RemoteDeptService.class, departments);
|
||||
Object[] dependencies = Arrays.stream(constructor.getParameterTypes())
|
||||
.map(type -> supplied.containsKey(type) ? supplied.get(type) : mock(type)).toArray();
|
||||
service = (WalletManageServiceImpl) constructor.newInstance(dependencies);
|
||||
ReflectionTestUtils.setField(service, "baseMapper", mapper);
|
||||
WalletTypeEntity type = new WalletTypeEntity();
|
||||
type.setId(11L);
|
||||
when(types.list(any())).thenReturn(List.of(type));
|
||||
when(typeService.list()).thenReturn(List.of(type));
|
||||
when(users.getInnerUserById(any())).thenReturn(R.ok(List.of()));
|
||||
when(departments.getDeptByDeptIds(any())).thenReturn(R.ok(Map.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bothDirectionsUseIndependentCountStablePagingAndOnlyCurrentPageProfiles() {
|
||||
for (boolean ascending : List.of(true, false)) {
|
||||
reset(mapper, users);
|
||||
when(users.getInnerUserById(any())).thenReturn(R.ok(List.of()));
|
||||
Page<WalletManageVo> page = sortedPage(ascending);
|
||||
page.addOrder(OrderItem.asc("untrusted_field"));
|
||||
DataScope scope = DataScope.of().deptIds(new ArrayList<>(List.of(7L)));
|
||||
when(mapper.countQbWallets(any(), any())).thenAnswer(call -> {
|
||||
DataScope countScope = call.getArgument(1);
|
||||
assertNotSame(scope, countScope);
|
||||
assertEquals(DataScopeFuncEnum.COUNT, countScope.getFunc());
|
||||
countScope.getDeptList().add(8L);
|
||||
return 100L;
|
||||
});
|
||||
when(mapper.scopeQbBalancePage(any(), any(), any(), any())).thenAnswer(call -> {
|
||||
Page<WalletManageVo> query = call.getArgument(0);
|
||||
assertFalse(query.searchCount());
|
||||
assertEquals(List.of("balance", "id"), query.orders().stream().map(OrderItem::getColumn).toList());
|
||||
assertEquals(ascending, query.orders().get(0).isAsc());
|
||||
assertFalse(query.orders().get(1).isAsc());
|
||||
assertSame(scope, call.getArgument(3));
|
||||
assertEquals(List.of(7L), scope.getDeptList());
|
||||
assertEquals(LocalDateTime.of(2026, 11, 1, 0, 0), call.getArgument(2));
|
||||
return query.setRecords(List.of(wallet(1), wallet(2)));
|
||||
});
|
||||
assertSame(page, service.scopeQbPage(page, query(), scope));
|
||||
assertTrue(page.searchCount());
|
||||
assertEquals(100, page.getTotal());
|
||||
verify(users).getInnerUserById(List.of(71L));
|
||||
verify(mapper, never()).scopePage(any(), any(), any());
|
||||
verify(mapper, never()).selectBalance(any(), any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyPagesSkipHistoryAndProfileQueries() {
|
||||
for (long count : List.of(0L)) {
|
||||
reset(mapper);
|
||||
when(mapper.countQbWallets(any(), any())).thenReturn(count);
|
||||
Page<WalletManageVo> page = sortedPage(true);
|
||||
page.setCurrent(2);
|
||||
assertTrue(service.scopeQbPage(page, query(), DataScope.of()).getRecords().isEmpty());
|
||||
verify(mapper, never()).scopeQbBalancePage(any(), any(), any(), any());
|
||||
}
|
||||
verifyZeroInteractions(users, departments);
|
||||
}
|
||||
|
||||
@Test
|
||||
void countDisabledStillReadsPageAndFailuresRestoreCountFlag() {
|
||||
Page<WalletManageVo> page = sortedPage(true);
|
||||
page.setSearchCount(false);
|
||||
when(mapper.scopeQbBalancePage(any(), any(), any(), any())).thenReturn(page);
|
||||
service.scopeQbPage(page, query(), DataScope.of());
|
||||
verify(mapper, never()).countQbWallets(any(), any());
|
||||
assertFalse(page.searchCount());
|
||||
page.setSearchCount(true);
|
||||
when(mapper.countQbWallets(any(), any())).thenReturn(10L);
|
||||
when(mapper.scopeQbBalancePage(any(), any(), any(), any())).thenThrow(new IllegalStateException("database unavailable"));
|
||||
assertThrows(IllegalStateException.class, () -> service.scopeQbPage(page, query(), DataScope.of()));
|
||||
assertTrue(page.searchCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidMonthAndUnboundedSortingAreRejectedBeforeDatabaseAccess() {
|
||||
for (String month : Arrays.asList(null, "", "2026-1", "2026-13", "0999-01", "9999-12")) {
|
||||
WalletManageBalanceDto dto = query(); dto.setWalletRecordMonth(month);
|
||||
assertThrows(IllegalArgumentException.class, () -> service.scopeQbPage(sortedPage(true), dto, DataScope.of()));
|
||||
}
|
||||
Page<WalletManageVo> page = sortedPage(true); page.setSize(-1);
|
||||
assertThrows(IllegalArgumentException.class, () -> service.scopeQbPage(page, query(), DataScope.of()));
|
||||
verifyZeroInteractions(mapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ordinaryPageFillsDuplicateBalancesWhileExportKeepsExistingDeduplication() {
|
||||
WalletManageBalanceVo history = new WalletManageBalanceVo();
|
||||
history.setUserId(71L); history.setWalletTypeId(11L); history.setBalance(-123);
|
||||
history.setCreateTime(LocalDateTime.of(2026, 10, 31, 12, 0));
|
||||
when(mapper.selectBalance(any(), any())).thenReturn(List.of(history));
|
||||
when(mapper.scopePage(any(), any(), any())).thenAnswer(call ->
|
||||
new Page<WalletManageVo>().setTotal(2).setRecords(List.of(wallet(1), wallet(2))));
|
||||
List<WalletManageVo> ordinary = service.scopeQbPage(new Page<>(1, 20), query(), DataScope.of()).getRecords();
|
||||
assertEquals(List.of(-123, -123), ordinary.stream().map(WalletManageVo::getBalance).toList());
|
||||
when(mapper.scopeExportPage(any(), any(), any())).thenAnswer(call ->
|
||||
new Page<WalletManageVo>().setRecords(List.of(wallet(1), wallet(2))));
|
||||
when(mapper.selectBalanceExportPage(any(), any(), any())).thenReturn(new Page<WalletManageBalanceVo>().setRecords(List.of(history)));
|
||||
List<WalletManageVo> exported = service.scopeQbExportPage(new Page<>(1, 20), query(), DataScope.of()).getRecords();
|
||||
assertEquals(List.of(-123, 0), exported.stream().map(WalletManageVo::getBalance).toList());
|
||||
assertEquals(history.getCreateTime(), exported.get(0).getLastTopUpTime());
|
||||
assertNull(exported.get(1).getLastTopUpTime());
|
||||
}
|
||||
|
||||
private static Page<WalletManageVo> sortedPage(boolean ascending) {
|
||||
return new Page<WalletManageVo>(1, 2).addOrder(ascending ? OrderItem.asc("balance") : OrderItem.desc("balance"));
|
||||
}
|
||||
|
||||
private static WalletManageBalanceDto query() {
|
||||
WalletManageBalanceDto dto = new WalletManageBalanceDto(); dto.setWalletRecordMonth("2026-10");
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static WalletManageVo wallet(long id) {
|
||||
WalletManageVo row = new WalletManageVo(); row.setId(id); row.setUserId(71L); row.setDeptId(7L);
|
||||
row.setWalletTypeId(11L); row.setBalance(999999);
|
||||
return row;
|
||||
}
|
||||
}
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
package com.bsd.pjcloud.wallet.total;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScopeFuncEnum;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScopeInnerInterceptor;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScopeSqlInjector;
|
||||
import com.bsd.pjcloud.common.data.tenant.PjcloudTenantConfigProperties;
|
||||
import com.bsd.pjcloud.common.data.tenant.PjcloudTenantHandler;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
|
||||
import com.bsd.pjcloud.wallet.api.dto.WalletManageBalanceDto;
|
||||
import com.bsd.pjcloud.wallet.api.dto.WalletManageDto;
|
||||
import com.bsd.pjcloud.wallet.api.vo.WalletManageVo;
|
||||
import com.bsd.pjcloud.wallet.mapper.WalletManageMapper;
|
||||
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
|
||||
import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
import org.apache.ibatis.session.LocalCacheScope;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.sql.Connection;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/** 以生产 XML 和租户、数据权限、分页拦截器验证月末余额排序;数据全部位于独立 H2 内存库。 */
|
||||
class WalletQbBalanceSortSqlTest {
|
||||
/** 十月月末的排他上界,十一月零点流水不能参与十月余额。 */
|
||||
private static final LocalDateTime MONTH_END = LocalDateTime.of(2026, 11, 1, 0, 0);
|
||||
/** 每个测试单独创建并关闭的数据库会话。 */
|
||||
private SqlSession session;
|
||||
/** 指向生产 SQL 的 Mapper,不使用模拟余额查询。 */
|
||||
private WalletManageMapper mapper;
|
||||
/** 仅控制测试操作者是否拥有全部数据权限;有限权限仍由生产拦截器拼装。 */
|
||||
private boolean unrestricted = true;
|
||||
|
||||
/** 建立生产相同的拦截顺序,并写入跨租户、重复钱包和历史流水边界。 */
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
MybatisConfiguration configuration = new MybatisConfiguration();
|
||||
configuration.setMapUnderscoreToCamelCase(true);
|
||||
configuration.setLocalCacheScope(LocalCacheScope.STATEMENT);
|
||||
configuration.setEnvironment(new Environment("wallet-balance-sort", new JdbcTransactionFactory(),
|
||||
new UnpooledDataSource("org.h2.Driver", "jdbc:h2:mem:wallet_balance_sort_" + UUID.randomUUID()
|
||||
+ ";MODE=MySQL;DATABASE_TO_LOWER=TRUE", "sa", "")));
|
||||
GlobalConfigUtils.getGlobalConfig(configuration).setSqlInjector(new DataScopeSqlInjector());
|
||||
PjcloudTenantConfigProperties tenants = new PjcloudTenantConfigProperties();
|
||||
tenants.setTables(List.of("wallet_manage", "wallet_transaction", "wallet_type"));
|
||||
MybatisPlusInterceptor plugins = new MybatisPlusInterceptor();
|
||||
plugins.addInnerInterceptor(new TenantLineInnerInterceptor(new PjcloudTenantHandler(tenants)));
|
||||
DataScopeInnerInterceptor permissions = new DataScopeInnerInterceptor();
|
||||
permissions.setDataScopeHandle(scope -> unrestricted);
|
||||
plugins.addInnerInterceptor(permissions);
|
||||
plugins.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
configuration.addInterceptor(plugins);
|
||||
String resource = "mapper/WalletManageMapper.xml";
|
||||
try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource)) {
|
||||
assertNotNull(input);
|
||||
new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
|
||||
}
|
||||
session = new MybatisSqlSessionFactoryBuilder().build(configuration).openSession(true);
|
||||
mapper = session.getMapper(WalletManageMapper.class);
|
||||
TenantContextHolder.setTenantId(7L);
|
||||
Connection database = session.getConnection();
|
||||
try (var statement = database.createStatement()) {
|
||||
statement.execute("""
|
||||
create table wallet_manage(id bigint primary key,user_id bigint,wallet_type_id bigint,dept_id bigint,
|
||||
card_number varchar(64),balance int,total_expense int,total_recharge int,freeze_price int,
|
||||
is_freeze varchar(8),active_bind_time timestamp,status varchar(8),version int,
|
||||
create_by varchar(64),create_time timestamp,update_by varchar(64),update_time timestamp,
|
||||
del_flag varchar(1),tenant_id bigint);
|
||||
create table wallet_type(id bigint primary key,tenant_id bigint,del_flag varchar(1));
|
||||
create table wallet_transaction(id bigint primary key,wallet_id bigint,user_id bigint,
|
||||
balance int,create_time timestamp,del_flag varchar(1),tenant_id bigint);
|
||||
insert into wallet_type values (11,7,'0'),(22,7,'0'),(33,7,'1'),(81,8,'0');
|
||||
insert into wallet_manage(id,user_id,wallet_type_id,dept_id,balance,create_by,del_flag,tenant_id) values
|
||||
(101,1,11,10,999,'admin','0',7),(102,2,11,10,999,'admin','0',7),
|
||||
(103,3,11,20,999,'admin','0',7),(104,4,22,20,999,'admin','0',7),
|
||||
(105,1,11,20,999,'admin','0',7),(106,5,11,10,999,'admin','0',7),
|
||||
(107,6,33,10,999,'admin','0',7),(108,7,44,30,999,'self','0',7),
|
||||
(109,1,11,30,999,'admin','1',7),(201,1,81,10,999,'admin','0',8);
|
||||
insert into wallet_transaction values
|
||||
(1,101,1,100,'2026-10-01 12:00:00','0',7),
|
||||
(2,101,1,120,'2026-10-20 12:00:00','0',7),
|
||||
(3,101,1,140,'2026-10-20 12:00:00','0',7),
|
||||
(4,101,1,9999,'2026-10-31 23:59:59','1',7),
|
||||
(5,101,1,9999,'2026-11-01 00:00:00','0',7),
|
||||
(6,109,1,160,'2026-10-25 12:00:00','0',7),
|
||||
(7,102,2,-50,'2026-10-31 23:59:59','0',7),
|
||||
(8,104,4,0,'2026-10-15 12:00:00','0',7),
|
||||
(9,106,5,null,'2026-10-15 12:00:00','0',7),
|
||||
(10,107,6,7000,'2026-10-15 12:00:00','0',7),
|
||||
(11,108,7,8000,'2026-10-15 12:00:00','0',7),
|
||||
(12,201,1,700,'2026-10-15 12:00:00','0',8),
|
||||
(13,101,1,9999,'2026-10-31 23:59:59','0',8),
|
||||
(14,101,1,9999,null,'0',7);
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
||||
/** 关闭连接即销毁该测试的内存数据库,清理线程租户状态。 */
|
||||
@AfterEach
|
||||
void close() {
|
||||
TenantContextHolder.clear();
|
||||
if (session != null) session.close();
|
||||
}
|
||||
|
||||
/** 第一页先比较全部历史余额,翻页保留余额顺序和同额主键顺序。 */
|
||||
@Test
|
||||
void ascendingAndDescendingSortAllRowsBeforePagination() {
|
||||
assertEquals(List.of(102L, 108L, 107L), ids(page(new WalletManageDto(), true, 1, 3, DataScope.of())));
|
||||
assertEquals(List.of(106L, 104L, 103L), ids(page(new WalletManageDto(), true, 2, 3, DataScope.of())));
|
||||
assertEquals(List.of(105L, 101L), ids(page(new WalletManageDto(), true, 3, 3, DataScope.of())));
|
||||
assertEquals(List.of(105L, 101L, 108L), ids(page(new WalletManageDto(), false, 1, 3, DataScope.of())));
|
||||
assertEquals(List.of(-50, 0, 0, 0, 0, 0, 160, 160),
|
||||
page(new WalletManageDto(), true, 1, 50, DataScope.of()).getRecords().stream().map(WalletManageVo::getBalance).toList());
|
||||
WalletManageVo nullBalance = page(new WalletManageDto(), true, 1, 50, DataScope.of()).getRecords().stream()
|
||||
.filter(wallet -> wallet.getId().equals(106L)).findFirst().orElseThrow();
|
||||
assertNull(nullBalance.getLastTopUpTime(), "空余额的历史时间不能在切换排序后突然显示");
|
||||
assertEquals(8L, mapper.countQbWallets(new WalletManageDto(), DataScope.of().func(DataScopeFuncEnum.COUNT)));
|
||||
}
|
||||
|
||||
/** 历史跨钱包取值不受当前部门过滤影响,类型单值与列表取交集。 */
|
||||
@Test
|
||||
void departmentUserAndTypeFiltersKeepHistoricalBalance() {
|
||||
WalletManageDto query = new WalletManageDto();
|
||||
query.setDeptIds(List.of(20L));
|
||||
query.setIsInclude(true);
|
||||
assertEquals(List.of(105L, 104L, 103L), ids(page(query, false, 1, 50, DataScope.of())));
|
||||
assertEquals(160, page(query, false, 1, 50, DataScope.of()).getRecords().get(0).getBalance());
|
||||
query.setIsInclude(false);
|
||||
assertEquals(List.of(101L, 108L, 107L, 106L, 102L), ids(page(query, false, 1, 50, DataScope.of())));
|
||||
query = new WalletManageDto();
|
||||
query.setUserIds(List.of(1L, 2L));
|
||||
query.setWalletTypeId(11L);
|
||||
query.setWalletTypeIdList(List.of(11L, 22L));
|
||||
assertEquals(List.of(105L, 101L, 102L), ids(page(query, false, 1, 50, DataScope.of())));
|
||||
query.setWalletTypeIdList(List.of(22L));
|
||||
assertTrue(page(query, false, 1, 50, DataScope.of()).getRecords().isEmpty());
|
||||
}
|
||||
|
||||
/** 真实权限拦截器先限制候选钱包,再由分页插件在最外层排序和截取。 */
|
||||
@Test
|
||||
void departmentSelfAndDeniedPermissionsApplyBeforePageLimit() {
|
||||
unrestricted = false;
|
||||
assertEquals(List.of(101L, 107L), ids(page(new WalletManageDto(), false, 1, 2, DataScope.of().deptIds(List.of(10L)))));
|
||||
assertEquals(4L, mapper.countQbWallets(new WalletManageDto(), DataScope.of().deptIds(List.of(10L)).func(DataScopeFuncEnum.COUNT)));
|
||||
DataScope ownOrDepartment = DataScope.of().deptIds(List.of(10L));
|
||||
ownOrDepartment.setUsername("self");
|
||||
assertEquals(List.of(101L, 108L), ids(page(new WalletManageDto(), false, 1, 2, ownOrDepartment)));
|
||||
DataScope own = DataScope.of();
|
||||
own.setUsername("self");
|
||||
assertEquals(List.of(108L), ids(page(new WalletManageDto(), false, 1, 10, own)));
|
||||
assertTrue(page(new WalletManageDto(), false, 1, 10, DataScope.of()).getRecords().isEmpty());
|
||||
assertEquals(0L, mapper.countQbWallets(new WalletManageDto(), DataScope.of().func(DataScopeFuncEnum.COUNT)));
|
||||
}
|
||||
|
||||
/** 切换租户后,同一用户 ID 和异常跨租户流水不会污染余额及分页总数。 */
|
||||
@Test
|
||||
void productionTenantInterceptorScopesWalletsTypesAndHistory() {
|
||||
TenantContextHolder.setTenantId(8L);
|
||||
Page<WalletManageVo> result = page(new WalletManageDto(), false, 1, 10, DataScope.of());
|
||||
assertEquals(List.of(201L), ids(result));
|
||||
assertEquals(700, result.getRecords().get(0).getBalance());
|
||||
assertEquals(1L, mapper.countQbWallets(new WalletManageDto(), DataScope.of().func(DataScopeFuncEnum.COUNT)));
|
||||
}
|
||||
|
||||
/** 同秒取最大 ID,月界、删除和无时间流水不改变普通列表与排序的共同余额来源。 */
|
||||
@Test
|
||||
void ordinaryBalanceQueryUsesSameDeterministicHistory() {
|
||||
WalletManageBalanceDto query = new WalletManageBalanceDto();
|
||||
query.setWalletTypeId(11L);
|
||||
query.setUserId(1L);
|
||||
var beforeLaterWallet = mapper.selectBalance(query, LocalDateTime.of(2026, 10, 21, 0, 0));
|
||||
assertEquals(1, beforeLaterWallet.size());
|
||||
assertEquals(140, beforeLaterWallet.get(0).getBalance());
|
||||
var monthEnd = mapper.selectBalance(query, MONTH_END);
|
||||
assertEquals(1, monthEnd.size());
|
||||
assertEquals(160, monthEnd.get(0).getBalance());
|
||||
assertEquals(109L, monthEnd.get(0).getWalletId());
|
||||
assertEquals(LocalDateTime.of(2026, 10, 25, 12, 0), monthEnd.get(0).getCreateTime());
|
||||
}
|
||||
|
||||
/** 仅使用服务端固定排序字段,关闭自动统计以覆盖实际服务的独立计数路径。 */
|
||||
private Page<WalletManageVo> page(WalletManageDto query, boolean ascending, long current, long size, DataScope scope) {
|
||||
Page<WalletManageVo> page = new Page<>(current, size, false);
|
||||
page.addOrder(ascending ? OrderItem.asc("balance") : OrderItem.desc("balance"), OrderItem.desc("id"));
|
||||
return mapper.scopeQbBalancePage(page, query, MONTH_END, scope);
|
||||
}
|
||||
|
||||
/** 提取页面顺序,断言分页边界和同额主键顺序。 */
|
||||
private List<Long> ids(Page<WalletManageVo> page) {
|
||||
return page.getRecords().stream().map(WalletManageVo::getId).toList();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user