mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-26 06:19:04 +00:00
本次提交包含多项优化和修复: 1. 修复CRUD初始化参数传递、搜索参数处理逻辑 2. 更新环境配置中的大模型相关参数 3. 重构部分服务方法命名,统一代码风格 4. 新增多个枚举类型,补充模型关联关系和加载选项 5. 优化查询参数类实现,完善字段校验逻辑 6. 调整Pydantic模型字段注释和类型定义 7. 简化并移除冗余的CRUD方法实现 8. 新增超级管理员权限装饰器 9. 修复邮件日志模型的租户关联和字段定义
501 lines
16 KiB
Python
501 lines
16 KiB
Python
from datetime import datetime
|
|
|
|
from app.core.base_schema import AuthSchema
|
|
from app.core.database import async_db_session
|
|
from app.core.dependencies import require_superadmin
|
|
from app.core.exceptions import CustomException
|
|
from app.utils.email_util import render_template, send_email
|
|
|
|
from .crud import EmailConfigCRUD, EmailLogCRUD, EmailTemplateCRUD
|
|
from .schema import (
|
|
EmailConfigCreateSchema,
|
|
EmailConfigOutSchema,
|
|
EmailConfigQueryParam,
|
|
EmailConfigUpdateSchema,
|
|
EmailLogOutSchema,
|
|
EmailLogQueryParam,
|
|
EmailSendSchema,
|
|
EmailTemplateCreateSchema,
|
|
EmailTemplateOutSchema,
|
|
EmailTemplateQueryParam,
|
|
EmailTemplateUpdateSchema,
|
|
EmailTestSchema,
|
|
)
|
|
|
|
|
|
class EmailConfigService:
|
|
"""
|
|
邮件配置服务
|
|
"""
|
|
|
|
"""
|
|
邮件配置服务
|
|
"""
|
|
"""SMTP 配置管理(仅超级管理员可操作)"""
|
|
|
|
@classmethod
|
|
@require_superadmin
|
|
async def page_service(
|
|
cls,
|
|
auth: AuthSchema,
|
|
page_no: int,
|
|
page_size: int,
|
|
search: EmailConfigQueryParam | None = None,
|
|
order_by: list | None = None,
|
|
) -> dict:
|
|
"""
|
|
SMTP 配置分页查询
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- page_no (int): 页码
|
|
- page_size (int): 每页数量
|
|
- search (EmailConfigQueryParam | None): 查询参数
|
|
- order_by (list | None): 排序参数
|
|
|
|
返回:
|
|
- dict: 分页数据
|
|
"""
|
|
return await EmailConfigCRUD(auth).page(
|
|
offset=(page_no - 1) * page_size,
|
|
limit=page_size,
|
|
order_by=order_by or [{"id": "asc"}],
|
|
search=vars(search) if search else None,
|
|
out_schema=EmailConfigOutSchema,
|
|
)
|
|
|
|
@classmethod
|
|
@require_superadmin
|
|
async def detail_service(cls, auth: AuthSchema, id: int) -> EmailConfigOutSchema:
|
|
"""
|
|
SMTP 配置详情
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): SMTP 配置 ID
|
|
|
|
返回:
|
|
- EmailConfigOutSchema: SMTP 配置详情
|
|
"""
|
|
return await EmailConfigCRUD(auth).get_or_404(id=id, out_schema=EmailConfigOutSchema, msg="SMTP 配置不存在")
|
|
|
|
@classmethod
|
|
async def create_service(cls, auth: AuthSchema, data: EmailConfigCreateSchema) -> EmailConfigOutSchema:
|
|
"""
|
|
创建 SMTP 配置
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- data (EmailConfigCreateSchema): SMTP 配置创建模型
|
|
|
|
返回:
|
|
- EmailConfigOutSchema: 新创建的 SMTP 配置
|
|
"""
|
|
crud = EmailConfigCRUD(auth)
|
|
if data.is_default:
|
|
await crud.clear_default()
|
|
obj = await crud.create(data=data)
|
|
return EmailConfigOutSchema.model_validate(obj)
|
|
|
|
@classmethod
|
|
async def update_service(cls, auth: AuthSchema, id: int, data: EmailConfigUpdateSchema) -> EmailConfigOutSchema:
|
|
"""
|
|
更新 SMTP 配置
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): SMTP 配置 ID
|
|
- data (EmailConfigUpdateSchema): SMTP 配置更新模型
|
|
|
|
返回:
|
|
- EmailConfigOutSchema: 更新后的 SMTP 配置
|
|
"""
|
|
crud = EmailConfigCRUD(auth)
|
|
_ = await crud.get_or_404(id=id, msg="SMTP 配置不存在")
|
|
if data.is_default is True:
|
|
await crud.clear_default()
|
|
updated = await crud.update(id=id, data=data)
|
|
return EmailConfigOutSchema.model_validate(updated)
|
|
|
|
@classmethod
|
|
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
|
"""
|
|
删除 SMTP 配置(默认配置不可删除)
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- ids (list[int]): SMTP 配置 ID 列表
|
|
|
|
返回:
|
|
- None
|
|
"""
|
|
if not ids:
|
|
raise CustomException(msg="删除对象不能为空")
|
|
crud = EmailConfigCRUD(auth)
|
|
configs = await crud.list(search={"id": ("in", ids)})
|
|
for obj in configs:
|
|
if obj.is_default:
|
|
raise CustomException(msg=f"配置「{obj.name}」是默认配置,请先将其他配置设为默认后再删除")
|
|
await crud.delete(ids=ids)
|
|
|
|
@classmethod
|
|
@require_superadmin
|
|
async def test_service(cls, auth: AuthSchema, data: EmailTestSchema) -> dict:
|
|
"""
|
|
测试 SMTP 连接:发送一封测试邮件
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- data (EmailTestSchema): 测试参数(配置 ID 与目标邮箱)
|
|
|
|
返回:
|
|
- dict: 测试结果
|
|
"""
|
|
config = await EmailConfigCRUD(auth).get(id=data.config_id)
|
|
if not config:
|
|
raise CustomException(msg="SMTP 配置不存在")
|
|
|
|
subject = "【FastapiAdmin】SMTP 连接测试"
|
|
body_html = ("<p>这是一封测试邮件,SMTP 配置「{name}」连接成功!</p><p>发送时间:{time}</p>").format(name=config.name, time=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
|
|
|
try:
|
|
await send_email(
|
|
smtp_host=config.smtp_host,
|
|
smtp_port=config.smtp_port,
|
|
smtp_user=config.smtp_user,
|
|
smtp_password=config.smtp_password,
|
|
use_tls=config.use_tls,
|
|
from_name=config.from_name,
|
|
to_email=data.to_email,
|
|
to_name=None,
|
|
subject=subject,
|
|
body_html=body_html,
|
|
timeout=config.timeout,
|
|
)
|
|
except Exception as e:
|
|
raise CustomException(msg=f"SMTP 连接测试失败:{e!s}") from e
|
|
|
|
return {"message": f"测试邮件已发送至 {data.to_email}"}
|
|
|
|
|
|
class EmailTemplateService:
|
|
"""
|
|
邮件模板服务
|
|
"""
|
|
|
|
"""邮件模板管理(仅超级管理员可操作)"""
|
|
|
|
@classmethod
|
|
@require_superadmin
|
|
async def page_service(
|
|
cls,
|
|
auth: AuthSchema,
|
|
page_no: int,
|
|
page_size: int,
|
|
search: EmailTemplateQueryParam | None = None,
|
|
order_by: list | None = None,
|
|
) -> dict:
|
|
"""
|
|
邮件模板分页查询
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- page_no (int): 页码
|
|
- page_size (int): 每页数量
|
|
- search (EmailTemplateQueryParam | None): 查询参数
|
|
- order_by (list | None): 排序参数
|
|
|
|
返回:
|
|
- dict: 分页数据
|
|
"""
|
|
return await EmailTemplateCRUD(auth).page(
|
|
offset=(page_no - 1) * page_size,
|
|
limit=page_size,
|
|
order_by=order_by or [{"id": "asc"}],
|
|
search=vars(search) if search else None,
|
|
out_schema=EmailTemplateOutSchema,
|
|
)
|
|
|
|
@classmethod
|
|
@require_superadmin
|
|
async def detail_service(cls, auth: AuthSchema, id: int) -> EmailTemplateOutSchema:
|
|
"""
|
|
邮件模板详情
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 邮件模板 ID
|
|
|
|
返回:
|
|
- EmailTemplateOutSchema: 邮件模板详情
|
|
"""
|
|
return await EmailTemplateCRUD(auth).get_or_404(id=id, out_schema=EmailTemplateOutSchema, msg="邮件模板不存在")
|
|
|
|
@classmethod
|
|
@require_superadmin
|
|
async def create_service(cls, auth: AuthSchema, data: EmailTemplateCreateSchema) -> EmailTemplateOutSchema:
|
|
"""
|
|
创建邮件模板
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- data (EmailTemplateCreateSchema): 邮件模板创建模型
|
|
|
|
返回:
|
|
- EmailTemplateOutSchema: 新创建的邮件模板
|
|
"""
|
|
existing = await EmailTemplateCRUD(auth).get_by_code(template_code=data.template_code)
|
|
if existing:
|
|
raise CustomException(msg=f"模板编码「{data.template_code}」已存在")
|
|
|
|
obj = await EmailTemplateCRUD(auth).create(data=data)
|
|
return EmailTemplateOutSchema.model_validate(obj)
|
|
|
|
@classmethod
|
|
@require_superadmin
|
|
async def update_service(cls, auth: AuthSchema, id: int, data: EmailTemplateUpdateSchema) -> EmailTemplateOutSchema:
|
|
"""
|
|
更新邮件模板
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- id (int): 邮件模板 ID
|
|
- data (EmailTemplateUpdateSchema): 邮件模板更新模型
|
|
|
|
返回:
|
|
- EmailTemplateOutSchema: 更新后的邮件模板
|
|
"""
|
|
_ = await EmailTemplateCRUD(auth).get_or_404(id=id, msg="邮件模板不存在")
|
|
|
|
updated = await EmailTemplateCRUD(auth).update(id=id, data=data)
|
|
return EmailTemplateOutSchema.model_validate(updated)
|
|
|
|
@classmethod
|
|
@require_superadmin
|
|
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
|
"""
|
|
删除邮件模板
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- ids (list[int]): 邮件模板 ID 列表
|
|
|
|
返回:
|
|
- None
|
|
"""
|
|
if not ids:
|
|
raise CustomException(msg="删除对象不能为空")
|
|
await EmailTemplateCRUD(auth).delete(ids=ids)
|
|
|
|
|
|
class EmailSendService:
|
|
"""
|
|
邮件发送服务
|
|
"""
|
|
|
|
"""
|
|
邮件发送服务
|
|
"""
|
|
"""
|
|
邮件发送服务 — 供其他模块调用。
|
|
|
|
设计原则:
|
|
1. 优先使用 is_default=True 的 SMTP 配置,无默认配置时降级为站内信(仅记录日志)
|
|
2. 每次发送都写 platform_email_log
|
|
3. 渲染失败/发送失败均记录 error_msg,不向调用方抛出异常(静默降级)
|
|
4. 使用独立数据库会话:邮件发送失败不因调用方事务回滚而丢失日志
|
|
"""
|
|
|
|
@classmethod
|
|
async def send_by_template(
|
|
cls,
|
|
*,
|
|
to_email: str,
|
|
template_code: str,
|
|
variables: dict,
|
|
biz_type: str = "other",
|
|
to_name: str | None = None,
|
|
tenant_id: int | None = None,
|
|
config_id: int | None = None,
|
|
) -> bool:
|
|
"""
|
|
通过模板发送邮件(核心方法,供内部调用)。
|
|
|
|
参数:
|
|
- to_email (str): 收件人邮箱
|
|
- template_code (str): 模板编码
|
|
- variables (dict): 模板变量
|
|
- biz_type (str): 业务类型
|
|
- to_name (str | None): 收件人姓名
|
|
- tenant_id (int | None): 租户 ID
|
|
- config_id (int | None): 指定 SMTP 配置 ID
|
|
|
|
返回:
|
|
- bool: True=发送成功,False=失败(已降级记录日志)
|
|
"""
|
|
# ① 获取模板(独立会话,不影响调用方事务)
|
|
async with async_db_session() as session:
|
|
auth = AuthSchema(db=session, check_data_scope=False)
|
|
template = await EmailTemplateCRUD(auth).get_active_by_code(template_code)
|
|
|
|
if not template:
|
|
await EmailLogCRUD.create_log(
|
|
to_email=to_email,
|
|
subject=f"[{template_code}]",
|
|
biz_type=biz_type,
|
|
to_name=to_name,
|
|
template_code=template_code,
|
|
tenant_id=tenant_id,
|
|
status=2,
|
|
error_msg=f"模板「{template_code}」不存在或已禁用",
|
|
)
|
|
return False
|
|
|
|
# ② 渲染主题 & 正文
|
|
try:
|
|
rendered_subject = render_template(template.subject, variables)
|
|
rendered_html = render_template(template.body_html, variables)
|
|
rendered_text = render_template(template.body_text, variables) if template.body_text else None
|
|
except Exception as e:
|
|
await EmailLogCRUD.create_log(
|
|
to_email=to_email,
|
|
subject=template.subject,
|
|
biz_type=biz_type,
|
|
to_name=to_name,
|
|
template_code=template_code,
|
|
tenant_id=tenant_id,
|
|
status=2,
|
|
error_msg=f"模板渲染失败:{e!s}",
|
|
)
|
|
return False
|
|
|
|
# ③ 获取 SMTP 配置(独立会话)
|
|
async with async_db_session() as session:
|
|
auth = AuthSchema(db=session, check_data_scope=False)
|
|
if config_id:
|
|
config = await EmailConfigCRUD(auth).get_active_by_id(config_id)
|
|
else:
|
|
config = await EmailConfigCRUD(auth).get_active_default()
|
|
|
|
if not config:
|
|
await EmailLogCRUD.create_log(
|
|
to_email=to_email,
|
|
subject=rendered_subject,
|
|
biz_type=biz_type,
|
|
to_name=to_name,
|
|
template_code=template_code,
|
|
tenant_id=tenant_id,
|
|
status=2,
|
|
error_msg="无可用的 SMTP 配置,邮件未发送(已降级为站内信)",
|
|
)
|
|
return False
|
|
|
|
# ④ 发送
|
|
try:
|
|
await send_email(
|
|
smtp_host=config.smtp_host,
|
|
smtp_port=config.smtp_port,
|
|
smtp_user=config.smtp_user,
|
|
smtp_password=config.smtp_password,
|
|
use_tls=config.use_tls,
|
|
from_name=config.from_name,
|
|
to_email=to_email,
|
|
to_name=to_name,
|
|
subject=rendered_subject,
|
|
body_html=rendered_html,
|
|
body_text=rendered_text,
|
|
timeout=config.timeout,
|
|
)
|
|
except Exception as e:
|
|
await EmailLogCRUD.create_log(
|
|
to_email=to_email,
|
|
subject=rendered_subject,
|
|
biz_type=biz_type,
|
|
to_name=to_name,
|
|
template_code=template_code,
|
|
config_id=config.id,
|
|
tenant_id=tenant_id,
|
|
status=2,
|
|
error_msg=f"SMTP 发送失败:{e!s}",
|
|
)
|
|
return False
|
|
|
|
# ⑤ 写成功日志
|
|
await EmailLogCRUD.create_log(
|
|
config_id=config.id,
|
|
template_code=template_code,
|
|
to_email=to_email,
|
|
to_name=to_name,
|
|
subject=rendered_subject,
|
|
biz_type=biz_type,
|
|
status=1,
|
|
tenant_id=tenant_id,
|
|
)
|
|
|
|
return True
|
|
|
|
@classmethod
|
|
async def manual_send_service(cls, auth: AuthSchema, data: EmailSendSchema) -> dict:
|
|
"""
|
|
超管手动触发发送(测试/补发用)
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- data (EmailSendSchema): 发送参数
|
|
|
|
返回:
|
|
- dict: 发送结果
|
|
"""
|
|
success = await cls.send_by_template(
|
|
to_email=data.to_email,
|
|
template_code=data.template_code,
|
|
variables=data.variables,
|
|
biz_type=data.biz_type,
|
|
to_name=data.to_name,
|
|
config_id=data.config_id,
|
|
)
|
|
if not success:
|
|
raise CustomException(msg="邮件发送失败,请查看邮件日志获取详情")
|
|
return {"message": f"邮件已发送至 {data.to_email}"}
|
|
|
|
|
|
class EmailLogService:
|
|
"""
|
|
邮件日志服务
|
|
"""
|
|
|
|
"""
|
|
邮件日志服务
|
|
"""
|
|
"""邮件日志查询"""
|
|
|
|
@classmethod
|
|
async def page_service(
|
|
cls,
|
|
auth: AuthSchema,
|
|
page_no: int,
|
|
page_size: int,
|
|
search: EmailLogQueryParam | None = None,
|
|
order_by: list | None = None,
|
|
) -> dict:
|
|
"""
|
|
邮件发送日志分页查询
|
|
|
|
参数:
|
|
- auth (AuthSchema): 认证信息模型
|
|
- page_no (int): 页码
|
|
- page_size (int): 每页数量
|
|
- search (EmailLogQueryParam | None): 查询参数
|
|
- order_by (list | None): 排序参数
|
|
|
|
返回:
|
|
- dict: 分页数据
|
|
"""
|
|
return await EmailLogCRUD(auth).page(
|
|
offset=(page_no - 1) * page_size,
|
|
limit=page_size,
|
|
order_by=order_by or [{"created_time": "desc"}],
|
|
search=vars(search) if search else None,
|
|
out_schema=EmailLogOutSchema,
|
|
)
|