QuecCloudObject.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. # -*- encoding: utf-8 -*-
  2. """
  3. @File : QuecCloudObject.py
  4. @Time : 2025/11/13 14:31
  5. @Author : stephen
  6. @Email : zhangdongming@asj6.wecom.work
  7. @Software: PyCharm
  8. """
  9. import hashlib
  10. import logging
  11. # 1. 标准库导入
  12. import time
  13. from typing import Dict, Any, Optional, Final, List
  14. # 2. 第三方库导入
  15. import requests
  16. from requests.exceptions import RequestException
  17. # 3. 本地应用导入(假设日志配置在项目config模块中)
  18. from Ansjer.config import LOGGER
  19. # 类型注解日志对象
  20. LOGGER: logging.Logger
  21. class QuecCloudApiClient:
  22. """
  23. 移远通信安防按量计费OpenAPI工具类
  24. 封装系统级参数处理、签名算法及流量池相关业务查询接口
  25. 遵循项目代码规范,支持可扩展、可复用的API调用
  26. """
  27. # 基础配置常量(全大写蛇形命名)
  28. BASE_URL: Final[str] = "https://api.quectel.com/openapi/router"
  29. TIME_DIFF_THRESHOLD: Final[int] = 300 # UTC时间戳最大误差(5分钟)
  30. DEFAULT_TIMEOUT: Final[int] = 10 # 请求超时时间(秒)
  31. MAX_PAGE_SIZE: Final[int] = 100 # 分页查询最大每页条数
  32. def __init__(self, app_key: str, secret: str):
  33. """
  34. 初始化API客户端
  35. Args:
  36. app_key (str): 应用键(从移远平台API管理页面获取)
  37. secret (str): 应用密钥(与appKey对应,需妥善保管)
  38. """
  39. self.app_key = app_key
  40. self.secret = secret
  41. self.session = requests.Session() # 复用会话提升性能
  42. def _generate_sign(self, params: Dict[str, Any]) -> str:
  43. """
  44. 生成签名串(遵循文档1.6签名算法规范)
  45. Args:
  46. params (Dict[str, Any]): 待签名的所有参数(系统级+业务级)
  47. Returns:
  48. str: 十六进制SHA1签名字符串
  49. Raises:
  50. ValueError: 当参数为空或签名过程失败时抛出
  51. """
  52. try:
  53. # 1. 过滤空值参数,按参数名ASCII升序排列
  54. # 注意:必须将所有参数值转换为字符串,并过滤None值
  55. filtered_params = {}
  56. for k, v in params.items():
  57. if v is not None:
  58. # 将所有值转换为字符串
  59. filtered_params[k] = str(v)
  60. # 按参数名升序排序
  61. sorted_params = sorted(filtered_params.items(), key=lambda x: x[0])
  62. # 2. 拼接参数名和参数值
  63. param_str = "".join([f"{k}{v}" for k, v in sorted_params])
  64. # 3. 首尾添加secret
  65. sign_str = f"{self.secret}{param_str}{self.secret}"
  66. # 调试:打印签名相关信息
  67. LOGGER.debug(f"参与签名的参数: {sorted_params}")
  68. LOGGER.debug(f"签名字符串: {sign_str}")
  69. # 4. SHA1加密并转为十六进制字符串
  70. sha1 = hashlib.sha1()
  71. sha1.update(sign_str.encode("UTF-8"))
  72. signature = sha1.hexdigest().upper()
  73. LOGGER.debug(f"生成的签名: {signature}")
  74. return signature
  75. except Exception as e:
  76. error_msg = f"签名生成失败: {str(e)}"
  77. LOGGER.error(f"{error_msg} 行号: {e.__traceback__.tb_lineno}")
  78. raise ValueError(error_msg) from e
  79. def _get_utc_timestamp(self) -> int:
  80. """
  81. 获取当前UTC时间戳(秒),确保与服务器时间误差不超过5分钟
  82. Returns:
  83. int: UTC时间戳
  84. Raises:
  85. RuntimeError: 当系统时间异常时抛出
  86. """
  87. timestamp = int(time.time())
  88. # 此处可添加时间校准逻辑(如对接NTP服务器),避免本地时间偏差
  89. return timestamp
  90. def _send_request(self, method: str, business_params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
  91. """
  92. 发送API请求(统一请求处理逻辑)
  93. Args:
  94. method (str): 服务方法名(如fc.function.pool.package.getall)
  95. business_params (Optional[Dict[str, Any]]): 业务级参数,可选
  96. Returns:
  97. Dict[str, Any]: 接口返回的JSON数据
  98. Raises:
  99. RequestException: 网络请求异常时抛出
  100. ValueError: 接口返回错误时抛出
  101. """
  102. # 1. 构建系统级参数
  103. system_params = {
  104. "appKey": self.app_key,
  105. "t": self._get_utc_timestamp(),
  106. "method": method
  107. # 注意:sign参数不参与签名生成
  108. }
  109. # 2. 合并所有参数(过滤空值)
  110. all_params = system_params.copy()
  111. if business_params:
  112. # 过滤掉业务参数中的None值
  113. all_params.update({k: v for k, v in business_params.items() if v is not None})
  114. # 3. 生成签名(使用所有参数,但不包括sign本身)
  115. sign = self._generate_sign(all_params)
  116. # 4. 将签名添加到请求参数中
  117. all_params["sign"] = sign
  118. LOGGER.info(f"API请求参数 - 方法: {method}, 时间戳: {all_params['t']}")
  119. LOGGER.debug(f"完整请求参数: {all_params}")
  120. try:
  121. # 5. 发送POST请求(支持form-data格式)
  122. response = self.session.post(
  123. url=self.BASE_URL,
  124. data=all_params,
  125. timeout=self.DEFAULT_TIMEOUT,
  126. headers={"Content-Type": "application/x-www-form-urlencoded"}
  127. )
  128. response.raise_for_status() # 抛出HTTP状态码异常
  129. result = response.json()
  130. LOGGER.info(f"API响应结果 - 方法: {method}, 状态: {result.get('resultCode')}")
  131. # 6. 校验接口返回状态
  132. result_code = result.get("resultCode", -1)
  133. if result_code != 0:
  134. error_msg = f"接口调用失败 - 错误码: {result_code}, 错误信息: {result.get('errorMessage', '未知错误')}"
  135. LOGGER.error(error_msg)
  136. raise ValueError(error_msg)
  137. return result
  138. except RequestException as e:
  139. error_msg = f"网络请求异常 - 方法: {method}, 错误: {str(e)}"
  140. LOGGER.error(f"{error_msg} 行号: {e.__traceback__.tb_lineno}")
  141. raise RequestException(error_msg) from e
  142. except ValueError as e:
  143. # 已记录日志,直接向上抛出
  144. raise
  145. except Exception as e:
  146. error_msg = f"请求处理异常 - 方法: {method}, 错误: {str(e)}"
  147. LOGGER.error(f"{error_msg} 行号: {e.__traceback__.tb_lineno}")
  148. raise RuntimeError(error_msg) from e
  149. def query_enterprise_pool_packages(self) -> List[Dict[str, Any]]:
  150. """
  151. 3.1.1 企业套餐查询接口
  152. 查询企业与移远约定的流量池计费套餐列表
  153. Returns:
  154. List[Dict[str, Any]]: 套餐信息列表,包含id、name、flowsize等字段
  155. Example:
  156. >>> client = QuecCloudApiClient("appKey", "secret")
  157. >>> packages = client.query_enterprise_pool_packages()
  158. >>> print(packages[0]["name"])
  159. "100G/360天"
  160. """
  161. method = "fc.function.pool.package.getall"
  162. result = self._send_request(method=method)
  163. # 按文档规范,返回数据在data字段中
  164. return result.get("data", [])
  165. def query_enterprise_pools(
  166. self,
  167. msisdn: Optional[str] = None,
  168. iccid: Optional[str] = None,
  169. poolcode: Optional[str] = None,
  170. page_no: Optional[int] = 1,
  171. page_size: Optional[int] = 10
  172. ) -> Dict[str, Any]:
  173. """
  174. 3.1.2 企业流量池查询接口
  175. 查询企业账户下的流量池列表,支持多条件过滤和分页
  176. Args:
  177. msisdn (Optional[str]): 物联卡号码,与iccid二选一
  178. iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
  179. poolcode (Optional[str]): 指定流量池编号,精准查询
  180. page_no (Optional[int]): 分页页码,默认1
  181. page_size (Optional[int]): 分页大小,默认10,最大100
  182. Returns:
  183. Dict[str, Any]: 流量池查询结果,包含分页信息和流量池列表
  184. Raises:
  185. ValueError: 当分页参数无效时抛出
  186. Example:
  187. >>> client = QuecCloudApiClient("appKey", "secret")
  188. >>> result = client.query_enterprise_pools(poolcode="PC1629946707")
  189. >>> print(result["data"][0]["poolname"])
  190. "测试流量池"
  191. """
  192. # 参数校验
  193. if page_size and page_size > self.MAX_PAGE_SIZE:
  194. raise ValueError(f"分页大小不能超过{self.MAX_PAGE_SIZE}")
  195. # 构建业务参数
  196. business_params = {
  197. "msisdn": msisdn,
  198. "iccid": iccid,
  199. "poolcode": poolcode,
  200. "pageNo": page_no,
  201. "pageSize": page_size
  202. }
  203. method = "fc.function.pool.list"
  204. result = self._send_request(method=method, business_params=business_params)
  205. return result
  206. def query_single_card_info(
  207. self,
  208. msisdn: Optional[str] = None,
  209. iccid: Optional[str] = None
  210. ) -> Dict[str, Any]:
  211. """
  212. 4.1.1 单卡信息查询接口
  213. 查询物联卡的详细信息,包括基本状态、流量使用、定向信息等
  214. Args:
  215. msisdn (Optional[str]): 物联卡号码,与iccid二选一
  216. iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
  217. Returns:
  218. Dict[str, Any]: 单卡详细信息
  219. Raises:
  220. ValueError: 当msisdn和iccid都未提供时抛出
  221. Example:
  222. >>> client = QuecCloudApiClient("appKey", "secret")
  223. >>> card_info = client.query_single_card_info(iccid="89860446091891282224")
  224. >>> print(card_info["msisdn"], card_info["status"])
  225. """
  226. if not msisdn and not iccid:
  227. raise ValueError("msisdn和iccid参数必须至少提供一个")
  228. business_params = {
  229. "msisdn": msisdn,
  230. "iccid": iccid
  231. }
  232. method = "fc.function.card.info"
  233. result = self._send_request(method=method, business_params=business_params)
  234. # 移除resultCode和errorMessage,直接返回数据主体
  235. result.pop("resultCode", None)
  236. result.pop("errorMessage", None)
  237. return result
  238. def query_single_card_realtimestatus(
  239. self,
  240. msisdn: Optional[str] = None,
  241. iccid: Optional[str] = None
  242. ) -> Dict[str, Any]:
  243. """
  244. 4.1.8.资产实时状态
  245. 查询资产的实时状态
  246. Args:
  247. msisdn (Optional[str]): 物联卡号码,与iccid二选一
  248. iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
  249. Returns:
  250. Dict[str, Any]: 单卡详细信息
  251. Raises:
  252. ValueError: 当msisdn和iccid都未提供时抛出
  253. """
  254. if not msisdn and not iccid:
  255. raise ValueError("msisdn和iccid参数必须至少提供一个")
  256. business_params = {
  257. "msisdn": msisdn,
  258. "iccid": iccid
  259. }
  260. method = "fc.function.card.realtimestatus"
  261. result = self._send_request(method=method, business_params=business_params)
  262. # 移除resultCode和errorMessage,直接返回数据主体
  263. result.pop("resultCode", None)
  264. result.pop("errorMessage", None)
  265. return result
  266. def query_section_day_flow(
  267. self,
  268. section: Optional[str] = None,
  269. msisdn: Optional[str] = None,
  270. iccid: Optional[str] = None
  271. ) -> Dict[str, Any]:
  272. """
  273. 4.1.7.单卡日流量查询
  274. 资产指定时间段的流量查询
  275. Args:
  276. section (Optional[str]): 查询时间段,20201020
  277. msisdn (Optional[str]): 物联卡号码,与iccid二选一
  278. iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
  279. Returns:
  280. Dict[str, Any]: 单卡详细信息
  281. Raises:
  282. ValueError: 当msisdn和iccid都未提供时抛出
  283. """
  284. if not section:
  285. raise ValueError("section参数必须提供")
  286. if not msisdn and not iccid:
  287. raise ValueError("msisdn和iccid参数必须至少提供一个")
  288. business_params = {
  289. "section": section,
  290. "msisdn": msisdn,
  291. "iccid": iccid
  292. }
  293. method = "fc.function.billing.section.dayflow"
  294. result = self._send_request(method=method, business_params=business_params)
  295. # 移除resultCode和errorMessage,直接返回数据主体
  296. result.pop("resultCode", None)
  297. result.pop("errorMessage", None)
  298. return result
  299. def query_orientation_info(
  300. self,
  301. strategycode: Optional[str] = None
  302. ) -> List[Dict[str, Any]]:
  303. """
  304. 4.1.11 定向信息查询接口
  305. 查询企业名下的定向策略信息
  306. Args:
  307. strategycode (Optional[str]): 定向策略编号,不传则查询所有定向信息
  308. Returns:
  309. List[Dict[str, Any]]: 定向策略信息列表
  310. Example:
  311. >>> client = QuecCloudApiClient("appKey", "secret")
  312. >>> strategies = client.query_orientation_info()
  313. >>> print(f"查询到{len(strategies)}个定向策略")
  314. """
  315. business_params = {
  316. "strategycode": strategycode
  317. }
  318. method = "fc.function.orientation.info"
  319. result = self._send_request(method=method, business_params=business_params)
  320. return result.get("data", [])
  321. def manage_card_flow(
  322. self,
  323. msisdn: Optional[str] = None,
  324. iccid: Optional[str] = None,
  325. notify_emails: Optional[str] = None,
  326. alarm_threshold1: Optional[str] = None,
  327. alarm_threshold2: Optional[str] = None,
  328. alarm_threshold3: Optional[str] = None,
  329. alarm_type: Optional[int] = None,
  330. alarm_status: Optional[int] = None,
  331. pause_flow_threshold: Optional[int] = None,
  332. pause_status: Optional[int] = None,
  333. pause_type: Optional[int] = None
  334. ) -> bool:
  335. """
  336. 4.2.1 单卡流量管理接口
  337. 支持对单卡的流量使用设置预警、阈值,达到预警进行告警,达到阈值进行停机
  338. Args:
  339. msisdn (Optional[str]): 物联卡号码,与iccid二选一
  340. iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
  341. notify_emails (Optional[str]): 通知邮箱,多个邮箱以英文逗号隔开
  342. alarm_threshold1 (Optional[str]): 预警阶段1
  343. alarm_threshold2 (Optional[str]): 预警阶段2
  344. alarm_threshold3 (Optional[str]): 预警阶段3
  345. alarm_type (Optional[int]): 预警阶段类型,1百分比,2数值
  346. alarm_status (Optional[int]): 预警启用状态,0停用,1启用
  347. pause_flow_threshold (Optional[int]): 停机阈值(MB)
  348. pause_status (Optional[int]): 停机阈值启用状态,0停用,1启用
  349. pause_type (Optional[int]): 停机类型,1当月流量,2当月流量池内流量
  350. Returns:
  351. bool: 操作是否成功
  352. Raises:
  353. ValueError: 当msisdn和iccid都未提供时抛出
  354. Example:
  355. >>> client = QuecCloudApiClient("appKey", "secret")
  356. >>> success = client.manage_card_flow(
  357. ... iccid="89860446091891282224",
  358. ... notify_emails="admin@example.com",
  359. ... alarm_threshold1="1024",
  360. ... alarm_type=2,
  361. ... alarm_status=1,
  362. ... pause_flow_threshold=20480,
  363. ... pause_status=1,
  364. ... pause_type=1
  365. ... )
  366. """
  367. if not msisdn and not iccid:
  368. raise ValueError("msisdn和iccid参数必须至少提供一个")
  369. # 构建流量管理参数JSON
  370. param_json = {}
  371. if notify_emails is not None:
  372. param_json["notifyemails"] = notify_emails
  373. if alarm_threshold1 is not None:
  374. param_json["alarmthreshold1"] = alarm_threshold1
  375. if alarm_threshold2 is not None:
  376. param_json["alarmthreshold2"] = alarm_threshold2
  377. if alarm_threshold3 is not None:
  378. param_json["alarmthreshold3"] = alarm_threshold3
  379. if alarm_type is not None:
  380. param_json["alarmtype"] = alarm_type
  381. if alarm_status is not None:
  382. param_json["alarmstatus"] = alarm_status
  383. if pause_flow_threshold is not None:
  384. param_json["pauseflowthreshold"] = pause_flow_threshold
  385. if pause_status is not None:
  386. param_json["pausestatus"] = pause_status
  387. if pause_type is not None:
  388. param_json["pausetype"] = pause_type
  389. import json
  390. param_json_str = json.dumps(param_json, ensure_ascii=False)
  391. business_params = {
  392. "msisdn": msisdn,
  393. "iccid": iccid,
  394. "paramJson": param_json_str
  395. }
  396. method = "fc.function.billing.card.alarm.new"
  397. self._send_request(method=method, business_params=business_params)
  398. return True
  399. def suspend_single_card(
  400. self,
  401. msisdn: Optional[str] = None,
  402. iccid: Optional[str] = None
  403. ) -> bool:
  404. """
  405. 4.2.2 单卡停机接口
  406. 主动停机处于正常状态的卡
  407. Args:
  408. msisdn (Optional[str]): 物联卡号码,与iccid二选一
  409. iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
  410. Returns:
  411. bool: 操作是否成功
  412. Raises:
  413. ValueError: 当msisdn和iccid都未提供时抛出
  414. Example:
  415. >>> client = QuecCloudApiClient("appKey", "secret")
  416. >>> success = client.suspend_single_card(iccid="89860446091891282224")
  417. """
  418. if not msisdn and not iccid:
  419. raise ValueError("msisdn和iccid参数必须至少提供一个")
  420. business_params = {
  421. "msisdn": msisdn,
  422. "iccid": iccid
  423. }
  424. method = "fc.function.card.pause"
  425. self._send_request(method=method, business_params=business_params)
  426. return True
  427. def resume_single_card(
  428. self,
  429. msisdn: Optional[str] = None,
  430. iccid: Optional[str] = None
  431. ) -> bool:
  432. """
  433. 4.2.3 单卡复机接口
  434. 主动复机处于停机状态的卡
  435. Args:
  436. msisdn (Optional[str]): 物联卡号码,与iccid二选一
  437. iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
  438. Returns:
  439. bool: 操作是否成功
  440. Raises:
  441. ValueError: 当msisdn和iccid都未提供时抛出
  442. Example:
  443. >>> client = QuecCloudApiClient("appKey", "secret")
  444. >>> success = client.resume_single_card(iccid="89860446091891282224")
  445. """
  446. if not msisdn and not iccid:
  447. raise ValueError("msisdn和iccid参数必须至少提供一个")
  448. business_params = {
  449. "msisdn": msisdn,
  450. "iccid": iccid
  451. }
  452. method = "fc.function.card.resume"
  453. self._send_request(method=method, business_params=business_params)
  454. return True
  455. def close_gprs_service(self, msisdn: Optional[str] = None, iccid: Optional[str] = None) -> bool:
  456. """
  457. 4.2.4 单卡GPRS关闭接口
  458. 主动关闭物联卡的GPRS(数据服务)。只能处理隶属于该企业下的卡。
  459. Args:
  460. msisdn (Optional[str]): 物联卡号码,与iccid任选其一。
  461. iccid (Optional[str]): 集成电路卡识别码,与msisdn任选其一。
  462. Returns:
  463. Dict[str, Any]: 接口返回的JSON数据,包含resultCode和errorMessage字段。
  464. Raises:
  465. ValueError: 当msisdn和iccid均为空时抛出。
  466. """
  467. try:
  468. if not msisdn and not iccid:
  469. raise ValueError("必须提供msisdn或iccid参数")
  470. method = "fc.function.card.gprs.close"
  471. business_params = {}
  472. if msisdn:
  473. business_params["msisdn"] = msisdn
  474. if iccid:
  475. business_params["iccid"] = iccid
  476. self._send_request(method=method, business_params=business_params)
  477. return True
  478. except Exception as e:
  479. LOGGER.error(f"{iccid}关闭GPRS服务失败: {str(e)}")
  480. return False
  481. def open_gprs_service(self, msisdn: Optional[str] = None, iccid: Optional[str] = None) -> bool:
  482. """
  483. 4.2.5 单卡GPRS开启接口
  484. 主动开启物联卡的GPRS(数据服务)。只能处理隶属于该企业下的卡。
  485. Args:
  486. msisdn (Optional[str]): 物联卡号码,与iccid任选其一。
  487. iccid (Optional[str]): 集成电路卡识别码,与msisdn任选其一。
  488. Returns:
  489. Dict[str, Any]: 接口返回的JSON数据,包含resultCode和errorMessage字段。
  490. Raises:
  491. ValueError: 当msisdn和iccid均为空时抛出。
  492. """
  493. try:
  494. if not msisdn and not iccid:
  495. raise ValueError("必须提供msisdn或iccid参数")
  496. method = "fc.function.card.gprs.open"
  497. business_params = {}
  498. if msisdn:
  499. business_params["msisdn"] = msisdn
  500. if iccid:
  501. business_params["iccid"] = iccid
  502. self._send_request(method=method, business_params=business_params)
  503. return True
  504. except Exception as e:
  505. LOGGER.error(f"{iccid}开启GPRS服务失败: {str(e)}")
  506. return False
  507. def activate_flow_card(
  508. self,
  509. msisdn: Optional[str] = None,
  510. iccid: Optional[str] = None
  511. ) -> bool:
  512. """
  513. 4.2.6 流量卡激活接口
  514. 激活名下的物联卡(同一卡号30分钟内不能重复办理)
  515. Args:
  516. msisdn (Optional[str]): 物联卡号码,与iccid二选一
  517. iccid (Optional[str]): 集成电路卡识别码,与msisdn二选一
  518. Returns:
  519. bool: 操作是否成功
  520. Raises:
  521. ValueError: 当msisdn和iccid都未提供时抛出
  522. Example:
  523. >>> client = QuecCloudApiClient("appKey", "secret")
  524. >>> success = client.activate_flow_card(iccid="89860446091891282224")
  525. """
  526. if not msisdn and not iccid:
  527. raise ValueError("msisdn和iccid参数必须至少提供一个")
  528. business_params = {
  529. "msisdn": msisdn,
  530. "iccid": iccid
  531. }
  532. method = "fc.function.card.activate"
  533. self._send_request(method=method, business_params=business_params)
  534. return True