Compare commits

...

2 Commits

Author SHA1 Message Date
范馨遥 fb2b97ffe0 Merge: 合并远程仓库,解决冲突 3 weeks ago
范馨遥 4f6283e6bd Initial commit: 课程爬虫项目 3 weeks ago
  1. 33
      .gitignore
  2. 4
      .trae-cn/worktrees/project/course-analysis copy/output/courses.csv
  3. 43
      .trae-cn/worktrees/project/course-analysis copy/output/courses.json
  4. 8
      .trae-cn/worktrees/project/course-analysis copy/run.bat
  5. 261
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/CourseAnalysis.java
  6. 180
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/CourseSystemTest.java
  7. 195
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/DatabaseUtil.java
  8. 105
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExceptionDemo.java
  9. 68
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExceptionTest.java
  10. 86
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExportTest.java
  11. 54
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/HnuCourseSystem.java
  12. 89
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/Pair.java
  13. 68
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/controller/CourseController.java
  14. 78
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/CrawlerContext.java
  15. 221
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/CrawlerService.java
  16. 99
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/HnuParseStrategy.java
  17. 30
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/ParseStrategy.java
  18. 98
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/SduParseStrategy.java
  19. 127
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/entity/Course.java
  20. 31
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/BizException.java
  21. 34
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/CrawlerException.java
  22. 30
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/NetworkException.java
  23. 31
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/ParseException.java
  24. 60
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/CsvExporter.java
  25. 28
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/ExportService.java
  26. 8
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/Exporter.java
  27. 33
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/JsonExporter.java
  28. 182
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/repository/CourseRepository.java
  29. 229
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/service/CourseService.java
  30. 129
      .trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/view/CourseView.java
  31. 33
      .trae-cn/worktrees/project/course-analysis copy/src/main/resources/application.properties
  32. 31
      .trae-cn/worktrees/project/course-analysis copy/src/main/resources/schema.sql
  33. 328
      .trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/analysis.html
  34. 169
      .trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/course-list.html
  35. 24
      .trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/error.html
  36. 318
      .trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/index.html

33
.gitignore

@ -1,17 +1,36 @@
# Maven构建产物 # Build outputs
target/ target/
*.class
# IDE相关文件 # IDE
.idea/ .idea/
*.iml
.vscode/ .vscode/
# 系统文件 # Logs
*.log
# OS
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# 日志文件 # Database
*.log *.db
*.db-journal
# 临时文件 # Temp files
*.xml
*.ps1
report.txt
Untitled-*.java
*.tmp *.tmp
*.temp *.temp
# Maven
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
build.log

4
.trae-cn/worktrees/project/course-analysis copy/output/courses.csv

@ -0,0 +1,4 @@
id,courseCode,courseName,credit,teacher,department,capacity,enrolled,classTime,classRoom,courseType,semester,createTime
1,CS101,计算机科学导论,3.0,李教授,计算机学院,100,95,,,必修课,2024-2025-1,
2,MATH101,高等数学,5.0,王教授,数学学院,120,110,,,必修课,2024-2025-1,
3,ENG101,大学英语,2.0,张教授,外国语学院,150,120,,,必修课,2024-2025-1,
1 id courseCode courseName credit teacher department capacity enrolled classTime classRoom courseType semester createTime
2 1 CS101 计算机科学导论 3.0 李教授 计算机学院 100 95 必修课 2024-2025-1
3 2 MATH101 高等数学 5.0 王教授 数学学院 120 110 必修课 2024-2025-1
4 3 ENG101 大学英语 2.0 张教授 外国语学院 150 120 必修课 2024-2025-1

43
.trae-cn/worktrees/project/course-analysis copy/output/courses.json

@ -0,0 +1,43 @@
[ {
"id" : 1,
"courseCode" : "CS101",
"courseName" : "计算机科学导论",
"credit" : 3.0,
"teacher" : "李教授",
"department" : "计算机学院",
"capacity" : 100,
"enrolled" : 95,
"classTime" : null,
"classRoom" : null,
"courseType" : "必修课",
"semester" : "2024-2025-1",
"createTime" : null
}, {
"id" : 2,
"courseCode" : "MATH101",
"courseName" : "高等数学",
"credit" : 5.0,
"teacher" : "王教授",
"department" : "数学学院",
"capacity" : 120,
"enrolled" : 110,
"classTime" : null,
"classRoom" : null,
"courseType" : "必修课",
"semester" : "2024-2025-1",
"createTime" : null
}, {
"id" : 3,
"courseCode" : "ENG101",
"courseName" : "大学英语",
"credit" : 2.0,
"teacher" : "张教授",
"department" : "外国语学院",
"capacity" : 150,
"enrolled" : 120,
"classTime" : null,
"classRoom" : null,
"courseType" : "必修课",
"semester" : "2024-2025-1",
"createTime" : null
} ]

8
.trae-cn/worktrees/project/course-analysis copy/run.bat

@ -0,0 +1,8 @@
@echo off
echo 正在编译项目...
javac -d target/classes src/main/java/com/example/entity/Course.java src/main/java/com/example/DatabaseUtil.java src/main/java/com/example/CourseAnalysis.java src/main/java/com/example/HnuCourseSystem.java src/main/java/com/example/CourseSystemTest.java
echo 正在运行测试程序...
java -cp target/classes;C:\Users\范馨遥\.m2\repository\org\xerial\sqlite-jdbc\3.44.1.0\sqlite-jdbc-3.44.1.0.jar com.example.CourseSystemTest
pause

261
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/CourseAnalysis.java

@ -0,0 +1,261 @@
package com.example;
import com.example.entity.Course;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.stream.Collectors;
/**
* 课程分析类
* 不依赖Spring Boot使用普通Java类实现
*/
public class CourseAnalysis {
// 无参构造方法
public CourseAnalysis() {
}
// 获取课程类型分布
public Map<String, Integer> getCourseTypeDistribution() {
return DatabaseUtil.getCourseTypeDistribution();
}
// 获取院系课程分布
public Map<String, Integer> getDepartmentDistribution() {
return DatabaseUtil.getDepartmentDistribution();
}
// 获取学分分布
public Map<Double, Integer> getCreditDistribution() {
Map<Double, Integer> distribution = new HashMap<>();
List<Course> courses = DatabaseUtil.getAllCourses();
for (Course course : courses) {
double credit = course.getCredit();
distribution.put(credit, distribution.getOrDefault(credit, 0) + 1);
}
return distribution;
}
// 获取热门课程
public List<Map<String, Object>> getTopCourses() {
return DatabaseUtil.getTopCourses();
}
// 获取课程容量使用率
public List<Map<String, Object>> getCourseUsageRate() {
List<Map<String, Object>> usageRates = new ArrayList<>();
List<Course> courses = DatabaseUtil.getAllCourses();
for (Course course : courses) {
Map<String, Object> usageRate = new HashMap<>();
usageRate.put("courseName", course.getCourseName());
usageRate.put("capacity", course.getCapacity());
usageRate.put("enrolled", course.getEnrolled());
double rate = course.getCapacity() > 0 ? (double) course.getEnrolled() / course.getCapacity() * 100 : 0;
usageRate.put("usageRate", rate);
usageRates.add(usageRate);
}
// 按使用率排序
usageRates.sort((a, b) -> Double.compare((Double) b.get("usageRate"), (Double) a.get("usageRate")));
return usageRates;
}
// 获取整体统计信息
public Map<String, Object> getOverallStatistics() {
Map<String, Object> statistics = new HashMap<>();
try {
List<Course> allCourses = DatabaseUtil.getAllCourses();
// 总课程数
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);
// 总容量
int totalCapacity = allCourses.stream()
.mapToInt(Course::getCapacity)
.sum();
statistics.put("totalCapacity", totalCapacity);
// 总已选人数
int totalEnrolled = allCourses.stream()
.mapToInt(Course::getEnrolled)
.sum();
statistics.put("totalEnrolled", totalEnrolled);
// 总体使用率
double overallUsageRate = totalCapacity > 0 ? (double) totalEnrolled / totalCapacity * 100 : 0;
statistics.put("overallUsageRate", overallUsageRate);
// 课程类型数量
long requiredCourses = allCourses.stream()
.filter(course -> "必修课".equals(course.getCourseType()))
.count();
statistics.put("requiredCourses", requiredCourses);
long electiveCourses = allCourses.stream()
.filter(course -> "选修课".equals(course.getCourseType()))
.count();
statistics.put("electiveCourses", electiveCourses);
// 院系数量
long departmentCount = allCourses.stream()
.map(Course::getDepartment)
.distinct()
.count();
statistics.put("departmentCount", departmentCount);
} catch (Exception e) {
System.err.println("获取整体统计信息失败:" + e.getMessage());
// 返回默认值
statistics.put("totalCourses", 0);
statistics.put("totalCredits", 0.0);
statistics.put("averageCredit", 0.0);
statistics.put("totalCapacity", 0);
statistics.put("totalEnrolled", 0);
statistics.put("overallUsageRate", 0.0);
statistics.put("requiredCourses", 0);
statistics.put("electiveCourses", 0);
statistics.put("departmentCount", 0);
}
return statistics;
}
// 获取按院系分组的课程统计
public Map<String, Map<String, Object>> getDepartmentStatistics() {
Map<String, Map<String, Object>> deptStats = new HashMap<>();
try {
List<Course> allCourses = DatabaseUtil.getAllCourses();
// 按院系分组
Map<String, List<Course>> coursesByDept = allCourses.stream()
.collect(Collectors.groupingBy(Course::getDepartment));
for (Map.Entry<String, List<Course>> entry : coursesByDept.entrySet()) {
String department = entry.getKey();
List<Course> deptCourses = entry.getValue();
Map<String, Object> stats = new HashMap<>();
stats.put("courseCount", deptCourses.size());
double deptCredits = deptCourses.stream()
.mapToDouble(Course::getCredit)
.sum();
stats.put("totalCredits", deptCredits);
int deptCapacity = deptCourses.stream()
.mapToInt(Course::getCapacity)
.sum();
stats.put("totalCapacity", deptCapacity);
int deptEnrolled = deptCourses.stream()
.mapToInt(Course::getEnrolled)
.sum();
stats.put("totalEnrolled", deptEnrolled);
double deptUsageRate = deptCapacity > 0 ? (double) deptEnrolled / deptCapacity * 100 : 0;
stats.put("usageRate", deptUsageRate);
deptStats.put(department, stats);
}
} catch (Exception e) {
System.err.println("获取院系统计信息失败:" + e.getMessage());
}
return deptStats;
}
// 获取教师课程统计
public Map<String, Map<String, Object>> getTeacherStatistics() {
Map<String, Map<String, Object>> teacherStats = new HashMap<>();
try {
List<Course> allCourses = DatabaseUtil.getAllCourses();
// 按教师分组
Map<String, List<Course>> coursesByTeacher = allCourses.stream()
.collect(Collectors.groupingBy(Course::getTeacher));
for (Map.Entry<String, List<Course>> entry : coursesByTeacher.entrySet()) {
String teacher = entry.getKey();
List<Course> teacherCourses = entry.getValue();
Map<String, Object> stats = new HashMap<>();
stats.put("courseCount", teacherCourses.size());
double totalCredits = teacherCourses.stream()
.mapToDouble(Course::getCredit)
.sum();
stats.put("totalCredits", totalCredits);
int totalEnrolled = teacherCourses.stream()
.mapToInt(Course::getEnrolled)
.sum();
stats.put("totalEnrolled", totalEnrolled);
teacherStats.put(teacher, stats);
}
} catch (Exception e) {
System.err.println("获取教师统计信息失败:" + e.getMessage());
}
return teacherStats;
}
// 获取课程容量利用率分析
public Map<String, Object> getCapacityAnalysis() {
Map<String, Object> analysis = new HashMap<>();
try {
List<Course> allCourses = DatabaseUtil.getAllCourses();
int totalCapacity = allCourses.stream()
.mapToInt(Course::getCapacity)
.sum();
int totalEnrolled = allCourses.stream()
.mapToInt(Course::getEnrolled)
.sum();
int availableCapacity = totalCapacity - totalEnrolled;
// 计算不同使用率区间的课程数量
long highUsage = allCourses.stream()
.filter(course -> course.getCapacity() > 0 &&
(double) course.getEnrolled() / course.getCapacity() >= 0.9)
.count();
long mediumUsage = allCourses.stream()
.filter(course -> course.getCapacity() > 0 &&
(double) course.getEnrolled() / course.getCapacity() >= 0.5 &&
(double) course.getEnrolled() / course.getCapacity() < 0.9)
.count();
long lowUsage = allCourses.stream()
.filter(course -> course.getCapacity() > 0 &&
(double) course.getEnrolled() / course.getCapacity() < 0.5)
.count();
analysis.put("totalCapacity", totalCapacity);
analysis.put("totalEnrolled", totalEnrolled);
analysis.put("availableCapacity", availableCapacity);
analysis.put("highUsageCourses", highUsage);
analysis.put("mediumUsageCourses", mediumUsage);
analysis.put("lowUsageCourses", lowUsage);
} catch (Exception e) {
System.err.println("获取容量分析失败:" + e.getMessage());
}
return analysis;
}
}

