commit
4f6283e6bd
36 changed files with 3537 additions and 0 deletions
@ -0,0 +1,25 @@ |
|||
# Build outputs |
|||
target/ |
|||
*.class |
|||
|
|||
# IDE |
|||
.idea/ |
|||
*.iml |
|||
.vscode/ |
|||
|
|||
# Logs |
|||
*.log |
|||
|
|||
# OS |
|||
.DS_Store |
|||
Thumbs.db |
|||
|
|||
# Database |
|||
*.db |
|||
*.db-journal |
|||
|
|||
# Temp files |
|||
*.xml |
|||
*.ps1 |
|||
report.txt |
|||
Untitled-*.java |
|||
@ -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 |
|||
} ] |
|||
@ -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 |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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 门课程"); |
|||
} |
|||
} |
|||
@ -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()); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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===== 异常测试完成 ====="); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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> |
|||
"""; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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"; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
@ -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"; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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 |
|||
@ -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'); |
|||
@ -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>© 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> |
|||
@ -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>© 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> |
|||
@ -0,0 +1,24 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="zh-CN"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<title>错误页面</title> |
|||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> |
|||
</head> |
|||
<body> |
|||
<div class="container mt-5"> |
|||
<div class="row justify-content-center"> |
|||
<div class="col-md-6"> |
|||
<div class="card"> |
|||
<div class="card-body text-center"> |
|||
<h1 class="text-danger">500 - 服务器内部错误</h1> |
|||
<p class="mt-3">服务器遇到了一个内部错误,请稍后再试。</p> |
|||
<a href="/" class="btn btn-primary mt-4">返回首页</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</body> |
|||
</html> |
|||
@ -0,0 +1,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>© 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…
Reference in new issue