dto_activity.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. // 活动域请求 DTO(端点 22~37 的**入参形状**)。
  2. //
  3. // 依据:unified/03-migration/academic-frontend-contract-v1.md
  4. // - §2.2 活动(/admin/activities)端点 22~37 的「请求要点」列;
  5. // - §3.4 B 活动端点字段表(响应侧;本文件只放入参 + 少量响应拼装结构);
  6. // - §1.5 B1/B2(时间与量纲:`startTime`/`endTime` 是业务时间列,金额是两位小数字符串)。
  7. //
  8. // ---------------------------------------------------------------------------
  9. // 三条 DTO 铁律(与 `dto_fund.go` 同源,**不另立一套**)
  10. // ---------------------------------------------------------------------------
  11. //
  12. // 1. **金额一律 `string`**(`"100.00"`),Handler 用 `parseMoney` → `money.Parse`。
  13. // 不声明 `float64`/`money.Amount`:前者把二进制浮点误差带进对账,后者让 JSON number 也能解码
  14. // (等于偷偷接受 float —— 见 `parseMoney` 注释的"连宽松接受数字都不做")。
  15. // 2. **缺参 → 2001 由 `validate` tag 表达;值非法 → 2002 由 Handler 显式判**。
  16. // 因此**枚举字段不写 `oneof=`**(`oneof` 失败会被 `infra/validator` 归成 2001,
  17. // 而"传了非法枚举"按设计 §10.1 属 2002)。枚举白名单一律在 Handler 里判。
  18. // 3. **可选与必填要能区分**:`numberSign` 用 `*int32`(契约 §3.4 B 行 25(v1.6)「传即 2002」,
  19. // "传了 0"与"没传"必须区分开,只有指针能表达);其它可选字段用值类型 + 空串/0 语义。
  20. //
  21. // 命名:camelCase JSON(与契约逐字一致);Go 侧 ID 后缀全大写。
  22. package handler
  23. // CreateActivityReq 端点 23 新建活动请求体(`POST /admin/activities`)。
  24. //
  25. // 字段 → 目标列(DDL 第 7 表):
  26. //
  27. // type→type / campusId→campus_id / title→title / content→content / memo→memo /
  28. // thumbnail→thumbnail / money→money / startTime→start_time / endTime→end_time /
  29. // numberMax→number_max / status→status / toped→toped / subjectId→subject_id / gradeId→grade_id
  30. //
  31. // **不接受的字段**(写进来也无效,故不声明):
  32. //
  33. // numberSign 报名计数列(只能由报名/取消增减;契约行 22 注「活动更新时不可写」)
  34. // view 浏览量(详情页访问自增)
  35. // createTime 由 DB 默认写入(业务时间列,勿手改)
  36. // institutionId 由请求作用域决定(见下方说明)
  37. type CreateActivityReq struct {
  38. // Type 活动类型(必填;2 充值赠送 / 3 代金券 / 4 老带新;其它 → 2002)。
  39. Type int8 `json:"type" validate:"required"`
  40. // CampusID 校区(0/缺省 = 机构所有自营校区;>0 时校验归属本机构)。
  41. CampusID int64 `json:"campusId"`
  42. // InstitutionID 机构(可选:非超管传了但与本作用域不一致 → **1004**;超管可用它指定)。
  43. InstitutionID int64 `json:"institutionId"`
  44. // Title 活动标题(必填,契约行 23 的"新建"未列字段,此处按旧系统编辑页的必填项取 title)。
  45. Title string `json:"title" validate:"required,max=500"`
  46. // Content 活动内容(HTML;可空)。
  47. Content string `json:"content"`
  48. // Memo 备注(可空)。
  49. Memo string `json:"memo" validate:"max=500"`
  50. // Thumbnail 缩略图 URL(可空)。
  51. Thumbnail string `json:"thumbnail" validate:"max=500"`
  52. // Money 活动金额/额度(字符串;可空 → 0.00;**≤0 不拒**,§3.8-Z1)。
  53. Money string `json:"money"`
  54. // StartTime 开始时间(RFC3339 或 `2006-01-02`;可空)。
  55. StartTime string `json:"startTime"`
  56. // EndTime 结束时间(同上;给了 StartTime 时不得早于它 → 否则 2002)。
  57. EndTime string `json:"endTime"`
  58. // NumberMax 名额上限(0 = 不限;负数 → 2002)。
  59. NumberMax int32 `json:"numberMax"`
  60. // Status 活动状态(0 停用 / 1 开启 / 2 已结束;缺省 0 = DDL 默认)。
  61. Status int8 `json:"status"`
  62. // Toped 置顶(0/1)。
  63. Toped int8 `json:"toped"`
  64. // SubjectID 科目(0 = 混合)。
  65. SubjectID int32 `json:"subjectId"`
  66. // GradeID 年级(0 = 混合)。
  67. GradeID int32 `json:"gradeId"`
  68. }
  69. // UpdateActivityReq 端点 25 更新活动请求体(`PUT /admin/activities/:id`,**整体替换**语义)。
  70. //
  71. // 与 `CreateActivityReq` 的差异只有两处:
  72. // - `NumberSign *int32`:**只要出现即 2002**(契约 §3.4 B 行 25,v1.6)。用指针是为了区分
  73. // "没传"与"传了 0"——`{"numberSign":0}` 同样是调用方试图写计数列,必须拒;
  74. // - 字段必填性更松:`title` 仍必填(整体替换后不能没有标题),其余同新建。
  75. type UpdateActivityReq struct {
  76. // Type 活动类型(2/3/4)。
  77. Type int8 `json:"type" validate:"required"`
  78. // CampusID 校区(0 = 机构所有自营校区)。
  79. CampusID int64 `json:"campusId"`
  80. // InstitutionID 机构(非超管须与本作用域一致)。
  81. InstitutionID int64 `json:"institutionId"`
  82. // Title 活动标题(必填)。
  83. Title string `json:"title" validate:"required,max=500"`
  84. // Content 活动内容(可空;PUT 语义下未传即清空)。
  85. Content string `json:"content"`
  86. // Memo 备注。
  87. Memo string `json:"memo" validate:"max=500"`
  88. // Thumbnail 缩略图。
  89. Thumbnail string `json:"thumbnail" validate:"max=500"`
  90. // Money 活动金额(≤0 允许)。
  91. Money string `json:"money"`
  92. // StartTime 开始时间。
  93. StartTime string `json:"startTime"`
  94. // EndTime 结束时间。
  95. EndTime string `json:"endTime"`
  96. // NumberMax 名额上限。
  97. NumberMax int32 `json:"numberMax"`
  98. // Status 活动状态。
  99. Status int8 `json:"status"`
  100. // Toped 置顶。
  101. Toped int8 `json:"toped"`
  102. // SubjectID 科目。
  103. SubjectID int32 `json:"subjectId"`
  104. // GradeID 年级。
  105. GradeID int32 `json:"gradeId"`
  106. // NumberSign **禁写列**:非 nil 即 2002("传了"的判据,见类型注释)。
  107. NumberSign *int32 `json:"numberSign"`
  108. }
  109. // SignActivityReq 端点 26 报名请求体(`POST /admin/activities/:id/sign`)。
  110. //
  111. // 对应 SP19 `sp_courses_activity_signed_add(uid, activityid, status, recommendUid, createUid)`:
  112. // `activityid` 取路径、`createUid` 取作用域,故 body 只有前三个。
  113. type SignActivityReq struct {
  114. // UserID 报名用户(必填;ULID)。
  115. UserID string `json:"userId" validate:"required,max=26"`
  116. // Status 报名状态(可空:<1 → 1,dump 762924-762926 同口径)。
  117. Status int32 `json:"status"`
  118. // RecommendUserID 推荐人(可空;老带新活动才有意义)。
  119. RecommendUserID string `json:"recommendUserId" validate:"max=26"`
  120. }
  121. // CancelActivitySignReq 端点 27 取消报名请求体(`POST /admin/activities/:id/cancel-sign`)。
  122. //
  123. // 对应 SP20 `sp_courses_activity_signed_cancel(uid, activityid)`:`activityid` 取路径。
  124. type CancelActivitySignReq struct {
  125. // UserID 报名用户(必填)。
  126. UserID string `json:"userId" validate:"required,max=26"`
  127. }
  128. // SettleRecommendReq 端点 29 老带新结算请求体(`POST /admin/activities/:id/recommend-settle`)。
  129. //
  130. // 对应 SP18 `sp_courses_activity_use_activity_recommend(uid, order, remark, createUid)`;
  131. // `createUid` 取作用域。
  132. type SettleRecommendReq struct {
  133. // UserID 被推荐人(必填)。
  134. UserID string `json:"userId" validate:"required,max=26"`
  135. // OrderNo 报课记录 ID 的字符串形式(必填:活动上机构/校区为 0 时靠它回查)。
  136. OrderNo string `json:"orderNo" validate:"required,max=100"`
  137. // Remark 备注(可空)。
  138. Remark string `json:"remark" validate:"max=500"`
  139. }
  140. // CancelRechargeReq 端点 30 取消充值活动请求体(`POST /admin/activities/:id/cancel-recharge`)。
  141. //
  142. // 对应 SP8 `sp_user_expense_cancel_recharge_activity(uid, activityid, campusid, money, requestid, remark, createUid)`;
  143. // `activityid` 取路径、`createUid` 取作用域。
  144. type CancelRechargeReq struct {
  145. // UserID 用户(必填)。
  146. UserID string `json:"userId" validate:"required,max=26"`
  147. // CampusID 校区(必填 >0:资金落库必须有明确校区;校区不存在 → 3104)。
  148. CampusID int64 `json:"campusId" validate:"required,gt=0"`
  149. // Money 取消金额(必填字符串)。
  150. Money string `json:"money" validate:"required"`
  151. // RequestID 幂等请求号(可空 = 不享幂等)。
  152. RequestID string `json:"requestId" validate:"max=64"`
  153. // Remark 备注(可空)。
  154. Remark string `json:"remark" validate:"max=500"`
  155. }
  156. // UseActivityDiscountReq 端点 32 使用赠送请求体(`POST /admin/activities/discounts/use`)。
  157. //
  158. // 对应 SP15 `sp_courses_activity_user_discount_use(uid, activityid, stuCourseId, amount, memo)`。
  159. type UseActivityDiscountReq struct {
  160. // UserID 学生(必填)。
  161. UserID string `json:"userId" validate:"required,max=26"`
  162. // ActivityID 活动(必填 >0:**单活动**取池,不得跨活动扣减)。
  163. ActivityID int64 `json:"activityId" validate:"required,gt=0"`
  164. // CoursesStudentID 报课记录(→ discount_log.courses_student_id)。
  165. CoursesStudentID int64 `json:"coursesStudentId"`
  166. // Amount 使用金额(字符串;<=0 → no-op,§3.8-Z1)。
  167. Amount string `json:"amount"`
  168. // Memo 备注(可空)。
  169. Memo string `json:"memo" validate:"max=500"`
  170. }
  171. // RefundActivityDiscountReq 端点 33 赠送退回请求体(`POST /admin/activities/discounts/refund`)。
  172. //
  173. // 对应 SP16 `sp_courses_activity_user_discount_refund(stuCourseId, amount, memo)`。
  174. type RefundActivityDiscountReq struct {
  175. // CoursesStudentID 报课记录(必填 >0:退回对象按流水 `courses_student_id` 定位)。
  176. CoursesStudentID int64 `json:"coursesStudentId" validate:"required,gt=0"`
  177. // Amount 应退金额(字符串;<=0 → no-op)。
  178. Amount string `json:"amount"`
  179. // Memo 备注(可空 → "退回赠送金额")。
  180. Memo string `json:"memo" validate:"max=500"`
  181. }
  182. // RefundDiscountByActivityReq 端点 34 按活动退回请求体
  183. // (`POST /admin/activities/discounts/refund-by-activity`)。
  184. //
  185. // 对应 SP17 `sp_courses_activity_user_discount_refund_user_activity(uid, activityid, amount, memo)`。
  186. type RefundDiscountByActivityReq struct {
  187. // UserID 学生(必填)。
  188. UserID string `json:"userId" validate:"required,max=26"`
  189. // ActivityID 活动(必填 >0)。
  190. ActivityID int64 `json:"activityId" validate:"required,gt=0"`
  191. // Amount 应退金额(字符串;type=3 代金券会被服务端按 A19 改为"最近一条 log 的 ABS")。
  192. Amount string `json:"amount"`
  193. // Memo 备注(可空 → "退回活动金额")。
  194. Memo string `json:"memo" validate:"max=500"`
  195. }
  196. // UseRechargeGiftReq 端点 35 充值赠送使用请求体(`POST /admin/activities/recharge-gifts/use`)。
  197. //
  198. // 对应 SP12 `sp_courses_activity_use_activity_recharge(uid, activityid, stuCourseId, amount, memo)`。
  199. type UseRechargeGiftReq struct {
  200. // UserID 学生(必填)。
  201. UserID string `json:"userId" validate:"required,max=26"`
  202. // ActivityID 活动(必填 >0)。
  203. ActivityID int64 `json:"activityId" validate:"required,gt=0"`
  204. // CoursesStudentID 报课记录(→ used_log.order_no)。
  205. CoursesStudentID int64 `json:"coursesStudentId"`
  206. // Amount 使用金额(字符串;<=0 → no-op)。
  207. Amount string `json:"amount"`
  208. // Memo 备注(可空 → "使用充值赠送金额")。
  209. Memo string `json:"memo" validate:"max=500"`
  210. }
  211. // RefundRechargeGiftReq 端点 36 充值赠送退回请求体(`POST /admin/activities/recharge-gifts/refund`)。
  212. //
  213. // 对应 SP13 `sp_courses_activity_refund_activity_recharge(stuCourseId, amount, memo)`。
  214. type RefundRechargeGiftReq struct {
  215. // CoursesStudentID 报课记录(必填 >0)。
  216. CoursesStudentID int64 `json:"coursesStudentId" validate:"required,gt=0"`
  217. // Amount 应退金额(字符串;<=0 → no-op)。
  218. Amount string `json:"amount"`
  219. // Memo 备注(可空 → "退回活动金额")。
  220. Memo string `json:"memo" validate:"max=500"`
  221. }
  222. // CreateVisitReq 端点 37 回访登记请求体(`POST /admin/activities/:id/visits`)。
  223. //
  224. // 契约 §2.2 行 37 写作 `activitySignedId,memo`(**两个都无问号 ⇒ 必填**)。
  225. type CreateVisitReq struct {
  226. // ActivitySignedID 报名行(必填 >0;不存在/跨作用域 → 3001)。
  227. ActivitySignedID int64 `json:"activitySignedId" validate:"required,gt=0"`
  228. // Memo 回访内容(必填)。
  229. Memo string `json:"memo" validate:"required,max=500"`
  230. }
  231. // ---------------------------------------------------------------- 响应拼装(只放契约要求的键)
  232. // activitySignResp 端点 26 响应(契约 §3.4 B 行 26:只要求 `signedId`)。
  233. type activitySignResp struct {
  234. // SignedID 报名行 ID。
  235. SignedID int64 `json:"signedId"`
  236. }
  237. // activityCancelledResp 端点 27 / 30 响应(契约 §3.4 B 行 27、30:只要求 `cancelled`)。
  238. type activityCancelledResp struct {
  239. // Cancelled 是否已取消(成功恒 true)。
  240. Cancelled bool `json:"cancelled"`
  241. }
  242. // activitySettledResp 端点 29 响应(契约 §3.4 B 行 29:`settled` + `activityId`)。
  243. type activitySettledResp struct {
  244. // Settled 本次是否真正结算(false = dump 的 LEAVE 分支:无推荐人/金额≤0/校区回查不到,零写入)。
  245. Settled bool `json:"settled"`
  246. // ActivityID 结算的活动 ID。
  247. ActivityID int64 `json:"activityId"`
  248. }
  249. // activityUsedResp 端点 32 / 35 响应(契约 §3.4 B 行 32、35:`used`)。
  250. type activityUsedResp struct {
  251. // Used 是否真的发生了使用(金额 ≤0 时服务端 no-op ⇒ false)。
  252. Used bool `json:"used"`
  253. }
  254. // activityRefundedResp 端点 33 / 34 / 36 响应(契约 §3.4 B 行 33、34、36:`refunded`)。
  255. type activityRefundedResp struct {
  256. // Refunded 是否真的发生了退回(金额 ≤0 或无可用行 ⇒ false)。
  257. Refunded bool `json:"refunded"`
  258. }