Files
2026-06-08 18:14:59 +08:00

961 lines
35 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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="微信公众号模板消息测试发送成功")