添加了swagger接口、重置密码、修改了usercontroller

Change-Id: Ib651fa9b0fe0b220eb8cb88dde2b63d6bf54895e
diff --git "a/API\346\216\245\345\217\243\346\226\207\346\241\243.txt" "b/API\346\216\245\345\217\243\346\226\207\346\241\243.txt"
deleted file mode 100644
index ade70d0..0000000
--- "a/API\346\216\245\345\217\243\346\226\207\346\241\243.txt"
+++ /dev/null
@@ -1,177 +0,0 @@
-
-
-以下是当前项目所有后端接口及其说明,前端可以根据此文档完成联调。
-
----
-
-## 通用返回结构
-
-所有接口均返回如下 JSON:
-
-```json
-{
-  "code": <int>,     // 0=成功,1=失败
-  "msg": "<string>", // 成功或失败的提示信息
-  "data": <object>   // 成功时返回的数据,失败时为 null
-}
-```
-
----
-
-## 1. 发送邮箱验证码
-
-```
-POST /sendVerification
-Content-Type: application/x-www-form-urlencoded
-```
-
-**请求参数(Form)**
-
-| 字段         | 类型     | 必填 | 说明         |
-| ---------- | ------ | -- | ---------- |
-| email      | String | 是  | 接收验证码的邮箱   |
-| inviteCode | String | 是  | 前端用户填写的邀请码 |
-
-**成功响应示例**
-
-```json
-{
-  "code": 0,
-  "msg": "验证码已发送",
-  "data": null
-}
-```
-
-**错误示例**
-
-```json
-{
-  "code": 1,
-  "msg": "无效邀请码",
-  "data": null
-}
-```
-
----
-
-## 2. 用户注册
-
-```
-POST /register
-Content-Type: application/x-www-form-urlencoded
-```
-
-**请求参数(Form)**
-
-| 字段                   | 类型      | 必填 | 说明                  |
-| -------------------- | ------- | -- | ------------------- |
-| username             | String  | 是  | 用户名(前端输入)           |
-| email                | String  | 是  | 邮箱(用于接收验证码 & 唯一性校验) |
-| verificationCode     | String  | 是  | 邮箱收到的 6 位验证码        |
-| password             | String  | 是  | 登录密码                |
-| identificationNumber | Integer | 是  | 身份证号(8 位数字)         |
-| inviteCode           | String  | 是  | 邀请码(预先在数据库插入并发放给用户) |
-
-**成功响应示例**
-
-```json
-{
-  "code": 0,
-  "msg": "注册成功",
-  "data": null
-}
-```
-
-**常见错误示例**
-
-```json
-{ "code":1, "msg":"参数不完整", "data":null }
-```
-
-```json
-{ "code":1, "msg":"验证码错误或已过期", "data":null }
-```
-
-```json
-{ "code":1, "msg":"无效邀请码或已过期", "data":null }
-```
-
-```json
-{ "code":1, "msg":"邮箱已被注册", "data":null }
-```
-
----
-
-## 3. 用户登录
-
-```
-POST /login
-Content-Type: application/x-www-form-urlencoded
-```
-
-**请求参数(Form)**
-
-| 字段                   | 类型      | 必填 | 说明          |
-| -------------------- | ------- | -- | ----------- |
-| identificationNumber | Integer | 是  | 身份证号(8 位数字) |
-| password             | String  | 是  | 登录密码        |
-
-**成功响应示例**
-
-```json
-{
-  "code": 0,
-  "msg": "登录成功",
-  "data": "<JWT_TOKEN>"
-}
-```
-
-* `data`:返回的 JWT,后续请求请在请求头 `token: <JWT_TOKEN>` 中携带。
-
-**失败示例**
-
-```json
-{ "code":1, "msg":"用户名或密码错误", "data":null }
-```
-
----
-
-## 4. 获取当前用户信息
-
-```
-GET /api/me
-Headers:
-  token: <JWT_TOKEN>
-```
-
-**说明**
-
-* 需在请求头中携带登录时返回的 JWT。若未携带或验证失败,将返回 `NOT_LOGIN`。
-
-**成功响应示例**
-
-```json
-{
-  "code": 0,
-  "msg": "success",
-  "data": {
-    "id": 10,
-    "username": "usertest1",
-    "identificationNumber": 12345678
-  }
-}
-```
-
-**失效示例**
-
-```json
-{ "code":1, "msg":"NOT_LOGIN", "data":null }
-```
-
----
-
-## 静态页面
-
-* `login.html`:登录页(填写身份证号、密码)
-* `register.html`:注册页(填写用户名、邮箱、验证码、密码、身份证号、邀请码)
-* `home.html`:受保护的首页示例(从 `localStorage` 读取 token 并调用 `/api/me`)
diff --git a/pom.xml b/pom.xml
index 17ed152..e7e976f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -43,12 +43,6 @@
         </dependency>
 
         <dependency>
