doge 1 dzień temu
rodzic
commit
7d7e6f750c

+ 12 - 2
app/Http/Controllers/admin/TelegramChannel.php

@@ -59,12 +59,22 @@ class TelegramChannel extends Controller
 
     public function privateMenu(Request $request, TelegramChannelService $channels): JsonResponse
     {
-        return $this->respond(fn () => $channels->privateMenu($this->id($request), $this->menuLanguage($request)));
+        return $this->respond(fn () => $request->has('language')
+            ? $channels->privateMenu($this->id($request), $this->menuLanguage($request))
+            : ['menus' => (object) $channels->privateMenus($this->id($request))]);
     }
 
     public function savePrivateMenu(Request $request, TelegramChannelService $channels): JsonResponse
     {
-        return $this->respond(fn () => $channels->savePrivateMenu($this->id($request), $this->menuLanguage($request), $request->all()));
+        return $this->respond(function () use ($request, $channels) {
+            $id = $this->id($request);
+            if ($request->has('language')) {
+                return $channels->savePrivateMenu($id, $this->menuLanguage($request), $request->all());
+            }
+
+            $data = $request->validate(['menus' => 'required|array:zh,en,vi|min:1', 'menus.*' => 'required|array']);
+            return ['menus' => $channels->savePrivateMenus($id, $data['menus'])];
+        });
     }
 
     public function registerPrivateWebhook(Request $request, TelegramChannelService $channels): JsonResponse

+ 2 - 0
app/Http/Controllers/api/TelegramWebHook.php

@@ -193,6 +193,8 @@ class TelegramWebHook extends BaseController
                         //关键字回复
                         $res = KeyboardService::getKeyWordReply($chatId, $text);
                         if (!empty($res)) return $res;
+                        $res = KeyboardService::replyToTranslatedMenu($chatId, $text);
+                        if ($res) return $res;
                         $stepStatus = intval(Cache::get(get_step_key($chatId), -1));
                         $res = QianBaoWithdrawService::onMessage($chatId, $text, $messageId, $stepStatus);
                         if (empty($res)) $res = SanJinRechargeService::onMessage($chatId, $text, $messageId, $stepStatus);

+ 52 - 0
app/Services/KeyboardService.php

@@ -199,6 +199,58 @@ class KeyboardService extends BaseService
         return false;
     }
 
+    public static function translatedMenuAction(string $text): ?string
+    {
+        $label = self::menuLabel($text);
+        $actions = [
+            '近期注单' => 'recentBets',
+            '今日流水' => 'flowList',
+            '流水列表' => 'flowList',
+            '开奖历史' => 'winningHistory',
+            '当期下注' => 'currentBetting',
+            '本期下注' => 'currentBetting',
+            '查看余额' => 'checkBalance',
+            '选择语言' => 'selectLanguage',
+            '优惠活动' => 'promotionalActivity',
+            '更多玩法' => 'football',
+            'F1bet娱乐更多玩法' => 'football',
+            '充值上分' => 'topUp',
+            '联系客服' => '联系客服',
+            '投注大群' => '投注大群',
+            'F1BET投注大群' => '投注大群',
+        ];
+
+        foreach ($actions as $key => $action) {
+            foreach (['zh', 'en', 'vi'] as $language) {
+                if ($label === self::menuLabel(trans("messages.{$key}", [], $language))) {
+                    return $action;
+                }
+            }
+        }
+        return null;
+    }
+
+    public static function replyToTranslatedMenu($chatId, string $text)
+    {
+        $action = self::translatedMenuAction($text);
+        if ($action === null) {
+            return false;
+        }
+        if (!in_array($action, ['联系客服', '投注大群'], true)) {
+            return self::menuEvent($chatId, $action);
+        }
+
+        // Reuse an existing keyword reply even when its label has a leading emoji.
+        $keywords = self::model()::where('button', 'like', '%' . $action . '%')->pluck('button');
+        $keyword = $keywords->first(fn ($keyword) => self::translatedMenuAction($keyword) === $action);
+        return $keyword === null ? false : self::getKeyWordReply($chatId, $keyword);
+    }
+
+    private static function menuLabel(string $text): string
+    {
+        return preg_replace('/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/u', '', trim($text)) ?? $text;
+    }
+
     // 开始菜单触发事件
     public static function menuEvent($chatId, $event = '')
     {

+ 46 - 14
app/Services/TelegramChannelService.php

@@ -334,31 +334,64 @@ class TelegramChannelService
     }
 
     public function privateMenu(int $id, string $language): array
