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