Commit e56e6914 authored by 刘小敏's avatar 刘小敏

小红书运营监控接口

parent cb8be815
<?php
namespace addons\shopro\controller\user;
use addons\shopro\controller\Common;
use app\admin\model\shopro\xhs\AdTracking as AdTrackingModel;
use app\admin\model\shopro\xhs\ConversionLog as ConversionLogModel;
/**
* 小红书聚光「种草直达-微信小程序」广告追踪 & 转化回传
*/
class XhsAd extends Common
{
protected $noNeedLogin = ['*'];
protected $noNeedRight = ['*'];
/**
* 接收追踪参数接口
* 小程序从广告跳入时调用,存储 click_id 与用户绑定
*
* POST /addons/shopro/user.xhs_ad/track
* @param click_id string 小红书广告点击唯一标识(必填)
* @param request_id string 小红书广告请求 ID
* @param event_type string 事件类型(如 122)
* @param landing_page string 落地页路径
*/
public function track()
{
if (!$this->request->isPost()) {
$this->error('请求方式错误');
}
$params = $this->request->only(['click_id', 'request_id', 'event_type', 'landing_page']);
$this->svalidate($params, '.track');
$user = auth_user();
$userId = $user->id ?? 0;
custom_log('[xhs.track] 收到追踪请求, user_id=' . $userId . ', params=' . json_encode($params, JSON_UNESCAPED_UNICODE), $userId, 'xhs_ad');
if (empty($userId)) {
custom_log('[xhs.track] user_id 为空,拒绝记录', $userId, 'xhs_ad', 'warning');
$this->error('用户未登录');
}
if (empty($params['click_id'])) {
custom_log('[xhs.track] click_id 为空,拒绝记录', $userId, 'xhs_ad', 'warning');
$this->error('缺少 click_id 参数');
}
// 记录/更新追踪信息
$tracking = AdTrackingModel::record([
'user_id' => $userId,
'click_id' => $params['click_id'],
'request_id' => $params['request_id'] ?? '',
'event_type' => $params['event_type'] ?? '',
'landing_page' => $params['landing_page'] ?? '',
]);
if (!$tracking) {
custom_log('[xhs.track] 追踪记录写入失败, user_id=' . $userId, $userId, 'xhs_ad', 'warning');
$this->error('记录追踪信息失败');
}
custom_log('[xhs.track] 追踪记录成功, tracking_id=' . $tracking->id . ', click_id=' . $params['click_id'], $userId, 'xhs_ad');
$this->success('记录成功', [
'tracking_id' => $tracking->id,
]);
}
/**
* 转化事件记录接口
* 用户在小程序内完成转化后调用,触发异步回传
*
* POST /addons/shopro/user.xhs_ad/conversion
* @param conversion_type string 转化类型: register/order/payment/add_wechat(必填)
* @param value float 转化金额
* @param order_id string 订单号
*/
public function conversion()
{
if (!$this->request->isPost()) {
$this->error('请求方式错误');
}
$params = $this->request->only(['conversion_type', 'value', 'order_id']);
$this->svalidate($params, '.conversion');
$user = auth_user();
$userId = $user->id ?? 0;
custom_log('[xhs.conversion] 收到转化记录请求, user_id=' . $userId . ', params=' . json_encode($params, JSON_UNESCAPED_UNICODE), $userId, 'xhs_ad');
if (empty($userId)) {
custom_log('[xhs.conversion] user_id 为空', $userId, 'xhs_ad', 'warning');
$this->error('用户未登录');
}
$conversionType = $params['conversion_type'];
$value = floatval($params['value'] ?? 0);
$orderId = $params['order_id'] ?? '';
// 查询有效追踪记录
$tracking = AdTrackingModel::getValidByUser($userId);
$clickId = '';
$trackingId = 0;
$status = ConversionLogModel::STATUS_NO_TRACK;
if ($tracking) {
$clickId = $tracking->click_id;
$trackingId = $tracking->id;
$status = ConversionLogModel::STATUS_PENDING;
// 去重检查
if (ConversionLogModel::isDuplicated($clickId, $conversionType, $orderId)) {
custom_log('[xhs.conversion] 重复转化,拒绝记录, click_id=' . $clickId . ', type=' . $conversionType . ', order=' . $orderId, $userId, 'xhs_ad', 'warning');
$this->error('该转化事件已记录,请勿重复提交');
}
} else {
custom_log('[xhs.conversion] 无有效追踪记录,记录为无需回传, user_id=' . $userId, $userId, 'xhs_ad', 'warning');
}
$now = time();
// 写入转化日志
try {
$log = ConversionLogModel::create([
'user_id' => $userId,
'tracking_id' => $trackingId,
'click_id' => $clickId,
'conversion_type' => $conversionType,
'value' => $value,
'order_id' => $orderId,
'status' => $status,
'retry_count' => 0,
'conversion_time' => $now,
'createtime' => $now,
'updatetime' => $now,
]);
custom_log('[xhs.conversion] 转化日志写入成功, log_id=' . $log->id . ', status=' . $status, $userId, 'xhs_ad');
} catch (\Exception $e) {
custom_log('[xhs.conversion] 转化日志写入失败: ' . $e->getMessage(), $userId, 'xhs_ad', 'error');
$this->error('记录转化事件失败');
}
// 如果有有效 click_id,投递异步队列回传
if ($status === ConversionLogModel::STATUS_PENDING && !empty($clickId)) {
try {
$jobData = [
'log_id' => $log->id,
'user_id' => $userId,
'click_id' => $clickId,
'conversion_type' => $conversionType,
'value' => $value,
'order_id' => $orderId,
'conversion_time' => $now,
];
\think\Queue::push('addons\shopro\job\XhsConversionReport', $jobData, 'shopro');
custom_log('[xhs.conversion] 异步队列投递成功, log_id=' . $log->id, $userId, 'xhs_ad');
} catch (\Exception $e) {
custom_log('[xhs.conversion] 异步队列投递失败: ' . $e->getMessage(), $userId, 'xhs_ad', 'error');
// 队列投递失败不阻塞主流程,后续可手动重试
}
}
$this->success('转化事件记录成功', [
'log_id' => $log->id,
'status' => $status,
]);
}
}
<?php
namespace addons\shopro\job;
use think\queue\Job;
use addons\shopro\service\xhs\ConversionService;
/**
* 小红书转化回传异步队列任务
*
* 用法:
* \think\Queue::push('addons\shopro\job\XhsConversionReport', $data, 'shopro');
*
* $data 参数:
* - log_id int 转化日志 ID
* - user_id int 用户 ID
* - click_id string 广告点击 ID
* - conversion_type string 转化类型
* - value float 转化金额
* - order_id string 订单号
* - conversion_time int 转化发生时间(秒级时间戳)
*/
class XhsConversionReport extends BaseJob
{
/**
* 任务失败重试次数上限
*/
const MAX_ATTEMPTS = 3;
/**
* 执行队列任务
*
* @param Job $job 队列任务
* @param array $data 任务数据
*/
public function exec(Job $job, $data)
{
$logId = $data['log_id'] ?? 0;
$userId = $data['user_id'] ?? 0;
custom_log('[xhs.job] 队列任务开始执行, log_id=' . $logId . ', user_id=' . $userId, $userId, 'xhs_ad');
try {
// 校验入参
if (empty($logId) || empty($data['click_id'])) {
custom_log('[xhs.job] 入参有空值,删除job, log_id=' . $logId, $userId, 'xhs_ad', 'warning');
$job->delete();
return false;
}
$service = new ConversionService();
$result = $service->handleReport($data);
if ($result) {
custom_log('[xhs.job] 回传成功,删除job, log_id=' . $logId, $userId, 'xhs_ad');
$job->delete();
return true;
} else {
$attempts = $job->attempts();
if ($attempts >= self::MAX_ATTEMPTS) {
custom_log('[xhs.job] 已达最大尝试次数(' . self::MAX_ATTEMPTS . '),删除job, log_id=' . $logId, $userId, 'xhs_ad', 'error');
$job->delete();
return false;
}
// 阶梯延迟重试:第1次 30s,第2次 120s,第3次 300s
$delayMap = [1 => 30, 2 => 120, 3 => 300];
$delay = $delayMap[$attempts + 1] ?? 60;
custom_log('[xhs.job] 回传失败,第' . $attempts . '次尝试,' . $delay . '秒后重试, log_id=' . $logId, $userId, 'xhs_ad', 'warning');
$job->release($delay);
return false;
}
} catch (\Exception $e) {
custom_log('[xhs.job] 队列异常: ' . $e->getMessage() . ', log_id=' . $logId, $userId, 'xhs_ad', 'error');
format_log_error_custom_name($e, '[XHS转化回传] log_id:' . $logId, '', 'xhs_ad');
// 超过重试次数则删除
if ($job->attempts() >= self::MAX_ATTEMPTS) {
$job->delete();
return false;
}
// 异常时 60s 后重试
$job->release(60);
return false;
}
}
}
This diff is collapsed.
<?php
namespace addons\shopro\validate\user;
use think\Validate;
class XhsAd extends Validate
{
protected $rule = [
'click_id' => 'require|length:0,128',
'request_id' => 'length:0,128',
'event_type' => 'length:0,32',
'landing_page' => 'length:0,512',
'conversion_type' => 'require|in:register,order,payment,add_wechat',
'value' => 'float',
'order_id' => 'length:0,64',
];
protected $message = [
'click_id.require' => 'click_id 参数缺失',
'click_id.length' => 'click_id 长度不能超过 128 位',
'request_id.length' => 'request_id 长度不能超过 128 位',
'event_type.length' => 'event_type 长度不能超过 32 位',
'landing_page.length' => 'landing_page 长度不能超过 512 位',
'conversion_type.require' => '请选择转化类型',
'conversion_type.in' => '转化类型不合法',
'value.float' => '转化金额必须为数字',
'order_id.length' => 'order_id 长度不能超过 64 位',
];
protected $scene = [
'track' => ['click_id', 'request_id', 'event_type', 'landing_page'],
'conversion' => ['conversion_type', 'value', 'order_id'],
];
}
<?php
namespace app\admin\model\shopro\xhs;
use app\admin\model\shopro\Common;
/**
* 小红书广告追踪模型
*/
class AdTracking extends Common
{
protected $name = 'xhs_ad_tracking';
protected $updateTime = 'updatetime';
protected $createTime = 'createtime';
protected $autoWriteTimestamp = 'integer';
// 30 天归因窗口(秒)
const ATTRIBUTION_WINDOW = 30 * 86400;
// 平台来源常量
const PLATFORM_MINIPROGRAM = 'WechatMiniProgram';
/**
* 记录广告追踪信息
* 同一用户在归因期内多次点击,更新为最新的 click_id
*
* @param array $params
* @return AdTracking|null
*/
public static function record($params)
{
$userId = $params['user_id'] ?? 0;
$clickId = $params['click_id'] ?? '';
$platform = $params['platform'] ?? self::PLATFORM_MINIPROGRAM;
if (empty($userId) || empty($clickId)) {
return null;
}
$now = time();
$expiresAt = $now + self::ATTRIBUTION_WINDOW;
// 查询已有记录(未过期)
$exist = self::where('user_id', $userId)
->where('expires_at', '>', $now)
->find();
if ($exist) {
// 更新为最新 click_id
$exist->click_id = $clickId;
$exist->request_id = $params['request_id'] ?? $exist->request_id;
$exist->event_type = $params['event_type'] ?? $exist->event_type;
$exist->landing_page = $params['landing_page'] ?? $exist->landing_page;
$exist->expires_at = $expiresAt;
$exist->updatetime = $now;
$exist->save();
return $exist;
}
// 新建记录
return self::create([
'user_id' => $userId,
'click_id' => $clickId,
'request_id' => $params['request_id'] ?? '',
'event_type' => $params['event_type'] ?? '',
'landing_page' => $params['landing_page'] ?? '',
'platform' => $platform,
'createtime' => $now,
'updatetime' => $now,
'expires_at' => $expiresAt,
]);
}
/**
* 根据 user_id 获取有效期内的有效 click_id
*
* @param int $userId
* @return AdTracking|null
*/
public static function getValidByUser($userId)
{
$now = time();
return self::where('user_id', $userId)
->where('expires_at', '>', $now)
->order('id', 'desc')
->find();
}
}
<?php
namespace app\admin\model\shopro\xhs;
use app\admin\model\shopro\Common;
/**
* 小红书转化回传日志模型
*/
class ConversionLog extends Common
{
protected $name = 'xhs_conversion_log';
protected $updateTime = 'updatetime';
protected $createTime = 'createtime';
protected $autoWriteTimestamp = 'integer';
// 回传状态
const STATUS_PENDING = 0; // 待回传
const STATUS_SUCCESS = 1; // 回传成功
const STATUS_FAILED = 2; // 回传失败
const STATUS_NO_TRACK = 3; // 无需回传(无有效 click_id)
const STATUS_EXPIRED = 4; // 追踪已过期
// 最大重试次数
const MAX_RETRY = 3;
/**
* 状态文本映射
* @return array
*/
public function statusList()
{
return [
self::STATUS_PENDING => '待回传',
self::STATUS_SUCCESS => '回传成功',
self::STATUS_FAILED => '回传失败',
self::STATUS_NO_TRACK => '无需回传',
self::STATUS_EXPIRED => '追踪已过期',
];
}
/**
* 去重检查:同一 click_id + conversion_type + order_id 不可重复回传
*
* @param string $clickId
* @param string $conversionType
* @param string $orderId
* @return bool
*/
public static function isDuplicated($clickId, $conversionType, $orderId)
{
if (empty($clickId) || empty($conversionType)) {
return false;
}
$where = [
'click_id' => $clickId,
'conversion_type' => $conversionType,
];
if (!empty($orderId)) {
$where['order_id'] = $orderId;
}
return self::where($where)->find() ? true : false;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment