跳转到内容

学期计算逻辑说明 ​

核心概念 ​

全局学期配置 ​

  • 存储格式: YYYY-YYYY-N (起始学年-结束学年-学期序号)
  • 示例: 2025-2026-2 表示 2025-2026学年的第2学期
  • 语义:
    • 学期序号1 = 秋季学期(9月-次年1月)
    • 学期序号2 = 春季学期(3月-7月)

班级相对学期 ​

每个班级根据入学年份不同,在全局同一时间点处于不同的相对学期。

计算公式:

javascript
// 1. 计算年级
grade = startYear - enrollmentYear + 1;

// 2. 计算该班级的相对学期序号
currentSemesterNum = (grade - 1) * 2 + semesterIndex;

实际计算示例 ​

全局配置: 2025-2026-2 (2026年春季)

  • startYear = 2025
  • semesterIndex = 2
入学年份年级计算学期计算结果说明
2025年9月2025-2025+1 = 1(1-1)×2 + 2 = 21年级第2学期2025秋→2026春
2024年9月2025-2024+1 = 2(2-1)×2 + 2 = 42年级第4学期2024秋→2025春→2025秋→2026春
2023年9月2025-2023+1 = 3(3-1)×2 + 2 = 63年级第6学期已上6个学期

时间轴示意 ​

2023级: [1]2023秋 [2]2024春 [3]2024秋 [4]2025春 [5]2025秋 [6]2026春 ← 当前
2024级:             [1]2024秋 [2]2025春 [3]2025秋 [4]2026春 ← 当前
2025级:                       [1]2025秋 [2]2026春 ← 当前
                                 ↑
                          全局配置指向这里
                        2025-2026学年 第2学期

代码位置 ​

后端计算 ​

  • 文件: server/src/services/semester.service.js(第 86-95 行)
  • 函数: calcClassSemester(cls, semesterInfo)
  • 用途: 开课查询、教材查询、数据导出
  • 说明: 所有学期逻辑(calcClassSemester / parseSemester / formatSemesterLabel)的真正实现均收敛于此文件;server/src/routes/query.routes.js 中无任何对 calcClassSemester 或 parseSemester 的引用

前端计算 ​

  • 文件: client/src/views/class/components/ClassTable.vue
  • 函数: calcGrade(row)
  • 用途: 班级列表显示年级
  • 逻辑: 直接使用后端计算的 grade 字段,前端不再重复实现公式(FR3 修复,消除重复公式和硬编码边界月):
    javascript
    // 年级由后端列表接口统一计算(含学制上限/未入学过滤),无值时显示“-”
    function calcGrade(row) {
      return row.grade ?? null;
    }

本地回退学期计算(1 月归属规则) ​

  • 文件: client/src/composables/useSemesters.js
  • 函数: getCurrentSemester(semesterStartMonth = 8)
  • 用途: 后端系统设置不可用时,按本地日期推算当前学期
  • 归属规则:
    • month >= semesterStartMonth(默认 8 月):当年秋季第 1 学期 YYYY-(YYYY+1)-1
    • month === 1:仍处秋季学期末(期末考试周),归上一学年第 1 学期 (YYYY-1)-YYYY-1,对齐本文档“秋季 = 9 月-次年 1 月”口径
    • 其余月份(2–7 月):上一学年春季第 2 学期 (YYYY-1)-YYYY-2

格式化显示 ​

  • 后端: server/src/services/semester.service.js - formatSemesterLabel()(第 68-72 行)
  • 前端: client/src/stores/settings.js - formatSemesterLabel()
  • 输出: 2026年春季(第2学期)
  • 说明: server/src/services/settings.service.js 仅 re-export,真正实现位于 semester.service.js

学期格式校验规则 ​

后端 parseSemester / parseSemesterString 函数对学期字符串执行严格校验,不合法格式返回 null。

javascript
// server/src/services/semester.service.js — parseSemester(真正实现,第 28-59 行)
// server/src/services/arrange/queries.js — 仅 re-export parseSemester
// server/src/services/settings.service.js — 仅保留 @deprecated 的 parseSemesterString 兼容包装

校验规则:

规则合法示例非法示例说明
格式 YYYY-YYYY-N2025-2026-125-26-1, 2025-2必须为完整的四位数年份
学期序号 ∈2025-2026-1, 2025-2026-22025-2026-3仅支持秋季(1)和春季(2)
endYear = startYear + 12025-2026-2 ✅2025-2027-1 ✗H2 修复:强制年份连续性
endYear > startYear2025-2026-1 ✅2026-2025-1 ✗由上一条规则自然约束

