Переглянути джерело

message统计增加下钻查看每日详情功能

guoziyun 9 місяців тому
батько
коміт
6dc2ae6998

+ 1 - 1
oms/dist/services/cron-jobs/index.js

@@ -17,7 +17,7 @@ const settings = [
     // ["active-user-daily-notify", "30 18 * * *", require("./active-user-daily-notify") as CronJobModule], // 每天下午6点,开始活跃用户新作品消息推送
     // ["fcm-notify", "30 18 * * *", require("./fcm-notify") as CronJobModule], // 每天下午6点,基于原来的active-user-daily-notify,增加schedule推送,AB测试
     ["daily-notify-at-morning", "0 19 * * *", require("./notify/daily-notify-at-morning")], // 每天下午7点,对应巴西时间早上8点推送一轮
-    ["daily-notify-at-midday", "0 23 * * *", require("./notify/daily-notify-at-midday")], // 每天下午7点,对应巴西时间早上8点推送一轮
+    ["daily-notify-at-midday", "0 23 * * *", require("./notify/daily-notify-at-midday")], // 每天下午7点,对应巴西时间中午12点推送一轮
     ["daily-notify-at-evening", "0 3 * * *", require("./notify/daily-notify-at-evening")], // 每天凌晨3点,对应巴西时间下午4点
     ["daily-notify-at-afternoon", "0 7 * * *", require("./notify/daily-notify-at-afternoon")], // 每天上午7点,对应巴西时间晚上8点
 ];

+ 145 - 83
oms/dist/src/controllers/messageRecordController.js

