Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
消息中心模块
|
||||
"""
|
||||
@@ -0,0 +1,960 @@
|
||||
#!/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="微信公众号模板消息测试发送成功")
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
消息模型
|
||||
"""
|
||||
from sqlalchemy import Column, String, Text, Integer, Boolean, DateTime, Index, JSON
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class Message(BaseModel):
|
||||
"""站内消息"""
|
||||
__tablename__ = "core_message"
|
||||
|
||||
# 接收人(逻辑外键)
|
||||
recipient_id = Column(String(50), nullable=False, index=True, comment="接收人ID")
|
||||
|
||||
# 消息内容
|
||||
title = Column(String(200), nullable=False, comment="消息标题")
|
||||
content = Column(Text, nullable=False, comment="消息内容")
|
||||
msg_type = Column(String(20), default="system", index=True, comment="消息类型: system/workflow/todo/announcement")
|
||||
status = Column(String(20), default="unread", index=True, comment="消息状态: unread/read")
|
||||
|
||||
# 关联信息(可选,用于点击跳转)
|
||||
link_type = Column(String(50), default="", comment="关联类型")
|
||||
link_id = Column(String(50), default="", comment="关联对象ID")
|
||||
|
||||
# 阅读时间
|
||||
read_at = Column(DateTime, nullable=True, comment="阅读时间")
|
||||
|
||||
# 发送人(逻辑外键,可选)
|
||||
sender_id = Column(String(50), nullable=True, comment="发送人ID")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_message_recipient_status", "recipient_id", "status"),
|
||||
Index("ix_message_recipient_type", "recipient_id", "msg_type"),
|
||||
)
|
||||
|
||||
|
||||
class Announcement(BaseModel):
|
||||
"""公告"""
|
||||
__tablename__ = "core_announcement"
|
||||
|
||||
# 公告内容
|
||||
title = Column(String(200), nullable=False, comment="公告标题")
|
||||
content = Column(Text, nullable=False, comment="公告内容")
|
||||
summary = Column(String(500), default="", comment="摘要")
|
||||
|
||||
# 状态与优先级
|
||||
status = Column(String(20), default="draft", index=True, comment="状态: draft/published/expired")
|
||||
priority = Column(Integer, default=0, index=True, comment="优先级: 0普通/1重要/2紧急")
|
||||
is_top = Column(Boolean, default=False, comment="是否置顶")
|
||||
|
||||
# 接收范围
|
||||
target_type = Column(String(20), default="all", comment="接收范围类型: all/dept/role/user")
|
||||
target_ids = Column(JSON, default=list, comment="接收目标ID列表")
|
||||
|
||||
# 发布时间
|
||||
publish_time = Column(DateTime, nullable=True, comment="发布时间")
|
||||
expire_time = Column(DateTime, nullable=True, comment="过期时间")
|
||||
|
||||
# 发布人(逻辑外键)
|
||||
publisher_id = Column(String(50), nullable=True, comment="发布人ID")
|
||||
|
||||
# 统计
|
||||
read_count = Column(Integer, default=0, comment="阅读次数")
|
||||
|
||||
|
||||
class DingtalkTodoRecord(BaseModel):
|
||||
"""钉钉待办记录(业务ID与钉钉待办ID映射)"""
|
||||
__tablename__ = "core_dingtalk_todo_record"
|
||||
|
||||
source_type = Column(String(50), nullable=False, index=True, comment="来源类型: workflow_task/workflow_instance/message")
|
||||
source_id = Column(String(50), nullable=False, index=True, comment="业务ID")
|
||||
user_id = Column(String(50), nullable=False, index=True, comment="系统用户ID")
|
||||
dingtalk_union_id = Column(String(100), nullable=False, comment="钉钉unionId")
|
||||
dingtalk_task_id = Column(String(100), nullable=False, comment="钉钉待办taskId")
|
||||
status = Column(String(20), default="pending", index=True, comment="状态: pending/done/deleted")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_dingtalk_todo_source", "source_type", "source_id"),
|
||||
)
|
||||
|
||||
|
||||
class AnnouncementRead(BaseModel):
|
||||
"""公告阅读记录"""
|
||||
__tablename__ = "core_announcement_read"
|
||||
|
||||
# 公告(逻辑外键)
|
||||
announcement_id = Column(String(50), nullable=False, index=True, comment="公告ID")
|
||||
|
||||
# 用户(逻辑外键)
|
||||
user_id = Column(String(50), nullable=False, index=True, comment="用户ID")
|
||||
|
||||
# 阅读时间
|
||||
read_at = Column(DateTime, nullable=True, comment="阅读时间")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_announcement_read_unique", "announcement_id", "user_id", unique=True),
|
||||
)
|
||||
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
消息中心 Schema
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
from app.base_schema import CSTDatetime
|
||||
|
||||
|
||||
# ============ 消息相关 Schema ============
|
||||
class MessageOut(BaseModel):
|
||||
"""消息输出"""
|
||||
id: str
|
||||
title: str
|
||||
content: str
|
||||
msg_type: str
|
||||
status: str
|
||||
link_type: str = ""
|
||||
link_id: str = ""
|
||||
sender_name: str = ""
|
||||
read_at: Optional[CSTDatetime] = None
|
||||
created_at: Optional[CSTDatetime] = Field(None, alias="sys_create_datetime")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
|
||||
|
||||
class MessageListOut(BaseModel):
|
||||
"""消息列表输出"""
|
||||
id: str
|
||||
title: str
|
||||
content: str
|
||||
msg_type: str
|
||||
status: str
|
||||
link_type: str = ""
|
||||
link_id: str = ""
|
||||
sender_id: Optional[str] = None
|
||||
sender_name: str = ""
|
||||
created_at: Optional[CSTDatetime] = Field(None, alias="sys_create_datetime")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
|
||||
|
||||
class UnreadCountOut(BaseModel):
|
||||
"""未读数量输出"""
|
||||
total: int
|
||||
by_type: Dict[str, int]
|
||||
|
||||
|
||||
class MarkReadInput(BaseModel):
|
||||
"""标记已读输入"""
|
||||
msg_type: Optional[str] = Field(None, description="消息类型,不传则标记全部")
|
||||
|
||||
|
||||
# ============ 公告相关 Schema ============
|
||||
class AnnouncementCreate(BaseModel):
|
||||
"""创建公告"""
|
||||
title: str = Field(..., max_length=200, description="公告标题")
|
||||
content: str = Field(..., description="公告内容")
|
||||
summary: str = Field(default="", max_length=500, description="摘要")
|
||||
status: str = Field(default="draft", description="状态: draft/published")
|
||||
priority: int = Field(default=0, description="优先级: 0普通/1重要/2紧急")
|
||||
is_top: bool = Field(default=False, description="是否置顶")
|
||||
target_type: str = Field(default="all", description="接收范围: all/dept/role/user")
|
||||
target_ids: List[str] = Field(default=[], description="接收目标ID列表")
|
||||
publish_time: Optional[datetime] = Field(None, description="发布时间")
|
||||
expire_time: Optional[datetime] = Field(None, description="过期时间")
|
||||
|
||||
|
||||
class AnnouncementUpdate(BaseModel):
|
||||
"""更新公告"""
|
||||
title: Optional[str] = Field(None, max_length=200, description="公告标题")
|
||||
content: Optional[str] = Field(None, description="公告内容")
|
||||
summary: Optional[str] = Field(None, max_length=500, description="摘要")
|
||||
status: Optional[str] = Field(None, description="状态")
|
||||
priority: Optional[int] = Field(None, description="优先级")
|
||||
is_top: Optional[bool] = Field(None, description="是否置顶")
|
||||
target_type: Optional[str] = Field(None, description="接收范围")
|
||||
target_ids: Optional[List[str]] = Field(None, description="接收目标ID列表")
|
||||
publish_time: Optional[datetime] = Field(None, description="发布时间")
|
||||
expire_time: Optional[datetime] = Field(None, description="过期时间")
|
||||
|
||||
|
||||
class AnnouncementOut(BaseModel):
|
||||
"""公告输出"""
|
||||
id: str
|
||||
title: str
|
||||
content: str
|
||||
summary: str = ""
|
||||
status: str
|
||||
priority: int
|
||||
is_top: bool
|
||||
target_type: str
|
||||
target_ids: List[str] = []
|
||||
publish_time: Optional[CSTDatetime] = None
|
||||
expire_time: Optional[CSTDatetime] = None
|
||||
publisher_id: Optional[str] = None
|
||||
publisher_name: str = ""
|
||||
read_count: int = 0
|
||||
created_at: Optional[CSTDatetime] = Field(None, alias="sys_create_datetime")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
|
||||
|
||||
class AnnouncementListOut(BaseModel):
|
||||
"""公告列表输出"""
|
||||
id: str
|
||||
title: str
|
||||
summary: str = ""
|
||||
status: str
|
||||
priority: int
|
||||
is_top: bool
|
||||
target_type: str
|
||||
publisher_name: str = ""
|
||||
read_count: int = 0
|
||||
publish_time: Optional[CSTDatetime] = None
|
||||
created_at: Optional[CSTDatetime] = Field(None, alias="sys_create_datetime")
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
|
||||
|
||||
class UserAnnouncementOut(BaseModel):
|
||||
"""用户公告输出"""
|
||||
id: str
|
||||
title: str
|
||||
summary: str = ""
|
||||
content: str
|
||||
priority: int
|
||||
is_top: bool
|
||||
is_read: bool = False
|
||||
publisher_name: str = ""
|
||||
publish_time: Optional[CSTDatetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
|
||||
|
||||
class ReadStatsOut(BaseModel):
|
||||
"""阅读统计输出"""
|
||||
total_read: int
|
||||
readers: List[dict]
|
||||
|
||||
|
||||
# ============ 发送消息 Schema ============
|
||||
class SendMessageInput(BaseModel):
|
||||
"""发送站内消息"""
|
||||
recipient_ids: List[str] = Field(..., description="接收人用户ID列表")
|
||||
title: str = Field(..., max_length=200, description="消息标题")
|
||||
content: str = Field(..., description="消息内容")
|
||||
msg_type: str = Field(default="system", description="消息类型: system/workflow/todo")
|
||||
channels: List[str] = Field(default=["site"], description="发送渠道: site/email/dingtalk/dingtalk_todo/feishu/wechat/wechat_mp/sms/chat")
|
||||
|
||||
|
||||
# ============ 邮件相关 Schema ============
|
||||
class EmailTestInput(BaseModel):
|
||||
"""测试邮件输入"""
|
||||
to_email: str = Field(..., description="收件人邮箱")
|
||||
|
||||
|
||||
class EmailSendInput(BaseModel):
|
||||
"""发送邮件输入"""
|
||||
recipient_ids: List[str] = Field(..., description="收件人用户ID列表")
|
||||
title: str = Field(..., max_length=200, description="邮件主题")
|
||||
content: str = Field(..., description="邮件内容")
|
||||
|
||||
|
||||
# ============ 钉钉相关 Schema ============
|
||||
class DingTalkWebhookTestInput(BaseModel):
|
||||
"""钉钉 Webhook 测试输入"""
|
||||
content: str = Field(default="这是一条测试消息", description="消息内容")
|
||||
|
||||
|
||||
class DingTalkWorkNoticeTestInput(BaseModel):
|
||||
"""钉钉工作通知测试输入"""
|
||||
user_id: str = Field(..., description="系统用户ID(需已绑定钉钉)")
|
||||
|
||||
|
||||
# ============ 钉钉待办相关 Schema ============
|
||||
class DingTalkTodoTestInput(BaseModel):
|
||||
"""钉钉待办测试输入"""
|
||||
user_id: str = Field(..., description="系统用户ID(需已绑定钉钉)")
|
||||
title: str = Field(default="测试待办任务", description="待办标题")
|
||||
content: str = Field(default="这是一条测试钉钉待办,用于验证钉钉待办推送功能。", description="待办内容")
|
||||
|
||||
|
||||
class DingTalkTodoCompleteInput(BaseModel):
|
||||
"""钉钉待办完成输入"""
|
||||
source_type: str = Field(..., description="来源类型: workflow_task/workflow_instance/message")
|
||||
source_id: str = Field(..., description="业务ID")
|
||||
|
||||
|
||||
# ============ 飞书相关 Schema ============
|
||||
class FeishuWebhookTestInput(BaseModel):
|
||||
"""飞书 Webhook 测试输入"""
|
||||
content: str = Field(default="这是一条测试消息", description="消息内容")
|
||||
|
||||
|
||||
class FeishuAppMessageTestInput(BaseModel):
|
||||
"""飞书应用消息测试输入"""
|
||||
user_id: str = Field(..., description="系统用户ID(需已绑定飞书)")
|
||||
|
||||
|
||||
# ============ 企业微信相关 Schema ============
|
||||
class WecomWebhookTestInput(BaseModel):
|
||||
"""企业微信 Webhook 测试输入"""
|
||||
content: str = Field(default="这是一条测试消息", description="消息内容")
|
||||
|
||||
|
||||
class WecomAppMessageTestInput(BaseModel):
|
||||
"""企业微信应用消息测试输入"""
|
||||
user_id: str = Field(..., description="系统用户ID(需已绑定企业微信)")
|
||||
|
||||
|
||||
# ============ 微信公众号相关 Schema ============
|
||||
class WechatMPTestInput(BaseModel):
|
||||
"""微信公众号模板消息测试输入"""
|
||||
user_id: str = Field(..., description="系统用户ID(需已绑定微信)")
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user