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 nonebot.rule import Rule
from pathlib import Path from pathlib import Path
from .config import Config from .config import Config
from .gacha import GachaSystem from .gacha import GachaSystem
from .utils import format_sign_in_message, format_user_mention, get_image_path 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 .sign_in import award_daily_sign_in_points
from . import web_api from .api_utils import process_ssr_sp_reward, process_achievement_reward
from danding_bot.plugins.danding_points import points_api from . import web_api
from danding_bot.plugins.danding_points import points_api
# 创建Config实例 # 创建Config实例
config = Config() config = Config()
@@ -66,18 +67,19 @@ async def try_handle_daily_sign_in(matcher, user_id: str, user_name: str) -> Non
"""处理抽卡成功后的每日签到,不影响主流程""" """处理抽卡成功后的每日签到,不影响主流程"""
try: try:
points = random.randint(SIGN_IN_MIN_POINTS, SIGN_IN_MAX_POINTS) points = random.randint(SIGN_IN_MIN_POINTS, SIGN_IN_MAX_POINTS)
success, new_balance = await points_api.add_points( success, new_balance, status = await award_daily_sign_in_points(
user_id, data_manager=gacha_system.data_manager,
points, points_api=points_api,
SIGN_IN_SOURCE, user_id=user_id,
SIGN_IN_REASON, points=points,
) source=SIGN_IN_SOURCE,
if not success: reason=SIGN_IN_REASON,
logger.error("抽卡签到积分发放失败 user_id=%s points=%s", user_id, points) )
return if not success:
if status == "points_failed":
if not await gacha_system.data_manager.record_sign_in(user_id, points): logger.error("抽卡签到记录成功但积分发放失败 user_id=%s points=%s", user_id, points)
logger.warning("抽卡签到落库冲突,积分已发放但签到记录重复 user_id=%s", user_id) else:
logger.info("抽卡签到未发放积分 user_id=%s status=%s", user_id, status)
return return
await matcher.send(format_sign_in_message(user_id, user_name, points, new_balance)) 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 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: class FakeResponse:
"""模拟 aiohttp 响应上下文。""" """模拟 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.record_sign_in("10001", 20) is False
assert await manager.get_rank() == [] assert await manager.get_rank() == []
assert "sqlite3" not in inspect.getsource(data_manager_module) 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)]