userName 2 weeks ago
parent
commit
042b2ebd58

+ 15 - 4
pom.xml

@@ -25,6 +25,11 @@
             <version>2.3.1</version>
         </dependency>
         <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-mail</artifactId>
+        </dependency>
+
+        <dependency>
             <groupId>javax.activation</groupId>
             <artifactId>activation</artifactId>
             <version>1.1.1</version>
@@ -43,7 +48,7 @@
         <dependency>
             <groupId>org.redisson</groupId>
             <artifactId>redisson</artifactId>
-            <version>3.12.1</version>
+            <version>3.16.4</version>
         </dependency>
         <dependency>
             <groupId>cn.hutool</groupId>
@@ -63,7 +68,11 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter</artifactId>
         </dependency>
-
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>0.9.1</version>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
@@ -98,7 +107,10 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-jpa</artifactId>
         </dependency>
-
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-thymeleaf</artifactId>
+        </dependency>
         <!-- MongoDB依赖 -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
@@ -141,7 +153,6 @@
                 <version>${spring-boot.version}</version>
                 <configuration>
                     <mainClass>com.zhentao.ImApplication</mainClass>
-                    <skip>true</skip>
                 </configuration>
                 <executions>
                     <execution>

+ 17 - 0
src/main/java/com/zhentao/config/CorsConfig.java

@@ -0,0 +1,17 @@
+package com.zhentao.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class CorsConfig implements WebMvcConfigurer {
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+            .allowedOriginPatterns("*")
+            .allowedMethods("*")
+            .allowCredentials(true)
+            .maxAge(3600);
+    }
+}

+ 36 - 0
src/main/java/com/zhentao/config/MailConfig.java

@@ -0,0 +1,36 @@
+package com.zhentao.config;
+
+import org.apache.tomcat.util.compat.TLS;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.JavaMailSenderImpl;
+
+import java.util.Properties;
+
+@Configuration
+public class MailConfig {
+    private final String host = "smtp.126.com";
+    private final int port = 465; // 或587,根据加密方式
+    private final String username = "vx3181048507@126.com";
+    private final String password = "RXdi3ZLGijcbEfSB"; // 126邮箱生成的授权码
+
+    @Bean
+    public JavaMailSender javaMailSender() {
+        JavaMailSenderImpl sender = new JavaMailSenderImpl();
+        sender.setHost(host);
+        sender.setPort(port);
+        sender.setUsername(username);
+        sender.setPassword(password);
+
+        Properties props = sender.getJavaMailProperties();
+        props.put("mail.smtp.auth", "true");
+        // 匹配端口的加密方式:
+        // - 465端口(SSL):props.put("mail.smtp.ssl.enable", "true");
+        props.put("mail.smtp.ssl.enable", "true"); // 关键:启用SSL
+
+        props.put("mail.debug", "true"); // 开启调试日志(关键!)
+
+        return sender;
+    }
+}

+ 2 - 1
src/main/java/com/zhentao/enums/ApiServerException.java

@@ -8,7 +8,8 @@ public enum ApiServerException implements BaseExceptionEnum{
     INTERRUPT(1,"操作中断"),
     REGISTERED(1,"已注册"),
     NOTE_ERROR(1,"验证码错误"),
-    NULL_PASSWORD(1,"密码错误");
+    NULL_PASSWORD(1,"密码错误"),
+    EMAIL_EXIST(1,"邮箱已存在");
 
     ApiServerException(Integer code,String msg){
         this.code=code;

+ 119 - 0
src/main/java/com/zhentao/tool/AppJwtUtil.java

@@ -0,0 +1,119 @@
+package com.zhentao.tool;
+
+import io.jsonwebtoken.*;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.util.*;
+
+public class AppJwtUtil {
+
+    // TOKEN的有效期一天(S)
+    private static final int TOKEN_TIME_OUT = 3_600;
+    // 加密KEY
+    private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
+    // 最小刷新间隔(S)
+    private static final int REFRESH_TIME = 300;
+
+    // 生产ID
+    public static String getToken(Long id){
+        Map<String, Object> claimMaps = new HashMap<>();
+        claimMaps.put("id",id);
+        long currentTime = System.currentTimeMillis();
+        return Jwts.builder()
+                .setId(UUID.randomUUID().toString())
+                .setIssuedAt(new Date(currentTime))  //签发时间
+                .setSubject("system")  //说明
+                .setIssuer("") //签发者信息
+                .setAudience("app")  //接收用户
+                .compressWith(CompressionCodecs.GZIP)  //数据压缩方式
+                .signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
+                .setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000))  //过期时间戳
+                .addClaims(claimMaps) //cla信息
+                .compact();
+    }
+
+    /**
+     * 获取token中的claims信息
+     *
+     * @param token
+     * @return
+     */
+    private static Jws<Claims> getJws(String token) {
+            return Jwts.parser()
+                    .setSigningKey(generalKey())
+                    .parseClaimsJws(token);
+    }
+
+    /**
+     * 获取payload body信息
+     *
+     * @param token
+     * @return
+     */
+    public static Claims getClaimsBody(String token) {
+        try {
+            return getJws(token).getBody();
+        }catch (ExpiredJwtException e){
+            return null;
+        }
+    }
+
+    /**
+     * 获取hearder body信息
+     *
+     * @param token
+     * @return
+     */
+    public static JwsHeader getHeaderBody(String token) {
+        return getJws(token).getHeader();
+    }
+
+    /**
+     * 是否过期
+     *
+     * @param claims
+     * @return -1:有效,0:有效,1:过期,2:过期
+     */
+    public static int verifyToken(Claims claims) {
+        if(claims==null){
+            return 1;
+        }
+        try {
+            claims.getExpiration()
+                    .before(new Date());
+            // 需要自动刷新TOKEN
+            if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){
+                return -1;
+            }else {
+                return 0;
+            }
+        } catch (ExpiredJwtException ex) {
+            return 1;
+        }catch (Exception e){
+            return 2;
+        }
+    }
+
+    /**
+     * 由字符串生成加密key
+     *
+     * @return
+     */
+    public static SecretKey generalKey() {
+        byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
+        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
+        return key;
+    }
+
+    public static void main(String[] args) {
+       /* Map map = new HashMap();
+        map.put("id","11");*/
+        System.out.println(AppJwtUtil.getToken(1102L));
+        Jws<Claims> jws = AppJwtUtil.getJws("eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQqEMAwA_5KzhURNt_qb1KZYQSi0wi6Lf9942NsMw3zh6AVW2DYmDGl2WabkZgreCaM6VXzhFBfJMcMARTqsxIG9Z888QLui3e3Tup5Pb81013KKmVzJTGo11nf9n8v4nMUaEY73DzTabjmDAAAA.4SuqQ42IGqCgBai6qd4RaVpVxTlZIWC826QA9kLvt9d-yVUw82gU47HDaSfOzgAcloZedYNNpUcd18Ne8vvjQA");
+        Claims claims = jws.getBody();
+        System.out.println(claims.get("id"));
+
+    }
+
+}

