|
|
@@ -1,6 +1,6 @@
|
|
|
import fs from 'node:fs/promises';
|
|
|
import path from 'node:path';
|
|
|
-import { chromium, type Browser } from 'playwright';
|
|
|
+import { chromium, type Browser, type Page, type BrowserContext } from 'playwright';
|
|
|
import * as XLSXNS from 'xlsx';
|
|
|
import { AppDataSource, PlatformAccount } from '../models/index.js';
|
|
|
import { BrowserManager } from '../automation/browser.js';
|
|
|
@@ -447,6 +447,14 @@ export class BaijiahaoContentOverviewImportService {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 统一入口:定时任务与添加账号均调用此方法,执行“内容分析-基础数据-近30天 + 粉丝 getFansBasicInfo”
|
|
|
+ */
|
|
|
+ static async runDailyImport(): Promise<void> {
|
|
|
+ const svc = new BaijiahaoContentOverviewImportService();
|
|
|
+ await svc.runDailyImportForAllBaijiahaoAccounts();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 为所有百家号账号导出“数据中心-内容分析-基础数据-近30天”并导入 user_day_statistics
|
|
|
*/
|
|
|
async runDailyImportForAllBaijiahaoAccounts(): Promise<void> {
|
|
|
@@ -644,6 +652,16 @@ export class BaijiahaoContentOverviewImportService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 粉丝数据:直接请求 getFansBasicInfo(近30天:昨天为结束,往前推30天),不打开页面、不等待点击
|
|
|
+ try {
|
|
|
+ await this.importFansDataByApi(context, account);
|
|
|
+ } catch (e) {
|
|
|
+ logger.warn(
|
|
|
+ `[BJ Import] Fans data import failed (non-fatal). accountId=${account.id}`,
|
|
|
+ e instanceof Error ? e.message : e
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
await context.close();
|
|
|
} finally {
|
|
|
if (shouldClose) {
|
|
|
@@ -651,5 +669,115 @@ export class BaijiahaoContentOverviewImportService {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 粉丝数据:直接请求 getFansBasicInfo(近30天 = 中国时区昨天为结束,往前推 30 天),不打开页面
|
|
|
+ * sum_fans_count → fans_count,new_fans_count → fans_increase
|
|
|
+ * 使用中国时区计算日期,避免服务器非东八区时只拿到部分天数
|
|
|
+ */
|
|
|
+ private async importFansDataByApi(context: BrowserContext, account: PlatformAccount): Promise<void> {
|
|
|
+ const chinaTz = 'Asia/Shanghai';
|
|
|
+ const toChinaYMD = (date: Date): { y: number; m: number; d: number } => {
|
|
|
+ const formatter = new Intl.DateTimeFormat('en-CA', { timeZone: chinaTz, year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
|
+ const parts = formatter.formatToParts(date);
|
|
|
+ const get = (type: string) => parts.find((p) => p.type === type)?.value ?? '0';
|
|
|
+ return { y: parseInt(get('year'), 10), m: parseInt(get('month'), 10), d: parseInt(get('day'), 10) };
|
|
|
+ };
|
|
|
+ const now = new Date();
|
|
|
+ const today = toChinaYMD(now);
|
|
|
+ const yesterdayDate = new Date(Date.UTC(today.y, today.m - 1, today.d, 0, 0, 0, 0) - 24 * 60 * 60 * 1000);
|
|
|
+ const startDate = new Date(yesterdayDate.getTime() - 29 * 24 * 60 * 60 * 1000);
|
|
|
+ const endYMD = toChinaYMD(yesterdayDate);
|
|
|
+ const startYMD = toChinaYMD(startDate);
|
|
|
+ const pad = (n: number) => String(n).padStart(2, '0');
|
|
|
+ const startStr = `${startYMD.y}${pad(startYMD.m)}${pad(startYMD.d)}`;
|
|
|
+ const endStr = `${endYMD.y}${pad(endYMD.m)}${pad(endYMD.d)}`;
|
|
|
+ const apiUrl = `https://baijiahao.baidu.com/author/eco/statistics/getFansBasicInfo?start=${startStr}&end=${endStr}&fans_type=new%2Csum&sort=asc&is_page=0&show_type=chart`;
|
|
|
+
|
|
|
+ logger.info(`[BJ Import] getFansBasicInfo range (China). accountId=${account.id} start=${startStr} end=${endStr}`);
|
|
|
+
|
|
|
+ let body: Record<string, unknown> | null = null;
|
|
|
+ try {
|
|
|
+ const res = await (context as any).request.get(apiUrl, {
|
|
|
+ headers: { Referer: 'https://baijiahao.baidu.com/builder/rc/analysisfans/basedata' },
|
|
|
+ });
|
|
|
+ if (res.ok()) body = await res.json().catch(() => null);
|
|
|
+ } catch (e) {
|
|
|
+ logger.warn(`[BJ Import] getFansBasicInfo request failed. accountId=${account.id}`, e);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!body || typeof body !== 'object') {
|
|
|
+ logger.warn(`[BJ Import] getFansBasicInfo response not valid JSON, skip. accountId=${account.id}`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const errno = (body as any).errno;
|
|
|
+ if (errno !== 0 && errno !== undefined) {
|
|
|
+ logger.warn(`[BJ Import] getFansBasicInfo errno=${errno}, skip. accountId=${account.id}`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const list = this.parseGetFansBasicInfoResponse(body);
|
|
|
+ if (!list.length) {
|
|
|
+ logger.info(`[BJ Import] No fans data from getFansBasicInfo. accountId=${account.id}`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const firstDay = list[0]?.recordDate;
|
|
|
+ const lastDay = list[list.length - 1]?.recordDate;
|
|
|
+ const fmtDay = (d: Date) => (d ? `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` : '');
|
|
|
+ logger.info(`[BJ Import] getFansBasicInfo response. accountId=${account.id} count=${list.length} first=${fmtDay(firstDay)} last=${fmtDay(lastDay)}`);
|
|
|
+
|
|
|
+ let updated = 0;
|
|
|
+ for (const { recordDate, fansCount, fansIncrease } of list) {
|
|
|
+ const r = await this.userDayStatisticsService.saveStatisticsForDate(account.id, recordDate, {
|
|
|
+ fansCount,
|
|
|
+ fansIncrease,
|
|
|
+ });
|
|
|
+ updated += r.inserted + r.updated;
|
|
|
+ }
|
|
|
+ logger.info(`[BJ Import] Fans data imported. accountId=${account.id} days=${list.length} updated=${updated}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析 getFansBasicInfo 接口返回,提取 (recordDate, fansCount, fansIncrease) 列表
|
|
|
+ * sum_fans_count → fans_count,new_fans_count → fans_increase;"--" 或无效值跳过或按 0 处理
|
|
|
+ */
|
|
|
+ private parseGetFansBasicInfoResponse(
|
|
|
+ body: Record<string, unknown>
|
|
|
+ ): Array<{ recordDate: Date; fansCount: number; fansIncrease: number }> {
|
|
|
+ const list: Array<{ recordDate: Date; fansCount: number; fansIncrease: number }> = [];
|
|
|
+ const data = body.data as Record<string, unknown> | undefined;
|
|
|
+ if (!data || typeof data !== 'object') return list;
|
|
|
+
|
|
|
+ const arr = data.list as unknown[] | undefined;
|
|
|
+ if (!Array.isArray(arr)) return list;
|
|
|
+
|
|
|
+ for (const item of arr) {
|
|
|
+ if (!item || typeof item !== 'object') continue;
|
|
|
+ const o = item as Record<string, unknown>;
|
|
|
+ const dayRaw = o.day;
|
|
|
+ if (dayRaw == null) continue;
|
|
|
+ const dayStr = String(dayRaw).trim();
|
|
|
+ if (!/^\d{8}$/.test(dayStr)) continue;
|
|
|
+ const d = normalizeDateText(dayStr);
|
|
|
+ if (!d) continue;
|
|
|
+
|
|
|
+ const sumRaw = o.sum_fans_count;
|
|
|
+ const newRaw = o.new_fans_count;
|
|
|
+ const toNum = (v: unknown): number => {
|
|
|
+ if (v === null || v === undefined) return 0;
|
|
|
+ if (typeof v === 'number' && Number.isFinite(v)) return Math.max(0, Math.round(v));
|
|
|
+ const s = String(v).trim();
|
|
|
+ if (s === '' || s === '--') return 0;
|
|
|
+ const n = Number(s.replace(/,/g, ''));
|
|
|
+ return Number.isFinite(n) ? Math.max(0, Math.round(n)) : 0;
|
|
|
+ };
|
|
|
+ const fansCount = toNum(sumRaw);
|
|
|
+ const fansIncrease = toNum(newRaw);
|
|
|
+
|
|
|
+ list.push({ recordDate: d, fansCount, fansIncrease });
|
|
|
+ }
|
|
|
+ return list;
|
|
|
+ }
|
|
|
}
|
|
|
|