Bladeren bron

验证码接口改用 DTO/VO 接收与返回

- /api/captcha/image 返回 ImageCaptchaResp(base64 / key / md5),不再用 Map
- /api/captcha/sendSmsCaptcha 改用 SendSmsCaptchaReq 接收扁平 JSON,并加 @Valid 非空校验
- 图形验证码 Redis 缓存结构改为 CheckCodeDto,序列化换 fastjson2
- pom 新增 fastjson2 2.0.53
- 自测:image 正常返回三字段;空 body 命中校验提示;错误图形码返回「图形验证码不存在,请重新获取」
tanlie 1 week geleden
bovenliggende
commit
2e3dcfb5a9

+ 5 - 0
yoshop-java/pom.xml

@@ -74,6 +74,11 @@
             <artifactId>poi-ooxml</artifactId>
             <version>5.4.1</version>
         </dependency>
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+            <version>2.0.53</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 17 - 19
yoshop-java/src/main/java/cn/yoshop/api/controller/ApiCaptchaController.java

@@ -1,18 +1,12 @@
 package cn.yoshop.api.controller;
 
+import cn.yoshop.api.req.SendSmsCaptchaReq;
+import cn.yoshop.api.resp.ImageCaptchaResp;
 import cn.yoshop.api.service.ApiCaptchaService;
 import cn.yoshop.common.api.R;
-import cn.yoshop.common.util.Params;
-
-import jakarta.servlet.http.HttpServletRequest;
+import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.Map;
+import org.springframework.web.bind.annotation.*;
 
 /**
  * 客户端验证码,对齐 PHP app/api/controller/Captcha。
@@ -24,17 +18,21 @@ public class ApiCaptchaController {
 
     private final ApiCaptchaService apiCaptchaService;
 
-    /** /api/captcha/image 图形验证码(base64 / key / md5) */
+    /**
+     * /api/captcha/image 图形验证码(base64 / key / md5)
+     */
     @GetMapping("/captcha/image")
