fix: 固定跨午夜日报刷新基准并补充验收记录

This commit is contained in:
hectorzhao
2026-09-17 11:14:21 +08:00
parent 572290308c
commit 627fa7ec97
2 changed files with 77 additions and 68 deletions
@@ -45,78 +45,85 @@ export class SignatureAnalyticsService implements OnModuleInit, OnModuleDestroy
/** Explicit offline backfill only; no controller exposes this write operation. Existing frozen days cannot be overwritten. */ /** Explicit offline backfill only; no controller exposes this write operation. Existing frozen days cannot be overwritten. */
async generate(value: string, backfill = false) { async generate(value: string, backfill = false) {
const date = analyticsDate(value); const startedAt = new Date();
if (!mutableDay(date) && !backfill) return { skipped: true }; const date = analyticsDate(value, startedAt);
if (date >= todayKey()) throw new Error('日报只生成完整自然日'); if (!mutableDay(date, startedAt) && !backfill) return { skipped: true };
if (date >= todayKey(startedAt)) throw new Error('日报只生成完整自然日');
const businessDate = databaseDay(date); const businessDate = databaseDay(date);
const existing = await this.db.signatureAnalyticsDay.findUnique({ where: { businessDate } }); const existing = await this.db.signatureAnalyticsDay.findUnique({ where: { businessDate } });
if (backfill && existing?.publishedGenerationId) throw new Error('历史补建不得覆盖已发布日报'); if (backfill && existing?.publishedGenerationId) throw new Error('历史补建不得覆盖已发布日报');
await this.db.signatureAnalyticsDay.upsert({ where: { businessDate }, create: { businessDate }, update: {} }); await this.db.signatureAnalyticsDay.upsert({ where: { businessDate }, create: { businessDate }, update: {} });
return await analyticsJob(this.db, 'daily', date, async (tx, generationId) => { return await analyticsJob(
const sourceAsOf = new Date(); this.db,
await tx.signatureAnalyticsGeneration.create({ data: { id: generationId, businessDate, sourceAsOf } }); 'daily',
const quality = await new OperationsQualityQueries(tx).signatureQualityLive({ date }, true); date,
const dimensions = await activityDimensions(tx, date); async (tx, generationId) => {
const counts = await activityCounts(tx, dimensions, date); const sourceAsOf = new Date();
const unreported = await unreportedRows(tx, date); await tx.signatureAnalyticsGeneration.create({ data: { id: generationId, businessDate, sourceAsOf } });
// All candidate rows and manifest publication share this transaction: readers never see half a day. const quality = await new OperationsQualityQueries(tx).signatureQualityLive({ date }, true);
for (let i = 0; i < quality.items.length; i += 250) const dimensions = await activityDimensions(tx, date);
await tx.signatureQualityDaily.createMany({ const counts = await activityCounts(tx, dimensions, date);
data: quality.items.slice(i, i + 250).map((row) => ({ const unreported = await unreportedRows(tx, date);
businessDate, // All candidate rows and manifest publication share this transaction: readers never see half a day.
generationId, for (let i = 0; i < quality.items.length; i += 250)
signatureId: row.signatureId, await tx.signatureQualityDaily.createMany({
signatureName: row.signatureName, data: quality.items.slice(i, i + 250).map((row) => ({
tenantId: row.tenantId, businessDate,
tenantName: row.tenantName, generationId,
applicationNames: row.applicationNames ?? '', signatureId: row.signatureId,
total: row.total, signatureName: row.signatureName,
payload: JSON.parse(JSON.stringify(row)) as Prisma.InputJsonValue, tenantId: row.tenantId,
})), tenantName: row.tenantName,
applicationNames: row.applicationNames ?? '',
total: row.total,
payload: JSON.parse(JSON.stringify(row)) as Prisma.InputJsonValue,
})),
});
for (let i = 0; i < dimensions.length; i += 250)
await tx.signatureActivityDaily.createMany({
data: dimensions.slice(i, i + 250).map((d) => ({
...d,
businessDate,
generationId,
...(counts.get(d.dimensionKey) ?? {
submittedAttempts: 0,
acceptedBusinessCount: 0,
deliveredBusinessCount: 0,
}),
applicability: 'applicable',
})),
});
for (let i = 0; i < unreported.length; i += 250)
await tx.unreportedSignatureDaily.createMany({
data: unreported.slice(i, i + 250).map((r) => ({ ...r, businessDate, generationId })),
});
if (!backfill && !mutableDay(date)) throw new Error('日报已进入冻结区,拒绝跨日发布');
if (backfill) {
const current = await tx.signatureAnalyticsDay.findUniqueOrThrow({ where: { businessDate } });
if (current.publishedGenerationId) throw new Error('已有发布版本,拒绝覆盖');
}
await tx.signatureAnalyticsDay.update({
where: { businessDate },
data: {
publishedGenerationId: generationId,
state: 'ready',
error: null,
generatedAt: new Date(),
sourceAsOf,
refreshFor: databaseDay(todayKey(startedAt)),
provenance: backfill ? 'backfill-current-source' : 'daily',
rowCounts: { quality: quality.items.length, activity: dimensions.length, unreported: unreported.length },
},
}); });
for (let i = 0; i < dimensions.length; i += 250) return {
await tx.signatureActivityDaily.createMany({ date,
data: dimensions.slice(i, i + 250).map((d) => ({ generationId,
...d, quality: quality.items.length,
businessDate, activity: dimensions.length,
generationId, unreported: unreported.length,
...(counts.get(d.dimensionKey) ?? { };
submittedAttempts: 0, },
acceptedBusinessCount: 0, startedAt,
deliveredBusinessCount: 0, );
}),
applicability: 'applicable',
})),
});
for (let i = 0; i < unreported.length; i += 250)
await tx.unreportedSignatureDaily.createMany({
data: unreported.slice(i, i + 250).map((r) => ({ ...r, businessDate, generationId })),
});
if (!backfill && !mutableDay(date)) throw new Error('日报已进入冻结区,拒绝跨日发布');
if (backfill) {
const current = await tx.signatureAnalyticsDay.findUniqueOrThrow({ where: { businessDate } });
if (current.publishedGenerationId) throw new Error('已有发布版本,拒绝覆盖');
}
await tx.signatureAnalyticsDay.update({
where: { businessDate },
data: {
publishedGenerationId: generationId,
state: 'ready',
error: null,
generatedAt: new Date(),
sourceAsOf,
refreshFor: databaseDay(todayKey()),
provenance: backfill ? 'backfill-current-source' : 'daily',
rowCounts: { quality: quality.items.length, activity: dimensions.length, unreported: unreported.length },
},
});
return {
date,
generationId,
quality: quality.items.length,
activity: dimensions.length,
unreported: unreported.length,
};
});
} }
} }
+2
View File
@@ -5137,3 +5137,5 @@ CUA本轮可用,实际后端文档三尺寸1600×1000/1366×768/390×844无页
- 性能:30000消息/600维度活动聚合,旧10343.912ms,新624.940ms,热缓存下降93.96%accepted逐维度一致。原EXPLAIN单维度源访问30113行×600外推18067800,新批量60039;样本外推不冒称全批实测。未测整机CPU、365日真实大批次、峰值内存、p95和短信队列;不能据此宣布预生产CPU问题已完全解决。 - 性能:30000消息/600维度活动聚合,旧10343.912ms,新624.940ms,热缓存下降93.96%accepted逐维度一致。原EXPLAIN单维度源访问30113行×600外推18067800,新批量60039;样本外推不冒称全批实测。未测整机CPU、365日真实大批次、峰值内存、p95和短信队列;不能据此宣布预生产CPU问题已完全解决。
- 原始证据保留于忽略目录`.local-data/signature-optimization-20260917/`integration-6、uplink-real-3、browser-5、performance-1及最终门禁日志。前面失败(迁移SQL未生成、缺carriers、测试断言漏计新增样本、账号缺email、浏览器按钮名称/模糊标签定位)已修正后重跑;不删除原失败日志。Redis5.0.14.1的BullMQ版本建议及PG驱动在嵌套关系读取时的并发query弃用提示保留,未改依赖版本。 - 原始证据保留于忽略目录`.local-data/signature-optimization-20260917/`integration-6、uplink-real-3、browser-5、performance-1及最终门禁日志。前面失败(迁移SQL未生成、缺carriers、测试断言漏计新增样本、账号缺email、浏览器按钮名称/模糊标签定位)已修正后重跑;不删除原失败日志。Redis5.0.14.1的BullMQ版本建议及PG驱动在嵌套关系读取时的并发query弃用提示保留,未改依赖版本。
- 文档:优化方案第8节、上行诊断第6节、需求/测试用例/清退设计同步。本地提交仅纳入本轮代码、两项迁移、验收脚本及相关文档精确新增段落;已有metrics、版本、部署和发布工具修改保留。推送:未执行;测试部署:未执行;预生产部署:未执行。历史日报补建、真实外部MO投递、线上权限/供应商扩展码、午夜冻结过程、现场恢复/回退及完整容量指标未验证,按后续授权实施。 - 文档:优化方案第8节、上行诊断第6节、需求/测试用例/清退设计同步。本地提交仅纳入本轮代码、两项迁移、验收脚本及相关文档精确新增段落;已有metrics、版本、部署和发布工具修改保留。推送:未执行;测试部署:未执行;预生产部署:未执行。历史日报补建、真实外部MO投递、线上权限/供应商扩展码、午夜冻结过程、现场恢复/回退及完整容量指标未验证,按后续授权实施。
- 最终补充:API全量覆盖率语句67.76%/分支52.88%/函数68.62%/行70.54%,增量覆盖率87.98%/77.03%/95.91%/91.22%,均通过各自门槛;前端覆盖率88.48%/85.09%/84%/88.19%163项通过。固定日报refreshFor为任务启动日后,类型检查、9项日期测试及真实PG模拟跨午夜通过:T-1跨日仍记录原启动日,T-3跨入T-4拒绝发布且回滚。最终browser-final仍为三尺寸28请求、页面异常0。最后停止本轮本机PG/Redis/预览服务,保留验收库及日志资产。测试基于受保护工作区,发布时仍由标准工具核验精确提交证据;未把本轮本地检查冒称部署验收。