|
|
@@ -20,8 +20,10 @@ import com.etotem.cfc.entity.HealthReport;
|
|
|
import com.etotem.cfc.entity.HealthReportDraft;
|
|
|
import com.etotem.cfc.entity.NutritionDeficiencyRecord;
|
|
|
import com.etotem.cfc.entity.NutritionIndicatorMapping;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
import com.etotem.cfc.mapper.HealthReportDraftMapper;
|
|
|
import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
import com.etotem.cfc.service.FamilyMemberNutritionProfileService;
|
|
|
import com.etotem.cfc.service.DimensionScoreService;
|
|
|
import com.etotem.cfc.service.FoodRecommendService;
|
|
|
@@ -127,6 +129,9 @@ public class HealthReportController {
|
|
|
@Resource
|
|
|
private WechatServiceInterface wechatService;
|
|
|
|
|
|
+ @Resource
|
|
|
+ private UserMapper userMapper;
|
|
|
+
|
|
|
/**
|
|
|
* 创建健康报告(含指标明细)
|
|
|
*/
|
|
|
@@ -1560,4 +1565,189 @@ public class HealthReportController {
|
|
|
log.info("报告文件已保存: {}", relativePath);
|
|
|
return relativePath;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查是否为授权代上传角色
|
|
|
+ */
|
|
|
+ private boolean isProxyUploader(String role, List<String> roles, User user) {
|
|
|
+ if ("teacher".equals(role)) return true;
|
|
|
+ if (roles != null && roles.contains("teacher")) return true;
|
|
|
+ if (user != null && "approved".equals(user.getNutritionistStatus())) return true;
|
|
|
+ if (user != null && "planner".equals(user.getVendorType()) && "approved".equals(user.getVendorStatus())) return true;
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 规划师/营养师代用户上传报告(预览模式,仅解析不入库)
|
|
|
+ */
|
|
|
+ @Operation(summary = "代上传报告(预览)")
|
|
|
+ @PostMapping("/report/proxy-parse")
|
|
|
+ public Result<Map<String, Object>> proxyParsePreview(
|
|
|
+ @RequestParam("file") MultipartFile file,
|
|
|
+ @RequestParam(value = "familyId", required = false) Long familyId,
|
|
|
+ @RequestParam(value = "subjectId", required = false) Long subjectId,
|
|
|
+ @RequestParam(value = "reportType", defaultValue = "auto") String reportType,
|
|
|
+ @RequestParam(value = "note", required = false) String note,
|
|
|
+ @RequestAttribute("userId") Long userId,
|
|
|
+ @RequestAttribute("role") String role,
|
|
|
+ @RequestAttribute("roles") List<String> roles,
|
|
|
+ HttpServletRequest request) {
|
|
|
+
|
|
|
+ if (file.isEmpty()) {
|
|
|
+ return Result.error("文件不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ User user = userMapper.selectById(userId);
|
|
|
+ if (!isProxyUploader(role, roles, user)) {
|
|
|
+ return Result.error("无权限代上传报告,仅规划师/营养师可操作");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (familyId == null) {
|
|
|
+ return Result.error("familyId不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ String absolutePath = saveUploadFileForProxy(file, userId);
|
|
|
+
|
|
|
+ // 调用 LangGraph 解析
|
|
|
+ Map<String, Object> result = callLangGraphParse(absolutePath, userId, familyId, subjectId);
|
|
|
+
|
|
|
+ if (result == null) {
|
|
|
+ return Result.error("报告解析失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存草稿
|
|
|
+ Long draftId = (Long) result.get("draftId");
|
|
|
+ if (draftId != null && note != null) {
|
|
|
+ healthReportDraftService.updateNote(draftId, note);
|
|
|
+ }
|
|
|
+
|
|
|
+ result.put("proxyUploaderId", userId);
|
|
|
+ result.put("proxyUploadNote", note);
|
|
|
+ return Result.success(result);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("代上传解析失败", e);
|
|
|
+ return Result.error("解析失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 规划师/营养师代确认报告草稿(入库)
|
|
|
+ */
|
|
|
+ @Operation(summary = "代确认报告(入库)")
|
|
|
+ @PostMapping("/report/proxy-confirm")
|
|
|
+ public Result<Map<String, Object>> proxyConfirm(
|
|
|
+ @RequestBody Map<String, Object> params,
|
|
|
+ @RequestAttribute("userId") Long userId,
|
|
|
+ @RequestAttribute("role") String role,
|
|
|
+ @RequestAttribute("roles") List<String> roles) {
|
|
|
+
|
|
|
+ User user = userMapper.selectById(userId);
|
|
|
+ if (!isProxyUploader(role, roles, user)) {
|
|
|
+ return Result.error("无权限");
|
|
|
+ }
|
|
|
+
|
|
|
+ Long draftId = params.get("draftId") != null
|
|
|
+ ? Long.valueOf(params.get("draftId").toString()) : null;
|
|
|
+ Long subjectId = params.get("subjectId") != null
|
|
|
+ ? Long.valueOf(params.get("subjectId").toString()) : null;
|
|
|
+
|
|
|
+ if (draftId == null) {
|
|
|
+ return Result.error("draftId不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 代上传模式跳过草稿归属校验
|
|
|
+ Map<String, Object> result = healthReportService.auditConfirm(draftId, subjectId, userId);
|
|
|
+ Long reportId = (Long) result.get("reportId");
|
|
|
+
|
|
|
+ // 设置代上传信息
|
|
|
+ if (reportId != null) {
|
|
|
+ healthReportService.setProxyUploader(reportId, userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 触发 AI 解读
|
|
|
+ if (reportId != null && subjectId != null) {
|
|
|
+ triggerAiAnalysis(reportId, subjectId, userId);
|
|
|
+ }
|
|
|
+
|
|
|
+ return Result.success(result);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("代确认失败", e);
|
|
|
+ return Result.error("确认失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 代上传场景:保存文件(用代上传人目录)
|
|
|
+ */
|
|
|
+ private String saveUploadFileForProxy(MultipartFile file, Long proxyUserId) throws IOException {
|
|
|
+ String uploadDir = "uploads" + File.separator + "health-reports-proxy" + File.separator + proxyUserId;
|
|
|
+ File dir = new File(uploadDir);
|
|
|
+ if (!dir.exists()) {
|
|
|
+ dir.mkdirs();
|
|
|
+ }
|
|
|
+ String originalFilename = file.getOriginalFilename();
|
|
|
+ if (originalFilename == null) originalFilename = "report.pdf";
|
|
|
+ String extension = originalFilename.substring(originalFilename.lastIndexOf('.'));
|
|
|
+ String savedFilename = System.currentTimeMillis() + "_" + UUID.randomUUID().toString().substring(0, 8) + extension;
|
|
|
+ Path filePath = Paths.get(uploadDir, savedFilename);
|
|
|
+ Files.copy(file.getInputStream(), filePath);
|
|
|
+ return uploadDir.replace("\\", "/") + "/" + savedFilename;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 调用 LangGraph 解析报告
|
|
|
+ */
|
|
|
+ private Map<String, Object> callLangGraphParse(String absolutePath, Long userId, Long familyId, Long subjectId) {
|
|
|
+ try {
|
|
|
+ Map<String, Object> lgRequest = new HashMap<>();
|
|
|
+ lgRequest.put("file_path", absolutePath);
|
|
|
+ lgRequest.put("user_id", userId);
|
|
|
+ lgRequest.put("family_id", familyId);
|
|
|
+
|
|
|
+ ResponseEntity<Map> lgResponse = restTemplate.postForEntity(
|
|
|
+ langgraphBaseUrl + "/api/v1/report/parse", lgRequest, Map.class);
|
|
|
+ Map<String, Object> lgBody = lgResponse.getBody();
|
|
|
+ if (lgBody != null && Integer.valueOf(200).equals(lgBody.get("code"))) {
|
|
|
+ Map<String, Object> lgData = (Map<String, Object>) lgBody.get("data");
|
|
|
+ if (lgData != null) {
|
|
|
+ return lgData;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("LangGraph 解析失败,回退: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 触发 AI 解读
|
|
|
+ */
|
|
|
+ private void triggerAiAnalysis(Long reportId, Long subjectId, Long proxyUploaderId) {
|
|
|
+ try {
|
|
|
+ Map<String, Object> analyzeReq = new HashMap<>();
|
|
|
+ analyzeReq.put("report_id", reportId);
|
|
|
+ analyzeReq.put("user_id", subjectId);
|
|
|
+ analyzeReq.put("focus", "代上传报告解读");
|
|
|
+
|
|
|
+ ResponseEntity<Map> resp = restTemplate.postForEntity(
|
|
|
+ langgraphBaseUrl + "/api/v1/analyze", analyzeReq, Map.class);
|
|
|
+ if (resp.getBody() != null) {
|
|
|
+ Map<String, Object> body = resp.getBody();
|
|
|
+ String analysis = (String) body.get("analysis");
|
|
|
+ if (analysis != null && !analysis.isEmpty()) {
|
|
|
+ Map<String, Object> aiData = new HashMap<>();
|
|
|
+ aiData.put("analysis", analysis);
|
|
|
+ aiData.put("traceId", body.get("trace_id"));
|
|
|
+ aiData.put("proxyUploaderId", proxyUploaderId);
|
|
|
+ healthReportService.setAiAnalysis(reportId, aiData);
|
|
|
+ log.info("AI解读已保存: reportId={}, traceId={}", reportId, body.get("trace_id"));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("AI解读触发失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|