guoziyun 8 сар өмнө
parent
commit
b11095ac5c

+ 91 - 0
oms/dist/src/scripts/backfill-done-rate-tip-count.js

@@ -0,0 +1,91 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.runBackfill = runBackfill;
+const dayjs_1 = __importDefault(require("dayjs"));
+const clients_1 = require("../services/clients");
+const mongoose_1 = __importDefault(require("mongoose"));
+const doneRateModel_1 = __importDefault(require("../models/doneRateModel"));
+// 导入 DoneRate 模型
+// ClickHouse 表名
+const CLICKHOUSE_EVENTS_TABLE = "events";
+// =========================================================================
+// !!! 必须修改这里以设置回填的起始日期 (YYYY-MM-DD 格式) !!!
+// 示例: 如果您的 tipCount 追踪是从 2024 年 1 月 1 日开始的,则设置为 "2024-01-01"
+const BACKFILL_START_DATE = "2025-01-03";
+/**
+ * 遍历指定日期范围,从 ClickHouse 提取 tipCount 并回填到 DoneRate 记录中。
+ * @returns Promise<string> - 返回回填结果的摘要信息。
+ */
+async function runBackfill() {
+    console.log(`[TipCount Backfill] Starting tipCount initialization for DoneRate model.`);
+    // 设置日期范围:从 BACKFILL_START_DATE 到昨天
+    let startDate = (0, dayjs_1.default)(BACKFILL_START_DATE).startOf("day");
+    const endDate = (0, dayjs_1.default)().subtract(1, "day").startOf("day");
+    if (!startDate.isValid() || startDate.isAfter(endDate)) {
+        const errorMsg = `[TipCount Backfill] Invalid start date ${BACKFILL_START_DATE} or date range is empty. Aborting.`;
+        console.error(errorMsg);
+        return errorMsg;
+    }
+    console.log(`[TipCount Backfill] Date Range: ${startDate.format("YYYY-MM-DD")} to ${endDate.format("YYYY-MM-DD")}`);
+    let totalDaysProcessed = 0;
+    let totalDoneRateUpdated = 0;
+    // 循环遍历历史数据中的每一天
+    for (let currentDay = startDate; currentDay.isBefore(endDate) || currentDay.isSame(endDate); currentDay = currentDay.add(1, "day")) {
+        const currentYYYYMMDD = currentDay.format("YYYYMMDD");
+        const currentStartString = currentDay.format("YYYY-MM-DD HH:mm:ss");
+        const currentEndString = currentDay.endOf("day").format("YYYY-MM-DD HH:mm:ss");
+        console.log(`\n--- [TipCount Backfill] Processing date: ${currentYYYYMMDD} ---`);
+        totalDaysProcessed++;
+        try {
+            // 1. 从 ClickHouse 中提取当天的 tipCount
+            const tipCountsQuery = `
+        SELECT
+            res,
+            count() AS tip_count
+        FROM ${CLICKHOUSE_EVENTS_TABLE}
+        WHERE event = 'color_tip'
+          AND time >= toDateTime('${currentStartString}')
+          AND time < toDateTime('${currentEndString}')
+        GROUP BY res
+        HAVING res IS NOT NULL
+      `;
+            const tipResults = await clients_1.clickhouseService.queryEvents(tipCountsQuery);
+            console.log(`[TipCount Backfill] Retrieved ${tipResults.length} records with 'color_tip' event.`);
+            if (tipResults.length === 0) {
+                // 如果当天没有 tip 事件,但可能存在 DoneRate 记录,我们仍然需要更新它们 (如果它们是 null 或 undefined)
+                // 但由于 DoneRateModel 默认 tipCount: 0,所以我们只需要处理有 tip 事件的记录。
+                console.log(`[TipCount Backfill] No 'color_tip' events found for ${currentYYYYMMDD}. Skipping MongoDB update.`);
+                continue;
+            }
+            // 2. 批量更新 MongoDB 中的 DoneRate 记录
+            const bulkOps = tipResults
+                .filter((row) => row.res && mongoose_1.default.Types.ObjectId.isValid(row.res)) // 过滤掉无效的 res ID
+                .map((row) => ({
+                updateOne: {
+                    // 查找条件:日期 + 作品ID (唯一复合索引)
+                    filter: { date: currentYYYYMMDD, res: new mongoose_1.default.Types.ObjectId(row.res) },
+                    // 更新操作:设置 tipCount 字段
+                    // 注意:我们只更新 tipCount,不影响 startCount 和 doneCount
+                    update: { $set: { tipCount: row.tip_count } },
+                    // 如果 DoneRate 记录不存在,则忽略。因为 DoneRate 记录通常由 color_start/color_done 事件创建。
+                    upsert: false,
+                },
+            }));
+            if (bulkOps.length > 0) {
+                const updateResult = await doneRateModel_1.default.bulkWrite(bulkOps, { ordered: false });
+                const modifiedCount = updateResult.modifiedCount || 0;
+                totalDoneRateUpdated += modifiedCount;
+                console.log(`[TipCount Backfill] Successfully updated ${modifiedCount} DoneRate documents for ${currentYYYYMMDD}.`);
+            }
+        }
+        catch (error) {
+            console.error(`[TipCount Backfill] Fatal error processing date ${currentYYYYMMDD}:`, error);
+        }
+    }
+    const summary = `[TipCount Backfill] DoneRate tipCount backfill completed. Total days processed: ${totalDaysProcessed}. Total DoneRate documents updated: ${totalDoneRateUpdated}.`;
+    console.log(`\n${summary}`);
+    return summary;
+}

