Selaa lähdekoodia

fix(academic): D11 新建课程 status 落 1——去掉 model tag 的 default:1(DDL 不动)

Q 10 tuntia sitten
vanhempi
sitoutus
612bf9b009

+ 11 - 15
internal/academic/handler/course_handler_test.go

@@ -392,8 +392,7 @@ func TestCreateCourseIsUnpublished(t *testing.T) {
 	if err := db.First(&row, int64(id)).Error; err != nil {
 		t.Fatalf("回读课程失败: %v", err)
 	}
-	// ⚠ status 断言拆到 TestCreateCourseStatusIsUnpublished_AC11Defect:
-	// 本批实测发现 AC-11 的 `CreateCourse` 落库后 status = 1(而非 0),成因与处置见该用例注释。
+	// status 断言拆到 TestCreateCourseStatusIsUnpublished(D11 的回归门禁,含缺陷成因与修法)。
 	if row.InstitutionID != 1 {
 		t.Fatalf("institution_id = %d,期望 1(取作用域,不信 body 的 99)", row.InstitutionID)
 	}
@@ -412,22 +411,19 @@ func TestCreateCourseIsUnpublished(t *testing.T) {
 	}
 }
 
-// TestCreateCourseStatusIsUnpublished_AC11Defect **复现 AC-11 缺陷(本批只报不修,待裁决)**。
+// TestCreateCourseStatusIsUnpublished **新建课程必须落 status=0(未发布)**——D11 的回归门禁。
 //
-// 现象(实测,非推测):`CourseService.CreateCourse` 里显式写了
-// `Status: model.CoursesStatusUnpublished`(0),但 `model.Courses.Status` 的 GORM tag 是
-// `not null;default:1` —— GORM 对"带 default tag 且值为零值"的字段**不把该列写进 INSERT**,
-// 改由数据库默认值填充 ⇒ 新建课程实际落 **status=1(发布未开始)**。
+// 缺陷原貌(2026-11-09 实测):`CourseService.CreateCourse` 显式写 `Status=0`,但
+// `model.Courses.Status` 的 gorm tag 带 `default:1` ⇒ GORM 对"有 default tag 且值为零"的列
+// **跳过该列**、由 DB 默认值填充 ⇒ 落库 `status=1`("新建即发布":课程直接出现在可报名列表)。
+// MySQL 与 SQLite 走同一段 GORM 回调,生产同样成立。
 //
-// 为什么不是测试环境假象:MySQL 与 SQLite 走的是同一段 GORM 回调(`Create` 前对 default 列
-// 置 `DefaultValueInterface`),因此生产同样会落 1。后果是"新建即发布"——课程直接出现在
-// 前台可报名列表里,与 `CreateCourse` 注释"新建恒落未发布、发布走状态机"直接矛盾。
+// 修复(team-lead 裁决):**去掉 `model.Courses.Status` 的 `default:1`(DDL 不动)**,
+// 而不是 `tx.Select("*").Create(row)` —— 后者会把 `created_at`/`updated_at` 等零值一并写入,
+// 正是 `a1971b7`(审计时间零值落库)修过的反向坑。
 //
-// 为什么不本批直接修:修复点落在 **AC-11 的 `repository.CreateCourse`**(`tx.Create(row)`
-// 需改为 `tx.Select("*").Create(row)` 或显式列出含 `status` 的列),属"改 AC-11 既有实现",
-// 按本批纪律**先报不改**。修复落地后,删掉下面这行 `t.Skip` 即可作为回归门禁。
-func TestCreateCourseStatusIsUnpublished_AC11Defect(t *testing.T) {
-	t.Skip("AC-11 缺陷待裁决:新建课程 status 被 GORM default tag 吞掉,实际落 1(发布未开始)")
+// 本用例是**回归门禁**:把 `default:1` 加回 tag,本用例必红(实测见交付报告)。
+func TestCreateCourseStatusIsUnpublished(t *testing.T) {
 	db := newCourseTestDB(t)
 	app := newCourseTestApp(t, db)
 	code, msg, data := doJSON(t, app, fiber.MethodPost, PathCourses, map[string]any{

+ 5 - 1
internal/academic/model/models_course.go

@@ -86,7 +86,11 @@ type Courses struct {
 	InstitutionID int64      `gorm:"index:idx_inst_campus_status,priority:1;column:institution_id;not null;default:0" json:"institutionId"` // 机构 ID(原 schoolid;作用域列)|枚举:无|写入:创建时锁定|FK:→ t_institution|红线:scoping 强依赖
 	CampusID      int64      `gorm:"index:idx_inst_campus_status,priority:2;column:campus_id;not null;default:0" json:"campusId"`           // 校区 ID(原 campusid;作用域列)|枚举:无|写入:创建/回填(campus_id=0 的多校区课程不更新)|FK:→ t_campus|红线:backfill 仅唯一校区时回填(设计 §3.5 #29)
 	ClassifyID    int64      `gorm:"index:idx_classify;column:classify_id;not null;default:0" json:"classifyId"`                            // 课程分类 ID(原 classifyid)|枚举:无|写入:创建/编辑|FK:→ t_courses_classify.id|红线:回填 max_people 依赖本列
-	Status        int32      `gorm:"index:idx_inst_campus_status,priority:3;column:status;not null;default:1" json:"status"`                // 状态(0 未发布/1 发布未开始/2 进行中/3 已结束)|枚举:CoursesStatus*|写入:发布/结课任务|FK:无|红线:状态机单向推进
+	// ⚠ 本列**故意不写 `default:1`**:GORM 对"有 default tag 且字段为零值"的列会**跳过该列**、改由 DB 默认值填充,
+	// 而 0(未发布)恰恰是本列最常用的零值 ⇒ 新建课程会被静默写成 status=1("新建即发布")。
+	// 去掉 tag 后 GORM 总是显式写 status,0 能落库;DDL 的 `DEFAULT 1` 保持不变(只影响裸 SQL 插入)。
+	// 同类坑本项目已踩两次(另一次是 `create_time`):凡有 `default:` 又可能被赋零值的列,先问"零值能落库吗"。
+	Status        int32      `gorm:"index:idx_inst_campus_status,priority:3;column:status;not null" json:"status"`                // 状态(0 未发布/1 发布未开始/2 进行中/3 已结束)|枚举:CoursesStatus*|写入:发布/结课任务|FK:无|红线:状态机单向推进
 	Name          string     `gorm:"column:name;size:500;not null" json:"name"`                                                             // 课程名称|枚举:无|写入:创建/编辑|FK:无|红线:用户输入,展示转义
 	Number        int32      `gorm:"column:number;not null;default:0" json:"number"`                                                        // 上课次数(课次)|枚举:无|写入:创建/编辑|FK:无|红线:与 record 生成条数关联,改动须校验已开课
 	Duration      string     `gorm:"column:duration;type:decimal(5,1);not null;default:0.0" json:"duration"`                                // 单节课时长(课时,1 位小数,保留旧精度)|枚举:无|写入:创建/编辑|FK:无|红线:精度例外列(非 12,2)