961 lines
35 KiB
Python
961 lines
35 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
消息中心 API(异步版本)
|
||
"""
|
||
from typing import Dict, List, Optional
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.database import get_db
|
||
from app.base_schema import PaginatedResponse, ResponseModel
|
||
from core.message.schema import (
|
||
MessageOut,
|
||
MessageListOut,
|
||
UnreadCountOut,
|
||
MarkReadInput,
|
||
SendMessageInput,
|
||
AnnouncementCreate,
|
||
AnnouncementUpdate,
|
||
AnnouncementOut,
|
||
AnnouncementListOut,
|
||
UserAnnouncementOut,
|
||
ReadStatsOut,
|
||
EmailTestInput,
|
||
EmailSendInput,
|
||
DingTalkWebhookTestInput,
|
||
DingTalkWorkNoticeTestInput,
|
||
DingTalkTodoTestInput,
|
||
DingTalkTodoCompleteInput,
|
||
FeishuWebhookTestInput,
|
||
FeishuAppMessageTestInput,
|
||
WecomWebhookTestInput,
|
||
WecomAppMessageTestInput,
|
||
WechatMPTestInput,
|
||
)
|
||
from core.message.service import MessageService, AnnouncementService, NotifyService
|
||
|
||
router = APIRouter(prefix="/message", tags=["消息中心"])
|
||
|
||
|
||
# ============ 消息 API ============
|
||
|
||
@router.get("/list", response_model=PaginatedResponse[MessageListOut], summary="消息列表")
|
||
async def list_messages(
|
||
request: Request,
|
||
msg_type: str = Query(None, description="消息类型"),
|
||
status: str = Query(None, description="状态: unread/read"),
|
||
page: int = Query(default=1, ge=1, description="页码"),
|
||
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize", description="每页数量"),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取当前用户的消息列表"""
|
||
user_id = request.state.user_id
|
||
items, total = await MessageService.get_list(
|
||
db=db,
|
||
user_id=user_id,
|
||
msg_type=msg_type,
|
||
status=status,
|
||
page=page,
|
||
page_size=page_size,
|
||
)
|
||
|
||
# 批量获取发送人名称
|
||
sender_ids = list(set(str(item.sender_id) for item in items if item.sender_id))
|
||
sender_names = await _batch_get_user_names(db, sender_ids) if sender_ids else {}
|
||
|
||
return PaginatedResponse(
|
||
items=[
|
||
_build_message_list_out(item, sender_names.get(str(item.sender_id), "") if item.sender_id else "")
|
||
for item in items
|
||
],
|
||
total=total,
|
||
)
|
||
|
||
|
||
@router.get("/unread-count", response_model=UnreadCountOut, summary="未读数量")
|
||
async def get_unread_count(
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取未读消息数量"""
|
||
user_id = request.state.user_id
|
||
total = await MessageService.get_unread_count(db, user_id)
|
||
by_type = await MessageService.get_unread_count_by_type(db, user_id)
|
||
|
||
return UnreadCountOut(total=total, by_type=by_type)
|
||
|
||
|
||
@router.post("/read-all", response_model=ResponseModel, summary="全部已读")
|
||
async def mark_all_as_read(
|
||
request: Request,
|
||
data: MarkReadInput = None,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""标记所有消息为已读"""
|
||
user_id = request.state.user_id
|
||
msg_type = data.msg_type if data else None
|
||
count = await MessageService.mark_all_as_read(db, user_id, msg_type)
|
||
|
||
return ResponseModel(message=f"已标记 {count} 条消息为已读")
|
||
|
||
|
||
@router.delete("/clear-read", response_model=ResponseModel, summary="清空已读")
|
||
async def clear_read_messages(
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""清空所有已读消息"""
|
||
user_id = request.state.user_id
|
||
count = await MessageService.delete_all_read(db, user_id)
|
||
|
||
return ResponseModel(message=f"已删除 {count} 条已读消息")
|
||
|
||
|
||
@router.get("/{message_id}", response_model=MessageOut, summary="消息详情")
|
||
async def get_message(
|
||
request: Request,
|
||
message_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取消息详情"""
|
||
user_id = request.state.user_id
|
||
message = await MessageService.get_by_id(db, message_id, user_id)
|
||
if not message:
|
||
raise HTTPException(status_code=404, detail="消息不存在")
|
||
|
||
return await _build_message_out(db, message)
|
||
|
||
|
||
@router.post("/{message_id}/read", response_model=ResponseModel, summary="标记已读")
|
||
async def mark_as_read(
|
||
request: Request,
|
||
message_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""标记单条消息为已读"""
|
||
user_id = request.state.user_id
|
||
success = await MessageService.mark_as_read(db, message_id, user_id)
|
||
if not success:
|
||
raise HTTPException(status_code=404, detail="消息不存在")
|
||
|
||
return ResponseModel(message="已标记为已读")
|
||
|
||
|
||
@router.delete("/{message_id}", response_model=ResponseModel, summary="删除消息")
|
||
async def delete_message(
|
||
request: Request,
|
||
message_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""删除单条消息"""
|
||
user_id = request.state.user_id
|
||
success = await MessageService.delete(db, message_id, user_id)
|
||
if not success:
|
||
raise HTTPException(status_code=404, detail="消息不存在")
|
||
|
||
return ResponseModel(message="删除成功")
|
||
|
||
|
||
@router.post("/send", response_model=ResponseModel, summary="发送消息")
|
||
async def send_message(
|
||
request: Request,
|
||
data: SendMessageInput,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""发送站内消息(支持多渠道)"""
|
||
sender_id = request.state.user_id
|
||
|
||
if not data.recipient_ids:
|
||
raise HTTPException(status_code=400, detail="接收人不能为空")
|
||
if not data.title.strip():
|
||
raise HTTPException(status_code=400, detail="消息标题不能为空")
|
||
if not data.content.strip():
|
||
raise HTTPException(status_code=400, detail="消息内容不能为空")
|
||
|
||
results = await NotifyService.send(
|
||
db=db,
|
||
recipient_ids=data.recipient_ids,
|
||
title=data.title,
|
||
content=data.content,
|
||
channels=data.channels,
|
||
msg_type=data.msg_type,
|
||
sender_id=sender_id,
|
||
)
|
||
|
||
return ResponseModel(
|
||
message=f"消息已发送给 {len(data.recipient_ids)} 人",
|
||
data={"results": results, "recipient_count": len(data.recipient_ids)},
|
||
)
|
||
|
||
|
||
# ============ 公告管理端 API ============
|
||
|
||
announcement_router = APIRouter(prefix="/announcement", tags=["公告管理"])
|
||
|
||
|
||
@announcement_router.get("/admin/list", response_model=PaginatedResponse[AnnouncementListOut], summary="公告列表(管理)")
|
||
async def list_announcements(
|
||
status: str = Query(None, description="状态: draft/published/expired"),
|
||
keyword: str = Query(None, description="关键词搜索"),
|
||
page: int = Query(default=1, ge=1, description="页码"),
|
||
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize", description="每页数量"),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取公告列表(管理端)"""
|
||
items, total = await AnnouncementService.get_list(
|
||
db=db,
|
||
page=page,
|
||
page_size=page_size,
|
||
status=status,
|
||
keyword=keyword,
|
||
)
|
||
|
||
# 批量查询发布人名称
|
||
publisher_names = await _batch_get_user_names(db, [a.publisher_id for a in items if a.publisher_id])
|
||
|
||
return PaginatedResponse(
|
||
items=[_build_announcement_list_out(item, publisher_names) for item in items],
|
||
total=total,
|
||
)
|
||
|
||
|
||
@announcement_router.get("/admin/{announcement_id}", response_model=AnnouncementOut, summary="公告详情(管理)")
|
||
async def get_announcement(
|
||
announcement_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取公告详情"""
|
||
announcement = await AnnouncementService.get_by_id(db, announcement_id)
|
||
if not announcement:
|
||
raise HTTPException(status_code=404, detail="公告不存在")
|
||
|
||
return await _build_announcement_out(db, announcement)
|
||
|
||
|
||
@announcement_router.post("/admin", response_model=AnnouncementOut, summary="创建公告")
|
||
async def create_announcement(
|
||
request: Request,
|
||
data: AnnouncementCreate,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""创建公告"""
|
||
user_id = request.state.user_id
|
||
announcement = await AnnouncementService.create(db, data.model_dump(), user_id)
|
||
return await _build_announcement_out(db, announcement)
|
||
|
||
|
||
@announcement_router.put("/admin/{announcement_id}", response_model=AnnouncementOut, summary="更新公告")
|
||
async def update_announcement(
|
||
request: Request,
|
||
announcement_id: str,
|
||
data: AnnouncementUpdate,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""更新公告"""
|
||
user_id = request.state.user_id
|
||
announcement = await AnnouncementService.update(
|
||
db, announcement_id, data.model_dump(exclude_unset=True), user_id
|
||
)
|
||
if not announcement:
|
||
raise HTTPException(status_code=404, detail="公告不存在")
|
||
|
||
return await _build_announcement_out(db, announcement)
|
||
|
||
|
||
@announcement_router.delete("/admin/{announcement_id}", response_model=ResponseModel, summary="删除公告")
|
||
async def delete_announcement(
|
||
request: Request,
|
||
announcement_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""删除公告"""
|
||
user_id = request.state.user_id
|
||
success = await AnnouncementService.delete(db, announcement_id, user_id)
|
||
if not success:
|
||
raise HTTPException(status_code=404, detail="公告不存在")
|
||
|
||
return ResponseModel(message="删除成功")
|
||
|
||
|
||
@announcement_router.post("/admin/{announcement_id}/publish", response_model=AnnouncementOut, summary="发布公告")
|
||
async def publish_announcement(
|
||
request: Request,
|
||
announcement_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""发布公告"""
|
||
user_id = request.state.user_id
|
||
try:
|
||
announcement = await AnnouncementService.publish(db, announcement_id, user_id)
|
||
if not announcement:
|
||
raise HTTPException(status_code=404, detail="公告不存在")
|
||
return await _build_announcement_out(db, announcement)
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=400, detail=str(e))
|
||
|
||
|
||
@announcement_router.get("/admin/{announcement_id}/stats", response_model=ReadStatsOut, summary="阅读统计")
|
||
async def get_read_stats(
|
||
announcement_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取公告阅读统计"""
|
||
stats = await AnnouncementService.get_read_stats(db, announcement_id)
|
||
if not stats:
|
||
raise HTTPException(status_code=404, detail="公告不存在")
|
||
|
||
return ReadStatsOut(**stats)
|
||
|
||
|
||
# ============ 公告用户端 API ============
|
||
|
||
@announcement_router.get("/user/list", response_model=PaginatedResponse[UserAnnouncementOut], summary="我的公告")
|
||
async def list_user_announcements(
|
||
request: Request,
|
||
unread_only: bool = Query(False, description="只看未读"),
|
||
page: int = Query(default=1, ge=1, description="页码"),
|
||
page_size: int = Query(default=20, ge=1, le=100, alias="pageSize", description="每页数量"),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取当前用户可见的公告列表"""
|
||
user_id = request.state.user_id
|
||
# 从token中获取用户的部门和角色信息
|
||
user_dept_ids = []
|
||
user_role_ids = []
|
||
if hasattr(request.state, 'token_payload'):
|
||
payload = request.state.token_payload
|
||
if payload.get('dept_id'):
|
||
user_dept_ids = [payload.get('dept_id')]
|
||
if payload.get('role_id'):
|
||
user_role_ids = [payload.get('role_id')]
|
||
|
||
items, total = await AnnouncementService.get_user_announcements(
|
||
db=db,
|
||
user_id=user_id,
|
||
user_dept_ids=user_dept_ids,
|
||
user_role_ids=user_role_ids,
|
||
page=page,
|
||
page_size=page_size,
|
||
unread_only=unread_only,
|
||
)
|
||
|
||
# 批量查询发布人名称
|
||
publisher_names = await _batch_get_user_names(db, [a.publisher_id for a in items if a.publisher_id])
|
||
|
||
return PaginatedResponse(
|
||
items=[_build_user_announcement_out(item, publisher_names) for item in items],
|
||
total=total,
|
||
)
|
||
|
||
|
||
@announcement_router.get("/user/unread-count", response_model=dict, summary="未读公告数量")
|
||
async def get_user_unread_count(
|
||
request: Request,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取未读公告数量"""
|
||
user_id = request.state.user_id
|
||
user_dept_ids = []
|
||
user_role_ids = []
|
||
if hasattr(request.state, 'token_payload'):
|
||
payload = request.state.token_payload
|
||
if payload.get('dept_id'):
|
||
user_dept_ids = [payload.get('dept_id')]
|
||
if payload.get('role_id'):
|
||
user_role_ids = [payload.get('role_id')]
|
||
|
||
count = await AnnouncementService.get_unread_count(
|
||
db, user_id, user_dept_ids, user_role_ids
|
||
)
|
||
return {"count": count}
|
||
|
||
|
||
@announcement_router.get("/user/{announcement_id}", response_model=UserAnnouncementOut, summary="公告详情")
|
||
async def get_user_announcement(
|
||
request: Request,
|
||
announcement_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""获取公告详情并标记已读"""
|
||
user_id = request.state.user_id
|
||
announcement = await AnnouncementService.get_by_id(db, announcement_id)
|
||
if not announcement:
|
||
raise HTTPException(status_code=404, detail="公告不存在")
|
||
|
||
# 标记已读
|
||
await AnnouncementService.mark_as_read(db, announcement_id, user_id)
|
||
|
||
# 重新获取以更新is_read状态
|
||
announcement.is_read = True
|
||
|
||
publisher_names = await _batch_get_user_names(db, [announcement.publisher_id] if announcement.publisher_id else [])
|
||
return _build_user_announcement_out(announcement, publisher_names)
|
||
|
||
|
||
@announcement_router.post("/user/{announcement_id}/read", response_model=ResponseModel, summary="标记已读")
|
||
async def mark_announcement_as_read(
|
||
request: Request,
|
||
announcement_id: str,
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
"""标记公告为已读"""
|
||
user_id = request.state.user_id
|
||
success = await AnnouncementService.mark_as_read(db, announcement_id, user_id)
|
||
if not success:
|
||
raise HTTPException(status_code=404, detail="公告不存在")
|
||
|
||
return ResponseModel(message="已标记为已读")
|
||
|
||
|
||
# ============ 辅助函数 ============
|
||
|
||
async def _batch_get_user_names(db: AsyncSession, user_ids: list) -> Dict[str, str]:
|
||
"""批量查询用户名称,返回 {user_id: name} 映射"""
|
||
if not user_ids:
|
||
return {}
|
||
from core.user.model import User
|
||
from sqlalchemy import select
|
||
unique_ids = list(set(str(uid) for uid in user_ids))
|
||
stmt = select(User.id, User.name).where(User.id.in_(unique_ids))
|
||
result = await db.execute(stmt)
|
||
return {str(row[0]): row[1] or "" for row in result.all()}
|
||
|
||
|
||
async def _build_message_out(db: AsyncSession, message) -> dict:
|
||
"""构建消息详情输出"""
|
||
sender_name = ""
|
||
if message.sender_id:
|
||
sender_names = await _batch_get_user_names(db, [message.sender_id])
|
||
sender_name = sender_names.get(str(message.sender_id), "")
|
||
|
||
return {
|
||
"id": str(message.id),
|
||
"title": message.title,
|
||
"content": message.content,
|
||
"msg_type": message.msg_type,
|
||
"status": message.status,
|
||
"link_type": message.link_type or "",
|
||
"link_id": message.link_id or "",
|
||
"sender_name": sender_name,
|
||
"read_at": message.read_at,
|
||
"created_at": message.sys_create_datetime,
|
||
}
|
||
|
||
|
||
def _build_message_list_out(message, sender_name: str = "") -> dict:
|
||
"""构建消息列表输出"""
|
||
return {
|
||
"id": str(message.id),
|
||
"title": message.title,
|
||
"content": message.content[:100] if message.content else "",
|
||
"msg_type": message.msg_type,
|
||
"status": message.status,
|
||
"link_type": message.link_type or "",
|
||
"link_id": message.link_id or "",
|
||
"sender_id": str(message.sender_id) if message.sender_id else None,
|
||
"sender_name": sender_name,
|
||
"created_at": message.sys_create_datetime,
|
||
}
|
||
|
||
|
||
async def _build_announcement_out(db: AsyncSession, announcement) -> dict:
|
||
"""构建公告输出"""
|
||
publisher_name = ""
|
||
if announcement.publisher_id:
|
||
from core.user.model import User
|
||
from sqlalchemy import select
|
||
stmt = select(User).where(User.id == announcement.publisher_id)
|
||
result = await db.execute(stmt)
|
||
publisher = result.scalar_one_or_none()
|
||
if publisher:
|
||
publisher_name = publisher.name or ""
|
||
|
||
return {
|
||
"id": str(announcement.id),
|
||
"title": announcement.title,
|
||
"content": announcement.content,
|
||
"summary": announcement.summary or "",
|
||
"status": announcement.status,
|
||
"priority": announcement.priority,
|
||
"is_top": announcement.is_top,
|
||
"target_type": announcement.target_type,
|
||
"target_ids": announcement.target_ids or [],
|
||
"publish_time": announcement.publish_time,
|
||
"expire_time": announcement.expire_time,
|
||
"publisher_id": str(announcement.publisher_id) if announcement.publisher_id else None,
|
||
"publisher_name": publisher_name,
|
||
"read_count": announcement.read_count or 0,
|
||
"created_at": announcement.sys_create_datetime,
|
||
}
|
||
|
||
|
||
def _build_announcement_list_out(announcement, publisher_names: Dict[str, str] = None) -> dict:
|
||
"""构建公告列表输出"""
|
||
publisher_name = ""
|
||
if publisher_names and announcement.publisher_id:
|
||
publisher_name = publisher_names.get(str(announcement.publisher_id), "")
|
||
return {
|
||
"id": str(announcement.id),
|
||
"title": announcement.title,
|
||
"summary": announcement.summary or "",
|
||
"status": announcement.status,
|
||
"priority": announcement.priority,
|
||
"is_top": announcement.is_top,
|
||
"target_type": announcement.target_type,
|
||
"publisher_name": publisher_name,
|
||
"read_count": announcement.read_count or 0,
|
||
"publish_time": announcement.publish_time,
|
||
"created_at": announcement.sys_create_datetime,
|
||
}
|
||
|
||
|
||
def _build_user_announcement_out(announcement, publisher_names: Dict[str, str] = None) -> dict:
|
||
"""构建用户公告输出"""
|
||
publisher_name = ""
|
||
if publisher_names and announcement.publisher_id:
|
||
publisher_name = publisher_names.get(str(announcement.publisher_id), "")
|
||
return {
|
||
"id": str(announcement.id),
|
||
"title": announcement.title,
|
||
"summary": announcement.summary or "",
|
||
"content": announcement.content,
|
||
"priority": announcement.priority,
|
||
"is_top": announcement.is_top,
|
||
"is_read": getattr(announcement, "is_read", False),
|
||
"publisher_name": publisher_name,
|
||
"publish_time": announcement.publish_time,
|
||
}
|
||
|
||
|
||
# ============ 邮件 API ============
|
||
|
||
@router.get("/email/status", response_model=ResponseModel, summary="邮件配置状态")
|
||
async def email_status():
|
||
"""查看 SMTP 邮件配置状态(通过 config_manager 三级获取)"""
|
||
from app.email import EmailSender
|
||
|
||
config = await EmailSender._get_smtp_config()
|
||
configured = bool(config["host"] and config["user"] and config["password"])
|
||
return ResponseModel(
|
||
message="SMTP 已配置" if configured else "SMTP 未配置",
|
||
data={
|
||
"configured": configured,
|
||
"smtp_host": config["host"] or "",
|
||
"smtp_port": config["port"],
|
||
"smtp_user": config["user"][:3] + "***" if config["user"] else "",
|
||
"smtp_use_tls": config["use_tls"],
|
||
"from_name": config["from_name"] or "",
|
||
"from_email": config["from_email"] or "",
|
||
},
|
||
)
|
||
|
||
|
||
@router.post("/email/test", response_model=ResponseModel, summary="测试邮件发送")
|
||
async def email_test(data: EmailTestInput):
|
||
"""发送测试邮件以验证 SMTP 配置"""
|
||
from app.email import EmailSender
|
||
|
||
if not await EmailSender.is_configured_async():
|
||
raise HTTPException(status_code=400, detail="SMTP 未配置,请先在系统配置中配置邮箱相关参数")
|
||
|
||
html_content = EmailSender.build_notification_html(
|
||
title="测试邮件",
|
||
content="这是一封测试邮件,用于验证 SMTP 配置是否正确。如果您收到了这封邮件,说明邮件推送功能已正常工作。",
|
||
)
|
||
success = await EmailSender.send(
|
||
to_email=data.to_email,
|
||
subject="[测试] 邮件推送配置验证",
|
||
content=html_content,
|
||
html=True,
|
||
)
|
||
|
||
if not success:
|
||
raise HTTPException(status_code=500, detail="邮件发送失败,请检查 SMTP 配置")
|
||
|
||
return ResponseModel(message="测试邮件发送成功")
|
||
|
||
|
||
@router.post("/email/send", response_model=ResponseModel, summary="发送邮件通知")
|
||
async def email_send(data: EmailSendInput, db: AsyncSession = Depends(get_db)):
|
||
"""手动向指定用户发送邮件通知"""
|
||
from core.message.service import NotifyService
|
||
|
||
result = await NotifyService._send_email(
|
||
db=db,
|
||
recipient_ids=data.recipient_ids,
|
||
title=data.title,
|
||
content=data.content,
|
||
)
|
||
|
||
return ResponseModel(
|
||
message="邮件发送成功" if result else "部分或全部邮件发送失败",
|
||
data={"success": result},
|
||
)
|
||
|
||
|
||
# ============ 钉钉通知 API ============
|
||
|
||
@router.get("/dingtalk/status", response_model=ResponseModel, summary="钉钉通知配置状态")
|
||
async def dingtalk_status():
|
||
"""查看钉钉通知配置状态(通过 config_manager 三级获取)"""
|
||
from app.dingtalk import DingTalkWebhook, DingTalkWorkNotice
|
||
from app.config_manager import config_manager
|
||
|
||
notify_config = await config_manager.get_group("notify_dingtalk")
|
||
oauth_config = await config_manager.get_group("oauth_dingtalk")
|
||
|
||
webhook_url = notify_config.get("webhook_url") or ""
|
||
webhook_secret = notify_config.get("webhook_secret") or ""
|
||
agent_id = notify_config.get("agent_id") or ""
|
||
app_id = oauth_config.get("app_id") or ""
|
||
app_secret = oauth_config.get("app_secret") or ""
|
||
|
||
return ResponseModel(
|
||
message="钉钉通知配置状态",
|
||
data={
|
||
"webhook": {
|
||
"configured": bool(webhook_url),
|
||
"url": (webhook_url[:30] + "***") if webhook_url else "",
|
||
"has_secret": bool(webhook_secret),
|
||
},
|
||
"work_notice": {
|
||
"configured": bool(app_id and app_secret and agent_id),
|
||
"agent_id": agent_id,
|
||
"app_id": (app_id[:4] + "***") if app_id else "",
|
||
},
|
||
},
|
||
)
|
||
|
||
|
||
@router.post("/dingtalk/webhook/test", response_model=ResponseModel, summary="测试钉钉 Webhook")
|
||
async def dingtalk_webhook_test(data: DingTalkWebhookTestInput):
|
||
"""发送测试消息到钉钉群机器人"""
|
||
from app.dingtalk import DingTalkWebhook
|
||
|
||
if not await DingTalkWebhook.is_configured_async():
|
||
raise HTTPException(status_code=400, detail="钉钉 Webhook 未配置")
|
||
|
||
success = await DingTalkWebhook.send_text(content=f"[测试] {data.content}")
|
||
if not success:
|
||
raise HTTPException(status_code=500, detail="钉钉 Webhook 发送失败")
|
||
|
||
return ResponseModel(message="钉钉 Webhook 测试消息发送成功")
|
||
|
||
|
||
@router.post("/dingtalk/work-notice/test", response_model=ResponseModel, summary="测试钉钉工作通知")
|
||
async def dingtalk_work_notice_test(data: DingTalkWorkNoticeTestInput, db: AsyncSession = Depends(get_db)):
|
||
"""发送测试工作通知到指定用户"""
|
||
from app.dingtalk import DingTalkWorkNotice
|
||
from core.message.service import NotifyService
|
||
|
||
if not await DingTalkWorkNotice.is_configured_async():
|
||
raise HTTPException(status_code=400, detail="钉钉工作通知未配置,需要配置钉钉 OAuth 应用及 agent_id")
|
||
|
||
# 查询用户的钉钉 unionId
|
||
from core.user.model import User
|
||
from sqlalchemy import select
|
||
stmt = select(User.dingtalk_unionid, User.name).where(User.id == data.user_id)
|
||
result = await db.execute(stmt)
|
||
row = result.first()
|
||
|
||
if not row or not row[0]:
|
||
raise HTTPException(status_code=400, detail="该用户未绑定钉钉账号")
|
||
|
||
# unionId 转 userId
|
||
user_ids = await NotifyService._convert_dingtalk_unionids([row[0]])
|
||
if not user_ids:
|
||
raise HTTPException(status_code=500, detail="钉钉 unionId 转 userId 失败")
|
||
|
||
success = await DingTalkWorkNotice.send_text(
|
||
userid_list=user_ids,
|
||
content=f"[测试] 这是一条发送给 {row[1] or '用户'} 的测试工作通知",
|
||
)
|
||
if not success:
|
||
raise HTTPException(status_code=500, detail="钉钉工作通知发送失败")
|
||
|
||
return ResponseModel(message="钉钉工作通知测试发送成功")
|
||
|
||
|
||
# ============ 钉钉待办 API ============
|
||
|
||
@router.get("/dingtalk-todo/status", response_model=ResponseModel, summary="钉钉待办配置状态")
|
||
async def dingtalk_todo_status():
|
||
"""查看钉钉待办配置状态"""
|
||
from app.dingtalk import DingTalkTodo
|
||
from app.config_manager import config_manager
|
||
|
||
configured = await DingTalkTodo.is_configured_async()
|
||
notify_config = await config_manager.get_group("notify_dingtalk")
|
||
|
||
return ResponseModel(
|
||
message="钉钉待办配置状态",
|
||
data={
|
||
"configured": configured,
|
||
"todo_pc_url": notify_config.get("todo_pc_url") or "",
|
||
"todo_app_url": notify_config.get("todo_app_url") or "",
|
||
},
|
||
)
|
||
|
||
|
||
@router.post("/dingtalk-todo/test", response_model=ResponseModel, summary="测试钉钉待办")
|
||
async def dingtalk_todo_test(data: DingTalkTodoTestInput, db: AsyncSession = Depends(get_db)):
|
||
"""创建一条测试钉钉待办"""
|
||
from app.dingtalk import DingTalkTodo
|
||
|
||
if not await DingTalkTodo.is_configured_async():
|
||
raise HTTPException(status_code=400, detail="钉钉待办未配置,需要配置钉钉 OAuth 应用(app_id/app_secret)")
|
||
|
||
from core.user.model import User
|
||
from sqlalchemy import select
|
||
stmt = select(User.dingtalk_unionid, User.name).where(User.id == data.user_id)
|
||
result = await db.execute(stmt)
|
||
row = result.first()
|
||
|
||
if not row or not row[0]:
|
||
raise HTTPException(status_code=400, detail="该用户未绑定钉钉账号")
|
||
|
||
task_id = await DingTalkTodo.create_todo(
|
||
union_id=row[0],
|
||
subject=data.title,
|
||
description=data.content,
|
||
executor_ids=[row[0]],
|
||
priority=20,
|
||
)
|
||
|
||
if not task_id:
|
||
raise HTTPException(status_code=500, detail="钉钉待办创建失败")
|
||
|
||
return ResponseModel(
|
||
message="钉钉待办测试创建成功",
|
||
data={"task_id": task_id},
|
||
)
|
||
|
||
|
||
@router.post("/dingtalk-todo/complete", response_model=ResponseModel, summary="完成钉钉待办")
|
||
async def dingtalk_todo_complete(data: DingTalkTodoCompleteInput, db: AsyncSession = Depends(get_db)):
|
||
"""完成指定业务来源的钉钉待办"""
|
||
completed = await NotifyService.complete_dingtalk_todo(
|
||
db=db,
|
||
source_type=data.source_type,
|
||
source_id=data.source_id,
|
||
)
|
||
|
||
return ResponseModel(
|
||
message=f"已完成 {completed} 条钉钉待办",
|
||
data={"completed_count": completed},
|
||
)
|
||
|
||
|
||
# ============ 飞书通知 API ============
|
||
|
||
@router.get("/feishu/status", response_model=ResponseModel, summary="飞书通知配置状态")
|
||
async def feishu_status():
|
||
"""查看飞书通知配置状态(通过 config_manager 三级获取)"""
|
||
from app.config_manager import config_manager
|
||
|
||
notify_config = await config_manager.get_group("notify_feishu")
|
||
oauth_config = await config_manager.get_group("oauth_feishu")
|
||
|
||
webhook_url = notify_config.get("webhook_url") or ""
|
||
webhook_secret = notify_config.get("webhook_secret") or ""
|
||
app_id = oauth_config.get("app_id") or ""
|
||
app_secret = oauth_config.get("app_secret") or ""
|
||
|
||
return ResponseModel(
|
||
message="飞书通知配置状态",
|
||
data={
|
||
"webhook": {
|
||
"configured": bool(webhook_url),
|
||
"url": (webhook_url[:30] + "***") if webhook_url else "",
|
||
"has_secret": bool(webhook_secret),
|
||
},
|
||
"app_message": {
|
||
"configured": bool(app_id and app_secret),
|
||
"app_id": (app_id[:4] + "***") if app_id else "",
|
||
},
|
||
},
|
||
)
|
||
|
||
|
||
@router.post("/feishu/webhook/test", response_model=ResponseModel, summary="测试飞书 Webhook")
|
||
async def feishu_webhook_test(data: FeishuWebhookTestInput):
|
||
"""发送测试消息到飞书群机器人"""
|
||
from app.feishu import FeishuWebhook
|
||
|
||
if not await FeishuWebhook.is_configured_async():
|
||
raise HTTPException(status_code=400, detail="飞书 Webhook 未配置")
|
||
|
||
success = await FeishuWebhook.send_text(text=f"[测试] {data.content}")
|
||
if not success:
|
||
raise HTTPException(status_code=500, detail="飞书 Webhook 发送失败")
|
||
|
||
return ResponseModel(message="飞书 Webhook 测试消息发送成功")
|
||
|
||
|
||
@router.post("/feishu/app-message/test", response_model=ResponseModel, summary="测试飞书应用消息")
|
||
async def feishu_app_message_test(data: FeishuAppMessageTestInput, db: AsyncSession = Depends(get_db)):
|
||
"""发送测试应用消息到指定用户"""
|
||
from app.feishu import FeishuAppMessage
|
||
|
||
if not await FeishuAppMessage.is_configured_async():
|
||
raise HTTPException(status_code=400, detail="飞书应用消息未配置,需要配置飞书 OAuth 应用")
|
||
|
||
# 查询用户的飞书 union_id
|
||
from core.user.model import User
|
||
from sqlalchemy import select
|
||
stmt = select(User.feishu_union_id, User.name).where(User.id == data.user_id)
|
||
result = await db.execute(stmt)
|
||
row = result.first()
|
||
|
||
if not row or not row[0]:
|
||
raise HTTPException(status_code=400, detail="该用户未绑定飞书账号")
|
||
|
||
# union_id 转 open_id
|
||
id_map = await FeishuAppMessage.get_open_ids_by_union_ids([row[0]])
|
||
open_id = id_map.get(row[0])
|
||
if not open_id:
|
||
raise HTTPException(status_code=500, detail="飞书 union_id 转 open_id 失败")
|
||
|
||
success = await FeishuAppMessage.send_text(
|
||
open_id=open_id,
|
||
text=f"[测试] 这是一条发送给 {row[1] or '用户'} 的测试消息",
|
||
)
|
||
if not success:
|
||
raise HTTPException(status_code=500, detail="飞书应用消息发送失败")
|
||
|
||
return ResponseModel(message="飞书应用消息测试发送成功")
|
||
|
||
|
||
# ============ 企业微信通知 API ============
|
||
|
||
@router.get("/wecom/status", response_model=ResponseModel, summary="企业微信通知配置状态")
|
||
async def wecom_status():
|
||
"""查看企业微信通知配置状态(通过 config_manager 三级获取)"""
|
||
from app.config_manager import config_manager
|
||
|
||
notify_config = await config_manager.get_group("notify_wecom")
|
||
oauth_config = await config_manager.get_group("oauth_wecom")
|
||
|
||
webhook_url = notify_config.get("webhook_url") or ""
|
||
corp_id = oauth_config.get("corp_id") or ""
|
||
agent_id = oauth_config.get("agent_id") or ""
|
||
app_secret = oauth_config.get("app_secret") or ""
|
||
|
||
return ResponseModel(
|
||
message="企业微信通知配置状态",
|
||
data={
|
||
"webhook": {
|
||
"configured": bool(webhook_url),
|
||
"url": (webhook_url[:30] + "***") if webhook_url else "",
|
||
},
|
||
"app_message": {
|
||
"configured": bool(corp_id and app_secret and agent_id),
|
||
"corp_id": (corp_id[:4] + "***") if corp_id else "",
|
||
"agent_id": agent_id,
|
||
},
|
||
},
|
||
)
|
||
|
||
|
||
@router.post("/wecom/webhook/test", response_model=ResponseModel, summary="测试企业微信 Webhook")
|
||
async def wecom_webhook_test(data: WecomWebhookTestInput):
|
||
"""发送测试消息到企业微信群机器人"""
|
||
from app.wecom import WecomWebhook
|
||
|
||
if not await WecomWebhook.is_configured_async():
|
||
raise HTTPException(status_code=400, detail="企业微信 Webhook 未配置")
|
||
|
||
success = await WecomWebhook.send_text(content=f"[测试] {data.content}")
|
||
if not success:
|
||
raise HTTPException(status_code=500, detail="企业微信 Webhook 发送失败")
|
||
|
||
return ResponseModel(message="企业微信 Webhook 测试消息发送成功")
|
||
|
||
|
||
@router.post("/wecom/app-message/test", response_model=ResponseModel, summary="测试企业微信应用消息")
|
||
async def wecom_app_message_test(data: WecomAppMessageTestInput, db: AsyncSession = Depends(get_db)):
|
||
"""发送测试应用消息到指定用户"""
|
||
from app.wecom import WecomAppMessage
|
||
|
||
if not await WecomAppMessage.is_configured_async():
|
||
raise HTTPException(status_code=400, detail="企业微信应用消息未配置,需要配置企业微信 OAuth 应用")
|
||
|
||
# 查询用户的企业微信 userid
|
||
from core.user.model import User
|
||
from sqlalchemy import select
|
||
stmt = select(User.wecom_userid, User.name).where(User.id == data.user_id)
|
||
result = await db.execute(stmt)
|
||
row = result.first()
|
||
|
||
if not row or not row[0]:
|
||
raise HTTPException(status_code=400, detail="该用户未绑定企业微信账号")
|
||
|
||
success = await WecomAppMessage.send_text(
|
||
userid_list=[row[0]],
|
||
content=f"[测试] 这是一条发送给 {row[1] or '用户'} 的测试消息",
|
||
)
|
||
if not success:
|
||
raise HTTPException(status_code=500, detail="企业微信应用消息发送失败")
|
||
|
||
return ResponseModel(message="企业微信应用消息测试发送成功")
|
||
|
||
|
||
# ============ 微信公众号模板消息 API ============
|
||
|
||
@router.get("/wechat-mp/status", response_model=ResponseModel, summary="微信公众号通知配置状态")
|
||
async def wechat_mp_status():
|
||
"""查看微信公众号模板消息配置状态(通过 config_manager 三级获取)"""
|
||
from app.config_manager import config_manager
|
||
|
||
oauth_config = await config_manager.get_group("oauth_wechat")
|
||
mp_config = await config_manager.get_group("notify_wechat_mp")
|
||
|
||
app_id = oauth_config.get("app_id") or ""
|
||
app_secret = oauth_config.get("app_secret") or ""
|
||
template_id = mp_config.get("template_id") or ""
|
||
mp_url = mp_config.get("url") or ""
|
||
mini_appid = mp_config.get("mini_appid") or ""
|
||
|
||
return ResponseModel(
|
||
message="微信公众号通知配置状态",
|
||
data={
|
||
"configured": bool(app_id and app_secret and template_id),
|
||
"app_id": (app_id[:4] + "***") if app_id else "",
|
||
"template_id": (template_id[:8] + "***") if template_id else "",
|
||
"has_url": bool(mp_url),
|
||
"has_miniprogram": bool(mini_appid),
|
||
},
|
||
)
|
||
|
||
|
||
@router.post("/wechat-mp/test", response_model=ResponseModel, summary="测试微信公众号模板消息")
|
||
async def wechat_mp_test(data: WechatMPTestInput, db: AsyncSession = Depends(get_db)):
|
||
"""发送测试模板消息到指定用户"""
|
||
from app.wechat import WechatMPMessage
|
||
|
||
if not await WechatMPMessage.is_configured_async():
|
||
raise HTTPException(status_code=400, detail="微信公众号模板消息未配置,需要配置微信 OAuth 应用及模板 ID")
|
||
|
||
# 查询用户的微信 openid
|
||
from core.user.model import User
|
||
from sqlalchemy import select
|
||
stmt = select(User.wechat_openid, User.name).where(User.id == data.user_id)
|
||
result = await db.execute(stmt)
|
||
row = result.first()
|
||
|
||
if not row or not row[0]:
|
||
raise HTTPException(status_code=400, detail="该用户未绑定微信账号")
|
||
|
||
success = await WechatMPMessage.send_notification(
|
||
openid=row[0],
|
||
title="测试通知",
|
||
content=f"这是一条发送给 {row[1] or '用户'} 的测试模板消息",
|
||
remark="如果您收到此消息,说明微信公众号通知配置正确",
|
||
)
|
||
if not success:
|
||
raise HTTPException(status_code=500, detail="微信模板消息发送失败")
|
||
|
||
return ResponseModel(message="微信公众号模板消息测试发送成功")
|