diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/WalletActiveController.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/WalletActiveController.java index da403219e..e977cb018 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/WalletActiveController.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/WalletActiveController.java @@ -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 diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/WalletActiveService.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/WalletActiveService.java index 31f6781ce..08ed8da42 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/WalletActiveService.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/WalletActiveService.java @@ -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 { /** * 一键开卡 @@ -39,6 +42,14 @@ public interface WalletActiveService extends IService { */ Boolean cancelAdminBindCard(Long activeId); + /** + * 批量删除钱包开卡记录,并同步清除仍绑定这些卡号的钱包信息。 + * + * @param ids 待删除的钱包开卡记录 ID;空集合或不存在的 ID 不产生数据变更 + * @return 是否至少删除了一条有效的钱包开卡记录 + */ + Boolean removeWithWalletCardByIds(List ids); + /** * 卡号是否存在 */ diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveServiceImpl.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveServiceImpl.java index f2c39a96d..0b36c178a 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveServiceImpl.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveServiceImpl.java @@ -116,7 +116,8 @@ public class WalletActiveServiceImpl extends ServiceImpl饭卡记录和钱包记录均通过带未绑定条件的更新完成抢占;任一记录已被并发请求绑定时抛出异常, - * 当前事务会回滚,避免同一卡号覆盖绑定到多个用户或同一钱包关联多张饭卡。

+ * 当前事务会回滚,避免同一卡号覆盖绑定到多个用户或同一钱包关联多张饭卡;钱包卡号为 + * {@code NULL} 或历史空字符串时均视为未绑定。

* * @param dto 待绑定的用户、钱包和饭卡卡号 * @return 完成绑定后的饭卡记录 @@ -149,11 +150,13 @@ public class WalletActiveServiceImpl extends ServiceImpl() .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删除前锁定开卡记录,钱包更新时同时校验钱包 ID 和当前卡号,防止异常历史关联或并发换卡 + * 导致其他有效卡号被误清除;钱包解绑与开卡记录逻辑删除在同一事务中完成。

+ * + * @param ids 待删除的钱包开卡记录 ID;空集合或不存在的 ID 不产生数据变更 + * @return 是否至少删除了一条有效的钱包开卡记录 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean removeWithWalletCardByIds(List ids) { + if (ObjectUtil.isEmpty(ids)) { + return false; + } + List validIds = ids.stream().filter(Objects::nonNull).distinct().toList(); + if (validIds.isEmpty()) { + return false; + } + List walletActiveList = baseMapper.selectList(new LambdaQueryWrapper() + .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 walletUpdateWrapper = new LambdaUpdateWrapper() + .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() diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveServiceImplTest.java b/pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveServiceImplTest.java new file mode 100644 index 000000000..04907639a --- /dev/null +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveServiceImplTest.java @@ -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> 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 =")); + } +}