├── requirements.txt ├── setting.py ├── login ├── sha1.py ├── __init__.py ├── des_3.py ├── rsa_encrypt.py └── campus.py ├── CHANGELOG.md ├── utils ├── log.py ├── config.py ├── pipehub.py ├── bark.py ├── qmsg.py ├── server_chan.py ├── email_push.py └── wechat_enterprise.py ├── .github └── workflows │ ├── dev.yml │ └── main.yml ├── api ├── user_info.py ├── healthy2_check.py ├── healthy1_check.py ├── ykt_score.py ├── campus_check.py └── wanxiao_push.py ├── LICENSE ├── conf ├── push.json └── user.json ├── README.md └── index.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests~=2.25.1 2 | urllib3~=1.26.7 3 | json5~=0.9.6 4 | pycryptodome==3.12.0 -------------------------------------------------------------------------------- /setting.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from utils.log import init_log 3 | 4 | __all__ = [ 5 | 'log', 6 | 7 | ] 8 | 9 | log = init_log(logging.INFO) 10 | -------------------------------------------------------------------------------- /login/sha1.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | 4 | def sha256(string): 5 | sha1_object = hashlib.sha256() 6 | sha1_object.update(str(string)) 7 | return sha1_object.hexdigest() -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### ⚡ChangeLog 2 | 3 | * 🔥添加 ver 验证字段,当前部分学校已经开始检验此字段,没有当前字段可能会显示打卡模板不对 4 | * 🌟基于目前频繁出现交换密钥失败,添加重试装饰器,失败时会休眠 10 s,重试 5 次,大概率能解决此问题 5 | * 👏服务器或本地运行请下载没有 CF 后缀的,云函数请下载带 CF 后缀,不同的 py 后缀是不同的 py 版本 6 | ------ 7 | 蓝奏云链接:https://lingsiki.lanzouw.com/b0ekhmcxe 密码:2333 -------------------------------------------------------------------------------- /login/__init__.py: -------------------------------------------------------------------------------- 1 | # 文件名: __init__ 2 | # 创建日期:2020年09月13日09点44分 3 | # 作者:Zhongbr 4 | # 邮箱:zhongbr@icloud.com 5 | """ 6 | 修改:ReaJason 7 | 修改日期:2021年2月1日 8 | 邮箱:reajason@163.com 9 | 博客:https://reajason.top 10 | ChangeLog: 11 | 1、优化原作者写的账密登录方式和接口替换(server -> app) 12 | 2、增加短信登录方式 13 | """ 14 | from login.campus import CampusLogin 15 | -------------------------------------------------------------------------------- /utils/log.py: -------------------------------------------------------------------------------- 1 | """ 2 | 初始化日志模块 3 | 4 | %(asctime)s - [%(filename)s/%(funcName)s()] - [%(levelname)s] - %(message)s 5 | """ 6 | import logging 7 | 8 | 9 | def init_log(level=logging.INFO): 10 | logging.basicConfig( 11 | level=level, 12 | format='%(asctime)s - [%(levelname)s] - %(message)s', 13 | datefmt='%Y-%m-%d %H:%M:%S' 14 | ) 15 | log = logging.getLogger('script log') 16 | return log -------------------------------------------------------------------------------- /utils/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json5 3 | 4 | 5 | def load_config(path): 6 | if path: 7 | if os.path.exists(path): 8 | with open(path, 'r', encoding='utf-8') as f: 9 | try: 10 | return json5.load(f) 11 | except Exception as e: 12 | raise e.__class__(f'配置文件格式错误,请仔细确认有没有少填或多填了引号和冒号') 13 | raise RuntimeError("配置文件未找到!") 14 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: auto-build 2 | 3 | on: 4 | push: 5 | branches: master 6 | paths: 7 | - '**.py' 8 | - '**/dev.yml' 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | python-version: ["3.9", "3.10"] 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | 23 | - name: Pip Install 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install -r requirements.txt -t . 27 | 28 | - name: Upload a Build Artifact 29 | uses: actions/upload-artifact@v3.1.1 30 | with: 31 | name: 17wanxiaoCheckin-CF.py${{ matrix.python-version }} 32 | path: ./ 33 | -------------------------------------------------------------------------------- /utils/pipehub.py: -------------------------------------------------------------------------------- 1 | """ 2 | PipeHub 3 | https://www.pipehub.net/ 4 | """ 5 | import requests 6 | 7 | 8 | def pipe_push(callbackCode, content): 9 | """ 10 | :param callbackCode: 官网获取 Callback URL,最后一个/后的代码 11 | :param content: 发送文本 12 | :return: 13 | """ 14 | send_url = f"https://www.pipehub.net/send/{callbackCode}" 15 | try: 16 | res = requests.post(send_url, data=content).json() 17 | """ 18 | {'request_id': 'b5436e9d-af52-4044-aae8-90b49222efe0', 'success': True, 'error_message': '', 'hint': 'Retried 0 times.'} 19 | """ 20 | if res["success"]: 21 | return {"status": 1, "msg": "PipeHub推送成功"} 22 | else: 23 | return {"status": 0, "errmsg": f"PipeHub推送失败,{res['error_message']}"} 24 | except Exception as e: 25 | return {"status": 0, "errmsg": f"PipeHub推送失败,{e}"} 26 | -------------------------------------------------------------------------------- /utils/bark.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | 4 | 5 | def bark_push(device_key, msg, title, group): 6 | try: 7 | response = requests.post( 8 | url="https://api.day.app/push", 9 | headers={ 10 | "Content-Type": "application/json; charset=utf-8", 11 | }, 12 | data=json.dumps({ 13 | "body": msg, 14 | "device_key": device_key, 15 | "title": title, 16 | "icon": "https://www.17wanxiao.com/new/images/logo.png", 17 | "group": group 18 | }) 19 | ).json() 20 | if response['code']: 21 | return {"status": 1, "msg": "Bark推送成功"} 22 | return {"status": 0, "errmsg": f"Bark推送失败,{response['message']}"} 23 | except Exception as e: 24 | return {"status": 0, "errmsg": f"Bark推送失败,{e}"} 25 | -------------------------------------------------------------------------------- /login/des_3.py: -------------------------------------------------------------------------------- 1 | from Crypto.Cipher import DES3 2 | from Crypto.Util.Padding import pad, unpad 3 | import base64, json 4 | 5 | 6 | def des_3_encrypt(string, key, iv): 7 | cipher = DES3.new(key, DES3.MODE_CBC, iv.encode("utf-8")) 8 | ct_bytes = cipher.encrypt(pad(string.encode('utf8'), DES3.block_size)) 9 | ct = base64.b64encode(ct_bytes).decode('utf8') 10 | return ct 11 | 12 | 13 | def des_3_decode(string, key, iv): 14 | ct = base64.b64decode(string) 15 | cipher = DES3.new(key.encode('utf-8'), DES3.MODE_CBC, iv.encode('utf-8')) 16 | pt = unpad(cipher.decrypt(ct), DES3.block_size) 17 | return pt 18 | 19 | 20 | def object_encrypt(object_to_encrypt, key, iv="66666666"): 21 | return des_3_encrypt(json.dumps(object_to_encrypt), key, iv) 22 | 23 | 24 | def object_decrypt(string, key, iv="66666666"): 25 | string = string.replace('\n', '') 26 | return json.loads(des_3_decode(string, key, iv)) 27 | -------------------------------------------------------------------------------- /utils/qmsg.py: -------------------------------------------------------------------------------- 1 | """ 2 | Qmsg酱 3 | https://qmsg.zendee.cn/index.html 4 | """ 5 | import requests 6 | 7 | 8 | def qmsg_push(key, qq_num, msg, send_type="send"): 9 | """ 10 | :param key: qmsg酱的key,官网获取 11 | :param qq_num: qq号或qq群组号,要与 send_type 对应 12 | :param msg: 发送消息 13 | :param send_type: 发送模式,"send"为发送给个人,"group"为发送给群组 14 | :return: 15 | """ 16 | post_data = { 17 | "msg": msg, 18 | "qq": qq_num 19 | } 20 | try: 21 | res = requests.post(f"https://qmsg.zendee.cn/{send_type}/{key}", data=post_data).json() 22 | """ 23 | {"success":true,"reason":"操作成功","code":0,"info":{}} 24 | {"success":false,"reason":"消息内容不能为空","code":500,"info":{}} 25 | """ 26 | if res['success']: 27 | return {"status": 1, "msg": "Qmsg酱推送服务成功"} 28 | return {"status": 0, "errmsg": f"Qmsg酱推送服务失败,{res['reason']}"} 29 | except Exception as e: 30 | return {"status": 0, "errmsg": f"Qmsg酱推送服务失败,{e}"} 31 | 32 | -------------------------------------------------------------------------------- /utils/server_chan.py: -------------------------------------------------------------------------------- 1 | """ 2 | Server酱推送服务 3 | https://sct.ftqq.com/ 4 | """ 5 | import requests 6 | 7 | 8 | def server_push(send_key, title, desp): 9 | """ 10 | :param send_key: 官网获取 send_key,用来发送消息 11 | :param title: 发送消息的标题 12 | :param desp: 发送文本 13 | :return: 14 | """ 15 | send_url = f"https://sctapi.ftqq.com/{send_key}.send" 16 | params = {"text": title, "desp": desp} 17 | try: 18 | res = requests.post(send_url, data=params).json() 19 | """ 20 | {'message': '[AUTH]用户不存在或者权限不足', 'code': 40001, 'info': '用户不存在或者权限不足', 'args': [None]} 21 | {'code': 0, 'message': '', 'data': {'pushid': '851777', 'readkey': 'SCTHPzE9Yvar1eA', 'error': 'SUCCESS', 'errno': 0}} 22 | """ 23 | if not res["code"]: 24 | return {"status": 1, "msg": "Server酱推送服务成功"} 25 | else: 26 | return {"status": 0, "errmsg": f"Server酱推送服务失败,{res['message']}"} 27 | except Exception as e: 28 | return {"status": 0, "errmsg": f"Server酱推送服务失败,{e}"} 29 | -------------------------------------------------------------------------------- /api/user_info.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | def get_school_name(token): 5 | post_data = {"token": token, "method": "WX_BASE_INFO", "param": "%7B%7D"} 6 | headers = {"Content-Type": "application/x-www-form-urlencoded"} 7 | try: 8 | res = requests.post( 9 | "https://server.59wanmei.com/YKT_Interface/xyk", 10 | data=post_data, 11 | headers=headers, 12 | timeout=10 13 | ) 14 | return res.json()["data"]["customerName"] 15 | except: 16 | return "Bad Req" 17 | 18 | 19 | def get_user_info(token): 20 | data = {"appClassify": "DK", "token": token} 21 | for _ in range(3): 22 | try: 23 | res = requests.post( 24 | "https://reportedh5.17wanxiao.com/api/clock/school/getUserInfo", data=data, timeout=10 25 | ) 26 | user_info = res.json()["userInfo"] 27 | user_info['school'] = get_school_name(token) 28 | return user_info 29 | except TimeoutError: 30 | continue 31 | except: 32 | return None 33 | return None 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ReaJason 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. -------------------------------------------------------------------------------- /conf/push.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": { // 自定义邮箱推送 3 | "enable": false, 4 | "smtp_address": "smtp.qq.com", // stmp服务地址 5 | "smtp_port": 465, // stmp服务端口 6 | "send_email": "***@qq.com", // 发送邮箱的邮箱地址 7 | "send_pwd": "***", // 发送邮箱的邮箱授权码 8 | "receive_email": "***@qq.com" // 接收信息的邮箱地址,可自己给自己发 9 | }, 10 | "wechat": { // Server酱·Turbo版:https://sct.ftqq.com/ 11 | "enable": false, 12 | "send_key": "**" // Server 酱推送密钥 13 | }, 14 | "qmsg": { // Qmsg酱:https://qmsg.zendee.cn/index.html 15 | "enable": false, 16 | "key": "**", 17 | "type": "send", // send 私聊消息推送,group 群消息推送 18 | "qq_num": "**" // 上面设置send,则填写接受推送的qq号,group则填写群号 19 | }, 20 | "pipehub": { // PipeHub:https://www.pipehub.net/ 21 | "enable": false, 22 | "key": "" 23 | }, 24 | "wechat_enterprise": { // 企业微信:https://work.weixin.qq.com/ 25 | "enable": false, 26 | "corp_id": "", // 企业 ID 27 | "corp_secret": "", // 自建应用 Secret 28 | "agent_id": "", // 自建应用 ID 29 | "to_user": "" // 接收者用户,多用户用|分割,所有用户填写 @all 30 | }, 31 | "bark": { // Bark: https://github.com/Finb/Bark 32 | "enable": false, 33 | "device_key": "", // Bark device_key 34 | "group": "健康打卡" // 推送消息分组 35 | } 36 | } -------------------------------------------------------------------------------- /utils/email_push.py: -------------------------------------------------------------------------------- 1 | """ 2 | QQ邮箱推送 3 | 小号往大号邮箱进行发送邮箱 4 | """ 5 | 6 | import smtplib 7 | from email.mime.text import MIMEText 8 | # from email.header import Header 9 | 10 | 11 | def email_push(send_email, send_pwd, receive_email, title, text, 12 | text_type="html", smtp_address="smtp.qq.com", smtp_port=465): 13 | """ 14 | :param send_email: 发送邮箱的邮箱地址 15 | 默认为:qq 邮箱,其他邮箱请修改 stmp 地址和端口 16 | :param send_pwd: 发送邮箱的邮箱授权码 17 | :param receive_email: 接收信息的邮箱地址(随意是什么邮箱) 18 | 如果是多个请列表传入 ["第一个", "第二个"] 19 | :param title: 邮箱标题 20 | :param text: 需要发送的消息 21 | :param text_type: 纯文本:"plain",默认为发送 html:"html" 22 | :param smtp_address: stmp 服务地址 23 | :param smtp_port: stmp 服务端口 24 | :return: 25 | """ 26 | msg = MIMEText(text, text_type, "utf-8") 27 | # msg["From"] = Header("LingSiKi", "utf-8") # 设置发送方别名 28 | msg["From"] = send_email 29 | msg["Subject"] = title 30 | try: 31 | with smtplib.SMTP_SSL(smtp_address, smtp_port) as server: 32 | server.login(send_email, send_pwd) 33 | server.sendmail(send_email, receive_email, msg.as_string()) 34 | return {"status": 1, "msg": "邮箱推送成功"} 35 | except Exception as e: 36 | return {"status": 0, "errmsg": f'邮箱推送失败:{e}'} -------------------------------------------------------------------------------- /login/rsa_encrypt.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from Crypto.PublicKey import RSA 3 | from Crypto.Cipher import PKCS1_v1_5 4 | from Crypto import Random 5 | 6 | random_generator = Random.new().read 7 | 8 | 9 | def create_key_pair(size): 10 | rsa = RSA.generate(size, random_generator) 11 | private_key = str(rsa.export_key(), 'utf8') 12 | private_key = private_key.split('-\n')[1].split('\n-')[0] 13 | public_key = str(rsa.publickey().export_key(), 'utf8') 14 | public_key = public_key.split('-\n')[1].split('\n-')[0] 15 | return public_key, private_key 16 | 17 | 18 | def rsa_encrypt(input_string, public_key): 19 | rsa_key = RSA.importKey("-----BEGIN PUBLIC KEY-----\n" + public_key + "\n-----END PUBLIC KEY-----") 20 | cipher = PKCS1_v1_5.new(rsa_key) 21 | return str(base64.b64encode(cipher.encrypt(input_string.encode('utf-8'))), 'utf-8') 22 | 23 | 24 | def rsa_decrypt(input_string, private_key): 25 | input_bytes = base64.b64decode(input_string) 26 | rsa_key = RSA.importKey("-----BEGIN RSA PRIVATE KEY-----\n" + private_key + "\n-----END RSA PRIVATE KEY-----") 27 | cipher = PKCS1_v1_5.new(rsa_key) 28 | return str(cipher.decrypt(input_bytes, random_generator), 'utf-8') 29 | 30 | 31 | if __name__ == '__main__': 32 | pub, pri = create_key_pair(1024) 33 | i = rsa_encrypt("123456", pub) 34 | print(i) 35 | print(rsa_decrypt(i, pri)) 36 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: auto-release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | path: '36' 15 | - uses: actions/setup-python@v4 16 | with: 17 | python-version: '3.6' 18 | - run: cd 36 && zip -r ../17wanxiaoCheckin.${{ github.ref_name }}.zip ./ 19 | - run: cd 36 && pip install -r requirements.txt -t . && zip -r ../17wanxiaoCheckin-CF.py36.${{ github.ref_name }}.zip ./ 20 | 21 | - uses: actions/checkout@v3 22 | with: 23 | path: '37' 24 | - uses: actions/setup-python@v4 25 | with: 26 | python-version: '3.7' 27 | - run: cd 37 && pip install -r requirements.txt -t . && zip -r ../17wanxiaoCheckin-CF.py37.${{ github.ref_name }}.zip ./ 28 | 29 | - uses: actions/checkout@v3 30 | with: 31 | path: '39' 32 | - uses: actions/setup-python@v4 33 | with: 34 | python-version: '3.9' 35 | - run: cd 39 && pip install -r requirements.txt -t . && zip -r ../17wanxiaoCheckin-CF.py39.${{ github.ref_name }}.zip ./ 36 | 37 | - run: ls 38 | - name: GH Release 39 | uses: softprops/action-gh-release@v0.1.14 40 | # if: startsWith(github.ref, 'refs/tags/') 41 | with: 42 | tag_name: ${{ github.ref_name }} 43 | name: 17wanxiaoCheckin ${{ github.ref_name }} 44 | body_path: 36/CHANGELOG.md 45 | draft: false 46 | prerelease: false 47 | files: | 48 | 17wanxiaoCheckin.${{ github.ref_name }}.zip 49 | 17wanxiaoCheckin-CF.py36.${{ github.ref_name }}.zip 50 | 17wanxiaoCheckin-CF.py37.${{ github.ref_name }}.zip 51 | 17wanxiaoCheckin-CF.py39.${{ github.ref_name }}.zip -------------------------------------------------------------------------------- /utils/wechat_enterprise.py: -------------------------------------------------------------------------------- 1 | """ 2 | 企业微信推送 3 | https://work.weixin.qq.com/ 4 | """ 5 | import json 6 | 7 | import requests 8 | 9 | 10 | def wechat_enterprise_push(corp_id, corp_secret, agent_id, to_user, msg): 11 | """ 12 | 13 | :param corp_id: 企业 ID 14 | :param corp_secret: 自建应用 Secret 15 | :param agent_id: 应用 ID 16 | :param to_user: 接收者用户,多用户用|分割,所有用户填写 @all 17 | :param msg: 推送消息 18 | :return: 19 | """ 20 | # 获取 access_token 21 | get_access_token_url = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken' 22 | values = { 23 | 'corpid': corp_id, 24 | 'corpsecret': corp_secret, 25 | } 26 | try: 27 | res = requests.post(get_access_token_url, params=values).json() 28 | if not res['errcode']: 29 | access_token = res["access_token"] 30 | elif res['errcode'] == 40001: 31 | return {"status": 0, "errmsg": "不合法的 secret 参数,https://open.work.weixin.qq.com/devtool/query?e=40001"} 32 | elif res['errcode'] == 40013: 33 | return {"status": 0, "errmsg": "不合法的 CorpID,https://open.work.weixin.qq.com/devtool/query?e=40013"} 34 | else: 35 | return {"status": 0, "errmsg": res['errmsg']} 36 | except Exception as e: 37 | return {"status": 0, "errmsg": f"获取企业微信 access_token 失败,{e}"} 38 | 39 | # 推送消息 40 | send_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' 41 | post_data = { 42 | "touser": to_user, 43 | "msgtype": "text", 44 | "agentid": agent_id, 45 | "text": { 46 | "content": msg 47 | }, 48 | "safe": "0" 49 | } 50 | try: 51 | res = requests.post(send_url, data=json.dumps(post_data)).json() 52 | if not res['errcode']: 53 | return {"status": 1, "msg": "企业微信推送服务成功"} 54 | elif res['errcode'] == 40056: 55 | return {"status": 0, "errmsg": "不合法的 agentid,https://open.work.weixin.qq.com/devtool/query?e=40056"} 56 | elif res['reecode'] == 81013: 57 | return {"status": 0, 58 | "errmsg": "UserID、部门 ID、标签 ID 全部非法或无权限,https://open.work.weixin.qq.com/devtool/query?e=81013"} 59 | except Exception as e: 60 | return {"status": 0, "errmsg": f"企业微信推送服务失败,{e}"} 61 | -------------------------------------------------------------------------------- /api/healthy2_check.py: -------------------------------------------------------------------------------- 1 | """ 2 | 第二类健康打卡相关函数 3 | @create:2021/03/10 4 | @filename:healthy2_check.py 5 | @author:ReaJason 6 | @email_addr:reajason@163.com 7 | @blog_website:https://reajason.top 8 | @last_modify:2021/04/24 9 | """ 10 | import time 11 | import requests 12 | 13 | from setting import log 14 | 15 | 16 | def get_healthy2_check_posh_json(token): 17 | """ 18 | 获取第二类健康打卡的打卡数据 19 | :param token: 用户令牌 20 | :return: 返回dict数据 21 | """ 22 | for _ in range(3): 23 | try: 24 | res = requests.post( 25 | url="https://reportedh5.17wanxiao.com/api/reported/recall", 26 | data={"token": token}, 27 | timeout=10, 28 | ).json() 29 | except: 30 | log.warning("完美校园第二类健康打卡post参数获取失败,正在重试...") 31 | time.sleep(1) 32 | continue 33 | if res["code"] == 0: 34 | log.info("完美校园第二类健康打卡post参数获取成功") 35 | return res["data"] 36 | else: 37 | log.warning(f"完美校园第二类健康打卡post参数获取失败,{res}") 38 | return None 39 | 40 | 41 | def healthy2_check_in(token, custom_id, post_dict): 42 | """ 43 | 第二类健康打卡 44 | :param token: 用户令牌 45 | :param custom_id: 健康打卡id 46 | :param post_dict: 健康打卡数据 47 | :return: 48 | """ 49 | if not post_dict.get("whereabouts"): 50 | errmsg = f"完美校园第二类健康打卡方式错误,请选第一类健康打卡" 51 | log.warning(errmsg) 52 | return {'status': 0, 'errmsg': errmsg} 53 | check_json = { 54 | "userId": post_dict["userId"], 55 | "name": post_dict["name"], 56 | "stuNo": post_dict["stuNo"], 57 | "whereabouts": post_dict["whereabouts"], 58 | "familyWhereabouts": "", 59 | "beenToWuhan": post_dict["beenToWuhan"], 60 | "contactWithPatients": post_dict["contactWithPatients"], 61 | "symptom": post_dict["symptom"], 62 | "fever": post_dict["fever"], 63 | "cough": post_dict["cough"], 64 | "soreThroat": post_dict["soreThroat"], 65 | "debilitation": post_dict["debilitation"], 66 | "diarrhea": post_dict["diarrhea"], 67 | "cold": post_dict["cold"], 68 | "staySchool": post_dict["staySchool"], 69 | "contacts": post_dict["contacts"], 70 | "emergencyPhone": post_dict["emergencyPhone"], 71 | "address": post_dict["address"], 72 | "familyForAddress": "", 73 | "collegeId": post_dict["collegeId"], 74 | "majorId": post_dict["majorId"], 75 | "classId": post_dict["classId"], 76 | "classDescribe": post_dict["classDescribeAll"], 77 | "temperature": post_dict["temperature"], 78 | "confirmed": post_dict["confirmed"], 79 | "isolated": post_dict["isolated"], 80 | "passingWuhan": post_dict["passingWuhan"], 81 | "passingHubei": post_dict["passingHubei"], 82 | "patientSide": post_dict["patientSide"], 83 | "patientContact": post_dict["patientContact"], 84 | "mentalHealth": post_dict["mentalHealth"], 85 | "wayToSchool": post_dict["wayToSchool"], 86 | "backToSchool": post_dict["backToSchool"], 87 | "haveBroadband": post_dict["haveBroadband"], 88 | "emergencyContactName": post_dict["emergencyContactName"], 89 | "helpInfo": "", 90 | "passingCity": "", 91 | "longitude": post_dict["longitude"], 92 | "latitude": post_dict["latitude"], 93 | "token": token, 94 | } 95 | headers = { 96 | "referer": f"https://reportedh5.17wanxiao.com/nCovReport/index.html?token={token}&customerId={custom_id}", 97 | "content-type": "application/x-www-form-urlencoded;charset=UTF-8", 98 | } 99 | try: 100 | res = requests.post( 101 | "https://reportedh5.17wanxiao.com/api/reported/receive", 102 | headers=headers, 103 | data=check_json, 104 | ).json() 105 | log.info(res) 106 | return { 107 | 'status': 1, 108 | 'res': res, 109 | 'post_dict': { 110 | 'name': post_dict["name"], 111 | "updatainfo_detail": post_dict, 112 | 'checkbox': [{'description': key, 'value': value} for key, value in check_json.items()] 113 | }, 114 | 'check_json': check_json, 115 | 'type': "healthy2", 116 | } 117 | except: 118 | errmsg = f"完美校园第二类健康打卡打卡请求出错" 119 | log.warning(errmsg) 120 | return {'status': 0, 'errmsg': errmsg} 121 | -------------------------------------------------------------------------------- /api/healthy1_check.py: -------------------------------------------------------------------------------- 1 | """ 2 | 第一类健康打卡相关函数 3 | @create:2021/03/10 4 | @filename:healthy1_check.py 5 | @author:ReaJason 6 | @email_addr:reajason@163.com 7 | @blog_website:https://reajason.top 8 | @last_modify:2021/03/15 9 | """ 10 | import time 11 | import json 12 | import requests 13 | from setting import log 14 | 15 | 16 | def get_healthy1_check_post_json(token, templateid): 17 | """ 18 | 获取打卡数据 19 | :param token: 20 | :return: 21 | """ 22 | healthy1_check_post_json = { 23 | "businessType": "epmpics", 24 | "jsonData": {"templateid": templateid, "token": token}, 25 | "method": "userComeApp", 26 | } 27 | for _ in range(3): 28 | try: 29 | res = requests.post( 30 | url="https://reportedh5.17wanxiao.com/sass/api/epmpics", 31 | json=healthy1_check_post_json, 32 | timeout=10, 33 | ).json() 34 | except: 35 | log.warning("完美校园第一类健康打卡post参数获取失败,正在重试...") 36 | time.sleep(1) 37 | continue 38 | if res["code"] != "10000": 39 | """ 40 | {'msg': '业务异常', 'code': '10007', 'data': '无法找到该机构的投票模板数据!'} 41 | """ 42 | log.warning(f'完美校园第一类健康打卡post参数获取失败{res}') 43 | return None 44 | data = json.loads(res["data"]) 45 | post_dict = { 46 | "areaStr": data['areaStr'], 47 | "ver": data["ver"], 48 | "deptStr": data['deptStr'], 49 | "deptid": data['deptStr']['deptid'] if data['deptStr'] else None, 50 | "customerid": data['customerid'], 51 | "userid": data['userid'], 52 | "username": data['username'], 53 | "stuNo": data['stuNo'], 54 | "phonenum": data["phonenum"], 55 | "templateid": data["templateid"], 56 | "updatainfo": [ 57 | {"propertyname": i["propertyname"], "value": i["value"]} 58 | for i in data["cusTemplateRelations"] 59 | ], 60 | "updatainfo_detail": [ 61 | { 62 | "propertyname": i["propertyname"], 63 | "checkValues": i["checkValues"], 64 | "description": i["decription"], 65 | "value": i["value"], 66 | } 67 | for i in data["cusTemplateRelations"] 68 | ], 69 | "checkbox": [ 70 | {"description": i["decription"], "value": i["value"], "propertyname": i["propertyname"]} 71 | for i in data["cusTemplateRelations"] 72 | ], 73 | } 74 | log.info("完美校园第一类健康打卡post参数获取成功") 75 | return post_dict 76 | return None 77 | 78 | 79 | def healthy1_check_in(token, phone, post_dict): 80 | """ 81 | 第一类健康打卡 82 | :param phone: 手机号 83 | :param token: 用户令牌 84 | :param post_dict: 打卡数据 85 | :return: 86 | """ 87 | check_json = { 88 | "businessType": "epmpics", 89 | "method": "submitUpInfo", 90 | "jsonData": { 91 | "deptStr": post_dict["deptStr"], 92 | "areaStr": post_dict["areaStr"], 93 | "reportdate": round(time.time() * 1000), 94 | "customerid": post_dict["customerid"], 95 | "deptid": post_dict['deptStr']['deptid'] if post_dict['deptStr'] else None, 96 | "source": "app", 97 | "templateid": post_dict["templateid"], 98 | "stuNo": post_dict["stuNo"], 99 | "username": post_dict["username"], 100 | "phonenum": phone, 101 | "userid": post_dict["userid"], 102 | "updatainfo": post_dict["updatainfo"], 103 | "gpsType": 1, 104 | "ver": post_dict["ver"], 105 | "token": token, 106 | }, 107 | } 108 | for _ in range(3): 109 | try: 110 | res = requests.post( 111 | "https://reportedh5.17wanxiao.com/sass/api/epmpics", json=check_json 112 | ).json() 113 | if res['code'] == '10000': 114 | log.info(res) 115 | return { 116 | "status": 1, 117 | "res": res, 118 | "post_dict": post_dict, 119 | "check_json": check_json, 120 | "type": "healthy1", 121 | } 122 | elif res['data'] == "areaStr can not be null": 123 | log.warning('当前用户无法获取第一类健康打卡地址信息,请前往配置 user.json 文件,one_check 下的 areaStr 设置地址信息') 124 | elif "频繁" in res['data']: 125 | log.info(res) 126 | return { 127 | "status": 1, 128 | "res": res, 129 | "post_dict": post_dict, 130 | "check_json": check_json, 131 | "type": "healthy1", 132 | } 133 | else: 134 | log.warning(res) 135 | return {"status": 0, "errmsg": f"{post_dict['username']}: {res}"} 136 | except: 137 | errmsg = f"```打卡请求出错```" 138 | log.warning("健康打卡请求出错") 139 | return {"status": 0, "errmsg": errmsg} 140 | return {"status": 0, "errmsg": "健康打卡请求出错"} 141 | -------------------------------------------------------------------------------- /api/ykt_score.py: -------------------------------------------------------------------------------- 1 | """ 2 | 粮票获取 3 | @create:2021/04/24 4 | @filename:ykt_score.py 5 | @author:ReaJason 6 | @email_addr:reajason@163.com 7 | @blog_website:https://reajason.top 8 | @last_modify:2021/04/26 9 | """ 10 | import requests 11 | 12 | from setting import log 13 | 14 | 15 | def get_task_list(token): 16 | data = f"token={token}" \ 17 | "&method=makeScoreTask" \ 18 | f"¶m=%7B%22token%22%3A%22{token}%22%2C%22qudao%22%3A%220%22%2C%22device%22%3A%221%22%7D" 19 | headers = { 20 | 'content-type': 'application/x-www-form-urlencoded' 21 | } 22 | try: 23 | res = requests.post("https://server.17wanxiao.com/YKT_Interface/score", data=data, headers=headers).json() 24 | if res['result_']: 25 | return [{"name": task["name"], "finished": task["finished"]} for task in res['data']['taskList']] 26 | except Exception as e: 27 | log.warning(f'{e.__class__}:{e} 获取任务列表失败') 28 | return None 29 | 30 | 31 | def ykt_check_in(token): 32 | """ 33 | 获取签到粮票 34 | :param token: 35 | """ 36 | data = f"token={token}" \ 37 | "&method=WX_h5signIn" \ 38 | f"¶m=%7B%22token%22%3A%22{token}%22%7D" 39 | headers = { 40 | 'content-type': 'application/x-www-form-urlencoded' 41 | } 42 | try: 43 | res = requests.post("https://server.17wanxiao.com/YKT_Interface/xyk", data=data, headers=headers).json() 44 | log.info(res['data']['alertMessage']) 45 | except: 46 | log.warning("签到失败") 47 | 48 | 49 | def get_article_id(token): 50 | """ 51 | 获取文章 id 52 | :return: 53 | """ 54 | post_json = { 55 | "typeCode": "campusNews", 56 | "pageSize": 10, 57 | "pageNo": 1, 58 | "token": token 59 | } 60 | try: 61 | res = requests.post("https://information.17wanxiao.com/cms/api/info/list", json=post_json).json() 62 | return res['data']['rows'][0]['id'] 63 | except: 64 | return None 65 | 66 | 67 | def get_article_score(token, article_id): 68 | """ 69 | 查看文章 70 | :param article_id: 71 | :param token: 72 | :return: 73 | """ 74 | data = { 75 | "id": article_id, 76 | "token": token 77 | } 78 | try: 79 | res = requests.post("https://information.17wanxiao.com/cms/api/info/detail", data=data).json() 80 | if res['result_']: 81 | # log.info('查看文章成功') 82 | pass 83 | else: 84 | log.warning(f'查看文章失败,{res}') 85 | except Exception as e: 86 | log.warning(f'查看文章失败,{e}') 87 | 88 | 89 | def get_talents_token(token): 90 | try: 91 | res = requests.get(f"https://api.xiaozhao365.com/operation/pub/iface/userInfo?token={token}").json() 92 | return res['userInfo']['talents_token'] 93 | except: 94 | return None 95 | 96 | 97 | def get_class_score(token): 98 | post_json = {"token": token, "command": "CURRI_SERVER.WEEK_CURRI", "week": ""} 99 | try: 100 | res = requests.post("https://course.59wanmei.com/campus-score/curriculum/_iface/server/invokInfo.action", 101 | json=post_json) 102 | if res.status_code == 200: 103 | log.info("查看课表成功") 104 | else: 105 | log.warning("查看课表失败") 106 | except Exception as e: 107 | log.warning(f"{e} 查看课表失败") 108 | 109 | 110 | def get_score_list(token): 111 | """ 112 | 获取所有的奖励数据 113 | :param token 114 | :return: dict 115 | """ 116 | data = f"token={token}" \ 117 | "&method=gainScoreCircleList" \ 118 | f"¶m=%7B%22token%22%3A%22{token}%22%7D" 119 | headers = { 120 | 'content-type': 'application/x-www-form-urlencoded' 121 | } 122 | try: 123 | res = requests.post("https://server.17wanxiao.com/YKT_Interface/score", data=data, headers=headers).json() 124 | return { 125 | 'sign': res['signCircleStatus'], 126 | 'active': res['activeCircleList'], 127 | 'circle': res['circleList'] 128 | } 129 | except: 130 | return None 131 | 132 | 133 | def get_active_score(token, active_dict): 134 | """ 135 | 获取活动奖励 136 | """ 137 | data = f"token={token}" \ 138 | "&method=gainActiveScoreCircle" \ 139 | f"¶m=%7B%22token%22%3A%22{token}%22%2C%22scoreCircleId%22%3A{active_dict['id']}%7D" 140 | headers = { 141 | 'content-type': 'application/x-www-form-urlencoded' 142 | } 143 | try: 144 | res = requests.post("https://server.17wanxiao.com/YKT_Interface/score", data=data, headers=headers).json() 145 | msg = f'{active_dict["title"]}({active_dict["id"]}):{active_dict["foodCoupon"]}个粮票,' 146 | if res['result_']: 147 | log.info(msg + res['message_']) 148 | else: 149 | log.warning(msg + res['message_']) 150 | except Exception as e: 151 | log.warning(f'{e.__class__}:{e}操作失败') 152 | 153 | 154 | def get_circle_score(token, circle_dict): 155 | """ 156 | 获取其他奖励 157 | """ 158 | data = f"token={token}" \ 159 | "&method=gainScoreCircle" \ 160 | f"¶m=%7B%22token%22%3A%22{token}%22%2C%22scoreCircleId%22%3A{circle_dict['id']}%7D" 161 | headers = { 162 | 'content-type': 'application/x-www-form-urlencoded' 163 | } 164 | try: 165 | res = requests.post("https://server.17wanxiao.com/YKT_Interface/score", data=data, headers=headers).json() 166 | msg = f'{circle_dict["title"]}({circle_dict["id"]}):{circle_dict["foodCoupon"]}个粮票,' 167 | if res['result_']: 168 | log.info(msg + res['message_']) 169 | else: 170 | log.warning(msg + res['message_']) 171 | except Exception as e: 172 | log.warning(f'{e.__class__}:{e}操作失败') 173 | 174 | 175 | def get_all_score(token): 176 | for _ in range(2): 177 | circle_dict_list = get_score_list(token)['circle'] 178 | if circle_dict_list: 179 | for circle_dict in circle_dict_list: 180 | get_circle_score(token, circle_dict) 181 | else: 182 | break 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 🌈17wanxiaoCheckin 4 |

