新版数智员工

This commit is contained in:
wong
2026-03-24 10:39:16 +08:00
parent 1853934d85
commit 7fb61b77a4
123 changed files with 38491 additions and 1018 deletions

View File

@@ -0,0 +1,234 @@
<?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]);
}
}