Эх сурвалжийг харах

Merge remote-tracking branch 'origin/feature/mall_product' into member_dev

# Conflicts:
#	yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImpl.java
owen 1 жил өмнө
parent
commit
b7f54a3061

+ 180 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayClientTest.java

@@ -0,0 +1,180 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
+
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
+import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
+import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.DefaultSigner;
+import com.alipay.api.domain.AlipayTradeRefundModel;
+import com.alipay.api.request.AlipayTradeRefundRequest;
+import com.alipay.api.response.AlipayTradeRefundResponse;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentMatcher;
+import org.mockito.Mock;
+
+import javax.validation.ConstraintViolationException;
+import java.util.Date;
+
+import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author jason
+ */
+public abstract class AbstractAlipayClientTest extends BaseMockitoUnitTest {
+
+    private final String privateKey = randomString();
+
+    protected AlipayPayClientConfig config = randomPojo(AlipayPayClientConfig.class, t -> {
+        t.setServerUrl(randomURL());
+        t.setPrivateKey(privateKey);
+        t.setMode(MODE_PUBLIC_KEY);
+        t.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
+        t.setAppCertContent("");
+        t.setAlipayPublicCertContent("");
+        t.setRootCertContent("");
+    });
+
+    @Mock
+    protected DefaultAlipayClient defaultAlipayClient;
+
+    private AbstractAlipayPayClient client;
+
+    public void setClient(AbstractAlipayPayClient client) {
+        this.client = client;
+    }
+
+    @Test
+    @DisplayName("支付宝 Client 初始化")
+    public void test_do_init() {
+        client.doInit();
+        DefaultAlipayClient realClient = (DefaultAlipayClient) ReflectUtil.getFieldValue(client, "client");
+        assertNotSame(defaultAlipayClient, realClient);
+        assertInstanceOf(DefaultSigner.class, realClient.getSigner());
+        assertEquals(privateKey, ((DefaultSigner) realClient.getSigner()).getPrivateKey());
+    }
+
+    @Test
+    @DisplayName("支付宝 Client 统一退款成功")
+    public void test_unified_refund_success() throws AlipayApiException {
+        // 准备返回对象
+        String notifyUrl = randomURL();
+        Date refundTime = randomDate();
+        String outRefundNo = randomString();
+        String outTradeNo = randomString();
+        Integer refundAmount = randomInteger();
+        AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> {
+            o.setSubCode("");
+            o.setGmtRefundPay(refundTime);
+        });
+        // mock
+        when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> {
+            assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel());
+            AlipayTradeRefundModel bizModel = (AlipayTradeRefundModel) request.getBizModel();
+            assertEquals(outRefundNo, bizModel.getOutRequestNo());
+            assertEquals(outTradeNo, bizModel.getOutTradeNo());
+            assertEquals(String.valueOf(refundAmount / 100.0), bizModel.getRefundAmount());
+            return true;
+        }))).thenReturn(response);
+        // 准备请求参数
+        PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
+            o.setOutRefundNo(outRefundNo);
+            o.setOutTradeNo(outTradeNo);
+            o.setNotifyUrl(notifyUrl);
+            o.setRefundPrice(refundAmount);
+        });
+        PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO);
+        // 断言
+        assertEquals(PayRefundStatusRespEnum.SUCCESS.getStatus(), resp.getStatus());
+        assertNull(resp.getChannelRefundNo());
+        assertEquals(LocalDateTimeUtil.of(refundTime), resp.getSuccessTime());
+        assertEquals(outRefundNo, resp.getOutRefundNo());
+        assertSame(response, resp.getRawData());
+    }
+
+    @Test
+    @DisplayName("支付宝 Client 统一退款,渠道返回失败")
+    public void test_unified_refund_channel_failed() throws AlipayApiException {
+        // 准备返回对象
+        String notifyUrl = randomURL();
+        String subCode = randomString();
+        String subMsg = randomString();
+        AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> {
+            o.setSubCode(subCode);
+            o.setSubMsg(subMsg);
+        });
+        // mock
+        when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> {
+            assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel());
+            return true;
+        }))).thenReturn(response);
+        // 准备请求参数
+        String outRefundNo = randomString();
+        String outTradeNo = randomString();
+        PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
+            o.setOutRefundNo(outRefundNo);
+            o.setOutTradeNo(outTradeNo);
+            o.setNotifyUrl(notifyUrl);
+        });
+        PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO);
+        // 断言
+        assertEquals(PayRefundStatusRespEnum.FAILURE.getStatus(), resp.getStatus());
+        assertNull(resp.getChannelRefundNo());
+        assertEquals(subCode, resp.getChannelErrorCode());
+        assertEquals(subMsg, resp.getChannelErrorMsg());
+        assertNull(resp.getSuccessTime());
+        assertEquals(outRefundNo, resp.getOutRefundNo());
+        assertSame(response, resp.getRawData());
+    }
+
+    @Test
+    @DisplayName("支付宝 Client 统一退款,参数校验不通过")
+    public void test_unified_refund_param_validate() {
+        // 准备请求参数
+        String notifyUrl = randomURL();
+        PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
+            o.setOutTradeNo("");
+            o.setNotifyUrl(notifyUrl);
+        });
+        // 断言
+        assertThrows(ConstraintViolationException.class, () -> client.unifiedRefund(refundReqDTO));
+    }
+
+    @Test
+    @DisplayName("支付宝 Client 统一退款,抛出业务异常")
+    public void test_unified_refund_throw_service_exception() throws AlipayApiException {
+        // mock
+        when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> true)))
+                .thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR));
+        // 准备请求参数
+        String notifyUrl = randomURL();
+        PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl));
+        // 断言
+        assertThrows(ServiceException.class, () -> client.unifiedRefund(refundReqDTO));
+    }
+
+    @Test
+    @DisplayName("支付宝 Client 统一退款,抛出系统异常")
+    public void test_unified_refund_throw_pay_exception() throws AlipayApiException {
+        // mock
+        when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> true)))
+                .thenThrow(new RuntimeException("系统异常"));
+        // 准备请求参数
+        String notifyUrl = randomURL();
+        PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> o.setNotifyUrl(notifyUrl));
+        // 断言
+        assertThrows(PayException.class, () -> client.unifiedRefund(refundReqDTO));
+    }
+}

