This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { addDays, startOfDay, todayKey } from '../signature-analytics/analytics-date';
|
||||
import { sourceFacts } from './home-source';
|
||||
import { pruneHomeVersions } from './home-retention';
|
||||
|
||||
@Injectable()
|
||||
export class HomeProjection implements OnModuleInit, OnModuleDestroy {
|
||||
private timer?: NodeJS.Timeout;
|
||||
private readonly logger = new Logger(HomeProjection.name);
|
||||
constructor(private readonly db: PrismaService) {}
|
||||
onModuleInit() {
|
||||
if (
|
||||
process.env.NODE_ENV === 'test' ||
|
||||
process.env.HOME_DASHBOARD_ENABLED === 'false' ||
|
||||
(process.env.CMPP_PROCESS_ROLE && process.env.CMPP_PROCESS_ROLE !== 'api')
|
||||
)
|
||||
return;
|
||||
this.timer = setInterval(() => void this.run(), 10_000);
|
||||
this.timer.unref();
|
||||
void this.run();
|
||||
}
|
||||
onModuleDestroy() {
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
}
|
||||
private async run() {
|
||||
try {
|
||||
await this.tick();
|
||||
} catch (error) {
|
||||
this.logger.error('首页统计投影失败', error instanceof Error ? error.stack : String(error));
|
||||
await this.db.homeProjectionState
|
||||
.update({ where: { id: 'home' }, data: { lastError: '统计更新失败,等待重试' } })
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
async tick(now = new Date()) {
|
||||
return this.db.$transaction(
|
||||
async (tx) => {
|
||||
const [lock] = await tx.$queryRaw<
|
||||
Array<{ locked: boolean }>
|
||||
>`SELECT pg_try_advisory_xact_lock(17100917) AS locked`;
|
||||
if (!lock.locked) return { busy: true };
|
||||
const date = todayKey(now),
|
||||
first = addDays(date, -3);
|
||||
const state = await tx.homeProjectionState.findUniqueOrThrow({ where: { id: 'home' } });
|
||||
if (state.seededDay !== date) {
|
||||
await tx.$executeRaw`INSERT INTO "HomeProjectionDirty"("messageRecordId") SELECT id FROM "SmsMessageRecord"
|
||||
WHERE "queuedAt">=${startOfDay(first)} AND "queuedAt"<${startOfDay(addDays(date, 1))} ON CONFLICT DO NOTHING`;
|
||||
}
|
||||
const work = await tx.$queryRaw<
|
||||
Array<{ messageRecordId: string }>
|
||||
>`SELECT "messageRecordId" FROM "HomeProjectionDirty" ORDER BY "enqueuedAt","messageRecordId" LIMIT 500 FOR UPDATE SKIP LOCKED`;
|
||||
const ids = work.map((w) => w.messageRecordId),
|
||||
version = state.version + 1;
|
||||
const sources = await sourceFacts(tx, ids);
|
||||
const prior = await tx.homeMessageFact.findMany({ where: { messageRecordId: { in: ids }, toVersion: null } });
|
||||
for (const id of ids) {
|
||||
const next = sources.find((s) => s.message.id === id);
|
||||
const old = prior.find((p) => p.messageRecordId === id);
|
||||
const day = next ? todayKey(next.message.queuedAt) : '';
|
||||
const payload = next && day >= first && day <= date ? next.fact : null;
|
||||
if (old && old.queuedDay === day && JSON.stringify(old.payload) === JSON.stringify(payload)) continue;
|
||||
if (old)
|
||||
await tx.homeMessageFact.update({
|
||||
where: { messageRecordId_fromVersion: { messageRecordId: id, fromVersion: old.fromVersion } },
|
||||
data: { toVersion: version },
|
||||
});
|
||||
if (payload)
|
||||
await tx.homeMessageFact.create({
|
||||
data: {
|
||||
messageRecordId: id,
|
||||
fromVersion: version,
|
||||
queuedDay: day,
|
||||
payload: payload as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
await tx.homeProjectionDirty.deleteMany({ where: { messageRecordId: { in: ids } } });
|
||||
const remaining = await tx.homeProjectionDirty.count();
|
||||
await tx.homeProjectionState.update({
|
||||
where: { id: 'home' },
|
||||
data: {
|
||||
version,
|
||||
seededDay: date,
|
||||
initialized: (state.seededDay === date && state.initialized) || remaining === 0,
|
||||
updatedAt: now,
|
||||
lastError: null,
|
||||
},
|
||||
});
|
||||
if (remaining === 0) await pruneHomeVersions(tx, now, addDays(first, -1), version);
|
||||
return { remaining, version };
|
||||
},
|
||||
{ timeout: 60_000, isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user