diff --git a/app/services/notification_service.py b/app/services/notification_service.py
new file mode 100644
index 0000000..850df57
--- /dev/null
+++ b/app/services/notification_service.py
@@ -0,0 +1,46 @@
+from datetime import datetime
+from typing import Any, Dict, List
+from app.config import settings
+from app.wechat.client import WeChatClient
+
+
+class NotificationService:
+ def __init__(self):
+ self.client = WeChatClient()
+
+ async def send(self, announcements: List[Dict[str, Any]]) -> int:
+ if not settings.wechat_enabled:
+ return 0
+ if not announcements:
+ return 0
+
+ sent = 0
+ for ann in announcements:
+ try:
+ title = ann.get("title", "")
+ if len(title) > 128:
+ title = title[:125] + "..."
+
+ purchase_name = ann.get("purchase_name", "")
+ if len(purchase_name) > 25:
+ purchase_name = purchase_name[:22] + "..."
+
+ pub_date = ann.get("publish_date")
+ time_str = pub_date.strftime("%Y-%m-%d %H:%M") if pub_date else "时间未知"
+
+ source_name = ann.get("source_name", "")
+
+ description = (
+ f'
'
+ f'{source_name} | {purchase_name} | {time_str}'
+ f'
'
+ )
+
+ url = ann.get("content_url", "")
+
+ if await self.client.send_textcard(title, description, url):
+ sent += 1
+ except Exception:
+ continue
+
+ return sent
diff --git a/tests/test_services/test_notification_service.py b/tests/test_services/test_notification_service.py
new file mode 100644
index 0000000..2b19452
--- /dev/null
+++ b/tests/test_services/test_notification_service.py
@@ -0,0 +1,40 @@
+import pytest
+from unittest.mock import AsyncMock, patch
+from app.services.notification_service import NotificationService
+
+
+@pytest.mark.asyncio
+async def test_notification_send():
+ svc = NotificationService()
+ announcements = [
+ {
+ "title": "测试公告",
+ "publish_date": None,
+ "purchase_name": "测试单位",
+ "content_url": "https://x.com/1",
+ "source_code": "test",
+ "source_name": "测试来源",
+ "announcement_type": "purchase",
+ }
+ ]
+
+ with patch.object(svc.client, "send_textcard", AsyncMock(return_value=True)):
+ count = await svc.send(announcements)
+ assert count == 1
+
+
+@pytest.mark.asyncio
+async def test_notification_send_empty():
+ svc = NotificationService()
+ count = await svc.send([])
+ assert count == 0
+
+
+@pytest.mark.asyncio
+async def test_notification_send_disabled():
+ svc = NotificationService()
+ announcements = [{"title": "test"}]
+
+ with patch.object(svc.client, "send_textcard", AsyncMock(return_value=False)):
+ count = await svc.send(announcements)
+ assert count == 0