From 02a25a2a5714fa4da428b6678b41698e7c33d35d Mon Sep 17 00:00:00 2001
From: dzc <2388224167@qq.com>
Date: Mon, 17 Aug 2026 20:02:10 +0800
Subject: [PATCH] 11
---
.../controller/WalletActiveController.java | 8 +-
.../wallet/service/WalletActiveService.java | 11 ++
.../service/impl/WalletActiveServiceImpl.java | 49 ++++++++-
.../impl/WalletActiveServiceImplTest.java | 102 ++++++++++++++++++
4 files changed, 166 insertions(+), 4 deletions(-)
create mode 100644 pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/WalletActiveServiceImplTest.java
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 ="));
+ }
+}