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