commit 4f6283e6bd9fa7a7e6f4089a30ec92dc6c89fcb9 Author: 范馨遥 <3603458499qq.com> Date: Fri May 29 22:47:07 2026 +0800 Initial commit: 课程爬虫项目 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..af570f5 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/output/courses.csv b/.trae-cn/worktrees/project/course-analysis copy/output/courses.csv new file mode 100644 index 0000000..3a13a5d --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/output/courses.csv @@ -0,0 +1,4 @@ +id,courseCode,courseName,credit,teacher,department,capacity,enrolled,classTime,classRoom,courseType,semester,createTime +1,CS101,计算机科学导论,3.0,李教授,计算机学院,100,95,,,必修课,2024-2025-1, +2,MATH101,高等数学,5.0,王教授,数学学院,120,110,,,必修课,2024-2025-1, +3,ENG101,大学英语,2.0,张教授,外国语学院,150,120,,,必修课,2024-2025-1, diff --git a/.trae-cn/worktrees/project/course-analysis copy/output/courses.json b/.trae-cn/worktrees/project/course-analysis copy/output/courses.json new file mode 100644 index 0000000..014d50e --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/output/courses.json @@ -0,0 +1,43 @@ +[ { + "id" : 1, + "courseCode" : "CS101", + "courseName" : "计算机科学导论", + "credit" : 3.0, + "teacher" : "李教授", + "department" : "计算机学院", + "capacity" : 100, + "enrolled" : 95, + "classTime" : null, + "classRoom" : null, + "courseType" : "必修课", + "semester" : "2024-2025-1", + "createTime" : null +}, { + "id" : 2, + "courseCode" : "MATH101", + "courseName" : "高等数学", + "credit" : 5.0, + "teacher" : "王教授", + "department" : "数学学院", + "capacity" : 120, + "enrolled" : 110, + "classTime" : null, + "classRoom" : null, + "courseType" : "必修课", + "semester" : "2024-2025-1", + "createTime" : null +}, { + "id" : 3, + "courseCode" : "ENG101", + "courseName" : "大学英语", + "credit" : 2.0, + "teacher" : "张教授", + "department" : "外国语学院", + "capacity" : 150, + "enrolled" : 120, + "classTime" : null, + "classRoom" : null, + "courseType" : "必修课", + "semester" : "2024-2025-1", + "createTime" : null +} ] \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/run.bat b/.trae-cn/worktrees/project/course-analysis copy/run.bat new file mode 100644 index 0000000..e861cb7 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/run.bat @@ -0,0 +1,8 @@ +@echo off +echo 正在编译项目... +javac -d target/classes src/main/java/com/example/entity/Course.java src/main/java/com/example/DatabaseUtil.java src/main/java/com/example/CourseAnalysis.java src/main/java/com/example/HnuCourseSystem.java src/main/java/com/example/CourseSystemTest.java + +echo 正在运行测试程序... +java -cp target/classes;C:\Users\范馨遥\.m2\repository\org\xerial\sqlite-jdbc\3.44.1.0\sqlite-jdbc-3.44.1.0.jar com.example.CourseSystemTest + +pause \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/CourseAnalysis.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/CourseAnalysis.java new file mode 100644 index 0000000..7eba498 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/CourseAnalysis.java @@ -0,0 +1,261 @@ +package com.example; + +import com.example.entity.Course; + +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.stream.Collectors; + +/** + * 课程分析类 + * 不依赖Spring Boot,使用普通Java类实现 + */ +public class CourseAnalysis { + + // 无参构造方法 + public CourseAnalysis() { + } + + // 获取课程类型分布 + public Map getCourseTypeDistribution() { + return DatabaseUtil.getCourseTypeDistribution(); + } + + // 获取院系课程分布 + public Map getDepartmentDistribution() { + return DatabaseUtil.getDepartmentDistribution(); + } + + // 获取学分分布 + public Map getCreditDistribution() { + Map distribution = new HashMap<>(); + List courses = DatabaseUtil.getAllCourses(); + + for (Course course : courses) { + double credit = course.getCredit(); + distribution.put(credit, distribution.getOrDefault(credit, 0) + 1); + } + + return distribution; + } + + // 获取热门课程 + public List> getTopCourses() { + return DatabaseUtil.getTopCourses(); + } + + // 获取课程容量使用率 + public List> getCourseUsageRate() { + List> usageRates = new ArrayList<>(); + List courses = DatabaseUtil.getAllCourses(); + + for (Course course : courses) { + Map 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 getOverallStatistics() { + Map statistics = new HashMap<>(); + try { + List 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> getDepartmentStatistics() { + Map> deptStats = new HashMap<>(); + try { + List allCourses = DatabaseUtil.getAllCourses(); + + // 按院系分组 + Map> coursesByDept = allCourses.stream() + .collect(Collectors.groupingBy(Course::getDepartment)); + + for (Map.Entry> entry : coursesByDept.entrySet()) { + String department = entry.getKey(); + List deptCourses = entry.getValue(); + + Map 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> getTeacherStatistics() { + Map> teacherStats = new HashMap<>(); + try { + List allCourses = DatabaseUtil.getAllCourses(); + + // 按教师分组 + Map> coursesByTeacher = allCourses.stream() + .collect(Collectors.groupingBy(Course::getTeacher)); + + for (Map.Entry> entry : coursesByTeacher.entrySet()) { + String teacher = entry.getKey(); + List teacherCourses = entry.getValue(); + + Map 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 getCapacityAnalysis() { + Map analysis = new HashMap<>(); + try { + List 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; + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/CourseSystemTest.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/CourseSystemTest.java new file mode 100644 index 0000000..f542800 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/CourseSystemTest.java @@ -0,0 +1,180 @@ +package com.example; + +import com.example.controller.CourseController; +import com.example.crawler.CrawlerService; +import com.example.entity.Course; +import com.example.exception.BizException; +import com.example.exception.ParseException; + +/** + * 测试类,用于验证湖大选课系统功能(MVC架构 + 策略模式爬虫) + */ +public class CourseSystemTest { + + public static void main(String[] args) { + // 测试爬虫功能 + testCrawler(); + + // 创建Controller + CourseController controller = new CourseController(); + + // 初始化数据库 + controller.initDatabase(); + + // 清空现有数据 + controller.clearAllCourses(); + + // 添加测试数据 + addTestData(controller); + + // 测试整体统计信息 + System.out.println("\n===== 测试整体统计信息 ====="); + System.out.println(controller.getOverallStatistics()); + + // 测试课程类型分布 + System.out.println("\n===== 测试课程类型分布 ====="); + System.out.println(controller.getCourseTypeDistribution()); + + // 测试院系统计 + System.out.println("\n===== 测试院系统计 ====="); + System.out.println(controller.getDepartmentDistribution()); + + // 测试学分分布 + System.out.println("\n===== 测试学分分布 ====="); + System.out.println(controller.getCreditDistribution()); + + // 测试热门课程 + System.out.println("\n===== 测试热门课程 ====="); + System.out.println(controller.getTopCourses()); + + // 测试课程容量使用率 + System.out.println("\n===== 测试课程容量使用率 ====="); + System.out.println(controller.getCourseUsageRate()); + + // 测试按院系分组的统计 + System.out.println("\n===== 测试按院系分组的统计 ====="); + System.out.println(controller.getDepartmentStatistics()); + + // 测试教师课程统计 + System.out.println("\n===== 测试教师课程统计 ====="); + System.out.println(controller.getTeacherStatistics()); + + // 测试课程容量利用率分析 + System.out.println("\n===== 测试课程容量利用率分析 ====="); + System.out.println(controller.getCapacityAnalysis()); + + System.out.println("\n===== 测试完成 ====="); + } + + /** + * 测试爬虫多网站解析功能(策略模式) + */ + private static void testCrawler() { + System.out.println("===== 测试爬虫多网站解析功能 ====="); + + CrawlerService crawler = new CrawlerService(); + + System.out.println("支持的网站: " + crawler.getSupportedWebsites()); + + // 测试湖南大学解析 + System.out.println("\n--- 测试湖南大学解析 ---"); + try { + Course[] hnuCourses = crawler.simulateCrawl("hnu"); + System.out.println("解析到 " + hnuCourses.length + " 门课程"); + for (Course c : hnuCourses) { + System.out.println(" - " + c.getCourseName() + " (" + c.getCourseCode() + ")"); + System.out.println(" 教师: " + c.getTeacher() + ", 学分: " + c.getCredit()); + } + } catch (ParseException | BizException e) { + System.err.println("湖南大学解析失败: " + e.getMessage()); + } + + // 测试山东大学解析 + System.out.println("\n--- 测试山东大学解析 ---"); + try { + Course[] sduCourses = crawler.simulateCrawl("sdu"); + System.out.println("解析到 " + sduCourses.length + " 门课程"); + for (Course c : sduCourses) { + System.out.println(" - " + c.getCourseName() + " (" + c.getCourseCode() + ")"); + System.out.println(" 教师: " + c.getTeacher() + ", 学分: " + c.getCredit()); + } + } catch (ParseException | BizException e) { + System.err.println("山东大学解析失败: " + e.getMessage()); + } + } + + // 添加测试数据 + private static void addTestData(CourseController controller) { + Course course1 = new Course(); + course1.setCourseCode("CS101"); + course1.setCourseName("计算机科学导论"); + course1.setCredit(3.0); + course1.setTeacher("张教授"); + course1.setDepartment("计算机学院"); + course1.setCapacity(100); + course1.setEnrolled(95); + course1.setClassTime("周一 8:00-10:00"); + course1.setClassRoom("A101"); + course1.setCourseType("必修课"); + course1.setSemester("2024春季"); + controller.saveCourse(course1); + + Course course2 = new Course(); + course2.setCourseCode("CS102"); + course2.setCourseName("数据结构"); + course2.setCredit(4.0); + course2.setTeacher("李教授"); + course2.setDepartment("计算机学院"); + course2.setCapacity(80); + course2.setEnrolled(75); + course2.setClassTime("周二 10:00-12:00"); + course2.setClassRoom("A102"); + course2.setCourseType("必修课"); + course2.setSemester("2024春季"); + controller.saveCourse(course2); + + Course course3 = new Course(); + course3.setCourseCode("MATH101"); + course3.setCourseName("高等数学"); + course3.setCredit(5.0); + course3.setTeacher("王教授"); + course3.setDepartment("数学学院"); + course3.setCapacity(120); + course3.setEnrolled(110); + course3.setClassTime("周三 8:00-10:00"); + course3.setClassRoom("B101"); + course3.setCourseType("必修课"); + course3.setSemester("2024春季"); + controller.saveCourse(course3); + + Course course4 = new Course(); + course4.setCourseCode("ENG101"); + course4.setCourseName("大学英语"); + course4.setCredit(2.0); + course4.setTeacher("刘教授"); + course4.setDepartment("外国语学院"); + course4.setCapacity(150); + course4.setEnrolled(120); + course4.setClassTime("周四 14:00-16:00"); + course4.setClassRoom("C101"); + course4.setCourseType("选修课"); + course4.setSemester("2024春季"); + controller.saveCourse(course4); + + Course course5 = new Course(); + course5.setCourseCode("PHYS101"); + course5.setCourseName("大学物理"); + course5.setCredit(4.0); + course5.setTeacher("陈教授"); + course5.setDepartment("物理学院"); + course5.setCapacity(90); + course5.setEnrolled(85); + course5.setClassTime("周五 10:00-12:00"); + course5.setClassRoom("D101"); + course5.setCourseType("必修课"); + course5.setSemester("2024春季"); + controller.saveCourse(course5); + + System.out.println("测试数据添加完成,共添加 5 门课程"); + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/DatabaseUtil.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/DatabaseUtil.java new file mode 100644 index 0000000..aa461e0 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/DatabaseUtil.java @@ -0,0 +1,195 @@ +package com.example; + +import com.example.entity.Course; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** + * 数据库工具类 + * 提供数据库操作相关方法 + */ +public class DatabaseUtil { + // 数据库URL + private static final String DB_URL = "jdbc:sqlite:course.db"; + + // 静态初始化块,加载SQLite驱动 + static { + try { + Class.forName("org.sqlite.JDBC"); + System.out.println("SQLite驱动加载成功"); + } catch (ClassNotFoundException e) { + System.err.println("SQLite驱动加载失败: " + e.getMessage()); + } + } + + // 初始化数据库 + public static void initDatabase() { + try (Connection conn = DriverManager.getConnection(DB_URL); + Statement stmt = conn.createStatement()) { + + // 创建courses表 + String createTableSQL = "CREATE TABLE IF NOT EXISTS courses (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "course_code TEXT," + + "course_name TEXT," + + "credit REAL," + + "teacher TEXT," + + "department TEXT," + + "capacity INTEGER," + + "enrolled INTEGER," + + "class_time TEXT," + + "class_room TEXT," + + "course_type TEXT," + + "semester TEXT," + + "create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + + ")"; + stmt.executeUpdate(createTableSQL); + + System.out.println("数据库初始化成功"); + } catch (SQLException e) { + System.err.println("数据库初始化失败: " + e.getMessage()); + } + } + + // 获取所有课程 + public static List getAllCourses() { + List 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 getCourseTypeDistribution() { + Map 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 getDepartmentDistribution() { + Map 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> getTopCourses() { + List> 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 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()); + } + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExceptionDemo.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExceptionDemo.java new file mode 100644 index 0000000..d2718d0 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExceptionDemo.java @@ -0,0 +1,105 @@ +package com.example; + +import com.example.crawler.CrawlerService; +import com.example.exception.BizException; +import com.example.exception.ParseException; + +/** + * 异常体系演示类 - 独立展示各种异常场景 + */ +public class ExceptionDemo { + + public static void main(String[] args) { + System.out.println("══════════════════════════════════════════"); + System.out.println(" 异常体系测试演示"); + System.out.println("══════════════════════════════════════════\n"); + + CrawlerService crawler = new CrawlerService(); + + // 测试1: 正常解析 + testNormalParse(crawler); + + // 测试2: 不支持的网站 - ParseException + testUnsupportedSite(crawler); + + // 测试3: 空URL - BizException + testEmptyUrl(crawler); + + // 测试4: 空HTML - BizException + testEmptyHtml(crawler); + + // 测试5: null参数 - BizException + testNullUrl(crawler); + + System.out.println("\n══════════════════════════════════════════"); + System.out.println(" 测试完成!"); + System.out.println("══════════════════════════════════════════"); + } + + private static void testNormalParse(CrawlerService crawler) { + System.out.println("【测试1】正常解析(湖南大学)"); + try { + crawler.simulateCrawl("hnu"); + System.out.println(" ✓ 解析成功"); + } catch (Exception e) { + System.out.println(" ✗ 失败: " + e.getMessage()); + } + System.out.println(); + } + + private static void testUnsupportedSite(CrawlerService crawler) { + System.out.println("【测试2】不支持的网站 → ParseException"); + try { + crawler.parse("https://www.unknown.edu.cn", ""); + } 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("", ""); + } 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, ""); + } 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(); + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExceptionTest.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExceptionTest.java new file mode 100644 index 0000000..7131a4d --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExceptionTest.java @@ -0,0 +1,68 @@ +package com.example; + +import com.example.crawler.CrawlerService; +import com.example.exception.BizException; +import com.example.exception.ParseException; + +/** + * 异常体系测试类 - 演示各种异常场景 + */ +public class ExceptionTest { + + public static void main(String[] args) { + CrawlerService crawler = new CrawlerService(); + + System.out.println("===== 测试异常体系 ====="); + + // 测试1: 正常解析(不抛出异常) + System.out.println("\n--- 测试1: 正常解析(湖南大学)---"); + try { + crawler.simulateCrawl("hnu"); + System.out.println("✓ 解析成功"); + } catch (Exception e) { + System.out.println("✗ 失败: " + e.getMessage()); + } + + // 测试2: 测试不支持的网站(抛出ParseException) + System.out.println("\n--- 测试2: 测试不支持的网站 ---"); + try { + crawler.parse("https://www.unknown.edu.cn", ""); + } 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("", ""); + } 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, ""); + } catch (BizException e) { + System.out.println("✓ BizException: " + e.getErrorCode() + " - " + e.getMessage()); + } catch (Exception e) { + System.out.println("✗ 其他异常: " + e.getMessage()); + } + + System.out.println("\n===== 异常测试完成 ====="); + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExportTest.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExportTest.java new file mode 100644 index 0000000..208796e --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/ExportTest.java @@ -0,0 +1,86 @@ +package com.example; + +import com.example.entity.Course; +import com.example.export.ExportService; + +import java.util.ArrayList; +import java.util.List; + +public class ExportTest { + public static void main(String[] args) { + System.out.println("══════════════════════════════════════════"); + System.out.println(" 数据导出功能测试"); + System.out.println("══════════════════════════════════════════\n"); + + List 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 createTestData() { + List 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; + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/HnuCourseSystem.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/HnuCourseSystem.java new file mode 100644 index 0000000..f2b28b6 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/HnuCourseSystem.java @@ -0,0 +1,54 @@ +package com.example; + +import com.example.controller.CourseController; +import com.example.view.CourseView; + +/** + * 湖大选课系统主应用类 + * MVC架构入口,协调Controller和View + */ +public class HnuCourseSystem { + + public static void main(String[] args) { + // 创建Controller和View + CourseController controller = new CourseController(); + CourseView view = new CourseView(); + + // 初始化数据库 + controller.initDatabase(); + + try { + int choice; + + do { + // 显示菜单 + view.displayMenu(); + + // 获取用户选择 + choice = view.getUserChoice(); + + // 根据选择执行相应操作 + switch (choice) { + case 1 -> view.displayOverallStatistics(controller.getOverallStatistics()); + case 2 -> view.displayCourseTypeDistribution(controller.getCourseTypeDistribution()); + case 3 -> view.displayDepartmentDistribution(controller.getDepartmentDistribution()); + case 4 -> view.displayCreditDistribution(controller.getCreditDistribution()); + case 5 -> view.displayTopCourses(controller.getTopCourses()); + case 6 -> view.displayCourseUsageRate(controller.getCourseUsageRate()); + case 7 -> view.displayDepartmentStatistics(controller.getDepartmentStatistics()); + case 0 -> view.displayExitMessage(); + default -> view.displayInvalidChoice(); + } + + // 非退出操作时等待用户确认 + if (choice != 0) { + view.displayContinuePrompt(); + } + + } while (choice != 0); + + } finally { + view.close(); + } + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/Pair.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/Pair.java new file mode 100644 index 0000000..6fc8436 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/Pair.java @@ -0,0 +1,89 @@ +package com.example; + +/** + * 泛型Pair类,用于存储两个不同类型的值 + * @param 键的类型 + * @param 值的类型 + */ +public class Pair { + 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 键的类型 + * @param 值的类型 + * @return Pair实例 + */ + public static Pair of(K key, V value) { + return new Pair<>(key, value); + } +} diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/controller/CourseController.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/controller/CourseController.java new file mode 100644 index 0000000..ea309b6 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/controller/CourseController.java @@ -0,0 +1,68 @@ +package com.example.controller; + +import com.example.entity.Course; +import com.example.service.CourseService; + +import java.util.List; +import java.util.Map; + +/** + * 课程控制器(Controller) + * 负责接收用户请求,调用Service层,返回结果 + */ +public class CourseController { + + private final CourseService service; + + public CourseController() { + this.service = new CourseService(); + } + + public void initDatabase() { + service.initDatabase(); + } + + public void clearAllCourses() { + service.clearAllCourses(); + } + + public void saveCourse(Course course) { + service.saveCourse(course); + } + + public Map getOverallStatistics() { + return service.getOverallStatistics(); + } + + public Map getCourseTypeDistribution() { + return service.getCourseTypeDistribution(); + } + + public Map getDepartmentDistribution() { + return service.getDepartmentDistribution(); + } + + public Map getCreditDistribution() { + return service.getCreditDistribution(); + } + + public List> getTopCourses() { + return service.getTopCourses(); + } + + public List> getCourseUsageRate() { + return service.getCourseUsageRate(); + } + + public Map> getDepartmentStatistics() { + return service.getDepartmentStatistics(); + } + + public Map> getTeacherStatistics() { + return service.getTeacherStatistics(); + } + + public Map getCapacityAnalysis() { + return service.getCapacityAnalysis(); + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/CrawlerContext.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/CrawlerContext.java new file mode 100644 index 0000000..6822b04 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/CrawlerContext.java @@ -0,0 +1,78 @@ +package com.example.crawler; + +import java.util.ArrayList; +import java.util.List; + +import com.example.crawler.strategy.ParseStrategy; +import com.example.entity.Course; + +/** + * 爬虫上下文类 + * 负责策略的选择和执行 + */ +public class CrawlerContext { + + private ParseStrategy strategy; + private final List 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 getAllStrategies() { + return new ArrayList<>(strategies); + } + + public List getSupportedWebsites() { + List websites = new ArrayList<>(); + for (ParseStrategy s : strategies) { + websites.add(s.getName()); + } + return websites; + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/CrawlerService.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/CrawlerService.java new file mode 100644 index 0000000..b04a44f --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/CrawlerService.java @@ -0,0 +1,221 @@ +package com.example.crawler; + +import com.example.crawler.strategy.HnuParseStrategy; +import com.example.crawler.strategy.ParseStrategy; +import com.example.crawler.strategy.SduParseStrategy; +import com.example.entity.Course; +import com.example.exception.BizException; +import com.example.exception.NetworkException; +import com.example.exception.ParseException; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +/** + * 爬虫服务类 + * 提供网页爬取和课程解析功能 + */ +public class CrawlerService { + + private final CrawlerContext context; + + public CrawlerService() { + this.context = new CrawlerContext(); + initStrategies(); + } + + private void initStrategies() { + context.registerStrategies( + new HnuParseStrategy(), + new SduParseStrategy() + ); + } + + public void addStrategy(ParseStrategy strategy) { + context.registerStrategy(strategy); + } + + public java.util.List 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 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 """ + +
+ CS101 + 计算机科学导论 + 张教授 + 计算机学院 + 3.0 + 100 + 95 + 周一 8:00-10:00 + A101 + 必修课 +
+
+ CS102 + 数据结构 + 李教授 + 计算机学院 + 4.0 + 80 + 75 + 周二 10:00-12:00 + A102 + 必修课 +
+ + """; + } else { + return """ + +
+ MATH101 + 高等数学 + 王教授 + 数学学院 + 5.0 + 120 + 110 + 周三 8:00-10:00 + B101 + 必修课 +
+ + """; + } + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/HnuParseStrategy.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/HnuParseStrategy.java new file mode 100644 index 0000000..5cf1945 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/HnuParseStrategy.java @@ -0,0 +1,99 @@ +package com.example.crawler.strategy; + +import com.example.entity.Course; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +/** + * 湖南大学课程解析策略 + * 针对湖大选课系统的HTML结构进行解析 + */ +public class HnuParseStrategy implements ParseStrategy { + + private static final String DOMAIN = "hnu.edu.cn"; + private static final String NAME = "湖南大学解析策略"; + + @Override + public Course[] parse(String html) { + Document doc = Jsoup.parse(html); + + Elements courseElements = doc.select("div.course-item, tr.course-row, .course-list .item"); + + return courseElements.stream() + .map(this::parseCourseElement) + .filter(course -> course.getCourseName() != null && !course.getCourseName().isEmpty()) + .toArray(Course[]::new); + } + + private Course parseCourseElement(Element element) { + Course course = new Course(); + + // 尝试多种选择器解析湖大课程信息 + course.setCourseCode(getText(element, ".course-code", "code", "td:nth-child(1)")); + course.setCourseName(getText(element, ".course-name", ".title", "td:nth-child(2)", "h3")); + course.setTeacher(getText(element, ".teacher", ".instructor", "td:nth-child(3)")); + course.setDepartment(getText(element, ".department", ".college", "td:nth-child(4)")); + course.setCourseType(getText(element, ".course-type", ".type", "td:nth-child(5)")); + + // 解析数值字段 + try { + course.setCredit(parseDouble(getText(element, ".credit", ".credits", "td:nth-child(6)"))); + } catch (Exception e) { + course.setCredit(0.0); + } + + try { + course.setCapacity(parseInt(getText(element, ".capacity", ".max-students", "td:nth-child(7)"))); + } catch (Exception e) { + course.setCapacity(0); + } + + try { + course.setEnrolled(parseInt(getText(element, ".enrolled", ".current-students", "td:nth-child(8)"))); + } catch (Exception e) { + course.setEnrolled(0); + } + + course.setClassTime(getText(element, ".class-time", ".time", ".schedule")); + course.setClassRoom(getText(element, ".class-room", ".room", ".location")); + course.setSemester("2024春季"); + + return course; + } + + private String getText(Element element, String... selectors) { + for (String selector : selectors) { + Element found = element.selectFirst(selector); + if (found != null && !found.text().trim().isEmpty()) { + return found.text().trim(); + } + } + return ""; + } + + private double parseDouble(String value) { + if (value == null || value.isEmpty()) { + return 0.0; + } + return Double.parseDouble(value.replaceAll("[^0-9.]", "")); + } + + private int parseInt(String value) { + if (value == null || value.isEmpty()) { + return 0; + } + return Integer.parseInt(value.replaceAll("[^0-9]", "")); + } + + @Override + public boolean supports(String url) { + return url != null && (url.contains(DOMAIN) || url.contains("hunan.edu")); + } + + @Override + public String getName() { + return NAME; + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/ParseStrategy.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/ParseStrategy.java new file mode 100644 index 0000000..f7635b5 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/ParseStrategy.java @@ -0,0 +1,30 @@ +package com.example.crawler.strategy; + +import com.example.entity.Course; + +/** + * 解析策略接口 + * 定义不同网站的课程解析规范 + */ +public interface ParseStrategy { + + /** + * 解析HTML内容,提取课程信息 + * @param html 网页HTML内容 + * @return 课程数组 + */ + Course[] parse(String html); + + /** + * 判断该策略是否支持指定URL + * @param url 目标网站URL + * @return 是否支持 + */ + boolean supports(String url); + + /** + * 获取策略名称 + * @return 策略名称 + */ + String getName(); +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/SduParseStrategy.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/SduParseStrategy.java new file mode 100644 index 0000000..97943df --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/crawler/strategy/SduParseStrategy.java @@ -0,0 +1,98 @@ +package com.example.crawler.strategy; + +import com.example.entity.Course; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +/** + * 山东大学课程解析策略 + * 针对山大选课系统的HTML结构进行解析 + */ +public class SduParseStrategy implements ParseStrategy { + + private static final String DOMAIN = "sdu.edu.cn"; + private static final String NAME = "山东大学解析策略"; + + @Override + public Course[] parse(String html) { + Document doc = Jsoup.parse(html); + + Elements courseElements = doc.select("div.sdu-course-item, table.course-table tr, .course-card"); + + return courseElements.stream() + .map(this::parseCourseElement) + .filter(course -> course.getCourseName() != null && !course.getCourseName().isEmpty()) + .toArray(Course[]::new); + } + + private Course parseCourseElement(Element element) { + Course course = new Course(); + + // 山东大学特有的解析逻辑 + course.setCourseCode(getText(element, ".sdu-code", ".course-id", ".code")); + course.setCourseName(getText(element, ".sdu-name", ".course-title", ".title")); + course.setTeacher(getText(element, ".sdu-teacher", ".professor", ".teacher-name")); + course.setDepartment(getText(element, ".sdu-dept", ".school", ".department")); + course.setCourseType(getText(element, ".sdu-type", ".course-category", ".type")); + + try { + course.setCredit(parseDouble(getText(element, ".sdu-credit", ".credit-value"))); + } catch (Exception e) { + course.setCredit(0.0); + } + + try { + course.setCapacity(parseInt(getText(element, ".sdu-capacity", ".max-num"))); + } catch (Exception e) { + course.setCapacity(0); + } + + try { + course.setEnrolled(parseInt(getText(element, ".sdu-enrolled", ".registered"))); + } catch (Exception e) { + course.setEnrolled(0); + } + + course.setClassTime(getText(element, ".sdu-time", ".schedule")); + course.setClassRoom(getText(element, ".sdu-room", ".location")); + course.setSemester("2024春季"); + + return course; + } + + private String getText(Element element, String... selectors) { + for (String selector : selectors) { + Element found = element.selectFirst(selector); + if (found != null && !found.text().trim().isEmpty()) { + return found.text().trim(); + } + } + return ""; + } + + private double parseDouble(String value) { + if (value == null || value.isEmpty()) { + return 0.0; + } + return Double.parseDouble(value.replaceAll("[^0-9.]", "")); + } + + private int parseInt(String value) { + if (value == null || value.isEmpty()) { + return 0; + } + return Integer.parseInt(value.replaceAll("[^0-9]", "")); + } + + @Override + public boolean supports(String url) { + return url != null && (url.contains(DOMAIN) || url.contains("shandong.edu")); + } + + @Override + public String getName() { + return NAME; + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/entity/Course.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/entity/Course.java new file mode 100644 index 0000000..10ef333 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/entity/Course.java @@ -0,0 +1,127 @@ +package com.example.entity; + +import java.time.LocalDateTime; + +/** + * 课程实体类 + * 不依赖Spring Boot,使用普通Java类实现 + */ +public class Course { + private Long id; + private String courseCode; + private String courseName; + private Double credit; + private String teacher; + private String department; + private Integer capacity; + private Integer enrolled; + private String classTime; + private String classRoom; + private String courseType; + private String semester; + private LocalDateTime createTime; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getCourseCode() { + return courseCode; + } + + public void setCourseCode(String courseCode) { + this.courseCode = courseCode; + } + + public String getCourseName() { + return courseName; + } + + public void setCourseName(String courseName) { + this.courseName = courseName; + } + + public Double getCredit() { + return credit; + } + + public void setCredit(Double credit) { + this.credit = credit; + } + + public String getTeacher() { + return teacher; + } + + public void setTeacher(String teacher) { + this.teacher = teacher; + } + + public String getDepartment() { + return department; + } + + public void setDepartment(String department) { + this.department = department; + } + + public Integer getCapacity() { + return capacity; + } + + public void setCapacity(Integer capacity) { + this.capacity = capacity; + } + + public Integer getEnrolled() { + return enrolled; + } + + public void setEnrolled(Integer enrolled) { + this.enrolled = enrolled; + } + + public String getClassTime() { + return classTime; + } + + public void setClassTime(String classTime) { + this.classTime = classTime; + } + + public String getClassRoom() { + return classRoom; + } + + public void setClassRoom(String classRoom) { + this.classRoom = classRoom; + } + + public String getCourseType() { + return courseType; + } + + public void setCourseType(String courseType) { + this.courseType = courseType; + } + + public String getSemester() { + return semester; + } + + public void setSemester(String semester) { + this.semester = semester; + } + + public LocalDateTime getCreateTime() { + return createTime; + } + + public void setCreateTime(LocalDateTime createTime) { + this.createTime = createTime; + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/BizException.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/BizException.java new file mode 100644 index 0000000..26c96a3 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/BizException.java @@ -0,0 +1,31 @@ +package com.example.exception; + +/** + * 业务异常 + * 用于处理业务逻辑相关的错误,如数据校验失败、权限不足、资源不存在等 + */ +public class BizException extends CrawlerException { + + // 错误码常量 + public static final String DATA_VALIDATION_ERROR = "BIZ_VALIDATION_ERROR"; + public static final String RESOURCE_NOT_FOUND = "BIZ_RESOURCE_NOT_FOUND"; + public static final String DUPLICATE_DATA = "BIZ_DUPLICATE_DATA"; + public static final String PERMISSION_DENIED = "BIZ_PERMISSION_DENIED"; + public static final String SERVICE_UNAVAILABLE = "BIZ_SERVICE_UNAVAILABLE"; + + public BizException(String message) { + super(DATA_VALIDATION_ERROR, message); + } + + public BizException(String errorCode, String message) { + super(errorCode, message); + } + + public BizException(String message, Throwable cause) { + super(DATA_VALIDATION_ERROR, message, cause); + } + + public BizException(String errorCode, String message, Throwable cause) { + super(errorCode, message, cause); + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/CrawlerException.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/CrawlerException.java new file mode 100644 index 0000000..a2c3190 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/CrawlerException.java @@ -0,0 +1,34 @@ +package com.example.exception; + +/** + * 爬虫基异常类 + * 所有爬虫相关异常的父类 + */ +public class CrawlerException extends Exception { + + private final String errorCode; + + public CrawlerException(String message) { + super(message); + this.errorCode = "CRAWLER_ERROR"; + } + + public CrawlerException(String message, Throwable cause) { + super(message, cause); + this.errorCode = "CRAWLER_ERROR"; + } + + public CrawlerException(String errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + public CrawlerException(String errorCode, String message, Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + } + + public String getErrorCode() { + return errorCode; + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/NetworkException.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/NetworkException.java new file mode 100644 index 0000000..66196f2 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/NetworkException.java @@ -0,0 +1,30 @@ +package com.example.exception; + +/** + * 网络异常 + * 用于处理网络相关的错误,如连接超时、无法连接、HTTP错误等 + */ +public class NetworkException extends CrawlerException { + + public static final String CONNECTION_TIMEOUT = "NETWORK_TIMEOUT"; + public static final String CONNECTION_REFUSED = "NETWORK_REFUSED"; + public static final String HTTP_ERROR = "NETWORK_HTTP_ERROR"; + public static final String SSL_ERROR = "NETWORK_SSL_ERROR"; + public static final String UNKNOWN_HOST = "NETWORK_UNKNOWN_HOST"; + + public NetworkException(String message) { + super(CONNECTION_TIMEOUT, message); + } + + public NetworkException(String errorCode, String message) { + super(errorCode, message); + } + + public NetworkException(String message, Throwable cause) { + super(CONNECTION_TIMEOUT, message, cause); + } + + public NetworkException(String errorCode, String message, Throwable cause) { + super(errorCode, message, cause); + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/ParseException.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/ParseException.java new file mode 100644 index 0000000..0547763 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/exception/ParseException.java @@ -0,0 +1,31 @@ +package com.example.exception; + +/** + * 解析异常 + * 用于处理HTML/JSON解析相关的错误,如格式错误、字段缺失等 + */ +public class ParseException extends CrawlerException { + + // 错误码常量 + public static final String HTML_PARSE_ERROR = "PARSE_HTML_ERROR"; + public static final String JSON_PARSE_ERROR = "PARSE_JSON_ERROR"; + public static final String FIELD_MISSING = "PARSE_FIELD_MISSING"; + public static final String FORMAT_ERROR = "PARSE_FORMAT_ERROR"; + public static final String NO_STRATEGY_FOUND = "PARSE_NO_STRATEGY"; + + public ParseException(String message) { + super(HTML_PARSE_ERROR, message); + } + + public ParseException(String errorCode, String message) { + super(errorCode, message); + } + + public ParseException(String message, Throwable cause) { + super(HTML_PARSE_ERROR, message, cause); + } + + public ParseException(String errorCode, String message, Throwable cause) { + super(errorCode, message, cause); + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/CsvExporter.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/CsvExporter.java new file mode 100644 index 0000000..c3a8e33 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/CsvExporter.java @@ -0,0 +1,60 @@ +package com.example.export; + +import java.io.FileWriter; +import java.io.PrintWriter; +import java.lang.reflect.Field; +import java.util.List; + +public class CsvExporter implements Exporter { + private static final String CSV_SEPARATOR = ","; + + @Override + public void export(List 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"; + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/ExportService.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/ExportService.java new file mode 100644 index 0000000..0c1848b --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/ExportService.java @@ -0,0 +1,28 @@ +package com.example.export; + +import java.util.List; + +public class ExportService { + public enum ExportFormat { + JSON, CSV + } + + public void exportToJson(List data, String filePath) { + new JsonExporter().export(data, filePath); + } + + public void exportToCsv(List data, String filePath) { + new CsvExporter().export(data, filePath); + } + + public Exporter getExporter(ExportFormat format) { + switch (format) { + case JSON: + return new JsonExporter<>(); + case CSV: + return new CsvExporter<>(); + default: + throw new IllegalArgumentException("不支持的导出格式: " + format); + } + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/Exporter.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/Exporter.java new file mode 100644 index 0000000..7d1eb26 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/Exporter.java @@ -0,0 +1,8 @@ +package com.example.export; + +import java.util.List; + +public interface Exporter { + void export(List data, String filePath); + String getFormat(); +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/JsonExporter.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/JsonExporter.java new file mode 100644 index 0000000..99014e7 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/export/JsonExporter.java @@ -0,0 +1,33 @@ +package com.example.export; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import java.io.File; +import java.util.List; + +public class JsonExporter implements Exporter { + 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 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"; + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/repository/CourseRepository.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/repository/CourseRepository.java new file mode 100644 index 0000000..6af50d1 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/repository/CourseRepository.java @@ -0,0 +1,182 @@ +package com.example.repository; + +import com.example.entity.Course; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +/** + * 课程数据访问层(Repository) + * 负责与数据库交互,提供数据访问接口 + */ +public class CourseRepository { + private static final String DB_URL = "jdbc:sqlite:course.db"; + + static { + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException e) { + System.err.println("SQLite驱动加载失败: " + e.getMessage()); + } + } + + public void initDatabase() { + try (Connection conn = DriverManager.getConnection(DB_URL); + Statement stmt = conn.createStatement()) { + + String createTableSQL = "CREATE TABLE IF NOT EXISTS courses (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT," + + "course_code TEXT," + + "course_name TEXT," + + "credit REAL," + + "teacher TEXT," + + "department TEXT," + + "capacity INTEGER," + + "enrolled INTEGER," + + "class_time TEXT," + + "class_room TEXT," + + "course_type TEXT," + + "semester TEXT," + + "create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + + ")"; + stmt.executeUpdate(createTableSQL); + + } catch (SQLException e) { + System.err.println("数据库初始化失败: " + e.getMessage()); + } + } + + public List findAll() { + List 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 findCourseTypeDistribution() { + Map 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 findDepartmentDistribution() { + Map 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> findTopCourses() { + List> 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 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; + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/service/CourseService.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/service/CourseService.java new file mode 100644 index 0000000..75e9d38 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/service/CourseService.java @@ -0,0 +1,229 @@ +package com.example.service; + +import com.example.entity.Course; +import com.example.repository.CourseRepository; + +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.stream.Collectors; + +/** + * 课程业务逻辑层(Service) + * 负责封装业务逻辑,协调数据访问和业务处理 + */ +public class CourseService { + + private final CourseRepository repository; + + public CourseService() { + this.repository = new CourseRepository(); + } + + public void initDatabase() { + repository.initDatabase(); + } + + public void clearAllCourses() { + repository.clearAll(); + } + + public void saveCourse(Course course) { + repository.save(course); + } + + public List getAllCourses() { + return repository.findAll(); + } + + public Map getOverallStatistics() { + Map statistics = new HashMap<>(); + try { + List 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 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 getCourseTypeDistribution() { + return repository.findCourseTypeDistribution(); + } + + public Map getDepartmentDistribution() { + return repository.findDepartmentDistribution(); + } + + public Map getCreditDistribution() { + Map distribution = new HashMap<>(); + List courses = repository.findAll(); + + for (Course course : courses) { + double credit = course.getCredit(); + distribution.put(credit, distribution.getOrDefault(credit, 0) + 1); + } + + return distribution; + } + + public List> getTopCourses() { + return repository.findTopCourses(); + } + + public List> getCourseUsageRate() { + List> usageRates = new ArrayList<>(); + List courses = repository.findAll(); + + for (Course course : courses) { + Map 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> getDepartmentStatistics() { + Map> deptStats = new HashMap<>(); + try { + List allCourses = repository.findAll(); + + Map> coursesByDept = allCourses.stream() + .collect(Collectors.groupingBy(Course::getDepartment)); + + for (Map.Entry> entry : coursesByDept.entrySet()) { + String department = entry.getKey(); + List deptCourses = entry.getValue(); + + Map 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> getTeacherStatistics() { + Map> teacherStats = new HashMap<>(); + try { + List allCourses = repository.findAll(); + + Map> coursesByTeacher = allCourses.stream() + .collect(Collectors.groupingBy(Course::getTeacher)); + + for (Map.Entry> entry : coursesByTeacher.entrySet()) { + String teacher = entry.getKey(); + List teacherCourses = entry.getValue(); + + Map 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 getCapacityAnalysis() { + Map analysis = new HashMap<>(); + try { + List 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; + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/view/CourseView.java b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/view/CourseView.java new file mode 100644 index 0000000..f284079 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/java/com/example/view/CourseView.java @@ -0,0 +1,129 @@ +package com.example.view; + +import java.util.List; +import java.util.Map; +import java.util.Scanner; + +/** + * 课程视图层(View) + * 负责用户界面的展示和用户输入的获取 + */ +public class CourseView { + + private final Scanner scanner; + + public CourseView() { + this.scanner = new Scanner(System.in); + } + + public void close() { + scanner.close(); + } + + public void displayMenu() { + System.out.println("\n===== 湖大选课系统 ====="); + System.out.println("1. 查看整体统计信息"); + System.out.println("2. 查看课程类型分布"); + System.out.println("3. 查看院系统计"); + System.out.println("4. 查看学分分布"); + System.out.println("5. 查看热门课程"); + System.out.println("6. 查看课程容量使用率"); + System.out.println("7. 查看按院系分组的统计"); + System.out.println("0. 退出系统"); + System.out.print("请选择操作: "); + } + + public int getUserChoice() { + while (!scanner.hasNextInt()) { + System.out.println("请输入有效的数字!"); + scanner.next(); + System.out.print("请选择操作: "); + } + int choice = scanner.nextInt(); + scanner.nextLine(); + return choice; + } + + public void displayOverallStatistics(Map 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 distribution) { + System.out.println("\n===== 课程类型分布 ====="); + for (Map.Entry entry : distribution.entrySet()) { + System.out.println(entry.getKey() + ": " + entry.getValue() + "门"); + } + } + + public void displayDepartmentDistribution(Map distribution) { + System.out.println("\n===== 院系统计 ====="); + for (Map.Entry entry : distribution.entrySet()) { + System.out.println(entry.getKey() + ": " + entry.getValue() + "门课程"); + } + } + + public void displayCreditDistribution(Map distribution) { + System.out.println("\n===== 学分分布 ====="); + for (Map.Entry entry : distribution.entrySet()) { + System.out.println(entry.getKey() + "学分: " + entry.getValue() + "门课程"); + } + } + + public void displayTopCourses(List> topCourses) { + System.out.println("\n===== 热门课程 ====="); + for (int i = 0; i < topCourses.size(); i++) { + Map 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> usageRates) { + System.out.println("\n===== 课程容量使用率 ====="); + int limit = Math.min(10, usageRates.size()); + for (int i = 0; i < limit; i++) { + Map 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> deptStats) { + System.out.println("\n===== 按院系分组的统计 ====="); + for (Map.Entry> entry : deptStats.entrySet()) { + String department = entry.getKey(); + Map 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(); + } +} \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/application.properties b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/application.properties new file mode 100644 index 0000000..5e8bf15 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/application.properties @@ -0,0 +1,33 @@ +spring.application.name=hnu-course-analysis + +server.port=8084 + +# SQLite数据库配置 +spring.datasource.url=jdbc:sqlite:course.db +spring.datasource.driver-class-name=org.sqlite.JDBC +spring.jpa.database-platform=org.hibernate.community.dialect.SQLiteDialect + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.open-in-view=false + +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:schema.sql + +spring.thymeleaf.cache=false +spring.thymeleaf.prefix=classpath:/templates/ +spring.thymeleaf.suffix=.html + +logging.level.root=INFO +logging.level.com.example=DEBUG + +# 湖南大学选课系统配置 +crawler.hnu.login.url=https://jwxt.hnu.edu.cn/xtgl/login_login.html +crawler.hnu.username=your_username +crawler.hnu.password=your_password +crawler.hnu.course.list.url=https://jwxt.hnu.edu.cn/kbcx/xskbcx_cxXsKb.html + +# Selenium配置 +selenium.webdriver.chrome.driver.path=C:/chromedriver/chromedriver.exe +selenium.webdriver.headless=false \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/schema.sql b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/schema.sql new file mode 100644 index 0000000..53a3489 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/schema.sql @@ -0,0 +1,31 @@ +-- 湖南大学选课系统数据库初始化脚本 + +-- 创建课程表 +CREATE TABLE IF NOT EXISTS courses ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + course_code TEXT, + course_name TEXT, + credit REAL, + teacher TEXT, + department TEXT, + capacity INTEGER, + enrolled INTEGER, + class_time TEXT, + class_room TEXT, + course_type TEXT, + semester TEXT, + create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 插入测试数据 +INSERT INTO courses (course_code, course_name, credit, teacher, department, capacity, enrolled, class_time, class_room, course_type, semester) VALUES +('CS101', '计算机科学导论', 3.0, '张教授', '计算机科学与技术学院', 100, 85, '周一 1-2节', 'A101', '必修课', '2024-2025-1'), +('CS102', '数据结构', 4.0, '李教授', '计算机科学与技术学院', 80, 70, '周二 3-4节', 'B202', '必修课', '2024-2025-1'), +('CS103', '算法设计', 3.0, '王教授', '计算机科学与技术学院', 60, 55, '周三 5-6节', 'C303', '选修课', '2024-2025-1'), +('MA101', '高等数学', 5.0, '赵教授', '数学学院', 120, 110, '周一 3-4节', 'D404', '必修课', '2024-2025-1'), +('EN101', '大学英语', 3.0, '刘教授', '外国语学院', 100, 90, '周二 1-2节', 'E505', '必修课', '2024-2025-1'), +('PH101', '大学物理', 4.0, '钱教授', '物理学院', 90, 85, '周三 1-2节', 'F606', '必修课', '2024-2025-1'), +('CH101', '大学化学', 3.0, '孙教授', '化学学院', 70, 65, '周四 3-4节', 'G707', '选修课', '2024-2025-1'), +('HI101', '中国近现代史', 2.0, '周教授', '历史学院', 150, 145, '周五 1-2节', 'H808', '必修课', '2024-2025-1'), +('PE101', '体育', 1.0, '吴教授', '体育学院', 50, 50, '周三 7-8节', '体育馆', '必修课', '2024-2025-1'), +('AR101', '艺术鉴赏', 2.0, '郑教授', '艺术学院', 80, 75, '周四 5-6节', '艺术楼', '选修课', '2024-2025-1'); \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/analysis.html b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/analysis.html new file mode 100644 index 0000000..6ca13bc --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/analysis.html @@ -0,0 +1,328 @@ + + + + + + 数据分析 - 湖南大学选课系统分析 + + + + + + + + + +
+
+

数据分析

+

深入分析选课系统数据,获取有价值的洞察

+
+
+ + +
+ +
+
+

课程类型分布

+
+
+
+
+
+ + +
+
+

院系课程分布

+
+
+
+
+
+ + +
+
+

学分分布

+
+
+
+
+
+ + +
+
+

课程容量使用率

+
+
+
+
+
+ + +
+
+

院系统计详情

+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
院系课程数总学分总容量总已选使用率
+
+
+
+
+ + +
+
+

© 2024 湖南大学选课系统分析平台

+
+
+ + + + + \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/course-list.html b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/course-list.html new file mode 100644 index 0000000..3615157 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/course-list.html @@ -0,0 +1,169 @@ + + + + + + 课程列表 - 湖南大学选课系统分析 + + + + + + + + +
+
+

课程列表

+

查看所有课程的详细信息

+
+
+ + +
+
+
+

课程详情

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
课程代码课程名称学分教师院系容量已选使用率上课时间上课地点课程类型
+ + + +
+
+
+
+ + +
+
+

© 2024 湖南大学选课系统分析平台

+
+
+ + + + + \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/error.html b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/error.html new file mode 100644 index 0000000..cf86f23 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/error.html @@ -0,0 +1,24 @@ + + + + + + 错误页面 + + + +
+
+
+
+
+

500 - 服务器内部错误

+

服务器遇到了一个内部错误,请稍后再试。

+ 返回首页 +
+
+
+
+
+ + \ No newline at end of file diff --git a/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/index.html b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/index.html new file mode 100644 index 0000000..95da865 --- /dev/null +++ b/.trae-cn/worktrees/project/course-analysis copy/src/main/resources/templates/index.html @@ -0,0 +1,318 @@ + + + + + + 湖南大学选课系统分析 + + + + + + + + + +
+
+

湖南大学选课系统数据分析平台

+

实时爬取、分析和可视化展示选课系统数据

+ +
+
+ + +
+
+
+
+
+
+
总课程数
+
+
+
+
+
+
+
+
总学分
+
+
+
+
+
+
+
+
总已选人数
+
+
+
+
+
+
+
+ 使用率 +
+
总体使用率
+
+
+
+
+
+ + +
+

热门课程排行

+
+
+
+
+
+
+ + +
+

课程类型分布

+
+
+
+
+
+
+ + +
+

院系课程分布

+
+
+
+
+
+
+ + +
+
+

© 2024 湖南大学选课系统分析平台

+
+
+ + + + + \ No newline at end of file