base.py 34 KB

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