feat: 添加飞书同步功能,增强数据采集工具
在产品历史KOC助手中新增飞书同步功能,允许用户将采集到的数据同步到飞书多维表格。此更新包括飞书应用ID、密钥及表格ID的配置选项,支持对已存在记录的检查与批量上传,提升了数据管理的灵活性和效率。同时,优化了数据筛选条件,确保采集的数据符合用户需求。
This commit is contained in:
parent
805d3037e6
commit
ce078e9da6
@ -14,12 +14,13 @@
|
|||||||
// @grant GM_setClipboard
|
// @grant GM_setClipboard
|
||||||
// @connect chanmama.com
|
// @connect chanmama.com
|
||||||
// @connect api-service.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
|
// @license MIT
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
/* ==UserConfig==
|
/* ==UserConfig==
|
||||||
settings:
|
basic:
|
||||||
enabled:
|
enabled:
|
||||||
title: 启用功能
|
title: 启用功能
|
||||||
description: 是否启用蝉妈妈产品历史KOC助手功能
|
description: 是否启用蝉妈妈产品历史KOC助手功能
|
||||||
@ -38,23 +39,94 @@ settings:
|
|||||||
description: 启用详细日志输出
|
description: 启用详细日志输出
|
||||||
type: checkbox
|
type: checkbox
|
||||||
default: false
|
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== */
|
==/UserConfig== */
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
// 检测依赖库加载状态
|
||||||
|
function checkLibraryStatus() {
|
||||||
|
if (typeof Swal !== 'undefined') {
|
||||||
|
log('SweetAlert2 库加载成功');
|
||||||
|
} else {
|
||||||
|
log('SweetAlert2 库未加载,将使用备用方案', 'warn');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 配置常量
|
// 配置常量
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
debug: true, // 强制开启调试模式
|
debug: GM_getValue('basic.debug', false),
|
||||||
enabled: GM_getValue('settings.enabled', true),
|
enabled: GM_getValue('basic.enabled', true),
|
||||||
timeout: GM_getValue('settings.timeout', 30) * 1000,
|
timeout: GM_getValue('basic.timeout', 30) * 1000,
|
||||||
api: {
|
api: {
|
||||||
baseUrl: 'https://api-service.chanmama.com',
|
baseUrl: 'https://api-service.chanmama.com',
|
||||||
endpoint: '/v1/product/author/analysis'
|
endpoint: '/v1/product/author/analysis'
|
||||||
},
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
pageSize: 30,
|
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,
|
timeout: CONFIG.timeout,
|
||||||
anonymous: false,
|
anonymous: false,
|
||||||
onload: (response) => {
|
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) {
|
if (response.status >= 200 && response.status < 300) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(response.responseText);
|
const data = JSON.parse(response.responseText);
|
||||||
@ -96,7 +175,16 @@ settings:
|
|||||||
resolve(response.responseText);
|
resolve(response.responseText);
|
||||||
}
|
}
|
||||||
} else {
|
} 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('网络请求失败')),
|
onerror: () => reject(new Error('网络请求失败')),
|
||||||
@ -105,68 +193,307 @@ 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) {
|
function showInfo(title, text) {
|
||||||
Swal.fire({
|
if (typeof Swal !== 'undefined') {
|
||||||
title: title,
|
Swal.fire({
|
||||||
text: text,
|
title: title,
|
||||||
icon: 'info',
|
text: text,
|
||||||
confirmButtonText: '确定'
|
icon: 'info',
|
||||||
});
|
confirmButtonText: '确定'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert(`${title}\n${text}`);
|
||||||
|
log(`Info: ${title} - ${text}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showSuccess(title, text) {
|
function showSuccess(title, text) {
|
||||||
Swal.fire({
|
if (typeof Swal !== 'undefined') {
|
||||||
title: title,
|
Swal.fire({
|
||||||
text: text,
|
title: title,
|
||||||
icon: 'success',
|
text: text,
|
||||||
timer: 3000,
|
icon: 'success',
|
||||||
showConfirmButton: false
|
timer: 3000,
|
||||||
});
|
showConfirmButton: false
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showToast(`${title}: ${text}`, 'success');
|
||||||
|
log(`Success: ${title} - ${text}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(title, text) {
|
function showError(title, text) {
|
||||||
Swal.fire({
|
if (typeof Swal !== 'undefined') {
|
||||||
title: title,
|
Swal.fire({
|
||||||
text: text,
|
title: title,
|
||||||
icon: 'error',
|
text: text,
|
||||||
confirmButtonText: '确定'
|
icon: 'error',
|
||||||
});
|
confirmButtonText: '确定'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert(`错误: ${title}\n${text}`);
|
||||||
|
log(`Error: ${title} - ${text}`, 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showConfirm(title, text) {
|
async function showConfirm(title, text) {
|
||||||
const result = await Swal.fire({
|
if (typeof Swal !== 'undefined') {
|
||||||
title: title,
|
const result = await Swal.fire({
|
||||||
text: text,
|
title: title,
|
||||||
icon: 'question',
|
text: text,
|
||||||
showCancelButton: true,
|
icon: 'question',
|
||||||
confirmButtonText: '确定',
|
showCancelButton: true,
|
||||||
cancelButtonText: '取消'
|
confirmButtonText: '确定',
|
||||||
});
|
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;
|
return result.isConfirmed;
|
||||||
} catch (error) {
|
} else {
|
||||||
log(`获取KOC历史数据失败: ${error.message}`, 'error');
|
return confirm(`${title}\n${text}`);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,6 +568,12 @@ settings:
|
|||||||
baseInfo.预估关联商品视频销售金额 = totalVideoAmount;
|
baseInfo.预估关联商品视频销售金额 = totalVideoAmount;
|
||||||
baseInfo.关联商品蝉妈妈ID = Array.from(videoProductIds).join(',');
|
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);
|
results.push(baseInfo);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -270,6 +603,14 @@ settings:
|
|||||||
<div class="koc-status-indicator"></div>
|
<div class="koc-status-indicator"></div>
|
||||||
<span class="koc-status-text">准备采集...</span>
|
<span class="koc-status-text">准备采集...</span>
|
||||||
</div>
|
</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">
|
<div class="koc-actions">
|
||||||
<button class="koc-download-btn" type="button">
|
<button class="koc-download-btn" type="button">
|
||||||
<svg class="koc-btn-icon" viewBox="0 0 24 24" fill="currentColor">
|
<svg class="koc-btn-icon" viewBox="0 0 24 24" fill="currentColor">
|
||||||
@ -456,6 +797,30 @@ settings:
|
|||||||
font-weight: 500;
|
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 {
|
.koc-floating-window.dragging {
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||||
@ -577,7 +942,7 @@ settings:
|
|||||||
function extractProductIdFromUrl() {
|
function extractProductIdFromUrl() {
|
||||||
const url = window.location.href;
|
const url = window.location.href;
|
||||||
const match = url.match(/promotionRank\/([^\.]+)\.html/);
|
const match = url.match(/promotionRank\/([^\.]+)\.html/);
|
||||||
return match ? match[1] : "MUJ0VtooAuIRKA33vMWQN2QUo4A016WL";
|
return match ? match[1] : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算时间范围(近365天,T-1结束)
|
// 计算时间范围(近365天,T-1结束)
|
||||||
@ -644,19 +1009,45 @@ settings:
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查金额门槛 - 使用预估关联商品视频销售金额的最小值
|
// 应用筛选条件
|
||||||
const amountValues = parsedData.map(author => author.预估关联商品视频销售金额 || 0);
|
const filteredData = parsedData.filter(author => {
|
||||||
const minAmount = Math.min(...amountValues);
|
// 检查金额门槛
|
||||||
log(`第 ${currentPage} 页最小金额: ${minAmount}`, 'debug');
|
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');
|
log(`达到金额门槛 ${CONFIG.pagination.amountThreshold},停止采集`, 'info');
|
||||||
// 过滤掉低于门槛的数据
|
|
||||||
const filteredData = parsedData.filter(author => author.预估关联商品视频销售金额 >= CONFIG.pagination.amountThreshold);
|
|
||||||
allData = allData.concat(filteredData);
|
allData = allData.concat(filteredData);
|
||||||
shouldContinue = false;
|
shouldContinue = false;
|
||||||
} else {
|
} else {
|
||||||
allData = allData.concat(parsedData);
|
allData = allData.concat(filteredData);
|
||||||
currentPage++;
|
currentPage++;
|
||||||
|
|
||||||
// 防止无限循环,最多采集100页
|
// 防止无限循环,最多采集100页
|
||||||
@ -676,15 +1067,98 @@ settings:
|
|||||||
log(`=== 数据采集完成 ===`, 'info');
|
log(`=== 数据采集完成 ===`, 'info');
|
||||||
log(`总共采集 ${allData.length} 条符合条件的达人数据`, 'info');
|
log(`总共采集 ${allData.length} 条符合条件的达人数据`, 'info');
|
||||||
|
|
||||||
|
// 显示采集结果的toast提示
|
||||||
|
showToast(`📊 采集完成:找到 ${allData.length} 个满足条件的达人`, 'success');
|
||||||
|
|
||||||
// 打印完整数据到日志
|
// 打印完整数据到日志
|
||||||
log('📊 KOC数据采集结果:', 'info');
|
log('📊 KOC数据采集结果:', 'info');
|
||||||
log(`📈 共采集 ${allData.length} 条达人数据`, 'info');
|
log(`📈 共采集 ${allData.length} 条达人数据`, 'info');
|
||||||
log('📋 数据详情:', 'info');
|
log('📋 数据详情:', 'info');
|
||||||
log(JSON.stringify(allData, null, 2), 'info');
|
log(JSON.stringify(allData, null, 2), 'info');
|
||||||
|
|
||||||
// 显示成功提示
|
// 飞书同步处理
|
||||||
setStatus(floatingWindow, 'success', `已采集 ${allData.length} 条数据`);
|
let feishuResult = 'Success';
|
||||||
showSuccess('采集成功', `已采集 ${allData.length} 条符合条件的达人数据,请查看控制台`);
|
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秒后恢复按钮状态
|
// 5秒后恢复按钮状态
|
||||||
setTimeout(() => {
|
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() {
|
async function init() {
|
||||||
if (!CONFIG.enabled) {
|
if (!CONFIG.enabled) {
|
||||||
@ -729,10 +1187,30 @@ settings:
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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('🎯 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();
|
await waitForPageLoad();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user