Java实现期末成绩发布邮件通知
- 通知最新的课程成绩到邮箱
- 批量通知其他同学查询成绩
- 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
效果展示
本人通知:
其他同学通知
本文是原创文章,完整转载请注明来自 小希的网站
评论
匿名评论
隐私政策
你无需删除空行,直接评论以获取最佳展示效果