Merge commit 'ed96e0f8589042fb8e9d6cfee713c5150612bb34'

This commit is contained in:
mzl
2025-08-22 18:16:13 +08:00
parent d715708728
commit 11165fcfb9
23 changed files with 1465 additions and 32 deletions
@@ -0,0 +1,52 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\MessageNotificationsService;
use app\common\model\MessageNotifications;
use support\Request;
class MessageNotificationsController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new MessageNotificationsService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function indexFormat($data)
{
if (isset($data['created_at']))
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
return $data;
}
protected function showFormat($data)
{
if (isset($data['created_at']))
$data['created_at'] = date('Y-m-d H:i:s', $data['created_at']);
return $data;
}
public function push(Request $request)
{
try {
$data = $this->validate($this->service->getValidator(), $request->post(), 'store');
$res = $this->service->servicePush($request, $data);
return $this->success($res, '推送成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
@@ -0,0 +1,95 @@
<?php
namespace app\common\controller;
use app\common\controller\BaseController;
use app\common\traits\crud\CrudControllerTrait;
use app\common\service\MessageUserStatusService;
use support\Request;
class MessageUserStatusController extends BaseController
{
use CrudControllerTrait;
protected $service;
public function __construct()
{
$this->service = new MessageUserStatusService();
}
/**
* 列表页数据格式化
*
* @param array $data
* @return array
*/
protected function indexFormat($data)
{
if (isset($data['read_at']) && $data['read_at'] !== null) {
$data['read_at'] = date('Y-m-d H:i:s', $data['read_at']);
} else {
$data['read_at'] = ' - ';
}
if (isset($data['message']))
$data['message']['created_at'] = date('Y-m-d H:i:s', $data['message']['created_at']);
return $data;
}
protected function showFormat($data)
{
if (isset($data['read_at']) && $data['read_at'] !== null) {
$data['read_at'] = date('Y-m-d H:i:s', $data['read_at']);
} else {
$data['read_at'] = ' - ';
}
if (isset($data['message']))
$data['message']['created_at'] = date('Y-m-d H:i:s', $data['message']['created_at']);
return $data;
}
protected function unReadFormat($data)
{
foreach ($data as &$item) {
if (isset($item['read_at']) && $item['read_at'] !== null) {
$item['read_at'] = date('Y-m-d H:i:s', $item['read_at']);
} else {
$item['read_at'] = ' - ';
}
if (isset($item['message'])) {
$item['message']['created_at'] = date('Y-m-d H:i:s', $item['message']['created_at']);
}
}
return $data;
}
public function read(Request $request)
{
try {
$data = $this->validate($this->service->getValidator(), $request->post(), 'read');
$this->service->serviceRead($request, $data);
return $this->success([], '操作成功');
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
public function listUnRead(Request $request)
{
try {
$raw = $this->service->serviceListUnRead($request);
$data = $this->transformData($raw, method_exists($this, 'unReadFormat')
? [$this, 'unReadFormat']
: null);
$data = [
'list' => $data,
'counts' => count($data)
];
return $this->success($data);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
}
}
@@ -0,0 +1,29 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\MessageNotifications;
class MessageNotificationsLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = MessageNotifications::class;
public function logicDeleteBy($config)
{
$msg = $this->logicShow($config);
if ($msg) {
// 删除关联的用户状态记录
$msg->userStatuses()->delete();
// 删除消息本身
$msg->delete();
}
return true;
}
}
@@ -0,0 +1,17 @@
<?php
namespace app\common\logic;
use app\common\traits\crud\CrudLogicTrait;
use app\common\model\MessageUserStatus;
class MessageUserStatusLogic
{
use CrudLogicTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = MessageUserStatus::class;
}
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace app\common\model;
use support\Model;
class MessageNotifications extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'message_notifications';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
// const UPDATED_AT = false;
// protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'msg_no', 'sender_id', 'sender_type', 'recipient', 'title', 'content', 'scene', 'push_method', 'created_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
// 消息创建
const SCENE = [
self::SCENE0 => '系统消息',
self::SCENE1 => '系统通知'
];
const SCENE0 = 0;
const SCENE1 = 1;
// 推送方式
const PUSH_METHOD = [
self::PUSH_METHOD0 => '系统推送',
self::PUSH_METHOD1 => '手动推送'
];
const PUSH_METHOD0 = 0;
const PUSH_METHOD1 = 1;
/**
* 关联用户消息状态 (一对多)
*/
public function userStatuses()
{
return $this->hasMany(MessageUserStatus::class, 'msg_id', 'id');
}
/**
* 获取已读用户状态
*/
public function readUsers()
{
return $this->hasMany(MessageUserStatus::class, 'msg_id', 'id')
->where('status', 1);
}
/**
* 获取未读用户状态
*/
public function unreadUsers()
{
return $this->hasMany(MessageUserStatus::class, 'msg_id', 'id')
->where('status', 0);
}
public function sender()
{
return $this->belongsTo(User::class, 'sender_id', 'id');
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace app\common\model;
use support\Model;
class MessageUserStatus extends Model
{
/**
* The connection name for the model.
*
* @var string|null
*/
protected $connection = 'mysql';
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'message_user_status';
/**
* The primary key associated with the table.
*
* @var string
*/
protected $primaryKey = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = false;
// protected $dateFormat = 'U';
/**
* 允许自动填充模型字段
*/
protected $fillable = ['id', 'msg_id', 'uid', 'status', 'read_at'];
/**
* 不允许自动填充模型字段
*/
protected $guarded = [];
/**
* 关联消息 (多对一)
*/
public function message()
{
return $this->belongsTo(MessageNotifications::class, 'msg_id', 'id');
}
/**
* 关联用户 (假设你有 User 模型)
*/
public function user()
{
return $this->belongsTo(User::class, 'uid', 'id');
}
}
@@ -0,0 +1,276 @@
<?php
namespace app\common\service;
use app\common\logic\MessageNotificationsLogic;
use app\common\validate\MessageNotificationsValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\MessageNotifications;
use app\common\model\MessageUserStatus;
use app\common\model\User;
use support\Db;
use support\Request;
class MessageNotificationsService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = MessageNotifications::class;
/**
* 验证器
*/
protected $validator = MessageNotificationsValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'index':
$config['order']['id'] = 'desc';
break;
case 'show':
$config['where']['id'] = $p['id'];
break;
case 'store':
break;
case 'update':
$config['where']['id'] = $p['id'];
break;
case 'destroy':
$config['where']['id'] = $p['id'];
break;
}
return $config;
}
public function serviceDestroy(Request $request)
{
$config = $this->buildQueryConfig($request, 'destroy');
$deleted = (new MessageNotificationsLogic())->logicDeleteBy($config);
return $this->postProcess('Destroy', $deleted);
}
/**
* 添加前置处理
*/
public function servicePush(Request $request, $data)
{
$pushData = [
'sender_id' => $request->user->uid,
'sender_type' => 'admin',
'title' => $data['title'],
'content' => $data['content'],
'scene' => 1,
'recipient' => $data['recipient'],
'push_method' => 1
];
$msgId = (new MessageNotificationsService())->pushNotice($pushData);
return ['msg_id' => $msgId];
}
public function afterServiceList($data)
{
$data->each(function (&$item) {
$temp = explode(',', $item->recipient);
$item->recipient_list = User::whereIn('id', $temp)->pluck('name')->toArray();
$item->scene_name = MessageNotifications::SCENE[$item->scene];
$item->push_method_name = MessageNotifications::PUSH_METHOD[$item->push_method];
});
unset($item);
return $data;
}
public function afterServiceShow($data)
{
$temp = explode(',', $data->recipient);
$data->recipient_list = User::whereIn('id', $temp)->pluck('name')->toArray();
$data->scene_name = MessageNotifications::SCENE[$data->scene];
$data->push_method_name = MessageNotifications::PUSH_METHOD[$data->push_method];
return $data;
}
// ====== ====== ====== 通知操作的处理 ====== ====== ======
/**
* 推送消息
*/
public function pushNotice(array $data = [], string $scene = "Default"): int
{
// 拼装数据(可能需要处理 消息标题、内容等等数据,可自行处理)
$pushData = $this->assemblyProcess($scene, $data);
// 写入消息
$msgId = $this->insertMessage($pushData);
// 推送
$this->pushWs($msgId, $pushData);
return $msgId;
}
/**
* 执行推送
*/
public function pushWs(int $msgId, array $pushData)
{
foreach ($pushData['recipient'] as $userId) {
// 查询消息数据
$config['model'] = MessageUserStatus::class;
$config['order']['id'] = 'desc';
$config['with']['message'] = ['sender_id', 'sender_type', 'msg_no', 'title', 'content', 'scene', 'created_at'];
$config['where']['uid'] = $userId;
$config['where']['msg_id'] = $msgId;
$config['where']['status'] = 0;
$wsData = (new MessageUserStatusService())->logicShow($config)->toArray();
// 组装数据
if (isset($wsData['message']) && $wsData['message']) {
$wsData['message']['sender_name'] = User::where('id', $wsData['message']['sender_id'])->value('name') ?: '系统';
$wsData['message']['scene_name'] = MessageNotifications::SCENE[$wsData['message']['scene']];
$wsData['message']['created_at'] = date('Y-m-d H:i:s', $wsData['message']['created_at']);
}
// 获取未读消息数
$counts = MessageUserStatus::where('uid', $userId)->where('status', 0)->count('id');
$data = [
'notice_data' => $wsData,
'unread_counts' => $counts
];
// 执行推送
(new \app\common\utils\WsEvents())->pushWsNotice($data, $userId);
}
return true;
}
/**
* 插入消息
* @param array $data
* @return int
*/
public function insertMessage(array $data): int
{
// 写消息
$msgId = $this->createMessage([
'sender_id' => $data['sender_id'],
'sender_type' => $data['sender_type'],
'title' => $data['title'],
'content' => $data['content'],
'scene' => $data['scene'],
'recipient' => implode(',', $data['recipient']),
'push_method' => $data['push_method'],
]);
// 写用户状态
$this->addUserStatus($msgId, $data['recipient']);
return $msgId;
}
/**
* 数据拼装方法调用
*/
protected function assemblyProcess(string $op, mixed $data): mixed
{
$hook = "assembly{$op}";
return method_exists($this, $hook)
? $this->{$hook}($data)
: $data;
}
/**
* 默认数据拼装
*/
public function assemblyDefault(array $data)
{
return [
'sender_id' => isset($data['sender_id']) && $data['sender_id'] ? $data['sender_id'] : 0,
'sender_type' => isset($data['sender_type']) && $data['sender_type'] ? $data['sender_type'] : 'system',
'title' => isset($data['title']) && $data['title'] ? $data['title'] : '系统通知',
'content' => isset($data['content']) && $data['content'] ? $data['content'] : '系统通知-默认',
'scene' => isset($data['scene']) && $data['scene'] ? $data['scene'] : 1,
'recipient' => isset($data['recipient']) && count($data['recipient']) > 0 ? $data['recipient'] : $this->getAllUserIds(),
'push_method' => isset($data['push_method']) && $data['push_method'] ? $data['push_method'] : 0,
];
}
// *** *** 可以扩展数据组拼装方法 *** ***
// ====== ====== ====== 通知操作的基础方法 ====== ====== ======
/**
* 创建一条消息
*/
public function createMessage(array $data): int
{
$msgId = Db::table('message_notifications')->insertGetId([
'msg_no' => generateMsgId(),
'sender_id' => $data['sender_id'],
'sender_type' => $data['sender_type'],
'title' => $data['title'],
'content' => $data['content'],
'scene' => $data['scene'],
'recipient' => $data['recipient'],
'push_method' => $data['push_method'],
'created_at' => time(),
]);
return $msgId;
}
/**
* 批量插入用户消息状态
*/
public function addUserStatus(int $msgId, array $uids)
{
$rows = [];
foreach ($uids as $uid) {
$rows[] = [
'msg_id' => $msgId,
'uid' => $uid,
'status' => 0,
'read_at' => null
];
}
if ($rows) {
Db::table('message_user_status')->insert($rows);
}
}
/**
* 标记消息已读
*/
public function markAsRead(array|string $msgIds, int $uid)
{
$query = Db::table('message_user_status')->where('uid', $uid)->where('status', 0);
if (is_array($msgIds)) {
$query->whereIn('msg_id', $msgIds);
}
return $query->update([
'status' => 1,
'read_at' => time()
]);
}
/**
* 获取所有用户ID
*/
public function getAllUserIds(): array
{
return User::where('status', 1)->pluck('id')->toArray();
}
}
@@ -0,0 +1,136 @@
<?php
namespace app\common\service;
use app\common\model\MessageNotifications;
use app\common\validate\MessageUserStatusValidate;
use app\common\traits\crud\CrudServiceTrait;
use app\common\model\MessageUserStatus;
use support\Request;
use app\common\model\User;
class MessageUserStatusService
{
use CrudServiceTrait;
/**
* 指定当前要操作的模型
*/
protected string $modelClass = MessageUserStatus::class;
/**
* 验证器
*/
protected $validator = MessageUserStatusValidate::class;
/**
* 获取验证器
*/
public function getValidator()
{
return new $this->validator;
}
protected function buildQueryConfig(Request $request, string $scene = 'default'): array
{
$p = $request->all();
$config = [];
$config['model'] = $this->modelClass;
switch ($scene) {
case 'unread':
$config['order']['id'] = 'desc';
$config['with']['message'] = ['sender_id', 'sender_type', 'msg_no', 'title', 'content', 'scene', 'created_at'];
$config['where']['uid'] = $request->user->uid;
$config['where']['status'] = 0;
break;
case 'index':
$config['order']['id'] = 'desc';
$config['with']['message'] = ['sender_id', 'sender_type', 'msg_no', 'title', 'content', 'scene', 'created_at'];
$config['where']['uid'] = $request->user->uid;
break;
case 'show':
$config['where']['id'] = $p['id'];
$config['with']['message'] = ['sender_id', 'sender_type', 'msg_no', 'title', 'content', 'scene', 'created_at'];
$config['where']['uid'] = $request->user->uid;
break;
case 'store':
break;
case 'update':
case 'read':
$config['where']['id'] = $p['id'];
$config['where']['msg_id'] = $p['msg_id'];
$config['where']['uid'] = $request->user->uid;
break;
case 'destroy':
$config['where']['id'] = $p['id'];
$config['where']['uid'] = $request->user->uid;
break;
}
return $config;
}
public function serviceListUnRead(Request $request)
{
$config = $this->buildQueryConfig($request, 'unread');
$result = $this->logicListAll($config);
return $this->postProcess('ListUnRead', $result);
}
public function serviceRead(Request $request, array $data = [])
{
return $this->noticeRead($request->post('msg_ids'), $request->user->uid);
}
public function noticeRead(array|string $msgIds, int $uid): bool
{
$model = MessageUserStatus::where('uid', $uid)->where('status', 0);
if (is_array($msgIds)) {
$model->whereIn('msg_id', $msgIds);
}
$udata['read_at'] = time();
$udata['status'] = 1;
return $model->update($udata);
}
protected function afterServiceList($res)
{
$res->each(function (&$item) {
if (isset($item->message)) {
$item->message->sender_name = User::where('id', $item->message->sender_id)->value('name') ?: '系统';
$item->message->scene_name = MessageNotifications::SCENE[$item->message->scene];
}
});
unset($item);
return $res;
}
protected function afterServiceShow($res)
{
if (isset($res->message)) {
$res->message->sender_name = User::where('id', $res->message->sender_id)->value('name') ?: '系统';
$res->message->scene_name = MessageNotifications::SCENE[$res->message->scene];
}
// 标记已读
if ($res->status == 0) {
$this->noticeRead([$res->msg_id], $res->uid);
}
return $res;
}
protected function afterServiceListUnRead($res)
{
$res->each(function (&$item) {
if (isset($item->message)) {
$item->message->sender_name = User::where('id', $item->message->sender_id)->value('name') ?: '系统';
$item->message->scene_name = MessageNotifications::SCENE[$item->message->scene];
}
});
unset($item);
return $res;
}
}
+244
View File
@@ -0,0 +1,244 @@
<?php
namespace app\common\utils;
use GatewayWorker\Lib\Gateway;
use app\common\utils\WsResponse;
use app\common\service\MessageNotificationsService;
class WsEvents
{
private $registerAddress = '127.0.0.1:1236';
protected $clientId;
protected $eventData;
public function __construct()
{
Gateway::$registerAddress = $this->registerAddress;
}
/**
* 监听消息接收
*/
public function message($client_id, $message)
{
$this->clientId = $client_id;
$payload = json_decode($message, true);
$event = $payload['event'];
$this->eventData = $payload['data'] ?? [];
$traceId = $payload['trace_id'] ?? null;
$method = $this->eventToMethod($event);
if (method_exists($this, $method)) {
try {
$this->$method($traceId);
} catch (\Throwable $th) {
Gateway::sendToClient($client_id, WsResponse::error(
$event,
500,
$th->getMessage(),
$traceId
));
}
} else {
Gateway::sendToClient($client_id, WsResponse::error(
$event,
404,
"Event handler not found: $method",
$traceId
));
}
}
// ================= 系统事件 =================
/**
* 认证
*/
public function auth($client_id, $payload)
{
try {
// 检查来源
if (!isset($payload['server']['HTTP_ORIGIN'])) {
flg('[x] [TOKEN认证] 未定义HTTP_ORIGIN', false);
Gateway::sendToClient($client_id, WsResponse::error(
'system.auth',
1001,
'[x] [TOKEN认证] 未定义HTTP_ORIGIN'
));
Gateway::closeClient($client_id);
return;
}
// 连接是否满足条件
// if ($payload['server']['HTTP_ORIGIN'] != 'https://wstool.js.org') {
// flg('[x] [TOKEN认证] HTTP_ORIGIN不满足条件', false);
// Gateway::sendToClient($client_id, WsResponse::error(
// 'system.auth',
// 1001,
// '[x] [TOKEN认证] HTTP_ORIGIN不满足条件'
// ));
// Gateway::closeClient($client_id);
// return;
// }
// 签名检查
if (!isset($payload['get']['token'])) {
flg('[x] [TOKEN认证] 未携带TOKEN参数', false);
Gateway::sendToClient($client_id, WsResponse::error(
'system.auth',
1001,
'[x] [TOKEN认证] 未携带TOKEN参数'
));
Gateway::closeClient($client_id);
return;
}
// 解析TOKEN
$user = (new Jwt())->verifyToken($payload['get']['token']);
// 绑定用户ID
Gateway::bindUid($client_id, $user->uid);
// 响应
Gateway::sendToClient($client_id, WsResponse::success(
'system.auth',
['uid' => $user->uid]
));
} catch (\Throwable $th) {
flg('[x] [TOKEN认证] TOKEN认证失败:'.$th->getMessage(), false);
Gateway::sendToClient($client_id, WsResponse::error(
'system.auth',
1001,
'[x] [TOKEN认证] TOKEN认证失败:'.$th->getMessage()
));
Gateway::closeClient($client_id);
return;
}
}
/**
* 系统心跳
*
* @param string|null $traceId
*/
public function systemPing(?string $traceId)
{
Gateway::sendToClient($this->clientId, WsResponse::success(
'system.pong',
['time' => time()],
$traceId
));
}
// ================= 消息通知事件 =================
/**
* 管理端主动推送通知
*/
public function notifyPush(?string $traceId)
{
$title = $this->eventData['title'] ?? null;
$content = $this->eventData['content'] ?? null;
$toUids = $this->eventData['to_uids'] ?? [];
if (!$title || !$content) {
Gateway::sendToClient($this->clientId, WsResponse::error(
'notify.push.result',
400,
'标题和内容不能为空',
$traceId
));
return;
}
// 写消息
$msgService = new MessageNotificationsService();
$msgId = $msgService->createMessage([
'sender_id' => $this->eventData['sender_id'] ?? 0,
'sender_type' => $this->eventData['sender_type'] ?? 'system',
'title' => $title,
'content' => $content,
'scene' => $this->eventData['scene'] ?? 0,
'recipient' => $this->eventData['to_uids'] ? implode(',', $this->eventData['to_uids']) : '',
'push_method' => isset($this->eventData['push_method']) ? $this->eventData['push_method'] : 0
]);
// 写用户状态
$uids = !empty($toUids) ? $toUids : $msgService->getAllUserIds();
$msgService->addUserStatus($msgId, $uids);
$payload = WsResponse::success('notify.receive', [
'msg_id' => $msgId,
'title' => $title,
'content' => $content,
'created_at' => time()
], $traceId);
if (!empty($toUids)) {
// 推送给指定用户
foreach ($toUids as $uid) {
Gateway::sendToUid($uid, $payload);
}
} else {
// 推送给全体在线用户
Gateway::sendToAll($payload);
}
// 回复发送者确认
Gateway::sendToClient($this->clientId, WsResponse::success(
'notify.push.result',
['msg_id' => $msgId],
$traceId,
'消息发送成功'
));
}
/**
* 客户端标记通知已读
*/
public function notifyRead(?string $traceId)
{
$msgIds = $this->eventData['msg_ids'];
if (!is_array($msgIds) && $msgIds !== 'all') {
Gateway::sendToClient($this->clientId, WsResponse::error(
'notify.read.result',
400,
'消息ID不能为空',
$traceId
));
return;
}
// 更新消息为已读
$msgService = new MessageNotificationsService();
$msgService->markAsRead($msgIds, Gateway::getUidByClientId($this->clientId));
Gateway::sendToClient($this->clientId, WsResponse::success(
'notify.read.result',
['msg_ids' => $msgIds],
$traceId
));
}
/**
* 推送消息通知
*/
public function pushWsNotice(array $wsData, int $userId)
{
Gateway::sendToUid($userId, WsResponse::success('notify.receive', $wsData));
}
// ================= 辅助方法 =================
protected function eventToMethod(string $event): string
{
$parts = explode('.', $event);
$method = array_shift($parts);
foreach ($parts as $part) {
$method .= ucfirst($part);
}
return $method;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace app\common\utils;
class WsResponse
{
/**
* 成功返回
*
* @param string $event 事件名称
* @param array $data 返回数据
* @param string|null $traceId 请求追踪ID,可选,默认 null
* @param int $code 错误码
* @param string $message 错误信息
* @return string JSON
*/
public static function success(string $event, array $data = [], ?string $traceId = null, string $message = 'ok', int $code = 0): string
{
return json_encode([
'event' => $event,
'code' => $code,
'message' => $message,
'data' => $data,
'trace_id' => $traceId, // 如果没有传就是 null
], JSON_UNESCAPED_UNICODE);
}
/**
* 错误返回
*
* @param string $event 事件名称
* @param int $code 错误码
* @param string $message 错误信息
* @param string|null $traceId 请求追踪ID,可选,默认 null
* @param array $data 附加数据(可选)
* @return string JSON
*/
public static function error(string $event, int $code, string $message, ?string $traceId = null, array $data = []): string
{
return json_encode([
'event' => $event,
'code' => $code,
'message' => $message,
'data' => $data,
'trace_id' => $traceId, // 如果没有传就是 null
], JSON_UNESCAPED_UNICODE);
}
}
@@ -0,0 +1,29 @@
<?php
namespace app\common\validate;
use think\Validate;
class MessageNotificationsValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
@@ -0,0 +1,29 @@
<?php
namespace app\common\validate;
use think\Validate;
class MessageUserStatusValidate extends Validate
{
// 规则
protected $rule = [
];
// 提示
protected $message = [
];
// 场景
protected $scene = [
'store' => [
],
'update' => [
]
];
}
+33 -17
View File
@@ -6,6 +6,7 @@ use app\common\model\InquiryLog;
use app\common\model\User;
use app\common\model\UserRoles;
use app\common\model\UserSystems;
use app\common\service\MessageNotificationsService;
use app\common\traits\DataPermission;
use app\common\utils\email\MailSend;
use app\common\utils\scaffold\Generate;
@@ -77,25 +78,25 @@ class IndexController
$options = [
'controller' => [
'module' => 'common',
'name' => 'resources'
'name' => 'message_notifications'
],
'model' => [
'module' => 'common',
'name' => 'resources'
'name' => 'message_notifications'
],
'service' => [
'module' => 'common',
'name' => 'resources'
'name' => 'message_notifications'
],
'logic' => [
'module' => 'common',
'name' => 'resources'
'name' => 'message_notifications'
],
'validate' => [
'module' => 'common',
'name' => 'resources'
'name' => 'message_notifications'
],
'table' => 'resources'
'table' => 'message_notifications'
];
$result = (new Generate($options))->doGenerate();
@@ -107,20 +108,35 @@ class IndexController
{
// $data = [
// 'code' => 'GD20250703-1DC80',
// 'category' => '账号类问题',
// 'content' => '这是一条测试工单内容。'
// 'sender_id' => 0,
// 'sender_type' => 'system',
// 'title' => '测试消息 - 标题',
// 'content' => '测试消息 - 内容',
// 'scene' => 1,
// 'recipient' => [1],
// ];
// \app\common\utils\email\MailSend::send('mzl@hnxinyouhe.com', '【XYH】新的工单提醒', replaceTpl($data, 'work_order'));
// $options = [
// 'corp_id' => 3,
// 'user_id' => 666
// ];
// \Webman\RedisQueue\Redis::send('perm-agt', $options);
$data['recipient'] = [1, 74];
// // 同步推送
// (new MessageNotificationsService())->pushNotice($data);
$res = Cache::delete("user_resources_74_fdesk");
return 22;
// 异步推送
\Webman\RedisQueue\Redis::send('notice', $data);
return 'success';
// return json($data);
// $phone = '';
// $email = '';
// $userByPhone = User::where('corp_id', 2)->where('phone', $phone)->where('phone', '!=', '')->first();
// $userByEmail = User::where('corp_id', 2)->where('email', $email)->where('email', '!=', '')->first();
// $user = $userByPhone ?: $userByEmail;
// return json($user);
}
}
+38 -10
View File
@@ -4,6 +4,7 @@ namespace app\crmAdmin\controller;
use app\common\model\Corp;
use app\crm\model\Customer;
use app\crm\utils\Util;
use app\crmAdmin\service\CustomerService;
use support\Request;
@@ -148,19 +149,18 @@ class CustomerController extends BaseController
}
$customer_id = explode(',', $param['customer_id']);
$customer = Customer::whereIn('id',$customer_id)->withTrashed()->first();
if(empty($customer)){
$query = Customer::whereIn('id',$customer_id)->withTrashed();
if(!$query->exists()){
return $this->error( trans('fail'));
}
if($param['scene'] == 'list'){ //管理端客户列表移除
$customer->status = 4;
}else{ //公海软删除
$customer->status = 3;
$scene = $param['scene'] ?? '';
if ($scene === 'list') { //管理端客户列表移除
$res = $query->update(['status' => 4, 'deleted_at' => time()]);
} elseif ($scene === 'force') { //强删除
$res = $query->forceDelete();
} else { //公海软删除
$res = $query->update(['status' => 3, 'deleted_at' => time()]);
}
$res = $customer->save();
$customer->delete();
return $this->success($res);
} catch (\Throwable $th) {
@@ -168,6 +168,34 @@ class CustomerController extends BaseController
}
}
/**
* 客户恢复
* @param Request $request
* @return \support\Response
*/
public function restore(Request $request)
{
try {
$id = $request->input('id', '');
if (Util::isEmpty($id)) {
return $this->error(trans('id_empty'));
}
$ids = explode(',', $id);
$query = Customer::withTrashed()->whereIn('id', $ids);
if (!$query->exists()) {
return $this->error(trans('fail'));
}
// 恢复软删除(清空 deleted_at)
$query->restore();
// 恢复为在库状态,放入公海
$res = Customer::whereIn('id', $ids)->update(['status' => 1, 'user_id' => 0]);
return $this->success($res, trans('success'));
} catch (\Throwable $th) {
return $this->error($th->getMessage());
}
}
/**
* 客户详情
* @param Request $request
+13 -3
View File
@@ -51,12 +51,14 @@ class CustomerService
$follow_last = $params['follow_last'] ?? '';
$tagId = $params['tag_id'] ?? [];
$user_id = $params['user_id'] ?? '';
$seas = $params['seas'] ?? '';
$seas = $params['seas'] ?? ''; //recyclea:管理端回收站,audit:待审核列表,seas:公海
$scene = $params['scene'] ?? '';
$where = [];
if ($scene == 'audit') {
$where[] = ['status', '=', 2];
} elseif ($scene == 'recyclea') {
$where[] = ['status', '>', 2];
} else {
$where[] = ['status', '=', 1];
}
@@ -121,8 +123,8 @@ class CustomerService
if ($recycle == 'recycle' || $scene == 'recycle' || $scene == 'audit') {
$query->onlyTrashed();
}
if ($seas == 'seas' || $scene == 'seas') {
$query->withTrashed();
if ($seas == 'seas' || $scene == 'recyclea') {
$query = $query->withTrashed();
}
if (!$export) {
@@ -153,6 +155,14 @@ class CustomerService
}
$item['audit'] = $audit;
}
if ($scene == 'recyclea' && $item['status'] == 3) { //回收站删除来源
$item['del_source'] = '公海';
}elseif ($scene == 'recyclea' && $item['status'] == 4) {
$item['del_source'] = '管理端';
}elseif ($scene == 'recyclea' && $item['status'] == 5) {
$source = (!empty($item['user_id'])) ? (':' . User::where('id',$item['user_id'])->value('name')) : '';
$item['del_source'] = '业务员' . $source;
}
}
}
unset($item);
+24
View File
@@ -284,3 +284,27 @@ if (!function_exists('normalizeRichText')) {
return preg_replace('/\s+/u', ' ', $result);
}
}
/**
* 生成消息编号(唯一业务标识)
* 格式:年月日时分秒 + 微秒 + 随机数
* 例如:20250820175530987654321
*
* @return string
*/
if (!function_exists('generateMsgId')) {
function generateMsgId(): string
{
// 时间部分(精确到微秒)
$time = microtime(true);
$micro = sprintf("%06d", ($time - floor($time)) * 1e6);
$date = date('YmdHis', (int)$time);
// 随机部分(3位)
$rand = mt_rand(100, 999);
// 拼接:日期时间 + 微秒 + 随机数
return $date . $micro . $rand;
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace app\queue\redis;
use support\exception\BusinessException;
use Webman\RedisQueue\Consumer;
class Notice implements Consumer
{
// 要消费的队列名
public $queue = 'notice';
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
public $connection = 'default';
// 消费
public function consume($data)
{
try {
(new \app\common\service\MessageNotificationsService())->pushNotice($data);
return true;
} catch (\Throwable $th) {
var_dump('4 :'.$th->getFile()."|".$th->getLine()."|".$th->getMessage());
throw new BusinessException($th->getFile()."|".$th->getLine()."|".$th->getMessage());
}
}
// 消费失败回调
/*
$package = [
'id' => 1357277951, // 消息ID
'time' => 1709170510, // 消息时间
'delay' => 0, // 延迟时间
'attempts' => 2, // 消费次数
'queue' => 'send-mail', // 队列名
'data' => ['to' => 'tom@gmail.com', 'content' => 'hello'], // 消息内容
'max_attempts' => 5, // 最大重试次数
'error' => '错误信息' // 错误信息
]
*/
public function onConsumeFailure(\Throwable $e, $package)
{
echo "consume failure\n";
echo $e->getMessage() . "\n";
// 无需反序列化
var_export($package);
}
}
+2 -1
View File
@@ -45,7 +45,8 @@
"symfony/filesystem": "^7.3",
"workerman/crontab": "^1.0",
"phpoffice/phpspreadsheet": "^4.5",
"phpmailer/phpmailer": "^6.10"
"phpmailer/phpmailer": "^6.10",
"webman/gateway-worker": "^1.0"
},
"suggest": {
"ext-event": "For better performance. "
Generated
+88 -1
View File
@@ -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": "505fb3d97c338ea53b787196fcc3daaa",
"content-hash": "7c70d8c8bda8a5098b8baed463db8839",
"packages": [
{
"name": "aliyuncs/oss-sdk-php",
@@ -6634,6 +6634,39 @@
},
"time": "2025-04-04T13:23:25+00:00"
},
{
"name": "webman/gateway-worker",
"version": "v1.0.9",
"source": {
"type": "git",
"url": "https://github.com/webman-php/gateway-worker.git",
"reference": "6780c1c788c273a24699caee84feb14224234000"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webman-php/gateway-worker/zipball/6780c1c788c273a24699caee84feb14224234000",
"reference": "6780c1c788c273a24699caee84feb14224234000",
"shasum": ""
},
"require": {
"workerman/gateway-worker": ">=3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Webman\\GatewayWorker\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"support": {
"issues": "https://github.com/webman-php/gateway-worker/issues",
"source": "https://github.com/webman-php/gateway-worker/tree/v1.0.9"
},
"time": "2024-04-30T01:05:42+00:00"
},
{
"name": "webman/redis",
"version": "v2.1.3",
@@ -6793,6 +6826,60 @@
},
"time": "2025-01-15T07:20:50+00:00"
},
{
"name": "workerman/gateway-worker",
"version": "v4.0.1",
"source": {
"type": "git",
"url": "https://github.com/walkor/GatewayWorker.git",
"reference": "25e4430982caa98f82e9187d092ce34b2fddfc90"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/walkor/GatewayWorker/zipball/25e4430982caa98f82e9187d092ce34b2fddfc90",
"reference": "25e4430982caa98f82e9187d092ce34b2fddfc90",
"shasum": ""
},
"require": {
"php": ">=7.2",
"workerman/workerman": "^4.0.30 || ^5.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0-dev"
}
},
"autoload": {
"psr-4": {
"GatewayWorker\\": "./src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"homepage": "http://www.workerman.net",
"keywords": [
"communication",
"distributed"
],
"support": {
"issues": "https://github.com/walkor/GatewayWorker/issues",
"source": "https://github.com/walkor/GatewayWorker/tree/v4.0.1"
},
"funding": [
{
"url": "https://opencollective.com/walkor",
"type": "open_collective"
},
{
"url": "https://www.patreon.com/walkor",
"type": "patreon"
}
],
"time": "2025-06-27T01:30:09+00:00"
},
{
"name": "workerman/redis",
"version": "v2.0.4",
@@ -0,0 +1,4 @@
<?php
return [
'enable' => true,
];
@@ -0,0 +1,39 @@
<?php
use Webman\GatewayWorker\Gateway;
use Webman\GatewayWorker\BusinessWorker;
use Webman\GatewayWorker\Register;
return [
'gateway' => [
'handler' => Gateway::class,
'listen' => 'websocket://0.0.0.0:5151',
'count' => 2,
'reloadable' => false,
'constructor' => ['config' => [
'lanIp' => '127.0.0.1',
'startPort' => 2300,
'pingInterval' => 55,
'pingNotResponseLimit' => 1,
'pingData' => '',
'registerAddress' => '127.0.0.1:1236',
'onConnect' => function(){},
]]
],
'worker' => [
'handler' => BusinessWorker::class,
'count' => cpu_count()*2,
'constructor' => ['config' => [
'eventHandler' => plugin\webman\gateway\Events::class,
'name' => 'ChatBusinessWorker',
'registerAddress' => '127.0.0.1:1236',
]]
],
'register' => [
'handler' => Register::class,
'listen' => 'text://127.0.0.1:1236',
'count' => 1, // Must be 1
'reloadable' => false,
'constructor' => []
],
];
+20
View File
@@ -64,10 +64,29 @@ Route::group('/common', function () {
Route::any('/cache/clear', [app\common\controller\PresetController::class, 'cacheClear']);
Route::any('/menus', [app\common\controller\PresetController::class, 'menus']);
});
// 消息通知
Route::group('/notice', function () {
Route::any('/list', [app\common\controller\MessageUserStatusController::class, 'index']);
Route::any('/show', [app\common\controller\MessageUserStatusController::class, 'show']);
Route::any('/read', [app\common\controller\MessageUserStatusController::class, 'read']);
Route::any('/destroy', [app\common\controller\MessageUserStatusController::class, 'destroy']);
Route::any('/unread', [app\common\controller\MessageUserStatusController::class, 'listUnRead']);
});
});
// 后台管理
Route::group('/admin', function () {
// 消息通知
Route::group('/notice', function () {
Route::any('/list', [app\common\controller\MessageNotificationsController::class, 'index']);
Route::any('/show', [app\common\controller\MessageNotificationsController::class, 'show']);
Route::any('/push', [app\common\controller\MessageNotificationsController::class, 'push']);
Route::any('/destroy', [app\common\controller\MessageNotificationsController::class, 'destroy']);
});
// 角色
Route::group('/role', function () {
Route::any('/list', [app\admin\controller\RoleController::class, 'index']);
@@ -157,6 +176,7 @@ Route::group('/admin', function () {
Route::any('/transfer', [app\crmAdmin\controller\CustomerController::class, 'transfer']);
Route::any('/detail', [app\crmAdmin\controller\CustomerController::class, 'detail']);
Route::any('/delete', [app\crmAdmin\controller\CustomerController::class, 'delete']);
Route::any('/restore', [app\crmAdmin\controller\CustomerController::class, 'restore']); //恢复客户
Route::any('/cede', [app\crm\controller\CustomerController::class, 'cede']);
Route::any('/follow', [app\crmAdmin\controller\CustomerController::class, 'follow']);
Route::any('/allocation', [app\crmAdmin\controller\CustomerController::class, 'allocation']);
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace plugin\webman\gateway;
use app\common\utils\WsEvents;
use GatewayWorker\Lib\Gateway;
class Events
{
protected static $wsEvents;
public static function onWorkerStart($worker)
{
self::$wsEvents = new WsEvents();
}
public static function onConnect($client_id)
{
// 仅 TCP 连接,不一定有 WebSocket 握手
}
public static function onWebSocketConnect($client_id, $data)
{
self::$wsEvents->auth($client_id, $data);
}
public static function onMessage($client_id, $message)
{
self::$wsEvents->message($client_id, $message);
}
public static function onClose($client_id)
{
var_dump('关闭客户端ID:'.$client_id);
}
}