commit
89413b0f2a
17 changed files with 1027 additions and 0 deletions
@ -0,0 +1,23 @@ |
|||||
|
# Maven构建产物 |
||||
|
target/ |
||||
|
|
||||
|
# IDE相关文件 |
||||
|
.idea/ |
||||
|
.vscode/ |
||||
|
|
||||
|
# 数据库文件 |
||||
|
*.db |
||||
|
|
||||
|
# 系统文件 |
||||
|
.DS_Store |
||||
|
Thumbs.db |
||||
|
|
||||
|
# 日志文件 |
||||
|
*.log |
||||
|
|
||||
|
# 临时文件 |
||||
|
*.tmp |
||||
|
*.temp |
||||
|
|
||||
|
# 批处理文件 |
||||
|
run.bat |
||||
@ -0,0 +1,76 @@ |
|||||
|
<?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.example</groupId> |
||||
|
<artifactId>hnu-course-analysis</artifactId> |
||||
|
<version>1.0-SNAPSHOT</version> |
||||
|
|
||||
|
<name>湖南大学选课系统分析</name> |
||||
|
<description>湖南大学选课系统数据爬取、分析与可视化</description> |
||||
|
|
||||
|
<properties> |
||||
|
<java.version>17</java.version> |
||||
|
<spring-boot.version>3.2.4</spring-boot.version> |
||||
|
<selenium.version>4.18.1</selenium.version> |
||||
|
<jsoup.version>1.17.2</jsoup.version> |
||||
|
<sqlite.version>3.45.3.0</sqlite.version> |
||||
|
</properties> |
||||
|
|
||||
|
<parent> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-starter-parent</artifactId> |
||||
|
<version>3.2.4</version> |
||||
|
<relativePath/> |
||||
|
</parent> |
||||
|
|
||||
|
<dependencies> |
||||
|
<dependency> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-starter-web</artifactId> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-starter-thymeleaf</artifactId> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.seleniumhq.selenium</groupId> |
||||
|
<artifactId>selenium-java</artifactId> |
||||
|
<version>${selenium.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.jsoup</groupId> |
||||
|
<artifactId>jsoup</artifactId> |
||||
|
<version>${jsoup.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.xerial</groupId> |
||||
|
<artifactId>sqlite-jdbc</artifactId> |
||||
|
<version>${sqlite.version}</version> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.projectlombok</groupId> |
||||
|
<artifactId>lombok</artifactId> |
||||
|
</dependency> |
||||
|
<dependency> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-starter-test</artifactId> |
||||
|
<scope>test</scope> |
||||
|
</dependency> |
||||
|
</dependencies> |
||||
|
|
||||
|
<build> |
||||
|
<plugins> |
||||
|
<plugin> |
||||
|
<groupId>org.springframework.boot</groupId> |
||||
|
<artifactId>spring-boot-maven-plugin</artifactId> |
||||
|
</plugin> |
||||
|
</plugins> |
||||
|
</build> |
||||
|
</project> |
||||
@ -0,0 +1,11 @@ |
|||||
|
package com.example; |
||||
|
|
||||
|
import org.springframework.boot.SpringApplication; |
||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication; |
||||
|
|
||||
|
@SpringBootApplication |
||||
|
public class HnuCourseAnalysisApplication { |
||||
|
public static void main(String[] args) { |
||||
|
SpringApplication.run(HnuCourseAnalysisApplication.class, args); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,88 @@ |
|||||
|
package com.example.analyzer; |
||||
|
|
||||
|
import com.example.entity.Course; |
||||
|
import com.example.repository.CourseRepository; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Component |
||||
|
public class CourseAnalyzer { |
||||
|
@Autowired |
||||
|
private CourseRepository courseRepository; |
||||
|
|
||||
|
// 获取整体统计信息
|
||||
|
public Map<String, Object> getOverallStatistics() { |
||||
|
Map<String, Object> statistics = new HashMap<>(); |
||||
|
try { |
||||
|
List<Course> allCourses = courseRepository.findAll(); |
||||
|
|
||||
|
// 总课程数
|
||||
|
statistics.put("totalCourses", allCourses.size()); |
||||
|
|
||||
|
// 总学分
|
||||
|
double totalCredits = allCourses.stream() |
||||
|
.mapToDouble(Course::getCredit) |
||||
|
.sum(); |
||||
|
statistics.put("totalCredits", totalCredits); |
||||
|
|
||||
|
// 平均学分
|
||||
|
double avgCredit = allCourses.isEmpty() ? 0 : totalCredits / allCourses.size(); |
||||
|
statistics.put("averageCredit", avgCredit); |
||||
|
|
||||
|
// 课程类型统计
|
||||
|
List<Map<String, Object>> courseTypeStats = courseRepository.getCourseTypeStatistics(); |
||||
|
statistics.put("courseTypeStatistics", courseTypeStats); |
||||
|
|
||||
|
// 院系统计
|
||||
|
List<Map<String, Object>> departmentStats = courseRepository.getDepartmentStatistics(); |
||||
|
statistics.put("departmentStatistics", departmentStats); |
||||
|
|
||||
|
// 选课情况统计
|
||||
|
List<Map<String, Object>> enrollmentStats = courseRepository.getCourseEnrollmentStatistics(); |
||||
|
statistics.put("enrollmentStatistics", enrollmentStats); |
||||
|
} catch (Exception e) { |
||||
|
System.err.println("获取整体统计信息失败:" + e.getMessage()); |
||||
|
// 返回默认值
|
||||
|
statistics.put("totalCourses", 0); |
||||
|
statistics.put("totalCredits", 0); |
||||
|
statistics.put("averageCredit", 0); |
||||
|
statistics.put("courseTypeStatistics", List.of()); |
||||
|
statistics.put("departmentStatistics", List.of()); |
||||
|
statistics.put("enrollmentStatistics", List.of()); |
||||
|
} |
||||
|
return statistics; |
||||
|
} |
||||
|
|
||||
|
// 获取院系统计信息
|
||||
|
public List<Map<String, Object>> getDepartmentStatistics() { |
||||
|
try { |
||||
|
return courseRepository.getDepartmentStatistics(); |
||||
|
} catch (Exception e) { |
||||
|
System.err.println("获取院系统计信息失败:" + e.getMessage()); |
||||
|
return List.of(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 获取课程类型统计信息
|
||||
|
public List<Map<String, Object>> getCourseTypeStatistics() { |
||||
|
try { |
||||
|
return courseRepository.getCourseTypeStatistics(); |
||||
|
} catch (Exception e) { |
||||
|
System.err.println("获取课程类型统计信息失败:" + e.getMessage()); |
||||
|
return List.of(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 获取选课情况统计信息
|
||||
|
public List<Map<String, Object>> getEnrollmentStatistics() { |
||||
|
try { |
||||
|
return courseRepository.getCourseEnrollmentStatistics(); |
||||
|
} catch (Exception e) { |
||||
|
System.err.println("获取选课情况统计信息失败:" + e.getMessage()); |
||||
|
return List.of(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,9 @@ |
|||||
|
package com.example.config; |
||||
|
|
||||
|
import org.springframework.context.annotation.Configuration; |
||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; |
||||
|
|
||||
|
@Configuration |
||||
|
public class WebConfig implements WebMvcConfigurer { |
||||
|
// 可以在这里添加Web配置
|
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
package com.example.controller; |
||||
|
|
||||
|
import com.example.analyzer.CourseAnalyzer; |
||||
|
import com.example.crawler.CourseCrawler; |
||||
|
import com.example.entity.Course; |
||||
|
import com.example.repository.CourseRepository; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Controller; |
||||
|
import org.springframework.ui.Model; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.ResponseBody; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Controller |
||||
|
public class CourseController { |
||||
|
@Autowired |
||||
|
private CourseCrawler courseCrawler; |
||||
|
|
||||
|
@Autowired |
||||
|
private CourseAnalyzer courseAnalyzer; |
||||
|
|
||||
|
@Autowired |
||||
|
private CourseRepository courseRepository; |
||||
|
|
||||
|
// 首页
|
||||
|
@GetMapping("/") |
||||
|
public String index() { |
||||
|
return "index"; |
||||
|
} |
||||
|
|
||||
|
// 爬取课程数据
|
||||
|
@PostMapping("/crawl") |
||||
|
@ResponseBody |
||||
|
public Map<String, Object> crawlCourses() { |
||||
|
List<Course> courses = courseCrawler.crawlCourses(); |
||||
|
Map<String, Object> response = new HashMap<>(); |
||||
|
response.put("success", true); |
||||
|
response.put("message", "成功爬取 " + courses.size() + " 门课程"); |
||||
|
response.put("data", courses); |
||||
|
return response; |
||||
|
} |
||||
|
|
||||
|
// 查看课程列表
|
||||
|
@GetMapping("/courses") |
||||
|
public String courseList(Model model) { |
||||
|
List<Course> courses = courseRepository.findAll(); |
||||
|
model.addAttribute("courses", courses); |
||||
|
return "course-list"; |
||||
|
} |
||||
|
|
||||
|
// 查看数据分析
|
||||
|
@GetMapping("/analysis") |
||||
|
public String analysis(Model model) { |
||||
|
Map<String, Object> statistics = courseAnalyzer.getOverallStatistics(); |
||||
|
model.addAttribute("statistics", statistics); |
||||
|
return "analysis"; |
||||
|
} |
||||
|
|
||||
|
// 获取分析数据(用于前端图表)
|
||||
|
@GetMapping("/api/analysis") |
||||
|
@ResponseBody |
||||
|
public Map<String, Object> getAnalysisData() { |
||||
|
return courseAnalyzer.getOverallStatistics(); |
||||
|
} |
||||
|
|
||||
|
// 错误处理
|
||||
|
@GetMapping("/error") |
||||
|
public String error() { |
||||
|
return "error"; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
package com.example.crawler; |
||||
|
|
||||
|
import com.example.entity.Course; |
||||
|
import com.example.repository.CourseRepository; |
||||
|
import org.jsoup.Jsoup; |
||||
|
import org.jsoup.nodes.Document; |
||||
|
import org.jsoup.nodes.Element; |
||||
|
import org.jsoup.select.Elements; |
||||
|
import org.openqa.selenium.By; |
||||
|
import org.openqa.selenium.WebDriver; |
||||
|
import org.openqa.selenium.support.ui.ExpectedConditions; |
||||
|
import org.openqa.selenium.support.ui.WebDriverWait; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import java.time.Duration; |
||||
|
import java.time.LocalDateTime; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
@Component |
||||
|
public class CourseCrawler { |
||||
|
@Autowired |
||||
|
private LoginHandler loginHandler; |
||||
|
|
||||
|
@Autowired |
||||
|
private CourseRepository courseRepository; |
||||
|
|
||||
|
@Value("${crawler.hnu.course.list.url}") |
||||
|
private String courseListUrl; |
||||
|
|
||||
|
public List<Course> crawlCourses() { |
||||
|
WebDriver driver = null; |
||||
|
List<Course> courses = new ArrayList<>(); |
||||
|
|
||||
|
try { |
||||
|
// 登录
|
||||
|
driver = loginHandler.login(); |
||||
|
if (driver == null) { |
||||
|
System.err.println("登录失败,无法爬取课程数据"); |
||||
|
return courses; |
||||
|
} |
||||
|
|
||||
|
// 访问课程列表页面
|
||||
|
driver.get(courseListUrl); |
||||
|
|
||||
|
// 等待页面加载完成
|
||||
|
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); |
||||
|
wait.until(ExpectedConditions.presenceOfElementLocated(By.className("gridtable"))); |
||||
|
|
||||
|
// 获取页面源码
|
||||
|
String pageSource = driver.getPageSource(); |
||||
|
|
||||
|
// 使用Jsoup解析页面
|
||||
|
Document doc = Jsoup.parse(pageSource); |
||||
|
Elements courseRows = doc.select(".gridtable tr:not(:first-child)"); |
||||
|
|
||||
|
// 解析课程数据
|
||||
|
for (Element row : courseRows) { |
||||
|
Elements cells = row.select("td"); |
||||
|
if (cells.size() >= 10) { |
||||
|
Course course = new Course(); |
||||
|
course.setCourseCode(cells.get(0).text().trim()); |
||||
|
course.setCourseName(cells.get(1).text().trim()); |
||||
|
course.setCredit(Double.parseDouble(cells.get(2).text().trim())); |
||||
|
course.setTeacher(cells.get(3).text().trim()); |
||||
|
course.setDepartment(cells.get(4).text().trim()); |
||||
|
course.setCapacity(Integer.parseInt(cells.get(5).text().trim())); |
||||
|
course.setEnrolled(Integer.parseInt(cells.get(6).text().trim())); |
||||
|
course.setClassTime(cells.get(7).text().trim()); |
||||
|
course.setClassRoom(cells.get(8).text().trim()); |
||||
|
course.setCourseType(cells.get(9).text().trim()); |
||||
|
course.setSemester("2024-2025-2"); |
||||
|
course.setCreateTime(LocalDateTime.now()); |
||||
|
|
||||
|
courses.add(course); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 保存到数据库
|
||||
|
courseRepository.saveAll(courses); |
||||
|
System.out.println("成功爬取并保存 " + courses.size() + " 门课程"); |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
System.err.println("爬取课程数据失败:" + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
} finally { |
||||
|
if (driver != null) { |
||||
|
loginHandler.logout(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return courses; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
package com.example.crawler; |
||||
|
|
||||
|
import org.openqa.selenium.By; |
||||
|
import org.openqa.selenium.WebDriver; |
||||
|
import org.openqa.selenium.WebElement; |
||||
|
import org.openqa.selenium.chrome.ChromeDriver; |
||||
|
import org.openqa.selenium.chrome.ChromeOptions; |
||||
|
import org.openqa.selenium.support.ui.ExpectedConditions; |
||||
|
import org.openqa.selenium.support.ui.WebDriverWait; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
import java.time.Duration; |
||||
|
|
||||
|
@Component |
||||
|
public class LoginHandler { |
||||
|
@Value("${crawler.hnu.login.url}") |
||||
|
private String loginUrl; |
||||
|
|
||||
|
@Value("${crawler.hnu.username}") |
||||
|
private String username; |
||||
|
|
||||
|
@Value("${crawler.hnu.password}") |
||||
|
private String password; |
||||
|
|
||||
|
private WebDriver driver; |
||||
|
|
||||
|
public WebDriver login() { |
||||
|
setupDriver(); |
||||
|
try { |
||||
|
driver.get(loginUrl); |
||||
|
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(30)); |
||||
|
|
||||
|
// 等待用户名输入框出现
|
||||
|
WebElement usernameElement = wait.until(ExpectedConditions.presenceOfElementLocated(By.id("yhm"))); |
||||
|
usernameElement.sendKeys(username); |
||||
|
|
||||
|
// 输入密码
|
||||
|
WebElement passwordElement = driver.findElement(By.id("mm")); |
||||
|
passwordElement.sendKeys(password); |
||||
|
|
||||
|
// 点击登录按钮
|
||||
|
WebElement loginButton = driver.findElement(By.id("dl")); |
||||
|
loginButton.click(); |
||||
|
|
||||
|
// 等待登录成功
|
||||
|
wait.until(ExpectedConditions.titleContains("湖南大学")); |
||||
|
System.out.println("登录成功"); |
||||
|
return driver; |
||||
|
} catch (Exception e) { |
||||
|
System.err.println("登录失败:" + e.getMessage()); |
||||
|
e.printStackTrace(); |
||||
|
if (driver != null) { |
||||
|
driver.quit(); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void logout() { |
||||
|
if (driver != null) { |
||||
|
try { |
||||
|
// 这里可以添加退出登录的逻辑
|
||||
|
System.out.println("退出登录"); |
||||
|
} finally { |
||||
|
driver.quit(); |
||||
|
driver = null; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void setupDriver() { |
||||
|
ChromeOptions options = new ChromeOptions(); |
||||
|
options.addArguments("--headless"); // 无头模式
|
||||
|
options.addArguments("--disable-gpu"); |
||||
|
options.addArguments("--no-sandbox"); |
||||
|
options.addArguments("--disable-dev-shm-usage"); |
||||
|
|
||||
|
driver = new ChromeDriver(options); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,157 @@ |
|||||
|
package com.example.entity; |
||||
|
|
||||
|
import jakarta.persistence.Entity; |
||||
|
import jakarta.persistence.GeneratedValue; |
||||
|
import jakarta.persistence.GenerationType; |
||||
|
import jakarta.persistence.Id; |
||||
|
import jakarta.persistence.Table; |
||||
|
import jakarta.persistence.Column; |
||||
|
import java.time.LocalDateTime; |
||||
|
|
||||
|
@Entity |
||||
|
@Table(name = "courses") |
||||
|
public class Course { |
||||
|
@Id |
||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY) |
||||
|
private Long id; |
||||
|
|
||||
|
@Column(name = "course_code") |
||||
|
private String courseCode; |
||||
|
|
||||
|
@Column(name = "course_name") |
||||
|
private String courseName; |
||||
|
|
||||
|
@Column(name = "credit") |
||||
|
private double credit; |
||||
|
|
||||
|
@Column(name = "teacher") |
||||
|
private String teacher; |
||||
|
|
||||
|
@Column(name = "department") |
||||
|
private String department; |
||||
|
|
||||
|
@Column(name = "capacity") |
||||
|
private int capacity; |
||||
|
|
||||
|
@Column(name = "enrolled") |
||||
|
private int enrolled; |
||||
|
|
||||
|
@Column(name = "class_time") |
||||
|
private String classTime; |
||||
|
|
||||
|
@Column(name = "class_room") |
||||
|
private String classRoom; |
||||
|
|
||||
|
@Column(name = "course_type") |
||||
|
private String courseType; |
||||
|
|
||||
|
@Column(name = "semester") |
||||
|
private String semester; |
||||
|
|
||||
|
@Column(name = "create_time") |
||||
|
private LocalDateTime createTime; |
||||
|
|
||||
|
public Long getId() { |
||||
|
return id; |
||||
|
} |
||||
|
|
||||
|
public void setId(Long id) { |
||||
|
this.id = id; |
||||
|
} |
||||
|
|
||||
|
public String getCourseCode() { |
||||
|
return courseCode; |
||||
|
} |
||||
|
|
||||
|
public void setCourseCode(String courseCode) { |
||||
|
this.courseCode = courseCode; |
||||
|
} |
||||
|
|
||||
|
public String getCourseName() { |
||||
|
return courseName; |
||||
|
} |
||||
|
|
||||
|
public void setCourseName(String courseName) { |
||||
|
this.courseName = courseName; |
||||
|
} |
||||
|
|
||||
|
public double getCredit() { |
||||
|
return credit; |
||||
|
} |
||||
|
|
||||
|
public void setCredit(double credit) { |
||||
|
this.credit = credit; |
||||
|
} |
||||
|
|
||||
|
public String getTeacher() { |
||||
|
return teacher; |
||||
|
} |
||||
|
|
||||
|
public void setTeacher(String teacher) { |
||||
|
this.teacher = teacher; |
||||
|
} |
||||
|
|
||||
|
public String getDepartment() { |
||||
|
return department; |
||||
|
} |
||||
|
|
||||
|
public void setDepartment(String department) { |
||||
|
this.department = department; |
||||
|
} |
||||
|
|
||||
|
public int getCapacity() { |
||||
|
return capacity; |
||||
|
} |
||||
|
|
||||
|
public void setCapacity(int capacity) { |
||||
|
this.capacity = capacity; |
||||
|
} |
||||
|
|
||||
|
public int getEnrolled() { |
||||
|
return enrolled; |
||||
|
} |
||||
|
|
||||
|
public void setEnrolled(int enrolled) { |
||||
|
this.enrolled = enrolled; |
||||
|
} |
||||
|
|
||||
|
public String getClassTime() { |
||||
|
return classTime; |
||||
|
} |
||||
|
|
||||
|
public void setClassTime(String classTime) { |
||||
|
this.classTime = classTime; |
||||
|
} |
||||
|
|
||||
|
public String getClassRoom() { |
||||
|
return classRoom; |
||||
|
} |
||||
|
|
||||
|
public void setClassRoom(String classRoom) { |
||||
|
this.classRoom = classRoom; |
||||
|
} |
||||
|
|
||||
|
public String getCourseType() { |
||||
|
return courseType; |
||||
|
} |
||||
|
|
||||
|
public void setCourseType(String courseType) { |
||||
|
this.courseType = courseType; |
||||
|
} |
||||
|
|
||||
|
public String getSemester() { |
||||
|
return semester; |
||||
|
} |
||||
|
|
||||
|
public void setSemester(String semester) { |
||||
|
this.semester = semester; |
||||
|
} |
||||
|
|
||||
|
public LocalDateTime getCreateTime() { |
||||
|
return createTime; |
||||
|
} |
||||
|
|
||||
|
public void setCreateTime(LocalDateTime createTime) { |
||||
|
this.createTime = createTime; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
package com.example.repository; |
||||
|
|
||||
|
import com.example.entity.Course; |
||||
|
import org.springframework.data.jpa.repository.JpaRepository; |
||||
|
import org.springframework.data.jpa.repository.Query; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Repository |
||||
|
public interface CourseRepository extends JpaRepository<Course, Long> { |
||||
|
// 根据课程类型查询
|
||||
|
List<Course> findByCourseType(String courseType); |
||||
|
|
||||
|
// 获取所有课程类型统计
|
||||
|
@Query(value = "SELECT course_type, COUNT(*) as count FROM courses GROUP BY course_type", nativeQuery = true) |
||||
|
List<Map<String, Object>> getCourseTypeStatistics(); |
||||
|
|
||||
|
// 获取各院系课程数量统计
|
||||
|
@Query(value = "SELECT department, COUNT(*) as count FROM courses GROUP BY department", nativeQuery = true) |
||||
|
List<Map<String, Object>> getDepartmentStatistics(); |
||||
|
|
||||
|
// 获取课程容量与已选人数统计
|
||||
|
@Query(value = "SELECT course_name, capacity, enrolled FROM courses", nativeQuery = true) |
||||
|
List<Map<String, Object>> getCourseEnrollmentStatistics(); |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
# 服务器配置 |
||||
|
server.port=8084 |
||||
|
|
||||
|
# SQLite数据库配置 |
||||
|
spring.datasource.url=jdbc:sqlite:course.db |
||||
|
spring.datasource.driver-class-name=org.sqlite.JDBC |
||||
|
spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect |
||||
|
spring.jpa.hibernate.ddl-auto=none |
||||
|
spring.jpa.open-in-view=false |
||||
|
|
||||
|
# 爬虫配置 |
||||
|
crawler.hnu.login.url=https://ids.hnu.edu.cn/authserver/login |
||||
|
crawler.hnu.course.list.url=https://jxgl.hnu.edu.cn/courseselect/welcome.do |
||||
|
crawler.hnu.username=test |
||||
|
crawler.hnu.password=test |
||||
|
|
||||
|
# Thymeleaf配置 |
||||
|
spring.thymeleaf.cache=false |
||||
@ -0,0 +1,24 @@ |
|||||
|
CREATE TABLE IF NOT EXISTS courses ( |
||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
|
course_code TEXT, |
||||
|
course_name TEXT, |
||||
|
credit REAL, |
||||
|
teacher TEXT, |
||||
|
department TEXT, |
||||
|
capacity INTEGER, |
||||
|
enrolled INTEGER, |
||||
|
class_time TEXT, |
||||
|
class_room TEXT, |
||||
|
course_type TEXT, |
||||
|
semester TEXT, |
||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
||||
|
); |
||||
|
|
||||
|
-- 插入测试数据 |
||||
|
INSERT INTO courses (course_code, course_name, credit, teacher, department, capacity, enrolled, class_time, class_room, course_type, semester) |
||||
|
VALUES |
||||
|
('CS101', '计算机基础', 3.0, '张老师', '计算机学院', 100, 85, '周一 1-2节', '科技楼101', '必修课', '2024-2025-2'), |
||||
|
('MA201', '高等数学', 4.0, '李老师', '数学学院', 120, 110, '周二 3-4节', '教学楼201', '必修课', '2024-2025-2'), |
||||
|
('EN301', '英语口语', 2.0, '王老师', '外国语学院', 60, 55, '周三 5-6节', '外语楼301', '选修课', '2024-2025-2'), |
||||
|
('PH101', '大学物理', 3.0, '刘老师', '物理学院', 90, 80, '周四 1-2节', '物理楼101', '必修课', '2024-2025-2'), |
||||
|
('HI201', '中国历史', 2.0, '陈老师', '历史学院', 80, 75, '周五 3-4节', '文科楼201', '选修课', '2024-2025-2'); |
||||
@ -0,0 +1,171 @@ |
|||||
|
<!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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> |
||||
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> |
||||
|
</head> |
||||
|
<body> |
||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> |
||||
|
<div class="container"> |
||||
|
<a class="navbar-brand" href="/">湖南大学选课系统分析</a> |
||||
|
<div class="collapse navbar-collapse"> |
||||
|
<ul class="navbar-nav me-auto"> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link" href="/">首页</a> |
||||
|
</li> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link" href="/courses">课程列表</a> |
||||
|
</li> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link active" href="/analysis">数据分析</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</nav> |
||||
|
|
||||
|
<div class="container mt-5"> |
||||
|
<h2>数据分析</h2> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-4"> |
||||
|
<div class="card"> |
||||
|
<div class="card-body"> |
||||
|
<h5 class="card-title">总课程数</h5> |
||||
|
<p class="card-text display-4" th:text="${statistics.totalCourses}"></p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-md-4"> |
||||
|
<div class="card"> |
||||
|
<div class="card-body"> |
||||
|
<h5 class="card-title">总学分</h5> |
||||
|
<p class="card-text display-4" th:text="${statistics.totalCredits}"></p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-md-4"> |
||||
|
<div class="card"> |
||||
|
<div class="card-body"> |
||||
|
<h5 class="card-title">平均学分</h5> |
||||
|
<p class="card-text display-4" th:text="${statistics.averageCredit}"></p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row mt-5"> |
||||
|
<div class="col-md-6"> |
||||
|
<div class="card"> |
||||
|
<div class="card-body"> |
||||
|
<h5 class="card-title">课程类型分布</h5> |
||||
|
<div id="courseTypeChart" style="width: 100%; height: 400px;"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-md-6"> |
||||
|
<div class="card"> |
||||
|
<div class="card-body"> |
||||
|
<h5 class="card-title">院系统计</h5> |
||||
|
<div id="departmentChart" style="width: 100%; height: 400px;"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<script> |
||||
|
// 初始化图表 |
||||
|
document.addEventListener('DOMContentLoaded', function() { |
||||
|
fetch('/api/analysis') |
||||
|
.then(response => response.json()) |
||||
|
.then(data => { |
||||
|
// 课程类型分布图表 |
||||
|
const courseTypeChart = echarts.init(document.getElementById('courseTypeChart')); |
||||
|
const courseTypeData = data.courseTypeStatistics.map(item => [item.course_type, item.count]); |
||||
|
courseTypeChart.setOption({ |
||||
|
tooltip: { |
||||
|
trigger: 'item' |
||||
|
}, |
||||
|
legend: { |
||||
|
top: '5%', |
||||
|
left: 'center' |
||||
|
}, |
||||
|
series: [ |
||||
|
{ |
||||
|
name: '课程类型', |
||||
|
type: 'pie', |
||||
|
radius: ['40%', '70%'], |
||||
|
avoidLabelOverlap: false, |
||||
|
itemStyle: { |
||||
|
borderRadius: 10, |
||||
|
borderColor: '#fff', |
||||
|
borderWidth: 2 |
||||
|
}, |
||||
|
label: { |
||||
|
show: false, |
||||
|
position: 'center' |
||||
|
}, |
||||
|
emphasis: { |
||||
|
label: { |
||||
|
show: true, |
||||
|
fontSize: '18', |
||||
|
fontWeight: 'bold' |
||||
|
} |
||||
|
}, |
||||
|
labelLine: { |
||||
|
show: false |
||||
|
}, |
||||
|
data: courseTypeData |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// 院系统计图表 |
||||
|
const departmentChart = echarts.init(document.getElementById('departmentChart')); |
||||
|
const departmentData = data.departmentStatistics.map(item => [item.department, item.count]); |
||||
|
departmentChart.setOption({ |
||||
|
tooltip: { |
||||
|
trigger: 'axis', |
||||
|
axisPointer: { |
||||
|
type: 'shadow' |
||||
|
} |
||||
|
}, |
||||
|
grid: { |
||||
|
left: '3%', |
||||
|
right: '4%', |
||||
|
bottom: '3%', |
||||
|
containLabel: true |
||||
|
}, |
||||
|
xAxis: { |
||||
|
type: 'category', |
||||
|
data: departmentData.map(item => item[0]), |
||||
|
axisLabel: { |
||||
|
rotate: 45 |
||||
|
} |
||||
|
}, |
||||
|
yAxis: { |
||||
|
type: 'value' |
||||
|
}, |
||||
|
series: [ |
||||
|
{ |
||||
|
name: '课程数量', |
||||
|
type: 'bar', |
||||
|
data: departmentData.map(item => item[1]) |
||||
|
} |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
// 响应式调整 |
||||
|
window.addEventListener('resize', function() { |
||||
|
courseTypeChart.resize(); |
||||
|
departmentChart.resize(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
|
</script> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,63 @@ |
|||||
|
<!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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> |
||||
|
</head> |
||||
|
<body> |
||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> |
||||
|
<div class="container"> |
||||
|
<a class="navbar-brand" href="/">湖南大学选课系统分析</a> |
||||
|
<div class="collapse navbar-collapse"> |
||||
|
<ul class="navbar-nav me-auto"> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link" href="/">首页</a> |
||||
|
</li> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link active" href="/courses">课程列表</a> |
||||
|
</li> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link" href="/analysis">数据分析</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</nav> |
||||
|
|
||||
|
<div class="container mt-5"> |
||||
|
<h2>课程列表</h2> |
||||
|
<table class="table table-striped"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>课程代码</th> |
||||
|
<th>课程名称</th> |
||||
|
<th>学分</th> |
||||
|
<th>教师</th> |
||||
|
<th>院系</th> |
||||
|
<th>容量</th> |
||||
|
<th>已选</th> |
||||
|
<th>上课时间</th> |
||||
|
<th>上课地点</th> |
||||
|
<th>课程类型</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<tr th:each="course : ${courses}"> |
||||
|
<td th:text="${course.courseCode}"></td> |
||||
|
<td th:text="${course.courseName}"></td> |
||||
|
<td th:text="${course.credit}"></td> |
||||
|
<td th:text="${course.teacher}"></td> |
||||
|
<td th:text="${course.department}"></td> |
||||
|
<td th:text="${course.capacity}"></td> |
||||
|
<td th:text="${course.enrolled}"></td> |
||||
|
<td th:text="${course.classTime}"></td> |
||||
|
<td th:text="${course.classRoom}"></td> |
||||
|
<td th:text="${course.courseType}"></td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,24 @@ |
|||||
|
<!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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div class="container mt-5"> |
||||
|
<div class="row justify-content-center"> |
||||
|
<div class="col-md-6"> |
||||
|
<div class="card"> |
||||
|
<div class="card-body text-center"> |
||||
|
<h1 class="text-danger">500 - 服务器内部错误</h1> |
||||
|
<p class="mt-3">服务器遇到了一个内部错误,请稍后再试。</p> |
||||
|
<a href="/" class="btn btn-primary mt-4">返回首页</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,76 @@ |
|||||
|
<!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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> |
||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> |
||||
|
</head> |
||||
|
<body> |
||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-primary"> |
||||
|
<div class="container"> |
||||
|
<a class="navbar-brand" href="/">湖南大学选课系统分析</a> |
||||
|
<div class="collapse navbar-collapse"> |
||||
|
<ul class="navbar-nav me-auto"> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link active" href="/">首页</a> |
||||
|
</li> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link" href="/courses">课程列表</a> |
||||
|
</li> |
||||
|
<li class="nav-item"> |
||||
|
<a class="nav-link" href="/analysis">数据分析</a> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</div> |
||||
|
</nav> |
||||
|
|
||||
|
<div class="container mt-5"> |
||||
|
<div class="jumbotron"> |
||||
|
<h1 class="display-4">湖南大学选课系统分析</h1> |
||||
|
<p class="lead">本系统用于爬取、分析和可视化湖南大学选课系统的数据</p> |
||||
|
<hr class="my-4"> |
||||
|
<p>点击下方按钮开始爬取课程数据</p> |
||||
|
<button id="crawlBtn" class="btn btn-primary btn-lg">爬取课程数据</button> |
||||
|
</div> |
||||
|
|
||||
|
<div id="result" class="mt-4 d-none"> |
||||
|
<div class="alert alert-success" role="alert"> |
||||
|
<h4 class="alert-heading">爬取成功!</h4> |
||||
|
<p id="resultMessage"></p> |
||||
|
<hr> |
||||
|
<a href="/courses" class="btn btn-secondary">查看课程列表</a> |
||||
|
<a href="/analysis" class="btn btn-primary">查看数据分析</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<script> |
||||
|
document.getElementById('crawlBtn').addEventListener('click', function() { |
||||
|
this.disabled = true; |
||||
|
this.textContent = '爬取中...'; |
||||
|
|
||||
|
fetch('/crawl', { |
||||
|
method: 'POST' |
||||
|
}) |
||||
|
.then(response => response.json()) |
||||
|
.then(data => { |
||||
|
if (data.success) { |
||||
|
document.getElementById('resultMessage').textContent = data.message; |
||||
|
document.getElementById('result').classList.remove('d-none'); |
||||
|
} |
||||
|
}) |
||||
|
.catch(error => { |
||||
|
console.error('Error:', error); |
||||
|
alert('爬取失败,请稍后重试'); |
||||
|
}) |
||||
|
.finally(() => { |
||||
|
this.disabled = false; |
||||
|
this.textContent = '爬取课程数据'; |
||||
|
}); |
||||
|
}); |
||||
|
</script> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,13 @@ |
|||||
|
package com.example; |
||||
|
|
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||
|
|
||||
|
@SpringBootTest |
||||
|
class HnuCourseAnalysisApplicationTests { |
||||
|
|
||||
|
@Test |
||||
|
void contextLoads() { |
||||
|
} |
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue