feat(wechat): 微信机器人全链路集成与稳定性修复

- 新增:管理员后台的微信机器人双向收发参数设置页面及扫码绑定能力。
- 新增:WechatBotApiService 与 KafkaConsumerService 模块打通过往僵尸进程导致的拒绝连接问题。
- 新增:下发所有群发/私聊通知时统一带上「[和平聊吧]」标注前缀。
- 优化:前端个人中心绑定逻辑支持一键生成及复制动态口令。
- 修复:闭环联调修补各个模型中产生的变量警告如 stdClass 对象获取等异常预警。
This commit is contained in:
2026-04-02 14:56:51 +08:00
parent 8a809e3cc0
commit fc57f97c9e
19 changed files with 1552 additions and 6 deletions
@@ -54,6 +54,10 @@
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.system.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
{!! '⚙️ 聊天室参数' !!}
</a>
<a href="{{ route('admin.wechat_bot.edit') }}"
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.wechat_bot.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
{!! '🤖 微信机器人' !!}
</a>
<a href="{{ route('admin.currency-logs.index') }}"
class="block px-4 py-3 rounded-md transition {{ request()->routeIs('admin.currency-logs.*') ? 'bg-indigo-600 font-bold' : 'hover:bg-white/10' }}">
{!! '💴 用户流水' !!}
@@ -0,0 +1,121 @@
@extends('admin.layouts.app')
@section('title', '微信机器人配置')
@section('content')
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="p-6 border-b border-gray-100 flex justify-between items-center bg-gray-50">
<div>
<h2 class="text-lg font-bold text-gray-800">微信机器人全站配置</h2>
<p class="text-xs text-gray-500 mt-1">保存后如涉及 Kafka 参数修改,需要重启后端消息消费守护进程。</p>
</div>
</div>
@if (session('success'))
<div class="mx-6 mt-4 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">
{{ session('success') }}
</div>
@endif
<div class="p-6">
<form action="{{ route('admin.wechat_bot.update') }}" method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
<!-- 1. Kafka API 基础配置 -->
<div class="mb-8">
<h3 class="text-md font-bold text-gray-800 border-b pb-2 mb-4">核心参数 (Kafka / API)</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">Kafka Brokers</label>
<input type="text" name="kafka_brokers" value="{{ old('kafka_brokers', $config['kafka']['brokers'] ?? '') }}" class="w-full border-gray-300 rounded-md shadow-sm p-2.5 bg-gray-50 border" placeholder="如 10.10.11.18:9092">
</div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">Kafka Topic</label>
<input type="text" name="kafka_topic" value="{{ old('kafka_topic', $config['kafka']['topic'] ?? '') }}" class="w-full border-gray-300 rounded-md shadow-sm p-2.5 bg-gray-50 border" placeholder="监听的主题">
</div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">Kafka Group ID</label>
<input type="text" name="kafka_group_id" value="{{ old('kafka_group_id', $config['kafka']['group_id'] ?? 'chatroom_wechat_bot') }}" class="w-full border-gray-300 rounded-md shadow-sm p-2.5 bg-gray-50 border" placeholder="消费组ID">
</div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">机器人微信号 (对外展示)</label>
<input type="text" name="kafka_bot_wxid" value="{{ old('kafka_bot_wxid', $config['kafka']['bot_wxid'] ?? '') }}" class="w-full border-gray-300 rounded-md shadow-sm p-2.5 bg-gray-50 border" placeholder="供用户添加好友用的微信号">
</div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">机器人二维码 (对外展示图)</label>
<input type="file" name="qrcode_image" accept="image/*" class="w-full border-gray-300 rounded-md shadow-sm p-2 bg-gray-50 border">
@if(!empty($config['api']['qrcode_image']))
<div class="mt-2">
<img src="{{ Storage::url($config['api']['qrcode_image']) }}" alt="QR Code" class="h-20 w-auto rounded border border-gray-200">
</div>
@endif
</div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">被调用方机器人服务基础API URL</label>
<input type="text" name="api_base_url" value="{{ old('api_base_url', $config['api']['base_url'] ?? '') }}" class="w-full border-gray-300 rounded-md shadow-sm p-2.5 bg-gray-50 border" placeholder="如 http://10.10.11.14:8848">
</div>
<div>
<label class="block text-sm font-bold text-gray-700 mb-1">机器人 Key (必需)</label>
<input type="text" name="api_bot_key" value="{{ old('api_bot_key', $config['api']['bot_key'] ?? '') }}" class="w-full border-gray-300 rounded-md shadow-sm p-2.5 bg-gray-50 border" placeholder="机器人的对接 Key">
</div>
</div>
</div>
<!-- 2. 群通知设置 -->
<div class="mb-8">
<h3 class="text-md font-bold text-gray-800 border-b pb-2 mb-4">群聊通知设置</h3>
<div class="mb-6 max-w-lg">
<label class="block text-sm font-bold text-gray-700 mb-1">目标微信群 Wxid</label>
<input type="text" name="group_target_wxid" value="{{ old('group_target_wxid', $config['group_notify']['target_wxid'] ?? '') }}" class="w-full border-gray-300 rounded-md shadow-sm p-2.5 bg-gray-50 border" placeholder="群的wxid标识">
<p class="text-xs mt-1 text-gray-500">以下开关针对此群发送通知</p>
</div>
<div class="space-y-4">
<label class="flex items-center cursor-pointer">
<input type="hidden" name="toggle_admin_online" value="0">
<input type="checkbox" name="toggle_admin_online" value="1" class="form-checkbox h-5 w-5 text-indigo-600 rounded border-gray-300 focus:ring-indigo-500" {{ !empty($config['group_notify']['toggle_admin_online']) ? 'checked' : '' }}>
<span class="ml-2 text-sm text-gray-700 font-bold">管理员上线通知</span>
</label>
<label class="flex items-center cursor-pointer">
<input type="hidden" name="toggle_baccarat_result" value="0">
<input type="checkbox" name="toggle_baccarat_result" value="1" class="form-checkbox h-5 w-5 text-indigo-600 rounded border-gray-300 focus:ring-indigo-500" {{ !empty($config['group_notify']['toggle_baccarat_result']) ? 'checked' : '' }}>
<span class="ml-2 text-sm text-gray-700 font-bold">百家乐开奖通知</span>
</label>
<label class="flex items-center cursor-pointer">
<input type="hidden" name="toggle_lottery_result" value="0">
<input type="checkbox" name="toggle_lottery_result" value="1" class="form-checkbox h-5 w-5 text-indigo-600 rounded border-gray-300 focus:ring-indigo-500" {{ !empty($config['group_notify']['toggle_lottery_result']) ? 'checked' : '' }}>
<span class="ml-2 text-sm text-gray-700 font-bold">彩票开奖通知</span>
</label>
</div>
</div>
<!-- 3. 点对点私聊通知设置 -->
<div class="mb-8">
<h3 class="text-md font-bold text-gray-800 border-b pb-2 mb-4">一对一私聊通知设置 (目标为用户微信)</h3>
<div class="space-y-4">
<label class="flex items-center cursor-pointer">
<input type="hidden" name="toggle_friend_online" value="0">
<input type="checkbox" name="toggle_friend_online" value="1" class="form-checkbox h-5 w-5 text-indigo-600 rounded border-gray-300 focus:ring-indigo-500" {{ !empty($config['personal_notify']['toggle_friend_online']) ? 'checked' : '' }}>
<span class="ml-2 text-sm text-gray-700 font-bold">好友上线 (带有30分钟冷却机制判断)</span>
</label>
<label class="flex items-center cursor-pointer">
<input type="hidden" name="toggle_spouse_online" value="0">
<input type="checkbox" name="toggle_spouse_online" value="1" class="form-checkbox h-5 w-5 text-indigo-600 rounded border-gray-300 focus:ring-indigo-500" {{ !empty($config['personal_notify']['toggle_spouse_online']) ? 'checked' : '' }}>
<span class="ml-2 text-sm text-gray-700 font-bold">夫妻上线 (带有30分钟冷却机制判断)</span>
</label>
<label class="flex items-center cursor-pointer">
<input type="hidden" name="toggle_level_change" value="0">
<input type="checkbox" name="toggle_level_change" value="1" class="form-checkbox h-5 w-5 text-indigo-600 rounded border-gray-300 focus:ring-indigo-500" {{ !empty($config['personal_notify']['toggle_level_change']) ? 'checked' : '' }}>
<span class="ml-2 text-sm text-gray-700 font-bold">等级变动</span>
</label>
</div>
</div>
<div class="mt-8 pt-6 border-t flex space-x-3">
<button type="submit" class="px-6 py-2 bg-indigo-600 text-white rounded-md font-bold hover:bg-indigo-700 shadow-sm transition">保存配置</button>
</div>
</form>
</div>
</div>
@endsection
@@ -169,6 +169,49 @@
</div>
</div>
{{-- 微信绑定 --}}
<div style="margin-bottom:16px; border:1px solid #e0e0e0; border-radius:6px; padding:12px;">
<div style="font-size:12px; font-weight:bold; color:#336699; margin-bottom:8px;">💬 微信绑定</div>
<div style="display:flex; flex-direction:column; gap:6px;" id="wechat-bind-container">
@if (empty(Auth::user()->wxid))
@php
$botConfigBody = \App\Models\SysParam::where('alias', 'wechat_bot_config')->value('body');
$botConfig = $botConfigBody ? json_decode($botConfigBody, true) : [];
$botWxid = $botConfig['kafka']['bot_wxid'] ?? '暂未配置';
$qrcodeImage = $botConfig['api']['qrcode_image'] ?? null;
@endphp
<div style="font-size:12px; color:#666;">
您尚未绑定微信。<br>
@if($qrcodeImage)
扫码添加机器人微信:<br>
<img src="{{ \Illuminate\Support\Facades\Storage::url($qrcodeImage) }}" alt="机器人二维码" style="max-height:100px; display:block; margin: 6px 0; border: 1px solid #ddd; border-radius: 4px;">
@else
请添加机器人微信:<strong style="color:#d97706">{{ $botWxid }}</strong><br>
@endif
并发送以下绑定代码完成绑定:
</div>
<div style="display:flex; align-items:center; gap:8px;">
<input id="wechat-bind-code" type="text" readonly value="点击生成"
style="flex:1; padding:6px 8px; border:1px dashed #336699; background:#f9fafb; border-radius:4px; font-size:13px; font-weight:bold; color:#336699; text-align:center; cursor:text;">
<button type="button" id="btn-copy-bind-code" onclick="copyWechatBindCode()"
style="display:none; padding:5px 10px; border:1px solid #10b981; background:#ecfdf5; color:#10b981; border-radius:4px; font-size:12px; cursor:pointer; white-space:nowrap;">
复制
</button>
<button type="button" id="btn-generate-bind-code" onclick="generateWechatBindCode()"
style="padding:5px 10px; border:1px solid #336699; background:#eef5ff; color:#336699; border-radius:4px; font-size:12px; cursor:pointer; white-space:nowrap;">
生成代码
</button>
</div>
<div id="bind-code-tip" style="font-size:11px; color:#888; display:none; text-align:center;">有效时间 5 分钟,绑定成功后请刷新页面。</div>
@else
<div style="font-size:12px; color:#16a34a; font-weight:bold; display:flex; justify-content:space-between; align-items:center;">
<span>已绑定微信,可接收提醒通知。</span>
<button type="button" onclick="unbindWechat()" style="padding:4px 8px; background:#fee2e2; color:#dc2626; border:1px solid #fecaca; border-radius:4px; font-size:11px; cursor:pointer;">解除绑定</button>
</div>
@endif
</div>
</div>
{{-- 内联操作结果提示(仿百家乐已押注卡片风格) --}}
<div id="settings-inline-msg"
style="display:none; border-radius:10px; padding:10px 14px;
@@ -553,12 +596,103 @@
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
} catch (e) {
window.chatDialog.alert('网络异常,验证码发送失败,请稍后重试。', '错误', '#6b7280');
btn.innerText = '获取验证码';
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
}
/**
* 生成微信绑定验证码
*/
async function generateWechatBindCode() {
const btn = document.getElementById('btn-generate-bind-code');
const input = document.getElementById('wechat-bind-code');
const tip = document.getElementById('bind-code-tip');
btn.disabled = true;
btn.innerText = '生成中...';
try {
const res = await fetch('{{ route('user.generate_wechat_code') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
}
});
const data = await res.json();
if (res.ok && data.status === 'success') {
input.value = data.code;
tip.style.display = 'block';
document.getElementById('btn-copy-bind-code').style.display = 'inline-block';
showInlineMsg('settings-inline-msg', '✅ 绑定代码生成成功,请在5分钟内发送给机器人', true);
} else {
showInlineMsg('settings-inline-msg', '❌ 生成失败:' + (data.message || '未知错误'), false);
}
} catch (e) {
window.chatDialog.alert('网络异常,验证码发送失败,请稍后重试', '错误', '#6b7280');
btn.innerText = '获取验证码';
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
showInlineMsg('settings-inline-msg', '🌐 网络异常,请稍后重试', false);
}
btn.disabled = false;
btn.innerText = '重新生成';
}
/**
* 复制微信绑定验证码
*/
function copyWechatBindCode() {
const input = document.getElementById('wechat-bind-code');
if (input.value && input.value !== '点击生成' && input.value !== '生成中...') {
input.select();
input.setSelectionRange(0, 99999);
try {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(input.value);
} else {
document.execCommand('copy');
}
const btn = document.getElementById('btn-copy-bind-code');
const originalText = btn.innerText;
btn.innerText = '已复制';
setTimeout(() => {
btn.innerText = originalText;
}, 2000);
} catch (err) {
showInlineMsg('settings-inline-msg', '❌ 复制失败,请手动复制', false);
}
}
}
/**
* 解除微信绑定
*/
async function unbindWechat() {
if (!confirm('确定要解除微信绑定吗?解除后将无法接收任何机器人推送通知。')) {
return;
}
try {
const res = await fetch('{{ route('user.unbind_wechat') }}', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
}
});
const data = await res.json();
if (res.ok && data.status === 'success') {
alert('✅ 解绑成功!请刷新页面获取最新状态。');
location.reload();
} else {
alert('❌ 解绑失败:' + (data.message || '未知错误'));
}
} catch (e) {
alert('网络异常,解绑失败');
}
}
</script>