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
b989e7b5
Commit
b989e7b5
authored
Jul 16, 2026
by
刘小敏
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat(xhs): 对齐小红书种草直达API并完善转化回传
parent
31a2b4a6
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
166 additions
and
35 deletions
+166
-35
XhsAd.php
addons/shopro/controller/user/XhsAd.php
+6
-4
XhsConversionReport.php
addons/shopro/job/XhsConversionReport.php
+17
-10
ConversionService.php
addons/shopro/service/xhs/ConversionService.php
+141
-19
ConversionLog.php
application/admin/model/shopro/xhs/ConversionLog.php
+2
-2
No files found.
addons/shopro/controller/user/XhsAd.php
View file @
b989e7b5
...
...
@@ -38,10 +38,10 @@ class XhsAd extends Common
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($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'
);
...
...
@@ -55,6 +55,7 @@ class XhsAd extends Common
'request_id'
=>
$params
[
'request_id'
]
??
''
,
'event_type'
=>
$params
[
'event_type'
]
??
''
,
'landing_page'
=>
$params
[
'landing_page'
]
??
''
,
'platform'
=>
'BEEVERA WechatMiniProgram'
,
]);
if
(
!
$tracking
)
{
...
...
@@ -148,6 +149,7 @@ class XhsAd extends Common
'value'
=>
$value
,
'order_id'
=>
$orderId
,
'conversion_time'
=>
$now
,
'platform'
=>
'WechatMiniProgram'
,
];
\think\Queue
::
push
(
'addons\shopro\job\XhsConversionReport'
,
$jobData
,
'shopro'
);
...
...
addons/shopro/job/XhsConversionReport.php
View file @
b989e7b5
...
...
@@ -19,13 +19,23 @@ use addons\shopro\service\xhs\ConversionService;
* - value float 转化金额
* - order_id string 订单号
* - conversion_time int 转化发生时间(秒级时间戳)
* - platform string 平台来源,默认 WechatMiniProgram
*
* 重试规则(对齐小红书官方要求):
* - 最多重试 30 次
* - 每次重试间隔 60 秒(1 分钟)
*/
class
XhsConversionReport
extends
BaseJob
{
/**
* 任务失败重试次数上限
* 任务失败重试次数上限
(官方要求 30 次)
*/
const
MAX_ATTEMPTS
=
3
;
const
MAX_ATTEMPTS
=
30
;
/**
* 重试间隔(秒),官方要求每次间隔 1 分钟
*/
const
RETRY_DELAY
=
60
;
/**
* 执行队列任务
...
...
@@ -64,12 +74,9 @@ class XhsConversionReport extends BaseJob
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
);
// 固定 60 秒后重试(对齐官方要求)
custom_log
(
'[xhs.job] 回传失败,第'
.
$attempts
.
'次尝试,'
.
self
::
RETRY_DELAY
.
'秒后重试, log_id='
.
$logId
,
$userId
,
'xhs_ad'
,
'warning'
);
$job
->
release
(
self
::
RETRY_DELAY
);
return
false
;
}
}
catch
(
\Exception
$e
)
{
...
...
@@ -82,8 +89,8 @@ class XhsConversionReport extends BaseJob
return
false
;
}
// 异常时 60
s
后重试
$job
->
release
(
60
);
// 异常时 60
秒
后重试
$job
->
release
(
self
::
RETRY_DELAY
);
return
false
;
}
}
...
...
addons/shopro/service/xhs/ConversionService.php
View file @
b989e7b5
...
...
@@ -17,12 +17,14 @@ class ConversionService
{
// API 基础地址(以小红书开放平台官方文档为准)
const
API_BASE
=
'https://ad-market.xiaohongshu.com/open/api'
;
// const API_BASE = 'https://adapi.xiaohongshu.com';
// Token 接口
const
TOKEN_URL
=
'/oauth/token'
;
// 转化回传接口(具体路径以开放平台文档为准)
const
CONVERSION_URL
=
'/conversion/upload'
;
// const CONVERSION_URL = '/api/open/conversion';
// Token 缓存 key 前缀
const
TOKEN_CACHE_KEY
=
'shopro:xhs:access_token'
;
...
...
@@ -96,14 +98,16 @@ class ConversionService
}
/**
* 回传转化事件到小红书
* 回传转化事件到小红书(对齐官方「种草直达」API 文档)
* 官方文档: https://adapi.xiaohongshu.com/api/open/conversion
*
* @param array $data 转化数据
* - click_id string 广告点击 ID
* - conversion_type string 转化类型
* - click_id string 广告点击 ID
(必填)
* - conversion_type string 转化类型
(必填)
* - value float 转化金额
* - order_id string 订单号
* - order_id string 订单号
(用于查询商品上下文)
* - conversion_time int 转化发生时间(秒级时间戳)
* - platform string 平台来源,默认 WechatMiniProgram
* @return array ['success' => bool, 'response' => mixed]
*/
public
function
reportConversion
(
$data
)
...
...
@@ -115,31 +119,42 @@ class ConversionService
return
[
'success'
=>
false
,
'response'
=>
'click_id 为空'
];
}
// 获取 app_id 和 access_token
$appId
=
$this
->
getConfig
(
'APP_ID'
,
''
);
try
{
$accessToken
=
$this
->
getAccessToken
();
}
catch
(
\Exception
$e
)
{
return
[
'success'
=>
false
,
'response'
=>
'获取 Token 失败: '
.
$e
->
getMessage
()];
}
// 转换事件类型为小红书平台编码(具体编码以开放平台文档为准)
if
(
empty
(
$appId
))
{
return
[
'success'
=>
false
,
'response'
=>
'app_id 未配置'
];
}
// 转换事件类型为小红书平台编码
// 1=商品成交, 122=微信加好友, 123=注册, 124=微信小程序访问
$eventType
=
$this
->
mapEventType
(
$conversionType
);
// 构建 context.product 商品上下文(仅商品成交事件需要)
$context
=
$this
->
buildContext
(
$data
,
$eventType
);
$url
=
self
::
API_BASE
.
self
::
CONVERSION_URL
;
// 对齐官方 API 请求体格式: app_id、access_token 放在 body 中,不放在 Header
$body
=
[
'app_id'
=>
$appId
,
'access_token'
=>
$accessToken
,
'event_type'
=>
strval
(
$eventType
),
'timestamp'
=>
intval
(
microtime
(
true
)
*
1000
),
// 毫秒级时间戳
'click_id'
=>
$clickId
,
'event_type'
=>
$eventType
,
'event_time'
=>
intval
(
$data
[
'conversion_time'
]
??
0
),
'conversion_value'
=>
floatval
(
$data
[
'value'
]
??
0
),
'platform'
=>
$data
[
'platform'
]
??
'爱岛珠宝微信小程序'
,
];
// 有订单号时附加
if
(
!
empty
(
$data
[
'order_id'
]))
{
$body
[
'order_id'
]
=
$data
[
'order_id'
];
if
(
!
empty
(
$context
))
{
$body
[
'context'
]
=
$context
;
}
$headers
=
[
'Authorization: Bearer '
.
$accessToken
,
'Content-Type: application/json'
,
];
...
...
@@ -158,6 +173,108 @@ class ConversionService
}
/**
* 构建转化上下文(context.product)
* 对于商品成交(event_type=1),自动查询订单获取商品信息
*
* @param array $data 转化数据
* @param int $eventType 小红书事件类型编码
* @return array
*/
private
function
buildContext
(
$data
,
$eventType
)
{
$context
=
[];
// 只有商品成交事件(event_type=1)需要商品上下文
if
(
$eventType
!=
1
)
{
return
$context
;
}
$orderId
=
$data
[
'order_id'
]
??
''
;
if
(
empty
(
$orderId
))
{
return
$context
;
}
try
{
$orderModel
=
\app\admin\model\shopro\order\Order
::
where
(
'id'
,
$orderId
)
->
find
();
if
(
!
$orderModel
)
{
custom_log
(
'[xhs.context] 未找到订单, order_sn='
.
$orderId
,
0
,
'xhs_ad'
,
'warning'
);
return
$context
;
}
$items
=
\app\admin\model\shopro\order\OrderItem
::
where
(
'order_id'
,
$orderId
)
->
select
();
$orderCount
=
0
;
$productId
=
''
;
$productName
=
''
;
$productPrice
=
0
;
$productImg
=
''
;
foreach
(
$items
as
$item
)
{
$orderCount
+=
intval
(
$item
->
goods_num
??
1
);
if
(
empty
(
$productId
))
{
$productId
=
strval
(
$item
->
goods_id
??
''
);
$productName
=
$item
->
goods_title
??
''
;
$productPrice
=
floatval
(
$item
->
goods_price
??
0
);
$productImg
=
$item
->
goods_image
??
''
;
}
}
$context
[
'product'
]
=
[
'product_id'
=>
$productId
,
'product_name'
=>
$productName
,
'product_price'
=>
$productPrice
,
'product_category'
=>
$this
->
getCategoryName
(
$orderModel
),
'img_url'
=>
$productImg
,
'shop_name'
=>
'BEEVERA'
,
'order_id'
=>
$orderId
,
'order_count'
=>
$orderCount
,
'pay_amount'
=>
floatval
(
$data
[
'value'
]
??
(
$orderModel
->
pay_fee
??
0
)),
];
custom_log
(
'[xhs.context] 商品上下文构建成功, order_sn='
.
$orderId
,
0
,
'xhs_ad'
);
}
catch
(
\Exception
$e
)
{
custom_log
(
'[xhs.context] 获取商品上下文异常: '
.
$e
->
getMessage
(),
0
,
'xhs_ad'
,
'error'
);
}
return
$context
;
}
/**
* 获取订单商品分类名称
*
* @param \app\admin\model\shopro\order\Order $order
* @return string
*/
private
function
getCategoryName
(
$order
)
{
try
{
$items
=
$order
->
items
;
if
(
!
empty
(
$items
))
{
$firstItem
=
$items
[
0
];
$goodsId
=
$firstItem
->
goods_id
??
0
;
if
(
$goodsId
)
{
$goods
=
\app\admin\model\shopro\goods\Goods
::
find
(
$goodsId
);
if
(
$goods
&&
!
empty
(
$goods
->
category_ids
))
{
$categoryIds
=
is_string
(
$goods
->
category_ids
)
?
json_decode
(
$goods
->
category_ids
,
true
)
:
$goods
->
category_ids
;
if
(
!
empty
(
$categoryIds
)
&&
is_array
(
$categoryIds
))
{
$categoryId
=
$categoryIds
[
0
]
??
0
;
if
(
$categoryId
)
{
$category
=
\app\admin\model\shopro\category\Category
::
find
(
$categoryId
);
return
$category
?
$category
->
name
:
''
;
}
}
}
}
}
}
catch
(
\Exception
$e
)
{
// 分类查询失败不影响主流程
}
return
''
;
}
/**
* 处理转化回传(含重试逻辑)
*
* @param array $jobData 队列数据
...
...
@@ -226,21 +343,26 @@ class ConversionService
/**
* 转化类型映射:内部类型 → 小红书事件编码
* ⚠️ 具体编码以小红书开放平台文档为准
* 官方事件枚举:
* 1 = 商品成交(支持联调/报表展示/优化)
* 122 = 微信加好友(支持联调/报表展示,不支持优化)
* 123 = 注册(支持联调/报表展示,不支持优化)
* 124 = 微信小程序访问(支持联调/报表展示,不支持优化)
*
* @param string $type
* @return
string
* @return
int
*/
private
function
mapEventType
(
$type
)
{
$map
=
[
'register'
=>
'REGISTER'
,
'order'
=>
'ORDER'
,
'payment'
=>
'PURCHASE'
,
'add_wechat'
=>
'ADD_WECHAT'
,
'register'
=>
123
,
// 注册
'order'
=>
1
,
// 商品成交(下单)
// 'payment' => 1, // 商品成交(付款)
// 'add_wechat' => 122, // 微信加好友
'visit'
=>
124
,
// 微信小程序访问
];
return
$map
[
$type
]
??
strtoupper
(
$type
)
;
return
$map
[
$type
]
??
0
;
}
/**
...
...
application/admin/model/shopro/xhs/ConversionLog.php
View file @
b989e7b5
...
...
@@ -22,8 +22,8 @@ class ConversionLog extends Common
const
STATUS_NO_TRACK
=
3
;
// 无需回传(无有效 click_id)
const
STATUS_EXPIRED
=
4
;
// 追踪已过期
// 最大重试次数
const
MAX_RETRY
=
3
;
// 最大重试次数
(对齐小红书官方要求 30 次)
const
MAX_RETRY
=
3
0
;
/**
* 状态文本映射
...
...
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