+ 40 - 0
oms/dist/src/scripts/initialize-tip-count.js

@@ -0,0 +1,40 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.runInitialization = runInitialization;
+const doneRateModel_1 = __importDefault(require("../models/doneRateModel"));
+/**
+ * 初始化所有 DoneRate 历史文档中缺失的 tipCount 字段为 0。
+ * 确保所有文档在开始回填前拥有 tipCount 字段,以保证数据一致性。
+ * @returns Promise<string> - 返回初始化结果的摘要信息。
+ */
+async function runInitialization() {
+    console.log("[TipCount Initialization] Starting to set missing tipCount field to 0 for all historical DoneRate documents...");
+    const startTime = Date.now();
+    try {
+        // 查找条件: 查找 tipCount 字段不存在的文档 (即历史数据)
+        const filter = { tipCount: { $exists: false } };
+        // 更新操作: 将 tipCount 设置为 0
+        const update = { $set: { tipCount: 0 } };
+        // 选项: 更新所有匹配的文档
+        const options = { multi: true };
+        const result = await doneRateModel_1.default.updateMany(filter, update, options).exec();
+        const endTime = Date.now();
+        const timeTaken = ((endTime - startTime) / 1000).toFixed(2);
+        if (result.modifiedCount === 0) {
+            console.log("[TipCount Initialization] No missing tipCount fields found. Historical data seems consistent or already initialized.");
+        }
+        else {
+            console.log(`[TipCount Initialization] Successfully initialized ${result.modifiedCount} historical DoneRate documents to tipCount: 0.`);
+        }
+        const summary = `[TipCount Initialization] Initialization completed in ${timeTaken} seconds. Total documents modified: ${result.modifiedCount}.`;
+        console.log(`\n${summary}`);
+        return summary;
+    }
+    catch (error) {
+        console.error("[TipCount Initialization] Fatal error during initialization:", error);
+        throw new Error("Failed to initialize DoneRate tipCount field.");
+    }
+}

+ 89 - 0
oms/dist/src/scripts/recalculate-total-done-rate.js

