codes.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. // Package apperr academic(教务)域业务错误码(**31xx 段**,跨任务唯一定义点)。
  2. //
  3. // 依据:unified/03-migration/academic-domain-design.md §10.1「错误码段位(academic = 31xx)」。
  4. //
  5. // 设计约束:
  6. // - 31xx 段业务码全部在 **3100~3199** 内,禁止越界(单测断言,CI 门禁);
  7. // - 通用语义复用 infra/errors 的 sentinel:1001 未登录 / 1003 无权限 / 1004 作用域越界 /
  8. // 2001 参数缺失 / 2002 格式错误 / 3001 不存在 / 5002 DB 异常——**本包不重复定义**;
  9. // - 3103(可退金额不足)为 **warn 级**:不阻断流程,响应体带 badDebt 字段;
  10. // - 3114(幂等重复)**仅内部日志用**,对外 code 仍返回 200 + data.repeated=true。
  11. //
  12. // 使用约定(Service 层):
  13. //
  14. // return nil, apperr.ErrGiftAmountShort // 纯业务错误
  15. // return nil, infraerrors.WithCause(apperr.ErrBalanceInsufficient, err) // 需保留底层原因排障
  16. package apperr
  17. import infraerrors "yuansiwei/unified-backend/internal/infra/errors"
  18. // academic 业务错误码常量(31xx 段,取值与设计 §10.1 逐条对应)。
  19. const (
  20. // CodeBalanceInsufficient 余额不足(消费/转结时可用余额 < 需要)。
  21. CodeBalanceInsufficient = 3101
  22. // CodeNoBalanceToRefund 账户余额为 0,不可退费(custom_refund / cleared_balance 前置校验)。
  23. CodeNoBalanceToRefund = 3102
  24. // CodeRefundableShort 可退金额不足,差额计坏账(**warn 级,不阻断**)。
  25. CodeRefundableShort = 3103
  26. // CodeCampusNotFound 校区不存在或未归属机构(跨域读 t_campus 失败 / school_id<=0)。
  27. CodeCampusNotFound = 3104
  28. // CodeActivityAlreadySigned 已报名该活动(signed_add 重复)。
  29. CodeActivityAlreadySigned = 3105
  30. // CodeRecommendLimited 老带新活动每人限参加一次(signed_add,活动 type=4)。
  31. CodeRecommendLimited = 3106
  32. // CodeActivitySignedAudited 活动已审核通过,不可取消(signed_cancel,type∈{2,3} 且已审核)。
  33. CodeActivitySignedAudited = 3107
  34. // CodeRecommendUsed 老带新已产生消费,不可取消(signed_cancel,type=4 且有 used_log)。
  35. CodeRecommendUsed = 3108
  36. // CodeGiftAmountShort 可用赠送金额不足(user_discount_use)。
  37. CodeGiftAmountShort = 3109
  38. // CodeActivityUnavailable 活动已过期或不可用(活动 status/时间窗校验)。
  39. CodeActivityUnavailable = 3110
  40. // CodeDivideQuotaExceeded 分账额度/层级超限,**同一个码两种业务判据**(对外文案各带具体数字):
  41. // ① 建节点时层级超**业务**上限 3 层(divide_into_add,F20 硬规则,2026-11-10);
  42. // ② 改额度时新额度 > 父节点额度(update_quota 超上线)。
  43. //
  44. // ⚠ 2026-11-11 拍板 **A3**:原先另有两条判据(层级超**结构**上限 100 / 上行链超 100 或成环)
  45. // 属**数据损坏**而非业务规则,已改判 **5001**(`infraerrors.ErrInternal`),不再走本码。
  46. //
  47. // 理由一:用户对数据损坏无能为力(只能找管理员),报"额度超限"会把运维引向额度,方向完全错;
  48. // 理由二:前端**读不到** Cause(`response.Fail` 只取文案字段)⇒ 同码不同 Cause 在响应体里
  49. // 本来就不成立,靠 message 分流是一个从未兑现的约定(台账 F32)。
  50. //
  51. // 现两条 3111 一律通过 `errors.WithPublicMsg` 填写对外文案:
  52. //
  53. // ① → "分账层级已达上限:最多 3 层(超管 → 校区主任 → 老师),不能再添加第 N 层"
  54. // ② → "分成额度不得超过上级:本次 X 元,上级额度 Y 元"
  55. CodeDivideQuotaExceeded = 3111
  56. // CodePayrollPlanMissing 课酬方案未配置(get_teacher_detail 无可用方案)。
  57. CodePayrollPlanMissing = 3112
  58. // CodeArrearsCounteractFailed 欠费核销失败(counteract_arrears 冲突)。
  59. CodeArrearsCounteractFailed = 3113
  60. // CodeRepeated 幂等重复请求(**仅内部日志**;对外仍 code=200 + data.repeated=true)。
  61. CodeRepeated = 3114
  62. // CodeClearBalanceRejected 余额清零前置条件不满足(余额≤0 / 校区余额>总余额)。
  63. CodeClearBalanceRejected = 3120
  64. // CodeRecordStateInvalid 打卡/上课状态非法(record start/finish 状态机)。
  65. CodeRecordStateInvalid = 3121
  66. // CodeScheduleConflict 排班冲突(record generate 同 courses+start_time 重复)。
  67. CodeScheduleConflict = 3122
  68. // CodeBusinessParam 多参数联合校验失败(campusId<=0、金额<=0 等;单参数优先 2001/2002)。
  69. CodeBusinessParam = 3140
  70. // CodeActivityFull 报名人数已满(signed_add:number_sign >= number_max)。
  71. CodeActivityFull = 3141
  72. // CodeEnrollmentDuplicate 重复报名(同一学生在本课程已有在读报课行)。
  73. //
  74. // 为什么单独立码而不复用 3140:§10.1 v0.1.19 明令 3140 只收"**参数之间**互相矛盾"
  75. // (句式「A 与 B 矛盾」)。"该学生已报过名"是**业务状态冲突**,不是参数矛盾;
  76. // 塞进 3140 会把 3140 变成垃圾桶(该禁令见 §10.1)。
  77. CodeEnrollmentDuplicate = 3143
  78. )
  79. // 金额/申请/活动/分账/课酬/课程 业务错误的预置 sentinel(业务域直接复用,禁止散落 magic message)。
  80. //
  81. // 文案口径:面向终端用户的中文短句,**不含 SQL/堆栈/内部编码**(脱敏纪律,架构规格 §3.3)。
  82. var (
  83. // ErrBalanceInsufficient 余额不足(3101)。
  84. ErrBalanceInsufficient = infraerrors.New(CodeBalanceInsufficient, "余额不足")
  85. // ErrNoBalanceToRefund 余额为 0 不可退费(3102)。
  86. ErrNoBalanceToRefund = infraerrors.New(CodeNoBalanceToRefund, "账户余额为 0,不可退费")
  87. // ErrRefundableShort 可退金额不足(3103,warn 级:差额转坏账,不阻断主流程)。
  88. ErrRefundableShort = infraerrors.New(CodeRefundableShort, "可退金额不足,差额将计入坏账")
  89. // ErrCampusNotFound 校区不存在或未归属该机构(3104;跨域 port.CampusReader 失败的统一出口)。
  90. ErrCampusNotFound = infraerrors.New(CodeCampusNotFound, "校区不存在或未归属该机构")
  91. // ErrActivityAlreadySigned 已报名该活动(3105)。
  92. ErrActivityAlreadySigned = infraerrors.New(CodeActivityAlreadySigned, "已报名该活动")
  93. // ErrRecommendLimited 老带新每人限参加一次(3106)。
  94. ErrRecommendLimited = infraerrors.New(CodeRecommendLimited, "老带新活动每人限参加一次")
  95. // ErrActivitySignedAudited 活动已审核通过不可取消(3107)。
  96. ErrActivitySignedAudited = infraerrors.New(CodeActivitySignedAudited, "活动已审核通过,不可取消")
  97. // ErrRecommendUsed 老带新已产生消费不可取消(3108)。
  98. ErrRecommendUsed = infraerrors.New(CodeRecommendUsed, "老带新已产生消费,不可取消")
  99. // ErrGiftAmountShort 可用赠送金额不足(3109)。
  100. ErrGiftAmountShort = infraerrors.New(CodeGiftAmountShort, "可用赠送金额不足")
  101. // ErrActivityUnavailable 活动已过期或不可用(3110)。
  102. ErrActivityUnavailable = infraerrors.New(CodeActivityUnavailable, "活动已过期或不可用")
  103. // ErrDivideQuotaExceeded 分账层级超限或额度超上限(3111;两种判据见 CodeDivideQuotaExceeded)。
  104. ErrDivideQuotaExceeded = infraerrors.New(CodeDivideQuotaExceeded, "分账层级超限或额度超上限")
  105. // ErrPayrollPlanMissing 课酬方案未配置(3112)。
  106. ErrPayrollPlanMissing = infraerrors.New(CodePayrollPlanMissing, "课酬方案未配置")
  107. // ErrArrearsCounteractFailed 欠费核销失败(3113)。
  108. ErrArrearsCounteractFailed = infraerrors.New(CodeArrearsCounteractFailed, "欠费核销失败")
  109. // ErrRepeated 幂等重复请求(3114;对外 code 仍 200,仅内部日志/指标使用)。
  110. ErrRepeated = infraerrors.New(CodeRepeated, "重复请求(幂等命中)")
  111. // ErrClearBalanceRejected 余额清零前置条件不满足(3120)。
  112. ErrClearBalanceRejected = infraerrors.New(CodeClearBalanceRejected, "余额清零前置条件不满足")
  113. // ErrRecordStateInvalid 打卡/上课状态非法(3121)。
  114. ErrRecordStateInvalid = infraerrors.New(CodeRecordStateInvalid, "上课状态非法")
  115. // ErrScheduleConflict 排班冲突(3122)。
  116. ErrScheduleConflict = infraerrors.New(CodeScheduleConflict, "排班冲突(同一课程同一上课时间已存在)")
  117. // ErrBusinessParam 多参数联合校验失败(3140)。
  118. ErrBusinessParam = infraerrors.New(CodeBusinessParam, "业务参数错误")
  119. // ErrActivityFull 报名人数已满(3141)。
  120. ErrActivityFull = infraerrors.New(CodeActivityFull, "报名人数已满")
  121. // ErrEnrollmentDuplicate 重复报名(3143)。
  122. ErrEnrollmentDuplicate = infraerrors.New(CodeEnrollmentDuplicate, "该学生已在本课程在读,请勿重复报名")
  123. )
  124. // CodeLowerBound / CodeUpperBound academic 业务码段位上下界(含端点,单测与门禁断言用)。
  125. const (
  126. // CodeLowerBound 段位下界。
  127. CodeLowerBound = 3100
  128. // CodeUpperBound 段位上界。
  129. CodeUpperBound = 3199
  130. )
  131. // all 本包定义的全部业务 sentinel(顺序与设计 §10.1 表格一致;返回新切片,调用方可安全 append)。
  132. var all = []*infraerrors.AppError{
  133. ErrBalanceInsufficient,
  134. ErrNoBalanceToRefund,
  135. ErrRefundableShort,
  136. ErrCampusNotFound,
  137. ErrActivityAlreadySigned,
  138. ErrRecommendLimited,
  139. ErrActivitySignedAudited,
  140. ErrRecommendUsed,
  141. ErrGiftAmountShort,
  142. ErrActivityUnavailable,
  143. ErrDivideQuotaExceeded,
  144. ErrPayrollPlanMissing,
  145. ErrArrearsCounteractFailed,
  146. ErrRepeated,
  147. ErrClearBalanceRejected,
  148. ErrRecordStateInvalid,
  149. ErrScheduleConflict,
  150. ErrBusinessParam,
  151. ErrActivityFull,
  152. ErrEnrollmentDuplicate,
  153. }
  154. // All 返回本包定义的 academic 业务错误全集(供单测断言段位/唯一性、供 Handler 生成错误码文档)。
  155. //
  156. // 出参:[]*infraerrors.AppError——新切片(每次调用重新分配,调用方 append 不影响内部)。
  157. func All() []*infraerrors.AppError {
  158. out := make([]*infraerrors.AppError, len(all))
  159. copy(out, all)
  160. return out
  161. }
  162. // IsAcademicCode 判断错误码是否落在 academic 31xx 段内(3100~3199,含端点)。
  163. //
  164. // 入参:code int——任意错误码。出参:true 表示属 academic 业务码段。
  165. func IsAcademicCode(code int) bool {
  166. return code >= CodeLowerBound && code <= CodeUpperBound
  167. }