180
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/CourseSystemTest.java

@ -0,0 +1,180 @@
package com.example;
import com.example.controller.CourseController;
import com.example.crawler.CrawlerService;
import com.example.entity.Course;
import com.example.exception.BizException;
import com.example.exception.ParseException;
/**
* 测试类用于验证湖大选课系统功能MVC架构 + 策略模式爬虫
*/
public class CourseSystemTest {
public static void main(String[] args) {
// 测试爬虫功能
testCrawler();
// 创建Controller
CourseController controller = new CourseController();
// 初始化数据库
controller.initDatabase();
// 清空现有数据
controller.clearAllCourses();
// 添加测试数据
addTestData(controller);
// 测试整体统计信息
System.out.println("\n===== 测试整体统计信息 =====");
System.out.println(controller.getOverallStatistics());
// 测试课程类型分布
System.out.println("\n===== 测试课程类型分布 =====");
System.out.println(controller.getCourseTypeDistribution());
// 测试院系统计
System.out.println("\n===== 测试院系统计 =====");
System.out.println(controller.getDepartmentDistribution());
// 测试学分分布
System.out.println("\n===== 测试学分分布 =====");
System.out.println(controller.getCreditDistribution());
// 测试热门课程
System.out.println("\n===== 测试热门课程 =====");
System.out.println(controller.getTopCourses());
// 测试课程容量使用率
System.out.println("\n===== 测试课程容量使用率 =====");
System.out.println(controller.getCourseUsageRate());
// 测试按院系分组的统计
System.out.println("\n===== 测试按院系分组的统计 =====");
System.out.println(controller.getDepartmentStatistics());
// 测试教师课程统计
System.out.println("\n===== 测试教师课程统计 =====");
System.out.println(controller.getTeacherStatistics());
// 测试课程容量利用率分析
System.out.println("\n===== 测试课程容量利用率分析 =====");
System.out.println(controller.getCapacityAnalysis());
System.out.println("\n===== 测试完成 =====");
}
/**
* 测试爬虫多网站解析功能策略模式
*/
private static void testCrawler() {
System.out.println("===== 测试爬虫多网站解析功能 =====");
CrawlerService crawler = new CrawlerService();
System.out.println("支持的网站: " + crawler.getSupportedWebsites());
// 测试湖南大学解析
System.out.println("\n--- 测试湖南大学解析 ---");
try {
Course[] hnuCourses = crawler.simulateCrawl("hnu");
System.out.println("解析到 " + hnuCourses.length + " 门课程");
for (Course c : hnuCourses) {
System.out.println(" - " + c.getCourseName() + " (" + c.getCourseCode() + ")");
System.out.println(" 教师: " + c.getTeacher() + ", 学分: " + c.getCredit());
}
} catch (ParseException | BizException e) {
System.err.println("湖南大学解析失败: " + e.getMessage());
}
// 测试山东大学解析
System.out.println("\n--- 测试山东大学解析 ---");
try {
Course[] sduCourses = crawler.simulateCrawl("sdu");
System.out.println("解析到 " + sduCourses.length + " 门课程");
for (Course c : sduCourses) {
System.out.println(" - " + c.getCourseName() + " (" + c.getCourseCode() + ")");
System.out.println(" 教师: " + c.getTeacher() + ", 学分: " + c.getCredit());
}
} catch (ParseException | BizException e) {
System.err.println("山东大学解析失败: " + e.getMessage());
}
}
// 添加测试数据
private static void addTestData(CourseController controller) {
Course course1 = new Course();
course1.setCourseCode("CS101");
course1.setCourseName("计算机科学导论");
course1.setCredit(3.0);
course1.setTeacher("张教授");
course1.setDepartment("计算机学院");
course1.setCapacity(100);
course1.setEnrolled(95);
course1.setClassTime("周一 8:00-10:00");
course1.setClassRoom("A101");
course1.setCourseType("必修课");
course1.setSemester("2024春季");
controller.saveCourse(course1);
Course course2 = new Course();
course2.setCourseCode("CS102");
course2.setCourseName("数据结构");
course2.setCredit(4.0);
course2.setTeacher("李教授");
course2.setDepartment("计算机学院");
course2.setCapacity(80);
course2.setEnrolled(75);
course2.setClassTime("周二 10:00-12:00");
course2.setClassRoom("A102");
course2.setCourseType("必修课");
course2.setSemester("2024春季");
controller.saveCourse(course2);
Course course3 = new Course();
course3.setCourseCode("MATH101");
course3.setCourseName("高等数学");
course3.setCredit(5.0);
course3.setTeacher("王教授");
course3.setDepartment("数学学院");
course3.setCapacity(120);
course3.setEnrolled(110);
course3.setClassTime("周三 8:00-10:00");
course3.setClassRoom("B101");
course3.setCourseType("必修课");
course3.setSemester("2024春季");
controller.saveCourse(course3);
Course course4 = new Course();
course4.setCourseCode("ENG101");
course4.setCourseName("大学英语");
course4.setCredit(2.0);
course4.setTeacher("刘教授");
course4.setDepartment("外国语学院");
course4.setCapacity(150);
course4.setEnrolled(120);
course4.setClassTime("周四 14:00-16:00");
course4.setClassRoom("C101");
course4.setCourseType("选修课");
course4.setSemester("2024春季");
controller.saveCourse(course4);
Course course5 = new Course();
course5.setCourseCode("PHYS101");
course5.setCourseName("大学物理");
course5.setCredit(4.0);
course5.setTeacher("陈教授");
course5.setDepartment("物理学院");
course5.setCapacity(90);
course5.setEnrolled(85);
course5.setClassTime("周五 10:00-12:00");
course5.setClassRoom("D101");
course5.setCourseType("必修课");
course5.setSemester("2024春季");
controller.saveCourse(course5);
System.out.println("测试数据添加完成,共添加 5 门课程");
}
}

195
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/DatabaseUtil.java

@ -0,0 +1,195 @@
package com.example;
import com.example.entity.Course;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
/**
* 数据库工具类
* 提供数据库操作相关方法
*/
public class DatabaseUtil {
// 数据库URL
private static final String DB_URL = "jdbc:sqlite:course.db";
// 静态初始化块,加载SQLite驱动
static {
try {
Class.forName("org.sqlite.JDBC");
System.out.println("SQLite驱动加载成功");
} catch (ClassNotFoundException e) {
System.err.println("SQLite驱动加载失败: " + e.getMessage());
}
}
// 初始化数据库
public static void initDatabase() {
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement()) {
// 创建courses表
String createTableSQL = "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" +
")";
stmt.executeUpdate(createTableSQL);
System.out.println("数据库初始化成功");
} catch (SQLException e) {
System.err.println("数据库初始化失败: " + e.getMessage());
}
}
// 获取所有课程
public static List<Course> getAllCourses() {
List<Course> courses = new ArrayList<>();
String sql = "SELECT * FROM courses";
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
Course course = new Course();
course.setId(rs.getLong("id"));
course.setCourseCode(rs.getString("course_code"));
course.setCourseName(rs.getString("course_name"));
course.setCredit(rs.getDouble("credit"));
course.setTeacher(rs.getString("teacher"));
course.setDepartment(rs.getString("department"));
course.setCapacity(rs.getInt("capacity"));
course.setEnrolled(rs.getInt("enrolled"));
course.setClassTime(rs.getString("class_time"));
course.setClassRoom(rs.getString("class_room"));
course.setCourseType(rs.getString("course_type"));
course.setSemester(rs.getString("semester"));
courses.add(course);
}
} catch (SQLException e) {
System.err.println("获取课程列表失败: " + e.getMessage());
}
return courses;
}
// 获取课程类型分布
public static Map<String, Integer> getCourseTypeDistribution() {
Map<String, Integer> distribution = new HashMap<>();
String sql = "SELECT course_type, COUNT(*) as count FROM courses GROUP BY course_type";
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
String type = rs.getString("course_type");
int count = rs.getInt("count");
distribution.put(type, count);
}
} catch (SQLException e) {
System.err.println("获取课程类型分布失败: " + e.getMessage());
}
return distribution;
}
// 获取院系分布
public static Map<String, Integer> getDepartmentDistribution() {
Map<String, Integer> distribution = new HashMap<>();
String sql = "SELECT department, COUNT(*) as count FROM courses GROUP BY department";
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
String department = rs.getString("department");
int count = rs.getInt("count");
distribution.put(department, count);
}
} catch (SQLException e) {
System.err.println("获取院系分布失败: " + e.getMessage());
}
return distribution;
}
// 获取热门课程
public static List<Map<String, Object>> getTopCourses() {
List<Map<String, Object>> topCourses = new ArrayList<>();
String sql = "SELECT course_name, teacher, department, capacity, enrolled FROM courses ORDER BY enrolled DESC LIMIT 10";
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
Map<String, Object> course = new HashMap<>();
course.put("courseName", rs.getString("course_name"));
course.put("teacher", rs.getString("teacher"));
course.put("department", rs.getString("department"));
course.put("capacity", rs.getInt("capacity"));
course.put("enrolled", rs.getInt("enrolled"));
topCourses.add(course);
}
} catch (SQLException e) {
System.err.println("获取热门课程失败: " + e.getMessage());
}
return topCourses;
}
// 保存课程
public static void saveCourse(Course course) {
String sql = "INSERT INTO courses (course_code, course_name, credit, teacher, department, capacity, enrolled, class_time, class_room, course_type, semester) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
try (Connection conn = DriverManager.getConnection(DB_URL);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, course.getCourseCode());
pstmt.setString(2, course.getCourseName());
pstmt.setDouble(3, course.getCredit());
pstmt.setString(4, course.getTeacher());
pstmt.setString(5, course.getDepartment());
pstmt.setInt(6, course.getCapacity());
pstmt.setInt(7, course.getEnrolled());
pstmt.setString(8, course.getClassTime());
pstmt.setString(9, course.getClassRoom());
pstmt.setString(10, course.getCourseType());
pstmt.setString(11, course.getSemester());
pstmt.executeUpdate();
} catch (SQLException e) {
System.err.println("保存课程失败: " + e.getMessage());
}
}
// 清空课程数据
public static void clearCourses() {
String sql = "DELETE FROM courses";
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement()) {
stmt.executeUpdate(sql);
System.out.println("课程数据清空成功");
} catch (SQLException e) {
System.err.println("清空课程数据失败: " + e.getMessage());
}
}
}

