当前位置: 首页 > news >正文

ssm项目,邮箱验证码

一,

注册或重置密码时,用自身邮箱向用户发送验证码,验证时比对用户输入与数据库中存储的验证码:一致则操作成功;不一致则限制 1 分钟后可重新发送。

二,大体思路

 将后端生成的验证码,保存到数据库,然后同时发送到邮箱,然后将邮箱验证码发送到填到前端的输入框中,和数据库的验证码进行比对。

三,具体的实现

密码可以增加加密

1.先开启qq邮箱的邮箱服务器

2.写一下自己的配置文件

3.生成验证码

package com.snapan.service;import org.springframework.mail.javamail.JavaMailSenderImpl;public interface EmailService {void setMailSender(JavaMailSenderImpl mailSender);void setFromEmail(String fromEmail);boolean sendVerificationCode(String email, String verificationCode, Byte type);
}

------

将这个qq号换成自己的

package com.snapan.service.impl;import com.snapan.service.EmailService;import javax.mail.internet.MimeMessage;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;@Service
public class EmailServiceImpl implements EmailService {// 1. 调整mailSender的类型为JavaMailSenderImpl(与接口方法参数一致)@Autowiredprivate JavaMailSenderImpl mailSender;  // 现在Spring会主动赋值,不再为nullprivate String fromEmail = "1659301571@qq.com";// 2. 实现接口的setMailSender方法@Overridepublic void setMailSender(JavaMailSenderImpl mailSender) {this.mailSender = mailSender;}// 3. 实现接口的setFromEmail方法@Value("${mail.smtp.username:}")  // 对应mail.properties中的配置,如果不存在则使用空字符串public void setFromEmail(String fromEmail) {this.fromEmail = fromEmail;}@Overridepublic boolean sendVerificationCode(String toEmail, String verificationCode, Byte type) {try {String subject = getEmailSubject(type);String content = buildEmailContent(verificationCode, type);sendHtmlEmail(toEmail, subject, content);System.out.println("验证码邮件发送成功至: " + toEmail);return true;  // 发送成功返回true} catch (Exception e) {System.err.println("邮件发送失败: " + e.getMessage());e.printStackTrace();  // 打印详细堆栈,方便排查问题return false;  // 发送失败返回false}}// 以下方法保持不变private String getEmailSubject(Byte type) {switch (type) {case 1: return "Snapan - 注册验证码";case 2: return "Snapan - 密码重置验证码";default: return "Snapan - 验证码";}}// 邮件发送方法private void sendHtmlEmail(String toEmail, String subject, String htmlContent) throws Exception {MimeMessage message = mailSender.createMimeMessage();MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");helper.setFrom(fromEmail);helper.setTo(toEmail);helper.setSubject(subject);helper.setText(htmlContent, true);mailSender.send(message);}private String buildEmailContent(String verificationCode, Byte type) {String title = type == 1 ? "邮箱验证" : "密码重置";String action = type == 1 ? "注册" : "重置密码";return "<!DOCTYPE html>" +"<html lang=\"zh-CN\">" +"<head>" +"    <meta charset=\"UTF-8\">" +"    <style>" +"        body { font-family: 'Microsoft YaHei', Arial, sans-serif; line-height: 1.6; color: #333; background: #f5f5f5; padding: 20px; }" +"        .container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }" +"        .header { text-align: center; margin-bottom: 30px; border-bottom: 1px solid #eee; padding-bottom: 20px; }" +"        .code { font-size: 32px; color: #1890ff; font-weight: bold; text-align: center; margin: 30px 0; padding: 20px; background: #f0f8ff; border-radius: 6px; letter-spacing: 8px; }" +"        .tip { color: #666; margin: 15px 0; line-height: 1.8; }" +"        .footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee; color: #999; font-size: 12px; text-align: center; }" +"        .warning { color: #ff4d4f; font-weight: bold; }" +"    </style>" +"</head>" +"<body>" +"    <div class=\"container\">" +"        <div class=\"header\">" +"            <h2>Snapan " + title + "</h2>" +"        </div>" +"        <p>您好!</p>" +"        <p>您正在申请" + action + " Snapan 服务,您的验证码是:</p>" +"        <div class=\"code\">" + verificationCode + "</div>" +"        <p class=\"tip\">• 验证码有效期为 <span class=\"warning\">10 分钟</span></p>" +"        <p class=\"tip\">• 请尽快完成操作</p>" +"        <p class=\"tip\">• 如果这不是您本人的操作,请忽略此邮件</p>" +"        <div class=\"footer\">" +"            <p>Snapan 团队</p>" +"            <p>" + new java.util.Date() + "</p>" +"        </div>" +"    </div>" +"</body>" +"</html>";}
}

---------

4.controller层

