From cbc0f5198acf43f566e262af49d2028ff6b3e672 Mon Sep 17 00:00:00 2001 From: "Mr.Xia" <1424473282@qq.com> Date: Sat, 20 Jun 2026 19:00:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(onmyoji=5Fgacha):=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E7=AD=BE=E5=88=B0=E5=8F=91=E6=94=BE=E7=A7=AF?= =?UTF-8?q?=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- danding_bot/plugins/onmyoji_gacha/__init__.py | 38 +++++----- danding_bot/plugins/onmyoji_gacha/sign_in.py | 40 ++++++++++ tests/test_onmyoji_gacha_http_api.py | 73 +++++++++++++++++++ 3 files changed, 133 insertions(+), 18 deletions(-) create mode 100644 danding_bot/plugins/onmyoji_gacha/sign_in.py diff --git a/danding_bot/plugins/onmyoji_gacha/__init__.py b/danding_bot/plugins/onmyoji_gacha/__init__.py index f57b5bb..5eb627d 100644 --- a/danding_bot/plugins/onmyoji_gacha/__init__.py +++ b/danding_bot/plugins/onmyoji_gacha/__init__.py @@ -8,12 +8,13 @@ from nonebot.typing import T_State from nonebot.rule import Rule from pathlib import Path -from .config import Config -from .gacha import GachaSystem -from .utils import format_sign_in_message, format_user_mention, get_image_path -from .api_utils import process_ssr_sp_reward, process_achievement_reward -from . import web_api -from danding_bot.plugins.danding_points import points_api +from .config import Config +from .gacha import GachaSystem +from .utils import format_sign_in_message, format_user_mention, get_image_path +from .sign_in import award_daily_sign_in_points +from .api_utils import process_ssr_sp_reward, process_achievement_reward +from . import web_api +from danding_bot.plugins.danding_points import points_api # 创建Config实例 config = Config() @@ -66,18 +67,19 @@ async def try_handle_daily_sign_in(matcher, user_id: str, user_name: str) -> Non """处理抽卡成功后的每日签到,不影响主流程""" try: points = random.randint(SIGN_IN_MIN_POINTS, SIGN_IN_MAX_POINTS) - success, new_balance = await points_api.add_points( - user_id, - points, - SIGN_IN_SOURCE, - SIGN_IN_REASON, - ) - if not success: - logger.error("抽卡签到积分发放失败 user_id=%s points=%s", user_id, points) - return - - if not await gacha_system.data_manager.record_sign_in(user_id, points): - logger.warning("抽卡签到落库冲突,积分已发放但签到记录重复 user_id=%s", user_id) + success, new_balance, status = await award_daily_sign_in_points( + data_manager=gacha_system.data_manager, + points_api=points_api, + user_id=user_id, + points=points, + source=SIGN_IN_SOURCE, + reason=SIGN_IN_REASON, + ) + if not success: + if status == "points_failed": + logger.error("抽卡签到记录成功但积分发放失败 user_id=%s points=%s", user_id, points) + else: + logger.info("抽卡签到未发放积分 user_id=%s status=%s", user_id, status) return await matcher.send(format_sign_in_message(user_id, user_name, points, new_balance)) diff --git a/danding_bot/plugins/onmyoji_gacha/sign_in.py b/danding_bot/plugins/onmyoji_gacha/sign_in.py new file mode 100644 index 0000000..45f01a9 --- /dev/null +++ b/danding_bot/plugins/onmyoji_gacha/sign_in.py @@ -0,0 +1,40 @@ +"""抽卡签到积分编排。""" + +from __future__ import annotations + +from typing import Any, Protocol + + +class GachaSignInRecorder(Protocol): + async def record_sign_in(self, user_id: str, points_awarded: int) -> bool: + """记录抽卡签到,首次签到返回 True。""" + + +class PointsAwarder(Protocol): + async def add_points(self, user_id: str, amount: int, source: str, reason: str) -> tuple[bool, int]: + """发放积分,返回是否成功与新余额。""" + + +async def award_daily_sign_in_points( + *, + data_manager: GachaSignInRecorder, + points_api: PointsAwarder, + user_id: str, + points: int, + source: str, + reason: str, +) -> tuple[bool, int, str]: + """先记录签到再发放积分,避免重复签到时重复发积分。 + + `/bot/gacha/sign-in` 负责判断当天是否已经签到;只有它确认首次签到后, + nonebot 才调用 `/bot/points/add` 发积分。 + """ + + signed = await data_manager.record_sign_in(user_id, points) + if not signed: + return False, 0, "signed_already_or_failed" + + success, new_balance = await points_api.add_points(user_id, points, source, reason) + if not success: + return False, new_balance, "points_failed" + return True, new_balance, "awarded" diff --git a/tests/test_onmyoji_gacha_http_api.py b/tests/test_onmyoji_gacha_http_api.py index 326d57a..19cc3d6 100644 --- a/tests/test_onmyoji_gacha_http_api.py +++ b/tests/test_onmyoji_gacha_http_api.py @@ -41,6 +41,22 @@ DataManager = data_manager_module.DataManager GachaSystem = gacha_module.GachaSystem +def load_sign_in_module(): + """直接加载签到编排模块,避免导入 NoneBot 插件入口。""" + + plugin_dir = Path(__file__).resolve().parents[1] / "danding_bot" / "plugins" / "onmyoji_gacha" + module_name = "_onmyoji_gacha_sign_in_under_test" + spec = importlib.util.spec_from_file_location(module_name, plugin_dir / "sign_in.py") + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + assert spec and spec.loader + spec.loader.exec_module(module) + return module + + +sign_in_module = load_sign_in_module() + + class FakeResponse: """模拟 aiohttp 响应上下文。""" @@ -245,3 +261,60 @@ async def test_network_error_keeps_failure_shapes_and_no_sqlite(monkeypatch): assert await manager.record_sign_in("10001", 20) is False assert await manager.get_rank() == [] assert "sqlite3" not in inspect.getsource(data_manager_module) + + +@pytest.mark.asyncio +async def test_daily_sign_in_awards_points_only_after_sign_recorded(): + calls = [] + + class FakeDataManager: + async def record_sign_in(self, user_id, points_awarded): + calls.append(("sign-in", user_id, points_awarded)) + return True + + class FakePointsAPI: + async def add_points(self, user_id, amount, source, reason): + calls.append(("add-points", user_id, amount, source, reason)) + return True, 66 + + success, balance, status = await sign_in_module.award_daily_sign_in_points( + data_manager=FakeDataManager(), + points_api=FakePointsAPI(), + user_id="10001", + points=20, + source="gacha_sign", + reason="抽卡签到", + ) + + assert (success, balance, status) == (True, 66, "awarded") + assert calls == [ + ("sign-in", "10001", 20), + ("add-points", "10001", 20, "gacha_sign", "抽卡签到"), + ] + + +@pytest.mark.asyncio +async def test_daily_sign_in_repeat_does_not_award_points(): + calls = [] + + class FakeDataManager: + async def record_sign_in(self, user_id, points_awarded): + calls.append(("sign-in", user_id, points_awarded)) + return False + + class FakePointsAPI: + async def add_points(self, user_id, amount, source, reason): + calls.append(("add-points", user_id, amount, source, reason)) + return True, 66 + + success, balance, status = await sign_in_module.award_daily_sign_in_points( + data_manager=FakeDataManager(), + points_api=FakePointsAPI(), + user_id="10001", + points=20, + source="gacha_sign", + reason="抽卡签到", + ) + + assert (success, balance, status) == (False, 0, "signed_already_or_failed") + assert calls == [("sign-in", "10001", 20)]