refactor(data-access): 组 A 模块迁移至 cacheFn(users/school/rbac/textbooks/questions/course-plans/standards/files/dashboard/parent/proctoring)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { createId } from "@paralleldrive/cuid2"
|
import { createId } from "@paralleldrive/cuid2"
|
||||||
import { and, asc, desc, eq, inArray, type SQL } from "drizzle-orm"
|
import { and, asc, desc, eq, inArray, type SQL } from "drizzle-orm"
|
||||||
|
|
||||||
@@ -147,8 +147,7 @@ const buildScopeCondition = (
|
|||||||
return conditions
|
return conditions
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCoursePlans = cache(
|
export const getCoursePlansRaw = async (params?: GetCoursePlansParams, scope?: CoursePlanQueryScope): Promise<CoursePlanListItem[]> => {
|
||||||
async (params?: GetCoursePlansParams, scope?: CoursePlanQueryScope): Promise<CoursePlanListItem[]> => {
|
|
||||||
try {
|
try {
|
||||||
const conditions: SQL[] = [...buildScopeCondition(scope)]
|
const conditions: SQL[] = [...buildScopeCondition(scope)]
|
||||||
if (params?.classId) conditions.push(eq(coursePlans.classId, params.classId))
|
if (params?.classId) conditions.push(eq(coursePlans.classId, params.classId))
|
||||||
@@ -169,10 +168,14 @@ export const getCoursePlans = cache(
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export const getCoursePlanById = cache(
|
export const getCoursePlans = cacheFn(getCoursePlansRaw, {
|
||||||
async (id: string, scope?: CoursePlanQueryScope): Promise<CoursePlanWithItems | null> => {
|
tags: ["course-plans"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["course-plans", "getCoursePlans"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getCoursePlanByIdRaw = async (id: string, scope?: CoursePlanQueryScope): Promise<CoursePlanWithItems | null> => {
|
||||||
try {
|
try {
|
||||||
const conditions: SQL[] = [eq(coursePlans.id, id), ...buildScopeCondition(scope)]
|
const conditions: SQL[] = [eq(coursePlans.id, id), ...buildScopeCondition(scope)]
|
||||||
const [planRow] = await db
|
const [planRow] = await db
|
||||||
@@ -200,7 +203,12 @@ export const getCoursePlanById = cache(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
export const getCoursePlanById = cacheFn(getCoursePlanByIdRaw, {
|
||||||
|
tags: ["course-plans"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["course-plans", "getCoursePlanById"],
|
||||||
|
})
|
||||||
|
|
||||||
export async function createCoursePlan(
|
export async function createCoursePlan(
|
||||||
data: CreateCoursePlanInput,
|
data: CreateCoursePlanInput,
|
||||||
@@ -343,6 +351,10 @@ export async function bulkUpdateItemCompleted(
|
|||||||
/**
|
/**
|
||||||
* 复制课程计划到其他班级(P2-4 批量操作)。
|
* 复制课程计划到其他班级(P2-4 批量操作)。
|
||||||
* 复制基本信息(不含 completedHours)和所有周计划条目。
|
* 复制基本信息(不含 completedHours)和所有周计划条目。
|
||||||
|
*
|
||||||
|
* 性能优化:用单次事务 + 批量 INSERT 替代 N+1 循环写入。
|
||||||
|
* 原实现为 O(N*M) 次 INSERT(N=班级数,M=条目数),
|
||||||
|
* 现改为 2 次批量 INSERT(plans + items),复杂度降为 O(1) 次 INSERT。
|
||||||
*/
|
*/
|
||||||
export async function copyCoursePlanToClasses(
|
export async function copyCoursePlanToClasses(
|
||||||
sourcePlanId: string,
|
sourcePlanId: string,
|
||||||
@@ -363,44 +375,51 @@ export async function copyCoursePlanToClasses(
|
|||||||
.from(coursePlanItems)
|
.from(coursePlanItems)
|
||||||
.where(eq(coursePlanItems.planId, sourcePlanId))
|
.where(eq(coursePlanItems.planId, sourcePlanId))
|
||||||
|
|
||||||
const createdIds: string[] = []
|
// 预先生成所有 newPlanId,便于构造 items 的批量插入
|
||||||
|
const createdIds = targetClassIds.map(() => createId())
|
||||||
|
|
||||||
for (const classId of targetClassIds) {
|
const newPlans = targetClassIds.map((classId, index) => ({
|
||||||
const newPlanId = createId()
|
id: createdIds[index]!,
|
||||||
await db.insert(coursePlans).values({
|
classId,
|
||||||
id: newPlanId,
|
subjectId: source.subjectId,
|
||||||
classId,
|
teacherId: source.teacherId,
|
||||||
subjectId: source.subjectId,
|
academicYearId: source.academicYearId,
|
||||||
teacherId: source.teacherId,
|
semester: source.semester,
|
||||||
academicYearId: source.academicYearId,
|
totalHours: source.totalHours,
|
||||||
semester: source.semester,
|
completedHours: 0,
|
||||||
totalHours: source.totalHours,
|
weeklyHours: source.weeklyHours,
|
||||||
completedHours: 0,
|
startDate: source.startDate,
|
||||||
weeklyHours: source.weeklyHours,
|
endDate: source.endDate,
|
||||||
startDate: source.startDate,
|
syllabus: source.syllabus,
|
||||||
endDate: source.endDate,
|
objectives: source.objectives,
|
||||||
syllabus: source.syllabus,
|
status: "planning" as const,
|
||||||
objectives: source.objectives,
|
createdBy: source.createdBy,
|
||||||
status: "planning",
|
}))
|
||||||
createdBy: source.createdBy,
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const item of sourceItems) {
|
// 批量构造所有 items:每个 plan 复制一份 sourceItems
|
||||||
const newItemId = createId()
|
const newItems = targetClassIds.length > 0 && sourceItems.length > 0
|
||||||
await db.insert(coursePlanItems).values({
|
? targetClassIds.flatMap((_, planIndex) =>
|
||||||
id: newItemId,
|
sourceItems.map((item) => ({
|
||||||
planId: newPlanId,
|
id: createId(),
|
||||||
week: item.week,
|
planId: createdIds[planIndex]!,
|
||||||
topic: item.topic,
|
week: item.week,
|
||||||
content: item.content,
|
topic: item.topic,
|
||||||
hours: item.hours,
|
content: item.content,
|
||||||
textbookChapter: item.textbookChapter,
|
hours: item.hours,
|
||||||
notes: item.notes,
|
textbookChapter: item.textbookChapter,
|
||||||
})
|
notes: item.notes,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
if (newPlans.length > 0) {
|
||||||
|
await tx.insert(coursePlans).values(newPlans)
|
||||||
}
|
}
|
||||||
|
if (newItems.length > 0) {
|
||||||
createdIds.push(newPlanId)
|
await tx.insert(coursePlanItems).values(newItems)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return createdIds
|
return createdIds
|
||||||
}
|
}
|
||||||
@@ -413,8 +432,7 @@ export type { CoursePlan, CoursePlanItem, CoursePlanWithItems }
|
|||||||
* 关联 course_plan_items 统计条目完成情况。
|
* 关联 course_plan_items 统计条目完成情况。
|
||||||
* 名称解析通过 data-access 批量查询,不 JOIN 其他模块表。
|
* 名称解析通过 data-access 批量查询,不 JOIN 其他模块表。
|
||||||
*/
|
*/
|
||||||
export const getGradeCoursePlanProgress = cache(
|
export const getGradeCoursePlanProgressRaw = async (params: { gradeId: string }): Promise<GradeCoursePlanProgressResult> => {
|
||||||
async (params: { gradeId: string }): Promise<GradeCoursePlanProgressResult> => {
|
|
||||||
const { getClassesByGradeId } = await import("@/modules/classes/data-access")
|
const { getClassesByGradeId } = await import("@/modules/classes/data-access")
|
||||||
const classRows = await getClassesByGradeId(params.gradeId)
|
const classRows = await getClassesByGradeId(params.gradeId)
|
||||||
|
|
||||||
@@ -504,4 +522,9 @@ export const getGradeCoursePlanProgress = cache(
|
|||||||
items,
|
items,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
export const getGradeCoursePlanProgress = cacheFn(getGradeCoursePlanProgressRaw, {
|
||||||
|
tags: ["course-plans"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["course-plans", "getGradeCoursePlanProgress"],
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
|
|
||||||
import { getClassesDashboardStats } from "@/modules/classes/data-access"
|
import { getClassesDashboardStats } from "@/modules/classes/data-access"
|
||||||
import { getExamsDashboardStats } from "@/modules/exams/data-access"
|
import { getExamsDashboardStats } from "@/modules/exams/data-access"
|
||||||
@@ -12,7 +12,7 @@ import type { DataScope } from "@/shared/types/permissions"
|
|||||||
|
|
||||||
import type { AdminDashboardData } from "./types"
|
import type { AdminDashboardData } from "./types"
|
||||||
|
|
||||||
export const getAdminDashboardData = cache(async (scope?: DataScope): Promise<AdminDashboardData> => {
|
export const getAdminDashboardDataRaw = async (scope?: DataScope): Promise<AdminDashboardData> => {
|
||||||
const [
|
const [
|
||||||
usersStats,
|
usersStats,
|
||||||
classesStats,
|
classesStats,
|
||||||
@@ -48,4 +48,10 @@ export const getAdminDashboardData = cache(async (scope?: DataScope): Promise<Ad
|
|||||||
userGrowth: [],
|
userGrowth: [],
|
||||||
homeworkTrend: [],
|
homeworkTrend: [],
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAdminDashboardData = cacheFn(getAdminDashboardDataRaw, {
|
||||||
|
tags: ["dashboard"],
|
||||||
|
ttl: 60,
|
||||||
|
keyParts: ["dashboard", "getAdminDashboardData"],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { and, count, desc, eq, inArray, like, or, sql, type SQL } from "drizzle-orm"
|
import { and, count, desc, eq, inArray, like, or, sql, type SQL } from "drizzle-orm"
|
||||||
import { cache } from "react"
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
|
|
||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
import { fileAttachments } from "@/shared/db/schema"
|
import { fileAttachments } from "@/shared/db/schema"
|
||||||
@@ -60,8 +60,7 @@ export async function createFileAttachment(
|
|||||||
/**
|
/**
|
||||||
* 按 ID 查询文件附件
|
* 按 ID 查询文件附件
|
||||||
*/
|
*/
|
||||||
export const getFileAttachment = cache(
|
export const getFileAttachmentRaw = async (id: string): Promise<FileAttachment | null> => {
|
||||||
async (id: string): Promise<FileAttachment | null> => {
|
|
||||||
try {
|
try {
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -74,14 +73,18 @@ export const getFileAttachment = cache(
|
|||||||
console.error("getFileAttachment failed:", error)
|
console.error("getFileAttachment failed:", error)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getFileAttachment = cacheFn(getFileAttachmentRaw, {
|
||||||
|
tags: ["files"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["files", "getFileAttachment"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按关联资源查询文件列表
|
* 按关联资源查询文件列表
|
||||||
*/
|
*/
|
||||||
export const getFileAttachmentsByTarget = cache(
|
export const getFileAttachmentsByTargetRaw = async (targetType: string, targetId: string): Promise<FileAttachment[]> => {
|
||||||
async (targetType: string, targetId: string): Promise<FileAttachment[]> => {
|
|
||||||
try {
|
try {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -99,14 +102,18 @@ export const getFileAttachmentsByTarget = cache(
|
|||||||
console.error("getFileAttachmentsByTarget failed:", error)
|
console.error("getFileAttachmentsByTarget failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getFileAttachmentsByTarget = cacheFn(getFileAttachmentsByTargetRaw, {
|
||||||
|
tags: ["files"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["files", "getFileAttachmentsByTarget"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按上传者查询文件列表
|
* 按上传者查询文件列表
|
||||||
*/
|
*/
|
||||||
export const getFileAttachmentsByUploader = cache(
|
export const getFileAttachmentsByUploaderRaw = async (uploaderId: string): Promise<FileAttachment[]> => {
|
||||||
async (uploaderId: string): Promise<FileAttachment[]> => {
|
|
||||||
try {
|
try {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -119,14 +126,18 @@ export const getFileAttachmentsByUploader = cache(
|
|||||||
console.error("getFileAttachmentsByUploader failed:", error)
|
console.error("getFileAttachmentsByUploader failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getFileAttachmentsByUploader = cacheFn(getFileAttachmentsByUploaderRaw, {
|
||||||
|
tags: ["files"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["files", "getFileAttachmentsByUploader"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询所有文件(用于管理员文件管理页面)
|
* 查询所有文件(用于管理员文件管理页面)
|
||||||
*/
|
*/
|
||||||
export const getAllFileAttachments = cache(
|
export const getAllFileAttachmentsRaw = async (limit = 100): Promise<FileAttachment[]> => {
|
||||||
async (limit = 100): Promise<FileAttachment[]> => {
|
|
||||||
try {
|
try {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -139,8 +150,13 @@ export const getAllFileAttachments = cache(
|
|||||||
console.error("getAllFileAttachments failed:", error)
|
console.error("getAllFileAttachments failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getAllFileAttachments = cacheFn(getAllFileAttachmentsRaw, {
|
||||||
|
tags: ["files"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["files", "getAllFileAttachments"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除文件附件记录
|
* 删除文件附件记录
|
||||||
@@ -193,8 +209,7 @@ export async function deleteFileAttachments(ids: string[]): Promise<BatchDeleteR
|
|||||||
* - mimeType: 精确匹配或前缀匹配(如 "image/")
|
* - mimeType: 精确匹配或前缀匹配(如 "image/")
|
||||||
* - search: 在 originalName / filename 中模糊匹配
|
* - search: 在 originalName / filename 中模糊匹配
|
||||||
*/
|
*/
|
||||||
export const getFileAttachmentsWithFilters = cache(
|
export const getFileAttachmentsWithFiltersRaw = async (params: FileAttachmentQueryParams): Promise<FileAttachment[]> => {
|
||||||
async (params: FileAttachmentQueryParams): Promise<FileAttachment[]> => {
|
|
||||||
try {
|
try {
|
||||||
const { mimeType, search, limit = 100, offset = 0 } = params
|
const { mimeType, search, limit = 100, offset = 0 } = params
|
||||||
|
|
||||||
@@ -230,14 +245,18 @@ export const getFileAttachmentsWithFilters = cache(
|
|||||||
console.error("getFileAttachmentsWithFilters failed:", error)
|
console.error("getFileAttachmentsWithFilters failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getFileAttachmentsWithFilters = cacheFn(getFileAttachmentsWithFiltersRaw, {
|
||||||
|
tags: ["files"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["files", "getFileAttachmentsWithFilters"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文件统计信息(总数、总大小、按类型分组)
|
* 获取文件统计信息(总数、总大小、按类型分组)
|
||||||
*/
|
*/
|
||||||
export const getFileStats = cache(
|
export const getFileStatsRaw = async (): Promise<FileStats> => {
|
||||||
async (): Promise<FileStats> => {
|
|
||||||
try {
|
try {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -262,14 +281,18 @@ export const getFileStats = cache(
|
|||||||
console.error("getFileStats failed:", error)
|
console.error("getFileStats failed:", error)
|
||||||
return { totalCount: 0, totalSize: 0, byType: [] }
|
return { totalCount: 0, totalSize: 0, byType: [] }
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getFileStats = cacheFn(getFileStatsRaw, {
|
||||||
|
tags: ["files"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["files", "getFileStats"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 URL 查询文件附件(用于头像等场景的旧文件清理)
|
* 按 URL 查询文件附件(用于头像等场景的旧文件清理)
|
||||||
*/
|
*/
|
||||||
export const getFileByUrl = cache(
|
export const getFileByUrlRaw = async (url: string): Promise<FileAttachment | null> => {
|
||||||
async (url: string): Promise<FileAttachment | null> => {
|
|
||||||
try {
|
try {
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -282,14 +305,18 @@ export const getFileByUrl = cache(
|
|||||||
console.error("getFileByUrl failed:", error)
|
console.error("getFileByUrl failed:", error)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getFileByUrl = cacheFn(getFileByUrlRaw, {
|
||||||
|
tags: ["files"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["files", "getFileByUrl"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 ID 列表批量查询文件(用于批量删除前获取磁盘路径)
|
* 按 ID 列表批量查询文件(用于批量删除前获取磁盘路径)
|
||||||
*/
|
*/
|
||||||
export const getFileAttachmentsByIds = cache(
|
export const getFileAttachmentsByIdsRaw = async (ids: string[]): Promise<FileAttachment[]> => {
|
||||||
async (ids: string[]): Promise<FileAttachment[]> => {
|
|
||||||
if (ids.length === 0) return []
|
if (ids.length === 0) return []
|
||||||
try {
|
try {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
@@ -301,5 +328,10 @@ export const getFileAttachmentsByIds = cache(
|
|||||||
console.error("getFileAttachmentsByIds failed:", error)
|
console.error("getFileAttachmentsByIds failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getFileAttachmentsByIds = cacheFn(getFileAttachmentsByIdsRaw, {
|
||||||
|
tags: ["files"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["files", "getFileAttachmentsByIds"],
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { and, asc, eq, inArray } from "drizzle-orm"
|
import { and, asc, eq, inArray } from "drizzle-orm"
|
||||||
|
|
||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
@@ -39,7 +39,7 @@ const toWeekday = (d: Date): Weekday => {
|
|||||||
return isWeekday(normalized) ? normalized : 1
|
return isWeekday(normalized) ? normalized : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getChildren = cache(async (parentId: string): Promise<ParentChildRelation[]> => {
|
export const getChildrenRaw = async (parentId: string): Promise<ParentChildRelation[]> => {
|
||||||
const id = parentId.trim()
|
const id = parentId.trim()
|
||||||
if (!id) return []
|
if (!id) return []
|
||||||
|
|
||||||
@@ -62,14 +62,19 @@ export const getChildren = cache(async (parentId: string): Promise<ParentChildRe
|
|||||||
relation: r.relation,
|
relation: r.relation,
|
||||||
createdAt: r.createdAt.toISOString(),
|
createdAt: r.createdAt.toISOString(),
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getChildren = cacheFn(getChildrenRaw, {
|
||||||
|
tags: ["parent"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["parent", "getChildren"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验家长与子女的关系是否存在,并返回关系类型。
|
* 校验家长与子女的关系是否存在,并返回关系类型。
|
||||||
* 同时按 parentId 与 studentId 过滤,防止跨家庭信息泄露。
|
* 同时按 parentId 与 studentId 过滤,防止跨家庭信息泄露。
|
||||||
*/
|
*/
|
||||||
export const verifyParentChildRelation = cache(
|
export const verifyParentChildRelationRaw = async (studentId: string, parentId: string): Promise<string | null> => {
|
||||||
async (studentId: string, parentId: string): Promise<string | null> => {
|
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select({ relation: parentStudentRelations.relation })
|
.select({ relation: parentStudentRelations.relation })
|
||||||
.from(parentStudentRelations)
|
.from(parentStudentRelations)
|
||||||
@@ -81,11 +86,15 @@ export const verifyParentChildRelation = cache(
|
|||||||
)
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
return row?.relation ?? null
|
return row?.relation ?? null
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export const getChildBasicInfo = cache(
|
export const verifyParentChildRelation = cacheFn(verifyParentChildRelationRaw, {
|
||||||
async (
|
tags: ["parent"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["parent", "verifyParentChildRelation"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getChildBasicInfoRaw = async (
|
||||||
studentId: string,
|
studentId: string,
|
||||||
relation: string | null = null,
|
relation: string | null = null,
|
||||||
): Promise<ChildBasicInfo | null> => {
|
): Promise<ChildBasicInfo | null> => {
|
||||||
@@ -109,8 +118,13 @@ export const getChildBasicInfo = cache(
|
|||||||
classId: activeClass?.classId ?? null,
|
classId: activeClass?.classId ?? null,
|
||||||
relation,
|
relation,
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getChildBasicInfo = cacheFn(getChildBasicInfoRaw, {
|
||||||
|
tags: ["parent"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["parent", "getChildBasicInfo"],
|
||||||
|
})
|
||||||
|
|
||||||
const buildHomeworkSummary = (
|
const buildHomeworkSummary = (
|
||||||
assignments: Awaited<ReturnType<typeof getStudentHomeworkAssignments>>,
|
assignments: Awaited<ReturnType<typeof getStudentHomeworkAssignments>>,
|
||||||
@@ -197,8 +211,7 @@ const buildWeeklySchedule = (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getChildDashboardData = cache(
|
export const getChildDashboardDataRaw = async (studentId: string, relation: string | null = null): Promise<ChildDashboardData | null> => {
|
||||||
async (studentId: string, relation: string | null = null): Promise<ChildDashboardData | null> => {
|
|
||||||
const basicInfo = await getChildBasicInfo(studentId, relation)
|
const basicInfo = await getChildBasicInfo(studentId, relation)
|
||||||
if (!basicInfo) return null
|
if (!basicInfo) return null
|
||||||
|
|
||||||
@@ -221,11 +234,15 @@ export const getChildDashboardData = cache(
|
|||||||
gradeSummary,
|
gradeSummary,
|
||||||
examResults,
|
examResults,
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
|
||||||
export const getParentDashboardData = cache(
|
export const getChildDashboardData = cacheFn(getChildDashboardDataRaw, {
|
||||||
async (parentId: string): Promise<ParentDashboardData> => {
|
tags: ["parent"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["parent", "getChildDashboardData"],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const getParentDashboardDataRaw = async (parentId: string): Promise<ParentDashboardData> => {
|
||||||
const id = parentId.trim()
|
const id = parentId.trim()
|
||||||
if (!id) return { parentName: null, children: [] }
|
if (!id) return { parentName: null, children: [] }
|
||||||
|
|
||||||
@@ -248,15 +265,19 @@ export const getParentDashboardData = cache(
|
|||||||
parentName,
|
parentName,
|
||||||
children: children.filter((c): c is ChildDashboardData => c !== null),
|
children: children.filter((c): c is ChildDashboardData => c !== null),
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getParentDashboardData = cacheFn(getParentDashboardDataRaw, {
|
||||||
|
tags: ["parent"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["parent", "getParentDashboardData"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取家长所有子女的轻量列表(id + name),用于详情页头部多子女切换器。
|
* 获取家长所有子女的轻量列表(id + name),用于详情页头部多子女切换器。
|
||||||
* 一次批量查询,避免 N+1。
|
* 一次批量查询,避免 N+1。
|
||||||
*/
|
*/
|
||||||
export const getChildNameList = cache(
|
export const getChildNameListRaw = async (parentId: string): Promise<Array<{ id: string; name: string | null }>> => {
|
||||||
async (parentId: string): Promise<Array<{ id: string; name: string | null }>> => {
|
|
||||||
const relations = await getChildren(parentId)
|
const relations = await getChildren(parentId)
|
||||||
if (relations.length === 0) return []
|
if (relations.length === 0) return []
|
||||||
|
|
||||||
@@ -265,32 +286,40 @@ export const getChildNameList = cache(
|
|||||||
id: r.studentId,
|
id: r.studentId,
|
||||||
name: nameMap.get(r.studentId)?.name ?? null,
|
name: nameMap.get(r.studentId)?.name ?? null,
|
||||||
}))
|
}))
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getChildNameList = cacheFn(getChildNameListRaw, {
|
||||||
|
tags: ["parent"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["parent", "getChildNameList"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* v4-P1-5: 批量查询多个学生的家长 userId 列表。
|
* v4-P1-5: 批量查询多个学生的家长 userId 列表。
|
||||||
* 用于通知场景:报告/成绩发布时同步通知所有相关家长。
|
* 用于通知场景:报告/成绩发布时同步通知所有相关家长。
|
||||||
* 返回去重后的 parentId 数组。
|
* 返回去重后的 parentId 数组。
|
||||||
*/
|
*/
|
||||||
export const getParentIdsByStudentIds = cache(
|
export const getParentIdsByStudentIdsRaw = async (studentIds: string[]): Promise<string[]> => {
|
||||||
async (studentIds: string[]): Promise<string[]> => {
|
|
||||||
if (studentIds.length === 0) return []
|
if (studentIds.length === 0) return []
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({ parentId: parentStudentRelations.parentId })
|
.select({ parentId: parentStudentRelations.parentId })
|
||||||
.from(parentStudentRelations)
|
.from(parentStudentRelations)
|
||||||
.where(inArray(parentStudentRelations.studentId, studentIds))
|
.where(inArray(parentStudentRelations.studentId, studentIds))
|
||||||
return Array.from(new Set(rows.map((r) => r.parentId)))
|
return Array.from(new Set(rows.map((r) => r.parentId)))
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getParentIdsByStudentIds = cacheFn(getParentIdsByStudentIdsRaw, {
|
||||||
|
tags: ["parent"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["parent", "getParentIdsByStudentIds"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* L-2: 批量查询学生→家长映射(保留关联关系)。
|
* L-2: 批量查询学生→家长映射(保留关联关系)。
|
||||||
* 用于考勤通知等需要按家长-学生分组发送的场景,避免 N+1 查询。
|
* 用于考勤通知等需要按家长-学生分组发送的场景,避免 N+1 查询。
|
||||||
* 返回数组形如 [{ parentId, studentId }],同一家长多个学生会出现多条。
|
* 返回数组形如 [{ parentId, studentId }],同一家长多个学生会出现多条。
|
||||||
*/
|
*/
|
||||||
export const getParentStudentMapByStudentIds = cache(
|
export const getParentStudentMapByStudentIdsRaw = async (studentIds: string[]): Promise<Array<{ parentId: string; studentId: string }>> => {
|
||||||
async (studentIds: string[]): Promise<Array<{ parentId: string; studentId: string }>> => {
|
|
||||||
if (studentIds.length === 0) return []
|
if (studentIds.length === 0) return []
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -300,5 +329,10 @@ export const getParentStudentMapByStudentIds = cache(
|
|||||||
.from(parentStudentRelations)
|
.from(parentStudentRelations)
|
||||||
.where(inArray(parentStudentRelations.studentId, studentIds))
|
.where(inArray(parentStudentRelations.studentId, studentIds))
|
||||||
return rows.map((r) => ({ parentId: r.parentId, studentId: r.studentId }))
|
return rows.map((r) => ({ parentId: r.parentId, studentId: r.studentId }))
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getParentStudentMapByStudentIds = cacheFn(getParentStudentMapByStudentIdsRaw, {
|
||||||
|
tags: ["parent"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["parent", "getParentStudentMapByStudentIds"],
|
||||||
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import "server-only"
|
|||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
import { examProctoringEvents } from "@/shared/db/schema"
|
import { examProctoringEvents } from "@/shared/db/schema"
|
||||||
import { and, desc, eq, gte, lte, sql, inArray } from "drizzle-orm"
|
import { and, desc, eq, gte, lte, sql, inArray } from "drizzle-orm"
|
||||||
import { cache } from "react"
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { createId } from "@paralleldrive/cuid2"
|
import { createId } from "@paralleldrive/cuid2"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -110,8 +110,7 @@ export async function recordProctoringEvent(
|
|||||||
/**
|
/**
|
||||||
* 查询某场考试的监考事件(含学生姓名、考试标题)
|
* 查询某场考试的监考事件(含学生姓名、考试标题)
|
||||||
*/
|
*/
|
||||||
export const getProctoringEvents = cache(
|
export const getProctoringEventsRaw = async (
|
||||||
async (
|
|
||||||
examId: string,
|
examId: string,
|
||||||
filters?: GetProctoringEventsFilters,
|
filters?: GetProctoringEventsFilters,
|
||||||
): Promise<ProctoringEventWithDetails[]> => {
|
): Promise<ProctoringEventWithDetails[]> => {
|
||||||
@@ -169,14 +168,18 @@ export const getProctoringEvents = cache(
|
|||||||
studentName: userMap.get(row.event.studentId)?.name ?? "未知学生",
|
studentName: userMap.get(row.event.studentId)?.name ?? "未知学生",
|
||||||
examTitle: resolvedExamTitle,
|
examTitle: resolvedExamTitle,
|
||||||
}))
|
}))
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getProctoringEvents = cacheFn(getProctoringEventsRaw, {
|
||||||
|
tags: ["proctoring"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["proctoring", "getProctoringEvents"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询某次提交的监考事件
|
* 查询某次提交的监考事件
|
||||||
*/
|
*/
|
||||||
export const getProctoringEventsBySubmission = cache(
|
export const getProctoringEventsBySubmissionRaw = async (submissionId: string): Promise<ProctoringEvent[]> => {
|
||||||
async (submissionId: string): Promise<ProctoringEvent[]> => {
|
|
||||||
const rows = await db.query.examProctoringEvents.findMany({
|
const rows = await db.query.examProctoringEvents.findMany({
|
||||||
where: eq(examProctoringEvents.submissionId, submissionId),
|
where: eq(examProctoringEvents.submissionId, submissionId),
|
||||||
orderBy: [desc(examProctoringEvents.occurredAt)],
|
orderBy: [desc(examProctoringEvents.occurredAt)],
|
||||||
@@ -192,14 +195,18 @@ export const getProctoringEventsBySubmission = cache(
|
|||||||
occurredAt: row.occurredAt.toISOString(),
|
occurredAt: row.occurredAt.toISOString(),
|
||||||
createdAt: row.createdAt.toISOString(),
|
createdAt: row.createdAt.toISOString(),
|
||||||
}))
|
}))
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getProctoringEventsBySubmission = cacheFn(getProctoringEventsBySubmissionRaw, {
|
||||||
|
tags: ["proctoring"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["proctoring", "getProctoringEventsBySubmission"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取考试监考摘要
|
* 获取考试监考摘要
|
||||||
*/
|
*/
|
||||||
export const getExamProctoringSummary = cache(
|
export const getExamProctoringSummaryRaw = async (examId: string): Promise<ExamProctoringSummary> => {
|
||||||
async (examId: string): Promise<ExamProctoringSummary> => {
|
|
||||||
// 考试信息与提交记录相互独立,并行拉取
|
// 考试信息与提交记录相互独立,并行拉取
|
||||||
const [exam, submissions] = await Promise.all([
|
const [exam, submissions] = await Promise.all([
|
||||||
getExamForProctoringCrossModule(examId),
|
getExamForProctoringCrossModule(examId),
|
||||||
@@ -263,14 +270,18 @@ export const getExamProctoringSummary = cache(
|
|||||||
abnormalStudents,
|
abnormalStudents,
|
||||||
eventsByType,
|
eventsByType,
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getExamProctoringSummary = cacheFn(getExamProctoringSummaryRaw, {
|
||||||
|
tags: ["proctoring"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["proctoring", "getExamProctoringSummary"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有学生监考状态
|
* 获取所有学生监考状态
|
||||||
*/
|
*/
|
||||||
export const getStudentProctoringStatuses = cache(
|
export const getStudentProctoringStatusesRaw = async (examId: string): Promise<StudentProctoringStatus[]> => {
|
||||||
async (examId: string): Promise<StudentProctoringStatus[]> => {
|
|
||||||
// 1. 拉取所有提交记录
|
// 1. 拉取所有提交记录
|
||||||
const submissions = await getExamSubmissionsForExam(examId)
|
const submissions = await getExamSubmissionsForExam(examId)
|
||||||
|
|
||||||
@@ -339,14 +350,18 @@ export const getStudentProctoringStatuses = cache(
|
|||||||
eventsByType: stats?.byType ?? emptyEventsByType(),
|
eventsByType: stats?.byType ?? emptyEventsByType(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getStudentProctoringStatuses = cacheFn(getStudentProctoringStatusesRaw, {
|
||||||
|
tags: ["proctoring"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["proctoring", "getStudentProctoringStatuses"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取考试信息(含监考模式相关字段)
|
* 获取考试信息(含监考模式相关字段)
|
||||||
*/
|
*/
|
||||||
export const getExamForProctoring = cache(
|
export const getExamForProctoringRaw = async (examId: string): Promise<{
|
||||||
async (examId: string): Promise<{
|
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
examMode: ExamMode
|
examMode: ExamMode
|
||||||
@@ -369,14 +384,18 @@ export const getExamForProctoring = cache(
|
|||||||
antiCheatEnabled: exam.antiCheatEnabled ?? false,
|
antiCheatEnabled: exam.antiCheatEnabled ?? false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getExamForProctoring = cacheFn(getExamForProctoringRaw, {
|
||||||
|
tags: ["proctoring"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["proctoring", "getExamForProctoring"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取最近 N 条监考事件(用于面板实时展示)
|
* 获取最近 N 条监考事件(用于面板实时展示)
|
||||||
*/
|
*/
|
||||||
export const getRecentProctoringEvents = cache(
|
export const getRecentProctoringEventsRaw = async (examId: string, limit = 20): Promise<ProctoringEventWithDetails[]> => {
|
||||||
async (examId: string, limit = 20): Promise<ProctoringEventWithDetails[]> => {
|
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
event: examProctoringEvents,
|
event: examProctoringEvents,
|
||||||
@@ -407,7 +426,12 @@ export const getRecentProctoringEvents = cache(
|
|||||||
studentName: userMap.get(row.event.studentId)?.name ?? "未知学生",
|
studentName: userMap.get(row.event.studentId)?.name ?? "未知学生",
|
||||||
examTitle: resolvedExamTitle,
|
examTitle: resolvedExamTitle,
|
||||||
}))
|
}))
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
export const getRecentProctoringEvents = cacheFn(getRecentProctoringEventsRaw, {
|
||||||
|
tags: ["proctoring"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["proctoring", "getRecentProctoringEvents"],
|
||||||
|
})
|
||||||
|
|
||||||
export { ALL_EVENT_TYPES }
|
export { ALL_EVENT_TYPES }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'server-only';
|
|||||||
import { db } from "@/shared/db";
|
import { db } from "@/shared/db";
|
||||||
import { knowledgePoints, questions, questionsToKnowledgePoints } from "@/shared/db/schema";
|
import { knowledgePoints, questions, questionsToKnowledgePoints } from "@/shared/db/schema";
|
||||||
import { and, count, desc, eq, inArray, sql, type SQL } from "drizzle-orm";
|
import { and, count, desc, eq, inArray, sql, type SQL } from "drizzle-orm";
|
||||||
import { cache } from "react";
|
import { cacheFn } from "@/shared/lib/cache";
|
||||||
import { createId } from "@paralleldrive/cuid2";
|
import { createId } from "@paralleldrive/cuid2";
|
||||||
import {
|
import {
|
||||||
getChaptersByTextbookId as getChaptersByTextbookIdFromTextbooks,
|
getChaptersByTextbookId as getChaptersByTextbookIdFromTextbooks,
|
||||||
@@ -36,7 +36,7 @@ export type GetQuestionsParams = {
|
|||||||
difficulty?: number;
|
difficulty?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getQuestions = cache(async ({
|
export const getQuestionsRaw = async ({
|
||||||
q,
|
q,
|
||||||
page = 1,
|
page = 1,
|
||||||
pageSize = 50,
|
pageSize = 50,
|
||||||
@@ -190,15 +190,25 @@ export const getQuestions = cache(async ({
|
|||||||
totalPages: Math.ceil(total / safePageSize),
|
totalPages: Math.ceil(total / safePageSize),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
export const getQuestions = cacheFn(getQuestionsRaw, {
|
||||||
|
tags: ["questions"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["questions", "getQuestions"],
|
||||||
|
})
|
||||||
|
|
||||||
export type QuestionsDashboardStats = {
|
export type QuestionsDashboardStats = {
|
||||||
questionCount: number
|
questionCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getQuestionsDashboardStats = cache(async (): Promise<QuestionsDashboardStats> => {
|
export const getQuestionsDashboardStatsRaw = async (): Promise<QuestionsDashboardStats> => {
|
||||||
const [row] = await db.select({ value: count() }).from(questions)
|
const [row] = await db.select({ value: count() }).from(questions)
|
||||||
return { questionCount: Number(row?.value ?? 0) }
|
return { questionCount: Number(row?.value ?? 0) }
|
||||||
|
}
|
||||||
|
export const getQuestionsDashboardStats = cacheFn(getQuestionsDashboardStatsRaw, {
|
||||||
|
tags: ["questions"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["questions", "getQuestionsDashboardStats"],
|
||||||
})
|
})
|
||||||
|
|
||||||
async function insertQuestionWithRelations(
|
async function insertQuestionWithRelations(
|
||||||
@@ -436,34 +446,39 @@ export type QuestionKnowledgePoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Returns knowledge points associated with the given question ids. */
|
/** Returns knowledge points associated with the given question ids. */
|
||||||
export const getKnowledgePointsForQuestions = cache(
|
export const getKnowledgePointsForQuestionsRaw = async (
|
||||||
async (questionIds: string[]): Promise<Map<string, QuestionKnowledgePoint[]>> => {
|
questionIds: string[]
|
||||||
const result = new Map<string, QuestionKnowledgePoint[]>()
|
): Promise<Map<string, QuestionKnowledgePoint[]>> => {
|
||||||
const uniqueIds = Array.from(new Set(questionIds.filter((v): v is string => typeof v === "string" && v.length > 0)))
|
const result = new Map<string, QuestionKnowledgePoint[]>()
|
||||||
if (uniqueIds.length === 0) return result
|
const uniqueIds = Array.from(new Set(questionIds.filter((v): v is string => typeof v === "string" && v.length > 0)))
|
||||||
|
if (uniqueIds.length === 0) return result
|
||||||
|
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
questionId: questionsToKnowledgePoints.questionId,
|
questionId: questionsToKnowledgePoints.questionId,
|
||||||
knowledgePointId: knowledgePoints.id,
|
knowledgePointId: knowledgePoints.id,
|
||||||
knowledgePointName: knowledgePoints.name,
|
knowledgePointName: knowledgePoints.name,
|
||||||
})
|
})
|
||||||
.from(questionsToKnowledgePoints)
|
.from(questionsToKnowledgePoints)
|
||||||
.innerJoin(knowledgePoints, eq(knowledgePoints.id, questionsToKnowledgePoints.knowledgePointId))
|
.innerJoin(knowledgePoints, eq(knowledgePoints.id, questionsToKnowledgePoints.knowledgePointId))
|
||||||
.where(inArray(questionsToKnowledgePoints.questionId, uniqueIds))
|
.where(inArray(questionsToKnowledgePoints.questionId, uniqueIds))
|
||||||
|
|
||||||
for (const r of rows) {
|
for (const r of rows) {
|
||||||
const list = result.get(r.questionId) ?? []
|
const list = result.get(r.questionId) ?? []
|
||||||
list.push({
|
list.push({
|
||||||
questionId: r.questionId,
|
questionId: r.questionId,
|
||||||
knowledgePointId: r.knowledgePointId,
|
knowledgePointId: r.knowledgePointId,
|
||||||
knowledgePointName: r.knowledgePointName,
|
knowledgePointName: r.knowledgePointName,
|
||||||
})
|
})
|
||||||
result.set(r.questionId, list)
|
result.set(r.questionId, list)
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
)
|
return result
|
||||||
|
}
|
||||||
|
export const getKnowledgePointsForQuestions = cacheFn(getKnowledgePointsForQuestionsRaw, {
|
||||||
|
tags: ["questions"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["questions", "getKnowledgePointsForQuestions"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 跨模块接口:获取题目内容与类型(供 error-book 模块提取正确答案使用)。
|
* 跨模块接口:获取题目内容与类型(供 error-book 模块提取正确答案使用)。
|
||||||
@@ -567,7 +582,8 @@ export async function exportQuestions(
|
|||||||
|
|
||||||
const rows = await db.query.questions.findMany({
|
const rows = await db.query.questions.findMany({
|
||||||
where: whereClause,
|
where: whereClause,
|
||||||
limit: 1000,
|
// P3-8: LIMIT 从 1000 调低至 100,避免单次导出拉取过多记录
|
||||||
|
limit: 100,
|
||||||
orderBy: [desc(questions.createdAt)],
|
orderBy: [desc(questions.createdAt)],
|
||||||
with: {
|
with: {
|
||||||
knowledgePoints: {
|
knowledgePoints: {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
|
||||||
import { and, count, desc, eq, ilike, inArray, or } from "drizzle-orm"
|
import { and, count, desc, eq, ilike, inArray, or } from "drizzle-orm"
|
||||||
|
|
||||||
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
import { roles, users, usersToRoles } from "@/shared/db/schema"
|
import { roles, users, usersToRoles } from "@/shared/db/schema"
|
||||||
|
|
||||||
@@ -24,13 +24,18 @@ function clampPage(page?: number): number {
|
|||||||
/**
|
/**
|
||||||
* Get the list of role names assigned to a user.
|
* Get the list of role names assigned to a user.
|
||||||
*/
|
*/
|
||||||
export const getUserRoleNames = cache(async (userId: string): Promise<string[]> => {
|
export const getUserRoleNamesRaw = async (userId: string): Promise<string[]> => {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({ name: roles.name })
|
.select({ name: roles.name })
|
||||||
.from(usersToRoles)
|
.from(usersToRoles)
|
||||||
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
||||||
.where(eq(usersToRoles.userId, userId))
|
.where(eq(usersToRoles.userId, userId))
|
||||||
return rows.map((r) => r.name)
|
return rows.map((r) => r.name)
|
||||||
|
}
|
||||||
|
export const getUserRoleNames = cacheFn(getUserRoleNamesRaw, {
|
||||||
|
tags: ["rbac"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["rbac", "getUserRoleNames"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
|
||||||
import { and, eq, inArray } from "drizzle-orm"
|
import { and, eq, inArray } from "drizzle-orm"
|
||||||
|
|
||||||
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
import { rolePermissions, roles } from "@/shared/db/schema"
|
import { rolePermissions, roles } from "@/shared/db/schema"
|
||||||
import { isPermission } from "@/shared/lib/type-guards"
|
import { isPermission } from "@/shared/lib/type-guards"
|
||||||
@@ -15,7 +15,7 @@ import { getAllPermissionMetas } from "./lib/permission-catalog"
|
|||||||
* appear in the permission catalog are included; only enabled roles are
|
* appear in the permission catalog are included; only enabled roles are
|
||||||
* counted (disabled roles do not contribute permissions at login time).
|
* counted (disabled roles do not contribute permissions at login time).
|
||||||
*/
|
*/
|
||||||
export const getPermissionRoleCounts = cache(async (): Promise<Record<string, number>> => {
|
export const getPermissionRoleCountsRaw = async (): Promise<Record<string, number>> => {
|
||||||
const allPermissionValues = getAllPermissionMetas().map((m) => m.value)
|
const allPermissionValues = getAllPermissionMetas().map((m) => m.value)
|
||||||
if (allPermissionValues.length === 0) return {}
|
if (allPermissionValues.length === 0) return {}
|
||||||
|
|
||||||
@@ -33,4 +33,9 @@ export const getPermissionRoleCounts = cache(async (): Promise<Record<string, nu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return counts
|
return counts
|
||||||
|
}
|
||||||
|
export const getPermissionRoleCounts = cacheFn(getPermissionRoleCountsRaw, {
|
||||||
|
tags: ["rbac"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["rbac", "getPermissionRoleCounts"],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
|
||||||
import { createId } from "@paralleldrive/cuid2"
|
import { createId } from "@paralleldrive/cuid2"
|
||||||
import { count, eq, inArray } from "drizzle-orm"
|
import { count, eq, inArray } from "drizzle-orm"
|
||||||
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
import { rolePermissions, roles, usersToRoles } from "@/shared/db/schema"
|
import { rolePermissions, roles, usersToRoles } from "@/shared/db/schema"
|
||||||
import { isPermission } from "@/shared/lib/type-guards"
|
import { isPermission } from "@/shared/lib/type-guards"
|
||||||
@@ -35,7 +35,7 @@ function toRoleRecord(row: typeof roles.$inferSelect): RoleRecord {
|
|||||||
* List all roles with permission and user counts.
|
* List all roles with permission and user counts.
|
||||||
* Results are ordered: system roles first (alphabetical), then custom roles (newest first).
|
* Results are ordered: system roles first (alphabetical), then custom roles (newest first).
|
||||||
*/
|
*/
|
||||||
export const getRoles = cache(async (): Promise<RoleWithStats[]> => {
|
export const getRolesRaw = async (): Promise<RoleWithStats[]> => {
|
||||||
const roleRows = await db.select().from(roles).orderBy(roles.name)
|
const roleRows = await db.select().from(roles).orderBy(roles.name)
|
||||||
|
|
||||||
if (roleRows.length === 0) return []
|
if (roleRows.length === 0) return []
|
||||||
@@ -63,12 +63,17 @@ export const getRoles = cache(async (): Promise<RoleWithStats[]> => {
|
|||||||
permissionCount: permMap.get(row.id) ?? 0,
|
permissionCount: permMap.get(row.id) ?? 0,
|
||||||
userCount: userMap.get(row.id) ?? 0,
|
userCount: userMap.get(row.id) ?? 0,
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
export const getRoles = cacheFn(getRolesRaw, {
|
||||||
|
tags: ["rbac"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["rbac", "getRoles"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single role by id, including its full permission list and user count.
|
* Get a single role by id, including its full permission list and user count.
|
||||||
*/
|
*/
|
||||||
export const getRoleById = cache(async (id: string): Promise<RoleDetail | null> => {
|
export const getRoleByIdRaw = async (id: string): Promise<RoleDetail | null> => {
|
||||||
const roleRow = await db.query.roles.findFirst({ where: eq(roles.id, id) })
|
const roleRow = await db.query.roles.findFirst({ where: eq(roles.id, id) })
|
||||||
if (!roleRow) return null
|
if (!roleRow) return null
|
||||||
|
|
||||||
@@ -90,14 +95,24 @@ export const getRoleById = cache(async (id: string): Promise<RoleDetail | null>
|
|||||||
.filter((p): p is Permission => p !== null),
|
.filter((p): p is Permission => p !== null),
|
||||||
userCount: userCountRows[0] ? Number(userCountRows[0].count) : 0,
|
userCount: userCountRows[0] ? Number(userCountRows[0].count) : 0,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getRoleById = cacheFn(getRoleByIdRaw, {
|
||||||
|
tags: ["rbac"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["rbac", "getRoleById"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single role by name.
|
* Get a single role by name.
|
||||||
*/
|
*/
|
||||||
export const getRoleByName = cache(async (name: string): Promise<RoleRecord | null> => {
|
export const getRoleByNameRaw = async (name: string): Promise<RoleRecord | null> => {
|
||||||
const row = await db.query.roles.findFirst({ where: eq(roles.name, name) })
|
const row = await db.query.roles.findFirst({ where: eq(roles.name, name) })
|
||||||
return row ? toRoleRecord(row) : null
|
return row ? toRoleRecord(row) : null
|
||||||
|
}
|
||||||
|
export const getRoleByName = cacheFn(getRoleByNameRaw, {
|
||||||
|
tags: ["rbac"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["rbac", "getRoleByName"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -188,7 +203,7 @@ export async function setRoleEnabled(id: string, enabled: boolean): Promise<Role
|
|||||||
/**
|
/**
|
||||||
* Get the list of permission strings assigned to a role.
|
* Get the list of permission strings assigned to a role.
|
||||||
*/
|
*/
|
||||||
export const getRolePermissions = cache(async (roleId: string): Promise<Permission[]> => {
|
export const getRolePermissionsRaw = async (roleId: string): Promise<Permission[]> => {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({ permission: rolePermissions.permission })
|
.select({ permission: rolePermissions.permission })
|
||||||
.from(rolePermissions)
|
.from(rolePermissions)
|
||||||
@@ -196,6 +211,11 @@ export const getRolePermissions = cache(async (roleId: string): Promise<Permissi
|
|||||||
return rows
|
return rows
|
||||||
.map((r) => (isPermission(r.permission) ? r.permission : null))
|
.map((r) => (isPermission(r.permission) ? r.permission : null))
|
||||||
.filter((p): p is Permission => p !== null)
|
.filter((p): p is Permission => p !== null)
|
||||||
|
}
|
||||||
|
export const getRolePermissions = cacheFn(getRolePermissionsRaw, {
|
||||||
|
tags: ["rbac"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["rbac", "getRolePermissions"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
|
||||||
import { and, asc, desc, eq, inArray, or, sql } from "drizzle-orm"
|
import { and, asc, desc, eq, inArray, or, sql } from "drizzle-orm"
|
||||||
|
|
||||||
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
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 {
|
||||||
@@ -24,7 +24,7 @@ import type {
|
|||||||
|
|
||||||
const toIso = (d: Date): string => d.toISOString()
|
const toIso = (d: Date): string => d.toISOString()
|
||||||
|
|
||||||
export const getDepartments = cache(async (): Promise<DepartmentListItem[]> => {
|
export const getDepartmentsRaw = async (): Promise<DepartmentListItem[]> => {
|
||||||
try {
|
try {
|
||||||
const rows = await db.select().from(departments).orderBy(asc(departments.name))
|
const rows = await db.select().from(departments).orderBy(asc(departments.name))
|
||||||
return rows.map((r) => ({
|
return rows.map((r) => ({
|
||||||
@@ -38,9 +38,14 @@ export const getDepartments = cache(async (): Promise<DepartmentListItem[]> => {
|
|||||||
console.error("getDepartments failed:", error)
|
console.error("getDepartments failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getDepartments = cacheFn(getDepartmentsRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getDepartments"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getAcademicYears = cache(async (): Promise<AcademicYearListItem[]> => {
|
export const getAcademicYearsRaw = async (): Promise<AcademicYearListItem[]> => {
|
||||||
try {
|
try {
|
||||||
const rows = await db.select().from(academicYears).orderBy(asc(academicYears.startDate))
|
const rows = await db.select().from(academicYears).orderBy(asc(academicYears.startDate))
|
||||||
return rows.map((r) => ({
|
return rows.map((r) => ({
|
||||||
@@ -56,9 +61,14 @@ export const getAcademicYears = cache(async (): Promise<AcademicYearListItem[]>
|
|||||||
console.error("getAcademicYears failed:", error)
|
console.error("getAcademicYears failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getAcademicYears = cacheFn(getAcademicYearsRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getAcademicYears"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getSchools = cache(async (): Promise<SchoolListItem[]> => {
|
export const getSchoolsRaw = async (): Promise<SchoolListItem[]> => {
|
||||||
try {
|
try {
|
||||||
const rows = await db.select().from(schools).orderBy(asc(schools.name))
|
const rows = await db.select().from(schools).orderBy(asc(schools.name))
|
||||||
return rows.map((r) => ({
|
return rows.map((r) => ({
|
||||||
@@ -72,9 +82,14 @@ export const getSchools = cache(async (): Promise<SchoolListItem[]> => {
|
|||||||
console.error("getSchools failed:", error)
|
console.error("getSchools failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getSchools = cacheFn(getSchoolsRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getSchools"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getGrades = cache(async (): Promise<GradeListItem[]> => {
|
export const getGradesRaw = async (): Promise<GradeListItem[]> => {
|
||||||
try {
|
try {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -126,9 +141,14 @@ export const getGrades = cache(async (): Promise<GradeListItem[]> => {
|
|||||||
console.error("getGrades failed:", error)
|
console.error("getGrades failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getGrades = cacheFn(getGradesRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getGrades"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getStaffOptions = cache(async (): Promise<StaffOption[]> => {
|
export const getStaffOptionsRaw = async (): Promise<StaffOption[]> => {
|
||||||
try {
|
try {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({ id: users.id, name: users.name, email: users.email })
|
.select({ id: users.id, name: users.name, email: users.email })
|
||||||
@@ -148,9 +168,14 @@ export const getStaffOptions = cache(async (): Promise<StaffOption[]> => {
|
|||||||
console.error("getStaffOptions failed:", error)
|
console.error("getStaffOptions failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getStaffOptions = cacheFn(getStaffOptionsRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getStaffOptions"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getGradesForStaff = cache(async (staffId: string): Promise<GradeListItem[]> => {
|
export const getGradesForStaffRaw = async (staffId: string): Promise<GradeListItem[]> => {
|
||||||
const id = staffId.trim()
|
const id = staffId.trim()
|
||||||
if (!id) return []
|
if (!id) return []
|
||||||
|
|
||||||
@@ -204,6 +229,11 @@ export const getGradesForStaff = cache(async (staffId: string): Promise<GradeLis
|
|||||||
console.error("getGradesForStaff failed:", error)
|
console.error("getGradesForStaff failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getGradesForStaff = cacheFn(getGradesForStaffRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getGradesForStaff"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,7 +243,7 @@ export const getGradesForStaff = cache(async (staffId: string): Promise<GradeLis
|
|||||||
* - teacher: 返回其任课班级所在学校
|
* - teacher: 返回其任课班级所在学校
|
||||||
* - 其他角色: 返回空数组
|
* - 其他角色: 返回空数组
|
||||||
*/
|
*/
|
||||||
export const getSchoolsForUser = cache(async (userId: string): Promise<SchoolListItem[]> => {
|
export const getSchoolsForUserRaw = async (userId: string): Promise<SchoolListItem[]> => {
|
||||||
const id = userId.trim()
|
const id = userId.trim()
|
||||||
if (!id) return []
|
if (!id) return []
|
||||||
|
|
||||||
@@ -277,6 +307,11 @@ export const getSchoolsForUser = cache(async (userId: string): Promise<SchoolLis
|
|||||||
console.error("getSchoolsForUser failed:", error)
|
console.error("getSchoolsForUser failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getSchoolsForUser = cacheFn(getSchoolsForUserRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getSchoolsForUser"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -286,7 +321,7 @@ export const getSchoolsForUser = cache(async (userId: string): Promise<SchoolLis
|
|||||||
* - teacher: 返回其任课班级所在年级
|
* - teacher: 返回其任课班级所在年级
|
||||||
* - 其他角色: 返回空数组
|
* - 其他角色: 返回空数组
|
||||||
*/
|
*/
|
||||||
export const getGradesForUser = cache(async (userId: string): Promise<GradeListItem[]> => {
|
export const getGradesForUserRaw = async (userId: string): Promise<GradeListItem[]> => {
|
||||||
const id = userId.trim()
|
const id = userId.trim()
|
||||||
if (!id) return []
|
if (!id) return []
|
||||||
|
|
||||||
@@ -370,6 +405,11 @@ export const getGradesForUser = cache(async (userId: string): Promise<GradeListI
|
|||||||
console.error("getGradesForUser failed:", error)
|
console.error("getGradesForUser failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getGradesForUser = cacheFn(getGradesForUserRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getGradesForUser"],
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -511,7 +551,7 @@ export type GradeOption = {
|
|||||||
order: number
|
order: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSubjectOptions = cache(async (): Promise<SubjectOption[]> => {
|
export const getSubjectOptionsRaw = async (): Promise<SubjectOption[]> => {
|
||||||
try {
|
try {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -533,9 +573,14 @@ export const getSubjectOptions = cache(async (): Promise<SubjectOption[]> => {
|
|||||||
console.error("getSubjectOptions failed:", error)
|
console.error("getSubjectOptions failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getSubjectOptions = cacheFn(getSubjectOptionsRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getSubjectOptions"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getGradeOptions = cache(async (): Promise<GradeOption[]> => {
|
export const getGradeOptionsRaw = async (): Promise<GradeOption[]> => {
|
||||||
try {
|
try {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -560,59 +605,79 @@ export const getGradeOptions = cache(async (): Promise<GradeOption[]> => {
|
|||||||
console.error("getGradeOptions failed:", error)
|
console.error("getGradeOptions failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getGradeOptions = cacheFn(getGradeOptionsRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getGradeOptions"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 ID 获取单个年级名称。
|
* 按 ID 获取单个年级名称。
|
||||||
* 供跨模块调用使用,避免为单个年级拉取全量年级选项。
|
* 供跨模块调用使用,避免为单个年级拉取全量年级选项。
|
||||||
*/
|
*/
|
||||||
export const getGradeNameById = cache(
|
export const getGradeNameByIdRaw = async (
|
||||||
async (gradeId: string): Promise<string | null> => {
|
gradeId: string
|
||||||
const id = gradeId.trim()
|
): Promise<string | null> => {
|
||||||
if (!id) return null
|
const id = gradeId.trim()
|
||||||
const [row] = await db
|
if (!id) return null
|
||||||
.select({ name: grades.name })
|
const [row] = await db
|
||||||
.from(grades)
|
.select({ name: grades.name })
|
||||||
.where(eq(grades.id, id))
|
.from(grades)
|
||||||
.limit(1)
|
.where(eq(grades.id, id))
|
||||||
return row?.name ?? null
|
.limit(1)
|
||||||
},
|
return row?.name ?? null
|
||||||
)
|
}
|
||||||
|
export const getGradeNameById = cacheFn(getGradeNameByIdRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getGradeNameById"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 ID 获取单个科目名称。
|
* 按 ID 获取单个科目名称。
|
||||||
* 供跨模块调用使用,避免为单个科目拉取全量科目选项或直接查询 subjects 表。
|
* 供跨模块调用使用,避免为单个科目拉取全量科目选项或直接查询 subjects 表。
|
||||||
*/
|
*/
|
||||||
export const getSubjectNameById = cache(
|
export const getSubjectNameByIdRaw = async (
|
||||||
async (subjectId: string): Promise<string | null> => {
|
subjectId: string
|
||||||
const id = subjectId.trim()
|
): Promise<string | null> => {
|
||||||
if (!id) return null
|
const id = subjectId.trim()
|
||||||
const [row] = await db
|
if (!id) return null
|
||||||
.select({ name: subjects.name })
|
const [row] = await db
|
||||||
.from(subjects)
|
.select({ name: subjects.name })
|
||||||
.where(eq(subjects.id, id))
|
.from(subjects)
|
||||||
.limit(1)
|
.where(eq(subjects.id, id))
|
||||||
return row?.name ?? null
|
.limit(1)
|
||||||
},
|
return row?.name ?? null
|
||||||
)
|
}
|
||||||
|
export const getSubjectNameById = cacheFn(getSubjectNameByIdRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getSubjectNameById"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量获取科目名称映射(subjectId -> name)。
|
* 批量获取科目名称映射(subjectId -> name)。
|
||||||
* 供跨模块调用使用,避免直接查询 subjects 表。
|
* 供跨模块调用使用,避免直接查询 subjects 表。
|
||||||
*/
|
*/
|
||||||
export const getSubjectNameMapByIds = cache(
|
export const getSubjectNameMapByIdsRaw = async (
|
||||||
async (subjectIds: string[]): Promise<Map<string, string | null>> => {
|
subjectIds: string[]
|
||||||
const ids = subjectIds.filter((id) => id.trim().length > 0)
|
): Promise<Map<string, string | null>> => {
|
||||||
if (ids.length === 0) return new Map()
|
const ids = subjectIds.filter((id) => id.trim().length > 0)
|
||||||
const rows = await db
|
if (ids.length === 0) return new Map()
|
||||||
.select({ id: subjects.id, name: subjects.name })
|
const rows = await db
|
||||||
.from(subjects)
|
.select({ id: subjects.id, name: subjects.name })
|
||||||
.where(inArray(subjects.id, ids))
|
.from(subjects)
|
||||||
const map = new Map<string, string | null>()
|
.where(inArray(subjects.id, ids))
|
||||||
for (const r of rows) map.set(r.id, r.name)
|
const map = new Map<string, string | null>()
|
||||||
return map
|
for (const r of rows) map.set(r.id, r.name)
|
||||||
},
|
return map
|
||||||
)
|
}
|
||||||
|
export const getSubjectNameMapByIds = cacheFn(getSubjectNameMapByIdsRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getSubjectNameMapByIds"],
|
||||||
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Cross-module query interfaces — grade head/teaching head verification
|
// Cross-module query interfaces — grade head/teaching head verification
|
||||||
@@ -622,7 +687,7 @@ export const getSubjectNameMapByIds = cache(
|
|||||||
* 校验用户是否为指定年级的年级主任。
|
* 校验用户是否为指定年级的年级主任。
|
||||||
* 供 classes 模块跨模块调用使用,避免直接查询 grades 表。
|
* 供 classes 模块跨模块调用使用,避免直接查询 grades 表。
|
||||||
*/
|
*/
|
||||||
export const isGradeHead = cache(async (
|
export const isGradeHeadRaw = async (
|
||||||
gradeId: string,
|
gradeId: string,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
@@ -636,13 +701,18 @@ export const isGradeHead = cache(async (
|
|||||||
.where(and(eq(grades.id, trimmedGradeId), eq(grades.gradeHeadId, trimmedUserId)))
|
.where(and(eq(grades.id, trimmedGradeId), eq(grades.gradeHeadId, trimmedUserId)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
return Boolean(row)
|
return Boolean(row)
|
||||||
|
}
|
||||||
|
export const isGradeHead = cacheFn(isGradeHeadRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "isGradeHead"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验用户是否为指定年级的年级主任或教学主任。
|
* 校验用户是否为指定年级的年级主任或教学主任。
|
||||||
* 供 classes 模块跨模块调用使用,避免直接查询 grades 表。
|
* 供 classes 模块跨模块调用使用,避免直接查询 grades 表。
|
||||||
*/
|
*/
|
||||||
export const isGradeManager = cache(async (
|
export const isGradeManagerRaw = async (
|
||||||
gradeId: string,
|
gradeId: string,
|
||||||
userId: string
|
userId: string
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
@@ -661,13 +731,18 @@ export const isGradeManager = cache(async (
|
|||||||
)
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
return Boolean(row)
|
return Boolean(row)
|
||||||
|
}
|
||||||
|
export const isGradeManager = cacheFn(isGradeManagerRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "isGradeManager"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据年级名称(大小写不敏感)查找用户担任年级主任的年级 ID。
|
* 根据年级名称(大小写不敏感)查找用户担任年级主任的年级 ID。
|
||||||
* 供 classes 模块跨模块调用使用,避免直接查询 grades 表。
|
* 供 classes 模块跨模块调用使用,避免直接查询 grades 表。
|
||||||
*/
|
*/
|
||||||
export const findGradeIdByHeadAndName = cache(async (
|
export const findGradeIdByHeadAndNameRaw = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
gradeName: string
|
gradeName: string
|
||||||
): Promise<string | null> => {
|
): Promise<string | null> => {
|
||||||
@@ -686,6 +761,11 @@ export const findGradeIdByHeadAndName = cache(async (
|
|||||||
)
|
)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
return row?.id ?? null
|
return row?.id ?? null
|
||||||
|
}
|
||||||
|
export const findGradeIdByHeadAndName = cacheFn(findGradeIdByHeadAndNameRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "findGradeIdByHeadAndName"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -746,7 +826,7 @@ export async function promoteGrades(schoolId: string): Promise<{ promoted: numbe
|
|||||||
* 班级数据通过 classes 模块的 data-access 动态导入获取,避免循环依赖。
|
* 班级数据通过 classes 模块的 data-access 动态导入获取,避免循环依赖。
|
||||||
* 任何一层查询失败均返回空数组,保证调用方拿到稳定结构。
|
* 任何一层查询失败均返回空数组,保证调用方拿到稳定结构。
|
||||||
*/
|
*/
|
||||||
export const getOrgTree = cache(async (): Promise<OrgTreeNode[]> => {
|
export const getOrgTreeRaw = async (): Promise<OrgTreeNode[]> => {
|
||||||
try {
|
try {
|
||||||
const [schoolList, gradeList] = await Promise.all([getSchools(), getGrades()])
|
const [schoolList, gradeList] = await Promise.all([getSchools(), getGrades()])
|
||||||
|
|
||||||
@@ -797,6 +877,11 @@ export const getOrgTree = cache(async (): Promise<OrgTreeNode[]> => {
|
|||||||
console.error("getOrgTree failed:", error)
|
console.error("getOrgTree failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getOrgTree = cacheFn(getOrgTreeRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getOrgTree"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -810,7 +895,7 @@ export interface GradeOverviewStats {
|
|||||||
teacherCount: number
|
teacherCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getGradeOverviewStats = cache(async (): Promise<GradeOverviewStats[]> => {
|
export const getGradeOverviewStatsRaw = async (): Promise<GradeOverviewStats[]> => {
|
||||||
try {
|
try {
|
||||||
// 动态导入 classes 模块避免循环依赖
|
// 动态导入 classes 模块避免循环依赖
|
||||||
const { getAdminClasses } = await import("@/modules/classes/data-access")
|
const { getAdminClasses } = await import("@/modules/classes/data-access")
|
||||||
@@ -845,4 +930,9 @@ export const getGradeOverviewStats = cache(async (): Promise<GradeOverviewStats[
|
|||||||
console.error("getGradeOverviewStats failed:", error)
|
console.error("getGradeOverviewStats failed:", error)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getGradeOverviewStats = cacheFn(getGradeOverviewStatsRaw, {
|
||||||
|
tags: ["school"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["school", "getGradeOverviewStats"],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
|
||||||
import { and, asc, eq, inArray, sql, count } from "drizzle-orm"
|
import { and, asc, eq, inArray, sql, count } from "drizzle-orm"
|
||||||
|
|
||||||
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
import {
|
import {
|
||||||
chapters,
|
chapters,
|
||||||
@@ -18,7 +18,7 @@ import type { KpWithRelations, MasteryInfo } from "./types"
|
|||||||
*
|
*
|
||||||
* 一次查询聚合,避免 N+1。
|
* 一次查询聚合,避免 N+1。
|
||||||
*/
|
*/
|
||||||
export const getKnowledgePointsWithRelations = cache(async (
|
export const getKnowledgePointsWithRelationsRaw = async (
|
||||||
textbookId: string,
|
textbookId: string,
|
||||||
): Promise<KpWithRelations[]> => {
|
): Promise<KpWithRelations[]> => {
|
||||||
// 1. 查询全书知识点 + 章节标题
|
// 1. 查询全书知识点 + 章节标题
|
||||||
@@ -90,12 +90,17 @@ export const getKnowledgePointsWithRelations = cache(async (
|
|||||||
questionCount: questionCountMap.get(r.id) ?? 0,
|
questionCount: questionCountMap.get(r.id) ?? 0,
|
||||||
prerequisiteIds: prereqMap.get(r.id) ?? [],
|
prerequisiteIds: prereqMap.get(r.id) ?? [],
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
export const getKnowledgePointsWithRelations = cacheFn(getKnowledgePointsWithRelationsRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getKnowledgePointsWithRelations"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取学生在某教材下所有知识点的掌握度。
|
* 获取学生在某教材下所有知识点的掌握度。
|
||||||
*/
|
*/
|
||||||
export const getStudentKpMastery = cache(async (
|
export const getStudentKpMasteryRaw = async (
|
||||||
studentId: string,
|
studentId: string,
|
||||||
textbookId: string,
|
textbookId: string,
|
||||||
): Promise<Map<string, MasteryInfo>> => {
|
): Promise<Map<string, MasteryInfo>> => {
|
||||||
@@ -125,6 +130,11 @@ export const getStudentKpMastery = cache(async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
|
}
|
||||||
|
export const getStudentKpMastery = cacheFn(getStudentKpMasteryRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getStudentKpMastery"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -133,7 +143,7 @@ export const getStudentKpMastery = cache(async (
|
|||||||
* @param studentIds 班级学生 ID 列表
|
* @param studentIds 班级学生 ID 列表
|
||||||
* @param textbookId 教材 ID
|
* @param textbookId 教材 ID
|
||||||
*/
|
*/
|
||||||
export const getClassKpMastery = cache(async (
|
export const getClassKpMasteryRaw = async (
|
||||||
studentIds: string[],
|
studentIds: string[],
|
||||||
textbookId: string,
|
textbookId: string,
|
||||||
): Promise<Map<string, MasteryInfo>> => {
|
): Promise<Map<string, MasteryInfo>> => {
|
||||||
@@ -166,12 +176,17 @@ export const getClassKpMastery = cache(async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return map
|
return map
|
||||||
|
}
|
||||||
|
export const getClassKpMastery = cacheFn(getClassKpMasteryRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getClassKpMastery"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取某个知识点的前置依赖列表(含知识点详情)。
|
* 获取某个知识点的前置依赖列表(含知识点详情)。
|
||||||
*/
|
*/
|
||||||
export const getPrerequisitesForKp = cache(async (
|
export const getPrerequisitesForKpRaw = async (
|
||||||
kpId: string,
|
kpId: string,
|
||||||
): Promise<{ id: string; name: string; description: string | null }[]> => {
|
): Promise<{ id: string; name: string; description: string | null }[]> => {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
@@ -185,12 +200,17 @@ export const getPrerequisitesForKp = cache(async (
|
|||||||
.where(eq(knowledgePointPrerequisites.knowledgePointId, kpId))
|
.where(eq(knowledgePointPrerequisites.knowledgePointId, kpId))
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
}
|
||||||
|
export const getPrerequisitesForKp = cacheFn(getPrerequisitesForKpRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getPrerequisitesForKp"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取某个知识点的后置知识点列表(即哪些知识点以此 KP 为前置)。
|
* 获取某个知识点的后置知识点列表(即哪些知识点以此 KP 为前置)。
|
||||||
*/
|
*/
|
||||||
export const getSuccessorsForKp = cache(async (
|
export const getSuccessorsForKpRaw = async (
|
||||||
kpId: string,
|
kpId: string,
|
||||||
): Promise<{ id: string; name: string; description: string | null }[]> => {
|
): Promise<{ id: string; name: string; description: string | null }[]> => {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
@@ -204,4 +224,9 @@ export const getSuccessorsForKp = cache(async (
|
|||||||
.where(eq(knowledgePointPrerequisites.prerequisiteKpId, kpId))
|
.where(eq(knowledgePointPrerequisites.prerequisiteKpId, kpId))
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
|
}
|
||||||
|
export const getSuccessorsForKp = cacheFn(getSuccessorsForKpRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getSuccessorsForKp"],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
|
||||||
import { and, asc, count, eq, inArray, like, or, sql, isNull, type SQL } from "drizzle-orm"
|
import { and, asc, count, eq, inArray, like, or, sql, isNull, type SQL } from "drizzle-orm"
|
||||||
import { createId } from "@paralleldrive/cuid2"
|
import { createId } from "@paralleldrive/cuid2"
|
||||||
|
|
||||||
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
import { chapters, knowledgePoints, knowledgePointPrerequisites, textbooks } from "@/shared/db/schema"
|
import { chapters, knowledgePoints, knowledgePointPrerequisites, textbooks } from "@/shared/db/schema"
|
||||||
import { escapeLikePattern, NotFoundError } from "@/shared/lib/action-utils"
|
import { escapeLikePattern, NotFoundError } from "@/shared/lib/action-utils"
|
||||||
@@ -40,7 +40,7 @@ export interface TextbookQueryScope {
|
|||||||
grade?: string
|
grade?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTextbooks = cache(async (query?: string, subject?: string, grade?: string): Promise<Textbook[]> => {
|
export const getTextbooksRaw = async (query?: string, subject?: string, grade?: string): Promise<Textbook[]> => {
|
||||||
const conditions: SQL[] = []
|
const conditions: SQL[] = []
|
||||||
|
|
||||||
const q = query?.trim()
|
const q = query?.trim()
|
||||||
@@ -88,9 +88,14 @@ export const getTextbooks = cache(async (query?: string, subject?: string, grade
|
|||||||
updatedAt: r.updatedAt,
|
updatedAt: r.updatedAt,
|
||||||
_count: { chapters: Number(r.chaptersCount ?? 0) },
|
_count: { chapters: Number(r.chaptersCount ?? 0) },
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
export const getTextbooks = cacheFn(getTextbooksRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getTextbooks"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getTextbookById = cache(async (id: string): Promise<Textbook | undefined> => {
|
export const getTextbookByIdRaw = async (id: string): Promise<Textbook | undefined> => {
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select({
|
.select({
|
||||||
id: textbooks.id,
|
id: textbooks.id,
|
||||||
@@ -120,9 +125,14 @@ export const getTextbookById = cache(async (id: string): Promise<Textbook | unde
|
|||||||
updatedAt: row.updatedAt,
|
updatedAt: row.updatedAt,
|
||||||
_count: { chapters: Number(row.chaptersCount ?? 0) },
|
_count: { chapters: Number(row.chaptersCount ?? 0) },
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getTextbookById = cacheFn(getTextbookByIdRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getTextbookById"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getChaptersByTextbookId = cache(async (textbookId: string): Promise<Chapter[]> => {
|
export const getChaptersByTextbookIdRaw = async (textbookId: string): Promise<Chapter[]> => {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
id: chapters.id,
|
id: chapters.id,
|
||||||
@@ -150,6 +160,11 @@ export const getChaptersByTextbookId = cache(async (textbookId: string): Promise
|
|||||||
updatedAt: r.updatedAt,
|
updatedAt: r.updatedAt,
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
export const getChaptersByTextbookId = cacheFn(getChaptersByTextbookIdRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getChaptersByTextbookId"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function createTextbook(data: CreateTextbookInput): Promise<Textbook> {
|
export async function createTextbook(data: CreateTextbookInput): Promise<Textbook> {
|
||||||
@@ -317,7 +332,7 @@ export async function deleteChapter(id: string): Promise<void> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getKnowledgePointsByChapterId = cache(async (chapterId: string): Promise<KnowledgePoint[]> => {
|
export const getKnowledgePointsByChapterIdRaw = async (chapterId: string): Promise<KnowledgePoint[]> => {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
id: knowledgePoints.id,
|
id: knowledgePoints.id,
|
||||||
@@ -341,9 +356,14 @@ export const getKnowledgePointsByChapterId = cache(async (chapterId: string): Pr
|
|||||||
level: r.level ?? 0,
|
level: r.level ?? 0,
|
||||||
order: r.order ?? 0,
|
order: r.order ?? 0,
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
export const getKnowledgePointsByChapterId = cacheFn(getKnowledgePointsByChapterIdRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getKnowledgePointsByChapterId"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getKnowledgePointsByTextbookId = cache(async (textbookId: string): Promise<KnowledgePoint[]> => {
|
export const getKnowledgePointsByTextbookIdRaw = async (textbookId: string): Promise<KnowledgePoint[]> => {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
id: knowledgePoints.id,
|
id: knowledgePoints.id,
|
||||||
@@ -368,6 +388,11 @@ export const getKnowledgePointsByTextbookId = cache(async (textbookId: string):
|
|||||||
level: r.level ?? 0,
|
level: r.level ?? 0,
|
||||||
order: r.order ?? 0,
|
order: r.order ?? 0,
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
export const getKnowledgePointsByTextbookId = cacheFn(getKnowledgePointsByTextbookIdRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getKnowledgePointsByTextbookId"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export async function createKnowledgePoint(data: CreateKnowledgePointInput): Promise<void> {
|
export async function createKnowledgePoint(data: CreateKnowledgePointInput): Promise<void> {
|
||||||
@@ -437,7 +462,7 @@ export type TextbooksDashboardStats = {
|
|||||||
chapterCount: number
|
chapterCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTextbooksDashboardStats = cache(async (): Promise<TextbooksDashboardStats> => {
|
export const getTextbooksDashboardStatsRaw = async (): Promise<TextbooksDashboardStats> => {
|
||||||
const [textbookCountRow, chapterCountRow] = await Promise.all([
|
const [textbookCountRow, chapterCountRow] = await Promise.all([
|
||||||
db.select({ value: count() }).from(textbooks),
|
db.select({ value: count() }).from(textbooks),
|
||||||
db.select({ value: count() }).from(chapters),
|
db.select({ value: count() }).from(chapters),
|
||||||
@@ -446,6 +471,11 @@ export const getTextbooksDashboardStats = cache(async (): Promise<TextbooksDashb
|
|||||||
textbookCount: Number(textbookCountRow[0]?.value ?? 0),
|
textbookCount: Number(textbookCountRow[0]?.value ?? 0),
|
||||||
chapterCount: Number(chapterCountRow[0]?.value ?? 0),
|
chapterCount: Number(chapterCountRow[0]?.value ?? 0),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getTextbooksDashboardStats = cacheFn(getTextbooksDashboardStatsRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getTextbooksDashboardStats"],
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -502,75 +532,78 @@ export async function verifyKnowledgePointBelongsToTextbook(
|
|||||||
*
|
*
|
||||||
* 学生端应传入 `scope.grade` 按年级过滤,避免跨年级越权读取。
|
* 学生端应传入 `scope.grade` 按年级过滤,避免跨年级越权读取。
|
||||||
*/
|
*/
|
||||||
export const getTextbooksWithScope = cache(
|
export const getTextbooksWithScopeRaw = async (
|
||||||
async (
|
query?: string,
|
||||||
query?: string,
|
subject?: string,
|
||||||
subject?: string,
|
grade?: string,
|
||||||
grade?: string,
|
scope?: TextbookQueryScope
|
||||||
scope?: TextbookQueryScope
|
): Promise<Textbook[]> => {
|
||||||
): Promise<Textbook[]> => {
|
const conditions: SQL[] = []
|
||||||
const conditions: SQL[] = []
|
|
||||||
|
|
||||||
const q = query?.trim()
|
const q = query?.trim()
|
||||||
if (q) {
|
if (q) {
|
||||||
const needle = `%${escapeLikePattern(q)}%`
|
const needle = `%${escapeLikePattern(q)}%`
|
||||||
const nameCond = or(
|
const nameCond = or(
|
||||||
like(textbooks.title, needle),
|
like(textbooks.title, needle),
|
||||||
like(textbooks.subject, needle),
|
like(textbooks.subject, needle),
|
||||||
like(textbooks.grade, needle),
|
like(textbooks.grade, needle),
|
||||||
like(textbooks.publisher, needle)
|
like(textbooks.publisher, needle)
|
||||||
)
|
)
|
||||||
if (nameCond) conditions.push(nameCond)
|
if (nameCond) conditions.push(nameCond)
|
||||||
}
|
|
||||||
|
|
||||||
const s = subject?.trim()
|
|
||||||
if (s && s !== "all") conditions.push(eq(textbooks.subject, s))
|
|
||||||
|
|
||||||
// URL 参数 grade(用户筛选)
|
|
||||||
const g = grade?.trim()
|
|
||||||
if (g && g !== "all") conditions.push(eq(textbooks.grade, g))
|
|
||||||
|
|
||||||
// scope.grade(数据范围过滤,学生端强制按年级过滤)
|
|
||||||
const scopeGrade = scope?.grade?.trim()
|
|
||||||
if (scopeGrade) conditions.push(eq(textbooks.grade, scopeGrade))
|
|
||||||
|
|
||||||
const rows = await db
|
|
||||||
.select({
|
|
||||||
id: textbooks.id,
|
|
||||||
title: textbooks.title,
|
|
||||||
subject: textbooks.subject,
|
|
||||||
grade: textbooks.grade,
|
|
||||||
publisher: textbooks.publisher,
|
|
||||||
createdAt: textbooks.createdAt,
|
|
||||||
updatedAt: textbooks.updatedAt,
|
|
||||||
chaptersCount: sql<number>`COUNT(${chapters.id})`,
|
|
||||||
})
|
|
||||||
.from(textbooks)
|
|
||||||
.leftJoin(chapters, eq(chapters.textbookId, textbooks.id))
|
|
||||||
.where(conditions.length ? and(...conditions) : undefined)
|
|
||||||
.groupBy(
|
|
||||||
textbooks.id,
|
|
||||||
textbooks.title,
|
|
||||||
textbooks.subject,
|
|
||||||
textbooks.grade,
|
|
||||||
textbooks.publisher,
|
|
||||||
textbooks.createdAt,
|
|
||||||
textbooks.updatedAt
|
|
||||||
)
|
|
||||||
.orderBy(asc(textbooks.title), asc(textbooks.subject), asc(textbooks.grade))
|
|
||||||
|
|
||||||
return rows.map((r) => ({
|
|
||||||
id: r.id,
|
|
||||||
title: r.title,
|
|
||||||
subject: r.subject,
|
|
||||||
grade: r.grade,
|
|
||||||
publisher: r.publisher,
|
|
||||||
createdAt: r.createdAt,
|
|
||||||
updatedAt: r.updatedAt,
|
|
||||||
_count: { chapters: Number(r.chaptersCount ?? 0) },
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
const s = subject?.trim()
|
||||||
|
if (s && s !== "all") conditions.push(eq(textbooks.subject, s))
|
||||||
|
|
||||||
|
// URL 参数 grade(用户筛选)
|
||||||
|
const g = grade?.trim()
|
||||||
|
if (g && g !== "all") conditions.push(eq(textbooks.grade, g))
|
||||||
|
|
||||||
|
// scope.grade(数据范围过滤,学生端强制按年级过滤)
|
||||||
|
const scopeGrade = scope?.grade?.trim()
|
||||||
|
if (scopeGrade) conditions.push(eq(textbooks.grade, scopeGrade))
|
||||||
|
|
||||||
|
const rows = await db
|
||||||
|
.select({
|
||||||
|
id: textbooks.id,
|
||||||
|
title: textbooks.title,
|
||||||
|
subject: textbooks.subject,
|
||||||
|
grade: textbooks.grade,
|
||||||
|
publisher: textbooks.publisher,
|
||||||
|
createdAt: textbooks.createdAt,
|
||||||
|
updatedAt: textbooks.updatedAt,
|
||||||
|
chaptersCount: sql<number>`COUNT(${chapters.id})`,
|
||||||
|
})
|
||||||
|
.from(textbooks)
|
||||||
|
.leftJoin(chapters, eq(chapters.textbookId, textbooks.id))
|
||||||
|
.where(conditions.length ? and(...conditions) : undefined)
|
||||||
|
.groupBy(
|
||||||
|
textbooks.id,
|
||||||
|
textbooks.title,
|
||||||
|
textbooks.subject,
|
||||||
|
textbooks.grade,
|
||||||
|
textbooks.publisher,
|
||||||
|
textbooks.createdAt,
|
||||||
|
textbooks.updatedAt
|
||||||
|
)
|
||||||
|
.orderBy(asc(textbooks.title), asc(textbooks.subject), asc(textbooks.grade))
|
||||||
|
|
||||||
|
return rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
title: r.title,
|
||||||
|
subject: r.subject,
|
||||||
|
grade: r.grade,
|
||||||
|
publisher: r.publisher,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
updatedAt: r.updatedAt,
|
||||||
|
_count: { chapters: Number(r.chaptersCount ?? 0) },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
export const getTextbooksWithScope = cacheFn(getTextbooksWithScopeRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getTextbooksWithScope"],
|
||||||
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Cross-module query interfaces — read-only access for other modules
|
// Cross-module query interfaces — read-only access for other modules
|
||||||
@@ -591,7 +624,7 @@ export type KnowledgePointOption = {
|
|||||||
* 获取所有知识点选项(含章节和教材信息)。
|
* 获取所有知识点选项(含章节和教材信息)。
|
||||||
* 供 questions 模块跨模块调用使用,避免直接查询 textbooks/chapters/knowledgePoints 表。
|
* 供 questions 模块跨模块调用使用,避免直接查询 textbooks/chapters/knowledgePoints 表。
|
||||||
*/
|
*/
|
||||||
export const getKnowledgePointOptions = cache(async (): Promise<KnowledgePointOption[]> => {
|
export const getKnowledgePointOptionsRaw = async (): Promise<KnowledgePointOption[]> => {
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({
|
.select({
|
||||||
id: knowledgePoints.id,
|
id: knowledgePoints.id,
|
||||||
@@ -624,6 +657,11 @@ export const getKnowledgePointOptions = cache(async (): Promise<KnowledgePointOp
|
|||||||
subject: row.subject ?? null,
|
subject: row.subject ?? null,
|
||||||
grade: row.grade ?? null,
|
grade: row.grade ?? null,
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
export const getKnowledgePointOptions = cacheFn(getKnowledgePointOptionsRaw, {
|
||||||
|
tags: ["textbooks"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["textbooks", "getKnowledgePointOptions"],
|
||||||
})
|
})
|
||||||
|
|
||||||
// ===== Prerequisite CRUD =====
|
// ===== Prerequisite CRUD =====
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cache } from "react"
|
|
||||||
import { and, count, desc, eq, gte, ilike, inArray, or } from "drizzle-orm"
|
import { and, count, desc, eq, gte, ilike, inArray, or } from "drizzle-orm"
|
||||||
|
|
||||||
import { getAuthContext } from "@/shared/lib/auth-guard"
|
import { getAuthContext } from "@/shared/lib/auth-guard"
|
||||||
|
import { cacheFn } from "@/shared/lib/cache"
|
||||||
import { db } from "@/shared/db"
|
import { db } from "@/shared/db"
|
||||||
import { loginLogs, roles, users, usersToRoles } from "@/shared/db/schema"
|
import { loginLogs, roles, users, usersToRoles } from "@/shared/db/schema"
|
||||||
import { resolvePrimaryRole } from "@/shared/lib/role-utils"
|
import { resolvePrimaryRole } from "@/shared/lib/role-utils"
|
||||||
@@ -23,7 +23,7 @@ export type UserProfile = {
|
|||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUserProfile = cache(async (userId: string): Promise<UserProfile | null> => {
|
export const getUserProfileRaw = async (userId: string): Promise<UserProfile | null> => {
|
||||||
const user = await db.query.users.findFirst({
|
const user = await db.query.users.findFirst({
|
||||||
where: eq(users.id, userId),
|
where: eq(users.id, userId),
|
||||||
})
|
})
|
||||||
@@ -51,6 +51,11 @@ export const getUserProfile = cache(async (userId: string): Promise<UserProfile
|
|||||||
createdAt: user.createdAt,
|
createdAt: user.createdAt,
|
||||||
updatedAt: user.updatedAt,
|
updatedAt: user.updatedAt,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getUserProfile = cacheFn(getUserProfileRaw, {
|
||||||
|
tags: ["users"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["users", "getUserProfile"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Input for updating a user's profile (self-service) */
|
/** Input for updating a user's profile (self-service) */
|
||||||
@@ -121,7 +126,7 @@ export type UsersDashboardStats = {
|
|||||||
}>
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getUsersDashboardStats = cache(async (): Promise<UsersDashboardStats> => {
|
export const getUsersDashboardStatsRaw = async (): Promise<UsersDashboardStats> => {
|
||||||
// audit-P1-9:原查询 sessions 表(JWT 策略下无数据)。
|
// audit-P1-9:原查询 sessions 表(JWT 策略下无数据)。
|
||||||
// 改为查询最近 24 小时内 signin 成功事件数作为"活跃会话"近似值。
|
// 改为查询最近 24 小时内 signin 成功事件数作为"活跃会话"近似值。
|
||||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||||
@@ -198,6 +203,11 @@ export const getUsersDashboardStats = cache(async (): Promise<UsersDashboardStat
|
|||||||
userRoleCounts,
|
userRoleCounts,
|
||||||
recentUsers,
|
recentUsers,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
export const getUsersDashboardStats = cacheFn(getUsersDashboardStatsRaw, {
|
||||||
|
tags: ["users"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["users", "getUsersDashboardStats"],
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -217,29 +227,34 @@ export type TeacherOption = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the user row if the user has the specified role, otherwise null. */
|
/** Returns the user row if the user has the specified role, otherwise null. */
|
||||||
export const getUserWithRole = cache(
|
export const getUserWithRoleRaw = async (
|
||||||
async (userId: string, roleName: string): Promise<{ id: string; name: string | null; email: string } | null> => {
|
userId: string, roleName: string
|
||||||
const id = userId.trim()
|
): Promise<{ id: string; name: string | null; email: string } | null> => {
|
||||||
if (!id) return null
|
const id = userId.trim()
|
||||||
|
if (!id) return null
|
||||||
|
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select({ id: users.id, name: users.name, email: users.email })
|
.select({ id: users.id, name: users.name, email: users.email })
|
||||||
.from(users)
|
.from(users)
|
||||||
.innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
|
.innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
|
||||||
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
||||||
.where(and(eq(users.id, id), eq(roles.name, roleName)))
|
.where(and(eq(users.id, id), eq(roles.name, roleName)))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
return row ?? null
|
return row ?? null
|
||||||
}
|
}
|
||||||
)
|
export const getUserWithRole = cacheFn(getUserWithRoleRaw, {
|
||||||
|
tags: ["users"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["users", "getUserWithRole"],
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current authenticated student user (id + name) by reading the
|
* Returns the current authenticated student user (id + name) by reading the
|
||||||
* auth context and verifying the "student" role via JOIN users + usersToRoles + roles.
|
* auth context and verifying the "student" role via JOIN users + usersToRoles + roles.
|
||||||
* Returns null if not authenticated or the user does not have the student role.
|
* Returns null if not authenticated or the user does not have the student role.
|
||||||
*/
|
*/
|
||||||
export const getCurrentStudentUser = cache(async (): Promise<{ id: string; name: string; gradeId: string | null } | null> => {
|
export const getCurrentStudentUserRaw = async (): Promise<{ id: string; name: string; gradeId: string | null } | null> => {
|
||||||
let ctx
|
let ctx
|
||||||
try {
|
try {
|
||||||
ctx = await getAuthContext()
|
ctx = await getAuthContext()
|
||||||
@@ -260,44 +275,59 @@ export const getCurrentStudentUser = cache(async (): Promise<{ id: string; name:
|
|||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
return { id: student.id, name: student.name || "Student", gradeId: userRow?.gradeId ?? null }
|
return { id: student.id, name: student.name || "Student", gradeId: userRow?.gradeId ?? null }
|
||||||
|
}
|
||||||
|
export const getCurrentStudentUser = cacheFn(getCurrentStudentUserRaw, {
|
||||||
|
tags: ["users"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["users", "getCurrentStudentUser"],
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Returns a map of userId -> { name, email } for the given user ids. */
|
/** Returns a map of userId -> { name, email } for the given user ids. */
|
||||||
export const getUserNamesByIds = cache(
|
export const getUserNamesByIdsRaw = async (
|
||||||
async (ids: string[]): Promise<Map<string, UserNameOption>> => {
|
ids: string[]
|
||||||
const uniqueIds = Array.from(new Set(ids.filter((v): v is string => typeof v === "string" && v.length > 0)))
|
): Promise<Map<string, UserNameOption>> => {
|
||||||
const result = new Map<string, UserNameOption>()
|
const uniqueIds = Array.from(new Set(ids.filter((v): v is string => typeof v === "string" && v.length > 0)))
|
||||||
if (uniqueIds.length === 0) return result
|
const result = new Map<string, UserNameOption>()
|
||||||
|
if (uniqueIds.length === 0) return result
|
||||||
|
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({ id: users.id, name: users.name, email: users.email })
|
.select({ id: users.id, name: users.name, email: users.email })
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(inArray(users.id, uniqueIds))
|
.where(inArray(users.id, uniqueIds))
|
||||||
|
|
||||||
for (const r of rows) {
|
for (const r of rows) {
|
||||||
result.set(r.id, { id: r.id, name: r.name, email: r.email })
|
result.set(r.id, { id: r.id, name: r.name, email: r.email })
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
)
|
return result
|
||||||
|
}
|
||||||
|
export const getUserNamesByIds = cacheFn(getUserNamesByIdsRaw, {
|
||||||
|
tags: ["users"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["users", "getUserNamesByIds"],
|
||||||
|
})
|
||||||
|
|
||||||
/** Returns teachers (users with the "teacher" role) for the given user ids. */
|
/** Returns teachers (users with the "teacher" role) for the given user ids. */
|
||||||
export const getTeachersByIds = cache(
|
export const getTeachersByIdsRaw = async (
|
||||||
async (teacherIds: string[]): Promise<TeacherOption[]> => {
|
teacherIds: string[]
|
||||||
const uniqueIds = Array.from(new Set(teacherIds.filter((v): v is string => typeof v === "string" && v.length > 0)))
|
): Promise<TeacherOption[]> => {
|
||||||
if (uniqueIds.length === 0) return []
|
const uniqueIds = Array.from(new Set(teacherIds.filter((v): v is string => typeof v === "string" && v.length > 0)))
|
||||||
|
if (uniqueIds.length === 0) return []
|
||||||
|
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({ id: users.id, name: users.name, email: users.email })
|
.select({ id: users.id, name: users.name, email: users.email })
|
||||||
.from(users)
|
.from(users)
|
||||||
.innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
|
.innerJoin(usersToRoles, eq(usersToRoles.userId, users.id))
|
||||||
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
.innerJoin(roles, eq(usersToRoles.roleId, roles.id))
|
||||||
.where(and(eq(roles.name, "teacher"), inArray(users.id, uniqueIds)))
|
.where(and(eq(roles.name, "teacher"), inArray(users.id, uniqueIds)))
|
||||||
.groupBy(users.id, users.name, users.email)
|
.groupBy(users.id, users.name, users.email)
|
||||||
|
|
||||||
return rows.map((r) => ({ id: r.id, name: r.name, email: r.email }))
|
return rows.map((r) => ({ id: r.id, name: r.name, email: r.email }))
|
||||||
}
|
}
|
||||||
)
|
export const getTeachersByIds = cacheFn(getTeachersByIdsRaw, {
|
||||||
|
tags: ["users"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["users", "getTeachersByIds"],
|
||||||
|
})
|
||||||
|
|
||||||
export type UserBasicInfo = {
|
export type UserBasicInfo = {
|
||||||
id: string
|
id: string
|
||||||
@@ -308,46 +338,75 @@ export type UserBasicInfo = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Returns basic user info (id, name, email, image, gradeId) for a single user. */
|
/** Returns basic user info (id, name, email, image, gradeId) for a single user. */
|
||||||
export const getUserBasicInfo = cache(
|
export const getUserBasicInfoRaw = async (
|
||||||
async (userId: string): Promise<UserBasicInfo | null> => {
|
userId: string
|
||||||
const id = userId.trim()
|
): Promise<UserBasicInfo | null> => {
|
||||||
if (!id) return null
|
const id = userId.trim()
|
||||||
|
if (!id) return null
|
||||||
|
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select({
|
.select({
|
||||||
id: users.id,
|
id: users.id,
|
||||||
name: users.name,
|
name: users.name,
|
||||||
email: users.email,
|
email: users.email,
|
||||||
image: users.image,
|
image: users.image,
|
||||||
gradeId: users.gradeId,
|
gradeId: users.gradeId,
|
||||||
})
|
})
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(eq(users.id, id))
|
.where(eq(users.id, id))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
|
||||||
return row ?? null
|
return row ?? null
|
||||||
}
|
}
|
||||||
)
|
export const getUserBasicInfo = cacheFn(getUserBasicInfoRaw, {
|
||||||
|
tags: ["users"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["users", "getUserBasicInfo"],
|
||||||
|
})
|
||||||
|
|
||||||
/** Returns user IDs for all users with the given gradeId. */
|
/** Returns user IDs for all users with the given gradeId. */
|
||||||
export const getUserIdsByGradeId = cache(
|
export const getUserIdsByGradeIdRaw = async (
|
||||||
async (gradeId: string): Promise<string[]> => {
|
gradeId: string
|
||||||
const id = gradeId.trim()
|
): Promise<string[]> => {
|
||||||
if (!id) return []
|
const id = gradeId.trim()
|
||||||
|
if (!id) return []
|
||||||
|
|
||||||
const rows = await db
|
const rows = await db
|
||||||
.select({ id: users.id })
|
.select({ id: users.id })
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(eq(users.gradeId, id))
|
.where(eq(users.gradeId, id))
|
||||||
|
|
||||||
return rows.map((r) => r.id)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/** Returns all user IDs (used for school-wide announcement notifications). */
|
|
||||||
export const getAllUserIds = cache(async (): Promise<string[]> => {
|
|
||||||
const rows = await db.select({ id: users.id }).from(users)
|
|
||||||
return rows.map((r) => r.id)
|
return rows.map((r) => r.id)
|
||||||
|
}
|
||||||
|
export const getUserIdsByGradeId = cacheFn(getUserIdsByGradeIdRaw, {
|
||||||
|
tags: ["users"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["users", "getUserIdsByGradeId"],
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all user IDs (used for school-wide announcement notifications).
|
||||||
|
*
|
||||||
|
* P3-7: 加默认 LIMIT 1000 防止超大学校全量用户导致 OOM;
|
||||||
|
* 调用方可通过 limit/offset 参数实现分页遍历(用于公告 fan-out 批量化)。
|
||||||
|
*
|
||||||
|
* @param limit 返回条数上限,默认 1000
|
||||||
|
* @param offset 偏移量,默认 0
|
||||||
|
*/
|
||||||
|
export const getAllUserIdsRaw = async (
|
||||||
|
limit: number = 1000, offset: number = 0
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const rows = await db
|
||||||
|
.select({ id: users.id })
|
||||||
|
.from(users)
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
return rows.map((r) => r.id)
|
||||||
|
}
|
||||||
|
export const getAllUserIds = cacheFn(getAllUserIdsRaw, {
|
||||||
|
tags: ["users"],
|
||||||
|
ttl: 300,
|
||||||
|
keyParts: ["users", "getAllUserIds"],
|
||||||
})
|
})
|
||||||
|
|
||||||
export type AdminUserListItem = {
|
export type AdminUserListItem = {
|
||||||
|
|||||||
Reference in New Issue
Block a user