@@ -13,8 +13,7 @@ class MessageRecordController {
          */
         this.createRecord = async (req, res) => {
             try {
-                const newRecord = new messageRecordModel_1.MessageRecord(req.body);
-                await newRecord.save();
+                const newRecord = await this.messageRecordService.createMessageRecord(req.body);
                 return res.status(201).json({ success: true, data: newRecord });
             }
             catch (error) {
@@ -28,74 +27,28 @@ class MessageRecordController {
          * Filters can be applied by uid, activityName, strategyName, templateName, status, and various date fields.
          * Date fields can be filtered by a single date or a range (e.g., "?createdAt=2023-01-01,2023-01-31").
          * @access Private
-         * @returns A JSON response with paginated records and pagination metadata.
+         * @returns A JSON response with paginated records and the total count.
          */
         this.getPaginatedRecords = async (req, res) => {
-            const { page = 1, limit = 30, uid, activityName, templateName, strategyName, status } = req.query;
-            const pageNum = parseInt(page, 10);
-            const limitNum = parseInt(limit, 10);
-            // Build the query filters dynamically based on request parameters
-            const filters = {};
-            if (uid) {
-                filters.uid = uid;
-            }
-            if (status) {
-                const statusNum = parseInt(status, 10);
-                if (!isNaN(statusNum)) {
-                    filters.status = statusNum;
-                }
-            }
-            if (activityName) {
-                filters.activityName = activityName;
-            }
-            if (strategyName) {
-                filters.strategyName = strategyName;
-            }
-            if (templateName) {
-                filters.templateName = templateName;
-            }
-            // List of all date fields that can be filtered
-            const dateQueryKeys = ["plannedSendAt", "actualSendAt", "deliveredAt", "openedAt", "createdAt", "updatedAt"];
-            // Iterate through date fields to handle single dates or date ranges
-            dateQueryKeys.forEach((key) => {
-                const queryValue = req.query[key];
-                if (queryValue) {
-                    const dates = queryValue.split(",");
-                    const startDate = new Date(dates[0]);
-                    const endDate = dates.length > 1 ? new Date(dates[1]) : null;
-                    if (!isNaN(startDate.getTime())) {
-                        if (endDate && !isNaN(endDate.getTime())) {
-                            // Filter by date range (e.g., plannedSendAt=2023-01-01,2023-01-31)
-                            filters[key] = {
-                                $gte: startDate,
-                                $lte: endDate,
-                            };
-                        }
-                        else {
-                            // Filter by a single date
-                            filters[key] = startDate;
-                        }
-                    }
-                    else {
-                        console.warn(`[API] Invalid date format for ${key}: ${queryValue}. Skipping.`);
-                    }
-                }
-            });
             try {
-                const records = await messageRecordModel_1.MessageRecord.find(filters)
-                    .sort({ createdAt: -1 })
-                    .skip((pageNum - 1) * limitNum)
-                    .limit(limitNum);
-                const total = await messageRecordModel_1.MessageRecord.countDocuments(filters);
-                return res.status(200).json({
+                const { page, limit, sortField, sortOrder, ...filters } = req.query;
+                const paginatedRecords = await this.messageRecordService.getPaginatedRecords(parseInt(page) || 1, parseInt(limit) || 10, filters, sortField, sortOrder);
+                // return res.status(200).json({
+                //         success: true,
+                //         data: records,
+                //         pagination: {
+                //           total,
+                //           page: pageNum,
+                //           limit: limitNum,
+                //           totalPages: Math.ceil(total / limitNum),
+                //         },
+                //       });
+                return res
+                    .status(200)
+                    .json({
                     success: true,
-                    data: records,
-                    pagination: {
-                        total,
-                        page: pageNum,
-                        limit: limitNum,
-                        totalPages: Math.ceil(total / limitNum),
-                    },
+                    data: paginatedRecords.records,
+                    pagination: { total: paginatedRecords.total, page: paginatedRecords.page, limit: paginatedRecords.limit, totalPages: paginatedRecords.totalPages },
                 });
             }
             catch (error) {
@@ -169,16 +122,17 @@ class MessageRecordController {
         };
         /**
          * @route GET /api/message/statistics/overall
-         * @desc Retrieves overall message push statistics
+         * @desc Retrieves overall message push statistics, with optional date and strategy filters.
          * @access Private
          * @returns A JSON response with overall statistics.
          */
         this.getOverallStatistics = async (req, res) => {
             try {
-                const { startDate, endDate } = req.query;
+                const { startDate, endDate, strategyName } = req.query;
                 const start = startDate ? new Date(startDate) : undefined;
                 const end = endDate ? new Date(endDate) : undefined;
-                const stats = await this.messageRecordService.getOverallStatistics(start, end);
+                const stratName = strategyName ? strategyName : undefined;
+                const stats = await this.messageRecordService.getOverallStatistics(start, end, stratName);
                 return res.status(200).json({ success: true, data: stats });
             }
             catch (error) {
@@ -213,10 +167,11 @@ class MessageRecordController {
          */
         this.getStatisticsByStrategy = async (req, res) => {
             try {
-                const { startDate, endDate } = req.query;
+                const { startDate, endDate, strategyName } = req.query;
                 const start = startDate ? new Date(startDate) : undefined;
                 const end = endDate ? new Date(endDate) : undefined;
-                const stats = await this.messageRecordService.getStatisticsByStrategy(start, end);
+                const stratName = strategyName ? strategyName : undefined;
+                const stats = await this.messageRecordService.getStatisticsByStrategy(start, end, stratName);
                 return res.status(200).json({ success: true, data: stats });
             }
             catch (error) {
@@ -226,16 +181,18 @@ class MessageRecordController {
         };
         /**
          * @route GET /api/message/statistics/by-template
-         * @desc Retrieves message push statistics grouped by template
+         * @desc Retrieves message push statistics grouped by template, with optional date and strategy filters.
          * @access Private
          * @returns A JSON response with template-based statistics.
          */
         this.getStatisticsByTemplate = async (req, res) => {
             try {
-                const { startDate, endDate } = req.query;
+                // 从查询参数中获取 startDate, endDate 和 strategyName
+                const { startDate, endDate, strategyName } = req.query;
                 const start = startDate ? new Date(startDate) : undefined;
                 const end = endDate ? new Date(endDate) : undefined;
-                const stats = await this.messageRecordService.getStatisticsByTemplate(start, end);
+                const stratName = strategyName ? strategyName : undefined;
+                const stats = await this.messageRecordService.getStatisticsByTemplate(start, end, stratName);
                 return res.status(200).json({ success: true, data: stats });
             }
             catch (error) {
@@ -245,16 +202,18 @@ class MessageRecordController {
         };
         /**
          * @route GET /api/message/statistics/by-cc
-         * @desc Retrieves message push statistics grouped by cc
+         * @desc Retrieves message push statistics grouped by cc, with optional date and strategy filters.
          * @access Private
          * @returns A JSON response with cc-based statistics.
          */
         this.getStatisticsByCc = async (req, res) => {
             try {
-                const { startDate, endDate } = req.query;
+                // 从查询参数中获取 startDate, endDate 和 strategyName
+                const { startDate, endDate, strategyName } = req.query;
                 const start = startDate ? new Date(startDate) : undefined;
                 const end = endDate ? new Date(endDate) : undefined;
-                const stats = await this.messageRecordService.getStatisticsByCc(start, end);
+                const stratName = strategyName ? strategyName : undefined;
+                const stats = await this.messageRecordService.getStatisticsByCc(start, end, stratName);
                 return res.status(200).json({ success: true, data: stats });
             }
             catch (error) {
@@ -264,16 +223,18 @@ class MessageRecordController {
         };
         /**
          * @route GET /api/message/statistics/by-image
-         * @desc Retrieves message push statistics grouped by image
+         * @desc Retrieves message push statistics grouped by image, with optional date and strategy filters.
          * @access Private
          * @returns A JSON response with image-based statistics.
          */
         this.getStatisticsByImage = async (req, res) => {
             try {
-                const { startDate, endDate } = req.query;
+                // 从查询参数中获取 startDate, endDate 和 strategyName
+                const { startDate, endDate, strategyName } = req.query;
                 const start = startDate ? new Date(startDate) : undefined;
                 const end = endDate ? new Date(endDate) : undefined;
-                const stats = await this.messageRecordService.getStatisticsByImage(start, end);
+                const stratName = strategyName ? strategyName : undefined;
+                const stats = await this.messageRecordService.getStatisticsByImage(start, end, stratName);
                 return res.status(200).json({ success: true, data: stats });
             }
             catch (error) {
@@ -283,16 +244,18 @@ class MessageRecordController {
         };
         /**
          * @route GET /api/message/statistics/daily-trends
-         * @desc Retrieves daily sent trend statistics
+         * @desc Retrieves daily sent trend statistics, with optional date and strategy filters.
          * @access Private
          * @returns A JSON response with daily trend statistics.
          */
         this.getDailySentTrends = async (req, res) => {
             try {
-                const { startDate, endDate } = req.query;
+                // 从查询参数中获取 startDate, endDate 和 strategyName
+                const { startDate, endDate, strategyName } = req.query;
                 const start = startDate ? new Date(startDate) : undefined;
                 const end = endDate ? new Date(endDate) : undefined;
-                const stats = await this.messageRecordService.getDailySentTrends(start, end);
+                const stratName = strategyName ? strategyName : undefined;
+                const stats = await this.messageRecordService.getDailySentTrends(start, end, stratName);
                 return res.status(200).json({ success: true, data: stats });
             }
             catch (error) {
@@ -330,6 +293,105 @@ class MessageRecordController {
                 return res.status(500).json({ success: false, message: "Server error", error: error.message });
             }
         };
+        /**
+         * @route GET /api/message/daily/trends/by-cc/:cc
+         * @desc Retrieves daily message trends by country code
+         * @access Private
+         * @returns A JSON response with daily statistics.
+         */
+        this.getDailyTrendsByCc = async (req, res) => {
+            try {
+                const { cc } = req.params;
+                // 从查询参数中获取 startDate, endDate 和 strategyName
+                const { startDate, endDate, strategyName } = req.query;
+                const start = startDate ? new Date(startDate) : undefined;
+                const end = endDate ? new Date(endDate) : undefined;
+                const stratName = strategyName ? strategyName : undefined;
+                if (!cc) {
+                    return res.status(400).json({ success: false, message: "Country code is required." });
+                }
+                const stats = await this.messageRecordService.getDailyTrendsByCc(cc, start, end, stratName);
+                return res.status(200).json({ success: true, data: stats });
+            }
+            catch (error) {
+                console.error("Error fetching daily trends by cc:", error);
+                return res.status(500).json({ success: false, message: "Server error", error: error.message });
+            }
+        };
+        /**
+         * @route GET /api/message/daily/trends/by-strategy/:strategyName
+         * @desc Retrieves daily message trends by strategy name
+         * @access Private
+         * @returns A JSON response with daily statistics.
+         */
+        this.getDailyTrendsByStrategy = async (req, res) => {
+            try {
+                const { strategyName } = req.params;
+                // 从查询参数中获取 startDate, endDate
+                const { startDate, endDate } = req.query;
+                const start = startDate ? new Date(startDate) : undefined;
+                const end = endDate ? new Date(endDate) : undefined;
+                if (!strategyName) {
+                    return res.status(400).json({ success: false, message: "Strategy name is required." });
+                }
+                const stats = await this.messageRecordService.getDailyTrendsByStrategy(strategyName, start, end);
+                return res.status(200).json({ success: true, data: stats });
+            }
+            catch (error) {
+                console.error("Error fetching daily trends by strategy:", error);
+                return res.status(500).json({ success: false, message: "Server error", error: error.message });
+            }
+        };
+        /**
+         * @route GET /api/message/daily/trends/by-template/:templateName
+         * @desc Retrieves daily message trends by template ID
+         * @access Private
+         * @returns A JSON response with daily statistics.
+         */
+        this.getDailyTrendsByTemplate = async (req, res) => {
+            try {
+                const { templateName } = req.params;
+                // 从查询参数中获取 startDate, endDate 和 strategyName
+                const { startDate, endDate, strategyName } = req.query;
+                const start = startDate ? new Date(startDate) : undefined;
+                const end = endDate ? new Date(endDate) : undefined;
+                const stratName = strategyName ? strategyName : undefined;
+                if (!templateName) {
+                    return res.status(400).json({ success: false, message: "Template Name is required." });
+                }
+                const stats = await this.messageRecordService.getDailyTrendsByTemplate(templateName, start, end, stratName);
+                return res.status(200).json({ success: true, data: stats });
+            }
+            catch (error) {
+                console.error("Error fetching daily trends by template:", error);
+                return res.status(500).json({ success: false, message: "Server error", error: error.message });
+            }
+        };
+        /**
+         * @route GET /api/message/daily/trends/by-image/:image
+         * @desc Retrieves daily message trends by image URL
+         * @access Private
+         * @returns A JSON response with daily statistics.
+         */
+        this.getDailyTrendsByImage = async (req, res) => {
+            try {
+                const { image } = req.params;
+                // 从查询参数中获取 startDate, endDate 和 strategyName
+                const { startDate, endDate, strategyName } = req.query;
+                const start = startDate ? new Date(startDate) : undefined;
+                const end = endDate ? new Date(endDate) : undefined;
+                const stratName = strategyName ? strategyName : undefined;
+                if (!image) {
+                    return res.status(400).json({ success: false, message: "Image URL is required." });
+                }
+                const stats = await this.messageRecordService.getDailyTrendsByImage(image, start, end, stratName);
+                return res.status(200).json({ success: true, data: stats });
+            }
+            catch (error) {
+                console.error("Error fetching daily trends by image:", error);
+                return res.status(500).json({ success: false, message: "Server error", error: error.message });
+            }
+        };
         this.messageRecordService = new messageRecordService_1.MessageRecordService();
     }
 }

+ 5 - 0
oms/dist/src/routes/apiRoutes.js

@@ -28,6 +28,11 @@ router.get("/message/statistics/by-cc", messageRecordController_1.default.getSta
 router.get("/message/statistics/by-image", messageRecordController_1.default.getStatisticsByImage);
 router.get("/message/statistics/daily-trends", messageRecordController_1.default.getDailySentTrends);
 router.get("/message/statistics/multi-dimensional", messageRecordController_1.default.getMultiDimensionalStatistics); // 新增:多维度统计路由
+// 新增:下钻统计路由
+router.get("/message/daily/trends/by-cc/:cc", messageRecordController_1.default.getDailyTrendsByCc);
+router.get("/message/daily/trends/by-strategy/:strategyName", messageRecordController_1.default.getDailyTrendsByStrategy);
+router.get("/message/daily/trends/by-template/:templateName", messageRecordController_1.default.getDailyTrendsByTemplate);
+router.get("/message/daily/trends/by-image/:image", messageRecordController_1.default.getDailyTrendsByImage);
 // 应用认证中间件,保护所有下面的路由
 router.use(authMiddleware_1.authMiddleware);
 // User routes

+ 688 - 228
oms/dist/src/services/messageRecordService.js

@@ -36,12 +36,59 @@ class MessageRecordService {
         if (filters.status !== undefined) {
             query.status = filters.status;
         }
-        const sort = {};
-        sort[sortField] = sortOrder === "asc" ? 1 : -1;
-        const skip = (page - 1) * limit;
-        const records = await messageRecordModel_1.MessageRecord.find(query).sort(sort).skip(skip).limit(limit);
+        if (filters.createdAt) {
+            const dates = filters.createdAt.split(",");
+            const startDate = new Date(dates[0]);
+            if (dates.length > 1) {
+                const endDate = new Date(dates[1]);
+                query.createdAt = { $gte: startDate, $lte: endDate };
+            }
+            else {
+                query.createdAt = { $gte: startDate };
+            }
+        }
+        if (filters.sentAt) {
+            const dates = filters.sentAt.split(",");
+            const startDate = new Date(dates[0]);
+            if (dates.length > 1) {
+                const endDate = new Date(dates[1]);
+                query.sentAt = { $gte: startDate, $lte: endDate };
+            }
+            else {
+                query.sentAt = { $gte: startDate };
+            }
+        }
+        if (filters.displayedAt) {
+            const dates = filters.displayedAt.split(",");
+            const startDate = new Date(dates[0]);
+            if (dates.length > 1) {
+                const endDate = new Date(dates[1]);
+                query.displayedAt = { $gte: startDate, $lte: endDate };
+            }
+            else {
+                query.displayedAt = { $gte: startDate };
+            }
+        }
+        if (filters.openedAt) {
+            const dates = filters.openedAt.split(",");
+            const startDate = new Date(dates[0]);
+            if (dates.length > 1) {
+                const endDate = new Date(dates[1]);
+                query.openedAt = { $gte: startDate, $lte: endDate };
+            }
+            else {
+                query.openedAt = { $gte: startDate };
+            }
+        }
+        const sortOption = {};
+        sortOption[sortField] = sortOrder === "asc" ? 1 : -1;
         const total = await messageRecordModel_1.MessageRecord.countDocuments(query);
-        return { records, total };
+        const records = await messageRecordModel_1.MessageRecord.find(query)
+            .sort(sortOption)
+            .skip((page - 1) * limit)
+            .limit(limit);
+        const totalPages = Math.ceil(total / limit);
+        return { records, total, page, limit, totalPages };
     }
     /**
      * 根据用户UID获取其所有消息推送记录
@@ -80,41 +127,37 @@ class MessageRecordService {
      * 获取整体消息推送统计数据
      * @param startDate 可选的开始日期
      * @param endDate 可选的结束日期
+     * @param strategyName 可选的策略名称筛选
      */
-    async getOverallStatistics(startDate, endDate) {
+    async getOverallStatistics(startDate, endDate, strategyName) {
         try {
             const pipeline = [];
-            // 如果提供了日期,添加 $match 阶段
+            const matchConditions = {};
+            // 添加日期筛选条件
             if (startDate && endDate) {
                 // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
                 if (endDate.getTime() <= startDate.getTime()) {
                     endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
                 }
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                            $lte: endDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                    $lte: endDate,
+                };
             }
             else if (startDate) {
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = { $gte: startDate };
             }
             else if (endDate) {
+                matchConditions.createdAt = { $lte: endDate };
+            }
+            // 添加 strategyName 筛选条件
+            if (strategyName) {
+                matchConditions.strategyName = strategyName;
+            }
+            // 如果存在任何筛选条件,则添加 $match 阶段
+            if (Object.keys(matchConditions).length > 0) {
                 pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $lte: endDate,
-                        },
-                    },
+                    $match: matchConditions,
                 });
             }
             pipeline.push(
@@ -320,41 +363,41 @@ class MessageRecordService {
      * 按策略获取消息统计数据
      * @param startDate 可选的开始日期
      * @param endDate 可选的结束日期
+     * @param strategyName 可选的策略名称
      */
-    async getStatisticsByStrategy(startDate, endDate) {
+    async getStatisticsByStrategy(startDate, endDate, strategyName) {
         try {
             const pipeline = [];
-            // 如果提供了日期,添加 $match 阶段
+            const matchConditions = {};
+            // 如果提供了日期,添加日期筛选条件
             if (startDate && endDate) {
                 // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
                 if (endDate.getTime() <= startDate.getTime()) {
                     endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
                 }
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                            $lte: endDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                    $lte: endDate,
+                };
             }
             else if (startDate) {
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                };
             }
             else if (endDate) {
+                matchConditions.createdAt = {
+                    $lte: endDate,
+                };
+            }
+            // 如果提供了 strategyName,添加策略名称筛选条件
+            if (strategyName) {
+                matchConditions.strategyName = strategyName;
+            }
+            // 如果有任何筛选条件,将 $match 阶段添加到管道
+            if (Object.keys(matchConditions).length > 0) {
                 pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $lte: endDate,
-                        },
-                    },
+                    $match: matchConditions,
                 });
             }
             pipeline.push(
@@ -446,41 +489,41 @@ class MessageRecordService {
      * 按模板获取消息统计数据
      * @param startDate 可选的开始日期
      * @param endDate 可选的结束日期
+     * @param strategyName 可选的策略名称
      */
-    async getStatisticsByTemplate(startDate, endDate) {
+    async getStatisticsByTemplate(startDate, endDate, strategyName) {
         try {
             const pipeline = [];
-            // 如果提供了日期,添加 $match 阶段
+            const matchConditions = {};
+            // 如果提供了日期,添加日期筛选条件
             if (startDate && endDate) {
                 // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
                 if (endDate.getTime() <= startDate.getTime()) {
                     endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
                 }
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                            $lte: endDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                    $lte: endDate,
+                };
             }
             else if (startDate) {
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                };
             }
             else if (endDate) {
+                matchConditions.createdAt = {
+                    $lte: endDate,
+                };
+            }
+            // 如果提供了 strategyName,添加策略名称筛选条件
+            if (strategyName) {
+                matchConditions.strategyName = strategyName;
+            }
+            // 如果有任何筛选条件,将 $match 阶段添加到管道
+            if (Object.keys(matchConditions).length > 0) {
                 pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $lte: endDate,
-                        },
-                    },
+                    $match: matchConditions,
                 });
             }
             pipeline.push(
@@ -572,41 +615,37 @@ class MessageRecordService {
      * 按时间维度的趋势分析,每日统计
      * @param startDate 可选的开始日期
      * @param endDate 可选的结束日期
+     * @param strategyName 可选的策略名称筛选
      */
-    async getDailySentTrends(startDate, endDate) {
+    async getDailySentTrends(startDate, endDate, strategyName) {
         try {
             const pipeline = [];
-            // 如果提供了日期,添加 $match 阶段
+            const matchConditions = {};
+            // 添加日期筛选条件
             if (startDate && endDate) {
                 // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
                 if (endDate.getTime() <= startDate.getTime()) {
                     endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
                 }
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                            $lte: endDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                    $lte: endDate,
+                };
             }
             else if (startDate) {
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = { $gte: startDate };
             }
             else if (endDate) {
+                matchConditions.createdAt = { $lte: endDate };
+            }
+            // 添加 strategyName 筛选条件
+            if (strategyName) {
+                matchConditions.strategyName = strategyName;
+            }
+            // 如果存在任何筛选条件,则添加 $match 阶段
+            if (Object.keys(matchConditions).length > 0) {
                 pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $lte: endDate,
-                        },
-                    },
+                    $match: matchConditions,
                 });
             }
             pipeline.push(
@@ -699,90 +738,91 @@ class MessageRecordService {
         }
     }
     /**
-     * 按多个维度获取消息推送统计数据。
-     * 支持日期范围、模板、图片、国家和策略的组合查询。
-     * @param filters 包含查询条件的过滤器对象
-     * @returns 多维度统计结果
+     * 按国家代码获取消息统计数据
+     * @param startDate 可选的开始日期
+     * @param endDate 可选的结束日期
+     * @param strategyName 可选的策略名称筛选
      */
-    async getMultiDimensionalStatistics(filters) {
+    async getStatisticsByCc(startDate, endDate, strategyName) {
         try {
             const pipeline = [];
-            // 1. $match (筛选阶段)
-            const matchStage = {};
-            if (filters.startDate || filters.endDate) {
-                matchStage.createdAt = {};
-                if (filters.startDate) {
-                    matchStage.createdAt.$gte = filters.startDate;
-                }
-                if (filters.endDate) {
-                    // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
-                    if (filters.startDate && filters.endDate.getTime() <= filters.startDate.getTime()) {
-                        filters.endDate = new Date(filters.startDate.getTime() + 24 * 60 * 60 * 1000);
-                    }
-                    matchStage.createdAt.$lte = filters.endDate;
+            const matchConditions = {};
+            // 添加日期筛选条件
+            if (startDate && endDate) {
+                // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
+                if (endDate.getTime() <= startDate.getTime()) {
+                    endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
                 }
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                    $lte: endDate,
+                };
             }
-            if (filters.templateName) {
-                matchStage.templateName = filters.templateName;
-            }
-            if (filters.strategyName) {
-                matchStage.strategyName = filters.strategyName;
+            else if (startDate) {
+                matchConditions.createdAt = { $gte: startDate };
             }
-            if (filters.cc) {
-                matchStage.cc = filters.cc;
+            else if (endDate) {
+                matchConditions.createdAt = { $lte: endDate };
             }
-            if (filters.image) {
-                matchStage.image = filters.image;
+            // 添加 strategyName 筛选条件
+            if (strategyName) {
+                matchConditions.strategyName = strategyName;
             }
-            if (Object.keys(matchStage).length > 0) {
-                pipeline.push({ $match: matchStage });
+            // 如果存在任何筛选条件,则添加 $match 阶段
+            if (Object.keys(matchConditions).length > 0) {
+                pipeline.push({
+                    $match: matchConditions,
+                });
             }
-            // 2. $group (分组和聚合阶段)
-            pipeline.push({
+            pipeline.push(
+            // 1. 根据 cc, status, strategyName 和 inforeground 进行分组
+            {
                 $group: {
                     _id: {
-                        date: { $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" } },
-                        templateName: "$templateName",
-                        strategyName: "$strategyName",
                         cc: "$cc",
-                        image: "$image",
                         status: "$status",
                         inforeground: "$inforeground",
+                        strategyName: "$strategyName", // 增加 strategyName
                     },
                     count: { $sum: 1 },
                 },
-            });
-            // 3. 第二次 $group (汇总阶段)
-            pipeline.push({
+            }, 
+            // 2. 将数据重组,以便按 cc 汇总
+            {
                 $group: {
-                    _id: {
-                        date: "$_id.date",
-                        templateName: "$_id.templateName",
-                        strategyName: "$_id.strategyName",
-                        cc: "$_id.cc",
-                        image: "$_id.image",
-                    },
+                    _id: "$_id.cc",
+                    cc: { $first: "$_id.cc" },
                     totalRecords: { $sum: "$count" },
-                    sent: { $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] } },
-                    delivered: { $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] } },
-                    opened: { $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] } },
-                    failed: { $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] } },
+                    sent: {
+                        $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] },
+                    },
+                    delivered: {
+                        $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] },
+                    },
+                    opened: {
+                        $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] },
+                    },
+                    failed: {
+                        $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] },
+                    },
                     displayCount: {
                         $sum: {
-                            $cond: [{ $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }] }, "$count", 0],
+                            $cond: [
+                                {
+                                    $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }],
+                                },
+                                "$count",
+                                0,
+                            ],
                         },
                     },
                 },
-            });
-            // 4. $project (计算比率和格式化输出)
-            pipeline.push({
+            }, 
+            // 3. 计算比率并格式化输出
+            {
                 $project: {
                     _id: 0,
-                    date: "$_id.date",
-                    templateName: "$_id.templateName",
-                    strategyName: "$_id.strategyName",
-                    cc: "$_id.cc",
-                    image: "$_id.image",
+                    cc: "$cc",
                     totalRecords: "$totalRecords",
                     sent: "$sent",
                     delivered: "$delivered",
@@ -805,77 +845,74 @@ class MessageRecordService {
                         $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$failed", "$totalRecords"] }],
                     },
                 },
-            });
-            // 5. $sort (排序)
-            pipeline.push({
-                $sort: { date: 1, templateName: 1, cc: 1, image: 1 },
+            }, 
+            // 4. 按 deliveredRate 降序排序
+            {
+                $sort: { deliveredRate: -1 },
             });
             const results = await messageRecordModel_1.MessageRecord.aggregate(pipeline);
             return results;
         }
         catch (error) {
-            console.error("Error fetching multi-dimensional statistics:", error);
+            console.error("Error fetching statistics by cc:", error);
             return [];
         }
     }
     /**
-     * 按国家代码获取消息统计数据
+     * 按图片 URL 获取消息统计数据
      * @param startDate 可选的开始日期
      * @param endDate 可选的结束日期
+     * @param strategyName 可选的策略名称筛选
      */
-    async getStatisticsByCc(startDate, endDate) {
+    async getStatisticsByImage(startDate, endDate, strategyName) {
         try {
             const pipeline = [];
-            // 如果提供了日期,添加 $match 阶段
+            const matchConditions = {};
+            // 添加日期筛选条件
             if (startDate && endDate) {
                 // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
                 if (endDate.getTime() <= startDate.getTime()) {
                     endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
                 }
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                            $lte: endDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                    $lte: endDate,
+                };
             }
             else if (startDate) {
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = { $gte: startDate };
             }
             else if (endDate) {
+                matchConditions.createdAt = { $lte: endDate };
+            }
+            // 添加 strategyName 筛选条件
+            if (strategyName) {
+                matchConditions.strategyName = strategyName;
+            }
+            // 如果存在任何筛选条件,则添加 $match 阶段
+            if (Object.keys(matchConditions).length > 0) {
                 pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $lte: endDate,
-                        },
-                    },
+                    $match: matchConditions,
                 });
             }
             pipeline.push(
-            // 1. 根据 cc, status, 和 inforeground 进行分组
+            // 1. 根据 image, status, strategyName 和 inforeground 进行分组
             {
                 $group: {
                     _id: {
-                        cc: "$cc",
+                        image: "$image",
                         status: "$status",
                         inforeground: "$inforeground",
+                        strategyName: "$strategyName", // 增加 strategyName
                     },
                     count: { $sum: 1 },
                 },
             }, 
-            // 2. 将数据重组,以便按 cc 汇总
+            // 2. 将数据重组,以便按 image 汇总
             {
                 $group: {
-                    _id: "$_id.cc",
-                    cc: { $first: "$_id.cc" },
+                    _id: "$_id.image",
+                    image: { $first: "$_id.image" },
                     totalRecords: { $sum: "$count" },
                     sent: {
                         $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] },
@@ -906,7 +943,7 @@ class MessageRecordService {
             {
                 $project: {
                     _id: 0,
-                    cc: "$cc",
+                    image: "$image",
                     totalRecords: "$totalRecords",
                     sent: "$sent",
                     delivered: "$delivered",
@@ -938,68 +975,315 @@ class MessageRecordService {
             return results;
         }
         catch (error) {
-            console.error("Error fetching statistics by cc:", error);
+            console.error("Error fetching statistics by image:", error);
             return [];
         }
     }
     /**
-     * 按图片 URL 获取消息统计数据
-     * @param startDate 可选的开始日期
-     * @param endDate 可选的结束日期
-     */
-    async getStatisticsByImage(startDate, endDate) {
-        try {
-            const pipeline = [];
-            // 如果提供了日期,添加 $match 阶段
+     * 按多个维度获取消息推送统计数据。
+     * 支持日期范围、模板、图片、国家和策略的组合查询。
+     * @param filters 包含查询条件的过滤器对象
+     * @returns 多维度统计结果
+     */
+    async getMultiDimensionalStatistics(filters) {
+        try {
+            const pipeline = [];
+            // 1. $match (筛选阶段)
+            const matchStage = {};
+            if (filters.startDate || filters.endDate) {
+                matchStage.createdAt = {};
+                if (filters.startDate) {
+                    matchStage.createdAt.$gte = filters.startDate;
+                }
+                if (filters.endDate) {
+                    // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
+                    if (filters.startDate && filters.endDate.getTime() <= filters.startDate.getTime()) {
+                        filters.endDate = new Date(filters.startDate.getTime() + 24 * 60 * 60 * 1000);
+                    }
+                    matchStage.createdAt.$lte = filters.endDate;
+                }
+            }
+            if (filters.templateName) {
+                matchStage.templateName = filters.templateName;
+            }
+            if (filters.strategyName) {
+                matchStage.strategyName = filters.strategyName;
+            }
+            if (filters.cc) {
+                matchStage.cc = filters.cc;
+            }
+            if (filters.image) {
+                matchStage.image = filters.image;
+            }
+            if (Object.keys(matchStage).length > 0) {
+                pipeline.push({ $match: matchStage });
+            }
+            // 2. $group (分组和聚合阶段)
+            pipeline.push({
+                $group: {
+                    _id: {
+                        date: { $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" } },
+                        templateName: "$templateName",
+                        strategyName: "$strategyName",
+                        cc: "$cc",
+                        image: "$image",
+                        status: "$status",
+                        inforeground: "$inforeground",
+                    },
+                    count: { $sum: 1 },
+                },
+            });
+            // 3. 第二次 $group (汇总阶段)
+            pipeline.push({
+                $group: {
+                    _id: {
+                        date: "$_id.date",
+                        templateName: "$_id.templateName",
+                        strategyName: "$_id.strategyName",
+                        cc: "$_id.cc",
+                        image: "$_id.image",
+                    },
+                    totalRecords: { $sum: "$count" },
+                    sent: { $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] } },
+                    delivered: { $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] } },
+                    opened: { $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] } },
+                    failed: { $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] } },
+                    displayCount: {
+                        $sum: {
+                            $cond: [{ $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }] }, "$count", 0],
+                        },
+                    },
+                },
+            });
+            // 4. $project (计算比率和格式化输出)
+            pipeline.push({
+                $project: {
+                    _id: 0,
+                    date: "$_id.date",
+                    templateName: "$_id.templateName",
+                    strategyName: "$_id.strategyName",
+                    cc: "$_id.cc",
+                    image: "$_id.image",
+                    totalRecords: "$totalRecords",
+                    sent: "$sent",
+                    delivered: "$delivered",
+                    opened: "$opened",
+                    failed: "$failed",
+                    displayCount: "$displayCount",
+                    sentSuccessRate: {
+                        $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$sent", "$totalRecords"] }],
+                    },
+                    deliveredRate: {
+                        $cond: [{ $eq: ["$sent", 0] }, 0, { $divide: ["$delivered", "$sent"] }],
+                    },
+                    displayRate: {
+                        $cond: [{ $eq: ["$delivered", 0] }, 0, { $divide: ["$displayCount", "$delivered"] }],
+                    },
+                    clickThroughRate: {
+                        $cond: [{ $eq: ["$displayCount", 0] }, 0, { $divide: ["$opened", "$displayCount"] }],
+                    },
+                    tokenInvalidationRate: {
+                        $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$failed", "$totalRecords"] }],
+                    },
+                },
+            });
+            // 5. $sort (排序)
+            pipeline.push({
+                $sort: { date: 1, templateName: 1, cc: 1, image: 1 },
+            });
+            const results = await messageRecordModel_1.MessageRecord.aggregate(pipeline);
+            return results;
+        }
+        catch (error) {
+            console.error("Error fetching multi-dimensional statistics:", error);
+            return [];
+        }
+    }
+    /**
+     * 按国家代码和时间维度获取每日统计数据
+     * @param cc 必须提供的国家代码
+     * @param startDate 可选的开始日期
+     * @param endDate 可选的结束日期
+     * @param strategyName 可选的策略名称筛选
+     * @returns 每日统计数据列表
+     */
+    async getDailyTrendsByCc(cc, startDate, endDate, strategyName) {
+        try {
+            const pipeline = [];
+            const matchConditions = {
+                cc: cc,
+            };
+            // 添加日期筛选条件
             if (startDate && endDate) {
                 // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
                 if (endDate.getTime() <= startDate.getTime()) {
                     endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
                 }
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                            $lte: endDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                    $lte: endDate,
+                };
             }
             else if (startDate) {
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $gte: startDate,
-                        },
-                    },
-                });
+                matchConditions.createdAt = { $gte: startDate };
             }
             else if (endDate) {
-                pipeline.push({
-                    $match: {
-                        createdAt: {
-                            $lte: endDate,
+                matchConditions.createdAt = { $lte: endDate };
+            }
+            // 添加 strategyName 筛选条件
+            if (strategyName) {
+                matchConditions.strategyName = strategyName;
+            }
+            // 添加 $match 阶段
+            pipeline.push({
+                $match: matchConditions,
+            });
+            pipeline.push(
+            // 1. 将 createdAt 字段转换为日期,忽略时分秒
+            {
+                $project: {
+                    _id: 0,
+                    date: {
+                        $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" },
+                    },
+                    status: "$status",
+                    inforeground: "$inforeground",
+                },
+            }, 
+            // 2. 根据日期、状态和 inforeground 进行分组
+            {
+                $group: {
+                    _id: { date: "$date", status: "$status", inforeground: "$inforeground" },
+                    count: { $sum: 1 },
+                },
+            }, 
+            // 3. 将数据重组,以便按日期汇总
+            {
+                $group: {
+                    _id: "$_id.date",
+                    totalRecords: { $sum: "$count" },
+                    sent: {
+                        $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] },
+                    },
+                    delivered: {
+                        $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] },
+                    },
+                    opened: {
+                        $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] },
+                    },
+                    failed: {
+                        $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] },
+                    },
+                    displayCount: {
+                        $sum: {
+                            $cond: [
+                                {
+                                    $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }],
+                                },
+                                "$count",
+                                0,
+                            ],
                         },
                     },
