diff --git a/src/main/java/com/gxwebsoft/common/core/constants/RedisConstants.java b/src/main/java/com/gxwebsoft/common/core/constants/RedisConstants.java index 605f2d9..f047f98 100644 --- a/src/main/java/com/gxwebsoft/common/core/constants/RedisConstants.java +++ b/src/main/java/com/gxwebsoft/common/core/constants/RedisConstants.java @@ -5,6 +5,10 @@ public class RedisConstants { public static final String SMS_CODE_KEY = "sms"; // 验证码过期时间 public static final Long SMS_CODE_TTL = 5L; + // 邮箱验证码Key + public static final String EMAIL_CODE_KEY = "emailCode"; + // 邮箱验证码过期时间(分钟) + public static final Long EMAIL_CODE_TTL = 5L; // 微信凭证access-token public static final String ACCESS_TOKEN_KEY = "access-token"; // 空值防止击穿数据库 diff --git a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java index 127c6d5..fdc9d43 100644 --- a/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java +++ b/src/main/java/com/gxwebsoft/common/core/security/SecurityConfig.java @@ -54,6 +54,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { "/webjars/**", "/hxz/v1/**", "/api/sendSmsCaptcha", + "/api/sendEmailCaptcha", "/api/loginBySms", "/api/loginBySuperAdminSms", "/api/loginByDeveloperSms", diff --git a/src/main/java/com/gxwebsoft/common/core/utils/CommonUtil.java b/src/main/java/com/gxwebsoft/common/core/utils/CommonUtil.java index 828e800..9c6e4ef 100644 --- a/src/main/java/com/gxwebsoft/common/core/utils/CommonUtil.java +++ b/src/main/java/com/gxwebsoft/common/core/utils/CommonUtil.java @@ -290,4 +290,20 @@ public class CommonUtil { return pattern.matcher(phoneNumber).matches(); } + /** + * 校验邮箱格式是否有效 + * + * @param email 要验证的邮箱字符串 + * @return 如果字符串是有效的邮箱地址,则返回true;否则返回false + */ + public static boolean isValidEmail(String email) { + if (email == null) { + return false; + } + // 邮箱格式正则:本地部分@域名.顶级域 + String regex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"; + Pattern pattern = Pattern.compile(regex); + return pattern.matcher(email).matches(); + } + } diff --git a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java index 8235fdd..4843570 100644 --- a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java +++ b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java @@ -33,6 +33,8 @@ import com.gxwebsoft.common.system.entity.*; import com.gxwebsoft.common.system.mapper.CompanyMapper; import com.gxwebsoft.common.system.param.LoginParam; import com.gxwebsoft.common.system.param.SmsCaptchaParam; +import com.gxwebsoft.common.system.param.EmailCaptchaParam; +import com.gxwebsoft.common.core.constants.RedisConstants; import com.gxwebsoft.common.system.param.FindAccountByPhoneParam; import com.gxwebsoft.common.system.param.ResetPasswordParam; import com.gxwebsoft.common.system.param.UpdatePasswordParam; @@ -390,8 +392,33 @@ public class MainController extends BaseController { update.setAvatar(user.getAvatar()); update.setBgImage(user.getBgImage()); update.setSex(user.getSex()); - update.setPhone(user.getPhone()); - update.setEmail(user.getEmail()); + // 手机号变更:仅当提交短信验证码且校验通过时才更新,防止未经验证的手机号被绑定 + if (StrUtil.isNotBlank(user.getSmsCode())) { + String newPhone = user.getPhone(); + if (StrUtil.isBlank(newPhone) || !CommonUtil.isValidPhoneNumber(newPhone)) { + return fail("手机号格式不正确", null); + } + String key = "code:" + newPhone; + String cached = redisUtil.get(key); + String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS); + if (StrUtil.isBlank(cached) || (!cached.equals(user.getSmsCode()) && !user.getSmsCode().equals(devCode))) { + return fail("短信验证码不正确", null); + } + update.setPhone(newPhone); + redisUtil.delete(key); + cacheClient.delete(newPhone); + } + // 邮箱变更:仅当提交邮箱验证码且校验通过时才更新,防止未经验证的邮箱被绑定 + if (StrUtil.isNotBlank(user.getEmailCode())) { + String key = RedisConstants.EMAIL_CODE_KEY + ":" + user.getEmail(); + String cached = redisUtil.get(key); + if (StrUtil.isBlank(cached) || !cached.equals(user.getEmailCode())) { + return fail("邮箱验证码不正确", null); + } + update.setEmail(user.getEmail()); + redisUtil.delete(key); + } + // 未提交邮箱验证码时不更新邮箱(保持原值,禁止绕过校验) update.setProvince(user.getProvince()); update.setCity(user.getCity()); update.setRegion(user.getRegion()); @@ -684,6 +711,32 @@ public class MainController extends BaseController { } } + @Operation(summary = "发送邮箱验证码") + @PostMapping("/sendEmailCaptcha") + public ApiResult sendEmailCaptcha(@RequestBody EmailCaptchaParam param) { + if (param == null || StrUtil.isBlank(param.getEmail())) { + return fail("邮箱不能为空"); + } + if (!CommonUtil.isValidEmail(param.getEmail())) { + return fail("请输入有效的邮箱地址"); + } + // 生成6位邮箱验证码 + String code = Integer.toString(ThreadLocalRandom.current().nextInt(100000, 1000000)); + // 存储到Redis,5分钟有效期(key 与校验时保持一致) + String key = RedisConstants.EMAIL_CODE_KEY + ":" + param.getEmail(); + redisUtil.set(key, code, RedisConstants.EMAIL_CODE_TTL, TimeUnit.MINUTES); + cacheClient.set(param.getEmail(), code, RedisConstants.EMAIL_CODE_TTL, TimeUnit.MINUTES); + Integer tenantId = getTenantId(); + try { + emailTemplateUtil.sendCaptchaEmail(param.getEmail(), code, tenantId); + log.info("邮箱验证码发送成功 email={}", DesensitizedUtil.email(param.getEmail())); + return success("验证码已发送,请查收邮箱"); + } catch (Exception e) { + log.error("邮箱验证码发送失败 email={}", param.getEmail(), e); + return fail("邮件发送失败,请稍后重试"); + } + } + @OperationLog @Operation(summary = "重置密码") @PutMapping("/password") diff --git a/src/main/java/com/gxwebsoft/common/system/entity/User.java b/src/main/java/com/gxwebsoft/common/system/entity/User.java index 4939cf1..47dce0e 100644 --- a/src/main/java/com/gxwebsoft/common/system/entity/User.java +++ b/src/main/java/com/gxwebsoft/common/system/entity/User.java @@ -61,6 +61,14 @@ public class User implements UserDetails { @Schema(description = "邮箱") private String email; + @Schema(description = "邮箱验证码(非数据库字段,仅用于绑定/修改邮箱时校验)") + @TableField(exist = false) + private String emailCode; + + @Schema(description = "短信验证码(非数据库字段,仅用于绑定/修改手机号时校验)") + @TableField(exist = false) + private String smsCode; + @Schema(description = "资质") private String aptitude; diff --git a/src/main/java/com/gxwebsoft/common/system/param/EmailCaptchaParam.java b/src/main/java/com/gxwebsoft/common/system/param/EmailCaptchaParam.java new file mode 100644 index 0000000..a9dd144 --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/param/EmailCaptchaParam.java @@ -0,0 +1,30 @@ +package com.gxwebsoft.common.system.param; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; + +/** + * 发送邮箱验证码参数 + * + * @author WebSoft + * @since 2026-07-20 + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "发送邮箱验证码参数") +public class EmailCaptchaParam implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "邮箱") + private String email; + + @Schema(description = "租户ID") + private String tenantId; + + @Schema(description = "场景") + private String scene; + +} diff --git a/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java b/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java index c547387..4b98adb 100644 --- a/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java +++ b/src/main/java/com/gxwebsoft/common/system/util/EmailTemplateUtil.java @@ -196,4 +196,20 @@ public class EmailTemplateUtil { sendNotificationEmailWithAction(title, content, email, tenantId, actionUrl, actionText); } + + /** + * 发送邮箱验证码邮件 + * + * @param email 收件人邮箱 + * @param code 验证码 + * @param tenantId 租户ID + */ + public void sendCaptchaEmail(String email, String code, Integer tenantId) { + if (email == null || email.trim().isEmpty()) { + return; + } + String title = "邮箱验证码 - WebSoft"; + String content = "您正在绑定或修改邮箱,验证码为:" + code + ",5 分钟内有效,请勿泄露给他人。如非本人操作,请忽略此邮件。"; + sendNotificationEmail(title, content, email, tenantId, "尊敬的用户", "验证码用于确认邮箱真实性,请勿转发给他人。", null, null); + } }