更新
This commit is contained in:
@@ -183,11 +183,18 @@ class QywxCustomerAcquisitionApiService
|
||||
|
||||
return $this->request($method, $path, $body, true);
|
||||
}
|
||||
$errorMessage = trim((string) ($decoded['errmsg'] ?? '未知错误')) ?: '未知错误';
|
||||
if ($errcode === 60111) {
|
||||
throw new RuntimeException(
|
||||
'所选医助的企业微信 userid 不存在,或不在获客助手可调用应用的可见范围;'
|
||||
. '请检查后台账号绑定和企业微信应用可见范围。企业微信返回:' . $errorMessage
|
||||
);
|
||||
}
|
||||
|
||||
throw new RuntimeException(sprintf(
|
||||
'企业微信获客助手接口失败[%d]:%s',
|
||||
$errcode,
|
||||
trim((string) ($decoded['errmsg'] ?? '未知错误')) ?: '未知错误'
|
||||
$errorMessage
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,13 @@ class QywxCustomerAcquisitionCustomerService
|
||||
'event_time' => $eventTime,
|
||||
'snapshot' => $message,
|
||||
], false);
|
||||
self::recordPromotionAssignment(
|
||||
(string) ($message['State'] ?? $message['state'] ?? ''),
|
||||
$remoteLinkId,
|
||||
$userId,
|
||||
$externalUserId,
|
||||
$eventTime
|
||||
);
|
||||
self::finishEvent($eventId, 1, '', $remoteLinkId, $externalUserId, $userId);
|
||||
|
||||
return ['duplicate' => false, 'status' => 'success'];
|
||||
@@ -126,6 +133,13 @@ class QywxCustomerAcquisitionCustomerService
|
||||
'event_time' => $eventTime,
|
||||
'snapshot' => $chat,
|
||||
], true);
|
||||
self::recordPromotionAssignment(
|
||||
(string) ($chatInfo['state'] ?? $message['State'] ?? ''),
|
||||
$remoteLinkId,
|
||||
$userId,
|
||||
$externalUserId,
|
||||
$eventTime
|
||||
);
|
||||
self::finishEvent($eventId, 1, '', $remoteLinkId, $externalUserId, $userId);
|
||||
|
||||
return ['duplicate' => false, 'status' => 'success'];
|
||||
@@ -419,6 +433,40 @@ class QywxCustomerAcquisitionCustomerService
|
||||
return [$adminId, $deptId];
|
||||
}
|
||||
|
||||
private static function recordPromotionAssignment(
|
||||
string $state,
|
||||
string $remoteLinkId,
|
||||
string $userId,
|
||||
string $externalUserId,
|
||||
int $eventTime
|
||||
): void {
|
||||
$result = QywxPromotionMemberSchedulerService::recordFromState(
|
||||
$state,
|
||||
$userId,
|
||||
$externalUserId,
|
||||
$eventTime,
|
||||
'customer_acquisition'
|
||||
);
|
||||
if ((int) ($result['pool_id'] ?? 0) <= 0) {
|
||||
$result = QywxPromotionMemberSchedulerService::recordFromRemoteLink(
|
||||
$remoteLinkId,
|
||||
$userId,
|
||||
$externalUserId,
|
||||
$eventTime,
|
||||
'customer_acquisition'
|
||||
);
|
||||
}
|
||||
if (($result['status'] ?? '') !== 'counted' || (int) ($result['pool_id'] ?? 0) <= 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 优先在本次回调完成后切换,后台分钟任务继续承担失败重试。
|
||||
(new QywxPromotionRangeSyncService())->syncPool((int) $result['pool_id']);
|
||||
} catch (\Throwable) {
|
||||
// 同步服务已经保存失败原因和下次重试时间,不能让远端网络错误回滚已记账的客户。
|
||||
}
|
||||
}
|
||||
|
||||
private static function encodeJson(mixed $value): string
|
||||
{
|
||||
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\qywx;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/** 企业微信获客助手链接校验。 */
|
||||
class QywxCustomerAcquisitionLinkService
|
||||
{
|
||||
@@ -39,4 +41,98 @@ class QywxCustomerAcquisitionLinkService
|
||||
{
|
||||
return 'https://work.weixin.qq.com/ca/xxxxxxxx';
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化企业微信 create_link/get 返回的链接详情。
|
||||
*
|
||||
* get 接口把 range、priority_option 放在响应根级,link 只包含链接本身;
|
||||
* 旧响应或测试桩可能把这些字段放在 link 内,因此保留兼容回退。
|
||||
*
|
||||
* @return array{
|
||||
* link_id:string,
|
||||
* link_name:string,
|
||||
* url:string,
|
||||
* create_time:int,
|
||||
* range_userids:list<string>,
|
||||
* range_department_ids:list<string>,
|
||||
* skip_verify:bool,
|
||||
* priority_option:array<string,mixed>,
|
||||
* snapshot:array<string,mixed>
|
||||
* }
|
||||
*/
|
||||
public static function normaliseRemoteResponse(array $response, string $fallbackId = ''): array
|
||||
{
|
||||
$link = isset($response['link']) && is_array($response['link']) ? $response['link'] : $response;
|
||||
$range = isset($response['range']) && is_array($response['range'])
|
||||
? $response['range']
|
||||
: (isset($link['range']) && is_array($link['range']) ? $link['range'] : []);
|
||||
$priorityOption = isset($response['priority_option']) && is_array($response['priority_option'])
|
||||
? $response['priority_option']
|
||||
: (isset($link['priority_option']) && is_array($link['priority_option']) ? $link['priority_option'] : []);
|
||||
$linkId = trim((string) ($link['link_id'] ?? $response['link_id'] ?? $fallbackId));
|
||||
$url = trim((string) ($link['url'] ?? $link['link_url'] ?? $response['url'] ?? ''));
|
||||
if ($linkId === '') {
|
||||
throw new RuntimeException('企业微信获客链接详情缺少 link_id');
|
||||
}
|
||||
if (!self::isAllowed($url)) {
|
||||
throw new RuntimeException('企业微信获客链接详情未返回有效的 https://work.weixin.qq.com/ca/... 地址');
|
||||
}
|
||||
|
||||
return [
|
||||
'link_id' => $linkId,
|
||||
'link_name' => trim((string) ($link['link_name'] ?? $link['name'] ?? $linkId)),
|
||||
'url' => $url,
|
||||
'create_time' => max(0, (int) ($link['create_time'] ?? 0)),
|
||||
'range_userids' => self::normaliseScalarList($range['user_list'] ?? []),
|
||||
'range_department_ids' => self::normaliseScalarList($range['department_list'] ?? []),
|
||||
'skip_verify' => !empty($link['skip_verify']),
|
||||
'priority_option' => $priorityOption,
|
||||
'snapshot' => $response,
|
||||
];
|
||||
}
|
||||
|
||||
/** 追加或替换企业微信获客助手的自定义渠道标识。 */
|
||||
public static function withCustomerChannel(string $url, string $channel): string
|
||||
{
|
||||
$url = trim($url);
|
||||
$channel = trim($channel);
|
||||
if (!self::isAllowed($url) || $channel === '' || strlen($channel) > 64) {
|
||||
return '';
|
||||
}
|
||||
// 冒号在查询参数中是合法字符,保留“命名空间:编号”的可读结构;其余字符仍编码。
|
||||
$parameter = 'customer_channel=' . str_replace('%3A', ':', rawurlencode($channel));
|
||||
if (preg_match('/([?&])customer_channel=[^&#]*/i', $url) === 1) {
|
||||
return (string) preg_replace_callback(
|
||||
'/([?&])customer_channel=[^&#]*/i',
|
||||
static fn (array $matches): string => $matches[1] . $parameter,
|
||||
$url,
|
||||
1
|
||||
);
|
||||
}
|
||||
$fragment = '';
|
||||
$fragmentPosition = strpos($url, '#');
|
||||
if ($fragmentPosition !== false) {
|
||||
$fragment = substr($url, $fragmentPosition);
|
||||
$url = substr($url, 0, $fragmentPosition);
|
||||
}
|
||||
|
||||
return $url . (str_contains($url, '?') ? '&' : '?') . $parameter . $fragment;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private static function normaliseScalarList(mixed $values): array
|
||||
{
|
||||
$result = [];
|
||||
$seen = [];
|
||||
foreach ((array) $values as $value) {
|
||||
$normalised = trim((string) $value);
|
||||
$key = 'value:' . $normalised;
|
||||
if ($normalised !== '' && !isset($seen[$key])) {
|
||||
$seen[$key] = true;
|
||||
$result[] = $normalised;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ namespace app\common\service\qywx;
|
||||
|
||||
use think\facade\Db;
|
||||
|
||||
/** 公开获客助手链接分流:按权重随机,并在事务内维护当日限额与点击计数。 */
|
||||
/** 公开获客助手链接:新方案直达单个官方链接,旧 /go 入口继续兼容历史分流。 */
|
||||
class QywxPromotionRedirectService
|
||||
{
|
||||
/** @return array{status:int,widget_config_json:?string}|null */
|
||||
/** @return array{status:int,widget_config_json:?string,target_url:string}|null */
|
||||
public static function publicPoolConfig(string $publicKey): ?array
|
||||
{
|
||||
if (preg_match('/^[a-f0-9]{32}$/', $publicKey) !== 1) {
|
||||
@@ -19,17 +19,32 @@ class QywxPromotionRedirectService
|
||||
$row = Db::name('qywx_promotion_pool')
|
||||
->where('public_key', $publicKey)
|
||||
->whereNull('delete_time')
|
||||
->field('status,widget_config_json')
|
||||
->field('id,status,widget_config_json')
|
||||
->find();
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$link = Db::name('qywx_promotion_link')
|
||||
->where('pool_id', (int) $row['id'])
|
||||
->where('status', 1)
|
||||
->where('remote_status', 1)
|
||||
->where('remote_link_id', '<>', '')
|
||||
->whereNull('delete_time')
|
||||
->order('id', 'desc')
|
||||
->field('wecom_url')
|
||||
->find();
|
||||
$targetUrl = QywxCustomerAcquisitionLinkService::withCustomerChannel(
|
||||
(string) ($link['wecom_url'] ?? ''),
|
||||
'zyt_pool:' . (int) $row['id']
|
||||
);
|
||||
|
||||
return [
|
||||
'status' => (int) ($row['status'] ?? 0),
|
||||
'widget_config_json' => isset($row['widget_config_json'])
|
||||
? (string) $row['widget_config_json']
|
||||
: null,
|
||||
'target_url' => $targetUrl,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -131,11 +131,11 @@ class QywxPromotionWidgetService
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成可直接跨站安装的完整脚本。真实获客链接始终只由跳转端点选择。
|
||||
* 生成可直接跨站安装的完整脚本,点击后直达当前方案的企业微信官方链接。
|
||||
*
|
||||
* @param array<string, mixed> $config
|
||||
*/
|
||||
public static function renderScript(string $key, string $goUrl, array $config, bool $poolEnabled = true): string
|
||||
public static function renderScript(string $key, string $targetUrl, array $config, bool $poolEnabled = true): string
|
||||
{
|
||||
$config = self::fromInput($config);
|
||||
if (!$poolEnabled) {
|
||||
@@ -143,14 +143,14 @@ class QywxPromotionWidgetService
|
||||
}
|
||||
|
||||
$jsonKey = self::jsonForScript($key);
|
||||
$jsonGo = self::jsonForScript($goUrl);
|
||||
$jsonTarget = self::jsonForScript($targetUrl);
|
||||
$jsonConfig = self::jsonForScript($config);
|
||||
|
||||
return <<<JS
|
||||
(function(w,d){
|
||||
'use strict';
|
||||
var key={$jsonKey},goPath={$jsonGo},config={$jsonConfig},scriptNode=d.currentScript||null;
|
||||
var go=resolveGoUrl(goPath);
|
||||
var key={$jsonKey},targetPath={$jsonTarget},config={$jsonConfig},scriptNode=d.currentScript||null;
|
||||
var targetUrl=resolveTargetUrl(targetPath);
|
||||
var registry=w.WecomPromotion=w.WecomPromotion||{};
|
||||
var previous=registry[key];
|
||||
if(previous&&previous.__widgetVersion===1&&typeof previous.destroy==='function'){
|
||||
@@ -170,7 +170,7 @@ class QywxPromotionWidgetService
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveGoUrl(value){
|
||||
function resolveTargetUrl(value){
|
||||
if(/^https?:\/\//i.test(value)){return value;}
|
||||
var node=findScriptNode();
|
||||
if(node&&node.src&&typeof w.URL==='function'){
|
||||
@@ -179,14 +179,8 @@ class QywxPromotionWidgetService
|
||||
return value;
|
||||
}
|
||||
|
||||
function sourceUrl(){
|
||||
var location=w.location||{};
|
||||
var origin=location.origin||((location.protocol&&location.host)?location.protocol+'//'+location.host:'');
|
||||
return origin+(location.pathname||'/');
|
||||
}
|
||||
|
||||
function openPromotion(){
|
||||
w.location.assign(go+'?from='+encodeURIComponent(sourceUrl()));
|
||||
if(targetUrl){w.location.assign(targetUrl);}
|
||||
}
|
||||
|
||||
function handleDocumentClick(event){
|
||||
|
||||
Reference in New Issue
Block a user