含 AI 助手、抖音 OAuth、流量池推送、支付获客、SuperAdmin 设备与场景管理等本地迭代;以本地为准单向推送,未拉取远程。 Co-authored-by: Cursor <cursoragent@cursor.com>
136 lines
4.4 KiB
PHP
136 lines
4.4 KiB
PHP
<?php
|
||
/**
|
||
* 开放对接平台 · Webhook 订阅入口(v1.1)
|
||
*
|
||
* 第三方通过 API 自助订阅"客资变化"事件推送(订阅式而非轮询)。
|
||
* 复用 PlanLeadWebhookService 的现有触发机制(在 task_customer create/update/tags_updated 后已自动推送)。
|
||
*
|
||
* 写入位置:customer_acquisition_task.sceneConf 的 leadPushWebhook* 字段(不动表结构)。
|
||
*
|
||
* 端点:
|
||
* POST /v1/open/leads/subscribe { url, secret? }
|
||
* POST /v1/open/leads/unsubscribe
|
||
* GET /v1/open/leads/subscription
|
||
*
|
||
* 文档:开发文档/5、接口/08-存客宝开放对接平台/06-Webhook订阅与黑名单.md
|
||
*
|
||
* @package app\common\controller
|
||
*/
|
||
|
||
namespace app\common\controller;
|
||
|
||
use app\common\service\OpenPlatformService;
|
||
use app\cunkebao\service\PlanLeadWebhookService;
|
||
use library\ResponseHelper;
|
||
use think\Controller;
|
||
use think\facade\Request;
|
||
|
||
class OpenWebhookController extends Controller
|
||
{
|
||
/**
|
||
* 鉴权 → 返回 ctx
|
||
*/
|
||
private function ctx(): array
|
||
{
|
||
$ctx = OpenPlatformService::resolveContext();
|
||
if (!$ctx) {
|
||
ResponseHelper::unauthorized('未授权或 Token 已过期')->send();
|
||
exit;
|
||
}
|
||
return $ctx;
|
||
}
|
||
|
||
/**
|
||
* 订阅客资 webhook
|
||
* POST /v1/open/leads/subscribe
|
||
*
|
||
* 入参:
|
||
* url string 必填,必须 http:// 或 https://
|
||
* secret string 可选,HMAC-SHA256 签名秘钥(推送时放 X-CKB-Signature 头)
|
||
*/
|
||
public function subscribe()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$url = trim((string) Request::param('url', ''));
|
||
$secret = (string) Request::param('secret', '');
|
||
|
||
if ($url === '') {
|
||
return ResponseHelper::error('url 不能为空', 400);
|
||
}
|
||
// 复用 PlanLeadWebhookService 的校验规则
|
||
$err = PlanLeadWebhookService::validateConfig([
|
||
'leadPushWebhookEnabled' => 1,
|
||
'leadPushWebhook' => $url,
|
||
]);
|
||
if ($err) {
|
||
return ResponseHelper::error($err, 400);
|
||
}
|
||
|
||
$r = OpenPlatformService::saveWebhookSubscription($ctx['planId'], $url, $secret);
|
||
if (($r['code'] ?? 0) !== 200) {
|
||
return ResponseHelper::error($r['msg'] ?? 'subscribe failed', $r['code'] ?? 400);
|
||
}
|
||
return ResponseHelper::success($r['data'], $r['msg']);
|
||
}
|
||
|
||
/**
|
||
* 取消订阅
|
||
* POST /v1/open/leads/unsubscribe
|
||
*/
|
||
public function unsubscribe()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$r = OpenPlatformService::removeWebhookSubscription($ctx['planId']);
|
||
if (($r['code'] ?? 0) !== 200) {
|
||
return ResponseHelper::error($r['msg'] ?? 'unsubscribe failed', $r['code'] ?? 400);
|
||
}
|
||
return ResponseHelper::success($r['data'], $r['msg']);
|
||
}
|
||
|
||
/**
|
||
* 查询当前订阅状态
|
||
* GET /v1/open/leads/subscription
|
||
*/
|
||
public function subscription()
|
||
{
|
||
$ctx = $this->ctx();
|
||
$r = OpenPlatformService::readWebhookSubscription($ctx['planId']);
|
||
if (($r['code'] ?? 0) !== 200) {
|
||
return ResponseHelper::error($r['msg'] ?? 'query failed', $r['code'] ?? 400);
|
||
}
|
||
return ResponseHelper::success($r['data'], $r['msg']);
|
||
}
|
||
|
||
/**
|
||
* 触发一次测试推送(不写入数据,仅按真实事件链路推一次 ping)
|
||
* POST /v1/open/leads/test
|
||
*/
|
||
public function test()
|
||
{
|
||
$ctx = $this->ctx();
|
||
// 读最近一条 task_customer 作为样本;若无则返回提示
|
||
$sample = \think\Db::name('task_customer')
|
||
->where('task_id', $ctx['planId'])
|
||
->order('id DESC')
|
||
->find();
|
||
if (!$sample) {
|
||
return ResponseHelper::error('计划下还没有客资可供测试推送', 404);
|
||
}
|
||
try {
|
||
PlanLeadWebhookService::dispatch(
|
||
(int) $ctx['planId'],
|
||
(int) $sample['id'],
|
||
PlanLeadWebhookService::EVENT_TAGS_UPDATED
|
||
);
|
||
return ResponseHelper::success([
|
||
'planId' => $ctx['planId'],
|
||
'sampleId' => (int) $sample['id'],
|
||
'event' => PlanLeadWebhookService::EVENT_TAGS_UPDATED,
|
||
'dispatched' => true,
|
||
], '测试推送已发出(请到你的接收端确认)');
|
||
} catch (\Throwable $e) {
|
||
return ResponseHelper::error('推送失败:' . $e->getMessage(), 500);
|
||
}
|
||
}
|
||
}
|