HealthReportDraftService.java 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. package com.etotem.cfc.service;
  2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
  3. import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
  4. import com.etotem.cfc.entity.HealthReportDraft;
  5. import com.etotem.cfc.mapper.HealthReportDraftMapper;
  6. import lombok.extern.slf4j.Slf4j;
  7. import org.springframework.stereotype.Service;
  8. import javax.annotation.Resource;
  9. import java.util.Date;
  10. /**
  11. * 健康报告草稿服务 — 两阶段入库的中间态管理。
  12. * 草稿保存时间默认 24 小时,过期后 confirm 接口拒绝。
  13. */
  14. @Slf4j
  15. @Service
  16. public class HealthReportDraftService {
  17. /** 草稿有效期(毫秒) */
  18. public static final long DRAFT_TTL_MS = 72L * 60 * 60 * 1000;
  19. @Resource
  20. private HealthReportDraftMapper draftMapper;
  21. /**
  22. * 创建草稿。返回带 id 的实体。
  23. */
  24. public HealthReportDraft createDraft(Long userId, Long familyId, String reportType,
  25. String fileUrl, String originalFilename, String payloadJson) {
  26. return createDraft(userId, familyId, reportType, fileUrl, originalFilename, payloadJson, null);
  27. }
  28. /**
  29. * 创建草稿(含文件哈希,用于重复上传检测)。
  30. */
  31. public HealthReportDraft createDraft(Long userId, Long familyId, String reportType,
  32. String fileUrl, String originalFilename,
  33. String payloadJson, String fileHash) {
  34. return createDraft(userId, familyId, reportType, fileUrl, originalFilename, payloadJson, fileHash, null);
  35. }
  36. /**
  37. * 创建草稿(含文件哈希 + 归属成员ID,A2/DAN 自动采集必需)。
  38. */
  39. public HealthReportDraft createDraft(Long userId, Long familyId, String reportType,
  40. String fileUrl, String originalFilename,
  41. String payloadJson, String fileHash, Long memberId) {
  42. HealthReportDraft draft = new HealthReportDraft();
  43. draft.setUserId(userId);
  44. draft.setFamilyId(familyId);
  45. draft.setMemberId(memberId);
  46. draft.setReportType(reportType);
  47. draft.setFileUrl(fileUrl);
  48. draft.setOriginalFilename(originalFilename);
  49. draft.setPayloadJson(payloadJson);
  50. draft.setFileHash(fileHash);
  51. draft.setStatus("pending");
  52. Date now = new Date();
  53. draft.setCreatedAt(now);
  54. draft.setUpdatedAt(now);
  55. draft.setExpireAt(new Date(now.getTime() + DRAFT_TTL_MS));
  56. draftMapper.insert(draft);
  57. log.info("草稿已创建: draftId={}, userId={}, reportType={}, memberId={}, fileHash={}",
  58. draft.getId(), userId, reportType, memberId, fileHash);
  59. return draft;
  60. }
  61. /**
  62. * 按主键查询草稿(无视状态/过期,供异步采集等内部逻辑使用)。
  63. */
  64. public HealthReportDraft getDraftById(Long draftId) {
  65. return draftMapper.selectById(draftId);
  66. }
  67. /**
  68. * 查询草稿(仅返回 pending 且未过期的)。
  69. */
  70. public HealthReportDraft getActiveDraft(Long draftId) {
  71. HealthReportDraft draft = draftMapper.selectById(draftId);
  72. if (draft == null) {
  73. return null;
  74. }
  75. if (!"pending".equals(draft.getStatus())) {
  76. return null;
  77. }
  78. if (draft.getExpireAt() != null && draft.getExpireAt().before(new Date())) {
  79. // expire_at 可能被 ON UPDATE CURRENT_TIMESTAMP 在解析写回时重置;
  80. // 仍以 created_at + TTL 为准,避免刚解析完的草稿被误判过期。
  81. Date created = draft.getCreatedAt();
  82. if (created == null || System.currentTimeMillis() - created.getTime() > DRAFT_TTL_MS) {
  83. return null;
  84. }
  85. log.warn("草稿 expire_at 已过期但 created_at 仍在 TTL 内,放行: draftId={}, expireAt={}, createdAt={}",
  86. draftId, draft.getExpireAt(), created);
  87. }
  88. return draft;
  89. }
  90. /**
  91. * 标记草稿已确认(成功入库后调用)。
  92. */
  93. public void markConfirmed(Long draftId, Long reportId) {
  94. LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
  95. uw.eq(HealthReportDraft::getId, draftId)
  96. .set(HealthReportDraft::getStatus, "confirmed")
  97. .set(HealthReportDraft::getConfirmedReportId, reportId)
  98. .set(HealthReportDraft::getUpdatedAt, new Date());
  99. draftMapper.update(null, uw);
  100. }
  101. /**
  102. * 标记草稿已丢弃。
  103. */
  104. public void markDiscarded(Long draftId) {
  105. LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
  106. uw.eq(HealthReportDraft::getId, draftId)
  107. .set(HealthReportDraft::getStatus, "discarded")
  108. .set(HealthReportDraft::getUpdatedAt, new Date());
  109. draftMapper.update(null, uw);
  110. }
  111. /**
  112. * 清理过期草稿(可由定时任务调用)。
  113. */
  114. public int purgeExpired() {
  115. LambdaQueryWrapper<HealthReportDraft> qw = new LambdaQueryWrapper<>();
  116. qw.lt(HealthReportDraft::getExpireAt, new Date())
  117. .eq(HealthReportDraft::getStatus, "pending");
  118. int count = draftMapper.selectCount(qw).intValue();
  119. if (count > 0) {
  120. draftMapper.delete(qw);
  121. log.info("已清理 {} 条过期草稿", count);
  122. }
  123. return count;
  124. }
  125. /**
  126. * 更新草稿载荷 JSON(解析完成后写入)
  127. */
  128. public void updatePayload(Long draftId, String payloadJson) {
  129. Date now = new Date();
  130. LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
  131. uw.eq(HealthReportDraft::getId, draftId)
  132. .set(HealthReportDraft::getPayloadJson, payloadJson)
  133. .set(HealthReportDraft::getExpireAt, new Date(now.getTime() + DRAFT_TTL_MS))
  134. .set(HealthReportDraft::getUpdatedAt, now);
  135. draftMapper.update(null, uw);
  136. }
  137. public void updateNote(Long draftId, String note) {
  138. LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
  139. uw.eq(HealthReportDraft::getId, draftId)
  140. .set(HealthReportDraft::getRemark, note)
  141. .set(HealthReportDraft::getUpdatedAt, new Date());
  142. draftMapper.update(null, uw);
  143. }
  144. /**
  145. * 标记异步解析状态(collecting/completed/failed)
  146. */
  147. public void updateParseStatus(Long draftId, String parseStatus) {
  148. LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
  149. uw.eq(HealthReportDraft::getId, draftId)
  150. .set(HealthReportDraft::getParseStatus, parseStatus)
  151. .set(HealthReportDraft::getUpdatedAt, new Date());
  152. draftMapper.update(null, uw);
  153. }
  154. /**
  155. * 查询某用户仍处于"采集中"的草稿(供报告列表顶部展示进度)
  156. */
  157. public java.util.List<HealthReportDraft> listCollectingByUser(Long userId) {
  158. if (userId == null) {
  159. return java.util.Collections.emptyList();
  160. }
  161. LambdaQueryWrapper<HealthReportDraft> qw = new LambdaQueryWrapper<>();
  162. qw.eq(HealthReportDraft::getUserId, userId)
  163. .eq(HealthReportDraft::getParseStatus, "collecting")
  164. .orderByDesc(HealthReportDraft::getCreatedAt)
  165. .last("LIMIT 20");
  166. return draftMapper.selectList(qw);
  167. }
  168. /**
  169. * 异步解析开始时记录解析方式
  170. */
  171. public void markCollecting(Long draftId, String parseMethod) {
  172. LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
  173. uw.eq(HealthReportDraft::getId, draftId)
  174. .set(HealthReportDraft::getParseStatus, "collecting")
  175. .set(HealthReportDraft::getParseMethod, parseMethod)
  176. .set(HealthReportDraft::getParseError, null)
  177. .set(HealthReportDraft::getUpdatedAt, new Date());
  178. draftMapper.update(null, uw);
  179. }
  180. /**
  181. * 解析方式确定后单独更新(用于异步流程:先置 collecting,指纹判定后写具体方式)
  182. */
  183. public void updateParseMethod(Long draftId, String parseMethod) {
  184. LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
  185. uw.eq(HealthReportDraft::getId, draftId)
  186. .set(HealthReportDraft::getParseMethod, parseMethod)
  187. .set(HealthReportDraft::getUpdatedAt, new Date());
  188. draftMapper.update(null, uw);
  189. }
  190. /**
  191. * 报告类型变更后单独更新(DAN 识别完成:draft.reportType 置为 dan_a2,前端按此跳转报告页)
  192. */
  193. public void updateReportType(Long draftId, String reportType) {
  194. LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
  195. uw.eq(HealthReportDraft::getId, draftId)
  196. .set(HealthReportDraft::getReportType, reportType)
  197. .set(HealthReportDraft::getUpdatedAt, new Date());
  198. draftMapper.update(null, uw);
  199. }
  200. /**
  201. * 异步解析失败记录错误
  202. */
  203. public void markCollectFailed(Long draftId, String error) {
  204. LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
  205. String safe = error != null && error.length() > 2000 ? error.substring(0, 2000) : error;
  206. uw.eq(HealthReportDraft::getId, draftId)
  207. .set(HealthReportDraft::getParseStatus, "failed")
  208. .set(HealthReportDraft::getParseError, safe)
  209. .set(HealthReportDraft::getUpdatedAt, new Date());
  210. draftMapper.update(null, uw);
  211. }
  212. /**
  213. * 异步解析完成 + 已入库,写入状态与关联报告ID
  214. */
  215. public void markCollectCompleted(Long draftId, Long reportId) {
  216. LambdaUpdateWrapper<HealthReportDraft> uw = new LambdaUpdateWrapper<>();
  217. uw.eq(HealthReportDraft::getId, draftId)
  218. .set(HealthReportDraft::getParseStatus, "completed")
  219. .set(HealthReportDraft::getParseError, null)
  220. .set(HealthReportDraft::getStatus, "confirmed")
  221. .set(HealthReportDraft::getConfirmedReportId, reportId)
  222. .set(HealthReportDraft::getUpdatedAt, new Date());
  223. draftMapper.update(null, uw);
  224. }
  225. }