refactor(logging): replace console.* with module loggers in server modules

34 个服务端文件替换 86 处 console.* 调用为 createModuleLogger。模块覆盖: exams/audit/school/files/classes/notifications/grades/auth/homework/announcements/settings/lesson-preparation。shared lib: cache/redis-store, rate-limit/redis-limiter, redis-client, exam-homework-port。API routes: web-vitals, cron/audit-cleanup, proctoring/event。

web-vitals/route.ts 从 edge runtime 改为 nodejs runtime,因 pino 依赖 Node.js stream 内置模块,不兼容 Edge Runtime (V8 Isolate)。
This commit is contained in:
SpecialX
2026-07-07 12:34:57 +08:00
parent 0a034945d4
commit 12a766d3ee
34 changed files with 266 additions and 141 deletions

View File

@@ -2,6 +2,9 @@ import { NextResponse } from "next/server"
import { getAuditRetentionConfig, purgeExpiredAuditLogs } from "@/modules/audit/retention" import { getAuditRetentionConfig, purgeExpiredAuditLogs } from "@/modules/audit/retention"
import { trackEvent } from "@/shared/lib/track-event" import { trackEvent } from "@/shared/lib/track-event"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("audit-cleanup")
/** /**
* 审计日志定时清理 Cron 端点 * 审计日志定时清理 Cron 端点
@@ -27,7 +30,7 @@ export async function GET(request: Request): Promise<NextResponse> {
const authHeader = request.headers.get("authorization") const authHeader = request.headers.get("authorization")
const secret = process.env.CRON_SECRET const secret = process.env.CRON_SECRET
if (!secret) { if (!secret) {
console.error("[audit-cleanup] CRON_SECRET is not configured") log.error("CRON_SECRET is not configured")
return NextResponse.json({ error: "Cron secret not configured" }, { status: 500 }) return NextResponse.json({ error: "Cron secret not configured" }, { status: 500 })
} }
if (authHeader !== `Bearer ${secret}`) { if (authHeader !== `Bearer ${secret}`) {
@@ -69,7 +72,7 @@ export async function GET(request: Request): Promise<NextResponse> {
result, result,
}) })
} catch (error) { } catch (error) {
console.error("[audit-cleanup] failed:", error) log.error({ err: error }, "audit-cleanup failed")
return NextResponse.json( return NextResponse.json(
{ error: error instanceof Error ? error.message : "Unknown error" }, { error: error instanceof Error ? error.message : "Unknown error" },
{ status: 500 }, { status: 500 },

View File

@@ -6,6 +6,9 @@ import { examSubmissions } from "@/shared/db/schema"
import { and, eq } from "drizzle-orm" import { and, eq } from "drizzle-orm"
import { recordProctoringEvent } from "@/modules/proctoring/data-access" import { recordProctoringEvent } from "@/modules/proctoring/data-access"
import type { ProctoringEventType } from "@/modules/proctoring/types" import type { ProctoringEventType } from "@/modules/proctoring/types"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("proctoring")
export const dynamic = "force-dynamic" export const dynamic = "force-dynamic"
@@ -82,7 +85,7 @@ export async function POST(req: Request) {
{ status: 401 }, { status: 401 },
) )
} }
console.error("POST /api/proctoring/event error:", error) log.error({ err: error }, "POST /api/proctoring/event error")
return NextResponse.json( return NextResponse.json(
{ success: false, message: "Failed to record proctoring event" }, { success: false, message: "Failed to record proctoring event" },
{ status: 500 }, { status: 500 },

View File

@@ -1,4 +1,7 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { createModuleLogger } from "@/shared/lib/logger";
const log = createModuleLogger("web-vitals");
/** /**
* Web Vitals 上报端点 * Web Vitals 上报端点
@@ -10,7 +13,7 @@ import { NextResponse } from "next/server";
* - LCP ≤ 2.5s,INP ≤ 200ms,CLS ≤ 0.05,TTFB ≤ 800ms,FCP ≤ 1.8s * - LCP ≤ 2.5s,INP ≤ 200ms,CLS ≤ 0.05,TTFB ≤ 800ms,FCP ≤ 1.8s
* - 超出预算的 "poor" 级别会以 warn 级别日志记录便于排查 * - 超出预算的 "poor" 级别会以 warn 级别日志记录便于排查
*/ */
export const runtime = "edge"; export const runtime = "nodejs";
type WebVitalEntry = { type WebVitalEntry = {
name: string; name: string;
@@ -52,21 +55,27 @@ export async function POST(request: Request): Promise<NextResponse> {
// good/info 级别用 info,rating poor 或超预算用 warn // good/info 级别用 info,rating poor 或超预算用 warn
if (payload.rating === "poor" || isOverBudget) { if (payload.rating === "poor" || isOverBudget) {
console.warn("[web-vitals] over-budget", { log.warn(
name: payload.name, {
value: payload.value, name: payload.name,
rating: payload.rating, value: payload.value,
budget, rating: payload.rating,
path: payload.path, budget,
id: payload.id, path: payload.path,
}); id: payload.id,
},
"over-budget",
);
} else { } else {
console.info("[web-vitals] reported", { log.info(
name: payload.name, {
value: payload.value, name: payload.name,
rating: payload.rating, value: payload.value,
path: payload.path, rating: payload.rating,
}); path: payload.path,
},
"reported",
);
} }
return NextResponse.json({ ok: true }); return NextResponse.json({ ok: true });

View File

@@ -27,6 +27,9 @@ import {
resolveAnnouncementTargetUserIds, resolveAnnouncementTargetUserIds,
} from "./data-access" } from "./data-access"
import type { GetAnnouncementsParams, Announcement } from "./types" import type { GetAnnouncementsParams, Announcement } from "./types"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("announcements")
/** /**
* P1-7: 统一错误处理。 * P1-7: 统一错误处理。
@@ -39,7 +42,7 @@ async function handleActionError(
actionName: string, actionName: string,
t: (key: string) => string t: (key: string) => string
): Promise<ActionState<never>> { ): Promise<ActionState<never>> {
console.error(`[announcements] ${actionName} failed:`, e) log.error({ err: e, action: actionName }, `${actionName} failed`)
void trackEvent({ void trackEvent({
event: "announcement.action_error", event: "announcement.action_error",
@@ -89,7 +92,7 @@ async function notifyAnnouncementPublished(announcement: Announcement): Promise<
await sendBatchNotifications(payloads) await sendBatchNotifications(payloads)
} catch (error) { } catch (error) {
// 通知发送失败不阻塞公告发布流程,仅记录错误 // 通知发送失败不阻塞公告发布流程,仅记录错误
console.error("Failed to send announcement notifications:", error) log.error({ err: error }, "Failed to send announcement notifications")
} }
} }

View File

@@ -4,6 +4,7 @@ import { and, asc, desc, eq, gte, lte, count, like, type SQL, sql } from "drizzl
import { db } from "@/shared/db" import { db } from "@/shared/db"
import { cacheFn } from "@/shared/lib/cache" import { cacheFn } from "@/shared/lib/cache"
import { createModuleLogger } from "@/shared/lib/logger"
import { auditLogs, loginLogs, dataChangeLogs } from "@/shared/db/schema" import { auditLogs, loginLogs, dataChangeLogs } from "@/shared/db/schema"
import type { import type {
AuditLog, AuditLog,
@@ -19,6 +20,8 @@ import type {
PaginatedResult, PaginatedResult,
} from "./types" } from "./types"
const log = createModuleLogger("audit")
const toIso = (d: Date): string => d.toISOString() const toIso = (d: Date): string => d.toISOString()
const DEFAULT_PAGE_SIZE = 20 const DEFAULT_PAGE_SIZE = 20
@@ -85,7 +88,7 @@ export async function getAuditLogsRaw(
totalPages: Math.ceil(total / pageSize), totalPages: Math.ceil(total / pageSize),
} }
} catch (error) { } catch (error) {
console.error("getAuditLogs failed:", error) log.error({ err: error }, "getAuditLogs failed")
throw error throw error
} }
} }
@@ -143,7 +146,7 @@ export async function getLoginLogsRaw(
totalPages: Math.ceil(total / pageSize), totalPages: Math.ceil(total / pageSize),
} }
} catch (error) { } catch (error) {
console.error("getLoginLogs failed:", error) log.error({ err: error }, "getLoginLogs failed")
throw error throw error
} }
} }
@@ -162,7 +165,7 @@ export async function getAuditModuleOptionsRaw(): Promise<string[]> {
.orderBy(asc(auditLogs.module)) .orderBy(asc(auditLogs.module))
return rows.map((r) => r.module) return rows.map((r) => r.module)
} catch (error) { } catch (error) {
console.error("getAuditModuleOptions failed:", error) log.error({ err: error }, "getAuditModuleOptions failed")
throw error throw error
} }
} }
@@ -222,7 +225,7 @@ export async function getDataChangeLogsRaw(
totalPages: Math.ceil(total / pageSize), totalPages: Math.ceil(total / pageSize),
} }
} catch (error) { } catch (error) {
console.error("getDataChangeLogs failed:", error) log.error({ err: error }, "getDataChangeLogs failed")
throw error throw error
} }
} }
@@ -245,7 +248,7 @@ export async function getDataChangeStatsRaw(): Promise<DataChangeStat[]> {
.orderBy(desc(count())) .orderBy(desc(count()))
return rows.map((r) => ({ tableName: r.tableName, count: Number(r.count) })) return rows.map((r) => ({ tableName: r.tableName, count: Number(r.count) }))
} catch (error) { } catch (error) {
console.error("getDataChangeStats failed:", error) log.error({ err: error }, "getDataChangeStats failed")
throw error throw error
} }
} }
@@ -264,7 +267,7 @@ export async function getDataChangeTableOptionsRaw(): Promise<string[]> {
.orderBy(asc(dataChangeLogs.tableName)) .orderBy(asc(dataChangeLogs.tableName))
return rows.map((r) => r.tableName) return rows.map((r) => r.tableName)
} catch (error) { } catch (error) {
console.error("getDataChangeTableOptions failed:", error) log.error({ err: error }, "getDataChangeTableOptions failed")
throw error throw error
} }
} }
@@ -377,7 +380,7 @@ export async function getAuditOverviewStatsRaw(): Promise<AuditOverviewStats> {
totalAuditLogs: Number(totalAudit[0]?.value ?? 0), totalAuditLogs: Number(totalAudit[0]?.value ?? 0),
} }
} catch (error) { } catch (error) {
console.error("getAuditOverviewStats failed:", error) log.error({ err: error }, "getAuditOverviewStats failed")
throw error throw error
} }
} }
@@ -442,7 +445,7 @@ export async function getAuditTrendRaw(days: number = 7): Promise<AuditTrendPoin
} }
return result return result
} catch (error) { } catch (error) {
console.error("getAuditTrend failed:", error) log.error({ err: error }, "getAuditTrend failed")
throw error throw error
} }
} }
@@ -467,7 +470,7 @@ export async function getDataChangeActionStatsRaw(): Promise<DataChangeActionSta
.groupBy(dataChangeLogs.action) .groupBy(dataChangeLogs.action)
return rows.map((r) => ({ action: r.action, count: Number(r.count) })) return rows.map((r) => ({ action: r.action, count: Number(r.count) }))
} catch (error) { } catch (error) {
console.error("getDataChangeActionStats failed:", error) log.error({ err: error }, "getDataChangeActionStats failed")
throw error throw error
} }
} }

View File

@@ -8,6 +8,7 @@ import {
getSystemSetting, getSystemSetting,
upsertSystemSetting, upsertSystemSetting,
} from "@/modules/settings/data-access-system-settings" } from "@/modules/settings/data-access-system-settings"
import { createModuleLogger } from "@/shared/lib/logger"
import { import {
DEFAULT_RETENTION_DAYS, DEFAULT_RETENTION_DAYS,
DEFAULT_LOGIN_LOG_RETENTION_DAYS, DEFAULT_LOGIN_LOG_RETENTION_DAYS,
@@ -16,6 +17,8 @@ import {
} from "./types" } from "./types"
import type { AuditRetentionConfig, PurgeResult } from "./types" import type { AuditRetentionConfig, PurgeResult } from "./types"
const log = createModuleLogger("audit")
// 保留天数常量从 types.ts 重新导出,保持现有 import 路径兼容 // 保留天数常量从 types.ts 重新导出,保持现有 import 路径兼容
export { export {
DEFAULT_RETENTION_DAYS, DEFAULT_RETENTION_DAYS,
@@ -80,7 +83,7 @@ export async function getAuditRetentionConfig(): Promise<AuditRetentionConfig> {
autoCleanupEnabled, autoCleanupEnabled,
} }
} catch (error) { } catch (error) {
console.error("getAuditRetentionConfig failed:", error) log.error({ err: error }, "getAuditRetentionConfig failed")
throw error throw error
} }
} }
@@ -130,7 +133,7 @@ export async function saveAuditRetentionConfig(
}), }),
]) ])
} catch (error) { } catch (error) {
console.error("saveAuditRetentionConfig failed:", error) log.error({ err: error }, "saveAuditRetentionConfig failed")
throw error throw error
} }
} }
@@ -177,7 +180,7 @@ export async function purgeExpiredAuditLogs(
dataChangeLogsDeleted: dataChangeResult[0].affectedRows, dataChangeLogsDeleted: dataChangeResult[0].affectedRows,
} }
} catch (error) { } catch (error) {
console.error("purgeExpiredAuditLogs failed:", error) log.error({ err: error }, "purgeExpiredAuditLogs failed")
throw error throw error
} }
} }

View File

@@ -22,6 +22,9 @@ import {
type RegisterErrorCode, type RegisterErrorCode,
type RegisterResult, type RegisterResult,
} from "./types" } from "./types"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("auth")
/** /**
* 注册速率限制规则。 * 注册速率限制规则。
@@ -245,8 +248,12 @@ export async function registerAction(
if (!consumed) { if (!consumed) {
// 邀请码已被他人使用(极端并发场景) // 邀请码已被他人使用(极端并发场景)
console.warn( log.warn(
`[register] Invitation code ${invitationCodeForConsume} was already consumed by another user (email=${normalizedEmail})`, {
invitationCode: invitationCodeForConsume,
email: normalizedEmail,
},
"Invitation code was already consumed by another user",
) )
} else { } else {
void trackEvent({ void trackEvent({

View File

@@ -17,6 +17,7 @@ import {
usersToRoles, usersToRoles,
} from "@/shared/db/schema" } from "@/shared/db/schema"
import { ROLE_NAMES } from "@/shared/types/permissions" import { ROLE_NAMES } from "@/shared/types/permissions"
import { createModuleLogger } from "@/shared/lib/logger"
import { DEFAULT_CLASS_SUBJECTS } from "./types" import { DEFAULT_CLASS_SUBJECTS } from "./types"
import type { import type {
AdminClassListItem, AdminClassListItem,
@@ -33,6 +34,7 @@ import {
isDuplicateInvitationCodeError, isDuplicateInvitationCodeError,
} from "./data-access" } from "./data-access"
const log = createModuleLogger("classes")
const isClassSubject = (v: unknown): v is ClassSubject => const isClassSubject = (v: unknown): v is ClassSubject =>
typeof v === "string" && (DEFAULT_CLASS_SUBJECTS as readonly string[]).includes(v) typeof v === "string" && (DEFAULT_CLASS_SUBJECTS as readonly string[]).includes(v)
@@ -88,7 +90,7 @@ export const getAdminClassesRaw = async (): Promise<AdminClassListItem[]> => {
asc(classes.room) asc(classes.room)
) )
} catch (error) { } catch (error) {
console.error("getAdminClasses primary query failed, falling back:", error) log.error({ err: error }, "getAdminClasses primary query failed, falling back")
return await db return await db
.select({ .select({
id: classes.id, id: classes.id,
@@ -249,7 +251,7 @@ export const getGradeManagedClassesRaw = async (userId: string): Promise<AdminCl
asc(classes.room) asc(classes.room)
) )
} catch (error) { } catch (error) {
console.error("getGradeManagedClasses primary query failed:", error) log.error({ err: error }, "getGradeManagedClasses primary query failed")
return [] return []
} }
})(), })(),

View File

@@ -15,6 +15,7 @@ import {
getHomeworkSubmissionsForAssignments, getHomeworkSubmissionsForAssignments,
getPublishedHomeworkAssignmentsWithSubject, getPublishedHomeworkAssignmentsWithSubject,
} from "@/modules/homework/data-access-classes" } from "@/modules/homework/data-access-classes"
import { createModuleLogger } from "@/shared/lib/logger"
import type { import type {
ClassStudent, ClassStudent,
StudentEnrolledClass, StudentEnrolledClass,
@@ -26,6 +27,8 @@ import {
getTeacherSubjectIdsForClass, getTeacherSubjectIdsForClass,
} from "./data-access" } from "./data-access"
const log = createModuleLogger("classes")
export const getStudentsSubjectScoresRaw = async ( export const getStudentsSubjectScoresRaw = async (
studentIds: string[] studentIds: string[]
): Promise<Map<string, Record<string, number | null>>> => { ): Promise<Map<string, Record<string, number | null>>> => {
@@ -167,7 +170,7 @@ export const getStudentClassesRaw = async (studentId: string): Promise<StudentEn
.where(and(eq(classEnrollments.studentId, id), eq(classEnrollments.status, "active"))) .where(and(eq(classEnrollments.studentId, id), eq(classEnrollments.status, "active")))
.orderBy(asc(classes.schoolName), asc(classes.grade), asc(classes.name), asc(classes.homeroom), asc(classes.room)) .orderBy(asc(classes.schoolName), asc(classes.grade), asc(classes.name), asc(classes.homeroom), asc(classes.room))
} catch (error) { } catch (error) {
console.error("getStudentClasses primary query failed, falling back:", error) log.error({ err: error }, "getStudentClasses primary query failed, falling back")
return await db return await db
.select({ .select({
id: classes.id, id: classes.id,

View File

@@ -15,6 +15,7 @@ import {
usersToRoles, usersToRoles,
} from "@/shared/db/schema" } from "@/shared/db/schema"
import { ROLE_NAMES } from "@/shared/types/permissions" import { ROLE_NAMES } from "@/shared/types/permissions"
import { createModuleLogger } from "@/shared/lib/logger"
import { DEFAULT_CLASS_SUBJECTS } from "./types" import { DEFAULT_CLASS_SUBJECTS } from "./types"
import type { import type {
ClassSubject, ClassSubject,
@@ -37,6 +38,7 @@ import {
isDuplicateInvitationCodeError, isDuplicateInvitationCodeError,
} from "./data-access-invitations" } from "./data-access-invitations"
const log = createModuleLogger("classes")
const isClassSubject = (v: unknown): v is ClassSubject => const isClassSubject = (v: unknown): v is ClassSubject =>
typeof v === "string" && (DEFAULT_CLASS_SUBJECTS as readonly string[]).includes(v) typeof v === "string" && (DEFAULT_CLASS_SUBJECTS as readonly string[]).includes(v)
@@ -70,7 +72,7 @@ export const getTeacherClassesRaw = async (params?: { teacherId?: string }): Pro
.groupBy(classes.id, classes.schoolName, classes.name, classes.grade, classes.homeroom, classes.room, classes.invitationCode) .groupBy(classes.id, classes.schoolName, classes.name, classes.grade, classes.homeroom, classes.room, classes.invitationCode)
.orderBy(asc(classes.schoolName), asc(classes.grade), asc(classes.name), asc(classes.homeroom), asc(classes.room)) .orderBy(asc(classes.schoolName), asc(classes.grade), asc(classes.name), asc(classes.homeroom), asc(classes.room))
} catch (error) { } catch (error) {
console.error("getTeacherClasses query failed:", error) log.error({ err: error }, "getTeacherClasses query failed")
throw new Error("Failed to load teacher classes") throw new Error("Failed to load teacher classes")
} }
})() })()

View File

@@ -7,6 +7,7 @@ import { and, asc, desc, eq, inArray, type SQL } from "drizzle-orm"
import { db } from "@/shared/db" import { db } from "@/shared/db"
import { coursePlanItems, coursePlans } from "@/shared/db/schema" import { coursePlanItems, coursePlans } from "@/shared/db/schema"
import { safeParseDate } from "@/shared/lib/action-utils" import { safeParseDate } from "@/shared/lib/action-utils"
import { createModuleLogger } from "@/shared/lib/logger"
import type { import type {
CoursePlan, CoursePlan,
CoursePlanItem, CoursePlanItem,
@@ -25,6 +26,7 @@ import type {
UpdateCoursePlanItemInput, UpdateCoursePlanItemInput,
} from "./schema" } from "./schema"
const log = createModuleLogger("course-plans")
const toIso = (d: Date | null | undefined): string | null => const toIso = (d: Date | null | undefined): string | null =>
d ? d.toISOString() : null d ? d.toISOString() : null
@@ -164,7 +166,7 @@ export const getCoursePlansRaw = async (params?: GetCoursePlansParams, scope?: C
return await enrichPlanRows(rows) return await enrichPlanRows(rows)
} catch (error) { } catch (error) {
console.error("getCoursePlans failed:", error) log.error({ err: error }, "getCoursePlans failed")
return [] return []
} }
} }
@@ -199,7 +201,7 @@ export const getCoursePlanByIdRaw = async (id: string, scope?: CoursePlanQuerySc
items: itemRows.map(mapItemRow), items: itemRows.map(mapItemRow),
} }
} catch (error) { } catch (error) {
console.error("getCoursePlanById failed:", error) log.error({ err: error }, "getCoursePlanById failed")
return null return null
} }
} }

View File

@@ -16,6 +16,7 @@ import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guar
import { Permissions } from "@/shared/types/permissions" import { Permissions } from "@/shared/types/permissions"
import { z } from "zod" import { z } from "zod"
import { handleActionError, safeJsonParse } from "@/shared/lib/action-utils" import { handleActionError, safeJsonParse } from "@/shared/lib/action-utils"
import { createModuleLogger } from "@/shared/lib/logger"
import { createQuestionWithRelations } from "@/modules/questions/data-access" import { createQuestionWithRelations } from "@/modules/questions/data-access"
import { import {
getExamCreatorId, getExamCreatorId,
@@ -35,6 +36,8 @@ import {
successState, successState,
} from "./actions-helpers" } from "./actions-helpers"
const log = createModuleLogger("exams")
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Schemas // Schemas
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -175,7 +178,7 @@ export async function createExamFromRichEditorAction(
if (error instanceof PermissionDeniedError) { if (error instanceof PermissionDeniedError) {
return failState<string>(error.message) return failState<string>(error.message)
} }
console.error("[createExamFromRichEditorAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "createExamFromRichEditorAction failed")
return handleActionError(error) return handleActionError(error)
} }
} }
@@ -285,7 +288,7 @@ export async function updateExamFromRichEditorAction(
if (error instanceof PermissionDeniedError) { if (error instanceof PermissionDeniedError) {
return failState<string>(error.message) return failState<string>(error.message)
} }
console.error("[updateExamFromRichEditorAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "updateExamFromRichEditorAction failed")
return handleActionError(error) return handleActionError(error)
} }
} }

View File

@@ -11,6 +11,7 @@ import {
safeJsonParse, safeJsonParse,
} from "@/shared/lib/action-utils" } from "@/shared/lib/action-utils"
import { trackExamEvent } from "@/shared/lib/track-event" import { trackExamEvent } from "@/shared/lib/track-event"
import { createModuleLogger } from "@/shared/lib/logger"
import { import {
deleteExamById, deleteExamById,
duplicateExam, duplicateExam,
@@ -44,6 +45,8 @@ import {
successState, successState,
} from "./actions-helpers" } from "./actions-helpers"
const log = createModuleLogger("exams")
// Re-export 从拆分文件迁移的类型导出,保持外部 import 路径不变 // Re-export 从拆分文件迁移的类型导出,保持外部 import 路径不变
// 注意Next.js 16 禁止在 "use server" 文件中 re-export 值(仅允许 async 函数导出), // 注意Next.js 16 禁止在 "use server" 文件中 re-export 值(仅允许 async 函数导出),
// autoMarkExamAction / createExamFromRichEditorAction / updateExamFromRichEditorAction // autoMarkExamAction / createExamFromRichEditorAction / updateExamFromRichEditorAction
@@ -189,7 +192,7 @@ export async function createExamAction(
examModeConfig: parseExamModeConfig(formData), examModeConfig: parseExamModeConfig(formData),
}) })
} catch (error) { } catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "ExamAction failed")
return failState<string>(t("dbCreateFailed")) return failState<string>(t("dbCreateFailed"))
} }
@@ -310,7 +313,7 @@ export async function createAiExamAction(
examModeConfig: parseExamModeConfig(formData), examModeConfig: parseExamModeConfig(formData),
}) })
} catch (error) { } catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "ExamAction failed")
return failState<string>(t("dbCreateFailed")) return failState<string>(t("dbCreateFailed"))
} }
@@ -423,7 +426,7 @@ export async function regenerateAiQuestionAction(
content: result.data.content, content: result.data.content,
}) })
} catch (error) { } catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "ExamAction failed")
return failState<AiRewriteQuestionData>(t("aiQuestionFormatInvalid")) return failState<AiRewriteQuestionData>(t("aiQuestionFormatInvalid"))
} }
} catch (error) { } catch (error) {
@@ -492,7 +495,7 @@ export async function updateExamAction(
status, status,
}) })
} catch (error) { } catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "ExamAction failed")
return failState<string>(t("dbUpdateFailed")) return failState<string>(t("dbUpdateFailed"))
} }
@@ -550,7 +553,7 @@ export async function deleteExamAction(
try { try {
await deleteExamById(examId) await deleteExamById(examId)
} catch (error) { } catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "ExamAction failed")
return failState<string>(t("dbDeleteFailed")) return failState<string>(t("dbDeleteFailed"))
} }
@@ -604,7 +607,7 @@ export async function duplicateExamAction(
} }
newExamId = duplicatedId newExamId = duplicatedId
} catch (error) { } catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "ExamAction failed")
return failState<string>(t("dbDuplicateFailed")) return failState<string>(t("dbDuplicateFailed"))
} }
@@ -644,7 +647,7 @@ export async function getExamPreviewAction(
questions: exam.questions, questions: exam.questions,
}) })
} catch (error) { } catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "ExamAction failed")
return failState<{ structure: unknown; questions: Array<{ id: string }> }>(t("loadPreviewFailed")) return failState<{ structure: unknown; questions: Array<{ id: string }> }>(t("loadPreviewFailed"))
} }
} catch (error) { } catch (error) {
@@ -664,7 +667,7 @@ export async function getSubjectsAction(): Promise<ActionState<{ id: string; nam
const allSubjects = await getExamSubjects() const allSubjects = await getExamSubjects()
return successState(allSubjects) return successState(allSubjects)
} catch (error) { } catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "ExamAction failed")
return failState<{ id: string; name: string }[]>(t("loadSubjectsFailed")) return failState<{ id: string; name: string }[]>(t("loadSubjectsFailed"))
} }
} catch (error) { } catch (error) {
@@ -684,7 +687,7 @@ export async function getGradesAction(): Promise<ActionState<{ id: string; name:
const allGrades = await getExamGrades() const allGrades = await getExamGrades()
return successState(allGrades) return successState(allGrades)
} catch (error) { } catch (error) {
console.error("[ExamAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "ExamAction failed")
return failState<{ id: string; name: string }[]>(t("loadGradesFailed")) return failState<{ id: string; name: string }[]>(t("loadGradesFailed"))
} }
} catch (error) { } catch (error) {

View File

@@ -16,6 +16,7 @@ import { Permissions } from "@/shared/types/permissions"
import { z } from "zod" import { z } from "zod"
import { handleActionError } from "@/shared/lib/action-utils" import { handleActionError } from "@/shared/lib/action-utils"
import { isRecord } from "@/shared/lib/type-guards" import { isRecord } from "@/shared/lib/type-guards"
import { createModuleLogger } from "@/shared/lib/logger"
import { import {
failState, failState,
successState, successState,
@@ -23,6 +24,8 @@ import {
getStringValue, getStringValue,
} from "../actions-helpers" } from "../actions-helpers"
const log = createModuleLogger("exams")
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Schema & 类型 // Schema & 类型
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -390,7 +393,7 @@ export async function autoMarkExamAction(
if (error instanceof PermissionDeniedError) { if (error instanceof PermissionDeniedError) {
return failState<AutoMarkResult>(error.message) return failState<AutoMarkResult>(error.message)
} }
console.error("[autoMarkExamAction]", error instanceof Error ? error.message : String(error)) log.error({ err: error }, "autoMarkExamAction failed")
return handleActionError(error) return handleActionError(error)
} }
} }

View File

@@ -10,6 +10,8 @@
import { z } from "zod" import { z } from "zod"
import { createAiChatCompletion, getAiErrorMessage } from "@/shared/lib/ai" import { createAiChatCompletion, getAiErrorMessage } from "@/shared/lib/ai"
import { env } from "@/env.mjs" import { env } from "@/env.mjs"
import { createModuleLogger } from "@/shared/lib/logger"
import { isRecord } from "../lib/type-guards"
import { import {
AI_EXAM_SOURCE_VALIDATION_SYSTEM_PROMPT, AI_EXAM_SOURCE_VALIDATION_SYSTEM_PROMPT,
@@ -25,6 +27,8 @@ import {
parseAiResponse, parseAiResponse,
} from "./parse" } from "./parse"
const log = createModuleLogger("exams")
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Shared types // Shared types
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -191,12 +195,11 @@ export const parseQuestionDetail = async (input: {
aiProviderId?: string aiProviderId?: string
}): Promise<z.infer<typeof AiQuestionSchema>> => { }): Promise<z.infer<typeof AiQuestionSchema>> => {
const normalizeQuestionCandidate = (value: unknown): unknown => { const normalizeQuestionCandidate = (value: unknown): unknown => {
if (!value || typeof value !== "object") return value if (!isRecord(value)) return value
// 从 unknown 收窄为 Record<string, unknown> 以进行字段检查 const record = value
const record = value as Record<string, unknown>
const contentRaw = record.content const contentRaw = record.content
if (!contentRaw || typeof contentRaw !== "object") return value if (!isRecord(contentRaw)) return value
const content = contentRaw as Record<string, unknown> const content = contentRaw
const normalizedSubQuestions = Array.isArray(content.subQuestions) const normalizedSubQuestions = Array.isArray(content.subQuestions)
? content.subQuestions ? content.subQuestions
: Array.isArray(content.subquestions) : Array.isArray(content.subquestions)
@@ -246,7 +249,7 @@ export const parseQuestionDetail = async (input: {
} satisfies z.infer<typeof AiQuestionSchema> } satisfies z.infer<typeof AiQuestionSchema>
} }
} catch (error) { } catch (error) {
console.warn("[parseQuestionDetail] Falling back to text question:", error instanceof Error ? error.message : String(error)) log.warn({ err: error }, "parseQuestionDetail falling back to text question")
} }
return { return {

View File

@@ -12,6 +12,8 @@
import { createId } from "@paralleldrive/cuid2" import { createId } from "@paralleldrive/cuid2"
import { z } from "zod" import { z } from "zod"
import { createModuleLogger } from "@/shared/lib/logger"
import { import {
AiExamResponseSchema, AiExamResponseSchema,
AiQuestionSchema, AiQuestionSchema,
@@ -25,6 +27,8 @@ import {
} from "./parse" } from "./parse"
import type { SplitQuestionItem } from "./request" import type { SplitQuestionItem } from "./request"
const log = createModuleLogger("exams")
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Structure splitting // Structure splitting
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -78,7 +82,7 @@ export const mapWithConcurrency = async <T, R>(
} catch (error) { } catch (error) {
// Catch per-item errors so a single failure doesn't reject the whole batch. // Catch per-item errors so a single failure doesn't reject the whole batch.
// The result slot stays undefined; callers should handle missing entries. // The result slot stays undefined; callers should handle missing entries.
console.error("[mapWithConcurrency] worker error at index", index, error instanceof Error ? error.message : String(error)) log.error({ err: error, index }, "mapWithConcurrency worker error")
} }
} }
} }

View File

@@ -4,6 +4,7 @@ import { and, count, desc, eq, inArray, like, or, sql, type SQL } from "drizzle-
import { cacheFn } from "@/shared/lib/cache" import { cacheFn } from "@/shared/lib/cache"
import { db } from "@/shared/db" import { db } from "@/shared/db"
import { createModuleLogger } from "@/shared/lib/logger"
import { fileAttachments } from "@/shared/db/schema" import { fileAttachments } from "@/shared/db/schema"
import type { import type {
BatchDeleteResult, BatchDeleteResult,
@@ -13,6 +14,7 @@ import type {
FileStats, FileStats,
} from "./types" } from "./types"
const log = createModuleLogger("files")
const toIso = (d: Date): string => d.toISOString() const toIso = (d: Date): string => d.toISOString()
const mapRow = (row: typeof fileAttachments.$inferSelect): FileAttachment => ({ const mapRow = (row: typeof fileAttachments.$inferSelect): FileAttachment => ({
@@ -52,7 +54,7 @@ export async function createFileAttachment(
const created = await getFileAttachment(data.id) const created = await getFileAttachment(data.id)
return created return created
} catch (error) { } catch (error) {
console.error("createFileAttachment failed:", error) log.error({ err: error }, "createFileAttachment failed")
return null return null
} }
} }
@@ -70,7 +72,7 @@ export const getFileAttachmentRaw = async (id: string): Promise<FileAttachment |
return row ? mapRow(row) : null return row ? mapRow(row) : null
} catch (error) { } catch (error) {
console.error("getFileAttachment failed:", error) log.error({ err: error }, "getFileAttachment failed")
return null return null
} }
} }
@@ -99,7 +101,7 @@ export const getFileAttachmentsByTargetRaw = async (targetType: string, targetId
return rows.map(mapRow) return rows.map(mapRow)
} catch (error) { } catch (error) {
console.error("getFileAttachmentsByTarget failed:", error) log.error({ err: error }, "getFileAttachmentsByTarget failed")
return [] return []
} }
} }
@@ -123,7 +125,7 @@ export const getFileAttachmentsByUploaderRaw = async (uploaderId: string): Promi
return rows.map(mapRow) return rows.map(mapRow)
} catch (error) { } catch (error) {
console.error("getFileAttachmentsByUploader failed:", error) log.error({ err: error }, "getFileAttachmentsByUploader failed")
return [] return []
} }
} }
@@ -147,7 +149,7 @@ export const getAllFileAttachmentsRaw = async (limit = 100): Promise<FileAttachm
return rows.map(mapRow) return rows.map(mapRow)
} catch (error) { } catch (error) {
console.error("getAllFileAttachments failed:", error) log.error({ err: error }, "getAllFileAttachments failed")
return [] return []
} }
} }
@@ -166,7 +168,7 @@ export async function deleteFileAttachment(id: string): Promise<boolean> {
await db.delete(fileAttachments).where(eq(fileAttachments.id, id)) await db.delete(fileAttachments).where(eq(fileAttachments.id, id))
return true return true
} catch (error) { } catch (error) {
console.error("deleteFileAttachment failed:", error) log.error({ err: error }, "deleteFileAttachment failed")
return false return false
} }
} }
@@ -183,7 +185,7 @@ export async function deleteFileAttachments(ids: string[]): Promise<BatchDeleteR
await db.delete(fileAttachments).where(inArray(fileAttachments.id, ids)) await db.delete(fileAttachments).where(inArray(fileAttachments.id, ids))
return { success: true, deletedCount: ids.length, failedIds: [] } return { success: true, deletedCount: ids.length, failedIds: [] }
} catch (error) { } catch (error) {
console.error("deleteFileAttachments batch failed:", error) log.error({ err: error }, "deleteFileAttachments batch failed")
// 失败时回退到逐条删除,尽量多删 // 失败时回退到逐条删除,尽量多删
const failedIds: string[] = [] const failedIds: string[] = []
let deletedCount = 0 let deletedCount = 0
@@ -192,7 +194,7 @@ export async function deleteFileAttachments(ids: string[]): Promise<BatchDeleteR
await db.delete(fileAttachments).where(eq(fileAttachments.id, id)) await db.delete(fileAttachments).where(eq(fileAttachments.id, id))
deletedCount += 1 deletedCount += 1
} catch (err) { } catch (err) {
console.error("deleteFileAttachments single failed:", err) log.error({ err }, "deleteFileAttachments single failed")
failedIds.push(id) failedIds.push(id)
} }
} }
@@ -242,7 +244,7 @@ export const getFileAttachmentsWithFiltersRaw = async (params: FileAttachmentQue
return rows.map(mapRow) return rows.map(mapRow)
} catch (error) { } catch (error) {
console.error("getFileAttachmentsWithFilters failed:", error) log.error({ err: error }, "getFileAttachmentsWithFilters failed")
return [] return []
} }
} }
@@ -278,7 +280,7 @@ export const getFileStatsRaw = async (): Promise<FileStats> => {
return { totalCount, totalSize, byType } return { totalCount, totalSize, byType }
} catch (error) { } catch (error) {
console.error("getFileStats failed:", error) log.error({ err: error }, "getFileStats failed")
return { totalCount: 0, totalSize: 0, byType: [] } return { totalCount: 0, totalSize: 0, byType: [] }
} }
} }
@@ -302,7 +304,7 @@ export const getFileByUrlRaw = async (url: string): Promise<FileAttachment | nul
return row ? mapRow(row) : null return row ? mapRow(row) : null
} catch (error) { } catch (error) {
console.error("getFileByUrl failed:", error) log.error({ err: error }, "getFileByUrl failed")
return null return null
} }
} }
@@ -325,7 +327,7 @@ export const getFileAttachmentsByIdsRaw = async (ids: string[]): Promise<FileAtt
.where(inArray(fileAttachments.id, ids)) .where(inArray(fileAttachments.id, ids))
return rows.map(mapRow) return rows.map(mapRow)
} catch (error) { } catch (error) {
console.error("getFileAttachmentsByIds failed:", error) log.error({ err: error }, "getFileAttachmentsByIds failed")
return [] return []
} }
} }

View File

@@ -16,6 +16,9 @@ import {
} from "./data-access-drafts" } from "./data-access-drafts"
import { assertClassInScope } from "./lib/scope-check" import { assertClassInScope } from "./lib/scope-check"
import { getUserNamesByIds } from "@/modules/users/data-access" import { getUserNamesByIds } from "@/modules/users/data-access"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("grades")
/** /**
* P3-6 协同录入锁 Server Actions。 * P3-6 协同录入锁 Server Actions。
@@ -199,7 +202,7 @@ export async function releaseDraftLockAction(params: {
return { success: true, data: { released: true } } return { success: true, data: { released: true } }
} catch (e) { } catch (e) {
// 释放失败不应阻断主流程,降级为日志 // 释放失败不应阻断主流程,降级为日志
console.error("[grades] releaseDraftLockAction failed:", e) log.error({ err: e }, "releaseDraftLockAction failed")
return { success: true, data: { released: true } } return { success: true, data: { released: true } }
} }
} }

View File

@@ -52,6 +52,9 @@ import type {
ClassRankingItem, ClassRankingItem,
GradeRecord, GradeRecord,
} from "./types" } from "./types"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("grades")
/** /**
* grades 模块核心 Server Actions编排层 * grades 模块核心 Server Actions编排层
@@ -306,9 +309,9 @@ export async function batchCreateGradeRecordsByExamAction(
]) ])
if (errorBookResult.status === "rejected") { if (errorBookResult.status === "rejected") {
console.error( log.error(
`[grade-entry] 考试错题采集失败 submission=${r.submissionId}:`, { err: errorBookResult.reason, submissionId: r.submissionId },
errorBookResult.reason, "考试错题采集失败",
) )
} }
}), }),

View File

@@ -9,6 +9,9 @@ import type { DataScope } from "@/shared/types/permissions"
import { getGradeTrend } from "../data-access-analytics" import { getGradeTrend } from "../data-access-analytics"
import { detectScoreAnomaly, type ScoreAnomalyResult } from "../stats-service" import { detectScoreAnomaly, type ScoreAnomalyResult } from "../stats-service"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("grades")
/** /**
* v4-P1-6: 成绩录入后通知学生和家长。 * v4-P1-6: 成绩录入后通知学生和家长。
@@ -117,9 +120,9 @@ export async function notifyScoreAnomalyIfNeeded(params: {
subjectId: params.subjectId, subjectId: params.subjectId,
}) })
} catch (error) { } catch (error) {
console.error( log.error(
`[grade-anomaly] 检测学生 ${studentId} 异常失败:`, { err: error, studentId },
error, "检测学生异常失败",
) )
} }
}), }),

View File

@@ -30,6 +30,9 @@ import {
import { getScansBySubmissionId } from "./data-access-scans" import { getScansBySubmissionId } from "./data-access-scans"
import { getExcellentSubmissions, getUnsubmittedStudents, getHomeworkAssignmentById } from "./data-access" import { getExcellentSubmissions, getUnsubmittedStudents, getHomeworkAssignmentById } from "./data-access"
import type { ExcellentSubmissionItem, ScanAttachment } from "./types" import type { ExcellentSubmissionItem, ScanAttachment } from "./types"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("homework")
const parseStudentIds = (raw: string): string[] => { const parseStudentIds = (raw: string): string[] => {
return raw return raw
@@ -83,10 +86,16 @@ async function runPostGradingHooks(submissionId: string, studentId: string): Pro
]) ])
if (errorBookResult.status === "rejected") { if (errorBookResult.status === "rejected") {
console.error(`[post-grading] 错题采集失败 submission=${submissionId}:`, errorBookResult.reason) log.error(
{ err: errorBookResult.reason, submissionId },
"错题采集失败",
)
} }
if (masteryResult.status === "rejected") { if (masteryResult.status === "rejected") {
console.error(`[post-grading] 掌握度更新失败 submission=${submissionId}:`, masteryResult.reason) log.error(
{ err: masteryResult.reason, submissionId },
"掌握度更新失败",
)
} }
} }
@@ -682,8 +691,13 @@ export async function remindUnsubmittedAction(params: {
const failedCount = results.length - successCount const failedCount = results.length - successCount
if (failedCount > 0) { if (failedCount > 0) {
console.error( log.error(
`[remindUnsubmitted] ${failedCount}/${results.length} notifications failed for assignment ${params.assignmentId}` {
failedCount,
total: results.length,
assignmentId: params.assignmentId,
},
"Notifications failed for assignment",
) )
} }

View File

@@ -12,6 +12,9 @@ import { getStudentIdsByClassIds } from "@/modules/classes/data-access";
import { normalizeDocument } from "./data-access"; import { normalizeDocument } from "./data-access";
import { isExerciseBlockData, isLessonPlanStatus, isValidQuestionType } from "./lib/type-guards"; import { isExerciseBlockData, isLessonPlanStatus, isValidQuestionType } from "./lib/type-guards";
import type { LessonPlanDocument, LessonPlan, LessonPlanStatus } from "./types"; import type { LessonPlanDocument, LessonPlan, LessonPlanStatus } from "./types";
import { createModuleLogger } from "@/shared/lib/logger";
const log = createModuleLogger("lesson-preparation");
interface PublishInput { interface PublishInput {
planId: string; planId: string;
@@ -208,13 +211,16 @@ export async function publishLessonPlanHomework(
} catch (e) { } catch (e) {
// 补偿错误处理:记录已创建的资源 ID 便于排查孤儿数据 // 补偿错误处理:记录已创建的资源 ID 便于排查孤儿数据
if (e instanceof PublishServiceError) throw e; if (e instanceof PublishServiceError) throw e;
console.error("[publishLessonPlanHomework] 部分失败,可能存在孤儿数据", { log.error(
planId: input.planId, {
examId, err: e,
assignmentId, planId: input.planId,
createdQuestionIds, examId,
error: e instanceof Error ? e.message : String(e), assignmentId,
}); createdQuestionIds,
},
"publishLessonPlanHomework 部分失败,可能存在孤儿数据",
);
throw e; throw e;
} }

View File

@@ -2,6 +2,9 @@ import "server-only"
import { getQuestions } from "@/modules/questions/data-access" import { getQuestions } from "@/modules/questions/data-access"
import type { QuestionPickerItem, QuestionPickerParams } from "../providers/lesson-plan-provider" import type { QuestionPickerItem, QuestionPickerParams } from "../providers/lesson-plan-provider"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("lesson-preparation")
/** /**
* 跨模块题目查询桥接器V4 P0-4 修复)。 * 跨模块题目查询桥接器V4 P0-4 修复)。
@@ -38,7 +41,7 @@ export async function fetchExternalQuestions(
return { success: true, data: { data: items } } return { success: true, data: { data: items } }
} catch (e) { } catch (e) {
console.error("[external-questions-bridge] fetchExternalQuestions failed", e) log.error({ err: e }, "fetchExternalQuestions failed")
return { success: false, message: "external_questions_unavailable" } return { success: false, message: "external_questions_unavailable" }
} }
} }

View File

@@ -22,7 +22,9 @@ import type {
NotificationChannel, NotificationChannel,
} from "../types" } from "../types"
import type { NotificationChannelSender, ChannelRecipient } from "./types" import type { NotificationChannelSender, ChannelRecipient } from "./types"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("notifications")
const channel: NotificationChannel = "email" const channel: NotificationChannel = "email"
/** 从环境变量读取邮件配置 */ /** 从环境变量读取邮件配置 */
@@ -111,8 +113,9 @@ class MockEmailSender implements NotificationChannelSender {
payload: NotificationPayload, payload: NotificationPayload,
recipient: ChannelRecipient recipient: ChannelRecipient
): Promise<ChannelSendResult> { ): Promise<ChannelSendResult> {
console.info( log.info(
`[MockEmail] send to ${recipient.email ?? "(no email)"}: subject="${payload.title}"` { email: recipient.email ?? null, subject: payload.title },
"MockEmail send"
) )
return { return {
channel, channel,

View File

@@ -23,7 +23,9 @@ import type {
NotificationChannel, NotificationChannel,
} from "../types" } from "../types"
import type { NotificationChannelSender, ChannelRecipient } from "./types" import type { NotificationChannelSender, ChannelRecipient } from "./types"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("notifications")
const channel: NotificationChannel = "sms" const channel: NotificationChannel = "sms"
type SmsProvider = "aliyun" | "tencent" | "mock" type SmsProvider = "aliyun" | "tencent" | "mock"
@@ -73,9 +75,9 @@ class MockSmsSender implements NotificationChannelSender {
): Promise<ChannelSendResult> { ): Promise<ChannelSendResult> {
const params = buildTemplateParams(payload) const params = buildTemplateParams(payload)
// 开发环境仅记录日志,不实际发送 // 开发环境仅记录日志,不实际发送
console.info( log.info(
`[MockSms] send to ${recipient.phone ?? "(no phone)"}: title="${payload.title}" params=`, { phone: recipient.phone ?? null, title: payload.title, params },
params "MockSms send"
) )
return { return {
channel, channel,

View File

@@ -24,7 +24,10 @@ import type {
NotificationChannel, NotificationChannel,
} from "../types" } from "../types"
import type { NotificationChannelSender, ChannelRecipient } from "./types" import type { NotificationChannelSender, ChannelRecipient } from "./types"
import { isStringRecord } from "../lib/type-guards"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("notifications")
const channel: NotificationChannel = "wechat" const channel: NotificationChannel = "wechat"
const WECHAT_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token" const WECHAT_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token"
const WECHAT_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send" const WECHAT_SEND_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send"
@@ -103,9 +106,10 @@ async function getAccessToken(): Promise<string> {
function buildTemplateData( function buildTemplateData(
payload: NotificationPayload payload: NotificationPayload
): Record<string, { value: string }> { ): Record<string, { value: string }> {
// as 断言:metadata 为 Record<string, unknown>此处需按 string 字典取值 // metadata 为 Record<string, unknown>用类型守卫收窄为 string 字典;
// 运行时若类型不符会被 ?? 兜底为空对象,安全无副作用 // 不符合时 ?? 兜底为空对象
const custom = (payload.metadata?.wechatKeywords as Record<string, string> | undefined) ?? {} const rawKeywords = payload.metadata?.wechatKeywords
const custom = isStringRecord(rawKeywords) ? rawKeywords : {}
return { return {
keyword1: { value: custom.keyword1 ?? payload.title }, keyword1: { value: custom.keyword1 ?? payload.title },
keyword2: { value: custom.keyword2 ?? payload.content }, keyword2: { value: custom.keyword2 ?? payload.content },
@@ -121,8 +125,9 @@ class MockWechatSender implements NotificationChannelSender {
payload: NotificationPayload, payload: NotificationPayload,
recipient: ChannelRecipient recipient: ChannelRecipient
): Promise<ChannelSendResult> { ): Promise<ChannelSendResult> {
console.info( log.info(
`[MockWechat] send to openId=${recipient.wechatOpenId ?? "(no openId)"}: title="${payload.title}"` { openId: recipient.wechatOpenId ?? null, title: payload.title },
"MockWechat send"
) )
return { return {
channel, channel,

View File

@@ -23,6 +23,7 @@ import { and, count, desc, eq } from "drizzle-orm"
import { db } from "@/shared/db" import { db } from "@/shared/db"
import { messageNotifications, notificationLogs, users } from "@/shared/db/schema" import { messageNotifications, notificationLogs, users } from "@/shared/db/schema"
import { cacheFn } from "@/shared/lib/cache" import { cacheFn } from "@/shared/lib/cache"
import { createModuleLogger } from "@/shared/lib/logger"
import type { ChannelRecipient } from "./channels/types" import type { ChannelRecipient } from "./channels/types"
import type { import type {
ChannelSendResult, ChannelSendResult,
@@ -34,6 +35,7 @@ import type {
PaginatedResult, PaginatedResult,
} from "./types" } from "./types"
const log = createModuleLogger("notifications")
const toIsoRequired = (d: Date): string => d.toISOString() const toIsoRequired = (d: Date): string => d.toISOString()
const isNotificationType = (v: unknown): v is NotificationType => const isNotificationType = (v: unknown): v is NotificationType =>
@@ -247,19 +249,23 @@ export const getUserContactInfo = cacheFn(getUserContactInfoRaw, {
* - 通知送达率统计 * - 通知送达率统计
* - 渠道健康度监控 * - 渠道健康度监控
* *
* DB 写入失败时降级为 console.info不阻塞通知发送流程。 * DB 写入失败时降级为 logger.info不阻塞通知发送流程。
*/ */
export async function logNotificationSend( export async function logNotificationSend(
result: ChannelSendResult, result: ChannelSendResult,
payload?: { userId: string; title: string } payload?: { userId: string; title: string }
): Promise<void> { ): Promise<void> {
const status = result.success ? "success" : "failure" const status = result.success ? "success" : "failure"
const errorPart = result.error ? ` error="${result.error}"` : ""
// 始终输出 console 日志(便于开发调试) // 始终输出结构化日志(便于开发调试与监控
// TODO V3-P2-8: 接入统一日志服务shared/lib/logger替换 console.info log.info(
console.info( {
`[NotificationLog] ${result.success ? "OK" : "FAIL"} channel=${result.channel} messageId=${result.messageId ?? "-"}${errorPart}` channel: result.channel,
messageId: result.messageId ?? null,
success: result.success,
error: result.error ?? null,
},
result.success ? "NotificationLog OK" : "NotificationLog FAIL"
) )
// 持久化到 DB需要 payload 提供 userId 和 title // 持久化到 DB需要 payload 提供 userId 和 title
@@ -278,8 +284,7 @@ export async function logNotificationSend(
}) })
} catch (dbError) { } catch (dbError) {
// DB 写入失败不阻塞通知流程,仅记录错误 // DB 写入失败不阻塞通知流程,仅记录错误
// TODO V3-P2-8: 接入统一日志服务shared/lib/logger替换 console.error log.error({ err: dbError }, "NotificationLog failed to persist log")
console.error("[NotificationLog] Failed to persist log:", dbError)
} }
} }
} }

View File

@@ -4,6 +4,7 @@ import { and, asc, desc, eq, inArray, or, sql } from "drizzle-orm"
import { cacheFn } from "@/shared/lib/cache" import { cacheFn } from "@/shared/lib/cache"
import { db } from "@/shared/db" import { db } from "@/shared/db"
import { createModuleLogger } from "@/shared/lib/logger"
import { academicYears, departments, grades, roles, schools, subjects, users, usersToRoles } from "@/shared/db/schema" import { academicYears, departments, grades, roles, schools, subjects, users, usersToRoles } from "@/shared/db/schema"
import type { import type {
AcademicYearInsertData, AcademicYearInsertData,
@@ -22,6 +23,7 @@ import type {
StaffOption, StaffOption,
} from "./types" } from "./types"
const log = createModuleLogger("school")
const toIso = (d: Date): string => d.toISOString() const toIso = (d: Date): string => d.toISOString()
export const getDepartmentsRaw = async (): Promise<DepartmentListItem[]> => { export const getDepartmentsRaw = async (): Promise<DepartmentListItem[]> => {
@@ -35,7 +37,7 @@ export const getDepartmentsRaw = async (): Promise<DepartmentListItem[]> => {
updatedAt: toIso(r.updatedAt), updatedAt: toIso(r.updatedAt),
})) }))
} catch (error) { } catch (error) {
console.error("getDepartments failed:", error) log.error({ err: error }, "getDepartments failed")
return [] return []
} }
} }
@@ -58,7 +60,7 @@ export const getAcademicYearsRaw = async (): Promise<AcademicYearListItem[]> =>
updatedAt: toIso(r.updatedAt), updatedAt: toIso(r.updatedAt),
})) }))
} catch (error) { } catch (error) {
console.error("getAcademicYears failed:", error) log.error({ err: error }, "getAcademicYears failed")
return [] return []
} }
} }
@@ -79,7 +81,7 @@ export const getSchoolsRaw = async (): Promise<SchoolListItem[]> => {
updatedAt: toIso(r.updatedAt), updatedAt: toIso(r.updatedAt),
})) }))
} catch (error) { } catch (error) {
console.error("getSchools failed:", error) log.error({ err: error }, "getSchools failed")
return [] return []
} }
} }
@@ -138,7 +140,7 @@ export const getGradesRaw = async (): Promise<GradeListItem[]> => {
updatedAt: toIso(r.updatedAt), updatedAt: toIso(r.updatedAt),
})) }))
} catch (error) { } catch (error) {
console.error("getGrades failed:", error) log.error({ err: error }, "getGrades failed")
return [] return []
} }
} }
@@ -165,7 +167,7 @@ export const getStaffOptionsRaw = async (): Promise<StaffOption[]> => {
email: r.email, email: r.email,
})) }))
} catch (error) { } catch (error) {
console.error("getStaffOptions failed:", error) log.error({ err: error }, "getStaffOptions failed")
return [] return []
} }
} }
@@ -226,7 +228,7 @@ export const getGradesForStaffRaw = async (staffId: string): Promise<GradeListIt
updatedAt: toIso(r.updatedAt), updatedAt: toIso(r.updatedAt),
})) }))
} catch (error) { } catch (error) {
console.error("getGradesForStaff failed:", error) log.error({ err: error }, "getGradesForStaff failed")
return [] return []
} }
} }
@@ -304,7 +306,7 @@ export const getSchoolsForUserRaw = async (userId: string): Promise<SchoolListIt
updatedAt: toIso(r.updatedAt), updatedAt: toIso(r.updatedAt),
})) }))
} catch (error) { } catch (error) {
console.error("getSchoolsForUser failed:", error) log.error({ err: error }, "getSchoolsForUser failed")
return [] return []
} }
} }
@@ -402,7 +404,7 @@ export const getGradesForUserRaw = async (userId: string): Promise<GradeListItem
return [] return []
} catch (error) { } catch (error) {
console.error("getGradesForUser failed:", error) log.error({ err: error }, "getGradesForUser failed")
return [] return []
} }
} }
@@ -570,7 +572,7 @@ export const getSubjectOptionsRaw = async (): Promise<SubjectOption[]> => {
order: Number(r.order ?? 0), order: Number(r.order ?? 0),
})) }))
} catch (error) { } catch (error) {
console.error("getSubjectOptions failed:", error) log.error({ err: error }, "getSubjectOptions failed")
return [] return []
} }
} }
@@ -602,7 +604,7 @@ export const getGradeOptionsRaw = async (): Promise<GradeOption[]> => {
order: Number(r.order ?? 0), order: Number(r.order ?? 0),
})) }))
} catch (error) { } catch (error) {
console.error("getGradeOptions failed:", error) log.error({ err: error }, "getGradeOptions failed")
return [] return []
} }
} }
@@ -874,7 +876,7 @@ export const getOrgTreeRaw = async (): Promise<OrgTreeNode[]> => {
children: gradesBySchoolId.get(school.id) ?? [], children: gradesBySchoolId.get(school.id) ?? [],
})) }))
} catch (error) { } catch (error) {
console.error("getOrgTree failed:", error) log.error({ err: error }, "getOrgTree failed")
return [] return []
} }
} }
@@ -927,7 +929,7 @@ export const getGradeOverviewStatsRaw = async (): Promise<GradeOverviewStats[]>
teacherCount: s.teacherIds.size, teacherCount: s.teacherIds.size,
})) }))
} catch (error) { } catch (error) {
console.error("getGradeOverviewStats failed:", error) log.error({ err: error }, "getGradeOverviewStats failed")
return [] return []
} }
} }

