├── readme_images ├── qrcode.jpg ├── evaluation-v2.jpg └── cpdaily-auto-sign-in.jpg ├── .gitattributes ├── src ├── main │ ├── resources │ │ ├── static │ │ │ ├── css │ │ │ │ ├── button │ │ │ │ │ ├── 16148 │ │ │ │ │ ├── demoad.js │ │ │ │ │ ├── C6AILKT.json │ │ │ │ │ ├── normalize.css │ │ │ │ │ ├── demoadpacks.css │ │ │ │ │ ├── vicons-font.css │ │ │ │ │ ├── carbon.js │ │ │ │ │ └── base.css │ │ │ │ ├── Icomoon │ │ │ │ │ ├── icomoon.eot │ │ │ │ │ ├── icomoon.ttf │ │ │ │ │ └── icomoon.woff │ │ │ │ ├── loading.css │ │ │ │ ├── normalize.css │ │ │ │ ├── loading2.css │ │ │ │ ├── semantic-custom.css │ │ │ │ └── default.css │ │ │ ├── fonts │ │ │ │ ├── icomoon.eot │ │ │ │ ├── icomoon.ttf │ │ │ │ ├── icomoon.woff │ │ │ │ └── icomoon.svg │ │ │ ├── js │ │ │ │ └── app.js │ │ │ ├── cpdaily-options-page.html │ │ │ ├── score-notify.html │ │ │ ├── exam-list.html │ │ │ ├── score-list.html │ │ │ ├── cpdaily-del-user.html │ │ │ ├── bind.html │ │ │ └── exam.html │ │ ├── 4803746_www.starix.top.pfx │ │ ├── application-prod.yml │ │ ├── application-dev.yml │ │ ├── python │ │ │ ├── jw_captcha_ocr.py │ │ │ ├── cpdaily_captcha_ocr.py │ │ │ ├── email_util.py │ │ │ ├── encode.js │ │ │ ├── cpdaily_auth.py │ │ │ ├── cpdaily_sign_in.py │ │ │ └── slide_captcha_pass.py │ │ └── application.yml │ └── java │ │ └── com │ │ └── starix │ │ └── gdou │ │ ├── response │ │ ├── IErrorCode.java │ │ ├── ResultCode.java │ │ └── CommonResult.java │ │ ├── dto │ │ ├── response │ │ │ ├── ExamQueryResponseDTO.java │ │ │ ├── YearOptionListResponseDTO.java │ │ │ └── ScoreQueryResponseDTO.java │ │ ├── request │ │ │ ├── ExamQueryRquestDTO.java │ │ │ └── ScoreQueryRquestDTO.java │ │ ├── LoginResultV2.java │ │ ├── LoginResult.java │ │ ├── ScoreNotifyDTO.java │ │ └── WxMessage.java │ │ ├── vo │ │ ├── ExamVO.java │ │ ├── ScoreVO.java │ │ └── QuerySocreRequestVO.java │ │ ├── service │ │ ├── CpdailyUserService.java │ │ ├── UserBindService.java │ │ ├── GdouJWScoreNotifyService.java │ │ ├── impl │ │ │ ├── UserBindServiceImpl.java │ │ │ ├── GdouJWScoreNotifyServiceImpl.java │ │ │ └── CpdailyUserServiceImpl.java │ │ ├── GdouJWServiceV2.java │ │ └── GdouJWService.java │ │ ├── repository │ │ ├── CpdailyUserRepository.java │ │ └── StudentRepository.java │ │ ├── Application.java │ │ ├── exception │ │ ├── CustomException.java │ │ └── BaseExceptionHandler.java │ │ ├── utils │ │ ├── HtmlMailRenderUtil.java │ │ ├── RSAUtil.java │ │ ├── MailUtil.java │ │ ├── WxMessagePushUtil.java │ │ ├── SslUtil.java │ │ └── HttpClientUtil.java │ │ ├── entity │ │ ├── Student.java │ │ └── CpdailyUser.java │ │ ├── common │ │ └── Constant.java │ │ ├── task │ │ ├── ScoreNotifySchedule.java │ │ ├── CpdailyAutoSignInSchedule.java │ │ ├── CpdailySignInAsyncTask.java │ │ └── ScoreNotifyAsyncTask.java │ │ ├── config │ │ ├── AsyncConfig.java │ │ ├── RedisConfig.java │ │ └── ConnectorConfig.java │ │ └── controller │ │ ├── GdouJWScoreNotifyController.java │ │ ├── CpdailyController.java │ │ ├── GdouJWUserBindController.java │ │ ├── GdouJWController.java │ │ └── GdouJWControllerV2.java └── test │ └── java │ └── com │ └── starix │ └── gdou │ ├── WxPushTest.java │ └── EmailTest.java ├── .gitignore ├── README.md └── pom.xml /readme_images/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Starix610/gdou-tools/HEAD/readme_images/qrcode.jpg -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-language=java 2 | *.css linguist-language=java 3 | *.html linguist-language=java 4 | -------------------------------------------------------------------------------- /readme_images/evaluation-v2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Starix610/gdou-tools/HEAD/readme_images/evaluation-v2.jpg -------------------------------------------------------------------------------- /readme_images/cpdaily-auto-sign-in.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Starix610/gdou-tools/HEAD/readme_images/cpdaily-auto-sign-in.jpg -------------------------------------------------------------------------------- /src/main/resources/static/css/button/16148: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Starix610/gdou-tools/HEAD/src/main/resources/static/css/button/16148 -------------------------------------------------------------------------------- /src/main/resources/static/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Starix610/gdou-tools/HEAD/src/main/resources/static/fonts/icomoon.eot -------------------------------------------------------------------------------- /src/main/resources/static/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Starix610/gdou-tools/HEAD/src/main/resources/static/fonts/icomoon.ttf -------------------------------------------------------------------------------- /src/main/resources/4803746_www.starix.top.pfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Starix610/gdou-tools/HEAD/src/main/resources/4803746_www.starix.top.pfx -------------------------------------------------------------------------------- /src/main/resources/static/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Starix610/gdou-tools/HEAD/src/main/resources/static/fonts/icomoon.woff -------------------------------------------------------------------------------- /src/main/resources/static/css/Icomoon/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Starix610/gdou-tools/HEAD/src/main/resources/static/css/Icomoon/icomoon.eot -------------------------------------------------------------------------------- /src/main/resources/static/css/Icomoon/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Starix610/gdou-tools/HEAD/src/main/resources/static/css/Icomoon/icomoon.ttf -------------------------------------------------------------------------------- /src/main/resources/static/css/Icomoon/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Starix610/gdou-tools/HEAD/src/main/resources/static/css/Icomoon/icomoon.woff -------------------------------------------------------------------------------- /src/main/resources/application-prod.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: prod 3 | 4 | python: 5 | # python脚本绝对路径 6 | path: /opt/server/gdou-jw-tools/pyhton/ 7 | name: slide_captcha_pass.py 8 | # python命令行输出的字符编码,在不同环境下运行需要进行不同的设置:Windows下为GBK,Linux下为UTF-8 9 | output-encoding: UTF-8 -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/response/IErrorCode.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.response; 2 | 3 | /** 4 | * @author Starix 5 | * @date 2019-09-28 6 | * @description: 7 | */ 8 | public interface IErrorCode { 9 | 10 | Integer getCode(); 11 | 12 | String getMessage(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: dev 3 | 4 | python: 5 | # python脚本路径 6 | path: D:\IdeaProjects\project\gdou-jw-tools\src\main\resources\python\ 7 | name: slide_captcha_pass.py 8 | # python命令行输出的字符编码,在不同环境下运行需要进行不同的设置:Windows下为GBK,Linux下为UTF-8 9 | output-encoding: GBK -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/dto/response/ExamQueryResponseDTO.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 Meituan 2 | // All rights reserved 3 | package com.starix.gdou.dto.response; 4 | 5 | import lombok.Data; 6 | 7 | /** 8 | * @author shiwenjie 9 | * @created 2020/7/1 2:45 下午 10 | **/ 11 | @Data 12 | public class ExamQueryResponseDTO { 13 | 14 | 15 | 16 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/vo/ExamVO.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.vo; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author Starix 7 | * @date 2019-11-22 16:06 8 | */ 9 | @Data 10 | public class ExamVO { 11 | 12 | private String courseName; 13 | 14 | private String stuName; 15 | 16 | private String examTime; 17 | 18 | private String examPlace; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/vo/ScoreVO.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.vo; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author Starix 7 | * @date 2019-11-20 19:54 8 | */ 9 | 10 | @Data 11 | public class ScoreVO { 12 | 13 | private String courseName; 14 | 15 | private String credit; 16 | 17 | private String gradePoint; 18 | 19 | private String score; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/vo/QuerySocreRequestVO.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.vo; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author Starix 7 | * @date 2019-11-20 20:54 8 | */ 9 | @Data 10 | public class QuerySocreRequestVO { 11 | 12 | //学号 13 | private String xh; 14 | 15 | //密码 16 | private String password; 17 | 18 | //学年 19 | private String year; 20 | 21 | //学期 22 | private String semester; 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/service/CpdailyUserService.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.service; 2 | 3 | import com.starix.gdou.entity.CpdailyUser; 4 | 5 | /** 6 | * @author Starix 7 | * @date 2020-04-06 13:27 8 | */ 9 | public interface CpdailyUserService { 10 | 11 | void saveUser(CpdailyUser user) throws Exception; 12 | 13 | void deleteUser(String username, String password) throws Exception; 14 | 15 | void updateUser(CpdailyUser user) throws Exception; 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /src/main/resources/python/jw_captcha_ocr.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from school_api.check_code import CHECK_CODE 3 | 4 | 5 | def getCode(): 6 | response = requests.get('http://210.38.137.77:8016/CheckCode.aspx') 7 | code = CHECK_CODE.verify(response.content) 8 | cookies = requests.utils.dict_from_cookiejar(response.cookies) 9 | # 输出code和cookie,给Java使用 10 | print(code) 11 | print(cookies['ASP.NET_SessionId']) 12 | 13 | 14 | if __name__ == '__main__': 15 | getCode() -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/dto/request/ExamQueryRquestDTO.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.dto.request; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | import org.apache.http.cookie.Cookie; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author shiwenjie 11 | * @created 2020/7/1 2:38 下午 12 | **/ 13 | @Data 14 | @Builder 15 | public class ExamQueryRquestDTO { 16 | 17 | private List cookies; 18 | 19 | private String year; 20 | 21 | private String semester; 22 | 23 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/dto/request/ScoreQueryRquestDTO.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.dto.request; 2 | 3 | import lombok.Builder; 4 | import lombok.Data; 5 | import org.apache.http.cookie.Cookie; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author shiwenjie 11 | * @created 2020/7/1 2:38 下午 12 | **/ 13 | @Data 14 | @Builder 15 | public class ScoreQueryRquestDTO { 16 | 17 | private List cookies; 18 | 19 | private String year; 20 | 21 | private String semester; 22 | 23 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/repository/CpdailyUserRepository.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.repository; 2 | 3 | import com.starix.gdou.entity.CpdailyUser; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | /** 7 | * @author Starix 8 | * @date 2020-04-06 12:14 9 | */ 10 | public interface CpdailyUserRepository extends JpaRepository { 11 | 12 | CpdailyUser findByUsername(String username); 13 | 14 | void deleteByUsername(String username); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/service/UserBindService.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.service; 2 | 3 | /** 4 | * @author Starix 5 | * @date 2019-11-23 16:43 6 | */ 7 | public interface UserBindService { 8 | 9 | /** 10 | * 根据openid判断当前用户是否已经绑定过学号 11 | * @param openid openid 12 | * @return 已绑定返回true,否则返回false 13 | */ 14 | boolean isBinding(String openid); 15 | 16 | void bind(String openid, String xh, String password); 17 | 18 | String queryUsernameByOpenid(String openid); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/repository/StudentRepository.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.repository; 2 | 3 | import com.starix.gdou.entity.Student; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author Starix 10 | * @date 2019-11-23 20:16 11 | */ 12 | public interface StudentRepository extends JpaRepository { 13 | 14 | Student findByOpenid(String openid); 15 | 16 | List findAllByNotifyStatus(int notifyStatus); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/service/GdouJWScoreNotifyService.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.service; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * @author Starix 7 | * @date 2020-07-17 21:59 8 | */ 9 | public interface GdouJWScoreNotifyService { 10 | 11 | void enableNotify(String openid, String email) throws Exception; 12 | 13 | void disableNotify(String openid); 14 | 15 | Map queryNotifyStatus(String openid); 16 | 17 | // 查询自定义的年份学期,不使用官网当前默认的年份学期 18 | String queryNotifyYearSemester(); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/Application.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | 7 | /** 8 | * @author Starix 9 | * @date 2019-11-18 18:55 10 | */ 11 | 12 | @SpringBootApplication 13 | @EnableScheduling 14 | public class Application { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(Application.class, args); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/dto/LoginResultV2.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 Meituan 2 | // All rights reserved 3 | package com.starix.gdou.dto; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import org.apache.http.cookie.Cookie; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * v2版本: 13 | * 分装登录后的cookie以及用户名信息,用于维持教务系统登录态,无需每次请求都要重新登录 14 | * 15 | * @author shiwenjie 16 | * @created 2020/7/1 4:02 下午 17 | **/ 18 | @Data 19 | @AllArgsConstructor 20 | public class LoginResultV2 { 21 | 22 | private List cookies; 23 | 24 | private String username; 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/exception/CustomException.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.exception; 2 | 3 | 4 | import com.starix.gdou.response.CommonResult; 5 | 6 | /** 7 | * 自定义业务异常类 8 | */ 9 | public class CustomException extends RuntimeException { 10 | 11 | private CommonResult commonResult; 12 | 13 | public CustomException(CommonResult commonResult) { 14 | this.commonResult = commonResult; 15 | } 16 | 17 | public CommonResult getCommonResult() { 18 | return commonResult; 19 | } 20 | 21 | public void setCommonResult(CommonResult commonResult) { 22 | this.commonResult = commonResult; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/utils/HtmlMailRenderUtil.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.utils; 2 | 3 | import com.jfinal.kit.Kv; 4 | import com.jfinal.template.Engine; 5 | import com.jfinal.template.Template; 6 | 7 | /** 8 | * @author Starix 9 | * @date 2020-07-18 16:31 10 | */ 11 | public class HtmlMailRenderUtil { 12 | 13 | public static String render(String templatePath, Kv data){ 14 | Engine engine = Engine.use(); 15 | engine.setDevMode(true); 16 | engine.setToClassPathSourceFactory(); 17 | Template template = engine.getTemplate(templatePath); 18 | return template.renderToString(data); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/static/js/app.js: -------------------------------------------------------------------------------- 1 | // var BASE_URL = "https://localhost:9800" 2 | // var BASE_URL = "http://localhost:9801" 3 | // var BASE_URL = "https://139.196.102.109:9800" 4 | var BASE_URL = "https://www.starix.top:9800" 5 | 6 | // 自动签到功能页使用百度地图api,页面用https的话会导致不能正常使用定位,所以这个页面使用http方式来访问 7 | var BASE_HTTP_URL = "http://www.starix.top:9801" 8 | 9 | 10 | function getQueryVariable(variable) { 11 | var query = window.location.search.substring(1); 12 | var vars = query.split("&"); 13 | for (var i=0;i uids; 29 | 30 | private List topicIds; 31 | 32 | private Integer contentType; 33 | 34 | private String content; 35 | 36 | private String summary; 37 | /** 38 | * 消息附带的url,仅针对text消息类型有效 39 | */ 40 | private String url; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/resources/static/css/loading.css: -------------------------------------------------------------------------------- 1 | .loading{ 2 | /*width: 80px;*/ 3 | height: 40px; 4 | /*margin: 0 auto;*/ 5 | margin-top:100px; 6 | text-align: center; 7 | } 8 | .loading span{ 9 | display: inline-block; 10 | width: 8px; 11 | height: 100%; 12 | border-radius: 4px; 13 | background: lightgreen; 14 | -webkit-animation: load 1.04s ease infinite; 15 | } 16 | @-webkit-keyframes load{ 17 | 0%,100%{ 18 | height: 40px; 19 | background: lightgreen; 20 | } 21 | 50%{ 22 | height: 60px; 23 | margin-top: -20px; 24 | background: lightblue; 25 | } 26 | } 27 | .loading span:nth-child(2){ 28 | -webkit-animation-delay:0.13s; 29 | } 30 | .loading span:nth-child(3){ 31 | -webkit-animation-delay:0.26s; 32 | } 33 | .loading span:nth-child(4){ 34 | -webkit-animation-delay:0.39s; 35 | } 36 | .loading span:nth-child(5){ 37 | -webkit-animation-delay:0.52s; 38 | } -------------------------------------------------------------------------------- /src/main/resources/python/email_util.py: -------------------------------------------------------------------------------- 1 | import smtplib 2 | import time 3 | 4 | from email.mime.text import MIMEText 5 | 6 | # 发送方邮箱 7 | msg_from = 'starix610@163.com' 8 | # 发送方邮箱授权码 9 | password = 'xxxx' 10 | 11 | 12 | def send_email(msg_to, subject, content): 13 | subject = subject # 主题 14 | content = content 15 | # 生成一个MIMEText对象(还有一些其它参数) 16 | msg = MIMEText(content) 17 | # 放入邮件主题 18 | msg['Subject'] = subject 19 | # 也可以这样传参 20 | # msg['Subject'] = Header(subject, 'utf-8') 21 | # 放入发件人 22 | msg['From'] = msg_from 23 | try: 24 | # 通过ssl方式发送,服务器地址,端口 25 | s = smtplib.SMTP_SSL("smtp.163.com", 465) 26 | # 登录到邮箱 27 | s.login(msg_from, password) 28 | # 发送邮件:发送方,收件方,要发送的消息 29 | s.sendmail(msg_from, msg_to, msg.as_string()) 30 | print('result-邮件发送成功') 31 | except s.SMTPException as e: 32 | print('result-邮件发送失败:%s' % e) 33 | finally: 34 | s.quit() 35 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/task/ScoreNotifySchedule.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.task; 2 | 3 | import com.starix.gdou.utils.WxMessagePushUtil; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.scheduling.annotation.Scheduled; 7 | import org.springframework.stereotype.Component; 8 | 9 | import static com.starix.gdou.common.Constant.WX_PUSH_TOKEN_MAIN_LOG; 10 | 11 | /** 12 | * 2020.08.07 关闭成绩更新检测 13 | * @author Starix 14 | * @date 2020-07-18 12:14 15 | */ 16 | @Slf4j 17 | @Component 18 | public class ScoreNotifySchedule { 19 | 20 | @Autowired 21 | private ScoreNotifyAsyncTask scoreNotifyAsyncTask; 22 | 23 | // 定时检测成绩是否更新,更新则发送邮件通知 24 | @Scheduled(cron = "0 0 8,20 * * ?") 25 | public void signIntask() throws Exception { 26 | log.info("==============开始执行成绩更新检测任务=============="); 27 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, "开始执行成绩更新检测任务"); 28 | scoreNotifyAsyncTask.checkScoreUpdateAndNotify(); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/resources/static/css/button/demoad.js: -------------------------------------------------------------------------------- 1 | var filename='http://tympanus.net/codrops/adpacks/demoadpacks.css?' + new Date().getTime(); 2 | var fileref=document.createElement("link"); 3 | fileref.setAttribute("rel", "stylesheet"); 4 | fileref.setAttribute("type", "text/css"); 5 | fileref.setAttribute("href", filename); 6 | document.getElementsByTagName("head")[0].appendChild(fileref); 7 | 8 | var demoad = document.createElement('div'); 9 | demoad.id = 'cdawrap'; 10 | demoad.innerHTML = ''; 11 | document.getElementsByTagName('body')[0].appendChild(demoad); 12 | 13 | document.getElementById('cda-remove').addEventListener('click',function(e){ 14 | demoad.style.display = 'none'; 15 | e.preventDefault(); 16 | }); 17 | 18 | var bsa = document.createElement('script'); 19 | bsa.type = 'text/javascript'; 20 | bsa.async = true; 21 | bsa.id = '_carbonads_js'; 22 | bsa.src = '//cdn.carbonads.com/carbon.js?zoneid=1673&serve=C6AILKT&placement=codrops'; 23 | demoad.appendChild(bsa); 24 | 25 | if(window._gaq) { 26 | _gaq.push(['_trackEvent', 'Codrops Demo Ad', 'Carbon ad included', '1']); 27 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/dto/response/YearOptionListResponseDTO.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 Meituan 2 | // All rights reserved 3 | package com.starix.gdou.dto.response; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * @author shiwenjie03 14 | * @created 2020/7/7 7:28 下午 15 | **/ 16 | @Data 17 | @AllArgsConstructor 18 | @NoArgsConstructor 19 | @Builder 20 | public class YearOptionListResponseDTO { 21 | 22 | /** 23 | * 学年列表数据value 24 | */ 25 | private List yearValueList; 26 | 27 | /** 28 | * 学年列表数据text 29 | */ 30 | private List yearTextList; 31 | 32 | /** 33 | * 学期列表数据value 34 | */ 35 | private List semesterValueList; 36 | 37 | /** 38 | * 学期列表数据text 39 | */ 40 | private List semesterTextList; 41 | 42 | /** 43 | * 当前选中学年的索引 44 | */ 45 | private int selectedYear; 46 | 47 | /** 48 | * 当前选中学期的索引 49 | */ 50 | private int selectedSemester; 51 | 52 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/utils/RSAUtil.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.utils; 2 | 3 | import javax.crypto.Cipher; 4 | import java.math.BigInteger; 5 | import java.security.KeyFactory; 6 | import java.security.PublicKey; 7 | import java.security.spec.RSAPublicKeySpec; 8 | import java.util.Base64; 9 | 10 | /** 11 | * @author Starix 12 | * @date 2020-07-05 22:55 13 | */ 14 | public class RSAUtil { 15 | 16 | public static String encrypt(String modules, String exponent, String str) throws Exception { 17 | byte[] bModules = Base64.getDecoder().decode(modules); 18 | byte[] bExponent = Base64.getDecoder().decode(exponent); 19 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 20 | RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(new BigInteger(bModules), new BigInteger(bExponent)); 21 | PublicKey publicKey = keyFactory.generatePublic(publicKeySpec); 22 | Cipher cipher = Cipher.getInstance("RSA"); 23 | cipher.init(Cipher.ENCRYPT_MODE,publicKey); 24 | byte[] encryptData = cipher.doFinal(str.getBytes()); 25 | // rsa加密结果再进行base64加密 26 | return Base64.getEncoder().encodeToString(encryptData); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/static/css/button/C6AILKT.json: -------------------------------------------------------------------------------- 1 | _carbonads_go({"ads":[{"startingdate":"1420214400","org_zoneid":"1673","marketplace_zoneid":"1297933","link":"http://carbonads.net","styles":"","org_bannerid":"14319","statimp":"http://srv.buysellads.com/ads/imp/x/GTND423LCAADV2QYCKY4YKQWCVSIE2JWFTYILZ3JCE7D553MCE7DP2QKC6BD627YCESDVK3EHJWNBADLKM7UCBZG2Y","zonekey":"C6AILKT","marketplace_siteid":"246311","image_constraint":"130x100","org_creativeid":"19596","bannerid":"18931","i":"1427849064","image":"https://assets.servedby-buysellads.com/p/manage/asset/id/16148","showbsalink":"0","description":"We connect highly qualified audiences with highly relevant services, products, and brands.","description_constraint":"150","image_options":"gif,png,jpg,jpeg","zoneid":"1990","endingdate":"1443623400","timestamp":"1427849064","customcriterion":"","width":"130","extendo_0":"1","evenodd":"0","creativeid":"26287","statlink":"http://srv.buysellads.com/ads/click/x/GTND423LCAADV2QYCKY4YKQWCVSIE2JWFTYILZ3JCE7D553MCE7DP2QKC6BD627YCESDVK3EHJNCLSIZ","buynow_url":"http://buysellads.com/buy/detail/246311/zone/1297933?utm_source=site_246311&utm_medium=website&utm_campaign=imagetext&utm_content=zone_1297933","height":"100","active":1},{}]}); -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/task/CpdailyAutoSignInSchedule.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.task; 2 | 3 | import com.starix.gdou.entity.CpdailyUser; 4 | import com.starix.gdou.repository.CpdailyUserRepository; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.scheduling.annotation.Scheduled; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.List; 11 | 12 | 13 | /** 14 | * TODO 停止使用 15 | * @author Starix 16 | * @date 2020-04-06 1:00 17 | */ 18 | @Slf4j 19 | // @Component 20 | public class CpdailyAutoSignInSchedule { 21 | 22 | @Autowired 23 | private CpdailySignInAsyncTask signInAsyncTask; 24 | @Autowired 25 | private CpdailyUserRepository userRepository; 26 | 27 | //定时为已记录的账号进行签到,每天中午12点执行一次 28 | // @Scheduled(fixedRate = 1000*60*60*24) 29 | // @Scheduled(cron = "0 0 12 * * ?") 30 | public void signIntask() throws Exception { 31 | log.info("==============开始执行签到=============="); 32 | List userList = userRepository.findAll(); 33 | for (CpdailyUser cpdailyUser : userList) { 34 | //异步任务签到 35 | signInAsyncTask.doSignIn(cpdailyUser); 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/config/AsyncConfig.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.core.task.TaskExecutor; 6 | import org.springframework.scheduling.annotation.EnableAsync; 7 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 8 | 9 | import java.util.concurrent.ThreadPoolExecutor; 10 | 11 | /** 12 | * @author Starix 13 | * @date 2019-09-11 23:11 14 | */ 15 | @Configuration 16 | @EnableAsync 17 | public class AsyncConfig { 18 | 19 | @Bean 20 | public TaskExecutor taskExecutor() { 21 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 22 | executor.setCorePoolSize(10); // 设置核心线程数 23 | executor.setMaxPoolSize(20); // 设置最大线程数 24 | executor.setQueueCapacity(100) ; // 设置队列容量 25 | executor.setKeepAliveSeconds(60); // 设置线程活跃时间(秒) 26 | executor.setThreadNamePrefix("async-task-"); // 设置默认线程名称 27 | executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 设置拒绝策略 28 | executor.setWaitForTasksToCompleteOnShutdown(true); // 等待所有任务结束后再关闭线程池 29 | return executor; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.redis.connection.RedisConnectionFactory; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; 8 | import org.springframework.data.redis.serializer.StringRedisSerializer; 9 | 10 | 11 | /** 12 | * @author Tobu 13 | * @date 2019/7/3 19:22 14 | */ 15 | @Configuration 16 | public class RedisConfig { 17 | 18 | /** 19 | * 自定义RedisTemplate的序列化方式 20 | */ 21 | @Bean(name = "redisTemplate") 22 | public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { 23 | RedisTemplate template = new RedisTemplate<>(); 24 | //key使用String形式序列化 25 | template.setKeySerializer(new StringRedisSerializer()); 26 | template.setHashKeySerializer(new StringRedisSerializer()); 27 | 28 | 29 | //value使用Json形式序列化 30 | template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); 31 | template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); 32 | template.setConnectionFactory(redisConnectionFactory); 33 | return template; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GDOU校园助手V2.0 2 | 3 | ### 介绍 4 | 一个提供GDOU校园内网站便捷小功能的项目,功能大多都是基于Python和Java写的爬虫实现的。 5 | 6 | ### 主要技术栈: 7 | + 前端:JQuery、HTML、CSS、VUE 8 | + 后台:SpringBoot、SpringMVC、JPA 9 | + 数据库:MySQL 10 | + Java爬虫:Jsoup、HttpClient 11 | + Python爬虫:requests、BeautifulSoup 12 | + 其它主要Python库:OpenCV、pytesseract、Pillow、pyDes、rsa、fastapi、uvicorn、base64等 13 | 14 | ### 目前拥有的功能: 15 | + 成绩查询 16 | + 考试查询 17 | + 学号绑定 18 | + 一键评价(V2.0完全使用Python实现:独立项目地址) 19 | + 自动抢课(已停止维护,不再使用) 20 | + 今日校园自动签到(已下线) 21 | 22 | ### 部分预览: 23 |
24 | image 25 | image 26 | image 27 |  image 28 |  image 29 |
30 | 31 | ### 功能已经集成到我的个人微信公众号上面: 32 |
33 | image 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /src/test/java/com/starix/gdou/WxPushTest.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.starix.gdou.common.Constant; 5 | import com.starix.gdou.dto.WxMessage; 6 | import com.starix.gdou.utils.HttpClientUtil; 7 | import com.starix.gdou.utils.WxMessagePushUtil; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import java.io.IOException; 14 | import java.util.Arrays; 15 | 16 | /** 17 | * @author Starix 18 | * @date 2020-07-19 23:52 19 | */ 20 | @SpringBootTest 21 | @RunWith(SpringRunner.class) 22 | public class WxPushTest { 23 | 24 | @Test 25 | public void testPushWxMessage() throws IOException { 26 | HttpClientUtil httpClient = new HttpClientUtil(); 27 | WxMessage wxMessage = WxMessage.builder() 28 | .appToken("AT_ZDSHlFh0W3wGpBl7wGn1owM2somLTTJO") 29 | .uids(Arrays.asList("UID_RrJlsX3ns3sncf2ZI58UyhliOq9f")) 30 | .contentType(WxMessage.CONTENT_TYPE_TEXT) 31 | .content("测试发送") 32 | .build(); 33 | httpClient.doPost("http://wxpusher.zjiecode.com/api/send/message", JSONObject.toJSONString(wxMessage)); 34 | } 35 | 36 | @Test 37 | public void testPushWxMessageUtil() { 38 | WxMessagePushUtil.push(Constant.WX_PUSH_TOKEN_MAIN_LOG, "测试来了"); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/entity/CpdailyUser.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.entity; 2 | 3 | import lombok.Data; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.validation.constraints.Email; 10 | import javax.validation.constraints.NotBlank; 11 | import javax.validation.constraints.Pattern; 12 | import java.util.Date; 13 | 14 | /** 15 | * (CpdailyUser)表实体类 16 | * 17 | * @author Starix 18 | * @since 2020-04-06 12:11:41 19 | */ 20 | @SuppressWarnings("serial") 21 | @Data 22 | @Entity 23 | public class CpdailyUser { 24 | 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.IDENTITY) 27 | private Integer id; 28 | //用户名(学号) 29 | @NotBlank(message = "用户名不能为空") 30 | private String username; 31 | //密码 32 | @NotBlank(message = "密码不能为空") 33 | private String password; 34 | //经度 35 | @NotBlank(message = "经度不能为空") 36 | @Pattern(regexp = "^[0-9]+\\.[0-9]{6}$", message = "经度数据格式有误") 37 | private String longitude; 38 | //纬度 39 | @NotBlank(message = "纬度不能为空") 40 | @Pattern(regexp = "^[0-9]+\\.[0-9]{6}$", message = "纬度数据格式有误") 41 | private String latitude; 42 | //签到内容 43 | @NotBlank(message = "签到内容不能为空") 44 | private String abnormalreason; 45 | //地理位置 46 | @NotBlank(message = "地理位置不能为空") 47 | private String position; 48 | //邮箱 49 | @NotBlank(message = "邮箱不能为空") 50 | @Email(message = "邮箱格式有误") 51 | private String email; 52 | //创建时间 53 | private Date createTime; 54 | //更新时间 55 | private Date updateTime; 56 | 57 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/utils/MailUtil.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.utils; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.mail.javamail.JavaMailSender; 5 | import org.springframework.mail.javamail.MimeMessageHelper; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.mail.internet.MimeMessage; 9 | 10 | /** 11 | * @author Starix 12 | * @date 2020-07-18 16:26 13 | */ 14 | @Component 15 | public class MailUtil { 16 | 17 | @Autowired 18 | private JavaMailSender javaMailSender; 19 | 20 | public void sendMail(String receiver, String subject, String content) throws Exception { 21 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 22 | MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true); 23 | messageHelper.setFrom("在思考的猫"); 24 | messageHelper.setTo(receiver); 25 | messageHelper.setSubject(subject); 26 | messageHelper.setText(content, false); 27 | SslUtil.ignoreSsl(); 28 | javaMailSender.send(mimeMessage); 29 | } 30 | 31 | public void sendHtmlMail(String receiver, String subject, String html) throws Exception { 32 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 33 | MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true); 34 | messageHelper.setFrom("在思考的猫"); 35 | messageHelper.setTo(receiver); 36 | messageHelper.setSubject(subject); 37 | messageHelper.setText(html, true); 38 | SslUtil.ignoreSsl(); 39 | javaMailSender.send(mimeMessage); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/service/impl/UserBindServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.service.impl; 2 | 3 | import com.starix.gdou.entity.Student; 4 | import com.starix.gdou.exception.CustomException; 5 | import com.starix.gdou.repository.StudentRepository; 6 | import com.starix.gdou.response.CommonResult; 7 | import com.starix.gdou.service.UserBindService; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.Date; 12 | 13 | /** 14 | * @author Starix 15 | * @date 2019-11-23 16:43 16 | */ 17 | @Service 18 | public class UserBindServiceImpl implements UserBindService { 19 | 20 | @Autowired 21 | private StudentRepository studentRepository; 22 | 23 | @Override 24 | public boolean isBinding(String openid) { 25 | 26 | Student student = studentRepository.findByOpenid(openid); 27 | 28 | return student != null; 29 | } 30 | 31 | @Override 32 | public void bind(String openid, String xh, String password) { 33 | if (isBinding(openid)){ 34 | throw new CustomException(CommonResult.failed("你已经绑定过学号啦")); 35 | } 36 | Student student = new Student(); 37 | student.setOpenid(openid); 38 | student.setUsername(xh); 39 | student.setPassword(password); 40 | student.setNotifyStatus(0); 41 | student.setCreateTime(new Date()); 42 | studentRepository.save(student); 43 | } 44 | 45 | @Override 46 | public String queryUsernameByOpenid(String openid) { 47 | Student student = studentRepository.findByOpenid(openid); 48 | if (student != null){ 49 | return student.getUsername(); 50 | } 51 | return null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/resources/static/css/normalize.css: -------------------------------------------------------------------------------- 1 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;}audio,canvas,video{display:inline-block;}audio:not([controls]){display:none;height:0;}[hidden]{display:none;}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}body{margin:0;}a:focus{outline:thin dotted;}a:active,a:hover{outline:0;}h1{font-size:2em;margin:0.67em 0;}abbr[title]{border-bottom:1px dotted;}b,strong{font-weight:bold;}dfn{font-style:italic;}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0;}mark{background:#ff0;color:#000;}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em;}pre{white-space:pre-wrap;}q{quotes:"\201C" "\201D" "\2018" "\2019";}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{top:-0.5em;}sub{bottom:-0.25em;}img{border:0;}svg:not(:root){overflow:hidden;}figure{margin:0;}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;}legend{border:0;padding:0;}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;}button,input{line-height:normal;}button,select{text-transform:none;}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;}button[disabled],html input[disabled]{cursor:default;}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}textarea{overflow:auto;vertical-align:top;}table{border-collapse:collapse;border-spacing:0;} -------------------------------------------------------------------------------- /src/main/resources/static/css/button/normalize.css: -------------------------------------------------------------------------------- 1 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;}audio,canvas,video{display:inline-block;}audio:not([controls]){display:none;height:0;}[hidden]{display:none;}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}body{margin:0;}a:focus{outline:thin dotted;}a:active,a:hover{outline:0;}h1{font-size:2em;margin:0.67em 0;}abbr[title]{border-bottom:1px dotted;}b,strong{font-weight:bold;}dfn{font-style:italic;}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0;}mark{background:#ff0;color:#000;}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em;}pre{white-space:pre-wrap;}q{quotes:"\201C" "\201D" "\2018" "\2019";}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{top:-0.5em;}sub{bottom:-0.25em;}img{border:0;}svg:not(:root){overflow:hidden;}figure{margin:0;}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;}legend{border:0;padding:0;}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;}button,input{line-height:normal;}button,select{text-transform:none;}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;}button[disabled],html input[disabled]{cursor:default;}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}textarea{overflow:auto;vertical-align:top;}table{border-collapse:collapse;border-spacing:0;} -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | profiles: 3 | active: prod 4 | datasource: 5 | driver-class-name: com.mysql.cj.jdbc.Driver 6 | # schema: classpath:db/schema-h2.sql 7 | # data: classpath:db/data-h2.sql 8 | url: jdbc:mysql://xxxx:3306/weixin_mp?characterEncoding=UTF-8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai 9 | username: root 10 | password: xxxx 11 | druid: 12 | initial-size: 5 #连接池初始化大小 13 | min-idle: 10 #最小空闲连接数 14 | max-active: 20 #最大连接数 15 | web-stat-filter: 16 | exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" #不统计这些请求数据 17 | stat-view-servlet: #访问监控网页的登录用户名和密码 18 | login-username: druid 19 | login-password: druid 20 | # Redis 21 | redis: 22 | database: 0 23 | host: xxxx 24 | # host: 127.0.0.1 25 | port: 6379 26 | password: xxxx 27 | timeout: 60000ms #毫秒 28 | #springboot2.x 默认使用lettuce作为redis客户端工具 29 | lettuce: 30 | pool: 31 | # 连接池最大连接数(使用负值表示没有限制) 默认 8 32 | max-active: 200 33 | # 连接池中的最大空闲连接 默认 8 34 | max-idle: 100 35 | # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 36 | max-wait: 60s 37 | # 连接池中的最小空闲连接 默认 0 38 | min-idle: 50 39 | mail: 40 | host: smtp.163.com 41 | port: 465 42 | username: starix610@163.com 43 | password: xxxx 44 | protocol: smtp 45 | properties: 46 | mail.smtp.ssl.enable: true 47 | 48 | 49 | 50 | server: 51 | # 这个用作https端口 52 | port: 9800 53 | #这个字段属于自定义的,用于http端口 54 | custom: 55 | httpPort: 9801 56 | ssl: 57 | key-store: classpath:4803746_www.starix.top.pfx 58 | key-store-password: xxxx 59 | key-store-type: PKCS12 60 | 61 | 62 | gdou: 63 | # jw-url: http://210.38.137.77:8016 64 | # webvpn-url: https://webvpn.gdou.edu.cn 65 | jw-url: https://jw.gdou.edu.cn 66 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/dto/response/ScoreQueryResponseDTO.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 Meituan 2 | // All rights reserved 3 | package com.starix.gdou.dto.response; 4 | 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import org.springframework.util.StringUtils; 8 | 9 | /** 10 | * @author shiwenjie 11 | * @created 2020/7/1 2:45 下午 12 | **/ 13 | @Data 14 | @Builder 15 | public class ScoreQueryResponseDTO { 16 | 17 | /** 18 | * 课程名称 19 | */ 20 | private String courseName; 21 | 22 | /** 23 | * 学分 24 | */ 25 | private String credit; 26 | 27 | /** 28 | * 成绩 29 | */ 30 | private String score; 31 | 32 | /** 33 | * 绩点 34 | */ 35 | private String gpa; 36 | 37 | /** 38 | * 课程性质 39 | */ 40 | private String property; 41 | 42 | /** 43 | * 课程类别 44 | */ 45 | private String category; 46 | 47 | /** 48 | * 课程归属 49 | */ 50 | private String belongTo; 51 | 52 | 53 | /** 54 | * 重写eauals,用于成绩更新的List判断 55 | * @param obj 56 | * @return 57 | */ 58 | @Override 59 | public boolean equals(Object obj){ 60 | if (this == obj){ 61 | return true; 62 | } 63 | if (obj == null){ 64 | return false; 65 | } 66 | if (getClass() != obj.getClass()){ 67 | return false; 68 | } 69 | ScoreQueryResponseDTO other = (ScoreQueryResponseDTO) obj; 70 | if (StringUtils.isEmpty(other.getCourseName())){ 71 | return false; 72 | } 73 | if (StringUtils.isEmpty(courseName)){ 74 | if (!StringUtils.isEmpty(other.getCourseName())){ 75 | return false; 76 | } 77 | }else if (!courseName.equals(other.getCourseName())){ 78 | return false; 79 | } 80 | return true; 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /src/main/resources/static/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/service/GdouJWServiceV2.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.service; 2 | 3 | import com.starix.gdou.dto.LoginResultV2; 4 | import com.starix.gdou.dto.request.ExamQueryRquestDTO; 5 | import com.starix.gdou.dto.request.ScoreQueryRquestDTO; 6 | import com.starix.gdou.dto.response.ExamQueryResponseDTO; 7 | import com.starix.gdou.dto.response.ScoreQueryResponseDTO; 8 | import com.starix.gdou.dto.response.YearOptionListResponseDTO; 9 | import org.apache.http.cookie.Cookie; 10 | 11 | import java.io.IOException; 12 | import java.util.List; 13 | 14 | /** 15 | * 教务系统服务service 16 | * v2版本 17 | * 由于v1的service接口不太通用,因此直接构建v2版本的新sercice接口 18 | * @author shiwenjie 19 | * @created 2020/7/1 2:23 下午 20 | **/ 21 | public interface GdouJWServiceV2 { 22 | 23 | 24 | /** 25 | * 执行登录,返回登录成功的cookie,用于维持登录状态,后续进行查询无需再次登录 26 | * @param useranme 学号 27 | * @param password 密码 28 | * @return 29 | */ 30 | LoginResultV2 login(String useranme, String password) throws Exception; 31 | 32 | 33 | /** 34 | * 通过openid查询已绑定的学号信息实现自动登录 35 | * @param openid 36 | * @return 37 | */ 38 | LoginResultV2 loginByOpenid(String openid) throws Exception; 39 | 40 | /** 41 | * 查询成绩信息 42 | * @param scoreQueryRquestDTO 43 | * @return 44 | */ 45 | List queryScore(ScoreQueryRquestDTO scoreQueryRquestDTO) throws IOException; 46 | 47 | /** 48 | * 查询考试信息 49 | * @param examQueryRquestDTO 50 | * @return 51 | */ 52 | ExamQueryResponseDTO queryExam(ExamQueryRquestDTO examQueryRquestDTO); 53 | 54 | /** 55 | * 获得查成绩页面年份下拉列表数据 56 | * @param cookies 57 | * @return 58 | */ 59 | YearOptionListResponseDTO getSocreYearOptionList(List cookies) throws Exception; 60 | 61 | /** 62 | * 获得查考试页面年份下拉列表数据 63 | * @return 64 | */ 65 | List getExamYearOptionList(List cookies) throws Exception; 66 | 67 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/utils/WxMessagePushUtil.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.utils; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.starix.gdou.common.Constant; 5 | import com.starix.gdou.dto.WxMessage; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | import java.io.IOException; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Arrays; 11 | import java.util.Date; 12 | 13 | import static com.starix.gdou.common.Constant.*; 14 | 15 | /** 16 | * @author Starix 17 | * @date 2020-07-21 0:03 18 | */ 19 | @Slf4j 20 | public class WxMessagePushUtil { 21 | 22 | private static final String PUSH_URL = "http://wxpusher.zjiecode.com/api/send/message"; 23 | 24 | public static void push(String token, String message){ 25 | SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 26 | String date = format.format(new Date()) + "\n"; 27 | String split = "-------------------------------\n"; 28 | String content = ""; 29 | switch (token){ 30 | case WX_PUSH_TOKEN_MAIN_LOG: 31 | content = "[业务日志]\n"; 32 | break; 33 | case WX_PUSH_TOKEN_EXCEPTION: 34 | content = "[异常日志]\n"; 35 | break; 36 | case WX_PUSH_TOKEN_MP_MSG: 37 | content = "[公众号消息]\n"; 38 | break; 39 | case WX_PUSH_TOKEN_SUBSCRIBE: 40 | // content = "[用户关注/取关]\n"; 41 | break; 42 | 43 | } 44 | HttpClientUtil httpClient = new HttpClientUtil(); 45 | WxMessage wxMessage = WxMessage.builder() 46 | .appToken(token) 47 | .uids(Arrays.asList(Constant.WX_PUSH_UID)) 48 | .contentType(WxMessage.CONTENT_TYPE_TEXT) 49 | .content(content + date + split + message) 50 | .build(); 51 | try { 52 | httpClient.doPost(PUSH_URL, JSONObject.toJSONString(wxMessage)); 53 | } catch (IOException e) { 54 | log.error("推送微信消息失败", e); 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/resources/python/encode.js: -------------------------------------------------------------------------------- 1 | function ef(str) { 2 | function AA() { 3 | _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; 4 | 5 | this.encode = function (input) { 6 | var output = ""; 7 | var chr1, chr2, chr3, enc1, enc2, enc3, enc4; 8 | var i = 0; 9 | input = _utf8_encode(input); 10 | while (i < input.length) { 11 | chr1 = input.charCodeAt(i++); 12 | chr2 = input.charCodeAt(i++); 13 | chr3 = input.charCodeAt(i++); 14 | enc1 = chr1 >> 2; 15 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); 16 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); 17 | enc4 = chr3 & 63; 18 | if (isNaN(chr2)) { 19 | enc3 = enc4 = 64; 20 | } else if (isNaN(chr3)) { 21 | enc4 = 64; 22 | } 23 | output = output + 24 | _keyStr.charAt(enc1) + _keyStr.charAt(enc2) + 25 | _keyStr.charAt(enc3) + _keyStr.charAt(enc4); 26 | } 27 | return output; 28 | } 29 | 30 | _utf8_encode = function (string) { 31 | string = string.replace(/\r\n/g, "\n"); 32 | var utftext = ""; 33 | for (var n = 0; n < string.length; n++) { 34 | var c = string.charCodeAt(n); 35 | if (c < 128) { 36 | utftext += String.fromCharCode(c); 37 | } else if ((c > 127) && (c < 2048)) { 38 | utftext += String.fromCharCode((c >> 6) | 192); 39 | utftext += String.fromCharCode((c & 63) | 128); 40 | } else { 41 | utftext += String.fromCharCode((c >> 12) | 224); 42 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 43 | utftext += String.fromCharCode((c & 63) | 128); 44 | } 45 | } 46 | return utftext; 47 | } 48 | } 49 | 50 | var aa = new AA(); 51 | return aa.encode(str); 52 | } -------------------------------------------------------------------------------- /src/main/resources/static/css/loading2.css: -------------------------------------------------------------------------------- 1 | #loading{ 2 | background-color: #55b8b6; 3 | height: 100%; 4 | width: 100%; 5 | position: fixed; 6 | z-index: 1; 7 | margin-top: 0px; 8 | top: 0px; 9 | } 10 | #loading-center{ 11 | width: 100%; 12 | height: 100%; 13 | position: relative; 14 | } 15 | #loading-center-absolute { 16 | position: absolute; 17 | left: 50%; 18 | top: 40%; 19 | height: 200px; 20 | width: 200px; 21 | margin-top: -100px; 22 | margin-left: -100px; 23 | -ms-transform: rotate(-135deg); 24 | -webkit-transform: rotate(-135deg); 25 | transform: rotate(-135deg); 26 | } 27 | .object{ 28 | 29 | -moz-border-radius: 50% 50% 50% 50%; 30 | -webkit-border-radius: 50% 50% 50% 50%; 31 | border-radius: 50% 50% 50% 50%; 32 | position: absolute; 33 | border-top: 5px solid #FFF; 34 | border-bottom: 5px solid transparent; 35 | border-left: 5px solid #FFF; 36 | border-right: 5px solid transparent; 37 | 38 | -webkit-animation: animate 2s infinite; 39 | animation: animate 2s infinite; 40 | 41 | } 42 | #object_one{ 43 | left: 75px; 44 | top: 75px; 45 | width: 50px; 46 | height: 50px; 47 | } 48 | 49 | #object_two{ 50 | left: 65px; 51 | top: 65px; 52 | width: 70px; 53 | height: 70px; 54 | -webkit-animation-delay: 0.2s; 55 | animation-delay: 0.2s; 56 | } 57 | 58 | #object_three{ 59 | left: 55px; 60 | top: 55px; 61 | width: 90px; 62 | height: 90px; 63 | -webkit-animation-delay: 0.4s; 64 | animation-delay: 0.4s; 65 | } 66 | #object_four{ 67 | left: 45px; 68 | top: 45px; 69 | width: 110px; 70 | height: 110px; 71 | -webkit-animation-delay: 0.6s; 72 | animation-delay: 0.6s; 73 | 74 | } 75 | 76 | @-webkit-keyframes animate { 77 | 78 | 50% { 79 | 80 | -ms-transform: rotate(360deg) scale(0.8); 81 | -webkit-transform: rotate(360deg) scale(0.8); 82 | transform: rotate(360deg) scale(0.8); 83 | } 84 | } 85 | 86 | @keyframes animate { 87 | 88 | 50% { 89 | 90 | -ms-transform: rotate(360deg) scale(0.8); 91 | -webkit-transform: rotate(360deg) scale(0.8); 92 | transform: rotate(360deg) scale(0.8); 93 | } 94 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/utils/SslUtil.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.utils; 2 | 3 | import java.security.cert.CertificateException; 4 | import java.security.cert.X509Certificate; 5 | 6 | import javax.net.ssl.HostnameVerifier; 7 | import javax.net.ssl.HttpsURLConnection; 8 | import javax.net.ssl.SSLContext; 9 | import javax.net.ssl.SSLSession; 10 | import javax.net.ssl.TrustManager; 11 | import javax.net.ssl.X509TrustManager; 12 | 13 | public class SslUtil { 14 | private static void trustAllHttpsCertificates() throws Exception { 15 | TrustManager[] trustAllCerts = new TrustManager[1]; 16 | TrustManager tm = new miTM(); 17 | trustAllCerts[0] = tm; 18 | SSLContext sc = SSLContext.getInstance("SSL"); 19 | sc.init(null, trustAllCerts, null); 20 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); 21 | } 22 | 23 | static class miTM implements TrustManager,X509TrustManager { 24 | public X509Certificate[] getAcceptedIssuers() { 25 | return null; 26 | } 27 | 28 | public boolean isServerTrusted(X509Certificate[] certs) { 29 | return true; 30 | } 31 | 32 | public boolean isClientTrusted(X509Certificate[] certs) { 33 | return true; 34 | } 35 | 36 | public void checkServerTrusted(X509Certificate[] certs, String authType) 37 | throws CertificateException { 38 | return; 39 | } 40 | 41 | public void checkClientTrusted(X509Certificate[] certs, String authType) 42 | throws CertificateException { 43 | return; 44 | } 45 | } 46 | 47 | /** 48 | * 忽略HTTPS请求的SSL证书,必须在openConnection之前调用 49 | * @throws Exception 50 | */ 51 | public static void ignoreSsl() throws Exception{ 52 | HostnameVerifier hv = new HostnameVerifier() { 53 | @Override 54 | public boolean verify(String urlHostName, SSLSession session) { 55 | System.out.println("Warning: URL Host: " + urlHostName + " vs. " + session.getPeerHost()); 56 | return true; 57 | } 58 | }; 59 | trustAllHttpsCertificates(); 60 | HttpsURLConnection.setDefaultHostnameVerifier(hv); 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/service/GdouJWService.java: -------------------------------------------------------------------------------- 1 | // package com.starix.gdou.service; 2 | // 3 | // import com.starix.gdou.dto.LoginResult; 4 | // import com.starix.gdou.vo.ExamVO; 5 | // import com.starix.gdou.vo.ScoreVO; 6 | // 7 | // import java.io.IOException; 8 | // import java.util.List; 9 | // 10 | // /** 11 | // * 教务系统服务service 12 | // * v1版本 13 | // * @author Starix 14 | // * @date 2019-11-20 20:05 15 | // */ 16 | // public interface GdouJWService { 17 | // 18 | // 19 | // /** 20 | // * 执行登录 21 | // * @param xh 学号 22 | // * @param password 密码 23 | // * @return LoginResult 封装登录成功之后的cookie值等关键数据,用于维持登录 24 | // */ 25 | // LoginResult login(String xh, String password) throws Exception; 26 | // 27 | // 28 | // /** 29 | // * 抓取成绩信息 30 | // * @param loginResult 登录成功之后的必要信息 31 | // * @param year 学年 32 | // * @param semester 学期 33 | // * @return 34 | // */ 35 | // List getScore(LoginResult loginResult, String year, String semester) throws IOException; 36 | // 37 | // 38 | // /** 39 | // * 抓取考试信息 40 | // * @param loginResult 41 | // * @param year 42 | // * @param semester 43 | // * @return 44 | // */ 45 | // List getExam(LoginResult loginResult, String year, String semester) throws Exception; 46 | // 47 | // /** 48 | // * 获得查成绩页面年份下拉列表数据 49 | // * @param loginResult 50 | // * @return 51 | // */ 52 | // List getSocreYearOptionsList(LoginResult loginResult) throws Exception; 53 | // 54 | // 55 | // /** 56 | // * 获得查考试页面年份下拉列表数据 57 | // * @param loginResult 58 | // * @return 59 | // */ 60 | // List getExamYearOptionsList(LoginResult loginResult) throws Exception; 61 | // 62 | // 63 | // /** 64 | // * 通过openid查询已绑定的学号信息实现自动登录 65 | // * @param openid 66 | // * @return 67 | // * @throws Exception 68 | // */ 69 | // LoginResult loginByOpenid(String openid) throws Exception; 70 | // 71 | // 72 | // /** 73 | // * 一键自动评教 74 | // * @param mode 一键评教模式:0->全程自动,1->自动填写,官网手动修改提交 75 | // * @throws Exception 76 | // */ 77 | // void autoEvaluate(LoginResult loginResult, String content, Integer mode) throws Exception; 78 | // } 79 | -------------------------------------------------------------------------------- /src/main/resources/python/cpdaily_auth.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | import cpdaily_captcha_ocr as ocr 4 | import argparse 5 | headers = { 6 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' 7 | 'Chrome/79.0.3945.130 Safari/537.36', 8 | } 9 | 10 | 11 | def get_argument(): 12 | # 读取命令行参数 13 | ap = argparse.ArgumentParser() 14 | ap.add_argument("-u", "--username", required=True, help="学号") 15 | ap.add_argument("-p", "--password", required=True, help="密码") 16 | args = vars(ap.parse_args()) 17 | return args['username'], args['password'] 18 | 19 | 20 | def do_auth(username, password): 21 | session = requests.session() 22 | auth_url = 'https://authserver.gdou.edu.cn/authserver/login?service=https%3A%2F%2Fgdou.cpdaily.com%2Fwec-counselor' \ 23 | '-sign-apps%2Fstu%2Fsign%2FgetStuSignInfosInOneDay' 24 | response = session.get(auth_url, headers=headers, verify=False) 25 | soup = BeautifulSoup(response.text, 'html.parser') 26 | lt = soup.find(attrs={'name': 'lt'})["value"] 27 | execution = soup.find(attrs={'name': 'execution'})["value"] 28 | captcha_url = 'http://authserver.gdou.edu.cn/authserver/captcha.html' 29 | while True: 30 | response = session.get(captcha_url, headers=headers) 31 | captcha = ocr.get_code(response.content) 32 | data = { 33 | "username": username, 34 | "password": password, 35 | "captchaResponse": captcha, 36 | "lt": lt, 37 | "execution": execution, 38 | "_eventId": "submit", 39 | "rmShown": 1 40 | } 41 | response = session.post(auth_url, data=data, headers=headers) 42 | if '密码有误' in response.text: 43 | print('result-认证失败') 44 | return None 45 | elif '无效的验证码' in response.text: 46 | print('验证码错误,正在重试') 47 | else: 48 | # 经测试发现有时候Cookie中MOD_AUTH_CAS会是一个空值,需要重试获取 49 | if 'MOD_AUTH_CAS' not in session.cookies: 50 | print('MOD_AUTH_CAS为空,正在重试') 51 | continue 52 | print('result-认证成功') 53 | return session.cookies['MOD_AUTH_CAS'] 54 | 55 | 56 | if __name__ == '__main__': 57 | do_auth(get_argument()[0], get_argument()[1]) -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/controller/GdouJWScoreNotifyController.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.controller; 2 | 3 | import com.starix.gdou.response.CommonResult; 4 | import com.starix.gdou.response.ResultCode; 5 | import com.starix.gdou.service.GdouJWScoreNotifyService; 6 | import com.starix.gdou.utils.WxMessagePushUtil; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.util.StringUtils; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | import static com.starix.gdou.common.Constant.WX_PUSH_TOKEN_MAIN_LOG; 13 | 14 | /** 15 | * @author Starix 16 | * @date 2020-07-17 21:29 17 | */ 18 | @RestController 19 | @RequestMapping("/jw/notify") 20 | @CrossOrigin 21 | @Slf4j 22 | public class GdouJWScoreNotifyController { 23 | 24 | @Autowired 25 | private GdouJWScoreNotifyService gdouJWScoreNotifyService; 26 | 27 | @PostMapping("/enable") 28 | public CommonResult enableNotify(String openid, String email, int operation) throws Exception { 29 | log.info("开启/关闭成绩更新通知, openid: {}, email: {}, operation: {}", openid, email, operation); 30 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, 31 | String.format("开启成绩更新通知, openid: %s, email: %s", openid, email)); 32 | if (StringUtils.isEmpty(email)){ 33 | return CommonResult.failed(ResultCode.VALIDATE_FAILED); 34 | } 35 | if (StringUtils.isEmpty(openid)){ 36 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, "开启成绩通知页面未获取到关注用户信息"); 37 | return CommonResult.failed("未获取到关注用户的信息,请从公众号内进入该页面再进行操作!"); 38 | } 39 | if (operation == 1){ 40 | gdouJWScoreNotifyService.enableNotify(openid, email); 41 | } else { 42 | gdouJWScoreNotifyService.disableNotify(openid); 43 | } 44 | return CommonResult.success(); 45 | } 46 | 47 | @GetMapping("/status") 48 | public CommonResult getStatus(String openid) throws Exception { 49 | log.info("获取成绩更新通知状态, openid: {}", openid); 50 | if (StringUtils.isEmpty(openid)){ 51 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, "开启成绩通知页面未获取到关注用户信息"); 52 | return CommonResult.failed("未获取到关注用户的信息,请从公众号内进入该页面再进行操作!"); 53 | } 54 | return CommonResult.success(gdouJWScoreNotifyService.queryNotifyStatus(openid)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/static/css/button/demoadpacks.css: -------------------------------------------------------------------------------- 1 | #cdawrap { 2 | text-align: center; 3 | width: 178px; 4 | height: auto; 5 | right: 15px; 6 | top: 35px; 7 | position: fixed; 8 | background: rgba(255,255,255,0.8); 9 | border: 1px solid rgba(0,0,0,0.05); 10 | z-index: 1000000; 11 | font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 12 | padding: 24px 24px 10px 24px; 13 | font-size: 12px; 14 | font-weight: 700; 15 | } 16 | 17 | #cdawrap .carbon-img { 18 | display: block; 19 | text-align: center; 20 | border: none; 21 | } 22 | 23 | #cdawrap .carbon-text { 24 | color: #4f5152; 25 | padding: 10px 0 15px 0; 26 | display: block; 27 | line-height: 1.2; 28 | text-decoration: none; 29 | } 30 | 31 | #cdawrap a.carbon-poweredby { 32 | display: block; 33 | text-transform: uppercase; 34 | font-size: 65%; 35 | letter-spacing: 1px; 36 | color: #cc301e; 37 | font-weight: 700; 38 | text-decoration: none; 39 | } 40 | 41 | #cdawrap a.carbon-poweredby:hover, 42 | #cdawrap a.carbon-poweredby:focus { 43 | color: #333; 44 | } 45 | 46 | #cda-remove { 47 | width: 24px; 48 | height: 24px; 49 | position: absolute; 50 | top: 0; 51 | right: 0; 52 | line-height: 1; 53 | text-align: center; 54 | cursor: pointer; 55 | font-size: 12px; 56 | line-height: 24px; 57 | color: #777; 58 | } 59 | 60 | #cda-remove:hover { 61 | background: rgba(0,0,0,0.1); 62 | } 63 | 64 | #cda-remove:before { 65 | content: 'x'; 66 | display: inline-block; 67 | font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif; 68 | } 69 | 70 | @media screen and (max-width: 800px){ 71 | #cdawrap { 72 | width: 100%; 73 | height: auto; 74 | right: 0px; 75 | top: auto !important; 76 | bottom: 0px !important; 77 | left: 0px !important; 78 | background: #fff; 79 | padding: 15px 50px 10px 10px; 80 | text-align: left; 81 | font-size: 11px; 82 | } 83 | 84 | #cdawrap::before { 85 | content: 'Advertisement'; 86 | position: absolute; 87 | top: 0; 88 | left: 0; 89 | color: #999; 90 | font-size: 9px; 91 | padding: 10px; 92 | } 93 | 94 | #cdawrap .carbon-img { 95 | display: none; 96 | } 97 | 98 | #cda-remove { 99 | width: 30px; 100 | height: 30px; 101 | font-size: 16px; 102 | line-height: 30px; 103 | } 104 | } 105 | 106 | @media screen and (max-width: 460px){ 107 | #cdawrap { display: none; } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/config/ConnectorConfig.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.config; 2 | 3 | import org.apache.catalina.connector.Connector; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; 6 | import org.springframework.boot.web.servlet.server.ServletWebServerFactory; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | /** 11 | * 配置同时支持HTTP和HTTPS请求 12 | * 或者使HTTP请求重定向到HTTPS 13 | * @author Starix 14 | * @date 2019-12-29 14:42 15 | */ 16 | @Configuration 17 | public class ConnectorConfig { 18 | 19 | @Value("${server.custom.httpPort}") 20 | private int httpPort; 21 | 22 | // @Value("${server.port}") 23 | // private int httpsPort; 24 | 25 | @Bean 26 | public ServletWebServerFactory servletWebServerFactory() { 27 | //强制使用https并使http转向https配置 28 | // TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory() { 29 | // @Override 30 | // protected void postProcessContext(Context context) { 31 | // SecurityConstraint securityConstraint = new SecurityConstraint(); 32 | // securityConstraint.setUserConstraint("CONFIDENTIAL"); 33 | // SecurityCollection securityCollection = new SecurityCollection(); 34 | // securityCollection.addPattern("/*"); 35 | // securityConstraint.addCollection(securityCollection); 36 | // context.addConstraint(securityConstraint); 37 | // } 38 | // }; 39 | // factory.addAdditionalTomcatConnectors(redirectConnector()); 40 | 41 | // 添加http 42 | TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(); 43 | factory.addAdditionalTomcatConnectors(createStandardConnector()); 44 | return factory; 45 | } 46 | 47 | // 配置http 48 | private Connector createStandardConnector() { 49 | Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); 50 | connector.setPort(httpPort); 51 | return connector; 52 | } 53 | 54 | //http重定向到https 55 | // private Connector redirectConnector() { 56 | // Connector connector = new Connector(Http11NioProtocol.class.getName()); 57 | // connector.setScheme("http"); 58 | // connector.setPort(serverPortHttp); 59 | // connector.setSecure(false); 60 | // connector.setRedirectPort(serverPortHttps); 61 | // return connector; 62 | // } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/controller/CpdailyController.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.controller; 2 | 3 | import com.starix.gdou.entity.CpdailyUser; 4 | import com.starix.gdou.response.CommonResult; 5 | import com.starix.gdou.service.CpdailyUserService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.validation.BindingResult; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import javax.validation.Valid; 12 | 13 | /** 14 | * @author Starix 15 | * @date 2020-04-06 12:09 16 | */ 17 | @RestController 18 | @RequestMapping("/cpdaily") 19 | @Slf4j 20 | @CrossOrigin 21 | public class CpdailyController { 22 | 23 | @Autowired 24 | private CpdailyUserService cpdailyUserService; 25 | 26 | //新增签到用户 27 | @PostMapping(value = "/addUser") 28 | public CommonResult addCpdailyUser(@RequestBody @Valid CpdailyUser user, BindingResult bindingResult) throws Exception { 29 | // TODO: 2020-05-04 停止使用 30 | return CommonResult.failed("自动签到服务已停用!"); 31 | // if (bindingResult.hasErrors()){ 32 | // List allErrors = bindingResult.getAllErrors(); 33 | // //多个校验错误只返回第一个错误信息给前端 34 | // log.info("用户参数校验错误:[{}]", allErrors.get(0).getDefaultMessage()); 35 | // return CommonResult.validateFailed(allErrors.get(0).getDefaultMessage()); 36 | // } 37 | // cpdailyUserService.saveUser(user); 38 | // return CommonResult.success(); 39 | } 40 | 41 | //取消签到 42 | @PostMapping(value = "/delUser") 43 | public CommonResult delCpdailyUser(String username, String password) throws Exception { 44 | // TODO: 2020-05-04 停止使用 45 | return CommonResult.failed("自动签到服务已停用,无须手动取消!"); 46 | // if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){ 47 | // return CommonResult.validateFailed(); 48 | // } 49 | // cpdailyUserService.deleteUser(username, password); 50 | // return CommonResult.success(); 51 | } 52 | 53 | //更新签到信息 54 | @PostMapping(value = "/updateUser") 55 | public CommonResult udpateCpdailyUser(@RequestBody @Valid CpdailyUser user, BindingResult bindingResult) throws Exception { 56 | // TODO: 2020-05-04 停止使用 57 | return CommonResult.failed("自动签到服务已停用!"); 58 | // if (bindingResult.hasErrors()){ 59 | // List allErrors = bindingResult.getAllErrors(); 60 | // //多个校验错误只返回第一个错误信息给前端 61 | // log.info("用户参数校验错误:[{}]", allErrors.get(0).getDefaultMessage()); 62 | // return CommonResult.validateFailed(allErrors.get(0).getDefaultMessage()); 63 | // } 64 | // cpdailyUserService.updateUser(user); 65 | // return CommonResult.success(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/controller/GdouJWUserBindController.java: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2020 Meituan 2 | // All rights reserved 3 | package com.starix.gdou.controller; 4 | 5 | import com.starix.gdou.response.CommonResult; 6 | import com.starix.gdou.response.ResultCode; 7 | import com.starix.gdou.service.GdouJWServiceV2; 8 | import com.starix.gdou.service.UserBindService; 9 | import com.starix.gdou.utils.WxMessagePushUtil; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.util.StringUtils; 13 | import org.springframework.web.bind.annotation.CrossOrigin; 14 | import org.springframework.web.bind.annotation.PostMapping; 15 | import org.springframework.web.bind.annotation.RequestMapping; 16 | import org.springframework.web.bind.annotation.RestController; 17 | 18 | import static com.starix.gdou.common.Constant.WX_PUSH_TOKEN_MAIN_LOG; 19 | 20 | /** 21 | * @author shiwenjie03 22 | * @created 2020/7/7 5:08 下午 23 | **/ 24 | @RestController 25 | @RequestMapping("/jw") 26 | @CrossOrigin 27 | @Slf4j 28 | public class GdouJWUserBindController { 29 | 30 | @Autowired 31 | private UserBindService userBindService; 32 | @Autowired 33 | private GdouJWServiceV2 gdouJWService; 34 | 35 | @PostMapping("/bind") 36 | public CommonResult doBind(String openid, String username, String password) throws Exception { 37 | if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){ 38 | return CommonResult.failed(ResultCode.VALIDATE_FAILED); 39 | } 40 | if (StringUtils.isEmpty(openid)){ 41 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, "绑定学号页面未获取到关注用户信息"); 42 | return CommonResult.failed("未获取到关注用户的信息,请从公众号内进入该页面再进行绑定!"); 43 | } 44 | log.info("[{}]绑定学号", username); 45 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, String.format("[%s]绑定学号", username)); 46 | // 绑定前先验证账号密码是否正确 47 | gdouJWService.login(username, password); 48 | userBindService.bind(openid, username, password); 49 | return CommonResult.success(); 50 | } 51 | 52 | 53 | // TODO: 2020/7/7 解绑账号待实现,这个不重要 54 | // @PostMapping("/unbind") 55 | // public CommonResult doUnbind(String openid, String username, String password) throws Exception { 56 | // if (!checkBindParams(openid, username, password)){ 57 | // return CommonResult.failed(ResultCode.VALIDATE_FAILED); 58 | // } 59 | // log.info("[{}]绑定学号", username); 60 | // // 解除绑定前先验证账号密码是否正确 61 | // gdouJWService.login(username, password); 62 | // userBindService.unbind(openid, username, password); 63 | // return CommonResult.success(); 64 | // } 65 | 66 | private boolean checkBindParams(String openid, String username, String password){ 67 | return !StringUtils.isEmpty(openid) && !StringUtils.isEmpty(username) && !StringUtils.isEmpty(password); 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/task/CpdailySignInAsyncTask.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.task; 2 | 3 | import com.starix.gdou.entity.CpdailyUser; 4 | import com.starix.gdou.exception.CustomException; 5 | import com.starix.gdou.response.CommonResult; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.scheduling.annotation.Async; 9 | import org.springframework.scheduling.annotation.AsyncResult; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.io.*; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.concurrent.Future; 16 | import java.util.concurrent.FutureTask; 17 | 18 | /** 19 | * TODO 停止使用 20 | * @author Starix 21 | * @date 2020-04-06 12:03 22 | */ 23 | @Slf4j 24 | // @Component 25 | public class CpdailySignInAsyncTask { 26 | 27 | //python脚本绝对路径 28 | @Value("${python.path}") 29 | private String PYTHON_PATH_AUTH; 30 | //python脚本命令行输出的编码 31 | @Value("${python.output-encoding}") 32 | private String OUTPUT_ENCODING; 33 | 34 | @Async 35 | public Future doSignIn(CpdailyUser cpdailyUser) throws Exception { 36 | String[] cmd = new String[16]; 37 | cmd[0] = "python"; 38 | cmd[1] = "cpdaily_sign_in.py"; 39 | cmd[2] = "--username"; 40 | cmd[3] = cpdailyUser.getUsername(); 41 | cmd[4] = "--password"; 42 | cmd[5] = cpdailyUser.getPassword(); 43 | cmd[6] = "--longitude"; 44 | cmd[7] = cpdailyUser.getLongitude(); 45 | cmd[8] = "--latitude"; 46 | cmd[9] = cpdailyUser.getLatitude(); 47 | cmd[10] = "--abnormalReason"; 48 | cmd[11] = cpdailyUser.getAbnormalreason(); 49 | cmd[12] = "--position"; 50 | cmd[13] = cpdailyUser.getPosition(); 51 | cmd[14] = "--email"; 52 | cmd[15] = cpdailyUser.getEmail(); 53 | Process process = null; 54 | try { 55 | process = Runtime.getRuntime().exec(cmd, null, new File(PYTHON_PATH_AUTH)); 56 | } catch (IOException e) { 57 | e.printStackTrace(); 58 | log.error("[{}]签到失败,Python签到脚本执行出错:{}", cpdailyUser.getUsername(), e.getMessage()); 59 | return new AsyncResult<>("任务执行失败"); 60 | } 61 | int status = process.waitFor(); 62 | if (status != 0){ 63 | log.error("[{}]签到失败,Python签到脚本执行出错,status:{}", cpdailyUser.getUsername(), status); 64 | return new AsyncResult<>("任务执行失败"); 65 | } 66 | InputStream in = process.getInputStream(); 67 | BufferedReader buffReader = new BufferedReader(new InputStreamReader(in, OUTPUT_ENCODING)); 68 | String line; 69 | List resultLines = new ArrayList<>(); 70 | while ((line = buffReader.readLine()) != null){ 71 | resultLines.add(line); 72 | } 73 | //简单关闭流,暂时不考虑异常 74 | buffReader.close(); 75 | in.close(); 76 | 77 | if (resultLines.contains("result-签到成功")){ 78 | log.info("[{}]签到成功", cpdailyUser.getUsername()); 79 | }else { 80 | log.error("[{}]签到失败:{}", cpdailyUser.getUsername(), resultLines); 81 | } 82 | return new AsyncResult<>("任务执行完毕"); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/resources/static/cpdaily-options-page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 自动签到 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 38 | 39 | 40 | 41 | 42 |
43 |
44 |
45 |

签到操作

46 |
47 | 48 | 49 | 50 |
51 |
52 |
53 | 54 | 55 | 56 | 69 | 70 | -------------------------------------------------------------------------------- /src/main/resources/static/css/button/vicons-font.css: -------------------------------------------------------------------------------- 1 | @font-face { font-family: 'vicons'; font-weight: normal; font-style: normal; src: url('../fonts/vicons/vicons.eot?1u5j7r'); src: url('../fonts/vicons/vicons.eot?#iefix1u5j7r') format('embedded-opentype'), url('../fonts/vicons/vicons.woff?1u5j7r') format('woff'), url('../fonts/vicons/vicons.ttf?1u5j7r') format('truetype'), url('../fonts/vicons/vicons.svg?1u5j7r#vicons') format('svg'); } 2 | .icon { font-family: 'vicons'; font-size: inherit; font-weight: normal; font-style: normal; line-height: inherit; position: relative; display: inline-block; transform: translate(0, 0); vertical-align: middle; text-rendering: auto; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } 3 | .icon::before { display: inline-block; width: 100%; } 4 | .icon-box:before { content: '\e600'; } 5 | .icon-write:before { content: '\e601'; } 6 | .icon-clock:before { content: '\e602'; } 7 | .icon-reply:before { content: '\e603'; } 8 | .icon-reply-all:before { content: '\e604'; } 9 | .icon-forward:before { content: '\e605'; } 10 | .icon-flag:before { content: '\e606'; } 11 | .icon-search:before { content: '\e607'; } 12 | .icon-trash:before { content: '\e608'; } 13 | .icon-envelope:before { content: '\e609'; } 14 | .icon-bubble:before { content: '\e60a'; } 15 | .icon-bubbles:before { content: '\e60b'; } 16 | .icon-user:before { content: '\e60c'; } 17 | .icon-users:before { content: '\e60d'; } 18 | .icon-cloud:before { content: '\e60e'; } 19 | .icon-download:before { content: '\e60f'; } 20 | .icon-upload:before { content: '\e610'; } 21 | .icon-rain:before { content: '\e611'; } 22 | .icon-sun:before { content: '\e612'; } 23 | .icon-moon:before { content: '\e613'; } 24 | .icon-bell:before { content: '\e614'; } 25 | .icon-folder:before { content: '\e615'; } 26 | .icon-pin:before { content: '\e616'; } 27 | .icon-sound:before { content: '\e617'; } 28 | .icon-microphone:before { content: '\e618'; } 29 | .icon-camera:before { content: '\e619'; } 30 | .icon-image:before { content: '\e61a'; } 31 | .icon-cog:before { content: '\e61b'; } 32 | .icon-calendar:before { content: '\e61c'; } 33 | .icon-book:before { content: '\e61d'; } 34 | .icon-map-marker:before { content: '\e61e'; } 35 | .icon-store:before { content: '\e61f'; } 36 | .icon-support:before { content: '\e620'; } 37 | .icon-tag:before { content: '\e621'; } 38 | .icon-heart:before { content: '\e622'; } 39 | .icon-video-camera:before { content: '\e623'; } 40 | .icon-trophy:before { content: '\e624'; } 41 | .icon-cart:before { content: '\e625'; } 42 | .icon-eye:before { content: '\e626'; } 43 | .icon-cancel:before { content: '\e627'; } 44 | .icon-chart:before { content: '\e628'; } 45 | .icon-target:before { content: '\e629'; } 46 | .icon-printer:before { content: '\e62a'; } 47 | .icon-location:before { content: '\e62b'; } 48 | .icon-bookmark:before { content: '\e62c'; } 49 | .icon-monitor:before { content: '\e62d'; } 50 | .icon-cross:before { content: '\e62e'; } 51 | .icon-plus:before { content: '\e62f'; } 52 | .icon-left:before { content: '\e630'; } 53 | .icon-up:before { content: '\e631'; } 54 | .icon-browser:before { content: '\e632'; } 55 | .icon-windows:before { content: '\e633'; } 56 | .icon-switch:before { content: '\e634'; } 57 | .icon-dashboard:before { content: '\e635'; } 58 | .icon-play:before { content: '\e636'; } 59 | .icon-fast-forward:before { content: '\e637'; } 60 | .icon-next:before { content: '\e638'; } 61 | .icon-refresh:before { content: '\e639'; } 62 | .icon-film:before { content: '\e63a'; } 63 | .icon-home:before { content: '\e63b'; } -------------------------------------------------------------------------------- /src/main/resources/static/css/button/carbon.js: -------------------------------------------------------------------------------- 1 | var _carbonads={init:function(){var placement=this.getUrlVar('placement');var circle=this.getUrlVar('circle');var serve=this.getUrlVar('serve');var baseurl='srv.buysellads.com';if(window.location.protocol!='https:')baseurl='srv.carbonads.net';var pro=document.createElement('script');pro.id='_carbonads_projs';pro.type='text/javascript';pro.src='//'+baseurl+'/ads/'+serve+'.json?segment=placement:'+placement+'&callback=_carbonads_go';var ck=document.cookie,day=ck.indexOf('_bsap_daycap='),life=ck.indexOf('_bsap_lifecap=');day=day>=0?ck.substring(day+12+1).split(';')[0].split(','):[];life=life>=0?ck.substring(life+13+1).split(';')[0].split(','):[];if(day.length||life.length){var freqcap=[];for(var i=0;i0)fulllink=link[0]+'?segment=placement:'+placement+';';else fulllink=link[0];if(window.location.protocol!='https:')fulllink=fulllink.replace('srv.buysellads.com','srv.carbonads.net');var el=document.createElement('span');el.innerHTML=''+ad.description+'ads via Carbon';if(typeof ad.pixel!='undefined'){var pix=document.createElement('img');pix.src=ad.pixel;pix.border='0';pix.height='1';pix.width='1';pix.style.display='none';el.appendChild(pix)}var fdiv=document.createElement('div');fdiv.id='carbonads';fdiv.appendChild(el);var carbonjs=document.getElementById('_carbonads_js');if(carbonjs!=null)carbonjs.parentNode.insertBefore(fdiv,carbonjs.nextSibling);_bsap_serving_callback(ad.bannerid,ad.zonekey,ad.freqcap)}_carbonads.init();window['_bsap_serving_callback']=function(banner,zone,freqcap){var append=function(w,data,days){var c=document.cookie,i=c.indexOf(w+'='),existing=i>=0?c.substring(i+w.length+1).split(';')[0]+',':'',d=new Date();d.setTime(days*3600000+d);data=existing+data;data=data.substring(0,2048);document.cookie=w+'='+data+'; expires='+d.toGMTString()+'; path=\/'};if(freqcap){append('_bsap_daycap',banner,1);append('_bsap_lifecap',banner,365)}}; -------------------------------------------------------------------------------- /src/test/java/com/starix/gdou/EmailTest.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou; 2 | 3 | import com.jfinal.kit.Kv; 4 | import com.starix.gdou.dto.ScoreNotifyDTO; 5 | import com.starix.gdou.utils.HtmlMailRenderUtil; 6 | import com.starix.gdou.utils.MailUtil; 7 | import com.starix.gdou.utils.SslUtil; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.mail.javamail.JavaMailSender; 14 | import org.springframework.mail.javamail.MimeMessageHelper; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | 17 | import javax.mail.internet.MimeMessage; 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | 22 | /** 23 | * @author Starix 24 | * @date 2020-07-18 12:20 25 | */ 26 | @SpringBootTest 27 | @RunWith(SpringRunner.class) 28 | @Slf4j 29 | public class EmailTest { 30 | 31 | @Autowired 32 | private JavaMailSender javaMailSender; 33 | 34 | @Test 35 | public void testSend() throws Exception { 36 | MimeMessage mimeMessage = javaMailSender.createMimeMessage(); 37 | MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true); 38 | 39 | messageHelper.setFrom("在思考的猫"); 40 | messageHelper.setTo("646722505@qq.com"); 41 | messageHelper.setSubject("成绩更新通知"); 42 | String html = "

这是html

"; 43 | messageHelper.setText(html, true); 44 | SslUtil.ignoreSsl(); 45 | javaMailSender.send(mimeMessage); 46 | } 47 | 48 | @Autowired 49 | private MailUtil mailUtil; 50 | 51 | @Test 52 | public void testSendMailUtil() throws Exception { 53 | List notifyDTOList = new ArrayList<>(); 54 | ScoreNotifyDTO notifyDTO = ScoreNotifyDTO.builder() 55 | .courseName("计算机工程伦理与工程管理") 56 | .credit("2") 57 | .property("必修") 58 | .score("86") 59 | .isNew(false) 60 | .build(); 61 | notifyDTOList.add(notifyDTO); 62 | notifyDTO = ScoreNotifyDTO.builder() 63 | .courseName("形式与政策教育3") 64 | .credit("0.5") 65 | .property("必修") 66 | .score("85") 67 | .isNew(false) 68 | .build(); 69 | notifyDTOList.add(notifyDTO); 70 | notifyDTO = ScoreNotifyDTO.builder() 71 | .courseName("(网络课)转基因的科学——基因工程") 72 | .credit("1.5") 73 | .property("任选") 74 | .score("83") 75 | .isNew(false) 76 | .build(); 77 | notifyDTOList.add(notifyDTO); 78 | notifyDTO = ScoreNotifyDTO.builder() 79 | .courseName("创新创业教育2") 80 | .credit("0.5") 81 | .property("必修") 82 | .score("95") 83 | .isNew(true) 84 | .build(); 85 | notifyDTOList.add(notifyDTO); 86 | Kv data = Kv.create() 87 | .set("scoreList", notifyDTOList) 88 | .set("username", "201711xxxxxx") 89 | .set("year", "2019-2020") 90 | .set("semester", "2") 91 | .set("queryMore", "https://www.starix.top:9800/score.html"); 92 | String html = HtmlMailRenderUtil.render("templates/score-email-template.html", data); 93 | mailUtil.sendHtmlMail("646722505@qq.com", "成绩更新通知", html); 94 | } 95 | 96 | @Test 97 | public void testException(){ 98 | try { 99 | int a = 2/0; 100 | } catch (Exception e){ 101 | log.error("[{}]错误", "测试", e); 102 | } 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/main/resources/static/css/semantic-custom.css: -------------------------------------------------------------------------------- 1 | /*--------padding--------*/ 2 | .m-padded-mini { 3 | padding: 0.2em !important; 4 | } 5 | 6 | .m-padded-tiny { 7 | padding: 0.3em !important; 8 | } 9 | .m-padded { 10 | padding: 1em !important; 11 | } 12 | .m-padded-tb-mini { 13 | padding-top: 0.2em !important; 14 | padding-bottom: 0.2em !important; 15 | } 16 | .m-padded-tb-tiny { 17 | padding-top: 0.3em !important; 18 | padding-bottom: 0.3em !important; 19 | } 20 | .m-padded-tb-small { 21 | padding-top: 0.5em !important; 22 | padding-bottom: 0.5em !important; 23 | } 24 | .m-padded-tb { 25 | padding-top: 1em !important; 26 | padding-bottom: 1em !important; 27 | } 28 | .m-padded-tb-large { 29 | padding-top: 2em !important; 30 | padding-bottom: 2em !important; 31 | } 32 | .m-padded-tb-big { 33 | padding-top: 3em !important; 34 | padding-bottom: 3em !important; 35 | } 36 | .m-padded-tb-huge { 37 | padding-top: 4em !important; 38 | padding-bottom: 4em !important; 39 | } 40 | .m-padded-tb-massive { 41 | padding-top: 5em !important; 42 | padding-bottom: 5em !important; 43 | } 44 | .m-padded-lr-responsive { 45 | padding-left: 4em !important; 46 | padding-right: 4em !important; 47 | } 48 | 49 | /*--------margin--------*/ 50 | 51 | .m-margin-top-small { 52 | margin-top:0.5em !important; 53 | } 54 | .m-margin-top { 55 | margin-top: 1em !important; 56 | } 57 | .m-margin-top-large { 58 | margin-top: 2em !important; 59 | } 60 | 61 | .m-margin-tb-tiny { 62 | margin-top: 0.3em !important; 63 | margin-bottom: 0.3em !important; 64 | } 65 | .m-margin-bottom { 66 | margin-bottom: 1em !important; 67 | 68 | } 69 | .m-margin-bottom-large { 70 | margin-bottom: 2em !important; 71 | 72 | } 73 | .m-margin-bottom-small { 74 | margin-bottom: 0.5em !important; 75 | 76 | } 77 | 78 | /*-------text-------*/ 79 | .m-text { 80 | font-weight: 300 !important; 81 | letter-spacing: 1px !important; 82 | line-height: 1.8; 83 | } 84 | .m-text-thin { 85 | font-weight: 300 !important; 86 | } 87 | .m-text-spaced { 88 | letter-spacing: 1px !important; 89 | } 90 | .m-text-lined { 91 | line-height: 1.8; 92 | } 93 | .m-text-align-center{ 94 | text-align: center; 95 | } 96 | 97 | /*--------opacity--------*/ 98 | .m-opacity-mini { 99 | opacity: 0.8 !important; 100 | } 101 | .m-opacity-tiny { 102 | opacity: 0.6 !important; 103 | } 104 | 105 | /*--------position--------*/ 106 | .m-right-top { 107 | position: absolute; 108 | top: 0; 109 | right: 0; 110 | } 111 | .m-fixed { 112 | position: fixed !important; 113 | z-index: 10 !important; 114 | } 115 | .m-right-buttom { 116 | bottom: 0 !important; 117 | right: 0 !important; 118 | 119 | } 120 | 121 | /*--------display--------*/ 122 | .m-inline-block { 123 | display: inline-block !important; 124 | } 125 | 126 | 127 | /*--------container--------*/ 128 | .m-container { 129 | max-width: 72em !important; 130 | margin: auto !important; 131 | } 132 | .m-container-small { 133 | max-width: 60em !important; 134 | margin: auto !important; 135 | } 136 | 137 | 138 | /*--------shadow--------*/ 139 | .m-shadow-small { 140 | -webkit-box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important; 141 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2) !important; 142 | } 143 | 144 | /*--------color--------*/ 145 | .m-black { 146 | color: #333 !important; 147 | } 148 | 149 | .m-mobile-show { 150 | display: none !important; 151 | } 152 | 153 | /*当屏幕宽度小于768x时*/ 154 | @media screen and (max-width: 768px){ 155 | /*手机端隐藏导航条目*/ 156 | .m-mobile-hide { 157 | display: none !important; 158 | } 159 | /*手机端显示列表按钮*/ 160 | .m-mobile-show { 161 | display: block !important; 162 | } 163 | .m-padded-lr-responsive { 164 | padding-left: 0 !important; 165 | padding-right: 0 !important; 166 | } 167 | .m-mobile-lr-clear { 168 | padding-left: 0 !important; 169 | padding-right: 0 !important; 170 | } 171 | .m-mobile-wide { 172 | width: 100% !important; 173 | } 174 | } 175 | 176 | 177 | -------------------------------------------------------------------------------- /src/main/resources/python/cpdaily_sign_in.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import base64 3 | import requests 4 | import uuid 5 | from pyDes import * 6 | import cpdaily_auth 7 | import email_util 8 | 9 | headers = { 10 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' 11 | 'Chrome/79.0.3945.130 Safari/537.36' 12 | # 'Cookie': '', 13 | # 'Cpdaily-Extension': '' 14 | } 15 | 16 | # 读取命令行参数 17 | ap = argparse.ArgumentParser() 18 | ap.add_argument("-u", "--username", required=True, help="学号") 19 | ap.add_argument("-p", "--password", required=True, help="密码") 20 | ap.add_argument("-lon", "--longitude", required=True, help="经度") 21 | ap.add_argument("-lat", "--latitude", required=True, help="纬度") 22 | ap.add_argument("-reason", "--abnormalReason", required=True, help="签到内容") 23 | ap.add_argument("-pos", "--position", required=True, help="地理位置") 24 | ap.add_argument("-m", "--email", required=True, help="接收通知的邮箱") 25 | args = vars(ap.parse_args()) 26 | username = args['username'] 27 | password = args['password'] 28 | longitude = args['longitude'] 29 | latitude = args['latitude'] 30 | abnormalReason = args['abnormalReason'] 31 | position = args['position'] 32 | email = args['email'] 33 | 34 | 35 | # 执行登录,获取认证信息 36 | def authentication(): 37 | # MOD_AUTH_CAS是最关键认证字段 38 | mod_auth_cas = cpdaily_auth.do_auth(username, password) 39 | headers['Cookie'] = 'MOD_AUTH_CAS=%s' % mod_auth_cas 40 | 41 | 42 | # 获取签到信息 43 | def get_stu_sign_info(): 44 | try: 45 | response = requests.post('https://gdou.cpdaily.com/wec-counselor-sign-apps/stu/sign/getStuSignInfosInOneDay', 46 | json={}, headers=headers) 47 | except requests.exceptions.SSLError as e: 48 | print('result-获取信息失败') 49 | email_util.send_email(email, '签到失败', '签到失败:获取信息失败') 50 | exit(0) 51 | json = response.json() 52 | if json['datas']['signedTasks']: 53 | print('result-今日已签到,不再重复提交签到') 54 | email_util.send_email(email, '签到失败', '签到失败:今日您已完成签到,不再重复提交签到') 55 | exit(0) 56 | elif json['datas']['unSignedTasks']: 57 | return json['datas']['unSignedTasks'][0]['signInstanceWid'] 58 | else: 59 | email_util.send_email(email, '签到失败', '签到失败:未获取到签到信息') 60 | exit(0) 61 | 62 | 63 | # DES加密 64 | def encrypt(s, key='ST83=@XV'): 65 | key = key 66 | iv = b"\x01\x02\x03\x04\x05\x06\x07\x08" 67 | k = des(key, CBC, iv, pad=None, padmode=PAD_PKCS5) 68 | encrypt_str = k.encrypt(s) 69 | return base64.b64encode(encrypt_str).decode() 70 | 71 | 72 | # 生成Cpdaily-Extension关键签到参数 73 | def create_cpdaily_extension(lon, lat, uid): 74 | """ 75 | headers中的CpdailyInfo参数 76 | :param lon: 定位经度 77 | :param lat: 定位纬度 78 | :param uid: 学生学号 79 | :return: Cpdaily-Extension 80 | """ 81 | s = r'{"systemName":"android","systemVersion":"8.1.0","model":"16th",' \ 82 | r'"deviceId":"' + str(uuid.uuid1()) + '","appVersion":"8.1.11","lon":' \ 83 | + str(lon) + ',"lat":' + str(lat) + ',"userId":"' + str(uid) + '"}' 84 | extension = encrypt(s) 85 | return extension 86 | 87 | 88 | # 执行签到 89 | def submit_sign(): 90 | submit_url = 'https://gdou.cpdaily.com/wec-counselor-sign-apps/stu/sign/submitSign' 91 | wid = get_stu_sign_info() 92 | data = { 93 | "signInstanceWid": wid, 94 | "longitude": longitude, 95 | "latitude": latitude, 96 | "isMalposition": 1, 97 | "abnormalReason": abnormalReason, 98 | "signPhotoUrl": "", 99 | "position": position 100 | } 101 | headers['Cpdaily-Extension'] = create_cpdaily_extension(data['longitude'], data['latitude'], username) 102 | response = requests.post(submit_url, json=data, headers=headers) 103 | if response.json()['message'] == 'SUCCESS': 104 | print('result-签到成功') 105 | email_util.send_email(email, '签到成功', '今日签到已完成!') 106 | else: 107 | print('result-%s' % response.json()) 108 | email_util.send_email(email, '签到失败', '签到失败,详细信息:%s' % response.json()['message']) 109 | 110 | 111 | if __name__ == '__main__': 112 | authentication() 113 | submit_sign() 114 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 2.0.5.RELEASE 11 | 12 | 13 | 14 | com.starix 15 | gdou-jw-tools 16 | 1.0-SNAPSHOT 17 | 18 | 19 | 1.8 20 | 1.8 21 | 1.8 22 | 23 | 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | 32 | com.alibaba 33 | fastjson 34 | 1.2.70 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-data-redis 40 | 41 | 42 | 43 | org.apache.commons 44 | commons-pool2 45 | 46 | 47 | 48 | org.apache.httpcomponents 49 | httpclient 50 | 4.5.7 51 | 52 | 53 | 54 | org.jsoup 55 | jsoup 56 | 1.11.3 57 | 58 | 59 | 60 | cn.wanghaomiao 61 | JsoupXpath 62 | 0.3.2 63 | 64 | 65 | org.projectlombok 66 | lombok 67 | true 68 | 69 | 70 | com.alibaba 71 | druid-spring-boot-starter 72 | 1.1.10 73 | 74 | 75 | 76 | mysql 77 | mysql-connector-java 78 | 8.0.16 79 | 80 | 81 | 82 | org.springframework.boot 83 | spring-boot-starter-data-jpa 84 | 85 | 86 | 87 | org.springframework.boot 88 | spring-boot-starter-mail 89 | 90 | 91 | 92 | com.jfinal 93 | enjoy 94 | 4.3 95 | 96 | 97 | commons-io 98 | commons-io 99 | 2.4 100 | 101 | 102 | org.springframework.boot 103 | spring-boot-starter-test 104 | 105 | 106 | 107 | 108 | 109 | 110 | org.springframework.boot 111 | spring-boot-maven-plugin 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /src/main/resources/static/css/button/base.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Roboto:400,700); 2 | @font-face { 3 | font-weight: normal; 4 | font-style: normal; 5 | font-family: 'codropsicons'; 6 | src: url("../fonts/codropsicons/codropsicons.eot"); 7 | src: url("../fonts/codropsicons/codropsicons.eot?#iefix") format("embedded-opentype"), url("../fonts/codropsicons/codropsicons.woff") format("woff"), url("../fonts/codropsicons/codropsicons.ttf") format("truetype"), url("../fonts/codropsicons/codropsicons.svg#codropsicons") format("svg"); 8 | } 9 | *, 10 | *:after, 11 | *:before { 12 | -webkit-box-sizing: border-box; 13 | box-sizing: border-box; 14 | } 15 | .cf:before, 16 | .cf:after { 17 | content: ''; 18 | display: table; 19 | } 20 | .cf:after { 21 | clear: both; 22 | } 23 | body { 24 | background: #cfd8dc; 25 | color: #37474f; 26 | font-weight: 400; 27 | font-size: 1em; 28 | font-family: 'Raleway', Arial, sans-serif; 29 | } 30 | .support { 31 | font-weight: bold; 32 | padding: 2em 0 0 0; 33 | font-size: 1.4em; 34 | color: #ee2563; 35 | display: none; 36 | } 37 | a { 38 | color: #7986cb; 39 | text-decoration: none; 40 | outline: none; 41 | } 42 | a:hover, 43 | a:focus { 44 | color: #3f51b5; 45 | } 46 | .hidden { 47 | position: absolute; 48 | width: 0; 49 | height: 0; 50 | overflow: hidden; 51 | pointer-events: none; 52 | } 53 | .container { 54 | margin: 0 auto; 55 | text-align: center; 56 | overflow: hidden; 57 | } 58 | .content { 59 | padding: 2em 1em 5em; 60 | z-index: 1; 61 | max-width: 1000px; 62 | margin: 0 auto; 63 | } 64 | .content h2 { 65 | margin: 0 0 2em; 66 | } 67 | .content p { 68 | margin: 1em 0; 69 | padding: 0 0 2em; 70 | font-size: 0.85em; 71 | } 72 | .box { 73 | padding: 4.5em 0; 74 | display: -webkit-flex; 75 | display: -ms-flexbox; 76 | display: flex; 77 | -webkit-flex-wrap: wrap; 78 | -ms-flex-wrap: wrap; 79 | flex-wrap: wrap; 80 | -webkit-justify-content: center; 81 | justify-content: center; 82 | } 83 | 84 | /* Header */ 85 | .codrops-header { 86 | padding: 3em 190px 4em; 87 | letter-spacing: -1px; 88 | } 89 | .codrops-header h1 { 90 | font-weight: 200; 91 | font-size: 4em; 92 | line-height: 1; 93 | margin-bottom: 0; 94 | } 95 | .codrops-header h1 span { 96 | display: block; 97 | font-size: 40%; 98 | letter-spacing: 0; 99 | padding: 0.5em 0 1em 0; 100 | color: #A8B3B8; 101 | } 102 | 103 | /* Top Navigation Style */ 104 | .codrops-links { 105 | position: relative; 106 | display: inline-block; 107 | white-space: nowrap; 108 | font-size: 1.25em; 109 | text-align: center; 110 | } 111 | .codrops-links::after { 112 | position: absolute; 113 | top: 0; 114 | left: 50%; 115 | width: 1px; 116 | height: 100%; 117 | background: #BFCACF; 118 | content: ''; 119 | -webkit-transform: rotate3d(0, 0, 1, 22.5deg); 120 | transform: rotate3d(0, 0, 1, 22.5deg); 121 | } 122 | .codrops-icon { 123 | display: inline-block; 124 | margin: 0.5em; 125 | padding: 0em 0; 126 | width: 1.5em; 127 | text-decoration: none; 128 | } 129 | .codrops-icon:before { 130 | margin: 0 5px; 131 | text-transform: none; 132 | font-weight: normal; 133 | font-style: normal; 134 | font-variant: normal; 135 | font-family: 'codropsicons'; 136 | line-height: 1; 137 | speak: none; 138 | -webkit-font-smoothing: antialiased; 139 | } 140 | .codrops-icon span { 141 | display: none; 142 | } 143 | .codrops-icon--drop:before { 144 | content: "\e001"; 145 | } 146 | .codrops-icon--prev:before { 147 | content: "\e004"; 148 | } 149 | 150 | /* Related demos */ 151 | .content--related { 152 | text-align: center; 153 | font-weight: 600; 154 | } 155 | .media-item { 156 | display: inline-block; 157 | padding: 1em; 158 | margin: 1em 0 0 0; 159 | vertical-align: top; 160 | -webkit-transition: color 0.3s; 161 | transition: color 0.3s; 162 | } 163 | .media-item__img { 164 | opacity: 0.8; 165 | max-width: 100%; 166 | -webkit-transition: opacity 0.3s; 167 | transition: opacity 0.3s; 168 | } 169 | .media-item:hover .media-item__img, 170 | .media-item:focus .media-item__img { 171 | opacity: 1; 172 | } 173 | .media-item__title { 174 | font-size: 0.85em; 175 | margin: 0; 176 | padding: 0.5em; 177 | } 178 | @media screen and (max-width:50em) { 179 | .codrops-header { 180 | padding: 3em 10% 4em; 181 | } 182 | } 183 | @media screen and (max-width:40em) { 184 | .codrops-header h1 { 185 | font-size: 2.8em; 186 | } 187 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/response/CommonResult.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.response; 2 | 3 | 4 | /** 5 | * @author Starix 6 | * @date 2019-09-28 7 | * @description: 8 | */ 9 | public class CommonResult { 10 | private long code; 11 | private String message; 12 | private T data; 13 | 14 | public CommonResult() { 15 | } 16 | 17 | public CommonResult(long code, String message, T data) { 18 | this.code = code; 19 | this.message = message; 20 | this.data = data; 21 | } 22 | 23 | public CommonResult(long code, String message) { 24 | this.code = code; 25 | this.message = message; 26 | } 27 | 28 | 29 | /** 30 | * 成功返回结果 31 | * 32 | * @param data 获取的数据 33 | */ 34 | public static CommonResult success(T data) { 35 | return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); 36 | } 37 | 38 | /** 39 | * 成功返回结果 40 | * 41 | * @param data 获取的数据 42 | * @param message 提示信息 43 | */ 44 | public static CommonResult success(T data, String message) { 45 | return new CommonResult(ResultCode.SUCCESS.getCode(), message, data); 46 | } 47 | 48 | /** 49 | * 成功返回结果 50 | */ 51 | public static CommonResult success() { 52 | return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage()); 53 | } 54 | 55 | /** 56 | * 成功返回结果 57 | */ 58 | public static CommonResult successMsg(String msg) { 59 | return new CommonResult(ResultCode.SUCCESS.getCode(), msg); 60 | } 61 | 62 | /*public static CommonResult success(String message) { 63 | return new CommonResult(ResultCode.SUCCESS.getCode(),ResultCode.SUCCESS.getMessage()); 64 | }*/ 65 | 66 | /** 67 | * 失败返回结果 68 | * 69 | * @param errorCode 错误码 70 | */ 71 | public static CommonResult failed(IErrorCode errorCode) { 72 | return new CommonResult(errorCode.getCode(), errorCode.getMessage(), null); 73 | } 74 | 75 | /** 76 | * 失败返回结果 77 | * 78 | * @param message 提示信息 79 | */ 80 | public static CommonResult failed(String message) { 81 | return new CommonResult(ResultCode.FAILED.getCode(), message, null); 82 | } 83 | 84 | /** 85 | * 失败返回结果,并自定义错误信息 86 | * @param errorCode 87 | * @param message 88 | * @return 89 | */ 90 | public static CommonResult failed(IErrorCode errorCode, String message) { 91 | return new CommonResult(errorCode.getCode(), message, null); 92 | } 93 | 94 | 95 | /** 96 | * 自定义返回类型 97 | * @param code 98 | * @param msg 99 | * @return 100 | */ 101 | public static CommonResult fail(Integer code, String msg) { 102 | return new CommonResult(code, msg); 103 | } 104 | /** 105 | * 失败返回结果 106 | */ 107 | public static CommonResult failed() { 108 | return failed(ResultCode.FAILED); 109 | } 110 | 111 | /** 112 | * 参数验证失败返回结果 113 | */ 114 | public static CommonResult validateFailed() { 115 | return failed(ResultCode.VALIDATE_FAILED); 116 | } 117 | 118 | /** 119 | * 参数验证失败返回结果 120 | * 121 | * @param message 提示信息 122 | */ 123 | public static CommonResult validateFailed(String message) { 124 | return new CommonResult(ResultCode.VALIDATE_FAILED.getCode(), message, null); 125 | } 126 | 127 | /** 128 | * 未登录返回结果 129 | */ 130 | public static CommonResult unauthorized(T data) { 131 | return new CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); 132 | } 133 | 134 | /** 135 | * 未授权返回结果 136 | */ 137 | public static CommonResult forbidden(T data) { 138 | return new CommonResult(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); 139 | } 140 | 141 | public long getCode() { 142 | return code; 143 | } 144 | 145 | public void setCode(long code) { 146 | this.code = code; 147 | } 148 | 149 | public String getMessage() { 150 | return message; 151 | } 152 | 153 | public void setMessage(String message) { 154 | this.message = message; 155 | } 156 | 157 | public T getData() { 158 | return data; 159 | } 160 | 161 | public void setData(T data) { 162 | this.data = data; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/exception/BaseExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.exception; 2 | 3 | import com.starix.gdou.response.CommonResult; 4 | import com.starix.gdou.response.ResultCode; 5 | import com.starix.gdou.utils.WxMessagePushUtil; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.http.HttpStatus; 9 | import org.springframework.http.converter.HttpMessageNotReadableException; 10 | import org.springframework.web.HttpMediaTypeNotSupportedException; 11 | import org.springframework.web.HttpRequestMethodNotSupportedException; 12 | import org.springframework.web.bind.MissingServletRequestParameterException; 13 | import org.springframework.web.bind.annotation.ControllerAdvice; 14 | import org.springframework.web.bind.annotation.ExceptionHandler; 15 | import org.springframework.web.bind.annotation.ResponseBody; 16 | import org.springframework.web.bind.annotation.ResponseStatus; 17 | import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; 18 | 19 | import javax.servlet.http.HttpServletRequest; 20 | 21 | import static com.starix.gdou.common.Constant.WX_PUSH_TOKEN_EXCEPTION; 22 | import static com.starix.gdou.common.Constant.WX_PUSH_TOKEN_MAIN_LOG; 23 | 24 | 25 | @ControllerAdvice 26 | public class BaseExceptionHandler { 27 | 28 | private static final Logger logger = LoggerFactory.getLogger(BaseExceptionHandler.class); 29 | 30 | //处理其它异常(异常兜底处理) 31 | @ExceptionHandler(Exception.class) 32 | @ResponseBody 33 | @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR) 34 | public CommonResult handlerAny(Exception e){ 35 | logger.error("系统内部异常:[{}]",e.getMessage(), e); 36 | WxMessagePushUtil.push(WX_PUSH_TOKEN_EXCEPTION, String.format("系统内部异常:%s", e.toString())); 37 | return CommonResult.failed(ResultCode.SERVER_ERROR,"服务器似乎遇到了一些问题,请稍后重试"); 38 | } 39 | 40 | //处理自定义的业务异常 41 | @ExceptionHandler(CustomException.class) 42 | @ResponseBody 43 | public CommonResult handlerCustomException(Exception e){ 44 | CustomException customException = (CustomException) e; 45 | CommonResult result = customException.getCommonResult(); 46 | logger.error("业务异常:[code:{},msg:{}]", result.getCode(), result.getMessage()); 47 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, 48 | String.format("出现业务异常:code:%s,msg:%s", result.getCode(), result.getMessage())); 49 | return result; 50 | } 51 | 52 | //处理"请求方法不支持"异常 53 | @ExceptionHandler(HttpRequestMethodNotSupportedException.class) 54 | @ResponseBody 55 | @ResponseStatus(value = HttpStatus.METHOD_NOT_ALLOWED) 56 | public CommonResult handlerMethodNotSupported(HttpServletRequest request, Exception e){ 57 | logger.error("不支持的请求方法:[{}]",request.getMethod()); 58 | return CommonResult.failed(ResultCode.VALIDATE_FAILED, "请求方法不支持"); 59 | } 60 | 61 | 62 | //处理"Content-Type不支持"异常 63 | @ExceptionHandler(HttpMediaTypeNotSupportedException.class) 64 | @ResponseBody 65 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 66 | public CommonResult handlerContentTypeSupported(HttpServletRequest request, Exception e){ 67 | logger.error("不支持的Content-Type:[{}]",request.getContentType()); 68 | return CommonResult.failed(ResultCode.VALIDATE_FAILED, "请求的Content-Type不支持"); 69 | } 70 | 71 | 72 | //处理"MessageNotReadable"异常 73 | @ExceptionHandler(HttpMessageNotReadableException.class) 74 | @ResponseBody 75 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 76 | public CommonResult handlerMessageNotReadable(Exception e){ 77 | logger.error("请求数据格式不正确:{}", e.getMessage()); 78 | return CommonResult.failed(ResultCode.VALIDATE_FAILED, "请求的数据格式不对"); 79 | } 80 | 81 | //处理"MethodArgumentTypeMismatchException"异常 82 | @ExceptionHandler(MethodArgumentTypeMismatchException.class) 83 | @ResponseBody 84 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 85 | public CommonResult handlerMethodArgumentTypeMismatch(Exception e){ 86 | logger.error("请求参数类型不正确:{}", e.getMessage()); 87 | return CommonResult.failed(ResultCode.VALIDATE_FAILED, "请求参数类型错误"); 88 | } 89 | 90 | //处理"MissingServletRequestParameterException"异常 91 | @ExceptionHandler(MissingServletRequestParameterException.class) 92 | @ResponseBody 93 | @ResponseStatus(value = HttpStatus.BAD_REQUEST) 94 | public CommonResult handlerMissingRequestParameter(Exception e){ 95 | logger.error("请求参数缺失:{}", e.getMessage()); 96 | return CommonResult.failed(ResultCode.VALIDATE_FAILED, "请求参数缺失"); 97 | } 98 | 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/service/impl/GdouJWScoreNotifyServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.service.impl; 2 | 3 | import com.starix.gdou.common.Constant; 4 | import com.starix.gdou.dto.LoginResultV2; 5 | import com.starix.gdou.dto.request.ScoreQueryRquestDTO; 6 | import com.starix.gdou.dto.response.ScoreQueryResponseDTO; 7 | import com.starix.gdou.entity.Student; 8 | import com.starix.gdou.exception.CustomException; 9 | import com.starix.gdou.repository.StudentRepository; 10 | import com.starix.gdou.response.CommonResult; 11 | import com.starix.gdou.service.GdouJWScoreNotifyService; 12 | import com.starix.gdou.service.GdouJWServiceV2; 13 | import com.starix.gdou.utils.WxMessagePushUtil; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.data.redis.core.RedisTemplate; 17 | import org.springframework.data.redis.core.StringRedisTemplate; 18 | import org.springframework.stereotype.Service; 19 | 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Map; 23 | 24 | import static com.starix.gdou.common.Constant.WX_PUSH_TOKEN_MAIN_LOG; 25 | 26 | /** 27 | * @author Starix 28 | * @date 2020-07-17 21:59 29 | */ 30 | @Service 31 | @Slf4j 32 | public class GdouJWScoreNotifyServiceImpl implements GdouJWScoreNotifyService{ 33 | 34 | @Autowired 35 | private StudentRepository studentRepository; 36 | @Autowired 37 | private RedisTemplate redisTemplate; 38 | @Autowired 39 | private StringRedisTemplate stringRedisTemplate; 40 | @Autowired 41 | private GdouJWServiceV2 gdouJWService; 42 | 43 | @Override 44 | public void enableNotify(String openid, String email) throws Exception { 45 | Student student = studentRepository.findByOpenid(openid); 46 | if (student == null){ 47 | throw new CustomException(CommonResult.failed("未绑定学号")); 48 | } 49 | if (student.getNotifyStatus() == 1){ 50 | throw new CustomException(CommonResult.failed("你已经开启过成绩通知了")); 51 | } 52 | LoginResultV2 loginResult = gdouJWService.login(student.getUsername(), student.getPassword()); 53 | // YearOptionListResponseDTO yearOptionList = gdouJWService.getSocreYearOptionList(loginResult.getCookies()); 54 | // 成绩通知使用自定义的年份学期,不使用官网当前默认的年份学期 55 | String yearSemester = stringRedisTemplate.opsForValue().get("notify:year_semester"); 56 | String selectedYear = yearSemester.split("-")[0]; 57 | String selectedSemester = yearSemester.split("-")[1]; 58 | ScoreQueryRquestDTO scoreQueryRquestDTO = ScoreQueryRquestDTO.builder() 59 | .cookies(loginResult.getCookies()) 60 | .year(selectedYear) 61 | .semester(selectedSemester) 62 | .build(); 63 | List scoreList = gdouJWService.queryScore(scoreQueryRquestDTO); 64 | 65 | //查询当前成绩并存入redis,下次查询做对比 66 | redisTemplate.opsForValue().set(Constant.SCORE_NOTIFY_REDIS_KEY_PREFIX + loginResult.getUsername(), scoreList); 67 | 68 | //更新用户状态 69 | student.setEmail(email); 70 | student.setNotifyStatus(1); 71 | studentRepository.save(student); 72 | log.info("开启成绩更新通知成功, openid: {}, email: {}, username: {}", openid, email, student.getUsername()); 73 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, 74 | String.format("开启成绩更新通知成功, openid: %s, email: %s, username: %s", 75 | openid, email, student.getUsername())); 76 | } 77 | 78 | @Override 79 | public void disableNotify(String openid) { 80 | Student student = studentRepository.findByOpenid(openid); 81 | if (student == null){ 82 | throw new CustomException(CommonResult.failed("未绑定学号")); 83 | } 84 | redisTemplate.delete(Constant.SCORE_NOTIFY_REDIS_KEY_PREFIX + student.getUsername()); 85 | student.setNotifyStatus(0); 86 | studentRepository.save(student); 87 | log.info("[{}]关闭成绩更新通知成功", student.getUsername()); 88 | 89 | } 90 | 91 | @Override 92 | public Map queryNotifyStatus(String openid) { 93 | Student student = studentRepository.findByOpenid(openid); 94 | if (student == null){ 95 | throw new CustomException(CommonResult.failed("未绑定学号")); 96 | } 97 | Map map = new HashMap<>(); 98 | map.put("status", student.getNotifyStatus()); 99 | map.put("email", student.getEmail()); 100 | return map; 101 | } 102 | 103 | @Override 104 | public String queryNotifyYearSemester() { 105 | return (String) redisTemplate.opsForValue().get("notify:year_semester"); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/resources/static/score-notify.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 成绩更新通知 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 19 |
20 |
21 | 22 | 23 |
24 |
25 |
提示
26 |

开启成功!成绩如有更新会自动发送通知邮件到你的邮箱!

27 |
28 |
29 |
提示
30 |

请填写正确的邮箱地址

31 |
32 |
开启通知
33 |
34 |
35 |
36 | 37 | 38 | 39 | 134 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/service/impl/CpdailyUserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.service.impl; 2 | 3 | import com.starix.gdou.entity.CpdailyUser; 4 | import com.starix.gdou.exception.CustomException; 5 | import com.starix.gdou.repository.CpdailyUserRepository; 6 | import com.starix.gdou.response.CommonResult; 7 | import com.starix.gdou.service.CpdailyUserService; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.beans.BeanUtils; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.stereotype.Service; 13 | import org.springframework.transaction.annotation.Transactional; 14 | 15 | import java.io.BufferedReader; 16 | import java.io.File; 17 | import java.io.InputStream; 18 | import java.io.InputStreamReader; 19 | import java.util.ArrayList; 20 | import java.util.Date; 21 | import java.util.List; 22 | 23 | /** 24 | * @author Starix 25 | * @date 2020-04-06 13:28 26 | */ 27 | @Service 28 | @Slf4j 29 | public class CpdailyUserServiceImpl implements CpdailyUserService { 30 | 31 | //python脚本绝对路径 32 | @Value("${python.path}") 33 | private String PYTHON_PATH_AUTH; 34 | //python脚本命令行输出的编码 35 | @Value("${python.output-encoding}") 36 | private String OUTPUT_ENCODING; 37 | 38 | @Autowired 39 | private CpdailyUserRepository userRepository; 40 | 41 | @Override 42 | @Transactional 43 | public void saveUser(CpdailyUser user) throws Exception { 44 | log.info("[{}]正在添加用户", user.getUsername()); 45 | CpdailyUser cpdailyUser = userRepository.findByUsername(user.getUsername()); 46 | if (cpdailyUser != null){ 47 | throw new CustomException(CommonResult.failed("该账号已经提交过了")); 48 | } 49 | boolean result = authentication(user.getUsername(), user.getPassword()); 50 | if (!result){ 51 | throw new CustomException(CommonResult.failed("用户名或密码错误,请注意查看推文中关于账号密码的说明")); 52 | } 53 | user.setCreateTime(new Date()); 54 | userRepository.save(user); 55 | log.info("[{}]用户添加成功", user.getUsername()); 56 | } 57 | 58 | @Override 59 | @Transactional 60 | public void deleteUser(String username, String password) throws Exception { 61 | log.info("[{}]正在取消自动签到", username); 62 | CpdailyUser cpdailyUser = userRepository.findByUsername(username); 63 | if (cpdailyUser == null){ 64 | throw new CustomException(CommonResult.failed("用户记录不存在或已经取消自动签到")); 65 | } 66 | boolean result = authentication(username, password); 67 | if (!result){ 68 | throw new CustomException(CommonResult.failed("用户名或密码错误")); 69 | } 70 | userRepository.deleteByUsername(username); 71 | log.info("[{}]取消自动签到成功", username); 72 | } 73 | 74 | @Override 75 | @Transactional 76 | public void updateUser(CpdailyUser user) throws Exception { 77 | log.info("[{}]正在更新签到信息", user.getUsername()); 78 | CpdailyUser cpdailyUser = userRepository.findByUsername(user.getUsername()); 79 | if (cpdailyUser == null){ 80 | throw new CustomException(CommonResult.failed("修改失败,该账号未提交过")); 81 | } 82 | boolean result = authentication(user.getUsername(), user.getPassword()); 83 | if (!result){ 84 | throw new CustomException(CommonResult.failed("用户名或密码错误")); 85 | } 86 | BeanUtils.copyProperties(user, cpdailyUser, "id", "createTime"); 87 | cpdailyUser.setUpdateTime(new Date()); 88 | userRepository.save(cpdailyUser); 89 | log.info("[{}]签到信息更新成功", user.getUsername()); 90 | } 91 | 92 | /** 93 | * 用户认证,确保用户名和密码正确 94 | * @param username 95 | * @param password 96 | */ 97 | private boolean authentication(String username, String password) throws Exception { 98 | String[] cmd = new String[6]; 99 | cmd[0] = "python"; 100 | cmd[1] = "cpdaily_auth.py"; 101 | cmd[2] = "--username"; 102 | cmd[3] = username; 103 | cmd[4] = "--password"; 104 | cmd[5] = password; 105 | Process process = Runtime.getRuntime().exec(cmd, null, new File(PYTHON_PATH_AUTH)); 106 | int status = process.waitFor(); 107 | if (status != 0){ 108 | log.error("Python脚本执行出错,status:{}", status); 109 | throw new CustomException(CommonResult.failed("操作失败,请稍后重试或在公众号后台留言反馈")); 110 | } 111 | InputStream in = process.getInputStream(); 112 | BufferedReader buffReader = new BufferedReader(new InputStreamReader(in, OUTPUT_ENCODING)); 113 | String line; 114 | List resultLines = new ArrayList<>(); 115 | while ((line = buffReader.readLine()) != null){ 116 | resultLines.add(line); 117 | } 118 | //简单关闭流,暂时不考虑异常 119 | buffReader.close(); 120 | in.close(); 121 | 122 | if (resultLines.contains("result-认证失败")){ 123 | log.info("[{}]认证失败", username); 124 | return false; 125 | }else if (resultLines.contains("result-认证成功")){ 126 | log.info("[{}]认证成功", username); 127 | return true; 128 | }else { 129 | log.info("[{}]认证过程出现异常,Python输出内容:{}", username, resultLines); 130 | throw new CustomException(CommonResult.failed("操作失败,请稍后重试或在公众号后台留言反馈")); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/controller/GdouJWController.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.controller; 2 | 3 | /** 4 | * v1版本controller 5 | * @author Starix 6 | * @date 2019-11-18 18:56 7 | */ 8 | // @RestController 9 | // @CrossOrigin 10 | // @Slf4j 11 | // public class GdouJWController { 12 | // 13 | // @Autowired 14 | // private GdouJWService gdouJWService; 15 | // @Autowired 16 | // private UserBindService userBindService; 17 | // 18 | // @PostMapping("/login") 19 | // public CommonResult doLogin(String xh, String password, HttpSession httpSession) throws Exception { 20 | // if (StringUtils.isEmpty(xh) || StringUtils.isEmpty(password)){ 21 | // return CommonResult.failed(ResultCode.VALIDATE_FAILED); 22 | // } 23 | // LoginResult loginResult = gdouJWService.login(xh, password); 24 | // 25 | // httpSession.setAttribute("studentLoginInfo", loginResult); 26 | // 27 | // return CommonResult.success(); 28 | // } 29 | // 30 | // 31 | // @PostMapping("/autoLogin") 32 | // public CommonResult doAuoLogin(String openid, HttpSession httpSession) throws Exception { 33 | // if (StringUtils.isEmpty(openid)){ 34 | // return CommonResult.failed(ResultCode.VALIDATE_FAILED); 35 | // } 36 | // 37 | // LoginResult loginResult = gdouJWService.loginByOpenid(openid); 38 | // 39 | // httpSession.setAttribute("studentLoginInfo", loginResult); 40 | // 41 | // return CommonResult.success(); 42 | // } 43 | // 44 | // 45 | // 46 | // @PostMapping("/bind") 47 | // public CommonResult doBind(String openid, String xh, String password) throws Exception { 48 | // if (StringUtils.isEmpty(openid) || StringUtils.isEmpty(xh) || StringUtils.isEmpty(password)){ 49 | // return CommonResult.failed(ResultCode.VALIDATE_FAILED); 50 | // } 51 | // 52 | // log.info("[{}]正在绑定学号",xh); 53 | // 54 | // // 绑定前先验证账号密码是否正确 55 | // gdouJWService.login(xh, password); 56 | // 57 | // userBindService.bind(openid, xh, password); 58 | // 59 | // return CommonResult.success(); 60 | // } 61 | // 62 | // 63 | // @GetMapping("/queryScore") 64 | // public CommonResult doQueryScore(String year, String semester, HttpSession httpSession) throws Exception { 65 | // if (StringUtils.isEmpty(year) || StringUtils.isEmpty(semester)){ 66 | // return CommonResult.failed(ResultCode.VALIDATE_FAILED); 67 | // } 68 | // 69 | // LoginResult loginResult = (LoginResult) httpSession.getAttribute("studentLoginInfo"); 70 | // 71 | // if (loginResult == null){ 72 | // return CommonResult.failed(ResultCode.UNAUTHORIZED,"你还没有登录或者登录信息已经过期"); 73 | // } 74 | // 75 | // log.info("[{}]正在查询成绩",loginResult.getXh()); 76 | // List scoreList = gdouJWService.getScore(loginResult, year, semester); 77 | // 78 | // return CommonResult.success(scoreList); 79 | // } 80 | // 81 | // 82 | // @GetMapping("/queryExam") 83 | // public CommonResult doQueryExam(String year, String semester, HttpSession httpSession) throws Exception { 84 | // 85 | // if (StringUtils.isEmpty(year) || StringUtils.isEmpty(semester)){ 86 | // return CommonResult.failed(ResultCode.VALIDATE_FAILED); 87 | // } 88 | // 89 | // LoginResult loginResult = (LoginResult) httpSession.getAttribute("studentLoginInfo"); 90 | // 91 | // if (loginResult == null){ 92 | // return CommonResult.failed(ResultCode.UNAUTHORIZED,"你还没有登录或者登录信息已经过期"); 93 | // } 94 | // 95 | // log.info("[{}]正在查询考试", loginResult.getXh()); 96 | // List scoreList = gdouJWService.getExam(loginResult, year, semester); 97 | // 98 | // return CommonResult.success(scoreList); 99 | // } 100 | // 101 | // 102 | // 103 | // 104 | // //获得查成绩页面年份下拉列表数据 105 | // @GetMapping("/getScoreYearOptionsList") 106 | // public CommonResult doGetScoreYearOptionsList(HttpSession httpSession) throws Exception { 107 | // LoginResult loginResult = (LoginResult) httpSession.getAttribute("studentLoginInfo"); 108 | // if (loginResult == null){ 109 | // return CommonResult.failed(ResultCode.UNAUTHORIZED,"你还没有登录或者登录信息已经过期"); 110 | // } 111 | // List yearOptionsList = gdouJWService.getSocreYearOptionsList(loginResult); 112 | // return CommonResult.success(yearOptionsList); 113 | // } 114 | // 115 | // 116 | // //获得查考试页面年份下拉列表数据 117 | // @GetMapping("/getExamYearOptionsList") 118 | // public CommonResult doGetExamYearOptionsList(HttpSession httpSession) throws Exception { 119 | // LoginResult loginResult = (LoginResult) httpSession.getAttribute("studentLoginInfo"); 120 | // if (loginResult == null){ 121 | // return CommonResult.failed(ResultCode.UNAUTHORIZED,"你还没有登录或者登录信息已经过期"); 122 | // } 123 | // List yearOptionsList = gdouJWService.getExamYearOptionsList(loginResult); 124 | // return CommonResult.success(yearOptionsList); 125 | // } 126 | // 127 | // 128 | // //自动评教 129 | // @PostMapping("/autoEvaluate") 130 | // public CommonResult doAutoEval(String xh, String password, String content, Integer mode) throws Exception { 131 | // LoginResult loginResult = gdouJWService.login(xh, password); 132 | // log.info("[{}]正在自动评教,参数-->[password]:{},[content]:{},[mode]:{}",xh,password,content,mode); 133 | // gdouJWService.autoEvaluate(loginResult,content, mode); 134 | // return CommonResult.success(); 135 | // } 136 | // 137 | // } 138 | -------------------------------------------------------------------------------- /src/main/resources/static/exam-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 考试查询 6 | 7 | 8 | 9 | 10 | 11 | 12 | 24 | 25 |

考试快捷查询

26 |
27 |
28 | 学年: 29 | 33 | 学期: 34 | 38 |
39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
课程名姓名考试时间考试地点
59 | 60 |
61 | 62 | 63 | 148 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/controller/GdouJWControllerV2.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.controller; 2 | 3 | import com.starix.gdou.dto.LoginResultV2; 4 | import com.starix.gdou.dto.request.ExamQueryRquestDTO; 5 | import com.starix.gdou.dto.request.ScoreQueryRquestDTO; 6 | import com.starix.gdou.dto.response.ExamQueryResponseDTO; 7 | import com.starix.gdou.dto.response.ScoreQueryResponseDTO; 8 | import com.starix.gdou.dto.response.YearOptionListResponseDTO; 9 | import com.starix.gdou.response.CommonResult; 10 | import com.starix.gdou.response.ResultCode; 11 | import com.starix.gdou.service.GdouJWServiceV2; 12 | import com.starix.gdou.utils.WxMessagePushUtil; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.util.StringUtils; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | import javax.servlet.http.HttpSession; 19 | import java.util.List; 20 | 21 | import static com.starix.gdou.common.Constant.WX_PUSH_TOKEN_MAIN_LOG; 22 | 23 | /** 24 | * v2版本controller 25 | * @author shiwenjie 26 | * @created 2020/7/1 2:45 下午 27 | */ 28 | @RestController 29 | @RequestMapping("/jw") 30 | @CrossOrigin 31 | @Slf4j 32 | public class GdouJWControllerV2 { 33 | 34 | @Autowired 35 | private GdouJWServiceV2 gdouJWService; 36 | 37 | @GetMapping("/test") 38 | public CommonResult test() throws Exception { 39 | throw new RuntimeException("测试异常"); 40 | } 41 | 42 | @PostMapping("/login") 43 | public CommonResult doLogin(String username, String password, HttpSession httpSession) throws Exception { 44 | if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){ 45 | return CommonResult.failed(ResultCode.VALIDATE_FAILED); 46 | } 47 | log.info("[{}]用户登录", username); 48 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, String.format("[%s]用户登录", username)); 49 | LoginResultV2 loginResult = gdouJWService.login(username, password); 50 | httpSession.setAttribute("jw_cookie", loginResult); 51 | return CommonResult.success(); 52 | } 53 | 54 | 55 | @PostMapping("/autoLogin") 56 | public CommonResult doAuoLogin(String openid, HttpSession httpSession) throws Exception { 57 | if (StringUtils.isEmpty(openid)){ 58 | return CommonResult.failed(ResultCode.VALIDATE_FAILED); 59 | } 60 | LoginResultV2 loginResult = gdouJWService.loginByOpenid(openid); 61 | httpSession.setAttribute("jw_cookie", loginResult); 62 | return CommonResult.success(); 63 | } 64 | 65 | 66 | @GetMapping("/queryScore") 67 | public CommonResult doQueryScore(String year, String semester, HttpSession httpSession) throws Exception { 68 | LoginResultV2 loginResult = (LoginResultV2) httpSession.getAttribute("jw_cookie"); 69 | if (loginResult == null) { 70 | return CommonResult.failed(ResultCode.UNAUTHORIZED,"你还没有登录或者登录信息已经过期"); 71 | } 72 | log.info("[{}]查询成绩, 学年: {}, 学期: {}", loginResult.getUsername(), year, semester); 73 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, 74 | String.format("[%s]查询成绩, 学年: %s, 学期: %s", loginResult.getUsername(), year, semester)); 75 | ScoreQueryRquestDTO scoreQueryRquestDTO = ScoreQueryRquestDTO.builder() 76 | .cookies(loginResult.getCookies()) 77 | .year(year) 78 | .semester(semester) 79 | .build(); 80 | List resultList = gdouJWService.queryScore(scoreQueryRquestDTO); 81 | return CommonResult.success(resultList); 82 | } 83 | 84 | 85 | @GetMapping("/queryExam") 86 | public CommonResult doQueryExam(String year, String semester, HttpSession httpSession) throws Exception { 87 | LoginResultV2 loginResult = (LoginResultV2) httpSession.getAttribute("jw_cookie"); 88 | if (loginResult == null){ 89 | return CommonResult.failed(ResultCode.UNAUTHORIZED,"你还没有登录或者登录信息已经过期"); 90 | } 91 | log.info("[{}]查询考试, 学年: {}, 学期: {}", loginResult.getUsername(), year, semester); 92 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, 93 | String.format("[%s]查询考试, 学年: %s, 学期: %s", loginResult.getUsername(), year, semester)); 94 | ExamQueryRquestDTO examQueryRquestDTO = ExamQueryRquestDTO.builder() 95 | .cookies(loginResult.getCookies()) 96 | .year(year) 97 | .semester(semester) 98 | .build(); 99 | ExamQueryResponseDTO examQueryResponseDTO = gdouJWService.queryExam(examQueryRquestDTO); 100 | return CommonResult.success(examQueryResponseDTO); 101 | } 102 | 103 | 104 | 105 | 106 | //获得查成绩页面年份下拉列表数据 107 | @GetMapping("/getScoreYearOptionsList") 108 | public CommonResult doGetScoreYearOptionsList(HttpSession httpSession) throws Exception { 109 | LoginResultV2 loginResult = (LoginResultV2) httpSession.getAttribute("jw_cookie"); 110 | if (loginResult == null){ 111 | return CommonResult.failed(ResultCode.UNAUTHORIZED,"你还没有登录或者登录信息已经过期"); 112 | } 113 | YearOptionListResponseDTO yearOptionListResponseDTO = gdouJWService.getSocreYearOptionList(loginResult.getCookies()); 114 | return CommonResult.success(yearOptionListResponseDTO); 115 | } 116 | 117 | 118 | //获得查考试页面年份下拉列表数据 119 | @GetMapping("/getExamYearOptionsList") 120 | public CommonResult doGetExamYearOptionsList(HttpSession httpSession) throws Exception { 121 | LoginResultV2 loginResult = (LoginResultV2) httpSession.getAttribute("jw_cookie"); 122 | if (loginResult == null){ 123 | return CommonResult.failed(ResultCode.UNAUTHORIZED,"你还没有登录或者登录信息已经过期"); 124 | } 125 | List yearOptionsList = gdouJWService.getExamYearOptionList(loginResult.getCookies()); 126 | return CommonResult.success(yearOptionsList); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/task/ScoreNotifyAsyncTask.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.task; 2 | 3 | import com.jfinal.kit.Kv; 4 | import com.starix.gdou.common.Constant; 5 | import com.starix.gdou.dto.LoginResultV2; 6 | import com.starix.gdou.dto.ScoreNotifyDTO; 7 | import com.starix.gdou.dto.request.ScoreQueryRquestDTO; 8 | import com.starix.gdou.dto.response.ScoreQueryResponseDTO; 9 | import com.starix.gdou.entity.Student; 10 | import com.starix.gdou.repository.StudentRepository; 11 | import com.starix.gdou.service.GdouJWServiceV2; 12 | import com.starix.gdou.utils.HtmlMailRenderUtil; 13 | import com.starix.gdou.utils.MailUtil; 14 | import com.starix.gdou.utils.WxMessagePushUtil; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.springframework.beans.BeanUtils; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.data.redis.core.RedisTemplate; 19 | import org.springframework.data.redis.core.StringRedisTemplate; 20 | import org.springframework.scheduling.annotation.Async; 21 | import org.springframework.scheduling.annotation.AsyncResult; 22 | import org.springframework.stereotype.Component; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | import java.util.concurrent.Future; 27 | 28 | import static com.starix.gdou.common.Constant.WX_PUSH_TOKEN_EXCEPTION; 29 | import static com.starix.gdou.common.Constant.WX_PUSH_TOKEN_MAIN_LOG; 30 | 31 | /** 32 | * @author Starix 33 | * @date 2020-07-18 12:13 34 | */ 35 | @Slf4j 36 | @Component 37 | public class ScoreNotifyAsyncTask { 38 | 39 | @Autowired 40 | private StudentRepository studentRepository; 41 | @Autowired 42 | private GdouJWServiceV2 gdouJWService; 43 | @Autowired 44 | private RedisTemplate redisTemplate; 45 | @Autowired 46 | private StringRedisTemplate stringRedisTemplate; 47 | @Autowired 48 | private MailUtil mailUtil; 49 | 50 | @Async 51 | public Future checkScoreUpdateAndNotify() throws Exception { 52 | List studentList = studentRepository.findAllByNotifyStatus(1); 53 | for (Student student : studentList) { 54 | LoginResultV2 loginResult = gdouJWService.login(student.getUsername(), student.getPassword()); 55 | // YearOptionListResponseDTO yearOptionList = gdouJWService.getSocreYearOptionList(loginResult.getCookies()); 56 | // 成绩通知使用自定义的年份学期,不使用官网当前默认的年份学期 57 | String yearSemester = stringRedisTemplate.opsForValue().get("notify:year_semester"); 58 | String selectedYear = yearSemester.split("-")[0]; 59 | String selectedSemester = yearSemester.split("-")[1]; 60 | 61 | ScoreQueryRquestDTO scoreQueryRquestDTO = ScoreQueryRquestDTO.builder() 62 | .cookies(loginResult.getCookies()) 63 | .year(selectedYear) 64 | .semester(selectedSemester) 65 | .build(); 66 | //当前成绩 67 | List currentScoreDTOList = gdouJWService.queryScore(scoreQueryRquestDTO); 68 | //上一次查询的成绩 69 | List oldScoreDTOList = (List) redisTemplate.opsForValue() 70 | .get(Constant.SCORE_NOTIFY_REDIS_KEY_PREFIX + student.getUsername()); 71 | if (oldScoreDTOList.size() != currentScoreDTOList.size()){ 72 | List scoreNotifyDTOList = 73 | buildScoreNotifyDTOList(student.getUsername(), currentScoreDTOList, oldScoreDTOList); 74 | Kv emailData = Kv.create() 75 | .set("scoreList", scoreNotifyDTOList) 76 | .set("username", student.getUsername()) 77 | .set("year", selectedYear) 78 | .set("semester", selectedSemester) 79 | .set("queryMore", Constant.SCORE_QUERY_URL + "?openid=" + student.getOpenid()); 80 | //发送邮件 81 | sendNotifyEmail(student.getUsername(), student.getEmail(), emailData); 82 | }else { 83 | log.info("[{}]成绩未更新", student.getUsername()); 84 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, String.format("[%s]成绩未更新", student.getUsername())); 85 | } 86 | } 87 | return new AsyncResult<>("任务执行完毕"); 88 | } 89 | 90 | 91 | private List buildScoreNotifyDTOList(String username, List currentScoreDTOList, List oldScoreDTOList){ 92 | List scoreNotifyDTOList = new ArrayList<>(); 93 | for (ScoreQueryResponseDTO currentScoreDTO : currentScoreDTOList) { 94 | ScoreNotifyDTO scoreNotifyDTO = new ScoreNotifyDTO(); 95 | BeanUtils.copyProperties(currentScoreDTO, scoreNotifyDTO); 96 | if (!oldScoreDTOList.contains(currentScoreDTO)){ 97 | log.info("[{}]成绩更新-{}", username, currentScoreDTO.getCourseName()); 98 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, String.format("[%s]成绩更新-%s", username, currentScoreDTO.getCourseName())); 99 | scoreNotifyDTO.setNew(true); 100 | } 101 | scoreNotifyDTOList.add(scoreNotifyDTO); 102 | } 103 | //更新redis 104 | redisTemplate.opsForValue().set(Constant.SCORE_NOTIFY_REDIS_KEY_PREFIX + username, currentScoreDTOList); 105 | return scoreNotifyDTOList; 106 | } 107 | 108 | private void sendNotifyEmail(String username, String email, Kv emailData){ 109 | String html = HtmlMailRenderUtil.render("templates/score-email-template.html", emailData); 110 | try { 111 | mailUtil.sendHtmlMail(email, "成绩更新通知", html); 112 | log.info("[{}]发送通知邮件给{}成功", username, email); 113 | WxMessagePushUtil.push(WX_PUSH_TOKEN_MAIN_LOG, String.format("[%s]发送通知邮件给%s成功", username, email)); 114 | } catch (Exception e) { 115 | log.error("[{}]发送通知邮件给{}失败", username, email, e); 116 | WxMessagePushUtil.push(WX_PUSH_TOKEN_EXCEPTION, String.format("[%s]发送通知邮件给%s失败:%s", username, email, e.toString())); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/main/resources/python/slide_captcha_pass.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import json 3 | import re 4 | import time 5 | from urllib.parse import urlencode 6 | import cv2 7 | import numpy 8 | import requests 9 | import execjs 10 | 11 | 12 | class CaptchaProcessor(object): 13 | def __init__(self): 14 | self.session = requests.session() 15 | self.rtk = '' 16 | 17 | def get_img_url(self): 18 | # 获取JS中的rtk参数 19 | url = 'https://jw.gdou.edu.cn/zfcaptchaLogin' 20 | params = { 21 | 'type': 'resource', 22 | 'name': 'zfdun_captcha.js', 23 | 'instanceId': 'zfcaptchaLogin' 24 | } 25 | resp = self.session.get(url, params=params) 26 | rtk = re.search("rtk:'(.*)',", resp.text).group(1) 27 | self.rtk = rtk 28 | # 获取图片url 29 | params = { 30 | 'type': 'refresh', 31 | 'rtk': rtk, 32 | 'time': int(round(time.time() * 1000)), 33 | 'instanceId': 'zfcaptchaLogin' 34 | } 35 | resp = self.session.get(url, params=params) 36 | json = resp.json() 37 | img_params = { 38 | 'type': 'image', 39 | 'id': json['mi'], 40 | 'imtk': json['imtk'], 41 | 't': int(round(time.time() * 1000)), 42 | 'instanceId': 'zfcaptchaLogin' 43 | } 44 | slide_url = '%s?%s' % (url, urlencode(img_params)) 45 | img_params['id'] = json['si'] 46 | back_url = '%s?%s' % (url, urlencode(img_params)) 47 | return slide_url, back_url 48 | 49 | def get_img_bytes(self, url): 50 | resp = self.session.get(url) 51 | return resp.content 52 | 53 | # 清除滑块图片空白区域 54 | @staticmethod 55 | def clear_white(img): 56 | rows, cols, channel = img.shape 57 | min_x = 255 58 | min_y = 255 59 | max_x = 0 60 | max_y = 0 61 | for x in range(1, rows): 62 | for y in range(1, cols): 63 | t = set(img[x, y]) 64 | if len(t) >= 2: 65 | if x <= min_x: 66 | min_x = x 67 | elif x >= max_x: 68 | max_x = x 69 | 70 | if y <= min_y: 71 | min_y = y 72 | elif y >= max_y: 73 | max_y = y 74 | img1 = img[min_x:max_x, min_y: max_y] 75 | return img1 76 | 77 | # 边缘检测 78 | @staticmethod 79 | def image_edge_detection(img): 80 | edges = cv2.Canny(img, 100, 200) 81 | return edges 82 | 83 | # template match 84 | def template_match(self, tpl, target): 85 | result = cv2.matchTemplate(target, tpl, cv2.TM_CCOEFF_NORMED) 86 | # 寻找矩阵(一维数组当作向量,用Mat定义) 中最小匹配概率和最大匹配概率的位置 87 | min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) 88 | tl = max_loc 89 | # 输出横坐标, 即 滑块缺口在图片上的位置 90 | return tl[0] 91 | 92 | def detect_distance(self): 93 | slide_url, back_url = self.get_img_url() 94 | # 滑块图片 95 | slide_bytes = self.get_img_bytes(slide_url) 96 | slide = cv2.imdecode(numpy.frombuffer(slide_bytes, numpy.uint8), cv2.IMREAD_COLOR) 97 | slide = self.clear_white(slide) 98 | slide = cv2.cvtColor(slide, cv2.COLOR_RGB2GRAY) 99 | slide = self.image_edge_detection(slide) 100 | 101 | # 背景图片 102 | back_bytes = self.get_img_bytes(back_url) 103 | back = cv2.imdecode(numpy.frombuffer(back_bytes, numpy.uint8), cv2.IMREAD_GRAYSCALE) 104 | back = self.image_edge_detection(back) 105 | 106 | slide_pic = cv2.cvtColor(slide, cv2.COLOR_GRAY2RGB) 107 | back_pic = cv2.cvtColor(back, cv2.COLOR_GRAY2RGB) 108 | x = self.template_match(slide_pic, back_pic) 109 | return x 110 | 111 | # 移动轨迹生成 112 | def movement_track_generate(self, distance): 113 | v = 0 114 | t = 8 115 | now = int(round(time.time() * 1000)) 116 | start_time = now 117 | track = [] 118 | current = 0 119 | threshold = distance * 2 / 5 # 减速距离阀值 120 | while current < distance: 121 | if current < threshold: 122 | a = 0.01 # 低于减速阈值,加速度为0.01 123 | else: 124 | a = -0.007 # 达到减速阈值,加速度为-0.007 125 | s = v * t + 0.5 * a * (t ** 2) 126 | v = v + a * t 127 | current += round(s) 128 | location = { 129 | # 一般来说不会从屏幕0位置开始滑,所以统一加800,让轨迹更接近真实 130 | 'x': current + 800, 131 | # 服务端只校验x坐标,y坐标可以任意 132 | 'y': 483, 133 | 't': now, 134 | } 135 | track.append(location) 136 | # 每8ms一个记录 137 | now += 8 138 | 139 | # 轨迹参数加密,调原JS代码中的加密方法 140 | encoded_track = execjs.compile(open('encode.js').read()).call('ef', json.dumps(track)) 141 | return encoded_track 142 | 143 | # 提交验证,返回验证成功后的Cookie 144 | def submit(self, track): 145 | url = 'https://jw.gdou.edu.cn/zfcaptchaLogin' 146 | params = { 147 | 'type': 'verify', 148 | 'rtk': self.rtk, 149 | 'time': int(round(time.time() * 1000)), 150 | 'mt': track, 151 | 'instanceId': 'zfcaptchaLogin', 152 | 'extend': 'eyJhcHBOYW1lIjoiTmV0c2NhcGUiLCJ1c2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvODguMC40MzI0LjEwNCBTYWZhcmkvNTM3LjM2IiwiYXBwVmVyc2lvbiI6IjUuMCAoV2luZG93cyBOVCAxMC4wOyBXaW42NDsgeDY0KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvODguMC40MzI0LjEwNCBTYWZhcmkvNTM3LjM2In0=', 153 | } 154 | resp = self.session.post(url, data=params) 155 | return resp.json()['status'] 156 | 157 | 158 | if __name__ == "__main__": 159 | p = CaptchaProcessor() 160 | # 识别失败最大重试次数 161 | retry = 5 162 | while True: 163 | x = p.detect_distance() 164 | track = p.movement_track_generate(x) 165 | status = p.submit(track) 166 | if status == 'success': 167 | # 返回cookie 168 | for cookie in p.session.cookies: 169 | print(cookie.name + "=" + cookie.value) 170 | break 171 | if retry <= 0: 172 | exit(1) 173 | retry -= 1 174 | -------------------------------------------------------------------------------- /src/main/resources/static/score-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 成绩查询 6 | 7 | 8 | 9 | 10 | 11 | 12 | 24 | 25 |

成绩快捷查询

26 |
27 |
28 | 学年: 29 | 33 | 学期: 34 | 39 |
40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
课程名称成绩课程性质学分
60 | 61 |
62 | 63 | 64 | 159 | -------------------------------------------------------------------------------- /src/main/resources/static/css/default.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src:url('../fonts/icomoon.eot?rretjt'); 4 | src:url('../fonts/icomoon.eot?#iefixrretjt') format('embedded-opentype'), 5 | url('../fonts/icomoon.woff?rretjt') format('woff'), 6 | url('../fonts/icomoon.ttf?rretjt') format('truetype'), 7 | url('../fonts/icomoon.svg?rretjt#icomoon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | font-family: 'icomoon'; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | 21 | /* Better Font Rendering =========== */ 22 | -webkit-font-smoothing: antialiased; 23 | -moz-osx-font-smoothing: grayscale; 24 | } 25 | 26 | body, html { font-size: 100%; padding: 0; margin: 0;} 27 | 28 | /* Reset */ 29 | *, 30 | *:after, 31 | *:before { 32 | -webkit-box-sizing: border-box; 33 | -moz-box-sizing: border-box; 34 | box-sizing: border-box; 35 | } 36 | 37 | /* Clearfix hack by Nicolas Gallagher: http://nicolasgallagher.com/micro-clearfix-hack/ */ 38 | .clearfix:before, 39 | .clearfix:after { 40 | content: " "; 41 | display: table; 42 | } 43 | 44 | .clearfix:after { 45 | clear: both; 46 | } 47 | 48 | body{ 49 | background: #f9f7f6; 50 | color: #404d5b; 51 | font-weight: 500; 52 | font-size: 1.05em; 53 | font-family: "Segoe UI", "Lucida Grande", Helvetica, Arial, "Microsoft YaHei", FreeSans, Arimo, "Droid Sans", "wenquanyi micro hei", "Hiragino Sans GB", "Hiragino Sans GB W3", "FontAwesome", sans-serif; 54 | } 55 | a{color: #2fa0ec;text-decoration: none;outline: none;} 56 | a:hover,a:focus{color:#74777b;} 57 | 58 | .jq22-container{ 59 | margin: 0 auto; 60 | text-align: center; 61 | overflow: hidden; 62 | } 63 | .jq22-content { 64 | font-size: 150%; 65 | padding: 1em 0; 66 | } 67 | 68 | .jq22-content h2 { 69 | margin: 0 0 2em; 70 | opacity: 0.1; 71 | } 72 | 73 | .jq22-content p { 74 | margin: 1em 0; 75 | padding: 5em 0 0 0; 76 | font-size: 0.65em; 77 | } 78 | .bgcolor-1 { background: #f0efee; } 79 | .bgcolor-2 { background: #f9f9f9; } 80 | .bgcolor-3 { background: #e8e8e8; }/*light grey*/ 81 | .bgcolor-4 { background: #2f3238; color: #fff; }/*Dark grey*/ 82 | .bgcolor-5 { background: #df6659; color: #521e18; }/*pink1*/ 83 | .bgcolor-6 { background: #2fa8ec; }/*sky blue*/ 84 | .bgcolor-7 { background: #d0d6d6; }/*White tea*/ 85 | .bgcolor-8 { background: #3d4444; color: #fff; }/*Dark grey2*/ 86 | .bgcolor-9 { background: #ef3f52; color: #fff;}/*pink2*/ 87 | .bgcolor-10{ background: #64448f; color: #fff;}/*Violet*/ 88 | .bgcolor-11{ background: #3755ad; color: #fff;}/*dark blue*/ 89 | .bgcolor-12{ background: #3498DB; color: #fff;}/*light blue*/ 90 | /* Header */ 91 | .jq22-header{ 92 | padding: 1em 190px 1em; 93 | letter-spacing: -1px; 94 | text-align: center; 95 | } 96 | .jq22-header h1 { 97 | font-weight: 600; 98 | font-size: 2em; 99 | line-height: 1; 100 | margin-bottom: 0; 101 | font-family: "Segoe UI", "Lucida Grande", Helvetica, Arial, "Microsoft YaHei", FreeSans, Arimo, "Droid Sans", "wenquanyi micro hei", "Hiragino Sans GB", "Hiragino Sans GB W3", "FontAwesome", sans-serif; 102 | } 103 | .jq22-header h1 span { 104 | font-family: "Segoe UI", "Lucida Grande", Helvetica, Arial, "Microsoft YaHei", FreeSans, Arimo, "Droid Sans", "wenquanyi micro hei", "Hiragino Sans GB", "Hiragino Sans GB W3", "FontAwesome", sans-serif; 105 | display: block; 106 | font-size: 60%; 107 | font-weight: 400; 108 | padding: 0.8em 0 0.5em 0; 109 | color: #c3c8cd; 110 | } 111 | /*nav*/ 112 | .jq22-demo a{color: #1d7db1;text-decoration: none;} 113 | .jq22-demo{width: 100%;padding-left: 1.5em;padding-right: 1.5em;padding-bottom: 1.2em;} 114 | .jq22-demo a{display: inline-block;margin: 0.3em;padding: 0.2em 0.5em;border: 3px solid #1d7db1;font-weight: 700;} 115 | .jq22-demo a:hover{opacity: 0.6;} 116 | .jq22-demo a.current{background:#1d7db1;color: #fff; } 117 | /* Top Navigation Style */ 118 | .jq22-links { 119 | position: relative; 120 | display: inline-block; 121 | white-space: nowrap; 122 | font-size: 1.5em; 123 | text-align: center; 124 | } 125 | 126 | .jq22-links::after { 127 | position: absolute; 128 | top: 0; 129 | left: 50%; 130 | margin-left: -1px; 131 | width: 2px; 132 | height: 100%; 133 | background: #dbdbdb; 134 | content: ''; 135 | -webkit-transform: rotate3d(0,0,1,22.5deg); 136 | transform: rotate3d(0,0,1,22.5deg); 137 | } 138 | 139 | .jq22-icon { 140 | display: inline-block; 141 | margin: 0.5em; 142 | padding: 0em 0; 143 | width: 1.5em; 144 | text-decoration: none; 145 | } 146 | 147 | .jq22-icon span { 148 | display: none; 149 | } 150 | 151 | .jq22-icon:before { 152 | margin: 0 5px; 153 | text-transform: none; 154 | font-weight: normal; 155 | font-style: normal; 156 | font-variant: normal; 157 | font-family: 'icomoon'; 158 | line-height: 1; 159 | speak: none; 160 | -webkit-font-smoothing: antialiased; 161 | } 162 | /* footer */ 163 | .jq22-footer{width: 100%;padding-top: 10px;} 164 | .jq22-small{font-size: 0.8em;} 165 | .center{text-align: center;} 166 | /****/ 167 | .related { 168 | color: #fff; 169 | background: #333; 170 | text-align: center; 171 | font-size: 1.25em; 172 | padding: 0.5em 0; 173 | overflow: hidden; 174 | } 175 | 176 | .related > a { 177 | vertical-align: top; 178 | width: calc(100% - 20px); 179 | max-width: 340px; 180 | display: inline-block; 181 | text-align: center; 182 | margin: 20px 10px; 183 | padding: 25px; 184 | font-family: "Segoe UI", "Lucida Grande", Helvetica, Arial, "Microsoft YaHei", FreeSans, Arimo, "Droid Sans", "wenquanyi micro hei", "Hiragino Sans GB", "Hiragino Sans GB W3", "FontAwesome", sans-serif; 185 | } 186 | .related a { 187 | display: inline-block; 188 | text-align: left; 189 | margin: 20px auto; 190 | padding: 10px 20px; 191 | opacity: 0.8; 192 | -webkit-transition: opacity 0.3s; 193 | transition: opacity 0.3s; 194 | -webkit-backface-visibility: hidden; 195 | } 196 | 197 | .related a:hover, 198 | .related a:active { 199 | opacity: 1; 200 | } 201 | 202 | .related a img { 203 | max-width: 100%; 204 | opacity: 0.8; 205 | border-radius: 4px; 206 | } 207 | .related a:hover img, 208 | .related a:active img { 209 | opacity: 1; 210 | } 211 | .related h3{font-family: "Microsoft YaHei", sans-serif;} 212 | .related a h3 { 213 | font-weight: 300; 214 | margin-top: 0.15em; 215 | color: #fff; 216 | } 217 | /* icomoon */ 218 | .icon-jq22-home-outline:before { 219 | content: "\e5000"; 220 | } 221 | 222 | .icon-jq22-arrow-forward-outline:before { 223 | content: "\e5001"; 224 | } 225 | 226 | @media screen and (max-width: 50em) { 227 | .jq22-header { 228 | padding: 3em 10% 4em; 229 | } 230 | .jq22-header h1 { 231 | font-size:2em; 232 | } 233 | } 234 | 235 | 236 | @media screen and (max-width: 40em) { 237 | .jq22-header h1 { 238 | font-size: 1.5em; 239 | } 240 | } 241 | 242 | @media screen and (max-width: 30em) { 243 | .jq22-header h1 { 244 | font-size:1.2em; 245 | } 246 | } -------------------------------------------------------------------------------- /src/main/java/com/starix/gdou/utils/HttpClientUtil.java: -------------------------------------------------------------------------------- 1 | package com.starix.gdou.utils; 2 | 3 | import org.apache.http.*; 4 | import org.apache.http.client.CookieStore; 5 | import org.apache.http.client.HttpClient; 6 | import org.apache.http.client.entity.UrlEncodedFormEntity; 7 | import org.apache.http.client.methods.HttpGet; 8 | import org.apache.http.client.methods.HttpPost; 9 | import org.apache.http.client.methods.HttpRequestBase; 10 | import org.apache.http.client.utils.URIBuilder; 11 | import org.apache.http.conn.ssl.NoopHostnameVerifier; 12 | import org.apache.http.cookie.Cookie; 13 | import org.apache.http.entity.StringEntity; 14 | import org.apache.http.impl.client.BasicCookieStore; 15 | import org.apache.http.impl.client.CloseableHttpClient; 16 | import org.apache.http.impl.client.HttpClients; 17 | import org.apache.http.impl.client.LaxRedirectStrategy; 18 | import org.apache.http.impl.cookie.BasicClientCookie; 19 | import org.apache.http.message.BasicNameValuePair; 20 | import org.apache.http.ssl.SSLContexts; 21 | import org.apache.http.util.EntityUtils; 22 | import org.springframework.http.MediaType; 23 | 24 | import javax.net.ssl.SSLContext; 25 | import java.io.IOException; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | import java.util.Map; 29 | 30 | /** 31 | * 对httpclient进行一层封装 32 | * 33 | * @author shiwenjie 34 | * @created 2020/6/28 5:41 下午 35 | **/ 36 | public class HttpClientUtil { 37 | 38 | private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + 39 | "Chrome/79.0.3945.130 Safari/537.36"; 40 | 41 | private CookieStore cookieStore = new BasicCookieStore(); 42 | 43 | // 为了隔离每个请求线程的httpClient实例,使彼此之间互不干扰,这里不采用static而是作为实例变量 44 | private HttpClient httpClient = createHttpClient(); 45 | 46 | 47 | public CloseableHttpClient createHttpClient() { 48 | try { 49 | SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, (x509Certificates, s) -> true).build(); 50 | CloseableHttpClient client = HttpClients.custom() 51 | // 忽略ssl验证 52 | .setSSLContext(sslContext) 53 | .setSSLHostnameVerifier(new NoopHostnameVerifier()) 54 | // 自动跟踪重定向(支持POST) 55 | .setRedirectStrategy(new LaxRedirectStrategy()) 56 | // cookie管理 57 | .setDefaultCookieStore(cookieStore) 58 | .build(); 59 | return client; 60 | } catch (Exception e) { 61 | e.printStackTrace(); 62 | } 63 | return null; 64 | } 65 | 66 | public String doGet(String url) throws Exception { 67 | HttpGet httpGet = new HttpGet(url); 68 | httpGet.setHeader("User-Agent", USER_AGENT); 69 | return executeRequest(httpGet); 70 | } 71 | 72 | public String doGet(String url, Map params) throws Exception { 73 | url = buildUrlWithParams(url, params); 74 | HttpGet httpGet = new HttpGet(url); 75 | httpGet.setHeader("User-Agent", USER_AGENT); 76 | return executeRequest(httpGet); 77 | } 78 | 79 | public String doGet(String url, Map params, Map headerParams) throws Exception { 80 | url = buildUrlWithParams(url, params); 81 | HttpGet httpGet = new HttpGet(url); 82 | httpGet.setHeader("User-Agent", USER_AGENT); 83 | for (Map.Entry headerParam : headerParams.entrySet()) { 84 | httpGet.setHeader(headerParam.getKey(), headerParam.getValue()); 85 | } 86 | return executeRequest(httpGet); 87 | } 88 | 89 | public String doPost(String url, Map formData) throws IOException { 90 | HttpPost httpPost = new HttpPost(url); 91 | httpPost.setHeader("User-Agent", USER_AGENT); 92 | httpPost.addHeader("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE); 93 | List paramList = new ArrayList<>(); 94 | for (Map.Entry param : formData.entrySet()) { 95 | paramList.add(new BasicNameValuePair(param.getKey(), param.getValue())); 96 | } 97 | // 表单类型post参数 98 | httpPost.setEntity(new UrlEncodedFormEntity(paramList, Consts.UTF_8)); 99 | return executeRequest(httpPost); 100 | } 101 | 102 | public String doPost(String url, String json) throws IOException { 103 | HttpPost httpPost = new HttpPost(url); 104 | httpPost.setHeader("User-Agent", USER_AGENT); 105 | httpPost.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE); 106 | // json类型post参数 107 | StringEntity entity = new StringEntity(json, Consts.UTF_8); 108 | httpPost.setEntity(entity); 109 | return executeRequest(httpPost); 110 | } 111 | 112 | public String doPost(String url, Map formData, Map headerParams) throws IOException { 113 | HttpPost httpPost = new HttpPost(url); 114 | httpPost.setHeader("User-Agent", USER_AGENT); 115 | httpPost.addHeader("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE); 116 | for (Map.Entry headerParam : headerParams.entrySet()) { 117 | httpPost.setHeader(headerParam.getKey(), headerParam.getValue()); 118 | } 119 | List paramList = new ArrayList<>(); 120 | for (Map.Entry param : formData.entrySet()) { 121 | paramList.add(new BasicNameValuePair(param.getKey(), param.getValue())); 122 | } 123 | // 表单类型post参数 124 | httpPost.setEntity(new UrlEncodedFormEntity(paramList, Consts.UTF_8)); 125 | return executeRequest(httpPost); 126 | } 127 | 128 | public String doPost(String url, String json, Map headerParams) throws IOException { 129 | HttpPost httpPost = new HttpPost(url); 130 | httpPost.setHeader("User-Agent", USER_AGENT); 131 | httpPost.addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE); 132 | for (Map.Entry headerParam : headerParams.entrySet()) { 133 | httpPost.setHeader(headerParam.getKey(), headerParam.getValue()); 134 | } 135 | // json类型post参数 136 | StringEntity entity = new StringEntity(json, Consts.UTF_8); 137 | httpPost.setEntity(entity); 138 | return executeRequest(httpPost); 139 | } 140 | 141 | 142 | public CookieStore getCookieStore(){ 143 | return this.cookieStore; 144 | } 145 | 146 | public String getCookie(String name) { 147 | if (name == null || name.isEmpty()) { 148 | return null; 149 | } 150 | List cookies = cookieStore.getCookies(); 151 | for (Cookie cookie : cookies) { 152 | if (name.equals(cookie.getName())) { 153 | return cookie.getValue(); 154 | } 155 | } 156 | return null; 157 | } 158 | 159 | public void addCookie(String name, String value, Map settings) { 160 | if (name == null || name.isEmpty()) { 161 | return; 162 | } 163 | BasicClientCookie cookie = new BasicClientCookie(name, value); 164 | if(settings != null){ 165 | cookie.setPath(settings.get("path")); 166 | cookie.setDomain(settings.get("domain")); 167 | } 168 | cookieStore.addCookie(cookie); 169 | } 170 | 171 | private String executeRequest(HttpRequestBase httpRequest) throws IOException { 172 | try { 173 | HttpResponse response = httpClient.execute(httpRequest); 174 | HttpEntity httpEntity = response.getEntity(); 175 | if (!checkResult(response)) { 176 | throw new RuntimeException( 177 | String.format("http请求响应状态异常, 响应状态吗:%s, 响应报文:%s", 178 | response.getStatusLine().getStatusCode(), 179 | EntityUtils.toString(httpEntity, Consts.UTF_8))); 180 | } 181 | return EntityUtils.toString(response.getEntity(), Consts.UTF_8); 182 | } catch (IOException e) { 183 | throw e; 184 | } finally { 185 | httpRequest.releaseConnection(); 186 | } 187 | } 188 | 189 | private boolean checkResult(HttpResponse response){ 190 | return (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK 191 | || response.getStatusLine().getStatusCode() == HttpStatus.SC_MOVED_TEMPORARILY) 192 | && response.getEntity() != null; 193 | } 194 | 195 | public static String buildUrlWithParams(String url, Map params) throws Exception { 196 | if (params == null || params.isEmpty()) { 197 | return url; 198 | } 199 | URIBuilder uriBuilder = new URIBuilder(url); 200 | for (Map.Entry entry : params.entrySet()) { 201 | uriBuilder.addParameter(entry.getKey(), entry.getValue()); 202 | } 203 | return uriBuilder.build().toString(); 204 | } 205 | 206 | } -------------------------------------------------------------------------------- /src/main/resources/static/cpdaily-del-user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 自动签到 8 | 9 | 187 | 188 | 189 |

自动签到取消

190 | 208 | 209 | 210 | 277 | 278 | -------------------------------------------------------------------------------- /src/main/resources/static/bind.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 学号绑定 8 | 9 | 177 | 178 | 179 |

学号绑定

180 | 198 | 199 | 200 | 271 | 272 | -------------------------------------------------------------------------------- /src/main/resources/static/exam.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 考试查询 8 | 9 | 166 | 167 | 168 | 169 | 187 | 188 | 189 | 292 | 293 | --------------------------------------------------------------------------------