11
This commit is contained in:
1 parent
3dfac779ce
commit
02a25a2a57
4 files changed
+166
-4
No files matched your search
+7
-1
@@ -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
|
||||
|
||||
+11
@@ -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);
|
||||
|
||||
/**
|
||||
* 卡号是否存在
|
||||
*/
|
||||
|
||||
+46
-3
@@ -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>()
|
||||
|
||||
+102
@@ -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 ="));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user