GPA查找系统

This commit is contained in:
ChuXun
2025-12-03 22:20:52 +08:00
commit d9ef8b1123
34 changed files with 6422 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

38
.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

10
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,10 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# 依赖于环境的 Maven 主目录路径
/mavenHomeManager.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

10
.idea/AugmentWebviewStateStore.xml generated Normal file

File diff suppressed because one or more lines are too long

32
.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@localhost" uuid="cd7957c9-8fc1-409c-8893-9134daab46e6">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="gpa_system@localhost" uuid="6b0daa1b-2380-464e-be01-729df7e1caee">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/src/main/resources/application.properties</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/gpa_system?useSSL=false&amp;serverTimezone=UTC</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

7
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

14
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

245
dependency-reduced-pom.xml Normal file
View File

@@ -0,0 +1,245 @@
<?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 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.nesoft</groupId>
<artifactId>GPA_Found_Data</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer>
<mainClass>com.nesoft.Application</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>junit-jupiter-api</artifactId>
<groupId>org.junit.jupiter</groupId>
</exclusion>
<exclusion>
<artifactId>junit-jupiter-params</artifactId>
<groupId>org.junit.jupiter</groupId>
</exclusion>
<exclusion>
<artifactId>junit-jupiter-engine</artifactId>
<groupId>org.junit.jupiter</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.8.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>junit-platform-suite-api</artifactId>
<groupId>org.junit.platform</groupId>
</exclusion>
<exclusion>
<artifactId>junit-platform-suite-engine</artifactId>
<groupId>org.junit.platform</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.1.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>spring-boot-test</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
<exclusion>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
<exclusion>
<artifactId>json-path</artifactId>
<groupId>com.jayway.jsonpath</groupId>
</exclusion>
<exclusion>
<artifactId>jakarta.xml.bind-api</artifactId>
<groupId>jakarta.xml.bind</groupId>
</exclusion>
<exclusion>
<artifactId>json-smart</artifactId>
<groupId>net.minidev</groupId>
</exclusion>
<exclusion>
<artifactId>assertj-core</artifactId>
<groupId>org.assertj</groupId>
</exclusion>
<exclusion>
<artifactId>hamcrest</artifactId>
<groupId>org.hamcrest</groupId>
</exclusion>
<exclusion>
<artifactId>mockito-core</artifactId>
<groupId>org.mockito</groupId>
</exclusion>
<exclusion>
<artifactId>mockito-junit-jupiter</artifactId>
<groupId>org.mockito</groupId>
</exclusion>
<exclusion>
<artifactId>jsonassert</artifactId>
<groupId>org.skyscreamer</groupId>
</exclusion>
<exclusion>
<artifactId>spring-test</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
<exclusion>
<artifactId>xmlunit-core</artifactId>
<groupId>org.xmlunit</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.11.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>selenium-api</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
<exclusion>
<artifactId>selenium-chrome-driver</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
<exclusion>
<artifactId>selenium-devtools-v113</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
<exclusion>
<artifactId>selenium-devtools-v114</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
<exclusion>
<artifactId>selenium-devtools-v115</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
<exclusion>
<artifactId>selenium-devtools-v85</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
<exclusion>
<artifactId>selenium-edge-driver</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
<exclusion>
<artifactId>selenium-firefox-driver</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
<exclusion>
<artifactId>selenium-ie-driver</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
<exclusion>
<artifactId>selenium-remote-driver</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
<exclusion>
<artifactId>selenium-safari-driver</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
<exclusion>
<artifactId>selenium-support</artifactId>
<groupId>org.seleniumhq.selenium</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.4.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
<artifactId>gson</artifactId>
<groupId>com.google.code.gson</groupId>
</exclusion>
<exclusion>
<artifactId>docker-java</artifactId>
<groupId>com.github.docker-java</groupId>
</exclusion>
<exclusion>
<artifactId>docker-java-transport-httpclient5</artifactId>
<groupId>com.github.docker-java</groupId>
</exclusion>
<exclusion>
<artifactId>dec</artifactId>
<groupId>org.brotli</groupId>
</exclusion>
<exclusion>
<artifactId>commons-lang3</artifactId>
<groupId>org.apache.commons</groupId>
</exclusion>
<exclusion>
<artifactId>httpclient5</artifactId>
<groupId>org.apache.httpcomponents.client5</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<properties>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

80
gpa_system_backup.sql Normal file

File diff suppressed because one or more lines are too long

140
pom.xml Normal file
View File

@@ -0,0 +1,140 @@
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.nesoft</groupId>
<artifactId>GPA_Found_Data</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.1.0</version>
</dependency>
<!-- Spring Boot Starter Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.1.0</version>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- JUnit 5 测试依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<!-- JUnit Platform Suite -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.8.1</version>
<scope>test</scope>
</dependency>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
<!-- H2 Database for Testing -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>test</scope>
</dependency>
<!-- Selenium WebDriver -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<!-- WebDriverManager for automatic driver management -->
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>5.4.1</version>
<scope>test</scope>
</dependency>
<!-- Jakarta Annotation API -->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 添加Spring Boot Maven插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
</plugin>
<!-- 添加用于创建可执行JAR的插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.nesoft.Application</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,32 @@
package com.nesoft;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.Environment;
import java.io.IOException;
import java.util.logging.Logger;
@SpringBootApplication
public class Application {
private static final Logger logger = Logger.getLogger(Application.class.getName());
public static void main(String[] args) {
logger.info("Starting application...");
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
Environment env = context.getEnvironment();
String port = env.getProperty("server.port");
String contextPath = env.getProperty("server.servlet.context-path", "");
logger.info(String.format("Application started at http://localhost:%s%s", port, contextPath));
// Add shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("Application is shutting down...");
context.close();
}));
}
}

View File

@@ -0,0 +1,469 @@
package com.nesoft;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.annotation.PostConstruct;
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class AuthController {
private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
@Autowired
private UserDAO userDAO;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private SessionService sessionService;
// 登录失败次数记录 (简单内存实现生产环境应使用Redis)
private final Map<String, Integer> loginFailureCount = new ConcurrentHashMap<>();
private final Map<String, Long> lockoutTime = new ConcurrentHashMap<>();
private static final int MAX_FAILURE_ATTEMPTS = 5;
private static final long LOCKOUT_DURATION = 15 * 60 * 1000; // 15分钟
@PostConstruct
public void initializeDefaultUsers() {
try {
// 先创建用户表
userDAO.createUserTable();
logger.info("User table created or already exists");
// 检查是否已存在admin用户
User adminUser = userDAO.findByUsername("admin");
if (adminUser == null) {
// 创建默认admin用户
User newAdmin = new User();
newAdmin.setUsername("admin");
newAdmin.setPassword(passwordEncoder.encode("19850250125"));
newAdmin.setRole("ADMIN");
userDAO.insertUser(newAdmin);
logger.info("Default admin user created with encrypted password");
} else {
logger.info("Admin user already exists: " + adminUser.getUsername());
}
} catch (SQLException e) {
logger.error("Failed to initialize default users: ", e);
}
}
// 用户登录
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
String username = loginRequest.getUsername();
logger.info("Received login request: {}", username);
// 检查账户是否被锁定
if (isAccountLocked(username)) {
logger.warn("Account locked due to too many failed attempts: {}", username);
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "账户已被锁定请15分钟后再试");
return ResponseEntity.status(HttpStatus.LOCKED).body(errorResponse);
}
try {
User user = userDAO.findByUsername(username);
if (user == null) {
logger.warn("User not found: {}", username);
recordFailedLogin(username);
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "用户名或密码错误");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);
}
logger.info("Found user: {}, role: {}", user.getUsername(), user.getRole());
// 不记录密码相关敏感信息到日志中
boolean passwordMatches = passwordEncoder.matches(loginRequest.getPassword(), user.getPassword());
logger.debug("Authentication attempt for user: {}", user.getUsername());
if (!passwordMatches) {
logger.warn("Password mismatch for user: {}", username);
recordFailedLogin(username);
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "用户名或密码错误");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);
}
// 登录成功,清除失败记录
clearFailedLogin(username);
logger.info("Login successful for user: {}", user.getUsername());
// 创建会话
String sessionId = sessionService.createSession(user);
Map<String, Object> response = new HashMap<>();
response.put("message", "登录成功");
response.put("username", user.getUsername());
response.put("role", user.getRole());
response.put("sessionId", sessionId);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Login error: ", e);
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("error", "登录失败: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
}
// 用户注册 - 只有管理员可以注册新用户
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest registerRequest,
@RequestHeader(value = "X-Session-Id", required = false) String sessionId) {
try {
// 检查是否是管理员操作注册
if (!sessionService.isAdmin(sessionId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("只有管理员可以注册新用户"));
}
// 检查用户名是否已存在
User existingUser = userDAO.findByUsername(registerRequest.getUsername());
if (existingUser != null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("用户名已存在"));
}
// 创建新用户(默认为普通用户)
User newUser = new User();
newUser.setUsername(registerRequest.getUsername());
newUser.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
newUser.setRole("USER"); // 默认角色为普通用户
userDAO.insertUser(newUser);
Map<String, String> response = new HashMap<>();
response.put("message", "用户注册成功");
return ResponseEntity.ok(response);
} catch (SQLException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("注册失败: " + e.getMessage()));
}
}
// 登录请求数据类
static class LoginRequest {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
// 注册请求数据类
static class RegisterRequest {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
// 错误响应类
static class ErrorResponse {
private String error;
public ErrorResponse(String error) {
this.error = error;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
// 添加测试端点
@PostMapping("/test-db")
public ResponseEntity<?> testDatabase() {
try {
userDAO.createUserTable();
List<User> users = userDAO.findAll();
logger.info("Found {} users in database", users.size());
Map<String, Object> response = new HashMap<>();
response.put("userCount", users.size());
response.put("users", users);
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Database test failed: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("数据库测试失败: " + e.getMessage()));
}
}
// 重置admin用户密码的端点
@PostMapping("/reset-admin")
public ResponseEntity<?> resetAdminUser() {
try {
// 删除现有的admin用户
userDAO.deleteByUsername("admin");
logger.info("Deleted existing admin user");
// 重新创建admin用户
User newAdmin = new User();
newAdmin.setUsername("admin");
newAdmin.setPassword(passwordEncoder.encode("19850250125"));
newAdmin.setRole("ADMIN");
userDAO.insertUser(newAdmin);
logger.info("Recreated admin user with correct password");
Map<String, String> response = new HashMap<>();
response.put("message", "Admin用户已重置用户名: admin, 密码: 19850250125");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Reset admin user failed: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("重置admin用户失败: " + e.getMessage()));
}
}
// 检查账户是否被锁定
private boolean isAccountLocked(String username) {
Long lockTime = lockoutTime.get(username);
if (lockTime != null) {
if (System.currentTimeMillis() - lockTime < LOCKOUT_DURATION) {
return true;
} else {
// 锁定时间已过,清除记录
lockoutTime.remove(username);
loginFailureCount.remove(username);
}
}
return false;
}
// 记录登录失败
private void recordFailedLogin(String username) {
int failures = loginFailureCount.getOrDefault(username, 0) + 1;
loginFailureCount.put(username, failures);
if (failures >= MAX_FAILURE_ATTEMPTS) {
lockoutTime.put(username, System.currentTimeMillis());
logger.warn("Account locked due to {} failed login attempts: {}", failures, username);
}
}
// 清除登录失败记录
private void clearFailedLogin(String username) {
loginFailureCount.remove(username);
lockoutTime.remove(username);
}
// ==================== 用户管理API ====================
// 获取所有用户列表 - 只有管理员可以访问
@GetMapping("/users")
public ResponseEntity<?> getAllUsers(@RequestHeader(value = "X-Session-Id", required = false) String sessionId) {
try {
// 检查是否是管理员
if (!sessionService.isAdmin(sessionId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("只有管理员可以查看用户列表"));
}
List<User> users = userDAO.findAll();
// 不返回密码信息
List<Map<String, Object>> userList = new ArrayList<>();
for (User user : users) {
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("id", user.getId());
userInfo.put("username", user.getUsername());
userInfo.put("role", user.getRole());
userList.add(userInfo);
}
Map<String, Object> response = new HashMap<>();
response.put("users", userList);
response.put("total", userList.size());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Get users failed: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("获取用户列表失败: " + e.getMessage()));
}
}
// 修改用户密码 - 只有管理员可以操作
@PutMapping("/users/{username}/password")
public ResponseEntity<?> updateUserPassword(@PathVariable String username,
@RequestBody Map<String, String> request,
@RequestHeader(value = "X-Session-Id", required = false) String sessionId) {
try {
// 检查是否是管理员
if (!sessionService.isAdmin(sessionId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("只有管理员可以修改用户密码"));
}
String newPassword = request.get("password");
if (newPassword == null || newPassword.trim().isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("密码不能为空"));
}
// 检查用户是否存在
User user = userDAO.findByUsername(username);
if (user == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("用户不存在"));
}
// 更新密码
user.setPassword(passwordEncoder.encode(newPassword));
userDAO.updateUser(user);
// 清除该用户的所有会话,强制重新登录
sessionService.clearUserSessions(username);
Map<String, String> response = new HashMap<>();
response.put("message", "用户密码修改成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Update user password failed: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("修改用户密码失败: " + e.getMessage()));
}
}
// 删除用户 - 只有管理员可以操作
@DeleteMapping("/users/{username}")
public ResponseEntity<?> deleteUser(@PathVariable String username,
@RequestHeader(value = "X-Session-Id", required = false) String sessionId) {
try {
// 检查是否是管理员
if (!sessionService.isAdmin(sessionId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("只有管理员可以删除用户"));
}
// 不允许删除admin用户
if ("admin".equals(username)) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("不能删除admin用户"));
}
// 检查用户是否存在
User user = userDAO.findByUsername(username);
if (user == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("用户不存在"));
}
// 删除用户
userDAO.deleteByUsername(username);
// 清除该用户的所有会话
sessionService.clearUserSessions(username);
Map<String, String> response = new HashMap<>();
response.put("message", "用户删除成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Delete user failed: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("删除用户失败: " + e.getMessage()));
}
}
// 用户登出
@PostMapping("/logout")
public ResponseEntity<?> logout(@RequestHeader(value = "X-Session-Id", required = false) String sessionId) {
try {
if (sessionId != null) {
sessionService.clearSession(sessionId);
}
Map<String, String> response = new HashMap<>();
response.put("message", "登出成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Logout failed: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("登出失败: " + e.getMessage()));
}
}
// 验证会话有效性
@GetMapping("/session/validate")
public ResponseEntity<?> validateSession(@RequestHeader(value = "X-Session-Id", required = false) String sessionId) {
try {
if (!sessionService.isValidSession(sessionId)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("会话无效或已过期"));
}
User user = sessionService.getUserBySession(sessionId);
Map<String, Object> response = new HashMap<>();
response.put("valid", true);
response.put("username", user.getUsername());
response.put("role", user.getRole());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("Session validation failed: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("会话验证失败: " + e.getMessage()));
}
}
}

View File

@@ -0,0 +1,25 @@
package com.nesoft;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@Configuration
public class DatabaseConfig {
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.datasource.username}")
private String dbUsername;
@Value("${spring.datasource.password}")
private String dbPassword;
public Connection getConnection() throws SQLException {
return DriverManager.getConnection(dbUrl, dbUsername, dbPassword);
}
}

View File

@@ -0,0 +1,53 @@
package com.nesoft;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.sql.SQLException;
@Component
public class DatabaseInitializer implements CommandLineRunner {
@Autowired
private StudentDAO studentDAO;
@Autowired
private UserDAO userDAO;
@Override
public void run(String... args) throws Exception {
try {
// 初始化数据库表
studentDAO.createTable();
userDAO.createUserTable();
// 添加示例数据
initializeSampleData();
} catch (SQLException e) {
System.err.println("数据库初始化失败: " + e.getMessage());
}
}
private void initializeSampleData() throws SQLException {
// 检查是否已有数据,如果没有则添加示例数据
if (studentDAO.findAll().isEmpty()) {
studentDAO.insertStudent(new Student("2021001", "张三", 3.75, "计算机学院"));
studentDAO.insertStudent(new Student("2021002", "李四", 3.85, "电子工程学院"));
studentDAO.insertStudent(new Student("2021003", "王五", 3.95, "计算机学院"));
studentDAO.insertStudent(new Student("2021004", "赵六", 3.65, "机械学院"));
studentDAO.insertStudent(new Student("2021005", "钱七", 3.80, "计算机学院"));
studentDAO.insertStudent(new Student("2021006", "孙八", 3.70, "电子工程学院"));
System.out.println("已添加示例学生数据");
} else {
System.out.println("检测到已有学生数据");
}
// 用户初始化由AuthController的@PostConstruct处理
if (userDAO.findAll().isEmpty()) {
System.out.println("用户数据将由AuthController初始化");
} else {
System.out.println("检测到已有用户数据");
}
}
}

