base.py 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237
  1. # -*- coding: utf-8 -*-
  2. # @Author : YY
  3. from io import BufferedReader, BytesIO
  4. import io
  5. import os,socket,threading,re,base64,ipaddress,math,psutil
  6. from zipfile import is_zipfile
  7. import time
  8. from typing import Callable, List, Literal, Optional, Type, get_args, \
  9. get_origin
  10. from datetime import datetime
  11. from openpyxl import Workbook, load_workbook
  12. from openpyxl.worksheet.worksheet import Worksheet
  13. from openpyxl.styles import PatternFill
  14. from pydantic import BaseModel
  15. from werkzeug.exceptions import NotFound
  16. from werkzeug.datastructures import FileStorage
  17. from werkzeug.utils import secure_filename
  18. from flask import Response, request
  19. from jwt import api_jwt
  20. from logging import Logger
  21. from ruoyi_common.base.snippet import classproperty
  22. from ..constant import Constants
  23. class UtilException(Exception):
  24. def __init__(self, message, status):
  25. super().__init__(message)
  26. self.status = status
  27. class StringUtil:
  28. @classmethod
  29. def to_bool(cls, value) -> bool:
  30. """
  31. 将值转换为布尔值
  32. Args:
  33. value (str or bytes): 输入
  34. Returns:
  35. bool: 转化后的布尔值
  36. """
  37. if isinstance(value, str):
  38. value = value.lower()
  39. if value in ['true', '1', 'yes', 'y']:
  40. return True
  41. elif value in ['false', '0', 'no', 'n']:
  42. return False
  43. elif isinstance(value, bytes):
  44. value = value.decode('utf-8')
  45. return cls.to_bool(value)
  46. elif value is None:
  47. return False
  48. else:
  49. raise TypeError('value must be str or bytes')
  50. return bool(value)
  51. @classmethod
  52. def to_int(cls, value) -> int:
  53. """
  54. 将值转换为整数
  55. Args:
  56. value (str): 输入
  57. Returns:
  58. int: 转化后的整数
  59. """
  60. if isinstance(value, str):
  61. value = value.strip()
  62. if value.isdigit():
  63. return int(value)
  64. return int(value)
  65. @classmethod
  66. def to_float(cls, value) -> float:
  67. """
  68. 将值转换为浮点数
  69. Args:
  70. value (str): 输入
  71. Returns:
  72. float: 转化后的浮点数
  73. """
  74. if isinstance(value, str):
  75. value = value.strip()
  76. if value.replace('.', '', 1).isdigit():
  77. return float(value)
  78. return float(value)
  79. @classmethod
  80. def to_str(cls, value) -> str:
  81. """
  82. 将值转换为字符串
  83. Args:
  84. value (_type_): 输入
  85. Returns:
  86. str: 转化后的字符串
  87. """
  88. if isinstance(value, bool):
  89. return str(value).lower()
  90. elif isinstance(value, bytes):
  91. value = value.decode('utf-8')
  92. return value
  93. return str(value)
  94. @classmethod
  95. def to_datetime(cls, value) -> datetime:
  96. """
  97. 将字符串转换为datetime类型
  98. Args:
  99. value (str): 字符类型日期字符串
  100. Returns:
  101. datetime: datetime类型日期
  102. """
  103. if isinstance(value, str):
  104. value = value.strip()
  105. try:
  106. return datetime.strptime(value, cls.datatime_format)
  107. except ValueError:
  108. pass
  109. return value
  110. @classmethod
  111. def ishttp(cls, val:str) -> bool:
  112. """
  113. 判断是否为http或https开头的url
  114. Args:
  115. val (str): 输入字符串
  116. Returns:
  117. bool: 是否为http或https开头的url
  118. """
  119. return val.startswith('http://') or val.startswith('https://')
  120. @classmethod
  121. def pad_left(cls, value, length:int) -> str:
  122. """
  123. 左填充字符串,使其长度达到指定长度
  124. Args:
  125. value (_type_): 输入字符串
  126. length (int): 目标长度
  127. Returns:
  128. str: 填充后的字符串
  129. """
  130. return str(value).zfill(length)
  131. @classmethod
  132. def substring_after(cls, string:str, separator:str) -> str:
  133. """
  134. 获取字符串string中第一个分隔符separator之后的字符串
  135. Args:
  136. string (str): 输入字符串
  137. separator (str): 分隔符
  138. Returns:
  139. str: 转换后的字符串
  140. """
  141. if separator in string:
  142. return string.split(separator, 1)[1]
  143. return ""
  144. class DictUtil:
  145. @classmethod
  146. def upper_key(cls,dict_obj:dict) -> dict:
  147. '''
  148. 将配置对象中的所有键转换为大写
  149. Args:
  150. dict_obj(dict): 字典
  151. Returns:
  152. dict: 转换后的字典
  153. '''
  154. new_configobj = {}
  155. for k in dict_obj.keys():
  156. v = dict_obj.get(k)
  157. if k.islower():
  158. if isinstance(v, dict):
  159. new_configobj[k.upper()] = cls.upper_key(v)
  160. else:
  161. new_configobj[k.upper()] = v
  162. else:
  163. new_configobj[k] = v
  164. return new_configobj
  165. @classmethod
  166. def lower_key(cls,dict_obj:dict) -> dict:
  167. '''
  168. 将配置对象中的所有键转换为小写
  169. Args:
  170. dict_obj(dict): 字典
  171. Returns:
  172. dict: 转换后的字典
  173. '''
  174. new_configobj = {}
  175. for k in dict_obj.keys():
  176. v = dict_obj.get(k)
  177. if k.islower():
  178. if isinstance(v, dict):
  179. new_configobj[k.upper()] = cls.upper_key(v)
  180. else:
  181. new_configobj[k.upper()] = v
  182. return new_configobj
  183. @classmethod
  184. def flatten(cls,dict_obj) -> dict:
  185. '''
  186. 将字典展平为一级字典
  187. Args:
  188. dict_obj(dict): 字典
  189. Returns:
  190. dict: 展平后的字典
  191. '''
  192. new_dict = {}
  193. inner_dict = {}
  194. for k,v in dict_obj.items():
  195. if isinstance(v,dict):
  196. inner_dict.update(cls.flatten(v))
  197. else:
  198. new_dict[k] = v
  199. new_dict.update(inner_dict)
  200. return new_dict
  201. @classmethod
  202. def format_value(cls,dict_obj) -> dict:
  203. '''
  204. 格式化字典的值
  205. Args:
  206. dict_obj(dict): 字典
  207. Returns:
  208. dict: 格式化后的字典
  209. '''
  210. pattern = re.compile("\{(.*?)\}")
  211. new_dict = {}
  212. for k,v in dict_obj.items():
  213. if isinstance(v,str) and re.match(pattern,v):
  214. v_v = v.format(**dict_obj)
  215. new_dict[k] = v_v
  216. else:
  217. new_dict[k] = v
  218. return new_dict
  219. @classmethod
  220. def recurive_key(cls,dict_obj,pre_key="") -> dict:
  221. '''
  222. 递归处理字典中的键
  223. Args:
  224. dict_obj(dict): 字典
  225. pre_key(str): 前缀
  226. Returns:
  227. dict: 处理后的字典
  228. '''
  229. new_dict = {}
  230. new_key = ""
  231. for k1,v1 in dict_obj.items():
  232. if pre_key=="":
  233. new_key = k1
  234. else:
  235. new_key = "{}.{}".format(pre_key,k1)
  236. if isinstance(v1,dict):
  237. next_dict = cls.recurive_key(v1,new_key)
  238. new_dict[new_key] = next_dict
  239. else:
  240. new_dict[new_key] = v1
  241. return new_dict
  242. class Base64Util:
  243. @classmethod
  244. def decode(cls,data:str, is_padding=True) -> str:
  245. '''
  246. base64解码
  247. Args:
  248. data(str): base64编码数据
  249. is_padding(bool): 是否需要补位
  250. Returns:
  251. str: 解码后数据
  252. '''
  253. suplus = len(data) % 4
  254. if is_padding:
  255. missing_padding = 4 - suplus
  256. data += '='* missing_padding
  257. else:
  258. data = data[:-suplus] if suplus else data
  259. return str(base64.b64decode(data))
  260. class TokenUtil:
  261. default_algorithm = "HS512"
  262. default_headers = {
  263. "typ": None,
  264. "alg": default_algorithm
  265. }
  266. @classmethod
  267. def encode(cls, payload, secret, headers=None) -> str:
  268. '''
  269. 编码生成jwt token
  270. Args:
  271. payload(dict): 载荷
  272. secret(str): 密钥
  273. headers(dict): 头部
  274. Returns:
  275. str: jwt token
  276. '''
  277. if headers is None:
  278. headers = cls.default_headers
  279. if "alg" not in headers:
  280. headers["alg"] = cls.default_algorithm
  281. else:
  282. if not headers["alg"]:
  283. headers["alg"] = cls.default_algorithm
  284. algorithm = headers["alg"]
  285. secret_decoded = Base64Util.decode(secret)
  286. jwt = api_jwt.encode(
  287. payload,
  288. secret_decoded,
  289. algorithm,
  290. headers=headers
  291. )
  292. return jwt
  293. @classmethod
  294. def decode(cls, jwt, secret, algorithms=None, verify=True) -> dict:
  295. '''
  296. 解码jwt token
  297. Args:
  298. jwt(str): jwt token
  299. secret(str): 密钥
  300. algorithms(str): 算法
  301. verify(bool): 是否验证
  302. Returns:
  303. dict: 解码后的payload
  304. '''
  305. if algorithms is None:
  306. algorithms = cls.default_algorithm
  307. secret_decoded = Base64Util.decode(secret)
  308. payload = api_jwt.decode(jwt, secret_decoded, algorithms=algorithms, verify=verify)
  309. return payload
  310. @classmethod
  311. def get_from_request(cls) -> str:
  312. '''
  313. 从请求头中获取token
  314. Returns:
  315. str: token
  316. '''
  317. authorization = request.headers.get('Authorization', None)
  318. if authorization is None:
  319. raise Exception('Authorization header not found')
  320. authorization_split = authorization.split()
  321. if len(authorization_split)!= 2 or authorization_split[0].lower()!= 'bearer':
  322. raise Exception('Invalid authorization header')
  323. return authorization_split[1]
  324. @classmethod
  325. def verify_from_request(cls, key, algorithms=None):
  326. '''
  327. 从请求头中获取token,并验证token
  328. Args:
  329. key(str): 密钥
  330. algorithms(str): 算法
  331. Raises:
  332. UtilException: 验证失败
  333. '''
  334. encoded_token = cls.get_token_from_request()
  335. cls.decode(encoded_token, key, algorithms=algorithms, verify=True)
  336. class IpUtil:
  337. @classmethod
  338. def get_ip(cls):
  339. '''
  340. 获取请求ip
  341. Returns:
  342. str: ip
  343. '''
  344. ip = None
  345. if 'HTTP_X_FORWARDED_FOR' in request.headers:
  346. ip = request.headers['HTTP_X_FORWARDED_FOR']
  347. elif 'REMOTE_ADDR' in request.headers:
  348. ip = request.headers['REMOTE_ADDR']
  349. ip, _ = request.host.rsplit(':', 1)
  350. ip = "127.0.0.1" if ip == "localhost" else ip
  351. return ip
  352. @classmethod
  353. def get_local_ips(cls) -> List[str]:
  354. '''
  355. 获取本地ip
  356. Returns:
  357. List[str]: 本地ip列表
  358. '''
  359. ips = []
  360. addrs = psutil.net_if_addrs() # 获取所有网络接口信息
  361. for interface, addr_list in addrs.items():
  362. for addr in addr_list:
  363. if addr.family == socket.AF_INET and not addr.address.startswith("127."):
  364. ips.append(addr.address)
  365. return ips
  366. def is_valid_ip(ip: str) -> bool:
  367. try:
  368. ipaddress.ip_address(ip)
  369. return True
  370. except ValueError:
  371. return False
  372. class AddressUtil:
  373. @classmethod
  374. def get_address(cls, ip) -> str:
  375. '''
  376. 根据ip获取地址
  377. Args:
  378. ip(str): ip
  379. Returns:
  380. str: 地址
  381. '''
  382. # todo
  383. address = None
  384. return address
  385. class UserAgentUtil:
  386. @classmethod
  387. def get_user_agent(cls) -> str:
  388. '''
  389. 获取请求头中的user-agent
  390. Returns:
  391. str: user-agent
  392. '''
  393. user_agent = request.headers.get('User-Agent', None)
  394. return user_agent
  395. @classmethod
  396. def is_mobile(cls) -> bool:
  397. '''
  398. 判断是否为移动端
  399. Returns:
  400. bool: 是否为移动端
  401. '''
  402. user_agent = cls.get_user_agent()
  403. if user_agent is None:
  404. return False
  405. mobile_agents = [
  406. 'Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod'
  407. ]
  408. for agent in mobile_agents:
  409. if agent in user_agent:
  410. return True
  411. return False
  412. @classmethod
  413. def browser(cls) -> Literal['Chrome', 'Firefox', 'Safari', 'IE', None]:
  414. '''
  415. 获取浏览器类型
  416. Returns:
  417. Literal['Chrome', 'Firefox', 'Safari', 'IE', None]: 浏览器类型
  418. '''
  419. user_agent = cls.get_user_agent()
  420. if user_agent is None:
  421. return None
  422. if 'Chrome' in user_agent:
  423. return 'Chrome'
  424. elif 'Firefox' in user_agent:
  425. return 'Firefox'
  426. elif 'Safari' in user_agent:
  427. return 'Safari'
  428. elif 'MSIE' in user_agent or 'Trident' in user_agent:
  429. return 'IE'
  430. else:
  431. return None
  432. @classmethod
  433. def os(cls) -> Literal['Windows', 'Mac', 'Linux', 'Unix', None]:
  434. '''
  435. 获取操作系统类型
  436. Returns:
  437. Literal['Windows', 'Mac', 'Linux', 'Unix', None]: 操作系统类型
  438. '''
  439. user_agent = cls.get_user_agent()
  440. if user_agent is None:
  441. return None
  442. if 'Windows NT' in user_agent:
  443. return 'Windows'
  444. elif 'Macintosh' in user_agent:
  445. return 'Mac'
  446. elif 'Linux' in user_agent:
  447. return 'Linux'
  448. elif 'Unix' in user_agent:
  449. return 'Unix'
  450. else:
  451. return None
  452. @classmethod
  453. def is_pc(cls) -> bool:
  454. '''
  455. 判断是否为PC端
  456. Returns:
  457. bool: 是否为PC端
  458. '''
  459. user_agent = cls.get_user_agent()
  460. if user_agent is None:
  461. return False
  462. pc_agents = [
  463. 'Windows NT', 'Macintosh', 'Linux', 'Unix', 'FreeBSD', 'OpenBSD',
  464. 'NetBSD', 'SunOS', 'AIX', 'HP-UX', 'IRIX', 'OSF1', ' SCO', 'IRIX64'
  465. ]
  466. for agent in pc_agents:
  467. if agent in user_agent:
  468. return True
  469. return False
  470. @classmethod
  471. def is_weixin(cls) -> bool:
  472. '''
  473. 判断是否为微信端
  474. Returns:
  475. bool: 是否为微信端
  476. '''
  477. user_agent = cls.get_user_agent()
  478. if user_agent is None:
  479. return False
  480. weixin_agents = [
  481. 'MicroMessenger', 'WeChat', 'QQ', 'Weibo', 'TencentTraveler',
  482. 'QQBrowser', 'QQMobile', 'QQScan', 'Tenvideo', 'SogouExplorer',
  483. ]
  484. pass
  485. class MimeTypeUtil:
  486. IMAGE_PNG = "image/png"
  487. IMAGE_JPG = "image/jpg"
  488. IMAGE_JPEG = "image/jpeg"
  489. IMAGE_BMP = "image/bmp"
  490. IMAGE_GIF = "image/gif"
  491. VIDEO_EXTENSION = [ "mp4", "avi", "rmvb" ]
  492. MEDIA_EXTENSION = [ "swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
  493. "asf", "rm", "rmvb" ]
  494. FLASH_EXTENSION = [ "swf", "flv" ]
  495. IMAGE_EXTENSION = [ "bmp", "gif", "jpg", "jpeg", "png" ]
  496. DEFAULT_ALLOWED_EXTENSION = [
  497. # 图片
  498. "bmp", "gif", "jpg", "jpeg", "png",
  499. # word excel powerpoint
  500. "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
  501. # 压缩文件
  502. "rar", "zip", "gz", "bz2",
  503. # 视频格式
  504. "mp4", "avi", "rmvb",
  505. # pdf
  506. "pdf" ]
  507. def get_extension(cls, mime_type:str):
  508. '''
  509. 根据mime_type获取文件扩展名
  510. Args:
  511. mime_type(str): mime_type
  512. Returns:
  513. str: 文件扩展名
  514. '''
  515. match mime_type:
  516. case cls.IMAGE_PNG:
  517. return "png"
  518. case cls.IMAGE_JPG:
  519. return "jpg"
  520. case cls.IMAGE_JPEG:
  521. return "jpeg"
  522. case cls.IMAGE_BMP:
  523. return "bmp"
  524. case cls.IMAGE_GIF:
  525. return "gif"
  526. case _:
  527. return ""
  528. class DateUtil:
  529. YYYY = "%Y"
  530. YYYY_MM = "%Y-%m"
  531. YYYY_MM_DD = "%Y-%m-%d"
  532. YYYYMMDDHHMMSS = "%Y%m%d%H%M%S"
  533. YYYY_MM_DD_HH_MM_SS = "%Y-%m-%d %H:%M:%S"
  534. @classmethod
  535. def get_date_now(cls) -> str:
  536. """
  537. 获取当前日期 %Y-%m-%d
  538. Returns:
  539. str: 当前日期
  540. """
  541. return datetime.now().strftime(cls.YYYY_MM_DD)
  542. @classmethod
  543. def get_datetime_now(cls,fmt=None) -> str:
  544. """
  545. 获取当前日期 %Y%m%d%H%M%S
  546. Returns:
  547. str: 当前日期
  548. """
  549. fmt = fmt or cls.YYYYMMDDHHMMSS
  550. return datetime.now().strftime(fmt)
  551. @classmethod
  552. def get_time_now(cls) -> str:
  553. """
  554. 获取当前日期 '%Y-%m-%d %H:%M:%S'
  555. Returns:
  556. str: 当前日期
  557. """
  558. return datetime.now().strftime(cls.YYYY_MM_DD_HH_MM_SS)
  559. @classmethod
  560. def get_date_path(cls) -> str:
  561. """
  562. 获取当前日期 %Y/%m/%d
  563. Returns:
  564. str: 当前日期
  565. """
  566. return datetime.now().strftime("%Y/%m/%d")
  567. @classmethod
  568. def get_datepath(cls) -> str:
  569. """
  570. 获取当前日期 %Y%m%d
  571. Returns:
  572. str: 当前日期
  573. """
  574. return datetime.now().strftime("%Y%m%d")
  575. class FileUploadUtil:
  576. DEFAULT_MAX_SIZE = 50 * 1024 * 1024
  577. DEFAULT_FILE_NAME_LENGTH = 100
  578. @classmethod
  579. def upload(cls, file:FileStorage, base_path:str) -> str:
  580. '''
  581. 上传文件
  582. Args:
  583. file(FileStorage): 文件对象
  584. base_path(str): 上传路径
  585. Returns:
  586. str: 资源路径
  587. '''
  588. fn_len = len(file.filename)
  589. if fn_len > cls.DEFAULT_FILE_NAME_LENGTH:
  590. raise Exception("文件名长度超过限制")
  591. cls.check_allowed(file, MimeTypeUtil.DEFAULT_ALLOWED_EXTENSION)
  592. filename = cls.extract_file_name(file)
  593. filepath = os.path.join(base_path, filename)
  594. file_parpath = os.path.dirname(filepath)
  595. if not os.path.exists(file_parpath):
  596. os.makedirs(file_parpath)
  597. file.save(filepath)
  598. resource_path = Constants.RESOURCE_PREFIX + "/" + filepath
  599. return resource_path
  600. @classmethod
  601. def check_allowed(cls, file:FileStorage, allowed_extensions:List[str]):
  602. '''
  603. 文件大小校验
  604. Args:
  605. file(FileStorage): 文件对象
  606. allowed_extensions(List[str]): 允许的扩展名列表
  607. '''
  608. file_size = file.stream.tell()
  609. if file_size > cls.DEFAULT_MAX_SIZE:
  610. raise Exception("文件大小超过限制")
  611. extension = MimeTypeUtil.get_extension(file.content_type.lower())
  612. if extension not in allowed_extensions:
  613. if allowed_extensions == MimeTypeUtil.IMAGE_EXTENSION:
  614. raise Exception("图片格式不支持")
  615. elif allowed_extensions == MimeTypeUtil.VIDEO_EXTENSION:
  616. raise Exception("视频格式不支持")
  617. elif allowed_extensions == MimeTypeUtil.MEDIA_EXTENSION:
  618. raise Exception("媒体格式不支持")
  619. elif allowed_extensions == MimeTypeUtil.FLASH_EXTENSION:
  620. raise Exception("FLASH格式不支持")
  621. else:
  622. raise Exception("文件格式不支持")
  623. @classmethod
  624. def extract_file_name(cls, file:FileStorage) -> str:
  625. '''
  626. 提取文件名
  627. Args:
  628. file(FileStorage): 文件对象
  629. Returns:
  630. str: 文件名
  631. '''
  632. "{}/{}_{}.{}".format(
  633. DateUtil.get_date_path(),
  634. os.path.basename(file.filename),
  635. Seq.get_seq_id(cls.upload_seq_type),
  636. cls.get_extension(file)
  637. )
  638. @classmethod
  639. def get_extension(cls, file:FileStorage) -> str:
  640. '''
  641. 获取文件扩展名
  642. Args:
  643. file(FileStorage): 文件对象
  644. Returns:
  645. str: 文件扩展名
  646. '''
  647. extension = os.path.splitext(file.filename)[1]
  648. if extension is None or len(extension) == 0:
  649. extension = MimeTypeUtil.get_extension(file.content_type.lower())
  650. return extension
  651. @classmethod
  652. def get_filename(cls, filename:str) -> str:
  653. '''
  654. 获取文件名
  655. Args:
  656. filename(str): 带路径和后缀的文件名
  657. Returns:
  658. str: 文件名
  659. '''
  660. return os.path.basename(filename)
  661. class AtomicInteger:
  662. def __init__(self, initial=0):
  663. self._value = initial
  664. self._lock = threading.Lock()
  665. def get(self):
  666. '''
  667. 获取当前值
  668. Returns:
  669. int: 当前值
  670. '''
  671. with self._lock:
  672. return self._value
  673. def set(self, value):
  674. '''
  675. 设置新的值
  676. Args:
  677. value(int): 新的值
  678. '''
  679. with self._lock:
  680. self._value = value
  681. def increment(self):
  682. '''
  683. 原子增加操作
  684. Returns:
  685. int: 新值
  686. '''
  687. with self._lock:
  688. self._value += 1
  689. return self._value
  690. def decrement(self):
  691. '''
  692. 原子减少操作
  693. Returns:
  694. int: 新值
  695. '''
  696. with self._lock:
  697. self._value -= 1
  698. return self._value
  699. class Seq:
  700. common_seq_type = "common"
  701. upload_seq_type = "upload"
  702. common_seq = AtomicInteger(1)
  703. upload_seq = AtomicInteger(1)
  704. matchine_code = "A"
  705. @classmethod
  706. def get_seq_id(cls, seq_name:str = "common") -> int:
  707. '''
  708. 获取序列号
  709. Args:
  710. seq_name(str): 序列名称,common或upload
  711. Returns:
  712. int: 序列号
  713. '''
  714. ato = cls.upload_seq if seq_name == cls.upload_seq_type else cls.common_seq
  715. out = DateUtil.get_datetime_now() + cls.matchine_code + cls.get_seq(ato, 3)
  716. return out
  717. @classmethod
  718. def get_seq(cls, ato:AtomicInteger, length:int) -> str:
  719. '''
  720. 获取指定长度的序列号
  721. Args:
  722. ato(AtomicInteger): 原子整数
  723. length(int): 序列号长度
  724. Returns:
  725. str: 序列号
  726. '''
  727. seq = str(ato.increment())
  728. if ato.get() > math.pow(10, length):
  729. ato.set(1)
  730. return StringUtil.left_pad(seq, length)
  731. class MessageUtil:
  732. @staticmethod
  733. def message(code:str) -> str:
  734. """
  735. 根据code获取消息
  736. Args:
  737. code (str): 消息代码
  738. Returns:
  739. str: 消息内容
  740. """
  741. # todo
  742. return code
  743. class FileUtil:
  744. def delete_file(file_path:str) -> bool:
  745. '''
  746. 删除文件
  747. Args:
  748. file_path(str): 文件路径
  749. Returns:
  750. bool: 是否成功
  751. '''
  752. flag = False
  753. if os.path.isfile(file_path) and os.path.exists(file_path):
  754. os.remove(file_path)
  755. flag = True
  756. return flag
  757. class DescriptUtil:
  758. @classmethod
  759. def get_raw(cls, func:Callable) -> Callable:
  760. """
  761. 获取原始函数
  762. Args:
  763. func(Callable): 被装饰函数
  764. Returns:
  765. Callable: 原始函数
  766. """
  767. if hasattr(func, "__wrapped__"):
  768. func = func.__wrapped__
  769. return cls.get_raw(func)
  770. else:
  771. return func
  772. class ExcelUtil:
  773. default_header_fill = {
  774. "start_color": "FFFFFFFF",
  775. "end_color": "FFFFFFFF",
  776. "fill_type": None, # "solid" or None
  777. }
  778. default_row_fill = {
  779. "start_color": "FFFFFFFF",
  780. "end_color": "FFFFFFFF",
  781. "fill_type": None, # "solid" or None
  782. }
  783. allowed_extensions = ["xlsx","xls"]
  784. allowed_content_types = [
  785. "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
  786. ]
  787. max_content_length = 6 * 1024 * 1024 # 6M
  788. def __init__(self, model:Type[BaseModel]):
  789. self.model = model
  790. def write(self, data:List[BaseModel], sheetname:str) -> BytesIO:
  791. """
  792. 写入Excel文件
  793. Args:
  794. data(List[BaseModel]): 数据
  795. sheetname(str): 工作表名
  796. Returns:
  797. BytesIO: 文件字节流
  798. """
  799. if len(data) == 0:
  800. raise NotFound(description="无法导出excel,数据为空")
  801. workbook = Workbook()
  802. worksheet = workbook.create_sheet(title=sheetname)
  803. workbook.active = worksheet
  804. self.render_data(worksheet,data)
  805. output = BytesIO()
  806. workbook.save(output)
  807. output.seek(0)
  808. return output
  809. def render_header(self, sheet:Worksheet,fill:PatternFill=None):
  810. """
  811. 渲染Excel表头
  812. Args:
  813. sheet (Worksheet): 工作表
  814. fill(PatternFill): 表头填充
  815. """
  816. for col_index,access in enumerate(
  817. self.model.generate_excel_schema(),
  818. start=1
  819. ):
  820. _, access = access
  821. cell = sheet.cell(row=1,column=col_index,value=access.name)
  822. cell.fill = fill
  823. cell.font = access.header_font
  824. def render_row(self, sheet:Worksheet,row:BaseModel,row_index:int):
  825. """
  826. 渲染Excel行数据
  827. Args:
  828. sheet (Worksheet): 工作表
  829. row (BaseModel): 行数据模型
  830. row_index(int): 行索引
  831. """
  832. default_row_fill = PatternFill(
  833. **self.default_row_fill
  834. )
  835. for col_index,access in enumerate(row.generate_excel_data(),start=1):
  836. _,access = access
  837. cell = sheet.cell(row=row_index,column=col_index,value=access.val)
  838. cell.alignment = access.alignment
  839. cell.fill = access.fill if access.fill else default_row_fill
  840. cell.font = access.row_font
  841. def render_footer(self, sheet:Worksheet):
  842. """
  843. 渲染Excel表尾
  844. Args:
  845. sheet (Worksheet): 工作表
  846. """
  847. pass
  848. def render_data(self, sheet:Worksheet, data:List[BaseModel],header_fill:PatternFill=None):
  849. """
  850. 渲染Excel数据
  851. Args:
  852. sheet (Worksheet): 工作表
  853. data(List[BaseModel]): 数据模型列表
  854. """
  855. if not header_fill:
  856. header_fill = PatternFill(
  857. **self.default_header_fill
  858. )
  859. self.render_header(sheet,header_fill)
  860. for row_index,row in enumerate(data,start=2):
  861. self.render_row(sheet,row,row_index)
  862. self.render_footer(sheet)
  863. def export_response(self, data:List[BaseModel], sheetname:str) -> Response:
  864. """
  865. 响应Excel文件
  866. Args:
  867. data(List[BaseModel]): 数据
  868. sheetname(str): 工作表名
  869. Returns:
  870. Response: 文件流响应
  871. """
  872. output:BytesIO = self.write(data,sheetname)
  873. response = Response(
  874. response=output.getvalue(),
  875. status=200,
  876. mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  877. headers={"Content-Disposition": f"attachment; filename={time.time()}.xlsx"}
  878. )
  879. return response
  880. def import_template_response(self, sheetname:str) -> Response:
  881. """
  882. 响应导入模板
  883. Args:
  884. sheetname(str): 工作表名
  885. Returns:
  886. Response: 文件流响应
  887. """
  888. output:BytesIO = self.write_template(sheetname)
  889. response = Response(
  890. response=output.getvalue(),
  891. status=200,
  892. mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  893. headers={"Content-Disposition": f"attachment; filename={self.model.__name__}_import_template.xlsx"}
  894. )
  895. return response
  896. def write_template(self,sheetname:str) -> BytesIO:
  897. """
  898. 写入导入模板
  899. Args:
  900. sheetname(str): 工作表名
  901. Returns:
  902. BytesIO: 文件字节流
  903. """
  904. workbook = Workbook()
  905. worksheet = workbook.create_sheet(title=sheetname)
  906. workbook.active = worksheet
  907. self.render_template(worksheet)
  908. output = BytesIO()
  909. workbook.save(output)
  910. output.seek(0)
  911. return output
  912. def render_template(self, sheet:Worksheet):
  913. """
  914. 渲染导入模板
  915. Args:
  916. sheet (Worksheet): 工作表
  917. """
  918. header_fill = PatternFill(
  919. **self.default_header_fill
  920. )
  921. self.render_header(sheet,header_fill)
  922. def import_file(self, file:FileStorage, sheetname:str) -> List[BaseModel]:
  923. """
  924. 导入数据
  925. Args:
  926. file(FileStorage): 导入文件
  927. sheetname(str): 工作表名
  928. Returns:
  929. List[BaseModel]: 导入数据模型列表
  930. """
  931. self.check_file(file)
  932. buffer = io.BufferedReader(file.stream)
  933. data = self.read_buffer(buffer, sheetname)
  934. return data
  935. def check_file(self, file:FileStorage):
  936. '''
  937. 检查文件是否合法
  938. '''
  939. filename = secure_filename(file.filename)
  940. if "." not in filename:
  941. raise Exception("文件名称不正确")
  942. ext = filename.rsplit(".", 1)[1]
  943. if ext not in self.allowed_extensions:
  944. raise Exception("文件名称扩展名不正确")
  945. if file.content_type not in self.allowed_content_types:
  946. raise Exception("文件类型不正确")
  947. # 文件大小
  948. file.seek(0,os.SEEK_END)
  949. if file.tell() > self.max_content_length:
  950. raise Exception("文件大小超过限制")
  951. def read_buffer(self, buffer:BufferedReader,sheetname:str) -> List[BaseModel]:
  952. """
  953. 读取文件流
  954. Args:
  955. buffer(BufferedReader): 导入文件流
  956. sheetname(str): 工作表名
  957. Returns:
  958. List[BaseModel]: 导入数据模型列表
  959. """
  960. try:
  961. workbook = load_workbook(buffer,read_only=True,data_only=True)
  962. except Exception as e:
  963. raise Exception("文件格式不正确")
  964. if sheetname not in workbook.sheetnames:
  965. raise NotFound(description="工作表不存在")
  966. worksheet = workbook[sheetname]
  967. headers = worksheet[1]
  968. data = []
  969. for row in worksheet.iter_rows(min_row=2):
  970. row_data = {}
  971. for header, cell in zip(headers,row):
  972. row_data[header.value] = cell.value
  973. new_row = self.model.rebuild_excel_schema(row_data)
  974. data.append(self.model(**new_row))
  975. return data
  976. class LogUtil:
  977. @classproperty
  978. def logger(cls) -> Logger:
  979. """
  980. 获取日志对象
  981. Returns:
  982. logging.Logger: 日志对象
  983. """
  984. from flask import current_app
  985. app = current_app
  986. return app.logger
  987. def get_final_type(annotation) -> Type:
  988. args = get_args(annotation)
  989. if isinstance(args, tuple) and len(args) > 1:
  990. arg0 = args[0]
  991. if isinstance(arg0, Type):
  992. return arg0
  993. else:
  994. return get_final_type(arg0)
  995. def get_final_model(annotation) -> Optional[type]:
  996. origin = get_origin(annotation)
  997. if origin is None:
  998. return annotation
  999. else:
  1000. args = get_args(annotation)
  1001. for arg in args:
  1002. return get_final_model(arg)
  1003. else:
  1004. return None