1. 课程学习进度追踪
- 新增 UserCourseProgress 和 UserMaterialProgress 模型
- 新增 /api/v1/progress/* 进度追踪 API
- 更新 admin.py 使用真实课程完成率数据
2. 路由权限检查完善
- 新增前端 permissionChecker.ts 权限检查工具
- 更新 router/guard.ts 实现团队和课程权限验证
- 新增后端 permission_service.py
3. AI 陪练音频转文本
- 新增 speech_recognition.py 语音识别服务
- 新增 /api/v1/speech/* API
- 更新 ai-practice-coze.vue 支持语音输入
4. 双人对练报告生成
- 更新 practice_room_service.py 添加报告生成功能
- 新增 /rooms/{room_code}/report API
- 更新 duo-practice-report.vue 调用真实 API
5. 学习提醒推送
- 新增 notification_service.py 通知服务
- 新增 scheduler_service.py 定时任务服务
- 支持钉钉、企微、站内消息推送
6. 智能学习推荐
- 新增 recommendation_service.py 推荐服务
- 新增 /api/v1/recommendations/* API
- 支持错题、能力、进度、热门多维度推荐
7. 安全问题修复
- DEBUG 默认值改为 False
- 添加 SECRET_KEY 安全警告
- 新增 check_security_settings() 检查函数
8. 证书 PDF 生成
- 更新 certificate_service.py 添加 PDF 生成
- 添加 weasyprint、Pillow、qrcode 依赖
- 更新下载 API 支持 PDF 和 PNG 格式
This commit is contained in:
@@ -1,330 +1,419 @@
|
||||
"""
|
||||
站内消息通知服务
|
||||
提供通知的CRUD操作和业务逻辑
|
||||
通知推送服务
|
||||
支持钉钉、企业微信、站内消息等多种渠道
|
||||
"""
|
||||
from typing import List, Optional, Tuple
|
||||
from sqlalchemy import select, and_, desc, func, update
|
||||
from sqlalchemy.orm import selectinload
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List, Dict, Any
|
||||
import httpx
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, and_
|
||||
|
||||
from app.core.logger import get_logger
|
||||
from app.models.notification import Notification
|
||||
from app.models.user import User
|
||||
from app.schemas.notification import (
|
||||
NotificationCreate,
|
||||
NotificationBatchCreate,
|
||||
NotificationResponse,
|
||||
NotificationType,
|
||||
)
|
||||
from app.services.base_service import BaseService
|
||||
from app.models.notification import Notification
|
||||
|
||||
logger = get_logger(__name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NotificationService(BaseService[Notification]):
|
||||
"""
|
||||
站内消息通知服务
|
||||
class NotificationChannel:
|
||||
"""通知渠道基类"""
|
||||
|
||||
提供通知的创建、查询、标记已读等功能
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(Notification)
|
||||
|
||||
async def create_notification(
|
||||
async def send(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
notification_in: NotificationCreate
|
||||
) -> Notification:
|
||||
"""
|
||||
创建单个通知
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
notification_in: 通知创建数据
|
||||
|
||||
Returns:
|
||||
创建的通知对象
|
||||
"""
|
||||
notification = Notification(
|
||||
user_id=notification_in.user_id,
|
||||
title=notification_in.title,
|
||||
content=notification_in.content,
|
||||
type=notification_in.type.value if isinstance(notification_in.type, NotificationType) else notification_in.type,
|
||||
related_id=notification_in.related_id,
|
||||
related_type=notification_in.related_type,
|
||||
sender_id=notification_in.sender_id,
|
||||
is_read=False
|
||||
)
|
||||
|
||||
db.add(notification)
|
||||
await db.commit()
|
||||
await db.refresh(notification)
|
||||
|
||||
logger.info(
|
||||
"创建通知成功",
|
||||
notification_id=notification.id,
|
||||
user_id=notification_in.user_id,
|
||||
type=notification_in.type
|
||||
)
|
||||
|
||||
return notification
|
||||
|
||||
async def batch_create_notifications(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
batch_in: NotificationBatchCreate
|
||||
) -> List[Notification]:
|
||||
"""
|
||||
批量创建通知(发送给多个用户)
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
batch_in: 批量通知创建数据
|
||||
|
||||
Returns:
|
||||
创建的通知列表
|
||||
"""
|
||||
notifications = []
|
||||
notification_type = batch_in.type.value if isinstance(batch_in.type, NotificationType) else batch_in.type
|
||||
|
||||
for user_id in batch_in.user_ids:
|
||||
notification = Notification(
|
||||
user_id=user_id,
|
||||
title=batch_in.title,
|
||||
content=batch_in.content,
|
||||
type=notification_type,
|
||||
related_id=batch_in.related_id,
|
||||
related_type=batch_in.related_type,
|
||||
sender_id=batch_in.sender_id,
|
||||
is_read=False
|
||||
)
|
||||
notifications.append(notification)
|
||||
db.add(notification)
|
||||
|
||||
await db.commit()
|
||||
|
||||
# 刷新所有对象
|
||||
for notification in notifications:
|
||||
await db.refresh(notification)
|
||||
|
||||
logger.info(
|
||||
"批量创建通知成功",
|
||||
count=len(notifications),
|
||||
user_ids=batch_in.user_ids,
|
||||
type=batch_in.type
|
||||
)
|
||||
|
||||
return notifications
|
||||
|
||||
async def get_user_notifications(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
is_read: Optional[bool] = None,
|
||||
notification_type: Optional[str] = None
|
||||
) -> Tuple[List[NotificationResponse], int, int]:
|
||||
"""
|
||||
获取用户的通知列表
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
user_id: 用户ID
|
||||
skip: 跳过数量
|
||||
limit: 返回数量
|
||||
is_read: 是否已读筛选
|
||||
notification_type: 通知类型筛选
|
||||
|
||||
Returns:
|
||||
(通知列表, 总数, 未读数)
|
||||
"""
|
||||
# 构建基础查询条件
|
||||
conditions = [Notification.user_id == user_id]
|
||||
|
||||
if is_read is not None:
|
||||
conditions.append(Notification.is_read == is_read)
|
||||
|
||||
if notification_type:
|
||||
conditions.append(Notification.type == notification_type)
|
||||
|
||||
# 查询通知列表(带发送者信息)
|
||||
stmt = (
|
||||
select(Notification)
|
||||
.where(and_(*conditions))
|
||||
.order_by(desc(Notification.created_at))
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
notifications = result.scalars().all()
|
||||
|
||||
# 统计总数
|
||||
count_stmt = select(func.count()).select_from(Notification).where(and_(*conditions))
|
||||
total_result = await db.execute(count_stmt)
|
||||
total = total_result.scalar_one()
|
||||
|
||||
# 统计未读数
|
||||
unread_stmt = (
|
||||
select(func.count())
|
||||
.select_from(Notification)
|
||||
.where(and_(Notification.user_id == user_id, Notification.is_read == False))
|
||||
)
|
||||
unread_result = await db.execute(unread_stmt)
|
||||
unread_count = unread_result.scalar_one()
|
||||
|
||||
# 获取发送者信息
|
||||
sender_ids = [n.sender_id for n in notifications if n.sender_id]
|
||||
sender_names = {}
|
||||
if sender_ids:
|
||||
sender_stmt = select(User.id, User.full_name).where(User.id.in_(sender_ids))
|
||||
sender_result = await db.execute(sender_stmt)
|
||||
sender_names = {row[0]: row[1] for row in sender_result.fetchall()}
|
||||
|
||||
# 构建响应
|
||||
responses = []
|
||||
for notification in notifications:
|
||||
response = NotificationResponse(
|
||||
id=notification.id,
|
||||
user_id=notification.user_id,
|
||||
title=notification.title,
|
||||
content=notification.content,
|
||||
type=notification.type,
|
||||
is_read=notification.is_read,
|
||||
related_id=notification.related_id,
|
||||
related_type=notification.related_type,
|
||||
sender_id=notification.sender_id,
|
||||
sender_name=sender_names.get(notification.sender_id) if notification.sender_id else None,
|
||||
created_at=notification.created_at,
|
||||
updated_at=notification.updated_at
|
||||
)
|
||||
responses.append(response)
|
||||
|
||||
return responses, total, unread_count
|
||||
|
||||
async def get_unread_count(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: int
|
||||
) -> Tuple[int, int]:
|
||||
"""
|
||||
获取用户未读通知数量
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
(未读数, 总数)
|
||||
"""
|
||||
# 统计未读数
|
||||
unread_stmt = (
|
||||
select(func.count())
|
||||
.select_from(Notification)
|
||||
.where(and_(Notification.user_id == user_id, Notification.is_read == False))
|
||||
)
|
||||
unread_result = await db.execute(unread_stmt)
|
||||
unread_count = unread_result.scalar_one()
|
||||
|
||||
# 统计总数
|
||||
total_stmt = (
|
||||
select(func.count())
|
||||
.select_from(Notification)
|
||||
.where(Notification.user_id == user_id)
|
||||
)
|
||||
total_result = await db.execute(total_stmt)
|
||||
total = total_result.scalar_one()
|
||||
|
||||
return unread_count, total
|
||||
|
||||
async def mark_as_read(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
notification_ids: Optional[List[int]] = None
|
||||
) -> int:
|
||||
"""
|
||||
标记通知为已读
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
user_id: 用户ID
|
||||
notification_ids: 通知ID列表,为空则标记全部
|
||||
|
||||
Returns:
|
||||
更新的数量
|
||||
"""
|
||||
conditions = [
|
||||
Notification.user_id == user_id,
|
||||
Notification.is_read == False
|
||||
]
|
||||
|
||||
if notification_ids:
|
||||
conditions.append(Notification.id.in_(notification_ids))
|
||||
|
||||
stmt = (
|
||||
update(Notification)
|
||||
.where(and_(*conditions))
|
||||
.values(is_read=True)
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
await db.commit()
|
||||
|
||||
updated_count = result.rowcount
|
||||
|
||||
logger.info(
|
||||
"标记通知已读",
|
||||
user_id=user_id,
|
||||
notification_ids=notification_ids,
|
||||
updated_count=updated_count
|
||||
)
|
||||
|
||||
return updated_count
|
||||
|
||||
async def delete_notification(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
notification_id: int
|
||||
title: str,
|
||||
content: str,
|
||||
**kwargs
|
||||
) -> bool:
|
||||
"""
|
||||
删除通知
|
||||
发送通知
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
user_id: 用户ID
|
||||
notification_id: 通知ID
|
||||
title: 通知标题
|
||||
content: 通知内容
|
||||
|
||||
Returns:
|
||||
是否删除成功
|
||||
是否发送成功
|
||||
"""
|
||||
stmt = select(Notification).where(
|
||||
and_(
|
||||
Notification.id == notification_id,
|
||||
Notification.user_id == user_id
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DingtalkChannel(NotificationChannel):
|
||||
"""
|
||||
钉钉通知渠道
|
||||
|
||||
使用钉钉工作通知 API 发送消息
|
||||
文档: https://open.dingtalk.com/document/orgapp/asynchronous-sending-of-enterprise-session-messages
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app_key: Optional[str] = None,
|
||||
app_secret: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
):
|
||||
self.app_key = app_key or os.getenv("DINGTALK_APP_KEY")
|
||||
self.app_secret = app_secret or os.getenv("DINGTALK_APP_SECRET")
|
||||
self.agent_id = agent_id or os.getenv("DINGTALK_AGENT_ID")
|
||||
self._access_token = None
|
||||
self._token_expires_at = None
|
||||
|
||||
async def _get_access_token(self) -> str:
|
||||
"""获取钉钉访问令牌"""
|
||||
if (
|
||||
self._access_token
|
||||
and self._token_expires_at
|
||||
and datetime.now() < self._token_expires_at
|
||||
):
|
||||
return self._access_token
|
||||
|
||||
url = "https://oapi.dingtalk.com/gettoken"
|
||||
params = {
|
||||
"appkey": self.app_key,
|
||||
"appsecret": self.app_secret,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, params=params, timeout=10.0)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
self._access_token = result["access_token"]
|
||||
self._token_expires_at = datetime.now() + timedelta(seconds=7000)
|
||||
return self._access_token
|
||||
else:
|
||||
raise Exception(f"获取钉钉Token失败: {result.get('errmsg')}")
|
||||
|
||||
async def send(
|
||||
self,
|
||||
user_id: int,
|
||||
title: str,
|
||||
content: str,
|
||||
dingtalk_user_id: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> bool:
|
||||
"""发送钉钉工作通知"""
|
||||
if not all([self.app_key, self.app_secret, self.agent_id]):
|
||||
logger.warning("钉钉配置不完整,跳过发送")
|
||||
return False
|
||||
|
||||
if not dingtalk_user_id:
|
||||
logger.warning(f"用户 {user_id} 没有绑定钉钉ID")
|
||||
return False
|
||||
|
||||
try:
|
||||
access_token = await self._get_access_token()
|
||||
|
||||
url = f"https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_token={access_token}"
|
||||
|
||||
# 构建消息体
|
||||
msg = {
|
||||
"agent_id": self.agent_id,
|
||||
"userid_list": dingtalk_user_id,
|
||||
"msg": {
|
||||
"msgtype": "text",
|
||||
"text": {
|
||||
"content": f"{title}\n\n{content}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, json=msg, timeout=10.0)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
logger.info(f"钉钉消息发送成功: user_id={user_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"钉钉消息发送失败: {result.get('errmsg')}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"钉钉消息发送异常: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
class WeworkChannel(NotificationChannel):
|
||||
"""
|
||||
企业微信通知渠道
|
||||
|
||||
使用企业微信应用消息 API
|
||||
文档: https://developer.work.weixin.qq.com/document/path/90236
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
corp_id: Optional[str] = None,
|
||||
corp_secret: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
):
|
||||
self.corp_id = corp_id or os.getenv("WEWORK_CORP_ID")
|
||||
self.corp_secret = corp_secret or os.getenv("WEWORK_CORP_SECRET")
|
||||
self.agent_id = agent_id or os.getenv("WEWORK_AGENT_ID")
|
||||
self._access_token = None
|
||||
self._token_expires_at = None
|
||||
|
||||
async def _get_access_token(self) -> str:
|
||||
"""获取企业微信访问令牌"""
|
||||
if (
|
||||
self._access_token
|
||||
and self._token_expires_at
|
||||
and datetime.now() < self._token_expires_at
|
||||
):
|
||||
return self._access_token
|
||||
|
||||
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
||||
params = {
|
||||
"corpid": self.corp_id,
|
||||
"corpsecret": self.corp_secret,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(url, params=params, timeout=10.0)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
self._access_token = result["access_token"]
|
||||
self._token_expires_at = datetime.now() + timedelta(seconds=7000)
|
||||
return self._access_token
|
||||
else:
|
||||
raise Exception(f"获取企微Token失败: {result.get('errmsg')}")
|
||||
|
||||
async def send(
|
||||
self,
|
||||
user_id: int,
|
||||
title: str,
|
||||
content: str,
|
||||
wework_user_id: Optional[str] = None,
|
||||
**kwargs
|
||||
) -> bool:
|
||||
"""发送企业微信应用消息"""
|
||||
if not all([self.corp_id, self.corp_secret, self.agent_id]):
|
||||
logger.warning("企业微信配置不完整,跳过发送")
|
||||
return False
|
||||
|
||||
if not wework_user_id:
|
||||
logger.warning(f"用户 {user_id} 没有绑定企业微信ID")
|
||||
return False
|
||||
|
||||
try:
|
||||
access_token = await self._get_access_token()
|
||||
|
||||
url = f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}"
|
||||
|
||||
# 构建消息体
|
||||
msg = {
|
||||
"touser": wework_user_id,
|
||||
"msgtype": "text",
|
||||
"agentid": int(self.agent_id),
|
||||
"text": {
|
||||
"content": f"{title}\n\n{content}"
|
||||
}
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(url, json=msg, timeout=10.0)
|
||||
result = response.json()
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
logger.info(f"企微消息发送成功: user_id={user_id}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"企微消息发送失败: {result.get('errmsg')}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"企微消息发送异常: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
class InAppChannel(NotificationChannel):
|
||||
"""站内消息通道"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def send(
|
||||
self,
|
||||
user_id: int,
|
||||
title: str,
|
||||
content: str,
|
||||
notification_type: str = "system",
|
||||
**kwargs
|
||||
) -> bool:
|
||||
"""创建站内消息"""
|
||||
try:
|
||||
notification = Notification(
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
content=content,
|
||||
type=notification_type,
|
||||
is_read=False,
|
||||
)
|
||||
self.db.add(notification)
|
||||
await self.db.commit()
|
||||
logger.info(f"站内消息创建成功: user_id={user_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"站内消息创建失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
class NotificationService:
|
||||
"""
|
||||
通知服务
|
||||
|
||||
统一管理多渠道通知发送
|
||||
"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
self.channels = {
|
||||
"dingtalk": DingtalkChannel(),
|
||||
"wework": WeworkChannel(),
|
||||
"inapp": InAppChannel(db),
|
||||
}
|
||||
|
||||
async def send_notification(
|
||||
self,
|
||||
user_id: int,
|
||||
title: str,
|
||||
content: str,
|
||||
channels: Optional[List[str]] = None,
|
||||
**kwargs
|
||||
) -> Dict[str, bool]:
|
||||
"""
|
||||
发送通知
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
title: 通知标题
|
||||
content: 通知内容
|
||||
channels: 发送渠道列表,默认全部发送
|
||||
|
||||
Returns:
|
||||
各渠道发送结果
|
||||
"""
|
||||
# 获取用户信息
|
||||
user = await self._get_user(user_id)
|
||||
if not user:
|
||||
return {"error": "用户不存在"}
|
||||
|
||||
# 准备用户渠道标识
|
||||
user_channels = {
|
||||
"dingtalk_user_id": getattr(user, "dingtalk_id", None),
|
||||
"wework_user_id": getattr(user, "wework_userid", None),
|
||||
}
|
||||
|
||||
# 确定发送渠道
|
||||
target_channels = channels or ["inapp"] # 默认只发站内消息
|
||||
|
||||
results = {}
|
||||
for channel_name in target_channels:
|
||||
if channel_name in self.channels:
|
||||
channel = self.channels[channel_name]
|
||||
success = await channel.send(
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
content=content,
|
||||
**user_channels,
|
||||
**kwargs
|
||||
)
|
||||
results[channel_name] = success
|
||||
|
||||
return results
|
||||
|
||||
async def send_learning_reminder(
|
||||
self,
|
||||
user_id: int,
|
||||
course_name: str,
|
||||
days_inactive: int = 3,
|
||||
) -> Dict[str, bool]:
|
||||
"""发送学习提醒"""
|
||||
title = "📚 学习提醒"
|
||||
content = f"您已有 {days_inactive} 天没有学习《{course_name}》课程了,快来继续学习吧!"
|
||||
|
||||
return await self.send_notification(
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
content=content,
|
||||
channels=["inapp", "dingtalk", "wework"],
|
||||
notification_type="learning_reminder",
|
||||
)
|
||||
|
||||
async def send_task_deadline_reminder(
|
||||
self,
|
||||
user_id: int,
|
||||
task_name: str,
|
||||
deadline: datetime,
|
||||
) -> Dict[str, bool]:
|
||||
"""发送任务截止提醒"""
|
||||
days_left = (deadline - datetime.now()).days
|
||||
title = "⏰ 任务截止提醒"
|
||||
content = f"任务《{task_name}》将于 {deadline.strftime('%Y-%m-%d %H:%M')} 截止,还有 {days_left} 天,请尽快完成!"
|
||||
|
||||
return await self.send_notification(
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
content=content,
|
||||
channels=["inapp", "dingtalk", "wework"],
|
||||
notification_type="task_deadline",
|
||||
)
|
||||
|
||||
async def send_exam_reminder(
|
||||
self,
|
||||
user_id: int,
|
||||
exam_name: str,
|
||||
exam_time: datetime,
|
||||
) -> Dict[str, bool]:
|
||||
"""发送考试提醒"""
|
||||
title = "📝 考试提醒"
|
||||
content = f"考试《{exam_name}》将于 {exam_time.strftime('%Y-%m-%d %H:%M')} 开始,请提前做好准备!"
|
||||
|
||||
return await self.send_notification(
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
content=content,
|
||||
channels=["inapp", "dingtalk", "wework"],
|
||||
notification_type="exam_reminder",
|
||||
)
|
||||
|
||||
async def send_weekly_report(
|
||||
self,
|
||||
user_id: int,
|
||||
study_time: int,
|
||||
courses_completed: int,
|
||||
exams_passed: int,
|
||||
) -> Dict[str, bool]:
|
||||
"""发送周学习报告"""
|
||||
title = "📊 本周学习报告"
|
||||
content = (
|
||||
f"本周学习总结:\n"
|
||||
f"• 学习时长:{study_time // 60} 分钟\n"
|
||||
f"• 完成课程:{courses_completed} 门\n"
|
||||
f"• 通过考试:{exams_passed} 次\n\n"
|
||||
f"继续加油!💪"
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
notification = result.scalar_one_or_none()
|
||||
|
||||
if notification:
|
||||
await db.delete(notification)
|
||||
await db.commit()
|
||||
|
||||
logger.info(
|
||||
"删除通知成功",
|
||||
notification_id=notification_id,
|
||||
user_id=user_id
|
||||
)
|
||||
return True
|
||||
|
||||
return False
|
||||
return await self.send_notification(
|
||||
user_id=user_id,
|
||||
title=title,
|
||||
content=content,
|
||||
channels=["inapp", "dingtalk", "wework"],
|
||||
notification_type="weekly_report",
|
||||
)
|
||||
|
||||
async def _get_user(self, user_id: int) -> Optional[User]:
|
||||
"""获取用户信息"""
|
||||
result = await self.db.execute(
|
||||
select(User).where(User.id == user_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
# 创建服务实例
|
||||
notification_service = NotificationService()
|
||||
|
||||
# 便捷函数
|
||||
def get_notification_service(db: AsyncSession) -> NotificationService:
|
||||
"""获取通知服务实例"""
|
||||
return NotificationService(db)
|
||||
|
||||
Reference in New Issue
Block a user