refactor: P2 core+common 建设(应用工厂/扩展/工具层迁入 src/)

【结构】
- src/core/:database/response/jwt/password/security/redis_client/mail/scheduler
- src/common/:base_model/base_db/forms(base_form+fields)/constants/utils
- src/models_registry.py:模型聚合器(Flask-Migrate 全量表识别)

【绞杀者 shim】extensions/、utils/、application/constants|forms|models 打兼容转发
【兼容修复】应用工厂抽取至 application/factory.py(P2 前置,P1 已完成)

【基线修复】application/models(25) + middleware(7) 原被嵌套 .gitignore(*)
误排除出版本库,已补入(base_model/base_db 现为 shim,原版在 src/common/)

【登记既有缺陷】
1. flask db CLI 因 .env FLASK_VERSION 与 --version 冲突报错 → Makefile 已加 FLASK_SKIP_DOTENV=1
2. 本机 Redis 未设密码但 REDIS_AUTH=True,/captcha 报 AUTH 错误(环境问题)

【验收】22 模型表全量识别;src/ flake8 零告警;pytest 5 passed;应用启动;
/login 表单校验、/user/page 鉴权拦截冒烟正常

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
DjangoAdmin Dev
2026-08-07 13:20:46 +08:00
co-authored by Claude
parent 92130ef070
commit 658ee6d596
96 changed files with 9926 additions and 4153 deletions
+10
View File
@@ -0,0 +1,10 @@
"""
src 包 —— DjangoAdmin 后端源码根包(重构目标结构)
=============================================================
结构:
core/ 基础设施层(不依赖业务)
common/ 共享层(基类、常量、工具)
modules/ 业务模块层(P3 阶段建设)
routes/ 路由注册中心
"""
+12
View File
@@ -0,0 +1,12 @@
"""
common 共享层
=============================================================
提供业务模块共享的基类与工具:
- base_model.py / base_db.py 模型基类
- forms/ 表单基类
- constants/ 常量
- utils/ 工具函数
依赖约束:common 层可依赖 core 层,不得依赖 modules 层。
"""
+208
View File
@@ -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
+671
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
"""
constants 常量包
=============================================================
- common.py 通用常量(分页、默认密码、消息文本、序列化排除字段)
- dictionary_constants.py 字典相关常量(原 application/constants/dict.py)
- error_code.py 错误码定义(后续阶段建设)
"""
+85
View File
@@ -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'})
@@ -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: "下架", # 下架/草稿
}
+7
View File
@@ -0,0 +1,7 @@
"""
forms 表单基类包
=============================================================
- base_form.py 表单基类(BaseForm,支持 JSON 数组)
- fields.py 自定义字段(ArrayField)
"""
+105
View File
@@ -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)
+177
View File
@@ -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 '[]'
+17
View File
@@ -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/,不在此包。
"""
+230
View File
@@ -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
+327
View File
@@ -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)
+407
View File
@@ -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)
+107
View File
@@ -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] + '...'
+55
View File
@@ -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 "未知"
+179
View File
@@ -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"]
Binary file not shown.
+91
View File
@@ -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)
+90
View File
@@ -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
]
+80
View File
@@ -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)
+196
View File
@@ -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)}%'
+98
View File
@@ -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
+75
View File
@@ -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
+14
View File
@@ -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 层。
"""
+87
View File
@@ -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()
+634
View File
@@ -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 <token>"
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
+46
View File
@@ -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)
+72
View File
@@ -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'))
+377
View File
@@ -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()
+222
View File
@@ -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 # 服务器错误
+199
View File
@@ -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()
+121
View File
@@ -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 ''
+25
View File
@@ -0,0 +1,25 @@
"""
模型注册中心
=============================================================
统一在此导入全部数据库模型,确保 Flask-Migrate 能全量识别表结构
(migrations/env.py 依赖 db.metadata 收集所有表)。
P2 阶段:模型仍位于 application/models/,经其 __init__ 全量导入。
P3 阶段:随模块逐个迁入 src/modules/,此处逐行改为
from modules.<domain>.<name>.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
# ...
# ============================================================