@@ -0,0 +1,89 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+    return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.runRecalculation = runRecalculation;
+const doneRateModel_1 = __importDefault(require("../models/doneRateModel"));
+const totalDoneRateModel_1 = __importDefault(require("../models/totalDoneRateModel"));
+/**
+ * 重新计算 TotalDoneRate 表中的所有累计字段。
+ * 通过聚合 DoneRate 表中的所有历史数据来实现。
+ * 此脚本应在 DoneRate 表的 tipCount 字段回填完成后运行。
+ * @returns Promise<string> - 返回累计更新结果的摘要信息。
+ */
+async function runRecalculation() {
+    console.log("[TotalDoneRate Recalculation] Starting full recalculation of TotalDoneRate table...");
+    const startTime = Date.now();
+    try {
+        // 1. 使用聚合操作,从 DoneRate 表中计算每个作品的总累计数据
+        console.log("[TotalDoneRate Recalculation] Step 1: Aggregating all DoneRate records...");
+        // 聚合管道定义
+        const aggregationPipeline = [
+            {
+                // 1. 按作品 ID (res) 分组
+                $group: {
+                    _id: "$res", // 使用 res 作为分组键
+                    totalStartCount: { $sum: "$startCount" },
+                    totalDoneCount: { $sum: "$doneCount" },
+                    totalTipCount: { $sum: "$tipCount" }, // 累加 tipCount
+                },
+            },
+            {
+                // 2. 计算最终的 completionRate
+                $project: {
+                    _id: "$_id",
+                    totalStartCount: "$totalStartCount",
+                    totalDoneCount: "$totalDoneCount",
+                    totalTipCount: "$totalTipCount",
+                    completionRate: {
+                        $cond: [
+                            { $gt: ["$totalStartCount", 0] },
+                            { $multiply: [{ $divide: ["$totalDoneCount", "$totalStartCount"] }, 100] },
+                            0, // 如果 totalStartCount 为 0,则 completionRate 为 0
+                        ],
+                    },
+                },
+            },
+        ];
+        const aggregatedResults = await doneRateModel_1.default.aggregate(aggregationPipeline).exec();
+        console.log(`[TotalDoneRate Recalculation] Aggregation complete. Found ${aggregatedResults.length} unique artworks to update.`);
+        // 2. 批量更新 TotalDoneRate 表
+        const bulkOps = aggregatedResults.map((result) => ({
+            updateOne: {
+                // 查找条件:使用聚合结果的 _id (即作品 ObjectId)
+                filter: { _id: result._id },
+                // 更新操作:设置所有累计字段
+                update: {
+                    $set: {
+                        totalStartCount: result.totalStartCount,
+                        totalDoneCount: result.totalDoneCount,
+                        totalTipCount: result.totalTipCount, // 设置重新计算的总道具使用数
+                        completionRate: result.completionRate,
+                    },
+                },
+                // 如果 TotalDoneRate 记录不存在,则创建新记录
+                upsert: true,
+            },
+        }));
+        if (bulkOps.length > 0) {
+            console.log(`[TotalDoneRate Recalculation] Step 2: Running bulk update for ${bulkOps.length} documents...`);
+            const updateResult = await totalDoneRateModel_1.default.bulkWrite(bulkOps, { ordered: false });
+            const modifiedCount = (updateResult.modifiedCount || 0) + (updateResult.upsertedCount || 0);
+            const endTime = Date.now();
+            const timeTaken = ((endTime - startTime) / 1000).toFixed(2);
+            const summary = `[TotalDoneRate Recalculation] Full recalculation completed in ${timeTaken} seconds. Total artworks updated/created: ${modifiedCount}.`;
+            console.log(`\n${summary}`);
+            return summary;
+        }
+        else {
+            const summary = "[TotalDoneRate Recalculation] Aggregation returned no results. TotalDoneRate table update skipped.";
+            console.log(summary);
+            return summary;
+        }
+    }
+    catch (error) {
+        console.error("[TotalDoneRate Recalculation] Fatal error during full recalculation:", error);
+        throw new Error("Failed to recalculate TotalDoneRate history.");
+    }
+}

+ 16 - 0
oms/dist/src/scripts/run-tip-count.js

@@ -0,0 +1,16 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+const backfill_done_rate_tip_count_1 = require("./backfill-done-rate-tip-count");
+const initialize_tip_count_1 = require("./initialize-tip-count");
+const recalculate_total_done_rate_1 = require("./recalculate-total-done-rate");
+async function run() {
+    await (0, initialize_tip_count_1.runInitialization)();
+    await (0, backfill_done_rate_tip_count_1.runBackfill)();
+    await (0, recalculate_total_done_rate_1.runRecalculation)();
+}
+if (require.main === module) {
+    run().catch((err) => {
+        console.error("初始化tipCount失败:", err);
+        process.exit(1);
+    });
+}