+ 7 - 146
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java

@@ -1,68 +1,44 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 
-import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.util.RandomUtil;
-import cn.hutool.core.util.ReflectUtil;
 import cn.iocoder.yudao.framework.common.exception.ServiceException;
 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
-import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import com.alipay.api.AlipayApiException;
-import com.alipay.api.DefaultAlipayClient;
-import com.alipay.api.domain.AlipayTradeRefundModel;
 import com.alipay.api.request.AlipayTradePrecreateRequest;
-import com.alipay.api.request.AlipayTradeRefundRequest;
 import com.alipay.api.response.AlipayTradePrecreateResponse;
-import com.alipay.api.response.AlipayTradeRefundResponse;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentMatcher;
 import org.mockito.InjectMocks;
-import org.mockito.Mock;
 
 import javax.validation.ConstraintViolationException;
-import java.util.Date;
 
-import static cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_PUBLIC_KEY;
 import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.CLOSED;
 import static cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum.WAITING;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.when;
+
 /**
  * {@link AlipayQrPayClient} 单元测试
  *
  * @author jason
  */
-public class AlipayQrPayClientTest extends BaseMockitoUnitTest {
-
-    private final AlipayPayClientConfig config = randomPojo(AlipayPayClientConfig.class, t -> {
-        t.setServerUrl(randomURL());
-        t.setMode(MODE_PUBLIC_KEY);
-        t.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT);
-        t.setAppCertContent("");
-        t.setAlipayPublicCertContent("");
-        t.setRootCertContent("");
-    });
+public class AlipayQrPayClientTest extends AbstractAlipayClientTest {
 
     @InjectMocks
-    AlipayQrPayClient client = new AlipayQrPayClient(randomLongId(), config);
-
-    @Mock
-    private DefaultAlipayClient defaultAlipayClient;
+    private AlipayQrPayClient client = new AlipayQrPayClient(randomLongId(), config);
 
-    @Test
-    public void test_do_init() {
-        client.doInit();
-        assertNotSame(defaultAlipayClient, ReflectUtil.getFieldValue(client, "defaultAlipayClient"));
+    @BeforeEach
+    public void setUp() {
+        setClient(client);
     }
 
     @Test
@@ -176,119 +152,4 @@ public class AlipayQrPayClientTest extends BaseMockitoUnitTest {
             o.setBody(RandomUtil.randomString(32));
         });
     }