105
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExceptionDemo.java

@ -0,0 +1,105 @@
package com.example;
import com.example.crawler.CrawlerService;
import com.example.exception.BizException;
import com.example.exception.ParseException;
/**
* 异常体系演示类 - 独立展示各种异常场景
*/
public class ExceptionDemo {
public static void main(String[] args) {
System.out.println("══════════════════════════════════════════");
System.out.println(" 异常体系测试演示");
System.out.println("══════════════════════════════════════════\n");
CrawlerService crawler = new CrawlerService();
// 测试1: 正常解析
testNormalParse(crawler);
// 测试2: 不支持的网站 - ParseException
testUnsupportedSite(crawler);
// 测试3: 空URL - BizException
testEmptyUrl(crawler);
// 测试4: 空HTML - BizException
testEmptyHtml(crawler);
// 测试5: null参数 - BizException
testNullUrl(crawler);
System.out.println("\n══════════════════════════════════════════");
System.out.println(" 测试完成!");
System.out.println("══════════════════════════════════════════");
}
private static void testNormalParse(CrawlerService crawler) {
System.out.println("【测试1】正常解析(湖南大学)");
try {
crawler.simulateCrawl("hnu");
System.out.println(" ✓ 解析成功");
} catch (Exception e) {
System.out.println(" ✗ 失败: " + e.getMessage());
}
System.out.println();
}
private static void testUnsupportedSite(CrawlerService crawler) {
System.out.println("【测试2】不支持的网站 → ParseException");
try {
crawler.parse("https://www.unknown.edu.cn", "<html></html>");
} catch (ParseException e) {
System.out.println(" ✓ 捕获异常:");
System.out.println(" 错误码: " + e.getErrorCode());
System.out.println(" 错误信息: " + e.getMessage());
} catch (Exception e) {
System.out.println(" ✗ 其他异常: " + e.getMessage());
}
System.out.println();
}
private static void testEmptyUrl(CrawlerService crawler) {
System.out.println("【测试3】空URL → BizException");
try {
crawler.parse("", "<html></html>");
} catch (BizException e) {
System.out.println(" ✓ 捕获异常:");
System.out.println(" 错误码: " + e.getErrorCode());
System.out.println(" 错误信息: " + e.getMessage());
} catch (Exception e) {
System.out.println(" ✗ 其他异常: " + e.getMessage());
}
System.out.println();
}
private static void testEmptyHtml(CrawlerService crawler) {
System.out.println("【测试4】空HTML内容 → BizException");
try {
crawler.parse("https://www.hnu.edu.cn", "");
} catch (BizException e) {
System.out.println(" ✓ 捕获异常:");
System.out.println(" 错误码: " + e.getErrorCode());
System.out.println(" 错误信息: " + e.getMessage());
} catch (Exception e) {
System.out.println(" ✗ 其他异常: " + e.getMessage());
}
System.out.println();
}
private static void testNullUrl(CrawlerService crawler) {
System.out.println("【测试5】null参数 → BizException");
try {
crawler.parse(null, "<html></html>");
} catch (BizException e) {
System.out.println(" ✓ 捕获异常:");
System.out.println(" 错误码: " + e.getErrorCode());
System.out.println(" 错误信息: " + e.getMessage());
} catch (Exception e) {
System.out.println(" ✗ 其他异常: " + e.getMessage());
}
System.out.println();
}
}

68
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExceptionTest.java

@ -0,0 +1,68 @@
package com.example;
import com.example.crawler.CrawlerService;
import com.example.exception.BizException;
import com.example.exception.ParseException;
/**
* 异常体系测试类 - 演示各种异常场景
*/
public class ExceptionTest {
public static void main(String[] args) {
CrawlerService crawler = new CrawlerService();
System.out.println("===== 测试异常体系 =====");
// 测试1: 正常解析(不抛出异常)
System.out.println("\n--- 测试1: 正常解析(湖南大学)---");
try {
crawler.simulateCrawl("hnu");
System.out.println("✓ 解析成功");
} catch (Exception e) {
System.out.println("✗ 失败: " + e.getMessage());
}
// 测试2: 测试不支持的网站(抛出ParseException)
System.out.println("\n--- 测试2: 测试不支持的网站 ---");
try {
crawler.parse("https://www.unknown.edu.cn", "<html></html>");
} catch (ParseException e) {
System.out.println("✓ ParseException: " + e.getErrorCode() + " - " + e.getMessage());
} catch (Exception e) {
System.out.println("✗ 其他异常: " + e.getMessage());
}
// 测试3: 测试空URL(抛出BizException)
System.out.println("\n--- 测试3: 测试空URL ---");
try {
crawler.parse("", "<html></html>");
} catch (BizException e) {
System.out.println("✓ BizException: " + e.getErrorCode() + " - " + e.getMessage());
} catch (Exception e) {
System.out.println("✗ 其他异常: " + e.getMessage());
}
// 测试4: 测试空HTML(抛出BizException)
System.out.println("\n--- 测试4: 测试空HTML ---");
try {
crawler.parse("https://www.hnu.edu.cn", "");
} catch (BizException e) {
System.out.println("✓ BizException: " + e.getErrorCode() + " - " + e.getMessage());
} catch (Exception e) {
System.out.println("✗ 其他异常: " + e.getMessage());
}
// 测试5: 测试null参数(抛出BizException)
System.out.println("\n--- 测试5: 测试null参数 ---");
try {
crawler.parse(null, "<html></html>");
} catch (BizException e) {
System.out.println("✓ BizException: " + e.getErrorCode() + " - " + e.getMessage());
} catch (Exception e) {
System.out.println("✗ 其他异常: " + e.getMessage());
}
System.out.println("\n===== 异常测试完成 =====");
}
}

86
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExportTest.java

@ -0,0 +1,86 @@
package com.example;
import com.example.entity.Course;
import com.example.export.ExportService;
import java.util.ArrayList;
import java.util.List;
public class ExportTest {
public static void main(String[] args) {
System.out.println("══════════════════════════════════════════");
System.out.println(" 数据导出功能测试");
System.out.println("══════════════════════════════════════════\n");
List<Course> courses = createTestData();
ExportService exportService = new ExportService();
// 测试JSON导出
System.out.println("【测试1】导出为JSON格式");
try {
exportService.exportToJson(courses, "output/courses.json");
System.out.println(" ✓ JSON导出成功\n");
} catch (Exception e) {
System.out.println(" ✗ JSON导出失败: " + e.getMessage() + "\n");
}
// 测试CSV导出
System.out.println("【测试2】导出为CSV格式");
try {
exportService.exportToCsv(courses, "output/courses.csv");
System.out.println(" ✓ CSV导出成功\n");
} catch (Exception e) {
System.out.println(" ✗ CSV导出失败: " + e.getMessage() + "\n");
}
System.out.println("══════════════════════════════════════════");
System.out.println(" 导出测试完成!");
System.out.println("══════════════════════════════════════════");
}
private static List<Course> createTestData() {
List<Course> courses = new ArrayList<>();
Course c1 = new Course();
c1.setId(1L);
c1.setCourseCode("CS101");
c1.setCourseName("计算机科学导论");
c1.setCredit(3.0);
c1.setTeacher("李教授");
c1.setDepartment("计算机学院");
c1.setCapacity(100);
c1.setEnrolled(95);
c1.setCourseType("必修课");
c1.setSemester("2024-2025-1");
courses.add(c1);
Course c2 = new Course();
c2.setId(2L);
c2.setCourseCode("MATH101");
c2.setCourseName("高等数学");
c2.setCredit(5.0);
c2.setTeacher("王教授");
c2.setDepartment("数学学院");
c2.setCapacity(120);
c2.setEnrolled(110);
c2.setCourseType("必修课");
c2.setSemester("2024-2025-1");
courses.add(c2);
Course c3 = new Course();
c3.setId(3L);
c3.setCourseCode("ENG101");
c3.setCourseName("大学英语");
c3.setCredit(2.0);
c3.setTeacher("张教授");
c3.setDepartment("外国语学院");
c3.setCapacity(150);
c3.setEnrolled(120);
c3.setCourseType("必修课");
c3.setSemester("2024-2025-1");
courses.add(c3);
return courses;
}
}

54
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/HnuCourseSystem.java

