guoziyun 1 ماه پیش
والد
کامیت
c301144d78

+ 1 - 1
AGENTS.md

@@ -117,4 +117,4 @@ For database or migration work:
 
 - Choose the safer operational path.
 - Prefer observability over speculation.
-- Add logs and narrow validation before making a second larger change.
+- Add logs and narrow validation before making a second larger change.

+ 1 - 1
oms/OPTIMIZATION_TRACKER.md

@@ -177,4 +177,4 @@
 
 1. 先在线上执行一次 `done-rate`,观察新增的 ClickHouse 耗时日志。
 2. 如果 ClickHouse 已明显变快,再评估 `TotalDoneRate` 的 Mongo 聚合更新是否成为新瓶颈。
-3. 若后续继续做性能治理,优先处理 `done-rate` 的 Mongo 批量更新。
+3. 若后续继续做性能治理,优先处理 `done-rate` 的 Mongo 批量更新。

+ 1 - 1
oms/dist/services/cron-jobs/monthly-archive-messagerecords.js

@@ -37,7 +37,7 @@ async function run() {
     let conn = mongoose_1.default.connection;
     if (conn.readyState !== 1) {
         // 若默认连接未就绪,创建专用连接
-        conn = await mongoose_1.default.createConnection(MONGO_URI).asPromise();
+        conn = (await mongoose_1.default.createConnection(MONGO_URI).asPromise());
     }
     const hotCol = conn.collection("messagerecords");
     const yyyymm = fmtYYYYMM(lastMonthStart);

+ 5 - 6
oms/dist/src/services/dashboardService.js

@@ -26,7 +26,9 @@ class DashboardService {
      */
     async getKpi(dateRange = 7) {
         try {
-            const startDate = (0, dayjs_1.default)().subtract(dateRange - 1, "day").format("YYYY-MM-DD");
+            const startDate = (0, dayjs_1.default)()
+                .subtract(dateRange - 1, "day")
+                .format("YYYY-MM-DD");
             const endDate = (0, dayjs_1.default)().format("YYYY-MM-DD");
             const today = (0, dayjs_1.default)().format("YYYY-MM-DD");
             // 查询日活用户数聚合数据
@@ -48,10 +50,7 @@ class DashboardService {
         WHERE toDate(time) = '${today}' AND project = 1
       `;
             // 执行查询
-            const [dauTrend, todayDauResult] = await Promise.all([
-                clients_1.clickhouseService.queryEvents(dauQuery),
-                clients_1.clickhouseService.queryEvents(todayDauQuery),
-            ]);
+            const [dauTrend, todayDauResult] = await Promise.all([clients_1.clickhouseService.queryEvents(dauQuery), clients_1.clickhouseService.queryEvents(todayDauQuery)]);
             const todayDau = todayDauResult?.[0]?.dau ?? 0;
             return {
                 dau: {
@@ -225,7 +224,7 @@ class DashboardService {
                     return {
                         resId: art.resId,
                         name: art.name,
-                        thumbnail: art.pageId ? `/thumbs/v2/page/320/${art.pageId}.png` : "",
+                        thumbnail: art.pageId ? `http://color2.jccytech.cn/thumbs/v2/page/320/${art.pageId}.png` : "",
                         clickCount: stat.clickCount,
                         completionCount: stat.completionCount,
                         clickRate: parseFloat(clickRate.toFixed(4)),

+ 5 - 27
oms/dist/src/services/messageRecordService.js

@@ -56,15 +56,11 @@ class MessageRecordService {
             query.templateName = filters.templateName;
         if (filters.status !== undefined)
             query.status = filters.status;
-        const safeSortField = MessageRecordService.ALLOWED_SORT_FIELDS.has(sortField)
-            ? sortField
-            : MessageRecordService.DEFAULT_SORT_FIELD;
+        const safeSortField = MessageRecordService.ALLOWED_SORT_FIELDS.has(sortField) ? sortField : MessageRecordService.DEFAULT_SORT_FIELD;
         const sortOption = {};
         sortOption[safeSortField] = sortOrder === "asc" ? 1 : -1;
         const isUnfilteredQuery = Object.keys(query).length === 0;
-        const total = isUnfilteredQuery
-            ? await messageRecordModel_1.MessageRecord.estimatedDocumentCount()
-            : await messageRecordModel_1.MessageRecord.countDocuments(query);
+        const total = isUnfilteredQuery ? await messageRecordModel_1.MessageRecord.estimatedDocumentCount() : await messageRecordModel_1.MessageRecord.countDocuments(query);
         const isTotalEstimated = isUnfilteredQuery;
         const records = await messageRecordModel_1.MessageRecord.find(query)
             .sort(sortOption)
@@ -342,11 +338,7 @@ class MessageRecordService {
             limit: safeLimit,
         });
         // 并行检查全部缓存
-        const [cachedOverall, cachedDailyTrends, cachedStrategy] = await Promise.all([
-            this.getCache(overallCacheKey),
-            this.getCache(dailyTrendsCacheKey),
-            this.getCache(strategyCacheKey),
-        ]);
+        const [cachedOverall, cachedDailyTrends, cachedStrategy] = await Promise.all([this.getCache(overallCacheKey), this.getCache(dailyTrendsCacheKey), this.getCache(strategyCacheKey)]);
         if (cachedOverall && cachedDailyTrends && cachedStrategy) {
             // 全部命中,直接返回
             console.log(`[MessageStatsCache] hit key=summary:all-3`);
@@ -396,13 +388,7 @@ class MessageRecordService {
             cachedDailyTrends ? Promise.resolve() : this.setCache(dailyTrendsCacheKey, dailyTrends),
             cachedStrategy ? Promise.resolve() : this.setCache(strategyCacheKey, strategyStats),
         ]);
-        const missKeys = [
-            !cachedOverall ? "overall" : null,
-            !cachedDailyTrends ? "daily-trends" : null,
-            !cachedStrategy ? "by-strategy" : null,
-        ]
-            .filter(Boolean)
-            .join(",");
+        const missKeys = [!cachedOverall ? "overall" : null, !cachedDailyTrends ? "daily-trends" : null, !cachedStrategy ? "by-strategy" : null].filter(Boolean).join(",");
         console.log(`[MessageStatsCache] miss key=summary:facet misses=${missKeys}`);
         this.logPerf("summary", "total", Date.now() - summaryStartedAt, {
             dailyTrendRows: this.resolveResultRows(dailyTrends),
@@ -1318,15 +1304,7 @@ MessageRecordService.DEFAULT_LIMIT = 10;
 MessageRecordService.MAX_LIMIT = 100;
 MessageRecordService.DEFAULT_SORT_FIELD = "createdAt";
 MessageRecordService.DEFAULT_SORT_ORDER = "desc";
-MessageRecordService.ALLOWED_SORT_FIELDS = new Set([
-    "createdAt",
-    "updatedAt",
-    "status",
-    "uid",
-    "strategyName",
-    "templateName",
-    "activityName",
-]);
+MessageRecordService.ALLOWED_SORT_FIELDS = new Set(["createdAt", "updatedAt", "status", "uid", "strategyName", "templateName", "activityName"]);
 MessageRecordService.DEFAULT_STATS_PAGE = 1;
 MessageRecordService.DEFAULT_STATS_LIMIT = 50;
 MessageRecordService.MAX_STATS_LIMIT = 200;

+ 6 - 14
oms/services/cron-jobs/done-rate.ts

@@ -103,7 +103,7 @@ async function run(dateStr?: string | dayjs.Dayjs): Promise<string> {
     });
 
     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}.`
+      `[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 模型 (每日记录) ---
@@ -114,9 +114,7 @@ async function run(dateStr?: string | dayjs.Dayjs): Promise<string> {
     const allResIds = [...new Set([...artworkStartCounts.keys(), ...artworkDoneCounts.keys(), ...artworkTipCounts.keys()])];
 
     const doneRateBulkStartedAt = Date.now();
-    console.log(
-      `[DoneRate Cron] 开始通过 bulkWrite 更新每日 DoneRate。总计 ${allResIds.length} 个作品,batchSize=${DAILY_DONE_RATE_BULK_SIZE}。`
-    );
+    console.log(`[DoneRate Cron] 开始通过 bulkWrite 更新每日 DoneRate。总计 ${allResIds.length} 个作品,batchSize=${DAILY_DONE_RATE_BULK_SIZE}。`);
 
     for (let i = 0; i < allResIds.length; i += DAILY_DONE_RATE_BULK_SIZE) {
       const chunk = allResIds.slice(i, i + DAILY_DONE_RATE_BULK_SIZE);
@@ -159,7 +157,7 @@ async function run(dateStr?: string | dayjs.Dayjs): Promise<string> {
     const totalProcessedArtworks = createdRecordsCount + updatedRecordsCount;
 
     console.log(
-      `[DoneRate Cron] DoneRate model (Daily) update completed. Total artworks processed: ${totalProcessedArtworks}. Created: ${createdRecordsCount}, Updated: ${updatedRecordsCount}. Elapsed: ${Date.now() - doneRateBulkStartedAt}ms.`
+      `[DoneRate Cron] DoneRate model (Daily) update completed. Total artworks processed: ${totalProcessedArtworks}. Created: ${createdRecordsCount}, Updated: ${updatedRecordsCount}. Elapsed: ${Date.now() - doneRateBulkStartedAt}ms.`,
     );
 
     // --- 3. 重新聚合并更新本地的 TotalDoneRate 表 (累计记录) ---
@@ -183,16 +181,12 @@ async function run(dateStr?: string | dayjs.Dayjs): Promise<string> {
     const aggregatedTotals = await DoneRateModel.aggregate(aggregationPipeline);
     const totalAggregateElapsedMs = Date.now() - totalAggregateStartedAt;
 
-    console.log(
-      `[DoneRate Cron] TotalDoneRate 聚合完成。作品数 ${aggregatedTotals.length},聚合耗时 ${totalAggregateElapsedMs}ms。`
-    );
+    console.log(`[DoneRate Cron] TotalDoneRate 聚合完成。作品数 ${aggregatedTotals.length},聚合耗时 ${totalAggregateElapsedMs}ms。`);
 
     const totalUpdateStartedAt = Date.now();
     let updatedTotalDoneRateCount = 0;
 
-    console.log(
-      `[DoneRate Cron] 开始通过 bulkWrite 更新本地 TotalDoneRate 表。总计 ${aggregatedTotals.length} 个作品,batchSize=${TOTAL_DONE_RATE_BULK_SIZE}。`
-    );
+    console.log(`[DoneRate Cron] 开始通过 bulkWrite 更新本地 TotalDoneRate 表。总计 ${aggregatedTotals.length} 个作品,batchSize=${TOTAL_DONE_RATE_BULK_SIZE}。`);
 
     for (let i = 0; i < aggregatedTotals.length; i += TOTAL_DONE_RATE_BULK_SIZE) {
       const chunk = aggregatedTotals.slice(i, i + TOTAL_DONE_RATE_BULK_SIZE);
@@ -228,9 +222,7 @@ async function run(dateStr?: string | dayjs.Dayjs): Promise<string> {
     }
 
     const totalUpdateElapsedMs = Date.now() - totalUpdateStartedAt;
-    console.log(
-      `[DoneRate Cron] 本地 TotalDoneRate 表更新完成。处理记录数 ${aggregatedTotals.length},写入阶段耗时 ${totalUpdateElapsedMs}ms。`
-    );
+    console.log(`[DoneRate Cron] 本地 TotalDoneRate 表更新完成。处理记录数 ${aggregatedTotals.length},写入阶段耗时 ${totalUpdateElapsedMs}ms。`);
 
     const summary = `[DoneRate Cron] Daily done-rate calculation for ${yesterdayYYYYMMDD} completed. Total DoneRate processed: ${totalProcessedArtworks}. Created DoneRate: ${createdRecordsCount}, Updated DoneRate: ${updatedRecordsCount}. Updated TotalDoneRate records: ${updatedTotalDoneRateCount}.`;
     console.log(summary);

+ 3 - 3
oms/services/cron-jobs/monthly-archive-messagerecords.ts

@@ -38,7 +38,7 @@ export async function run(): Promise<string> {
   let conn = mongoose.connection;
   if (conn.readyState !== 1) {
     // 若默认连接未就绪,创建专用连接
-    conn = await mongoose.createConnection(MONGO_URI).asPromise() as any;
+    conn = (await mongoose.createConnection(MONGO_URI).asPromise()) as any;
   }
 
   const hotCol = conn.collection("messagerecords");
@@ -75,7 +75,7 @@ export async function run(): Promise<string> {
     if (batch.length >= BATCH_SIZE) {
       const result = await archiveCol.bulkWrite(
         batch.map((d) => ({ replaceOne: { filter: { _id: d._id }, replacement: d, upsert: true } })),
-        { ordered: false }
+        { ordered: false },
       );
       batchInserted += result.upsertedCount + result.modifiedCount;
       batch = [];
@@ -85,7 +85,7 @@ export async function run(): Promise<string> {
   if (batch.length > 0) {
     const result = await archiveCol.bulkWrite(
       batch.map((d) => ({ replaceOne: { filter: { _id: d._id }, replacement: d, upsert: true } })),
-      { ordered: false }
+      { ordered: false },
     );
     batchInserted += result.upsertedCount + result.modifiedCount;
   }

+ 1 - 4
oms/src/controllers/dashboardController.ts

@@ -57,10 +57,7 @@ class DashboardController {
       console.log(`[DashboardController] getArtworkStats start, top=${top}, sortBy=${sortBy}`);
       const startedAt = Date.now();
 
-      const artworkStats = await dashboardService.getArtworkStats(
-        top,
-        sortBy as "clickRate" | "completionRate" | "clickCount" | "completionCount"
-      );
+      const artworkStats = await dashboardService.getArtworkStats(top, sortBy as "clickRate" | "completionRate" | "clickCount" | "completionCount");
 
       console.log(`[DashboardController] getArtworkStats end, durationMs=${Date.now() - startedAt}`);
 

+ 3 - 3
oms/src/controllers/messageRecordController.ts

@@ -86,7 +86,7 @@ class MessageRecordController {
         page: req.query.page || null,
         limit: req.query.limit || null,
         ...extra,
-      })
+      }),
     );
 
     return (resultRows: number, success: boolean, errorMessage?: string) => {
@@ -98,7 +98,7 @@ class MessageRecordController {
           resultRows,
           success,
           errorMessage: errorMessage || null,
-        })
+        }),
       );
     };
   }
@@ -140,7 +140,7 @@ class MessageRecordController {
         parseInt(limit as string) || 10,
         filters,
         sortField as string,
-        sortOrder as "asc" | "desc"
+        sortOrder as "asc" | "desc",
       );
 
       // return res.status(200).json({

+ 5 - 17
oms/src/models/messageRecordModel.ts

@@ -155,7 +155,7 @@ const messageRecordSchema = new Schema<IMessageRecord>(
   },
   {
     timestamps: true, // Automatically adds createdAt and updatedAt
-  }
+  },
 );
 
 // Added compound index for common queries by user and status
@@ -211,36 +211,24 @@ messageRecordSchema.index({ createdAt: 1, status: 1 });
  * 聚合覆盖索引1:by-template / getDailyTrendsByTemplate
  * pipeline 访问字段:createdAt, templateName, (templateId 已从分组移除), status, inforeground, uid
  */
-messageRecordSchema.index(
-  { createdAt: 1, templateName: 1, status: 1, inforeground: 1, uid: 1 },
-  { name: "idx_agg_template" }
-);
+messageRecordSchema.index({ createdAt: 1, templateName: 1, status: 1, inforeground: 1, uid: 1 }, { name: "idx_agg_template" });
 
 /**
  * 聚合覆盖索引2:by-image / getDailyTrendsByImage
  * pipeline 访问字段:createdAt, image, status, inforeground, uid
  */
-messageRecordSchema.index(
-  { createdAt: 1, image: 1, status: 1, inforeground: 1, uid: 1 },
-  { name: "idx_agg_image" }
-);
+messageRecordSchema.index({ createdAt: 1, image: 1, status: 1, inforeground: 1, uid: 1 }, { name: "idx_agg_image" });
 
 /**
  * 聚合覆盖索引3:by-cc / getDailyTrendsByCc
  * pipeline 访问字段:createdAt, cc, status, inforeground, uid
  */
-messageRecordSchema.index(
-  { createdAt: 1, cc: 1, status: 1, inforeground: 1, uid: 1 },
-  { name: "idx_agg_cc" }
-);
+messageRecordSchema.index({ createdAt: 1, cc: 1, status: 1, inforeground: 1, uid: 1 }, { name: "idx_agg_cc" });
 
 /**
  * 聚合覆盖索引4:overall / daily-trends / by-strategy(summary 首屏高频)
  * pipeline 访问字段:createdAt, strategyName, status, inforeground, uid
  */
-messageRecordSchema.index(
-  { createdAt: 1, strategyName: 1, status: 1, inforeground: 1, uid: 1 },
-  { name: "idx_agg_strategy" }
-);
+messageRecordSchema.index({ createdAt: 1, strategyName: 1, status: 1, inforeground: 1, uid: 1 }, { name: "idx_agg_strategy" });
 
 export const MessageRecord = model<IMessageRecord>("MessageRecord", messageRecordSchema);

+ 2 - 2
oms/src/models/messageStatsDailyUidModel.ts

@@ -30,7 +30,7 @@ const messageStatsDailyUidSchema = new Schema<IMessageStatsDailyUid>(
     uid: { type: String, required: true, index: true },
     msgCount: { type: Number, required: true, default: 0 },
   },
-  { timestamps: true }
+  { timestamps: true },
 );
 
 // 幂等唯一键:每天、每维度、每状态、每用户一条 rollup 记录
@@ -46,7 +46,7 @@ messageStatsDailyUidSchema.index(
     inforeground: 1,
     uid: 1,
   },
-  { unique: true, name: "uniq_daily_uid_rollup" }
+  { unique: true, name: "uniq_daily_uid_rollup" },
 );
 
 // 常见查询路径索引

+ 1 - 3
oms/src/scripts/verify-messagerecords-archive.ts

@@ -87,9 +87,7 @@ async function run() {
     const diff = archiveCount - hotCount;
     const diffStr = diff >= 0 ? `+${diff}` : `${diff}`;
 
-    console.log(
-      `[Verify] ${yyyymm}: hot=${hotCount.toLocaleString().padStart(10)} archive=${archiveCount.toLocaleString().padStart(10)} diff=${diffStr.padStart(7)} → ${status}`
-    );
+    console.log(`[Verify] ${yyyymm}: hot=${hotCount.toLocaleString().padStart(10)} archive=${archiveCount.toLocaleString().padStart(10)} diff=${diffStr.padStart(7)} → ${status}`);
 
     if (status === "PASS") {
       passCount++;

+ 7 - 14
oms/src/services/dashboardService.ts

@@ -62,7 +62,9 @@ class DashboardService {
    */
   public async getKpi(dateRange: number = 7): Promise<KpiData> {
     try {
-      const startDate = dayjs().subtract(dateRange - 1, "day").format("YYYY-MM-DD");
+      const startDate = dayjs()
+        .subtract(dateRange - 1, "day")
+        .format("YYYY-MM-DD");
       const endDate = dayjs().format("YYYY-MM-DD");
       const today = dayjs().format("YYYY-MM-DD");
 
@@ -86,12 +88,8 @@ class DashboardService {
         WHERE toDate(time) = '${today}' AND project = 1
       `;
 
-
       // 执行查询
-      const [dauTrend, todayDauResult] = await Promise.all([
-        clickhouseService.queryEvents<{ date: string; dau: number }>(dauQuery),
-        clickhouseService.queryEvents<{ dau: number }>(todayDauQuery),
-      ]);
+      const [dauTrend, todayDauResult] = await Promise.all([clickhouseService.queryEvents<{ date: string; dau: number }>(dauQuery), clickhouseService.queryEvents<{ dau: number }>(todayDauQuery)]);
 
       const todayDau = todayDauResult?.[0]?.dau ?? 0;
 
@@ -116,10 +114,7 @@ class DashboardService {
    * @param sortBy 排序字段:clickRate | completionRate | clickCount | completionCount
    * @returns 作品统计数据
    */
-  public async getArtworkStats(
-    top: number = 10,
-    sortBy: "clickRate" | "completionRate" | "clickCount" | "completionCount" = "clickCount"
-  ): Promise<ArtworkStatsResponse> {
+  public async getArtworkStats(top: number = 10, sortBy: "clickRate" | "completionRate" | "clickCount" | "completionCount" = "clickCount"): Promise<ArtworkStatsResponse> {
     try {
       const today = dayjs().format("YYYY-MM-DD");
 
@@ -207,9 +202,7 @@ class DashboardService {
         .select({ _id: 1, name: 1, pageId: 1, publishTime: 1 })
         .lean();
 
-      const dayKeys = Array.from({ length: safeDays }, (_, index) =>
-        latestDayStart.subtract(index, "day").format("YYYY-MM-DD")
-      );
+      const dayKeys = Array.from({ length: safeDays }, (_, index) => latestDayStart.subtract(index, "day").format("YYYY-MM-DD"));
 
       const artsByDay = new Map<string, Array<{ resId: string; name: string; pageId: string }>>();
       for (const dayKey of dayKeys) {
@@ -296,7 +289,7 @@ class DashboardService {
           return {
             resId: art.resId,
             name: art.name,
-            thumbnail: art.pageId ? `/thumbs/v2/page/320/${art.pageId}.png` : "",
+            thumbnail: art.pageId ? `http://color2.jccytech.cn/thumbs/v2/page/320/${art.pageId}.png` : "",
             clickCount: stat.clickCount,
             completionCount: stat.completionCount,
             clickRate: parseFloat(clickRate.toFixed(4)),

+ 21 - 65
oms/src/services/messageRecordService.ts

@@ -11,15 +11,7 @@ export class MessageRecordService {
   private static readonly MAX_LIMIT = 100;
   private static readonly DEFAULT_SORT_FIELD = "createdAt";
   private static readonly DEFAULT_SORT_ORDER: "asc" | "desc" = "desc";
-  private static readonly ALLOWED_SORT_FIELDS = new Set([
-    "createdAt",
-    "updatedAt",
-    "status",
-    "uid",
-    "strategyName",
-    "templateName",
-    "activityName",
-  ]);
+  private static readonly ALLOWED_SORT_FIELDS = new Set(["createdAt", "updatedAt", "status", "uid", "strategyName", "templateName", "activityName"]);
 
   public static readonly DEFAULT_STATS_PAGE = 1;
   public static readonly DEFAULT_STATS_LIMIT = 50;
@@ -35,7 +27,7 @@ export class MessageRecordService {
         stage,
         durationMs,
         ...extra,
-      })
+      }),
     );
   }
 
@@ -76,7 +68,7 @@ export class MessageRecordService {
     limit: number = MessageRecordService.DEFAULT_LIMIT,
     filters: { [key: string]: any } = {},
     sortField: string = MessageRecordService.DEFAULT_SORT_FIELD,
-    sortOrder: "asc" | "desc" = MessageRecordService.DEFAULT_SORT_ORDER
+    sortOrder: "asc" | "desc" = MessageRecordService.DEFAULT_SORT_ORDER,
   ): Promise<{ records: any[]; total: number; page: number; limit: number; totalPages: number; isTotalEstimated: boolean }> {
     const safePage = Number.isNaN(page) ? MessageRecordService.DEFAULT_PAGE : Math.max(1, page);
     const safeLimit = Number.isNaN(limit) ? MessageRecordService.DEFAULT_LIMIT : Math.min(MessageRecordService.MAX_LIMIT, Math.max(1, limit));
@@ -90,17 +82,13 @@ export class MessageRecordService {
     if (filters.templateName) query.templateName = filters.templateName;
     if (filters.status !== undefined) query.status = filters.status;
 
-    const safeSortField = MessageRecordService.ALLOWED_SORT_FIELDS.has(sortField)
-      ? sortField
-      : MessageRecordService.DEFAULT_SORT_FIELD;
+    const safeSortField = MessageRecordService.ALLOWED_SORT_FIELDS.has(sortField) ? sortField : MessageRecordService.DEFAULT_SORT_FIELD;
 
     const sortOption: any = {};
     sortOption[safeSortField] = sortOrder === "asc" ? 1 : -1;
 
     const isUnfilteredQuery = Object.keys(query).length === 0;
-    const total = isUnfilteredQuery
-      ? await MessageRecord.estimatedDocumentCount()
-      : await MessageRecord.countDocuments(query);
+    const total = isUnfilteredQuery ? await MessageRecord.estimatedDocumentCount() : await MessageRecord.countDocuments(query);
     const isTotalEstimated = isUnfilteredQuery;
 
     const records = await MessageRecord.find(query)
@@ -198,7 +186,7 @@ export class MessageRecordService {
     endDate?: Date,
     strategyName?: string,
     page: number = MessageRecordService.DEFAULT_STATS_PAGE,
-    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT
+    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT,
   ) {
     const cacheKey = this.buildStatsCacheKey("by-strategy", {
       startDate: startDate?.toISOString(),
@@ -249,7 +237,7 @@ export class MessageRecordService {
     endDate?: Date,
     strategyName?: string,
     page: number = MessageRecordService.DEFAULT_STATS_PAGE,
-    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT
+    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT,
   ) {
     const cacheKey = this.buildStatsCacheKey("by-template", {
       startDate: startDate?.toISOString(),
@@ -291,7 +279,7 @@ export class MessageRecordService {
     endDate?: Date,
     strategyName?: string,
     page: number = MessageRecordService.DEFAULT_STATS_PAGE,
-    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT
+    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT,
   ) {
     const cacheKey = this.buildStatsCacheKey("by-cc", {
       startDate: startDate?.toISOString(),
@@ -331,7 +319,7 @@ export class MessageRecordService {
     endDate?: Date,
     strategyName?: string,
     page: number = MessageRecordService.DEFAULT_STATS_PAGE,
-    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT
+    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT,
   ) {
     const cacheKey = this.buildStatsCacheKey("by-image", {
       startDate: startDate?.toISOString(),
@@ -411,15 +399,12 @@ export class MessageRecordService {
     endDate?: Date,
     strategyName?: string,
     page: number = MessageRecordService.DEFAULT_STATS_PAGE,
-    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT
+    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT,
   ) {
     const summaryStartedAt = Date.now();
 
     const safePage = Math.max(1, Math.floor(page || MessageRecordService.DEFAULT_STATS_PAGE));
-    const safeLimit = Math.min(
-      MessageRecordService.MAX_STATS_LIMIT,
-      Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT))
-    );
+    const safeLimit = Math.min(MessageRecordService.MAX_STATS_LIMIT, Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT)));
 
     // 预先构建 3 个子查询的 cache key,与各自方法保持完全一致
     const overallCacheKey = this.buildStatsCacheKey("overall", {
@@ -441,11 +426,7 @@ export class MessageRecordService {
     });
 
     // 并行检查全部缓存
-    const [cachedOverall, cachedDailyTrends, cachedStrategy] = await Promise.all([
-      this.getCache(overallCacheKey),
-      this.getCache(dailyTrendsCacheKey),
-      this.getCache(strategyCacheKey),
-    ]);
+    const [cachedOverall, cachedDailyTrends, cachedStrategy] = await Promise.all([this.getCache(overallCacheKey), this.getCache(dailyTrendsCacheKey), this.getCache(strategyCacheKey)]);
 
     if (cachedOverall && cachedDailyTrends && cachedStrategy) {
       // 全部命中,直接返回
@@ -501,13 +482,7 @@ export class MessageRecordService {
       cachedStrategy ? Promise.resolve() : this.setCache(strategyCacheKey, strategyStats),
     ]);
 
-    const missKeys = [
-      !cachedOverall ? "overall" : null,
-      !cachedDailyTrends ? "daily-trends" : null,
-      !cachedStrategy ? "by-strategy" : null,
-    ]
-      .filter(Boolean)
-      .join(",");
+    const missKeys = [!cachedOverall ? "overall" : null, !cachedDailyTrends ? "daily-trends" : null, !cachedStrategy ? "by-strategy" : null].filter(Boolean).join(",");
     console.log(`[MessageStatsCache] miss key=summary:facet misses=${missKeys}`);
 
     this.logPerf("summary", "total", Date.now() - summaryStartedAt, {
@@ -833,10 +808,7 @@ export class MessageRecordService {
 
   private async getByStrategyFromPreAgg(startDate: Date, endDate: Date, strategyName: string | undefined, page: number, limit: number) {
     const safePage = Math.max(1, Math.floor(page || MessageRecordService.DEFAULT_STATS_PAGE));
-    const safeLimit = Math.min(
-      MessageRecordService.MAX_STATS_LIMIT,
-      Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT))
-    );
+    const safeLimit = Math.min(MessageRecordService.MAX_STATS_LIMIT, Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT)));
 
     const pipeline: any[] = [
       { $match: this.buildPreAggMatch(startDate, endDate, strategyName) },
@@ -884,10 +856,7 @@ export class MessageRecordService {
 
   private async getByTemplateFromPreAgg(startDate: Date, endDate: Date, strategyName: string | undefined, page: number, limit: number) {
     const safePage = Math.max(1, Math.floor(page || MessageRecordService.DEFAULT_STATS_PAGE));
-    const safeLimit = Math.min(
-      MessageRecordService.MAX_STATS_LIMIT,
-      Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT))
-    );
+    const safeLimit = Math.min(MessageRecordService.MAX_STATS_LIMIT, Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT)));
 
     const pipeline: any[] = [
       { $match: this.buildPreAggMatch(startDate, endDate, strategyName) },
@@ -932,10 +901,7 @@ export class MessageRecordService {
 
   private async getByCcFromPreAgg(startDate: Date, endDate: Date, strategyName: string | undefined, page: number, limit: number) {
     const safePage = Math.max(1, Math.floor(page || MessageRecordService.DEFAULT_STATS_PAGE));
-    const safeLimit = Math.min(
-      MessageRecordService.MAX_STATS_LIMIT,
-      Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT))
-    );
+    const safeLimit = Math.min(MessageRecordService.MAX_STATS_LIMIT, Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT)));
 
     const pipeline: any[] = [
       { $match: this.buildPreAggMatch(startDate, endDate, strategyName) },
@@ -980,10 +946,7 @@ export class MessageRecordService {
 
   private async getByImageFromPreAgg(startDate: Date, endDate: Date, strategyName: string | undefined, page: number, limit: number) {
     const safePage = Math.max(1, Math.floor(page || MessageRecordService.DEFAULT_STATS_PAGE));
-    const safeLimit = Math.min(
-      MessageRecordService.MAX_STATS_LIMIT,
-      Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT))
-    );
+    const safeLimit = Math.min(MessageRecordService.MAX_STATS_LIMIT, Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT)));
 
     const pipeline: any[] = [
       { $match: this.buildPreAggMatch(startDate, endDate, strategyName) },
@@ -1098,7 +1061,7 @@ export class MessageRecordService {
     endDate: Date,
     strategyName: string | undefined,
     page: number,
-    limit: number
+    limit: number,
   ): Promise<{ overall: any; strategyStats: any[]; dailyTrends: any[] }> {
     const [overall, strategyStats, dailyTrends] = await Promise.all([
       this.getOverallFromPreAgg(startDate, endDate, strategyName),
@@ -1117,11 +1080,7 @@ export class MessageRecordService {
    * summary 首屏接口专用:单次 $facet 查询同时计算 overall / byStrategy / dailyTrends,
    * 替代 3 次独立聚合,减少对同一时间窗口数据的重复扫描。
    */
-  private async runSummaryFacetQuery(
-    matchConditions: any,
-    page: number,
-    limit: number
-  ): Promise<{ overall: any; strategyStats: any[]; dailyTrends: any[] }> {
+  private async runSummaryFacetQuery(matchConditions: any, page: number, limit: number): Promise<{ overall: any; strategyStats: any[]; dailyTrends: any[] }> {
     const startedAt = Date.now();
 
     // overall 子管道(等价于 getStatisticsByGroup(matchConditions, []))
@@ -1287,7 +1246,7 @@ export class MessageRecordService {
     groupFields: string[] = [],
     sortOrder: any = { _id: 1 },
     page: number = MessageRecordService.DEFAULT_STATS_PAGE,
-    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT
+    limit: number = MessageRecordService.DEFAULT_STATS_LIMIT,
   ) {
     try {
       const startedAt = Date.now();
@@ -1359,10 +1318,7 @@ export class MessageRecordService {
 
       if (groupFields.length > 0) {
         const safePage = Math.max(1, Math.floor(page || MessageRecordService.DEFAULT_STATS_PAGE));
-        const safeLimit = Math.min(
-          MessageRecordService.MAX_STATS_LIMIT,
-          Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT))
-        );
+        const safeLimit = Math.min(MessageRecordService.MAX_STATS_LIMIT, Math.max(1, Math.floor(limit || MessageRecordService.DEFAULT_STATS_LIMIT)));
         pipeline.push({ $skip: (safePage - 1) * safeLimit });
         pipeline.push({ $limit: safeLimit });
       }

+ 69 - 22
omsapp/src/app/pages/dashboard.component.ts

@@ -38,10 +38,17 @@ import { NzMessageService } from 'ng-zorro-antd/message';
       <nz-page-header [nzGhost]="false">
         <nz-page-header-title>数据看板</nz-page-header-title>
         <nz-page-header-extra>
-          <span nz-icon nzType="sync" nzTheme="outline" (click)="refreshData()"></span>
+          <span
+            nz-icon
+            nzType="sync"
+            nzTheme="outline"
+            (click)="refreshData()"
+          ></span>
         </nz-page-header-extra>
         <nz-page-header-content>
-          <p>最后更新时间:{{ lastUpdateTime | date : 'yyyy-MM-dd HH:mm:ss' }}</p>
+          <p>
+            最后更新时间:{{ lastUpdateTime | date: 'yyyy-MM-dd HH:mm:ss' }}
+          </p>
         </nz-page-header-content>
       </nz-page-header>
 
@@ -66,23 +73,33 @@ import { NzMessageService } from 'ng-zorro-antd/message';
                   class="range-btn"
                   [class.active]="selectedDauRange === 7"
                   (click)="changeDauRange(7)"
-                >7天</button>
+                >
+                  7天
+                </button>
                 <button
                   type="button"
                   class="range-btn"
                   [class.active]="selectedDauRange === 14"
                   (click)="changeDauRange(14)"
-                >14天</button>
+                >
+                  14天
+                </button>
                 <button
                   type="button"
                   class="range-btn"
                   [class.active]="selectedDauRange === 30"
                   (click)="changeDauRange(30)"
-                >30天</button>
+                >
+                  30天
+                </button>
               </div>
 
               <div class="chart-wrapper">
-                <svg viewBox="0 0 600 180" class="line-chart" preserveAspectRatio="none">
+                <svg
+                  viewBox="0 0 600 180"
+                  class="line-chart"
+                  preserveAspectRatio="none"
+                >
                   <polyline
                     *ngIf="dauChartPoints"
                     [attr.points]="dauChartPoints"
@@ -106,9 +123,14 @@ import { NzMessageService } from 'ng-zorro-antd/message';
 
           <nz-col [nzXs]="24" [nzLg]="14">
             <nz-card nzTitle="最近7天上新作品表现" nzHoverable>
-              <nz-tabset [nzSelectedIndex]="activeArtworkTabIndex" (nzSelectedIndexChange)="activeArtworkTabIndex = $event">
+              <nz-tabset
+                [nzSelectedIndex]="activeArtworkTabIndex"
+                (nzSelectedIndexChange)="activeArtworkTabIndex = $event"
+              >
                 <nz-tab *ngFor="let tab of artworkTabs" [nzTitle]="tab.label">
-                  <div class="tab-subtitle">{{ tab.date }} · 当日DAU {{ tab.dau }}</div>
+                  <div class="tab-subtitle">
+                    {{ tab.date }} · 当日DAU {{ tab.dau }}
+                  </div>
 
                   <nz-table
                     #worksTable
@@ -130,16 +152,27 @@ import { NzMessageService } from 'ng-zorro-antd/message';
                     <tbody>
                       <tr *ngFor="let work of worksTable.data">
                         <td>
-                          <div class="work-thumbnail" [style.backgroundImage]="'url(' + work.thumbnail + ')'" ></div>
+                          <div
+                            class="work-thumbnail"
+                            [style.backgroundImage]="
+                              'url(' + work.thumbnail + ')'
+                            "
+                          ></div>
                         </td>
                         <td>{{ work.name }}</td>
-                        <td>{{ (work.clickRate * 100) | number : '1.1-2' }}%</td>
+                        <td>{{ work.clickRate * 100 | number: '1.1-2' }}%</td>
                         <td>
                           <nz-progress
-                            [nzPercent]="(work.completionRate * 100)"
+                            [nzPercent]="work.completionRate * 100"
                             [nzShowInfo]="true"
                             [nzStrokeWidth]="6"
-                            [nzStrokeColor]="work.completionRate >= 0.6 ? '#52c41a' : work.completionRate >= 0.35 ? '#faad14' : '#ff4d4f'"
+                            [nzStrokeColor]="
+                              work.completionRate >= 0.6
+                                ? '#52c41a'
+                                : work.completionRate >= 0.35
+                                  ? '#faad14'
+                                  : '#ff4d4f'
+                            "
                           ></nz-progress>
                         </td>
                         <td>{{ work.clickCount }}</td>
@@ -148,7 +181,9 @@ import { NzMessageService } from 'ng-zorro-antd/message';
                     </tbody>
                   </nz-table>
 
-                  <div class="empty-tip" *ngIf="tab.artworks.length === 0">当日无上新作品</div>
+                  <div class="empty-tip" *ngIf="tab.artworks.length === 0">
+                    当日无上新作品
+                  </div>
                 </nz-tab>
               </nz-tabset>
             </nz-card>
@@ -249,7 +284,7 @@ export class DashboardComponent implements OnInit {
   constructor(
     private modalService: NzModalService,
     private message: NzMessageService,
-    private dashboardService: DashboardService
+    private dashboardService: DashboardService,
   ) {}
 
   ngOnInit(): void {
@@ -275,12 +310,14 @@ export class DashboardComponent implements OnInit {
     const minVal = Math.min(...values);
     const maxVal = Math.max(...values);
     const span = Math.max(1, maxVal - minVal);
-    const stepX = values.length > 1 ? (width - padding * 2) / (values.length - 1) : 0;
+    const stepX =
+      values.length > 1 ? (width - padding * 2) / (values.length - 1) : 0;
 
     return values
       .map((value, idx) => {
         const x = padding + idx * stepX;
-        const y = height - padding - ((value - minVal) / span) * (height - padding * 2);
+        const y =
+          height - padding - ((value - minVal) / span) * (height - padding * 2);
         return `${x},${y}`;
       })
       .join(' ');
@@ -300,10 +337,15 @@ export class DashboardComponent implements OnInit {
         this.activeUsersToday = dau.today;
         this.dauTrendLabels = dau.trend.map((item) => item.date);
         this.dauTrendData = dau.trend.map((item) => item.dau);
-        this.dauMin = this.dauTrendData.length ? Math.min(...this.dauTrendData) : 0;
-        this.dauMax = this.dauTrendData.length ? Math.max(...this.dauTrendData) : 0;
+        this.dauMin = this.dauTrendData.length
+          ? Math.min(...this.dauTrendData)
+          : 0;
+        this.dauMax = this.dauTrendData.length
+          ? Math.max(...this.dauTrendData)
+          : 0;
         this.dauTrendStartDate = this.dauTrendLabels[0] || '';
-        this.dauTrendEndDate = this.dauTrendLabels[this.dauTrendLabels.length - 1] || '';
+        this.dauTrendEndDate =
+          this.dauTrendLabels[this.dauTrendLabels.length - 1] || '';
         this.dauChartPoints = this.buildDauChartPoints(this.dauTrendData);
 
         this.lastUpdateTime = new Date();
@@ -352,10 +394,15 @@ export class DashboardComponent implements OnInit {
         this.activeUsersToday = dau.today;
         this.dauTrendLabels = dau.trend.map((item) => item.date);
         this.dauTrendData = dau.trend.map((item) => item.dau);
-        this.dauMin = this.dauTrendData.length ? Math.min(...this.dauTrendData) : 0;
-        this.dauMax = this.dauTrendData.length ? Math.max(...this.dauTrendData) : 0;
+        this.dauMin = this.dauTrendData.length
+          ? Math.min(...this.dauTrendData)
+          : 0;
+        this.dauMax = this.dauTrendData.length
+          ? Math.max(...this.dauTrendData)
+          : 0;
         this.dauTrendStartDate = this.dauTrendLabels[0] || '';
-        this.dauTrendEndDate = this.dauTrendLabels[this.dauTrendLabels.length - 1] || '';
+        this.dauTrendEndDate =
+          this.dauTrendLabels[this.dauTrendLabels.length - 1] || '';
         this.dauChartPoints = this.buildDauChartPoints(this.dauTrendData);
 
         await this.loadArtworkTabsData();

+ 32 - 29
omsapp/src/app/pages/message-dashboard.component.ts

@@ -280,7 +280,7 @@ export class MessageDashboardComponent implements OnInit {
     private http: HttpClient,
     private message: NzMessageService,
     private router: Router,
-    private cd: ChangeDetectorRef
+    private cd: ChangeDetectorRef,
   ) {
     Chart.register(...registerables);
   }
@@ -322,7 +322,7 @@ export class MessageDashboardComponent implements OnInit {
           console.error('Failed to load strategies:', err);
           this.message.error('加载策略列表失败');
           return of([]);
-        })
+        }),
       )
       .subscribe((data) => {
         this.strategies = data.map((item: any) => item.name).filter(Boolean);
@@ -343,18 +343,21 @@ export class MessageDashboardComponent implements OnInit {
     const params = new HttpParams()
       .set(
         'startDate',
-        this.dateRange[0] ? this.formatDateForApi(this.dateRange[0]) : ''
+        this.dateRange[0] ? this.formatDateForApi(this.dateRange[0]) : '',
       )
       .set(
         'endDate',
-        this.dateRange[1] ? this.formatDateForApi(this.dateRange[1]) : ''
+        this.dateRange[1] ? this.formatDateForApi(this.dateRange[1]) : '',
       )
       .set('strategyName', this.selectedStrategy || '')
       .set('page', '1')
       .set('limit', '50');
 
     const summaryRequestKey = params.toString();
-    if (this.summaryRequestInFlight && this.lastSummaryRequestKey === summaryRequestKey) {
+    if (
+      this.summaryRequestInFlight &&
+      this.lastSummaryRequestKey === summaryRequestKey
+    ) {
       return;
     }
 
@@ -380,7 +383,7 @@ export class MessageDashboardComponent implements OnInit {
           this.dailyTrendsLoading = false;
           this.strategyLoading = false;
           this.summaryRequestInFlight = false;
-        })
+        }),
       )
       .subscribe((summary) => {
         if (!summary) {
@@ -444,7 +447,7 @@ export class MessageDashboardComponent implements OnInit {
           this.message.error('加载策略统计失败');
           return of([]);
         }),
-        finalize(() => (this.strategyLoading = false))
+        finalize(() => (this.strategyLoading = false)),
       )
       .subscribe((data) => {
         this.strategyStats = data.map((item: any) => ({
@@ -467,7 +470,7 @@ export class MessageDashboardComponent implements OnInit {
           this.message.error('加载模板统计失败');
           return of([]);
         }),
-        finalize(() => (this.templateLoading = false))
+        finalize(() => (this.templateLoading = false)),
       )
       .subscribe((data) => {
         this.templateStats = data.map((item: any) => ({
@@ -490,7 +493,7 @@ export class MessageDashboardComponent implements OnInit {
           this.message.error('加载国家统计失败');
           return of([]);
         }),
-        finalize(() => (this.ccLoading = false))
+        finalize(() => (this.ccLoading = false)),
       )
       .subscribe((data) => {
         this.ccStats = data.map((item: any) => ({
@@ -513,7 +516,7 @@ export class MessageDashboardComponent implements OnInit {
           this.message.error('加载图片统计失败');
           return of([]);
         }),
-        finalize(() => (this.imageLoading = false))
+        finalize(() => (this.imageLoading = false)),
       )
       .subscribe((data) => {
         this.imageStats = data.map((item: any) => ({
@@ -532,11 +535,11 @@ export class MessageDashboardComponent implements OnInit {
     const params = new HttpParams()
       .set(
         'startDate',
-        this.dateRange[0] ? this.formatDateForApi(this.dateRange[0]) : ''
+        this.dateRange[0] ? this.formatDateForApi(this.dateRange[0]) : '',
       )
       .set(
         'endDate',
-        this.dateRange[1] ? this.formatDateForApi(this.dateRange[1]) : ''
+        this.dateRange[1] ? this.formatDateForApi(this.dateRange[1]) : '',
       )
       .set('strategyName', this.selectedStrategy || '');
 
@@ -598,7 +601,7 @@ export class MessageDashboardComponent implements OnInit {
     data: any[],
     field: string,
     currentSortField: string | null,
-    currentSortDirection: 'ascend' | 'descend' | null
+    currentSortDirection: 'ascend' | 'descend' | null,
   ): {
     sortedData: any[];
     newSortField: string;
@@ -647,7 +650,7 @@ export class MessageDashboardComponent implements OnInit {
       this.strategyStats,
       field,
       this.strategySortField,
-      this.strategySortDirection
+      this.strategySortDirection,
     );
     this.strategyStats = result.sortedData;
     this.strategySortField = result.newSortField;
@@ -660,7 +663,7 @@ export class MessageDashboardComponent implements OnInit {
       this.templateStats,
       field,
       this.templateSortField,
-      this.templateSortDirection
+      this.templateSortDirection,
     );
     this.templateStats = result.sortedData;
     this.templateSortField = result.newSortField;
@@ -673,7 +676,7 @@ export class MessageDashboardComponent implements OnInit {
       this.ccStats,
       field,
       this.ccSortField,
-      this.ccSortDirection
+      this.ccSortDirection,
     );
     this.ccStats = result.sortedData;
     this.ccSortField = result.newSortField;
@@ -686,7 +689,7 @@ export class MessageDashboardComponent implements OnInit {
       this.imageStats,
       field,
       this.imageSortField,
-      this.imageSortDirection
+      this.imageSortDirection,
     );
     this.imageStats = result.sortedData;
     this.imageSortField = result.newSortField;
@@ -731,25 +734,25 @@ export class MessageDashboardComponent implements OnInit {
         {
           ...this.combinedChartData.datasets[7],
           data: this.dailyTrends.map((t) =>
-            this.preciseRound((t.deliveredRate || 0) * 100, 2)
+            this.preciseRound((t.deliveredRate || 0) * 100, 2),
           ),
         },
         {
           ...this.combinedChartData.datasets[8],
           data: this.dailyTrends.map((t) =>
-            this.preciseRound((t.displayRate || 0) * 100, 2)
+            this.preciseRound((t.displayRate || 0) * 100, 2),
           ),
         },
         {
           ...this.combinedChartData.datasets[9],
           data: this.dailyTrends.map((t) =>
-            this.preciseRound((t.clickThroughRate || 0) * 100, 2)
+            this.preciseRound((t.clickThroughRate || 0) * 100, 2),
           ),
         },
         {
           ...this.combinedChartData.datasets[10],
           data: this.dailyTrends.map((t) =>
-            this.preciseRound((t.actualClickThroughRate || 0) * 100, 2)
+            this.preciseRound((t.actualClickThroughRate || 0) * 100, 2),
           ),
         },
       ],
@@ -802,7 +805,7 @@ export class MessageDashboardComponent implements OnInit {
   // 展开/折叠行并加载数据
   toggleExpand(
     element: any,
-    type: 'strategy' | 'template' | 'cc' | 'image'
+    type: 'strategy' | 'template' | 'cc' | 'image',
   ): void {
     element.expanded = !element.expanded;
 
@@ -824,11 +827,11 @@ export class MessageDashboardComponent implements OnInit {
     const params = new HttpParams()
       .set(
         'startDate',
-        this.dateRange[0] ? this.formatDateForApi(this.dateRange[0]) : ''
+        this.dateRange[0] ? this.formatDateForApi(this.dateRange[0]) : '',
       )
       .set(
         'endDate',
-        this.dateRange[1] ? this.formatDateForApi(this.dateRange[1]) : ''
+        this.dateRange[1] ? this.formatDateForApi(this.dateRange[1]) : '',
       )
       .set('strategyName', this.selectedStrategy || '');
 
@@ -836,22 +839,22 @@ export class MessageDashboardComponent implements OnInit {
     switch (type) {
       case 'strategy':
         url = `/api/message/daily/trends/by-strategy/${encodeURIComponent(
-          element.strategyName
+          element.strategyName,
         )}`;
         break;
       case 'template':
         url = `/api/message/daily/trends/by-template/${encodeURIComponent(
-          element.templateName
+          element.templateName,
         )}`;
         break;
       case 'cc':
         url = `/api/message/daily/trends/by-cc/${encodeURIComponent(
-          element.cc
+          element.cc,
         )}`;
         break;
       case 'image':
         url = `/api/message/daily/trends/by-image/${encodeURIComponent(
-          element.image
+          element.image,
         )}`;
         break;
       default:
@@ -872,7 +875,7 @@ export class MessageDashboardComponent implements OnInit {
         finalize(() => {
           element.loading = false;
           element.subscription = null;
-        })
+        }),
       )
       .subscribe((data) => {
         element.dailyData = data;

+ 20 - 5
omsapp/src/app/services/dashboard.service.ts

@@ -77,7 +77,9 @@ export class DashboardService {
     if (dateRange) {
       params = params.set('dateRange', dateRange.toString());
     }
-    return this.http.get<ApiResponse<KpiData>>('/api/dashboard/kpi', { params });
+    return this.http.get<ApiResponse<KpiData>>('/api/dashboard/kpi', {
+      params,
+    });
   }
 
   /**
@@ -88,21 +90,34 @@ export class DashboardService {
    */
   getArtworkStats(
     top: number = 10,
-    sortBy: 'clickRate' | 'completionRate' | 'clickCount' | 'completionCount' = 'clickCount'
+    sortBy:
+      | 'clickRate'
+      | 'completionRate'
+      | 'clickCount'
+      | 'completionCount' = 'clickCount',
   ): Observable<ApiResponse<ArtworkStatsResponse>> {
     let params = new HttpParams()
       .set('top', top.toString())
       .set('sortBy', sortBy);
-    return this.http.get<ApiResponse<ArtworkStatsResponse>>('/api/dashboard/artwork-stats', { params });
+    return this.http.get<ApiResponse<ArtworkStatsResponse>>(
+      '/api/dashboard/artwork-stats',
+      { params },
+    );
   }
 
   /**
    * 获取最近 N 天上新作品分日 tab 数据
    */
-  getNewArtworkTabs(days: number = 7, topPerDay: number = 20): Observable<ApiResponse<NewArtworkTabsResponse>> {
+  getNewArtworkTabs(
+    days: number = 7,
+    topPerDay: number = 20,
+  ): Observable<ApiResponse<NewArtworkTabsResponse>> {
     const params = new HttpParams()
       .set('days', days.toString())
       .set('topPerDay', topPerDay.toString());
-    return this.http.get<ApiResponse<NewArtworkTabsResponse>>('/api/dashboard/new-artwork-tabs', { params });
+    return this.http.get<ApiResponse<NewArtworkTabsResponse>>(
+      '/api/dashboard/new-artwork-tabs',
+      { params },
+    );
   }
 }