liujie
15 小时以前 fddf3d913d47f391f84f9e460cb76d427ef02c44
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
package com.ruoyi.user.service.impl;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
 
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.user.domain.AccountLevelApply;
import com.ruoyi.user.domain.AccountUser;
import com.ruoyi.user.mapper.AccountCertificationMapper;
import com.ruoyi.user.mapper.AccountLevelApplyMapper;
import com.ruoyi.user.mapper.AccountUserMapper;
import com.ruoyi.user.service.IAccountUserService;
 
/**
 * 等级升级审核服务单元测试
 *
 * 重点覆盖审核通过时的四条分支(无上级、上级 L2/L3/L4、上级 L1 有上级、上级 L1 无上级)
 * 与三类拒绝(降级、目标等级非法、供货关系异常),以及「算不出来就一行都不落」这条约定。
 *
 * @author hugray
 */
@ExtendWith(MockitoExtension.class)
class AccountLevelApplyServiceImplTest
{
    private static final Long APPLY_ID = 100L;
    private static final Long USER_ID = 300L;
 
    @Mock
    private AccountLevelApplyMapper levelApplyMapper;
 
    @Mock
    private AccountUserMapper accountUserMapper;
 
    @Mock
    private AccountCertificationMapper certificationMapper;
 
    @Mock
    private IAccountUserService accountUserService;
 
    @InjectMocks
    private AccountLevelApplyServiceImpl levelApplyService;
 
    private static AccountLevelApply applyOf(String targetLevel)
    {
        AccountLevelApply apply = new AccountLevelApply();
        apply.setId(APPLY_ID);
        apply.setUserId(USER_ID);
        apply.setCurrentLevel("C");
        apply.setTargetLevel(targetLevel);
        apply.setAuditStatus("0");
        return apply;
    }
 
    private static AccountUser userOf(String level, Long supplierId)
    {
        AccountUser user = new AccountUser();
        user.setUserId(USER_ID);
        user.setSystemLevel(level);
        user.setSupplierId(supplierId);
        // 推荐关系非空,用来验证换挂时 referrerId 被原样带上而不是被清空
        user.setReferrerId(777L);
        return user;
    }
 
    private static AccountUser memberOf(Long userId, String level, Long supplierId)
    {
        AccountUser member = new AccountUser();
        member.setUserId(userId);
        member.setSystemLevel(level);
        member.setSupplierId(supplierId);
        return member;
    }
 
    /** 捕获写入 account_user 的等级语句参数,供断言实际生效的等级 */
    private AccountUser captureLevelUpdate()
    {
        ArgumentCaptor<AccountUser> captor = ArgumentCaptor.forClass(AccountUser.class);
        verify(accountUserMapper).updateUserLevel(captor.capture());
        return captor.getValue();
    }
 
    @Test
    @DisplayName("审核结果既不是通过也不是驳回时应拒绝,且不碰任何数据")
    void audit_shouldThrow_whenStatusInvalid()
    {
        ServiceException ex = assertThrows(ServiceException.class,
            () -> levelApplyService.auditLevelApply(APPLY_ID, "3", "随便写写"));
        assertEquals("审核结果只能是「通过」或「驳回」", ex.getMessage());
        verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class));
    }
 
    @Test
    @DisplayName("驳回未填审核意见时应拒绝")
    void audit_shouldThrow_whenRejectWithoutRemark()
    {
        ServiceException ex = assertThrows(ServiceException.class,
            () -> levelApplyService.auditLevelApply(APPLY_ID, "2", "  "));
        assertEquals("驳回时必须填写审核意见", ex.getMessage());
        verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class));
    }
 
    @Test
    @DisplayName("申请不存在时应拒绝")
    void audit_shouldThrow_whenApplyMissing()
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(null);
        ServiceException ex = assertThrows(ServiceException.class,
            () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null));
        assertEquals("升级申请不存在", ex.getMessage());
    }
 
    @Test
    @DisplayName("并发闸门更新 0 行时应拒绝,且等级与关系一行都没动")
    void audit_shouldThrow_whenGateRejects()
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2"));
        when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", null));
        when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(0);
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            ServiceException ex = assertThrows(ServiceException.class,
                () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null));
            assertEquals("该申请已审核,不能重复处理", ex.getMessage());
        }
        // 闸门是第一条写语句,被挡住时后面两步不该执行
        verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class));
        verify(accountUserService, never()).updateAccountUserRelation(any(AccountUser.class));
    }
 
    @Test
    @DisplayName("驳回只改申请单,不读会员也不动等级与关系")
    void audit_reject_shouldOnlyUpdateApply()
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2"));
        when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1);
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            mocked.when(SecurityUtils::getUsername).thenReturn("admin");
            assertEquals(1, levelApplyService.auditLevelApply(APPLY_ID, "2", "条件未达标"));
 
            ArgumentCaptor<AccountLevelApply> captor = ArgumentCaptor.forClass(AccountLevelApply.class);
            verify(levelApplyMapper).auditLevelApply(captor.capture());
            // 驳回不产生生效结果,两列必须保持为空,否则事后分不清这单是没生效还是生效成了空
            assertNull(captor.getValue().getFinalLevel());
            assertNull(captor.getValue().getSupplierAfterId());
        }
        verify(accountUserMapper, never()).selectAccountUserById(any());
        verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class));
        verify(accountUserService, never()).updateAccountUserRelation(any(AccountUser.class));
    }
 
    @Test
    @DisplayName("无上级时通过应升到目标等级,且不改供货关系")
    void audit_passWithoutSupplier_shouldUseTargetLevel()
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2"));
        when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", null));
        when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1);
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            mocked.when(SecurityUtils::getUsername).thenReturn("admin");
            levelApplyService.auditLevelApply(APPLY_ID, "1", null);
 
            assertEquals("L2", captureLevelUpdate().getSystemLevel());
 
            ArgumentCaptor<AccountLevelApply> captor = ArgumentCaptor.forClass(AccountLevelApply.class);
            verify(levelApplyMapper).auditLevelApply(captor.capture());
            assertEquals("L2", captor.getValue().getFinalLevel());
            assertNull(captor.getValue().getSupplierAfterId());
        }
        verify(accountUserService, never()).updateAccountUserRelation(any(AccountUser.class));
    }
 
    @ParameterizedTest(name = "上级等级 {0} 时通过应压到 L1")
    @ValueSource(strings = { "L2", "L3", "L4" })
    @DisplayName("上级等级高于 L1 时通过应压到 L1,供货关系不动")
    void audit_passWithHigherSupplier_shouldForceL1(String supplierLevel)
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L4"));
        when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L));
        when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, supplierLevel, null));
        when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1);
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            mocked.when(SecurityUtils::getUsername).thenReturn("admin");
            levelApplyService.auditLevelApply(APPLY_ID, "1", null);
 
            // 目标等级是 L4,但有上级,故生效等级是 L1 —— 这正是必须留 final_level 的原因
            assertEquals("L1", captureLevelUpdate().getSystemLevel());
 
            ArgumentCaptor<AccountLevelApply> captor = ArgumentCaptor.forClass(AccountLevelApply.class);
            verify(levelApplyMapper).auditLevelApply(captor.capture());
            assertEquals("L1", captor.getValue().getFinalLevel());
            assertEquals(301L, captor.getValue().getSupplierAfterId());
        }
        // 上级仍严格高于 L1,关系合法,不该触发换挂
        verify(accountUserService, never()).updateAccountUserRelation(any(AccountUser.class));
    }
 
    @Test
    @DisplayName("上级是 L1 且有上级时通过应换挂到上级的供货人,且推荐关系原样保留")
    void audit_passWithL1Supplier_shouldRebindToGrandSupplier()
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2"));
        when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L));
        when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, "L1", 302L));
        when(accountUserMapper.selectAccountUserById(302L)).thenReturn(memberOf(302L, "L3", null));
        when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1);
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            mocked.when(SecurityUtils::getUsername).thenReturn("admin");
            levelApplyService.auditLevelApply(APPLY_ID, "1", null);
 
            assertEquals("L1", captureLevelUpdate().getSystemLevel());
 
            ArgumentCaptor<AccountUser> captor = ArgumentCaptor.forClass(AccountUser.class);
            verify(accountUserService).updateAccountUserRelation(captor.capture());
            assertEquals(302L, captor.getValue().getSupplierId());
            // 换挂走的是无条件赋值 4 列的语句,referrerId 漏传就是静默解除推荐关系
            assertEquals(777L, captor.getValue().getReferrerId());
 
            ArgumentCaptor<AccountLevelApply> applyCaptor = ArgumentCaptor.forClass(AccountLevelApply.class);
            verify(levelApplyMapper).auditLevelApply(applyCaptor.capture());
            assertEquals(302L, applyCaptor.getValue().getSupplierAfterId());
        }
    }
 
    @Test
    @DisplayName("上级是 L1 且自己也没有上级时通过应解绑供货人")
    void audit_passWithL1SupplierNoGrand_shouldUnbind()
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L3"));
        when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L));
        when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, "L1", null));
        when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1);
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            mocked.when(SecurityUtils::getUsername).thenReturn("admin");
            levelApplyService.auditLevelApply(APPLY_ID, "1", null);
 
            ArgumentCaptor<AccountUser> captor = ArgumentCaptor.forClass(AccountUser.class);
            verify(accountUserService).updateAccountUserRelation(captor.capture());
            assertNull(captor.getValue().getSupplierId());
            assertEquals(777L, captor.getValue().getReferrerId());
 
            ArgumentCaptor<AccountLevelApply> applyCaptor = ArgumentCaptor.forClass(AccountLevelApply.class);
            verify(levelApplyMapper).auditLevelApply(applyCaptor.capture());
            assertNull(applyCaptor.getValue().getSupplierAfterId());
        }
    }
 
    @Test
    @DisplayName("换挂时应先写等级再写关系,关系校验才按升级后的等级为基线")
    void audit_passWithRebind_shouldWriteLevelBeforeRelation()
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2"));
        when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L));
        when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, "L1", 302L));
        when(accountUserMapper.selectAccountUserById(302L)).thenReturn(memberOf(302L, "L2", null));
        when(levelApplyMapper.auditLevelApply(any(AccountLevelApply.class))).thenReturn(1);
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            mocked.when(SecurityUtils::getUsername).thenReturn("admin");
            levelApplyService.auditLevelApply(APPLY_ID, "1", null);
        }
 
        InOrder inOrder = inOrder(accountUserMapper, accountUserService);
        inOrder.verify(accountUserMapper).updateUserLevel(any(AccountUser.class));
        inOrder.verify(accountUserService).updateAccountUserRelation(any(AccountUser.class));
    }
 
    @Test
    @DisplayName("供货人已被逻辑删除时应拒绝且不落库,不静默当「无上级」")
    void audit_shouldThrow_whenSupplierMissing()
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2"));
        when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L));
        // selectAccountUserById 的 where 自带 del_flag='0',已删除的供货人查不到
        when(accountUserMapper.selectAccountUserById(301L)).thenReturn(null);
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            ServiceException ex = assertThrows(ServiceException.class,
                () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null));
            assertEquals("该会员的供货人(编号 301)不存在或已注销,请先修正供货关系再审核", ex.getMessage());
        }
        verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class));
        verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class));
    }
 
    @Test
    @DisplayName("换挂后的新供货人等级仍不高于生效等级时应拒绝(含自环脏数据)")
    void audit_shouldThrow_whenGrandSupplierNotHigher()
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2"));
        when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", 301L));
        when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, "L1", 302L));
        // 302 是 L1,不严格高于生效等级 L1:换挂等于白换,还是一条非法关系
        when(accountUserMapper.selectAccountUserById(302L)).thenReturn(memberOf(302L, "L1", null));
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            ServiceException ex = assertThrows(ServiceException.class,
                () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null));
            assertEquals("该会员上级(编号 301)的供货关系异常,换挂后等级仍不合法,请先修正关系", ex.getMessage());
        }
        verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class));
        verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class));
    }
 
    @Test
    @DisplayName("按规则算出的等级低于会员当前等级时应拒绝(升级审核不支持降级)")
    void audit_shouldThrow_whenDowngrade()
    {
        // 会员已是 L2,有上级(L4),按规则通过后是 L1 —— 低于当前等级
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L3"));
        when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("L2", 301L));
        when(accountUserMapper.selectAccountUserById(301L)).thenReturn(memberOf(301L, "L4", null));
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            ServiceException ex = assertThrows(ServiceException.class,
                () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null));
            assertEquals("按当前规则通过后等级为 L1,低于该会员现有等级 L2,升级审核不支持降级,请驳回该申请",
                ex.getMessage());
        }
        verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class));
        verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class));
    }
 
    @Test
    @DisplayName("目标等级不在 C/L1~L4 之内时应拒绝(无上级分支会把它直接落到会员身上)")
    void audit_shouldThrow_whenTargetLevelInvalid()
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L5"));
        when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(userOf("C", null));
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            ServiceException ex = assertThrows(ServiceException.class,
                () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null));
            assertEquals("申请的目标等级不合法:L5", ex.getMessage());
        }
        verify(accountUserMapper, never()).updateUserLevel(any(AccountUser.class));
    }
 
    @Test
    @DisplayName("申请人已被逻辑删除时应拒绝")
    void audit_shouldThrow_whenApplicantMissing()
    {
        when(levelApplyMapper.selectLevelApplyById(APPLY_ID)).thenReturn(applyOf("L2"));
        when(accountUserMapper.selectAccountUserById(USER_ID)).thenReturn(null);
 
        try (MockedStatic<SecurityUtils> mocked = mockStatic(SecurityUtils.class))
        {
            ServiceException ex = assertThrows(ServiceException.class,
                () -> levelApplyService.auditLevelApply(APPLY_ID, "1", null));
            assertEquals("申请人不存在或已注销", ex.getMessage());
        }
        verify(levelApplyMapper, never()).auditLevelApply(any(AccountLevelApply.class));
    }
}