-
-    @Test
-    @DisplayName("支付包扫描退款成功")
-    public void test_unified_refund_success() throws AlipayApiException {
-        // 准备返回对象
-        String notifyUrl = randomURL();
-        Date refundTime = randomDate();
-        String outRefundNo = randomString();
-        String outTradeNo = randomString();
-        Integer refundAmount = randomInteger();
-        AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> {
-            o.setSubCode("");
-            o.setGmtRefundPay(refundTime);
-        });
-        // mock
-        when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> {
-            assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel());
-            AlipayTradeRefundModel bizModel = (AlipayTradeRefundModel) request.getBizModel();
-            assertEquals(outRefundNo, bizModel.getOutRequestNo());
-            assertEquals(outTradeNo, bizModel.getOutTradeNo());
-            assertEquals(String.valueOf(refundAmount / 100.0), bizModel.getRefundAmount());
-            return true;
-        }))).thenReturn(response);
-        // 准备请求参数
-        PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
-            o.setOutRefundNo(outRefundNo);
-            o.setOutTradeNo(outTradeNo);
-            o.setNotifyUrl(notifyUrl);
-            o.setRefundPrice(refundAmount);
-        });
-        PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO);
-        // 断言
-        assertEquals(PayRefundStatusRespEnum.SUCCESS.getStatus(), resp.getStatus());
-        assertNull(resp.getChannelRefundNo());
-        assertEquals(LocalDateTimeUtil.of(refundTime), resp.getSuccessTime());
-        assertEquals(outRefundNo, resp.getOutRefundNo());
-        assertSame(response, resp.getRawData());
-    }
-
-    @Test
-    @DisplayName("支付包扫描退款,渠道返回失败")
-    public void test_unified_refund_channel_failed() throws AlipayApiException {
-        // 准备返回对象
-        String notifyUrl = randomURL();
-        String subCode = randomString();
-        String subMsg = randomString();
-        AlipayTradeRefundResponse response = randomPojo(AlipayTradeRefundResponse.class, o -> {
-            o.setSubCode(subCode);
-            o.setSubMsg(subMsg);
-        });
-        // mock
-        when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> {
-            assertInstanceOf(AlipayTradeRefundModel.class, request.getBizModel());
-            return true;
-        }))).thenReturn(response);
-        // 准备请求参数
-        String outRefundNo = randomString();
-        String outTradeNo = randomString();
-        PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
-            o.setOutRefundNo(outRefundNo);
-            o.setOutTradeNo(outTradeNo);
-            o.setNotifyUrl(notifyUrl);
-        });
-        PayRefundRespDTO resp = client.unifiedRefund(refundReqDTO);
-        // 断言
-        assertEquals(PayRefundStatusRespEnum.FAILURE.getStatus(), resp.getStatus());
-        assertNull(resp.getChannelRefundNo());
-        assertEquals(subCode, resp.getChannelErrorCode());
-        assertEquals(subMsg, resp.getChannelErrorMsg());
-        assertNull(resp.getSuccessTime());
-        assertEquals(outRefundNo, resp.getOutRefundNo());
-        assertSame(response, resp.getRawData());
-    }
-
-    @Test
-    @DisplayName("支付包扫描退款,参数校验不通过")
-    public void test_unified_refund_param_validate() {
-        // 准备请求参数
-        String notifyUrl = randomURL();
-        PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
-            o.setOutTradeNo("");
-            o.setNotifyUrl(notifyUrl);
-        });
-        // 断言
-        assertThrows(ConstraintViolationException.class, () -> client.unifiedRefund(refundReqDTO));
-    }
-
-    @Test
-    @DisplayName("支付包扫描退款,抛出业务异常")
-    public void test_unified_refund_throw_service_exception() throws AlipayApiException {
-        // mock
-        when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> true)))
-                .thenThrow(ServiceExceptionUtil.exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR));
-        // 准备请求参数
-        String notifyUrl = randomURL();
-        PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
-            o.setNotifyUrl(notifyUrl);
-        });
-        // 断言
-        assertThrows(ServiceException.class, () -> client.unifiedRefund(refundReqDTO));
-    }
-    @Test
-    @DisplayName("支付包扫描退款,抛出系统异常")
-    public void test_unified_refund_throw_pay_exception() throws AlipayApiException {
-        // mock
-        when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeRefundRequest>) request -> true)))
-                .thenThrow(new RuntimeException("系统异常"));
-        // 准备请求参数
-        String notifyUrl = randomURL();
-        PayRefundUnifiedReqDTO refundReqDTO = randomPojo(PayRefundUnifiedReqDTO.class, o -> {
-            o.setNotifyUrl(notifyUrl);
-        });
-        // 断言
-        assertThrows(PayException.class, () -> client.unifiedRefund(refundReqDTO));
-    }
 }