@ -0,0 +1,54 @@
package com.example;
import com.example.controller.CourseController;
import com.example.view.CourseView;
/**
* 湖大选课系统主应用类
* MVC架构入口协调Controller和View
*/
public class HnuCourseSystem {
public static void main(String[] args) {
// 创建Controller和View
CourseController controller = new CourseController();
CourseView view = new CourseView();
// 初始化数据库
controller.initDatabase();
try {
int choice;
do {
// 显示菜单
view.displayMenu();
// 获取用户选择
choice = view.getUserChoice();
// 根据选择执行相应操作
switch (choice) {
case 1 -> view.displayOverallStatistics(controller.getOverallStatistics());
case 2 -> view.displayCourseTypeDistribution(controller.getCourseTypeDistribution());
case 3 -> view.displayDepartmentDistribution(controller.getDepartmentDistribution());
case 4 -> view.displayCreditDistribution(controller.getCreditDistribution());
case 5 -> view.displayTopCourses(controller.getTopCourses());
case 6 -> view.displayCourseUsageRate(controller.getCourseUsageRate());
case 7 -> view.displayDepartmentStatistics(controller.getDepartmentStatistics());
case 0 -> view.displayExitMessage();
default -> view.displayInvalidChoice();
}
// 非退出操作时等待用户确认
if (choice != 0) {
view.displayContinuePrompt();
}
} while (choice != 0);
} finally {
view.close();
}
}
}

89
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/Pair.java

@ -0,0 +1,89 @@
package com.example;
/**
* 泛型Pair类用于存储两个不同类型的值
* @param <K> 键的类型
* @param <V> 值的类型
*/
public class Pair<K, V> {
private K key;
private V value;
/**
* 构造方法
* @param key
* @param value
*/
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
/**
* 获取键
* @return
*/
public K getKey() {
return key;
}
/**
* 设置键
* @param key
*/
public void setKey(K key) {
this.key = key;
}
/**
* 获取值
* @return
*/
public V getValue() {
return value;
}
/**
* 设置值
* @param value
*/
public void setValue(V value) {
this.value = value;
}
@Override
public String toString() {
return "Pair{" +
"key=" + key +
", value=" + value +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Pair<?, ?> pair = (Pair<?, ?>) o;
if (key != null ? !key.equals(pair.key) : pair.key != null) return false;
return value != null ? value.equals(pair.value) : pair.value == null;
}
@Override
public int hashCode() {
int result = key != null ? key.hashCode() : 0;
result = 31 * result + (value != null ? value.hashCode() : 0);
return result;
}
/**
* 静态工厂方法创建Pair实例
* @param key
* @param value
* @param <K> 键的类型
* @param <V> 值的类型
* @return Pair实例
*/
public static <K, V> Pair<K, V> of(K key, V value) {
return new Pair<>(key, value);
}
}

68
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/controller/CourseController.java

@ -0,0 +1,68 @@
package com.example.controller;
import com.example.entity.Course;
import com.example.service.CourseService;
import java.util.List;
import java.util.Map;
/**
* 课程控制器Controller
* 负责接收用户请求调用Service层返回结果
*/
public class CourseController {
private final CourseService service;
public CourseController() {
this.service = new CourseService();
}
public void initDatabase() {
service.initDatabase();
}
public void clearAllCourses() {
service.clearAllCourses();
}
public void saveCourse(Course course) {
service.saveCourse(course);
}
public Map<String, Object> getOverallStatistics() {
return service.getOverallStatistics();
}
public Map<String, Integer> getCourseTypeDistribution() {
return service.getCourseTypeDistribution();
}
public Map<String, Integer> getDepartmentDistribution() {
return service.getDepartmentDistribution();
}
public Map<Double, Integer> getCreditDistribution() {
return service.getCreditDistribution();
}
public List<Map<String, Object>> getTopCourses() {
return service.getTopCourses();
}
public List<Map<String, Object>> getCourseUsageRate() {
return service.getCourseUsageRate();
}
public Map<String, Map<String, Object>> getDepartmentStatistics() {
return service.getDepartmentStatistics();
}
public Map<String, Map<String, Object>> getTeacherStatistics() {
return service.getTeacherStatistics();
}
public Map<String, Object> getCapacityAnalysis() {
return service.getCapacityAnalysis();
}
}

78
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/CrawlerContext.java

@ -0,0 +1,78 @@
package com.example.crawler;
import java.util.ArrayList;
import java.util.List;
import com.example.crawler.strategy.ParseStrategy;
import com.example.entity.Course;
/**
* 爬虫上下文类
* 负责策略的选择和执行
*/
public class CrawlerContext {
private ParseStrategy strategy;
private final List<ParseStrategy> strategies;
public CrawlerContext() {
this.strategies = new ArrayList<>();
}
public void registerStrategy(ParseStrategy strategy) {
if (!strategies.contains(strategy)) {
strategies.add(strategy);
System.out.println("注册解析策略: " + strategy.getName());
}
}
public void registerStrategies(ParseStrategy... newStrategies) {
for (ParseStrategy strategy : newStrategies) {
registerStrategy(strategy);
}
}
/**
* 根据URL自动选择合适的解析策略
* @return 是否找到合适的策略
*/
public boolean selectStrategy(String url) {
this.strategy = strategies.stream()
.filter(s -> s.supports(url))
.findFirst()
.orElse(null);
if (strategy != null) {
System.out.println("选择解析策略: " + strategy.getName());
return true;
}
return false;
}
public Course[] executeParse(String html) {
if (strategy == null) {
throw new IllegalStateException("请先选择解析策略");
}
return strategy.parse(html);
}
public void setStrategy(ParseStrategy strategy) {
this.strategy = strategy;
}
public ParseStrategy getStrategy() {
return strategy;
}
public List<ParseStrategy> getAllStrategies() {
return new ArrayList<>(strategies);
}
public List<String> getSupportedWebsites() {
List<String> websites = new ArrayList<>();
for (ParseStrategy s : strategies) {
websites.add(s.getName());
}
return websites;
}
}

221
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/CrawlerService.java

@ -0,0 +1,221 @@
package com.example.crawler;
import com.example.crawler.strategy.HnuParseStrategy;
import com.example.crawler.strategy.ParseStrategy;
import com.example.crawler.strategy.SduParseStrategy;
import com.example.entity.Course;
import com.example.exception.BizException;
import com.example.exception.NetworkException;
import com.example.exception.ParseException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/**
* 爬虫服务类
* 提供网页爬取和课程解析功能
*/
public class CrawlerService {
private final CrawlerContext context;
public CrawlerService() {
this.context = new CrawlerContext();
initStrategies();
}
private void initStrategies() {
context.registerStrategies(
new HnuParseStrategy(),
new SduParseStrategy()
);
}
public void addStrategy(ParseStrategy strategy) {
context.registerStrategy(strategy);
}
public java.util.List<ParseStrategy> getStrategies() {
return context.getAllStrategies();
}
/**
* 爬取指定URL并解析课程信息
* @throws NetworkException 网络异常
* @throws ParseException 解析异常
* @throws BizException 业务异常
*/
public Course[] crawl(String url) throws NetworkException, ParseException, BizException {
// 参数校验
if (url == null || url.trim().isEmpty()) {
throw new BizException(BizException.DATA_VALIDATION_ERROR, "URL不能为空");
}
try {
// 下载HTML内容
String html = downloadHtml(url);
// 选择解析策略
if (!context.selectStrategy(url)) {
throw new ParseException(ParseException.NO_STRATEGY_FOUND,
"不支持的网站: " + url + ",支持的网站: " + getSupportedWebsites());
}
// 执行解析
Course[] courses = context.executeParse(html);
if (courses == null || courses.length == 0) {
throw new BizException(BizException.RESOURCE_NOT_FOUND,
"未解析到任何课程信息");
}
return courses;
} catch (NetworkException e) {
throw e;
} catch (ParseException e) {
throw e;
} catch (MalformedURLException e) {
throw new BizException(BizException.DATA_VALIDATION_ERROR,
"URL格式不正确: " + url, e);
} catch (java.net.SocketTimeoutException e) {
throw new NetworkException(NetworkException.CONNECTION_TIMEOUT,
"连接超时: " + url, e);
} catch (java.net.UnknownHostException e) {
throw new NetworkException(NetworkException.UNKNOWN_HOST,
"无法解析主机: " + url, e);
} catch (java.io.IOException e) {
throw new NetworkException(NetworkException.CONNECTION_REFUSED,
"网络连接失败: " + url, e);
} catch (Exception e) {
throw new BizException(BizException.SERVICE_UNAVAILABLE,
"爬取服务异常: " + e.getMessage(), e);
}
}
/**
* 直接解析HTML内容
* @throws ParseException 解析异常
* @throws BizException 业务异常
*/
public Course[] parse(String url, String html) throws ParseException, BizException {
if (url == null || url.trim().isEmpty()) {
throw new BizException(BizException.DATA_VALIDATION_ERROR, "URL不能为空");
}
if (html == null || html.trim().isEmpty()) {
throw new BizException(BizException.DATA_VALIDATION_ERROR, "HTML内容不能为空");
}
if (!context.selectStrategy(url)) {
throw new ParseException(ParseException.NO_STRATEGY_FOUND,
"不支持的网站: " + url);
}
return context.executeParse(html);
}
/**
* 下载网页HTML内容
*/
private String downloadHtml(String urlStr) throws Exception {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
conn.setReadTimeout(10000);
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
int responseCode = conn.getResponseCode();
if (responseCode >= 400) {
throw new NetworkException(NetworkException.HTTP_ERROR,
"HTTP错误码: " + responseCode);
}
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder html = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
html.append(line).append("\n");
}
return html.toString();
} finally {
conn.disconnect();
}
}
public java.util.List<String> getSupportedWebsites() {
return context.getSupportedWebsites();
}
/**
* 模拟爬取用于测试
*/
public Course[] simulateCrawl(String siteType) throws ParseException, BizException {
String mockHtml = generateMockHtml(siteType);
String mockUrl = switch (siteType.toLowerCase()) {
case "hnu" -> "https://www.hnu.edu.cn";
case "sdu" -> "https://www.sdu.edu.cn";
default -> "https://www.example.edu.cn";
};
return parse(mockUrl, mockHtml);
}
private String generateMockHtml(String siteType) {
if (siteType.equalsIgnoreCase("hnu")) {
return """
<html>
<div class="course-item">
<span class="course-code">CS101</span>
<span class="course-name">计算机科学导论</span>
<span class="teacher">张教授</span>
<span class="department">计算机学院</span>
<span class="credit">3.0</span>
<span class="capacity">100</span>
<span class="enrolled">95</span>
<span class="class-time">周一 8:00-10:00</span>
<span class="class-room">A101</span>
<span class="course-type">必修课</span>
</div>
<div class="course-item">
<span class="course-code">CS102</span>
<span class="course-name">数据结构</span>
<span class="teacher">李教授</span>
<span class="department">计算机学院</span>
<span class="credit">4.0</span>
<span class="capacity">80</span>
<span class="enrolled">75</span>
<span class="class-time">周二 10:00-12:00</span>
<span class="class-room">A102</span>
<span class="course-type">必修课</span>
</div>
</html>
""";
} else {
return """
<html>
<div class="sdu-course-item">
<span class="sdu-code">MATH101</span>
<span class="sdu-name">高等数学</span>
<span class="sdu-teacher">王教授</span>
<span class="sdu-dept">数学学院</span>
<span class="sdu-credit">5.0</span>
<span class="sdu-capacity">120</span>
<span class="sdu-enrolled">110</span>
<span class="sdu-time">周三 8:00-10:00</span>
<span class="sdu-room">B101</span>
<span class="sdu-type">必修课</span>
</div>
</html>
""";
}
}
}

