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
+8
View File
@@ -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. 向用户私人推送"系统为你自动存点"信息,在其聊天框显示
@@ -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);
}
}
@@ -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;
}
}
}
@@ -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请重启后端监听队列守护进程。');
}
}
+10
View File
@@ -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()]);
}
}
/**
+39
View File
@@ -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' => '解绑成功',
]);
}
/**
* 通用权限校验:检查操作者是否有权操作目标用户
*
+9
View File
@@ -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
View 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(),
]);
}
}
}
+8
View File
@@ -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()]);
}
}
/**
@@ -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;
}
}
@@ -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()];
}
}
}
@@ -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;
}
}