diff --git a/.github/workflows/schedule.yml b/.github/workflows/schedule.yml
index 1992840..6e805dd 100644
--- a/.github/workflows/schedule.yml
+++ b/.github/workflows/schedule.yml
@@ -40,6 +40,9 @@ jobs:
with:
distribution: 'temurin'
java-version: '17'
+
+ - name: "Setup tesseract-ocr"
+ run: "sudo apt install tesseract-ocr -y && mv config/tessdata action/config/tessdata"
# 缓存信息,当缓存发生变化时,请修改key值
- name: "Cache autocard cache file"
diff --git a/.gitignore b/.gitignore
index 1ef2e8f..fef42d4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,3 +39,5 @@ cache/
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
+test_*.properties
+code/
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..eaf91e2
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..22a2817
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..4140949
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..a468a99
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..719f22c
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
new file mode 100644
index 0000000..93e4b17
--- /dev/null
+++ b/.idea/runConfigurations.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..c8397c9
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 88b17e3..a922885 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,27 @@
|dmg |MacOS |
|exe |Win |
-**STEP 3 下载作者提供的发行版**
+**STEP 3 安装tesseract-ocr**
+该项目基于tesseract-ocr做验证码识别,需要在系统环境中安装tesseract-ocr引擎,可以参考[tesseract-ocr主页](https://tesseract-ocr.github.io/tessdoc/Home.html)。安装配置完成可以正常使用`tesseract`命令。
+```
+(base) [admin@Sat May 07-22:06:17 autocard]$ tesseract
+Usage:
+ tesseract --help | --help-extra | --version
+ tesseract --list-langs
+ tesseract imagename outputbase [options...] [configfile...]
+
+OCR options:
+ -l LANG[+LANG] Specify language(s) used for OCR.
+NOTE: These options must occur before any configfile.
+
+Single options:
+ --help Show this help message.
+ --help-extra Show extra help for advanced users.
+ --version Show version information.
+ --list-langs List available languages for tesseract engine.
+```
+
+**STEP 4 下载作者提供的发行版**
在[gitee](https://gitee.com/GCSZHN/AutoCard/releases/)或[github](https://github.com/GCS-ZHN/AutoCard/releases)的项目发行版页面,下载最新的发行版(autocard-XXX.zip,XXX为版本号)。并解压。可以看到解压后目录结构如下
```txt
@@ -42,8 +62,9 @@
----config/
------application.json ## 用户配置,如账号密码等
------log4j2.xml ## 日志配置,不用修改
+------tessdata/ ## 放置OCR模型数据
```
-**STEP 4 修改application.json**
+**STEP 5 修改application.json**
用任意文本编辑器打开config目录下的application.json,配置下列信息。
```json
@@ -121,7 +142,7 @@ cron表达式是用于定时任务的经典表达式,该参数允许用户自
```
delay参数为true时,每次执行任务会随机延时0~1800秒,这样的好处在于每天打卡时间不固定。
-**STEP 5 运行程序**
+**STEP 6 运行程序**
需要通过命令行来运行程序,在Windows下,常见的命令行是cmd和powershell,打开方式“WIN + R”,输入"cmd"或"powershell",确定即可。linux服务器打开即是shell命令行页面(To小白:如何连接Linux服务器请自行百度一下,拥有服务器用户名、密码、IP、端口,通过ssh客户端访问)。
@@ -240,6 +261,8 @@ powershell build.ps1 ## windows
若打卡题目被更新或者你的任何信息情况有变化(如返校),请先手动打卡一次。本项目仅供学习参考。使用时请确保信息的正确性。滥用造成的后果请自行承担。
## 八、更新记录
+### v1.4.7
+2022年5月7日,学校引入图片验证码,特发布此次更新支持验证码识别。同时修复了相关issue的BUG。
### v1.4.6
修复了相关[issue](https://github.com/GCS-ZHN/AutoCard/issues/11),支持了设置最大重试次数。
### v1.4.5
diff --git a/action/autocard.jar b/action/autocard.jar
index 8fa7838..ee2d070 100644
Binary files a/action/autocard.jar and b/action/autocard.jar differ
diff --git a/config/tessdata/eng.traineddata b/config/tessdata/eng.traineddata
new file mode 100644
index 0000000..f4744c2
Binary files /dev/null and b/config/tessdata/eng.traineddata differ
diff --git a/pom.xml b/pom.xml
index 386aa94..911318b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
org.gcszhn
autocard
- 1.4.6
+ 1.4.7
jar
Auto Heathy Report for Zhejiang University
@@ -111,6 +111,21 @@
mail
1.4.7
+
+
+
+ net.java.dev.jna
+ jna
+ 5.10.0
+
+
+
+
+ net.sourceforge.tess4j
+ tess4j
+ 5.1.0
+
+
org.projectlombok
diff --git a/src/main/java/org/gcszhn/autocard/service/AutoCardJob.java b/src/main/java/org/gcszhn/autocard/service/AutoCardJob.java
index 225746f..b46da3e 100644
--- a/src/main/java/org/gcszhn/autocard/service/AutoCardJob.java
+++ b/src/main/java/org/gcszhn/autocard/service/AutoCardJob.java
@@ -53,7 +53,15 @@ public static void execute(
String dingtalkURL = dataMap.getString("dingtalkurl");
String dingtalkSecret = dataMap.getString("dingtalksecret");
int maxTrial = Optional.ofNullable(dataMap.getString("maxtrial"))
- .map((String value)->Integer.parseInt(value))
+ .map((String value)-> {
+ try{
+ if (value.equals("")) return DEFAULT_MAX_TRIAL;
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ LogUtils.printMessage("无效的整数格式", LogUtils.Level.ERROR);
+ return DEFAULT_MAX_TRIAL;
+ }
+ })
.orElse(DEFAULT_MAX_TRIAL);
//开启随机延迟,这样可以避免每次打卡时间过于固定
try {
diff --git a/src/main/java/org/gcszhn/autocard/service/AutoCardService.java b/src/main/java/org/gcszhn/autocard/service/AutoCardService.java
index d0e7808..95b4644 100644
--- a/src/main/java/org/gcszhn/autocard/service/AutoCardService.java
+++ b/src/main/java/org/gcszhn/autocard/service/AutoCardService.java
@@ -15,6 +15,8 @@
*/
package org.gcszhn.autocard.service;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
@@ -24,13 +26,13 @@
import com.alibaba.fastjson.JSONObject;
+import lombok.Getter;
+import net.sourceforge.tess4j.TesseractException;
+import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.message.BasicNameValuePair;
import org.gcszhn.autocard.AppConfig;
-import org.gcszhn.autocard.utils.DigestUtils;
-import org.gcszhn.autocard.utils.ImageUtils;
-import org.gcszhn.autocard.utils.LogUtils;
-import org.gcszhn.autocard.utils.StatusCode;
+import org.gcszhn.autocard.utils.*;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
@@ -39,6 +41,8 @@
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
+import javax.imageio.ImageIO;
+
/**
* 健康打卡实现类
* @author Zhang.H.N
@@ -57,20 +61,35 @@ public class AutoCardService implements AppService {
private String reportUrl;
@Value("${app.autoCard.submitUrl}")
private String submitUrl;
+ @Value("${app.autoCard.codeUrl}")
+ private String codeUrl;
/**浙大通行证客户端 */
@Autowired
private ZJUClientService client;
/**应用配置实例 */
@Autowired
private AppConfig appConfig;
+ /**是否在线 */
+ private @Getter boolean online = false;
/**
- * 用于访问打卡页面
* @param username 用户名
* @param password 密码
+ * */
+ public void login(String username, String password) {
+ try {
+ online = client.login(username, password);
+ } catch (Exception e) {
+ LogUtils.printMessage("用户登录失败", LogUtils.Level.ERROR);
+ }
+
+ }
+ /**
+ * 用于访问打卡页面
+
* @return 打卡页面HTML源码
*/
- public String getPage(String username, String password) {
- if(client.login(username, password)) {
+ public String getPage() {
+ if(isOnline()) {
String page1 = client.doGetText(reportUrl);
Boolean formvalidation = appConfig.getConfigItem("formvalidation", Boolean.class);
if (formvalidation!=null && !formvalidation) {
@@ -89,6 +108,8 @@ public String getPage(String username, String password) {
} else {
LogUtils.printMessage("表单校验失败,请检查健康打卡页面是否更新或等待一会再次尝试,若更新请删除缓存文件并重启打卡程序", LogUtils.Level.ERROR);
}
+ } else {
+ LogUtils.printMessage("用户未登录", LogUtils.Level.ERROR);
}
return null;
}
@@ -122,12 +143,10 @@ public boolean formValidation(String html) {
}
/**
* 用于提取已有提交信息
- * @param username 用户名
- * @param password 密码
* @return 已有提交信息组成的键值对列表
*/
- public ArrayList getOldInfo(String username, String password) {
- String page = getPage(username, password);
+ public ArrayList getOldInfo() {
+ String page = getPage();
if (page==null) return null;
ArrayList res = new ArrayList<>();
// 该部分模拟网页JS代码进行信息合并
@@ -176,6 +195,56 @@ public ArrayList getOldInfo(String username, String password) {
}
return res;
}
+ public BufferedImage getCodeImage() {
+ if (isOnline()) {
+ LogUtils.printMessage("获取验证码", LogUtils.Level.INFO);
+ return Optional.ofNullable(client.doGet(codeUrl + "?_t=" + Math.random())).map((HttpDataPair pair)-> {
+ try {
+ HttpEntity entity = null;
+ if (pair.getResponse() != null && pair.getResponse().getEntity() != null) {
+ BufferedImage image = ImageIO.read(pair.getResponse().getEntity().getContent());
+ return image;
+
+ }
+ } catch (IOException e) {
+ LogUtils.printMessage(null, e, LogUtils.Level.ERROR);
+ } finally {
+ try {
+ pair.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return null;
+ }).orElse(null);
+ } else {
+ LogUtils.printMessage("用户未登录", LogUtils.Level.ERROR);
+ }
+ return null;
+ }
+ public String getCode(BufferedImage codeImage) {
+ Optional code = Optional.ofNullable(codeImage).map((BufferedImage image)->{
+ try {
+ LogUtils.printMessage("识别验证码", LogUtils.Level.INFO);
+ String value = OCRUtils.recognize(image);
+ if (value != null) {
+ value = value.strip().toUpperCase();
+ value.replaceAll("1", "I");
+ if (value.length() != 4) {
+ value = null;
+ }
+ }
+ return value;
+ } catch (TesseractException e) {
+ LogUtils.printMessage("验证码识别异常", e, LogUtils.Level.ERROR);
+ return null;
+ }
+ });
+ return code.isPresent() ? code.get(): null;
+ }
+ public String getCode() {
+ return getCode(getCodeImage());
+ }
/**
* 用于提交打卡信息
* @param username 用户名
@@ -188,40 +257,69 @@ public ArrayList getOldInfo(String username, String password) {
public StatusCode submit(String username, String password) {
StatusCode statusCode = new StatusCode();
try {
- LogUtils.printMessage("准备提交打卡 " + username);
- ArrayList info = getOldInfo(username, password);
+ login(username, password);
+
+ int maxTrial = 20;
+ JSONObject resp = null;
+ LogUtils.Level level = null;
+ int status = 3;
+ NameValuePair codePair = null;
+ String area = null;
+ ArrayList info = getOldInfo();
if (info==null) {
LogUtils.printMessage("打卡信息获取失败", LogUtils.Level.ERROR);
statusCode.setMessage(username+"的打卡信息获取失败,可能是打卡更新了或网络不稳定,请查看后台打卡日志输出");
statusCode.setStatus(-1);
return statusCode;
}
- String area = null;
+
for (NameValuePair pair: info) {
if (pair.getName().equals("area")) {
area = pair.getValue();
break;
}
}
-
- JSONObject resp;
- try {
- resp = JSONObject.parseObject(client.doPostText(submitUrl, info));
- } catch (Exception e) {
- resp = new JSONObject();
- resp.put("e", 3);
- resp.put("m", "打卡提交失败");
- }
+ SAVE: while (maxTrial > 0) {
- int status = resp.getIntValue("e");
- LogUtils.Level level = null;
- switch(status) {
- case 0:{level= LogUtils.Level.INFO;break;}
- case 1:{level= LogUtils.Level.ERROR;break;}
- default: {
- level = LogUtils.Level.ERROR;
+ while (maxTrial > 0) {
+ maxTrial --;
+ String code = getCode();
+ if (code != null) {
+ if (codePair != null) info.remove(codePair);
+ codePair = new BasicNameValuePair("verifyCode", code);
+ info.add(codePair);
+ break;
+ } else {
+ LogUtils.printMessage("验证码识别错误,剩余机会:" + maxTrial, LogUtils.Level.ERROR);
+ }
}
+
+ try {
+ LogUtils.printMessage("准备提交打卡 " + username);
+ resp = JSONObject.parseObject(client.doPostText(submitUrl, info));
+ } catch (Exception e) {
+ resp = new JSONObject();
+ resp.put("e", 3);
+ resp.put("m", "打卡提交失败");
+ }
+
+ status = resp.getIntValue("e");
+ switch(status) {
+ case 0:{level= LogUtils.Level.INFO;break;}
+ case 1:{
+ if (maxTrial > 0 && resp.getString("m").equals("验证码错误")) {
+ LogUtils.printMessage("验证码识别错误,剩余机会:" + maxTrial, LogUtils.Level.ERROR);
+ Thread.sleep(2000);
+ continue SAVE;
+ }
+ }
+ default: {
+ level = LogUtils.Level.ERROR;
+ }
+ }
+ break;
}
+
JSONObject userInfo = client.getUserInfo();
String message = String.format("%s,你好,今日自动健康打卡状态:%s,打卡地区为:%s(如若区域不符,请次日手动打卡更改地址)",
userInfo == null || userInfo.getString("userName") == null? username: userInfo.getString("userName"),
@@ -253,6 +351,7 @@ public StatusCode submit(String username, String password) {
return statusCode;
}
+
@Override
public void close() {
try {
@@ -267,5 +366,6 @@ public void close() {
*/
public void logout() {
client.clearCookie();
+ online = false;
}
}
diff --git a/src/main/java/org/gcszhn/autocard/service/DingTalkHookService.java b/src/main/java/org/gcszhn/autocard/service/DingTalkHookService.java
index 16fee63..f335828 100644
--- a/src/main/java/org/gcszhn/autocard/service/DingTalkHookService.java
+++ b/src/main/java/org/gcszhn/autocard/service/DingTalkHookService.java
@@ -22,6 +22,7 @@
import javax.crypto.spec.SecretKeySpec;
import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Base64;
@@ -49,7 +50,7 @@ public class DingTalkHookService implements WebHookService {
* @param isAtAll 是否at全体
* @return 发送状态对象
*/
- public StatusCode send(String payLoadURL, JSONObject message, String type, boolean isAtAll) {
+ public StatusCode send(String payLoadURL, JSONObject message, String type, boolean isAtAll, String... atMobiles) {
StatusCode statusCode = checkURL(payLoadURL);
if (statusCode.getStatus() == 0) {
JSONObject jsonObject = new JSONObject();
@@ -57,7 +58,13 @@ public StatusCode send(String payLoadURL, JSONObject message, String type, boole
jsonObject.put(type, message);
jsonObject.put("at", new JSONObject());
jsonObject.getJSONObject("at").put("isAtAll", isAtAll);
- JSONObject res = JSON.parseObject(client.entityToString(client.getResponseContent(client.doPost(payLoadURL, jsonObject.toJSONString(), "application/json"))));
+ JSONArray mobileArray = new JSONArray();
+ jsonObject.getJSONObject("at").put("atMobiles", mobileArray);
+ for (String mobile: atMobiles) {
+ mobileArray.add(mobile);
+ }
+
+ JSONObject res = JSON.parseObject(client.getTextContent(client.doPost(payLoadURL, jsonObject.toJSONString(), "application/json")));
statusCode.setStatus(res.getIntValue("errcode"));
statusCode.setMessage(res.getString("errmsg"));
}
@@ -76,10 +83,10 @@ public StatusCode sendText(String payLoadURL, String info) {
* @param isAtAll 是否at全体
* @return 发送状态对象
*/
- public StatusCode sendText(String payLoadURL, String info, boolean isAtAll) {
+ public StatusCode sendText(String payLoadURL, String info, boolean isAtAll, String... atMobiles) {
JSONObject message = new JSONObject();
message.put("content", info);
- return send(payLoadURL, message, "text", isAtAll);
+ return send(payLoadURL, message, "text", isAtAll, atMobiles);
}
@Override
@@ -95,11 +102,11 @@ public StatusCode sendMarkdown(String payLoadURL, String title, String content)
* @param isAtAll 是否at全体
* @return 发送状态对象
*/
- public StatusCode sendMarkdown(String payLoadURL, String title, String content, boolean isAtAll) {
+ public StatusCode sendMarkdown(String payLoadURL, String title, String content, boolean isAtAll, String... atMobiles) {
JSONObject message = new JSONObject();
message.put("title", title);
message.put("text", content);
- return send(payLoadURL, message, "markdown", isAtAll);
+ return send(payLoadURL, message, "markdown", isAtAll, atMobiles);
}
/**
diff --git a/src/main/java/org/gcszhn/autocard/service/ZJUClientService.java b/src/main/java/org/gcszhn/autocard/service/ZJUClientService.java
index 668ee1d..7d0f4a6 100644
--- a/src/main/java/org/gcszhn/autocard/service/ZJUClientService.java
+++ b/src/main/java/org/gcszhn/autocard/service/ZJUClientService.java
@@ -33,11 +33,8 @@
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.message.BasicNameValuePair;
import org.gcszhn.autocard.AppConfig;
-import org.gcszhn.autocard.utils.HttpClientUtils;
-import org.gcszhn.autocard.utils.LogUtils;
+import org.gcszhn.autocard.utils.*;
import org.gcszhn.autocard.utils.LogUtils.Level;
-import org.gcszhn.autocard.utils.RSAEncryptUtils;
-import org.gcszhn.autocard.utils.StatusCode;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.beans.factory.annotation.Value;
@@ -91,12 +88,12 @@ private StatusCode getExecution() {
try {
/**暂时创建一个禁止自动重定向的客户端 */
setHttpClient(false, 0);
- CloseableHttpResponse response = doGet(loginUrl);
- int httpStatus = response.getStatusLine().getStatusCode();
+ HttpDataPair dataPair = doGet(loginUrl);
+ int httpStatus = dataPair.getResponse().getStatusLine().getStatusCode();
if (httpStatus==302) {
statusCode.setStatus(1);
} else {
- String textContent = entityToString(getResponseContent(response));
+ String textContent = getTextContent(dataPair);
Document document = Jsoup.parse(textContent);
statusCode.setStatus(0);
statusCode.setMessage(document.getElementsByAttributeValue("name", "execution").val());
@@ -173,13 +170,13 @@ public String login(String username, String password, String targetService, bool
parameters.add(new BasicNameValuePair("_eventId", "submit"));
parameters.add(new BasicNameValuePair("authcode", ""));
parameters.add(new BasicNameValuePair("execution", execution.getMessage()));
-
- //登录正常时,返回为302重定向
- CloseableHttpResponse response = doPost(loginUrl, parameters);
- String textContent = targetService!=null?entityToString(getResponseContent(response)):"";
+
+ HttpDataPair dataPair = doPost(loginUrl, parameters);
+ String textContent = targetService!=null? getTextContent(dataPair):"";
if (checkUserInfo(username)) {
return textContent;
}
+ if (dataPair != null) dataPair.close();
} catch (Exception e) {
LogUtils.printMessage(null, e, Level.ERROR);
}
diff --git a/src/main/java/org/gcszhn/autocard/utils/HttpClientUtils.java b/src/main/java/org/gcszhn/autocard/utils/HttpClientUtils.java
index c3491e8..243e52c 100644
--- a/src/main/java/org/gcszhn/autocard/utils/HttpClientUtils.java
+++ b/src/main/java/org/gcszhn/autocard/utils/HttpClientUtils.java
@@ -29,14 +29,10 @@
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
-import org.apache.http.StatusLine;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCookieStore;
@@ -95,10 +91,14 @@ public void setHttpClient(boolean redirectsEnabled, int maxRedirects) {
.setCircularRedirectsAllowed(true)
.setMaxRedirects(maxRedirects)
.setRedirectsEnabled(redirectsEnabled)
+ .setConnectionRequestTimeout(20000)
+ .setSocketTimeout(20000)
+ .setConnectionRequestTimeout(10000)
.build();
if (cookieStore==null) initCookieStore();
httpClient = HttpClients.custom()
.setDefaultCookieStore(cookieStore)
+ .setMaxConnTotal(20)
.setDefaultRequestConfig(config)
.setUserAgent(USER_AGENT).build();
} catch (Exception e) {
@@ -122,57 +122,23 @@ private void initCookieStore() {
LogUtils.printMessage(e.getMessage(), LogUtils.Level.DEBUG);
}
}
- /**
- * 判断HTTP响应状态是否成功
- * @param response 响应实例
- * @return true为成功
- */
- public CloseableHttpResponse isSuccess(CloseableHttpResponse response) {
- if (response==null) return null;
- StatusLine statusLine = response.getStatusLine();
- try {
- if (statusLine.getStatusCode()<400) {
- return doRedirects(response);
- }
- } catch (Exception e) {
- LogUtils.printMessage(null, e, LogUtils.Level.ERROR);
- }
- LogUtils.printMessage(statusLine.toString(), LogUtils.Level.ERROR);
- return null;
- }
/**
* 完成响应的302重定向,对于POST等请求,httpclient无法自动重定向其响应
- * @param response 未重定向响应
- * @return 重定向后的响应,若没有重定向,则返回原响应
+ * @param dataPair HTTP请求-响应对
+ * @return 重定向后的HTTP请求-响应对,若没有重定向,则返回原HTTP请求-响应对
*/
- private CloseableHttpResponse doRedirects(CloseableHttpResponse response) {
- if (!isRedirectsEnabled()||response==null) return response;
+ private HttpDataPair doRedirects(HttpDataPair dataPair) {
+ if (!isRedirectsEnabled()||dataPair==null) return dataPair;
try {
- int statusCode = response.getStatusLine().getStatusCode();
+ int statusCode = dataPair.getResponse().getStatusLine().getStatusCode();
if (statusCode == 302) {
- String url = response.getFirstHeader("Location").getValue();
+ String url = dataPair.getResponse().getFirstHeader("Location").getValue();
if (url!=null) {
- response.close();
+ dataPair.close();
return doRedirects(doGet(url));
}
}
- return response;
- } catch (Exception e) {
- LogUtils.printMessage(null, e, LogUtils.Level.ERROR);
- }
- return null;
- }
- /**
- * 获取响应正文的字符串编码数据
- * @param response 响应实例
- * @return 请求正文实体
- */
- public HttpEntity getResponseContent(CloseableHttpResponse response) {
- response = isSuccess(response);
- try {
- if (response!=null) {
- return response.getEntity();
- };
+ return dataPair;
} catch (Exception e) {
LogUtils.printMessage(null, e, LogUtils.Level.ERROR);
}
@@ -184,10 +150,14 @@ public HttpEntity getResponseContent(CloseableHttpResponse response) {
* @param headers 可选请求头
* @return 响应实例
*/
- private CloseableHttpResponse getResponse(HttpUriRequest request, Header... headers) {
+ private HttpDataPair getResponse(HttpRequestBase request, Header... headers) {
request.setHeaders(headers);
try {
- return httpClient.execute(request);
+ HttpDataPair dataPair = new HttpDataPair();
+ dataPair.setRequest(request);
+ dataPair.setResponse(httpClient.execute(request));
+ dataPair = doRedirects(dataPair);
+ return dataPair;
} catch (Exception e) {
LogUtils.printMessage(null, e, LogUtils.Level.ERROR);
}
@@ -199,7 +169,7 @@ private CloseableHttpResponse getResponse(HttpUriRequest request, Header... head
* @param headers 可选请求头
* @return 响应
*/
- public CloseableHttpResponse doGet(String url, Header... headers) {
+ public HttpDataPair doGet(String url, Header... headers) {
return doGet(url, null, headers);
}
/**
@@ -209,7 +179,7 @@ public CloseableHttpResponse doGet(String url, Header... headers) {
* @param headers 可选请求头
* @return 响应
*/
- public CloseableHttpResponse doGet(String url, List parameters,Header... headers) {
+ public HttpDataPair doGet(String url, List parameters,Header... headers) {
try {
URIBuilder uriBuilder = new URIBuilder(url, AppConfig.APP_CHARSET);
if (parameters != null) uriBuilder.setParameters(parameters);
@@ -228,7 +198,7 @@ public CloseableHttpResponse doGet(String url, List parameters,He
* @param headers 可选请求头
* @return 响应正文
*/
- public CloseableHttpResponse doPost(String url, Header... headers) {
+ public HttpDataPair doPost(String url, Header... headers) {
return doPost(url, null, headers);
}
/**
@@ -238,7 +208,7 @@ public CloseableHttpResponse doPost(String url, Header... headers) {
* @param headers 可选请求头
* @return 响应
*/
- public CloseableHttpResponse doPost(String url, List parameters, Header... headers) {
+ public HttpDataPair doPost(String url, List parameters, Header... headers) {
LogUtils.printMessage("Try to post " + url, LogUtils.Level.DEBUG);
try {
HttpPost request = new HttpPost(url);
@@ -253,7 +223,7 @@ public CloseableHttpResponse doPost(String url, List parameters,
}
return null;
}
- public CloseableHttpResponse doPost(String url, String content, String contentType, Header... headers) {
+ public HttpDataPair doPost(String url, String content, String contentType, Header... headers) {
LogUtils.printMessage("Try to post " + url, LogUtils.Level.DEBUG);
try {
HttpPost request = new HttpPost(url);
@@ -285,7 +255,7 @@ public String doGetText(String url, Header... headers) {
* @return 文本
*/
public String doGetText(String url, List parameters, Header... headers) {
- return entityToString(getResponseContent(doGet(url, parameters, headers)));
+ return getTextContent(doGet(url, parameters, headers));
}
/**
* 无参数Post请求获取文本
@@ -304,7 +274,7 @@ public String doPostText(String url, Header... headers) {
* @return 文本
*/
public String doPostText(String url, List parameters, Header... headers) {
- return entityToString(getResponseContent(doPost(url, parameters, headers)));
+ return getTextContent(doPost(url, parameters, headers));
}
/**
* GET请求下载文件
@@ -325,14 +295,16 @@ public void doDownload(String filename, String url, Header...headers) {
public void doDownload(String filename, String url, Methods methods, Header... headers) {
File file = new File(filename);
file.getParentFile().mkdirs();
+ HttpDataPair dataPair = null;
try(FileOutputStream fos = new FileOutputStream(file)) {
- HttpEntity entity = null;
switch (methods) {
- case GET:entity=getResponseContent(doGet(url, headers)); break;
- case POST:entity=getResponseContent(doPost(url, headers)); break;
+ case GET:dataPair = doGet(url, headers); break;
+ case POST:dataPair = doPost(url, headers); break;
default:throw new UnsupportedOperationException("Only support GET/POST currently");
}
- if (entity!=null) {
+
+ if (dataPair != null && dataPair.getResponse() != null) {
+ HttpEntity entity = dataPair.getResponse().getEntity();
byte[] buffer = new byte[1024];
InputStream inputStream = entity.getContent();
int len;
@@ -343,20 +315,35 @@ public void doDownload(String filename, String url, Methods methods, Header... h
LogUtils.printMessage("Saved to " + file.getCanonicalPath());
} catch (Exception e) {
LogUtils.printMessage(null, e, LogUtils.Level.ERROR);
+ } finally {
+ if (dataPair != null) {
+ try {
+ dataPair.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
}
}
/**
* 将响应实体编码为字符串,主要用于文本响应解析
- * @param entity 响应实体
+ * @param dataPair HTTP请求-响应对
* @return 编码为字符串
*/
- public String entityToString(HttpEntity entity){
+ public String getTextContent(HttpDataPair dataPair){
try {
- if (entity != null) {
- return EntityUtils.toString(entity);
+ if (dataPair != null && dataPair.getResponse() != null) {
+ String data = EntityUtils.toString(dataPair.getResponse().getEntity());
+ return data;
}
} catch (Exception e) {
LogUtils.printMessage(null, e, LogUtils.Level.ERROR);
+ } finally {
+ try {
+ dataPair.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
}
return null;
}
diff --git a/src/main/java/org/gcszhn/autocard/utils/HttpDataPair.java b/src/main/java/org/gcszhn/autocard/utils/HttpDataPair.java
new file mode 100644
index 0000000..c77d3ee
--- /dev/null
+++ b/src/main/java/org/gcszhn/autocard/utils/HttpDataPair.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2021 Zhang.H.N.
+ *
+ * Licensed under the Apache License, Version 2.0 (thie "License");
+ * You may not use this file except in compliance with the license.
+ * You may obtain a copy of the License at
+ *
+ * http://wwww.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language govering permissions and
+ * limitations under the License.
+ */
+package org.gcszhn.autocard.utils;
+
+import lombok.Data;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.util.EntityUtils;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+@Data
+public class HttpDataPair implements Closeable {
+ HttpRequestBase request;
+ CloseableHttpResponse response;
+
+ @Override
+ public void close() throws IOException {
+ if (response != null) {
+ EntityUtils.consumeQuietly(response.getEntity());
+ response.close();
+ }
+ if (request != null) {
+ request.releaseConnection();
+ }
+ LogUtils.printMessage("关闭连接" + request.getURI(), LogUtils.Level.DEBUG);
+ }
+}
diff --git a/src/main/java/org/gcszhn/autocard/utils/OCRUtils.java b/src/main/java/org/gcszhn/autocard/utils/OCRUtils.java
new file mode 100644
index 0000000..d2941da
--- /dev/null
+++ b/src/main/java/org/gcszhn/autocard/utils/OCRUtils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2021 Zhang.H.N.
+ *
+ * Licensed under the Apache License, Version 2.0 (thie "License");
+ * You may not use this file except in compliance with the license.
+ * You may obtain a copy of the License at
+ *
+ * http://wwww.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language govering permissions and
+ * limitations under the License.
+ */
+package org.gcszhn.autocard.utils;
+
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+
+import net.sourceforge.tess4j.ITesseract;
+import net.sourceforge.tess4j.Tesseract;
+import net.sourceforge.tess4j.TesseractException;
+import net.sourceforge.tess4j.util.ImageHelper;
+
+import javax.imageio.ImageIO;
+
+public class OCRUtils {
+ public static String recognize(String filename) throws TesseractException, IOException {
+ return recognize(new File(filename));
+ }
+
+ public static String recognize(File file) throws TesseractException, IOException {
+ return recognize(ImageIO.read(file));
+ }
+
+ public static String recognize(BufferedImage image) throws TesseractException {
+ if (image == null) return null;
+ ITesseract instance = new Tesseract();
+ instance.setDatapath("config/tessdata");
+ instance.setLanguage("eng");
+ image = ImageHelper.convertImageToGrayscale(image);
+ image = ImageHelper.convertImageToBinary(image);
+ // System.out.println("Resizing");
+ //image = ImageHelper.getScaledInstance(image, image.getWidth() * 5, image.getWidth() * 5);
+ return instance.doOCR(image);
+ }
+}
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 4590447..a930cd1 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -24,6 +24,7 @@ app:
autoCard:
reportUrl: https://healthreport.zju.edu.cn/ncov/wap/default/index
submitUrl: https://healthreport.zju.edu.cn/ncov/wap/default/save
+ codeUrl: https://healthreport.zju.edu.cn/ncov/wap/default/code
cronExpression: 0 0 9 * * ? * ## 每天09:00:00触发
immediate: false
config: "file://config/application.json"
diff --git a/src/test/java/org/gcszhn/autocard/AppTest.java b/src/test/java/org/gcszhn/autocard/AppTest.java
index bd2d408..e4cd82d 100644
--- a/src/test/java/org/gcszhn/autocard/AppTest.java
+++ b/src/test/java/org/gcszhn/autocard/AppTest.java
@@ -15,6 +15,8 @@
*/
package org.gcszhn.autocard;
+import java.util.ResourceBundle;
+
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@@ -27,9 +29,19 @@
@RunWith(SpringRunner.class)
@SpringBootTest
public abstract class AppTest {
- protected static final String USERNAME = "***";
- protected static final String PASSWORD = "***";
- protected static final String MAIL = "***";
- protected static final String PAYLOAD_URL = "https://oapi.dingtalk.com/robot/send?access_token=***";
- protected static final String SECRET = "SEC***";
+ protected static final String USERNAME ;
+ protected static final String PASSWORD;
+ protected static final String MAIL;
+ protected static final String PAYLOAD_URL;
+ protected static final String SECRET;
+ protected static final String PHONE;
+ static {
+ ResourceBundle bundle = ResourceBundle.getBundle("test_config");
+ USERNAME = bundle.getString("username");
+ PASSWORD = bundle.getString("password");
+ MAIL = bundle.getString("mail");
+ PAYLOAD_URL = bundle.getString("dingtalkurl");
+ SECRET = bundle.getString("dingtalksecret");
+ PHONE = bundle.getString("telephone");
+ }
}
diff --git a/src/test/java/org/gcszhn/autocard/service/AutoCardServiceTest.java b/src/test/java/org/gcszhn/autocard/service/AutoCardServiceTest.java
index ea1af10..250398b 100644
--- a/src/test/java/org/gcszhn/autocard/service/AutoCardServiceTest.java
+++ b/src/test/java/org/gcszhn/autocard/service/AutoCardServiceTest.java
@@ -15,13 +15,19 @@
*/
package org.gcszhn.autocard.service;
+import net.sourceforge.tess4j.TesseractException;
import org.gcszhn.autocard.AppTest;
+import org.gcszhn.autocard.utils.ImageUtils;
+import org.gcszhn.autocard.utils.OCRUtils;
import org.gcszhn.autocard.utils.StatusCode;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
+import java.awt.image.BufferedImage;
+import java.io.File;
+
/**
* 打卡服务测试
* @author Zhang.H.N
@@ -41,7 +47,8 @@ public void afterTest() {
public void getPageTest() {
try {
for (int i = 0; i < 2; i++) {
- String page = autoCardService.getPage(USERNAME, PASSWORD);
+ autoCardService.login(USERNAME, PASSWORD);
+ String page = autoCardService.getPage();
Assert.assertNotNull(page);
Assert.assertTrue(autoCardService.formValidation(page));
}
@@ -51,11 +58,22 @@ public void getPageTest() {
}
@Test
public void getOldInfoTest() {
- System.out.println(autoCardService.getOldInfo(USERNAME, PASSWORD));
+ autoCardService.login(USERNAME, PASSWORD);
+ System.out.println(autoCardService.getOldInfo());
}
@Test
public void submitReportTest() {
StatusCode statusCode = autoCardService.submit(USERNAME, PASSWORD);
Assert.assertNotEquals(statusCode.getStatus(), -1);
}
+ @Test
+ public void validCodeTest() throws TesseractException, InterruptedException {
+ autoCardService.login(USERNAME, PASSWORD);
+ BufferedImage image = autoCardService.getCodeImage();
+ Assert.assertNotNull(image);
+ String validCode = autoCardService.getCode(image);
+ if (validCode != null) {
+ ImageUtils.write(image, "png", new File("code/" + validCode + ".png"));
+ }
+ }
}
diff --git a/src/test/java/org/gcszhn/autocard/service/DingTalkHookServiceTest.java b/src/test/java/org/gcszhn/autocard/service/DingTalkHookServiceTest.java
index 398a415..fac6a14 100644
--- a/src/test/java/org/gcszhn/autocard/service/DingTalkHookServiceTest.java
+++ b/src/test/java/org/gcszhn/autocard/service/DingTalkHookServiceTest.java
@@ -36,7 +36,7 @@ public void addSignature() {
@Test
public void sendTextTest() {
- Assert.assertEquals(service.sendText(encrypt_url, "打卡信息获取失败", true).getStatus(), 0);
+ Assert.assertEquals(service.sendText(encrypt_url, "打卡信息获取失败, @"+ PHONE, false, PHONE).getStatus(), 0);
}
@Test
diff --git a/src/test/java/org/gcszhn/autocard/utils/OCRUtilsTest.java b/src/test/java/org/gcszhn/autocard/utils/OCRUtilsTest.java
new file mode 100644
index 0000000..755dff3
--- /dev/null
+++ b/src/test/java/org/gcszhn/autocard/utils/OCRUtilsTest.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright © 2021 Zhang.H.N.
+ *
+ * Licensed under the Apache License, Version 2.0 (thie "License");
+ * You may not use this file except in compliance with the license.
+ * You may obtain a copy of the License at
+ *
+ * http://wwww.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language govering permissions and
+ * limitations under the License.
+ */
+package org.gcszhn.autocard.utils;
+
+import org.gcszhn.autocard.AppTest;
+import org.junit.Test;
+
+public class OCRUtilsTest extends AppTest {
+ @Test
+ public void recongnizeTest() {
+ try {
+ System.out.println(OCRUtils.recognize("code/AENZ.png"));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/test/resources/.gitkeep b/src/test/resources/.gitkeep
deleted file mode 100644
index e69de29..0000000