H2 修复说明:修复前 parseSemester 不校验年份逻辑关系,2026-2025-1 等非法格式可被解析。修复后与 parseSemesterString 保持一致,强制 endYear === startYear + 1。所有调用方已处理 null 返回值。

有效性校验 ​

年级显示规则 ​

javascript
// 前端年级显示(ClassTable.vue)
const grade = startYear - enrollmentYear + 1;
const durationYears = row.durationYears || row.duration_years || 99;

// 年级必须在有效范围内才显示:
// 1. 年级 >= 1(已入学)
// 2. 年级 <= 学制年限(未毕业)
if (grade >= 1 && grade <= durationYears) {
  return grade;
}

// 超出学制范围(毕业)或未入学,显示为空
return null;

重要变更历史:

v1.3.7 错误理解(已回退):

  • ❌ 错误逻辑:grade >= 1 即可,毕业班级也显示年级数字
  • ❌ 问题:2023年入学3年制在2027-2028学年显示"5年级"(错误)

v1.3.8 正确逻辑(当前版本):

  • ✅ 正确逻辑:grade >= 1 && grade <= durationYears
  • ✅ 设计理念:年级数字表示"当前在读第几年",毕业班级不再属于任何年级
  • ✅ 毕业由同一状态机推导为 graduated(见下节),不是独立录入的字段

示例 (系统设置 2027-2028-1, startYear=2027):

入学年份年级计算学制结果状态年级显示说明
2027年2027-2027+1=13年1年级在读1年级 ✅新生
2026年2027-2026+1=23年2年级在读2年级 ✅正常
2025年2027-2025+1=33年3年级在读3年级 ✅最后一年
2024年2027-2024+1=43年4年级毕业空 ✅毕业1年
2023年2027-2023+1=53年5年级毕业空 ✅毕业2年
2028年2027-2028+1=03年0年级未入学空 ✅未入学

班级状态判断规则 ​

状态是推导值,不是录入值(classes.status 列仅供人工查库参考,全站读取处一律现算)。判定顺序:

javascript
// server/src/services/semester.service.js#resolveClassStatus
if (cls.is_left_school) return "left_school"; // 手动标记,优先级最高
const grade = startYear - cls.enrollment_year + 1; // 无法解析 → unknown
if (grade < 1) return "not_enrolled";
if (grade > cls.duration_years) return "graduated";
return "active";
code文案条件
active在读1 <= grade <= 学制 且未标记离校
graduated毕业grade > 学制
left_school离校is_left_school = true(优先级最高)
not_enrolled未入学grade < 1(入学年份晚于判定学年)
unknown未知入学年份或学制缺失/非正数,无法推出年级

五个取值互斥且覆盖全表:按状态筛选的分桶数之和恒等于「全部」。

班级状态计算规则 ​

核心逻辑 ​

班级状态基于入学年份、学制和判定学期自动计算,全站只有一个实现:

  • resolveClassStatus(cls, semesterInfo) → 状态码(内存推导:列表标签、创建/更新写入 classes.status)
  • getClassStatusText(cls, semesterInfo) → 中文文案(班级导出、教师课时归档)
  • parseClassStatusText(text) → 状态码(Excel 导入侧的逆映射,兼容历史「已毕业」)
  • buildClassStatusWhere(code, semesterInfo) → Prisma WHERE(状态筛选,与上面同口径)

判定学期缺省规则:resolveSemesterForStatus() 依次取「调用方传入的学期 → 系统设置的当前学期 → 按系统日期与开学月份(semester_start_month)推导」。因此未配置当前学期时,列表标签、状态筛选、 班级导出仍按同一学年起点判定,不会出现「筛选不过滤、标签另算一套」的分裂。

关键规则:

  • 学生于入学年份的9月入学(秋季学期)
  • 每个学年包含2个学期:秋季(学期序号1)、春季(学期序号2)
  • 在读学期总数 = 学制年数 × 2
  • 在读条件: 当前年级 1..学制
  • 毕业条件: 当前年级 > 学制

实际计算示例 ​

全局配置: 2025-2026-2 (2025-2026学年第2学期,即2026年春季)

  • startYear = 2025
  • semesterIndex = 2
