|
@@ -1028,19 +1028,41 @@ func (s *RecordViewService) lookupPayrollDetailID(ctx context.Context, rec *mode
|
|
|
return quote.DetailID
|
|
return quote.DetailID
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// recordRewardQuoter 把 `PayrollService` 适配成 `RecordService` 的课酬端口(AC-11 `TeacherRewardQuoter`)——
|
|
|
|
|
-// **D10 裁决后已补齐"按比例提成"**。
|
|
|
|
|
|
|
+// recordRewardQuoter 把 `PayrollService` 适配成 `RecordService` 的课酬端口(AC-11 `TeacherRewardQuoter`)。
|
|
|
//
|
|
//
|
|
|
-// 单节课课酬的完整算法(dump `sp_course_payment_detail_get_teacher_detail` 口径):
|
|
|
|
|
-//
|
|
|
|
|
-// ① base = round(Income × percentage / 100) ← 比例部分(percentage 是**百分数**:"85.0" = 85%)
|
|
|
|
|
-// ② base = clamp(base, Min, Max) ← Min/Max 为 0 表示"不限制"
|
|
|
|
|
-// ③ total = base + Reward ← Reward 是"额外课酬"(在比例基础上追加)
|
|
|
|
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
|
|
+// 权威口径:**PHP 应用层**,不是 dump(`min_payment` 在存储过程里零命中)
|
|
|
|
|
+// ---------------------------------------------------------------------------
|
|
|
//
|
|
//
|
|
|
-// 三条实现红线:
|
|
|
|
|
-// - **全程整数分运算**(`money.Amount` 是分),不在任何一步引入 float;
|
|
|
|
|
-// - 比例用"千分之一"整数承载(`"85.0"` → `850`),`round` 用 `+500 后整除 1000`(四舍五入到分);
|
|
|
|
|
-// - `percentage` 列写坏(非数字、负数)→ **ErrDBError 整体回滚**,不静默按 0 算
|
|
|
|
|
|
|
+// 逐行照抄 `E://study//php//ysw-finance//yuansiwei-finance//modules//courses//helpers//CoursesRecordHelper.php:781-821`
|
|
|
|
|
+// (方法 `settlementExpenses`,正常结课与补签共用):
|
|
|
|
|
+//
|
|
|
|
|
+// $percentage = $coursePaymentDetail->percentage/100; // 百分数 → 比率("85.0" → 0.85)
|
|
|
|
|
+// $addedCost = $coursePaymentDetail->reward; // 额外课酬
|
|
|
|
|
+// $income = round(sum(cost), 3); // 本课全部学生 cost 合计
|
|
|
|
|
+// $teacherReward = 0;
|
|
|
|
|
+// if ($income>0 || $attendClassStudentNumber) { // 守卫:有学生即使收入 0 也算
|
|
|
|
|
+// $teacherReward = round($income*$percentage,3) + $addedCost; // ① 先算提成、② 再加 reward
|
|
|
|
|
+// if ($maxPayment>0 && $teacherReward>$maxPayment) $teacherReward = $maxPayment; // ③ 先夹上限
|
|
|
|
|
+// elseif ($minPayment>0 && $teacherReward<$minPayment) $teacherReward = $minPayment; // ④ 再夹下限
|
|
|
|
|
+// }
|
|
|
|
|
+// if ($teacherRewardType==2) { // 固定课酬:**不看比例**
|
|
|
|
|
+// $teacherReward = self::$makeupCost[$stuCount] ?? self::$makeupCost['max'];
|
|
|
|
|
+// }
|
|
|
|
|
+// if ($attendClassStudentNumber<=0) $teacherReward = 0; // 无人上课 → 课酬 0
|
|
|
|
|
+//
|
|
|
|
|
+// ⚠ **顺序是「加 reward → 再夹 [min,max]」,不是「夹完再加 reward」**:
|
|
|
|
|
+// 当"提成 + reward"超过 `max_payment` 时,结果是 **max(reward 被吃掉)**;
|
|
|
|
|
+// 反过来写会得到 `max + reward` = **多发钱**。这条顺序差有真实金额影响,改动前先回 PHP 原文。
|
|
|
|
|
+//
|
|
|
|
|
+// 另外三条照抄语义:
|
|
|
|
|
+// - `max`/`min` 为 **0 = 不设限**(不是"上限为 0");
|
|
|
|
|
+// - **`elseif` = 上限优先**:两者同时触发时取 `max`;
|
|
|
|
|
+// - `percentage` 是**百分数**:`"85.0"` 表示 85%,不是 0.85,也不是 8500。
|
|
|
|
|
+//
|
|
|
|
|
+// 实现红线:
|
|
|
|
|
+// - **全程整数分运算**,比例用"千分之一"整数承载(`"85.0"` → `850`),`+500 后整除 1000` = 四舍五入到分;
|
|
|
|
|
+// - `percentage` 列写坏(非数字/负数/小数位过多)→ **ErrDBError 整体回滚**,不静默按 0 算
|
|
|
// ("课酬算成 0 却结课成功"是少发钱,比报错难查得多)。
|
|
// ("课酬算成 0 却结课成功"是少发钱,比报错难查得多)。
|
|
|
//
|
|
//
|
|
|
// 已知口径缺口(**不是静默正确**):`TeacherExp` 目前恒 0(AC-11 未接教师档案的年数换算),
|
|
// 已知口径缺口(**不是静默正确**):`TeacherExp` 目前恒 0(AC-11 未接教师档案的年数换算),
|
|
@@ -1072,14 +1094,19 @@ func (q recordRewardQuoter) QuoteRecordReward(ctx context.Context, in RewardQuot
|
|
|
if quote == nil {
|
|
if quote == nil {
|
|
|
return 0, nil
|
|
return 0, nil
|
|
|
}
|
|
}
|
|
|
- return computeRecordReward(quote, in.Income)
|
|
|
|
|
|
|
+ return computeRecordReward(quote, in.Income, in.StudentCount, in.RewardType)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// computeRecordReward 由"命中的课酬规则 + 本次课收入"算出单节课课酬(`recordRewardQuoter` 的算法体)。
|
|
|
|
|
|
|
+// computeRecordReward 由"命中的课酬规则 + 本次课收入/人数/结算方式"算出单节课课酬。
|
|
|
//
|
|
//
|
|
|
-// 入参:quote 命中规则(Percentage/Min/Max/Reward);income 本次课收入。
|
|
|
|
|
|
|
+// 入参:quote 命中规则(Percentage/Min/Max/Reward);income 本次课收入;
|
|
|
|
|
+// studentCount 上课人数;rewardType 结算方式(1 正常 / 2 固定课酬)。
|
|
|
// 出参:单节课课酬(分);2002(收入为负 = 调用方缺陷)/5002(percentage 列写坏)。
|
|
// 出参:单节课课酬(分);2002(收入为负 = 调用方缺陷)/5002(percentage 列写坏)。
|
|
|
-func computeRecordReward(quote *PayrollQuote, income money.Amount) (money.Amount, error) {
|
|
|
|
|
|
|
+//
|
|
|
|
|
+// 顺序**逐行对齐 PHP**(`CoursesRecordHelper.php:794-821`),见 `recordRewardQuoter` 注释:
|
|
|
|
|
+//
|
|
|
|
|
+// 守卫 → income×pct + reward → 夹上限 → 否则夹下限 → (type=2 覆盖为固定课酬)→ 无人则 0
|
|
|
|
|
+func computeRecordReward(quote *PayrollQuote, income money.Amount, studentCount int32, rewardType int8) (money.Amount, error) {
|
|
|
if quote == nil {
|
|
if quote == nil {
|
|
|
return 0, nil
|
|
return 0, nil
|
|
|
}
|
|
}
|
|
@@ -1087,18 +1114,30 @@ func computeRecordReward(quote *PayrollQuote, income money.Amount) (money.Amount
|
|
|
return 0, infraerrors.WithCause(infraerrors.ErrBadParam,
|
|
return 0, infraerrors.WithCause(infraerrors.ErrBadParam,
|
|
|
errString("service: 课酬试算的收入不能为负"))
|
|
errString("service: 课酬试算的收入不能为负"))
|
|
|
}
|
|
}
|
|
|
- base, perr := applyPercentage(quote.Percentage, income)
|
|
|
|
|
- if perr != nil {
|
|
|
|
|
- return 0, perr
|
|
|
|
|
|
|
+ total := money.Amount(0)
|
|
|
|
|
+ // 守卫:`if ($income>0 || $attendClassStudentNumber)` —— 收入 0 但有学生仍要算(0 + reward 再夹)
|
|
|
|
|
+ if income.GreaterThan(0) || studentCount > 0 {
|
|
|
|
|
+ base, perr := applyPercentage(quote.Percentage, income)
|
|
|
|
|
+ if perr != nil {
|
|
|
|
|
+ return 0, perr
|
|
|
|
|
+ }
|
|
|
|
|
+ // ① 先加 reward,② 再夹 —— 顺序不可交换(见类型注释)
|
|
|
|
|
+ total = base.Add(quote.Reward)
|
|
|
|
|
+ // ③ 上限优先(PHP 是 elseif:上限命中就不看下限,两者同时触发取 max)
|
|
|
|
|
+ if quote.Max.GreaterThan(0) && total.GreaterThan(quote.Max) {
|
|
|
|
|
+ total = quote.Max
|
|
|
|
|
+ } else if quote.Min.GreaterThan(0) && total.LessThan(quote.Min) {
|
|
|
|
|
+ total = quote.Min
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- // Min/Max 为 0 表示"不限制"(dump 语义),**不能**当成"下限 0/上限 0"去夹
|
|
|
|
|
- if quote.Min.GreaterThan(0) && base.LessThan(quote.Min) {
|
|
|
|
|
- base = quote.Min
|
|
|
|
|
|
|
+ // 固定课酬(`teacher_reward_type == 2`):**不看比例**,按人数查 PHP 硬编码表
|
|
|
|
|
+ if rewardType == model.CoursesRecordRewardTypeNoMinimum && studentCount > 0 {
|
|
|
|
|
+ total = makeupCostFor(studentCount)
|
|
|
}
|
|
}
|
|
|
- if quote.Max.GreaterThan(0) && base.GreaterThan(quote.Max) {
|
|
|
|
|
- base = quote.Max
|
|
|
|
|
|
|
+ // 无人上课 ⇒ 不产生课酬(PHP `:821`;即使上面算出 reward 也要归零)
|
|
|
|
|
+ if studentCount <= 0 {
|
|
|
|
|
+ total = 0
|
|
|
}
|
|
}
|
|
|
- total := base.Add(quote.Reward)
|
|
|
|
|
if total.IsNegative() {
|
|
if total.IsNegative() {
|
|
|
return 0, infraerrors.WithCause(infraerrors.ErrDBError,
|
|
return 0, infraerrors.WithCause(infraerrors.ErrDBError,
|
|
|
errString("service: 课酬规则算出负值(percentage/min/max/reward 数据异常)"))
|
|
errString("service: 课酬规则算出负值(percentage/min/max/reward 数据异常)"))
|
|
@@ -1106,6 +1145,36 @@ func computeRecordReward(quote *PayrollQuote, income money.Amount) (money.Amount
|
|
|
return total, nil
|
|
return total, nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// makeupCostTable 固定课酬表(`teacher_reward_type == 2`):人数 → 单节课课酬(元)。
|
|
|
|
|
+//
|
|
|
|
|
+// **逐字复刻 PHP 魔法常量**(`CoursesRecordHelper.php:33`,注释原文"单独给学生补课上课的课酬,人数=>课酬"):
|
|
|
|
|
+//
|
|
|
|
|
+// static public $makeupCost = [1=>70,2=>100,3=>120,4=>150,5=>170,6=>200,'max'=>200];
|
|
|
|
|
+//
|
|
|
|
|
+// ⚠ **这张表在数据库里没有**(没有字典表、没有配置项)⇒ 只能按常量复刻;
|
|
|
|
|
+// 改表必须同时改 PHP 与 Go 两处,否则结课与双跑会分叉。D7 双跑也只能从 PHP 行为反推。
|
|
|
|
|
+var makeupCostTable = map[int32]money.Amount{
|
|
|
|
|
+ 1: money.FromCents(7000),
|
|
|
|
|
+ 2: money.FromCents(10000),
|
|
|
|
|
+ 3: money.FromCents(12000),
|
|
|
|
|
+ 4: money.FromCents(15000),
|
|
|
|
|
+ 5: money.FromCents(17000),
|
|
|
|
|
+ 6: money.FromCents(20000),
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// makeupCostMax 超出表区间时的兜底值(PHP `'max' => 200`)。
|
|
|
|
|
+const makeupCostMaxCents = int64(20000)
|
|
|
|
|
+
|
|
|
|
|
+// makeupCostFor 按人数取固定课酬(人数 > 6 → 200.00,与 PHP `?? $makeupCost['max']` 一致)。
|
|
|
|
|
+//
|
|
|
|
|
+// 入参:studentCount(>0,由调用方保证)。出参:单节课课酬。
|
|
|
|
|
+func makeupCostFor(studentCount int32) money.Amount {
|
|
|
|
|
+ if v, ok := makeupCostTable[studentCount]; ok {
|
|
|
|
|
+ return v
|
|
|
|
|
+ }
|
|
|
|
|
+ return money.FromCents(makeupCostMaxCents)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// percentageDenom 比例的分母(`percentage` 是百分数:`"85.0"` = 85% ⇒ 分母 100;
|
|
// percentageDenom 比例的分母(`percentage` 是百分数:`"85.0"` = 85% ⇒ 分母 100;
|
|
|
// 再乘 10 是为了容纳 1 位小数 ⇒ 统一用"千分之一"整数运算,见 `applyPercentage`)。
|
|
// 再乘 10 是为了容纳 1 位小数 ⇒ 统一用"千分之一"整数运算,见 `applyPercentage`)。
|
|
|
const percentageDenom = 1000
|
|
const percentageDenom = 1000
|