|
|
@@ -0,0 +1,320 @@
|
|
|
+package shop.alien.dining.service.impl;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.beans.BeanUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import shop.alien.dining.config.BaseRedisService;
|
|
|
+import shop.alien.dining.dto.ChangePhoneDto;
|
|
|
+import shop.alien.dining.dto.UserProfileUpdateDto;
|
|
|
+import shop.alien.dining.feign.AlienStoreFeign;
|
|
|
+import shop.alien.dining.service.DiningUserService;
|
|
|
+import shop.alien.dining.util.WeChatMiniProgramUtil;
|
|
|
+import shop.alien.entity.result.R;
|
|
|
+import shop.alien.dining.vo.DiningUserVo;
|
|
|
+import shop.alien.entity.store.LifeUser;
|
|
|
+import shop.alien.entity.store.vo.LifeUserVo;
|
|
|
+import shop.alien.mapper.LifeUserMapper;
|
|
|
+import shop.alien.util.common.JwtUtil;
|
|
|
+
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 点餐用户服务实现类(新方式:仅 phoneCode,无需 wx.login)
|
|
|
+ *
|
|
|
+ * @author ssk
|
|
|
+ * @version 1.0
|
|
|
+ * @date 2024/12/4
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class DiningUserServiceImpl implements DiningUserService {
|
|
|
+
|
|
|
+ private final LifeUserMapper lifeUserMapper;
|
|
|
+ private final BaseRedisService baseRedisService;
|
|
|
+ private final WeChatMiniProgramUtil weChatMiniProgramUtil;
|
|
|
+ private final AlienStoreFeign alienStoreFeign;
|
|
|
+
|
|
|
+ @Value("${jwt.expiration-time}")
|
|
|
+ private String effectiveTime;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @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分钟且仅能使用一次)");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ log.info("成功获取手机号: {}", phone.substring(0, Math.min(7, phone.length())) + "****");
|
|
|
+
|
|
|
+ // 2. 记录店铺ID(如果提供)
|
|
|
+ if (storeId != null) {
|
|
|
+ log.info("登录请求包含店铺ID: storeId={}", storeId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 根据手机号查询用户
|
|
|
+ LambdaQueryWrapper<LifeUser> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(LifeUser::getUserPhone, phone);
|
|
|
+ LifeUser user = lifeUserMapper.selectOne(queryWrapper);
|
|
|
+
|
|
|
+ // 4. 用户不存在则创建
|
|
|
+ if (user == null) {
|
|
|
+ user = new LifeUser();
|
|
|
+ user.setUserPhone(phone);
|
|
|
+ user.setUserName(phone);
|
|
|
+ user.setRealName(phone);
|
|
|
+ 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 {
|
|
|
+ log.info("用户已存在,直接登录: phone={}, userId={}", phone, user.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 检查用户状态
|
|
|
+ if (user.getIsBanned() != null && user.getIsBanned() == 1) {
|
|
|
+ log.warn("用户已被封禁: userId={}", user.getId());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
|
|
|
+ log.warn("用户已注销: userId={}", user.getId());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 生成 token(不含 openid,新方式不做 code2Session)
|
|
|
+ Map<String, String> tokenMap = new HashMap<>();
|
|
|
+ tokenMap.put("phone", user.getUserPhone());
|
|
|
+ tokenMap.put("userName", user.getUserName() != null ? user.getUserName() : "用户");
|
|
|
+ tokenMap.put("userId", user.getId().toString());
|
|
|
+ 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);
|
|
|
+
|
|
|
+ DiningUserVo diningUserVo = new DiningUserVo();
|
|
|
+ diningUserVo.setId(user.getId().longValue());
|
|
|
+ diningUserVo.setPhone(user.getUserPhone());
|
|
|
+ diningUserVo.setNickName(user.getUserName());
|
|
|
+ diningUserVo.setAvatarUrl(user.getUserImage());
|
|
|
+ diningUserVo.setStatus(0);
|
|
|
+ diningUserVo.setCreatedTime(user.getCreatedTime());
|
|
|
+ diningUserVo.setToken(token);
|
|
|
+
|
|
|
+ return diningUserVo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String generateToken(String userId, String userName, Map<String, String> tokenMap) {
|
|
|
+ int effectiveTimeInt = Integer.parseInt(effectiveTime.substring(0, effectiveTime.length() - 1));
|
|
|
+ String effectiveTimeUnit = effectiveTime.substring(effectiveTime.length() - 1);
|
|
|
+ long effectiveTimeIntLong = 0L;
|
|
|
+ switch (effectiveTimeUnit) {
|
|
|
+ case "s":
|
|
|
+ effectiveTimeIntLong = effectiveTimeInt * 1000L;
|
|
|
+ break;
|
|
|
+ case "m":
|
|
|
+ effectiveTimeIntLong = effectiveTimeInt * 60L * 1000L;
|
|
|
+ break;
|
|
|
+ case "h":
|
|
|
+ effectiveTimeIntLong = effectiveTimeInt * 60L * 60L * 1000L;
|
|
|
+ break;
|
|
|
+ case "d":
|
|
|
+ effectiveTimeIntLong = effectiveTimeInt * 24L * 60L * 60L * 1000L;
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ effectiveTimeIntLong = effectiveTimeInt * 24L * 60L * 60L * 1000L;
|
|
|
+ }
|
|
|
+ return JwtUtil.createJWT("user_" + userId, userName, JSONObject.toJSONString(tokenMap), effectiveTimeIntLong);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public DiningUserVo updateProfile(UserProfileUpdateDto dto) {
|
|
|
+ // 1. 查询用户是否存在
|
|
|
+ LifeUser user = lifeUserMapper.selectById(dto.getUserId().intValue());
|
|
|
+ if (user == null) {
|
|
|
+ log.warn("更新个人信息失败:用户不存在, userId={}", dto.getUserId());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 更新用户信息(只更新非空字段)
|
|
|
+ if (StringUtils.isNotBlank(dto.getNickName())) {
|
|
|
+ user.setUserName(dto.getNickName());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(dto.getAvatarUrl())) {
|
|
|
+ user.setUserImage(dto.getAvatarUrl());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(dto.getGender())) {
|
|
|
+ user.setUserSex(dto.getGender());
|
|
|
+ }
|
|
|
+ if (dto.getBirthday() != null) {
|
|
|
+ user.setUserBirthday(dto.getBirthday());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(dto.getRealName())) {
|
|
|
+ user.setRealName(dto.getRealName());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(dto.getProvince())) {
|
|
|
+ user.setProvince(dto.getProvince());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(dto.getCity())) {
|
|
|
+ user.setCity(dto.getCity());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(dto.getDistrict())) {
|
|
|
+ user.setDistrict(dto.getDistrict());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(dto.getAddress())) {
|
|
|
+ user.setAddress(dto.getAddress());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(dto.getJianjie())) {
|
|
|
+ user.setJianjie(dto.getJianjie());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 设置更新时间
|
|
|
+ user.setUpdatedTime(new Date());
|
|
|
+
|
|
|
+ // 4. 执行更新
|
|
|
+ int result = lifeUserMapper.updateById(user);
|
|
|
+ if (result != 1) {
|
|
|
+ log.error("更新用户信息失败, userId={}", dto.getUserId());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ log.info("用户信息更新成功, userId={}", dto.getUserId());
|
|
|
+
|
|
|
+ // 5. 返回更新后的用户信息
|
|
|
+ return buildDiningUserVo(user);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public DiningUserVo getUserInfo(Long userId) {
|
|
|
+ // 1. 查询用户
|
|
|
+ LifeUser user = lifeUserMapper.selectById(userId.intValue());
|
|
|
+ if (user == null) {
|
|
|
+ log.warn("获取用户信息失败:用户不存在, userId={}", userId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 检查用户状态
|
|
|
+ if (user.getIsBanned() != null && user.getIsBanned() == 1) {
|
|
|
+ log.warn("用户已被封禁: userId={}", userId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
|
|
|
+ log.warn("用户已注销: userId={}", userId);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 返回用户信息
|
|
|
+ return buildDiningUserVo(user);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public DiningUserVo changePhone(ChangePhoneDto dto) {
|
|
|
+ String newPhone = dto.getNewPhone().trim();
|
|
|
+ String codeStr = dto.getCode().trim();
|
|
|
+
|
|
|
+ // 1. 校验验证码(Feign 调 store ali/checkSmsCode,appType=0 用户端,businessType=3 修改手机号)
|
|
|
+ int codeInt;
|
|
|
+ try {
|
|
|
+ codeInt = Integer.parseInt(codeStr);
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.warn("更换手机号失败:验证码格式错误, userId={}, newPhone={}", dto.getUserId(), newPhone);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ R checkRes = alienStoreFeign.checkSmsCode(newPhone, 0, 3, codeInt);
|
|
|
+ if (!R.isSuccess(checkRes)) {
|
|
|
+ log.warn("更换手机号失败:验证码错误或已过期, userId={}, newPhone={}", dto.getUserId(), newPhone);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 查询用户
|
|
|
+ LifeUser user = lifeUserMapper.selectById(dto.getUserId().intValue());
|
|
|
+ if (user == null) {
|
|
|
+ log.warn("更换手机号失败:用户不存在, userId={}", dto.getUserId());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (user.getIsBanned() != null && user.getIsBanned() == 1) {
|
|
|
+ log.warn("更换手机号失败:用户已被封禁, userId={}", dto.getUserId());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
|
|
|
+ log.warn("更换手机号失败:用户已注销, userId={}", dto.getUserId());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 新手机号与当前相同则无需更新
|
|
|
+ if (newPhone.equals(user.getUserPhone())) {
|
|
|
+ log.info("新手机号与当前相同,无需更换, userId={}", dto.getUserId());
|
|
|
+ return buildDiningUserVo(user);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 新手机号是否已被其他用户使用
|
|
|
+ LambdaQueryWrapper<LifeUser> q = new LambdaQueryWrapper<>();
|
|
|
+ q.eq(LifeUser::getUserPhone, newPhone);
|
|
|
+ LifeUser existing = lifeUserMapper.selectOne(q);
|
|
|
+ if (existing != null && !existing.getId().equals(user.getId())) {
|
|
|
+ log.warn("更换手机号失败:新手机号已被其他用户使用, newPhone={}", newPhone);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 更新 user_phone
|
|
|
+ String oldPhone = user.getUserPhone();
|
|
|
+ user.setUserPhone(newPhone);
|
|
|
+ user.setUpdatedTime(new Date());
|
|
|
+ int n = lifeUserMapper.updateById(user);
|
|
|
+ if (n != 1) {
|
|
|
+ log.error("更换手机号失败:更新数据库异常, userId={}", dto.getUserId());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 6. 清除旧手机号对应 token,强制重新登录
|
|
|
+ baseRedisService.delete("user_" + oldPhone);
|
|
|
+ log.info("更换手机号成功, userId={}, oldPhone={}, newPhone={}", dto.getUserId(), oldPhone, newPhone);
|
|
|
+
|
|
|
+ return buildDiningUserVo(user);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建 DiningUserVo
|
|
|
+ */
|
|
|
+ private DiningUserVo buildDiningUserVo(LifeUser user) {
|
|
|
+ DiningUserVo diningUserVo = new DiningUserVo();
|
|
|
+ diningUserVo.setId(user.getId().longValue());
|
|
|
+ diningUserVo.setPhone(user.getUserPhone());
|
|
|
+ diningUserVo.setNickName(user.getUserName());
|
|
|
+ diningUserVo.setAvatarUrl(user.getUserImage());
|
|
|
+ diningUserVo.setStatus(0);
|
|
|
+ diningUserVo.setCreatedTime(user.getCreatedTime());
|
|
|
+ // 补充更多字段
|
|
|
+ diningUserVo.setGender(user.getUserSex());
|
|
|
+ diningUserVo.setBirthday(user.getUserBirthday());
|
|
|
+ diningUserVo.setRealName(user.getRealName());
|
|
|
+ diningUserVo.setProvince(user.getProvince());
|
|
|
+ diningUserVo.setCity(user.getCity());
|
|
|
+ diningUserVo.setDistrict(user.getDistrict());
|
|
|
+ diningUserVo.setAddress(user.getAddress());
|
|
|
+ diningUserVo.setJianjie(user.getJianjie());
|
|
|
+ return diningUserVo;
|
|
|
+ }
|
|
|
+}
|