This commit is contained in:
dzc committed 2026-08-17 20:02:10 +08:00
1 parent 3dfac779ce
commit 02a25a2a57
4 files changed
+166 -4

No files matched your search

@@ -213,12 +213,18 @@ public class WalletActiveController {
return R.ok(walletActiveService.edit(walletActive));
}
/**
* 批量删除钱包开卡记录,并同步解除这些开卡记录与用户钱包之间的卡号绑定。
*
* @param ids 待删除的钱包开卡记录 ID 数组
* @return 是否至少删除了一条有效的钱包开卡记录
*/
@Operation(summary = "通过id删除钱包开卡", description = "通过id删除钱包开卡")
@SysLog("通过id删除钱包开卡")
@DeleteMapping
@PreAuthorize("@pms.hasPermission('wallet_walletActive_del')")
public R removeById(@RequestBody Long[] ids) {
return R.ok(walletActiveService.removeBatchByIds(CollUtil.toList(ids)));
return R.ok(walletActiveService.removeWithWalletCardByIds(CollUtil.toList(ids)));
}
@ResponseExcel
@@ -9,6 +9,9 @@ import com.bsd.pjcloud.wallet.api.entity.WalletActiveEntity;
import com.bsd.pjcloud.wallet.api.entity.WalletManageEntity;
import com.bsd.pjcloud.wallet.api.vo.WalletActiveVo;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
public interface WalletActiveService extends IService<WalletActiveEntity> {
/**
* 一键开卡
@@ -39,6 +42,14 @@ public interface WalletActiveService extends IService<WalletActiveEntity> {
*/
Boolean cancelAdminBindCard(Long activeId);
/**
* 批量删除钱包开卡记录,并同步清除仍绑定这些卡号的钱包信息。
*
* @param ids 待删除的钱包开卡记录 ID;空集合或不存在的 ID 不产生数据变更
* @return 是否至少删除了一条有效的钱包开卡记录
*/
Boolean removeWithWalletCardByIds(List<Long> ids);
/**
* 卡号是否存在
*/
@@ -116,7 +116,8 @@ public class WalletActiveServiceImpl extends ServiceImpl<WalletActiveMapper, Wal
/**
* 将指定空闲饭卡绑定到用户的饭卡钱包。
* <p>饭卡记录和钱包记录均通过带未绑定条件的更新完成抢占;任一记录已被并发请求绑定时抛出异常,
* 当前事务会回滚,避免同一卡号覆盖绑定到多个用户或同一钱包关联多张饭卡。</p>
* 当前事务会回滚,避免同一卡号覆盖绑定到多个用户或同一钱包关联多张饭卡;钱包卡号为
* {@code NULL} 或历史空字符串时均视为未绑定。</p>
*
* @param dto 待绑定的用户、钱包和饭卡卡号
* @return 完成绑定后的饭卡记录
@@ -149,11 +150,13 @@ public class WalletActiveServiceImpl extends ServiceImpl<WalletActiveMapper, Wal
if (activeUpdateCount == 0) {
throw new RuntimeException("该卡号已经绑定用户");
}
// 钱包也需要条件更新;失败时抛出异常,由事务回滚前面已经完成的饭卡抢占。
// 历史未绑定钱包的卡号同时存在 NULL 和空字符串,原子更新必须兼容两种值;失败时由事务回滚饭卡抢占。
WalletManageEntity walletObj = WalletManageEntity.builder().id(selfWallet.getId()).cardNumber(walletActive.getCardNumber()).status(WalleCardStatusEnum.ACTIVE.getCode()).activeBindTime(LocalDateTime.now()).build();
boolean walletUpdateSuccess = walletManageService.update(walletObj, new LambdaUpdateWrapper<WalletManageEntity>()
.eq(WalletManageEntity::getId, selfWallet.getId())
.isNull(WalletManageEntity::getCardNumber));
.and(wrapper -> wrapper.isNull(WalletManageEntity::getCardNumber)
.or()
.eq(WalletManageEntity::getCardNumber, "")));
if (!walletUpdateSuccess) {
throw new RuntimeException("用户已绑定饭卡");
}
@@ -239,6 +242,46 @@ public class WalletActiveServiceImpl extends ServiceImpl<WalletActiveMapper, Wal
return true;
}
/**
* 批量删除钱包开卡记录,并同步清除对应用户钱包的卡号、激活状态和绑定时间。
* <p>删除前锁定开卡记录,钱包更新时同时校验钱包 ID 和当前卡号,防止异常历史关联或并发换卡
* 导致其他有效卡号被误清除;钱包解绑与开卡记录逻辑删除在同一事务中完成。</p>
*
* @param ids 待删除的钱包开卡记录 ID;空集合或不存在的 ID 不产生数据变更
* @return 是否至少删除了一条有效的钱包开卡记录
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean removeWithWalletCardByIds(List<Long> ids) {
if (ObjectUtil.isEmpty(ids)) {
return false;
}
List<Long> validIds = ids.stream().filter(Objects::nonNull).distinct().toList();
if (validIds.isEmpty()) {
return false;
}
List<WalletActiveEntity> walletActiveList = baseMapper.selectList(new LambdaQueryWrapper<WalletActiveEntity>()
.in(WalletActiveEntity::getId, validIds)
.last("for update"));
if (ObjectUtil.isEmpty(walletActiveList)) {
return false;
}
for (WalletActiveEntity walletActive : walletActiveList) {
if (ObjectUtil.isEmpty(walletActive.getWalletId()) || ObjectUtil.isEmpty(walletActive.getCardNumber())) {
continue;
}
// 只清除仍绑定当前开卡卡号的钱包,避免历史脏关联误伤钱包后来绑定的新卡。
LambdaUpdateWrapper<WalletManageEntity> walletUpdateWrapper = new LambdaUpdateWrapper<WalletManageEntity>()
.eq(WalletManageEntity::getId, walletActive.getWalletId())
.eq(WalletManageEntity::getCardNumber, walletActive.getCardNumber())
.set(WalletManageEntity::getCardNumber, null)
.set(WalletManageEntity::getStatus, WalleCardStatusEnum.NONE_ACTIVE.getCode())
.set(WalletManageEntity::getActiveBindTime, null);
walletManageService.update(walletUpdateWrapper);
}
return baseMapper.deleteByIds(walletActiveList.stream().map(WalletActiveEntity::getId).toList()) > 0;
}
@Override
public Boolean exitCardNumber(Long id, String CardNumber) {
Long count = baseMapper.selectCount(new LambdaQueryWrapper<WalletActiveEntity>()
@@ -0,0 +1,102 @@
package com.bsd.pjcloud.wallet.service.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.bsd.pjcloud.admin.api.feign.RemoteDeptService;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
import com.bsd.pjcloud.wallet.api.dto.WalletActiveBindCardDto;
import com.bsd.pjcloud.wallet.api.entity.WalletActiveEntity;
import com.bsd.pjcloud.wallet.api.entity.WalletManageEntity;
import com.bsd.pjcloud.wallet.mapper.WalletActiveMapper;
import com.bsd.pjcloud.wallet.mapper.WalletTypeMapper;
import com.bsd.pjcloud.wallet.service.MchManageService;
import com.bsd.pjcloud.wallet.service.WalletManageService;
import com.bsd.pjcloud.wallet.service.WalletTypeService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 饭卡绑定的历史数据兼容与原子更新条件测试。
*/
@ExtendWith(MockitoExtension.class)
class WalletActiveServiceImplTest {
/** 钱包管理服务 Mock,用于模拟历史钱包卡号和捕获绑定条件。 */
@Mock
private WalletManageService walletManageService;
/** 钱包类型服务 Mock,用于满足被测服务的构造依赖。 */
@Mock
private WalletTypeService walletTypeService;
/** 商户管理服务 Mock,用于满足被测服务的构造依赖。 */
@Mock
private MchManageService mchManageService;
/** 钱包类型 Mapper Mock,用于满足被测服务的构造依赖。 */
@Mock
private WalletTypeMapper walletTypeMapper;
/** 用户远程服务 Mock,用于满足被测服务的构造依赖。 */
@Mock
private RemoteUserService remoteUserService;
/** 部门远程服务 Mock,用于满足被测服务的构造依赖。 */
@Mock
private RemoteDeptService remoteDeptService;
/** 饭卡 Mapper Mock,用于模拟空闲饭卡抢占和绑定结果查询。 */
@Mock
private WalletActiveMapper walletActiveMapper;
/** 被测饭卡服务。 */
@InjectMocks
private WalletActiveServiceImpl service;
/**
* 将 MyBatis-Plus 基类 Mapper 指向当前 Mock,避免启动 Spring 与数据库。
*/
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(service, "baseMapper", walletActiveMapper);
}
/**
* 历史钱包卡号为空字符串时应允许绑定,且原子更新条件必须同时接受 NULL 和空字符串。
*/
@Test
void bindCardShouldAcceptLegacyEmptyCardNumber() {
WalletActiveEntity availableCard = WalletActiveEntity.builder().id(100L).cardNumber("card-001").build();
WalletManageEntity selfWallet = WalletManageEntity.builder().id(200L).userId(300L).walletTypeId(400L).cardNumber("").build();
WalletActiveEntity boundCard = WalletActiveEntity.builder().id(100L).userId(300L).walletId(200L).cardNumber("card-001").build();
WalletActiveBindCardDto dto = new WalletActiveBindCardDto();
dto.setUserId(300L);
dto.setWalletId(200L);
dto.setCardNumber("card-001");
when(walletActiveMapper.selectOne(any(Wrapper.class))).thenReturn(availableCard);
when(walletManageService.getWallet("card-001")).thenReturn(null);
when(walletManageService.getWallet(200L)).thenReturn(selfWallet);
when(walletActiveMapper.update(any(WalletActiveEntity.class), any(Wrapper.class))).thenReturn(1);
when(walletManageService.update(any(WalletManageEntity.class), any(Wrapper.class))).thenReturn(true);
when(walletActiveMapper.selectById(100L)).thenReturn(boundCard);
assertSame(boundCard, service.bindCard(dto));
ArgumentCaptor<Wrapper<WalletManageEntity>> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
verify(walletManageService).update(any(WalletManageEntity.class), wrapperCaptor.capture());
String sqlSegment = wrapperCaptor.getValue().getSqlSegment();
assertTrue(sqlSegment.contains("card_number IS NULL"));
assertTrue(sqlSegment.contains("card_number ="));
}
}