fix(plan): 修复客资列表500并支持全局Webhook回退
traffic_pool_company 查询改用 identifier|phone|ownerWechatId;扩展查询失败降级不抛500;计划未配 Webhook 时回退全局配置。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -546,6 +546,4 @@ class PlanSceneV1Controller extends BaseController
|
||||
return ResponseHelper::success($data, '获取成功');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
28
Server/tests/task_customer_list_smoke.php
Normal file
28
Server/tests/task_customer_list_smoke.php
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user