*/ class OpenScenariosController extends Controller { /** * 线索上报入口 * POST /v1/open/scenarios */ public function submit() { try { // ── 1. 从 JWT 中取当前用户(由 jwt 中间件注入)───────────────── $userInfo = $this->request->userInfo ?? []; $companyId = (int)($userInfo['companyId'] ?? 0); if (empty($userInfo['id']) || empty($companyId)) { return $this->error('未授权访问', 401); } $params = $this->request->param(); // ── 2. planId 校验(必须属于当前账号的 companyId,且已启用)── if (empty($params['planId'])) { return $this->error('planId不能为空', 400); } $plan = Db::name('customer_acquisition_task') ->where('id', intval($params['planId'])) ->where('companyId', $companyId) ->where('status', 1) ->find(); if (!$plan) { return $this->error('计划不存在或已停用', 404); } // ── 3. 主标识(wechatId 优先,phone 次之)───────────────────── $wechatId = trim($params['wechatId'] ?? ''); $phone = trim($params['phone'] ?? ''); $identifier = $wechatId ?: $phone; if (empty($identifier)) { return $this->error('wechatId 和 phone 至少传一个', 400); } // ── 4. 渠道 ID(可选,分销场景)────────────────────────────── $channelId = !empty($params['cid']) ? intval($params['cid']) : 0; $finalChannelId = $this->resolveChannelId($channelId, $plan); // ── 5. 查重 & 写入 task_customer ────────────────────────────── $taskCustomer = Db::name('task_customer') ->where('task_id', $plan['id']) ->where('phone', $identifier) ->find(); if ($taskCustomer) { // 已存在:仅追加站内标签 if (!empty($params['siteTags'])) { $this->mergeSiteTags( $taskCustomer['id'], explode(',', $params['siteTags']) ); } return $this->success($identifier, '已存在'); } // 新线索 $tags = !empty($params['tags']) ? explode(',', $params['tags']) : []; $siteTags = !empty($params['siteTags']) ? explode(',', $params['siteTags']) : []; $customerId = Db::name('task_customer')->insertGetId([ 'task_id' => $plan['id'], 'channelId' => $finalChannelId, 'phone' => $identifier, 'name' => $params['name'] ?? '', 'source' => $params['source'] ?? '', 'remark' => $params['remark'] ?? '', 'tags' => json_encode($tags, JSON_UNESCAPED_UNICODE), 'siteTags' => json_encode($siteTags, JSON_UNESCAPED_UNICODE), 'createTime' => time(), ]); // ── 6. 同步到 V2 流量池(异步,不影响主流程)───────────────── if ($customerId) { $this->syncToTrafficPool( $identifier, $phone, $wechatId, $companyId, $plan, $params, $finalChannelId, $customerId ); } // ── 7. 分销获客奖励(异步,不影响主流程)──────────────────── if ($customerId && $finalChannelId > 0) { try { DistributionRewardService::recordCustomerReward( $plan['id'], $customerId, $identifier, $finalChannelId ); } catch (\Exception $e) { Log::error('[OpenScenarios] 记录获客奖励失败:' . $e->getMessage()); } } return $this->success($identifier, '新增成功'); } catch (\Exception $e) { Log::error('[OpenScenarios] 系统错误:' . $e->getMessage() . "\n" . $e->getTraceAsString()); return $this->error('系统错误: ' . $e->getMessage(), 500); } } // ───────────────────────────────────────────────────────────────────────── private function resolveChannelId(int $channelId, array $plan): int { if ($channelId <= 0) { return 0; } $sceneConf = json_decode($plan['sceneConf'] ?? '{}', true) ?: []; $distConfig = $sceneConf['distribution'] ?? []; $allowedIds = $distConfig['channels'] ?? []; if (empty($distConfig['enabled']) || !in_array($channelId, $allowedIds)) { return 0; } $channel = Db::name('distribution_channel') ->where([ ['id', '=', $channelId], ['companyId', '=', $plan['companyId']], ['status', '=', 'enabled'], ['deleteTime', '=', 0], ]) ->find(); return $channel ? $channelId : 0; } private function mergeSiteTags(int $taskCustomerId, array $newTags): void { try { $row = Db::name('task_customer')->where('id', $taskCustomerId)->find(); if (!$row) { return; } $existing = !empty($row['siteTags']) ? (json_decode($row['siteTags'], true) ?: []) : []; $merged = array_values(array_unique(array_filter( array_merge($existing, $newTags), fn($t) => trim($t) !== '' ))); Db::name('task_customer')->where('id', $taskCustomerId)->update([ 'siteTags' => json_encode($merged, JSON_UNESCAPED_UNICODE), 'updateTime' => time(), ]); } catch (\Exception $e) { Log::error('[OpenScenarios] 合并站内标签失败:' . $e->getMessage()); } } private function syncToTrafficPool( string $identifier, string $phone, string $wechatId, int $companyId, array $plan, array $params, int $finalChannelId, int $customerId ): void { try { $isPhone = (bool)preg_match('/^\+?\d{6,}$/', $identifier); $poolService = new TrafficPoolService(); $poolService->enterPool( $identifier, $companyId, TrafficPoolSource::SOURCE_TYPE_API, [ 'identifierType' => $isPhone ? 2 : 1, 'mobile' => $phone ?: ($isPhone ? $identifier : ''), 'wechatId' => $wechatId ?: (!$isPhone ? $identifier : ''), 'nickname' => $params['name'] ?? '', ], [ 'phone' => $phone ?: ($isPhone ? $identifier : ''), 'realName' => $params['name'] ?? '', 'remark' => $params['remark'] ?? '', ], [ 'sourceName' => !empty($params['source']) ? $params['source'] : ('场景获客_' . $plan['name']), 'remark' => $params['remark'] ?? '', 'extra' => json_encode([ 'planId' => $plan['id'], 'planName' => $plan['name'], 'channelId' => $finalChannelId, 'customerId' => $customerId, 'via' => 'open_api', ], JSON_UNESCAPED_UNICODE), ] ); } catch (\Exception $e) { Log::error('[OpenScenarios] 同步 V2 流量池失败:' . $e->getMessage()); } } private function success(string $data, string $message = 'success') { return json(['code' => 200, 'message' => $message, 'data' => $data]); } private function error(string $message, int $code = 400) { return json(['code' => $code, 'message' => $message, 'data' => null]); } }