368 lines
13 KiB
Python
368 lines
13 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
超星系统 - 学生学籍异动信息API请求脚本
|
|
API路径: xjydsjpi_48018/list
|
|
"""
|
|
|
|
import hashlib
|
|
import base64
|
|
import json
|
|
import requests
|
|
import pymysql
|
|
import time
|
|
from typing import Dict, Any, Optional, List
|
|
from Crypto.Cipher import AES
|
|
from Crypto.Util.Padding import unpad
|
|
from db_config import DB_CONFIG, TABLE_MAPPING
|
|
|
|
|
|
class ChaoxingStudentChangeAPI:
|
|
"""超星学生学籍异动信息API请求类"""
|
|
|
|
def __init__(self, app_id: str, app_key: str, fid: str = "19628"):
|
|
"""
|
|
初始化API客户端
|
|
|
|
Args:
|
|
app_id: API的appId
|
|
app_key: API的appKey
|
|
fid: 单位ID,默认19628
|
|
"""
|
|
self.app_id = app_id
|
|
self.app_key = app_key
|
|
self.fid = fid
|
|
self.base_url = "https://appswh.chaoxing.com/biapi/api"
|
|
self.api_path = "xjydsjpi_48018"
|
|
# 生成AES解密密钥:appId去除'-'后取前16位
|
|
self.aes_key = app_id.replace("-", "")[:16].encode('utf-8')
|
|
|
|
def generate_enc(self, page_num: int, page_size: int) -> str:
|
|
"""
|
|
生成enc加密验证串
|
|
|
|
enc加密方式:32位大写的MD5字符串
|
|
格式:[apiPath=xjydsjpi_48018#$pageNum=页码#$pageSize=单页条数#$fid=19628#$appKey=授权appKey]
|
|
|
|
Args:
|
|
page_num: 页码
|
|
page_size: 单页条数
|
|
|
|
Returns:
|
|
32位大写MD5字符串
|
|
"""
|
|
# 构建加密字符串
|
|
enc_string = f"[apiPath={self.api_path}#$pageNum={page_num}#$pageSize={page_size}#$fid={self.fid}#$appKey={self.app_key}]"
|
|
|
|
# MD5加密并转为大写
|
|
md5_hash = hashlib.md5(enc_string.encode('utf-8')).hexdigest().upper()
|
|
|
|
return md5_hash
|
|
|
|
def decrypt_data(self, ciphertext: str) -> Any:
|
|
"""
|
|
AES解密data字段
|
|
|
|
解密算法:AES/ECB/PKCS5Padding
|
|
密钥:appId去除'-'后前16位字符
|
|
|
|
Args:
|
|
ciphertext: Base64编码的密文
|
|
|
|
Returns:
|
|
解密后的数据(可能是JSON字符串,需要进一步解析)
|
|
"""
|
|
try:
|
|
# Base64解码
|
|
encrypted_bytes = base64.b64decode(ciphertext)
|
|
|
|
# AES解密
|
|
cipher = AES.new(self.aes_key, AES.MODE_ECB)
|
|
decrypted_bytes = cipher.decrypt(encrypted_bytes)
|
|
|
|
# PKCS5Padding去填充
|
|
decrypted_bytes = unpad(decrypted_bytes, AES.block_size)
|
|
|
|
# 转为字符串
|
|
decrypted_text = decrypted_bytes.decode('utf-8')
|
|
|
|
# 尝试解析为JSON
|
|
try:
|
|
return json.loads(decrypted_text)
|
|
except json.JSONDecodeError:
|
|
# 如果不是JSON,直接返回字符串
|
|
return decrypted_text
|
|
|
|
except Exception as e:
|
|
print(f"解密失败: {e}")
|
|
return None
|
|
|
|
def get_student_change_list(self, page_num: int = 1, page_size: int = 100) -> Dict[str, Any]:
|
|
"""
|
|
获取学生学籍异动信息列表
|
|
|
|
Args:
|
|
page_num: 页码,默认1
|
|
page_size: 页条数,默认100,最大1000
|
|
|
|
Returns:
|
|
API响应结果
|
|
"""
|
|
# 验证page_size
|
|
if page_size > 1000:
|
|
page_size = 1000
|
|
print("警告: pageSize超过最大限制1000,已自动调整为1000")
|
|
|
|
# 生成enc加密串
|
|
enc = self.generate_enc(page_num, page_size)
|
|
|
|
# 构建请求URL
|
|
url = f"{self.base_url}/{self.api_path}/list"
|
|
|
|
# 构建请求参数
|
|
params = {
|
|
"pageNum": page_num,
|
|
"pageSize": page_size,
|
|
"fid": self.fid,
|
|
"enc": enc
|
|
}
|
|
|
|
# 构建请求头(appId放在请求头中)
|
|
headers = {
|
|
"appId": self.app_id,
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
}
|
|
|
|
try:
|
|
# 发送GET请求
|
|
print(f"正在请求: {url}")
|
|
print(f"参数: {params}")
|
|
print(f"请求头appId: {self.app_id}")
|
|
|
|
response = requests.get(url, params=params, headers=headers, timeout=30)
|
|
|
|
# 检查HTTP状态码
|
|
response.raise_for_status()
|
|
|
|
# 解析JSON响应
|
|
result = response.json()
|
|
|
|
# 解密data字段
|
|
if result.get("data") and isinstance(result.get("data"), str):
|
|
decrypted_data = self.decrypt_data(result["data"])
|
|
if decrypted_data is not None:
|
|
result["data"] = decrypted_data
|
|
else:
|
|
print("警告: data字段解密失败,保持原始值")
|
|
|
|
return result
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
print(f"请求失败: {e}")
|
|
return {
|
|
"code": -1,
|
|
"message": f"请求异常: {str(e)}",
|
|
"data": None
|
|
}
|
|
except ValueError as e:
|
|
print(f"JSON解析失败: {e}")
|
|
return {
|
|
"code": -1,
|
|
"message": f"响应解析失败: {str(e)}",
|
|
"data": None
|
|
}
|
|
|
|
def get_all_student_changes(self, max_page_size: int = 500) -> list:
|
|
"""
|
|
获取所有学生学籍异动信息(自动分页)
|
|
|
|
Args:
|
|
max_page_size: 每页最大条数,默认500
|
|
|
|
Returns:
|
|
所有学籍异动信息列表
|
|
"""
|
|
all_data = []
|
|
page_num = 1
|
|
total_pages = None # 总页数,从第一页响应中获取
|
|
|
|
while True:
|
|
print(f"\n正在获取第 {page_num} 页数据...")
|
|
result = self.get_student_change_list(page_num, max_page_size)
|
|
|
|
# 超星API成功时code=1,失败时code!=1
|
|
if result.get("code") != 1:
|
|
print(f"获取第 {page_num} 页失败: {result.get('message', '未知错误')}")
|
|
break
|
|
|
|
# 获取分页信息(从extendData中)
|
|
extend_data = result.get("extendData", {})
|
|
if total_pages is None and extend_data:
|
|
total_pages = int(extend_data.get("pages", 0))
|
|
total_count = int(extend_data.get("count", 0))
|
|
print(f"总页数: {total_pages}, 总条数: {total_count}")
|
|
|
|
data = result.get("data")
|
|
if not data:
|
|
print("没有更多数据")
|
|
break
|
|
|
|
# 如果是列表,直接添加
|
|
if isinstance(data, list):
|
|
all_data.extend(data)
|
|
print(f"第 {page_num} 页获取 {len(data)} 条数据,累计 {len(all_data)} 条")
|
|
|
|
# 如果知道总页数,检查是否已获取完所有页
|
|
if total_pages and page_num >= total_pages:
|
|
break
|
|
# 如果返回的数据少于page_size,说明已经是最后一页
|
|
if len(data) < max_page_size:
|
|
break
|
|
# 如果是字典,可能包含分页信息
|
|
elif isinstance(data, dict):
|
|
items = data.get("list", data.get("records", []))
|
|
if items:
|
|
all_data.extend(items)
|
|
print(f"第 {page_num} 页获取 {len(items)} 条数据,累计 {len(all_data)} 条")
|
|
# 检查是否还有下一页
|
|
total = data.get("total", 0)
|
|
if total and len(all_data) >= total:
|
|
break
|
|
else:
|
|
break
|
|
else:
|
|
break
|
|
|
|
page_num += 1
|
|
|
|
print(f"\n总共获取 {len(all_data)} 条学籍异动信息")
|
|
return all_data
|
|
|
|
def clear_table(self) -> bool:
|
|
"""
|
|
清空学籍异动数据表
|
|
|
|
Returns:
|
|
是否成功
|
|
"""
|
|
try:
|
|
connection = pymysql.connect(**DB_CONFIG)
|
|
try:
|
|
with connection.cursor() as cursor:
|
|
table_name = TABLE_MAPPING["student_change"]
|
|
cursor.execute(f"TRUNCATE TABLE `{table_name}`")
|
|
connection.commit()
|
|
print(f"✓ 已清空表 {table_name}")
|
|
return True
|
|
finally:
|
|
connection.close()
|
|
except Exception as e:
|
|
print(f"✗ 清空表失败: {e}")
|
|
return False
|
|
|
|
def save_to_database(self, data_list: List[Dict]) -> int:
|
|
"""
|
|
将学籍异动数据保存到数据库
|
|
|
|
Args:
|
|
data_list: 学籍异动数据列表
|
|
|
|
Returns:
|
|
成功插入的记录数
|
|
"""
|
|
if not data_list:
|
|
print("数据列表为空,跳过保存")
|
|
return 0
|
|
|
|
try:
|
|
connection = pymysql.connect(**DB_CONFIG)
|
|
try:
|
|
current_time = int(time.time() * 1000) # 毫秒时间戳
|
|
table_name = TABLE_MAPPING["student_change"]
|
|
|
|
with connection.cursor() as cursor:
|
|
# 构建插入SQL
|
|
insert_sql = f"""
|
|
INSERT INTO `{table_name}` (
|
|
`inserttime`, `updatetime`,
|
|
`xjyd_yjsqsj`, `xjyd_ydsxxq`, `xjydsp_ydlx`, `xjydsp_sqrxh`,
|
|
`xjydsp_xm`, `xjydsp_ydqssxq`, `xjydsp_ydqsznj`, `xjydsp_ydqszyx`,
|
|
`xjydsp_ydqszzy`, `xjydsp_ydqszbj`, `xjydsp_ydqxz`, `xjydsp_ydhszbj`,
|
|
`xjydsp_ydhsznj`, `xjydsp_ydhszyx`, `xjydsp_ydhszzy`, `xjydsp_ydhssxq`,
|
|
`xjydsp_ydhxz`, `xjydsp_ydhbzrgh`, `xjyd_name`, `xjydsp_sqyy`,
|
|
`xjydsp_ydhbzr`, `xjydsp_sxqm`, `xjydsp_zmfj`, `xjyd_shzt`,
|
|
`xjydsp_ydhyxbh`, `xjydsp_ydhzybh`, `xjydsp_ydhbjbh`
|
|
) VALUES (
|
|
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
|
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
|
|
%s, %s, %s, %s, %s, %s, %s, %s, %s
|
|
)
|
|
"""
|
|
|
|
success_count = 0
|
|
for item in data_list:
|
|
try:
|
|
cursor.execute(insert_sql, (
|
|
current_time, current_time, # inserttime, updatetime
|
|
item.get('xjyd_yjsqsj'), item.get('xjyd_ydsxxq'), item.get('xjydsp_ydlx'), item.get('xjydsp_sqrxh'),
|
|
item.get('xjydsp_xm'), item.get('xjydsp_ydqssxq'), item.get('xjydsp_ydqsznj'), item.get('xjydsp_ydqszyx'),
|
|
item.get('xjydsp_ydqszzy'), item.get('xjydsp_ydqszbj'), item.get('xjydsp_ydqxz'), item.get('xjydsp_ydhszbj'),
|
|
item.get('xjydsp_ydhsznj'), item.get('xjydsp_ydhszyx'), item.get('xjydsp_ydhszzy'), item.get('xjydsp_ydhssxq'),
|
|
item.get('xjydsp_ydhxz'), item.get('xjydsp_ydhbzrgh'), item.get('xjyd_name'), item.get('xjydsp_sqyy'),
|
|
item.get('xjydsp_ydhbzr'), item.get('xjydsp_sxqm'), item.get('xjydsp_zmfj'), item.get('xjyd_shzt'),
|
|
item.get('xjydsp_ydhyxbh'), item.get('xjydsp_ydhzybh'), item.get('xjydsp_ydhbjbh')
|
|
))
|
|
success_count += 1
|
|
except Exception as e:
|
|
print(f"插入单条数据失败: {e}, 数据: {item}")
|
|
|
|
connection.commit()
|
|
print(f"✓ 成功插入 {success_count}/{len(data_list)} 条数据到表 {table_name}")
|
|
return success_count
|
|
finally:
|
|
connection.close()
|
|
except Exception as e:
|
|
print(f"✗ 保存数据到数据库失败: {e}")
|
|
return 0
|
|
|
|
|
|
def main():
|
|
"""主函数 - 示例用法"""
|
|
# API配置信息
|
|
APP_ID = "4083b823-cc45-4b4b-8eb4-a789f791b23c"
|
|
APP_KEY = "dyjs_xjyd"
|
|
FID = "19628" # 单位ID
|
|
|
|
# 创建API客户端
|
|
api = ChaoxingStudentChangeAPI(app_id=APP_ID, app_key=APP_KEY, fid=FID)
|
|
|
|
# 获取所有数据并保存到数据库
|
|
print("=" * 60)
|
|
print("获取所有学籍异动信息并保存到数据库")
|
|
print("=" * 60)
|
|
|
|
# 步骤1: 清空表
|
|
print("\n步骤1: 清空学籍异动数据表...")
|
|
api.clear_table()
|
|
|
|
# 步骤2: 获取所有数据
|
|
print("\n步骤2: 获取所有学籍异动信息(自动分页)...")
|
|
all_changes = api.get_all_student_changes(max_page_size=500)
|
|
|
|
# 步骤3: 保存到数据库
|
|
if all_changes:
|
|
print(f"\n步骤3: 保存数据到数据库...")
|
|
saved_count = api.save_to_database(all_changes)
|
|
print(f"\n✓ 完成!共获取 {len(all_changes)} 条数据,成功保存 {saved_count} 条")
|
|
|
|
print(f"\n前3条数据示例:")
|
|
for i, change in enumerate(all_changes[:3], 1):
|
|
print(f"{i}. {change}")
|
|
else:
|
|
print("\n✗ 未获取到数据")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|