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
89 lines
2.8 KiB
TypeScript
89 lines
2.8 KiB
TypeScript
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 个 SQL(pendingLottery 需 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,
|
||
}
|
||
}
|
||
)
|