Kaynağa Gözat

docs(academic): 更正 AC-10 注释中的 dump 行号引用(逐行 awk 核对)

问题:AC-10 的四处文件头/方法注释里的 dump 行号整体偏移,其中一处(`>1` 判据)
不仅行号错、**计数也错**(原文写 4 处,实际 3 处)。错误引用比没有引用更危险:
后来人会去错位行看到无关语句,进而怀疑结论或"确认"一个假命题。

本次以 `awk 'NR>=… && NR<=… {printf "%d: %s\n", NR, $0}'` 逐行打印原文后逐条更正:

## sp_course_payment_detail_get_teacher_detail(763577 CREATE → 763665 END)
- 游标           763609 → **763594**(`DECLARE cur CURSOR ... WHERE id=inClassifyid`)
- 教龄钳 0       763621 → **763609**
- 教师持证       763624 → **763613**
- 方案①         763627 → **763616**
- 方案②         763629 → **763617-763619**
- 无方案 LEAVE   763632 → **763621-763623**
- 个人闸门 COUNT 763636 → **763627**
- 个人档①        763638 → **763630-763634**
- 个人档②        763643 → **763636-763640**
- 持证通用③      763650 → **763646-763651**
- 无证通用④      763659 → **763654-763656**
- 收尾 SELECT    763665 → **763663**
- 补充缺失的骨架行:763596 HANDLER / 763603 FETCH / 763605 done 判断 / 763659 END LOOP

## 更正计数与"第四档无判据"(本次最重要的一处)
- `IF coursePaymentDetailid>1 THEN LEAVE readLoop` 实际在 **763632 / 763638 / 763649 三处**
  (原注释写 763640/763645/763653/763657 四处,四行全部错位:763640 是 `END IF`、
  763645 是 `IF teacherCert>0`、763653 空行、763657 是 `/* 获取详细的课酬规则 END */`)。
- **第四档(763654-763656)没有该判据**:三行 SELECT ... INTO 之后直接到 763657 注释与
  763659 `END LOOP`。这一点必须写进注释,否则下一个人会来"补"它。
- 为什么无害(已写进注释):游标 763594 按主键 `id=inClassifyid` 过滤 ⇒ **最多 1 行**,
  第四档后回到 readLoop 顶部 FETCH 撞 763596 的 `CONTINUE HANDLER FOR NOT found` ⇒ done=1 ⇒
  763605 `LEAVE readLoop` 正常退出,值保留 ⇒ 与 Go 侧"首命中即返回"**等价**,不存在被后级覆盖。
- 结论不变(仍按 `>0` 实现、差异面仅 id=1 一行),自证失败用例不变。

## sp_user_expense_get_bill(764094 CREATE → 764238 END)
- 4 张临时表区间:764124-764130 / 764133-764143 / 764146-764153 / 764155-764163
  → **764125-764128 / 764132-764139 / 764143-764149 / 764152-764157**
- 游标(充值+赠送):764168 → **764116 DECLARE / 764159 OPEN**
- 推荐额 764170 → **764173**;消费 764172 → **764175**
- 期末 764174 → **764177**;期初 764176 → **764179**
- 赠送结余 764178-764192 → **764182-764189**(当前 764182-764184 / 上一次 764187-764189)
- 当期使用赠送 764194 → **764193**
- 收尾 9 列 SELECT 764218-764230 → **764228-764236**
- SP 范围 764094~764232 → **764094~764238**;过程体 764095~764232 → **764095~764237**

纯注释改动(0 行业务逻辑变更):go build / go vet / go test -short ./internal/academic/... 全绿。
教训入注释纪律:**引用 dump 行号前先 awk 打出该行原文**,计数与行号都必须落到原文上。
Alex 16 saat önce
ebeveyn
işleme
34cb615d9e

+ 11 - 11
internal/academic/repository/bill_repo.go

@@ -1,7 +1,7 @@
 // 学生账单对账仓储(sp_user_expense_get_bill 的 4 张 MEMORY 临时表 → 内存结构)。
 //
 // 依据:unified/03-migration/academic-domain-design.md
-//   - §3.4 #25 `sp_user_expense_get_bill`(dump **764094~764232**)
+//   - §3.4 #25 `sp_user_expense_get_bill`(dump **764094~764238**)
 //   - §3.8-**E2**(时间列三分类)/ §3.8-**A18**(`type=108` 归因,**只归因不裁剪**)
 //   - §5.1 纪律 4「横切优先」(账单恒等式:期末−期初 = Σ(窗口内流水))
 //
@@ -9,15 +9,15 @@
 // dump 原文与 target 的逐项映射(4 张临时表 → 4 个查询)
 // ---------------------------------------------------------------------------
 //
-//	dump 764124-764130  temp_user_balance_request  ← LoadRechargeRequestNos
+//	dump 764125-764128  temp_user_balance_request  ← LoadRechargeRequestNos
 //	  INSERT ... SELECT id FROM t_user_balance_request
 //	  WHERE userid=? AND aproved=1 AND type=100 AND response_time BETWEEN start AND end
-//	dump 764133-764143  temp_user_expense_log      ← LoadLogs / PageLogs / CountLogs
+//	dump 764132-764139  temp_user_expense_log      ← LoadLogs / PageLogs / CountLogs
 //	  INSERT ... SELECT type,logid,money,create_time FROM t_user_expense_log WHERE userid=?
 //	  (**无时间过滤**:区间过滤在后续每次 SUM 的 WHERE 里现做)
-//	dump 764146-764153  temp_courses_activity_signed ← LoadActivitySigned
+//	dump 764143-764149  temp_courses_activity_signed ← LoadActivitySigned
 //	  INSERT ... SELECT activityid,activity_money,create_time,requestid FROM .. WHERE userid=?
