From db8560535208551f1b2ec53d8cac484bbee086ee Mon Sep 17 00:00:00 2001 From: Manus AI Date: Mon, 25 May 2026 13:17:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(plan):=20=E4=BF=AE=E5=A4=8D=E5=AE=A2?= =?UTF-8?q?=E8=B5=84=E5=88=97=E8=A1=A8500=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=85=A8=E5=B1=80Webhook=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit traffic_pool_company 查询改用 identifier|phone|ownerWechatId;扩展查询失败降级不抛500;计划未配 Webhook 时回退全局配置。 Co-authored-by: Cursor --- .../list/components/CustomerListModal.tsx | 4 +- .../controller/plan/PlanSceneV1Controller.php | 4 +- .../service/PlanLeadWebhookService.php | 49 ++++++++-- .../service/TaskCustomerListService.php | 91 ++++++++++++++----- Server/tests/task_customer_list_smoke.php | 28 ++++++ 5 files changed, 140 insertions(+), 36 deletions(-) create mode 100644 Server/tests/task_customer_list_smoke.php diff --git a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/CustomerListModal.tsx b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/CustomerListModal.tsx index a5ddefad3..289b0cb52 100644 --- a/Cunkebao/src/pages/mobile/scenarios/plan/list/components/CustomerListModal.tsx +++ b/Cunkebao/src/pages/mobile/scenarios/plan/list/components/CustomerListModal.tsx @@ -145,7 +145,9 @@ const CustomerListModal: React.FC = ({ status: statusFilter, }); Toast.show({ - content: res?.message || `已成功推送 ${res?.successCount ?? total} 条`, + content: res?.source === "global" + ? `${res?.message || "推送成功"}(使用全局 Webhook)` + : res?.message || `已成功推送 ${res?.successCount ?? total} 条`, position: "top", }); } catch (error: any) { diff --git a/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php b/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php index abff49092..687d98096 100644 --- a/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php +++ b/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php @@ -546,6 +546,4 @@ class PlanSceneV1Controller extends BaseController return ResponseHelper::success($data, '获取成功'); } - - -} \ No newline at end of file +} \ No newline at end of file diff --git a/Server/application/cunkebao/service/PlanLeadWebhookService.php b/Server/application/cunkebao/service/PlanLeadWebhookService.php index dc9b490d8..ad6705020 100644 --- a/Server/application/cunkebao/service/PlanLeadWebhookService.php +++ b/Server/application/cunkebao/service/PlanLeadWebhookService.php @@ -103,9 +103,12 @@ class PlanLeadWebhookService return ['success' => false, 'message' => '计划不存在']; } - $config = self::parseWebhookConfig($plan); - if (empty($config['enabled']) || empty($config['url'])) { - return ['success' => false, 'message' => '未启用或未配置 Webhook']; + $config = self::resolveWebhookConfig($plan); + if (empty($config['url'])) { + return ['success' => false, 'message' => '未配置 Webhook(计划与全局均未设置)']; + } + if (empty($config['enabled'])) { + return ['success' => false, 'message' => '未启用 Webhook']; } $customer = Db::name('task_customer')->where('id', $customerId)->find(); @@ -131,9 +134,9 @@ class PlanLeadWebhookService return ['success' => false, 'message' => '计划不存在']; } - $config = self::parseWebhookConfig($plan); + $config = self::resolveWebhookConfig($plan); if (empty($config['url'])) { - return ['success' => false, 'message' => '请先在计划设置中配置 Webhook 地址']; + return ['success' => false, 'message' => '请先在计划或全局设置中配置 Webhook 地址']; } $data = TaskCustomerListService::fetchList($plan, $type, array_merge($options, [ @@ -179,6 +182,7 @@ class PlanLeadWebhookService 'failCount' => $failCount, 'errors' => $errors, 'url' => $config['url'], + 'source' => $config['source'] ?? 'plan', ]; } @@ -196,9 +200,9 @@ class PlanLeadWebhookService return ['success' => false, 'message' => '计划不存在']; } - $config = self::parseWebhookConfig($plan); + $config = self::resolveWebhookConfig($plan); if (empty($config['url'])) { - return ['success' => false, 'message' => '未配置 Webhook 地址']; + return ['success' => false, 'message' => '未配置 Webhook 地址(计划与全局均未设置)']; } $customer = Db::name('task_customer')->where('id', $customerId)->find(); @@ -224,9 +228,9 @@ class PlanLeadWebhookService return ['success' => false, 'message' => '计划不存在']; } - $config = self::parseWebhookConfig($plan); + $config = self::resolveWebhookConfig($plan); if (empty($config['url'])) { - return ['success' => false, 'message' => '请先配置 Webhook 地址']; + return ['success' => false, 'message' => '请先在计划或全局设置中配置 Webhook 地址']; } $payload = self::buildTestPayload($plan); @@ -363,6 +367,33 @@ class PlanLeadWebhookService 'enabled' => !empty($sceneConf['leadPushWebhookEnabled']), 'url' => trim($sceneConf['leadPushWebhook'] ?? ''), 'secret' => trim($sceneConf['leadPushWebhookSecret'] ?? ''), + 'source' => 'plan', + ]; + } + + /** + * 计划 Webhook 优先;计划未配 URL 时回退公司全局 Webhook + */ + public static function resolveWebhookConfig(array $plan): array + { + $planConfig = self::parseWebhookConfig($plan); + if (!empty($planConfig['url'])) { + return $planConfig; + } + + $companyId = (int)($plan['companyId'] ?? 0); + $global = AcquisitionGlobalSettingsService::getConfig($companyId); + $globalWebhook = is_array($global['webhook'] ?? null) ? $global['webhook'] : []; + $globalUrl = trim((string)($globalWebhook['url'] ?? '')); + if ($globalUrl === '') { + return $planConfig; + } + + return [ + 'enabled' => !empty($globalWebhook['enabled']), + 'url' => $globalUrl, + 'secret' => trim((string)($globalWebhook['secret'] ?? '')), + 'source' => 'global', ]; } diff --git a/Server/application/cunkebao/service/TaskCustomerListService.php b/Server/application/cunkebao/service/TaskCustomerListService.php index 6b971b057..03f1d0a51 100644 --- a/Server/application/cunkebao/service/TaskCustomerListService.php +++ b/Server/application/cunkebao/service/TaskCustomerListService.php @@ -56,7 +56,18 @@ class TaskCustomerListService $list = []; foreach ($rows as $row) { - $list[] = self::enrichRow($row, $task); + try { + $list[] = self::enrichRow($row, $task); + } catch (\Throwable $e) { + $item = is_array($row) ? $row : $row->toArray(); + unset($item['processed_wechat_ids'], $item['task_id']); + $item['statusText'] = self::statusText((int)($item['status'] ?? 0)); + $item['displayName'] = trim((string)($item['name'] ?? '')) ?: (trim((string)($item['phone'] ?? '')) ?: '未命名'); + $item['userinfo'] = []; + $item['userJourney'] = []; + $item['enrichError'] = '部分扩展信息加载失败'; + $list[] = $item; + } } return ['total' => $total, 'list' => $list]; @@ -68,14 +79,7 @@ class TaskCustomerListService unset($item['processed_wechat_ids'], $item['task_id']); $phone = trim((string)($item['phone'] ?? '')); - $userinfo = []; - if ($phone !== '') { - $userinfo = Db::table('s2_wechat_friend') - ->field('alias,wechatId,nickname,avatar,conRemark,phone') - ->where('alias|wechatId|phone|conRemark', 'like', '%' . $phone . '%') - ->order('id DESC') - ->find() ?: []; - } + $userinfo = self::lookupWechatFriend($phone); $tags = !empty($item['tags']) ? (json_decode($item['tags'], true) ?: []) : []; $siteTags = !empty($item['siteTags']) ? (json_decode($item['siteTags'], true) ?: []) : []; @@ -95,7 +99,7 @@ class TaskCustomerListService $name = $nickname; } - $item['userinfo'] = $userinfo ?: new \stdClass(); + $item['userinfo'] = $userinfo; $item['tags'] = $tags; $item['siteTags'] = $siteTags; $item['statusText'] = self::statusText($status); @@ -192,13 +196,7 @@ class TaskCustomerListService $poolCompanyId = self::resolvePoolCompanyId($customer, (int)($task['companyId'] ?? 0)); if ($poolCompanyId > 0) { - $behaviors = Db::name('traffic_pool_behavior') - ->where('poolCompanyId', $poolCompanyId) - ->order('behaviorTime DESC') - ->limit(20) - ->select(); - foreach ($behaviors as $b) { - $row = is_array($b) ? $b : $b->toArray(); + foreach (self::lookupPoolBehaviors($poolCompanyId) as $row) { $journey[] = [ 'time' => self::formatTime($row['behaviorTime'] ?? 0), 'name' => (string)($row['behaviorName'] ?? '行为'), @@ -210,6 +208,49 @@ class TaskCustomerListService return $journey; } + private static function lookupWechatFriend(string $phone): array + { + if ($phone === '') { + return []; + } + + try { + return Db::table('s2_wechat_friend') + ->field('alias,wechatId,nickname,avatar,conRemark,phone') + ->where('alias|wechatId|phone|conRemark', 'like', '%' . $phone . '%') + ->order('id DESC') + ->find() ?: []; + } catch (\Throwable $e) { + return []; + } + } + + /** + * @return array> + */ + private static function lookupPoolBehaviors(int $poolCompanyId): array + { + if ($poolCompanyId <= 0) { + return []; + } + + try { + $rows = Db::name('traffic_pool_behavior') + ->where('poolCompanyId', $poolCompanyId) + ->order('behaviorTime DESC') + ->limit(20) + ->select(); + + $list = []; + foreach ($rows as $b) { + $list[] = is_array($b) ? $b : $b->toArray(); + } + return $list; + } catch (\Throwable $e) { + return []; + } + } + private static function resolvePoolCompanyId(array $customer, int $companyId): int { $phone = trim((string)($customer['phone'] ?? '')); @@ -217,13 +258,17 @@ class TaskCustomerListService return 0; } - $pool = Db::name('traffic_pool_company') - ->where(['companyId' => $companyId]) - ->where('identifier|phone|wechatId', 'like', '%' . $phone . '%') - ->order('id DESC') - ->find(); + try { + $pool = Db::name('traffic_pool_company') + ->where(['companyId' => $companyId]) + ->where('identifier|phone|ownerWechatId', 'like', '%' . $phone . '%') + ->order('id DESC') + ->find(); - return (int)($pool['id'] ?? 0); + return (int)($pool['id'] ?? 0); + } catch (\Throwable $e) { + return 0; + } } private static function formatTime($ts): string diff --git a/Server/tests/task_customer_list_smoke.php b/Server/tests/task_customer_list_smoke.php new file mode 100644 index 000000000..e2a62d12c --- /dev/null +++ b/Server/tests/task_customer_list_smoke.php @@ -0,0 +1,28 @@ +initialize(); + +$planId = (int)($argv[1] ?? 160); +$task = \think\Db::name('customer_acquisition_task')->where('id', $planId)->find(); + +if (!$task) { + fwrite(STDERR, "plan {$planId} not found\n"); + exit(1); +} + +try { + $data = \app\cunkebao\service\TaskCustomerListService::fetchList($task, 1, [ + 'page' => 1, + 'pageSize' => 20, + ]); + echo 'OK total=' . ($data['total'] ?? 0) . ' list=' . count($data['list'] ?? []) . PHP_EOL; + exit(0); +} catch (\Throwable $e) { + fwrite(STDERR, 'FAIL: ' . $e->getMessage() . PHP_EOL); + exit(1); +}