dto_expenditure.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. // 收支端点(端点 60)请求 DTO / 响应视图——AC-13d 第三批。
  2. //
  3. // 依据:unified/03-migration/academic-frontend-contract-v1.md §3.4 D 表 60(v1.9 重写)。
  4. //
  5. // ---------------------------------------------------------------------------
  6. // 契约原写法与目标态的四处冲突,逐条落地(v1.9 已裁决,这里只写明"为什么")
  7. // ---------------------------------------------------------------------------
  8. //
  9. // ① 原行字段 `direction` **删除**:与 `type` 是同一事实的两个名字,两张表都没有该列。
  10. // 前端用 `type` 判方向(1 支出 / 2 收入)。留着一个"看起来能读"的字段,迟早有人拿它
  11. // 和 `type` 比对,然后发现两者永远一致(因为它俩本来就是一件事)。
  12. // ② 原 `name` 在**记录表不存在** ⇒ 由字典 `t_expenditure.name` 提供(后端补齐,前端不拼)。
  13. // ③ 原行**缺 `expenditureId`** ⇒ 补上:只有 `name` 没有 id,前端"编辑"按钮无法回填
  14. // (名称可重名,且改一次名就对不上了)。
  15. // ④ 原 `userId` ⇒ **`createUserId`**:记录表只有 `create_user_id`,语义是**记账人**。
  16. // 契约里 `userId` 读起来像"收付对象",而"收付对象"这一列**不存在**(要加列 + 迁移)。
  17. // 名字不改,前端会把"谁记的账"当"付给谁"展示 —— 数据有值、语义全错。
  18. //
  19. // 另:`money` ← `cost`(列名 `cost`,契约字段名 `money`;不改前端字段名,只映射)。
  20. package handler
  21. import "time"
  22. // CreateExpenditureLogReq 端点 60 记账请求体(`POST /admin/expenditures`)。
  23. //
  24. // 字段 → 目标列(`t_expenditure_log`):
  25. //
  26. // institutionId → institution_id(**必填 >0**;不从 token 猜,超管要能替机构记账)
  27. // campusId → campus_id(0 = 机构级;未传且有校区作用域时取作用域校区)
  28. // expenditureId → expenditure_id(必填 >0;须在字典中且对本机构可见,否则 3001)
  29. // money → cost(**元字符串**,必填 > 0;方向由字典 `type` 决定,本列恒存正数)
  30. // remark → remark(可空)
  31. //
  32. // ⚠ 三个"故意不收"的字段:
  33. //
  34. // `bizNo` 后端生成(26 位 ULID)。让前端传单号 = 可枚举、可撞唯一键。
  35. // `type` 来自字典 `t_expenditure.type`,**不是入参**:否则"支出类型 + 收入方向"这种
  36. // 自相矛盾的记录会被写进去,报表净额怎么算都是错的。
  37. // `createUserId` 取当前登录用户(作用域 `userId`),不接受前端指定(否则可伪造记账人)。
  38. type CreateExpenditureLogReq struct {
  39. // InstitutionID 机构 ID(必填 >0;非超管须与作用域机构一致,否则 1004)。
  40. InstitutionID int64 `json:"institutionId"`
  41. // CampusID 校区 ID(0 = 机构级;负数 → 2002)。
  42. CampusID int64 `json:"campusId"`
  43. // ExpenditureID 收支类型 ID(必填 >0;字典 `t_expenditure.id`)。
  44. ExpenditureID int64 `json:"expenditureId" validate:"required,gt=0"`
  45. // Money 金额(元字符串,如 "100.00";必填且 > 0)。
  46. Money string `json:"money" validate:"required"`
  47. // Remark 备注(可空;超长 → 2002)。
  48. Remark string `json:"remark" validate:"max=2000"`
  49. }
  50. // expenditureLogView 收支记录行(契约 D 表 60 的 9 个字段)。
  51. //
  52. // 字段 → 来源列(**逐列溯源**,契约 §3.4 ③ 门槛):
  53. //
  54. // id ← t_expenditure_log.id
  55. // bizNo ← biz_no
  56. // expenditureId ← expenditure_id
  57. // type ← t_expenditure.type(**字典**;记录表无此列)
  58. // name ← t_expenditure.name(**字典**;记录表无此列)
  59. // money ← cost(金额,恒 2 位小数字符串)
  60. // createUserId ← create_user_id(可空 → `null`;**记账人**,不是收付对象)
  61. // campusId ← campus_id
  62. // createTime ← create_time
  63. //
  64. // 契约里**不再有** `direction` / `userId`(理由见文件头 ①④)。
  65. //
  66. // 字典未命中时的取值口径:`type=0`、`name=""`(**不编"未知类型"**——造的值追不到列,
  67. // 且前端按 `type` 分组时会被这个假值带偏)。
  68. type expenditureLogView struct {
  69. // ID 记录 ID。
  70. ID int64 `json:"id"`
  71. // BizNo 业务单号(对外唯一标识;不可变)。
  72. BizNo string `json:"bizNo"`
  73. // ExpenditureID 收支类型 ID(编辑表单回填用)。
  74. ExpenditureID int64 `json:"expenditureId"`
  75. // Type 收支方向(1 支出 / 2 收入;来自字典)。
  76. Type int32 `json:"type"`
  77. // Name 收支类型名称(来自字典)。
  78. Name string `json:"name"`
  79. // Money 金额(元,恒 2 位小数字符串)。
  80. Money string `json:"money"`
  81. // CreateUserID 记账人 ULID(可空 → `null`)。
  82. CreateUserID *string `json:"createUserId"`
  83. // CampusID 校区 ID(0 = 机构级)。
  84. CampusID int64 `json:"campusId"`
  85. // CreateTime 记账时间(`create_time`)。
  86. CreateTime time.Time `json:"createTime"`
  87. }
  88. // expenditureTypeView 收支字典项(契约:`types:[{id, type, name, parentId}]`)。
  89. //
  90. // 字段 → 来源列:`id` ← id;`type` ← type;`name` ← name;`parentId` ← parent_id(0 = 顶级)。
  91. // **不含 `institutionId`**:契约没给,且前端也用不上(平台通用字典与机构字典在同一个下拉里)。
  92. type expenditureTypeView struct {
  93. // ID 类型 ID。
  94. ID int64 `json:"id"`
  95. // Type 收支方向(1 支出 / 2 收入)。
  96. Type int32 `json:"type"`
  97. // Name 类型名称。
  98. Name string `json:"name"`
  99. // ParentID 父类型 ID(0 = 顶级;自引用树)。
  100. ParentID int64 `json:"parentId"`
  101. }
  102. // expenditureTypesResp 字典响应包裹(契约形状是 `{"types":[...]}`,不是裸数组)。
  103. type expenditureTypesResp struct {
  104. // Types 字典项(含平台通用字典,按 type/parentId/id 递增)。
  105. Types []expenditureTypeView `json:"types"`
  106. }
  107. // createdExpenditureLogView 记账响应(契约:POST → `{id, bizNo}`)。
  108. //
  109. // 只回这两个字段是**有意**的:金额/类型/时间都已经由请求确定,回显没有信息量;
  110. // 而 `bizNo` 是前端后续对账/查单的唯一 handle。
  111. type createdExpenditureLogView struct {
  112. // ID 新记录 ID。
  113. ID int64 `json:"id"`
  114. // BizNo 新记录业务单号。
  115. BizNo string `json:"bizNo"`
  116. }