99
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/HnuParseStrategy.java

@ -0,0 +1,99 @@
package com.example.crawler.strategy;
import com.example.entity.Course;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
/**
* 湖南大学课程解析策略
* 针对湖大选课系统的HTML结构进行解析
*/
public class HnuParseStrategy implements ParseStrategy {
private static final String DOMAIN = "hnu.edu.cn";
private static final String NAME = "湖南大学解析策略";
@Override
public Course[] parse(String html) {
Document doc = Jsoup.parse(html);
Elements courseElements = doc.select("div.course-item, tr.course-row, .course-list .item");
return courseElements.stream()
.map(this::parseCourseElement)
.filter(course -> course.getCourseName() != null && !course.getCourseName().isEmpty())
.toArray(Course[]::new);
}
private Course parseCourseElement(Element element) {
Course course = new Course();
// 尝试多种选择器解析湖大课程信息
course.setCourseCode(getText(element, ".course-code", "code", "td:nth-child(1)"));
course.setCourseName(getText(element, ".course-name", ".title", "td:nth-child(2)", "h3"));
course.setTeacher(getText(element, ".teacher", ".instructor", "td:nth-child(3)"));
course.setDepartment(getText(element, ".department", ".college", "td:nth-child(4)"));
course.setCourseType(getText(element, ".course-type", ".type", "td:nth-child(5)"));
// 解析数值字段
try {
course.setCredit(parseDouble(getText(element, ".credit", ".credits", "td:nth-child(6)")));
} catch (Exception e) {
course.setCredit(0.0);
}
try {
course.setCapacity(parseInt(getText(element, ".capacity", ".max-students", "td:nth-child(7)")));
} catch (Exception e) {
course.setCapacity(0);
}
try {
course.setEnrolled(parseInt(getText(element, ".enrolled", ".current-students", "td:nth-child(8)")));
} catch (Exception e) {
course.setEnrolled(0);
}
course.setClassTime(getText(element, ".class-time", ".time", ".schedule"));
course.setClassRoom(getText(element, ".class-room", ".room", ".location"));
course.setSemester("2024春季");
return course;
}
private String getText(Element element, String... selectors) {
for (String selector : selectors) {
Element found = element.selectFirst(selector);
if (found != null && !found.text().trim().isEmpty()) {
return found.text().trim();
}
}
return "";
}
private double parseDouble(String value) {
if (value == null || value.isEmpty()) {
return 0.0;
}
return Double.parseDouble(value.replaceAll("[^0-9.]", ""));
}
private int parseInt(String value) {
if (value == null || value.isEmpty()) {
return 0;
}
return Integer.parseInt(value.replaceAll("[^0-9]", ""));
}
@Override
public boolean supports(String url) {
return url != null && (url.contains(DOMAIN) || url.contains("hunan.edu"));
}
@Override
public String getName() {
return NAME;
}
}

30
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/ParseStrategy.java

@ -0,0 +1,30 @@
package com.example.crawler.strategy;
import com.example.entity.Course;
/**
* 解析策略接口
* 定义不同网站的课程解析规范
*/
public interface ParseStrategy {
/**
* 解析HTML内容提取课程信息
* @param html 网页HTML内容
* @return 课程数组
*/
Course[] parse(String html);
/**
* 判断该策略是否支持指定URL
* @param url 目标网站URL
* @return 是否支持
*/
boolean supports(String url);
/**
* 获取策略名称
* @return 策略名称
*/
String getName();
}

98
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/SduParseStrategy.java

@ -0,0 +1,98 @@
package com.example.crawler.strategy;
import com.example.entity.Course;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
/**
* 山东大学课程解析策略
* 针对山大选课系统的HTML结构进行解析
*/
public class SduParseStrategy implements ParseStrategy {
private static final String DOMAIN = "sdu.edu.cn";
private static final String NAME = "山东大学解析策略";
@Override
public Course[] parse(String html) {
Document doc = Jsoup.parse(html);
Elements courseElements = doc.select("div.sdu-course-item, table.course-table tr, .course-card");
return courseElements.stream()
.map(this::parseCourseElement)
.filter(course -> course.getCourseName() != null && !course.getCourseName().isEmpty())
.toArray(Course[]::new);
}
private Course parseCourseElement(Element element) {
Course course = new Course();
// 山东大学特有的解析逻辑
course.setCourseCode(getText(element, ".sdu-code", ".course-id", ".code"));
course.setCourseName(getText(element, ".sdu-name", ".course-title", ".title"));
course.setTeacher(getText(element, ".sdu-teacher", ".professor", ".teacher-name"));
course.setDepartment(getText(element, ".sdu-dept", ".school", ".department"));
course.setCourseType(getText(element, ".sdu-type", ".course-category", ".type"));
try {
course.setCredit(parseDouble(getText(element, ".sdu-credit", ".credit-value")));
} catch (Exception e) {
course.setCredit(0.0);
}
try {
course.setCapacity(parseInt(getText(element, ".sdu-capacity", ".max-num")));
} catch (Exception e) {
course.setCapacity(0);
}
try {
course.setEnrolled(parseInt(getText(element, ".sdu-enrolled", ".registered")));
} catch (Exception e) {
course.setEnrolled(0);
}
course.setClassTime(getText(element, ".sdu-time", ".schedule"));
course.setClassRoom(getText(element, ".sdu-room", ".location"));
course.setSemester("2024春季");
return course;
}
private String getText(Element element, String... selectors) {
for (String selector : selectors) {
Element found = element.selectFirst(selector);
if (found != null && !found.text().trim().isEmpty()) {
return found.text().trim();
}
}
return "";
}
private double parseDouble(String value) {
if (value == null || value.isEmpty()) {
return 0.0;
}
return Double.parseDouble(value.replaceAll("[^0-9.]", ""));
}
private int parseInt(String value) {
if (value == null || value.isEmpty()) {
return 0;
}
return Integer.parseInt(value.replaceAll("[^0-9]", ""));
}
@Override
public boolean supports(String url) {
return url != null && (url.contains(DOMAIN) || url.contains("shandong.edu"));
}
@Override
public String getName() {
return NAME;
}
}

127
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/entity/Course.java

@ -0,0 +1,127 @@
package com.example.entity;
import java.time.LocalDateTime;
/**
* 课程实体类
* 不依赖Spring Boot使用普通Java类实现
*/
public class Course {
private Long id;
private String courseCode;
private String courseName;
private Double credit;
private String teacher;
private String department;
private Integer capacity;
private Integer enrolled;
private String classTime;
private String classRoom;
private String courseType;
private String semester;
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 Integer getCapacity() {
return capacity;
}
public void setCapacity(Integer capacity) {
this.capacity = capacity;
}
public Integer getEnrolled() {
return enrolled;
}
public void setEnrolled(Integer 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;
}
}

31
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/BizException.java

@ -0,0 +1,31 @@
package com.example.exception;
/**
* 业务异常
* 用于处理业务逻辑相关的错误如数据校验失败权限不足资源不存在等
*/
public class BizException extends CrawlerException {
// 错误码常量
public static final String DATA_VALIDATION_ERROR = "BIZ_VALIDATION_ERROR";
public static final String RESOURCE_NOT_FOUND = "BIZ_RESOURCE_NOT_FOUND";
public static final String DUPLICATE_DATA = "BIZ_DUPLICATE_DATA";
public static final String PERMISSION_DENIED = "BIZ_PERMISSION_DENIED";
public static final String SERVICE_UNAVAILABLE = "BIZ_SERVICE_UNAVAILABLE";
public BizException(String message) {
super(DATA_VALIDATION_ERROR, message);
}
public BizException(String errorCode, String message) {
super(errorCode, message);
}
public BizException(String message, Throwable cause) {
super(DATA_VALIDATION_ERROR, message, cause);
}
public BizException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
}

34
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/CrawlerException.java

@ -0,0 +1,34 @@
package com.example.exception;
/**
* 爬虫基异常类
* 所有爬虫相关异常的父类
*/
public class CrawlerException extends Exception {
private final String errorCode;
public CrawlerException(String message) {
super(message);
this.errorCode = "CRAWLER_ERROR";
}
public CrawlerException(String message, Throwable cause) {
super(message, cause);
this.errorCode = "CRAWLER_ERROR";
}
public CrawlerException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public CrawlerException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}

30
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/NetworkException.java

@ -0,0 +1,30 @@
package com.example.exception;
/**
* 网络异常
* 用于处理网络相关的错误如连接超时无法连接HTTP错误等
*/
public class NetworkException extends CrawlerException {
public static final String CONNECTION_TIMEOUT = "NETWORK_TIMEOUT";
public static final String CONNECTION_REFUSED = "NETWORK_REFUSED";
public static final String HTTP_ERROR = "NETWORK_HTTP_ERROR";
public static final String SSL_ERROR = "NETWORK_SSL_ERROR";
public static final String UNKNOWN_HOST = "NETWORK_UNKNOWN_HOST";
public NetworkException(String message) {
super(CONNECTION_TIMEOUT, message);
}
public NetworkException(String errorCode, String message) {
super(errorCode, message);
}
public NetworkException(String message, Throwable cause) {
super(CONNECTION_TIMEOUT, message, cause);
}
public NetworkException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
}

31
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/ParseException.java

@ -0,0 +1,31 @@
package com.example.exception;
/**
* 解析异常
* 用于处理HTML/JSON解析相关的错误如格式错误字段缺失等
*/
public class ParseException extends CrawlerException {
// 错误码常量
public static final String HTML_PARSE_ERROR = "PARSE_HTML_ERROR";
public static final String JSON_PARSE_ERROR = "PARSE_JSON_ERROR";
public static final String FIELD_MISSING = "PARSE_FIELD_MISSING";
public static final String FORMAT_ERROR = "PARSE_FORMAT_ERROR";
public static final String NO_STRATEGY_FOUND = "PARSE_NO_STRATEGY";
public ParseException(String message) {
super(HTML_PARSE_ERROR, message);
}
public ParseException(String errorCode, String message) {
super(errorCode, message);
}
public ParseException(String message, Throwable cause) {
super(HTML_PARSE_ERROR, message, cause);
}
public ParseException(String errorCode, String message, Throwable cause) {
super(errorCode, message, cause);
}
}

60
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/CsvExporter.java