+ 136 - 0
src/main/java/com/zhentao/tool/SnowflakeIdGenerator.java

@@ -0,0 +1,136 @@
+package com.zhentao.tool;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+
+public class SnowflakeIdGenerator {
+    // 基础配置(Java 8+)
+    private static final long EPOCH = 1288834974657L; // 2010-11-04 00:00:00 UTC
+    private static final int DATACENTER_ID_BITS = 5;
+    private static final int WORKER_ID_BITS = 5;
+    private static final int SEQUENCE_BITS = 12;
+    // 位运算掩码
+    private static final long DATACENTER_ID_MASK = (1L << DATACENTER_ID_BITS) - 1;
+    private static final long WORKER_ID_MASK = (1L << WORKER_ID_BITS) - 1;
+    private static final long SEQUENCE_MASK = (1L << SEQUENCE_BITS) - 1;
+
+    // 位移偏移量
+    private static final int WORKER_ID_SHIFT = SEQUENCE_BITS;
+    private static final int DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
+    private static final int TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
+
+    private final long datacenterId;
+    private final long workerId;
+    private long sequence = 0L;
+    private long lastTimestamp = -1L;
+
+    private final Object lock = new Object();
+
+    public SnowflakeIdGenerator(long datacenterId, long workerId) {
+        if ((datacenterId & DATACENTER_ID_MASK) != datacenterId) {
+            throw new IllegalArgumentException("DataCenter ID 必须在 0-31 之间");
+        }
+        if ((workerId & WORKER_ID_MASK) != workerId) {
+            throw new IllegalArgumentException("Worker ID 必须在 0-31 之间");
+        }
+        this.datacenterId = datacenterId;
+        this.workerId = workerId;
+    }
+
+    public synchronized long nextId() {
+        long currentTimestamp = timeGen();
+
+        if (currentTimestamp < lastTimestamp) {
+            throw new IllegalStateException(
+                    String.format("时钟回退 %d 毫秒,禁止生成ID(上次: %d,当前: %d)",
+                            lastTimestamp - currentTimestamp, lastTimestamp, currentTimestamp));
+        }
+
+        if (currentTimestamp == lastTimestamp) {
+            sequence = (sequence + 1) & SEQUENCE_MASK;
+            if (sequence == 0) {
+                currentTimestamp = waitNextMillis(lastTimestamp);
+            }
+        } else {
+            sequence = 0;
+        }
+
+        lastTimestamp = currentTimestamp;
+
+        return ((currentTimestamp - EPOCH) << TIMESTAMP_SHIFT) |
+                (datacenterId << DATACENTER_ID_SHIFT) |
+                (workerId << WORKER_ID_SHIFT) |
+                sequence;
+    }
+
+    private long waitNextMillis(long lastTimestamp) {
+        long timestamp = timeGen();
+        while (timestamp <= lastTimestamp) {
+            timestamp = System.currentTimeMillis(); // 改用更可靠的时间源
+        }
+        return timestamp;
+    }
+
+    private long timeGen() {
+        return System.currentTimeMillis();
+    }
+
+    // 添加 parseId 方法
+    public static SnowflakeMeta parseId(long snowflakeId) {
+        long sequence = snowflakeId & SEQUENCE_MASK;
+        long workerId = (snowflakeId >> WORKER_ID_SHIFT) & WORKER_ID_MASK;
+        long datacenterId = (snowflakeId >> DATACENTER_ID_SHIFT) & DATACENTER_ID_MASK;
+        long timestamp = (snowflakeId >> TIMESTAMP_SHIFT) + EPOCH;
+
+        return new SnowflakeMeta(
+                timestamp,
+                datacenterId,
+                workerId,
+                sequence,
+                LocalDateTime.ofEpochSecond(timestamp / 1000, 0, ZoneOffset.ofHours(8))
+        );
+    }
+    public static Long getSnowId(){
+        SnowflakeIdGenerator generator=new SnowflakeIdGenerator(1,1);
+        Long id=generator.nextId();
+        SnowflakeMeta meta=SnowflakeIdGenerator.parseId(id);
+        return id;
+    }
+    // ---------------- 修复:添加完整的getter方法 ----------------
+    public static class SnowflakeMeta {
+        private final long timestamp;
+        private final long datacenterId;
+        private final long workerId;
+        private final long sequence;
+        private final LocalDateTime dateTime;
+
+        public SnowflakeMeta(long timestamp, long datacenterId, long workerId, long sequence, LocalDateTime dateTime) {
+            this.timestamp = timestamp;
+            this.datacenterId = datacenterId;
+            this.workerId = workerId;
+            this.sequence = sequence;
+            this.dateTime = dateTime;
+        }
+
+        // 显式定义getter(修复爆红)
+        public long getTimestamp() {
+            return timestamp;
+        }
+
+        public long getDatacenterId() {
+            return datacenterId;
+        }
+
+        public long getWorkerId() {
+            return workerId;
+        }
+
+        public long getSequence() {
+            return sequence;
+        }
+
+        public LocalDateTime getDateTime() {
+            return dateTime;
+        }
+    }
+}

