├── LICENSE ├── README.md └── sport_bot.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Qinyu Liu 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XJTU西安交大研究生自动体育打卡脚本 2 | 3 | ## 简介 4 | 5 | 此脚本用于西安交通大学研究生体美劳中体育打卡系统的自动签到与签退。脚本模拟用户登录,获取认证令牌,并在指定时间间隔后完成签到和签退操作,支持邮件通知功能。 6 | 7 | --- 8 | 9 | ## 环境配置 10 | 11 | ### 依赖安装 12 | 13 | 运行前需安装以下Python库: 14 | 15 | ```bash 16 | pip install requests pycryptodome 17 | ``` 18 | 19 | --- 20 | 21 | ## 使用说明 22 | 23 | ### 1. 配置用户信息 24 | 25 | 打开脚本文件 sport_bot.py,修改以下字段: 26 | 27 | - **学号与密码** 28 | 找到 `Config` 类中的变量: 29 | ```python 30 | USER = "user" # 改为你的学号 31 | PASSWORD = "password" # 改为你的密码 32 | ``` 33 | 34 | - **位置坐标** 35 | 默认使用涵英楼北草坪坐标,可自行修改: 36 | ```python 37 | LONGTITUDE = 108.654387 # 经度 38 | LATITUDE = 34.257229 # 纬度 39 | ``` 40 | 41 | --- 42 | 43 | ### 2. 邮件通知配置(可选) 44 | 45 | 若需启用邮件通知,按以下步骤配置: 46 | 47 | 1. **开启QQ邮箱SMTP服务** 48 | - 登录QQ邮箱 → 设置 → 账户 → 开启“POP3/SMTP服务” 49 | - 生成16位SMTP授权码(`auth_code`) 50 | 51 | 2. **修改脚本配置** 52 | 在 `Config` 类中设置: 53 | ```python 54 | SEND_EMAIL = True # 设为True启用邮件通知 55 | SMTP_AUTH_CODE = "你的SMTP授权码" # 从QQ邮箱获取 56 | EMAIL_SENDER = "发件人QQ邮箱@qq.com" 57 | EMAIL_RECEIVER = "接收通知的邮箱" # 可以是同一邮箱 58 | ``` 59 | 60 | --- 61 | 62 | ## 运行脚本 63 | 64 | ### 方法1:直接执行脚本: 65 | ```bash 66 | python sport_bot.py 67 | ``` 68 | 69 | ### 方法2:使用定时任务: 70 | 可使用系统工具(如cron或任务计划程序)设置定时运行,实现全自动打卡。 71 | 72 | **在Linux系统中使用crontab定时运行脚本:** 73 | 1. 打开crontab编辑器: 74 | ```bash 75 | crontab -e 76 | ``` 77 | 2. 添加定时任务(每天中午12点运行): 78 | ```bash 79 | 0 12 * * * /usr/bin/python3 /path/to/sport_bot.py 80 | ``` 81 | 注意替换`/usr/bin/python3`为你的Python解释器路径,`/path/to/sport_bot.py`为脚本的绝对路径。 82 | 83 | 若打卡失败,请检查日志文件 `sport_bot.log` 或根据邮件提示排查错误。 84 | 85 | --- 86 | 87 | ## 免责声明 88 | 此脚本仅供学习和交流使用,请勿用于任何商业用途或违反学校规定的行为。使用此脚本所产生的任何后果由使用者自行承担,作者不对任何损失或损害负责。请尊重他人劳动成果,合理使用技术手段。 89 | -------------------------------------------------------------------------------- /sport_bot.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | import time 4 | import smtplib 5 | from email.mime.text import MIMEText 6 | from email.header import Header 7 | import json 8 | from urllib.parse import urlparse, parse_qs 9 | from Crypto.Cipher import AES 10 | from Crypto.Util.Padding import pad 11 | import base64 12 | import os 13 | 14 | 15 | # 配置信息 16 | class Config: 17 | # 用户信息 18 | USER = "student_id" # 学号 19 | PASSWORD = "password" # 密码 20 | 21 | # 涵英楼北草坪坐标 22 | LONGITUDE = 108.654387 # 经度 23 | LATITUDE = 34.257229 # 纬度 24 | 25 | # 邮件配置 26 | SEND_EMAIL = True # 设为True启用邮件通知 27 | SMTP_AUTH_CODE = "你的SMTP授权码" # 从QQ邮箱获取 28 | EMAIL_SENDER = "发件人QQ邮箱@qq.com" 29 | EMAIL_RECEIVER = "接收通知的邮箱" # 可以是同一邮箱 30 | 31 | # 加密公钥,无需修改 32 | AES_PUBLIC_KEY = "0725@pwdorgopenp" 33 | 34 | # 日志配置 35 | LOG_FILE = os.path.join(os.path.dirname(__file__), "sport_bot.log") 36 | LOG_LEVEL = logging.INFO 37 | 38 | 39 | # 初始化日志 40 | logging.basicConfig( 41 | filename=Config.LOG_FILE, 42 | level=Config.LOG_LEVEL, 43 | format="%(asctime)s - %(levelname)s - %(message)s", 44 | encoding="utf-8", 45 | ) 46 | 47 | # 前端界面的加密函数 48 | def aes_ecb_encrypt(pwd_val: str, public_key=Config.AES_PUBLIC_KEY) -> str: 49 | key = public_key.encode("utf-8") 50 | cipher = AES.new(key, AES.MODE_ECB) 51 | padded_data = pad(pwd_val.encode("utf-8"), AES.block_size) 52 | encrypted_data = cipher.encrypt(padded_data) 53 | return base64.b64encode(encrypted_data).decode("utf-8") 54 | 55 | 56 | def get_token(user, password): 57 | try: 58 | # Step 1: 获取初始Cookie 59 | response = requests.get( 60 | "https://org.xjtu.edu.cn/openplatform/oauth/authorize", 61 | headers={ 62 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 63 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", 64 | }, 65 | params={ 66 | "appId": "1740", 67 | "redirectUri": "https://ipahw.xjtu.edu.cn/sso/callback", 68 | "responseType": "code", 69 | "scope": "user_info", 70 | "state": "1234", 71 | }, 72 | allow_redirects=False, 73 | ) 74 | cookies = response.cookies.get_dict() 75 | 76 | # Step 2: 获取验证码Cookie 77 | response = requests.post( 78 | "https://org.xjtu.edu.cn/openplatform/g/admin/getJcaptchaCode", 79 | headers={ 80 | "Cookie": f"route={cookies['route']}; rdstate={cookies['rdstate']}; cur_appId_={cookies['cur_appId_']}; state={cookies['state']}", 81 | "Content-Type": "application/x-www-form-urlencoded", 82 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", 83 | }, 84 | ) 85 | cookies.update(response.cookies.get_dict()) 86 | 87 | # Step 3: 执行登录 88 | response = requests.post( 89 | "https://org.xjtu.edu.cn/openplatform/g/admin/login", 90 | headers={ 91 | "Cookie": f"route={cookies['route']}; rdstate={cookies['rdstate']}; cur_appId_={cookies['cur_appId_']}; state={cookies['state']}; JSESSIONID={cookies['JSESSIONID']}; sid_code={cookies['sid_code']}", 92 | "Content-Type": "application/json;charset=UTF-8", 93 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", 94 | }, 95 | data=json.dumps( 96 | { 97 | "loginType": 1, 98 | "username": user, 99 | "pwd": password, 100 | "jcaptchaCode": "", 101 | } 102 | ), 103 | ) 104 | response_json = response.json() 105 | if not response_json["message"] == "成功": 106 | raise Exception("统一身份认证登录失败") 107 | 108 | # Step 4-5: 获取最终token 109 | cookies["open_Platform_User"] = response_json["data"]["tokenKey"] 110 | response = requests.get( 111 | "https://org.xjtu.edu.cn/openplatform/oauth/auth/getRedirectUrl", 112 | params={ 113 | "userType": "1", 114 | "personNo": user, 115 | "_": str(int(time.time() * 1000)), 116 | }, 117 | headers={"Cookie": "; ".join([f"{k}={v}" for k, v in cookies.items()])}, 118 | ) 119 | oauth_code = parse_qs(urlparse(response.json()["data"]).query)["code"][0] 120 | 121 | response = requests.get( 122 | "https://ipahw.xjtu.edu.cn/szjy-boot/sso/codeLogin", 123 | params={"userType": "1", "code": oauth_code, "employeeNo": user}, 124 | headers={"Referer": response.json()["data"]}, 125 | ) 126 | return response.json()["data"]["token"] 127 | 128 | except Exception as e: 129 | logging.error(f"Token获取失败: {str(e)}") 130 | return None 131 | 132 | 133 | def sign_operation(url, payload, token, operation_name): 134 | try: 135 | response = requests.post( 136 | url, 137 | headers={ 138 | "Content-Type": "application/json", 139 | "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/537.36 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/537.36", 140 | "token": token, 141 | }, 142 | data=json.dumps(payload), 143 | ) 144 | logging.info(f"{operation_name}请求: {response.status_code}") 145 | 146 | if response.status_code == 200: 147 | response_data = response.json() 148 | if response_data.get("success"): 149 | logging.info(f"{operation_name}成功") 150 | return True 151 | logging.warning(f"{operation_name}失败: {response_data.get('msg')}") 152 | return False 153 | except Exception as e: 154 | logging.error(f"{operation_name}异常: {str(e)}") 155 | return False 156 | 157 | 158 | def send_email(content): 159 | try: 160 | msg = MIMEText(content, "plain", "utf-8") 161 | msg["From"] = Header(Config.EMAIL_SENDER) 162 | msg["To"] = Header(Config.EMAIL_RECEIVER) 163 | msg["Subject"] = Header("运动打卡通知", "utf-8") 164 | 165 | with smtplib.SMTP_SSL("smtp.qq.com", 465) as smtp: 166 | smtp.login(Config.EMAIL_SENDER, Config.SMTP_AUTH_CODE) 167 | smtp.sendmail(Config.EMAIL_SENDER, [Config.EMAIL_RECEIVER], msg.as_string()) 168 | smtp.quit() 169 | logging.info("邮件发送成功") 170 | except Exception as e: 171 | logging.error(f"邮件发送失败: {str(e)}") 172 | 173 | 174 | def main(): 175 | # 获取加密后的密码 176 | crypto_pwd = aes_ecb_encrypt(Config.PASSWORD) 177 | 178 | # 获取访问令牌 179 | token = get_token(Config.USER, crypto_pwd) 180 | if not token: 181 | logging.error("获取token失败,终止流程") 182 | if Config.SEND_EMAIL: 183 | send_email("获取token失败,请检查账号密码") 184 | return 185 | 186 | # 执行签到 187 | sign_in_success = sign_operation( 188 | "https://ipahw.xjtu.edu.cn/szjy-boot/api/v1/sportActa/signRun", 189 | { 190 | "sportType": 2, 191 | "longitude": Config.LONGITUDE, 192 | "latitude": Config.LATITUDE, 193 | "courseInfoId": "null", 194 | }, 195 | token, 196 | "签到", 197 | ) 198 | 199 | if sign_in_success: 200 | logging.info("等待31分钟后签退...") 201 | print("签到成功!请勿关闭程序,31分钟后自动签退...") 202 | time.sleep(31*60) # 31分钟 203 | 204 | sign_out_success = sign_operation( 205 | "https://ipahw.xjtu.edu.cn/szjy-boot/api/v1/sportActa/signOutTrain", 206 | {"longitude": Config.LONGITUDE, "latitude": Config.LATITUDE}, 207 | token, 208 | "签退", 209 | ) 210 | if sign_out_success: 211 | notice_msg = "打卡成功" 212 | print("已签退,本次打卡有效!") 213 | else: 214 | notice_msg = "签到成功但签退失败" 215 | print("签退失败,请手动签退") 216 | else: 217 | notice_msg = "签到失败" 218 | print("签到失败!请检查sport_bot.log文件排查错误") 219 | 220 | if Config.SEND_EMAIL: 221 | send_email(notice_msg) 222 | 223 | 224 | if __name__ == "__main__": 225 | main() 226 | --------------------------------------------------------------------------------