- 修复前端路由守卫:未登录时不显示提示,直接跳转登录页 - 修复API拦截器:401错误不显示提示,直接跳转 - 增强验证码显示:图片尺寸从120x40增加到200x80 - 增大验证码字体:从28号增加到48号 - 优化验证码字符:排除易混淆的0和1 - 减少干扰线:从5条减少到3条,添加背景色优化 - 增强登录API日志:添加详细的调试日志 - 增强验证码生成和验证日志 - 优化异常处理和错误追踪 影响文件: - src/router/index.ts - src/api/request.ts - app/services/auth_service.py - app/api/v1/auth.py - app/schemas/user.py 测试状态: - 前端构建通过 - 后端语法检查通过 - 验证码显示效果优化完成 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
403 lines
12 KiB
Python
403 lines
12 KiB
Python
"""
|
|
消息通知服务层
|
|
"""
|
|
from typing import Optional, List, Dict, Any
|
|
from datetime import datetime
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from app.crud.notification import notification_crud
|
|
from app.models.notification import NotificationTemplate
|
|
from app.models.user import User
|
|
from app.schemas.notification import (
|
|
NotificationCreate,
|
|
NotificationBatchCreate,
|
|
NotificationSendFromTemplate
|
|
)
|
|
import json
|
|
|
|
|
|
class NotificationService:
|
|
"""消息通知服务类"""
|
|
|
|
async def get_notification(self, db: AsyncSession, notification_id: int) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
获取消息通知详情
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
notification_id: 通知ID
|
|
|
|
Returns:
|
|
通知信息
|
|
"""
|
|
notification = await notification_crud.get(db, notification_id)
|
|
if not notification:
|
|
return None
|
|
|
|
return {
|
|
"id": notification.id,
|
|
"recipient_id": notification.recipient_id,
|
|
"recipient_name": notification.recipient_name,
|
|
"title": notification.title,
|
|
"content": notification.content,
|
|
"notification_type": notification.notification_type,
|
|
"priority": notification.priority,
|
|
"is_read": notification.is_read,
|
|
"read_at": notification.read_at,
|
|
"related_entity_type": notification.related_entity_type,
|
|
"related_entity_id": notification.related_entity_id,
|
|
"action_url": notification.action_url,
|
|
"extra_data": notification.extra_data,
|
|
"sent_via_email": notification.sent_via_email,
|
|
"sent_via_sms": notification.sent_via_sms,
|
|
"created_at": notification.created_at,
|
|
"expire_at": notification.expire_at,
|
|
}
|
|
|
|
async def get_notifications(
|
|
self,
|
|
db: AsyncSession,
|
|
*,
|
|
skip: int = 0,
|
|
limit: int = 20,
|
|
recipient_id: Optional[int] = None,
|
|
notification_type: Optional[str] = None,
|
|
priority: Optional[str] = None,
|
|
is_read: Optional[bool] = None,
|
|
start_time: Optional[datetime] = None,
|
|
end_time: Optional[datetime] = None,
|
|
keyword: Optional[str] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
获取消息通知列表
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
skip: 跳过条数
|
|
limit: 返回条数
|
|
recipient_id: 接收人ID
|
|
notification_type: 通知类型
|
|
priority: 优先级
|
|
is_read: 是否已读
|
|
start_time: 开始时间
|
|
end_time: 结束时间
|
|
keyword: 关键词
|
|
|
|
Returns:
|
|
通知列表和总数
|
|
"""
|
|
items, total = await notification_crud.get_multi(
|
|
db,
|
|
skip=skip,
|
|
limit=limit,
|
|
recipient_id=recipient_id,
|
|
notification_type=notification_type,
|
|
priority=priority,
|
|
is_read=is_read,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
keyword=keyword
|
|
)
|
|
|
|
return {
|
|
"items": [
|
|
{
|
|
"id": item.id,
|
|
"recipient_id": item.recipient_id,
|
|
"recipient_name": item.recipient_name,
|
|
"title": item.title,
|
|
"content": item.content,
|
|
"notification_type": item.notification_type,
|
|
"priority": item.priority,
|
|
"is_read": item.is_read,
|
|
"read_at": item.read_at,
|
|
"action_url": item.action_url,
|
|
"created_at": item.created_at,
|
|
}
|
|
for item in items
|
|
],
|
|
"total": total
|
|
}
|
|
|
|
async def create_notification(
|
|
self,
|
|
db: AsyncSession,
|
|
obj_in: NotificationCreate
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
创建消息通知
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
obj_in: 创建数据
|
|
|
|
Returns:
|
|
创建的通知信息
|
|
"""
|
|
# 获取接收人信息
|
|
user_result = await db.execute(
|
|
select(User).where(User.id == obj_in.recipient_id)
|
|
)
|
|
user = user_result.scalar_one_or_none()
|
|
if not user:
|
|
raise ValueError("接收人不存在")
|
|
|
|
# 转换为字典
|
|
obj_in_data = obj_in.model_dump()
|
|
obj_in_data["recipient_name"] = user.real_name
|
|
|
|
# 处理复杂类型
|
|
if obj_in_data.get("extra_data"):
|
|
obj_in_data["extra_data"] = json.loads(obj_in.extra_data.model_dump_json()) if isinstance(obj_in.extra_data, dict) else obj_in.extra_data
|
|
|
|
# 设置邮件和短信发送标记
|
|
obj_in_data["sent_via_email"] = obj_in_data.pop("send_email", False)
|
|
obj_in_data["sent_via_sms"] = obj_in_data.pop("send_sms", False)
|
|
|
|
notification = await notification_crud.create(db, obj_in=obj_in_data)
|
|
|
|
# TODO: 发送邮件和短信
|
|
# if notification.sent_via_email:
|
|
# await self._send_email(notification)
|
|
# if notification.sent_via_sms:
|
|
# await self._send_sms(notification)
|
|
|
|
return {
|
|
"id": notification.id,
|
|
"recipient_id": notification.recipient_id,
|
|
"title": notification.title,
|
|
}
|
|
|
|
async def batch_create_notifications(
|
|
self,
|
|
db: AsyncSession,
|
|
batch_in: NotificationBatchCreate
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
批量创建消息通知
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
batch_in: 批量创建数据
|
|
|
|
Returns:
|
|
创建结果
|
|
"""
|
|
# 获取接收人信息
|
|
user_results = await db.execute(
|
|
select(User).where(User.id.in_(batch_in.recipient_ids))
|
|
)
|
|
users = {user.id: user.real_name for user in user_results.scalars()}
|
|
|
|
# 准备通知数据
|
|
notification_data = {
|
|
"title": batch_in.title,
|
|
"content": batch_in.content,
|
|
"notification_type": batch_in.notification_type.value,
|
|
"priority": batch_in.priority.value,
|
|
"action_url": batch_in.action_url,
|
|
"extra_data": json.loads(batch_in.extra_data.model_dump_json()) if batch_in.extra_data else {},
|
|
}
|
|
|
|
# 批量创建
|
|
notifications = await notification_crud.batch_create(
|
|
db,
|
|
recipient_ids=batch_in.recipient_ids,
|
|
notification_data=notification_data
|
|
)
|
|
|
|
# 更新接收人姓名
|
|
for notification in notifications:
|
|
notification.recipient_name = users.get(notification.recipient_id, "")
|
|
|
|
await db.flush()
|
|
|
|
return {
|
|
"count": len(notifications),
|
|
"notification_ids": [n.id for n in notifications]
|
|
}
|
|
|
|
async def mark_as_read(
|
|
self,
|
|
db: AsyncSession,
|
|
notification_id: int
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
标记为已读
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
notification_id: 通知ID
|
|
|
|
Returns:
|
|
更新结果
|
|
"""
|
|
notification = await notification_crud.mark_as_read(
|
|
db,
|
|
notification_id=notification_id,
|
|
read_at=datetime.utcnow()
|
|
)
|
|
|
|
if not notification:
|
|
raise ValueError("通知不存在")
|
|
|
|
return {
|
|
"id": notification.id,
|
|
"is_read": notification.is_read,
|
|
"read_at": notification.read_at
|
|
}
|
|
|
|
async def mark_all_as_read(
|
|
self,
|
|
db: AsyncSession,
|
|
recipient_id: int
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
标记所有未读为已读
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
recipient_id: 接收人ID
|
|
|
|
Returns:
|
|
更新结果
|
|
"""
|
|
count = await notification_crud.mark_all_as_read(
|
|
db,
|
|
recipient_id=recipient_id,
|
|
read_at=datetime.utcnow()
|
|
)
|
|
|
|
return {
|
|
"count": count,
|
|
"message": f"已标记 {count} 条通知为已读"
|
|
}
|
|
|
|
async def delete_notification(self, db: AsyncSession, notification_id: int) -> None:
|
|
"""
|
|
删除消息通知
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
notification_id: 通知ID
|
|
"""
|
|
await notification_crud.delete(db, notification_id=notification_id)
|
|
|
|
async def batch_delete_notifications(
|
|
self,
|
|
db: AsyncSession,
|
|
notification_ids: List[int]
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
批量删除通知
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
notification_ids: 通知ID列表
|
|
|
|
Returns:
|
|
删除结果
|
|
"""
|
|
count = await notification_crud.batch_delete(db, notification_ids=notification_ids)
|
|
|
|
return {
|
|
"count": count,
|
|
"message": f"已删除 {count} 条通知"
|
|
}
|
|
|
|
async def get_unread_count(self, db: AsyncSession, recipient_id: int) -> Dict[str, Any]:
|
|
"""
|
|
获取未读通知数量
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
recipient_id: 接收人ID
|
|
|
|
Returns:
|
|
未读数量
|
|
"""
|
|
count = await notification_crud.get_unread_count(db, recipient_id)
|
|
|
|
return {"unread_count": count}
|
|
|
|
async def get_statistics(self, db: AsyncSession, recipient_id: int) -> Dict[str, Any]:
|
|
"""
|
|
获取通知统计信息
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
recipient_id: 接收人ID
|
|
|
|
Returns:
|
|
统计信息
|
|
"""
|
|
return await notification_crud.get_statistics(db, recipient_id)
|
|
|
|
async def send_from_template(
|
|
self,
|
|
db: AsyncSession,
|
|
template_in: NotificationSendFromTemplate
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
从模板发送通知
|
|
|
|
Args:
|
|
db: 数据库会话
|
|
template_in: 模板发送数据
|
|
|
|
Returns:
|
|
发送结果
|
|
"""
|
|
# 获取模板
|
|
result = await db.execute(
|
|
select(NotificationTemplate).where(
|
|
and_(
|
|
NotificationTemplate.template_code == template_in.template_code,
|
|
NotificationTemplate.is_active == True
|
|
)
|
|
)
|
|
)
|
|
template = result.scalar_one_or_none()
|
|
if not template:
|
|
raise ValueError(f"通知模板 {template_in.template_code} 不存在或未启用")
|
|
|
|
# 渲染标题和内容
|
|
title = self._render_template(template.title_template, template_in.variables)
|
|
content = self._render_template(template.content_template, template_in.variables)
|
|
|
|
# 创建批量通知数据
|
|
batch_data = NotificationBatchCreate(
|
|
recipient_ids=template_in.recipient_ids,
|
|
title=title,
|
|
content=content,
|
|
notification_type=template.notification_type,
|
|
priority=template.priority,
|
|
action_url=template_in.action_url,
|
|
extra_data={
|
|
"template_code": template.template_code,
|
|
"variables": template_in.variables
|
|
}
|
|
)
|
|
|
|
return await self.batch_create_notifications(db, batch_data)
|
|
|
|
def _render_template(self, template: str, variables: Dict[str, Any]) -> str:
|
|
"""
|
|
渲染模板
|
|
|
|
Args:
|
|
template: 模板字符串
|
|
variables: 变量字典
|
|
|
|
Returns:
|
|
渲染后的字符串
|
|
"""
|
|
try:
|
|
return template.format(**variables)
|
|
except KeyError as e:
|
|
raise ValueError(f"模板变量缺失: {e}")
|
|
|
|
|
|
# 创建全局实例
|
|
notification_service = NotificationService()
|