Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
L
loveisland
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
刘小敏
loveisland
Commits
e56e6914
Commit
e56e6914
authored
Jul 15, 2026
by
刘小敏
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
小红书运营监控接口
parent
cb8be815
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
801 additions
and
0 deletions
+801
-0
XhsAd.php
addons/shopro/controller/user/XhsAd.php
+175
-0
XhsConversionReport.php
addons/shopro/job/XhsConversionReport.php
+90
-0
ConversionService.php
addons/shopro/service/xhs/ConversionService.php
+342
-0
XhsAd.php
addons/shopro/validate/user/XhsAd.php
+35
-0
AdTracking.php
application/admin/model/shopro/xhs/AdTracking.php
+91
-0
ConversionLog.php
application/admin/model/shopro/xhs/ConversionLog.php
+68
-0
No files found.
addons/shopro/controller/user/XhsAd.php
0 → 100644
View file @
e56e6914
<?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
,
]);
}
}
addons/shopro/job/XhsConversionReport.php
0 → 100644
View file @
e56e6914
<?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
;
}
}
}
addons/shopro/service/xhs/ConversionService.php
0 → 100644
View file @
e56e6914
<?php
namespace
addons\shopro\service\xhs
;
use
app\admin\model\shopro\xhs\ConversionLog
as
ConversionLogModel
;
/**
* 小红书聚光 Marketing API 转化回传服务
*
* 负责调用小红书开放平台接口回传转化事件
* 包含 Token 管理、API 调用、重试逻辑
*
* ⚠️ 使用前需在聚光开放平台 (ad-market.xiaohongshu.com) 申请应用,
* 并在环境变量中配置 PHP_XHS_APP_KEY / PHP_XHS_APP_SECRET
*/
class
ConversionService
{
// API 基础地址(以小红书开放平台官方文档为准)
const
API_BASE
=
'https://ad-market.xiaohongshu.com/open/api'
;
// Token 接口
const
TOKEN_URL
=
'/oauth/token'
;
// 转化回传接口(具体路径以开放平台文档为准)
const
CONVERSION_URL
=
'/conversion/upload'
;
// Token 缓存 key 前缀
const
TOKEN_CACHE_KEY
=
'shopro:xhs:access_token'
;
// Token 提前刷新时间(秒),在过期前 5 分钟刷新
const
TOKEN_REFRESH_AHEAD
=
300
;
/**
* 获取 Access Token(带缓存)
*
* @return string|null
* @throws \Exception
*/
public
function
getAccessToken
()
{
// 检查缓存
$cached
=
redis_cache
(
'?'
.
self
::
TOKEN_CACHE_KEY
);
if
(
$cached
)
{
$tokenData
=
json_decode
(
$cached
,
true
);
if
(
$tokenData
&&
isset
(
$tokenData
[
'token'
]))
{
$now
=
time
();
if
(
$tokenData
[
'expires_at'
]
>
(
$now
+
self
::
TOKEN_REFRESH_AHEAD
))
{
return
$tokenData
[
'token'
];
}
}
}
// 获取新 Token
return
$this
->
refreshAccessToken
();
}
/**
* 强制刷新 Access Token
*
* @return string|null
* @throws \Exception
*/
private
function
refreshAccessToken
()
{
$appKey
=
$this
->
getConfig
(
'APP_KEY'
,
''
);
$appSecret
=
$this
->
getConfig
(
'APP_SECRET'
,
''
);
if
(
empty
(
$appKey
)
||
empty
(
$appSecret
))
{
throw
new
\Exception
(
'小红书聚光 API 凭证未配置(PHP_XHS_APP_KEY / PHP_XHS_APP_SECRET)'
);
}
$url
=
self
::
API_BASE
.
self
::
TOKEN_URL
;
$params
=
[
'app_key'
=>
$appKey
,
'app_secret'
=>
$appSecret
,
'grant_type'
=>
'client_credentials'
,
];
$response
=
$this
->
httpPost
(
$url
,
$params
);
if
(
empty
(
$response
[
'access_token'
]))
{
throw
new
\Exception
(
'获取 access_token 失败: '
.
json_encode
(
$response
));
}
$token
=
$response
[
'access_token'
];
$expiresIn
=
intval
(
$response
[
'expires_in'
]
??
7200
);
// 缓存 Token
$tokenData
=
[
'token'
=>
$token
,
'expires_at'
=>
time
()
+
$expiresIn
,
];
redis_cache
(
self
::
TOKEN_CACHE_KEY
,
json_encode
(
$tokenData
),
$expiresIn
);
return
$token
;
}
/**
* 回传转化事件到小红书
*
* @param array $data 转化数据
* - click_id string 广告点击 ID
* - conversion_type string 转化类型
* - value float 转化金额
* - order_id string 订单号
* - conversion_time int 转化发生时间(秒级时间戳)
* @return array ['success' => bool, 'response' => mixed]
*/
public
function
reportConversion
(
$data
)
{
$clickId
=
$data
[
'click_id'
]
??
''
;
$conversionType
=
$data
[
'conversion_type'
]
??
''
;
if
(
empty
(
$clickId
))
{
return
[
'success'
=>
false
,
'response'
=>
'click_id 为空'
];
}
try
{
$accessToken
=
$this
->
getAccessToken
();
}
catch
(
\Exception
$e
)
{
return
[
'success'
=>
false
,
'response'
=>
'获取 Token 失败: '
.
$e
->
getMessage
()];
}
// 转换事件类型为小红书平台编码(具体编码以开放平台文档为准)
$eventType
=
$this
->
mapEventType
(
$conversionType
);
$url
=
self
::
API_BASE
.
self
::
CONVERSION_URL
;
$body
=
[
'click_id'
=>
$clickId
,
'event_type'
=>
$eventType
,
'event_time'
=>
intval
(
$data
[
'conversion_time'
]
??
0
),
'conversion_value'
=>
floatval
(
$data
[
'value'
]
??
0
),
];
// 有订单号时附加
if
(
!
empty
(
$data
[
'order_id'
]))
{
$body
[
'order_id'
]
=
$data
[
'order_id'
];
}
$headers
=
[
'Authorization: Bearer '
.
$accessToken
,
'Content-Type: application/json'
,
];
$result
=
$this
->
httpPostJson
(
$url
,
$body
,
$headers
);
// 判断回传是否成功(根据小红书返回结构判断)
if
(
isset
(
$result
[
'code'
])
&&
$result
[
'code'
]
===
0
)
{
return
[
'success'
=>
true
,
'response'
=>
json_encode
(
$result
)];
}
return
[
'success'
=>
false
,
'response'
=>
json_encode
(
$result
),
'err_msg'
=>
$result
[
'msg'
]
??
(
$result
[
'message'
]
??
'未知错误'
),
];
}
/**
* 处理转化回传(含重试逻辑)
*
* @param array $jobData 队列数据
* @return bool
*/
public
function
handleReport
(
$jobData
)
{
$logId
=
$jobData
[
'log_id'
]
??
0
;
$userId
=
$jobData
[
'user_id'
]
??
0
;
custom_log
(
'[xhs.report] 开始处理转化回传, log_id='
.
$logId
.
', data='
.
json_encode
(
$jobData
,
JSON_UNESCAPED_UNICODE
),
$userId
,
'xhs_ad'
);
// 查询日志记录
$log
=
ConversionLogModel
::
find
(
$logId
);
if
(
!
$log
)
{
custom_log
(
'[xhs.report] 转化日志不存在, log_id='
.
$logId
,
$userId
,
'xhs_ad'
,
'warning'
);
return
false
;
}
// 非待回传状态跳过
if
(
$log
->
status
!=
ConversionLogModel
::
STATUS_PENDING
)
{
custom_log
(
'[xhs.report] 日志状态非待回传,跳过, log_id='
.
$logId
.
', status='
.
$log
->
status
,
$userId
,
'xhs_ad'
,
'warning'
);
return
true
;
}
// 超过最大重试次数
if
(
$log
->
retry_count
>=
ConversionLogModel
::
MAX_RETRY
)
{
$log
->
status
=
ConversionLogModel
::
STATUS_FAILED
;
$log
->
api_err_msg
=
'超过最大重试次数('
.
ConversionLogModel
::
MAX_RETRY
.
')'
;
$log
->
updatetime
=
time
();
$log
->
save
();
custom_log
(
'[xhs.report] 超过最大重试次数,标记失败, log_id='
.
$logId
,
$userId
,
'xhs_ad'
,
'error'
);
return
false
;
}
// 调用回传
$result
=
$this
->
reportConversion
(
$jobData
);
$now
=
time
();
$log
->
retry_count
=
$log
->
retry_count
+
1
;
$log
->
report_time
=
$now
;
$log
->
updatetime
=
$now
;
if
(
$result
[
'success'
])
{
$log
->
status
=
ConversionLogModel
::
STATUS_SUCCESS
;
$log
->
api_response
=
$result
[
'response'
];
$log
->
save
();
custom_log
(
'[xhs.report] 转化回传成功, log_id='
.
$logId
,
$userId
,
'xhs_ad'
);
return
true
;
}
else
{
$log
->
status
=
(
$log
->
retry_count
>=
ConversionLogModel
::
MAX_RETRY
)
?
ConversionLogModel
::
STATUS_FAILED
:
ConversionLogModel
::
STATUS_PENDING
;
$log
->
api_response
=
$result
[
'response'
]
??
''
;
$log
->
api_err_msg
=
$result
[
'err_msg'
]
??
'API 返回异常'
;
$log
->
save
();
custom_log
(
'[xhs.report] 转化回传失败, log_id='
.
$logId
.
', retry='
.
$log
->
retry_count
.
', err='
.
$log
->
api_err_msg
,
$userId
,
'xhs_ad'
,
'error'
);
return
false
;
}
}
/**
* 转化类型映射:内部类型 → 小红书事件编码
* ⚠️ 具体编码以小红书开放平台文档为准
*
* @param string $type
* @return string
*/
private
function
mapEventType
(
$type
)
{
$map
=
[
'register'
=>
'REGISTER'
,
'order'
=>
'ORDER'
,
'payment'
=>
'PURCHASE'
,
'add_wechat'
=>
'ADD_WECHAT'
,
];
return
$map
[
$type
]
??
strtoupper
(
$type
);
}
/**
* 获取配置(优先环境变量,其次 shopro 配置组)
*
* @param string $key
* @param mixed $default
* @return mixed
*/
private
function
getConfig
(
$key
,
$default
=
null
)
{
// 1. 优先环境变量(如 PHP_XHS_APP_KEY / PHP_XHS_APP_SECRET)
$envKey
=
'PHP_XHS_'
.
strtoupper
(
$key
);
$envValue
=
getenv
(
$envKey
);
if
(
$envValue
!==
false
&&
$envValue
!==
''
)
{
return
$envValue
;
}
// 2. 从 shopro 配置组中获取(需先在 fa_shopro_config 表添加 code=xhs_ad 的配置组)
$config
=
sheep_config
(
'xhs_ad'
);
if
(
$config
&&
isset
(
$config
[
$key
]))
{
return
$config
[
$key
];
}
return
$default
;
}
/**
* HTTP POST (form-urlencoded)
*/
private
function
httpPost
(
$url
,
$data
)
{
try
{
$ch
=
curl_init
();
curl_setopt
(
$ch
,
CURLOPT_URL
,
$url
);
curl_setopt
(
$ch
,
CURLOPT_POST
,
true
);
curl_setopt
(
$ch
,
CURLOPT_POSTFIELDS
,
http_build_query
(
$data
));
curl_setopt
(
$ch
,
CURLOPT_RETURNTRANSFER
,
true
);
curl_setopt
(
$ch
,
CURLOPT_SSL_VERIFYPEER
,
false
);
curl_setopt
(
$ch
,
CURLOPT_TIMEOUT
,
10
);
$result
=
curl_exec
(
$ch
);
$httpCode
=
curl_getinfo
(
$ch
,
CURLINFO_HTTP_CODE
);
$error
=
curl_error
(
$ch
);
curl_close
(
$ch
);
if
(
$error
)
{
custom_log
(
'[xhs.api] curl error: '
.
$error
,
0
,
'xhs_ad'
,
'error'
);
return
null
;
}
if
(
$httpCode
!=
200
)
{
custom_log
(
'[xhs.api] http error: '
.
$httpCode
.
', body='
.
substr
(
$result
,
0
,
500
),
0
,
'xhs_ad'
,
'error'
);
return
null
;
}
return
json_decode
(
$result
,
true
)
?:
null
;
}
catch
(
\Exception
$e
)
{
custom_log
(
'[xhs.api] POST 请求异常: '
.
$e
->
getMessage
(),
0
,
'xhs_ad'
,
'error'
);
return
null
;
}
}
/**
* HTTP POST (JSON body)
*/
private
function
httpPostJson
(
$url
,
$data
,
$headers
=
[])
{
try
{
$ch
=
curl_init
();
curl_setopt
(
$ch
,
CURLOPT_URL
,
$url
);
curl_setopt
(
$ch
,
CURLOPT_POST
,
true
);
curl_setopt
(
$ch
,
CURLOPT_POSTFIELDS
,
json_encode
(
$data
));
curl_setopt
(
$ch
,
CURLOPT_RETURNTRANSFER
,
true
);
curl_setopt
(
$ch
,
CURLOPT_SSL_VERIFYPEER
,
false
);
curl_setopt
(
$ch
,
CURLOPT_TIMEOUT
,
15
);
curl_setopt
(
$ch
,
CURLOPT_HTTPHEADER
,
$headers
);
$result
=
curl_exec
(
$ch
);
$httpCode
=
curl_getinfo
(
$ch
,
CURLINFO_HTTP_CODE
);
$error
=
curl_error
(
$ch
);
curl_close
(
$ch
);
if
(
$error
)
{
custom_log
(
'[xhs.api] curl error: '
.
$error
,
0
,
'xhs_ad'
,
'error'
);
return
null
;
}
if
(
$httpCode
!=
200
)
{
custom_log
(
'[xhs.api] http error: '
.
$httpCode
.
', body='
.
substr
(
$result
,
0
,
500
),
0
,
'xhs_ad'
,
'error'
);
}
return
json_decode
(
$result
,
true
)
?:
null
;
}
catch
(
\Exception
$e
)
{
custom_log
(
'[xhs.api] POST JSON 请求异常: '
.
$e
->
getMessage
(),
0
,
'xhs_ad'
,
'error'
);
return
null
;
}
}
}
addons/shopro/validate/user/XhsAd.php
0 → 100644
View file @
e56e6914
<?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'
],
];
}
application/admin/model/shopro/xhs/AdTracking.php
0 → 100644
View file @
e56e6914
<?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
();
}
}
application/admin/model/shopro/xhs/ConversionLog.php
0 → 100644
View file @
e56e6914
<?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
;
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment