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;
+}