|
@@ -327,10 +327,12 @@ public class HealthReportController {
|
|
|
*/
|
|
*/
|
|
|
@Operation(summary = "获取健康报告列表")
|
|
@Operation(summary = "获取健康报告列表")
|
|
|
@PostMapping("/report/list")
|
|
@PostMapping("/report/list")
|
|
|
- public Result<List<HealthReport>> listReports(
|
|
|
|
|
|
|
+ public Result<Object> listReports(
|
|
|
@RequestBody Map<String, Object> params,
|
|
@RequestBody Map<String, Object> params,
|
|
|
@RequestAttribute("userId") Long userId,
|
|
@RequestAttribute("userId") Long userId,
|
|
|
@RequestAttribute(value = "familyId", required = false) Long familyId) {
|
|
@RequestAttribute(value = "familyId", required = false) Long familyId) {
|
|
|
|
|
+ // B3-4:分页支持——page 不传时行为与现一致(返回 List,兼容小程序端);page 传时返回分页结构。
|
|
|
|
|
+ int page = params.get("page") != null ? Integer.parseInt(params.get("page").toString()) : 0;
|
|
|
// 返回条数:显式传 size 则截取前 size 条;不传则返回全部分页列表
|
|
// 返回条数:显式传 size 则截取前 size 条;不传则返回全部分页列表
|
|
|
int size = params.get("size") != null ? Integer.parseInt(params.get("size").toString()) : 0;
|
|
int size = params.get("size") != null ? Integer.parseInt(params.get("size").toString()) : 0;
|
|
|
Long memberId = ParamUtils.getLong(params.get("memberId"));
|
|
Long memberId = ParamUtils.getLong(params.get("memberId"));
|
|
@@ -369,6 +371,27 @@ public class HealthReportController {
|
|
|
// 合并 DAN 报告(已确认入库的)到列表,保证报告列表页能看到 DAN 报告
|
|
// 合并 DAN 报告(已确认入库的)到列表,保证报告列表页能看到 DAN 报告
|
|
|
List<HealthReport> merged = mergeDanReports(reports, memberId, familyId, visibleForDan, userId);
|
|
List<HealthReport> merged = mergeDanReports(reports, memberId, familyId, visibleForDan, userId);
|
|
|
|
|
|
|
|
|
|
+ if (page > 0) {
|
|
|
|
|
+ // B3-4:分页返回结构(对齐项目分页惯例,与 /report/admin-list 的 records/total 口径一致);
|
|
|
|
|
+ // teacher 端代传历史页已按 res.data.records / res.data.total 读取。
|
|
|
|
|
+ int pageSize = size > 0 ? size : 20;
|
|
|
|
|
+ int total = merged.size();
|
|
|
|
|
+ int from = (page - 1) * pageSize;
|
|
|
|
|
+ if (from > total) {
|
|
|
|
|
+ from = total;
|
|
|
|
|
+ }
|
|
|
|
|
+ int to = Math.min(from + pageSize, total);
|
|
|
|
|
+ List<HealthReport> records = from < total
|
|
|
|
|
+ ? new ArrayList<>(merged.subList(from, to)) : new ArrayList<>();
|
|
|
|
|
+ healthReportService.fillSubjectAndProxyNames(records);
|
|
|
|
|
+ Map<String, Object> pageResult = new java.util.HashMap<>();
|
|
|
|
|
+ pageResult.put("records", records);
|
|
|
|
|
+ pageResult.put("total", total);
|
|
|
|
|
+ pageResult.put("current", page);
|
|
|
|
|
+ pageResult.put("size", pageSize);
|
|
|
|
|
+ pageResult.put("pages", (int) Math.ceil((double) total / pageSize));
|
|
|
|
|
+ return Result.success(pageResult);
|
|
|
|
|
+ }
|
|
|
if (size > 0) {
|
|
if (size > 0) {
|
|
|
// B1-6:列表补展示字段(报告对象姓名 / 代上传人姓名),实体上的 @TableField(exist=false) 字段
|
|
// B1-6:列表补展示字段(报告对象姓名 / 代上传人姓名),实体上的 @TableField(exist=false) 字段
|
|
|
List<HealthReport> limited = limitLatest(merged, size);
|
|
List<HealthReport> limited = limitLatest(merged, size);
|
|
@@ -2725,9 +2748,17 @@ public class HealthReportController {
|
|
|
*
|
|
*
|
|
|
* <p>T29-B1-1:重构为与「两阶段入库」一致的模式——先建草稿拿到 draftId,再解析并回写草稿载荷,
|
|
* <p>T29-B1-1:重构为与「两阶段入库」一致的模式——先建草稿拿到 draftId,再解析并回写草稿载荷,
|
|
|
* 前端「确认入库」才能拿到 draftId(原先透传 LangGraph 原始返回,恒无 draftId → 前端卡死)。</p>
|
|
* 前端「确认入库」才能拿到 draftId(原先透传 LangGraph 原始返回,恒无 draftId → 前端卡死)。</p>
|
|
|
- * <p>链路:storeFile → createDraft → updateNote(备注) → getFilePath → LangGraph 解析
|
|
|
|
|
|
|
+ * <p>链路:storeFile → createDraft → updateRemark(备注) → getFilePath → LangGraph 解析
|
|
|
* (失败回退本地 Java 解析)→ 返回 draftId/payload/成员匹配/提取字段。</p>
|
|
* (失败回退本地 Java 解析)→ 返回 draftId/payload/成员匹配/提取字段。</p>
|
|
|
*
|
|
*
|
|
|
|
|
+ * <p><b>B3-2 草稿过期降级:</b>入参支持 <code>file</code>(新上传,multipart)或
|
|
|
|
|
+ * <code>fileUrl</code>(草稿过期后基于原文件重新解析,无需重新上传 PDF)。二者至少给一个,
|
|
|
|
|
+ * 复用本接口同一条「建草稿 → 解析」链路,不另建新接口。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>B3-3 本地兜底提示:</b>LangGraph 不可用而走本地 Java 解析时,返回体带
|
|
|
|
|
+ * <code>parseFallback=true</code> / <code>parseMethod="local"</code>,供前端提示
|
|
|
|
|
+ * 「解析服务暂不可用,已用本地解析」。</p>
|
|
|
|
|
+ *
|
|
|
* <p><b>T29-B2-6 权限编排(三层校验,各层失败返回码):</b>
|
|
* <p><b>T29-B2-6 权限编排(三层校验,各层失败返回码):</b>
|
|
|
* <ol>
|
|
* <ol>
|
|
|
* <li>① 角色校验 {@link #isProxyUploader}:非 admin/teacher/approved 营养师/approved 规划师
|
|
* <li>① 角色校验 {@link #isProxyUploader}:非 admin/teacher/approved 营养师/approved 规划师
|
|
@@ -2743,7 +2774,8 @@ public class HealthReportController {
|
|
|
@Operation(summary = "代上传报告(预览)")
|
|
@Operation(summary = "代上传报告(预览)")
|
|
|
@PostMapping("/report/proxy-parse")
|
|
@PostMapping("/report/proxy-parse")
|
|
|
public Result<Map<String, Object>> proxyParsePreview(
|
|
public Result<Map<String, Object>> proxyParsePreview(
|
|
|
- @RequestParam("file") MultipartFile file,
|
|
|
|
|
|
|
+ @RequestParam(value = "file", required = false) MultipartFile file,
|
|
|
|
|
+ @RequestParam(value = "fileUrl", required = false) String fileUrl,
|
|
|
@RequestParam(value = "familyId", required = false) Long familyId,
|
|
@RequestParam(value = "familyId", required = false) Long familyId,
|
|
|
@RequestParam(value = "subjectId", required = false) Long subjectId,
|
|
@RequestParam(value = "subjectId", required = false) Long subjectId,
|
|
|
@RequestParam(value = "reportType", defaultValue = "auto") String reportType,
|
|
@RequestParam(value = "reportType", defaultValue = "auto") String reportType,
|
|
@@ -2753,8 +2785,11 @@ public class HealthReportController {
|
|
|
@RequestAttribute("roles") List<String> roles,
|
|
@RequestAttribute("roles") List<String> roles,
|
|
|
HttpServletRequest request) {
|
|
HttpServletRequest request) {
|
|
|
|
|
|
|
|
- if (file.isEmpty()) {
|
|
|
|
|
- return Result.error("文件不能为空");
|
|
|
|
|
|
|
+ // B3-2:两种入参二选一——file(新上传,multipart)或 fileUrl(草稿过期后基于原文件重新解析)
|
|
|
|
|
+ boolean hasFile = file != null && !file.isEmpty();
|
|
|
|
|
+ boolean hasFileUrl = fileUrl != null && !fileUrl.trim().isEmpty();
|
|
|
|
|
+ if (!hasFile && !hasFileUrl) {
|
|
|
|
|
+ return Result.error("请选择PDF文件或提供fileUrl");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
User user = userMapper.selectById(userId);
|
|
User user = userMapper.selectById(userId);
|
|
@@ -2772,17 +2807,22 @@ public class HealthReportController {
|
|
|
return Result.error("无权为该家庭代传报告");
|
|
return Result.error("无权为该家庭代传报告");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- String contentType = file.getContentType();
|
|
|
|
|
- String originalFilename = file.getOriginalFilename();
|
|
|
|
|
- String lcName = originalFilename != null ? originalFilename.toLowerCase() : "";
|
|
|
|
|
- if (!lcName.endsWith(".pdf")
|
|
|
|
|
- && (contentType == null || !contentType.equalsIgnoreCase("application/pdf"))) {
|
|
|
|
|
- return Result.error("仅支持PDF文件");
|
|
|
|
|
|
|
+ // 新上传路径才校验 PDF 类型(fileUrl 重新解析路径的文件已在首次上传时校验过)
|
|
|
|
|
+ if (hasFile) {
|
|
|
|
|
+ String contentType = file.getContentType();
|
|
|
|
|
+ String originalFilename = file.getOriginalFilename();
|
|
|
|
|
+ String lcName = originalFilename != null ? originalFilename.toLowerCase() : "";
|
|
|
|
|
+ if (!lcName.endsWith(".pdf")
|
|
|
|
|
+ && (contentType == null || !contentType.equalsIgnoreCase("application/pdf"))) {
|
|
|
|
|
+ return Result.error("仅支持PDF文件");
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- // ① 存文件(本地磁盘 / 京东云 OSS)
|
|
|
|
|
- String fileUrl = storageService.storeFile(file, "health-reports");
|
|
|
|
|
|
|
+ // ① 确定文件访问地址:新上传 → storeFile 存本地/OSS;fileUrl → 复用既有文件
|
|
|
|
|
+ boolean reparse = !hasFile && hasFileUrl;
|
|
|
|
|
+ String resolvedFileUrl = hasFile ? storageService.storeFile(file, "health-reports") : fileUrl.trim();
|
|
|
|
|
+ String originalFilename = hasFile ? file.getOriginalFilename() : null;
|
|
|
|
|
|
|
|
// ② 先建草稿:保证预览必然有 draftId 供 /report/proxy-confirm 入库
|
|
// ② 先建草稿:保证预览必然有 draftId 供 /report/proxy-confirm 入库
|
|
|
// reportType 传 auto 时落 gut_flora(与非代传链路一致)
|
|
// reportType 传 auto 时落 gut_flora(与非代传链路一致)
|
|
@@ -2792,15 +2832,15 @@ public class HealthReportController {
|
|
|
? "gut_flora" : reportType;
|
|
? "gut_flora" : reportType;
|
|
|
String emptyPayloadJson = objectMapper.writeValueAsString(new ParsedReportPayload.Payload());
|
|
String emptyPayloadJson = objectMapper.writeValueAsString(new ParsedReportPayload.Payload());
|
|
|
HealthReportDraft draft = healthReportDraftService.createDraft(
|
|
HealthReportDraft draft = healthReportDraftService.createDraft(
|
|
|
- userId, familyId, draftReportType, fileUrl, originalFilename, emptyPayloadJson);
|
|
|
|
|
|
|
+ userId, familyId, draftReportType, resolvedFileUrl, originalFilename, emptyPayloadJson);
|
|
|
|
|
|
|
|
- // ③ 备注写入草稿 remark(updateNote 实际写 remark 列),确认入库时同步到 proxy_upload_note
|
|
|
|
|
|
|
+ // ③ 备注写入草稿 remark(B3-6:updateRemark 实际写 remark 列),确认入库时同步到 proxy_upload_note
|
|
|
if (note != null && !note.isEmpty()) {
|
|
if (note != null && !note.isEmpty()) {
|
|
|
- healthReportDraftService.updateNote(draft.getId(), note);
|
|
|
|
|
|
|
+ healthReportDraftService.updateRemark(draft.getId(), note);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ④ LangGraph 需要本地路径:经 getFilePath 下载/定位到共享卷
|
|
// ④ LangGraph 需要本地路径:经 getFilePath 下载/定位到共享卷
|
|
|
- String absolutePath = storageService.getFilePath(fileUrl);
|
|
|
|
|
|
|
+ String absolutePath = storageService.getFilePath(resolvedFileUrl);
|
|
|
|
|
|
|
|
// ⑤ 解析(LangGraph 优先,失败回退本地 Java 解析)
|
|
// ⑤ 解析(LangGraph 优先,失败回退本地 Java 解析)
|
|
|
Map<String, Object> data = parseProxyDraftFile(draft, absolutePath, userId, familyId, subjectId);
|
|
Map<String, Object> data = parseProxyDraftFile(draft, absolutePath, userId, familyId, subjectId);
|
|
@@ -2815,9 +2855,10 @@ public class HealthReportController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ⑥ 返回体补代传相关字段(前端既有字段契约)
|
|
// ⑥ 返回体补代传相关字段(前端既有字段契约)
|
|
|
- data.put("fileUrl", fileUrl);
|
|
|
|
|
|
|
+ data.put("fileUrl", resolvedFileUrl);
|
|
|
data.put("proxyUploaderId", userId);
|
|
data.put("proxyUploaderId", userId);
|
|
|
data.put("proxyUploadNote", note);
|
|
data.put("proxyUploadNote", note);
|
|
|
|
|
+ data.put("reparse", reparse);
|
|
|
return Result.success(data);
|
|
return Result.success(data);
|
|
|
|
|
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
@@ -2845,6 +2886,7 @@ public class HealthReportController {
|
|
|
if (lgData != null) {
|
|
if (lgData != null) {
|
|
|
Map<String, Object> data = buildParseResultFromLg(lgData, draft, familyId).getData();
|
|
Map<String, Object> data = buildParseResultFromLg(lgData, draft, familyId).getData();
|
|
|
putExtractedReportMeta(data, lgData);
|
|
putExtractedReportMeta(data, lgData);
|
|
|
|
|
+ markParseMethod(data, false, "langgraph");
|
|
|
return data;
|
|
return data;
|
|
|
}
|
|
}
|
|
|
log.warn("代传解析:LangGraph 无有效返回,回退本地 Java 解析 draftId={}", draft.getId());
|
|
log.warn("代传解析:LangGraph 无有效返回,回退本地 Java 解析 draftId={}", draft.getId());
|
|
@@ -2853,13 +2895,33 @@ public class HealthReportController {
|
|
|
draft.getId(), e.getMessage());
|
|
draft.getId(), e.getMessage());
|
|
|
}
|
|
}
|
|
|
try {
|
|
try {
|
|
|
- return parseLocalFallback(absolutePath, draft, familyId, userId).getData();
|
|
|
|
|
|
|
+ Map<String, Object> data = parseLocalFallback(absolutePath, draft, familyId, userId).getData();
|
|
|
|
|
+ if (data != null) {
|
|
|
|
|
+ // B3-3:本地兜底成功 → 前端提示「解析服务暂不可用,已用本地解析」
|
|
|
|
|
+ markParseMethod(data, true, "local");
|
|
|
|
|
+ }
|
|
|
|
|
+ return data;
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
log.error("代传解析:本地 Java 解析失败 draftId={}", draft.getId(), e);
|
|
log.error("代传解析:本地 Java 解析失败 draftId={}", draft.getId(), e);
|
|
|
return null;
|
|
return null;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * B3-3:标注本次解析走 LangGraph 还是本地 Java 兜底,供前端差异化提示。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param data 预览结果体(非 null)
|
|
|
|
|
+ * @param fallback 是否走了本地 Java 兜底
|
|
|
|
|
+ * @param method 解析方式标识(langgraph / local)
|
|
|
|
|
+ */
|
|
|
|
|
+ private void markParseMethod(Map<String, Object> data, boolean fallback, String method) {
|
|
|
|
|
+ if (data == null) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ data.put("parseFallback", fallback);
|
|
|
|
|
+ data.put("parseMethod", method);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 补齐 LangGraph 预览结果的 extractedReportNumber / extractedReportDate。
|
|
* 补齐 LangGraph 预览结果的 extractedReportNumber / extractedReportDate。
|
|
|
* buildParseResultFromLg 只回填 extractedName/Gender/Age,而前端预览页三行
|
|
* buildParseResultFromLg 只回填 extractedName/Gender/Age,而前端预览页三行
|