+ 10 - 2
src/main/java/com/zhentao/user/controller/UserController.java

@@ -24,7 +24,16 @@ public class UserController {
     @PostMapping("/register")
     @NullLogin
     public Result Register(@RequestBody @Valid UserRegister userRegister){
-        return userLoginService.register(userRegister);
+        System.out.println(userRegister.toString());
+        userLoginService.register(userRegister);
+        return Result.OK(null,"发送成功,请查收验证邮件");
+    }
+
+    @PostMapping("/set-password")
+    @NullLogin
+    public Result setPassword(@RequestBody EmailDto emailDto) {
+        Result result= userLoginService.setUsernameAndPassword(emailDto);
+        return result;
     }
 
     //验证码
@@ -46,7 +55,6 @@ public class UserController {
     @PostMapping("/UserPassLogin")
     @NullLogin
     public Result UserPassLogin(@RequestBody @Valid UserPassDto userPassDto) {
-
         return userLoginService.UserPassLogin(userPassDto);
     }
 

+ 73 - 0
src/main/java/com/zhentao/user/controller/VerificationController.java

@@ -0,0 +1,73 @@
+package com.zhentao.user.controller;
+
+import com.zhentao.config.NullLogin;
+import com.zhentao.user.service.UserLoginService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.servlet.mvc.support.RedirectAttributes;
+
+import javax.servlet.http.HttpSession;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Date;
+
+@Controller
+public class VerificationController {
+
+    @Autowired
+    private UserLoginService userService;
+    @Value("${verification.secretKey}")
+    private String secretKey;
+
+    @Value("${verification.expiryMinutes}")
+    private long expiryMinutes;
+
+    @GetMapping("/verify-email")
+    @NullLogin
+    public String verifyEmail(@RequestParam("email") String email,
+                              @RequestParam("timestamp") long timestamp,
+                              @RequestParam("signature") String signature,
+                              HttpSession session,
+                              Model model, RedirectAttributes redirectAttributes) {
+        // 验证链接是否过期
+        long currentTime = System.currentTimeMillis();
+        long linkAgeMinutes = (currentTime - timestamp) / (1000 * 60);
+
+        if (!isValidVerificationLink(email, timestamp, signature)) {
+            model.addAttribute("errorMessage", "无效或过期的验证链接");
+            return "verification-error"; // 对应验证错误页面的视图名称
+        }
+        session.setAttribute("verifiedEmail", email);
+        session.setAttribute("verificationTime", new Date());
+        // 签名验证通过,标记邮箱为已验证
+        model.addAttribute("email", email);
+        return "registration-form";
+    }
+    private boolean isValidVerificationLink(String email, long timestamp, String signature) {
+        // 验证时间戳
+        long currentTime = System.currentTimeMillis();
+        if ((currentTime - timestamp) / (1000 * 60) > expiryMinutes) {
+            return false;
+        }
+
+        // 验证签名
+        String expectedSignature = generateSignature(email, timestamp);
+        return expectedSignature.equals(signature);
+    }
+    private String generateSignature(String email, long timestamp) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            String content = email + timestamp + secretKey;
+            byte[] hash = digest.digest(content.getBytes(StandardCharsets.UTF_8));
+            return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("生成签名失败", e);
+        }
+    }
+}

