GPA查找系统
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
38
.gitignore
vendored
Normal file
38
.gitignore
vendored
Normal 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
10
.idea/.gitignore
generated
vendored
Normal 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
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
32
.idea/dataSources.xml
generated
Normal 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&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
7
.idea/encodings.xml
generated
Normal 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
14
.idea/misc.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
245
dependency-reduced-pom.xml
Normal 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
80
gpa_system_backup.sql
Normal file
File diff suppressed because one or more lines are too long
140
pom.xml
Normal file
140
pom.xml
Normal 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>
|
||||||
32
src/main/java/com/nesoft/Application.java
Normal file
32
src/main/java/com/nesoft/Application.java
Normal 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();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
469
src/main/java/com/nesoft/AuthController.java
Normal file
469
src/main/java/com/nesoft/AuthController.java
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
25
src/main/java/com/nesoft/DatabaseConfig.java
Normal file
25
src/main/java/com/nesoft/DatabaseConfig.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/main/java/com/nesoft/DatabaseInitializer.java
Normal file
53
src/main/java/com/nesoft/DatabaseInitializer.java
Normal 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("检测到已有用户数据");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
357
src/main/java/com/nesoft/FileUploadController.java
Normal file
357
src/main/java/com/nesoft/FileUploadController.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/main/java/com/nesoft/FilterResult.java
Normal file
71
src/main/java/com/nesoft/FilterResult.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/main/java/com/nesoft/LoginRequest.java
Normal file
40
src/main/java/com/nesoft/LoginRequest.java
Normal 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]'" +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
469
src/main/java/com/nesoft/Main.java
Normal file
469
src/main/java/com/nesoft/Main.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/main/java/com/nesoft/RegisterRequest.java
Normal file
51
src/main/java/com/nesoft/RegisterRequest.java
Normal 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 + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/main/java/com/nesoft/SecurityConfig.java
Normal file
57
src/main/java/com/nesoft/SecurityConfig.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
83
src/main/java/com/nesoft/SessionService.java
Normal file
83
src/main/java/com/nesoft/SessionService.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
65
src/main/java/com/nesoft/Student.java
Normal file
65
src/main/java/com/nesoft/Student.java
Normal 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 + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
407
src/main/java/com/nesoft/StudentController.java
Normal file
407
src/main/java/com/nesoft/StudentController.java
Normal 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
299
src/main/java/com/nesoft/StudentDAO.java
Normal file
299
src/main/java/com/nesoft/StudentDAO.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/main/java/com/nesoft/User.java
Normal file
59
src/main/java/com/nesoft/User.java
Normal 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 + '\'' +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
113
src/main/java/com/nesoft/UserDAO.java
Normal file
113
src/main/java/com/nesoft/UserDAO.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
12
src/main/resources/application.properties
Normal file
12
src/main/resources/application.properties
Normal 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
|
||||||
61
src/main/resources/static/debug.html
Normal file
61
src/main/resources/static/debug.html
Normal 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>
|
||||||
933
src/main/resources/static/index.html
Normal file
933
src/main/resources/static/index.html
Normal 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>
|
||||||
544
src/main/resources/static/login.html
Normal file
544
src/main/resources/static/login.html
Normal 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>
|
||||||
|
|
||||||
198
src/main/resources/static/register.html
Normal file
198
src/main/resources/static/register.html
Normal 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>
|
||||||
835
src/main/resources/static/script.js
Normal file
835
src/main/resources/static/script.js
Normal 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)}¤tFilter=${currentFilterResult.isFiltered}`;
|
||||||
|
if (currentFilterResult.isFiltered && currentFilterResult.students.length > 0) {
|
||||||
|
const studentIds = currentFilterResult.students.map(s => s.studentId).join(',');
|
||||||
|
url += `¤tStudentIds=${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}¤tFilter=${currentFilterResult.isFiltered}`;
|
||||||
|
if (currentFilterResult.isFiltered && currentFilterResult.students.length > 0) {
|
||||||
|
const studentIds = currentFilterResult.students.map(s => s.studentId).join(',');
|
||||||
|
url += `¤tStudentIds=${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)}¤tFilter=${currentFilterResult.isFiltered}`;
|
||||||
|
if (currentFilterResult.isFiltered && currentFilterResult.students.length > 0) {
|
||||||
|
const studentIds = currentFilterResult.students.map(s => s.studentId).join(',');
|
||||||
|
url += `¤tStudentIds=${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);
|
||||||
|
}
|
||||||
605
src/main/resources/static/styles.css
Normal file
605
src/main/resources/static/styles.css
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user