package com.snapan.controller;import com.snapan.entity.EmailVerification;
import com.snapan.service.VerificationCodeService;
import com.snapan.vo.BaseResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/api/verification")
public class VerificationCodeController {@Autowiredprivate VerificationCodeService verificationCodeService;/*** 发送验证码* @param email 邮箱地址* @param type 验证码类型:1-注册,2-重置密码*/@PostMapping("/send")public BaseResponse sendVerificationCode(@RequestParam String email,@RequestParam Byte type) {// 验证参数if (email == null || email.trim().isEmpty()) {return BaseResponse.error("邮箱地址不能为空");}if (type == null || (type != 1 && type != 2)) {return BaseResponse.error("验证码类型错误");}// 检查发送频率if (verificationCodeService.isTooFrequent(email, type)) {return BaseResponse.error("请求过于频繁,请1分钟后再试");}// 生成并发送验证码boolean result = verificationCodeService.generateAndSendCode(email, type);return result ? BaseResponse.success("验证码发送成功") :BaseResponse.error("验证码发送失败");}/*** 验证验证码* @param email 邮箱地址* @param code 用户输入的验证码* @param type 验证码类型*/@PostMapping("/verify")public BaseResponse verifyCode(@RequestParam String email,@RequestParam String code,@RequestParam Byte type) {// 验证参数if (email == null || email.trim().isEmpty()) {return BaseResponse.error("邮箱地址不能为空");}if (code == null || code.trim().isEmpty()) {return BaseResponse.error("验证码不能为空");}if (type == null || (type != 1 && type != 2)) {return BaseResponse.error("验证码类型错误");}boolean isValid = verificationCodeService.verifyCode(email, code, type);return isValid ? BaseResponse.success("验证成功") :BaseResponse.error("验证码错误或已过期");}
}

dao层

package com.snapan.dao;import com.snapan.entity.EmailVerification;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;import java.util.Date;
import java.util.List;/*** 邮箱验证码表数据访问接口*/
@Repository
public interface EmailVerificationDao {/*** 根据ID查询验证码记录*/EmailVerification selectById(Long id);/*** 查询所有*/List<EmailVerification> selectAll();/*** 更新*/int update(EmailVerification emailVerification);/*** 根据ID删除*/int deleteById(Long id);/*** 检查邮箱在指定时间内是否已发送过验证码(防滥用)*/int countRecentRequests(@Param("email") String email);/*** 标记验证码为已使用*/int updateToUsed(@Param("id") Long id);/*** 根据邮箱和类型查询所有验证码记录*/List<EmailVerification> selectByEmailAndType(@Param("email") String email,@Param("type") Byte type);/*** 插入验证码记录*/int insert(EmailVerification record);/*** 查找有效的验证码记录*/EmailVerification findValidCode(@Param("email") String email,@Param("code") String code,@Param("type") Byte type,@Param("now") Date now);/*** 统计近期请求次数*/Integer countRecentRequests(@Param("email") String email,@Param("type") Byte type,@Param("afterTime") Date afterTime);/*** 标记为已使用*/int markAsUsed(@Param("id") Long id);/*** 删除过期验证码*/int deleteExpired();}
package com.snapan.entity;import org.springframework.mail.javamail.JavaMailSenderImpl;public class Email {private String fromEmail;//    public void setMailSender(String mailSender) {
//        this.mailSender = mailSender;
//    }public void setFromEmail(String fromEmail) {this.fromEmail = fromEmail;}public void setMailSender(JavaMailSenderImpl mailSender) {}
}
package com.snapan.entity;import java.util.Date;/*** 邮箱验证码表*/
public class EmailVerification {/*** 验证码ID*/private Long id;/*** 邮箱地址*/private String email;/*** 验证码*/private String verificationCode;/*** 验证码类型:1-注册,2-重置密码*/private Byte type;/*** 过期时间*/private Date expireTime;/*** 是否已使用:0-未使用,1-已使用*/private Byte used;/*** 创建时间*/private Date createTime;public EmailVerification() {}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}public String getVerificationCode() {return verificationCode;}public void setVerificationCode(String verificationCode) {this.verificationCode = verificationCode;}public Byte getType() {return type;}public void setType(Byte type) {this.type = type;}public Date getExpireTime() {return expireTime;}public void setExpireTime(Date expireTime) {this.expireTime = expireTime;}public Byte getUsed() {return used;}public void setUsed(Byte used) {this.used = used;}public Date getCreateTime() {return createTime;}public void setCreateTime(Date createTime) {this.createTime = createTime;}}
package com.snapan.service.impl;import com.snapan.dao.EmailVerificationDao;
import com.snapan.entity.EmailVerification;
import com.snapan.service.EmailService;
import com.snapan.service.VerificationCodeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;import java.util.Calendar;
import java.util.Date;
import java.util.Random;@Service
public class VerificationCodeServiceImpl implements VerificationCodeService {@Autowiredprivate EmailVerificationDao emailVerificationDao;@Autowiredprivate EmailService emailService;// 验证码长度private static final int CODE_LENGTH = 6;// 验证码有效期(分钟)private static final int EXPIRE_MINUTES = 10;// 发送频率限制(分钟)private static final int FREQUENCY_LIMIT_MINUTES = 1;@Overridepublic boolean generateAndSendCode(String email, Byte type) {try {// 1. 生成随机验证码String verificationCode = generateRandomCode();// 2. 计算过期时间Date expireTime = calculateExpireTime();// 3. 创建验证码记录EmailVerification record = new EmailVerification();record.setEmail(email);record.setVerificationCode(verificationCode);record.setType(type);record.setExpireTime(expireTime);record.setUsed((byte) 0); // 0-未使用record.setCreateTime(new Date());// 4. 保存到数据库int result = emailVerificationDao.insert(record);if (result > 0) {// 5. 发送邮件return emailService.sendVerificationCode(email, verificationCode, type);}return false;} catch (Exception e) {e.printStackTrace();return false;}}@Overridepublic boolean verifyCode(String email, String userInputCode, Byte type) {try {// 查找有效的验证码记录EmailVerification record = emailVerificationDao.findValidCode(email, userInputCode, type, new Date());if (record == null) {return false; // 验证失败:验证码不存在、已使用、已过期或类型不匹配}// 标记为已使用emailVerificationDao.markAsUsed(record.getId());return true; // 验证成功} catch (Exception e) {e.printStackTrace();return false;}}@Overridepublic boolean isTooFrequent(String email, Byte type) {try {Integer count = emailVerificationDao.countRecentRequests(email, type, getTimeBeforeMinutes(FREQUENCY_LIMIT_MINUTES));return count != null && count > 0;} catch (Exception e) {e.printStackTrace();return true; // 出现异常时限制发送,保证安全}}@Override@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行清理public void cleanExpiredCodes() {try {emailVerificationDao.deleteExpired();System.out.println("已清理过期验证码");} catch (Exception e) {e.printStackTrace();}}/*** 生成随机验证码(6位数字)*/private String generateRandomCode() {Random random = new Random();StringBuilder code = new StringBuilder();for (int i = 0; i < CODE_LENGTH; i++) {code.append(random.nextInt(10));}return code.toString();}/*** 计算过期时间*/private Date calculateExpireTime() {Calendar calendar = Calendar.getInstance();calendar.add(Calendar.MINUTE, EXPIRE_MINUTES);return calendar.getTime();}/*** 获取指定分钟前的时间*/private Date getTimeBeforeMinutes(int minutes) {Calendar calendar = Calendar.getInstance();calendar.add(Calendar.MINUTE, -minutes);return calendar.getTime();}
}
package com.snapan.service;public interface VerificationCodeService {/*** 生成并发送验证码* @param email 邮箱地址* @param type 验证码类型* @return 是否成功*/boolean generateAndSendCode(String email, Byte type);/*** 验证验证码* @param email 邮箱地址* @param code 用户输入的验证码* @param type 验证码类型* @return 是否验证成功*/boolean verifyCode(String email, String code, Byte type);/*** 检查是否发送过于频繁* @param email 邮箱地址* @param type 验证码类型* @return 是否过于频繁*/boolean isTooFrequent(String email, Byte type);/*** 清理过期验证码*/void cleanExpiredCodes();
}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.snapan.dao.EmailVerificationDao"><!-- 结果映射 --><resultMap id="BaseResultMap" type="com.snapan.entity.EmailVerification"><id column="id" property="id" jdbcType="BIGINT"/><result column="email" property="email" jdbcType="VARCHAR"/><result column="verification_code" property="verificationCode" jdbcType="VARCHAR"/><result column="type" property="type" jdbcType="TINYINT"/><result column="expire_time" property="expireTime" jdbcType="TIMESTAMP"/><result column="used" property="used" jdbcType="TINYINT"/><result column="create_time" property="createTime" jdbcType="TIMESTAMP"/></resultMap><!-- 基础字段 --><sql id="Base_Column_List">id, email, verification_code, type, expire_time, used, create_time</sql><!-- 根据ID查询 --><select id="selectById" parameterType="java.lang.Long" resultMap="BaseResultMap">SELECT <include refid="Base_Column_List"/>FROM email_verificationWHERE id = #{id}</select><!-- 查询所有 --><select id="selectAll" resultMap="BaseResultMap">SELECT <include refid="Base_Column_List"/>FROM email_verification</select><!-- 插入 --><insert id="insertEmail" parameterType="com.snapan.entity.EmailVerification" useGeneratedKeys="true" keyProperty="id">INSERT INTO email_verification (id, email, verification_code, type, expire_time, used, create_time) VALUES (#{id}, #{email}, #{verificationCode}, #{type}, #{expireTime}, #{used}, #{createTime})</insert><!-- 更新 --><update id="update" parameterType="com.snapan.entity.EmailVerification">UPDATE email_verificationSET email = #{email}, verification_code = #{verificationCode}, type = #{type}, expire_time = #{expireTime}, used = #{used}, create_time = #{createTime}WHERE id = #{id}</update><!-- 根据ID删除 --><delete id="deleteById" parameterType="java.lang.Long">DELETE FROM email_verification WHERE id = #{id}</delete><!-- 根据邮箱和验证码查询有效记录 --><select id="findValidCode" parameterType="map"resultType="com.snapan.entity.EmailVerification">SELECTid,email,verification_code as verificationCode,type,expire_time as expireTime,used,create_time as createTimeFROM email_verificationWHEREemail = #{email}AND verification_code = #{code}AND type = #{type}AND used = 0AND expire_time > NOW()ORDER BY create_time DESCLIMIT 1</select><!-- 标记验证码为已使用 --><update id="markAsUsed" parameterType="long">UPDATE email_verificationSET used = 1WHERE id = #{id}</update><!-- 清理过期的验证码记录 --><delete id="cleanExpiredCodes">DELETE FROM email_verificationWHERE expire_time &lt; NOW()</delete><!-- 检查邮箱在指定时间内是否已发送过验证码(防滥用) --><select id="countRecentRequests" parameterType="string" resultType="int">SELECT COUNT(*)FROM email_verificationWHEREemail = #{email}AND create_time > DATE_SUB(NOW(), INTERVAL 1 MINUTE)</select><!-- 根据邮箱查询最新的有效验证码,type1是注册,2是重置密码 --><select id="findLatestValidCodeByEmail" parameterType="map"resultType="com.snapan.entity.EmailVerification">SELECTid,email,verification_code as verificationCode,type,expire_time as expireTime,used,create_time as createTimeFROM email_verificationWHEREemail = #{email}AND type = #{type}AND used = 0AND expire_time > NOW()ORDER BY create_time DESCLIMIT 1</select><!-- 标记验证码为已使用 --><update id="updateToUsed" parameterType="java.lang.Long">UPDATE email_verificationSET used = 1WHERE id = #{id}</update><!-- 删除过期的验证码记录,过期的时间小于当前的时间 --><delete id="deleteExpired">DELETE FROM email_verificationWHERE expire_time <![CDATA[ < ]]> NOW()</delete><!-- 根据邮箱和类型查询所有验证码记录 --><select id="selectByEmailAndType" resultMap="BaseResultMap">SELECT<include refid="Base_Column_List"/>FROM email_verificationWHERE email = #{email}AND type = #{type}ORDER BY create_time DESC</select><insert id="insert" parameterType="com.snapan.entity.EmailVerification">INSERT INTO email_verification (email,verification_code,type,expire_time,used,create_time) VALUES (#{email,jdbcType=VARCHAR},#{verificationCode,jdbcType=VARCHAR},#{type,jdbcType=TINYINT},#{expireTime,jdbcType=TIMESTAMP},#{used,jdbcType=TINYINT},#{createTime,jdbcType=TIMESTAMP})</insert></mapper>
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>用户注册 - Snapan</title><style>* {margin: 0;padding: 0;box-sizing: border-box;font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;}body {background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);display: flex;justify-content: center;align-items: center;min-height: 100vh;padding: 20px;}.register-container {background-color: white;border-radius: 12px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);width: 100%;max-width: 450px;padding: 40px 35px;position: relative;overflow: hidden;animation: fadeIn 0.5s ease-out;}.register-container::before {content: '';position: absolute;top: 0;left: 0;width: 100%;height: 5px;background: linear-gradient(to right, #6a11cb, #2575fc);}@keyframes fadeIn {from { opacity: 0; transform: translateY(20px); }to { opacity: 1; transform: translateY(0); }}h1 {text-align: center;margin-bottom: 30px;color: #333;font-weight: 600;font-size: 28px;}.input-group {margin-bottom: 20px;position: relative;}label {display: block;margin-bottom: 8px;color: #555;font-weight: 500;font-size: 14px;}input[type="text"],input[type="email"],input[type="password"] {width: 100%;padding: 14px 15px;border: 1px solid #ddd;border-radius: 8px;font-size: 16px;transition: all 0.3s;}input[type="text"]:focus,input[type="email"]:focus,input[type="password"]:focus {border-color: #6a11cb;box-shadow: 0 0 0 2px rgba(106, 17, 203, 0.2);outline: none;}.input-error {border-color: #ff4757 !important;box-shadow: 0 0 0 2px rgba(255, 71, 87, 0.2) !important;}.error-message {color: #ff4757;font-size: 12px;margin-top: 5px;display: none;}.captcha-group {display: flex;gap: 10px;}.captcha-input {flex: 1;}.captcha-btn {padding: 0 20px;background: linear-gradient(to right, #6a11cb, #2575fc);color: white;border: none;border-radius: 8px;cursor: pointer;font-size: 14px;transition: all 0.3s;white-space: nowrap;}.captcha-btn:hover {opacity: 0.9;}.captcha-btn:disabled {background: #ccc;cursor: not-allowed;}.terms {display: flex;align-items: flex-start;margin-bottom: 25px;font-size: 14px;}.terms input {margin-right: 10px;margin-top: 3px;}.terms a {color: #6a11cb;text-decoration: none;}.terms a:hover {text-decoration: underline;}.register-btn {width: 100%;padding: 14px;background: linear-gradient(to right, #6a11cb, #2575fc);color: white;border: none;border-radius: 8px;font-size: 16px;font-weight: 600;cursor: pointer;transition: transform 0.2s, box-shadow 0.2s;margin-bottom: 25px;}.register-btn:hover {transform: translateY(-2px);box-shadow: 0 5px 15px rgba(106, 17, 203, 0.4);}.register-btn:active {transform: translateY(0);}.login-link {text-align: center;color: #666;font-size: 14px;}.login-link a {color: #6a11cb;text-decoration: none;font-weight: 500;transition: color 0.2s;}.login-link a:hover {color: #2575fc;text-decoration: underline;}@media (max-width: 480px) {.register-container {padding: 30px 25px;}h1 {font-size: 24px;}.captcha-group {flex-direction: column;}.captcha-btn {padding: 12px;margin-top: 10px;}}.success-message {display: none;background-color: #2ed573;color: white;padding: 12px;border-radius: 8px;text-align: center;margin-bottom: 20px;}</style>
</head>
<body>
<div class="register-container"><h1>用户注册</h1><div class="success-message" id="successMessage">注册成功!正在跳转到登录页面...</div><form id="registerForm" action="/api/user/register" method="post" enctype="multipart/form-data"><div class="input-group"><label for="username">用户名</label><input type="text" id="username" name="username" placeholder="请输入用户名" required><div class="error-message" id="usernameError">用户名不能为空</div></div><div class="input-group"><label for="email">邮箱号</label><input type="email" id="email" name="email" placeholder="请输入您的邮箱" required><div class="error-message" id="emailError">请输入有效的邮箱地址</div></div><div class="input-group"><label for="password">密码</label><input type="password" id="password" name="password" placeholder="请输入密码" required><div class="error-message" id="passwordError">密码长度至少8位,包含字母和数字</div></div><div class="input-group"><label for="confirmPassword">确认密码</label><input type="password" id="confirmPassword" name="confirmPassword" placeholder="请再次输入密码" required><div class="error-message" id="confirmPasswordError">两次输入的密码不一致</div></div><div class="input-group"><label for="captcha">验证码</label><div class="captcha-group"><input type="text" id="captcha" name="captcha" class="captcha-input" placeholder="请输入验证码" required><button type="button" class="captcha-btn" id="getCaptcha">获取验证码</button></div><div class="error-message" id="captchaError">请输入正确的验证码</div></div><button type="submit" class="register-btn">注册</button></form><div class="login-link">已有账号?<a href="/login">立即登录</a></div>
</div><script>// 获取DOM元素(与注册页面HTML元素ID严格对应)const registerForm = document.getElementById('registerForm');const emailInput = document.getElementById('email');const usernameInput = document.getElementById('username');const passwordInput = document.getElementById('password');const confirmPasswordInput = document.getElementById('confirmPassword');const captchaInput = document.getElementById('captcha');const getCaptchaBtn = document.getElementById('getCaptcha');const successMessage = document.getElementById('successMessage');// 验证正则规则(与后端逻辑保持一致)const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 邮箱格式const PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/; // 密码:8位+字母+数字const USERNAME_REGEX = /^.{2,20}$/; // 用户名:2-20位字符(可按需调整)/*** 显示错误信息(复用注册页样式,与输入框ID关联)* @param {HTMLElement} inputElement - 输入框元素* @param {string} message - 错误提示内容*/function showError(inputElement, message) {const errorElement = document.getElementById(inputElement.id + 'Error');inputElement.classList.add('input-error');errorElement.textContent = message;errorElement.style.display = 'block';}/*** 清除错误信息* @param {HTMLElement} inputElement - 输入框元素*/function clearError(inputElement) {const errorElement = document.getElementById(inputElement.id + 'Error');inputElement.classList.remove('input-error');errorElement.style.display = 'none';}/*** 验证单个输入字段(适配所有表单字段,与后端校验逻辑对齐)* @param {HTMLElement} inputElement - 输入框元素* @returns {boolean} - 验证结果(true为通过)*/function validateField(inputElement) {const value = inputElement.value.trim();// 通用非空验证if (!value) {showError(inputElement, '此字段不能为空');return false;}// 按字段类型/ID差异化验证switch (inputElement.id) {case 'username':// 用户名长度验证(2-20位)if (!USERNAME_REGEX.test(value)) {showError(inputElement, '用户名长度需为2-20位');return false;}break;case 'email':// 邮箱格式验证if (!EMAIL_REGEX.test(value)) {showError(inputElement, '请输入有效的邮箱地址');return false;}break;case 'password':// 密码强度验证(与后端密码规则保持一致)if (!PASSWORD_REGEX.test(value)) {showError(inputElement, '密码长度至少8位,包含字母和数字');return false;}break;case 'confirmPassword':// 确认密码与原密码一致性验证const password = passwordInput.value.trim();if (value !== password) {showError(inputElement, '两次输入的密码不一致');return false;}break;case 'captcha':// 验证码非空及长度验证(假设验证码为6位,可按需调整)if (value.length !== 6) {showError(inputElement, '验证码需为6位字符');return false;}break;}clearError(inputElement);return true;}/*** 发送注册验证码(对接后端 /sendRegisterCode 接口)* @param {string} email - 目标邮箱* @returns {Promise<Object>} - 接口响应结果(success: boolean, message: string)*/async function sendRegisterCode(email) {try {const response = await fetch('/spi/verification/sendRegisterCode', {method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded',},body: new URLSearchParams({email: email // 与后端接口参数名一致})});const result = await response.json();return result; // 后端返回格式:{success: true/false, message: "提示信息"}} catch (error) {console.error('发送验证码失败,或者用户昵称已存在:', error);return { success: false, message: '网络异常,请稍后重试' };}}/*** 发送注册验证码(对接新的后端接口)* @param {string} email - 目标邮箱* @returns {Promise<Object>} - 接口响应结果*/async function sendRegisterCode(email) {try {const response = await fetch('/api/verification/send', {method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded',},body: new URLSearchParams({email: email,type: '1' // 注册类型,对应后端的Byte类型1})});const result = await response.json();return result; // 后端返回格式:{success: true/false, message: "提示信息"}} catch (error) {console.error('发送验证码失败或者用户名已存在:', error);return { success: false, message: '网络异常,请稍后重试' };}}/*** 验证注册验证码(对接新的后端接口)* @param {string} email - 目标邮箱* @param {string} code - 用户输入的验证码* @returns {Promise<boolean>} - 验证结果(true为通过)*/async function verifyRegisterCode(email, code) {try {const response = await fetch('/api/verification/verify', {method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded',},body: new URLSearchParams({email: email,code: code,type: '1' // 注册类型验证码,与后端定义的 (byte)1 一致})});const result = await response.json();if (!result.success) {// 验证码错误时,显示后端返回的提示showError(captchaInput, result.message);return false;}return true;} catch (error) {console.error('验证验证码失败:', error);showError(captchaInput, '网络异常,验证失败');return false;}}/*** 提交注册表单(对接新的后端接口)* @param {FormData} formData - 表单数据* @returns {Promise<Object>} - 接口响应结果*/async function submitRegisterForm(formData) {try {const response = await fetch('/api/user/register', {method: 'POST',body: formData});const result = await response.json();return result; // 后端返回格式:{success: true/false, message: "注册结果提示", data: 用户信息}} catch (error) {console.error('注册请求失败:', error);return { success: false, message: '网络异常,注册失败' };}}/*** 验证码倒计时功能(防止重复发送)* @param {HTMLButtonElement} button - 验证码按钮元素* @param {number} countdownSeconds - 倒计时秒数(默认60秒)*/function startCountdown(button, countdownSeconds = 60) {let countdown = countdownSeconds;button.disabled = true;button.textContent = `重新发送(${countdown}s)`;const timer = setInterval(() => {countdown--;button.textContent = `重新发送(${countdown}s)`;if (countdown <= 0) {clearInterval(timer);button.disabled = false;button.textContent = '获取验证码';}}, 1000);}// 2. 输入框实时验证(失焦时校验,输入时清除错误)document.querySelectorAll('#username, #email, #password, #confirmPassword, #captcha').forEach(input => {input.addEventListener('blur', () => validateField(input));input.addEventListener('input', () => clearError(input));});// 3. 「获取验证码」按钮点击事件(对接发送验证码接口)getCaptchaBtn.addEventListener('click', async function () {const email = emailInput.value.trim();const button = this;// 先验证邮箱格式if (!validateField(emailInput)) {return;}// 禁用按钮,防止重复点击button.disabled = true;button.textContent = '发送中...';// 调用发送验证码接口const sendResult = await sendRegisterCode(email);if (sendResult.success) {// 发送成功:启动倒计时,提示用户alert(sendResult.message);startCountdown(button);} else {// 发送失败:恢复按钮,显示错误alert(sendResult.message); // 如“该邮箱已注册”button.disabled = false;button.textContent = '获取验证码';}});// 4. 注册表单提交事件registerForm.addEventListener('submit', async function (e) {e.preventDefault();// 步骤1:验证所有表单字段const formFields = [usernameInput, emailInput, passwordInput, confirmPasswordInput, captchaInput];let isFormValid = true;formFields.forEach(field => {if (!validateField(field)) {isFormValid = false;}});// 验证不通过,终止提交if (!isFormValid) {return;}// 步骤3:验证验证码const email = emailInput.value.trim();const captcha = captchaInput.value.trim();const isCodeValid = await verifyRegisterCode(email, captcha);if (!isCodeValid) {return;}// 步骤4:准备表单数据(适配新的后端接口参数)const formData = new FormData();// 添加必填字段formData.append('email', email);formData.append('username', usernameInput.value.trim());formData.append('password', passwordInput.value.trim());// 步骤5:禁用注册按钮const registerBtn = this.querySelector('.register-btn');registerBtn.disabled = true;registerBtn.textContent = '注册中...';// 步骤6:提交注册请求const registerResult = await submitRegisterForm(formData);// 步骤7:处理注册结果if (registerResult.success) {successMessage.style.display = 'block';registerForm.style.display = 'none';setTimeout(() => {window.location.href = '/login';}, 2000);} else {alert(registerResult.message); // 显示后端返回的错误信息registerBtn.disabled = false;registerBtn.textContent = '注册';}});
</script>
</body>
</html>

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>重置密码 - SnapAn</title><style>* {margin: 0;padding: 0;box-sizing: border-box;font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;}body {background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);display: flex;justify-content: center;align-items: center;min-height: 100vh;padding: 20px;}.reset-container {background-color: white;border-radius: 12px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);width: 100%;max-width: 450px;padding: 40px 35px;position: relative;overflow: hidden;animation: fadeIn 0.5s ease-out;}.reset-container::before {content: '';position: absolute;top: 0;left: 0;width: 100%;height: 5px;background: linear-gradient(to right, #6a11cb, #2575fc);}@keyframes fadeIn {from { opacity: 0; transform: translateY(20px); }to { opacity: 1; transform: translateY(0); }}h1 {text-align: center;margin-bottom: 30px;color: #333;font-weight: 600;font-size: 28px;}.input-group {margin-bottom: 20px;position: relative;}label {display: block;margin-bottom: 8px;color: #555;font-weight: 500;font-size: 14px;}input[type="email"],input[type="password"],input[type="text"] {width: 100%;padding: 14px 15px;border: 1px solid #ddd;border-radius: 8px;font-size: 16px;transition: all 0.3s;height: 48px; /* 统一高度 */}input[type="email"]:focus,input[type="password"]:focus,input[type="text"]:focus {border-color: #6a11cb;box-shadow: 0 0 0 2px rgba(106, 17, 203, 0.2);outline: none;}.input-error {border-color: #ff4757 !important;box-shadow: 0 0 0 2px rgba(255, 71, 87, 0.2) !important;}.error-message {color: #ff4757;font-size: 12px;margin-top: 5px;display: none;}.captcha-group {display: flex;gap: 10px;}.captcha-input {flex: 1;}.captcha-btn {padding: 0 20px;background: linear-gradient(to right, #6a11cb, #2575fc);color: white;border: none;border-radius: 8px;cursor: pointer;font-size: 14px;transition: all 0.3s;white-space: nowrap;height: 48px; /* 与输入框高度一致 */min-width: 120px; /* 确保按钮有足够宽度 */}.captcha-btn:hover {opacity: 0.9;}.captcha-btn:disabled {background: #ccc;cursor: not-allowed;}.reset-btn {width: 100%;padding: 14px;background: linear-gradient(to right, #6a11cb, #2575fc);color: white;border: none;border-radius: 8px;font-size: 16px;font-weight: 600;cursor: pointer;transition: transform 0.2s, box-shadow 0.2s;margin-bottom: 25px;height: 48px; /* 保持一致性 */}.reset-btn:hover {transform: translateY(-2px);box-shadow: 0 5px 15px rgba(106, 17, 203, 0.4);}.reset-btn:active {transform: translateY(0);}.login-link {text-align: center;color: #666;font-size: 14px;}.login-link a {color: #6a11cb;text-decoration: none;font-weight: 500;transition: color 0.2s;}.login-link a:hover {color: #2575fc;text-decoration: underline;}@media (max-width: 480px) {.reset-container {padding: 30px 25px;}h1 {font-size: 24px;}.captcha-group {flex-direction: column;}.captcha-btn {padding: 12px;margin-top: 10px;height: 48px; /* 移动端保持高度一致 */}}.success-message {display: none;background-color: #2ed573;color: white;padding: 12px;border-radius: 8px;text-align: center;margin-bottom: 20px;}</style>
</head>
<body>
<div class="reset-container"><h1>重置密码</h1><div class="success-message" id="successMessage">密码重置成功!正在跳转到登录页面...</div><form id="resetForm" action="reset" method="post"><div class="input-group"><label for="email">账号(邮箱)</label><input type="email" id="email" name="email" placeholder="请输入您的邮箱" required><div class="error-message" id="emailError">请输入有效的邮箱地址</div></div><div class="input-group"><label for="captcha">验证码</label><div class="captcha-group"><input type="text" id="captcha" name="captcha" class="captcha-input" placeholder="请输入验证码" required><button type="button" class="captcha-btn" id="getCaptcha">获取验证码</button></div><div class="error-message" id="captchaError">请输入正确的验证码</div></div><div class="input-group"><label for="newPassword">新密码</label><input type="password" id="newPassword" name="newPassword" placeholder="请输入新密码" required><div class="error-message" id="newPasswordError">密码长度至少8位,包含字母和数字</div></div><div class="input-group"><label for="confirmPassword">确认新密码</label><input type="password" id="confirmPassword" name="confirmPassword" placeholder="请再次输入新密码" required><div class="error-message" id="confirmPasswordError">两次输入的密码不一致</div></div><button type="submit" class="reset-btn">确定</button></form><div class="login-link">想起密码了?<a href="/login">返回登录</a></div>
</div><script>// 获取DOM元素const resetForm = document.getElementById('resetForm');const emailInput = document.getElementById('email');const newPasswordInput = document.getElementById('newPassword');const confirmPasswordInput = document.getElementById('confirmPassword');const captchaInput = document.getElementById('captcha');const getCaptchaBtn = document.getElementById('getCaptcha');const successMessage = document.getElementById('successMessage');// 验证正则规则const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // 邮箱格式const PASSWORD_REGEX = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/; // 密码:8位+字母+数字/*** 显示错误信息* @param {HTMLElement} inputElement - 输入框元素* @param {string} message - 错误提示内容*/function showError(inputElement, message) {const errorElement = document.getElementById(inputElement.id + 'Error');inputElement.classList.add('input-error');errorElement.textContent = message;errorElement.style.display = 'block';}/*** 清除错误信息* @param {HTMLElement} inputElement - 输入框元素*/function clearError(inputElement) {const errorElement = document.getElementById(inputElement.id + 'Error');inputElement.classList.remove('input-error');errorElement.style.display = 'none';}/*** 验证单个输入字段* @param {HTMLElement} inputElement - 输入框元素* @returns {boolean} - 验证结果(true为通过)*/function validateField(inputElement) {const value = inputElement.value.trim();// 通用非空验证if (!value) {showError(inputElement, '此字段不能为空');return false;}// 按字段类型/ID差异化验证switch (inputElement.id) {case 'email':// 邮箱格式验证if (!EMAIL_REGEX.test(value)) {showError(inputElement, '请输入有效的邮箱地址');return false;}break;// case 'confirmEmail':// 确认邮箱与原邮箱一致性验证const email = emailInput.value.trim();if (value !== email) {showError(inputElement, '两次输入的邮箱不一致');return false;}break;case 'newPassword':// 密码强度验证if (!PASSWORD_REGEX.test(value)) {showError(inputElement, '密码长度至少8位,包含字母和数字');return false;}break;case 'confirmPassword':// 确认密码与原密码一致性验证const password = newPasswordInput.value.trim();if (value !== password) {showError(inputElement, '两次输入的密码不一致');return false;}break;case 'captcha':// 验证码非空及长度验证if (value.length !== 6) {showError(inputElement, '验证码需为6位字符');return false;}break;}clearError(inputElement);return true;}/*** 发送重置密码验证码(对接新的后端接口)* @param {string} email - 目标邮箱* @returns {Promise<Object>} - 接口响应结果*/async function sendResetCode(email) {try {const response = await fetch('/api/verification/send', {method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded',},body: new URLSearchParams({email: email,type: '2' // 重置密码类型,对应后端的Byte类型2})});const result = await response.json();return result; // 后端返回格式:{success: true/false, message: "提示信息"}} catch (error) {console.error('发送验证码失败:', error);return { success: false, message: '网络异常,请稍后重试' };}}/*** 验证重置密码验证码(对接新的后端接口)* @param {string} email - 目标邮箱* @param {string} code - 用户输入的验证码* @returns {Promise<boolean>} - 验证结果(true为通过)*/async function verifyResetCode(email, code) {try {const response = await fetch('/api/verification/verify', {method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded',},body: new URLSearchParams({email: email,code: code,type: '2' // 重置密码类型})});const result = await response.json();if (!result.success) {// 验证码错误时,显示后端返回的提示showError(captchaInput, result.message);return false;}return true;} catch (error) {console.error('验证验证码失败:', error);showError(captchaInput, '网络异常,验证失败');return false;}}/*** 提交重置密码表单(适配后端返回纯字符串)* @param {FormData} formData - 表单数据* @returns {Promise<Object>} - 接口响应结果*/async function submitResetForm(formData) {try {const response = await fetch('/api/user/update-password', {method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded',},body: new URLSearchParams({email: formData.get('email'),newPassword: formData.get('newPassword')})});// 后端返回的是纯字符串,不是JSONconst resultText = await response.text();// 根据HTTP状态码判断成功与否if (response.ok) {return { success: true, message: resultText };} else {return { success: false, message: resultText || '重置密码失败' };}} catch (error) {console.error('重置密码请求失败:', error);return { success: false, message: '网络异常,重置失败' };}}/*** 验证码倒计时功能(防止重复发送)* @param {HTMLButtonElement} button - 验证码按钮元素* @param {number} countdownSeconds - 倒计时秒数(默认60秒)*/function startCountdown(button, countdownSeconds = 60) {let countdown = countdownSeconds;button.disabled = true;button.textContent = `重新发送(${countdown}s)`;const timer = setInterval(() => {countdown--;button.textContent = `重新发送(${countdown}s)`;if (countdown <= 0) {clearInterval(timer);button.disabled = false;button.textContent = '获取验证码';}}, 1000);}// 修复输入框验证选择器(删除不存在的 confirmEmail)document.querySelectorAll('#email, #newPassword, #confirmPassword, #captcha').forEach(input => {input.addEventListener('blur', () => validateField(input));input.addEventListener('input', () => clearError(input));});// 「获取验证码」按钮点击事件getCaptchaBtn.addEventListener('click', async function () {const email = emailInput.value.trim();const button = this;// 先验证邮箱格式if (!validateField(emailInput)) {return;}// 禁用按钮,防止重复点击button.disabled = true;button.textContent = '发送中...';// 调用发送验证码接口const sendResult = await sendResetCode(email);if (sendResult.success) {// 发送成功:启动倒计时,提示用户alert(sendResult.message);startCountdown(button);} else {// 发送失败:恢复按钮,显示错误alert(sendResult.message);button.disabled = false;button.textContent = '获取验证码';}});// 重置密码表单提交事件resetForm.addEventListener('submit', async function (e) {e.preventDefault();// 步骤1:验证所有表单字段const formFields = [emailInput, newPasswordInput, confirmPasswordInput, captchaInput];let isFormValid = true;formFields.forEach(field => {if (!validateField(field)) {isFormValid = false;}});if (!isFormValid) {return;}// 步骤2:验证验证码const email = emailInput.value.trim();const captcha = captchaInput.value.trim();const isCodeValid = await verifyResetCode(email, captcha);if (!isCodeValid) {return;}// 步骤3:准备表单数据const formData = new FormData(resetForm);formData.delete('confirmPassword');// 步骤4:禁用重置按钮const resetBtn = this.querySelector('.reset-btn');resetBtn.disabled = true;resetBtn.textContent = '处理中...';// 步骤5:提交重置密码请求const resetResult = await submitResetForm(formData);// 步骤6:处理重置结果if (resetResult.success) {// 重置成功:显示成功提示,跳转登录页successMessage.style.display = 'block';resetForm.style.display = 'none';setTimeout(() => {window.location.href = '/login';}, 2000);} else {// 重置失败:恢复按钮,提示错误alert(resetResult.message);resetBtn.disabled = false;resetBtn.textContent = '确定';}});</script>
</body>
</html>

三,登录,需要开启拦截器

package com.snapan.config;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;/*** 自定义拦截器(例如:登录校验拦截器)*/
public class LoginInterceptor implements HandlerInterceptor {/*** 预处理:在Controller方法执行前调用* 返回true:放行(继续执行后续拦截器或Controller)* 返回false:拦截(不再执行后续操作)*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 示例:校验用户是否登录(从Session中获取用户信息)Object user = request.getSession().getAttribute("loginUser");if (user == null) {// 未登录:跳转到登录页response.sendRedirect(request.getContextPath() + "/login");return false; // 拦截}// 已登录:放行return true;// 辅助方法:判断是否为公开接口(无需登录)}/*** 后处理:在Controller方法执行后、视图渲染前调用* 可用于修改ModelAndView数据*/@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {// 例如:添加全局共享数据if (modelAndView != null) {modelAndView.addObject("globalKey", "全局值");}}/*** 完成处理:在视图渲染完成后调用(整个请求结束)* 可用于释放资源、记录日志等*/@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 例如:记录请求耗时long startTime = (long) request.getAttribute("startTime");long endTime = System.currentTimeMillis();System.out.println("请求耗时:" + (endTime - startTime) + "ms");}
}
package com.snapan.config;import com.snapan.interceptor.LogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebMvcConfig implements WebMvcConfigurer {// 注册拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 添加登录拦截器registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") // 拦截所有路径.excludePathPatterns( // 排除不拦截的路径"/login","/api/user/**","/user/login","/user/register","/css/**","/js/**","/images/**","/api/verification/**","/forgetPassword");}
//      "/api/user/login",//登录
//              "/forgetPassword", // 新增:放行重置密码页面
//              "/registration",// 新增:放行注册页面
//              "/api/user/info",//更新用户信息
//              "/api/user/{id}/storage",//查询用户存储使用情况(已用/总空间,单位:字节)
//              "/api/user/check-email",//查邮箱是否已存在
//              "/api/user/register",//用户注册接口
//              "/api/user/update-password"//修改密码// 配置静态资源放行(与XML的<mvc:resources>等效)@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {registry.addResourceHandler("/css/**").addResourceLocations("/css/");registry.addResourceHandler("/js/**").addResourceLocations("/js/");registry.addResourceHandler("/images/**").addResourceLocations("/images/");}
}
package com.snapan.controller;import com.snapan.entity.User;
import com.snapan.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 用户表控制器*/
@RestController
@RequestMapping("/api/user")
public class UserController {@Autowiredprivate UserService userService;/*** 获取列表*/@GetMapping("/list")public Map<String, Object> getList() {Map<String, Object> result = new HashMap<>();try {List<User> list = userService.getAll();result.put("success", true);result.put("data", list);result.put("message", "获取列表成功");} catch (Exception e) {result.put("success", false);result.put("message", "获取列表失败:" + e.getMessage());}return result;}/*** 根据ID获取*/@GetMapping("/{id}")public Map<String, Object> getById(@PathVariable Long id) {Map<String, Object> result = new HashMap<>();try {User user = userService.getById(id);if (user != null) {result.put("success", true);result.put("data", user);result.put("message", "获取成功");} else {result.put("success", false);result.put("message", "数据不存在");}} catch (Exception e) {result.put("success", false);result.put("message", "获取失败:" + e.getMessage());}return result;}/*** 添加*/@PostMapping("/add")public Map<String, Object> add(@RequestBody User user) {Map<String, Object> result = new HashMap<>();try {boolean success = userService.add(user);result.put("success", success);result.put("message", success ? "添加成功" : "添加失败");} catch (Exception e) {result.put("success", false);result.put("message", "添加失败:" + e.getMessage());}return result;}/*** 更新用户信息 - 支持部分字段更新* @return 更新后的用户信息*/@PutMapping(value = "/info", produces = "application/json;charset=UTF-8")@ResponseBodypublic Integer updateUser(@RequestParam(required = false) String username,@RequestParam(required = false) String avatar,@RequestParam Long id) {Integer rows = userService.update(username,avatar,id);return rows;}/*** 删除*/@DeleteMapping("/{id}")public Map<String, Object> delete(@PathVariable Long id) {Map<String, Object> result = new HashMap<>();try {boolean success = userService.delete(id);result.put("success", success);result.put("message", success ? "删除成功" : "删除失败");} catch (Exception e) {result.put("success", false);result.put("message", "删除失败:" + e.getMessage());}return result;}/*** 查询用户存储使用情况(已用/总空间,单位:字节)*/@GetMapping("/{id}/storage")public Map<String, Object> getStorage(@PathVariable Long id) {Map<String, Object> result = new HashMap<>();try {Long used = userService.getUsedSpace(id);Long total = userService.getTotalSpace(id);Map<String, Object> data = new HashMap<>();data.put("used", used);data.put("total", total);result.put("success", true);result.put("data", data);} catch (Exception e) {result.put("success", false);result.put("message", "查询存储失败:" + e.getMessage());}return result;}@PostMapping("/login")@ResponseBodypublic Map<String, Object> login(@RequestParam String email,@RequestParam String password,HttpSession session) {Map<String, Object> result = new HashMap<>();// 调用Service层查询用户User user = userService.selectByEmailAndPassword(email, password);if (user != null) {// 登录成功:存入Session,返回成功标识(不做后端跳转)session.setAttribute("loginUser", user);result.put("success", true);result.put("message", "登录成功");} else {// 登录失败:返回失败信息result.put("success", false);result.put("message", "邮箱或密码错误");}return result; // 返回JSON给前端,由前端决定跳转}/*** 处理退出登录* @param session 销毁Session,清除登录状态* @return 重定向到登录页*/@GetMapping("/user/logout")public String logout(HttpSession session) {session.invalidate(); // 销毁Sessionreturn "/login";}/*** 检查用户是否登录的接口* 前端调用此接口判断是否需要跳转登录页*/@GetMapping("/check-login")@ResponseBody // 确保返回JSON格式public Map<String, Object> checkLoginStatus(HttpSession session) {Map<String, Object> result = new HashMap<>();// 从Session中获取登录用户(登录成功时存入的"loginUser")User loginUser = (User) session.getAttribute("loginUser");if (loginUser != null) {// 已登录:返回登录状态+用户基本信息(DTO)User userDTO = new User();userDTO.setId(loginUser.getId());userDTO.setEmail(loginUser.getEmail());userDTO.setNickname(loginUser.getNickname()); // 假设User实体有nickname字段result.put("loggedIn", true);  // 登录状态:已登录result.put("user", userDTO);   // 用户信息(非敏感)} else {// 未登录:返回未登录状态result.put("loggedIn", false); // 登录状态:未登录result.put("user", null);}return result;}/*** 从Session中获取当前登录用户信息(示例接口)* 用于前端需要展示用户信息的场景(如顶部导航显示用户名)*/@GetMapping("/current-user")@ResponseBodypublic Map<String, Object> getCurrentUser(HttpSession session) {Map<String, Object> result = new HashMap<>();User loginUser = (User) session.getAttribute("loginUser");if (loginUser != null) {User userDTO = new User();userDTO.setId(loginUser.getId());userDTO.setEmail(loginUser.getEmail());userDTO.setNickname(loginUser.getNickname());result.put("success", true);result.put("data", userDTO);} else {result.put("success", false);result.put("message", "用户未登录");}return result;}/*** 检查邮箱是否已存在* @param email 邮箱地址* @return 检查结果*/@GetMapping("/check-email")public ResponseEntity<?> checkEmailExists(@RequestParam String email) {try {if (email == null || email.trim().isEmpty()) {return ResponseEntity.badRequest().body(createErrorResponse("邮箱参数不能为空"));}boolean exists = userService.existsByEmail(email);Map<String, Object> result = new HashMap<>();result.put("email", email);result.put("exists", exists);result.put("message", exists ? "邮箱已被注册" : "邮箱可用");return ResponseEntity.ok(createSuccessResponse("检查完成", result));} catch (Exception e) {return ResponseEntity.internalServerError().body(createErrorResponse("检查失败:" + e.getMessage()));}}/*** 用户注册接口*/@PostMapping("/register")public ResponseEntity<?> register(@RequestParam String email,@RequestParam String username,@RequestParam String password,@RequestParam(required = false) String nickname,@RequestParam(required = false) String avatar) {try {// 1. 基本参数验证(非空校验)if (email == null || email.trim().isEmpty()) {return ResponseEntity.badRequest().body(createErrorResponse("邮箱不能为空"));}if (username == null || username.trim().isEmpty()) {return ResponseEntity.badRequest().body(createErrorResponse("用户名不能为空"));}if (password == null || password.trim().isEmpty()) {return ResponseEntity.badRequest().body(createErrorResponse("密码不能为空"));}// 2. 验证邮箱是否已注册if (userService.existsByEmail(email)) {return ResponseEntity.badRequest().body(createErrorResponse("该邮箱已被注册,请更换邮箱"));}// 3. 构建用户对象并设置属性User user = new User();user.setEmail(email.trim()); // 去除空格user.setUsername(username.trim());user.setPassword(password); // 注意:实际生产环境需在此处加密密码(如BCrypt)// 昵称默认使用用户名(若未传入)user.setNickname(nickname != null && !nickname.trim().isEmpty() ? nickname.trim() : username.trim());user.setAvatar(avatar); // 头像可为空(使用默认头像)// 4. 设置默认值(与业务规则匹配)user.setTotalSpace(5L * 1024 * 1024 * 1024); // 默认5GB存储空间user.setUsedSpace(0L); // 初始已用空间为0user.setStatus((byte) 1); // 默认为正常状态(1-正常,0-禁用)user.setIsAdmin((byte) 0); // 默认为普通用户(0-非管理员)user.setJoinTime(new Date()); // 注册时间为当前时间// 5. 调用Service层执行注册(返回影响行数)Integer affectedRows = userService.register(user);// 6. 根据影响行数判断注册结果if (affectedRows != null && affectedRows > 0) {// 注册成功:返回用户信息(不含敏感字段)Map<String, Object> userInfo = new HashMap<>();userInfo.put("id", user.getId()); // MyBatis已通过useGeneratedKeys回填iduserInfo.put("username", user.getUsername());userInfo.put("email", user.getEmail());userInfo.put("nickname", user.getNickname());userInfo.put("avatar", user.getAvatar());userInfo.put("totalSpace", user.getTotalSpace());userInfo.put("usedSpace", user.getUsedSpace());userInfo.put("joinTime", user.getJoinTime());return ResponseEntity.ok(createSuccessResponse("注册成功", userInfo));} else {// 注册失败:影响行数为0或nullreturn ResponseEntity.badRequest().body(createErrorResponse("注册失败,请重试"));}} catch (Exception e) {// 捕获异常(如数据库异常)return ResponseEntity.internalServerError().body(createErrorResponse("服务器异常:" + e.getMessage()));}}@PostMapping("/update-password")public ResponseEntity<String> updatePassword( @RequestParam String email, @RequestParam String newPassword) {// 调用服务层执行修改userService.updatePasswordByEmail(email, newPassword);return ResponseEntity.ok("密码修改成功,请使用新密码登录");}/*** 统一成功响应格式*/private Map<String, Object> createSuccessResponse(String message, Object data) {Map<String, Object> response = new HashMap<>();response.put("success", true);response.put("code", 200);response.put("message", message);response.put("data", data);response.put("timestamp", System.currentTimeMillis());return response;}/*** 统一错误响应格式*/private Map<String, Object> createErrorResponse(String message) {Map<String, Object> response = new HashMap<>();response.put("success", false);response.put("code", 400);response.put("message", message);response.put("timestamp", System.currentTimeMillis());return response;}}
package com.snapan.dao;import com.snapan.entity.User;
import org.apache.ibatis.annotations.Param;import java.util.List;/*** 用户表数据访问接口*/
public interface UserDao {/*** 根据ID查询*/User selectById(Long id);/*** 查询所有*/List<User> selectAll();/*** 插入*/int insert(User user);/*** 更新,添加@Param注解,让mybati识别*/// 用@Param指定参数名,与mapper.xml中的#{username}等对应int update(@Param("username") String username,@Param("avatar") String avatar,@Param("id") Long id);/*** 根据ID删除*/int deleteById(Long id);/*** 统计用户已使用空间(字节)*/Long sumUsedSpaceByUserId(@Param("userId") Long userId);Integer register(User user);/*** 检验邮箱是否已经存在*/User existsByEmail(String email);/*** 根据邮箱和密码查询用户(登录验证)* @param email 邮箱* @param password 加密后的密码* @return 用户信息(若不存在则返回null)*/User selectByEmailAndPassword(@Param("email") String email, @Param("password") String password);/*** 根据邮箱更新密码* @param email 邮箱* @param newPassword 加密后的新密码* @return 影响行数*/int updatePasswordByEmail(@Param("email") String email, @Param("newPassword") String newPassword);
}
package com.snapan.entity;import java.util.Date;/*** 用户表*/
public class User {/*** 用户唯一标识*/private Long id;/*** 用户名*/private String username;/*** 密码(加密)*/private String password;/*** 邮箱*/private String email;/*** 用户昵称*/private String nickname;/*** 头像路径*/private String avatar;/*** 总存储空间(字节,默认5GB)*/private Long totalSpace;/*** 已用空间(字节)*/private Long usedSpace;/*** 账号状态(0-禁用,1-正常)*/private Byte status;/*** 是否管理员(0-否,1-是)*/private Byte isAdmin;/*** 创建时间*/private Date joinTime;/*** 更新时间*/private Date updateTime;/*** QQ*/private String qqOpenid;/***最后登录时间*/private Date lastLoginTime;public Date getLastLoginTime() {return lastLoginTime;}public void setLastLoginTime(Date lastLoginTime) {this.lastLoginTime = lastLoginTime;}public String getQqOpenid() {return qqOpenid;}public void setQqOpenid(String qqOpenid) {this.qqOpenid = qqOpenid;}public User() {}public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public String getEmail() {return email;}public void setEmail(String email) {this.email = email;}public String getNickname() {return nickname;}public void setNickname(String nickname) {this.nickname = nickname;}public String getAvatar() {return avatar;}public void setAvatar(String avatar) {this.avatar = avatar;}public Long getTotalSpace() {return totalSpace;}public void setTotalSpace(Long totalSpace) {this.totalSpace = totalSpace;}public Long getUsedSpace() {return usedSpace;}public void setUsedSpace(Long usedSpace) {this.usedSpace = usedSpace;}public Byte getStatus() {return status;}public void setStatus(Byte status) {this.status = status;}public Byte getIsAdmin() {return isAdmin;}public void setIsAdmin(Byte isAdmin) {this.isAdmin = isAdmin;}public Date getJoinTime() {return joinTime;}public void setJoinTime(Date joinTime) {this.joinTime = joinTime;}public Date getUpdateTime() {return updateTime;}public void setUpdateTime(Date updateTime) {this.updateTime = updateTime;}@Overridepublic String toString() {return "User{" +"id=" + id +", username='" + username + '\'' +", email='" + email + '\'' +", nickname='" + nickname + '\'' +", status=" + status +", isAdmin=" + isAdmin +'}';}
}
package com.snapan.service.impl;import com.snapan.dao.UserDao;
import com.snapan.entity.User;
import com.snapan.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import java.util.List;/*** 用户表服务实现类*/
@Service
@Transactional
public class UserServiceImpl implements UserService {@Autowiredprivate UserDao userDao;@Overridepublic User getById(Long id) {return userDao.selectById(id);}@Overridepublic List<User> getAll() {return userDao.selectAll();}@Overridepublic boolean add(User user) {return userDao.insert(user) > 0;}@Overridepublic int update(String username, String avatar, Long id) {return userDao.update(username,avatar,id);}@Overridepublic boolean delete(Long id) {return userDao.deleteById(id) > 0;}@Overridepublic Long getUsedSpace(Long userId) {Long sum = userDao.sumUsedSpaceByUserId(userId);return sum == null ? 0L : sum;}@Overridepublic Long getTotalSpace(Long userId) {User user = userDao.selectById(userId);return user == null || user.getTotalSpace() == null ? 0L : user.getTotalSpace();}@Overridepublic boolean existsByEmail(String email) {if(userDao.existsByEmail(email)!=null){return true;}return false;}@Overridepublic Integer register(User user) {return userDao.register(user);}@Overridepublic User selectByEmailAndPassword(String email, String password) {return userDao.selectByEmailAndPassword(email,password);}@Overridepublic int updatePasswordByEmail(String email, String newPassword) {return userDao.updatePasswordByEmail(email,newPassword);}}
package com.snapan.service;import com.snapan.entity.User;
import org.apache.ibatis.annotations.Param;import java.util.List;/*** 用户表服务接口*/
public interface UserService {/*** 根据ID查询*/User getById(Long id);/*** 查询所有*/List<User> getAll();/*** 添加*/boolean add(User user);/*** 更新*/int update(String username,String avatar,Long id);/*** 删除*/boolean delete(Long id);/*** 统计用户已使用空间(字节)*/Long getUsedSpace(Long userId);/*** 获取用户总空间(字节)*/Long getTotalSpace(Long userId);boolean existsByEmail(String email);/*** 用户注册* @param user 用户信息* @return 注册后的用户信息*/Integer register(User user);User selectByEmailAndPassword(@Param("email") String email, @Param("password") String password);/*** 根据邮箱修改密码* @param email 邮箱* @param newPassword 原始新密码(未加密)*/int updatePasswordByEmail(String email, String newPassword);}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.snapan.dao.UserDao"><!-- 结果映射 --><resultMap id="BaseResultMap" type="com.snapan.entity.User"><id column="id" property="id" jdbcType="BIGINT"/><result column="email" property="email" jdbcType="VARCHAR"/><result column="nickname" property="nickname" jdbcType="VARCHAR"/><result column="password" property="password" jdbcType="VARCHAR"/><result column="avatar" property="avatar" jdbcType="VARCHAR"/><result column="total_space" property="totalSpace" jdbcType="BIGINT"/><result column="used_space" property="usedSpace" jdbcType="BIGINT"/><result column="status" property="status" jdbcType="TINYINT"/><result column="is_admin" property="isAdmin" jdbcType="TINYINT"/><result column="create_time" property="joinTime" jdbcType="TIMESTAMP"/></resultMap><!-- 基础字段 --><sql id="Base_Column_List">id, email, nickname, password, avatar, total_space, used_space, status, is_admin, create_time</sql><!-- 根据ID查询 --><select id="selectById" parameterType="java.lang.Long" resultMap="BaseResultMap">SELECT <include refid="Base_Column_List"/>FROM userWHERE id = #{id}</select><!-- 查询所有 --><select id="selectAll" resultMap="BaseResultMap">SELECT <include refid="Base_Column_List"/>FROM user</select><!-- 插入,注册 --><insert id="register" parameterType="com.snapan.entity.User" useGeneratedKeys="true" keyProperty="id">INSERT INTO user<trim prefix="(" suffix=")" suffixOverrides=","><!-- 移除id字段,让数据库自动生成 --><if test="email != null and email != ''">email,</if><if test="nickname != null and nickname != ''">nickname,</if><if test="password != null and password != ''">password,</if><if test="avatar != null and avatar != ''">avatar,</if><if test="totalSpace != null">total_space,</if><if test="usedSpace != null">used_space,</if><if test="status != null">status,</if><if test="isAdmin != null">is_admin,</if><if test="joinTime != null">create_time,</if><if test="username != null">username,</if></trim><trim prefix="VALUES (" suffix=")" suffixOverrides=","><if test="email != null and email != ''">#{email},</if><if test="nickname != null and nickname != ''">#{nickname},</if><if test="password != null and password != ''">#{password},</if><if test="avatar != null and avatar != ''">#{avatar},</if><if test="totalSpace != null">#{totalSpace},</if><if test="usedSpace != null">#{usedSpace},</if><if test="status != null">#{status},</if><if test="isAdmin != null">#{isAdmin},</if><if test="joinTime != null">#{joinTime},</if><if test="username != null">#{username}</if></trim></insert><!-- 更新 --><update id="update" parameterType="com.snapan.entity.User">UPDATE user<set><if test="username != null and username  != ''">username  = #{username },</if><if test="avatar != null and avatar != ''">avatar = #{avatar},</if><!-- 直接设置为当前时间,无需判断参数(每次更新都刷新) -->update_time = NOW()</set>WHERE id = #{id}</update><!-- 根据ID删除 --><delete id="deleteById" parameterType="java.lang.Long">DELETE FROM user WHERE id = #{id}</delete><!-- 计算用户已使用空间(合计 file_base.file_size,过滤目录与无效记录) --><select id="sumUsedSpaceByUserId" parameterType="java.lang.Long" resultType="long">SELECT COALESCE(SUM(fb.file_size), 0)FROM user_file ufJOIN file_base fb ON uf.file_base_id = fb.idWHERE uf.user_id = #{userId}AND uf.status = 1AND uf.is_directory = 0</select><!-- 登录验证:根据邮箱和密码查询用户(状态需为正常) --><select id="selectByEmailAndPassword" resultMap="BaseResultMap">SELECT <include refid="Base_Column_List"/>FROM userWHERE email = #{email}AND password = #{password}AND status = 1 <!-- 仅允许状态为“正常”的用户登录 --></select><!--在注册的时候,检验邮箱是否已经存在--><select id="existsByEmail" resultMap="BaseResultMap">SELECT <include refid="Base_Column_List"/>FROM userWHERE email = #{email}</select><!--忘记密码,修改密码--><update id="updatePasswordByEmail">UPDATE userSET password = #{newPassword},  -- 密码update_time = NOW()        -- 自动更新修改时间WHERE email = #{email}</update></mapper>

登录邮箱的正则表达式

<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>用户登录</title><style>/* 保持原有CSS样式不变 */* {margin: 0;padding: 0;box-sizing: border-box;font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;}body {background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);display: flex;justify-content: center;align-items: center;min-height: 100vh;padding: 20px;}.login-container {background-color: white;border-radius: 12px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);width: 100%;max-width: 420px;padding: 40px 35px;position: relative;overflow: hidden;}.login-container::before {content: '';position: absolute;top: 0;left: 0;width: 100%;height: 5px;background: linear-gradient(to right, #6a11cb, #2575fc);}h1 {text-align: center;margin-bottom: 30px;color: #333;font-weight: 600;font-size: 28px;}.input-group {margin-bottom: 20px;position: relative;}label {display: block;margin-bottom: 8px;color: #555;font-weight: 500;font-size: 14px;}input[type="email"],input[type="password"] {width: 100%;padding: 14px 15px;border: 1px solid #ddd;border-radius: 8px;font-size: 16px;transition: all 0.3s;}input[type="email"]:focus,input[type="password"]:focus {border-color: #6a11cb;box-shadow: 0 0 0 2px rgba(106, 17, 203, 0.2);outline: none;}.options {display: flex;justify-content: space-between;align-items: center;margin-bottom: 25px;font-size: 14px;}.forgot-password {color: #6a11cb;text-decoration: none;transition: color 0.2s;}.forgot-password:hover {color: #2575fc;text-decoration: underline;}.register {color: #666;}.register a {color: #6a11cb;text-decoration: none;font-weight: 500;transition: color 0.2s;}.register a:hover {color: #2575fc;text-decoration: underline;}.login-btn {width: 100%;padding: 14px;background: linear-gradient(to right, #6a11cb, #2575fc);color: white;border: none;border-radius: 8px;font-size: 16px;font-weight: 600;cursor: pointer;transition: transform 0.2s, box-shadow 0.2s;margin-bottom: 25px;}.login-btn:hover {transform: translateY(-2px);box-shadow: 0 5px 15px rgba(106, 17, 203, 0.4);}.login-btn:active {transform: translateY(0);}.divider {text-align: center;position: relative;margin: 25px 0;color: #999;font-size: 14px;}.divider::before {content: '';position: absolute;top: 50%;left: 0;width: 45%;height: 1px;background-color: #eee;}.divider::after {content: '';position: absolute;top: 50%;right: 0;width: 45%;height: 1px;background-color: #eee;}.qq-login {display: flex;justify-content: center;align-items: center;padding: 12px;border: 1px solid #ddd;border-radius: 8px;background-color: #f8f9fa;cursor: pointer;transition: background-color 0.2s;}.qq-login:hover {background-color: #f0f2f5;}.qq-icon {width: 24px;height: 24px;margin-right: 10px;background-color: #12B7F5;border-radius: 50%;display: flex;justify-content: center;align-items: center;color: white;font-weight: bold;font-size: 14px;}.qq-login span {color: #333;font-size: 15px;}/* 新增按钮容器样式 */.additional-buttons {display: flex;gap: 15px;margin-top: 20px;}/* 次要按钮样式 - 与主按钮保持相同色调但不同样式 */.secondary-btn {flex: 1;padding: 12px;background: transparent;border: 2px solid #6a11cb;color: #6a11cb;border-radius: 8px;font-size: 16px;font-weight: 600;cursor: pointer;transition: all 0.3s;}.secondary-btn:hover {background: linear-gradient(to right, #6a11cb, #2575fc);color: white;transform: translateY(-2px);box-shadow: 0 5px 15px rgba(106, 17, 203, 0.3);}.secondary-btn:active {transform: translateY(0);}/* 移动端适配 */@media (max-width: 480px) {.additional-buttons {flex-direction: column;gap: 10px;}.secondary-btn {padding: 14px;}}@media (max-width: 480px) {.login-container {padding: 30px 25px;}h1 {font-size: 24px;}.options {flex-direction: column;align-items: flex-start;}.register {margin-top: 10px;}}</style>
</head>
<body>
<div class="login-container"><h1>用户登录</h1><!-- 修改表单,移除action属性,使用JavaScript处理 --><form id="loginForm" method="post"><!-- 登录失败提示 --><p th:if="${param.error}" style="color: red;">邮箱或密码错误</p><div class="input-group"><label for="email">邮箱</label><input type="email" id="email" name="email" placeholder="请输入邮箱" required></div><div class="input-group"><label for="password">密码</label><input type="password" id="password" name="password" placeholder="请输入密码" required></div><button type="submit" class="login-btn">登录</button></form><!-- 新增:重置密码和注册按钮 --><div class="additional-buttons"><button type="button" class="secondary-btn" id="forgetPasswordBtn">重置密码</button><button type="button" class="secondary-btn" id="registerBtn">注册</button></div><!-- 原QQ登录模块保持注释,如需启用可直接取消注释 -->
<!--  <%&#45;&#45;    <div class="divider">或使用以下方式登录</div>&#45;&#45;%>-->
<!--  <%&#45;&#45;    <div class="qq-login">&#45;&#45;%>-->
<!--  <%&#45;&#45;        <div class="qq-icon">QQ</div>&#45;&#45;%>-->
<!--  <%&#45;&#45;        <span>QQ登录</span>&#45;&#45;%>-->
<!--  <%&#45;&#45;    </div>&#45;&#45;%>-->
</div><script>let isLoginBtnClicked = false;document.querySelector('.login-btn').addEventListener('click', function() {isLoginBtnClicked = true;});// 修改表单提交处理document.getElementById('loginForm').addEventListener('submit', async function(e) {e.preventDefault();if (!isLoginBtnClicked) {alert('请点击「登录」按钮提交表单');return;}const email = document.getElementById('email').value;const password = document.getElementById('password').value;try {const response = await fetch('/api/user/login', {method: 'POST',headers: {'Content-Type': 'application/x-www-form-urlencoded',},body: new URLSearchParams({email: email,password: password})});// 若接口返回404/500,主动抛出错误if (!response.ok) {throw new Error(`接口错误:${response.status}`);}const data = await response.json();if (data.success) {// 登录成功:跳转到首页(使用相对路径,避免硬编码端口)window.location.href = '/';} else {alert(data.message); // 显示后端返回的失败信息}} catch (error) {console.error('登录请求失败:', error);// 更精准的错误提示,帮助排查问题alert('登录失败:可能是接口路径错误或服务器异常,请检查后端配置');} finally {isLoginBtnClicked = false; // 重置标记,避免重复提交}});// 新增:重置密码按钮点击事件document.getElementById('forgetPasswordBtn').addEventListener('click', function() {window.location.href = '/forgetPassword'; // 跳转到重置密码页面});// 新增:注册按钮点击事件document.getElementById('registerBtn').addEventListener('click', function() {window.location.href = '/registration'; // 跳转到注册页面});</script>
</body>
</html>

http://www.dtcms.com/a/597350.html

相关文章:

  • 多网卡同网段 IP 引发的 ARP Flux
  • 机器学习日报16
  • 11月份运维面试题
  • H100服务器维修“病历卡”:五大常见故障现象与根源分析
  • 9. Linux-riscv内存管理41-46问
  • 用mcu做灯光效果网站大连金州新区规划建设局网站
  • React 实战: Todo 应用学习小结
  • 网站性能优化方案网络设计及网络设计文档
  • 香港科技大学广州|可持续能源与环境学域博士招生宣讲会—兰州大学专场
  • 下午察:当机器人变得太像人
  • 青海城乡与建设厅网站个人简历简短范文
  • 黑马JAVAWeb -Vue工程化-API风格 - 组合式API
  • ubuntu更新nvidia显卡驱动
  • React Native 自建 JS Bundle OTA 更新系统:从零到一的完整实现与踩坑记录
  • 珠海建设网站公司代刷网站只做软件下载
  • 磐安县建设局网站甘肃营销型网站制作
  • UEC++ 如何知道有哪些UComponent?
  • 创建轻量级 3D 资产 - Three.js 中的 GLTF 案例
  • Android 主线程性能优化实战:从 90% 降至 13%
  • EPLAN电气设计-EPLAN在翻译中遇到的问题解析
  • 了解正向代理服务器:功能与用途
  • 建设厅网站业绩备案公示期诸城网络推广公司
  • sendfile函数与传统 read+write 拷贝相比的优势
  • ARL部署
  • 突破智能体训练瓶颈:DreamGym如何通过经验合成实现可扩展的强化学习?
  • 如何学习销售技巧,提高销售能力?
  • 建设北京公司网站兰州网站建设方案
  • 乐趣做网站公众信息服务平台
  • 有源代码怎么制作网站企业网络营销推广方案策划
  • C#使用Chart图表控件实时显示运动坐标