# +====================================================================== # | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] # +---------------------------------------------------------------------- # | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 # +---------------------------------------------------------------------- # | 开源协议: Apache-2.0 # | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 # +---------------------------------------------------------------------- # | 官方网站: https://www.djangoadmin.cn # +---------------------------------------------------------------------- # | 研发团队: 小蚂蚁云团队 荣誉出品 # +====================================================================== # | 版权和免责声明: # | 一、知识产权声明 # | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 # | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, # | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 # | 二、使用限制与禁止条款 # | 您理解并同意,禁止将本产品用于以下任何行为: # | 1. 任何违反中华人民共和国法律法规的用途; # | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; # | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 # | 三、责任豁免 # | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 # | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 # | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 # | 四、违约与法律责任 # | 如发现任何违反本声明的行为,本团队有权: # | 1. 无条件配合公安机关及其他有权机关的调查取证工作; # | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; # | 3. 保留进一步采取刑事控告等法律措施的权利。 # | 五、许可范围说明 # | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 # | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 # | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 # | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 # | 成的一切经济损失及商誉损害。 # | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 # | 许可协议》附件为准。 # +====================================================================== """ 通用工具函数模块 ============================================================= 该模块提供项目中广泛使用的通用工具函数,涵盖token解析、类型判断、 文件处理、文件操作等常用功能。这些工具函数在整个项目中重复使用, 提供了统一的处理逻辑,减少代码冗余。 主要功能: 1. Token解析工具 - 从请求中解析JWT token,获取用户信息 2. 类型判断工具 - 判断变量类型并返回可读的类型名称 3. 文件处理工具 - 文件URL处理、临时文件迁移、远程文件保存 4. 富文本内容处理 - 提取并保存富文本中的文件 5. IP地址处理 - 获取客户端IP和地理位置 6. User-Agent解析 - 解析操作系统和浏览器信息 7. 参数格式化 - 格式化请求参数和返回结果 所属包: utils 所在文件: utils/utils.py 依赖模块: os, re, time - 标准库 config.env - 项目配置 utils.file.mkdir - 目录创建工具 """ import logging import os import time from django.conf import settings from config.env import DJANGO_FILE_URL, DJANGO_TEMP_PATH, DJANGO_UPLOAD_DIR import json import re import urllib.request from utils.file import mkdir # 获取日志记录器 logger = logging.getLogger(__name__) # ============================= 文件处理工具 ============================= # 获取文件地址 def get_file_url(path): """ 获取完整的文件访问URL ============================================================= 将文件的相对路径拼接上文件域名,生成完整的可访问URL。 如果路径为空,直接返回None,避免空值拼接。 参数: path (str): 文件的相对路径,可以是空字符串、None或有效路径 返回: str or None: 完整的文件访问URL,如果path为空则返回None """ # 参数验证:如果路径为空或None,直接返回None if not path: return None # 拼接文件域名和相对路径 return DJANGO_FILE_URL + path def save_file(url, directory): """ 保存文件到指定目录 ============================================================= 处理文件保存逻辑,主要针对本站临时文件进行迁移。 将临时目录中的文件移动到正式存储目录,并返回新的相对路径。 参数: url (str): 文件的完整URL或相对路径 dirname (str): 目标子目录名称,用于分类存储 返回: str: 文件的相对路径 失败时返回空字符串 处理流程: 1. 检查URL有效性 2. 判断是否为本站文件 3. 判断是否为临时文件 4. 创建目标目录 5. 移动文件到正式目录 6. 返回新路径 文件分类说明: - 临时文件:存储在 temp 目录下,需要迁移 - 正式文件:已存储在 images 目录下,直接返回相对路径 - 远程文件:其他站点的文件,暂未实现处理逻辑 """ # 判断文件地址是否为空 if not url: return "文件地址不能为空" # 判断是否是本站文件 # 说明:通过判断URL是否包含文件域名来识别本站文件 if url.find(DJANGO_FILE_URL) != -1: # 本站文件处理 if url.find("/temp") != -1: # 临时文件处理 # 说明:临时文件需要移动到正式目录 # 构建目标目录路径 # 格式:正式文件路径/分类目录/日期目录 path = DJANGO_UPLOAD_DIR + "/" + directory + "/" + time.strftime('%Y%m%d') # 创建存储目录 # 函数:mkdir(path) # 说明:确保目标目录存在 mkdir(path) # 原始文件地址 # 说明:将URL转换为本地文件系统路径 old_path = url.replace(DJANGO_FILE_URL, DJANGO_UPLOAD_DIR) # 目标目录地址 # 说明:构建移动后的新路径,移除temp目录部分 new_path = DJANGO_UPLOAD_DIR + "/" + directory + old_path.replace(DJANGO_TEMP_PATH, "") # 移动文件 # 函数:os.rename(oldPath, newPath) # 说明:将文件从临时位置移动到正式位置 os.rename(old_path, new_path) # 返回结果 # 说明:返回新路径的相对部分(不包含上传目录) return new_path.replace(DJANGO_UPLOAD_DIR, "") else: # 非临时文件处理 # 说明:已经是正式文件,直接返回相对路径 return url.replace(DJANGO_FILE_URL, "") else: # 远程文件处理 # 说明:非本站文件,暂未实现下载保存逻辑 print("远程文件处理") # 返回空字符串 return "" def save_content(content, title, directory): """ 保存富文本编辑器内容 ============================================================= 处理富文本编辑器提交的内容,提取其中的文件并进行保存。 同时处理文件的alt属性,将其设置为文章标题。 参数: content (str): 富文本HTML内容 title (str): 文章标题,用于设置文件alt属性 dirname (str): 文件存储的子目录名称 返回: str: 处理后的富文本内容,文件地址已被替换 处理流程: 1. 使用正则表达式提取所有img标签的src属性 2. 遍历每个文件URL,调用save_file保存 3. 将原URL替换为新URL(带[IMG_URL]标记) 4. 设置空alt为文章标题 5. 返回处理后的内容 正则表达式说明: 'img src="(.*?)"' - img src=" : 匹配img标签的src属性开始 - (.*?) : 非贪婪匹配,捕获引号内的URL - " : 匹配结束引号 - re.S : 使.匹配包括换行符在内的所有字符 """ # 正则取出富文本文件地址 # 说明:找出所有img标签中的src属性值 image_urls = re.findall('img src="(.*?)"', content, re.S) # 遍历文件地址 for url in image_urls: # 保存文件至本地 # 函数:save_file(url, dirname) # 说明:处理文件保存,返回新路径 image = save_file(url, directory) if image: # 替换原文件地址 # 说明:将原URL替换为新地址,添加[IMG_URL]标记用于后续处理 content = content.replace(url, "[IMG_URL]" + image) # 设置alt标题 # 说明:为没有alt属性的文件设置标题,有利于SEO # 确保 title 是字符串类型 title = str(title) if title else "" if content.find("alt=\"\"") and title: content = content.replace("alt=\"\"", "alt=\"" + title + "\"") # 返回结果 return content def get_table_name(base_name): """获取带前缀的表名""" prefix = getattr(settings, 'DATABASE_PREFIX', '') return f"{prefix}{base_name}" # ============================= IP地址处理工具 ============================= def get_client_ip(request) -> str: """ 获取客户端真实IP地址 ============================================================= 通过HTTP请求头获取客户端的真实IP地址,支持代理环境。 优先从 HTTP_X_FORWARDED_FOR 获取,其次使用 REMOTE_ADDR。 Args: request: HttpRequest对象 Returns: str: 客户端IP地址 处理逻辑: 1. 检查 HTTP_X_FORWARDED_FOR 头(适用于代理环境) 2. 如果存在,取第一个IP(客户端真实IP) 3. 否则使用 REMOTE_ADDR(直接连接IP) 使用示例: >>> ip = get_client_ip(request) >>> print(f"客户端IP: {ip}") """ # 获取代理转发的IP列表 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: # 取第一个IP(客户端真实IP) ip = x_forwarded_for.split(',')[0] else: # 直接获取连接IP ip = request.META.get('REMOTE_ADDR') return ip def get_ip_location(ip: str) -> str: """ 根据IP获取地理位置(使用纯真IP库或第三方API) ============================================================= 调用第三方IP地理位置API,获取IP所在的国家、省份、城市信息。 Args: ip: IP地址 Returns: str: 地理位置信息,格式:"国家 省份 城市" 处理逻辑: 1. 内网IP直接返回"内网IP" 2. 调用 ip-api.com API 获取地理位置 3. 解析返回的JSON数据 4. 返回格式化后的地理位置 注意: - ip-api.com 免费版限制45次/分钟 - 生产环境建议使用本地IP库如纯真IP库 """ # 内网IP或本地IP判断 if ip in ['127.0.0.1', 'localhost'] or ip.startswith('192.168.') or ip.startswith('10.'): return "内网IP" try: # 使用ip-api.com获取地理位置(免费,限制45次/分钟) # 生产环境建议使用本地IP库如纯真IP库 url = f"http://ip-api.com/json/{ip}?lang=zh-CN" with urllib.request.urlopen(url, timeout=3) as response: data = json.loads(response.read().decode('utf-8')) if data.get('status') == 'success': # 返回格式化后的地理位置 return f"{data.get('country', '')} {data.get('regionName', '')} {data.get('city', '')}" except Exception as e: # 记录警告日志 logger.warning(f"获取IP地理位置失败: {e}") return "未知" # ============================= User-Agent解析工具 ============================= def parse_user_agent(user_agent: str) -> dict: """ 解析User-Agent,获取操作系统和浏览器信息 ============================================================= 通过正则表达式匹配User-Agent字符串,识别用户的操作系统和浏览器类型。 Args: user_agent: User-Agent字符串 Returns: dict: 包含os和browser的字典 { 'os': '操作系统', 'browser': '浏览器' } 识别规则: 操作系统: - Windows系列: Windows 10/8/7/XP - macOS: macOS - Linux: Linux - Android: Android - iOS: iOS 浏览器: - Edge: Edge - Chrome: Chrome - Safari: Safari - Firefox: Firefox - IE: IE 使用示例: >>> ua = request.META.get('HTTP_USER_AGENT', '') >>> info = parse_user_agent(ua) >>> print(f"操作系统: {info['os']}, 浏览器: {info['browser']}") """ # 初始化返回结果 result = { 'os': '未知', 'browser': '未知' } # 空值判断 if not user_agent: return result # 转换为小写,便于匹配 ua_lower = user_agent.lower() # 操作系统识别规则 os_patterns = [ (r'windows nt 10\.0', 'Windows 10'), (r'windows nt 6\.1', 'Windows 7'), (r'windows nt 6\.2', 'Windows 8'), (r'windows nt 6\.3', 'Windows 8.1'), (r'windows nt', 'Windows'), (r'mac os x', 'macOS'), (r'linux', 'Linux'), (r'android', 'Android'), (r'iphone', 'iOS'), (r'ipad', 'iOS'), ] # 遍历匹配操作系统 for pattern, os_name in os_patterns: if re.search(pattern, ua_lower): result['os'] = os_name break # 浏览器识别规则 browser_patterns = [ (r'edg', 'Edge'), (r'chrome', 'Chrome'), (r'safari', 'Safari'), (r'firefox', 'Firefox'), (r'msie', 'IE'), (r'trident', 'IE'), ] # 遍历匹配浏览器 for pattern, browser_name in browser_patterns: if re.search(pattern, ua_lower): result['browser'] = browser_name break return result # ============================= 参数格式化工具 ============================= def format_params(params) -> str: """ 格式化请求参数,用于日志存储 ============================================================= 将请求参数字典转换为JSON字符串,并过滤敏感信息。 Args: params: 请求参数(字典或其他类型) Returns: str: 格式化后的JSON字符串 处理逻辑: 1. 如果参数为空,返回空字符串 2. 如果是字典类型,复制并过滤敏感字段 3. 敏感字段值替换为 '******' 4. 转换为JSON字符串 5. 如果转换失败,返回字符串表示(限制500字符) 敏感字段: - password: 密码 - old_password: 旧密码 - new_password: 新密码 - confirm_password: 确认密码 使用示例: >>> params = {'username': 'admin', 'password': '123456'} >>> result = format_params(params) >>> # 结果: {"username": "admin", "password": "******"} """ # 空值判断 if not params: return "" try: # 敏感字段列表 sensitive_fields = ['password', 'old_password', 'new_password', 'confirm_password'] # 如果是字典类型,需要过滤敏感信息 if isinstance(params, dict): # 复制字典,避免修改原数据 params_copy = params.copy() # 遍历敏感字段,替换值 for field in sensitive_fields: if field in params_copy: params_copy[field] = '******' # 转换为JSON字符串 return json.dumps(params_copy, ensure_ascii=False) # 非字典类型直接转换 return json.dumps(params, ensure_ascii=False) except Exception: # 转换失败时,返回字符串表示(限制500字符) return str(params)[:500] def get_method_name(request): """ 获取请求方法名(如:user.add) ============================================================= 从Django的路由解析结果中获取视图方法名称。 Args: request: HttpRequest对象 Returns: str: 方法名称,获取失败返回空字符串 使用场景: - 操作日志记录,标识具体执行的方法 - 权限验证,根据方法名判断权限 注意: - 需要在URL配置中定义了name属性才能获取到 - 如果未定义name,返回空字符串 """ # 从request.resolver_match获取路由名称 if hasattr(request, 'resolver_match') and request.resolver_match: return request.resolver_match.view_name or '' return '' def format_result(result): """ 格式化返回结果 ============================================================= 将返回结果转换为适合存储的字符串格式,支持JsonResponse、字典等类型。 Args: result: 返回结果(JsonResponse 或其他类型) Returns: str: 格式化后的结果字符串 处理逻辑: 1. 空值判断 2. JsonResponse类型:解析content内容 3. 字典类型:转换为JSON字符串 4. 其他类型:转换为字符串 5. 长度超过2000字符时截断 使用场景: - 操作日志记录,保存接口返回结果 - 接口调试,查看返回数据 使用示例: >>> response = R.ok(data={'id': 1, 'name': '张三'}) >>> result_str = format_result(response) >>> # 结果: {"code": 0, "data": {"id": 1, "name": "张三"}, "msg": "操作成功", "ok": true} """ # 空值判断 if not result: return '' try: # 处理 JsonResponse 对象(R.ok 或 R.failed 返回的类型) if hasattr(result, 'content') and result.content: # 解码响应内容 content = result.content.decode('utf-8') if content: try: # 尝试解析并格式化 JSON data = json.loads(content) result_str = json.dumps(data, ensure_ascii=False) except: # 不是 JSON 直接返回字符串 result_str = content # 限制长度,避免数据库字段溢出 if len(result_str) > 10000: result_str = truncate_json(result_str, 10000) return result_str return None # 处理字典类型 elif isinstance(result, dict): result_str = json.dumps(result, ensure_ascii=False) if len(result_str) > 10000: result_str = truncate_json(result_str, 10000) return result_str # 处理其他类型 else: result_str = str(result) if len(result_str) > 10000: result_str = truncate_json(result_str, 10000) return result_str except Exception as e: # 记录异常日志 logger.error(f"格式化返回结果异常: {e}") # 返回截断后的字符串 return truncate_json(str(result), 10000) if result else '' def truncate_json(json_str, max_len): """ 截断JSON字符串 ============================================================= 将超长的字符串截断,添加省略号提示。 Args: json_str: JSON字符串 max_len: 最大长度 Returns: str: 截断后的字符串 处理逻辑: 1. 空值判断,返回空字符串 2. 长度未超限,返回原字符串 3. 长度超限,截取前max_len个字符,添加省略号 使用示例: >>> long_text = 'a' * 3000 >>> result = truncate_json(long_text, 2000) >>> # 结果: 'a' * 2000 + '...' """ # 空值判断 if not json_str: return '' # 长度判断 if len(json_str) <= max_len: return json_str # 截断并添加省略号 return json_str[:max_len] + '...' def parse_request_body(request): """ 解析请求体JSON数据 Args: request: HttpRequest对象 Returns: tuple: (data, error) 成功时 data 为解析后的字典,error 为 None 失败时 data 为 None,error 为错误描述字符串 Examples: >>> data, error = parse_request_body(request) >>> if error: ... return R.failed(error) >>> username = data.get('username') """ try: # 检查请求体是否存在 if not hasattr(request, 'body') or request.body is None: # 没有请求体,返回空数据但不报错 return None, None # 获取请求体内容 if not request.body: return None, "参数不能为空" json_data = request.body.decode('utf-8') if not json_data: return None, "参数不能为空" # 解析JSON data = json.loads(json_data) return data, None except json.JSONDecodeError as e: logging.error(f"JSON解析异常: {str(e)}") return None, "参数格式错误" except UnicodeDecodeError as e: logging.error(f"字符编码异常: {str(e)}") return None, "参数编码错误" except Exception as e: logging.error(f"解析请求体异常: {str(e)}") return None, "参数处理失败" def format_datetime(dt, format_str='%Y-%m-%d %H:%M:%S'): """ 格式化日期时间 Args: dt: datetime对象 format_str: 日期时间格式字符串,默认为 '%Y-%m-%d %H:%M:%S' Returns: str: 格式化后的日期字符串,如果输入为None则返回None """ return dt.strftime(format_str) if dt else None