-            <groupId>org.mybatis.spring.boot</groupId>
-            <artifactId>mybatis-spring-boot-starter</artifactId>
-            <version>2.3.0</version>
-        </dependency>
-
-        <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
             <optional>true</optional>
@@ -67,12 +61,25 @@
         </dependency>
 
         <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
+            <version>2.1.0</version> <!-- 或最新稳定版 -->
+        </dependency>
+
+
+        <dependency>
             <groupId>io.springfox</groupId>
             <artifactId>springfox-boot-starter</artifactId>
             <version>3.0.0</version>
         </dependency>
 
         <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-ui</artifactId>
+            <version>1.6.14</version> <!-- 或者最新版本 -->
+        </dependency>
+
+        <dependency>
             <groupId>com.volcengine</groupId>
             <artifactId>volcengine-java-sdk-ark-runtime</artifactId>
             <version>LATEST</version>
@@ -117,6 +124,7 @@
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
+            <version>5.5.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
diff --git a/src/main/java/edu/bjtu/groupone/backend/config/Config.java b/src/main/java/edu/bjtu/groupone/backend/config/Config.java
index cfc3745..6816c05 100644
--- a/src/main/java/edu/bjtu/groupone/backend/config/Config.java
+++ b/src/main/java/edu/bjtu/groupone/backend/config/Config.java
@@ -28,7 +28,14 @@
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
         registry.addInterceptor(authInterceptor)
-                .addPathPatterns("/**");
+                .addPathPatterns("/**")
+                .excludePathPatterns(
+                        // 放行 swagger-ui 的所有静态资源和 API 文档
+                        "/swagger-ui/**",
+                        "/swagger-ui.html",
+                        "/v3/api-docs/**",
+                        "/favicon.ico"
+                );
     }
     // 其它 CORS、静态资源、Swagger 配置保持不动
 }
diff --git a/src/main/java/edu/bjtu/groupone/backend/config/SwaggerConfig.java b/src/main/java/edu/bjtu/groupone/backend/config/SwaggerConfig.java
index 042c278..8d85d7a 100644
--- a/src/main/java/edu/bjtu/groupone/backend/config/SwaggerConfig.java
+++ b/src/main/java/edu/bjtu/groupone/backend/config/SwaggerConfig.java
@@ -1,3 +1,4 @@
+// src/main/java/edu/bjtu/groupone/backend/config/SwaggerConfig.java
 package edu.bjtu.groupone.backend.config;
 
 import org.springframework.context.annotation.Bean;
@@ -5,35 +6,33 @@
 import springfox.documentation.builders.PathSelectors;
 import springfox.documentation.builders.RequestHandlerSelectors;
 import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.Contact;
 import springfox.documentation.spi.DocumentationType;
 import springfox.documentation.spring.web.plugins.Docket;
+import java.util.Collections;
 
 @Configuration