@ -0,0 +1,60 @@
package com.example.export;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;
public class CsvExporter<T> implements Exporter<T> {
private static final String CSV_SEPARATOR = ",";
@Override
public void export(List<T> data, String filePath) {
if (data == null || data.isEmpty()) {
throw new IllegalArgumentException("数据不能为空");
}
try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) {
T firstItem = data.get(0);
writer.println(getHeader(firstItem.getClass()));
for (T item : data) {
writer.println(toCsvRow(item));
}
System.out.println("数据已导出到CSV文件: " + filePath);
} catch (Exception e) {
throw new RuntimeException("CSV导出失败: " + e.getMessage(), e);
}
}
private String getHeader(Class<?> clazz) {
StringBuilder header = new StringBuilder();
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
if (i > 0) header.append(CSV_SEPARATOR);
header.append(fields[i].getName());
}
return header.toString();
}
private String toCsvRow(T item) {
StringBuilder row = new StringBuilder();
Field[] fields = item.getClass().getDeclaredFields();
try {
for (int i = 0; i < fields.length; i++) {
if (i > 0) row.append(CSV_SEPARATOR);
fields[i].setAccessible(true);
Object value = fields[i].get(item);
row.append(value != null ? value.toString() : "");
}
} catch (Exception e) {
throw new RuntimeException("CSV行转换失败: " + e.getMessage(), e);
}
return row.toString();
}
@Override
public String getFormat() {
return "CSV";
}
}

28
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/ExportService.java

@ -0,0 +1,28 @@
package com.example.export;
import java.util.List;
public class ExportService {
public enum ExportFormat {
JSON, CSV
}
public <T> void exportToJson(List<T> data, String filePath) {
new JsonExporter<T>().export(data, filePath);
}
public <T> void exportToCsv(List<T> data, String filePath) {
new CsvExporter<T>().export(data, filePath);
}
public <T> Exporter<T> getExporter(ExportFormat format) {
switch (format) {
case JSON:
return new JsonExporter<>();
case CSV:
return new CsvExporter<>();
default:
throw new IllegalArgumentException("不支持的导出格式: " + format);
}
}
}

8
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/Exporter.java

@ -0,0 +1,8 @@
package com.example.export;
import java.util.List;
public interface Exporter<T> {
void export(List<T> data, String filePath);
String getFormat();
}

33
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/JsonExporter.java

@ -0,0 +1,33 @@
package com.example.export;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.File;
import java.util.List;
public class JsonExporter<T> implements Exporter<T> {
private final ObjectMapper objectMapper;
public JsonExporter() {
this.objectMapper = new ObjectMapper();
this.objectMapper.registerModule(new JavaTimeModule());
this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
}
@Override
public void export(List<T> data, String filePath) {
try {
objectMapper.writeValue(new File(filePath), data);
System.out.println("数据已导出到JSON文件: " + filePath);
} catch (Exception e) {
throw new RuntimeException("JSON导出失败: " + e.getMessage(), e);
}
}
@Override
public String getFormat() {
return "JSON";
}
}

182
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/repository/CourseRepository.java

@ -0,0 +1,182 @@
package com.example.repository;
import com.example.entity.Course;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
/**
* 课程数据访问层Repository
* 负责与数据库交互提供数据访问接口
*/
public class CourseRepository {
private static final String DB_URL = "jdbc:sqlite:course.db";
static {
try {
Class.forName("org.sqlite.JDBC");
} catch (ClassNotFoundException e) {
System.err.println("SQLite驱动加载失败: " + e.getMessage());
}
}
public void initDatabase() {
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement()) {
String createTableSQL = "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" +
")";
stmt.executeUpdate(createTableSQL);
} catch (SQLException e) {
System.err.println("数据库初始化失败: " + e.getMessage());
}
}
public List<Course> findAll() {
List<Course> courses = new ArrayList<>();
String sql = "SELECT * FROM courses";
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
courses.add(mapResultSetToCourse(rs));
}
} catch (SQLException e) {
System.err.println("获取课程列表失败: " + e.getMessage());
}
return courses;
}
public Map<String, Integer> findCourseTypeDistribution() {
Map<String, Integer> distribution = new HashMap<>();
String sql = "SELECT course_type, COUNT(*) as count FROM courses GROUP BY course_type";
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
distribution.put(rs.getString("course_type"), rs.getInt("count"));
}
} catch (SQLException e) {
System.err.println("获取课程类型分布失败: " + e.getMessage());
}
return distribution;
}
public Map<String, Integer> findDepartmentDistribution() {
Map<String, Integer> distribution = new HashMap<>();
String sql = "SELECT department, COUNT(*) as count FROM courses GROUP BY department";
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
distribution.put(rs.getString("department"), rs.getInt("count"));
}
} catch (SQLException e) {
System.err.println("获取院系分布失败: " + e.getMessage());
}
return distribution;
}
public List<Map<String, Object>> findTopCourses() {
List<Map<String, Object>> topCourses = new ArrayList<>();
String sql = "SELECT course_name, teacher, department, capacity, enrolled FROM courses ORDER BY enrolled DESC LIMIT 10";
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
Map<String, Object> course = new HashMap<>();
course.put("courseName", rs.getString("course_name"));
course.put("teacher", rs.getString("teacher"));
course.put("department", rs.getString("department"));
course.put("capacity", rs.getInt("capacity"));
course.put("enrolled", rs.getInt("enrolled"));
topCourses.add(course);
}
} catch (SQLException e) {
System.err.println("获取热门课程失败: " + e.getMessage());
}
return topCourses;
}
public void save(Course course) {
String sql = "INSERT INTO courses (course_code, course_name, credit, teacher, department, capacity, enrolled, class_time, class_room, course_type, semester) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
try (Connection conn = DriverManager.getConnection(DB_URL);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, course.getCourseCode());
pstmt.setString(2, course.getCourseName());
pstmt.setDouble(3, course.getCredit());
pstmt.setString(4, course.getTeacher());
pstmt.setString(5, course.getDepartment());
pstmt.setInt(6, course.getCapacity());
pstmt.setInt(7, course.getEnrolled());
pstmt.setString(8, course.getClassTime());
pstmt.setString(9, course.getClassRoom());
pstmt.setString(10, course.getCourseType());
pstmt.setString(11, course.getSemester());
pstmt.executeUpdate();
} catch (SQLException e) {
System.err.println("保存课程失败: " + e.getMessage());
}
}
public void clearAll() {
String sql = "DELETE FROM courses";
try (Connection conn = DriverManager.getConnection(DB_URL);
Statement stmt = conn.createStatement()) {
stmt.executeUpdate(sql);
} catch (SQLException e) {
System.err.println("清空课程数据失败: " + e.getMessage());
}
}
private Course mapResultSetToCourse(ResultSet rs) throws SQLException {
Course course = new Course();
course.setId(rs.getLong("id"));
course.setCourseCode(rs.getString("course_code"));
course.setCourseName(rs.getString("course_name"));
course.setCredit(rs.getDouble("credit"));
course.setTeacher(rs.getString("teacher"));
course.setDepartment(rs.getString("department"));
course.setCapacity(rs.getInt("capacity"));
course.setEnrolled(rs.getInt("enrolled"));
course.setClassTime(rs.getString("class_time"));
course.setClassRoom(rs.getString("class_room"));
course.setCourseType(rs.getString("course_type"));
course.setSemester(rs.getString("semester"));
return course;
}
}

229
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/service/CourseService.java

@ -0,0 +1,229 @@
package com.example.service;
import com.example.entity.Course;
import com.example.repository.CourseRepository;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.stream.Collectors;
/**
* 课程业务逻辑层Service
* 负责封装业务逻辑协调数据访问和业务处理
*/
public class CourseService {
private final CourseRepository repository;
public CourseService() {
this.repository = new CourseRepository();
}
public void initDatabase() {
repository.initDatabase();
}
public void clearAllCourses() {
repository.clearAll();
}
public void saveCourse(Course course) {
repository.save(course);
}
public List<Course> getAllCourses() {
return repository.findAll();
}
public Map<String, Object> getOverallStatistics() {
Map<String, Object> statistics = new HashMap<>();
try {
List<Course> allCourses = repository.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);
int totalCapacity = allCourses.stream()
.mapToInt(Course::getCapacity)
.sum();
statistics.put("totalCapacity", totalCapacity);
int totalEnrolled = allCourses.stream()
.mapToInt(Course::getEnrolled)
.sum();
statistics.put("totalEnrolled", totalEnrolled);
double overallUsageRate = totalCapacity > 0 ? (double) totalEnrolled / totalCapacity * 100 : 0;
statistics.put("overallUsageRate", overallUsageRate);
long requiredCourses = allCourses.stream()
.filter(course -> "必修课".equals(course.getCourseType()))
.count();
statistics.put("requiredCourses", requiredCourses);
long electiveCourses = allCourses.stream()
.filter(course -> "选修课".equals(course.getCourseType()))
.count();
statistics.put("electiveCourses", electiveCourses);
long departmentCount = allCourses.stream()
.map(Course::getDepartment)
.distinct()
.count();
statistics.put("departmentCount", departmentCount);
} catch (Exception e) {
System.err.println("获取整体统计信息失败:" + e.getMessage());
setDefaultStatistics(statistics);
}
return statistics;
}
private void setDefaultStatistics(Map<String, Object> statistics) {
statistics.put("totalCourses", 0);
statistics.put("totalCredits", 0.0);
statistics.put("averageCredit", 0.0);
statistics.put("totalCapacity", 0);
statistics.put("totalEnrolled", 0);
statistics.put("overallUsageRate", 0.0);
statistics.put("requiredCourses", 0);
statistics.put("electiveCourses", 0);
statistics.put("departmentCount", 0);
}
public Map<String, Integer> getCourseTypeDistribution() {
return repository.findCourseTypeDistribution();
}
public Map<String, Integer> getDepartmentDistribution() {
return repository.findDepartmentDistribution();
}
public Map<Double, Integer> getCreditDistribution() {
Map<Double, Integer> distribution = new HashMap<>();
List<Course> courses = repository.findAll();
for (Course course : courses) {
double credit = course.getCredit();
distribution.put(credit, distribution.getOrDefault(credit, 0) + 1);
}
return distribution;
}
public List<Map<String, Object>> getTopCourses() {
return repository.findTopCourses();
}
public List<Map<String, Object>> getCourseUsageRate() {
List<Map<String, Object>> usageRates = new ArrayList<>();
List<Course> courses = repository.findAll();
for (Course course : courses) {
Map<String, Object> usageRate = new HashMap<>();
usageRate.put("courseName", course.getCourseName());
usageRate.put("capacity", course.getCapacity());
usageRate.put("enrolled", course.getEnrolled());
double rate = course.getCapacity() > 0 ? (double) course.getEnrolled() / course.getCapacity() * 100 : 0;
usageRate.put("usageRate", rate);
usageRates.add(usageRate);
}
usageRates.sort((a, b) -> Double.compare((Double) b.get("usageRate"), (Double) a.get("usageRate")));
return usageRates;
}
public Map<String, Map<String, Object>> getDepartmentStatistics() {
Map<String, Map<String, Object>> deptStats = new HashMap<>();
try {
List<Course> allCourses = repository.findAll();
Map<String, List<Course>> coursesByDept = allCourses.stream()
.collect(Collectors.groupingBy(Course::getDepartment));
for (Map.Entry<String, List<Course>> entry : coursesByDept.entrySet()) {
String department = entry.getKey();
List<Course> deptCourses = entry.getValue();
Map<String, Object> stats = new HashMap<>();
stats.put("courseCount", deptCourses.size());
stats.put("totalCredits", deptCourses.stream().mapToDouble(Course::getCredit).sum());
stats.put("totalCapacity", deptCourses.stream().mapToInt(Course::getCapacity).sum());
stats.put("totalEnrolled", deptCourses.stream().mapToInt(Course::getEnrolled).sum());
int deptCapacity = (Integer) stats.get("totalCapacity");
int deptEnrolled = (Integer) stats.get("totalEnrolled");
stats.put("usageRate", deptCapacity > 0 ? (double) deptEnrolled / deptCapacity * 100 : 0);
deptStats.put(department, stats);
}
} catch (Exception e) {
System.err.println("获取院系统计信息失败:" + e.getMessage());
}
return deptStats;
}
public Map<String, Map<String, Object>> getTeacherStatistics() {
Map<String, Map<String, Object>> teacherStats = new HashMap<>();
try {
List<Course> allCourses = repository.findAll();
Map<String, List<Course>> coursesByTeacher = allCourses.stream()
.collect(Collectors.groupingBy(Course::getTeacher));
for (Map.Entry<String, List<Course>> entry : coursesByTeacher.entrySet()) {
String teacher = entry.getKey();
List<Course> teacherCourses = entry.getValue();
Map<String, Object> stats = new HashMap<>();
stats.put("courseCount", teacherCourses.size());
stats.put("totalCredits", teacherCourses.stream().mapToDouble(Course::getCredit).sum());
stats.put("totalEnrolled", teacherCourses.stream().mapToInt(Course::getEnrolled).sum());
teacherStats.put(teacher, stats);
}
} catch (Exception e) {
System.err.println("获取教师统计信息失败:" + e.getMessage());
}
return teacherStats;
}
public Map<String, Object> getCapacityAnalysis() {
Map<String, Object> analysis = new HashMap<>();
try {
List<Course> allCourses = repository.findAll();
int totalCapacity = allCourses.stream().mapToInt(Course::getCapacity).sum();
int totalEnrolled = allCourses.stream().mapToInt(Course::getEnrolled).sum();
analysis.put("totalCapacity", totalCapacity);
analysis.put("totalEnrolled", totalEnrolled);
analysis.put("availableCapacity", totalCapacity - totalEnrolled);
analysis.put("highUsageCourses", allCourses.stream()
.filter(c -> c.getCapacity() > 0 && (double) c.getEnrolled() / c.getCapacity() >= 0.9)
.count());
analysis.put("mediumUsageCourses", allCourses.stream()
.filter(c -> c.getCapacity() > 0 && (double) c.getEnrolled() / c.getCapacity() >= 0.5 &&
(double) c.getEnrolled() / c.getCapacity() < 0.9)
.count());
analysis.put("lowUsageCourses", allCourses.stream()
.filter(c -> c.getCapacity() > 0 && (double) c.getEnrolled() / c.getCapacity() < 0.5)
.count());
} catch (Exception e) {
System.err.println("获取容量分析失败:" + e.getMessage());
}
return analysis;
}
}

