From a1591aa1df96233587b4d5ee3ecfa5aacf35bb2a Mon Sep 17 00:00:00 2001 From: ws1065 Date: Fri, 8 May 2026 11:23:23 +0800 Subject: [PATCH] =?UTF-8?q?fix(bible):=20=E4=BF=AE=E5=A4=8D=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E5=85=B3=E7=B3=BB=E6=8C=87=E6=95=B0=E5=A2=9E=E9=95=BF?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E6=95=B0=E6=8D=AE=E5=BA=93=E7=88=86=E7=82=B8?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bible_service.add_character() 原先每次调用都会全量加载 Bible、删除所有 子表数据后重新写入,多次调用会导致关系数据按 2^n 倍增。实测 bible_character_relationships 表膨胀至 406 万行(1.4GB),其中每条 关系被复制 131,072 次。 修复方案: - 新增 SqliteBibleRepository.add_character_incremental() 增量写入方法 - BibleService.add_character() 改为增量 INSERT,不再 DELETE + 全量重写 - 更新单元测试适配新逻辑 Co-Authored-By: Claude Opus 4.7 --- application/world/services/bible_service.py | 46 +++++++++++++++---- .../database/sqlite_bible_repository.py | 45 ++++++++++++++++++ .../services/test_bible_service.py | 14 ++++-- 3 files changed, 90 insertions(+), 15 deletions(-) diff --git a/application/world/services/bible_service.py b/application/world/services/bible_service.py index 8970beebb..72258bead 100644 --- a/application/world/services/bible_service.py +++ b/application/world/services/bible_service.py @@ -13,7 +13,7 @@ from domain.bible.repositories.bible_repository import BibleRepository from domain.novel.repositories.novel_repository import NovelRepository from domain.novel.repositories.chapter_repository import ChapterRepository -from domain.shared.exceptions import EntityNotFoundError +from domain.shared.exceptions import EntityNotFoundError, InvalidOperationError from application.world.dtos.bible_dto import BibleDTO, CharacterDTO if TYPE_CHECKING: @@ -105,7 +105,7 @@ def add_character( description: str, relationships: list = None ) -> BibleDTO: - """添加人物 + """添加人物(增量写入,不触碰已有角色和关系) Args: novel_id: 小说 ID @@ -124,15 +124,41 @@ def add_character( if bible is None: raise EntityNotFoundError("Bible", f"for novel {novel_id}") - character = Character( - id=CharacterId(character_id), - name=name, - description=description, - relationships=relationships or [], - ) - bible.add_character(character) - self.bible_repository.save(bible) + # 检查角色是否已存在 + if bible.get_character(CharacterId(character_id)) is not None: + raise InvalidOperationError( + f"Character with id '{character_id}' already exists" + ) + # 增量写入:只 INSERT 新角色及其关系,不 DELETE 其他数据 + repo = self.bible_repository + if hasattr(repo, "add_character_incremental"): + rel_dicts = [] + for rel in (relationships or []): + if isinstance(rel, dict): + rel_dicts.append(rel) + elif hasattr(rel, "model_dump"): + rel_dicts.append(rel.model_dump()) + elif hasattr(rel, "dict"): + rel_dicts.append(rel.dict()) + else: + rel_dicts.append({"target": str(rel), "relation": "", "description": ""}) + repo.add_character_incremental( + novel_id, character_id, name, description, rel_dicts, + ) + else: + # fallback:非 SQLite 仓储走原路径 + character = Character( + id=CharacterId(character_id), + name=name, + description=description, + relationships=relationships or [], + ) + bible.add_character(character) + repo.save(bible) + + # 重新加载最新状态返回 + bible = self.bible_repository.get_by_novel_id(NovelId(novel_id)) return BibleDTO.from_domain(bible) def update_character_voice_anchors( diff --git a/infrastructure/persistence/database/sqlite_bible_repository.py b/infrastructure/persistence/database/sqlite_bible_repository.py index 52fffb590..b2c4cdb79 100644 --- a/infrastructure/persistence/database/sqlite_bible_repository.py +++ b/infrastructure/persistence/database/sqlite_bible_repository.py @@ -315,6 +315,51 @@ def exists(self, bible_id: str) -> bool: r = self.db.fetch_one("SELECT 1 AS o FROM bibles WHERE id = ?", (bible_id,)) return r is not None + def add_character_incremental( + self, + novel_id: str, + character_id: str, + name: str, + description: str, + relationships: List[Dict[str, str]], + *, + mental_state: str = "NORMAL", + verbal_tic: str = "", + idle_behavior: str = "", + ) -> None: + """增量添加单个角色及其关系(不触碰其他角色)。""" + now = self._now() + conn = self._conn() + try: + conn.execute( + """ + INSERT OR REPLACE INTO bible_characters ( + id, novel_id, name, description, + mental_state, mental_state_reason, verbal_tic, idle_behavior, + created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, '', ?, ?, ?, ?) + """, + (character_id, novel_id, name, description, + mental_state, verbal_tic, idle_behavior, now, now), + ) + for i, rel in enumerate(relationships): + rid = f"{character_id}-rel-{i}-{uuid.uuid4().hex[:6]}" + target_name = rel.get("target", "") or "" + relation = rel.get("relation", "") or "" + desc = rel.get("description", "") or "" + conn.execute( + """ + INSERT INTO bible_character_relationships + (id, character_id, target_name, relation, description) + VALUES (?, ?, ?, ?, ?) + """, + (rid, character_id, target_name, relation, desc), + ) + conn.commit() + except Exception: + conn.rollback() + raise + def update_character_anchors( self, novel_id: str, diff --git a/tests/unit/application/services/test_bible_service.py b/tests/unit/application/services/test_bible_service.py index 9813335aa..dac071cda 100644 --- a/tests/unit/application/services/test_bible_service.py +++ b/tests/unit/application/services/test_bible_service.py @@ -40,9 +40,13 @@ def test_create_bible(self, service, mock_repository): def test_add_character(self, service, mock_repository): """测试添加人物""" - # 准备 mock 数据 - bible = Bible(id="bible-1", novel_id=NovelId("novel-1")) - mock_repository.get_by_novel_id.return_value = bible + # 准备 mock 数据:第一次返回空 Bible,第二次返回含角色的 Bible(模拟增量写入后) + bible_empty = Bible(id="bible-1", novel_id=NovelId("novel-1")) + bible_with_char = Bible(id="bible-1", novel_id=NovelId("novel-1")) + bible_with_char.add_character( + Character(id=CharacterId("char-1"), name="主角", description="主角描述") + ) + mock_repository.get_by_novel_id.side_effect = [bible_empty, bible_with_char] bible_dto = service.add_character( novel_id="novel-1", @@ -56,8 +60,8 @@ def test_add_character(self, service, mock_repository): assert bible_dto.characters[0].id == "char-1" assert bible_dto.characters[0].name == "主角" - # 验证调用了 save - mock_repository.save.assert_called_once() + # 验证调用了增量写入 + mock_repository.add_character_incremental.assert_called_once() def test_add_character_bible_not_found(self, service, mock_repository): """测试向不存在的 Bible 添加人物"""