feat: 优化人群画像批量下载功能,支持多种格式导出
对人群画像批量下载功能进行了优化,新增支持导出为多种格式(如JSON、Excel等),提升用户体验。同时,改进了数据请求与处理逻辑,确保下载过程更加高效稳定。
This commit is contained in:
parent
03d2c74dea
commit
c5fe080aa6
480
.cursor/rules/scriptcat.mdc
Normal file
480
.cursor/rules/scriptcat.mdc
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs: .js
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# 浏览器脚本开发助手
|
||||||
|
|
||||||
|
## 角色和专长
|
||||||
|
你是一个专业的浏览器脚本开发专家,精通 Tampermonkey 和 ScriptCat 扩展的脚本开发。你的代码简洁、高效,严格遵循浏览器脚本开发的最佳实践。
|
||||||
|
|
||||||
|
## 编码标准
|
||||||
|
|
||||||
|
### 通用原则
|
||||||
|
- 编写简洁、可读的 JavaScript 代码
|
||||||
|
- 使用现代 ES6+ 语法特性
|
||||||
|
- 所有函数都要有适当的错误处理
|
||||||
|
- 使用有意义的变量和函数命名
|
||||||
|
- 添加必要的注释说明复杂逻辑
|
||||||
|
|
||||||
|
### 浏览器脚本最佳实践
|
||||||
|
- 始终使用 IIFE 包装脚本代码:`(function() { 'use strict'; ... })()`
|
||||||
|
- 优先使用 GM_* API 而不是原生 Web API
|
||||||
|
- 所有网络请求使用 `GM_xmlhttpRequest` 并添加 `@connect` 声明
|
||||||
|
- 使用 `GM_log` 进行日志记录,支持不同日志级别
|
||||||
|
- 合理使用 `GM_setValue/GM_getValue` 进行数据持久化
|
||||||
|
|
||||||
|
### 命名规范
|
||||||
|
- **变量和函数**: camelCase
|
||||||
|
- **常量**: UPPER_SNAKE_CASE
|
||||||
|
- **配置对象**: CONFIG
|
||||||
|
- **日志函数**: log
|
||||||
|
- **通知函数**: showNotification
|
||||||
|
|
||||||
|
### 错误处理
|
||||||
|
- 所有异步操作都要用 try-catch 包装
|
||||||
|
- 使用 `GM_log` 记录错误,级别设为 'error'
|
||||||
|
- 重要错误要通过 `GM_notification` 通知用户
|
||||||
|
- 网络请求要处理超时和网络错误
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
按功能清晰分离代码:
|
||||||
|
- **配置部分**: 使用 CONFIG 对象存储所有配置
|
||||||
|
- **工具函数**: log, showNotification, makeRequest 等通用函数
|
||||||
|
- **主逻辑**: init 和具体业务逻辑函数
|
||||||
|
- **启动逻辑**: 检查页面加载状态后启动
|
||||||
|
|
||||||
|
## 依赖管理
|
||||||
|
- 弹窗使用 SweetAlert2:`@require https://cdn.jsdelivr.net/npm/sweetalert2@11`
|
||||||
|
- 避免使用可能冲突的第三方库
|
||||||
|
- 所有外部资源使用 HTTPS 链接
|
||||||
|
|
||||||
|
## 元数据标准
|
||||||
|
- 使用语义化版本号 (MAJOR.MINOR.PATCH)
|
||||||
|
- `@match` 精确匹配目标网站
|
||||||
|
- `@grant` 明确声明所需权限
|
||||||
|
- `@connect` 声明所有网络请求的域名
|
||||||
|
- 添加 `@license` 声明许可证
|
||||||
|
|
||||||
|
## 用户配置规范
|
||||||
|
使用 `==UserConfig==` 块定义用户配置:
|
||||||
|
```yaml
|
||||||
|
settings:
|
||||||
|
enabled:
|
||||||
|
title: 启用功能
|
||||||
|
description: 是否启用此功能
|
||||||
|
type: checkbox
|
||||||
|
default: true
|
||||||
|
timeout:
|
||||||
|
title: 超时时间
|
||||||
|
description: 网络请求超时时间
|
||||||
|
type: number
|
||||||
|
default: 10
|
||||||
|
min: 5
|
||||||
|
max: 60
|
||||||
|
unit: 秒
|
||||||
|
theme:
|
||||||
|
title: 主题选择
|
||||||
|
description: 选择界面主题
|
||||||
|
type: select
|
||||||
|
default: "dark"
|
||||||
|
values: ["light", "dark", "auto"]
|
||||||
|
customText:
|
||||||
|
title: 自定义文本
|
||||||
|
description: 输入自定义文本内容
|
||||||
|
type: text
|
||||||
|
default: ""
|
||||||
|
max: 100
|
||||||
|
longText:
|
||||||
|
title: 长文本配置
|
||||||
|
description: 输入长文本内容
|
||||||
|
type: textarea
|
||||||
|
default: ""
|
||||||
|
multiSelect:
|
||||||
|
title: 多选配置
|
||||||
|
description: 选择多个选项
|
||||||
|
type: mult-select
|
||||||
|
default: [1]
|
||||||
|
values: [1,2,3,4,5]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后台脚本规范
|
||||||
|
- 使用 `@background true` 声明后台脚本
|
||||||
|
- 使用 `@crontab` 声明定时脚本,支持 cron 表达式和 once 语义
|
||||||
|
- 后台脚本无法操作 DOM,只能使用 GM_* API
|
||||||
|
- 添加运行状态检查,避免重复执行
|
||||||
|
|
||||||
|
### Crontab 表达式示例
|
||||||
|
```javascript
|
||||||
|
// @crontab * * * * * * // 每秒运行一次
|
||||||
|
// @crontab * * * * * // 每分钟运行一次
|
||||||
|
// @crontab 0 */6 * * * // 每6小时的0分时执行一次
|
||||||
|
// @crontab * once * * * // 每小时运行一次
|
||||||
|
// @crontab * * once * * // 每天运行一次
|
||||||
|
// @crontab * 10 once * * // 每天10点-10:59中运行一次
|
||||||
|
```
|
||||||
|
|
||||||
|
## GM API 使用规范
|
||||||
|
|
||||||
|
### 基本 API
|
||||||
|
- `GM_info`: 获取脚本信息
|
||||||
|
- `GM_setValue/GM_getValue/GM_deleteValue`: 数据存储
|
||||||
|
- `GM_listValues`: 列出所有存储的键
|
||||||
|
- `GM_log(message, level)`: 日志记录,支持 debug/info/warn/error 级别
|
||||||
|
|
||||||
|
### 网络请求
|
||||||
|
```javascript
|
||||||
|
GM_xmlhttpRequest({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'https://api.example.com/data',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
timeout: 10000,
|
||||||
|
anonymous: true,
|
||||||
|
cookie: 'custom=value',
|
||||||
|
onload: (response) => {},
|
||||||
|
onerror: (error) => {},
|
||||||
|
ontimeout: () => {}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通知系统
|
||||||
|
```javascript
|
||||||
|
GM_notification({
|
||||||
|
title: '通知标题',
|
||||||
|
text: '通知内容',
|
||||||
|
image: 'https://example.com/icon.png',
|
||||||
|
timeout: 5000,
|
||||||
|
progress: 50, // 进度条
|
||||||
|
buttons: [ // 按钮(Firefox不支持)
|
||||||
|
{ title: '确定' },
|
||||||
|
{ title: '取消' }
|
||||||
|
],
|
||||||
|
onclick: (id, index) => {},
|
||||||
|
ondone: (clicked, id) => {}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cookie 操作
|
||||||
|
```javascript
|
||||||
|
GM_cookie('list', { name: 'cookieName' }, (cookies, error) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('Cookie操作失败:', error);
|
||||||
|
} else {
|
||||||
|
console.log('获取到的cookies:', cookies);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 其他 API
|
||||||
|
- `GM_openInTab(url, options)`: 打开新标签页
|
||||||
|
- `GM_setClipboard(data, info)`: 设置剪贴板
|
||||||
|
- `GM_addStyle(css)`: 添加样式
|
||||||
|
- `GM_registerMenuCommand/GM_unregisterMenuCommand`: 菜单命令
|
||||||
|
- `GM_getResourceText/GM_getResourceURL`: 获取资源
|
||||||
|
- `GM_download(details)`: 下载文件
|
||||||
|
|
||||||
|
## SweetAlert2 使用规范
|
||||||
|
|
||||||
|
### 基本弹窗函数
|
||||||
|
```javascript
|
||||||
|
// 信息提示
|
||||||
|
function showInfo(title, text) {
|
||||||
|
Swal.fire({
|
||||||
|
title: title,
|
||||||
|
text: text,
|
||||||
|
icon: 'info',
|
||||||
|
confirmButtonText: '确定'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功提示
|
||||||
|
function showSuccess(title, text) {
|
||||||
|
Swal.fire({
|
||||||
|
title: title,
|
||||||
|
text: text,
|
||||||
|
icon: 'success',
|
||||||
|
timer: 3000,
|
||||||
|
showConfirmButton: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误提示
|
||||||
|
function showError(title, text) {
|
||||||
|
Swal.fire({
|
||||||
|
title: title,
|
||||||
|
text: text,
|
||||||
|
icon: 'error',
|
||||||
|
confirmButtonText: '确定'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认对话框
|
||||||
|
async function showConfirm(title, text) {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: title,
|
||||||
|
text: text,
|
||||||
|
icon: 'question',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消'
|
||||||
|
});
|
||||||
|
return result.isConfirmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 输入对话框
|
||||||
|
async function showInput(title, placeholder) {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: title,
|
||||||
|
input: 'text',
|
||||||
|
inputPlaceholder: placeholder,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
inputValidator: (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return '请输入内容!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 代码模板
|
||||||
|
|
||||||
|
### 标准脚本模板
|
||||||
|
```javascript
|
||||||
|
// ==UserScript==
|
||||||
|
// @name 脚本名称
|
||||||
|
// @namespace https://github.com/username/
|
||||||
|
// @version 1.0.0
|
||||||
|
// @description 脚本功能描述
|
||||||
|
// @author 作者名
|
||||||
|
// @match *://*.example.com/*
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// @grant GM_notification
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_log
|
||||||
|
// @connect api.example.com
|
||||||
|
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
|
||||||
|
// @license MIT
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/* ==UserConfig==
|
||||||
|
settings:
|
||||||
|
enabled:
|
||||||
|
title: 启用功能
|
||||||
|
description: 是否启用此脚本功能
|
||||||
|
type: checkbox
|
||||||
|
default: true
|
||||||
|
timeout:
|
||||||
|
title: 超时时间
|
||||||
|
description: 网络请求超时时间
|
||||||
|
type: number
|
||||||
|
default: 10
|
||||||
|
min: 5
|
||||||
|
max: 60
|
||||||
|
unit: 秒
|
||||||
|
==/UserConfig== */
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 配置常量
|
||||||
|
const CONFIG = {
|
||||||
|
debug: GM_getValue('settings.debug', false),
|
||||||
|
enabled: GM_getValue('settings.enabled', true),
|
||||||
|
timeout: GM_getValue('settings.timeout', 10) * 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
function log(message, level = 'info') {
|
||||||
|
GM_log(`[${GM_info.script.name}] ${message}`, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(title, text, type = 'info') {
|
||||||
|
GM_notification({
|
||||||
|
title: title,
|
||||||
|
text: text,
|
||||||
|
timeout: 5000,
|
||||||
|
onclick: () => log('通知被点击')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeRequest(url, options = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
GM_xmlhttpRequest({
|
||||||
|
method: options.method || 'GET',
|
||||||
|
url: url,
|
||||||
|
headers: options.headers || {},
|
||||||
|
data: options.data,
|
||||||
|
timeout: CONFIG.timeout,
|
||||||
|
anonymous: true,
|
||||||
|
onload: (response) => {
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
resolve(response);
|
||||||
|
} else {
|
||||||
|
reject(new Error(`HTTP ${response.status}: ${response.statusText}`));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror: () => reject(new Error('网络请求失败')),
|
||||||
|
ontimeout: () => reject(new Error('请求超时'))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主逻辑
|
||||||
|
async function init() {
|
||||||
|
if (!CONFIG.enabled) {
|
||||||
|
log('脚本已禁用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
log('脚本初始化开始');
|
||||||
|
|
||||||
|
// 主要功能代码
|
||||||
|
await mainLogic();
|
||||||
|
|
||||||
|
log('脚本初始化完成');
|
||||||
|
} catch (error) {
|
||||||
|
log(`初始化失败: ${error.message}`, 'error');
|
||||||
|
showNotification('脚本错误', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mainLogic() {
|
||||||
|
// 具体业务逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动逻辑
|
||||||
|
if (document.readyState === 'complete') {
|
||||||
|
init();
|
||||||
|
} else {
|
||||||
|
window.addEventListener('load', init);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 后台脚本模板
|
||||||
|
```javascript
|
||||||
|
// ==UserScript==
|
||||||
|
// @name 后台脚本名称
|
||||||
|
// @background true
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// @grant GM_notification
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_log
|
||||||
|
// @connect api.example.com
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
let isRunning = false;
|
||||||
|
|
||||||
|
async function backgroundTask() {
|
||||||
|
if (isRunning) {
|
||||||
|
GM_log('后台任务正在运行中,跳过本次执行', 'warn');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
GM_log('后台任务开始执行', 'info');
|
||||||
|
|
||||||
|
// 执行后台任务逻辑
|
||||||
|
await performBackgroundWork();
|
||||||
|
|
||||||
|
GM_log('后台任务执行完成', 'info');
|
||||||
|
} catch (error) {
|
||||||
|
GM_log(`后台任务执行失败: ${error.message}`, 'error');
|
||||||
|
GM_notification({
|
||||||
|
title: '后台任务错误',
|
||||||
|
text: error.message,
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
isRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performBackgroundWork() {
|
||||||
|
// 具体的后台工作逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动后台任务
|
||||||
|
backgroundTask();
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
### 定时脚本模板
|
||||||
|
```javascript
|
||||||
|
// ==UserScript==
|
||||||
|
// @name 定时脚本名称
|
||||||
|
// @crontab */10 * * * *
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// @grant GM_notification
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_log
|
||||||
|
// @connect api.example.com
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
async function cronTask() {
|
||||||
|
try {
|
||||||
|
GM_log('定时任务开始执行', 'info');
|
||||||
|
|
||||||
|
const result = await performScheduledTask();
|
||||||
|
|
||||||
|
if (result.shouldNotify) {
|
||||||
|
GM_notification({
|
||||||
|
title: '定时任务通知',
|
||||||
|
text: result.message,
|
||||||
|
timeout: 10000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
GM_log('定时任务执行完成', 'info');
|
||||||
|
} catch (error) {
|
||||||
|
GM_log(`定时任务执行失败: ${error.message}`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function performScheduledTask() {
|
||||||
|
// 定时任务的具体逻辑
|
||||||
|
return { shouldNotify: false, message: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行定时任务
|
||||||
|
cronTask();
|
||||||
|
})();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 安全要求
|
||||||
|
- 避免使用 `eval()` 和 `new Function()`
|
||||||
|
- 对用户输入进行验证和清理
|
||||||
|
- 使用最小权限原则,只申请必要的 `@grant` 权限
|
||||||
|
- 谨慎处理来自页面的数据
|
||||||
|
- 所有网络请求都要在 `@connect` 中声明域名
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
- 使用 `MutationObserver` 监听 DOM 变化而非轮询
|
||||||
|
- 合理使用防抖(debounce)和节流(throttle)
|
||||||
|
- 避免频繁的 DOM 操作
|
||||||
|
- 缓存重复计算的结果
|
||||||
|
- 异步操作使用 Promise 和 async/await
|
||||||
|
|
||||||
|
## 调试和测试
|
||||||
|
- 使用 `GM_log` 进行调试输出,支持不同级别
|
||||||
|
- 在开发阶段启用详细日志
|
||||||
|
- 测试各种边界情况和错误场景
|
||||||
|
- 验证用户配置的有效性
|
||||||
|
|
||||||
|
- 测试网络请求的超时和错误处理
|
||||||
1072
chanmama/productHistoryKOCHelper.user.js
Normal file
1072
chanmama/productHistoryKOCHelper.user.js
Normal file
File diff suppressed because it is too large
Load Diff
7380
chanmama/res.json
Normal file
7380
chanmama/res.json
Normal file
File diff suppressed because it is too large
Load Diff
704
douyinLive/liveRoomHourData/liveRoomHourData.user.js
Normal file
704
douyinLive/liveRoomHourData/liveRoomHourData.user.js
Normal file
@ -0,0 +1,704 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name 抖音直播间数据监控(全自动)半小时
|
||||||
|
// @namespace https://bbs.tampermonkey.net.cn/
|
||||||
|
// @version 2.3
|
||||||
|
// @description 后台定时获取直播间数据,并在多维表格中显示
|
||||||
|
// @author 汪喜
|
||||||
|
// @require https://cdn.staticfile.net/sweetalert2/11.10.3/sweetalert2.all.min.js
|
||||||
|
// @match https://compass.jinritemai.com/screen/live/shop?live_room_id=*
|
||||||
|
// @match https://compass.jinritemai.com/screen/live/*
|
||||||
|
// @match https://compass.jinritemai.com/shop/live-detail?live_room_id=*
|
||||||
|
// @grant GM_xmlhttpRequest
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_getValue
|
||||||
|
// @grant GM_deleteValue
|
||||||
|
// @crontab */30 * * * *
|
||||||
|
// @connect open.feishu.cn
|
||||||
|
// @connect compass.jinritemai.com
|
||||||
|
// @connect compass.jinritemai.com
|
||||||
|
// @connect qianchuan.jinritemai.com
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
/*
|
||||||
|
更新日志:
|
||||||
|
1. 支持多个直播间
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
rooms: [
|
||||||
|
{
|
||||||
|
name: "夸迪官方旗舰店",
|
||||||
|
aadvid: "1794468035923978",
|
||||||
|
type: "official",
|
||||||
|
},/*
|
||||||
|
{
|
||||||
|
name: "夸迪官方旗舰店直播间",
|
||||||
|
aadvid: "1798388578394176",
|
||||||
|
type: "shop",
|
||||||
|
},*/
|
||||||
|
],
|
||||||
|
|
||||||
|
id: "cli_a73b734d0564d01c",
|
||||||
|
secret: "xeKSIOpQiVW6oHeJPH4EvlZMZ1QmnCxx",
|
||||||
|
appId: "W4jbbkRRbaOdFCsB6D2czBovn7d",
|
||||||
|
tableId: "tblLYkzpzy8TqZtP",
|
||||||
|
logTableId: "tbld90KC73YBSu0B",
|
||||||
|
rawDataTableId: "tblbzx4nG0PbZ6US",
|
||||||
|
|
||||||
|
urls: {
|
||||||
|
tenantAccessToken:
|
||||||
|
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
|
||||||
|
bitableUpdateRecords: (appId, tableId) =>
|
||||||
|
`https://open.feishu.cn/open-apis/bitable/v1/apps/${appId}/tables/${tableId}/records`,
|
||||||
|
fetchLiveRoomId:
|
||||||
|
"https://compass.jinritemai.com/compass_api/shop/live/live_list/realtime?page_no=1&page_size=100",
|
||||||
|
/*fetchLiveRoomData: (roomId) => {
|
||||||
|
const indexSelected = encodeURIComponent("pay_ucnt,live_show_watch_cnt_ratio,avg_watch_duration,online_user_cnt,live_show_cnt,real_refund_amt");
|
||||||
|
return `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`;
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
fetchQianchuanData: (aadvid) =>
|
||||||
|
`https://qianchuan.jinritemai.com/ad/api/pmc/v1/standard/get_summary_info?aavid=${aadvid}`,
|
||||||
|
fetchCommentsData: (roomId) =>
|
||||||
|
`https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/five_min_data?room_id=${roomId}`, //使用五分钟数据来加总评论次数
|
||||||
|
fetchConvertData: (roomId) =>
|
||||||
|
`https://compass.jinritemai.com/business_api/shop/live_room/flow/gmv_interaction?live_room_id=${roomId}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let logs = [];
|
||||||
|
let liveRoomId = null;
|
||||||
|
let intervalId = null;
|
||||||
|
let timeoutId = null;
|
||||||
|
//let commentCounts = []; // 用于存储每五分钟获取到的评论次数
|
||||||
|
|
||||||
|
function log(message) {
|
||||||
|
logs.push(message);
|
||||||
|
console.log(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const retry = async (fn, retries = 3, delay = 10000) => {
|
||||||
|
for (let i = 0; i < retries; i++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (error) {
|
||||||
|
if (i < retries - 1) {
|
||||||
|
console.warn(`Retrying... (${i + 1}/${retries})`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function sendHttpRequest(
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
body = null,
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return retry(
|
||||||
|
() =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
GM_xmlhttpRequest({
|
||||||
|
method: method,
|
||||||
|
url: url,
|
||||||
|
data: body,
|
||||||
|
headers: headers,
|
||||||
|
onload: function (response) {
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
try {
|
||||||
|
const jsonResponse = JSON.parse(response.responseText);
|
||||||
|
if (jsonResponse.msg) {
|
||||||
|
log(jsonResponse.msg);
|
||||||
|
}
|
||||||
|
resolve(jsonResponse);
|
||||||
|
} catch (e) {
|
||||||
|
reject(`Failed to parse JSON: ${e}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(`Error: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onerror: function (error) {
|
||||||
|
reject(`Network Error: ${error}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchTenantAccessToken(id, secret) {
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest(
|
||||||
|
"POST",
|
||||||
|
CONFIG.urls.tenantAccessToken,
|
||||||
|
JSON.stringify({
|
||||||
|
app_id: id,
|
||||||
|
app_secret: secret,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return response.tenant_access_token;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Error fetching Tenant Access Token: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新记录
|
||||||
|
async function updateBitableRecords(accessToken, appId, tableId, items) {
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest(
|
||||||
|
"POST",
|
||||||
|
CONFIG.urls.bitableUpdateRecords(appId, tableId),
|
||||||
|
JSON.stringify(items),
|
||||||
|
{
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
log("Updated record: " + JSON.stringify(response));
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to update record in Bitable: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//获取正在直播的直播间id
|
||||||
|
async function fetchLiveRoomId(authorNickName) {
|
||||||
|
const url = CONFIG.urls.fetchLiveRoomId;
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest("GET", url);
|
||||||
|
const liveRooms = response.data.card_list;
|
||||||
|
const targetRoom = liveRooms.find(
|
||||||
|
(room) => room.author.author_nick_name === authorNickName
|
||||||
|
);
|
||||||
|
if (targetRoom) {
|
||||||
|
return targetRoom.live_room_id;
|
||||||
|
} else {
|
||||||
|
throw new Error("未找到符合条件的直播间");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log("Error fetching live room list: " + error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取直播间大屏数据
|
||||||
|
async function fetchLiveRoomData(roomId, roomType) {
|
||||||
|
try {
|
||||||
|
let url;
|
||||||
|
if (roomType === "official") {
|
||||||
|
const indexSelected = encodeURIComponent(
|
||||||
|
"pay_ucnt,live_show_watch_cnt_ratio,avg_watch_duration,online_user_cnt,live_show_cnt,real_refund_amt"
|
||||||
|
);
|
||||||
|
url = `https://compass.jinritemai.com/compass_api/content_live/shop_official/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`;
|
||||||
|
} else if (roomType === "shop") {
|
||||||
|
const indexSelected = encodeURIComponent(
|
||||||
|
"pay_ucnt,watch_cnt_show_ratio,avg_watch_duration,current_cnt,live_show_cnt,real_refund_amt"
|
||||||
|
);
|
||||||
|
url = `https://compass.jinritemai.com/compass_api/shop/live/live_screen/core_data?room_id=${roomId}&index_selected=${indexSelected}`;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unknown room type: ${roomType}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
accept: "application/json, text/plain, */*",
|
||||||
|
};
|
||||||
|
const response = await sendHttpRequest("GET", url, null, headers);
|
||||||
|
|
||||||
|
const data = response.data;
|
||||||
|
if (roomType === "official") {
|
||||||
|
return {
|
||||||
|
pay_amt: data.pay_amt.value,
|
||||||
|
pay_ucnt: data.core_data.find((d) => d.index_display === "成交人数")
|
||||||
|
.value.value,
|
||||||
|
real_refund_amt: data.core_data.find(
|
||||||
|
(d) => d.index_display === "退款金额"
|
||||||
|
).value.value,
|
||||||
|
avg_watch_duration: data.core_data.find(
|
||||||
|
(d) => d.index_display === "人均观看时长"
|
||||||
|
).value.value,
|
||||||
|
current_cnt: data.core_data.find(
|
||||||
|
(d) => d.index_display === "实时在线人数"
|
||||||
|
).value.value,
|
||||||
|
live_show_watch_cnt_ratio: data.core_data.find(
|
||||||
|
(d) => d.index_display === "曝光-观看率(次数)"
|
||||||
|
).value.value,
|
||||||
|
live_show_cnt: data.core_data.find(
|
||||||
|
(d) => d.index_display === "曝光次数"
|
||||||
|
).value.value,
|
||||||
|
};
|
||||||
|
} else if (roomType === "shop") {
|
||||||
|
// 根据实际返回的数据结构,提取所需字段
|
||||||
|
// 这里假设返回的数据结构与官方直播间类似
|
||||||
|
return {
|
||||||
|
pay_amt: data.pay_amt.value,
|
||||||
|
pay_ucnt: data.core_data.find((d) => d.index_display === "成交人数")
|
||||||
|
.value.value,
|
||||||
|
real_refund_amt: data.core_data.find(
|
||||||
|
(d) => d.index_display === "退款金额"
|
||||||
|
).value.value,
|
||||||
|
avg_watch_duration: data.core_data.find(
|
||||||
|
(d) => d.index_display === "人均观看时长"
|
||||||
|
).value.value,
|
||||||
|
current_cnt: data.core_data.find(
|
||||||
|
(d) => d.index_display === "实时在线人数"
|
||||||
|
).value.value,
|
||||||
|
live_show_watch_cnt_ratio: data.core_data.find(
|
||||||
|
(d) => d.index_display === "曝光-观看率(次数)"
|
||||||
|
).value.value,
|
||||||
|
live_show_cnt: data.core_data.find(
|
||||||
|
(d) => d.index_display === "曝光次数"
|
||||||
|
).value.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log("Error fetching live room data: " + error.message);
|
||||||
|
return {
|
||||||
|
pay_amt: 0,
|
||||||
|
pay_ucnt: 0,
|
||||||
|
real_refund_amt: 0,
|
||||||
|
avg_watch_duration: 0,
|
||||||
|
current_cnt: 0,
|
||||||
|
live_show_watch_cnt_ratio: 0,
|
||||||
|
live_show_cnt: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
//获取评论次数的数据
|
||||||
|
async function fetchCommentsData(roomId) {
|
||||||
|
try {
|
||||||
|
const url = CONFIG.urls.fetchCommentsData(roomId);
|
||||||
|
const response = await sendHttpRequest('GET', url);
|
||||||
|
const comments = response.data.card.find(d => d.index_display === '评论次数').value.value;
|
||||||
|
const updateTimeStr = response.data.update_time.replace("更新", "").trim();
|
||||||
|
const updateTime = new Date(updateTimeStr).getTime();
|
||||||
|
return { count: comments, timestamp: updateTime };
|
||||||
|
} catch (error) {
|
||||||
|
log('Error fetching interaction data: ' + error.message);
|
||||||
|
return { count: 0, timestamp: new Date().getTime() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每五分钟获取一次评论数据
|
||||||
|
async function fetchComments() {
|
||||||
|
try {
|
||||||
|
log('Fetching comments data...');
|
||||||
|
const commentsData = await fetchCommentsData(liveRoomId);
|
||||||
|
commentCounts.push(commentsData);
|
||||||
|
log(commentCounts)
|
||||||
|
log(`Fetched comments count: ${commentsData.count} at ${new Date(commentsData.timestamp).toLocaleString()}`);
|
||||||
|
} catch (error) {
|
||||||
|
log('Error fetching comments data: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化评论次数数组并设置定时器
|
||||||
|
async function initializeCommentFetching() {
|
||||||
|
if (intervalId !== null) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
commentCounts = [];
|
||||||
|
await fetchComments(); // 立即执行一次
|
||||||
|
intervalId = setInterval(fetchComments, 5 * 60 * 1000); // 每五分钟重新获取一次评论数据
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算过去一小时的评论次数
|
||||||
|
function getCommentsCountLastHour() {
|
||||||
|
log(`Current commentCounts array: ${JSON.stringify(commentCounts)}`);
|
||||||
|
const oneHourAgo = new Date().getTime() - (60 * 60 * 1000);
|
||||||
|
const filteredComments = commentCounts.filter(comment => comment.timestamp >= oneHourAgo);
|
||||||
|
log(`Filtered comments array: ${JSON.stringify(filteredComments)}`);
|
||||||
|
const totalComments = filteredComments.reduce((total, comment) => total + comment.count, 0);
|
||||||
|
log(`Total comments in the last hour: ${totalComments}`);
|
||||||
|
return totalComments;
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
//获取直播间开始时间数据
|
||||||
|
async function fetchLiveRoomStartTime(roomId) {
|
||||||
|
const url = `https://compass.jinritemai.com/compass_api/shop/live/live_screen/live_base_info?room_id=${roomId}`;
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest("GET", url);
|
||||||
|
return response.data.live_start_time;
|
||||||
|
} catch (error) {
|
||||||
|
log("Error fetching live room start time: " + error.message);
|
||||||
|
return null; // 默认返回 null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取千川消耗金额数据
|
||||||
|
async function fetchQianchuanData(aadvid) {
|
||||||
|
const now = new Date();
|
||||||
|
const formatNumber = (num) => num.toString().padStart(2, "0");
|
||||||
|
|
||||||
|
// 结束时间为动态的当前时间
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = formatNumber(now.getMonth() + 1); // 月份从0开始
|
||||||
|
const day = formatNumber(now.getDate());
|
||||||
|
const hours = formatNumber(now.getHours());
|
||||||
|
const minutes = formatNumber(now.getMinutes());
|
||||||
|
const seconds = formatNumber(now.getSeconds());
|
||||||
|
const end_time = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||||
|
|
||||||
|
let start_time;
|
||||||
|
|
||||||
|
log("Failed to fetch start time, using default start time.");
|
||||||
|
if (now.getHours() >= 6) {
|
||||||
|
// 时间超过6点,则使用当天6点作为开始时间
|
||||||
|
start_time = `${year}-${month}-${day} 06:00:00`;
|
||||||
|
} else {
|
||||||
|
// 如果当前时间在0点到6点之间,则开始时间为昨天的6点
|
||||||
|
const yesterday = new Date(now);
|
||||||
|
yesterday.setDate(now.getDate() - 1);
|
||||||
|
const yesterdayYear = yesterday.getFullYear();
|
||||||
|
const yesterdayMonth = formatNumber(yesterday.getMonth() + 1);
|
||||||
|
const yesterdayDay = formatNumber(yesterday.getDate());
|
||||||
|
start_time = `${yesterdayYear}-${yesterdayMonth}-${yesterdayDay} 06:00:00`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
DataSetKey: "home_cost_total_prom",
|
||||||
|
Dimensions: ["adlab_mode", "pricing_category", "qcpx_category"],
|
||||||
|
Metrics: ["stat_cost"],
|
||||||
|
StartTime: start_time,
|
||||||
|
EndTime: end_time,
|
||||||
|
Filters: {
|
||||||
|
Conditions: [
|
||||||
|
{
|
||||||
|
Field: "advertiser_id",
|
||||||
|
Values: [aadvid],
|
||||||
|
Operator: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "ecp_app_id",
|
||||||
|
Values: ["1", "2"],
|
||||||
|
Operator: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Field: "query_self_data",
|
||||||
|
Values: ["off"],
|
||||||
|
Operator: 7,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ConditionRelationshipType: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await sendHttpRequest(
|
||||||
|
"POST",
|
||||||
|
`https://qianchuan.jinritemai.com/ad/api/data/v1/common/statQuery?reqFrom=data-summary&aavid=${aadvid}`,
|
||||||
|
JSON.stringify(body),
|
||||||
|
{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data.StatsData.Totals.stat_cost.Value;
|
||||||
|
} catch (error) {
|
||||||
|
log("Error fetching Qianchuan data: " + error.message);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取直播间转化漏斗数据
|
||||||
|
async function fetchConvertData(roomId) {
|
||||||
|
try {
|
||||||
|
const url = CONFIG.urls.fetchConvertData(roomId);
|
||||||
|
const response = await sendHttpRequest("GET", url);
|
||||||
|
return {
|
||||||
|
exposure_ucnt: response.data.gmv_change.find(
|
||||||
|
(d) => d.index_name === "直播间曝光人数"
|
||||||
|
).value,
|
||||||
|
watch_ucnt: response.data.gmv_change.find(
|
||||||
|
(d) => d.index_name === "直播间观看人数"
|
||||||
|
).value,
|
||||||
|
product_exposure_ucnt: response.data.gmv_change.find(
|
||||||
|
(d) => d.index_name === "商品曝光人数"
|
||||||
|
).value,
|
||||||
|
product_click_ucnt: response.data.gmv_change.find(
|
||||||
|
(d) => d.index_name === "商品点击人数"
|
||||||
|
).value,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log("Error fetching flow data: " + error.message);
|
||||||
|
return {
|
||||||
|
exposure_ucnt: 0,
|
||||||
|
watch_ucnt: 0,
|
||||||
|
product_exposure_ucnt: 0,
|
||||||
|
product_click_ucnt: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储数据,每个直播间单独一个key,不覆盖之前的数据
|
||||||
|
function saveDataUsingGM(roomId, data) {
|
||||||
|
let existingData = GM_getValue(roomId, []);
|
||||||
|
if (!Array.isArray(existingData)) {
|
||||||
|
existingData = [];
|
||||||
|
}
|
||||||
|
existingData.push(data); // 将新的数据推入到现有数据数组中
|
||||||
|
GM_setValue(roomId, existingData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取数据
|
||||||
|
function getDataUsingGM(roomId) {
|
||||||
|
let data = GM_getValue(roomId, []);
|
||||||
|
return Array.isArray(data) ? data : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
//延迟函数
|
||||||
|
function delay(ms) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
//计算直播场次所在的日期
|
||||||
|
function calculateSessionDate(timestamp) {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
|
||||||
|
// 直播时间在凌晨2点前,则认为是前一天的场次
|
||||||
|
if (date.getHours() <= 2) {
|
||||||
|
date.setDate(date.getDate() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个新的日期对象,表示计算后的日期,并返回其时间戳
|
||||||
|
const sessionDate = new Date(
|
||||||
|
date.getFullYear(),
|
||||||
|
date.getMonth(),
|
||||||
|
date.getDate()
|
||||||
|
);
|
||||||
|
return sessionDate.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAndUploadLiveRoomData() {
|
||||||
|
for (const room of CONFIG.rooms) {
|
||||||
|
try {
|
||||||
|
log(`Fetching live room id for ${room.name}`);
|
||||||
|
const liveRoomId = await fetchLiveRoomId(room.name);
|
||||||
|
|
||||||
|
if (!liveRoomId) {
|
||||||
|
log(`未找到符合条件的直播间 ${room.name},跳过执行`);
|
||||||
|
continue; // 未找到符合条件的直播间,跳过执行
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Fetching live room start time for ${room.name}...`);
|
||||||
|
const start_time = await fetchLiveRoomStartTime(liveRoomId);
|
||||||
|
if (!start_time) {
|
||||||
|
Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: `无法获取直播间 ${room.name} 开始时间,请检查直播间ID`,
|
||||||
|
text: "使用默认开始时间 06:00:00",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`Fetching live room data for ${room.name}...`);
|
||||||
|
const liveRoomData = await fetchLiveRoomData(liveRoomId, room.type);
|
||||||
|
log("Live room data fetched successfully.");
|
||||||
|
|
||||||
|
await delay(2000);
|
||||||
|
|
||||||
|
log(`Fetching Qianchuan data for ${room.name}...`);
|
||||||
|
const qianchuanData = await fetchQianchuanData(room.aadvid);
|
||||||
|
log("Qianchuan data fetched successfully.");
|
||||||
|
|
||||||
|
await delay(2000);
|
||||||
|
|
||||||
|
log(`Fetching flow data for ${room.name}...`);
|
||||||
|
const convertData = await fetchConvertData(liveRoomId);
|
||||||
|
log("Flow data fetched successfully.");
|
||||||
|
|
||||||
|
const previousData = getDataUsingGM(liveRoomId);
|
||||||
|
|
||||||
|
const currentTime = new Date().toLocaleString("zh-CN", {
|
||||||
|
timeZone: "Asia/Shanghai",
|
||||||
|
});
|
||||||
|
|
||||||
|
let incrementData = {};
|
||||||
|
|
||||||
|
if (previousData.length > 0) {
|
||||||
|
const lastData = previousData[previousData.length - 1];
|
||||||
|
incrementData = {
|
||||||
|
直播间: room.name, // 添加直播间名字字段
|
||||||
|
GMV: liveRoomData.pay_amt / 100 - lastData.pay_amt,
|
||||||
|
商品成交人数: liveRoomData.pay_ucnt - lastData.pay_ucnt,
|
||||||
|
实时在线人数: liveRoomData.current_cnt,
|
||||||
|
退款金额:
|
||||||
|
liveRoomData.real_refund_amt / 100 - lastData.real_refund_amt,
|
||||||
|
千川消耗: qianchuanData - lastData.qianchuan_cost,
|
||||||
|
直播间曝光人数: convertData.exposure_ucnt - lastData.exposure_ucnt,
|
||||||
|
直播观看人数: convertData.watch_ucnt - lastData.watch_ucnt,
|
||||||
|
商品曝光人数:
|
||||||
|
convertData.product_exposure_ucnt - lastData.product_exposure_ucnt,
|
||||||
|
商品点击人数:
|
||||||
|
convertData.product_click_ucnt - lastData.product_click_ucnt,
|
||||||
|
直播间ID: liveRoomId,
|
||||||
|
时间: new Date(currentTime).getTime(),
|
||||||
|
直播间曝光次数: liveRoomData.live_show_cnt - lastData.live_show_cnt,
|
||||||
|
直播观看次数:
|
||||||
|
liveRoomData.live_show_watch_cnt_ratio *
|
||||||
|
liveRoomData.live_show_cnt -
|
||||||
|
lastData.live_show_cnt * lastData.live_show_watch_cnt_ratio,
|
||||||
|
人均停留时长:
|
||||||
|
(liveRoomData.avg_watch_duration * convertData.watch_ucnt -
|
||||||
|
lastData.avg_watch_duration * lastData.watch_ucnt) /
|
||||||
|
(convertData.watch_ucnt - lastData.watch_ucnt),
|
||||||
|
直播场次日期: calculateSessionDate(new Date(currentTime).getTime()),
|
||||||
|
差值时间: lastData.timestamp,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
incrementData = {
|
||||||
|
直播间: room.name, // 添加直播间名字字段
|
||||||
|
GMV: liveRoomData.pay_amt / 100,
|
||||||
|
商品成交人数: liveRoomData.pay_ucnt,
|
||||||
|
实时在线人数: liveRoomData.current_cnt,
|
||||||
|
退款金额: liveRoomData.real_refund_amt / 100,
|
||||||
|
千川消耗: qianchuanData,
|
||||||
|
直播间曝光人数: convertData.exposure_ucnt,
|
||||||
|
直播观看人数: convertData.watch_ucnt,
|
||||||
|
商品曝光人数: convertData.product_exposure_ucnt,
|
||||||
|
商品点击人数: convertData.product_click_ucnt,
|
||||||
|
直播间ID: liveRoomId,
|
||||||
|
时间: new Date(currentTime).getTime(),
|
||||||
|
直播间曝光次数: liveRoomData.live_show_cnt,
|
||||||
|
直播观看次数:
|
||||||
|
liveRoomData.live_show_watch_cnt_ratio * liveRoomData.live_show_cnt,
|
||||||
|
人均停留时长: liveRoomData.avg_watch_duration,
|
||||||
|
直播场次日期: calculateSessionDate(new Date(currentTime).getTime()),
|
||||||
|
差值时间: new Date(start_time).getTime(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
log("Data extracted successfully");
|
||||||
|
log(incrementData);
|
||||||
|
|
||||||
|
// 获取tenant access token
|
||||||
|
log("Fetching tenant access token...");
|
||||||
|
const accessToken = await fetchTenantAccessToken(
|
||||||
|
CONFIG.id,
|
||||||
|
CONFIG.secret
|
||||||
|
);
|
||||||
|
log("Tenant access token fetched successfully.");
|
||||||
|
|
||||||
|
// 把计算后的数据写入到多维表格
|
||||||
|
log(`Uploading live room data to bitable for ${room.name}...`);
|
||||||
|
const itemsToWrite = {
|
||||||
|
fields: incrementData,
|
||||||
|
};
|
||||||
|
await updateBitableRecords(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.tableId,
|
||||||
|
itemsToWrite
|
||||||
|
);
|
||||||
|
log("Live room data uploaded successfully.");
|
||||||
|
|
||||||
|
log(`Uploading live room data to localstorage for ${room.name}...`);
|
||||||
|
|
||||||
|
// 写入localStorage
|
||||||
|
const dataToSave = {
|
||||||
|
room_name: room.name,
|
||||||
|
pay_amt: liveRoomData.pay_amt / 100,
|
||||||
|
pay_ucnt: liveRoomData.pay_ucnt,
|
||||||
|
real_refund_amt: liveRoomData.real_refund_amt / 100,
|
||||||
|
avg_watch_duration: liveRoomData.avg_watch_duration,
|
||||||
|
current_cnt: liveRoomData.current_cnt,
|
||||||
|
qianchuan_cost: qianchuanData,
|
||||||
|
exposure_ucnt: convertData.exposure_ucnt,
|
||||||
|
watch_ucnt: convertData.watch_ucnt,
|
||||||
|
product_exposure_ucnt: convertData.product_exposure_ucnt,
|
||||||
|
product_click_ucnt: convertData.product_click_ucnt,
|
||||||
|
liveRoomId: liveRoomId,
|
||||||
|
timestamp: new Date(currentTime).getTime(),
|
||||||
|
timestring: currentTime,
|
||||||
|
live_show_watch_cnt_ratio: liveRoomData.live_show_watch_cnt_ratio,
|
||||||
|
live_show_cnt: liveRoomData.live_show_cnt,
|
||||||
|
};
|
||||||
|
log(dataToSave);
|
||||||
|
saveDataUsingGM(liveRoomId, dataToSave);
|
||||||
|
log("Writing to localstorage successfully");
|
||||||
|
|
||||||
|
// 写入rawdata多维表格 - 暂时取消此功能
|
||||||
|
/*
|
||||||
|
log(`Uploading saved data to another bitable table for ${room.name}...`);
|
||||||
|
await updateBitableRecords(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.rawDataTableId,
|
||||||
|
{
|
||||||
|
fields: dataToSave,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
log("Saved data uploaded to another bitable table successfully.");
|
||||||
|
*/
|
||||||
|
} catch (error) {
|
||||||
|
log(`Error for ${room.name}: ` + error.message);
|
||||||
|
Swal.fire({
|
||||||
|
icon: "error",
|
||||||
|
title: `直播间 ${room.name} 出错了,请联系@汪喜`,
|
||||||
|
text: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
let runResult = "成功"; // 默认值为成功
|
||||||
|
let logs = [];
|
||||||
|
|
||||||
|
const log = (message) => {
|
||||||
|
logs.push(message);
|
||||||
|
console.log(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function logRunResult() {
|
||||||
|
try {
|
||||||
|
/*
|
||||||
|
const accessToken = await fetchTenantAccessToken(
|
||||||
|
CONFIG.id,
|
||||||
|
CONFIG.secret
|
||||||
|
);
|
||||||
|
const runTime = new Date().getTime();
|
||||||
|
const logData = {
|
||||||
|
fields: {
|
||||||
|
表名: "直播间数据",
|
||||||
|
表格id: CONFIG.tableId,
|
||||||
|
最近运行时间: runTime,
|
||||||
|
运行结果: runResult,
|
||||||
|
日志: logs.join("\n"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await updateBitableRecords(
|
||||||
|
accessToken,
|
||||||
|
CONFIG.appId,
|
||||||
|
CONFIG.logTableId,
|
||||||
|
logData
|
||||||
|
);
|
||||||
|
log("Run result logged successfully.");
|
||||||
|
*/
|
||||||
|
// 暂时取消日志上传功能,仅在控制台记录
|
||||||
|
log("Run result logging disabled.");
|
||||||
|
} catch (error) {
|
||||||
|
log("Failed to log run result: " + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function () {
|
||||||
|
try {
|
||||||
|
log("Script started");
|
||||||
|
await fetchAndUploadLiveRoomData();
|
||||||
|
log("Data fetched and uploaded");
|
||||||
|
} catch (error) {
|
||||||
|
runResult = "失败";
|
||||||
|
log("Script execution failed: " + error.message);
|
||||||
|
} finally {
|
||||||
|
|
||||||
|
await logRunResult();
|
||||||
|
log("Script execution completed");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user