+ 7 - 6
src/main/java/com/zhentao/user/domain/UserLogin.java

@@ -1,23 +1,27 @@
 package com.zhentao.user.domain;
 
-import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
 import java.io.Serializable;
 import java.util.Date;
-import lombok.Data;
 
 /**
  * 用户
  * @TableName user_login
  */
+@Entity
 @TableName(value ="user_login")
 @Data
 public class UserLogin implements Serializable {
     /**
      * 用户ID
      */
+    @Id
     @TableId
     private Long id;
 
@@ -46,10 +50,6 @@ public class UserLogin implements Serializable {
      */
     private String avatar;
 
-    /**
-     * 用户名称
-     */
-    private String userName;
 
     /**
      * 性别1男2女3未知
@@ -135,6 +135,7 @@ public class UserLogin implements Serializable {
      * 
      */
     private String uniId;
+    private String email;
 
     @TableField(exist = false)
     private static final long serialVersionUID = 1L;

+ 14 - 0
src/main/java/com/zhentao/user/dto/EmailDto.java

@@ -0,0 +1,14 @@
+package com.zhentao.user.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class EmailDto {
+    private String email;
+    private String username;
+    private String password;
+}

+ 4 - 15
src/main/java/com/zhentao/user/dto/UserRegister.java

@@ -2,25 +2,14 @@ package com.zhentao.user.dto;
 
 import lombok.Data;
 
+import javax.validation.constraints.Email;
 import javax.validation.constraints.NotBlank;
-import javax.validation.constraints.Pattern;
 
 @Data
 public class UserRegister {
     //手机号
-    @NotBlank(message = "手机号不能为空")
-    @Pattern(regexp = "^1[3-9]\\d{9}$",message = "手机号格式不正确" )
-    private String phone;
+    @NotBlank(message = "邮箱不能为空")
+    @Email(message = "请提供有效的邮箱地址")
+    private String email;
 
-    //验证码
-    @NotBlank(message = "验证码不能为空")
-    private String code;
-
-    //用户名
-    @NotBlank(message = "用户名不能为空")
-    private String username;
-
-    //密码
-    @NotBlank(message = "密码不能为空")
-    private String password;
 }

+ 1 - 3
src/main/java/com/zhentao/user/mapper/UserLoginMapper.java

@@ -1,8 +1,7 @@
 package com.zhentao.user.mapper;
 
-import com.zhentao.user.domain.UserLogin;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import org.apache.ibatis.annotations.Mapper;
+import com.zhentao.user.domain.UserLogin;
 
 /**
 * @author 86183
@@ -10,7 +9,6 @@ import org.apache.ibatis.annotations.Mapper;
 * @createDate 2025-06-03 18:38:51
 * @Entity com.zhentao.user.domain.UserLogin
 */
-@Mapper
 public interface UserLoginMapper extends BaseMapper<UserLogin> {
 
 }

+ 5 - 0
src/main/java/com/zhentao/user/service/EmailService.java

@@ -0,0 +1,5 @@
+package com.zhentao.user.service;
+
+public interface EmailService {
+    void sendVerificationEmail(String to);
+}

+ 4 - 1
src/main/java/com/zhentao/user/service/UserLoginService.java

@@ -1,7 +1,7 @@
 package com.zhentao.user.service;
 
-import com.zhentao.user.domain.UserLogin;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.zhentao.user.domain.UserLogin;
 import com.zhentao.user.dto.*;
 import com.zhentao.vo.Result;
 
@@ -23,4 +23,7 @@ public interface UserLoginService extends IService<UserLogin> {
     Result UserPassLogin(UserPassDto userPassDto);
     //    忘记密码
     Result ForgetPass(ForgetPassDto forgetPassDto);
+
+    Result setUsernameAndPassword(EmailDto emailDto);
+
 }

+ 12 - 0
src/main/java/com/zhentao/user/service/UserRepository.java

@@ -0,0 +1,12 @@
+package com.zhentao.user.service;
+
+
+import com.zhentao.user.domain.UserLogin;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface UserRepository extends JpaRepository<UserLogin, Long> {
+    Optional<UserLogin> findByEmail(String email);
+
+}    

+ 57 - 0
src/main/java/com/zhentao/user/service/impl/EmailServiceImpl.java

@@ -0,0 +1,57 @@
+package com.zhentao.user.service.impl;
+
+import com.zhentao.user.service.EmailService;
+import lombok.SneakyThrows;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Service;
+
+import javax.mail.internet.MimeMessage;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+@Service
+public class EmailServiceImpl implements EmailService {
+    @Autowired
+    private JavaMailSender mailSender;
+    @Value("${verification.secretKey}")
+    private String secretKey;
+
+    @SneakyThrows
+    @Override
+    public void sendVerificationEmail(String to) {
+        MimeMessage message = mailSender.createMimeMessage();
+        MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
+        // 生成带签名和时间戳的验证URL
+        long timestamp = System.currentTimeMillis();
+        String signature = generateSignature(to, timestamp);
+        String verifyUrl = "http://47.111.130.63:8081/verify-email" +
+                "?email=" + to +
+                "&timestamp=" + timestamp +
+                "&signature=" + signature;
+        helper.setFrom("vx3181048507@126.com");
+        helper.setTo(to);
+        helper.setSubject("邮箱验证");
+        String htmlContent = "<html><body>" +
+                "<h3>请点击以下链接验证您的邮箱:</h3>" +
+                "<a href=\""+verifyUrl + "\">" +"点击即可注册"+ "</a>" +
+                "<p>链接有效期为30分钟,请尽快完成验证。</p>" +
+                "</body></html>";
+        helper.setText(htmlContent,true);
+        mailSender.send(message);
+    }
+    private String generateSignature(String email, long timestamp) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            String content = email + timestamp + secretKey;
+            byte[] hash = digest.digest(content.getBytes(StandardCharsets.UTF_8));
+            return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("生成签名失败", e);
+        }
+    }
+}

