// 收支端点(端点 60)请求 DTO / 响应视图——AC-13d 第三批。 // // 依据:unified/03-migration/academic-frontend-contract-v1.md §3.4 D 表 60(v1.9 重写)。 // // --------------------------------------------------------------------------- // 契约原写法与目标态的四处冲突,逐条落地(v1.9 已裁决,这里只写明"为什么") // --------------------------------------------------------------------------- // // ① 原行字段 `direction` **删除**:与 `type` 是同一事实的两个名字,两张表都没有该列。 // 前端用 `type` 判方向(1 支出 / 2 收入)。留着一个"看起来能读"的字段,迟早有人拿它 // 和 `type` 比对,然后发现两者永远一致(因为它俩本来就是一件事)。 // ② 原 `name` 在**记录表不存在** ⇒ 由字典 `t_expenditure.name` 提供(后端补齐,前端不拼)。 // ③ 原行**缺 `expenditureId`** ⇒ 补上:只有 `name` 没有 id,前端"编辑"按钮无法回填 // (名称可重名,且改一次名就对不上了)。 // ④ 原 `userId` ⇒ **`createUserId`**:记录表只有 `create_user_id`,语义是**记账人**。 // 契约里 `userId` 读起来像"收付对象",而"收付对象"这一列**不存在**(要加列 + 迁移)。 // 名字不改,前端会把"谁记的账"当"付给谁"展示 —— 数据有值、语义全错。 // // 另:`money` ← `cost`(列名 `cost`,契约字段名 `money`;不改前端字段名,只映射)。 package handler import "time" // CreateExpenditureLogReq 端点 60 记账请求体(`POST /admin/expenditures`)。 // // 字段 → 目标列(`t_expenditure_log`): // // institutionId → institution_id(**必填 >0**;不从 token 猜,超管要能替机构记账) // campusId → campus_id(0 = 机构级;未传且有校区作用域时取作用域校区) // expenditureId → expenditure_id(必填 >0;须在字典中且对本机构可见,否则 3001) // money → cost(**元字符串**,必填 > 0;方向由字典 `type` 决定,本列恒存正数) // remark → remark(可空) // // ⚠ 三个"故意不收"的字段: // // `bizNo` 后端生成(26 位 ULID)。让前端传单号 = 可枚举、可撞唯一键。 // `type` 来自字典 `t_expenditure.type`,**不是入参**:否则"支出类型 + 收入方向"这种 // 自相矛盾的记录会被写进去,报表净额怎么算都是错的。 // `createUserId` 取当前登录用户(作用域 `userId`),不接受前端指定(否则可伪造记账人)。 type CreateExpenditureLogReq struct { // InstitutionID 机构 ID(必填 >0;非超管须与作用域机构一致,否则 1004)。 InstitutionID int64 `json:"institutionId"` // CampusID 校区 ID(0 = 机构级;负数 → 2002)。 CampusID int64 `json:"campusId"` // ExpenditureID 收支类型 ID(必填 >0;字典 `t_expenditure.id`)。 ExpenditureID int64 `json:"expenditureId" validate:"required,gt=0"` // Money 金额(元字符串,如 "100.00";必填且 > 0)。 Money string `json:"money" validate:"required"` // Remark 备注(可空;超长 → 2002)。 Remark string `json:"remark" validate:"max=2000"` } // expenditureLogView 收支记录行(契约 D 表 60 的 9 个字段)。 // // 字段 → 来源列(**逐列溯源**,契约 §3.4 ③ 门槛): // // id ← t_expenditure_log.id // bizNo ← biz_no // expenditureId ← expenditure_id // type ← t_expenditure.type(**字典**;记录表无此列) // name ← t_expenditure.name(**字典**;记录表无此列) // money ← cost(金额,恒 2 位小数字符串) // createUserId ← create_user_id(可空 → `null`;**记账人**,不是收付对象) // campusId ← campus_id // createTime ← create_time // // 契约里**不再有** `direction` / `userId`(理由见文件头 ①④)。 // // 字典未命中时的取值口径:`type=0`、`name=""`(**不编"未知类型"**——造的值追不到列, // 且前端按 `type` 分组时会被这个假值带偏)。 type expenditureLogView struct { // ID 记录 ID。 ID int64 `json:"id"` // BizNo 业务单号(对外唯一标识;不可变)。 BizNo string `json:"bizNo"` // ExpenditureID 收支类型 ID(编辑表单回填用)。 ExpenditureID int64 `json:"expenditureId"` // Type 收支方向(1 支出 / 2 收入;来自字典)。 Type int32 `json:"type"` // Name 收支类型名称(来自字典)。 Name string `json:"name"` // Money 金额(元,恒 2 位小数字符串)。 Money string `json:"money"` // CreateUserID 记账人 ULID(可空 → `null`)。 CreateUserID *string `json:"createUserId"` // CampusID 校区 ID(0 = 机构级)。 CampusID int64 `json:"campusId"` // CreateTime 记账时间(`create_time`)。 CreateTime time.Time `json:"createTime"` } // expenditureTypeView 收支字典项(契约:`types:[{id, type, name, parentId}]`)。 // // 字段 → 来源列:`id` ← id;`type` ← type;`name` ← name;`parentId` ← parent_id(0 = 顶级)。 // **不含 `institutionId`**:契约没给,且前端也用不上(平台通用字典与机构字典在同一个下拉里)。 type expenditureTypeView struct { // ID 类型 ID。 ID int64 `json:"id"` // Type 收支方向(1 支出 / 2 收入)。 Type int32 `json:"type"` // Name 类型名称。 Name string `json:"name"` // ParentID 父类型 ID(0 = 顶级;自引用树)。 ParentID int64 `json:"parentId"` } // expenditureTypesResp 字典响应包裹(契约形状是 `{"types":[...]}`,不是裸数组)。 type expenditureTypesResp struct { // Types 字典项(含平台通用字典,按 type/parentId/id 递增)。 Types []expenditureTypeView `json:"types"` } // createdExpenditureLogView 记账响应(契约:POST → `{id, bizNo}`)。 // // 只回这两个字段是**有意**的:金额/类型/时间都已经由请求确定,回显没有信息量; // 而 `bizNo` 是前端后续对账/查单的唯一 handle。 type createdExpenditureLogView struct { // ID 新记录 ID。 ID int64 `json:"id"` // BizNo 新记录业务单号。 BizNo string `json:"bizNo"` }