5 | 6 | ![](https://img.shields.io/github/stars/ReaJason/17wanxiaoCheckin-Actions?style=social "Star数量") 7 | ![](https://img.shields.io/github/forks/ReaJason/17wanxiaoCheckin-Actions?style=social "Fork数量") 8 |
9 | [![](https://img.shields.io/github/license/ReaJason/17wanxiaoCheckin-Actions "协议")](https://github.com/ReaJason/17wanxiaoCheckin/blob/master/LICENSE) 10 | [![](https://img.shields.io/github/v/release/ReaJason/17wanxiaoCheckin-Actions "release版本")](https://github.com/ReaJason/17wanxiaoCheckin/releases) 11 | [![](https://github.com/ReaJason/17wanxiaoCheckin/actions/workflows/main.yml/badge.svg)](https://github.com/ReaJason/17wanxiaoCheckin/actions/workflows/main.yml) 12 | [![](https://github.com/ReaJason/17wanxiaoCheckin/actions/workflows/dev.yml/badge.svg)](https://github.com/ReaJason/17wanxiaoCheckin/actions/workflows/dev.yml) 13 |
14 | 15 | ## ✨项目介绍 16 | 17 |   伴随着疫情的到来,学校为了解在校师生的健康状况,全校师生都规定在特定的时间进行健康打卡 or 校内打卡,本项目旨在帮助使用完美校园打卡的在校师生提供帮助,每天指定时间进行自动打卡,从每天指定时间打卡的压力中解放出来,全身心地投入到社会主义建设之中去。 18 | 19 |   本项目使用了 `requests`、`json5`、`pycryptodome` 第三方库,2.0 版本迎来项目重构,打卡数据错误修改方法,不再是以前的修改代码(不懂代码容易改错或无法下手),而是通过直接修改配置文件即可,**本脚本使用虚拟 id 来登录,如果使用了本脚本就不要再用手机登录 app 了,如果一定要用 app 请不要使用当前脚本**。 20 | 21 |   由于完美校园就设备做了验证,只允许一个设备登录,获取本机的 device_id 可参考 [此处](https://github.com/ReaJason/17wanxiaoCheckin/wiki/%E8%8E%B7%E5%8F%96%E6%9C%AC%E6%9C%BA-device_id) 22 | 23 | 24 | 25 | ## 🔰项目功能 26 | 27 | * [x] 完美校园模拟登录获取 token 28 | * [x] 自动获取上次提交的打卡数据,也可通过配置文件修改 29 | * [x] 支持健康打卡(学生打卡、教师打卡)和校内打卡 30 | * [x] 支持多人打卡配置,可单人自定义推送,也可统一推送 31 | * [x] 支持粮票签到收集,自动完成查看课表和校园头条任务 32 | * [x] 支持 qq 邮箱、Qmsg、Server 酱、PipeHub 推送打卡消息 33 | 34 | 35 | ## 🎨配置文件 36 | 37 | ### 💃用户配置 38 | 39 | - 打卡用户配置文件位于:`conf/user.json` 40 | - 整个 json 文件使用一个 `[]` 列表用来存储打卡用户数据,每一个用户占据了一个 `{}`键值对,初次修改务必填写的数据为:**phone**、**password**、**device_id**(获取方法:[蓝奏云](https://lingsiki.lanzoui.com/iQamDmt165i),下载解压使用)、**健康打卡的开关**(根据截图判断自己属于哪一类(不一定和我截图一模一样,好看就选 1)[【1】](https://cdn.jsdelivr.net/gh/ReaJason/17wanxiaoCheckin/Pictures/one.png)、[【2】](https://cdn.jsdelivr.net/gh/ReaJason/17wanxiaoCheckin/Pictures/two.png)),校内打卡开关(有则开),推送设置 **push**(推荐使用 qq 邮箱推送)。 41 | - 关于 `post_json`,如若打卡推送数据中无错误,则不用管,若有 null,或其他获取不到的情况,则酌情修改即可,和推送是一一对应的。 42 | - 如果多人打卡,则复制单个用户完整的 `{}`,紧接在上个用户其后即可。 43 | 44 | ```js 45 | // 萌新不建议精简配置,出现报错还需要加上必要的配置,以下为个人最简配置示例,请确定自己的打卡方式,删掉不需要的即可 46 | { 47 | "welcome": "用户一,这是一条欢迎语,每次打卡前会打印这句话,用来标记当前打卡用户,如:正在为 *** 打卡......", 48 | "phone": "123", // 完美校园登录账号,必填 49 | "password": "456", // 完美校园登录密码,必填 50 | "device_id": "789", // 已验证完美校园登录设备ID,获取方式为下载蓝奏云链接中的 RegisterDeviceID.zip,必填 51 | "healthy_checkin": { // 必选一个打卡方式 52 | "one_check": { // 第一类健康打卡 53 | "enable": true // true 为打开,false 为关闭 54 | } 55 | }, 56 | "campus_checkin": { // 校内打卡,没有就不用管 57 | "enable": true // true 为打开,false 为关闭 58 | }, 59 | "push": { // 必选一个,单人推送设置,若全部关闭,则使用 push.json 文件的配置,进行统一推送 60 | "email": { // 自定义邮箱推送,使用 qq 邮箱推送,就用 qq 邮箱的 smtp 服务地址和端口 61 | "enable": true, // true 为打开,false 为关闭 62 | "smtp_address": "smtp.qq.com", // stmp服务地址 63 | "smtp_port": 465, // stmp服务端口 64 | "send_email": "***@qq.com", // 发送邮箱的邮箱地址 65 | "send_pwd": "****", // 发送邮箱的邮箱授权码 66 | "receive_email": "**@qq.com" // 接收信息的邮箱地址,可自己给自己发 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | ### 🤝统一推送配置 73 | 74 | - 统一推送配置文件位于:`conf/push.json` 75 | - 若多用户打卡使用统一推送而不是个别单独推送则在此文件下进行推送的配置 76 | 77 | 78 | 79 | ## 💦使用方法(云函数) 80 | 81 | > 详细图文教程请前往:[博客](https://reajason.github.io/2021/03/19/17wanxiaoCheckInSCF/),请所有步骤及常见问题通读一遍再动手 82 | 83 | - 云函数 — 函数服务 — 新建云函数 84 | 85 | - 自定义创建 — 本地上传 zip 包(17wanxiaoCheckin-SCF v*.*.zip:[蓝奏云](https://lingsiki.lanzoui.com/b0ekhmcxe),密码:2333) 86 | 87 | - 上传之后往下滑 — 触发器配置 — 自定义创建 — 触发周期:自定义触发 — Cron 表达式:0 0 6,14 * * * * — 完成 — 立即跳转 88 | 89 | - 函数管理 — 函数配置 — 编辑 — 执行超时时间:900 — 保存 90 | 91 | - 函数代码 — `src/conf/user.json` — 根据上方的用户配置文件介绍以及里面的注释进行设置【第一次使用推荐 QQ 邮箱推送,数据推送全面】 92 | 93 | - 测试 — 若弹框【检测到您的函数未部署......】选是 — 查看执行日志以及推送信息(执行失败请带上执行日志完整截图反馈) 94 | 95 | - 第一类健康打卡成功结果:`{'msg': '成功', 'code': '10000', 'data': 1}`,显示打卡频繁也算 96 | 97 | - 第二类健康打卡成功结果:`{'code': 0, 'msg': '成功'}` 98 | 99 | - 校内打卡成功结果:`{'msg': '成功', 'code': '10000', 'data': 1}` 100 | 101 | - 出现成功,如果邮箱推送表格没有 None 值或支付宝小程序的健康助手有信息则是真正的打卡成功 102 | 103 | - 如果你们学校会记录打卡成功与否可直接在 **支付宝小程序** 查看是否记录上去(手机 app 登录的话之前获取的 device_id 就失效了) 104 | 105 | - 最后检查推送数据,如果表格中有 None,请根据第二行的信息,搭配第一行推送信息的格式,修改配置文件 106 | 107 | - 打开第一行,找到 updatainfo 这个东西,下面的有 null 的对应就是表格中的 None,记住它的 propertyname 108 | 109 | - 打开第二行,找到对应 perpertyname 的部分,根据 checkValue 的 text 选择你需要的选项,温度自己填个值就可 110 | 111 | - 打开配置文件,找到 post_json 下的 updatainfo,在里面加入你需要修改的值,格式和第一行里面的打卡数据一样 112 | 113 | - ``` 114 | "updatainfo":[ 115 | { 116 | "propertyname": "temperature", // 这个为第一行中找到值为 null 的那一项 117 | "value": "35.7" // 这个值为你想改的值,第二行中获取,如果是温度,自己填自己想的即可 118 | }, 119 | { 120 | "propertyname": "wengdu", 121 | "value": "36.4" 122 | } 123 | ] 124 | ``` 125 | 126 | 127 | 128 | - 由于前面使用软件获取了 device_id,所以请使用 **支付宝小程序** 查看打卡结果是否记录上去,以免手机登录 device_id 失效 129 | 130 | - 由于前面使用软件获取了 device_id,所以请使用 **支付宝小程序** 查看打卡结果是否记录上去,以免手机登录 device_id 失效 131 | 132 | - 由于前面使用软件获取了 device_id,所以请使用 **支付宝小程序** 查看打卡结果是否记录上去,以免手机登录 device_id 失效 133 | 134 | 135 | 136 | 137 | ## 🙋‍脚本有问题 138 | * 有问题可提 [issue](https://github.com/ReaJason/17wanxiaoCheckin-Actions/issues) 139 | 140 | 141 | 142 | ## 🎯ToDo 143 | > 2022-12-29 项目停止维护,期待在其他地方和大家见面 144 | 145 | > ~~本人希望自己代码能越写越好,因此在功能完善的情况下不断重构代码到满意的结果,希望能和想要技术交流的小伙伴一起学习( https://reajason.github.io )~~ 146 | 147 | - [ ] ~~面向对象方式重构代码~~ 148 | - [ ] ~~更优雅地处理错误和抛出异常~~ 149 | - [ ] ~~精简配置文件~~ 150 | 151 | 152 | 153 | ## 📜FQA 154 | 155 | - 若第一类健康打卡或校内打卡推送显示,需要修改对应位置下的 areaStr,修改格式为:`"areaStr": "{\"address\":\"天心区青园路251号中南林业科技大学\",\"text\":\"湖南省-长沙市-天心区\",\"code\":\"\"}"` ,`address`:对应手机打卡界面的下面一行,`text`:对应手机打卡界面的上面一行,根据自己的来,上面填什么就是什么,若是校内打卡的地址获取不到,可查看健康打卡的打卡数据推送里面的 areaStr 复制即可。 156 | - 若打卡结果为 `{'msg': '参数不合法', 'code': '10002', 'data': ;'deptid can not be null'}`,初步认为你们学校打卡数据无法自动获取,每次需要自己填写数据,解决办法为手机登录打卡抓签到包,然后在配置文件的 `post_json` 中填下你的打卡数据。 157 | - ~~若腾讯云函数测试失败,执行日志中出现 `......\nKeyError: 'whereabouts'","statusCode":430}`,这种情况就是选错健康打卡方式了,请选第一种健康打卡。~~(已在代码中给出相应提示) 158 | - 腾讯云函数测试失败返回结果中有 `Invoking task timed out after 3 seconds` ,请在函数设置中设置超时时间 900s。 159 | - 选择了第二类打卡也提示成功了,但是却没有打上,请尝试添加经纬度参数或者 **选择第一类健康打卡**。 160 | - 打卡失败显示 `无法找到该机构的投票模板数据`,则是选错了打卡方式或是第一类打卡 templateid 不对,可通过配置文件对应位置修改 161 | - 打卡失败显示 `您正在新设备上使用完美校园 | 获取 token 失败`,请重新获取 device_id 并确保不要再使用手机 app 登录完美校园 162 | - 打卡失败显示 `您当前打卡内容与现有模板不符,请重新打卡~`,请使用最新的打卡 scf 压缩包重新部署 163 | - 等待反馈...... 164 | 165 | ## 🌟Star History 166 | 167 | [![Star History Chart](https://api.star-history.com/svg?repos=ReaJason/17wanxiaoCheckin&type=Date)](https://star-history.com/#ReaJason/17wanxiaoCheckin&Date) 168 | -------------------------------------------------------------------------------- /conf/user.json: -------------------------------------------------------------------------------- 1 | // 使用 json5 标准格式,https://json5.org/ 2 | [ 3 | { 4 | "welcome": "用户一,这是一条欢迎语,每次打卡前会打印这句话,用来标记当前打卡用户,如:正在为 *** 打卡......", 5 | "phone": "", // 完美校园登录账号,必填 6 | "password": "", // 完美校园登录密码,必填 7 | "device_id": "", // 已验证完美校园登录设备ID,获取方式为下载蓝奏云链接中的 RegisterDeviceID.zip,必填 8 | "ykt_score": false, // 粮票收集,默认关闭,打卡完后自动签到收集粮票 9 | "healthy_checkin": { // 必选一个打卡方式 10 | "one_check": { // 第一类健康打卡 11 | "enable": false, // true 为打开,false 为关闭 12 | "templateid": "pneumonia", // 默认为学生打卡模板,pneumoniaTe 为教师打卡模板 13 | // 初次填写配置文件无需写, 以下为数据修改,如果数据获取出现问题,请抓包修改对应项 14 | "post_json": { 15 | "areaStr": "", 16 | "deptStr": "", 17 | "deptid": "", 18 | "stuNo": "", 19 | "username": "", 20 | "userid": "", 21 | "updatainfo": [] 22 | } 23 | }, 24 | "two_check": { // 第二类健康打卡 25 | "enable": false, // true 为打开,false 为关闭 26 | // 初次填写配置文件无需写,以下为数据修改,如果数据获取出现问题,请抓包修改或添加 27 | "post_json":{ 28 | "address": "", 29 | "stuNo": "", 30 | // 百度坐标拾取:http://api.map.baidu.com/lbsapi/getpoint/index.html,可用来获取下面两个值 31 | "longitude": "", // 请对应 address 填写该值,否则老师那边会显示手动签到 32 | "latitude": "" // 请对应 address 填写该值,否则老师那边会显示手动签到 33 | } 34 | } 35 | }, 36 | "campus_checkin": { // 校内打卡,没有就不用管 37 | "enable": false, // true 为打开,false 为关闭 38 | "post_json": { // 初次填写配置文件无需写,以下为数据修改,如果数据获取出现问题,请抓包修改对应项 39 | "areaStr": "", 40 | "deptStr": "", 41 | "deptid": "", 42 | "stuNo": "", 43 | "username": "", 44 | "userid": "", 45 | "updatainfo": [ 46 | { 47 | "propertyname": "temperature", 48 | "value": "36.4" 49 | }, 50 | { 51 | "propertyname": "symptom", 52 | "value": "无症状" 53 | } 54 | ] 55 | } 56 | }, 57 | "push": { // 必选一个,单人推送设置,若全部关闭,则使用 push.json 文件的配置,进行统一推送 58 | "email": { // 自定义邮箱推送,使用 qq 邮箱推送,就用 qq 邮箱的 smtp 服务地址和端口 59 | "enable": false, // true 为打开,false 为关闭 60 | "smtp_address": "smtp.qq.com", // stmp服务地址 61 | "smtp_port": 465, // stmp服务端口 62 | "send_email": "***@qq.com", // 发送邮箱的邮箱地址 63 | "send_pwd": "****", // 发送邮箱的邮箱授权码 64 | "receive_email": "**@qq.com" // 接收信息的邮箱地址,可自己给自己发 65 | }, 66 | "wechat": { // Server酱·Turbo版:https://sct.ftqq.com/ 67 | "enable": false, // true 为打开,false 为关闭 68 | "send_key": "**" // Server 酱推送密钥 69 | }, 70 | "qmsg": { // Qmsg酱:https://qmsg.zendee.cn/index.html 71 | "enable": false, // true 为打开,false 为关闭 72 | "key": "**", // 推送所必须的 key 73 | "type": "send", // send 私聊消息推送,group 群消息推送 74 | "qq_num": "**" // 上面设置send,则填写接受推送的qq号,group则填写群号,根据官网要求群号需要邮箱申请才能推送 75 | }, 76 | "pipehub": { // PipeHub:https://www.pipehub.net/ 77 | "enable": false, 78 | "key": "" 79 | }, 80 | "wechat_enterprise": { // 企业微信:https://work.weixin.qq.com/ 81 | "enable": false, 82 | "corp_id": "", // 企业 ID 83 | "corp_secret": "", // 自建应用 Secret 84 | "agent_id": "", // 应用 ID 85 | "to_user": "" // 接收者用户,多用户用|分割,所有用户填写 @all 86 | }, 87 | "bark": { // Bark: https://github.com/Finb/Bark 88 | "enable": false, 89 | "device_key": "", // Bark device_key 90 | "group": "健康打卡" // 推送消息分组 91 | } 92 | } 93 | }, 94 | { 95 | "welcome": "用户二,这是一条欢迎语,每次打卡前会打印这句话,用来标记当前打卡用户,如:正在为 *** 打卡......", 96 | "phone": "", // 完美校园登录账号 97 | "password": "", // 完美校园登录密码 98 | "device_id": "", // 已验证完美校园登录设备ID,获取方式为下载蓝奏云链接中的 RegisterDeviceID.zip 99 | "ykt_score": false, // 粮票收集,默认关闭,打卡完后自动签到收集粮票 100 | "healthy_checkin": { 101 | "one_check": { // 第一类健康打卡 102 | "enable": false, // true 为打开,false 为关闭 103 | "templateid": "pneumonia", // 默认为学生打卡模板,pneumoniaTe 为教师打卡模板 104 | // 以下为数据修改,如果数据获取出现问题,请抓包修改对应项 105 | "post_json": { 106 | "areaStr": "", 107 | "deptStr": "", 108 | "deptid": "", 109 | "stuNo": "", 110 | "username": "", 111 | "userid": "", 112 | "updatainfo": [] 113 | } 114 | }, 115 | "two_check": { // 第二类健康打卡 116 | "enable": false, // true 为打开,false 为关闭 117 | // 以下为数据修改,如果数据获取出现问题,请抓包修改或添加 118 | "post_json":{ 119 | "address": "", 120 | "stuNo": "", 121 | // 百度坐标拾取:http://api.map.baidu.com/lbsapi/getpoint/index.html,可用来获取下面两个值 122 | "longitude": "", // 请对应 address 填写该值,否则老师那边会显示手动签到 123 | "latitude": "" // 请对应 address 填写该值,否则老师那边会显示手动签到 124 | } 125 | } 126 | }, 127 | "campus_checkin": { // 校内打卡 128 | "enable": false, // true 为打开,false 为关闭 129 | "post_json": { // 以下为数据修改,如果数据获取出现问题,请抓包修改对应项 130 | "areaStr": "", 131 | "deptStr": "", 132 | "deptid": "", 133 | "stuNo": "", 134 | "username": "", 135 | "userid": "", 136 | "updatainfo": [ 137 | { 138 | "propertyname": "temperature", 139 | "value": "36.4" 140 | }, 141 | { 142 | "propertyname": "symptom", 143 | "value": "无症状" 144 | } 145 | ] 146 | } 147 | }, 148 | "push": { // 单人推送设置,若全部关闭,则使用 push.json 文件的配置,进行统一推送 149 | "email": { // 自定义邮箱推送,使用 qq 邮箱推送,就用 qq 邮箱的 smtp 服务地址和端口 150 | "enable": false, // true 为打开,false 为关闭 151 | "smtp_address": "smtp.qq.com", // stmp服务地址 152 | "smtp_port": 465, // stmp服务端口 153 | "send_email": "***@qq.com", // 发送邮箱的邮箱地址 154 | "send_pwd": "****", // 发送邮箱的邮箱授权码 155 | "receive_email": "****@qq.com" // 接收信息的邮箱地址,可自己给自己发 156 | }, 157 | "wechat": { // Server酱·Turbo版:https://sct.ftqq.com/ 158 | "enable": false, // true 为打开,false 为关闭 159 | "send_key": "" // Server 酱推送密钥 160 | }, 161 | "qmsg": { // Qmsg酱:https://qmsg.zendee.cn/index.html 162 | "enable": false, // true 为打开,false 为关闭 163 | "key": "", // 推送所必须的 key 164 | "type": "send", // send 私聊消息推送,group 群消息推送 165 | "qq_num": "" // 上面设置send,则填写接受推送的qq号,group则填写群号,根据官网要求群号需要邮箱申请才能推送 166 | }, 167 | "pipehub": { // PipeHub:https://www.pipehub.net/ 168 | "enable": false, 169 | "key": "" 170 | }, 171 | "wechat_enterprise": { // 企业微信:https://work.weixin.qq.com/ 172 | "enable": false, 173 | "corp_id": "", // 企业 ID 174 | "corp_secret": "", // 自建应用 Secret 175 | "agent_id": "", // 自建应用 ID 176 | "to_user": "" // 接收者用户,多用户用|分割,所有用户填写 @all 177 | }, 178 | "bark": { // Bark: https://github.com/Finb/Bark 179 | "enable": false, 180 | "device_key": "", // Bark device_key 181 | "group": "健康打卡" // 推送消息分组 182 | } 183 | } 184 | } 185 | ] -------------------------------------------------------------------------------- /api/campus_check.py: -------------------------------------------------------------------------------- 1 | """ 2 | 校内打卡相关函数 3 | @create:2021/03/10 4 | @filename:campus_check.py 5 | @author:ReaJason 6 | @email_addr:reajason@163.com 7 | @blog_website:https://reajason.top 8 | @last_modify:2021/03/15 9 | """ 10 | import time 11 | import json 12 | 13 | import requests 14 | 15 | from setting import log 16 | 17 | 18 | def get_id_list_v2(token, custom_type_id): 19 | """ 20 | 通过校内模板id获取校内打卡具体的每个时间段id 21 | :param token: 用户令牌 22 | :param custom_type_id: 校内打卡模板id 23 | :return: 返回校内打卡id列表 24 | """ 25 | post_data = { 26 | "customerAppTypeId": custom_type_id, 27 | "longitude": "", 28 | "latitude": "", 29 | "token": token, 30 | } 31 | try: 32 | res = requests.post( 33 | "https://reportedh5.17wanxiao.com/api/clock/school/rules", data=post_data 34 | ) 35 | return res.json()["customerAppTypeDto"]["ruleList"] 36 | except: 37 | return None 38 | 39 | 40 | def get_id_list_v1(token): 41 | """ 42 | 通过校内模板id获取校内打卡具体的每个时间段id(初版,暂留) 43 | :param token: 用户令牌 44 | :return: 返回校内打卡id列表 45 | """ 46 | post_data = {"appClassify": "DK", "token": token} 47 | try: 48 | res = requests.post( 49 | "https://reportedh5.17wanxiao.com/api/clock/school/childApps", 50 | data=post_data, 51 | ).json() 52 | if res.json()["appList"]: 53 | app_list = res["appList"][-1]["customerAppTypeRuleList"] \ 54 | if res["appList"][-1]["customerAppTypeRuleList"] \ 55 | else res["appList"][0]["customerAppTypeRuleList"] 56 | id_list = sorted( 57 | app_list, 58 | key=lambda x: x["id"], 59 | ) 60 | res_dict = [ 61 | {"customerAppTypeId": j["id"], "templateid": f"clockSign{i + 1}"} 62 | for i, j in enumerate(id_list) 63 | ] 64 | return res_dict 65 | return None 66 | except: 67 | return None 68 | 69 | 70 | def get_customer_type_id(token): 71 | """ 72 | 通过校内模板id获取校内打卡具体的每个时间段id(初版,暂留) 73 | :param token: 用户令牌 74 | :return: 返回校内打卡id列表 75 | """ 76 | post_data = {"appClassify": "DK", "token": token} 77 | try: 78 | res = requests.post( 79 | "https://reportedh5.17wanxiao.com/api/clock/school/childApps", 80 | data=post_data, 81 | ).json() 82 | for app in res["appList"]: 83 | if '校内' in app['name']: 84 | return app['id'] 85 | except: 86 | return None 87 | 88 | 89 | def get_campus_check_post(template_id, custom_rule_id, stu_num, token): 90 | """ 91 | 获取打卡数据 92 | :param template_id: 93 | :param custom_rule_id: 94 | :param stu_num: 95 | :param token: 96 | :return: 97 | """ 98 | campus_check_post_json = { 99 | "businessType": "epmpics", 100 | "jsonData": { 101 | "templateid": template_id, 102 | "customerAppTypeRuleId": custom_rule_id, 103 | "stuNo": stu_num, 104 | "token": token 105 | }, 106 | "method": "userComeAppSchool", 107 | "token": token 108 | } 109 | for _ in range(3): 110 | try: 111 | res = requests.post( 112 | url="https://reportedh5.17wanxiao.com/sass/api/epmpics", 113 | json=campus_check_post_json, 114 | timeout=10, 115 | ).json() 116 | except: 117 | log.warning("完美校园校内打卡post参数获取失败,正在重试...") 118 | time.sleep(1) 119 | continue 120 | if res["code"] != "10000": 121 | """ 122 | {'msg': '业务异常', 'code': '10007', 'data': '无法找到该机构的投票模板数据!'} 123 | """ 124 | log.warning(res['data']) 125 | return None 126 | data = json.loads(res["data"]) 127 | post_dict = { 128 | "areaStr": data['areaStr'], 129 | "deptStr": data['deptStr'], 130 | "deptid": data['deptStr']['deptid'] if data['deptStr'] else None, 131 | "customerid": data['customerid'], 132 | "userid": data['userid'], 133 | "username": data['username'], 134 | "stuNo": data['stuNo'], 135 | "phonenum": data["phonenum"], 136 | "templateid": data["templateid"], 137 | "updatainfo": [ 138 | {"propertyname": i["propertyname"], "value": i["value"]} 139 | for i in data["cusTemplateRelations"] 140 | ], 141 | "updatainfo_detail": [ 142 | { 143 | "propertyname": i["propertyname"], 144 | "checkValues": i["checkValues"], 145 | "description": i["decription"], 146 | "value": i["value"], 147 | } 148 | for i in data["cusTemplateRelations"] 149 | ], 150 | "checkbox": [ 151 | {"description": i["decription"], "value": i["value"], "propertyname": i["propertyname"]} 152 | for i in data["cusTemplateRelations"] 153 | ], 154 | } 155 | log.info("完美校园校内打卡post参数获取成功") 156 | return post_dict 157 | return None 158 | 159 | 160 | def campus_check_in(phone, token, post_dict, custom_rule_id): 161 | """ 162 | 校内打卡 163 | :param phone: 电话号 164 | :param token: 用户令牌 165 | :param post_dict: 校内打卡数据 166 | :param custom_rule_id: 校内打卡id 167 | :return: 168 | """ 169 | check_json = { 170 | "businessType": "epmpics", 171 | "method": "submitUpInfoSchool", 172 | "jsonData": { 173 | "deptStr": post_dict["deptStr"], 174 | "areaStr": post_dict["areaStr"], 175 | "reportdate": round(time.time() * 1000), 176 | "customerid": post_dict["customerid"], 177 | "deptid": post_dict["deptid"], 178 | "source": "app", 179 | "templateid": post_dict["templateid"], 180 | "stuNo": post_dict["stuNo"], 181 | "username": post_dict["username"], 182 | "phonenum": phone, 183 | "userid": post_dict["userid"], 184 | "updatainfo": post_dict["updatainfo"], 185 | "customerAppTypeRuleId": custom_rule_id, 186 | "clockState": 0, 187 | "token": token, 188 | }, 189 | "token": token, 190 | } 191 | try: 192 | res = requests.post( 193 | "https://reportedh5.17wanxiao.com/sass/api/epmpics", json=check_json 194 | ).json() 195 | """ 196 | {'msg': '业务异常', 'code': '10007', 'data': '请在正确的打卡时间打卡'} 197 | """ 198 | if res["code"] == "10000": 199 | log.info(res) 200 | elif res['data'] == "areaStr can not be null": 201 | log.warning("当前用户无法获取校内打卡地址信息,请前往配置文件,campus_checkin 下的 areaStr 设置地址信息") 202 | elif res['data'] == "请在正确的打卡时间打卡": 203 | log.warning( f'当前已不在该打卡时间范围内,{res["data"]}') 204 | else: 205 | log.warning(res) 206 | return { 207 | 'status': 1, 208 | 'res': res, 209 | 'post_dict': post_dict, 210 | 'check_json': check_json, 211 | 'type': post_dict["templateid"] 212 | } 213 | except: 214 | errmsg = f"```校内打卡请求出错```" 215 | log.warning("校内打卡请求出错") 216 | return {'status': 0, 'errmsg': errmsg} 217 | -------------------------------------------------------------------------------- /api/wanxiao_push.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | 4 | from utils.bark import bark_push 5 | from utils.server_chan import server_push 6 | from utils.wechat_enterprise import wechat_enterprise_push 7 | from utils.email_push import email_push 8 | from utils.qmsg import qmsg_push 9 | from utils.pipehub import pipe_push 10 | 11 | 12 | def wanxiao_server_push(send_key, check_info_list): 13 | utc8_time = datetime.datetime.utcnow() + datetime.timedelta(hours=8) 14 | push_list = [f""" 15 | ------ 16 | #### 现在时间: 17 | ``` 18 | {utc8_time.strftime("%Y-%m-%d %H:%M:%S %p")} 19 | ```"""] 20 | for check_info in check_info_list: 21 | if check_info["status"]: 22 | if check_info["post_dict"].get("checkbox"): 23 | post_msg = "\n".join( 24 | [ 25 | f"| {i['description']} | {i['value']} |" 26 | for i in check_info["post_dict"]["checkbox"] 27 | ] 28 | ) 29 | else: 30 | post_msg = "暂无详情" 31 | name = check_info["post_dict"].get("username") 32 | if not name: 33 | name = check_info["post_dict"]["name"] 34 | push_list.append( 35 | f"""#### {name}{check_info['type']}打卡信息: 36 | ``` 37 | {json.dumps(check_info['check_json'], sort_keys=True, indent=4, ensure_ascii=False)} 38 | ``` 39 | ------ 40 | | Text | Message | 41 | | :----------------------------------- | :--- | 42 | {post_msg} 43 | ------ 44 | ``` 45 | {check_info['res']} 46 | ```""" 47 | ) 48 | else: 49 | push_list.append( 50 | f"""------ 51 | #### {check_info['errmsg']} 52 | ------ 53 | """ 54 | ) 55 | push_list.append( 56 | f""" 57 | > 58 | > [17wanxiaoCheckin](https://github.com/ReaJason/17wanxiaoCheckin-Actions) 59 | > 60 | >期待你给项目的star✨ 61 | """ 62 | ) 63 | return server_push(send_key, "健康打卡", "\n".join(push_list)) 64 | 65 | 66 | def wanxiao_email_push(send_email, send_pwd, receive_email, smtp_address, smtp_port, check_info_list): 67 | mail_msg_list = [] 68 | for check in check_info_list: 69 | if check["status"]: 70 | name = check['post_dict'].get('username') 71 | if not name: 72 | name = check['post_dict']['name'] 73 | mail_msg_list.append(f"""
74 |
75 | {name}:{check["type"]} 打卡结果:{check['res']} 76 |

 77 | {json.dumps(check['check_json'], sort_keys=True, indent=4, ensure_ascii=False)}
 78 | 
79 |
80 |
81 | >>>填写数据抓包详情(用于 updatainfo 数据的修改)<<< 82 |

 83 | {json.dumps(check['post_dict']['updatainfo_detail'], sort_keys=True, indent=4, ensure_ascii=False)}
 84 | 
85 |
86 | >>>打卡信息数据表格<<< 87 | 88 | 89 | 90 | 91 | 92 | """ 93 | ) 94 | for index, box in enumerate(check["post_dict"]["checkbox"]): 95 | if index % 2: 96 | mail_msg_list.append( 97 | f""" 98 | 99 | 100 | """ 101 | ) 102 | else: 103 | mail_msg_list.append(f""" 104 | 105 | 106 | """ 107 | ) 108 | mail_msg_list.append( 109 | f""" 110 |
TextValue
{box['description']}{box['value']}
{box['description']}{box['value']}
""" 111 | ) 112 | else: 113 | mail_msg_list.append( 114 | f"""
115 | {check['errmsg']}""" 116 | ) 117 | css = """""" 148 | mail_msg_list.append(css) 149 | mail_msg_list.append(f""" 150 |

>>>> 17wanxiaoCheckin-Actions 151 | <<<<

152 | """) 153 | return email_push(send_email, send_pwd, receive_email, 154 | title="完美校园健康打卡", text="".join(mail_msg_list), 155 | smtp_address=smtp_address, smtp_port=smtp_port) 156 | 157 | 158 | def wanxiao_qmsg_push(key, qq_num, type, check_info_list): 159 | utc8_time = datetime.datetime.utcnow() + datetime.timedelta(hours=8) 160 | push_list = [f'@face=74@ {utc8_time.strftime("%Y-%m-%d %H:%M:%S")} @face=74@ '] 161 | for check_info in check_info_list: 162 | if check_info["status"]: 163 | name = check_info["post_dict"].get("username") 164 | if not name: 165 | name = check_info["post_dict"]["name"] 166 | push_list.append(f"""\ 167 | @face=54@ {name}{check_info['type']} @face=54@ 168 | @face=211@ 169 | {check_info['res']} 170 | @face=211@""") 171 | else: 172 | push_list.append(check_info['errmsg']) 173 | return qmsg_push(key, qq_num, "\n".join(push_list), type) 174 | 175 | 176 | def wanxiao_pipe_push(key, check_info_list): 177 | utc8_time = datetime.datetime.utcnow() + datetime.timedelta(hours=8) 178 | push_list = [f'打卡时间: {utc8_time.strftime("%Y-%m-%d %H:%M:%S")}'] 179 | for check_info in check_info_list: 180 | if check_info["status"]: 181 | name = check_info["post_dict"].get("username") 182 | if not name: 183 | name = check_info["post_dict"]["name"] 184 | push_list.append(f"""\ 185 | {name}{check_info['type']} 186 | {check_info['res']} 187 | """) 188 | else: 189 | push_list.append(check_info['errmsg']) 190 | return pipe_push(key, "\n".join(push_list).encode()) 191 | 192 | 193 | def wanxiao_wechat_enterprise_push(corp_id, corp_secret, agent_id, to_user, check_info_list): 194 | utc8_time = datetime.datetime.utcnow() + datetime.timedelta(hours=8) 195 | push_list = [f'打卡时间:\n{utc8_time.strftime("%Y-%m-%d %H:%M:%S")}'] 196 | for check_info in check_info_list: 197 | if check_info["status"]: 198 | name = check_info["post_dict"].get("username") 199 | if not name: 200 | name = check_info["post_dict"]["name"] 201 | push_list.append(f"{name}{check_info['type']}:\n{check_info['res']}") 202 | else: 203 | push_list.append(check_info['errmsg']) 204 | return wechat_enterprise_push(corp_id, corp_secret, agent_id, to_user, "\n".join(push_list)) 205 | 206 | 207 | def wanxiao_bark_push(device_key, group, check_info_list): 208 | utc8_time = datetime.datetime.utcnow() + datetime.timedelta(hours=8) 209 | title = f"""{utc8_time.strftime("%Y-%m-%d")} 健康打卡""" 210 | push_list = [] 211 | for check_info in check_info_list: 212 | if check_info["status"]: 213 | name = check_info["post_dict"].get("username") 214 | if not name: 215 | name = check_info["post_dict"]["name"] 216 | if check_info['res']['code'] == "10000": 217 | push_list.append(f"""{name}:打卡{check_info['res']['msg']}😄😄😄""") 218 | else: 219 | push_list.append(f"""{name}:{check_info["res"]["data"]}😢😢😢""") 220 | else: 221 | push_list.append(check_info['errmsg']) 222 | return bark_push(device_key, "\n".join(push_list), title, group) 223 | -------------------------------------------------------------------------------- /login/campus.py: -------------------------------------------------------------------------------- 1 | # 文件名:campus.py 2 | # 创建日期:2020年09月13日09点44分 3 | # 作者:Zhongbr 4 | # 邮箱:zhongbr@icloud.com 5 | """ 6 | 修改:ReaJason 7 | 修改日期:2021年2月1日 8 | 邮箱:reajason@163.com 9 | 博客:https://reajason.top 10 | ChangeLog: 11 | 1、优化原作者写的账密登录方式和接口替换(server.17wanxiao.com -> app.17wanxiao.com) 12 | 2、增加短信登录方式 13 | """ 14 | import json 15 | import hashlib 16 | import requests 17 | import urllib3 18 | 19 | from login import des_3 20 | from login import rsa_encrypt as rsa 21 | 22 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 23 | 24 | 25 | class CampusLogin: 26 | __slots__ = ['login_info'] 27 | 28 | def __init__(self, phone_num, device_id, app_version=10558101, 29 | user_agent="Dalvik/2.1.0 (Linux; U; Android 11; Redmi K20 Pro Premium Edition Build/RKQ1.200826.002)", 30 | phone_code="raphael", sys_type="android", sys_version="11", phone_model="Redmi K20 Pro Premium Edition" 31 | ): 32 | """ 33 | 初始化CampusLogin对象并交换密钥 34 | :param phone_num: 完美校园登录账号(手机号) 35 | :param device_id: 设备ID,模拟器IMEI或其他方式获取的可用于完美校园登录的ID 36 | :param app_version: 完美校园app版本 37 | :param user_agent: 账密登录时所用的用户代理 38 | :param phone_code: 手机代号 39 | :param sys_type: 系统类型,android,ipad,iphone 40 | :param sys_version: 系统版本 41 | :param phone_model: 手机机型 42 | """ 43 | self.login_info = { 44 | "phoneNum": phone_num, 45 | "deviceId": device_id, 46 | "appKey": "", 47 | "sessionId": "", 48 | "wanxiaoVersion": app_version, 49 | "userAgent": user_agent, 50 | "shebeixinghao": phone_code, 51 | "systemType": sys_type, 52 | "telephoneInfo": sys_version, 53 | "telephoneModel": phone_model 54 | } 55 | self.exchange_secret() 56 | 57 | def exchange_secret(self): 58 | """ 59 | 交换密钥 60 | :return: if fail, raise error 61 | """ 62 | rsa_keys = rsa.create_key_pair(1024) 63 | try: 64 | resp = requests.post( 65 | 'https://app.17wanxiao.com/campus/cam_iface46/exchangeSecretkey.action', 66 | headers={'User-Agent': self.login_info['userAgent']}, 67 | json={'key': rsa_keys[0]}, 68 | verify=False, 69 | timeout=30 70 | ) 71 | session_info = json.loads(rsa.rsa_decrypt(resp.text.encode(resp.apparent_encoding), rsa_keys[1])) 72 | self.login_info['sessionId'] = session_info['session'] 73 | self.login_info['appKey'] = session_info['key'][:24] 74 | except Exception as e: 75 | raise e.__class__("完美校园交换密钥失败", e) 76 | 77 | def pwd_login(self, password): 78 | """ 79 | 完美校园密码登录 80 | :param password: 完美校园登录密码 81 | :return: dict 82 | if success:return { 83 | "status": 1, 84 | "token": token, 85 | "msg": msg 86 | } 87 | if fail:return { 88 | "status": 0, 89 | "errmsg": errmsg 90 | } 91 | """ 92 | password_list = [des_3.des_3_encrypt(i, self.login_info['appKey'], '66666666') for i in password] 93 | login_args = { 94 | 'appCode': 'M002', 95 | 'deviceId': self.login_info['deviceId'], 96 | 'netWork': 'wifi', 97 | 'password': password_list, 98 | 'qudao': 'guanwang', 99 | 'requestMethod': 'cam_iface46/loginnew.action', 100 | 'shebeixinghao': self.login_info['shebeixinghao'], 101 | 'systemType': self.login_info['systemType'], 102 | 'telephoneInfo': self.login_info['telephoneInfo'], 103 | 'telephoneModel': self.login_info['telephoneModel'], 104 | 'type': '1', 105 | 'userName': self.login_info['phoneNum'], 106 | 'wanxiaoVersion': self.login_info['wanxiaoVersion'] 107 | } 108 | upload_args = { 109 | 'session': self.login_info['sessionId'], 110 | 'data': des_3.object_encrypt(login_args, self.login_info['appKey']) 111 | } 112 | try: 113 | resp = requests.post( 114 | 'https://app.17wanxiao.com/campus/cam_iface46/loginnew.action', 115 | headers={'campusSign': hashlib.sha256(json.dumps(upload_args).encode('utf-8')).hexdigest()}, 116 | json=upload_args, 117 | verify=False, 118 | timeout=30, 119 | ).json() 120 | """ 121 | {'result_': True, 'data': '........', 'message_': '登录成功', 'code_': '0'} 122 | {'result_': False, 'message_': '该手机号未注册完美校园', 'code_': '4'} 123 | {'result_': False, 'message_': '您正在新设备上使用完美校园,请使用验证码进行验证登录', 'code_': '5'} 124 | {'result_': False, 'message_': '密码错误,您还有5次机会!', 'code_': '5'} 125 | """ 126 | if resp['result_']: 127 | return {"status": 1, "token": self.login_info['sessionId'], "msg": resp['message_']} 128 | return {"status": 0, "errmsg": resp['message_']} 129 | except Exception as e: 130 | return {"status": 0, "errmsg": f"{e.__class__},{e}"} 131 | 132 | def send_sms(self): 133 | """ 134 | 发送短信 135 | :return: dict 136 | if success:return { 137 | "status": 1, 138 | "msg": msg 139 | } 140 | if fail:return { 141 | "status": 0, 142 | "errmsg": errmsg 143 | } 144 | """ 145 | send = { 146 | 'action': "registAndLogin", 147 | 'deviceId': self.login_info['deviceId'], 148 | 'mobile': self.login_info['phoneNum'], 149 | 'requestMethod': "cam_iface46/gainMatrixCaptcha.action", 150 | 'type': "sms" 151 | } 152 | upload_args = { 153 | "session": self.login_info["sessionId"], 154 | "data": des_3.object_encrypt(send, self.login_info["appKey"]) 155 | } 156 | try: 157 | resp = requests.post( 158 | "https://app.17wanxiao.com/campus/cam_iface46/gainMatrixCaptcha.action", 159 | headers={"campusSign": hashlib.sha256(json.dumps(upload_args).encode('utf-8')).hexdigest()}, 160 | json=upload_args, 161 | verify=False, 162 | timeout=30 163 | ).json() 164 | """ 165 | {"result_":true,"message_":"聚合验证码发送成功。","code_":0} 166 | """ 167 | if resp['result_']: 168 | return {"status": 1, "msg": resp['message_']} 169 | return {"status": 0, "errmsg": f"短信发送失败,{resp}"} 170 | except Exception as e: 171 | return {"status": 0, "errmsg": f"{e.__class__},{e}"} 172 | 173 | def sms_login(self, sms): 174 | """ 175 | 短信登录 176 | :param sms: 收到的验证码 177 | :return: dict 178 | if success:return { 179 | "status": 1, 180 | "token": token, 181 | "msg": msg 182 | } 183 | if fail:return { 184 | "status": 0, 185 | "errmsg": errmsg 186 | } 187 | """ 188 | data = { 189 | 'appCode': "M002", 190 | 'deviceId': self.login_info['deviceId'], 191 | 'netWork': "wifi", 192 | 'qudao': "guanwang", 193 | 'requestMethod': "cam_iface46/registerUsersByTelAndLoginNew.action", 194 | 'shebeixinghao': self.login_info['shebeixinghao'], 195 | 'sms': sms, 196 | 'systemType': self.login_info['systemType'], 197 | 'telephoneInfo': self.login_info['telephoneInfo'], 198 | 'telephoneModel': self.login_info['telephoneModel'], 199 | 'mobile': self.login_info['phoneNum'], 200 | 'type': '2', 201 | 'wanxiaoVersion': self.login_info['wanxiaoVersion'] 202 | } 203 | upload_args = { 204 | "session": self.login_info["sessionId"], 205 | "data": des_3.object_encrypt(data, self.login_info["appKey"]) 206 | } 207 | try: 208 | resp = requests.post( 209 | "https://app.17wanxiao.com/campus/cam_iface46/registerUsersByTelAndLoginNew.action", 210 | headers={"campusSign": hashlib.sha256(json.dumps(upload_args).encode('utf-8')).hexdigest()}, 211 | json=upload_args, 212 | verify=False, 213 | timeout=30 214 | ).json() 215 | """ 216 | {"result_":true,"data":"******","message_":"登录成功","code_":"0"} 217 | {"result_":false,"message_":"短信验证码错误","code_":"6"} 218 | """ 219 | if resp['result_']: 220 | return {"status": 1, "token": self.login_info['sessionId'], "msg": resp['message_']} 221 | return {"status": 0, "errmsg": resp['message_']} 222 | except Exception as e: 223 | return {"status": 0, "errmsg": f"{e.__class__},{e}"} 224 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | 4 | from api.campus_check import get_id_list_v1, get_id_list_v2, get_customer_type_id, get_campus_check_post, \ 5 | campus_check_in 6 | from api.healthy1_check import get_healthy1_check_post_json, healthy1_check_in 7 | from api.healthy2_check import get_healthy2_check_posh_json, healthy2_check_in 8 | from api.user_info import get_user_info 9 | from api.wanxiao_push import wanxiao_qmsg_push, wanxiao_server_push, wanxiao_email_push, wanxiao_pipe_push, \ 10 | wanxiao_wechat_enterprise_push, wanxiao_bark_push 11 | from api.ykt_score import get_score_list, get_active_score, get_task_list, get_article_id, get_all_score, \ 12 | get_article_score, get_class_score, ykt_check_in 13 | from login import CampusLogin 14 | from setting import log 15 | from utils.config import load_config 16 | 17 | 18 | def retry(retry_num, delay): 19 | def wrapper(func): 20 | def inner(*args, **kwargs): 21 | result = None 22 | for _ in range(retry_num): 23 | result = func(*args, **kwargs) 24 | if result: 25 | return result 26 | time.sleep(delay) 27 | return result 28 | return inner 29 | return wrapper 30 | 31 | 32 | @retry(5, 10) 33 | def get_token(username, password, device_id): 34 | try: 35 | campus_login = CampusLogin(phone_num=username, device_id=device_id) 36 | except Exception as e: 37 | log.warning(e) 38 | return None 39 | login_dict = campus_login.pwd_login(password) 40 | if login_dict["status"]: 41 | log.info(f"{username[:4]},{login_dict['msg']}") 42 | return login_dict["token"] 43 | else: 44 | log.warning(f"{username[:4]},{login_dict['errmsg']}") 45 | return None 46 | 47 | 48 | def merge_post_json(dict1, dict2): 49 | for key, value in dict2.items(): 50 | if value and key in dict1 and key != 'updatainfo': 51 | dict1[key] = value 52 | if key == 'updatainfo': 53 | dict3 = {} 54 | for i, j in enumerate(dict1['updatainfo']): 55 | dict3[j['propertyname']] = i 56 | for i in value: 57 | if i['propertyname'] in dict3: 58 | dict1['updatainfo'][dict3[i['propertyname']]]['value'] = i['value'] 59 | dict1['checkbox'][dict3[i['propertyname']]]['value'] = i['value'] 60 | 61 | 62 | def info_push(push_dict, raw_info): 63 | push_funcs = { 64 | "email": wanxiao_email_push, 65 | "wechat": wanxiao_server_push, 66 | "qmsg": wanxiao_qmsg_push, 67 | "pipehub": wanxiao_pipe_push, 68 | "wechat_enterprise": wanxiao_wechat_enterprise_push, 69 | "bark": wanxiao_bark_push 70 | } 71 | 72 | push_raw_info = { 73 | "check_info_list": raw_info 74 | } 75 | 76 | for push_name, push_func in push_funcs.items(): 77 | enable = push_dict.get(push_name, {}).get("enable") 78 | if not enable: 79 | pass 80 | else: 81 | del push_dict[push_name]["enable"] 82 | push_dict[push_name].update(push_raw_info) 83 | params_dict = push_dict[push_name] 84 | push_res = push_func(**params_dict) 85 | if push_res['status']: 86 | log.info(push_res["msg"]) 87 | else: 88 | log.warning(push_res["errmsg"]) 89 | 90 | 91 | def check_in(user): 92 | check_dict_list = [] 93 | 94 | # 登录获取token用于打卡 95 | token = get_token(user['phone'], user['password'], user['device_id']) 96 | 97 | if not token: 98 | errmsg = f"{user['phone'][:4]},获取token失败,打卡失败," 99 | log.warning(errmsg) 100 | check_dict_list.append({"status": 0, "errmsg": errmsg}) 101 | return check_dict_list 102 | 103 | # 获取个人信息 104 | user_info = get_user_info(token) 105 | if not user_info: 106 | errmsg = f"{user['phone'][:4]},获取个人信息失败,打卡失败" 107 | log.warning(errmsg) 108 | check_dict_list.append({"status": 0, "errmsg": errmsg}) 109 | return check_dict_list 110 | log.info(f'{user_info["username"][0]}-{user_info["school"]},获取个人信息成功') 111 | 112 | healthy1_check_config = user.get('healthy_checkin', {}).get('one_check') 113 | healthy2_check_config = user.get('healthy_checkin', {}).get('two_check') 114 | if healthy1_check_config.get('enable'): 115 | # 第一类健康打卡 116 | 117 | # 获取第一类健康打卡的参数 118 | post_dict = get_healthy1_check_post_json(token, healthy1_check_config.get('templateid', "pneumonia")) 119 | 120 | # 合并配置文件的打卡信息 121 | merge_post_json(post_dict, healthy1_check_config.get('post_json', {})) 122 | 123 | healthy1_check_dict = healthy1_check_in(token, user['phone'], post_dict) 124 | check_dict_list.append(healthy1_check_dict) 125 | elif healthy2_check_config.get('enable'): 126 | # 第二类健康打卡 127 | 128 | # 获取第二类健康打卡参数 129 | post_dict = get_healthy2_check_posh_json(token) 130 | 131 | # 合并配置文件的打卡信息 132 | if not healthy2_check_config['post_json']['latitude'] and not healthy2_check_config['post_json']['longitude']: 133 | post_dict['latitude'] = "" 134 | post_dict['longitude'] = "" 135 | log.info('当前打卡未设置经纬度,后台会将此次打卡计为手动打卡(学校没做要求可不管)') 136 | for i, j in healthy2_check_config['post_json'].items(): 137 | if j: 138 | post_dict[i] = j 139 | healthy2_check_dict = healthy2_check_in(token, user_info["customerId"], post_dict) 140 | 141 | check_dict_list.append(healthy2_check_dict) 142 | else: 143 | log.info('当前并未配置健康打卡方式,暂不进行打卡操作') 144 | 145 | # 校内打卡 146 | campus_check_config = user.get('campus_checkin', {}) 147 | if not campus_check_config.get('enable'): 148 | log.info('当前并未开启校内打卡,暂不进行打卡操作') 149 | else: 150 | # 获取校内打卡ID 151 | custom_type_id = user_info.get('customerAppTypeId', get_customer_type_id(token)) 152 | if custom_type_id: 153 | id_list = get_id_list_v2(token, custom_type_id) 154 | else: 155 | id_list = get_id_list_v1(token) 156 | 157 | if not id_list: 158 | log.warning('当前未获取到校内打卡ID,请尝试重新运行,如仍未获取到,请反馈') 159 | return check_dict_list 160 | for index, i in enumerate(id_list): 161 | start_end = f'{i["templateid"]} ({i.get("startTime", "")}-{i.get("endTime", "")})' 162 | log.info(f"{start_end:-^40}") 163 | 164 | # 获取校内打卡参数 165 | campus_dict = get_campus_check_post( 166 | template_id=i['templateid'], 167 | custom_rule_id=i['id'], 168 | stu_num=user_info['stuNo'], 169 | token=token 170 | ) 171 | # 合并配置文件的打卡信息 172 | merge_post_json(campus_dict, campus_check_config['post_json']) 173 | 174 | # 校内打卡 175 | campus_check_dict = campus_check_in(user['phone'], token, campus_dict, i['id']) 176 | check_dict_list.append(campus_check_dict) 177 | log.info("-" * 40) 178 | 179 | # 粮票收集 180 | if user.get('ykt_score'): 181 | ykt_check_in(token) 182 | get_all_score(token) 183 | task_list = get_task_list(token) 184 | for task in task_list: 185 | if task['name'] == '校园头条': 186 | if not task['finished']: 187 | article_id = get_article_id(token) 188 | for _ in range(8): 189 | time.sleep(1) 190 | get_article_score(token, article_id) 191 | else: 192 | log.info("校园头条任务已完成") 193 | get_all_score(token) 194 | if task['name'] == '查看课表': 195 | if not task['finished']: 196 | get_class_score(token) 197 | else: 198 | log.info("查看课表任务已完成") 199 | # 获取活跃奖励 200 | get_active_score(token, get_score_list(token)['active'][0]) 201 | 202 | # 获取其他奖励 203 | get_all_score(token) 204 | 205 | return check_dict_list 206 | 207 | 208 | def main_handler(*args, **kwargs): 209 | # 推送数据 210 | raw_info = [] 211 | 212 | # 加载用户配置文件 213 | user_config_path = kwargs['user_config_path'] if kwargs.get('user_config_path') else './conf/user.json' 214 | push_config_path = kwargs['push_config_path'] if kwargs.get('push_config_path') else './conf/push.json' 215 | user_config_dict = load_config(user_config_path) 216 | for user_config in user_config_dict: 217 | if not user_config['phone']: 218 | continue 219 | log.info(user_config.get('welcome')) 220 | 221 | # 单人打卡 222 | check_dict = check_in(user_config) 223 | # 单人推送 224 | info_push(user_config['push'], check_dict) 225 | raw_info.extend(check_dict) 226 | 227 | # 统一推送 228 | all_push_config = load_config(push_config_path) 229 | info_push(all_push_config, raw_info) 230 | 231 | 232 | def functiongraph_handler(event, context): 233 | ''' 234 | 华为云函数工作流 FunctionGraph 执行入口 235 | ''' 236 | user_config_path: str = 'code/conf/user.json' 237 | push_config_path: str = 'code/conf/push.json' 238 | main_handler(user_config_path=user_config_path, push_config_path=push_config_path) 239 | 240 | 241 | if __name__ == "__main__": 242 | user_config_path = os.path.join(os.path.dirname(__file__), 'conf', 'user.json') 243 | push_config_path = os.path.join(os.path.dirname(__file__), 'conf', 'push.json') 244 | main_handler(user_config_path=user_config_path, push_config_path=push_config_path) 245 | --------------------------------------------------------------------------------