From 0c68b7e03330c71d5906a057df6b8a1426f4a183 Mon Sep 17 00:00:00 2001 From: jsowell <123@jsowell.com> Date: Wed, 13 May 2026 17:18:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=AC=E5=85=B1=E7=99=BB=E9=99=86=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E6=96=B9=E6=B3=95V2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/jsowell/service/MemberService.java | 247 ++++++++++++++++ .../java/com/jsowell/service/TempService.java | 21 ++ .../MemberServiceRegisterAndLoginTest.java | 275 ++++++++++++++++++ .../pile/dto/ImportMemberBalanceDTO.java | 14 + 4 files changed, 557 insertions(+) create mode 100644 jsowell-admin/src/test/java/com/jsowell/service/MemberServiceRegisterAndLoginTest.java create mode 100644 jsowell-pile/src/main/java/com/jsowell/pile/dto/ImportMemberBalanceDTO.java diff --git a/jsowell-admin/src/main/java/com/jsowell/service/MemberService.java b/jsowell-admin/src/main/java/com/jsowell/service/MemberService.java index 98a49dec1..0f7fa3453 100644 --- a/jsowell-admin/src/main/java/com/jsowell/service/MemberService.java +++ b/jsowell-admin/src/main/java/com/jsowell/service/MemberService.java @@ -277,6 +277,253 @@ public class MemberService { } } + /** + * 公共登录注册方法 V2 + * + *

该方法与 {@link #memberRegisterAndLogin(MemberRegisterAndLoginDTO)} 保持相同业务逻辑, + * 但将流程拆成更清晰的几个步骤:校验请求、获取锁、查找或创建会员、同步第三方身份、生成 token。

+ * + * @param dto 登录/注册入参 + * @return 服务端生成的会员 token + */ + protected String memberRegisterAndLoginV2(MemberRegisterAndLoginDTO dto) { + String phoneNumber = dto.getMobileNumber(); + String firstLevelMerchantId = dto.getFirstLevelMerchantId(); + log.info("公共登录注册方法V2, phoneNumber:{}, firstLevelMerchantId:{}, openId:{}", phoneNumber, firstLevelMerchantId, dto.getOpenId()); + + // 1. 先做参数校验,尽早拦截无效请求。 + validateMemberRegisterAndLoginV2Request(dto); + + // 2. 使用“手机号 + 一级运营商”作为锁粒度,串行化同一会员的注册/登录流程。 + String lockKey = buildMemberRegisterAndLoginLockKey(phoneNumber, firstLevelMerchantId); + String requestId = IdUtils.fastUUID(); + Boolean isLock = false; + try { + // 3. 获取锁,防止并发下重复注册或重复创建钱包。 + isLock = acquireMemberRegisterAndLoginLock(lockKey, requestId, phoneNumber, firstLevelMerchantId); + + // 4. 会员不存在就静默注册,已存在则按请求来源同步 openId / buyerId。 + MemberBasicInfo memberBasicInfo = findOrCreateMemberForRegisterAndLoginV2(dto); + + // 5. 最终统一生成会员 token 返回前端。 + return JWTUtils.createMemberToken(memberBasicInfo.getMemberId(), memberBasicInfo.getNickName()); + } finally { + releaseMemberRegisterAndLoginLock(lockKey, requestId, phoneNumber, firstLevelMerchantId, isLock); + } + } + + /** + * 校验 V2 登录/注册请求中的关键参数。 + * + * @param dto 登录/注册入参 + */ + private void validateMemberRegisterAndLoginV2Request(MemberRegisterAndLoginDTO dto) { + if (StringUtils.isBlank(dto.getMobileNumber())) { + throw new BusinessException(ReturnCodeEnum.CODE_GET_MOBILE_NUMBER_BY_CODE_ERROR); + } + if (StringUtils.isBlank(dto.getFirstLevelMerchantId())) { + throw new BusinessException(ReturnCodeEnum.CODE_GET_MERCHANT_ID_BY_APP_ID_ERROR); + } + if (isWechatLiteRequest(dto) && StringUtils.isBlank(dto.getOpenId())) { + throw new BusinessException(ReturnCodeEnum.CODE_OPEN_ID_IS_NULL_ERROR); + } + } + + /** + * 生成登录/注册流程使用的分布式锁 key。 + * + * @param phoneNumber 手机号 + * @param firstLevelMerchantId 一级运营商 ID + * @return 分布式锁 key + */ + private String buildMemberRegisterAndLoginLockKey(String phoneNumber, String firstLevelMerchantId) { + return CacheConstants.USER_APP_REGISTER + phoneNumber + ":" + firstLevelMerchantId; + } + + /** + * 获取登录/注册流程的分布式锁,避免同一会员并发注册。 + * + * @param lockKey 锁 key + * @param requestId 当前请求唯一标识 + * @param phoneNumber 手机号 + * @param firstLevelMerchantId 一级运营商 ID + * @return 是否成功获取锁 + */ + private boolean acquireMemberRegisterAndLoginLock(String lockKey, String requestId, String phoneNumber, String firstLevelMerchantId) { + boolean isLock = redisCache.lock(lockKey, requestId, 60); + if (!isLock) { + log.warn("获取注册锁失败,可能有并发请求正在处理, phoneNumber:{}, merchantId:{}", phoneNumber, firstLevelMerchantId); + throw new BusinessException(ReturnCodeEnum.CODE_MEMBER_REGISTER_AND_LOGIN_PROCESSING); + } + return true; + } + + /** + * 释放登录/注册流程的分布式锁。 + * + * @param lockKey 锁 key + * @param requestId 当前请求唯一标识 + * @param phoneNumber 手机号 + * @param firstLevelMerchantId 一级运营商 ID + * @param isLock 是否持有锁 + */ + private void releaseMemberRegisterAndLoginLock(String lockKey, String requestId, String phoneNumber, String firstLevelMerchantId, Boolean isLock) { + if (!Boolean.TRUE.equals(isLock)) { + return; + } + try { + Object lockValue = redisCache.getCacheObject(lockKey); + if (lockValue != null && requestId.equals(lockValue.toString())) { + redisCache.unLock(lockKey); + } + } catch (Exception e) { + log.error("释放注册锁失败, phoneNumber:{}, merchantId:{}, error:{}", phoneNumber, firstLevelMerchantId, e.getMessage()); + } + } + + /** + * 根据手机号和运营商查询会员,不存在则静默注册,存在则同步第三方身份信息。 + * + * @param dto 登录/注册入参 + * @return 已存在或新创建的会员信息 + */ + private MemberBasicInfo findOrCreateMemberForRegisterAndLoginV2(MemberRegisterAndLoginDTO dto) { + MemberBasicInfo memberBasicInfo = memberBasicInfoService.selectInfoByMobileNumber(dto.getMobileNumber(), dto.getFirstLevelMerchantId()); + if (Objects.isNull(memberBasicInfo)) { + return registerMemberForRegisterAndLoginV2(dto); + } + syncMemberThirdPartyIdentityForRegisterAndLoginV2(memberBasicInfo, dto); + return memberBasicInfo; + } + + /** + * 执行静默注册流程,并在唯一索引冲突时回查已创建的会员。 + * + * @param dto 登录/注册入参 + * @return 注册完成后的会员信息 + */ + private MemberBasicInfo registerMemberForRegisterAndLoginV2(MemberRegisterAndLoginDTO dto) { + MemberBasicInfo memberBasicInfo = buildNewMemberForRegisterAndLoginV2(dto); + MemberTransactionDTO memberTransactionDTO = buildMemberTransactionForRegisterAndLoginV2(memberBasicInfo, dto.getFirstLevelMerchantId()); + try { + transactionService.createMember(memberTransactionDTO); + return memberBasicInfo; + } catch (DuplicateKeyException e) { + return reloadMemberAfterDuplicateKeyForRegisterAndLoginV2(dto); + } + } + + /** + * 构造一个新的会员基础信息对象。 + * + * @param dto 登录/注册入参 + * @return 待入库的会员基础信息 + */ + private MemberBasicInfo buildNewMemberForRegisterAndLoginV2(MemberRegisterAndLoginDTO dto) { + String memberId = generateNewMemberId(); + MemberBasicInfo memberBasicInfo = new MemberBasicInfo(); + memberBasicInfo.setStatus(Constants.ONE); + memberBasicInfo.setMemberId(memberId); + memberBasicInfo.setNickName("会员" + memberId); + memberBasicInfo.setMobileNumber(dto.getMobileNumber()); + memberBasicInfo.setMerchantId(Long.valueOf(dto.getFirstLevelMerchantId())); + fillMemberThirdPartyIdentityForRegisterAndLoginV2(memberBasicInfo, dto); + return memberBasicInfo; + } + + /** + * 组装会员注册事务对象,必要时附带创建钱包信息。 + * + * @param memberBasicInfo 会员基础信息 + * @param firstLevelMerchantId 一级运营商 ID + * @return 注册事务对象 + */ + private MemberTransactionDTO buildMemberTransactionForRegisterAndLoginV2(MemberBasicInfo memberBasicInfo, String firstLevelMerchantId) { + MemberTransactionDTO memberTransactionDTO = new MemberTransactionDTO(); + memberTransactionDTO.setMemberBasicInfo(memberBasicInfo); + if (MerchantUtils.isXiXiaoMerchant(firstLevelMerchantId)) { + MemberWalletInfo memberWalletInfo = MemberWalletInfo.builder() + .memberId(memberBasicInfo.getMemberId()) + .merchantId(MerchantUtils.XIXIAO_MERCHANT_ID) + .walletCode(memberBasicInfoService.generateWalletCode()) + .build(); + memberTransactionDTO.setMemberWalletInfo(memberWalletInfo); + } + return memberTransactionDTO; + } + + /** + * 在注册发生唯一索引冲突时,回查已经被并发请求创建好的会员。 + * + * @param dto 登录/注册入参 + * @return 已存在的会员信息 + */ + private MemberBasicInfo reloadMemberAfterDuplicateKeyForRegisterAndLoginV2(MemberRegisterAndLoginDTO dto) { + log.warn("会员注册时检测到唯一索引冲突,重新查询已存在的会员, phoneNumber:{}, merchantId:{}", dto.getMobileNumber(), dto.getFirstLevelMerchantId()); + MemberBasicInfo memberBasicInfo = memberBasicInfoService.selectInfoByMobileNumber(dto.getMobileNumber(), dto.getFirstLevelMerchantId()); + if (memberBasicInfo == null) { + log.error("唯一索引冲突后重新查询会员信息为空, phoneNumber:{}, merchantId:{}", dto.getMobileNumber(), dto.getFirstLevelMerchantId()); + throw new BusinessException(ReturnCodeEnum.CODE_MEMBER_REGISTER_AND_LOGIN_ERROR); + } + return memberBasicInfo; + } + + /** + * 同步已有会员的第三方身份标识。 + * + * @param memberBasicInfo 已存在会员 + * @param dto 登录/注册入参 + */ + private void syncMemberThirdPartyIdentityForRegisterAndLoginV2(MemberBasicInfo memberBasicInfo, MemberRegisterAndLoginDTO dto) { + boolean needUpdate = false; + if (isWechatLiteRequest(dto) && !StringUtils.equals(memberBasicInfo.getOpenId(), dto.getOpenId())) { + memberBasicInfo.setOpenId(dto.getOpenId()); + needUpdate = true; + } + if (isAlipayLiteRequest(dto) && !StringUtils.equals(memberBasicInfo.getBuyerId(), dto.getBuyerId())) { + memberBasicInfo.setBuyerId(dto.getBuyerId()); + needUpdate = true; + } + if (needUpdate) { + memberBasicInfoService.updateMemberBasicInfo(memberBasicInfo); + } + } + + /** + * 在新建会员时,根据请求来源写入第三方身份标识。 + * + * @param memberBasicInfo 待入库会员 + * @param dto 登录/注册入参 + */ + private void fillMemberThirdPartyIdentityForRegisterAndLoginV2(MemberBasicInfo memberBasicInfo, MemberRegisterAndLoginDTO dto) { + if (isWechatLiteRequest(dto) && StringUtils.isNotBlank(dto.getOpenId())) { + memberBasicInfo.setOpenId(dto.getOpenId()); + } + if (isAlipayLiteRequest(dto) && StringUtils.isNotBlank(dto.getBuyerId())) { + memberBasicInfo.setBuyerId(dto.getBuyerId()); + } + } + + /** + * 判断当前请求是否来自微信小程序。 + * + * @param dto 登录/注册入参 + * @return 是否为微信小程序请求 + */ + private boolean isWechatLiteRequest(MemberRegisterAndLoginDTO dto) { + return AdapayPayChannelEnum.WX_LITE.getValue().equals(dto.getRequestSource()); + } + + /** + * 判断当前请求是否来自支付宝小程序。 + * + * @param dto 登录/注册入参 + * @return 是否为支付宝小程序请求 + */ + private boolean isAlipayLiteRequest(MemberRegisterAndLoginDTO dto) { + return AdapayPayChannelEnum.ALIPAY_LITE.getValue().equals(dto.getRequestSource()); + } + private String generateNewMemberId() { while (true) { String memberId = IdUtils.getMemberId(); diff --git a/jsowell-admin/src/main/java/com/jsowell/service/TempService.java b/jsowell-admin/src/main/java/com/jsowell/service/TempService.java index fd923ec6f..c70507198 100644 --- a/jsowell-admin/src/main/java/com/jsowell/service/TempService.java +++ b/jsowell-admin/src/main/java/com/jsowell/service/TempService.java @@ -1553,5 +1553,26 @@ public class TempService { } } + + /** + * 从文件中导入会员余额 + */ + public void batchImportMemberBalance(List list) { + if (CollectionUtils.isEmpty(list)) { + return; + } + list.parallelStream().forEach(this::importMemberBalance); + } + + private void importMemberBalance(ImportMemberBalanceDTO memberBalanceDTO) { + // 1. 根据手机号查询万车充会员信息 + MemberBasicInfo memberBasicInfo = memberBasicInfoService.selectInfoByMobileNumber(memberBalanceDTO.getPhone(), "1"); + // 2. 如果没有万车充会员信息,则自动注册万车充会员,并创建“南通晨鸣中锦置业有限责任公司”运营商钱包 + if (memberBasicInfo == null) { + + } + + // 3. 根据balance添加余额 + } } diff --git a/jsowell-admin/src/test/java/com/jsowell/service/MemberServiceRegisterAndLoginTest.java b/jsowell-admin/src/test/java/com/jsowell/service/MemberServiceRegisterAndLoginTest.java new file mode 100644 index 000000000..ca297aaae --- /dev/null +++ b/jsowell-admin/src/test/java/com/jsowell/service/MemberServiceRegisterAndLoginTest.java @@ -0,0 +1,275 @@ +package com.jsowell.service; + +import com.alipay.api.AlipayClient; +import com.jsowell.alipay.factory.AlipayClientFactory; +import com.jsowell.common.constant.CacheConstants; +import com.jsowell.common.core.redis.RedisCache; +import com.jsowell.common.enums.adapay.AdapayPayChannelEnum; +import com.jsowell.common.enums.ykc.ReturnCodeEnum; +import com.jsowell.common.exception.BusinessException; +import com.jsowell.common.util.JWTUtils; +import com.jsowell.pile.domain.MemberBasicInfo; +import com.jsowell.pile.dto.MemberRegisterAndLoginDTO; +import com.jsowell.pile.service.MemberBasicInfoService; +import com.jsowell.pile.transaction.dto.MemberTransactionDTO; +import com.jsowell.pile.transaction.service.TransactionService; +import com.jsowell.pile.util.MerchantUtils; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.dao.DuplicateKeyException; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class MemberServiceRegisterAndLoginTest { + + private static final LoginInvoker OLD_LOGIN = MemberService::memberRegisterAndLogin; + private static final LoginInvoker V2_LOGIN = MemberService::memberRegisterAndLoginV2; + + @BeforeAll + static void initJwtConfig() { + setStaticField(JWTUtils.class, "secret", "dGVzdC1zZWNyZXQ="); + setStaticField(JWTUtils.class, "serviceExpireTime", 60); + } + + @Test + void memberRegisterAndLoginV2_shouldCreateMemberAndWallet_whenXiXiaoMerchantFirstLogin() { + RedisCache redisCache = mock(RedisCache.class); + TransactionService transactionService = mock(TransactionService.class); + MemberBasicInfoService memberBasicInfoService = mock(MemberBasicInfoService.class); + + when(redisCache.lock(anyString(), anyString(), eq(60))).thenReturn(true); + when(memberBasicInfoService.selectInfoByMobileNumber("13800138000", MerchantUtils.XIXIAO_MERCHANT_ID)).thenReturn(null); + when(memberBasicInfoService.queryMemberInfoByMemberId(anyString())).thenReturn(null); + when(memberBasicInfoService.generateWalletCode()).thenReturn("WALLET-001"); + + MemberService service = newMemberService(redisCache, transactionService, memberBasicInfoService); + MemberRegisterAndLoginDTO dto = MemberRegisterAndLoginDTO.builder() + .mobileNumber("13800138000") + .firstLevelMerchantId(MerchantUtils.XIXIAO_MERCHANT_ID) + .openId("wx-open-id") + .requestSource(AdapayPayChannelEnum.WX_LITE.getValue()) + .build(); + + String memberToken = service.memberRegisterAndLoginV2(dto); + + assertNotNull(memberToken); + + ArgumentCaptor captor = ArgumentCaptor.forClass(MemberTransactionDTO.class); + verify(transactionService).createMember(captor.capture()); + + MemberTransactionDTO memberTransactionDTO = captor.getValue(); + assertNotNull(memberTransactionDTO.getMemberBasicInfo()); + assertEquals("13800138000", memberTransactionDTO.getMemberBasicInfo().getMobileNumber()); + assertEquals("wx-open-id", memberTransactionDTO.getMemberBasicInfo().getOpenId()); + assertNotNull(memberTransactionDTO.getMemberWalletInfo()); + assertEquals(MerchantUtils.XIXIAO_MERCHANT_ID, memberTransactionDTO.getMemberWalletInfo().getMerchantId()); + assertEquals("WALLET-001", memberTransactionDTO.getMemberWalletInfo().getWalletCode()); + verify(memberBasicInfoService, never()).updateMemberBasicInfo(any(MemberBasicInfo.class)); + } + + @Test + void memberRegisterAndLoginV2_shouldUpdateOpenId_whenExistingWechatMemberOpenIdChanged() { + RedisCache redisCache = mock(RedisCache.class); + TransactionService transactionService = mock(TransactionService.class); + MemberBasicInfoService memberBasicInfoService = mock(MemberBasicInfoService.class); + + when(redisCache.lock(anyString(), anyString(), eq(60))).thenReturn(true); + + MemberBasicInfo memberBasicInfo = MemberBasicInfo.builder() + .memberId("M001") + .nickName("会员M001") + .mobileNumber("13800138001") + .merchantId(99L) + .openId("old-open-id") + .build(); + when(memberBasicInfoService.selectInfoByMobileNumber("13800138001", "99")).thenReturn(memberBasicInfo); + + MemberService service = newMemberService(redisCache, transactionService, memberBasicInfoService); + MemberRegisterAndLoginDTO dto = MemberRegisterAndLoginDTO.builder() + .mobileNumber("13800138001") + .firstLevelMerchantId("99") + .openId("new-open-id") + .requestSource(AdapayPayChannelEnum.WX_LITE.getValue()) + .build(); + + String memberToken = service.memberRegisterAndLoginV2(dto); + + assertNotNull(memberToken); + assertEquals("new-open-id", memberBasicInfo.getOpenId()); + verify(memberBasicInfoService).updateMemberBasicInfo(memberBasicInfo); + verify(transactionService, never()).createMember(any(MemberTransactionDTO.class)); + } + + @Test + void memberRegisterAndLoginV2_shouldThrow_whenRegisterLockNotAcquired() { + RedisCache redisCache = mock(RedisCache.class); + TransactionService transactionService = mock(TransactionService.class); + MemberBasicInfoService memberBasicInfoService = mock(MemberBasicInfoService.class); + + when(redisCache.lock(anyString(), anyString(), eq(60))).thenReturn(false); + + MemberService service = newMemberService(redisCache, transactionService, memberBasicInfoService); + MemberRegisterAndLoginDTO dto = MemberRegisterAndLoginDTO.builder() + .mobileNumber("13800138002") + .firstLevelMerchantId("88") + .openId("wx-open-id") + .requestSource(AdapayPayChannelEnum.WX_LITE.getValue()) + .build(); + + BusinessException exception = assertThrows(BusinessException.class, () -> service.memberRegisterAndLoginV2(dto)); + + assertEquals(ReturnCodeEnum.CODE_MEMBER_REGISTER_AND_LOGIN_PROCESSING.getCode(), exception.getCode()); + verify(memberBasicInfoService, never()).selectInfoByMobileNumber(anyString(), anyString()); + verify(redisCache, never()).unLock(CacheConstants.USER_APP_REGISTER + "13800138002:88"); + verify(transactionService, never()).createMember(any(MemberTransactionDTO.class)); + } + + @Test + void memberRegisterAndLoginV2_shouldMatchOriginal_whenExistingAlipayMemberBuyerIdChanged() { + exerciseExistingAlipayBuyerIdUpdate(OLD_LOGIN); + exerciseExistingAlipayBuyerIdUpdate(V2_LOGIN); + } + + @Test + void memberRegisterAndLoginV2_shouldMatchOriginal_whenDuplicateKeyHappensDuringRegister() { + exerciseDuplicateKeyFallback(OLD_LOGIN); + exerciseDuplicateKeyFallback(V2_LOGIN); + } + + @Test + void memberRegisterAndLoginV2_shouldMatchOriginal_whenLockNotAcquired() { + exerciseLockFailure(OLD_LOGIN); + exerciseLockFailure(V2_LOGIN); + } + + private static void exerciseExistingAlipayBuyerIdUpdate(LoginInvoker invoker) { + RedisCache redisCache = mock(RedisCache.class); + TransactionService transactionService = mock(TransactionService.class); + MemberBasicInfoService memberBasicInfoService = mock(MemberBasicInfoService.class); + + when(redisCache.lock(anyString(), anyString(), eq(60))).thenReturn(true); + + MemberBasicInfo memberBasicInfo = MemberBasicInfo.builder() + .memberId("A001") + .nickName("会员A001") + .mobileNumber("13900139000") + .merchantId(66L) + .buyerId("old-buyer-id") + .build(); + when(memberBasicInfoService.selectInfoByMobileNumber("13900139000", "66")).thenReturn(memberBasicInfo); + + MemberService service = newMemberService(redisCache, transactionService, memberBasicInfoService); + MemberRegisterAndLoginDTO dto = MemberRegisterAndLoginDTO.builder() + .mobileNumber("13900139000") + .firstLevelMerchantId("66") + .buyerId("new-buyer-id") + .requestSource(AdapayPayChannelEnum.ALIPAY_LITE.getValue()) + .build(); + + String memberToken = invoker.invoke(service, dto); + + assertNotNull(memberToken); + assertEquals("new-buyer-id", memberBasicInfo.getBuyerId()); + verify(memberBasicInfoService).updateMemberBasicInfo(memberBasicInfo); + verify(transactionService, never()).createMember(any(MemberTransactionDTO.class)); + } + + private static void exerciseDuplicateKeyFallback(LoginInvoker invoker) { + RedisCache redisCache = mock(RedisCache.class); + TransactionService transactionService = mock(TransactionService.class); + MemberBasicInfoService memberBasicInfoService = mock(MemberBasicInfoService.class); + + when(redisCache.lock(anyString(), anyString(), eq(60))).thenReturn(true); + + MemberBasicInfo existingMember = MemberBasicInfo.builder() + .memberId("D001") + .nickName("会员D001") + .mobileNumber("13700137000") + .merchantId(77L) + .openId("wx-dup-open-id") + .build(); + when(memberBasicInfoService.selectInfoByMobileNumber("13700137000", "77")).thenReturn(null, existingMember); + when(memberBasicInfoService.queryMemberInfoByMemberId(anyString())).thenReturn(null); + doThrow(new DuplicateKeyException("duplicate key")).when(transactionService).createMember(any(MemberTransactionDTO.class)); + + MemberService service = newMemberService(redisCache, transactionService, memberBasicInfoService); + MemberRegisterAndLoginDTO dto = MemberRegisterAndLoginDTO.builder() + .mobileNumber("13700137000") + .firstLevelMerchantId("77") + .openId("wx-dup-open-id") + .requestSource(AdapayPayChannelEnum.WX_LITE.getValue()) + .build(); + + String memberToken = invoker.invoke(service, dto); + + assertNotNull(memberToken); + verify(transactionService).createMember(any(MemberTransactionDTO.class)); + verify(memberBasicInfoService, times(2)).selectInfoByMobileNumber("13700137000", "77"); + verify(memberBasicInfoService, never()).updateMemberBasicInfo(any(MemberBasicInfo.class)); + } + + private static void exerciseLockFailure(LoginInvoker invoker) { + RedisCache redisCache = mock(RedisCache.class); + TransactionService transactionService = mock(TransactionService.class); + MemberBasicInfoService memberBasicInfoService = mock(MemberBasicInfoService.class); + + when(redisCache.lock(anyString(), anyString(), eq(60))).thenReturn(false); + + MemberService service = newMemberService(redisCache, transactionService, memberBasicInfoService); + MemberRegisterAndLoginDTO dto = MemberRegisterAndLoginDTO.builder() + .mobileNumber("13600136000") + .firstLevelMerchantId("55") + .openId("wx-lock-open-id") + .requestSource(AdapayPayChannelEnum.WX_LITE.getValue()) + .build(); + + BusinessException exception = assertThrows(BusinessException.class, () -> invoker.invoke(service, dto)); + + assertEquals(ReturnCodeEnum.CODE_MEMBER_REGISTER_AND_LOGIN_PROCESSING.getCode(), exception.getCode()); + verify(memberBasicInfoService, never()).selectInfoByMobileNumber(anyString(), anyString()); + verify(transactionService, never()).createMember(any(MemberTransactionDTO.class)); + } + + private static MemberService newMemberService(RedisCache redisCache, + TransactionService transactionService, + MemberBasicInfoService memberBasicInfoService) { + AlipayClientFactory alipayClientFactory = mock(AlipayClientFactory.class); + when(alipayClientFactory.getAlipayClient()).thenReturn(mock(AlipayClient.class)); + + MemberService service = new MemberService(alipayClientFactory); + setField(service, "redisCache", redisCache); + setField(service, "transactionService", transactionService); + setField(service, "memberBasicInfoService", memberBasicInfoService); + return service; + } + + private static void setField(Object target, String fieldName, Object value) { + try { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void setStaticField(Class targetClass, String fieldName, Object value) { + try { + Field field = targetClass.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(null, value); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @FunctionalInterface + private interface LoginInvoker { + String invoke(MemberService service, MemberRegisterAndLoginDTO dto); + } +} diff --git a/jsowell-pile/src/main/java/com/jsowell/pile/dto/ImportMemberBalanceDTO.java b/jsowell-pile/src/main/java/com/jsowell/pile/dto/ImportMemberBalanceDTO.java new file mode 100644 index 000000000..b6089d61e --- /dev/null +++ b/jsowell-pile/src/main/java/com/jsowell/pile/dto/ImportMemberBalanceDTO.java @@ -0,0 +1,14 @@ +package com.jsowell.pile.dto; + +import lombok.Data; + +import java.math.BigDecimal; + +@Data +public class ImportMemberBalanceDTO { + // 手机号 + private String phone; + + // 余额 + private BigDecimal balance; +}