// 财务域请求 DTO(端点 1~17、21 的入参形状 + 少量响应拼装结构)。 // // 依据:unified/03-migration/academic-domain-design.md §4.1(资金)/ §4.2 端点 17(账单)/ §4.6(响应契约)。 // // --------------------------------------------------------------------------- // 三条 DTO 铁律(AC-12 验收项 ②/④/⑥ 的落点) // --------------------------------------------------------------------------- // // 1. **金额一律 `string`**(`"100.00"`),Handler 用 `parseMoney` → `money.Parse` 解析。 // 绝不声明成 `float64`/`money.Amount`:前者引入二进制浮点(0.1+0.2 级误差进对账), // 后者让 JSON number 也能解码(等于偷偷接受 float)。前端契约 v1.2 已按字符串对接。 // 2. **必填/格式 2001 由 `validate` tag 表达**(`required` / `gt=0` / `oneof=...`); // **值非法 2002 由 Handler 显式判定**(金额 Parse 失败、枚举不在白名单)。 // 两者分工见 `parseMoney` 注释,不要互相顶替。 // 3. **响应 DTO 逐字段声明,不直接序列化 model**:`imgs`(凭证)/`pay_return_data`(渠道原始报文) // 这类字段必须"压根不在结构里",而不是"靠 model 的 `json:"-"` 兜底"—— // 兜底只要有人加一个 `json:"-"` 漏掉字母就会泄露(AC-12 验收 ⑥)。 // // 命名:字段用 camelCase JSON(前端契约同源),Go 侧保持缩写惯例(ID 后缀全大写)。 package handler import "time" // RechargeReq 端点 3 充值请求体(`POST /admin/finance/recharges`)。 // // 字段: // - UserID 充值用户(ULID;必填); // - InstitutionID / CampusID 机构/校区(必填 >0:资金落库必须有明确归属); // - Money 金额字符串(必填;>0 由 Service 校验 → 2002); // - Type 流水类型(可空:<100 一律归 100,对齐 dump `IF inputType<100`); // - RequestID 幂等请求号(可空:空 = 不享幂等,重复提交会二次入账——前端务必传); // - Remark 备注(可空:按类型取默认文案)。 type RechargeReq struct { UserID string `json:"userId" validate:"required,max=26"` InstitutionID int64 `json:"institutionId" validate:"required,gt=0"` CampusID int64 `json:"campusId" validate:"required,gt=0"` Money string `json:"money" validate:"required"` Type int32 `json:"type"` RequestID string `json:"requestId" validate:"max=64"` Remark string `json:"remark" validate:"max=1000"` } // ExpenditureReq 端点 4 消费请求体(`POST /admin/finance/expenditures`)。 // // `Type` 必填且必须属消费白名单(101/104/105/200/201/204/205)——不在白名单 → **2002** // (白名单来自 model.ExpenseLogType*,Service 也会再校验一次;Handler 抢先是为了给出准确字段名)。 type ExpenditureReq struct { UserID string `json:"userId" validate:"required,max=26"` InstitutionID int64 `json:"institutionId" validate:"required,gt=0"` CampusID int64 `json:"campusId" validate:"required,gt=0"` Money string `json:"money" validate:"required"` Type int32 `json:"type" validate:"required"` RequestID string `json:"requestId" validate:"max=64"` Remark string `json:"remark" validate:"max=1000"` } // RefundReq 端点 5 退费请求体(`POST /admin/finance/refunds`)。 // // `Type` 必填(流水类型,通常 207 余额退费);`Money<=0` 由 Service 统一 2002(§3.8-R6), // 因此响应体的 `changed` **恒 true**(字段仅为契约兼容保留,见 §4.1 端点 5 注)。 type RefundReq struct { UserID string `json:"userId" validate:"required,max=26"` InstitutionID int64 `json:"institutionId" validate:"required,gt=0"` CampusID int64 `json:"campusId" validate:"required,gt=0"` Money string `json:"money" validate:"required"` Type int32 `json:"type" validate:"required"` RequestID string `json:"requestId" validate:"max=64"` Remark string `json:"remark" validate:"max=1000"` } // CustomRefundReq 端点 6 自定义退费请求体(`POST /admin/finance/refunds/custom`)。 // // `RequestID` **必填**(设计 §4.1 端点 6 原文):本流程含"活动失效"副作用, // 没有幂等键时重试会把同一批活动重复置为失效并重复写 207/108 双流水。 type CustomRefundReq struct { UserID string `json:"userId" validate:"required,max=26"` InstitutionID int64 `json:"institutionId" validate:"required,gt=0"` CampusID int64 `json:"campusId" validate:"required,gt=0"` Money string `json:"money" validate:"required"` RequestID string `json:"requestId" validate:"required,max=64"` Remark string `json:"remark" validate:"max=1000"` } // TransferReq 端点 7 课费转结请求体(`POST /admin/finance/transfers`)。 // // 四个维度都必填:转出/转入的机构+校区(跨机构转结需超管作用域,Service 层校验)。 type TransferReq struct { UserID string `json:"userId" validate:"required,max=26"` FromInstitutionID int64 `json:"fromInstitutionId" validate:"required,gt=0"` FromCampusID int64 `json:"fromCampusId" validate:"required,gt=0"` ToInstitutionID int64 `json:"toInstitutionId" validate:"required,gt=0"` ToCampusID int64 `json:"toCampusId" validate:"required,gt=0"` Money string `json:"money" validate:"required"` RequestID string `json:"requestId" validate:"max=64"` Remark string `json:"remark" validate:"max=1000"` } // BadDebtReq 端点 8 坏账请求体(`POST /admin/finance/bad-debts`)。 // // ⚠ dump 口径(§3.8-A18):独立坏账是 **`balance` 与 `special_expenses` 双增**, // 不是"只加特殊费用"——响应里 `balance` 已包含本次增量。 type BadDebtReq struct { UserID string `json:"userId" validate:"required,max=26"` InstitutionID int64 `json:"institutionId" validate:"required,gt=0"` CampusID int64 `json:"campusId" validate:"required,gt=0"` Money string `json:"money" validate:"required"` RequestID string `json:"requestId" validate:"max=64"` Remark string `json:"remark" validate:"max=1000"` } // ClearBalanceReq 端点 9 余额清零请求体(`POST /admin/finance/cleared-balances`)。 // // **无金额入参**(清零额 = 清零前校区余额,由 Service 读库决定);dump 亦无请求号, // 故本端点不享幂等(重复调用第二次会因"余额已 0"命中 3120)。 type ClearBalanceReq struct { UserID string `json:"userId" validate:"required,max=26"` InstitutionID int64 `json:"institutionId" validate:"required,gt=0"` CampusID int64 `json:"campusId" validate:"required,gt=0"` Remark string `json:"remark" validate:"max=1000"` } // SpendReq 端点 10 累计消费请求体(`POST /admin/finance/spends`)。 // // `SpendMoney` 语义(§3.1 #11):累加到 `spend` 列(**不动 balance**); // 负数由 Service 归 0(dump INSERT 分支同口径),故 Handler 不做 >0 断言。 type SpendReq struct { UserID string `json:"userId" validate:"required,max=26"` InstitutionID int64 `json:"institutionId" validate:"required,gt=0"` CampusID int64 `json:"campusId" validate:"required,gt=0"` SpendMoney string `json:"spendMoney" validate:"required"` } // CreateRequestReq 端点 13 创建余额工单请求体(`POST /admin/finance/requests`)。 // // 字段: // - Type 工单类型(必填;白名单 100/101/202/203/300 → 非法 2002); // - ToInstitutionID/ToCampusID 仅 type=300 转结使用(其余类型传了也忽略); // - Imgs 凭证图片(**只写不读**:响应里不会出现,见 RequestView)。 type CreateRequestReq struct { Type int32 `json:"type" validate:"required"` UserID string `json:"userId" validate:"required,max=26"` InstitutionID int64 `json:"institutionId" validate:"required,gt=0"` CampusID int64 `json:"campusId"` ToInstitutionID int64 `json:"toInstitutionId"` ToCampusID int64 `json:"toCampusId"` Money string `json:"money" validate:"required"` TransferAccountsType int8 `json:"transferAccountsType"` OrderNo string `json:"orderNo" validate:"max=50"` Item string `json:"item" validate:"max=1000"` Imgs string `json:"imgs" validate:"max=65535"` Remark string `json:"remark" validate:"max=1000"` } // ApproveReq 端点 14 审批通过请求体(`POST /admin/finance/requests/:id/approve`)。 // // 无必填字段(审批意见可空)——**没有 body 也能通过**(前端"一键通过"场景)。 type ApproveReq struct { ResponseMsg string `json:"responseMsg" validate:"max=1000"` } // RejectReq 端点 15 驳回请求体(`POST /admin/finance/requests/:id/reject`)。 // // `ResponseMsg` **必填**:驳回必须留理由(对账/申诉依据)。 type RejectReq struct { ResponseMsg string `json:"responseMsg" validate:"required,max=1000"` } // BillQuery 端点 17 账单查询参数(`GET /student/bill`)。 // // 字段: // - UserID 目标学生(**非本人需 super_admin/parent**,见 BillHandler 权限判定); // - CampusIDs 校区逗号串(dump 第二入参;**只回显不过滤**,见 service.GetBillInput); // - From/To 时间窗(`to` **不含**上界); // - Page/PageSize 分页(已钳制)。 type BillQuery struct { // UserID 学生(空 = 用作用域用户)。 UserID string // CampusIDs 校区逗号串(可空)。 CampusIDs string // From 起始时间(含)。 From time.Time // To 结束时间(不含)。 To time.Time // Page 页码。 Page int // PageSize 每页条数。 PageSize int }