ソースを参照

fix(academic): 端点51 divide[] 按裁决改为 {userId,type,amount}(删 level/quota)

Q 11 時間 前
親
コミット
9ed6936d41

+ 1 - 1
internal/academic/handler/course_handler.go

@@ -944,7 +944,7 @@ func toFinishRecordResp(view *service.RecordFinishView) finishRecordResp {
 		d := &view.Divide[i]
 		divide = append(divide, divideGrantView{
 			UserID: d.UserID,
-			Quota:  d.Quota.String(),
+			Type:   d.Type,
 			Amount: d.Amount.String(),
 		})
 	}

+ 12 - 1
internal/academic/handler/course_handler_test.go

@@ -1094,11 +1094,22 @@ func TestFinishRecordCrossTableLinkage(t *testing.T) {
 	}
 	d0, _ := divide[0].(map[string]any)
 	if got, _ := d0["amount"].(string); got != "20.00" {
-		t.Fatalf("divide[0].amount = %v,期望 \"20.00\"(上级 quota)", d0["amount"])
+		t.Fatalf("divide[0].amount = %v,期望 \"20.00\"(源列 money)", d0["amount"])
 	}
 	if got, _ := d0["userId"].(string); got != fx.Upline {
 		t.Fatalf("divide[0].userId = %v,期望 %s", d0["userId"], fx.Upline)
 	}
