实现登录注册接口
Change-Id: I3d57cca89cac8945d562f6a39127b3454c1cd9ac
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"
new file mode 100644
index 0000000..ade70d0
--- /dev/null
+++ "b/API\346\216\245\345\217\243\346\226\207\346\241\243.txt"
@@ -0,0 +1,177 @@
+
+
+以下是当前项目所有后端接口及其说明,前端可以根据此文档完成联调。
+
+---
+
+## 通用返回结构
+
+所有接口均返回如下 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/LICENSE b/LICENSE
new file mode 100644
index 0000000..f63f5a9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,194 @@
+木兰宽松许可证,第2版
+
+木兰宽松许可证,第2版
+
+2020年1月 http://license.coscl.org.cn/MulanPSL2
+
+您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束:
+
+0. 定义
+
+“软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。
+
+“贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。
+
+“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。
+
+“法人实体” 是指提交贡献的机构及其“关联实体”。
+
+“关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是
+指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。
+
+1. 授予版权许可
+
+每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可
+以复制、使用、修改、分发其“贡献”,不论修改与否。
+
+2. 授予专利许可
+
+每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定
+撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡
+献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软
+件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“
+关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或
+其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权
+行动之日终止。
+
+3. 无商标许可
+
+“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定
+的声明义务而必须使用除外。
+
+4. 分发限制
+
+您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“
+本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。
+
+5. 免责声明与责任限制
+
+“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对
+任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于
+何种法律理论,即使其曾被建议有此种损失的可能性。
+
+6. 语言
+
+“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文
+版为准。
+
+条款结束
+
+如何将木兰宽松许可证,第2版,应用到您的软件
+
+如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步:
+
+1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字;
+
+2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中;
+
+3, 请将如下声明文本放入每个源文件的头部注释中。
+
+Copyright (c) [Year] [name of copyright holder]
+[Software Name] is licensed under Mulan PSL v2.
+You can use this software according to the terms and conditions of the Mulan
+PSL v2.
+You may obtain a copy of Mulan PSL v2 at:
+ http://license.coscl.org.cn/MulanPSL2
+THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
+KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
+NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+See the Mulan PSL v2 for more details.
+
+Mulan Permissive Software License,Version 2
+
+Mulan Permissive Software License,Version 2 (Mulan PSL v2)
+
+January 2020 http://license.coscl.org.cn/MulanPSL2
+
+Your reproduction, use, modification and distribution of the Software shall
+be subject to Mulan PSL v2 (this License) with the following terms and
+conditions:
+
+0. Definition
+
+Software means the program and related documents which are licensed under
+this License and comprise all Contribution(s).
+
+Contribution means the copyrightable work licensed by a particular
+Contributor under this License.
+
+Contributor means the Individual or Legal Entity who licenses its
+copyrightable work under this License.
+
+Legal Entity means the entity making a Contribution and all its
+Affiliates.
+
+Affiliates means entities that control, are controlled by, or are under
+common control with the acting entity under this License, ‘control’ means
+direct or indirect ownership of at least fifty percent (50%) of the voting
+power, capital or other securities of controlled or commonly controlled
+entity.
+
+1. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to you a perpetual, worldwide, royalty-free, non-exclusive,
+irrevocable copyright license to reproduce, use, modify, or distribute its
+Contribution, with modification or not.
+
+2. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to you a perpetual, worldwide, royalty-free, non-exclusive,
+irrevocable (except for revocation under this Section) patent license to
+make, have made, use, offer for sale, sell, import or otherwise transfer its
+Contribution, where such patent license is only limited to the patent claims
+owned or controlled by such Contributor now or in future which will be
+necessarily infringed by its Contribution alone, or by combination of the
+Contribution with the Software to which the Contribution was contributed.
+The patent license shall not apply to any modification of the Contribution,
+and any other combination which includes the Contribution. If you or your
+Affiliates directly or indirectly institute patent litigation (including a
+cross claim or counterclaim in a litigation) or other patent enforcement
+activities against any individual or entity by alleging that the Software or
+any Contribution in it infringes patents, then any patent license granted to
+you under this License for the Software shall terminate as of the date such
+litigation or activity is filed or taken.
+
+3. No Trademark License
+
+No trademark license is granted to use the trade names, trademarks, service
+marks, or product names of Contributor, except as required to fulfill notice
+requirements in section 4.
+
+4. Distribution Restriction
+
+You may distribute the Software in any medium with or without modification,
+whether in source or executable forms, provided that you provide recipients
+with a copy of this License and retain copyright, patent, trademark and
+disclaimer statements in the Software.
+
+5. Disclaimer of Warranty and Limitation of Liability
+
+THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY
+KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR
+COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT
+LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING
+FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO
+MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGES.
+
+6. Language
+
+THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION
+AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF
+DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION
+SHALL PREVAIL.
+
+END OF THE TERMS AND CONDITIONS
+
+How to Apply the Mulan Permissive Software License,Version 2
+(Mulan PSL v2) to Your Software
+
+To apply the Mulan PSL v2 to your work, for easy identification by
+recipients, you are suggested to complete following three steps:
+
+i. Fill in the blanks in following statement, including insert your software
+name, the year of the first publication of your software, and your name
+identified as the copyright owner;
+
+ii. Create a file named "LICENSE" which contains the whole context of this
+License in the first directory of your software package;
+
+iii. Attach the statement to the appropriate annotated syntax at the
+beginning of each source file.
+
+Copyright (c) [Year] [name of copyright holder]
+[Software Name] is licensed under Mulan PSL v2.
+You can use this software according to the terms and conditions of the Mulan
+PSL v2.
+You may obtain a copy of Mulan PSL v2 at:
+ http://license.coscl.org.cn/MulanPSL2
+THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
+KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
+NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+See the Mulan PSL v2 for more details.
diff --git a/pom.xml b/pom.xml
index 8ec67fa..17ed152 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,54 +1,179 @@
<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>3.4.4</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <groupId>com.example</groupId>
- <artifactId>backend</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <name>backend</name>
- <description>backend</description>
- <url/>
- <licenses>
- <license/>
- </licenses>
- <developers>
- <developer/>
- </developers>
- <scm>
- <connection/>
- <developerConnection/>
- <tag/>
- <url/>
- </scm>
- <properties>
- <java.version>17</java.version>
- </properties>
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter</artifactId>
- </dependency>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-parent</artifactId>
+ <version>3.4.5</version>
+ <relativePath /> <!-- lookup parent from repository -->
+ </parent>
+ <groupId>edu.bjtu.groupone</groupId>
+ <artifactId>backend</artifactId>
+ <version>0.0.1-SNAPSHOT</version>
+ <name>backend</name>
+ <description>backend</description>
+ <url />
+ <licenses>
+ <license />
+ </licenses>
+ <developers>
+ <developer />
+ </developers>
+ <scm>
+ <connection />
+ <developerConnection />
+ <tag />
+ <url />
+ </scm>
+ <properties>
+ <java.version>17</java.version>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
+ <dependency>
+ <groupId>com.h2database</groupId>
+ <artifactId>h2</artifactId>
+ <scope>test</scope>
+ </dependency>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
+ <dependency>
+ <groupId>org.mybatis.spring.boot</groupId>
+ <artifactId>mybatis-spring-boot-starter</artifactId>
+ <version>2.3.0</version>
+ </dependency>
-</project>
+ <dependency>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>com.mysql</groupId>
+ <artifactId>mysql-connector-j</artifactId>
+ <scope>runtime</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>io.springfox</groupId>
+ <artifactId>springfox-boot-starter</artifactId>
+ <version>3.0.0</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.volcengine</groupId>
+ <artifactId>volcengine-java-sdk-ark-runtime</artifactId>
+ <version>LATEST</version>
+ </dependency>
+ <dependency>
+ <groupId>com.aliyun.oss</groupId>
+ <artifactId>aliyun-sdk-oss</artifactId>
+ <version>3.17.4</version>
+ </dependency>
+ <dependency>
+ <groupId>io.jsonwebtoken</groupId>
+ <artifactId>jjwt</artifactId>
+ <version>0.9.1</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.theokanning.openai-gpt3-java</groupId>
+ <artifactId>api</artifactId>
+ <version>0.9.0</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mybatis.spring.boot</groupId>
+ <artifactId>mybatis-spring-boot-starter</artifactId>
+ <version>3.0.3</version>
+ </dependency>
+ <dependency>
+ <groupId>com.theokanning.openai-gpt3-java</groupId>
+ <artifactId>client</artifactId>
+ <version>0.9.0</version>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-test</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-mail</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.sun.mail</groupId>
+ <artifactId>jakarta.mail</artifactId>
+ <version>2.0.1</version>
+ </dependency>
+
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.11.0</version>
+ <configuration>
+ <source>17</source> <!-- 保持与Java版本一致 -->
+ <target>17</target>
+ <compilerArgs>
+ <arg>-parameters</arg> <!-- 保留方法参数名 -->
+ </compilerArgs>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ <version>1.18.30</version>
+ </path>
+ </annotationProcessorPaths>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-maven-plugin</artifactId>
+ <version>3.1.5</version>
+ <configuration>
+ <excludes>
+ <exclude>
+ <groupId>org.projectlombok</groupId>
+ <artifactId>lombok</artifactId>
+ </exclude>
+ </excludes>
+ <executable>true</executable><!-- 支持可执行JAR -->
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>repackage</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
\ No newline at end of file
diff --git a/src/main/java/edu/bjtu/groupone/backend/BackendApplication.java b/src/main/java/edu/bjtu/groupone/backend/BackendApplication.java
index 32033e0..1cc2086 100644
--- a/src/main/java/edu/bjtu/groupone/backend/BackendApplication.java
+++ b/src/main/java/edu/bjtu/groupone/backend/BackendApplication.java
@@ -1,13 +1,19 @@
package edu.bjtu.groupone.backend;
+import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.web.servlet.ServletComponentScan;
+import org.springframework.scheduling.annotation.EnableScheduling;
+@EnableScheduling
+@ServletComponentScan
@SpringBootApplication
+@MapperScan("edu.bjtu.groupone.backend.mapper")
public class BackendApplication {
- public static void main(String[] args) {
- SpringApplication.run(BackendApplication.class, args);
- }
+ public static void main(String[] args) {
+ SpringApplication.run(BackendApplication.class, args);
+ }
}
diff --git a/src/main/java/edu/bjtu/groupone/backend/config/Config.java b/src/main/java/edu/bjtu/groupone/backend/config/Config.java
new file mode 100644
index 0000000..cfc3745
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/config/Config.java
@@ -0,0 +1,34 @@
+// src/main/java/edu/bjtu/groupone/backend/config/Config.java
+package edu.bjtu.groupone.backend.config;
+
+import edu.bjtu.groupone.backend.interceptor.Interceptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.*;
+
+@Configuration
+public class Config implements WebMvcConfigurer {
+
+ @Autowired
+ private Interceptor authInterceptor;
+
+ /** 全局 CORS 配置 */
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/**")
+ // Vite 默认端口 5173,如果你的 React 在 3000 也一并写上
+ .allowedOrigins("http://localhost:5173", "http://localhost:3000")
+ .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
+ .allowedHeaders("*")
+ // 如果需要前端带 Cookie(如 JWT 存在 Cookie)就打开
+ .allowCredentials(true)
+ .maxAge(3600);
+ }
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry) {
+ registry.addInterceptor(authInterceptor)
+ .addPathPatterns("/**");
+ }
+ // 其它 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
new file mode 100644
index 0000000..042c278
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/config/SwaggerConfig.java
@@ -0,0 +1,39 @@
+package edu.bjtu.groupone.backend.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+
+@Configuration
+//@EnableOpenApi
+public class SwaggerConfig {
+ @Bean
+ public Docket docket(){
+ return new Docket(DocumentationType.OAS_30)
+ .apiInfo(apiInfo())
+ .enable(true)
+ .groupName("CJB")
+ .select()
+ .apis(RequestHandlerSelectors.basePackage("com.example.eliteedu_prism.controller"))
+ .paths(PathSelectors.ant("/controller/**"))
+ .build();
+ }
+
+
+ @SuppressWarnings("all")
+ public ApiInfo apiInfo(){
+ return new ApiInfo(
+ "zrj's api",
+ "redis project",
+ "v1.0",
+ "2261839618@qq.com", //开发者团队的邮箱
+ "ZRJ",
+ "Apache 2.0", //许可证
+ "http://www.apache.org/licenses/LICENSE-2.0" //许可证链接
+ );
+ }
+}
\ 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
new file mode 100644
index 0000000..1388940
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/controller/UserController.java
@@ -0,0 +1,113 @@
+package edu.bjtu.groupone.backend.controller;
+
+import edu.bjtu.groupone.backend.model.InviteCode;
+import edu.bjtu.groupone.backend.model.Result;
+import edu.bjtu.groupone.backend.model.User;
+import edu.bjtu.groupone.backend.service.InviteCodeService;
+import edu.bjtu.groupone.backend.service.UserService;
+import edu.bjtu.groupone.backend.utils.JwtUtils;
+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
+@RestController
+public class UserController {
+
+ @Autowired
+ private UserService userService;
+ @Autowired
+ private InviteCodeService inviteCodeService;
+
+ /** 登录:接收表单字段 identificationNumber、password */
+ @PostMapping("/login")
+ public Result login(
+ @RequestParam Integer identificationNumber,
+ @RequestParam String password
+ ) {
+ User u = new User();
+ u.setIdentificationNumber(identificationNumber);
+ 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());
+ String token = JwtUtils.generateJwt(payload);
+
+ // 打印到控制台
+ log.info("【登录成功】生成的 JWT token = {}", token);
+ return Result.success(token);
+ }
+ return Result.error("用户名或密码错误");
+ }
+
+ /** 注册:接收表单字段 username、email、password、verificationCode、identificationNumber、inviteCode */
+ @PostMapping("/register")
+ public Result register(
+ @RequestParam String username,
+ @RequestParam String email,
+ @RequestParam String password,
+ @RequestParam String verificationCode,
+ @RequestParam Integer identificationNumber,
+ @RequestParam String inviteCode
+ ) {
+ // 1. 验证邀请码
+ if (inviteCodeService.validateInviteCode(inviteCode) == null) {
+ return Result.error("无效邀请码或已过期");
+ }
+ // 2. 验证邮箱验证码
+ if (!userService.verifyCode(email, verificationCode)) {
+ return Result.error("验证码错误或已过期");
+ }
+ // 3. 检查邮箱唯一
+ if (userService.isEmailExists(email)) {
+ return Result.error("邮箱已被注册");
+ }
+ // 4. 构造用户实体并设置所有非空字段
+ 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. 插入到数据库
+ userService.register(newUser);
+ // 6. 扣减邀请码次数
+ if (!inviteCodeService.useInviteCode(inviteCode)) {
+ log.error("邀请码扣减失败, code={}", inviteCode);
+ }
+ return Result.success("注册成功");
+ }
+
+ /** 发送邮箱验证码:接收表单字段 email、inviteCode */
+ @PostMapping("/sendVerification")
+ public Result sendVerification(
+ @RequestParam String email,
+ @RequestParam String inviteCode
+ ) {
+ if (inviteCodeService.validateInviteCode(inviteCode) == null) {
+ return Result.error("无效邀请码");
+ }
+ userService.sendVerificationCode(email);
+ return Result.success("验证码已发送");
+ }
+
+ @GetMapping("/api/me")
+ public Result me(@RequestHeader("token") String token) {
+ // 从 token 解析出用户信息,JwtUtils.parseJwt 返回一个 Map
+ var claims = JwtUtils.parseJwt(token);
+ // 你可以根据需要返回完整 User,也可以直接返回用户名
+ return Result.success(claims);
+ }
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java b/src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java
new file mode 100644
index 0000000..f3b9d2d
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java
@@ -0,0 +1,58 @@
+// src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java
+package edu.bjtu.groupone.backend.interceptor;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import edu.bjtu.groupone.backend.model.Result;
+import edu.bjtu.groupone.backend.utils.JwtUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+@Slf4j
+@Component
+public class Interceptor implements HandlerInterceptor {
+ @Override
+ public boolean preHandle(HttpServletRequest req,
+ HttpServletResponse resp,
+ Object handler) throws Exception {
+ String url = req.getRequestURI();
+ log.info("Request URI: {}", url);
+ // 放行无需鉴权的路径:登录、注册、发码、静态资源、HTML 页面
+ if (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("/favicon.ico") ||
+ url.startsWith("/static/")) {
+ return true;
+ }
+ // 从请求头读取 token
+ String token = req.getHeader("token");
+ if (!StringUtils.hasLength(token)) {
+ resp.setContentType("application/json;charset=UTF-8");
+ resp.getWriter()
+ .write(new ObjectMapper()
+ .writeValueAsString(Result.error("NOT_LOGIN")));
+ return false;
+ }
+ try {
+ JwtUtils.parseJwt(token);
+ return true;
+ } catch (Exception e) {
+ resp.setContentType("application/json;charset=UTF-8");
+ resp.getWriter()
+ .write(new ObjectMapper()
+ .writeValueAsString(Result.error("NOT_LOGIN")));
+ return false;
+ }
+ }
+
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/mapper/InviteCodeMapper.java b/src/main/java/edu/bjtu/groupone/backend/mapper/InviteCodeMapper.java
new file mode 100644
index 0000000..78bb7e0
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/mapper/InviteCodeMapper.java
@@ -0,0 +1,14 @@
+package edu.bjtu.groupone.backend.mapper;
+
+import edu.bjtu.groupone.backend.model.InviteCode;
+import org.apache.ibatis.annotations.*;
+
+@Mapper
+public interface InviteCodeMapper {
+ @Select("SELECT * FROM invite_code WHERE code = #{code}")
+ InviteCode selectByCode(String code);
+
+ @Update("UPDATE invite_code SET remaining_uses = remaining_uses - 1 " +
+ "WHERE code = #{code} AND remaining_uses > 0")
+ int decrementRemainingUses(String code);
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java b/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java
new file mode 100644
index 0000000..3cbf2c1
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java
@@ -0,0 +1,22 @@
+package edu.bjtu.groupone.backend.mapper;
+
+
+import edu.bjtu.groupone.backend.model.User;
+import org.apache.ibatis.annotations.*;
+
+@Mapper
+public interface UserMapper {
+ @Select("select * from user where identification_number=#{identificationNumber} and password=#{password}")
+ User login(User user);
+
+ @Select("SELECT * FROM user WHERE username = #{username}")
+ User selectByUsername(String username);
+
+ @Select("SELECT * FROM user WHERE email = #{email}")
+ User selectByEmail(String email);
+
+ @Insert("INSERT INTO user(username, email, password, registration_date, identification_number) " +
+ "VALUES(#{username}, #{email}, #{password}, #{registrationDate}, #{identificationNumber})")
+ @Options(useGeneratedKeys = true, keyProperty = "userId")
+ void insertUser(User user);
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/model/InviteCode.java b/src/main/java/edu/bjtu/groupone/backend/model/InviteCode.java
new file mode 100644
index 0000000..f1019f9
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/model/InviteCode.java
@@ -0,0 +1,18 @@
+package edu.bjtu.groupone.backend.model;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import java.time.LocalDateTime;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class InviteCode {
+ private String code;
+ private int maxUses;
+ private int remainingUses;
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime expiryTime; //失效时间
+ private int createdBy; //创建者的id
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/model/Result.java b/src/main/java/edu/bjtu/groupone/backend/model/Result.java
new file mode 100644
index 0000000..f3d44ec
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/model/Result.java
@@ -0,0 +1,27 @@
+package edu.bjtu.groupone.backend.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class Result {
+ private Integer code;//0表示成功,1表示失败
+ private String msg;//提示信息
+ private Object data;//要返回的数据
+ public static Result success() {//增删改响应成功
+
+ return new Result(0, "success", null);
+ }
+ public static Result success(Object data) {//增删改响应成功
+
+ return new Result(0, "success", data);
+ }
+ public static Result error(String msg) {//增删改响应成功
+
+ return new Result(1, msg, null);
+ }
+
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/model/User.java b/src/main/java/edu/bjtu/groupone/backend/model/User.java
new file mode 100644
index 0000000..95b2f00
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/model/User.java
@@ -0,0 +1,23 @@
+package edu.bjtu.groupone.backend.model;
+
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class User {
+ private int userId;
+ private String username;
+ private String email;
+ private String password;
+ private String address;
+ private String role;
+ private String profilePic;
+ private String registrationDate;
+ private int identificationNumber;
+ private String avatar;// 头像
+ private boolean isfollowed = false;
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/model/UserDTO.java b/src/main/java/edu/bjtu/groupone/backend/model/UserDTO.java
new file mode 100644
index 0000000..279ac26
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/model/UserDTO.java
@@ -0,0 +1,16 @@
+package edu.bjtu.groupone.backend.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class UserDTO {
+ private User user; // 原来的 User 对象
+ private boolean isFollowed; // 是否关注的状态
+
+ // 构造函数
+
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/service/InviteCodeService.java b/src/main/java/edu/bjtu/groupone/backend/service/InviteCodeService.java
new file mode 100644
index 0000000..9391e14
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/service/InviteCodeService.java
@@ -0,0 +1,8 @@
+package edu.bjtu.groupone.backend.service;
+
+import edu.bjtu.groupone.backend.model.InviteCode;
+
+public interface InviteCodeService {
+ InviteCode validateInviteCode(String code);
+ boolean useInviteCode(String code);
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/service/UserService.java b/src/main/java/edu/bjtu/groupone/backend/service/UserService.java
new file mode 100644
index 0000000..662dafe
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/service/UserService.java
@@ -0,0 +1,15 @@
+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);
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/service/impl/InviteCodeServiceImpl.java b/src/main/java/edu/bjtu/groupone/backend/service/impl/InviteCodeServiceImpl.java
new file mode 100644
index 0000000..b08b7a5
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/service/impl/InviteCodeServiceImpl.java
@@ -0,0 +1,45 @@
+package edu.bjtu.groupone.backend.service.impl;
+
+import edu.bjtu.groupone.backend.mapper.InviteCodeMapper;
+import edu.bjtu.groupone.backend.model.InviteCode;
+import edu.bjtu.groupone.backend.service.InviteCodeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+@Service
+public class InviteCodeServiceImpl implements InviteCodeService {
+
+ @Autowired
+ private InviteCodeMapper inviteCodeMapper;
+
+ /**
+ * 验证邀请码是否存在、未过期且剩余次数大于 0
+ */
+ @Override
+ public InviteCode validateInviteCode(String code) {
+ InviteCode inviteCode = inviteCodeMapper.selectByCode(code);
+ if (inviteCode == null
+ || inviteCode.getRemainingUses() <= 0
+ || inviteCode.getExpiryTime().isBefore(LocalDateTime.now())) {
+ return null;
+ }
+ return inviteCode;
+ }
+
+ /**
+ * 使用一次邀请码:先验证再扣减剩余次数
+ */
+ @Override
+ public boolean useInviteCode(String code) {
+ // 先调用 validateInviteCode 保证业务合理性
+ InviteCode valid = validateInviteCode(code);
+ if (valid == null) {
+ return false;
+ }
+ // 再执行扣减
+ int updated = inviteCodeMapper.decrementRemainingUses(code);
+ return updated > 0;
+ }
+}
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
new file mode 100644
index 0000000..828b78d
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/service/impl/UserServImpl.java
@@ -0,0 +1,91 @@
+package edu.bjtu.groupone.backend.service.impl;
+
+import edu.bjtu.groupone.backend.mapper.UserMapper;
+import edu.bjtu.groupone.backend.model.User;
+import edu.bjtu.groupone.backend.service.UserService;
+import edu.bjtu.groupone.backend.utils.EmailUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Random;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Service
+public class UserServImpl implements UserService {
+
+ @Autowired
+ private UserMapper userMapper;
+ @Autowired
+ private EmailUtil emailUtil;
+ private final ConcurrentHashMap<String, String> emailCodes = new ConcurrentHashMap<>();
+ private static final long CODE_EXPIRE_MINUTES = 5;
+
+ @Override
+ public User login(User user) {
+ return userMapper.login(user);
+ }
+
+ @Override
+ public boolean isUsernameExists(String username) {
+ return userMapper.selectByUsername(username) != null;
+ }
+
+ @Override
+ public boolean isEmailExists(String email) {
+ return userMapper.selectByEmail(email) != null;
+ }
+
+ @Override
+ public void register(User user) {
+ // 如果前端未提供用户名,可生成默认
+ String username = user.getUsername();
+ if (username == null || username.isBlank()) {
+ do {
+ username = generateDefaultUsername(user.getEmail());
+ } while (isUsernameExists(username));
+ user.setUsername(username);
+ }
+ // 设置注册时间
+ user.setRegistrationDate(
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
+ );
+ // 插入用户记录
+ userMapper.insertUser(user);
+ }
+
+ @Override
+ public void sendVerificationCode(String email) {
+ String code = generateRandomCode();
+ emailUtil.sendVerificationEmail(email, code);
+ long expireAt = System.currentTimeMillis() + CODE_EXPIRE_MINUTES * 60 * 1000;
+ emailCodes.put(email, code + "|" + expireAt);
+ }
+
+ @Override
+ public boolean verifyCode(String email, String inputCode) {
+ String stored = emailCodes.get(email);
+ if (stored == null) {
+ return false;
+ }
+ String[] parts = stored.split("\\|");
+ if (parts.length != 2) {
+ return false;
+ }
+ String code = parts[0];
+ long expireTime = Long.parseLong(parts[1]);
+ return System.currentTimeMillis() <= expireTime && code.equals(inputCode);
+ }
+
+ private String generateRandomCode() {
+ return String.format("%06d", new Random().nextInt(1_000000));
+ }
+
+ private String generateDefaultUsername(String email) {
+ String prefix = email != null && email.contains("@")
+ ? email.substring(0, email.indexOf("@"))
+ : "user";
+ return "user_" + prefix + "_" + String.format("%04d", new Random().nextInt(10000));
+ }
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/utils/AiUtils.java b/src/main/java/edu/bjtu/groupone/backend/utils/AiUtils.java
new file mode 100644
index 0000000..8dd8a8b
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/utils/AiUtils.java
@@ -0,0 +1,55 @@
+// ChatCompletionsService.java
+package edu.bjtu.groupone.backend.utils;
+
+import com.volcengine.ark.runtime.model.completion.chat.ChatCompletionRequest;
+import com.volcengine.ark.runtime.model.completion.chat.ChatMessage;
+import com.volcengine.ark.runtime.model.completion.chat.ChatMessageRole;
+import com.volcengine.ark.runtime.service.ArkService;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+public class AiUtils {
+
+ private ArkService service;
+
+ public void ChatCompletionsService(String apiKey) {
+ this.service = new ArkService(apiKey);
+ }
+
+ public String getAIResponse(String userInput) {
+ List<ChatMessage> messages = new ArrayList<>();
+ messages.add(ChatMessage.builder()
+ .role(ChatMessageRole.SYSTEM)
+ .content("你是豆包,是由字节跳动开发的 AI 人工智能助手")
+ .build());
+
+ messages.add(ChatMessage.builder()
+ .role(ChatMessageRole.USER)
+ .content(userInput)
+ .build());
+
+ ChatCompletionRequest request = ChatCompletionRequest.builder()
+ .model("ep-20241106142047-5g46t") // 使用指定的模型
+ .messages(messages)
+ .build();
+
+ final StringBuilder aiResponse = new StringBuilder();
+
+ service.streamChatCompletion(request)
+ .doOnError(Throwable::printStackTrace)
+ .blockingForEach(choice -> {
+ if (choice.getChoices().size() > 0) {
+ aiResponse.append(choice.getChoices().get(0).getMessage().getContent());
+ }
+ });
+
+ return aiResponse.toString();
+ }
+
+ public void shutdown() {
+ service.shutdownExecutor();
+ }
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/utils/AliOSSUtils.java b/src/main/java/edu/bjtu/groupone/backend/utils/AliOSSUtils.java
new file mode 100644
index 0000000..bedda4f
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/utils/AliOSSUtils.java
@@ -0,0 +1,45 @@
+package edu.bjtu.groupone.backend.utils;
+
+import com.aliyun.oss.OSS;
+import com.aliyun.oss.OSSClientBuilder;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.UUID;
+
+/**
+ * 阿里云 OSS 工具类
+ */
+@Component
+public class AliOSSUtils {
+
+ private String endpoint = "https://oss-cn-beijing.aliyuncs.com";
+ private String accessKeyId = "LTAI5tQjb3fy8hTxxRGoS9b3";
+ private String accessKeySecret = "luBkGCqe3mqfYuiSOawC0KgPCi1yI4";
+ private String bucketName = "avatar-wzh";
+
+ /**
+ * 实现上传图片到OSS
+ */
+ public String upload(MultipartFile file) throws IOException {
+ // 获取上传的文件的输入流
+ InputStream inputStream = file.getInputStream();
+
+ // 避免文件覆盖
+ String originalFilename = file.getOriginalFilename();
+ String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));
+
+ //上传文件到 OSS
+ OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
+ ossClient.putObject(bucketName, fileName, inputStream);
+
+ //文件访问路径
+ String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;
+ // 关闭ossClient
+ ossClient.shutdown();
+ return url;// 把上传到oss的路径返回
+ }
+
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/utils/EmailUtil.java b/src/main/java/edu/bjtu/groupone/backend/utils/EmailUtil.java
new file mode 100644
index 0000000..3ff7822
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/utils/EmailUtil.java
@@ -0,0 +1,39 @@
+package edu.bjtu.groupone.backend.utils;
+
+import jakarta.mail.MessagingException;
+import jakarta.mail.internet.MimeMessage;
+import lombok.extern.slf4j.Slf4j;
+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.Component;
+
+@Component
+@Slf4j
+public class EmailUtil {
+ @Autowired
+ private JavaMailSender mailSender;
+
+ @Value("${spring.mail.username}") // 确保从配置读取
+ private String from;
+
+ public void sendVerificationEmail(String to, String code) {
+ try {
+ MimeMessage message = mailSender.createMimeMessage();
+ MimeMessageHelper helper = new MimeMessageHelper(message);
+
+ // 关键修改:使用配置的授权邮箱作为发件人
+ helper.setFrom(from);
+ helper.setTo(to);
+ helper.setSubject("[PT SITE] 验证码通知");
+ helper.setText("您的验证码是:" + code + ",5分钟内有效");
+
+ mailSender.send(message);
+ log.info("邮件发送成功:{}", to);
+ } catch (MessagingException e) {
+ log.error("邮件发送失败", e);
+ throw new RuntimeException("邮件服务异常");
+ }
+ }
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/utils/GetTokenUserId.java b/src/main/java/edu/bjtu/groupone/backend/utils/GetTokenUserId.java
new file mode 100644
index 0000000..b798f05
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/utils/GetTokenUserId.java
@@ -0,0 +1,18 @@
+package edu.bjtu.groupone.backend.utils;
+
+import io.jsonwebtoken.Claims;
+import jakarta.servlet.http.HttpServletRequest;
+
+public class GetTokenUserId {
+
+ public static String getUserId(HttpServletRequest request) {
+ String token = request.getHeader("Authorization");
+ if (token == null || !token.startsWith("Bearer ")) {
+ return null;
+ }
+ // 解析 JWT Token,获取用户 ID
+ String jwt = token.substring(7); // 去掉 'Bearer ' 前缀
+ Claims claims = JwtUtils.parseJwt(jwt); // 从 JWT 中获取用户 ID
+ return claims.get("id").toString();
+ }
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/utils/JwtUtils.java b/src/main/java/edu/bjtu/groupone/backend/utils/JwtUtils.java
new file mode 100644
index 0000000..65e917a
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/utils/JwtUtils.java
@@ -0,0 +1,34 @@
+package edu.bjtu.groupone.backend.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+
+import java.util.Date;
+import java.util.Map;
+
+public class JwtUtils {
+
+ // 生成JWT// 生成JWT
+ public static String generateJwt(Map<String,Object> claims) {
+
+ String jwt = Jwts.builder().
+ addClaims(claims)
+ .signWith(io.jsonwebtoken.SignatureAlgorithm.HS256, "Bjtu")
+ .setExpiration(new Date(System.currentTimeMillis() + 43200000L))
+ .compact();
+
+
+ return jwt;
+
+
+ }
+
+ public static Claims parseJwt(String jwt) {
+
+ return Jwts.parser()
+ .setSigningKey("Bjtu")
+ .parseClaimsJws(jwt)
+ .getBody();
+ }
+
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/utils/OpenAiUtil.java b/src/main/java/edu/bjtu/groupone/backend/utils/OpenAiUtil.java
new file mode 100644
index 0000000..1dbf943
--- /dev/null
+++ b/src/main/java/edu/bjtu/groupone/backend/utils/OpenAiUtil.java
@@ -0,0 +1,35 @@
+package edu.bjtu.groupone.backend.utils;
+
+import com.theokanning.openai.OpenAiService;
+import com.theokanning.openai.completion.CompletionChoice;
+import com.theokanning.openai.completion.CompletionRequest;
+import jakarta.annotation.PostConstruct;
+import org.springframework.beans.factory.annotation.Value;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.List;
+
+
+public class OpenAiUtil {
+ @Value("${openai.secret_key}")
+ private String token;
+
+ private OpenAiService service;
+
+ @PostConstruct
+ public void init(){
+ service= new OpenAiService(token, Duration.ofSeconds(60L));
+ }
+ public List<CompletionChoice> sendComplete(String prompt) {
+ CompletionRequest completionRequest = CompletionRequest.builder()
+ .model("text-davinci-003") //采用最强模型,达芬奇模型3
+ .maxTokens(1500)
+ .prompt(prompt)
+ .user("testing")
+ .logitBias(new HashMap<>())
+ .build();
+
+ return service.createCompletion(completionRequest).getChoices();
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 3ca17a4..db34ebe 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,19 @@
-spring.application.name=backend
+# ??MySQL??
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+spring.datasource.url=jdbc:mysql://localhost:3306/groupone_db\
+?useSSL=false\
+&serverTimezone=Asia/Shanghai\
+&characterEncoding=utf8\
+&allowPublicKeyRetrieval=true
+
+spring.datasource.username=root
+spring.datasource.password=Rfw@2935
+
+# ???????????
+spring.datasource.hikari.maximum-pool-size=10
+spring.datasource.hikari.minimum-idle=5
+spring.datasource.hikari.idle-timeout=600000
+spring.datasource.hikari.connection-timeout=30000
+
+# ???? MyBatis ?????
+mybatis.configuration.map-underscore-to-camel-case=true
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..6cf60f3
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,60 @@
+spring:
+ application:
+ name: PtSite
+ mail:
+ host: smtp.qq.com
+ port: 465
+ username: 583398241@qq.com
+ password: feojrgbupvmxbbgi
+ protocol: smtps
+ properties:
+ mail.smtp.auth: true
+ mail.smtp.ssl.enable: true
+ mail.smtp.ssl.trust: smtp.qq.com
+ mail.smtp.socketFactory.class: javax.net.ssl.SSLSocketFactory
+
+ datasource:
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ url: jdbc:mysql://localhost:3306/groupone_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
+ username: root
+ password: "Rfw@2935"
+ hikari:
+ maximum-pool-size: 10
+ minimum-idle: 5
+ idle-timeout: 600000
+ connection-timeout: 30000
+
+ elasticsearch:
+ rest:
+ uris: http://192.168.74.129:9200
+
+ servlet:
+ multipart:
+ enabled: true
+ max-file-size: 1024MB
+ max-request-size: 2048MB
+
+ # Redis配置(当前已注释)
+ # data:
+ # redis:
+ # host: 192.168.74.129
+ # port: 6379
+ # database: 0
+ # lettuce:
+ # pool:
+ # max-active: 8
+ # max-idle: 8
+ # min-idle: 0
+
+mybatis:
+ configuration:
+ log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+ map-underscore-to-camel-case: true
+
+server:
+ port: 8080
+
+ark:
+ api:
+ key: 5c648105-9e49-4a78-a1b2-ee8fcc7c02ad
+
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
new file mode 100644
index 0000000..8bb0c85
--- /dev/null
+++ b/src/main/resources/schema.sql
@@ -0,0 +1,45 @@
+CREATE TABLE `user` (
+ `user_id` INT PRIMARY KEY AUTO_INCREMENT,
+ `username` VARCHAR(255) NOT NULL UNIQUE,
+ `email` VARCHAR(255) NOT NULL UNIQUE,
+ `password` VARCHAR(255) NOT NULL,
+ `address` VARCHAR(255),
+ `role` VARCHAR(50) NOT NULL DEFAULT 'user',
+ `profile_pic` VARCHAR(255),
+ `registration_date` DATETIME NOT NULL, -- 推荐用时间类型(而非字符串)
+ `identification_number` VARCHAR(18), -- 身份证号建议用 VARCHAR(18)
+ `avatar` VARCHAR(255),
+ `isfollowed` BOOLEAN NOT NULL DEFAULT FALSE,
+ INDEX `idx_user_id` (`user_id`) -- 可选:主键通常自带索引
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+CREATE TABLE invite_code (
+ code VARCHAR(20) PRIMARY KEY,
+ max_uses INT NOT NULL,
+ remaining_uses INT NOT NULL,
+ expiry_time DATETIME NOT NULL,
+ created_by INT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (created_by) REFERENCES user(user_id)
+);
+
+CREATE TABLE user_follow (
+ follower_id INT NOT NULL,
+ followed_id INT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (follower_id, followed_id),
+ FOREIGN KEY (follower_id) REFERENCES user(user_id),
+ FOREIGN KEY (followed_id) REFERENCES user(user_id)
+);
+
+INSERT INTO `user` (
+ username, email, password, registration_date, identification_number, role
+) VALUES (
+ 'admin', 'admin@example.com', 'admin123', NOW(), 87654321, 'admin'
+ );
+
+INSERT INTO invite_code (
+ code, max_uses, remaining_uses, expiry_time, created_by
+) VALUES
+ ('WELCOME2025', 5, 5, '2026-01-01 00:00:00', 1),
+ ('INVITEONLY', 3, 3, '2025-12-31 23:59:59', 1);
\ No newline at end of file
diff --git a/src/main/resources/static/home.html b/src/main/resources/static/home.html
new file mode 100644
index 0000000..635856a
--- /dev/null
+++ b/src/main/resources/static/home.html
@@ -0,0 +1,24 @@
+<!-- src/main/resources/static/home.html -->
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head><meta charset="UTF-8"><title>首页</title></head>
+<body>
+<h1>欢迎访问受保护页面</h1>
+<div id="userInfo"></div>
+<script>
+ (async ()=>{
+ const token = localStorage.getItem('token');
+ if (!token) return window.location.href='login.html';
+ 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;
+ })();
+</script>
+</body>
+</html>
diff --git a/src/main/resources/static/login.html b/src/main/resources/static/login.html
new file mode 100644
index 0000000..993941d
--- /dev/null
+++ b/src/main/resources/static/login.html
@@ -0,0 +1,38 @@
+<!-- src/main/resources/static/login.html -->
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+ <meta charset="UTF-8">
+ <title>登录</title>
+</head>
+<body>
+<h2>用户登录</h2>
+<form id="loginForm">
+ <label>身份证号:</label>
+ <input type="number" name="identificationNumber" required>
+ <label>密码:</label>
+ <input type="password" name="password" required>
+ <button type="submit">登录</button>
+</form>
+<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);
+ }
+ });
+</script>
+</body>
+</html>
diff --git a/src/main/resources/static/register.html b/src/main/resources/static/register.html
new file mode 100644
index 0000000..1e43e90
--- /dev/null
+++ b/src/main/resources/static/register.html
@@ -0,0 +1,54 @@
+<!-- src/main/resources/static/register.html -->
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+ <meta charset="UTF-8">
+ <title>注册</title>
+</head>
+<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>
+ <label>邀请码:</label>
+ <input type="text" name="inviteCode" required>
+ <button type="submit">注册</button>
+</form>
+<script>
+ document.getElementById('sendCode')
+ .addEventListener('click', async () => {
+ const form = new URLSearchParams();
+ form.set('email', document.querySelector('[name=email]').value);
+ form.set('inviteCode', document.querySelector('[name=inviteCode]').value);
+ const res = await fetch('/sendVerification', {
+ method: 'POST',
+ body: form
+ });
+ alert((await res.json()).msg);
+ });
+ 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';
+ }
+ });
+</script>
+</body>
+</html>
diff --git a/src/test/java/edu/bjtu/groupone/backend/UserControllerTest.java b/src/test/java/edu/bjtu/groupone/backend/UserControllerTest.java
new file mode 100644
index 0000000..7e2f691
--- /dev/null
+++ b/src/test/java/edu/bjtu/groupone/backend/UserControllerTest.java
@@ -0,0 +1,82 @@
+package edu.bjtu.groupone.backend;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import java.time.LocalDateTime;
+
+import ch.qos.logback.classic.Logger;
+import org.slf4j.LoggerFactory;
+import edu.bjtu.groupone.backend.mapper.UserMapper;
+import edu.bjtu.groupone.backend.mapper.InviteCodeMapper;
+import edu.bjtu.groupone.backend.model.User;
+import edu.bjtu.groupone.backend.model.InviteCode;
+import edu.bjtu.groupone.backend.service.impl.UserServImpl;
+import edu.bjtu.groupone.backend.service.impl.InviteCodeServiceImpl;
+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;
+
+@ExtendWith(MockitoExtension.class)
+public class UserControllerTest {
+
+ // 初始化 Logger,避免空指针
+ private static final Logger log = (Logger) LoggerFactory.getLogger(UserControllerTest.class);
+
+ @Mock
+ private UserMapper userMapper;
+
+ @Mock
+ private InviteCodeMapper inviteCodeMapper;
+
+ @InjectMocks
+ private UserServImpl userService;
+
+ @InjectMocks
+ private InviteCodeServiceImpl inviteCodeService;
+
+ @Test
+ public void testLogin() {
+ User loginUser = new User();
+ loginUser.setIdentificationNumber(123456);
+ loginUser.setPassword("password123");
+ User expected = new User();
+ expected.setUsername("expectedUsername");
+
+ // 模拟 mapper 返回
+ when(userMapper.login(loginUser)).thenReturn(expected);
+
+ // 调用 service 并断言
+ User result = userService.login(loginUser);
+ assertThat(result).isNotNull();
+ assertThat(result.getUsername()).isEqualTo("expectedUsername");
+ verify(userMapper, times(1)).login(loginUser);
+ }
+
+ @Test
+ public void testUseInviteCode() {
+ String inviteCode = "TESTCODE";
+
+ // 构造一个有效的 InviteCode,注意设置 expiryTime
+ InviteCode dummy = new InviteCode();
+ dummy.setCode(inviteCode);
+ dummy.setRemainingUses(1);
+ // 设置有效期在未来,避免调用 expiryTime.isBefore(...)
+ dummy.setExpiryTime(LocalDateTime.now().plusDays(1));
+
+ // Stub 两个 Mapper 方法
+ when(inviteCodeMapper.selectByCode(inviteCode)).thenReturn(dummy);
+ when(inviteCodeMapper.decrementRemainingUses(inviteCode)).thenReturn(1);
+
+ // 调用 service
+ boolean used = inviteCodeService.useInviteCode(inviteCode);
+
+ // 断言和验证
+ assertThat(used).isTrue();
+ verify(inviteCodeMapper, times(1)).selectByCode(inviteCode);
+ verify(inviteCodeMapper, times(1)).decrementRemainingUses(inviteCode);
+ }
+}