-//	dump 764155-764163  temp_courses_activity_used_log ← LoadActivityUsedLogs
+//	dump 764152-764157  temp_courses_activity_used_log ← LoadActivityUsedLogs
 //	  INSERT ... SELECT activityid,activity_money_used,create_time FROM .. WHERE userid=?
 //
 // 四张临时表在 dump 里都是「该 user 的全量」——本仓储同样按 user 全量取回,
@@ -171,9 +171,9 @@ func (r *BillRepo) CountLogs(ctx context.Context, userID string) (int64, error)
 	return total, nil
 }
 
-// LoadRechargeRequestNos 取区间内的**已通过普通充值工单**业务单号(dump `temp_user_balance_request`,764124)。
+// LoadRechargeRequestNos 取区间内的**已通过普通充值工单**业务单号(dump `temp_user_balance_request`,764125)。
 //
-// dump 原文(764128-764130):
+// dump 原文(764128 单条 INSERT;表结构 764125-764127):
 //
 //	INSERT INTO temp_user_balance_request
 //	SELECT id FROM t_user_balance_request
@@ -215,9 +215,9 @@ func (r *BillRepo) LoadRechargeRequestNos(
 	return nos, nil
 }
 
-// LoadActivitySigned 取该 user 的活动报名行(dump `temp_courses_activity_signed`,764146)。
+// LoadActivitySigned 取该 user 的活动报名行(dump `temp_courses_activity_signed`,764143)。
 //
-// dump 原文(764151-764153):
+// dump 原文(764149 单条 INSERT;表结构 764143-764148):
 //
 //	INSERT INTO temp_courses_activity_signed
 //	SELECT activityid,activity_money,create_time,requestid FROM t_courses_activity_signed WHERE userid=?
@@ -247,9 +247,9 @@ func (r *BillRepo) LoadActivitySigned(ctx context.Context, userID string) ([]mod
 	return rows, nil
 }
 
-// LoadActivityUsedLogs 取该 user 的老带新获得/使用记录(dump `temp_courses_activity_used_log`,764155)。
+// LoadActivityUsedLogs 取该 user 的老带新获得/使用记录(dump `temp_courses_activity_used_log`,764152)。
 //
-// dump 原文(764160-764163):
+// dump 原文(764157 单条 INSERT;表结构 764152-764156):
 //
 //	INSERT INTO temp_courses_activity_used_log
 //	SELECT activityid,activity_money_used,create_time FROM t_courses_activity_used_log WHERE userid=?

+ 27 - 22
internal/academic/repository/payroll_repo.go

@@ -8,24 +8,28 @@
 // dump 原文与本仓储方法的映射(逐行号)
 // ---------------------------------------------------------------------------
 //
-//	763609-763613  游标 `SELECT gradeid,subjectid,class_type FROM t_courses_classify WHERE id=inClassifyid`
-//	               → GetClassify(**按主键单查**,替代游标)
-//	763621-763622  `IF inTeacherExp<0 THEN SET inTeacherExp=0`(教龄下限钳 0)→ Service 侧做
-//	763624         `SELECT teacher_cert FROM t_teacher_information WHERE teacherid=inUserid`
+//	763594         游标 `DECLARE cur CURSOR FOR SELECT gradeid,subjectid,class_type FROM t_courses_classify WHERE id=inClassifyid`
+//	               → GetClassify(**按主键单查**,替代游标;按主键过滤 ⇒ 最多 1 行)
+//	763609-763611  `IF inTeacherExp<0 THEN SET inTeacherExp=0`(教龄下限钳 0)→ Service 侧做
+//	763613         `SELECT teacher_cert FROM t_teacher_information WHERE teacherid=inUserid`
 //	               → TeacherHasCert(**无行 = 无证**,见方法注释)
-//	763627-763632  方案两级回退:
-//	                 ① `schoolid=? AND campusid=? AND actived=1 AND effect_date<=? ORDER BY effect_date DESC LIMIT 1`
-//	                 ② `coursePaymentid<1` → `schoolid=? AND campusid=0 ...`(机构级)
+//	763616-763623  方案两级回退:
+//	                 ① 763616 `schoolid=? AND campusid=? AND actived=1 AND effect_date<=? ORDER BY effect_date DESC LIMIT 1`
+//	                 ② 763617-763619 `coursePaymentid<1 || IS NULL` → `schoolid=? AND campusid=0 ...`(机构级)
+//	                 ③ 763621-763623 仍 <1 → `LEAVE readLoop`(无方案 ⇒ 3112)
 //	               → FindActiveScheme(两次调用,回退顺序在 Service)
-//	763635-763647  个人配置两级:
-//	                 ① `COUNT(1)` where course_paymentid/actived/teacherid/class_type → oneself
-//	                 ② `classifyid=inClassifyid LIMIT 1`(**无 ORDER BY**,本实现补 id ASC 定序)
-//	                 ③ `gradeid IN (g,-1) AND subjectid IN (s,-1) ORDER BY gradeid DESC,subjectid DESC LIMIT 1`
+//	763627-763642  个人配置两级:
+//	                 ① 763627 `SELECT(SELECT COUNT(1) ... course_paymentid/actived/teacherid/class_type) INTO oneself`(闸门)
+//	                 ② 763630-763631 `classifyid=inClassifyid LIMIT 1`(**无 ORDER BY**,本实现补 id ASC 定序)
+//	                 ③ 763636-763637 `gradeid IN (g,-1) AND subjectid IN (s,-1) ORDER BY gradeid DESC,subjectid DESC LIMIT 1`
 //	               → CountPersonalRules / FindPersonalByClassify / FindPersonalByGradeSubject
-//	763650-763664  通用两级:
-//	                 ④ `teacherid=0 AND teacher_cert=1 AND ... AND inTeacherExp BETWEEN exp_min AND exp_max`
-//	                 ⑤ `teacherid=0 AND teacher_cert=0 AND ...`(同上,无证)
+//	763645-763656  通用两级:
+//	                 ④ 763646-763648 `teacherid=0 AND teacher_cert=1 AND ... AND inTeacherExp BETWEEN exp_min AND exp_max`
+//	                 ⑤ 763654-763656 `teacherid=0 AND teacher_cert=0 AND ...`(同上,无证)
 //	               → FindGeneralRule(cert 参数区分 ④/⑤)
