feat(phase2): shared/ 共享层迁移
- shared/: enums/constants/pagination/base_model/base_db/base_repository/base_service - apps/constants 与 apps/models/base_model、base_db 改为垫片 - 新增 pagination 单元测试 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+2
-205
@@ -1,205 +1,2 @@
|
|||||||
# +======================================================================
|
# 兼容垫片:由 shared/enums.py 迁移,保留旧导入路径
|
||||||
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
from shared.enums import * # noqa: F401,F403
|
||||||
# +----------------------------------------------------------------------
|
|
||||||
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
|
|
||||||
# +----------------------------------------------------------------------
|
|
||||||
# | 开源协议: Apache-2.0
|
|
||||||
# | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。
|
|
||||||
# +----------------------------------------------------------------------
|
|
||||||
# | 官方网站: https://www.djangoadmin.cn
|
|
||||||
# +----------------------------------------------------------------------
|
|
||||||
# | 研发团队: 一米阳光团队 荣誉出品
|
|
||||||
# +======================================================================
|
|
||||||
# | 版权和免责声明:
|
|
||||||
# | 一、知识产权声明
|
|
||||||
# | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、
|
|
||||||
# | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权,
|
|
||||||
# | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。
|
|
||||||
# | 二、使用限制与禁止条款
|
|
||||||
# | 您理解并同意,禁止将本产品用于以下任何行为:
|
|
||||||
# | 1. 任何违反中华人民共和国法律法规的用途;
|
|
||||||
# | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为;
|
|
||||||
# | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。
|
|
||||||
# | 三、责任豁免
|
|
||||||
# | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失
|
|
||||||
# | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三
|
|
||||||
# | 方提起索赔或行政机关进行调查,您应独立承担全部责任。
|
|
||||||
# | 四、违约与法律责任
|
|
||||||
# | 如发现任何违反本声明的行为,本团队有权:
|
|
||||||
# | 1. 无条件配合公安机关及其他有权机关的调查取证工作;
|
|
||||||
# | 2. 依法追究相关方的民事责任,并要求赔偿全部损失;
|
|
||||||
# | 3. 保留进一步采取刑事控告等法律措施的权利。
|
|
||||||
# | 五、许可范围说明
|
|
||||||
# | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包
|
|
||||||
# | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通
|
|
||||||
# | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、
|
|
||||||
# | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造
|
|
||||||
# | 成的一切经济损失及商誉损害。
|
|
||||||
# | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件
|
|
||||||
# | 许可协议》附件为准。
|
|
||||||
# +======================================================================
|
|
||||||
|
|
||||||
# 性别
|
|
||||||
GENDER_LIST = {
|
|
||||||
1: "男",
|
|
||||||
2: "女",
|
|
||||||
3: "保密",
|
|
||||||
}
|
|
||||||
# 部门类型
|
|
||||||
DEPT_TYPE_LIST = {
|
|
||||||
1: "公司",
|
|
||||||
2: "子公司",
|
|
||||||
3: "部门",
|
|
||||||
4: "小组",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 站点类型
|
|
||||||
ITEM_TYPE_LIST = {
|
|
||||||
1: "普通站点",
|
|
||||||
2: "其他站点"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 友链类型:1友情链接 2合作伙伴
|
|
||||||
LINK_TYPE_LIST = {
|
|
||||||
1: "友情链接",
|
|
||||||
2: "合作伙伴"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 友链投放平台:1PC站 2WAP站 3微信小程序 4APP应用
|
|
||||||
LINK_PLATFORM_LIST = {
|
|
||||||
1: "PC站",
|
|
||||||
2: "WAP站",
|
|
||||||
3: "微信小程序",
|
|
||||||
4: "APP应用"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 友链形式:1文字链接 2图片链接
|
|
||||||
LINK_FORM_LIST = {
|
|
||||||
1: "文字链接",
|
|
||||||
2: "图片链接",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 广告投放平台:1PC站 2WAP站 3微信小程序 4APP应用
|
|
||||||
AD_SORT_PLATFORM_LIST = {
|
|
||||||
1: "PC站",
|
|
||||||
2: "WAP站",
|
|
||||||
3: "微信小程序",
|
|
||||||
4: "APP应用"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 广告类型:1图片 2文字 3视频 4推荐
|
|
||||||
AD_TYPE_LIST = {
|
|
||||||
1: "图片",
|
|
||||||
2: "文字",
|
|
||||||
3: "视频",
|
|
||||||
4: "推荐"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 通知来源:1官方平台 2开源中国 3CSDN官方 4新浪微博
|
|
||||||
NOTICE_SOURCE_LIST = {
|
|
||||||
1: "官方平台",
|
|
||||||
2: "开源中国",
|
|
||||||
3: "CSDN官方",
|
|
||||||
4: "新浪微博",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 会员注册来源:1-网站注册 2-客户端注册 3-小程序注册 4-手机站注册 5-后台添加
|
|
||||||
MEMBER_SOURCE_LIST = {
|
|
||||||
1: "网站注册",
|
|
||||||
2: "客户端注册",
|
|
||||||
3: "小程序注册",
|
|
||||||
4: "手机站注册",
|
|
||||||
5: "后台添加"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 配置项类型
|
|
||||||
CONFIG_DATA_TYPE_LIST = {
|
|
||||||
"text": "单行文本",
|
|
||||||
"textarea": "多行文本",
|
|
||||||
"ueditor": "富文本编辑器",
|
|
||||||
"date": "日期",
|
|
||||||
"datetime": "时间",
|
|
||||||
"number": "数字",
|
|
||||||
"select": "下拉框",
|
|
||||||
"radio": "单选框",
|
|
||||||
"checkbox": "复选框",
|
|
||||||
"image": "单张图片",
|
|
||||||
"images": "多张图片",
|
|
||||||
"password": "密码",
|
|
||||||
"icon": "字体图标",
|
|
||||||
"file": "单个文件",
|
|
||||||
"files": "多个文件",
|
|
||||||
"hidden": "隐藏",
|
|
||||||
"readonly": "只读文本",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 菜单类型:0-菜单 1-节点
|
|
||||||
MENU_TYPE_LIST = {
|
|
||||||
0: '菜单',
|
|
||||||
1: '节点',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 打开方式:1-内部打开 2-外部打开
|
|
||||||
MENU_TARGET_LIST = {
|
|
||||||
1: '内部打开',
|
|
||||||
2: '外部打开',
|
|
||||||
}
|
|
||||||
|
|
||||||
# 城市级别
|
|
||||||
CITY_LEVEL_LIST = {
|
|
||||||
1: "省份",
|
|
||||||
2: "城市",
|
|
||||||
3: "县区",
|
|
||||||
4: "街道"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 日志类型:1-登录,2-退出
|
|
||||||
LOG_TYPE_LIST = {
|
|
||||||
1: "登录",
|
|
||||||
2: "退出",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 登录来源:0-后台,1-前台
|
|
||||||
LOGIN_SOURCE_LIST = {
|
|
||||||
0: "后台",
|
|
||||||
1: "前台",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 登录状态:0-正常,1-异常
|
|
||||||
LOGIN_STATUS_LIST = {
|
|
||||||
0: "正常",
|
|
||||||
1: "异常",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 操作日志来源:0-后台,1-前台
|
|
||||||
OPERATION_SOURCE_LIST = {
|
|
||||||
0: "后台",
|
|
||||||
1: "前台",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 操作日志状态:0-正常,1-异常
|
|
||||||
OPERATION_STATUS_LIST = {
|
|
||||||
0: "正常",
|
|
||||||
1: "异常",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 任务状态:0-未发布,1-运行中,2-暂停,3-删除
|
|
||||||
JOB_STATUS_LIST = {
|
|
||||||
0: "未发布",
|
|
||||||
1: "运行中",
|
|
||||||
2: "暂停",
|
|
||||||
3: "删除",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 执行策略:1-立即执行,2-执行一次,3-放弃执行
|
|
||||||
JOB_EXECUTE_POLICY_LIST = {
|
|
||||||
1: "立即执行",
|
|
||||||
2: "执行一次",
|
|
||||||
3: "放弃执行",
|
|
||||||
}
|
|
||||||
|
|
||||||
# 任务日志执行状态:0-正常 1-失败
|
|
||||||
JOB_LOG_STATUS_LIST = {
|
|
||||||
0: "正常",
|
|
||||||
1: "失败",
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,85 +1,2 @@
|
|||||||
# +======================================================================
|
# 兼容垫片:由 shared/constants.py 迁移,保留旧导入路径
|
||||||
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
from shared.constants import * # noqa: F401,F403
|
||||||
# +----------------------------------------------------------------------
|
|
||||||
# | 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,17 @@
|
|||||||
|
# +======================================================================
|
||||||
|
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||||
|
# +----------------------------------------------------------------------
|
||||||
|
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
|
||||||
|
# | 开源协议: Apache-2.0
|
||||||
|
# +======================================================================
|
||||||
|
|
||||||
|
"""跨模块共享层:枚举、常量、分页、模型基类、Repository 基类。"""
|
||||||
|
|
||||||
|
from shared.constants import ( # noqa: F401
|
||||||
|
PAGE_SIZE,
|
||||||
|
DEFAULT_PASSWORD,
|
||||||
|
MESSAGE_OK,
|
||||||
|
MESSAGE_FAIL,
|
||||||
|
SERIALIZE_EXCLUDE_FIELDS,
|
||||||
|
)
|
||||||
|
from shared.enums import * # noqa: F401,F403
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# +======================================================================
|
||||||
|
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||||
|
# +----------------------------------------------------------------------
|
||||||
|
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
|
||||||
|
# | 开源协议: Apache-2.0
|
||||||
|
# +======================================================================
|
||||||
|
# 由 apps/models/base_db.py 迁移而来。
|
||||||
|
|
||||||
|
"""
|
||||||
|
数据库操作基类模块
|
||||||
|
=============================================================
|
||||||
|
|
||||||
|
提供数据库模型的通用操作方法,包括:
|
||||||
|
- 单条数据保存(save):add + flush,不提交事务
|
||||||
|
- 多条数据批量保存(save_all):add_all + flush,不提交事务
|
||||||
|
- 数据删除(delete):delete + flush,不提交事务
|
||||||
|
|
||||||
|
事务边界由 db_session_middleware 统一管理:
|
||||||
|
- 请求成功且为写操作 → 自动 commit
|
||||||
|
- 请求异常 → 自动 rollback
|
||||||
|
- 业务代码无需关心事务提交
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from core.database import get_db_session
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class base_db:
|
||||||
|
__abstract__ = True
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""
|
||||||
|
保存当前实例到数据库
|
||||||
|
|
||||||
|
将对象添加到会话并 flush(发送 SQL 到数据库获取自增 ID),
|
||||||
|
但不提交事务。事务由中间件统一管理。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
self: 当前实例,支持链式调用。通过 self.id 可获取自增主键。
|
||||||
|
"""
|
||||||
|
db = get_db_session()
|
||||||
|
db.add(self)
|
||||||
|
db.flush()
|
||||||
|
return self
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def save_all(*args):
|
||||||
|
"""批量保存多个实例到数据库"""
|
||||||
|
if not args:
|
||||||
|
return
|
||||||
|
db = get_db_session()
|
||||||
|
db.add_all(args)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""从数据库删除当前实例"""
|
||||||
|
db = get_db_session()
|
||||||
|
db.delete(self)
|
||||||
|
db.flush()
|
||||||
|
return True
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# +======================================================================
|
||||||
|
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||||
|
# +----------------------------------------------------------------------
|
||||||
|
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
|
||||||
|
# | 开源协议: Apache-2.0
|
||||||
|
# +======================================================================
|
||||||
|
# 由 apps/models/base_model.py 迁移而来。
|
||||||
|
|
||||||
|
"""数据库模型基类:通用字段、序列化与通用查询方法。"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import date, datetime, timezone
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Integer, DATETIME, and_
|
||||||
|
|
||||||
|
from core import R
|
||||||
|
from core.database import Base, get_db_session
|
||||||
|
from shared.constants import SERIALIZE_EXCLUDE_FIELDS
|
||||||
|
from utils.request import parse_id_list
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_UTC_NOW = lambda: datetime.now(tz=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
class base_model(Base):
|
||||||
|
"""
|
||||||
|
数据库模型基类
|
||||||
|
|
||||||
|
所有业务模型应继承此类,自动获得通用字段和公共方法。
|
||||||
|
|
||||||
|
提供的功能:
|
||||||
|
1. 通用字段:id、创建人、创建时间、更新人、更新时间、软删除标识
|
||||||
|
2. 序列化方法:to_dict()
|
||||||
|
3. 查询方法:get_by_id()、get_by_ids()、get_all_active()、exists()
|
||||||
|
4. 删除方法:batch_delete_r()
|
||||||
|
"""
|
||||||
|
|
||||||
|
__abstract__ = True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 通用字段定义
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True, comment="主键ID")
|
||||||
|
create_user = Column(Integer, default=0, comment="创建人")
|
||||||
|
create_time = Column(DATETIME, default=_UTC_NOW, comment="创建时间")
|
||||||
|
update_user = Column(Integer, default=0, comment="更新人")
|
||||||
|
update_time = Column(DATETIME, default=_UTC_NOW, onupdate=_UTC_NOW, comment="更新时间")
|
||||||
|
is_delete = Column(Integer, default=0, comment="删除标识:0-正常 1-已删除")
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 序列化方法
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
将模型实例转换为字典
|
||||||
|
|
||||||
|
自动排除敏感字段(is_delete、password、salt)。
|
||||||
|
datetime 类型格式化为字符串,UTC 时间转为本地时间便于前端展示。
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
for column in self.__table__.columns:
|
||||||
|
if column.name in SERIALIZE_EXCLUDE_FIELDS:
|
||||||
|
continue
|
||||||
|
value = getattr(self, column.name)
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
if value.tzinfo is not None:
|
||||||
|
# UTC 时间转为本地时间
|
||||||
|
value = value.astimezone()
|
||||||
|
value = value.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
elif isinstance(value, date):
|
||||||
|
# 纯日期类型(如生日)无时分秒,单独格式化,避免 JSON 序列化失败
|
||||||
|
value = value.strftime('%Y-%m-%d')
|
||||||
|
result[column.name] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 通用查询方法
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_id(cls, id: Optional[int]) -> Optional['base_model']:
|
||||||
|
"""根据ID获取未删除的记录,不存在返回 None"""
|
||||||
|
if id is None or id <= 0:
|
||||||
|
return None
|
||||||
|
db = get_db_session()
|
||||||
|
return db.query(cls).filter(cls.id == id, cls.is_delete == 0).first()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_by_ids(cls, ids: List[int]) -> List['base_model']:
|
||||||
|
"""根据ID列表批量获取未删除的记录"""
|
||||||
|
if not ids:
|
||||||
|
return []
|
||||||
|
valid_ids = [i for i in ids if i and i > 0]
|
||||||
|
if not valid_ids:
|
||||||
|
return []
|
||||||
|
db = get_db_session()
|
||||||
|
return db.query(cls).filter(cls.id.in_(valid_ids), cls.is_delete == 0).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_active(cls) -> List['base_model']:
|
||||||
|
"""获取所有未删除的记录"""
|
||||||
|
db = get_db_session()
|
||||||
|
return db.query(cls).filter(cls.is_delete == 0).all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def exists(cls, id: Optional[int]) -> bool:
|
||||||
|
"""检查未删除记录是否存在"""
|
||||||
|
if id is None or id <= 0:
|
||||||
|
return False
|
||||||
|
db = get_db_session()
|
||||||
|
return db.query(cls).filter(cls.id == id, cls.is_delete == 0).count() > 0
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 删除方法
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def batch_delete_r(cls, ids_str: Optional[str]):
|
||||||
|
"""
|
||||||
|
批量软删除并返回R对象
|
||||||
|
|
||||||
|
封装ID校验、解析、软删除的完整流程。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids_str: 逗号分隔的ID字符串,如 "1,2,3"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
R: 操作结果
|
||||||
|
"""
|
||||||
|
if not ids_str:
|
||||||
|
return R.failed("记录ID不存在")
|
||||||
|
|
||||||
|
id_list = parse_id_list(ids_str)
|
||||||
|
if not id_list:
|
||||||
|
return R.failed("记录ID不存在")
|
||||||
|
|
||||||
|
db = get_db_session()
|
||||||
|
records = db.query(cls).filter(and_(cls.id.in_(id_list), cls.is_delete == 0)).all()
|
||||||
|
if len(records) != len(id_list):
|
||||||
|
return R.failed("记录不存在")
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
record.is_delete = 1
|
||||||
|
|
||||||
|
return R.ok(msg="本次共删除{0}条数据".format(len(records)))
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# +======================================================================
|
||||||
|
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||||
|
# +----------------------------------------------------------------------
|
||||||
|
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
|
||||||
|
# | 开源协议: Apache-2.0
|
||||||
|
# +======================================================================
|
||||||
|
|
||||||
|
"""通用 Repository 基类:封装各模型的数据访问层公共操作。
|
||||||
|
|
||||||
|
各模块 repository.py 继承本类并提供自定义查询,service 通过 repository 访问数据库。
|
||||||
|
过渡期与 base_model 的 classmethod 双轨并存,全部模块迁移完成后收敛。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Generic, Optional, TypeVar
|
||||||
|
|
||||||
|
from core.database import get_db_session
|
||||||
|
from shared.constants import PAGE_SIZE
|
||||||
|
from utils.request import parse_id_list
|
||||||
|
|
||||||
|
M = TypeVar("M")
|
||||||
|
|
||||||
|
|
||||||
|
class BaseRepository(Generic[M]):
|
||||||
|
"""模型数据访问基类。"""
|
||||||
|
|
||||||
|
# 软删除列名,JobLogRepository 覆盖为 "del_flag"
|
||||||
|
soft_delete_col: str = "is_delete"
|
||||||
|
|
||||||
|
def __init__(self, model_cls):
|
||||||
|
self.model = model_cls
|
||||||
|
|
||||||
|
@property
|
||||||
|
def db(self):
|
||||||
|
"""当前请求的数据库会话。"""
|
||||||
|
return get_db_session()
|
||||||
|
|
||||||
|
def _soft_col(self):
|
||||||
|
return getattr(self.model, self.soft_delete_col)
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 基础查询
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def get_by_id(self, obj_id: Optional[int]) -> Optional[M]:
|
||||||
|
"""根据ID获取未删除记录。"""
|
||||||
|
if obj_id is None or obj_id <= 0:
|
||||||
|
return None
|
||||||
|
return self.db.query(self.model).filter(
|
||||||
|
self.model.id == obj_id, self._soft_col() == 0
|
||||||
|
).first()
|
||||||
|
|
||||||
|
def get_by_ids(self, ids) -> list:
|
||||||
|
"""根据ID列表批量获取未删除记录。"""
|
||||||
|
valid = [i for i in ids if i and i > 0]
|
||||||
|
if not valid:
|
||||||
|
return []
|
||||||
|
return self.db.query(self.model).filter(
|
||||||
|
self.model.id.in_(valid), self._soft_col() == 0
|
||||||
|
).all()
|
||||||
|
|
||||||
|
def get_all_active(self) -> list:
|
||||||
|
"""获取所有未删除记录。"""
|
||||||
|
return self.db.query(self.model).filter(self._soft_col() == 0).all()
|
||||||
|
|
||||||
|
def exists(self, obj_id: Optional[int]) -> bool:
|
||||||
|
"""检查未删除记录是否存在。"""
|
||||||
|
return self.get_by_id(obj_id) is not None
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 分页查询
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def paginate(self, page_no: int = 1, page_size: int = PAGE_SIZE, query=None):
|
||||||
|
"""
|
||||||
|
分页查询,返回 (items, total)。自动追加软删过滤。
|
||||||
|
传入 query 可在外部先构建过滤条件。
|
||||||
|
"""
|
||||||
|
q = query if query is not None else self.db.query(self.model)
|
||||||
|
q = q.filter(self._soft_col() == 0)
|
||||||
|
total = q.count()
|
||||||
|
items = q.limit(page_size).offset((page_no - 1) * page_size).all()
|
||||||
|
return items, total
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 写操作
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def create(self, **fields) -> M:
|
||||||
|
"""创建记录(不提交事务,由中间件统一管理)。"""
|
||||||
|
obj = self.model(**fields)
|
||||||
|
obj.save()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def update(self, obj_id, data: dict) -> int:
|
||||||
|
"""按ID更新,返回受影响行数。"""
|
||||||
|
return self.db.query(self.model).filter(
|
||||||
|
self.model.id == obj_id, self._soft_col() == 0
|
||||||
|
).update(data)
|
||||||
|
|
||||||
|
def soft_delete(self, ids) -> bool:
|
||||||
|
"""
|
||||||
|
批量软删除(等价 base_model.batch_delete_r 的语义)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ids: 逗号分隔字符串或整数列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 是否全部删除成功(ID 数需与匹配记录数一致)
|
||||||
|
"""
|
||||||
|
id_list = parse_id_list(ids) if isinstance(ids, str) else [i for i in ids if i]
|
||||||
|
if not id_list:
|
||||||
|
return False
|
||||||
|
records = self.db.query(self.model).filter(
|
||||||
|
self.model.id.in_(id_list), self._soft_col() == 0
|
||||||
|
).all()
|
||||||
|
if len(records) != len(id_list):
|
||||||
|
return False
|
||||||
|
for record in records:
|
||||||
|
setattr(record, self.soft_delete_col, 1)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def hard_delete(self, obj) -> bool:
|
||||||
|
"""物理删除实例。"""
|
||||||
|
self.db.delete(obj)
|
||||||
|
self.db.flush()
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 自定义查询出口
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
def filter(self, *criterion):
|
||||||
|
"""返回过滤后的 query,供子类组合复杂查询。"""
|
||||||
|
return self.db.query(self.model).filter(*criterion)
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# +======================================================================
|
||||||
|
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||||
|
# +----------------------------------------------------------------------
|
||||||
|
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
|
||||||
|
# | 开源协议: Apache-2.0
|
||||||
|
# +======================================================================
|
||||||
|
|
||||||
|
"""Service 基类(可选):为面向对象的 service 提供 repository 与响应便捷属性。
|
||||||
|
|
||||||
|
本项目现有 service 为模块级函数风格,如后续需要类风格 service 可继承本类。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from core import response as R
|
||||||
|
|
||||||
|
|
||||||
|
class BaseService:
|
||||||
|
"""Service 基类:绑定 repository 与统一响应助手。"""
|
||||||
|
|
||||||
|
repo = None
|
||||||
|
|
||||||
|
def __init__(self, repo=None):
|
||||||
|
self.repo = repo or self.repo
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ok(self):
|
||||||
|
return R.ok
|
||||||
|
|
||||||
|
@property
|
||||||
|
def failed(self):
|
||||||
|
return R.failed
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# +======================================================================
|
||||||
|
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||||
|
# +----------------------------------------------------------------------
|
||||||
|
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
|
||||||
|
# | 开源协议: Apache-2.0
|
||||||
|
# +======================================================================
|
||||||
|
# 由 apps/constants/message.py 迁移而来。
|
||||||
|
|
||||||
|
"""全局常量:分页、默认密码、消息文本、序列化排除字段。"""
|
||||||
|
|
||||||
|
# ======================= 分页配置 ==========================
|
||||||
|
# 默认每页显示记录数
|
||||||
|
PAGE_SIZE = 10
|
||||||
|
|
||||||
|
# ======================= 用户配置 ==========================
|
||||||
|
# 默认密码(新创建用户或重置密码时的默认密码,建议首次登录后强制修改)
|
||||||
|
DEFAULT_PASSWORD = "123456"
|
||||||
|
|
||||||
|
# ======================= 消息提示配置 ==========================
|
||||||
|
# 操作成功提示信息
|
||||||
|
MESSAGE_OK = "操作成功"
|
||||||
|
|
||||||
|
# 操作失败提示信息
|
||||||
|
MESSAGE_FAIL = "操作失败"
|
||||||
|
|
||||||
|
# ======================= 序列化安全配置 ==========================
|
||||||
|
# 模型序列化时统一排除的敏感字段,防止密码、盐值等泄露给前端
|
||||||
|
SERIALIZE_EXCLUDE_FIELDS = frozenset({'is_delete', 'password', 'salt'})
|
||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
# +======================================================================
|
||||||
|
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||||
|
# +----------------------------------------------------------------------
|
||||||
|
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
|
||||||
|
# | 开源协议: Apache-2.0
|
||||||
|
# +======================================================================
|
||||||
|
# 由 apps/constants/constants.py 迁移而来。
|
||||||
|
|
||||||
|
"""业务枚举字典:各模块的状态/类型展示映射。"""
|
||||||
|
|
||||||
|
# 性别
|
||||||
|
GENDER_LIST = {
|
||||||
|
1: "男",
|
||||||
|
2: "女",
|
||||||
|
3: "保密",
|
||||||
|
}
|
||||||
|
# 部门类型
|
||||||
|
DEPT_TYPE_LIST = {
|
||||||
|
1: "公司",
|
||||||
|
2: "子公司",
|
||||||
|
3: "部门",
|
||||||
|
4: "小组",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 站点类型
|
||||||
|
ITEM_TYPE_LIST = {
|
||||||
|
1: "普通站点",
|
||||||
|
2: "其他站点"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 友链类型:1友情链接 2合作伙伴
|
||||||
|
LINK_TYPE_LIST = {
|
||||||
|
1: "友情链接",
|
||||||
|
2: "合作伙伴"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 友链投放平台:1PC站 2WAP站 3微信小程序 4APP应用
|
||||||
|
LINK_PLATFORM_LIST = {
|
||||||
|
1: "PC站",
|
||||||
|
2: "WAP站",
|
||||||
|
3: "微信小程序",
|
||||||
|
4: "APP应用"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 友链形式:1文字链接 2图片链接
|
||||||
|
LINK_FORM_LIST = {
|
||||||
|
1: "文字链接",
|
||||||
|
2: "图片链接",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 广告投放平台:1PC站 2WAP站 3微信小程序 4APP应用
|
||||||
|
AD_SORT_PLATFORM_LIST = {
|
||||||
|
1: "PC站",
|
||||||
|
2: "WAP站",
|
||||||
|
3: "微信小程序",
|
||||||
|
4: "APP应用"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 广告类型:1图片 2文字 3视频 4推荐
|
||||||
|
AD_TYPE_LIST = {
|
||||||
|
1: "图片",
|
||||||
|
2: "文字",
|
||||||
|
3: "视频",
|
||||||
|
4: "推荐"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 通知来源:1官方平台 2开源中国 3CSDN官方 4新浪微博
|
||||||
|
NOTICE_SOURCE_LIST = {
|
||||||
|
1: "官方平台",
|
||||||
|
2: "开源中国",
|
||||||
|
3: "CSDN官方",
|
||||||
|
4: "新浪微博",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 会员注册来源:1-网站注册 2-客户端注册 3-小程序注册 4-手机站注册 5-后台添加
|
||||||
|
MEMBER_SOURCE_LIST = {
|
||||||
|
1: "网站注册",
|
||||||
|
2: "客户端注册",
|
||||||
|
3: "小程序注册",
|
||||||
|
4: "手机站注册",
|
||||||
|
5: "后台添加"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 配置项类型
|
||||||
|
CONFIG_DATA_TYPE_LIST = {
|
||||||
|
"text": "单行文本",
|
||||||
|
"textarea": "多行文本",
|
||||||
|
"ueditor": "富文本编辑器",
|
||||||
|
"date": "日期",
|
||||||
|
"datetime": "时间",
|
||||||
|
"number": "数字",
|
||||||
|
"select": "下拉框",
|
||||||
|
"radio": "单选框",
|
||||||
|
"checkbox": "复选框",
|
||||||
|
"image": "单张图片",
|
||||||
|
"images": "多张图片",
|
||||||
|
"password": "密码",
|
||||||
|
"icon": "字体图标",
|
||||||
|
"file": "单个文件",
|
||||||
|
"files": "多个文件",
|
||||||
|
"hidden": "隐藏",
|
||||||
|
"readonly": "只读文本",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 菜单类型:0-菜单 1-节点
|
||||||
|
MENU_TYPE_LIST = {
|
||||||
|
0: '菜单',
|
||||||
|
1: '节点',
|
||||||
|
}
|
||||||
|
|
||||||
|
# 打开方式:1-内部打开 2-外部打开
|
||||||
|
MENU_TARGET_LIST = {
|
||||||
|
1: '内部打开',
|
||||||
|
2: '外部打开',
|
||||||
|
}
|
||||||
|
|
||||||
|
# 城市级别
|
||||||
|
CITY_LEVEL_LIST = {
|
||||||
|
1: "省份",
|
||||||
|
2: "城市",
|
||||||
|
3: "县区",
|
||||||
|
4: "街道"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 日志类型:1-登录,2-退出
|
||||||
|
LOG_TYPE_LIST = {
|
||||||
|
1: "登录",
|
||||||
|
2: "退出",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 登录来源:0-后台,1-前台
|
||||||
|
LOGIN_SOURCE_LIST = {
|
||||||
|
0: "后台",
|
||||||
|
1: "前台",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 登录状态:0-正常,1-异常
|
||||||
|
LOGIN_STATUS_LIST = {
|
||||||
|
0: "正常",
|
||||||
|
1: "异常",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 操作日志来源:0-后台,1-前台
|
||||||
|
OPERATION_SOURCE_LIST = {
|
||||||
|
0: "后台",
|
||||||
|
1: "前台",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 操作日志状态:0-正常,1-异常
|
||||||
|
OPERATION_STATUS_LIST = {
|
||||||
|
0: "正常",
|
||||||
|
1: "异常",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 任务状态:0-未发布,1-运行中,2-暂停,3-删除
|
||||||
|
JOB_STATUS_LIST = {
|
||||||
|
0: "未发布",
|
||||||
|
1: "运行中",
|
||||||
|
2: "暂停",
|
||||||
|
3: "删除",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行策略:1-立即执行,2-执行一次,3-放弃执行
|
||||||
|
JOB_EXECUTE_POLICY_LIST = {
|
||||||
|
1: "立即执行",
|
||||||
|
2: "执行一次",
|
||||||
|
3: "放弃执行",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 任务日志执行状态:0-正常 1-失败
|
||||||
|
JOB_LOG_STATUS_LIST = {
|
||||||
|
0: "正常",
|
||||||
|
1: "失败",
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# +======================================================================
|
||||||
|
# | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ]
|
||||||
|
# +----------------------------------------------------------------------
|
||||||
|
# | Copyright (c) 2021-2026 一米阳光团队 版权所有
|
||||||
|
# | 开源协议: Apache-2.0
|
||||||
|
# +======================================================================
|
||||||
|
|
||||||
|
"""分页统一封装:解析 pageNo/pageSize 并对 SQLAlchemy query 应用分页。"""
|
||||||
|
|
||||||
|
from shared.constants import PAGE_SIZE
|
||||||
|
|
||||||
|
|
||||||
|
def get_page_params(query_params, default_size: int = PAGE_SIZE) -> tuple[int, int]:
|
||||||
|
"""
|
||||||
|
从 request.query_params 解析分页参数。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query_params: request.query_params(类 dict 对象)
|
||||||
|
default_size: 默认每页条数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(page, size):page>=1,size>=1
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
page = int(query_params.get('pageNo', 1) or 1)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
page = 1
|
||||||
|
try:
|
||||||
|
size = int(query_params.get('pageSize', default_size) or default_size)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
size = default_size
|
||||||
|
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
if size < 1:
|
||||||
|
size = default_size
|
||||||
|
return page, size
|
||||||
|
|
||||||
|
|
||||||
|
def paginate_query(query, page: int, size: int) -> tuple[list, int]:
|
||||||
|
"""
|
||||||
|
对 SQLAlchemy query 应用分页并返回 (items, total)。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: 已构建的 SQLAlchemy Query 对象(不含 count/limit/offset)
|
||||||
|
page: 页码(>=1)
|
||||||
|
size: 每页条数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(items, total):当前页数据列表与总记录数
|
||||||
|
"""
|
||||||
|
total = query.count()
|
||||||
|
items = query.limit(size).offset((page - 1) * size).all()
|
||||||
|
return items, total
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"""shared/pagination.py 分页工具单元测试。"""
|
||||||
|
|
||||||
|
from shared.pagination import get_page_params
|
||||||
|
|
||||||
|
|
||||||
|
def test_defaults():
|
||||||
|
page, size = get_page_params({})
|
||||||
|
assert page == 1
|
||||||
|
assert size == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_valid_params():
|
||||||
|
page, size = get_page_params({"pageNo": "3", "pageSize": "20"})
|
||||||
|
assert page == 3
|
||||||
|
assert size == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_params_fall_back():
|
||||||
|
page, size = get_page_params({"pageNo": "abc", "pageSize": "0"})
|
||||||
|
assert page == 1
|
||||||
|
assert size == 10
|
||||||
Reference in New Issue
Block a user