瀏覽代碼

Merge remote-tracking branch 'origin/sit' into uat-20260202

dujian 5 小時之前
父節點
當前提交
02afc4bb63
共有 29 個文件被更改,包括 590 次插入60 次删除
  1. 6 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeUser.java
  2. 25 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/LifeUserPasswordDto.java
  3. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserVo.java
  4. 9 3
      alien-entity/src/main/java/shop/alien/mapper/StoreCommentAppealMapper.java
  5. 1 1
      alien-gateway/src/main/java/shop/alien/gateway/controller/LifeUserController.java
  6. 42 0
      alien-gateway/src/main/java/shop/alien/gateway/controller/LifeUserPasswordController.java
  7. 146 0
      alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserPasswordService.java
  8. 2 0
      alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserService.java
  9. 1 1
      alien-gateway/src/main/java/shop/alien/gateway/service/impl/LawyerUserLogInServiceImpl.java
  10. 2 2
      alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/LawyerUserLogInServiceImpl.java
  11. 4 0
      alien-second/src/main/java/shop/alien/second/controller/SecondGoodsController.java
  12. 7 0
      alien-second/src/main/java/shop/alien/second/service/SecondGoodsService.java
  13. 26 0
      alien-second/src/main/java/shop/alien/second/service/impl/SecondGoodsServiceImpl.java
  14. 1 1
      alien-store-platform/src/main/java/shop/alien/storeplatform/controller/StoreBusinessController.java
  15. 4 4
      alien-store-platform/src/main/java/shop/alien/storeplatform/controller/StorePlatformLoginController.java
  16. 11 5
      alien-store/src/main/java/shop/alien/store/controller/AliController.java
  17. 38 0
      alien-store/src/main/java/shop/alien/store/controller/LifeUserPasswordController.java
  18. 4 4
      alien-store/src/main/java/shop/alien/store/controller/StoreCommentAppealController.java
  19. 1 1
      alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java
  20. 4 4
      alien-store/src/main/java/shop/alien/store/controller/StoreUserController.java
  21. 10 0
      alien-store/src/main/java/shop/alien/store/service/LifeUserPasswordService.java
  22. 7 1
      alien-store/src/main/java/shop/alien/store/service/StoreCommentAppealService.java
  23. 1 1
      alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java
  24. 1 1
      alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponStoreFriendServiceImpl.java
  25. 50 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeUserPasswordServiceImpl.java
  26. 78 31
      alien-store/src/main/java/shop/alien/store/service/impl/StoreCommentAppealServiceImpl.java
  27. 14 0
      alien-store/src/main/java/shop/alien/store/util/ali/AliSms.java
  28. 11 0
      alien-store/src/main/java/shop/alien/store/util/ali/SmsSendLimitException.java
  29. 81 0
      alien-store/src/main/java/shop/alien/store/util/ali/UserForgetPasswordSmsDailyLimiter.java

+ 6 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeUser.java

@@ -2,6 +2,7 @@ package shop.alien.entity.store;
 
 import com.baomidou.mybatisplus.annotation.*;
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
@@ -49,6 +50,11 @@ public class LifeUser implements Serializable {
     @TableField("user_phone")
     private String userPhone;
 
+    @ApiModelProperty(value = "登录密码")
+    @TableField("password")
+    @JsonIgnore
+    private String password;
+
     @ApiModelProperty(value = "用户头像照片")
     @TableField("user_image")
     private String userImage;

+ 25 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/LifeUserPasswordDto.java

@@ -0,0 +1,25 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("用户端密码相关请求")
+public class LifeUserPasswordDto {
+
+    @ApiModelProperty("手机号")
+    private String phoneNum;
+
+    @ApiModelProperty("密码")
+    private String password;
+
+    @ApiModelProperty("确认密码")
+    private String confirmPassword;
+
+    @ApiModelProperty("短信验证码(忘记密码时使用)")
+    private String code;
+
+    @ApiModelProperty("设备标识(登录时使用,可选)")
+    private String macIp;
+}

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserVo.java

@@ -69,4 +69,7 @@ public class LifeUserVo extends LifeUser {
     @ApiModelProperty(value = "验证码")
     @JsonAlias("code")
     private String verificationCode;
+
+    @ApiModelProperty(value = "是否已设置登录密码")
+    private Boolean hasLoginPassword;
 }

+ 9 - 3
alien-entity/src/main/java/shop/alien/mapper/StoreCommentAppealMapper.java

@@ -133,9 +133,15 @@ public interface StoreCommentAppealMapper extends BaseMapper<StoreCommentAppeal>
                           @Param("appealStatus") Integer appealStatus,
                           @Param("finalResult") String finalResult);
 
-    @Select("select count(1)\n" +
+    /**
+     * 查询用户近30天评价被申诉通过的记录,按通过时间倒序(最新在前)
+     */
+    @Select("select *\n" +
             "from store_comment_appeal sca\n" +
             "where sca.comment_id in (select id from common_rating cr where cr.user_id=#{userId} and cr.business_type = 1)\n" +
-            "and sca.created_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)")
-    int canRate(@Param("userId") Integer userId);
+            "and sca.appeal_status = 2\n" +
+            "and sca.delete_flag = 0\n" +
+            "and sca.created_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)\n" +
+            "order by sca.updated_time desc")
+    List<StoreCommentAppeal> canRate(@Param("userId") Integer userId);
 }

