// 活动域请求 DTO(端点 22~37 的**入参形状**)。 // // 依据:unified/03-migration/academic-frontend-contract-v1.md // - §2.2 活动(/admin/activities)端点 22~37 的「请求要点」列; // - §3.4 B 活动端点字段表(响应侧;本文件只放入参 + 少量响应拼装结构); // - §1.5 B1/B2(时间与量纲:`startTime`/`endTime` 是业务时间列,金额是两位小数字符串)。 // // --------------------------------------------------------------------------- // 三条 DTO 铁律(与 `dto_fund.go` 同源,**不另立一套**) // --------------------------------------------------------------------------- // // 1. **金额一律 `string`**(`"100.00"`),Handler 用 `parseMoney` → `money.Parse`。 // 不声明 `float64`/`money.Amount`:前者把二进制浮点误差带进对账,后者让 JSON number 也能解码 // (等于偷偷接受 float —— 见 `parseMoney` 注释的"连宽松接受数字都不做")。 // 2. **缺参 → 2001 由 `validate` tag 表达;值非法 → 2002 由 Handler 显式判**。 // 因此**枚举字段不写 `oneof=`**(`oneof` 失败会被 `infra/validator` 归成 2001, // 而"传了非法枚举"按设计 §10.1 属 2002)。枚举白名单一律在 Handler 里判。 // 3. **可选与必填要能区分**:`numberSign` 用 `*int32`(契约 §3.4 B 行 25(v1.6)「传即 2002」, // "传了 0"与"没传"必须区分开,只有指针能表达);其它可选字段用值类型 + 空串/0 语义。 // // 命名:camelCase JSON(与契约逐字一致);Go 侧 ID 后缀全大写。 package handler // CreateActivityReq 端点 23 新建活动请求体(`POST /admin/activities`)。 // // 字段 → 目标列(DDL 第 7 表): // // type→type / campusId→campus_id / title→title / content→content / memo→memo / // thumbnail→thumbnail / money→money / startTime→start_time / endTime→end_time / // numberMax→number_max / status→status / toped→toped / subjectId→subject_id / gradeId→grade_id // // **不接受的字段**(写进来也无效,故不声明): // // numberSign 报名计数列(只能由报名/取消增减;契约行 22 注「活动更新时不可写」) // view 浏览量(详情页访问自增) // createTime 由 DB 默认写入(业务时间列,勿手改) // institutionId 由请求作用域决定(见下方说明) type CreateActivityReq struct { // Type 活动类型(必填;2 充值赠送 / 3 代金券 / 4 老带新;其它 → 2002)。 Type int8 `json:"type" validate:"required"` // CampusID 校区(0/缺省 = 机构所有自营校区;>0 时校验归属本机构)。 CampusID int64 `json:"campusId"` // InstitutionID 机构(可选:非超管传了但与本作用域不一致 → **1004**;超管可用它指定)。 InstitutionID int64 `json:"institutionId"` // Title 活动标题(必填,契约行 23 的"新建"未列字段,此处按旧系统编辑页的必填项取 title)。 Title string `json:"title" validate:"required,max=500"` // Content 活动内容(HTML;可空)。 Content string `json:"content"` // Memo 备注(可空)。 Memo string `json:"memo" validate:"max=500"` // Thumbnail 缩略图 URL(可空)。 Thumbnail string `json:"thumbnail" validate:"max=500"` // Money 活动金额/额度(字符串;可空 → 0.00;**≤0 不拒**,§3.8-Z1)。 Money string `json:"money"` // StartTime 开始时间(RFC3339 或 `2006-01-02`;可空)。 StartTime string `json:"startTime"` // EndTime 结束时间(同上;给了 StartTime 时不得早于它 → 否则 2002)。 EndTime string `json:"endTime"` // NumberMax 名额上限(0 = 不限;负数 → 2002)。 NumberMax int32 `json:"numberMax"` // Status 活动状态(0 停用 / 1 开启 / 2 已结束;缺省 0 = DDL 默认)。 Status int8 `json:"status"` // Toped 置顶(0/1)。 Toped int8 `json:"toped"` // SubjectID 科目(0 = 混合)。 SubjectID int32 `json:"subjectId"` // GradeID 年级(0 = 混合)。 GradeID int32 `json:"gradeId"` } // UpdateActivityReq 端点 25 更新活动请求体(`PUT /admin/activities/:id`,**整体替换**语义)。 // // 与 `CreateActivityReq` 的差异只有两处: // - `NumberSign *int32`:**只要出现即 2002**(契约 §3.4 B 行 25,v1.6)。用指针是为了区分 // "没传"与"传了 0"——`{"numberSign":0}` 同样是调用方试图写计数列,必须拒; // - 字段必填性更松:`title` 仍必填(整体替换后不能没有标题),其余同新建。 type UpdateActivityReq struct { // Type 活动类型(2/3/4)。 Type int8 `json:"type" validate:"required"` // CampusID 校区(0 = 机构所有自营校区)。 CampusID int64 `json:"campusId"` // InstitutionID 机构(非超管须与本作用域一致)。 InstitutionID int64 `json:"institutionId"` // Title 活动标题(必填)。 Title string `json:"title" validate:"required,max=500"` // Content 活动内容(可空;PUT 语义下未传即清空)。 Content string `json:"content"` // Memo 备注。 Memo string `json:"memo" validate:"max=500"` // Thumbnail 缩略图。 Thumbnail string `json:"thumbnail" validate:"max=500"` // Money 活动金额(≤0 允许)。 Money string `json:"money"` // StartTime 开始时间。 StartTime string `json:"startTime"` // EndTime 结束时间。 EndTime string `json:"endTime"` // NumberMax 名额上限。 NumberMax int32 `json:"numberMax"` // Status 活动状态。 Status int8 `json:"status"` // Toped 置顶。 Toped int8 `json:"toped"` // SubjectID 科目。 SubjectID int32 `json:"subjectId"` // GradeID 年级。 GradeID int32 `json:"gradeId"` // NumberSign **禁写列**:非 nil 即 2002("传了"的判据,见类型注释)。 NumberSign *int32 `json:"numberSign"` } // SignActivityReq 端点 26 报名请求体(`POST /admin/activities/:id/sign`)。 // // 对应 SP19 `sp_courses_activity_signed_add(uid, activityid, status, recommendUid, createUid)`: // `activityid` 取路径、`createUid` 取作用域,故 body 只有前三个。 type SignActivityReq struct { // UserID 报名用户(必填;ULID)。 UserID string `json:"userId" validate:"required,max=26"` // Status 报名状态(可空:<1 → 1,dump 762924-762926 同口径)。 Status int32 `json:"status"` // RecommendUserID 推荐人(可空;老带新活动才有意义)。 RecommendUserID string `json:"recommendUserId" validate:"max=26"` } // CancelActivitySignReq 端点 27 取消报名请求体(`POST /admin/activities/:id/cancel-sign`)。 // // 对应 SP20 `sp_courses_activity_signed_cancel(uid, activityid)`:`activityid` 取路径。 type CancelActivitySignReq struct { // UserID 报名用户(必填)。 UserID string `json:"userId" validate:"required,max=26"` } // SettleRecommendReq 端点 29 老带新结算请求体(`POST /admin/activities/:id/recommend-settle`)。 // // 对应 SP18 `sp_courses_activity_use_activity_recommend(uid, order, remark, createUid)`; // `createUid` 取作用域。 type SettleRecommendReq struct { // UserID 被推荐人(必填)。 UserID string `json:"userId" validate:"required,max=26"` // OrderNo 报课记录 ID 的字符串形式(必填:活动上机构/校区为 0 时靠它回查)。 OrderNo string `json:"orderNo" validate:"required,max=100"` // Remark 备注(可空)。 Remark string `json:"remark" validate:"max=500"` } // CancelRechargeReq 端点 30 取消充值活动请求体(`POST /admin/activities/:id/cancel-recharge`)。 // // 对应 SP8 `sp_user_expense_cancel_recharge_activity(uid, activityid, campusid, money, requestid, remark, createUid)`; // `activityid` 取路径、`createUid` 取作用域。 type CancelRechargeReq struct { // UserID 用户(必填)。 UserID string `json:"userId" validate:"required,max=26"` // CampusID 校区(必填 >0:资金落库必须有明确校区;校区不存在 → 3104)。 CampusID int64 `json:"campusId" validate:"required,gt=0"` // Money 取消金额(必填字符串)。 Money string `json:"money" validate:"required"` // RequestID 幂等请求号(可空 = 不享幂等)。 RequestID string `json:"requestId" validate:"max=64"` // Remark 备注(可空)。 Remark string `json:"remark" validate:"max=500"` } // UseActivityDiscountReq 端点 32 使用赠送请求体(`POST /admin/activities/discounts/use`)。 // // 对应 SP15 `sp_courses_activity_user_discount_use(uid, activityid, stuCourseId, amount, memo)`。 type UseActivityDiscountReq struct { // UserID 学生(必填)。 UserID string `json:"userId" validate:"required,max=26"` // ActivityID 活动(必填 >0:**单活动**取池,不得跨活动扣减)。 ActivityID int64 `json:"activityId" validate:"required,gt=0"` // CoursesStudentID 报课记录(→ discount_log.courses_student_id)。 CoursesStudentID int64 `json:"coursesStudentId"` // Amount 使用金额(字符串;<=0 → no-op,§3.8-Z1)。 Amount string `json:"amount"` // Memo 备注(可空)。 Memo string `json:"memo" validate:"max=500"` } // RefundActivityDiscountReq 端点 33 赠送退回请求体(`POST /admin/activities/discounts/refund`)。 // // 对应 SP16 `sp_courses_activity_user_discount_refund(stuCourseId, amount, memo)`。 type RefundActivityDiscountReq struct { // CoursesStudentID 报课记录(必填 >0:退回对象按流水 `courses_student_id` 定位)。 CoursesStudentID int64 `json:"coursesStudentId" validate:"required,gt=0"` // Amount 应退金额(字符串;<=0 → no-op)。 Amount string `json:"amount"` // Memo 备注(可空 → "退回赠送金额")。 Memo string `json:"memo" validate:"max=500"` } // RefundDiscountByActivityReq 端点 34 按活动退回请求体 // (`POST /admin/activities/discounts/refund-by-activity`)。 // // 对应 SP17 `sp_courses_activity_user_discount_refund_user_activity(uid, activityid, amount, memo)`。 type RefundDiscountByActivityReq struct { // UserID 学生(必填)。 UserID string `json:"userId" validate:"required,max=26"` // ActivityID 活动(必填 >0)。 ActivityID int64 `json:"activityId" validate:"required,gt=0"` // Amount 应退金额(字符串;type=3 代金券会被服务端按 A19 改为"最近一条 log 的 ABS")。 Amount string `json:"amount"` // Memo 备注(可空 → "退回活动金额")。 Memo string `json:"memo" validate:"max=500"` } // UseRechargeGiftReq 端点 35 充值赠送使用请求体(`POST /admin/activities/recharge-gifts/use`)。 // // 对应 SP12 `sp_courses_activity_use_activity_recharge(uid, activityid, stuCourseId, amount, memo)`。 type UseRechargeGiftReq struct { // UserID 学生(必填)。 UserID string `json:"userId" validate:"required,max=26"` // ActivityID 活动(必填 >0)。 ActivityID int64 `json:"activityId" validate:"required,gt=0"` // CoursesStudentID 报课记录(→ used_log.order_no)。 CoursesStudentID int64 `json:"coursesStudentId"` // Amount 使用金额(字符串;<=0 → no-op)。 Amount string `json:"amount"` // Memo 备注(可空 → "使用充值赠送金额")。 Memo string `json:"memo" validate:"max=500"` } // RefundRechargeGiftReq 端点 36 充值赠送退回请求体(`POST /admin/activities/recharge-gifts/refund`)。 // // 对应 SP13 `sp_courses_activity_refund_activity_recharge(stuCourseId, amount, memo)`。 type RefundRechargeGiftReq struct { // CoursesStudentID 报课记录(必填 >0)。 CoursesStudentID int64 `json:"coursesStudentId" validate:"required,gt=0"` // Amount 应退金额(字符串;<=0 → no-op)。 Amount string `json:"amount"` // Memo 备注(可空 → "退回活动金额")。 Memo string `json:"memo" validate:"max=500"` } // CreateVisitReq 端点 37 回访登记请求体(`POST /admin/activities/:id/visits`)。 // // 契约 §2.2 行 37 写作 `activitySignedId,memo`(**两个都无问号 ⇒ 必填**)。 type CreateVisitReq struct { // ActivitySignedID 报名行(必填 >0;不存在/跨作用域 → 3001)。 ActivitySignedID int64 `json:"activitySignedId" validate:"required,gt=0"` // Memo 回访内容(必填)。 Memo string `json:"memo" validate:"required,max=500"` } // ---------------------------------------------------------------- 响应拼装(只放契约要求的键) // activitySignResp 端点 26 响应(契约 §3.4 B 行 26:只要求 `signedId`)。 type activitySignResp struct { // SignedID 报名行 ID。 SignedID int64 `json:"signedId"` } // activityCancelledResp 端点 27 / 30 响应(契约 §3.4 B 行 27、30:只要求 `cancelled`)。 type activityCancelledResp struct { // Cancelled 是否已取消(成功恒 true)。 Cancelled bool `json:"cancelled"` } // activitySettledResp 端点 29 响应(契约 §3.4 B 行 29:`settled` + `activityId`)。 type activitySettledResp struct { // Settled 本次是否真正结算(false = dump 的 LEAVE 分支:无推荐人/金额≤0/校区回查不到,零写入)。 Settled bool `json:"settled"` // ActivityID 结算的活动 ID。 ActivityID int64 `json:"activityId"` } // activityUsedResp 端点 32 / 35 响应(契约 §3.4 B 行 32、35:`used`)。 type activityUsedResp struct { // Used 是否真的发生了使用(金额 ≤0 时服务端 no-op ⇒ false)。 Used bool `json:"used"` } // activityRefundedResp 端点 33 / 34 / 36 响应(契约 §3.4 B 行 33、34、36:`refunded`)。 type activityRefundedResp struct { // Refunded 是否真的发生了退回(金额 ≤0 或无可用行 ⇒ false)。 Refunded bool `json:"refunded"` }