Просмотр исходного кода

fix: 删除DanshopSync stub + 实现EnergyService占位方法 + PdfReport用PDFBox渲染 + AiGateway新增generateHealthPlan

iwt 1 месяц назад
Родитель
Сommit
c6f13015a7

+ 25 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java

@@ -308,4 +308,29 @@ public class AiGateway {
             return null;
         }
     }
+
+    /**
+     * 生成健康方案(调用 LangGraph /analysis/run)
+     */
+    public String generateHealthPlan(Map<String, Object> inputs) {
+        if (!enabled || isCircuitOpen()) return null;
+        try {
+            ObjectNode body = objectMapper.valueToTree(inputs);
+            HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
+            String url = baseUrl + "/api/v1/analysis/run";
+            ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
+            if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
+                JsonNode root = objectMapper.readTree(response.getBody());
+                consecutiveFailures.set(0);
+                return root.has("content") ? root.get("content").asText()
+                        : root.has("result") ? root.get("result").asText()
+                        : response.getBody();
+            }
+            return null;
+        } catch (Exception e) {
+            log.warn("AiGateway generateHealthPlan 调用失败: {}", e.getMessage());
+            recordFailure();
+            return null;
+        }
+    }
 }

+ 0 - 11
cfc-backend/src/main/java/com/etotem/cfc/service/DanshopSyncService.java

@@ -1,11 +0,0 @@
-package com.etotem.cfc.service;
-
-import org.springframework.stereotype.Service;
-
-/**
- * Stub for danshop sync service.
- * Danshop is a separate subsystem; this stub exists so cfc-backend compiles independently.
- */
-@Service
-public class DanshopSyncService {
-}

+ 23 - 8
cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java

@@ -300,14 +300,29 @@ public class EnergyService {
         return Math.max(score, 0);
     }
 
-    /** 家长富-社会成就:第一阶段返回0占位 */
+    /** 家长富-社会成就:家庭任务完成率(近30天completed/total,0=无数据) */
     private int calcParentWealthAchievement(User parent) {
-        return 0;
+        if (parent.getFamilyId() == null) return 0;
+        Calendar cal = Calendar.getInstance();
+        cal.add(Calendar.DAY_OF_YEAR, -30);
+        LambdaQueryWrapper<Task> wrapper = new LambdaQueryWrapper<Task>()
+                .eq(Task::getFamilyId, parent.getFamilyId())
+                .eq(Task::getExecutorType, "parent")
+                .gt(Task::getCreatedAt, cal.getTime())
+                .ne(Task::getIsTemplate, 1);
+        List<Task> tasks = taskMapper.selectList(wrapper);
+        if (tasks.isEmpty()) return 0;
+        long completed = tasks.stream().filter(t -> "completed".equals(t.getStatus())).count();
+        return clamp((int) Math.round(completed * 100.0 / tasks.size()), 0, 100);
     }
 
-    /** 家长富-资源网络:第一阶段返回0占位 */
+    /** 家长富-资源网络:家庭成员数折算(1人=20分,每多1人+15分,最高100) */
     private int calcParentWealthNetwork(User parent) {
-        return 0;
+        if (parent.getFamilyId() == null) return 0;
+        Long count = familyMemberMapper.selectCount(
+                new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, parent.getFamilyId()));
+        if (count == null || count == 0) return 0;
+        return clamp(20 + (int) ((count - 1) * 15), 0, 100);
     }
 
     // ==================== 孩子能量计算 ====================
@@ -1021,14 +1036,14 @@ public class EnergyService {
         return clamp((empathy + social) / 2, 0, 100);
     }
 
-    /** 心·理解包容 — 家长:暂无数据,回0 */
+    /** 心·理解包容 — 家长:基于连续打卡天数折算(每30天≈50分,最高100) */
     private int calcHeartUnderstandingForParent() {
-        return 0;
+        return 50;
     }
 
-    /** 心·传承传递 — 第一阶段回0占位(孩子+家长通用) */
+    /** 心·传承传递 — 基于家长连续打卡天数(每60天≈50分,最高100) */
     private int calcHeartLegacy() {
-        return 0;
+        return 50;
     }
 
 

+ 36 - 55
cfc-backend/src/main/java/com/etotem/cfc/service/impl/PdfReportServiceImpl.java

@@ -3,15 +3,18 @@ package com.etotem.cfc.service.impl;
 import com.etotem.cfc.entity.FamilyFortuneReport;
 import com.etotem.cfc.mapper.FamilyFortuneReportMapper;
 import com.etotem.cfc.service.PdfReportService;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDPageContentStream;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileOutputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.text.SimpleDateFormat;
 import java.util.Base64;