+ 1 - 1
alien-gateway/src/main/java/shop/alien/gateway/controller/LifeUserController.java

@@ -57,7 +57,7 @@ public class LifeUserController {
             return R.fail("当前验证码过期或未发送");
         }
         if (!cacheCode.trim().equals(code.trim())) {
-            return R.fail("验证码错误");
+            return R.fail("验证码错误,请重新输入");
         }
 
         if(StringUtils.isNotBlank(inviteCode)){

+ 42 - 0
alien-gateway/src/main/java/shop/alien/gateway/controller/LifeUserPasswordController.java

@@ -0,0 +1,42 @@
+package shop.alien.gateway.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.LifeUserPasswordDto;
+import shop.alien.entity.store.vo.LifeUserVo;
+import shop.alien.gateway.service.LifeUserPasswordService;
+
+/**
+ * 用户端密码登录(独立接口,不影响验证码登录)
+ */
+@Api(tags = {"一期-用户-密码登录"})
+@Slf4j
+@CrossOrigin
+@RestController
+@RequestMapping("/user/password")
+@RequiredArgsConstructor
+public class LifeUserPasswordController {
+
+    private final LifeUserPasswordService lifeUserPasswordService;
+
+    @ApiOperation("用户密码登录")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/login")
+    public R<LifeUserVo> passwordLogin(@RequestBody LifeUserPasswordDto dto) {
+        log.info("LifeUserPasswordController.passwordLogin?phoneNum={}", dto != null ? dto.getPhoneNum() : null);
+        return lifeUserPasswordService.passwordLogin(dto);
+    }
+
+    @ApiOperation("忘记密码")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/forget")
+    public R<String> forgetPassword(@RequestBody LifeUserPasswordDto dto) {
+        log.info("LifeUserPasswordController.forgetPassword?phoneNum={}", dto != null ? dto.getPhoneNum() : null);
+        return lifeUserPasswordService.forgetPassword(dto);
+    }
+}

+ 146 - 0
alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserPasswordService.java

@@ -0,0 +1,146 @@
+package shop.alien.gateway.service;
+
+import com.alibaba.fastjson2.JSONObject;
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.dto.LifeUserPasswordDto;
+import shop.alien.entity.store.vo.LifeUserVo;
+import shop.alien.gateway.config.BaseRedisService;
+import shop.alien.gateway.mapper.LifeUserGatewayMapper;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 用户端密码登录服务(独立实现,不修改原有验证码登录逻辑)
+ */
+@Service
+@RequiredArgsConstructor
+public class LifeUserPasswordService {
+
+    private final LifeUserGatewayMapper lifeUserMapper;
+
+    private final BaseRedisService baseRedisService;
+
+    private final LifeUserService lifeUserService;
+
+    @Value("${jwt.expiration-time}")
+    private String effectiveTime;
+
+    public R<LifeUserVo> passwordLogin(LifeUserPasswordDto dto) {
+        if (dto == null || StringUtils.isBlank(dto.getPhoneNum()) || StringUtils.isBlank(dto.getPassword())) {
+            return R.fail("手机号和密码不能为空");
+        }
+        LifeUser user = lifeUserService.getUserByPhone(dto.getPhoneNum());
+        if (user == null) {
+            return R.fail("当前账号不存在,请先去注册账号");
+        }
+        if (StringUtils.isBlank(user.getPassword())) {
+            return R.fail("尚未设置登录密码,请使用验证码登录或设置密码");
+        }
+        if (!Objects.equals(dto.getPassword(), user.getPassword())) {
+            return R.fail("密码错误");
+        }
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            return R.fail("您的账户因严重违规导致被封禁");
+        }
+        if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+            return R.fail("你的账号已注销");
+        }
+        return R.data(createLoginVo(user, dto.getPhoneNum(), dto.getMacIp()));
+    }
+
+    public R<String> forgetPassword(LifeUserPasswordDto dto) {
+        if (dto == null || StringUtils.isBlank(dto.getPhoneNum()) || StringUtils.isBlank(dto.getCode())) {
+            return R.fail("手机号和验证码不能为空");
+        }
+        if (StringUtils.isBlank(dto.getPassword())) {
+            return R.fail("密码不能为空");
+        }
+        if (!Objects.equals(dto.getPassword(), dto.getConfirmPassword())) {
+            return R.fail("两次密码输入不一致");
+        }
+        String cacheCode = baseRedisService.getString("verification_user_forget_password_" + dto.getPhoneNum());
+        if (cacheCode == null) {
+            return R.fail("验证码过期或未发送");
+        }
+        if (!cacheCode.trim().equals(dto.getCode().trim())) {
+            return R.fail("验证码错误,请重新输入");
+        }
+        LifeUser user = lifeUserService.getUserByPhone(dto.getPhoneNum());
+        if (user == null) {
+            return R.fail("当前账号不存在,请先去注册账号");
+        }
+        LifeUser update = new LifeUser();
+        update.setId(user.getId());
+        update.setPassword(dto.getPassword());
+        if (lifeUserMapper.updateById(update) <= 0) {
+            return R.fail("重置密码失败");
+        }
+        baseRedisService.delete("verification_user_forget_password_" + dto.getPhoneNum());
+        invalidateUserSessions(dto.getPhoneNum());
+        return R.success("密码重置成功");
+    }
+
+    private LifeUserVo createLoginVo(LifeUser user, String phoneNum, String macIp) {
+        LifeUserVo userVo = new LifeUserVo();
+        BeanUtils.copyProperties(user, userVo);
+        userVo.setPassword(null);
+        Map<String, String> tokenMap = new HashMap<>();
+        tokenMap.put("phone", phoneNum);
+        tokenMap.put("userName", user.getUserName());
+        tokenMap.put("userId", user.getId().toString());
+        tokenMap.put("userType", "user");
+        String token = createToken(phoneNum, user.getUserName(), tokenMap);
+        userVo.setToken(token);
+        addSessionToken(phoneNum, token);
+        lifeUserService.addLifeUserLogInfo(user, macIp);
+        return userVo;
+    }
+
+    private String createToken(String phoneNum, 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:
+                break;
+        }
+        return JwtUtil.createJWT("user_" + phoneNum, userName, JSONObject.toJSONString(tokenMap), effectiveTimeIntLong);
+    }
+
+    private void addSessionToken(String phone, String token) {
+        String legacyKey = "user_" + phone;
+        String sessionSetKey = "user_sessions:" + phone;
+        String oldSingle = baseRedisService.getString(legacyKey);
+        if (oldSingle != null && !oldSingle.isEmpty()) {
+            baseRedisService.setSetList(sessionSetKey, oldSingle);
+        }
+        baseRedisService.delete(legacyKey);
+        baseRedisService.setSetList(sessionSetKey, token);
+    }
+
+    private void invalidateUserSessions(String phone) {
+        baseRedisService.delete("user_" + phone);
+        baseRedisService.delete("user_sessions:" + phone);
+    }
+}

