• 通知最新的课程成绩到邮箱
  • 批量通知其他同学查询成绩
  • Cookie过期提醒

实现方式:使用Java发送http请求并解析结果,保存结果为txt文件,方便下次查询进行比对。如果发现新的课程成绩发布就发送邮件通知

Java代码实现

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.io.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScoreMonitor {

    // 从配置文件加载的属性
    private static String SCORE_URL;
    private static String COOKIE;
    private static String RECORD_FILE;
    private static String EMAIL_FROM;
    private static String EMAIL_PASSWORD;
    private static String EMAIL_TO;
    private static String MUTI_EMAIL_TO;
    private static String SMTP_HOST;
    private static String SMTP_PORT;

    private static final ObjectMapper objectMapper = new ObjectMapper();
    private static final Properties config = new Properties();

    static {
        File configFile = new File("config.properties"); // 相对于当前工作目录
        if (!configFile.exists()) {
            System.err.println("❌ config.properties 未在当前目录找到: " + configFile.getAbsolutePath());
            System.exit(1);
        }

        try (FileInputStream input = new FileInputStream(configFile)) {
            config.load(input);

            SCORE_URL = getRequiredProperty("score.url");
            COOKIE = getRequiredProperty("cookie");
            RECORD_FILE = getRequiredProperty("record.file");
            EMAIL_FROM = getRequiredProperty("email.from");
            EMAIL_PASSWORD = getRequiredProperty("email.password");
            EMAIL_TO = getRequiredProperty("email.to");
            MUTI_EMAIL_TO = getRequiredProperty("email.multi.to");
            SMTP_HOST = getRequiredProperty("smtp.host");
            SMTP_PORT = getRequiredProperty("smtp.port");

        } catch (IOException e) {
            System.err.println("❌ 加载 config.properties 失败: " + e.getMessage());
            e.printStackTrace();
            System.exit(1);
        }
    }

    private static String getRequiredProperty(String key) {
        String value = config.getProperty(key);
        if (value == null || value.trim().isEmpty()) {
            System.err.println("❌ 配置项缺失: " + key);
            System.exit(1);
        }
        return value.trim();
    }

    public static void main(String[] args) {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
        System.out.println("开始监控成绩更新...");

        scheduler.scheduleAtFixedRate(ScoreMonitor::checkForNewScores, 0, 20, TimeUnit.SECONDS);
    }

    private static void checkForNewScores() {
        System.out.println(currentTime() + "正在检查成绩更新...");
        try {
            String responseBody = fetchScores();
            if(responseBody.contains("登录")) {
                System.out.println(currentTime() + "登录已过期,请重新登录...");
                sendErrorEmailNotification("登录已过期,请重新登录...");
                //终止程序
                System.exit(0);
            }

            // 提取JSON部分

            JsonNode rootNode = objectMapper.readTree(responseBody);

            Set<String> existingCourseNames = loadExistingCourseNames();
            List<Map<String, String>> newEntries = new ArrayList<>();

            // 遍历外层数组(通常只有一个元素)

            boolean hasCourses = false;
            for (JsonNode item : rootNode) {
                JsonNode cjList = item.get("cjList");
                if (cjList != null && cjList.isArray()) {
                    hasCourses = true;
                    for (JsonNode course : cjList) {
                        String courseName = course.get("courseName").asText();
                        String cj = course.get("cj").asText(); // 注意:cj 是字符串类型 "86.0"

                        // 检查是否为新成绩
                        if (!existingCourseNames.contains(courseName)) {
                            System.out.println("发现新成绩: " + courseName + ", 成绩: " + cj);
                            
                            Map<String, String> entry = new HashMap<>();
                            entry.put("courseName", courseName);
                            entry.put("score", cj);
                            newEntries.add(entry);
                        }
                    }
                }
            }
            
            // 如果没有获取到任何课程成绩,发送错误通知
            if (!hasCourses) {
                sendErrorEmailNotification("未能获取到任何课程成绩");
            }

            // 如果有新成绩,则保存并发送邮件通知
            if (!newEntries.isEmpty()) {
                saveNewRecords(newEntries);
                sendEmailNotification(newEntries);
                sendCourseNameOnlyEmail(newEntries);
            }else{
                System.out.println(currentTime() + "本次查询未发现新的成绩...");
            }
            
        } catch (Exception e) {
            //e.printStackTrace();
            if(e instanceof java.net.SocketException) {
                System.out.println(currentTime() + "网络异常,等待下次查询...");
            }else{
                sendErrorEmailNotification(e.getMessage());
            }
        }
    }

    private static String fetchScores() throws Exception {
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpGet request = new HttpGet(SCORE_URL);
            request.setHeader("User-Agent", "Mozilla/5.0");
            request.setHeader("Cookie", COOKIE);
            try (CloseableHttpResponse response = client.execute(request)) {
                return EntityUtils.toString(response.getEntity(), "UTF-8");
            }
        }
    }


    private static Set<String> loadExistingCourseNames() {
        Set<String> courseNames = new HashSet<>();
        File file = new File(RECORD_FILE);
        if (!file.exists()) return courseNames;

        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String line;
            while ((line = reader.readLine()) != null) {
                if (line.contains("|")) {
                    String key = line.split(" \\| ")[0];
                    courseNames.add(key);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return courseNames;
    }

    private static void saveNewRecords(List<Map<String, String>> entries) {
        try (PrintWriter writer = new PrintWriter(new FileWriter(RECORD_FILE, true))) {
            for (Map<String, String> entry : entries) {
                String logLine = entry.get("courseName") + " | " + entry.get("score") + " | " + currentTime();
                writer.println(logLine);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void sendEmailNotification(List<Map<String, String>> entries) {
        Properties props = new Properties();
        props.put("mail.smtp.host", SMTP_HOST);
        props.put("mail.smtp.port", SMTP_PORT);
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");

        Session session = Session.getInstance(props, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(EMAIL_FROM, EMAIL_PASSWORD);
            }
        });

        try {
            Message message = new MimeMessage(session);
            message.setFrom(new InternetAddress(EMAIL_FROM));
            message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(EMAIL_TO));
            message.setSubject("【成绩更新通知】你有新的课程成绩!");

            StringBuilder content = new StringBuilder();
            content.append("<h2>发现 ").append(entries.size()).append(" 门新课程成绩:</h2><ul>");
            for (Map<String, String> e : entries) {
                content.append("<li><b>").append(e.get("courseName"))
                       .append("</b>:").append(e.get("score")).append(" 分</li>");
            }
            content.append("</ul><p>时间:").append(currentTime()).append("</p>");

            message.setContent(content.toString(), "text/html; charset=utf-8");
            Transport.send(message);
            System.out.println("✅ 邮件已发送!");
        } catch (MessagingException e) {
            System.err.println("❌ 邮件发送失败:");
            e.printStackTrace();
        }
    }

    private static void sendCourseNameOnlyEmail(List<Map<String, String>> entries) {
        Properties props = new Properties();
        props.put("mail.smtp.host", SMTP_HOST);
        props.put("mail.smtp.port", SMTP_PORT);
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");

        Session session = Session.getInstance(props, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(EMAIL_FROM, EMAIL_PASSWORD);
            }
        });

        try {
            Message message = new MimeMessage(session);
            message.setFrom(new InternetAddress(EMAIL_FROM));
            // 解析多个收件人
            String[] tos = MUTI_EMAIL_TO.split(",");
            InternetAddress[] addresses = new InternetAddress[tos.length];
            for (int i = 0; i < tos.length; i++) {
                addresses[i] = new InternetAddress(tos[i].trim());
            }
            message.setRecipients(Message.RecipientType.TO, addresses);
            message.setSubject("【成绩通知】有新的课程成绩发布!");

            StringBuilder content = new StringBuilder();
            content.append("<h2>发现 ").append(entries.size()).append(" 门新课程:</h2><ul>");
            for (Map<String, String> e : entries) {
                content.append("<li><b>").append(e.get("courseName")).append("</b></li>");
            }
            content.append("</ul>");
            content.append("<p>请登录教务系统查看具体课程信息</p>");
            content.append("<p>时间:").append(currentTime()).append("</p>");

            message.setContent(content.toString(), "text/html; charset=utf-8");
            Transport.send(message);
            System.out.println("✅ 课程名称邮件已批量发送!");
        } catch (MessagingException e) {
            System.err.println("❌ 课程名称邮件发送失败:");
            e.printStackTrace();
        }
    }

    private static void sendErrorEmailNotification(String errorMessage) {
        Properties props = new Properties();
        props.put("mail.smtp.host", SMTP_HOST);
        props.put("mail.smtp.port", SMTP_PORT);
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.starttls.enable", "true");

        Session session = Session.getInstance(props, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(EMAIL_FROM, EMAIL_PASSWORD);
            }
        });

        try {
            Message message = new MimeMessage(session);
            message.setFrom(new InternetAddress(EMAIL_FROM));
            message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(EMAIL_TO));
            message.setSubject("【成绩监控通知】出错了!");

            StringBuilder content = new StringBuilder();
            content.append("<h2>成绩监控出错:</h2>");
            content.append("<p><b>错误信息:</b>").append(errorMessage).append("</p>");
            content.append("<p>时间:").append(currentTime()).append("</p>");

            message.setContent(content.toString(), "text/html; charset=utf-8");
            Transport.send(message);
            System.out.println("✅ 错误通知邮件已发送!");
        } catch (MessagingException e) {
            System.err.println("❌ 错误通知邮件发送失败:");
            e.printStackTrace();
        }
    }

    private static String currentTime() {
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
}

