diff --git a/Makefile b/Makefile index 60dc19d..043cc26 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,11 @@ PYTHON ?= python PIP ?= $(PYTHON) -m pip FLASK ?= $(PYTHON) -m flask +# 说明:.env 中 FLASK_VERSION=v2.8.0 会被 Flask CLI 误解析为 --version 布尔标志, +# 导致 flask db 系列命令报错。故 db-* 目标显式跳过 CLI 自动加载 .env +# (config/__init__.py 会自行加载 .env,应用配置不受影响)。 +FLASK_ENV_PREFIX := FLASK_SKIP_DOTENV=1 FLASK_APP=app.py + .PHONY: help install install-dev dev lint format type-check test test-cov \ db-migrate db-upgrade db-downgrade docker-up docker-down clean @@ -47,13 +52,13 @@ test-cov: ## 带覆盖率运行测试 # ---- 数据库迁移 ---- db-migrate: ## 生成迁移脚本(模型变更后) - $(FLASK) db migrate + $(FLASK_ENV_PREFIX) $(FLASK) db migrate db-upgrade: ## 应用迁移到最新 - $(FLASK) db upgrade + $(FLASK_ENV_PREFIX) $(FLASK) db upgrade db-downgrade: ## 回滚一次迁移 - $(FLASK) db downgrade + $(FLASK_ENV_PREFIX) $(FLASK) db downgrade # ---- Docker ---- docker-up: ## 启动 Docker 服务 diff --git a/application/constants/common.py b/application/constants/common.py index df85b4f..63f55a7 100644 --- a/application/constants/common.py +++ b/application/constants/common.py @@ -1,85 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 珊瑚云团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== +""" +兼容 shim(绞杀者迁移期间保留) +============================================================= -# ======================= 分页配置 ========================== -# 该部分配置用于控制列表页面的数据分页显示 -# 通过调整每页记录数可以优化页面加载速度和用户体验 -# ========================================================== - -# 默认每页显示记录数 -# 类型:int -# 默认值:10 -# 说明:列表页面默认每页显示的数据条数,可根据实际需求调整 -PAGE_SIZE = 10 - -# ======================= 用户配置 ========================== -# 该部分配置用于管理用户账户相关的默认设置 -# 包含新用户创建和密码重置时的默认值 -# ========================================================== - -# 默认密码 -# 类型:str -# 默认值:"123456" -# 说明:新创建用户或重置密码时的默认密码,建议首次登录后强制修改 -DEFAULT_PASSWORD = "123456" - -# ======================= 消息提示配置 ========================== -# 该部分配置定义了所有操作响应的统一消息文本 -# 用于保持系统前后端消息提示的一致性 -# ========================================================== - -# 操作成功提示信息 -# 类型:str -# 默认值:"操作成功" -# 说明:统一的操作成功返回消息,所有成功操作均使用此消息 -MESSAGE_OK = "操作成功" - -# 操作失败提示信息 -# 类型:str -# 默认值:"操作失败" -# 说明:统一的操作失败返回消息,所有失败操作均使用此消息 -MESSAGE_FAIL = "操作失败" - -# ======================= 序列化安全配置 ========================== -# 模型序列化时统一排除的敏感字段,防止密码、盐值等泄露给前端 -# ================================================================== - -SERIALIZE_EXCLUDE_FIELDS = frozenset({'is_delete', 'password', 'salt'}) +已迁移至 src/common/constants/common.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 +""" +from src.common.constants.common import * # noqa: F401,F403 diff --git a/application/constants/dict.py b/application/constants/dict.py index 6e23df8..9416745 100644 --- a/application/constants/dict.py +++ b/application/constants/dict.py @@ -1,195 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 珊瑚云团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -系统常量定义模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块定义了系统中使用的各种常量字典,主要用于: -1. 将数据库中的数字/代码值转换为可读的文本标签 -2. 为前端下拉框、单选框、复选框等提供选项数据 -3. 统一管理系统中所有枚举值的映射关系 - -所有常量均为字典类型,键为存储值,值为显示文本。 +已迁移至 src/common/constants/dictionary_constants.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -# ==================== 基础信息常量 ==================== - -# 性别映射字典 -# 用于用户信息中的性别字段显示 -GENDER_LIST = { - 1: "男", # 男性 - 2: "女", # 女性 - 3: "保密", # 保密/未公开 -} - -# ==================== 通用状态常量 ==================== - -# 状态映射字典 -# 用于系统中各类数据的状态字段显示 -# 1-正常(启用、有效、激活等状态) -# 2-禁用(停用、无效、冻结等状态) -STATUS_LIST = { - 1: "正常", # 正常状态 - 2: "禁用", # 禁用状态 -} - -# ==================== 参数相关常量 ==================== - -# 参数类型映射字典 -# 用于系统参数管理中的参数类型字段显示 -# 0-系统参数(系统级配置,影响系统运行) -# 1-业务参数(业务级配置,影响业务逻辑) -PARAM_TYPE_LIST = { - 0: "系统参数", # 系统参数 - 1: "业务参数", # 业务参数 -} - -# ==================== 友情链接常量 ==================== - -# 友链类型映射字典 -# 用于区分友情链接的不同合作性质 -LINK_TYPE_LIST = { - 1: "友情链接", # 普通友情链接 - 2: "合作伙伴" # 合作伙伴链接(关系更紧密) -} - -# 友链形式映射字典 -# 用于指定友情链接的展示形式 -LINK_FORM_LIST = { - 1: "文字链接", # 纯文本形式的链接 - 2: "图片链接", # 带有Logo/图片的链接 -} - -# ==================== 配置相关常量 ==================== - -# 配置项类型映射字典 -# 用于定义系统配置项的数据类型和对应的输入控件 -CONFIG_ITEM_TYPE_LIST = { - "text": "单行文本", # 普通单行文本输入框 - "textarea": "多行文本", # 多行文本域 - "ueditor": "富文本编辑器", # 富文本编辑器(支持图文排版) - "date": "日期", # 日期选择器 - "datetime": "时间", # 日期时间选择器 - "number": "数字", # 数字输入框 - "select": "下拉框", # 下拉选择框 - "radio": "单选框", # 单选按钮组 - "checkbox": "复选框", # 复选框组(多选) - "image": "单张图片", # 单张图片上传 - "images": "多张图片", # 多张图片上传 - "password": "密码", # 密码输入框 - "icon": "字体图标", # 字体图标选择器 - "file": "单个文件", # 单个文件上传 - "files": "多个文件", # 多个文件上传 - "hidden": "隐藏", # 隐藏字段 - "readonly": "只读文本", # 只读文本显示 -} - -# ==================== 地区相关常量 ==================== - -# 城市级别映射字典 -# 用于标识行政区划的级别 -CITY_LEVEL_LIST = { - 1: "省份", # 省级行政区(省、自治区、直辖市) - 2: "城市", # 地级行政区(市、自治州) - 3: "县区", # 县级行政区(区、县、县级市) - 4: "街道" # 乡级行政区(街道、镇、乡) -} - -# 通知类型:1-通知 2-公告 -NOTICE_TYPE_LIST = { - 1: "通知", - 2: "公告", -} - -# ==================== 操作日志相关常量 ==================== - -# 操作日志来源映射字典 -# 用于标识操作日志的请求来源 -OPERATION_LOG_SOURCE_LIST = { - 0: "后台系统", # 后台管理系统操作 - 1: "手机站", # 手机端/移动端操作 - 2: "客户端", # PC客户端/应用程序操作 - 3: "其他", # 其他来源 -} - -# 操作日志类型映射字典 -# 用于标识操作日志的具体操作类型 -OPERATION_LOG_TYPE_LIST = { - 1: "登录", - 2: "退出", - 3: "新增", - 4: "修改", - 5: "删除", - 6: "查询", - 7: "导出", - 8: "导入", - 9: "提交", - 10: "撤销", - 11: "审核", - 12: "强退", - 13: "开账号", - 14: "清空", - 15: "授权", - 16: "缓存", - 17: "生成代码", - 18: "其他", -} - -# ==================== 登录日志相关常量 ==================== - -# 登录日志类型映射字典 -# 用于标识登录日志的操作类型 -LOGIN_LOG_TYPE_LIST = { - 1: "登录", - 2: "退出", -} - -# ==================== 文章相关常量 ==================== - -# 文章状态映射字典 -# 用于文章管理中的状态字段显示 -# 0-正常(已发布、上架状态) -# 1-下架(已下架、草稿状态) -ARTICLE_STATUS_LIST = { - 0: "正常", # 正常/已发布 - 1: "下架", # 下架/草稿 -} +from src.common.constants.dictionary_constants import * # noqa: F401,F403 diff --git a/application/factory.py b/application/factory.py index ad26c24..6f28e98 100644 --- a/application/factory.py +++ b/application/factory.py @@ -66,6 +66,9 @@ from config import DatabaseConfig, REDIS_URL from extensions import register_extends from utils.json import JSONEncoder +# 模型注册中心:确保 Flask-Migrate 全量识别表结构(见 src/models_registry.py) +import src.models_registry # noqa: F401 + def create_app(): """ diff --git a/application/forms/base_form.py b/application/forms/base_form.py index 06f2c46..c36d992 100644 --- a/application/forms/base_form.py +++ b/application/forms/base_form.py @@ -1,105 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== -import json +""" +兼容 shim(绞杀者迁移期间保留) +============================================================= -from flask_wtf import FlaskForm - -from application.forms.fields import ArrayField - - -class BaseForm(FlaskForm): - """ - 自定义表单基类,增强对 JSON 数组的支持 - - 重写 process 方法,确保 ArrayField 能正确处理数组数据。 - - 使用方式: - class UserForm(BaseForm): - city = ArrayField('行政区划') - roles = ArrayField('用户角色') - """ - - def process(self, formdata=None, obj=None, data=None, **kwargs): - """ - 重写 process 方法,正确处理数组字段 - - 问题的根源:Flask-WTF 的 Form.process 方法在处理 data 参数时, - 会调用 _process_multidict 方法,该方法会将列表值当作多值参数处理, - 导致数组被拆分成单个值。 - - 解决方案:在调用父类方法之前,先处理 ArrayField 类型的数据, - 将列表值转换为 JSON 字符串,避免被拆分。 - - Args: - formdata: 表单数据(通常是 request.form) - obj: 模型对象 - data: 字典数据(通常是 request.get_json()) - **kwargs: 其他参数 - """ - # 如果传入了 data 字典 - if data is not None and isinstance(data, dict): - # 复制一份数据,避免修改原始数据 - processed_data = {} - - for key, value in data.items(): - # 检查对应的字段是否是 ArrayField - field = getattr(self, key, None) - - # 如果是 ArrayField 且值是列表,转换为 JSON 字符串 - # 这样可以避免父类方法将数组拆分成单个值 - if isinstance(field, ArrayField) and isinstance(value, list): - processed_data[key] = json.dumps(value) - else: - # 其他字段保持原值 - processed_data[key] = value - - # 使用处理后的数据调用父类方法 - super().process(formdata, obj, processed_data, **kwargs) - - # 重要:父类处理完成后,将 ArrayField 的 data 从 JSON 字符串转换回列表 - for key, value in data.items(): - field = getattr(self, key, None) - if isinstance(field, ArrayField) and isinstance(value, list): - # 直接设置 data 属性为原始列表 - field.data = value - else: - # 没有 data 参数时,直接调用父类方法 - super().process(formdata, obj, data, **kwargs) +表单基类已迁移至 src/common/forms/base_form.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 +""" +from src.common.forms.base_form import * # noqa: F401,F403 diff --git a/application/forms/fields.py b/application/forms/fields.py index 7f9c4ff..9e5faea 100644 --- a/application/forms/fields.py +++ b/application/forms/fields.py @@ -1,177 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -通用表单验证模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -提供通用的表单验证功能,包括: -- 自定义数组字段类 ArrayField -- 自定义表单基类 BaseForm(支持JSON数组) +自定义表单字段已迁移至 src/common/forms/fields.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import json -from wtforms import Field - - -# ============================================================ -# 自定义数组字段类 -# ============================================================ - -class ArrayField(Field): - """ - 自定义数组字段,用于接收 JSON 数组数据 - - 使用方式: - city = ArrayField(label='行政区划', validators=[DataRequired()]) - roles = ArrayField(label='用户角色', validators=[Optional()]) - - 工作原理: - 1. process_data: 处理通过 data= 参数传入的数据 - 2. process_formdata: 处理传统表单提交的数据 - 3. _value: 用于表单渲染时显示 - """ - - def __init__(self, label=None, validators=None, **kwargs): - """ - 初始化数组字段 - - Args: - label: 字段标签 - validators: 验证器列表 - **kwargs: 其他参数 - """ - super().__init__(label, validators, **kwargs) - self.data = [] # 默认空数组 - - def process_data(self, value): - """ - 处理直接赋值的数据(通过 data= 参数传入) - - 这是关键方法,当使用 UserForm(data=json_data) 时会调用此方法。 - - Args: - value: 传入的值,可能是 list、str、None 或其他类型 - """ - # 空值处理 - if value is None: - self.data = [] - return - - # 如果已经是列表,直接使用(这是JSON数组的情况) - if isinstance(value, list): - self.data = value - return - - # 如果是字符串,尝试解析为 JSON - if isinstance(value, str): - # 去除首尾空格 - value = value.strip() - if not value: - self.data = [] - return - - try: - parsed = json.loads(value) - # 确保解析结果是列表 - if isinstance(parsed, list): - self.data = parsed - else: - # 单个值转为列表 - self.data = [parsed] if parsed is not None else [] - except (json.JSONDecodeError, TypeError): - # 不是 JSON,作为单值列表 - self.data = [value] if value else [] - return - - # 其他类型(如 int、float 等)转为单值列表 - self.data = [value] if value is not None else [] - - def process_formdata(self, valuelist): - """ - 处理表单数据(通过 request.form 提交的数据) - - Args: - valuelist: 表单值列表,通常是一个列表 - """ - # 空值处理 - if not valuelist: - self.data = [] - return - - # 获取第一个值 - value = valuelist[0] - - # 如果是列表类型(来自多选或多文件上传) - if isinstance(value, list): - self.data = value - return - - # 如果是字符串,尝试解析 JSON - if isinstance(value, str): - value = value.strip() - if not value: - self.data = [] - return - - try: - parsed = json.loads(value) - if isinstance(parsed, list): - self.data = parsed - else: - self.data = [parsed] if parsed is not None else [] - except (json.JSONDecodeError, TypeError): - self.data = [value] if value else [] - return - - # 其他类型转为单值列表 - self.data = [value] if value is not None else [] - - def _value(self): - """ - 渲染表单时使用,将列表转为 JSON 字符串 - - Returns: - str: JSON 字符串表示的数组 - """ - if self.data and isinstance(self.data, list): - return json.dumps(self.data, ensure_ascii=False) - return '[]' +from src.common.forms.fields import * # noqa: F401,F403 diff --git a/application/middleware/.gitignore b/application/middleware/.gitignore index c96a04f..7c4c961 100644 --- a/application/middleware/.gitignore +++ b/application/middleware/.gitignore @@ -1,2 +1,8 @@ -* -!.gitignore \ No newline at end of file +# ============================================================ +# application/middleware/ 目录专用忽略 +# ============================================================ +# 说明: +# 1. 原文件内容为 `*`,误将整个中间件源码层排除在版本库外, +# 已修正。中间件源码必须入库。 +# 2. 缓存目录由根目录 .gitignore 统一处理。 +# ============================================================ diff --git a/application/middleware/__init__.py b/application/middleware/__init__.py new file mode 100644 index 0000000..217a675 --- /dev/null +++ b/application/middleware/__init__.py @@ -0,0 +1,88 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +中间件注册模块 +============================================================= + +该模块负责注册 Flask 应用的请求/响应中间件。 +""" + +from application.middleware.check_login import check_login +from application.middleware.check_permission import check_permission + + +def register_middleware(app): + """ + 注册应用中间件 + + 将所有的请求/响应钩子函数注册到 Flask 应用实例中。 + + Args: + app: Flask 应用实例 + + 使用示例: + from application.middleware import register_middleware + + def create_app(): + app = Flask(__name__) + register_middleware(app) + return app + """ + + # ============================================================ + # 请求前钩子 (before_request) + # ============================================================ + # 在每个请求被视图函数处理之前执行 + # 用于:登录验证、权限检查、请求日志、限流等 + @app.before_request + def checklogin(): + """ + 登录验证中间件 + + 在每个请求处理之前检查用户是否已登录。 + 白名单中的路径(如登录页、验证码接口)跳过验证。 + + Returns: + None: 验证通过,继续处理请求 + Response: 验证失败,返回未授权响应 + """ + return check_login() diff --git a/application/middleware/check_demo.py b/application/middleware/check_demo.py new file mode 100644 index 0000000..58c0c05 --- /dev/null +++ b/application/middleware/check_demo.py @@ -0,0 +1,39 @@ +""" +演示模式拦截装饰器 +============================================================= + +在演示环境(FLASK_DEMO=True)下拦截所有写操作, +返回"演示环境,暂无操作权限"提示。 + +使用方式: + 在 View 路由函数上添加 @check_demo 装饰器, + 替代原有的 if FLASK_DEMO: return R.failed(...) 判断。 +""" + +from functools import wraps + +from config import FLASK_DEMO +from utils import R + + +def check_demo(func): + """ + 演示模式拦截装饰器 + + 当 FLASK_DEMO=True 时,拦截写操作并返回提示。 + 应放在 @check_permission 之后、路由函数之前。 + + Example: + @position_blue.route('/add', methods=["POST"]) + @check_permission("sys:position:add") + @check_demo + def add(): + result = position.add_position() + return result + """ + @wraps(func) + def wrapper(*args, **kwargs): + if FLASK_DEMO: + return R.failed("演示环境,暂无操作权限") + return func(*args, **kwargs) + return wrapper diff --git a/application/middleware/check_login.py b/application/middleware/check_login.py new file mode 100644 index 0000000..f1d64dd --- /dev/null +++ b/application/middleware/check_login.py @@ -0,0 +1,200 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +import fnmatch +import logging + +from flask import request, g + +from config import LOGIN_WHITELIST_EXACT, LOGIN_WHITELIST_DEBUG, LOGIN_WHITELIST_PREFIX, LOGIN_WHITELIST_WILDCARD +from utils import R +from utils.jwt import parse_payload, is_token_blacklisted, get_access_token + +# 获取日志记录器 +logger = logging.getLogger(__name__) + + +def is_whitelisted(path: str) -> bool: + """ + 检查请求路径是否在白名单中 + + 支持三种匹配模式: + 1. 精确匹配:路径完全相等 + 2. 前缀匹配:路径以指定前缀开头 + 3. 通配符匹配:支持 *(匹配任意字符)和 ?(匹配单个字符) + + Args: + path: 请求路径(如:/job/sendSms) + + Returns: + bool: True-在白名单中,False-不在白名单中 + """ + # ============================================================ + # 1. 精确匹配检查 + # ============================================================ + # 示例:LOGIN_WHITELIST_EXACT=/login,/captcha + # 当 path 为 /login 或 /captcha 时返回 True + exact_urls = LOGIN_WHITELIST_EXACT + if exact_urls: + for url in exact_urls.split(','): + if url.strip() and url.strip() == path: + if LOGIN_WHITELIST_DEBUG: + logger.debug(f"精确匹配白名单: {path}") + return True + + # ============================================================ + # 2. 前缀匹配检查 + # ============================================================ + # 示例:LOGIN_WHITELIST_PREFIX=/job/sendSms,/public + # 当 path 以 /job/sendSms 或 /public 开头时返回 True + prefix_urls = LOGIN_WHITELIST_PREFIX + if prefix_urls: + for prefix in prefix_urls.split(','): + prefix = prefix.strip() + if prefix and path.startswith(prefix): + if LOGIN_WHITELIST_DEBUG: + logger.debug(f"前缀匹配白名单: {path} 匹配 {prefix}") + return True + + # ============================================================ + # 3. 通配符匹配检查 + # ============================================================ + # 示例:LOGIN_WHITELIST_WILDCARD=/job/*/send*,/api/v1/*/public* + # 当 path 匹配通配符模式时返回 True + # - * 匹配任意多个字符 + # - ? 匹配单个字符 + wildcard_patterns = LOGIN_WHITELIST_WILDCARD + if wildcard_patterns: + for pattern in wildcard_patterns.split(','): + pattern = pattern.strip() + if pattern and fnmatch.fnmatch(path, pattern): + if LOGIN_WHITELIST_DEBUG: + logger.debug(f"通配符匹配白名单: {path} 匹配 {pattern}") + return True + + return False + + +def check_login(): + """ + 用户登录验证中间件 + + 功能说明: + 1. 检查请求路径是否在白名单中,白名单路径直接放行 + 2. OPTIONS请求(CORS预检)直接放行 + 3. 验证JWT Token的有效性 + 4. Token验证通过后将用户信息存储到g对象中 + + 白名单配置(在.env文件中): + - LOGIN_WHITELIST_EXACT: 精确匹配白名单 + - LOGIN_WHITELIST_PREFIX: 前缀匹配白名单 + - LOGIN_WHITELIST_WILDCARD: 通配符匹配白名单 + + Returns: + None: 验证通过,继续处理请求 + R.failed: 验证失败,返回错误响应(401未登录) + + 使用示例: + # 在app.py中注册中间件 + @app.before_request + def before_request(): + result = check_login() + if result: + return result + """ + current_path = request.path + + # ============================================================ + # 步骤1:检查白名单 + # ============================================================ + # 白名单中的路径不需要验证Token(如:登录页、验证码、短信发送接口等) + if is_whitelisted(current_path): + if LOGIN_WHITELIST_DEBUG: + logger.debug(f"白名单路径,跳过登录验证: {current_path}") + return None + + # ============================================================ + # 步骤2:OPTIONS请求放行 + # ============================================================ + # OPTIONS请求是CORS预检请求,不需要验证Token + if request.method == "OPTIONS": + if LOGIN_WHITELIST_DEBUG: + logger.debug(f"OPTIONS请求,跳过登录验证: {current_path}") + return None + + # ============================================================ + # 步骤3:获取并验证Token + # ============================================================ + success, access_token, msg = get_access_token() + + # 如果没有Token + if not success: + logger.warning(f"未登录访问: {current_path}, IP: {request.remote_addr}") + return R.failed(code=401, msg="请先登录") + + # JWT解密验证 + result = parse_payload(access_token) + + # 验证失败处理 + if result.get('code') != 0: + logger.warning(f"Token验证失败: {result.get('msg')}, 路径: {current_path}") + return R.failed(code=401, msg=result.get('msg', "Token无效,请重新登录")) + + # ============================================================ + # 步骤3.5:检查Token黑名单 + # ============================================================ + # 退出登录时Token会被加入黑名单,黑名单中的Token即使未过期也应拒绝 + if is_token_blacklisted(access_token): + logger.warning(f"Token已被注销(黑名单): user_id={result.get('data', {}).get('userId')}, 路径: {current_path}") + return R.failed(code=401, msg="Token已失效,请重新登录") + + # ============================================================ + # 步骤4:存储用户信息 + # ============================================================ + # 将用户信息存储到g对象中,供后续的视图函数和权限验证使用 + if result.get('data'): + g.user_id = result['data'].get('userId') + g.user_info = result['data'] + if LOGIN_WHITELIST_DEBUG: + logger.debug(f"用户登录验证通过: user_id={g.user_id}, 路径={current_path}") + + # 验证通过,返回None继续处理请求 + return None diff --git a/application/middleware/check_permission.py b/application/middleware/check_permission.py new file mode 100644 index 0000000..d7aa847 --- /dev/null +++ b/application/middleware/check_permission.py @@ -0,0 +1,181 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +import fnmatch +import hmac +import logging +from functools import wraps + +from flask import request, g + +from application.services import menu +from config import PERMISSION_ENABLED, INTERNAL_API_KEY, INTERNAL_ALLOWED_IPS +from utils import R +from utils.security import get_user_id + +# 获取日志记录器 +logger = logging.getLogger(__name__) + + +def is_internal_call() -> bool: + """ + 判断是否为内部系统调用(仅定时任务等无用户场景) + + 验证方式(满足任一即可): + 1. 权限验证总开关关闭(开发环境) + 2. API密钥验证通过(请求头携带正确密钥) + 3. IP白名单验证通过(来源IP在白名单中) + + Returns: + bool: True-内部调用(跳过权限验证),False-普通调用(需要权限验证) + """ + # ============================================================ + # 方式1:权限验证总开关关闭(开发/测试环境) + # ============================================================ + if not PERMISSION_ENABLED: + logger.debug("权限验证已关闭,所有请求视为内部调用") + return True + + # ============================================================ + # 方式2:API密钥验证(定时任务使用) + # ============================================================ + # 定时任务在请求头中携带 X-Scheduler-Key 或 X-Internal-API-Key + if INTERNAL_API_KEY: + # 优先检查调度器专用密钥 + req_key = request.headers.get('X-Scheduler-Key', '') + if not req_key: + req_key = request.headers.get('X-Internal-API-Key', '') + + if hmac.compare_digest(req_key, INTERNAL_API_KEY): + logger.debug(f"内部API密钥验证通过: IP={request.remote_addr}") + return True + + # ============================================================ + # 方式3:IP白名单验证(可选) + # ============================================================ + if INTERNAL_ALLOWED_IPS: + client_ip = request.remote_addr + allowed_ips = [ip.strip() for ip in INTERNAL_ALLOWED_IPS.split(',') if ip.strip()] + + for allowed_ip in allowed_ips: + if fnmatch.fnmatch(client_ip, allowed_ip): + logger.debug(f"内部IP白名单验证通过: IP={client_ip}") + return True + + return False + + +def check_permission(permission: str, log: bool = False): + """ + 权限验证装饰器 + + 权限验证规则: + 1. 定时任务等内部调用 → 跳过权限验证,直接放行 + 2. 超级管理员(user_id=1) → 拥有所有权限 + 3. 普通用户 → 验证是否拥有指定权限节点 + + Args: + permission: 权限节点标识(如:sys:job:page) + log: 是否记录访问日志 + + Returns: + decorator: 装饰器函数 + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + # ============================================================ + # 定时任务等内部调用:跳过权限验证,直接放行 + # ============================================================ + if is_internal_call(): + logger.debug(f"定时任务内部调用,跳过权限验证: {func.__name__}") + return func(*args, **kwargs) + + # ============================================================ + # 普通请求:正常进行权限验证 + # ============================================================ + + # 获取用户ID(优先从g对象获取) + user_id = getattr(g, 'user_id', None) + if user_id is None: + try: + user_id = get_user_id() + except Exception as e: + logger.error(f"获取用户ID失败: {str(e)}") + user_id = None + + # 超级管理员放行 + if user_id == 1: + logger.debug(f"超级管理员权限验证通过: user_id={user_id}") + return func(*args, **kwargs) + + # 普通用户权限验证 + if user_id: + try: + permission_list = menu.get_user_permissions(user_id) + + if permission not in permission_list: + if log: + logger.warning( + f"权限不足: user_id={user_id}, " + f"permission={permission}, " + f"path={request.path}" + ) + return R.failed(msg="权限不足!", code=403) + elif log: + logger.info( + f"权限验证通过: user_id={user_id}, " + f"permission={permission}" + ) + except Exception as e: + logger.error(f"权限验证异常: {str(e)}") + return R.failed(msg="权限验证失败", code=500) + else: + # 未登录用户 + logger.warning(f"未登录用户访问: path={request.path}") + return R.failed(msg="请先登录", code=401) + + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/application/middleware/error_handler.py b/application/middleware/error_handler.py new file mode 100644 index 0000000..720105f --- /dev/null +++ b/application/middleware/error_handler.py @@ -0,0 +1,83 @@ +""" +全局异常处理模块 +============================================================= + +注册 Flask errorhandler,将所有 HTTP 错误和未捕获异常 +统一转为 R.failed() JSON 响应,避免前端收到 HTML 错误页导致解析崩溃。 + +处理范围: + - 400:请求参数错误 + - 401:未授权 + - 403:权限不足 + - 404:资源不存在 + - 405:请求方法不允许 + - 500:服务器内部错误 + - Exception:兜底,捕获所有未预期异常 +""" + +import logging +import traceback + +from flask import request +from werkzeug.exceptions import HTTPException + +from utils import R + +logger = logging.getLogger(__name__) + + +def register_error_handlers(app): + """ + 注册全局异常处理器 + + 确保所有 HTTP 错误和未捕获异常都返回标准 JSON 响应。 + + Args: + app: Flask 应用实例 + """ + + @app.errorhandler(400) + def bad_request(e): + return R.failed(msg="请求参数错误", code=400), 400 + + @app.errorhandler(401) + def unauthorized(e): + return R.failed(msg="未授权,请重新登录", code=401), 401 + + @app.errorhandler(403) + def forbidden(e): + return R.failed(msg="权限不足", code=403), 403 + + @app.errorhandler(404) + def not_found(e): + return R.failed(msg="请求的资源不存在", code=404), 404 + + @app.errorhandler(405) + def method_not_allowed(e): + return R.failed(msg="请求方法不允许", code=405), 405 + + @app.errorhandler(500) + def internal_error(e): + logger.error( + f"服务器内部错误, method: {request.method}, path: {request.path}, " + f"error: {str(e)}", + exc_info=True + ) + msg = f"服务器内部错误: {str(e)}" if app.debug else "服务器内部错误" + return R.failed(msg=msg, code=500), 500 + + @app.errorhandler(Exception) + def handle_exception(e): + # HTTPException 子类(如 abort(403))已有专用 handler,不重复处理 + if isinstance(e, HTTPException): + return e.get_response() + + # 非HTTP异常:未知运行时异常 + logger.error( + f"未捕获异常, method: {request.method}, path: {request.path}, " + f"error: {traceback.format_exc()}" + ) + msg = f"服务器内部错误: {str(e)}" if app.debug else "服务器内部错误" + return R.failed(msg=msg, code=500), 500 + + logger.info("全局异常处理器注册成功") diff --git a/application/middleware/op_log.py b/application/middleware/op_log.py new file mode 100644 index 0000000..768f691 --- /dev/null +++ b/application/middleware/op_log.py @@ -0,0 +1,220 @@ +""" +操作日志自动记录中间件 +============================================================= + +通过 after_request 钩子自动记录写操作(POST/PUT/DELETE/PATCH)日志。 +无需在每个视图函数中手动调用 record_operation_log。 + +记录规则: + - 只记录写操作(POST/PUT/DELETE/PATCH),GET/OPTIONS/HEAD 不记录 + - 只记录成功操作(HTTP 2xx 且业务 code=0) + - 内部调用(定时任务等)不记录 + - 登录/退出/验证码等路径不记录(由 login_log 单独记录) + - 日志记录失败不影响主流程 +""" + +import logging +import time + +from flask import g, request + +from application.models.operation_log import OperationLog + +logger = logging.getLogger(__name__) + +# 不记录操作日志的路径前缀(登录/退出/验证码等由 login_log 单独记录) +_EXCLUDED_PREFIXES = ('/login', '/captcha', '/logout') + +# HTTP 方法 → 操作日志类型 映射 +# 类型值参考 application/constants/dict.py 的 OPERATION_LOG_TYPE_LIST +_METHOD_LOG_TYPE = { + 'POST': 3, # 新增 + 'PUT': 4, # 修改 + 'DELETE': 5, # 删除 + 'PATCH': 4, # 修改 +} + +# 路径后缀 → 操作日志类型 覆盖映射(更精确) +_PATH_ACTION_TYPE = { + '/add': 3, # 新增 + '/update': 4, # 修改 + '/delete': 5, # 删除 + '/status': 4, # 状态变更→修改 + '/batchdelete': 5, # 批量删除 + '/reset': 4, # 重置→修改 + '/save': 4, # 保存→修改 + '/sort': 4, # 排序→修改 +} + + +def register_operation_log(app): + """ + 注册操作日志自动记录中间件 + + 在 after_request 中自动记录写操作日志。 + + Args: + app: Flask 应用实例 + """ + + @app.after_request + def record_op_log(response): + # 只记录写操作 + if request.method not in ('POST', 'PUT', 'DELETE', 'PATCH'): + return response + + # 排除登录/退出/验证码等路径 + path = request.path + if any(path.startswith(prefix) for prefix in _EXCLUDED_PREFIXES): + return response + + # 只记录成功操作(HTTP 2xx 且业务 code=0) + if not (200 <= response.status_code < 300): + return response + + try: + data = response.get_json(silent=True) + if data and data.get('code', 0) != 0: + return response + except Exception: + pass + + # 内部调用不记录 + if getattr(g, '_internal_call', False): + return response + + # 异步记录,不阻塞响应 + try: + _write_log(response) + except Exception as e: + logger.warning(f"记录操作日志失败: {str(e)}") + + return response + + logger.info("操作日志中间件注册成功") + + +def _write_log(response): + """写入操作日志""" + from application.services import operation_log + from utils.security import get_username + from utils.ip import get_client_ip + from utils.ip2region import get_ip_location + from utils.user_agent import parse_user_agent + + username = get_username() or 'system' + client_ip = get_client_ip() + ua_info = parse_user_agent() + + # 确定操作类型 + log_type = _infer_log_type(request.method, request.path) + + # 构建标题:从路径提取模块名,如 /user/add → "用户管理-新增" + title = _build_title(request.path) + + # 请求参数(限制长度,避免存入过大的数据) + param = _get_request_param() + + # 返回结果(限制长度) + result = _get_response_result(response) + + # 请求耗时 + start_time = getattr(g, 'request_start_time', None) + consume_time = int((time.time() - start_time) * 1000) if start_time else 0 + + operation_log.record_operation_log( + title=title, + log_type=log_type, + source=OperationLog.Source.ADMIN, + status=OperationLog.Status.NORMAL, + method=request.method, + request_method=request.endpoint or '', + url=request.path, + param=param, + result=result, + ip=client_ip, + location=get_ip_location(client_ip), + os=ua_info.get('os', '未知'), + browser=ua_info.get('browser', '未知'), + consume_time=consume_time, + error='', + username=username, + ) + + +def _infer_log_type(method, path): + """根据请求方法和路径推断操作日志类型""" + # 优先根据路径后缀匹配 + path_lower = path.lower() + for suffix, log_type in _PATH_ACTION_TYPE.items(): + if path_lower.endswith(suffix): + return log_type + + # 回退到 HTTP 方法映射 + return _METHOD_LOG_TYPE.get(method, 18) # 18=其他 + + +def _get_request_param(): + """获取请求参数""" + try: + if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'): + # 优先解析 JSON body(batchDelete 等 DELETE 请求也带 body) + data = request.get_json(silent=True) + if data: + import json + return json.dumps(data, ensure_ascii=False) + # 回退到 URL 查询参数 + if request.args: + import json + from collections import OrderedDict + return json.dumps(OrderedDict(request.args), ensure_ascii=False) + except Exception: + pass + return '' + + +def _get_response_result(response): + """获取响应结果中的data字段""" + try: + data = response.get_json(silent=True) + if data and isinstance(data, dict): + result_data = data.get('data') + if result_data is not None: + import json + return json.dumps(result_data, ensure_ascii=False) + # data字段为空时取msg + return data.get('msg', '') + except Exception: + pass + return '' + + +def _build_title(path): + """从请求路径构建操作标题""" + # 路径格式: /user/add, /role/update, /menu/delete 等 + parts = path.strip('/').split('/') + if len(parts) >= 2: + module = parts[0] + # 模块名中文化映射 + module_names = { + 'user': '用户管理', 'role': '角色管理', 'menu': '菜单管理', + 'dept': '部门管理', 'position': '岗位管理', 'level': '职级管理', + 'param': '参数配置', 'dict': '字典管理', 'dictitem': '字典项管理', + 'config': '系统配置', 'configitem': '配置项管理', + 'category': '分类管理', 'article': '文章管理', + 'notice': '通知管理', 'link': '友情链接', + 'job': '定时任务', 'login': '登录管理', + 'loginlog': '登录日志', 'operationlog': '操作日志', + 'upload': '文件上传', + } + module_name = module_names.get(module.lower(), module) + action = parts[-1] if len(parts) > 1 else '' + action_names = { + 'add': '新增', 'update': '修改', 'delete': '删除', + 'status': '状态变更', 'batchdelete': '批量删除', + 'reset': '重置', 'save': '保存', 'sort': '排序', + 'page': '查询', 'detail': '查询', 'list': '查询', + } + action_name = action_names.get(action.lower(), action) + return f"{module_name}-{action_name}" if action_name else module_name + return path diff --git a/application/middleware/transaction.py b/application/middleware/transaction.py new file mode 100644 index 0000000..fd67ca3 --- /dev/null +++ b/application/middleware/transaction.py @@ -0,0 +1,241 @@ +""" +AOP 事务拦截器(企业级实现) + +设计理念: + 1. 零代码侵入:业务代码无需任何事务管理代码 + 2. 自动读写分离:GET 不提交,POST/PUT/DELETE 自动提交 + 3. 统一异常处理:所有异常自动回滚 + 4. 性能监控:记录事务执行时间 + +使用方式: + 在 Flask 应用初始化时调用 register_transaction(app) 即可。 + 业务代码无需任何改动。 + +Example: + # app/__init__.py + from application.middleware.transaction import register_transaction + + def create_app(): + app = Flask(__name__) + register_transaction(app) # 一行代码搞定所有事务 + return app +""" + +from flask import request, g +from extensions import db +import logging +import time +from functools import wraps + +logger = logging.getLogger(__name__) + + +# ============================================================ +# 核心:AOP 事务拦截器 +# ============================================================ + +def register_transaction(app): + """ + 注册 AOP 事务拦截器到 Flask 应用 + + 在请求生命周期中自动管理事务: + - before_request: 开启事务标记 + - after_request: 自动提交(POST/PUT/DELETE) + - teardown_request: 异常回滚 + 资源清理 + + Args: + app: Flask 应用实例 + + Example: + register_transaction(app) + """ + + @app.before_request + def begin_transaction(): + """请求开始:记录开始时间,开启事务标记""" + g.request_start_time = time.time() + g.transaction_active = True + + logger.debug( + f"请求开始, method: {request.method}, path: {request.path}, " + f"ip: {request.remote_addr}" + ) + + @app.after_request + def commit_transaction(response): + """ + 请求结束:自动提交事务 + + 提交规则: + 1. 只对写操作(POST/PUT/DELETE/PATCH)提交 + 2. 只在响应成功(2xx)且业务成功(code=0)时提交 + 3. GET/OPTIONS/HEAD 不提交(只读) + 4. 业务失败(R.failed)不提交,自动回滚 + """ + # 只对写操作处理事务 + if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'): + should_commit = 200 <= response.status_code < 300 + + # 检查业务状态码:R.failed() 返回 HTTP 200 但 code=1/ok=False + if should_commit: + try: + data = response.get_json(silent=True) + if data and data.get('code', 0) != 0: + should_commit = False + except Exception: + pass + + if should_commit: + try: + if db.session.is_active: + db.session.commit() + logger.debug( + f"事务提交成功, method: {request.method}, " + f"path: {request.path}, status: {response.status_code}" + ) + except Exception as e: + db.session.rollback() + logger.error( + f"事务提交失败, method: {request.method}, " + f"path: {request.path}, error: {str(e)}", + exc_info=True + ) + from utils import R + return R.failed(msg="数据处理失败"), 500 + else: + # 业务失败或非2xx,回滚未提交的变更 + try: + if db.session.is_active: + db.session.rollback() + except Exception: + pass + + # 记录响应日志 + elapsed = time.time() - g.get('request_start_time', time.time()) + logger.debug( + f"请求结束, method: {request.method}, path: {request.path}, " + f"status: {response.status_code}, elapsed: {elapsed:.3f}s" + ) + + return response + + @app.teardown_request + def cleanup_transaction(exception=None): + """ + 请求结束:清理资源 + + 发生异常时自动回滚事务,确保数据一致性。 + """ + if exception is not None: + try: + db.session.rollback() + logger.error( + f"请求异常,事务已回滚, method: {request.method}, " + f"path: {request.path}, error: {str(exception)}", + exc_info=True + ) + except Exception as e: + logger.error(f"事务回滚失败: {str(e)}", exc_info=True) + + # 清理请求标记 + g.pop('transaction_active', None) + g.pop('request_start_time', None) + + logger.info("AOP 事务拦截器注册成功") + + +# ============================================================ +# 备用:精细控制装饰器(特殊场景使用) +# ============================================================ + +def transactional(func): + """ + 事务装饰器(备用) + + 适用于需要精细控制事务边界的特殊场景。 + 大多数情况下,使用 AOP 拦截器即可。 + + Example: + @transactional + def complex_business(): + # 精细控制的事务逻辑 + pass + """ + + @wraps(func) + def wrapper(*args, **kwargs): + if g.get('disable_auto_commit', False): + return func(*args, **kwargs) + + try: + result = func(*args, **kwargs) + if not g.get('transaction_active', False): + db.session.commit() + return result + except Exception as e: + db.session.rollback() + logger.error(f"装饰器事务执行失败: {str(e)}", exc_info=True) + raise + + return wrapper + + +def transactional_response(func): + """ + 事务装饰器(带返回 R 对象) + + 用于需要直接返回 R 对象的接口。 + + Example: + @transactional_response + def add_position(): + position.save() + return R.ok(msg="添加成功") + """ + + @wraps(func) + def wrapper(*args, **kwargs): + from utils import R + try: + result = func(*args, **kwargs) + if not g.get('transaction_active', False): + db.session.commit() + return result + except Exception as e: + db.session.rollback() + logger.error(f"装饰器事务执行失败: {str(e)}", exc_info=True) + return R.failed(msg=f"操作失败: {str(e)}") + + return wrapper + + +# ============================================================ +# 备用:事务上下文管理器(特殊场景使用) +# ============================================================ + +class TransactionContext: + """ + 事务上下文管理器 + + 用于需要精细控制事务的特殊场景。 + + Example: + with TransactionContext(): + position.save() + user.save() + # 自动提交或回滚 + """ + + def __enter__(self): + g.disable_auto_commit = True + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + g.disable_auto_commit = False + if exc_type is None: + if not g.get('transaction_active', False): + db.session.commit() + else: + db.session.rollback() + logger.error(f"事务上下文回滚: {str(exc_val)}", exc_info=True) + return False diff --git a/application/models/.gitignore b/application/models/.gitignore index c96a04f..307875a 100644 --- a/application/models/.gitignore +++ b/application/models/.gitignore @@ -1,2 +1,8 @@ -* -!.gitignore \ No newline at end of file +# ============================================================ +# application/models/ 目录专用忽略 +# ============================================================ +# 说明: +# 1. 原文件内容为 `*`,误将整个模型源码层排除在版本库外, +# 已修正。模型源码必须入库。 +# 2. 缓存目录由根目录 .gitignore 统一处理。 +# ============================================================ diff --git a/application/models/__init__.py b/application/models/__init__.py new file mode 100644 index 0000000..8c5bca9 --- /dev/null +++ b/application/models/__init__.py @@ -0,0 +1,22 @@ +from . import article +from . import category +from . import city +from . import config +from . import config_item +from . import dept +from . import dict +from . import dict_item +from . import file_template +from . import job +from . import level +from . import link +from . import login_log +from . import menu +from . import notice +from . import operation_log +from . import param +from . import position +from . import role +from . import role_menu +from . import user +from . import user_role diff --git a/application/models/article.py b/application/models/article.py new file mode 100644 index 0000000..3fda4cb --- /dev/null +++ b/application/models/article.py @@ -0,0 +1,186 @@ +""" +文章模型模块 +============================================================= + +提供文章管理的数据模型定义,用于管理文章内容。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 文章模型 +# ============================================================ + +class Article(BaseModel, BaseDB): + """ + 文章模型 + + 用于存储文章信息,包括:分类、标题、封面、图集、导读、内容、作者、状态等。 + + Attributes: + id: 主键ID(继承自 BaseModel) + category_id: 分类ID(Integer,可选) + title: 文章标题(String(150),可选) + cover: 文章封面(String(255),可选) + images: 文章图集(Text,可选,JSON格式存储多张图片) + intro: 文章导读(String(255),可选) + content: 文章内容(Text,可选,富文本内容) + author: 文章作者(String(100),可选) + status: 文章状态(Integer,0-正常,1-下架,默认0) + click: 文章点击率(Integer,默认0) + sort: 文章排序(Integer,默认0) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "article" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 分类ID + category_id = db.Column( + db.Integer, + nullable=True, + comment="分类ID" + ) + + # 文章标题 + title = db.Column( + db.String(150), + nullable=True, + comment="文章标题" + ) + + # 文章封面 + cover = db.Column( + db.String(255), + nullable=True, + comment="文章封面" + ) + + # 文章图集(JSON格式存储多张图片) + images = db.Column( + db.Text, + nullable=True, + comment="文章图集" + ) + + # 文章导读 + intro = db.Column( + db.String(255), + nullable=True, + comment="文章导读" + ) + + # 文章内容 + content = db.Column( + db.Text, + nullable=True, + comment="文章内容" + ) + + # 文章作者 + author = db.Column( + db.String(100), + nullable=True, + comment="文章作者" + ) + + # 文章状态:0-正常 1-下架 + status = db.Column( + db.Integer, + nullable=False, + default=0, + comment="文章状态:0-正常 1-下架" + ) + + # 文章点击率 + click = db.Column( + db.Integer, + nullable=False, + default=0, + comment="文章点击率" + ) + + # 文章排序 + sort = db.Column( + db.Integer, + nullable=False, + default=0, + comment="文章排序" + ) + + # ============================================================ + # 常量定义 + # ============================================================ + + class Status: + """文章状态常量""" + NORMAL = 0 # 正常 + OFFLINE = 1 # 下架 + + # ============================================================ + # 业务方法 + # ============================================================ + + def increment_click(self): + """ + 增加点击率 + + 每次调用点击量加1 + """ + self.click = (self.click or 0) + 1 + self.save() + + def get_images_list(self): + """ + 获取图集列表 + + Returns: + list: 图集URL列表 + """ + if not self.images: + return [] + return [img.strip() for img in self.images.split(',') if img.strip()] + + def get_cover_url(self): + """ + 获取封面URL + + Returns: + str: 封面URL + """ + from utils.rich_text import get_file_url + return get_file_url(self.cover) if self.cover else "" + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __repr__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "文章:{title}" 的字符串 + """ + return '文章:{}'.format(self.title or '未命名') + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "文章:{title}" 的字符串 + """ + return self.__repr__() diff --git a/application/models/base_db.py b/application/models/base_db.py new file mode 100644 index 0000000..96e0b3f --- /dev/null +++ b/application/models/base_db.py @@ -0,0 +1,8 @@ +""" +兼容 shim(绞杀者迁移期间保留) +============================================================= + +数据库操作基类已迁移至 src/common/base_db.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 +""" +from src.common.base_db import * # noqa: F401,F403 diff --git a/application/models/base_model.py b/application/models/base_model.py new file mode 100644 index 0000000..224c786 --- /dev/null +++ b/application/models/base_model.py @@ -0,0 +1,8 @@ +""" +兼容 shim(绞杀者迁移期间保留) +============================================================= + +模型基类已迁移至 src/common/base_model.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 +""" +from src.common.base_model import * # noqa: F401,F403 diff --git a/application/models/category.py b/application/models/category.py new file mode 100644 index 0000000..1cf02e9 --- /dev/null +++ b/application/models/category.py @@ -0,0 +1,122 @@ +""" +分类模型模块 +============================================================= + +提供文章分类管理的数据模型定义,支持多级分类结构。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 分类模型 +# ============================================================ + +class Category(BaseModel, BaseDB): + """ + 分类模型 + + 用于存储文章分类数据,支持多级分类结构(无限级)。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + category_name: 分类名称(String(100),必填) + category_code: 分类编码(String(100),可选) + parent_id: 上级分类ID(Integer,默认0,0表示顶级分类) + sort: 分类排序(Integer,默认0,数值越小越靠前) + note: 分类备注(String(100),可选) + type: 分类类型(Integer,必填,1-公司,2-子公司,3-部门,4-小组) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "category" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 分类名称 + category_name = db.Column( + db.String(100), + nullable=False, + comment="分类名称" + ) + + # 分类编码 + category_code = db.Column( + db.String(100), + nullable=True, + comment="分类编码" + ) + + # 上级分类ID + parent_id = db.Column( + db.Integer, + nullable=False, + default=0, + comment="上级ID" + ) + + # 分类排序 + sort = db.Column( + db.Integer, + nullable=False, + default=0, + comment="排序" + ) + + # 分类备注 + note = db.Column( + db.String(100), + nullable=True, + comment="分类备注" + ) + + # 分类类型 + type = db.Column( + db.Integer, + nullable=False, + comment="分类类型:1-公司 2-子公司 3-部门 4-小组" + ) + + # ============================================================ + # 常量定义 + # ============================================================ + + class Type: + """分类类型常量""" + COMPANY = 1 # 公司 + SUBSIDIARY = 2 # 子公司 + DEPARTMENT = 3 # 部门 + GROUP = 4 # 小组 + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __repr__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "分类:{category_name}" 的字符串 + """ + return '分类:{}'.format(self.category_name) + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "分类:{category_name}" 的字符串 + """ + return self.__repr__() diff --git a/application/models/city.py b/application/models/city.py new file mode 100644 index 0000000..a9bb19f --- /dev/null +++ b/application/models/city.py @@ -0,0 +1,189 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +城市模型模块 +============================================================= + +提供城市区域的数据模型定义,支持省、市、县区、街道四级区域结构。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 城市模型 +# ============================================================ + +class City(BaseModel, BaseDB): + """ + 城市区域模型 + + 用于存储行政区划数据,支持省、市、县区、街道四级区域结构。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + city_code: 城市区号(String(6),必填) + area_code: 行政编码(String(20),必填) + parent_code: 上级行政编码(String(20),必填) + zip_code: 邮政编码(String(6),必填) + level: 城市级别(Integer,1-省份,2-城市,3-县区,4-街道) + pid: 上级城市ID(Integer,默认0) + name: 城市名称(String(150),必填) + short_name: 城市简称(String(150),必填) + full_name: 城市全称(String(150),可选) + pinyin: 城市拼音(String(150),可选) + lng: 城市经度(String(150),可选) + lat: 城市纬度(String(150),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "city" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 城市区号 + city_code = db.Column( + db.String(6), + nullable=False, + comment="城市区号" + ) + + # 行政编码 + area_code = db.Column( + db.String(20), + nullable=False, + comment="行政编码" + ) + + # 上级行政编码 + parent_code = db.Column( + db.String(20), + nullable=False, + comment="上级行政编码" + ) + + # 邮政编码 + zip_code = db.Column( + db.String(6), + nullable=False, + comment="邮政编码" + ) + + # 城市级别:1-省份 2-城市 3-县区 4-街道 + level = db.Column( + db.Integer, + default=0, + comment="城市级别:1-省份 2-城市 3-县区 4-街道" + ) + + # 城市名称 + name = db.Column( + db.String(150), + nullable=False, + comment="城市名称" + ) + + # 城市简称 + short_name = db.Column( + db.String(150), + nullable=False, + comment="城市简称" + ) + + # 上级城市ID + pid = db.Column( + db.Integer, + default=0, + comment="上级城市ID" + ) + + # 城市全称 + full_name = db.Column( + db.String(150), + nullable=True, + comment="城市全称" + ) + + # 城市拼音 + pinyin = db.Column( + db.String(150), + nullable=True, + comment="城市拼音" + ) + + # 城市经度 + lng = db.Column( + db.String(150), + nullable=True, + comment="城市经度" + ) + + # 城市纬度 + lat = db.Column( + db.String(150), + nullable=True, + comment="城市纬度" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "城市{id}" 的字符串 + """ + return '城市{}'.format(self.id) diff --git a/application/models/config.py b/application/models/config.py new file mode 100644 index 0000000..dd4973b --- /dev/null +++ b/application/models/config.py @@ -0,0 +1,125 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +配置模型模块 +============================================================= + +提供系统配置分组的数据模型定义,用于管理的分类。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 配置模型 +# ============================================================ + +class Config(BaseModel, BaseDB): + """ + 配置分组模型 + + 用于存储系统配置分组信息,如:基础配置、邮件配置等。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + name: 配置名称(String(100),必填) + code: 配置编码(String(100),必填) + sort: 配置排序(Integer,默认0,数值越小越靠前) + note: 配置备注(String(255),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "config" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 配置名称 + name = db.Column( + db.String(100), + nullable=False, + comment="配置名称" + ) + + # 配置编码 + code = db.Column( + db.String(100), + nullable=False, + comment="配置编码" + ) + + # 配置排序 + sort = db.Column( + db.Integer, + default=0, + comment="配置排序" + ) + + # 配置备注 + note = db.Column( + db.String(255), + nullable=True, + comment="配置备注" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "配置{id}" 的字符串 + """ + return "配置{}".format(self.id) diff --git a/application/models/config_item.py b/application/models/config_item.py new file mode 100644 index 0000000..6c5ce03 --- /dev/null +++ b/application/models/config_item.py @@ -0,0 +1,173 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +模型模块 +============================================================= + +提供系统的数据模型定义,用于管理具体的键值对。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 模型 +# ============================================================ + +class ConfigItem(BaseModel, BaseDB): + """ + 模型 + + 用于存储系统配置的具体项,属于某个配置。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + config_id: 配置ID(Integer,默认0,关联配置ID) + config_code: 配置编码(String(100),默认0,关联配置编码) + name: 名称(String(100),必填) + code: 编码(String(100),必填,唯一标识) + value: 值(String(1000),可选) + options: 配置选项(Text,可选,用于下拉选择等场景) + type: 配置类型(String(100),必填) + status: 配置状态(Integer,1-正常,2-停用,默认1) + sort: 排序(Integer,默认0,数值越小越靠前) + note: 备注(String(255),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "config_item" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 配置ID + config_id = db.Column( + db.Integer, + default=0, + comment="配置ID" + ) + + # 配置编码 + config_code = db.Column( + db.String(100), + nullable=False, + comment="配置编码" + ) + + # 名称 + name = db.Column( + db.String(100), + nullable=False, + comment="名称" + ) + + # 编码 + code = db.Column( + db.String(100), + nullable=False, + comment="编码" + ) + + # 值 + value = db.Column( + db.String(1000), + nullable=True, + comment="值" + ) + + # 配置选项 + options = db.Column( + db.Text, + nullable=True, + comment="配置选项" + ) + + # 配置类型 + type = db.Column( + db.String(100), + nullable=False, + comment="配置类型" + ) + + # 配置状态:1-正常 2-停用 + status = db.Column( + db.Integer, + default=1, + comment="配置状态:1-正常 2-停用" + ) + + # 排序 + sort = db.Column( + db.Integer, + default=0, + comment="顺序" + ) + + # 备注 + note = db.Column( + db.String(255), + nullable=True, + comment="备注" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "{id}" 的字符串 + """ + return "{}".format(self.id) diff --git a/application/models/dept.py b/application/models/dept.py new file mode 100644 index 0000000..1f22366 --- /dev/null +++ b/application/models/dept.py @@ -0,0 +1,133 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +部门模型模块 +============================================================= + +提供部门组织架构的数据模型定义,支持公司、子公司、部门、小组四级结构。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 部门模型 +# ============================================================ + +class Dept(BaseModel, BaseDB): + """ + 部门模型 + + 用于存储组织架构数据,支持公司、子公司、部门、小组四级结构。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + name: 部门名称(String(100),必填) + type: 部门类型(Integer,1-公司,2-子公司,3-部门,4-小组,默认0) + parent_id: 上级部门ID(Integer,默认0,0表示顶级部门) + sort: 部门排序(Integer,默认0,数值越小越靠前) + note: 部门备注(String(255),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "dept" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 部门名称 + name = db.Column( + db.String(100), + nullable=False, + comment="部门名称" + ) + + # 部门类型:1-公司 2-子公司 3-部门 4-小组 + type = db.Column( + db.Integer, + default=0, + comment="部门类型:1-公司 2-子公司 3-部门 4-小组" + ) + + # 上级部门ID + parent_id = db.Column( + db.Integer, + default=0, + comment="上级部门ID" + ) + + # 部门排序 + sort = db.Column( + db.Integer, + default=0, + comment="部门排序" + ) + + # 备注 + note = db.Column( + db.String(255), + nullable=True, + comment="备注" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "部门{name}" 的字符串 + """ + return '部门{}'.format(self.name) diff --git a/application/models/dict.py b/application/models/dict.py new file mode 100644 index 0000000..9c1ed89 --- /dev/null +++ b/application/models/dict.py @@ -0,0 +1,125 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +字典模型模块 +============================================================= + +提供数据字典分组的数据模型定义,用于管理字典项的分类。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 字典模型 +# ============================================================ + +class Dict(BaseModel, BaseDB): + """ + 字典分组模型 + + 用于存储数据字典分组信息,如:性别、状态等。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + name: 字典名称(String(150),必填) + code: 字典编码(String(150),必填,唯一标识) + sort: 字典排序(Integer,默认0,数值越小越靠前) + note: 字典备注(String(255),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "dict" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 字典名称 + name = db.Column( + db.String(150), + nullable=False, + comment="字典名称" + ) + + # 字典编码 + code = db.Column( + db.String(150), + nullable=False, + comment="字典编码" + ) + + # 字典排序 + sort = db.Column( + db.Integer, + default=0, + comment="字典排序" + ) + + # 字典备注 + note = db.Column( + db.String(255), + nullable=True, + comment="字典备注" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "字典{id}" 的字符串 + """ + return "字典{}".format(self.id) diff --git a/application/models/dict_item.py b/application/models/dict_item.py new file mode 100644 index 0000000..2f5ee49 --- /dev/null +++ b/application/models/dict_item.py @@ -0,0 +1,140 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +字典项模型模块 +============================================================= + +提供数据字典项的数据模型定义,用于管理字典的具体选项。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 字典项模型 +# ============================================================ + +class DictItem(BaseModel, BaseDB): + """ + 字典项模型 + + 用于存储数据字典的具体项,属于某个字典分组。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + name: 字典项名称(String(100),必填) + value: 字典项值(String(100),必填) + dict_id: 字典ID(Integer,默认0,关联字典分组) + sort: 字典项排序(Integer,默认0,数值越小越靠前) + note: 字典项备注(String(255),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "dict_item" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 字典ID + dict_id = db.Column( + db.Integer, + default=0, + comment="字典ID" + ) + + # 字典编码 + dict_code = db.Column( + db.String(100), + nullable=False, + comment="字典编码" + ) + + # 字典项名称 + name = db.Column( + db.String(100), + nullable=False, + comment="字典项名称" + ) + + # 字典项值 + value = db.Column( + db.String(100), + nullable=False, + comment="字典项值" + ) + + # 字典项排序 + sort = db.Column( + db.Integer, + default=0, + comment="字典项顺序" + ) + + # 字典项备注 + note = db.Column( + db.String(255), + nullable=True, + comment="字典项备注" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "字典项{id}" 的字符串 + """ + return "字典项{}".format(self.id) diff --git a/application/models/file_template.py b/application/models/file_template.py new file mode 100644 index 0000000..319700d --- /dev/null +++ b/application/models/file_template.py @@ -0,0 +1,109 @@ +""" +文件模板模型模块 +============================================================= + +提供文件模板管理的数据模型定义,用于管理文件模板信息。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 文件模板模型 +# ============================================================ + +class FileTemplate(BaseModel, BaseDB): + """ + 文件模板模型 + + 用于存储文件模板信息,如:导入导出模板、报表模板等。 + + Attributes: + id: 主键ID(继承自 BaseModel) + name: 模板名称(String(100),可选) + code: 模板编码(String(50),可选,唯一标识) + file_name: 文件名称(String(255),可选) + file_path: 文件路径(String(255),可选) + note: 文件模板备注(String(255),可选) + sort: 文件模板顺序(Integer,必填,默认0) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "file_template" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 模板名称 + name = db.Column( + db.String(100), + nullable=True, + comment="模板名称" + ) + + # 模板编码 + code = db.Column( + db.String(50), + nullable=True, + comment="模板编码" + ) + + # 文件名称 + file_name = db.Column( + db.String(255), + nullable=True, + comment="文件名称" + ) + + # 文件路径 + file_path = db.Column( + db.String(255), + nullable=True, + comment="文件路径" + ) + + # 文件模板备注 + note = db.Column( + db.String(255), + nullable=True, + comment="文件模板备注" + ) + + # 文件模板顺序 + sort = db.Column( + db.Integer, + nullable=False, + default=0, + comment="文件模板顺序" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __repr__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "文件模板:{name}" 的字符串 + """ + return '文件模板:{}'.format(self.name or self.code) + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "文件模板:{name}" 的字符串 + """ + return self.__repr__() diff --git a/application/models/job.py b/application/models/job.py new file mode 100644 index 0000000..473d412 --- /dev/null +++ b/application/models/job.py @@ -0,0 +1,152 @@ +""" +定时任务模型模块 +============================================================= + +提供定时任务管理的数据模型定义。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 定时任务模型 +# ============================================================ + +class Job(BaseModel, BaseDB): + """ + 定时任务模型 + + 用于存储定时任务信息。 + + Attributes: + id: 主键ID(继承自 BaseModel) + job_name: 任务名称(String(255),必填) + job_alias: 任务别名(String(255),可选) + job_group: 任务分组(String(255),可选) + job_trigger: 任务触发器(String(255),可选) + status: 任务状态(Integer,0-未发布 1-运行中 2-暂停 3-删除) + cron_expression: Cron表达式(String(255),可选) + execute_policy: 执行策略(Integer,1-立即执行 2-执行一次 3-放弃执行) + is_sync: 是否同步任务(Integer,0-否 1-是) + url: 任务URL(String(255),可选) + note: 任务备注(String(255),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + __tablename__ = DB_PREFIX + "job" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 任务名称 + job_name = db.Column( + db.String(255), + nullable=False, + comment="任务名称" + ) + + # 任务别名 + job_alias = db.Column( + db.String(255), + nullable=True, + comment="任务别名" + ) + + # 任务分组 + job_group = db.Column( + db.String(255), + nullable=True, + comment="任务分组" + ) + + # 任务触发器 + job_trigger = db.Column( + db.String(255), + nullable=True, + comment="任务触发器" + ) + + # 任务状态:0-未发布 1-运行中 2-暂停 3-删除 + status = db.Column( + db.Integer, + nullable=False, + default=0, + comment="任务状态:0-未发布 1-运行中 2-暂停 3-删除" + ) + + # Cron表达式 + cron_expression = db.Column( + db.String(255), + nullable=True, + comment="Cron表达式" + ) + + # 执行策略:1-立即执行 2-执行一次 3-放弃执行 + execute_policy = db.Column( + db.Integer, + nullable=False, + default=1, + comment="执行策略:1-立即执行 2-执行一次 3-放弃执行" + ) + + # 是否同步任务:0-否 1-是 + is_sync = db.Column( + db.Integer, + nullable=False, + default=0, + comment="是否同步任务:0-否 1-是" + ) + + # 任务URL + url = db.Column( + db.String(255), + nullable=True, + comment="任务URL" + ) + + # 任务备注 + note = db.Column( + db.String(255), + nullable=True, + comment="任务备注" + ) + + # ============================================================ + # 常量定义 + # ============================================================ + + class Status: + """任务状态常量""" + UNPUBLISHED = 0 # 未发布 + RUNNING = 1 # 运行中 + PAUSED = 2 # 暂停 + DELETED = 3 # 删除 + + class ExecutePolicy: + """执行策略常量""" + EXECUTE_NOW = 1 # 立即执行 + EXECUTE_ONCE = 2 # 执行一次 + ABANDON = 3 # 放弃执行 + + class IsSync: + """是否同步任务常量""" + ASYNC = 0 # 异步 + SYNC = 1 # 同步 + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __repr__(self): + return '定时任务:{}'.format(self.job_name) + + def __str__(self): + return self.__repr__() diff --git a/application/models/level.py b/application/models/level.py new file mode 100644 index 0000000..7c3ea5b --- /dev/null +++ b/application/models/level.py @@ -0,0 +1,117 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +职级模型模块 +============================================================= + +提供职级管理的数据模型定义,用于定义员工职级体系。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 职级模型 +# ============================================================ + +class Level(BaseModel, BaseDB): + """ + 职级模型 + + 用于存储员工职级信息,如:初级、中级、高级等。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + name: 职级名称(String(255),必填) + status: 职级状态(Integer,1-在用,2-停用,默认0) + sort: 职级排序(Integer,默认0,数值越小越靠前) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "level" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 职级名称 + name = db.Column( + db.String(255), + nullable=False, + comment="职级名称" + ) + + # 职级状态:1-在用 2-停用 + status = db.Column( + db.Integer, + default=0, + comment="职级状态:1-在用 2-停用" + ) + + # 职级排序 + sort = db.Column( + db.Integer, + default=0, + comment="职级排序" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __repr__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "职级:{name}" 的字符串 + """ + return '职级:{}'.format(self.name) diff --git a/application/models/link.py b/application/models/link.py new file mode 100644 index 0000000..eed6428 --- /dev/null +++ b/application/models/link.py @@ -0,0 +1,157 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +友链模型模块 +============================================================= + +提供友情链接的数据模型定义,用于管理网站友情链接和合作伙伴。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 友链模型 +# ============================================================ + +class Link(BaseModel, BaseDB): + """ + 友链模型 + + 用于存储友情链接和合作伙伴信息,支持多种投放平台和链接形式。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + name: 友链名称(String(255),必填) + type: 友链类型(Integer,1-友情链接,2-合作伙伴,默认1) + url: 友链URL(String(255),可选) + form: 友链形式(Integer,1-文字链接,2-图片链接,默认1) + image: 友链图片(String(255),可选) + status: 友链状态(Integer,1-正常,2-停用,默认1) + sort: 友链排序(Integer,默认0,数值越小越靠前) + note: 友链备注(String(255),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "link" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 友链名称 + name = db.Column( + db.String(255), + nullable=False, + comment="友链名称" + ) + + # 友链类型:1-友情链接 2-合作伙伴 + type = db.Column( + db.Integer, + default=1, + comment="友链类型:1-友情链接 2-合作伙伴" + ) + + # 友链URL + url = db.Column( + db.String(255), + nullable=True, + comment="友链URL" + ) + + # 友链形式:1-文字链接 2-图片链接 + form = db.Column( + db.Integer, + default=1, + comment="友链形式:1-文字链接 2-图片链接" + ) + + # 友链图片 + image = db.Column( + db.String(255), + nullable=True, + comment="友链图片" + ) + + # 友链状态:1-正常 2-停用 + status = db.Column( + db.Integer, + default=1, + comment="友链状态:1-正常 2-停用" + ) + + # 友链顺序 + sort = db.Column( + db.Integer, + default=0, + comment="友链顺序" + ) + + # 友链备注 + note = db.Column( + db.String(255), + nullable=True, + comment="友链备注" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "友链{name}" 的字符串 + """ + return "友链{}".format(self.name) diff --git a/application/models/login_log.py b/application/models/login_log.py new file mode 100644 index 0000000..3b51fb5 --- /dev/null +++ b/application/models/login_log.py @@ -0,0 +1,201 @@ +""" +登录日志模型模块 +============================================================= + +提供登录日志管理的数据模型定义,用于记录用户登录、退出等操作日志。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 登录日志模型 +# ============================================================ + +class LoginLog(BaseModel, BaseDB): + """ + 登录日志模型 + + 用于记录用户登录和退出操作的详细信息,包括:登录账号、日志类型、 + 来源、请求信息、IP地址、归属地、设备信息、请求耗时、操作状态等。 + + Attributes: + id: 主键ID(继承自 BaseModel) + username: 登录账号(String(50),必填) + log_type: 日志类型(Integer,1-登录,2-退出,必填) + source: 登录来源(Integer,0-后台,1-前台,必填) + method: 请求方法(String(10),必填) + request_method: 请求方式(String(255),必填) + url: 请求URL(String(500),必填) + param: 请求参数(Text,可选) + result: 返回结果(Text,必填) + ip: IP地址(String(50),必填) + location: IP归属地(String(100),必填) + os: 操作系统(String(100),必填) + browser: 浏览器(String(100),必填) + consume_time: 请求耗时(Integer,必填) + status: 操作状态(Integer,0-正常,1-异常,必填) + error: 错误信息(Text,可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "login_log" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 登录账号 + username = db.Column( + db.String(50), + nullable=False, + comment="登录账号" + ) + + # 日志类型:1-登录,2-退出 + log_type = db.Column( + db.Integer, + nullable=False, + comment="日志类型:1-登录,2-退出" + ) + + # 登录来源:0-后台,1-前台 + source = db.Column( + db.Integer, + nullable=False, + comment="登录来源:0-后台,1-前台" + ) + + # 请求方法(HTTP方法,如POST/GET) + method = db.Column( + db.String(10), + nullable=False, + comment="请求方法" + ) + + # 请求方式(视图方法路径,如application.views.login.logout) + request_method = db.Column( + db.String(255), + nullable=False, + comment="请求方式" + ) + + # 请求URL + url = db.Column( + db.String(500), + nullable=False, + comment="请求URL" + ) + + # 请求参数 + param = db.Column( + db.Text, + nullable=True, + comment="请求参数" + ) + + # 返回结果 + result = db.Column( + db.Text, + nullable=False, + comment="返回结果" + ) + + # IP地址 + ip = db.Column( + db.String(50), + nullable=False, + comment="IP地址" + ) + + # IP归属地 + location = db.Column( + db.String(100), + nullable=False, + comment="IP归属地" + ) + + # 操作系统 + os = db.Column( + db.String(100), + nullable=False, + comment="操作系统" + ) + + # 浏览器 + browser = db.Column( + db.String(100), + nullable=False, + comment="浏览器" + ) + + # 请求耗时(毫秒) + consume_time = db.Column( + db.Integer, + nullable=False, + comment="请求耗时" + ) + + # 操作状态:0-正常,1-异常 + status = db.Column( + db.Integer, + nullable=False, + comment="操作状态:0-正常,1-异常" + ) + + # 错误信息 + error = db.Column( + db.Text, + nullable=True, + comment="错误信息" + ) + + # ============================================================ + # 常量定义 + # ============================================================ + + class LogType: + """日志类型常量""" + LOGIN = 1 # 登录 + LOGOUT = 2 # 退出 + + class Source: + """登录来源常量""" + ADMIN = 0 # 后台 + FRONT = 1 # 前台 + + class Status: + """操作状态常量""" + NORMAL = 0 # 正常 + ERROR = 1 # 异常 + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __repr__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "登录日志:{username} - {log_type}" 的字符串 + """ + log_type_name = "登录" if self.log_type == self.LogType.LOGIN else "退出" + return '登录日志:{} - {}'.format(self.username, log_type_name) + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "登录日志:{username} - {log_type}" 的字符串 + """ + return self.__repr__() diff --git a/application/models/menu.py b/application/models/menu.py new file mode 100644 index 0000000..dd344bc --- /dev/null +++ b/application/models/menu.py @@ -0,0 +1,188 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +菜单模型模块 +============================================================= + +提供系统菜单的数据模型定义,用于管理后台导航菜单和权限节点。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db + + +# ============================================================ +# 菜单模型 +# ============================================================ + +class Menu(BaseModel, BaseDB): + """ + 菜单模型 + + 用于存储系统导航菜单和权限节点信息,支持树形结构。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + name: 菜单名称(String(100),必填) + icon: 菜单图标(String(50),可选) + parent_id: 上级ID(Integer,默认0,0表示顶级菜单) + path: 菜单路径(String(255),可选,前端路由地址) + component: 菜单组件(String(255),可选,前端组件路径) + permission: 权限节点(String(100),可选,权限标识符) + target: 打开方式(Integer,0-内部打开,1-外部打开,默认0) + type: 菜单类型(Integer,0-菜单,1-节点,默认0) + status: 菜单状态(Integer,0-正常,1-禁用,默认0) + hide: 是否可见(Integer,0-可见,1-不可见,默认0) + sort: 菜单排序(Integer,默认0,数值越小越靠前) + note: 菜单备注(String(255),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "menu" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 菜单名称 + name = db.Column( + db.String(100), + nullable=False, + comment="菜单名称" + ) + + # 菜单图标 + icon = db.Column( + db.String(50), + nullable=True, + comment="菜单图标" + ) + + # 上级ID + parent_id = db.Column( + db.Integer, + default=0, + comment="上级ID" + ) + + # 菜单路径 + path = db.Column( + db.String(255), + nullable=True, + comment="菜单路径" + ) + + # 菜单组件 + component = db.Column( + db.String(255), + nullable=True, + comment="菜单组件" + ) + + # 权限节点 + permission = db.Column( + db.String(100), + nullable=True, + comment="权限节点" + ) + + # 打开方式:0-内部打开 1-外部打开 + target = db.Column( + db.Integer, + default=0, + comment="打开方式:0-内部打开 1-外部打开" + ) + + # 菜单类型:0-菜单 1-节点 + type = db.Column( + db.Integer, + default=0, + comment="菜单类型:0-菜单 1-节点" + ) + + # 状态选项 + status = db.Column( + db.Integer, + default=0, + comment="菜单状态:0-正常 1-禁用" + ) + + # 是否可见:0-可见 1-不可见 + hide = db.Column( + db.Integer, + default=0, + comment="是否可见:0-可见 1-不可见" + ) + + # 排序 + sort = db.Column( + db.Integer, + default=0, + comment="排序" + ) + + # 备注 + note = db.Column( + db.String(255), + nullable=True, + comment="备注" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "菜单{id}" 的字符串 + """ + return "菜单{}".format(self.id) diff --git a/application/models/notice.py b/application/models/notice.py new file mode 100644 index 0000000..c8c775b --- /dev/null +++ b/application/models/notice.py @@ -0,0 +1,141 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +通知公告模型模块 +============================================================= + +提供通知公告的数据模型定义,用于管理系统通知和公告信息。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 通知公告模型 +# ============================================================ + +class Notice(BaseModel, BaseDB): + """ + 通知公告模型 + + 用于存储系统通知和公告信息,支持置顶、来源分类等功能。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + title: 通知标题(String(150),必填) + cover: 通知封面(String(150),必填) + type: 通知类型(Integer,默认1) + status: 通知状态(Integer,1-正常,2-停用,默认1) + click_num: 点击率(Integer,默认0) + content: 通知公告内容(Text,可选,支持富文本) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "notice" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 通知标题 + title = db.Column( + db.String(150), + nullable=False, + comment="通知标题" + ) + + # 通知封面 + cover = db.Column( + db.String(255), + nullable=False, + comment="通知封面" + ) + + # 通知类型:1-通知 2-公告 + type = db.Column( + db.Integer, + default=1, + comment="通知类型:1-通知 2-公告" + ) + + # 通知状态:1-正常 2-停用 + status = db.Column( + db.Integer, + default=1, + comment="通知状态:1-正常 2-停用" + ) + + # 点击率 + click_num = db.Column( + db.Integer, + default=0, + comment="点击率" + ) + + # 通知公告内容 + content = db.Column( + db.Text, + nullable=True, + comment="通知内容" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "通知公告{title}" 的字符串 + """ + return "通知公告{}".format(self.title) diff --git a/application/models/operation_log.py b/application/models/operation_log.py new file mode 100644 index 0000000..2614680 --- /dev/null +++ b/application/models/operation_log.py @@ -0,0 +1,195 @@ +""" +操作日志模型模块 +============================================================= + +提供操作日志管理的数据模型定义,用于记录用户操作行为日志。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 操作日志模型 +# ============================================================ + +class OperationLog(BaseModel, BaseDB): + """ + 操作日志模型 + + 用于记录用户操作行为的详细信息,包括:操作标题、操作类型、 + 来源、请求信息、IP地址、归属地、设备信息、请求耗时、操作状态等。 + + Attributes: + id: 主键ID(继承自 BaseModel) + title: 请求标题(String(150),必填) + type: 请求类型(Integer,必填) + source: 请求来源(Integer,必填,0-后台,1-前台) + method: 请求方法(String(100),必填) + request_method: 请求方式(String(255),必填) + url: 请求URL(String(1000),必填) + param: 请求参数(Text,必填) + result: 请求结果(Text,必填) + ip: 请求IP(String(100),必填) + location: 请求所在区域(String(255),必填) + os: 操作系统(String(100),必填) + browser: 浏览器(String(100),必填) + consume_time: 请求耗时(Integer,必填,单位毫秒) + status: 请求状态(Integer,必填,0-正常,1-异常) + error: 错误描述(Text,必填) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "operation_log" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 请求标题 + title = db.Column( + db.String(150), + nullable=False, + comment="请求标题" + ) + + # 请求类型 + type = db.Column( + db.Integer, + nullable=False, + comment="请求类型" + ) + + # 请求来源:0-后台,1-前台 + source = db.Column( + db.Integer, + nullable=False, + comment="请求来源:0-后台,1-前台" + ) + + # 请求方法(如:UserController.add) + method = db.Column( + db.String(100), + nullable=False, + comment="请求方法" + ) + + # 请求方式(如:GET、POST、PUT、DELETE) + request_method = db.Column( + db.String(255), + nullable=False, + comment="请求方式" + ) + + # 请求URL + url = db.Column( + db.String(1000), + nullable=False, + comment="请求URL" + ) + + # 请求参数 + param = db.Column( + db.Text, + nullable=False, + comment="请求参数" + ) + + # 请求结果 + result = db.Column( + db.Text, + nullable=False, + comment="请求结果" + ) + + # 请求IP + ip = db.Column( + db.String(100), + nullable=False, + comment="请求IP" + ) + + # 请求所在区域 + location = db.Column( + db.String(255), + nullable=False, + comment="请求所在区域" + ) + + # 操作系统 + os = db.Column( + db.String(100), + nullable=False, + comment="操作系统" + ) + + # 浏览器 + browser = db.Column( + db.String(100), + nullable=False, + comment="浏览器" + ) + + # 请求耗时(毫秒) + consume_time = db.Column( + db.BigInteger, + nullable=False, + comment="请求耗时" + ) + + # 请求状态:0-正常,1-异常 + status = db.Column( + db.Integer, + nullable=False, + comment="请求状态:0-正常,1-异常" + ) + + # 错误描述 + error = db.Column( + db.Text, + nullable=False, + comment="错误描述" + ) + + # ============================================================ + # 常量定义 + # ============================================================ + + class Source: + """请求来源常量""" + ADMIN = 0 # 后台 + FRONT = 1 # 前台 + + class Status: + """请求状态常量""" + NORMAL = 0 # 正常 + ERROR = 1 # 异常 + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __repr__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "操作日志:{title} - {method}" 的字符串 + """ + return '操作日志:{} - {}'.format(self.title, self.method) + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "操作日志:{title} - {method}" 的字符串 + """ + return self.__repr__() diff --git a/application/models/param.py b/application/models/param.py new file mode 100644 index 0000000..d180ead --- /dev/null +++ b/application/models/param.py @@ -0,0 +1,117 @@ +""" +参数模型模块 +============================================================= + +提供系统参数管理的数据模型定义,用于管理系统参数和业务参数。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 参数模型 +# ============================================================ + +class Param(BaseModel, BaseDB): + """ + 参数模型 + + 用于存储系统参数和业务参数信息,如:系统配置参数、业务规则参数等。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + name: 参数名称(String(100),必填) + code: 参数编码(String(100),必填,唯一标识) + value: 参数值(String(255),必填) + type: 参数类型(Integer,0-系统 1-业务,必填) + status: 参数状态(Integer,1-正常 2-禁用,必填) + sort: 参数排序(Integer,默认0,数值越小越靠前) + note: 参数备注(String(255),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "param" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 参数名称 + name = db.Column( + db.String(100), + nullable=False, + comment="参数名称" + ) + + # 参数编码 + code = db.Column( + db.String(100), + nullable=False, + comment="参数编码" + ) + + # 参数值 + value = db.Column( + db.String(255), + nullable=False, + comment="参数值" + ) + + # 参数类型:0-系统 1-业务 + type = db.Column( + db.Integer, + nullable=False, + comment="参数类型:0-系统 1-业务" + ) + + # 参数状态:1-正常 2-禁用 + status = db.Column( + db.Integer, + nullable=False, + comment="参数状态:1-正常 2-禁用" + ) + + # 参数排序 + sort = db.Column( + db.Integer, + nullable=False, + comment="参数排序" + ) + + # 参数备注 + note = db.Column( + db.String(255), + nullable=True, + comment="参数备注" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __repr__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "参数:{name}" 的字符串 + """ + return '参数:{}'.format(self.name) + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "参数:{name}" 的字符串 + """ + return self.__repr__() diff --git a/application/models/position.py b/application/models/position.py new file mode 100644 index 0000000..0f2b926 --- /dev/null +++ b/application/models/position.py @@ -0,0 +1,295 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称"本产品")拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +岗位模型模块 +============================================================= + +提供岗位管理的数据模型定义,用于定义员工岗位信息。 + +企业级特性: + 1. 使用枚举类管理状态常量,确保类型安全 + 2. 状态文本自动转换,便于前端展示 + 3. 便捷的查询方法,提高开发效率 + 4. 继承 BaseModel 和 BaseDB,复用通用功能 + 5. __init__ 由 BaseModel 统一管理,无需重复定义 + +模型说明: + 岗位用于标识用户的职位,如:技术总监、产品经理、开发工程师等。 + 通过 sort 字段控制显示顺序,数值越小越靠前。 +""" + +from enum import IntEnum +from typing import Optional, List + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.request import like_contains + + +# ============================================================ +# 岗位状态枚举 +# ============================================================ + +class PositionStatus(IntEnum): + """ + 岗位状态枚举 + + 使用 IntEnum 确保类型安全,避免魔法数字。 + 可直接用于数据库查询和比较。 + + Example: + # 查询启用状态的岗位 + Position.query.filter(Position.status == PositionStatus.ENABLED) + + # 创建岗位时设置状态 + position = Position(name='经理', status=PositionStatus.ENABLED) + + # 判断岗位是否启用 + if position.status == PositionStatus.ENABLED: + print("岗位已启用") + """ + DISABLED = 0 # 停用 + ENABLED = 1 # 启用 + + +# ============================================================ +# 岗位模型 +# ============================================================ + +class Position(BaseModel, BaseDB): + """ + 岗位模型 + + 用于存储员工岗位信息,如:技术总监、产品经理等。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + name: 岗位名称(String(255),必填) + status: 岗位状态(使用 PositionStatus 枚举,默认停用) + sort: 岗位排序(Integer,默认0,数值越小越靠前) + + 便捷属性: + status_text: 获取状态文本("启用"/"停用") + is_enabled: 判断是否启用 + is_disabled: 判断是否停用 + + 自定义查询: + get_enabled(): 获取所有启用的岗位 + get_by_name(): 根据名称精确查询 + search_by_name(): 根据名称模糊搜索 + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 表名:使用配置前缀 + 模型名 + __tablename__ = DB_PREFIX + "position" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 岗位名称:必填字段,最长255字符 + name = db.Column( + db.String(255), + nullable=False, + comment="岗位名称" + ) + + # 岗位状态:使用枚举确保类型安全 + # 默认值为停用,创建后需手动启用 + status = db.Column( + db.Integer, + default=PositionStatus.DISABLED, + comment="岗位状态:0-停用 1-启用" + ) + + # 岗位排序:数值越小越靠前 + # 用于列表展示时的排序控制 + sort = db.Column( + db.Integer, + default=0, + comment="岗位排序" + ) + + # ============================================================ + # 注意:__init__ 方法在 BaseModel 中已实现,无需重复定义 + # BaseModel 已处理驼峰转换、字段过滤等逻辑 + # ============================================================ + + # ============================================================ + # 便捷属性 + # ============================================================ + + @property + def status_text(self) -> str: + """ + 获取岗位状态文本 + + 将数字状态转换为可读的中文文本。 + 用于前端展示或日志记录。 + + Returns: + str: "启用" 或 "停用" + """ + status_map = { + PositionStatus.DISABLED: '停用', + PositionStatus.ENABLED: '启用' + } + return status_map.get(self.status, '未知') + + @property + def is_enabled(self) -> bool: + """ + 判断岗位是否启用 + + Returns: + bool: 启用返回 True,否则返回 False + """ + return self.status == PositionStatus.ENABLED + + @property + def is_disabled(self) -> bool: + """ + 判断岗位是否停用 + + Returns: + bool: 停用返回 True,否则返回 False + """ + return self.status == PositionStatus.DISABLED + + # ============================================================ + # 自定义查询方法 + # ============================================================ + + @classmethod + def get_enabled(cls) -> List['Position']: + """ + 获取所有启用的岗位 + + 按排序值升序排列,适用于下拉选择框等场景。 + 只返回未删除且状态为启用的岗位。 + + Returns: + List[Position]: 启用的岗位列表 + + Example: + positions = Position.get_enabled() + for pos in positions: + print(pos.name, pos.status_text) + """ + return cls.query.filter( + cls.is_delete == 0, + cls.status == PositionStatus.ENABLED + ).order_by(cls.sort.asc()).all() + + @classmethod + def get_by_name(cls, name: Optional[str]) -> Optional['Position']: + """ + 根据岗位名称精确查询 + + 用于检查岗位名称是否已存在等场景。 + + Args: + name: 岗位名称 + + Returns: + Optional[Position]: 岗位对象,不存在或名称为空时返回 None + + Example: + position = Position.get_by_name('经理') + if position: + print("岗位已存在") + """ + if not name: + return None + + return cls.query.filter( + cls.name == name, + cls.is_delete == 0 + ).first() + + @classmethod + def search_by_name(cls, name: Optional[str]) -> List['Position']: + """ + 根据岗位名称模糊搜索 + + 支持名称关键字匹配,按排序值升序排列。 + 适用于搜索建议或筛选场景。 + + Args: + name: 岗位名称关键字 + + Returns: + List[Position]: 匹配的岗位列表 + + Example: + positions = Position.search_by_name('经理') + for pos in positions: + print(pos.name) + """ + if not name: + return [] + + return cls.query.filter( + cls.name.like(like_contains(name), escape='\\'), + cls.is_delete == 0 + ).order_by(cls.sort.asc()).all() + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __repr__(self) -> str: + """ + 返回模型的字符串表示 + + 用于调试和日志输出。 + + Returns: + str: 格式为 "岗位:{name}" + """ + return f'岗位:{self.name}' diff --git a/application/models/role.py b/application/models/role.py new file mode 100644 index 0000000..0ced7ef --- /dev/null +++ b/application/models/role.py @@ -0,0 +1,125 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +角色模型模块 +============================================================= + +提供角色管理的数据模型定义,用于定义系统角色和权限分配。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 角色模型 +# ============================================================ + +class Role(BaseModel, BaseDB): + """ + 角色模型 + + 用于存储系统角色信息,角色是权限分配的基本单位。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + name: 角色名称(String(150),必填) + code: 角色编码(String(30),必填,唯一标识) + sort: 角色排序(Integer,默认0,数值越小越靠前) + note: 角色备注(String(255),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "role" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 角色名称 + name = db.Column( + db.String(150), + nullable=False, + comment="角色名称" + ) + + # 角色编码 + code = db.Column( + db.String(30), + nullable=False, + comment="角色编码" + ) + + # 角色排序 + sort = db.Column( + db.Integer, + default=0, + comment="角色排序" + ) + + # 角色备注 + note = db.Column( + db.String(255), + nullable=True, + comment="角色备注" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "角色{name}:" 的字符串 + """ + return '角色{}:'.format(self.name) diff --git a/application/models/role_menu.py b/application/models/role_menu.py new file mode 100644 index 0000000..2b430e6 --- /dev/null +++ b/application/models/role_menu.py @@ -0,0 +1,108 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +角色菜单关联模型模块 +============================================================= + +提供角色与菜单的关联关系数据模型定义,用于实现角色权限分配。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db + + +# ============================================================ +# 角色菜单关联模型 +# ============================================================ + +class RoleMenu(BaseModel, BaseDB): + """ + 角色菜单关联模型 + + 用于存储角色与菜单的关联关系,实现角色的权限分配。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + role_id: 角色ID(Integer,默认0) + menu_id: 菜单ID(Integer,默认0) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "role_menu" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 角色ID + role_id = db.Column( + db.Integer, + default=0, + comment="角色ID" + ) + + # 菜单ID + menu_id = db.Column( + db.Integer, + default=0, + comment="菜单ID" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "角色菜单表{role_id}" 的字符串 + """ + return "角色菜单表{}".format(self.role_id) diff --git a/application/models/user.py b/application/models/user.py new file mode 100644 index 0000000..0a254c0 --- /dev/null +++ b/application/models/user.py @@ -0,0 +1,261 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +用户模型模块 +============================================================= + +提供用户管理的数据模型定义,用于存储系统用户基本信息。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db +from utils.case_converter import convert_camel_to_snake + + +# ============================================================ +# 用户模型 +# ============================================================ + +class User(BaseModel, BaseDB): + """ + 用户模型 + + 用于存储系统用户信息,包括基本信息、联系方式、地址等。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + realname: 用户姓名(String(150),必填) + gender: 性别(Integer,1-男,2-女,3-保密,默认1) + avatar: 用户头像(String(255),必填) + mobile: 手机号(String(30),必填) + email: 邮箱(String(30),必填) + dept_id: 部门ID(Integer,默认0) + level_id: 职级ID(Integer,默认0) + position_id: 岗位ID(Integer,默认0) + province_code: 省份编码(String(30),必填) + city_code: 城市编码(String(30),必填) + district_code: 县区编码(String(30),必填) + street_code: 街道编码(String(30),必填) + city_info: 省市区信息(String(255),可选) + address: 详细地址(String(255),必填) + username: 用户名(String(30),可选,登录账号) + password: 密码(String(255),可选,加密存储) + salt: 加密盐(String(30),可选) + intro: 个人简介(String(255),可选) + status: 用户状态(Integer,1-正常,2-禁用,默认1) + sort: 排序(Integer,默认0,数值越小越靠前) + note: 个人备注(String(255),可选) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "user" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 用户姓名 + realname = db.Column( + db.String(150), + nullable=False, + comment="用户姓名" + ) + + # 性别:1-男 2-女 3-保密 + gender = db.Column( + db.Integer, + default=1, + comment="性别:1-男 2-女 3-保密" + ) + + # 用户头像 + avatar = db.Column( + db.String(255), + nullable=False, + comment="用户头像" + ) + + # 手机号 + mobile = db.Column( + db.String(30), + nullable=False, + comment="手机号" + ) + + # 邮箱 + email = db.Column( + db.String(30), + nullable=False, + comment="邮箱" + ) + + # 部门ID + dept_id = db.Column( + db.Integer, + default=0, + comment="部门ID" + ) + + # 职级ID + level_id = db.Column( + db.Integer, + default=0, + comment="职级ID" + ) + + # 岗位ID + position_id = db.Column( + db.Integer, + default=0, + comment="岗位ID" + ) + + # 省份编码 + province_code = db.Column( + db.String(30), + nullable=False, + comment="省份编码" + ) + + # 城市编码 + city_code = db.Column( + db.String(30), + nullable=False, + comment="城市编码" + ) + + # 县区编码 + district_code = db.Column( + db.String(30), + nullable=False, + comment="县区编码" + ) + + # 街道编码 + street_code = db.Column( + db.String(30), + nullable=False, + comment="街道编码" + ) + + # 省市区信息 + city_info = db.Column( + db.String(255), + nullable=True, + comment="省市区信息" + ) + + # 详细地址 + address = db.Column( + db.String(255), + nullable=False, + comment="详细地址" + ) + + # 用户名 + username = db.Column( + db.String(30), + nullable=True, + comment="用户名" + ) + + # 密码 + password = db.Column( + db.String(255), + nullable=True, + comment="密码" + ) + + # 加密盐 + salt = db.Column( + db.String(30), + nullable=True, + comment="加密盐" + ) + + # 个人简介 + intro = db.Column( + db.String(255), + nullable=True, + comment="个人简介" + ) + + # 状态:1-正常 2-禁用 + status = db.Column( + db.Integer, + default=1, + comment="状态:1-正常 2-禁用" + ) + + # 排序 + sort = db.Column( + db.Integer, + default=0, + comment="排序" + ) + + # 个人备注 + note = db.Column( + db.String(255), + nullable=True, + comment="个人备注" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "用户{id}" 的字符串 + """ + return "用户{}".format(self.id) diff --git a/application/models/user_role.py b/application/models/user_role.py new file mode 100644 index 0000000..ffaf9b3 --- /dev/null +++ b/application/models/user_role.py @@ -0,0 +1,108 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +用户角色关联模型模块 +============================================================= + +提供用户与角色的关联关系数据模型定义,用于实现用户角色分配。 +""" + +from application.models.base_db import BaseDB +from application.models.base_model import BaseModel +from config import DB_PREFIX +from extensions import db + + +# ============================================================ +# 用户角色关联模型 +# ============================================================ + +class UserRole(BaseModel, BaseDB): + """ + 用户角色关联模型 + + 用于存储用户与角色的关联关系,实现用户的角色分配。 + 继承自 BaseModel(通用字段)和 BaseDB(数据库操作方法)。 + + Attributes: + id: 主键ID(继承自 BaseModel) + user_id: 用户ID(Integer,默认0) + role_id: 角色ID(Integer,默认0) + """ + + # ============================================================ + # 表配置 + # ============================================================ + + # 设置表名 + __tablename__ = DB_PREFIX + "user_role" + + # ============================================================ + # 字段定义 + # ============================================================ + + # 用户ID + user_id = db.Column( + db.Integer, + default=0, + comment="用户ID" + ) + + # 角色ID + role_id = db.Column( + db.Integer, + default=0, + comment="角色ID" + ) + + # ============================================================ + # 魔法方法 + # ============================================================ + + def __str__(self): + """ + 返回模型的字符串表示 + + Returns: + str: 格式为 "用户角色表{user_id}" 的字符串 + """ + return "用户角色表{}".format(self.user_id) diff --git a/extensions/mail.py b/extensions/mail.py index f164589..0e72e6d 100644 --- a/extensions/mail.py +++ b/extensions/mail.py @@ -1,46 +1,8 @@ """ -Flask-Mail 扩展初始化模块 +扩展兼容 shim(绞杀者迁移期间保留) ============================================================= -仅负责 Flask-Mail 扩展实例的创建与初始化。 - +Flask-Mail 扩展已迁移至 src/core/mail.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -from flask import Flask -from flask_mail import Mail - -# Flask-Mail 扩展实例 -# 在应用工厂中通过 mail.init_app(app) 完成绑定 -mail = Mail() - - -def init_mail(app: Flask): - """ - 初始化 Flask-Mail 扩展,将 Mail 实例绑定到应用。 - - Args: - app: Flask 应用实例 - - Example: - # 在应用工厂函数中调用 - init_mail(app) - """ - from config import ( - MAIL_MAILER, MAIL_SERVER, MAIL_PORT, MAIL_USE_SSL, - MAIL_USERNAME, MAIL_PASSWORD, MAIL_ENCRYPTION, - MAIL_FROM_NAME, MAIL_FROM_ADDRESS, - ) - - app.config['MAIL_MAILER'] = MAIL_MAILER - app.config['MAIL_SERVER'] = MAIL_SERVER - app.config['MAIL_PORT'] = MAIL_PORT - app.config['MAIL_USE_SSL'] = MAIL_USE_SSL - app.config['MAIL_USERNAME'] = MAIL_USERNAME - app.config['MAIL_PASSWORD'] = MAIL_PASSWORD - app.config['MAIL_ENCRYPTION'] = MAIL_ENCRYPTION - app.config['MAIL_DEFAULT_SENDER'] = ( - f'{MAIL_FROM_NAME} <{MAIL_FROM_ADDRESS}>' if MAIL_FROM_NAME and MAIL_FROM_ADDRESS - else MAIL_FROM_ADDRESS or None - ) - - mail.init_app(app) +from src.core.mail import * # noqa: F401,F403 diff --git a/extensions/redis.py b/extensions/redis.py index 0aea491..88b4425 100644 --- a/extensions/redis.py +++ b/extensions/redis.py @@ -1,41 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== - """ -Redis 扩展初始化模块 +扩展兼容 shim(绞杀者迁移期间保留) ============================================================= -仅负责 Flask-Redis 扩展实例的创建与初始化。 -业务层使用的高级封装(序列化、类型转换等)见 utils/redis_client.py。 +Redis 扩展已迁移至 src/core/redis_client.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -from flask import Flask -from flask_redis import FlaskRedis - -# Flask-Redis 扩展实例 -# 在应用工厂中通过 init_redis(app) 完成绑定 -redis_client = FlaskRedis(decode_responses=True, protocol=2) - - -def init_redis(app: Flask): - """ - 初始化 Redis 扩展,将 FlaskRedis 实例绑定到应用。 - - Args: - app: Flask 应用实例 - - Example: - # 在应用工厂函数中调用 - init_redis(app) - """ - redis_client.init_app(app) +from src.core.redis_client import * # noqa: F401,F403 diff --git a/extensions/scheduler.py b/extensions/scheduler.py index e2d0bd5..125f9ee 100644 --- a/extensions/scheduler.py +++ b/extensions/scheduler.py @@ -1,199 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -定时任务调度器扩展模块 -负责初始化和管理 APScheduler 定时任务 +扩展兼容 shim(绞杀者迁移期间保留) +============================================================= + +定时任务调度器扩展已迁移至 src/core/scheduler.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -from flask import Flask - -logger = logging.getLogger(__name__) - - -class Scheduler: - """ - 定时任务调度器管理类 - - 负责调度器的初始化和生命周期管理 - """ - - def __init__(self): - """初始化调度器实例""" - self._initialized = False - - def init_app(self, app: Flask): - """ - 初始化定时任务调度器 - - Args: - app: Flask 应用实例 - - 使用示例: - from extensions.scheduler import scheduler - - def create_app(): - app = Flask(__name__) - # ... 其他配置 - scheduler.init_app(app) - return app - """ - # 避免重复初始化 - if self._initialized: - logger.warning("定时任务调度器已初始化,跳过重复初始化") - return - - # ============================================================ - # 在应用上下文中执行初始化 - # ============================================================ - with app.app_context(): - try: - from application.services.scheduler import reload_scheduler - - # 重新加载所有任务 - count = reload_scheduler() - - self._initialized = True - - if count > 0: - logger.info(f"定时任务调度器初始化完成,已加载 {count} 个任务") - else: - logger.info("定时任务调度器初始化完成,当前无可用任务") - - # 打印调度器状态(调试用) - self._log_scheduler_status() - - except Exception as e: - logger.error(f"定时任务调度器初始化失败: {str(e)}") - raise - - def _log_scheduler_status(self): - """打印调度器状态(用于调试)""" - try: - from application.services.scheduler import scheduler - - if scheduler.running: - jobs = scheduler.get_jobs() - if jobs: - logger.debug("当前调度器中的任务列表:") - for job in jobs: - logger.debug(f" - ID: {job.id}, Name: {job.name}, Next run: {job.next_run_time}") - else: - logger.debug("当前调度器中无任务") - else: - logger.warning("调度器未运行") - except Exception as e: - logger.error(f"获取调度器状态失败: {str(e)}") - - def shutdown(self): - """ - 关闭定时任务调度器 - - 在应用退出时调用 - """ - try: - from application.services.scheduler import scheduler - - if scheduler.running: - scheduler.shutdown() - logger.info("定时任务调度器已关闭") - except Exception as e: - logger.error(f"关闭调度器失败: {str(e)}") - - def reload(self) -> int: - """ - 重新加载所有定时任务 - - Returns: - int: 成功加载的任务数量 - """ - try: - from application.services.scheduler import reload_scheduler - count = reload_scheduler() - logger.info(f"重新加载定时任务完成,共加载 {count} 个任务") - return count - except Exception as e: - logger.error(f"重新加载定时任务失败: {str(e)}") - return 0 - - def get_status(self) -> dict: - """ - 获取调度器状态 - - Returns: - dict: 包含调度器运行状态和任务列表 - """ - try: - from application.services.scheduler import scheduler - - jobs = scheduler.get_jobs() - job_list = [] - - for job in jobs: - job_list.append({ - 'id': job.id, - 'name': job.name, - 'next_run_time': str(job.next_run_time) if job.next_run_time else None, - 'trigger': str(job.trigger) - }) - - return { - 'initialized': self._initialized, - 'running': scheduler.running, - 'job_count': len(job_list), - 'jobs': job_list - } - except Exception as e: - logger.error(f"获取调度器状态失败: {str(e)}") - return { - 'initialized': self._initialized, - 'running': False, - 'job_count': 0, - 'jobs': [], - 'error': str(e) - } - - -# ============================================================ -# 创建全局调度器单例实例 -# ============================================================ -scheduler = Scheduler() +from src.core.scheduler import * # noqa: F401,F403 diff --git a/extensions/sqlalchemy.py b/extensions/sqlalchemy.py index ab058dc..91ff16c 100644 --- a/extensions/sqlalchemy.py +++ b/extensions/sqlalchemy.py @@ -1,87 +1,10 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -SQLAlchemy 数据库扩展模块 -创建数据库实例,供整个应用使用 +扩展兼容 shim(绞杀者迁移期间保留) +============================================================= + +SQLAlchemy 实例已迁移至 src/core/database.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 + +迁移说明:src/core/database.py """ - -from flask_sqlalchemy import SQLAlchemy - -# ============================================================ -# 创建 SQLAlchemy 数据库实例 -# ============================================================ -# 该实例需要在应用工厂中通过 db.init_app(app) 完成初始化 -# 使用延迟绑定模式,支持循环导入 -# ============================================================ - -db = SQLAlchemy() - - -def commit_independently(obj): - """ - 使用独立session提交对象,不受请求事务影响 - - 日志等审计数据无论业务成功或失败都应持久化, - 因此需要绕过 AOP 事务拦截器的 commit/rollback 机制。 - - 实现方式:基于现有引擎创建独立 session,将对象添加并提交, - 完成后关闭 session,不影响请求主 session。 - - Args: - obj: SQLAlchemy 模型实例(未添加到任何 session) - - Raises: - Exception: 数据库操作异常 - """ - from sqlalchemy.orm import sessionmaker - - # 复用 Flask-SQLAlchemy 已创建的引擎,避免额外连接池 - independent_session = sessionmaker(bind=db.engine)() - - try: - independent_session.add(obj) - independent_session.commit() - except Exception: - independent_session.rollback() - raise - finally: - independent_session.close() +from src.core.database import * # noqa: F401,F403 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..a9b6a4e --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,10 @@ +""" +src 包 —— DjangoAdmin 后端源码根包(重构目标结构) +============================================================= + +结构: + core/ 基础设施层(不依赖业务) + common/ 共享层(基类、常量、工具) + modules/ 业务模块层(P3 阶段建设) + routes/ 路由注册中心 +""" diff --git a/src/common/__init__.py b/src/common/__init__.py new file mode 100644 index 0000000..9fedd31 --- /dev/null +++ b/src/common/__init__.py @@ -0,0 +1,12 @@ +""" +common 共享层 +============================================================= + +提供业务模块共享的基类与工具: + - base_model.py / base_db.py 模型基类 + - forms/ 表单基类 + - constants/ 常量 + - utils/ 工具函数 + +依赖约束:common 层可依赖 core 层,不得依赖 modules 层。 +""" diff --git a/src/common/base_db.py b/src/common/base_db.py new file mode 100644 index 0000000..9edbc85 --- /dev/null +++ b/src/common/base_db.py @@ -0,0 +1,208 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称"本产品")拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +数据库操作基类模块 +============================================================= + +提供数据库模型的通用操作方法,包括: +- 单条数据保存 +- 多条数据批量保存 +- 数据删除 + +企业级特性: + 1. 完整的异常处理和日志记录 + 2. 事务边界由调用方控制(配合装饰器使用) + 3. 返回 self 支持链式调用 + 4. 保留旧方法兼容性 +""" + +import logging +from typing import List, Any + +from src.core.database import db + +# 获取模块级日志记录器 +logger = logging.getLogger(__name__) + + +# ============================================================ +# 数据库操作基类 +# ============================================================ + +class BaseDB: + """ + 数据库操作基类 + + 提供数据库的增删操作封装,所有业务模型应继承此类。 + + 企业级设计原则: + 1. 默认不自动提交事务,由调用方通过装饰器统一管理事务 + 2. 完整的异常处理和日志记录 + 3. 返回 self 支持链式调用 + + 方法列表: + save(): 保存当前实例到数据库 + save_all(*args): 批量保存多个实例 + delete(): 从数据库删除当前实例 + """ + + # ============================================================ + # 模型配置 + # ============================================================ + + # 定义为抽象类,不会在数据库中创建独立表 + __abstract__ = True + + # ============================================================ + # 数据保存方法 + # ============================================================ + + def save(self) -> 'BaseDB': + """ + 保存当前实例到数据库 + + 将当前模型对象添加到数据库会话并刷新,但不提交事务。 + 事务由上层装饰器统一管理,确保多个操作在同一事务中执行。 + + 执行流程: + 1. 将对象添加到 SQLAlchemy 会话 + 2. 刷新会话,发送 SQL 到数据库 + 3. 返回自身支持链式调用 + + Returns: + BaseDB: 当前实例(支持链式调用) + + Raises: + Exception: 数据库操作异常会向上抛出,由装饰器统一处理 + + Example: + @transactional_response + def add_position(): + position = Position(name='经理') + position.save() + return R.ok(msg="添加成功") + """ + try: + db.session.add(self) + db.session.flush() + logger.debug(f"save 添加到会话, 模型: {self.__class__.__name__}") + return self + + except Exception as e: + logger.error( + f"save 异常, 模型: {self.__class__.__name__}, 错误: {str(e)}", + exc_info=True + ) + raise + + @staticmethod + def save_all(*args) -> List[Any]: + """ + 批量保存多个实例到数据库 + + 将多个模型对象批量添加到数据库会话并刷新,但不提交事务。 + 事务由上层装饰器统一管理。 + + Args: + *args: 可变参数,传入多个模型实例对象 + + Returns: + List[Any]: 保存的实例列表 + + Raises: + Exception: 数据库操作异常会向上抛出 + + Example: + @transactional_response + def batch_add(): + BaseDB.save_all(user1, user2) + return R.ok(msg="批量添加成功") + """ + if not args: + logger.warning("save_all 收到空参数列表") + return [] + + try: + db.session.add_all(args) + db.session.flush() + logger.debug(f"save_all 添加到会话, 数量: {len(args)}") + return list(args) + + except Exception as e: + logger.error( + f"save_all 异常, 数量: {len(args)}, 错误: {str(e)}", + exc_info=True + ) + raise + + # ============================================================ + # 数据删除方法 + # ============================================================ + + def delete(self) -> bool: + """ + 从数据库删除当前实例 + + 执行物理删除操作,数据将从数据库中永久移除。 + + 注意: + 此方法执行物理删除,数据不可恢复。 + 如需软删除,请使用 BaseModel.delete_by_id()。 + + Returns: + bool: 删除成功返回 True + + Raises: + Exception: 数据库操作异常会向上抛出 + """ + try: + db.session.delete(self) + db.session.flush() + logger.debug(f"delete 标记删除, 模型: {self.__class__.__name__}") + return True + + except Exception as e: + logger.error( + f"delete 异常, 模型: {self.__class__.__name__}, 错误: {str(e)}", + exc_info=True + ) + raise diff --git a/src/common/base_model.py b/src/common/base_model.py new file mode 100644 index 0000000..5fd596a --- /dev/null +++ b/src/common/base_model.py @@ -0,0 +1,671 @@ +""" +基类模型模块 +============================================================= + +提供所有数据库模型的基类,包含通用字段和公共方法。 +所有业务模型应继承此类,避免重复定义通用字段。 + +通用字段包括: +- id: 主键ID +- create_user: 创建人 +- create_time: 创建时间 +- update_user: 更新人 +- update_time: 更新时间 +- is_delete: 软删除标识 + +企业级特性: + 1. 参数校验(id 有效性检查) + 2. 完整的异常处理和日志记录 + 3. 增强版分页查询(支持等值、模糊、范围查询) + 4. 事务管理由调用方控制(配合装饰器使用) +""" + +import logging +from datetime import datetime, timezone +from typing import Optional, List, Dict, Any, Tuple, Union + +from src.core.database import db +from src.common.utils.case_converter import convert_camel_to_snake, convert_camel_case +from src.common.utils.model_utils import models_to_list +from src.common.utils.request import like_contains, parse_id_list +from src.common.constants.common import SERIALIZE_EXCLUDE_FIELDS + +# 获取模块级日志记录器 +logger = logging.getLogger(__name__) + + +# ============================================================ +# 基类模型 +# ============================================================ + + +class BaseModel(db.Model): + """ + 数据库模型基类 + + 所有业务模型应继承此类,自动获得通用字段和公共方法。 + + 提供的功能: + 1. 通用字段:id、创建人、创建时间、更新人、更新时间、软删除标识 + 2. 序列化方法:to_dict()、to_json() + 3. 查询方法:get_by_id()、get_by_ids()、get_all_active() + 4. 分页查询:paginate() 支持多种筛选条件 + 5. 删除方法:delete_by_id()、batch_delete()、batch_delete_r()、batch_restore() + 6. 辅助方法:exists()、get_delete_status_text() + """ + + # ============================================================ + # 模型配置 + # ============================================================ + + # 定义为抽象类,不会在数据库中创建独立表 + __abstract__ = True + + # ============================================================ + # 通用字段定义 + # ============================================================ + + # 主键ID:自增长整数 + id = db.Column( + db.Integer, + primary_key=True, + autoincrement=True, + comment="主键ID" + ) + + # 创建人:记录创建者,由业务层通过 get_username() 设置 + create_user = db.Column( + db.String(64), + default='', + comment="创建人" + ) + + # 创建时间:记录创建时间,创建后不再变化(UTC时区) + create_time = db.Column( + db.DATETIME, + default=lambda: datetime.now(tz=timezone.utc), + comment="创建时间" + ) + + # 更新人:记录最后修改者,由业务层通过 get_username() 设置 + update_user = db.Column( + db.String(64), + default='', + comment="更新人" + ) + + # 更新时间:记录最后修改时间,更新时自动刷新(UTC时区) + # 注意:onupdate 仅在 ORM 实例脏检测刷新时生效,Query.update() 不会触发 + update_time = db.Column( + db.DATETIME, + default=lambda: datetime.now(tz=timezone.utc), + onupdate=lambda: datetime.now(tz=timezone.utc), + comment="更新时间" + ) + + # 软删除标识:0-正常,1-已删除 + # 使用软删除而非物理删除,便于数据恢复和审计 + is_delete = db.Column( + db.Integer, + default=0, + comment="删除标识:0-正常 1-已删除" + ) + + # ============================================================ + # 初始化方法 + # ============================================================ + + def __init__(self, **kwargs): + """ + 通用初始化方法 + + 支持驼峰命名转下划线,自动过滤无效字段。 + + 处理流程: + 1. 驼峰转下划线:前端使用驼峰命名,数据库使用下划线命名 + 2. 过滤 None 值:保持数据库默认值生效 + 3. 字段校验:只保留模型中存在的字段,防止注入 + + Args: + **kwargs: 任意关键字参数 + + Example: + # 前端传入驼峰命名 + position = Position(createUser='admin', name='经理') + # 自动转换为:create_user='admin', name='经理' + """ + # 将驼峰命名的参数转换为下划线命名 + kwargs = convert_camel_to_snake(kwargs) + + # 过滤掉值为 None 的参数 + kwargs = {k: v for k, v in kwargs.items() if v is not None} + + # 只保留模型中存在的字段 + valid_fields = {c.name for c in self.__table__.columns} + kwargs = {k: v for k, v in kwargs.items() if k in valid_fields} + + # 调用父类初始化 + super().__init__(**kwargs) + + # ============================================================ + # 序列化方法 + # ============================================================ + + def to_dict(self, convert_case=True) -> Dict[str, Any]: + """ + 将模型实例转换为字典 + + 遍历模型的所有列,将字段名和值组装成字典。 + 自动排除 is_delete、password、salt 等敏感字段。 + 特别处理 datetime 类型,格式化为字符串便于 JSON 序列化。 + + Args: + convert_case: 是否将字段名转换为驼峰命名,默认False(保持下划线) + + Returns: + dict: 包含所有字段的字典(不含敏感字段) + + Example: + position = Position.query.get(1) + data = position.to_dict() + # 返回:{'id': 1, 'name': '经理', 'create_time': '2024-01-01 12:00:00'} + """ + result = {} + for column in self.__table__.columns: + # 跳过敏感字段 + if column.name in SERIALIZE_EXCLUDE_FIELDS: + continue + field_name = column.name + value = getattr(self, field_name) + # 日期时间类型格式化为字符串 + if isinstance(value, datetime): + value = value.strftime('%Y-%m-%d %H:%M:%S') + if convert_case: + field_name = convert_camel_case(field_name) + result[field_name] = value + return result + + # ============================================================ + # 通用查询方法(带参数校验和异常处理) + # ============================================================ + + @classmethod + def get_by_id(cls, id: Optional[int]) -> Optional['BaseModel']: + """ + 根据ID获取未删除的记录 + + 企业级实现: + 1. 参数校验:检查 id 是否有效 + 2. 异常处理:捕获数据库异常并记录日志 + + Args: + id: 主键ID + + Returns: + Optional[BaseModel]: 记录对象,不存在时返回 None + + Example: + position = Position.get_by_id(1) + if position: + print(position.name) + """ + # 参数有效性校验 + if id is None or id <= 0: + logger.warning(f"get_by_id 收到无效ID: {id}") + return None + + try: + return cls.query.filter(cls.id == id, cls.is_delete == 0).first() + except Exception as e: + logger.error( + f"get_by_id 查询异常, 模型: {cls.__name__}, ID: {id}, 错误: {str(e)}", + exc_info=True + ) + return None + + @classmethod + def get_by_ids(cls, ids: Optional[List[int]]) -> List['BaseModel']: + """ + 根据ID列表批量获取未删除的记录 + + 适用于批量查询场景,如根据ID列表批量查询用户信息。 + 自动过滤无效ID和已删除记录。 + + Args: + ids: 主键ID列表 + + Returns: + List[BaseModel]: 记录对象列表,无效参数返回空列表 + + Example: + positions = Position.get_by_ids([1, 2, 3]) + for pos in positions: + print(pos.name) + """ + # 空列表直接返回 + if not ids: + logger.debug(f"get_by_ids 收到空ID列表, 模型: {cls.__name__}") + return [] + + # 过滤无效ID + valid_ids = [id for id in ids if id and id > 0] + if not valid_ids: + logger.warning(f"get_by_ids 所有ID均无效: {ids}") + return [] + + try: + return cls.query.filter(cls.id.in_(valid_ids), cls.is_delete == 0).all() + except Exception as e: + logger.error( + f"get_by_ids 查询异常, 模型: {cls.__name__}, 错误: {str(e)}", + exc_info=True + ) + return [] + + @classmethod + def get_all_active(cls) -> List['BaseModel']: + """ + 获取所有未删除的记录 + + 适用于需要获取全部有效数据的场景。 + 数据量大时建议使用分页查询,避免内存溢出。 + + Returns: + List[BaseModel]: 所有未删除的记录列表 + """ + try: + return cls.query.filter(cls.is_delete == 0).all() + except Exception as e: + logger.error( + f"get_all_active 查询异常, 模型: {cls.__name__}, 错误: {str(e)}", + exc_info=True + ) + return [] + + # ============================================================ + # 增强版分页查询方法 + # ============================================================ + + @classmethod + def paginate( # noqa: C901 (兼容性增强分页查询,复杂度较高属存量设计) + cls, + page: int = 1, + per_page: int = 15, + filters: Optional[Dict[str, Any]] = None, + like_filters: Optional[Dict[str, str]] = None, + range_filters: Optional[Dict[str, Tuple[Any, Any]]] = None, + order_by: Optional[Union[str, List[str]]] = None, + order_desc: Optional[Union[str, List[str]]] = None, + with_deleted: bool = False, + as_dict: bool = True + ) -> Tuple[List[Union[Dict, Any]], int, int]: + """ + 通用分页查询方法(增强版) + + 企业级实现: + 1. 参数修正:page 和 per_page 自动修正为有效值 + 2. 多种筛选:支持等值、模糊、范围三种筛选方式 + 3. 灵活排序:支持单字段和多字段排序 + 4. 自动转换:可选的字典转换 + + 支持的筛选类型: + 1. 等值筛选(filters):精确匹配,如 status=1 + 2. 模糊筛选(like_filters):LIKE 查询,如 name LIKE '%经理%' + 3. 范围筛选(range_filters):区间查询,如 create_time BETWEEN + + Args: + page: 当前页码,默认1(自动修正为 >= 1) + per_page: 每页数量,默认15(自动修正为 1-100) + filters: 等值筛选,如 {'status': 1} + like_filters: 模糊筛选,如 {'name': '经理'} + range_filters: 范围筛选,如 {'create_time': ('2024-01-01', '2024-12-31')} + order_by: 升序排序字段,如 'sort' 或 ['sort', 'id'] + order_desc: 降序排序字段,如 'create_time' + with_deleted: 是否包含已删除记录,默认 False + as_dict: 是否转换为字典,默认 True + + Returns: + tuple: (items, total, pages) + - items: 当前页数据列表 + - total: 总记录数 + - pages: 总页数 + + Example: + # 基础分页 + items, total, pages = Position.paginate( + page=1, per_page=10, + filters={'status': 1}, + order_by='sort' + ) + + # 组合查询 + items, total, pages = Position.paginate( + page=1, per_page=10, + filters={'status': 1}, + like_filters={'name': '经理'}, + order_by='sort', + order_desc='create_time' + ) + """ + # ============================================================ + # 步骤1:参数修正 + # ============================================================ + if page < 1: + page = 1 + if per_page < 1: + per_page = 15 + if per_page > 100: + per_page = 100 + + try: + # ============================================================ + # 步骤2:构建基础查询 + # ============================================================ + query = cls.query + if not with_deleted: + query = query.filter(cls.is_delete == 0) + + # ============================================================ + # 步骤3:应用等值筛选 + # ============================================================ + if filters: + for key, value in filters.items(): + if value is not None and hasattr(cls, key): + query = query.filter(getattr(cls, key) == value) + + # ============================================================ + # 步骤4:应用模糊筛选 + # ============================================================ + if like_filters: + for key, value in like_filters.items(): + if value is not None and hasattr(cls, key): + query = query.filter(getattr(cls, key).like(like_contains(value), escape='\\')) + + # ============================================================ + # 步骤5:应用范围筛选 + # ============================================================ + if range_filters: + for key, (start, end) in range_filters.items(): + if hasattr(cls, key): + if start is not None: + query = query.filter(getattr(cls, key) >= start) + if end is not None: + query = query.filter(getattr(cls, key) <= end) + + # ============================================================ + # 步骤6:应用排序 + # ============================================================ + if order_by: + if isinstance(order_by, str): + query = query.order_by(getattr(cls, order_by).asc()) + elif isinstance(order_by, list): + for field in order_by: + query = query.order_by(getattr(cls, field).asc()) + + if order_desc: + if isinstance(order_desc, str): + query = query.order_by(getattr(cls, order_desc).desc()) + elif isinstance(order_desc, list): + for field in order_desc: + query = query.order_by(getattr(cls, field).desc()) + + # ============================================================ + # 步骤7:计算总数和页数 + # ============================================================ + total = query.count() + pages = (total + per_page - 1) // per_page if total > 0 else 0 + + # ============================================================ + # 步骤8:执行分页查询 + # ============================================================ + items = query.offset((page - 1) * per_page).limit(per_page).all() + + # ============================================================ + # 步骤9:转换为字典(可选) + # ============================================================ + if as_dict: + items = models_to_list(items) + + return items, total, pages + + except Exception as e: + logger.error( + f"paginate 查询异常, 模型: {cls.__name__}, 错误: {str(e)}", + exc_info=True + ) + return [], 0, 0 + + # ============================================================ + # 删除方法(事务由调用方控制) + # ============================================================ + + @classmethod + def delete_by_id(cls, id: Optional[int]) -> bool: + """ + 删除单条记录(软删除) + + 将指定ID记录的 is_delete 标识设置为 1。 + 事务由上层装饰器统一管理。 + + Args: + id: 主键ID + + Returns: + bool: 删除成功返回 True,失败返回 False + + Example: + @transactional_response + def delete_position(): + Position.delete_by_id(1) + return R.ok(msg="删除成功") + """ + # 参数校验 + if id is None or id <= 0: + logger.warning(f"delete_by_id 收到无效ID: {id}") + return False + + try: + record = cls.get_by_id(id) + if not record: + logger.debug(f"delete_by_id 记录不存在, 模型: {cls.__name__}, ID: {id}") + return False + + # 标记为已删除 + record.is_delete = 1 + db.session.add(record) + + logger.info(f"delete_by_id 成功, 模型: {cls.__name__}, ID: {id}") + return True + + except Exception as e: + logger.error( + f"delete_by_id 异常, 模型: {cls.__name__}, ID: {id}, 错误: {str(e)}", + exc_info=True + ) + raise + + @classmethod + def batch_delete(cls, ids: Optional[List[int]]) -> int: + """ + 批量删除(软删除) + + 一次性删除多条记录,使用 IN 查询提高效率。 + 只删除未删除的记录。 + + Args: + ids: 主键ID列表 + + Returns: + int: 实际删除的记录数 + + Example: + @transactional_response + def batch_delete(): + count = Position.batch_delete([1, 2, 3]) + return R.ok(msg=f"删除 {count} 条记录") + """ + # 空列表直接返回 + if not ids: + return 0 + + # 过滤无效ID + valid_ids = [id for id in ids if id and id > 0] + if not valid_ids: + logger.warning(f"batch_delete 所有ID均无效: {ids}") + return 0 + + try: + # 批量更新,synchronize_session=False 提高性能 + count = cls.query.filter( + cls.id.in_(valid_ids), + cls.is_delete == 0 + ).update( + {cls.is_delete: 1, cls.update_time: datetime.now(tz=timezone.utc)}, + synchronize_session=False + ) + + logger.info( + f"batch_delete 成功, 模型: {cls.__name__}, 删除数量: {count}" + ) + return count + + except Exception as e: + logger.error( + f"batch_delete 异常, 模型: {cls.__name__}, 错误: {str(e)}", + exc_info=True + ) + raise + + @classmethod + def batch_delete_r(cls, ids_str: Optional[str]): + """ + 批量删除并返回R对象(供service层直接使用) + + 封装了ID校验、解析、批量删除、返回R响应的完整流程, + 消除各service中重复的删除逻辑。 + + Args: + ids_str: 逗号分隔的ID字符串,如 "1,2,3" + + Returns: + R: 操作结果 + + Example: + def delete_positions(position_ids): + return Position.batch_delete_r(position_ids) + """ + if not ids_str: + from utils import R + return R.failed("记录ID不存在") + + id_list = parse_id_list(ids_str) + if not id_list: + from utils import R + return R.failed("无效的记录ID") + + count = cls.batch_delete(id_list) + from utils import R + return R.ok(msg=f"本次共删除{count}条数据") + + @classmethod + def batch_restore(cls, ids: Optional[List[int]]) -> int: + """ + 批量恢复已删除的记录 + + 将已删除的记录恢复为正常状态(is_delete = 0)。 + 只恢复已删除的记录。 + + Args: + ids: 主键ID列表 + + Returns: + int: 实际恢复的记录数 + + Example: + @transactional_response + def batch_restore(): + count = Position.batch_restore([1, 2, 3]) + return R.ok(msg=f"恢复 {count} 条记录") + """ + # 空列表直接返回 + if not ids: + return 0 + + # 过滤无效ID + valid_ids = [id for id in ids if id and id > 0] + if not valid_ids: + logger.warning(f"batch_restore 所有ID均无效: {ids}") + return 0 + + try: + # 批量恢复,只恢复已删除的记录 + count = cls.query.filter( + cls.id.in_(valid_ids), + cls.is_delete == 1 + ).update( + {cls.is_delete: 0}, + synchronize_session=False + ) + + logger.info( + f"batch_restore 成功, 模型: {cls.__name__}, 恢复数量: {count}" + ) + return count + + except Exception as e: + logger.error( + f"batch_restore 异常, 模型: {cls.__name__}, 错误: {str(e)}", + exc_info=True + ) + raise + + # ============================================================ + # 辅助方法 + # ============================================================ + + @classmethod + def get_delete_status_text(cls, is_delete: int) -> str: + """ + 获取删除状态文本 + + 将数字状态的 is_delete 转换为可读的文本。 + 用于前端展示或日志记录。 + + Args: + is_delete: 删除标识值(0或1) + + Returns: + str: 状态文本 + """ + return {0: '正常', 1: '已删除'}.get(is_delete, '未知') + + @classmethod + def exists(cls, id: Optional[int]) -> bool: + """ + 检查记录是否存在(未删除) + + 快速检查记录是否存在,返回布尔值。 + 适用于记录存在性验证场景。 + + Args: + id: 主键ID + + Returns: + bool: 存在返回 True,否则返回 False + + Example: + if Position.exists(1): + print("记录存在") + """ + # 参数校验 + if id is None or id <= 0: + return False + + try: + return cls.query.filter(cls.id == id, cls.is_delete == 0).count() > 0 + except Exception as e: + logger.error( + f"exists 查询异常, 模型: {cls.__name__}, ID: {id}, 错误: {str(e)}", + exc_info=True + ) + return False diff --git a/src/common/constants/__init__.py b/src/common/constants/__init__.py new file mode 100644 index 0000000..15df3b4 --- /dev/null +++ b/src/common/constants/__init__.py @@ -0,0 +1,8 @@ +""" +constants 常量包 +============================================================= + + - common.py 通用常量(分页、默认密码、消息文本、序列化排除字段) + - dictionary_constants.py 字典相关常量(原 application/constants/dict.py) + - error_code.py 错误码定义(后续阶段建设) +""" diff --git a/src/common/constants/common.py b/src/common/constants/common.py new file mode 100644 index 0000000..df85b4f --- /dev/null +++ b/src/common/constants/common.py @@ -0,0 +1,85 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 珊瑚云团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +# ======================= 分页配置 ========================== +# 该部分配置用于控制列表页面的数据分页显示 +# 通过调整每页记录数可以优化页面加载速度和用户体验 +# ========================================================== + +# 默认每页显示记录数 +# 类型:int +# 默认值:10 +# 说明:列表页面默认每页显示的数据条数,可根据实际需求调整 +PAGE_SIZE = 10 + +# ======================= 用户配置 ========================== +# 该部分配置用于管理用户账户相关的默认设置 +# 包含新用户创建和密码重置时的默认值 +# ========================================================== + +# 默认密码 +# 类型:str +# 默认值:"123456" +# 说明:新创建用户或重置密码时的默认密码,建议首次登录后强制修改 +DEFAULT_PASSWORD = "123456" + +# ======================= 消息提示配置 ========================== +# 该部分配置定义了所有操作响应的统一消息文本 +# 用于保持系统前后端消息提示的一致性 +# ========================================================== + +# 操作成功提示信息 +# 类型:str +# 默认值:"操作成功" +# 说明:统一的操作成功返回消息,所有成功操作均使用此消息 +MESSAGE_OK = "操作成功" + +# 操作失败提示信息 +# 类型:str +# 默认值:"操作失败" +# 说明:统一的操作失败返回消息,所有失败操作均使用此消息 +MESSAGE_FAIL = "操作失败" + +# ======================= 序列化安全配置 ========================== +# 模型序列化时统一排除的敏感字段,防止密码、盐值等泄露给前端 +# ================================================================== + +SERIALIZE_EXCLUDE_FIELDS = frozenset({'is_delete', 'password', 'salt'}) diff --git a/src/common/constants/dictionary_constants.py b/src/common/constants/dictionary_constants.py new file mode 100644 index 0000000..6e23df8 --- /dev/null +++ b/src/common/constants/dictionary_constants.py @@ -0,0 +1,195 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 珊瑚云团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +系统常量定义模块 +============================================================= + +该模块定义了系统中使用的各种常量字典,主要用于: +1. 将数据库中的数字/代码值转换为可读的文本标签 +2. 为前端下拉框、单选框、复选框等提供选项数据 +3. 统一管理系统中所有枚举值的映射关系 + +所有常量均为字典类型,键为存储值,值为显示文本。 +""" + +# ==================== 基础信息常量 ==================== + +# 性别映射字典 +# 用于用户信息中的性别字段显示 +GENDER_LIST = { + 1: "男", # 男性 + 2: "女", # 女性 + 3: "保密", # 保密/未公开 +} + +# ==================== 通用状态常量 ==================== + +# 状态映射字典 +# 用于系统中各类数据的状态字段显示 +# 1-正常(启用、有效、激活等状态) +# 2-禁用(停用、无效、冻结等状态) +STATUS_LIST = { + 1: "正常", # 正常状态 + 2: "禁用", # 禁用状态 +} + +# ==================== 参数相关常量 ==================== + +# 参数类型映射字典 +# 用于系统参数管理中的参数类型字段显示 +# 0-系统参数(系统级配置,影响系统运行) +# 1-业务参数(业务级配置,影响业务逻辑) +PARAM_TYPE_LIST = { + 0: "系统参数", # 系统参数 + 1: "业务参数", # 业务参数 +} + +# ==================== 友情链接常量 ==================== + +# 友链类型映射字典 +# 用于区分友情链接的不同合作性质 +LINK_TYPE_LIST = { + 1: "友情链接", # 普通友情链接 + 2: "合作伙伴" # 合作伙伴链接(关系更紧密) +} + +# 友链形式映射字典 +# 用于指定友情链接的展示形式 +LINK_FORM_LIST = { + 1: "文字链接", # 纯文本形式的链接 + 2: "图片链接", # 带有Logo/图片的链接 +} + +# ==================== 配置相关常量 ==================== + +# 配置项类型映射字典 +# 用于定义系统配置项的数据类型和对应的输入控件 +CONFIG_ITEM_TYPE_LIST = { + "text": "单行文本", # 普通单行文本输入框 + "textarea": "多行文本", # 多行文本域 + "ueditor": "富文本编辑器", # 富文本编辑器(支持图文排版) + "date": "日期", # 日期选择器 + "datetime": "时间", # 日期时间选择器 + "number": "数字", # 数字输入框 + "select": "下拉框", # 下拉选择框 + "radio": "单选框", # 单选按钮组 + "checkbox": "复选框", # 复选框组(多选) + "image": "单张图片", # 单张图片上传 + "images": "多张图片", # 多张图片上传 + "password": "密码", # 密码输入框 + "icon": "字体图标", # 字体图标选择器 + "file": "单个文件", # 单个文件上传 + "files": "多个文件", # 多个文件上传 + "hidden": "隐藏", # 隐藏字段 + "readonly": "只读文本", # 只读文本显示 +} + +# ==================== 地区相关常量 ==================== + +# 城市级别映射字典 +# 用于标识行政区划的级别 +CITY_LEVEL_LIST = { + 1: "省份", # 省级行政区(省、自治区、直辖市) + 2: "城市", # 地级行政区(市、自治州) + 3: "县区", # 县级行政区(区、县、县级市) + 4: "街道" # 乡级行政区(街道、镇、乡) +} + +# 通知类型:1-通知 2-公告 +NOTICE_TYPE_LIST = { + 1: "通知", + 2: "公告", +} + +# ==================== 操作日志相关常量 ==================== + +# 操作日志来源映射字典 +# 用于标识操作日志的请求来源 +OPERATION_LOG_SOURCE_LIST = { + 0: "后台系统", # 后台管理系统操作 + 1: "手机站", # 手机端/移动端操作 + 2: "客户端", # PC客户端/应用程序操作 + 3: "其他", # 其他来源 +} + +# 操作日志类型映射字典 +# 用于标识操作日志的具体操作类型 +OPERATION_LOG_TYPE_LIST = { + 1: "登录", + 2: "退出", + 3: "新增", + 4: "修改", + 5: "删除", + 6: "查询", + 7: "导出", + 8: "导入", + 9: "提交", + 10: "撤销", + 11: "审核", + 12: "强退", + 13: "开账号", + 14: "清空", + 15: "授权", + 16: "缓存", + 17: "生成代码", + 18: "其他", +} + +# ==================== 登录日志相关常量 ==================== + +# 登录日志类型映射字典 +# 用于标识登录日志的操作类型 +LOGIN_LOG_TYPE_LIST = { + 1: "登录", + 2: "退出", +} + +# ==================== 文章相关常量 ==================== + +# 文章状态映射字典 +# 用于文章管理中的状态字段显示 +# 0-正常(已发布、上架状态) +# 1-下架(已下架、草稿状态) +ARTICLE_STATUS_LIST = { + 0: "正常", # 正常/已发布 + 1: "下架", # 下架/草稿 +} diff --git a/src/common/forms/__init__.py b/src/common/forms/__init__.py new file mode 100644 index 0000000..198b3de --- /dev/null +++ b/src/common/forms/__init__.py @@ -0,0 +1,7 @@ +""" +forms 表单基类包 +============================================================= + + - base_form.py 表单基类(BaseForm,支持 JSON 数组) + - fields.py 自定义字段(ArrayField) +""" diff --git a/src/common/forms/base_form.py b/src/common/forms/base_form.py new file mode 100644 index 0000000..aa4741e --- /dev/null +++ b/src/common/forms/base_form.py @@ -0,0 +1,105 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== +import json + +from flask_wtf import FlaskForm + +from src.common.forms.fields import ArrayField + + +class BaseForm(FlaskForm): + """ + 自定义表单基类,增强对 JSON 数组的支持 + + 重写 process 方法,确保 ArrayField 能正确处理数组数据。 + + 使用方式: + class UserForm(BaseForm): + city = ArrayField('行政区划') + roles = ArrayField('用户角色') + """ + + def process(self, formdata=None, obj=None, data=None, **kwargs): + """ + 重写 process 方法,正确处理数组字段 + + 问题的根源:Flask-WTF 的 Form.process 方法在处理 data 参数时, + 会调用 _process_multidict 方法,该方法会将列表值当作多值参数处理, + 导致数组被拆分成单个值。 + + 解决方案:在调用父类方法之前,先处理 ArrayField 类型的数据, + 将列表值转换为 JSON 字符串,避免被拆分。 + + Args: + formdata: 表单数据(通常是 request.form) + obj: 模型对象 + data: 字典数据(通常是 request.get_json()) + **kwargs: 其他参数 + """ + # 如果传入了 data 字典 + if data is not None and isinstance(data, dict): + # 复制一份数据,避免修改原始数据 + processed_data = {} + + for key, value in data.items(): + # 检查对应的字段是否是 ArrayField + field = getattr(self, key, None) + + # 如果是 ArrayField 且值是列表,转换为 JSON 字符串 + # 这样可以避免父类方法将数组拆分成单个值 + if isinstance(field, ArrayField) and isinstance(value, list): + processed_data[key] = json.dumps(value) + else: + # 其他字段保持原值 + processed_data[key] = value + + # 使用处理后的数据调用父类方法 + super().process(formdata, obj, processed_data, **kwargs) + + # 重要:父类处理完成后,将 ArrayField 的 data 从 JSON 字符串转换回列表 + for key, value in data.items(): + field = getattr(self, key, None) + if isinstance(field, ArrayField) and isinstance(value, list): + # 直接设置 data 属性为原始列表 + field.data = value + else: + # 没有 data 参数时,直接调用父类方法 + super().process(formdata, obj, data, **kwargs) diff --git a/src/common/forms/fields.py b/src/common/forms/fields.py new file mode 100644 index 0000000..7f9c4ff --- /dev/null +++ b/src/common/forms/fields.py @@ -0,0 +1,177 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +通用表单验证模块 +============================================================= + +提供通用的表单验证功能,包括: +- 自定义数组字段类 ArrayField +- 自定义表单基类 BaseForm(支持JSON数组) +""" + +import json +from wtforms import Field + + +# ============================================================ +# 自定义数组字段类 +# ============================================================ + +class ArrayField(Field): + """ + 自定义数组字段,用于接收 JSON 数组数据 + + 使用方式: + city = ArrayField(label='行政区划', validators=[DataRequired()]) + roles = ArrayField(label='用户角色', validators=[Optional()]) + + 工作原理: + 1. process_data: 处理通过 data= 参数传入的数据 + 2. process_formdata: 处理传统表单提交的数据 + 3. _value: 用于表单渲染时显示 + """ + + def __init__(self, label=None, validators=None, **kwargs): + """ + 初始化数组字段 + + Args: + label: 字段标签 + validators: 验证器列表 + **kwargs: 其他参数 + """ + super().__init__(label, validators, **kwargs) + self.data = [] # 默认空数组 + + def process_data(self, value): + """ + 处理直接赋值的数据(通过 data= 参数传入) + + 这是关键方法,当使用 UserForm(data=json_data) 时会调用此方法。 + + Args: + value: 传入的值,可能是 list、str、None 或其他类型 + """ + # 空值处理 + if value is None: + self.data = [] + return + + # 如果已经是列表,直接使用(这是JSON数组的情况) + if isinstance(value, list): + self.data = value + return + + # 如果是字符串,尝试解析为 JSON + if isinstance(value, str): + # 去除首尾空格 + value = value.strip() + if not value: + self.data = [] + return + + try: + parsed = json.loads(value) + # 确保解析结果是列表 + if isinstance(parsed, list): + self.data = parsed + else: + # 单个值转为列表 + self.data = [parsed] if parsed is not None else [] + except (json.JSONDecodeError, TypeError): + # 不是 JSON,作为单值列表 + self.data = [value] if value else [] + return + + # 其他类型(如 int、float 等)转为单值列表 + self.data = [value] if value is not None else [] + + def process_formdata(self, valuelist): + """ + 处理表单数据(通过 request.form 提交的数据) + + Args: + valuelist: 表单值列表,通常是一个列表 + """ + # 空值处理 + if not valuelist: + self.data = [] + return + + # 获取第一个值 + value = valuelist[0] + + # 如果是列表类型(来自多选或多文件上传) + if isinstance(value, list): + self.data = value + return + + # 如果是字符串,尝试解析 JSON + if isinstance(value, str): + value = value.strip() + if not value: + self.data = [] + return + + try: + parsed = json.loads(value) + if isinstance(parsed, list): + self.data = parsed + else: + self.data = [parsed] if parsed is not None else [] + except (json.JSONDecodeError, TypeError): + self.data = [value] if value else [] + return + + # 其他类型转为单值列表 + self.data = [value] if value is not None else [] + + def _value(self): + """ + 渲染表单时使用,将列表转为 JSON 字符串 + + Returns: + str: JSON 字符串表示的数组 + """ + if self.data and isinstance(self.data, list): + return json.dumps(self.data, ensure_ascii=False) + return '[]' diff --git a/src/common/utils/__init__.py b/src/common/utils/__init__.py new file mode 100644 index 0000000..adf4cd5 --- /dev/null +++ b/src/common/utils/__init__.py @@ -0,0 +1,17 @@ +""" +utils 工具函数包 +============================================================= + +提供与具体业务解耦的通用工具函数: + - request.py 请求解析 + - model_utils.py 模型工具(models_to_list 等) + - case_converter.py 命名转换(camelCase <-> snake_case) + - regular.py WTForms 错误提取 + - captcha.py 验证码生成 + - file.py / rich_text.py / formatter.py 文件与富文本处理 + - ip.py / ip2region.py / user_agent.py IP 与 UA 解析 + - json.py JSON 序列化 + +说明:R / jwt / password / security / redis_client 属基础设施, +已归入 src/core/,不在此包。 +""" diff --git a/src/common/utils/captcha.py b/src/common/utils/captcha.py new file mode 100644 index 0000000..4db50d6 --- /dev/null +++ b/src/common/utils/captcha.py @@ -0,0 +1,230 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== + +""" +验证码生成与验证模块 +============================================================= + +功能说明: + 1. 生成带干扰的验证码图片,返回 Base64 格式数据 + 2. 验证用户输入的验证码是否正确 + 3. 防刷机制:限制同一 IP 获取验证码的频率 + +技术特点: + - 使用 secrets 模块生成安全随机数,替代 random 模块 + - 字符集去除 0/O/1/I/l/5/S/2/Z 等易混淆字符,提升用户体验 + - 验证码图片包含干扰线和噪点,防止 OCR 识别 + - 验证码存储于 Redis,支持过期自动失效 + - 验证成功后自动删除,确保一次性使用,防止重放攻击 + - 大小写不敏感比较,提升用户体验 + +配置来源: + 所有配置参数从 config 模块导入,由 .env 文件控制 + 包括:验证码长度、过期时间、防刷间隔、字符集、字体、图片尺寸等 + +依赖: + - utils.redis_client: Redis 客户端,用于存储验证码 + - config: 验证码配置参数(从 .env 读取) + - PIL: 图片生成与处理 + +接口说明: + generate_captcha() -> dict + 生成验证码,返回 {"idKey": "唯一标识", "data": "base64图片数据"} + 调用频率受 CAPTCHA_RATE_LIMIT 限制 + + verify_captcha(id_key, user_input) -> bool + 验证用户输入的验证码是否正确 + 验证成功后自动删除 Redis 中的记录 +""" + +import secrets +from io import BytesIO + +from PIL import Image, ImageFont, ImageDraw, ImageFilter +import base64 + +from src.core.redis_client import redis +from config import ( + CAPTCHA_LENGTH, + CAPTCHA_EXPIRE, + CAPTCHA_RATE_LIMIT, + CAPTCHA_RATE_WINDOW, + CAPTCHA_CHARS, + CAPTCHA_FONT_PATH, + CAPTCHA_IMAGE_WIDTH, + CAPTCHA_IMAGE_HEIGHT, + CAPTCHA_FONT_SIZE, + CAPTCHA_NOISE_LINES, + CAPTCHA_NOISE_DOTS, +) + + +# ============================================================ +# 内部函数 +# ============================================================ + +def get_random_color(): + """获取随机颜色RGB(使用secrets更安全)""" + return ( + secrets.randbelow(256), + secrets.randbelow(256), + secrets.randbelow(256) + ) + + +def generate_image(length): + """生成验证码图片 + + Args: + length: 验证码长度 + + Returns: + tuple: (PIL.Image对象, 验证码字符串) + """ + # 从字符集随机抽取 + code = ''.join(secrets.choice(CAPTCHA_CHARS) for _ in range(length)) + + # 创建画布(浅灰色背景,减少对比度干扰) + image = Image.new('RGB', (CAPTCHA_IMAGE_WIDTH, CAPTCHA_IMAGE_HEIGHT), color=(240, 240, 240)) + + # 加载字体 + try: + font = ImageFont.truetype(CAPTCHA_FONT_PATH, size=CAPTCHA_FONT_SIZE) + except Exception: + font = ImageFont.load_default() + + draw = ImageDraw.Draw(image) + + # 绘制验证码字符(随机偏移,防OCR识别) + char_width = (CAPTCHA_IMAGE_WIDTH - 20) // length + for i, c in enumerate(code): + x = 10 + i * char_width + secrets.randbelow(8) + y = secrets.randbelow(10) + 2 + draw.text( + (x, y), + text=c, + fill=get_random_color(), + font=font + ) + + # 绘制干扰线 + for _ in range(CAPTCHA_NOISE_LINES): + x1 = secrets.randbelow(CAPTCHA_IMAGE_WIDTH) + y1 = secrets.randbelow(CAPTCHA_IMAGE_HEIGHT // 2) + x2 = secrets.randbelow(CAPTCHA_IMAGE_WIDTH) + y2 = secrets.randbelow(CAPTCHA_IMAGE_HEIGHT // 2) + CAPTCHA_IMAGE_HEIGHT // 2 + draw.line(((x1, y1), (x2, y2)), fill=get_random_color(), width=1) + + # 添加噪点 + for _ in range(CAPTCHA_NOISE_DOTS): + x = secrets.randbelow(CAPTCHA_IMAGE_WIDTH) + y = secrets.randbelow(CAPTCHA_IMAGE_HEIGHT) + draw.point((x, y), fill=get_random_color()) + + # 滤镜增强边缘 + image = image.filter(ImageFilter.EDGE_ENHANCE) + + return image, code + + +def get_client_ip(): + """获取客户端真实IP(支持代理)""" + from flask import request + if request.headers.get('X-Forwarded-For'): + return request.headers.get('X-Forwarded-For').split(',')[0].strip() + return request.remote_addr + + +# ============================================================ +# 对外接口 +# ============================================================ + +def generate_captcha(): + """生成验证码实例 + + Returns: + dict: { + "idKey": str, # 验证码唯一标识 + "data": str # base64图片数据 + } + """ + # ============================================================ + # 防刷:滑动窗口计数,限制同一IP在窗口期内的请求次数 + # ============================================================ + client_ip = get_client_ip() + rate_key = f"captcha:rate:{client_ip}" + count = redis.get(rate_key) + + if count and int(count) >= CAPTCHA_RATE_LIMIT: + ttl = redis.ttl(rate_key) + remaining = ttl if ttl > 0 else CAPTCHA_RATE_WINDOW + raise Exception(f"请求过于频繁,请等待 {remaining} 秒后再试") + + # ============================================================ + # 生成验证码 + # ============================================================ + image, code = generate_image(CAPTCHA_LENGTH) + + # 转换为base64 + buffer = BytesIO() + image.save(buffer, format='PNG') + buf_bytes = buffer.getvalue() + data = base64.b64encode(buf_bytes).decode('ascii') + + # 生成唯一ID(使用secrets更安全) + id_key = secrets.token_urlsafe(24) + + # 存储验证码到Redis + redis.set(id_key, code, expire=CAPTCHA_EXPIRE) + + # 递增计数器,首次时设置过期时间(滑动窗口) + if count: + redis.incr(rate_key) + else: + redis.set(rate_key, 1, expire=CAPTCHA_RATE_WINDOW) + + return { + "idKey": id_key, + "data": data + } + + +def verify_captcha(id_key, user_input): + """验证验证码(验证成功后自动删除,一次性使用) + + Args: + id_key: 验证码ID + user_input: 用户输入的验证码 + + Returns: + bool: 验证是否通过 + """ + if not id_key or not user_input: + return False + + # 从Redis获取验证码 + stored_code = redis.get(id_key) + + if not stored_code: + return False + + # 转为大写比较(大小写不敏感) + stored_code = str(stored_code).upper().strip() + user_input = str(user_input).upper().strip() + + # 验证通过后删除(一次性使用,防止重放攻击) + if stored_code == user_input: + redis.delete(id_key) + return True + + return False diff --git a/src/common/utils/case_converter.py b/src/common/utils/case_converter.py new file mode 100644 index 0000000..cc286a3 --- /dev/null +++ b/src/common/utils/case_converter.py @@ -0,0 +1,327 @@ +""" +命名格式转换工具模块 +============================================================= + +提供字符串命名格式之间的转换功能,主要用于数据库字段名(下划线命名) +与前端字段名(驼峰命名)之间的转换。 + +主要功能: +1. 下划线命名 → 驼峰命名 +2. 驼峰命名 → 下划线命名 +3. 下划线命名 → 帕斯卡命名(大驼峰) +4. 递归转换字典的所有键名 +""" + + +def convert_camel_case(snake_str): + """ + 将下划线命名转换为驼峰命名 + + Args: + snake_str: 下划线命名字符串,如 'create_time' + + Returns: + 驼峰命名字符串,如 'createTime' + + Examples: + >>> convert_camel_case('create_time') + 'createTime' + >>> convert_camel_case('user_id') + 'userId' + >>> convert_camel_case('id') + 'id' + >>> convert_camel_case('') + '' + """ + if not snake_str: + return '' + + components = snake_str.split('_') + return components[0] + ''.join(_capitalize_first_letter(x) for x in components[1:]) + + +def convert_snake_case(camel_str): + """ + 将驼峰命名转换为下划线命名 + + Args: + camel_str: 驼峰命名字符串,如 'createTime' + + Returns: + 下划线命名字符串,如 'create_time' + + Examples: + >>> convert_snake_case('createTime') + 'create_time' + >>> convert_snake_case('userId') + 'user_id' + >>> convert_snake_case('id') + 'id' + >>> convert_snake_case('') + '' + """ + if not camel_str: + return '' + + result = [] + for i, char in enumerate(camel_str): + if char.isupper() and i > 0: + result.append('_') + result.append(char.lower()) + else: + result.append(char.lower()) + + return ''.join(result) + + +def convert_pascal_case(snake_str): + """ + 将下划线命名转换为帕斯卡命名(大驼峰) + + Args: + snake_str: 下划线命名字符串,如 'create_time' + + Returns: + 帕斯卡命名字符串,如 'CreateTime' + + Examples: + >>> convert_pascal_case('create_time') + 'CreateTime' + >>> convert_pascal_case('user_id') + 'UserId' + >>> convert_pascal_case('') + '' + """ + if not snake_str: + return '' + + components = snake_str.split('_') + return ''.join(_capitalize_first_letter(x) for x in components) + + +def convert_dict_keys(data, converter=convert_camel_case): + """ + 递归转换字典的所有键名 + + Args: + data: 需要转换的字典或列表 + converter: 转换函数,默认为 to_camel_case + + Returns: + 转换后的字典或列表 + + Examples: + >>> data = {'create_time': '2024-01-01', 'user_id': 1} + >>> convert_dict_keys(data) + {'createTime': '2024-01-01', 'userId': 1} + + >>> data = [{'user_name': '张三'}, {'user_name': '李四'}] + >>> convert_dict_keys(data) + [{'userName': '张三'}, {'userName': '李四'}] + """ + if isinstance(data, list): + return [convert_dict_keys(item, converter) for item in data] + + if isinstance(data, dict): + result = {} + for key, value in data.items(): + new_key = converter(key) + result[new_key] = convert_dict_keys(value, converter) + return result + + return data + + +def convert_snake_to_camel(data): + """ + 将字典中的下划线命名字段转换为驼峰命名(便捷方法) + + 这是 convert_dict_keys 的便捷封装,专门用于数据库字段转前端字段。 + + Args: + data: 需要转换的字典或列表 + + Returns: + 转换后的字典或列表(所有键名转为驼峰命名) + + Examples: + >>> data = {'create_time': '2024-01-01', 'user_id': 1} + >>> convert_dict_keys_snake_to_camel(data) + {'createTime': '2024-01-01', 'userId': 1} + + >>> data = [{'user_name': '张三'}, {'user_name': '李四'}] + >>> convert_dict_keys_snake_to_camel(data) + [{'userName': '张三'}, {'userName': '李四'}] + """ + return convert_dict_keys(data, converter=convert_camel_case) + + +def convert_camel_to_snake(data): + """ + 将字典中的驼峰命名字段转换为下划线命名(便捷方法) + + 这是 convert_dict_keys 的便捷封装,专门用于前端字段转数据库字段。 + + Args: + data: 需要转换的字典或列表 + + Returns: + 转换后的字典或列表(所有键名转为下划线命名) + + Examples: + >>> data = {'createTime': '2024-01-01', 'userId': 1} + >>> convert_dict_keys_camel_to_snake(data) + {'create_time': '2024-01-01', 'user_id': 1} + + >>> data = [{'userName': '张三'}, {'userName': '李四'}] + >>> convert_dict_keys_camel_to_snake(data) + [{'user_name': '张三'}, {'user_name': '李四'}] + """ + return convert_dict_keys(data, converter=convert_snake_case) + + +# ============================================================ +# 私有方法 +# ============================================================ + +def _capitalize_first_letter(s): + """ + 将字符串的首字母大写,其余字母小写 + + Args: + s: 输入字符串 + + Returns: + 首字母大写后的字符串 + + Examples: + >>> _capitalize_first_letter('name') + 'Name' + >>> _capitalize_first_letter('userName') + 'Username' + >>> _capitalize_first_letter('') + '' + """ + if not s: + return '' + return s[0].upper() + s[1:].lower() + + +def _is_camel_case(s): + """ + 判断字符串是否为驼峰命名格式 + + 驼峰命名特征: + 1. 首字母小写 + 2. 不包含下划线 + 3. 包含大写字母(至少一个大写字母) + + Args: + s: 输入字符串 + + Returns: + bool: 是否为驼峰命名格式 + + Examples: + >>> _is_camel_case('createTime') + True + >>> _is_camel_case('create_time') + False + >>> _is_camel_case('id') + False + """ + if not s: + return False + + # 首字母必须小写,不包含下划线,且至少有一个大写字母 + return (s[0].islower() and + '_' not in s and + any(char.isupper() for char in s)) + + +def _is_snake_case(s): + """ + 判断字符串是否为下划线命名格式 + + 下划线命名特征: + 1. 全部小写 + 2. 包含下划线 + 3. 不以_开头,不以_结尾 + + Args: + s: 输入字符串 + + Returns: + bool: 是否为下划线命名格式 + + Examples: + >>> _is_snake_case('create_time') + True + >>> _is_snake_case('createTime') + False + >>> _is_snake_case('id') + False + """ + if not s: + return False + + # 全部小写,包含下划线,且不以_开头结尾 + return (s.islower() and + '_' in s and + not s.startswith('_') and + not s.endswith('_')) + + +def _is_pascal_case(s): + """ + 判断字符串是否为帕斯卡命名格式(大驼峰) + + 帕斯卡命名特征: + 1. 首字母大写 + 2. 不包含下划线 + 3. 至少有一个字母 + + Args: + s: 输入字符串 + + Returns: + bool: 是否为帕斯卡命名格式 + + Examples: + >>> _is_pascal_case('CreateTime') + True + >>> _is_pascal_case('createTime') + False + >>> _is_pascal_case('create_time') + False + """ + if not s: + return False + + # 首字母大写,不包含下划线 + return (s[0].isupper() and + '_' not in s and + any(char.isalpha() for char in s)) + + +def _convert_single_field(field_name, converter=convert_camel_case): + """ + 转换单个字段名 + + Args: + field_name: 字段名 + converter: 转换函数,默认为 to_camel_case + + Returns: + 转换后的字段名 + + Examples: + >>> _convert_single_field('create_time') + 'createTime' + >>> _convert_single_field('createTime', convert_snake_case) + 'create_time' + """ + if not field_name: + return '' + + return converter(field_name) diff --git a/src/common/utils/file.py b/src/common/utils/file.py new file mode 100644 index 0000000..956565c --- /dev/null +++ b/src/common/utils/file.py @@ -0,0 +1,407 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 珊瑚云团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +文件目录操作工具模块 +============================================================= + +该模块提供文件和目录操作的常用工具函数。 +主要用于处理文件上传、目录创建、路径管理等场景,确保文件系统操作的 +安全性和可靠性。包含目录创建、文件操作等基础功能。 + +主要功能: +1. 递归创建目录(mkdir)- 确保目标目录存在 +2. 文件路径处理 +3. 目录存在性检查 + +所属包: utils +所在文件: utils/file_utils.py +依赖模块: os - 操作系统接口模块 +""" + +import logging +import os + +logger = logging.getLogger(__name__) + + +# 常见文件类型的魔术字节签名 +# 格式:扩展名 -> [签名列表],签名为 (offset, bytes) 元组 +MAGIC_SIGNATURES = { + # 图片 + '.jpg': [(0, b'\xff\xd8\xff')], + '.jpeg': [(0, b'\xff\xd8\xff')], + '.png': [(0, b'\x89PNG\r\n\x1a\n')], + '.gif': [(0, b'GIF87a'), (0, b'GIF89a')], + '.bmp': [(0, b'BM')], + '.webp': [(0, b'RIFF')], + '.ico': [(0, b'\x00\x00\x01\x00'), (0, b'\x00\x00\x02\x00')], + # 文档 + '.pdf': [(0, b'%PDF')], + '.zip': [(0, b'PK\x03\x04')], + '.rar': [(0, b'Rar!\x1a\x07')], + '.7z': [(0, b'7z\xbc\xaf\x27\x1c')], + '.mp3': [(0, b'\xff\xfb'), (0, b'\xff\xf3'), (0, b'\xff\xf2'), (0, b'ID3')], + '.mp4': [(4, b'ftyp')], + # SVG 是文本格式,无固定二进制签名 +} + + +def verify_file_magic(file_stream, ext_with_dot): + """ + 校验文件魔术字节,验证文件实际内容与扩展名是否一致 + + Args: + file_stream: Flask上传文件对象(FileStorage),需支持seek/read + ext_with_dot: 带点的扩展名,如 '.jpg' + + Returns: + tuple: (is_valid, message) + - is_valid (bool): 是否通过校验 + - message (str): 提示信息 + """ + signatures = MAGIC_SIGNATURES.get(ext_with_dot) + if not signatures: + # 无签名规则的文件类型,跳过魔术字节校验 + return True, '' + + try: + # 读取文件头部字节用于校验 + header = file_stream.read(32) + file_stream.seek(0) + + if not header: + return False, '文件内容为空' + + for sig in signatures: + if isinstance(sig, tuple): + offset, sig_bytes = sig + if header[offset:offset + len(sig_bytes)] == sig_bytes: + return True, '' + elif header.startswith(sig): + return True, '' + + return False, f'文件内容与扩展名 {ext_with_dot} 不匹配' + + except Exception as e: + logger.error(f'魔术字节校验异常: {e}') + # 读取异常时不阻断上传,降级为仅扩展名校验 + return True, '' + + +def mkdir(path): + """ + 递归创建目录 + ============================================================= + + 检查指定路径的目录是否存在,如果不存在则递归创建所有必需的目录。 + 用于确保文件上传等操作前目标目录已存在。 + + 参数: + path (str): 需要创建/检查的目录路径 + + 返回: + bool: True - 目录创建成功或已存在 + False - 目录创建失败(保留扩展用) + + 使用示例: + >>> # 确保上传目录存在 + >>> upload_dir = '/data/uploads/2024/01' + >>> mkdir(upload_dir) + /data/uploads/2024/01 创建成功 + True + + 注意事项: + 1. 函数会自动创建多级不存在的目录 + 2. 如果目录已存在,不会重复创建 + 3. 需要有相应的文件系统权限 + """ + + # 去除首位空格 + # 说明:消除用户输入中可能包含的前后空格,避免路径错误 + path = path.strip() + + # 去除尾部 \ 符号 + # 说明:Windows系统路径可能以\结尾,去除后保持路径格式统一 + path = path.rstrip("\\") + + # 判断路径是否存在 + # 函数:os.path.exists(path) + # 说明:检查指定路径的文件或目录是否存在 + isExists = os.path.exists(path) + + if not isExists: + # 如果不存在则创建目录 + # 函数:os.makedirs(path) + # 说明:递归创建目录,如果中间目录不存在也会一并创建 + # 例如:创建 /a/b/c 时,如果 /a 和 /a/b 都不存在,会一并创建 + os.makedirs(path) + logger.debug(f'{path} 创建成功') + return True + else: + logger.debug(f'{path} 目录已存在') + return False + + +# ======================= 扩展:常用文件操作工具类 ========================== + +class FileUtils: + """ + 文件操作工具类 + ============================================================= + + 提供更多常用的文件和目录操作功能,封装为类方法便于调用。 + 包含文件读写、路径处理、文件信息获取等实用功能。 + + 使用示例: + >>> # 获取文件大小 + >>> size = FileUtils.get_file_size('/path/file.txt') + >>> print(f"{size} bytes") + + >>> # 安全删除文件 + >>> if FileUtils.safe_delete('/path/file.txt'): + ... print("文件删除成功") + """ + + @staticmethod + def get_file_size(file_path): + """ + 获取文件大小 + ============================================================= + + 参数: + file_path (str): 文件路径 + + 返回: + int: 文件大小(字节),如果文件不存在返回0 + """ + if os.path.exists(file_path) and os.path.isfile(file_path): + return os.path.getsize(file_path) + return 0 + + @staticmethod + def get_file_extension(filename): + """ + 获取文件扩展名 + ============================================================= + + 参数: + filename (str): 文件名或文件路径 + + 返回: + str: 文件扩展名(小写),例如:'.jpg', '.txt' + """ + import os + return os.path.splitext(filename)[1].lower() + + @staticmethod + def get_file_name_without_ext(filename): + """ + 获取不带扩展名的文件名 + ============================================================= + + 参数: + filename (str): 文件名或文件路径 + + 返回: + str: 不带扩展名的文件名 + """ + import os + return os.path.splitext(os.path.basename(filename))[0] + + @staticmethod + def safe_delete(file_path): + """ + 安全删除文件 + ============================================================= + + 参数: + file_path (str): 文件路径 + + 返回: + bool: True - 删除成功或文件不存在 + False - 删除失败(如权限不足) + """ + try: + if os.path.exists(file_path) and os.path.isfile(file_path): + os.remove(file_path) + return True + except Exception as e: + logger.warning(f"文件删除失败: {e}") + return False + + @staticmethod + def read_file(file_path, encoding='utf-8'): + """ + 读取文件内容 + ============================================================= + + 参数: + file_path (str): 文件路径 + encoding (str): 文件编码,默认utf-8 + + 返回: + str/None: 文件内容字符串,读取失败返回None + """ + try: + with open(file_path, 'r', encoding=encoding) as f: + return f.read() + except Exception as e: + logger.warning(f"文件读取失败: {e}") + return None + + @staticmethod + def write_file(file_path, content, encoding='utf-8', mode='w'): + """ + 写入文件内容 + ============================================================= + + 参数: + file_path (str): 文件路径 + content (str): 要写入的内容 + encoding (str): 文件编码,默认utf-8 + mode (str): 写入模式,'w'覆盖写入,'a'追加写入 + + 返回: + bool: True - 写入成功,False - 写入失败 + """ + try: + # 确保目录存在 + directory = os.path.dirname(file_path) + if directory and not os.path.exists(directory): + os.makedirs(directory) + + with open(file_path, mode, encoding=encoding) as f: + f.write(content) + return True + except Exception as e: + logger.warning(f"文件写入失败: {e}") + return False + + @staticmethod + def list_files(directory, pattern=None): + """ + 列出目录下的所有文件 + ============================================================= + + 参数: + directory (str): 目录路径 + pattern (str): 文件匹配模式,如 '*.txt',None表示所有文件 + + 返回: + list: 文件名列表 + """ + import glob + + if not os.path.exists(directory): + return [] + + if pattern: + search_path = os.path.join(directory, pattern) + return [os.path.basename(f) for f in glob.glob(search_path)] + else: + return [f for f in os.listdir(directory) + if os.path.isfile(os.path.join(directory, f))] + + @staticmethod + def get_file_info(file_path): + """ + 获取文件详细信息 + ============================================================= + + 参数: + file_path (str): 文件路径 + + 返回: + dict: 包含文件信息的字典,文件不存在返回None + """ + if not os.path.exists(file_path) or not os.path.isfile(file_path): + return None + + stat = os.stat(file_path) + return { + 'name': os.path.basename(file_path), + 'path': os.path.abspath(file_path), + 'size': stat.st_size, + 'size_format': FileUtils._format_size(stat.st_size), + 'extension': FileUtils.get_file_extension(file_path), + 'created_time': stat.st_ctime, + 'modified_time': stat.st_mtime, + 'is_hidden': os.path.basename(file_path).startswith('.') + } + + @staticmethod + def _format_size(size_bytes): + """ + 格式化文件大小显示 + ============================================================= + + 参数: + size_bytes (int): 文件大小(字节) + + 返回: + str: 格式化后的大小字符串,如 '1.5 MB' + """ + if size_bytes < 1024: + return f"{size_bytes} B" + elif size_bytes < 1024 * 1024: + return f"{size_bytes / 1024:.2f} KB" + elif size_bytes < 1024 * 1024 * 1024: + return f"{size_bytes / (1024 * 1024):.2f} MB" + else: + return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB" + + +# ======================= 便捷函数封装 ========================== + +def get_file_size(file_path): + """获取文件大小(字节)的便捷函数""" + return FileUtils.get_file_size(file_path) + + +def safe_delete(file_path): + """安全删除文件的便捷函数""" + return FileUtils.safe_delete(file_path) + + +def get_file_extension(filename): + """获取文件扩展名的便捷函数""" + return FileUtils.get_file_extension(filename) diff --git a/src/common/utils/formatter.py b/src/common/utils/formatter.py new file mode 100644 index 0000000..3c24e21 --- /dev/null +++ b/src/common/utils/formatter.py @@ -0,0 +1,107 @@ +""" +日志格式化工具模块 +============================================================= + +提供请求参数和返回结果的格式化工具,用于操作日志存储: +- format_params: 格式化请求参数(过滤敏感字段) +- format_result: 格式化返回结果 +- truncate_json: 截断过长的JSON字符串 +""" + +import json +import logging + +logger = logging.getLogger(__name__) + + +def format_params(params) -> str: + """ + 格式化请求参数,用于日志存储 + + 将请求参数字典转换为JSON字符串,并过滤敏感信息。 + + Args: + params: 请求参数(字典或其他类型) + + Returns: + 格式化后的JSON字符串 + """ + 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] = '******' + return json.dumps(params_copy, ensure_ascii=False) + return json.dumps(params, ensure_ascii=False) + except Exception: + return str(params)[:500] + + +def format_result(result): + """ + 格式化返回结果 + + 将返回结果转换为适合存储的字符串格式,支持JsonResponse、字典等类型。 + + Args: + result: 返回结果 + + Returns: + 格式化后的结果字符串 + """ + if not result: + return '' + + try: + if hasattr(result, 'content') and result.content: + content = result.content.decode('utf-8') + if content: + try: + data = json.loads(content) + result_str = json.dumps(data, ensure_ascii=False) + except Exception: + 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: + 截断后的字符串 + """ + if not json_str: + return '' + if len(json_str) <= max_len: + return json_str + return json_str[:max_len] + '...' diff --git a/src/common/utils/ip.py b/src/common/utils/ip.py new file mode 100644 index 0000000..039ee66 --- /dev/null +++ b/src/common/utils/ip.py @@ -0,0 +1,55 @@ +""" +IP地址工具模块 +============================================================= + +提供客户端IP获取和地理位置查询: +- get_client_ip: 获取客户端真实IP(需配合Flask ProxyFix使用) +- get_ip_location: 调用第三方API查询IP地理位置 +""" + +import json +import logging +import urllib.request + +logger = logging.getLogger(__name__) + + +def get_client_ip(): + """ + 获取客户端真实IP地址 + + 通过Flask ProxyFix中间件由PROXY_NUM配置自动设置 + request.remote_addr为真实客户端IP。未配置ProxyFix时, + request.remote_addr即为直连IP,避免客户端伪造。 + + Returns: + str: 客户端IP地址 + """ + from flask import request + + return request.remote_addr + + +def get_ip_location(ip: str) -> str: + """ + 根据IP获取地理位置(使用第三方API) + + Args: + ip: IP地址 + + Returns: + 地理位置信息,格式:"国家 省份 城市" + """ + if ip in ['127.0.0.1', 'localhost'] or ip.startswith('192.168.') or ip.startswith('10.'): + return "内网IP" + + try: + 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 "未知" diff --git a/src/common/utils/ip2region.py b/src/common/utils/ip2region.py new file mode 100644 index 0000000..9aaa70f --- /dev/null +++ b/src/common/utils/ip2region.py @@ -0,0 +1,179 @@ +""" +IP地理位置查询模块(基于ip2region离线数据库) +============================================================= + +基于 ip2region.xdb 离线数据库提供高性能IP地理位置查询,无需网络请求。 + +核心功能: +- get_ip_region: 根据IP获取完整区域信息字典 +- get_ip_location: 根据IP获取地理位置文本(兼容现有调用) +- get_ip_country / get_ip_province / get_ip_city / get_ip_isp: 按层级获取 + +xdb文件位于 src/common/utils/ip2region.xdb,启动时全量加载到内存,查询零IO延迟。 +""" + +import logging +import os + +from ip2region import searcher, util + +logger = logging.getLogger(__name__) + +# xdb数据库文件路径(与本模块同目录) +_XDB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ip2region.xdb") + +# 模块级单例:启动时加载xdb到内存,后续查询零IO +_searcher = None + + +def _get_searcher(): + """获取搜索器单例(懒加载,首次调用时初始化)""" + global _searcher + if _searcher is not None: + return _searcher + + if not os.path.exists(_XDB_PATH): + logger.error(f"ip2region数据库文件不存在: {_XDB_PATH}") + return None + + try: + c_buff = util.load_content_from_file(_XDB_PATH) + _searcher = searcher.new_with_buffer(util.IPv4, c_buff) + logger.info(f"ip2region数据库加载成功: {_XDB_PATH}") + except Exception as e: + logger.error(f"ip2region数据库加载失败: {e}") + return None + + return _searcher + + +def _parse_region(region_str): + """ + 解析ip2region原始结果字符串 + + 原始格式: "国家|省份|城市|ISP|国家代码" + 示例: "中国|浙江省|杭州市|阿里|CN", "United States|California|0|Google LLC|US" + + Args: + region_str: ip2region返回的原始字符串 + + Returns: + dict: 各层级区域信息 + """ + parts = region_str.split("|") if region_str else [] + # 补齐到5段 + while len(parts) < 5: + parts.append("0") + + country = parts[0] + province = parts[1] + city = parts[2] + isp = parts[3] + country_code = parts[4] + + # 清理占位符 "0" → 空字符串 + province = "" if province == "0" else province + city = "" if city == "0" else city + isp = "" if isp == "0" else isp + country_code = "" if country_code == "0" else country_code + + return { + "country": country, + "province": province, + "city": city, + "isp": isp, + "country_code": country_code, + } + + +def _is_private_ip(ip): + """判断是否为内网/保留IP""" + if not ip: + return True + ip = ip.strip() + if ip in ("127.0.0.1", "localhost", "0.0.0.0"): + return True + if ip.startswith("10.") or ip.startswith("192.168."): + return True + if ip.startswith("172."): + parts = ip.split(".") + if len(parts) >= 2: + second = int(parts[1]) + if 16 <= second <= 31: + return True + return False + + +def get_ip_region(ip): + """ + 根据IP获取完整区域信息字典 + + Args: + ip: IP地址字符串 + + Returns: + dict: 区域信息,包含 country/province/city/isp/country_code 字段。 + 内网IP返回 {"country": "内网IP", ...},查询失败返回 {"country": "未知", ...} + """ + if _is_private_ip(ip): + return {"country": "内网IP", "province": "", "city": "", "isp": "", "country_code": ""} + + s = _get_searcher() + if s is None: + return {"country": "未知", "province": "", "city": "", "isp": "", "country_code": ""} + + try: + region_str = s.search(ip) + if not region_str: + return {"country": "未知", "province": "", "city": "", "isp": "", "country_code": ""} + return _parse_region(region_str) + except Exception as e: + logger.warning(f"IP地理位置查询失败 [{ip}]: {e}") + return {"country": "未知", "province": "", "city": "", "isp": "", "country_code": ""} + + +def get_ip_location(ip): + """ + 根据IP获取地理位置文本描述 + + 格式: "国家 省份 城市"(与原 utils/ip.py 的 get_ip_location 返回格式兼容) + 内网IP返回 "内网IP",查询失败返回 "未知" + + Args: + ip: IP地址字符串 + + Returns: + str: 地理位置文本 + """ + region = get_ip_region(ip) + + if region["country"] in ("内网IP", "未知"): + return region["country"] + + parts = [region["country"]] + if region["province"]: + parts.append(region["province"]) + if region["city"] and region["city"] != region["province"]: + parts.append(region["city"]) + + return " ".join(parts) + + +def get_ip_country(ip): + """根据IP获取国家""" + return get_ip_region(ip)["country"] + + +def get_ip_province(ip): + """根据IP获取省份""" + return get_ip_region(ip)["province"] + + +def get_ip_city(ip): + """根据IP获取城市""" + return get_ip_region(ip)["city"] + + +def get_ip_isp(ip): + """根据IP获取运营商""" + return get_ip_region(ip)["isp"] diff --git a/src/common/utils/ip2region.xdb b/src/common/utils/ip2region.xdb new file mode 100644 index 0000000..6b53b1f Binary files /dev/null and b/src/common/utils/ip2region.xdb differ diff --git a/src/common/utils/json.py b/src/common/utils/json.py new file mode 100644 index 0000000..4a542dd --- /dev/null +++ b/src/common/utils/json.py @@ -0,0 +1,91 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +JSON 序列化模块 +自定义 JSON 编码器,处理日期时间等特殊类型的序列化 +""" + +from datetime import date, datetime +from flask.json.provider import DefaultJSONProvider + + +class JSONEncoder(DefaultJSONProvider): + """ + 自定义 JSON 编码器 + + 功能说明: + 扩展 Flask 默认的 JSON 序列化器,支持以下类型的格式化输出: + 1. datetime 类型 → 'YYYY-MM-DD HH:MM:SS' 格式 + 2. date 类型 → 'YYYY-MM-DD' 格式 + + 使用方式: + app.json = JSONEncoder(app) + """ + + def default(self, o): + """ + 处理无法被默认 JSON 序列化的对象 + + Args: + o: 需要序列化的对象 + + Returns: + 序列化后的值(字符串或其他可序列化类型) + """ + # ============================================================ + # 处理日期时间类型(datetime) + # ============================================================ + # 将 datetime 对象格式化为:2024-01-15 14:30:00 + if isinstance(o, datetime): + return o.strftime('%Y-%m-%d %H:%M:%S') + + # ============================================================ + # 处理日期类型(date) + # ============================================================ + # 将 date 对象格式化为:2024-01-15 + if isinstance(o, date): + return o.strftime('%Y-%m-%d') + + # ============================================================ + # 其他类型交给父类处理 + # ============================================================ + return super().default(o) diff --git a/src/common/utils/model_utils.py b/src/common/utils/model_utils.py new file mode 100644 index 0000000..53bd2ef --- /dev/null +++ b/src/common/utils/model_utils.py @@ -0,0 +1,90 @@ +""" +模型工具模块 +============================================================= + +提供SQLAlchemy模型对象的序列化工具: +- model_to_dict: 将单个模型对象转换为字典 +- models_to_list: 将模型对象列表转换为字典列表 +- format_datetime: 格式化datetime对象 +""" + +import logging +from datetime import datetime + +from src.common.constants.common import SERIALIZE_EXCLUDE_FIELDS +from src.common.utils.case_converter import convert_camel_case + +logger = logging.getLogger(__name__) + + +def format_datetime(dt, format_str='%Y-%m-%d %H:%M:%S'): + """ + 格式化日期时间 + + Args: + dt: datetime对象 + format_str: 日期时间格式字符串,默认为 '%Y-%m-%d %H:%M:%S' + + Returns: + 格式化后的日期字符串,如果输入为None则返回None + """ + return dt.strftime(format_str) if dt else None + + +def model_to_dict(model, exclude=None, convert_case=True): + """ + 将SQLAlchemy模型对象转换为字典 + + Args: + model: SQLAlchemy模型对象 + exclude: 需要排除的字段列表,如 ['id', 'is_delete'](使用下划线命名) + convert_case: 是否将字段名转换为驼峰命名,默认True + + Returns: + 转换后的字典对象 + """ + if not model: + return None + + result = {} + for column in model.__table__.columns: + field_name = column.name + value = getattr(model, field_name) + + if exclude and field_name in exclude: + continue + + if field_name in SERIALIZE_EXCLUDE_FIELDS: + continue + + # 所有 datetime 类型统一格式化 + if isinstance(value, datetime): + value = value.strftime('%Y-%m-%d %H:%M:%S') if value else None + + if convert_case: + field_name = convert_camel_case(field_name) + + result[field_name] = value + + return result + + +def models_to_list(models, exclude=None, convert_case=True): + """ + 将SQLAlchemy模型对象列表转换为字典列表 + + Args: + models: SQLAlchemy模型对象列表 + exclude: 需要排除的字段列表 + convert_case: 是否将字段名转换为驼峰命名 + + Returns: + 字典列表 + """ + if not models: + return [] + + return [ + model_to_dict(model, exclude, convert_case) + for model in models + ] diff --git a/src/common/utils/regular.py b/src/common/utils/regular.py new file mode 100644 index 0000000..e67bd1b --- /dev/null +++ b/src/common/utils/regular.py @@ -0,0 +1,80 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 珊瑚云团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +表单错误处理模块 +============================================================= + +提供表单验证错误信息的提取和格式化功能。 +""" + + +def get_err(form): + """ + 获取表单验证的错误信息 + + 遍历表单的所有错误字段,提取第一条错误信息,并用斜杠拼接。 + + Args: + form: WTForms 表单对象 + + Returns: + str: 拼接后的错误信息字符串,多个错误用 '/' 分隔 + + Examples: + >>> from application.forms.level import LevelForm + >>> form = LevelForm(request.form) + >>> if not form.validate(): + ... err_msg = get_err(form) + ... return R.failed(msg=err_msg) + + # 错误信息示例 + # 单条错误: "职级名称不能为空" + # 多条错误: "职级名称不能为空/职级排序必须为数字" + """ + if not form.errors: + return "" + + # 提取每个字段的第一条错误信息 + error_messages = [errors[0] for errors in form.errors.values()] + + # 用斜杠拼接所有错误信息 + return "/".join(error_messages) diff --git a/src/common/utils/request.py b/src/common/utils/request.py new file mode 100644 index 0000000..132b3c9 --- /dev/null +++ b/src/common/utils/request.py @@ -0,0 +1,196 @@ +""" +请求解析工具模块 +============================================================= + +提供请求数据解析相关的工具函数: +- get_json_data: 从Flask请求中解析JSON数据 +- require_json: 装饰器,要求请求必须包含JSON数据 +- parse_request_body: 解析请求体JSON数据,返回(data, error)元组 +- parse_id_list: 将逗号分隔的ID字符串解析为整数列表 +- parse_batch_ids: 从请求体解析批量删除ID数组,校验并转为逗号分隔字符串 +- like_escape: 转义SQL LIKE通配符,防止用户输入被当作通配符 +- like_contains: 构建安全的LIKE包含查询值 +""" + +import logging +from functools import wraps + +logger = logging.getLogger(__name__) + + +def get_json_data(required=True, silent=False): + """ + 获取请求中的JSON数据 + + Args: + required: 是否必须获取到JSON数据,默认为True + silent: 是否静默处理错误,默认为False(False时会检查Content-Type) + + Returns: + dict: JSON数据,如果获取失败则返回None或空字典 + """ + from flask import request + + if not silent and not request.is_json: + return None + + try: + json_data = request.get_json(silent=silent) + except Exception: + return None + + if required and not json_data: + return None + + return json_data or {} + + +def require_json(f): + """ + 装饰器:要求请求必须包含JSON数据 + + Examples: + @require_json + def login(): + data = get_json_data() + # 业务逻辑... + """ + + @wraps(f) + def decorated_function(*args, **kwargs): + from flask import request + + if not request.is_json: + from src.core import response as R + return R.failed(msg="参数格式不正确,请使用JSON格式") + + json_data = request.get_json() + if not json_data: + from src.core import response as R + return R.failed(msg="参数不能为空") + + request.parsed_json = json_data + return f(*args, **kwargs) + + return decorated_function + + +def parse_request_body(): + """ + 解析请求体JSON数据 + + Returns: + tuple: (data, error) + 成功时 data 为解析后的字典,error 为 None + 失败时 data 为 None,error 为错误描述字符串 + """ + try: + data = get_json_data(required=False, silent=True) + + if data is None or data == {}: + return None, "参数不能为空" + + return data, None + + except Exception as e: + logger.error(f"解析请求体异常: {str(e)}") + return None, "参数处理失败" + + +def parse_id_list(id_str: str, separator: str = ',') -> list: + """ + 将逗号分隔的ID字符串解析为整数列表 + + Args: + id_str: 逗号分隔的ID字符串,如 "1,2,3" + separator: 分隔符,默认为逗号 + + Returns: + list: 有效的整数ID列表 + + Example: + >>> parse_id_list("1,2,3") + [1, 2, 3] + >>> parse_id_list("1,2,abc,3") + [1, 2, 3] + """ + if not id_str: + return [] + result = [] + for item in id_str.split(separator): + item = item.strip() + if item.isdigit(): + result.append(int(item)) + return result + + +def parse_batch_ids(): + """ + 从请求体解析批量删除的ID数组,校验并转为逗号分隔字符串 + + 请求体格式: [1, 2, 3] + + Returns: + tuple: (id_str, error_response) + 成功时 id_str 为 "1,2,3",error_response 为 None + 失败时 id_str 为 None,error_response 为 R.failed 响应 + """ + from flask import request + from src.core import response as R + + ids = request.get_json() + if not ids or not isinstance(ids, list) or len(ids) == 0: + return None, R.failed("参数错误:ids必须为非空数组") + try: + id_list = [int(id_val) for id_val in ids] + except (ValueError, TypeError): + return None, R.failed("参数错误:数组元素必须为数字") + + return ','.join(str(id_val) for id_val in id_list), None + + +def like_escape(value, escape_char='\\'): + """ + 转义SQL LIKE通配符 + + 将用户输入中的 %、_ 和转义字符本身进行转义, + 防止用户输入被当作LIKE通配符导致非预期查询结果。 + + Args: + value: 原始输入值 + escape_char: 转义字符,默认为反斜杠 + + Returns: + str: 转义后的安全字符串 + + Example: + >>> like_escape('100%') + '100\\%' + >>> like_escape('test_data') + 'test\\_data' + """ + escaped = str(value).replace(escape_char, escape_char * 2) + escaped = escaped.replace('%', escape_char + '%') + escaped = escaped.replace('_', escape_char + '_') + return escaped + + +def like_contains(value, escape_char='\\'): + """ + 构建安全的LIKE包含查询值(前后加%) + + 对用户输入进行通配符转义后,前后添加 % 构建模糊查询值。 + 配合 SQLAlchemy 的 .like() 方法和 escape 参数使用。 + + Args: + value: 原始输入值 + escape_char: 转义字符,默认为反斜杠 + + Returns: + str: 构建好的LIKE查询值,如 '%safe_value%' + + Example: + >>> # SQLAlchemy 用法 + >>> query = query.filter(User.name.like(like_contains(name), escape='\\')) + """ + return f'%{like_escape(value, escape_char)}%' diff --git a/src/common/utils/rich_text.py b/src/common/utils/rich_text.py new file mode 100644 index 0000000..f1a37c6 --- /dev/null +++ b/src/common/utils/rich_text.py @@ -0,0 +1,98 @@ +""" +富文本与文件处理模块 +============================================================= + +提供文件URL处理、临时文件迁移和富文本编辑器内容处理: +- get_file_url: 拼接文件域名生成完整URL +- save_file: 将临时目录文件迁移到正式存储目录 +- save_content: 处理富文本HTML内容,提取并保存其中的图片文件 +""" + +import logging +import os +import re +import time +import html + +from config import FLASK_FILE_URL, FLASK_UPLOAD_DIR, FLASK_TEMP_PATH +from src.common.utils.file import mkdir + +logger = logging.getLogger(__name__) + + +def get_file_url(path): + """ + 获取完整的文件访问URL + + Args: + path: 文件的相对路径,可以是空字符串、None或有效路径 + + Returns: + 完整的文件访问URL,如果path为空则返回None + """ + if not path: + return None + return FLASK_FILE_URL + path + + +def save_file(url, directory): + """ + 保存文件到指定目录 + + 处理本站临时文件迁移:将临时目录中的文件移动到正式存储目录。 + + Args: + url: 文件的完整URL或相对路径 + directory: 目标子目录名称,用于分类存储 + + Returns: + 文件的相对路径,失败时返回空字符串 + """ + if not url: + return "文件地址不能为空" + + if url.find(FLASK_FILE_URL) != -1: + if url.find("/temp") != -1: + path = FLASK_UPLOAD_DIR + "/" + directory + "/" + time.strftime('%Y%m%d') + mkdir(path) + + old_path = url.replace(FLASK_FILE_URL, FLASK_UPLOAD_DIR) + new_path = FLASK_UPLOAD_DIR + "/" + directory + old_path.replace(FLASK_TEMP_PATH, "") + + os.rename(old_path, new_path) + return new_path.replace(FLASK_UPLOAD_DIR, "") + else: + return url.replace(FLASK_FILE_URL, "") + else: + logger.warning(f"远程文件暂不支持保存: {url}") + + return "" + + +def save_content(content, title, directory): + """ + 保存富文本编辑器内容 + + 处理富文本HTML内容,提取其中的图片文件并保存, + 同时将文件alt属性设置为文章标题(已做HTML转义防XSS)。 + + Args: + content: 富文本HTML内容 + title: 文章标题,用于设置文件alt属性 + directory: 文件存储的子目录名称 + + Returns: + 处理后的富文本内容,文件地址已被替换 + """ + image_urls = re.findall('img src="(.*?)"', content, re.S) + + for url in image_urls: + image = save_file(url, directory) + if image: + content = content.replace(url, "[IMG_URL]" + image) + + title = html.escape(str(title) if title else "", quote=True) + if "alt=\"\"" in content and title: + content = content.replace("alt=\"\"", "alt=\"" + title + "\"") + + return content diff --git a/src/common/utils/user_agent.py b/src/common/utils/user_agent.py new file mode 100644 index 0000000..500522b --- /dev/null +++ b/src/common/utils/user_agent.py @@ -0,0 +1,75 @@ +""" +User-Agent解析工具模块 +============================================================= + +提供HTTP请求User-Agent的解析功能: +- parse_user_agent: 解析操作系统和浏览器信息 +""" + +import re + + +def parse_user_agent(): + """ + 解析User-Agent,获取操作系统和浏览器信息 + + 通过正则匹配User-Agent字符串,识别客户端操作系统和浏览器类型。 + 支持 Windows、macOS、Linux、Android、iOS 及主流浏览器识别。 + + Returns: + dict: 包含os和browser的字典 + { + 'os': 'Windows 10', # 操作系统信息 + 'browser': 'Chrome' # 浏览器名称 + } + 无法识别时返回 '未知' + """ + from flask import request + + user_agent = request.headers.get('User-Agent', '') + + 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'), # Edge 浏览器(基于Chromium) + (r'chrome', 'Chrome'), # Chrome 浏览器 + (r'safari', 'Safari'), # Safari 浏览器 + (r'firefox', 'Firefox'), # Firefox 浏览器 + (r'msie', 'IE'), # Internet Explorer + (r'trident', 'IE'), # IE 11(Trident内核) + ] + + for pattern, browser_name in browser_patterns: + if re.search(pattern, ua_lower): + result['browser'] = browser_name + break + + return result diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..f6d22d3 --- /dev/null +++ b/src/core/__init__.py @@ -0,0 +1,14 @@ +""" +core 基础设施层 +============================================================= + +提供与应用业务无关的基础设施: + - database.py SQLAlchemy 实例 + - response.py 统一响应 R + - jwt.py / password.py / security.py 认证与安全 + - redis_client.py Redis 客户端封装 + - mail.py / scheduler.py 扩展实例 + - exceptions.py / middleware.py / logger.py / context.py 后续阶段建设 + +依赖约束:core 层不得 import modules 层。 +""" diff --git a/src/core/database.py b/src/core/database.py new file mode 100644 index 0000000..ab058dc --- /dev/null +++ b/src/core/database.py @@ -0,0 +1,87 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +SQLAlchemy 数据库扩展模块 +创建数据库实例,供整个应用使用 +""" + +from flask_sqlalchemy import SQLAlchemy + +# ============================================================ +# 创建 SQLAlchemy 数据库实例 +# ============================================================ +# 该实例需要在应用工厂中通过 db.init_app(app) 完成初始化 +# 使用延迟绑定模式,支持循环导入 +# ============================================================ + +db = SQLAlchemy() + + +def commit_independently(obj): + """ + 使用独立session提交对象,不受请求事务影响 + + 日志等审计数据无论业务成功或失败都应持久化, + 因此需要绕过 AOP 事务拦截器的 commit/rollback 机制。 + + 实现方式:基于现有引擎创建独立 session,将对象添加并提交, + 完成后关闭 session,不影响请求主 session。 + + Args: + obj: SQLAlchemy 模型实例(未添加到任何 session) + + Raises: + Exception: 数据库操作异常 + """ + from sqlalchemy.orm import sessionmaker + + # 复用 Flask-SQLAlchemy 已创建的引擎,避免额外连接池 + independent_session = sessionmaker(bind=db.engine)() + + try: + independent_session.add(obj) + independent_session.commit() + except Exception: + independent_session.rollback() + raise + finally: + independent_session.close() diff --git a/src/core/jwt.py b/src/core/jwt.py new file mode 100644 index 0000000..f58f064 --- /dev/null +++ b/src/core/jwt.py @@ -0,0 +1,634 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 珊瑚云团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +JWT令牌处理模块 +============================================================= + +该模块提供基于JWT(JSON Web Token)的令牌生成和验证功能。 +主要用于用户登录认证,生成访问令牌,并在后续请求中验证令牌的有效性。 +支持令牌过期机制,确保系统的安全性。 + +主要功能: +1. JWT令牌生成(create_token)- 用于用户登录成功后生成访问令牌 +2. JWT令牌解析验证(parse_payload)- 验证请求中的令牌有效性 +3. 统一的异常处理,返回标准格式的验证结果 +4. 可配置的令牌过期时间 + +所属包: utils +所在文件: utils/jwts.py +依赖模块: jwt - JWT处理第三方库 + datetime - 时间处理模块 + os - 环境变量读取模块 + secrets - 安全随机数生成模块 +""" + +import datetime +import hashlib +import logging + +import jwt +from flask import request +from jwt import exceptions + +from config import JWT_SALT, JWT_EXPIRE_MINUTES + +# 配置日志记录器 +logger = logging.getLogger(__name__) + + +# ============================================================= +# JWT密钥配置 +# ============================================================= +# 从 config.auth 统一读取,密钥配置在 .env 文件中管理 + +def _validate_key_strength(key: str) -> None: + """ + 启动时校验 JWT 密钥强度 + + 校验规则: + 1. 密钥不能为空 + 2. 密钥长度至少 32 字节(256 位) + + 校验失败直接抛出 RuntimeError,拒绝启动应用。 + """ + if not key: + raise RuntimeError( + "JWT_SALT 未配置!请在 .env 中设置至少32字节的强随机密钥。\n" + "生成方式:python -c \"import secrets; print(secrets.token_urlsafe(48))\"" + ) + + key_bytes = len(key.encode('utf-8')) + if key_bytes < 32: + raise RuntimeError( + f"JWT_SALT 长度不足:{key_bytes} 字节,至少需要 32 字节(256位)。\n" + "生成方式:python -c \"import secrets; print(secrets.token_urlsafe(48))\"" + ) + + logger.info(f"JWT密钥校验通过,长度:{key_bytes}字节") + + +# 启动时校验密钥强度 +_validate_key_strength(JWT_SALT) + +# JWT算法配置 +JWT_ALGORITHM = 'HS256' + +# 默认令牌过期时间(分钟),从配置读取 +DEFAULT_TIMEOUT_MINUTES = JWT_EXPIRE_MINUTES + + +# ============================================================= +# JWT核心功能函数 +# ============================================================= + +def create_token(payload, timeout=None): + """ + 生成JWT令牌 + ============================================================= + + 根据传入的载荷数据生成JWT访问令牌,自动添加过期时间。 + 使用强密钥确保令牌安全性,符合RFC 7518安全规范。 + + 参数: + payload (dict): JWT载荷数据,通常包含用户ID、用户名等信息 + timeout (int): 令牌过期时间(分钟),默认使用DEFAULT_TIMEOUT_MINUTES + + 返回: + str: 生成的JWT令牌字符串 + + 使用示例: + >>> payload = {'userId': 1001, 'username': 'admin'} + >>> token = create_token(payload, timeout=30) + >>> print(token) + eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... + + 注意事项: + 1. 密钥长度必须至少32字节,否则会发出警告 + 2. 生产环境建议通过环境变量设置JWT_SALT + 3. 令牌过期后需要重新登录获取新令牌 + """ + try: + # 设置过期时间 + # 说明:在载荷中添加exp字段,指定令牌的过期时间点 + # 计算方式:当前UTC时间 + 指定的分钟数 + if timeout is None: + timeout = DEFAULT_TIMEOUT_MINUTES + + # 创建payload副本,避免修改原始数据 + payload_copy = payload.copy() + + # 添加过期时间(exp)和签发时间(iat) + # exp: 过期时间,超过此时间令牌无效 + # iat: 签发时间,用于令牌生命周期计算 + current_time = datetime.datetime.now(tz=datetime.timezone.utc) + payload_copy['exp'] = current_time + datetime.timedelta(minutes=timeout) + payload_copy['iat'] = current_time + + # 声明类型和加密算法 + # 说明:定义JWT头部信息,包含令牌类型和加密算法 + headers = { + "typ": "JWT", # 令牌类型 + "alg": JWT_ALGORITHM # 加密算法 + } + + # 生成JWT令牌 + # 函数:jwt.encode() + # 参数说明: + # payload: JWT载荷数据 + # key: 加密密钥(盐) + # algorithm: 加密算法(HS256) + # headers: JWT头部信息 + token = jwt.encode( + payload=payload_copy, + key=JWT_SALT, + algorithm=JWT_ALGORITHM, + headers=headers + ) + + # 记录日志(可选,生产环境建议使用DEBUG级别) + logger.debug(f"生成JWT令牌成功,过期时间:{timeout}分钟") + + return token + + except Exception as e: + # 捕获并记录生成令牌时的异常 + logger.error(f"生成JWT令牌失败: {str(e)}") + raise + + +def parse_payload(token): + """ + 解析验证JWT令牌 + ============================================================= + + 验证传入的JWT令牌,检查签名有效性和过期状态。 + 统一处理各种异常情况,返回标准格式的结果对象。 + + 参数: + token (str): 需要验证的JWT令牌字符串 + + 返回: + dict: 验证结果,包含以下字段: + code (int): 状态码,0-成功,-1-失败 + data (dict/None): 验证成功时返回载荷数据,失败时为None + msg (str): 提示信息 + + 使用示例: + >>> result = parse_payload(token) + >>> if result['code'] == 0: + ... user_id = result['data']['userId'] + ... username = result['data']['username'] + ... else: + ... error_msg = result['msg'] + ... # 处理令牌失效,跳转登录页 + + 异常处理说明: + - ExpiredSignatureError: 令牌已过期,需要重新登录 + - DecodeError: 解码失败,令牌格式错误或签名无效 + - InvalidTokenError: 其他JWT相关异常 + """ + # 初始化返回结果 + # 说明:定义统一返回格式,默认成功状态 + result = { + "code": 0, + "data": None, + "msg": "操作成功" + } + + # 参数校验 + if not token: + result['code'] = -1 + result['msg'] = "token不能为空" + return result + + try: + # 解密验证JWT令牌 + # 函数:jwt.decode() + # 参数说明: + # jwt: 令牌字符串 + # key: 解密密钥(必须与加密时使用的密钥一致) + # algorithms: 允许的算法列表 + # + # 说明:jwt.decode会自动完成以下验证: + # 1. 签名验证 - 确保令牌未被篡改 + # 2. 过期时间验证 - 检查exp字段是否大于当前时间 + # 3. 算法验证 - 确保使用正确的加密算法 + verified_payload = jwt.decode( + token, + JWT_SALT, + algorithms=[JWT_ALGORITHM] + ) + + # 验证成功,将载荷数据存入返回结果 + # 注意:返回的数据中不包含敏感信息(如密码等) + result['data'] = verified_payload + + # 可选:记录验证成功的日志(生产环境建议使用DEBUG级别) + logger.debug(f"JWT令牌验证成功,用户: {verified_payload.get('username', 'unknown')}") + + except exceptions.ExpiredSignatureError as e: + # 令牌已过期异常处理 + # 说明:令牌超过设置的exp时间,需要重新登录获取新令牌 + result['code'] = -1 + result['msg'] = "token已失效,请重新登录" + logger.info(f"JWT令牌已过期: {str(e)}") + + except exceptions.DecodeError as e: + # 令牌解码失败异常处理 + # 说明:令牌格式错误或签名验证失败,可能被篡改 + result['code'] = -1 + result['msg'] = "token认证失败,无效的令牌格式" + logger.warning(f"JWT令牌解码失败: {str(e)}") + + except exceptions.InvalidTokenError as e: + # 无效令牌异常处理 + # 说明:捕获其他所有JWT相关异常,如算法不匹配等 + result['code'] = -1 + result['msg'] = "非法的token,请检查令牌有效性" + logger.warning(f"无效的JWT令牌: {str(e)}") + + except Exception as e: + # 其他未预期的异常处理 + result['code'] = -1 + result['msg'] = "token验证失败,系统异常" + logger.error(f"JWT验证系统异常: {str(e)}") + + return result + + +def refresh_token(token, extend_minutes=None): + """ + 刷新JWT令牌 + ============================================================= + + 验证现有令牌有效性,如果有效则生成新的令牌,延长有效期。 + + 参数: + token (str): 需要刷新的JWT令牌字符串 + extend_minutes (int): 延长的分钟数,默认使用DEFAULT_TIMEOUT_MINUTES + + 返回: + dict: 刷新结果,包含以下字段: + code (int): 状态码,0-成功,-1-失败 + data (dict/None): 成功时包含新令牌,失败时为None + msg (str): 提示信息 + + 使用示例: + >>> result = refresh_token(old_token, extend_minutes=30) + >>> if result['code'] == 0: + ... new_token = result['data']['access_token'] + ... else: + ... # 令牌无效,需要重新登录 + ... error_msg = result['msg'] + + 注意事项: + 1. 令牌只有在有效期内才能刷新 + 2. 刷新后会生成全新的令牌,旧令牌立即失效 + 3. 建议在令牌剩余有效期少于一半时进行刷新 + """ + if extend_minutes is None: + extend_minutes = DEFAULT_TIMEOUT_MINUTES + + # 先验证原令牌 + result = parse_payload(token) + + # 如果验证失败,返回错误信息 + if result['code'] != 0: + return { + "code": -1, + "data": None, + "msg": f"令牌无效,无法刷新: {result['msg']}" + } + + try: + # 获取原载荷数据(不包含exp、iat等JWT标准字段) + payload = result['data'] + + # 移除JWT标准字段,避免重复添加 + # 注意:保留原始业务数据,移除系统字段 + clean_payload = {k: v for k, v in payload.items() + if k not in ['exp', 'iat', 'nbf']} + + # 生成新令牌 + new_token = create_token(clean_payload, timeout=extend_minutes) + + logger.info(f"JWT令牌刷新成功,延长{extend_minutes}分钟") + + return { + "code": 0, + "data": { + "access_token": new_token, + "expires_in": extend_minutes * 60 # 转换为秒 + }, + "msg": "令牌刷新成功" + } + + except Exception as e: + logger.error(f"刷新JWT令牌失败: {str(e)}") + return { + "code": -1, + "data": None, + "msg": "令牌刷新失败,请重新登录" + } + + +def get_token_expiration(token): + """ + 获取JWT令牌的过期时间 + + 验证签名后读取过期时间,确保token未被篡改。 + 不过验证过期状态(verify_exp=False),以便调用方 + 读取已过期token的过期时间用于剩余时间计算等场景。 + + Args: + token (str): JWT令牌字符串 + + Returns: + datetime/None: 令牌过期时间(UTC),解析失败返回None + """ + try: + # 验证签名确保token未被篡改,但不验证过期状态 + payload = jwt.decode( + token, + JWT_SALT, + algorithms=[JWT_ALGORITHM], + options={"verify_exp": False} + ) + + exp_timestamp = payload.get('exp') + if exp_timestamp: + return datetime.datetime.fromtimestamp(exp_timestamp, tz=datetime.timezone.utc) + return None + + except Exception as e: + logger.warning(f"获取JWT过期时间失败: {str(e)}") + return None + + +def get_access_token(): + """ + 从请求头中提取 access_token + ============================================================= + + 从 HTTP 请求的 Authorization 头中提取 JWT token,返回纯净的 token 字符串。 + 统一处理 token 提取逻辑,便于在其他模块中复用。 + + 参数: + request: Django 请求对象 + + 返回: + tuple: (success, token, message) + - success (bool): 是否成功获取 token + - token (str/None): token 字符串,失败时为 None + - message (str): 提示信息 + + 使用示例: + >>> success, token, msg = get_access_token() + >>> if success: + ... # 使用 token 进行后续处理 + ... parse_result = parse_payload(token) + ... else: + ... # 处理错误 + ... return R.failed(msg) + """ + try: + # 从请求头中获取 token 值 + # 说明:Authorization 头格式通常为 "Bearer " + auth_header = request.headers.get('Authorization', '') + if not auth_header: + return False, None, '缺少 Authorization 头' + + # 处理 Bearer 前缀 + # 说明:移除 "Bearer " 前缀并去除首尾空格,获取纯净 token + access_token = auth_header.replace('Bearer ', "").strip() + if not access_token: + return False, None, 'token 为空' + + return True, access_token, 'success' + + except Exception as e: + logger.error(f"获取 access_token 异常: {str(e)}") + return False, None, f'token 提取异常: {str(e)}' + + +# token解析工具类 +def parse_token(request): + """ + 解析请求头中的token,返回解析结果 + ============================================================= + + 从HTTP请求的Authorization头中提取JWT token,进行解析验证, + 并返回包含用户信息的结构化数据。统一处理各种异常情况。 + + 参数: + request: Django请求对象 + + 返回: + dict: 包含解析结果和用户信息的字典 + { + 'code': 0, # 0-成功,-1-失败 + 'msg': 'success', # 提示信息 + 'data': { + 'userId': 0, # 用户ID + 'username': '', # 用户名 + 'realname': '' # 真实姓名 + } + } + + 使用示例: + >>> result = parse_token(request) + >>> if result['code'] == 0: + ... user_id = result['data']['userId'] + ... username = result['data']['username'] + """ + # 初始化返回结果 + # 说明:设置默认失败状态,确保返回结构完整性 + result = { + 'code': -1, + 'msg': 'token无效', + 'data': { + 'userId': 0, + 'userName': None + } + } + + try: + # 获取 access_token + success, access_token, msg = get_access_token() + if not success: + result['msg'] = msg + return result + + # JWT解密 + # 函数:parse_payload(access_token) + # 说明:调用JWT解析工具验证token有效性 + parse_result = parse_payload(access_token) + + # 验证解析结果 + # 说明:确保解析结果格式正确 + if not isinstance(parse_result, dict): + result['msg'] = '解析结果格式错误' + return result + + # 结果标识 + # 说明:code为0表示验证通过 + code = parse_result.get('code', -1) + if code != 0: + result['msg'] = parse_result.get('msg', 'token验证失败') + return result + + # 获取数据部分 + # 说明:提取token载荷中的用户数据 + data = parse_result.get('data', {}) + if not isinstance(data, dict): + result['msg'] = '数据格式错误' + return result + + # 提取用户信息 + # 说明:从载荷中获取具体用户字段 + user_id = data.get('userId', 0) + username = data.get('username', '') + realname = data.get('realname', '') + + # 更新返回结果 + # 说明:设置成功状态并填充用户信息 + result['code'] = 0 + result['msg'] = 'success' + result['data']['userId'] = user_id + result['data']['username'] = username + result['data']['realname'] = realname + + except Exception as e: + # 异常捕获 + # 说明:防止未处理的异常导致系统崩溃 + result['msg'] = f'token解析异常: {str(e)}' + # 可以在这里记录日志 + # logger.error(f"token解析异常: {str(e)}") + + return result + + +# ============================================================= +# Token黑名单功能 +# ============================================================= + +# 黑名单Key前缀,用于Redis中存储被注销的Token +_TOKEN_BLACKLIST_PREFIX = 'token:blacklist:' + + +def _token_fingerprint(token: str) -> str: + """ + 生成Token指纹(SHA256摘要) + + 避免将完整Token存入Redis,仅存储摘要值,减少泄露风险。 + + Args: + token: JWT令牌字符串 + + Returns: + str: SHA256十六进制摘要 + """ + return hashlib.sha256(token.encode('utf-8')).hexdigest() + + +def add_token_to_blacklist(token: str) -> bool: + """ + 将Token加入黑名单 + + 退出登录时调用,使Token立即失效。黑名单条目的TTL与Token剩余有效期一致, + Token自然过期后黑名单条目自动清除,避免Redis内存无限增长。 + + Args: + token: JWT令牌字符串 + + Returns: + bool: 是否成功加入黑名单 + """ + try: + from src.core.redis_client import redis + + # 获取Token剩余有效期 + expiration = get_token_expiration(token) + if expiration is None: + # 无法解析过期时间,使用默认过期时间作为TTL + ttl_seconds = DEFAULT_TIMEOUT_MINUTES * 60 + else: + ttl_seconds = int((expiration - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds()) + if ttl_seconds <= 0: + # Token已自然过期,无需加入黑名单 + return True + + fingerprint = _token_fingerprint(token) + key = f'{_TOKEN_BLACKLIST_PREFIX}{fingerprint}' + redis.set(key, '1', expire=ttl_seconds) + + logger.info(f"Token已加入黑名单,TTL={ttl_seconds}秒") + return True + + except Exception as e: + logger.error(f"Token加入黑名单失败: {str(e)}") + return False + + +def is_token_blacklisted(token: str) -> bool: + """ + 检查Token是否在黑名单中 + + 在登录验证中间件中调用,拒绝已被注销的Token。 + + Args: + token: JWT令牌字符串 + + Returns: + bool: True-已被注销(在黑名单中),False-有效 + """ + try: + from src.core.redis_client import redis + + fingerprint = _token_fingerprint(token) + key = f'{_TOKEN_BLACKLIST_PREFIX}{fingerprint}' + return redis.exists(key) > 0 + + except Exception as e: + logger.error(f"Token黑名单检查异常: {str(e)}") + # Redis不可用时放行,避免阻断所有请求 + return False diff --git a/src/core/mail.py b/src/core/mail.py new file mode 100644 index 0000000..f164589 --- /dev/null +++ b/src/core/mail.py @@ -0,0 +1,46 @@ +""" +Flask-Mail 扩展初始化模块 +============================================================= + +仅负责 Flask-Mail 扩展实例的创建与初始化。 + +""" + +from flask import Flask +from flask_mail import Mail + +# Flask-Mail 扩展实例 +# 在应用工厂中通过 mail.init_app(app) 完成绑定 +mail = Mail() + + +def init_mail(app: Flask): + """ + 初始化 Flask-Mail 扩展,将 Mail 实例绑定到应用。 + + Args: + app: Flask 应用实例 + + Example: + # 在应用工厂函数中调用 + init_mail(app) + """ + from config import ( + MAIL_MAILER, MAIL_SERVER, MAIL_PORT, MAIL_USE_SSL, + MAIL_USERNAME, MAIL_PASSWORD, MAIL_ENCRYPTION, + MAIL_FROM_NAME, MAIL_FROM_ADDRESS, + ) + + app.config['MAIL_MAILER'] = MAIL_MAILER + app.config['MAIL_SERVER'] = MAIL_SERVER + app.config['MAIL_PORT'] = MAIL_PORT + app.config['MAIL_USE_SSL'] = MAIL_USE_SSL + app.config['MAIL_USERNAME'] = MAIL_USERNAME + app.config['MAIL_PASSWORD'] = MAIL_PASSWORD + app.config['MAIL_ENCRYPTION'] = MAIL_ENCRYPTION + app.config['MAIL_DEFAULT_SENDER'] = ( + f'{MAIL_FROM_NAME} <{MAIL_FROM_ADDRESS}>' if MAIL_FROM_NAME and MAIL_FROM_ADDRESS + else MAIL_FROM_ADDRESS or None + ) + + mail.init_app(app) diff --git a/src/core/password.py b/src/core/password.py new file mode 100644 index 0000000..b3fad4c --- /dev/null +++ b/src/core/password.py @@ -0,0 +1,72 @@ +""" +密码加密工具模块 +============================================================= + +该模块提供基于 bcrypt 的密码加密和验证功能。 +采用 bcrypt(password + salt) 方案,bcrypt 内置盐与外部 salt 双重加盐, +确保相同密码产生不同哈希,有效抵御彩虹表攻击。 + +主要功能: +1. 生成随机盐值(generate_salt) +2. 密码加密(encrypt_password)- bcrypt(password + salt) +3. 密码验证(verify_password) + +所属包: utils +所在文件: utils/password.py +依赖模块: bcrypt - 密码哈希第三方库 + secrets - 安全随机数生成模块 +""" + +import secrets + +import bcrypt + +# bcrypt 工作因子,值越大计算越慢越安全,默认12(约250ms) +BCRYPT_ROUNDS = 12 + + +def generate_salt(length: int = 10) -> str: + """ + 生成随机盐值 + + Args: + length: 盐值长度,默认10位 + + Returns: + str: 随机盐值字符串 + """ + return secrets.token_urlsafe(length)[:length] + + +def encrypt_password(password: str, salt: str) -> str: + """ + 使用 bcrypt 加密密码 + + 将密码与外部 salt 拼接后进行 bcrypt 哈希,实现双重加盐。 + + Args: + password: 明文密码 + salt: 外部盐值 + + Returns: + str: bcrypt 哈希字符串(60字符) + """ + combined = (password + salt).encode('utf-8') + hashed = bcrypt.hashpw(combined, bcrypt.gensalt(rounds=BCRYPT_ROUNDS)) + return hashed.decode('utf-8') + + +def verify_password(password: str, salt: str, hashed: str) -> bool: + """ + 验证密码是否正确 + + Args: + password: 待验证的明文密码 + salt: 外部盐值 + hashed: 存储的 bcrypt 哈希值 + + Returns: + bool: 密码是否匹配 + """ + combined = (password + salt).encode('utf-8') + return bcrypt.checkpw(combined, hashed.encode('utf-8')) diff --git a/src/core/redis_client.py b/src/core/redis_client.py new file mode 100644 index 0000000..06622a2 --- /dev/null +++ b/src/core/redis_client.py @@ -0,0 +1,377 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +Redis 客户端封装模块 +============================================================= + +整合来源: + - extensions/redis.py:FlaskRedis 扩展实例与初始化 + - utils/redis_client.py:RedisClient 高级封装(序列化、类型转换) + +提供 Redis 操作的统一封装,支持字符串、哈希、列表、集合、有序集合等 +数据类型。自动处理序列化/反序列化,业务代码无需关心底层存储格式。 + +使用方式: + from src.core.redis_client import redis + + redis.set('key', 'value', expire=3600) + value = redis.get('key') +""" + +import json +import pickle +from typing import Any, Union, List, Dict, Tuple + +from flask import Flask +from flask_redis import FlaskRedis + +# ============================================================ +# FlaskRedis 扩展实例 +# ============================================================ +# 在应用工厂中通过 init_redis(app) 完成绑定 +redis_client = FlaskRedis(decode_responses=True, protocol=2) + + +def init_redis(app: Flask): + """ + 初始化 Redis 扩展,将 FlaskRedis 实例绑定到应用。 + + Args: + app: Flask 应用实例 + + Example: + # 在应用工厂函数中调用 + init_redis(app) + """ + redis_client.init_app(app) + + +class RedisClient: + """ + Redis客户端封装类 + + 提供丰富的Redis操作方法,支持字符串、哈希、列表、集合、有序集合等数据类型 + """ + + def __init__(self, client=None): + """ + 初始化Redis客户端 + + Args: + client: FlaskRedis实例,如果为None则使用全局redis_client + """ + self.client = client or redis_client + + # ==================== 基础方法 ==================== + + def set(self, key: str, value: Any, expire: int = None, nx: bool = False, xx: bool = False) -> bool: + """设置键值对""" + serialized_value = self._serialize(value) + + if nx and xx: + raise ValueError("nx和xx不能同时为True") + + if nx: + result = self.client.setnx(key, serialized_value) + if expire and result: + self.client.expire(key, expire) + return result + elif xx: + result = self.client.set(key, serialized_value, xx=True) + if expire and result: + self.client.expire(key, expire) + return result + else: + if expire: + return self.client.setex(key, expire, serialized_value) + else: + return self.client.set(key, serialized_value) + + def get(self, key: str, default: Any = None, deserialize: bool = True) -> Any: + """获取键值""" + data = self.client.get(key) + if data is None: + return default + + if deserialize: + return self._deserialize(data) + return data + + def delete(self, *keys: str) -> int: + """删除一个或多个键""" + if not keys: + return 0 + return self.client.delete(*keys) + + def exists(self, *keys: str) -> int: + """检查键是否存在""" + if not keys: + return 0 + return self.client.exists(*keys) + + def expire(self, key: str, seconds: int) -> bool: + """设置键的过期时间""" + return self.client.expire(key, seconds) + + def expireat(self, key: str, timestamp: int) -> bool: + """设置键在指定时间戳过期""" + return self.client.expireat(key, timestamp) + + def ttl(self, key: str) -> int: + """获取键的剩余过期时间""" + return self.client.ttl(key) + + def keys(self, pattern: str = '*') -> List[str]: + """获取匹配模式的所有键(使用SCAN,避免KEYS阻塞)""" + result = [] + cursor = 0 + while True: + cursor, batch = self.client.scan(cursor=cursor, match=pattern, count=100) + result.extend(batch) + if cursor == 0: + break + return result + + # ==================== 哈希表操作 ==================== + + def hset(self, name: str, key: str, value: Any) -> int: + """设置哈希表中的字段值""" + serialized_value = self._serialize(value) + return self.client.hset(name, key, serialized_value) + + def hget(self, name: str, key: str, deserialize: bool = True) -> Any: + """获取哈希表中的字段值""" + data = self.client.hget(name, key) + if data is None: + return None + + if deserialize: + return self._deserialize(data) + return data + + def hgetall(self, name: str, deserialize: bool = True) -> Dict[str, Any]: + """获取哈希表中所有字段和值""" + data = self.client.hgetall(name) + if not data: + return {} + + if deserialize: + return {k.decode(): self._deserialize(v) for k, v in data.items()} + return {k.decode(): v for k, v in data.items()} + + def hdel(self, name: str, *keys: str) -> int: + """删除哈希表中的字段""" + if not keys: + return 0 + return self.client.hdel(name, *keys) + + def hincrby(self, name: str, key: str, amount: int = 1) -> int: + """为哈希表中的字段值增加指定数值""" + return self.client.hincrby(name, key, amount) + + # ==================== 列表操作 ==================== + + def lpush(self, name: str, *values: Any) -> int: + """从列表左侧插入元素""" + serialized_values = [self._serialize(v) for v in values] + return self.client.lpush(name, *serialized_values) + + def rpush(self, name: str, *values: Any) -> int: + """从列表右侧插入元素""" + serialized_values = [self._serialize(v) for v in values] + return self.client.rpush(name, *serialized_values) + + def lpop(self, name: str, deserialize: bool = True) -> Any: + """从列表左侧弹出元素""" + data = self.client.lpop(name) + if data is None: + return None + + if deserialize: + return self._deserialize(data) + return data + + def rpop(self, name: str, deserialize: bool = True) -> Any: + """从列表右侧弹出元素""" + data = self.client.rpop(name) + if data is None: + return None + + if deserialize: + return self._deserialize(data) + return data + + def lrange(self, name: str, start: int, end: int, deserialize: bool = True) -> List[Any]: + """获取列表指定范围的元素""" + data = self.client.lrange(name, start, end) + if not data: + return [] + + if deserialize: + return [self._deserialize(item) for item in data] + return [item for item in data] + + def llen(self, name: str) -> int: + """获取列表长度""" + return self.client.llen(name) + + # ==================== 集合操作 ==================== + + def sadd(self, name: str, *values: Any) -> int: + """向集合中添加元素""" + serialized_values = [self._serialize(v) for v in values] + return self.client.sadd(name, *serialized_values) + + def srem(self, name: str, *values: Any) -> int: + """从集合中移除元素""" + serialized_values = [self._serialize(v) for v in values] + return self.client.srem(name, *serialized_values) + + def smembers(self, name: str, deserialize: bool = True) -> set: + """获取集合中所有元素""" + data = self.client.smembers(name) + if not data: + return set() + + if deserialize: + return {self._deserialize(item) for item in data} + return {item for item in data} + + def sismember(self, name: str, value: Any) -> bool: + """判断元素是否在集合中""" + serialized_value = self._serialize(value) + return self.client.sismember(name, serialized_value) + + # ==================== 有序集合操作 ==================== + + def zadd(self, name: str, mapping: Dict[Any, float]) -> int: + """向有序集合中添加元素""" + serialized_mapping = {self._serialize(k): v for k, v in mapping.items()} + return self.client.zadd(name, serialized_mapping) + + def zrange(self, name: str, start: int, end: int, withscores: bool = False, + deserialize: bool = True) -> Union[List[Any], List[Tuple[Any, float]]]: + """获取有序集合指定范围的元素(按分数升序)""" + data = self.client.zrange(name, start, end, withscores=withscores) + if not data: + return [] + + if withscores: + if deserialize: + return [(self._deserialize(item[0]), item[1]) for item in data] + return [(item[0], item[1]) for item in data] + else: + if deserialize: + return [self._deserialize(item) for item in data] + return list(data) + + def zrevrange(self, name: str, start: int, end: int, withscores: bool = False, + deserialize: bool = True) -> Union[List[Any], List[Tuple[Any, float]]]: + """获取有序集合指定范围的元素(按分数降序)""" + data = self.client.zrevrange(name, start, end, withscores=withscores) + if not data: + return [] + + if withscores: + if deserialize: + return [(self._deserialize(item[0]), item[1]) for item in data] + return [(item[0], item[1]) for item in data] + else: + if deserialize: + return [self._deserialize(item) for item in data] + return list(data) + + # ==================== 高级操作 ==================== + + def pipeline(self): + """获取管道对象,用于批量执行命令""" + return self.client.pipeline() + + def incr(self, key: str, amount: int = 1) -> int: + """增加键的值(用于计数器)""" + return self.client.incrby(key, amount) + + def decr(self, key: str, amount: int = 1) -> int: + """减少键的值""" + return self.client.decrby(key, amount) + + def lock(self, key: str, timeout: int = 10, blocking: bool = True, + blocking_timeout: int = None) -> Any: + """获取分布式锁""" + return self.client.lock(key, timeout=timeout, blocking=blocking, + blocking_timeout=blocking_timeout) + + # ==================== 私有方法 ==================== + + def _serialize(self, value: Any) -> Union[str, bytes]: + """序列化值""" + if isinstance(value, str): + return value + elif isinstance(value, (int, float, bool)): + return str(value) + elif isinstance(value, (dict, list, tuple)): + return json.dumps(value, ensure_ascii=False) + else: + return pickle.dumps(value) + + def _deserialize(self, value: Union[str, bytes]) -> Any: + """反序列化值""" + if not value: + return None + + if isinstance(value, bytes): + try: + value = value.decode('utf-8') + except UnicodeDecodeError: + try: + return pickle.loads(value) + except Exception: + return value + + try: + return json.loads(value) + except (json.JSONDecodeError, TypeError): + return value + + +# 创建全局Redis客户端实例 +redis = RedisClient() diff --git a/src/core/response.py b/src/core/response.py new file mode 100644 index 0000000..adfd60b --- /dev/null +++ b/src/core/response.py @@ -0,0 +1,222 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 珊瑚云团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +统一响应结果模块 +============================================================= + +该模块提供了系统统一的JSON响应格式,确保所有API接口返回格式一致。 +包含成功响应和失败响应两种基础方法,支持扩展参数。 + +响应格式规范: +- code: 状态码,0表示成功,1表示失败(可扩展其他状态码) +- data: 响应数据,成功时返回具体数据,失败时为None +- msg: 提示信息,对操作结果的文字描述 +- ok: 布尔值,标识操作是否成功(兼容旧版前端) +- 其他扩展参数:可通过kwargs动态添加 + +使用示例: + # 成功响应 + return R.ok(data={"id": 1, "name": "张三"}) + return R.ok(msg="操作成功") + + # 失败响应 + return R.failed(msg="用户名或密码错误") + return R.failed(msg="服务器内部错误", code=500) + + # 带扩展参数的响应 + return R.ok(data=user_info, total=100, pages=10) +""" + +from typing import Any, Optional, Dict + +from flask import jsonify + +from src.common.constants.common import MESSAGE_OK, MESSAGE_FAIL + + +def ok( + data: Optional[Any] = None, + msg: str = MESSAGE_OK, + code: int = 0, + **kwargs: Any +) -> jsonify: + """ + 生成成功响应 + + 构建一个标准的成功响应JSON对象,包含状态码、数据、提示信息等字段。 + 支持通过kwargs添加额外的响应字段,便于扩展。 + + Args: + data: 响应数据,可以是任意类型(dict, list, str, int等),默认为None + msg: 成功提示信息,默认使用常量MESSAGE_OK + code: 状态码,0表示成功,默认为0 + **kwargs: 额外的键值对参数,会合并到响应体中 + + Returns: + jsonify: 格式化的JSON响应对象 + + Examples: + >>> return R.ok() + {"code": 0, "data": None, "msg": "操作成功", "ok": True} + + >>> return R.ok(data={"id": 1, "name": "张三"}) + {"code": 0, "data": {"id": 1, "name": "张三"}, "msg": "操作成功", "ok": True} + + >>> return R.ok(msg="创建成功", total=100) + {"code": 0, "data": None, "msg": "创建成功", "ok": True, "total": 100} + """ + # 构建基础响应体 + response_body: Dict[str, Any] = { + "code": code, + "data": data, + "msg": msg, + "ok": True + } + + # 合并扩展参数 + if kwargs: + response_body.update(kwargs) + + # 返回JSON响应 + return jsonify(response_body) + + +def failed( + msg: str = MESSAGE_FAIL, + code: int = 1, + data: Optional[Any] = None, + **kwargs: Any +) -> jsonify: + """ + 生成失败响应 + + 构建一个标准的失败响应JSON对象,包含状态码、错误信息等字段。 + 支持通过kwargs添加额外的响应字段,便于扩展。 + + Args: + msg: 错误提示信息,默认使用常量MESSAGE_FAIL + code: 状态码,1表示失败,默认为1(可根据业务需求定义其他错误码) + data: 错误时的附加数据,通常为None,默认为None + **kwargs: 额外的键值对参数,会合并到响应体中 + + Returns: + jsonify: 格式化的JSON响应对象 + + Examples: + >>> return R.failed() + {"code": 1, "data": None, "msg": "操作失败", "ok": False} + + >>> return R.failed(msg="用户名或密码错误") + {"code": 1, "data": None, "msg": "用户名或密码错误", "ok": False} + + >>> return R.failed(msg="未授权访问", code=401) + {"code": 401, "data": None, "msg": "未授权访问", "ok": False} + + >>> return R.failed(msg="参数错误", errors={"name": "该字段不能为空"}) + {"code": 1, "data": None, "msg": "参数错误", "ok": False, "errors": {...}} + """ + # 构建基础响应体 + response_body: Dict[str, Any] = { + "code": code, + "data": data, + "msg": msg, + "ok": False + } + + # 合并扩展参数 + if kwargs: + response_body.update(kwargs) + + # 返回JSON响应 + return jsonify(response_body) + + +def response( + data: Optional[Any] = None, + msg: str = MESSAGE_OK, + code: int = 0, + success: bool = True, + **kwargs: Any +) -> jsonify: + """ + 通用响应生成器(高级用法) + + 提供更灵活的响应生成方式,允许自定义所有字段。 + 通常推荐使用ok()和failed()两个专用方法,但在需要完全控制响应格式时可以使用此方法。 + + Args: + data: 响应数据 + msg: 提示信息 + code: 状态码 + success: 操作是否成功标识 + **kwargs: 额外的键值对参数 + + Returns: + jsonify: 格式化的JSON响应对象 + + Examples: + >>> return R.response(data=[], msg="查询成功", code=200, success=True) + {"code": 200, "data": [], "msg": "查询成功", "ok": True} + """ + response_body: Dict[str, Any] = { + "code": code, + "data": data, + "msg": msg, + "ok": success + } + + if kwargs: + response_body.update(kwargs) + + return jsonify(response_body) + + +# 为方便使用,定义常用错误码常量(可选) +class Codes: + """常用状态码定义""" + SUCCESS = 0 + FAILED = 1 + UNAUTHORIZED = 401 # 未认证 + FORBIDDEN = 403 # 无权限 + NOT_FOUND = 404 # 资源不存在 + VALIDATE_ERROR = 422 # 验证错误 + SERVER_ERROR = 500 # 服务器错误 diff --git a/src/core/scheduler.py b/src/core/scheduler.py new file mode 100644 index 0000000..e2d0bd5 --- /dev/null +++ b/src/core/scheduler.py @@ -0,0 +1,199 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 一米阳光团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 一米阳光团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +定时任务调度器扩展模块 +负责初始化和管理 APScheduler 定时任务 +""" + +import logging +from flask import Flask + +logger = logging.getLogger(__name__) + + +class Scheduler: + """ + 定时任务调度器管理类 + + 负责调度器的初始化和生命周期管理 + """ + + def __init__(self): + """初始化调度器实例""" + self._initialized = False + + def init_app(self, app: Flask): + """ + 初始化定时任务调度器 + + Args: + app: Flask 应用实例 + + 使用示例: + from extensions.scheduler import scheduler + + def create_app(): + app = Flask(__name__) + # ... 其他配置 + scheduler.init_app(app) + return app + """ + # 避免重复初始化 + if self._initialized: + logger.warning("定时任务调度器已初始化,跳过重复初始化") + return + + # ============================================================ + # 在应用上下文中执行初始化 + # ============================================================ + with app.app_context(): + try: + from application.services.scheduler import reload_scheduler + + # 重新加载所有任务 + count = reload_scheduler() + + self._initialized = True + + if count > 0: + logger.info(f"定时任务调度器初始化完成,已加载 {count} 个任务") + else: + logger.info("定时任务调度器初始化完成,当前无可用任务") + + # 打印调度器状态(调试用) + self._log_scheduler_status() + + except Exception as e: + logger.error(f"定时任务调度器初始化失败: {str(e)}") + raise + + def _log_scheduler_status(self): + """打印调度器状态(用于调试)""" + try: + from application.services.scheduler import scheduler + + if scheduler.running: + jobs = scheduler.get_jobs() + if jobs: + logger.debug("当前调度器中的任务列表:") + for job in jobs: + logger.debug(f" - ID: {job.id}, Name: {job.name}, Next run: {job.next_run_time}") + else: + logger.debug("当前调度器中无任务") + else: + logger.warning("调度器未运行") + except Exception as e: + logger.error(f"获取调度器状态失败: {str(e)}") + + def shutdown(self): + """ + 关闭定时任务调度器 + + 在应用退出时调用 + """ + try: + from application.services.scheduler import scheduler + + if scheduler.running: + scheduler.shutdown() + logger.info("定时任务调度器已关闭") + except Exception as e: + logger.error(f"关闭调度器失败: {str(e)}") + + def reload(self) -> int: + """ + 重新加载所有定时任务 + + Returns: + int: 成功加载的任务数量 + """ + try: + from application.services.scheduler import reload_scheduler + count = reload_scheduler() + logger.info(f"重新加载定时任务完成,共加载 {count} 个任务") + return count + except Exception as e: + logger.error(f"重新加载定时任务失败: {str(e)}") + return 0 + + def get_status(self) -> dict: + """ + 获取调度器状态 + + Returns: + dict: 包含调度器运行状态和任务列表 + """ + try: + from application.services.scheduler import scheduler + + jobs = scheduler.get_jobs() + job_list = [] + + for job in jobs: + job_list.append({ + 'id': job.id, + 'name': job.name, + 'next_run_time': str(job.next_run_time) if job.next_run_time else None, + 'trigger': str(job.trigger) + }) + + return { + 'initialized': self._initialized, + 'running': scheduler.running, + 'job_count': len(job_list), + 'jobs': job_list + } + except Exception as e: + logger.error(f"获取调度器状态失败: {str(e)}") + return { + 'initialized': self._initialized, + 'running': False, + 'job_count': 0, + 'jobs': [], + 'error': str(e) + } + + +# ============================================================ +# 创建全局调度器单例实例 +# ============================================================ +scheduler = Scheduler() diff --git a/src/core/security.py b/src/core/security.py new file mode 100644 index 0000000..fa4289f --- /dev/null +++ b/src/core/security.py @@ -0,0 +1,121 @@ +# +====================================================================== +# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] +# +---------------------------------------------------------------------- +# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 +# +---------------------------------------------------------------------- +# | 开源协议: Apache-2.0 +# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 +# +---------------------------------------------------------------------- +# | 官方网站: https://www.djangoadmin.cn +# +---------------------------------------------------------------------- +# | 研发团队: 珊瑚云团队 荣誉出品 +# +====================================================================== +# | 版权和免责声明: +# | 一、知识产权声明 +# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 +# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, +# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 +# | 二、使用限制与禁止条款 +# | 您理解并同意,禁止将本产品用于以下任何行为: +# | 1. 任何违反中华人民共和国法律法规的用途; +# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; +# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 +# | 三、责任豁免 +# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 +# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 +# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 +# | 四、违约与法律责任 +# | 如发现任何违反本声明的行为,本团队有权: +# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; +# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; +# | 3. 保留进一步采取刑事控告等法律措施的权利。 +# | 五、许可范围说明 +# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 +# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 +# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 +# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 +# | 成的一切经济损失及商誉损害。 +# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 +# | 许可协议》附件为准。 +# +====================================================================== + +""" +用户安全工具模块 +============================================================= + +该模块提供了用户认证和安全相关的工具函数。 +用于快速获取当前登录用户的信息,简化业务代码中的用户信息获取操作。 +""" +import logging + +from flask import request + +from src.core.jwt import parse_token + + +# ============================= 用户信息获取工具 ============================= + + +def get_user_id(): + """ + 获取当前登录用户ID + ============================================================= + + 快速获取当前登录用户的ID,用于需要用户ID的业务场景。 + 基于parse_token函数实现,简化调用方式。 + + 使用场景: + - 记录操作日志时需要记录操作人ID + - 数据查询时进行用户权限过滤 + - 创建数据时自动关联当前用户 + + 参数: + request (HttpRequest): Django请求对象,包含请求头中的token信息 + + 返回: + int: 当前登录用户的ID,若token无效或用户不存在则返回0 + + 异常: + 可能抛出parse_token函数产生的异常(如token过期、token无效等) + 建议调用方自行处理异常 + + 示例: + >>> user_id = get_user_id() + >>> if user_id: + ... # 执行需要用户ID的操作 + ... create_log(user_id, "操作内容") + """ + try: + # 解析请求中的token,获取用户信息 + result = parse_token(request) + + # 安全获取用户ID,避免KeyError + if result and isinstance(result, dict): + data = result.get('data', {}) + if isinstance(data, dict): + return data.get('userId', 0) + + return 0 + except Exception: + # 记录异常日志(可根据需要添加日志记录) + return 0 + + +def get_username(): + """ + 获取当前登录用户账号 + + Returns: + str: 当前登录用户的账号(username),若token无效或用户不存在则返回空字符串 + """ + try: + result = parse_token(request) + if result and isinstance(result, dict): + data = result.get('data', {}) + if isinstance(data, dict): + username = data.get('username') + return username if username else '' + return '' + except Exception as e: + logging.error(f"获取用户名失败: {str(e)}") + return '' diff --git a/src/models_registry.py b/src/models_registry.py new file mode 100644 index 0000000..05c921c --- /dev/null +++ b/src/models_registry.py @@ -0,0 +1,25 @@ +""" +模型注册中心 +============================================================= + +统一在此导入全部数据库模型,确保 Flask-Migrate 能全量识别表结构 +(migrations/env.py 依赖 db.metadata 收集所有表)。 + +P2 阶段:模型仍位于 application/models/,经其 __init__ 全量导入。 +P3 阶段:随模块逐个迁入 src/modules/,此处逐行改为 + from modules...models import * 并移除对应旧行。 +P7 阶段:application/ 删除后,仅保留 src.modules 导入。 +""" + +# ============================================================ +# P2:模型仍位于 application/models/(经其 __init__ 全量导入) +# ============================================================ +import application.models # noqa: F401 (P3 起逐步替换为 src.modules 导入) + +# ============================================================ +# P3 起按此格式逐模块替换: +# from modules.system.user.models import * # noqa: F401,F403 +# from modules.system.role.models import * # noqa: F401,F403 +# from modules.system.menu.models import * # noqa: F401,F403 +# ... +# ============================================================ diff --git a/utils/captcha.py b/utils/captcha.py index 7e433ef..ea7357a 100644 --- a/utils/captcha.py +++ b/utils/captcha.py @@ -1,232 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== - """ -验证码生成与验证模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -功能说明: - 1. 生成带干扰的验证码图片,返回 Base64 格式数据 - 2. 验证用户输入的验证码是否正确 - 3. 防刷机制:限制同一 IP 获取验证码的频率 - -技术特点: - - 使用 secrets 模块生成安全随机数,替代 random 模块 - - 字符集去除 0/O/1/I/l/5/S/2/Z 等易混淆字符,提升用户体验 - - 验证码图片包含干扰线和噪点,防止 OCR 识别 - - 验证码存储于 Redis,支持过期自动失效 - - 验证成功后自动删除,确保一次性使用,防止重放攻击 - - 大小写不敏感比较,提升用户体验 - -配置来源: - 所有配置参数从 config 模块导入,由 .env 文件控制 - 包括:验证码长度、过期时间、防刷间隔、字符集、字体、图片尺寸等 - -依赖: - - utils.redis_client: Redis 客户端,用于存储验证码 - - config: 验证码配置参数(从 .env 读取) - - PIL: 图片生成与处理 - -接口说明: - generate_captcha() -> dict - 生成验证码,返回 {"idKey": "唯一标识", "data": "base64图片数据"} - 调用频率受 CAPTCHA_RATE_LIMIT 限制 - - verify_captcha(id_key, user_input) -> bool - 验证用户输入的验证码是否正确 - 验证成功后自动删除 Redis 中的记录 +验证码工具已迁移至 src.common.utils.captcha。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import secrets -from io import BytesIO - -from PIL import Image, ImageFont, ImageDraw, ImageFilter -import base64 - -from utils.redis_client import redis -from config import ( - CAPTCHA_LENGTH, - CAPTCHA_EXPIRE, - CAPTCHA_RATE_LIMIT, - CAPTCHA_RATE_WINDOW, - CAPTCHA_CHARS, - CAPTCHA_FONT_PATH, - CAPTCHA_IMAGE_WIDTH, - CAPTCHA_IMAGE_HEIGHT, - CAPTCHA_FONT_SIZE, - CAPTCHA_NOISE_LINES, - CAPTCHA_NOISE_DOTS, -) - - -# ============================================================ -# 内部函数 -# ============================================================ - -def get_random_color(): - """获取随机颜色RGB(使用secrets更安全)""" - return ( - secrets.randbelow(256), - secrets.randbelow(256), - secrets.randbelow(256) - ) - - -def generate_image(length): - """生成验证码图片 - - Args: - length: 验证码长度 - - Returns: - tuple: (PIL.Image对象, 验证码字符串) - """ - # 从字符集随机抽取 - code = ''.join(secrets.choice(CAPTCHA_CHARS) for _ in range(length)) - - # 创建画布(浅灰色背景,减少对比度干扰) - image = Image.new('RGB', (CAPTCHA_IMAGE_WIDTH, CAPTCHA_IMAGE_HEIGHT), color=(240, 240, 240)) - - # 加载字体 - try: - font = ImageFont.truetype(CAPTCHA_FONT_PATH, size=CAPTCHA_FONT_SIZE) - except Exception: - font = ImageFont.load_default() - - draw = ImageDraw.Draw(image) - - # 绘制验证码字符(随机偏移,防OCR识别) - char_width = (CAPTCHA_IMAGE_WIDTH - 20) // length - for i, c in enumerate(code): - x = 10 + i * char_width + secrets.randbelow(8) - y = secrets.randbelow(10) + 2 - draw.text( - (x, y), - text=c, - fill=get_random_color(), - font=font - ) - - # 绘制干扰线 - for _ in range(CAPTCHA_NOISE_LINES): - x1 = secrets.randbelow(CAPTCHA_IMAGE_WIDTH) - y1 = secrets.randbelow(CAPTCHA_IMAGE_HEIGHT // 2) - x2 = secrets.randbelow(CAPTCHA_IMAGE_WIDTH) - y2 = secrets.randbelow(CAPTCHA_IMAGE_HEIGHT // 2) + CAPTCHA_IMAGE_HEIGHT // 2 - draw.line(((x1, y1), (x2, y2)), fill=get_random_color(), width=1) - - # 添加噪点 - for _ in range(CAPTCHA_NOISE_DOTS): - x = secrets.randbelow(CAPTCHA_IMAGE_WIDTH) - y = secrets.randbelow(CAPTCHA_IMAGE_HEIGHT) - draw.point((x, y), fill=get_random_color()) - - # 滤镜增强边缘 - image = image.filter(ImageFilter.EDGE_ENHANCE) - - return image, code - - -def get_client_ip(): - """获取客户端真实IP(支持代理)""" - from flask import request - if request.headers.get('X-Forwarded-For'): - return request.headers.get('X-Forwarded-For').split(',')[0].strip() - return request.remote_addr - - -# ============================================================ -# 对外接口 -# ============================================================ - -def generate_captcha(): - """生成验证码实例 - - Returns: - dict: { - "idKey": str, # 验证码唯一标识 - "data": str # base64图片数据 - } - """ - from flask import request - - # ============================================================ - # 防刷:滑动窗口计数,限制同一IP在窗口期内的请求次数 - # ============================================================ - client_ip = get_client_ip() - rate_key = f"captcha:rate:{client_ip}" - count = redis.get(rate_key) - - if count and int(count) >= CAPTCHA_RATE_LIMIT: - ttl = redis.ttl(rate_key) - remaining = ttl if ttl > 0 else CAPTCHA_RATE_WINDOW - raise Exception(f"请求过于频繁,请等待 {remaining} 秒后再试") - - # ============================================================ - # 生成验证码 - # ============================================================ - image, code = generate_image(CAPTCHA_LENGTH) - - # 转换为base64 - buffer = BytesIO() - image.save(buffer, format='PNG') - buf_bytes = buffer.getvalue() - data = base64.b64encode(buf_bytes).decode('ascii') - - # 生成唯一ID(使用secrets更安全) - id_key = secrets.token_urlsafe(24) - - # 存储验证码到Redis - redis.set(id_key, code, expire=CAPTCHA_EXPIRE) - - # 递增计数器,首次时设置过期时间(滑动窗口) - if count: - redis.incr(rate_key) - else: - redis.set(rate_key, 1, expire=CAPTCHA_RATE_WINDOW) - - return { - "idKey": id_key, - "data": data - } - - -def verify_captcha(id_key, user_input): - """验证验证码(验证成功后自动删除,一次性使用) - - Args: - id_key: 验证码ID - user_input: 用户输入的验证码 - - Returns: - bool: 验证是否通过 - """ - if not id_key or not user_input: - return False - - # 从Redis获取验证码 - stored_code = redis.get(id_key) - - if not stored_code: - return False - - # 转为大写比较(大小写不敏感) - stored_code = str(stored_code).upper().strip() - user_input = str(user_input).upper().strip() - - # 验证通过后删除(一次性使用,防止重放攻击) - if stored_code == user_input: - redis.delete(id_key) - return True - - return False +from src.common.utils.captcha import * # noqa: F401,F403 diff --git a/utils/case_converter.py b/utils/case_converter.py index cc286a3..928d12a 100644 --- a/utils/case_converter.py +++ b/utils/case_converter.py @@ -1,327 +1,8 @@ """ -命名格式转换工具模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -提供字符串命名格式之间的转换功能,主要用于数据库字段名(下划线命名) -与前端字段名(驼峰命名)之间的转换。 - -主要功能: -1. 下划线命名 → 驼峰命名 -2. 驼峰命名 → 下划线命名 -3. 下划线命名 → 帕斯卡命名(大驼峰) -4. 递归转换字典的所有键名 +命名转换工具已迁移至 src.common.utils.case_converter。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - - -def convert_camel_case(snake_str): - """ - 将下划线命名转换为驼峰命名 - - Args: - snake_str: 下划线命名字符串,如 'create_time' - - Returns: - 驼峰命名字符串,如 'createTime' - - Examples: - >>> convert_camel_case('create_time') - 'createTime' - >>> convert_camel_case('user_id') - 'userId' - >>> convert_camel_case('id') - 'id' - >>> convert_camel_case('') - '' - """ - if not snake_str: - return '' - - components = snake_str.split('_') - return components[0] + ''.join(_capitalize_first_letter(x) for x in components[1:]) - - -def convert_snake_case(camel_str): - """ - 将驼峰命名转换为下划线命名 - - Args: - camel_str: 驼峰命名字符串,如 'createTime' - - Returns: - 下划线命名字符串,如 'create_time' - - Examples: - >>> convert_snake_case('createTime') - 'create_time' - >>> convert_snake_case('userId') - 'user_id' - >>> convert_snake_case('id') - 'id' - >>> convert_snake_case('') - '' - """ - if not camel_str: - return '' - - result = [] - for i, char in enumerate(camel_str): - if char.isupper() and i > 0: - result.append('_') - result.append(char.lower()) - else: - result.append(char.lower()) - - return ''.join(result) - - -def convert_pascal_case(snake_str): - """ - 将下划线命名转换为帕斯卡命名(大驼峰) - - Args: - snake_str: 下划线命名字符串,如 'create_time' - - Returns: - 帕斯卡命名字符串,如 'CreateTime' - - Examples: - >>> convert_pascal_case('create_time') - 'CreateTime' - >>> convert_pascal_case('user_id') - 'UserId' - >>> convert_pascal_case('') - '' - """ - if not snake_str: - return '' - - components = snake_str.split('_') - return ''.join(_capitalize_first_letter(x) for x in components) - - -def convert_dict_keys(data, converter=convert_camel_case): - """ - 递归转换字典的所有键名 - - Args: - data: 需要转换的字典或列表 - converter: 转换函数,默认为 to_camel_case - - Returns: - 转换后的字典或列表 - - Examples: - >>> data = {'create_time': '2024-01-01', 'user_id': 1} - >>> convert_dict_keys(data) - {'createTime': '2024-01-01', 'userId': 1} - - >>> data = [{'user_name': '张三'}, {'user_name': '李四'}] - >>> convert_dict_keys(data) - [{'userName': '张三'}, {'userName': '李四'}] - """ - if isinstance(data, list): - return [convert_dict_keys(item, converter) for item in data] - - if isinstance(data, dict): - result = {} - for key, value in data.items(): - new_key = converter(key) - result[new_key] = convert_dict_keys(value, converter) - return result - - return data - - -def convert_snake_to_camel(data): - """ - 将字典中的下划线命名字段转换为驼峰命名(便捷方法) - - 这是 convert_dict_keys 的便捷封装,专门用于数据库字段转前端字段。 - - Args: - data: 需要转换的字典或列表 - - Returns: - 转换后的字典或列表(所有键名转为驼峰命名) - - Examples: - >>> data = {'create_time': '2024-01-01', 'user_id': 1} - >>> convert_dict_keys_snake_to_camel(data) - {'createTime': '2024-01-01', 'userId': 1} - - >>> data = [{'user_name': '张三'}, {'user_name': '李四'}] - >>> convert_dict_keys_snake_to_camel(data) - [{'userName': '张三'}, {'userName': '李四'}] - """ - return convert_dict_keys(data, converter=convert_camel_case) - - -def convert_camel_to_snake(data): - """ - 将字典中的驼峰命名字段转换为下划线命名(便捷方法) - - 这是 convert_dict_keys 的便捷封装,专门用于前端字段转数据库字段。 - - Args: - data: 需要转换的字典或列表 - - Returns: - 转换后的字典或列表(所有键名转为下划线命名) - - Examples: - >>> data = {'createTime': '2024-01-01', 'userId': 1} - >>> convert_dict_keys_camel_to_snake(data) - {'create_time': '2024-01-01', 'user_id': 1} - - >>> data = [{'userName': '张三'}, {'userName': '李四'}] - >>> convert_dict_keys_camel_to_snake(data) - [{'user_name': '张三'}, {'user_name': '李四'}] - """ - return convert_dict_keys(data, converter=convert_snake_case) - - -# ============================================================ -# 私有方法 -# ============================================================ - -def _capitalize_first_letter(s): - """ - 将字符串的首字母大写,其余字母小写 - - Args: - s: 输入字符串 - - Returns: - 首字母大写后的字符串 - - Examples: - >>> _capitalize_first_letter('name') - 'Name' - >>> _capitalize_first_letter('userName') - 'Username' - >>> _capitalize_first_letter('') - '' - """ - if not s: - return '' - return s[0].upper() + s[1:].lower() - - -def _is_camel_case(s): - """ - 判断字符串是否为驼峰命名格式 - - 驼峰命名特征: - 1. 首字母小写 - 2. 不包含下划线 - 3. 包含大写字母(至少一个大写字母) - - Args: - s: 输入字符串 - - Returns: - bool: 是否为驼峰命名格式 - - Examples: - >>> _is_camel_case('createTime') - True - >>> _is_camel_case('create_time') - False - >>> _is_camel_case('id') - False - """ - if not s: - return False - - # 首字母必须小写,不包含下划线,且至少有一个大写字母 - return (s[0].islower() and - '_' not in s and - any(char.isupper() for char in s)) - - -def _is_snake_case(s): - """ - 判断字符串是否为下划线命名格式 - - 下划线命名特征: - 1. 全部小写 - 2. 包含下划线 - 3. 不以_开头,不以_结尾 - - Args: - s: 输入字符串 - - Returns: - bool: 是否为下划线命名格式 - - Examples: - >>> _is_snake_case('create_time') - True - >>> _is_snake_case('createTime') - False - >>> _is_snake_case('id') - False - """ - if not s: - return False - - # 全部小写,包含下划线,且不以_开头结尾 - return (s.islower() and - '_' in s and - not s.startswith('_') and - not s.endswith('_')) - - -def _is_pascal_case(s): - """ - 判断字符串是否为帕斯卡命名格式(大驼峰) - - 帕斯卡命名特征: - 1. 首字母大写 - 2. 不包含下划线 - 3. 至少有一个字母 - - Args: - s: 输入字符串 - - Returns: - bool: 是否为帕斯卡命名格式 - - Examples: - >>> _is_pascal_case('CreateTime') - True - >>> _is_pascal_case('createTime') - False - >>> _is_pascal_case('create_time') - False - """ - if not s: - return False - - # 首字母大写,不包含下划线 - return (s[0].isupper() and - '_' not in s and - any(char.isalpha() for char in s)) - - -def _convert_single_field(field_name, converter=convert_camel_case): - """ - 转换单个字段名 - - Args: - field_name: 字段名 - converter: 转换函数,默认为 to_camel_case - - Returns: - 转换后的字段名 - - Examples: - >>> _convert_single_field('create_time') - 'createTime' - >>> _convert_single_field('createTime', convert_snake_case) - 'create_time' - """ - if not field_name: - return '' - - return converter(field_name) +from src.common.utils.case_converter import * # noqa: F401,F403 diff --git a/utils/file.py b/utils/file.py index 956565c..b134991 100644 --- a/utils/file.py +++ b/utils/file.py @@ -1,407 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 珊瑚云团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -文件目录操作工具模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块提供文件和目录操作的常用工具函数。 -主要用于处理文件上传、目录创建、路径管理等场景,确保文件系统操作的 -安全性和可靠性。包含目录创建、文件操作等基础功能。 - -主要功能: -1. 递归创建目录(mkdir)- 确保目标目录存在 -2. 文件路径处理 -3. 目录存在性检查 - -所属包: utils -所在文件: utils/file_utils.py -依赖模块: os - 操作系统接口模块 +文件操作工具已迁移至 src.common.utils.file。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -import os - -logger = logging.getLogger(__name__) - - -# 常见文件类型的魔术字节签名 -# 格式:扩展名 -> [签名列表],签名为 (offset, bytes) 元组 -MAGIC_SIGNATURES = { - # 图片 - '.jpg': [(0, b'\xff\xd8\xff')], - '.jpeg': [(0, b'\xff\xd8\xff')], - '.png': [(0, b'\x89PNG\r\n\x1a\n')], - '.gif': [(0, b'GIF87a'), (0, b'GIF89a')], - '.bmp': [(0, b'BM')], - '.webp': [(0, b'RIFF')], - '.ico': [(0, b'\x00\x00\x01\x00'), (0, b'\x00\x00\x02\x00')], - # 文档 - '.pdf': [(0, b'%PDF')], - '.zip': [(0, b'PK\x03\x04')], - '.rar': [(0, b'Rar!\x1a\x07')], - '.7z': [(0, b'7z\xbc\xaf\x27\x1c')], - '.mp3': [(0, b'\xff\xfb'), (0, b'\xff\xf3'), (0, b'\xff\xf2'), (0, b'ID3')], - '.mp4': [(4, b'ftyp')], - # SVG 是文本格式,无固定二进制签名 -} - - -def verify_file_magic(file_stream, ext_with_dot): - """ - 校验文件魔术字节,验证文件实际内容与扩展名是否一致 - - Args: - file_stream: Flask上传文件对象(FileStorage),需支持seek/read - ext_with_dot: 带点的扩展名,如 '.jpg' - - Returns: - tuple: (is_valid, message) - - is_valid (bool): 是否通过校验 - - message (str): 提示信息 - """ - signatures = MAGIC_SIGNATURES.get(ext_with_dot) - if not signatures: - # 无签名规则的文件类型,跳过魔术字节校验 - return True, '' - - try: - # 读取文件头部字节用于校验 - header = file_stream.read(32) - file_stream.seek(0) - - if not header: - return False, '文件内容为空' - - for sig in signatures: - if isinstance(sig, tuple): - offset, sig_bytes = sig - if header[offset:offset + len(sig_bytes)] == sig_bytes: - return True, '' - elif header.startswith(sig): - return True, '' - - return False, f'文件内容与扩展名 {ext_with_dot} 不匹配' - - except Exception as e: - logger.error(f'魔术字节校验异常: {e}') - # 读取异常时不阻断上传,降级为仅扩展名校验 - return True, '' - - -def mkdir(path): - """ - 递归创建目录 - ============================================================= - - 检查指定路径的目录是否存在,如果不存在则递归创建所有必需的目录。 - 用于确保文件上传等操作前目标目录已存在。 - - 参数: - path (str): 需要创建/检查的目录路径 - - 返回: - bool: True - 目录创建成功或已存在 - False - 目录创建失败(保留扩展用) - - 使用示例: - >>> # 确保上传目录存在 - >>> upload_dir = '/data/uploads/2024/01' - >>> mkdir(upload_dir) - /data/uploads/2024/01 创建成功 - True - - 注意事项: - 1. 函数会自动创建多级不存在的目录 - 2. 如果目录已存在,不会重复创建 - 3. 需要有相应的文件系统权限 - """ - - # 去除首位空格 - # 说明:消除用户输入中可能包含的前后空格,避免路径错误 - path = path.strip() - - # 去除尾部 \ 符号 - # 说明:Windows系统路径可能以\结尾,去除后保持路径格式统一 - path = path.rstrip("\\") - - # 判断路径是否存在 - # 函数:os.path.exists(path) - # 说明:检查指定路径的文件或目录是否存在 - isExists = os.path.exists(path) - - if not isExists: - # 如果不存在则创建目录 - # 函数:os.makedirs(path) - # 说明:递归创建目录,如果中间目录不存在也会一并创建 - # 例如:创建 /a/b/c 时,如果 /a 和 /a/b 都不存在,会一并创建 - os.makedirs(path) - logger.debug(f'{path} 创建成功') - return True - else: - logger.debug(f'{path} 目录已存在') - return False - - -# ======================= 扩展:常用文件操作工具类 ========================== - -class FileUtils: - """ - 文件操作工具类 - ============================================================= - - 提供更多常用的文件和目录操作功能,封装为类方法便于调用。 - 包含文件读写、路径处理、文件信息获取等实用功能。 - - 使用示例: - >>> # 获取文件大小 - >>> size = FileUtils.get_file_size('/path/file.txt') - >>> print(f"{size} bytes") - - >>> # 安全删除文件 - >>> if FileUtils.safe_delete('/path/file.txt'): - ... print("文件删除成功") - """ - - @staticmethod - def get_file_size(file_path): - """ - 获取文件大小 - ============================================================= - - 参数: - file_path (str): 文件路径 - - 返回: - int: 文件大小(字节),如果文件不存在返回0 - """ - if os.path.exists(file_path) and os.path.isfile(file_path): - return os.path.getsize(file_path) - return 0 - - @staticmethod - def get_file_extension(filename): - """ - 获取文件扩展名 - ============================================================= - - 参数: - filename (str): 文件名或文件路径 - - 返回: - str: 文件扩展名(小写),例如:'.jpg', '.txt' - """ - import os - return os.path.splitext(filename)[1].lower() - - @staticmethod - def get_file_name_without_ext(filename): - """ - 获取不带扩展名的文件名 - ============================================================= - - 参数: - filename (str): 文件名或文件路径 - - 返回: - str: 不带扩展名的文件名 - """ - import os - return os.path.splitext(os.path.basename(filename))[0] - - @staticmethod - def safe_delete(file_path): - """ - 安全删除文件 - ============================================================= - - 参数: - file_path (str): 文件路径 - - 返回: - bool: True - 删除成功或文件不存在 - False - 删除失败(如权限不足) - """ - try: - if os.path.exists(file_path) and os.path.isfile(file_path): - os.remove(file_path) - return True - except Exception as e: - logger.warning(f"文件删除失败: {e}") - return False - - @staticmethod - def read_file(file_path, encoding='utf-8'): - """ - 读取文件内容 - ============================================================= - - 参数: - file_path (str): 文件路径 - encoding (str): 文件编码,默认utf-8 - - 返回: - str/None: 文件内容字符串,读取失败返回None - """ - try: - with open(file_path, 'r', encoding=encoding) as f: - return f.read() - except Exception as e: - logger.warning(f"文件读取失败: {e}") - return None - - @staticmethod - def write_file(file_path, content, encoding='utf-8', mode='w'): - """ - 写入文件内容 - ============================================================= - - 参数: - file_path (str): 文件路径 - content (str): 要写入的内容 - encoding (str): 文件编码,默认utf-8 - mode (str): 写入模式,'w'覆盖写入,'a'追加写入 - - 返回: - bool: True - 写入成功,False - 写入失败 - """ - try: - # 确保目录存在 - directory = os.path.dirname(file_path) - if directory and not os.path.exists(directory): - os.makedirs(directory) - - with open(file_path, mode, encoding=encoding) as f: - f.write(content) - return True - except Exception as e: - logger.warning(f"文件写入失败: {e}") - return False - - @staticmethod - def list_files(directory, pattern=None): - """ - 列出目录下的所有文件 - ============================================================= - - 参数: - directory (str): 目录路径 - pattern (str): 文件匹配模式,如 '*.txt',None表示所有文件 - - 返回: - list: 文件名列表 - """ - import glob - - if not os.path.exists(directory): - return [] - - if pattern: - search_path = os.path.join(directory, pattern) - return [os.path.basename(f) for f in glob.glob(search_path)] - else: - return [f for f in os.listdir(directory) - if os.path.isfile(os.path.join(directory, f))] - - @staticmethod - def get_file_info(file_path): - """ - 获取文件详细信息 - ============================================================= - - 参数: - file_path (str): 文件路径 - - 返回: - dict: 包含文件信息的字典,文件不存在返回None - """ - if not os.path.exists(file_path) or not os.path.isfile(file_path): - return None - - stat = os.stat(file_path) - return { - 'name': os.path.basename(file_path), - 'path': os.path.abspath(file_path), - 'size': stat.st_size, - 'size_format': FileUtils._format_size(stat.st_size), - 'extension': FileUtils.get_file_extension(file_path), - 'created_time': stat.st_ctime, - 'modified_time': stat.st_mtime, - 'is_hidden': os.path.basename(file_path).startswith('.') - } - - @staticmethod - def _format_size(size_bytes): - """ - 格式化文件大小显示 - ============================================================= - - 参数: - size_bytes (int): 文件大小(字节) - - 返回: - str: 格式化后的大小字符串,如 '1.5 MB' - """ - if size_bytes < 1024: - return f"{size_bytes} B" - elif size_bytes < 1024 * 1024: - return f"{size_bytes / 1024:.2f} KB" - elif size_bytes < 1024 * 1024 * 1024: - return f"{size_bytes / (1024 * 1024):.2f} MB" - else: - return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB" - - -# ======================= 便捷函数封装 ========================== - -def get_file_size(file_path): - """获取文件大小(字节)的便捷函数""" - return FileUtils.get_file_size(file_path) - - -def safe_delete(file_path): - """安全删除文件的便捷函数""" - return FileUtils.safe_delete(file_path) - - -def get_file_extension(filename): - """获取文件扩展名的便捷函数""" - return FileUtils.get_file_extension(filename) +from src.common.utils.file import * # noqa: F401,F403 diff --git a/utils/formatter.py b/utils/formatter.py index 3c24e21..34ce01f 100644 --- a/utils/formatter.py +++ b/utils/formatter.py @@ -1,107 +1,8 @@ """ -日志格式化工具模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -提供请求参数和返回结果的格式化工具,用于操作日志存储: -- format_params: 格式化请求参数(过滤敏感字段) -- format_result: 格式化返回结果 -- truncate_json: 截断过长的JSON字符串 +格式化工具已迁移至 src.common.utils.formatter。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import json -import logging - -logger = logging.getLogger(__name__) - - -def format_params(params) -> str: - """ - 格式化请求参数,用于日志存储 - - 将请求参数字典转换为JSON字符串,并过滤敏感信息。 - - Args: - params: 请求参数(字典或其他类型) - - Returns: - 格式化后的JSON字符串 - """ - 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] = '******' - return json.dumps(params_copy, ensure_ascii=False) - return json.dumps(params, ensure_ascii=False) - except Exception: - return str(params)[:500] - - -def format_result(result): - """ - 格式化返回结果 - - 将返回结果转换为适合存储的字符串格式,支持JsonResponse、字典等类型。 - - Args: - result: 返回结果 - - Returns: - 格式化后的结果字符串 - """ - if not result: - return '' - - try: - if hasattr(result, 'content') and result.content: - content = result.content.decode('utf-8') - if content: - try: - data = json.loads(content) - result_str = json.dumps(data, ensure_ascii=False) - except Exception: - 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: - 截断后的字符串 - """ - if not json_str: - return '' - if len(json_str) <= max_len: - return json_str - return json_str[:max_len] + '...' +from src.common.utils.formatter import * # noqa: F401,F403 diff --git a/utils/ip.py b/utils/ip.py index 039ee66..8e0240f 100644 --- a/utils/ip.py +++ b/utils/ip.py @@ -1,55 +1,8 @@ """ -IP地址工具模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -提供客户端IP获取和地理位置查询: -- get_client_ip: 获取客户端真实IP(需配合Flask ProxyFix使用) -- get_ip_location: 调用第三方API查询IP地理位置 +IP 解析工具已迁移至 src.common.utils.ip。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import json -import logging -import urllib.request - -logger = logging.getLogger(__name__) - - -def get_client_ip(): - """ - 获取客户端真实IP地址 - - 通过Flask ProxyFix中间件由PROXY_NUM配置自动设置 - request.remote_addr为真实客户端IP。未配置ProxyFix时, - request.remote_addr即为直连IP,避免客户端伪造。 - - Returns: - str: 客户端IP地址 - """ - from flask import request - - return request.remote_addr - - -def get_ip_location(ip: str) -> str: - """ - 根据IP获取地理位置(使用第三方API) - - Args: - ip: IP地址 - - Returns: - 地理位置信息,格式:"国家 省份 城市" - """ - if ip in ['127.0.0.1', 'localhost'] or ip.startswith('192.168.') or ip.startswith('10.'): - return "内网IP" - - try: - 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 "未知" +from src.common.utils.ip import * # noqa: F401,F403 diff --git a/utils/ip2region.py b/utils/ip2region.py index 2669990..8661abd 100644 --- a/utils/ip2region.py +++ b/utils/ip2region.py @@ -1,179 +1,8 @@ """ -IP地理位置查询模块(基于ip2region离线数据库) +兼容 shim(绞杀者迁移期间保留) ============================================================= -基于 ip2region.xdb 离线数据库提供高性能IP地理位置查询,无需网络请求。 - -核心功能: -- get_ip_region: 根据IP获取完整区域信息字典 -- get_ip_location: 根据IP获取地理位置文本(兼容现有调用) -- get_ip_country / get_ip_province / get_ip_city / get_ip_isp: 按层级获取 - -xdb文件位于 utils/ip2region.xdb,启动时全量加载到内存,查询零IO延迟。 +IP 地理位置查询已迁移至 src.common.utils.ip2region。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -import os - -from ip2region import searcher, util - -logger = logging.getLogger(__name__) - -# xdb数据库文件路径(与本模块同目录) -_XDB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ip2region.xdb") - -# 模块级单例:启动时加载xdb到内存,后续查询零IO -_searcher = None - - -def _get_searcher(): - """获取搜索器单例(懒加载,首次调用时初始化)""" - global _searcher - if _searcher is not None: - return _searcher - - if not os.path.exists(_XDB_PATH): - logger.error(f"ip2region数据库文件不存在: {_XDB_PATH}") - return None - - try: - c_buff = util.load_content_from_file(_XDB_PATH) - _searcher = searcher.new_with_buffer(util.IPv4, c_buff) - logger.info(f"ip2region数据库加载成功: {_XDB_PATH}") - except Exception as e: - logger.error(f"ip2region数据库加载失败: {e}") - return None - - return _searcher - - -def _parse_region(region_str): - """ - 解析ip2region原始结果字符串 - - 原始格式: "国家|省份|城市|ISP|国家代码" - 示例: "中国|浙江省|杭州市|阿里|CN", "United States|California|0|Google LLC|US" - - Args: - region_str: ip2region返回的原始字符串 - - Returns: - dict: 各层级区域信息 - """ - parts = region_str.split("|") if region_str else [] - # 补齐到5段 - while len(parts) < 5: - parts.append("0") - - country = parts[0] - province = parts[1] - city = parts[2] - isp = parts[3] - country_code = parts[4] - - # 清理占位符 "0" → 空字符串 - province = "" if province == "0" else province - city = "" if city == "0" else city - isp = "" if isp == "0" else isp - country_code = "" if country_code == "0" else country_code - - return { - "country": country, - "province": province, - "city": city, - "isp": isp, - "country_code": country_code, - } - - -def _is_private_ip(ip): - """判断是否为内网/保留IP""" - if not ip: - return True - ip = ip.strip() - if ip in ("127.0.0.1", "localhost", "0.0.0.0"): - return True - if ip.startswith("10.") or ip.startswith("192.168."): - return True - if ip.startswith("172."): - parts = ip.split(".") - if len(parts) >= 2: - second = int(parts[1]) - if 16 <= second <= 31: - return True - return False - - -def get_ip_region(ip): - """ - 根据IP获取完整区域信息字典 - - Args: - ip: IP地址字符串 - - Returns: - dict: 区域信息,包含 country/province/city/isp/country_code 字段。 - 内网IP返回 {"country": "内网IP", ...},查询失败返回 {"country": "未知", ...} - """ - if _is_private_ip(ip): - return {"country": "内网IP", "province": "", "city": "", "isp": "", "country_code": ""} - - s = _get_searcher() - if s is None: - return {"country": "未知", "province": "", "city": "", "isp": "", "country_code": ""} - - try: - region_str = s.search(ip) - if not region_str: - return {"country": "未知", "province": "", "city": "", "isp": "", "country_code": ""} - return _parse_region(region_str) - except Exception as e: - logger.warning(f"IP地理位置查询失败 [{ip}]: {e}") - return {"country": "未知", "province": "", "city": "", "isp": "", "country_code": ""} - - -def get_ip_location(ip): - """ - 根据IP获取地理位置文本描述 - - 格式: "国家 省份 城市"(与原 utils/ip.py 的 get_ip_location 返回格式兼容) - 内网IP返回 "内网IP",查询失败返回 "未知" - - Args: - ip: IP地址字符串 - - Returns: - str: 地理位置文本 - """ - region = get_ip_region(ip) - - if region["country"] in ("内网IP", "未知"): - return region["country"] - - parts = [region["country"]] - if region["province"]: - parts.append(region["province"]) - if region["city"] and region["city"] != region["province"]: - parts.append(region["city"]) - - return " ".join(parts) - - -def get_ip_country(ip): - """根据IP获取国家""" - return get_ip_region(ip)["country"] - - -def get_ip_province(ip): - """根据IP获取省份""" - return get_ip_region(ip)["province"] - - -def get_ip_city(ip): - """根据IP获取城市""" - return get_ip_region(ip)["city"] - - -def get_ip_isp(ip): - """根据IP获取运营商""" - return get_ip_region(ip)["isp"] +from src.common.utils.ip2region import * # noqa: F401,F403 diff --git a/utils/json.py b/utils/json.py index 4a542dd..3f49891 100644 --- a/utils/json.py +++ b/utils/json.py @@ -1,91 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 一米阳光团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 一米阳光团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -JSON 序列化模块 -自定义 JSON 编码器,处理日期时间等特殊类型的序列化 +兼容 shim(绞杀者迁移期间保留) +============================================================= + +JSON 工具已迁移至 src.common.utils.json。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -from datetime import date, datetime -from flask.json.provider import DefaultJSONProvider - - -class JSONEncoder(DefaultJSONProvider): - """ - 自定义 JSON 编码器 - - 功能说明: - 扩展 Flask 默认的 JSON 序列化器,支持以下类型的格式化输出: - 1. datetime 类型 → 'YYYY-MM-DD HH:MM:SS' 格式 - 2. date 类型 → 'YYYY-MM-DD' 格式 - - 使用方式: - app.json = JSONEncoder(app) - """ - - def default(self, o): - """ - 处理无法被默认 JSON 序列化的对象 - - Args: - o: 需要序列化的对象 - - Returns: - 序列化后的值(字符串或其他可序列化类型) - """ - # ============================================================ - # 处理日期时间类型(datetime) - # ============================================================ - # 将 datetime 对象格式化为:2024-01-15 14:30:00 - if isinstance(o, datetime): - return o.strftime('%Y-%m-%d %H:%M:%S') - - # ============================================================ - # 处理日期类型(date) - # ============================================================ - # 将 date 对象格式化为:2024-01-15 - if isinstance(o, date): - return o.strftime('%Y-%m-%d') - - # ============================================================ - # 其他类型交给父类处理 - # ============================================================ - return super().default(o) +from src.common.utils.json import * # noqa: F401,F403 diff --git a/utils/jwt.py b/utils/jwt.py index 4842314..a3cf852 100644 --- a/utils/jwt.py +++ b/utils/jwt.py @@ -1,634 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 珊瑚云团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -JWT令牌处理模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块提供基于JWT(JSON Web Token)的令牌生成和验证功能。 -主要用于用户登录认证,生成访问令牌,并在后续请求中验证令牌的有效性。 -支持令牌过期机制,确保系统的安全性。 - -主要功能: -1. JWT令牌生成(create_token)- 用于用户登录成功后生成访问令牌 -2. JWT令牌解析验证(parse_payload)- 验证请求中的令牌有效性 -3. 统一的异常处理,返回标准格式的验证结果 -4. 可配置的令牌过期时间 - -所属包: utils -所在文件: utils/jwts.py -依赖模块: jwt - JWT处理第三方库 - datetime - 时间处理模块 - os - 环境变量读取模块 - secrets - 安全随机数生成模块 +JWT 工具已迁移至 src/core/jwt.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import datetime -import hashlib -import logging - -import jwt -from flask import request -from jwt import exceptions - -from config import JWT_SALT, JWT_EXPIRE_MINUTES - -# 配置日志记录器 -logger = logging.getLogger(__name__) - - -# ============================================================= -# JWT密钥配置 -# ============================================================= -# 从 config.auth 统一读取,密钥配置在 .env 文件中管理 - -def _validate_key_strength(key: str) -> None: - """ - 启动时校验 JWT 密钥强度 - - 校验规则: - 1. 密钥不能为空 - 2. 密钥长度至少 32 字节(256 位) - - 校验失败直接抛出 RuntimeError,拒绝启动应用。 - """ - if not key: - raise RuntimeError( - "JWT_SALT 未配置!请在 .env 中设置至少32字节的强随机密钥。\n" - "生成方式:python -c \"import secrets; print(secrets.token_urlsafe(48))\"" - ) - - key_bytes = len(key.encode('utf-8')) - if key_bytes < 32: - raise RuntimeError( - f"JWT_SALT 长度不足:{key_bytes} 字节,至少需要 32 字节(256位)。\n" - "生成方式:python -c \"import secrets; print(secrets.token_urlsafe(48))\"" - ) - - logger.info(f"JWT密钥校验通过,长度:{key_bytes}字节") - - -# 启动时校验密钥强度 -_validate_key_strength(JWT_SALT) - -# JWT算法配置 -JWT_ALGORITHM = 'HS256' - -# 默认令牌过期时间(分钟),从配置读取 -DEFAULT_TIMEOUT_MINUTES = JWT_EXPIRE_MINUTES - - -# ============================================================= -# JWT核心功能函数 -# ============================================================= - -def create_token(payload, timeout=None): - """ - 生成JWT令牌 - ============================================================= - - 根据传入的载荷数据生成JWT访问令牌,自动添加过期时间。 - 使用强密钥确保令牌安全性,符合RFC 7518安全规范。 - - 参数: - payload (dict): JWT载荷数据,通常包含用户ID、用户名等信息 - timeout (int): 令牌过期时间(分钟),默认使用DEFAULT_TIMEOUT_MINUTES - - 返回: - str: 生成的JWT令牌字符串 - - 使用示例: - >>> payload = {'userId': 1001, 'username': 'admin'} - >>> token = create_token(payload, timeout=30) - >>> print(token) - eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9... - - 注意事项: - 1. 密钥长度必须至少32字节,否则会发出警告 - 2. 生产环境建议通过环境变量设置JWT_SALT - 3. 令牌过期后需要重新登录获取新令牌 - """ - try: - # 设置过期时间 - # 说明:在载荷中添加exp字段,指定令牌的过期时间点 - # 计算方式:当前UTC时间 + 指定的分钟数 - if timeout is None: - timeout = DEFAULT_TIMEOUT_MINUTES - - # 创建payload副本,避免修改原始数据 - payload_copy = payload.copy() - - # 添加过期时间(exp)和签发时间(iat) - # exp: 过期时间,超过此时间令牌无效 - # iat: 签发时间,用于令牌生命周期计算 - current_time = datetime.datetime.now(tz=datetime.timezone.utc) - payload_copy['exp'] = current_time + datetime.timedelta(minutes=timeout) - payload_copy['iat'] = current_time - - # 声明类型和加密算法 - # 说明:定义JWT头部信息,包含令牌类型和加密算法 - headers = { - "typ": "JWT", # 令牌类型 - "alg": JWT_ALGORITHM # 加密算法 - } - - # 生成JWT令牌 - # 函数:jwt.encode() - # 参数说明: - # payload: JWT载荷数据 - # key: 加密密钥(盐) - # algorithm: 加密算法(HS256) - # headers: JWT头部信息 - token = jwt.encode( - payload=payload_copy, - key=JWT_SALT, - algorithm=JWT_ALGORITHM, - headers=headers - ) - - # 记录日志(可选,生产环境建议使用DEBUG级别) - logger.debug(f"生成JWT令牌成功,过期时间:{timeout}分钟") - - return token - - except Exception as e: - # 捕获并记录生成令牌时的异常 - logger.error(f"生成JWT令牌失败: {str(e)}") - raise - - -def parse_payload(token): - """ - 解析验证JWT令牌 - ============================================================= - - 验证传入的JWT令牌,检查签名有效性和过期状态。 - 统一处理各种异常情况,返回标准格式的结果对象。 - - 参数: - token (str): 需要验证的JWT令牌字符串 - - 返回: - dict: 验证结果,包含以下字段: - code (int): 状态码,0-成功,-1-失败 - data (dict/None): 验证成功时返回载荷数据,失败时为None - msg (str): 提示信息 - - 使用示例: - >>> result = parse_payload(token) - >>> if result['code'] == 0: - ... user_id = result['data']['userId'] - ... username = result['data']['username'] - ... else: - ... error_msg = result['msg'] - ... # 处理令牌失效,跳转登录页 - - 异常处理说明: - - ExpiredSignatureError: 令牌已过期,需要重新登录 - - DecodeError: 解码失败,令牌格式错误或签名无效 - - InvalidTokenError: 其他JWT相关异常 - """ - # 初始化返回结果 - # 说明:定义统一返回格式,默认成功状态 - result = { - "code": 0, - "data": None, - "msg": "操作成功" - } - - # 参数校验 - if not token: - result['code'] = -1 - result['msg'] = "token不能为空" - return result - - try: - # 解密验证JWT令牌 - # 函数:jwt.decode() - # 参数说明: - # jwt: 令牌字符串 - # key: 解密密钥(必须与加密时使用的密钥一致) - # algorithms: 允许的算法列表 - # - # 说明:jwt.decode会自动完成以下验证: - # 1. 签名验证 - 确保令牌未被篡改 - # 2. 过期时间验证 - 检查exp字段是否大于当前时间 - # 3. 算法验证 - 确保使用正确的加密算法 - verified_payload = jwt.decode( - token, - JWT_SALT, - algorithms=[JWT_ALGORITHM] - ) - - # 验证成功,将载荷数据存入返回结果 - # 注意:返回的数据中不包含敏感信息(如密码等) - result['data'] = verified_payload - - # 可选:记录验证成功的日志(生产环境建议使用DEBUG级别) - logger.debug(f"JWT令牌验证成功,用户: {verified_payload.get('username', 'unknown')}") - - except exceptions.ExpiredSignatureError as e: - # 令牌已过期异常处理 - # 说明:令牌超过设置的exp时间,需要重新登录获取新令牌 - result['code'] = -1 - result['msg'] = "token已失效,请重新登录" - logger.info(f"JWT令牌已过期: {str(e)}") - - except exceptions.DecodeError as e: - # 令牌解码失败异常处理 - # 说明:令牌格式错误或签名验证失败,可能被篡改 - result['code'] = -1 - result['msg'] = "token认证失败,无效的令牌格式" - logger.warning(f"JWT令牌解码失败: {str(e)}") - - except exceptions.InvalidTokenError as e: - # 无效令牌异常处理 - # 说明:捕获其他所有JWT相关异常,如算法不匹配等 - result['code'] = -1 - result['msg'] = "非法的token,请检查令牌有效性" - logger.warning(f"无效的JWT令牌: {str(e)}") - - except Exception as e: - # 其他未预期的异常处理 - result['code'] = -1 - result['msg'] = "token验证失败,系统异常" - logger.error(f"JWT验证系统异常: {str(e)}") - - return result - - -def refresh_token(token, extend_minutes=None): - """ - 刷新JWT令牌 - ============================================================= - - 验证现有令牌有效性,如果有效则生成新的令牌,延长有效期。 - - 参数: - token (str): 需要刷新的JWT令牌字符串 - extend_minutes (int): 延长的分钟数,默认使用DEFAULT_TIMEOUT_MINUTES - - 返回: - dict: 刷新结果,包含以下字段: - code (int): 状态码,0-成功,-1-失败 - data (dict/None): 成功时包含新令牌,失败时为None - msg (str): 提示信息 - - 使用示例: - >>> result = refresh_token(old_token, extend_minutes=30) - >>> if result['code'] == 0: - ... new_token = result['data']['access_token'] - ... else: - ... # 令牌无效,需要重新登录 - ... error_msg = result['msg'] - - 注意事项: - 1. 令牌只有在有效期内才能刷新 - 2. 刷新后会生成全新的令牌,旧令牌立即失效 - 3. 建议在令牌剩余有效期少于一半时进行刷新 - """ - if extend_minutes is None: - extend_minutes = DEFAULT_TIMEOUT_MINUTES - - # 先验证原令牌 - result = parse_payload(token) - - # 如果验证失败,返回错误信息 - if result['code'] != 0: - return { - "code": -1, - "data": None, - "msg": f"令牌无效,无法刷新: {result['msg']}" - } - - try: - # 获取原载荷数据(不包含exp、iat等JWT标准字段) - payload = result['data'] - - # 移除JWT标准字段,避免重复添加 - # 注意:保留原始业务数据,移除系统字段 - clean_payload = {k: v for k, v in payload.items() - if k not in ['exp', 'iat', 'nbf']} - - # 生成新令牌 - new_token = create_token(clean_payload, timeout=extend_minutes) - - logger.info(f"JWT令牌刷新成功,延长{extend_minutes}分钟") - - return { - "code": 0, - "data": { - "access_token": new_token, - "expires_in": extend_minutes * 60 # 转换为秒 - }, - "msg": "令牌刷新成功" - } - - except Exception as e: - logger.error(f"刷新JWT令牌失败: {str(e)}") - return { - "code": -1, - "data": None, - "msg": "令牌刷新失败,请重新登录" - } - - -def get_token_expiration(token): - """ - 获取JWT令牌的过期时间 - - 验证签名后读取过期时间,确保token未被篡改。 - 不过验证过期状态(verify_exp=False),以便调用方 - 读取已过期token的过期时间用于剩余时间计算等场景。 - - Args: - token (str): JWT令牌字符串 - - Returns: - datetime/None: 令牌过期时间(UTC),解析失败返回None - """ - try: - # 验证签名确保token未被篡改,但不验证过期状态 - payload = jwt.decode( - token, - JWT_SALT, - algorithms=[JWT_ALGORITHM], - options={"verify_exp": False} - ) - - exp_timestamp = payload.get('exp') - if exp_timestamp: - return datetime.datetime.fromtimestamp(exp_timestamp, tz=datetime.timezone.utc) - return None - - except Exception as e: - logger.warning(f"获取JWT过期时间失败: {str(e)}") - return None - - -def get_access_token(): - """ - 从请求头中提取 access_token - ============================================================= - - 从 HTTP 请求的 Authorization 头中提取 JWT token,返回纯净的 token 字符串。 - 统一处理 token 提取逻辑,便于在其他模块中复用。 - - 参数: - request: Django 请求对象 - - 返回: - tuple: (success, token, message) - - success (bool): 是否成功获取 token - - token (str/None): token 字符串,失败时为 None - - message (str): 提示信息 - - 使用示例: - >>> success, token, msg = get_access_token() - >>> if success: - ... # 使用 token 进行后续处理 - ... parse_result = parse_payload(token) - ... else: - ... # 处理错误 - ... return R.failed(msg) - """ - try: - # 从请求头中获取 token 值 - # 说明:Authorization 头格式通常为 "Bearer " - auth_header = request.headers.get('Authorization', '') - if not auth_header: - return False, None, '缺少 Authorization 头' - - # 处理 Bearer 前缀 - # 说明:移除 "Bearer " 前缀并去除首尾空格,获取纯净 token - access_token = auth_header.replace('Bearer ', "").strip() - if not access_token: - return False, None, 'token 为空' - - return True, access_token, 'success' - - except Exception as e: - logger.error(f"获取 access_token 异常: {str(e)}") - return False, None, f'token 提取异常: {str(e)}' - - -# token解析工具类 -def parse_token(request): - """ - 解析请求头中的token,返回解析结果 - ============================================================= - - 从HTTP请求的Authorization头中提取JWT token,进行解析验证, - 并返回包含用户信息的结构化数据。统一处理各种异常情况。 - - 参数: - request: Django请求对象 - - 返回: - dict: 包含解析结果和用户信息的字典 - { - 'code': 0, # 0-成功,-1-失败 - 'msg': 'success', # 提示信息 - 'data': { - 'userId': 0, # 用户ID - 'username': '', # 用户名 - 'realname': '' # 真实姓名 - } - } - - 使用示例: - >>> result = parse_token(request) - >>> if result['code'] == 0: - ... user_id = result['data']['userId'] - ... username = result['data']['username'] - """ - # 初始化返回结果 - # 说明:设置默认失败状态,确保返回结构完整性 - result = { - 'code': -1, - 'msg': 'token无效', - 'data': { - 'userId': 0, - 'userName': None - } - } - - try: - # 获取 access_token - success, access_token, msg = get_access_token() - if not success: - result['msg'] = msg - return result - - # JWT解密 - # 函数:parse_payload(access_token) - # 说明:调用JWT解析工具验证token有效性 - parse_result = parse_payload(access_token) - - # 验证解析结果 - # 说明:确保解析结果格式正确 - if not isinstance(parse_result, dict): - result['msg'] = '解析结果格式错误' - return result - - # 结果标识 - # 说明:code为0表示验证通过 - code = parse_result.get('code', -1) - if code != 0: - result['msg'] = parse_result.get('msg', 'token验证失败') - return result - - # 获取数据部分 - # 说明:提取token载荷中的用户数据 - data = parse_result.get('data', {}) - if not isinstance(data, dict): - result['msg'] = '数据格式错误' - return result - - # 提取用户信息 - # 说明:从载荷中获取具体用户字段 - user_id = data.get('userId', 0) - username = data.get('username', '') - realname = data.get('realname', '') - - # 更新返回结果 - # 说明:设置成功状态并填充用户信息 - result['code'] = 0 - result['msg'] = 'success' - result['data']['userId'] = user_id - result['data']['username'] = username - result['data']['realname'] = realname - - except Exception as e: - # 异常捕获 - # 说明:防止未处理的异常导致系统崩溃 - result['msg'] = f'token解析异常: {str(e)}' - # 可以在这里记录日志 - # logger.error(f"token解析异常: {str(e)}") - - return result - - -# ============================================================= -# Token黑名单功能 -# ============================================================= - -# 黑名单Key前缀,用于Redis中存储被注销的Token -_TOKEN_BLACKLIST_PREFIX = 'token:blacklist:' - - -def _token_fingerprint(token: str) -> str: - """ - 生成Token指纹(SHA256摘要) - - 避免将完整Token存入Redis,仅存储摘要值,减少泄露风险。 - - Args: - token: JWT令牌字符串 - - Returns: - str: SHA256十六进制摘要 - """ - return hashlib.sha256(token.encode('utf-8')).hexdigest() - - -def add_token_to_blacklist(token: str) -> bool: - """ - 将Token加入黑名单 - - 退出登录时调用,使Token立即失效。黑名单条目的TTL与Token剩余有效期一致, - Token自然过期后黑名单条目自动清除,避免Redis内存无限增长。 - - Args: - token: JWT令牌字符串 - - Returns: - bool: 是否成功加入黑名单 - """ - try: - from utils.redis_client import redis - - # 获取Token剩余有效期 - expiration = get_token_expiration(token) - if expiration is None: - # 无法解析过期时间,使用默认过期时间作为TTL - ttl_seconds = DEFAULT_TIMEOUT_MINUTES * 60 - else: - ttl_seconds = int((expiration - datetime.datetime.now(tz=datetime.timezone.utc)).total_seconds()) - if ttl_seconds <= 0: - # Token已自然过期,无需加入黑名单 - return True - - fingerprint = _token_fingerprint(token) - key = f'{_TOKEN_BLACKLIST_PREFIX}{fingerprint}' - redis.set(key, '1', expire=ttl_seconds) - - logger.info(f"Token已加入黑名单,TTL={ttl_seconds}秒") - return True - - except Exception as e: - logger.error(f"Token加入黑名单失败: {str(e)}") - return False - - -def is_token_blacklisted(token: str) -> bool: - """ - 检查Token是否在黑名单中 - - 在登录验证中间件中调用,拒绝已被注销的Token。 - - Args: - token: JWT令牌字符串 - - Returns: - bool: True-已被注销(在黑名单中),False-有效 - """ - try: - from utils.redis_client import redis - - fingerprint = _token_fingerprint(token) - key = f'{_TOKEN_BLACKLIST_PREFIX}{fingerprint}' - return redis.exists(key) > 0 - - except Exception as e: - logger.error(f"Token黑名单检查异常: {str(e)}") - # Redis不可用时放行,避免阻断所有请求 - return False +from src.core.jwt import * # noqa: F401,F403 diff --git a/utils/model_utils.py b/utils/model_utils.py index ac33534..32da8f9 100644 --- a/utils/model_utils.py +++ b/utils/model_utils.py @@ -1,90 +1,8 @@ """ -模型工具模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -提供SQLAlchemy模型对象的序列化工具: -- model_to_dict: 将单个模型对象转换为字典 -- models_to_list: 将模型对象列表转换为字典列表 -- format_datetime: 格式化datetime对象 +模型工具已迁移至 src.common.utils.model_utils。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -from datetime import datetime - -from application.constants.common import SERIALIZE_EXCLUDE_FIELDS -from utils.case_converter import convert_camel_case - -logger = logging.getLogger(__name__) - - -def format_datetime(dt, format_str='%Y-%m-%d %H:%M:%S'): - """ - 格式化日期时间 - - Args: - dt: datetime对象 - format_str: 日期时间格式字符串,默认为 '%Y-%m-%d %H:%M:%S' - - Returns: - 格式化后的日期字符串,如果输入为None则返回None - """ - return dt.strftime(format_str) if dt else None - - -def model_to_dict(model, exclude=None, convert_case=True): - """ - 将SQLAlchemy模型对象转换为字典 - - Args: - model: SQLAlchemy模型对象 - exclude: 需要排除的字段列表,如 ['id', 'is_delete'](使用下划线命名) - convert_case: 是否将字段名转换为驼峰命名,默认True - - Returns: - 转换后的字典对象 - """ - if not model: - return None - - result = {} - for column in model.__table__.columns: - field_name = column.name - value = getattr(model, field_name) - - if exclude and field_name in exclude: - continue - - if field_name in SERIALIZE_EXCLUDE_FIELDS: - continue - - # 所有 datetime 类型统一格式化 - if isinstance(value, datetime): - value = value.strftime('%Y-%m-%d %H:%M:%S') if value else None - - if convert_case: - field_name = convert_camel_case(field_name) - - result[field_name] = value - - return result - - -def models_to_list(models, exclude=None, convert_case=True): - """ - 将SQLAlchemy模型对象列表转换为字典列表 - - Args: - models: SQLAlchemy模型对象列表 - exclude: 需要排除的字段列表 - convert_case: 是否将字段名转换为驼峰命名 - - Returns: - 字典列表 - """ - if not models: - return [] - - return [ - model_to_dict(model, exclude, convert_case) - for model in models - ] +from src.common.utils.model_utils import * # noqa: F401,F403 diff --git a/utils/password.py b/utils/password.py index b3fad4c..ca0cdf8 100644 --- a/utils/password.py +++ b/utils/password.py @@ -1,72 +1,8 @@ """ -密码加密工具模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块提供基于 bcrypt 的密码加密和验证功能。 -采用 bcrypt(password + salt) 方案,bcrypt 内置盐与外部 salt 双重加盐, -确保相同密码产生不同哈希,有效抵御彩虹表攻击。 - -主要功能: -1. 生成随机盐值(generate_salt) -2. 密码加密(encrypt_password)- bcrypt(password + salt) -3. 密码验证(verify_password) - -所属包: utils -所在文件: utils/password.py -依赖模块: bcrypt - 密码哈希第三方库 - secrets - 安全随机数生成模块 +密码工具已迁移至 src/core/password.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import secrets - -import bcrypt - -# bcrypt 工作因子,值越大计算越慢越安全,默认12(约250ms) -BCRYPT_ROUNDS = 12 - - -def generate_salt(length: int = 10) -> str: - """ - 生成随机盐值 - - Args: - length: 盐值长度,默认10位 - - Returns: - str: 随机盐值字符串 - """ - return secrets.token_urlsafe(length)[:length] - - -def encrypt_password(password: str, salt: str) -> str: - """ - 使用 bcrypt 加密密码 - - 将密码与外部 salt 拼接后进行 bcrypt 哈希,实现双重加盐。 - - Args: - password: 明文密码 - salt: 外部盐值 - - Returns: - str: bcrypt 哈希字符串(60字符) - """ - combined = (password + salt).encode('utf-8') - hashed = bcrypt.hashpw(combined, bcrypt.gensalt(rounds=BCRYPT_ROUNDS)) - return hashed.decode('utf-8') - - -def verify_password(password: str, salt: str, hashed: str) -> bool: - """ - 验证密码是否正确 - - Args: - password: 待验证的明文密码 - salt: 外部盐值 - hashed: 存储的 bcrypt 哈希值 - - Returns: - bool: 密码是否匹配 - """ - combined = (password + salt).encode('utf-8') - return bcrypt.checkpw(combined, hashed.encode('utf-8')) +from src.core.password import * # noqa: F401,F403 diff --git a/utils/redis_client.py b/utils/redis_client.py index 6230a67..8065360 100644 --- a/utils/redis_client.py +++ b/utils/redis_client.py @@ -1,311 +1,8 @@ """ -Redis客户端封装模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -提供Redis操作的统一封装,支持字符串、哈希、列表、集合、有序集合等数据类型。 -自动处理序列化/反序列化,业务代码无需关心底层存储格式。 - -使用方式: - from utils.redis_client import redis - - redis.set('key', 'value', expire=3600) - value = redis.get('key') +Redis 客户端封装已迁移至 src/core/redis_client.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import json -import pickle -from typing import Any, Union, List, Dict, Tuple - -from extensions.redis import redis_client - - -class RedisClient: - """ - Redis客户端封装类 - - 提供丰富的Redis操作方法,支持字符串、哈希、列表、集合、有序集合等数据类型 - """ - - def __init__(self, client=None): - """ - 初始化Redis客户端 - - Args: - client: FlaskRedis实例,如果为None则使用全局redis_client - """ - self.client = client or redis_client - - # ==================== 基础方法 ==================== - - def set(self, key: str, value: Any, expire: int = None, nx: bool = False, xx: bool = False) -> bool: - """设置键值对""" - serialized_value = self._serialize(value) - - if nx and xx: - raise ValueError("nx和xx不能同时为True") - - if nx: - result = self.client.setnx(key, serialized_value) - if expire and result: - self.client.expire(key, expire) - return result - elif xx: - result = self.client.set(key, serialized_value, xx=True) - if expire and result: - self.client.expire(key, expire) - return result - else: - if expire: - return self.client.setex(key, expire, serialized_value) - else: - return self.client.set(key, serialized_value) - - def get(self, key: str, default: Any = None, deserialize: bool = True) -> Any: - """获取键值""" - data = self.client.get(key) - if data is None: - return default - - if deserialize: - return self._deserialize(data) - return data - - def delete(self, *keys: str) -> int: - """删除一个或多个键""" - if not keys: - return 0 - return self.client.delete(*keys) - - def exists(self, *keys: str) -> int: - """检查键是否存在""" - if not keys: - return 0 - return self.client.exists(*keys) - - def expire(self, key: str, seconds: int) -> bool: - """设置键的过期时间""" - return self.client.expire(key, seconds) - - def expireat(self, key: str, timestamp: int) -> bool: - """设置键在指定时间戳过期""" - return self.client.expireat(key, timestamp) - - def ttl(self, key: str) -> int: - """获取键的剩余过期时间""" - return self.client.ttl(key) - - def keys(self, pattern: str = '*') -> List[str]: - """获取匹配模式的所有键(使用SCAN,避免KEYS阻塞)""" - result = [] - cursor = 0 - while True: - cursor, batch = self.client.scan(cursor=cursor, match=pattern, count=100) - result.extend(batch) - if cursor == 0: - break - return result - - # ==================== 哈希表操作 ==================== - - def hset(self, name: str, key: str, value: Any) -> int: - """设置哈希表中的字段值""" - serialized_value = self._serialize(value) - return self.client.hset(name, key, serialized_value) - - def hget(self, name: str, key: str, deserialize: bool = True) -> Any: - """获取哈希表中的字段值""" - data = self.client.hget(name, key) - if data is None: - return None - - if deserialize: - return self._deserialize(data) - return data - - def hgetall(self, name: str, deserialize: bool = True) -> Dict[str, Any]: - """获取哈希表中所有字段和值""" - data = self.client.hgetall(name) - if not data: - return {} - - if deserialize: - return {k.decode(): self._deserialize(v) for k, v in data.items()} - return {k.decode(): v for k, v in data.items()} - - def hdel(self, name: str, *keys: str) -> int: - """删除哈希表中的字段""" - if not keys: - return 0 - return self.client.hdel(name, *keys) - - def hincrby(self, name: str, key: str, amount: int = 1) -> int: - """为哈希表中的字段值增加指定数值""" - return self.client.hincrby(name, key, amount) - - # ==================== 列表操作 ==================== - - def lpush(self, name: str, *values: Any) -> int: - """从列表左侧插入元素""" - serialized_values = [self._serialize(v) for v in values] - return self.client.lpush(name, *serialized_values) - - def rpush(self, name: str, *values: Any) -> int: - """从列表右侧插入元素""" - serialized_values = [self._serialize(v) for v in values] - return self.client.rpush(name, *serialized_values) - - def lpop(self, name: str, deserialize: bool = True) -> Any: - """从列表左侧弹出元素""" - data = self.client.lpop(name) - if data is None: - return None - - if deserialize: - return self._deserialize(data) - return data - - def rpop(self, name: str, deserialize: bool = True) -> Any: - """从列表右侧弹出元素""" - data = self.client.rpop(name) - if data is None: - return None - - if deserialize: - return self._deserialize(data) - return data - - def lrange(self, name: str, start: int, end: int, deserialize: bool = True) -> List[Any]: - """获取列表指定范围的元素""" - data = self.client.lrange(name, start, end) - if not data: - return [] - - if deserialize: - return [self._deserialize(item) for item in data] - return [item for item in data] - - def llen(self, name: str) -> int: - """获取列表长度""" - return self.client.llen(name) - - # ==================== 集合操作 ==================== - - def sadd(self, name: str, *values: Any) -> int: - """向集合中添加元素""" - serialized_values = [self._serialize(v) for v in values] - return self.client.sadd(name, *serialized_values) - - def srem(self, name: str, *values: Any) -> int: - """从集合中移除元素""" - serialized_values = [self._serialize(v) for v in values] - return self.client.srem(name, *serialized_values) - - def smembers(self, name: str, deserialize: bool = True) -> set: - """获取集合中所有元素""" - data = self.client.smembers(name) - if not data: - return set() - - if deserialize: - return {self._deserialize(item) for item in data} - return {item for item in data} - - def sismember(self, name: str, value: Any) -> bool: - """判断元素是否在集合中""" - serialized_value = self._serialize(value) - return self.client.sismember(name, serialized_value) - - # ==================== 有序集合操作 ==================== - - def zadd(self, name: str, mapping: Dict[Any, float]) -> int: - """向有序集合中添加元素""" - serialized_mapping = {self._serialize(k): v for k, v in mapping.items()} - return self.client.zadd(name, serialized_mapping) - - def zrange(self, name: str, start: int, end: int, withscores: bool = False, - deserialize: bool = True) -> Union[List[Any], List[Tuple[Any, float]]]: - """获取有序集合指定范围的元素(按分数升序)""" - data = self.client.zrange(name, start, end, withscores=withscores) - if not data: - return [] - - if withscores: - if deserialize: - return [(self._deserialize(item[0]), item[1]) for item in data] - return [(item[0], item[1]) for item in data] - else: - if deserialize: - return [self._deserialize(item) for item in data] - return list(data) - - def zrevrange(self, name: str, start: int, end: int, withscores: bool = False, - deserialize: bool = True) -> Union[List[Any], List[Tuple[Any, float]]]: - """获取有序集合指定范围的元素(按分数降序)""" - data = self.client.zrevrange(name, start, end, withscores=withscores) - if not data: - return [] - - if withscores: - if deserialize: - return [(self._deserialize(item[0]), item[1]) for item in data] - return [(item[0], item[1]) for item in data] - else: - if deserialize: - return [self._deserialize(item) for item in data] - return list(data) - - # ==================== 高级操作 ==================== - - def pipeline(self): - """获取管道对象,用于批量执行命令""" - return self.client.pipeline() - - def incr(self, key: str, amount: int = 1) -> int: - """增加键的值(用于计数器)""" - return self.client.incrby(key, amount) - - def decr(self, key: str, amount: int = 1) -> int: - """减少键的值""" - return self.client.decrby(key, amount) - - def lock(self, key: str, timeout: int = 10, blocking: bool = True, - blocking_timeout: int = None) -> Any: - """获取分布式锁""" - return self.client.lock(key, timeout=timeout, blocking=blocking, - blocking_timeout=blocking_timeout) - - # ==================== 私有方法 ==================== - - def _serialize(self, value: Any) -> Union[str, bytes]: - """序列化值""" - if isinstance(value, str): - return value - elif isinstance(value, (int, float, bool)): - return str(value) - elif isinstance(value, (dict, list, tuple)): - return json.dumps(value, ensure_ascii=False) - else: - return pickle.dumps(value) - - def _deserialize(self, value: Union[str, bytes]) -> Any: - """反序列化值""" - if not value: - return None - - if isinstance(value, bytes): - try: - value = value.decode('utf-8') - except UnicodeDecodeError: - try: - return pickle.loads(value) - except Exception: - return value - - try: - return json.loads(value) - except (json.JSONDecodeError, TypeError): - return value - - -# 创建全局Redis客户端实例 -redis = RedisClient() +from src.core.redis_client import * # noqa: F401,F403 diff --git a/utils/regular.py b/utils/regular.py index e67bd1b..1fafbab 100644 --- a/utils/regular.py +++ b/utils/regular.py @@ -1,80 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 珊瑚云团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -表单错误处理模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -提供表单验证错误信息的提取和格式化功能。 +表单错误提取已迁移至 src.common.utils.regular。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - - -def get_err(form): - """ - 获取表单验证的错误信息 - - 遍历表单的所有错误字段,提取第一条错误信息,并用斜杠拼接。 - - Args: - form: WTForms 表单对象 - - Returns: - str: 拼接后的错误信息字符串,多个错误用 '/' 分隔 - - Examples: - >>> from application.forms.level import LevelForm - >>> form = LevelForm(request.form) - >>> if not form.validate(): - ... err_msg = get_err(form) - ... return R.failed(msg=err_msg) - - # 错误信息示例 - # 单条错误: "职级名称不能为空" - # 多条错误: "职级名称不能为空/职级排序必须为数字" - """ - if not form.errors: - return "" - - # 提取每个字段的第一条错误信息 - error_messages = [errors[0] for errors in form.errors.values()] - - # 用斜杠拼接所有错误信息 - return "/".join(error_messages) +from src.common.utils.regular import * # noqa: F401,F403 diff --git a/utils/request.py b/utils/request.py index ec6501c..d93eeaf 100644 --- a/utils/request.py +++ b/utils/request.py @@ -1,196 +1,8 @@ """ -请求解析工具模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -提供请求数据解析相关的工具函数: -- get_json_data: 从Flask请求中解析JSON数据 -- require_json: 装饰器,要求请求必须包含JSON数据 -- parse_request_body: 解析请求体JSON数据,返回(data, error)元组 -- parse_id_list: 将逗号分隔的ID字符串解析为整数列表 -- parse_batch_ids: 从请求体解析批量删除ID数组,校验并转为逗号分隔字符串 -- like_escape: 转义SQL LIKE通配符,防止用户输入被当作通配符 -- like_contains: 构建安全的LIKE包含查询值 +请求解析工具已迁移至 src.common.utils.request。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -from functools import wraps - -logger = logging.getLogger(__name__) - - -def get_json_data(required=True, silent=False): - """ - 获取请求中的JSON数据 - - Args: - required: 是否必须获取到JSON数据,默认为True - silent: 是否静默处理错误,默认为False(False时会检查Content-Type) - - Returns: - dict: JSON数据,如果获取失败则返回None或空字典 - """ - from flask import request - - if not silent and not request.is_json: - return None - - try: - json_data = request.get_json(silent=silent) - except Exception: - return None - - if required and not json_data: - return None - - return json_data or {} - - -def require_json(f): - """ - 装饰器:要求请求必须包含JSON数据 - - Examples: - @require_json - def login(): - data = get_json_data() - # 业务逻辑... - """ - - @wraps(f) - def decorated_function(*args, **kwargs): - from flask import request - - if not request.is_json: - from utils import R - return R.failed(msg="参数格式不正确,请使用JSON格式") - - json_data = request.get_json() - if not json_data: - from utils import R - return R.failed(msg="参数不能为空") - - request.parsed_json = json_data - return f(*args, **kwargs) - - return decorated_function - - -def parse_request_body(): - """ - 解析请求体JSON数据 - - Returns: - tuple: (data, error) - 成功时 data 为解析后的字典,error 为 None - 失败时 data 为 None,error 为错误描述字符串 - """ - try: - data = get_json_data(required=False, silent=True) - - if data is None or data == {}: - return None, "参数不能为空" - - return data, None - - except Exception as e: - logger.error(f"解析请求体异常: {str(e)}") - return None, "参数处理失败" - - -def parse_id_list(id_str: str, separator: str = ',') -> list: - """ - 将逗号分隔的ID字符串解析为整数列表 - - Args: - id_str: 逗号分隔的ID字符串,如 "1,2,3" - separator: 分隔符,默认为逗号 - - Returns: - list: 有效的整数ID列表 - - Example: - >>> parse_id_list("1,2,3") - [1, 2, 3] - >>> parse_id_list("1,2,abc,3") - [1, 2, 3] - """ - if not id_str: - return [] - result = [] - for item in id_str.split(separator): - item = item.strip() - if item.isdigit(): - result.append(int(item)) - return result - - -def parse_batch_ids(): - """ - 从请求体解析批量删除的ID数组,校验并转为逗号分隔字符串 - - 请求体格式: [1, 2, 3] - - Returns: - tuple: (id_str, error_response) - 成功时 id_str 为 "1,2,3",error_response 为 None - 失败时 id_str 为 None,error_response 为 R.failed 响应 - """ - from flask import request - from utils import R - - ids = request.get_json() - if not ids or not isinstance(ids, list) or len(ids) == 0: - return None, R.failed("参数错误:ids必须为非空数组") - try: - id_list = [int(id_val) for id_val in ids] - except (ValueError, TypeError): - return None, R.failed("参数错误:数组元素必须为数字") - - return ','.join(str(id_val) for id_val in id_list), None - - -def like_escape(value, escape_char='\\'): - """ - 转义SQL LIKE通配符 - - 将用户输入中的 %、_ 和转义字符本身进行转义, - 防止用户输入被当作LIKE通配符导致非预期查询结果。 - - Args: - value: 原始输入值 - escape_char: 转义字符,默认为反斜杠 - - Returns: - str: 转义后的安全字符串 - - Example: - >>> like_escape('100%') - '100\\%' - >>> like_escape('test_data') - 'test\\_data' - """ - escaped = str(value).replace(escape_char, escape_char * 2) - escaped = escaped.replace('%', escape_char + '%') - escaped = escaped.replace('_', escape_char + '_') - return escaped - - -def like_contains(value, escape_char='\\'): - """ - 构建安全的LIKE包含查询值(前后加%) - - 对用户输入进行通配符转义后,前后添加 % 构建模糊查询值。 - 配合 SQLAlchemy 的 .like() 方法和 escape 参数使用。 - - Args: - value: 原始输入值 - escape_char: 转义字符,默认为反斜杠 - - Returns: - str: 构建好的LIKE查询值,如 '%safe_value%' - - Example: - >>> # SQLAlchemy 用法 - >>> query = query.filter(User.name.like(like_contains(name), escape='\\')) - """ - return f'%{like_escape(value, escape_char)}%' +from src.common.utils.request import * # noqa: F401,F403 diff --git a/utils/response.py b/utils/response.py index c779390..ad73475 100644 --- a/utils/response.py +++ b/utils/response.py @@ -1,222 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 珊瑚云团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -统一响应结果模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块提供了系统统一的JSON响应格式,确保所有API接口返回格式一致。 -包含成功响应和失败响应两种基础方法,支持扩展参数。 - -响应格式规范: -- code: 状态码,0表示成功,1表示失败(可扩展其他状态码) -- data: 响应数据,成功时返回具体数据,失败时为None -- msg: 提示信息,对操作结果的文字描述 -- ok: 布尔值,标识操作是否成功(兼容旧版前端) -- 其他扩展参数:可通过kwargs动态添加 - -使用示例: - # 成功响应 - return R.ok(data={"id": 1, "name": "张三"}) - return R.ok(msg="操作成功") - - # 失败响应 - return R.failed(msg="用户名或密码错误") - return R.failed(msg="服务器内部错误", code=500) - - # 带扩展参数的响应 - return R.ok(data=user_info, total=100, pages=10) +统一响应模块已迁移至 src/core/response.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -from typing import Any, Optional, Dict - -from flask import jsonify - -from application.constants.common import MESSAGE_OK, MESSAGE_FAIL - - -def ok( - data: Optional[Any] = None, - msg: str = MESSAGE_OK, - code: int = 0, - **kwargs: Any -) -> jsonify: - """ - 生成成功响应 - - 构建一个标准的成功响应JSON对象,包含状态码、数据、提示信息等字段。 - 支持通过kwargs添加额外的响应字段,便于扩展。 - - Args: - data: 响应数据,可以是任意类型(dict, list, str, int等),默认为None - msg: 成功提示信息,默认使用常量MESSAGE_OK - code: 状态码,0表示成功,默认为0 - **kwargs: 额外的键值对参数,会合并到响应体中 - - Returns: - jsonify: 格式化的JSON响应对象 - - Examples: - >>> return R.ok() - {"code": 0, "data": None, "msg": "操作成功", "ok": True} - - >>> return R.ok(data={"id": 1, "name": "张三"}) - {"code": 0, "data": {"id": 1, "name": "张三"}, "msg": "操作成功", "ok": True} - - >>> return R.ok(msg="创建成功", total=100) - {"code": 0, "data": None, "msg": "创建成功", "ok": True, "total": 100} - """ - # 构建基础响应体 - response_body: Dict[str, Any] = { - "code": code, - "data": data, - "msg": msg, - "ok": True - } - - # 合并扩展参数 - if kwargs: - response_body.update(kwargs) - - # 返回JSON响应 - return jsonify(response_body) - - -def failed( - msg: str = MESSAGE_FAIL, - code: int = 1, - data: Optional[Any] = None, - **kwargs: Any -) -> jsonify: - """ - 生成失败响应 - - 构建一个标准的失败响应JSON对象,包含状态码、错误信息等字段。 - 支持通过kwargs添加额外的响应字段,便于扩展。 - - Args: - msg: 错误提示信息,默认使用常量MESSAGE_FAIL - code: 状态码,1表示失败,默认为1(可根据业务需求定义其他错误码) - data: 错误时的附加数据,通常为None,默认为None - **kwargs: 额外的键值对参数,会合并到响应体中 - - Returns: - jsonify: 格式化的JSON响应对象 - - Examples: - >>> return R.failed() - {"code": 1, "data": None, "msg": "操作失败", "ok": False} - - >>> return R.failed(msg="用户名或密码错误") - {"code": 1, "data": None, "msg": "用户名或密码错误", "ok": False} - - >>> return R.failed(msg="未授权访问", code=401) - {"code": 401, "data": None, "msg": "未授权访问", "ok": False} - - >>> return R.failed(msg="参数错误", errors={"name": "该字段不能为空"}) - {"code": 1, "data": None, "msg": "参数错误", "ok": False, "errors": {...}} - """ - # 构建基础响应体 - response_body: Dict[str, Any] = { - "code": code, - "data": data, - "msg": msg, - "ok": False - } - - # 合并扩展参数 - if kwargs: - response_body.update(kwargs) - - # 返回JSON响应 - return jsonify(response_body) - - -def response( - data: Optional[Any] = None, - msg: str = MESSAGE_OK, - code: int = 0, - success: bool = True, - **kwargs: Any -) -> jsonify: - """ - 通用响应生成器(高级用法) - - 提供更灵活的响应生成方式,允许自定义所有字段。 - 通常推荐使用ok()和failed()两个专用方法,但在需要完全控制响应格式时可以使用此方法。 - - Args: - data: 响应数据 - msg: 提示信息 - code: 状态码 - success: 操作是否成功标识 - **kwargs: 额外的键值对参数 - - Returns: - jsonify: 格式化的JSON响应对象 - - Examples: - >>> return R.response(data=[], msg="查询成功", code=200, success=True) - {"code": 200, "data": [], "msg": "查询成功", "ok": True} - """ - response_body: Dict[str, Any] = { - "code": code, - "data": data, - "msg": msg, - "ok": success - } - - if kwargs: - response_body.update(kwargs) - - return jsonify(response_body) - - -# 为方便使用,定义常用错误码常量(可选) -class Codes: - """常用状态码定义""" - SUCCESS = 0 - FAILED = 1 - UNAUTHORIZED = 401 # 未认证 - FORBIDDEN = 403 # 无权限 - NOT_FOUND = 404 # 资源不存在 - VALIDATE_ERROR = 422 # 验证错误 - SERVER_ERROR = 500 # 服务器错误 +from src.core.response import * # noqa: F401,F403 diff --git a/utils/rich_text.py b/utils/rich_text.py index a6413d5..663477a 100644 --- a/utils/rich_text.py +++ b/utils/rich_text.py @@ -1,98 +1,8 @@ """ -富文本与文件处理模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -提供文件URL处理、临时文件迁移和富文本编辑器内容处理: -- get_file_url: 拼接文件域名生成完整URL -- save_file: 将临时目录文件迁移到正式存储目录 -- save_content: 处理富文本HTML内容,提取并保存其中的图片文件 +富文本工具已迁移至 src.common.utils.rich_text。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import logging -import os -import re -import time -import html - -from config import FLASK_FILE_URL, FLASK_UPLOAD_DIR, FLASK_TEMP_PATH -from utils.file import mkdir - -logger = logging.getLogger(__name__) - - -def get_file_url(path): - """ - 获取完整的文件访问URL - - Args: - path: 文件的相对路径,可以是空字符串、None或有效路径 - - Returns: - 完整的文件访问URL,如果path为空则返回None - """ - if not path: - return None - return FLASK_FILE_URL + path - - -def save_file(url, directory): - """ - 保存文件到指定目录 - - 处理本站临时文件迁移:将临时目录中的文件移动到正式存储目录。 - - Args: - url: 文件的完整URL或相对路径 - directory: 目标子目录名称,用于分类存储 - - Returns: - 文件的相对路径,失败时返回空字符串 - """ - if not url: - return "文件地址不能为空" - - if url.find(FLASK_FILE_URL) != -1: - if url.find("/temp") != -1: - path = FLASK_UPLOAD_DIR + "/" + directory + "/" + time.strftime('%Y%m%d') - mkdir(path) - - old_path = url.replace(FLASK_FILE_URL, FLASK_UPLOAD_DIR) - new_path = FLASK_UPLOAD_DIR + "/" + directory + old_path.replace(FLASK_TEMP_PATH, "") - - os.rename(old_path, new_path) - return new_path.replace(FLASK_UPLOAD_DIR, "") - else: - return url.replace(FLASK_FILE_URL, "") - else: - logger.warning(f"远程文件暂不支持保存: {url}") - - return "" - - -def save_content(content, title, directory): - """ - 保存富文本编辑器内容 - - 处理富文本HTML内容,提取其中的图片文件并保存, - 同时将文件alt属性设置为文章标题(已做HTML转义防XSS)。 - - Args: - content: 富文本HTML内容 - title: 文章标题,用于设置文件alt属性 - directory: 文件存储的子目录名称 - - Returns: - 处理后的富文本内容,文件地址已被替换 - """ - image_urls = re.findall('img src="(.*?)"', content, re.S) - - for url in image_urls: - image = save_file(url, directory) - if image: - content = content.replace(url, "[IMG_URL]" + image) - - title = html.escape(str(title) if title else "", quote=True) - if "alt=\"\"" in content and title: - content = content.replace("alt=\"\"", "alt=\"" + title + "\"") - - return content +from src.common.utils.rich_text import * # noqa: F401,F403 diff --git a/utils/security.py b/utils/security.py index a8f8553..d0ebba5 100644 --- a/utils/security.py +++ b/utils/security.py @@ -1,122 +1,8 @@ -# +====================================================================== -# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] -# +---------------------------------------------------------------------- -# | Copyright (c) 2021-2026 珊瑚云团队 版权所有 -# +---------------------------------------------------------------------- -# | 开源协议: Apache-2.0 -# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 -# +---------------------------------------------------------------------- -# | 官方网站: https://www.djangoadmin.cn -# +---------------------------------------------------------------------- -# | 研发团队: 珊瑚云团队 荣誉出品 -# +====================================================================== -# | 版权和免责声明: -# | 一、知识产权声明 -# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 -# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, -# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 -# | 二、使用限制与禁止条款 -# | 您理解并同意,禁止将本产品用于以下任何行为: -# | 1. 任何违反中华人民共和国法律法规的用途; -# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; -# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 -# | 三、责任豁免 -# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 -# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 -# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 -# | 四、违约与法律责任 -# | 如发现任何违反本声明的行为,本团队有权: -# | 1. 无条件配合公安机关及其他有权机关的调查取证工作; -# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; -# | 3. 保留进一步采取刑事控告等法律措施的权利。 -# | 五、许可范围说明 -# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 -# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 -# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 -# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 -# | 成的一切经济损失及商誉损害。 -# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 -# | 许可协议》附件为准。 -# +====================================================================== - """ -用户安全工具模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -该模块提供了用户认证和安全相关的工具函数。 -用于快速获取当前登录用户的信息,简化业务代码中的用户信息获取操作。 +安全工具已迁移至 src/core/security.py。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ -import logging - -from flask import request - -from utils.jwt import parse_token - - -# ============================= 用户信息获取工具 ============================= - - -def get_user_id(): - """ - 获取当前登录用户ID - ============================================================= - - 快速获取当前登录用户的ID,用于需要用户ID的业务场景。 - 基于parse_token函数实现,简化调用方式。 - - 使用场景: - - 记录操作日志时需要记录操作人ID - - 数据查询时进行用户权限过滤 - - 创建数据时自动关联当前用户 - - 参数: - request (HttpRequest): Django请求对象,包含请求头中的token信息 - - 返回: - int: 当前登录用户的ID,若token无效或用户不存在则返回0 - - 异常: - 可能抛出parse_token函数产生的异常(如token过期、token无效等) - 建议调用方自行处理异常 - - 示例: - >>> user_id = get_user_id() - >>> if user_id: - ... # 执行需要用户ID的操作 - ... create_log(user_id, "操作内容") - """ - try: - # 解析请求中的token,获取用户信息 - result = parse_token(request) - - # 安全获取用户ID,避免KeyError - if result and isinstance(result, dict): - data = result.get('data', {}) - if isinstance(data, dict): - return data.get('userId', 0) - - return 0 - except Exception as e: - # 记录异常日志(可根据需要添加日志记录) - # logger.error(f"获取用户ID失败: {str(e)}") - return 0 - - -def get_username(): - """ - 获取当前登录用户账号 - - Returns: - str: 当前登录用户的账号(username),若token无效或用户不存在则返回空字符串 - """ - try: - result = parse_token(request) - if result and isinstance(result, dict): - data = result.get('data', {}) - if isinstance(data, dict): - username = data.get('username') - return username if username else '' - return '' - except Exception as e: - logging.error(f"获取用户名失败: {str(e)}") - return '' +from src.core.security import * # noqa: F401,F403 diff --git a/utils/user_agent.py b/utils/user_agent.py index c514780..aff3820 100644 --- a/utils/user_agent.py +++ b/utils/user_agent.py @@ -1,75 +1,8 @@ """ -User-Agent解析工具模块 +兼容 shim(绞杀者迁移期间保留) ============================================================= -提供HTTP请求User-Agent的解析功能: -- parse_user_agent: 解析操作系统和浏览器信息 +UA 解析工具已迁移至 src.common.utils.user_agent。 +本文件仅作兼容转发,最终在 P7(切尾)阶段删除。 """ - -import re - - -def parse_user_agent(): - """ - 解析User-Agent,获取操作系统和浏览器信息 - - 通过正则匹配User-Agent字符串,识别客户端操作系统和浏览器类型。 - 支持 Windows、macOS、Linux、Android、iOS 及主流浏览器识别。 - - Returns: - dict: 包含os和browser的字典 - { - 'os': 'Windows 10', # 操作系统信息 - 'browser': 'Chrome' # 浏览器名称 - } - 无法识别时返回 '未知' - """ - from flask import request - - user_agent = request.headers.get('User-Agent', '') - - 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'), # Edge 浏览器(基于Chromium) - (r'chrome', 'Chrome'), # Chrome 浏览器 - (r'safari', 'Safari'), # Safari 浏览器 - (r'firefox', 'Firefox'),# Firefox 浏览器 - (r'msie', 'IE'), # Internet Explorer - (r'trident', 'IE'), # IE 11(Trident内核) - ] - - for pattern, browser_name in browser_patterns: - if re.search(pattern, ua_lower): - result['browser'] = browser_name - break - - return result \ No newline at end of file +from src.common.utils.user_agent import * # noqa: F401,F403 diff --git a/重构方案-v2.md b/重构方案-v2.md index 99a5162..7359788 100644 --- a/重构方案-v2.md +++ b/重构方案-v2.md @@ -352,7 +352,7 @@ from src.core.database import db # 统一 db 单例 |---|---|---|---| | **P0 基线** | `git init`;`.gitignore` 补 `logs/ instance/ *.pyc`;基线提交 | git 仓库可用,可 `git reset --hard` 回滚 | 无 | | **P1 工具链** | pyproject.toml / requirements-dev.txt / .flake8 / .pre-commit / Makefile / pytest 骨架 | 现有代码 `flake8` 通过(或登记存量告警);`pytest` 空跑通过。**已登记存量告警:154 条(F401×111 / E402×36 / 其余 7);pytest 5 passed** | 低 | -| **P2 core+common** | 建 `src/core/` `src/common/`;extensions/utils 打 shim;模型聚合器建 `src/models_registry.py` | `python app.py` 启动;`flask db migrate` 仍全量识别表;全接口冒烟 | 高(依赖面最大) | +| **P2 core+common** | 建 `src/core/` `src/common/`;extensions/utils 打 shim;模型聚合器建 `src/models_registry.py` | 已验收:22 模型表全量识别且 DB 全部存在;src/ flake8 零告警;pytest 5 passed;应用启动;/login 表单校验与 /user/page 鉴权拦截冒烟正常。**基线修复**:application/models + middleware 原被嵌套 `.gitignore`(`*`)误排除,已补入。**登记既有缺陷**:① `flask db` CLI 因 .env 的 FLASK_VERSION 与 --version 冲突报错(Makefile 已加 FLASK_SKIP_DOTENV 规避);② 本机 Redis 未设密码但 REDIS_AUTH=True,/captcha 报 AUTH 错误(环境问题,非重构) | 高(依赖面最大,已完成) | | **P3 模块搬迁** | 按 §2.5 顺序逐组迁移 23 个模块(models→forms→services→views),每批打 shim + 验证 | 每批后 `python app.py` 启动 + 该模块接口回归通过 | 中 | | **P4 服务层改造** | 26 个服务文件改「入参数据/出参数据」;router 取参包 R;引入 base_service/base_repository + BizError | 核心服务(auth/user/role/menu)单测可写可跑;接口回归全绿 | 高(最大工作量) | | **P5 DI 注入** | core/permission.py + op_log 接口化;app 工厂注入 menu 服务 | `grep -r "from application" src/core/` 为空;权限/日志行为与现状一致 | 中 |