+ 2 - 0
alien-gateway/src/main/java/shop/alien/gateway/service/LifeUserService.java

@@ -92,6 +92,7 @@ public class LifeUserService extends ServiceImpl<LifeUserGatewayMapper, LifeUser
                 addLifeUserSessionToken(phoneNum, token);
                 // 二手平台登录log,同一个macip登录多账号记录
                 addLifeUserLogInfo(user2, macIp);
+                userVo.setHasLoginPassword(StringUtils.isNotBlank(user2.getPassword()));
 
                 return userVo;
             } else {
@@ -110,6 +111,7 @@ public class LifeUserService extends ServiceImpl<LifeUserGatewayMapper, LifeUser
             addLifeUserSessionToken(phoneNum, token);
             // 二手平台登录log,同一个macip登录多账号记录
             addLifeUserLogInfo(user, macIp);
+            userVo.setHasLoginPassword(StringUtils.isNotBlank(user.getPassword()));
 
             return userVo;
         }

+ 1 - 1
alien-gateway/src/main/java/shop/alien/gateway/service/impl/LawyerUserLogInServiceImpl.java

@@ -140,7 +140,7 @@
 //                        .eq(LawyerUser::getPhone, lawyerUserDto.getPhone()));
 //                return R.data(lawyerUser);
 //            }else {
-//                return R.fail("验证码错误");
+//                return R.fail("验证码错误,请重新输入");
 //            }
 //        }else {
 //            return R.fail("验证码已过期");

+ 2 - 2
alien-lawyer/src/main/java/shop/alien/lawyer/service/impl/LawyerUserLogInServiceImpl.java