+ 47 - 77
src/main/java/com/zhentao/user/service/impl/UserLoginServiceImpl.java

@@ -1,15 +1,17 @@
 package com.zhentao.user.service.impl;
 
-import cn.hutool.core.util.IdUtil;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.zhentao.enums.ApiServerException;
 import com.zhentao.exception.AsynException;
+import com.zhentao.tool.SnowflakeIdGenerator;
 import com.zhentao.tool.TokenUtils;
 import com.zhentao.user.domain.UserLogin;
 import com.zhentao.user.dto.*;
-import com.zhentao.user.service.UserLoginService;
 import com.zhentao.user.mapper.UserLoginMapper;
+import com.zhentao.user.service.EmailService;
+import com.zhentao.user.service.UserLoginService;
+import com.zhentao.user.service.UserRepository;
 import com.zhentao.vo.Result;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
@@ -17,7 +19,9 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.util.DigestUtils;
+import org.springframework.util.StringUtils;
 
+import java.nio.charset.StandardCharsets;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
@@ -35,82 +39,25 @@ public class UserLoginServiceImpl extends ServiceImpl<UserLoginMapper, UserLogin
     private RedissonClient redissonClient;
     @Autowired
     private StringRedisTemplate stringRedisTemplate;
+    @Autowired
+    private UserRepository userRepository;
+    @Autowired
+    private EmailService emailService;
 
     //注册
     @Override
     public Result register(UserRegister userRegister) {
-        //打印用户注册的信息
-        System.err.println(userRegister);
-        //使用redisson客户端获取分布式锁,确保并发情况下用户注册的安全性
-        RLock lock = redissonClient.getLock(userRegister.getPhone() + userRegister.getPassword());
-        try {
-            boolean b = lock.tryLock(10, 20, TimeUnit.SECONDS);
-
-            if(b){
-                //用来判断验证码是否正确
-                String s = stringRedisTemplate.opsForValue().get(userRegister.getPhone());
-                System.err.println("redis取出来的验证码"+s);
-
-                //验证码不匹配就抛出异常
-                if(!s.equals(userRegister.getCode())){
-                    throw new AsynException(ApiServerException.NOTE_ERROR);
-                }
-                //根据手机号查询信息
-                QueryWrapper<UserLogin> queryWrapper=new QueryWrapper<>();
-                queryWrapper.eq("user_mobile",userRegister.getPhone());
-                UserLogin one = this.getOne(queryWrapper);
-
-                if(one==null){
-                    //新用户注册的流程
-                    UserLogin userLogin=new UserLogin();
-                    userLogin.setUserMobile(userRegister.getPhone());
-                    userLogin.setUserUsername(userRegister.getUsername());
-                    //随机字符串
-                    String uuid = String.valueOf(UUID.randomUUID());
-                    userLogin.setSalt(uuid);
-
-                    //md5加密
-                    String s1 = DigestUtils.md5DigestAsHex((uuid + userRegister.getPassword()).getBytes());
-                    userLogin.setUserPassword(s1);
-
-                    //生成唯一Id
-                    long l = IdUtil.getSnowflake(1, 1).nextId();
-                    userLogin.setId(l);
-                    //进行注册
-                    System.err.println(userLogin);
-                    boolean save = this.save(userLogin);
-                    if(save){
-                        return Result.OK(save,"注册成功");
-                    }else{
-                        return Result.ERR(save,"注册失败");
-                    }
-                }else{
-                    //老用户更新信息流程
-                    one.setUserUsername(userRegister.getUsername());
-                    //随机字符串
-                    String uuid = String.valueOf(UUID.randomUUID());
-                    one.setSalt(uuid);
-                    //md5加密
-                    String s1 = DigestUtils.md5DigestAsHex((uuid + userRegister.getPassword()).getBytes());
-                    one.setUserPassword(s1);
-
-                    //进行更新
-                    boolean b1 = this.updateById(one);
-                    if(b1){
-                        return Result.OK(b1,"注册成功");
-                    }else{
-                        return Result.ERR(b1,"注册失败");
-                    }
-                }
-            }
-
-        }catch (InterruptedException e){
-            Thread.currentThread().interrupt();
-        }finally {
-            //释放锁
-            lock.unlock();
+        UserLogin userLogin = new UserLogin();
+        QueryWrapper<UserLogin> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq("email", userRegister.getEmail());
+        UserLogin user = userLoginMapper.selectOne(queryWrapper);
+        if (user == null) {
+            userLogin.setId(SnowflakeIdGenerator.getSnowId());
+            emailService.sendVerificationEmail(userRegister.getEmail());
+            return Result.OK(null,"邮箱发送成功,请前往邮箱注册");
+        } else {
+            throw new AsynException(ApiServerException.EMAIL_EXIST);
         }
-        return null;
     }
 
 
@@ -122,7 +69,6 @@ public class UserLoginServiceImpl extends ServiceImpl<UserLoginMapper, UserLogin
         System.err.println("手机号:"+noteDto.getPhone());
         System.err.println("验证码:"+randomSixDigit);
         stringRedisTemplate.opsForValue().set(noteDto.getPhone(),randomSixDigit+"");
-
         return Result.OK(randomSixDigit,"发送成功");
     }
     //登录
