| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247 |
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- """
- 超星系统 - 班级信息完整同步脚本
- 功能:
- 1. 从超星API获取班级信息(API路径: bjsjpi_53146/list)
- 2. 保存到cx_bjsj表
- 3. 从cx_bjsj表同步到sys_dept表(学工系统部门表)
- 4. 从cx_bjsj表同步到xg_class_msg表(学工系统班级信息表)
- 同步规则(从cx_bjsj表同步到sys_dept表):
- - 筛选条件:bjxx_sfqy='是' 的数据才可以同步
- - 判断是否已存在:dept_category='bj-' + bjxx_bjbh AND dept_name=bjxx_bjmc
- - 父部门查找:通过 dept_category='zy-' + bjxx_zybh AND dept_name=bjxx_zy 查找父部门ID
- - dept_id:当前时间戳(%Y%m%d%H%M%S)+ 0001开始递增
- - parent_id:通过专业信息查找得到的dept_id
- - ancestors:根据parent_id计算
- - order_num:按照返回数据的顺序,每个父部门下独立递增
- 同步规则(从cx_bjsj表同步到xg_class_msg表):
- - 筛选条件:与sys_dept表同步相同的筛选条件(bjxx_sfqy='是')
- - 判断是否已存在:id = sys_dept表的dept_id
- - id:与sys_dept表的dept_id一致
- - major_id和major_name:通过 dept_category='zy-' + bjxx_zybh AND dept_name=bjxx_zy 查找
- - college_id和college_name:通过major_id查找parent_id,然后检查dept_category是否以"xy-"开头
- - school_system:通过bjxx_zybh关联cx_zysj表的zysj_zybh,获取zysj_xzm
- - grade和enrollment_year:使用bjxx_rxnf,如果为空则使用当前年
- - 其他字段:按照需求设置(大部分为空或默认值)
- """
- import hashlib
- import base64
- import json
- import requests
- import pymysql
- import time
- import re
- from datetime import datetime
- 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 ChaoxingClassAPI:
- """超星班级信息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 = "bjsjpi_53146"
- # 生成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=bjsjpi_53146#$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_class_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
- }
-
- # 构建请求头
- 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_classes(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_class_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["class"]
- 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:
- """
- 将班级数据保存到数据库
- 只保留近5年的数据(根据bjxx_rxnf字段,即班级入学年份)
-
- 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["class"]
-
- with connection.cursor() as cursor:
- # 构建插入SQL
- insert_sql = f"""
- INSERT INTO `{table_name}` (
- `inserttime`, `updatetime`,
- `bjxx_bjmc`, `bjxx_bjbh`, `bjxx_ssyx`, `bjxx_yxbm`,
- `bjxx_zy`, `bjxx_zybh`, `bjxx_rxnf`, `bjxx_bzr`,
- `bjxx_bzrgh`, `bjxx_szxq`, `bjxx_xqbm`, `bjxx_njrs`,
- `bjxx_bjzdrs`, `bjxx_ssjys`, `bjxx_jysbh`, `bjxx_bynf`,
- `bjxx_jbny`, `bjxx_byxnxq`, `bjxx_byny`, `bjxx_gdjs`,
- `bjxx_gdjsbh`, `bjxx_bjjc`, `bjxx_sfqy`, `bjxx_sznf`,
- `dataMigrateUniqueId`
- ) 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
- )
- """
-
- success_count = 0
-
- for item in data_list:
- try:
- cursor.execute(insert_sql, (
- current_time, current_time, # inserttime, updatetime
- item.get('bjxx_bjmc'), item.get('bjxx_bjbh'), item.get('bjxx_ssyx'), item.get('bjxx_yxbm'),
- item.get('bjxx_zy'), item.get('bjxx_zybh'), item.get('bjxx_rxnf'), item.get('bjxx_bzr'),
- item.get('bjxx_bzrgh'), item.get('bjxx_szxq'), item.get('bjxx_xqbm'), item.get('bjxx_njrs'),
- item.get('bjxx_bjzdrs'), item.get('bjxx_ssjys'), item.get('bjxx_jysbh'), item.get('bjxx_bynf'),
- item.get('bjxx_jbny'), item.get('bjxx_byxnxq'), item.get('bjxx_byny'), item.get('bjxx_gdjs'),
- item.get('bjxx_gdjsbh'), item.get('bjxx_bjjc'), item.get('bjxx_sfqy'), item.get('bjxx_sznf'),
- item.get('dataMigrateUniqueId')
- ))
- 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
- class ClassSync:
- """班级数据同步类 - 从cx_bjsj表同步到sys_dept表"""
-
- def __init__(self):
- """初始化"""
- self.tenant_id = '000000'
-
- def get_valid_classes(self) -> List[Dict[str, Any]]:
- """
- 从cx_bjsj表中获取符合条件的数据(bjxx_sfqy = "是")
-
- Returns:
- 符合条件的班级数据列表
- """
- try:
- connection = pymysql.connect(**DB_CONFIG)
- try:
- with connection.cursor(pymysql.cursors.DictCursor) as cursor:
- # 查询符合条件的数据:bjxx_sfqy = "是"
- sql = """
- SELECT
- bjxx_bjmc, bjxx_bjbh, bjxx_zy, bjxx_zybh, bjxx_rxnf
- FROM cx_bjsj
- WHERE bjxx_sfqy = '是'
- ORDER BY inserttime ASC
- """
- cursor.execute(sql)
- results = cursor.fetchall()
- print(f"✓ 从cx_bjsj表中查询到 {len(results)} 条符合条件的数据(bjxx_sfqy='是')")
- return results
- finally:
- connection.close()
- except Exception as e:
- print(f"✗ 查询cx_bjsj表失败: {e}")
- return []
-
- def check_class_exists(self, dept_name: str, dept_category: str) -> bool:
- """
- 检查班级是否已存在
-
- Args:
- dept_name: 部门名称(班级名称)
- dept_category: 部门类别编码("bj-" + bjxx_bjbh)
-
- Returns:
- 是否存在
- """
- try:
- connection = pymysql.connect(**DB_CONFIG)
- try:
- with connection.cursor() as cursor:
- sql = """
- SELECT COUNT(*) as count
- FROM sys_dept
- WHERE dept_name = %s AND dept_category = %s
- """
- cursor.execute(sql, (dept_name, dept_category))
- result = cursor.fetchone()
- return result[0] > 0
- finally:
- connection.close()
- except Exception as e:
- print(f"✗ 检查班级是否存在失败: {e}")
- return False
-
- def find_parent_dept_id(self, zy_name: str, zybh: str) -> Optional[int]:
- """
- 查找父部门ID(专业)
-
- 通过 dept_category = "zy-" + bjxx_zybh 和 dept_name = bjxx_zy 查找
-
- Args:
- zy_name: 专业名称
- zybh: 专业编号
-
- Returns:
- 父部门ID,如果未找到则返回None
- """
- if not zybh:
- return None
-
- try:
- connection = pymysql.connect(**DB_CONFIG)
- try:
- with connection.cursor() as cursor:
- dept_category = f"zy-{zybh}"
- sql = """
- SELECT dept_id, ancestors
- FROM sys_dept
- WHERE dept_name = %s AND dept_category = %s
- LIMIT 1
- """
- cursor.execute(sql, (zy_name, dept_category))
- result = cursor.fetchone()
- if result:
- return result[0] # 返回dept_id
- return None
- finally:
- connection.close()
- except Exception as e:
- print(f"✗ 查找父部门失败: {e}")
- return None
-
- def find_major_info(self, zy_name: str, zybh: str, cursor=None) -> Optional[Dict[str, Any]]:
- """
- 查找专业信息(major_id和major_name)
-
- 通过 dept_category = "zy-" + bjxx_zybh 和 dept_name = bjxx_zy 查找
-
- Args:
- zy_name: 专业名称
- zybh: 专业编号
- cursor: 数据库游标对象,如果提供则使用该游标,否则创建新连接
-
- Returns:
- 专业信息字典(包含major_id和major_name),如果未找到则返回None
- """
- if not zybh:
- return None
-
- try:
- if cursor is not None:
- # 使用提供的游标
- dept_category = f"zy-{zybh}"
- sql = """
- SELECT dept_id as major_id, dept_name as major_name
- FROM sys_dept
- WHERE dept_name = %s AND dept_category = %s
- LIMIT 1
- """
- cursor.execute(sql, (zy_name, dept_category))
- result = cursor.fetchone()
- if result:
- return {
- 'major_id': result[0] if isinstance(result, tuple) else result.get('major_id'),
- 'major_name': result[1] if isinstance(result, tuple) else result.get('major_name')
- }
- return None
- else:
- # 创建新连接
- connection = pymysql.connect(**DB_CONFIG)
- try:
- with connection.cursor(pymysql.cursors.DictCursor) as cursor:
- dept_category = f"zy-{zybh}"
- sql = """
- SELECT dept_id as major_id, dept_name as major_name
- FROM sys_dept
- WHERE dept_name = %s AND dept_category = %s
- LIMIT 1
- """
- cursor.execute(sql, (zy_name, dept_category))
- result = cursor.fetchone()
- return result
- finally:
- connection.close()
- except Exception as e:
- print(f"✗ 查找专业信息失败: {e}")
- return None
-
- def find_college_info(self, major_id: int, cursor=None) -> Optional[Dict[str, Any]]:
- """
- 查找学院信息(college_id和college_name)
-
- 通过major_id找到parent_id,然后检查dept_category是否以"xy-"开头
-
- Args:
- major_id: 专业ID
- cursor: 数据库游标对象,如果提供则使用该游标,否则创建新连接
-
- Returns:
- 学院信息字典(包含college_id和college_name),如果未找到则返回None
- """
- if not major_id:
- return None
-
- try:
- if cursor is not None:
- # 使用提供的游标(普通游标,返回元组)
- # 第一步:通过major_id查找parent_id
- sql1 = """
- SELECT parent_id
- FROM sys_dept
- WHERE dept_id = %s
- LIMIT 1
- """
- cursor.execute(sql1, (major_id,))
- result1 = cursor.fetchone()
- if not result1 or not result1[0]:
- return None
-
- # 普通游标返回元组,取第一个元素(parent_id)
- parent_id = result1[0]
-
- # 第二步:查询parent_id对应的部门信息,检查dept_category是否以"xy-"开头
- # 根据专业信息API的代码,专业的parent_id应该就是学院的dept_id
- sql2 = """
- SELECT dept_id, dept_name, dept_category
- FROM sys_dept
- WHERE dept_id = %s
- LIMIT 1
- """
- cursor.execute(sql2, (parent_id,))
- result2 = cursor.fetchone()
- if result2:
- # 普通游标返回元组 (dept_id, dept_name, dept_category)
- dept_id = result2[0]
- dept_name = result2[1]
- dept_category = result2[2] if len(result2) > 2 else None
-
- # 检查dept_category是否以"xy-"开头(学院标识)
- if dept_category and dept_category.startswith('xy-'):
- return {
- 'college_id': dept_id,
- 'college_name': dept_name
- }
-
- return None
- else:
- # 创建新连接(使用DictCursor)
- connection = pymysql.connect(**DB_CONFIG)
- try:
- with connection.cursor(pymysql.cursors.DictCursor) as cursor:
- # 第一步:通过major_id查找parent_id
- sql1 = """
- SELECT parent_id
- FROM sys_dept
- WHERE dept_id = %s
- LIMIT 1
- """
- cursor.execute(sql1, (major_id,))
- result1 = cursor.fetchone()
- if not result1 or not result1.get('parent_id'):
- return None
-
- parent_id = result1.get('parent_id')
-
- # 第二步:查询parent_id对应的部门信息,检查dept_category是否以"xy-"开头
- sql2 = """
- SELECT dept_id, dept_name, dept_category
- FROM sys_dept
- WHERE dept_id = %s
- LIMIT 1
- """
- cursor.execute(sql2, (parent_id,))
- result2 = cursor.fetchone()
- if result2:
- dept_category = result2.get('dept_category')
- # 检查dept_category是否以"xy-"开头(学院标识)
- if dept_category and dept_category.startswith('xy-'):
- return {
- 'college_id': result2.get('dept_id'),
- 'college_name': result2.get('dept_name')
- }
-
- return None
- finally:
- connection.close()
- except Exception as e:
- print(f"✗ 查找学院信息失败: {e}")
- import traceback
- traceback.print_exc()
- return None
-
- def find_school_system(self, zybh: str, cursor=None) -> Optional[int]:
- """
- 查找学制(school_system)
-
- 通过bjxx_zybh关联cx_zysj表的zysj_zybh,获取cx_zysj表的zysj_xzm
-
- Args:
- zybh: 专业编号
- cursor: 数据库游标对象,如果提供则使用该游标,否则创建新连接
-
- Returns:
- 学制(年),如果未找到则返回None
- """
- if not zybh:
- return None
-
- try:
- if cursor is not None:
- # 使用提供的游标
- sql = """
- SELECT zysj_xzm
- FROM cx_zysj
- WHERE zysj_zybh = %s
- LIMIT 1
- """
- cursor.execute(sql, (zybh,))
- result = cursor.fetchone()
- if result and result[0]:
- # 尝试将zysj_xzm转换为整数
- try:
- school_system = int(result[0])
- return school_system
- except (ValueError, TypeError):
- # 如果无法转换为整数,尝试提取数字
- match = re.search(r'\d+', str(result[0]))
- if match:
- return int(match.group())
- return None
- return None
- else:
- # 创建新连接
- connection = pymysql.connect(**DB_CONFIG)
- try:
- with connection.cursor() as cursor:
- sql = """
- SELECT zysj_xzm
- FROM cx_zysj
- WHERE zysj_zybh = %s
- LIMIT 1
- """
- cursor.execute(sql, (zybh,))
- result = cursor.fetchone()
- if result and result[0]:
- # 尝试将zysj_xzm转换为整数
- try:
- school_system = int(result[0])
- return school_system
- except (ValueError, TypeError):
- # 如果无法转换为整数,尝试提取数字
- import re
- match = re.search(r'\d+', str(result[0]))
- if match:
- return int(match.group())
- return None
- return None
- finally:
- connection.close()
- except Exception as e:
- print(f"✗ 查找学制失败: {e}")
- return None
-
- def check_class_msg_exists(self, class_id: int, cursor=None) -> bool:
- """
- 检查班级信息是否已存在于xg_class_msg表
-
- Args:
- class_id: 班级ID(与sys_dept表的dept_id一致)
- cursor: 数据库游标对象,如果提供则使用该游标,否则创建新连接
-
- Returns:
- 是否存在
- """
- try:
- if cursor is not None:
- # 使用提供的游标
- sql = """
- SELECT COUNT(*) as count
- FROM xg_class_msg
- WHERE id = %s
- """
- cursor.execute(sql, (class_id,))
- result = cursor.fetchone()
- return result[0] > 0 if result else False
- else:
- # 创建新连接
- connection = pymysql.connect(**DB_CONFIG)
- try:
- with connection.cursor() as cursor:
- sql = """
- SELECT COUNT(*) as count
- FROM xg_class_msg
- WHERE id = %s
- """
- cursor.execute(sql, (class_id,))
- result = cursor.fetchone()
- return result[0] > 0 if result else False
- finally:
- connection.close()
- except Exception as e:
- print(f"✗ 检查班级信息是否存在失败: {e}")
- return False
-
- def insert_class_msg(self, class_data: Dict[str, Any], connection) -> bool:
- """
- 插入班级信息到xg_class_msg表
-
- Args:
- class_data: 班级数据字典
- connection: 数据库连接对象
-
- Returns:
- 是否成功
- """
- try:
- with connection.cursor() as cursor:
- insert_sql = """
- INSERT INTO xg_class_msg (
- id, tenant_id, create_dept, create_by, create_time,
- update_by, update_time, deleted,
- college_id, college_name, major_id, major_name,
- class_code, class_name, class_type, grade, enrollment_year,
- graduation_year, training_type, degree_type,
- counselor_id, counselor_name, class_teacher_id, class_teacher_name,
- class_advisor_id, class_advisor_name,
- class_monitor_id, class_monitor_name,
- deputy_monitor_id, deputy_monitor_name,
- class_status, is_graduation, graduation_date, school_system
- ) 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, %s,
- %s, %s, %s, %s
- )
- """
- cursor.execute(insert_sql, (
- class_data.get('id'), # id
- class_data.get('tenant_id'), # tenant_id
- class_data.get('create_dept'), # create_dept
- class_data.get('create_by'), # create_by
- class_data.get('create_time'), # create_time
- class_data.get('update_by'), # update_by
- class_data.get('update_time'), # update_time
- class_data.get('deleted'), # deleted
- class_data.get('college_id'), # college_id
- class_data.get('college_name'), # college_name
- class_data.get('major_id'), # major_id
- class_data.get('major_name'), # major_name
- class_data.get('class_code'), # class_code
- class_data.get('class_name'), # class_name
- class_data.get('class_type'), # class_type
- class_data.get('grade'), # grade
- class_data.get('enrollment_year'), # enrollment_year
- class_data.get('graduation_year'), # graduation_year
- class_data.get('training_type'), # training_type
- class_data.get('degree_type'), # degree_type
- class_data.get('counselor_id'), # counselor_id
- class_data.get('counselor_name'), # counselor_name
- class_data.get('class_teacher_id'), # class_teacher_id
- class_data.get('class_teacher_name'), # class_teacher_name
- class_data.get('class_advisor_id'), # class_advisor_id
- class_data.get('class_advisor_name'), # class_advisor_name
- class_data.get('class_monitor_id'), # class_monitor_id
- class_data.get('class_monitor_name'), # class_monitor_name
- class_data.get('deputy_monitor_id'), # deputy_monitor_id
- class_data.get('deputy_monitor_name'), # deputy_monitor_name
- class_data.get('class_status'), # class_status
- class_data.get('is_graduation'), # is_graduation
- class_data.get('graduation_date'), # graduation_date
- class_data.get('school_system') # school_system
- ))
- return True
- except Exception as e:
- print(f"✗ 插入班级信息到xg_class_msg表失败: {e}")
- return False
-
- def get_parent_ancestors(self, parent_id: int) -> str:
- """
- 获取父部门的ancestors
-
- Args:
- parent_id: 父部门ID
-
- Returns:
- ancestors字符串(格式:0,100,101,parent_id)
- """
- try:
- connection = pymysql.connect(**DB_CONFIG)
- try:
- with connection.cursor() as cursor:
- sql = """
- SELECT ancestors
- FROM sys_dept
- WHERE dept_id = %s
- LIMIT 1
- """
- cursor.execute(sql, (parent_id,))
- result = cursor.fetchone()
- if result and result[0]:
- # 父部门的ancestors + 父部门ID
- return f"{result[0]},{parent_id}"
- else:
- # 如果找不到父部门,返回默认值
- return f"0,100,101,{parent_id}"
- finally:
- connection.close()
- except Exception as e:
- print(f"✗ 获取父部门ancestors失败: {e}")
- return f"0,100,101,{parent_id}"
-
- def get_max_order_num(self, parent_id: int) -> int:
- """
- 获取指定父节点下的最大order_num
-
- Args:
- parent_id: 父部门ID
-
- Returns:
- 最大order_num,如果没有数据则返回0
- """
- try:
- connection = pymysql.connect(**DB_CONFIG)
- try:
- with connection.cursor() as cursor:
- sql = """
- SELECT MAX(order_num) as max_order
- FROM sys_dept
- WHERE parent_id = %s
- """
- cursor.execute(sql, (parent_id,))
- result = cursor.fetchone()
- return result[0] if result[0] is not None else 0
- finally:
- connection.close()
- except Exception as e:
- print(f"✗ 获取最大order_num失败: {e}")
- return 0
-
- def generate_dept_id(self, base_timestamp: str, index: int) -> int:
- """
- 生成dept_id
- 格式:当前时间戳(%Y%m%d%H%M%S)+ 0001开始递增
-
- Args:
- base_timestamp: 基础时间戳字符串(格式:%Y%m%d%H%M%S)
- index: 序号(从1开始)
-
- Returns:
- dept_id
- """
- # 序号格式化为4位数字(0001, 0002, ...)
- sequence = str(index).zfill(4)
- # 拼接:时间戳 + 序号
- dept_id_str = base_timestamp + sequence
- return int(dept_id_str)
-
- def sync_classes(self) -> Dict[str, Any]:
- """
- 同步班级数据到sys_dept表和xg_class_msg表
-
- Returns:
- 同步结果统计(包含成功、跳过、失败的班级编号列表)
- """
- # 获取符合条件的数据
- valid_classes = self.get_valid_classes()
- if not valid_classes:
- print("没有符合条件的数据需要同步")
- return {
- "total": 0,
- "inserted": 0,
- "skipped": 0,
- "failed": 0,
- "inserted_bjbh": [],
- "skipped_bjbh": [],
- "failed_bjbh": [],
- "class_msg_inserted": 0, # xg_class_msg表插入成功数
- "class_msg_failed": 0 # xg_class_msg表插入失败数
- }
-
- # 生成基础时间戳(用于生成dept_id)
- base_timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
-
- # 统计信息
- stats = {
- "total": len(valid_classes),
- "inserted": 0,
- "skipped": 0,
- "failed": 0,
- "inserted_bjbh": [], # 成功插入的班级编号列表
- "skipped_bjbh": [], # 跳过的班级编号列表
- "failed_bjbh": [], # 失败的班级编号列表
- "class_msg_inserted": 0, # xg_class_msg表插入成功数
- "class_msg_failed": 0 # xg_class_msg表插入失败数
- }
-
- # 当前时间
- current_time = datetime.now()
- current_year = current_time.year
-
- # 用于记录每个parent_id对应的最大order_num和当前order_num
- parent_order_map = {}
-
- try:
- connection = pymysql.connect(**DB_CONFIG)
- try:
- with connection.cursor() as cursor:
- # 准备插入sys_dept的SQL
- insert_dept_sql = """
- INSERT INTO sys_dept (
- dept_id, tenant_id, parent_id, ancestors, dept_name, dept_category,
- order_num, leader, phone, email, status, del_flag,
- create_dept, create_by, create_time, update_by, update_time
- ) VALUES (
- %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
- )
- """
-
- # 用于生成dept_id的序号计数器(只对实际插入的数据计数)
- insert_index = 0
-
- # 遍历数据,逐个处理
- for class_info in valid_classes:
- bjxx_bjmc = class_info.get('bjxx_bjmc')
- bjxx_bjbh = class_info.get('bjxx_bjbh')
- bjxx_zy = class_info.get('bjxx_zy')
- bjxx_zybh = class_info.get('bjxx_zybh')
- bjxx_rxnf = class_info.get('bjxx_rxnf') # 入学年份
-
- # 验证数据
- if not bjxx_bjmc:
- print(f"⚠ 跳过:班级名称为空 (bjxx_bjbh: {bjxx_bjbh})")
- stats["skipped"] += 1
- if bjxx_bjbh:
- stats["skipped_bjbh"].append(bjxx_bjbh)
- continue
-
- if not bjxx_bjbh:
- print(f"⚠ 跳过:班级编号为空 (bjxx_bjmc: {bjxx_bjmc})")
- stats["skipped"] += 1
- # 班级编号为空,无法记录
- continue
-
- # 检查dept_name长度(sys_dept表中dept_name是varchar(30))
- dept_name = bjxx_bjmc
- if len(dept_name) > 30:
- print(f"⚠ 警告:班级名称超过30字符,将被截断: {dept_name} (长度: {len(dept_name)})")
- dept_name = dept_name[:30]
-
- # 构建dept_category
- dept_category = f"bj-{bjxx_bjbh}"
-
- # 检查是否已存在
- if self.check_class_exists(dept_name, dept_category):
- print(f"⏭ 跳过已存在的班级: {dept_name} (dept_category: {dept_category})")
- stats["skipped"] += 1
- stats["skipped_bjbh"].append(bjxx_bjbh)
- continue
-
- # 查找父部门ID(专业)
- parent_id = self.find_parent_dept_id(bjxx_zy, bjxx_zybh)
- if parent_id is None:
- print(f"⚠ 跳过:未找到父部门(专业): {bjxx_zy} (bjxx_zybh: {bjxx_zybh}, bjxx_bjbh: {bjxx_bjbh})")
- stats["skipped"] += 1
- stats["skipped_bjbh"].append(bjxx_bjbh)
- continue
-
- # 获取或初始化该parent_id的order_num
- if parent_id not in parent_order_map:
- max_order = self.get_max_order_num(parent_id)
- parent_order_map[parent_id] = max_order
- parent_order_map[parent_id] += 1
- current_order_num = parent_order_map[parent_id]
-
- # 获取父部门的ancestors
- ancestors = self.get_parent_ancestors(parent_id)
-
- # 递增插入序号(只对实际插入的数据计数)
- insert_index += 1
-
- # 生成dept_id
- dept_id = self.generate_dept_id(base_timestamp, insert_index)
-
- # 准备插入sys_dept数据
- try:
- cursor.execute(insert_dept_sql, (
- dept_id, # dept_id
- self.tenant_id, # tenant_id
- parent_id, # parent_id
- ancestors, # ancestors
- dept_name, # dept_name
- dept_category, # dept_category
- current_order_num, # order_num
- None, # leader
- None, # phone
- None, # email
- '0', # status
- '0', # del_flag
- 0, # create_dept
- 1, # create_by
- current_time, # create_time
- 1, # update_by
- current_time # update_time
- ))
- stats["inserted"] += 1
- stats["inserted_bjbh"].append(bjxx_bjbh)
- print(f"✓ 插入班级到sys_dept: {dept_name} (dept_id: {dept_id}, dept_category: {dept_category}, parent_id: {parent_id})")
-
- # ========== 插入到xg_class_msg表 ==========
- # 检查xg_class_msg表是否已存在
- if not self.check_class_msg_exists(dept_id, cursor):
- # 查找专业信息
- major_info = self.find_major_info(bjxx_zy, bjxx_zybh, cursor)
- if not major_info:
- print(f"⚠ 警告:未找到专业信息,跳过xg_class_msg插入: {bjxx_zy} (bjxx_zybh: {bjxx_zybh})")
- stats["class_msg_failed"] += 1
- else:
- major_id = major_info.get('major_id')
- major_name = major_info.get('major_name')
-
- # 查找学院信息
- college_info = self.find_college_info(major_id, cursor)
- if college_info:
- college_id = college_info.get('college_id')
- college_name = college_info.get('college_name')
- else:
- college_id = None
- college_name = None
- print(f" ⚠ 警告:未找到学院信息 (major_id: {major_id}, 专业: {major_name})")
-
- # 查找学制
- school_system = self.find_school_system(bjxx_zybh, cursor)
-
- # 处理入学年份
- enrollment_year = current_year
- grade = str(current_year)
- if bjxx_rxnf:
- try:
- enrollment_year = int(str(bjxx_rxnf).strip())
- grade = str(enrollment_year)
- except (ValueError, TypeError):
- # 如果无法转换为整数,使用当前年
- enrollment_year = current_year
- grade = str(current_year)
-
- # 构建xg_class_msg表的数据
- class_msg_data = {
- 'id': dept_id, # id(与sys_dept表的dept_id一致)
- 'tenant_id': self.tenant_id, # tenant_id
- 'create_dept': 0, # create_dept
- 'create_by': 1, # create_by
- 'create_time': current_time, # create_time
- 'update_by': 1, # update_by
- 'update_time': current_time, # update_time
- 'deleted': 0, # deleted
- 'college_id': college_id, # college_id
- 'college_name': college_name, # college_name
- 'major_id': major_id, # major_id
- 'major_name': major_name, # major_name
- 'class_code': bjxx_bjbh, # class_code
- 'class_name': bjxx_bjmc, # class_name
- 'class_type': None, # class_type(为空)
- 'grade': grade, # grade
- 'enrollment_year': enrollment_year, # enrollment_year
- 'graduation_year': None, # graduation_year(为空)
- 'training_type': None, # training_type(为空)
- 'degree_type': None, # degree_type(为空)
- 'counselor_id': None, # counselor_id(为空)
- 'counselor_name': None, # counselor_name(为空)
- 'class_teacher_id': None, # class_teacher_id(为空)
- 'class_teacher_name': None, # class_teacher_name(为空)
- 'class_advisor_id': None, # class_advisor_id(为空)
- 'class_advisor_name': None, # class_advisor_name(为空)
- 'class_monitor_id': None, # class_monitor_id(为空)
- 'class_monitor_name': None, # class_monitor_name(为空)
- 'deputy_monitor_id': None, # deputy_monitor_id(为空)
- 'deputy_monitor_name': None, # deputy_monitor_name(为空)
- 'class_status': '1', # class_status(1)
- 'is_graduation': 0, # is_graduation(0)
- 'graduation_date': None, # graduation_date(为空)
- 'school_system': school_system # school_system
- }
-
- # 插入到xg_class_msg表
- if self.insert_class_msg(class_msg_data, connection):
- stats["class_msg_inserted"] += 1
- print(f" ✓ 插入班级到xg_class_msg: {bjxx_bjmc} (id: {dept_id}, major_id: {major_id}, college_id: {college_id})")
- else:
- stats["class_msg_failed"] += 1
- print(f" ✗ 插入班级到xg_class_msg失败: {bjxx_bjmc}")
- else:
- print(f" ⏭ 跳过已存在的xg_class_msg记录: {bjxx_bjmc} (id: {dept_id})")
-
- except Exception as e:
- stats["failed"] += 1
- stats["failed_bjbh"].append(bjxx_bjbh)
- print(f"✗ 插入班级失败: {dept_name}, 错误: {e}")
-
- # 提交事务
- connection.commit()
- print(f"\n✓ 事务提交成功")
-
- finally:
- connection.close()
- except Exception as e:
- print(f"✗ 同步过程发生错误: {e}")
- stats["failed"] = stats["total"] - stats["inserted"] - stats["skipped"]
-
- return stats
-
- def print_statistics(self, stats: Dict[str, Any]):
- """
- 打印统计信息
-
- Args:
- stats: 统计信息字典(包含班级编号列表)
- """
- print("\n" + "=" * 60)
- print("同步统计信息")
- print("=" * 60)
- print(f"总数据量: {stats['total']}")
- print(f"sys_dept表 - 成功插入: {stats['inserted']}")
- print(f"sys_dept表 - 跳过(已存在/未找到父部门): {stats['skipped']}")
- print(f"sys_dept表 - 失败: {stats['failed']}")
- print(f"xg_class_msg表 - 成功插入: {stats.get('class_msg_inserted', 0)}")
- print(f"xg_class_msg表 - 失败: {stats.get('class_msg_failed', 0)}")
- print("=" * 60)
-
- # 输出班级编号列表(逗号分隔)
- if stats.get('inserted_bjbh'):
- inserted_bjbh_str = ','.join(stats['inserted_bjbh'])
- print(f"\n成功插入的班级编号: {inserted_bjbh_str}")
-
- if stats.get('skipped_bjbh'):
- skipped_bjbh_str = ','.join(stats['skipped_bjbh'])
- print(f"跳过的班级编号: {skipped_bjbh_str}")
-
- if stats.get('failed_bjbh'):
- failed_bjbh_str = ','.join(stats['failed_bjbh'])
- print(f"失败的班级编号: {failed_bjbh_str}")
-
- print("=" * 60)
- def main():
- """主函数 - 完整同步流程"""
- # API配置信息
- APP_ID = "73462569-d0da-4a05-bd6a-a9077c723dba"
- APP_KEY = "dyjs_bjxx"
- FID = "19628" # 单位ID
-
- print("=" * 60)
- print("超星班级信息完整同步流程")
- print("=" * 60)
- print("流程:超星API -> cx_bjsj表 -> sys_dept表")
- print("=" * 60)
-
- # ========== 第一阶段:从超星API获取数据并保存到cx_bjsj表 ==========
- print("\n" + "=" * 60)
- print("第一阶段:从超星API获取班级信息")
- print("=" * 60)
-
- # 创建API客户端
- api = ChaoxingClassAPI(app_id=APP_ID, app_key=APP_KEY, fid=FID)
-
- # 步骤1: 清空表
- print("\n步骤1: 清空班级数据表(cx_bjsj)...")
- api.clear_table()
-
- # 步骤2: 获取所有数据
- print("\n步骤2: 获取所有班级信息(自动分页)...")
- all_classes = api.get_all_classes(max_page_size=500)
-
- # 步骤3: 保存到数据库
- if all_classes:
- print(f"\n步骤3: 保存数据到cx_bjsj表...")
- saved_count = api.save_to_database(all_classes)
- print(f"\n✓ 第一阶段完成!共获取 {len(all_classes)} 条数据,成功保存 {saved_count} 条到cx_bjsj表")
- else:
- print("\n✗ 未获取到数据,终止流程")
- return
-
- # ========== 第二阶段:从cx_bjsj表同步到sys_dept表 ==========
- print("\n" + "=" * 60)
- print("第二阶段:从cx_bjsj表同步到sys_dept表")
- print("=" * 60)
- print(f"筛选条件: bjxx_sfqy='是'")
- print(f"判断存在: dept_category='bj-' + bjxx_bjbh AND dept_name=bjxx_bjmc")
- print(f"父部门查找: dept_category='zy-' + bjxx_zybh AND dept_name=bjxx_zy")
- print("=" * 60)
-
- # 创建同步实例
- sync = ClassSync()
-
- # 执行同步
- stats = sync.sync_classes()
-
- # 打印统计信息
- sync.print_statistics(stats)
-
- # ========== 总结 ==========
- print("\n" + "=" * 60)
- print("完整同步流程总结")
- print("=" * 60)
- print(f"✓ 从超星API获取: {len(all_classes)} 条数据")
- print(f"✓ 保存到cx_bjsj表: {saved_count} 条数据")
- print(f"✓ 同步到sys_dept表: 插入 {stats['inserted']} 条,跳过 {stats['skipped']} 条,失败 {stats['failed']} 条")
- print(f"✓ 同步到xg_class_msg表: 插入 {stats.get('class_msg_inserted', 0)} 条,失败 {stats.get('class_msg_failed', 0)} 条")
- print("=" * 60)
-
- if stats["inserted"] > 0:
- print("\n✓ 完整同步流程执行成功!")
- elif stats["skipped"] == stats["total"]:
- print("\n✓ 所有数据已存在,无需同步")
- else:
- print("\n⚠ 同步过程中出现问题,请检查日志")
- if __name__ == "__main__":
- main()
|