@@ -165,7 +165,7 @@ public class LawyerUserLogInServiceImpl extends ServiceImpl<LawyerUserMapper, La
                 }
                 return R.data(ObjectUtils.isNotEmpty(lawyerUser) ? vo : "null");
             }else {
-                return R.fail("验证码错误");
+                return R.fail("验证码错误,请重新输入");
             }
         }else {
             return R.fail("验证码已过期");
@@ -201,7 +201,7 @@ public class LawyerUserLogInServiceImpl extends ServiceImpl<LawyerUserMapper, La
                 }
                 return R.data(ObjectUtils.isNotEmpty(lawyerUser) ? vo : "null");
             }else {
-                return R.fail("验证码错误");
+                return R.fail("验证码错误,请重新输入");
             }
         }else {
             return R.fail("验证码已过期");

+ 4 - 0
alien-second/src/main/java/shop/alien/second/controller/SecondGoodsController.java

@@ -114,6 +114,10 @@ public class SecondGoodsController {
             }
             return R.success("保存商品为草稿成功");
         } else {
+            String validateMsg = secondGoodsService.validatePublishQualification(secondGoods.getUserId());
+            if (validateMsg != null) {
+                return R.fail(validateMsg);
+            }
             // 添加商品 0 创建
             if (!secondGoodsService.createBasicInfo(secondGoods,0)) {
                 return R.fail("添加二手商品失败");

+ 7 - 0
alien-second/src/main/java/shop/alien/second/service/SecondGoodsService.java

@@ -223,4 +223,11 @@ public interface SecondGoodsService extends IService<SecondGoods> {
      * @param goods 商品信息
      */
     void performPublishRiskCheck(SecondGoods goods);
+
+    /**
+     * 校验用户是否具备发布商品资格(实名认证 + 信用分≥100)
+     * @param userId 用户ID
+     * @return 校验不通过时返回错误信息,通过时返回 null
+     */
+    String validatePublishQualification(Integer userId);
 }

+ 26 - 0
alien-second/src/main/java/shop/alien/second/service/impl/SecondGoodsServiceImpl.java

@@ -597,6 +597,32 @@ public class SecondGoodsServiceImpl extends ServiceImpl<SecondGoodsMapper, Secon
     }
 
     /**
+     * 校验用户是否具备发布商品资格
+     */
+    @Override
+    public String validatePublishQualification(Integer userId) {
+        if (userId == null) {
+            return "用户未登录,无法发布商品";
+        }
+        LifeUser user = lifeUserMapper.selectById(userId);
+        if (user == null) {
+            return "用户不存在";
+        }
+        if (StringUtils.isBlank(user.getRealName()) || StringUtils.isBlank(user.getIdCard())) {
+            return "请先完成实名认证后再发布商品";
+        }
+        LambdaQueryWrapper<SecondUserCredit> creditWrapper = new LambdaQueryWrapper<>();
+        creditWrapper.eq(SecondUserCredit::getUserId, userId)
+                .eq(SecondUserCredit::getDeleteFlag, 0);
+        SecondUserCredit userCredit = secondUserCreditMapper.selectOne(creditWrapper);
+        int userPoints = userCredit != null && userCredit.getUserPoints() != null ? userCredit.getUserPoints() : 0;
+        if (userPoints < 100) {
+            return "信用分不足100,无法发布商品";
+        }
+        return null;
+    }
+
+    /**
      * 保存商品为草稿状态
      * @param goods 商品实体
      * @return 是否成功保存

+ 1 - 1
alien-store-platform/src/main/java/shop/alien/storeplatform/controller/StoreBusinessController.java

@@ -372,7 +372,7 @@ public class StoreBusinessController {
             return R.fail("当验证码过期或未发送");
         }
         if (!cacheCode.trim().equals(storeInfo.getVerificationCode().trim())) {
-            return R.fail("验证码错误");
+            return R.fail("验证码错误,请重新输入");
         }
         try {
             storeBusinessService.logoutStore(storeInfo);

+ 4 - 4
alien-store-platform/src/main/java/shop/alien/storeplatform/controller/StorePlatformLoginController.java

@@ -47,7 +47,7 @@ public class StorePlatformLoginController {
             return R.fail("当前验证码已过期或未发送");
         }
         if (!cacheCode.trim().equals(code.trim())) {
-            return R.fail("验证码错误");
+            return R.fail("验证码错误,请重新输入");
         }
         log.info("StorePlatformLoginController.register?phone={}&password={}", phone, password);
         return storePlatformLoginService.register(phone, password);
@@ -88,7 +88,7 @@ public class StorePlatformLoginController {
                 return R.fail("验证码过期或未发送");
             }
             if (!cacheCode.trim().equals(code.trim())) {
-                return R.fail("验证码错误");
+                return R.fail("验证码错误,请重新输入");
             }
         } else {
 //            String cacheCode = baseRedisService.getString("store_platform_captcha_" + phone);
@@ -97,7 +97,7 @@ public class StorePlatformLoginController {
                 return R.fail("验证码已过期");
             }
             if (!cacheCode.trim().equals(captcha.trim())) {
-                return R.fail("验证码错误");
+                return R.fail("验证码错误,请重新输入");
             }
         }
 
@@ -154,7 +154,7 @@ public class StorePlatformLoginController {
                 return R.fail("当验证码过期或未发送");
             }
             if (!cacheCode.trim().equals(verificationCode.trim())) {
-                return R.fail("验证码错误");
+                return R.fail("验证码错误,请重新输入");
             }
         }
         return storePlatformLoginService.forgetOrModifyPassword(phone, newPhone, oldPassword, newPassword, confirmNewPassword, verificationCode, type);

+ 11 - 5
alien-store/src/main/java/shop/alien/store/controller/AliController.java

@@ -14,6 +14,7 @@ import shop.alien.store.service.AliService;
 import shop.alien.store.service.LifeUserService;
 import shop.alien.store.util.ali.AliApi;
 import shop.alien.store.util.ali.AliSms;
+import shop.alien.store.util.ali.SmsSendLimitException;
 import shop.alien.util.ali.AliOSSUtil;
 import shop.alien.util.common.AlipayTradeAppPay;
 import shop.alien.util.common.AlipayTradeRefund;
@@ -119,12 +120,17 @@ public class AliController {
             @RequestParam("appType") Integer appType,
             @RequestParam("businessType") Integer businessType
     ) {
-        Integer code = aliSmsConfig.sendSms(phone, appType, businessType);
-        log.info("AliController.sendSms?phone={}&code={}&businessType={}", phone, code, businessType);
-        if (code != null) {
-            return R.data("短信发送成功");
+        try {
+            Integer code = aliSmsConfig.sendSms(phone, appType, businessType);
+            log.info("AliController.sendSms?phone={}&code={}&businessType={}", phone, code, businessType);
+            if (code != null) {
+                return R.data("短信发送成功");
+            }
+            return R.fail("短信发送失败");
+        } catch (SmsSendLimitException e) {
+            log.warn("AliController.sendSms limit exceeded, phone={}, businessType={}", phone, businessType);
+            return R.fail(e.getMessage());
         }
-        return R.fail("短信发送失败");
     }
 
 

+ 38 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeUserPasswordController.java

@@ -0,0 +1,38 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.entity.store.dto.LifeUserPasswordDto;
+import shop.alien.store.service.LifeUserPasswordService;
+import shop.alien.util.common.TokenInfo;
+import springfox.documentation.annotations.ApiIgnore;
+
+/**
+ * 用户端登录密码(设置密码在 store 服务,登录/忘记密码仍在 gateway)
+ */
+@Api(tags = {"一期-用户-密码登录"})
+@Slf4j
+@CrossOrigin
+@RestController
+@RequestMapping("/user/password")
+@RequiredArgsConstructor
+public class LifeUserPasswordController {
+
+    private final LifeUserPasswordService lifeUserPasswordService;
+
+    @ApiOperation("设置当前账号登录密码(需登录)")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/set")
+    public R<String> setPassword(@ApiIgnore @TokenInfo UserLoginInfo userLoginInfo,
+                                 @RequestBody LifeUserPasswordDto dto) {
+        log.info("LifeUserPasswordController.setPassword, userId={}",
+                userLoginInfo == null ? null : userLoginInfo.getUserId());
+        return lifeUserPasswordService.setPassword(userLoginInfo, dto);
+    }
+}

+ 4 - 4
alien-store/src/main/java/shop/alien/store/controller/StoreCommentAppealController.java

@@ -13,6 +13,7 @@ import shop.alien.entity.store.vo.StoreCommentAppealVo;
 import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StoreCommentAppealService;
 
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -157,11 +158,10 @@ public class StoreCommentAppealController {
         return R.data(storeCommentAppealService.getAppealHistoryCountStatus(storeId));
     }
 
-    // TODO 查询当前用户前30天所有评价被申诉通过的次数,如果> 3 次返回true
-    @ApiOperation("是否可以评价")
+    @ApiOperation("是否可以评价(含最新申诉通过记录与10天冷却倒计时)")
     @GetMapping("/canRate")
-    public R<Boolean> canRate(Integer userId) {
-        log.info("StoreCommentAppealController.canRate");
+    public R<Map<String, Object>> canRate(Integer userId) {
+        log.info("StoreCommentAppealController.canRate?userId={}", userId);
         return R.data(storeCommentAppealService.canRate(userId));
     }
 }

+ 1 - 1
alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java

@@ -600,7 +600,7 @@ public class StoreInfoController {
             return R.fail("当验证码过期或未发送");
         }
         if (!cacheCode.trim().equals(storeInfo.getVerificationCode().trim())) {
-            return R.fail("验证码错误");
+            return R.fail("验证码错误,请重新输入");
         }
         try {
             storeInfoService.logoutStore(storeInfo);

+ 4 - 4
alien-store/src/main/java/shop/alien/store/controller/StoreUserController.java

@@ -100,7 +100,7 @@ public class StoreUserController {
             return R.fail("当验证码过期或未发送");
         }
         if (StringUtils.isBlank(oldPassword) && !cacheCode.trim().equals(verificationCode.trim())) {
-            return R.fail("验证码错误");
+            return R.fail("验证码错误,请重新输入");
         }
         return storeUserService.forgetOrModifyPassword(phone, newPhone, oldPassword, newPassword, confirmNewPassword, verificationCode, type);
     }
@@ -117,7 +117,7 @@ public class StoreUserController {
             return R.fail("当验证码过期或未发送");
         }
         if (!cacheCode.trim().equals(verificationCode.trim())) {
-            return R.fail("验证码错误");
+            return R.fail("验证码错误,请重新输入");
         }
         try {
             return R.data(storeUserService.changePhoneVerification(phone, oldPassword, verificationCode));
@@ -331,7 +331,7 @@ public class StoreUserController {
             return R.fail("当验证码过期或未发送");
         }
         if (!cacheCode.trim().equals(code.trim())) {
-            return R.fail("验证码错误");
+            return R.fail("验证码错误,请重新输入");
         }
         log.info("StoreUserController.register?phone={}&password={}", phone, password);
         return storeUserService.register(phone, password);
@@ -393,7 +393,7 @@ public class StoreUserController {
             return R.fail("当验证码过期或未发送");
         }
         if (!cacheCode.trim().equals(storeUserVo.getVerificationCode().trim())) {
-            return R.fail("验证码错误");
+            return R.fail("验证码错误,请重新输入");
         }
         try {
             storeUserService.storeCancelAccount(storeUserVo);

+ 10 - 0
alien-store/src/main/java/shop/alien/store/service/LifeUserPasswordService.java

@@ -0,0 +1,10 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.entity.store.dto.LifeUserPasswordDto;
+
+public interface LifeUserPasswordService {
+
+    R<String> setPassword(UserLoginInfo login, LifeUserPasswordDto dto);
+}

+ 7 - 1
alien-store/src/main/java/shop/alien/store/service/StoreCommentAppealService.java

@@ -104,5 +104,11 @@ public interface StoreCommentAppealService extends IService<StoreCommentAppeal>
      */
     ResponseEntity<byte[]> exportToExcel();
 
-    Boolean canRate(Integer userId);
+    /**
+     * 判断用户是否可以评价,并返回最新申诉通过记录及10天冷却倒计时
+     *
+     * @param userId 用户ID
+     * @return canRate-是否可评价, passedCount-近30天申诉通过次数, latestPassedAppeal-最新通过记录, countdown-10天内剩余冷却(X天X小时)
+     */
+    Map<String, Object> canRate(Integer userId);
 }

+ 1 - 1
alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java

@@ -1293,7 +1293,7 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
         QueryWrapper<CommonRating> wrapper = new QueryWrapper<>();
         wrapper.eq(businessType!=null,"cr.business_type", businessType)
                 .eq(userId != null, "cr.user_id", userId)
-                .eq("cr.delete_flag", 0)
+//                .eq("cr.delete_flag", 0)
                 .eq(auditStatus != null,"cr.audit_status", auditStatus);
         wrapper.eq("cr.is_show", 1);
         wrapper.orderByDesc("cr.created_time");

+ 1 - 1
alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponStoreFriendServiceImpl.java

@@ -342,7 +342,7 @@ public class LifeDiscountCouponStoreFriendServiceImpl extends ServiceImpl<LifeDi
             }
         }
         LifeNotice lifeMessage = new LifeNotice();
