Merge branch 'feature/distinct' into develop_bm

This commit is contained in:
2025-08-26 09:30:04 +08:00
38 changed files with 1886 additions and 103 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;
}
+5 -1
View File
@@ -60,9 +60,13 @@ class InquiryLog extends Model
return $this->hasOne(User::class, 'id', 'enter_user_id');
}
// public function recipientInfo()
// {
// return $this->hasOne(User::class, 'id', 'recipient')->where('status', 1);
// }
public function recipientInfo()
{
return $this->hasOne(User::class, 'id', 'recipient')->where('status', 1);
return $this->hasOne(User::class, 'id', 'recipient');
}
public function channelInfo()
+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;
}
}
+4 -3
View File
@@ -18,12 +18,13 @@ trait ResponseTrait
*/
public function success(
$data = [],
string $message = '请求成功',
int $code = 200,
string $message = '请求成功',
string $status = 'success',
int $code = 200,
int $httpStatus = 200,
array $header = ['Content-Type' => 'application/json']
): Response {
return $this->result('success', $code, $httpStatus, $message, $data, $header);
return $this->result($status, $code, $httpStatus, $message, $data, $header);
}
/**
+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);
}
}
+1 -1
View File
@@ -436,7 +436,7 @@ class Emails
// 初始化 UID 轮询器
$uidRotator = new \app\common\utils\RedisUidRotator('email_parsed_result_20250804');
$uidRotator->setUids([64, 63, 83, 93, 108, 118, 131, 132, 145, 150, 143, 156, 157, 158], false);
$uidRotator->setUids([64, 63, 83, 93, 108, 118, 132, 145, 150, 143, 156, 157, 158], false);
EmailParsedResult::where('status', 0)
->chunkById($batchSize, function ($records) use ($delaySeconds, $throttleAfter, $throttleSeconds, $enterUser, $channelInfo, $uidRotator) {
@@ -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' => [
]
];
}
+39 -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,41 @@ 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);
$info = InquiryLog::with(['channelInfo', 'countryInfo', 'enter'])->where('id', 1)->where('sync_crm', 0)->first();
var_dump(empty($info->recipient));
if ($info) {
var_dump(11);
} else {
var_dump(22);
}
if ($info !== NULL) {
var_dump(33);
} else {
var_dump(44);
}
return json(['code' => 0, 'msg' => 'success']);
}
}
+35 -3
View File
@@ -163,7 +163,7 @@ class CustomerController extends BaseController
$perPage = $request->input('size', 10);
$where = [
['user_id', '=', $userId],
// ['corp_abbr', '=', $request->basic_data['corp_abbr']],
//['corp_abbr', '=', $request->basic_data['corp_abbr']],
['follow_next', '<>', 0],
];
@@ -306,6 +306,7 @@ class CustomerController extends BaseController
$contacts = Util::getParam($request, 'contacts');
$email = Util::getParam($request, 'email');
$phone = Util::getParam($request, 'phone');
$whatsApp = Util::getParam($request, 'WhatsApp', '');
if (Util::isEmpty($contacts) && Util::isEmpty($email) && Util::isEmpty($phone)) {
return $this->error(trans('contacts_not_null'));
}
@@ -314,7 +315,7 @@ class CustomerController extends BaseController
$corpId = $corpInfo->id;
$data = [
'user_id' => $request->user->uid,
'corp_abbr' => $corpId,
'corp_id' => $corpId,
'grade' => Util::getParam($request, 'grade', 0),
'channel_id' => $channel_id,
'country_id' => $country_id,
@@ -322,7 +323,7 @@ class CustomerController extends BaseController
'magnitude' => Util::getParam($request, 'magnitude', 0),
'email' => $email,
'phone' => $phone,
'WhatsApp' => Util::getParam($request, 'WhatsApp', ''),
'WhatsApp' => $whatsApp,
'contacts' => $contacts,
'describe' => Util::getParam($request, 'describe'),
'lat' => Util::getParam($request, 'lat', 0),
@@ -335,6 +336,15 @@ class CustomerController extends BaseController
if (Util::isEmpty($id)) {
$data['enter_user_id'] = $request->user->uid;
$distinct = [
'email' => $email,
'phone' => $phone,
'whatsApp' => $whatsApp,
];
$dis = (new CustomerService())->distinct($distinct,$corpId);
if($dis){
return $this->success($dis,'客户线索重复,已经业务跟进','distinct');
}
$res = Customer::create($data);
} else {
unset($data['grade']);
@@ -351,6 +361,25 @@ class CustomerController extends BaseController
}
}
/**
* 重复通知
* @param Request $request
* @return \support\Response
*/
public function distinctNotice(Request $request){
$customer_id = Util::getParam($request, 'customer_id');
if (Util::isEmpty($customer_id)) {
return $this->error('客户id不能为空');
}
$res = (new CustomerService())->distinctNotice($customer_id);
if ($res) {
return $this->success($res, '成功');
} else {
return $this->error(trans('fail'));
}
}
/**
@@ -506,12 +535,14 @@ class CustomerController extends BaseController
{
$customerId = Util::getParam($request, 'customer_id');
$userId = $request->user->uid;
$user = User::where(['id' => $userId])->first();
if (Util::isEmpty($customerId)) {
return $this->error(trans('customer_id').trans('not_null'));
}
$data = [
'user_id' => $request->user->uid,
'obtain' => time(),
'corp_id' => $user->corp_id,
];
try {
@@ -592,6 +623,7 @@ class CustomerController extends BaseController
$data = [
'user_id' => $user->id,
'obtain' => time(),
'corp_id' => $user->corp_id,
];
$res = $query->update($data);
+1 -1
View File
@@ -103,7 +103,7 @@ class FollowController extends BaseController
$corpId = $corpInfo->id;
$data = [
'user_id' => $request->user->uid,
'corp_abbr' => $corpId,
'corp_id' => $corpId,
'customer_id' => $customer_id,
'type' => $type,
'picture' => $picture,
+16 -9
View File
@@ -36,15 +36,22 @@ class Follow extends Model
*
* @var bool
*/
public $timestamps = true;
public $timestamps = false;
protected $dates = ['created_at', 'updated_at', 'follow_time'];
/**
* 在保存时手动设置时间戳
*/
protected static function boot()
{
parent::boot();
protected $casts = [
'updated_at' => 'datetime:Y-m-d H:i:s', // 指定序列化格式
'created_at' => 'datetime:Y-m-d H:i:s', // 指定序列化格式
'follow_time' => 'datetime:Y-m-d H:i:s', // 指定序列化格式
// 'updated_at' => 'timestamp', // 指定序列化格式
// 'created_at' => 'timestamp', // 指定序列化格式
];
static::creating(function ($model) {
$model->created_at = time();
$model->updated_at = time();
});
static::updating(function ($model) {
$model->updated_at = time();
});
}
}
+48
View File
@@ -319,4 +319,52 @@ class CustomerService
return $customer;
}
public function distinct($params,$corpId){
$customer = Customer::where('corp_id',$corpId)
->Orwhere('email', '=', $params['email'])
->Orwhere('phone', '=', $params['phone'])
->Orwhere('whatsApp', '=', $params['whatsApp'])
->first();
if (!empty($customer)) {
//客户重复,消息通知
// if($corpId != $customer->corp_id){
// return false;
// }
return $customer;
}
return false;
}
/**
* 客户重复通知
* @param $params
* @param $corpId
* @return Customer|false|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|object
*/
public function distinctNotice($customer_id){
$customer = Customer::where('id', $customer_id)
->first();
if (!empty($customer)) {
$contacts = $customer->contacts??$customer['phone'];
$contacts = $contacts??$customer['whatsApp'];
$contacts = $contacts??$customer['email'];
$data = [
'sender_id' => 0, //发送人ID 0默认系统
'sender_type' => 'system', //发送者类型 user admin system
'title' => '客户跟进提醒 - ' . $contacts, //标题
'content' => $contacts . ' 产生了新的询单,请及时跟进', //内容
'scene' => 1, //场景 0系统公共 1系统通知
'recipient' => [$customer->user_id], //接收人 空为全部
'push_method' => 0 //推送方式0系统推送1手动推送
];
// 异步推送
\Webman\RedisQueue\Redis::send('notice', $data);
return $customer;
}
return false;
}
}
+3
View File
@@ -59,6 +59,9 @@ class FollowService
$oss_picture = $ossservice->generatePrivateUrl($follow['picture'], 86400);
}
$follow['picture_full'] = $oss_picture;
$follow = Util::formatDate($follow,['created_at','updated_at','follow_time']);
$follow['follow_time'] = empty($follow['follow_time'])?'':$follow['follow_time'];
// $follow = Util::formatDate($follow);
return $follow;
}
+5
View File
@@ -126,9 +126,14 @@ class Util
public static function mask($str)
{
$length = strlen($str);
if(empty($str)){
return '';
}
if($length <= 6){
return $str;
}
return substr($str, 0, 3) . '****' . substr($str, -4);
}
+40 -12
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;
@@ -28,7 +29,7 @@ class CustomerController extends BaseController
try {
$param = $request->all();
$corp = Corp::where(['abbr' => $request->basic_data['corp_abbr']])->first();
$param['corp_abbr'] = $corp->id;
$param['corp_id'] = $corp->id;
// $param['corp_abbr'] = $request->basic_data['corp_abbr'];
$result = (new CustomerService())->list($param);
@@ -48,7 +49,7 @@ class CustomerController extends BaseController
try {
$param = $request->all();
// $corp = Corp::where(['abbr' => $request->basic_data['corp_abbr']])->first();
// $param['corp_abbr'] = $corp->id;
// $param['corp_id'] = $corp->id;
$result = (new CustomerService())->export($param);
$excelContent = file_get_contents($result['filepath']);
@@ -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
+27 -11
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);
@@ -329,6 +339,7 @@ class CustomerService
$query= Customer::whereIn('id',explode(',',$customerId));
$customer = $query->get();
$user = User::where(['id' => $userId])->first();
foreach ($customer as $item) {
//记录客户关系变化
@@ -347,6 +358,7 @@ class CustomerService
}
$data = [
'user_id' => $userId,
'corp_id' => $user->corp_id,
'obtain' => time(),
];
$res = $query->update($data);
@@ -469,6 +481,8 @@ class CustomerService
$ossservice = new OssService();
foreach ($follow as $item) {
$item = Util::formatDate($item,['created_at','updated_at','follow_time']);
$item['follow_time'] = empty($item['follow_time'])?'':$item['follow_time'];
$item['picture_full'] = empty($item->picture)?'':$ossservice->generatePrivateUrl($item->picture);
}
@@ -517,14 +531,16 @@ class CustomerService
$uidRotator = new \app\common\utils\RedisUidRotator('allocation_customer_user');
$uidRotator->setUids($userId, false);
Customer::whereIn('user_id',$resignUerIds)->chunk(500, function ($customers) use ($uidRotator){
foreach ($customers as $customer) {
// 获取下一个分配的UID
$newUserId = $uidRotator->next();
$customer->user_id = $newUserId;
$customer->save();
}
});
Customer::whereIn('user_id', $resignUerIds)
->orderBy('id', 'asc')
->chunkById(500, function ($customers) use ($uidRotator) {
foreach ($customers as $customer) {
// 获取下一个分配的UID
$newUserId = $uidRotator->next();
$customer->user_id = $newUserId;
$customer->save();
}
}, 'id');
return [
+24
View File
@@ -283,4 +283,28 @@ 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;
}
}
+7 -5
View File
@@ -162,6 +162,8 @@ class InquiryLogService
$data['r_time'] = time();
$data['t_time'] = time();
$data['status'] = 2;
} else {
$data['recipient'] = 0;
}
return $data;
}
@@ -260,7 +262,7 @@ class InquiryLogService
*/
protected function afterServiceRelease($res)
{
RedisQueueRedis::send('inquiry-to-crm', $res);
RedisQueueRedis::send('inquiry-to-crm-new', $res);
return $res;
}
@@ -269,7 +271,7 @@ class InquiryLogService
*/
protected function afterServiceAllocation($res)
{
RedisQueueRedis::send('inquiry-to-crm', $res);
RedisQueueRedis::send('inquiry-to-crm-new', $res);
return $res;
}
@@ -278,7 +280,7 @@ class InquiryLogService
*/
protected function afterServiceStore($res)
{
RedisQueueRedis::send('inquiry-to-crm', $res);
RedisQueueRedis::send('inquiry-to-crm-new', $res);
return $res;
}
@@ -288,7 +290,7 @@ class InquiryLogService
protected function afterServiceUpdate($res)
{
if ($res['recipient']) {
RedisQueueRedis::send('inquiry-to-crm-update', $res);
RedisQueueRedis::send('inquiry-to-crm-new', $res);
}
return $res;
}
@@ -298,7 +300,7 @@ class InquiryLogService
*/
protected function afterServiceReAllocation($res)
{
RedisQueueRedis::send('inquiry-to-crm-update', $res);
RedisQueueRedis::send('inquiry-to-crm-new', $res);
return $res;
}
+24 -17
View File
@@ -37,18 +37,11 @@ class InquiryToCrm implements Consumer
if ($uinfo) {
$corpId = $uinfo->corp_id;
}
// // 消息推送参数 分配
// $scene = 'inquiry-person-allocation';
// $receiveType = 'person';
} else {
// // 消息推送参数 发布
// $scene = 'inquiry-group-send';
// $receiveType = 'group';
}
// 同步客户数据
$crmCustomer = new CrmCustomer();
$crmCustomer->sn = $info->order_no;
$crmCustomer->corp_abbr = $corpId;
$crmCustomer->corp_id = $corpId;
$crmCustomer->user_id = $recipient;
$crmCustomer->type = 1;
$crmCustomer->channel_id = $info->channel;
@@ -61,6 +54,9 @@ class InquiryToCrm implements Consumer
$crmCustomer->desc_collect = $info->content;
$crmCustomer->enter_user_id = $info->enter_user_id;
$crmCustomer->obtain = time();
if(empty($recipient)){
$crmCustomer->deleted_at = time();
}
// 修改同步CRM状态
$info->sync_crm = 1;
// 保存
@@ -68,15 +64,26 @@ class InquiryToCrm implements Consumer
throw new BusinessException('同步失败~');
}
// 发送邮件消息
if ($uinfo && $uinfo->email) {
$options = [
'id' => $info->id,
'email' => $uinfo->email
];
// Log::info('发送邮件消息:' . json_encode($options));
\Webman\RedisQueue\Redis::send('email-message', $options);
}
// // 发送邮件消息
// if ($uinfo && $uinfo->email) {
// $options = [
// 'id' => $info->id,
// 'email' => $uinfo->email
// ];
// // Log::info('发送邮件消息:' . json_encode($options));
// \Webman\RedisQueue\Redis::send('email-message', $options);
// }
$wsdata = [
'sender_id' => 0,
'sender_type' => 'system',
'title' => '有新的客户线索,' . $info->order_no . ',请及时处理',
'content' => '有新的客户线索,' . $info->order_no . ',请及时处理',
'scene' => 1,
'recipient' => [$info->recipient]
];
// 异步推送
\Webman\RedisQueue\Redis::send('notice', $wsdata);
}
} catch (\Throwable $th) {
throw $th;
+207
View File
@@ -0,0 +1,207 @@
<?php
namespace app\queue\redis;
use app\inquiry\service\InquiryLogService;
use app\common\model\CrmCustomer;
use app\common\model\InquiryLog;
use Webman\RedisQueue\Consumer;
use app\common\model\User;
use app\common\utils\FeishuMessage;
use support\exception\BusinessException;
use support\Log;
use support\Model;
class InquiryToCrmNew implements Consumer
{
// 要消费的队列名
public $queue = 'inquiry-to-crm-new';
// 连接名,对应 plugin/webman/redis-queue/redis.php 里的连接`
public $connection = 'default';
// 消费
public function consume($data)
{
/*
询单 与 CRM 之间的同步场景描述
添加(询单)
添加 不发 不分 (不同步)1
添加 发了 不分 (要同步)2
添加 不发 分了 (要同步)3
添加 不发 直分 (要同步)4
更新(询单)
没发 没分 更新 (不同步)5
没发 分过 更新 (要同步)6
发过 没分 更新 (要同步)7
发过 分过 更新 (要同步)8
*/
try {
// 获取询盘信息
$info = InquiryLog::with(['channelInfo', 'countryInfo', 'enter'])->where('id', $data['id'])->first();
// 无论添加 或 更新,询单必然要存在
if (!$info) {
throw new BusinessException('询盘不存在:' . $data['id']);
}
if ($info->status == 0 && $info->recipient == 0) {
// 如果询单未发、未分,则不与crm同步,不做任何处理
return true;
}
// 更改库信息
$this->syncCustomer($info);
} catch (\Throwable $th) {
throw $th;
}
}
public function pushWs($title, $content, $recipient = [])
{
$wsdata = [
'sender_id' => 0,
'sender_type' => 'system',
'title' => $title,
'content' => $content,
'scene' => 1,
'recipient' => $recipient
];
// 异步推送
\Webman\RedisQueue\Redis::send('notice', $wsdata);
}
public function syncCustomer(Model $info)
{
$uInfo = $this->recipientInfo($info);
$crmCustomerInfo = CrmCustomer::where('sn', $info->order_no)->first();
if ($crmCustomerInfo) {
// 消息推送
if ($info->status == 2 && empty($crmCustomerInfo->user_id)) {
// $notice = '分配';
$title = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
$content = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
$recipient = [$info->recipient];
$this->pushWs($title, $content, $recipient);
} else {
if ($info->status == 2 && $crmCustomerInfo->user_id == $info->recipient) {
// $notice = '更新';
$title = '[询单·更新] 客户线索' . $info->order_no . '有新的调整,请及时处理';
$content = '[询单·更新] 客户线索' . $info->order_no . '有新的调整,请及时处理';
$recipient = [$info->recipient];
$this->pushWs($title, $content, $recipient);
} else {
// $notice = '重新分配';
$title = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
$content = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
$recipient = [$info->recipient];
$this->pushWs($title, $content, $recipient);
}
}
$udata = [
'corp_id' => $uInfo['corpId'],
'user_id' => $info->recipient,
'channel_id' => $info->channel,
'country_id' => $info->country,
'magnitude' => $info->quality,
'email' => $info->email,
'phone' => $info->tel,
'WhatsApp' => $info->whatsapp,
'contacts' => $info->contacts,
'desc_collect' => $info->content,
];
if (!$crmCustomerInfo->update($udata)) {
throw new BusinessException('同步失败,更新客户线索失败');
}
} else {
$crmCustomer = new CrmCustomer();
$crmCustomer->sn = $info->order_no;
$crmCustomer->corp_id = $uInfo['corpId'];
$crmCustomer->user_id = $uInfo['recipient'];
$crmCustomer->type = 1;
$crmCustomer->channel_id = $info->channel;
$crmCustomer->country_id = $info->country;
$crmCustomer->magnitude = $info->quality;
$crmCustomer->email = $info->email;
$crmCustomer->phone = $info->tel;
$crmCustomer->WhatsApp = $info->whatsapp;
$crmCustomer->contacts = $info->contacts;
$crmCustomer->desc_collect = $info->content;
$crmCustomer->enter_user_id = $info->enter_user_id;
$crmCustomer->obtain = time();
if(empty($uInfo['recipient'])){
$crmCustomer->deleted_at = time();
}
// 修改同步CRM状态
$info->sync_crm = 1;
// 保存
if (!$crmCustomer->save() || !$info->save()) {
throw new BusinessException('同步失败,创建客户线索失败');
}
// 消息推送
if ($info->status == 1) {
// $notice = '发布';
$title = '[询单·公海] 新的客户线索【' . $info->order_no . '】发布';
$content = '[询单·公海] 新的客户线索【' . $info->order_no . '】发布';
$recipient = [$info->recipient];
$this->pushWs($title, $content);
}
if ($info->status == 2) {
// $notice = '分配';
$title = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
$content = '[询单·分配] 新的客户线索【' . $info->order_no . '】,请及时处理';
$recipient = [$info->recipient];
$this->pushWs($title, $content, $recipient);
}
}
return true;
}
public function recipientInfo(Model $info)
{
$recipient = 0;
$corpId = 0;
$uinfo = null;
if (!empty($info->recipient)) {
$recipient = $info->recipient;
$uinfo = User::where('id', $info->recipient)->first();
if ($uinfo) {
$corpId = $uinfo->corp_id;
}
}
return [
'recipient' => $recipient,
'corpId' => $corpId,
'uinfo' => $uinfo
];
}
// 消费失败回调
/*
$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);
}
}
+5 -2
View File
@@ -38,7 +38,7 @@ class InquiryToCrmUpdate implements Consumer
$crmCustomerInfo = CrmCustomer::where('sn', $info->order_no)->first();
if ($crmCustomerInfo) {
$udata = [
'corp_abbr' => $corpId,
'corp_id' => $corpId,
'user_id' => $info->recipient,
'channel_id' => $info->channel,
'country_id' => $info->country,
@@ -54,7 +54,7 @@ class InquiryToCrmUpdate implements Consumer
// 同步客户数据
$crmCustomer = new CrmCustomer();
$crmCustomer->sn = $info->order_no;
$crmCustomer->corp_abbr = $corpId;
$crmCustomer->corp_id = $corpId;
$crmCustomer->user_id = $info->recipient;
$crmCustomer->type = 1;
$crmCustomer->channel_id = $info->channel;
@@ -66,6 +66,9 @@ class InquiryToCrmUpdate implements Consumer
$crmCustomer->contacts = $info->contacts;
$crmCustomer->desc_collect = $info->content;
$crmCustomer->enter_user_id = $info->enter_user_id;
if(empty($info->recipient)){
$crmCustomer->deleted_at = time();
}
// 修改同步CRM状态
$info->sync_crm = 1;
// 保存
+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);
}
}
+24 -19
View File
@@ -3,6 +3,7 @@
namespace app\ticket\service;
use app\common\model\User;
use app\common\service\MessageNotificationsService;
use app\common\service\OssService;
use app\ticket\logic\CategorieLogic;
use app\ticket\logic\ConversationLogic;
@@ -54,43 +55,47 @@ class ConversationService
$ticket = (new TicketLogic())->find($data['ticket_id']);
$categorie= (new CategorieLogic())->find($ticket['categorie_id']);
$categorieName = isset($categorie['translation']) ? $categorie['translation']['name'] : '';
$notice = [
'code' => $ticket['code'],
'category' => $categorieName,
'content' => $data['content'],
'reply' => '',
];
//$emailDefault = 'app@hnxinyouhe.com'; //公共邮箱
//$emailDefault = 'support@hnxinyouhe.com'; //公共邮箱
$emailDefault = env('TICKET_EMAIL_DEFAULT');
if (empty($conversation)) {
$email = $emailDefault;
$userDefault = [1, 75, 78];
$tiket_content = $ticket['content'];
if (empty($conversation)) { //没有沟通记录
$title = '新工单提醒-'.$categorieName;
$notice_user_id = $userDefault;
}else{
$title = '工单回复';
$tiket = (new TicketLogic())->find($conversation['ticket_id']);
if($data['user_id'] != $tiket['user_id']){
$email = User::where('id',$tiket['user_id'])->value('email');
$notice_user_id = [$tiket['user_id']];
}else{
$conversationLast = Conversation::where('ticket_id', '=', $conversation['ticket_id'])
->where('user_id', '!=', $data['user_id'])
->orderBy('id', 'desc')
->first();
$email = (!empty($conversationLast['user_id']))?User::where('id',$conversationLast['user_id'])->value('email',$emailDefault):$emailDefault;
$notice_user_id = (!empty($conversationLast['user_id']))? [$conversationLast['user_id']]:$userDefault;
}
$notice['content'] = $tiket['content'];
$tiket_content = $tiket['content'];
$status_fanyi = array_map(fn($v) => "{{{$v}}}", array_flip(Constants::TICKET_STATUS));
$locale = locale();
$ticket_status = $locale == 'zh'? Constants::TICKET_STATUS_ZH:Constants::TICKET_STATUS_EN;
$content = str_replace($status_fanyi,$ticket_status,$data['content']);
$notice['reply'] = '<li>最新回复:' . $content . '</li>';
}
if(!empty($emailDefault)){
\app\common\utils\email\MailSend::send($email, '【XYH】新的工单提醒', replaceTpl($notice, 'work_order'));
$reply = $content?($content."\n".'工单内容:'):'' ;
}
$notice = ['sender_id' => 0, //发送人ID 0默认系统
'sender_type' => 'system', //发送者类型 user admin system
'title' => $title, //标题
'content' => ($reply??'') . $tiket_content, //内容
'scene' => 1, //场景 0系统公共 1系统通知
'recipient' => $notice_user_id, //接收人 空为全部
'push_method' => 0 //推送方式0系统推送1手动推送
];
// 异步推送
\Webman\RedisQueue\Redis::send('notice', $notice);
unset($data['categorie_id']);
unset($data['code']);
+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:' . getenv('GATEWAY_LISTEN'),
'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' => []
],
];
+21
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']);
@@ -214,6 +234,7 @@ Route::group('/crm', function () {
Route::any('/customer/getCountry', [app\crm\controller\CustomerController::class, 'getCountry']);
Route::any('/customer/followNext', [app\crm\controller\CustomerController::class, 'followNext']);
Route::any('/customer/report', [app\crm\controller\CustomerController::class, 'report']);
Route::any('/customer/distinctNotice', [app\crm\controller\CustomerController::class, 'distinctNotice']);
Route::any('/tagGroup/list', [app\crm\controller\TagGroupController::class, 'list']);
Route::any('/tagGroup/updatePivot', [app\crm\controller\TagGroupController::class, 'updatePivot']);
+13
View File
@@ -0,0 +1,13 @@
<?php
return [
'basi_cdata' => [
'/', // 首页
'/index/index', // 首页
'/index/view', // 飞书调试
'/perm', // 调试-添加权限
'/common/preset/corps', // 公司列表
'/common/preset/feishu/app', // 飞书应用信息
]
];
+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);
}
}