235 lines
9.2 KiB
PHP
235 lines
9.2 KiB
PHP
<?php
|
||
|
||
namespace app\common\controller;
|
||
|
||
use think\Controller;
|
||
use think\Db;
|
||
use think\facade\Log;
|
||
use app\common\model\TrafficPoolSource;
|
||
use app\cunkebao\service\TrafficPoolService;
|
||
use app\cunkebao\service\DistributionRewardService;
|
||
|
||
/**
|
||
* 对外开放接口 — 场景获客线索上报
|
||
*
|
||
* 鉴权:Bearer JWT(通过 POST /v1/open/auth/token 获取)
|
||
* 路由:POST /v1/open/scenarios(挂 jwt 中间件)
|
||
*
|
||
* 第三方调用流程:
|
||
* 1. POST /v1/open/auth/token → 获得 JWT Token
|
||
* 2. POST /v1/open/scenarios → Header: Authorization: Bearer <token>
|
||
*/
|
||
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]);
|
||
}
|
||
}
|