+    {
+        return $this->privateMenus($id)[$language] ?? ['reply' => '', 'buttons' => [], 'type' => 'text', 'image' => '', 'video' => ''];
+    }
+
+    public function privateMenus(int $id): array
     {
         $menus = json_decode($this->findBot($id)->private_menus ?? '{}', true);
-        return $menus[$language] ?? ['reply' => '', 'buttons' => []];
+        foreach ($menus as &$menu) {
+            $menu += ['type' => 'text', 'image' => '', 'video' => ''];
+        }
+        return $menus;
     }
 
     public function savePrivateMenu(int $id, string $language, array $input): array
     {
-        $menu = Validator::make($input, [
-            'reply' => 'required|string|max:4096',
-            'buttons' => 'present|array|list|max:20',
-            'buttons.*' => 'required|array|list',
-            'buttons.*.*' => 'required|string|max:128',
-        ])->validate();
+        return $this->savePrivateMenus($id, [$language => $input])[$language];
+    }
+
+    public function savePrivateMenus(int $id, array $input): array
+    {
+        $menus = [];
+        foreach ($input as $language => $menu) {
+            $menus[$language] = $this->validatePrivateMenu($menu);
+        }
 
-        return DB::transaction(function () use ($id, $language, $menu) {
+        DB::transaction(function () use ($id, $menus) {
             $bot = DB::table('telegram_bots')->where('id', $id)->lockForUpdate()->first()
                 ?? throw ValidationException::withMessages(['id' => '机器人不存在']);
-            $menus = json_decode($bot->private_menus ?? '{}', true);
-            $menus[$language] = $menu;
+            $existing = json_decode($bot->private_menus ?? '{}', true);
             DB::table('telegram_bots')->where('id', $id)->update([
-                'private_menus' => json_encode($menus, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
+                'private_menus' => json_encode(array_replace($existing, $menus), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
                 'updated_at' => now(),
             ]);
-            return $menu;
         }, 3);
+        return $menus;
+    }
+
+    private function validatePrivateMenu(array $input): array
+    {
+        $menu = Validator::make($input, [
+            'buttons' => 'present|array|list|max:20',
+            'buttons.*' => 'required|array|list',
+            'buttons.*.*' => 'required|string|max:128',
+        ])->validate();
+        $content = self::message([
+            'type' => $input['type'] ?? 'text',
+            'text' => $input['reply'] ?? null,
+            'image' => $input['image'] ?? null,
+            'video' => $input['video'] ?? null,
+        ]);
+        $menu += [
+            'type' => $content['type'],
+            'reply' => $content['text'],
+            'image' => $content['type'] === 'image' ? $content['image'] : '',
+            'video' => $content['type'] === 'video' ? $content['video'] : '',
+        ];
+
+        return $menu;
     }
 
     public function registerPrivateWebhook(int $id): array
@@ -456,9 +489,8 @@ class TelegramChannelService
         $keyboard = $menu['buttons'] === []
             ? ['remove_keyboard' => true]
             : ['keyboard' => $menu['buttons'], 'resize_keyboard' => true];
-        $telegram->sendMessage([
+        $this->send($telegram, ['text' => $menu['reply']] + $menu + ['type' => 'text'], [
             'chat_id' => $chatId,
-            'text' => $menu['reply'],
             'reply_markup' => json_encode($keyboard, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE),
         ]);
     }

+ 6 - 2
docs/前端交付/bot-28/后台/Telegram频道管理-前端接入.md

@@ -46,10 +46,14 @@
 | `GET /admin/telegramBot/list` | 返回 `data.list`,元素含 `id`、`bot_username` 和 `bot_name` |
 | `POST /admin/telegramBot/save` | 提交 `bot_token`,可选 `bot_username`,更换 Token 时带 `id` |
 | `POST /admin/telegramBot/delete` | 提交 `{ "id": 8 }` |
-| `GET /admin/telegramBot/privateMenu?id=8&language=zh` | 返回该机器人该语言的 `reply` 和 `buttons` |
-| `POST /admin/telegramBot/privateMenu` | 保存机器人私聊菜单 |
+| `GET /admin/telegramBot/privateMenu?id=8` | 返回 `data.menus`,按语言读取 `type`、`reply`、`image`、`video`、`buttons` |
+| `POST /admin/telegramBot/privateMenu` | 提交 `id` 和 `menus`,一次保存多个语言 |
 | `POST /admin/telegramBot/privateWebhook` | 提交 `{ "id": 8 }`,首次接入 Telegram |
 
 读取权限为 `admin/telegramChannel/list`,修改权限为 `admin/telegramChannel/save`。Token 输入框使用密码类型,接口不会回传。一个机器人可供多个频道选择,解除所有频道绑定后可删除。
 
 在机器人行的「私聊底部菜单」保存,如 `{"id":8,"language":"zh","reply":"欢迎","buttons":[["💁联系客服"],["帮助","关于"] ]}`。按钮是按行排列的文字,`[]` 会在下次 `/start` 时收起键盘。用户发送 `/start` 后收到该机器人的回复和底部键盘。频道专用机器人首次使用时点「接入 Telegram」。系统机器人使用现有接入。已经绑定到其他 Webhook 的机器人会提示先处理原接入。
+
+开始回复可选 `type: text | image | video`。文本填写 `reply`。图片或视频复用频道消息上传接口,将返回地址填入 `image` 或 `video`,`reply` 可作为说明。旧请求省略 `type` 时按文本处理。保存后向该机器人发送 `/start`,回复和底部键盘会一起下发。
+
+多语言表单按按钮所在行和列对应中文、英文、越南语。批量保存时提交 `menus: { "zh": { ... }, "en": { ... }, "vi": { ... } }`,每个语言的 `buttons` 为二维字符串数组。未配置的语言可省略,旧客户端也可继续传 `language` 保存单一语言。

+ 6 - 0
lang/en/messages.php

@@ -223,6 +223,12 @@ return [
     "🔥本期下注" => "🔥 Bet for This Period",
     "💰查看余额" => "💰 View Balance",
     "投注大群" => "Betting Group",
+    "本期下注" => "Bet for This Period",
+    "优惠活动" => "Promotions",
+    "充值上分" => "Top Up",
+    "F1BET投注大群" => "F1BET Betting Group",
+    "F1bet娱乐更多玩法" => "F1BET More Games",
+    "更多玩法" => "More Games",
     "选择语言" => "Select Language",
     "钱包充值记录" => "Wallet Top-up History",
     "人工充值" => "Manual Top-up",

+ 6 - 0
lang/vi/messages.php

@@ -223,6 +223,12 @@ return [
     "🔥本期下注" => "🔥 Đặt cược trong kỳ này",
     "💰查看余额" => "💰 Xem số dư",
     "投注大群" => "Nhóm cược lớn",
+    "本期下注" => "Đặt cược trong kỳ này",
+    "优惠活动" => "Khuyến mãi",
+    "充值上分" => "Nạp tiền",
+    "F1BET投注大群" => "Nhóm cược F1BET",
+    "F1bet娱乐更多玩法" => "Thêm trò chơi F1BET",
+    "更多玩法" => "Thêm trò chơi",
     "选择语言" => "Chọn ngôn ngữ",
     "钱包充值记录" => "Lịch sử nạp tiền vào ví",
     "人工充值" => "Nạp tiền thủ công",

+ 6 - 0
lang/zh/messages.php

@@ -223,6 +223,12 @@ return [
     "🔥本期下注" => "🔥本期下注",
     "💰查看余额" => "💰查看余额",
     "投注大群" => "投注大群",
+    "本期下注" => "本期下注",
+    "优惠活动" => "优惠活动",
+    "充值上分" => "充值上分",
+    "F1BET投注大群" => "F1BET投注大群",
+    "F1bet娱乐更多玩法" => "F1bet娱乐更多玩法",
+    "更多玩法" => "更多玩法",
     "选择语言" => "选择语言",
     "钱包充值记录" => "钱包充值记录",
     "人工充值" => "人工充值",

+ 17 - 0
tests/Integration/TelegramChannelTest.php

@@ -420,6 +420,23 @@ class TelegramChannelTest extends TestCase
         parse_str((string) $request->getBody(), $sent);
         $this->assertSame('欢迎二', $sent['text']);
         $this->assertSame($buttons, json_decode($sent['reply_markup'], true)['keyboard']);
+
+        $this->app->register(FilesystemServiceProvider::class);
+        foreach (['image' => 'sendPhoto', 'video' => 'sendVideo'] as $type => $method) {
+            $media = "https://example.com/welcome-{$type}";
+            $this->service->savePrivateMenu($second['id'], 'zh', [
+                'type' => $type, 'reply' => '媒体说明', $type => $media, 'buttons' => $buttons,
+            ]);
+            $this->assertSame($media, $this->service->privateMenu($second['id'], 'zh')[$type]);
+            $this->telegramResult(['message_id' => 26]);
+            $this->service->handlePrivateWebhook($second['id'], $registration['secret_token'], $update);
+            $request = end($this->requests)['request'];
+            $this->assertSame("/bot222:fixture_second/{$method}", $request->getUri()->getPath());
+            parse_str((string) $request->getBody(), $sent);
+            $this->assertSame('媒体说明', $sent['caption']);
+            $this->assertSame($media, $sent[$type === 'image' ? 'photo' : 'video']);
+            $this->assertSame($buttons, json_decode($sent['reply_markup'], true)['keyboard']);
+        }
     }
 
     public function test_unbound_channel_can_be_saved_and_scheduled_messages_require_a_bot(): void