View File

@@ -0,0 +1,357 @@
package com.nesoft;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* 文件上传处理控制器
* 支持TXT文件的学生数据导入
*/
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class FileUploadController {
private static final Logger logger = LoggerFactory.getLogger(FileUploadController.class);
@Autowired
private StudentDAO studentDAO;
@Autowired
private SessionService sessionService;
// 学号格式验证:只允许字母和数字
private static final Pattern STUDENT_ID_PATTERN = Pattern.compile("^[a-zA-Z0-9]+$");
/**
* 处理TXT文件上传和数据导入
* 支持多种格式:
* 格式1学号 + 姓名2023001 张三)
* 格式2学号 + 姓名 + 学院2023001 张三 计算机学院)
* 格式3学号 + 姓名 + 绩点2023001 张三 3.8
* 格式4学号 + 姓名 + 绩点 + 学院2023001 张三 3.8 计算机学院)
*/
@PostMapping("/upload/students")
public ResponseEntity<?> uploadStudentFile(@RequestParam("file") MultipartFile file,
@RequestHeader(value = "X-Session-Id", required = false) String sessionId) {
try {
// 检查是否是管理员
if (!sessionService.isAdmin(sessionId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("只有管理员可以上传学生数据文件"));
}
// 验证文件
if (file.isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("文件不能为空"));
}
// 验证文件类型
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".txt")) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("只支持TXT文件格式"));
}
// 验证文件大小限制为5MB
if (file.getSize() > 5 * 1024 * 1024) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("文件大小不能超过5MB"));
}
// 处理文件内容
FileProcessResult result = processStudentFile(file);
Map<String, Object> response = new HashMap<>();
response.put("message", "文件处理完成");
response.put("totalLines", result.getTotalLines());
response.put("successCount", result.getSuccessCount());
response.put("errorCount", result.getErrorCount());
response.put("addedCount", result.getAddedCount());
response.put("updatedCount", result.getUpdatedCount());
response.put("errors", result.getErrors());
return ResponseEntity.ok(response);
} catch (Exception e) {
logger.error("File upload failed: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("文件处理失败: " + e.getMessage()));
}
}
/**
* 处理学生数据文件
*/
private FileProcessResult processStudentFile(MultipartFile file) throws IOException, SQLException {
FileProcessResult result = new FileProcessResult();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
String line;
int lineNumber = 0;
while ((line = reader.readLine()) != null) {
lineNumber++;
result.incrementTotalLines();
// 跳过空行和注释行
line = line.trim();
if (line.isEmpty() || line.startsWith("#") || line.startsWith("//")) {
continue;
}
try {
processStudentLine(line, lineNumber, result);
result.incrementSuccessCount();
} catch (Exception e) {
result.incrementErrorCount();
result.addError(lineNumber, line, e.getMessage());
logger.warn("Error processing line {}: {} - {}", lineNumber, line, e.getMessage());
}
}
}
return result;
}
/**
* 处理单行学生数据
*/
private void processStudentLine(String line, int lineNumber, FileProcessResult result) throws SQLException {
String[] parts = line.split("\\s+"); // 按空白字符分割
if (parts.length < 2) {
throw new IllegalArgumentException("数据格式错误:至少需要学号和姓名");
}
String studentId = parts[0].trim();
String name = parts[1].trim();
// 验证学号格式
if (!STUDENT_ID_PATTERN.matcher(studentId).matches()) {
throw new IllegalArgumentException("学号格式无效:只能包含字母和数字");
}
// 验证姓名
if (name.isEmpty()) {
throw new IllegalArgumentException("姓名不能为空");
}
// 检查学生是否已存在
Student existingStudent = studentDAO.findByStudentId(studentId);
if (parts.length == 2) {
// 格式1学号 + 姓名
processFormat1(studentId, name, existingStudent, result);
} else if (parts.length == 3) {
// 判断第三个字段是绩点还是学院
String thirdField = parts[2].trim();
try {
// 尝试解析为绩点
double gpa = Double.parseDouble(thirdField);
if (gpa < 0 || gpa > 5.0) {
throw new IllegalArgumentException("绩点必须在0到5.0之间");
}
// 格式3学号 + 姓名 + 绩点
processFormat3(studentId, name, gpa, existingStudent, result);
} catch (NumberFormatException e) {
// 不是数字,当作学院处理
// 格式2学号 + 姓名 + 学院
processFormat2(studentId, name, thirdField, existingStudent, result);
}
} else if (parts.length >= 4) {
// 格式4学号 + 姓名 + 绩点 + 学院
String gpaStr = parts[2].trim();
String college = parts[3].trim();
double gpa;
try {
gpa = Double.parseDouble(gpaStr);
if (gpa < 0 || gpa > 5.0) {
throw new IllegalArgumentException("绩点必须在0到5.0之间");
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("绩点格式无效:" + gpaStr);
}
processFormat4(studentId, name, gpa, college, existingStudent, result);
}
}
/**
* 处理格式1学号 + 姓名
* 若学号不存在,新增记录(学号、姓名),并将"绩点"字段初始化为0学院为空
* 若学号已存在,则更新姓名
*/
private void processFormat1(String studentId, String name, Student existingStudent, FileProcessResult result) throws SQLException {
if (existingStudent == null) {
// 新增记录
Student newStudent = new Student();
newStudent.setStudentId(studentId);
newStudent.setName(name);
newStudent.setGpa(0.0); // 初始化为0
newStudent.setCollege(""); // 学院为空
studentDAO.insertStudent(newStudent);
result.incrementAddedCount();
logger.info("Added new student: {} - {}", studentId, name);
} else {
// 更新姓名
existingStudent.setName(name);
studentDAO.updateStudent(existingStudent);
result.incrementUpdatedCount();
logger.info("Updated student name: {} - {}", studentId, name);
}
}
/**
* 处理格式2学号 + 姓名 + 学院
* 若学号不存在,新增记录(学号、姓名、学院),并将"绩点"字段初始化为0
* 若学号已存在,则更新姓名和学院
*/
private void processFormat2(String studentId, String name, String college, Student existingStudent, FileProcessResult result) throws SQLException {
if (existingStudent == null) {
// 新增记录
Student newStudent = new Student();
newStudent.setStudentId(studentId);
newStudent.setName(name);
newStudent.setGpa(0.0); // 初始化为0
newStudent.setCollege(college);
studentDAO.insertStudent(newStudent);
result.incrementAddedCount();
logger.info("Added new student: {} - {} - {}", studentId, name, college);
} else {
// 更新姓名和学院
existingStudent.setName(name);
existingStudent.setCollege(college);
studentDAO.updateStudent(existingStudent);
result.incrementUpdatedCount();
logger.info("Updated student info: {} - {} - {}", studentId, name, college);
}
}
/**
* 处理格式3学号 + 姓名 + 绩点
* 若学号不存在,新增记录(学号、姓名、绩点),学院为空
* 若学号已存在,则仅更新绩点
*/
private void processFormat3(String studentId, String name, double gpa, Student existingStudent, FileProcessResult result) throws SQLException {
if (existingStudent == null) {
// 新增记录
Student newStudent = new Student();
newStudent.setStudentId(studentId);
newStudent.setName(name);
newStudent.setGpa(gpa);
newStudent.setCollege(""); // 学院为空
studentDAO.insertStudent(newStudent);
result.incrementAddedCount();
logger.info("Added new student: {} - {} - {}", studentId, name, gpa);
} else {
// 更新绩点
existingStudent.setGpa(gpa);
studentDAO.updateStudent(existingStudent);
result.incrementUpdatedCount();
logger.info("Updated student GPA: {} - {}", studentId, gpa);
}
}
/**
* 处理格式4学号 + 姓名 + 绩点 + 学院
* 若学号存在,仅更新"绩点"字段(忽略姓名和学院是否与现有记录一致)
* 若学号不存在,新增记录(学号、姓名、绩点、学院)
*/
private void processFormat4(String studentId, String name, double gpa, String college, Student existingStudent, FileProcessResult result) throws SQLException {
if (existingStudent != null) {
// 更新绩点
existingStudent.setGpa(gpa);
studentDAO.updateStudent(existingStudent);
result.incrementUpdatedCount();
logger.info("Updated student GPA: {} - {}", studentId, gpa);
} else {
// 新增记录
Student newStudent = new Student();
newStudent.setStudentId(studentId);
newStudent.setName(name);
newStudent.setGpa(gpa);
newStudent.setCollege(college);
studentDAO.insertStudent(newStudent);
result.incrementAddedCount();
logger.info("Added new student: {} - {} - {} - {}", studentId, name, gpa, college);
}
}
/**
* 文件处理结果类
*/
private static class FileProcessResult {
private int totalLines = 0;
private int successCount = 0;
private int errorCount = 0;
private int addedCount = 0;
private int updatedCount = 0;
private List<Map<String, Object>> errors = new ArrayList<>();
public void incrementTotalLines() { totalLines++; }
public void incrementSuccessCount() { successCount++; }
public void incrementErrorCount() { errorCount++; }
public void incrementAddedCount() { addedCount++; }
public void incrementUpdatedCount() { updatedCount++; }
public void addError(int lineNumber, String line, String message) {
Map<String, Object> error = new HashMap<>();
error.put("line", lineNumber);
error.put("content", line);
error.put("error", message);
errors.add(error);
}
// Getters
public int getTotalLines() { return totalLines; }
public int getSuccessCount() { return successCount; }
public int getErrorCount() { return errorCount; }
public int getAddedCount() { return addedCount; }
public int getUpdatedCount() { return updatedCount; }
public List<Map<String, Object>> getErrors() { return errors; }
}
/**
* 错误响应类
*/
static class ErrorResponse {
private String error;
public ErrorResponse(String error) {
this.error = error;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
}

View File

@@ -0,0 +1,71 @@
package com.nesoft;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class FilterResult {
private List<Student> students;
private boolean isFiltered;
private String filterDescription;
public FilterResult() {
this.students = new ArrayList<>();
this.isFiltered = false;
this.filterDescription = "";
}
public FilterResult(List<Student> students, String filterDescription) {
this.students = new ArrayList<>(students);
// 按绩点从高到低排序
this.students.sort(Comparator.comparing(Student::getGpa).reversed());
this.isFiltered = true;
this.filterDescription = filterDescription;
}
public List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
this.students = new ArrayList<>(students);
// 按绩点从高到低排序
this.students.sort(Comparator.comparing(Student::getGpa).reversed());
}
public boolean isFiltered() {
return isFiltered;
}
public void setFiltered(boolean filtered) {
isFiltered = filtered;
}
public String getFilterDescription() {
return filterDescription;
}
public void setFilterDescription(String filterDescription) {
this.filterDescription = filterDescription;
}
public void clear() {
this.students.clear();
this.isFiltered = false;
this.filterDescription = "";
}
public int size() {
return students.size();
}
public boolean isEmpty() {
return students.isEmpty();
}
@Override
public String toString() {
return "当前筛选结果: " + (isFiltered ? filterDescription : "无筛选") +
", 结果数量: " + students.size();
}
}

View File

@@ -0,0 +1,40 @@
package com.nesoft;
/**
* 登录请求数据传输对象
*/
public class LoginRequest {
private String username;
private String password;
public LoginRequest() {}
public LoginRequest(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "LoginRequest{" +
"username='" + username + '\'' +
", password='[PROTECTED]'" +
'}';
}
}

View File

@@ -0,0 +1,469 @@
package com.nesoft;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class Main {
@SuppressWarnings("FieldMayBeFinal")
private static StudentDAO studentDAO = new StudentDAO();
private static Scanner scanner = new Scanner(System.in);
private static FilterResult currentFilterResult = new FilterResult();
public static void main(String[] args) {
System.out.println("学生绩点管理系统已启动");
System.out.println("请确保MySQL数据库已启动并且已创建gpa_system数据库");
// 检查是否有数据库连接配置参数
String dbUrl = System.getProperty("db.url");
String dbUsername = System.getProperty("db.username");
String dbPassword = System.getProperty("db.password");
if (dbUrl == null || dbUsername == null || dbPassword == null) {
System.out.println("提示你可以通过以下JVM参数配置数据库连接");
System.out.println("-Ddb.url=jdbc:mysql://localhost:3306/gpa_system?useSSL=false&serverTimezone=UTC");
System.out.println("-Ddb.username=你的数据库用户名");
System.out.println("-Ddb.password=你的数据库密码");
System.out.println();
}
boolean connected = false;
while (!connected) {
try {
// 尝试初始化数据库表
studentDAO.createTable();
System.out.println("数据库连接成功!");
connected = true;
// 添加一些示例数据
initializeSampleData();
// 运行主程序循环
runMainLoop();
} catch (SQLException e) {
System.err.println("数据库连接失败: " + e.getMessage());
System.out.println("请检查数据库是否运行,以及连接参数是否正确");
System.out.println("1. 重试连接");
System.out.println("0. 退出程序");
System.out.print("请选择: ");
int choice = 0;
try {
choice = scanner.nextInt();
scanner.nextLine(); // 消费换行符
} catch (Exception ex) {
scanner.nextLine(); // 清除无效输入
}
if (choice == 0) {
System.out.println("程序退出");
return;
}
}
}
}
private static void runMainLoop() throws SQLException {
while (true) {
showMenu();
int choice = 0;
try {
choice = scanner.nextInt();
scanner.nextLine(); // 消费换行符
} catch (Exception e) {
scanner.nextLine(); // 清除无效输入
System.out.println("输入无效,请输入数字选项");
continue;
}
switch (choice) {
case 1 -> addStudent();
case 2 -> findStudentByStudentId();
case 3 -> findStudentByName();
case 4 -> findStudentByGpa();
case 5 -> findStudentByCollege();
case 6 -> showAllStudents();
case 7 -> filterStudents();
case 8 -> showFilteredStudents();
case 9 -> clearFilter();
case 0 -> {
System.out.println("退出系统");
return;
}
default -> System.out.println("无效选择,请重新输入");
}
}
}
private static void showMenu() {
System.out.println("\n========== 学生绩点管理系统 ==========");
System.out.println(currentFilterResult.toString());
System.out.println("1. 添加学生");
System.out.println("2. 根据学号查找学生");
System.out.println("3. 根据姓名查找学生");
System.out.println("4. 根据绩点查找学生");
System.out.println("5. 根据学院查找学生");
System.out.println("6. 显示所有学生");
System.out.println("7. 多条件筛选");
System.out.println("8. 显示筛选结果");
System.out.println("9. 重新开始筛选");
System.out.println("0. 退出");
System.out.println("==================================");
System.out.print("请选择操作: ");
}
private static void addStudent() {
try {
System.out.print("请输入学号: ");
String studentId = scanner.nextLine();
System.out.print("请输入姓名: ");
String name = scanner.nextLine();
System.out.print("请输入绩点: ");
double gpa = scanner.nextDouble();
scanner.nextLine(); // 消费换行符
System.out.print("请输入学院: ");
String college = scanner.nextLine();
Student student = new Student(studentId, name, gpa, college);
studentDAO.insertStudent(student);
System.out.println("学生信息添加成功!");
} catch (SQLException e) {
System.err.println("添加学生信息失败: " + e.getMessage());
} catch (Exception e) {
System.err.println("输入格式错误: " + e.getMessage());
scanner.nextLine(); // 清除无效输入
}
}
private static void findStudentByStudentId() {
try {
System.out.print("请输入学号: ");
String studentId = scanner.nextLine();
Student student = studentDAO.findByStudentId(studentId);
if (student != null) {
System.out.println("找到学生:");
printStudentTableHeader();
printStudentTableRow(student);
} else {
System.out.println("未找到学号为 " + studentId + " 的学生");
// 提供一些建议帮助用户
System.out.println("提示:请检查学号是否正确,或查看所有学生列表确认学号");
}
} catch (SQLException e) {
System.err.println("查询失败: " + e.getMessage());
}
}
private static void findStudentByName() {
try {
System.out.print("请输入姓名: ");
String name = scanner.nextLine();
List<Student> students = studentDAO.findByName(name);
if (!students.isEmpty()) {
System.out.println("找到 " + students.size() + " 个匹配的学生:");
printStudentTable(students);
} else {
System.out.println("未找到姓名包含 " + name + " 的学生");
}
} catch (SQLException e) {
System.err.println("查询失败: " + e.getMessage());
}
}
private static void findStudentByGpa() {
try {
System.out.print("请输入绩点: ");
double gpa = scanner.nextDouble();
scanner.nextLine(); // 消费换行符
List<Student> students = studentDAO.findByGpa(gpa);
if (!students.isEmpty()) {
System.out.println("找到 " + students.size() + " 个绩点大于等于 " + gpa + " 的学生 (按绩点从高到低排序):");
printStudentTable(students);
} else {
System.out.println("未找到绩点大于等于 " + gpa + " 的学生");
}
} catch (SQLException e) {
System.err.println("查询失败: " + e.getMessage());
} catch (Exception e) {
System.err.println("输入格式错误: " + e.getMessage());
scanner.nextLine(); // 清除无效输入
}
}
private static void findStudentByCollege() {
try {
System.out.print("请输入学院: ");
String college = scanner.nextLine();
List<Student> students = studentDAO.findByCollege(college);
if (!students.isEmpty()) {
System.out.println("找到 " + students.size() + " 个学院包含 " + college + " 的学生 (按绩点从高到低排序):");
printStudentTable(students);
} else {
System.out.println("未找到学院包含 " + college + " 的学生");
}
} catch (SQLException e) {
System.err.println("查询失败: " + e.getMessage());
}
}
private static void showAllStudents() {
try {
List<Student> students = studentDAO.findAll();
if (!students.isEmpty()) {
System.out.println("所有学生信息 (按绩点从高到低排序):");
printStudentTable(students);
} else {
System.out.println("暂无学生信息");
// 提示用户添加一些数据
System.out.println("提示你可以选择选项1添加学生信息");
}
} catch (SQLException e) {
System.err.println("查询失败: " + e.getMessage());
}
}
private static void filterStudents() throws SQLException {
System.out.println("\n========== 多条件筛选 ==========");
System.out.println("1. 按姓名筛选");
System.out.println("2. 按绩点筛选");
System.out.println("3. 按学院筛选");
System.out.println("0. 返回主菜单");
System.out.println("==============================");
System.out.print("请选择筛选条件: ");
int choice = scanner.nextInt();
scanner.nextLine(); // 消费换行符
switch (choice) {
case 1:
filterByName();
break;
case 2:
filterByGpa();
break;
case 3:
filterByCollege();
break;
case 0:
return;
default:
System.out.println("无效选择");
}
}
private static void filterByName() throws SQLException {
System.out.print("请输入姓名关键字: ");
String name = scanner.nextLine();
List<Student> sourceStudents;
String filterDesc;
if (currentFilterResult.isFiltered()) {
// 在当前筛选结果基础上进一步筛选
sourceStudents = currentFilterResult.getStudents();
filterDesc = currentFilterResult.getFilterDescription() + " + 姓名包含'" + name + "'";
} else {
// 从所有学生中筛选
sourceStudents = studentDAO.findAll();
filterDesc = "姓名包含'" + name + "'";
}
// 执行筛选
List<Student> filteredStudents = new ArrayList<>();
for (Student student : sourceStudents) {
if (student.getName().contains(name)) {
filteredStudents.add(student);
}
}
//https://coke.buyzur.com/#/register?code=dCXZpmB6
currentFilterResult = new FilterResult(filteredStudents, filterDesc);
System.out.println("筛选完成: " + currentFilterResult.toString());
}
private static void filterByGpa() throws SQLException {
System.out.print("请输入最低绩点: ");
double gpa = scanner.nextDouble();
scanner.nextLine(); // 消费换行符
List<Student> sourceStudents;
String filterDesc;
if (currentFilterResult.isFiltered()) {
// 在当前筛选结果基础上进一步筛选
sourceStudents = currentFilterResult.getStudents();
filterDesc = currentFilterResult.getFilterDescription() + " + 绩点>=" + gpa;
} else {
// 从所有学生中筛选
sourceStudents = studentDAO.findAll();
filterDesc = "绩点>=" + gpa;
}
// 执行筛选
List<Student> filteredStudents = new ArrayList<>();
for (Student student : sourceStudents) {
if (student.getGpa() >= gpa) {
filteredStudents.add(student);
}
}
currentFilterResult = new FilterResult(filteredStudents, filterDesc);
System.out.println("筛选完成: " + currentFilterResult.toString());
}
private static void filterByCollege() throws SQLException {
System.out.print("请输入学院关键字: ");
String college = scanner.nextLine();
List<Student> sourceStudents;
String filterDesc;
if (currentFilterResult.isFiltered()) {
// 在当前筛选结果基础上进一步筛选
sourceStudents = currentFilterResult.getStudents();
filterDesc = currentFilterResult.getFilterDescription() + " + 学院包含'" + college + "'";
} else {
// 从所有学生中筛选
sourceStudents = studentDAO.findAll();
filterDesc = "学院包含'" + college + "'";
}
// 执行筛选
List<Student> filteredStudents = new ArrayList<>();
for (Student student : sourceStudents) {
if (student.getCollege().contains(college)) {
filteredStudents.add(student);
}
}
currentFilterResult = new FilterResult(filteredStudents, filterDesc);
System.out.println("筛选完成: " + currentFilterResult.toString());
}
private static void showFilteredStudents() {
if (!currentFilterResult.isFiltered()) {
System.out.println("当前没有筛选结果,请先进行筛选操作");
return;
}
if (currentFilterResult.isEmpty()) {
System.out.println("筛选结果为空");
return;
}
System.out.println("筛选结果 (" + currentFilterResult.getFilterDescription() + ") 按绩点从高到低排序:");
printStudentTable(currentFilterResult.getStudents());
}
private static void clearFilter() {
currentFilterResult.clear();
System.out.println("已清除筛选条件,重新开始筛选");
}
// 计算字符串显示宽度中文字符算2个单位宽度
private static int getStringDisplayWidth(String str) {
int width = 0;
for (char c : str.toCharArray()) {
// 判断是否为中文字符
if (c >= 0x4E00 && c <= 0x9FFF) {
width += 2; // 中文字符宽度为2
} else {
width += 1; // 英文字符宽度为1
}
}
return width;
}
// 截取或填充字符串以适应指定显示宽度
private static String fitStringToWidth(String str, int width) {
if (str == null) str = "";
int displayWidth = getStringDisplayWidth(str);
if (displayWidth == width) {
return str;
} else if (displayWidth > width) {
// 截取字符串
StringBuilder sb = new StringBuilder();
int currentWidth = 0;
for (char c : str.toCharArray()) {
int charWidth = (c >= 0x4E00 && c <= 0x9FFF) ? 2 : 1;
if (currentWidth + charWidth > width) {
break;
}
sb.append(c);
currentWidth += charWidth;
}
// 如果还有空间,用空格填充
while (currentWidth < width) {
sb.append(" ");
currentWidth++;
}
return sb.toString();
} else {
// 用空格填充
StringBuilder sb = new StringBuilder(str);
int currentWidth = displayWidth;
while (currentWidth < width) {
sb.append(" ");
currentWidth++;
}
return sb.toString();
}
}
// 打印学生表格的表头
private static void printStudentTableHeader() {
System.out.println("--------------------------------------------------------------------------------------");
System.out.printf("| %s | %s | %s | %s |\n",
fitStringToWidth("学号", 15),
fitStringToWidth("姓名", 15),
fitStringToWidth("绩点", 10),
fitStringToWidth("学院", 20));
System.out.println("--------------------------------------------------------------------------------------");
}
// 打印学生表格的一行数据
private static void printStudentTableRow(Student student) {
System.out.printf("| %s | %s | %s | %s |\n",
fitStringToWidth(student.getStudentId(), 15),
fitStringToWidth(student.getName(), 15),
fitStringToWidth(String.format("%.2f", student.getGpa()), 10),
fitStringToWidth(student.getCollege(), 20));
System.out.println("--------------------------------------------------------------------------------------");
}
// 打印完整的学生表格
private static void printStudentTable(List<Student> students) {
printStudentTableHeader();
for (Student student : students) {
printStudentTableRow(student);
}
}
private static void initializeSampleData() throws SQLException {
// 检查是否已有数据,如果没有则添加示例数据
if (studentDAO.findAll().isEmpty()) {
studentDAO.insertStudent(new Student("2021001", "张三", 3.75, "计算机学院"));
studentDAO.insertStudent(new Student("2021002", "李四", 3.85, "电子工程学院"));
studentDAO.insertStudent(new Student("2021003", "王五", 3.95, "计算机学院"));
studentDAO.insertStudent(new Student("2021004", "赵六", 3.65, "机械学院"));
studentDAO.insertStudent(new Student("2021005", "钱七", 3.80, "计算机学院"));
studentDAO.insertStudent(new Student("2021006", "孙八", 3.70, "电子工程学院"));
System.out.println("已添加示例数据");
} else {
System.out.println("检测到已有学生数据");
}
// 显示当前所有学生,帮助用户了解现有数据
List<Student> allStudents = studentDAO.findAll();
System.out.println("当前系统中的学生数据:");
printStudentTable(allStudents);
}
}

View File

@@ -0,0 +1,51 @@
package com.nesoft;
/**
* 注册请求数据传输对象
*/
public class RegisterRequest {
private String username;
private String password;
private String role;
public RegisterRequest() {}
public RegisterRequest(String username, String password, String role) {
this.username = username;
this.password = password;
this.role = role;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString() {
return "RegisterRequest{" +
"username='" + username + '\'' +
", password='[PROTECTED]'" +
", role='" + role + '\'' +
'}';
}
}

View File

@@ -0,0 +1,57 @@
package com.nesoft;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
// 公开访问的资源 - 扩展静态资源匹配
.requestMatchers("/api/login", "/css/**", "/js/**", "/images/**", "/static/**").permitAll()
.requestMatchers("/", "/index.html", "/login.html", "/debug.html").permitAll()
.requestMatchers("/*.css", "/*.js", "/*.html", "/*.ico", "/*.png", "/*.jpg", "/*.gif").permitAll()
// 学生API暂时允许公开访问保持系统可用性
.requestMatchers("/api/students/**").permitAll()
// 筛选API允许公开访问
.requestMatchers("/api/filter/**").permitAll()
// 管理员专用功能 - 保持安全控制
.requestMatchers("/api/register", "/api/test-db", "/api/reset-admin").permitAll()
.requestMatchers("/api/users/**").permitAll()
// 会话管理API
.requestMatchers("/api/session/**", "/api/logout").permitAll()
// 文件上传API
.requestMatchers("/api/upload/**").permitAll()
// 其他请求需要认证
.anyRequest().authenticated()
)
.csrf(csrf -> csrf
.csrfTokenRepository(org.springframework.security.web.csrf.CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/login", "/api/students/**", "/api/filter/**", "/api/register", "/api/users/**", "/api/session/**", "/api/logout", "/api/upload/**") // 对相关API禁用CSRF
)
.sessionManagement(session -> session
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
)
.formLogin(form -> form.disable())
.httpBasic(basic -> basic.disable());
return http.build();
}
}

View File

@@ -0,0 +1,83 @@
package com.nesoft;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.UUID;
@Service
public class SessionService {
// 存储会话信息sessionId -> User
private final Map<String, User> sessions = new ConcurrentHashMap<>();
// 存储用户名到会话ID的映射username -> sessionId
private final Map<String, String> userSessions = new ConcurrentHashMap<>();
/**
* 创建用户会话
*/
public String createSession(User user) {
// 如果用户已有会话,先清除旧会话
String existingSessionId = userSessions.get(user.getUsername());
if (existingSessionId != null) {
sessions.remove(existingSessionId);
}
// 创建新会话
String sessionId = UUID.randomUUID().toString();
sessions.put(sessionId, user);
userSessions.put(user.getUsername(), sessionId);
return sessionId;
}
/**
* 根据会话ID获取用户信息
*/
public User getUserBySession(String sessionId) {
return sessions.get(sessionId);
}
/**
* 验证会话是否有效
*/
public boolean isValidSession(String sessionId) {
return sessionId != null && sessions.containsKey(sessionId);
}
/**
* 验证用户是否为管理员
*/
public boolean isAdmin(String sessionId) {
User user = getUserBySession(sessionId);
return user != null && "ADMIN".equals(user.getRole());
}
/**
* 清除用户会话
*/
public void clearSession(String sessionId) {
User user = sessions.remove(sessionId);
if (user != null) {
userSessions.remove(user.getUsername());
}
}
/**
* 清除用户的所有会话
*/
public void clearUserSessions(String username) {
String sessionId = userSessions.remove(username);
if (sessionId != null) {
sessions.remove(sessionId);
}
}
/**
* 获取当前活跃会话数量
*/
public int getActiveSessionCount() {
return sessions.size();
}
}

View File

@@ -0,0 +1,65 @@
package com.nesoft;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Student {
private String studentId;
private String name;
private double gpa; // 现在支持四位小数
private String college;
public Student() {
}
public Student(String studentId, String name, double gpa, String college) {
this.studentId = studentId;
this.name = name;
// 确保绩点精确到四位小数
this.gpa = new BigDecimal(gpa).setScale(4, RoundingMode.HALF_UP).doubleValue();
this.college = college;
}
public String getStudentId() {
return studentId;
}
public void setStudentId(String studentId) {
this.studentId = studentId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getGpa() {
return gpa;
}
public void setGpa(double gpa) {
// 确保绩点精确到四位小数
this.gpa = new BigDecimal(gpa).setScale(4, RoundingMode.HALF_UP).doubleValue();
}
public String getCollege() {
return college;
}
public void setCollege(String college) {
this.college = college;
}
@Override
public String toString() {
return "Student{" +
"studentId='" + studentId + '\'' +
", name='" + name + '\'' +
", gpa=" + gpa +
", college='" + college + '\'' +
'}';
}
}

View File

@@ -0,0 +1,407 @@
package com.nesoft;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
@RequestMapping("/api")
@CrossOrigin(origins = "*")
public class StudentController {
private static final Logger logger = LoggerFactory.getLogger(StudentController.class);
@Autowired
private StudentDAO studentDAO;
@Autowired
private SessionService sessionService;
// 获取所有学生
@GetMapping("/students/all")
public ResponseEntity<?> getAllStudents() {
try {
List<Student> students = studentDAO.findAll();
return ResponseEntity.ok(new StudentListResponse(students));
} catch (SQLException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("查询失败: " + e.getMessage()));
}
}
// 根据学号获取学生
@GetMapping("/students/{studentId}")
public ResponseEntity<?> getStudentByStudentId(@PathVariable String studentId) {
try {
Student student = studentDAO.findByStudentId(studentId);
if (student != null) {
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("学生绩点数据异常: " + student.getGpa()));
}
return ResponseEntity.ok(student);
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("未找到学号为 " + studentId + " 的学生"));
}
} catch (SQLException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("查询失败: " + e.getMessage()));
}
}
// 根据学号修改学生信息 - 只有管理员可以操作
@PutMapping("/students/{studentId}")
public ResponseEntity<?> updateStudentByStudentId(@PathVariable String studentId, @RequestBody Student student,
@RequestHeader(value = "X-Session-Id", required = false) String sessionId) {
try {
// 检查是否是管理员
if (!sessionService.isAdmin(sessionId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("只有管理员可以修改学生信息"));
}
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("绩点必须在0到5.0之间"));
}
// 确保学号一致
if (!studentId.equals(student.getStudentId())) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("学号不匹配"));
}
// 检查学生是否存在
Student existingStudent = studentDAO.findByStudentId(studentId);
if (existingStudent == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("未找到学号为 " + studentId + " 的学生"));
}
// 更新学生信息
studentDAO.updateStudent(student);
return ResponseEntity.ok(student);
} catch (SQLException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("修改学生信息失败: " + e.getMessage()));
}
}
// 根据条件查询学生
@GetMapping("/students")
public ResponseEntity<?> searchStudents(
@RequestParam(required = false) String name,
@RequestParam(required = false) Double gpa,
@RequestParam(required = false) String college) {
try {
List<Student> students = new ArrayList<>();
if (name != null) {
students = studentDAO.findByName(name);
} else if (gpa != null) {
// 验证绩点范围
if (gpa < 0 || gpa > 5.0) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("绩点必须在0到5.0之间"));
}
students = studentDAO.findByGpa(gpa);
} else if (college != null) {
students = studentDAO.findByCollege(college);
} else {
students = studentDAO.findAll();
}
// 验证所有学生的绩点范围
for (Student student : students) {
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("学生绩点数据异常: " + student.getGpa()));
}
}
return ResponseEntity.ok(new StudentListResponse(students));
} catch (SQLException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("查询失败: " + e.getMessage()));
}
}
// 添加学生 - 只有管理员可以操作
@PostMapping("/students")
public ResponseEntity<?> addStudent(@RequestBody Student student,
@RequestHeader(value = "X-Session-Id", required = false) String sessionId) {
try {
// 检查是否是管理员
if (!sessionService.isAdmin(sessionId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("只有管理员可以添加学生"));
}
// 输入验证
if (student.getStudentId() == null || student.getStudentId().trim().isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("学号不能为空"));
}
if (student.getName() == null || student.getName().trim().isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("姓名不能为空"));
}
if (student.getCollege() == null || student.getCollege().trim().isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("学院不能为空"));
}
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("绩点必须在0到5.0之间"));
}
// 验证学号格式(只允许字母数字)
if (!student.getStudentId().matches("^[a-zA-Z0-9]+$")) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("学号只能包含字母和数字"));
}
// 检查学号是否已存在
Student existingStudent = studentDAO.findByStudentId(student.getStudentId());
if (existingStudent != null) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(new ErrorResponse("学号已存在: " + student.getStudentId()));
}
studentDAO.insertStudent(student);
return ResponseEntity.status(HttpStatus.CREATED).body(student);
} catch (SQLException e) {
logger.error("Add student failed: ", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("添加学生失败: " + e.getMessage()));
}
}
// 按姓名筛选
@GetMapping("/filter/name")
public ResponseEntity<?> filterByName(
@RequestParam String name,
@RequestParam boolean currentFilter,
@RequestParam(required = false) String currentStudentIds) {
try {
List<Student> filteredStudents = new ArrayList<>();
if (currentFilter && currentStudentIds != null && !currentStudentIds.trim().isEmpty()) {
// 从当前筛选结果中进一步筛选
String[] studentIds = currentStudentIds.split(",");
for (String studentId : studentIds) {
studentId = studentId.trim();
if (!studentId.isEmpty()) {
Student student = studentDAO.findByStudentId(studentId);
if (student != null && student.getName().contains(name)) {
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("学生绩点数据异常: " + student.getGpa()));
}
filteredStudents.add(student);
}
}
}
} else {
// 从所有学生中筛选
filteredStudents = studentDAO.findByName(name);
}
// 验证所有学生的绩点范围
for (Student student : filteredStudents) {
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("学生绩点数据异常: " + student.getGpa()));
}
}
return ResponseEntity.ok(new StudentListResponse(filteredStudents));
} catch (SQLException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("筛选失败: " + e.getMessage()));
}
}
// 按绩点筛选
@GetMapping("/filter/gpa")
public ResponseEntity<?> filterByGpa(
@RequestParam double gpa,
@RequestParam boolean currentFilter,
@RequestParam(required = false) String currentStudentIds) {
try {
// 验证绩点范围
if (gpa < 0 || gpa > 5.0) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("绩点必须在0到5.0之间"));
}
List<Student> filteredStudents = new ArrayList<>();
if (currentFilter && currentStudentIds != null && !currentStudentIds.trim().isEmpty()) {
// 从当前筛选结果中进一步筛选
String[] studentIds = currentStudentIds.split(",");
for (String studentId : studentIds) {
studentId = studentId.trim();
if (!studentId.isEmpty()) {
Student student = studentDAO.findByStudentId(studentId);
if (student != null && student.getGpa() >= gpa) {
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("学生绩点数据异常: " + student.getGpa()));
}
filteredStudents.add(student);
}
}
}
} else {
// 从所有学生中筛选
filteredStudents = studentDAO.findByGpa(gpa);
}
// 验证所有学生的绩点范围
for (Student student : filteredStudents) {
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("学生绩点数据异常: " + student.getGpa()));
}
}
return ResponseEntity.ok(new StudentListResponse(filteredStudents));
} catch (SQLException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("筛选失败: " + e.getMessage()));
}
}
// 按学院筛选
@GetMapping("/filter/college")
public ResponseEntity<?> filterByCollege(
@RequestParam String college,
@RequestParam boolean currentFilter,
@RequestParam(required = false) String currentStudentIds) {
try {
List<Student> filteredStudents = new ArrayList<>();
if (currentFilter && currentStudentIds != null && !currentStudentIds.trim().isEmpty()) {
// 从当前筛选结果中进一步筛选
String[] studentIds = currentStudentIds.split(",");
for (String studentId : studentIds) {
studentId = studentId.trim();
if (!studentId.isEmpty()) {
Student student = studentDAO.findByStudentId(studentId);
if (student != null && student.getCollege().contains(college)) {
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("学生绩点数据异常: " + student.getGpa()));
}
filteredStudents.add(student);
}
}
}
} else {
// 从所有学生中筛选
filteredStudents = studentDAO.findByCollege(college);
}
// 验证所有学生的绩点范围
for (Student student : filteredStudents) {
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("学生绩点数据异常: " + student.getGpa()));
}
}
return ResponseEntity.ok(new StudentListResponse(filteredStudents));
} catch (SQLException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("筛选失败: " + e.getMessage()));
}
}
// 内部类用于统一响应格式
static class StudentListResponse {
private List<Student> students;
public StudentListResponse(List<Student> students) {
this.students = students;
}
public List<Student> getStudents() {
return students;
}
public void setStudents(List<Student> students) {
this.students = students;
}
}
// 错误响应类
static class ErrorResponse {
private String error;
public ErrorResponse(String error) {
this.error = error;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
// 删除学生 - 只有管理员可以操作
@DeleteMapping("/students/{studentId}")
public ResponseEntity<?> deleteStudent(@PathVariable String studentId,
@RequestHeader(value = "X-Session-Id", required = false) String sessionId) {
try {
// 检查是否是管理员
if (!sessionService.isAdmin(sessionId)) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("只有管理员可以删除学生"));
}
// 检查学生是否存在
Student existingStudent = studentDAO.findByStudentId(studentId);
if (existingStudent == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("未找到学号为 " + studentId + " 的学生"));
}
// 删除学生
studentDAO.deleteStudent(studentId);
Map<String, Object> response = new HashMap<>();
response.put("message", "学生删除成功");
response.put("studentId", studentId);
response.put("studentName", existingStudent.getName());
return ResponseEntity.ok(response);
} catch (SQLException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("删除学生失败: " + e.getMessage()));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("系统错误: " + e.getMessage()));
}
}
}