+ 3 - 5
oms/services/cron-jobs/backfill-done-rate-tip-count.ts → oms/src/scripts/backfill-done-rate-tip-count.ts

@@ -1,7 +1,7 @@
 import dayjs from "dayjs";
-import { clickhouseService } from "../../src/services/clients";
+import { clickhouseService } from "../services/clients";
 import mongoose, { Model } from "mongoose";
-import DoneRate, { IDoneRate } from "../../src/models/doneRateModel";
+import DoneRate, { IDoneRate } from "../models/doneRateModel";
 // 导入 DoneRate 模型
 
 // ClickHouse 表名
@@ -25,7 +25,7 @@ interface ClickHouseTipCountResult {
  * 遍历指定日期范围,从 ClickHouse 提取 tipCount 并回填到 DoneRate 记录中。
  * @returns Promise<string> - 返回回填结果的摘要信息。
  */
-async function runBackfill(): Promise<string> {
+export async function runBackfill(): Promise<string> {
   console.log(`[TipCount Backfill] Starting tipCount initialization for DoneRate model.`);
 
   // 设置日期范围:从 BACKFILL_START_DATE 到昨天
@@ -108,5 +108,3 @@ async function runBackfill(): Promise<string> {
   console.log(`\n${summary}`);
   return summary;
 }
-
-export = { runBackfill };

+ 2 - 4
oms/services/cron-jobs/initialize-tip-count.ts → oms/src/scripts/initialize-tip-count.ts

@@ -1,4 +1,4 @@
-import DoneRate from "../../src/models/doneRateModel";
+import DoneRate from "../models/doneRateModel";
 import mongoose from "mongoose";
 
 /**
@@ -6,7 +6,7 @@ import mongoose from "mongoose";
  * 确保所有文档在开始回填前拥有 tipCount 字段,以保证数据一致性。
  * @returns Promise<string> - 返回初始化结果的摘要信息。
  */
-async function runInitialization(): Promise<string> {
+export async function runInitialization(): Promise<string> {
   console.log("[TipCount Initialization] Starting to set missing tipCount field to 0 for all historical DoneRate documents...");
   const startTime = Date.now();
 
@@ -37,5 +37,3 @@ async function runInitialization(): Promise<string> {
     throw new Error("Failed to initialize DoneRate tipCount field.");
   }
 }
-
-export = { runInitialization };

+ 3 - 5
oms/services/cron-jobs/recalculate-total-done-rate.ts → oms/src/scripts/recalculate-total-done-rate.ts

@@ -1,6 +1,6 @@
 import mongoose from "mongoose";
-import DoneRate from "../../src/models/doneRateModel";
-import TotalDoneRate from "../../src/models/totalDoneRateModel";
+import DoneRate from "../models/doneRateModel";
+import TotalDoneRate from "../models/totalDoneRateModel";
 
 /**
  * 重新计算 TotalDoneRate 表中的所有累计字段。
@@ -8,7 +8,7 @@ import TotalDoneRate from "../../src/models/totalDoneRateModel";
  * 此脚本应在 DoneRate 表的 tipCount 字段回填完成后运行。
  * @returns Promise<string> - 返回累计更新结果的摘要信息。
  */
-async function runRecalculation(): Promise<string> {
+export async function runRecalculation(): Promise<string> {
   console.log("[TotalDoneRate Recalculation] Starting full recalculation of TotalDoneRate table...");
   const startTime = Date.now();
 
@@ -90,5 +90,3 @@ async function runRecalculation(): Promise<string> {
     throw new Error("Failed to recalculate TotalDoneRate history.");
   }
 }
-
-export = { runRecalculation };

+ 16 - 0
oms/src/scripts/run-tip-count.ts

@@ -0,0 +1,16 @@
+import { runBackfill } from "./backfill-done-rate-tip-count";
+import { runInitialization } from "./initialize-tip-count";
+import { runRecalculation } from "./recalculate-total-done-rate";
+
+async function run() {
+  await runInitialization();
+  await runBackfill();
+  await runRecalculation();
+}
+
+if (require.main === module) {
+  run().catch((err) => {
+    console.error("初始化tipCount失败:", err);
+    process.exit(1);
+  });
+}