+	// 裁决后形状 `{userId, type, amount}`:type 必须来自日志行的 `type`(夹具用 type=2 按课次)
+	if got, _ := d0["type"].(float64); got != float64(model.DivideTypePerLesson) {
+		t.Fatalf("divide[0].type = %v,期望 %d(源列 type = 按课次)", d0["type"], model.DivideTypePerLesson)
+	}
+	// 防回潮:`level`/`quota` 无对应列,**任何形式的下发都不允许**(包括"填常量让它看起来有值")
+	for _, forbidden := range []string{"level", "quota"} {
+		if v, has := d0[forbidden]; has {
+			t.Fatalf("divide[0] 下发了 %q=%v:该字段在 t_user_divide_into_log 里没有对应列,"+
+				"填常量顶替属伪造历史(团队裁决已删)", forbidden, v)
+		}
+	}
 
 	// ① 打卡行三列
 	var att struct {

+ 16 - 5
internal/academic/handler/dto_course.go

@@ -360,13 +360,24 @@ type attendanceResp struct {
 	Checked int32 `json:"checked"`
 }
 
-// divideGrantView 端点 51 `divide[]` 的元素(契约 §3.4 C 行 51 去掉无列字段 `level`)。
+// divideGrantView 端点 51 `divide[]` 的元素 —— **团队裁决后的最终形状 `[{userId, type, amount}]`**。
+//
+// 与契约 v1.6 mock 的差异(**已批准,待契约升版,前端清单由 team-lead 统一出**):
+//
+//	删 `level` —— `t_user_divide_into_log` 无层级列;`t_user_divide_into.layer` 是当前层级,
+//	              不是入账当时的层级,用当前值顶替 = 伪造历史。
+//	删 `quota` —— 节点 quota 事后可被 `update_quota` 改,日志未留"当时额度",同理不得顶替。
+//	增 `type`  —— 源列 `type`(1 一次付清 / 2 按课次),语义清晰且**确实存在于日志行上**。
+//	增 `userId`— 源列 `user_id`(谁分了这笔钱),是数组最关键的维度。
+//	留 `amount`— 源列 `money`。
+//
+// ⚠ 不得为了迁就旧 mock 给 `level` 填常量(有值、恒为某常数、永远查不出是假的)。
 type divideGrantView struct {
-	// UserID 获得分成的用户(源列 `t_user_divide_into_log.user_id`;契约未列,用于区分元素)。
+	// UserID 获得分成的用户(源列 `t_user_divide_into_log.user_id`)。
 	UserID string `json:"userId"`
-	// Quota 本次分成额度(源列 `t_user_divide_into_log.money`,入账当时即节点 quota)。
-	Quota string `json:"quota"`
-	// Amount 本次入账金额(同上列)。
+	// Type 分成类型(源列 `type`:1 一次付清 / 2 按课次)。
+	Type int32 `json:"type"`
+	// Amount 本次入账金额(源列 `money`)。
 	Amount string `json:"amount"`
 }
 

+ 25 - 16
internal/academic/service/course_query_service.go

@@ -842,26 +842,35 @@ func (s *CourseService) ListScheduleRows(ctx context.Context, q ScheduleQuery) (
 // 端点 51:结课响应装配(只读,不进结账事务)
 // ---------------------------------------------------------------------------
 
-// DivideGrantRow 单条分账入账明细(端点 51 的 `divide[]`)。
+// DivideGrantRow 单条分账入账明细(端点 51 的 `divide[]`)——**team-lead 裁决后的最终形状**
+// `[{userId, type, amount}]`。
 //
-// 字段来源:
+// 字段来源(**逐字段列溯源**,契约 §3.4 ③):
+//
+//	UserID ← t_user_divide_into_log.user_id(这笔上课的钱分给了谁)
+//	Type   ← t_user_divide_into_log.type(分成类型 1 一次付清 / 2 按课次)
+//	Amount ← t_user_divide_into_log.money(本次入账金额)
+//
+// ⚠ **契约原 mock 的 `level` 与 `quota` 均不下发(团队裁决,2026-11-09)**:
+//
+//	`level` —— `t_user_divide_into_log` **没有层级列**;`t_user_divide_into.layer` 是**当前**层级,
+//	          不是"入账当时"的层级,拿当前值顶替历史值 = 「事后调整树结构 ⇒ 历史记录显示错」,
+//	          属静默错,比字段缺失更难查。
+//	`quota` —— 同上:节点 quota 事后可被 `update_quota` 改,日志里没留"当时额度";
+//	          用当前值顶替即是伪造历史。
 //
-//	UserID ← t_user_divide_into_log.user_id(获得分成的用户)
-//	Quota  ← t_user_divide_into_log.money(入账金额;**该值就是入账当时的节点 quota**,
-//	         见 model.UserDivideIntoLog.Money 注释"写入:上行分成时取节点 quota")
-//	Amount ← t_user_divide_into_log.money(同上)
+// 为什么**不是**把整个数组删掉:它承载"这笔钱分给了谁、各多少"这个核心信息;
+// `level`/`quota` 只是附加维度,缺了不影响主信息。因此改为**补 `type`(有列)**、删两个无列字段。
+// 若产品确需"当时层级/当时额度":需在日志表加列并回填 = 迁移变更,另立需求(本批不做)。
 //
-// ⚠ **契约的 `divide[].level` 不下发**:`t_user_divide_into_log` **没有层级列**
-// (只有 `type` = 分成类型 1 一次付清 / 2 按课次);层级 `t_user_divide_into.layer` 是**当前值**,
-// 不是"入账当时"的值,用它顶替会出现"历史明细显示今天的层级"(时间维度错位,比字段恒空更难查)。
-// 按契约 §3.4 ③「找不到列的一律不写」⇒ 删除该字段,并补 `userId` 以免数组元素无法区分。
-// 已登记偏差,待契约升版。
+// ⚠ 反模式提醒(团队明确禁止):为了让前端 mock 好看而给 `level` 填常量 1。
+// "字段有值、永远等于 1、没人能发现它是假的"是本类缺陷里最难查的一种。
 type DivideGrantRow struct {
-	// UserID 获得分成的用户(ULID)。
+	// UserID 获得分成的用户(ULID,源列 `user_id`)。
 	UserID string
-	// Quota 本次分成额度(= 入账金额)。
-	Quota money.Amount
-	// Amount 本次入账金额。
+	// Type 分成类型(源列 `type`:1 一次付清 / 2 按课次)。
+	Type int32
+	// Amount 本次入账金额(源列 `money`)。
 	Amount money.Amount
 }
 
@@ -975,7 +984,7 @@ func (s *RecordViewService) FinishView(ctx context.Context, res *FinishRecordRes
 			}
 			view.Divide = append(view.Divide, DivideGrantRow{
 				UserID: logs[j].UserID,
-				Quota:  amount,
+				Type:   int32(logs[j].Type),
 				Amount: amount,
 			})
 		}