diff --git a/backend/app/api/v1/mixed/task_demo.py b/backend/app/api/v1/mixed/task_demo.py index 03680499..cacc46e5 100644 --- a/backend/app/api/v1/mixed/task_demo.py +++ b/backend/app/api/v1/mixed/task_demo.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- import datetime -from fastapi import APIRouter, Query +from fastapi import APIRouter, Query, File, UploadFile, Form from typing_extensions import Annotated from backend.app.common.response.response_schema import response_base @@ -69,3 +69,12 @@ async def task_demo_delete(job_id: Annotated[str, Query(..., description='任务 scheduler.remove_job(job_id=job_id) return await response_base.success({'msg': 'success'}) + + +@router.post('/files', summary='文件上传') +async def create_file(file: bytes = File(), fileb: UploadFile = File(), token: str = Form()): + return { + 'file_size': len(file), + 'token': token, + 'fileb_content_type': fileb.content_type, + } diff --git a/backend/app/common/response/response_schema.py b/backend/app/common/response/response_schema.py index 3b710ce2..a871b78e 100644 --- a/backend/app/common/response/response_schema.py +++ b/backend/app/common/response/response_schema.py @@ -6,6 +6,7 @@ from typing import Any from asgiref.sync import sync_to_async from pydantic import validate_arguments, BaseModel +from backend.app.core.conf import settings from backend.app.utils.encoders import jsonable_encoder _ExcludeData = set[int | str] | dict[int | str, Any] @@ -37,7 +38,7 @@ class ResponseModel(BaseModel): data: Any | None = None class Config: - json_encoders = {datetime: lambda x: x.strftime('%Y-%m-%d %H:%M:%S')} + json_encoders = {datetime: lambda x: x.strftime(settings.DATETIME_FORMAT)} class ResponseBase: @@ -58,7 +59,7 @@ class ResponseBase: @staticmethod @sync_to_async def __json_encoder(data: Any, exclude: _ExcludeData | None = None, **kwargs): - custom_encoder = {datetime: lambda x: x.strftime('%Y-%m-%d %H:%M:%S')} + custom_encoder = {datetime: lambda x: x.strftime(settings.DATETIME_FORMAT)} kwargs.update({'custom_encoder': custom_encoder}) result = jsonable_encoder(data, exclude=exclude, **kwargs) return result diff --git a/backend/app/core/conf.py b/backend/app/core/conf.py index cd826c5a..85f1c8e5 100644 --- a/backend/app/core/conf.py +++ b/backend/app/core/conf.py @@ -63,6 +63,10 @@ class Settings(BaseSettings): # Limiter LIMITER_REDIS_PREFIX: str = 'fba_limiter' + # DateTime + DATETIME_TIMEZONE: str = 'Asia/Shanghai' + DATETIME_FORMAT: str = '%Y-%m-%d %H:%M:%S' + # MySQL DB_ECHO: bool = False DB_DATABASE: str = 'fba' diff --git a/backend/app/schemas/base.py b/backend/app/schemas/base.py index c811dfb5..f90e6ce3 100644 --- a/backend/app/schemas/base.py +++ b/backend/app/schemas/base.py @@ -32,7 +32,6 @@ SCHEMA_ERROR_MSG_TEMPLATES: dict[str, str] = { 'type_error.subclass': '预期 {expected_class} 的子类', 'type_error.tuple': '值不是有效的元组', 'type_error.uuid': '值不是有效的 UUID', - # Value Errors 'value_error.any_str.max_length': '确保此值最多包含 {limit_value} 个字符', 'value_error.any_str.min_length': '确保此值至少包含 {limit_value} 个字符', @@ -44,7 +43,7 @@ SCHEMA_ERROR_MSG_TEMPLATES: dict[str, str] = { 'value_error.decimal.max_places': '确保小数位数不超过 {decimal_places} 位', 'value_error.decimal.not_finite': '值不是有效的小数(Decimal)', 'value_error.decimal.whole_digits': '确保小数点前不超过 {whole_digits} 位', - 'value_error.discriminated_union.invalid_discriminator': '不匹配鉴别器 {discriminator_key!r} 和值 {discriminator_value!r}(允许的值:{allowed_values})', + 'value_error.discriminated_union.invalid_discriminator': '不匹配鉴别器 {discriminator_key!r} 和值 {discriminator_value!r}(允许的值:{allowed_values})', # noqa: E501 'value_error.discriminated_union.missing_discriminator': '鉴别器 {discriminator_key!r} 的值缺失', 'value_error.extra': '不允许使用额外字段', 'value_error.frozenset.max_items': '确保此值最多包含 {limit_value} 个项目', diff --git a/backend/app/utils/datetime.py b/backend/app/utils/datetime.py new file mode 100644 index 00000000..33a6d713 --- /dev/null +++ b/backend/app/utils/datetime.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import datetime +import pytz + +from backend.app.core.conf import settings + + +class DateTimeUtils: + def __init__(self, timezone_str=settings.DATETIME_TIMEZONE): + """ + 初始化函数,设置时区 + + :param timezone_str: 时区字符串,默认为 UTC + """ + self.timezone_str = timezone_str + self.timezone = pytz.timezone(self.timezone_str) + + def get_current_time(self) -> datetime.datetime: + """ + 获取当前时间 + + :return: 当前时间的 datetime 对象 + """ + return datetime.datetime.now(self.timezone) + + @staticmethod + def get_current_timestamp() -> int: + """ + 获取当前时间戳 (秒) + + :return: 当前时间戳 (秒) + """ + return int(datetime.datetime.now().timestamp()) + + @staticmethod + def get_current_milliseconds() -> int: + """ + 获取当前时间戳 (毫秒) + + :return: 当前时间戳 (毫秒) + """ + return int(datetime.datetime.now().timestamp() * 1000) + + def timestamp_to_datetime(self, timestamp: int) -> datetime.datetime: + """ + 时间戳转 datetime 对象 + + :param timestamp: 时间戳 (秒) + :return: datetime 对象 + """ + return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=self.timezone) + + def datetime_to_timestamp(self, dt: datetime.datetime) -> int: + """ + datetime 对象转时间戳(秒) + + :param dt: datetime 对象 + :return: 时间戳 (秒) + """ + return int(dt.astimezone(self.timezone).timestamp()) + + def datetime_to_milliseconds(self, dt: datetime.datetime) -> int: + """ + datetime 对象转时间戳(毫秒) + + :param dt: datetime 对象 + :return: 时间戳 (毫秒) + """ + return int(dt.astimezone(self.timezone).timestamp() * 1000) + + def str_to_datetime(self, time_str: str, format_str: str = settings.DATETIME_FORMAT) -> datetime.datetime: + """ + 时间字符串转 datetime 对象 + + :param time_str: 时间字符串 + :param format_str: 时间字符串的格式,默认为 '%Y-%m-%d %H:%M:%S' + :return: datetime 对象 + """ + return datetime.datetime.strptime(time_str, format_str).replace(tzinfo=self.timezone) + + def datetime_to_str(self, dt: datetime.datetime, format_str: str = settings.DATETIME_FORMAT) -> str: + """ + datetime 对象转时间字符串 + + :param dt: datetime 对象 + :param format_str: 时间字符串的格式,默认为 '%Y-%m-%d %H:%M:%S' + :return: 时间字符串 + """ + return dt.astimezone(self.timezone).strftime(format_str) + + @staticmethod + def get_timezone(timezone_str: str) -> pytz.timezone: + """ + 获取指定时区的 pytz.timezone 对象 + + :param timezone_str: 时区字符串 + :return: pytz.timezone 对象 + """ + return pytz.timezone(timezone_str) + + def get_timezone_time(self, timezone_str: str) -> datetime.datetime: + """ + 获取指定时区的当前时间 + + :param timezone_str: 时区字符串 + :return: 当前时间的 datetime 对象 + """ + timezone = self.get_timezone(timezone_str) + return datetime.datetime.now(timezone) + + def datetime_to_timezone(self, dt: datetime.datetime, timezone_str: str) -> datetime.datetime: + """ + 将 datetime 对象转换为指定时区的 datetime 对象 + + :param dt: datetime 对象 + :param timezone_str: 目标时区字符串 + :return: 目标时区的 datetime 对象 + """ + timezone = self.get_timezone(timezone_str) + return dt.astimezone(timezone) + + def datetime_to_timezone_str( + self, dt: datetime.datetime, timezone_str: str, format_str: str = settings.DATETIME_FORMAT + ) -> str: + """ + 将 datetime 对象转换为指定时区的时间字符串 + + :param dt: datetime 对象 + :param timezone_str: 目标时区字符串 + :param format_str: 时间字符串的格式,默认为 '%Y-%m-%d %H:%M:%S' + :return: 目标时区的时间字符串 + """ + dt_timezone = self.datetime_to_timezone(dt, timezone_str) + return dt_timezone.strftime(format_str) + + def str_to_timezone( + self, time_str: str, timezone_str: str, format_str: str = settings.DATETIME_FORMAT + ) -> datetime.datetime: + """ + 将指定时区的时间字符串转换为 datetime 对象 + + :param time_str: 指定时区的时间字符串 + :param timezone_str: 指定时区字符串 + :param format_str: 时间字符串的格式,默认为 '%Y-%m-%d %H:%M:%S' + :return: datetime 对象 + """ + dt = datetime.datetime.strptime(time_str, format_str).replace(tzinfo=self.timezone) + return self.datetime_to_timezone(dt, timezone_str) + + @staticmethod + def datetime_to_utc(dt: datetime.datetime) -> datetime.datetime: + """ + 将 datetime 对象转换为 UTC 时间 + + :param dt: datetime 对象 + :return: UTC 时间的 datetime 对象 + """ + return dt.astimezone(pytz.utc) + + def str_to_utc(self, time_str: str, format_str: str = settings.DATETIME_FORMAT) -> datetime.datetime: + """ + 将时间字符串转换为 UTC 时间的 datetime 对象 + + :param time_str: 时间字符串 + :param format_str: 时间字符串的格式,默认为 '%Y-%m-%d %H:%M:%S' + :return: UTC 时间的 datetime 对象 + """ + dt = datetime.datetime.strptime(time_str, format_str).replace(tzinfo=self.timezone) + return self.datetime_to_utc(dt) + + def utc_to_datetime(self, utc_time: datetime.datetime) -> datetime.datetime: + """ + 将 UTC 时间的 datetime 对象转换为指定时区的 datetime 对象 + + :param utc_time: UTC 时间的 datetime 对象 + :return: 目标时区的 datetime 对象 + """ + return utc_time.replace(tzinfo=pytz.utc).astimezone(self.timezone).replace(tzinfo=None) + + def get_expire_time(self, expires_delta: datetime.timedelta) -> datetime: + """ + 获取过期时间 + + :param expires_delta: 时间间隔对象 + :return: 过期时间的 datetime 对象 + """ + return self.get_current_time() + expires_delta + + @staticmethod + def get_expire_time_from_datetime(expire_time: datetime, seconds: int) -> datetime: + """ + 获取从指定时间开始一定时间后的过期时间 + + :param expire_time: 指定时间的 datetime 对象 + :param seconds: 时间间隔(秒) + :return: 过期时间的 datetime 对象 + """ + return expire_time + datetime.timedelta(seconds=seconds) + + @staticmethod + def get_expire_seconds(expires_delta: datetime.timedelta) -> int: + """ + 获取过期时间(秒) + + :param expires_delta: 时间间隔对象 + :return: 过期时间(秒) + """ + return int(expires_delta.total_seconds()) + + def get_expire_seconds_from_datetime(self, expire_datetime: datetime) -> int: + """ + 获取从指定时间开始到当前时间的时间间隔(秒) + + :param expire_datetime: 指定时间的 datetime 对象 + :return: 时间间隔(秒) + """ + current_time = self.get_current_time() + if expire_datetime < current_time: + return 0 + return int((expire_datetime - current_time).total_seconds()) + + +datetime_utils = DateTimeUtils() diff --git a/backend/app/utils/server_info.py b/backend/app/utils/server_info.py index f4285a53..313c6931 100644 --- a/backend/app/utils/server_info.py +++ b/backend/app/utils/server_info.py @@ -7,6 +7,8 @@ from typing import List import psutil +from backend.app.core.conf import settings + class ServerInfo: @staticmethod @@ -103,6 +105,6 @@ class ServerInfo: 'mem_vms': ServerInfo.format_bytes(mem_info.vms), # 虚拟内存, 即当前进程申请的虚拟内存 'mem_rss': ServerInfo.format_bytes(mem_info.rss), # 常驻内存, 即当前进程实际使用的物理内存 'mem_free': ServerInfo.format_bytes(mem_info.vms - mem_info.rss), # 空闲内存 - 'startup': start_time.strftime('%Y-%m-%d %H:%M:%S'), + 'startup': start_time.strftime(settings.DATETIME_FORMAT), 'elapsed': f'{ServerInfo.fmt_timedelta(datetime.now() - start_time)}', } diff --git a/requirements.txt b/requirements.txt index afdc6d20..16166a05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,6 +27,7 @@ pytest==7.2.2 pytest-pretty==1.2.0 python-jose==3.3.0 python-multipart==0.0.5 +pytz==2023.3 redis[hiredis]==4.5.5 ruff==0.0.262 SQLAlchemy==2.0.8