From 5c3c429620e4635218c5257336e9ba5d6ad1bc82 Mon Sep 17 00:00:00 2001 From: konjacpotato Date: Sun, 15 Feb 2026 15:53:48 +0800 Subject: [PATCH] task: add send common mail task --- .env | 14 +++++++ arlo.py | 9 ++--- config/__init__.py | 1 + config/database.py | 18 +++++++++ config/env_loader.py | 22 +++++++++++ config/settings.py | 31 +++++++++++++++ database/database.py | 9 ++++- database/tcontent/crud.py | 32 +++++++++++++++ database/tcontent/debug.py | 15 +++++++ database/tcontent/model.py | 12 ++++++ docker-compose.yml | 2 + models/__init__.py | 2 + models/article.py | 50 +++++++++++++++++++++++ models/base.py | 4 ++ models/source_content.py | 57 +++++++++++++++++++++++++++ requirements.txt | Bin 1340 -> 1416 bytes task/common_mail/common_mail_task.py | 24 +++++++++++ utils/__init__.py | 1 + utils/logger.py | 37 +++++++++++++++++ 19 files changed, 334 insertions(+), 6 deletions(-) create mode 100644 .env create mode 100644 config/__init__.py create mode 100644 config/database.py create mode 100644 config/env_loader.py create mode 100644 config/settings.py create mode 100644 database/tcontent/crud.py create mode 100644 database/tcontent/debug.py create mode 100644 database/tcontent/model.py create mode 100644 models/__init__.py create mode 100644 models/article.py create mode 100644 models/base.py create mode 100644 models/source_content.py create mode 100644 task/common_mail/common_mail_task.py create mode 100644 utils/__init__.py create mode 100644 utils/logger.py diff --git a/.env b/.env new file mode 100644 index 0000000..d5c246a --- /dev/null +++ b/.env @@ -0,0 +1,14 @@ +ENV=dev + +DEBUG=true + +# 日志配置 +LOG_LEVEL=DEBUG +LOG_TYPE=console + +# 数据库配置 +DB_HOST= 47.119.128.161 # 192.168.1.200 +DB_PORT=19732 +DB_USER=postgres +DB_PASS=postgres +DB_NAME=peter \ No newline at end of file diff --git a/arlo.py b/arlo.py index ec96373..d8f2a49 100644 --- a/arlo.py +++ b/arlo.py @@ -7,7 +7,6 @@ include exception tracebacks for easier debugging. import datetime import signal -import sys import traceback from functools import partial from typing import Any @@ -16,8 +15,8 @@ from apscheduler.events import EVENT_JOB_ERROR from apscheduler.schedulers.blocking import BlockingScheduler from config import config -from log.log_manager import log, logger from task.manager_task import manager_task +from utils import logger def _format_exception(exc: BaseException) -> str: @@ -84,7 +83,7 @@ def main() -> None: # Graceful shutdown handlers def _shutdown(signum, frame): - log(f"Received signal {signum}. Shutting down scheduler...") + logger.info(f"Received signal {signum}. Shutting down scheduler...") try: scheduler.shutdown(wait=False) except Exception: @@ -96,10 +95,10 @@ def main() -> None: signal.signal(signal.SIGTERM, _shutdown) try: - log("started successfully.") + logger.info("started successfully.") scheduler.start() # 阻塞运行 except (KeyboardInterrupt, SystemExit): - log("Shutting down ...") + logger.info("Shutting down ...") if scheduler.state == 1: try: scheduler.shutdown(wait=False) diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..f56bbbd --- /dev/null +++ b/config/__init__.py @@ -0,0 +1 @@ +from config.settings import settings \ No newline at end of file diff --git a/config/database.py b/config/database.py new file mode 100644 index 0000000..4a79780 --- /dev/null +++ b/config/database.py @@ -0,0 +1,18 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, scoped_session +from config.settings import settings + +SQLALCHEMY_SYNC_URL = ( + f"postgresql+psycopg://{settings.DB_USER}:{settings.DB_PASS}" + f"@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_NAME}" +) + +engine = create_engine( + SQLALCHEMY_SYNC_URL, + echo=False, # 开发可改 True + future=True +) + +SessionLocal = scoped_session( + sessionmaker(bind=engine, autoflush=False, autocommit=False) +) \ No newline at end of file diff --git a/config/env_loader.py b/config/env_loader.py new file mode 100644 index 0000000..01521dd --- /dev/null +++ b/config/env_loader.py @@ -0,0 +1,22 @@ +import os +from dotenv import load_dotenv + +def load_env(): + """ + 自动根据 ENV 加载对应的 .env 文件 + """ + base_file = ".env" + prod_file = ".env.prod" + test_file = ".env.test" + + # 先加载基础 .env + if os.path.exists(base_file): + load_dotenv(base_file) + + # 根据参数 ENV 再加载其他环境 + env = os.getenv("ENV", "dev") + + if env == "prod" and os.path.exists(prod_file): + load_dotenv(prod_file, override=True) + elif env == "test" and os.path.exists(test_file): + load_dotenv(test_file, override=True) \ No newline at end of file diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..7254194 --- /dev/null +++ b/config/settings.py @@ -0,0 +1,31 @@ +from pydantic_settings import BaseSettings +from pydantic import Field +from config.env_loader import load_env + +# 先加载 ENV & .env +load_env() + +class Settings(BaseSettings): + # 环境 + ENV: str = Field("dev") + DEBUG: bool = Field(True) + + # 日志 + LOG_LEVEL: str = Field("LOG_LEVEL") + LOG_FILE_PATH: str = Field("logs") + LOG_TYPE: str = Field("console") + + # 数据库 + DB_HOST: str + DB_PORT: int + DB_USER: str + DB_PASS: str + DB_NAME: str + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + + +# 全局唯一配置实例 +settings = Settings() \ No newline at end of file diff --git a/database/database.py b/database/database.py index c80c549..31a8903 100644 --- a/database/database.py +++ b/database/database.py @@ -21,7 +21,14 @@ engine = create_engine( 'keepalives_count': 5 } ) -Base.metadata.create_all(engine) + +def get_engine(): + return engine + +# 不在模块导入时自动创建表;提供显式创建函数 +def create_tables(): + # 确保相关 model 模块已被导入并注册到 Base + Base.metadata.create_all(engine) @contextmanager def get_session(): diff --git a/database/tcontent/crud.py b/database/tcontent/crud.py new file mode 100644 index 0000000..e2b70fc --- /dev/null +++ b/database/tcontent/crud.py @@ -0,0 +1,32 @@ +from database.tcontent.model import TContent + +def create_content(db, content: TContent): + db.add(content) + db.commit() + db.refresh(content) + return content + +def get_content_by_id(db, content_id: int): + return db.query(TContent).filter(TContent.id == content_id).first() + +def update_content(db, content_id: int, updates: dict): + content = db.query(TContent).filter(TContent.id == content_id).first() + if content: + for key, value in updates.items(): + setattr(content, key, value) + db.commit() + db.refresh(content) + return content + +def delete_content(db, content_id: int): + content = db.query(TContent).filter(TContent.id == content_id).first() + if content: + db.delete(content) + db.commit() + return content + +def drop_table(db): + TContent.__table__.drop(db.get_bind(), checkfirst=True) + +def create_table(db): + TContent.__table__.create(db.get_bind(), checkfirst=True) \ No newline at end of file diff --git a/database/tcontent/debug.py b/database/tcontent/debug.py new file mode 100644 index 0000000..5393868 --- /dev/null +++ b/database/tcontent/debug.py @@ -0,0 +1,15 @@ +import database.tcontent.model +from database.database import get_session, create_tables +from database.tcontent.crud import drop_table, get_content_by_id + + + +# create_tables() + +# with get_session() as db: +# task = get_content_by_id(db, 1) +# print(task) +# print(task.id) + +with get_session() as db: + drop_table(db) \ No newline at end of file diff --git a/database/tcontent/model.py b/database/tcontent/model.py new file mode 100644 index 0000000..f330160 --- /dev/null +++ b/database/tcontent/model.py @@ -0,0 +1,12 @@ +from sqlalchemy import Column, Integer, String, Text, DateTime, func +from database.database import Base + +class TContent(Base): + __tablename__ = 't_content' + + id = Column(Integer, primary_key=True, autoincrement=True, comment='自动递增的唯一内容ID') + project = Column(String(64), nullable=False, comment='项目名称', index=True) + subject = Column(String(256), nullable=False, comment='主题') + content = Column(Text, nullable=True, comment='内容') + create_time = Column(DateTime, server_default=func.now(), nullable=False, comment='创建时间') + update_time = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False, comment='更新时间') \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9a67f20..99c6a15 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,3 +5,5 @@ services: image: arlo:latest container_name: arlo restart: always + environment: + - TZ=Asia/Shanghai # 设置时区环境变量 diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..c998ac6 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,2 @@ +from models.source_content import SourceContent +from models.article import Article \ No newline at end of file diff --git a/models/article.py b/models/article.py new file mode 100644 index 0000000..8164cb5 --- /dev/null +++ b/models/article.py @@ -0,0 +1,50 @@ +from datetime import datetime +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String, Text, Integer, DateTime, func +from models.base import Base + + +class Article(Base): + __tablename__ = "t_article" + + id: Mapped[int] = mapped_column( + Integer, + primary_key=True, + autoincrement=True, + comment="自动递增的唯一内容ID" + ) + + title: Mapped[str] = mapped_column( + String(256), + nullable=False, + index=True, + comment="标题" + ) + + keywords: Mapped[str | None] = mapped_column( + Text, + nullable=True, + comment="关键词" + ) + + content: Mapped[str | None] = mapped_column( + Text, + nullable=True, + comment="内容" + ) + + create_time: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False, + comment="创建时间" + ) + + used: Mapped[bool] = mapped_column( + default=False, + nullable=False, + comment="是否已被使用" + ) + + def __repr__(self): + return f"
" diff --git a/models/base.py b/models/base.py new file mode 100644 index 0000000..2278416 --- /dev/null +++ b/models/base.py @@ -0,0 +1,4 @@ +from sqlalchemy.orm import DeclarativeBase + +class Base(DeclarativeBase): + pass \ No newline at end of file diff --git a/models/source_content.py b/models/source_content.py new file mode 100644 index 0000000..7dbd941 --- /dev/null +++ b/models/source_content.py @@ -0,0 +1,57 @@ +from datetime import datetime +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String, Text, Integer, DateTime, Index, func +from models.base import Base + + +class SourceContent(Base): + __tablename__ = "t_source_content" + + id: Mapped[int] = mapped_column( + Integer, + primary_key=True, + autoincrement=True, + comment="自动递增的唯一内容ID" + ) + + link: Mapped[str] = mapped_column( + String(2048), + nullable=False, + index=True, + comment="链接" + ) + + platform: Mapped[str] = mapped_column( + String(32), + nullable=False, + comment="平台" + ) + + content: Mapped[str | None] = mapped_column( + Text, + nullable=True, + comment="内容" + ) + + create_time: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False, + comment="创建时间" + ) + + update_time: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + comment="更新时间" + ) + + # ——可选优化:添加 项目 + 主题 联合唯一索引—— + __table_args__ = ( + Index("link", "link", unique=True), + ) + + def __repr__(self): + return f"" diff --git a/requirements.txt b/requirements.txt index 32b3743acf30a5108e5c4d59c8372293742069de..797ea58f11069b9c0bcaee720fc5684148ce55b8 100644 GIT binary patch delta 266 zcmdnP)xo`?foZY=^P9~kEF6sW1q_u8B@7u1`3!jsx(q2mx`ZJW$Sz~BW3Xi~WYA+U zVBlroVgRX2VMqkZlrUs6Bm>nJ1J#v)d3g-!KpuYeFj+&Wc{xCv(iuv97>XE5!CDP~ zR+)pX3T6mo@L_Od$N^fD0kk9+Xf;T?5t3tyfnv!(BMN|qfaHupa+7B=DQduZV6zf| NrhrUH0y`Sn5df51EQ0_5 delta 162 zcmeC+-ov$_fl0EMp^_n)A)lduA)Ub%2#p!^7>t0}WU@W0Vl;{f11|#?P+cWM3PU18 z9zzL3CQv6x1xURig8`5<#;PVBY(o)4DqOV@gZboa7RkwdO!p?Uv5N4+Oo5nTwmF@d Gmk|IGWE{level: <8} " + "| {name}:{function}:{line} " + "- {message}", + ) + +if "file" in settings.LOG_TYPE: + # 日志目录 + LOG_DIR = settings.LOG_FILE_PATH + if not os.path.exists(LOG_DIR): + os.makedirs(LOG_DIR) + + # ======== 文件输出(按天切割)======== + logger.add( + f"{LOG_DIR}/app_{{time:YYYY-MM-DD}}.log", + rotation="00:00", # 每天 0 点切割 + retention="7 days", # 保存 7 天 + encoding="utf-8", + level=settings.LOG_LEVEL, + enqueue=True, # 多线程安全 + compression="zip", # 自动压缩旧日志 + ) + +__all__ = ["logger"] \ No newline at end of file