liujie
昨天 384bce80449b17651937429038c4370dbce60e7b
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
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
package com.ruoyi.user.service.impl;
 
import java.util.Collections;
import java.util.Date;
import java.security.SecureRandom;
import java.util.List;
import java.util.Objects;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.core.constant.UserConstants;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.DateUtils;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.user.domain.AccountAddress;
import com.ruoyi.user.config.UniversalCodeProperties;
import com.ruoyi.user.constant.ReferralConstants;
import com.ruoyi.user.domain.AccountUser;
import com.ruoyi.user.domain.vo.AccountUserDetailVo;
import com.ruoyi.user.domain.vo.MemberOptionVo;
import com.ruoyi.user.mapper.AccountAddressMapper;
import com.ruoyi.user.mapper.AccountAgreementMapper;
import com.ruoyi.user.mapper.AccountCertificationMapper;
import com.ruoyi.user.domain.vo.AccountUserInfoVo;
import com.ruoyi.user.domain.vo.EarningSummaryVo;
import com.ruoyi.user.enums.MemberLevelEnum;
import com.ruoyi.user.mapper.AccountEarningRecordMapper;
import com.ruoyi.user.mapper.AccountUserMapper;
import com.ruoyi.user.mapper.AccountWarehouseMapper;
import com.ruoyi.user.service.IAccountUserService;
 
/**
 * 会员用户 业务层实现
 *
 * @author ruoyi
 */
@Service
public class AccountUserServiceImpl implements IAccountUserService
{
    private static final Logger log = LoggerFactory.getLogger(AccountUserServiceImpl.class);
 
    @Autowired
    private AccountUserMapper accountUserMapper;
 
    @Autowired
    private AccountCertificationMapper accountCertificationMapper;
 
    @Autowired
    private AccountAddressMapper accountAddressMapper;
 
    @Autowired
    private AccountAgreementMapper accountAgreementMapper;
 
    @Autowired
    private AccountWarehouseMapper accountWarehouseMapper;
 
    @Autowired
    private AccountEarningRecordMapper accountEarningRecordMapper;
 
    @Autowired
    private UniversalCodeProperties universalCodeProperties;
 
    /** 推荐码随机源(SecureRandom 线程安全,避免每次创建) */
    private static final SecureRandom RANDOM = new SecureRandom();
 
    @Override
    public AccountUser selectAccountUserById(Long userId)
    {
        return accountUserMapper.selectAccountUserById(userId);
    }
 
    @Override
    public AccountUser selectAccountUserByPhone(String phone)
    {
        return accountUserMapper.selectAccountUserByPhone(phone);
    }
 
    @Override
    public List<AccountUser> selectAccountUserList(AccountUser accountUser)
    {
        return accountUserMapper.selectAccountUserList(accountUser);
    }
 
    @Override
    public boolean checkUsernameUnique(AccountUser accountUser)
    {
        Long userId = StringUtils.isNull(accountUser.getUserId()) ? -1L : accountUser.getUserId();
        AccountUser info = accountUserMapper.selectAccountUserByUsername(accountUser.getUserName());
        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
        {
            return UserConstants.NOT_UNIQUE;
        }
        return UserConstants.UNIQUE;
    }
 
    @Override
    public boolean checkPhoneUnique(AccountUser accountUser)
    {
        // 手机号非必填,为空时不做唯一性校验,否则会与库里其它空手机号记录相撞
        if (StringUtils.isEmpty(accountUser.getPhone()))
        {
            return UserConstants.UNIQUE;
        }
        Long userId = StringUtils.isNull(accountUser.getUserId()) ? -1L : accountUser.getUserId();
        AccountUser info = accountUserMapper.selectAccountUserByPhone(accountUser.getPhone());
        if (StringUtils.isNotNull(info) && info.getUserId().longValue() != userId.longValue())
        {
            return UserConstants.NOT_UNIQUE;
        }
        return UserConstants.UNIQUE;
    }
 
    @Override
    public int insertAccountUser(AccountUser accountUser)
    {
        // C端注册无密码,密码为空时跳过加密,避免 encryptPassword(null) 抛 NPE
        if (StringUtils.isNotEmpty(accountUser.getPassword()))
        {
            accountUser.setPassword(SecurityUtils.encryptPassword(accountUser.getPassword()));
        }
        return accountUserMapper.insertAccountUser(accountUser);
    }
 