-        String text = "您的好友" + storeName + "送了您" + qty + "张优惠券,快去使用吧!";
+        String text = "您的好友" + storeName + "送了您" + qty + "张优惠券";
         JSONObject jsonObject = new JSONObject();
         jsonObject.put("message", text);
         lifeMessage.setReceiverId("store_" + friendPhone);

+ 50 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeUserPasswordServiceImpl.java

@@ -0,0 +1,50 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.entity.store.UserLoginInfo;
+import shop.alien.entity.store.dto.LifeUserPasswordDto;
+import shop.alien.mapper.LifeUserMapper;
+import shop.alien.store.service.LifeUserPasswordService;
+
+import java.util.Objects;
+
+@Service
+@RequiredArgsConstructor
+public class LifeUserPasswordServiceImpl implements LifeUserPasswordService {
+
+    private final LifeUserMapper lifeUserMapper;
+
+    @Override
+    public R<String> setPassword(UserLoginInfo login, LifeUserPasswordDto dto) {
+        if (login == null || login.getUserId() <= 0) {
+            return R.fail("请先登录");
+        }
+        if (!"user".equals(login.getType()) && !"miniprogram_user".equals(login.getType())) {
+            return R.fail("仅支持用户端操作");
+        }
+        if (dto == null || StringUtils.isBlank(dto.getPassword())) {
+            return R.fail("密码不能为空");
+        }
+        if (!Objects.equals(dto.getPassword(), dto.getConfirmPassword())) {
+            return R.fail("两次密码输入不一致");
+        }
+        LifeUser user = lifeUserMapper.selectById(login.getUserId());
+        if (user == null) {
+            return R.fail("用户不存在");
+        }
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            return R.fail("您的账户因严重违规导致被封禁");
+        }
+        LifeUser update = new LifeUser();
+        update.setId(login.getUserId());
+        update.setPassword(dto.getPassword());
+        if (lifeUserMapper.updateById(update) <= 0) {
+            return R.fail("设置密码失败");
+        }
+        return R.success("设置密码成功");
+    }
+}

