|
|
@@ -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;
|