You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

111 lines
6.2 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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="邮件正文 HTMLJinja2 模板)")
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 覆盖 ModelMixin0=待发送 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")