Files
NextEdu/src/modules/elective/data-access-stats.ts
SpecialX 138b6f1b00 feat(dashboard,diagnostic,elective): add widgets, layout, parent dashboard, role-config, services, elective components
dashboard:

- Add comparison-badge, dashboard-notification-widget, dashboard-responsive-layout, dashboard-time-range-filter

- Add parent-dashboard components directory

- Add config, hooks, and services directories

diagnostic:

- Add role-config and services directory

elective:

- Add elective-course-detail, elective-stats-cards, parent-selection-view components

- Add data-access-settings and data-access-stats
2026-07-03 10:25:46 +08:00

89 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import "server-only"
import { cache } from "react"
import { count, eq, sql } from "drizzle-orm"
import { db } from "@/shared/db"
import { courseSelections, electiveCourses } from "@/shared/db/schema"
/**
* 选课模块管理员概览统计P1-13 新增)。
* 用于 admin/teacher 列表页顶部展示关键指标。
*/
export interface ElectiveOverviewStats {
/** 课程总数 */
totalCourses: number
/** 总选课人数(已录取 + 候补 + 已选) */
totalEnrolled: number
/** 平均容量使用率百分比0-100 */
avgUtilization: number
/** 待抽签的课程数selectionMode=lottery 且 status=open 且存在 selected 记录) */
pendingLottery: number
}
/**
* 获取选修课全局概览统计admin 视角)。
*
* 实现要点:
* - 4 个独立查询合并为 3 个 SQLpendingLottery 需 join避免 N+1
* - 使用 SQL 聚合而非拉全表后 reduce避免大数据量内存峰值
* - admin 不做 scope 过滤(统计全部课程)
*/
export const getElectiveOverviewStats = cache(
async (): Promise<ElectiveOverviewStats> => {
// 并行执行聚合查询
const [totalRow, enrolledRow, utilizationRow, pendingRow] = await Promise.all([
// 1. 课程总数
db
.select({ total: count() })
.from(electiveCourses),
// 2. 总选课人数(活跃选课记录数)
db
.select({ total: count() })
.from(courseSelections)
.where(
sql`${courseSelections.status} IN ('selected', 'enrolled', 'waitlist')`
),
// 3. 平均容量使用率capacity > 0 时计算 enrolledCount/capacity 平均值)
db
.select({
avg: sql<number>`COALESCE(
AVG(
CASE
WHEN ${electiveCourses.capacity} > 0
THEN ${electiveCourses.enrolledCount}::float / ${electiveCourses.capacity}
ELSE 0
END
) * 100,
0
)`,
})
.from(electiveCourses),
// 4. 待抽签课程数lottery 模式且 status=open 且有 selected 状态的选课记录
db
.select({ total: sql<number>`count(distinct ${electiveCourses.id})` })
.from(electiveCourses)
.innerJoin(
courseSelections,
eq(courseSelections.courseId, electiveCourses.id)
)
.where(
sql`${electiveCourses.selectionMode} = 'lottery'
AND ${electiveCourses.status} = 'open'
AND ${courseSelections.status} = 'selected'`
),
])
return {
totalCourses: totalRow[0]?.total ?? 0,
totalEnrolled: enrolledRow[0]?.total ?? 0,
avgUtilization: Math.round(Number(utilizationRow[0]?.avg ?? 0)),
pendingLottery: pendingRow[0]?.total ?? 0,
}
}
)