    @Override
    public int updateAccountUser(AccountUser accountUser)
    {
        // 不为空才加密;为空时保持 null,mapper 的 <if> 会跳过该列,原密码不受影响
        if (StringUtils.isNotEmpty(accountUser.getPassword()))
        {
            accountUser.setPassword(SecurityUtils.encryptPassword(accountUser.getPassword()));
        }
        return accountUserMapper.updateAccountUser(accountUser);
    }
 
    @Override
    public int deleteAccountUserByIds(Long[] userIds)
    {
        return accountUserMapper.deleteAccountUserByIds(userIds);
    }
 
    @Override
    public AccountUserDetailVo selectAccountUserDetail(Long userId)
    {
        AccountUserDetailVo detail = new AccountUserDetailVo();
        detail.setUser(accountUserMapper.selectAccountUserVoById(userId));
        detail.setCertification(accountCertificationMapper.selectCertificationByUserId(userId));
        detail.setAddresses(accountAddressMapper.selectAddressByUserId(userId));
        detail.setAgreements(accountAgreementMapper.selectAgreementByUserId(userId));
        detail.setWarehouses(accountWarehouseMapper.selectWarehouseByUserId(userId));
        return detail;
    }
 
    @Override
    public int updateAccountUserRelation(AccountUser accountUser)
    {
        AccountUser old = accountUserMapper.selectAccountUserById(accountUser.getUserId());
        if (StringUtils.isNull(old))
        {
            return 0;
        }
        validateSupplier(old, accountUser.getSupplierId());
        // 绑定时间由服务端推导:换成另一个人的记为当前时间,解绑则清空,
        // 关系没动则保留原值——否则每次保存都会把绑定时间刷成今天。
        Date now = DateUtils.getNowDate();
        accountUser.setReferrerBindTime(
            resolveBindTime(old.getReferrerId(), accountUser.getReferrerId(), old.getReferrerBindTime(), now));
        accountUser.setSupplierBindTime(
            resolveBindTime(old.getSupplierId(), accountUser.getSupplierId(), old.getSupplierBindTime(), now));
        return accountUserMapper.updateUserRelation(accountUser);
    }
 
    @Override
    public List<MemberOptionVo> searchSupplierCandidates(Long userId, String keyword)
    {
        // 空关键字不回结果:候选是等级高于该会员的全量集合,放开等于把大半张会员表倒进下拉。
        // 与 AccountCityCompanyServiceImpl.searchL4Member 是同一口径(那边搜 L4,这边搜等级高于该会员)。
        if (StringUtils.isEmpty(keyword))
        {
            return Collections.emptyList();
        }
        AccountUser member = accountUserMapper.selectAccountUserById(userId);
        if (StringUtils.isNull(member))
        {
            throw new ServiceException("会员不存在");
        }
        // 严格高于该会员的等级集合;该会员已是最高等级时为空,必须提前返回——
        // 空集合会让候选 SQL 拼出 in () 的语法错误。
        List<String> levels = AccountUser.levelsAbove(member.getSystemLevel());
        if (levels.isEmpty())
        {
            return Collections.emptyList();
        }
        String kw = keyword.trim();
        // 纯数字时补一个 keywordId 走等值匹配(会员编号),非数字则不参与该列比较——
        // 写成 user_id like 或 cast(user_id as char) 都会让主键索引用不上。同 searchL4Member。
        Long keywordId = null;
        try
        {
            keywordId = Long.valueOf(kw);
        }
        catch (NumberFormatException ignored)
        {
            // 非纯数字:只走手机号/昵称/姓名模糊匹配
        }
        return accountUserMapper.selectSupplierCandidates(userId, levels, kw, keywordId);
    }
 
    /**
     * 校验新供货人的等级必须严格高于该会员(C < L1 < L2 < L3 < L4)。
     *
     * 这是提交时的第二道闸:弹窗只列出合法候选,但 /account/user/relation 是公开入口,
     * 绕过页面直接打接口也得拦住。解除供货(传 null)不需要校验。
     *
     * 与旧值相同时直接放行:存量关系是「历来没有等级限制」时期写下的(例如 L4 的供货人是 C),
     * 不该因为这次没换人被卡死——那是另一件事,按本项目的规矩也不在这里顺手修历史数据。
     */
    private void validateSupplier(AccountUser old, Long newSupplierId)
    {
        if (newSupplierId == null || newSupplierId.equals(old.getSupplierId()))
        {
            return;
        }
        // selectAccountUserById 的 where 自带 del_flag = '0',查不到即「不存在或已删除」
        AccountUser supplier = accountUserMapper.selectAccountUserById(newSupplierId);
        if (StringUtils.isNull(supplier))
        {
            throw new ServiceException("新供货人不存在");
        }
        if (!AccountUser.isHigherLevel(supplier.getSystemLevel(), old.getSystemLevel()))
        {
            throw new ServiceException("新供货人的系统等级必须高于该会员(该会员:" + old.getSystemLevel()
                + ",所选:" + supplier.getSystemLevel() + ")");
        }
    }
 
