|
|
from datetime import datetime
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, Text
|
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates
|
|
|
|
|
|
from app.core.base_model import ModelMixin, TenantMixin, UserMixin
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
from app.api.v1.module_platform.email.model import EmailConfigModel
|
|
|
|
|
|
|
|
|
class EmailConfigModel(ModelMixin):
|
|
|
"""
|
|
|
邮件 SMTP 配置表
|
|
|
|
|
|
每条记录对应一套 SMTP 配置(平台级,tenant_id=1)。
|
|
|
通过 is_default 标记当前生效的配置;只能有一条 is_default=True。
|
|
|
"""
|
|
|
|
|
|
__tablename__: str = "platform_email_config"
|
|
|
__table_args__: dict = {"comment": "邮件 SMTP 配置表"}
|
|
|
|
|
|
name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, comment="配置名称")
|
|
|
smtp_host: Mapped[str] = mapped_column(String(255), nullable=False, comment="SMTP 服务器地址")
|
|
|
smtp_port: Mapped[int] = mapped_column(Integer, nullable=False, default=465, comment="SMTP 端口(465=SSL, 587=TLS)")
|
|
|
smtp_user: Mapped[str] = mapped_column(String(255), nullable=False, comment="SMTP 登录用户名(发件邮箱)")
|
|
|
smtp_password: Mapped[str] = mapped_column(String(255), nullable=False, comment="SMTP 授权密码(AES 加密存储)")
|
|
|
from_name: Mapped[str] = mapped_column(String(100), nullable=False, default="FastapiAdmin", comment="发件人显示名")
|
|
|
use_tls: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, comment="是否启用 SSL/TLS")
|
|
|
is_default: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, comment="是否为默认配置")
|
|
|
timeout: Mapped[int] = mapped_column(Integer, nullable=False, default=30, comment="连接超时(秒)")
|
|
|
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启动 1:停用)", index=True)
|
|
|
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
|
|
|
|
|
|
@validates("smtp_port")
|
|
|
def validate_port(self, key: str, value: int) -> int:
|
|
|
if value < 1 or value > 65535:
|
|
|
raise ValueError("SMTP 端口范围为 1-65535")
|
|
|
return value
|
|
|
|
|
|
@validates("timeout")
|
|
|
def validate_timeout(self, key: str, value: int) -> int:
|
|
|
if value < 5 or value > 120:
|
|
|
raise ValueError("超时时间应在 5-120 秒之间")
|
|
|
return value
|
|
|
|
|
|
|
|
|
class EmailTemplateModel(ModelMixin):
|
|
|
"""
|
|
|
邮件模板表
|
|
|
|
|
|
支持变量插值(Jinja2 语法),如 {{ username }}、{{ reset_link }}。
|
|
|
template_code 是唯一业务键,代码中通过 code 引用,不依赖自增 ID。
|
|
|
"""
|
|
|
|
|
|
__tablename__: str = "platform_email_template"
|
|
|
__table_args__: dict = {"comment": "邮件模板表"}
|
|
|
|
|
|
name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, comment="模板名称")
|
|
|
template_code: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, comment="模板编码(业务键)")
|
|
|
subject: Mapped[str] = mapped_column(String(255), nullable=False, comment="邮件主题(可含变量)")
|
|
|
body_html: Mapped[str] = mapped_column(Text, nullable=False, comment="邮件正文 HTML(Jinja2 模板)")
|
|
|
body_text: Mapped[str | None] = mapped_column(Text, nullable=True, default=None, comment="邮件纯文本版本(降级用)")
|
|
|
variables: Mapped[str | None] = mapped_column(Text, nullable=True, default=None, comment='模板变量说明(JSON 格式,如 {"username": "用户名", "link": "链接"})')
|
|
|
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启动 1:停用)", index=True)
|
|
|
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
|
|
|
|
|
|
@validates("template_code")
|
|
|
def validate_code(self, key: str, value: str) -> str:
|
|
|
value = value.strip()
|
|
|
if not value:
|
|
|
raise ValueError("模板编码不能为空")
|
|
|
return value
|
|
|
|
|
|
|
|
|
class EmailLogModel(ModelMixin, TenantMixin, UserMixin):
|
|
|
"""
|
|
|
邮件发送日志表
|
|
|
|
|
|
记录每次邮件发送的结果,支持重试追踪。
|
|
|
status 覆盖 ModelMixin:0=待发送 1=成功 2=失败。
|
|
|
tenant_id 为可空:平台主动发送邮件(注册/重置密码等)无租户归属。
|
|
|
"""
|
|
|
|
|
|
__tablename__: str = "platform_email_log"
|
|
|
__table_args__: dict = {"comment": "邮件发送日志表"}
|
|
|
__loader_options__: list[str] = ["created_by", "updated_by", "deleted_by", "tenant_by"]
|
|
|
|
|
|
config_id: Mapped[int | None] = mapped_column(Integer, ForeignKey("platform_email_config.id", ondelete="SET NULL", onupdate="CASCADE"), nullable=True, index=True, comment="使用的 SMTP 配置 ID")
|
|
|
template_code: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="模板编码(冗余存储,模板删除后仍可追溯)")
|
|
|
to_email: Mapped[str] = mapped_column(String(255), nullable=False, index=True, comment="收件人邮箱")
|
|
|
to_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="收件人姓名")
|
|
|
subject: Mapped[str] = mapped_column(String(255), nullable=False, comment="邮件主题(渲染后)")
|
|
|
biz_type: Mapped[str] = mapped_column(String(50), nullable=False, default="other", comment="业务类型(register/reset_password/invite/expiry_warning/ticket_reply/other)")
|
|
|
error_msg: Mapped[str | None] = mapped_column(Text, nullable=True, default=None, comment="失败原因")
|
|
|
retry_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="重试次数")
|
|
|
tenant_id: Mapped[int | None] = mapped_column(
|
|
|
Integer,
|
|
|
ForeignKey("platform_tenant.id", ondelete="SET NULL", onupdate="CASCADE"),
|
|
|
nullable=True,
|
|
|
index=True,
|
|
|
comment="关联租户 ID(可为空,如平台注册邮件)",
|
|
|
)
|
|
|
sent_time: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, default=None, comment="实际发送时间")
|
|
|
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态(0:启动 1:停用)", index=True)
|
|
|
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="备注")
|
|
|
|
|
|
# 关联关系
|
|
|
config: Mapped["EmailConfigModel | None"] = relationship("EmailConfigModel", lazy="selectin")
|