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
This commit is contained in:
88
src/modules/elective/data-access-stats.ts
Normal file
88
src/modules/elective/data-access-stats.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user