+ 78 - 31
alien-store/src/main/java/shop/alien/store/service/impl/StoreCommentAppealServiceImpl.java

@@ -9,18 +9,13 @@ import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.core.toolkit.StringUtils;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.google.common.collect.Lists;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.cloud.context.config.annotation.RefreshScope;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.ResponseEntity;
+import org.springframework.http.*;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.client.RestTemplate;
@@ -28,25 +23,22 @@ import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.MultipartRequest;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.excelVo.util.ExcelExporter;
-import shop.alien.entity.store.vo.StoreCommentAppealLogVo;
-import shop.alien.entity.store.vo.StoreCommentAppealInfoVo;
-import shop.alien.entity.store.vo.StoreCommentAppealSupplementVo;
-import shop.alien.entity.store.vo.StoreCommentAppealVo;
-import shop.alien.entity.store.vo.WebSocketVo;
+import shop.alien.entity.store.vo.*;
 import shop.alien.mapper.*;
 import shop.alien.store.config.WebSocketProcess;
 import shop.alien.store.service.CommonRatingService;
 import shop.alien.store.service.StoreCommentAppealService;
 import shop.alien.store.util.FileUploadUtil;
 import shop.alien.store.util.ai.AiAuthTokenUtil;
+import shop.alien.util.common.constant.CommentSourceTypeEnum;
 import shop.alien.util.common.netease.TextCheckUtil;
-import shop.alien.util.common.safe.TextModerationResultVO;
 import shop.alien.util.common.safe.TextModerationUtil;
-import shop.alien.util.common.safe.TextReviewServiceEnum;
-import shop.alien.util.common.constant.CommentSourceTypeEnum;
 
 import java.net.URLEncoder;
 import java.text.SimpleDateFormat;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -190,13 +182,13 @@ public class StoreCommentAppealServiceImpl extends ServiceImpl<StoreCommentAppea
                 return 3;
             }*/
 
