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:
@@ -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 },
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
"考试错题采集失败",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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,
|
"检测学生异常失败",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
src/shared/lib/cache/redis-store.ts
vendored
21
src/shared/lib/cache/redis-store.ts
vendored
@@ -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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user