129
.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/view/CourseView.java

@ -0,0 +1,129 @@
package com.example.view;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
/**
* 课程视图层View
* 负责用户界面的展示和用户输入的获取
*/
public class CourseView {
private final Scanner scanner;
public CourseView() {
this.scanner = new Scanner(System.in);
}
public void close() {
scanner.close();
}
public void displayMenu() {
System.out.println("\n===== 湖大选课系统 =====");
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("0. 退出系统");
System.out.print("请选择操作: ");
}
public int getUserChoice() {
while (!scanner.hasNextInt()) {
System.out.println("请输入有效的数字!");
scanner.next();
System.out.print("请选择操作: ");
}
int choice = scanner.nextInt();
scanner.nextLine();
return choice;
}
public void displayOverallStatistics(Map<String, Object> statistics) {
System.out.println("\n===== 整体统计信息 =====");
System.out.println("总课程数: " + statistics.get("totalCourses"));
System.out.println("总学分: " + statistics.get("totalCredits"));
System.out.println("平均学分: " + String.format("%.2f", statistics.get("averageCredit")));
System.out.println("总容量: " + statistics.get("totalCapacity"));
System.out.println("总已选人数: " + statistics.get("totalEnrolled"));
System.out.println("总体使用率: " + String.format("%.2f%%", statistics.get("overallUsageRate")));
System.out.println("必修课数量: " + statistics.get("requiredCourses"));
System.out.println("选修课数量: " + statistics.get("electiveCourses"));
System.out.println("院系数量: " + statistics.get("departmentCount"));
}
public void displayCourseTypeDistribution(Map<String, Integer> distribution) {
System.out.println("\n===== 课程类型分布 =====");
for (Map.Entry<String, Integer> entry : distribution.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue() + "门");
}
}
public void displayDepartmentDistribution(Map<String, Integer> distribution) {
System.out.println("\n===== 院系统计 =====");
for (Map.Entry<String, Integer> entry : distribution.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue() + "门课程");
}
}
public void displayCreditDistribution(Map<Double, Integer> distribution) {
System.out.println("\n===== 学分分布 =====");
for (Map.Entry<Double, Integer> entry : distribution.entrySet()) {
System.out.println(entry.getKey() + "学分: " + entry.getValue() + "门课程");
}
}
public void displayTopCourses(List<Map<String, Object>> topCourses) {
System.out.println("\n===== 热门课程 =====");
for (int i = 0; i < topCourses.size(); i++) {
Map<String, Object> course = topCourses.get(i);
System.out.println((i + 1) + ". " + course.get("courseName"));
System.out.println(" 教师: " + course.get("teacher"));
System.out.println(" 院系: " + course.get("department"));
System.out.println(" 容量: " + course.get("capacity") + ", 已选: " + course.get("enrolled"));
}
}
public void displayCourseUsageRate(List<Map<String, Object>> usageRates) {
System.out.println("\n===== 课程容量使用率 =====");
int limit = Math.min(10, usageRates.size());
for (int i = 0; i < limit; i++) {
Map<String, Object> usage = usageRates.get(i);
System.out.println((i + 1) + ". " + usage.get("courseName"));
System.out.println(" 容量: " + usage.get("capacity") + ", 已选: " + usage.get("enrolled"));
System.out.println(" 使用率: " + String.format("%.2f%%", usage.get("usageRate")));
}
}
public void displayDepartmentStatistics(Map<String, Map<String, Object>> deptStats) {
System.out.println("\n===== 按院系分组的统计 =====");
for (Map.Entry<String, Map<String, Object>> entry : deptStats.entrySet()) {
String department = entry.getKey();
Map<String, Object> stats = entry.getValue();
System.out.println("院系: " + department);
System.out.println(" 课程数量: " + stats.get("courseCount"));
System.out.println(" 总学分: " + stats.get("totalCredits"));
System.out.println(" 总容量: " + stats.get("totalCapacity"));
System.out.println(" 总已选人数: " + stats.get("totalEnrolled"));
System.out.println(" 使用率: " + String.format("%.2f%%", stats.get("usageRate")));
}
}
public void displayExitMessage() {
System.out.println("\n退出系统...");
}
public void displayInvalidChoice() {
System.out.println("无效选择,请重新输入!");
}
public void displayContinuePrompt() {
System.out.println("\n按Enter键继续...");
scanner.nextLine();
}
}

33
.trae-cn/worktrees/project/course-analysis copy/src/main/resources/application.properties

@ -0,0 +1,33 @@
spring.application.name=hnu-course-analysis
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=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.open-in-view=false
spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:schema.sql
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
logging.level.root=INFO
logging.level.com.example=DEBUG
# 湖南大学选课系统配置
crawler.hnu.login.url=https://jwxt.hnu.edu.cn/xtgl/login_login.html
crawler.hnu.username=your_username
crawler.hnu.password=your_password
crawler.hnu.course.list.url=https://jwxt.hnu.edu.cn/kbcx/xskbcx_cxXsKb.html
# Selenium配置
selenium.webdriver.chrome.driver.path=C:/chromedriver/chromedriver.exe
selenium.webdriver.headless=false

31
.trae-cn/worktrees/project/course-analysis copy/src/main/resources/schema.sql

@ -0,0 +1,31 @@
-- 湖南大学选课系统数据库初始化脚本
-- 创建课程表
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节', 'A101', '必修课', '2024-2025-1'),
('CS102', '数据结构', 4.0, '李教授', '计算机科学与技术学院', 80, 70, '周二 3-4节', 'B202', '必修课', '2024-2025-1'),
('CS103', '算法设计', 3.0, '王教授', '计算机科学与技术学院', 60, 55, '周三 5-6节', 'C303', '选修课', '2024-2025-1'),
('MA101', '高等数学', 5.0, '赵教授', '数学学院', 120, 110, '周一 3-4节', 'D404', '必修课', '2024-2025-1'),
('EN101', '大学英语', 3.0, '刘教授', '外国语学院', 100, 90, '周二 1-2节', 'E505', '必修课', '2024-2025-1'),
('PH101', '大学物理', 4.0, '钱教授', '物理学院', 90, 85, '周三 1-2节', 'F606', '必修课', '2024-2025-1'),
('CH101', '大学化学', 3.0, '孙教授', '化学学院', 70, 65, '周四 3-4节', 'G707', '选修课', '2024-2025-1'),
('HI101', '中国近现代史', 2.0, '周教授', '历史学院', 150, 145, '周五 1-2节', 'H808', '必修课', '2024-2025-1'),
('PE101', '体育', 1.0, '吴教授', '体育学院', 50, 50, '周三 7-8节', '体育馆', '必修课', '2024-2025-1'),
('AR101', '艺术鉴赏', 2.0, '郑教授', '艺术学院', 80, 75, '周四 5-6节', '艺术楼', '选修课', '2024-2025-1');

328
.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/analysis.html