-            List<String> servicesList = Lists.newArrayList();
-            servicesList.add(TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService());
-            servicesList.add(TextReviewServiceEnum.LLM_QUERY_MODERATION.getService());
-            TextModerationResultVO textCheckResult = textModerationUtil.invokeFunction(appealReason, servicesList);
-            if ("high".equals(textCheckResult.getRiskLevel())) {
-                return 3;
-            }
+//            List<String> servicesList = Lists.newArrayList();
+//            servicesList.add(TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService());
+//            servicesList.add(TextReviewServiceEnum.LLM_QUERY_MODERATION.getService());
+//            TextModerationResultVO textCheckResult = textModerationUtil.invokeFunction(appealReason, servicesList);
+//            if ("high".equals(textCheckResult.getRiskLevel())) {
+//                return 3;
+//            }
 
             List<String> fileNameSet = new ArrayList<>(multipartRequest.getMultiFileMap().keySet());
             LambdaQueryWrapper<StoreCommentAppeal> wrapper = new LambdaQueryWrapper<>();
@@ -274,14 +266,14 @@ public class StoreCommentAppealServiceImpl extends ServiceImpl<StoreCommentAppea
 //                return storeCommentAppealInfoVo;
 //            }
 
-            List<String> servicesList = Lists.newArrayList();
-            servicesList.add(TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService());
-            servicesList.add(TextReviewServiceEnum.LLM_QUERY_MODERATION.getService());
-            TextModerationResultVO textCheckResult = textModerationUtil.invokeFunction(appealReason, servicesList);
-            if ("high".equals(textCheckResult.getRiskLevel())) {
-                storeCommentAppealInfoVo.setResult(3);
-                return storeCommentAppealInfoVo;
-            }
+//            List<String> servicesList = Lists.newArrayList();
+//            servicesList.add(TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService());
+//            servicesList.add(TextReviewServiceEnum.LLM_QUERY_MODERATION.getService());
+//            TextModerationResultVO textCheckResult = textModerationUtil.invokeFunction(appealReason, servicesList);
+//            if ("high".equals(textCheckResult.getRiskLevel())) {
+//                storeCommentAppealInfoVo.setResult(3);
+//                return storeCommentAppealInfoVo;
+//            }
 
             LambdaQueryWrapper<StoreCommentAppeal> wrapper = new LambdaQueryWrapper<>();
             //待审批, 已通过
@@ -722,9 +714,64 @@ public class StoreCommentAppealServiceImpl extends ServiceImpl<StoreCommentAppea
         }
     }
 
+    /**
+     * 判断用户是否可以评价
+     * 规则:近30天申诉通过次数不超过3次,且距最近一次申诉通过未满10天不可评价
+     */
     @Override
