package com.intc.web.controller; import cn.dev33.satoken.annotation.SaIgnore; import cn.hutool.captcha.AbstractCaptcha; import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.RandomUtil; import com.intc.common.core.constant.Constants; import com.intc.common.core.constant.GlobalConstants; import com.intc.common.core.domain.R; import com.intc.common.core.exception.ServiceException; import com.intc.common.core.utils.SpringUtils; import com.intc.common.core.utils.StringUtils; import com.intc.common.core.utils.reflect.ReflectUtils; import com.intc.common.mail.config.properties.MailProperties; import com.intc.common.mail.utils.MailUtils; import com.intc.common.ratelimiter.annotation.RateLimiter; import com.intc.common.ratelimiter.enums.LimitType; import com.intc.common.redis.utils.RedisUtils; import com.intc.common.sms.config.properties.SmsProperties; import com.intc.common.web.config.properties.CaptchaProperties; import com.intc.common.web.enums.CaptchaType; import com.intc.web.domain.vo.CaptchaVo; import com.intc.fishery.domain.bo.ReqVerifySmsCode; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.sms4j.api.SmsBlend; import org.dromara.sms4j.api.entity.SmsResponse; import org.dromara.sms4j.core.factory.SmsFactory; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.validation.annotation.Validated; 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.RestController; import java.time.Duration; import java.util.LinkedHashMap; /** * 验证码操作处理 * * @author Lion Li */ @SaIgnore @Slf4j @Validated @RequiredArgsConstructor @RestController public class CaptchaController { private final CaptchaProperties captchaProperties; private final MailProperties mailProperties; private final SmsProperties smsProperties; /** * 短信验证码 * * @param phonenumber 用户手机号 */ @RateLimiter(key = "#phonenumber", time = 60, count = 1) @GetMapping("/resource/sms/code") public R smsCode(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) { String key = GlobalConstants.CAPTCHA_CODE_KEY + phonenumber; String code = RandomUtil.randomNumbers(6); RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); // 验证码模板id 从配置文件读取 String templateId = smsProperties.getCodeTemplateId(); if (StringUtils.isBlank(templateId)) { log.error("短信验证码模板ID未配置,请在配置文件中设置 sms.plus.code-template-id"); return R.fail("短信服务配置错误,请联系管理员"); } LinkedHashMap map = new LinkedHashMap<>(1); map.put("code", code); SmsBlend smsBlend = SmsFactory.getSmsBlend("config1"); SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map); if (!smsResponse.isSuccess()) { log.error("验证码短信发送异常 => {}", smsResponse); // 解析错误信息 String errorMsg = "短信发送失败"; if (smsResponse.getData() != null) { String data = smsResponse.getData().toString(); if (data.contains("SMS_TEMPLATE_ILLEGAL")) { errorMsg = "短信模板不存在,请联系管理员配置正确的模板ID"; } else if (data.contains("SMS_SIGNATURE_ILLEGAL")) { errorMsg = "短信签名不存在,请联系管理员配置正确的签名"; } else if (data.contains("Message")) { // 尝试提取Message字段 try { int msgStart = data.indexOf("Message\":\"") + 10; int msgEnd = data.indexOf("\"", msgStart); if (msgStart > 10 && msgEnd > msgStart) { String message = data.substring(msgStart, msgEnd); errorMsg = "短信发送失败:" + message; } } catch (Exception e) { log.warn("解析短信错误信息失败", e); } } else if (data.contains("isv.")) { errorMsg = "短信服务配置错误,请联系管理员"; } } return R.fail(errorMsg); } return R.ok(); } /** * 验证短信验证码 * * @param request 手机号和验证码数据 * @return 验证结果 */ @PostMapping("/resource/sms/verify") public R verifySmsCode(@Validated @RequestBody ReqVerifySmsCode request) { String key = GlobalConstants.CAPTCHA_CODE_KEY + request.getMobilePhone(); String cachedCode = RedisUtils.getCacheObject(key); if (StringUtils.isBlank(cachedCode)) { return R.fail("短信验证码已过期"); } if (!cachedCode.equals(request.getSmsCode())) { return R.fail("短信验证码错误"); } // 验证成功后删除验证码(根据 C# 代码中的 true 参数) RedisUtils.deleteObject(key); return R.ok(); } /** * 邮箱验证码 * * @param email 邮箱 */ @GetMapping("/resource/email/code") public R emailCode(@NotBlank(message = "{user.email.not.blank}") String email) { if (!mailProperties.getEnabled()) { return R.fail("当前系统没有开启邮箱功能!"); } SpringUtils.getAopProxy(this).emailCodeImpl(email); return R.ok(); } /** * 邮箱验证码 * 独立方法避免验证码关闭之后仍然走限流 */ @RateLimiter(key = "#email", time = 60, count = 1) public void emailCodeImpl(String email) { String key = GlobalConstants.CAPTCHA_CODE_KEY + email; String code = RandomUtil.randomNumbers(4); RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); try { MailUtils.sendText(email, "登录验证码", "您本次验证码为:" + code + ",有效性为" + Constants.CAPTCHA_EXPIRATION + "分钟,请尽快填写。"); } catch (Exception e) { log.error("验证码短信发送异常 => {}", e.getMessage()); throw new ServiceException(e.getMessage()); } } /** * 生成验证码 */ @GetMapping("/auth/code") public R getCode() { boolean captchaEnabled = captchaProperties.getEnable(); if (!captchaEnabled) { CaptchaVo captchaVo = new CaptchaVo(); captchaVo.setCaptchaEnabled(false); return R.ok(captchaVo); } return R.ok(SpringUtils.getAopProxy(this).getCodeImpl()); } /** * 生成验证码 * 独立方法避免验证码关闭之后仍然走限流 */ @RateLimiter(time = 60, count = 10, limitType = LimitType.IP) public CaptchaVo getCodeImpl() { // 保存验证码信息 String uuid = IdUtil.simpleUUID(); String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid; // 生成验证码 CaptchaType captchaType = captchaProperties.getType(); CodeGenerator codeGenerator; if (CaptchaType.MATH == captchaType) { codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getNumberLength(), false); } else { codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getCharLength()); } AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz()); captcha.setGenerator(codeGenerator); captcha.createCode(); // 如果是数学验证码,使用SpEL表达式处理验证码结果 String code = captcha.getCode(); if (CaptchaType.MATH == captchaType) { ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(StringUtils.remove(code, "=")); code = exp.getValue(String.class); } RedisUtils.setCacheObject(verifyKey, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION)); CaptchaVo captchaVo = new CaptchaVo(); captchaVo.setUuid(uuid); captchaVo.setImg(captcha.getImageBase64()); return captchaVo; } }