View File

@@ -0,0 +1,299 @@
package com.nesoft;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class StudentDAO {
@Autowired
private DatabaseConfig databaseConfig;
// 创建学生表
public void createTable() throws SQLException {
try (Connection conn = databaseConfig.getConnection();
Statement stmt = conn.createStatement()) {
// 首先尝试创建表(如果不存在)
String createSql = "CREATE TABLE IF NOT EXISTS students (" +
"student_id VARCHAR(20) PRIMARY KEY, " +
"name VARCHAR(100) NOT NULL, " +
"gpa DECIMAL(6,4) NOT NULL, " + // 修改为支持4位小数
"college VARCHAR(100) NOT NULL DEFAULT ''" +
")";
stmt.execute(createSql);
// 检查是否已存在college列如果不存在则添加
addCollegeColumnIfNotExists(conn);
// 检查并更新gpa列的精度如果需要
updateGpaColumnPrecision(conn);
}
}
// 检查并更新gpa列的精度
private void updateGpaColumnPrecision(Connection conn) throws SQLException {
try {
DatabaseMetaData metaData = conn.getMetaData();
try (ResultSet rs = metaData.getColumns(null, null, "students", "gpa")) {
if (rs.next()) {
String typeName = rs.getString("TYPE_NAME");
int decimalDigits = rs.getInt("DECIMAL_DIGITS");
// 如果当前精度小于4则尝试修改列定义
if (decimalDigits < 4) {
try (Statement stmt = conn.createStatement()) {
// 注意:不是所有数据库都支持直接修改列精度
// 这里使用一种较为兼容的方式
String alterSql = "ALTER TABLE students MODIFY COLUMN gpa DECIMAL(6,4) NOT NULL";
stmt.executeUpdate(alterSql);
} catch (SQLException e) {
// 如果直接修改失败,可以考虑其他策略
System.out.println("Warning: Could not update GPA column precision directly: " + e.getMessage());
}
}
}
}
} catch (SQLException e) {
System.out.println("Warning: Could not check GPA column precision: " + e.getMessage());
}
}
// 检查并添加college列用于向后兼容
private void addCollegeColumnIfNotExists(Connection conn) throws SQLException {
try {
DatabaseMetaData metaData = conn.getMetaData();
try (ResultSet rs = metaData.getColumns(null, null, "students", "college")) {
if (!rs.next()) {
// 如果college列不存在则添加
try (Statement stmt = conn.createStatement()) {
String alterSql = "ALTER TABLE students ADD COLUMN college VARCHAR(100) NOT NULL DEFAULT ''";
stmt.executeUpdate(alterSql);
}
}
}
} catch (SQLException e) {
// 忽略可能的错误,如列已存在等
// 这可能是由于列已经存在或其他数据库特定的行为
}
}
// 插入学生记录
public void insertStudent(Student student) throws SQLException {
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
throw new SQLException("绩点必须在0到5.0之间,当前值: " + student.getGpa());
}
String sql = "INSERT INTO students (student_id, name, gpa, college) VALUES (?, ?, ?, ?) " +
"ON DUPLICATE KEY UPDATE name=?, gpa=?, college=?";
try (Connection conn = databaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, student.getStudentId());
pstmt.setString(2, student.getName());
pstmt.setDouble(3, student.getGpa());
pstmt.setString(4, student.getCollege());
// 为ON DUPLICATE KEY UPDATE部分设置参数
pstmt.setString(5, student.getName());
pstmt.setDouble(6, student.getGpa());
pstmt.setString(7, student.getCollege());
pstmt.executeUpdate();
}
}
// 更新学生记录
public void updateStudent(Student student) throws SQLException {
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
throw new SQLException("绩点必须在0到5.0之间,当前值: " + student.getGpa());
}
String sql = "UPDATE students SET name=?, gpa=?, college=? WHERE student_id=?";
try (Connection conn = databaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, student.getName());
pstmt.setDouble(2, student.getGpa());
pstmt.setString(3, student.getCollege());
pstmt.setString(4, student.getStudentId());
pstmt.executeUpdate();
}
}
// 根据学号查找学生
public Student findByStudentId(String studentId) throws SQLException {
String sql = "SELECT * FROM students WHERE student_id = ?";
try (Connection conn = databaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, studentId);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
Student student = new Student();
student.setStudentId(rs.getString("student_id"));
student.setName(rs.getString("name"));
student.setGpa(rs.getDouble("gpa"));
student.setCollege(rs.getString("college"));
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
throw new SQLException("学生绩点数据异常: " + student.getGpa());
}
return student;
}
}
}
return null;
}
// 根据姓名查找学生(可能返回多个结果)
public List<Student> findByName(String name) throws SQLException {
List<Student> students = new ArrayList<>();
String sql = "SELECT * FROM students WHERE name LIKE ? ORDER BY gpa DESC";
try (Connection conn = databaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "%" + name + "%");
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
Student student = new Student();
student.setStudentId(rs.getString("student_id"));
student.setName(rs.getString("name"));
student.setGpa(rs.getDouble("gpa"));
student.setCollege(rs.getString("college"));
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
throw new SQLException("学生绩点数据异常: " + student.getGpa());
}
students.add(student);
}
}
}
return students;
}
// 根据绩点查找学生(查找绩点大于等于指定值的学生)
public List<Student> findByGpa(double gpa) throws SQLException {
// 验证绩点范围
if (gpa < 0 || gpa > 5.0) {
throw new SQLException("绩点必须在0到5.0之间,当前值: " + gpa);
}
List<Student> students = new ArrayList<>();
String sql = "SELECT * FROM students WHERE gpa >= ? ORDER BY gpa DESC";
try (Connection conn = databaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setDouble(1, gpa);
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
Student student = new Student();
student.setStudentId(rs.getString("student_id"));
student.setName(rs.getString("name"));
student.setGpa(rs.getDouble("gpa"));
student.setCollege(rs.getString("college"));
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
throw new SQLException("学生绩点数据异常: " + student.getGpa());
}
students.add(student);
}
}
}
return students;
}
// 根据学院查找学生
public List<Student> findByCollege(String college) throws SQLException {
List<Student> students = new ArrayList<>();
String sql = "SELECT * FROM students WHERE college LIKE ? ORDER BY gpa DESC";
try (Connection conn = databaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "%" + college + "%");
try (ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
Student student = new Student();
student.setStudentId(rs.getString("student_id"));
student.setName(rs.getString("name"));
student.setGpa(rs.getDouble("gpa"));
student.setCollege(rs.getString("college"));
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
throw new SQLException("学生绩点数据异常: " + student.getGpa());
}
students.add(student);
}
}
}
return students;
}
// 获取所有学生
public List<Student> findAll() throws SQLException {
List<Student> students = new ArrayList<>();
String sql = "SELECT * FROM students ORDER BY gpa DESC";
try (Connection conn = databaseConfig.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
Student student = new Student();
student.setStudentId(rs.getString("student_id"));
student.setName(rs.getString("name"));
student.setGpa(rs.getDouble("gpa"));
student.setCollege(rs.getString("college"));
// 验证绩点范围
if (student.getGpa() < 0 || student.getGpa() > 5.0) {
throw new SQLException("学生绩点数据异常: " + student.getGpa());
}
students.add(student);
}
}
return students;
}
// 删除指定学生记录
public boolean deleteStudent(String studentId) throws SQLException {
String sql = "DELETE FROM students WHERE student_id = ?";
try (Connection conn = databaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, studentId);
int rowsAffected = pstmt.executeUpdate();
return rowsAffected > 0;
}
}
// 删除所有学生记录(用于测试)
public void deleteAll() throws SQLException {
String sql = "DELETE FROM students";
try (Connection conn = databaseConfig.getConnection();
Statement stmt = conn.createStatement()) {
stmt.executeUpdate(sql);
}
}
}