-//@EnableOpenApi
 public class SwaggerConfig {
     @Bean
-    public Docket docket(){
-        return new Docket(DocumentationType.OAS_30)
+    public Docket apiDocket() {
+        return new Docket(DocumentationType.OAS_30) // 使用 OpenAPI 3.0
                 .apiInfo(apiInfo())
-                .enable(true)
-                .groupName("CJB")
                 .select()
-                .apis(RequestHandlerSelectors.basePackage("com.example.eliteedu_prism.controller"))
-                .paths(PathSelectors.ant("/controller/**"))
+                .apis(RequestHandlerSelectors.basePackage("edu.bjtu.groupone.backend.controller"))
+                .paths(PathSelectors.any())
                 .build();
     }
 
-
-    @SuppressWarnings("all")
-    public ApiInfo apiInfo(){
+    private ApiInfo apiInfo() {
         return new ApiInfo(
-                "zrj's api",
-                "redis project",
+                "PtSite 后端 API 文档",
+                "登录 / 注册 / 重置密码 等接口",
                 "v1.0",
-                "2261839618@qq.com", //开发者团队的邮箱
-                "ZRJ",
-                "Apache 2.0",  //许可证
-                "http://www.apache.org/licenses/LICENSE-2.0" //许可证链接
+                null,
+                new Contact("Team1 后端", "", "1697232065@qq.com"),
+                "Apache 2.0",
+                "http://www.apache.org/licenses/LICENSE-2.0",
+                Collections.emptyList()
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/controller/UserController.java b/src/main/java/edu/bjtu/groupone/backend/controller/UserController.java
index 7ac29f1..0f32a74 100644
--- a/src/main/java/edu/bjtu/groupone/backend/controller/UserController.java
+++ b/src/main/java/edu/bjtu/groupone/backend/controller/UserController.java
@@ -1,99 +1,137 @@
+// src/main/java/edu/bjtu/groupone/backend/controller/UserController.java
 package edu.bjtu.groupone.backend.controller;
 
 import edu.bjtu.groupone.backend.model.Result;
 import edu.bjtu.groupone.backend.model.User;
 import edu.bjtu.groupone.backend.service.UserService;
 import edu.bjtu.groupone.backend.utils.JwtUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.HashMap;
 import java.util.Map;
 
 @CrossOrigin
-@Slf4j
+@Tag(name = "用户相关接口") //输入http://localhost:8080/swagger-ui/index.html或http://localhost:8080/v3/api-docs/可查看接口文档
 @RestController
+@Slf4j
 public class UserController {
 
     @Autowired
     private UserService userService;
 
-    /** 登录:接收表单字段 identificationNumber、password */
+    /** 邮箱+密码 登录 */
+    @Operation(summary = "用户登录", description = "使用邮箱和密码进行登录,成功后返回 JWT token")
     @PostMapping("/login")
-    public Result login(
-            @RequestParam Integer identificationNumber,
-            @RequestParam String password
-    ) {
+    public Result login(@RequestBody Map<String,String> body) {
+        String email = body.get("email");
+        String password = body.get("password");
+        if (email == null || password == null) {
+            return Result.error("参数不完整");
+        }
         User u = new User();
-        u.setIdentificationNumber(identificationNumber);
+        u.setEmail(email);
         u.setPassword(password);
         User user = userService.login(u);
         if (user != null) {
-            Map<String, Object> payload = new HashMap<>();
-            payload.put("id", user.getUserId());
-            payload.put("username", user.getUsername());
-            payload.put("identificationNumber", user.getIdentificationNumber());
+            var payload = Map.<String,Object>of(
+                    "id",       user.getUserId(),
+                    "username", user.getUsername(),
+                    "email",    user.getEmail()
+            );
             String token = JwtUtils.generateJwt(payload);
-
-            // 打印到控制台
-            log.info("【登录成功】生成的 JWT token = {}", token);
             return Result.success(token);
         }
-        return Result.error("用户名或密码错误");
+        return Result.error("邮箱或密码错误");
     }
 
-    /** 注册:接收表单字段 username、email、password、verificationCode、identificationNumber、inviteCode */
+    /** 注册 */
+    @Operation(summary = "用户注册", description = "注册新用户,必须提供用户名、邮箱、验证码、密码和身份证号码")
     @PostMapping("/register")
-    public Result register(
-            @RequestParam String username,
-            @RequestParam String email,
-            @RequestParam String password,
-            @RequestParam String verificationCode,
-            @RequestParam Integer identificationNumber
-    ) {
-
-        // 2. 验证邮箱验证码
-        if (!userService.verifyCode(email, verificationCode)) {
+    public Result register(@RequestBody Map<String,String> body) {
+        String username = body.get("username");
+        String email    = body.get("email");
+        String code     = body.get("verificationCode");
+        String pwd      = body.get("password");
+        String idNumStr = body.get("identificationNumber");
+        if (email==null||code==null||pwd==null||idNumStr==null) {
+            return Result.error("参数不完整");
+        }
+        int idNum = Integer.parseInt(idNumStr);
+        if (!userService.verifyCode(email, code)) {
             return Result.error("验证码错误或已过期");
         }
-        // 3. 检查邮箱唯一
         if (userService.isEmailExists(email)) {
             return Result.error("邮箱已被注册");
         }
-        // 4. 构造用户实体并设置所有非空字段
+        if (username!=null && userService.isUsernameExists(username)) {
+            return Result.error("用户名已被占用");
+        }
+        if (idNumStr == null || idNumStr.isBlank()) {
+            return Result.error("身份证号码不能为空");
+        }
+        if (idNumStr.length() !=8){
+            return Result.error("身份证号码长度不正确,应为8位");
+        }
+        if (userService.isIdentificationNumberExists(idNum)) {
+            return Result.error("身份证号码已被注册");
+        }
+
         User newUser = new User();
         newUser.setUsername(username);
         newUser.setEmail(email);
-        newUser.setPassword(password);
-        newUser.setIdentificationNumber(identificationNumber);
-        // registrationDate 格式要和你的 Mapper SQL 对应,如果是 String 存储:
-        newUser.setRegistrationDate(
-                LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
-        );
-        // 5. 插入到数据库
+        newUser.setPassword(pwd);
+        newUser.setIdentificationNumber(idNum);
         userService.register(newUser);
-
         return Result.success("注册成功");
     }
 
-    /** 发送邮箱验证码:接收表单字段 email、inviteCode */
+    /** 发送注册验证码 */
+    @Operation(summary = "发送注册验证码", description = "向指定邮箱发送注册验证码")
     @PostMapping("/sendVerification")
-    public Result sendVerification(
-            @RequestParam String email
-    ) {
-
+    public Result sendVerification(@RequestBody Map<String,String> p) {
+        String email = p.get("email");
+        if (email==null) {
+            return Result.error("邮箱不能为空");
+        }
         userService.sendVerificationCode(email);
         return Result.success("验证码已发送");
     }
 
+    @Operation(summary = "发送重置密码验证码", description = "向指定邮箱发送重置密码的验证码")
+    @PostMapping("/sendResetCode")
+    public Result sendResetCode(@RequestBody Map<String, String> body) {
+        String email = body.get("email");
+        userService.sendResetCode(email);
+        return Result.success("重置验证码已发送");
+    }
+
+    @Operation(summary = "重置密码", description = "根据邮箱和验证码重置密码")
+    @PostMapping("/resetPassword")
+    public Result resetPassword(@RequestBody Map<String, String> body) {
+        String email       = body.get("email");
+        String code        = body.get("code");
+        String newPassword = body.get("newPassword");
+        boolean ok = userService.resetPassword(email, code, newPassword);
+        return ok
+                ? Result.success("密码已重置")
+                : Result.error("验证码错误或已过期");
+    }
+
+    /** 前端受保护页面获取当前用户信息 */
+    @Operation(summary = "获取当前用户信息", description = "需要提供有效的 JWT token")
     @GetMapping("/api/me")
     public Result me(@RequestHeader("token") String token) {
-        // 从 token 解析出用户信息,JwtUtils.parseJwt 返回一个 Map
         var claims = JwtUtils.parseJwt(token);
-        // 你可以根据需要返回完整 User,也可以直接返回用户名
-        return Result.success(claims);
+        if (claims == null) {
+            return Result.error("无效的 token");
+        }
+        var info = Map.<String,Object>of(
+                "username", claims.get("username"),
+                "token", token
+        );
+        return Result.success(info);
     }
 }
diff --git a/src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java b/src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java
index f00a646..09c65c9 100644
--- a/src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java
+++ b/src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java
@@ -23,15 +23,17 @@
         String url = req.getRequestURI();
         log.info("Request URI: {}", url);
         // 放行无需鉴权的路径:登录、注册、发码、静态资源、HTML 页面
-        if (url.equals("/") ||
+        if (url.contains("sendResetCode") ||  // 发送重置验证码
+                url.contains("resetPassword") ||
+                url.equals("/") ||
                 url.endsWith(".html") ||
                 url.contains("login") ||
                 url.contains("register") ||
                 url.equals("/api/me") ||      // 放行用户信息接口
                 url.equals("/error") ||       // 放行错误页面
                 url.contains("sendVerification") ||
-                url.startsWith("/swagger-ui") ||
-                url.startsWith("/v3/api-docs") ||
+                url.startsWith("/swagger-ui/") ||
+                url.startsWith("/v3/api-docs/") ||
                 url.startsWith("/favicon.ico") ||
                 url.startsWith("/static/")) {
             return true;
diff --git a/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java b/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java
index 3cbf2c1..b1bda89 100644
--- a/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java
+++ b/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java
@@ -6,7 +6,7 @@
 
 @Mapper
 public interface UserMapper {
-     @Select("select * from user where identification_number=#{identificationNumber} and password=#{password}")
+     @Select("select * from user where email=#{email} and password=#{password}")
      User login(User user);
 
      @Select("SELECT * FROM user WHERE username = #{username}")
@@ -19,4 +19,11 @@
             "VALUES(#{username}, #{email}, #{password}, #{registrationDate}, #{identificationNumber})")
     @Options(useGeneratedKeys = true, keyProperty = "userId")
     void insertUser(User user);
+
+    @Update("UPDATE `user` SET password = #{password} WHERE email = #{email}")
+    int updatePasswordByEmail(@Param("email") String email,
+                              @Param("password") String password);
+
+    @Select("SELECT COUNT(*) FROM user WHERE identification_number = #{identificationNumber}")
+    int countByIdentificationNumber(int identificationNumber);
 }
diff --git a/src/main/java/edu/bjtu/groupone/backend/service/UserService.java b/src/main/java/edu/bjtu/groupone/backend/service/UserService.java
index 662dafe..37831ec 100644
--- a/src/main/java/edu/bjtu/groupone/backend/service/UserService.java
+++ b/src/main/java/edu/bjtu/groupone/backend/service/UserService.java
@@ -1,15 +1,17 @@
+// src/main/java/edu/bjtu/groupone/backend/service/UserService.java
 package edu.bjtu.groupone.backend.service;
 
-
 import edu.bjtu.groupone.backend.model.User;
 
 public interface UserService {
-
     User login(User user);
-
     boolean isUsernameExists(String username);
     boolean isEmailExists(String email);
     void register(User user);
     void sendVerificationCode(String email);
     boolean verifyCode(String email, String code);
+    boolean isIdentificationNumberExists(int idNum);
+    // 新增重置密码相关
+    void sendResetCode(String email);
+    boolean resetPassword(String email, String code, String newPassword);
 }
diff --git a/src/main/java/edu/bjtu/groupone/backend/service/impl/UserServImpl.java b/src/main/java/edu/bjtu/groupone/backend/service/impl/UserServImpl.java
index 828b78d..27ba52f 100644
--- a/src/main/java/edu/bjtu/groupone/backend/service/impl/UserServImpl.java
+++ b/src/main/java/edu/bjtu/groupone/backend/service/impl/UserServImpl.java
@@ -20,6 +20,7 @@
     @Autowired
     private EmailUtil emailUtil;
     private final ConcurrentHashMap<String, String> emailCodes = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<String, String> resetCodes = new ConcurrentHashMap<>();
     private static final long CODE_EXPIRE_MINUTES = 5;
 
     @Override
@@ -38,6 +39,11 @@
     }
 
     @Override
+    public boolean isIdentificationNumberExists(int idNum) {
+        return userMapper.countByIdentificationNumber(idNum) > 0;
+    }
+
+    @Override
     public void register(User user) {
         // 如果前端未提供用户名,可生成默认
         String username = user.getUsername();
@@ -78,6 +84,36 @@
         return System.currentTimeMillis() <= expireTime && code.equals(inputCode);
     }
 
+
+    @Override
+    public void sendResetCode(String email) {
+        if (!isEmailExists(email)) {
+            throw new IllegalArgumentException("该邮箱尚未注册");
+        }
+        String code = generateRandomCode();
+        emailUtil.sendVerificationEmail(email, code);
+        long expireAt = System.currentTimeMillis() + CODE_EXPIRE_MINUTES * 60 * 1000;
+        resetCodes.put(email, code + "|" + expireAt);
+    }
+
+    @Override
+    public boolean resetPassword(String email, String code, String newPassword) {
+        String stored = resetCodes.get(email);
+        if (stored == null) return false;
+        String[] p = stored.split("\\|");
+        long exp = Long.parseLong(p[1]);
+        if (System.currentTimeMillis() > exp || !p[0].equals(code)) {
+            return false;
+        }
+        int cnt = userMapper.updatePasswordByEmail(email, newPassword);
+        if (cnt == 1) {
+            resetCodes.remove(email);
+            return true;
+        }
+        return false;
+    }
+
+
     private String generateRandomCode() {
         return String.format("%06d", new Random().nextInt(1_000000));
     }
diff --git a/src/main/resources/static/home.html b/src/main/resources/static/home.html
index 5cac114..507e3ad 100644
--- a/src/main/resources/static/home.html
+++ b/src/main/resources/static/home.html
@@ -9,22 +9,20 @@
 <div id="userInfo"></div>
 <script>
     (async () => {
-        const token = localStorage.getItem('token'); // 从 localStorage 获取 token
-        if (!token) return window.location.href = 'login.html'; // 如果 token 不存在,重定向到登录页面
-
-        const res = await fetch('/api/me', {
-            headers: { 'token': token } // 使用 token 请求用户信息
-        });
-
-        const json = await res.json(); // 解析响应数据
-        if (json.code !== 0) {
-            return window.location.href = 'login.html'; // 如果获取用户信息失败,重定向到登录页面
+        const token = localStorage.getItem('token');
+        if (!token) {
+            return window.location.href = 'login.html';
         }
-
-        // 输出当前用户和 token
-        document.getElementById('userInfo')
-            .innerText = '当前用户:' + json.data.username + ',Token:' + json.data; // 显示用户名和 token
+        const res = await fetch('/api/me', {
+            headers: { 'token': token }
+        });
+        const json = await res.json();
+        if (json.code !== 0) {
+            return window.location.href = 'login.html';
+        }
+        document.getElementById('userInfo').innerText =
+            `当前用户:${json.data.username},Token:${json.data.token}`;
     })();
 </script>
 </body>
-</html>
\ No newline at end of file
+</html>
diff --git a/src/main/resources/static/login.html b/src/main/resources/static/login.html
index 993941d..f7d02c7 100644
--- a/src/main/resources/static/login.html
+++ b/src/main/resources/static/login.html
@@ -8,31 +8,38 @@
 <body>
 <h2>用户登录</h2>
 <form id="loginForm">
-    <label>身份证号:</label>
-    <input type="number" name="identificationNumber" required>
-    <label>密码:</label>
-    <input type="password" name="password" required>
+    <div>
+        <label>邮箱:</label>
+        <input type="email" name="email" required>
+    </div>
+    <div>
+        <label>密码:</label>
+        <input type="password" name="password" required>
+    </div>
     <button type="submit">登录</button>
 </form>
++  <p><a href="reset.html">忘记密码?</a></p>
+
 <script>
-    document.getElementById('loginForm')
-        .addEventListener('submit', async e => {
-            e.preventDefault();
-            const form = new URLSearchParams(new FormData(e.target));
-            const res = await fetch('/login', {
-                method: 'POST',
-                body: form
-            });
-            const json = await res.json();
-            if (res.ok && json.code === 0) {
-                // 保存 token
-                localStorage.setItem('token', json.data);
-                // 跳转到受保护的页面
-                window.location.href = 'home.html';
-            } else {
-                alert(json.msg);
-            }
+    document.getElementById('loginForm').addEventListener('submit', async e => {
+        e.preventDefault();
+        const data = {
+            email:    e.target.email.value,
+            password: e.target.password.value
+        };
+        const res = await fetch('/login', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify(data)
         });
+        const json = await res.json();
+        if (res.ok && json.code === 0) {
+            localStorage.setItem('token', json.data);
+            window.location.href = 'home.html';
+        } else {
+            alert(json.msg || json.error);
+        }
+    });
 </script>
 </body>
 </html>
diff --git a/src/main/resources/static/register.html b/src/main/resources/static/register.html
index 549c790..72fc1b8 100644
--- a/src/main/resources/static/register.html
+++ b/src/main/resources/static/register.html
@@ -1,4 +1,3 @@
-<!-- src/main/resources/static/register.html -->
 <!DOCTYPE html>
 <html lang="zh-CN">
 <head>
@@ -8,44 +7,65 @@
 <body>
 <h2>用户注册</h2>
 <form id="regForm">
-    <label>用户名:</label>
-    <input type="text" name="username" required>
-    <label>邮箱:</label>
-    <input type="email" name="email" required>
-    <button type="button" id="sendCode">发送验证码</button>
-    <label>邮箱验证码:</label>
-    <input type="text" name="verificationCode" required>
-    <label>密码:</label>
-    <input type="password" name="password" required>
-    <label>身份证号(8 位数字):</label>
-    <input type="number" name="identificationNumber" required>
+    <div>
+        <label>用户名:</label>
+        <input type="text" name="username" required>
+    </div>
+    <div>
+        <label>邮箱:</label>
+        <input type="email" name="email" required>
+        <button type="button" id="sendCode">发送验证码</button>
+    </div>
+    <div>
+        <label>邮箱验证码:</label>
+        <input type="text" name="verificationCode" required>
+    </div>
+    <div>
+        <label>密码:</label>
+        <input type="password" name="password" required>
+    </div>
+    <div>
+        <label>身份证号(8 位):</label>
+        <input type="text" name="identificationNumber" required pattern="\d{8}" maxlength="8">
+    </div>
     <button type="submit">注册</button>
 </form>
+
 <script>
-    document.getElementById('sendCode')
-        .addEventListener('click', async () => {
-            const form = new URLSearchParams();
-            form.set('email', document.querySelector('[name=email]').value);
-            const res = await fetch('/sendVerification', {
-                method: 'POST',
-                body: form
-            });
-            alert((await res.json()).msg);
+    // 发送注册验证码
+    document.getElementById('sendCode').addEventListener('click', async () => {
+        const email = document.querySelector('[name=email]').value;
+        if (!email) { alert('请先输入邮箱'); return; }
+        const res = await fetch('/sendVerification', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ email })
         });
-    document.getElementById('regForm')
-        .addEventListener('submit', async e => {
-            e.preventDefault();
-            const form = new URLSearchParams(new FormData(e.target));
-            const res = await fetch('/register', {
-                method: 'POST',
-                body: form
-            });
-            const json = await res.json();
-            alert(json.msg);
-            if (res.ok && json.code===0) {
-                window.location.href='login.html';
-            }
+        const json = await res.json();
+        alert(json.msg || json.error);
+    });
+
+    // 提交注册
+    document.getElementById('regForm').addEventListener('submit', async e => {
+        e.preventDefault();
+        const data = {
+            username:             e.target.username.value,
+            email:                e.target.email.value,
+            verificationCode:     e.target.verificationCode.value,
+            password:             e.target.password.value,
+            identificationNumber: e.target.identificationNumber.value
+        };
+        const res = await fetch('/register', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify(data)
         });
+        const json = await res.json();
+        alert(json.msg || json.error);
+        if (res.ok && json.code === 0) {
+            window.location.href = 'login.html';
+        }
+    });
 </script>
 </body>
 </html>
diff --git a/src/main/resources/static/reset.html b/src/main/resources/static/reset.html
new file mode 100644
index 0000000..7a87ccf
--- /dev/null
+++ b/src/main/resources/static/reset.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <title>重置密码</title>
+</head>
+<body>
+<h2>重置密码</h2>
+<form id="resetForm">
+    <div>
+        <label>邮箱:</label>
+        <input type="email" name="email" required>
+        <button type="button" id="sendResetCode">获取重置验证码</button>
+    </div>
+    <div>
+        <label>验证码:</label>
+        <input type="text" name="code" required>
+    </div>
+    <div>
+        <label>新密码:</label>
+        <input type="password" name="newPassword" required>
+    </div>
+    <button type="submit">重置密码</button>
+</form>
+
+<script>
+    // 发送重置验证码
+    document.getElementById('sendResetCode').addEventListener('click', async () => {
+        const email = document.querySelector('[name=email]').value;
+        if (!email) { alert('请先输入邮箱'); return; }
+        const res = await fetch('/sendResetCode', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify({ email })
+        });
+        const json = await res.json();
+        alert(json.msg || json.error);
+    });
+
+    // 提交重置请求
+    document.getElementById('resetForm').addEventListener('submit', async e => {
+        e.preventDefault();
+        const data = {
+            email:       e.target.email.value,
+            code:        e.target.code.value,
+            newPassword: e.target.newPassword.value
+        };
+        const res = await fetch('/resetPassword', {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/json' },
+            body: JSON.stringify(data)
+        });
+        const json = await res.json();
+        alert(json.msg || json.error);
+        if (res.ok && json.code === 0) {
+            window.location.href = 'login.html';
+        }
+    });
+</script>
+</body>
+</html>
diff --git a/src/test/java/edu/bjtu/groupone/backend/UserServImplTest.java b/src/test/java/edu/bjtu/groupone/backend/UserServImplTest.java
new file mode 100644
index 0000000..beacf28
--- /dev/null
+++ b/src/test/java/edu/bjtu/groupone/backend/UserServImplTest.java
@@ -0,0 +1,124 @@
+package edu.bjtu.groupone.backend;
+
+import edu.bjtu.groupone.backend.mapper.UserMapper;
+import edu.bjtu.groupone.backend.utils.EmailUtil;
+import edu.bjtu.groupone.backend.service.impl.UserServImpl;
+import edu.bjtu.groupone.backend.model.User;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.lang.reflect.Field;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * 单元测试 UserServImpl 的核心逻辑:登录、发/验注册码、发/验重置码、重置密码。
+ */
+@ExtendWith(MockitoExtension.class)
+public class UserServImplTest {
+
+    @Mock
+    private UserMapper userMapper;
+
+    @Mock
+    private EmailUtil emailUtil;
+
+    @InjectMocks
+    private UserServImpl userService;
+
+    @BeforeEach
+    void clearCaches() throws Exception {
+        clearMap("emailCodes");
+        clearMap("resetCodes");
+    }
+
+    @Test
+    public void login_shouldReturnUser_whenCredentialsMatch() {
+        User in = new User();
+        in.setEmail("a@b.com");
+        in.setPassword("pwd");
+        User out = new User();
+        out.setUserId(1);
+        out.setUsername("alice");
+        out.setEmail("a@b.com");
+        when(userMapper.login(in)).thenReturn(out);
+
+        User result = userService.login(in);
+        assertThat(result).isSameAs(out);
+        verify(userMapper, times(1)).login(in);
+    }
+
+    @Test
+    public void sendVerification_and_verifyCode_workCorrectly() throws Exception {
+        String email = "test@x.com";
+        userService.sendVerificationCode(email);
+        verify(emailUtil).sendVerificationEmail(eq(email), anyString());
+
+        String stored = readMap("emailCodes", email);
+        String code = stored.split("\\|")[0];
+        assertThat(userService.verifyCode(email, code)).isTrue();
+        assertThat(userService.verifyCode(email, "000000")).isFalse();
+
+        // 模拟过期
+        patchExpiry("emailCodes", email, -1000L);
+        assertThat(userService.verifyCode(email, code)).isFalse();
+    }
+
+    @Test
+    public void sendResetCode_and_resetPassword_workCorrectly() throws Exception {
+        String email = "reset@x.com";
+        when(userMapper.selectByEmail(email)).thenReturn(null);
+        assertThatThrownBy(() -> userService.sendResetCode(email))
+                .isInstanceOf(IllegalArgumentException.class);
+
+        User u = new User();
+        u.setEmail(email);
+        when(userMapper.selectByEmail(email)).thenReturn(u);
+
+        userService.sendResetCode(email);
+        verify(emailUtil).sendVerificationEmail(eq(email), anyString());
+        String stored = readMap("resetCodes", email);
+        String code = stored.split("\\|")[0];
+
+        when(userMapper.updatePasswordByEmail(email, "newpwd")).thenReturn(1);
+        assertThat(userService.resetPassword(email, code, "newpwd")).isTrue();
+        assertThat(userService.resetPassword(email, code, "abc")).isFalse();
+
+        userService.sendResetCode(email);
+        String stored2 = readMap("resetCodes", email);
+        String code2 = stored2.split("\\|")[0];
+        patchExpiry("resetCodes", email, -2000L);
+        assertThat(userService.resetPassword(email, code2, "pw")).isFalse();
+    }
+
+    // --- 反射辅助方法 ---
+    @SuppressWarnings("unchecked")
+    private void clearMap(String fieldName) throws Exception {
+        Field f = UserServImpl.class.getDeclaredField(fieldName);
+        f.setAccessible(true);
+        ((java.util.Map<String, String>)f.get(userService)).clear();
+    }
+
+    @SuppressWarnings("unchecked")
+    private String readMap(String fieldName, String key) throws Exception {
+        Field f = UserServImpl.class.getDeclaredField(fieldName);
+        f.setAccessible(true);
+        return ((java.util.Map<String, String>)f.get(userService)).get(key);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void patchExpiry(String fieldName, String key, long offsetMs) throws Exception {
+        String combined = readMap(fieldName, key);
+        String code = combined.split("\\|")[0];
+        long expire = System.currentTimeMillis() + offsetMs;
+        String updated = code + "|" + expire;
+        Field f = UserServImpl.class.getDeclaredField(fieldName);
+        f.setAccessible(true);
+        ((java.util.Map<String, String>)f.get(userService)).put(key, updated);
+    }
+}