| | |
| | | package com.ruoyi.user.service.impl; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Date; |
| | | import java.security.SecureRandom; |
| | |
| | | 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.AccountInviteeVo; |
| | | import com.ruoyi.user.domain.vo.AccountReferrerVo; |
| | | import com.ruoyi.user.domain.vo.AccountUserDetailVo; |
| | | import com.ruoyi.user.domain.vo.InviteCodeVo; |
| | | import com.ruoyi.user.domain.vo.MemberOptionVo; |
| | | import com.ruoyi.user.mapper.AccountAddressMapper; |
| | | import com.ruoyi.user.mapper.AccountAgreementMapper; |
| | |
| | | /** 推荐码随机源(SecureRandom 线程安全,避免每次创建) */ |
| | | private static final SecureRandom RANDOM = new SecureRandom(); |
| | | |
| | | /** 头像地址最大长度(对应 account_user.avatar varchar(255),超长直接拒绝避免截断) */ |
| | | private static final int AVATAR_MAX_LENGTH = 255; |
| | | |
| | | /** 认证状态:未认证(auth_status 为空时的兜底值) */ |
| | | private static final String AUTH_STATUS_NONE = "0"; |
| | | |
| | | @Override |
| | | public AccountUser selectAccountUserById(Long userId) |
| | | { |
| | |
| | | public AccountUser selectAccountUserByPhone(String phone) |
| | | { |
| | | return accountUserMapper.selectAccountUserByPhone(phone); |
| | | } |
| | | |
| | | @Override |
| | | public AccountUser selectAccountUserByUsername(String userName) |
| | | { |
| | | return accountUserMapper.selectAccountUserByUsername(userName); |
| | | } |
| | | |
| | | @Override |
| | |
| | | return accountAddressMapper.deleteAddressByIds(ids); |
| | | } |
| | | |
| | | @Override |
| | | public List<AccountAddress> getAddressList(Long userId) |
| | | { |
| | | return accountAddressMapper.selectAddressByUserId(userId); |
| | | } |
| | | |
| | | @Override |
| | | public AccountAddress getAddressDetail(Long userId, Long id) |
| | | { |
| | | // 编辑页回填:需校验归属,防止拿到他人地址 |
| | | return queryOwnedAddress(userId, id); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional |
| | | public int insertMyAddress(AccountAddress accountAddress) |
| | | { |
| | | // STEP 1: 会员身份防御性校验(正常由登录态注入,必非空) |
| | | if (accountAddress.getUserId() == null) |
| | | { |
| | | throw new ServiceException("会员身份缺失"); |
| | | } |
| | | // STEP 2: 是否默认完全以用户勾选为准,不做「首条自动默认」的强制改写; |
| | | // 为空时明确兜底为 0,避免落库为 null 影响前端判定 |
| | | if (!"1".equals(accountAddress.getIsDefault())) |
| | | { |
| | | accountAddress.setIsDefault("0"); |
| | | } |
| | | // STEP 3: 置为默认前先清掉旧默认,保证"每会员至多一条默认"(与插入同事务) |
| | | if ("1".equals(accountAddress.getIsDefault())) |
| | | { |
| | | accountAddressMapper.clearDefaultByUserId(accountAddress.getUserId()); |
| | | } |
| | | // STEP 4: 落库 |
| | | return accountAddressMapper.insertAddress(accountAddress); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional |
| | | public int deleteMyAddress(Long userId, Long id) |
| | | { |
| | | // 校验归属:非本人地址直接抛异常,避免越权删除(deleteAddressByIds 本身不带 user_id) |
| | | queryOwnedAddress(userId, id); |
| | | return accountAddressMapper.deleteAddressByIds(new Long[]{ id }); |
| | | } |
| | | |
| | | /** |
| | | * 查询地址并校验归属 |
| | | * |
| | | * 地址不存在或不属于该会员时统一抛业务异常(避免暴露他人地址是否存在)。 |
| | | */ |
| | | private AccountAddress queryOwnedAddress(Long userId, Long id) |
| | | { |
| | | AccountAddress address = accountAddressMapper.selectAddressById(id); |
| | | if (address == null || !Objects.equals(address.getUserId(), userId)) |
| | | { |
| | | throw new ServiceException("收货地址不存在或不属于该会员"); |
| | | } |
| | | return address; |
| | | } |
| | | |
| | | |
| | | private Date resolveBindTime(Long oldId, Long newId, Date oldTime, Date now) |
| | | { |
| | |
| | | } |
| | | |
| | | @Override |
| | | public void resetPasswordByPhone(String phone, String rawPassword) |
| | | { |
| | | // STEP 1: 入参兜底校验(认证服务已校验,此处做服务层兜底) |
| | | if (StringUtils.isEmpty(phone)) |
| | | { |
| | | throw new ServiceException("请输入手机号"); |
| | | } |
| | | if (StringUtils.isEmpty(rawPassword)) |
| | | { |
| | | throw new ServiceException("请输入新密码"); |
| | | } |
| | | // STEP 2: 按手机号定位会员(逻辑删除已在 SQL 中过滤) |
| | | AccountUser accountUser = accountUserMapper.selectAccountUserByPhone(phone); |
| | | if (StringUtils.isNull(accountUser)) |
| | | { |
| | | throw new ServiceException("该手机号未注册,请先注册"); |
| | | } |
| | | // STEP 3: 构造单列更新对象,密码 BCrypt 加密后落库(不触碰其它资料) |
| | | AccountUser update = new AccountUser(); |
| | | update.setUserId(accountUser.getUserId()); |
| | | update.setPassword(SecurityUtils.encryptPassword(rawPassword)); |
| | | update.setUpdateBy(phone); |
| | | // STEP 4: 只更新 password 一列;影响行数为 0 说明会员不存在或已删除 |
| | | if (accountUserMapper.updateUserPassword(update) == 0) |
| | | { |
| | | throw new ServiceException("密码重置失败,请稍后重试"); |
| | | } |
| | | log.info("[MEMBER_RESET_PWD] reset success, phone={}", maskPhone(phone)); |
| | | } |
| | | |
| | | @Override |
| | | public AccountUserInfoVo getAccountUserInfo(Long userId) |
| | | { |
| | | // STEP 1: 校验会员ID(登录态缺失时直接失败,避免无归属查询) |
| | |
| | | vo.setPhone(maskPhone(accountUser.getPhone())); |
| | | vo.setAvatar(accountUser.getAvatar()); |
| | | vo.setMemberLevel(MemberLevelEnum.normalize(accountUser.getSystemLevel())); |
| | | // 认证状态为空(历史数据)时兜底为「未认证」,避免前端徽标无值可判 |
| | | vo.setAuthStatus(StringUtils.isEmpty(accountUser.getAuthStatus()) |
| | | ? AUTH_STATUS_NONE : accountUser.getAuthStatus()); |
| | | // 企业/个体认证状态同样兜底为「未认证」(与个人认证状态相互独立) |
| | | vo.setCompanyAuthStatus(StringUtils.isEmpty(accountUser.getCompanyAuthStatus()) |
| | | ? AUTH_STATUS_NONE : accountUser.getCompanyAuthStatus()); |
| | | vo.setEarnings(earnings); |
| | | // STEP 5: 返回个人中心展示数据 |
| | | return vo; |
| | | } |
| | | |
| | | @Override |
| | | public void updateAvatar(Long userId, String avatarUrl) |
| | | { |
| | | // STEP 1: 校验会员ID(登录态缺失时直接失败,避免无归属更新) |
| | | if (StringUtils.isNull(userId)) |
| | | { |
| | | throw new ServiceException("更新头像失败"); |
| | | } |
| | | // STEP 2: 校验头像地址非空 |
| | | if (StringUtils.isEmpty(avatarUrl)) |
| | | { |
| | | throw new ServiceException("头像地址不能为空"); |
| | | } |
| | | // STEP 3: 校验长度,超出列长直接拒绝,避免落库截断 |
| | | if (avatarUrl.length() > AVATAR_MAX_LENGTH) |
| | | { |
| | | throw new ServiceException("头像地址过长"); |
| | | } |
| | | // STEP 4: 只更新 avatar 一列(单列语句,不触碰其它资料) |
| | | AccountUser update = new AccountUser(); |
| | | update.setUserId(userId); |
| | | update.setAvatar(avatarUrl); |
| | | update.setUpdateBy(SecurityUtils.getUsername()); |
| | | // STEP 5: 影响行数为 0 说明会员不存在或已删除(SQL 自带 del_flag='0' 过滤) |
| | | if (accountUserMapper.updateUserAvatar(update) == 0) |
| | | { |
| | | throw new ServiceException("更新头像失败"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public InviteCodeVo getInviteCode(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: 组装展示对象(邀请码注册时生成,C 端会员必有) |
| | | InviteCodeVo vo = new InviteCodeVo(); |
| | | vo.setReferralCode(accountUser.getMyReferralCode()); |
| | | return vo; |
| | | } |
| | | |
| | | @Override |
| | | public List<AccountInviteeVo> getInvitees(Long userId) |
| | | { |
| | | // STEP 1: 校验会员ID(登录态缺失时直接失败,避免无归属查询) |
| | | if (StringUtils.isNull(userId)) |
| | | { |
| | | throw new ServiceException("获取邀请列表失败"); |
| | | } |
| | | // STEP 2: 按 referrer_id 查询直接下级,SQL 已按绑定时间倒序(配合 Controller 分页) |
| | | List<AccountUser> invitees = accountUserMapper.selectInvitees(userId); |
| | | // STEP 3: 逐条组装展示对象;手机号脱敏、绑定日格式化,等级原值直出 |
| | | List<AccountInviteeVo> result = new ArrayList<>(invitees.size()); |
| | | for (AccountUser invitee : invitees) |
| | | { |
| | | AccountInviteeVo vo = new AccountInviteeVo(); |
| | | vo.setUserId(invitee.getUserId()); |
| | | vo.setNickname(invitee.getNickname()); |
| | | vo.setAvatar(invitee.getAvatar()); |
| | | vo.setPhone(maskPhone(invitee.getPhone())); |
| | | vo.setSystemLevel(invitee.getSystemLevel()); |
| | | vo.setBindDate(formatBindDate(invitee.getReferrerBindTime())); |
| | | vo.setBuyAmountTotal(invitee.getBuyAmountTotal()); |
| | | result.add(vo); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | @Override |
| | | public AccountReferrerVo getReferrer(Long userId) |
| | | { |
| | | // STEP 1: 校验会员ID(登录态缺失时直接失败,避免无归属查询) |
| | | if (StringUtils.isNull(userId)) |
| | | { |
| | | throw new ServiceException("获取上级信息失败"); |
| | | } |
| | | // STEP 2: 查询会员本人,取其 referrer_id 与绑定时间 |
| | | AccountUser self = accountUserMapper.selectAccountUserById(userId); |
| | | if (StringUtils.isNull(self)) |
| | | { |
| | | throw new ServiceException("获取上级信息失败"); |
| | | } |
| | | AccountReferrerVo vo = new AccountReferrerVo(); |
| | | Long referrerId = self.getReferrerId(); |
| | | // STEP 3: 无上级(命中通用推荐码注册或历史数据)时返回空对象,前端渲染空态 |
| | | if (StringUtils.isNull(referrerId)) |
| | | { |
| | | return vo; |
| | | } |
| | | // STEP 4: 按主键查上级;上级已被逻辑删除时同样返回空对象 |
| | | AccountUser referrer = accountUserMapper.selectAccountUserById(referrerId); |
| | | if (StringUtils.isNull(referrer)) |
| | | { |
| | | return vo; |
| | | } |
| | | // STEP 5: 组装展示对象;绑定时间取本人记录上的 referrer_bind_time |
| | | vo.setUserId(referrer.getUserId()); |
| | | vo.setNickname(referrer.getNickname()); |
| | | vo.setAvatar(referrer.getAvatar()); |
| | | vo.setPhone(maskPhone(referrer.getPhone())); |
| | | vo.setSystemLevel(referrer.getSystemLevel()); |
| | | vo.setBindDate(formatBindDate(self.getReferrerBindTime())); |
| | | return vo; |
| | | } |
| | | |
| | | /** |
| | | * 绑定时间格式化(yyyy-MM-dd) |
| | | * |
| | | * @param bindTime 绑定时间,可为空 |
| | | * @return 格式化后的日期字符串;入参为空时返回 null |
| | | */ |
| | | private String formatBindDate(Date bindTime) |
| | | { |
| | | return StringUtils.isNull(bindTime) ? null : DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD, bindTime); |
| | | } |
| | | |
| | | /** |
| | | * 判定是否为通用推荐码 |
| | | * |