dto_payroll.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. // 课酬 / 维护 端点请求 DTO(端点 59、61、62、63)——AC-13d 第一批。
  2. //
  3. // 依据:unified/03-migration/academic-frontend-contract-v1.md §2.5(端点表)/ §3.4 D 表(字段级);
  4. // 设计 `academic-domain-design.md` §4.5(端点清单与角色)/ §3.5(SP28/SP29)/ §3.6(SP30/SP31)。
  5. //
  6. // ---------------------------------------------------------------------------
  7. // 本批**只做 59/61/62/63**,58 与 60 因契约形状冲突暂停(已登记偏差,等裁决)
  8. // ---------------------------------------------------------------------------
  9. //
  10. // 58 课酬方案 —— 契约把"方案(t_course_payment)"与"明细(t_course_payment_detail)"**压成一行**:
  11. // `id/schoolId/campusId/effectDate` 是方案列,`teacherId/classifyId/percentage/min/max/reward` 是明细列,
  12. // `actived` 两级都有;且 `teacherExp:number` **一个字段要承载 `teacher_exp_min` + `teacher_exp_max` 两列**。
  13. // ⇒ 需要契约先回答三件事:入参是否嵌套 `details[]`、`id` 指方案还是明细、教龄区间怎么表达。
  14. // 在此之前**不落代码**(按"字段准入条件"硬门槛,写不出源列就不进契约)。
  15. //
  16. // 60 收支 —— 契约 `GET/POST /admin/expenditures[/types]` 把**字典**(`t_expenditure`)与
  17. // **记录**(`t_expenditure_log`)放在同一组路径下,而行字段 `type/name/direction` 三个里
  18. // `name`/`direction` 在记录表**不存在**(记录表只有 `expenditure_id`,方向要 JOIN 字典的 `type`);
  19. // `bizNo` 只在记录表有。⇒ 需要契约把"字典"与"记录"的响应分别定义(当前一行行结构无法同时满足两者)。
  20. //
  21. // 下面四个端点的字段全部可逐列溯源,故先行落地。
  22. package handler
  23. import "time"
  24. // ---------------------------------------------------------------- 端点 59:课酬试算
  25. // PayrollQuoteReq 端点 59 课酬试算请求体(`POST /admin/payroll/quote`)。
  26. //
  27. // 契约 §2.5 行 59 原文入参:`teacherId,schoolId,campusId,classifyId,teacherExp,date`(六个都必填)。
  28. //
  29. // ⚠ **本端点无法给出"最终课酬金额"**:最终金额还需要 `income`(本次课收入)、`studentCount`、
  30. // `teacherRewardType` 三个入参,契约没给 ⇒ 本端点只返回**命中的规则**(含 percentage/min/max/reward),
  31. // 由前端自行展示;**最终金额只在结课(端点 51)产生**,两者共用同一 `computeRecordReward`(不重复实现公式)。
  32. // 已在交付报告登记为偏差(若产品要"试算金额",契约补三个入参即可)。
  33. type PayrollQuoteReq struct {
  34. // TeacherID 老师 ULID(必填)。
  35. TeacherID string `json:"teacherId" validate:"required,max=26"`
  36. // SchoolID 机构 ID(必填 >0;旧口径 schoolid = 目标态 institution_id)。
  37. SchoolID int64 `json:"schoolId" validate:"required,gt=0"`
  38. // CampusID 校区 ID(0 = 机构级方案;契约要求必传,0 合法)。
  39. CampusID int64 `json:"campusId"`
  40. // ClassifyID 课程分类 ID(必填 >0;提供 grade/subject/class_type 三个维度)。
  41. ClassifyID int64 `json:"classifyId" validate:"required,gt=0"`
  42. // TeacherExp 教龄(年;可含小数;负数按 0,与 dump 763609 一致)。
  43. TeacherExp float64 `json:"teacherExp"`
  44. // Date 试算日期(`2006-01-02` 或 RFC3339;方案 `effect_date <= date`,**含当日**)。
  45. Date string `json:"date" validate:"required"`
  46. }
  47. // payrollQuoteView 端点 59 响应(契约 §4.6 `PayrollQuote` 的 9 个字段,**不得裁掉溯源字段**)。
  48. //
  49. // 字段来源(逐列溯源):
  50. //
  51. // detailId ← t_course_payment_detail.id
  52. // schemeId ← t_course_payment.id
  53. // schemeCampusId ← t_course_payment.campus_id(0 = 机构级方案)
  54. // schemeScope ← 由命中层级推导(campus | institution)——**不是列**,而是"从哪个 campus_id 查到的"
  55. // ruleTier ← 由命中规则类型推导(personal_classify | personal_grade_subject | cert_general | nocert_general)
  56. // percentage ← t_course_payment_detail.percentage(**百分数字符串**,非金额)
  57. // min ← min_payment;max ← max_payment;reward ← reward
  58. //
  59. // `schemeScope` / `ruleTier` 属**可推导的溯源标签**(不是新造业务值):它们回答"这条规则为什么被命中",
  60. // 是排障必需项(§4.6 v0.1.29 明确"不得裁掉")。
  61. type payrollQuoteView struct {
  62. // DetailID 命中的课酬明细规则 ID。
  63. DetailID int64 `json:"detailId"`
  64. // SchemeID 命中的课酬方案 ID。
  65. SchemeID int64 `json:"schemeId"`
  66. // SchemeCampusID 命中方案所属校区(0 = 机构级)。
  67. SchemeCampusID int64 `json:"schemeCampusId"`
  68. // SchemeScope 方案层级(campus / institution)。
  69. SchemeScope string `json:"schemeScope"`
  70. // RuleTier 规则层级(personal_classify / personal_grade_subject / cert_general / nocert_general)。
  71. RuleTier string `json:"ruleTier"`
  72. // Percentage 课酬比例(**百分数字符串** "85.0",不是 0.85、不是金额)。
  73. Percentage string `json:"percentage"`
  74. // Min 最低课酬标准(元字符串;"0.00" = 不限制)。
  75. Min string `json:"min"`
  76. // Max 最高课酬标准(元字符串;"0.00" = 不限制)。
  77. Max string `json:"max"`
  78. // Reward 额外课酬(元字符串;在比例基础上追加)。
  79. Reward string `json:"reward"`
  80. }
  81. // ---------------------------------------------------------------- 端点 61 / 62 / 63:维护
  82. // MaintenanceResultResp 维护类端点(61/62/63)的响应(契约 §3.4 D 表:`affected` + `message`)。
  83. //
  84. // `affected` = 本次**实际写库**的行数;`message` = 面向运维的一段人话(含"可重跑/已是最新"判据)。
  85. // 三个端点响应同构,用同一结构体(避免三份一模一样的东西漂移)。
  86. type MaintenanceResultResp struct {
  87. // Affected 本次实际影响(写入)的行数。
  88. Affected int64 `json:"affected"`
  89. // Message 结果说明(含幂等判据:第二次执行应为 0 / 已是最新)。
  90. Message string `json:"message"`
  91. }
  92. // ActivitySyncReq 端点 63 活动数据同步请求体(`POST /admin/maintenance/activity-sync`)。
  93. //
  94. // 契约 §2.5 行 63:`mode` ∈ {`used-log`, `signed`};缺省/非法 → **2001**(契约明确写 2001,
  95. // 不是 2002——因为"缺参"与"枚举非法"在这里都按"必须指定同步模式"处理)。
  96. type ActivitySyncReq struct {
  97. // Mode 同步模式(`used-log` → SP30 重建 used_log;`signed` → SP31 重放 signed)。
  98. //
  99. // ⚠ **不写 `validate:"required"`**:`required` 失败统一归 2001,而本端点要把
  100. // "缺失"(2001)与"传了但非法"(2002)分成两个码 ⇒ 缺失由 Handler 显式判。
  101. Mode string `json:"mode"`
  102. }
  103. // ---------------------------------------------------------------- 端点 58:课酬方案 + 明细
  104. // PayrollPlanDetailReq 明细入参(POST / PUT 共用,**两级形状的内层**)。
  105. //
  106. // 字段 → 目标列(`t_course_payment_detail`):
  107. //
  108. // teacherId→teacher_id / classifyId→classify_id / classType→class_type / gradeId→grade_id /
  109. // subjectId→subject_id / teacherCert→teacher_cert / teacherExpMin→teacher_exp_min /
  110. // teacherExpMax→teacher_exp_max / percentage→percentage / min→min_payment / max→max_payment /
  111. // reward→reward / displayName→name / detailActived→actived
  112. //
  113. // ⚠ 三处命名**故意与契约 mock 不同**(D17 裁决,因两表同名列会互相覆盖):
  114. //
  115. // `displayName` ← 明细 `name`(计算方式名);方案名走 `schemeName`;
  116. // `detailActived` ← 明细 `actived`;方案级启用走 `actived`;
  117. // 教龄是**区间两字段** `teacherExpMin/teacherExpMax`(单值装不下区间,且前端改档会把 max 写坏)。
  118. type PayrollPlanDetailReq struct {
  119. // TeacherID 老师 ULID(空 = 通用规则;个人规则才填)。
  120. TeacherID string `json:"teacherId" validate:"max=26"`
  121. // ClassifyID 课程分类(0 = 不限定分类;个人规则精确档用)。
  122. ClassifyID int64 `json:"classifyId"`
  123. // ClassType 班级类型(对应 t_courses_classify.class_type;必填 >0)。
  124. ClassType int32 `json:"classType" validate:"required,gt=0"`
  125. // GradeID 年级(-1 = 通配;0 = 混合)。
  126. GradeID int32 `json:"gradeId"`
  127. // SubjectID 科目(-1 = 通配;0 = 混合)。
  128. SubjectID int32 `json:"subjectId"`
  129. // TeacherCert 是否持证档(true = 持证通用档;false = 通用/无证档)。
  130. TeacherCert bool `json:"teacherCert"`
  131. // TeacherExpMin 教龄区间下界("1.000";空 → "0.000")。
  132. TeacherExpMin string `json:"teacherExpMin" validate:"max=10"`
  133. // TeacherExpMax 教龄区间上界("5.000";空 → "0.000";**不得小于 Min**,否则 2002)。
  134. TeacherExpMax string `json:"teacherExpMax" validate:"max=10"`
  135. // Percentage 课酬比例(**百分数** "85.0" = 85%;0~100,最多 1 位小数;必填)。
  136. Percentage string `json:"percentage" validate:"required,max=10"`
  137. // Min 最低课酬标准(元字符串;可空 → "0.00" = 不限制)。
  138. Min string `json:"min"`
  139. // Max 最高课酬标准(元字符串;可空 → "0.00" = 不限制)。
  140. Max string `json:"max"`
  141. // Reward 额外课酬(元字符串;可空 → "0.00")。
  142. Reward string `json:"reward"`
  143. // DisplayName 计算方式名称(源列 `name`;与方案名 schemeName 分开)。
  144. DisplayName string `json:"displayName" validate:"max=300"`
  145. // DetailActived 明细级启用(源列 `actived`;与方案级 actived 分开)。
  146. DetailActived bool `json:"detailActived"`
  147. }
  148. // CreatePayrollPlanReq 端点 58 新建请求体(`POST /admin/payroll/plans`)。
  149. type CreatePayrollPlanReq struct {
  150. // InstitutionID 机构(非超管须与作用域一致,否则 1004)。
  151. InstitutionID int64 `json:"institutionId"`
  152. // CampusID 校区(0 = 机构级方案)。
  153. CampusID int64 `json:"campusId"`
  154. // SchemeName 方案名称(必填;源列 `t_course_payment.name`)。
  155. SchemeName string `json:"schemeName" validate:"required,max=200"`
  156. // EffectDate 生效日期(`2006-01-02` 或 RFC3339;可空)。
  157. EffectDate string `json:"effectDate"`
  158. // Actived 方案级启用(源列 `t_course_payment.actived`)。
  159. Actived bool `json:"actived"`
  160. // Details 明细(可空 = 只建空方案;通常至少一条)。
  161. Details []PayrollPlanDetailReq `json:"details"`
  162. }
  163. // UpdatePayrollPlanReq 端点 58 更新请求体(`PUT /admin/payroll/plans/:id`,`:id` = **方案 id**)。
  164. //
  165. // 语义:方案字段 nil = 不改;`details` 缺省(nil)= **不动明细**,给了(哪怕空数组)= **整体替换**。
  166. // 用 `*[]T` 表达这个区别——只有指针能区分"没传"与"传了空数组"。
  167. type UpdatePayrollPlanReq struct {
  168. // InstitutionID 机构(非超管须与作用域一致)。
  169. InstitutionID int64 `json:"institutionId"`
  170. // CampusID 校区(nil = 不改)。
  171. CampusID *int64 `json:"campusId"`
  172. // SchemeName 方案名称(nil = 不改)。
  173. SchemeName *string `json:"schemeName"`
  174. // EffectDate 生效日期(nil = 不改)。
  175. EffectDate *string `json:"effectDate"`
  176. // Actived 方案级启用(nil = 不改)。
  177. Actived *bool `json:"actived"`
  178. // Details 明细(nil = 不动;`[]` = 清空;非空 = 整体替换)。
  179. Details *[]PayrollPlanDetailReq `json:"details"`
  180. }
  181. // payrollPlanDetailView 端点 58 响应的明细(契约 D 表 + D17 裁决字段 + 出口定点化)。
  182. //
  183. // 出场口径(**三条都有"写错就红"的用例兜着**):
  184. //
  185. // teacherId → `null`(通用档,源列 NULL)或老师 ULID;**不下发空串**(空串无法区分"通用"与"脏数据")。
  186. // percentage → 恒 1 位小数("85.0")。`decimal(5,1)` 的位数就是它的量纲指纹:被按金额
  187. // 格式化成 "85.00" 说明量纲被污染,数值恰好不变 ⇒ 只有位数断言抓得住。
  188. // min/max/reward、teacherExpMin/Max → 金额 2 位、教龄 3 位(`money.Amount.String()` / `FormatYears`)。
  189. type payrollPlanDetailView struct {
  190. // DetailID 明细 ID(源列 `t_course_payment_detail.id`)。
  191. DetailID int64 `json:"detailId"`
  192. // TeacherID 老师 ULID(源列 `teacher_id`;NULL → `null`)。
  193. TeacherID *string `json:"teacherId"`
  194. // ClassifyID 课程分类 ID。
  195. ClassifyID int64 `json:"classifyId"`
  196. // ClassType 班级类型。
  197. ClassType int32 `json:"classType"`
  198. // GradeID 年级(-1 通配)。
  199. GradeID int32 `json:"gradeId"`
  200. // SubjectID 科目(-1 通配)。
  201. SubjectID int32 `json:"subjectId"`
  202. // TeacherCert 是否持证档。
  203. TeacherCert bool `json:"teacherCert"`
  204. // TeacherExpMin 教龄下界(源列 `teacher_exp_min`)。
  205. TeacherExpMin string `json:"teacherExpMin"`
  206. // TeacherExpMax 教龄上界(源列 `teacher_exp_max`)。
  207. TeacherExpMax string `json:"teacherExpMax"`
  208. // Percentage 课酬比例(**百分数**,非金额)。
  209. Percentage string `json:"percentage"`
  210. // Min 最低课酬(源列 `min_payment`)。
  211. Min string `json:"min"`
  212. // Max 最高课酬(源列 `max_payment`)。
  213. Max string `json:"max"`
  214. // Reward 额外课酬(源列 `reward`)。
  215. Reward string `json:"reward"`
  216. // DisplayName 计算方式名(源列 `name`)。
  217. DisplayName string `json:"displayName"`
  218. // DetailActived 明细级启用(源列 `actived`)。
  219. DetailActived bool `json:"detailActived"`
  220. }
  221. // payrollPlanView 端点 58 响应(方案 + 明细两级)。
  222. type payrollPlanView struct {
  223. // ID 方案 ID(= `PUT /plans/:id` 的 `:id`)。
  224. ID int64 `json:"id"`
  225. // InstitutionID 机构(源列 `institution_id`)。
  226. InstitutionID int64 `json:"institutionId"`
  227. // CampusID 校区(源列 `campus_id`;0 = 机构级)。
  228. CampusID int64 `json:"campusId"`
  229. // SchemeName 方案名称(源列 `t_course_payment.name`)。
  230. SchemeName string `json:"schemeName"`
  231. // EffectDate 生效日期(源列 `effect_date`;可空 → null)。
  232. EffectDate *time.Time `json:"effectDate"`
  233. // Actived 方案级启用(源列 `t_course_payment.actived`)。
  234. Actived bool `json:"actived"`
  235. // Details 明细(空 → [])。
  236. Details []payrollPlanDetailView `json:"details"`
  237. }