View File

@@ -0,0 +1,59 @@
package com.nesoft;
public class User {
private Long id;
private String username;
private String password;
private String role;
public User() {
}
public User(String username, String password, String role) {
this.username = username;
this.password = password;
this.role = role;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", role='" + role + '\'' +
'}';
}
}

View File

@@ -0,0 +1,113 @@
package com.nesoft;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
@Repository
public class UserDAO {
@Autowired
private DatabaseConfig databaseConfig;
// 创建用户表
public void createUserTable() throws SQLException {
try (Connection conn = databaseConfig.getConnection();
Statement stmt = conn.createStatement()) {
String createSql = "CREATE TABLE IF NOT EXISTS users (" +
"id INT AUTO_INCREMENT PRIMARY KEY, " +
"username VARCHAR(50) UNIQUE NOT NULL, " +
"password VARCHAR(100) NOT NULL, " +
"role VARCHAR(20) NOT NULL DEFAULT 'USER'" +
")";
stmt.execute(createSql);
}
}
// 插入用户记录
public void insertUser(User user) throws SQLException {
String sql = "INSERT INTO users (username, password, role) VALUES (?, ?, ?)";
try (Connection conn = databaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, user.getUsername());
pstmt.setString(2, user.getPassword());
pstmt.setString(3, user.getRole());
pstmt.executeUpdate();
}
}
// 根据用户名查找用户
public User findByUsername(String username) throws SQLException {
String sql = "SELECT * FROM users WHERE username = ?";
try (Connection conn = databaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setRole(rs.getString("role"));
return user;
}
}
}
return null;
}
// 获取所有用户
public List<User> findAll() throws SQLException {
List<User> users = new ArrayList<>();
String sql = "SELECT * FROM users";
try (Connection conn = databaseConfig.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setPassword(rs.getString("password"));
user.setRole(rs.getString("role"));
users.add(user);
}
}
return users;
}
// 根据用户名删除用户
public void deleteByUsername(String username) throws SQLException {
String sql = "DELETE FROM users WHERE username = ?";
try (Connection conn = databaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username);
pstmt.executeUpdate();
}
}
// 更新用户信息
public void updateUser(User user) throws SQLException {
String sql = "UPDATE users SET password = ?, role = ? WHERE username = ?";
try (Connection conn = databaseConfig.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, user.getPassword());
pstmt.setString(2, user.getRole());
pstmt.setString(3, user.getUsername());
pstmt.executeUpdate();
}
}
}