@ -0,0 +1,328 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<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.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
background-color: #f8f9fa;
}
.hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 0;
margin-bottom: 30px;
}
.card {
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.card-header {
background-color: #667eea;
color: white;
border-radius: 10px 10px 0 0;
}
.btn-primary {
background-color: #667eea;
border-color: #667eea;
}
.btn-primary:hover {
background-color: #5a6fd8;
border-color: #5a6fd8;
}
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">湖南大学选课系统分析</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-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="hero">
<div class="container">
<h1 class="display-4">数据分析</h1>
<p class="lead">深入分析选课系统数据,获取有价值的洞察</p>
</div>
</div>
<!-- 分析图表 -->
<div class="container mb-5">
<!-- 课程类型分布 -->
<div class="card">
<div class="card-header">
<h3>课程类型分布</h3>
</div>
<div class="card-body">
<div id="courseTypeChart" style="width: 100%; height: 400px;"></div>
</div>
</div>
<!-- 院系分布 -->
<div class="card">
<div class="card-header">
<h3>院系课程分布</h3>
</div>
<div class="card-body">
<div id="departmentChart" style="width: 100%; height: 400px;"></div>
</div>
</div>
<!-- 学分分布 -->
<div class="card">
<div class="card-header">
<h3>学分分布</h3>
</div>
<div class="card-body">
<div id="creditChart" style="width: 100%; height: 400px;"></div>
</div>
</div>
<!-- 课程使用率 -->
<div class="card">
<div class="card-header">
<h3>课程容量使用率</h3>
</div>
<div class="card-body">
<div id="usageRateChart" style="width: 100%; height: 400px;"></div>
</div>
</div>
<!-- 院系统计 -->
<div class="card">
<div class="card-header">
<h3>院系统计详情</h3>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>院系</th>
<th>课程数</th>
<th>总学分</th>
<th>总容量</th>
<th>总已选</th>
<th>使用率</th>
</tr>
</thead>
<tbody>
<tr th:each="dept : ${departmentStats}">
<td th:text="${dept.key}"></td>
<td th:text="${dept.value.courseCount}"></td>
<td th:text="${dept.value.totalCredits}"></td>
<td th:text="${dept.value.totalCapacity}"></td>
<td th:text="${dept.value.totalEnrolled}"></td>
<td th:text="${#numbers.formatDecimal(dept.value.usageRate, 0, 1)}%"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="bg-dark text-white py-4">
<div class="container text-center">
<p>&copy; 2024 湖南大学选课系统分析平台</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 初始化课程类型分布图表
fetch('/api/course-type-distribution')
.then(response => response.json())
.then(data => {
const types = Object.keys(data);
const counts = Object.values(data);
const chart = echarts.init(document.getElementById('courseTypeChart'));
chart.setOption({
title: {
text: '课程类型分布',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [{
name: '课程类型',
type: 'pie',
radius: '60%',
data: types.map((type, index) => ({
name: type,
value: counts[index]
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
});
});
// 初始化院系分布图表
fetch('/api/department-distribution')
.then(response => response.json())
.then(data => {
const departments = Object.keys(data);
const counts = Object.values(data);
const chart = echarts.init(document.getElementById('departmentChart'));
chart.setOption({
title: {
text: '院系课程分布',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [{
name: '院系',
type: 'pie',
radius: '60%',
data: departments.map((dept, index) => ({
name: dept,
value: counts[index]
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
});
});
// 初始化学分分布图表
fetch('/api/credit-distribution')
.then(response => response.json())
.then(data => {
const credits = Object.keys(data).map(Number).sort((a, b) => a - b);
const counts = credits.map(credit => data[credit]);
const chart = echarts.init(document.getElementById('creditChart'));
chart.setOption({
title: {
text: '学分分布',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: credits.map(c => c + '学分'),
name: '学分'
},
yAxis: {
type: 'value',
name: '课程数'
},
series: [{
data: counts,
type: 'bar',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: '#667eea'},
{offset: 1, color: '#764ba2'}
])
}
}]
});
});
// 初始化课程使用率图表
fetch('/api/top-courses')
.then(response => response.json())
.then(data => {
const courseNames = data.map(item => item.course_name);
const enrolledCounts = data.map(item => item.enrolled);
const chart = echarts.init(document.getElementById('usageRateChart'));
chart.setOption({
title: {
text: '热门课程使用率',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'category',
data: courseNames,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: '已选人数'
},
series: [{
data: enrolledCounts,
type: 'bar',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: '#667eea'},
{offset: 1, color: '#764ba2'}
])
}
}]
});
});
// 响应式调整
window.addEventListener('resize', function() {
const charts = ['courseTypeChart', 'departmentChart', 'creditChart', 'usageRateChart'];
charts.forEach(id => {
const chart = echarts.getInstanceByDom(document.getElementById(id));
if (chart) {
chart.resize();
}
});
});
</script>
</body>
</html>

169
.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/course-list.html

@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<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.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
background-color: #f8f9fa;
}
.hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 0;
margin-bottom: 30px;
}
.card {
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.table-container {
background: white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
padding: 20px;
}
.btn-primary {
background-color: #667eea;
border-color: #667eea;
}
.btn-primary:hover {
background-color: #5a6fd8;
border-color: #5a6fd8;
}
.status-badge {
padding: 5px 10px;
border-radius: 20px;
font-size: 0.8rem;
}
.status-high {
background-color: #f8d7da;
color: #721c24;
}
.status-medium {
background-color: #fff3cd;
color: #856404;
}
.status-low {
background-color: #d4edda;
color: #155724;
}
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">湖南大学选课系统分析</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-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="hero">
<div class="container">
<h1 class="display-4">课程列表</h1>
<p class="lead">查看所有课程的详细信息</p>
</div>
</div>
<!-- 课程列表 -->
<div class="container mb-5">
<div class="table-container">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>课程详情</h2>
<button class="btn btn-primary" onclick="crawlData()">
<i class="bi bi-download"></i> 刷新数据
</button>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>课程代码</th>
<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>
<span th:class="|status-badge ${course.enrolled * 100 / course.capacity > 80 ? 'status-high' : (course.enrolled * 100 / course.capacity > 50 ? 'status-medium' : 'status-low')}">
<span th:text="${#numbers.formatDecimal(course.enrolled * 100 / course.capacity, 0, 1)}%"></span>
</span>
</td>
<td th:text="${course.classTime}"></td>
<td th:text="${course.classRoom}"></td>
<td th:text="${course.courseType}"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="bg-dark text-white py-4">
<div class="container text-center">
<p>&copy; 2024 湖南大学选课系统分析平台</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 爬取数据
function crawlData() {
fetch('/crawl', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
window.location.reload();
} else {
alert('爬取失败');
}
})
.catch(error => {
console.error('Error:', error);
alert('爬取失败');
});
}
</script>
</body>
</html>

24
.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/error.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>

318
.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/index.html

@ -0,0 +1,318 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<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.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
body {
font-family: 'Microsoft YaHei', sans-serif;
background-color: #f8f9fa;
}
.hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 60px 0;
margin-bottom: 30px;
}
.card {
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
.stat-card {
text-align: center;
padding: 20px;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
color: #667eea;
}
.stat-label {
font-size: 1rem;
color: #6c757d;
}
.btn-primary {
background-color: #667eea;
border-color: #667eea;
}
.btn-primary:hover {
background-color: #5a6fd8;
border-color: #5a6fd8;
}
</style>
</head>
<body>
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="/">湖南大学选课系统分析</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-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="hero">
<div class="container">
<h1 class="display-4">湖南大学选课系统数据分析平台</h1>
<p class="lead">实时爬取、分析和可视化展示选课系统数据</p>
<button class="btn btn-light btn-lg" onclick="crawlData()">
<i class="bi bi-download"></i> 爬取最新数据
</button>
</div>
</div>
<!-- 统计卡片 -->
<div class="container mb-5">
<div class="row">
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body">
<div class="stat-value" th:text="${overallStats.totalCourses}"></div>
<div class="stat-label">总课程数</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body">
<div class="stat-value" th:text="${overallStats.totalCredits}"></div>
<div class="stat-label">总学分</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body">
<div class="stat-value" th:text="${overallStats.totalEnrolled}"></div>
<div class="stat-label">总已选人数</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card stat-card">
<div class="card-body">
<div class="stat-value" th:text="${#numbers.formatDecimal(overallStats.overallUsageRate, 0, 1)}%">
使用率
</div>
<div class="stat-label">总体使用率</div>
</div>
</div>
</div>
</div>
</div>
<!-- 热门课程 -->
<div class="container mb-5">
<h2 class="text-center mb-4">热门课程排行</h2>
<div class="card">
<div class="card-body">
<div id="topCoursesChart" style="width: 100%; height: 400px;"></div>
</div>
</div>
</div>
<!-- 课程类型分布 -->
<div class="container mb-5">
<h2 class="text-center mb-4">课程类型分布</h2>
<div class="card">
<div class="card-body">
<div id="courseTypeChart" style="width: 100%; height: 400px;"></div>
</div>
</div>
</div>
<!-- 院系分布 -->
<div class="container mb-5">
<h2 class="text-center mb-4">院系课程分布</h2>
<div class="card">
<div class="card-body">
<div id="departmentChart" style="width: 100%; height: 400px;"></div>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="bg-dark text-white py-4">
<div class="container text-center">
<p>&copy; 2024 湖南大学选课系统分析平台</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 爬取数据
function crawlData() {
fetch('/crawl', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(data.message);
window.location.reload();
} else {
alert('爬取失败');
}
})
.catch(error => {
console.error('Error:', error);
alert('爬取失败');
});
}
// 初始化热门课程图表
fetch('/api/top-courses')
.then(response => response.json())
.then(data => {
const courseNames = data.map(item => item.course_name);
const enrolledCounts = data.map(item => item.enrolled);
const chart = echarts.init(document.getElementById('topCoursesChart'));
chart.setOption({
title: {
text: '热门课程排行',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
xAxis: {
type: 'category',
data: courseNames,
axisLabel: {
rotate: 45
}
},
yAxis: {
type: 'value',
name: '已选人数'
},
series: [{
data: enrolledCounts,
type: 'bar',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{offset: 0, color: '#667eea'},
{offset: 1, color: '#764ba2'}
])
}
}]
});
});
// 初始化课程类型分布图表
fetch('/api/course-type-distribution')
.then(response => response.json())
.then(data => {
const types = Object.keys(data);
const counts = Object.values(data);
const chart = echarts.init(document.getElementById('courseTypeChart'));
chart.setOption({
title: {
text: '课程类型分布',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [{
name: '课程类型',
type: 'pie',
radius: '60%',
data: types.map((type, index) => ({
name: type,
value: counts[index]
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
});
});
// 初始化院系分布图表
fetch('/api/department-distribution')
.then(response => response.json())
.then(data => {
const departments = Object.keys(data);
const counts = Object.values(data);
const chart = echarts.init(document.getElementById('departmentChart'));
chart.setOption({
title: {
text: '院系课程分布',
left: 'center'
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [{
name: '院系',
type: 'pie',
radius: '60%',
data: departments.map((dept, index) => ({
name: dept,
value: counts[index]
})),
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
});
});
// 响应式调整
window.addEventListener('resize', function() {
const charts = ['topCoursesChart', 'courseTypeChart', 'departmentChart'];
charts.forEach(id => {
const chart = echarts.getInstanceByDom(document.getElementById(id));
if (chart) {
chart.resize();
}
});
});
</script>
</body>
</html>
Loading…
Cancel
Save