入学年份年级计算年级结果学制状态判断结果说明
2025年9月2025-2025+11年级3年1 <= 3在读第2学期
2024年9月2025-2024+12年级3年2 <= 3在读第4学期
2023年9月2025-2023+13年级3年3 <= 3在读第6学期(最后学期)
2022年9月2025-2022+14年级3年4 > 3毕业应处于第7学期

重要说明 ​

2023级案例分析 (3年制,当前为2025-2026学年第2学期):

  • 当前年级: 3年级
  • 当前学期: 第6学期 (3-1)×2 + 2 = 6
  • 总学期数: 3 × 2 = 6 学期
  • 状态判断: 3 <= 3 → 在读 ✓

虽然2023级已经上到第6学期(最后一个学期),但仍然是"在读"状态,因为:

  1. 他们还没有完成第6学期的学习
  2. 只有到了下一学期(2026-2027学年第1学期,即第7学期)才会变为"毕业"

这种设计确保了:

  • 学生在整个最后一学年(包括两个学期)都保持"在读"状态
  • 毕业后自动切换为"毕业"状态,无需手动操作

代码实现位置 ​

唯一实现: server/src/services/semester.service.js

  • resolveClassStatus / getClassStatusText / parseClassStatusText / buildClassStatusWhere
  • 年级公式只存在于内部 calcClassGrade,calcClassSemester(学期序号)与状态推导共用它

消费方:

  • class.controller.js:列表标签与 classes.status 写入、批量更新取值校验
  • class-filter.service.js:status 查询参数 → Prisma WHERE
  • controllers/export/data-export.controller.js:班级导出「状态」列
  • controllers/import/classes.js:Excel「状态」列解析
  • controllers/teaching-arrange.controller.js:教师课时归档的班级状态

培养方案课程匹配 ​

通过计算出的 currentSemesterNum 筛选该学期应开设的课程:

javascript
const planCourses = plan.planCourses.filter(
  (pc) =>
    pc.startSemester <= calc.currentSemesterNum &&
    pc.endSemester >= calc.currentSemesterNum,
);

示例 (某课程设置为第3-4学期):

  • 2025级(第2学期): 不匹配 ✗
  • 2024级(第4学期): 匹配 ✓
  • 2023级(第6学期): 不匹配 ✗

常见误区 ​

误区1: "所有班级都是第2学期" ​

错误理解: 看到配置中的 -2 就认为所有班级都是第2学期

正确理解:

  • -2 表示"学年的第2学期"(春季)
  • 每个班级的相对学期序号 = (年级-1)×2 + 2
  • 不同年级的班级处于不同的相对学期

误区2: "学期序号应该从1连续递增" ​

错误理解: 希望学期序号是 1,2,3,4... 连续递增

正确理解:

  • 学期序号是相对概念,表示"这是该班级的第几个学期"
  • 1年级对应学期 1-2
  • 2年级对应学期 3-4
  • 3年级对应学期 5-6

修改记录 ​

  • 审计修复批次:

    • 更新“前端计算”一节:calcGrade 已改为直接使用后端 grade 字段(FR3 修复)
    • 新增“本地回退学期计算”一节:useSemesters.js 的 1 月归上一学年第 1 学期规则
  • 2026-06-28 (H2 修复):

    • 新增“学期格式校验规则”章节,文档化 parseSemester 年份连续性校验
    • endYear !== startYear + 1 时返回 null,与 parseSemesterString 行为统一
  • 2026-06-15 (v1.3.8):

    • 回退 v1.3.7 的错误逻辑,恢复年级显示的学制上限检查
    • 修正年级计算:毕业班级(超出学制)年级显示为空
    • 2023年入学3年制在2027-2028学年应显示为空(而非5年级)
    • 年级显示条件:grade >= 1 && grade <= durationYears
    • 更新文档示例,使用系统设置 2027-2028-1 作为场景
  • 2026-06-15 (v1.3.7):

    • 修正年级计算逻辑,使用后端提供的学期信息(startYear)而非当前年份
    • 移除年级显示的上限限制,毕业班级也显示年级数字(❌ 此逻辑错误,已在 v1.3.8 回退)
    • 修正文档中的代码位置引用(ClassTable.vue, class.controller.js)
  • 2026-06-08: 优化学期显示格式为 "2026年春季(第2学期)"

  • 2026-06-08: 添加计算逻辑注释和文档说明