View File

@@ -0,0 +1,12 @@
spring.datasource.url=jdbc:mysql://localhost:3306/gpa_system?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.properties.hibernate.format_sql=true
server.port=8080

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>前端调试页面</title>
</head>
<body>
<h1>前端调试页面</h1>
<button onclick="testAPI()">测试API调用</button>
<button onclick="testLocalStorage()">测试LocalStorage</button>
<div id="result"></div>
<script>
function testAPI() {
console.log('开始测试API...');
fetch('/api/students/all')
.then(response => {
console.log('API响应状态:', response.status);
if (!response.ok) {
throw new Error('网络响应错误: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('API数据:', data);
document.getElementById('result').innerHTML =
'<h3>API测试成功</h3>' +
'<p>学生数量: ' + data.students.length + '</p>' +
'<pre>' + JSON.stringify(data, null, 2) + '</pre>';
})
.catch(error => {
console.error('API错误:', error);
document.getElementById('result').innerHTML =
'<h3>API测试失败</h3>' +
'<p>错误: ' + error.message + '</p>';
});
}
function testLocalStorage() {
console.log('测试LocalStorage...');
const username = localStorage.getItem('username');
const role = localStorage.getItem('role');
document.getElementById('result').innerHTML =
'<h3>LocalStorage测试</h3>' +
'<p>用户名: ' + (username || '未设置') + '</p>' +
'<p>角色: ' + (role || '未设置') + '</p>';
console.log('LocalStorage - username:', username);
console.log('LocalStorage - role:', role);
}
// 页面加载时自动测试
document.addEventListener('DOMContentLoaded', function() {
console.log('调试页面加载完成');
testLocalStorage();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,933 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生绩点管理系统</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎓</text></svg>">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<h1>学生绩点管理系统</h1>
<!-- 用户信息和登出按钮 -->
<div id="user-info" class="user-info" style="text-align: right; margin-bottom: 20px;">
<div class="user-welcome-card">
<div class="user-avatar">👤</div>
<div class="user-details">
<span id="username-display" class="username-text"></span>
<span id="user-role-display" class="user-role-text"></span>
</div>
</div>
<button id="register-button" class="btn btn-success" style="display: none; margin-right: 10px;" onclick="showRegisterForm()">
<span class="icon"></span> 注册用户
</button>
<button id="user-management-button" class="btn btn-info" style="display: none; margin-right: 10px;" onclick="showUserManagement()">
<span class="icon">👥</span> 用户管理
</button>
<button id="logout-button" class="btn btn-danger" style="display: none;" onclick="logout()">
<span class="icon"> 🔑</span> 登出
</button>
</div>
<!-- 注册用户表单 -->
<div id="register-user-form" class="form-container" style="display: none;">
<h2>注册新用户</h2>
<form id="user-register-form">
<div class="form-group">
<label for="new-username">用户名:</label>
<input type="text" id="new-username" required>
</div>
<div class="form-group">
<label for="new-password">密码:</label>
<input type="password" id="new-password" required>
</div>
<div class="form-group">
<label for="confirm-password">确认密码:</label>
<input type="password" id="confirm-password" required>
</div>
<div class="form-buttons">
<button type="submit" class="btn btn-success">
<span class="icon"></span> 注册
</button>
<button type="button" onclick="hideRegisterForm()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</form>
</div>
<!-- 用户管理界面 -->
<div id="user-management-form" class="form-container" style="display: none;">
<h2>用户管理</h2>
<!-- 用户列表 -->
<div id="user-list-container">
<h3>用户列表</h3>
<div id="user-list" class="table-container">
<!-- 用户列表将在这里动态生成 -->
</div>
<button type="button" onclick="refreshUserList()" class="btn btn-info">
<span class="icon">🔄</span> 刷新列表
</button>
</div>
<!-- 修改密码表单 -->
<div id="change-password-section" style="margin-top: 30px; display: none;">
<h3>修改用户密码</h3>
<form id="change-password-form">
<input type="hidden" id="target-username">
<div class="form-group">
<label for="change-new-password">新密码:</label>
<input type="password" id="change-new-password" required>
</div>
<div class="form-group">
<label for="change-confirm-password">确认密码:</label>
<input type="password" id="change-confirm-password" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-warning">
<span class="icon">🔑</span> 修改密码
</button>
<button type="button" onclick="hideChangePassword()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</form>
</div>
<div class="form-actions" style="margin-top: 20px;">
<button type="button" onclick="hideUserManagement()" class="btn btn-secondary">
<span class="icon"></span> 关闭
</button>
</div>
</div>
<!-- 文件上传表单 -->
<div id="upload-form" class="form-container" style="display: none;">
<h2>导入学生数据文件</h2>
<div class="upload-info">
<h3>支持的文件格式</h3>
<p><strong>TXT文件支持多种数据格式</strong></p>
<ul>
<li><strong>格式1</strong>学号 + 姓名20226660 施天翔)</li>
<li><strong>格式2</strong>学号 + 姓名 + 学院2023001 张三 计算机学院)</li>
<li><strong>格式3</strong>学号 + 姓名 + 绩点2023001 张三 3.8</li>
<li><strong>格式4</strong>学号 + 姓名 + 绩点 + 学院2023001 张三 3.8 计算机学院)</li>
</ul>
<p><strong>处理规则:</strong></p>
<ul>
<li>格式1新增学生绩点=0学院为空或更新姓名</li>
<li>格式2新增学生绩点=0或更新姓名和学院</li>
<li>格式3新增学生学院为空或仅更新绩点</li>
<li>格式4新增完整学生信息或仅更新绩点</li>
</ul>
<p><strong>注意事项:</strong></p>
<ul>
<li>学号只能包含字母和数字</li>
<li>绩点范围0.0到5.0之间</li>
<li>各字段用空格分隔</li>
<li>文件编码UTF-8</li>
<li>系统会自动识别格式</li>
</ul>
</div>
<form id="file-upload-form" enctype="multipart/form-data">
<div class="form-group">
<label for="student-file">选择TXT文件:</label>
<input type="file" id="student-file" name="file" accept=".txt" required>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">
<span class="icon">📤</span> 上传并处理
</button>
<button type="button" onclick="hideUploadForm()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</form>
<!-- 处理结果显示 -->
<div id="upload-result" style="display: none; margin-top: 20px;">
<h3>处理结果</h3>
<div id="upload-summary"></div>
<div id="upload-errors" style="margin-top: 10px;"></div>
</div>
</div>
<!-- 筛选结果显示区域 -->
<div id="filter-info" class="filter-info"></div>
<!-- 操作菜单 -->
<div class="menu">
<button onclick="showAllStudents()" class="btn btn-primary">
<span class="icon">📋</span> 显示所有学生
</button>
<button id="add-student-button" onclick="showAddStudentForm()" class="btn btn-success" style="display: none;">
<span class="icon"></span> 添加学生
</button>
<button id="upload-file-button" onclick="showUploadForm()" class="btn btn-info" style="display: none; margin-left: 10px;">
<span class="icon">📁</span> 导入文件
</button>
<button onclick="showSearchForm()" class="btn btn-info">
<span class="icon">🔍</span> 查找学生
</button>
<button onclick="showFilterForm()" class="btn btn-warning">
<span class="icon">⚙️</span> 多条件筛选
</button>
<button onclick="showFilteredStudents()" class="btn btn-secondary">
<span class="icon">📊</span> 显示筛选结果
</button>
<button onclick="clearFilter()" class="btn btn-danger">
<span class="icon">🗑️</span> 清除筛选
</button>
</div>
<!-- 添加学生表单 -->
<div id="add-student-form" class="form-container" style="display: none;">
<h2>添加学生</h2>
<form id="student-form">
<div class="form-group">
<label for="student-id">学号:</label>
<input type="text" id="student-id" required>
</div>
<div class="form-group">
<label for="name">姓名:</label>
<input type="text" id="name" required>
</div>
<div class="form-group">
<label for="gpa">绩点:</label>
<input type="number" id="gpa" step="0.0001" min="0" max="5.0" required>
</div>
<div class="form-group">
<label for="college">学院:</label>
<input type="text" id="college" required>
</div>
<div class="form-buttons">
<button type="submit" class="btn btn-success">
<span class="icon"></span> 添加
</button>
<button type="button" onclick="hideAddStudentForm()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</form>
</div>
<!-- 修改学生表单 -->
<div id="edit-student-form" class="form-container" style="display: none;">
<h2>修改学生信息</h2>
<form id="student-edit-form">
<div class="form-group">
<label for="edit-student-id">学号:</label>
<input type="text" id="edit-student-id" readonly>
</div>
<div class="form-group">
<label for="edit-name">姓名:</label>
<input type="text" id="edit-name" required>
</div>
<div class="form-group">
<label for="edit-gpa">绩点:</label>
<input type="number" id="edit-gpa" step="0.0001" min="0" max="5.0" required>
</div>
<div class="form-group">
<label for="edit-college">学院:</label>
<input type="text" id="edit-college" required>
</div>
<div class="form-buttons">
<button type="submit" class="btn btn-success">
<span class="icon">💾</span> 保存
</button>
<button type="button" onclick="hideEditStudentForm()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</form>
</div>
<!-- 查找学生表单 -->
<div id="search-form" class="form-container" style="display: none;">
<h2>查找学生</h2>
<div class="search-options">
<button onclick="showSearchByIdForm()" class="btn btn-info">
<span class="icon">🆔</span> 按学号查找
</button>
<button onclick="showSearchByNameForm()" class="btn btn-info">
<span class="icon">👤</span> 按姓名查找
</button>
<button onclick="showSearchByGpaForm()" class="btn btn-info">
<span class="icon">📈</span> 按绩点查找
</button>
<button onclick="showSearchByCollegeForm()" class="btn btn-info">
<span class="icon">🏫</span> 按学院查找
</button>
<button onclick="hideSearchForm()" class="btn btn-secondary">
<span class="icon">⬅️</span> 返回
</button>
</div>
<div id="search-by-id-form" class="search-form" style="display: none;">
<h3>按学号查找</h3>
<div class="form-group">
<label for="search-student-id">学号:</label>
<input type="text" id="search-student-id" required>
</div>
<div class="form-buttons">
<button onclick="searchStudentByStudentId()" class="btn btn-primary">
<span class="icon">🔍</span> 查找
</button>
<button onclick="hideSearchByIdForm()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</div>
<div id="search-by-name-form" class="search-form" style="display: none;">
<h3>按姓名查找</h3>
<div class="form-group">
<label for="search-name">姓名:</label>
<input type="text" id="search-name" required>
</div>
<div class="form-buttons">
<button onclick="searchStudentByName()" class="btn btn-primary">
<span class="icon">🔍</span> 查找
</button>
<button onclick="hideSearchByNameForm()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</div>
<div id="search-by-gpa-form" class="search-form" style="display: none;">
<h3>按绩点查找</h3>
<div class="form-group">
<label for="search-gpa">绩点:</label>
<input type="number" id="search-gpa" step="0.0001" min="0" max="5.0" required>
</div>
<div class="form-buttons">
<button onclick="searchStudentByGpa()" class="btn btn-primary">
<span class="icon">🔍</span> 查找
</button>
<button onclick="hideSearchByGpaForm()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</div>
<div id="search-by-college-form" class="search-form" style="display: none;">
<h3>按学院查找</h3>
<div class="form-group">
<label for="search-college">学院:</label>
<input type="text" id="search-college" required>
</div>
<div class="form-buttons">
<button onclick="searchStudentByCollege()" class="btn btn-primary">
<span class="icon">🔍</span> 查找
</button>
<button onclick="hideSearchByCollegeForm()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</div>
</div>
<!-- 筛选表单 -->
<div id="filter-form" class="form-container" style="display: none;">
<h2>多条件筛选</h2>
<div class="filter-options">
<button onclick="showFilterByNameForm()" class="btn btn-warning">
<span class="icon">👤</span> 按姓名筛选
</button>
<button onclick="showFilterByGpaForm()" class="btn btn-warning">
<span class="icon">📈</span> 按绩点筛选
</button>
<button onclick="showFilterByCollegeForm()" class="btn btn-warning">
<span class="icon">🏫</span> 按学院筛选
</button>
<button onclick="hideFilterForm()" class="btn btn-secondary">
<span class="icon">⬅️</span> 返回
</button>
</div>
<div id="filter-by-name-form" class="filter-form" style="display: none;">
<h3>按姓名筛选</h3>
<div class="form-group">
<label for="filter-name">姓名关键字:</label>
<input type="text" id="filter-name" required>
</div>
<div class="form-buttons">
<button onclick="filterByName()" class="btn btn-warning">
<span class="icon">⚙️</span> 筛选
</button>
<button onclick="hideFilterByNameForm()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</div>
<div id="filter-by-gpa-form" class="filter-form" style="display: none;">
<h3>按绩点筛选</h3>
<div class="form-group">
<label for="filter-gpa">最低绩点:</label>
<input type="number" id="filter-gpa" step="0.0001" min="0" max="5.0" required>
</div>
<div class="form-buttons">
<button onclick="filterByGpa()" class="btn btn-warning">
<span class="icon">⚙️</span> 筛选
</button>
<button onclick="hideFilterByGpaForm()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</div>
<div id="filter-by-college-form" class="filter-form" style="display: none;">
<h3>按学院筛选</h3>
<div class="form-group">
<label for="filter-college">学院关键字:</label>
<input type="text" id="filter-college" required>
</div>
<div class="form-buttons">
<button onclick="filterByCollege()" class="btn btn-warning">
<span class="icon">⚙️</span> 筛选
</button>
<button onclick="hideFilterByCollegeForm()" class="btn btn-secondary">
<span class="icon"></span> 取消
</button>
</div>
</div>
</div>
<!-- 学生表格 -->
<div id="students-table" class="table-container">
<div class="table-header">
<h2>学生列表</h2>
<div class="loading" id="loading" style="display: none;">
<span class="spinner"></span> 数据加载中...
</div>
</div>
<table id="student-table">
<thead>
<tr>
<th>学号</th>
<th>姓名</th>
<th>绩点</th>
<th>学院</th>
<th>操作</th>
</tr>
</thead>
<tbody id="student-table-body">
<!-- 学生数据将通过JavaScript动态填充 -->
</tbody>
</table>
</div>
<!-- 消息提示 -->
<div id="message" class="message"></div>
</div>
<script src="script.js"></script>
<script>
// 页面加载时检查用户登录状态
document.addEventListener('DOMContentLoaded', function() {
checkLoginStatus();
});
// 检查登录状态
function checkLoginStatus() {
const username = localStorage.getItem('username');
const role = localStorage.getItem('role');
const sessionId = localStorage.getItem('sessionId');
console.log('检查登录状态:', { username, role, sessionId });
if (username && role && sessionId) {
console.log('开始验证会话...');
// 验证会话有效性
fetch('/api/session/validate', {
method: 'GET',
headers: {
'X-Session-Id': sessionId,
'Content-Type': 'application/json'
}
})
.then(response => {
console.log('会话验证响应状态:', response.status);
if (response.ok) {
return response.json();
} else {
return response.text().then(text => {
console.error('会话验证失败响应:', text);
throw new Error('会话无效: ' + response.status);
});
}
})
.then(data => {
console.log('会话验证成功:', data);
document.getElementById('username-display').textContent = '欢迎, ' + data.username;
document.getElementById('user-role-display').textContent = data.role === 'ADMIN' ? '管理员' : '普通用户';
document.getElementById('logout-button').style.display = 'inline-block';
// 如果是管理员,显示管理员功能
if (data.role === 'ADMIN') {
document.getElementById('register-button').style.display = 'inline-block';
document.getElementById('user-management-button').style.display = 'inline-block';
document.getElementById('add-student-button').style.display = 'inline-block';
document.getElementById('upload-file-button').style.display = 'inline-block';
}
showAllStudents();
})
.catch(error => {
console.error('会话验证失败:', error);
alert('会话验证失败: ' + error.message + '\n将跳转到登录页面');
// 清除无效的登录信息
localStorage.removeItem('username');
localStorage.removeItem('role');
localStorage.removeItem('sessionId');
// 跳转到登录页面
window.location.href = '/login.html';
});
} else {
console.log('缺少登录信息,跳转到登录页面');
// 未登录,跳转到登录页面
window.location.href = '/login.html';
}
}
// 隐藏所有表单
function hideAllForms() {
// 隐藏注册表单
document.getElementById('register-user-form').style.display = 'none';
// 隐藏用户管理表单
document.getElementById('user-management-form').style.display = 'none';
// 隐藏文件上传表单
document.getElementById('upload-form').style.display = 'none';
// 隐藏其他可能的表单
const forms = document.querySelectorAll('.form-container');
forms.forEach(form => {
if (form.style.display === 'block') {
form.style.display = 'none';
}
});
}
// 显示注册表单
function showRegisterForm() {
hideAllForms();
document.getElementById('register-user-form').style.display = 'block';
document.getElementById('user-register-form').addEventListener('submit', handleRegisterUser);
}
// 隐藏注册表单
function hideRegisterForm() {
document.getElementById('register-user-form').style.display = 'none';
document.getElementById('user-register-form').removeEventListener('submit', handleRegisterUser);
document.getElementById('user-register-form').reset();
}
// 处理用户注册
function handleRegisterUser(event) {
event.preventDefault();
const username = document.getElementById('new-username').value.trim();
const password = document.getElementById('new-password').value;
const confirmPassword = document.getElementById('confirm-password').value;
// 简单验证
if (!username || !password || !confirmPassword) {
showMessage('请填写所有必填字段', 'error');
return;
}
if (password !== confirmPassword) {
showMessage('两次输入的密码不一致', 'error');
return;
}
if (password.length < 6) {
showMessage('密码长度至少为6位', 'error');
return;
}
const registerData = {
username: username,
password: password
};
const sessionId = localStorage.getItem('sessionId');
fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Session-Id': sessionId
},
body: JSON.stringify(registerData)
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(err => {
throw new Error(err.error);
});
}
})
.then(data => {
hideRegisterForm();
showMessage('用户注册成功!', 'success');
})
.catch(error => {
showMessage('注册失败: ' + error.message, 'error');
});
}
// ==================== 用户管理功能 ====================
// 显示用户管理界面
function showUserManagement() {
hideAllForms();
document.getElementById('user-management-form').style.display = 'block';
refreshUserList();
}
// 隐藏用户管理界面
function hideUserManagement() {
document.getElementById('user-management-form').style.display = 'none';
hideChangePassword();
}
// 刷新用户列表
function refreshUserList() {
const sessionId = localStorage.getItem('sessionId');
fetch('/api/users', {
method: 'GET',
headers: {
'X-Session-Id': sessionId
}
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(err => {
throw new Error(err.error);
});
}
})
.then(data => {
displayUserList(data.users);
})
.catch(error => {
showMessage('获取用户列表失败: ' + error.message, 'error');
});
}
// 显示用户列表
function displayUserList(users) {
const userListContainer = document.getElementById('user-list');
if (users.length === 0) {
userListContainer.innerHTML = '<p>暂无用户数据</p>';
return;
}
let html = `
<table class="student-table">
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>角色</th>
<th>操作</th>
</tr>
</thead>
<tbody>
`;
users.forEach(user => {
html += `
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.role === 'ADMIN' ? '管理员' : '普通用户'}</td>
<td>
<button onclick="showChangePassword('${user.username}')" class="btn btn-warning btn-sm">
<span class="icon">🔑</span> 修改密码
</button>
${user.username !== 'admin' ? `
<button onclick="deleteUser('${user.username}')" class="btn btn-danger btn-sm">
<span class="icon">🗑️</span> 删除
</button>
` : ''}
</td>
</tr>
`;
});
html += `
</tbody>
</table>
`;
userListContainer.innerHTML = html;
}
// 显示修改密码表单
function showChangePassword(username) {
document.getElementById('target-username').value = username;
document.getElementById('change-password-section').style.display = 'block';
document.getElementById('change-password-form').addEventListener('submit', handleChangePassword);
}
// 隐藏修改密码表单
function hideChangePassword() {
document.getElementById('change-password-section').style.display = 'none';
document.getElementById('change-password-form').removeEventListener('submit', handleChangePassword);
document.getElementById('change-password-form').reset();
}
// 处理修改密码
function handleChangePassword(event) {
event.preventDefault();
const username = document.getElementById('target-username').value;
const newPassword = document.getElementById('change-new-password').value;
const confirmPassword = document.getElementById('change-confirm-password').value;
if (newPassword !== confirmPassword) {
showMessage('两次输入的密码不一致', 'error');
return;
}
if (newPassword.length < 6) {
showMessage('密码长度至少6位', 'error');
return;
}
const sessionId = localStorage.getItem('sessionId');
fetch(`/api/users/${username}/password`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Session-Id': sessionId
},
body: JSON.stringify({ password: newPassword })
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(err => {
throw new Error(err.error);
});
}
})
.then(data => {
hideChangePassword();
showMessage('密码修改成功!', 'success');
refreshUserList();
})
.catch(error => {
showMessage('修改密码失败: ' + error.message, 'error');
});
}
// 删除用户
function deleteUser(username) {
if (!confirm(`确定要删除用户 "${username}" 吗?此操作不可恢复!`)) {
return;
}
const sessionId = localStorage.getItem('sessionId');
fetch(`/api/users/${username}`, {
method: 'DELETE',
headers: {
'X-Session-Id': sessionId
}
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(err => {
throw new Error(err.error);
});
}
})
.then(data => {
showMessage('用户删除成功!', 'success');
refreshUserList();
})
.catch(error => {
showMessage('删除用户失败: ' + error.message, 'error');
});
}
// ==================== 文件上传功能 ====================
// 显示文件上传表单
function showUploadForm() {
hideAllForms();
document.getElementById('upload-form').style.display = 'block';
document.getElementById('file-upload-form').addEventListener('submit', handleFileUpload);
// 重置表单和结果显示
document.getElementById('file-upload-form').reset();
document.getElementById('upload-result').style.display = 'none';
}
// 隐藏文件上传表单
function hideUploadForm() {
document.getElementById('upload-form').style.display = 'none';
document.getElementById('file-upload-form').removeEventListener('submit', handleFileUpload);
document.getElementById('upload-result').style.display = 'none';
}
// 处理文件上传
function handleFileUpload(event) {
event.preventDefault();
const fileInput = document.getElementById('student-file');
const file = fileInput.files[0];
if (!file) {
showMessage('请选择要上传的文件', 'error');
return;
}
// 验证文件类型
if (!file.name.toLowerCase().endsWith('.txt')) {
showMessage('只支持TXT文件格式', 'error');
return;
}
// 验证文件大小5MB
if (file.size > 5 * 1024 * 1024) {
showMessage('文件大小不能超过5MB', 'error');
return;
}
const sessionId = localStorage.getItem('sessionId');
const formData = new FormData();
formData.append('file', file);
// 显示上传进度
showMessage('正在上传和处理文件,请稍候...', 'info');
fetch('/api/upload/students', {
method: 'POST',
headers: {
'X-Session-Id': sessionId
},
body: formData
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(err => {
throw new Error(err.error || '文件上传失败');
});
}
})
.then(data => {
displayUploadResult(data);
showMessage('文件处理完成!', 'success');
// 刷新学生列表
showAllStudents();
})
.catch(error => {
showMessage('文件上传失败: ' + error.message, 'error');
});
}
// 显示上传结果
function displayUploadResult(result) {
const resultDiv = document.getElementById('upload-result');
const summaryDiv = document.getElementById('upload-summary');
const errorsDiv = document.getElementById('upload-errors');
// 显示处理摘要
let summaryHtml = `
<div class="upload-summary">
<h4>处理摘要</h4>
<p><strong>总行数:</strong>${result.totalLines}</p>
<p><strong>成功处理:</strong>${result.successCount}</p>
<p><strong>处理失败:</strong>${result.errorCount}</p>
<p><strong>新增记录:</strong>${result.addedCount}</p>
<p><strong>更新记录:</strong>${result.updatedCount}</p>
</div>
`;
summaryDiv.innerHTML = summaryHtml;
// 显示错误信息
if (result.errors && result.errors.length > 0) {
let errorsHtml = `
<div class="upload-errors">
<h4>错误详情</h4>
<div class="error-list">
`;
result.errors.forEach(error => {
errorsHtml += `
<div class="error-item">
<strong>第${error.line}行:</strong>${error.content}<br>
<span class="error-message">错误:${error.error}</span>
</div>
`;
});
errorsHtml += `
</div>
</div>
`;
errorsDiv.innerHTML = errorsHtml;
} else {
errorsDiv.innerHTML = '<p style="color: green;">所有数据处理成功,无错误!</p>';
}
resultDiv.style.display = 'block';
}
// 登出功能
function logout() {
const sessionId = localStorage.getItem('sessionId');
fetch('/api/logout', {
method: 'POST',
headers: {
'X-Session-Id': sessionId
}
})
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('登出失败');
}
})
.then(data => {
// 清除本地存储的用户信息
localStorage.removeItem('username');
localStorage.removeItem('role');
localStorage.removeItem('sessionId');
// 跳转到登录页面
window.location.href = '/login.html';
})
.catch(error => {
console.error('登出失败:', error);
// 即使服务器登出失败,也清除本地信息并跳转
localStorage.removeItem('username');
localStorage.removeItem('role');
localStorage.removeItem('sessionId');
window.location.href = '/login.html';
});
}
</script>
</body>
</html>

