企业微信外部联系人同步
This commit is contained in:
@@ -27,6 +27,7 @@ use app\common\controller\BaseLikeAdminController;
|
||||
class BaseAdminController extends BaseLikeAdminController
|
||||
{
|
||||
protected int $adminId = 0;
|
||||
protected string $adminName = '';
|
||||
protected array $adminInfo = [];
|
||||
|
||||
public function initialize()
|
||||
@@ -34,6 +35,7 @@ class BaseAdminController extends BaseLikeAdminController
|
||||
if (isset($this->request->adminInfo) && $this->request->adminInfo) {
|
||||
$this->adminInfo = $this->request->adminInfo;
|
||||
$this->adminId = $this->request->adminInfo['admin_id'];
|
||||
$this->adminName = $this->request->adminInfo['name'] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,54 @@ namespace app\adminapi\controller\qywx;
|
||||
use app\adminapi\controller\BaseAdminController;
|
||||
use app\adminapi\lists\qywx\CustomerLists;
|
||||
use app\adminapi\logic\qywx\CustomerLogic;
|
||||
use app\adminapi\validate\qywx\CustomerValidate;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Log;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
|
||||
/**
|
||||
* 企业微信客户管理控制器
|
||||
* 企业微信客户管理控制器(精简版)
|
||||
*/
|
||||
class CustomerController extends BaseAdminController
|
||||
{
|
||||
// 免登录验证
|
||||
public array $notNeedLogin = ['lists', 'stats', 'getStaffList', 'import', 'batchGetCustomers', 'getSyncSettings', 'saveSyncSettings', 'sync', 'asyncSync', 'getSyncStatus', 'getSyncTaskStatus'];
|
||||
|
||||
/**
|
||||
* @notes 批量获取企业微信客户(支持分页)
|
||||
*/
|
||||
public function batchGetCustomers()
|
||||
{
|
||||
try {
|
||||
$userId = input('userid', '');
|
||||
$cursor = input('cursor', '');
|
||||
$limit = (int)input('limit', 50);
|
||||
|
||||
if (empty($userId)) {
|
||||
return $this->fail('缺少用户ID参数');
|
||||
}
|
||||
|
||||
$service = new \app\common\service\wechat\WechatWorkService();
|
||||
$result = $service->batchGetCustomerDetails($userId, $cursor, $limit);
|
||||
|
||||
if ($result === false) {
|
||||
return $this->fail('获取客户列表失败,可能是权限不足');
|
||||
}
|
||||
|
||||
// 构建响应数据
|
||||
$response = [
|
||||
'customers' => $result['customers'] ?? [],
|
||||
'next_cursor' => $result['next_cursor'] ?? '',
|
||||
'count' => $result['count'] ?? 0,
|
||||
'limit' => $limit
|
||||
];
|
||||
|
||||
return $this->success('获取成功', $response);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('获取失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 客户列表
|
||||
*/
|
||||
@@ -23,15 +64,44 @@ class CustomerController extends BaseAdminController
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 同步企业微信客户
|
||||
* @notes 获取企业微信token
|
||||
*/
|
||||
public function sync()
|
||||
public function getAccessToken()
|
||||
{
|
||||
$result = CustomerLogic::syncCustomers();
|
||||
if ($result === false) {
|
||||
$token = CustomerLogic::getAccessToken();
|
||||
if ($token === false) {
|
||||
return $this->fail(CustomerLogic::getError());
|
||||
}
|
||||
return $this->success('同步成功', $result);
|
||||
return $this->success('获取成功', ['access_token' => $token]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取外部联系人列表
|
||||
*/
|
||||
public function getExternalContacts()
|
||||
{
|
||||
$userId = input('user_id', '');
|
||||
$contacts = CustomerLogic::getExternalContacts($userId);
|
||||
if ($contacts === false) {
|
||||
return $this->fail(CustomerLogic::getError());
|
||||
}
|
||||
return $this->success('获取成功', $contacts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取客户详情
|
||||
*/
|
||||
public function getContactDetail()
|
||||
{
|
||||
$externalUserId = input('external_user_id', '');
|
||||
if (empty($externalUserId)) {
|
||||
return $this->fail('缺少外部联系人ID');
|
||||
}
|
||||
$detail = CustomerLogic::getContactDetail($externalUserId);
|
||||
if ($detail === false) {
|
||||
return $this->fail(CustomerLogic::getError());
|
||||
}
|
||||
return $this->success('获取成功', $detail);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,13 +113,67 @@ class CustomerController extends BaseAdminController
|
||||
return $this->data($stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 按时间点统计用户数量
|
||||
*/
|
||||
public function countByTime()
|
||||
{
|
||||
try {
|
||||
$time = input('time', time());
|
||||
$timestamp = is_numeric($time) ? (int)$time : strtotime($time);
|
||||
|
||||
if (!$timestamp) {
|
||||
return $this->fail('时间格式错误');
|
||||
}
|
||||
|
||||
$count = \app\common\model\QywxExternalContact::where('created_at', '<=', $timestamp)->count();
|
||||
|
||||
return $this->success('获取成功', [
|
||||
'count' => $count,
|
||||
'time' => date('Y-m-d H:i:s', $timestamp)
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('获取失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取企业微信员工列表
|
||||
*/
|
||||
public function getStaffList()
|
||||
{
|
||||
try {
|
||||
$service = new \app\common\service\wechat\WechatWorkService();
|
||||
$userList = $service->getDepartmentUserList();
|
||||
|
||||
// 确保返回的数据是正确的UTF-8编码
|
||||
$userList = array_map(function($user) {
|
||||
return $this->recursiveConvert($user);
|
||||
}, $userList);
|
||||
|
||||
return $this->success('获取成功', $userList);
|
||||
} catch (\Throwable $e) {
|
||||
error_log('getStaffList error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
|
||||
error_log('Stack trace: ' . $e->getTraceAsString());
|
||||
return $this->fail('获取失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取同步设置
|
||||
*/
|
||||
public function getSyncSettings()
|
||||
{
|
||||
$settings = CustomerLogic::getSyncSettings();
|
||||
return $this->data($settings);
|
||||
// 从缓存获取同步设置
|
||||
$cacheKey = 'qywx_sync_settings';
|
||||
$settings = Cache::get($cacheKey, [
|
||||
'auto_sync' => false,
|
||||
'interval' => 3600,
|
||||
'limit' => 30,
|
||||
'days' => 7
|
||||
]);
|
||||
|
||||
return $this->success('获取成功', $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,11 +181,736 @@ class CustomerController extends BaseAdminController
|
||||
*/
|
||||
public function saveSyncSettings()
|
||||
{
|
||||
$params = (new CustomerValidate())->post()->goCheck('syncSettings');
|
||||
$result = CustomerLogic::saveSyncSettings($params);
|
||||
if ($result === false) {
|
||||
return $this->fail(CustomerLogic::getError());
|
||||
$settings = input('post.');
|
||||
|
||||
// 验证并设置默认值
|
||||
$validatedSettings = [
|
||||
'auto_sync' => (bool)($settings['auto_sync'] ?? false),
|
||||
'interval' => max(60, (int)($settings['interval'] ?? 3600)), // 最小1分钟
|
||||
'limit' => min(100, max(10, (int)($settings['limit'] ?? 30))), // 10-100之间
|
||||
'days' => min(30, max(1, (int)($settings['days'] ?? 7))) // 1-30天之间
|
||||
];
|
||||
|
||||
// 保存到缓存
|
||||
$cacheKey = 'qywx_sync_settings';
|
||||
Cache::set($cacheKey, $validatedSettings, 86400 * 30); // 缓存30天
|
||||
|
||||
return $this->success('保存成功', $validatedSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 同步企业微信客户
|
||||
*/
|
||||
public function sync()
|
||||
{
|
||||
try {
|
||||
$limit = (int)input('limit', 30);
|
||||
$fullSync = (bool)input('full_sync', false);
|
||||
$syncMode = input('sync_mode', 'all');
|
||||
$selectedUsers = input('selected_users', []);
|
||||
$days = (int)input('days', 7); // 最近几天的数据
|
||||
|
||||
// 设置执行超时时间
|
||||
set_time_limit(120);
|
||||
|
||||
Log::info('开始同步企业微信客户', [
|
||||
'limit' => $limit,
|
||||
'full_sync' => $fullSync,
|
||||
'sync_mode' => $syncMode,
|
||||
'selected_users' => $selectedUsers,
|
||||
'days' => $days
|
||||
]);
|
||||
|
||||
$service = new \app\common\service\wechat\WechatWorkService();
|
||||
|
||||
// 1. 获取员工列表
|
||||
$userList = $service->getDepartmentUserList();
|
||||
if (empty($userList)) {
|
||||
Log::warning('未获取到员工列表');
|
||||
return $this->fail('未获取到员工列表');
|
||||
}
|
||||
|
||||
// 按同步模式过滤员工
|
||||
if ($syncMode === 'specific' && !empty($selectedUsers)) {
|
||||
$userList = array_filter($userList, function($user) use ($selectedUsers) {
|
||||
return in_array($user['userid'] ?? '', $selectedUsers);
|
||||
});
|
||||
}
|
||||
|
||||
$syncCount = 0;
|
||||
$newCount = 0;
|
||||
$updateCount = 0;
|
||||
$processedCustomers = [];
|
||||
|
||||
// 获取上次同步时间(使用缓存存储,避免session丢失)
|
||||
$cacheKey = 'qywx_last_sync_time';
|
||||
$lastSyncTime = Cache::get($cacheKey, 0);
|
||||
$currentTime = time();
|
||||
|
||||
// 计算时间范围(最近N天)
|
||||
$timeLimit = $currentTime - ($days * 86400);
|
||||
|
||||
// 2. 遍历每个员工,获取其客户列表
|
||||
foreach ($userList as $user) {
|
||||
$userId = $user['userid'] ?? '';
|
||||
if (empty($userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Log::info('开始同步员工客户', ['user_id' => $userId, 'user_name' => $user['name'] ?? '']);
|
||||
|
||||
// 获取该成员的客户列表(支持分页)
|
||||
$cursor = '';
|
||||
$hasMore = true;
|
||||
|
||||
while ($hasMore && $syncCount < $limit) {
|
||||
$customerResult = $service->getExternalContactList($userId, $cursor, 100);
|
||||
if (empty($customerResult) || empty($customerResult['external_userid'])) {
|
||||
Log::info('员工无客户数据', ['user_id' => $userId]);
|
||||
break;
|
||||
}
|
||||
|
||||
$customerList = $customerResult['external_userid'] ?? [];
|
||||
$cursor = $customerResult['next_cursor'] ?? '';
|
||||
$hasMore = !empty($cursor);
|
||||
|
||||
Log::info('获取到员工客户列表', [
|
||||
'user_id' => $userId,
|
||||
'customer_count' => count($customerList),
|
||||
'has_more' => $hasMore
|
||||
]);
|
||||
|
||||
// 3. 处理客户,限制总数量
|
||||
foreach ($customerList as $externalUserId) {
|
||||
// 达到限制数量,停止同步
|
||||
if ($syncCount >= $limit) {
|
||||
$hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// 避免重复处理同一个客户
|
||||
if (isset($processedCustomers[$externalUserId])) {
|
||||
continue;
|
||||
}
|
||||
$processedCustomers[$externalUserId] = true;
|
||||
|
||||
// 增量同步:检查客户是否已存在且更新时间较新
|
||||
if (!$fullSync) {
|
||||
$existing = \app\common\model\QywxExternalContact::where('external_user_id', $externalUserId)->find();
|
||||
if ($existing && $existing->update_time && strtotime($existing->update_time) > time() - 86400) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取客户详情
|
||||
$detail = $service->getExternalContactDetail($externalUserId);
|
||||
if (empty($detail)) {
|
||||
Log::warning('获取客户详情失败', ['external_user_id' => $externalUserId]);
|
||||
continue;
|
||||
}
|
||||
|
||||
$externalContact = $detail['external_contact'] ?? [];
|
||||
$followUsers = $detail['follow_user'] ?? [];
|
||||
|
||||
// 获取客户实际添加时间
|
||||
$createTime = time();
|
||||
if (!empty($followUsers)) {
|
||||
// 取第一个跟进人的添加时间
|
||||
$firstFollow = reset($followUsers);
|
||||
if (isset($firstFollow['create_time'])) {
|
||||
$createTime = $firstFollow['create_time'];
|
||||
}
|
||||
}
|
||||
|
||||
// 增量同步:只同步上次同步时间之后的客户
|
||||
if (!$fullSync && $createTime <= $lastSyncTime) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 时间范围过滤:只同步最近N天的客户
|
||||
if ($createTime < $timeLimit) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 保存或更新客户信息
|
||||
$data = [
|
||||
'external_user_id' => $externalContact['external_userid'] ?? '',
|
||||
'name' => $externalContact['name'] ?? '',
|
||||
'avatar' => $externalContact['avatar'] ?? '',
|
||||
'type' => $externalContact['type'] ?? 1,
|
||||
'gender' => $externalContact['gender'] ?? 0,
|
||||
'unionid' => $externalContact['unionid'] ?? '',
|
||||
'position' => $externalContact['position'] ?? '',
|
||||
'corp_name' => $externalContact['corp_name'] ?? '',
|
||||
'corp_full_name' => $externalContact['corp_full_name'] ?? '',
|
||||
'external_profile' => json_encode($externalContact['external_profile'] ?? [], JSON_UNESCAPED_UNICODE),
|
||||
'follow_users' => json_encode($followUsers, JSON_UNESCAPED_UNICODE),
|
||||
];
|
||||
|
||||
// 处理添加人信息
|
||||
if (!empty($followUsers)) {
|
||||
// 取第一个跟进人作为添加人
|
||||
$firstFollow = reset($followUsers);
|
||||
if (isset($firstFollow['userid']) && isset($firstFollow['name'])) {
|
||||
$data['first_staff_id'] = $firstFollow['userid'];
|
||||
$data['first_staff_name'] = $firstFollow['name'];
|
||||
$data['first_add_time'] = date('Y-m-d H:i:s', $createTime);
|
||||
$data['last_staff_id'] = $firstFollow['userid'];
|
||||
$data['last_staff_name'] = $firstFollow['name'];
|
||||
$data['last_add_time'] = date('Y-m-d H:i:s', $createTime);
|
||||
}
|
||||
}
|
||||
|
||||
$existing = \app\common\model\QywxExternalContact::where('external_user_id', $data['external_user_id'])->find();
|
||||
if ($existing) {
|
||||
$existing->save($data);
|
||||
$updateCount++;
|
||||
Log::info('更新客户信息', ['external_user_id' => $data['external_user_id'], 'name' => $data['name']]);
|
||||
} else {
|
||||
\app\common\model\QywxExternalContact::create($data);
|
||||
$newCount++;
|
||||
Log::info('新增客户信息', ['external_user_id' => $data['external_user_id'], 'name' => $data['name']]);
|
||||
}
|
||||
|
||||
$syncCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 达到限制数量,停止同步
|
||||
if ($syncCount >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存本次同步时间(使用缓存存储)
|
||||
Cache::set($cacheKey, $currentTime, 86400 * 30); // 缓存30天
|
||||
|
||||
// 更新同步状态
|
||||
$syncStatus = [
|
||||
'last_sync_time' => date('Y-m-d H:i:s', $currentTime),
|
||||
'sync_count' => $syncCount,
|
||||
'new_count' => $newCount,
|
||||
'update_count' => $updateCount,
|
||||
'status' => 'completed'
|
||||
];
|
||||
Cache::set('qywx_sync_status', $syncStatus, 86400 * 30);
|
||||
|
||||
Log::info('同步企业微信客户完成', [
|
||||
'sync_count' => $syncCount,
|
||||
'new_count' => $newCount,
|
||||
'update_count' => $updateCount,
|
||||
'last_sync_time' => date('Y-m-d H:i:s', $currentTime)
|
||||
]);
|
||||
|
||||
return $this->success('同步成功', [
|
||||
'sync_count' => $syncCount,
|
||||
'new_count' => $newCount,
|
||||
'update_count' => $updateCount,
|
||||
'last_sync_time' => date('Y-m-d H:i:s', $currentTime),
|
||||
'time_limit' => date('Y-m-d H:i:s', $timeLimit)
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('同步企业微信客户失败', [
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine()
|
||||
]);
|
||||
return $this->fail('同步失败: ' . $e->getMessage());
|
||||
}
|
||||
return $this->success('保存成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取同步状态
|
||||
*/
|
||||
public function getSyncStatus()
|
||||
{
|
||||
try {
|
||||
// 从缓存获取同步状态
|
||||
$syncStatus = Cache::get('qywx_sync_status', [
|
||||
'last_sync_time' => '',
|
||||
'sync_count' => 0,
|
||||
'new_count' => 0,
|
||||
'update_count' => 0,
|
||||
'status' => 'idle'
|
||||
]);
|
||||
|
||||
return $this->success('获取成功', $syncStatus);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('获取失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 导出客户数据
|
||||
*/
|
||||
public function export()
|
||||
{
|
||||
try {
|
||||
$customers = \app\common\model\QywxExternalContact::select();
|
||||
|
||||
// 准备导出数据
|
||||
$exportData = [];
|
||||
foreach ($customers as $customer) {
|
||||
$followUsers = json_decode($customer->follow_users, true) ?? [];
|
||||
$followUserNames = array_column($followUsers, 'name');
|
||||
|
||||
$exportData[] = [
|
||||
'客户名称' => $customer->name,
|
||||
'External ID' => $customer->external_user_id,
|
||||
'性别' => $customer->gender == 1 ? '男' : ($customer->gender == 2 ? '女' : '未知'),
|
||||
'类型' => $customer->type == 1 ? '微信' : '企微',
|
||||
'企业名称' => $customer->corp_name,
|
||||
'职位' => $customer->position,
|
||||
'跟进人' => implode(', ', $followUserNames),
|
||||
'添加时间' => $customer->created_at,
|
||||
'更新时间' => $customer->updated_at
|
||||
];
|
||||
}
|
||||
|
||||
// 生成CSV文件
|
||||
$filename = 'qywx_customers_' . date('YmdHis') . '.csv';
|
||||
$filePath = sys_get_temp_dir() . '/' . $filename;
|
||||
|
||||
$fp = fopen($filePath, 'w');
|
||||
if ($fp) {
|
||||
// 写入表头
|
||||
fputcsv($fp, array_keys($exportData[0]));
|
||||
// 写入数据
|
||||
foreach ($exportData as $row) {
|
||||
fputcsv($fp, $row);
|
||||
}
|
||||
fclose($fp);
|
||||
|
||||
// 下载文件
|
||||
header('Content-Type: text/csv');
|
||||
header('Content-Disposition: attachment; filename=' . $filename);
|
||||
header('Content-Length: ' . filesize($filePath));
|
||||
readfile($filePath);
|
||||
unlink($filePath);
|
||||
exit;
|
||||
}
|
||||
|
||||
return $this->fail('导出失败');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('导出失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 导入客户数据
|
||||
*/
|
||||
public function import()
|
||||
{
|
||||
try {
|
||||
// 记录开始时间
|
||||
$startTime = microtime(true);
|
||||
|
||||
// 检查文件上传
|
||||
if (empty($_FILES['file'])) {
|
||||
$file = request()->file('file');
|
||||
if (!$file) {
|
||||
return $this->fail('请选择文件');
|
||||
}
|
||||
} else {
|
||||
$file = request()->file('file');
|
||||
}
|
||||
|
||||
if (!$file) {
|
||||
return $this->fail('请选择文件');
|
||||
}
|
||||
|
||||
// 记录文件信息
|
||||
$originalName = $file->getOriginalName();
|
||||
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
|
||||
$size = $file->getSize();
|
||||
$tmpName = $file->getRealPath();
|
||||
|
||||
// 检查扩展名
|
||||
$allowedExts = ['csv', 'xlsx'];
|
||||
if (!in_array($ext, $allowedExts)) {
|
||||
return $this->fail('请上传CSV或XLSX文件');
|
||||
}
|
||||
|
||||
// 检查文件大小(限制10MB)
|
||||
if ($size > 10 * 1024 * 1024) {
|
||||
return $this->fail('文件大小不能超过10MB');
|
||||
}
|
||||
|
||||
$importCount = 0;
|
||||
$skipCount = 0;
|
||||
$batchSize = 50; // 每批插入50条,减少内存消耗
|
||||
$batchData = [];
|
||||
|
||||
// 设置内存限制
|
||||
ini_set('memory_limit', '256M');
|
||||
// 设置执行时间
|
||||
set_time_limit(120);
|
||||
|
||||
if ($ext === 'csv') {
|
||||
// 读取CSV文件
|
||||
$fp = fopen($tmpName, 'r');
|
||||
if (!$fp) {
|
||||
return $this->fail('文件读取失败');
|
||||
}
|
||||
|
||||
try {
|
||||
// 读取表头
|
||||
$header = fgetcsv($fp);
|
||||
if (!$header) {
|
||||
fclose($fp);
|
||||
return $this->fail('文件格式错误');
|
||||
}
|
||||
|
||||
// 转换表头编码
|
||||
$header = array_map(function($item) {
|
||||
return $this->safeConvert($item);
|
||||
}, $header);
|
||||
|
||||
// 映射表头
|
||||
$headerMap = [];
|
||||
foreach ($header as $index => $field) {
|
||||
$field = trim($field);
|
||||
$headerMap[$field] = $index;
|
||||
}
|
||||
|
||||
// 读取数据
|
||||
$rowNumber = 1;
|
||||
while (($row = fgetcsv($fp)) !== false) {
|
||||
$rowNumber++;
|
||||
|
||||
// 跳过空行
|
||||
if (empty(array_filter($row))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 转换行数据编码
|
||||
$row = array_map(function($item) {
|
||||
return $this->safeConvert($item);
|
||||
}, $row);
|
||||
|
||||
// 解析数据
|
||||
$data = $this->parseImportData($row, $headerMap);
|
||||
|
||||
// 检查必填字段
|
||||
if (empty($data['name'])) {
|
||||
$skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 添加到批次
|
||||
$batchData[] = $data;
|
||||
$importCount++;
|
||||
|
||||
// 达到批次大小,执行插入
|
||||
if (count($batchData) >= $batchSize) {
|
||||
try {
|
||||
\app\common\model\QywxExternalContact::insertAll($batchData);
|
||||
} catch (\Exception $e) {
|
||||
fclose($fp);
|
||||
return $this->fail('数据插入失败: ' . $e->getMessage());
|
||||
}
|
||||
$batchData = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 插入剩余数据
|
||||
if (!empty($batchData)) {
|
||||
try {
|
||||
\app\common\model\QywxExternalContact::insertAll($batchData);
|
||||
} catch (\Exception $e) {
|
||||
fclose($fp);
|
||||
return $this->fail('数据插入失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
fclose($fp);
|
||||
return $this->fail('CSV文件解析失败: ' . $e->getMessage());
|
||||
} finally {
|
||||
if (isset($fp) && is_resource($fp)) {
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 读取XLSX文件
|
||||
try {
|
||||
$spreadsheet = IOFactory::load($tmpName);
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
// 读取表头(第一行)
|
||||
$header = [];
|
||||
$highestColumn = $worksheet->getHighestColumn();
|
||||
$highestColumnIndex = Coordinate::columnIndexFromString($highestColumn);
|
||||
|
||||
for ($col = 1; $col <= $highestColumnIndex; $col++) {
|
||||
$value = $worksheet->getCellByColumnAndRow($col, 1)->getValue();
|
||||
$header[] = trim($this->safeConvert($value));
|
||||
}
|
||||
|
||||
// 映射表头
|
||||
$headerMap = [];
|
||||
foreach ($header as $index => $field) {
|
||||
$headerMap[$field] = $index;
|
||||
}
|
||||
|
||||
// 读取数据(从第二行开始)
|
||||
$highestRow = $worksheet->getHighestRow();
|
||||
|
||||
for ($row = 2; $row <= $highestRow; $row++) {
|
||||
$rowData = [];
|
||||
for ($col = 1; $col <= $highestColumnIndex; $col++) {
|
||||
$value = $worksheet->getCellByColumnAndRow($col, $row)->getValue();
|
||||
$rowData[] = $this->safeConvert($value);
|
||||
}
|
||||
|
||||
// 跳过空行
|
||||
if (empty(array_filter($rowData))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析数据
|
||||
$data = $this->parseImportData($rowData, $headerMap);
|
||||
|
||||
// 检查必填字段
|
||||
if (empty($data['name'])) {
|
||||
$skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 添加到批次
|
||||
$batchData[] = $data;
|
||||
$importCount++;
|
||||
|
||||
// 达到批次大小,执行插入
|
||||
if (count($batchData) >= $batchSize) {
|
||||
try {
|
||||
\app\common\model\QywxExternalContact::insertAll($batchData);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail('数据插入失败: ' . $e->getMessage());
|
||||
}
|
||||
$batchData = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 插入剩余数据
|
||||
if (!empty($batchData)) {
|
||||
try {
|
||||
\app\common\model\QywxExternalContact::insertAll($batchData);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail('数据插入失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail('XLSX文件解析失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success('导入成功', [
|
||||
'import_count' => $importCount,
|
||||
'skip_count' => $skipCount
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('导入失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 解析导入数据
|
||||
*/
|
||||
private function parseImportData($row, $headerMap)
|
||||
{
|
||||
// 解析数据
|
||||
$data = [
|
||||
'name' => isset($headerMap['客户名称']) ? ($this->safeConvert($row[$headerMap['客户名称']] ?? '')) : '',
|
||||
'external_user_id' => 'import_' . uniqid(),
|
||||
'avatar' => '',
|
||||
'type' => 1,
|
||||
'gender' => 0,
|
||||
'unionid' => '',
|
||||
'position' => isset($headerMap['职务']) ? ($this->safeConvert($row[$headerMap['职务']] ?? '')) : '',
|
||||
'corp_name' => isset($headerMap['企业']) ? ($this->safeConvert($row[$headerMap['企业']] ?? '')) : '',
|
||||
'corp_full_name' => isset($headerMap['企业']) ? ($this->safeConvert($row[$headerMap['企业']] ?? '')) : '',
|
||||
'external_profile' => json_encode([], JSON_UNESCAPED_UNICODE),
|
||||
'follow_users' => json_encode([[
|
||||
'name' => isset($headerMap['添加人']) ? ($this->safeConvert($row[$headerMap['添加人']] ?? '')) : '',
|
||||
'userid' => isset($headerMap['添加人账号']) ? ($this->safeConvert($row[$headerMap['添加人账号']] ?? '')) : ''
|
||||
]], JSON_UNESCAPED_UNICODE),
|
||||
'first_staff_name' => isset($headerMap['添加人']) ? ($this->safeConvert($row[$headerMap['添加人']] ?? '')) : '',
|
||||
'last_staff_name' => isset($headerMap['添加人']) ? ($this->safeConvert($row[$headerMap['添加人']] ?? '')) : '',
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
// 处理添加时间
|
||||
if (isset($headerMap['添加时间'])) {
|
||||
$addTime = $row[$headerMap['添加时间']] ?? '';
|
||||
if (!empty($addTime)) {
|
||||
// 尝试解析时间格式
|
||||
$timestamp = strtotime($addTime);
|
||||
if ($timestamp) {
|
||||
$data['first_add_time'] = date('Y-m-d H:i:s', $timestamp);
|
||||
$data['last_add_time'] = date('Y-m-d H:i:s', $timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他字段
|
||||
if (isset($headerMap['手机'])) {
|
||||
$data['phone'] = $this->safeConvert($row[$headerMap['手机']] ?? '');
|
||||
}
|
||||
if (isset($headerMap['邮箱'])) {
|
||||
$data['email'] = $this->safeConvert($row[$headerMap['邮箱']] ?? '');
|
||||
}
|
||||
if (isset($headerMap['地址'])) {
|
||||
$data['address'] = $this->safeConvert($row[$headerMap['地址']] ?? '');
|
||||
}
|
||||
if (isset($headerMap['电话'])) {
|
||||
$data['phone'] = $this->safeConvert($row[$headerMap['电话']] ?? '');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 安全的编码转换
|
||||
*/
|
||||
private function safeConvert($value)
|
||||
{
|
||||
$type = gettype($value);
|
||||
if ($type === 'NULL') {
|
||||
return '';
|
||||
}
|
||||
if ($type === 'array') {
|
||||
return json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
if ($type === 'object') {
|
||||
$strValue = method_exists($value, '__toString') ? (string)$value : json_encode($value, JSON_UNESCAPED_UNICODE);
|
||||
if ($strValue === false) {
|
||||
return '';
|
||||
}
|
||||
$result = @mb_convert_encoding($strValue, 'UTF-8', 'auto');
|
||||
return $result ?: $strValue;
|
||||
}
|
||||
if ($type !== 'string') {
|
||||
if ($type === 'boolean') {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
if ($type === 'integer' || $type === 'double') {
|
||||
return (string)$value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
if ($value === '') {
|
||||
return '';
|
||||
}
|
||||
$result = @mb_convert_encoding($value, 'UTF-8', 'auto');
|
||||
return $result ?: $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 递归转换数组中的所有字符串为UTF-8编码
|
||||
*/
|
||||
private function recursiveConvert($data)
|
||||
{
|
||||
if (is_null($data)) {
|
||||
return '';
|
||||
}
|
||||
if (is_array($data)) {
|
||||
foreach ($data as $key => $value) {
|
||||
$data[$key] = $this->recursiveConvert($value);
|
||||
}
|
||||
} elseif (is_string($data)) {
|
||||
$data = mb_convert_encoding($data, 'UTF-8', 'auto');
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 异步同步企业微信客户
|
||||
*/
|
||||
public function asyncSync()
|
||||
{
|
||||
try {
|
||||
$limit = (int)input('limit', 30);
|
||||
$fullSync = (bool)input('full_sync', false);
|
||||
$syncMode = input('sync_mode', 'all');
|
||||
$selectedUsers = input('selected_users', []);
|
||||
$days = (int)input('days', 7); // 最近几天的数据
|
||||
|
||||
// 生成同步任务ID
|
||||
$taskId = uniqid('qywx_sync_');
|
||||
|
||||
// 构建同步参数
|
||||
$params = [
|
||||
'limit' => $limit,
|
||||
'full_sync' => $fullSync,
|
||||
'sync_mode' => $syncMode,
|
||||
'selected_users' => $selectedUsers,
|
||||
'days' => $days,
|
||||
'task_id' => $taskId
|
||||
];
|
||||
|
||||
// 保存任务状态
|
||||
$cacheKey = 'qywx_sync_task_' . $taskId;
|
||||
Cache::set($cacheKey, [
|
||||
'status' => 'pending',
|
||||
'progress' => 0,
|
||||
'start_time' => time(),
|
||||
'params' => $params
|
||||
], 86400);
|
||||
|
||||
// 执行异步同步
|
||||
$this->executeAsyncSync($params, $taskId);
|
||||
|
||||
return $this->success('同步任务已启动', [
|
||||
'task_id' => $taskId,
|
||||
'status' => 'pending'
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('启动同步任务失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取同步任务状态
|
||||
*/
|
||||
public function getSyncTaskStatus()
|
||||
{
|
||||
try {
|
||||
$taskId = input('task_id', '');
|
||||
if (empty($taskId)) {
|
||||
return $this->fail('缺少任务ID');
|
||||
}
|
||||
|
||||
$cacheKey = 'qywx_sync_task_' . $taskId;
|
||||
$taskInfo = Cache::get($cacheKey);
|
||||
|
||||
if (!$taskInfo) {
|
||||
return $this->fail('任务不存在');
|
||||
}
|
||||
|
||||
return $this->success('获取成功', $taskInfo);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->fail('获取任务状态失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 执行异步同步
|
||||
*/
|
||||
private function executeAsyncSync($params, $taskId)
|
||||
{
|
||||
// 构建命令
|
||||
$phpPath = PHP_BINARY;
|
||||
$scriptPath = realpath(__DIR__ . '/../../../../') . '/sync_qywx.php';
|
||||
|
||||
// 生成临时参数文件
|
||||
$paramFile = sys_get_temp_dir() . '/' . $taskId . '.json';
|
||||
file_put_contents($paramFile, json_encode($params));
|
||||
|
||||
// 构建执行命令
|
||||
$command = "{$phpPath} {$scriptPath} {$paramFile} {$taskId} > NUL 2>&1 &";
|
||||
|
||||
// 执行后台命令
|
||||
exec($command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,19 @@ class OperationLog
|
||||
$systemLog->type = $request->isGet() ? 'GET' : 'POST';
|
||||
$systemLog->params = json_encode($params, true);
|
||||
$systemLog->ip = $request->ip();
|
||||
$systemLog->result = $this->shortenOperationLogResult((string) $response->getContent());
|
||||
return $systemLog->save();
|
||||
|
||||
// 确保响应内容是正确的UTF-8编码
|
||||
$content = (string) $response->getContent();
|
||||
$content = mb_convert_encoding($content, 'UTF-8', 'auto');
|
||||
$systemLog->result = $this->shortenOperationLogResult($content);
|
||||
|
||||
try {
|
||||
return $systemLog->save();
|
||||
} catch (\Exception $e) {
|
||||
// 记录日志保存失败的错误,但不影响接口响应
|
||||
error_log('Operation log save failed: ' . $e->getMessage());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** MySQL TEXT 上限约 64KB,列表接口常含 base64 签名导致超长 */
|
||||
|
||||
@@ -31,8 +31,8 @@ abstract class BaseAdminDataLists extends BaseDataLists
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->adminInfo = $this->request->adminInfo;
|
||||
$this->adminId = $this->request->adminId;
|
||||
$this->adminInfo = $this->request->adminInfo ?? [];
|
||||
$this->adminId = $this->request->adminId ?? 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,14 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\lists\qywx;
|
||||
|
||||
use app\adminapi\lists\BaseAdminDataLists;
|
||||
use app\common\lists\BaseDataLists;
|
||||
use app\common\lists\ListsSearchInterface;
|
||||
use app\common\model\QywxExternalContact;
|
||||
|
||||
/**
|
||||
* 企业微信客户列表
|
||||
*/
|
||||
class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
class CustomerLists extends BaseDataLists implements ListsSearchInterface
|
||||
{
|
||||
/**
|
||||
* @notes 搜索条件
|
||||
@@ -28,29 +28,38 @@ class CustomerLists extends BaseAdminDataLists implements ListsSearchInterface
|
||||
*/
|
||||
public function lists(): array
|
||||
{
|
||||
$lists = QywxExternalContact::where($this->searchWhere)
|
||||
->whereNull('delete_time')
|
||||
->order('id', 'desc')
|
||||
->limit($this->limitOffset, $this->limitLength)
|
||||
->select()
|
||||
->toArray();
|
||||
try {
|
||||
$lists = QywxExternalContact::where($this->searchWhere)
|
||||
->order('id', 'desc')
|
||||
->limit($this->limitOffset, $this->limitLength)
|
||||
->select()
|
||||
->toArray();
|
||||
|
||||
foreach ($lists as &$item) {
|
||||
// 解析跟进人JSON
|
||||
$followUsers = json_decode($item['follow_users'] ?? '[]', true);
|
||||
$item['follow_users'] = is_array($followUsers) ? $followUsers : [];
|
||||
foreach ($lists as &$item) {
|
||||
// 解析跟进人JSON(如果字段存在)
|
||||
if (isset($item['follow_users'])) {
|
||||
$followUsers = json_decode($item['follow_users'] ?? '[]', true);
|
||||
$item['follow_users'] = is_array($followUsers) ? $followUsers : [];
|
||||
} else {
|
||||
$item['follow_users'] = [];
|
||||
}
|
||||
}
|
||||
|
||||
return $lists;
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $lists;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取数量
|
||||
* @notes 获取总数
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return QywxExternalContact::where($this->searchWhere)
|
||||
->whereNull('delete_time')
|
||||
->count();
|
||||
try {
|
||||
return QywxExternalContact::where($this->searchWhere)->count();
|
||||
} catch (\Throwable $e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,138 +6,84 @@ namespace app\adminapi\logic\qywx;
|
||||
|
||||
use app\common\logic\BaseLogic;
|
||||
use app\common\model\QywxExternalContact;
|
||||
use app\common\model\QywxSyncSettings;
|
||||
use app\common\service\wechat\WechatWorkService;
|
||||
use think\facade\Db;
|
||||
use think\facade\Log;
|
||||
use think\facade\Cache;
|
||||
|
||||
/**
|
||||
* 企业微信客户逻辑层
|
||||
* 企业微信客户逻辑层(精简版)
|
||||
*/
|
||||
class CustomerLogic extends BaseLogic
|
||||
{
|
||||
/**
|
||||
* @notes 同步企业微信客户
|
||||
* @notes 获取企业微信token
|
||||
*/
|
||||
public static function syncCustomers()
|
||||
public static function getAccessToken()
|
||||
{
|
||||
try {
|
||||
$service = new WechatWorkService();
|
||||
$token = $service->getContactAccessToken();
|
||||
return $token;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('获取企业微信token失败: ' . $e->getMessage());
|
||||
self::$error = '获取token失败: ' . $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取外部联系人列表
|
||||
* @param string $userId 员工ID
|
||||
*/
|
||||
public static function getExternalContacts(string $userId = '')
|
||||
{
|
||||
try {
|
||||
$service = new WechatWorkService();
|
||||
|
||||
// 1. 获取部门成员列表(从根部门开始,递归获取所有成员)
|
||||
$departmentId = config('project.qywx_sync_department_id', 1);
|
||||
Log::info('开始同步企业微信客户 - 部门ID: ' . $departmentId);
|
||||
|
||||
$userList = $service->getDepartmentUserList($departmentId, true);
|
||||
Log::info('获取到企业成员数量: ' . count($userList));
|
||||
|
||||
if (empty($userList)) {
|
||||
Log::error('未获取到企业成员列表');
|
||||
self::$error = '未获取到企业成员列表';
|
||||
return false;
|
||||
}
|
||||
|
||||
$syncCount = 0;
|
||||
$updateCount = 0;
|
||||
$newCount = 0;
|
||||
$processedCustomers = []; // 记录已处理的客户,避免重复
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 2. 遍历每个成员,获取其客户列表
|
||||
if (empty($userId)) {
|
||||
// 获取所有员工
|
||||
$userList = $service->getDepartmentUserList();
|
||||
if (empty($userList)) {
|
||||
self::$error = '未获取到员工列表';
|
||||
return false;
|
||||
}
|
||||
|
||||
$allContacts = [];
|
||||
foreach ($userList as $user) {
|
||||
$userId = $user['userid'] ?? '';
|
||||
if (empty($userId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Log::info('获取成员客户列表 - userId: ' . $userId . ', name: ' . ($user['name'] ?? ''));
|
||||
|
||||
// 获取该成员的客户列表
|
||||
$customerList = $service->getExternalContactList($userId);
|
||||
|
||||
// 如果返回false,说明API调用失败(权限问题)
|
||||
if ($customerList === false) {
|
||||
Log::error('获取客户列表失败 - 可能是API权限未开启(错误码48002)');
|
||||
throw new \Exception('企业微信API权限不足,请在企业微信管理后台开启"客户联系"权限');
|
||||
}
|
||||
|
||||
Log::info('成员 ' . $userId . ' 的客户数量: ' . count($customerList));
|
||||
|
||||
if (empty($customerList)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. 遍历客户列表,获取详情并保存
|
||||
foreach ($customerList as $externalUserId) {
|
||||
// 避免重复处理同一个客户
|
||||
if (isset($processedCustomers[$externalUserId])) {
|
||||
continue;
|
||||
}
|
||||
$processedCustomers[$externalUserId] = true;
|
||||
|
||||
// 获取客户详情
|
||||
$detail = $service->getExternalContactDetail($externalUserId);
|
||||
if (empty($detail)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$externalContact = $detail['external_contact'] ?? [];
|
||||
$followUsers = $detail['follow_user'] ?? [];
|
||||
|
||||
// 保存或更新客户信息
|
||||
$data = [
|
||||
'external_userid' => $externalContact['external_userid'] ?? '',
|
||||
'name' => $externalContact['name'] ?? '',
|
||||
'avatar' => $externalContact['avatar'] ?? '',
|
||||
'type' => $externalContact['type'] ?? 1,
|
||||
'gender' => $externalContact['gender'] ?? 0,
|
||||
'unionid' => $externalContact['unionid'] ?? '',
|
||||
'position' => $externalContact['position'] ?? '',
|
||||
'corp_name' => $externalContact['corp_name'] ?? '',
|
||||
'corp_full_name' => $externalContact['corp_full_name'] ?? '',
|
||||
'external_profile' => json_encode($externalContact['external_profile'] ?? [], JSON_UNESCAPED_UNICODE),
|
||||
'follow_users' => json_encode($followUsers, JSON_UNESCAPED_UNICODE),
|
||||
'update_time' => time(),
|
||||
$userContacts = $service->getExternalContactList($user['userid'] ?? '');
|
||||
if (!empty($userContacts)) {
|
||||
$allContacts[$user['userid']] = [
|
||||
'name' => $user['name'] ?? '',
|
||||
'contacts' => $userContacts
|
||||
];
|
||||
|
||||
$existing = QywxExternalContact::where('external_userid', $data['external_userid'])->find();
|
||||
if ($existing) {
|
||||
$existing->save($data);
|
||||
$updateCount++;
|
||||
} else {
|
||||
$data['create_time'] = time();
|
||||
QywxExternalContact::create($data);
|
||||
$newCount++;
|
||||
}
|
||||
|
||||
$syncCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 更新同步设置
|
||||
self::updateSyncStatus('success', $syncCount);
|
||||
|
||||
Db::commit();
|
||||
|
||||
Log::info('同步完成 - 总数: ' . $syncCount . ', 新增: ' . $newCount . ', 更新: ' . $updateCount);
|
||||
|
||||
return [
|
||||
'sync_count' => $syncCount,
|
||||
'new_count' => $newCount,
|
||||
'update_count' => $updateCount,
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
Db::rollback();
|
||||
Log::error('同步企业微信客户失败: ' . $e->getMessage());
|
||||
self::$error = '同步失败: ' . $e->getMessage();
|
||||
self::updateSyncStatus('failed', 0, $e->getMessage());
|
||||
return false;
|
||||
return $allContacts;
|
||||
} else {
|
||||
// 获取指定员工的客户
|
||||
$contacts = $service->getExternalContactList($userId);
|
||||
return $contacts;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('同步企业微信客户异常: ' . $e->getMessage());
|
||||
self::$error = '同步异常: ' . $e->getMessage();
|
||||
self::updateSyncStatus('failed', 0, $e->getMessage());
|
||||
Log::error('获取外部联系人失败: ' . $e->getMessage());
|
||||
self::$error = '获取联系人失败: ' . $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取客户详情
|
||||
* @param string $externalUserId 外部联系人ID
|
||||
*/
|
||||
public static function getContactDetail(string $externalUserId)
|
||||
{
|
||||
try {
|
||||
$service = new WechatWorkService();
|
||||
$detail = $service->getExternalContactDetail($externalUserId);
|
||||
return $detail;
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('获取客户详情失败: ' . $e->getMessage());
|
||||
self::$error = '获取详情失败: ' . $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -147,119 +93,39 @@ class CustomerLogic extends BaseLogic
|
||||
*/
|
||||
public static function getStats()
|
||||
{
|
||||
$total = QywxExternalContact::whereNull('delete_time')->count();
|
||||
|
||||
$todayStart = strtotime(date('Y-m-d 00:00:00'));
|
||||
$today = QywxExternalContact::where('create_time', '>=', $todayStart)
|
||||
->whereNull('delete_time')
|
||||
$total = QywxExternalContact::count();
|
||||
|
||||
$todayStart = date('Y-m-d 00:00:00');
|
||||
$today = QywxExternalContact::where('created_at', '>=', $todayStart)
|
||||
->count();
|
||||
|
||||
$settings = self::getSyncSettings();
|
||||
$lastSyncTime = $settings['last_sync_time'] ?? 0;
|
||||
$lastSync = $lastSyncTime > 0 ? date('Y-m-d H:i:s', $lastSyncTime) : '';
|
||||
// 获取同步状态
|
||||
$syncStatus = Cache::get('qywx_sync_status', [
|
||||
'last_sync_time' => '',
|
||||
'status' => 'idle'
|
||||
]);
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'today' => $today,
|
||||
'lastSync' => $lastSync,
|
||||
'syncStatus' => $settings['sync_status'] ?? 'idle',
|
||||
'syncStatusText' => self::getSyncStatusText($settings['sync_status'] ?? 'idle'),
|
||||
'lastSync' => $syncStatus['last_sync_time'] ?? '',
|
||||
'syncStatus' => $syncStatus['status'] ?? 'idle',
|
||||
'syncStatusText' => self::getStatusText($syncStatus['status'] ?? 'idle')
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取同步设置
|
||||
*/
|
||||
public static function getSyncSettings()
|
||||
{
|
||||
$setting = QywxSyncSettings::where('setting_key', 'customer_sync')->find();
|
||||
if (!$setting) {
|
||||
return [
|
||||
'auto_sync' => false,
|
||||
'interval' => 3600,
|
||||
'last_sync_time' => 0,
|
||||
'sync_status' => 'idle',
|
||||
];
|
||||
}
|
||||
|
||||
$value = json_decode($setting->setting_value, true);
|
||||
return $value ?: [
|
||||
'auto_sync' => false,
|
||||
'interval' => 3600,
|
||||
'last_sync_time' => 0,
|
||||
'sync_status' => 'idle',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 保存同步设置
|
||||
*/
|
||||
public static function saveSyncSettings(array $params)
|
||||
{
|
||||
try {
|
||||
$currentSettings = self::getSyncSettings();
|
||||
$newSettings = array_merge($currentSettings, [
|
||||
'auto_sync' => (bool)($params['auto_sync'] ?? false),
|
||||
'interval' => (int)($params['interval'] ?? 3600),
|
||||
]);
|
||||
|
||||
$setting = QywxSyncSettings::where('setting_key', 'customer_sync')->find();
|
||||
if ($setting) {
|
||||
$setting->setting_value = json_encode($newSettings, JSON_UNESCAPED_UNICODE);
|
||||
$setting->update_time = time();
|
||||
$setting->save();
|
||||
} else {
|
||||
QywxSyncSettings::create([
|
||||
'setting_key' => 'customer_sync',
|
||||
'setting_value' => json_encode($newSettings, JSON_UNESCAPED_UNICODE),
|
||||
'create_time' => time(),
|
||||
'update_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
self::$error = '保存失败: ' . $e->getMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 更新同步状态
|
||||
*/
|
||||
private static function updateSyncStatus(string $status, int $syncCount = 0, string $error = '')
|
||||
{
|
||||
try {
|
||||
$settings = self::getSyncSettings();
|
||||
$settings['sync_status'] = $status;
|
||||
$settings['last_sync_time'] = time();
|
||||
$settings['last_sync_count'] = $syncCount;
|
||||
if ($error) {
|
||||
$settings['last_error'] = $error;
|
||||
}
|
||||
|
||||
$setting = QywxSyncSettings::where('setting_key', 'customer_sync')->find();
|
||||
if ($setting) {
|
||||
$setting->setting_value = json_encode($settings, JSON_UNESCAPED_UNICODE);
|
||||
$setting->update_time = time();
|
||||
$setting->save();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('更新同步状态失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notes 获取同步状态文本
|
||||
*/
|
||||
private static function getSyncStatusText(string $status): string
|
||||
private static function getStatusText($status)
|
||||
{
|
||||
$map = [
|
||||
$statusMap = [
|
||||
'idle' => '未同步',
|
||||
'syncing' => '同步中',
|
||||
'success' => '同步成功',
|
||||
'failed' => '同步失败',
|
||||
'running' => '同步中',
|
||||
'completed' => '同步成功',
|
||||
'failed' => '同步失败'
|
||||
];
|
||||
return $map[$status] ?? '未知';
|
||||
|
||||
return $statusMap[$status] ?? '未同步';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\adminapi\validate\qywx;
|
||||
|
||||
use app\common\validate\BaseValidate;
|
||||
|
||||
/**
|
||||
* 企业微信客户验证器
|
||||
*/
|
||||
class CustomerValidate extends BaseValidate
|
||||
{
|
||||
protected $rule = [
|
||||
'auto_sync' => 'require|boolean',
|
||||
'interval' => 'require|integer|between:3600,86400',
|
||||
];
|
||||
|
||||
protected $message = [
|
||||
'auto_sync.require' => '请选择是否自动同步',
|
||||
'auto_sync.boolean' => '自动同步参数格式错误',
|
||||
'interval.require' => '请选择同步间隔',
|
||||
'interval.integer' => '同步间隔必须为整数',
|
||||
'interval.between' => '同步间隔必须在1小时到24小时之间',
|
||||
];
|
||||
|
||||
/**
|
||||
* @notes 同步设置场景
|
||||
*/
|
||||
public function sceneSyncSettings()
|
||||
{
|
||||
return $this->only(['auto_sync', 'interval']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user