+//	  ⚠ `IF coursePaymentDetailid>1 THEN LEAVE readLoop` 只在 **763632 / 763638 / 763649** 三处出现;
+//	    第四档(763654-763656)**没有**该判据——因游标按主键过滤最多 1 行,缺 LEAVE 不会造成覆盖,
+//	    与"首命中即返回"等价。详见 service/payroll_service.go 文件头"有意差异"一节。
 //
 // ---------------------------------------------------------------------------
 // 作用域口径(**本仓储不调 Scoped,改用 assertInstitution**,必须读)
@@ -116,7 +120,7 @@ func (r *PayrollRepo) assertInstitution(institutionID int64) error {
 	return nil
 }
 
-// GetClassify 按主键取课程分类(dump 763609 游标的等价单查)。
+// GetClassify 按主键取课程分类(dump 763594 游标的等价单查)。
 //
 // 入参:ctx;classifyID 分类 ID(<=0 → ErrBadParam)。
 // 出参:*model.CoursesClassify;ErrNotFound(3001) / ErrBadParam(2001) / ErrDBError(5002)。
@@ -148,7 +152,7 @@ func (r *PayrollRepo) GetClassify(ctx context.Context, classifyID int64) (*model
 	return &out, nil
 }
 
-// TeacherHasCert 判断老师是否持教师资格证(dump 763624)。
+// TeacherHasCert 判断老师是否持教师资格证(dump 763613)。
 //
 // dump 原文:
 //
@@ -186,7 +190,7 @@ func (r *PayrollRepo) TeacherHasCert(ctx context.Context, teacherID string) (boo
 	return row.TeacherCert, nil
 }
 
-// FindActiveScheme 取生效的课酬方案(dump 763627-763632 的单次查询;两级回退由 Service 调用两次)。
+// FindActiveScheme 取生效的课酬方案(dump 763616-763619 的单次查询;两级回退由 Service 调用两次)。
 //
 // dump 原文(camupsid 为入参那一档):
 //
@@ -244,7 +248,7 @@ func (r *PayrollRepo) FindActiveScheme(
 	return &out, nil
 }
 
-// CountPersonalRules 统计"该老师在该方案下、该班型"的个人配置条数(dump 763636 的 `oneself`)。
+// CountPersonalRules 统计"该老师在该方案下、该班型"的个人配置条数(dump 763627 的 `oneself`)。
 //
 // dump 原文:
 //
@@ -284,7 +288,7 @@ func (r *PayrollRepo) CountPersonalRules(
 	return total, nil
 }
 
-// FindPersonalByClassify 个人配置·第一档:精确分类命中(dump 763638-763641)。
+// FindPersonalByClassify 个人配置·第一档:精确分类命中(dump 763630-763634)。
 //
 // dump 原文:
 //
@@ -331,7 +335,7 @@ func (r *PayrollRepo) FindPersonalByClassify(
 	return &out, nil
 }
 
-// FindPersonalByGradeSubject 个人配置·第二档:年级/科目通配命中(dump 763643-763647)。
+// FindPersonalByGradeSubject 个人配置·第二档:年级/科目通配命中(dump 763636-763640)。
 //
 // dump 原文:
 //
