$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 */ 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> */ 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> */ 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|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; } }