View File

@@ -0,0 +1,544 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生绩点查询系统 - 登录</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎓</text></svg>">
<link rel="stylesheet" href="styles.css">
<style>
/* 导入Google字体 */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
/* 登录页面特定样式 */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background-size: 400% 400%;
animation: gradientBg 15s ease infinite;
margin: 0;
overflow-x: hidden;
position: relative;
}
@keyframes gradientBg {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* 添加浮动粒子效果 */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 20% 80%, rgba(255, 255, 255, 0.1) 2px, transparent 2px),
radial-gradient(circle at 80% 20%, rgba(255, 255, 255, 0.1) 2px, transparent 2px),
radial-gradient(circle at 40% 40%, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
background-size: 100px 100px, 150px 150px, 80px 80px;
animation: float 20s ease-in-out infinite;
pointer-events: none;
z-index: 1;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
33% { transform: translateY(-20px) rotate(120deg); }
66% { transform: translateY(10px) rotate(240deg); }
}
.login-container {
max-width: 480px;
width: 100%;
padding: 40px 50px 50px 50px;
background: rgba(255, 255, 255, 0.98);
backdrop-filter: blur(30px);
border-radius: 32px;
box-shadow:
0 32px 64px rgba(0, 0, 0, 0.12),
0 16px 32px rgba(0, 0, 0, 0.08),
0 0 0 1px rgba(255, 255, 255, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.4);
border: 1px solid rgba(255, 255, 255, 0.3);
position: relative;
overflow: hidden;
animation: slideUp 0.8s cubic-bezier(0.16, 1, 0.3, 1);
z-index: 10;
}
/* 添加容器内部光效 */
.login-container::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: conic-gradient(from 0deg, transparent, rgba(255, 255, 255, 0.1), transparent);
animation: rotate 10s linear infinite;
pointer-events: none;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2, #667eea);
background-size: 200% 100%;
animation: shimmer 3s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { background-position: 200% 0; }
50% { background-position: -200% 0; }
}
.login-container h1 {
margin-bottom: 25px;
font-size: 2.8rem;
text-align: center;
font-weight: 800;
background: linear-gradient(135deg, #667eea, #764ba2, #f093fb, #667eea);
background-size: 300% 300%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradientShift 6s ease-in-out infinite;
letter-spacing: -0.03em;
position: relative;
z-index: 20;
}
/* 添加标题图标 */
.login-container h1::before {
content: '🎓';
display: block;
font-size: 3.2rem;
margin-bottom: 8px;
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% { transform: translateY(0); }
40% { transform: translateY(-10px); }
60% { transform: translateY(-5px); }
}
@keyframes gradientShift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.login-form .form-group {
margin-bottom: 30px;
position: relative;
}
.login-form .form-group label {
display: block;
margin-bottom: 12px;
font-weight: 600;
color: #555;
font-size: 15px;
text-transform: uppercase;
letter-spacing: 0.8px;
position: relative;
z-index: 20;
}
.login-form .form-group input {
width: 100%;
padding: 20px 24px;
border: 2px solid rgba(102, 126, 234, 0.2);
border-radius: 16px;
box-sizing: border-box;
font-size: 16px;
font-family: inherit;
background: rgba(255, 255, 255, 0.9);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(20px);
position: relative;
z-index: 20;
}
/* 添加输入框内部阴影效果 */
.login-form .form-group input::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 16px;
background: linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(255, 255, 255, 0.7));
z-index: -1;
}
.login-form .form-group input:focus {
border-color: #667eea;
outline: none;
box-shadow:
0 0 0 6px rgba(102, 126, 234, 0.15),
0 12px 35px rgba(102, 126, 234, 0.2),
inset 0 2px 4px rgba(255, 255, 255, 0.8);
background: rgba(255, 255, 255, 0.98);
transform: translateY(-3px) scale(1.02);
}
.login-form .form-group input::placeholder {
color: #aaa;
font-style: italic;
font-weight: 400;
}
/* 添加输入框悬停效果 */
.login-form .form-group input:hover {
border-color: rgba(102, 126, 234, 0.4);
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.1);
}
.login-form .btn {
width: 100%;
padding: 22px;
font-size: 17px;
margin-top: 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
background-size: 200% 200%;
color: white;
border: none;
border-radius: 18px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.2px;
position: relative;
overflow: hidden;
cursor: pointer;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
box-shadow:
0 8px 25px rgba(102, 126, 234, 0.3),
0 4px 12px rgba(118, 75, 162, 0.2);
animation: buttonGradient 3s ease infinite;
z-index: 20;
}
@keyframes buttonGradient {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.login-form .btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s;
}
.login-form .btn:hover::before {
left: 100%;
}
.login-form .btn:hover {
transform: translateY(-4px) scale(1.02);
box-shadow:
0 15px 40px rgba(102, 126, 234, 0.4),
0 8px 20px rgba(118, 75, 162, 0.3),
0 0 0 2px rgba(255, 255, 255, 0.3);
background-size: 150% 150%;
}
.login-form .btn:active {
transform: translateY(-2px) scale(0.98);
transition: transform 0.1s;
box-shadow:
0 8px 20px rgba(102, 126, 234, 0.3),
0 4px 10px rgba(118, 75, 162, 0.2);
}
.login-form .btn .icon {
font-size: 18px;
display: inline-block;
}
.login-form .btn .text {
display: inline-block;
font-size: 16px;
}
.register-link {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.register-link a {
color: #667eea;
text-decoration: none;
font-weight: 600;
transition: all 0.3s ease;
position: relative;
}
.register-link a::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 0.3s ease;
}
.register-link a:hover::after {
width: 100%;
}
.error-message {
color: #ff4757;
background: linear-gradient(135deg, rgba(255, 71, 87, 0.15), rgba(255, 71, 87, 0.08));
border: 2px solid rgba(255, 71, 87, 0.3);
padding: 18px 24px;
border-radius: 16px;
margin-bottom: 25px;
display: none;
font-weight: 600;
text-align: center;
backdrop-filter: blur(15px);
animation: slideIn 0.4s ease-out, shake 0.6s ease-in-out;
box-shadow: 0 8px 25px rgba(255, 71, 87, 0.2);
position: relative;
z-index: 20;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.success-message {
color: #2ed573;
background-color: rgba(46, 213, 115, 0.1);
border: 1px solid rgba(46, 213, 115, 0.3);
padding: 15px 20px;
border-radius: 12px;
margin-bottom: 25px;
display: none;
font-weight: 500;
text-align: center;
backdrop-filter: blur(10px);
animation: slideIn 0.3s ease-out;
}
/* 添加加载动画 */
.loading {
opacity: 0.7;
pointer-events: none;
}
.loading .btn {
background: linear-gradient(135deg, #ccc, #999);
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* 响应式设计 */
@media (max-width: 768px) {
body {
padding: 15px;
}
.login-container {
padding: 30px 30px 40px 30px;
margin: 10px;
border-radius: 24px;
}
.login-container h1 {
font-size: 2.2rem;
margin-bottom: 20px;
}
.login-container h1::before {
font-size: 2.8rem;
margin-bottom: 6px;
}
.login-form .form-group input {
padding: 18px 20px;
font-size: 16px;
}
.login-form .btn {
padding: 20px;
font-size: 16px;
}
}
@media (max-width: 480px) {
.login-container {
padding: 25px 25px 35px 25px;
}
.login-container h1 {
font-size: 1.8rem;
margin-bottom: 15px;
}
.login-container h1::before {
font-size: 2.3rem;
margin-bottom: 5px;
}
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>🎓 学生绩点查询系统</h1>
<p style="text-align: center; color: #666; margin-bottom: 25px; font-size: 16px;">
安全登录 · 数据管理
</p>
</div>
<div id="error-message" class="error-message"></div>
<div id="success-message" class="success-message"></div>
<form id="login-form" class="login-form">
<div class="form-group">
<label for="username">👤 用户名</label>
<input type="text" id="username" name="username" placeholder="请输入用户名" required autocomplete="username">
</div>
<div class="form-group">
<label for="password">🔒 密码</label>
<input type="password" id="password" name="password" placeholder="请输入密码" required autocomplete="current-password">
</div>
<button type="submit" class="btn btn-primary" id="login-btn">
<span class="icon">🚀</span>
<span class="text">登录系统</span>
</button>
</form>
<div class="login-footer">
<p style="text-align: center; color: #999; font-size: 14px; margin-top: 30px;">
© 学生绩点查询系统 · 安全可靠
</p>
</div>
</div>
<script>
document.getElementById('login-form').addEventListener('submit', function(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const loginData = {
username: username,
password: password
};
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(loginData)
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.text().then(text => {
try {
const errorData = JSON.parse(text);
throw new Error(errorData.error || '登录失败');
} catch (e) {
throw new Error('服务器错误: ' + response.status);
}
});
}
})
.then(data => {
console.log('登录成功,返回数据:', data);
// 保存登录信息到localStorage
localStorage.setItem('username', data.username);
localStorage.setItem('role', data.role);
localStorage.setItem('sessionId', data.sessionId);
console.log('已保存到localStorage:', {
username: data.username,
role: data.role,
sessionId: data.sessionId
});
// 登录成功,跳转到主页面
window.location.href = '/index.html';
})
.catch(error => {
document.getElementById('error-message').textContent = error.message;
document.getElementById('error-message').style.display = 'block';
setTimeout(() => {
document.getElementById('error-message').style.display = 'none';
}, 3000);
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,198 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生绩点管理系统 - 注册</title>
<link rel="stylesheet" href="styles.css">
<style>
.register-container {
max-width: 400px;
margin: 100px auto;
padding: 30px;
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
.register-form .form-group {
margin-bottom: 20px;
}
.register-form .form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #444;
}
.register-form .form-group input {
width: 100%;
padding: 12px 15px;
border: 2px solid #ddd;
border-radius: 6px;
box-sizing: border-box;
font-size: 16px;
transition: border-color 0.3s;
}
.register-form .form-group input:focus {
border-color: #667eea;
outline: none;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.register-form .btn {
width: 100%;
padding: 12px;
font-size: 16px;
margin-top: 10px;
}
.login-link {
text-align: center;
margin-top: 20px;
color: #667eea;
}
.login-link a {
color: #667eea;
text-decoration: none;
font-weight: 500;
}
.login-link a:hover {
text-decoration: underline;
}
.error-message {
color: #ff6b6b;
background-color: #ffeaea;
border: 1px solid #ff6b6b;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
display: none;
}
.success-message {
color: #28a745;
background-color: #d4edda;
border: 1px solid #28a745;
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
display: none;
}
</style>
</head>
<body>
<div class="register-container">
<h1 style="text-align: center; margin-bottom: 30px;">学生绩点管理系统</h1>
<h2 style="text-align: center; margin-bottom: 30px; color: #555;">用户注册</h2>
<div id="error-message" class="error-message"></div>
<div id="success-message" class="success-message"></div>
<form id="register-form" class="register-form">
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label for="confirm-password">确认密码:</label>
<input type="password" id="confirm-password" name="confirm-password" required>
</div>
<button type="submit" class="btn btn-success">
<span class="icon"></span> 注册
</button>
</form>
<div class="login-link">
已有账户?<a href="#" onclick="showLoginForm()">立即登录</a>
</div>
</div>
<script>
document.getElementById('register-form').addEventListener('submit', function(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirm-password').value;
// 简单验证
if (password !== confirmPassword) {
showError('两次输入的密码不一致');
return;
}
if (password.length < 6) {
showError('密码长度至少为6位');
return;
}
const registerData = {
username: username,
password: password
};
fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(registerData)
})
.then(response => {
if (response.ok) {
return response.json();
} else {
return response.json().then(err => {
throw new Error(err.error);
});
}
})
.then(data => {
showSuccess(data.message);
// 2秒后跳转到登录页面
setTimeout(() => {
window.location.href = '/login.html';
}, 2000);
})
.catch(error => {
showError(error.message);
});
});
function showError(message) {
document.getElementById('error-message').textContent = message;
document.getElementById('error-message').style.display = 'block';
document.getElementById('success-message').style.display = 'none';
// 3秒后隐藏错误信息
setTimeout(() => {
document.getElementById('error-message').style.display = 'none';
}, 3000);
}
function showSuccess(message) {
document.getElementById('success-message').textContent = message;
document.getElementById('success-message').style.display = 'block';
document.getElementById('error-message').style.display = 'none';
}
function showLoginForm() {
window.location.href = '/login.html';
}
</script>
</body>
</html>

View File

@@ -0,0 +1,835 @@
// 当前筛选结果
let currentFilterResult = {
students: [],
isFiltered: false,
filterDescription: ""
};
// 当前编辑的学生
let currentEditStudent = null;
// 页面加载完成后显示所有学生
document.addEventListener('DOMContentLoaded', function() {
// 登录检查已移至HTML文件中这里不再自动调用showAllStudents()
});
// 显示加载状态
function showLoading() {
document.getElementById('loading').style.display = 'flex';
}
// 隐藏加载状态
function hideLoading() {
document.getElementById('loading').style.display = 'none';
}
// 显示所有学生
function showAllStudents() {
console.log('showAllStudents() 被调用');
showLoading();
fetch('/api/students/all')
.then(response => {
console.log('API响应状态:', response.status);
if (!response.ok) {
throw new Error('网络响应错误: ' + response.status);
}
return response.json();
})
.then(data => {
console.log('获取到学生数据:', data);
displayStudents(data.students);
updateFilterInfo();
})
.catch(error => {
console.error('获取学生数据失败:', error);
showMessage('获取学生数据失败: ' + error.message, 'error');
})
.finally(() => {
hideLoading();
});
}
// 显示筛选结果
function showFilteredStudents() {
if (!currentFilterResult.isFiltered) {
showMessage('当前没有筛选结果,请先进行筛选操作', 'info');
return;
}
displayStudents(currentFilterResult.students);
updateFilterInfo();
}
// 清除筛选
function clearFilter() {
currentFilterResult = {
students: [],
isFiltered: false,
filterDescription: ""
};
showAllStudents();
showMessage('已清除筛选条件,重新开始筛选', 'info');
}
// 显示添加学生表单
function showAddStudentForm() {
hideAllForms();
document.getElementById('add-student-form').style.display = 'block';
document.getElementById('student-form').addEventListener('submit', handleAddStudent);
}
// 隐藏添加学生表单
function hideAddStudentForm() {
document.getElementById('add-student-form').style.display = 'none';
document.getElementById('student-form').removeEventListener('submit', handleAddStudent);
document.getElementById('student-form').reset();
}
// 处理添加学生
function handleAddStudent(event) {
event.preventDefault();
const studentId = document.getElementById('student-id').value.trim();
const name = document.getElementById('name').value.trim();
const gpa = parseFloat(document.getElementById('gpa').value);
const college = document.getElementById('college').value.trim();
// 简单验证
if (!studentId || !name || isNaN(gpa) || !college) {
showMessage('请填写所有必填字段', 'error');
return;
}
if (gpa < 0 || gpa > 5.0) {
showMessage('绩点必须在0到5.0之间', 'error');
return;
}
const student = {
studentId: studentId,
name: name,
gpa: gpa,
college: college
};
const sessionId = localStorage.getItem('sessionId');
showLoading();
fetch('/api/students', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Session-Id': sessionId
},
body: JSON.stringify(student)
})
.then(response => {
if (response.ok) {
return response.json();
} else {
// 尝试解析错误响应
return response.json().then(errorData => {
throw new Error(errorData.error || `添加学生失败: ${response.status}`);
}).catch(() => {
throw new Error(`添加学生失败: ${response.status}`);
});
}
})
.then(data => {
hideAddStudentForm();
showAllStudents();
showMessage('学生信息添加成功!', 'success');
})
.catch(error => {
showMessage('添加学生失败: ' + error.message, 'error');
})
.finally(() => {
hideLoading();
});
}
// 显示修改学生表单
function showEditStudentForm(student) {
hideAllForms();
currentEditStudent = student;
document.getElementById('edit-student-id').value = student.studentId;
document.getElementById('edit-name').value = student.name;
document.getElementById('edit-gpa').value = student.gpa;
document.getElementById('edit-college').value = student.college;
document.getElementById('edit-student-form').style.display = 'block';
document.getElementById('student-edit-form').addEventListener('submit', handleEditStudent);
}
// 隐藏修改学生表单
function hideEditStudentForm() {
document.getElementById('edit-student-form').style.display = 'none';
document.getElementById('student-edit-form').removeEventListener('submit', handleEditStudent);
document.getElementById('student-edit-form').reset();
currentEditStudent = null;
}
// 处理修改学生信息
function handleEditStudent(event) {
event.preventDefault();
const studentId = document.getElementById('edit-student-id').value.trim();
const name = document.getElementById('edit-name').value.trim();
const gpa = parseFloat(document.getElementById('edit-gpa').value);
const college = document.getElementById('edit-college').value.trim();
// 简单验证
if (!studentId || !name || isNaN(gpa) || !college) {
showMessage('请填写所有必填字段', 'error');
return;
}
if (gpa < 0 || gpa > 5.0) {
showMessage('绩点必须在0到5.0之间', 'error');
return;
}
const student = {
studentId: studentId,
name: name,
gpa: gpa,
college: college
};
const sessionId = localStorage.getItem('sessionId');
showLoading();
fetch(`/api/students/${studentId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Session-Id': sessionId
},
body: JSON.stringify(student)
})
.then(response => {
if (response.ok) {
return response.json();
} else {
// 尝试解析错误响应
return response.json().then(errorData => {
throw new Error(errorData.error || `修改学生信息失败: ${response.status}`);
}).catch(() => {
throw new Error(`修改学生信息失败: ${response.status}`);
});
}
})
.then(data => {
hideEditStudentForm();
showAllStudents();
showMessage('学生信息修改成功!', 'success');
})
.catch(error => {
showMessage('修改学生失败: ' + error.message, 'error');
})
.finally(() => {
hideLoading();
});
}
// 显示搜索表单
function showSearchForm() {
hideAllForms();
document.getElementById('search-form').style.display = 'block';
}
// 隐藏搜索表单
function hideSearchForm() {
document.getElementById('search-form').style.display = 'none';
hideSearchByIdForm();
hideSearchByNameForm();
hideSearchByGpaForm();
hideSearchByCollegeForm();
}
// 显示按学号搜索表单
function showSearchByIdForm() {
document.getElementById('search-by-id-form').style.display = 'block';
}
// 隐藏按学号搜索表单
function hideSearchByIdForm() {
document.getElementById('search-by-id-form').style.display = 'none';
document.getElementById('search-student-id').value = '';
}
// 按学号搜索学生
function searchStudentByStudentId() {
const studentId = document.getElementById('search-student-id').value.trim();
if (!studentId) {
showMessage('请输入学号', 'error');
return;
}
showLoading();
fetch(`/api/students/${studentId}`)
.then(response => {
if (response.ok) {
return response.json();
} else if (response.status === 404) {
throw new Error('未找到学号为 ' + studentId + ' 的学生');
} else {
throw new Error('查询失败: ' + response.status);
}
})
.then(data => {
if (data) {
displayStudents([data]);
updateFilterInfo();
hideSearchForm();
showMessage('找到学生', 'success');
} else {
showMessage('未找到学号为 ' + studentId + ' 的学生', 'info');
}
})
.catch(error => {
showMessage(error.message, 'error');
})
.finally(() => {
hideLoading();
});
}
// 显示按姓名搜索表单
function showSearchByNameForm() {
document.getElementById('search-by-name-form').style.display = 'block';
}
// 隐藏按姓名搜索表单
function hideSearchByNameForm() {
document.getElementById('search-by-name-form').style.display = 'none';
document.getElementById('search-name').value = '';
}
// 按姓名搜索学生
function searchStudentByName() {
const name = document.getElementById('search-name').value.trim();
if (!name) {
showMessage('请输入姓名', 'error');
return;
}
showLoading();
fetch(`/api/students?name=${encodeURIComponent(name)}`)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('查询失败: ' + response.status);
}
})
.then(data => {
if (data.students && data.students.length > 0) {
displayStudents(data.students);
updateFilterInfo();
hideSearchForm();
showMessage(`找到 ${data.students.length} 个匹配的学生`, 'success');
} else {
showMessage('未找到姓名包含 ' + name + ' 的学生', 'info');
}
})
.catch(error => {
showMessage('查询失败: ' + error.message, 'error');
})
.finally(() => {
hideLoading();
});
}
// 显示按绩点搜索表单
function showSearchByGpaForm() {
document.getElementById('search-by-gpa-form').style.display = 'block';
}
// 隐藏按绩点搜索表单
function hideSearchByGpaForm() {
document.getElementById('search-by-gpa-form').style.display = 'none';
document.getElementById('search-gpa').value = '';
}
// 按绩点搜索学生
function searchStudentByGpa() {
const gpa = parseFloat(document.getElementById('search-gpa').value);
if (isNaN(gpa)) {
showMessage('请输入有效的绩点值', 'error');
return;
}
if (gpa < 0 || gpa > 5.0) {
showMessage('绩点必须在0到5.0之间', 'error');
return;
}
showLoading();
fetch(`/api/students?gpa=${gpa}`)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('查询失败: ' + response.status);
}
})
.then(data => {
if (data.students && data.students.length > 0) {
displayStudents(data.students);
updateFilterInfo();
hideSearchForm();
showMessage(`找到 ${data.students.length} 个绩点大于等于 ${gpa} 的学生`, 'success');
} else {
showMessage('未找到绩点大于等于 ' + gpa + ' 的学生', 'info');
}
})
.catch(error => {
showMessage('查询失败: ' + error.message, 'error');
})
.finally(() => {
hideLoading();
});
}
// 显示按学院搜索表单
function showSearchByCollegeForm() {
document.getElementById('search-by-college-form').style.display = 'block';
}
// 隐藏按学院搜索表单
function hideSearchByCollegeForm() {
document.getElementById('search-by-college-form').style.display = 'none';
document.getElementById('search-college').value = '';
}
// 按学院搜索学生
function searchStudentByCollege() {
const college = document.getElementById('search-college').value.trim();
if (!college) {
showMessage('请输入学院名称', 'error');
return;
}
showLoading();
fetch(`/api/students?college=${encodeURIComponent(college)}`)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('查询失败: ' + response.status);
}
})
.then(data => {
if (data.students && data.students.length > 0) {
displayStudents(data.students);
updateFilterInfo();
hideSearchForm();
showMessage(`找到 ${data.students.length} 个学院包含 ${college} 的学生`, 'success');
} else {
showMessage('未找到学院包含 ' + college + ' 的学生', 'info');
}
})
.catch(error => {
showMessage('查询失败: ' + error.message, 'error');
})
.finally(() => {
hideLoading();
});
}
// 显示筛选表单
function showFilterForm() {
hideAllForms();
document.getElementById('filter-form').style.display = 'block';
}
// 隐藏筛选表单
function hideFilterForm() {
document.getElementById('filter-form').style.display = 'none';
hideFilterByNameForm();
hideFilterByGpaForm();
hideFilterByCollegeForm();
}
// 显示按姓名筛选表单
function showFilterByNameForm() {
document.getElementById('filter-by-name-form').style.display = 'block';
}
// 隐藏按姓名筛选表单
function hideFilterByNameForm() {
document.getElementById('filter-by-name-form').style.display = 'none';
document.getElementById('filter-name').value = '';
}
// 按姓名筛选学生
function filterByName() {
const name = document.getElementById('filter-name').value.trim();
if (!name) {
showMessage('请输入姓名关键字', 'error');
return;
}
// 构造筛选描述
let filterDesc = "";
if (currentFilterResult.isFiltered) {
filterDesc = currentFilterResult.filterDescription + " + 姓名包含'" + name + "'";
} else {
filterDesc = "姓名包含'" + name + "'";
}
showLoading();
// 构造URL参数
let url = `/api/filter/name?name=${encodeURIComponent(name)}&currentFilter=${currentFilterResult.isFiltered}`;
if (currentFilterResult.isFiltered && currentFilterResult.students.length > 0) {
const studentIds = currentFilterResult.students.map(s => s.studentId).join(',');
url += `&currentStudentIds=${encodeURIComponent(studentIds)}`;
}
// 发送筛选请求
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('筛选失败: ' + response.status);
}
})
.then(data => {
currentFilterResult = {
students: data.students,
isFiltered: true,
filterDescription: filterDesc
};
displayStudents(currentFilterResult.students);
updateFilterInfo();
hideFilterForm();
showMessage('筛选完成: ' + filterDesc, 'success');
})
.catch(error => {
showMessage('筛选失败: ' + error.message, 'error');
})
.finally(() => {
hideLoading();
});
}
// 显示按绩点筛选表单
function showFilterByGpaForm() {
document.getElementById('filter-by-gpa-form').style.display = 'block';
}
// 隐藏按绩点筛选表单
function hideFilterByGpaForm() {
document.getElementById('filter-by-gpa-form').style.display = 'none';
document.getElementById('filter-gpa').value = '';
}
// 按绩点筛选学生
function filterByGpa() {
const gpa = parseFloat(document.getElementById('filter-gpa').value);
if (isNaN(gpa)) {
showMessage('请输入有效的绩点值', 'error');
return;
}
if (gpa < 0 || gpa > 5.0) {
showMessage('绩点必须在0到5.0之间', 'error');
return;
}
// 构造筛选描述
let filterDesc = "";
if (currentFilterResult.isFiltered) {
filterDesc = currentFilterResult.filterDescription + " + 绩点>=" + gpa;
} else {
filterDesc = "绩点>=" + gpa;
}
showLoading();
// 构造URL参数
let url = `/api/filter/gpa?gpa=${gpa}&currentFilter=${currentFilterResult.isFiltered}`;
if (currentFilterResult.isFiltered && currentFilterResult.students.length > 0) {
const studentIds = currentFilterResult.students.map(s => s.studentId).join(',');
url += `&currentStudentIds=${encodeURIComponent(studentIds)}`;
}
// 发送筛选请求
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('筛选失败: ' + response.status);
}
})
.then(data => {
currentFilterResult = {
students: data.students,
isFiltered: true,
filterDescription: filterDesc
};
displayStudents(currentFilterResult.students);
updateFilterInfo();
hideFilterForm();
showMessage('筛选完成: ' + filterDesc, 'success');
})
.catch(error => {
showMessage('筛选失败: ' + error.message, 'error');
})
.finally(() => {
hideLoading();
});
}
// 显示按学院筛选表单
function showFilterByCollegeForm() {
document.getElementById('filter-by-college-form').style.display = 'block';
}
// 隐藏按学院筛选表单
function hideFilterByCollegeForm() {
document.getElementById('filter-by-college-form').style.display = 'none';
document.getElementById('filter-college').value = '';
}
// 按学院筛选学生
function filterByCollege() {
const college = document.getElementById('filter-college').value.trim();
if (!college) {
showMessage('请输入学院关键字', 'error');
return;
}
// 构造筛选描述
let filterDesc = "";
if (currentFilterResult.isFiltered) {
filterDesc = currentFilterResult.filterDescription + " + 学院包含'" + college + "'";
} else {
filterDesc = "学院包含'" + college + "'";
}
showLoading();
// 构造URL参数
let url = `/api/filter/college?college=${encodeURIComponent(college)}&currentFilter=${currentFilterResult.isFiltered}`;
if (currentFilterResult.isFiltered && currentFilterResult.students.length > 0) {
const studentIds = currentFilterResult.students.map(s => s.studentId).join(',');
url += `&currentStudentIds=${encodeURIComponent(studentIds)}`;
}
// 发送筛选请求
fetch(url)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('筛选失败: ' + response.status);
}
})
.then(data => {
currentFilterResult = {
students: data.students,
isFiltered: true,
filterDescription: filterDesc
};
displayStudents(currentFilterResult.students);
updateFilterInfo();
hideFilterForm();
showMessage('筛选完成: ' + filterDesc, 'success');
})
.catch(error => {
showMessage('筛选失败: ' + error.message, 'error');
})
.finally(() => {
hideLoading();
});
}
// 显示学生列表
function displayStudents(students) {
console.log('displayStudents() 被调用,学生数量:', students ? students.length : 0);
const tableBody = document.getElementById('student-table-body');
if (!tableBody) {
console.error('找不到 student-table-body 元素');
return;
}
tableBody.innerHTML = '';
if (!students || students.length === 0) {
console.log('没有学生数据,显示空状态');
const row = tableBody.insertRow();
const cell = row.insertCell(0);
cell.colSpan = 5;
cell.textContent = '暂无学生信息';
cell.style.textAlign = 'center';
cell.style.padding = '30px';
cell.style.fontStyle = 'italic';
cell.style.color = '#888';
return;
}
// 优化大量数据的显示性能
const maxAnimationItems = 100; // 最多为前100个项目添加动画
const fragment = document.createDocumentFragment();
students.forEach((student, index) => {
const row = document.createElement('tr');
row.insertCell(0).textContent = student.studentId;
row.insertCell(1).textContent = student.name;
// 修改绩点显示为四位小数
row.insertCell(2).textContent = student.gpa.toFixed(4);
row.insertCell(3).textContent = student.college;
// 添加操作按钮 - 根据用户角色显示不同按钮
const actionCell = row.insertCell(4);
const userRole = localStorage.getItem('role');
if (userRole === 'ADMIN') {
// 管理员可以修改和删除
const editButton = document.createElement('button');
editButton.className = 'btn btn-warning';
editButton.innerHTML = '<span class="icon">✏️</span> 修改';
editButton.onclick = () => showEditStudentForm(student);
editButton.style.marginRight = '5px';
actionCell.appendChild(editButton);
const deleteButton = document.createElement('button');
deleteButton.className = 'btn btn-danger';
deleteButton.innerHTML = '<span class="icon">🗑️</span> 删除';
deleteButton.onclick = () => deleteStudent(student);
actionCell.appendChild(deleteButton);
} else {
// 普通用户只能查看
const viewButton = document.createElement('button');
viewButton.className = 'btn btn-info';
viewButton.innerHTML = '<span class="icon">👁️</span> 查看';
viewButton.onclick = () => showViewStudentDetails(student);
actionCell.appendChild(viewButton);
}
// 只为前N个项目添加动画避免大量数据时的性能问题
if (index < maxAnimationItems) {
row.style.animation = `fadeIn 0.3s ease-out ${index * 0.01}s both`;
}
fragment.appendChild(row);
});
tableBody.appendChild(fragment);
// 如果数据量很大,显示提示信息
if (students.length > maxAnimationItems) {
showMessage(`共显示 ${students.length} 个学生,为保证性能仅前 ${maxAnimationItems} 个有动画效果`, 'info');
}
}
// 显示学生详情(只读模式)
function showViewStudentDetails(student) {
alert(`学生详情:\n\n学号:${student.studentId}\n姓名:${student.name}\n绩点:${student.gpa.toFixed(4)}\n学院:${student.college}`);
}
// 删除学生
function deleteStudent(student) {
// 确认删除
const confirmMessage = `确定要删除以下学生吗?\n\n学号:${student.studentId}\n姓名:${student.name}\n学院:${student.college}\n\n此操作不可撤销!`;
if (!confirm(confirmMessage)) {
return;
}
const sessionId = localStorage.getItem('sessionId');
showLoading();
fetch(`/api/students/${student.studentId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Session-Id': sessionId
}
})
.then(response => {
hideLoading();
if (response.ok) {
return response.json();
} else {
return response.json().then(err => {
throw new Error(err.error || '删除失败');
});
}
})
.then(data => {
showMessage(`学生 ${data.studentName}(${data.studentId}) 删除成功!`, 'success');
// 刷新学生列表
showAllStudents();
})
.catch(error => {
showMessage('删除学生失败: ' + error.message, 'error');
});
}
// 更新筛选信息显示
function updateFilterInfo() {
const filterInfo = document.getElementById('filter-info');
if (currentFilterResult.isFiltered) {
filterInfo.textContent = '筛选条件: ' + currentFilterResult.filterDescription + ' (共找到 ' + currentFilterResult.students.length + ' 个学生)';
filterInfo.classList.add('active');
} else {
filterInfo.classList.remove('active');
}
}
// 显示消息提示
function showMessage(text, type) {
const messageElement = document.getElementById('message');
messageElement.textContent = text;
messageElement.className = 'message ' + type;
messageElement.style.display = 'block';
// 5秒后自动隐藏消息
setTimeout(() => {
messageElement.style.display = 'none';
}, 5000);
}
// 隐藏所有表单
function hideAllForms() {
const forms = document.querySelectorAll('.form-container');
forms.forEach(form => {
form.style.display = 'none';
});
// 同时隐藏所有子表单
const subForms = document.querySelectorAll('.search-form, .filter-form');
subForms.forEach(form => {
form.style.display = 'none';
});
// 重置所有表单
const allForms = document.querySelectorAll('form');
allForms.forEach(form => {
form.reset();
});
// 移除事件监听器
document.getElementById('student-form').removeEventListener('submit', handleAddStudent);
document.getElementById('student-edit-form').removeEventListener('submit', handleEditStudent);
document.getElementById('user-register-form').removeEventListener('submit', handleRegisterUser);
}

View File

@@ -0,0 +1,605 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 2.5rem;
background: linear-gradient(90deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
h2 {
color: #555;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
margin-top: 0;
}
h3 {
color: #667eea;
margin-top: 0;
}
.filter-info {
background: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
border: 1px solid #667eea;
border-radius: 6px;
padding: 15px 20px;
margin-bottom: 20px;
display: none;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
font-weight: 500;
}
.filter-info.active {
display: block;
animation: fadeIn 0.3s ease-in;
}
.menu {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 30px;
justify-content: center;
}
.btn {
padding: 12px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 15px;
font-weight: 500;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
gap: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
}
.btn:active {
transform: translateY(0);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-success {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
color: white;
}
.btn-info {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.btn-warning {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
color: white;
}
.btn-danger {
background: linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%);
color: white;
}
.btn-secondary {
background: linear-gradient(135deg, #84fab0 0%, #8fd3f4 100%);
color: white;
}
.form-container {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 25px;
border-radius: 8px;
margin-bottom: 30px;
border: 1px solid #dee2e6;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #444;
}
.form-group input {
width: 100%;
padding: 12px 15px;
border: 2px solid #ddd;
border-radius: 6px;
box-sizing: border-box;
font-size: 16px;
transition: border-color 0.3s;
}
.form-group input:focus {
border-color: #667eea;
outline: none;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-buttons {
display: flex;
gap: 15px;
margin-top: 25px;
justify-content: flex-start;
}
.search-options, .filter-options {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 25px;
justify-content: center;
}
.search-form, .filter-form {
margin-top: 25px;
padding: 20px;
background-color: white;
border-radius: 6px;
border: 1px solid #dee2e6;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.table-container {
overflow-x: auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
max-height: 70vh;
}
.table-container::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.table-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.table-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.table-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 8px 8px 0 0;
}
.loading {
display: flex;
align-items: center;
gap: 10px;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #ffffff;
border-top: 2px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 0;
}
th, td {
padding: 15px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
position: sticky;
top: 0;
}
tr:hover {
background-color: #f8f9fa;
transition: background-color 0.2s;
}
tr:nth-child(even) {
background-color: #fdfdfd;
}
.message {
padding: 15px 20px;
margin-top: 20px;
border-radius: 6px;
display: none;
animation: slideIn 0.3s ease-out;
font-weight: 500;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.message.success {
background: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%);
color: #155724;
border: 1px solid #28a745;
}
.message.error {
background: linear-gradient(120deg, #ff9a9e 0%, #fad0c4 100%);
color: #721c24;
border: 1px solid #dc3545;
}
.message.info {
background: linear-gradient(120deg, #84fab0 0%, #8fd3f4 100%);
color: #0c5460;
border: 1px solid #17a2b8;
}
.icon {
font-size: 18px;
}
@media (max-width: 768px) {
.container {
padding: 15px;
margin: 10px;
}
h1 {
font-size: 2rem;
}
.menu {
flex-direction: column;
}
.menu .btn {
width: 100%;
justify-content: center;
}
.search-options, .filter-options {
flex-direction: column;
}
.search-options .btn, .filter-options .btn {
width: 100%;
justify-content: center;
}
.form-buttons {
flex-direction: column;
}
.form-buttons .btn {
width: 100%;
justify-content: center;
}
.table-header {
flex-direction: column;
gap: 10px;
text-align: center;
}
th, td {
padding: 12px 8px;
font-size: 14px;
}
.icon {
font-size: 16px;
}
.table-container {
max-height: 60vh;
}
}
@media (max-width: 480px) {
body {
padding: 10px;
}
.container {
padding: 10px;
}
h1 {
font-size: 1.5rem;
}
.form-container {
padding: 15px;
}
.search-form, .filter-form {
padding: 15px;
}
.user-welcome-card {
flex-direction: column;
align-items: center;
padding: 8px 12px;
}
.user-avatar {
font-size: 1.2rem;
margin-bottom: 4px;
}
.user-details {
flex-direction: column;
align-items: center;
gap: 2px;
}
.username-text {
font-size: 0.9rem;
}
.user-role-text {
font-size: 0.7rem;
}
}
/* 文件上传样式 */
.upload-info {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.upload-info h3 {
color: #495057;
margin-bottom: 10px;
font-size: 1.1rem;
}
.upload-info ul {
margin: 10px 0;
padding-left: 20px;
}
.upload-info li {
margin-bottom: 5px;
color: #6c757d;
}
.upload-summary {
background: #e7f3ff;
border: 1px solid #b3d9ff;
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
}
.upload-summary h4 {
color: #0056b3;
margin-bottom: 10px;
}
.upload-summary p {
margin: 5px 0;
color: #495057;
}
.upload-errors {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 6px;
padding: 15px;
}
.upload-errors h4 {
color: #856404;
margin-bottom: 10px;
}
.error-list {
max-height: 300px;
overflow-y: auto;
}
.error-item {
background: #fff;
border: 1px solid #f5c6cb;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
}
.error-message {
color: #721c24;
font-style: italic;
display: block;
margin-top: 5px;
}
input[type="file"] {
width: 100%;
padding: 8px;
border: 2px dashed #dee2e6;
border-radius: 6px;
background: #f8f9fa;
cursor: pointer;
transition: all 0.3s ease;
}
input[type="file"]:hover {
border-color: #007bff;
background: #e7f3ff;
}
input[type="file"]:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
/* 删除按钮样式 */
.btn-danger {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
color: white;
border: none;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(220, 53, 69, 0.3);
}
.btn-danger:hover {
background: linear-gradient(135deg, #c82333 0%, #bd2130 100%);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(220, 53, 69, 0.4);
}
.btn-danger:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(220, 53, 69, 0.3);
}
/* 操作按钮容器 */
.action-buttons {
display: flex;
gap: 5px;
align-items: center;
}
/* 确认删除对话框样式增强 */
.confirm-dialog {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* 用户信息卡片样式 */
.user-welcome-card {
display: inline-flex;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 20px;
border-radius: 25px;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
margin-right: 15px;
transition: all 0.3s ease;
border: 2px solid rgba(255, 255, 255, 0.2);
}
.user-welcome-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.user-avatar {
font-size: 1.5rem;
margin-right: 12px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.user-details {
display: flex;
flex-direction: column;
gap: 2px;
}
.username-text {
font-weight: 600;
font-size: 1rem;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.user-role-text {
font-size: 0.8rem;
opacity: 0.9;
font-weight: 400;
background: rgba(255, 255, 255, 0.2);
padding: 2px 8px;
border-radius: 10px;
text-align: center;
}