View File

@@ -10,6 +10,9 @@ import {
getFileByUrl, getFileByUrl,
} from "@/modules/files/data-access" } from "@/modules/files/data-access"
import { invalidateFor } from "@/shared/lib/cache" import { invalidateFor } from "@/shared/lib/cache"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("settings")
/** /**
* 清理旧头像文件(磁盘 + DB 记录) * 清理旧头像文件(磁盘 + DB 记录)
@@ -30,7 +33,7 @@ async function cleanupOldAvatarFile(oldImageUrl: string | null): Promise<void> {
// 删除 DB 记录 // 删除 DB 记录
await deleteFileAttachment(fileRecord.id) await deleteFileAttachment(fileRecord.id)
} catch (error) { } catch (error) {
console.error("cleanupOldAvatarFile failed:", error) log.error({ err: error }, "cleanupOldAvatarFile failed")
} }
} }

View File

@@ -23,9 +23,12 @@ import {
updateAiProvider, updateAiProvider,
} from "./data-access" } from "./data-access"
import type { AiProviderSummary, AiProviderVisibility } from "./types" import type { AiProviderSummary, AiProviderVisibility } from "./types"
import { createModuleLogger } from "@/shared/lib/logger"
export type { AiProviderSummary } from "./types" export type { AiProviderSummary } from "./types"
const log = createModuleLogger("settings")
const ProviderSchema = z.enum(["zhipu", "openai", "gemini", "custom", "ollama"]) const ProviderSchema = z.enum(["zhipu", "openai", "gemini", "custom", "ollama"])
const VisibilitySchema = z.enum(["public", "private"]) const VisibilitySchema = z.enum(["public", "private"])
@@ -217,7 +220,7 @@ export async function upsertAiProviderAction(
return { success: true, message: "AI provider created", data: id } return { success: true, message: "AI provider created", data: id }
} catch (error) { } catch (error) {
if (error instanceof PermissionDeniedError) return { success: false, message: error.message } if (error instanceof PermissionDeniedError) return { success: false, message: error.message }
console.error("[upsertAiProviderAction] Failed to save AI provider:", error) log.error({ err: error }, "Failed to save AI provider")
return { success: false, message: "Failed to save AI provider" } return { success: false, message: "Failed to save AI provider" }
} }
} }

View File

@@ -2,6 +2,9 @@ import "server-only"
import { getRedisClient } from "@/shared/lib/redis-client" import { getRedisClient } from "@/shared/lib/redis-client"
import type { CacheStore } from "./types" import type { CacheStore } from "./types"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("cache")
const KEY_PREFIX = "next-edu:cache:" const KEY_PREFIX = "next-edu:cache:"
const TAG_PREFIX = `${KEY_PREFIX}tag:` const TAG_PREFIX = `${KEY_PREFIX}tag:`
@@ -33,9 +36,9 @@ export class RedisCacheStore implements CacheStore {
return JSON.parse(cached) as T return JSON.parse(cached) as T
} }
} catch (error) { } catch (error) {
console.error( log.error(
"[cache] Redis get failure, falling back to producer:", { err: error },
error instanceof Error ? error.message : String(error), "Redis get failure, falling back to producer",
) )
} }
} }
@@ -58,9 +61,9 @@ export class RedisCacheStore implements CacheStore {
} }
} }
} catch (error) { } catch (error) {
console.error( log.error(
"[cache] Redis set failure, ignoring:", { err: error },
error instanceof Error ? error.message : String(error), "Redis set failure, ignoring",
) )
} }
} }
@@ -88,9 +91,9 @@ export class RedisCacheStore implements CacheStore {
await redis.del(...tagKeysToDelete) await redis.del(...tagKeysToDelete)
} }
} catch (error) { } catch (error) {
console.error( log.error(
"[cache] Redis invalidateTags failure, ignoring:", { err: error },
error instanceof Error ? error.message : String(error), "Redis invalidateTags failure, ignoring",
) )
} }
} }

View File

@@ -21,9 +21,12 @@
import { env } from "@/env.mjs" import { env } from "@/env.mjs"
import { getRedisClient } from "@/shared/lib/redis-client" import { getRedisClient } from "@/shared/lib/redis-client"
import { createModuleLogger } from "@/shared/lib/logger"
import type { RateLimiter, RateLimitParams, RateLimitResult } from "./types" import type { RateLimiter, RateLimitParams, RateLimitResult } from "./types"
const log = createModuleLogger("rate-limit")
/** 毫秒转 upstash `Ratelimit.slidingWindow` 接受的字符串时间单位 */ /** 毫秒转 upstash `Ratelimit.slidingWindow` 接受的字符串时间单位 */
function msToSlidingWindowArg(ms: number): string { function msToSlidingWindowArg(ms: number): string {
if (ms % (60 * 1000) === 0) { if (ms % (60 * 1000) === 0) {
@@ -148,9 +151,9 @@ export class RedisRateLimiter implements RateLimiter {
} }
} catch (error) { } catch (error) {
// 限流后端故障时降级为放行,避免阻断主流程 // 限流后端故障时降级为放行,避免阻断主流程
console.error( log.error(
"[rate-limit] Redis backend failure, falling back to allow:", { err: error },
error instanceof Error ? error.message : String(error), "Redis backend failure, falling back to allow",
) )
const resetTime = Date.now() + params.windowMs const resetTime = Date.now() + params.windowMs
return { return {
@@ -173,9 +176,9 @@ export class RedisRateLimiter implements RateLimiter {
} }
} catch (error) { } catch (error) {
// reset 失败不阻断主流程 // reset 失败不阻断主流程
console.error( log.error(
"[rate-limit] Redis reset failure, ignoring:", { err: error },
error instanceof Error ? error.message : String(error), "Redis reset failure, ignoring",
) )
} }
} }