+ 1 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/dal/dataobject/level/MemberLevelRecordDO.java

@@ -52,6 +52,7 @@ public class MemberLevelRecordDO extends BaseDO {
      * 会员此时的经验
      */
     private Integer userExperience;
+    // TODO @疯狂:是不是 remark 和 description 可以合并成 description 就够了
     /**
      * 备注
      */

+ 10 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberExperienceRecordService.java

@@ -39,6 +39,16 @@ public interface MemberExperienceRecordService {
      */
     PageResult<MemberExperienceRecordDO> getExperienceLogPage(MemberExperienceRecordPageReqVO pageReqVO);
 
+    /**
+     * 获得会员经验记录列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 会员经验记录列表
+     */
+    List<MemberExperienceLogDO> getExperienceLogList(MemberExperienceLogExportReqVO exportReqVO);
+
+    // TODO @疯狂:类似 MemberLevelLogService 的方法,这里也需要提供一个通用的方法,用于创建经验变动记录
+
     /**
      * 创建 手动调整 经验变动记录
      *

+ 10 - 0
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelRecordService.java

@@ -47,6 +47,16 @@ public interface MemberLevelRecordService {
      */
     PageResult<MemberLevelRecordDO> getLevelLogPage(MemberLevelRecordPageReqVO pageReqVO);
 
+    /**
+     * 获得会员等级记录列表, 用于 Excel 导出
+     *
+     * @param exportReqVO 查询条件
+     * @return 会员等级记录列表
+     */
+    List<MemberLevelLogDO> getLevelLogList(MemberLevelLogExportReqVO exportReqVO);
+
+    // TODO @疯狂:把 createCancelLog、createAdjustLog、createAutoUpgradeLog 几个日志合并成一个通用的日志方法;整体的内容,交给 MemberLevelService 去做;以及对应的 level 变化的通知;
+
     /**
      * 创建记录: 取消等级
      *

+ 16 - 20
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/service/level/MemberLevelServiceImpl.java

@@ -141,7 +141,6 @@ public class MemberLevelServiceImpl implements MemberLevelService {
     @VisibleForTesting
     void validateConfigValid(Long id, String name, Integer level, Integer experience) {
         List<MemberLevelDO> list = levelMapper.selectList();
-
         // 校验名称唯一
         validateNameUnique(list, id, name);
         // 校验等级唯一
@@ -178,19 +177,14 @@ public class MemberLevelServiceImpl implements MemberLevelService {
         return levelMapper.selectListByStatus(status);
     }
 
-    @Transactional(rollbackFor = Exception.class)
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void updateUserLevel(MemberUserUpdateLevelReqVO updateReqVO) {
         MemberUserDO user = memberUserMapper.selectById(updateReqVO.getId());
         if (user == null) {
             throw exception(USER_NOT_EXISTS);
         }
-
-        // 未调整的情况1
-        if (user.getLevelId() == null && updateReqVO.getLevelId() == null) {
-            return;
-        }
-        // 未调整的情况2
+        // 等级未发生变化
         if (ObjUtil.equal(user.getLevelId(), updateReqVO.getLevelId())) {
             return;
         }
@@ -218,36 +212,40 @@ public class MemberLevelServiceImpl implements MemberLevelService {
             updateUserLevelIdAndExperience(user.getId(), updateReqVO.getLevelId(), totalExperience);
         }
 
-
         // 记录会员经验变动
         memberExperienceRecordService.createAdjustLog(user.getId(), experience, totalExperience);
     }
 
-    @Transactional(rollbackFor = Exception.class)
+    // TODO @疯狂:方法名,建议改成 increase 或者 add 经验,和项目更统一一些
+    // TODO @疯狂:bizType 改成具体数值,主要是枚举在 api 不好传递,rpc 情况下
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public void plusExperience(Long userId, Integer experience, MemberExperienceBizTypeEnum bizType, String bizId) {
         if (experience == 0) {
             return;
         }
 
         MemberUserDO user = memberUserMapper.selectById(userId);
-        if (user.getExperience() == null) {
-            user.setExperience(0);
-        }
 
         // 防止扣出负数
         int userExperience = NumberUtil.max(user.getExperience() + experience, 0);
-
         // 创建经验记录
         memberExperienceRecordService.createBizLog(userId, experience, userExperience, bizType, bizId);
 
         // 计算会员等级
-        Long levelId = calcLevel(user, userExperience);
+        MemberLevelDO newLevel = calculateNewLevel(user, userExperience);
+        Long newLevelId = null;
+        if (newLevel != null) {
+            newLevelId = newLevel.getId();
+            // 保存等级变更记录
+            memberLevelRecordService.createAutoUpgradeLog(user, newLevel);
+        }
 
         // 更新会员表上的等级编号、经验值
-        updateUserLevelIdAndExperience(user.getId(), levelId, userExperience);
+        updateUserLevelIdAndExperience(user.getId(), newLevelId, userExperience);
     }
 
+    // TODO @疯狂:让 memberUserService 那开个方法;每个模块,不直接操作对方的 mapper;
     private void updateUserLevelIdAndExperience(Long userId, Long levelId, Integer experience) {
         memberUserMapper.updateById(new MemberUserDO()
                 .setId(userId)
@@ -262,7 +260,7 @@ public class MemberLevelServiceImpl implements MemberLevelService {
      * @param userExperience 会员当前的经验值
      * @return 会员等级编号,null表示无变化
      */
-    private Long calcLevel(MemberUserDO user, int userExperience) {
+    private MemberLevelDO calculateNewLevel(MemberUserDO user, int userExperience) {
         List<MemberLevelDO> list = getEnableLevelList();
         if (CollUtil.isEmpty(list)) {
             log.warn("计算会员等级失败:会员等级配置不存在");
@@ -283,8 +281,6 @@ public class MemberLevelServiceImpl implements MemberLevelService {
             return null;
         }
 
-        // 保存等级变更记录
-        memberLevelRecordService.createAutoUpgradeLog(user, matchLevel);
-        return matchLevel.getId();
+        return matchLevel;
     }
 }