Files
MBTI_wang/api/app/common/service/FeishuLeadWebhookService.php

278 lines
9.0 KiB
PHP
Raw 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
namespace app\common\service;
use think\facade\Db;
/**
* 飞书自定义机器人获客推送(对齐 Soul 存客宝卡片:标题/来源/对接人/姓名手机/时间/最近行为)
* 配置system_config key=feishu_lead_webhook enterprise_id=0 JSON
*/
class FeishuLeadWebhookService
{
public const CONFIG_KEY = 'feishu_lead_webhook';
/** 去重表 scene飞书获客与 OutboundPushHookService::DEDUP_SCENE_OUTBOUND 区分) */
private const DEDUP_SCENE = 'feishu_lead';
public static function getConfig(): array
{
$def = [
'enabled' => false,
'webhookUrl' => '',
'contactPerson' => '运营',
];
$row = Db::name('system_config')
->where('key', self::CONFIG_KEY)
->where('enterprise_id', 0)
->find();
if (!$row || empty($row['value'])) {
return $def;
}
$v = is_string($row['value']) ? json_decode($row['value'], true) : $row['value'];
if (!is_array($v)) {
return $def;
}
return array_merge($def, $v);
}
/**
* 支付成功(含测试付费、充值等):每订单仅推一次
*/
public static function onOrderPaid(int $orderDbId, int $userId): void
{
if ($orderDbId <= 0 || $userId <= 0) {
return;
}
$dedupKey = 'order_paid:' . $orderDbId;
$order = Db::name('orders')->where('id', $orderDbId)->find();
if (!$order) {
return;
}
$productType = (string) ($order['productType'] ?? '');
$amountFen = (int) ($order['amount'] ?? 0);
$title = (string) ($order['productTitle'] ?? '');
$source = self::sourceLabelForOrder($productType, $title, $amountFen);
self::pushLead([
'dedupKey' => $dedupKey,
'userId' => $userId,
'source' => $source,
'extraLine' => '订单号: ' . ($order['orderNo'] ?? '') . ' · 金额: ¥' . number_format($amountFen / 100, 2),
]);
}
/**
* 首次绑定手机号(测试完成留资)
*/
public static function onPhoneBound(int $userId, string $phone): void
{
if ($userId <= 0 || trim($phone) === '') {
return;
}
self::pushLead([
'dedupKey' => 'phone_bind:' . $userId,
'userId' => $userId,
'source' => '测试完成·授权手机号',
'phone' => $phone,
]);
}
/**
* 小程序「了解自己」页提交咨询申请(与 CrmReport 存客宝上报并行;飞书机器人需 feishu_lead_webhook.enabled
*/
public static function onDeepServiceConsultApply(int $userId, string $source, string $categoryTag): void
{
if ($userId <= 0) {
return;
}
$src = trim($source) !== '' ? trim($source) : '深度服务·申请咨询';
$tag = trim($categoryTag);
$dedup = 'deep_consult_apply:' . $userId . ':' . md5($src . '|' . $tag) . ':' . date('YmdH');
$extra = $tag !== '' ? ('套餐: ' . self::oneLine($tag, 60)) : '';
self::pushLead([
'dedupKey' => $dedup,
'userId' => $userId,
'source' => $src,
'extraLine' => $extra,
]);
}
/**
* @param array{dedupKey:string,userId:int,source:string,phone?:string,extraLine?:string} $p
*/
public static function pushLead(array $p): void
{
$cfg = self::getConfig();
if (empty($cfg['enabled'])) {
return;
}
$url = trim((string) ($cfg['webhookUrl'] ?? ''));
if ($url === '' || stripos($url, 'http') !== 0) {
return;
}
$dedupKey = $p['dedupKey'] ?? '';
if ($dedupKey === '') {
return;
}
if (!self::beginDedup($dedupKey)) {
return;
}
$userId = (int) ($p['userId'] ?? 0);
$wu = $userId > 0
? Db::name('wechat_users')->where('id', $userId)->field('nickname,phone')->find()
: null;
$nickname = trim((string) ($wu['nickname'] ?? ''));
if ($nickname === '') {
$nickname = '微信用户';
}
$phone = trim((string) ($p['phone'] ?? ($wu['phone'] ?? '')));
$contact = trim((string) ($cfg['contactPerson'] ?? '运营'));
if ($contact === '') {
$contact = '运营';
}
$source = (string) ($p['source'] ?? '小程序');
$now = date('Y-m-d H:i');
$text = "📋 新获客\n来源: {$source}\n对接人: {$contact}\n━━━━━━━━━━";
$text .= "\n姓名: {$nickname}";
if ($phone !== '') {
$text .= "\n手机: {$phone}";
}
$text .= "\n时间: {$now}";
if (!empty($p['extraLine'])) {
$text .= "\n" . $p['extraLine'];
}
$lines = UserJourneyService::recentBehaviorLines($userId, 8);
if (count($lines) > 0) {
$text .= "\n━━━━━━━━━━\n最近行为:";
$i = 1;
foreach ($lines as $line) {
$text .= "\n {$i}. {$line}";
$i++;
}
}
$ok = self::postWebhook($url, $text);
if (!$ok) {
self::rollbackDedup($dedupKey);
}
}
/**
* 订单来源文案(飞书「来源」与 HTTP 出站 Hook `sourceLabel` 共用)
*/
public static function sourceLabelForOrder(string $productType, string $title, int $amountFen): string
{
if ($productType === 'recharge') {
return '企业余额·充值支付成功';
}
$map = [
'face' => '面相测试',
'mbti' => 'MBTI测试',
'sbti' => 'SBTI测试',
'disc' => 'DISC测试',
'pdp' => 'PDP测试',
'resume' => '简历分析',
'report' => '完整报告',
'team_analysis' => '团队分析',
'deep_personal' => '个人深度服务',
'deep_team' => '团队深度服务',
'vip' => 'VIP',
];
$label = $map[$productType] ?? strtoupper($productType);
$suffix = '·支付成功';
if ($title !== '') {
return $label . '·「' . self::oneLine($title, 40) . '」' . $suffix;
}
if ($amountFen === 100) {
return $label . '·1元支付' . $suffix;
}
return $label . $suffix;
}
private static function oneLine(string $s, int $max): string
{
$s = preg_replace('/\s+/u', ' ', trim($s));
if (mb_strlen($s) > $max) {
return mb_substr($s, 0, $max) . '…';
}
return $s;
}
private static function beginDedup(string $dedupKey): bool
{
try {
Db::name('delivery_dedup')->insert([
'scene' => self::DEDUP_SCENE,
'dedupKey' => $dedupKey,
'createdAt' => date('Y-m-d H:i:s'),
]);
return true;
} catch (\Throwable $e) {
return false;
}
}
private static function rollbackDedup(string $dedupKey): void
{
try {
Db::name('delivery_dedup')
->where('scene', self::DEDUP_SCENE)
->where('dedupKey', $dedupKey)
->delete();
} catch (\Throwable $e) {
}
}
private static function postWebhook(string $url, string $text): bool
{
$payload = [];
if (stripos($url, 'qyapi.weixin.qq.com') !== false) {
$payload = [
'msgtype' => 'text',
'text' => ['content' => $text],
];
} else {
$payload = [
'msg_type' => 'text',
'content' => ['text' => $text],
];
}
$json = json_encode($payload, JSON_UNESCAPED_UNICODE);
$ch = curl_init($url);
if ($ch === false) {
return false;
}
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json; charset=utf-8']);
curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 8);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$body = curl_exec($ch);
$code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 400) {
return false;
}
if ($body !== false && $body !== '') {
$resp = json_decode($body, true);
if (is_array($resp)) {
if (isset($resp['code']) && (int) $resp['code'] !== 0) {
return false;
}
if (isset($resp['StatusCode']) && (int) $resp['StatusCode'] !== 0) {
return false;
}
}
}
return true;
}
}