From 297cd04fbc45459f3ea13b09664e1c12d756ae35 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Thu, 23 Jul 2026 14:12:41 +0800 Subject: [PATCH] fix: deduplicate SunnyPT site messages --- app/chain/site.py | 50 ++++++++++++++++++--------- app/db/message_oper.py | 9 +++++ app/db/models/message.py | 12 +++++++ app/modules/indexer/parser/sunnypt.py | 6 +++- tests/test_sunnypt_indexer.py | 41 ++++++++++++++++++++-- 5 files changed, 98 insertions(+), 20 deletions(-) diff --git a/app/chain/site.py b/app/chain/site.py index 032e22b14..8211c2abf 100644 --- a/app/chain/site.py +++ b/app/chain/site.py @@ -78,23 +78,7 @@ class SiteChain(ChainBase): eventmanager.send_event(EventType.SiteRefreshed, { "site_id": site.get("id") }) - # 发送站点消息 - if userdata.message_unread: - if userdata.message_unread_contents and len(userdata.message_unread_contents) > 0: - for head, date, content in userdata.message_unread_contents: - msg_title = f"【站点 {site.get('name')} 消息】" - msg_text = f"时间:{date}\n标题:{head}\n内容:\n{content}" - self.post_message(Notification( - mtype=NotificationType.SiteMessage, - title=msg_title, text=msg_text, link=site.get("url") - )) - else: - self.post_message(Notification( - mtype=NotificationType.SiteMessage, - title=f"站点 {site.get('name')} 收到 " - f"{userdata.message_unread} 条新消息,请登陆查看", - link=site.get("url") - )) + self._post_site_messages(site=site, userdata=userdata) # 低分享率警告 if userdata.ratio and float(userdata.ratio) < 1 and not bool( re.search(r"(贵宾|VIP?)", userdata.user_level or "", re.IGNORECASE)): @@ -105,6 +89,38 @@ class SiteChain(ChainBase): )) return userdata + def _post_site_messages(self, site: dict, userdata: SiteUserData) -> None: + """ + 发送站点未读消息,并按解析器提供的来源标识做持久化去重。 + + :param site: 站点索引配置 + :param userdata: 本次刷新的站点用户数据 + """ + if not userdata.message_unread: + return + if not userdata.message_unread_contents: + self.post_message(Notification( + mtype=NotificationType.SiteMessage, + title=f"站点 {site.get('name')} 收到 " + f"{userdata.message_unread} 条新消息,请登陆查看", + link=site.get("url") + )) + return + for message in userdata.message_unread_contents: + head, date, content, *metadata = message + message_source = metadata[0] if metadata else None + if message_source and self.messageoper.exists_by_source(message_source): + continue + msg_title = f"【站点 {site.get('name')} 消息】" + msg_text = f"时间:{date}\n标题:{head}\n内容:\n{content}" + self.post_message(Notification( + source=message_source, + mtype=NotificationType.SiteMessage, + title=msg_title, + text=msg_text, + link=site.get("url") + )) + def refresh_userdatas( self, progress_callback: Optional[Callable[..., None]] = None, diff --git a/app/db/message_oper.py b/app/db/message_oper.py index d0a8065bc..3e63a698b 100644 --- a/app/db/message_oper.py +++ b/app/db/message_oper.py @@ -105,6 +105,15 @@ class MessageOper(DbOper): """ return Message.list_by_page(self._db, page, count) + def exists_by_source(self, source: str) -> bool: + """ + 判断指定来源标识的消息记录是否存在。 + + :param source: 消息来源唯一标识 + :return: 是否存在匹配记录 + """ + return Message.exists_by_source(self._db, source) + async def async_list_by_page( self, page: Optional[int] = 1, count: Optional[int] = 30 ) -> list[Message]: diff --git a/app/db/models/message.py b/app/db/models/message.py index 1384b5452..ca342557a 100644 --- a/app/db/models/message.py +++ b/app/db/models/message.py @@ -62,6 +62,18 @@ class Message(Base): .all() ) + @classmethod + @db_query + def exists_by_source(cls, db: Session, source: str) -> bool: + """ + 判断指定来源标识的消息记录是否存在。 + + :param db: 数据库会话 + :param source: 消息来源唯一标识 + :return: 是否存在匹配记录 + """ + return db.query(cls.id).filter(cls.source == source).first() is not None + @classmethod @async_db_query async def async_list_by_page( diff --git a/app/modules/indexer/parser/sunnypt.py b/app/modules/indexer/parser/sunnypt.py index 35fc3c2bb..7c7418a70 100644 --- a/app/modules/indexer/parser/sunnypt.py +++ b/app/modules/indexer/parser/sunnypt.py @@ -123,8 +123,12 @@ class SunnyPTSiteUserInfo(SiteParserBase): title = message.get("title") content = message.get("content") created_at = StringUtils.unify_datetime_str(message.get("created_at")) + message_id = message.get("id") if title and content and created_at: - self.message_unread_contents.append((title, created_at, content)) + message_source = f"sunnypt-message:{message_id}" if message_id is not None else None + self.message_unread_contents.append( + (title, created_at, content, message_source) + ) return "next" if messages_data.get("has_more") else None def _parse_user_traffic_info(self, html_text: str) -> None: diff --git a/tests/test_sunnypt_indexer.py b/tests/test_sunnypt_indexer.py index 64c7a5041..1aac3adc1 100644 --- a/tests/test_sunnypt_indexer.py +++ b/tests/test_sunnypt_indexer.py @@ -9,10 +9,11 @@ from app.chain.download import DownloadChain from app.chain.site import SiteChain from app.core.config import settings from app.core.context import TorrentInfo +from app.db.message_oper import MessageOper from app.modules.indexer import IndexerModule from app.modules.indexer.parser.sunnypt import SunnyPTSiteUserInfo from app.modules.indexer.spider.sunnypt import SunnyPTSpider -from app.schemas import MediaType +from app.schemas import MediaType, NotificationType class _FakeResponse: @@ -286,11 +287,47 @@ def test_sunnypt_user_parser_reads_profile_and_messages_without_marking_read(mon assert parser.leeching_size == 10737418240 assert parser.message_unread == 1 assert parser.message_unread_contents == [ - ("种子审核通过", "2026-07-21 16:30:00", "你发布的种子已审核通过。") + ( + "种子审核通过", + "2026-07-21 16:30:00", + "你发布的种子已审核通过。", + "sunnypt-message:9001", + ) ] assert not any(url.endswith("/read") or url.endswith("/read-all") for url in requested_urls) +def test_site_messages_are_deduplicated_by_persisted_source(monkeypatch): + """站点消息应使用解析器保留的消息 ID 来源标识做持久化去重。""" + duplicate_source = "sunnypt-message:9001-dedup-test" + MessageOper().add( + source=duplicate_source, + mtype=NotificationType.SiteMessage, + title="existing", + text="existing", + ) + sent_messages = [] + chain = object.__new__(SiteChain) + chain.messageoper = MessageOper() + monkeypatch.setattr(chain, "post_message", sent_messages.append) + userdata = SimpleNamespace( + message_unread=2, + message_unread_contents=[ + ("重复消息", "2026-07-21 16:30:00", "旧内容", duplicate_source), + ("新消息", "2026-07-22 16:30:00", "新内容", "sunnypt-message:9002-dedup-test"), + ], + ) + + chain._post_site_messages( + site={"name": "Sunny", "url": "https://sunnypt.top/"}, + userdata=userdata, + ) + + assert len(sent_messages) == 1 + assert sent_messages[0].source == "sunnypt-message:9002-dedup-test" + assert sent_messages[0].title == "【站点 Sunny 消息】" + + def test_indexer_module_dispatches_sunnypt_search(monkeypatch): """IndexerModule 应把 SunnyPT 同步搜索参数交给专用 API Spider。""" captured = {}