@@ -383,7 +387,7 @@ func (r *PayrollRepo) FindPersonalByGradeSubject(
 // payrollWildcardID 课酬规则的年级/科目通配值(dump 里字面量 `-1`,表侧存 −1 表示"所有")。
 const payrollWildcardID int32 = -1
 
-// FindGeneralRule 通用档:持证(`teacher_cert=1`)或无证(`teacher_cert=0`)(dump 763650-763664)。
+// FindGeneralRule 通用档:持证(`teacher_cert=1`)或无证(`teacher_cert=0`)(dump 763646-763656)。
 //
 // dump 原文(持证档):
 //
@@ -394,7 +398,8 @@ const payrollWildcardID int32 = -1
 //	          AND inTeacherExp BETWEEN `teacher_exp_min` AND `teacher_exp_max`
 //	        ORDER BY `gradeid` DESC,`subjectid` DESC LIMIT 0,1) INTO coursePaymentDetailid;
 //
-// 无证档(763659-763664)逐字相同,只把 `teacher_cert` 换成 0。
+// 无证档(763654-763656)逐字相同,只把 `teacher_cert` 换成 0;**且它没有 `IF ... LEAVE` 判据**
+// (前三档的判据在 763632/763638/763649,理由见文件头映射块末注)。
 //
 // `teacherid=0` 在目标态的映射:`model.CoursePaymentDetail.TeacherID` 是 `*string`,
 // **NULL/"" = 通用规则**(旧库用 0 表示,ULID 化后没有"0 号老师"),故条件是

+ 29 - 28
internal/academic/service/bill_service.go

@@ -1,24 +1,25 @@
 // 学生账单查询服务(sp_user_expense_get_bill → BillService.GetBill)。
 //
 // 依据:unified/03-migration/academic-domain-design.md
-//   - §3.4 #25 `sp_user_expense_get_bill`(dump **764094~764232**;4 张 MEMORY 临时表 → 内存结构)
+//   - §3.4 #25 `sp_user_expense_get_bill`(dump **764094~764238**;4 张 MEMORY 临时表 → 内存结构)
 //   - §3.8-**E2**(`create_time` 属**业务时间**,参与对账、逐值 0 容差)
 //   - §3.8-**A18**(`type=108` 双来源:**只归因、不裁剪**——本服务的余额类汇总一律全类型计入)
 //   - §5.1 纪律 4「横切优先」(账单恒等式 + 期初/期末不随分页变化)
 //
 // ---------------------------------------------------------------------------
-// dump 骨架(764133~764180,**按原文,不做"优化"**)
+// dump 骨架(764116~764198,**按原文,不做"优化"**)
 // ---------------------------------------------------------------------------
 //
 //	CREATE TEMPORARY TABLE temp_user_expense_log(type INT, logid VARCHAR(50), money DECIMAL(10,3), create_time VARCHAR(100));
 //	INSERT INTO temp_user_expense_log SELECT type,logid,money,create_time FROM t_user_expense_log WHERE userid=inUserid;
 //	-- 同法 temp_courses_activity_signed / temp_courses_activity_used_log / temp_user_balance_request
 //
-//	764170  SELECT SUM(money) FROM temp_user_expense_log WHERE type=107 AND create_time BETWEEN inStartTime AND inEndTime; -- 当期推荐额
-//	764172  SELECT SUM(money) FROM temp_user_expense_log WHERE type in(101,201,104,204,105,205) AND create_time BETWEEN ...; -- 当期消费
-//	764174  SELECT SUM(money) FROM temp_user_expense_log            WHERE create_time<inEndTime;    -- **期末余额**
-//	764176  SELECT SUM(money) FROM temp_user_expense_log            WHERE create_time<inStartTime;  -- **期初余额**
-//	764178-764192  赠送额结余(当前/上一次)→ 当期使用赠送 = 当期赠送 + 上期结余 − 当前结余(764194)
+//	764173  SELECT SUM(money) FROM temp_user_expense_log WHERE type=107 AND create_time BETWEEN inStartTime AND inEndTime; -- 当期推荐额
+//	764175  SELECT SUM(money) FROM temp_user_expense_log WHERE type in(101,201,104,204,105,205) AND create_time BETWEEN ...; -- 当期消费
+//	764177  SELECT SUM(money) FROM temp_user_expense_log as log1    WHERE create_time<inEndTime;    -- **期末余额**
+//	764179  SELECT SUM(money) FROM temp_user_expense_log as log2    WHERE create_time<inStartTime;  -- **期初余额**
+//	764182-764189  赠送额结余(当前 764182-764184 / 上一次 764187-764189)
+//	               → 764193 当期使用赠送 = 当期赠送 + 上期结余 − 当前结余(`SET currentUsedGiveCost = ...`)
 //
 // ---------------------------------------------------------------------------
 // ★ 三种比较语义并存(**dump 的既有不对称,禁止统一**)
@@ -50,7 +51,7 @@
 // ★ `campusIds` 入参的真相(dump 里是**死参数**)
 // ---------------------------------------------------------------------------
 //
-// dump 764094 声明了 `IN inCampusids VARCHAR(1000)`,但 764095~764232 的**整段过程体一次都没引用它**
+// dump 764094 声明了 `IN inCampusids VARCHAR(1000)`,但 764095~764237 的**整段过程体一次都没引用它**
 // (在 dump 里 grep 结果只有声明行)。因此本服务的 `CampusIDs` **只做解析/回显,不做过滤**——
 // 与 dump 完全一致。绝不为此给 `t_user_expense_log` 加 campus 维度(§3.8-f 约束 2:
 // 流水表无 campus 列,账单恒等式只能按 user 做);若产品确需"校区视图",须另开"按
@@ -73,9 +74,9 @@ import (
 	appscope "yuansiwei/unified-backend/internal/infra/scope"
 )
 
-// 账单汇总用到的流水类型集合(dump 764172 原文 `type in(101,201,104,204,105,205)`)。
+// 账单汇总用到的流水类型集合(dump 764175 原文 `type in(101,201,104,204,105,205)`)。
 var (
-	// billSpendTypes 「当期消费」类型集合(逐值与 dump 764172 的 IN 列表一致)。
+	// billSpendTypes 「当期消费」类型集合(逐值与 dump 764175 的 IN 列表一致)。
 	billSpendTypes = []int32{
 		model.ExpenseLogTypeCourseCampus,  // 101
 		model.ExpenseLogTypeCourseCampusB, // 201
@@ -138,28 +139,28 @@ type GetBillInput struct {
 	PageSize int
 }
 
-// BillSummary 账单汇总(dump 764218-764230 的 9 个 SELECT 列 + 3 个对账辅助列)。
+// BillSummary 账单汇总(dump 764228-764236 的 9 个 SELECT 列 + 3 个对账辅助列)。
 //
 // 全部金额为 `money.Amount`(分,定点),JSON 输出为"两位小数字符串"(如 "123.45")——
 // 与旧库 `DECIMAL(10,3)` 的显示习惯对齐到 2 位(目标态 DDL 为 `decimal(12,2)`)。
 type BillSummary struct {
-	// PrevBalance 期初余额 = Σ(money WHERE create_time < From)(dump 764176 `previousBalance`)。
+	// PrevBalance 期初余额 = Σ(money WHERE create_time < From)(dump 764179 `previousBalance`)。
 	PrevBalance money.Amount `json:"prevBalance"`
-	// EndBalance 期末余额 = Σ(money WHERE create_time < To)(dump 764174 `currentBalance`)。
+	// EndBalance 期末余额 = Σ(money WHERE create_time < To)(dump 764177 `currentBalance`)。
 	EndBalance money.Amount `json:"endBalance"`
-	// Recharge 当期充值 = Σ(money WHERE type=100 AND From <= create_time <= To)(dump 764168 游标)。
+	// Recharge 当期充值 = Σ(money WHERE type=100 AND From <= create_time <= To)(dump 764116 `DECLARE rechargeCur CURSOR ...`,764159 `OPEN`)。
 	Recharge money.Amount `json:"recharge"`
-	// Gift 当期赠送额 = Σ(signed.activity_money)(经"充值工单 × 报名"关联,dump 764168 第二列)。
+	// Gift 当期赠送额 = Σ(signed.activity_money)(经"充值工单 × 报名"关联,dump 764116 游标第二列 `SUM(activity.activity_money)`)。
 	Gift money.Amount `json:"gift"`
-	// RecommendAmount 当期老带新推荐额 = Σ(money WHERE type=107 AND 区间内)(dump 764170)。
+	// RecommendAmount 当期老带新推荐额 = Σ(money WHERE type=107 AND 区间内)(dump 764173)。
 	RecommendAmount money.Amount `json:"recommendAmount"`
-	// Spend 当期消费 = Σ(money WHERE type IN (101,201,104,204,105,205) AND 区间内)(dump 764172)。
+	// Spend 当期消费 = Σ(money WHERE type IN (101,201,104,204,105,205) AND 区间内)(dump 764175)。
 	Spend money.Amount `json:"spend"`
-	// GiveUsed 当期使用赠送额(dump 764194 `currentUsedGiveCost` = Gift + GivePrevBalance − GiveEndBalance)。
+	// GiveUsed 当期使用赠送额(dump 764193 `currentUsedGiveCost` = Gift + GivePrevBalance − GiveEndBalance)。
 	GiveUsed money.Amount `json:"giveUsed"`
-	// GivePrevBalance 期初赠送结余(dump 764192 `previousGiveCost`;口径见 giveBalanceBefore)。
+	// GivePrevBalance 期初赠送结余(dump 764187-764189 `previousGiveCost`;口径见 giveBalanceBefore)。
 	GivePrevBalance money.Amount `json:"givePrevBalance"`
-	// GiveEndBalance 期末赠送结余(dump 764180 `currentGiveCost`;口径同上)。
+	// GiveEndBalance 期末赠送结余(dump 764182-764184 `currentGiveCost`;口径同上)。
 	GiveEndBalance money.Amount `json:"giveEndBalance"`
 	// FlowExclusiveEnd 窗口内全类型流水合计(From <= t **<** To)——
 	// **恒等于 EndBalance − PrevBalance**(§5.1 第 4 条:账单恒等式的可判定形式)。
@@ -250,7 +251,7 @@ func (s *BillService) GetBill(ctx context.Context, in GetBillInput) (*BillResult
 	if err != nil {
 		return nil, err
 	}
-	// 充值工单**只在"赠送额"这条链上用到**(dump 764168 游标的 LEFT JOIN),BETWEEN 含两端。
+	// 充值工单**只在"赠送额"这条链上用到**(dump 764116-764121 游标的 LEFT JOIN),BETWEEN 含两端。
 	reqNos, err := s.repo.LoadRechargeRequestNos(ctx, in.UserID, in.From, in.To)
 	if err != nil {
 		return nil, err
@@ -331,7 +332,7 @@ func buildBillSummary(
 ) (BillSummary, error) {
 	var out BillSummary
 
-	// dump 764168:充值工单参与集合(`temp_user_balance_request`)
+	// dump 764116 游标:充值工单参与集合(`temp_user_balance_request`,764128 装入)
 	rechargeReqNos := make(map[string]struct{}, len(reqNos))
 	for _, no := range reqNos {
 		if no == "" {
@@ -349,7 +350,7 @@ func buildBillSummary(
 		if err != nil {
 			return BillSummary{}, infraerrors.WithCause(infraerrors.ErrDBError, err)
 		}
-		// 期初 / 期末:`create_time < 边界`(不含右端,dump 764174 / 764176)
+		// 期初 / 期末:`create_time < 边界`(不含右端,dump 764177 / 764179)
 		if row.CreateTime.Before(from) {
 			out.PrevBalance = out.PrevBalance.Add(amount)
 		}
@@ -365,7 +366,7 @@ func buildBillSummary(
 				out.FlowInclusiveEnd = out.FlowInclusiveEnd.Add(amount)
 			}
 		}
-		// 三类「当期发生额」:BETWEEN = 两端闭(dump 764170 / 764172 / 764168)
+		// 三类「当期发生额」:BETWEEN = 两端闭(dump 764173 / 764175 / 764116)
 		if row.CreateTime.Before(from) || row.CreateTime.After(to) {
 			continue
 		}
@@ -400,7 +401,7 @@ func buildBillSummary(
 		}
 	}
 
-	// 赠送结余(dump 764178-764192):RIGHT JOIN 使"**只有出现过 used_log 的活动**"才进入汇总,
+	// 赠送结余(dump 764182-764189):RIGHT JOIN 使"**只有出现过 used_log 的活动**"才进入汇总,
 	// 且 used_log 的金额是**原值相加**(老带新结算写负值,重放写正值)——逐字复刻,不做符号归一。
 	giveEnd, err := giveBalanceBefore(used, signedByActivity, to)
 	if err != nil {
@@ -412,12 +413,12 @@ func buildBillSummary(
 	}
 	out.GiveEndBalance = giveEnd
 	out.GivePrevBalance = givePrev
-	// dump 764194:`currentUsedGiveCost = currentTotalActivityMoney + previousGiveCost - currentGiveCost`
+	// dump 764193:`SET currentUsedGiveCost = currentTotalActivityMoney + previousGiveCost - currentGiveCost`
 	out.GiveUsed = out.Gift.Add(givePrev).Sub(giveEnd)
 	return out, nil
 }
 
-// giveBalanceBefore 复刻 dump 764178-764192 的"某个 cutoff 之前的赠送金额结余"。
+// giveBalanceBefore 复刻 dump 764182-764189 的"某个 cutoff 之前的赠送金额结余"。
 //
 // dump 原文(原文嵌套三层;这里按语义等价拆解,**箭头右侧是本函数的实现**):
 //
@@ -467,7 +468,7 @@ func giveBalanceBefore(
 	return total, nil
 }
 
-// isBillSpendType 判断流水类型是否属于「当期消费」集合(dump 764172 的 IN 列表)。
+// isBillSpendType 判断流水类型是否属于「当期消费」集合(dump 764175 的 IN 列表)。
 func isBillSpendType(logType int32) bool {
 	for _, t := range billSpendTypes {
 		if t == logType {

+ 58 - 40
internal/academic/service/payroll_service.go

@@ -5,32 +5,48 @@
 //   - §10.1 `3112 课酬方案未配置`("无方案"与"无任何规则命中"两个出口都归 3112,见方法注释)
 //
 // ---------------------------------------------------------------------------
-// dump 骨架(763577 参数 → 763665 `SELECT * FROM t_course_payment_detail WHERE id=coursePaymentDetailid`)
+// dump 骨架(763577 CREATE PROCEDURE → 763663 收尾 SELECT;行号已逐行 awk 核对)
 // ---------------------------------------------------------------------------
 //
-//	763609  游标:`SELECT gradeid,subjectid,class_type FROM t_courses_classify WHERE id=inClassifyid`
-//	        —— **分类不存在 ⇒ 游标 0 行 ⇒ 循环体一次都不执行** ⇒ 末尾 `WHERE id=0` 返回空结果集。
-//	763621  `IF inTeacherExp<0 THEN SET inTeacherExp=0`(教龄负值钳 0,在循环体内但只依赖入参)
-//	763624  老师持证:`SELECT (SELECT teacher_cert FROM t_teacher_information WHERE teacherid=inUserid)`
+//	763594  游标:`DECLARE cur CURSOR FOR SELECT gradeid,subjectid,class_type FROM t_courses_classify WHERE id=inClassifyid`
+//	        —— **按主键过滤 ⇒ 最多 1 行**;分类不存在 ⇒ 0 行 ⇒ 循环体一次都不执行 ⇒ 末尾 `WHERE id=0` 返回空结果集
+//	763596  `DECLARE CONTINUE HANDLER FOR NOT found SET done = 1`
+//	763603  `FETCH cur INTO classifyGradeid,classifySubjectid,classifyClassType`
+//	763605  `IF done=1 THEN LEAVE readLoop`
+//	763609  `IF inTeacherExp<0 THEN SET inTeacherExp = 0`(教龄负值钳 0)
+//	763613  老师持证:`SELECT (SELECT teacher_cert FROM t_teacher_information WHERE teacherid=inUserid)`
 //	        —— **无行 ⇒ NULL ⇒ `IF teacherCert>0` 不成立 ⇒ 按无证走**
-//	763627  方案①:`schoolid+inCampusid+actived=1+effect_date<=inDate ORDER BY effect_date DESC LIMIT 1`
-//	763629  方案②(回退):`coursePaymentid<1` → 同条件但 `campusid=0`(机构级)
-//	763632  `IF coursePaymentid<1 THEN LEAVE readLoop` → 无任何方案 ⇒ **3112**
-//	763636  个人配置闸门:`COUNT(1) WHERE course_paymentid+actived+teacherid+class_type`
-//	763638  个人档 ①:+ `classifyid=inClassifyid`
-//	763643  个人档 ②:+ `gradeid IN (g,-1)`、`subjectid IN (s,-1)`,`ORDER BY gradeid DESC,subjectid DESC`
-//	763650  通用档 ③(持证):`teacherid=0 AND teacher_cert=1` + 年级/科目通配 + 教龄 BETWEEN
-//	763659  通用档 ④(无证):同上但 `teacher_cert=0`
-//	763665  收尾:按命中的 detail id 取整行(**未命中 ⇒ id=0 ⇒ 空结果集**)
+//	763616  方案①:`schoolid+inCampusid+actived=1+effect_date<=inDate ORDER BY effect_date DESC LIMIT 1`
+//	763617-763619  方案②(回退):`coursePaymentid<1 || IS NULL` → 同条件但 `campusid=0`(机构级)
+//	763621-763623  `IF coursePaymentid<1 THEN LEAVE readLoop` → 无任何方案 ⇒ **3112**
+//	763627  个人配置闸门:`SELECT(SELECT COUNT(1) ... course_paymentid+actived+teacherid+class_type) INTO oneself`
+//	763628-763642  `IF oneself>0 THEN` … 个人档 ① `classifyid=inClassifyid`(763630-763631)
+//	               与个人档 ② `gradeid IN (g,-1)`+`subjectid IN (s,-1)`(763636-763637)
+//	763645-763652  `IF teacherCert>0 THEN` … 通用档 ③(持证,`teacherid=0 AND teacher_cert=1`,763646-763648)
+//	763654-763656  通用档 ④(无证,`teacher_cert=0`)—— **本档没有 `IF ... LEAVE` 判据**(见下节)
+//	763659  `END LOOP`;763663 `SELECT * FROM t_course_payment_detail WHERE id=coursePaymentDetailid`
 //
 // ---------------------------------------------------------------------------
 // ★ 一处**有意差异**(必读):dump 的 `IF coursePaymentDetailid>1`
 // ---------------------------------------------------------------------------
 //
-// dump 763640 / 763645 / 763653 / 763657 判断的是 `coursePaymentDetailid **> 1**` 而不是 `> 0`。
+// `IF coursePaymentDetailid>1 THEN LEAVE readLoop` 在 dump 里出现在 **3 处**:
+// **763632 / 763638 / 763649**(分别对应个人档 ①、个人档 ②、通用档 ③)。
+//
+// **第四档(无证通用,763654-763656)没有这个判据**:它只有三行
+// `SELECT (SELECT ... ) INTO coursePaymentDetailid`,后面直接到 763657 注释与 763659 `END LOOP`,
+// 没有 `IF coursePaymentDetailid>1 THEN LEAVE readLoop`。
+//
+// **这个缺省为什么无害(务必写清,否则下一个人会来"补"它)**:
+// 游标 763594 是 `WHERE id=inClassifyid` —— **按主键过滤,最多 1 行**。第四档之后回到
+// `readLoop` 顶部 `FETCH` 撞上 763596 的 `CONTINUE HANDLER FOR NOT found` ⇒ `done=1` ⇒
+// 763605 `LEAVE readLoop` 正常退出,`coursePaymentDetailid` 保留第四档查到的值。
+// 即"缺 LEAVE 但循环本来就要结束" ⇒ 与 Go 侧"首命中即返回"**等价**,不存在"被后级覆盖"。
+//
+// 而**有**判据的前三档,判的是 `**> 1**` 而不是 `> 0`:
 // `t_course_payment_detail.id` 是自增主键、从 **1** 开始 ⇒ **id=1 那条规则永远"不算命中"**:
-// 该档查到 id=1 后不 LEAVE,被后续**更低优先级**的查询覆盖(后级无命中时覆盖成 NULL)→
-// 最终 `WHERE id=NULL` 返回空 ⇒ "配了规则却试算不出课酬"。
+// 该档查到 id=1 后不 LEAVE,控制流继续往下走,被后续**更低优先级**的查询覆盖
+// (后级无命中时覆盖成 NULL)→ 最终 `WHERE id=NULL`(763663)返回空 ⇒ "配了规则却试算不出课酬"。
 //
 // 这是 dump 的既有缺陷(错字级:`>1` 应为 `>0`),本次实现按 `>0`(即"找到即命中"),
 // 差异面**恰好只有 id=1 这一行**(id>=2 时 `>1` 与 `>0` 等价),登记为**有意差异(修正型)**:
@@ -67,21 +83,21 @@ import (
 
 // 课酬方案层级(两级回退的命中级别)。
 const (
-	// SchemeScopeCampus 校区级方案(dump 763627,第一个查询)。
+	// SchemeScopeCampus 校区级方案(dump 763616,第一个查询)。
 	SchemeScopeCampus = "campus"
-	// SchemeScopeInstitution 机构级方案(dump 763629,`campusid=0` 的第二级回退)。
+	// SchemeScopeInstitution 机构级方案(dump 763617-763619,`campusid=0` 的第二级回退)。
 	SchemeScopeInstitution = "institution"
 )
 
 // 课酬规则层级(三级回退的命中级别)。
 const (
-	// RuleTierPersonalClassify 个人配置·精确分类(dump 763638)。
+	// RuleTierPersonalClassify 个人配置·精确分类(dump 763630-763631)。
 	RuleTierPersonalClassify = "personal_classify"
-	// RuleTierPersonalGradeSubject 个人配置·年级/科目通配(dump 763643)。
+	// RuleTierPersonalGradeSubject 个人配置·年级/科目通配(dump 763636-763637)。
 	RuleTierPersonalGradeSubject = "personal_grade_subject"
-	// RuleTierCertGeneral 持证通用(dump 763650)。
+	// RuleTierCertGeneral 持证通用(dump 763646-763648)。
 	RuleTierCertGeneral = "cert_general"
-	// RuleTierNoCertGeneral 无证通用(dump 763659)。
+	// RuleTierNoCertGeneral 无证通用(dump 763654-763656)。
 	RuleTierNoCertGeneral = "nocert_general"
 )
 
@@ -107,7 +123,7 @@ func NewPayrollService(db *gorm.DB, sc *appscope.Scope) *PayrollService {
 //
 // 字段:
 //   - TeacherID 老师(ULID;旧 inUserid);
-//   - TeacherExp 教龄(年;旧 inTeacherExp FLOAT;**负数按 0 处理**,dump 763621);
+//   - TeacherExp 教龄(年;旧 inTeacherExp FLOAT;**负数按 0 处理**,dump 763609);
 //   - InstitutionID 机构 ID(旧 inSchoolid;<=0 时取作用域机构);
 //   - CampusID 校区 ID(旧 inCampusid;方案两级回退的第一级);
 //   - ClassifyID 课程分类 ID(旧 inClassifyid;提供 grade/subject/class_type 三个维度);
@@ -127,7 +143,7 @@ type QuotePayrollInput struct {
 	Date time.Time
 }
 
-// PayrollQuote 课酬试算结果(dump 763665 `SELECT *` 的 4 个业务列 + 溯源列)。
+// PayrollQuote 课酬试算结果(dump 763663 `SELECT *` 的 4 个业务列 + 溯源列)。
 //
 // 金额口径:`Min` / `Max` / `Reward` 来自 `decimal(12,2)` 列 → `money.Amount`(两位小数字符串输出)。
 //
@@ -194,7 +210,7 @@ func (s *PayrollService) QuoteTeacherReward(ctx context.Context, in QuotePayroll
 			errString("service: QuoteTeacherReward 需要机构 id(入参或作用域)"))
 	}
 
-	// 763609 分类(游标等价单查):不存在 ⇒ 与 dump 的"空结果集"同类 → 3112
+	// 763594 分类(游标等价单查):不存在 ⇒ 与 dump 的"空结果集"同类 → 3112
 	classify, err := s.repo.GetClassify(ctx, in.ClassifyID)
 	if err != nil {
 		if isNotFoundErr(err) {
@@ -205,29 +221,29 @@ func (s *PayrollService) QuoteTeacherReward(ctx context.Context, in QuotePayroll
 		return nil, err
 	}
 
-	// 763621 教龄负值钳 0(dump 在循环体内做,但只依赖入参,提出循环等价)
+	// 763609 教龄负值钳 0(dump 在循环体内做,但只依赖入参,提出循环等价)
 	teacherExp := formatTeacherExp(in.TeacherExp)
 
-	// 763624 持证判定(无行 = 无证)
+	// 763613 持证判定(无行 = 无证)
 	hasCert, err := s.repo.TeacherHasCert(ctx, in.TeacherID)
 	if err != nil {
 		return nil, err
 	}
 
-	// 763627-763632 方案两级回退
+	// 763616-763623 方案两级回退
 	scheme, scope, err := s.resolveScheme(ctx, institutionID, in.CampusID, in.Date)
 	if err != nil {
 		return nil, err
 	}
 
-	// 763636-763664 规则三级回退(个人配置内部再分两档)
+	// 763627-763656 规则三级回退(个人配置内部再分两档)
 	detail, tier, err := s.resolveDetail(ctx, scheme.ID, in.TeacherID, hasCert,
 		classify, teacherExp)
 	if err != nil {
 		return nil, err
 	}
 	if detail == nil {
-		// 763665:dump 返回空结果集 ⇒ Go 显式化 3112(与"无方案"同一个对外类别)
+		// 763663:dump 返回空结果集 ⇒ Go 显式化 3112(与"无方案"同一个对外类别)
 		return nil, infraerrors.WithCause(apperr.ErrPayrollPlanMissing,
 			errString("service: 该老师/分类/教龄未命中任何课酬规则(方案 "+fmt.Sprintf("%d", scheme.ID)+")"))
 	}
@@ -250,7 +266,7 @@ func (s *PayrollService) QuotePayroll(ctx context.Context, in QuotePayrollInput)
 	return s.QuoteTeacherReward(ctx, in)
 }
 
-// resolveScheme 方案两级回退(dump 763627-763632)。
+// resolveScheme 方案两级回退(dump 763616-763623)。
 //
 // 入参:ctx;institutionID(>0);campusID 入参校区;date 试算日期。
 // 出参:
@@ -259,7 +275,7 @@ func (s *PayrollService) QuotePayroll(ctx context.Context, in QuotePayrollInput)
 //   - 3112(两级都没有)/ ErrBadParam / ErrUnscoped / ErrDBError。
 //
 // 顺序**不可交换**:先校区级、再机构级(`campus_id=0`);校区级存在时**绝不**因为
-// "机构级更匹配"而回退——dump 只在 `coursePaymentid<1` 时才走第二级(763629)。
+// "机构级更匹配"而回退——dump 只在 `coursePaymentid<1` 时才走第二级(763617-763619)。
 func (s *PayrollService) resolveScheme(
 	ctx context.Context, institutionID, campusID int64, date time.Time,
 ) (*model.CoursePayment, string, error) {
@@ -270,7 +286,7 @@ func (s *PayrollService) resolveScheme(
 	if scheme != nil {
 		return scheme, SchemeScopeCampus, nil
 	}
-	// 763629:`campusid=0` 机构级方案(**campusID=0 时不必重查**:上面那次就是它)
+	// 763618:`campusid=0` 机构级方案(**campusID=0 时不必重查**:上面那次就是它)
 	if campusID != 0 {
 		scheme, err = s.repo.FindActiveScheme(ctx, institutionID, 0, date)
 		if err != nil {
@@ -280,13 +296,13 @@ func (s *PayrollService) resolveScheme(
 			return scheme, SchemeScopeInstitution, nil
 		}
 	}
-	// 763632:`IF coursePaymentid<1 THEN LEAVE readLoop` → 无方案
+	// 763621:`IF coursePaymentid<1 THEN LEAVE readLoop` → 无方案
 	return nil, "", infraerrors.WithCause(apperr.ErrPayrollPlanMissing,
 		errString("service: 机构 "+fmt.Sprintf("%d", institutionID)+" 校区 "+
 			fmt.Sprintf("%d", campusID)+" 无生效课酬方案(含机构级回退)"))
 }
 
-// resolveDetail 规则三级回退(dump 763636-763664)。
+// resolveDetail 规则三级回退(dump 763627-763656)。
 //
 // 入参:ctx;schemeID;teacherID;hasCert 是否持证;classify 分类维度;teacherExp 已钳 0 的 decimal 字符串。
 // 出参:
@@ -296,13 +312,15 @@ func (s *PayrollService) resolveScheme(
 //
 // 回退顺序(与 dump 逐行一致,**任何一步命中即停**):
 //
-//	个人档闸门(763636 COUNT>0)→ ① 精确分类(763638)→ ② 年级/科目通配(763643)
-//	→ 持证时 ③ 持证通用(763650)→ ④ 无证通用(763659)
+//	个人档闸门(763627 COUNT>0)→ ① 精确分类(763630)→ ② 年级/科目通配(763636)
+//	→ 持证时 ③ 持证通用(763646)→ ④ 无证通用(763654)
 //
 // 注意两处"看起来可以省掉、其实不能改"的细节:
 //  1. 个人档闸门是 `COUNT>0`(任意一条个人规则)而不是"命中某条"——它的作用只是决定
 //     "要不要尝试 ①/②",因此闸门开着但 ①② 都不命中时,**继续往 ③/④ 走**(不是返回空);
-//  2. 持证者若 ③ 未命中,**仍要**试 ④(无证通用档也对持证老师开放),dump 763659 无 else 分支。
+//  2. 持证者若 ③ 未命中,**仍要**试 ④(无证通用档也对持证老师开放):
+//     dump 763652 是 `END IF`(结束 `IF teacherCert>0`),紧随其后的 763654 就是无证档——
+//     **没有 else 分支**,所以持证老师的路径必然经过 ④。
 func (s *PayrollService) resolveDetail(
 	ctx context.Context, schemeID int64, teacherID string, hasCert bool,
 	classify *model.CoursesClassify, teacherExp string,
@@ -379,7 +397,7 @@ func buildPayrollQuote(detail *model.CoursePaymentDetail) (*PayrollQuote, error)
 
 // formatTeacherExp 把教龄(年)格式化成 `decimal(6,2)` 字符串(供 SQL 做 BETWEEN 比较)。
 //
-// 入参:exp 教龄(年;dump 为 FLOAT)。负数 → 按 0(dump 763621)。
+// 入参:exp 教龄(年;dump 为 FLOAT)。负数 → 按 0(dump 763609)。
 // 出参:形如 "3.50" 的定点字符串(**不经 float 参与累加**:只做一次"×100 取整"换算,
 // 之后全部由数据库按 decimal 语义比较,避免 3.4999999 之类的二进制尾差把边界行判掉)。
 func formatTeacherExp(exp float64) string {