View File

@@ -1,6 +1,9 @@
import "server-only" import "server-only"
import { env } from "@/env.mjs" import { env } from "@/env.mjs"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("redis")
/** /**
* 共享 Redis 客户端单例rate-limit + cache 共用)。 * 共享 Redis 客户端单例rate-limit + cache 共用)。
@@ -42,9 +45,9 @@ export async function getRedisClient(): Promise<unknown | null> {
singleton = client singleton = client
return client return client
} catch (error) { } catch (error) {
console.error( log.error(
"[redis-client] Failed to load @upstash/redis:", { err: error },
error instanceof Error ? error.message : String(error), "Failed to load @upstash/redis",
) )
return null return null
} finally { } finally {

View File

@@ -19,6 +19,9 @@ import type { DataScope } from "@/shared/types/permissions"
import type { Exam } from "@/modules/exams/types" import type { Exam } from "@/modules/exams/types"
import type { HomeworkAssignmentListItem } from "@/modules/homework/types" import type { HomeworkAssignmentListItem } from "@/modules/homework/types"
import type { ExamWithQuestionsForHomework } from "@/modules/exams/data-access" import type { ExamWithQuestionsForHomework } from "@/modules/exams/data-access"
import { createModuleLogger } from "@/shared/lib/logger"
const log = createModuleLogger("exam-homework-port")
/** /**
* 考试/作业模块对外暴露的服务契约。 * 考试/作业模块对外暴露的服务契约。
@@ -72,7 +75,7 @@ class ServiceProvider<T> {
if (this.impl !== null) { if (this.impl !== null) {
// 允许重复注册HMR 场景),覆盖旧实现 // 允许重复注册HMR 场景),覆盖旧实现
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
console.warn("[ServiceProvider] Re-registering service implementation (HMR?)") log.warn("Re-registering service implementation (HMR?)")
} }
} }
this.impl = impl this.impl = impl