-    public R<Map<String, Object>> image() {
-        return R.ok(apiCaptchaService.createImage());
+    public R<ImageCaptchaResp> image() {
+        ImageCaptchaResp image = apiCaptchaService.createImage();
+        return R.ok(image);
     }
-    
-    /** /api/captcha/sendSmsCaptcha 发送短信验证码(表单:mobile / captchaCode / captchaKey) */
+
+    /**
+     * /api/captcha/sendSmsCaptcha 发送短信验证码(表单:mobile / captchaCode / captchaKey)
+     */
     @PostMapping("/captcha/sendSmsCaptcha")
-    public R<Void> sendSmsCaptcha(@RequestBody(required = false) Map<String, Object> body,
-                                  HttpServletRequest request) {
-        Map<String, Object> params = Params.merge(body, request);
-        return R.okMsg(apiCaptchaService.sendSmsCaptcha(params));
+    public R<Void> sendSmsCaptcha(@RequestBody(required = false) @Valid SendSmsCaptchaReq req) {
+        String s = apiCaptchaService.sendSmsCaptcha(req);
+        return R.okMsg(s);
     }
 }

+ 19 - 0
yoshop-java/src/main/java/cn/yoshop/api/dto/CheckCodeDto.java

@@ -0,0 +1,19 @@
+package cn.yoshop.api.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-10-20 21:07
+ **/
+@Data
+public class CheckCodeDto implements Serializable {
+
+    private String code;
+
+    private Integer times = 0;
+
+}

+ 25 - 0
yoshop-java/src/main/java/cn/yoshop/api/req/SendSmsCaptchaReq.java

@@ -0,0 +1,25 @@
+package cn.yoshop.api.req;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-10-20 20:16
+ **/
+@Data
+public class SendSmsCaptchaReq implements Serializable {
+
+    @NotBlank(message = "验证码key不能为空")
+    private String captchaKey;
+
+    @NotBlank(message = "验证码不能为空")
+    private String captchaCode;
+
+    @NotBlank(message = "手机号不能为空")
+    private String mobile;
+
+}

+ 18 - 0
yoshop-java/src/main/java/cn/yoshop/api/resp/ImageCaptchaResp.java

@@ -0,0 +1,18 @@
+package cn.yoshop.api.resp;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author lie tan
+ * @description
+ * @date 2026-10-20 20:45
+ **/
+@Data
+public class ImageCaptchaResp implements Serializable {
+    private String base64;
+    private String key;
+    private String md5;
+
+}

+ 48 - 56
yoshop-java/src/main/java/cn/yoshop/api/service/ApiCaptchaService.java

@@ -3,16 +3,16 @@ package cn.yoshop.api.service;
 import cn.hutool.captcha.CaptchaUtil;
 import cn.hutool.captcha.LineCaptcha;
 import cn.hutool.crypto.digest.DigestUtil;
+import cn.yoshop.api.dto.CheckCodeDto;
+import cn.yoshop.api.req.SendSmsCaptchaReq;
+import cn.yoshop.api.resp.ImageCaptchaResp;
 import cn.yoshop.common.exception.BizException;
-import cn.yoshop.common.util.PhpStr;
 import cn.yoshop.utils.RedisUtils;
-
+import com.alibaba.fastjson2.JSON;
 import lombok.RequiredArgsConstructor;
 import org.springframework.security.crypto.bcrypt.BCrypt;
 import org.springframework.stereotype.Service;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
+import org.springframework.util.StringUtils;
 
 /**
  * 图形验证码与短信验证码下发,对齐 PHP:
@@ -38,19 +38,31 @@ import java.util.Map;
 @RequiredArgsConstructor
 public class ApiCaptchaService {
 
-    /** 图形验证码缓存键前缀 */
+    /**
+     * 图形验证码缓存键前缀
+     */
     private static final String IMAGE_KEY_PREFIX = "yoshop:api:captcha:";
-    /** 短信发送次数缓存键前缀 */
+    /**
+     * 短信发送次数缓存键前缀
+     */
     private static final String SEND_KEY_PREFIX = "yoshop:api:sms:send:";
 
-    /** 有效期(秒),对齐 PHP CaptchaApi::$expire */
+    /**
+     * 有效期(秒),对齐 PHP CaptchaApi::$expire
+     */
     private static final int EXPIRE_SECONDS = 1800;
-    /** 允许的错误次数,对齐 PHP CaptchaApi::$checkTimes */
+    /**
+     * 允许的错误次数,对齐 PHP CaptchaApi::$checkTimes
+     */
     private static final int CHECK_TIMES = 5;
 
-    /** 每日最大发送次数,对齐 PHP SmsCaptcha::$sendTimes */
+    /**
+     * 每日最大发送次数,对齐 PHP SmsCaptcha::$sendTimes
+     */
     private static final int SEND_TIMES = 10;
-    /** 发送次数统计窗口(秒),对齐 PHP SmsCaptcha::$safeTime */
+    /**
+     * 发送次数统计窗口(秒),对齐 PHP SmsCaptcha::$safeTime
+     */
     private static final int SEND_SAFE_SECONDS = 86400;
 
     private static final int IMAGE_WIDTH = 160;
@@ -62,22 +74,24 @@ public class ApiCaptchaService {
 
     // ------------------------------------------------------------------ 图形验证码
 
-    /** 生成图形验证码:{base64, key, md5} */
-    public Map<String, Object> createImage() {
+    /**
+     * 生成图形验证码:{base64, key, md5}
+     */
+    public ImageCaptchaResp createImage() {
         LineCaptcha captcha = CaptchaUtil.createLineCaptcha(IMAGE_WIDTH, IMAGE_HEIGHT, CODE_LENGTH, 40);
         String code = captcha.getCode() == null ? "" : captcha.getCode().toLowerCase();
         String key = BCrypt.hashpw(code, BCrypt.gensalt(10));
 
-        Map<String, Object> cache = new LinkedHashMap<>();
-        cache.put("code", code);
-        cache.put("times", CHECK_TIMES);
-        redisUtils.setString(IMAGE_KEY_PREFIX + key, PhpStr.jsonEncode(cache), EXPIRE_SECONDS);
+        CheckCodeDto cache = new CheckCodeDto();
+        cache.setCode(code);
+        cache.setTimes(CHECK_TIMES);
+        redisUtils.setString(IMAGE_KEY_PREFIX + key, JSON.toJSONString(cache), EXPIRE_SECONDS);
 
-        Map<String, Object> data = new LinkedHashMap<>();
+        ImageCaptchaResp data = new ImageCaptchaResp();
         // 对齐 PHP:base64 带 data URI 前缀,且去掉换行
-        data.put("base64", "data:image/png;base64," + captcha.getImageBase64().replace("\r\n", "").replace("\n", ""));
-        data.put("key", key);
-        data.put("md5", DigestUtil.md5Hex(code));
+        data.setBase64("data:image/png;base64," + captcha.getImageBase64().replace("\r\n", "").replace("\n", ""));
+        data.setKey(key);
+        data.setMd5(DigestUtil.md5Hex(code));
         return data;
     }
 
@@ -85,26 +99,25 @@ public class ApiCaptchaService {
      * 校验图形验证码,返回是否匹配(不匹配由调用方给文案,对齐 PHP CaptchaApi::check 返回 false)。
      * 记录不存在 / 次数用尽时直接抛业务异常。
      */
-    @SuppressWarnings("unchecked")
     public boolean checkImage(String code, String key) {
-        if (code == null || code.isBlank() || key == null || key.isBlank()) {
+        if (!StringUtils.hasText(code) || !StringUtils.hasText(key)) {
             throw new BizException("很抱歉,图形验证码不正确");
         }
         String cacheKey = IMAGE_KEY_PREFIX + key;
-        Map<?, ?> cache = read(cacheKey);
-        if (cache == null || cache.get("code") == null) {
+        CheckCodeDto cache = JSON.parseObject(redisUtils.getString(cacheKey), CheckCodeDto.class);
+        if (cache == null || cache.getCode() == null) {
             throw new BizException("图形验证码不存在,请重新获取");
         }
-        int times = toInt(cache.get("times"));
+        int times = cache.getTimes();
         if (times <= 0) {
             redisUtils.deleteString(cacheKey);
             throw new BizException("图形验证码已超出错误次数,请重新获取");
         }
         if (!BCrypt.checkpw(code.trim().toLowerCase(), key)) {
-            Map<String, Object> next = new LinkedHashMap<>();
-            next.put("code", String.valueOf(cache.get("code")));
-            next.put("times", times - 1);
-            redisUtils.setString(cacheKey, PhpStr.jsonEncode(next), EXPIRE_SECONDS);
+            CheckCodeDto next = new CheckCodeDto();
+            next.setCode(cache.getCode());
+            next.setTimes(times - 1);
+            redisUtils.setString(cacheKey, JSON.toJSONString(next), EXPIRE_SECONDS);
             return false;
         }
         redisUtils.deleteString(cacheKey);
@@ -118,26 +131,14 @@ public class ApiCaptchaService {
      *
      * @return 固定文案(对齐 PHP controller 的 "发送成功,请注意查收")
      */
-    public String sendSmsCaptcha(Map<String, Object> form) {
-        String mobile = cn.yoshop.common.util.Params.str(form, "mobile", "");
-        String captchaCode = cn.yoshop.common.util.Params.str(form, "captchaCode", "");
-        String captchaKey = cn.yoshop.common.util.Params.str(form, "captchaKey", "");
-        if (captchaCode.isBlank()) {
-            throw new BizException("图形验证码code不能为空");
-        }
-        if (captchaKey.isBlank()) {
-            throw new BizException("图形验证码key不能为空");
-        }
-        if (mobile.isBlank()) {
-            throw new BizException("手机号不能为空");
-        }
-        if (!checkImage(captchaCode, captchaKey)) {
+    public String sendSmsCaptcha(SendSmsCaptchaReq req) {
+        if (!checkImage(req.getCaptchaCode(), req.getCaptchaKey())) {
             throw new BizException("很抱歉,图形验证码不正确");
         }
         // 发送次数限制(每日 10 次)
-        String recordKey = SEND_KEY_PREFIX + mobile;
+        String recordKey = SEND_KEY_PREFIX + req.getMobile();
         String raw = redisUtils.getString(recordKey);
-        if (raw == null || raw.isBlank()) {
+        if (!StringUtils.hasText(raw)) {
             redisUtils.setString(recordKey, String.valueOf(SEND_TIMES - 1), SEND_SAFE_SECONDS);
         } else {
             int left = toInt(raw);
@@ -147,21 +148,12 @@ public class ApiCaptchaService {
             redisUtils.setString(recordKey, String.valueOf(left - 1), SEND_SAFE_SECONDS);
         }
         // 生成验证码并保存(短信通道未接入时由 ApiSmsService 记录日志)
-        smsService.send(mobile);
+        smsService.send(req.getMobile());
         return "发送成功,请注意查收";
     }
 
     // ------------------------------------------------------------------ 工具
 
-    private Map<?, ?> read(String key) {
-        String raw = redisUtils.getString(key);
-        if (raw == null || raw.isBlank()) {
-            return null;
-        }
-        Object decoded = PhpStr.jsonDecodeArray(raw);
-        return decoded instanceof Map<?, ?> map ? map : null;
-    }
-
     private static int toInt(Object value) {
         if (value == null) {
             return 0;