fix(onmyoji_gacha): 避免重复签到发放积分

This commit is contained in:
2026-06-20 19:00:41 +08:00
parent 8d26c46323
commit cbc0f5198a
3 changed files with 133 additions and 18 deletions

View File

@@ -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))

View File

@@ -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"

View File

@@ -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)]