-                });
+                },
+            }, 
+            // 4. 计算比率并格式化输出
+            {
+                $project: {
+                    _id: 0,
+                    date: "$_id",
+                    totalRecords: "$totalRecords",
+                    sent: "$sent",
+                    delivered: "$delivered",
+                    opened: "$opened",
+                    failed: "$failed",
+                    displayCount: "$displayCount",
+                    sentSuccessRate: {
+                        $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$sent", "$totalRecords"] }],
+                    },
+                    deliveredRate: {
+                        $cond: [{ $eq: ["$sent", 0] }, 0, { $divide: ["$delivered", "$sent"] }],
+                    },
+                    displayRate: {
+                        $cond: [{ $eq: ["$delivered", 0] }, 0, { $divide: ["$displayCount", "$delivered"] }],
+                    },
+                    clickThroughRate: {
+                        $cond: [{ $eq: ["$displayCount", 0] }, 0, { $divide: ["$opened", "$displayCount"] }],
+                    },
+                    tokenInvalidationRate: {
+                        $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$failed", "$totalRecords"] }],
+                    },
+                },
+            }, 
+            // 5. 按日期降序排序
+            {
+                $sort: { date: -1 },
+            });
+            const results = await messageRecordModel_1.MessageRecord.aggregate(pipeline);
+            return results;
+        }
+        catch (error) {
+            console.error("Error fetching daily trends by cc:", error);
+            return [];
+        }
+    }
+    /**
+     * 按策略名称和时间维度获取每日统计数据
+     * @param strategyName 必须提供的策略名称
+     * @param startDate 可选的开始日期
+     * @param endDate 可选的结束日期
+     * @returns 每日统计数据列表
+     */
+    async getDailyTrendsByStrategy(strategyName, startDate, endDate) {
+        try {
+            const pipeline = [];
+            const matchConditions = {
+                strategyName: strategyName,
+            };
+            // 添加日期筛选条件
+            if (startDate && endDate) {
+                // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
+                if (endDate.getTime() <= startDate.getTime()) {
+                    endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
+                }
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                    $lte: endDate,
+                };
+            }
+            else if (startDate) {
+                matchConditions.createdAt = { $gte: startDate };
+            }
+            else if (endDate) {
+                matchConditions.createdAt = { $lte: endDate };
             }
+            // 添加 $match 阶段
+            pipeline.push({
+                $match: matchConditions,
+            });
             pipeline.push(
-            // 1. 根据 image, status, 和 inforeground 进行分组
+            // 1. 将 createdAt 字段转换为日期,忽略时分秒
             {
-                $group: {
-                    _id: {
-                        image: "$image",
-                        status: "$status",
-                        inforeground: "$inforeground",
+                $project: {
+                    _id: 0,
+                    date: {
+                        $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" },
                     },
+                    status: "$status",
+                    inforeground: "$inforeground",
+                },
+            }, 
+            // 2. 根据日期、状态和 inforeground 进行分组
+            {
+                $group: {
+                    _id: { date: "$date", status: "$status", inforeground: "$inforeground" },
                     count: { $sum: 1 },
                 },
             }, 
-            // 2. 将数据重组,以便按 image 汇总
+            // 3. 将数据重组,以便按日期汇总
             {
                 $group: {
-                    _id: "$_id.image",
-                    image: { $first: "$_id.image" },
+                    _id: "$_id.date",
                     totalRecords: { $sum: "$count" },
                     sent: {
                         $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] },
@@ -1026,11 +1310,11 @@ class MessageRecordService {
                     },
                 },
             }, 
-            // 3. 计算比率并格式化输出
+            // 4. 计算比率并格式化输出
             {
                 $project: {
                     _id: 0,
-                    image: "$image",
+                    date: "$_id",
                     totalRecords: "$totalRecords",
                     sent: "$sent",
                     delivered: "$delivered",
@@ -1054,15 +1338,191 @@ class MessageRecordService {
                     },
                 },
             }, 
-            // 4. 按 deliveredRate 降序排序
+            // 5. 按日期升序排序(统一为升序,与getDailyTrendsByCc保持一致)
             {
-                $sort: { deliveredRate: -1 },
+                $sort: { date: -1 },
             });
             const results = await messageRecordModel_1.MessageRecord.aggregate(pipeline);
             return results;
         }
         catch (error) {
-            console.error("Error fetching statistics by image:", error);
+            console.error("Error fetching daily trends by strategy:", error);
+            return [];
+        }
+    }
+    /**
+     * 按模板ID和时间维度获取每日统计数据
+     * @param templateName 必须提供的模板ID
+     * @param startDate 可选的开始日期
+     * @param endDate 可选的结束日期
+     * @param strategyName 可选的策略名称筛选
+     * @returns 每日统计数据列表
+     */
+    async getDailyTrendsByTemplate(templateName, startDate, endDate, strategyName) {
+        try {
+            const pipeline = [];
+            const matchConditions = {
+                templateName: templateName,
+            };
+            if (startDate && endDate) {
+                if (endDate.getTime() <= startDate.getTime()) {
+                    endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
+                }
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                    $lte: endDate,
+                };
+            }
+            else if (startDate) {
+                matchConditions.createdAt = { $gte: startDate };
+            }
+            else if (endDate) {
+                matchConditions.createdAt = { $lte: endDate };
+            }
+            // 添加 strategyName 筛选条件
+            if (strategyName) {
+                matchConditions.strategyName = strategyName;
+            }
+            // 添加 $match 阶段
+            pipeline.push({
+                $match: matchConditions,
+            });
+            pipeline.push({
+                $project: {
+                    _id: 0,
+                    date: { $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" } },
+                    status: "$status",
+                    inforeground: "$inforeground",
+                },
+            }, {
+                $group: {
+                    _id: { date: "$date", status: "$status", inforeground: "$inforeground" },
+                    count: { $sum: 1 },
+                },
+            }, {
+                $group: {
+                    _id: "$_id.date",
+                    totalRecords: { $sum: "$count" },
+                    sent: { $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] } },
+                    delivered: { $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] } },
+                    opened: { $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] } },
+                    failed: { $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] } },
+                    displayCount: {
+                        $sum: { $cond: [{ $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }] }, "$count", 0] },
+                    },
+                },
+            }, {
+                $project: {
+                    _id: 0,
+                    date: "$_id",
+                    totalRecords: "$totalRecords",
+                    sent: "$sent",
+                    delivered: "$delivered",
+                    opened: "$opened",
+                    failed: "$failed",
+                    displayCount: "$displayCount",
+                    sentSuccessRate: { $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$sent", "$totalRecords"] }] },
+                    deliveredRate: { $cond: [{ $eq: ["$sent", 0] }, 0, { $divide: ["$delivered", "$sent"] }] },
+                    displayRate: { $cond: [{ $eq: ["$delivered", 0] }, 0, { $divide: ["$displayCount", "$delivered"] }] },
+                    clickThroughRate: { $cond: [{ $eq: ["$displayCount", 0] }, 0, { $divide: ["$opened", "$displayCount"] }] },
+                    tokenInvalidationRate: { $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$failed", "$totalRecords"] }] },
+                },
+            }, {
+                $sort: { date: -1 },
+            });
+            const results = await messageRecordModel_1.MessageRecord.aggregate(pipeline);
+            return results;
+        }
+        catch (error) {
+            console.error("Error fetching daily trends by template:", error);
+            return [];
+        }
+    }
+    /**
+     * 按图片URL和时间维度获取每日统计数据
+     * @param image 必须提供的图片URL
+     * @param startDate 可选的开始日期
+     * @param endDate 可选的结束日期
+     * @param strategyName 可选的策略名称筛选
+     * @returns 每日统计数据列表
+     */
+    async getDailyTrendsByImage(image, startDate, endDate, strategyName) {
+        try {
+            const pipeline = [];
+            const matchConditions = {
+                image: image,
+            };
+            if (startDate && endDate) {
+                if (endDate.getTime() <= startDate.getTime()) {
+                    endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
+                }
+                matchConditions.createdAt = {
+                    $gte: startDate,
+                    $lte: endDate,
+                };
+            }
+            else if (startDate) {
+                matchConditions.createdAt = { $gte: startDate };
+            }
+            else if (endDate) {
+                matchConditions.createdAt = { $lte: endDate };
+            }
+            // 添加 strategyName 筛选条件
+            if (strategyName) {
+                matchConditions.strategyName = strategyName;
+            }
+            // 添加 $match 阶段
+            pipeline.push({
+                $match: matchConditions,
+            });
+            pipeline.push({
+                $project: {
+                    _id: 0,
+                    date: { $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" } },
+                    status: "$status",
+                    inforeground: "$inforeground",
+                },
+            }, {
+                $group: {
+                    _id: { date: "$date", status: "$status", inforeground: "$inforeground" },
+                    count: { $sum: 1 },
+                },
+            }, {
+                $group: {
+                    _id: "$_id.date",
+                    totalRecords: { $sum: "$count" },
+                    sent: { $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] } },
+                    delivered: { $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] } },
+                    opened: { $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] } },
+                    failed: { $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] } },
+                    displayCount: {
+                        $sum: { $cond: [{ $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }] }, "$count", 0] },
+                    },
+                },
+            }, {
+                $project: {
+                    _id: 0,
+                    date: "$_id",
+                    totalRecords: "$totalRecords",
+                    sent: "$sent",
+                    delivered: "$delivered",
+                    opened: "$opened",
+                    failed: "$failed",
+                    displayCount: "$displayCount",
+                    sentSuccessRate: { $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$sent", "$totalRecords"] }] },
+                    deliveredRate: { $cond: [{ $eq: ["$sent", 0] }, 0, { $divide: ["$delivered", "$sent"] }] },
+                    displayRate: { $cond: [{ $eq: ["$delivered", 0] }, 0, { $divide: ["$displayCount", "$delivered"] }] },
+                    clickThroughRate: { $cond: [{ $eq: ["$displayCount", 0] }, 0, { $divide: ["$opened", "$displayCount"] }] },
+                    tokenInvalidationRate: { $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$failed", "$totalRecords"] }] },
+                },
+            }, {
+                $sort: { date: -1 },
+            });
+            const results = await messageRecordModel_1.MessageRecord.aggregate(pipeline);
+            return results;
+        }
+        catch (error) {
+            console.error("Error fetching daily trends by image:", error);
             return [];
         }
     }

+ 1 - 1
oms/public/app/index.html

