|
|
@@ -1,9 +1,12 @@
|
|
|
package com.yingpai.gupiao.service.impl;
|
|
|
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.yingpai.gupiao.domain.dto.WxCompleteUserInfoDTO;
|
|
|
import com.yingpai.gupiao.domain.dto.WxLoginResponse;
|
|
|
+import com.yingpai.gupiao.domain.dto.WxPhoneLoginDTO;
|
|
|
import com.yingpai.gupiao.domain.po.User;
|
|
|
import com.yingpai.gupiao.domain.vo.LoginVO;
|
|
|
+import com.yingpai.gupiao.domain.vo.WxLoginCheckVO;
|
|
|
import com.yingpai.gupiao.mapper.UserMapper;
|
|
|
import com.yingpai.gupiao.service.AuthService;
|
|
|
import com.yingpai.gupiao.util.JwtUtil;
|
|
|
@@ -13,11 +16,14 @@ import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
-import java.util.Random;
|
|
|
|
|
|
/**
|
|
|
* 认证服务实现类
|
|
|
- * 实现用户登录、注册、验证码发送等功能
|
|
|
+ *
|
|
|
+ * 完整登录流程:
|
|
|
+ * 1. wxSilentLogin - 检查是否为老用户(根据openid)
|
|
|
+ * 2. wxPhoneCheck - 验证手机号(解密获取手机号)
|
|
|
+ * 3. wxCompleteUserInfo - 完善用户信息(创建新用户)
|
|
|
*/
|
|
|
@Slf4j
|
|
|
@Service
|
|
|
@@ -28,225 +34,207 @@ public class AuthServiceImpl implements AuthService {
|
|
|
private final JwtUtil jwtUtil;
|
|
|
private final WxApiUtil wxApiUtil;
|
|
|
|
|
|
- /**
|
|
|
- * 临时存储验证码的Map(生产环境应使用Redis)
|
|
|
- */
|
|
|
- private static final java.util.Map<String, String> CODE_CACHE = new java.util.concurrent.ConcurrentHashMap<>();
|
|
|
-
|
|
|
/**
|
|
|
* 默认头像URL
|
|
|
*/
|
|
|
private static final String DEFAULT_AVATAR = "/static/images/head.png";
|
|
|
|
|
|
/**
|
|
|
- * 微信登录
|
|
|
- * @param code 微信登录code
|
|
|
- * @return 登录结果
|
|
|
+ * 第一步:微信静默登录(检查是否为老用户)
|
|
|
+ * @param loginCode 微信登录code
|
|
|
+ * @return 老用户返回token,新用户返回isSign=false
|
|
|
*/
|
|
|
@Override
|
|
|
- public LoginVO wxLogin(String code) {
|
|
|
+ public WxLoginCheckVO wxSilentLogin(String loginCode) {
|
|
|
try {
|
|
|
- // 调用微信API获取openid和session_key
|
|
|
- WxLoginResponse wxResponse = wxApiUtil.getOpenidByCode(code);
|
|
|
+ log.info("【第一步】微信静默登录,loginCode: {}", loginCode);
|
|
|
+
|
|
|
+ // 1. 调用微信API获取openid和unionid
|
|
|
+ WxLoginResponse wxResponse = wxApiUtil.getOpenidByCode(loginCode);
|
|
|
String openid = wxResponse.getOpenid();
|
|
|
String unionid = wxResponse.getUnionid();
|
|
|
|
|
|
- log.info("微信登录,openid: {}, unionid: {}", openid, unionid);
|
|
|
+ log.info("获取微信信息成功,openid: {}, unionid: {}", openid, unionid);
|
|
|
|
|
|
- // 根据openid查询用户
|
|
|
+ // 2. 根据openid查询用户
|
|
|
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
|
|
wrapper.eq(User::getOpenid, openid);
|
|
|
User user = userMapper.selectOne(wrapper);
|
|
|
|
|
|
- // 如果用户不存在,创建新用户
|
|
|
+ // 3. 判断用户是否存在
|
|
|
if (user == null) {
|
|
|
- user = new User();
|
|
|
- user.setOpenid(openid);
|
|
|
- user.setUnionid(unionid);
|
|
|
- // 使用openid后8位作为用户标识,确保唯一性
|
|
|
- String userCode = openid.substring(Math.max(0, openid.length() - 8));
|
|
|
- user.setNickname("微信用户" + userCode);
|
|
|
- user.setAvatar(DEFAULT_AVATAR);
|
|
|
- user.setStatus(0);
|
|
|
- user.setCreateTime(LocalDateTime.now());
|
|
|
- user.setUpdateTime(LocalDateTime.now());
|
|
|
- userMapper.insert(user);
|
|
|
- log.info("创建新用户,userId: {}, openid: {}, nickname: {}", user.getId(), openid, user.getNickname());
|
|
|
- } else {
|
|
|
- // 更新unionid(如果有)
|
|
|
- if (unionid != null && !unionid.equals(user.getUnionid())) {
|
|
|
- user.setUnionid(unionid);
|
|
|
- user.setUpdateTime(LocalDateTime.now());
|
|
|
- userMapper.updateById(user);
|
|
|
- }
|
|
|
- log.info("用户已存在,userId: {}, openid: {}", user.getId(), openid);
|
|
|
+ // 新用户,返回 isSign=false
|
|
|
+ log.info("新用户,需要授权手机号");
|
|
|
+ return WxLoginCheckVO.builder()
|
|
|
+ .isSign("false")
|
|
|
+ .build();
|
|
|
}
|
|
|
|
|
|
- // 生成token
|
|
|
- String token = jwtUtil.generateToken(user.getId(), user.getPhone());
|
|
|
+ // 4. 检查用户状态
|
|
|
+ if (user.getStatus() != null && user.getStatus() == 1) {
|
|
|
+ // 账号被禁用
|
|
|
+ log.warn("账号被禁用,userId: {}", user.getId());
|
|
|
+ return WxLoginCheckVO.builder()
|
|
|
+ .code(103)
|
|
|
+ .message("账号已被禁用")
|
|
|
+ .build();
|
|
|
+ }
|
|
|
|
|
|
- // 构建返回结果
|
|
|
- return buildLoginVO(user, token);
|
|
|
+ // 5. 老用户,生成token并返回
|
|
|
+ String token = jwtUtil.generateToken(user.getId(), user.getPhone());
|
|
|
+ log.info("老用户登录成功,userId: {}, token已生成", user.getId());
|
|
|
|
|
|
+ return WxLoginCheckVO.builder()
|
|
|
+ .isSign("true")
|
|
|
+ .token(token)
|
|
|
+ .build();
|
|
|
+
|
|
|
} catch (Exception e) {
|
|
|
- log.error("微信登录失败", e);
|
|
|
- throw new RuntimeException("微信登录失败: " + e.getMessage());
|
|
|
+ log.error("微信静默登录失败", e);
|
|
|
+ throw new RuntimeException("微信静默登录失败: " + e.getMessage());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 手机号验证码登录
|
|
|
- * @param phone 手机号
|
|
|
- * @param code 验证码
|
|
|
- * @return 登录结果
|
|
|
+ * 第二步:手机号授权验证
|
|
|
+ * @param dto 包含loginCode、phoneCode、encryptedData、iv
|
|
|
+ * @return 已注册返回token,未注册返回用户信息
|
|
|
*/
|
|
|
@Override
|
|
|
- public LoginVO phoneLogin(String phone, String code) {
|
|
|
- // 验证手机号格式
|
|
|
- if (!isValidPhone(phone)) {
|
|
|
- throw new RuntimeException("手机号格式不正确");
|
|
|
- }
|
|
|
-
|
|
|
- // 验证验证码
|
|
|
- if (!verifyCode(phone, code)) {
|
|
|
- throw new RuntimeException("验证码错误或已过期");
|
|
|
- }
|
|
|
-
|
|
|
- // 根据手机号查询用户
|
|
|
- LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
|
|
- wrapper.eq(User::getPhone, phone);
|
|
|
- User user = userMapper.selectOne(wrapper);
|
|
|
-
|
|
|
- // 如果用户不存在,创建新用户
|
|
|
- if (user == null) {
|
|
|
- user = new User();
|
|
|
- user.setPhone(phone);
|
|
|
- user.setNickname(phone); // 使用手机号作为昵称
|
|
|
- user.setAvatar(DEFAULT_AVATAR); // 使用默认头像
|
|
|
- user.setStatus(0);
|
|
|
- user.setCreateTime(LocalDateTime.now());
|
|
|
- user.setUpdateTime(LocalDateTime.now());
|
|
|
- userMapper.insert(user);
|
|
|
- log.info("创建新用户,userId: {}, phone: {}, nickname: {}", user.getId(), phone, user.getNickname());
|
|
|
+ public WxLoginCheckVO wxPhoneCheck(WxPhoneLoginDTO dto) {
|
|
|
+ try {
|
|
|
+ log.info("【第二步】手机号授权验证,loginCode: {}", dto.getLoginCode());
|
|
|
+
|
|
|
+ // 1. 调用微信API获取openid、unionid和session_key
|
|
|
+ WxLoginResponse wxResponse = wxApiUtil.getOpenidByCode(dto.getLoginCode());
|
|
|
+ String openid = wxResponse.getOpenid();
|
|
|
+ String unionid = wxResponse.getUnionid();
|
|
|
+ String sessionKey = wxResponse.getSessionKey();
|
|
|
+
|
|
|
+ log.info("获取微信信息成功,openid: {}, unionid: {}, sessionKey已获取", openid, unionid);
|
|
|
+
|
|
|
+ // 2. 解密手机号(使用encryptedData和iv方式)
|
|
|
+ String phone = null;
|
|
|
+
|
|
|
+ if (dto.getEncryptedData() != null && dto.getIv() != null && sessionKey != null) {
|
|
|
+ // 使用旧版解密方式(不需要开通手机号快速验证组件)
|
|
|
+ log.info("使用encryptedData解密手机号");
|
|
|
+ phone = wxApiUtil.decryptPhoneNumber(dto.getEncryptedData(), sessionKey, dto.getIv());
|
|
|
+ log.info("解密手机号成功: {}", phone);
|
|
|
+ } else {
|
|
|
+ log.error("缺少解密参数:encryptedData={}, iv={}, sessionKey={}",
|
|
|
+ dto.getEncryptedData() != null, dto.getIv() != null, sessionKey != null);
|
|
|
+ throw new RuntimeException("缺少手机号解密参数");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 根据手机号查询用户
|
|
|
+ LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(User::getPhone, phone);
|
|
|
+ User user = userMapper.selectOne(wrapper);
|
|
|
+
|
|
|
+ // 4. 判断用户是否存在
|
|
|
+ if (user == null) {
|
|
|
+ // 未注册,返回用户信息供前端完善资料
|
|
|
+ log.info("未注册用户,需要完善信息");
|
|
|
+ return WxLoginCheckVO.builder()
|
|
|
+ .isSign("false")
|
|
|
+ .openid(openid)
|
|
|
+ .unionid(unionid)
|
|
|
+ .phoneNumber(phone)
|
|
|
+ .build();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 已注册用户,更新openid和unionid(如果没有)
|
|
|
+ boolean needUpdate = false;
|
|
|
+
|
|
|
+ if (user.getOpenid() == null || user.getOpenid().isEmpty()) {
|
|
|
+ user.setOpenid(openid);
|
|
|
+ needUpdate = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (unionid != null && (user.getUnionid() == null || user.getUnionid().isEmpty())) {
|
|
|
+ user.setUnionid(unionid);
|
|
|
+ needUpdate = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (needUpdate) {
|
|
|
+ user.setUpdateTime(LocalDateTime.now());
|
|
|
+ userMapper.updateById(user);
|
|
|
+ log.info("更新用户微信信息,userId: {}", user.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 生成token并返回
|
|
|
+ String token = jwtUtil.generateToken(user.getId(), user.getPhone());
|
|
|
+ log.info("已注册用户登录成功,userId: {}, token已生成", user.getId());
|
|
|
+
|
|
|
+ return WxLoginCheckVO.builder()
|
|
|
+ .isSign("true")
|
|
|
+ .token(token)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("手机号授权验证失败", e);
|
|
|
+ throw new RuntimeException("手机号授权验证失败: " + e.getMessage());
|
|
|
}
|
|
|
-
|
|
|
- // 生成token
|
|
|
- String token = jwtUtil.generateToken(user.getId(), user.getPhone());
|
|
|
-
|
|
|
- // 清除验证码
|
|
|
- // redisTemplate.delete(SMS_CODE_PREFIX + phone);
|
|
|
-
|
|
|
- // 构建返回结果
|
|
|
- return buildLoginVO(user, token);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 发送短信验证码
|
|
|
- * @param phone 手机号
|
|
|
- * @return 是否发送成功
|
|
|
+ * 第三步:完善用户信息(首次登录)
|
|
|
+ * @param dto 包含完整用户信息
|
|
|
+ * @return 返回token
|
|
|
*/
|
|
|
@Override
|
|
|
- public boolean sendSmsCode(String phone) {
|
|
|
- // 验证手机号格式
|
|
|
- if (!isValidPhone(phone)) {
|
|
|
- throw new RuntimeException("手机号格式不正确");
|
|
|
- }
|
|
|
-
|
|
|
- // 生成6位随机验证码
|
|
|
- String code = generateCode();
|
|
|
-
|
|
|
- // TODO: 调用短信服务发送验证码
|
|
|
- // 实际开发中需要对接阿里云、腾讯云等短信服务
|
|
|
- log.info("发送验证码到手机号: {}, 验证码: {}", phone, code);
|
|
|
-
|
|
|
- // 将验证码存储到Redis(如果有Redis)
|
|
|
- // redisTemplate.opsForValue().set(
|
|
|
- // SMS_CODE_PREFIX + phone,
|
|
|
- // code,
|
|
|
- // CODE_EXPIRE_MINUTES,
|
|
|
- // TimeUnit.MINUTES
|
|
|
- // );
|
|
|
-
|
|
|
- // 临时方案:存储到内存(仅用于开发测试,生产环境必须使用Redis)
|
|
|
- CODE_CACHE.put(phone, code);
|
|
|
- log.info("验证码已存入缓存,phone: {}, code: {}", phone, code);
|
|
|
-
|
|
|
- return true;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 验证验证码
|
|
|
- * @param phone 手机号
|
|
|
- * @param code 验证码
|
|
|
- * @return 是否验证通过
|
|
|
- */
|
|
|
- private boolean verifyCode(String phone, String code) {
|
|
|
- // 从Redis获取验证码
|
|
|
- // String cachedCode = redisTemplate.opsForValue().get(SMS_CODE_PREFIX + phone);
|
|
|
- // return code.equals(cachedCode);
|
|
|
-
|
|
|
- // 临时方案:从内存获取验证码
|
|
|
- String cachedCode = CODE_CACHE.get(phone);
|
|
|
- log.info("验证验证码,phone: {}, 输入code: {}, 缓存code: {}", phone, code, cachedCode);
|
|
|
-
|
|
|
- if (cachedCode == null) {
|
|
|
- log.warn("验证码不存在或已过期,phone: {}", phone);
|
|
|
- return false;
|
|
|
- }
|
|
|
-
|
|
|
- boolean isValid = code.equals(cachedCode);
|
|
|
- if (isValid) {
|
|
|
- // 验证成功后删除验证码
|
|
|
- CODE_CACHE.remove(phone);
|
|
|
- log.info("验证码验证成功,phone: {}", phone);
|
|
|
- } else {
|
|
|
- log.warn("验证码错误,phone: {}, 输入: {}, 期望: {}", phone, code, cachedCode);
|
|
|
- }
|
|
|
-
|
|
|
- return isValid;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 生成6位随机验证码
|
|
|
- * @return 验证码
|
|
|
- */
|
|
|
- private String generateCode() {
|
|
|
- Random random = new Random();
|
|
|
- return String.format("%06d", random.nextInt(1000000));
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 验证手机号格式
|
|
|
- * @param phone 手机号
|
|
|
- * @return 是否有效
|
|
|
- */
|
|
|
- private boolean isValidPhone(String phone) {
|
|
|
- return phone != null && phone.matches("^1[3-9]\\d{9}$");
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 构建登录响应VO
|
|
|
- * @param user 用户实体
|
|
|
- * @param token JWT token
|
|
|
- * @return LoginVO
|
|
|
- */
|
|
|
- private LoginVO buildLoginVO(User user, String token) {
|
|
|
- // 手机号脱敏
|
|
|
- String maskedPhone = null;
|
|
|
- if (user.getPhone() != null) {
|
|
|
- maskedPhone = user.getPhone().replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
|
|
|
+ public LoginVO wxCompleteUserInfo(WxCompleteUserInfoDTO dto) {
|
|
|
+ try {
|
|
|
+ log.info("【第三步】完善用户信息,openid: {}, phone: {}, nickname: {}",
|
|
|
+ dto.getOpenid(), dto.getPhoneNumber(), dto.getNickname());
|
|
|
+
|
|
|
+ // 1. 检查手机号是否已被注册
|
|
|
+ LambdaQueryWrapper<User> phoneWrapper = new LambdaQueryWrapper<>();
|
|
|
+ phoneWrapper.eq(User::getPhone, dto.getPhoneNumber());
|
|
|
+ User existUser = userMapper.selectOne(phoneWrapper);
|
|
|
+
|
|
|
+ if (existUser != null) {
|
|
|
+ log.warn("手机号已被注册,phone: {}, userId: {}", dto.getPhoneNumber(), existUser.getId());
|
|
|
+ throw new RuntimeException("该手机号已被注册");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 检查openid是否已存在
|
|
|
+ LambdaQueryWrapper<User> openidWrapper = new LambdaQueryWrapper<>();
|
|
|
+ openidWrapper.eq(User::getOpenid, dto.getOpenid());
|
|
|
+ existUser = userMapper.selectOne(openidWrapper);
|
|
|
+
|
|
|
+ if (existUser != null) {
|
|
|
+ log.warn("openid已存在,openid: {}, userId: {}", dto.getOpenid(), existUser.getId());
|
|
|
+ throw new RuntimeException("该微信账号已注册");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 创建新用户
|
|
|
+ User newUser = new User();
|
|
|
+ newUser.setOpenid(dto.getOpenid());
|
|
|
+ newUser.setUnionid(dto.getUnionid());
|
|
|
+ newUser.setPhone(dto.getPhoneNumber());
|
|
|
+ newUser.setNickname(dto.getNickname());
|
|
|
+ newUser.setAvatar(dto.getAvatarUrl() != null ? dto.getAvatarUrl() : DEFAULT_AVATAR);
|
|
|
+ newUser.setStatus(0);
|
|
|
+ newUser.setCreateTime(LocalDateTime.now());
|
|
|
+ newUser.setUpdateTime(LocalDateTime.now());
|
|
|
+
|
|
|
+ userMapper.insert(newUser);
|
|
|
+ log.info("创建新用户成功,userId: {}, openid: {}, phone: {}, nickname: {}",
|
|
|
+ newUser.getId(), dto.getOpenid(), dto.getPhoneNumber(), dto.getNickname());
|
|
|
+
|
|
|
+ // 4. 生成token
|
|
|
+ String token = jwtUtil.generateToken(newUser.getId(), newUser.getPhone());
|
|
|
+
|
|
|
+ // 5. 返回token
|
|
|
+ return LoginVO.builder()
|
|
|
+ .token(token)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("完善用户信息失败", e);
|
|
|
+ throw new RuntimeException("注册失败: " + e.getMessage());
|
|
|
}
|
|
|
-
|
|
|
- LoginVO.UserInfoVO userInfo = LoginVO.UserInfoVO.builder()
|
|
|
- .id(user.getId())
|
|
|
- .nickname(user.getNickname())
|
|
|
- .avatar(user.getAvatar())
|
|
|
- .phone(maskedPhone)
|
|
|
- .build();
|
|
|
-
|
|
|
- return LoginVO.builder()
|
|
|
- .token(token)
|
|
|
- .userInfo(userInfo)
|
|
|
- .build();
|
|
|
}
|
|
|
}
|