Jelajahi Sumber

fix(health-report): P1-1 报告日期可修改·前端修复2 - 日期改用最小 payload,杜绝三表存量列被清

问题:submitDateChange 在 payload 缺 indicators/gutFlora/diseaseRisks 时,用详情接口的
三表数据回填成非空 List,触发 updateReportFromPayload 三个「先删后插」块;而这三处
insert 从不 set collectionDate / description / relatedBacteria → 老报告(无 payload_json)
只想改个日期,却把三列存量数据永久清掉。

修复:发送日期专用最小 payload {type, summary:{reportDate}},刻意不带那三个键。
后端对缺键已有 instanceof List 保护,缺键即整块跳过「先删后插」→ 三张明细表零 SQL。
summary 复制一份再覆盖 reportDate:payload.summary 与编辑态日期输入框 v-model 双向
绑定,直接改会把未保存/已失败的日期写进组件状态。

updateReportFromPayload 缺键审计(payload 只含 {type, summary} 时逐处取值):
| 位置      | 取值                              | 行为                                                | 结论 |
| L1817     | payload.get("summary")            | 是 Map → scoreSource = summary                       | 安全 |
| L1822-39  | scoreSource.get(10 个分数键)      | 缺失→null→toInteger→null→setter 有 !=null 守卫      | 不写空值 |
| L1840     | scoreSource.get("shannonIndex")   | null → 跳过                                          | 安全 |
| L1847     | scoreSource.get("reportDate")     | 有值→严格解析;非法/未来抛 IllegalArgumentException  | 正常 |
| L1869     | payload.get("indicators")         | null → 非 List → 跳过 delete+insert                  | 零 SQL |
| L1893     | payload.get("gutFlora")           | null → 跳过                                          | 零 SQL |
| L1919     | payload.get("diseaseRisks")       | null → 跳过                                          | 零 SQL |
| L1990     | writeValueAsString(payload)       | payload_json 快照覆写为最小 payload(health_report 单行 UPDATE) | 观察项* |
| L2010/20  | reportBlockAssembler.fromMap()    | 只读 summary+diseaseRisks,toInt/str 均 null 安全    | 不 NPE |
| L2022     | normalizeIndicatorPipeline        | indicators/gutFlora 为 null → 循环不执行(只 upsert,从不 delete) | 零 SQL |

scoreSource 结论:summary 存在时从 summary 取分数;最小 payload 的 summary 只覆盖
reportDate、沿用原有分数键 → 分数保留,缺失键返回 null 且全部有守卫,不会 NPE、
不会误删、不会误写空值。

实测(临时单测 TempDateOnlyPayloadAuditTest,跑完即删不提交,2 项 PASS):
1) 最小 payload → healthIndicator/GutFlora/DiseaseRisk 三个 mapper 的 delete 与 insert
   均 never 被调用(一条 SQL 都不执行),overallScore=88 保留,reportDate 正确更新;
2) 反向对照 → payload 带上那三个键(哪怕空 List)时 delete 必然被调用,
   证明零风险来自「缺键即跳过」而非其它巧合。

观察项*:step6 会把 health_report.payload_json 覆写为只含 summary 的最小快照;
step7 从 fromMap 结果重装 report_blocks(fromMap 本就不读 indicators/gutFlora,属 edit
流既有行为;最小 payload 无分数时 blocks 为空 → reportBlockService.save 空列表早退,
老报告 report_blocks 保持不变)。二者均为 updateReportFromPayload 对「提交了什么
payload」的既有处理,本次不改后端,对三张明细表无任何 SQL。

附:lastDetail 仍由 loadData 缓存,本次不再用于回填,未删除以免扩大改动面。
iwt 2 minggu lalu
induk
melakukan
2133cfaebd
1 mengubah file dengan 12 tambahan dan 19 penghapusan
  1. 12 19
      cfc-frontend/pages/health/report-detail.vue

+ 12 - 19
cfc-frontend/pages/health/report-detail.vue

@@ -1139,27 +1139,20 @@ export default {
         uni.showToast({ title: '请输入报告日期', icon: 'none' })
         uni.showToast({ title: '请输入报告日期', icon: 'none' })
         return
         return
       }
       }
-      // 组装保存用 payload:沿用查看态已加载的 payload 快照,仅覆盖 reportDate。
-      // 若老报告无 payload_json 快照(后端 detail 不返回 payload),用详情接口返回的三张表数据回填,
-      // 避免只改日期却把指标/菌群/风险清空。
-      var payload = self.payload
-      if (!payload.summary) payload.summary = {}
-      payload.summary.reportDate = val
-      var detail = self.lastDetail
-      if (detail) {
-        if ((!payload.indicators || !payload.indicators.length) && detail.indicators && detail.indicators.length) {
-          payload.indicators = detail.indicators
-        }
-        if ((!payload.gutFlora || !payload.gutFlora.length) && detail.gutFlora && detail.gutFlora.length) {
-          payload.gutFlora = detail.gutFlora
-        }
-        if ((!payload.diseaseRisks || !payload.diseaseRisks.length) && detail.diseaseRisks && detail.diseaseRisks.length) {
-          payload.diseaseRisks = detail.diseaseRisks
-        }
-      }
+      // 日期专用最小 payload:只带 summary(沿用现有分数等值,仅覆盖 reportDate)。
+      // 刻意不带 indicators/gutFlora/diseaseRisks —— 后端 updateReportFromPayload 对缺失键
+      // 已有 instanceof List 保护,缺键即跳过整表「先删后插」,避免误清 collectionDate /
+      // description / relatedBacteria 等 payload 快照不含的列(若回填这三张表,insert 不会
+      // 写这些列 → 存量数据被永久清掉)。
+      var srcSummary = (self.payload && self.payload.summary) ? self.payload.summary : {}
+      // 复制一份再改:payload.summary 与编辑态日期输入框 v-model 双向绑定,直接改会把
+      // 未保存的日期写进组件状态,保存失败后编辑态会带出错误日期。
+      var summary = Object.assign({}, srcSummary)
+      summary.reportDate = val
+      var dateOnlyPayload = { type: self.payload.type || 'gut_flora', summary: summary }
       var subjectId = self.selectedMemberId || self.createdMemberId || null
       var subjectId = self.selectedMemberId || self.createdMemberId || null
       uni.showLoading({ title: '保存中...' })
       uni.showLoading({ title: '保存中...' })
-      editHealthReport(self.reportId, payload, subjectId).then(function(res) {
+      editHealthReport(self.reportId, dateOnlyPayload, subjectId).then(function(res) {
         uni.hideLoading()
         uni.hideLoading()
         if (res.code === 200) {
         if (res.code === 200) {
           self.dateEditing = false
           self.dateEditing = false