|
|
@@ -9,26 +9,12 @@ import DoneRateModel from "../../src/models/doneRateModel"; // 导入 DoneRateMo
|
|
|
const CLICKHOUSE_EVENTS_TABLE = "events"; // 确保与 ClickHouseService 中的表名一致
|
|
|
|
|
|
/**
|
|
|
- * ClickHouse 查询结果接口:每日每个作品的独立开始用户数
|
|
|
+ * ClickHouse 查询结果接口:每日每个作品的开始、完成和道具使用统计。
|
|
|
*/
|
|
|
-interface ClickHouseStartCountResult {
|
|
|
+interface ClickHouseDoneRateAggregateResult {
|
|
|
res: string; // 作品 ID
|
|
|
unique_starts: number; // 独立开始用户数
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * ClickHouse 查询结果接口:每日每个作品的独立完成用户数
|
|
|
- */
|
|
|
-interface ClickHouseDoneCountResult {
|
|
|
- res: string; // 作品 ID
|
|
|
unique_dones: number; // 独立完成用户数
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * ClickHouse 查询结果接口:每日每个作品的使用道具数 (对应 tipCount)
|
|
|
- */
|
|
|
-interface ClickHouseTipCountResult {
|
|
|
- res: string; // 作品 ID
|
|
|
tip_count: number; // 道具使用次数
|
|
|
}
|
|
|
|
|
|
@@ -62,85 +48,61 @@ async function run(dateStr?: string | dayjs.Dayjs): Promise<string> {
|
|
|
const yesterday = targetDay;
|
|
|
const yesterdayYYYYMMDD = yesterday.format("YYYYMMDD");
|
|
|
const yesterdayStart = yesterday.toDate();
|
|
|
- const yesterdayEnd = yesterday.endOf("day").toDate();
|
|
|
+ const nextDayStart = yesterday.add(1, "day").startOf("day").toDate();
|
|
|
|
|
|
// 格式化日期字符串,使其符合 ClickHouse 的 toDateTime() 函数要求
|
|
|
const yesterdayStartString = dayjs(yesterdayStart).format("YYYY-MM-DD HH:mm:ss");
|
|
|
- const yesterdayEndString = dayjs(yesterdayEnd).format("YYYY-MM-DD HH:mm:ss");
|
|
|
+ const nextDayStartString = dayjs(nextDayStart).format("YYYY-MM-DD HH:mm:ss");
|
|
|
|
|
|
console.log(`[DoneRate Cron] Processing data for date: ${yesterdayYYYYMMDD}`);
|
|
|
|
|
|
try {
|
|
|
// --- 1. 从 ClickHouse 中提取数据 (Start, Done, Tip Counts) ---
|
|
|
-
|
|
|
- // 1.1 查询目标日期每个作品的独立开始用户数
|
|
|
- const startCountsQuery = `
|
|
|
+ const aggregateQuery = `
|
|
|
SELECT
|
|
|
res,
|
|
|
- count(DISTINCT uid) AS unique_starts
|
|
|
+ uniqIf(uid, event = 'color_start') AS unique_starts,
|
|
|
+ uniqIf(uid, event = 'color_done') AS unique_dones,
|
|
|
+ countIf(event = 'color_tip') AS tip_count
|
|
|
FROM ${CLICKHOUSE_EVENTS_TABLE}
|
|
|
- WHERE event = 'color_start'
|
|
|
+ WHERE event IN ('color_start', 'color_done', 'color_tip')
|
|
|
AND time >= toDateTime('${yesterdayStartString}')
|
|
|
- AND time < toDateTime('${yesterdayEndString}')
|
|
|
+ AND time < toDateTime('${nextDayStartString}')
|
|
|
GROUP BY res
|
|
|
HAVING res IS NOT NULL
|
|
|
`;
|
|
|
- const startResults = await clickhouseService.queryEvents<ClickHouseStartCountResult>(startCountsQuery);
|
|
|
- const artworkStartCounts = new Map<string, number>();
|
|
|
- startResults.forEach((row) => {
|
|
|
- if (row.res && mongoose.Types.ObjectId.isValid(row.res)) {
|
|
|
- artworkStartCounts.set(row.res, row.unique_starts);
|
|
|
- } else {
|
|
|
- console.warn(`[DoneRate Cron] Invalid artwork ID found in start_counts result: ${row.res}`);
|
|
|
- }
|
|
|
- });
|
|
|
- console.log(`[DoneRate Cron] Retrieved ${startResults.length} unique start counts from ClickHouse.`);
|
|
|
+ const clickhouseQueryStartTime = Date.now();
|
|
|
+ const aggregateResults = await clickhouseService.queryEvents<ClickHouseDoneRateAggregateResult>(aggregateQuery);
|
|
|
+ const clickhouseQueryElapsedMs = Date.now() - clickhouseQueryStartTime;
|
|
|
|
|
|
- // 1.2 查询目标日期每个作品的独立完成用户数
|
|
|
- const doneCountsQuery = `
|
|
|
- SELECT
|
|
|
- res,
|
|
|
- count(DISTINCT uid) AS unique_dones
|
|
|
- FROM ${CLICKHOUSE_EVENTS_TABLE}
|
|
|
- WHERE event = 'color_done'
|
|
|
- AND time >= toDateTime('${yesterdayStartString}')
|
|
|
- AND time < toDateTime('${yesterdayEndString}')
|
|
|
- GROUP BY res
|
|
|
- HAVING res IS NOT NULL
|
|
|
- `;
|
|
|
- const doneResults = await clickhouseService.queryEvents<ClickHouseDoneCountResult>(doneCountsQuery);
|
|
|
+ const artworkStartCounts = new Map<string, number>();
|
|
|
const artworkDoneCounts = new Map<string, number>();
|
|
|
- doneResults.forEach((row) => {
|
|
|
+ const artworkTipCounts = new Map<string, number>();
|
|
|
+ let invalidArtworkRowCount = 0;
|
|
|
+
|
|
|
+ aggregateResults.forEach((row) => {
|
|
|
if (row.res && mongoose.Types.ObjectId.isValid(row.res)) {
|
|
|
- artworkDoneCounts.set(row.res, row.unique_dones);
|
|
|
+ artworkStartCounts.set(row.res, Number(row.unique_starts));
|
|
|
+ artworkDoneCounts.set(row.res, Number(row.unique_dones));
|
|
|
+ artworkTipCounts.set(row.res, Number(row.tip_count));
|
|
|
} else {
|
|
|
- console.warn(`[DoneRate Cron] Invalid artwork ID found in done_counts result: ${row.res}`);
|
|
|
+ invalidArtworkRowCount++;
|
|
|
+ console.warn(`[DoneRate Cron] Invalid artwork ID found in aggregate result: ${row.res}`);
|
|
|
}
|
|
|
});
|
|
|
- console.log(`[DoneRate Cron] Retrieved ${doneResults.length} unique done counts from ClickHouse.`);
|
|
|
|
|
|
- // 1.3 查询目标日期每个作品的使用道具数
|
|
|
- const tipCountsQuery = `
|
|
|
- SELECT
|
|
|
- res,
|
|
|
- count() AS tip_count
|
|
|
- FROM ${CLICKHOUSE_EVENTS_TABLE}
|
|
|
- WHERE event = 'color_tip'
|
|
|
- AND time >= toDateTime('${yesterdayStartString}')
|
|
|
- AND time < toDateTime('${yesterdayEndString}')
|
|
|
- GROUP BY res
|
|
|
- HAVING res IS NOT NULL
|
|
|
- `;
|
|
|
- const tipResults = await clickhouseService.queryEvents<ClickHouseTipCountResult>(tipCountsQuery);
|
|
|
- const artworkTipCounts = new Map<string, number>();
|
|
|
- tipResults.forEach((row) => {
|
|
|
- if (row.res && mongoose.Types.ObjectId.isValid(row.res)) {
|
|
|
- artworkTipCounts.set(row.res, row.tip_count);
|
|
|
- } else {
|
|
|
- console.warn(`[DoneRate Cron] Invalid artwork ID found in tip_counts result: ${row.res}`);
|
|
|
- }
|
|
|
+ let totalStartUsers = 0;
|
|
|
+ let totalDoneUsers = 0;
|
|
|
+ let totalTipEvents = 0;
|
|
|
+ aggregateResults.forEach((row) => {
|
|
|
+ totalStartUsers += Number(row.unique_starts) || 0;
|
|
|
+ totalDoneUsers += Number(row.unique_dones) || 0;
|
|
|
+ totalTipEvents += Number(row.tip_count) || 0;
|
|
|
});
|
|
|
- console.log(`[DoneRate Cron] Retrieved ${tipResults.length} unique tip counts from ClickHouse.`);
|
|
|
+
|
|
|
+ console.log(
|
|
|
+ `[DoneRate Cron] ClickHouse aggregate query completed in ${clickhouseQueryElapsedMs}ms. Rows: ${aggregateResults.length}, valid artworks: ${artworkStartCounts.size}, invalid rows: ${invalidArtworkRowCount}, unique starts: ${totalStartUsers}, unique dones: ${totalDoneUsers}, tip events: ${totalTipEvents}.`
|
|
|
+ );
|
|
|
|
|
|
// --- 2. 合并数据并更新 DoneRate 模型 (每日记录) ---
|
|
|
let updatedRecordsCount = 0;
|