|
|
@@ -1073,6 +1073,99 @@ func TestStudentScheduleIsPerStudent(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// ---------------------------------------------------------------- 端点 51:课酬分摊口径(D16)
|
|
|
+
|
|
|
+// TestFinishRecordRewardDistributionSkipsGiveRows **D16:课酬只分给 `type∈{1,2}`,赠送行恒 0**。
|
|
|
+//
|
|
|
+// PHP 原文(`CoursesRecordHelper.php:806-818`):`$stuCount = count(type IN (1,2))`,
|
|
|
+// 平摊与写回**都只针对 `type IN (1,2)`**,赠送行**永远保持 0**。
|
|
|
+//
|
|
|
+// 夹具:1 条 type=1(正常)+ 1 条 type=3(赠送),单节课课酬合计 = 100.00。
|
|
|
+//
|
|
|
+// 正确(PHP):正常行 **100.00**、赠送行 **0.00**、Σ(type∈{1,2}) == 100.00
|
|
|
+// 错误(除数用全部行数):正常行 50.00、赠送行 50.00 ⇒ **多条断言同时红**
|
|
|
+func TestFinishRecordRewardDistributionSkipsGiveRows(t *testing.T) {
|
|
|
+ db := newCourseTestDB(t)
|
|
|
+ fx := seedFinishFixture(t, db)
|
|
|
+ const giveStudent = "01HSTUDENT0000000000000G"
|
|
|
+ app := newCourseTestApp(t, db)
|
|
|
+ code, msg, data := doJSON(t, app, fiber.MethodPost,
|
|
|
+ "/admin/courses/records/"+itoa(fx.Record.ID)+"/finish",
|
|
|
+ map[string]any{"items": []map[string]any{
|
|
|
+ {"userId": courseTestStudent, "type": 1, "cost": "100.00"},
|
|
|
+ {"userId": giveStudent, "type": 3, "cost": "0.00"},
|
|
|
+ }}, "X-Test-Token", "t")
|
|
|
+ if code != 200 {
|
|
|
+ t.Fatalf("结束上课(含赠送生)= %d(%s),期望 200", code, msg)
|
|
|
+ }
|
|
|
+ payroll, _ := jsonMap(t, data)["payroll"].(map[string]any)
|
|
|
+ if got, _ := payroll["reward"].(string); got != "100.00" {
|
|
|
+ t.Fatalf("payroll.reward = %v,期望 100.00(单节课课酬合计,与分摊无关)", payroll["reward"])
|
|
|
+ }
|
|
|
+ requireMoney(t, "正常行 teacher_reward", readTeacherReward(t, db, fx.Record.ID, courseTestStudent), "100.00")
|
|
|
+ requireMoney(t, "赠送行 teacher_reward", readTeacherReward(t, db, fx.Record.ID, giveStudent), "0.00")
|
|
|
+ requireMoney(t, "Σ(type∈{1,2})", readTeacherRewardSumExcludingGive(t, db, fx.Record.ID), "100.00")
|
|
|
+ // student_number 是**全部行数**(含赠送),与平摊人数是两个数
|
|
|
+ var studentNumber int32
|
|
|
+ db.Raw("SELECT student_number FROM t_courses_record WHERE id = ?", fx.Record.ID).Scan(&studentNumber)
|
|
|
+ if studentNumber != 2 {
|
|
|
+ t.Fatalf("student_number = %d,期望 2(全部打卡行数,含赠送生)", studentNumber)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TestFinishRecordAllGiveClassKeepsRecordReward **D16 边界:全班赠送(stuCount=0)**。
|
|
|
+//
|
|
|
+// PHP 的 `if($stuCount)` 不进入 ⇒ **一行都不写**(学生行全 0),但 record 上**仍写课酬**。
|
|
|
+// ⇒ 恒等式在这一情形**不成立**;断言只能是"所有行 0",**不得**断言 Σ == record
|
|
|
+// (无条件断言会让"全赠送班"恒红,最后被人改成跳过 = 把防线删掉)。
|
|
|
+func TestFinishRecordAllGiveClassKeepsRecordReward(t *testing.T) {
|
|
|
+ db := newCourseTestDB(t)
|
|
|
+ fx := seedFinishFixture(t, db)
|
|
|
+ app := newCourseTestApp(t, db)
|
|
|
+ code, msg, data := doJSON(t, app, fiber.MethodPost,
|
|
|
+ "/admin/courses/records/"+itoa(fx.Record.ID)+"/finish",
|
|
|
+ map[string]any{"items": []map[string]any{
|
|
|
+ {"userId": courseTestStudent, "type": 3, "cost": "0.00"},
|
|
|
+ }}, "X-Test-Token", "t")
|
|
|
+ if code != 200 {
|
|
|
+ t.Fatalf("结束上课(全班赠送)= %d(%s),期望 200", code, msg)
|
|
|
+ }
|
|
|
+ requireMoney(t, "赠送行 teacher_reward", readTeacherReward(t, db, fx.Record.ID, courseTestStudent), "0.00")
|
|
|
+ payroll, _ := jsonMap(t, data)["payroll"].(map[string]any)
|
|
|
+ if got, _ := payroll["reward"].(string); got != "100.00" {
|
|
|
+ t.Fatalf("payroll.reward = %v,期望 100.00(全班赠送照常算课酬,只是不分摊)", payroll["reward"])
|
|
|
+ }
|
|
|
+ var recReward string
|
|
|
+ db.Raw("SELECT teacher_reward FROM t_courses_record WHERE id = ?", fx.Record.ID).Scan(&recReward)
|
|
|
+ requireMoney(t, "record.teacher_reward", recReward, "100.00")
|
|
|
+}
|
|
|
+
|
|
|
+// readTeacherReward 读某学生在该课的 `teacher_reward`(无行 → 空串,断言会红)。
|
|
|
+func readTeacherReward(t *testing.T, db *gorm.DB, recordID int64, studentID string) string {
|
|
|
+ t.Helper()
|
|
|
+ var out struct {
|
|
|
+ Reward string `gorm:"column:teacher_reward"`
|
|
|
+ }
|
|
|
+ if err := db.Raw("SELECT teacher_reward FROM t_courses_record_student WHERE record_id = ? AND student_id = ?",
|
|
|
+ recordID, studentID).Scan(&out).Error; err != nil {
|
|
|
+ t.Fatalf("读 teacher_reward 失败: %v", err)
|
|
|
+ }
|
|
|
+ return out.Reward
|
|
|
+}
|
|
|
+
|
|
|
+// readTeacherRewardSumExcludingGive 求和非赠送行的 `teacher_reward`(D16 条件式恒等式)。
|
|
|
+func readTeacherRewardSumExcludingGive(t *testing.T, db *gorm.DB, recordID int64) string {
|
|
|
+ t.Helper()
|
|
|
+ var out struct {
|
|
|
+ Sum string `gorm:"column:sum_reward"`
|
|
|
+ }
|
|
|
+ if err := db.Raw("SELECT COALESCE(SUM(teacher_reward),0) AS sum_reward FROM t_courses_record_student "+
|
|
|
+ "WHERE record_id = ? AND type IN (1,2)", recordID).Scan(&out).Error; err != nil {
|
|
|
+ t.Fatalf("求和非赠送行 teacher_reward 失败: %v", err)
|
|
|
+ }
|
|
|
+ return out.Sum
|
|
|
+}
|
|
|
+
|
|
|
// ---------------------------------------------------------------- 端点 51:结束上课(跨 5 表)
|
|
|
|
|
|
// courseFinishFixture 端点 51 的完整夹具(结课链路的五张表全部就位)。
|