base.py 33 KB

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