├── src ├── __init__.py ├── BusinessViewLayer │ ├── __init__.py │ └── myapp │ │ ├── __init__.py │ │ ├── api.py │ │ └── forms.py ├── BusinessCentralLayer │ ├── __init__.py │ ├── middleware │ │ ├── __init__.py │ │ ├── work_io.py │ │ ├── interface_io.py │ │ └── flow_io.py │ ├── content.txt │ ├── coroutine_engine.py │ ├── setting.py │ └── scaffold.py ├── BusinessLogicLayer │ ├── __init__.py │ ├── apis │ │ ├── __init__.py │ │ ├── manager_screenshot.py │ │ ├── manager_cookie.py │ │ ├── manager_users.py │ │ ├── manager_timer.py │ │ └── vulcan_ash.py │ ├── cluster │ │ ├── __init__.py │ │ ├── osh_runner.py │ │ └── osh_core.py │ └── plugins │ │ ├── __init__.py │ │ ├── faker_any.py │ │ └── noticer.py └── config.py ├── docs ├── subdirectory │ ├── 开源计划.md │ ├── 技术文档(demo).md │ ├── 注意事项.md │ └── 更新日志.md ├── CPDS NoneBot.png ├── CPDS NoneBot.xmind └── material │ ├── 体温签到-流图1.png │ ├── 体温签到-流图2.png │ ├── demo_bot_twqdall.gif │ ├── demo_bot_twqd_stuNum.gif │ ├── 未命名绘图.drawio │ └── 体温签到-流图2.drawio ├── main.py ├── requirements.txt ├── LICENSE └── README.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/subdirectory/开源计划.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/BusinessViewLayer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/BusinessCentralLayer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/apis/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/cluster/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/BusinessViewLayer/myapp/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/BusinessCentralLayer/middleware/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/subdirectory/技术文档(demo).md: -------------------------------------------------------------------------------- 1 | # :gear:CampusDailyAutoSign 技术文档 -------------------------------------------------------------------------------- /docs/CPDS NoneBot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QIN2DIM/CampusDailyAutoSign/HEAD/docs/CPDS NoneBot.png -------------------------------------------------------------------------------- /docs/CPDS NoneBot.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QIN2DIM/CampusDailyAutoSign/HEAD/docs/CPDS NoneBot.xmind -------------------------------------------------------------------------------- /docs/material/体温签到-流图1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QIN2DIM/CampusDailyAutoSign/HEAD/docs/material/体温签到-流图1.png -------------------------------------------------------------------------------- /docs/material/体温签到-流图2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QIN2DIM/CampusDailyAutoSign/HEAD/docs/material/体温签到-流图2.png -------------------------------------------------------------------------------- /docs/material/demo_bot_twqdall.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QIN2DIM/CampusDailyAutoSign/HEAD/docs/material/demo_bot_twqdall.gif -------------------------------------------------------------------------------- /docs/material/demo_bot_twqd_stuNum.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QIN2DIM/CampusDailyAutoSign/HEAD/docs/material/demo_bot_twqd_stuNum.gif -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from src.BusinessCentralLayer.scaffold import * 2 | 3 | # 今日校园疫情体温检测--自动上报脚本 4 | if __name__ == '__main__': 5 | scaffold.startup(argv) 6 | -------------------------------------------------------------------------------- /src/BusinessCentralLayer/middleware/work_io.py: -------------------------------------------------------------------------------- 1 | from gevent.queue import Queue 2 | 3 | 4 | class Middleware(object): 5 | Poseidon = Queue() 6 | 7 | Apollo = Queue() 8 | -------------------------------------------------------------------------------- /docs/subdirectory/注意事项.md: -------------------------------------------------------------------------------- 1 | ## :small_red_triangle: 注意事项 2 | 3 | `“海南大学企业微信”`与`jzjy`体温签到接口的数据交互逻辑存在严重的信息安全漏洞。黑客可通过外部接口,轻松获取使用此签到系统的本校学生的隐私数据,包括但不限于本人真实姓名、学工号、常用手机号、“宿舍楼号”、学院专业班级;经过长期数据沉淀后,甚至可“推理”出学生的QQ号,微信号,户籍所在地(或家庭住址),日常活动范围,生活作息习惯等“价值数据”。若黑客掌握一定的撞库技巧并具备“价值链”,则很可能“推理”出身份证号,银行卡密码等数据。 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycurl 2 | apscheduler 3 | oss2 4 | psutil 5 | nonebot 6 | gevent>=20.9.0 7 | flask>=1.1.2 8 | wtforms>=2.3.3 9 | requests>=2.25.0 10 | pytz>=2020.4 11 | pymysql>=0.10.1 12 | pyyaml>=5.3.1 13 | pydes>=2.0.1 14 | crypto>=1.4.1 15 | environs>=9.2.0 16 | loguru>=0.5.3 17 | email_validator 18 | fake-useragent -------------------------------------------------------------------------------- /src/BusinessLogicLayer/plugins/faker_any.py: -------------------------------------------------------------------------------- 1 | __all__ = ['get_useragent'] 2 | 3 | from fake_useragent import UserAgent 4 | from fake_useragent import errors 5 | 6 | 7 | def get_useragent() -> str: 8 | try: 9 | return UserAgent().random 10 | except errors.FakeUserAgentError: 11 | exec('import os\nos.system("pip install -upgrade fake-useragent")') 12 | -------------------------------------------------------------------------------- /src/BusinessCentralLayer/content.txt: -------------------------------------------------------------------------------- 1 | HTTP/1.1 302 Moved Temporarily 2 | Server: openresty 3 | Date: Wed, 24 Feb 2021 02:49:19 GMT 4 | Content-Type: text/html 5 | Transfer-Encoding: chunked 6 | Connection: keep-alive 7 | Set-Cookie: MOD_AMP_AUTH=; Max-Age=0; path=/; Httponly 8 | Location: https://ehall.hainanu.edu.cn:443/amp-auth-adapter/login?service=https%3A%2F%2Fehall.hainanu.edu.cn%3A443%2Fqljfwapp%2Fsys%2FlwHainanuStuTempReport%2F*default%2Findex.do 9 | X-Frame-Options: SAMEORIGIN 10 | X-Frame-Options: SAMEORIGIN 11 | 12 | 13 | 302 Found 14 | 15 |

302 Found

16 |
nginx
17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/subdirectory/更新日志.md: -------------------------------------------------------------------------------- 1 | # :loudspeaker: 更新日志 2 | 3 | ## 2021.02.24 v_2.0.1.beta 4 | 5 | > **重要更新** 6 | 7 | 1. 项目重构,去除没有必要的接口函数,使用纯Python代码构建低代码模型; 8 | 2. 重写协程控件的中间函数,大幅度提升系统整体性能; 9 | 3. 接入Nonebot2交互机,实现动态化的实时的定时任务增删改查; 10 | 4. 重构弹性伸缩中间件,大幅度提升调度器的并发性能; 11 | 5. 微调请求算法,提升系统鲁棒性; 12 | 6. 修复已知bug。 13 | 14 | ## 2021.01.04 v_1.0.5.alpha 15 | 16 | 引入新平台的执行方案,向旧版本功能兼容。 17 | 18 | ## 2021.01.01 v_1.0.5.beta 19 | 20 | > **重要更新** 21 | 22 | 根据[《关于启用"网上服务大厅”学生体温上报服务的通知》](https://kdocs.cn/l/sl02ofSPeh7r?f=111 23 | )部分学子的体温签到任务自2021年1月4日起不再依托今日校园第三方应用程序。 24 | 25 | ## **2020.12.08 v_1.0.4.beta** 26 | 27 | > **功能迭代** 28 | 29 | 任务分发,解决因多用户并发而操作过热导致的IP封禁问题 30 | 31 | ## 2020.12.08 v_1.0.3.beta 32 | 33 | > **功能迭代** 34 | 35 | 1. 解决原方案在获取`MOD_AUTH_CAS`时`session`状态码`405`的问题 36 | 37 | 2. 修复消息通知功能异常的问题 38 | 39 | ## 2020.11.11 v_1.0.2.11162350.11 40 | 41 | > **重要更新** 42 | 43 | 1. 统一接口,部署`TrojanGoCDN`,集群解耦; 44 | 2. 改用 `GoroutineEngine` +` ConfigPath` 单机架构驱动业务核心; 45 | 3. 使用`Flask `+`Panel`接收验证数据; 46 | 47 | > **性能调优** 48 | 49 | 1. 将`API`打包成`ActionBase`,引入`T-SC ESS`并行伸缩技术驱动`ActionGeneral`弹性业务; 50 | 2. 编写`GeventSchedule`轻量化部署脚本(支持数据吞吐、弹性协程); 51 | 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 QIN2DIM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/BusinessViewLayer/myapp/api.py: -------------------------------------------------------------------------------- 1 | from src.BusinessCentralLayer.middleware.flow_io import sync_user_data 2 | from src.BusinessLogicLayer.apis.manager_screenshot import capture_and_upload_screenshot 3 | from src.BusinessLogicLayer.apis.manager_users import stu_twqd, check_display_state 4 | 5 | 6 | def _select_user_table(username) -> dict or bool: 7 | """ 8 | 同步用户表 9 | :param username: 10 | :return: 11 | """ 12 | # 若用户在库,返回用户密码,否则返回false bool 13 | for sud in sync_user_data(): 14 | if username == sud['username'] and sud['password']: 15 | break 16 | else: 17 | return False 18 | return {'password': sud['password']} 19 | 20 | 21 | def apis_stu_twqd(user: dict): 22 | user_ = {'username': user.get('username'), } 23 | 24 | # 启动节点任务,该操作为风险操作,权限越界,无需知道用户密码也可完成操作 25 | return stu_twqd(user_) 26 | 27 | 28 | def apis_stu_twqd2(user: dict): 29 | """ 30 | 当OSS没有截图时使用此接口 31 | 既已知OSS没有截图,故无论osh-slaver如何执行,都要调用 osh-s上传截图 32 | :param user: 33 | :return: 34 | """ 35 | 36 | user_ = {'username': user.get('username'), } 37 | 38 | # 用户鉴权 39 | ttp = _select_user_table(user_["username"]) 40 | if ttp: 41 | user_.update(ttp) 42 | else: 43 | return {'msg': 'failed', 'info': '用户权限不足', 'code': 101} 44 | 45 | # 使用osh-runner判断用户是否签到 46 | # 若未签到,startup osh-core 进行签到; 47 | # 若已签到,kill the osh-core 结束子程序 48 | stu_state: dict = check_display_state(user_) 49 | if stu_state['code'] != 902: 50 | return stu_twqd(user_) 51 | 52 | # 启动Selenium上传截图 并返回调试数据 53 | return capture_and_upload_screenshot(user_) 54 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/apis/manager_screenshot.py: -------------------------------------------------------------------------------- 1 | __all__ = ['UploadScreenshot', 'capture_and_upload_screenshot'] 2 | 3 | import sys 4 | 5 | sys.path.append("/qinse/CpdsNonebot") 6 | 7 | import requests 8 | 9 | from src.BusinessCentralLayer.setting import logger, OSH_STATUS_CODE, API_PORT, API_HOST, PUBLIC_API_STU_TWQD2 10 | from src.BusinessLogicLayer.cluster.osh_core import osh_core 11 | 12 | 13 | def capture_and_upload_screenshot(user: dict, silence=True, only_get_screenshot: bool = True, debug=False) -> dict: 14 | """ 15 | 通过模拟登陆的方案获取签到截图,并上传oss 16 | :param debug: 17 | :param only_get_screenshot:使用Selenium上传已签到截图(若今日签到任务未完成则不能执行) 18 | :param user: username and password 19 | :param silence: 20 | :return: 21 | """ 22 | params = osh_core(silence=silence, anti=False, debug=debug, only_get_screenshot=only_get_screenshot).run(user) 23 | response = {'code': params[0], 'username': params[-1], 'info': OSH_STATUS_CODE[params[0]]} 24 | # logger.info(f"{params[-1]} -- {OSH_STATUS_CODE[params[0]]}") 25 | return response 26 | 27 | 28 | class UploadScreenshot(object): 29 | def __init__(self): 30 | self.api_ = f"http://{API_HOST}:{API_PORT}{PUBLIC_API_STU_TWQD2}" 31 | # self.api_ = 'http://twqd.yaoqinse.com:6577/cpds/api/stu_twqd2' 32 | 33 | def run(self, user): 34 | """ 35 | 36 | :param user:{"username":, "password":str} 37 | :return: 38 | """ 39 | resp = requests.post(self.api_, data=user) 40 | response = resp.json() 41 | if response.get("code") == 300: 42 | logger.success(f'{response.get("username")} -- {response.get("info")}') 43 | else: 44 | logger.success(f'{response.get("username")} -- {response.get("info")}') 45 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > 📌 本项目服务主体暂无线上风控需要。本项目仓库归档存储。 2 | 3 | # CampusDailyAutoSign 4 | 5 | 今日校园 体温检测 自动签到(For:HainanUniversity) 6 | 7 | ## :carousel_horse: 项目简介 8 | 9 | > 1. 本项目仅为**海南大学**学子提供服务; 10 | > 3. 本项目**开源免费**,禁止任何人使用此项目及其分支提供任何形式的收费代理服务; 11 | 12 | ![CPDS NoneBot](https://i.loli.net/2021/02/24/MbkFBGQ3Ohj5X6p.png) 13 | 14 | - [程序流图1][6] **||**[程序流图2][7] ||[demo演示][5] 15 | 16 | - 本项目使用[`Nonebot2 hnu-temp-report-bot`](https://github.com/beiyuouo/hnu-temp-report-bot)跳板机实现人机交互,QQ群内对 **bot(851722457)** 使用`关键字指令`+`功能指令`既可完成体温签到打卡; 17 | 18 | ## :kick_scooter: 快速上手 19 | 20 | - **【方案一】用户** 21 | - 在群内拉入QQ机器人 **bot(851722457)** 22 | - 根据[技术文档][1]中的引导完成定时任务的增删改查或立即执行签到操作。 23 | - **【方案二】开发者** 24 | - Clone项目:champagne: 25 | - 根据[技术文档][1]合理配置启动参数并通过脚手架调试或部署项目。 26 | 27 | ## :ocean: 网上冲浪 28 | 29 | - :gear: [技术文档][1](更新中...) 30 | 31 | - :small_red_triangle: [注意事项][2] 32 | 33 | - :loudspeaker: [更新日志][3] 34 | - :world_map: [开源计划][4] 35 | 36 | ## :e-mail: 联系我们 37 | 38 | > 本项目由海南大学机器人与人工智能协会数据挖掘小组 **(A-RAI.DM)** 提供维护 39 | 40 | - [**Email**](mailto:HainanU_arai@163.com?subject=CampusDailyAutoSign-ISSUE) **||** [**Home**](https://a-rai.github.io/)**||** [**Studio**](https://jq.qq.com/?_wv=1027&k=a0BxYb35) 41 | 42 | [1]: https://github.com/QIN2DIM/CampusDailyAutoSign/blob/main/docs/subdirectory/技术文档(demo).md "技术文档" 43 | [2]: https://github.com/QIN2DIM/CampusDailyAutoSign/blob/main/docs/subdirectory/注意事项.md "注意事项" 44 | [3]: https://github.com/QIN2DIM/CampusDailyAutoSign/blob/main/docs/subdirectory/更新日志.md "更新日志" 45 | [4]: https://github.com/QIN2DIM/CampusDailyAutoSign/blob/main/docs/subdirectory/开源计划.md "开源计划" 46 | [5]: https://www.yuque.com/docs/share/b0634038-5ee5-4632-80c4-0564e7795489? "demo演示" 47 | [6]: https://www.yuque.com/docs/share/53c62de5-91bf-4f96-9330-fae53c6a4f0d? "程序流图1" 48 | [7]: https://www.yuque.com/docs/share/94dac928-a89c-47d3-9273-8bd049152b6e? "程序流图2" 49 | 50 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/apis/manager_cookie.py: -------------------------------------------------------------------------------- 1 | __all__ = ['reload_admin_cookie', 'quick_refresh_cookie', 'check_admin_cookie'] 2 | 3 | from typing import Tuple 4 | 5 | from src.BusinessCentralLayer.setting import SUPERUSER, logger, SERVER_PATH_COOKIES 6 | from src.BusinessLogicLayer.cluster.osh_core import osh_core 7 | 8 | 9 | def reload_admin_cookie(user=None) -> Tuple[int, str]: 10 | """ 11 | Obtain user cookies through Selenium 12 | :param user: When no parameters are passed in, update the cookie for the superuser 13 | :return: status code(int) and cookie(str) 14 | """ 15 | if user is None: 16 | user = SUPERUSER 17 | kernel = 'admin' 18 | else: 19 | kernel = 'general' 20 | return osh_core(silence=True, anti=False).rush_cookie_pool(user=user, kernel=kernel) 21 | 22 | 23 | def quick_refresh_cookie(user: dict = None) -> str: 24 | """ 25 | Obtain user cookies through ‘Requests POST’ 26 | :param user: username and password 27 | :return: cookie(str) 28 | """ 29 | from src.BusinessLogicLayer.cluster.osh_runner import runner 30 | return runner.quick_refresh_cookie(user=user) 31 | 32 | 33 | def check_admin_cookie(): 34 | """ 35 | Check the timeliness of the cookie, and automatically pull the cookie if the cookie fails 36 | :return: 37 | """ 38 | from src.BusinessLogicLayer.cluster.osh_runner import runner 39 | 40 | # Load superuser's cookie 41 | with open(SERVER_PATH_COOKIES, 'r', encoding='utf8') as f: 42 | cookie = f.read().strip() 43 | 44 | # Judging cookie timeliness 45 | is_available = runner.check_cookie(mod_amp_auth=cookie) 46 | logger.info(f" The status of superuser's cookie is :{is_available}")\ 47 | 48 | # If the cookie fails, call the relevant module to update the cookie 49 | if not is_available: 50 | logger.debug(" Try to reload superuser's cookie.") 51 | reload_admin_cookie() 52 | 53 | 54 | if __name__ == '__main__': 55 | check_admin_cookie() 56 | -------------------------------------------------------------------------------- /src/BusinessCentralLayer/coroutine_engine.py: -------------------------------------------------------------------------------- 1 | __all__ = ['lsu_'] 2 | 3 | import os 4 | from typing import List, Dict 5 | 6 | import gevent 7 | from gevent.queue import Queue 8 | 9 | from src.BusinessCentralLayer.setting import logger 10 | 11 | 12 | class _LightweightSpeedup(object): 13 | """轻量化的协程控件""" 14 | 15 | def __init__(self, work_q: Queue = Queue(), task_docker=None, power: int = os.cpu_count()): 16 | self.work_q = work_q 17 | self.task_docker = task_docker 18 | self.power = power 19 | self.temp_cache: Dict[str:int] = {} 20 | self.apollo: List[List[str]] = [] 21 | 22 | def launch(self): 23 | while not self.work_q.empty(): 24 | task = self.work_q.get_nowait() 25 | self.control_driver(task) 26 | 27 | @staticmethod 28 | def beat_sync(sleep_time: float): 29 | gevent.sleep(sleep_time) 30 | 31 | def control_driver(self, user): 32 | """ 33 | rewrite this method 34 | @param user: 35 | @return: 36 | """ 37 | 38 | def offload_task(self): 39 | """ 40 | 41 | @return: 42 | """ 43 | 44 | def killer(self): 45 | """ 46 | 47 | @return: 48 | """ 49 | pass 50 | 51 | def interface(self, power: int = os.cpu_count()) -> None: 52 | """ 53 | 54 | @param power: 协程功率 55 | @return: 56 | """ 57 | 58 | # logger.info(f" Atomic ash go! || <{self.__class__.__name__}>") 59 | 60 | # 任务重载 61 | self.offload_task() 62 | 63 | # 任务启动 64 | task_list = [] 65 | 66 | # 性能释放校准 67 | power_ = self.power if self.power else power 68 | power_ = self.work_q.qsize() if power_ > self.work_q.qsize() else power_ 69 | 70 | logger.info(f" power:{power_} qsize:{self.work_q.qsize()} || <{self.__class__.__name__}>") 71 | 72 | for x in range(power_): 73 | task = gevent.spawn(self.launch) 74 | task_list.append(task) 75 | gevent.joinall(task_list) 76 | 77 | # 性能回收 78 | self.killer() 79 | 80 | logger.success(f' MissionCompleted || <{self.__class__.__name__}>') 81 | 82 | 83 | lsu_ = _LightweightSpeedup 84 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/plugins/noticer.py: -------------------------------------------------------------------------------- 1 | __all__ = ['send_email'] 2 | 3 | import smtplib 4 | from email.header import Header 5 | from email.mime.text import MIMEText 6 | from typing import List 7 | 8 | from src.BusinessCentralLayer.setting import SMTP_SCKEY, logger 9 | 10 | 11 | def send_email(msg: str, to_: List[str] or str or set, headers: str = None): 12 | """ 13 | 发送运维信息,该函数仅用于发送简单文本信息 14 | :param msg: 正文内容 15 | :param to_: 发送对象 16 | 1. str 17 | to_ == 'self',发送给“自己” 18 | 2. List[str] 19 | 传入邮箱列表,群发邮件(内容相同)。 20 | :param headers: 21 | :return: 默认为'运维日志' 22 | """ 23 | headers = headers if headers else '运维日志' 24 | sender = SMTP_SCKEY.get('email') 25 | password = SMTP_SCKEY.get('sid') 26 | smtp_server = 'smtp.qq.com' 27 | message = MIMEText(msg, 'plain', 'utf-8') 28 | message['From'] = Header('ARAI.DM', 'utf-8') # 发送者 29 | message['Subject'] = Header(f"{headers}", 'utf-8') 30 | server = smtplib.SMTP_SSL(smtp_server, 465) 31 | 32 | # --------------------------------------- 33 | # 输入转换 34 | # --------------------------------------- 35 | 36 | # 处理单个传递对象 37 | if isinstance(to_, str): 38 | # 自发邮件 39 | if to_ == 'self': 40 | to_ = [sender, ] 41 | else: 42 | to_ = [to_, ] 43 | 44 | # 处理多个传递对象 45 | if isinstance(to_, list): 46 | # 自发邮件 47 | if 'self' in to_: 48 | to_.remove('self') 49 | to_.append(sender) 50 | to_ = set(to_) 51 | 52 | # 捕获意外情况 53 | if not (isinstance(to_, set) or isinstance(to_, list)): 54 | return False 55 | 56 | try: 57 | server.login(sender, password) 58 | for to in to_: 59 | try: 60 | message['To'] = Header("致开发者", 'utf-8') # 接收者 61 | server.sendmail(sender, to, message.as_string()) 62 | logger.success("发送成功->{}".format(to)) 63 | except smtplib.SMTPRecipientsRefused: 64 | logger.warning('邮箱填写错误或不存在->{}'.format(to)) 65 | except smtplib.SMTPAuthenticationError: 66 | logger.error("授权码已更新") 67 | # aim_server("HKX-邮件发送脚本出错", '授权码已更新') 68 | except Exception as e: 69 | logger.error('>>> 发送失败 || {}'.format(e)) 70 | finally: 71 | server.quit() 72 | -------------------------------------------------------------------------------- /src/BusinessCentralLayer/middleware/interface_io.py: -------------------------------------------------------------------------------- 1 | __all__ = ['sei'] 2 | 3 | import multiprocessing 4 | 5 | from src.BusinessCentralLayer.setting import * 6 | from src.BusinessLogicLayer.plugins.noticer import send_email 7 | from src.BusinessViewLayer.myapp.forms import app 8 | 9 | if 'win' in sys.platform: 10 | multiprocessing.freeze_support() 11 | 12 | 13 | class _SystemEngine(object): 14 | def __init__(self): 15 | 16 | # 截图上传权限 17 | logger.info(f' EnableUploadScreenshot || {ENABLE_UPLOAD}') 18 | 19 | # 是否部署定时任务 20 | logger.info(f' EnableTimedTask || {ENABLE_DEPLOY}') 21 | 22 | # 单机协程加速配置 23 | logger.info(f" EnableCoroutine || True(Forced to use)") 24 | 25 | # 初始化进程 26 | logger.info(' InitChildProcess') 27 | 28 | @staticmethod 29 | def run_deploy(): 30 | from src.BusinessLogicLayer.apis.manager_timer import time_container 31 | time_container() 32 | 33 | @staticmethod 34 | def run_server(): 35 | app.run(host='0.0.0.0', port=API_PORT, debug=API_DEBUG, threaded=API_THREADED) 36 | 37 | def startup(self): 38 | process_list = [] 39 | try: 40 | # 部署<单进程多线程>定时任务 41 | if ENABLE_DEPLOY: 42 | process_list.append(multiprocessing.Process(target=self.run_deploy, name='deploymentTimingTask')) 43 | 44 | # 部署flask 45 | if ENABLE_SERVER: 46 | process_list.append(multiprocessing.Process(target=self.run_server, name='deploymentFlaskAPI')) 47 | 48 | # 执行多进程任务 49 | for process_ in process_list: 50 | logger.success(f' Startup -- {process_.name}') 51 | process_.start() 52 | 53 | # 添加阻塞 54 | for process_ in process_list: 55 | process_.join() 56 | 57 | logger.success(' The core of the project is ready and the task is about to begin.') 58 | except TypeError or AttributeError as e: 59 | logger.exception(e) 60 | send_email(f" Program termination || {str(e)}", to_='self') 61 | except KeyboardInterrupt: 62 | # FIXME 确保进程间不产生通信的情况下终止 63 | logger.debug(' Received keyboard interrupt signal') 64 | for process_ in process_list: 65 | process_.terminate() 66 | finally: 67 | logger.success(' End the ') 68 | 69 | 70 | class _SystemEngineInterface(object): 71 | 72 | @staticmethod 73 | def startup_system(): 74 | _SystemEngine().startup() 75 | 76 | 77 | sei = _SystemEngineInterface() 78 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/apis/manager_users.py: -------------------------------------------------------------------------------- 1 | """管理用户状态,包括但不限于查看签到状态,执行签到等""" 2 | __all__ = ['apis_account_verify', 'check_display_state', 'stu_twqd'] 3 | 4 | import json 5 | 6 | import requests 7 | 8 | from src.BusinessCentralLayer.setting import OSH_STATUS_CODE, logger 9 | from src.BusinessLogicLayer.cluster.osh_runner import runner 10 | 11 | 12 | def apis_account_verify(user: dict): 13 | """ 14 | 验证今日校园账号正确 15 | 16 | :param user: username password 17 | :return: 18 | """ 19 | 20 | logging_api = "http://www.zimo.wiki:8080/wisedu-unified-login-api-v1.0/api/login" 21 | 22 | apis_ = { 23 | 'login-url': 'https://authserver.hainanu.edu.cn/authserver/login?service=https%3A%2F%2Fhainanu.campusphere.net%2Fiap%2FloginSuccess%3FsessionToken%3Df73b49371c0d4669aea95af37347e9fe', 24 | 'host': 'hainanu.campusphere.net' 25 | } 26 | 27 | params = { 28 | 'login_url': apis_['login-url'], 29 | 'needcaptcha_url': '', 30 | 'captcha_url': '', 31 | 'username': user['username'], 32 | 'password': user['password'] 33 | } 34 | cookies = dict() 35 | try: 36 | res = requests.post(url=logging_api, data=params, timeout=2) 37 | cookie_str = str(res.json()['cookies']) 38 | if cookie_str == 'None': 39 | if "网页中没有找到casLoginForm" in res.json()['msg']: 40 | return None 41 | else: 42 | return False 43 | # 解析cookie 44 | for line in cookie_str.split(';'): 45 | name, value = line.strip().split('=', 1) 46 | cookies[name] = value 47 | session = requests.session() 48 | session.cookies = requests.utils.cookiejar_from_dict(cookies, cookiejar=None, overwrite=True) 49 | if session: 50 | return {'msg': 'success', 'info': 'Verified successfully'} 51 | else: 52 | return {'info': 'Incorrect username or password'} 53 | except json.decoder.JSONDecodeError or requests.exceptions.ProxyError: 54 | logger.warning("目标或存在鉴权行为,请关闭本地网络代理") 55 | 56 | 57 | def check_display_state(user: dict, debug=False, _date=0) -> dict: 58 | """ 59 | 查询用户签到状态 60 | 61 | :param _date: 62 | :param user: only username 63 | :param debug: 64 | :return: 65 | """ 66 | params = runner(debug=debug).get_stu_temp_report_data( 67 | username=user["username"], 68 | only_check_status=True, 69 | _date=_date 70 | ) 71 | if isinstance(params, int): 72 | response = {'code': params, 'info': OSH_STATUS_CODE[params]} 73 | return response 74 | 75 | 76 | def stu_twqd(user: dict, cover=False): 77 | """ 78 | 用于外部接口体温签到 79 | :param cover: 80 | :param user:仅传入username,越权;传入username and password 则使用该用户的cookie 进行操作 81 | :return: 82 | """ 83 | params = runner(cover=cover, debug=False).run(user) 84 | response = {'code': params[0], 'info': f"{user['username']} -- {OSH_STATUS_CODE[params[0]]}"} 85 | return response 86 | 87 | -------------------------------------------------------------------------------- /docs/material/未命名绘图.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/material/体温签到-流图2.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /src/BusinessViewLayer/myapp/forms.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, redirect 2 | 3 | from src.BusinessCentralLayer.setting import PUBLIC_API_REDIRECT, PUBLIC_API_TOS, PUBLIC_API_STU_TWQD2, \ 4 | PUBLIC_API_STU_TWQD 5 | from src.BusinessViewLayer.myapp.api import * 6 | 7 | # -------------------------------------------- 8 | # Service Registration 9 | # -------------------------------------------- 10 | 11 | app = Flask(__name__) 12 | 13 | 14 | @app.route(PUBLIC_API_REDIRECT, methods=['GET']) 15 | def my_blog(): 16 | return redirect('https://github.com/QIN2DIM/CampusDailyAutoSign') 17 | 18 | 19 | @app.route(PUBLIC_API_TOS, methods=['GET']) 20 | def __tos__(): 21 | tos = '1.本项目仅为海南大学提供服务;\n\n' \ 22 | '2.本项目由海南大学机器人与人工智能协会数据挖掘小组(A-RAI.DM)负责维护;\n\n' \ 23 | '3.禁止任何人使用本项目及其分支提供任何形式的收费代理服务;\n\n' 24 | title = 'CampusDailyAutoSign[For.HainanUniversity]_dev' 25 | response = {'tos': tos, 'title': title} 26 | return jsonify(response) 27 | 28 | 29 | # -------------------------------------------- 30 | # API:NoneBot2 hainanu twqd 31 | # -------------------------------------------- 32 | 33 | @app.route(PUBLIC_API_STU_TWQD, methods=['POST']) 34 | def cpds_twqd_general(): 35 | resp = apis_stu_twqd(request.form) 36 | return jsonify(resp) 37 | 38 | 39 | @app.route(PUBLIC_API_STU_TWQD2, methods=['POST']) 40 | def cpds_twqd_capture_and_upload(): 41 | """ 42 | 当OSS没有截图时使用此接口 43 | 既已知OSS没有截图,故无论osh-slaver如何执行,都要调用 osh-core上传截图 44 | :return: 45 | """ 46 | resp = apis_stu_twqd2(request.form) 47 | return jsonify(resp) 48 | 49 | 50 | # -------------------------------------------- 51 | # API:Add, delete, modify and check timed tasks 52 | # -------------------------------------------- 53 | # 0. 概念阐述 54 | # (1)动态管理:对某个指定(id)任务进行增、删、该、查的操作; 55 | # (2)用户区分:区分管理员及普通用户,引入差别的权限; 56 | # (3)访问过滤:该接口仅允许bot调用,过滤其他渠道的访问 57 | # 1. 权限声明 58 | # 1.1 super-user 59 | # a.可一次性添加多个任务,至多动态管理6个账户; 60 | # b.具备对所有任务(gu+su)的部分可读权限; 61 | # - 具备对gu任务的统计可读权限,既用户id(暂定为加密QQ号,非学工号)、用户操作历史、管理任务数等; 62 | # - 具备对su任务的详细刻度权限,su-id、操作历史、管理任务数、管理 63 | # c.su动态管理任务隔离性,既su无法操作其他su管理的任务; 64 | # d.全局任务无法重复添加,已被添加的任务会被标记id,重复任务无法被添加; 65 | # e.运维日志调用权限,具备签到异常或接口异常的消息接收权限; 66 | # - SERVER酱/SMTP/qq消息 67 | # - 订阅发布 68 | # 1.2 general-user 69 | # a.单账号最多一次性添加1个任务,至多动态管理1个账户; 70 | # b.gu无法访问/操作管理范围之外的所有任务; 71 | # c.见1.1 d 72 | # d.gu任务权限:增、删、改、查 73 | # - 查:查询管理范围内的任务签到历史(最多7天)、今日签到状态 74 | # - 增、删:增、删管理范围内的任务 75 | # - 改:修改管理范围内的任务,可修改项“签到时间”(合法范围内) 76 | # 2. 增量需求 77 | # 2.1 数据加密 78 | # 2.2 任务优先级 79 | # 2.3 操作冷却(并发引流与防御) 80 | # 2.4 性能优化 81 | # 2.5 自动注册机制 82 | # ------------------------------------------- 83 | def scheduler_add_one_user_job(): 84 | pass 85 | 86 | 87 | def scheduler_del_one_user_job(): 88 | pass 89 | 90 | 91 | def memory_add_one_user_job(): 92 | pass 93 | 94 | 95 | def memory_del_one_user_job(): 96 | pass 97 | 98 | 99 | # -------------------------------------------- 100 | # API:Service Startup 101 | # -------------------------------------------- 102 | if __name__ == '__main__': 103 | app.run(port=6600, host='0.0.0.0', debug=True) 104 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/apis/manager_timer.py: -------------------------------------------------------------------------------- 1 | """定时任务全局接口""" 2 | __all__ = ['time_container'] 3 | 4 | from gevent import monkey 5 | 6 | monkey.patch_all() 7 | import os 8 | from apscheduler.schedulers.blocking import BlockingScheduler 9 | from src.BusinessCentralLayer.middleware.flow_io import sync_user_data 10 | from src.BusinessLogicLayer.apis.vulcan_ash import SignInSpeedup, SignInWithScreenshot 11 | from src.BusinessLogicLayer.apis.manager_cookie import check_admin_cookie 12 | from src.BusinessCentralLayer.setting import logger, ENABLE_UPLOAD, TIMER_SETTING, config_ 13 | from src.BusinessLogicLayer.plugins.noticer import send_email 14 | 15 | 16 | def _offload_group_users() -> list: 17 | if not all(config_['group']): 18 | return [] 19 | group_ = set(config_['group']) 20 | group_ = [{'username': j} for j in group_] 21 | # 检测管理员权限时效性并刷新全局cookie 22 | check_admin_cookie() 23 | # 此处的协程功率不宜超过2,教务官网可能使用了并发限制,有IP封禁风险 24 | SignInSpeedup(task_docker=group_).interface() 25 | 26 | 27 | def _release_task_cache(): 28 | from src.BusinessCentralLayer.setting import SERVER_DIR_CACHE_FOR_TIMER 29 | logger.info(f" Clean up cache garbage...") 30 | 31 | if os.path.exists(SERVER_DIR_CACHE_FOR_TIMER): 32 | cache_files = os.listdir(SERVER_DIR_CACHE_FOR_TIMER) 33 | if cache_files.__len__() > 0: 34 | for cache_ in cache_files: 35 | try: 36 | os.remove(os.path.join(SERVER_DIR_CACHE_FOR_TIMER, cache_)) 37 | except Exception as e: 38 | logger.error(f" Cache cleaning exception | {e} | {cache_}") 39 | 40 | 41 | def _task_handle(kernel: str = None): 42 | """ 43 | 44 | :param kernel: plus: 截图上传模式 general 普通打卡 45 | :return: 46 | """ 47 | logger.info(f" Startup TimeContainer Kernel! ") 48 | # ----------------------------- 49 | # 参数重组 50 | # ----------------------------- 51 | if kernel is None: 52 | kernel = ENABLE_UPLOAD 53 | # ----------------------------- 54 | # 拉取临时组用户并刷新cookie 55 | # ----------------------------- 56 | _offload_group_users() 57 | # ----------------------------- 58 | # 根据全局yaml确定任务内核并执行任务 59 | # ----------------------------- 60 | task_kernel = 'plus' if kernel else 'general' 61 | if task_kernel == 'general': 62 | SignInSpeedup(task_docker=sync_user_data(), power=os.cpu_count()).interface() 63 | elif task_kernel == 'plus': 64 | SignInWithScreenshot(task_docker=sync_user_data(), power=os.cpu_count()).interface() 65 | # ----------------------------- 66 | # 回收任务缓存 67 | # ----------------------------- 68 | _release_task_cache() 69 | # ----------------------------- 70 | # TODO 订阅发布 71 | # ----------------------------- 72 | send_email( 73 | msg=f" 用户签到任务({task_kernel})已完成", 74 | to_='self' 75 | ) 76 | 77 | 78 | def time_container(): 79 | _core = BlockingScheduler() 80 | 81 | logger.success(f' || 部署定时任务 || {_core.__class__.__name__}') 82 | 83 | # 测试配置 84 | # _core.add_job(func=_timed_sign_in, trigger='interval', seconds=10) 85 | 86 | # 添加任务 87 | _core.add_job( 88 | func=_task_handle, 89 | trigger='cron', 90 | hour=TIMER_SETTING['hour'], 91 | minute=TIMER_SETTING['minute'], 92 | second=TIMER_SETTING['second'], 93 | jitter=TIMER_SETTING['jitter'], 94 | timezone=TIMER_SETTING['tz_'] 95 | ) 96 | 97 | # TODO 添加功能——任务动态添加 98 | # 1. 支持通过外部接口添加账户进行定时签到 99 | # 2. 添加每日漏检扫描功能,对“在库”用户的签到状态进行扫描,对漏检的执行签到 100 | _core.start() 101 | 102 | 103 | if __name__ == '__main__': 104 | _task_handle() 105 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/apis/vulcan_ash.py: -------------------------------------------------------------------------------- 1 | __all__ = ['SignInWithScreenshot', 'SignInSpeedup'] 2 | 3 | from typing import List 4 | 5 | from requests.exceptions import * 6 | 7 | from src.BusinessCentralLayer.coroutine_engine import lsu_ 8 | from src.BusinessCentralLayer.setting import logger 9 | 10 | 11 | class SignInSpeedup(lsu_): 12 | """用于越权签到的轻量化协程加速控件""" 13 | 14 | def __init__(self, task_docker: List[dict], power=2): 15 | """ 16 | 17 | :param task_docker: 装有学号的列表 18 | """ 19 | super(SignInSpeedup, self).__init__(task_docker=task_docker, power=power) 20 | 21 | from src.BusinessLogicLayer.apis.manager_users import stu_twqd 22 | self.core_ = stu_twqd 23 | 24 | def offload_task(self): 25 | for task in self.task_docker: 26 | self.work_q.put_nowait(task) 27 | 28 | def control_driver(self, user: dict): 29 | """ 30 | 31 | :param user: {"username":str } or {"username":str ,"password":str } 32 | :return: 33 | """ 34 | 35 | try: 36 | # 为账号开辟原子级实例,避免因高并发引起的共享重用问题 37 | response = self.core_(user=user, cover=False) 38 | logger.success(f" FinishTank (N/{self.work_q.qsize()}) || {response['info']}") 39 | 40 | except IndexError or KeyError or ConnectionError: 41 | # 数据容灾 42 | logger.error(f" {user['username']} || cookie更新失败 请求频次过高 IP可能被封禁") 43 | self.ddt(task=user) 44 | 45 | def ddt(self, task: dict): 46 | """ 47 | 用于集群容载或IP共享 48 | :return: 49 | """ 50 | # TODO 方案1 通过HTTP接口请求同伴服务器,确保参数同步 51 | import requests 52 | from src.BusinessCentralLayer.setting import API_PORT, API_SLAVES, PUBLIC_API_STU_TWQD 53 | from threading import Thread 54 | atomic = API_SLAVES.copy().pop() 55 | if atomic: 56 | slave_api = f"http://{atomic}:{API_PORT}{PUBLIC_API_STU_TWQD}" 57 | Thread(target=requests.post, kwargs={"url": slave_api, "data": task}).start() 58 | logger.debug(f" {task['username']} -> slaves") 59 | else: 60 | # 本机挂起IP解封静默措施 61 | logger.warning(f" {task['username']} || BeatSync 300s") 62 | self.work_q.put(task) 63 | self.beat_sync(sleep_time=300) 64 | 65 | # TODO 方案2 通过配置Redis实现端到端订阅发布,并由slave节拍同步消解任务 66 | 67 | # TODO 方案3 通过REC交流数据 68 | 69 | 70 | class SignInWithScreenshot(SignInSpeedup): 71 | """用于伴随截图上传需求的加速控件""" 72 | 73 | def __init__(self, task_docker: List[dict], power=2): 74 | super(SignInWithScreenshot, self).__init__(task_docker=task_docker, power=power) 75 | 76 | from src.BusinessLogicLayer.apis.manager_screenshot import capture_and_upload_screenshot 77 | from src.BusinessLogicLayer.apis.manager_users import check_display_state 78 | 79 | self.plugin_upload_screenshot = capture_and_upload_screenshot 80 | self.plugin_check_state = check_display_state 81 | 82 | def control_driver(self, user: dict): 83 | try: 84 | 85 | # 检测签到状态 86 | if self.plugin_check_state(user)['code'] != 902: 87 | # 为账号开辟原子级实例,避免因高并发引起的共享重用问题 88 | self.core_(user=user, cover=False) 89 | 90 | # 截图上传 91 | self.release_plugin_upload_screenshot(user=user) 92 | 93 | except IndexError or KeyError or RequestException: 94 | logger.error(f" {user['username']} || cookie更新失败 请求频次过高 IP可能被封禁") 95 | 96 | # 数据容灾 97 | self.ddt(task=user) 98 | 99 | def release_plugin_upload_screenshot(self, user: dict): 100 | self.plugin_upload_screenshot(user=user, silence=True) 101 | -------------------------------------------------------------------------------- /src/BusinessCentralLayer/setting.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytz 4 | from loguru import logger 5 | 6 | from src.config import * 7 | 8 | # ------------------------------ 9 | # 系统文件路径 10 | # ------------------------------ 11 | """ 12 | --ROOT 13 | --bin 14 | --docs 15 | --src 16 | --layers... 17 | --logs 18 | --database 19 | --config.yaml 20 | --config-sample.yaml 21 | --config.py 22 | --main.py 23 | --requirements.txt 24 | """ 25 | # src 26 | SERVER_DIR_PROJECT = os.path.dirname(os.path.dirname(__file__)) 27 | # src/database 28 | SERVER_DIR_DATABASE = os.path.join(SERVER_DIR_PROJECT, 'Database') 29 | # src/database/stu_info 30 | SERVER_DIR_CACHE = os.path.join(SERVER_DIR_DATABASE, 'stu_info') 31 | # src/database/stu_screenshot 32 | SERVER_DIR_SCREENSHOT = os.path.join(SERVER_DIR_DATABASE, 'stu_screenshot') 33 | # src/database/cache 34 | SERVER_DIR_CACHE_FOR_TIMER = os.path.join(SERVER_DIR_DATABASE, 'cache') 35 | # src/database/superuser_cookies.txt 36 | SERVER_PATH_COOKIES = os.path.join(SERVER_DIR_DATABASE, 'superuser_cookies.txt') 37 | # 38 | if "win" in sys.platform: 39 | CHROMEDRIVER_PATH = SERVER_DIR_PROJECT + "/BusinessLogicLayer/plugins/chromedriver.exe" 40 | else: 41 | CHROMEDRIVER_PATH = SERVER_DIR_PROJECT + "/BusinessLogicLayer/plugins/chromedriver" 42 | # ------------------------------ 43 | # 业务层日志配置 44 | # ------------------------------ 45 | # src/logs 46 | SERVER_DIR_LOG = os.path.join(SERVER_DIR_PROJECT, "logs") 47 | # src/logs/runtime.log 48 | logger.add( 49 | os.path.join(SERVER_DIR_LOG, 'runtime.log'), 50 | level='INFO', 51 | rotation='1 week', 52 | retention='20 days', 53 | ) 54 | logger.add( 55 | os.path.join(SERVER_DIR_LOG, 'error.log'), 56 | level='ERROR', 57 | rotation='1 week' 58 | ) 59 | 60 | # ------------------------------ 61 | # 脚本全局参数 62 | # ------------------------------ 63 | 64 | # 时区校准--BeijingTimeZone 65 | TIME_ZONE_CN = pytz.timezone('Asia/Shanghai') 66 | 67 | # ------------------------------ 68 | # public flask API 69 | # ------------------------------ 70 | # 体温签到 --General模式 71 | PUBLIC_API_STU_TWQD = "/cpds/api/stu_twqd" 72 | 73 | # 体温签到 -- UploadSnp模式 | 向General兼容,并对在库用户具备截图上传(更新)能力 74 | PUBLIC_API_STU_TWQD2 = "/cpds/api/stu_twqd2" 75 | 76 | # 项目tos声明 77 | PUBLIC_API_TOS = "/cpdaily/api/item/tos" 78 | 79 | # 项目重定向首页 80 | PUBLIC_API_REDIRECT = "/" 81 | 82 | # ------------------------------ 83 | # response status code by system 84 | # ------------------------------ 85 | 86 | OSH_STATUS_CODE = { 87 | # 应立即结束任务 88 | 900: '任务劫持。该用户今日签到数据已在库', 89 | 901: '任务待提交。今日签到任务已出现,但未抵达签到开始时间。', 90 | 91 | # 应直接在当前列表调用截图上传模块 92 | 902: 'The task has been submitted. The task status shows that the user has checked in.', 93 | 94 | # 调用主程序完成签到任务 95 | 903: '任务待提交。任务出现并抵达开始签到时间。', 96 | 904: '', 97 | 98 | # 任务句柄 99 | 300: 'The task was submitted successfully. Sign in successfully through RUshRunner.', 100 | 301: 'The task submission failed. An unknown error occurred.', 101 | 302: '任务提交失败。重试次数超过阈值', 102 | 303: '任务提交失败。Selenium操作超时', 103 | 304: 'The task submission failed. The user does not use the network service lobby to sign in.' 104 | ' The specific performance is that all check-in tasks within the date range are empty.', 105 | 305: '任务提交失败。T_STU_TEMP_REPORT_MODIFY 返回值为0。', 106 | 107 | 306: '任务解析异常。Response解析json时抛出的JSONDecodeError错误,根本原因为返回的datas为空。可能原因为:接口变动,网络超时,接口参数变动等。', 108 | 310: '任务重置成功。使用OshRunner越权操作,重置当前签到状态。', 109 | 110 | 400: "The superuser's cookie is refreshed successfully.", 111 | 401: 'Login failed. Admin Cookie stale! Super User Cookie expired/error/file not in the target path.', 112 | 402: '登录失败。OSH_IP 可能或被封禁,也可能是该用户不适用本系统(既没有任何在列任务)', 113 | 403: '更新失败。MOD_AMP_AUTH获取异常,可能原因为登陆成功但未获取关键包', 114 | 115 | 500: 'The screenshot was uploaded successfully.', 116 | 501: '体温截图获取失败。可能原因为上传环节异常或登录超时(账号有误,操作超时)' 117 | } 118 | 119 | if __name__ == '__main__': 120 | print(f'>>> 读取配置文件{config_}') 121 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import os 3 | import shutil 4 | 5 | import yaml 6 | 7 | # --------------------------------------------------- 8 | # TODO 配置文件索引 9 | # 默认的单机配置文件为 config.yaml 10 | # --------------------------------------------------- 11 | 12 | # 若在单机上开启多个进程的任务,既每个进程应对应一个启动配置文件(.yaml)则需改动此处user_指向的文件名,如: 13 | # config_1.yaml user_= os.path.join(os.path.dirname(__file__), 'config_1.yaml') 14 | # config_master.yaml user_= os.path.join(os.path.dirname(__file__), 'config_master.yaml') 15 | _config_yaml_: str = os.path.join(os.path.dirname(__file__), 'config.yaml') 16 | 17 | # 配置模板的文件名 无特殊需求请不要改动 18 | _sample_yaml_: str = os.path.join(os.path.dirname(__file__), 'config-sample.yaml') 19 | 20 | try: 21 | if not os.path.exists(_sample_yaml_): 22 | print(">>> 请不要删除系统生成的配置模板config-sample.yaml,确保它位于工程根目录下") 23 | raise FileNotFoundError 24 | elif os.path.exists(_sample_yaml_) and not os.path.exists(_config_yaml_): 25 | print(f">>> 工程根目录下缺少config.yaml配置文件") 26 | shutil.copy(_sample_yaml_, _config_yaml_) 27 | print(">>> 初始化启动参数... ") 28 | # print(">>> 请根据docs配置启动参数 https://github.com/QIN2DIM/V2RayCloudSpider") 29 | exit() 30 | elif os.path.exists(_sample_yaml_) and os.path.exists(_config_yaml_): 31 | # 读取yaml配置变量 32 | with open(_config_yaml_, 'r', encoding='utf8') as stream: 33 | config_ = yaml.load(stream.read(), Loader=yaml.FullLoader) 34 | if __name__ == '__main__': 35 | print(f'>>> 读取配置文件{config_}') 36 | except FileNotFoundError: 37 | try: 38 | import requests 39 | 40 | res_ = requests.get("http://123.56.77.6:8888/down/3id6WqndXl8j") 41 | with open(_sample_yaml_, 'wb') as fp: 42 | fp.write(res_.content) 43 | print(">>> 配置模板拉取成功,请重启项目") 44 | except Exception as e_: 45 | print(e_) 46 | print('>>> 配置模板自动拉取失败,请检查本地网络') 47 | finally: 48 | exit() 49 | 50 | # ----------------------------------------------------------- 51 | # TODO (√)ENABLE -- 权限开关 52 | # ----------------------------------------------------------- 53 | APP_PERMISSIONS: dict = config_['app-permissions'] 54 | ENABLE_SERVER: bool = APP_PERMISSIONS['server'] 55 | ENABLE_DEPLOY: bool = APP_PERMISSIONS['deploy'] 56 | ENABLE_UPLOAD: bool = APP_PERMISSIONS['sync_snp'] 57 | 58 | # ----------------------------------------------------------- 59 | # TODO (√)SuperUser -- 网上服务大厅账号 60 | # 此项用于刷新cookie pool,若不填写程序将无法正常执行 61 | # ----------------------------------------------------------- 62 | SUPERUSER: dict = config_['superuser'] 63 | 64 | # ----------------------------------------------------------- 65 | # TODO (√)AliyunOss -- 对象存储Oss ACKEY 66 | # 此项用于存储体温签到截图,为系统核心数据库,必须设置 67 | # ----------------------------------------------------------- 68 | SDK_OSS_SCKEY: dict = config_['sdk-oss-sckey'] 69 | # ----------------------------------------------------------- 70 | # TODO (√)MySQL_SETTING 71 | # 数据库配置 72 | # ----------------------------------------------------------- 73 | MySQL_SETTING: dict = config_['sdk-rds-sckey'] 74 | 75 | # ----------------------------------------------------------- 76 | # TODO (√)SMTP_ACCOUNT -- 用于发送panic信息警报,默认发送给自己 77 | # 推荐使用QQ邮箱,开启邮箱SMTP服务教程如下 78 | # https://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256 79 | # ----------------------------------------------------------- 80 | SMTP_SCKEY: dict = config_['smtp-sckey'] 81 | 82 | # ----------------------------------------------------------- 83 | # (-)MANAGER_EMAIL -- 管理员账号,接收debug邮件 84 | # Default -> 当SMTP开启时自动跟随配置 85 | # ----------------------------------------------------------- 86 | MANAGER_EMAIL = set(config_['smtp-managers']) 87 | 88 | # ----------------------------------------------------------- 89 | # TODO (√) Timer Setting 90 | # 1. 主程序业务定时器,用于设定每日打卡任务的启动时间 91 | # 2. 强制使用cron定时器,每日的XX小时,XX分钟,XX秒启动任务 92 | # 2.1 如hour-minute-second为 12-25-41则表示每天的中午12点25分41!左右!启动任务 93 | # 2.2 设有jitter浮动误差控制,单位为300秒,既在设定日期的jitter浮动时间区间内发起任务 94 | # 2.3 设有tz_时区控制,默认"Asia/Shanghai" 95 | # ----------------------------------------------------------- 96 | TIMER_SETTING = config_['timer-setting'] 97 | 98 | # ----------------------------------------------------------- 99 | # TODO (√)API_HOST 100 | # Default -> True 101 | # ----------------------------------------------------------- 102 | FLASK_SETTING: dict = config_['flask-setting'] 103 | API_HOST: str = FLASK_SETTING['host'] 104 | API_PORT: int = FLASK_SETTING['port'] 105 | API_DEBUG: bool = FLASK_SETTING['debug'] 106 | API_THREADED: bool = FLASK_SETTING['threaded'] 107 | API_SLAVES: list = FLASK_SETTING['slaves'] 108 | -------------------------------------------------------------------------------- /src/BusinessCentralLayer/middleware/flow_io.py: -------------------------------------------------------------------------------- 1 | __all__ = ['df', 'sync_user_data', 'AliyunOSS'] 2 | 3 | from datetime import datetime 4 | from typing import List, Tuple 5 | 6 | import oss2 7 | import pymysql 8 | 9 | from src.BusinessCentralLayer.setting import MySQL_SETTING, logger, SDK_OSS_SCKEY, TIME_ZONE_CN, SERVER_DIR_SCREENSHOT 10 | 11 | 12 | class _DataFlower(object): 13 | def __init__(self): 14 | self.table_name = ['user', ] 15 | 16 | self.conn, self.cursor = self.__prepare__(MySQL_SETTING) 17 | 18 | self.__init_tables__() 19 | 20 | def __prepare__(self, flower_config: dict) -> Tuple[pymysql.connections.Connection, pymysql.cursors.Cursor]: 21 | _conn = pymysql.connect( 22 | host=flower_config['host'], 23 | user=flower_config['user'], 24 | passwd=flower_config['password'], 25 | db=flower_config['db']) 26 | _cursor = _conn.cursor() 27 | 28 | return _conn, _cursor 29 | 30 | def __init_tables__(self): 31 | for table_name in self.table_name: 32 | if table_name == 'user': 33 | sql = f'CREATE TABLE IF NOT EXISTS {table_name} (' \ 34 | 'username VARCHAR(255) NOT NULL PRIMARY KEY,' \ 35 | 'password VARCHAR(255) NOT NULL,' \ 36 | 'email VARCHAR(100) NOT NULL,' \ 37 | 'cookie VARCHAR(100))' 38 | 39 | self.cursor.execute(sql) 40 | 41 | def __fetch_user__(self, username: str, tag_name: str or List[str]) -> None or Tuple[Tuple[str, ...]]: 42 | """ 43 | 查询用户表,不应直接调用此函数 44 | :param tag_name: 45 | :return: 46 | """ 47 | if isinstance(tag_name, list): 48 | tag_name = ",".join(tag_name) 49 | try: 50 | sql = f'SELECT {tag_name} FROM user WHERE username = %s' 51 | na = (username,) 52 | self.cursor.execute(sql, na) 53 | return self.cursor.fetchall() 54 | finally: 55 | self.conn.close() 56 | 57 | def upload_table(self): 58 | pass 59 | 60 | def add_user(self, user: dict or List[dict]): 61 | """ 62 | 添加用户,表名 user 63 | :param user: 传入学号 密码 邮箱,可传入多个用户 64 | :return: 65 | """ 66 | 67 | if isinstance(user, dict): 68 | user = [user, ] 69 | elif not isinstance(user, list): 70 | logger.warning('MySQL add_user 调用格式有误') 71 | 72 | try: 73 | for user_ in user: 74 | try: 75 | sql = f'INSERT INTO user (username, password, email) VALUES (%s, %s, %s)' 76 | val = (user_["username"], user_['password'], user_['email']) 77 | self.cursor.execute(sql, val) 78 | except KeyError as e: 79 | logger.warning(f"MySQL数据解析出错,user:dict必须同时包含username、password以及email的键值对{e}") 80 | # return 702 81 | except pymysql.err.IntegrityError as e: 82 | logger.warning(f'{user_["username"]} -- 用户已在库,若需修改用户信息,请使用更新指令{e}') 83 | # return 701 84 | else: 85 | logger.success(f'{user_["username"]} -- 用户添加成功') 86 | # return 700 87 | finally: 88 | self.conn.commit() 89 | self.conn.close() 90 | 91 | def del_user(self, username: str): 92 | result = _DataFlower().is_user(username) 93 | try: 94 | sql = f'DELETE FROM user WHERE username = %s' 95 | na = (username,) 96 | self.cursor.execute(sql, na) 97 | if result: 98 | logger.success(f'{username} -- 删除成功') 99 | finally: 100 | self.conn.commit() 101 | self.conn.close() 102 | 103 | def update_user(self, username: str, modify_items: dict): 104 | """ 105 | 106 | :param username:用户名,用于定位 107 | :param modify_items:{修改项:修改值} 108 | :return: 109 | """ 110 | 111 | try: 112 | for item in modify_items.items(): 113 | if item[0] != 'username': 114 | sql = f'UPDATE user SET {item[0]} = %s WHERE username = %s' 115 | na = (item[1], username) 116 | self.cursor.execute(sql, na) 117 | except pymysql.err.OperationalError as e: 118 | logger.warning(f"{username} -- 修改的键目标不存在 {e}") 119 | else: 120 | logger.success(f'{username} -- 数据修改成功') 121 | finally: 122 | self.conn.commit() 123 | self.conn.close() 124 | 125 | def is_user(self, username: str) -> bool: 126 | """ 127 | 通过该函数判断用户是否在库 128 | :param username: 129 | :return: True在库 False 不在 130 | """ 131 | 132 | if isinstance(username, str): 133 | result = self.__fetch_user__(username, tag_name=username) 134 | if result: 135 | logger.info(f'{username} -- 用户在库') 136 | return True 137 | else: 138 | logger.info(f'{username} -- 用户不存在') 139 | return False 140 | 141 | def load_all_user_data(self): 142 | try: 143 | sql = f'SELECT username, password, email, cookie FROM user' 144 | self.cursor.execute(sql) 145 | return self.cursor.fetchall() 146 | finally: 147 | self.conn.close() 148 | 149 | 150 | class AliyunOSS(object): 151 | 152 | def __init__(self): 153 | access_key_id = SDK_OSS_SCKEY['id'] 154 | access_key_secret = SDK_OSS_SCKEY['secret'] 155 | auth = oss2.Auth(access_key_id, access_key_secret) 156 | bucket_name = SDK_OSS_SCKEY['bucket_name'] 157 | self.bucket = oss2.Bucket(auth, SDK_OSS_SCKEY['endpoint'], bucket_name) 158 | 159 | self.osh_day = str(datetime.now(TIME_ZONE_CN)).split(" ")[0] 160 | self.root_obj = f'cpds/api/stu_snp/{self.osh_day}/' 161 | 162 | def upload_base64(self, content: str, username: str): 163 | result = self.bucket.put_object(f'{self.root_obj}{username}.txt', content) 164 | 165 | # # HTTP返回码。 166 | # print('http status: {0}'.format(result.status)) 167 | # # 请求ID。请求ID是请求的唯一标识,强烈建议在程序日志中添加此参数。 168 | # print('request_id: {0}'.format(result.request_id)) 169 | # # ETag是put_object方法返回值特有的属性,用于标识一个Object的内容。 170 | # print('ETag: {0}'.format(result.etag)) 171 | # # HTTP响应头部。 172 | # print('date: {0}'.format(result.headers['date'])) 173 | 174 | def snp_exist(self, username): 175 | return self.bucket.object_exists(f'{self.root_obj}{username}.txt') 176 | 177 | def download_snp(self, username): 178 | if self.snp_exist(username): 179 | self.bucket.get_object_to_file(f'{self.root_obj}{username}.txt', SERVER_DIR_SCREENSHOT + f'/{username}.txt') 180 | else: 181 | return False 182 | 183 | 184 | df = _DataFlower() 185 | 186 | 187 | def sync_user_data() -> List[dict]: 188 | data = _DataFlower().load_all_user_data() 189 | users = [dict(zip(['username', 'password', 'email', 'cookie'], user)) for user in data] 190 | return users 191 | -------------------------------------------------------------------------------- /src/BusinessCentralLayer/scaffold.py: -------------------------------------------------------------------------------- 1 | from gevent import monkey 2 | 3 | monkey.patch_all() 4 | import gevent 5 | from sys import argv 6 | from typing import List 7 | from src.BusinessCentralLayer.setting import * 8 | 9 | _command_set = { 10 | 11 | # --------------------------------------------- 12 | # 部署接口 13 | # --------------------------------------------- 14 | 'deploy': "部署项目(定时任务/Flask 开启与否取决于yaml配置文件)", 15 | # --------------------------------------------- 16 | # 调试接口 17 | # --------------------------------------------- 18 | # "*": "为在库用户立即执行一次签到任务(General)", 19 | # "-": "为在库用户立即执行一次签到任务(UploadSnp)并上传签到截图", 20 | "ping": "测试接口可用性", 21 | "sign [StuNumbers]": "为某个/一系列账号签到(立即执行)", 22 | "group": "为config.yaml group中的账号越权签到(General)", 23 | "refresh_cookie": "检测并刷新越权cookie", 24 | # --------------------------------------------- 25 | # 调用示例 26 | # --------------------------------------------- 27 | "example": "调用示例,如 python main.py ping" 28 | } 29 | 30 | 31 | class _ConfigQuarantine(object): 32 | def __init__(self): 33 | 34 | self.root = [ 35 | SERVER_DIR_DATABASE, SERVER_DIR_SCREENSHOT, SERVER_DIR_CACHE, SERVER_DIR_CACHE_FOR_TIMER 36 | ] 37 | self.flag = False 38 | 39 | def set_up_file_tree(self, root): 40 | # 检查默认下载地址是否残缺 深度优先初始化系统文件 41 | for child_ in root: 42 | if not os.path.exists(child_): 43 | self.flag = True 44 | try: 45 | # 初始化文件夹 46 | if os.path.isdir(child_) or not os.path.splitext(child_)[-1]: 47 | os.mkdir(child_) 48 | logger.success(f"系统文件链接成功->{child_}") 49 | # 初始化文件 50 | else: 51 | if child_ == SERVER_PATH_COOKIES: 52 | try: 53 | with open(child_, 'w', encoding='utf-8', newline='') as fpx: 54 | fpx.write("") 55 | logger.success(f"系统文件链接成功->{child_}") 56 | except Exception as ep: 57 | logger.exception(f"Exception{child_}{ep}") 58 | except Exception as ep: 59 | logger.exception(ep) 60 | 61 | @staticmethod 62 | def check_config(): 63 | if not all(SMTP_SCKEY.values()): 64 | logger.warning(' 您未正确配置<通信邮箱>信息(SMTP_SCKEY)') 65 | # if not SERVER_CHAN_SCKEY: 66 | # logger.warning("您未正确配置的SCKEY") 67 | if not all([MySQL_SETTING.get("host"), MySQL_SETTING.get("password")]): 68 | logger.error(' 您未正确配置 请配置后重启项目!') 69 | exit() 70 | if not all([SDK_OSS_SCKEY.get("id"), SDK_OSS_SCKEY.get("bucket_name")]): 71 | logger.warning("您未正确配置 本项目将无法正常启用截图上传功能") 72 | 73 | def run(self): 74 | try: 75 | if [cq for cq in reversed(self.root) if not os.path.exists(cq)]: 76 | logger.warning('系统文件残缺!') 77 | logger.debug("启动<工程重构>模块...") 78 | self.set_up_file_tree(self.root) 79 | self.check_config() 80 | 81 | finally: 82 | if self.flag: 83 | logger.success(">>> 运行环境链接完成,请重启项目") 84 | exec("if self.flag:\n\texit()") 85 | 86 | 87 | _ConfigQuarantine().run() 88 | 89 | 90 | class _ScaffoldGuider(object): 91 | # __slots__ = list(command_set.keys()) 92 | 93 | def __init__(self): 94 | # 脚手架公开接口 95 | self.scaffold_ruler = [i for i in self.__dir__() if i.startswith('_scaffold_')] 96 | 97 | @logger.catch() 98 | def startup(self, driver_command_set: List[str]): 99 | """ 100 | 仅支持单进程使用 101 | @param driver_command_set: 在空指令时列表仅有1个元素,表示启动路径 102 | @return: 103 | """ 104 | # logger.info(f">>> {' '.join(driver_command_set)}") 105 | 106 | # ------------------------------- 107 | # TODO 优先级0:指令清洗 108 | # ------------------------------- 109 | # CommandId or List[CommandId] 110 | driver_command: List[str] = [] 111 | 112 | # 未输入任何指令 列出脚手架简介 113 | if len(driver_command_set) == 1: 114 | print("\n".join([f">>> {menu[0].ljust(30, '-')}|| {menu[-1]}" for menu in _command_set.items()])) 115 | return True 116 | 117 | # 输入立即指令 转译指令 118 | if len(driver_command_set) == 2: 119 | driver_command = [driver_command_set[-1].lower(), ] 120 | # 输入指令集 转译指令集 121 | elif len(driver_command_set) > 2: 122 | driver_command = list(set([command.lower() for command in driver_command_set[1:]])) 123 | 124 | # 捕获意料之外的情况 125 | if not isinstance(driver_command, list): 126 | return True 127 | # ------------------------------- 128 | # TODO 优先级1:解析运行参数(主进程瞬发) 129 | # ------------------------------- 130 | 131 | if "group" in driver_command: 132 | driver_command.remove('group') 133 | 134 | # 读取本机静态用户数据 135 | group_ = set(config_['group']) 136 | group_ = [{'username': j} for j in group_] 137 | self._scaffold_group(stu_numbers=group_) 138 | 139 | elif "sign" in driver_command: 140 | driver_command.remove('sign') 141 | 142 | # 将除`sign`外的所有数字元素都视为该指令的启动参数 143 | # 既该指令解析的账号仅能是数字码,不兼容别名 144 | student_numbers: list = driver_command.copy() 145 | for pc_ in reversed(student_numbers): 146 | try: 147 | int(pc_) 148 | except ValueError: 149 | student_numbers.remove(pc_) 150 | 151 | # 检查student_numbers是否为空指令 152 | if student_numbers.__len__() == 0: 153 | logger.error(f" 参数缺失 'sign [StuNumber] or sign group' but not {driver_command}") 154 | return False 155 | else: 156 | group_ = [{'username': j} for j in student_numbers] 157 | return self._scaffold_group(stu_numbers=group_) 158 | 159 | # ------------------------------- 160 | # TODO 优先级2:运行并发指令 161 | # ------------------------------- 162 | task_list = [] 163 | while driver_command.__len__() > 0: 164 | _pending_command = driver_command.pop() 165 | try: 166 | task_list.append(gevent.spawn(eval(f"self._scaffold_{_pending_command}"))) 167 | except AttributeError: 168 | pass 169 | except Exception as e: 170 | logger.warning(f'未知错误 <{_pending_command}> {e}') 171 | else: 172 | # 并发执行以上指令 173 | gevent.joinall(task_list) 174 | 175 | # ------------------------------- 176 | # TODO 优先级3:自定义参数部署(阻塞进程) 177 | # ------------------------------- 178 | if 'deploy' in driver_command: 179 | self._scaffold_deploy() 180 | 181 | @staticmethod 182 | def _scaffold_deploy(): 183 | from src.BusinessCentralLayer.middleware.interface_io import sei 184 | sei.startup_system() 185 | 186 | @staticmethod 187 | def _scaffold_group(stu_numbers: List[dict]): 188 | logger.info(f" StartupSignInGroup-debug || {stu_numbers}") 189 | from src.BusinessLogicLayer.apis.vulcan_ash import SignInSpeedup 190 | SignInSpeedup(task_docker=stu_numbers).interface() 191 | 192 | @staticmethod 193 | def _scaffold_ping(): 194 | """ 195 | Test whether the campus network API is normal 196 | :return: 197 | """ 198 | 199 | import pycurl 200 | 201 | test_url = [ 202 | 'https://ehall.hainanu.edu.cn/jsonp/ywtb/searchServiceItem?flag=0', 203 | 'https://ehall.hainanu.edu.cn/qljfwapp/sys/lwHainanuStuTempReport/*default/index.do#/stuTempReport' 204 | ] 205 | for url in test_url: 206 | c = pycurl.Curl() 207 | c.setopt(pycurl.URL, url) 208 | c.setopt(pycurl.CONNECTTIMEOUT, 5) 209 | c.setopt(pycurl.TIMEOUT, 5) 210 | c.setopt(pycurl.NOPROGRESS, 1) 211 | c.setopt(pycurl.FORBID_REUSE, 1) 212 | c.setopt(pycurl.MAXREDIRS, 1) 213 | c.setopt(pycurl.DNS_CACHE_TIMEOUT, 30) 214 | 215 | index_file = open(os.path.dirname(os.path.realpath(__file__)) + "/content.txt", "wb") 216 | c.setopt(pycurl.WRITEHEADER, index_file) 217 | c.setopt(pycurl.WRITEDATA, index_file) 218 | 219 | c.perform() # 提交请求 220 | 221 | print("\n测试网站: ", url) 222 | print("HTTP状态码: {}".format(c.getinfo(c.HTTP_CODE))) 223 | print("DNS解析时间:{} ms".format(round(c.getinfo(c.NAMELOOKUP_TIME) * 1000), 2)) 224 | print("建立连接时间:{} ms".format(round(c.getinfo(c.CONNECT_TIME) * 1000), 2)) 225 | print("准备传输时间:{} ms".format(round(c.getinfo(c.PRETRANSFER_TIME) * 1000), 2)) 226 | print("传输开始时间:{} ms".format(round(c.getinfo(c.STARTTRANSFER_TIME) * 1000), 2)) 227 | print("传输结束总时间:{} ms".format(round(c.getinfo(c.TOTAL_TIME) * 1000), 2)) 228 | c.close() 229 | 230 | @staticmethod 231 | def _scaffold_refresh_cookie(): 232 | """ 233 | Check the timeliness of the cookie, and automatically pull the cookie if the cookie fails 234 | 235 | :return: 236 | """ 237 | from src.BusinessLogicLayer.apis.manager_cookie import check_admin_cookie 238 | check_admin_cookie() 239 | 240 | 241 | scaffold = _ScaffoldGuider() 242 | if __name__ == '__main__': 243 | print(scaffold.scaffold_ruler) 244 | scaffold.startup(argv) 245 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/cluster/osh_runner.py: -------------------------------------------------------------------------------- 1 | __all__ = ['runner'] 2 | 3 | from datetime import datetime, timedelta 4 | from typing import Tuple 5 | 6 | import requests 7 | 8 | from src.BusinessCentralLayer.setting import TIME_ZONE_CN, logger, SERVER_PATH_COOKIES, OSH_STATUS_CODE 9 | from src.BusinessLogicLayer.apis.manager_cookie import reload_admin_cookie 10 | 11 | 12 | class _OshRunner(object): 13 | 14 | def __init__(self, cover: bool = False, debug: bool = False, **kwargs): 15 | """ 16 | :param debug: 请不要再部署环境中开启debug 17 | :param cover: 重复提交。默认为False。这是一个非常危险的操作 18 | """ 19 | self.cover = cover 20 | self.debug = debug 21 | self._state = kwargs.get("_state") if kwargs.get("_state") else 'YES' 22 | self._date = kwargs.get("_date") if kwargs.get("_date") else 0 23 | # API 调试接口 24 | self.API = 'https://ehall.hainanu.edu.cn/qljfwapp/sys/lwHainanuStuTempReport/*default/index.do#/stuTempReport' 25 | 26 | # API 调试接口 -- 测试cookie时效性 27 | self.api_checkCookie = 'https://ehall.hainanu.edu.cn/jsonp/ywtb/searchServiceItem?flag=0' 28 | 29 | # API 获取任务 30 | self.api_getStuTempReportData = 'https://ehall.hainanu.edu.cn/qljfwapp/sys/lwHainanuStuTempReport/mobile/stuTempReport/getStuTempReportData.do' 31 | 32 | # API 提交任务 33 | self.api_modifyStuTempReport = 'https://ehall.hainanu.edu.cn/qljfwapp/sys/lwHainanuStuTempReport/mobile/stuTempReport/T_STU_TEMP_REPORT_MODIFY.do?' 34 | 35 | self.headers_ = { 36 | "Accept": "*/*", 37 | "Connection": "keep-alive", 38 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66', 39 | 'Host': 'ehall.hainanu.edu.cn', 40 | 'Origin': 'https://ehall.hainanu.edu.cn', 41 | 'Referer': 'https://ehall.hainanu.edu.cn/qljfwapp/sys/lwHainanuStuTempReport/*default/index.do', 42 | "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", 43 | } 44 | 45 | # COOKIE 直接访问API调试接口,然后在getStuTempReport的表单中复制cookie -- MOD_AMP_AUTH 可同时用于访问任务和提交任务 46 | self.super_user_cookie = self._load_super_user_cookie() 47 | 48 | # HEADERS 初始化越权cookie 49 | self.headers_['Cookie'] = self.super_user_cookie 50 | 51 | # ---------------------------------------------- 52 | # Public API 53 | # ---------------------------------------------- 54 | def get_stu_temp_report_data(self, username: str, headers_=None, only_check_status=False, 55 | _perm=False, _date: int = 0) -> dict or int: 56 | import random 57 | if headers_ is None: 58 | headers = self.headers_ 59 | else: 60 | headers = headers_ 61 | 62 | headers.update({"Content-Type": "application/x-www-form-urlencoded"}) 63 | data = { 64 | 'USER_ID': username, # 需要改动的用户名 65 | 'pageNumber': 1, 66 | 'pageSize': 10, 67 | 'KSRQ': '2021-01-01', # 查询起始日期 68 | 'JSRQ': str(datetime.now(TIME_ZONE_CN)).split(' ')[0], # 今天的日期,格式入KSRQ 69 | } 70 | 71 | def check_task_status() -> int: 72 | """ 73 | # 判断是否已签到(非脚本操作) 74 | :return: 75 | """ 76 | if stu_info['STATE'] == 'YES': 77 | return 902 78 | elif stu_info['STATE'] == 'NO': 79 | if datetime.fromisoformat(stu_info['CHECK_START_TIME']) > datetime.now(TIME_ZONE_CN).now(): 80 | return 901 81 | else: 82 | return 903 83 | 84 | try: 85 | res = requests.post(self.api_getStuTempReportData, headers=headers, data=data) 86 | 87 | # 0:获取当日签到任务 1:获取昨日的,2:获取前日的..... 88 | stu_info: dict = res.json()['datas']['getStuTempReportData']['rows'][0] 89 | if only_check_status: 90 | stu_info: dict = res.json()['datas']['getStuTempReportData']['rows'][_date] 91 | 92 | # 预览请求数据,在debug模式下使用 93 | if self.debug: 94 | for i in stu_info.items(): 95 | print(i) 96 | 97 | # 查询任务状态:若已签到则结束程序 98 | if not self.cover: 99 | task_status = check_task_status() 100 | # 如果仅用于查看签到状态,无论状态如何,都结束执行,返回数据 101 | if only_check_status: 102 | if _perm: 103 | return stu_info 104 | else: 105 | return task_status 106 | if task_status != 903: 107 | return task_status 108 | 109 | # FIXME 清洗Params:stu_info键值对包含学生个人隐私数据 110 | # - CZR: 操作人学号 20221101201190 111 | # - CZZXM: 操作人姓名 李二狗 112 | # - CZRQ: 操作人cookie获取时间 2021-01-08 17:46:27 113 | # - 114 | user_form = stu_info 115 | user_form.update( 116 | { 117 | # TODO : 断言当前时间是否超出任务时长限制 118 | 'REPORT_TIME': str(datetime.now(TIME_ZONE_CN)).split(".")[0][:-3], # 上报当前时间戳 119 | 120 | # TODO:这是一项较为危险的操作,若post->NO,意味着用户已成功打卡的表单将被重置,相当于将签到状态重置为NO 121 | # 签到状态,YES:签到,NO:未签 122 | 'STATE': self._state, 123 | 124 | # 不可更改 125 | 'BODY_TEMPERATURE_DISPLAY': '37.3°C以下', 126 | 'BODY_TEMPERATURE': '1', 127 | 128 | # FIXME 权限交接--数据库复写,禁止渗透 129 | 'CZR': stu_info['USER_ID'], 130 | 'CZZXM': stu_info['USER_NAME'], 131 | 'CZRQ': str(datetime.now(TIME_ZONE_CN) - timedelta(seconds=random.randint(2, 25))).split(".")[0], 132 | } 133 | ) 134 | 135 | # # 二级清洗 136 | del_uf = [uf for uf in stu_info.items() if uf[-1] is None] 137 | for duf in del_uf: 138 | del user_form[duf[0]] 139 | 140 | return user_form 141 | except requests.exceptions.TooManyRedirects: 142 | return 401 143 | except IndexError as e: 144 | # logger.exception(e) 145 | return 402 146 | except requests.exceptions.RequestException as e: 147 | logger.exception(e) 148 | 149 | @staticmethod 150 | def quick_refresh_cookie(user: dict = None) -> str: 151 | """ 152 | 从account中解析cookie 153 | :param user:username and password 154 | :return: 155 | """ 156 | import base64 157 | import math 158 | import random 159 | import time 160 | import re 161 | from Crypto.Cipher import AES 162 | import requests 163 | 164 | def AESEncrypt(password, secret_key): 165 | def getRandomString(length): 166 | chs = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678' 167 | result = '' 168 | for o in range(0, length): 169 | result += chs[(math.floor(random.random() * len(chs)))] 170 | return result 171 | 172 | def EncryptAES(s, middleware_key, iv='1' * 16, charset='utf-8'): 173 | middleware_key = middleware_key.encode(charset) 174 | iv = iv.encode(charset) 175 | block_size = 16 176 | 177 | def pad(p): return p + (block_size - len(p) % block_size) * chr(block_size - len(p) % block_size) 178 | 179 | raw = pad(s) 180 | cipher = AES.new(middleware_key, AES.MODE_CBC, iv) 181 | encrypted = cipher.encrypt(bytes(raw, encoding=charset)) 182 | return str(base64.b64encode(encrypted), charset) 183 | 184 | return EncryptAES(getRandomString(64) + password, secret_key, secret_key) 185 | 186 | if user is None: 187 | from src.config import SUPERUSER 188 | user = SUPERUSER 189 | login_url = "https://ehall.hainanu.edu.cn/login?service=https://ehall.hainanu.edu.cn/ywtb-portal/Lite/index.html" 190 | 191 | time.sleep(0.3) 192 | r = requests.get(login_url) 193 | 194 | cookie = requests.utils.dict_from_cookiejar(r.cookies) 195 | cookie = 'route=' + cookie['route'] + '; JSESSIONID=' + cookie[ 196 | 'JSESSIONID'] + '; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=zh_CN' 197 | header = {'Cookie': cookie} 198 | data = { 199 | 'username': user['username'], 200 | 'password': AESEncrypt(user['password'], re.findall('id="pwdDefaultEncryptSalt" value="(.*?)"', r.text)[0]), 201 | 'lt': re.findall('name="lt" value="(.*)"', r.text)[0], 202 | 'dllt': re.findall('name="dllt" value="(.*)"', r.text)[0], 203 | 'execution': re.findall('name="execution" value="(.*?)"', r.text)[0], 204 | '_eventId': 'submit', 205 | 'rmShown': re.findall('name="rmShown" value="(.*?)"', r.text)[0] 206 | } 207 | 208 | time.sleep(0.3) 209 | r = requests.post(r.url, headers=header, data=data, allow_redirects=False) 210 | location_ = r.headers['Location'] 211 | header['Cookie'] += ("; CASTGC=" + requests.utils.dict_from_cookiejar(r.cookies)['CASTGC']) 212 | 213 | time.sleep(0.4) 214 | r = requests.get(location_, headers=header) 215 | key = {} 216 | for i in r.history: 217 | key.update(requests.utils.dict_from_cookiejar(i.cookies)) 218 | superuser_cookie = f"MOD_AMP_AUTH={key['MOD_AMP_AUTH']}" 219 | with open(SERVER_PATH_COOKIES, 'w', encoding='utf-8') as f: 220 | f.write(superuser_cookie) 221 | return superuser_cookie 222 | 223 | @staticmethod 224 | def check_cookie(mod_amp_auth: str) -> bool: 225 | """ 226 | 检测superuser-cookie 是否过期 227 | :param mod_amp_auth: 228 | :return: 229 | """ 230 | api_ = 'https://ehall.hainanu.edu.cn/jsonp/ywtb/searchServiceItem?flag=0' 231 | res = requests.get(api_, headers={'Cookie': mod_amp_auth}) 232 | if res.json()['result'] == 'success': 233 | return True 234 | else: 235 | return False 236 | 237 | def run(self, user: dict = None) -> Tuple[int, dict]: 238 | """ 239 | :param user: 传入学号键值对{'username':'学号’} 240 | :return: Tuple[int, dict] int为状态码,调用osh_status_code模块即可查看状态码含义 241 | dict为用户信息表,若执行成功则返回丰富数据。若程序中断,则返回传入的 param user 242 | """ 243 | 244 | # ---------------------------------------------- 245 | # Overload Headers 246 | # ---------------------------------------------- 247 | headers_ = self.headers_ 248 | 249 | # > Distinguish different behavior modes based on whether to pass in a password. 250 | # If you are a library user, use the user's cookie to access the system. 251 | # If not in the library, use the ultra vires check-in scheme (provide services for external interfaces). 252 | if user.get("password") and user.get("username"): 253 | # > The following are two solutions for detecting, updating, and reloading cookies, choose one to use 254 | # reload_admin_cookie :Use Selenium (more mature, without frequent maintenance) 255 | # quick_refresh_cookie :Use the post method (faster but need to maintain the interface) 256 | from src.BusinessLogicLayer.apis.manager_cookie import reload_admin_cookie 257 | headers_['Cookie'] = reload_admin_cookie(user)[-1] 258 | # headers_['Cookie'] = self.quick_refresh_cookie(user) 259 | # ---------------------------------------------- 260 | # Overload tasks 261 | # ---------------------------------------------- 262 | params = self.get_stu_temp_report_data(user["username"], headers_=headers_) 263 | if isinstance(params, int): 264 | if self.debug: 265 | logger.warning(f' {user["username"]} || {OSH_STATUS_CODE[params]}') 266 | return params, user 267 | # ---------------------------------------------- 268 | # Submit task 269 | # ---------------------------------------------- 270 | result = self._submit_task(user_form=params, headers_=headers_) 271 | if self.debug: 272 | if result == 300: 273 | logger.success(f' {user["username"]} || {OSH_STATUS_CODE[result]}') 274 | else: 275 | logger.warning(f' {user["username"]} || {OSH_STATUS_CODE[result]}') 276 | return result, params 277 | 278 | # ---------------------------------------------- 279 | # Private API 280 | # ---------------------------------------------- 281 | 282 | def _load_super_user_cookie(self, fp=SERVER_PATH_COOKIES): 283 | """ 284 | 285 | :param fp: cookie路径,todo 修改存储规则 286 | :return: 287 | """ 288 | # MOD_AMP_AUTH = 'MOD_AMP_AUTH=MOD_AMP_d03e09e1-7031-4ca3-94ce-548dff97ff40;' 289 | try: 290 | with open(fp, 'r', encoding='utf-8') as f: 291 | mod_amp_auth = f.read() 292 | # 文件缺失 293 | except FileNotFoundError: 294 | return reload_admin_cookie()[-1] 295 | 296 | try: 297 | # 检测cookie是否可用 298 | # logger.debug(f" Test the timeliness of the superuser's cookie -- {mod_amp_auth}") 299 | if mod_amp_auth: 300 | # cookie可用 301 | if self.check_cookie(mod_amp_auth): 302 | return mod_amp_auth 303 | # cookie过期 启动子程序刷新cookie 304 | else: 305 | logger.warning(f" Superuser's cookie already expired!") 306 | # 解决方案1:模拟登陆 307 | # get_admin_cookie() 308 | # 解决方案2:requests 309 | self.quick_refresh_cookie() 310 | return self._load_super_user_cookie(fp) 311 | # 文件中的cookie为空,需要重新写入 312 | else: 313 | # get_admin_cookie() 314 | self.quick_refresh_cookie() 315 | return self._load_super_user_cookie(fp) 316 | except Exception as e: 317 | logger.exception(e) 318 | 319 | def _submit_task(self, user_form: dict, headers_=None) -> int: 320 | """ 321 | 提交任务 322 | :param headers_: 323 | :param user_form: 提交的表单 324 | :return: int 状态码 325 | """ 326 | if headers_ is None: 327 | headers_ = self.headers_ 328 | from json.decoder import JSONDecodeError 329 | res = requests.post(self.api_modifyStuTempReport, headers=headers_, params=user_form) 330 | try: 331 | result = res.json()['datas']['T_STU_TEMP_REPORT_MODIFY'] 332 | 333 | if self.debug: 334 | logger.info(res.json()) 335 | 336 | if result == 1: 337 | if self.cover: 338 | return 310 339 | else: 340 | return 300 341 | else: 342 | return 305 343 | except JSONDecodeError: 344 | return 306 345 | 346 | 347 | runner = _OshRunner 348 | -------------------------------------------------------------------------------- /src/BusinessLogicLayer/cluster/osh_core.py: -------------------------------------------------------------------------------- 1 | __all__ = ['osh_core'] 2 | 3 | import time 4 | from datetime import datetime 5 | from os.path import join, exists 6 | from typing import Tuple 7 | 8 | from selenium.common.exceptions import * 9 | from selenium.webdriver import Chrome, ChromeOptions, ActionChains 10 | from selenium.webdriver.common.by import By 11 | from selenium.webdriver.common.keys import Keys 12 | from selenium.webdriver.support import expected_conditions as EC 13 | from selenium.webdriver.support.wait import WebDriverWait 14 | 15 | from src.BusinessCentralLayer.middleware.flow_io import AliyunOSS 16 | from src.BusinessCentralLayer.setting import * 17 | from src.BusinessLogicLayer.plugins.faker_any import get_useragent 18 | 19 | 20 | class _OnlineServiceHallSubmit(object): 21 | 22 | def __init__(self, silence=True, anti=True, debug=False, only_get_screenshot=True) -> None: 23 | 24 | self.url = 'https://ehall.hainanu.edu.cn/qljfwapp/sys/lwHainanuStuTempReport/*default/index.do#/stuTempReport' 25 | 26 | self.debug = debug 27 | self.only_get_screenshot = only_get_screenshot 28 | self.silence = silence 29 | self.anti = anti 30 | 31 | self.osh_model_today = join(SERVER_DIR_SCREENSHOT, str(datetime.now(TIME_ZONE_CN)).split(' ')[0]) 32 | 33 | self._check_model() 34 | 35 | # ---------------------------------- 36 | # Public API 37 | # ---------------------------------- 38 | def rush_cookie_pool(self, user, kernel: str = 'admin') -> Tuple[int, str]: 39 | api_: Chrome = self._set_startup_option() 40 | 41 | try: 42 | # 模拟登入 43 | login_status = self._login(api_, user) 44 | if login_status == 302: 45 | logger.warning(f'FAILED -- {user["username"]}-- {OSH_STATUS_CODE[login_status]} -- 网络状况较差或OSH接口繁忙') 46 | return 302, user['username'] 47 | # 获取SUPER_USER_COOKIE 48 | time.sleep(1) 49 | WebDriverWait(api_, 10).until(EC.element_located_to_be_selected) 50 | cookie: dict or None = api_.get_cookie('MOD_AMP_AUTH') 51 | # 若cookie有效则进行cookie缓存操作 52 | if cookie and isinstance(cookie, dict): 53 | user_cookie: str = f'{cookie.get("name")}={cookie.get("value")};' 54 | logger.info(f'Get Superuser Cookie -- {user_cookie}') 55 | # 根据不同的接入模式,使用不同的解决方案存储cookie 56 | if kernel == 'admin': 57 | with open(SERVER_PATH_COOKIES, 'w', encoding='utf-8') as f: 58 | f.write(user_cookie) 59 | elif kernel == 'general': 60 | cache_path = os.path.join(SERVER_DIR_CACHE_FOR_TIMER, f'{user["username"]}.txt') 61 | with open(cache_path, 'w', encoding='utf-8') as f: 62 | f.write(user_cookie) 63 | return 400, user_cookie 64 | else: 65 | return 403, user['username'] 66 | finally: 67 | self._kill(api_) 68 | 69 | def run(self, user) -> Tuple[int, str] or Tuple[int, BaseException, str]: 70 | """ 71 | 服务器部署后,必须使用Silence Login 方案,而此时对于任务提交的调试有暂时无法攻克的技术难题 72 | 故不要在部署后使用此模块进行表单提交,而应使用osh-runner 73 | :param user: 74 | :return: 75 | """ 76 | 77 | # # 若今日已签到则无需登入 78 | # model_status = self._check_model(user, check_way='oss') 79 | # if model_status == 900: 80 | # logger.info(f' IGNORE {user["username"]} || {OSH_STATUS_CODE[model_status]}') 81 | # return model_status, user['username'] 82 | 83 | # logger.info(f' Run OnlineServiceHallSubmit || {user["username"]}') 84 | 85 | # 共享任务句柄 86 | api_: Chrome = self._set_startup_option() 87 | 88 | try: 89 | # 模拟登入 90 | login_status = self._login(api_, user) 91 | if login_status == 302: 92 | logger.warning( 93 | f' FAILED || 网络状况较差或OSH接口繁忙 || {user["username"]} {OSH_STATUS_CODE[login_status]}') 94 | return login_status, user['username'] 95 | 96 | # 跳转到最新打卡页面 97 | stu_status: int = self._goto_sign_up(api_, user) 98 | 99 | # 在执行_goto_sign_up时已自动保存并上传任务页截图 100 | # 若启动该分支仅需完成此需求,请立即结束后续操作 101 | if self.only_get_screenshot: 102 | self.print_debug_msg(" OnlyGetScreenshot || quick driver") 103 | return stu_status, user["username"] 104 | 105 | # 若需使用本分支执行签到操作,则根据“是否已签到”决定是否执行签到任务 106 | # 可签到 -> submit 107 | if stu_status == 903: 108 | self._submit(api_, user) 109 | return 300, user["username"] 110 | else: 111 | return stu_status, user["username"] 112 | 113 | # NoneType: 用户无任何待处理表单,意味着该用户不适用当前签到方案 114 | except TypeError as e: 115 | logger.warning(f'{user["username"]} -- {OSH_STATUS_CODE[304]}') 116 | return 304, e, user["username"] 117 | # 未知异常 118 | except WebDriverException as e: 119 | logger.exception(f'{user["username"]} -- {e}') 120 | return 301, e, user["username"] 121 | # 垃圾回收 122 | finally: 123 | self._kill(api_) 124 | 125 | # ---------------------------------- 126 | # Private API 127 | # ---------------------------------- 128 | 129 | def _check_model(self, user=None, check_way='init') -> bool or int: 130 | if check_way == 'init': 131 | if not exists(self.osh_model_today): 132 | os.mkdir(self.osh_model_today) 133 | logger.info(f'今日首次打卡进程启动,已初始化文档树') 134 | return True 135 | 136 | elif check_way == 'stale': 137 | if "{}.png".format(user['username']) in os.listdir(self.osh_model_today): 138 | return 900 139 | 140 | elif check_way == 'oss': 141 | if AliyunOSS().snp_exist(user['username']): 142 | return 900 143 | else: 144 | return False 145 | 146 | def _set_startup_option(self) -> Chrome: 147 | """ 148 | ChromeDriver settings 149 | @return: 150 | """ 151 | 152 | self.print_debug_msg("初始化驱动") 153 | 154 | options = ChromeOptions() 155 | 156 | # 最高权限运行 157 | options.add_argument('--no-sandbox') 158 | 159 | # 隐身模式 160 | options.add_argument('-incognito') 161 | 162 | # 无缓存加载 163 | # options.add_argument('--disk-cache-') 164 | 165 | # 设置中文 166 | options.add_argument('lang=zh_CN.UTF-8') 167 | 168 | options.add_experimental_option('excludeSwitches', ['enable-automation']) 169 | 170 | # 更换头部 171 | options.add_argument(f'user-agent={get_useragent()}') 172 | 173 | # 静默启动 174 | if self.silence is True: 175 | options.add_argument('--headless') 176 | 177 | if not self.anti: 178 | chrome_pref = {"profile.default_content_settings": {"Images": 2, 'javascript': 2}, 179 | "profile.managed_default_content_settings": {"Images": 2}, 180 | } 181 | options.experimental_options['prefs'] = chrome_pref 182 | 183 | return Chrome(options=options, executable_path=CHROMEDRIVER_PATH) 184 | 185 | def _login(self, api: Chrome, user, retry_num=0, max_retry=3) -> bool or int: 186 | 187 | # FIXME: 加入用户密码错误鉴别 188 | # 重试次数达到阈值 189 | if retry_num >= max_retry: 190 | return 302 191 | 192 | api.get(self.url) 193 | try: 194 | if not self.anti: 195 | time.sleep(1) 196 | WebDriverWait(api, 10).until(EC.presence_of_element_located((By.ID, "username"))).send_keys( 197 | user['username']) 198 | api.find_element_by_id('password').send_keys(user['password']) 199 | api.find_element_by_tag_name('button').click() 200 | 201 | self.print_debug_msg("模拟登录成功") 202 | return True 203 | except TimeoutException or NoSuchElementException: 204 | retry_num += 1 205 | api.refresh() 206 | self._login(api, user, retry_num) 207 | except StaleElementReferenceException: 208 | time.sleep(1.2) 209 | self._login(api, user, retry_num) 210 | 211 | @staticmethod 212 | def _check_status(element) -> int: 213 | """ 214 | 215 | :param element: API定位的最新任务元素 216 | :return: 217 | """ 218 | 219 | # 解压数据 220 | stu_missions = {} 221 | i = 1 222 | stu_info: list = element.text.split('\n') 223 | while i < element.text.split('\n').__len__() - 1: 224 | stu_missions.update({stu_info[i]: stu_info[i + 1]}) 225 | i += 2 226 | 227 | # 今日签到任务tag已成功定位,但未抵达任务开放时间,此时默认上报状态为false 228 | if datetime.fromisoformat(stu_missions.get('上报开始时间')) > datetime.now(TIME_ZONE_CN).now(): 229 | return 901 230 | else: 231 | # 今日/昨日 任务已签到 232 | if stu_missions.get('是否上报') == '是': 233 | return 902 234 | # 今日/昨日 任务未签到 235 | elif stu_missions.get('是否上报') == '否': 236 | return 903 237 | 238 | # if stu_missions.get('是否上报') == '是': 239 | # return 902 240 | # elif stu_missions.get('是否上报') == '否': 241 | # if datetime.fromisoformat(stu_missions.get('上报开始时间')) > datetime.now(TIME_ZONE_CN).now(): 242 | # return 901 243 | # else: 244 | # return 903 245 | 246 | def print_debug_msg(self, msg: str): 247 | if self.debug: 248 | print(msg) 249 | 250 | def _goto_sign_up(self, api: Chrome, user=None, retry_num=0, max_retry=3) -> int: 251 | """ 252 | 253 | :param api: 254 | :param user: 255 | :param retry_num: 256 | :param max_retry: 257 | :return: 258 | """ 259 | # 超时中断 260 | if retry_num >= max_retry: 261 | logger.debug(f'FAILED -- {user["username"]}-- 该测试用例异常,可能原因为:账号密码错误或网络异常') 262 | return False 263 | 264 | try: 265 | # 获取最新任务tag 266 | time.sleep(1) 267 | latest_mission = WebDriverWait(api, 50).until(EC.presence_of_element_located(( 268 | By.XPATH, 269 | "//div[@class='mint-layout-container cjarv696w']" 270 | ))) 271 | 272 | # 查询任务状态 273 | task_status = self._check_status(latest_mission) 274 | 275 | # 可签到 276 | if task_status == 903: 277 | if self.only_get_screenshot: 278 | logger.error(f" {user['username']} || 截图捕获失败,今日签到任务未执行。") 279 | latest_mission.click() 280 | 281 | # 已签到 282 | elif task_status == 902: 283 | # logger.info(f"{user['username']} -- {OSH_STATUS_CODE[task_status]}") 284 | self._save_log(api, user) 285 | # logger.success(f"{user['username']} -- 截图上传成功") 286 | 287 | # 未开放 288 | elif task_status == 901: 289 | logger.warning(f'{user["username"]} -- {OSH_STATUS_CODE[task_status]}') 290 | return task_status 291 | 292 | # 元素更新/任务超时/网络通信异常 293 | except NoSuchElementException or TimeoutException: 294 | retry_num += 1 295 | logger.debug(f'{user["username"]} -- 任务超时,重试次数{retry_num}') 296 | self._goto_sign_up(api, retry_num=retry_num) 297 | 298 | def _submit(self, api: Chrome, user=None): 299 | 300 | def _scroll_the_window(): 301 | actions = ActionChains(api) 302 | actions.key_down(Keys.END).perform() 303 | actions.reset_actions() 304 | 305 | def _click_temperature_selection_bar(retry=0, max_retry=10): 306 | try: 307 | time.sleep(1) 308 | _scroll_the_window() 309 | api.find_element_by_xpath("//div[@class='__em-selectlist']").click() 310 | except ElementClickInterceptedException: 311 | if retry <= max_retry: 312 | retry += 1 313 | _click_temperature_selection_bar(retry=retry) 314 | 315 | def _click_confirm_button_of_the_temperature(): 316 | time.sleep(1) 317 | api.find_element_by_xpath("//div[@visible-item-count]//div[contains(@class,'confirm')]").click() 318 | 319 | def _click_submit_button_of_the_temperature(): 320 | time.sleep(0.5) 321 | api.find_element_by_xpath( 322 | "//button[@class='mint-button flowEditButton mt-btn-primary mint-button--normal']").click() 323 | 324 | def _click_submit_button_of_the_form(): 325 | try: 326 | self.print_debug_msg("尝试捕获弹窗") 327 | time.sleep(0.5) 328 | err_ = api.find_element_by_xpath("//div[contains(@class,'mint-msgbox-message')]") 329 | # api.find_element_by_class_name("mint-msgbox-content").text 330 | if err_ is not None: 331 | logger.error(f" {user['username']} || {err_.text}") 332 | except NoSuchElementException: 333 | print("123123123") 334 | pass 335 | 336 | time.sleep(0.5) 337 | api.find_element_by_xpath("//button[@class='mint-msgbox-btn mint-msgbox-confirm mt-btn-primary']").click() 338 | 339 | def _generate_behavior_response(): 340 | response = self._save_log(api, user, status_='goto') 341 | if response: 342 | logger.success(f" {user['username']} || 签到成功") 343 | else: 344 | logger.warning(f" {user['username']} || 签到异常") 345 | 346 | # 进入[体温签到页面] 347 | api.find_element_by_id("app").click() 348 | self.print_debug_msg("进入[体温签到页面]") 349 | 350 | # 点击[体温选择栏] 351 | _click_temperature_selection_bar() 352 | self.print_debug_msg("点击[体温选择栏]") 353 | 354 | # [确认]体温 355 | _click_confirm_button_of_the_temperature() 356 | self.print_debug_msg("[确认]体温") 357 | 358 | # 点击[提交数据]按钮 359 | _click_submit_button_of_the_temperature() 360 | self.print_debug_msg("点击[提交数据]按钮") 361 | 362 | # 点击[确认提交数据]按钮 363 | _click_submit_button_of_the_form() 364 | self.print_debug_msg("点击[确认提交数据]按钮") 365 | 366 | # 判断签到状态 并上传截图 367 | _generate_behavior_response() 368 | self.print_debug_msg("判断签到状态 并上传截图") 369 | 370 | def _save_log(self, api: Chrome, user, status_='wait') -> bool: 371 | """ 372 | 373 | :param api: 374 | :param user: 375 | :param status_: wait:已签到 ,goto:刚签到,返回上一级后截图 376 | :return: 377 | """ 378 | 379 | if status_ == 'wait': 380 | api.save_screenshot(join(self.osh_model_today, f"{user['username']}.png")) 381 | AliyunOSS().upload_base64(api.get_screenshot_as_base64(), user['username']) 382 | return True 383 | elif status_ == 'goto': 384 | api.back() 385 | time.sleep(0.5) 386 | if self._check_status(api.find_element_by_xpath("//div[@class='mint-layout-container cjarv696w']")) == 902: 387 | self._save_log(api, user) 388 | return True 389 | else: 390 | return False 391 | else: 392 | return False 393 | 394 | @staticmethod 395 | def _kill(api: Chrome) -> None: 396 | api.delete_all_cookies() 397 | api.quit() 398 | 399 | 400 | osh_core = _OnlineServiceHallSubmit 401 | --------------------------------------------------------------------------------