Files
cunkebao_v3/Server/application/common/controller/OpenWebhookController.php
Manus AI 03ae8c07b6 feat: 同步本地全量开发到 GitHub(前端/后端/超管/开发文档规则)
含 AI 助手、抖音 OAuth、流量池推送、支付获客、SuperAdmin 设备与场景管理等本地迭代;以本地为准单向推送,未拉取远程。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 22:12:40 +08:00

136 lines
4.4 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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);
}
}
}