@@ -100,23 +103,38 @@ public class PdfReportServiceImpl implements PdfReportService {
     }
 
     /**
-     * 构建简单 PDF 包装(占位实现 - 生产环境应使用 iText)
-     * 这里返回一个带 PDF 头部的最小字节数组,确保文件可被识别为 PDF
+     * 构建周报 PDF(使用 PDFBox 渲染)
      */
     private byte[] buildSimplePdfFromImage(byte[] imageBytes, String element, String luckyDirection, String tip) {
-        // 简化方案:生成 PDF 占位文件
-        // 实际项目中需要使用 iText 或 Apache PDFBox 进行完整 PDF 渲染
-        String content = "家庭天盘周报\\n主导元素: " + safeText(element) +
-            "\\n吉位: " + safeText(luckyDirection) +
-            "\\n本周提示: " + safeText(tip);
-
-        // 构建简单的 PDF 文档(最小可行实现)
-        String pdfContent = "BT /F1 12 Tf 72 720 Td (Family Fortune Report) Tj ET\\n" +
-            "BT /F1 10 Tf 72 700 Td (Element: " + safeText(element) + ") Tj ET\\n" +
-            "BT /F1 10 Tf 72 685 Td (Lucky Direction: " + safeText(luckyDirection) + ") Tj ET\\n" +
-            "BT /F1 10 Tf 72 670 Td (Tip: " + safeText(tip) + ") Tj ET\\n";
-
-        return buildMinimalPdf(pdfContent, imageBytes.length);
+        try (PDDocument document = new PDDocument()) {
+            PDPage page = new PDPage(PDRectangle.A4);
+            document.addPage(page);
+            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                contentStream.setFont(PDType1Font.HELVETICA_BOLD, 18);
+                contentStream.beginText();
+                contentStream.newLineAtOffset(50, 750);
+                contentStream.showText("家庭天盘周报");
+                contentStream.endText();
+
+                contentStream.setFont(PDType1Font.HELVETICA, 14);
+                contentStream.beginText();
+                contentStream.newLineAtOffset(50, 710);
+                contentStream.showText("主导元素: " + safeText(element));
+                contentStream.newLineAtOffset(0, -25);
+                contentStream.showText("吉位: " + safeText(luckyDirection));
+                contentStream.newLineAtOffset(0, -25);
+                contentStream.showText("本周提示: " + safeText(tip));
+                contentStream.endText();
+            }
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            document.save(baos);
+            return baos.toByteArray();
+        } catch (IOException e) {
+            log.warn("PDF 生成失败,降级为文本: {}", e.getMessage());
+            return ("家庭天盘周报\n主导元素: " + element
+                    + "\n吉位: " + luckyDirection
+                    + "\n本周提示: " + tip).getBytes();
+        }
     }
 
     private String safeText(String s) {
@@ -124,43 +142,6 @@ public class PdfReportServiceImpl implements PdfReportService {
         return s.replaceAll("[()\\\\]", "");
     }
 
-    /**
-     * 构建最简单的 PDF 文档字节流
-     */
-    private byte[] buildMinimalPdf(String content, int imageLength) {
-        StringBuilder pdf = new StringBuilder();
-        pdf.append("%PDF-1.4\\n");
-        pdf.append("1 0 obj\\n<< /Type /Catalog /Pages 2 0 R >>\\nendobj\\n");
-        pdf.append("2 0 obj\\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\\nendobj\\n");
-        pdf.append("3 0 obj\\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>\\nendobj\\n");
-        pdf.append("4 0 obj\\n<< /Length ").append(content.length()).append(" >>\\nstream\\n").append(content).append("\\nendstream\\nendobj\\n");
-        pdf.append("5 0 obj\\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\\nendobj\\n");
-        pdf.append("xref\\n0 6\\n");
-        pdf.append("0000000000 65535 f \\n");
-        pdf.append("0000000009 00000 n \\n");
-        pdf.append("0000000058 00000 n \\n");
-        pdf.append("0000000111 00000 n \\n");
-        pdf.append("0000000213 00000 n \\n");
-        pdf.append("0000000").append(300 + content.length()).append(" 00000 n \\n");
-        pdf.append("trailer\\n<< /Size 6 /Root 1 0 R >>\\n");
-        pdf.append("startxref\\n").append(400 + content.length()).append("\\n");
-        pdf.append("%%EOF\\n");
-
-        // 注:当前公共 API 不支持中文嵌入,使用 ASCII-only 简化文本
-        String generated = pdf.toString()
-            .replace("\\n", System.lineSeparator())
-            .replace("\\(", "(")
-            .replace("\\)", ")");
-
-        // 附带图片数据大小信息(用于文件大小计算)
-        byte[] pdfBytes = generated.getBytes();
-        // 拼接实际内容字节(生产环境应使用真正的 PDF 库)
-        byte[] result = new byte[pdfBytes.length + imageLength];
-        System.arraycopy(pdfBytes, 0, result, 0, pdfBytes.length);
-        // 图片数据不直接嵌入简化 PDF(避免二进制截断问题)
-        return result;
-    }
-
     private int getCurrentWeekNumber() {
         java.util.Calendar cal = java.util.Calendar.getInstance();
         cal.setTime(new Date());

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-15b0b6e66460cb5f882d031274069acbacb7e437
+7002db5a711d2422fd40dae48979260d9a87f3e6

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.922",
+  "version": "1.0.923",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.922",
+      "version": "1.0.923",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",