package cn.iocoder.yudao.module.member.service.user; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.*; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserPageReqVO; import cn.iocoder.yudao.module.member.controller.admin.user.vo.MemberUserUpdateReqVO; import cn.iocoder.yudao.module.member.controller.app.user.vo.*; import cn.iocoder.yudao.module.member.convert.auth.AuthConvert; import cn.iocoder.yudao.module.member.convert.user.MemberUserConvert; import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO; import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper; import cn.iocoder.yudao.module.member.mq.producer.user.MemberUserProducer; import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO; import cn.iocoder.yudao.module.system.api.social.SocialClientApi; import cn.iocoder.yudao.module.system.api.social.dto.SocialWxPhoneNumberInfoRespDTO; import cn.iocoder.yudao.module.system.enums.sms.SmsSceneEnum; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import jakarta.annotation.Resource; import jakarta.validation.Valid; import java.time.LocalDateTime; import java.util.Collection; import java.util.List; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.util.servlet.ServletUtils.getClientIP; import static cn.iocoder.yudao.module.member.enums.ErrorCodeConstants.*; /** * 会员 User Service 实现类 * * @author 芋道源码 */ @Service @Valid @Slf4j public class MemberUserServiceImpl implements MemberUserService { @Resource private MemberUserMapper memberUserMapper; @Resource private SmsCodeApi smsCodeApi; @Resource private SocialClientApi socialClientApi; @Resource private PasswordEncoder passwordEncoder; @Resource private MemberUserProducer memberUserProducer; @Override public MemberUserDO getUserByMobile(String mobile) { return memberUserMapper.selectByMobile(mobile); } @Override public List getUserListByNickname(String nickname) { return memberUserMapper.selectListByNicknameLike(nickname); } @Override @Transactional(rollbackFor = Exception.class) public MemberUserDO createUserIfAbsent(String mobile, String registerIp, Integer terminal) { // 用户已经存在 MemberUserDO user = memberUserMapper.selectByMobile(mobile); if (user != null) { return user; } // 用户不存在,则进行创建 return createUser(mobile, null, null, registerIp, terminal); } @Override @Transactional(rollbackFor = Exception.class) public MemberUserDO createUser(String nickname, String avtar, String registerIp, Integer terminal) { return createUser(null, nickname, avtar, registerIp, terminal); } private MemberUserDO createUser(String mobile, String nickname, String avtar, String registerIp, Integer terminal) { // 生成密码 String password = IdUtil.fastSimpleUUID(); // 插入用户 MemberUserDO user = new MemberUserDO(); user.setMobile(mobile); user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 user.setPassword(encodePassword(password)); // 加密密码 user.setRegisterIp(registerIp).setRegisterTerminal(terminal); user.setNickname(nickname).setAvatar(avtar); // 基础信息 if (StrUtil.isEmpty(nickname)) { // 昵称为空时,随机一个名字,避免一些依赖 nickname 的逻辑报错,或者有点丑。例如说,短信发送有昵称时~ user.setNickname("用户" + RandomUtil.randomNumbers(6)); } memberUserMapper.insert(user); // 发送 MQ 消息:用户创建 TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { @Override public void afterCommit() { memberUserProducer.sendUserCreateMessage(user.getId()); } }); return user; } @Override public void updateUserLogin(Long id, String loginIp) { memberUserMapper.updateById(new MemberUserDO().setId(id) .setLoginIp(loginIp).setLoginDate(LocalDateTime.now())); } @Override public MemberUserDO getUser(Long id) { return memberUserMapper.selectById(id); } @Override public List getUserList(Collection ids) { if (CollUtil.isEmpty(ids)) { return ListUtil.empty(); } return memberUserMapper.selectByIds(ids); } @Override public void updateUser(Long userId, AppMemberUserUpdateReqVO reqVO) { // 1.1 检测用户是否存在 validateUserExists(userId); // 1.2 校验手机是否已经被绑定 validateEmailUnique(userId, reqVO.getEmail()); // 2. 更新用户 MemberUserDO updateObj = BeanUtils.toBean(reqVO, MemberUserDO.class).setId(userId); memberUserMapper.updateById(updateObj); } @Override @Transactional(rollbackFor = Exception.class) public void updateUserMobile(Long userId, AppMemberUserUpdateMobileReqVO reqVO) { // 1.1 检测用户是否存在 MemberUserDO user = validateUserExists(userId); // 1.2 校验新手机是否已经被绑定 validateMobileUnique(null, reqVO.getMobile()); // 2.1 校验旧手机和旧验证码 // 补充说明:从安全性来说,老手机也校验 oldCode 验证码会更安全。但是由于 uni-app 商城界面暂时没做,所以这里不强制校验 if (StrUtil.isNotEmpty(reqVO.getOldCode())) { smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(user.getMobile()).setCode(reqVO.getOldCode()) .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())); } // 2.2 使用新验证码 smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(reqVO.getMobile()).setCode(reqVO.getCode()) .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())); // 3. 更新用户手机 memberUserMapper.updateById(MemberUserDO.builder().id(userId).mobile(reqVO.getMobile()).build()); } @Override public void updateUserMobileByWeixin(Long userId, AppMemberUserUpdateMobileByWeixinReqVO reqVO) { // 1.1 获得对应的手机号信息 SocialWxPhoneNumberInfoRespDTO phoneNumberInfo = socialClientApi.getWxMaPhoneNumberInfo( UserTypeEnum.MEMBER.getValue(), reqVO.getCode()); Assert.notNull(phoneNumberInfo, "获得手机信息失败,结果为空"); // 1.2 校验新手机是否已经被绑定 validateMobileUnique(userId, phoneNumberInfo.getPhoneNumber()); // 2. 更新用户手机 memberUserMapper.updateById(MemberUserDO.builder().id(userId).mobile(phoneNumberInfo.getPhoneNumber()).build()); } @Override public void updateUserPassword(Long userId, AppMemberUserUpdatePasswordReqVO reqVO) { // 检测用户是否存在 MemberUserDO user = validateUserExists(userId); // 校验验证码 smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(user.getMobile()).setCode(reqVO.getCode()) .setScene(SmsSceneEnum.MEMBER_UPDATE_PASSWORD.getScene()).setUsedIp(getClientIP())); // 更新用户密码 memberUserMapper.updateById(MemberUserDO.builder().id(userId) .password(passwordEncoder.encode(reqVO.getPassword())).build()); } @Override public void resetUserPassword(AppMemberUserResetPasswordReqVO reqVO) { // 检验用户是否存在 MemberUserDO user = validateUserExists(reqVO.getMobile()); // 使用验证码 smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_RESET_PASSWORD, getClientIP())); // 更新密码 memberUserMapper.updateById(MemberUserDO.builder().id(user.getId()) .password(passwordEncoder.encode(reqVO.getPassword())).build()); } private MemberUserDO validateUserExists(String mobile) { MemberUserDO user = memberUserMapper.selectByMobile(mobile); if (user == null) { throw exception(USER_MOBILE_NOT_EXISTS); } return user; } @Override public boolean isPasswordMatch(String rawPassword, String encodedPassword) { return passwordEncoder.matches(rawPassword, encodedPassword); } /** * 对密码进行加密 * * @param password 密码 * @return 加密后的密码 */ private String encodePassword(String password) { return passwordEncoder.encode(password); } @Override @Transactional(rollbackFor = Exception.class) public void updateUser(MemberUserUpdateReqVO updateReqVO) { // 校验存在 validateUserExists(updateReqVO.getId()); // 校验手机唯一 validateMobileUnique(updateReqVO.getId(), updateReqVO.getMobile()); // 校验邮箱唯一 validateEmailUnique(updateReqVO.getId(), updateReqVO.getEmail()); // 更新 MemberUserDO updateObj = MemberUserConvert.INSTANCE.convert(updateReqVO); memberUserMapper.updateById(updateObj); } @VisibleForTesting MemberUserDO validateUserExists(Long id) { if (id == null) { return null; } MemberUserDO user = memberUserMapper.selectById(id); if (user == null) { throw exception(USER_NOT_EXISTS); } return user; } @VisibleForTesting void validateMobileUnique(Long id, String mobile) { if (StrUtil.isBlank(mobile)) { return; } MemberUserDO user = memberUserMapper.selectByMobile(mobile); if (user == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的用户 if (id == null) { throw exception(USER_MOBILE_USED, mobile); } if (!user.getId().equals(id)) { throw exception(USER_MOBILE_USED, mobile); } } @VisibleForTesting void validateEmailUnique(Long id, String email) { if (StrUtil.isBlank(email)) { return; } MemberUserDO user = memberUserMapper.selectByEmail(email); if (user == null) { return; } // 如果 id 为空,说明不用比较是否为相同 id 的用户 if (id == null) { throw exception(USER_EMAIL_USED, email); } if (!user.getId().equals(id)) { throw exception(USER_EMAIL_USED, email); } } @Override public PageResult getUserPage(MemberUserPageReqVO pageReqVO) { return memberUserMapper.selectPage(pageReqVO); } @Override public void updateUserLevel(Long id, Long levelId, Integer experience) { // 0 代表无等级:防止UpdateById时,会被过滤掉的问题 levelId = ObjectUtil.defaultIfNull(levelId, 0L); memberUserMapper.updateById(new MemberUserDO() .setId(id) .setLevelId(levelId).setExperience(experience) ); } @Override public Long getUserCountByGroupId(Long groupId) { return memberUserMapper.selectCountByGroupId(groupId); } @Override public Long getUserCountByLevelId(Long levelId) { return memberUserMapper.selectCountByLevelId(levelId); } @Override public Long getUserCountByTagId(Long tagId) { return memberUserMapper.selectCountByTagId(tagId); } @Override public boolean updateUserPoint(Long id, Integer point) { if (point > 0) { memberUserMapper.updatePointIncr(id, point); } else if (point < 0) { return memberUserMapper.updatePointDecr(id, point) > 0; } return true; } }