commit
cff60e50fc
52 changed files with 4829 additions and 0 deletions
@ -0,0 +1,36 @@ |
|||||
|
# 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 |
||||
|
*.tmp |
||||
|
*.temp |
||||
|
|
||||
|
# Maven |
||||
|
pom.xml.tag |
||||
|
pom.xml.releaseBackup |
||||
|
pom.xml.versionsBackup |
||||
|
pom.xml.next |
||||
|
release.properties |
||||
|
dependency-reduced-pom.xml |
||||
|
build.log |
||||
@ -0,0 +1,2 @@ |
|||||
|
# 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> |
||||
@ -0,0 +1,50 @@ |
|||||
|
package com.example.shape; |
||||
|
|
||||
|
/** |
||||
|
* 圆形类 |
||||
|
* 继承自Shape抽象类,实现getArea()方法 |
||||
|
*/ |
||||
|
public class Circle extends Shape { |
||||
|
private double radius; |
||||
|
|
||||
|
/** |
||||
|
* 构造方法 |
||||
|
* @param radius 圆的半径 |
||||
|
*/ |
||||
|
public Circle(double radius) { |
||||
|
this.radius = radius; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算圆形面积 |
||||
|
* @return 圆形的面积 |
||||
|
*/ |
||||
|
@Override |
||||
|
public double getArea() { |
||||
|
return Math.PI * radius * radius; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取圆的半径 |
||||
|
* @return 圆的半径 |
||||
|
*/ |
||||
|
public double getRadius() { |
||||
|
return radius; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置圆的半径 |
||||
|
* @param radius 圆的半径 |
||||
|
*/ |
||||
|
public void setRadius(double radius) { |
||||
|
this.radius = radius; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 绘制圆形 |
||||
|
*/ |
||||
|
@Override |
||||
|
public void draw() { |
||||
|
System.out.println("绘制一个半径为 " + radius + " 的圆形"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
package com.example.shape; |
||||
|
|
||||
|
/** |
||||
|
* 矩形类 |
||||
|
* 继承自Shape抽象类,实现getArea()方法 |
||||
|
*/ |
||||
|
public class Rectangle extends Shape { |
||||
|
private double width; |
||||
|
private double height; |
||||
|
|
||||
|
/** |
||||
|
* 构造方法 |
||||
|
* @param width 矩形的宽度 |
||||
|
* @param height 矩形的高度 |
||||
|
*/ |
||||
|
public Rectangle(double width, double height) { |
||||
|
this.width = width; |
||||
|
this.height = height; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算矩形面积 |
||||
|
* @return 矩形的面积 |
||||
|
*/ |
||||
|
@Override |
||||
|
public double getArea() { |
||||
|
return width * height; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取矩形的宽度 |
||||
|
* @return 矩形的宽度 |
||||
|
*/ |
||||
|
public double getWidth() { |
||||
|
return width; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置矩形的宽度 |
||||
|
* @param width 矩形的宽度 |
||||
|
*/ |
||||
|
public void setWidth(double width) { |
||||
|
this.width = width; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取矩形的高度 |
||||
|
* @return 矩形的高度 |
||||
|
*/ |
||||
|
public double getHeight() { |
||||
|
return height; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置矩形的高度 |
||||
|
* @param height 矩形的高度 |
||||
|
*/ |
||||
|
public void setHeight(double height) { |
||||
|
this.height = height; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 绘制矩形 |
||||
|
*/ |
||||
|
@Override |
||||
|
public void draw() { |
||||
|
System.out.println("绘制一个宽为 " + width + ",高为 " + height + " 的矩形"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
package com.example.shape; |
||||
|
|
||||
|
/** |
||||
|
* 图形抽象类 |
||||
|
* 包含抽象方法getArea(),用于计算图形面积 |
||||
|
* 包含抽象方法draw(),用于绘制图形 |
||||
|
*/ |
||||
|
public abstract class Shape { |
||||
|
/** |
||||
|
* 计算图形面积 |
||||
|
* @return 图形的面积 |
||||
|
*/ |
||||
|
public abstract double getArea(); |
||||
|
|
||||
|
/** |
||||
|
* 绘制图形 |
||||
|
*/ |
||||
|
public abstract void draw(); |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
package com.example.shape; |
||||
|
|
||||
|
/** |
||||
|
* 图形面积计算器测试类 |
||||
|
*/ |
||||
|
public class ShapeCalculatorTest { |
||||
|
public static void main(String[] args) { |
||||
|
// 创建圆形对象,半径为5
|
||||
|
Circle circle = new Circle(5); |
||||
|
System.out.println("圆形的半径: " + circle.getRadius()); |
||||
|
ShapeUtil.printArea(circle); |
||||
|
ShapeUtil.ranShape(circle); |
||||
|
|
||||
|
// 创建矩形对象,宽为4,高为6
|
||||
|
Rectangle rectangle = new Rectangle(4, 6); |
||||
|
System.out.println("矩形的宽: " + rectangle.getWidth() + ", 高: " + rectangle.getHeight()); |
||||
|
ShapeUtil.printArea(rectangle); |
||||
|
ShapeUtil.ranShape(rectangle); |
||||
|
|
||||
|
// 创建三角形对象,底为8,高为3
|
||||
|
Triangle triangle = new Triangle(8, 3); |
||||
|
System.out.println("三角形的底: " + triangle.getBase() + ", 高: " + triangle.getHeight()); |
||||
|
ShapeUtil.printArea(triangle); |
||||
|
ShapeUtil.ranShape(triangle); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
package com.example.shape; |
||||
|
|
||||
|
/** |
||||
|
* 图形工具类 |
||||
|
* 提供打印图形面积的方法和绘制图形的方法 |
||||
|
*/ |
||||
|
public class ShapeUtil { |
||||
|
/** |
||||
|
* 打印图形的面积 |
||||
|
* @param shape 图形对象 |
||||
|
*/ |
||||
|
public static void printArea(Shape shape) { |
||||
|
double area = shape.getArea(); |
||||
|
System.out.println("图形的面积为: " + area); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 绘制图形 |
||||
|
* @param s 图形对象 |
||||
|
*/ |
||||
|
public static void ranShape(Shape s) { |
||||
|
s.draw(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,69 @@ |
|||||
|
package com.example.shape; |
||||
|
|
||||
|
/** |
||||
|
* 三角形类 |
||||
|
* 继承自Shape抽象类,实现getArea()方法 |
||||
|
*/ |
||||
|
public class Triangle extends Shape { |
||||
|
private double base; |
||||
|
private double height; |
||||
|
|
||||
|
/** |
||||
|
* 构造方法 |
||||
|
* @param base 三角形的底 |
||||
|
* @param height 三角形的高 |
||||
|
*/ |
||||
|
public Triangle(double base, double height) { |
||||
|
this.base = base; |
||||
|
this.height = height; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 计算三角形面积 |
||||
|
* @return 三角形的面积 |
||||
|
*/ |
||||
|
@Override |
||||
|
public double getArea() { |
||||
|
return base * height / 2; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取三角形的底 |
||||
|
* @return 三角形的底 |
||||
|
*/ |
||||
|
public double getBase() { |
||||
|
return base; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置三角形的底 |
||||
|
* @param base 三角形的底 |
||||
|
*/ |
||||
|
public void setBase(double base) { |
||||
|
this.base = base; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取三角形的高 |
||||
|
* @return 三角形的高 |
||||
|
*/ |
||||
|
public double getHeight() { |
||||
|
return height; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 设置三角形的高 |
||||
|
* @param height 三角形的高 |
||||
|
*/ |
||||
|
public void setHeight(double height) { |
||||
|
this.height = height; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 绘制三角形 |
||||
|
*/ |
||||
|
@Override |
||||
|
public void draw() { |
||||
|
System.out.println("绘制一个底为 " + base + ",高为 " + height + " 的三角形"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
public class HelloWorld { |
||||
|
public static void main(String[] args) { |
||||
|
System.out.println("Hello, World!"); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,74 @@ |
|||||
|
import java.util.Scanner; |
||||
|
|
||||
|
/** |
||||
|
* 温度转换器程序(Java版本) |
||||
|
* 支持摄氏度(C)与华氏度(F)之间互转 |
||||
|
* |
||||
|
* 对应Python原始程序的功能移植 |
||||
|
*/ |
||||
|
public class TemperatureConverter { |
||||
|
|
||||
|
/** |
||||
|
* 将摄氏度转换为华氏度 |
||||
|
* |
||||
|
* @param c 摄氏温度(浮点数) |
||||
|
* @return 对应的华氏温度(浮点数) |
||||
|
*/ |
||||
|
public static double celsiusToFahrenheit(double c) { |
||||
|
return c * 9.0 / 5.0 + 32.0; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 将华氏度转换为摄氏度 |
||||
|
* |
||||
|
* @param f 华氏温度(浮点数) |
||||
|
* @return 对应的摄氏温度(浮点数) |
||||
|
*/ |
||||
|
public static double fahrenheitToCelsius(double f) { |
||||
|
return (f - 32.0) * 5.0 / 9.0; |
||||
|
} |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
Scanner scanner = new Scanner(System.in); |
||||
|
|
||||
|
// 提示用户输入,格式示例:“36.6 C”或“97 F”
|
||||
|
System.out.print("请输入要转换的温度与单位(例如 36.6 C 或 97 F):"); |
||||
|
String input = scanner.nextLine().trim(); |
||||
|
|
||||
|
// 检查输入是否为空
|
||||
|
if (input.isEmpty()) { |
||||
|
System.out.println("输入为空,程序退出。"); |
||||
|
scanner.close(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 按空格分割输入
|
||||
|
String[] parts = input.split("\\s+"); |
||||
|
|
||||
|
try { |
||||
|
// 允许用户输入两个部分:数值与单位
|
||||
|
double value = Double.parseDouble(parts[0]); |
||||
|
String unit = (parts.length > 1) ? parts[1].toUpperCase() : "C"; |
||||
|
|
||||
|
// 判断单位并进行相应转换
|
||||
|
if (unit.startsWith("C")) { |
||||
|
// 从摄氏度转换为华氏度
|
||||
|
double f = celsiusToFahrenheit(value); |
||||
|
System.out.printf("%.2f °C = %.2f °F%n", value, f); |
||||
|
} else if (unit.startsWith("F")) { |
||||
|
// 从华氏度转换为摄氏度
|
||||
|
double c = fahrenheitToCelsius(value); |
||||
|
System.out.printf("%.2f °F = %.2f °C%n", value, c); |
||||
|
} else { |
||||
|
System.out.println("未知单位,请使用 C 或 F。"); |
||||
|
} |
||||
|
|
||||
|
} catch (NumberFormatException e) { |
||||
|
System.out.println("输入解析失败,请按示例输入数值与单位,例如:36.6 C"); |
||||
|
} catch (ArrayIndexOutOfBoundsException e) { |
||||
|
System.out.println("输入格式错误,请按示例输入数值与单位,例如:36.6 C"); |
||||
|
} finally { |
||||
|
scanner.close(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,8 @@ |
|||||
|
package com.example.datacollect.command; |
||||
|
|
||||
|
import com.example.datacollect.repository.ArticleRepository; |
||||
|
|
||||
|
public interface Command { |
||||
|
String getName(); |
||||
|
void execute(String[] args, ArticleRepository repository); |
||||
|
} |
||||
@ -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,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,136 @@ |
|||||
|
package com.example; |
||||
|
|
||||
|
import com.example.entity.Course; |
||||
|
|
||||
|
/** |
||||
|
* 测试类,用于验证湖大选课系统功能 |
||||
|
*/ |
||||
|
public class CourseSystemTest { |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
// 初始化数据库
|
||||
|
DatabaseUtil.initDatabase(); |
||||
|
|
||||
|
// 清空现有数据
|
||||
|
DatabaseUtil.clearCourses(); |
||||
|
|
||||
|
// 添加测试数据
|
||||
|
addTestData(); |
||||
|
|
||||
|
// 创建CourseAnalysis实例
|
||||
|
CourseAnalysis analysis = new CourseAnalysis(); |
||||
|
|
||||
|
// 测试整体统计信息
|
||||
|
System.out.println("===== 测试整体统计信息 ====="); |
||||
|
System.out.println(analysis.getOverallStatistics()); |
||||
|
|
||||
|
// 测试课程类型分布
|
||||
|
System.out.println("\n===== 测试课程类型分布 ====="); |
||||
|
System.out.println(analysis.getCourseTypeDistribution()); |
||||
|
|
||||
|
// 测试院系统计
|
||||
|
System.out.println("\n===== 测试院系统计 ====="); |
||||
|
System.out.println(analysis.getDepartmentDistribution()); |
||||
|
|
||||
|
// 测试学分分布
|
||||
|
System.out.println("\n===== 测试学分分布 ====="); |
||||
|
System.out.println(analysis.getCreditDistribution()); |
||||
|
|
||||
|
// 测试热门课程
|
||||
|
System.out.println("\n===== 测试热门课程 ====="); |
||||
|
System.out.println(analysis.getTopCourses()); |
||||
|
|
||||
|
// 测试课程容量使用率
|
||||
|
System.out.println("\n===== 测试课程容量使用率 ====="); |
||||
|
System.out.println(analysis.getCourseUsageRate()); |
||||
|
|
||||
|
// 测试按院系分组的统计
|
||||
|
System.out.println("\n===== 测试按院系分组的统计 ====="); |
||||
|
System.out.println(analysis.getDepartmentStatistics()); |
||||
|
|
||||
|
// 测试教师课程统计
|
||||
|
System.out.println("\n===== 测试教师课程统计 ====="); |
||||
|
System.out.println(analysis.getTeacherStatistics()); |
||||
|
|
||||
|
// 测试课程容量利用率分析
|
||||
|
System.out.println("\n===== 测试课程容量利用率分析 ====="); |
||||
|
System.out.println(analysis.getCapacityAnalysis()); |
||||
|
|
||||
|
System.out.println("\n===== 测试完成 ====="); |
||||
|
} |
||||
|
|
||||
|
// 添加测试数据
|
||||
|
private static void addTestData() { |
||||
|
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春季"); |
||||
|
DatabaseUtil.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春季"); |
||||
|
DatabaseUtil.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春季"); |
||||
|
DatabaseUtil.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春季"); |
||||
|
DatabaseUtil.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春季"); |
||||
|
DatabaseUtil.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,127 @@ |
|||||
|
package com.example; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Scanner; |
||||
|
|
||||
|
/** |
||||
|
* 湖大选课系统主应用类 |
||||
|
* 不依赖Spring Boot,使用普通Java类实现 |
||||
|
*/ |
||||
|
public class HnuCourseSystem { |
||||
|
|
||||
|
public static void main(String[] args) { |
||||
|
// 初始化数据库
|
||||
|
DatabaseUtil.initDatabase(); |
||||
|
|
||||
|
// 创建CourseAnalysis实例
|
||||
|
CourseAnalysis analysis = new CourseAnalysis(); |
||||
|
|
||||
|
// 显示菜单
|
||||
|
try (Scanner scanner = new Scanner(System.in)) { |
||||
|
int choice; |
||||
|
|
||||
|
do { |
||||
|
System.out.println("===== 湖大选课系统 ====="); |
||||
|
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("请选择操作: "); |
||||
|
|
||||
|
choice = scanner.nextInt(); |
||||
|
scanner.nextLine(); // 消费换行符
|
||||
|
|
||||
|
switch (choice) { |
||||
|
case 1 -> { |
||||
|
// 查看整体统计信息
|
||||
|
System.out.println("\n整体统计信息:"); |
||||
|
Map<String, Object> overallStats = analysis.getOverallStatistics(); |
||||
|
for (Map.Entry<String, Object> entry : overallStats.entrySet()) { |
||||
|
System.out.println(entry.getKey() + ": " + entry.getValue()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
case 2 -> { |
||||
|
// 查看课程类型分布
|
||||
|
System.out.println("\n课程类型分布:"); |
||||
|
Map<String, Integer> typeDistribution = analysis.getCourseTypeDistribution(); |
||||
|
for (Map.Entry<String, Integer> entry : typeDistribution.entrySet()) { |
||||
|
System.out.println(entry.getKey() + ": " + entry.getValue()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
case 3 -> { |
||||
|
// 查看院系统计
|
||||
|
System.out.println("\n院系统计:"); |
||||
|
Map<String, Integer> deptDistribution = analysis.getDepartmentDistribution(); |
||||
|
for (Map.Entry<String, Integer> entry : deptDistribution.entrySet()) { |
||||
|
System.out.println(entry.getKey() + ": " + entry.getValue()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
case 4 -> { |
||||
|
// 查看学分分布
|
||||
|
System.out.println("\n学分分布:"); |
||||
|
Map<Double, Integer> creditDistribution = analysis.getCreditDistribution(); |
||||
|
for (Map.Entry<Double, Integer> entry : creditDistribution.entrySet()) { |
||||
|
System.out.println(entry.getKey() + "学分: " + entry.getValue() + "门"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
case 5 -> { |
||||
|
// 查看热门课程
|
||||
|
System.out.println("\n热门课程:"); |
||||
|
List<Map<String, Object>> topCourses = analysis.getTopCourses(); |
||||
|
for (int i = 0; i < topCourses.size(); i++) { |
||||
|
Map<String, Object> course = topCourses.get(i); |
||||
|
System.out.println((i + 1) + ". " + course.get("courseName") + " - " + course.get("teacher")); |
||||
|
System.out.println(" 院系: " + course.get("department") + ", 容量: " + course.get("capacity") + ", 已选: " + course.get("enrolled")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
case 6 -> { |
||||
|
// 查看课程容量使用率
|
||||
|
System.out.println("\n课程容量使用率:"); |
||||
|
List<Map<String, Object>> usageRates = analysis.getCourseUsageRate(); |
||||
|
for (int i = 0; i < Math.min(10, usageRates.size()); 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") + ", 使用率: " + String.format("%.2f%%", usage.get("usageRate"))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
case 7 -> { |
||||
|
// 查看按院系分组的统计
|
||||
|
System.out.println("\n按院系分组的统计:"); |
||||
|
Map<String, Map<String, Object>> deptStats = analysis.getDepartmentStatistics(); |
||||
|
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"))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
case 0 -> System.out.println("退出系统..."); |
||||
|
|
||||
|
default -> System.out.println("无效选择,请重新输入"); |
||||
|
} |
||||
|
|
||||
|
if (choice != 0) { |
||||
|
System.out.println("\n按Enter键继续..."); |
||||
|
scanner.nextLine(); |
||||
|
} |
||||
|
|
||||
|
} while (choice != 0); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue