请求学籍异动信息API.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. 超星系统 - 学生学籍异动信息API请求脚本
  5. API路径: xjydsjpi_48018/list
  6. """
  7. import hashlib
  8. import base64
  9. import json
  10. import requests
  11. import pymysql
  12. import time
  13. from typing import Dict, Any, Optional, List
  14. from Crypto.Cipher import AES
  15. from Crypto.Util.Padding import unpad
  16. from db_config import DB_CONFIG, TABLE_MAPPING
  17. class ChaoxingStudentChangeAPI:
  18. """超星学生学籍异动信息API请求类"""
  19. def __init__(self, app_id: str, app_key: str, fid: str = "19628"):
  20. """
  21. 初始化API客户端
  22. Args:
  23. app_id: API的appId
  24. app_key: API的appKey
  25. fid: 单位ID,默认19628
  26. """
  27. self.app_id = app_id
  28. self.app_key = app_key
  29. self.fid = fid
  30. self.base_url = "https://appswh.chaoxing.com/biapi/api"
  31. self.api_path = "xjydsjpi_48018"
  32. # 生成AES解密密钥:appId去除'-'后取前16位
  33. self.aes_key = app_id.replace("-", "")[:16].encode('utf-8')
  34. def generate_enc(self, page_num: int, page_size: int) -> str:
  35. """
  36. 生成enc加密验证串
  37. enc加密方式:32位大写的MD5字符串
  38. 格式:[apiPath=xjydsjpi_48018#$pageNum=页码#$pageSize=单页条数#$fid=19628#$appKey=授权appKey]
  39. Args:
  40. page_num: 页码
  41. page_size: 单页条数
  42. Returns:
  43. 32位大写MD5字符串
  44. """
  45. # 构建加密字符串
  46. enc_string = f"[apiPath={self.api_path}#$pageNum={page_num}#$pageSize={page_size}#$fid={self.fid}#$appKey={self.app_key}]"
  47. # MD5加密并转为大写
  48. md5_hash = hashlib.md5(enc_string.encode('utf-8')).hexdigest().upper()
  49. return md5_hash
  50. def decrypt_data(self, ciphertext: str) -> Any:
  51. """
  52. AES解密data字段
  53. 解密算法:AES/ECB/PKCS5Padding
  54. 密钥:appId去除'-'后前16位字符
  55. Args:
  56. ciphertext: Base64编码的密文
  57. Returns:
  58. 解密后的数据(可能是JSON字符串,需要进一步解析)
  59. """
  60. try:
  61. # Base64解码
  62. encrypted_bytes = base64.b64decode(ciphertext)
  63. # AES解密
  64. cipher = AES.new(self.aes_key, AES.MODE_ECB)
  65. decrypted_bytes = cipher.decrypt(encrypted_bytes)
  66. # PKCS5Padding去填充
  67. decrypted_bytes = unpad(decrypted_bytes, AES.block_size)
  68. # 转为字符串
  69. decrypted_text = decrypted_bytes.decode('utf-8')
  70. # 尝试解析为JSON
  71. try:
  72. return json.loads(decrypted_text)
  73. except json.JSONDecodeError:
  74. # 如果不是JSON,直接返回字符串
  75. return decrypted_text
  76. except Exception as e:
  77. print(f"解密失败: {e}")
  78. return None
  79. def get_student_change_list(self, page_num: int = 1, page_size: int = 100) -> Dict[str, Any]:
  80. """
  81. 获取学生学籍异动信息列表
  82. Args:
  83. page_num: 页码,默认1
  84. page_size: 页条数,默认100,最大1000
  85. Returns:
  86. API响应结果
  87. """
  88. # 验证page_size
  89. if page_size > 1000:
  90. page_size = 1000
  91. print("警告: pageSize超过最大限制1000,已自动调整为1000")
  92. # 生成enc加密串
  93. enc = self.generate_enc(page_num, page_size)
  94. # 构建请求URL
  95. url = f"{self.base_url}/{self.api_path}/list"
  96. # 构建请求参数
  97. params = {
  98. "pageNum": page_num,
  99. "pageSize": page_size,
  100. "fid": self.fid,
  101. "enc": enc
  102. }
  103. # 构建请求头(appId放在请求头中)
  104. headers = {
  105. "appId": self.app_id,
  106. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  107. }
  108. try:
  109. # 发送GET请求
  110. print(f"正在请求: {url}")
  111. print(f"参数: {params}")
  112. print(f"请求头appId: {self.app_id}")
  113. response = requests.get(url, params=params, headers=headers, timeout=30)
  114. # 检查HTTP状态码
  115. response.raise_for_status()
  116. # 解析JSON响应
  117. result = response.json()
  118. # 解密data字段
  119. if result.get("data") and isinstance(result.get("data"), str):
  120. decrypted_data = self.decrypt_data(result["data"])
  121. if decrypted_data is not None:
  122. result["data"] = decrypted_data
  123. else:
  124. print("警告: data字段解密失败,保持原始值")
  125. return result
  126. except requests.exceptions.RequestException as e:
  127. print(f"请求失败: {e}")
  128. return {
  129. "code": -1,
  130. "message": f"请求异常: {str(e)}",
  131. "data": None
  132. }
  133. except ValueError as e:
  134. print(f"JSON解析失败: {e}")
  135. return {
  136. "code": -1,
  137. "message": f"响应解析失败: {str(e)}",
  138. "data": None
  139. }
  140. def get_all_student_changes(self, max_page_size: int = 500) -> list:
  141. """
  142. 获取所有学生学籍异动信息(自动分页)
  143. Args:
  144. max_page_size: 每页最大条数,默认500
  145. Returns:
  146. 所有学籍异动信息列表
  147. """
  148. all_data = []
  149. page_num = 1
  150. total_pages = None # 总页数,从第一页响应中获取
  151. while True:
  152. print(f"\n正在获取第 {page_num} 页数据...")
  153. result = self.get_student_change_list(page_num, max_page_size)
  154. # 超星API成功时code=1,失败时code!=1
  155. if result.get("code") != 1:
  156. print(f"获取第 {page_num} 页失败: {result.get('message', '未知错误')}")
  157. break
  158. # 获取分页信息(从extendData中)
  159. extend_data = result.get("extendData", {})
  160. if total_pages is None and extend_data:
  161. total_pages = int(extend_data.get("pages", 0))
  162. total_count = int(extend_data.get("count", 0))
  163. print(f"总页数: {total_pages}, 总条数: {total_count}")
  164. data = result.get("data")
  165. if not data:
  166. print("没有更多数据")
  167. break
  168. # 如果是列表,直接添加
  169. if isinstance(data, list):
  170. all_data.extend(data)
  171. print(f"第 {page_num} 页获取 {len(data)} 条数据,累计 {len(all_data)} 条")
  172. # 如果知道总页数,检查是否已获取完所有页
  173. if total_pages and page_num >= total_pages:
  174. break
  175. # 如果返回的数据少于page_size,说明已经是最后一页
  176. if len(data) < max_page_size:
  177. break
  178. # 如果是字典,可能包含分页信息
  179. elif isinstance(data, dict):
  180. items = data.get("list", data.get("records", []))
  181. if items:
  182. all_data.extend(items)
  183. print(f"第 {page_num} 页获取 {len(items)} 条数据,累计 {len(all_data)} 条")
  184. # 检查是否还有下一页
  185. total = data.get("total", 0)
  186. if total and len(all_data) >= total:
  187. break
  188. else:
  189. break
  190. else:
  191. break
  192. page_num += 1
  193. print(f"\n总共获取 {len(all_data)} 条学籍异动信息")
  194. return all_data
  195. def clear_table(self) -> bool:
  196. """
  197. 清空学籍异动数据表
  198. Returns:
  199. 是否成功
  200. """
  201. try:
  202. connection = pymysql.connect(**DB_CONFIG)
  203. try:
  204. with connection.cursor() as cursor:
  205. table_name = TABLE_MAPPING["student_change"]
  206. cursor.execute(f"TRUNCATE TABLE `{table_name}`")
  207. connection.commit()
  208. print(f"✓ 已清空表 {table_name}")
  209. return True
  210. finally:
  211. connection.close()
  212. except Exception as e:
  213. print(f"✗ 清空表失败: {e}")
  214. return False
  215. def save_to_database(self, data_list: List[Dict]) -> int:
  216. """
  217. 将学籍异动数据保存到数据库
  218. Args:
  219. data_list: 学籍异动数据列表
  220. Returns:
  221. 成功插入的记录数
  222. """
  223. if not data_list:
  224. print("数据列表为空,跳过保存")
  225. return 0
  226. try:
  227. connection = pymysql.connect(**DB_CONFIG)
  228. try:
  229. current_time = int(time.time() * 1000) # 毫秒时间戳
  230. table_name = TABLE_MAPPING["student_change"]
  231. with connection.cursor() as cursor:
  232. # 构建插入SQL
  233. insert_sql = f"""
  234. INSERT INTO `{table_name}` (
  235. `inserttime`, `updatetime`,
  236. `xjyd_yjsqsj`, `xjyd_ydsxxq`, `xjydsp_ydlx`, `xjydsp_sqrxh`,
  237. `xjydsp_xm`, `xjydsp_ydqssxq`, `xjydsp_ydqsznj`, `xjydsp_ydqszyx`,
  238. `xjydsp_ydqszzy`, `xjydsp_ydqszbj`, `xjydsp_ydqxz`, `xjydsp_ydhszbj`,
  239. `xjydsp_ydhsznj`, `xjydsp_ydhszyx`, `xjydsp_ydhszzy`, `xjydsp_ydhssxq`,
  240. `xjydsp_ydhxz`, `xjydsp_ydhbzrgh`, `xjyd_name`, `xjydsp_sqyy`,
  241. `xjydsp_ydhbzr`, `xjydsp_sxqm`, `xjydsp_zmfj`, `xjyd_shzt`,
  242. `xjydsp_ydhyxbh`, `xjydsp_ydhzybh`, `xjydsp_ydhbjbh`
  243. ) VALUES (
  244. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  245. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  246. %s, %s, %s, %s, %s, %s, %s, %s, %s
  247. )
  248. """
  249. success_count = 0
  250. for item in data_list:
  251. try:
  252. cursor.execute(insert_sql, (
  253. current_time, current_time, # inserttime, updatetime
  254. item.get('xjyd_yjsqsj'), item.get('xjyd_ydsxxq'), item.get('xjydsp_ydlx'), item.get('xjydsp_sqrxh'),
  255. item.get('xjydsp_xm'), item.get('xjydsp_ydqssxq'), item.get('xjydsp_ydqsznj'), item.get('xjydsp_ydqszyx'),
  256. item.get('xjydsp_ydqszzy'), item.get('xjydsp_ydqszbj'), item.get('xjydsp_ydqxz'), item.get('xjydsp_ydhszbj'),
  257. item.get('xjydsp_ydhsznj'), item.get('xjydsp_ydhszyx'), item.get('xjydsp_ydhszzy'), item.get('xjydsp_ydhssxq'),
  258. item.get('xjydsp_ydhxz'), item.get('xjydsp_ydhbzrgh'), item.get('xjyd_name'), item.get('xjydsp_sqyy'),
  259. item.get('xjydsp_ydhbzr'), item.get('xjydsp_sxqm'), item.get('xjydsp_zmfj'), item.get('xjyd_shzt'),
  260. item.get('xjydsp_ydhyxbh'), item.get('xjydsp_ydhzybh'), item.get('xjydsp_ydhbjbh')
  261. ))
  262. success_count += 1
  263. except Exception as e:
  264. print(f"插入单条数据失败: {e}, 数据: {item}")
  265. connection.commit()
  266. print(f"✓ 成功插入 {success_count}/{len(data_list)} 条数据到表 {table_name}")
  267. return success_count
  268. finally:
  269. connection.close()
  270. except Exception as e:
  271. print(f"✗ 保存数据到数据库失败: {e}")
  272. return 0
  273. def main():
  274. """主函数 - 示例用法"""
  275. # API配置信息
  276. APP_ID = "4083b823-cc45-4b4b-8eb4-a789f791b23c"
  277. APP_KEY = "dyjs_xjyd"
  278. FID = "19628" # 单位ID
  279. # 创建API客户端
  280. api = ChaoxingStudentChangeAPI(app_id=APP_ID, app_key=APP_KEY, fid=FID)
  281. # 获取所有数据并保存到数据库
  282. print("=" * 60)
  283. print("获取所有学籍异动信息并保存到数据库")
  284. print("=" * 60)
  285. # 步骤1: 清空表
  286. print("\n步骤1: 清空学籍异动数据表...")
  287. api.clear_table()
  288. # 步骤2: 获取所有数据
  289. print("\n步骤2: 获取所有学籍异动信息(自动分页)...")
  290. all_changes = api.get_all_student_changes(max_page_size=500)
  291. # 步骤3: 保存到数据库
  292. if all_changes:
  293. print(f"\n步骤3: 保存数据到数据库...")
  294. saved_count = api.save_to_database(all_changes)
  295. print(f"\n✓ 完成!共获取 {len(all_changes)} 条数据,成功保存 {saved_count} 条")
  296. print(f"\n前3条数据示例:")
  297. for i, change in enumerate(all_changes[:3], 1):
  298. print(f"{i}. {change}")
  299. else:
  300. print("\n✗ 未获取到数据")
  301. if __name__ == "__main__":
  302. main()