fix(ops): 企微报告保存改走 Prisma Client

空 @成员 会把 null 插进 tagged raw SQL,MySQL 下 Prisma 报 Code N/A。
This commit is contained in:
2026-09-02 22:15:07 +08:00
parent 92dfbf5722
commit e5d7e468cc
@@ -6,7 +6,7 @@ import {
OnModuleInit, OnModuleInit,
} from '@nestjs/common'; } from '@nestjs/common';
import { Cron } from '@nestjs/schedule'; import { Cron } from '@nestjs/schedule';
import { Prisma } from '@prisma/client'; import { Prisma, type WecomReportPush } from '@prisma/client';
import { import {
formatWecomReportMarkdown, formatWecomReportMarkdown,
isWecomReportKind, isWecomReportKind,
@@ -56,26 +56,7 @@ function clampMinute(n: number | undefined, fallback: number): number {
return Math.min(59, Math.max(0, Math.floor(n))); return Math.min(59, Math.max(0, Math.floor(n)));
} }
type ReportRow = { type ReportRow = WecomReportPush;
id: bigint;
kind: string;
name: string;
webhookUrl: string;
enabled: boolean | number;
mentionWecomUserId: string | null;
sendHour: number;
sendMinute: number;
sendWeekday: number;
sendMonthDay: number;
lastSentPeriod: string | null;
lastSentAt: Date | null;
createdAt: Date;
updatedAt: Date;
};
function asBool(v: boolean | number): boolean {
return v === true || v === 1;
}
@Injectable() @Injectable()
export class AdminWecomReportsService implements OnModuleInit { export class AdminWecomReportsService implements OnModuleInit {
@@ -98,42 +79,23 @@ export class AdminWecomReportsService implements OnModuleInit {
async ensureDefaults(): Promise<void> { async ensureDefaults(): Promise<void> {
for (const seed of KIND_SEED) { for (const seed of KIND_SEED) {
await this.prisma.$executeRaw` await this.prisma.wecomReportPush.upsert({
INSERT IGNORE INTO wecom_report_push where: { kind: seed.kind },
(kind, name, webhook_url, enabled, send_hour, send_minute, send_weekday, send_month_day) create: {
VALUES kind: seed.kind,
(${seed.kind}, ${seed.name}, ${WECOM_STORE_AUDIT_PLACEHOLDER_WEBHOOK}, 0, ${seed.sendHour}, ${seed.sendMinute}, 1, 1) name: seed.name,
`; webhookUrl: WECOM_STORE_AUDIT_PLACEHOLDER_WEBHOOK,
enabled: false,
sendHour: seed.sendHour,
sendMinute: seed.sendMinute,
sendWeekday: 1,
sendMonthDay: 1,
},
update: {},
});
} }
} }
private async findByKind(kind: string): Promise<ReportRow | null> {
const rows = await this.prisma.$queryRaw<ReportRow[]>`
SELECT id, kind, name,
webhook_url AS webhookUrl, enabled,
mention_wecom_user_id AS mentionWecomUserId,
send_hour AS sendHour, send_minute AS sendMinute,
send_weekday AS sendWeekday, send_month_day AS sendMonthDay,
last_sent_period AS lastSentPeriod, last_sent_at AS lastSentAt,
created_at AS createdAt, updated_at AS updatedAt
FROM wecom_report_push WHERE kind = ${kind} LIMIT 1
`;
return rows[0] ?? null;
}
private async findAll(): Promise<ReportRow[]> {
return this.prisma.$queryRaw<ReportRow[]>`
SELECT id, kind, name,
webhook_url AS webhookUrl, enabled,
mention_wecom_user_id AS mentionWecomUserId,
send_hour AS sendHour, send_minute AS sendMinute,
send_weekday AS sendWeekday, send_month_day AS sendMonthDay,
last_sent_period AS lastSentPeriod, last_sent_at AS lastSentAt,
created_at AS createdAt, updated_at AS updatedAt
FROM wecom_report_push
`;
}
parseKind(raw: string): WecomReportKind { parseKind(raw: string): WecomReportKind {
if (!isWecomReportKind(raw)) { if (!isWecomReportKind(raw)) {
throw new BadRequestException('报告类型须为 daily / weekly / monthly'); throw new BadRequestException('报告类型须为 daily / weekly / monthly');
@@ -143,7 +105,7 @@ export class AdminWecomReportsService implements OnModuleInit {
async list(): Promise<WecomReportPushDto[]> { async list(): Promise<WecomReportPushDto[]> {
await this.ensureDefaults(); await this.ensureDefaults();
const rows = await this.findAll(); const rows = await this.prisma.wecomReportPush.findMany();
const byKind = new Map(rows.map((r) => [r.kind, r])); const byKind = new Map(rows.map((r) => [r.kind, r]));
return KIND_SEED.map((s) => { return KIND_SEED.map((s) => {
const row = byKind.get(s.kind); const row = byKind.get(s.kind);
@@ -154,14 +116,14 @@ export class AdminWecomReportsService implements OnModuleInit {
async detail(kind: WecomReportKind): Promise<WecomReportPushDto> { async detail(kind: WecomReportKind): Promise<WecomReportPushDto> {
await this.ensureDefaults(); await this.ensureDefaults();
const row = await this.findByKind(kind); const row = await this.prisma.wecomReportPush.findUnique({ where: { kind } });
if (!row) throw new NotFoundException('报告配置不存在'); if (!row) throw new NotFoundException('报告配置不存在');
return this.toDto(row); return this.toDto(row);
} }
async update(kind: WecomReportKind, dto: UpdateWecomReportPushRequest): Promise<WecomReportPushDto> { async update(kind: WecomReportKind, dto: UpdateWecomReportPushRequest): Promise<WecomReportPushDto> {
await this.ensureDefaults(); await this.ensureDefaults();
const existing = await this.findByKind(kind); const existing = await this.prisma.wecomReportPush.findUnique({ where: { kind } });
if (!existing) throw new NotFoundException('报告配置不存在'); if (!existing) throw new NotFoundException('报告配置不存在');
const webhookUrl = const webhookUrl =
@@ -169,8 +131,8 @@ export class AdminWecomReportsService implements OnModuleInit {
if (!webhookUrl) throw new BadRequestException('请填写 Webhook URL'); if (!webhookUrl) throw new BadRequestException('请填写 Webhook URL');
const name = dto.name !== undefined ? dto.name.trim() || existing.name : existing.name; const name = dto.name !== undefined ? dto.name.trim() || existing.name : existing.name;
const enabled = dto.enabled !== undefined ? (dto.enabled ? 1 : 0) : asBool(existing.enabled) ? 1 : 0; const enabled = dto.enabled !== undefined ? dto.enabled : existing.enabled;
const mention = const mentionWecomUserId =
dto.mentionWecomUserId === undefined dto.mentionWecomUserId === undefined
? existing.mentionWecomUserId ? existing.mentionWecomUserId
: dto.mentionWecomUserId?.trim() || null; : dto.mentionWecomUserId?.trim() || null;
@@ -186,20 +148,19 @@ export class AdminWecomReportsService implements OnModuleInit {
? Math.min(31, Math.max(1, Math.floor(dto.sendMonthDay) || 1)) ? Math.min(31, Math.max(1, Math.floor(dto.sendMonthDay) || 1))
: existing.sendMonthDay; : existing.sendMonthDay;
await this.prisma.$executeRaw` const row = await this.prisma.wecomReportPush.update({
UPDATE wecom_report_push SET where: { kind },
name = ${name}, data: {
webhook_url = ${webhookUrl}, name,
enabled = ${enabled}, webhookUrl,
mention_wecom_user_id = ${mention}, enabled,
send_hour = ${sendHour}, mentionWecomUserId,
send_minute = ${sendMinute}, sendHour,
send_weekday = ${sendWeekday}, sendMinute,
send_month_day = ${sendMonthDay} sendWeekday,
WHERE kind = ${kind} sendMonthDay,
`; },
const row = await this.findByKind(kind); });
if (!row) throw new NotFoundException('报告配置不存在');
return this.toDto(row); return this.toDto(row);
} }
@@ -218,7 +179,7 @@ export class AdminWecomReportsService implements OnModuleInit {
async send(kind: WecomReportKind, opts?: { markSent?: boolean }): Promise<WecomReportSendResultDto> { async send(kind: WecomReportKind, opts?: { markSent?: boolean }): Promise<WecomReportSendResultDto> {
await this.ensureDefaults(); await this.ensureDefaults();
const row = await this.findByKind(kind); const row = await this.prisma.wecomReportPush.findUnique({ where: { kind } });
if (!row) throw new NotFoundException('报告配置不存在'); if (!row) throw new NotFoundException('报告配置不存在');
const url = row.webhookUrl.trim(); const url = row.webhookUrl.trim();
if (!url || url.includes('key=PENDING')) { if (!url || url.includes('key=PENDING')) {
@@ -236,12 +197,10 @@ export class AdminWecomReportsService implements OnModuleInit {
throw new BadRequestException('Webhook 发送失败,请检查地址或群机器人是否可用'); throw new BadRequestException('Webhook 发送失败,请检查地址或群机器人是否可用');
} }
if (opts?.markSent !== false) { if (opts?.markSent !== false) {
const sentAt = new Date(); await this.prisma.wecomReportPush.update({
await this.prisma.$executeRaw` where: { kind },
UPDATE wecom_report_push data: { lastSentPeriod: period.periodKey, lastSentAt: new Date() },
SET last_sent_period = ${period.periodKey}, last_sent_at = ${sentAt} });
WHERE kind = ${kind}
`;
} }
return { return {
ok: true, ok: true,
@@ -258,14 +217,14 @@ export class AdminWecomReportsService implements OnModuleInit {
return; return;
} }
const now = new Date(); const now = new Date();
const rows = await this.findAll(); const rows = await this.prisma.wecomReportPush.findMany();
for (const row of rows) { for (const row of rows) {
if (!asBool(row.enabled)) continue; if (!row.enabled) continue;
if (!isWecomReportKind(row.kind)) continue; if (!isWecomReportKind(row.kind)) continue;
const due = wecomReportShouldFire( const due = wecomReportShouldFire(
row.kind, row.kind,
{ {
enabled: asBool(row.enabled), enabled: row.enabled,
sendHour: row.sendHour, sendHour: row.sendHour,
sendMinute: row.sendMinute, sendMinute: row.sendMinute,
sendWeekday: row.sendWeekday, sendWeekday: row.sendWeekday,
@@ -366,7 +325,7 @@ export class AdminWecomReportsService implements OnModuleInit {
name: row.name, name: row.name,
webhookUrl: row.webhookUrl, webhookUrl: row.webhookUrl,
webhookUrlMasked: maskWecomWebhookUrl(row.webhookUrl), webhookUrlMasked: maskWecomWebhookUrl(row.webhookUrl),
enabled: asBool(row.enabled), enabled: row.enabled,
mentionWecomUserId: row.mentionWecomUserId, mentionWecomUserId: row.mentionWecomUserId,
sendHour: row.sendHour, sendHour: row.sendHour,
sendMinute: row.sendMinute, sendMinute: row.sendMinute,