请求学生信息API.py 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """
  4. 超星系统 - 学生基本信息API请求脚本
  5. API路径: xsjbxxpi_66247/list
  6. """
  7. import hashlib
  8. import base64
  9. import json
  10. import time
  11. from datetime import datetime
  12. from typing import Dict, Any, Optional, List, Tuple
  13. import bcrypt
  14. import pymysql
  15. import pymysql.cursors
  16. from pymysql.cursors import DictCursor
  17. import requests
  18. from Crypto.Cipher import AES
  19. from Crypto.Util.Padding import unpad
  20. from db_config import DB_CONFIG, TABLE_MAPPING
  21. class ChaoxingStudentAPI:
  22. """超星学生基本信息API请求类"""
  23. DEFAULT_TENANT_ID = "000000"
  24. DEFAULT_CREATE_BY = 1
  25. DEFAULT_UPDATE_BY = 1
  26. DEFAULT_ACCOUNT_TYPE = "2"
  27. DEFAULT_USER_TYPE = "sys_user"
  28. DEFAULT_PASSWORD = "Hzdyjs123456!@#"
  29. STUDENT_ROLE_ID = 1938120227459309569
  30. SYS_USER_REMARK = "学生信息系统自动创建(超星同步)"
  31. SYS_USER_STATUS = "0"
  32. SYS_USER_DEL_FLAG = "0"
  33. QUERY_BATCH_SIZE = 1000
  34. def __init__(self, app_id: str, app_key: str, fid: str = "19628"):
  35. """
  36. 初始化API客户端
  37. Args:
  38. app_id: API的appId
  39. app_key: API的appKey
  40. fid: 单位ID,默认19628
  41. """
  42. self.app_id = app_id
  43. self.app_key = app_key
  44. self.fid = fid
  45. self.base_url = "https://appswh.chaoxing.com/biapi/api"
  46. self.api_path = "xsjbxxpi_66247"
  47. # 生成AES解密密钥:appId去除'-'后取前16位
  48. self.aes_key = app_id.replace("-", "")[:16].encode('utf-8')
  49. # 数据库连接对象
  50. self._db_connection = None
  51. self._last_ping_time = 0
  52. self._ping_interval = 60 # 每60秒ping一次数据库
  53. def generate_enc(self, page_num: int, page_size: int) -> str:
  54. """
  55. 生成enc加密验证串
  56. enc加密方式:32位大写的MD5字符串
  57. 格式:[apiPath=xsjbxxpi_66247#$pageNum=页码#$pageSize=单页条数#$fid=19628#$appKey=授权appKey]
  58. Args:
  59. page_num: 页码
  60. page_size: 单页条数
  61. Returns:
  62. 32位大写MD5字符串
  63. """
  64. # 构建加密字符串
  65. enc_string = f"[apiPath={self.api_path}#$pageNum={page_num}#$pageSize={page_size}#$fid={self.fid}#$appKey={self.app_key}]"
  66. # MD5加密并转为大写
  67. md5_hash = hashlib.md5(enc_string.encode('utf-8')).hexdigest().upper()
  68. return md5_hash
  69. def decrypt_data(self, ciphertext: str) -> Any:
  70. """
  71. AES解密data字段
  72. 解密算法:AES/ECB/PKCS5Padding
  73. 密钥:appId去除'-'后前16位字符
  74. Args:
  75. ciphertext: Base64编码的密文
  76. Returns:
  77. 解密后的数据(可能是JSON字符串,需要进一步解析)
  78. """
  79. try:
  80. # Base64解码
  81. encrypted_bytes = base64.b64decode(ciphertext)
  82. # AES解密
  83. cipher = AES.new(self.aes_key, AES.MODE_ECB)
  84. decrypted_bytes = cipher.decrypt(encrypted_bytes)
  85. # PKCS5Padding去填充
  86. decrypted_bytes = unpad(decrypted_bytes, AES.block_size)
  87. # 转为字符串
  88. decrypted_text = decrypted_bytes.decode('utf-8')
  89. # 尝试解析为JSON
  90. try:
  91. return json.loads(decrypted_text)
  92. except json.JSONDecodeError:
  93. # 如果不是JSON,直接返回字符串
  94. return decrypted_text
  95. except Exception as e:
  96. print(f"解密失败: {e}")
  97. return None
  98. def get_student_list(self, page_num: int = 1, page_size: int = 100) -> Dict[str, Any]:
  99. """
  100. 获取学生基本信息列表
  101. Args:
  102. page_num: 页码,默认1
  103. page_size: 页条数,默认100,最大1000
  104. Returns:
  105. API响应结果
  106. """
  107. # 验证page_size
  108. if page_size > 1000:
  109. page_size = 1000
  110. print("警告: pageSize超过最大限制1000,已自动调整为1000")
  111. # 生成enc加密串
  112. enc = self.generate_enc(page_num, page_size)
  113. # 构建请求URL
  114. url = f"{self.base_url}/{self.api_path}/list"
  115. # 构建请求参数
  116. params = {
  117. "pageNum": page_num,
  118. "pageSize": page_size,
  119. "fid": self.fid,
  120. "enc": enc
  121. }
  122. # 构建请求头
  123. headers = {
  124. "appId": self.app_id,
  125. "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  126. }
  127. try:
  128. # 发送GET请求
  129. print(f"正在请求: {url}")
  130. print(f"参数: {params}")
  131. print(f"请求头appId: {self.app_id}")
  132. response = requests.get(url, params=params, headers=headers, timeout=30)
  133. # 检查HTTP状态码
  134. response.raise_for_status()
  135. # 解析JSON响应
  136. result = response.json()
  137. # 解密data字段
  138. if result.get("data") and isinstance(result.get("data"), str):
  139. decrypted_data = self.decrypt_data(result["data"])
  140. if decrypted_data is not None:
  141. result["data"] = decrypted_data
  142. else:
  143. print("警告: data字段解密失败,保持原始值")
  144. return result
  145. except requests.exceptions.RequestException as e:
  146. print(f"请求失败: {e}")
  147. return {
  148. "code": -1,
  149. "message": f"请求异常: {str(e)}",
  150. "data": None
  151. }
  152. except ValueError as e:
  153. print(f"JSON解析失败: {e}")
  154. return {
  155. "code": -1,
  156. "message": f"响应解析失败: {str(e)}",
  157. "data": None
  158. }
  159. def get_all_students(self, max_page_size: int = 500) -> list:
  160. """
  161. 获取所有学生基本信息(自动分页)
  162. Args:
  163. max_page_size: 每页最大条数,默认500
  164. Returns:
  165. 所有学生信息列表
  166. """
  167. all_data = []
  168. page_num = 1
  169. total_pages = None # 总页数,从第一页响应中获取
  170. while True:
  171. print(f"\n正在获取第 {page_num} 页数据...")
  172. result = self.get_student_list(page_num, max_page_size)
  173. # 超星API成功时code=1,失败时code!=1
  174. if result.get("code") != 1:
  175. print(f"获取第 {page_num} 页失败: {result.get('message', '未知错误')}")
  176. break
  177. # 获取分页信息(从extendData中)
  178. extend_data = result.get("extendData", {})
  179. if total_pages is None and extend_data:
  180. total_pages = int(extend_data.get("pages", 0))
  181. total_count = int(extend_data.get("count", 0))
  182. print(f"总页数: {total_pages}, 总条数: {total_count}")
  183. data = result.get("data")
  184. if not data:
  185. print("没有更多数据")
  186. break
  187. # 如果是列表,直接添加
  188. if isinstance(data, list):
  189. all_data.extend(data)
  190. print(f"第 {page_num} 页获取 {len(data)} 条数据,累计 {len(all_data)} 条")
  191. # 如果知道总页数,检查是否已获取完所有页
  192. if total_pages and page_num >= total_pages:
  193. break
  194. # 如果返回的数据少于page_size,说明已经是最后一页
  195. if len(data) < max_page_size:
  196. break
  197. # 如果是字典,可能包含分页信息
  198. elif isinstance(data, dict):
  199. items = data.get("list", data.get("records", []))
  200. if items:
  201. all_data.extend(items)
  202. print(f"第 {page_num} 页获取 {len(items)} 条数据,累计 {len(all_data)} 条")
  203. # 检查是否还有下一页
  204. total = data.get("total", 0)
  205. if total and len(all_data) >= total:
  206. break
  207. else:
  208. break
  209. else:
  210. break
  211. page_num += 1
  212. print(f"\n总共获取 {len(all_data)} 条学生信息")
  213. return all_data
  214. def _get_db_connection(self, force_new: bool = False):
  215. """
  216. 获取数据库连接,自动处理重连
  217. Args:
  218. force_new: 是否强制创建新连接
  219. Returns:
  220. 数据库连接对象
  221. """
  222. if force_new and self._db_connection:
  223. try:
  224. self._db_connection.close()
  225. except:
  226. pass
  227. self._db_connection = None
  228. if self._db_connection is None:
  229. print("创建新的数据库连接...")
  230. self._db_connection = pymysql.connect(
  231. **DB_CONFIG,
  232. cursorclass=DictCursor,
  233. autocommit=False,
  234. connect_timeout=30,
  235. read_timeout=60,
  236. write_timeout=60
  237. )
  238. self._last_ping_time = time.time()
  239. print("✓ 数据库连接已建立")
  240. else:
  241. # 检查连接是否需要ping
  242. current_time = time.time()
  243. if current_time - self._last_ping_time > self._ping_interval:
  244. try:
  245. self._db_connection.ping(reconnect=True)
  246. self._last_ping_time = current_time
  247. except Exception as e:
  248. print(f"连接ping失败,重新建立连接: {e}")
  249. self._db_connection = None
  250. return self._get_db_connection(force_new=True)
  251. return self._db_connection
  252. def _close_db_connection(self):
  253. """
  254. 关闭数据库连接
  255. """
  256. if self._db_connection:
  257. try:
  258. self._db_connection.close()
  259. print("✓ 数据库连接已关闭")
  260. except:
  261. pass
  262. self._db_connection = None
  263. def _execute_with_retry(self, func, max_retries: int = 3):
  264. """
  265. 执行数据库操作并在失败时重试
  266. Args:
  267. func: 要执行的函数,接收connection参数
  268. max_retries: 最大重试次数
  269. Returns:
  270. 函数执行结果
  271. """
  272. last_error = None
  273. for attempt in range(max_retries):
  274. try:
  275. connection = self._get_db_connection()
  276. result = func(connection)
  277. return result
  278. except (pymysql.err.OperationalError, pymysql.err.InterfaceError) as e:
  279. last_error = e
  280. error_code = e.args[0] if e.args else 0
  281. # 2013: Lost connection to MySQL server during query
  282. # 2006: MySQL server has gone away
  283. # 1053: Server shutdown in progress
  284. if error_code in (2013, 2006, 1053):
  285. print(f"数据库连接错误 (尝试 {attempt+1}/{max_retries}): {e}")
  286. self._db_connection = None
  287. if attempt < max_retries - 1:
  288. wait_time = (attempt + 1) * 2 # 递增等待时间
  289. print(f"等待 {wait_time} 秒后重试...")
  290. time.sleep(wait_time)
  291. continue
  292. else:
  293. raise
  294. except Exception as e:
  295. print(f"数据库操作失败: {e}")
  296. raise
  297. raise last_error if last_error else Exception("数据库操作失败")
  298. def clear_table(self) -> bool:
  299. """
  300. 清空学生数据表
  301. Returns:
  302. 是否成功
  303. """
  304. try:
  305. connection = pymysql.connect(**DB_CONFIG)
  306. try:
  307. with connection.cursor() as cursor:
  308. table_name = TABLE_MAPPING["student"]
  309. cursor.execute(f"TRUNCATE TABLE `{table_name}`")
  310. connection.commit()
  311. print(f"✓ 已清空表 {table_name}")
  312. return True
  313. finally:
  314. connection.close()
  315. except Exception as e:
  316. print(f"✗ 清空表失败: {e}")
  317. return False
  318. def save_to_database(self, data_list: List[Dict], batch_size: int = 100) -> int:
  319. """
  320. 将学生数据保存到数据库(支持分批提交)
  321. Args:
  322. data_list: 学生数据列表
  323. batch_size: 每批提交的记录数,默认100
  324. Returns:
  325. 成功插入的记录数
  326. """
  327. if not data_list:
  328. print("数据列表为空,跳过保存")
  329. return 0
  330. try:
  331. connection = pymysql.connect(**DB_CONFIG)
  332. try:
  333. current_time = int(time.time() * 1000) # 毫秒时间戳
  334. table_name = TABLE_MAPPING["student"]
  335. with connection.cursor() as cursor:
  336. # 构建插入SQL
  337. insert_sql = f"""
  338. INSERT INTO `{table_name}` (
  339. `inserttime`, `updatetime`,
  340. `xsjbxx_xm`, `xsjbxx_xh`, `xsjbxx_lxdh`, `xsjbxx_sfzjlx`,
  341. `xsjbxx_sfzjh`, `dataMigrateUniqueId`, `xsjbxx_bjxx`, `xsjbxx_bjbh`,
  342. `xsjbxx_szxq`, `xsjbxx_xqdm`, `xsjbxx_sznj`, `xsjbxx_rxnj`,
  343. `xsjbxx_yxxx`, `xsjbxx_yxdm`, `xsjbxx_zyxx`, `xsjbxx_zydm`,
  344. `xsjbxx_bzrgh`, `xsjbxx_xsdqzt`, `xsjbxx_xssfzx`, `xsjbxx_xjydlx`,
  345. `xsjbxx_pycc`, `xsjbxx_xz`, `xsjbxx_xb`, `xsjbxx_csrq`,
  346. `xsjbxx_gj`, `xsjbxx_jtzz`, `xsjbxx_jtyzbm`, `xsjbxx_jtlxr`,
  347. `xsjbxx_jtdh`, `xsjbxx_fmxm1`, `xsjbxx_fmxm2`, `xsjbxx_cygx1`,
  348. `xsjbxx_cygx2`, `xsjbxx_cysfwjhr1`, `xsjbxx_cysfwjhr2`,
  349. `xsjbxx_fmsjh1`, `xsjbxx_fmsjh2`, `xsjbxx_fmzjlx1`, `xsjbxx_fmzjlx2`,
  350. `xsjbxx_fmzjh1`, `xsjbxx_fmzjh2`, `xsjbxx_xmpy`, `xsjbxx_xjzt`
  351. ) VALUES (
  352. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  353. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  354. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  355. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  356. %s, %s, %s, %s, %s
  357. )
  358. """
  359. success_count = 0
  360. batch_count = 0
  361. for item in data_list:
  362. try:
  363. cursor.execute(insert_sql, (
  364. current_time, current_time, # inserttime, updatetime
  365. item.get('xsjbxx_xm'), item.get('xsjbxx_xh'), item.get('xsjbxx_lxdh'), item.get('xsjbxx_sfzjlx'),
  366. item.get('xsjbxx_sfzjh'), item.get('dataMigrateUniqueId'), item.get('xsjbxx_bjxx'), item.get('xsjbxx_bjbh'),
  367. item.get('xsjbxx_szxq'), item.get('xsjbxx_xqdm'), item.get('xsjbxx_sznj'), item.get('xsjbxx_rxnj'),
  368. item.get('xsjbxx_yxxx'), item.get('xsjbxx_yxdm'), item.get('xsjbxx_zyxx'), item.get('xsjbxx_zydm'),
  369. item.get('xsjbxx_bzrgh'), item.get('xsjbxx_xsdqzt'), item.get('xsjbxx_xssfzx'), item.get('xsjbxx_xjydlx'),
  370. item.get('xsjbxx_pycc'), item.get('xsjbxx_xz'), item.get('xsjbxx_xb'), item.get('xsjbxx_csrq'),
  371. item.get('xsjbxx_gj'), item.get('xsjbxx_jtzz'), item.get('xsjbxx_jtyzbm'), item.get('xsjbxx_jtlxr'),
  372. item.get('xsjbxx_jtdh'), item.get('xsjbxx_fmxm1'), item.get('xsjbxx_fmxm2'), item.get('xsjbxx_cygx1'),
  373. item.get('xsjbxx_cygx2'), item.get('xsjbxx_cysfwjhr1'), item.get('xsjbxx_cysfwjhr2'),
  374. item.get('xsjbxx_fmsjh1'), item.get('xsjbxx_fmsjh2'), item.get('xsjbxx_fmzjlx1'), item.get('xsjbxx_fmzjlx2'),
  375. item.get('xsjbxx_fmzjh1'), item.get('xsjbxx_fmzjh2'), item.get('xsjbxx_xmpy'), item.get('xsjbxx_xjzt')
  376. ))
  377. success_count += 1
  378. batch_count += 1
  379. # 达到批次大小时提交
  380. if batch_count >= batch_size:
  381. connection.commit()
  382. print(f"✓ 分批提交 {batch_count} 条数据到表 {table_name},累计 {success_count} 条")
  383. batch_count = 0
  384. except Exception as e:
  385. print(f"插入单条数据失败: {e}, 数据: {item}")
  386. # 提交剩余数据
  387. if batch_count > 0:
  388. connection.commit()
  389. print(f"✓ 分批提交剩余 {batch_count} 条数据到表 {table_name},累计 {success_count} 条")
  390. print(f"✓ 总共成功插入 {success_count}/{len(data_list)} 条数据到表 {table_name}")
  391. return success_count
  392. finally:
  393. connection.close()
  394. except Exception as e:
  395. print(f"✗ 保存数据到数据库失败: {e}")
  396. return 0
  397. def get_and_save_students_batch(self, max_page_size: int = 500, batch_size: int = 100) -> int:
  398. """
  399. 获取所有学生信息并分批保存到cx_xssj表
  400. Args:
  401. max_page_size: API每页最大条数,默认500
  402. batch_size: 每批提交到数据库的记录数,默认100
  403. Returns:
  404. 成功保存的总记录数
  405. """
  406. total_saved = 0
  407. page_num = 1
  408. total_pages = None
  409. try:
  410. current_time = int(time.time() * 1000) # 毫秒时间戳
  411. table_name = TABLE_MAPPING["student"]
  412. # 构建插入SQL
  413. insert_sql = f"""
  414. INSERT INTO `{table_name}` (
  415. `inserttime`, `updatetime`,
  416. `xsjbxx_xm`, `xsjbxx_xh`, `xsjbxx_lxdh`, `xsjbxx_sfzjlx`,
  417. `xsjbxx_sfzjh`, `dataMigrateUniqueId`, `xsjbxx_bjxx`, `xsjbxx_bjbh`,
  418. `xsjbxx_szxq`, `xsjbxx_xqdm`, `xsjbxx_sznj`, `xsjbxx_rxnj`,
  419. `xsjbxx_yxxx`, `xsjbxx_yxdm`, `xsjbxx_zyxx`, `xsjbxx_zydm`,
  420. `xsjbxx_bzrgh`, `xsjbxx_xsdqzt`, `xsjbxx_xssfzx`, `xsjbxx_xjydlx`,
  421. `xsjbxx_pycc`, `xsjbxx_xz`, `xsjbxx_xb`, `xsjbxx_csrq`,
  422. `xsjbxx_gj`, `xsjbxx_jtzz`, `xsjbxx_jtyzbm`, `xsjbxx_jtlxr`,
  423. `xsjbxx_jtdh`, `xsjbxx_fmxm1`, `xsjbxx_fmxm2`, `xsjbxx_cygx1`,
  424. `xsjbxx_cygx2`, `xsjbxx_cysfwjhr1`, `xsjbxx_cysfwjhr2`,
  425. `xsjbxx_fmsjh1`, `xsjbxx_fmsjh2`, `xsjbxx_fmzjlx1`, `xsjbxx_fmzjlx2`,
  426. `xsjbxx_fmzjh1`, `xsjbxx_fmzjh2`, `xsjbxx_xmpy`, `xsjbxx_xjzt`
  427. ) VALUES (
  428. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  429. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  430. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  431. %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
  432. %s, %s, %s, %s, %s
  433. )
  434. """
  435. while True:
  436. print(f"\n正在获取第 {page_num} 页数据...")
  437. result = self.get_student_list(page_num, max_page_size)
  438. # 超星API成功时code=1,失败时code!=1
  439. if result.get("code") != 1:
  440. print(f"获取第 {page_num} 页失败: {result.get('message', '未知错误')}")
  441. break
  442. # 获取分页信息(从extendData中)
  443. extend_data = result.get("extendData", {})
  444. if total_pages is None and extend_data:
  445. total_pages = int(extend_data.get("pages", 0))
  446. total_count = int(extend_data.get("count", 0))
  447. print(f"总页数: {total_pages}, 总条数: {total_count}")
  448. data = result.get("data")
  449. if not data:
  450. print("没有更多数据")
  451. break
  452. # 处理返回的数据
  453. items = []
  454. if isinstance(data, list):
  455. items = data
  456. elif isinstance(data, dict):
  457. items = data.get("list", data.get("records", []))
  458. if not items:
  459. break
  460. print(f"第 {page_num} 页获取 {len(items)} 条数据")
  461. # 使用重试机制插入数据
  462. def insert_batch(connection):
  463. nonlocal total_saved
  464. with connection.cursor() as cursor:
  465. for item in items:
  466. try:
  467. cursor.execute(insert_sql, (
  468. current_time, current_time, # inserttime, updatetime
  469. item.get('xsjbxx_xm'), item.get('xsjbxx_xh'), item.get('xsjbxx_lxdh'), item.get('xsjbxx_sfzjlx'),
  470. item.get('xsjbxx_sfzjh'), item.get('dataMigrateUniqueId'), item.get('xsjbxx_bjxx'), item.get('xsjbxx_bjbh'),
  471. item.get('xsjbxx_szxq'), item.get('xsjbxx_xqdm'), item.get('xsjbxx_sznj'), item.get('xsjbxx_rxnj'),
  472. item.get('xsjbxx_yxxx'), item.get('xsjbxx_yxdm'), item.get('xsjbxx_zyxx'), item.get('xsjbxx_zydm'),
  473. item.get('xsjbxx_bzrgh'), item.get('xsjbxx_xsdqzt'), item.get('xsjbxx_xssfzx'), item.get('xsjbxx_xjydlx'),
  474. item.get('xsjbxx_pycc'), item.get('xsjbxx_xz'), item.get('xsjbxx_xb'), item.get('xsjbxx_csrq'),
  475. item.get('xsjbxx_gj'), item.get('xsjbxx_jtzz'), item.get('xsjbxx_jtyzbm'), item.get('xsjbxx_jtlxr'),
  476. item.get('xsjbxx_jtdh'), item.get('xsjbxx_fmxm1'), item.get('xsjbxx_fmxm2'), item.get('xsjbxx_cygx1'),
  477. item.get('xsjbxx_cygx2'), item.get('xsjbxx_cysfwjhr1'), item.get('xsjbxx_cysfwjhr2'),
  478. item.get('xsjbxx_fmsjh1'), item.get('xsjbxx_fmsjh2'), item.get('xsjbxx_fmzjlx1'), item.get('xsjbxx_fmzjlx2'),
  479. item.get('xsjbxx_fmzjh1'), item.get('xsjbxx_fmzjh2'), item.get('xsjbxx_xmpy'), item.get('xsjbxx_xjzt')
  480. ))
  481. total_saved += 1
  482. # 达到批次大小时提交
  483. if total_saved % batch_size == 0:
  484. connection.commit()
  485. print(f"✓ 分批提交 {batch_size} 条数据到表 {table_name},累计 {total_saved} 条")
  486. except Exception as e:
  487. print(f"插入单条数据失败: {e}, 数据: {item}")
  488. # 提交当前批次剩余数据
  489. if total_saved % batch_size != 0:
  490. connection.commit()
  491. return total_saved
  492. # 执行插入操作(带重试)
  493. self._execute_with_retry(insert_batch)
  494. # 如果知道总页数,检查是否已获取完所有页
  495. if total_pages and page_num >= total_pages:
  496. break
  497. # 如果返回的数据少于page_size,说明已经是最后一页
  498. if len(items) < max_page_size:
  499. break
  500. page_num += 1
  501. print(f"\n✓ 总共成功保存 {total_saved} 条数据到表 {table_name}")
  502. return total_saved
  503. except Exception as e:
  504. print(f"✗ 获取并保存数据失败: {e}")
  505. import traceback
  506. traceback.print_exc()
  507. return total_saved # 返回已保存的数量
  508. finally:
  509. self._close_db_connection()
  510. # --------- 新增的方法:同步到xg_student_msg ---------
  511. def sync_student_msg(self) -> int:
  512. """
  513. 将cx_xssj表中的学生数据同步到xg_student_msg表
  514. Returns:
  515. 成功同步的记录数
  516. """
  517. try:
  518. def do_sync(connection):
  519. source_rows = self._fetch_source_students(connection)
  520. if not source_rows:
  521. print("cx_xssj表暂无可同步数据")
  522. return 0, 0, 0, 0
  523. print(f"待同步数据量: {len(source_rows)} 条")
  524. success, user_inserted, user_skipped, role_inserted = self._insert_into_target(connection, source_rows)
  525. connection.commit()
  526. return success, user_inserted, user_skipped, role_inserted
  527. success, user_inserted, user_skipped, role_inserted = self._execute_with_retry(do_sync)
  528. print(f"✓ 已同步 {success} 条数据至xg_student_msg")
  529. if success:
  530. print(f"✓ 新增 {user_inserted} 个sys_user账号,跳过 {user_skipped} 个已存在账号")
  531. print(f"✓ 新增 {role_inserted} 条学生角色绑定 (role_id={self.STUDENT_ROLE_ID})")
  532. return success
  533. except Exception as exc:
  534. print(f"✗ 同步到xg_student_msg失败: {exc}")
  535. import traceback
  536. traceback.print_exc()
  537. return 0
  538. finally:
  539. self._close_db_connection()
  540. def _fetch_source_students(self, connection) -> List[Dict[str, Any]]:
  541. """
  542. 从cx_xssj表读取需要同步的学生数据
  543. """
  544. select_sql = """
  545. SELECT
  546. xsjbxx_xm,
  547. xsjbxx_xh,
  548. xsjbxx_lxdh,
  549. xsjbxx_sfzjh,
  550. xsjbxx_xb,
  551. xsjbxx_csrq,
  552. xsjbxx_xjzt,
  553. xsjbxx_rxnj,
  554. xsjbxx_sznj,
  555. xsjbxx_bjbh,
  556. xsjbxx_bjxx
  557. FROM `{table_name}`
  558. """.format(table_name=TABLE_MAPPING["student"])
  559. with connection.cursor() as cursor:
  560. cursor.execute(select_sql)
  561. rows = cursor.fetchall()
  562. print(f"从{TABLE_MAPPING['student']}取回 {len(rows)} 条记录")
  563. return rows
  564. def _insert_into_target(self, connection, rows: List[Dict[str, Any]]) -> Tuple[int, List[Dict[str, Any]]]:
  565. """
  566. 将数据写入xg_student_msg表
  567. """
  568. insert_sql = """
  569. INSERT INTO `xg_student_msg` (
  570. `id`, `tenant_id`, `create_dept`, `create_by`, `create_time`,
  571. `update_by`, `update_time`, `deleted`, `student_no`, `id_card`,
  572. `name`, `gender`, `birth_date`, `student_status`, `enrollment_year`,
  573. `data_source`, `grade`, `class_id`, `class_name`, `major_id`,
  574. `major_name`, `college_id`, `college_name`
  575. ) VALUES (
  576. %s, %s, %s, %s, %s,
  577. %s, %s, %s, %s, %s,
  578. %s, %s, %s, %s, %s,
  579. %s, %s, %s, %s, %s,
  580. %s, %s, %s
  581. )
  582. """
  583. base_datetime = datetime.now()
  584. timestamp_str = base_datetime.strftime("%Y%m%d%H%M%S")
  585. success_count = 0
  586. sequence = 1
  587. batch_inserted = 0
  588. user_inserted_total = 0
  589. user_skipped_total = 0
  590. role_inserted_total = 0
  591. existing_student_nos = self._load_existing_student_nos(connection)
  592. with connection.cursor() as cursor:
  593. for row in rows:
  594. student_no = row.get("xsjbxx_xh")
  595. name = row.get("xsjbxx_xm")
  596. if not student_no or not name:
  597. print(f"跳过无效数据: {row}")
  598. continue
  599. if student_no in existing_student_nos:
  600. print(f"跳过已存在学生: student_no={student_no}")
  601. continue
  602. if sequence > 99999:
  603. print("同步条目超过单次支持的最大值99999,停止写入以避免ID溢出")
  604. break
  605. unique_id = self._generate_student_id(timestamp_str, sequence)
  606. gender = self._map_gender(row.get("xsjbxx_xb"))
  607. status = self._map_student_status(row.get("xsjbxx_xsdqzt"))
  608. class_id, class_name = self._find_class_dept(
  609. connection,
  610. row.get("xsjbxx_bjbh"),
  611. row.get("xsjbxx_bjxx")
  612. )
  613. major_id, major_name, college_id, college_name = self._find_major_info(connection, class_id)
  614. birth_date = self._normalize_birth_date(row.get("xsjbxx_csrq"))
  615. enrollment_year = self._normalize_enrollment_year(row.get("xsjbxx_rxnj"))
  616. params = (
  617. unique_id,
  618. self.DEFAULT_TENANT_ID,
  619. 0, # create_dept
  620. self.DEFAULT_CREATE_BY,
  621. base_datetime,
  622. self.DEFAULT_UPDATE_BY,
  623. base_datetime,
  624. 0,
  625. student_no,
  626. row.get("xsjbxx_sfzjh"),
  627. name,
  628. gender,
  629. birth_date,
  630. status,
  631. enrollment_year,
  632. "sync", # data_source
  633. row.get("xsjbxx_sznj"),
  634. class_id,
  635. class_name,
  636. major_id,
  637. major_name,
  638. college_id,
  639. college_name
  640. )
  641. try:
  642. cursor.execute(insert_sql, params)
  643. success_count += 1
  644. sequence += 1
  645. batch_inserted += 1
  646. existing_student_nos.add(student_no)
  647. student_profile = {
  648. "user_id": int(unique_id),
  649. "student_no": student_no,
  650. "name": name,
  651. "gender": gender,
  652. "phone": self._normalize_phone(row.get("xsjbxx_lxdh")),
  653. "email": None,
  654. "class_id": int(class_id) if class_id is not None else None,
  655. "tenant_id": self.DEFAULT_TENANT_ID,
  656. "create_time": base_datetime,
  657. "update_time": base_datetime,
  658. }
  659. inserted_flag, skipped_flag = self._create_sys_user_for_student(connection, student_profile)
  660. user_inserted_total += inserted_flag
  661. user_skipped_total += skipped_flag
  662. if inserted_flag:
  663. role_inserted_total += self._ensure_user_role(connection, student_profile["user_id"], self.STUDENT_ROLE_ID)
  664. else:
  665. # 即使用户已存在,仍尝试补齐角色
  666. role_inserted_total += self._ensure_user_role(connection, student_profile["user_id"], self.STUDENT_ROLE_ID)
  667. if batch_inserted >= 100:
  668. # 批量提交前ping一下连接
  669. try:
  670. connection.ping(reconnect=True)
  671. except Exception as e:
  672. print(f"警告: 连接ping失败: {e}")
  673. connection.commit()
  674. print("✓ xg_student_msg分批提交100条记录")
  675. batch_inserted = 0
  676. except Exception as exc:
  677. print(f"插入xg_student_msg失败: {exc}, 数据: {params}")
  678. if batch_inserted > 0:
  679. connection.commit()
  680. print(f"✓ xg_student_msg分批提交剩余 {batch_inserted} 条记录")
  681. return success_count, user_inserted_total, user_skipped_total, role_inserted_total
  682. def _load_existing_student_nos(self, connection) -> set:
  683. """
  684. 获取xg_student_msg表中已存在的学号集合
  685. """
  686. query = "SELECT student_no FROM `xg_student_msg`"
  687. with connection.cursor() as cursor:
  688. cursor.execute(query)
  689. rows = cursor.fetchall()
  690. return {row[0] if isinstance(row, tuple) else row.get("student_no") for row in rows}
  691. def _generate_student_id(self, timestamp_str: str, sequence: int) -> str:
  692. """
  693. 生成符合要求的学生ID
  694. """
  695. return f"{timestamp_str}{sequence:05d}"
  696. def _map_gender(self, gender_text: Optional[str]) -> Optional[int]:
  697. """
  698. 将性别文本映射为目标表字段
  699. """
  700. if gender_text == "男":
  701. return 0
  702. if gender_text == "女":
  703. return 1
  704. return 0
  705. def _map_student_status(self, status_text: Optional[str]) -> Optional[int]:
  706. """
  707. 将学生当前状态(xsjbxx_xsdqzt)文本映射为目标表字段
  708. """
  709. if not status_text:
  710. return 1
  711. mapping = {
  712. "毕业": 5,
  713. "退学": 4,
  714. "结业": 7,
  715. "在读": 1,
  716. "停学": 8,
  717. "分流": 9,
  718. "休学": 2,
  719. "复学": 1,
  720. }
  721. normalized = str(status_text).strip()
  722. mapped_value = mapping.get(normalized)
  723. if mapped_value is None:
  724. print(f"警告: 未配置的学生当前状态 -> raw='{status_text}',默认使用1(在读)")
  725. return 1
  726. return mapped_value
  727. def _find_class_dept(
  728. self,
  729. connection,
  730. class_code: Optional[str],
  731. class_name: Optional[str]
  732. ) -> Tuple[Optional[int], Optional[str]]:
  733. """
  734. 根据班级信息查找sys_dept中的班级部门
  735. """
  736. if not class_code or not class_name:
  737. return None, None
  738. dept_category = f"bj-{class_code}"
  739. query = """
  740. SELECT dept_id, dept_name
  741. FROM `sys_dept`
  742. WHERE dept_category = %s AND dept_name = %s
  743. LIMIT 1
  744. """
  745. with connection.cursor() as cursor:
  746. cursor.execute(query, (dept_category, class_name))
  747. result = cursor.fetchone()
  748. if result:
  749. return result["dept_id"], result["dept_name"]
  750. print(f"警告: 未在sys_dept中找到班级 -> dept_category={dept_category}, dept_name={class_name}")
  751. return None, None
  752. def _find_major_info(
  753. self,
  754. connection,
  755. class_id: Optional[int]
  756. ) -> Tuple[Optional[int], Optional[str], Optional[int], Optional[str]]:
  757. """
  758. 根据班级ID从xg_class_msg中查询专业与学院信息
  759. """
  760. if not class_id:
  761. return None, None, None, None
  762. query = """
  763. SELECT major_id, major_name, college_id, college_name
  764. FROM `xg_class_msg`
  765. WHERE id = %s
  766. LIMIT 1
  767. """
  768. with connection.cursor() as cursor:
  769. cursor.execute(query, (class_id,))
  770. result = cursor.fetchone()
  771. if result:
  772. return (
  773. result.get("major_id"),
  774. result.get("major_name"),
  775. result.get("college_id"),
  776. result.get("college_name")
  777. )
  778. print(f"警告: 未在xg_class_msg中找到班级信息 -> class_id={class_id}")
  779. return None, None, None, None
  780. def _normalize_birth_date(self, raw_value: Optional[str]) -> Optional[str]:
  781. """
  782. 将出生日期转换为YYYY-MM-DD格式, 无效值返回None
  783. """
  784. if not raw_value:
  785. return None
  786. text = str(raw_value).strip()
  787. if not text:
  788. return None
  789. for fmt in ("%Y-%m-%d", "%Y%m%d", "%Y/%m/%d"):
  790. try:
  791. parsed = datetime.strptime(text, fmt)
  792. return parsed.strftime("%Y-%m-%d")
  793. except ValueError:
  794. continue
  795. print(f"警告: 无法解析出生日期 -> raw='{raw_value}'")
  796. return None
  797. def _normalize_enrollment_year(self, raw_value: Optional[Any]) -> Optional[int]:
  798. """
  799. 清洗入学年份字段,返回有效的年份整数或None
  800. """
  801. if raw_value is None:
  802. return None
  803. text = str(raw_value).strip()
  804. if not text:
  805. return None
  806. if text.isdigit():
  807. return int(text)
  808. # 处理可能的日期格式,如2025-09-01
  809. try:
  810. parsed = datetime.strptime(text[:10], "%Y-%m-%d")
  811. return parsed.year
  812. except Exception:
  813. print(f"警告: 无法解析入学年份 -> raw='{raw_value}'")
  814. return None
  815. def _normalize_phone(self, raw_value: Optional[str]) -> Optional[str]:
  816. """
  817. 清洗手机号字段
  818. """
  819. if not raw_value:
  820. return None
  821. text = str(raw_value).strip()
  822. return text if text else None
  823. def _create_sys_user_for_student(self, connection, student: Dict[str, Any]) -> Tuple[int, int]:
  824. """
  825. 为学生创建sys_user账号
  826. Returns:
  827. (新增账号数量, 已存在账号数量)
  828. """
  829. user_id = student["user_id"]
  830. with connection.cursor() as cursor:
  831. cursor.execute("SELECT 1 FROM `sys_user` WHERE user_id = %s LIMIT 1", (user_id,))
  832. if cursor.fetchone():
  833. return 0, 1
  834. password_hash = bcrypt.hashpw(self.DEFAULT_PASSWORD.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
  835. insert_sql = """
  836. INSERT INTO `sys_user` (
  837. `user_id`, `dept_id`, `user_name`, `nick_name`, `user_type`,
  838. `email`, `phonenumber`, `sex`, `avatar`, `password`, `status`,
  839. `del_flag`, `login_ip`, `login_date`, `create_by`, `create_time`,
  840. `update_by`, `update_time`, `remark`, `tenant_id`, `account_type`
  841. ) VALUES (
  842. %s, %s, %s, %s, %s,
  843. %s, %s, %s, %s, %s, %s,
  844. %s, %s, %s, %s, %s,
  845. %s, %s, %s, %s, %s
  846. )
  847. """
  848. try:
  849. with connection.cursor() as cursor:
  850. cursor.execute(
  851. insert_sql,
  852. (
  853. user_id,
  854. student.get("class_id"),
  855. student["student_no"],
  856. student["name"],
  857. self.DEFAULT_USER_TYPE,
  858. student.get("email"),
  859. student.get("phone"),
  860. self._map_gender_for_user(student.get("gender")),
  861. None,
  862. password_hash,
  863. self.SYS_USER_STATUS,
  864. self.SYS_USER_DEL_FLAG,
  865. None,
  866. None,
  867. self.DEFAULT_CREATE_BY,
  868. student["create_time"],
  869. self.DEFAULT_UPDATE_BY,
  870. student["update_time"],
  871. self.SYS_USER_REMARK,
  872. student["tenant_id"],
  873. self.DEFAULT_ACCOUNT_TYPE,
  874. ),
  875. )
  876. return 1, 0
  877. except Exception as exc:
  878. print(f"插入sys_user失败: {exc}, user_id={user_id}, student_no={student['student_no']}")
  879. import traceback
  880. traceback.print_exc()
  881. return 0, 1
  882. def _ensure_user_role(self, connection, user_id: int, role_id: int) -> int:
  883. """
  884. 确保用户具备指定角色
  885. Returns:
  886. 新增的角色绑定数量
  887. """
  888. with connection.cursor() as cursor:
  889. cursor.execute(
  890. "SELECT 1 FROM `sys_user_role` WHERE user_id = %s AND role_id = %s LIMIT 1",
  891. (user_id, role_id),
  892. )
  893. if cursor.fetchone():
  894. return 0
  895. try:
  896. cursor.execute(
  897. """
  898. INSERT INTO `sys_user_role` (`user_id`, `role_id`)
  899. VALUES (%s, %s)
  900. """,
  901. (user_id, role_id),
  902. )
  903. return 1
  904. except Exception as exc:
  905. print(f"绑定角色失败: {exc}, user_id={user_id}, role_id={role_id}")
  906. return 0
  907. def _sync_sys_users(self, connection, students: List[Dict[str, Any]]) -> Tuple[int, int]:
  908. """
  909. 将新增学生同步到sys_user表
  910. Returns:
  911. (新增账号数量, 已存在账号数量)
  912. """
  913. if not students:
  914. return 0, 0
  915. user_ids = [student["user_id"] for student in students]
  916. existing_user_ids = self._load_existing_sys_user_ids(connection, user_ids)
  917. insert_sql = """
  918. INSERT INTO `sys_user` (
  919. `user_id`, `dept_id`, `user_name`, `nick_name`, `user_type`,
  920. `email`, `phonenumber`, `sex`, `avatar`, `password`, `status`,
  921. `del_flag`, `login_ip`, `login_date`, `create_by`, `create_time`,
  922. `update_by`, `update_time`, `remark`, `tenant_id`, `account_type`
  923. ) VALUES (
  924. %s, %s, %s, %s, %s,
  925. %s, %s, %s, %s, %s, %s,
  926. %s, %s, %s, %s, %s,
  927. %s, %s, %s, %s, %s
  928. )
  929. """
  930. inserted = 0
  931. skipped = 0
  932. with connection.cursor() as cursor:
  933. for student in students:
  934. user_id = student["user_id"]
  935. if user_id in existing_user_ids:
  936. skipped += 1
  937. continue
  938. password_hash = bcrypt.hashpw(self.DEFAULT_PASSWORD.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
  939. try:
  940. cursor.execute(
  941. insert_sql,
  942. (
  943. user_id,
  944. student.get("class_id"),
  945. student["student_no"],
  946. student["name"],
  947. self.DEFAULT_USER_TYPE,
  948. student.get("email"),
  949. student.get("phone"),
  950. self._map_gender_for_user(student.get("gender")),
  951. None,
  952. password_hash,
  953. self.SYS_USER_STATUS,
  954. self.SYS_USER_DEL_FLAG,
  955. None,
  956. None,
  957. self.DEFAULT_CREATE_BY,
  958. student["create_time"],
  959. self.DEFAULT_UPDATE_BY,
  960. student["update_time"],
  961. self.SYS_USER_REMARK,
  962. student["tenant_id"],
  963. self.DEFAULT_ACCOUNT_TYPE,
  964. ),
  965. )
  966. inserted += 1
  967. existing_user_ids.add(user_id)
  968. except Exception as exc:
  969. skipped += 1
  970. print(f"插入sys_user失败: {exc}, user_id={user_id}, student_no={student['student_no']}")
  971. return inserted, skipped
  972. def _sync_user_roles(self, connection, user_ids: List[int], role_id: int) -> int:
  973. """
  974. 为用户绑定指定角色
  975. Returns:
  976. 新增的角色绑定数量
  977. """
  978. if not user_ids:
  979. return 0
  980. existing = self._load_existing_user_role_ids(connection, user_ids, role_id)
  981. insert_sql = """
  982. INSERT INTO `sys_user_role` (
  983. `user_id`, `role_id`
  984. ) VALUES (
  985. %s, %s
  986. )
  987. """
  988. inserted = 0
  989. with connection.cursor() as cursor:
  990. for user_id in user_ids:
  991. if user_id in existing:
  992. continue
  993. try:
  994. cursor.execute(insert_sql, (user_id, role_id))
  995. inserted += 1
  996. except Exception as exc:
  997. print(f"绑定角色失败: {exc}, user_id={user_id}, role_id={role_id}")
  998. return inserted
  999. def _load_existing_sys_user_ids(self, connection, user_ids: List[int]) -> set:
  1000. """
  1001. 批量查询已存在的sys_user用户ID
  1002. """
  1003. if not user_ids:
  1004. return set()
  1005. existing_ids = set()
  1006. with connection.cursor() as cursor:
  1007. for batch in self._split_batches(user_ids):
  1008. placeholders = ",".join(["%s"] * len(batch))
  1009. sql = f"SELECT user_id FROM `sys_user` WHERE user_id IN ({placeholders})"
  1010. cursor.execute(sql, batch)
  1011. rows = cursor.fetchall()
  1012. for row in rows:
  1013. value = row["user_id"] if isinstance(row, dict) else row[0]
  1014. existing_ids.add(int(value))
  1015. return existing_ids
  1016. def _load_existing_user_role_ids(self, connection, user_ids: List[int], role_id: int) -> set:
  1017. """
  1018. 查询已经存在的用户角色关联
  1019. """
  1020. if not user_ids:
  1021. return set()
  1022. existing = set()
  1023. with connection.cursor() as cursor:
  1024. for batch in self._split_batches(user_ids):
  1025. placeholders = ",".join(["%s"] * len(batch))
  1026. sql = f"""
  1027. SELECT user_id FROM `sys_user_role`
  1028. WHERE role_id = %s AND user_id IN ({placeholders})
  1029. """
  1030. cursor.execute(sql, [role_id, *batch])
  1031. rows = cursor.fetchall()
  1032. for row in rows:
  1033. value = row["user_id"] if isinstance(row, dict) else row[0]
  1034. existing.add(int(value))
  1035. return existing
  1036. def _split_batches(self, values: List[int], size: int = QUERY_BATCH_SIZE) -> List[List[int]]:
  1037. """
  1038. 将列表按指定大小拆分
  1039. """
  1040. if not values:
  1041. return []
  1042. return [values[i : i + size] for i in range(0, len(values), size)]
  1043. def _map_gender_for_user(self, gender_code: Optional[int]) -> str:
  1044. """
  1045. 将性别编码转换为sys_user表所需的字符串
  1046. """
  1047. if gender_code == 0:
  1048. return "0"
  1049. if gender_code == 1:
  1050. return "1"
  1051. return "2"
  1052. def main():
  1053. """主函数 - 示例用法"""
  1054. # API配置信息
  1055. APP_ID = "acd4cc28-e8c1-4fb8-a38e-6de8cbaaee34"
  1056. APP_KEY = "dyjs_xsjbxx"
  1057. FID = "19628" # 单位ID
  1058. # 创建API客户端
  1059. api = ChaoxingStudentAPI(app_id=APP_ID, app_key=APP_KEY, fid=FID)
  1060. # 获取所有数据并分批保存到数据库
  1061. print("=" * 60)
  1062. print("获取所有学生信息并分批保存到数据库")
  1063. print("=" * 60)
  1064. # 步骤1: 清空表
  1065. print("\n步骤1: 清空学生数据表...")
  1066. api.clear_table()
  1067. # 步骤2: 获取数据并分批保存到cx_xssj表
  1068. print("\n步骤2: 获取学生信息并分批保存到cx_xssj表(每批100条)...")
  1069. saved_count = api.get_and_save_students_batch(max_page_size=500, batch_size=100)
  1070. if saved_count > 0:
  1071. print(f"\n✓ 完成!成功保存 {saved_count} 条数据到cx_xssj表")
  1072. # 步骤3: 同步数据至xg_student_msg
  1073. print("\n步骤3: 同步数据至xg_student_msg...")
  1074. synced_count = api.sync_student_msg()
  1075. print(f"\n✓ 学生信息同步完成,成功写入 {synced_count} 条记录")
  1076. else:
  1077. print("\n✗ 未获取到数据或保存失败")
  1078. if __name__ == "__main__":
  1079. main()