Bläddra i källkod

feat: make check-in messages configurable

AI-Co-Authored-By: Codex
chendeben 1 vecka sedan
förälder
incheckning
e7b63ab9ea

+ 20 - 0
admin-web/src/pages/ChatDetail.tsx

@@ -1343,6 +1343,26 @@ function PointRulesTab({ chatId }: { chatId: string }) {
             </section>
           ))}
         </div>
+        <section className="rule-group" style={{ marginTop: 16 }}>
+          <h3 className="rule-group-title">签到提示语</h3>
+          <Typography.Paragraph type="secondary">
+            可用变量:{'{points}'} 表示本次签到积分,{'{balance}'} 表示签到后的当前余额。
+          </Typography.Paragraph>
+          <Form.Item
+            name="checkin_success_message"
+            label="签到成功提示"
+            rules={[{ required: true, whitespace: true, message: '请输入签到成功提示' }]}
+          >
+            <Input.TextArea rows={2} maxLength={500} showCount />
+          </Form.Item>
+          <Form.Item
+            name="checkin_already_message"
+            label="今日已签到提示"
+            rules={[{ required: true, whitespace: true, message: '请输入今日已签到提示' }]}
+          >
+            <Input.TextArea rows={2} maxLength={500} showCount />
+          </Form.Item>
+        </section>
         <Button type="primary" htmlType="submit" style={{ marginTop: 16 }}>保存积分规则</Button>
       </Form>
     </Spin>

+ 20 - 0
admin-web/src/pages/Points.tsx

@@ -343,6 +343,26 @@ function RulesEditor({ chatId }: { chatId: string }) {
         <RuleGroup title="活跃消息" enabled="activity_enabled" fields={[['activity_points', '单次奖励'], ['activity_cooldown_seconds', '冷却秒数'], ['activity_daily_cap', '每日上限']]} />
         <RuleGroup title="有效点赞" enabled="upvote_enabled" fields={[['upvote_points', '单次奖励'], ['upvote_pair_cooldown_seconds', '双方冷却秒数'], ['upvote_daily_cap', '每日上限']]} />
       </div>
+      <section className="rule-group" style={{ marginTop: 16 }}>
+        <h3 className="rule-group-title">签到提示语</h3>
+        <Typography.Paragraph type="secondary">
+          可用变量:{'{points}'} 表示本次签到积分,{'{balance}'} 表示签到后的当前余额。
+        </Typography.Paragraph>
+        <Form.Item
+          name="checkin_success_message"
+          label="签到成功提示"
+          rules={[{ required: true, whitespace: true, message: '请输入签到成功提示' }]}
+        >
+          <Input.TextArea rows={2} maxLength={500} showCount />
+        </Form.Item>
+        <Form.Item
+          name="checkin_already_message"
+          label="今日已签到提示"
+          rules={[{ required: true, whitespace: true, message: '请输入今日已签到提示' }]}
+        >
+          <Input.TextArea rows={2} maxLength={500} showCount />
+        </Form.Item>
+      </section>
     </Form>
   );
 }

+ 2 - 0
admin-web/src/types.ts

