|
@@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
-import org.springframework.beans.BeanUtils;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
@@ -15,23 +14,28 @@ import shop.alien.dining.dto.UserProfileUpdateDto;
|
|
|
import shop.alien.dining.feign.AlienStoreFeign;
|
|
import shop.alien.dining.feign.AlienStoreFeign;
|
|
|
import shop.alien.dining.service.DiningUserService;
|
|
import shop.alien.dining.service.DiningUserService;
|
|
|
import shop.alien.dining.util.WeChatMiniProgramUtil;
|
|
import shop.alien.dining.util.WeChatMiniProgramUtil;
|
|
|
|
|
+import shop.alien.dining.util.WeChatMiniProgramUtil.WeChatSessionInfo;
|
|
|
import shop.alien.entity.result.R;
|
|
import shop.alien.entity.result.R;
|
|
|
import shop.alien.dining.vo.DiningUserVo;
|
|
import shop.alien.dining.vo.DiningUserVo;
|
|
|
|
|
+import shop.alien.dining.vo.TokenVerifyVo;
|
|
|
import shop.alien.entity.store.LifeUser;
|
|
import shop.alien.entity.store.LifeUser;
|
|
|
-import shop.alien.entity.store.vo.LifeUserVo;
|
|
|
|
|
import shop.alien.mapper.LifeUserMapper;
|
|
import shop.alien.mapper.LifeUserMapper;
|
|
|
import shop.alien.util.common.JwtUtil;
|
|
import shop.alien.util.common.JwtUtil;
|
|
|
|
|
+import io.jsonwebtoken.Claims;
|
|
|
|
|
+import io.jsonwebtoken.ExpiredJwtException;
|
|
|
|
|
+import io.jsonwebtoken.MalformedJwtException;
|
|
|
|
|
+import io.jsonwebtoken.SignatureException;
|
|
|
|
|
|
|
|
import java.util.Date;
|
|
import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
-import java.util.concurrent.ThreadLocalRandom;
|
|
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 点餐用户服务实现类(新方式:仅 phoneCode,无需 wx.login)
|
|
|
|
|
|
|
+ * 点餐用户服务实现类
|
|
|
|
|
+ * 标准流程:通过 wx.login() 获取 code,调用 jscode2session 获取 openid
|
|
|
*
|
|
*
|
|
|
* @author ssk
|
|
* @author ssk
|
|
|
- * @version 1.0
|
|
|
|
|
|
|
+ * @version 2.0
|
|
|
* @date 2024/12/4
|
|
* @date 2024/12/4
|
|
|
*/
|
|
*/
|
|
|
@Slf4j
|
|
@Slf4j
|
|
@@ -47,79 +51,241 @@ public class DiningUserServiceImpl implements DiningUserService {
|
|
|
@Value("${jwt.expiration-time}")
|
|
@Value("${jwt.expiration-time}")
|
|
|
private String effectiveTime;
|
|
private String effectiveTime;
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Redis key 常量(小程序专用,避免与 APP 端冲突)
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final String REDIS_KEY_OPENID_PREFIX = "wechat:openid:";
|
|
|
|
|
+ private static final String REDIS_KEY_TOKEN_PREFIX = "miniprogram_user_token:";
|
|
|
|
|
+ private static final String REDIS_KEY_USER_PHONE_PREFIX = "miniprogram_user_";
|
|
|
|
|
+ private static final long OPENID_MAPPING_EXPIRE_SECONDS = 30 * 24 * 60 * 60L; // 30天
|
|
|
|
|
+ private static final long TOKEN_EXPIRE_SECONDS = 7 * 24 * 60 * 60L; // 7天
|
|
|
|
|
+
|
|
|
@Override
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
- public DiningUserVo wechatLogin(String phoneCode, Long storeId, String macIp) {
|
|
|
|
|
- // 1. 通过 phonenumber.getPhoneNumber 换取手机号(必填)
|
|
|
|
|
- String phone = weChatMiniProgramUtil.getPhoneNumberByCode(phoneCode);
|
|
|
|
|
- if (StringUtils.isBlank(phone)) {
|
|
|
|
|
- log.warn("登录失败:无法通过 phoneCode 获取手机号(请先授权手机号,code 有效期5分钟且仅能使用一次)");
|
|
|
|
|
|
|
+ public DiningUserVo wechatLogin(String code, String phoneCode, String macIp) {
|
|
|
|
|
+ // 1. 通过 code2session 获取 openid 和 session_key
|
|
|
|
|
+ String openid = getOpenidFromCode(code);
|
|
|
|
|
+ if (StringUtils.isBlank(openid)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 查找或创建用户
|
|
|
|
|
+ LifeUser user = findOrCreateUser(openid, phoneCode);
|
|
|
|
|
+ if (user == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 检查用户状态(提前检查,避免不必要的操作)
|
|
|
|
|
+ if (!isUserValid(user)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 生成并存储 token
|
|
|
|
|
+ String token = generateAndStoreToken(openid, user);
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 构建返回对象
|
|
|
|
|
+ return buildDiningUserVo(user, token);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 通过 code 获取 openid
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getOpenidFromCode(String code) {
|
|
|
|
|
+ WeChatSessionInfo sessionInfo = weChatMiniProgramUtil.code2Session(code);
|
|
|
|
|
+ if (sessionInfo == null || sessionInfo.getErrcode() != null) {
|
|
|
|
|
+ Integer errcode = sessionInfo != null ? sessionInfo.getErrcode() : null;
|
|
|
|
|
+ String errmsg = sessionInfo != null ? sessionInfo.getErrmsg() : "调用微信接口失败";
|
|
|
|
|
+ log.error("登录失败:code2session 失败, errcode={}, errmsg={}", errcode, errmsg);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String openid = sessionInfo.getOpenid();
|
|
|
|
|
+ if (StringUtils.isBlank(openid)) {
|
|
|
|
|
+ log.error("登录失败:无法获取 openid");
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ log.info("成功获取 openid: {}", maskString(openid, 8));
|
|
|
|
|
+ return openid;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查找或创建用户
|
|
|
|
|
+ */
|
|
|
|
|
+ private LifeUser findOrCreateUser(String openid, String phoneCode) {
|
|
|
|
|
+ // 1. 通过 openid 查找用户(从 Redis 映射)
|
|
|
|
|
+ LifeUser user = findUserByOpenid(openid);
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 如果找不到,尝试通过手机号查找
|
|
|
|
|
+ String phone = null;
|
|
|
|
|
+ if (user == null && StringUtils.isNotBlank(phoneCode)) {
|
|
|
|
|
+ phone = weChatMiniProgramUtil.getPhoneNumberByCode(phoneCode);
|
|
|
|
|
+ if (StringUtils.isNotBlank(phone)) {
|
|
|
|
|
+ log.info("成功获取手机号: {}", maskString(phone, 7));
|
|
|
|
|
+ user = findUserByPhone(phone);
|
|
|
|
|
+ if (user != null) {
|
|
|
|
|
+ // 建立 openid 和 userId 的映射关系
|
|
|
|
|
+ saveOpenidMapping(openid, user.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 用户不存在则创建
|
|
|
|
|
+ if (user == null) {
|
|
|
|
|
+ user = createNewUser(openid, phone);
|
|
|
|
|
+ if (user != null) {
|
|
|
|
|
+ saveOpenidMapping(openid, user.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 4. 如果用户存在但没有手机号,且提供了 phoneCode,则更新手机号
|
|
|
|
|
+ if (StringUtils.isBlank(user.getUserPhone()) && StringUtils.isNotBlank(phone)) {
|
|
|
|
|
+ updateUserPhone(user, phone);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return user;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 通过 openid 查找用户
|
|
|
|
|
+ */
|
|
|
|
|
+ private LifeUser findUserByOpenid(String openid) {
|
|
|
|
|
+ String openidKey = REDIS_KEY_OPENID_PREFIX + openid;
|
|
|
|
|
+ String userIdStr = baseRedisService.getString(openidKey);
|
|
|
|
|
+
|
|
|
|
|
+ if (StringUtils.isBlank(userIdStr)) {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
- log.info("成功获取手机号: {}", phone.substring(0, Math.min(7, phone.length())) + "****");
|
|
|
|
|
|
|
|
|
|
- // 2. 记录店铺ID(如果提供)
|
|
|
|
|
- if (storeId != null) {
|
|
|
|
|
- log.info("登录请求包含店铺ID: storeId={}", storeId);
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ Integer userId = Integer.parseInt(userIdStr);
|
|
|
|
|
+ LifeUser user = lifeUserMapper.selectById(userId);
|
|
|
|
|
+ if (user != null) {
|
|
|
|
|
+ log.info("通过 openid 找到用户: openid={}, userId={}", maskString(openid, 8), userId);
|
|
|
|
|
+ }
|
|
|
|
|
+ return user;
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ log.warn("Redis 中的 userId 格式错误: {}", userIdStr);
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 3. 根据手机号查询用户
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 通过手机号查找用户
|
|
|
|
|
+ */
|
|
|
|
|
+ private LifeUser findUserByPhone(String phone) {
|
|
|
LambdaQueryWrapper<LifeUser> queryWrapper = new LambdaQueryWrapper<>();
|
|
LambdaQueryWrapper<LifeUser> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
queryWrapper.eq(LifeUser::getUserPhone, phone);
|
|
queryWrapper.eq(LifeUser::getUserPhone, phone);
|
|
|
LifeUser user = lifeUserMapper.selectOne(queryWrapper);
|
|
LifeUser user = lifeUserMapper.selectOne(queryWrapper);
|
|
|
|
|
+ if (user != null) {
|
|
|
|
|
+ log.info("通过手机号找到用户: phone={}, userId={}", maskString(phone, 7), user.getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ return user;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 4. 用户不存在则创建,并自动填默认/随机资料
|
|
|
|
|
- if (user == null) {
|
|
|
|
|
- user = new LifeUser();
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建新用户
|
|
|
|
|
+ */
|
|
|
|
|
+ private LifeUser createNewUser(String openid, String phone) {
|
|
|
|
|
+ LifeUser user = new LifeUser();
|
|
|
|
|
+ if (StringUtils.isNotBlank(phone)) {
|
|
|
user.setUserPhone(phone);
|
|
user.setUserPhone(phone);
|
|
|
- // 昵称:微信用户 + 6位随机数 + 时间戳后缀,避免重复
|
|
|
|
|
- String nickSuffix = ThreadLocalRandom.current().nextInt(100000, 1000000)
|
|
|
|
|
- + "_" + (System.currentTimeMillis() % 10000);
|
|
|
|
|
- user.setUserName("微信用户" + nickSuffix);
|
|
|
|
|
|
|
+ user.setUserName(phone);
|
|
|
user.setRealName(phone);
|
|
user.setRealName(phone);
|
|
|
- // 性别:随机 "男" 或 "女",存 user_sex(varchar),默认男
|
|
|
|
|
- user.setUserSex(ThreadLocalRandom.current().nextBoolean() ? "女" : "男");
|
|
|
|
|
- // 生日:先不填,保持 null
|
|
|
|
|
- user.setCreatedTime(new Date());
|
|
|
|
|
- int ret = lifeUserMapper.insert(user);
|
|
|
|
|
- if (ret != 1) {
|
|
|
|
|
- log.error("创建用户失败");
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
- queryWrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
- queryWrapper.eq(LifeUser::getUserPhone, phone);
|
|
|
|
|
- user = lifeUserMapper.selectOne(queryWrapper);
|
|
|
|
|
- log.info("创建新用户: phone={}, userId={}", phone, user.getId());
|
|
|
|
|
} else {
|
|
} else {
|
|
|
- log.info("用户已存在,直接登录: phone={}, userId={}", phone, user.getId());
|
|
|
|
|
|
|
+ // 没有手机号时,使用 openid 后8位作为临时用户名
|
|
|
|
|
+ String tempName = "微信用户" + openid.substring(Math.max(0, openid.length() - 8));
|
|
|
|
|
+ user.setUserName(tempName);
|
|
|
|
|
+ user.setRealName(tempName);
|
|
|
|
|
+ }
|
|
|
|
|
+ user.setCreatedTime(new Date());
|
|
|
|
|
+
|
|
|
|
|
+ int ret = lifeUserMapper.insert(user);
|
|
|
|
|
+ if (ret != 1) {
|
|
|
|
|
+ log.error("创建用户失败");
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // MyBatis-Plus 的 insert 会自动填充 ID,不需要重新查询
|
|
|
|
|
+ log.info("创建新用户: openid={}, userId={}, phone={}", maskString(openid, 8), user.getId(), maskString(phone, 7));
|
|
|
|
|
+ return user;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 更新用户手机号
|
|
|
|
|
+ */
|
|
|
|
|
+ private void updateUserPhone(LifeUser user, String phone) {
|
|
|
|
|
+ user.setUserPhone(phone);
|
|
|
|
|
+ if (StringUtils.isBlank(user.getUserName()) || user.getUserName().startsWith("微信用户")) {
|
|
|
|
|
+ user.setUserName(phone);
|
|
|
}
|
|
}
|
|
|
|
|
+ if (StringUtils.isBlank(user.getRealName()) || user.getRealName().startsWith("微信用户")) {
|
|
|
|
|
+ user.setRealName(phone);
|
|
|
|
|
+ }
|
|
|
|
|
+ user.setUpdatedTime(new Date());
|
|
|
|
|
+ lifeUserMapper.updateById(user);
|
|
|
|
|
+ log.info("更新用户手机号: userId={}, phone={}", user.getId(), maskString(phone, 7));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // 5. 检查用户状态
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存 openid 和 userId 的映射关系
|
|
|
|
|
+ */
|
|
|
|
|
+ private void saveOpenidMapping(String openid, Integer userId) {
|
|
|
|
|
+ String openidKey = REDIS_KEY_OPENID_PREFIX + openid;
|
|
|
|
|
+ baseRedisService.setString(openidKey, userId.toString(), OPENID_MAPPING_EXPIRE_SECONDS);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查用户状态是否有效
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean isUserValid(LifeUser user) {
|
|
|
if (user.getIsBanned() != null && user.getIsBanned() == 1) {
|
|
if (user.getIsBanned() != null && user.getIsBanned() == 1) {
|
|
|
log.warn("用户已被封禁: userId={}", user.getId());
|
|
log.warn("用户已被封禁: userId={}", user.getId());
|
|
|
- return null;
|
|
|
|
|
|
|
+ return false;
|
|
|
}
|
|
}
|
|
|
if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
|
|
if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
|
|
|
log.warn("用户已注销: userId={}", user.getId());
|
|
log.warn("用户已注销: userId={}", user.getId());
|
|
|
- return null;
|
|
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 生成并存储 token
|
|
|
|
|
+ */
|
|
|
|
|
+ private String generateAndStoreToken(String openid, LifeUser user) {
|
|
|
|
|
+ // 构建 token 信息
|
|
|
|
|
+ Map<String, String> tokenMap = buildTokenMap(openid, user);
|
|
|
|
|
+ String userName = StringUtils.isNotBlank(user.getUserName()) ? user.getUserName() : "用户";
|
|
|
|
|
+ String token = generateToken(openid, userName, tokenMap);
|
|
|
|
|
+
|
|
|
|
|
+ // 存入 Redis(使用 openid 作为 key)
|
|
|
|
|
+ baseRedisService.setString(REDIS_KEY_TOKEN_PREFIX + openid, token, TOKEN_EXPIRE_SECONDS);
|
|
|
|
|
+
|
|
|
|
|
+ // 兼容旧版本:如果用户有手机号,也存储
|
|
|
|
|
+ if (StringUtils.isNotBlank(user.getUserPhone())) {
|
|
|
|
|
+ baseRedisService.setString(REDIS_KEY_USER_PHONE_PREFIX + user.getUserPhone(), token, TOKEN_EXPIRE_SECONDS);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 6. 生成 token(不含 openid,新方式不做 code2Session)
|
|
|
|
|
|
|
+ return token;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建 token 信息 Map
|
|
|
|
|
+ */
|
|
|
|
|
+ private Map<String, String> buildTokenMap(String openid, LifeUser user) {
|
|
|
Map<String, String> tokenMap = new HashMap<>();
|
|
Map<String, String> tokenMap = new HashMap<>();
|
|
|
- tokenMap.put("phone", user.getUserPhone());
|
|
|
|
|
- tokenMap.put("userName", user.getUserName() != null ? user.getUserName() : "用户");
|
|
|
|
|
|
|
+ tokenMap.put("openid", openid);
|
|
|
|
|
+ tokenMap.put("phone", StringUtils.isNotBlank(user.getUserPhone()) ? user.getUserPhone() : "");
|
|
|
|
|
+ tokenMap.put("userName", StringUtils.isNotBlank(user.getUserName()) ? user.getUserName() : "用户");
|
|
|
tokenMap.put("userId", user.getId().toString());
|
|
tokenMap.put("userId", user.getId().toString());
|
|
|
tokenMap.put("userType", "user");
|
|
tokenMap.put("userType", "user");
|
|
|
- String token = generateToken(user.getUserPhone(), user.getUserName() != null ? user.getUserName() : "用户", tokenMap);
|
|
|
|
|
-
|
|
|
|
|
- // 7. 存入 Redis
|
|
|
|
|
- baseRedisService.setString("user_" + user.getUserPhone(), token);
|
|
|
|
|
-
|
|
|
|
|
- // 8. 构建返回
|
|
|
|
|
- LifeUserVo userVo = new LifeUserVo();
|
|
|
|
|
- BeanUtils.copyProperties(user, userVo);
|
|
|
|
|
- userVo.setToken(token);
|
|
|
|
|
|
|
+ return tokenMap;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 构建返回对象
|
|
|
|
|
+ */
|
|
|
|
|
+ private DiningUserVo buildDiningUserVo(LifeUser user, String token) {
|
|
|
DiningUserVo diningUserVo = new DiningUserVo();
|
|
DiningUserVo diningUserVo = new DiningUserVo();
|
|
|
diningUserVo.setId(user.getId().longValue());
|
|
diningUserVo.setId(user.getId().longValue());
|
|
|
diningUserVo.setPhone(user.getUserPhone());
|
|
diningUserVo.setPhone(user.getUserPhone());
|
|
@@ -128,10 +294,19 @@ public class DiningUserServiceImpl implements DiningUserService {
|
|
|
diningUserVo.setStatus(0);
|
|
diningUserVo.setStatus(0);
|
|
|
diningUserVo.setCreatedTime(user.getCreatedTime());
|
|
diningUserVo.setCreatedTime(user.getCreatedTime());
|
|
|
diningUserVo.setToken(token);
|
|
diningUserVo.setToken(token);
|
|
|
-
|
|
|
|
|
return diningUserVo;
|
|
return diningUserVo;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 掩码字符串(用于日志脱敏)
|
|
|
|
|
+ */
|
|
|
|
|
+ private String maskString(String str, int visibleLength) {
|
|
|
|
|
+ if (StringUtils.isBlank(str) || str.length() <= visibleLength) {
|
|
|
|
|
+ return "****";
|
|
|
|
|
+ }
|
|
|
|
|
+ return str.substring(0, Math.min(visibleLength, str.length())) + "****";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private String generateToken(String userId, String userName, Map<String, String> tokenMap) {
|
|
private String generateToken(String userId, String userName, Map<String, String> tokenMap) {
|
|
|
int effectiveTimeInt = Integer.parseInt(effectiveTime.substring(0, effectiveTime.length() - 1));
|
|
int effectiveTimeInt = Integer.parseInt(effectiveTime.substring(0, effectiveTime.length() - 1));
|
|
|
String effectiveTimeUnit = effectiveTime.substring(effectiveTime.length() - 1);
|
|
String effectiveTimeUnit = effectiveTime.substring(effectiveTime.length() - 1);
|
|
@@ -249,7 +424,7 @@ public class DiningUserServiceImpl implements DiningUserService {
|
|
|
log.warn("更换手机号失败:验证码格式错误, userId={}, newPhone={}", dto.getUserId(), newPhone);
|
|
log.warn("更换手机号失败:验证码格式错误, userId={}, newPhone={}", dto.getUserId(), newPhone);
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
- R checkRes = alienStoreFeign.checkSmsCode(newPhone, 0, 3, codeInt);
|
|
|
|
|
|
|
+ R<?> checkRes = alienStoreFeign.checkSmsCode(newPhone, 0, 3, codeInt);
|
|
|
if (!R.isSuccess(checkRes)) {
|
|
if (!R.isSuccess(checkRes)) {
|
|
|
log.warn("更换手机号失败:验证码错误或已过期, userId={}, newPhone={}", dto.getUserId(), newPhone);
|
|
log.warn("更换手机号失败:验证码错误或已过期, userId={}, newPhone={}", dto.getUserId(), newPhone);
|
|
|
return null;
|
|
return null;
|
|
@@ -295,13 +470,137 @@ public class DiningUserServiceImpl implements DiningUserService {
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 6. 清除旧手机号对应 token,强制重新登录
|
|
|
|
|
- baseRedisService.delete("user_" + oldPhone);
|
|
|
|
|
- log.info("更换手机号成功, userId={}, oldPhone={}, newPhone={}", dto.getUserId(), oldPhone, newPhone);
|
|
|
|
|
|
|
+ // 6. 只清除小程序旧手机号对应的 token,APP 的 token 保持不变
|
|
|
|
|
+ // 因为小程序更换手机号只影响小程序平台,APP 可以继续使用旧手机号登录
|
|
|
|
|
+ baseRedisService.delete(REDIS_KEY_USER_PHONE_PREFIX + oldPhone);
|
|
|
|
|
+ log.info("更换手机号成功, userId={}, oldPhone={}, newPhone={}(仅清除小程序 token,APP token 保持不变)", dto.getUserId(), oldPhone, newPhone);
|
|
|
|
|
|
|
|
return buildDiningUserVo(user);
|
|
return buildDiningUserVo(user);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public TokenVerifyVo verifyToken(String token) {
|
|
|
|
|
+ TokenVerifyVo verifyVo = new TokenVerifyVo();
|
|
|
|
|
+ verifyVo.setValid(false);
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 检查 token 是否为空
|
|
|
|
|
+ if (StringUtils.isBlank(token)) {
|
|
|
|
|
+ verifyVo.setReason("Token 不能为空");
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 去除 "Bearer " 前缀(不区分大小写)
|
|
|
|
|
+ token = token.trim();
|
|
|
|
|
+ if (token.length() > 7 && token.substring(0, 7).equalsIgnoreCase("Bearer ")) {
|
|
|
|
|
+ token = token.substring(7).trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (StringUtils.isBlank(token)) {
|
|
|
|
|
+ verifyVo.setReason("Token 去除前缀后为空");
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 2. 解析 token,验证格式和签名
|
|
|
|
|
+ Claims claims = JwtUtil.parseJWT(token);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 检查 token 是否过期
|
|
|
|
|
+ Date expiration = claims.getExpiration();
|
|
|
|
|
+ if (expiration != null && expiration.before(new Date())) {
|
|
|
|
|
+ verifyVo.setReason("Token 已过期");
|
|
|
|
|
+ verifyVo.setExpirationTime(expiration);
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 从 token 中提取用户信息
|
|
|
|
|
+ String sub = claims.get("sub").toString();
|
|
|
|
|
+ JSONObject tokenInfo = JSONObject.parseObject(sub);
|
|
|
|
|
+ String openid = tokenInfo.getString("openid");
|
|
|
|
|
+ String phone = tokenInfo.getString("phone");
|
|
|
|
|
+ String userIdStr = tokenInfo.getString("userId");
|
|
|
|
|
+ String userName = tokenInfo.getString("userName");
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 验证 Redis 中是否存在该 token(使用小程序专用的 key 前缀)
|
|
|
|
|
+ boolean tokenExists = false;
|
|
|
|
|
+ if (StringUtils.isNotBlank(openid)) {
|
|
|
|
|
+ // 优先使用 openid 查找
|
|
|
|
|
+ String redisToken = baseRedisService.getString(REDIS_KEY_TOKEN_PREFIX + openid);
|
|
|
|
|
+ if (token.equals(redisToken)) {
|
|
|
|
|
+ tokenExists = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 兼容旧版本:通过手机号查找
|
|
|
|
|
+ if (!tokenExists && StringUtils.isNotBlank(phone)) {
|
|
|
|
|
+ String redisToken = baseRedisService.getString(REDIS_KEY_USER_PHONE_PREFIX + phone);
|
|
|
|
|
+ if (token.equals(redisToken)) {
|
|
|
|
|
+ tokenExists = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!tokenExists) {
|
|
|
|
|
+ verifyVo.setReason("Token 不存在或已失效(可能已退出登录)");
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 验证用户状态(如果提供了 userId)
|
|
|
|
|
+ if (StringUtils.isNotBlank(userIdStr)) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Integer userId = Integer.parseInt(userIdStr);
|
|
|
|
|
+ LifeUser user = lifeUserMapper.selectById(userId);
|
|
|
|
|
+ if (user == null) {
|
|
|
|
|
+ verifyVo.setReason("用户不存在");
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (user.getIsBanned() != null && user.getIsBanned() == 1) {
|
|
|
|
|
+ verifyVo.setReason("用户已被封禁");
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
|
|
|
|
|
+ verifyVo.setReason("用户已注销");
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ log.warn("Token 中的 userId 格式错误: {}", userIdStr);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 7. Token 验证通过,返回用户信息
|
|
|
|
|
+ verifyVo.setValid(true);
|
|
|
|
|
+ if (StringUtils.isNotBlank(userIdStr)) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ verifyVo.setUserId(Long.parseLong(userIdStr));
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ log.warn("无法解析 userId: {}", userIdStr);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ verifyVo.setPhone(phone);
|
|
|
|
|
+ verifyVo.setNickName(userName);
|
|
|
|
|
+ verifyVo.setOpenid(openid);
|
|
|
|
|
+ verifyVo.setExpirationTime(expiration);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("Token 验证成功: userId={}, openid={}, phone={}", userIdStr, openid, phone);
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (ExpiredJwtException e) {
|
|
|
|
|
+ log.warn("Token 已过期: {}", e.getMessage());
|
|
|
|
|
+ verifyVo.setReason("Token 已过期: " + e.getMessage());
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+ } catch (MalformedJwtException e) {
|
|
|
|
|
+ log.warn("Token 格式错误: {}", e.getMessage());
|
|
|
|
|
+ verifyVo.setReason("Token 格式错误: " + e.getMessage());
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+ } catch (SignatureException e) {
|
|
|
|
|
+ log.warn("Token 签名验证失败: {}", e.getMessage());
|
|
|
|
|
+ verifyVo.setReason("Token 签名验证失败: " + e.getMessage());
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("Token 验证异常: {}", e.getMessage(), e);
|
|
|
|
|
+ verifyVo.setReason("Token 验证异常: " + e.getMessage());
|
|
|
|
|
+ return verifyVo;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 构建 DiningUserVo
|
|
* 构建 DiningUserVo
|
|
|
*/
|
|
*/
|