feat(wechat): 微信机器人全链路集成与稳定性修复
- 新增:管理员后台的微信机器人双向收发参数设置页面及扫码绑定能力。 - 新增:WechatBotApiService 与 KafkaConsumerService 模块打通过往僵尸进程导致的拒绝连接问题。 - 新增:下发所有群发/私聊通知时统一带上「[和平聊吧]」标注前缀。 - 优化:前端个人中心绑定逻辑支持一键生成及复制动态口令。 - 修复:闭环联调修补各个模型中产生的变量警告如 stdClass 对象获取等异常预警。
This commit is contained in:
@@ -208,6 +208,14 @@ class AutoSaveExp extends Command
|
||||
$this->chatState->pushMessage($roomId, $sysMsg);
|
||||
broadcast(new MessageSent($roomId, $sysMsg));
|
||||
SaveMessageJob::dispatch($sysMsg);
|
||||
|
||||
// 触发微信机器人私聊通知 (等级提升)
|
||||
try {
|
||||
$wechatService = app(\App\Services\WechatBot\WechatNotificationService::class);
|
||||
$wechatService->notifyLevelChange($user, $oldLevel, $newLevel);
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::error('WechatBot level change notification failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 向用户私人推送"系统为你自动存点"信息,在其聊天框显示
|
||||
|
||||
155
app/Console/Commands/ConsumeWechatMessages.php
Normal file
155
app/Console/Commands/ConsumeWechatMessages.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:微信机器人 Kafka 消费命令
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\WechatBot\KafkaConsumerService;
|
||||
use App\Services\WechatBot\WechatBotApiService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ConsumeWechatMessages extends Command
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'wechat-bot:consume';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '消费 Kafka 微信机器人消息(守护进程)';
|
||||
|
||||
protected KafkaConsumerService $kafkaService;
|
||||
|
||||
public function __construct(KafkaConsumerService $kafkaService)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->kafkaService = $kafkaService;
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('正在启动微信机器人 Kafka 消费者...');
|
||||
|
||||
$consumer = $this->kafkaService->createConsumer();
|
||||
if (! $consumer) {
|
||||
$this->error('Kafka 配置不完整或加载失败,请在后台检查机器人设置。');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('消费者已启动,等待消息...');
|
||||
|
||||
$apiService = new WechatBotApiService;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
$messageJson = $consumer->consume();
|
||||
if ($messageJson) {
|
||||
$rawJson = $messageJson->getValue();
|
||||
$this->info('--> 收到新的 Kafka 消息 (Raw Length: '.strlen($rawJson).')');
|
||||
|
||||
$messages = $this->kafkaService->parseKafkaMessage($rawJson);
|
||||
|
||||
if (empty($messages)) {
|
||||
$this->info('--> 解析后:无匹配的 AddMsgs 内容');
|
||||
}
|
||||
|
||||
foreach ($messages as $msg) {
|
||||
try {
|
||||
$this->processMessage($msg, $apiService);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('处理单条微信消息失败', [
|
||||
'error' => $e->getMessage(),
|
||||
'msg' => $msg,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$consumer->ack($messageJson);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Kafka 消费异常', ['error' => $e->getMessage()]);
|
||||
// 延迟重试避免死循环 CPU 空转
|
||||
sleep(2);
|
||||
}
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单条消息逻辑
|
||||
*/
|
||||
protected function processMessage(array $msg, WechatBotApiService $apiService): void
|
||||
{
|
||||
// 仅处理文本消息 (msg_type = 1)
|
||||
if ($msg['msg_type'] != 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$content = trim($msg['content']);
|
||||
$fromUser = $msg['from_user'];
|
||||
$isChatroom = $msg['is_chatroom'];
|
||||
|
||||
// 绑定逻辑:必须是私聊,且内容格式为 BD-xxxxxx
|
||||
if (! $isChatroom && preg_match('/^BD-\d{6}$/i', $content)) {
|
||||
$this->info("收到潜在绑定请求: {$content} from {$fromUser}");
|
||||
$this->handleBindRequest(strtoupper($content), $fromUser, $apiService);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理账号绑定请求
|
||||
*/
|
||||
protected function handleBindRequest(string $code, string $wxid, WechatBotApiService $apiService): void
|
||||
{
|
||||
$cacheKey = 'wechat_bind_code:'.$code;
|
||||
$username = Cache::get($cacheKey);
|
||||
|
||||
if (! $username) {
|
||||
$apiService->sendTextMessage($wxid, '❌ 绑定失败:该验证码无效或已过有效期(5分钟)。请在个人中心重新生成。');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$user = User::where('username', $username)->first();
|
||||
if (! $user) {
|
||||
$apiService->sendTextMessage($wxid, '❌ 绑定失败:找不到对应的用户账号。');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断该微信号是否已经被其他用户绑定(防止碰撞或安全隐患)
|
||||
$existing = User::where('wxid', $wxid)->where('id', '!=', $user->id)->first();
|
||||
if ($existing) {
|
||||
$apiService->sendTextMessage($wxid, "❌ 绑定失败:当前微信号已经被其他账号 [{$existing->username}] 绑定。请先解绑后再试。");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$user->wxid = $wxid;
|
||||
$user->save();
|
||||
|
||||
// 验证成功后立即销毁验证码
|
||||
Cache::forget($cacheKey);
|
||||
|
||||
$this->info("用户 [{$username}] 成功绑定微信: {$wxid}");
|
||||
|
||||
$successMsg = "🎉 绑定成功!\n"
|
||||
."您已成功绑定聊天室账号:[{$username}]。\n"
|
||||
.'现在您可以接收重要系统通知了。';
|
||||
|
||||
$apiService->sendTextMessage($wxid, $successMsg);
|
||||
}
|
||||
}
|
||||
78
app/Console/Commands/WechatBotTestSend.php
Normal file
78
app/Console/Commands/WechatBotTestSend.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:测试发送微信机器人消息
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\SysParam;
|
||||
use App\Services\WechatBot\WechatBotApiService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class WechatBotTestSend extends Command
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'wechat-bot:test-send';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '测试发送一条消息给管理员设定的微信群群 wxid';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): int
|
||||
{
|
||||
$this->info('开始测试微信机器人发送...');
|
||||
|
||||
$param = SysParam::where('alias', 'wechat_bot_config')->first();
|
||||
if (! $param || empty($param->body)) {
|
||||
$this->error('错误:未找到 wechat_bot_config 配置,请先在后台保存一次配置。');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$config = json_decode($param->body, true);
|
||||
$targetWxid = $config['group_notify']['target_wxid'] ?? '';
|
||||
|
||||
if (empty($targetWxid)) {
|
||||
$this->error('错误:请于后台填写【目标微信群 Wxid】。');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
if (empty($config['api']['bot_key'] ?? '')) {
|
||||
$this->error('错误:未配置【机器人 Key (必需)】,API请求将被拒绝(返回该链接不存在)。');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$service = new WechatBotApiService;
|
||||
|
||||
$this->info("发送目标: {$targetWxid}");
|
||||
$this->info('发送 API Base: '.($config['api']['base_url'] ?? ''));
|
||||
|
||||
$message = "【系统连通性测试】\n发送时间:".now()->format('Y-m-d H:i:s')."\n如果您看到了这条消息,说明 ChatRoom 通知全站群发接口配置正确!";
|
||||
|
||||
$result = $service->sendTextMessage($targetWxid, $message);
|
||||
|
||||
if ($result['success']) {
|
||||
$this->info('✅ 发送成功!');
|
||||
|
||||
return self::SUCCESS;
|
||||
} else {
|
||||
$this->error('❌ 发送失败:'.($result['error'] ?? '未知错误'));
|
||||
$this->warn('如果提示『该链接不存在』代表您的基础API URL 或接入 Key 有误。');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
128
app/Http/Controllers/Admin/WechatBotController.php
Normal file
128
app/Http/Controllers/Admin/WechatBotController.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:微信机器人配置控制器
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\SysParam;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class WechatBotController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示微信机器人配置表单
|
||||
*/
|
||||
public function edit(): View
|
||||
{
|
||||
// 从 SysParam 获取配置,若不存在赋予默认空 JSON
|
||||
$param = SysParam::firstOrCreate(
|
||||
['alias' => 'wechat_bot_config'],
|
||||
[
|
||||
'body' => json_encode([
|
||||
'kafka' => [
|
||||
'brokers' => '',
|
||||
'topic' => '',
|
||||
'group_id' => 'chatroom_wechat_bot',
|
||||
'bot_wxid' => '',
|
||||
],
|
||||
'api' => [
|
||||
'base_url' => '',
|
||||
'bot_key' => '',
|
||||
],
|
||||
'group_notify' => [
|
||||
'target_wxid' => '',
|
||||
'toggle_admin_online' => false,
|
||||
'toggle_baccarat_result' => false,
|
||||
'toggle_lottery_result' => false,
|
||||
],
|
||||
'personal_notify' => [
|
||||
'toggle_friend_online' => false,
|
||||
'toggle_spouse_online' => false,
|
||||
'toggle_level_change' => false,
|
||||
],
|
||||
]),
|
||||
'guidetxt' => '微信机器人全站配置(包含群聊推送和私聊推送开关及Kafka连接)',
|
||||
]
|
||||
);
|
||||
|
||||
$config = json_decode($param->body, true);
|
||||
|
||||
return view('admin.wechat_bot.edit', compact('config'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新微信机器人配置
|
||||
*/
|
||||
public function update(Request $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'kafka_brokers' => 'nullable|string',
|
||||
'kafka_topic' => 'nullable|string',
|
||||
'kafka_group_id' => 'nullable|string',
|
||||
'kafka_bot_wxid' => 'nullable|string',
|
||||
'api_base_url' => 'nullable|string',
|
||||
'api_bot_key' => 'nullable|string',
|
||||
'qrcode_image' => 'nullable|image|max:2048',
|
||||
'group_target_wxid' => 'nullable|string',
|
||||
'toggle_admin_online' => 'nullable|boolean',
|
||||
'toggle_baccarat_result' => 'nullable|boolean',
|
||||
'toggle_lottery_result' => 'nullable|boolean',
|
||||
'toggle_friend_online' => 'nullable|boolean',
|
||||
'toggle_spouse_online' => 'nullable|boolean',
|
||||
'toggle_level_change' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$param = SysParam::where('alias', 'wechat_bot_config')->first();
|
||||
$oldConfig = $param ? (json_decode($param->body, true) ?? []) : [];
|
||||
|
||||
$qrcodePath = $oldConfig['api']['qrcode_image'] ?? '';
|
||||
if ($request->hasFile('qrcode_image')) {
|
||||
// 删除旧图
|
||||
if ($qrcodePath && \Illuminate\Support\Facades\Storage::disk('public')->exists($qrcodePath)) {
|
||||
\Illuminate\Support\Facades\Storage::disk('public')->delete($qrcodePath);
|
||||
}
|
||||
$qrcodePath = $request->file('qrcode_image')->store('wechat', 'public');
|
||||
}
|
||||
|
||||
$config = [
|
||||
'kafka' => [
|
||||
'brokers' => $validated['kafka_brokers'] ?? '',
|
||||
'topic' => $validated['kafka_topic'] ?? '',
|
||||
'group_id' => $validated['kafka_group_id'] ?? 'chatroom_wechat_bot',
|
||||
'bot_wxid' => $validated['kafka_bot_wxid'] ?? '',
|
||||
],
|
||||
'api' => [
|
||||
'base_url' => $validated['api_base_url'] ?? '',
|
||||
'bot_key' => $validated['api_bot_key'] ?? '',
|
||||
'qrcode_image' => $qrcodePath,
|
||||
],
|
||||
'group_notify' => [
|
||||
'target_wxid' => $validated['group_target_wxid'] ?? '',
|
||||
'toggle_admin_online' => $validated['toggle_admin_online'] ?? false,
|
||||
'toggle_baccarat_result' => $validated['toggle_baccarat_result'] ?? false,
|
||||
'toggle_lottery_result' => $validated['toggle_lottery_result'] ?? false,
|
||||
],
|
||||
'personal_notify' => [
|
||||
'toggle_friend_online' => $validated['toggle_friend_online'] ?? false,
|
||||
'toggle_spouse_online' => $validated['toggle_spouse_online'] ?? false,
|
||||
'toggle_level_change' => $validated['toggle_level_change'] ?? false,
|
||||
],
|
||||
];
|
||||
|
||||
if ($param) {
|
||||
$param->update(['body' => json_encode($config)]);
|
||||
SysParam::clearCache('wechat_bot_config');
|
||||
}
|
||||
|
||||
return redirect()->route('admin.wechat_bot.edit')->with('success', '机器相关配置已更新完成。如修改了Kafka请重启后端监听队列守护进程。');
|
||||
}
|
||||
}
|
||||
@@ -160,6 +160,16 @@ class AuthController extends Controller
|
||||
'sdate' => now(),
|
||||
'uuname' => $user->username,
|
||||
]);
|
||||
|
||||
// 触发微信机器人消息推送 (登录上线类)
|
||||
try {
|
||||
$wechatService = new \App\Services\WechatBot\WechatNotificationService;
|
||||
$wechatService->notifyAdminOnline($user);
|
||||
$wechatService->notifyFriendsOnline($user);
|
||||
$wechatService->notifySpouseOnline($user);
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::error('WechatBot presence notification failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -220,6 +220,45 @@ class UserController extends Controller
|
||||
return response()->json(['status' => 'success', 'message' => '密码已成功修改。下次请使用新密码登录。']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成微信绑定代码
|
||||
*/
|
||||
public function generateWechatCode(\Illuminate\Http\Request $request): JsonResponse
|
||||
{
|
||||
$user = \Illuminate\Support\Facades\Auth::user();
|
||||
if (! $user) {
|
||||
return response()->json(['status' => 'error', 'message' => '未登录']);
|
||||
}
|
||||
|
||||
$code = 'BD-'.mt_rand(100000, 999999);
|
||||
\Illuminate\Support\Facades\Cache::put('wechat_bind_code:'.$code, $user->username, 300); // 5分钟有效
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'code' => $code,
|
||||
'message' => '生成成功',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消绑定微信
|
||||
*/
|
||||
public function unbindWechat(\Illuminate\Http\Request $request): JsonResponse
|
||||
{
|
||||
$user = \Illuminate\Support\Facades\Auth::user();
|
||||
if (! $user) {
|
||||
return response()->json(['status' => 'error', 'message' => '未登录']);
|
||||
}
|
||||
|
||||
$user->wxid = null;
|
||||
$user->save();
|
||||
|
||||
return response()->json([
|
||||
'status' => 'success',
|
||||
'message' => '解绑成功',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用权限校验:检查操作者是否有权操作目标用户
|
||||
*
|
||||
|
||||
@@ -97,6 +97,7 @@ class CloseBaccaratRoundJob implements ShouldQueue
|
||||
|
||||
DB::transaction(function () use ($bets, $result, $config, $currency, &$totalPayout, &$winners, &$losers) {
|
||||
foreach ($bets as $bet) {
|
||||
/** @var \App\Models\BaccaratBet $bet */
|
||||
$username = $bet->user->username ?? '匿名';
|
||||
|
||||
if ($result === 'kill') {
|
||||
@@ -223,5 +224,13 @@ class CloseBaccaratRoundJob implements ShouldQueue
|
||||
$chatState->pushMessage(1, $msg);
|
||||
broadcast(new MessageSent(1, $msg));
|
||||
SaveMessageJob::dispatch($msg);
|
||||
|
||||
// 触发微信机器人消息推送 (百家乐结果)
|
||||
try {
|
||||
$wechatService = new \App\Services\WechatBot\WechatNotificationService;
|
||||
$wechatService->notifyBaccaratResult($content);
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::error('WechatBot baccarat notification failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
79
app/Jobs/SendWechatBotMessage.php
Normal file
79
app/Jobs/SendWechatBotMessage.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:异步发送微信机器人消息任务
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\SysParam;
|
||||
use App\Services\WechatBot\WechatBotApiService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class SendWechatBotMessage implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* @var string 目标 wxid 或 群组 ID (@chatroom)
|
||||
*/
|
||||
protected string $target;
|
||||
|
||||
/**
|
||||
* @var string 要发送的消息内容
|
||||
*/
|
||||
protected string $message;
|
||||
|
||||
/**
|
||||
* 队列任务构造函数
|
||||
*
|
||||
* @param string $target 目标用户或群聊
|
||||
* @param string $message 正文内容
|
||||
*/
|
||||
public function __construct(string $target, string $message)
|
||||
{
|
||||
$this->target = $target;
|
||||
$this->message = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行任务
|
||||
*/
|
||||
public function handle(WechatBotApiService $apiService): void
|
||||
{
|
||||
if (empty($this->target)) {
|
||||
Log::warning('WechatBot: Target is empty, skipping message dispatch.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$params = SysParam::where('alias', 'wechat_bot_config')->first();
|
||||
if ($params && ! empty($params->body)) {
|
||||
$config = json_decode($params->body, true);
|
||||
$isEnabled = $config['global_enabled'] ?? false;
|
||||
|
||||
if (! $isEnabled) {
|
||||
return; // 全局未开启,直接抛弃不发
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$apiService->sendTextMessage($this->target, $this->message);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('WechatBot: Failed to send message in queue', [
|
||||
'target' => $this->target,
|
||||
'message' => $this->message,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -447,6 +447,14 @@ class LotteryService
|
||||
$content = "🎟️ 【双色球 第{$issue->issue_no}期 开奖】{$drawNums} {$line1}{$detailStr}";
|
||||
|
||||
$this->pushSystemMessage($content);
|
||||
|
||||
// 触发微信机器人消息推送 (彩票开奖)
|
||||
try {
|
||||
$wechatService = app(\App\Services\WechatBot\WechatNotificationService::class);
|
||||
$wechatService->notifyLotteryResult($content);
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::error('WechatBot lottery notification failed', ['error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
133
app/Services/WechatBot/KafkaConsumerService.php
Normal file
133
app/Services/WechatBot/KafkaConsumerService.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:Kafka 消费者服务
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Services\WechatBot;
|
||||
|
||||
use App\Models\SysParam;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use longlang\phpkafka\Consumer\Consumer;
|
||||
use longlang\phpkafka\Consumer\ConsumerConfig;
|
||||
|
||||
class KafkaConsumerService
|
||||
{
|
||||
/**
|
||||
* Kafka Broker 地址
|
||||
*/
|
||||
protected string $brokers = '';
|
||||
|
||||
/**
|
||||
* 消费 Topic
|
||||
*/
|
||||
protected string $topic = '';
|
||||
|
||||
/**
|
||||
* 消费者组 ID
|
||||
*/
|
||||
protected string $groupId = '';
|
||||
|
||||
/**
|
||||
* 构造函数 — 从 SysParam 获取配置
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$param = SysParam::where('alias', 'wechat_bot_config')->first();
|
||||
if ($param && ! empty($param->body)) {
|
||||
$config = json_decode($param->body, true);
|
||||
$this->brokers = $config['kafka']['brokers'] ?? '';
|
||||
$this->topic = $config['kafka']['topic'] ?? '';
|
||||
$this->groupId = $config['kafka']['group_id'] ?? 'chatroom_wechat_bot';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Kafka 消费者实例
|
||||
*/
|
||||
public function createConsumer(): ?Consumer
|
||||
{
|
||||
if (empty($this->brokers) || empty($this->topic)) {
|
||||
Log::warning('WechatBot Kafka: brokers or topic is empty. Consumer not started.');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$config = new ConsumerConfig;
|
||||
$config->setBroker($this->brokers);
|
||||
$config->setTopic($this->topic);
|
||||
$config->setGroupId($this->groupId);
|
||||
$config->setClientId('chatroom_wechat_bot_'.getmypid().'_'.uniqid());
|
||||
$config->setGroupInstanceId('chatroom_wechat_bot_instance_'.getmypid().'_'.uniqid());
|
||||
$config->setInterval(0.5); // 拉取间隔(秒)
|
||||
$config->setGroupRetry(5); // 组协调重试次数
|
||||
$config->setGroupRetrySleep(1); // 组协调重试间隔(秒)
|
||||
|
||||
return new Consumer($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 Kafka 原始消息,提取有效的聊天消息列表
|
||||
*
|
||||
* @param string $rawJson Kafka 消息体 JSON 字符串
|
||||
* @return array 解析后的消息数组
|
||||
*/
|
||||
public function parseKafkaMessage(string $rawJson): array
|
||||
{
|
||||
try {
|
||||
$data = json_decode($rawJson, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (\JsonException $e) {
|
||||
Log::warning('微信机器人 Kafka 消息 JSON 解析失败', ['error' => $e->getMessage()]);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// 只处理包含 AddMsgs 的消息
|
||||
$addMsgs = $data['AddMsgs'] ?? [];
|
||||
if (empty($addMsgs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$messages = [];
|
||||
foreach ($addMsgs as $msg) {
|
||||
$fromUser = $msg['from_user_name']['str'] ?? '';
|
||||
$toUser = $msg['to_user_name']['str'] ?? '';
|
||||
$msgType = $msg['msg_type'] ?? 0;
|
||||
$content = $msg['content']['str'] ?? '';
|
||||
|
||||
// 判断是否来自群聊
|
||||
$isChatroom = str_contains($fromUser, '@chatroom');
|
||||
|
||||
// 群聊消息中,from_user 是群ID,实际发送者在 content 中以 "wxid:\n内容" 格式出现
|
||||
$actualSender = $fromUser;
|
||||
$actualContent = $content;
|
||||
$chatroomId = null;
|
||||
|
||||
if ($isChatroom) {
|
||||
$chatroomId = $fromUser;
|
||||
// 解析群聊消息格式: "发送者wxid:\n实际内容"
|
||||
if (preg_match('/^(.+?):\n(.*)$/s', $content, $matches)) {
|
||||
$actualSender = $matches[1];
|
||||
$actualContent = $matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
$messages[] = [
|
||||
'msg_id' => $msg['new_msg_id'] ?? $msg['msg_id'] ?? 0,
|
||||
'from_user' => $actualSender,
|
||||
'to_user' => $toUser,
|
||||
'chatroom_id' => $chatroomId,
|
||||
'msg_type' => $msgType,
|
||||
'content' => $actualContent,
|
||||
'raw_content' => $content,
|
||||
'is_chatroom' => $isChatroom,
|
||||
];
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
}
|
||||
125
app/Services/WechatBot/WechatBotApiService.php
Normal file
125
app/Services/WechatBot/WechatBotApiService.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:微信机器人接口调用服务
|
||||
*
|
||||
* 封装与微信机器人 HTTP API 的通信逻辑,用于向上游服务发送文本和富文本消息。
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Services\WechatBot;
|
||||
|
||||
use App\Models\SysParam;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class WechatBotApiService
|
||||
{
|
||||
/**
|
||||
* @var string API 基础地址
|
||||
*/
|
||||
protected string $baseUrl;
|
||||
|
||||
/**
|
||||
* @var int 请求超时时间(秒)
|
||||
*/
|
||||
protected int $timeout = 10;
|
||||
|
||||
/**
|
||||
* @var string 机器人 Key
|
||||
*/
|
||||
protected string $botKey;
|
||||
|
||||
/**
|
||||
* 构造函数 — 从 SysParam 获取 api_base_url 配置
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$param = SysParam::where('alias', 'wechat_bot_config')->first();
|
||||
if ($param && ! empty($param->body)) {
|
||||
$config = json_decode($param->body, true);
|
||||
$this->baseUrl = rtrim($config['api']['base_url'] ?? '', '/');
|
||||
$this->botKey = $config['api']['bot_key'] ?? '';
|
||||
} else {
|
||||
$this->baseUrl = '';
|
||||
$this->botKey = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送文本消息
|
||||
*
|
||||
* @param string $toUser 目标用户 wxid 或群聊 ID
|
||||
* @param string $content 文本内容
|
||||
* @param array $atList 群聊中 @ 的用户 wxid 列表
|
||||
* @return array{success: bool, data: array|null, error: string|null}
|
||||
*/
|
||||
public function sendTextMessage(string $toUser, string $content, array $atList = []): array
|
||||
{
|
||||
if (empty($this->baseUrl)) {
|
||||
return ['success' => false, 'data' => null, 'error' => 'API Base URL is not configured.'];
|
||||
}
|
||||
|
||||
$url = "{$this->baseUrl}/message/SendTextMessage";
|
||||
|
||||
$finalContent = "[和平聊吧]\n".$content;
|
||||
|
||||
$payload = [
|
||||
'MsgItem' => [
|
||||
[
|
||||
'AtWxIDList' => $atList,
|
||||
'ImageContent' => '',
|
||||
'MsgType' => 0,
|
||||
'TextContent' => $finalContent,
|
||||
'ToUserName' => $toUser,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
$response = Http::timeout($this->timeout)
|
||||
->withHeaders([
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'application/json',
|
||||
])
|
||||
->post("{$url}?key={$this->botKey}", $payload);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
|
||||
if (($data['Code'] ?? 0) === 200) {
|
||||
Log::info('微信机器人消息发送成功', [
|
||||
'to_user' => $toUser,
|
||||
'content' => mb_substr($content, 0, 50),
|
||||
]);
|
||||
|
||||
return ['success' => true, 'data' => $data['Data'] ?? [], 'error' => null];
|
||||
}
|
||||
|
||||
$desc = $data['Text'] ?? 'Unknown';
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'data' => null,
|
||||
'error' => "API 返回错误: Code={$data['Code']}, Text={$desc}",
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'data' => null,
|
||||
'error' => "HTTP 请求失败: {$response->status()}",
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error('微信机器人消息发送异常', [
|
||||
'to_user' => $toUser,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return ['success' => false, 'data' => null, 'error' => '发送异常: '.$e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
194
app/Services/WechatBot/WechatNotificationService.php
Normal file
194
app/Services/WechatBot/WechatNotificationService.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 文件功能:微信消息发送核心业务逻辑服务
|
||||
*
|
||||
* @author ChatRoom Laravel
|
||||
*
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
namespace App\Services\WechatBot;
|
||||
|
||||
use App\Jobs\SendWechatBotMessage;
|
||||
use App\Models\SysParam;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class WechatNotificationService
|
||||
{
|
||||
protected array $config = [];
|
||||
|
||||
protected bool $globalEnabled = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$param = SysParam::where('alias', 'wechat_bot_config')->first();
|
||||
if ($param && ! empty($param->body)) {
|
||||
$this->config = json_decode($param->body, true) ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员上线群通知
|
||||
*/
|
||||
public function notifyAdminOnline(User $user): void
|
||||
{
|
||||
if (empty($this->config['group_notify']['toggle_admin_online'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$groupWxid = $this->config['group_notify']['target_wxid'] ?? '';
|
||||
if (! $groupWxid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断是否真的是管理员(这里假设大于等于某等级,比如 15)
|
||||
$adminLevel = (int) Sysparam::getValue('level_kick', '15');
|
||||
if ($user->user_level < $adminLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = "👑 【管理员上线】\n"
|
||||
."管理员 [{$user->username}] 刚刚登录了系统。";
|
||||
|
||||
SendWechatBotMessage::dispatch($groupWxid, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 百家乐开奖群通知
|
||||
*/
|
||||
public function notifyBaccaratResult(string $historyText): void
|
||||
{
|
||||
if (empty($this->config['group_notify']['toggle_baccarat_result'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$groupWxid = $this->config['group_notify']['target_wxid'] ?? '';
|
||||
if (! $groupWxid) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = "🎰 【百家乐开奖】\n{$historyText}";
|
||||
SendWechatBotMessage::dispatch($groupWxid, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 彩票开奖群通知
|
||||
*/
|
||||
public function notifyLotteryResult(string $historyText): void
|
||||
{
|
||||
if (empty($this->config['group_notify']['toggle_lottery_result'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$groupWxid = $this->config['group_notify']['target_wxid'] ?? '';
|
||||
if (! $groupWxid) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = "🎲 【彩票开奖】\n{$historyText}";
|
||||
SendWechatBotMessage::dispatch($groupWxid, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 好友上线私聊通知(带冷却)
|
||||
*/
|
||||
public function notifyFriendsOnline(User $user): void
|
||||
{
|
||||
if (empty($this->config['personal_notify']['toggle_friend_online'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cacheKey = "wechat_notify_cd:friend_online:{$user->id}";
|
||||
if (Redis::exists($cacheKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 假定有好友关系模型 Friends (视具体业务而定,目前先预留或者查询好友)
|
||||
$friends = $this->getUserFriends($user->id);
|
||||
|
||||
foreach ($friends as $friend) {
|
||||
if ($friend->wxid) {
|
||||
$message = "👋 【好友上线】\n您的好友 [{$user->username}] 刚刚上线了!";
|
||||
SendWechatBotMessage::dispatch($friend->wxid, $message);
|
||||
}
|
||||
}
|
||||
|
||||
// 冷却 30 分钟
|
||||
Redis::setex($cacheKey, 1800, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 夫妻上线私聊通知(带冷却)
|
||||
*/
|
||||
public function notifySpouseOnline(User $user): void
|
||||
{
|
||||
if (empty($this->config['personal_notify']['toggle_spouse_online'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cacheKey = "wechat_notify_cd:spouse_online:{$user->id}";
|
||||
if (Redis::exists($cacheKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取伴侣
|
||||
$spouse = $this->getUserSpouse($user);
|
||||
if ($spouse && $spouse->wxid) {
|
||||
$message = "❤️ 【伴侣上线】\n您的伴侣 [{$user->username}] 刚刚上线了!";
|
||||
SendWechatBotMessage::dispatch($spouse->wxid, $message);
|
||||
// 冷却 30 分钟
|
||||
Redis::setex($cacheKey, 1800, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 等级变动私聊通知
|
||||
*/
|
||||
public function notifyLevelChange(User $user, int $oldLevel, int $newLevel): void
|
||||
{
|
||||
if (empty($this->config['personal_notify']['toggle_level_change'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $user->wxid || $newLevel <= $oldLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = "✨ 【等级提升】\n"
|
||||
."恭喜您!您的聊天室等级已从 LV{$oldLevel} 提升至 LV{$newLevel}!";
|
||||
|
||||
SendWechatBotMessage::dispatch($user->wxid, $message);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Helper Methods (Mocking the real data retrieval methods)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
protected function getUserFriends(int $userId)
|
||||
{
|
||||
// 假定有好友表
|
||||
if (\Illuminate\Support\Facades\Schema::hasTable('friends')) {
|
||||
return \Illuminate\Support\Facades\DB::table('friends')
|
||||
->join('users', 'users.id', '=', 'friends.friend_id')
|
||||
->where('friends.user_id', $userId)
|
||||
->whereNotNull('users.wxid')
|
||||
->select('users.*')
|
||||
->get();
|
||||
}
|
||||
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
protected function getUserSpouse(User $user)
|
||||
{
|
||||
// 如果有配偶字段
|
||||
$mateName = $user->peiou ?? null;
|
||||
if ($mateName && $mateName !== '无' && $mateName !== '') {
|
||||
return User::where('username', $mateName)->whereNotNull('wxid')->first();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
"laravel/horizon": "^5.45",
|
||||
"laravel/reverb": "^1.8",
|
||||
"laravel/tinker": "^2.10.1",
|
||||
"longlang/phpkafka": "^1.2",
|
||||
"mews/captcha": "^3.4",
|
||||
"predis/predis": "^3.4",
|
||||
"stevebauman/location": "^7.6",
|
||||
|
||||
284
composer.lock
generated
284
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "56338775768722c90ec723eb5b939be1",
|
||||
"content-hash": "2c7aa959e462c8c80ba7e8ed493a2af3",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
@@ -135,6 +135,72 @@
|
||||
],
|
||||
"time": "2024-02-09T16:56:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "chdemko/sorted-collections",
|
||||
"version": "1.0.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/chdemko/php-sorted-collections.git",
|
||||
"reference": "d9cf7021e6fda1eb68b9f35caf99215327f6db76"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/chdemko/php-sorted-collections/zipball/d9cf7021e6fda1eb68b9f35caf99215327f6db76",
|
||||
"reference": "d9cf7021e6fda1eb68b9f35caf99215327f6db76",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"php-coveralls/php-coveralls": "^2.7",
|
||||
"phpbench/phpbench": "^1.3",
|
||||
"phpunit/phpunit": "^11.3",
|
||||
"squizlabs/php_codesniffer": "^3.10"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"chdemko\\SortedCollection\\": "src/SortedCollection"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christophe Demko",
|
||||
"email": "chdemko@gmail.com",
|
||||
"homepage": "https://chdemko.pagelab.univ-lr.fr/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Sorted Collections for PHP >= 8.1",
|
||||
"homepage": "https://php-sorted-collections.readthedocs.io/en/latest/?badge=latest",
|
||||
"keywords": [
|
||||
"avl",
|
||||
"collection",
|
||||
"iterator",
|
||||
"map",
|
||||
"ordered",
|
||||
"set",
|
||||
"sorted",
|
||||
"tree",
|
||||
"treemap",
|
||||
"treeset"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/chdemko/php-sorted-collections/issues",
|
||||
"source": "https://github.com/chdemko/php-sorted-collections/tree/1.0.10"
|
||||
},
|
||||
"time": "2024-08-04T14:31:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "clue/redis-protocol",
|
||||
"version": "v0.3.2",
|
||||
@@ -757,6 +823,50 @@
|
||||
},
|
||||
"time": "2023-08-08T05:53:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "exussum12/xxhash",
|
||||
"version": "1.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/exussum12/xxhash.git",
|
||||
"reference": "f5567ec5739ffee27aa3469e5001b4759b1b9e0d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/exussum12/xxhash/zipball/f5567ec5739ffee27aa3469e5001b4759b1b9e0d",
|
||||
"reference": "f5567ec5739ffee27aa3469e5001b4759b1b9e0d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-bcmath": "*",
|
||||
"php": ">=7.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.1|^8.0|^9.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"exussum12\\xxhash\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Scott Dutton",
|
||||
"email": "scott@exussum.co.uk"
|
||||
}
|
||||
],
|
||||
"description": "xxHash implementation in Pure PHP",
|
||||
"support": {
|
||||
"issues": "https://github.com/exussum12/xxhash/issues",
|
||||
"source": "https://github.com/exussum12/xxhash/tree/1.3"
|
||||
},
|
||||
"time": "2020-11-30T09:04:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fruitcake/php-cors",
|
||||
"version": "v1.4.0",
|
||||
@@ -886,6 +996,53 @@
|
||||
},
|
||||
"time": "2025-11-20T18:50:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "google/crc32",
|
||||
"version": "v0.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/google/php-crc32.git",
|
||||
"reference": "a8525f0dea6fca1893e1bae2f6e804c5f7d007fb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/google/php-crc32/zipball/a8525f0dea6fca1893e1bae2f6e804c5f7d007fb",
|
||||
"reference": "a8525f0dea6fca1893e1bae2f6e804c5f7d007fb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^1.13 || v2.14.2",
|
||||
"paragonie/random_compat": ">=2",
|
||||
"phpunit/phpunit": "^4"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Google\\CRC32\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Andrew Brampton",
|
||||
"email": "bramp@google.com"
|
||||
}
|
||||
],
|
||||
"description": "Various CRC32 implementations",
|
||||
"homepage": "https://github.com/google/php-crc32",
|
||||
"support": {
|
||||
"issues": "https://github.com/google/php-crc32/issues",
|
||||
"source": "https://github.com/google/php-crc32/tree/v0.1.0"
|
||||
},
|
||||
"abandoned": true,
|
||||
"time": "2019-05-09T06:24:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "graham-campbell/result-type",
|
||||
"version": "v1.1.4",
|
||||
@@ -2688,6 +2845,51 @@
|
||||
],
|
||||
"time": "2026-01-15T06:54:53+00:00"
|
||||
},
|
||||
{
|
||||
"name": "longlang/phpkafka",
|
||||
"version": "v1.2.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/swoole/phpkafka.git",
|
||||
"reference": "f42bfa7c97b1f2c751e2a956e597b970407faad1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/swoole/phpkafka/zipball/f42bfa7c97b1f2c751e2a956e597b970407faad1",
|
||||
"reference": "f42bfa7c97b1f2c751e2a956e597b970407faad1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"chdemko/sorted-collections": "^1.0",
|
||||
"exussum12/xxhash": "^1.0.0",
|
||||
"google/crc32": "^0.1.0",
|
||||
"php": ">=7.1",
|
||||
"symfony/polyfill-php81": "^1.23"
|
||||
},
|
||||
"require-dev": {
|
||||
"colinodell/json5": "^2.1",
|
||||
"friendsofphp/php-cs-fixer": "^2.18",
|
||||
"phpstan/phpstan": "^0.12.81",
|
||||
"phpunit/phpunit": "^7.5|^8.0|^9.0",
|
||||
"swoole/ide-helper": "^4.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"longlang\\phpkafka\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"Apache-2.0"
|
||||
],
|
||||
"description": "A kafka client. Support php-fpm and Swoole.",
|
||||
"support": {
|
||||
"issues": "https://github.com/swoole/phpkafka/issues",
|
||||
"source": "https://github.com/swoole/phpkafka/tree/v1.2.5"
|
||||
},
|
||||
"time": "2023-10-13T01:40:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "maxmind-db/reader",
|
||||
"version": "v1.13.1",
|
||||
@@ -6531,6 +6733,86 @@
|
||||
],
|
||||
"time": "2025-01-02T08:10:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php81",
|
||||
"version": "v1.33.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/polyfill-php81.git",
|
||||
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
|
||||
"reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"thanks": {
|
||||
"url": "https://github.com/symfony/polyfill",
|
||||
"name": "symfony/polyfill"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Symfony\\Polyfill\\Php81\\": ""
|
||||
},
|
||||
"classmap": [
|
||||
"Resources/stubs"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicolas Grekas",
|
||||
"email": "p@tchwork.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"compatibility",
|
||||
"polyfill",
|
||||
"portable",
|
||||
"shim"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/nicolas-grekas",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-09T11:45:10+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-php83",
|
||||
"version": "v1.33.0",
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (! Schema::hasColumn('users', 'wxid')) {
|
||||
$table->string('wxid', 100)->nullable()->unique()->after('username')->comment('绑定的微信ID');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('users', 'wxid')) {
|
||||
$table->dropColumn('wxid');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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' }}">
|
||||
{!! '💴 用户流水' !!}
|
||||
|
||||
121
resources/views/admin/wechat_bot/edit.blade.php
Normal file
121
resources/views/admin/wechat_bot/edit.blade.php
Normal file
@@ -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>
|
||||
|
||||
@@ -65,6 +65,8 @@ Route::middleware(['chat.auth'])->group(function () {
|
||||
// ---- 第七阶段:用户资料与特权管理 ----
|
||||
Route::get('/user/{username}', [UserController::class, 'show'])->name('user.show');
|
||||
Route::put('/user/profile', [UserController::class, 'updateProfile'])->name('user.update_profile');
|
||||
Route::post('/user/generate-wechat-code', [UserController::class, 'generateWechatCode'])->name('user.generate_wechat_code');
|
||||
Route::post('/user/unbind-wechat', [UserController::class, 'unbindWechat'])->name('user.unbind_wechat');
|
||||
Route::post('/user/send-email-code', [\App\Http\Controllers\Api\VerificationController::class, 'sendEmailCode'])->name('user.send_email_code');
|
||||
Route::put('/user/password', [UserController::class, 'changePassword'])->name('user.update_password');
|
||||
Route::post('/user/{username}/kick', [UserController::class, 'kick'])->name('user.kick');
|
||||
@@ -381,6 +383,10 @@ Route::middleware(['chat.auth', 'chat.has_position'])->prefix('admin')->name('ad
|
||||
Route::get('/system', [\App\Http\Controllers\Admin\SystemController::class, 'edit'])->name('system.edit');
|
||||
Route::put('/system', [\App\Http\Controllers\Admin\SystemController::class, 'update'])->name('system.update');
|
||||
|
||||
// 微信机器人配置
|
||||
Route::get('/wechat-bot', [\App\Http\Controllers\Admin\WechatBotController::class, 'edit'])->name('wechat_bot.edit');
|
||||
Route::put('/wechat-bot', [\App\Http\Controllers\Admin\WechatBotController::class, 'update'])->name('wechat_bot.update');
|
||||
|
||||
// 运维工具(仅 id=1 超管可用)
|
||||
Route::get('/ops', [\App\Http\Controllers\Admin\OpsController::class, 'index'])->name('ops.index');
|
||||
Route::post('/ops/clear-cache', [\App\Http\Controllers\Admin\OpsController::class, 'clearCache'])->name('ops.clear-cache');
|
||||
|
||||
Reference in New Issue
Block a user