@@ -9,5 +9,5 @@
   <style>body,html{width:100%;height:100%}*,:after,:before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}body{margin:0;color:#000000d9;font-size:14px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-variant:tabular-nums;line-height:1.5715;background-color:#fff;font-feature-settings:"tnum"}html{--antd-wave-shadow-color:#1890ff;--scroll-bar:0}</style><link rel="stylesheet" href="styles-LXBSU6DF.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-LXBSU6DF.css"></noscript></head>
   <body>
     <app-root></app-root>
-  <script src="polyfills-B6TNHZQ6.js" type="module"></script><script src="main-6WK7WHOW.js" type="module"></script></body>
+  <script src="polyfills-B6TNHZQ6.js" type="module"></script><script src="main-WIYOEJOD.js" type="module"></script></body>
 </html>

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
oms/public/app/main-WIYOEJOD.js


+ 1 - 1
oms/services/cron-jobs/index.ts

@@ -19,7 +19,7 @@ const settings: [string, string, CronJobModule][] = [
   // ["active-user-daily-notify", "30 18 * * *", require("./active-user-daily-notify") as CronJobModule], // 每天下午6点,开始活跃用户新作品消息推送
   // ["fcm-notify", "30 18 * * *", require("./fcm-notify") as CronJobModule], // 每天下午6点,基于原来的active-user-daily-notify,增加schedule推送,AB测试
   ["daily-notify-at-morning", "0 19 * * *", require("./notify/daily-notify-at-morning") as CronJobModule], // 每天下午7点,对应巴西时间早上8点推送一轮
-  ["daily-notify-at-midday", "0 23 * * *", require("./notify/daily-notify-at-midday") as CronJobModule], // 每天下午7点,对应巴西时间早上8点推送一轮
+  ["daily-notify-at-midday", "0 23 * * *", require("./notify/daily-notify-at-midday") as CronJobModule], // 每天下午7点,对应巴西时间中午12点推送一轮
   ["daily-notify-at-evening", "0 3 * * *", require("./notify/daily-notify-at-evening") as CronJobModule], // 每天凌晨3点,对应巴西时间下午4点
   ["daily-notify-at-afternoon", "0 7 * * *", require("./notify/daily-notify-at-afternoon") as CronJobModule], // 每天上午7点,对应巴西时间晚上8点
 ];

+ 166 - 94
oms/src/controllers/messageRecordController.ts

@@ -18,8 +18,7 @@ class MessageRecordController {
    */
   public createRecord = async (req: Request, res: Response): Promise<Response> => {
     try {
-      const newRecord = new MessageRecord(req.body);
-      await newRecord.save();
+      const newRecord = await this.messageRecordService.createMessageRecord(req.body);
       return res.status(201).json({ success: true, data: newRecord });
     } catch (error: any) {
       console.error("Error creating message record:", error);
@@ -33,86 +32,38 @@ class MessageRecordController {
    * Filters can be applied by uid, activityName, strategyName, templateName, status, and various date fields.
    * Date fields can be filtered by a single date or a range (e.g., "?createdAt=2023-01-01,2023-01-31").
    * @access Private
-   * @returns A JSON response with paginated records and pagination metadata.
+   * @returns A JSON response with paginated records and the total count.
    */
   public getPaginatedRecords = async (req: Request, res: Response): Promise<Response> => {
-    const { page = 1, limit = 30, uid, activityName, templateName, strategyName, status } = req.query;
-
-    const pageNum = parseInt(page as string, 10);
-    const limitNum = parseInt(limit as string, 10);
-
-    // Build the query filters dynamically based on request parameters
-    const filters: any = {};
-
-    if (uid) {
-      filters.uid = uid;
-    }
-
-    if (status) {
-      const statusNum = parseInt(status as string, 10);
-      if (!isNaN(statusNum)) {
-        filters.status = statusNum;
-      }
-    }
-
-    if (activityName) {
-      filters.activityName = activityName;
-    }
-
-    if (strategyName) {
-      filters.strategyName = strategyName;
-    }
+    try {
+      const { page, limit, sortField, sortOrder, ...filters } = req.query;
 
-    if (templateName) {
-      filters.templateName = templateName;
-    }
+      const paginatedRecords = await this.messageRecordService.getPaginatedRecords(
+        parseInt(page as string) || 1,
+        parseInt(limit as string) || 10,
+        filters,
+        sortField as string,
+        sortOrder as "asc" | "desc"
+      );
 
-    // List of all date fields that can be filtered
-    const dateQueryKeys: (keyof IMessageRecord)[] = ["plannedSendAt", "actualSendAt", "deliveredAt", "openedAt", "createdAt", "updatedAt"];
-
-    // Iterate through date fields to handle single dates or date ranges
-    dateQueryKeys.forEach((key) => {
-      const queryValue = req.query[key as string] as string;
-      if (queryValue) {
-        const dates = queryValue.split(",");
-        const startDate = new Date(dates[0]);
-        const endDate = dates.length > 1 ? new Date(dates[1]) : null;
-
-        if (!isNaN(startDate.getTime())) {
-          if (endDate && !isNaN(endDate.getTime())) {
-            // Filter by date range (e.g., plannedSendAt=2023-01-01,2023-01-31)
-            filters[key] = {
-              $gte: startDate,
-              $lte: endDate,
-            };
-          } else {
-            // Filter by a single date
-            filters[key] = startDate;
-          }
-        } else {
-          console.warn(`[API] Invalid date format for ${key}: ${queryValue}. Skipping.`);
-        }
-      }
-    });
+      // return res.status(200).json({
+      //         success: true,
+      //         data: records,
+      //         pagination: {
+      //           total,
+      //           page: pageNum,
+      //           limit: limitNum,
+      //           totalPages: Math.ceil(total / limitNum),
+      //         },
+      //       });
 
-    try {
-      const records = await MessageRecord.find(filters)
-        .sort({ createdAt: -1 })
-        .skip((pageNum - 1) * limitNum)
-        .limit(limitNum);
-
-      const total = await MessageRecord.countDocuments(filters);
-
-      return res.status(200).json({
-        success: true,
-        data: records,
-        pagination: {
-          total,
-          page: pageNum,
-          limit: limitNum,
-          totalPages: Math.ceil(total / limitNum),
-        },
-      });
+      return res
+        .status(200)
+        .json({
+          success: true,
+          data: paginatedRecords.records,
+          pagination: { total: paginatedRecords.total, page: paginatedRecords.page, limit: paginatedRecords.limit, totalPages: paginatedRecords.totalPages },
+        });
     } catch (error: any) {
       console.error("Error fetching paginated records:", error);
       return res.status(500).json({ success: false, message: "Server error", error: error.message });
@@ -185,16 +136,19 @@ class MessageRecordController {
 
   /**
    * @route GET /api/message/statistics/overall
-   * @desc Retrieves overall message push statistics
+   * @desc Retrieves overall message push statistics, with optional date and strategy filters.
    * @access Private
    * @returns A JSON response with overall statistics.
    */
   public getOverallStatistics = async (req: Request, res: Response): Promise<Response> => {
     try {
-      const { startDate, endDate } = req.query;
+      const { startDate, endDate, strategyName } = req.query;
       const start = startDate ? new Date(startDate as string) : undefined;
       const end = endDate ? new Date(endDate as string) : undefined;
-      const stats = await this.messageRecordService.getOverallStatistics(start, end);
+      const stratName = strategyName ? (strategyName as string) : undefined;
+
+      const stats = await this.messageRecordService.getOverallStatistics(start, end, stratName);
+
       return res.status(200).json({ success: true, data: stats });
     } catch (error: any) {
       console.error("Error fetching overall statistics:", error);
@@ -229,10 +183,11 @@ class MessageRecordController {
    */
   public getStatisticsByStrategy = async (req: Request, res: Response): Promise<Response> => {
     try {
-      const { startDate, endDate } = req.query;
+      const { startDate, endDate, strategyName } = req.query;
       const start = startDate ? new Date(startDate as string) : undefined;
       const end = endDate ? new Date(endDate as string) : undefined;
-      const stats = await this.messageRecordService.getStatisticsByStrategy(start, end);
+      const stratName = strategyName ? (strategyName as string) : undefined;
+      const stats = await this.messageRecordService.getStatisticsByStrategy(start, end, stratName);
       return res.status(200).json({ success: true, data: stats });
     } catch (error: any) {
       console.error("Error fetching statistics by strategy:", error);
@@ -242,16 +197,18 @@ class MessageRecordController {
 
   /**
    * @route GET /api/message/statistics/by-template
-   * @desc Retrieves message push statistics grouped by template
+   * @desc Retrieves message push statistics grouped by template, with optional date and strategy filters.
    * @access Private
    * @returns A JSON response with template-based statistics.
    */
   public getStatisticsByTemplate = async (req: Request, res: Response): Promise<Response> => {
     try {
-      const { startDate, endDate } = req.query;
+      // 从查询参数中获取 startDate, endDate 和 strategyName
+      const { startDate, endDate, strategyName } = req.query;
       const start = startDate ? new Date(startDate as string) : undefined;
       const end = endDate ? new Date(endDate as string) : undefined;
-      const stats = await this.messageRecordService.getStatisticsByTemplate(start, end);
+      const stratName = strategyName ? (strategyName as string) : undefined;
+      const stats = await this.messageRecordService.getStatisticsByTemplate(start, end, stratName);
       return res.status(200).json({ success: true, data: stats });
     } catch (error: any) {
       console.error("Error fetching statistics by template:", error);
@@ -261,16 +218,18 @@ class MessageRecordController {
 
   /**
    * @route GET /api/message/statistics/by-cc
-   * @desc Retrieves message push statistics grouped by cc
+   * @desc Retrieves message push statistics grouped by cc, with optional date and strategy filters.
    * @access Private
    * @returns A JSON response with cc-based statistics.
    */
   public getStatisticsByCc = async (req: Request, res: Response): Promise<Response> => {
     try {
-      const { startDate, endDate } = req.query;
+      // 从查询参数中获取 startDate, endDate 和 strategyName
+      const { startDate, endDate, strategyName } = req.query;
       const start = startDate ? new Date(startDate as string) : undefined;
       const end = endDate ? new Date(endDate as string) : undefined;
-      const stats = await this.messageRecordService.getStatisticsByCc(start, end);
+      const stratName = strategyName ? (strategyName as string) : undefined;
+      const stats = await this.messageRecordService.getStatisticsByCc(start, end, stratName);
       return res.status(200).json({ success: true, data: stats });
     } catch (error: any) {
       console.error("Error fetching statistics by cc:", error);
@@ -280,16 +239,18 @@ class MessageRecordController {
 
   /**
    * @route GET /api/message/statistics/by-image
-   * @desc Retrieves message push statistics grouped by image
+   * @desc Retrieves message push statistics grouped by image, with optional date and strategy filters.
    * @access Private
    * @returns A JSON response with image-based statistics.
    */
   public getStatisticsByImage = async (req: Request, res: Response): Promise<Response> => {
     try {
-      const { startDate, endDate } = req.query;
+      // 从查询参数中获取 startDate, endDate 和 strategyName
+      const { startDate, endDate, strategyName } = req.query;
       const start = startDate ? new Date(startDate as string) : undefined;
       const end = endDate ? new Date(endDate as string) : undefined;
-      const stats = await this.messageRecordService.getStatisticsByImage(start, end);
+      const stratName = strategyName ? (strategyName as string) : undefined;
+      const stats = await this.messageRecordService.getStatisticsByImage(start, end, stratName);
       return res.status(200).json({ success: true, data: stats });
     } catch (error: any) {
       console.error("Error fetching statistics by image:", error);
@@ -299,16 +260,18 @@ class MessageRecordController {
 
   /**
    * @route GET /api/message/statistics/daily-trends
-   * @desc Retrieves daily sent trend statistics
+   * @desc Retrieves daily sent trend statistics, with optional date and strategy filters.
    * @access Private
    * @returns A JSON response with daily trend statistics.
    */
   public getDailySentTrends = async (req: Request, res: Response): Promise<Response> => {
     try {
-      const { startDate, endDate } = req.query;
+      // 从查询参数中获取 startDate, endDate 和 strategyName
+      const { startDate, endDate, strategyName } = req.query;
       const start = startDate ? new Date(startDate as string) : undefined;
       const end = endDate ? new Date(endDate as string) : undefined;
-      const stats = await this.messageRecordService.getDailySentTrends(start, end);
+      const stratName = strategyName ? (strategyName as string) : undefined;
+      const stats = await this.messageRecordService.getDailySentTrends(start, end, stratName);
       return res.status(200).json({ success: true, data: stats });
     } catch (error: any) {
       console.error("Error fetching daily sent trends:", error);
@@ -341,6 +304,115 @@ class MessageRecordController {
       return res.status(500).json({ success: false, message: "Server error", error: error.message });
     }
   };
+
+  /**
+   * @route GET /api/message/daily/trends/by-cc/:cc
+   * @desc Retrieves daily message trends by country code
+   * @access Private
+   * @returns A JSON response with daily statistics.
+   */
+  public getDailyTrendsByCc = async (req: Request, res: Response): Promise<Response> => {
+    try {
+      const { cc } = req.params;
+      // 从查询参数中获取 startDate, endDate 和 strategyName
+      const { startDate, endDate, strategyName } = req.query;
+      const start = startDate ? new Date(startDate as string) : undefined;
+      const end = endDate ? new Date(endDate as string) : undefined;
+      const stratName = strategyName ? (strategyName as string) : undefined;
+
+      if (!cc) {
+        return res.status(400).json({ success: false, message: "Country code is required." });
+      }
+
+      const stats = await this.messageRecordService.getDailyTrendsByCc(cc as string, start, end, stratName);
+      return res.status(200).json({ success: true, data: stats });
+    } catch (error: any) {
+      console.error("Error fetching daily trends by cc:", error);
+      return res.status(500).json({ success: false, message: "Server error", error: error.message });
+    }
+  };
+
+  /**
+   * @route GET /api/message/daily/trends/by-strategy/:strategyName
+   * @desc Retrieves daily message trends by strategy name
+   * @access Private
+   * @returns A JSON response with daily statistics.
+   */
+  public getDailyTrendsByStrategy = async (req: Request, res: Response): Promise<Response> => {
+    try {
+      const { strategyName } = req.params;
+
+      // 从查询参数中获取 startDate, endDate
+      const { startDate, endDate } = req.query;
+      const start = startDate ? new Date(startDate as string) : undefined;
+      const end = endDate ? new Date(endDate as string) : undefined;
+
+      if (!strategyName) {
+        return res.status(400).json({ success: false, message: "Strategy name is required." });
+      }
+
+      const stats = await this.messageRecordService.getDailyTrendsByStrategy(strategyName as string, start, end);
+      return res.status(200).json({ success: true, data: stats });
+    } catch (error: any) {
+      console.error("Error fetching daily trends by strategy:", error);
+      return res.status(500).json({ success: false, message: "Server error", error: error.message });
+    }
+  };
+
+  /**
+   * @route GET /api/message/daily/trends/by-template/:templateName
+   * @desc Retrieves daily message trends by template ID
+   * @access Private
+   * @returns A JSON response with daily statistics.
+   */
+  public getDailyTrendsByTemplate = async (req: Request, res: Response): Promise<Response> => {
+    try {
+      const { templateName } = req.params;
+      // 从查询参数中获取 startDate, endDate 和 strategyName
+      const { startDate, endDate, strategyName } = req.query;
+      const start = startDate ? new Date(startDate as string) : undefined;
+      const end = endDate ? new Date(endDate as string) : undefined;
+      const stratName = strategyName ? (strategyName as string) : undefined;
+
+      if (!templateName) {
+        return res.status(400).json({ success: false, message: "Template Name is required." });
+      }
+
+      const stats = await this.messageRecordService.getDailyTrendsByTemplate(templateName as string, start, end, stratName);
+      return res.status(200).json({ success: true, data: stats });
+    } catch (error: any) {
+      console.error("Error fetching daily trends by template:", error);
+      return res.status(500).json({ success: false, message: "Server error", error: error.message });
+    }
+  };
+
+  /**
+   * @route GET /api/message/daily/trends/by-image/:image
+   * @desc Retrieves daily message trends by image URL
+   * @access Private
+   * @returns A JSON response with daily statistics.
+   */
+  public getDailyTrendsByImage = async (req: Request, res: Response): Promise<Response> => {
+    try {
+      const { image } = req.params;
+
+      // 从查询参数中获取 startDate, endDate 和 strategyName
+      const { startDate, endDate, strategyName } = req.query;
+      const start = startDate ? new Date(startDate as string) : undefined;
+      const end = endDate ? new Date(endDate as string) : undefined;
+      const stratName = strategyName ? (strategyName as string) : undefined;
+
+      if (!image) {
+        return res.status(400).json({ success: false, message: "Image URL is required." });
+      }
+
+      const stats = await this.messageRecordService.getDailyTrendsByImage(image as string, start, end, stratName);
+      return res.status(200).json({ success: true, data: stats });
+    } catch (error: any) {
+      console.error("Error fetching daily trends by image:", error);
+      return res.status(500).json({ success: false, message: "Server error", error: error.message });
+    }
+  };
 }
 
 export default new MessageRecordController();

+ 6 - 0
oms/src/routes/apiRoutes.ts

@@ -28,6 +28,12 @@ router.get("/message/statistics/by-image", messageRecordController.getStatistics
 router.get("/message/statistics/daily-trends", messageRecordController.getDailySentTrends);
 router.get("/message/statistics/multi-dimensional", messageRecordController.getMultiDimensionalStatistics); // 新增:多维度统计路由
 
+// 新增:下钻统计路由
+router.get("/message/daily/trends/by-cc/:cc", messageRecordController.getDailyTrendsByCc);
+router.get("/message/daily/trends/by-strategy/:strategyName", messageRecordController.getDailyTrendsByStrategy);
+router.get("/message/daily/trends/by-template/:templateName", messageRecordController.getDailyTrendsByTemplate);
+router.get("/message/daily/trends/by-image/:image", messageRecordController.getDailyTrendsByImage);
+
 // 应用认证中间件,保护所有下面的路由
 router.use(authMiddleware);
 

+ 732 - 238
oms/src/services/messageRecordService.ts

@@ -26,7 +26,7 @@ export class MessageRecordService {
     filters: { [key: string]: any } = {},
     sortField: string = "createdAt",
     sortOrder: "asc" | "desc" = "desc"
-  ): Promise<{ records: IMessageRecord[]; total: number }> {
+  ): Promise<{ records: IMessageRecord[]; total: number; page: number; limit: number; totalPages: number }> {
     // 构建查询条件
     const query: any = {};
     if (filters.uid) {
@@ -41,17 +41,59 @@ export class MessageRecordService {
     if (filters.status !== undefined) {
       query.status = filters.status;
     }
+    if (filters.createdAt) {
+      const dates = (filters.createdAt as string).split(",");
+      const startDate = new Date(dates[0]);
+      if (dates.length > 1) {
+        const endDate = new Date(dates[1]);
+        query.createdAt = { $gte: startDate, $lte: endDate };
+      } else {
+        query.createdAt = { $gte: startDate };
+      }
+    }
+    if (filters.sentAt) {
+      const dates = (filters.sentAt as string).split(",");
+      const startDate = new Date(dates[0]);
+      if (dates.length > 1) {
+        const endDate = new Date(dates[1]);
+        query.sentAt = { $gte: startDate, $lte: endDate };
+      } else {
+        query.sentAt = { $gte: startDate };
+      }
+    }
+    if (filters.displayedAt) {
+      const dates = (filters.displayedAt as string).split(",");
+      const startDate = new Date(dates[0]);
+      if (dates.length > 1) {
+        const endDate = new Date(dates[1]);
+        query.displayedAt = { $gte: startDate, $lte: endDate };
+      } else {
+        query.displayedAt = { $gte: startDate };
+      }
+    }
+    if (filters.openedAt) {
+      const dates = (filters.openedAt as string).split(",");
+      const startDate = new Date(dates[0]);
+      if (dates.length > 1) {
+        const endDate = new Date(dates[1]);
+        query.openedAt = { $gte: startDate, $lte: endDate };
+      } else {
+        query.openedAt = { $gte: startDate };
+      }
+    }
 
-    const sort: any = {};
-    sort[sortField] = sortOrder === "asc" ? 1 : -1;
-
-    const skip = (page - 1) * limit;
-
-    const records = await MessageRecord.find(query).sort(sort).skip(skip).limit(limit);
+    const sortOption: any = {};
+    sortOption[sortField] = sortOrder === "asc" ? 1 : -1;
 
     const total = await MessageRecord.countDocuments(query);
+    const records = await MessageRecord.find(query)
+      .sort(sortOption)
+      .skip((page - 1) * limit)
+      .limit(limit);
 
-    return { records, total };
+    const totalPages = Math.ceil(total / limit);
+
+    return { records, total, page, limit, totalPages };
   }
 
   /**
@@ -95,39 +137,38 @@ export class MessageRecordService {
    * 获取整体消息推送统计数据
    * @param startDate 可选的开始日期
    * @param endDate 可选的结束日期
+   * @param strategyName 可选的策略名称筛选
    */
-  public async getOverallStatistics(startDate?: Date, endDate?: Date) {
+  public async getOverallStatistics(startDate?: Date, endDate?: Date, strategyName?: string) {
     try {
       const pipeline: any[] = [];
-      // 如果提供了日期,添加 $match 阶段
+      const matchConditions: any = {};
+
+      // 添加日期筛选条件
       if (startDate && endDate) {
         // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
         if (endDate.getTime() <= startDate.getTime()) {
           endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
         }
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-              $lte: endDate,
-            },
-          },
-        });
+        matchConditions.createdAt = {
+          $gte: startDate,
+          $lte: endDate,
+        };
       } else if (startDate) {
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-            },
-          },
-        });
+        matchConditions.createdAt = { $gte: startDate };
       } else if (endDate) {
+        matchConditions.createdAt = { $lte: endDate };
+      }
+
+      // 添加 strategyName 筛选条件
+      if (strategyName) {
+        matchConditions.strategyName = strategyName;
+      }
+
+      // 如果存在任何筛选条件,则添加 $match 阶段
+      if (Object.keys(matchConditions).length > 0) {
         pipeline.push({
-          $match: {
-            createdAt: {
-              $lte: endDate,
-            },
-          },
+          $match: matchConditions,
         });
       }
 
@@ -337,39 +378,42 @@ export class MessageRecordService {
    * 按策略获取消息统计数据
    * @param startDate 可选的开始日期
    * @param endDate 可选的结束日期
+   * @param strategyName 可选的策略名称
    */
-  public async getStatisticsByStrategy(startDate?: Date, endDate?: Date) {
+  public async getStatisticsByStrategy(startDate?: Date, endDate?: Date, strategyName?: string) {
     try {
       const pipeline: any[] = [];
-      // 如果提供了日期,添加 $match 阶段
+      const matchConditions: any = {};
+
+      // 如果提供了日期,添加日期筛选条件
       if (startDate && endDate) {
         // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
         if (endDate.getTime() <= startDate.getTime()) {
           endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
         }
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-              $lte: endDate,
-            },
-          },
-        });
+        matchConditions.createdAt = {
+          $gte: startDate,
+          $lte: endDate,
+        };
       } else if (startDate) {
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-            },
-          },
-        });
+        matchConditions.createdAt = {
+          $gte: startDate,
+        };
       } else if (endDate) {
+        matchConditions.createdAt = {
+          $lte: endDate,
+        };
+      }
+
+      // 如果提供了 strategyName,添加策略名称筛选条件
+      if (strategyName) {
+        matchConditions.strategyName = strategyName;
+      }
+
+      // 如果有任何筛选条件,将 $match 阶段添加到管道
+      if (Object.keys(matchConditions).length > 0) {
         pipeline.push({
-          $match: {
-            createdAt: {
-              $lte: endDate,
-            },
-          },
+          $match: matchConditions,
         });
       }
 
@@ -463,39 +507,42 @@ export class MessageRecordService {
    * 按模板获取消息统计数据
    * @param startDate 可选的开始日期
    * @param endDate 可选的结束日期
+   * @param strategyName 可选的策略名称
    */
-  public async getStatisticsByTemplate(startDate?: Date, endDate?: Date) {
+  public async getStatisticsByTemplate(startDate?: Date, endDate?: Date, strategyName?: string) {
     try {
       const pipeline: any[] = [];
-      // 如果提供了日期,添加 $match 阶段
+      const matchConditions: any = {};
+
+      // 如果提供了日期,添加日期筛选条件
       if (startDate && endDate) {
         // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
         if (endDate.getTime() <= startDate.getTime()) {
           endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
         }
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-              $lte: endDate,
-            },
-          },
-        });
+        matchConditions.createdAt = {
+          $gte: startDate,
+          $lte: endDate,
+        };
       } else if (startDate) {
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-            },
-          },
-        });
+        matchConditions.createdAt = {
+          $gte: startDate,
+        };
       } else if (endDate) {
+        matchConditions.createdAt = {
+          $lte: endDate,
+        };
+      }
+
+      // 如果提供了 strategyName,添加策略名称筛选条件
+      if (strategyName) {
+        matchConditions.strategyName = strategyName;
+      }
+
+      // 如果有任何筛选条件,将 $match 阶段添加到管道
+      if (Object.keys(matchConditions).length > 0) {
         pipeline.push({
-          $match: {
-            createdAt: {
-              $lte: endDate,
-            },
-          },
+          $match: matchConditions,
         });
       }
 
@@ -589,39 +636,38 @@ export class MessageRecordService {
    * 按时间维度的趋势分析,每日统计
    * @param startDate 可选的开始日期
    * @param endDate 可选的结束日期
+   * @param strategyName 可选的策略名称筛选
    */
-  public async getDailySentTrends(startDate?: Date, endDate?: Date) {
+  public async getDailySentTrends(startDate?: Date, endDate?: Date, strategyName?: string) {
     try {
       const pipeline: any[] = [];
-      // 如果提供了日期,添加 $match 阶段
+      const matchConditions: any = {};
+
+      // 添加日期筛选条件
       if (startDate && endDate) {
         // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
         if (endDate.getTime() <= startDate.getTime()) {
           endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
         }
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-              $lte: endDate,
-            },
-          },
-        });
+        matchConditions.createdAt = {
+          $gte: startDate,
+          $lte: endDate,
+        };
       } else if (startDate) {
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-            },
-          },
-        });
+        matchConditions.createdAt = { $gte: startDate };
       } else if (endDate) {
+        matchConditions.createdAt = { $lte: endDate };
+      }
+
+      // 添加 strategyName 筛选条件
+      if (strategyName) {
+        matchConditions.strategyName = strategyName;
+      }
+
+      // 如果存在任何筛选条件,则添加 $match 阶段
+      if (Object.keys(matchConditions).length > 0) {
         pipeline.push({
-          $match: {
-            createdAt: {
-              $lte: endDate,
-            },
-          },
+          $match: matchConditions,
         });
       }
 
@@ -716,85 +762,333 @@ export class MessageRecordService {
   }
 
   /**
-   * 按多个维度获取消息推送统计数据。
-   * 支持日期范围、模板、图片、国家和策略的组合查询。
-   * @param filters 包含查询条件的过滤器对象
-   * @returns 多维度统计结果
+   * 按国家代码获取消息统计数据
+   * @param startDate 可选的开始日期
+   * @param endDate 可选的结束日期
+   * @param strategyName 可选的策略名称筛选
    */
-  public async getMultiDimensionalStatistics(filters: { startDate?: Date; endDate?: Date; templateName?: string; strategyName?: string; cc?: string; image?: string }) {
+  public async getStatisticsByCc(startDate?: Date, endDate?: Date, strategyName?: string) {
     try {
       const pipeline: any[] = [];
+      const matchConditions: any = {};
 
-      // 1. $match (筛选阶段)
-      const matchStage: any = {};
-      if (filters.startDate || filters.endDate) {
-        matchStage.createdAt = {};
-        if (filters.startDate) {
-          matchStage.createdAt.$gte = filters.startDate;
-        }
-        if (filters.endDate) {
-          // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
-          if (filters.startDate && filters.endDate.getTime() <= filters.startDate.getTime()) {
-            filters.endDate = new Date(filters.startDate.getTime() + 24 * 60 * 60 * 1000);
-          }
-          matchStage.createdAt.$lte = filters.endDate;
+      // 添加日期筛选条件
+      if (startDate && endDate) {
+        // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
+        if (endDate.getTime() <= startDate.getTime()) {
+          endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
         }
+        matchConditions.createdAt = {
+          $gte: startDate,
+          $lte: endDate,
+        };
+      } else if (startDate) {
+        matchConditions.createdAt = { $gte: startDate };
+      } else if (endDate) {
+        matchConditions.createdAt = { $lte: endDate };
       }
-      if (filters.templateName) {
-        matchStage.templateName = filters.templateName;
-      }
-      if (filters.strategyName) {
-        matchStage.strategyName = filters.strategyName;
-      }
-      if (filters.cc) {
-        matchStage.cc = filters.cc;
-      }
-      if (filters.image) {
-        matchStage.image = filters.image;
+
+      // 添加 strategyName 筛选条件
+      if (strategyName) {
+        matchConditions.strategyName = strategyName;
       }
-      if (Object.keys(matchStage).length > 0) {
-        pipeline.push({ $match: matchStage });
+
+      // 如果存在任何筛选条件,则添加 $match 阶段
+      if (Object.keys(matchConditions).length > 0) {
+        pipeline.push({
+          $match: matchConditions,
+        });
       }
 
-      // 2. $group (分组和聚合阶段)
-      pipeline.push({
-        $group: {
-          _id: {
-            date: { $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" } },
-            templateName: "$templateName",
-            strategyName: "$strategyName",
-            cc: "$cc",
-            image: "$image",
-            status: "$status",
-            inforeground: "$inforeground",
+      pipeline.push(
+        // 1. 根据 cc, status, strategyName 和 inforeground 进行分组
+        {
+          $group: {
+            _id: {
+              cc: "$cc",
+              status: "$status",
+              inforeground: "$inforeground",
+              strategyName: "$strategyName", // 增加 strategyName
+            },
+            count: { $sum: 1 },
           },
-          count: { $sum: 1 },
         },
-      });
-
-      // 3. 第二次 $group (汇总阶段)
-      pipeline.push({
-        $group: {
-          _id: {
-            date: "$_id.date",
-            templateName: "$_id.templateName",
-            strategyName: "$_id.strategyName",
-            cc: "$_id.cc",
-            image: "$_id.image",
+        // 2. 将数据重组,以便按 cc 汇总
+        {
+          $group: {
+            _id: "$_id.cc",
+            cc: { $first: "$_id.cc" },
+            totalRecords: { $sum: "$count" },
+            sent: {
+              $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] },
+            },
+            delivered: {
+              $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] },
+            },
+            opened: {
+              $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] },
+            },
+            failed: {
+              $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] },
+            },
+            displayCount: {
+              $sum: {
+                $cond: [
+                  {
+                    $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }],
+                  },
+                  "$count",
+                  0,
+                ],
+              },
+            },
           },
-          totalRecords: { $sum: "$count" },
-          sent: { $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] } },
-          delivered: { $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] } },
-          opened: { $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] } },
-          failed: { $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] } },
-          displayCount: {
-            $sum: {
-              $cond: [{ $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }] }, "$count", 0],
+        },
+        // 3. 计算比率并格式化输出
+        {
+          $project: {
+            _id: 0,
+            cc: "$cc",
+            totalRecords: "$totalRecords",
+            sent: "$sent",
+            delivered: "$delivered",
+            opened: "$opened",
+            failed: "$failed",
+            displayCount: "$displayCount",
+            sentSuccessRate: {
+              $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$sent", "$totalRecords"] }],
+            },
+            deliveredRate: {
+              $cond: [{ $eq: ["$sent", 0] }, 0, { $divide: ["$delivered", "$sent"] }],
+            },
+            displayRate: {
+              $cond: [{ $eq: ["$delivered", 0] }, 0, { $divide: ["$displayCount", "$delivered"] }],
+            },
+            clickThroughRate: {
+              $cond: [{ $eq: ["$displayCount", 0] }, 0, { $divide: ["$opened", "$displayCount"] }],
+            },
+            tokenInvalidationRate: {
+              $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$failed", "$totalRecords"] }],
             },
           },
         },
-      });
-
+        // 4. 按 deliveredRate 降序排序
+        {
+          $sort: { deliveredRate: -1 },
+        }
+      );
+      const results = await MessageRecord.aggregate(pipeline);
+      return results;
+    } catch (error) {
+      console.error("Error fetching statistics by cc:", error);
+      return [];
+    }
+  }
+
+  /**
+   * 按图片 URL 获取消息统计数据
+   * @param startDate 可选的开始日期
+   * @param endDate 可选的结束日期
+   * @param strategyName 可选的策略名称筛选
+   */
+  public async getStatisticsByImage(startDate?: Date, endDate?: Date, strategyName?: string) {
+    try {
+      const pipeline: any[] = [];
+      const matchConditions: any = {};
+
+      // 添加日期筛选条件
+      if (startDate && endDate) {
+        // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
+        if (endDate.getTime() <= startDate.getTime()) {
+          endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
+        }
+        matchConditions.createdAt = {
+          $gte: startDate,
+          $lte: endDate,
+        };
+      } else if (startDate) {
+        matchConditions.createdAt = { $gte: startDate };
+      } else if (endDate) {
+        matchConditions.createdAt = { $lte: endDate };
+      }
+
+      // 添加 strategyName 筛选条件
+      if (strategyName) {
+        matchConditions.strategyName = strategyName;
+      }
+
+      // 如果存在任何筛选条件,则添加 $match 阶段
+      if (Object.keys(matchConditions).length > 0) {
+        pipeline.push({
+          $match: matchConditions,
+        });
+      }
+
+      pipeline.push(
+        // 1. 根据 image, status, strategyName 和 inforeground 进行分组
+        {
+          $group: {
+            _id: {
+              image: "$image",
+              status: "$status",
+              inforeground: "$inforeground",
+              strategyName: "$strategyName", // 增加 strategyName
+            },
+            count: { $sum: 1 },
+          },
+        },
+        // 2. 将数据重组,以便按 image 汇总
+        {
+          $group: {
+            _id: "$_id.image",
+            image: { $first: "$_id.image" },
+            totalRecords: { $sum: "$count" },
+            sent: {
+              $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] },
+            },
+            delivered: {
+              $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] },
+            },
+            opened: {
+              $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] },
+            },
+            failed: {
+              $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] },
+            },
+            displayCount: {
+              $sum: {
+                $cond: [
+                  {
+                    $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }],
+                  },
+                  "$count",
+                  0,
+                ],
+              },
+            },
+          },
+        },
+        // 3. 计算比率并格式化输出
+        {
+          $project: {
+            _id: 0,
+            image: "$image",
+            totalRecords: "$totalRecords",
+            sent: "$sent",
+            delivered: "$delivered",
+            opened: "$opened",
+            failed: "$failed",
+            displayCount: "$displayCount",
+            sentSuccessRate: {
+              $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$sent", "$totalRecords"] }],
+            },
+            deliveredRate: {
+              $cond: [{ $eq: ["$sent", 0] }, 0, { $divide: ["$delivered", "$sent"] }],
+            },
+            displayRate: {
+              $cond: [{ $eq: ["$delivered", 0] }, 0, { $divide: ["$displayCount", "$delivered"] }],
+            },
+            clickThroughRate: {
+              $cond: [{ $eq: ["$displayCount", 0] }, 0, { $divide: ["$opened", "$displayCount"] }],
+            },
+            tokenInvalidationRate: {
+              $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$failed", "$totalRecords"] }],
+            },
+          },
+        },
+        // 4. 按 deliveredRate 降序排序
+        {
+          $sort: { deliveredRate: -1 },
+        }
+      );
+      const results = await MessageRecord.aggregate(pipeline);
+      return results;
+    } catch (error) {
+      console.error("Error fetching statistics by image:", error);
+      return [];
+    }
+  }
+
+  /**
+   * 按多个维度获取消息推送统计数据。
+   * 支持日期范围、模板、图片、国家和策略的组合查询。
+   * @param filters 包含查询条件的过滤器对象
+   * @returns 多维度统计结果
+   */
+  public async getMultiDimensionalStatistics(filters: { startDate?: Date; endDate?: Date; templateName?: string; strategyName?: string; cc?: string; image?: string }) {
+    try {
+      const pipeline: any[] = [];
+
+      // 1. $match (筛选阶段)
+      const matchStage: any = {};
+      if (filters.startDate || filters.endDate) {
+        matchStage.createdAt = {};
+        if (filters.startDate) {
+          matchStage.createdAt.$gte = filters.startDate;
+        }
+        if (filters.endDate) {
+          // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
+          if (filters.startDate && filters.endDate.getTime() <= filters.startDate.getTime()) {
+            filters.endDate = new Date(filters.startDate.getTime() + 24 * 60 * 60 * 1000);
+          }
+          matchStage.createdAt.$lte = filters.endDate;
+        }
+      }
+      if (filters.templateName) {
+        matchStage.templateName = filters.templateName;
+      }
+      if (filters.strategyName) {
+        matchStage.strategyName = filters.strategyName;
+      }
+      if (filters.cc) {
+        matchStage.cc = filters.cc;
+      }
+      if (filters.image) {
+        matchStage.image = filters.image;
+      }
+      if (Object.keys(matchStage).length > 0) {
+        pipeline.push({ $match: matchStage });
+      }
+
+      // 2. $group (分组和聚合阶段)
+      pipeline.push({
+        $group: {
+          _id: {
+            date: { $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" } },
+            templateName: "$templateName",
+            strategyName: "$strategyName",
+            cc: "$cc",
+            image: "$image",
+            status: "$status",
+            inforeground: "$inforeground",
+          },
+          count: { $sum: 1 },
+        },
+      });
+
+      // 3. 第二次 $group (汇总阶段)
+      pipeline.push({
+        $group: {
+          _id: {
+            date: "$_id.date",
+            templateName: "$_id.templateName",
+            strategyName: "$_id.strategyName",
+            cc: "$_id.cc",
+            image: "$_id.image",
+          },
+          totalRecords: { $sum: "$count" },
+          sent: { $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] } },
+          delivered: { $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] } },
+          opened: { $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] } },
+          failed: { $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] } },
+          displayCount: {
+            $sum: {
+              $cond: [{ $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }] }, "$count", 0],
+            },
+          },
+        },
+      });
+
       // 4. $project (计算比率和格式化输出)
       pipeline.push({
         $project: {
@@ -842,62 +1136,69 @@ export class MessageRecordService {
   }
 
   /**
-   * 按国家代码获取消息统计数据
+   * 按国家代码和时间维度获取每日统计数据
+   * @param cc 必须提供的国家代码
    * @param startDate 可选的开始日期
    * @param endDate 可选的结束日期
+   * @param strategyName 可选的策略名称筛选
+   * @returns 每日统计数据列表
    */
-  public async getStatisticsByCc(startDate?: Date, endDate?: Date) {
+  public async getDailyTrendsByCc(cc: string, startDate?: Date, endDate?: Date, strategyName?: string) {
     try {
       const pipeline: any[] = [];
-      // 如果提供了日期,添加 $match 阶段
+      const matchConditions: any = {
+        cc: cc,
+      };
+
+      // 添加日期筛选条件
       if (startDate && endDate) {
         // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
         if (endDate.getTime() <= startDate.getTime()) {
           endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
         }
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-              $lte: endDate,
-            },
-          },
-        });
+        matchConditions.createdAt = {
+          $gte: startDate,
+          $lte: endDate,
+        };
       } else if (startDate) {
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-            },
-          },
-        });
+        matchConditions.createdAt = { $gte: startDate };
       } else if (endDate) {
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $lte: endDate,
-            },
-          },
-        });
+        matchConditions.createdAt = { $lte: endDate };
       }
 
