|
|
@@ -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 {
|