    @Override
    public int changeStatus(AccountUser accountUser)
    {
        // 本接口只服务列表页的「禁用/启用」,故目标状态只允许「正常/禁用」两态。
        // 注销中、已注销必须由注销审核流程驱动(见 AccountCancelApplyServiceImpl):
        // 若这里放开,「已注销」的会员一次普通请求就能被改回「正常」,等于绕过审核。
        String target = accountUser.getStatus();
        if (!AccountUser.STATUS_NORMAL.equals(target) && !AccountUser.STATUS_DISABLED.equals(target))
        {
            throw new ServiceException("账户状态只能改为「正常」或「禁用」");
        }
        AccountUser old = accountUserMapper.selectAccountUserById(accountUser.getUserId());
        if (StringUtils.isNull(old))
        {
            throw new ServiceException("会员不存在");
        }
        // 反向也要拦:注销中/已注销的会员不允许从列表页改回正常或禁用,否则审核结论形同虚设
        if (AccountUser.STATUS_CANCELING.equals(old.getStatus()) || AccountUser.STATUS_CANCELED.equals(old.getStatus()))
        {
            throw new ServiceException("该会员正在注销或已注销,账户状态只能由注销审核流程变更");
        }
        return accountUserMapper.updateUserStatus(accountUser);
    }
 
    @Override
    @Transactional
    public int updateAccountAddress(AccountAddress accountAddress)
    {
        // 置为默认前先清掉同会员其它行的默认标记:MySQL 没有部分唯一索引,
        // "每会员至多一条 is_default='1'"只能靠这两条语句在同一事务内保证。
        if ("1".equals(accountAddress.getIsDefault()))
        {
            accountAddressMapper.clearDefaultByUserId(accountAddress.getUserId());
        }
        int rows = accountAddressMapper.updateAddress(accountAddress);
        // updateAddress 的 where 带 user_id,id 与 userId 对不上时会更新 0 行。
        // 此时上面的默认标记已被清掉,必须抛异常回滚,
        // 否则该会员会落到"一个默认地址都没有"的状态。
        if (rows == 0)
        {
            throw new ServiceException("收货地址不存在或不属于该会员");
        }
        return rows;
    }
 
    @Override
    public int deleteAccountAddressByIds(Long[] ids)
    {
        return accountAddressMapper.deleteAddressByIds(ids);
    }
 
 
    private Date resolveBindTime(Long oldId, Long newId, Date oldTime, Date now)
    {
        if (newId == null)
        {
            return null;
        }
        // 关系未变时沿用原绑定时间;oldTime 为空(历史数据没记)则补当前时间
        if (Objects.equals(oldId, newId) && oldTime != null)
        {
            return oldTime;
        }
        return now;
    }
 
    @Override
    public AccountUser registerMember(String phone, String referralCode)
    {
        // STEP 1: 二次校验手机号未注册(防并发 / 防重复提交,网关侧已用分布式锁兜底)
        if (StringUtils.isNotNull(accountUserMapper.selectAccountUserByPhone(phone)))
        {
            throw new ServiceException("手机号已注册,请前往登录");
        }
        // STEP 2: 判定是否命中通用推荐码(命中则不绑定推荐人,且直接发放 L4 会员等级)
        boolean universalReferral = isUniversalReferralCode(referralCode);
        // STEP 3: 解析推荐人——命中通用推荐码则不绑定推荐人,否则按码定位并校验
        Long referrerUserId = resolveReferrerUserId(phone, referralCode, universalReferral);
        // STEP 4: 生成全局唯一的本人推荐码(库内查重,冲突重试)
        String myReferralCode = generateUniqueReferralCode();
        // STEP 5: 组装会员信息(C端无密码,账号=手机号;姓名注册未采集,落库留空,由用户后续在资料页完善)
        AccountUser accountUser = new AccountUser();
        accountUser.setUserName(phone);
        accountUser.setPhone(phone);
        accountUser.setStatus(UserConstants.NORMAL);
        accountUser.setMyReferralCode(myReferralCode);
        accountUser.setReferrerId(referrerUserId);
        // 通用推荐码注册的会员直接发放 L4 等级;其余不设等级,走库默认值 C(普通用户)
        if (universalReferral)
        {
            accountUser.setSystemLevel(MemberLevelEnum.L4.getCode());
        }
        accountUser.setCreateBy(phone);
        if (insertAccountUser(accountUser) <= 0)
        {
            throw new ServiceException("注册失败,请稍后重试");
        }
        return accountUser;
    }
 
