企业微信外部联系人同步
This commit is contained in:
@@ -15,12 +15,21 @@ class WechatWorkService
|
||||
{
|
||||
private $corpId;
|
||||
private $secret;
|
||||
private $contactSecret;
|
||||
private $accessToken;
|
||||
private $contactAccessToken;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->corpId = config('pay.wechat_work.corp_id');
|
||||
$this->secret = config('pay.wechat_work.external_pay_secret');
|
||||
$this->contactSecret = config('pay.wechat_work.contact_secret');
|
||||
|
||||
Log::info('企业微信配置: ' . json_encode([
|
||||
'corp_id' => $this->corpId,
|
||||
'secret' => substr($this->secret, 0, 10) . '...',
|
||||
'contact_secret' => substr($this->contactSecret, 0, 10) . '...'
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,12 +72,80 @@ class WechatWorkService
|
||||
throw new \Exception('获取access_token失败: ' . ($response['errmsg'] ?? '未知错误'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取客户联系的access_token
|
||||
* @see https://developer.work.weixin.qq.com/document/path/92113
|
||||
*/
|
||||
private function getContactAccessToken(): string
|
||||
{
|
||||
if ($this->contactAccessToken) {
|
||||
return $this->contactAccessToken;
|
||||
}
|
||||
|
||||
// 从缓存获取
|
||||
$cacheKey = 'qywx_contact_access_token';
|
||||
$token = Cache::get($cacheKey);
|
||||
if ($token) {
|
||||
$this->contactAccessToken = $token;
|
||||
return $token;
|
||||
}
|
||||
|
||||
// 尝试使用 contactSecret 获取 token
|
||||
$token = $this->requestToken($this->contactSecret, $cacheKey, '客户联系');
|
||||
if ($token) {
|
||||
$this->contactAccessToken = $token;
|
||||
return $token;
|
||||
}
|
||||
|
||||
// 如果 contactSecret 失败,尝试使用 secret
|
||||
Log::info('企业微信-使用 contactSecret 获取 token 失败,尝试使用 secret');
|
||||
$token = $this->requestToken($this->secret, $cacheKey, '普通应用');
|
||||
if ($token) {
|
||||
$this->contactAccessToken = $token;
|
||||
return $token;
|
||||
}
|
||||
|
||||
throw new \Exception('获取客户联系access_token失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 请求获取 token
|
||||
*/
|
||||
private function requestToken(string $secret, string $cacheKey, string $type): string
|
||||
{
|
||||
$url = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken';
|
||||
$params = [
|
||||
'corpid' => $this->corpId,
|
||||
'corpsecret' => $secret,
|
||||
];
|
||||
|
||||
Log::info('企业微信-获取' . $type . 'AccessToken请求: ' . json_encode($params, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$response = $this->httpGet($url, $params);
|
||||
|
||||
Log::info('企业微信-获取' . $type . 'AccessToken响应: ' . json_encode($response, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
if (isset($response['access_token'])) {
|
||||
$token = $response['access_token'];
|
||||
$expiresIn = $response['expires_in'] ?? 7200;
|
||||
|
||||
// 缓存token,提前5分钟过期
|
||||
Cache::set($cacheKey, $token, $expiresIn - 300);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
Log::error('获取' . $type . 'token失败: ' . json_encode($response));
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取部门成员列表
|
||||
* @see https://developer.work.weixin.qq.com/document/path/90200
|
||||
*/
|
||||
public function getDepartmentUserList(int $departmentId = 1, bool $fetchChild = true): array
|
||||
{
|
||||
// 获取部门成员列表使用 external_pay_secret(普通应用Secret)
|
||||
$accessToken = $this->getAccessToken();
|
||||
$url = 'https://qyapi.weixin.qq.com/cgi-bin/user/list';
|
||||
$params = [
|
||||
@@ -77,6 +154,8 @@ class WechatWorkService
|
||||
'fetch_child' => $fetchChild ? 1 : 0,
|
||||
];
|
||||
|
||||
Log::info('企业微信-获取部门成员列表请求: ' . json_encode($params, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$response = $this->httpGet($url, $params);
|
||||
|
||||
Log::info('企业微信-获取部门成员列表响应: ' . json_encode($response, JSON_UNESCAPED_UNICODE));
|
||||
@@ -93,17 +172,37 @@ class WechatWorkService
|
||||
* @notes 获取成员的客户列表
|
||||
* @see https://developer.work.weixin.qq.com/document/path/92113
|
||||
* @param string $userId 成员ID
|
||||
* @param string $cursor 分页游标,上次返回的 next_cursor
|
||||
* @param int $limit 最大记录数,最大 100,默认 50
|
||||
* @return array|false 返回客户列表数组,权限错误返回false
|
||||
*/
|
||||
public function getExternalContactList(string $userId)
|
||||
public function getExternalContactList(string $userId, string $cursor = '', int $limit = 50)
|
||||
{
|
||||
$accessToken = $this->getAccessToken();
|
||||
// 生成缓存键
|
||||
$cacheKey = 'qywx_external_contact_list_' . $userId . '_' . $cursor . '_' . $limit;
|
||||
|
||||
// 尝试从缓存获取
|
||||
$cachedResult = Cache::get($cacheKey);
|
||||
if ($cachedResult) {
|
||||
Log::info('从缓存获取客户列表: ' . $userId);
|
||||
return $cachedResult;
|
||||
}
|
||||
|
||||
$accessToken = $this->getContactAccessToken();
|
||||
$url = 'https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list';
|
||||
$params = [
|
||||
'access_token' => $accessToken,
|
||||
'userid' => $userId,
|
||||
];
|
||||
|
||||
// 添加分页参数
|
||||
if (!empty($cursor)) {
|
||||
$params['cursor'] = $cursor;
|
||||
}
|
||||
if ($limit > 0) {
|
||||
$params['limit'] = min($limit, 100); // 最大100
|
||||
}
|
||||
|
||||
$response = $this->httpGet($url, $params);
|
||||
|
||||
Log::info('企业微信-获取成员客户列表响应 userId=' . $userId . ': ' . json_encode($response, JSON_UNESCAPED_UNICODE));
|
||||
@@ -115,25 +214,102 @@ class WechatWorkService
|
||||
}
|
||||
|
||||
if (isset($response['external_userid'])) {
|
||||
return $response['external_userid'];
|
||||
$result = [
|
||||
'external_userid' => $response['external_userid'],
|
||||
'next_cursor' => $response['next_cursor'] ?? ''
|
||||
];
|
||||
|
||||
// 缓存结果,有效期5分钟
|
||||
Cache::set($cacheKey, $result, 300);
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 如果没有客户,返回空数组(不记录错误)
|
||||
if (isset($response['errcode']) && $response['errcode'] == 0) {
|
||||
return [];
|
||||
$result = [
|
||||
'external_userid' => [],
|
||||
'next_cursor' => ''
|
||||
];
|
||||
|
||||
// 缓存结果,有效期5分钟
|
||||
Cache::set($cacheKey, $result, 300);
|
||||
return $result;
|
||||
}
|
||||
|
||||
Log::error('获取客户列表失败: ' . json_encode($response));
|
||||
return [];
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取外部联系人信息
|
||||
* @see https://developer.work.weixin.qq.com/document/path/92114
|
||||
* @param string $externalUserId 外部用户ID
|
||||
* @return array|false 返回外部联系人信息,失败返回false
|
||||
*/
|
||||
public function getExternalContactInfo(string $externalUserId)
|
||||
{
|
||||
$accessToken = $this->getAccessToken();
|
||||
$url = 'https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get';
|
||||
$params = [
|
||||
'access_token' => $accessToken,
|
||||
'external_userid' => $externalUserId,
|
||||
];
|
||||
|
||||
$response = $this->httpGet($url, $params);
|
||||
|
||||
Log::info('企业微信-获取外部联系人信息响应: ' . json_encode($response, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
if (isset($response['external_contact'])) {
|
||||
return $response['external_contact'];
|
||||
}
|
||||
|
||||
Log::error('获取外部联系人信息失败: ' . json_encode($response));
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 生成带参数的二维码
|
||||
* @see https://developer.work.weixin.qq.com/document/path/90239
|
||||
* @param string $scene 场景值
|
||||
* @param int $expireSeconds 过期时间(秒),最大2592000
|
||||
* @return array|false 返回二维码信息,失败返回false
|
||||
*/
|
||||
public function createQrCode(string $scene, int $expireSeconds = 2592000)
|
||||
{
|
||||
$accessToken = $this->getAccessToken();
|
||||
$url = "https://qyapi.weixin.qq.com/cgi-bin/qrcode/create?access_token={$accessToken}";
|
||||
|
||||
$data = [
|
||||
"action_info" => [
|
||||
"scene" => [
|
||||
"scene_str" => $scene
|
||||
]
|
||||
],
|
||||
"expire_seconds" => $expireSeconds,
|
||||
"action_name" => "QR_SCENE"
|
||||
];
|
||||
|
||||
Log::info('企业微信-生成二维码请求: ' . json_encode($data, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$response = $this->httpPost($url, json_encode($data));
|
||||
|
||||
Log::info('企业微信-生成二维码响应: ' . json_encode($response, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
if (isset($response['ticket'])) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
Log::error('生成二维码失败: ' . json_encode($response));
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取客户详情
|
||||
* @see https://developer.work.weixin.qq.com/document/path/92114
|
||||
*/
|
||||
public function getExternalContactDetail(string $externalUserId): array
|
||||
{
|
||||
$accessToken = $this->getAccessToken();
|
||||
$accessToken = $this->getContactAccessToken();
|
||||
$url = 'https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get';
|
||||
$params = [
|
||||
'access_token' => $accessToken,
|
||||
@@ -150,6 +326,240 @@ class WechatWorkService
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取所有员工的客户列表
|
||||
* @return array 所有员工的客户ID列表,格式:[staff_id => [external_userid1, external_userid2]]
|
||||
*/
|
||||
public function getAllStaffCustomers(): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
// 获取所有员工
|
||||
$staffList = $this->getDepartmentUserList();
|
||||
|
||||
if (empty($staffList)) {
|
||||
Log::info('企业微信-未获取到员工列表');
|
||||
return $result;
|
||||
}
|
||||
|
||||
Log::info('企业微信-开始获取员工客户列表,共 ' . count($staffList) . ' 名员工');
|
||||
|
||||
foreach ($staffList as $staff) {
|
||||
$userId = $staff['userid'] ?? '';
|
||||
if (empty($userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$customerResult = $this->getExternalContactList($userId);
|
||||
|
||||
if ($customerResult === false) {
|
||||
Log::error('企业微信-员工 ' . $userId . ' 获取客户列表权限不足');
|
||||
continue;
|
||||
}
|
||||
|
||||
$customerIds = $customerResult['external_userid'] ?? [];
|
||||
|
||||
if (!empty($customerIds)) {
|
||||
$result[$userId] = [
|
||||
'name' => $staff['name'] ?? '',
|
||||
'customers' => $customerIds
|
||||
];
|
||||
Log::info('企业微信-员工 ' . $userId . '(' . ($staff['name'] ?? '') . ') 有 ' . count($customerIds) . ' 个客户');
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('企业微信-共获取到 ' . count($result) . ' 名员工的客户');
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 同步所有客户到本地数据库
|
||||
* @return array 同步结果统计
|
||||
*/
|
||||
public function syncAllExternalContacts(): array
|
||||
{
|
||||
$stats = [
|
||||
'total_staff' => 0,
|
||||
'total_customers' => 0,
|
||||
'added' => 0,
|
||||
'updated' => 0,
|
||||
'errors' => 0
|
||||
];
|
||||
|
||||
// 获取所有员工的客户
|
||||
$allStaffCustomers = $this->getAllStaffCustomers();
|
||||
|
||||
if (empty($allStaffCustomers)) {
|
||||
Log::info('企业微信-没有客户需要同步');
|
||||
return $stats;
|
||||
}
|
||||
|
||||
$stats['total_staff'] = count($allStaffCustomers);
|
||||
|
||||
foreach ($allStaffCustomers as $staffId => $staffData) {
|
||||
$staffName = $staffData['name'] ?? '';
|
||||
$customers = $staffData['customers'] ?? [];
|
||||
|
||||
foreach ($customers as $externalUserId) {
|
||||
$stats['total_customers']++;
|
||||
|
||||
try {
|
||||
// 获取客户详情
|
||||
$customerDetail = $this->getExternalContactDetail($externalUserId);
|
||||
|
||||
if (empty($customerDetail) || !isset($customerDetail['external_contact'])) {
|
||||
Log::error('企业微信-获取客户详情失败: ' . $externalUserId);
|
||||
$stats['errors']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$contactInfo = $customerDetail['external_contact'];
|
||||
|
||||
// 更新到数据库
|
||||
$result = $this->saveExternalContact($staffId, $staffName, $contactInfo);
|
||||
|
||||
if ($result) {
|
||||
$stats[$result]++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error('企业微信-同步客户异常: ' . $e->getMessage());
|
||||
$stats['errors']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('企业微信-同步完成: ' . json_encode($stats));
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 批量获取成员的客户详情(支持分页)
|
||||
* @param string $userId 成员ID
|
||||
* @param string $cursor 分页游标,上次返回的 next_cursor
|
||||
* @param int $limit 最大记录数,最大 100,默认 50
|
||||
* @return array|false 返回客户详情列表,权限错误返回false
|
||||
*/
|
||||
public function batchGetCustomerDetails(string $userId, string $cursor = '', int $limit = 50)
|
||||
{
|
||||
// 生成缓存键
|
||||
$cacheKey = 'qywx_batch_customer_details_' . $userId . '_' . $cursor . '_' . $limit;
|
||||
|
||||
// 尝试从缓存获取
|
||||
$cachedResult = Cache::get($cacheKey);
|
||||
if ($cachedResult) {
|
||||
Log::info('从缓存获取客户详情: ' . $userId);
|
||||
return $cachedResult;
|
||||
}
|
||||
|
||||
// 先获取客户ID列表
|
||||
$listResult = $this->getExternalContactList($userId, $cursor, $limit);
|
||||
if ($listResult === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$externalUserIds = $listResult['external_userid'] ?? [];
|
||||
$nextCursor = $listResult['next_cursor'] ?? '';
|
||||
|
||||
$customers = [];
|
||||
|
||||
// 批量获取客户详情
|
||||
foreach ($externalUserIds as $externalUserId) {
|
||||
$detail = $this->getExternalContactDetail($externalUserId);
|
||||
if (!empty($detail) && isset($detail['external_contact'])) {
|
||||
$customers[] = $detail;
|
||||
}
|
||||
}
|
||||
|
||||
$result = [
|
||||
'customers' => $customers,
|
||||
'next_cursor' => $nextCursor,
|
||||
'count' => count($customers)
|
||||
];
|
||||
|
||||
// 缓存结果,有效期5分钟
|
||||
Cache::set($cacheKey, $result, 300);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 保存外部联系人到数据库
|
||||
* @param string $staffId 员工ID
|
||||
* @param string $staffName 员工姓名
|
||||
* @param array $contactInfo 联系人信息
|
||||
* @return string|false 返回added/updated,失败返回false
|
||||
*/
|
||||
private function saveExternalContact(string $staffId, string $staffName, array $contactInfo)
|
||||
{
|
||||
$externalUserId = $contactInfo['external_userid'] ?? '';
|
||||
if (empty($externalUserId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 查询是否已存在
|
||||
$model = new \app\common\model\QywxExternalContact();
|
||||
$exists = $model->where('external_user_id', $externalUserId)->find();
|
||||
|
||||
$data = [
|
||||
'external_user_id' => $externalUserId,
|
||||
'name' => $contactInfo['name'] ?? '',
|
||||
'avatar' => $contactInfo['avatar'] ?? '',
|
||||
'type' => $contactInfo['type'] ?? 1,
|
||||
'gender' => $contactInfo['gender'] ?? 0,
|
||||
'unionid' => $contactInfo['unionid'] ?? '',
|
||||
'position' => $contactInfo['position'] ?? '',
|
||||
'corp_name' => $contactInfo['corp_name'] ?? '',
|
||||
'corp_full_name' => $contactInfo['corp_full_name'] ?? '',
|
||||
'external_profile' => json_encode($contactInfo['external_profile'] ?? [], JSON_UNESCAPED_UNICODE),
|
||||
'update_time' => time()
|
||||
];
|
||||
|
||||
// 构建跟进人信息
|
||||
$followUser = [
|
||||
'userid' => $staffId,
|
||||
'name' => $staffName,
|
||||
'add_time' => time()
|
||||
];
|
||||
|
||||
if ($exists) {
|
||||
// 更新已有记录
|
||||
$existsData = json_decode($exists['follow_users'] ?? '{"followers":[]}', true);
|
||||
if (!isset($existsData['followers'])) {
|
||||
$existsData = ['followers' => []];
|
||||
}
|
||||
|
||||
// 检查是否已存在该跟进人
|
||||
$found = false;
|
||||
foreach ($existsData['followers'] as &$follower) {
|
||||
if ($follower['userid'] === $staffId) {
|
||||
$follower = $followUser;
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
unset($follower);
|
||||
|
||||
if (!$found) {
|
||||
$existsData['followers'][] = $followUser;
|
||||
}
|
||||
|
||||
$data['follow_users'] = json_encode($existsData, JSON_UNESCAPED_UNICODE);
|
||||
$data['update_time'] = time();
|
||||
|
||||
$exists->save($data);
|
||||
|
||||
return 'updated';
|
||||
} else {
|
||||
// 新增记录
|
||||
$data['follow_users'] = json_encode(['followers' => [$followUser]], JSON_UNESCAPED_UNICODE);
|
||||
$data['create_time'] = time();
|
||||
|
||||
$model->save($data);
|
||||
|
||||
return 'added';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 检查应用权限(用于诊断)
|
||||
* @return array 返回应用的权限信息
|
||||
@@ -181,7 +591,85 @@ class WechatWorkService
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 创建外部客户(扫码添加)
|
||||
* @param string $staffId 员工ID
|
||||
* @param string $staffName 员工姓名
|
||||
* @param array $customerInfo 客户信息
|
||||
* @return array|false 返回创建结果,失败返回false
|
||||
*/
|
||||
public function createExternalContact(string $staffId, string $staffName, array $customerInfo)
|
||||
{
|
||||
try {
|
||||
$accessToken = $this->getContactAccessToken();
|
||||
$url = "https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_contact?access_token={$accessToken}";
|
||||
|
||||
$data = [
|
||||
"userid" => $staffId,
|
||||
"external_contact" => [
|
||||
"name" => $customerInfo['name'] ?? '未知客户',
|
||||
"mobile" => $customerInfo['mobile'] ?? '',
|
||||
"position" => $customerInfo['position'] ?? '',
|
||||
"corp_name" => $customerInfo['corp_name'] ?? '',
|
||||
"avatar" => $customerInfo['avatar'] ?? ''
|
||||
]
|
||||
];
|
||||
|
||||
Log::info('企业微信-创建外部客户请求: ' . json_encode($data, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$response = $this->httpPost($url, json_encode($data));
|
||||
|
||||
Log::info('企业微信-创建外部客户响应: ' . json_encode($response, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
if (isset($response['external_userid'])) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
Log::error('创建外部客户失败: ' . json_encode($response));
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('创建外部客户异常: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes HTTP POST请求
|
||||
*/
|
||||
private function httpPost(string $url, string $data): array
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($data)
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
$output = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
Log::error('HTTP请求失败: ' . $url . ', HTTP Code: ' . $httpCode);
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = json_decode($output, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
Log::error('JSON解析失败: ' . $output);
|
||||
return [];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes HTTP GET请求
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user