fix: 固定跨午夜日报刷新基准并补充验收记录
This commit is contained in:
@@ -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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/预览服务,保留验收库及日志资产。测试基于受保护工作区,发布时仍由标准工具核验精确提交证据;未把本轮本地检查冒称部署验收。
|
||||||
|
|||||||
Reference in New Issue
Block a user