+      // 添加 strategyName 筛选条件
+      if (strategyName) {
+        matchConditions.strategyName = strategyName;
+      }
+
+      // 添加 $match 阶段
+      pipeline.push({
+        $match: matchConditions,
+      });
+
       pipeline.push(
-        // 1. 根据 cc, status, 和 inforeground 进行分组
+        // 1. 将 createdAt 字段转换为日期,忽略时分秒
         {
-          $group: {
-            _id: {
-              cc: "$cc",
-              status: "$status",
-              inforeground: "$inforeground",
+          $project: {
+            _id: 0,
+            date: {
+              $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" },
             },
+            status: "$status",
+            inforeground: "$inforeground",
+          },
+        },
+        // 2. 根据日期、状态和 inforeground 进行分组
+        {
+          $group: {
+            _id: { date: "$date", status: "$status", inforeground: "$inforeground" },
             count: { $sum: 1 },
           },
         },
-        // 2. 将数据重组,以便按 cc 汇总
+        // 3. 将数据重组,以便按日期汇总
         {
           $group: {
-            _id: "$_id.cc",
-            cc: { $first: "$_id.cc" },
+            _id: "$_id.date",
             totalRecords: { $sum: "$count" },
             sent: {
               $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] },
@@ -924,11 +1225,11 @@ export class MessageRecordService {
             },
           },
         },
-        // 3. 计算比率并格式化输出
+        // 4. 计算比率并格式化输出
         {
           $project: {
             _id: 0,
-            cc: "$cc",
+            date: "$_id",
             totalRecords: "$totalRecords",
             sent: "$sent",
             delivered: "$delivered",
@@ -952,76 +1253,77 @@ export class MessageRecordService {
             },
           },
         },
-        // 4. 按 deliveredRate 降序排序
+        // 5. 按日期降序排序
         {
-          $sort: { deliveredRate: -1 },
+          $sort: { date: -1 },
         }
       );
       const results = await MessageRecord.aggregate(pipeline);
       return results;
     } catch (error) {
-      console.error("Error fetching statistics by cc:", error);
+      console.error("Error fetching daily trends by cc:", error);
       return [];
     }
   }
 
   /**
-   * 按图片 URL 获取消息统计数据
+   * 按策略名称和时间维度获取每日统计数据
+   * @param strategyName 必须提供的策略名称
    * @param startDate 可选的开始日期
    * @param endDate 可选的结束日期
+   * @returns 每日统计数据列表
    */
