feat: 添加飞书同步功能,增强数据采集工具
在产品历史KOC助手中新增飞书同步功能,允许用户将采集到的数据同步到飞书多维表格。此更新包括飞书应用ID、密钥及表格ID的配置选项,支持对已存在记录的检查与批量上传,提升了数据管理的灵活性和效率。同时,优化了数据筛选条件,确保采集的数据符合用户需求。
This commit is contained in:
parent
805d3037e6
commit
ce078e9da6
@ -14,12 +14,13 @@
|
||||
// @grant GM_setClipboard
|
||||
// @connect chanmama.com
|
||||
// @connect api-service.chanmama.com
|
||||
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
|
||||
// @connect open.feishu.cn
|
||||
// @require https://unpkg.com/sweetalert2@11
|
||||
// @license MIT
|
||||
// ==/UserScript==
|
||||
|
||||
/* ==UserConfig==
|
||||
settings:
|
||||
basic:
|
||||
enabled:
|
||||
title: 启用功能
|
||||
description: 是否启用蝉妈妈产品历史KOC助手功能
|
||||
@ -38,23 +39,94 @@ settings:
|
||||
description: 启用详细日志输出
|
||||
type: checkbox
|
||||
default: false
|
||||
---
|
||||
feishu:
|
||||
enableSync:
|
||||
title: 启用飞书同步
|
||||
description: 是否将采集到的数据同步到飞书多维表格
|
||||
type: checkbox
|
||||
default: false
|
||||
appId:
|
||||
title: 飞书应用ID
|
||||
description: 飞书开放平台应用的App ID
|
||||
type: text
|
||||
default: "cli_a73b734d0564d01c"
|
||||
appSecret:
|
||||
title: 飞书应用密钥
|
||||
description: 飞书开放平台应用的App Secret
|
||||
type: text
|
||||
default: "xeKSIOpQiVW6oHeJPH4EvlZMZ1QmnCxx"
|
||||
password: true
|
||||
bitableAppId:
|
||||
title: 多维表格App ID
|
||||
description: 飞书多维表格的App ID
|
||||
type: text
|
||||
default: "ILIYb7MUCatbfNsdUMccHsgjnLf"
|
||||
tableId:
|
||||
title: 数据表ID
|
||||
description: 存储KOC数据的表格ID
|
||||
type: text
|
||||
default: "tblK71li9kOfOc7C"
|
||||
filters:
|
||||
amountThreshold:
|
||||
title: 金额门槛
|
||||
description: 预估关联商品视频销售金额的最低门槛(元),低于此值停止采集
|
||||
type: number
|
||||
default: 10000
|
||||
videoRatioThreshold:
|
||||
title: 视频带货占比门槛
|
||||
description: 视频销售金额占总销售金额的比例门槛,如0.5表示视频销售需占总销售的50%以上
|
||||
type: number
|
||||
default: 0.5
|
||||
maxFollowers:
|
||||
title: 粉丝数上限
|
||||
description: 达人粉丝数上限,只采集粉丝数少于此数值的达人
|
||||
type: number
|
||||
default: 500000
|
||||
==/UserConfig== */
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 检测依赖库加载状态
|
||||
function checkLibraryStatus() {
|
||||
if (typeof Swal !== 'undefined') {
|
||||
log('SweetAlert2 库加载成功');
|
||||
} else {
|
||||
log('SweetAlert2 库未加载,将使用备用方案', 'warn');
|
||||
}
|
||||
}
|
||||
|
||||
// 配置常量
|
||||
const CONFIG = {
|
||||
debug: true, // 强制开启调试模式
|
||||
enabled: GM_getValue('settings.enabled', true),
|
||||
timeout: GM_getValue('settings.timeout', 30) * 1000,
|
||||
debug: GM_getValue('basic.debug', false),
|
||||
enabled: GM_getValue('basic.enabled', true),
|
||||
timeout: GM_getValue('basic.timeout', 30) * 1000,
|
||||
api: {
|
||||
baseUrl: 'https://api-service.chanmama.com',
|
||||
endpoint: '/v1/product/author/analysis'
|
||||
},
|
||||
pagination: {
|
||||
pageSize: 30,
|
||||
amountThreshold: 10000 // 金额门槛,低于此值停止翻页
|
||||
amountThreshold: GM_getValue('filters.amountThreshold', 10000)
|
||||
},
|
||||
filters: {
|
||||
videoRatioThreshold: GM_getValue('filters.videoRatioThreshold', 0.5),
|
||||
maxFollowers: GM_getValue('filters.maxFollowers', 500000)
|
||||
},
|
||||
feishu: {
|
||||
enabled: GM_getValue('feishu.enableSync', false),
|
||||
appId: GM_getValue('feishu.appId', ''),
|
||||
appSecret: GM_getValue('feishu.appSecret', ''),
|
||||
bitableAppId: GM_getValue('feishu.bitableAppId', ''),
|
||||
tableId: GM_getValue('feishu.tableId', ''),
|
||||
urls: {
|
||||
tenantAccessToken: "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
||||
bitableBatchCreate: (appId, tableId) =>
|
||||
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records/batch_create`,
|
||||
bitableSearchRecords: (appId, tableId) =>
|
||||
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records/search`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -88,6 +160,13 @@ settings:
|
||||
timeout: CONFIG.timeout,
|
||||
anonymous: false,
|
||||
onload: (response) => {
|
||||
// 调试信息:记录详细的请求和响应
|
||||
if (CONFIG.debug) {
|
||||
log(`请求URL: ${url}`);
|
||||
log(`响应状态: ${response.status} ${response.statusText}`);
|
||||
log(`响应内容: ${response.responseText.substring(0, 500)}...`);
|
||||
}
|
||||
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
try {
|
||||
const data = JSON.parse(response.responseText);
|
||||
@ -96,7 +175,16 @@ settings:
|
||||
resolve(response.responseText);
|
||||
}
|
||||
} else {
|
||||
reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
|
||||
// 解析错误响应
|
||||
let errorDetail = response.statusText;
|
||||
try {
|
||||
const errorData = JSON.parse(response.responseText);
|
||||
errorDetail = errorData.msg || errorData.message || errorData.error || response.statusText;
|
||||
} catch (e) {
|
||||
// 如果无法解析JSON,使用原始响应
|
||||
errorDetail = response.responseText || response.statusText;
|
||||
}
|
||||
reject(new Error(`HTTP ${response.status}: ${errorDetail}`));
|
||||
}
|
||||
},
|
||||
onerror: () => reject(new Error('网络请求失败')),
|
||||
@ -105,17 +193,267 @@ settings:
|
||||
});
|
||||
}
|
||||
|
||||
// SweetAlert2 弹窗函数
|
||||
// 重试函数
|
||||
const retry = async (fn, retries = 3, delay = 1000) => {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (i < retries - 1) {
|
||||
log(`重试中... (${i + 1}/${retries})`, 'warn');
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 飞书API相关函数
|
||||
async function fetchFeishuTenantAccessToken() {
|
||||
if (!CONFIG.feishu.appId || !CONFIG.feishu.appSecret) {
|
||||
throw new Error('飞书应用ID或密钥未配置');
|
||||
}
|
||||
|
||||
log(`正在获取飞书访问令牌,App ID: ${CONFIG.feishu.appId.substring(0, 8)}...`);
|
||||
|
||||
try {
|
||||
const response = await retry(() => makeRequest(
|
||||
CONFIG.feishu.urls.tenantAccessToken,
|
||||
{
|
||||
method: 'POST',
|
||||
data: JSON.stringify({
|
||||
app_id: CONFIG.feishu.appId,
|
||||
app_secret: CONFIG.feishu.appSecret,
|
||||
})
|
||||
}
|
||||
));
|
||||
|
||||
log(`飞书API响应: code=${response.code}, msg=${response.msg || '无'}`);
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(`获取飞书访问令牌失败: ${response.msg || response.message || '未知错误'}`);
|
||||
}
|
||||
|
||||
if (!response.tenant_access_token) {
|
||||
throw new Error('飞书返回的访问令牌为空');
|
||||
}
|
||||
|
||||
log('飞书访问令牌获取成功');
|
||||
return response.tenant_access_token;
|
||||
} catch (error) {
|
||||
log(`获取飞书访问令牌失败: ${error.message}`, 'error');
|
||||
log(`请检查:1.应用ID和密钥是否正确 2.应用是否有多维表格权限 3.网络连接是否正常`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取飞书表格中所有已存在的达人UID(支持自动翻页)
|
||||
*/
|
||||
async function getExistingKOCUIDs(accessToken) {
|
||||
try {
|
||||
const url = CONFIG.feishu.urls.bitableSearchRecords(CONFIG.feishu.bitableAppId, CONFIG.feishu.tableId);
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const existingUIDs = new Set();
|
||||
let hasMore = true;
|
||||
let pageToken = null;
|
||||
let pageCount = 0;
|
||||
|
||||
log('开始获取飞书表格中已存在的达人UID...');
|
||||
|
||||
while (hasMore) {
|
||||
pageCount++;
|
||||
log(`获取第 ${pageCount} 页已存在的UID...`);
|
||||
|
||||
// 构造请求体
|
||||
const requestBody = {
|
||||
field_names: ['达人UID'], // 只获取达人UID字段
|
||||
automatic_fields: false
|
||||
};
|
||||
|
||||
// 构造URL参数
|
||||
let requestUrl = url + '?page_size=500&user_id_type=open_id';
|
||||
if (pageToken) {
|
||||
requestUrl += `&page_token=${encodeURIComponent(pageToken)}`;
|
||||
}
|
||||
|
||||
const response = await retry(() => makeRequest(requestUrl, {
|
||||
method: 'POST',
|
||||
data: JSON.stringify(requestBody),
|
||||
headers: headers
|
||||
}));
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(`获取已存在UID失败: ${response.msg || response.message || '未知错误'}`);
|
||||
}
|
||||
|
||||
const data = response.data || {};
|
||||
const items = data.items || [];
|
||||
|
||||
// 提取UID - 处理富文本格式
|
||||
items.forEach(item => {
|
||||
const fields = item.fields || {};
|
||||
const uidField = fields['达人UID'];
|
||||
|
||||
let uid = '';
|
||||
if (uidField) {
|
||||
if (Array.isArray(uidField) && uidField.length > 0) {
|
||||
// 处理富文本格式 [{"text": "值", "type": "text"}]
|
||||
for (const item of uidField) {
|
||||
if (item.type === 'text' && item.text) {
|
||||
uid += item.text;
|
||||
}
|
||||
}
|
||||
} else if (typeof uidField === 'string') {
|
||||
// 处理普通文本格式
|
||||
uid = uidField;
|
||||
}
|
||||
|
||||
if (uid) {
|
||||
existingUIDs.add(uid);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
log(`第 ${pageCount} 页获取到 ${items.length} 条记录`);
|
||||
|
||||
// 检查是否还有更多数据
|
||||
hasMore = data.has_more || false;
|
||||
pageToken = data.page_token || null;
|
||||
|
||||
// 防止无限循环
|
||||
if (pageCount > 1000) {
|
||||
log('达到最大翻页限制,停止获取', 'warn');
|
||||
break;
|
||||
}
|
||||
|
||||
// 批次间延迟,避免频率限制
|
||||
if (hasMore) {
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
}
|
||||
}
|
||||
|
||||
log(`总共获取 ${pageCount} 页,找到 ${existingUIDs.size} 个已存在的达人UID`);
|
||||
return existingUIDs;
|
||||
|
||||
} catch (error) {
|
||||
log(`获取已存在UID失败: ${error.message}`, 'error');
|
||||
// 如果获取失败,返回空集合,继续处理
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
async function batchUploadKOCRecordsToFeishu(accessToken, kocDataList) {
|
||||
try {
|
||||
const url = CONFIG.feishu.urls.bitableBatchCreate(CONFIG.feishu.bitableAppId, CONFIG.feishu.tableId);
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
log(`批量上传URL: ${url}`);
|
||||
log(`上传记录数: ${kocDataList.length}`);
|
||||
|
||||
// 构造批量记录数据
|
||||
const records = kocDataList.map(kocData => ({
|
||||
fields: {
|
||||
'昵称': kocData.昵称,
|
||||
'达人UID': kocData.达人UID,
|
||||
'粉丝数': kocData.粉丝数,
|
||||
'类型': kocData.类型,
|
||||
'口碑分': kocData.口碑分,
|
||||
'关联商品销售量区间': kocData.关联商品销售量区间,
|
||||
'关联商品销售金额区间': kocData.关联商品销售金额区间,
|
||||
'预估关联商品销售量': kocData.预估关联商品销售量,
|
||||
'预估关联商品销售金额': kocData.预估关联商品销售金额,
|
||||
'预估关联商品视频销售量': kocData.预估关联商品视频销售量,
|
||||
'预估关联商品视频销售金额': kocData.预估关联商品视频销售金额,
|
||||
'视频带货占比': kocData.视频带货占比,
|
||||
'关联商品蝉妈妈ID': kocData.关联商品蝉妈妈ID,
|
||||
'采集时间': new Date().getTime(), // 毫秒级时间戳
|
||||
}
|
||||
}));
|
||||
|
||||
// 分批上传,每批最多100条记录
|
||||
const batchSize = 100;
|
||||
const batches = [];
|
||||
for (let i = 0; i < records.length; i += batchSize) {
|
||||
batches.push(records.slice(i, i + batchSize));
|
||||
}
|
||||
|
||||
log(`分为 ${batches.length} 批上传,每批最多 ${batchSize} 条记录`);
|
||||
|
||||
let totalUploaded = 0;
|
||||
for (let i = 0; i < batches.length; i++) {
|
||||
const batch = batches[i];
|
||||
const body = JSON.stringify({ records: batch });
|
||||
|
||||
log(`上传第 ${i + 1}/${batches.length} 批,包含 ${batch.length} 条记录`);
|
||||
|
||||
const response = await retry(() => makeRequest(url, {
|
||||
method: 'POST',
|
||||
data: body,
|
||||
headers: headers
|
||||
}));
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(`批量上传到飞书失败: ${response.msg || response.message || '未知错误'}`);
|
||||
}
|
||||
|
||||
totalUploaded += batch.length;
|
||||
log(`第 ${i + 1} 批上传成功: ${batch.length} 条记录`);
|
||||
|
||||
// 批次间延迟500ms避免频率限制
|
||||
if (i < batches.length - 1) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
log(`批量上传完成: 总共 ${totalUploaded} 条记录`);
|
||||
return { code: 0, uploaded: totalUploaded };
|
||||
} catch (error) {
|
||||
// 特殊处理403权限错误
|
||||
if (error.message.includes('HTTP 403')) {
|
||||
log('HTTP 403权限错误诊断:', 'error');
|
||||
log(`- App ID: ${CONFIG.feishu.appId}`, 'error');
|
||||
log(`- Bitable App ID: ${CONFIG.feishu.bitableAppId}`, 'error');
|
||||
log(`- Table ID: ${CONFIG.feishu.tableId}`, 'error');
|
||||
log('请检查以下几点:', 'error');
|
||||
log('1. 飞书应用是否有多维表格权限', 'error');
|
||||
log('2. App ID和App Secret是否正确', 'error');
|
||||
log('3. 多维表格App ID和表格ID是否正确', 'error');
|
||||
log('4. 应用是否已发布并获得权限', 'error');
|
||||
throw new Error(`飞书权限错误 (403): 请检查应用权限配置`);
|
||||
}
|
||||
log(`批量上传到飞书失败: ${error.message}`, 'error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 弹窗函数 - 带 SweetAlert2 备用方案
|
||||
function showInfo(title, text) {
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
title: title,
|
||||
text: text,
|
||||
icon: 'info',
|
||||
confirmButtonText: '确定'
|
||||
});
|
||||
} else {
|
||||
alert(`${title}\n${text}`);
|
||||
log(`Info: ${title} - ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccess(title, text) {
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
title: title,
|
||||
text: text,
|
||||
@ -123,18 +461,28 @@ settings:
|
||||
timer: 3000,
|
||||
showConfirmButton: false
|
||||
});
|
||||
} else {
|
||||
showToast(`${title}: ${text}`, 'success');
|
||||
log(`Success: ${title} - ${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
function showError(title, text) {
|
||||
if (typeof Swal !== 'undefined') {
|
||||
Swal.fire({
|
||||
title: title,
|
||||
text: text,
|
||||
icon: 'error',
|
||||
confirmButtonText: '确定'
|
||||
});
|
||||
} else {
|
||||
alert(`错误: ${title}\n${text}`);
|
||||
log(`Error: ${title} - ${text}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function showConfirm(title, text) {
|
||||
if (typeof Swal !== 'undefined') {
|
||||
const result = await Swal.fire({
|
||||
title: title,
|
||||
text: text,
|
||||
@ -144,29 +492,8 @@ settings:
|
||||
cancelButtonText: '取消'
|
||||
});
|
||||
return result.isConfirmed;
|
||||
}
|
||||
|
||||
// 数据导出函数
|
||||
function exportToJSON(data, filename = 'koc_history') {
|
||||
// JSON导出逻辑
|
||||
log('导出JSON格式数据');
|
||||
const jsonStr = JSON.stringify(data, null, 2);
|
||||
GM_setClipboard(jsonStr);
|
||||
showSuccess('导出成功', 'JSON数据已复制到剪贴板');
|
||||
}
|
||||
|
||||
// KOC数据处理函数
|
||||
async function fetchKOCHistory(productId) {
|
||||
try {
|
||||
log(`获取产品 ${productId} 的KOC历史数据`);
|
||||
const response = await makeRequest(`${CONFIG.api.baseUrl}${CONFIG.api.endpoint}`, {
|
||||
method: 'POST',
|
||||
data: JSON.stringify({ productId })
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
log(`获取KOC历史数据失败: ${error.message}`, 'error');
|
||||
throw error;
|
||||
} else {
|
||||
return confirm(`${title}\n${text}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,6 +568,12 @@ settings:
|
||||
baseInfo.预估关联商品视频销售金额 = totalVideoAmount;
|
||||
baseInfo.关联商品蝉妈妈ID = Array.from(videoProductIds).join(',');
|
||||
|
||||
// 计算视频带货占比(限制最大值为1)
|
||||
const totalAmount = baseInfo.预估关联商品销售金额 || 0;
|
||||
let videoRatio = totalAmount > 0 ? totalVideoAmount / totalAmount : 0;
|
||||
videoRatio = Math.min(videoRatio, 1); // 限制最大值为1
|
||||
baseInfo.视频带货占比 = videoRatio;
|
||||
|
||||
results.push(baseInfo);
|
||||
});
|
||||
|
||||
@ -270,6 +603,14 @@ settings:
|
||||
<div class="koc-status-indicator"></div>
|
||||
<span class="koc-status-text">准备采集...</span>
|
||||
</div>
|
||||
${CONFIG.feishu.enabled ? `
|
||||
<div class="koc-feishu-status">
|
||||
<svg class="koc-feishu-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.94-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
||||
</svg>
|
||||
<span class="koc-feishu-text">飞书同步已启用</span>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="koc-actions">
|
||||
<button class="koc-download-btn" type="button">
|
||||
<svg class="koc-btn-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||
@ -456,6 +797,30 @@ settings:
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.koc-feishu-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(59, 130, 246, 0.1);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.koc-feishu-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #3b82f6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.koc-feishu-text {
|
||||
font-size: 11px;
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.koc-floating-window.dragging {
|
||||
transition: none !important;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
@ -577,7 +942,7 @@ settings:
|
||||
function extractProductIdFromUrl() {
|
||||
const url = window.location.href;
|
||||
const match = url.match(/promotionRank\/([^\.]+)\.html/);
|
||||
return match ? match[1] : "MUJ0VtooAuIRKA33vMWQN2QUo4A016WL";
|
||||
return match ? match[1] : "";
|
||||
}
|
||||
|
||||
// 计算时间范围(近365天,T-1结束)
|
||||
@ -644,19 +1009,45 @@ settings:
|
||||
break;
|
||||
}
|
||||
|
||||
// 检查金额门槛 - 使用预估关联商品视频销售金额的最小值
|
||||
const amountValues = parsedData.map(author => author.预估关联商品视频销售金额 || 0);
|
||||
const minAmount = Math.min(...amountValues);
|
||||
log(`第 ${currentPage} 页最小金额: ${minAmount}`, 'debug');
|
||||
// 应用筛选条件
|
||||
const filteredData = parsedData.filter(author => {
|
||||
// 检查金额门槛
|
||||
if (author.预估关联商品视频销售金额 < CONFIG.pagination.amountThreshold) {
|
||||
log(`筛选掉达人 ${author.昵称}: 视频销售金额 ${author.预估关联商品视频销售金额} < ${CONFIG.pagination.amountThreshold}`, 'debug');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (minAmount < CONFIG.pagination.amountThreshold) {
|
||||
// 检查粉丝数上限
|
||||
const followers = author.粉丝数 || 0;
|
||||
if (followers >= CONFIG.filters.maxFollowers) {
|
||||
log(`筛选掉达人 ${author.昵称}: 粉丝数 ${followers} >= ${CONFIG.filters.maxFollowers}`, 'debug');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查视频带货占比(使用已计算的值)
|
||||
const videoRatio = author.视频带货占比 || 0;
|
||||
|
||||
if (videoRatio < CONFIG.filters.videoRatioThreshold) {
|
||||
log(`筛选掉达人 ${author.昵称}: 视频带货占比 ${videoRatio.toFixed(3)} < ${CONFIG.filters.videoRatioThreshold}`, 'debug');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
log(`第 ${currentPage} 页原始数据 ${parsedData.length} 条,筛选后 ${filteredData.length} 条`, 'info');
|
||||
|
||||
// 检查金额门槛 - 使用预估关联商品销售金额的最大值来决定是否停止采集
|
||||
const amountValues = parsedData.map(author => author.预估关联商品销售金额 || 0);
|
||||
const maxAmount = Math.max(...amountValues);
|
||||
log(`第 ${currentPage} 页最大金额: ${maxAmount}`, 'debug');
|
||||
|
||||
if (maxAmount < CONFIG.pagination.amountThreshold) {
|
||||
log(`达到金额门槛 ${CONFIG.pagination.amountThreshold},停止采集`, 'info');
|
||||
// 过滤掉低于门槛的数据
|
||||
const filteredData = parsedData.filter(author => author.预估关联商品视频销售金额 >= CONFIG.pagination.amountThreshold);
|
||||
allData = allData.concat(filteredData);
|
||||
shouldContinue = false;
|
||||
} else {
|
||||
allData = allData.concat(parsedData);
|
||||
allData = allData.concat(filteredData);
|
||||
currentPage++;
|
||||
|
||||
// 防止无限循环,最多采集100页
|
||||
@ -676,15 +1067,98 @@ settings:
|
||||
log(`=== 数据采集完成 ===`, 'info');
|
||||
log(`总共采集 ${allData.length} 条符合条件的达人数据`, 'info');
|
||||
|
||||
// 显示采集结果的toast提示
|
||||
showToast(`📊 采集完成:找到 ${allData.length} 个满足条件的达人`, 'success');
|
||||
|
||||
// 打印完整数据到日志
|
||||
log('📊 KOC数据采集结果:', 'info');
|
||||
log(`📈 共采集 ${allData.length} 条达人数据`, 'info');
|
||||
log('📋 数据详情:', 'info');
|
||||
log(JSON.stringify(allData, null, 2), 'info');
|
||||
|
||||
// 显示成功提示
|
||||
setStatus(floatingWindow, 'success', `已采集 ${allData.length} 条数据`);
|
||||
showSuccess('采集成功', `已采集 ${allData.length} 条符合条件的达人数据,请查看控制台`);
|
||||
// 飞书同步处理
|
||||
let feishuResult = 'Success';
|
||||
let syncLogs = [];
|
||||
let syncedCount = 0;
|
||||
|
||||
if (CONFIG.feishu.enabled && allData.length > 0) {
|
||||
try {
|
||||
log(`=== 开始同步到飞书多维表格 ===`, 'info');
|
||||
setStatus(floatingWindow, 'processing', '同步到飞书中...');
|
||||
|
||||
// 检查飞书配置
|
||||
if (!CONFIG.feishu.bitableAppId || !CONFIG.feishu.tableId) {
|
||||
throw new Error('飞书多维表格配置不完整,请检查配置');
|
||||
}
|
||||
|
||||
const accessToken = await fetchFeishuTenantAccessToken();
|
||||
syncLogs.push('飞书访问令牌获取成功');
|
||||
|
||||
setStatus(floatingWindow, 'processing', '检查已存在记录...');
|
||||
|
||||
// 获取已存在的达人UID
|
||||
const existingUIDs = await getExistingKOCUIDs(accessToken);
|
||||
syncLogs.push(`查询到 ${existingUIDs.size} 个已存在的达人UID`);
|
||||
|
||||
// 过滤掉已存在的记录
|
||||
const newRecords = allData.filter(kocData => !existingUIDs.has(kocData.达人UID));
|
||||
const skippedCount = allData.length - newRecords.length;
|
||||
|
||||
log(`原始数据 ${allData.length} 条,已存在 ${skippedCount} 条,需要上传 ${newRecords.length} 条新记录`);
|
||||
syncLogs.push(`总计 ${allData.length} 条记录,跳过 ${skippedCount} 条已存在,上传 ${newRecords.length} 条新记录`);
|
||||
|
||||
if (newRecords.length > 0) {
|
||||
setStatus(floatingWindow, 'processing', `批量上传 ${newRecords.length} 条新记录...`);
|
||||
|
||||
const result = await batchUploadKOCRecordsToFeishu(accessToken, newRecords);
|
||||
syncedCount = result.uploaded || newRecords.length;
|
||||
syncLogs.push(`✅ 批量上传完成: ${syncedCount} 条记录`);
|
||||
|
||||
setStatus(floatingWindow, 'success', `上传完成: ${syncedCount} 条新记录`);
|
||||
|
||||
// 显示上传结果的toast提示
|
||||
showToast(`📤 飞书同步:${skippedCount} 个重复跳过,${syncedCount} 个新增写入`, 'info');
|
||||
} else {
|
||||
syncLogs.push('✅ 没有需要上传的新记录');
|
||||
setStatus(floatingWindow, 'success', '所有记录均已存在,无需上传');
|
||||
|
||||
// 显示全部重复的toast提示
|
||||
showToast(`📋 飞书同步:${allData.length} 个达人均已存在,无新增记录`, 'warning');
|
||||
}
|
||||
|
||||
log(`=== 飞书同步完成 ===`, 'info');
|
||||
log(`成功同步 ${syncedCount} 条新数据到飞书多维表格`, 'info');
|
||||
|
||||
} catch (error) {
|
||||
feishuResult = 'Failed';
|
||||
syncLogs.push(`❌ 飞书同步失败: ${error.message}`);
|
||||
log(`飞书同步失败: ${error.message}`, 'error');
|
||||
setStatus(floatingWindow, 'error', '飞书同步失败');
|
||||
showError('飞书同步失败', error.message);
|
||||
|
||||
// 显示同步失败的toast提示
|
||||
showToast(`❌ 飞书同步失败:${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示最终结果
|
||||
const successMessage = CONFIG.feishu.enabled
|
||||
? `已采集 ${allData.length} 条数据${syncedCount > 0 ? `,同步 ${syncedCount} 条新数据到飞书` : ',所有数据均已存在'}`
|
||||
: `已采集 ${allData.length} 条符合条件的达人数据,请查看控制台`;
|
||||
|
||||
setStatus(floatingWindow, 'success', `采集完成 (${allData.length}条)`);
|
||||
showSuccess('采集成功', successMessage);
|
||||
|
||||
// 显示最终结果的详细toast
|
||||
if (CONFIG.feishu.enabled) {
|
||||
if (syncedCount > 0) {
|
||||
showToast(`✅ 任务完成:采集 ${allData.length} 个达人,新增 ${syncedCount} 个到飞书`, 'success');
|
||||
} else {
|
||||
showToast(`✅ 任务完成:采集 ${allData.length} 个达人,全部已存在`, 'info');
|
||||
}
|
||||
} else {
|
||||
showToast(`✅ 任务完成:采集 ${allData.length} 个符合条件的达人`, 'success');
|
||||
}
|
||||
|
||||
// 5秒后恢复按钮状态
|
||||
setTimeout(() => {
|
||||
@ -705,22 +1179,6 @@ settings:
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentProductId() {
|
||||
// 从页面URL或DOM元素中获取产品ID
|
||||
const urlMatch = window.location.pathname.match(/\/product\/(\d+)/);
|
||||
if (urlMatch) {
|
||||
return urlMatch[1];
|
||||
}
|
||||
|
||||
// 尝试从页面元素获取
|
||||
const productElement = document.querySelector('[data-product-id]');
|
||||
if (productElement) {
|
||||
return productElement.getAttribute('data-product-id');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 主逻辑
|
||||
async function init() {
|
||||
if (!CONFIG.enabled) {
|
||||
@ -729,10 +1187,30 @@ settings:
|
||||
}
|
||||
|
||||
try {
|
||||
// 检测依赖库状态
|
||||
checkLibraryStatus();
|
||||
|
||||
// 检查飞书配置
|
||||
if (CONFIG.feishu.enabled) {
|
||||
if (!CONFIG.feishu.appId || !CONFIG.feishu.appSecret) {
|
||||
log('⚠️ 飞书同步已启用但应用ID或密钥未配置', 'warn');
|
||||
showToast('飞书配置不完整,请检查应用ID和密钥', 'warning');
|
||||
} else if (!CONFIG.feishu.bitableAppId || !CONFIG.feishu.tableId) {
|
||||
log('⚠️ 飞书多维表格配置不完整', 'warn');
|
||||
showToast('飞书多维表格配置不完整,请检查多维表格App ID和数据表ID', 'warning');
|
||||
} else {
|
||||
log('✅ 飞书同步配置完整', 'info');
|
||||
}
|
||||
} else {
|
||||
log('ℹ️ 飞书同步未启用', 'info');
|
||||
}
|
||||
|
||||
// 输出启动信息
|
||||
log('🎯 KOC数据采集工具启动', 'info');
|
||||
log(`📋 筛选条件:`, 'info');
|
||||
log(` - 金额门槛: ${CONFIG.pagination.amountThreshold}元`, 'info');
|
||||
log(` - 粉丝数上限: <${CONFIG.filters.maxFollowers.toLocaleString()}`, 'info');
|
||||
log(` - 视频带货占比: ≥${(CONFIG.filters.videoRatioThreshold * 100).toFixed(1)}%`, 'info');
|
||||
|
||||
// 等待页面加载完成
|
||||
await waitForPageLoad();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user