    @Override
    public AccountUserInfoVo getAccountUserInfo(Long userId)
    {
        // STEP 1: 校验会员ID(登录态缺失时直接失败,避免无归属查询)
        if (StringUtils.isNull(userId))
        {
            throw new ServiceException("获取会员信息失败");
        }
        // STEP 2: 按会员ID查询会员(逻辑删除已在 SQL 中过滤)
        AccountUser accountUser = accountUserMapper.selectAccountUserById(userId);
        if (StringUtils.isNull(accountUser))
        {
            throw new ServiceException("获取会员信息失败");
        }
        // STEP 3: 查询收益汇总(单位分),无任何流水时直接返回全零,避免前端 null 分支
        EarningSummaryVo earnings = accountEarningRecordMapper.selectEarningSummary(userId);
        if (StringUtils.isNull(earnings))
        {
            earnings = EarningSummaryVo.zero();
        }
        // STEP 4: 组装展示对象;等级为空或非法值统一兜底为 C(普通用户);手机号脱敏后下发
        AccountUserInfoVo vo = new AccountUserInfoVo();
        vo.setUserId(accountUser.getUserId());
        vo.setNickname(accountUser.getNickname());
        vo.setPhone(maskPhone(accountUser.getPhone()));
        vo.setAvatar(accountUser.getAvatar());
        vo.setMemberLevel(MemberLevelEnum.normalize(accountUser.getSystemLevel()));
        vo.setEarnings(earnings);
        // STEP 5: 返回个人中心展示数据
        return vo;
    }
 
    /**
     * 判定是否为通用推荐码
     *
     * @param referralCode 注册提交的推荐码
     * @return true 表示命中通用推荐码
     */
    private boolean isUniversalReferralCode(String referralCode)
    {
        String universalCode = universalCodeProperties.getUniversalReferralCode();
        return StringUtils.isNotEmpty(universalCode) && universalCode.equals(referralCode);
    }
 
    /**
     * 解析注册时的推荐人用户ID
     *
     * 命中通用推荐码(测试便利)时跳过推荐人查询与校验,返回 null(不绑定推荐人);
     * 否则按推荐码定位推荐人,不存在或已停用则拒绝注册。
     *
     * @param phone 注册手机号(仅用于日志留痕,脱敏输出)
     * @param referralCode 推荐码
     * @param universalReferral 是否已命中通用推荐码
     * @return 推荐人用户ID;命中通用码时为 null
     */
    private Long resolveReferrerUserId(String phone, String referralCode, boolean universalReferral)
    {
        // 命中通用推荐码:不绑定推荐人
        if (universalReferral)
        {
            log.warn("[REFERRAL] universal code hit, not binding referrer, phone={}", maskPhone(phone));
            return null;
        }
        // 按推荐码定位推荐人,不存在或已停用则拒绝注册
        AccountUser referrer = accountUserMapper.selectAccountUserByReferralCode(referralCode);
        if (StringUtils.isNull(referrer))
        {
            throw new ServiceException("推荐人码无效");
        }
        if (UserConstants.USER_DISABLE.equals(referrer.getStatus()))
        {
            throw new ServiceException("推荐人账号异常");
        }
        return referrer.getUserId();
    }
 
    /**
     * 手机号脱敏,形如 138****0000
     *
     * @param phone 手机号
     * @return 脱敏后的手机号
     */
    private String maskPhone(String phone)
    {
        if (StringUtils.isEmpty(phone) || phone.length() < 7)
        {
            return phone;
        }
        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
    }
 
    /**
     * 生成库内唯一的推荐码
     *
     * @return 8 位大写字母数字推荐码
     */
    private String generateUniqueReferralCode()
    {
        String chars = ReferralConstants.CODE_CHARS;
        for (int retry = 0; retry < ReferralConstants.GEN_MAX_RETRY; retry++)
        {
            StringBuilder sb = new StringBuilder(ReferralConstants.CODE_LENGTH);
            for (int i = 0; i < ReferralConstants.CODE_LENGTH; i++)
            {
                sb.append(chars.charAt(RANDOM.nextInt(chars.length())));
            }
            String code = sb.toString();
            if (StringUtils.isNull(accountUserMapper.selectAccountUserByReferralCode(code)))
            {
                return code;
            }
        }
        throw new ServiceException("注册失败,请稍后重试");
    }
}