-  public async getStatisticsByImage(startDate?: Date, endDate?: Date) {
+  public async getDailyTrendsByStrategy(strategyName: string, startDate?: Date, endDate?: Date) {
     try {
       const pipeline: any[] = [];
-      // 如果提供了日期,添加 $match 阶段
+      const matchConditions: any = {
+        strategyName: strategyName,
+      };
+
+      // 添加日期筛选条件
       if (startDate && endDate) {
         // 容错处理:如果 endDate <= startDate,将 endDate 设为 startDate 的后一天
         if (endDate.getTime() <= startDate.getTime()) {
           endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
         }
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-              $lte: endDate,
-            },
-          },
-        });
+        matchConditions.createdAt = {
+          $gte: startDate,
+          $lte: endDate,
+        };
       } else if (startDate) {
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $gte: startDate,
-            },
-          },
-        });
+        matchConditions.createdAt = { $gte: startDate };
       } else if (endDate) {
-        pipeline.push({
-          $match: {
-            createdAt: {
-              $lte: endDate,
-            },
-          },
-        });
+        matchConditions.createdAt = { $lte: endDate };
       }
 
+      // 添加 $match 阶段
+      pipeline.push({
+        $match: matchConditions,
+      });
+
       pipeline.push(
-        // 1. 根据 image, status, 和 inforeground 进行分组
+        // 1. 将 createdAt 字段转换为日期,忽略时分秒
         {
-          $group: {
-            _id: {
-              image: "$image",
-              status: "$status",
-              inforeground: "$inforeground",
+          $project: {
+            _id: 0,
+            date: {
+              $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" },
             },
+            status: "$status",
+            inforeground: "$inforeground",
+          },
+        },
+        // 2. 根据日期、状态和 inforeground 进行分组
+        {
+          $group: {
+            _id: { date: "$date", status: "$status", inforeground: "$inforeground" },
             count: { $sum: 1 },
           },
         },
-        // 2. 将数据重组,以便按 image 汇总
+        // 3. 将数据重组,以便按日期汇总
         {
           $group: {
-            _id: "$_id.image",
-            image: { $first: "$_id.image" },
+            _id: "$_id.date",
             totalRecords: { $sum: "$count" },
             sent: {
               $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] },
@@ -1048,11 +1350,11 @@ export class MessageRecordService {
             },
           },
         },
-        // 3. 计算比率并格式化输出
+        // 4. 计算比率并格式化输出
         {
           $project: {
             _id: 0,
-            image: "$image",
+            date: "$_id",
             totalRecords: "$totalRecords",
             sent: "$sent",
             delivered: "$delivered",
@@ -1076,15 +1378,207 @@ export class MessageRecordService {
             },
           },
         },
-        // 4. 按 deliveredRate 降序排序
+        // 5. 按日期升序排序(统一为升序,与getDailyTrendsByCc保持一致)
         {
-          $sort: { deliveredRate: -1 },
+          $sort: { date: -1 },
         }
       );
       const results = await MessageRecord.aggregate(pipeline);
       return results;
     } catch (error) {
-      console.error("Error fetching statistics by image:", error);
+      console.error("Error fetching daily trends by strategy:", error);
+      return [];
+    }
+  }
+
+  /**
+   * 按模板ID和时间维度获取每日统计数据
+   * @param templateName 必须提供的模板ID
+   * @param startDate 可选的开始日期
+   * @param endDate 可选的结束日期
+   * @param strategyName 可选的策略名称筛选
+   * @returns 每日统计数据列表
+   */
+  public async getDailyTrendsByTemplate(templateName: string, startDate?: Date, endDate?: Date, strategyName?: string) {
+    try {
+      const pipeline: any[] = [];
+      const matchConditions: any = {
+        templateName: templateName,
+      };
+
+      if (startDate && endDate) {
+        if (endDate.getTime() <= startDate.getTime()) {
+          endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
+        }
+        matchConditions.createdAt = {
+          $gte: startDate,
+          $lte: endDate,
+        };
+      } else if (startDate) {
+        matchConditions.createdAt = { $gte: startDate };
+      } else if (endDate) {
+        matchConditions.createdAt = { $lte: endDate };
+      }
+
+      // 添加 strategyName 筛选条件
+      if (strategyName) {
+        matchConditions.strategyName = strategyName;
+      }
+
+      // 添加 $match 阶段
+      pipeline.push({
+        $match: matchConditions,
+      });
+
+      pipeline.push(
+        {
+          $project: {
+            _id: 0,
+            date: { $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" } },
+            status: "$status",
+            inforeground: "$inforeground",
+          },
+        },
+        {
+          $group: {
+            _id: { date: "$date", status: "$status", inforeground: "$inforeground" },
+            count: { $sum: 1 },
+          },
+        },
+        {
+          $group: {
+            _id: "$_id.date",
+            totalRecords: { $sum: "$count" },
+            sent: { $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] } },
+            delivered: { $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] } },
+            opened: { $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] } },
+            failed: { $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] } },
+            displayCount: {
+              $sum: { $cond: [{ $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }] }, "$count", 0] },
+            },
+          },
+        },
+        {
+          $project: {
+            _id: 0,
+            date: "$_id",
+            totalRecords: "$totalRecords",
+            sent: "$sent",
+            delivered: "$delivered",
+            opened: "$opened",
+            failed: "$failed",
+            displayCount: "$displayCount",
+            sentSuccessRate: { $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$sent", "$totalRecords"] }] },
+            deliveredRate: { $cond: [{ $eq: ["$sent", 0] }, 0, { $divide: ["$delivered", "$sent"] }] },
+            displayRate: { $cond: [{ $eq: ["$delivered", 0] }, 0, { $divide: ["$displayCount", "$delivered"] }] },
+            clickThroughRate: { $cond: [{ $eq: ["$displayCount", 0] }, 0, { $divide: ["$opened", "$displayCount"] }] },
+            tokenInvalidationRate: { $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$failed", "$totalRecords"] }] },
+          },
+        },
+        {
+          $sort: { date: -1 },
+        }
+      );
+      const results = await MessageRecord.aggregate(pipeline);
+      return results;
+    } catch (error) {
+      console.error("Error fetching daily trends by template:", error);
+      return [];
+    }
+  }
+
+  /**
+   * 按图片URL和时间维度获取每日统计数据
+   * @param image 必须提供的图片URL
+   * @param startDate 可选的开始日期
+   * @param endDate 可选的结束日期
+   * @param strategyName 可选的策略名称筛选
+   * @returns 每日统计数据列表
+   */
+  public async getDailyTrendsByImage(image: string, startDate?: Date, endDate?: Date, strategyName?: string) {
+    try {
+      const pipeline: any[] = [];
+      const matchConditions: any = {
+        image: image,
+      };
+
+      if (startDate && endDate) {
+        if (endDate.getTime() <= startDate.getTime()) {
+          endDate = new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
+        }
+        matchConditions.createdAt = {
+          $gte: startDate,
+          $lte: endDate,
+        };
+      } else if (startDate) {
+        matchConditions.createdAt = { $gte: startDate };
+      } else if (endDate) {
+        matchConditions.createdAt = { $lte: endDate };
+      }
+
+      // 添加 strategyName 筛选条件
+      if (strategyName) {
+        matchConditions.strategyName = strategyName;
+      }
+
+      // 添加 $match 阶段
+      pipeline.push({
+        $match: matchConditions,
+      });
+
+      pipeline.push(
+        {
+          $project: {
+            _id: 0,
+            date: { $dateTrunc: { date: "$createdAt", unit: "day", timezone: "America/Los_Angeles" } },
+            status: "$status",
+            inforeground: "$inforeground",
+          },
+        },
+        {
+          $group: {
+            _id: { date: "$date", status: "$status", inforeground: "$inforeground" },
+            count: { $sum: 1 },
+          },
+        },
+        {
+          $group: {
+            _id: "$_id.date",
+            totalRecords: { $sum: "$count" },
+            sent: { $sum: { $cond: [{ $gte: ["$_id.status", 1] }, "$count", 0] } },
+            delivered: { $sum: { $cond: [{ $gte: ["$_id.status", 2] }, "$count", 0] } },
+            opened: { $sum: { $cond: [{ $eq: ["$_id.status", 3] }, "$count", 0] } },
+            failed: { $sum: { $cond: [{ $eq: ["$_id.status", -1] }, "$count", 0] } },
+            displayCount: {
+              $sum: { $cond: [{ $and: [{ $gte: ["$_id.status", 2] }, { $eq: ["$_id.inforeground", false] }] }, "$count", 0] },
+            },
+          },
+        },
+        {
+          $project: {
+            _id: 0,
+            date: "$_id",
+            totalRecords: "$totalRecords",
+            sent: "$sent",
+            delivered: "$delivered",
+            opened: "$opened",
+            failed: "$failed",
+            displayCount: "$displayCount",
+            sentSuccessRate: { $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$sent", "$totalRecords"] }] },
+            deliveredRate: { $cond: [{ $eq: ["$sent", 0] }, 0, { $divide: ["$delivered", "$sent"] }] },
+            displayRate: { $cond: [{ $eq: ["$delivered", 0] }, 0, { $divide: ["$displayCount", "$delivered"] }] },
+            clickThroughRate: { $cond: [{ $eq: ["$displayCount", 0] }, 0, { $divide: ["$opened", "$displayCount"] }] },
+            tokenInvalidationRate: { $cond: [{ $eq: ["$totalRecords", 0] }, 0, { $divide: ["$failed", "$totalRecords"] }] },
+          },
+        },
+        {
+          $sort: { date: -1 },
+        }
+      );
+      const results = await MessageRecord.aggregate(pipeline);
+      return results;
+    } catch (error) {
+      console.error("Error fetching daily trends by image:", error);
       return [];
     }
   }

+ 2 - 2
omsapp/src/app/layouts/main-layout.component.ts

@@ -115,7 +115,7 @@ interface TabItem {
                   <span nz-icon nzType="line-chart"></span>
                   <span>统计分析</span>
                 </li>
-
+                <!-- 
                 <li
                   nz-menu-item
                   [routerLink]="['/message-multi-dimentsional']"
@@ -124,7 +124,7 @@ interface TabItem {
                 >
                   <span nz-icon nzType="line-chart"></span>
                   <span>多维统计分析</span>
-                </li>
+                </li> -->
               </ul>
             </li>
           </ul>

+ 96 - 75
omsapp/src/app/pages/message-dashboard.component.css

@@ -3,6 +3,8 @@
   display: flex;
   align-items: center;
   margin-bottom: 16px;
+  flex-wrap: wrap;
+  gap: 16px;
 }
 
 /* 统计卡片样式 */
@@ -79,19 +81,6 @@ nz-progress {
   vertical-align: middle;
 }
 
-.progress-container {
-  display: flex;
-  align-items: center;
-}
-
-/* 百分比文本样式 */
-.percentage-text {
-  display: inline-block;
-  min-width: 60px;
-  font-size: 12px;
-  color: rgba(0, 0, 0, 0.65);
-}
-
 /* 特殊列样式 */
 .strategy-name-col,
 .template-name-col {
@@ -128,6 +117,91 @@ nz-tabs-nav {
   margin-bottom: 0;
 }
 
+/* 展开行样式 */
+.expanded-row {
+  padding: 0 !important;
+  background-color: #fafafa;
+}
+
+.expanded-content {
+  width: 100%;
+  overflow-x: auto;
+}
+
+.expanded-table {
+  width: 100%;
+  margin: 0;
+}
+
+/* 确保子表格与父表格对齐 */
+.expanded-table th,
+.expanded-table td {
+  padding: 8px 16px !important;
+  white-space: nowrap;
+}
+
+/* 展开图标样式 */
+.expand-icon-cell {
+  width: 60px;
+  text-align: center;
+  cursor: pointer;
+}
+
+.expand-icon {
+  transition: transform 0.3s;
+}
+
+.expand-icon.expanded {
+  transform: rotate(90deg);
+}
+
+/* 子表格样式 */
+.expanded-table nz-table {
+  background-color: transparent;
+}
+
+.expanded-table th {
+  background-color: #f5f5f5 !important;
+}
+
+/* 加载状态样式 */
+.loading-container {
+  padding: 16px;
+  text-align: center;
+}
+
+/* 图表图例样式 */
+.chart-legend {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: 16px;
+  gap: 16px;
+  justify-content: center;
+}
+
+.chart-legend div {
+  display: flex;
+  align-items: center;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.65);
+}
+
+.legend-color {
+  display: inline-block;
+  width: 12px;
+  height: 12px;
+  margin-right: 6px;
+  border-radius: 2px;
+}
+
+.legend-line {
+  display: inline-block;
+  width: 20px;
+  height: 0;
+  border-bottom: 2px solid;
+  margin-right: 6px;
+}
+
 /* 响应式调整 */
 @media (max-width: 1600px) {
   nz-table {
@@ -139,21 +213,11 @@ nz-tabs-nav {
     padding: 8px 12px !important;
   }
 
-  .progress-container {
-    flex-direction: column;
-    align-items: flex-start;
-  }
-
   nz-progress {
     width: 100%;
     margin-right: 0;
     margin-bottom: 4px;
   }
-
-  .percentage-text {
-    min-width: 100%;
-    text-align: left;
-  }
 }
 
 @media (max-width: 1200px) {
@@ -198,6 +262,15 @@ nz-tabs-nav {
   nz-statistic {
     margin-bottom: 8px;
   }
+
+  .chart-legend {
+    gap: 8px;
+    justify-content: flex-start;
+  }
+
+  .chart-legend div {
+    font-size: 11px;
+  }
 }
 
 /* 状态颜色 */
@@ -223,55 +296,3 @@ nz-spin {
 nz-card[nzTitle] {
   border-top: 3px solid #1890ff;
 }
-
-/* 新增图表图例样式 */
-.chart-legend {
-  display: flex;
-  flex-wrap: wrap;
-  margin-top: 16px;
-  gap: 16px;
-  justify-content: center;
-}
-
-.chart-legend div {
-  display: flex;
-  align-items: center;
-  font-size: 12px;
-  color: rgba(0, 0, 0, 0.65);
-}
-
-.legend-color {
-  display: inline-block;
-  width: 12px;
-  height: 12px;
-  margin-right: 6px;
-  border-radius: 2px;
-}
-
-.legend-line {
-  display: inline-block;
-  width: 20px;
-  height: 0;
-  border-bottom: 2px solid;
-  margin-right: 6px;
-}
-
-/* 调整图表容器 */
-.chart-container {
-  position: relative;
-  width: 100%;
-  min-height: 400px;
-  margin-top: 8px;
-}
-
-/* 响应式调整 */
-@media (max-width: 768px) {
-  .chart-legend {
-    gap: 8px;
-    justify-content: flex-start;
-  }
-
-  .chart-legend div {
-    font-size: 11px;
-  }
-}

+ 389 - 250
omsapp/src/app/pages/message-dashboard.component.html

@@ -5,6 +5,20 @@
       (ngModelChange)="refreshData()"
       style="width: 300px; margin-right: 16px"
     ></nz-range-picker>
+    <!-- 新增策略筛选下拉框 -->
+    <nz-select
+      [(ngModel)]="selectedStrategy"
+      (ngModelChange)="refreshData()"
+      style="width: 300px; margin-right: 16px"
+      nzPlaceHolder="选择策略"
+    >
+      <nz-option nzValue="" nzLabel="全部策略"></nz-option>
+      <nz-option
+        *ngFor="let strategy of strategies"
+        [nzValue]="strategy"
+        [nzLabel]="strategy"
+      ></nz-option>
+    </nz-select>
     <button
       nz-button
       nzType="primary"
@@ -151,6 +165,7 @@
 
     <!-- 统计表格标签页 -->
     <nz-tabset [(nzSelectedIndex)]="activeTab" style="margin-top: 16px">
+      <!-- 按策略统计表格 -->
       <nz-tab nzTitle="按策略统计">
         <nz-table
           #strategyTable
@@ -159,9 +174,11 @@
           [nzShowPagination]="false"
           [nzBordered]="true"
           [nzSize]="'small'"
+          nzTableLayout="fixed"
         >
           <thead>
             <tr>
+              <th nzWidth="50px"></th>
               <th (click)="sortStrategyTable('strategyName')">
                 策略名称
                 <span *ngIf="strategySortField === 'strategyName'">
@@ -308,75 +325,105 @@
             </tr>
           </thead>
           <tbody>
-            <tr *ngFor="let item of strategyTable.data">
-              <td class="strategy-name-col">
-                <a
-                  (click)="navigateToStrategy(item.strategyName)"
-                  *ngIf="item.strategyName"
+            <ng-template ngFor let-item [ngForOf]="strategyTable.data">
+              <tr>
+                <td
+                  (click)="toggleExpand(item, 'strategy')"
+                  class="expand-icon-cell"
                 >
-                  {{ item.strategyName || "-" }}
-                </a>
-                <span *ngIf="!item.strategyName">-</span>
-              </td>
-              <td>{{ item.totalRecords || 0 }}</td>
-              <td>{{ item.sent || 0 }}</td>
-              <td>{{ item.delivered || 0 }}</td>
-              <td class="display-count-column">{{ item.displayCount || 0 }}</td>
-              <td>{{ item.opened || 0 }}</td>
-              <td>{{ item.failed || 0 }}</td>
-              <td>
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.deliveredRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="normal"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td class="display-rate-column">
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.displayRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="active"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td class="click-rate-column">
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.clickThroughRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="active"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td>
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.tokenInvalidationRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="exception"
-                  ></nz-progress>
-                  <span class="percentage-text">{{
-                    formatPercentage(item.tokenInvalidationRate || 0)
-                  }}</span>
-                </div>
-              </td>
-            </tr>
+                  <i
+                    nz-icon
+                    [nzType]="item.expanded ? 'down' : 'right'"
+                    class="expand-icon"
+                  ></i>
+                </td>
+                <td class="strategy-name-col">
+                  <a
+                    (click)="navigateToStrategy(item.strategyName)"
+                    *ngIf="item.strategyName"
+                  >
+                    {{ item.strategyName || "-" }}
+                  </a>
+                  <span *ngIf="!item.strategyName">-</span>
+                </td>
+                <td>{{ item.totalRecords || 0 }}</td>
+                <td>{{ item.sent || 0 }}</td>
+                <td>{{ item.delivered || 0 }}</td>
+                <td>{{ item.displayCount || 0 }}</td>
+                <td>{{ item.opened || 0 }}</td>
+                <td>{{ item.failed || 0 }}</td>
+                <td>{{ formatPercentage(item.deliveredRate || 0) }}</td>
+                <td>{{ formatPercentage(item.displayRate || 0) }}</td>
+                <td>{{ formatPercentage(item.clickThroughRate || 0) }}</td>
+                <td>{{ formatPercentage(item.tokenInvalidationRate || 0) }}</td>
+              </tr>
+              <tr [nzExpand]="item.expanded">
+                <td colspan="12" *ngIf="item.expanded" class="expanded-row">
+                  <div *ngIf="item.loading" class="loading-container">
+                    <nz-spin></nz-spin>
+                  </div>
+                  <div
+                    *ngIf="item.dailyData && !item.loading"
+                    class="expanded-content"
+                  >
+                    <nz-table
+                      class="expanded-table"
+                      [nzData]="item.dailyData"
+                      [nzShowPagination]="false"
+                      [nzSize]="'small'"
+                      nzBordered
+                    >
+                      <thead>
+                        <tr>
+                          <th>日期</th>
+                          <th>总发送量</th>
+                          <th>成功数</th>
+                          <th>送达数</th>
+                          <th>展示数</th>
+                          <th>打开数</th>
+                          <th>失败数</th>
+                          <th>送达率</th>
+                          <th>展示率</th>
+                          <th>点击率</th>
+                          <th>失败率</th>
+                        </tr>
+                      </thead>
+                      <tbody>
+                        <tr *ngFor="let daily of item.dailyData">
+                          <td>{{ formatDate(daily.date) }}</td>
+                          <td>{{ daily.totalRecords || 0 }}</td>
+                          <td>{{ daily.sent || 0 }}</td>
+                          <td>{{ daily.delivered || 0 }}</td>
+                          <td>{{ daily.displayCount || 0 }}</td>
+                          <td>{{ daily.opened || 0 }}</td>
+                          <td>{{ daily.failed || 0 }}</td>
+                          <td>
+                            {{ formatPercentage(daily.deliveredRate || 0) }}
+                          </td>
+                          <td>
+                            {{ formatPercentage(daily.displayRate || 0) }}
+                          </td>
+                          <td>
+                            {{ formatPercentage(daily.clickThroughRate || 0) }}
+                          </td>
+                          <td>
+                            {{
+                              formatPercentage(daily.tokenInvalidationRate || 0)
+                            }}
+                          </td>
+                        </tr>
+                      </tbody>
+                    </nz-table>
+                  </div>
+                </td>
+              </tr>
+            </ng-template>
           </tbody>
         </nz-table>
         <nz-empty *ngIf="strategyStats.length === 0 && !isLoading"></nz-empty>
       </nz-tab>
 
+      <!-- 按模板统计表格 -->
       <nz-tab nzTitle="按模板统计">
         <nz-table
           #templateTable
@@ -385,9 +432,11 @@
           [nzShowPagination]="false"
           [nzBordered]="true"
           [nzSize]="'small'"
+          nzTableLayout="fixed"
         >
           <thead>
             <tr>
+              <th nzWidth="50px"></th>
               <th (click)="sortTemplateTable('templateName')">
                 模板名称
                 <span *ngIf="templateSortField === 'templateName'">
@@ -534,75 +583,105 @@
             </tr>
           </thead>
           <tbody>
-            <tr *ngFor="let item of templateTable.data">
-              <td class="template-name-col">
-                <a
-                  (click)="navigateToTemplate(item.templateName)"
-                  *ngIf="item.templateName"
+            <ng-template ngFor let-item [ngForOf]="templateTable.data">
+              <tr>
+                <td
+                  (click)="toggleExpand(item, 'template')"
+                  class="expand-icon-cell"
                 >
-                  {{ item.templateName || "-" }}
-                </a>
-                <span *ngIf="!item.templateName">-</span>
-              </td>
-              <td>{{ item.totalRecords || 0 }}</td>
-              <td>{{ item.sent || 0 }}</td>
-              <td>{{ item.delivered || 0 }}</td>
-              <td class="display-count-column">{{ item.displayCount || 0 }}</td>
-              <td>{{ item.opened || 0 }}</td>
-              <td>{{ item.failed || 0 }}</td>
-              <td>
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.deliveredRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="normal"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td class="display-rate-column">
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.displayRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="active"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td class="click-rate-column">
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.clickThroughRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="active"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td>
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.tokenInvalidationRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="exception"
-                  ></nz-progress>
-                  <span class="percentage-text">{{
-                    formatPercentage(item.tokenInvalidationRate || 0)
-                  }}</span>
-                </div>
-              </td>
-            </tr>
+                  <i
+                    nz-icon
+                    [nzType]="item.expanded ? 'down' : 'right'"
+                    class="expand-icon"
+                  ></i>
+                </td>
+                <td class="template-name-col">
+                  <a
+                    (click)="navigateToTemplate(item.templateName)"
+                    *ngIf="item.templateName"
+                  >
+                    {{ item.templateName || "-" }}
+                  </a>
+                  <span *ngIf="!item.templateName">-</span>
+                </td>
+                <td>{{ item.totalRecords || 0 }}</td>
+                <td>{{ item.sent || 0 }}</td>
+                <td>{{ item.delivered || 0 }}</td>
+                <td>{{ item.displayCount || 0 }}</td>
+                <td>{{ item.opened || 0 }}</td>
+                <td>{{ item.failed || 0 }}</td>
+                <td>{{ formatPercentage(item.deliveredRate || 0) }}</td>
+                <td>{{ formatPercentage(item.displayRate || 0) }}</td>
+                <td>{{ formatPercentage(item.clickThroughRate || 0) }}</td>
+                <td>{{ formatPercentage(item.tokenInvalidationRate || 0) }}</td>
+              </tr>
+              <tr [nzExpand]="item.expanded">
+                <td colspan="12" *ngIf="item.expanded" class="expanded-row">
+                  <div *ngIf="item.loading" class="loading-container">
+                    <nz-spin></nz-spin>
+                  </div>
+                  <div
+                    *ngIf="item.dailyData && !item.loading"
+                    class="expanded-content"
+                  >
+                    <nz-table
+                      class="expanded-table"
+                      [nzData]="item.dailyData"
+                      [nzShowPagination]="false"
+                      [nzSize]="'small'"
+                      nzBordered
+                    >
+                      <thead>
+                        <tr>
+                          <th>日期</th>
+                          <th>总发送量</th>
+                          <th>成功数</th>
+                          <th>送达数</th>
+                          <th>展示数</th>
+                          <th>打开数</th>
+                          <th>失败数</th>
+                          <th>送达率</th>
+                          <th>展示率</th>
+                          <th>点击率</th>
+                          <th>失败率</th>
+                        </tr>
+                      </thead>
+                      <tbody>
+                        <tr *ngFor="let daily of item.dailyData">
+                          <td>{{ formatDate(daily.date) }}</td>
+                          <td>{{ daily.totalRecords || 0 }}</td>
+                          <td>{{ daily.sent || 0 }}</td>
+                          <td>{{ daily.delivered || 0 }}</td>
+                          <td>{{ daily.displayCount || 0 }}</td>
+                          <td>{{ daily.opened || 0 }}</td>
+                          <td>{{ daily.failed || 0 }}</td>
+                          <td>
+                            {{ formatPercentage(daily.deliveredRate || 0) }}
+                          </td>
+                          <td>
+                            {{ formatPercentage(daily.displayRate || 0) }}
+                          </td>
+                          <td>
+                            {{ formatPercentage(daily.clickThroughRate || 0) }}
+                          </td>
+                          <td>
+                            {{
+                              formatPercentage(daily.tokenInvalidationRate || 0)
+                            }}
+                          </td>
+                        </tr>
+                      </tbody>
+                    </nz-table>
+                  </div>
+                </td>
+              </tr>
+            </ng-template>
           </tbody>
         </nz-table>
         <nz-empty *ngIf="templateStats.length === 0 && !isLoading"></nz-empty>
       </nz-tab>
 
+      <!-- 按国家统计表格 -->
       <nz-tab nzTitle="按国家统计">
         <nz-table
           #ccTable
@@ -611,9 +690,11 @@
           [nzShowPagination]="false"
           [nzBordered]="true"
           [nzSize]="'small'"
+          nzTableLayout="fixed"
         >
           <thead>
             <tr>
+              <th nzWidth="50px"></th>
               <th (click)="sortCcTable('cc')">
                 国家/地区
                 <span *ngIf="ccSortField === 'cc'">
@@ -738,69 +819,96 @@
             </tr>
           </thead>
           <tbody>
-            <tr *ngFor="let item of ccTable.data">
-              <td>
-                {{ formatCountry(item.cc) }}
-              </td>
-              <td>{{ item.totalRecords || 0 }}</td>
-              <td>{{ item.sent || 0 }}</td>
-              <td>{{ item.delivered || 0 }}</td>
-              <td class="display-count-column">{{ item.displayCount || 0 }}</td>
-              <td>{{ item.opened || 0 }}</td>
-              <td>{{ item.failed || 0 }}</td>
-              <td>
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.deliveredRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="normal"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td class="display-rate-column">
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.displayRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="active"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td class="click-rate-column">
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.clickThroughRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="active"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td>
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.tokenInvalidationRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="exception"
-                  ></nz-progress>
-                  <span class="percentage-text">{{
-                    formatPercentage(item.tokenInvalidationRate || 0)
-                  }}</span>
-                </div>
-              </td>
-            </tr>
+            <ng-template ngFor let-item [ngForOf]="ccTable.data">
+              <tr>
+                <td (click)="toggleExpand(item, 'cc')" class="expand-icon-cell">
+                  <i
+                    nz-icon
+                    [nzType]="item.expanded ? 'down' : 'right'"
+                    class="expand-icon"
+                  ></i>
+                </td>
+                <td>
+                  {{ formatCountry(item.cc) }}
+                </td>
+                <td>{{ item.totalRecords || 0 }}</td>
+                <td>{{ item.sent || 0 }}</td>
+                <td>{{ item.delivered || 0 }}</td>
+                <td>{{ item.displayCount || 0 }}</td>
+                <td>{{ item.opened || 0 }}</td>
+                <td>{{ item.failed || 0 }}</td>
+                <td>{{ formatPercentage(item.deliveredRate || 0) }}</td>
+                <td>{{ formatPercentage(item.displayRate || 0) }}</td>
+                <td>{{ formatPercentage(item.clickThroughRate || 0) }}</td>
+                <td>{{ formatPercentage(item.tokenInvalidationRate || 0) }}</td>
+              </tr>
+              <tr [nzExpand]="item.expanded">
+                <td colspan="12" *ngIf="item.expanded" class="expanded-row">
+                  <div *ngIf="item.loading" class="loading-container">
+                    <nz-spin></nz-spin>
+                  </div>
+                  <div
+                    *ngIf="item.dailyData && !item.loading"
+                    class="expanded-content"
+                  >
+                    <nz-table
+                      class="expanded-table"
+                      [nzData]="item.dailyData"
+                      [nzShowPagination]="false"
+                      [nzSize]="'small'"
+                      nzBordered
+                    >
+                      <thead>
+                        <tr>
+                          <th>日期</th>
+                          <th>总发送量</th>
+                          <th>成功数</th>
+                          <th>送达数</th>
+                          <th>展示数</th>
+                          <th>打开数</th>
+                          <th>失败数</th>
+                          <th>送达率</th>
+                          <th>展示率</th>
+                          <th>点击率</th>
+                          <th>失败率</th>
+                        </tr>
+                      </thead>
+                      <tbody>
+                        <tr *ngFor="let daily of item.dailyData">
+                          <td>{{ formatDate(daily.date) }}</td>
+                          <td>{{ daily.totalRecords || 0 }}</td>
+                          <td>{{ daily.sent || 0 }}</td>
+                          <td>{{ daily.delivered || 0 }}</td>
+                          <td>{{ daily.displayCount || 0 }}</td>
+                          <td>{{ daily.opened || 0 }}</td>
+                          <td>{{ daily.failed || 0 }}</td>
+                          <td>
+                            {{ formatPercentage(daily.deliveredRate || 0) }}
+                          </td>
+                          <td>
+                            {{ formatPercentage(daily.displayRate || 0) }}
+                          </td>
+                          <td>
+                            {{ formatPercentage(daily.clickThroughRate || 0) }}
+                          </td>
+                          <td>
+                            {{
+                              formatPercentage(daily.tokenInvalidationRate || 0)
+                            }}
+                          </td>
+                        </tr>
+                      </tbody>
+                    </nz-table>
+                  </div>
+                </td>
+              </tr>
+            </ng-template>
           </tbody>
         </nz-table>
         <nz-empty *ngIf="ccStats.length === 0 && !isLoading"></nz-empty>
       </nz-tab>
 
+      <!-- 按图片统计表格 -->
       <nz-tab nzTitle="按图片统计">
         <nz-table
           #imageTable
@@ -809,9 +917,11 @@
           [nzShowPagination]="false"
           [nzBordered]="true"
           [nzSize]="'small'"
+          nzTableLayout="fixed"
         >
           <thead>
             <tr>
+              <th nzWidth="50px"></th>
               <th>图片</th>
               <th (click)="sortImageTable('totalRecords')">
                 总发送量
@@ -946,72 +1056,101 @@
             </tr>
           </thead>
           <tbody>
-            <tr *ngFor="let item of imageTable.data">
-              <td>
-                <img
-                  nz-image
-                  width="80px"
-                  height="80px"
-                  *ngIf="item.image"
-                  nzSrc="{{ item.image }}"
-                  alt="统计图片"
-                />
-                <span *ngIf="!item.image">-</span>
-              </td>
-              <td>{{ item.totalRecords || 0 }}</td>
-              <td>{{ item.sent || 0 }}</td>
-              <td>{{ item.delivered || 0 }}</td>
-              <td class="display-count-column">{{ item.displayCount || 0 }}</td>
-              <td>{{ item.opened || 0 }}</td>
-              <td>{{ item.failed || 0 }}</td>
-              <td>
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.deliveredRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="normal"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td class="display-rate-column">
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.displayRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="active"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td class="click-rate-column">
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.clickThroughRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="active"
-                  ></nz-progress>
-                </div>
-              </td>
-              <td>
-                <div class="progress-container">
-                  <nz-progress
-                    [nzPercent]="
-                      formatPercentageToNumber(item.tokenInvalidationRate || 0)
-                    "
-                    nzSize="small"
-                    nzStatus="exception"
-                  ></nz-progress>
-                  <span class="percentage-text">{{
-                    formatPercentage(item.tokenInvalidationRate || 0)
-                  }}</span>
-                </div>
-              </td>
-            </tr>
+            <ng-template ngFor let-item [ngForOf]="imageTable.data">
+              <tr>
+                <td
+                  (click)="toggleExpand(item, 'image')"
+                  class="expand-icon-cell"
+                >
+                  <i
+                    nz-icon
+                    [nzType]="item.expanded ? 'down' : 'right'"
+                    class="expand-icon"
+                  ></i>
+                </td>
+                <td>
+                  <img
+                    nz-image
+                    width="80px"
+                    height="80px"
+                    *ngIf="item.image"
+                    nzSrc="{{ item.image }}"
+                    alt="统计图片"
+                  />
+                  <span *ngIf="!item.image">-</span>
+                </td>
+                <td>{{ item.totalRecords || 0 }}</td>
+                <td>{{ item.sent || 0 }}</td>
+                <td>{{ item.delivered || 0 }}</td>
+                <td>{{ item.displayCount || 0 }}</td>
+                <td>{{ item.opened || 0 }}</td>
+                <td>{{ item.failed || 0 }}</td>
+                <td>{{ formatPercentage(item.deliveredRate || 0) }}</td>
+                <td>{{ formatPercentage(item.displayRate || 0) }}</td>
+                <td>{{ formatPercentage(item.clickThroughRate || 0) }}</td>
+                <td>{{ formatPercentage(item.tokenInvalidationRate || 0) }}</td>
+              </tr>
+              <tr [nzExpand]="item.expanded">
+                <td colspan="12" *ngIf="item.expanded" class="expanded-row">
+                  <div *ngIf="item.loading" class="loading-container">
+                    <nz-spin></nz-spin>
+                  </div>
+                  <div
+                    *ngIf="item.dailyData && !item.loading"
+                    class="expanded-content"
+                  >
+                    <nz-table
+                      class="expanded-table"
+                      [nzData]="item.dailyData"
+                      [nzShowPagination]="false"
+                      [nzSize]="'small'"
+                      nzBordered
+                    >
+                      <thead>
+                        <tr>
+                          <th>日期</th>
+                          <th>总发送量</th>
+                          <th>成功数</th>
+                          <th>送达数</th>
+                          <th>展示数</th>
+                          <th>打开数</th>
+                          <th>失败数</th>
+                          <th>送达率</th>
+                          <th>展示率</th>
+                          <th>点击率</th>
+                          <th>失败率</th>
+                        </tr>
+                      </thead>
+                      <tbody>
+                        <tr *ngFor="let daily of item.dailyData">
+                          <td>{{ formatDate(daily.date) }}</td>
+                          <td>{{ daily.totalRecords || 0 }}</td>
+                          <td>{{ daily.sent || 0 }}</td>
+                          <td>{{ daily.delivered || 0 }}</td>
+                          <td>{{ daily.displayCount || 0 }}</td>
+                          <td>{{ daily.opened || 0 }}</td>
+                          <td>{{ daily.failed || 0 }}</td>
+                          <td>
+                            {{ formatPercentage(daily.deliveredRate || 0) }}
+                          </td>
+                          <td>
+                            {{ formatPercentage(daily.displayRate || 0) }}
+                          </td>
+                          <td>
+                            {{ formatPercentage(daily.clickThroughRate || 0) }}
+                          </td>
+                          <td>
+                            {{
+                              formatPercentage(daily.tokenInvalidationRate || 0)
+                            }}
+                          </td>
+                        </tr>
+                      </tbody>
+                    </nz-table>
+                  </div>
+                </td>
+              </tr>
+            </ng-template>
           </tbody>
         </nz-table>
         <nz-empty *ngIf="imageStats.length === 0 && !isLoading"></nz-empty>

+ 122 - 12
omsapp/src/app/pages/message-dashboard.component.ts

@@ -103,6 +103,9 @@ export class MessageDashboardComponent implements OnInit {
   // 日期范围
   dateRange: Date[] = [];
 
+  strategies: string[] = []; // 存储所有策略名称
+  selectedStrategy: string = ''; // 当前选中的策略
+
   // 组合图表配置
   public combinedChartData: ChartConfiguration<'bar' | 'line'>['data'] = {
     labels: [],
@@ -258,33 +261,46 @@ export class MessageDashboardComponent implements OnInit {
 
   ngOnInit(): void {
     this.loadAllStatistics();
+    this.loadStrategies();
   }
 
   // 格式化国家显示:国家名称(国家代码)
   formatCountry(cc: string): string {
     if (!cc) return '-';
 
-    // 统一转换为大写(ISO 3166-1 alpha-2 代码通常为大写)
     const code = cc.toUpperCase();
-
-    // 使用库获取中文名称(支持多种语言切换)
     const countryName =
       countries.getName(code, 'zh') ||
-      countries.getName(code, 'en') || // 英文作为备选
+      countries.getName(code, 'en') ||
       '未知国家';
 
     return `${countryName}(${code})`;
   }
 
+  // 获取策略列表
+  private loadStrategies(): void {
+    this.http
+      .get('/api/message-strategies')
+      .pipe(
+        map((res: any) => res || []),
+        catchError((err) => {
+          console.error('Failed to load strategies:', err);
+          this.message.error('加载策略列表失败');
+          return of([]);
+        })
+      )
+      .subscribe((data) => {
+        this.strategies = data.map((item: any) => item.name).filter(Boolean);
+      });
+  }
+
   loadAllStatistics(): void {
-    // 验证日期范围有效性
     if (!this.isDateRangeValid()) {
       return;
     }
 
     this.isLoading = true;
 
-    // 创建日期参数
     const params = new HttpParams()
       .set(
         'startDate',
@@ -293,7 +309,8 @@ export class MessageDashboardComponent implements OnInit {
       .set(
         'endDate',
         this.dateRange[1] ? this.formatDateForApi(this.dateRange[1]) : ''
-      );
+      )
+      .set('strategyName', this.selectedStrategy || '');
 
     forkJoin({
       overall: this.http
@@ -363,10 +380,30 @@ export class MessageDashboardComponent implements OnInit {
       )
       .subscribe((results: any) => {
         this.overallStats = results.overall;
-        this.strategyStats = results.strategies;
-        this.templateStats = results.templates;
-        this.ccStats = results.ccs;
-        this.imageStats = results.images;
+        this.strategyStats = results.strategies.map((item: any) => ({
+          ...item,
+          expanded: false,
+          dailyData: null,
+          loading: false,
+        }));
+        this.templateStats = results.templates.map((item: any) => ({
+          ...item,
+          expanded: false,
+          dailyData: null,
+          loading: false,
+        }));
+        this.ccStats = results.ccs.map((item: any) => ({
+          ...item,
+          expanded: false,
+          dailyData: null,
+          loading: false,
+        }));
+        this.imageStats = results.images.map((item: any) => ({
+          ...item,
+          expanded: false,
+          dailyData: null,
+          loading: false,
+        }));
         this.dailyTrends = results.dailyTrends;
 
         this.updateChartData();
@@ -604,8 +641,81 @@ export class MessageDashboardComponent implements OnInit {
     return Number(Math.round(num * multiplier) / multiplier);
   }
 
-  private formatDate(date: string | null): string {
+  public formatDate(date: string | null): string {
     if (!date) return '未知日期';
     return new Date(date).toLocaleDateString();
   }
+
+  // 展开/折叠行并加载数据
+  toggleExpand(
+    element: any,
+    type: 'strategy' | 'template' | 'cc' | 'image'
+  ): void {
+    element.expanded = !element.expanded;
+
+    // 如果展开且没有加载过数据,则加载
+    if (element.expanded && !element.dailyData && !element.loading) {
+      this.loadDailyData(element, type);
+    }
+  }
+
+  // 加载每日数据
+  private loadDailyData(element: any, type: string): void {
+    element.loading = true;
+    let url = '';
+    const params = new HttpParams()
+      .set(
+        'startDate',
+        this.dateRange[0] ? this.formatDateForApi(this.dateRange[0]) : ''
+      )
+      .set(
+        'endDate',
+        this.dateRange[1] ? this.formatDateForApi(this.dateRange[1]) : ''
+      )
+      .set('strategyName', this.selectedStrategy || '');
+
+    // 根据类型构建请求URL
+    switch (type) {
+      case 'strategy':
+        url = `/api/message/daily/trends/by-strategy/${encodeURIComponent(
+          element.strategyName
+        )}`;
+        break;
+      case 'template':
+        url = `/api/message/daily/trends/by-template/${encodeURIComponent(
+          element.templateName
+        )}`;
+        break;
+      case 'cc':
+        url = `/api/message/daily/trends/by-cc/${encodeURIComponent(
+          element.cc
+        )}`;
+        break;
+      case 'image':
+        url = `/api/message/daily/trends/by-image/${encodeURIComponent(
+          element.image
+        )}`;
+        break;
+      default:
+        element.loading = false;
+        return;
+    }
+
+    this.http
+      .get(url, { params })
+      .pipe(
+        map((res: any) => res?.data || []),
+        catchError((err) => {
+          console.error(`Error loading daily data for ${type}:`, err);
+          this.message.error(`加载每日数据失败`);
+          return of([]);
+        }),
+        finalize(() => {
+          element.loading = false;
+        })
+      )
+      .subscribe((data) => {
+        element.dailyData = data;
+      });
+  }
 }

Деякі файли не було показано, через те що забагато файлів було змінено