-    public Boolean canRate(Integer userId) {
-        return storeCommentAppealMapper.canRate(userId) > 3;
+    public Map<String, Object> canRate(Integer userId) {
+        log.info("StoreCommentAppealServiceImpl.canRate, userId={}", userId);
+        Map<String, Object> resultMap = new HashMap<>();
+        List<StoreCommentAppeal> passedAppealList = storeCommentAppealMapper.canRate(userId);
+
+        int passedCount = passedAppealList.size();
+        // 列表已按 updated_time 降序,第一条即为最新通过记录
+        StoreCommentAppeal latestPassedAppeal = passedAppealList.isEmpty() ? null : passedAppealList.get(0);
+        long countdownMillis = 0l;
+        if (passedCount > 3) {
+            countdownMillis = calculateAppealPassCooldownCountdown(latestPassedAppeal);
+        }
+
+        boolean canRate = passedCount <= 3 || countdownMillis <= 0;
+        resultMap.put("canRate", canRate);
+        resultMap.put("passedCount", passedCount);
+        resultMap.put("latestPassedAppeal", latestPassedAppeal);
+        resultMap.put("countdown", formatCountdownToDayHour(countdownMillis));
+
+        log.info("StoreCommentAppealServiceImpl.canRate result, userId={}, canRate={}, passedCount={}, countdown={}",
+                userId, canRate, passedCount, formatCountdownToDayHour(countdownMillis));
+        return resultMap;
+    }
+
+    /**
+     * 将毫秒倒计时转换为「X天X小时」格式
+     */
+    private String formatCountdownToDayHour(long countdownMillis) {
+        if (countdownMillis <= 0) {
+            return "0天0小时";
+        }
+        long totalHours = countdownMillis / (1000 * 60 * 60);
+        long days = totalHours / 24;
+        long hours = totalHours % 24;
+        return days + "天" + hours + "小时";
+    }
+
+    /**
+     * 计算申诉通过后的10天冷却倒计时(毫秒)
+     * 以申诉更新时间(通过时间)为起点,10天内返回剩余毫秒数,否则返回0
+     */
+    private long calculateAppealPassCooldownCountdown(StoreCommentAppeal latestPassedAppeal) {
+        if (latestPassedAppeal == null || latestPassedAppeal.getUpdatedTime() == null) {
+            return 0L;
+        }
+        LocalDateTime passTime = latestPassedAppeal.getUpdatedTime().toInstant()
+                .atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime cooldownEndTime = passTime.plusDays(10);
+        LocalDateTime now = LocalDateTime.now();
+        if (now.isBefore(cooldownEndTime)) {
+            return Duration.between(now, cooldownEndTime).toMillis();
+        }
+        return 0L;
     }
 
     /**

+ 14 - 0
alien-store/src/main/java/shop/alien/store/util/ali/AliSms.java

@@ -36,6 +36,8 @@ public class AliSms {
 
     private final StoreVerificationCodeMapper storeVerificationCodeMapper;
 
+    private final UserForgetPasswordSmsDailyLimiter userForgetPasswordSmsDailyLimiter;
+
     @Value("${ali.sms.accessKeyId}")
     private String accessKeyId;
 
@@ -83,6 +85,9 @@ public class AliSms {
      */
     public Integer sendSms(String phone, Integer appType, Integer businessType) {
         log.info("AliSmsConfig.sendSms?phone={}&appType={}&businessType={}", phone, appType, businessType);
+        if (userForgetPasswordSmsDailyLimiter.isUserForgetPassword(appType, businessType)) {
+            userForgetPasswordSmsDailyLimiter.checkLimit(phone);
+        }
         try {
             String appTypeStr = appType == 0 ? "user" : (appType == 1 ? "store" : "store_platform");
             String businessTypeStr;
@@ -158,8 +163,11 @@ public class AliSms {
             // 验证码发送成功,将验证码保存到redis中 设置60秒过期
             baseRedisService.setString("verification_" + appTypeStr + "_" + businessTypeStr + "_" + phone, code.toString(), codeTimeOut);
             saveVerificationCode(appType, businessType, phone, code.toString());
+            recordUserForgetPasswordSend(appType, businessType, phone);
 
             return code;
+        } catch (SmsSendLimitException e) {
+            throw e;
         } catch (Exception e) {
             log.error("AliSmsConfig.sendSms ERROR Msg={}", e.getMessage());
             return null;
@@ -257,6 +265,12 @@ public class AliSms {
         }
     }
 
+    private void recordUserForgetPasswordSend(Integer appType, Integer businessType, String phone) {
+        if (userForgetPasswordSmsDailyLimiter.isUserForgetPassword(appType, businessType)) {
+            userForgetPasswordSmsDailyLimiter.recordSend(phone);
+        }
+    }
+
     /**
      * JSON字符串转义,防止特殊字符破坏JSON格式
      */

+ 11 - 0
alien-store/src/main/java/shop/alien/store/util/ali/SmsSendLimitException.java

@@ -0,0 +1,11 @@
+package shop.alien.store.util.ali;
+
+/**
+ * 短信发送受限(如忘记密码每日次数上限)
+ */
+public class SmsSendLimitException extends RuntimeException {
+
+    public SmsSendLimitException(String message) {
+        super(message);
+    }
+}

+ 81 - 0
alien-store/src/main/java/shop/alien/store/util/ali/UserForgetPasswordSmsDailyLimiter.java

@@ -0,0 +1,81 @@
+package shop.alien.store.util.ali;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 用户端忘记密码短信每日发送次数限制(仅 appType=0, businessType=6)
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class UserForgetPasswordSmsDailyLimiter {
+
+    private static final int MAX_DAILY_COUNT = 5;
+    private static final int USER_APP_TYPE = 0;
+    private static final int FORGET_PASSWORD_BUSINESS_TYPE = 6;
+    private static final String KEY_PREFIX = "sms_user_forget_password_daily:";
+    private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
+
+    private final StringRedisTemplate stringRedisTemplate;
+
+    public boolean isUserForgetPassword(Integer appType, Integer businessType) {
+        return USER_APP_TYPE == appType && FORGET_PASSWORD_BUSINESS_TYPE == businessType;
+    }
+
+    /**
+     * 发送前校验,超限则抛出异常
+     */
+    public void checkLimit(String phone) {
+        if (phone == null || phone.trim().isEmpty()) {
+            return;
+        }
+        String key = buildDailyKey(phone.trim());
+        String countStr = stringRedisTemplate.opsForValue().get(key);
+        int count = 0;
+        if (countStr != null && !countStr.isEmpty()) {
+            try {
+                count = Integer.parseInt(countStr);
+            } catch (NumberFormatException e) {
+                log.warn("UserForgetPasswordSmsDailyLimiter invalid count, key={}, value={}", key, countStr);
+            }
+        }
+        if (count >= MAX_DAILY_COUNT) {
+            throw new SmsSendLimitException("今日忘记密码验证码发送次数已达上限,请明日再试");
+        }
+    }
+
+    /**
+     * 短信发送成功后累计次数
+     */
+    public void recordSend(String phone) {
+        if (phone == null || phone.trim().isEmpty()) {
+            return;
+        }
+        String key = buildDailyKey(phone.trim());
+        Long count = stringRedisTemplate.opsForValue().increment(key);
+        if (count != null && count == 1) {
+            stringRedisTemplate.expire(key, secondsUntilEndOfDay(), TimeUnit.SECONDS);
+        }
+    }
+
+    private String buildDailyKey(String phone) {
+        String date = LocalDate.now(ZONE).format(DateTimeFormatter.BASIC_ISO_DATE);
+        return KEY_PREFIX + phone + ":" + date;
+    }
+
+    private long secondsUntilEndOfDay() {
+        ZonedDateTime now = ZonedDateTime.now(ZONE);
+        ZonedDateTime endOfDay = now.toLocalDate().plusDays(1).atStartOfDay(ZONE);
+        return Math.max(Duration.between(now, endOfDay).getSeconds() + 1, 1);
+    }
+}