init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
This commit is contained in:
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
from app.config.setting import settings
|
||||
|
||||
|
||||
class CommonConstant:
|
||||
"""常用常量
|
||||
|
||||
HTTP: http请求
|
||||
HTTPS: https请求
|
||||
YES: 是否为系统默认(是)
|
||||
NO: 是否为系统默认(否)
|
||||
UNIQUE: 校验是否唯一的返回标识(是)
|
||||
NOT_UNIQUE: 校验是否唯一的返回标识(否)
|
||||
"""
|
||||
|
||||
# 域名相关
|
||||
HTTP = "http://"
|
||||
HTTPS = "https://"
|
||||
|
||||
# 系统标识
|
||||
YES = "Y"
|
||||
NO = "N"
|
||||
|
||||
# 唯一性校验
|
||||
UNIQUE = True
|
||||
NOT_UNIQUE = False
|
||||
|
||||
|
||||
class JobConstant:
|
||||
"""定时任务常量
|
||||
|
||||
JOB_ERROR_LIST: 定时任务禁止调用模块及违规字符串列表
|
||||
JOB_WHITE_LIST: 定时任务允许调用模块列表
|
||||
"""
|
||||
|
||||
JOB_ERROR_LIST = [
|
||||
"app",
|
||||
"config",
|
||||
"exceptions",
|
||||
"import ",
|
||||
"middlewares",
|
||||
"module_admin",
|
||||
"open(",
|
||||
"os.",
|
||||
"server",
|
||||
"sub_applications",
|
||||
"subprocess.",
|
||||
"sys.",
|
||||
"utils",
|
||||
"while ",
|
||||
"__import__",
|
||||
"'\"",
|
||||
"''",
|
||||
",",
|
||||
"?",
|
||||
":",
|
||||
";",
|
||||
"/",
|
||||
"|",
|
||||
"+",
|
||||
"-",
|
||||
"=",
|
||||
"~",
|
||||
"!",
|
||||
"#",
|
||||
"$",
|
||||
"%",
|
||||
"^",
|
||||
"&",
|
||||
"*",
|
||||
"<",
|
||||
">",
|
||||
"(",
|
||||
")",
|
||||
"[",
|
||||
"]",
|
||||
"{",
|
||||
"}",
|
||||
" ",
|
||||
]
|
||||
JOB_WHITE_LIST = ["function_task"]
|
||||
|
||||
|
||||
class GenConstant:
|
||||
"""代码生成常量
|
||||
|
||||
COLUMNTYPE_STR: 数据库字符串类型
|
||||
COLUMNTYPE_TEXT: 数据库文本类型
|
||||
COLUMNTYPE_TIME: 数据库时间类型
|
||||
COLUMNTYPE_GEOMETRY: 数据库字空间类型
|
||||
COLUMNTYPE_NUMBER: 数据库数字类型
|
||||
COLUMNNAME_NOT_EDIT: 页面不需要编辑字段
|
||||
COLUMNNAME_NOT_LIST: 页面不需要显示的列表字段
|
||||
COLUMNNAME_NOT_QUERY: 页面不需要查询字段
|
||||
BASE_ENTITY: Entity基类字段
|
||||
TREE_ENTITY: Tree基类字段
|
||||
HTML_INPUT: 文本框
|
||||
HTML_TEXTAREA: 文本域
|
||||
HTML_SELECT: 下拉框
|
||||
HTML_RADIO: 单选框
|
||||
HTML_CHECKBOX: 复选框
|
||||
HTML_DATETIME: 日期控件
|
||||
HTML_IMAGE_UPLOAD: 图片上传控件
|
||||
HTML_FILE_UPLOAD: 文件上传控件
|
||||
HTML_EDITOR: 富文本控件
|
||||
TYPE_DECIMAL: 高精度计算类型
|
||||
TYPE_DATE: 时间类型
|
||||
QUERY_LIKE: 模糊查询
|
||||
QUERY_EQ: 相等查询
|
||||
REQUIRE: 需要
|
||||
DB_TO_SQLALCHEMY_TYPE_MAPPING: 数据库类型与sqlalchemy类型映射
|
||||
DB_TO_PYTHON_TYPE_MAPPING: 数据库类型与python类型映射
|
||||
"""
|
||||
|
||||
# 数据库字符串类型
|
||||
COLUMNTYPE_STR = ["character varying", "varchar", "character", "char"] if settings.DATABASE_TYPE == "postgres" else ["char", "varchar", "nvarchar", "varchar2"]
|
||||
|
||||
# 数据库文本类型
|
||||
COLUMNTYPE_TEXT = ["text", "citext"] if settings.DATABASE_TYPE == "postgres" else ["tinytext", "text", "mediumtext", "longtext"]
|
||||
|
||||
# 数据库时间类型
|
||||
COLUMNTYPE_TIME = (
|
||||
[
|
||||
"date",
|
||||
"time",
|
||||
"time with time zone",
|
||||
"time without time zone",
|
||||
"timestamp",
|
||||
"timestamp with time zone",
|
||||
"timestamp without time zone",
|
||||
"interval",
|
||||
]
|
||||
if settings.DATABASE_TYPE == "postgres"
|
||||
else ["datetime", "time", "date", "timestamp"]
|
||||
)
|
||||
|
||||
# 数据库字空间类型
|
||||
COLUMNTYPE_GEOMETRY = (
|
||||
["point", "line", "lseg", "box", "path", "polygon", "circle"]
|
||||
if settings.DATABASE_TYPE == "postgres"
|
||||
else [
|
||||
"geometry",
|
||||
"point",
|
||||
"linestring",
|
||||
"polygon",
|
||||
"multipoint",
|
||||
"multilinestring",
|
||||
"multipolygon",
|
||||
"geometrycollection",
|
||||
]
|
||||
)
|
||||
|
||||
# 数据库数字类型
|
||||
COLUMNTYPE_NUMBER = [
|
||||
"tinyint",
|
||||
"smallint",
|
||||
"mediumint",
|
||||
"int",
|
||||
"number",
|
||||
"integer",
|
||||
"bit",
|
||||
"bigint",
|
||||
"float",
|
||||
"double",
|
||||
"decimal",
|
||||
"boolean",
|
||||
"bool",
|
||||
]
|
||||
# 页面不需要显示的添加字段
|
||||
COLUMNNAME_NOT_ADD_SHOW = ["created_time", "updated_time"]
|
||||
|
||||
# 页面不需要显示的编辑字段
|
||||
COLUMNNAME_NOT_EDIT_SHOW = ["uuid"]
|
||||
|
||||
# 页面不需要编辑字段
|
||||
COLUMNNAME_NOT_EDIT = ["id", "uuid", "created_time", "updated_time"]
|
||||
|
||||
# 页面不需要显示的列表字段
|
||||
COLUMNNAME_NOT_LIST = ["id", "uuid"]
|
||||
|
||||
# 页面不需要查询字段
|
||||
COLUMNNAME_NOT_QUERY = ["id", "uuid", "description"]
|
||||
|
||||
# Crud基类字段
|
||||
CRUD_COLUMN_NOT_EDIT = [
|
||||
"create_by",
|
||||
"description",
|
||||
"created_time",
|
||||
"updated_time",
|
||||
]
|
||||
|
||||
# 实体基类字段
|
||||
BASE_ENTITY = [
|
||||
"id",
|
||||
"uuid",
|
||||
"status",
|
||||
"description",
|
||||
"created_time",
|
||||
"updated_time",
|
||||
"created_id",
|
||||
"updated_id",
|
||||
]
|
||||
|
||||
# Tree基类字段
|
||||
TREE_ENTITY = [
|
||||
"parent_name",
|
||||
"parent_id",
|
||||
"order",
|
||||
"ancestors",
|
||||
"children",
|
||||
]
|
||||
|
||||
# 文本框
|
||||
HTML_INPUT = "input"
|
||||
|
||||
# 文本域
|
||||
HTML_TEXTAREA = "textarea"
|
||||
|
||||
# 下拉框
|
||||
HTML_SELECT = "select"
|
||||
|
||||
# 单选框
|
||||
HTML_RADIO = "radio"
|
||||
|
||||
# 复选框
|
||||
HTML_CHECKBOX = "checkbox"
|
||||
|
||||
# 日期控件
|
||||
HTML_DATETIME = "datetime"
|
||||
|
||||
# 图片上传控件
|
||||
HTML_IMAGE_UPLOAD = "imageUpload"
|
||||
|
||||
# 文件上传控件
|
||||
HTML_FILE_UPLOAD = "fileUpload"
|
||||
|
||||
# 富文本控件
|
||||
HTML_EDITOR = "editor"
|
||||
|
||||
# 高精度计算类型
|
||||
TYPE_DECIMAL = "Decimal"
|
||||
|
||||
# 时间类型
|
||||
TYPE_DATE = ["date", "time", "datetime"]
|
||||
|
||||
# 模糊查询
|
||||
QUERY_LIKE = "LIKE"
|
||||
|
||||
# 相等查询
|
||||
QUERY_EQ = "EQ"
|
||||
|
||||
# 需要
|
||||
REQUIRE = True
|
||||
|
||||
# 数据库类型与sqlalchemy类型映射
|
||||
DB_TO_SQLALCHEMY = (
|
||||
{
|
||||
"boolean": "Boolean",
|
||||
"smallint": "SmallInteger",
|
||||
"integer": "Integer",
|
||||
"int4": "Integer",
|
||||
"bigint": "BigInteger",
|
||||
"real": "Float",
|
||||
"double precision": "Float",
|
||||
"numeric": "Numeric",
|
||||
"decimal": "Numeric",
|
||||
"character varying": "String",
|
||||
"varchar": "String",
|
||||
"character": "String",
|
||||
"text": "Text",
|
||||
"bytea": "LargeBinary",
|
||||
"date": "Date",
|
||||
"time": "Time",
|
||||
"time with time zone": "Time",
|
||||
"time without time zone": "Time",
|
||||
"timestamp": "DateTime",
|
||||
"timestamp with time zone": "DateTime",
|
||||
"timestamp without time zone": "DateTime",
|
||||
"interval": "Interval",
|
||||
"json": "JSON",
|
||||
"jsonb": "JSONB",
|
||||
"uuid": "Uuid",
|
||||
"inet": "INET",
|
||||
"cidr": "CIDR",
|
||||
"macaddr": "MACADDR",
|
||||
"point": "Geometry",
|
||||
"line": "Geometry",
|
||||
"lseg": "Geometry",
|
||||
"box": "Geometry",
|
||||
"path": "Geometry",
|
||||
"polygon": "Geometry",
|
||||
"circle": "Geometry",
|
||||
"bit": "Bit",
|
||||
"bit varying": "Bit",
|
||||
"tsvector": "TSVECTOR",
|
||||
"tsquery": "TSQUERY",
|
||||
"xml": "String",
|
||||
"array": "ARRAY",
|
||||
"composite": "JSON",
|
||||
"enum": "Enum",
|
||||
"range": "Range",
|
||||
"money": "Numeric",
|
||||
"pg_lsn": "BigInteger",
|
||||
"txid_snapshot": "String",
|
||||
"oid": "BigInteger",
|
||||
"regproc": "String",
|
||||
"regclass": "String",
|
||||
"regtype": "String",
|
||||
"regrole": "String",
|
||||
"regnamespace": "String",
|
||||
"int2vector": "ARRAY",
|
||||
"oidvector": "ARRAY",
|
||||
"pg_node_tree": "Text",
|
||||
}
|
||||
if settings.DATABASE_TYPE == "postgres"
|
||||
else {
|
||||
# 布尔语义仅 tinyint(1),其余 tinyint 在 get_sqlalchemy_type 中映射为 SmallInteger
|
||||
"TINYINT": "SmallInteger",
|
||||
# 数值类型
|
||||
"SMALLINT": "SmallInteger",
|
||||
"MEDIUMINT": "Integer",
|
||||
"INT": "Integer",
|
||||
"INTEGER": "Integer",
|
||||
"BIGINT": "BigInteger",
|
||||
"FLOAT": "Float",
|
||||
"DOUBLE": "Float",
|
||||
"DECIMAL": "DECIMAL",
|
||||
"BIT": "Integer",
|
||||
"NUMERIC": "Numeric",
|
||||
# 日期和时间类型
|
||||
"DATE": "Date",
|
||||
"TIME": "Time",
|
||||
"DATETIME": "DateTime",
|
||||
"TIMESTAMP": "TIMESTAMP",
|
||||
"YEAR": "Integer",
|
||||
# 字符串类型
|
||||
"CHAR": "CHAR",
|
||||
"VARCHAR": "String",
|
||||
"TINYTEXT": "Text",
|
||||
"TEXT": "Text",
|
||||
"MEDIUMTEXT": "Text",
|
||||
"LONGTEXT": "Text",
|
||||
"BINARY": "BINARY",
|
||||
"VARBINARY": "VARBINARY",
|
||||
"TINYBLOB": "LargeBinary",
|
||||
"BLOB": "LargeBinary",
|
||||
"MEDIUMBLOB": "LargeBinary",
|
||||
"LONGBLOB": "LargeBinary",
|
||||
# 枚举和集合类型
|
||||
"ENUM": "Enum",
|
||||
"SET": "String",
|
||||
# JSON 类型
|
||||
"JSON": "JSON",
|
||||
# 空间数据类型(需要扩展支持,如 GeoAlchemy2)
|
||||
"GEOMETRY": "Geometry", # 需要安装 geoalchemy2
|
||||
"POINT": "Geometry",
|
||||
"LINESTRING": "Geometry",
|
||||
"POLYGON": "Geometry",
|
||||
"MULTIPOINT": "Geometry",
|
||||
"MULTILINESTRING": "Geometry",
|
||||
"MULTIPOLYGON": "Geometry",
|
||||
"GEOMETRYCOLLECTION": "Geometry",
|
||||
# 其他类型
|
||||
"BOOL": "Boolean",
|
||||
"UUID": "String",
|
||||
}
|
||||
)
|
||||
|
||||
# 数据库类型与python类型映射
|
||||
DB_TO_PYTHON = {
|
||||
# MySQL 整数类型
|
||||
"tinyint": "int",
|
||||
"smallint": "int",
|
||||
"mediumint": "int",
|
||||
"int": "int",
|
||||
"integer": "int",
|
||||
"bigint": "int",
|
||||
# MySQL 浮点类型
|
||||
"float": "float",
|
||||
"double": "float",
|
||||
"decimal": "Decimal",
|
||||
"numeric": "Decimal",
|
||||
# MySQL 字符串类型
|
||||
"char": "str",
|
||||
"varchar": "str",
|
||||
"tinytext": "str",
|
||||
"text": "str",
|
||||
"mediumtext": "str",
|
||||
"longtext": "str",
|
||||
# MySQL 二进制类型
|
||||
"binary": "bytes",
|
||||
"varbinary": "bytes",
|
||||
"tinyblob": "bytes",
|
||||
"blob": "bytes",
|
||||
"mediumblob": "bytes",
|
||||
"longblob": "bytes",
|
||||
# MySQL 日期时间类型
|
||||
"date": "date",
|
||||
"time": "time",
|
||||
"datetime": "datetime",
|
||||
"timestamp": "datetime",
|
||||
"year": "int",
|
||||
# MySQL 其他类型
|
||||
"json": "dict",
|
||||
"enum": "str",
|
||||
"set": "str",
|
||||
"bit": "int",
|
||||
# MySQL 空间数据类型
|
||||
"geometry": "bytes",
|
||||
"linestring": "bytes",
|
||||
"multipoint": "bytes",
|
||||
"multilinestring": "bytes",
|
||||
"multipolygon": "bytes",
|
||||
"geometrycollection": "bytes",
|
||||
# PostgreSQL 整数类型
|
||||
"int2": "int",
|
||||
"int4": "int",
|
||||
"int8": "int",
|
||||
# PostgreSQL 浮点类型
|
||||
"real": "float",
|
||||
"double precision": "float",
|
||||
"float8": "float",
|
||||
# PostgreSQL 字符串类型
|
||||
"character": "str",
|
||||
"character varying": "str",
|
||||
"citext": "str",
|
||||
# PostgreSQL 二进制类型
|
||||
"bytea": "bytes",
|
||||
# PostgreSQL 日期时间类型
|
||||
"time with time zone": "time",
|
||||
"timetz": "time",
|
||||
"time without time zone": "time",
|
||||
"timestamptz": "datetime",
|
||||
"timestamp with time zone": "datetime",
|
||||
"timestamp without time zone": "datetime",
|
||||
"interval": "timedelta",
|
||||
# PostgreSQL 布尔类型
|
||||
"boolean": "bool",
|
||||
"bool": "bool",
|
||||
# PostgreSQL JSON类型
|
||||
"jsonb": "dict",
|
||||
# PostgreSQL 其他类型
|
||||
"uuid": "str",
|
||||
"inet": "str",
|
||||
"cidr": "str",
|
||||
"macaddr": "str",
|
||||
# PostgreSQL 几何类型
|
||||
"point": "list",
|
||||
"line": "list",
|
||||
"lseg": "list",
|
||||
"box": "list",
|
||||
"path": "list",
|
||||
"polygon": "list",
|
||||
"circle": "list",
|
||||
# PostgreSQL 位类型
|
||||
"bit varying": "int",
|
||||
"varbit": "int",
|
||||
# PostgreSQL 文本搜索类型
|
||||
"tsvector": "str",
|
||||
"tsquery": "str",
|
||||
# PostgreSQL XML类型
|
||||
"xml": "str",
|
||||
# PostgreSQL 数组类型
|
||||
"array": "list",
|
||||
# PostgreSQL 范围类型
|
||||
"range": "list",
|
||||
"int4range": "list",
|
||||
"int8range": "list",
|
||||
"tsrange": "list",
|
||||
"tstzrange": "list",
|
||||
"daterange": "list",
|
||||
# PostgreSQL 货币类型
|
||||
"money": "Decimal",
|
||||
# PostgreSQL 对象标识符类型
|
||||
"oid": "int",
|
||||
"regproc": "str",
|
||||
"regclass": "str",
|
||||
"regtype": "str",
|
||||
"regrole": "str",
|
||||
"regnamespace": "str",
|
||||
# PostgreSQL 向量类型
|
||||
"int2vector": "list",
|
||||
"oidvector": "list",
|
||||
# PostgreSQL 其他内部类型
|
||||
"pg_lsn": "int",
|
||||
"txid_snapshot": "str",
|
||||
"pg_node_tree": "str",
|
||||
}
|
||||
|
||||
|
||||
# API 日期 / 时间 / 日期时间统一展示(validator、jsonable_response_content、文档约定一致)
|
||||
DATE_DISPLAY_FMT = "%Y-%m-%d"
|
||||
TIME_DISPLAY_FMT = "%H:%M:%S"
|
||||
DATETIME_DISPLAY_FMT = "%Y-%m-%d %H:%M:%S"
|
||||
@@ -0,0 +1,89 @@
|
||||
import dataclasses
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import Response
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class IpInfo:
|
||||
"""IP 归属地解析结果。"""
|
||||
|
||||
ip: str
|
||||
country: str | None
|
||||
region: str | None
|
||||
city: str | None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class UserAgentInfo:
|
||||
"""User-Agent 解析结果(操作系统、浏览器、设备)。"""
|
||||
|
||||
user_agent: str
|
||||
os: str | None
|
||||
browser: str | None
|
||||
device: str | None
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RequestCallNext:
|
||||
"""请求链路 call_next 封装结果(状态码、消息、异常、响应)。"""
|
||||
|
||||
code: str
|
||||
msg: str
|
||||
err: Exception | None
|
||||
response: Response
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class AccessToken:
|
||||
"""访问令牌及过期时间、会话 UUID。"""
|
||||
|
||||
access_token: str
|
||||
access_token_expire_time: datetime
|
||||
session_uuid: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class RefreshToken:
|
||||
"""刷新令牌及过期时间。"""
|
||||
|
||||
refresh_token: str
|
||||
refresh_token_expire_time: datetime
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class NewToken:
|
||||
"""刷新后的一对访问/刷新令牌及会话 UUID。"""
|
||||
|
||||
new_access_token: str
|
||||
new_access_token_expire_time: datetime
|
||||
new_refresh_token: str
|
||||
new_refresh_token_expire_time: datetime
|
||||
session_uuid: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class TokenPayload:
|
||||
"""JWT/会话载荷中的用户与会话标识。"""
|
||||
|
||||
id: int
|
||||
session_uuid: str
|
||||
expire_time: datetime
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class UploadUrl:
|
||||
"""上传完成后的访问 URL。"""
|
||||
|
||||
url: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SnowflakeInfo:
|
||||
"""雪花 ID 拆解后的各段信息。"""
|
||||
|
||||
timestamp: int
|
||||
datetime: str
|
||||
cluster_id: int
|
||||
node_id: int
|
||||
sequence: int
|
||||
@@ -0,0 +1,206 @@
|
||||
from enum import Enum, unique
|
||||
|
||||
|
||||
@unique
|
||||
class EnvironmentEnum(str, Enum):
|
||||
"""应用运行环境(开发 / 生产)。"""
|
||||
|
||||
DEV = "dev"
|
||||
PROD = "prod"
|
||||
|
||||
|
||||
@unique
|
||||
class BusinessType(Enum):
|
||||
"""业务操作类型
|
||||
|
||||
OTHER: 其它
|
||||
INSERT: 新增
|
||||
UPDATE: 修改
|
||||
DELETE: 删除
|
||||
GRANT: 授权
|
||||
EXPORT: 导出
|
||||
IMPORT: 导入
|
||||
FORCE: 强退
|
||||
GENCODE: 生成代码
|
||||
CLEAN: 清空数据
|
||||
"""
|
||||
|
||||
OTHER = 0
|
||||
INSERT = 1
|
||||
UPDATE = 2
|
||||
DELETE = 3
|
||||
GRANT = 4
|
||||
EXPORT = 5
|
||||
IMPORT = 6
|
||||
FORCE = 7
|
||||
GENCODE = 8
|
||||
CLEAN = 9
|
||||
|
||||
|
||||
@unique
|
||||
class RedisInitKeyConfig(Enum):
|
||||
"""系统内置Redis键名枚举"""
|
||||
|
||||
ACCESS_TOKEN = {"key": "access_token", "remark": "登录令牌信息"}
|
||||
REFRESH_TOKEN = {"key": "refresh_token", "remark": "刷新令牌信息"}
|
||||
USER_SESSION = {"key": "user_session", "remark": "用户会话信息"}
|
||||
CAPTCHA_CODES = {"key": "captcha_codes", "remark": "图片验证码"}
|
||||
EMAIL_CODES = {"key": "email_codes", "remark": "邮箱验证码"}
|
||||
SYSTEM_CONFIG = {"key": "system_config", "remark": "系统配置"}
|
||||
SYSTEM_DICT = {"key": "system_dict", "remark": "数据字典"}
|
||||
APSCHEDULER_LOCK_KEY = {"key": "scheduler_job_lock", "remark": "定时任务初始化锁"}
|
||||
AI_MODEL_CONFIG = {"key": "ai_model_config", "remark": "用户AI模型配置"}
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""获取 Redis 键名。"""
|
||||
return self.value.get("key", "")
|
||||
|
||||
@property
|
||||
def remark(self) -> str:
|
||||
"""获取 Redis 键说明。"""
|
||||
return self.value.get("remark", "")
|
||||
|
||||
|
||||
class SysParamKey(str, Enum):
|
||||
"""系统参数 config_key 常量定义
|
||||
|
||||
所有 sys_param 表的 config_key 值统一在此定义,避免前端/后端各处写死字符串导致拼写错误。
|
||||
"""
|
||||
|
||||
SYS_NAME = "sys_name"
|
||||
LOGO_URL = "logo_url"
|
||||
FAVICON = "favicon"
|
||||
LOGIN_BG = "login_bg"
|
||||
VERSION = "version"
|
||||
HELP_DOC = "help_doc"
|
||||
GIT_CODE = "git_code"
|
||||
COPYRIGHT = "copyright"
|
||||
KEEP_RECORD = "keep_record"
|
||||
PRIVACY = "privacy"
|
||||
CLAUSE = "clause"
|
||||
DEMO_ENABLE = "demo_enable"
|
||||
IP_WHITE_LIST = "ip_white_list"
|
||||
IP_BLACK_LIST = "ip_black_list"
|
||||
LOGIN_TITLE = "login_title"
|
||||
LOGIN_SUBTITLE = "login_subtitle"
|
||||
IP_LOCATION_ENABLE = "ip_location_enable"
|
||||
|
||||
|
||||
@unique
|
||||
class QueueEnum(str, Enum):
|
||||
"""队列枚举"""
|
||||
|
||||
none = "None"
|
||||
not_none = "not None"
|
||||
date = "date"
|
||||
month = "month"
|
||||
like = "like"
|
||||
eq = "eq"
|
||||
in_ = "in"
|
||||
between = "between"
|
||||
ne = "!="
|
||||
gt = ">"
|
||||
ge = ">="
|
||||
lt = "<"
|
||||
le = "<="
|
||||
|
||||
|
||||
@unique
|
||||
class OrderTypeEnum(str, Enum):
|
||||
"""订单类型"""
|
||||
|
||||
NEW = "new"
|
||||
RENEW = "renew"
|
||||
UPGRADE = "upgrade"
|
||||
DOWNGRADE = "downgrade"
|
||||
|
||||
|
||||
@unique
|
||||
class InvoiceTypeEnum(str, Enum):
|
||||
"""发票类型"""
|
||||
|
||||
VAT_NORMAL = "vat_normal"
|
||||
VAT_SPECIAL = "vat_special"
|
||||
|
||||
|
||||
@unique
|
||||
class TicketTypeEnum(str, Enum):
|
||||
"""工单类型"""
|
||||
|
||||
SUGGESTION = "suggestion"
|
||||
BUG = "bug"
|
||||
OPTIMIZE = "optimize"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
# ==================== 系统返回码 ====================
|
||||
|
||||
|
||||
class RET(Enum):
|
||||
"""系统返回码枚举
|
||||
|
||||
0~200: 成功状态码
|
||||
400~600: HTTP标准错误码
|
||||
4000+: 自定义业务错误码
|
||||
"""
|
||||
|
||||
# 成功状态码
|
||||
OK = (0, "成功")
|
||||
SUCCESS = (200, "操作成功")
|
||||
CREATED = (201, "创建成功")
|
||||
ACCEPTED = (202, "请求已接受")
|
||||
NO_CONTENT = (204, "操作成功,无返回数据")
|
||||
|
||||
# HTTP标准错误码
|
||||
ERROR = (1, "请求错误")
|
||||
BAD_REQUEST = (400, "参数错误")
|
||||
UNAUTHORIZED = (401, "未授权")
|
||||
FORBIDDEN = (403, "访问受限")
|
||||
NOT_FOUND = (404, "资源不存在")
|
||||
BAD_METHOD = (405, "不支持的请求方法")
|
||||
NOT_ACCEPTABLE = (406, "不接受的请求")
|
||||
CONFLICT = (409, "资源冲突")
|
||||
GONE = (410, "资源已删除")
|
||||
PRECONDITION_FAILED = (412, "前提条件失败")
|
||||
UNSUPPORTED_MEDIA_TYPE = (415, "不支持的媒体类型")
|
||||
UNPROCESSABLE_ENTITY = (422, "无法处理的实体")
|
||||
TOO_MANY_REQUESTS = (429, "请求过于频繁")
|
||||
|
||||
# 服务器错误码
|
||||
INTERNAL_SERVER_ERROR = (500, "服务器内部错误")
|
||||
NOT_IMPLEMENTED = (501, "功能未实现")
|
||||
BAD_GATEWAY = (502, "网关错误")
|
||||
SERVICE_UNAVAILABLE = (503, "服务不可用")
|
||||
GATEWAY_TIMEOUT = (504, "网关超时")
|
||||
HTTP_VERSION_NOT_SUPPORTED = (505, "HTTP版本不支持")
|
||||
|
||||
# 自定义业务错误码
|
||||
EXCEPTION = (-1, "系统异常")
|
||||
DATAEXIST = (4003, "数据已存在")
|
||||
DATAERR = (4004, "数据错误")
|
||||
PARAMERR = (4103, "参数错误")
|
||||
IOERR = (4302, "IO错误")
|
||||
SERVERERR = (4500, "服务错误")
|
||||
UNKOWNERR = (4501, "未知错误")
|
||||
TIMEOUT = (4502, "请求超时")
|
||||
RATE_LIMIT_EXCEEDED = (4503, "访问频率超限")
|
||||
|
||||
# Token相关错误码
|
||||
INVALID_TOKEN = (4504, "无效令牌")
|
||||
EXPIRED_TOKEN = (4505, "令牌过期")
|
||||
|
||||
# 认证授权错误码
|
||||
INVALID_CREDENTIALS = (4506, "无效凭证")
|
||||
|
||||
def __init__(self, code: int, msg: str) -> None:
|
||||
self._code = code
|
||||
self._msg = msg
|
||||
|
||||
@property
|
||||
def code(self) -> int:
|
||||
return self._code
|
||||
|
||||
@property
|
||||
def msg(self) -> str:
|
||||
return self._msg
|
||||
@@ -0,0 +1,59 @@
|
||||
from typing import Any
|
||||
|
||||
from app.common.enums import RET
|
||||
from app.core.base_schema import PageResultSchema
|
||||
from app.core.exceptions import CustomException
|
||||
|
||||
|
||||
class PaginationService:
|
||||
"""分页服务类(仅用于无法 SQL OFFSET/LIMIT 的数据源,如 Redis、本地文件列表、ORM 反射全量表名等)。"""
|
||||
|
||||
@staticmethod
|
||||
async def paginate(
|
||||
data_list: list[Any],
|
||||
page_no: int | None = None,
|
||||
page_size: int | None = None,
|
||||
) -> Any:
|
||||
"""对已在内存中的列表做切片分页。
|
||||
关系型表列表请使用 CRUDBase.page(数据库分页)。
|
||||
|
||||
参数:
|
||||
- data_list (list[Any]): 原始数据列表。
|
||||
- page_no (int | None): 当前页码,默认 None。
|
||||
- page_size (int | None): 每页数据量,默认 None。
|
||||
|
||||
返回:
|
||||
- PageResultSchema[Any]: 分页结果对象。
|
||||
|
||||
异常:
|
||||
- CustomException: 当分页参数不合法时抛出。
|
||||
"""
|
||||
total = len(data_list)
|
||||
|
||||
# 设置默认值
|
||||
if page_no is None:
|
||||
page_no = 1
|
||||
if page_size is None:
|
||||
page_size = 10
|
||||
|
||||
# 验证分页参数
|
||||
if page_no < 1 or page_size < 1:
|
||||
raise CustomException(code=RET.ERROR.code, msg="分页参数不合法")
|
||||
|
||||
# 计算起始索引和结束索引
|
||||
start = (page_no - 1) * page_size
|
||||
end = min(start + page_size, total)
|
||||
|
||||
# 根据计算得到的起始索引和结束索引对数据列表进行切片
|
||||
paginated_data = data_list[start:end]
|
||||
|
||||
# 判断是否有下一页
|
||||
has_next = end < total
|
||||
|
||||
return PageResultSchema(
|
||||
items=paginated_data,
|
||||
total=total,
|
||||
page_no=page_no,
|
||||
page_size=page_size,
|
||||
has_next=has_next,
|
||||
)
|
||||
@@ -0,0 +1,238 @@
|
||||
from collections.abc import Mapping
|
||||
from datetime import date, datetime, time
|
||||
from typing import Any
|
||||
|
||||
from fastapi import status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, PlainTextResponse, RedirectResponse, StreamingResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from starlette.background import BackgroundTask
|
||||
|
||||
from app.common.constant import DATE_DISPLAY_FMT, DATETIME_DISPLAY_FMT, TIME_DISPLAY_FMT
|
||||
from app.common.enums import RET
|
||||
|
||||
# 裸 datetime/date/time(未走 Pydantic 的 dict 等)JSON 输出与 constant 中展示格式一致
|
||||
_JSON_DATETIME_CUSTOM_ENCODER: dict[type[Any], Any] = {
|
||||
datetime: lambda d: d.strftime(DATETIME_DISPLAY_FMT),
|
||||
date: lambda d: d.strftime(DATE_DISPLAY_FMT),
|
||||
time: lambda t: t.strftime(TIME_DISPLAY_FMT),
|
||||
}
|
||||
|
||||
|
||||
def jsonable_response_content(content: Any) -> Any:
|
||||
return jsonable_encoder(content, custom_encoder=_JSON_DATETIME_CUSTOM_ENCODER)
|
||||
|
||||
|
||||
class ResponseSchema[T](BaseModel):
|
||||
"""响应模型"""
|
||||
|
||||
code: int = Field(default=RET.OK.code, description="业务状态码")
|
||||
msg: str = Field(default=RET.OK.msg, description="响应消息")
|
||||
data: T | None = Field(default=None, description="响应数据")
|
||||
status_code: int = Field(default=status.HTTP_200_OK, description="HTTP状态码")
|
||||
success: bool = Field(default=True, description="操作是否成功")
|
||||
|
||||
|
||||
class SuccessResponse(JSONResponse):
|
||||
"""成功响应类"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: Any | None = None,
|
||||
msg: str = RET.OK.msg,
|
||||
code: int = RET.OK.code,
|
||||
status_code: int = status.HTTP_200_OK,
|
||||
success: bool = True,
|
||||
) -> None:
|
||||
"""初始化成功响应类
|
||||
|
||||
参数:
|
||||
- data (Any | None): 响应数据。
|
||||
- msg (str): 响应消息。
|
||||
- code (int): 业务状态码。
|
||||
- status_code (int): HTTP 状态码。
|
||||
- success (bool): 操作是否成功。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
content = ResponseSchema(
|
||||
code=code,
|
||||
msg=msg,
|
||||
data=data,
|
||||
status_code=status_code,
|
||||
success=success,
|
||||
).model_dump()
|
||||
super().__init__(content=jsonable_response_content(content), status_code=status_code)
|
||||
self.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
|
||||
|
||||
class ErrorResponse(JSONResponse):
|
||||
"""错误响应类"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: Any = None,
|
||||
msg: str = RET.ERROR.msg,
|
||||
code: int = RET.ERROR.code,
|
||||
status_code: int = status.HTTP_400_BAD_REQUEST,
|
||||
success: bool = False,
|
||||
) -> None:
|
||||
"""初始化错误响应类
|
||||
|
||||
参数:
|
||||
- data (Any): 响应数据。
|
||||
- msg (str): 响应消息。
|
||||
- code (int): 业务状态码。
|
||||
- status_code (int): HTTP 状态码。
|
||||
- success (bool): 操作是否成功。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
content = ResponseSchema(
|
||||
code=code,
|
||||
msg=msg,
|
||||
data=data,
|
||||
status_code=status_code,
|
||||
success=success,
|
||||
).model_dump()
|
||||
super().__init__(content=jsonable_response_content(content), status_code=status_code)
|
||||
self.headers["Content-Type"] = "application/json; charset=utf-8"
|
||||
|
||||
|
||||
class StreamResponse(StreamingResponse):
|
||||
"""流式响应类"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: Any = None,
|
||||
status_code: int = status.HTTP_200_OK,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
media_type: str | None = None,
|
||||
background: BackgroundTask | None = None,
|
||||
) -> None:
|
||||
"""初始化流式响应类
|
||||
|
||||
参数:
|
||||
- data (Any): 响应数据。
|
||||
- status_code (int): HTTP 状态码。
|
||||
- headers (Mapping[str, str] | None): 响应头。
|
||||
- media_type (str | None): 媒体类型。
|
||||
- background (BackgroundTask | None): 后台任务。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
super().__init__(
|
||||
content=data,
|
||||
status_code=status_code,
|
||||
media_type=media_type, # 文件类型
|
||||
headers=headers, # 文件名
|
||||
background=background, # 文件大小
|
||||
)
|
||||
|
||||
|
||||
class PlainTextContentResponse(PlainTextResponse):
|
||||
"""纯文本响应类"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: str,
|
||||
status_code: int = status.HTTP_200_OK,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
) -> None:
|
||||
"""初始化纯文本响应类
|
||||
|
||||
参数:
|
||||
- content (str): 响应文本内容。
|
||||
- status_code (int): HTTP 状态码。
|
||||
- headers (Mapping[str, str] | None): 响应头。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
super().__init__(content=content, status_code=status_code, headers=headers)
|
||||
|
||||
|
||||
class HTMLContentResponse(HTMLResponse):
|
||||
"""HTML 响应类"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
content: str,
|
||||
status_code: int = status.HTTP_200_OK,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
) -> None:
|
||||
"""初始化 HTML 响应类
|
||||
|
||||
参数:
|
||||
- content (str): HTML 内容。
|
||||
- status_code (int): HTTP 状态码。
|
||||
- headers (Mapping[str, str] | None): 响应头。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
super().__init__(content=content, status_code=status_code, headers=headers)
|
||||
|
||||
|
||||
class RedirectContentResponse(RedirectResponse):
|
||||
"""重定向响应类"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: str,
|
||||
status_code: int = status.HTTP_302_FOUND,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
) -> None:
|
||||
"""初始化重定向响应类
|
||||
|
||||
参数:
|
||||
- url (str): 重定向目标 URL。
|
||||
- status_code (int): HTTP 状态码(默认 302)。
|
||||
- headers (Mapping[str, str] | None): 响应头。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
super().__init__(url=url, status_code=status_code, headers=headers)
|
||||
|
||||
|
||||
class UploadFileResponse(FileResponse):
|
||||
"""文件响应
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_path: str,
|
||||
filename: str,
|
||||
media_type: str = "application/octet-stream",
|
||||
headers: Mapping[str, str] | None = None,
|
||||
background: BackgroundTask | None = None,
|
||||
status_code: int = 200,
|
||||
) -> None:
|
||||
"""初始化文件响应类
|
||||
|
||||
参数:
|
||||
- file_path (str): 文件路径。
|
||||
- filename (str): 文件名。
|
||||
- media_type (str): 文件类型。
|
||||
- headers (Mapping[str, str] | None): 响应头。
|
||||
- background (BackgroundTask | None): 后台任务。
|
||||
- status_code (int): HTTP 状态码。
|
||||
|
||||
返回:
|
||||
- None
|
||||
"""
|
||||
super().__init__(
|
||||
path=file_path,
|
||||
status_code=status_code,
|
||||
headers=headers,
|
||||
media_type=media_type,
|
||||
background=background,
|
||||
filename=filename,
|
||||
stat_result=None,
|
||||
method=None,
|
||||
content_disposition_type="attachment",
|
||||
)
|
||||
Reference in New Issue
Block a user