@@ -209,9 +155,6 @@ public class UserLoginServiceImpl extends ServiceImpl<UserLoginMapper, UserLogin
                 }
                 // 生成JWT令牌
                 String jwtToken = TokenUtils.generateToken(one.getId()+"");
-                stringRedisTemplate.opsForValue().set(one.getId().toString(),jwtToken);
-                System.err.println(jwtToken);
-                System.err.println(stringRedisTemplate.opsForValue().get(one.getId().toString()));
                 // 返回登录成功结果和JWT令牌
                 return Result.OK("登录成功",jwtToken);
             }else {
@@ -264,6 +207,33 @@ public class UserLoginServiceImpl extends ServiceImpl<UserLoginMapper, UserLogin
         return null;
     }
 
+    @Override
+    public Result setUsernameAndPassword(EmailDto emailDto) {
+        if(emailDto.getUsername()!=null&&emailDto.getPassword()!=null){
+            if(StringUtils.isEmpty(emailDto.getEmail())){
+                return Result.ERR(null,"请求超时,请重新发送邮箱");
+            }
+            QueryWrapper<UserLogin> queryWrapper = new QueryWrapper<>();
+            queryWrapper.eq("user_username",emailDto.getUsername());
+            UserLogin userLogin = userLoginMapper.selectOne(queryWrapper);
+            if(userLogin!=null){
+                return Result.ERR(null,"用户已存在");
+            }
+            String salt = UUID.randomUUID().toString().replace("-", "");
+            UserLogin user=new UserLogin();
+            user.setId(SnowflakeIdGenerator.getSnowId());
+            user.setSalt(salt);
+            user.setUserUsername(emailDto.getUsername());
+            String s = DigestUtils.md5DigestAsHex((emailDto.getPassword()+salt).getBytes(StandardCharsets.UTF_8));
+            user.setUserPassword(s);
+            user.setEmail(emailDto.getEmail());
+            userLoginMapper.insert(user);
+            return Result.OK(null,"操作成功");
+        }
+        return Result.ERR(null,"用户名或密码不能为空");
+    }
+
+
 }
 
 

+ 3 - 0
src/main/resources/application.yml

@@ -39,3 +39,6 @@ aliyun:
     accessKeyId: LTAI5tH3XWv25v5LyeapQq1K
     accessKeySecret: pEP2P1ezDkPZJwuMFkdrqVNRTlATok
     bucketName: wangyongchun
+verification:
+  secretKey: 123456
+  expiryMinutes: 30

+ 1 - 11
src/main/resources/mapper/UserLoginMapper.xml

@@ -11,7 +11,6 @@
             <result property="salt" column="salt" jdbcType="VARCHAR"/>
             <result property="nickName" column="nick_name" jdbcType="VARCHAR"/>
             <result property="avatar" column="avatar" jdbcType="VARCHAR"/>
-            <result property="userName" column="user_name" jdbcType="VARCHAR"/>
             <result property="gender" column="gender" jdbcType="INTEGER"/>
             <result property="userIntro" column="user_intro" jdbcType="VARCHAR"/>
             <result property="userMobile" column="user_mobile" jdbcType="VARCHAR"/>
@@ -29,16 +28,7 @@
             <result property="openId" column="open_id" jdbcType="VARCHAR"/>
             <result property="sessionKey" column="session_key" jdbcType="VARCHAR"/>
             <result property="uniId" column="uni_id" jdbcType="VARCHAR"/>
+            <result property="email" column="email" jdbcType="VARCHAR"/>
     </resultMap>
 
-    <sql id="Base_Column_List">
-        id,user_username,user_password,
-        salt,nick_name,avatar,
-        user_name,gender,user_intro,
-        user_mobile,iden_no,grade_desc,
-        birth_day,birth_month,days,
-        label_list,status,remark,
-        created_time,updated_by,updated_time,
-        open_id,session_key,uni_id
-    </sql>
 </mapper>

+ 289 - 0
src/main/resources/templates/registration-form.html

