|
|
@@ -0,0 +1,253 @@
|
|
|
+package com.yingpai.gupiao.service.impl;
|
|
|
+
|
|
|
+import cn.hutool.core.util.StrUtil;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import com.yingpai.gupiao.config.WxConfig;
|
|
|
+import com.yingpai.gupiao.domain.dto.H5PhoneLoginDTO;
|
|
|
+import com.yingpai.gupiao.domain.po.User;
|
|
|
+import com.yingpai.gupiao.domain.vo.LoginVO;
|
|
|
+import com.yingpai.gupiao.domain.vo.WxH5UserInfoVO;
|
|
|
+import com.yingpai.gupiao.mapper.UserMapper;
|
|
|
+import com.yingpai.gupiao.service.H5AuthService;
|
|
|
+import com.yingpai.gupiao.util.JwtUtil;
|
|
|
+import com.yingpai.gupiao.util.WxApiUtil;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.web.client.RestTemplate;
|
|
|
+
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.UUID;
|
|
|
+
|
|
|
+/**
|
|
|
+ * H5公众号认证服务实现
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class H5AuthServiceImpl implements H5AuthService {
|
|
|
+
|
|
|
+ private final WxConfig wxConfig;
|
|
|
+ private final UserMapper userMapper;
|
|
|
+ private final JwtUtil jwtUtil;
|
|
|
+ private final RestTemplate restTemplate = new RestTemplate();
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ // 微信OAuth2.0授权URL
|
|
|
+ private static final String WX_OAUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize";
|
|
|
+ // 获取access_token的URL
|
|
|
+ private static final String WX_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
|
|
|
+ // 获取用户信息的URL
|
|
|
+ private static final String WX_USER_INFO_URL = "https://api.weixin.qq.com/sns/userinfo";
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getAuthUrl(String redirectUrl) {
|
|
|
+ try {
|
|
|
+ // 构建微信授权URL
|
|
|
+ // scope=snsapi_userinfo 可以获取用户基本信息(昵称、头像等)
|
|
|
+ // scope=snsapi_base 只能获openid,无法获取用户信息
|
|
|
+ String encodedRedirectUrl = URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8);
|
|
|
+ String state = UUID.randomUUID().toString().replace("-", "");
|
|
|
+ String authUrl = String.format(
|
|
|
+ "%s?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=%s#wechat_redirect",
|
|
|
+ WX_OAUTH_URL,
|
|
|
+ wxConfig.getAppid(),
|
|
|
+ encodedRedirectUrl,
|
|
|
+ state
|
|
|
+ );
|
|
|
+
|
|
|
+ log.info("生成微信授权URL: {}", authUrl);
|
|
|
+ return authUrl;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("生成授权URL失败", e);
|
|
|
+ throw new RuntimeException("生成授权URL失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public WxH5UserInfoVO getUserInfoByCode(String code) throws Exception {
|
|
|
+ try {
|
|
|
+ log.info("通过code获取用户信息,code: {}", code);
|
|
|
+
|
|
|
+ // 1. 通过code获取access_token和openid
|
|
|
+ String tokenUrl = String.format(
|
|
|
+ "%s?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
|
|
|
+ WX_ACCESS_TOKEN_URL,
|
|
|
+ wxConfig.getAppid(),
|
|
|
+ wxConfig.getSecret(),
|
|
|
+ code
|
|
|
+ );
|
|
|
+
|
|
|
+ String tokenResponse = restTemplate.getForObject(tokenUrl, String.class);
|
|
|
+ log.info("获取access_token响应: {}", tokenResponse);
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ Map<String, Object> tokenMap = objectMapper.readValue(tokenResponse, Map.class);
|
|
|
+
|
|
|
+ if (tokenMap.containsKey("errcode")) {
|
|
|
+ Integer errcode = (Integer) tokenMap.get("errcode");
|
|
|
+ String errmsg = (String) tokenMap.get("errmsg");
|
|
|
+ log.error("获取access_token失败,errcode: {}, errmsg: {}", errcode, errmsg);
|
|
|
+ throw new RuntimeException("获取access_token失败: " + errmsg);
|
|
|
+ }
|
|
|
+
|
|
|
+ String accessToken = (String) tokenMap.get("access_token");
|
|
|
+ String openid = (String) tokenMap.get("openid");
|
|
|
+ String unionid = (String) tokenMap.get("unionid");
|
|
|
+
|
|
|
+ log.info("获取access_token成功,openid: {}, unionid: {}", openid, unionid);
|
|
|
+
|
|
|
+ // 2. 通过access_token和openid获取用户信息
|
|
|
+ String userInfoUrl = String.format(
|
|
|
+ "%s?access_token=%s&openid=%s&lang=zh_CN",
|
|
|
+ WX_USER_INFO_URL,
|
|
|
+ accessToken,
|
|
|
+ openid
|
|
|
+ );
|
|
|
+
|
|
|
+ String userInfoResponse = restTemplate.getForObject(userInfoUrl, String.class);
|
|
|
+ log.info("获取用户信息响应: {}", userInfoResponse);
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ Map<String, Object> userInfoMap = objectMapper.readValue(userInfoResponse, Map.class);
|
|
|
+
|
|
|
+ if (userInfoMap.containsKey("errcode")) {
|
|
|
+ Integer errcode = (Integer) userInfoMap.get("errcode");
|
|
|
+ String errmsg = (String) userInfoMap.get("errmsg");
|
|
|
+ log.error("获取用户信息失败,errcode: {}, errmsg: {}", errcode, errmsg);
|
|
|
+ throw new RuntimeException("获取用户信息失败: " + errmsg);
|
|
|
+ }
|
|
|
+
|
|
|
+ String nickname = (String) userInfoMap.get("nickname");
|
|
|
+ String avatarUrl = (String) userInfoMap.get("headimgurl");
|
|
|
+ Integer sex = (Integer) userInfoMap.get("sex");
|
|
|
+ String country = (String) userInfoMap.get("country");
|
|
|
+ String province = (String) userInfoMap.get("province");
|
|
|
+ String city = (String) userInfoMap.get("city");
|
|
|
+
|
|
|
+ log.info("获取用户信息成功,nickname: {}, openid: {}", nickname, openid);
|
|
|
+
|
|
|
+ // 3. 检查用户是否已注册
|
|
|
+ LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(User::getOpenid, openid);
|
|
|
+ User user = userMapper.selectOne(wrapper);
|
|
|
+
|
|
|
+ boolean isRegistered = user != null;
|
|
|
+ String token = null;
|
|
|
+
|
|
|
+ if (isRegistered) {
|
|
|
+ // 老用户,直接生成token
|
|
|
+ token = jwtUtil.generateToken(user.getId());
|
|
|
+ log.info("老用户登录,userId: {}, token已生成", user.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ return WxH5UserInfoVO.builder()
|
|
|
+ .openid(openid)
|
|
|
+ .unionid(unionid)
|
|
|
+ .nickname(nickname)
|
|
|
+ .avatarUrl(avatarUrl)
|
|
|
+ .sex(sex)
|
|
|
+ .country(country)
|
|
|
+ .province(province)
|
|
|
+ .city(city)
|
|
|
+ .isRegistered(isRegistered)
|
|
|
+ .token(token)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("获取用户信息失败", e);
|
|
|
+ throw new Exception("获取用户信息失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public LoginVO phoneLogin(H5PhoneLoginDTO dto) throws Exception {
|
|
|
+ try {
|
|
|
+ log.info("手机号登录,openid: {}, phone: {}", dto.getOpenid(), dto.getPhone());
|
|
|
+
|
|
|
+ // 直接使用用户输入的手机号,不需要验证码验证
|
|
|
+ String phone = dto.getPhone();
|
|
|
+
|
|
|
+ if (StrUtil.isBlank(phone)) {
|
|
|
+ throw new RuntimeException("手机号不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 简单的手机号格式验证
|
|
|
+ if (!phone.matches("^1[3-9]\\d{9}$")) {
|
|
|
+ throw new RuntimeException("手机号格式不正确");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("使用手机号: {}", phone);
|
|
|
+
|
|
|
+ // 2. 查询用户是否存在
|
|
|
+ LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ wrapper.eq(User::getPhone, phone);
|
|
|
+ User user = userMapper.selectOne(wrapper);
|
|
|
+
|
|
|
+ if (user == null) {
|
|
|
+ // 新用户,创建账号
|
|
|
+ if (StrUtil.isBlank(dto.getNickname())) {
|
|
|
+ throw new RuntimeException("昵称不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ user = User.builder()
|
|
|
+ .openid(dto.getOpenid())
|
|
|
+ .unionid(dto.getUnionid())
|
|
|
+ .phone(phone)
|
|
|
+ .nickname(dto.getNickname())
|
|
|
+ .avatar(dto.getAvatarUrl())
|
|
|
+ .status(0)
|
|
|
+ .createTime(LocalDateTime.now())
|
|
|
+ .updateTime(LocalDateTime.now())
|
|
|
+ .build();
|
|
|
+
|
|
|
+ userMapper.insert(user);
|
|
|
+ log.info("创建新用户成功,userId: {}, phone: {}", user.getId(), phone);
|
|
|
+ } else {
|
|
|
+ // 老用户,更新openid和unionid
|
|
|
+ boolean needUpdate = false;
|
|
|
+
|
|
|
+ if (StrUtil.isBlank(user.getOpenid())) {
|
|
|
+ user.setOpenid(dto.getOpenid());
|
|
|
+ needUpdate = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (StrUtil.isNotBlank(dto.getUnionid()) && StrUtil.isBlank(user.getUnionid())) {
|
|
|
+ user.setUnionid(dto.getUnionid());
|
|
|
+ needUpdate = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (needUpdate) {
|
|
|
+ user.setUpdateTime(LocalDateTime.now());
|
|
|
+ userMapper.updateById(user);
|
|
|
+ log.info("更新用户微信信息,userId: {}", user.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("老用户登录,userId: {}", user.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 生成token
|
|
|
+ String token = jwtUtil.generateToken(user.getId());
|
|
|
+
|
|
|
+ return LoginVO.builder()
|
|
|
+ .token(token)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("手机号登录失败", e);
|
|
|
+ throw new Exception("登录失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void sendSmsCode(String phone) throws Exception {
|
|
|
+ // 不再需要短信验证码功能,使用微信手机号快速验证
|
|
|
+ throw new UnsupportedOperationException("请使用微信手机号快速验证");
|
|
|
+ }
|
|
|
+}
|