fix(plan): 修复客资列表500并支持全局Webhook回退

traffic_pool_company 查询改用 identifier|phone|ownerWechatId;扩展查询失败降级不抛500;计划未配 Webhook 时回退全局配置。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Manus AI
2026-05-25 13:17:46 +08:00
parent 434d09183b
commit db85605352
5 changed files with 140 additions and 36 deletions

View File

@@ -145,7 +145,9 @@ const CustomerListModal: React.FC<CustomerListModalProps> = ({
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) {

View File

@@ -546,6 +546,4 @@ class PlanSceneV1Controller extends BaseController
return ResponseHelper::success($data, '获取成功');
}
}
}

View File

@@ -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',
];
}

View File

@@ -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<int,array<string,mixed>>
*/
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

View File

@@ -0,0 +1,28 @@
<?php
/**
* 客资列表 smoke验证 enrich 不因 traffic_pool 字段差异 500
* 用法docker exec cunkebao-server php /var/www/html/tests/task_customer_list_smoke.php [planId]
*/
require __DIR__ . '/../thinkphp/base.php';
\think\Container::get('app')->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);
}