@@ -0,0 +1,289 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+    <title>完善注册信息</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+            font-family: 'Segoe UI', sans-serif;
+        }
+
+        body {
+            background: #f5f7fa;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            min-height: 100vh;
+            padding: 1rem; /* 添加移动端内边距 */
+        }
+
+        .container {
+            background: #fff;
+            padding: 2rem 1.5rem; /* 调整内边距 */
+            border-radius: 12px;
+            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+            width: 100%; /* 改为百分比宽度 */
+            max-width: 400px; /* 最大宽度限制 */
+        }
+
+        h2 {
+            text-align: center;
+            color: #1a1a1a;
+            margin-bottom: 1.5rem; /* 减小标题间距 */
+            font-size: 1.3rem; /* 增大标题字体 */
+        }
+
+        .form-group {
+            margin-bottom: 1.2rem; /* 调整间距 */
+        }
+
+        label {
+            display: block;
+            margin-bottom: 0.5rem;
+            color: #333;
+            font-weight: 500;
+            font-size: 1.05rem; /* 增大标签字体 */
+        }
+
+        input {
+            width: 100%;
+            padding: 14px; /* 增大输入框内边距 */
+            border: 2px solid #e0e0e0;
+            border-radius: 8px;
+            font-size: 1.05rem; /* 增大输入字体 */
+            transition: border-color 0.3s ease;
+        }
+
+        input:focus {
+            outline: none;
+            border-color: #4CAF50;
+            box-shadow: 0 0 8px rgba(76, 175, 80, 0.2);
+        }
+
+        button {
+            width: 100%;
+            padding: 14px; /* 增大按钮内边距 */
+            background: #4CAF50;
+            color: white;
+            border: none;
+            border-radius: 8px;
+            font-size: 1.1rem; /* 增大按钮字体 */
+            font-weight: 600;
+            cursor: pointer;
+            transition: background 0.3s ease;
+        }
+
+        button:hover {
+            background: #45a049;
+        }
+
+        button:disabled {
+            background: #ccc;
+            cursor: not-allowed;
+        }
+
+        .error-message {
+            color: #dc3545;
+            font-size: 0.9rem;
+            margin-top: 0.5rem;
+        }
+
+        .password-strength {
+            margin-top: 0.5rem;
+        }
+
+        .strength-bar {
+            height: 6px;
+            border-radius: 3px;
+            margin-top: 0.3rem;
+        }
+
+        .strength-weak { background: #ff4444; }
+        .strength-medium { background: #ffbb33; }
+        .strength-strong { background: #00c851; }
+
+        .loading-indicator {
+            display: inline-block;
+            width: 20px; /* 增大加载指示器 */
+            height: 20px;
+            margin-left: 0.5rem;
+            border: 3px solid #f3f3f3;
+            border-top: 3px solid #4CAF50;
+            border-radius: 50%;
+            animation: spin 1s linear infinite;
+        }
+
+        @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+
+        /* 响应式调整 */
+        @media (max-width: 480px) {
+            .container {
+                padding: 1.5rem 1rem;
+            }
+
+            h2 {
+                font-size: 1.2rem;
+                margin-bottom: 1.2rem;
+            }
+
+            label, input, button {
+                font-size: 1rem;
+            }
+
+            input, button {
+                padding: 12px;
+            }
+
+            .loading-indicator {
+                width: 16px;
+                height: 16px;
+            }
+        }
+    </style>
+</head>
+<body>
+<div class="container">
+    <h2>完善注册信息</h2>
+
+    <form id="registrationForm">
+        <input type="hidden" id="email" th:value="${email}">
+
+        <div class="form-group">
+            <label for="username">用户名</label>
+            <input type="text" id="username" required>
+            <div id="usernameError" class="error-message"></div>
+        </div>
+
+        <div class="form-group">
+            <label for="password">设置密码</label>
+            <input type="password" id="password" required>
+            <div class="password-strength">
+                <div class="strength-bar" id="strengthBar"></div>
+                <div id="strengthText" class="error-message"></div>
+            </div>
+        </div>
+
+        <div class="form-group">
+            <label for="confirmPassword">确认密码</label>
+            <input type="password" id="confirmPassword" required>
+            <div id="confirmError" class="error-message"></div>
+        </div>
+
+        <button id="submitBtn" type="button" onclick="submitForm()">
+            完成注册
+            <span id="loadingIndicator" class="loading-indicator" style="display: none;"></span>
+        </button>
+
+        <div id="message" class="error-message" style="margin-top: 1rem;" align="center"></div>
+    </form>
+</div>
+
+<script>
+    const username = document.getElementById('username');
+    const password = document.getElementById('password');
+    const confirmPassword = document.getElementById('confirmPassword');
+    const submitBtn = document.getElementById('submitBtn');
+    const loadingIndicator = document.getElementById('loadingIndicator');
+    const strengthBar = document.getElementById('strengthBar');
+    const strengthText = document.getElementById('strengthText');
+    const confirmError = document.getElementById('confirmError');
+    const usernameError = document.getElementById('usernameError');
+
+    // 用户名实时验证(优化提示逻辑)
+    username.addEventListener('input', function() {
+        const val = this.value.trim();
+        let errorMsg = '';
+        if (val.length < 3) errorMsg = '用户名至少需要3个字符';
+        else if (val.length > 20) errorMsg = '用户名最多20个字符';
+        else if (!/^[a-zA-Z0-9_]+$/.test(val)) errorMsg = '用户名只能包含字母、数字和下划线';
+
+        username.setCustomValidity(errorMsg);
+        usernameError.textContent = errorMsg;
+    });
+
+    // 密码强度验证(修复长度判断和提示信息)
+    password.addEventListener('input', function() {
+        const val = this.value;
+        let strength = 0;
+        let msg = '';
+
+        // 先检查长度
+        if (val.length < 8) {
+            strength = 0;
+            msg = '密码长度至少需要8个字符';
+        } else {
+            strength += /[A-Z]/.test(val) ? 1 : 0; // 大写字母
+            strength += /[a-z]/.test(val) ? 1 : 0; // 小写字母
+            strength += /\d/.test(val) ? 1 : 0;    // 数字
+            strength += /[^A-Za-z0-9]/.test(val) ? 1 : 0; // 特殊字符
+
+            switch (strength) {
+                case 1: msg = '密码强度弱(建议包含更多字符类型)'; break;
+                case 2: msg = '密码强度中等(建议添加特殊字符)'; break;
+                case 3:
+                case 4: msg = '密码强度强'; break;
+            }
+        }
+
+        // 更新强度条和提示
+        strengthBar.className = `strength-bar ${strength < 1 ? 'strength-weak' : strength < 3 ? 'strength-medium' : 'strength-strong'}`;
+        strengthBar.style.width = strength > 0 ? `${(strength / 4) * 100}%` : '0%'; // 按比例计算宽度
+        strengthText.textContent = msg;
+        strengthText.className = strength >= 3 ? 'success-message' : 'error-message';
+    });
+
+    // 确认密码验证(修复错误提示显示)
+    confirmPassword.addEventListener('input', function() {
+        const passwordVal = password.value;
+        const confirmVal = this.value;
+        const errorMsg = confirmVal !== passwordVal ? '两次输入的密码不一致' : '';
+
+        this.setCustomValidity(errorMsg);
+        confirmError.textContent = errorMsg;
+    });
+
+    function submitForm() {
+        if (!document.getElementById('registrationForm').checkValidity()) {
+            return;
+        }
+
+        submitBtn.disabled = true;
+        loadingIndicator.style.display = 'inline-block';
+
+        fetch('/user/set-password', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({
+                email: document.getElementById('email').value,
+                username: username.value,
+                password: password.value
+            })
+        })
+            .then(res => {
+                if (!res.ok) throw new Error('网络请求失败');
+                return res.json();
+            })
+            .then(data => {
+                const messageDiv = document.getElementById('message');
+                messageDiv.className = data.code === 200 ? 'success-message' : 'error-message';
+                messageDiv.textContent = data.msg;
+
+            })
+            .catch(error => {
+                document.getElementById('message').textContent = '网络错误,请重试';
+            })
+            .finally(() => {
+                submitBtn.disabled = false;
+                loadingIndicator.style.display = 'none';
+            });
+    }
+</script>
+</body>
+</html>

+ 88 - 0
src/main/resources/templates/verification-error.html

@@ -0,0 +1,88 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="UTF-8">
+    <title>验证失败</title>
+    <!-- 添加移动端视口适配 -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+
+    <style>
+        /* 全局样式 */
+        body {
+            margin: 0;
+            padding: 0;
+            font-family: 'Segoe UI', sans-serif;
+        }
+
+        .container {
+            /* 改为百分比宽度,适配不同屏幕 */
+            max-width: 90%;
+            margin: 40px auto;
+            padding: 20px;
+            text-align: center;
+            background-color: white;
+            border-radius: 12px;
+            box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+        }
+
+        .error-message {
+            color: #d9534f;
+            font-size: 16px; /* 减小字体大小 */
+            margin-bottom: 16px;
+            line-height: 1.5;
+        }
+
+        h1 {
+            font-size: 24px; /* 标题字体适配手机 */
+            margin-bottom: 12px;
+            color: #333;
+        }
+
+        p {
+            font-size: 14px;
+            color: #666;
+            margin-bottom: 24px;
+        }
+
+        /* 优化按钮样式,增加触控区域 */
+        .btn {
+            display: inline-block;
+            padding: 12px 24px; /* 增大点击区域 */
+            background-color: #007bff;
+            color: white;
+            text-decoration: none;
+            border-radius: 6px;
+            font-size: 14px;
+            font-weight: 500;
+            transition: background-color 0.3s;
+            width: 100%; /* 手机端占满宽度 */
+            max-width: 200px; /* 限制最大宽度 */
+            margin: 0 auto;
+        }
+
+        /* 媒体查询:更小屏幕优化 */
+        @media (max-width: 480px) {
+            .container {
+                margin: 20px auto;
+                padding: 16px;
+                border-radius: 8px;
+            }
+
+            h1 {
+                font-size: 20px;
+            }
+
+            .error-message {
+                font-size: 15px;
+            }
+        }
+    </style>
+</head>
+<body>
+<div class="container">
+    <h1>验证失败</h1>
+    <div class="error-message" th:text="${errorMessage}">无效或过期的验证链接</div>
+    <p>请检查链接是否完整,或尝试重新发送验证邮件。</p>
+</div>
+</body>
+</html>