config.propertis配置文件参考:

# 成绩查询接口
score.url=http://202.114.190.75/student/integratedQuery/scoreQuery/schemeScores/callback
cookie=student.urpSoft.cn=aaaF-CvacbXahsDshbSz; route=77c74b96690cdw2310a20d26497945; selectionBar=1343373
record.file=recorded_scores.txt

# 邮件发送配置
email.from=notify@litx.cn
email.password=%jUG3ajqdCPQgP22
email.to=x@litx.cn
email.multi.to=a@litx.cn,b@litx.cn
smtp.host=smtp.qiye.163.com
smtp.port=587

pom.xml项目依赖

如果需要打包成JAR并部署在服务器中运行,则需要在 pom.xml 中配置 maven-shade-plugin 插件来生成一个可执行的 JAR(包含主类和依赖)。否则会出现没有主清单属性错误:"C:\Users\x\Desktop\temp>java -jar score-notify-1.0-SNAPSHOT.jar
score-notify-1.0-SNAPSHOT.jar中没有主清单属性"

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.litx</groupId>
    <artifactId>score-notify</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- HTTP 客户端 -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.14</version>
        </dependency>

        <dependency>
            <groupId>org.json</groupId>
            <artifactId>json</artifactId>
            <version>20231013</version>
        </dependency>

        <!-- JSON 解析 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.3</version>
        </dependency>

        <!-- 邮件发送 -->
        <dependency>
            <groupId>com.sun.mail</groupId>
            <artifactId>javax.mail</artifactId>
            <version>1.6.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- 其他插件... -->

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.6.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>cn.litx.ScoreMonitor</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

使用Docker部署会出现容器时间不对,这是因为容器使用的是自己的时区或系统时间,而不是宿主机的时间。可以添加环境变量指定时区:

docker run -e TZ=Asia/Shanghai your_image

效果展示

本人通知:

其他同学通知