├── 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 | 
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 | 7Rxtd5o6+NfkY3sIgZB8BMVt9+72drM92+6XHaqo7KJ4Ede6X3+TEFRI7OgUsO3O6bGQhJA8728BoN784U0aLGd/JeMwBqYxfgCoD0wTGgSzf7xlk7cQk+YN0zQay0G7hmH0IyyelK3raByuSgOzJImzaFluHCWLRTjKSm1Bmib35WGTJC6/dRlMQ6VhOApitfVTNM5mxS6cXfvbMJrOijdDLPc3D4rBcierWTBO7veakA9QL02SLL+aP/TCmAOvgEv+3OBA73ZhabjI6jww/5P+sf7w3z9oZrn9yVXoWZ+8C2jm03wP4rXcsVxttilAEI4ZRORtkmazZJosgtjftXppsl6MQ/4eg93txrxPkiVrhKzxW5hlG4neYJ0lrGmWzWPZu8rS5N8tgBlovHwV/NUHtyubVsk6HYWP7FHuKAvSaZg9Mg5tkcKoOUzmYZZu2HNpGAdZ9L28jkCS1XQ7bgd5diGB/wRE6PCA44zDZhksSgjB/605zXj3sygLL1g3373LhiySdB7EuwHsair++xbw+oAi4GPgEeBS4DvA7QPPB74NCAGewbvIAFBTDPaAh4FvAmIAau1dUEB7/JrPAwExxQWbAfJ5KHvKh8WyGRTylcs1VKmKscOSXzKMBnEcxsk0DeZs4DJMIwbSMK32Xe868q0P853375nkqUdO38M0Cx8eJyiVAIoHLMnIUpJBR97f7+SCWYyZ7ckEbDREM9B++cyLajIvtLrkXqQigvOTD1ybXzDW4fyEOc+5SDAfBK4vxjB+ciTPuZaCvh1yYEdkbxplsjc1ZA/NNsneOigq17FWUMbRIrwolsYFJeRrszWCUs4TR9p5GBCziyCOpot8mm/rVRZNNuo8X3wX+APg9QB1Bf57wHOFiKTAJQLbLv/biUr+xtOuYXhz+3V4db23jppKgHW5W3HOx7ALV4zhj9uNL9rt3ewt2uHMw2EmVuYxVmG/rAWLRQ9EC+bLkqvvF2t1+VabXGu0+rpeMW1UQvSBdXDYEw5s1sJ+KREA7smNcalg6bbq8GmJJVqomMcWM0MNFB7bKmvkrHFAE/MdlwWJ3H5/xGSE0LdcgkTMKHZlxzwaj3PlEa6iH8GdmIqrj2USLTLB8LYH7D6fi+mLVa46GpNRFqqoZqJRzY5GRkHYlJAyyVnoZgbAdPOZP39pF7df9vv6D3Ly/G4j7xrV6U5dne4cqdPlo9ecKPeoxXAu7RK9YNsuT5IvTT5XoYTtQn6dOPC5EMfY5S4y5/Q4WK2iUd44iOLDJsUkWWSDYB7FHHY3zCJfsXVfhffs92MyZwZ/J6SCjyWVo5jdUdBZKAc0UE26WTK/W6+68mKoWSV+qDHokK0KS9qYH2NozOc6tonNFZ9n6rRqrpTPD/plRUWQBvY6H7I52J+HKGpU30CrLYVzFCqwzrE5B1S0K/JrYwt3GrCDGmxxD6AvfH7CvXrX/fCBCzImqrh9f64iqaoQkM7Db1chYAW2X/yhArjn77hsnRIJeksDep06MBuDvGrKXN2+fy/UMRVBDMz9UOaEynBCr/Ax79JduFloaoqLmLIhHudBj8+fP6vkf1yIa8KM1V4SJ6mYDU3IKByNtiP3eu6IbdnGafCGaIVliK2yTKtBMUhVxP39AjkGVaLwiKocg9rkGFON/b5QWYXLkHc0fkOrssp8LSRPyoDHGqehVZK3NOmOnyUruQ15MZFGJI+rsq5gvhQQQsjiGArj7yHHg9LDR4vf2hnPIvhdNxepoG1f5chE5fxhykscLidxcj+aBWl2OQ6y4C5YhQfUVkUzDXqDge89xZw+LiZqGpdOWUs5SKOloMawc5qiHFvj6Z8f5fAMxCumnEow3TY07oBB2qQa1ShVEHGujvEuAL8fft+Lxh8IwIMTOtQSLz8vgKF6ujgy3l41HKBhVCjlQLjdTdNgszdMqt/6L4LIqBBfPuVJg/lIpw0P+jjGCXycrTxQfJyxQ++MhnwcYmvkAEaqHICNGR5IjQv0rvvDr+71u20advjuzdXt9XEYqPqUk4mp9ynH+A7b+Lig2XFa3rAq0RuCNWiytjGefUShxvCkiSKIyAEGHhUZb8gjBHnGmw6KgIEuxt9stGBsh2Rs6TBLzDuE8YmQBO0Kkqgu56KLF9hNoYhABbRtpqPh07ShuNsrAJSN2/Sl/5Fx1AbsZy/1Wex2Q9mI1NS81rGh7F/Sl7ha0WjIBQ9qPuA4behXTYaKBx8HPNa+y/ypJTKqLFEjlUJpECLn2aunuVwupkU3fTFSqOIQMktsWwrRXTmfzid8Jtb9KWVF3VKHnB+6SnuRGtbu79KVnTdVA6HE7BKhSI2iiurCgSj4F6KT+r8mXt/e3FwP2dS3H9+/EAlaLRyD0NCF1LDO2m7OlDuP8MgzYEli1mVJ3KmMNTUsKeyS/MSNrDJ2+JkBXh1M+KGCIgN5VocHLINemhWWMXXxRKR1UBtjGXoeVU/PgWVwTZahndZOETUyVKq9YdzD9JUdZF91dfY2D0t4hfFPIT/oRgxA4F/hasUPiD4TztLkwtvmLAueBWd1zTa1Pf+jY+7HZVZ+lxw+LUPSqV1gn1/BQ0sZyUrcSZeKaLUCAp/rydt2+QbXPZ3bbV21ahs8G2T9PMbdBTpto1N8qiGobRZQW4xKB7Im22UXps4AfOQE6UHL7xSZ3BqZxROIT0ir+SdbF7Ywqc5SbCzuizWBp98+2OHDbXXOr3TKl0WZYlcnXF9FSrE2LRydUjyOudWY5NNkdP4RGk+45K6MN790GV3x5x1LVyTQrpAmqoPYeXV7Oz7H1nlQqta6qncnqtMxvO31/OGrwIflOCV8UM03HnSfeGgMHVSTdtGILRGlz7+6Qvu6439NfgirLAm7K1JzlMoD3bkdiOxWhRtVLdBXzFAM+rApjmK3uw8d5oVBu89FIv9/
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------