535 lines
18 KiB
PHP
Executable File
535 lines
18 KiB
PHP
Executable File
<?php
|
||
|
||
namespace app\traits\account;
|
||
|
||
use app\exception\RepositoryException;
|
||
use app\service\ali\Sms;
|
||
use app\service\wx\WechatApplets;
|
||
use app\validate\MessageValidate;
|
||
use Exception;
|
||
use GuzzleHttp\Exception\GuzzleException;
|
||
use app\model\{Account, Config, Message, MessageLog, SmsLog};
|
||
use app\exception\TraitException;
|
||
use think\Collection;
|
||
use think\facade\Log;
|
||
use think\Model;
|
||
use think\Validate;
|
||
|
||
/**
|
||
* 用户消息
|
||
*
|
||
* Trait MessageTrait
|
||
* @package app\traits\account
|
||
*/
|
||
trait AccountMessageTrait
|
||
{
|
||
/**
|
||
* 消息类型文本描述
|
||
* @return string[]
|
||
*/
|
||
public function messageTypeTextList(): array
|
||
{
|
||
return Message::typeTextList();
|
||
}
|
||
|
||
/**
|
||
* 目标人群文本描述
|
||
* @return string[]
|
||
*/
|
||
public function messageTargetTextList(): array
|
||
{
|
||
return Message::targetTextList();
|
||
}
|
||
|
||
/**
|
||
* 获取消息列表
|
||
*
|
||
* @param array $where
|
||
* @param array $fields
|
||
* @param int $page
|
||
* @param int $size
|
||
* @param callable|null $callback
|
||
* @param array $orders
|
||
* @return array
|
||
*/
|
||
public function messageList(array $where = [], array $fields = [], int $page = 1, int $size = 10, callable $callback = null, array $orders = []): array
|
||
{
|
||
try {
|
||
return Message::findList($where, $fields, $page, $size, $callback, $orders);
|
||
} catch (Exception $e) {
|
||
return [
|
||
'total' => 0,
|
||
'current' => $page,
|
||
'size' => $size,
|
||
'list' => new Collection(),
|
||
];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 消息详情
|
||
*
|
||
* @param int $messageId
|
||
* @param array $fields
|
||
* @return null
|
||
*/
|
||
public function messageInfo(int $messageId, array $fields = [])
|
||
{
|
||
try {
|
||
return Message::findById($messageId, $fields);
|
||
} catch (Exception $e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取已读消息记录
|
||
* @param int $accountId
|
||
* @param array $messageIds
|
||
* @return array
|
||
*/
|
||
public function getHadReadMessageIds(int $accountId, array $messageIds): array
|
||
{
|
||
try {
|
||
$hadItems = MessageLog::whereIn('message_id', $messageIds)
|
||
->where('account_id', $accountId)
|
||
->select();
|
||
|
||
return $hadItems->column('message_id');
|
||
} catch (Exception $e) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加阅读记录
|
||
*
|
||
* @param int $accountId
|
||
* @param array $messageIds
|
||
* @return bool
|
||
*/
|
||
public function addReadLogs(int $accountId, array $messageIds): bool
|
||
{
|
||
try {
|
||
$readMsgIds = $this->getHadReadMessageIds($accountId, $messageIds);
|
||
$newLogMsgIds = array_diff($messageIds, $readMsgIds);
|
||
|
||
if ($newLogMsgIds) {
|
||
$newLogs = [];
|
||
foreach ($newLogMsgIds as $msgId) {
|
||
$newLogs[] = [
|
||
'message_id' => $msgId,
|
||
'account_id' => $accountId,
|
||
'created_at' => date('Y-m-d H:i:s'),
|
||
];
|
||
}
|
||
MessageLog::insertAll($newLogs);
|
||
}
|
||
} catch (Exception $e) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 统计用户未读消息数量
|
||
*
|
||
* @param int $accountId
|
||
* @return int
|
||
*/
|
||
public function countUnReadMessage(int $accountId): int
|
||
{
|
||
$unReadCount = 0;
|
||
try {
|
||
$regTime = '';
|
||
$account = Account::findById($accountId);
|
||
if ($account) {
|
||
$regTime = $account['created_at'];
|
||
}
|
||
|
||
$allCount = Message::whereTime('send_at', '<=', date('Y-m-d H:i:s'))
|
||
->when(!empty($regTime), function ($q2) use ($regTime) {
|
||
$q2->whereTime('send_at', '>=', $regTime);
|
||
})
|
||
->where(function ($q2) use ($accountId) {
|
||
$q2->whereRaw('(target = "part" and find_in_set("'.$accountId.'", target_list)) or target <> "part"');
|
||
})
|
||
->count();
|
||
|
||
$readCount = MessageLog::where('account_id', $accountId)
|
||
->when(!empty($regTime), function ($q2) use ($regTime) {
|
||
$q2->whereTime('created_at', '>=', $regTime);
|
||
})
|
||
->distinct(true)
|
||
->field(['message_id'])
|
||
->count();
|
||
|
||
$unReadCount = $allCount - $readCount;
|
||
$unReadCount = $unReadCount > 0 ? $unReadCount : 0;
|
||
} catch (Exception $e) {
|
||
|
||
}
|
||
|
||
return $unReadCount;
|
||
}
|
||
|
||
|
||
/**
|
||
* 创建消息
|
||
*
|
||
* @param string $type 类型 ['system'|'notice']
|
||
* @param string $target 目标人群 ['all'| 'part']
|
||
* @param array $targetList 消息目标人群ID, $target='part' 时启用 []表示发送所有人
|
||
* @param array $messageData 消息内容 ['title' 标题|'summary' 概述|'content' 详情|'send_at' 发送时间]
|
||
* @return Message|Model
|
||
* @throws TraitException
|
||
*/
|
||
public function addMessage(string $type, string $target, array $targetList = [], array $messageData = [])
|
||
{
|
||
try {
|
||
if (!in_array($type, array_keys(Message::typeTextList()))) {
|
||
throw new TraitException('消息类型错误, 无此选项');
|
||
}
|
||
if (!in_array($target, array_keys(Message::targetTextList()))) {
|
||
throw new TraitException('目标人群错误, 无此选项');
|
||
}
|
||
$targetList = array_filter($targetList);
|
||
if ($target == Message::TARGET_PART && empty($targetList)) {
|
||
throw new TraitException('目标人群为部分接收时必须指定消息接收人员列表');
|
||
}
|
||
if ($target == Message::TARGET_ALL) {
|
||
$targetList = [];
|
||
}
|
||
|
||
$validate = new MessageValidate();
|
||
if (!$validate->scene('add')->check($messageData)) {
|
||
throw new TraitException($validate->getError());
|
||
}
|
||
|
||
$dataFields = [
|
||
'title', 'summary', 'content', 'send_at', 'is_push',
|
||
'to_subscribe', 'to_sms', 'subscribe_data', 'sms_data', 'subscribe_temp_id', 'sms_temp_id'
|
||
];
|
||
$messageData = arrayKeysFilter($messageData, $dataFields);
|
||
|
||
$messageData['send_at'] = empty($messageData['send_at'] ?? '') ? date('Y-m-d H:i:s') : $messageData['send_at'];
|
||
$messageData['created_at'] = date('Y-m-d H:i:s');
|
||
$messageData['type'] = $type;
|
||
$messageData['target'] = $target;
|
||
$messageData['target_list'] = empty($targetList) ? '' : implode(',', $targetList);
|
||
|
||
return Message::create($messageData);
|
||
|
||
} catch (TraitException $e) {
|
||
throw $e;
|
||
} catch (Exception $e) {
|
||
Log::error('消息创建失败:'.$e->getMessage().' file:'.$e->getFile().' line:'.$e->getLine());
|
||
throw new TraitException('消息创建失败');
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 编辑消息
|
||
*
|
||
* @param int $id 消息ID
|
||
* @param string $type 类型 ['system'|'notice']
|
||
* @param string $target 目标人群 ['all'| 'part']
|
||
* @param array $targetList 消息目标人群ID, $target='part' 时启用
|
||
* @param array $messageData 消息内容 ['title' 标题|'summary' 概述|'content' 详情|'send_at' 发送时间]
|
||
* @return Message
|
||
* @throws TraitException
|
||
*/
|
||
public function editMessage(int $id, string $type, string $target, array $targetList = [], array $messageData = [])
|
||
{
|
||
try {
|
||
if (!in_array($type, array_keys(Message::typeTextList()))) {
|
||
throw new TraitException('消息类型错误, 无此选项');
|
||
}
|
||
if (!in_array($target, array_keys(Message::targetTextList()))) {
|
||
throw new TraitException('目标人群错误, 无此选项');
|
||
}
|
||
$targetList = array_filter($targetList);
|
||
if ($target == Message::TARGET_PART && empty($targetList)) {
|
||
throw new TraitException('目标人群为部分接收时必须指定消息接收人员列表');
|
||
}
|
||
if ($target == Message::TARGET_ALL) {
|
||
$targetList = [];
|
||
}
|
||
|
||
$msg = Message::findById($id);
|
||
if (empty($msg)) {
|
||
throw new TraitException('没有相关的消息记录');
|
||
}
|
||
|
||
$validate = new MessageValidate();
|
||
if (!$validate->scene('edit')->check($messageData)) {
|
||
throw new TraitException($validate->getError());
|
||
}
|
||
|
||
$dataFields = ['title', 'summary', 'content', 'send_at'];
|
||
$messageData = arrayKeysFilter($messageData, $dataFields);
|
||
|
||
if (isset($messageData['send_at']) && empty($messageData['send_at'])) {
|
||
unset($messageData['send_at']);
|
||
}
|
||
$messageData['updated_at'] = date('Y-m-d H:i:s');
|
||
$messageData['type'] = $type;
|
||
$messageData['target'] = $target;
|
||
$messageData['target_list'] = empty($targetList) ? '' : implode(',', $targetList);
|
||
|
||
return Message::where('id', $id)->update($messageData);
|
||
} catch (TraitException $e) {
|
||
throw $e;
|
||
} catch (Exception $e) {
|
||
throw new TraitException('消息更新失败');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 删除消息记录
|
||
*
|
||
* @param array $ids
|
||
* @return bool
|
||
*/
|
||
public function deleteMessages(array $ids): bool
|
||
{
|
||
try {
|
||
Message::deleteByIds($ids);
|
||
// 删除关联的阅读记录
|
||
MessageLog::whereIn('message_id', $ids)->delete();
|
||
} catch (Exception $e) {
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 发送预约审核的订阅消息
|
||
*
|
||
* 注1:订阅消息需要在用户预约后订阅成功后才能发送,每次订阅只能推送一条订阅消息;
|
||
* 注2:用户取消订阅后无法下发订阅消息
|
||
* 注3:$msgData 参数格式必须严格按照消息模板字段的格式限制填写
|
||
*
|
||
* @param string $openid 接收者openid
|
||
* @param array $msgData 消息体 样例:['date1'=>date('Y年m月d日'), 'thing2'=>'植发', 'character_string12'=>'8:30 ~ 9:30']
|
||
* @param string $state 跳转小程序类型。 developer为开发版;trial为体验版;formal为正式版;默认为正式版
|
||
* @return array ['status'=>[false|true], 'msgid'=>'']
|
||
*/
|
||
public function sendAppointmentSubscribeMessage(string $openid, array $msgData, string $state = 'formal'): array
|
||
{
|
||
$resp = ['status' => false, 'msgid' => ''];
|
||
// 跳转到预约列表界面
|
||
$page = Config::MINI_PATH_APPOINTMENT;
|
||
|
||
try {
|
||
if (empty($openid) || empty($msgData)) {
|
||
return $resp;
|
||
}
|
||
|
||
$wxApp = WechatApplets::getInstance();
|
||
$dataList = [];
|
||
foreach ($msgData as $tk => $tv) {
|
||
$dataList[$tk]['value'] = $tv;
|
||
}
|
||
|
||
$templateId = WechatApplets::SUBSCRIBE_TPL_APPOINTMENT;
|
||
$data = [
|
||
// 所需下发的订阅模板id
|
||
'template_id' => $templateId,
|
||
// 接收者(用户)的 openid
|
||
'touser' => $openid,
|
||
// 点击模板卡片后的跳转页面,仅限本小程序内的页面。支持带参数,(示例index?foo=bar)。该字段不填则模板无跳转。
|
||
'page' => $page,
|
||
//跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版
|
||
'miniprogram_state' => $state,
|
||
// 模板内容,格式形如 { "key1": { "value": any }, "key2": { "value": any } }
|
||
'data' => $dataList,
|
||
|
||
];
|
||
|
||
$res = $wxApp->subscribe_message->send($data);
|
||
if (isset($res['errcode']) && $res['errcode'] == 0) {
|
||
$resp = ['status' => true, 'msgid' => $res['msgid'] ?? ''];
|
||
}
|
||
} catch (GuzzleException | Exception $e) {
|
||
|
||
}
|
||
|
||
return $resp;
|
||
}
|
||
|
||
/**
|
||
* 订阅消息发送
|
||
*
|
||
* @param int $messageId
|
||
* @param string $templateId
|
||
* @param array $targetList
|
||
* @param array $messageData
|
||
* @param string $page
|
||
* @param string $state
|
||
* @throws GuzzleException
|
||
* @throws RepositoryException
|
||
*/
|
||
public function subscribeSend(int $messageId, string $templateId, array $targetList = [], array $messageData = [], string $page = '', string $state = '')
|
||
{
|
||
try {
|
||
$accountModel = new Account();
|
||
if (!empty($targetList)) {
|
||
$accountModel = $accountModel->whereIn('id', $targetList);
|
||
}
|
||
|
||
$accountList = $accountModel->field('openid, id, nickname')->select();
|
||
|
||
// ['template_id' => '模版ID', 'msg' => msgBody, 'page' => '小程序跳转页面', 'state' => '小程序类型']
|
||
$data = [
|
||
'template_id' => $templateId,
|
||
'msg' => $messageData,
|
||
'page' => $page
|
||
];
|
||
|
||
if (!empty($state)) {
|
||
$data['state'] = $state;
|
||
}
|
||
|
||
WechatApplets::sendBatchSubscribeMessage($messageId, $accountList->column('openid'), $data);
|
||
} catch (RepositoryException | Exception $e) {
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 短信发送
|
||
*
|
||
* @param int $messageId
|
||
* @param string $templateId
|
||
* @param array $targetList
|
||
* @param array $messageData
|
||
* @throws GuzzleException
|
||
* @throws RepositoryException
|
||
*/
|
||
public function smsSendBak(int $messageId, string $templateId, array $targetList = [], array $messageData = [])
|
||
{
|
||
try {
|
||
// var_dump($messageData);
|
||
// exit;
|
||
$accountModel = new Account();
|
||
if (!empty($targetList)) {
|
||
$accountModel = $accountModel->whereIn('id', $targetList);
|
||
}
|
||
|
||
$accountList = $accountModel->field('mobile, id, nickname')->select();
|
||
|
||
// ['template_id' => '模版ID', 'msg' => msgBody]
|
||
$data = [
|
||
'phoneNumberJson' => '', // 如'["13541194xxxx"]'
|
||
'signNameJson' => '',// 如 '["商城","商城"]'
|
||
'templateCode' => '',
|
||
'templateParamJson' => '', //如 '[{"code":"12345"},{"code":"333"}]'
|
||
];
|
||
|
||
if ($accountList->count() <= 1000) {
|
||
$templateParamArr = [];
|
||
$signNameArr = [];
|
||
$mobileArr = [];
|
||
foreach ($accountModel->column('mobile') as $mobile) {
|
||
if (!empty($mobile)) {
|
||
array_push($mobileArr, $mobile);
|
||
array_push($signNameArr, Sms::SMS_SIGN);
|
||
if (!empty($messageData)) {
|
||
array_push($templateParamArr, $messageData);
|
||
}
|
||
}
|
||
}
|
||
|
||
$data['phoneNumberJson'] = json_encode($mobileArr, JSON_UNESCAPED_UNICODE);
|
||
$data['signNameJson'] = json_encode($signNameArr, JSON_UNESCAPED_UNICODE);
|
||
$data['templateCode'] = $templateId;
|
||
$data['templateParamJson'] = json_encode($templateParamArr, JSON_UNESCAPED_UNICODE);
|
||
|
||
Log::error('[短信批量发送日志]'.json_encode($data, JSON_UNESCAPED_UNICODE));
|
||
Sms::batchSendByApi($data);
|
||
} else {
|
||
Sms::batchSendByApi($data);
|
||
}
|
||
} catch (RepositoryException | Exception $e) {
|
||
throw $e;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 短信发送[支持批量]
|
||
*
|
||
* @param int $messageId
|
||
* @param string $templateId
|
||
* @param array $targetList 用户ID列表
|
||
* @param array $messageData
|
||
* @throws Exception
|
||
* @return array
|
||
*/
|
||
public function smsSend(int $messageId, string $templateId, array $targetList = [], array $messageData = []): array
|
||
{
|
||
try {
|
||
$accountModel = new Account();
|
||
if (!empty($targetList)) {
|
||
$accountModel = $accountModel->whereIn('id', $targetList);
|
||
}
|
||
|
||
$accountList = $accountModel->whereNotNull('mobile')->field('mobile, id, nickname')->select();
|
||
|
||
Log::error('[短信批量发送日志]'.json_encode($messageData, JSON_UNESCAPED_UNICODE));
|
||
|
||
$smsLog = [];
|
||
$now = date('Y-m-d H:i:s');
|
||
$successNum = 0;
|
||
$failNum = 0;
|
||
$total = $accountList->count();
|
||
foreach ($accountList->column('mobile') as $mobile) {
|
||
if (empty($mobile)) {
|
||
continue;
|
||
}
|
||
|
||
$arr = [];
|
||
$arr['phone'] = $mobile;
|
||
$arr['type'] = $templateId;
|
||
$arr['created_at'] = $now;
|
||
$arr['data'] = !empty($messageData) ? json_encode($messageData, JSON_UNESCAPED_UNICODE) : '';
|
||
$arr['message_id'] = $messageId;
|
||
$arr['status'] = Sms::STATUS_SUCCESS;
|
||
|
||
$res = Sms::send($mobile, $messageData, $templateId);
|
||
|
||
$success = true;
|
||
if ($res !== true) {
|
||
$arr['status'] = Sms::STATUS_FAIL;
|
||
$arr['remarks'] = $res;
|
||
$success = false;
|
||
}
|
||
|
||
if ($success) {
|
||
$successNum++;
|
||
} else {
|
||
$failNum++;
|
||
}
|
||
|
||
$smsLog[] = $arr;
|
||
}
|
||
|
||
if (!empty($smsLog)) {
|
||
(new SmsLog())->insertAll($smsLog);
|
||
}
|
||
|
||
return [
|
||
'status' => $total == $successNum,
|
||
'msg' => sprintf("总数%d条,发送成功%d条,发送失败%d条", $total, $successNum, $failNum)
|
||
];
|
||
} catch (Exception $e) {
|
||
return [
|
||
'status' => false,
|
||
'msg' => $e->getMessage()
|
||
];
|
||
}
|
||
}
|
||
|
||
} |