292 lines
10 KiB
PHP
292 lines
10 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\adminapi\logic\qywx;
|
||
|
||
use app\common\logic\BaseLogic;
|
||
use app\common\model\auth\Admin;
|
||
use app\common\model\QywxExternalContact;
|
||
use app\common\model\QywxMsgSendTask;
|
||
use app\common\model\QywxMsgSession;
|
||
use app\common\service\wechat\WeComFinanceSdkClient;
|
||
use app\common\service\wechat\WechatWorkService;
|
||
use think\facade\Log;
|
||
|
||
/**
|
||
* 企业微信消息业务逻辑
|
||
*
|
||
* - 员工代发消息:企业群发 add_msg_template(客户端显示为员工本人,员工手机端需确认)
|
||
* - 消息历史:来自会话内容存档落库(见 QywxMsgArchiveService)
|
||
* - 本类只封装"创建任务 + 查询送达 + 辅助接口",不处理拉取
|
||
*/
|
||
class MessageLogic extends BaseLogic
|
||
{
|
||
/**
|
||
* 创建企业群发任务
|
||
*
|
||
* @param array{sender_userid:string, external_userids:string[], msg_payload:array, chat_type?:string} $params
|
||
* @param int $adminId 后台发起人
|
||
*
|
||
* @return array{task_id:int, msg_template_id:string, fail_list: array}|false
|
||
*/
|
||
public static function createSendTask(array $params, int $adminId)
|
||
{
|
||
$chatType = (string) ($params['chat_type'] ?? 'single');
|
||
$sender = (string) $params['sender_userid'];
|
||
$externalIds = array_values(array_filter(array_map('strval', $params['external_userids'] ?? [])));
|
||
$payload = (array) $params['msg_payload'];
|
||
|
||
if ($externalIds === []) {
|
||
self::setError('请至少选择 1 位客户');
|
||
|
||
return false;
|
||
}
|
||
|
||
$body = self::buildAddMsgTemplateBody($chatType, $sender, $externalIds, $payload);
|
||
if ($body === null) {
|
||
// setError 已在 buildAddMsgTemplateBody 内调
|
||
return false;
|
||
}
|
||
|
||
$task = QywxMsgSendTask::create([
|
||
'admin_id' => $adminId,
|
||
'sender_userid' => $sender,
|
||
'external_userids' => $externalIds,
|
||
'chat_type' => $chatType === 'group' ? 2 : 1,
|
||
'msg_payload' => $payload,
|
||
'status' => QywxMsgSendTask::STATUS_PENDING,
|
||
'create_time' => time(),
|
||
'update_time' => time(),
|
||
]);
|
||
|
||
try {
|
||
$service = new WechatWorkService('customer_contact');
|
||
$resp = $service->addMsgTemplate($body);
|
||
$errcode = isset($resp['errcode']) ? (int) $resp['errcode'] : -1;
|
||
$errmsg = (string) ($resp['errmsg'] ?? '');
|
||
$msgid = (string) ($resp['msgid'] ?? '');
|
||
$failList = isset($resp['fail_list']) && is_array($resp['fail_list']) ? $resp['fail_list'] : [];
|
||
|
||
if ($errcode !== 0 || $msgid === '') {
|
||
QywxMsgSendTask::where('id', $task['id'])->update([
|
||
'status' => QywxMsgSendTask::STATUS_FAILED,
|
||
'error' => sprintf('errcode=%d errmsg=%s', $errcode, $errmsg),
|
||
'fail_list' => $failList,
|
||
'update_time' => time(),
|
||
]);
|
||
self::setError(sprintf('企微群发失败: [%d] %s', $errcode, $errmsg ?: '未知错误'));
|
||
|
||
return false;
|
||
}
|
||
|
||
QywxMsgSendTask::where('id', $task['id'])->update([
|
||
'status' => QywxMsgSendTask::STATUS_SUBMITTED,
|
||
'msg_template_id' => $msgid,
|
||
'fail_list' => $failList,
|
||
'update_time' => time(),
|
||
]);
|
||
|
||
return [
|
||
'task_id' => (int) $task['id'],
|
||
'msg_template_id' => $msgid,
|
||
'fail_list' => $failList,
|
||
];
|
||
} catch (\Throwable $e) {
|
||
Log::error('企微群发创建异常: ' . $e->getMessage());
|
||
QywxMsgSendTask::where('id', $task['id'])->update([
|
||
'status' => QywxMsgSendTask::STATUS_FAILED,
|
||
'error' => mb_substr('exception: ' . $e->getMessage(), 0, 500),
|
||
'update_time' => time(),
|
||
]);
|
||
self::setError($e->getMessage());
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询群发任务送达结果(企业微信返回每客户的确认/送达状态)
|
||
*
|
||
* @return array<string, mixed>
|
||
*/
|
||
public static function querySendTaskResult(int $taskId, string $cursor = ''): array
|
||
{
|
||
$task = QywxMsgSendTask::find($taskId);
|
||
if (!$task || $task['msg_template_id'] === '') {
|
||
return ['detail_list' => [], 'next_cursor' => '', 'task' => $task];
|
||
}
|
||
$service = new WechatWorkService('customer_contact');
|
||
$resp = $service->getGroupMsgSendResult($task['msg_template_id'], $task['sender_userid'], $cursor, 500);
|
||
$errcode = isset($resp['errcode']) ? (int) $resp['errcode'] : -1;
|
||
if ($errcode !== 0) {
|
||
return [
|
||
'detail_list' => [],
|
||
'next_cursor' => '',
|
||
'task' => $task,
|
||
'errcode' => $errcode,
|
||
'errmsg' => (string) ($resp['errmsg'] ?? ''),
|
||
];
|
||
}
|
||
$detailList = $resp['detail_list'] ?? [];
|
||
$nextCursor = (string) ($resp['next_cursor'] ?? '');
|
||
|
||
if (is_array($detailList) && !empty($detailList)) {
|
||
$sentCount = 0;
|
||
foreach ($detailList as $d) {
|
||
if (($d['status'] ?? 0) == 1) { // 1=已送达
|
||
$sentCount++;
|
||
}
|
||
}
|
||
if ($sentCount > 0 && $task['status'] == QywxMsgSendTask::STATUS_SUBMITTED) {
|
||
QywxMsgSendTask::where('id', $task['id'])->update([
|
||
'status' => QywxMsgSendTask::STATUS_SENT,
|
||
'update_time' => time(),
|
||
]);
|
||
}
|
||
}
|
||
|
||
return [
|
||
'detail_list' => $detailList,
|
||
'next_cursor' => $nextCursor,
|
||
'task' => $task->refresh(),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 可代发员工列表(只返回已绑定企微 userid 的 admin)
|
||
*
|
||
* @return array<int, array<string, mixed>>
|
||
*/
|
||
public static function staffList(string $keyword = ''): array
|
||
{
|
||
$query = Admin::where('work_wechat_userid', '<>', '')
|
||
->whereNotNull('work_wechat_userid');
|
||
if ($keyword !== '') {
|
||
$kw = addcslashes($keyword, '%_\\');
|
||
$query->where(function ($q) use ($kw) {
|
||
$q->whereLike('name', '%' . $kw . '%')
|
||
->whereOr('work_wechat_userid', 'like', '%' . $kw . '%');
|
||
});
|
||
}
|
||
|
||
return $query->field('id,name,avatar,work_wechat_userid as userid,department')
|
||
->limit(200)
|
||
->order('id', 'asc')
|
||
->select()
|
||
->toArray();
|
||
}
|
||
|
||
/**
|
||
* 某员工的已添加客户列表(基于 follow_users JSON 查询)
|
||
*
|
||
* @return array<int, array<string, mixed>>
|
||
*/
|
||
public static function customerOfStaff(string $staffUserid, string $keyword = '', int $limit = 200): array
|
||
{
|
||
if ($staffUserid === '') {
|
||
return [];
|
||
}
|
||
$query = QywxExternalContact::whereLike('follow_users', '%"userid":"' . $staffUserid . '"%');
|
||
if ($keyword !== '') {
|
||
$kw = addcslashes($keyword, '%_\\');
|
||
$query->whereLike('name', '%' . $kw . '%');
|
||
}
|
||
|
||
return $query->field('external_userid,name,avatar,type,gender,corp_name,unionid')
|
||
->limit($limit)
|
||
->order('id', 'desc')
|
||
->select()
|
||
->toArray();
|
||
}
|
||
|
||
public static function markSessionRead(int $sessionId): bool
|
||
{
|
||
$affected = QywxMsgSession::where('id', $sessionId)->update([
|
||
'unread_staff' => 0,
|
||
'update_time' => time(),
|
||
]);
|
||
|
||
return $affected >= 0;
|
||
}
|
||
|
||
/**
|
||
* 会话存档模块状态
|
||
*/
|
||
public static function archiveStatus(): array
|
||
{
|
||
$client = new WeComFinanceSdkClient();
|
||
|
||
return [
|
||
'enabled' => (bool) config('pay.wechat_work.msgaudit_enabled', false),
|
||
'available' => $client->isAvailable(),
|
||
'error' => $client->getLastError(),
|
||
'lib_path' => (string) config('pay.wechat_work.msgaudit_sdk_lib_path', ''),
|
||
'public_key_ver' => (int) config('pay.wechat_work.msgaudit_public_key_ver', 0),
|
||
'has_private_key' => self::hasPrivateKeyConfigured(),
|
||
];
|
||
}
|
||
|
||
private static function hasPrivateKeyConfigured(): bool
|
||
{
|
||
$path = (string) config('pay.wechat_work.msgaudit_private_key_path', '');
|
||
if ($path !== '' && is_file($path)) {
|
||
return true;
|
||
}
|
||
|
||
return ((string) config('pay.wechat_work.msgaudit_private_key', '')) !== '';
|
||
}
|
||
|
||
/**
|
||
* 构建 add_msg_template 请求体
|
||
*
|
||
* msg_payload 标准形:
|
||
* {
|
||
* "text": { "content": "你好" },
|
||
* "attachments": [
|
||
* { "msgtype":"image", "image":{"media_id":"xxx"} },
|
||
* { "msgtype":"video", "video":{"media_id":"xxx"} },
|
||
* { "msgtype":"file", "file":{"media_id":"xxx"} },
|
||
* { "msgtype":"link", "link":{"title":"...","picurl":"...","desc":"...","url":"https://..."} },
|
||
* { "msgtype":"miniprogram", "miniprogram":{"title":"...","pic_media_id":"...","appid":"wx...","page":"/pages/x/x"} }
|
||
* ]
|
||
* }
|
||
*
|
||
* @return array<string, mixed>|null
|
||
*/
|
||
private static function buildAddMsgTemplateBody(
|
||
string $chatType,
|
||
string $sender,
|
||
array $externalIds,
|
||
array $payload
|
||
): ?array {
|
||
$text = $payload['text'] ?? null;
|
||
$attachments = $payload['attachments'] ?? null;
|
||
if (($text === null || !isset($text['content']) || trim((string) $text['content']) === '')
|
||
&& empty($attachments)) {
|
||
self::setError('消息内容不能为空:至少填写文本或添加 1 个附件');
|
||
|
||
return null;
|
||
}
|
||
if (is_array($attachments) && count($attachments) > 9) {
|
||
self::setError('最多只能添加 9 个附件');
|
||
|
||
return null;
|
||
}
|
||
|
||
$body = [
|
||
'chat_type' => $chatType === 'group' ? 'group' : 'single',
|
||
'external_userid' => array_values($externalIds),
|
||
'sender' => $sender,
|
||
];
|
||
if (is_array($text) && trim((string) ($text['content'] ?? '')) !== '') {
|
||
$body['text'] = ['content' => (string) $text['content']];
|
||
}
|
||
if (is_array($attachments) && $attachments !== []) {
|
||
$body['attachments'] = array_values(array_filter($attachments, 'is_array'));
|
||
}
|
||
|
||
return $body;
|
||
}
|
||
}
|