@@ -335,6 +335,8 @@ export interface PointRules {
   checkin_enabled: boolean;
   checkin_button_enabled: boolean;
   checkin_points: number;
+  checkin_success_message: string;
+  checkin_already_message: string;
   activity_enabled: boolean;
   activity_points: number;
   activity_cooldown_seconds: number;

+ 7 - 0
admin-web/tests/unit/Points.test.tsx

@@ -16,6 +16,8 @@ const rules = {
   checkin_enabled: true,
   checkin_button_enabled: true,
   checkin_points: 10,
+  checkin_success_message: '签到成功:+{points} 积分,当前余额:{balance}。',
+  checkin_already_message: '你今天已经签到过了,当前余额:{balance}。',
   activity_enabled: true,
   activity_points: 1,
   activity_cooldown_seconds: 300,
@@ -75,7 +77,10 @@ it('展示余额排行、流水并保存积分规则', async () => {
 
   fireEvent.click(screen.getByRole('tab', { name: '自动积分规则' }));
   expect(await screen.findByRole('heading', { name: '签到' })).toBeInTheDocument();
+  expect(screen.getByRole('heading', { name: '签到提示语' })).toBeInTheDocument();
   expect(screen.getByRole('switch', { name: '底部签到按钮' })).toBeChecked();
+  expect(screen.getByLabelText('签到成功提示')).toHaveValue(rules.checkin_success_message);
+  expect(screen.getByLabelText('今日已签到提示')).toHaveValue(rules.checkin_already_message);
   await waitFor(() =>
     expect(apiRequest).toHaveBeenCalledWith('/chats/-100123/points/settings'),
   );
@@ -97,6 +102,8 @@ it('展示余额排行、流水并保存积分规则', async () => {
     expect(call).toBeTruthy();
     expect(JSON.parse(String(call?.[1]?.body))).toMatchObject({
       checkin_button_enabled: true,
+      checkin_success_message: rules.checkin_success_message,
+      checkin_already_message: rules.checkin_already_message,
       confirm: true,
     });
   });

+ 20 - 0
tests/test_points.py

@@ -150,6 +150,26 @@ async def test_checkin_uses_chat_timezone_day_boundary(app_modules):
     assert account["balance"] == 20
 
 
+async def test_checkin_messages_are_normalized_and_persisted(app_modules):
+    points = app_modules.load("wbb.utils.dbpoints")
+
+    saved = await points.set_point_rules(
+        -100,
+        {
+            "checkin_success_message": "  获得 {points},余额 {balance}  ",
+            "checkin_already_message": "",
+        },
+    )
+
+    assert saved["checkin_success_message"] == "获得 {points},余额 {balance}"
+    assert (
+        saved["checkin_already_message"]
+        == points.DEFAULT_CHECKIN_ALREADY_MESSAGE
+    )
+    loaded = await points.get_point_rules(-100)
+    assert loaded["checkin_success_message"] == "获得 {points},余额 {balance}"
+
+
 async def test_activity_cooldown_duplicate_content_and_daily_cap(app_modules):
     points = app_modules.load("wbb.utils.dbpoints")
     await points.set_point_rules(

+ 21 - 10
tests/test_points_commands.py

@@ -41,15 +41,9 @@ def _checkin_message() -> SimpleNamespace:
     )
 
 
-@pytest.mark.parametrize(
-    ("created", "expected_text"),
-    [
-        (True, "签到成功"),
-        (False, "已经签到过"),
-    ],
-)
+@pytest.mark.parametrize("created", [True, False])
 async def test_checkin_schedules_user_and_bot_messages_for_cleanup(
-    points_module, monkeypatch, created, expected_text
+    points_module, monkeypatch, created
 ):
     message = _checkin_message()
     account = {"balance": 10}
@@ -62,7 +56,13 @@ async def test_checkin_schedules_user_and_bot_messages_for_cleanup(
     monkeypatch.setattr(
         points_module,
         "get_point_rules",
-        AsyncMock(return_value={"checkin_points": 10}),
+        AsyncMock(
+            return_value={
+                "checkin_points": 10,
+                "checkin_success_message": "领取成功 +{points},余额 {balance}",
+                "checkin_already_message": "今天领过了,余额 {balance}",
+            }
+        ),
     )
     monkeypatch.setattr(
         points_module,
@@ -72,7 +72,14 @@ async def test_checkin_schedules_user_and_bot_messages_for_cleanup(
 
     response = await points_module.points_checkin_command(None, message)
 
-    assert expected_text in message.reply_text.await_args.args[0]
+    expected_message = (
+        "领取成功 +10,余额 10" if created else "今天领过了,余额 10"
+    )
+    assert message.reply_text.await_args.args == (expected_message,)
+    assert (
+        message.reply_text.await_args.kwargs["parse_mode"]
+        == points_module.ParseMode.DISABLED
+    )
     assert cleanup == [message, response]
 
 
@@ -95,4 +102,8 @@ async def test_checkin_error_schedules_user_and_bot_messages_for_cleanup(
     response = await points_module.points_checkin_command(None, message)
 
     assert message.reply_text.await_args.args == ("签到暂不可用",)
+    assert (
+        message.reply_text.await_args.kwargs["parse_mode"]
+        == points_module.ParseMode.DISABLED
+    )
     assert cleanup == [message, response]

+ 34 - 5
wbb/modules/points.py

@@ -19,6 +19,8 @@ from wbb.services.point_settings import (
     refresh_active_checkin_keyboards,
 )
 from wbb.utils.dbpoints import (
+    DEFAULT_CHECKIN_ALREADY_MESSAGE,
+    DEFAULT_CHECKIN_SUCCESS_MESSAGE,
     SOURCE_ADMIN,
     InsufficientPoints,
     PointsError,
@@ -92,11 +94,23 @@ def _message_content(message: Message) -> str:
 
 
 async def _reply_checkin(message: Message, text: str) -> Message:
-    response = await message.reply_text(text)
+    response = await message.reply_text(text, parse_mode=ParseMode.DISABLED)
     schedule_group_cleanup(message, response)
     return response
 
 
+def _checkin_message(
+    template: str,
+    *,
+    points: int,
+    balance: int,
+) -> str:
+    return (
+        template.replace("{points}", str(points))
+        .replace("{balance}", str(balance))
+    )
+
+
 async def _resolve_target(message: Message, raw: str | None = None) -> User | None:
     if message.reply_to_message and message.reply_to_message.from_user:
         return message.reply_to_message.from_user
@@ -150,16 +164,31 @@ async def points_checkin_command(_, message: Message):
         )
     except PointsError as exc:
         return await _reply_checkin(message, str(exc))
+    rules = await get_point_rules(message.chat.id)
+    points = int(rules.get("checkin_points", 0))
+    balance = int(account["balance"])
     if not created:
         return await _reply_checkin(
             message,
-            f"你今天已经签到过了,当前余额:{int(account['balance'])}。"
+            _checkin_message(
+                str(
+                    rules.get("checkin_already_message")
+                    or DEFAULT_CHECKIN_ALREADY_MESSAGE
+                ),
+                points=points,
+                balance=balance,
+            ),
         )
-    rules = await get_point_rules(message.chat.id)
     return await _reply_checkin(
         message,
-        f"签到成功:+{rules['checkin_points']} 积分,"
-        f"当前余额:{int(account['balance'])}。",
+        _checkin_message(
+            str(
+                rules.get("checkin_success_message")
+                or DEFAULT_CHECKIN_SUCCESS_MESSAGE
+            ),
+            points=points,
+            balance=balance,
+        ),
     )
 
 

+ 27 - 0
wbb/utils/dbpoints.py

@@ -23,6 +23,14 @@ SOURCE_GIVEAWAY_REFUND = "giveaway_refund"
 SOURCE_GIVEAWAY_PARTICIPATION = "giveaway_participation"
 SOURCE_GIVEAWAY_WINNER = "giveaway_winner"
 
+DEFAULT_CHECKIN_SUCCESS_MESSAGE = (
+    "签到成功:+{points} 积分,当前余额:{balance}。"
+)
+DEFAULT_CHECKIN_ALREADY_MESSAGE = (
+    "你今天已经签到过了,当前余额:{balance}。"
+)
+CHECKIN_MESSAGE_MAX_LENGTH = 500
+
 POINT_SOURCES = {
     SOURCE_CHECKIN,
     SOURCE_ACTIVITY,
@@ -40,6 +48,8 @@ DEFAULT_POINT_RULES: dict[str, Any] = {
     "checkin_enabled": True,
     "checkin_button_enabled": False,
     "checkin_points": 10,
+    "checkin_success_message": DEFAULT_CHECKIN_SUCCESS_MESSAGE,
+    "checkin_already_message": DEFAULT_CHECKIN_ALREADY_MESSAGE,
     "activity_enabled": True,
     "activity_points": 1,
     "activity_cooldown_seconds": 300,
@@ -126,6 +136,13 @@ def _safe_bool(value: Any, default: bool) -> bool:
     return default
 
 
+def _safe_text(value: Any, default: str, maximum: int) -> str:
+    text = str(value or "").strip()
+    if not text:
+        return default
+    return text[:maximum]
+
+
 def normalize_point_rules(raw: dict[str, Any] | None) -> dict[str, Any]:
     rules = dict(DEFAULT_POINT_RULES)
     if raw:
@@ -147,6 +164,16 @@ def normalize_point_rules(raw: dict[str, Any] | None) -> dict[str, Any]:
         timezone_name = "Asia/Shanghai"
     rules["timezone"] = timezone_name
     rules["checkin_points"] = _safe_int(rules["checkin_points"], 10, 0, 100000)
+    rules["checkin_success_message"] = _safe_text(
+        rules["checkin_success_message"],
+        DEFAULT_CHECKIN_SUCCESS_MESSAGE,
+        CHECKIN_MESSAGE_MAX_LENGTH,
+    )
+    rules["checkin_already_message"] = _safe_text(
+        rules["checkin_already_message"],
+        DEFAULT_CHECKIN_ALREADY_MESSAGE,
+        CHECKIN_MESSAGE_MAX_LENGTH,
+    )
     rules["activity_points"] = _safe_int(rules["activity_points"], 1, 0, 100000)
     rules["activity_cooldown_seconds"] = _safe_int(
         rules["activity_cooldown_seconds"], 300, 10, 86400