Эх сурвалжийг харах

fix: mock模式VIP自动激活 + List.of->Arrays.asList兼容Java8

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
liaoxg 3 сар өмнө
parent
commit
69fcbc44f3

+ 85 - 74
num-server/src/main/java/com/etotem/num/controller/PayController.java

@@ -1,75 +1,86 @@
-package com.etotem.num.controller;
-
-import com.etotem.num.common.Result;
-import com.etotem.num.entity.Order;
-import com.etotem.num.service.OrderService;
-import com.etotem.num.service.UserService;
-import com.etotem.num.service.WxPayService;
-import javax.servlet.http.HttpServletRequest;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
-
-import java.util.HashMap;
-import java.util.Map;
-
-@RestController
-@RequestMapping("/api/pay")
-public class PayController {
-
-    private final OrderService orderService;
-    private final UserService userService;
-    private final WxPayService wxPayService;
-
-    public PayController(OrderService orderService, UserService userService, WxPayService wxPayService) {
-        this.orderService = orderService;
-        this.userService = userService;
-        this.wxPayService = wxPayService;
-    }
-
-    @PostMapping("/create")
-    public Result<?> createOrder(HttpServletRequest request, @RequestBody Map<String, Object> body) {
-        Long userId = (Long) request.getAttribute("userId");
-        int totalFee = ((Number) body.get("totalFee")).intValue();
-        String payType = (String) body.get("payType");
-        String productType = (String) body.get("productType");
-        Boolean isUpgrade = body.get("isUpgrade") != null ? (Boolean) body.get("isUpgrade") : false;
-        Boolean isSeedPrice = body.get("isSeedPrice") != null ? (Boolean) body.get("isSeedPrice") : false;
-
-        // 1. Create order in DB
-        Order order = orderService.createOrder(userId, totalFee, payType, productType, isUpgrade, isSeedPrice);
-
-        // 2. Get WeChat Pay unified order params (mock in dev mode)
-        Map<String, String> payParams = wxPayService.createUnifiedOrder(
-                userId, totalFee, "数字能量学VIP会员", order.getOutTradeNo());
-
-        // 3. Return combined response
-        Map<String, Object> result = new HashMap<>();
-        result.put("orderId", order.getId());
-        result.put("outTradeNo", order.getOutTradeNo());
-        result.put("totalFee", order.getTotalFee());
-
-        boolean isMock = payParams.containsKey("package") &&
-                payParams.get("package").contains("mock_prepay");
-        if (isMock) {
-            result.put("mock", true);
+package com.etotem.num.controller;
+
+import com.etotem.num.common.Result;
+import com.etotem.num.entity.Order;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.etotem.num.service.OrderService;
+import com.etotem.num.service.UserService;
+import com.etotem.num.service.WxPayService;
+import javax.servlet.http.HttpServletRequest;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/pay")
+public class PayController {
+
+    private static final Logger log = LoggerFactory.getLogger(PayController.class);
+
+    private final OrderService orderService;
+    private final UserService userService;
+    private final WxPayService wxPayService;
+
+    public PayController(OrderService orderService, UserService userService, WxPayService wxPayService) {
+        this.orderService = orderService;
+        this.userService = userService;
+        this.wxPayService = wxPayService;
+    }
+
+    @PostMapping("/create")
+    public Result<?> createOrder(HttpServletRequest request, @RequestBody Map<String, Object> body) {
+        Long userId = (Long) request.getAttribute("userId");
+        int totalFee = ((Number) body.get("totalFee")).intValue();
+        String payType = (String) body.get("payType");
+        String productType = (String) body.get("productType");
+        Boolean isUpgrade = body.get("isUpgrade") != null ? (Boolean) body.get("isUpgrade") : false;
+        Boolean isSeedPrice = body.get("isSeedPrice") != null ? (Boolean) body.get("isSeedPrice") : false;
+
+        // 1. Create order in DB
+        Order order = orderService.createOrder(userId, totalFee, payType, productType, isUpgrade, isSeedPrice);
+
+        // 2. Get WeChat Pay unified order params (mock in dev mode)
+        Map<String, String> payParams = wxPayService.createUnifiedOrder(
+                userId, totalFee, "数字能量学VIP会员", order.getOutTradeNo());
+
+        // 3. Return combined response
+        Map<String, Object> result = new HashMap<>();
+        result.put("orderId", order.getId());
+        result.put("outTradeNo", order.getOutTradeNo());
+        result.put("totalFee", order.getTotalFee());
+
+boolean isMock = payParams.containsKey("package") &&
+                payParams.get("package").contains("mock_prepay");
+        if (isMock) {
+            result.put("mock", true);
+            log.info("Mock mode detected, auto-activating VIP for order: {}", order.getOutTradeNo());
+            try {
+                orderService.paySuccess(order.getOutTradeNo());
+                log.info("VIP activation completed for order: {}", order.getOutTradeNo());
+            } catch (Exception e) {
+                log.error("VIP activation FAILED for order: {}, error: {}", order.getOutTradeNo(), e.getMessage(), e);
+            }
         } else {
-            result.put("prepayId", payParams.get("package"));
-        }
-        result.put("timeStamp", payParams.get("timeStamp"));
-        result.put("nonceStr", payParams.get("nonceStr"));
-        result.put("package", payParams.get("package"));
-        result.put("signType", payParams.get("signType"));
-        result.put("paySign", payParams.get("paySign"));
-
-        return Result.success(result);
-    }
-
-    @PostMapping("/notify")
-    public Result<?> payNotify(@RequestBody Map<String, String> body) {
-        String outTradeNo = body.get("outTradeNo");
-        orderService.paySuccess(outTradeNo);
-        return Result.success(null);
-    }
-}
+            result.put("prepayId", payParams.get("package"));
+        }
+        result.put("timeStamp", payParams.get("timeStamp"));
+        result.put("nonceStr", payParams.get("nonceStr"));
+        result.put("package", payParams.get("package"));
+        result.put("signType", payParams.get("signType"));
+        result.put("paySign", payParams.get("paySign"));
+
+        return Result.success(result);
+    }
+
+    @PostMapping("/notify")
+    public Result<?> payNotify(@RequestBody Map<String, String> body) {
+        String outTradeNo = body.get("outTradeNo");
+        orderService.paySuccess(outTradeNo);
+        return Result.success(null);
+    }
+}

+ 737 - 500
num-server/src/main/java/com/etotem/num/service/DifyService.java

@@ -1,500 +1,737 @@
-package com.etotem.num.service;
-
-import com.google.gson.Gson;
-import com.google.gson.JsonObject;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.HttpEntity;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.MediaType;
-import org.springframework.stereotype.Service;
-import org.springframework.web.client.RestTemplate;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-@Service
-public class DifyService {
-
-    @Value("${num.dify.base-url}")
-    private String baseUrl;
-
-    @Value("${num.dify.api-key}")
-    private String apiKey;
-
-    private final RestTemplate restTemplate;
-    private final Gson gson;
-    private final ConfigService configService;
-
-    public DifyService(RestTemplate restTemplate, Gson gson, ConfigService configService) {
-        this.restTemplate = restTemplate;
-        this.gson = gson;
-        this.configService = configService;
-    }
-
-    /**
-     * Check whether Dify is configured with real credentials.
-     * Returns true if api-key or base-url are still placeholders.
-     */
-    public boolean isMockMode() {
-        return apiKey == null || apiKey.isEmpty() || apiKey.contains("your_")
-                || baseUrl == null || baseUrl.isEmpty() || baseUrl.contains("your-");
-    }
-
-    /**
-     * Invoke Dify chatflow, falling back to mock response when unavailable.
-     */
-    public String invokeChatflow(String query, String chartContext, String userId) {
-        if (isMockMode()) {
-            return generateMockResponse(chartContext, query);
-        }
-        try {
-            JsonObject body = new JsonObject();
-            body.addProperty("query", query);
-            body.addProperty("user", userId);
-            body.add("inputs", gson.toJsonTree(Collections.singletonMap("chart_context", chartContext)));
-
-            HttpHeaders headers = new HttpHeaders();
-            headers.set("Authorization", "Bearer " + apiKey);
-            headers.setContentType(MediaType.APPLICATION_JSON);
-            HttpEntity<String> entity = new HttpEntity<>(body.toString(), headers);
-
-            String response = restTemplate.postForObject(baseUrl + "/chat-messages", entity, String.class);
-            JsonObject json = gson.fromJson(response, JsonObject.class);
-            return json.get("answer").getAsString();
-        } catch (Exception e) {
-            // Fallback to mock when real Dify call fails
-            return generateMockResponse(chartContext, query);
-        }
-    }
-
-    /**
-     * Invoke Dify Workflow (for structured interpretation).
-     * Uses POST /v1/workflows/run endpoint.
-     * Returns the workflow outputs as JSON string.
-     */
-    public String invokeWorkflow(Map<String, Object> inputs, String userId) {
-        if (isMockMode()) {
-            return generateMockInterpretation(inputs);
-        }
-        try {
-            String workflowAppId = configService.getValue("dify.workflow.app_id", "");
-            JsonObject body = new JsonObject();
-            body.addProperty("app_id", workflowAppId);
-            body.add("inputs", gson.toJsonTree(inputs));
-            body.addProperty("user", userId);
-
-            HttpHeaders headers = new HttpHeaders();
-            headers.set("Authorization", "Bearer " + apiKey);
-            headers.setContentType(MediaType.APPLICATION_JSON);
-            HttpEntity<String> entity = new HttpEntity<>(body.toString(), headers);
-
-            String response = restTemplate.postForObject(baseUrl + "/workflows/run", entity, String.class);
-            JsonObject json = gson.fromJson(response, JsonObject.class);
-            JsonObject data = json.getAsJsonObject("data");
-            if (data != null && data.has("outputs")) {
-                return data.get("outputs").toString();
-            }
-            return json.toString();
-        } catch (Exception e) {
-            return generateMockInterpretation(inputs);
-        }
-    }
-
-    /**
-     * Generate a structured mock interpretation JSON from chart context.
-     * Sections: mainCharacter, left (21-40), top (41-60), right (61+).
-     */
-    @SuppressWarnings("unchecked")
-    String generateMockInterpretation(Map<String, Object> inputs) {
-        try {
-            String chartContext = (String) inputs.get("chart_context");
-            JsonObject chart = gson.fromJson(chartContext, JsonObject.class);
-            JsonObject pos = chart.getAsJsonObject("positions");
-            if (pos == null) pos = chart;
-
-            int O = getInt(pos, "O", 7);
-            int P = getInt(pos, "P", 5);
-            int Q = getInt(pos, "Q", 0);
-            int R = getInt(pos, "R", 0);
-            int V = getInt(pos, "V", 8);
-            int W = getInt(pos, "W", 0);
-            int X = getInt(pos, "X", 0);
-            int S = getInt(pos, "S", 9);
-            int T = getInt(pos, "T", 0);
-            int U = getInt(pos, "U", 0);
-
-            JsonObject result = new JsonObject();
-
-            // 主性格解读
-            JsonObject mainCharacter = new JsonObject();
-            mainCharacter.addProperty("number", O);
-            mainCharacter.addProperty("title", "主性格解读");
-            mainCharacter.addProperty("content", generateMainInterpretation(O));
-            result.add("mainCharacter", mainCharacter);
-
-            // 左区(21-40岁)
-            JsonObject left = new JsonObject();
-            left.addProperty("number", P);
-            left.addProperty("title", "左区(21-40岁)");
-            left.addProperty("content", generateZoneInterpretation("左区(青年发展期)", P, Q, R));
-            result.add("left", left);
-
-            // 顶部(41-60岁)
-            JsonObject top = new JsonObject();
-            top.addProperty("number", V);
-            top.addProperty("title", "顶部(41-60岁)");
-            top.addProperty("content", generateZoneInterpretation("顶部(中年发展期)", V, W, X));
-            result.add("top", top);
-
-            // 右区(61+岁)
-            JsonObject right = new JsonObject();
-            right.addProperty("number", S);
-            right.addProperty("title", "右区(61+岁)");
-            right.addProperty("content", generateZoneInterpretation("右区(成熟发展期)", S, T, U));
-            result.add("right", right);
-
-            return result.toString();
-        } catch (Exception e) {
-            return "{\"mainCharacter\":{\"number\":7,\"title\":\"主性格解读\",\"content\":\"请查看完整命盘以获取详细解读。\"}}";
-        }
-    }
-
-    // ── Academic orientation (US-9.1) ───────────────────────────────────────────────
-
-    public String invokeAcademicWorkflow(Map<String, Object> inputs, String userId) {
-        if (isMockMode()) {
-            return generateMockAcademicInterpretation(inputs);
-        }
-        try {
-            String workflowAppId = configService.getValue("dify.workflow.app_id", "");
-            JsonObject body = new JsonObject();
-            body.addProperty("app_id", workflowAppId);
-            body.add("inputs", gson.toJsonTree(inputs));
-            body.addProperty("user", userId);
-
-            HttpHeaders headers = new HttpHeaders();
-            headers.set("Authorization", "Bearer " + apiKey);
-            headers.setContentType(MediaType.APPLICATION_JSON);
-            HttpEntity<String> entity = new HttpEntity<>(body.toString(), headers);
-
-            String response = restTemplate.postForObject(baseUrl + "/workflows/run", entity, String.class);
-            JsonObject json = gson.fromJson(response, JsonObject.class);
-            JsonObject data = json.getAsJsonObject("data");
-            if (data != null && data.has("outputs")) {
-                return data.get("outputs").toString();
-            }
-            return json.toString();
-        } catch (Exception e) {
-            return generateMockAcademicInterpretation(inputs);
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    String generateMockAcademicInterpretation(Map<String, Object> inputs) {
-        try {
-            String chartContext = (String) inputs.get("chart_context");
-            JsonObject chart = gson.fromJson(chartContext, JsonObject.class);
-            JsonObject positions = chart.getAsJsonObject("positions");
-            if (positions == null) positions = chart;
-
-            int O = getInt(positions, "O", 7);
-            int P = getInt(positions, "P", 5);
-            int V = getInt(positions, "V", 8);
-            int S = getInt(positions, "S", 9);
-            int I = getInt(positions, "I", 0);
-            int J = getInt(positions, "J", 0);
-            int K = getInt(positions, "K", 0);
-            int L = getInt(positions, "L", 0);
-
-            JsonObject result = new JsonObject();
-
-            // 天赋倾向
-            JsonObject talentTendency = new JsonObject();
-            talentTendency.addProperty("title", "天赋倾向");
-            talentTendency.addProperty("content", generateAcademicTalent(O, I, J, K, L));
-            result.add("talentTendency", talentTendency);
-
-            // 适合方向
-            JsonObject suitableDirection = new JsonObject();
-            suitableDirection.addProperty("title", "适合方向");
-            suitableDirection.addProperty("content", "根据命盘数字组合分析,以下是不同方向的匹配度参考:");
-            Map<String, Integer> percentages = generateDirectionMatchPercentages(O, P, V, S);
-            JsonObject matchJson = new Gson().toJsonTree(percentages).getAsJsonObject();
-            suitableDirection.add("matchPercentages", matchJson);
-            result.add("suitableDirection", suitableDirection);
-
-            // 学习特征
-            JsonObject learningStyle = new JsonObject();
-            learningStyle.addProperty("title", "学习特征");
-            learningStyle.addProperty("content", generateLearningStyle(I, J, K, L, P, V, S));
-            result.add("learningStyle", learningStyle);
-
-            // 亲子沟通建议
-            JsonObject parentingAdvice = new JsonObject();
-            parentingAdvice.addProperty("title", "亲子沟通建议");
-            parentingAdvice.addProperty("content", generateParentingAdvice(O));
-            result.add("parentingAdvice", parentingAdvice);
-
-            // 关键期提醒
-            JsonObject keyPeriods = new JsonObject();
-            keyPeriods.addProperty("title", "关键期提醒");
-            keyPeriods.addProperty("content", generateKeyPeriods(V, getInt(positions, "W", 0), getInt(positions, "X", 0)));
-            result.add("keyPeriods", keyPeriods);
-
-            return result.toString();
-        } catch (Exception e) {
-            JsonObject fallback = new JsonObject();
-            fallback.addProperty("title", "学业方向分析");
-            fallback.addProperty("content", "命盘分析完成。请使用完整功能获取详细学业方向建议。");
-            JsonObject wrapped = new JsonObject();
-            wrapped.add("talentTendency", fallback);
-            return wrapped.toString();
-        }
-    }
-
-    private String generateAcademicTalent(int O, int I, int J, int K, int L) {
-        String mainDesc = NUMBER_DESC.getOrDefault(O > 9 ? 0 : O,
-                O > 9 ? "卓越数能量,蕴含强大潜力" : "能量独特,需结合整体命盘解读");
-        StringBuilder sb = new StringBuilder();
-        sb.append("孩子的主性格数字为 ").append(O).append("(").append(mainDesc).append(")。\n\n");
-        sb.append("内在基础数字:");
-        sb.append("日(").append(I).append(") ").append("月(").append(J).append(") ");
-        sb.append("年前半(").append(K).append(") ").append("年后半(").append(L).append(")").append("\n\n");
-        sb.append("这些数字共同构成了孩子的天赋底色:\n");
-        sb.append("• 主性格数字反映核心特质与驱动力\n");
-        sb.append("• 内在基础数字揭示潜意识需求与安全感来源\n");
-        sb.append("建议结合具体年龄阶段观察和引导,避免标签化。");
-        return sb.toString();
-    }
-
-    private Map<String, Integer> generateDirectionMatchPercentages(int O, int P, int V, int S) {
-        Map<String, Integer> map = new LinkedHashMap<>();
-        int humanities = 50 + (P==2?10: P==4?15: P==7?12: P==1?-5: P==5?5: 0)
-                      + (V==2?10: V==4?15: V==7?12: V==1?-5: V==5?5: 0)
-                      + (S==2?10: S==4?15: S==7?12: S==1?-5: S==5?5: 0);
-        int science = 50 + (P==1?12: P==5?10: P==8?15: P==2?-5: P==4?8: 0)
-                      + (V==1?12: V==5?10: V==8?15: V==2?-5: V==4?8: 0)
-                      + (S==1?12: S==5?10: S==8?15: S==2?-5: S==4?8: 0);
-        int arts = 50 + (P==3?15: P==6?10: P==9?5: 0)
-                   + (V==3?15: V==6?10: V==9?5: 0)
-                   + (S==3?15: S==6?10: S==9?5: 0);
-        int sports = 50 + (P==1?10: P==5?15: P==8?10: 0)
-                     + (V==1?10: V==5?15: V==8?10: 0)
-                     + (S==1?10: S==5?15: S==8?10: 0);
-        map.put("文科", Math.min(95, Math.max(5, humanities)));
-        map.put("理科", Math.min(95, Math.max(5, science)));
-        map.put("艺术", Math.min(95, Math.max(5, arts)));
-        map.put("体育", Math.min(95, Math.max(5, sports)));
-        return map;
-    }
-
-    private String generateLearningStyle(int I, int J, int K, int L, int P, int V, int S) {
-        StringBuilder sb = new StringBuilder();
-        sb.append("从内在基础数字(日、月、年前半、年后半)分析学习特征:\n\n");
-        String I_desc = NUMBER_DESC.getOrDefault(I > 9 ? 0 : I, "独特能量组合");
-        sb.append("• 外在表现(日 ").append(I).append("):").append(I_desc).append("。\n");
-        String J_desc = NUMBER_DESC.getOrDefault(J > 9 ? 0 : J, "独特能量组合");
-        sb.append("• 内在需求(月 ").append(J).append("):").append(J_desc).append("。\n");
-        String K_desc = NUMBER_DESC.getOrDefault(K > 9 ? 0 : K, "独特能量组合");
-        sb.append("• 早年家庭影响(年前半 ").append(K).append("):").append(K_desc).append("。\n");
-        String L_desc = NUMBER_DESC.getOrDefault(L > 9 ? 0 : L, "独特能量组合");
-        sb.append("• 后天发展潜力(年后半 ").append(L).append("):").append(L_desc).append("。\n\n");
-        sb.append("综合来看,建议注意:\n");
-        sb.append("1. 日与月的搭配体现孩子表里一致程度;\n");
-        sb.append("2. 年前半与年后半的关联揭示学习潜能的可塑性;\n");
-        sb.append("3. 发展阶段数字(左区").append(P).append("、顶部").append(V).append("、右区").append(S).append(")反映不同年龄段的精力投入方向。\n");
-        return sb.toString();
-    }
-
-    private String generateParentingAdvice(int O) {
-        String mainDesc = NUMBER_DESC.getOrDefault(O > 9 ? 0 : O,
-                O > 9 ? "卓越数能量,蕴含强大潜力" : "能量独特,需结合整体命盘解读");
-        StringBuilder sb = new StringBuilder();
-        sb.append("基于主性格数字 ").append(O).append(",亲子沟通建议:\n\n");
-        switch (O) {
-            case 1:
-                sb.append("• 尊重孩子的独立性,给予自主决策空间;\n");
-                sb.append("• 避免过度干涉,多用鼓励代替命令;\n");
-                break;
-            case 2:
-                sb.append("• 注重情感连接,营造安全和谐的家庭氛围;\n");
-                sb.append("• 多倾听少批评,培养合作意识;\n");
-                break;
-            case 3:
-                sb.append("• 鼓励创意表达,提供多样化活动体验;\n");
-                sb.append("• 避免刻板说教,用游戏化方式互动;\n");
-                break;
-            case 4:
-                sb.append("• 建立清晰规则,保持前后一致性;\n");
-                sb.append("• 提供结构化学习环境,循序渐进;\n");
-                break;
-            case 5:
-                sb.append("• 允许探索与试错,避免过度束缚;\n");
-                sb.append("• 多接触外界,满足好奇心;\n");
-                break;
-            case 6:
-                sb.append("• 给予充分关爱与责任感培养平衡;\n");
-                sb.append("• 参与家庭事务,增强归属感;\n");
-                break;
-            case 7:
-                sb.append("• 尊重思考空间,避免过多社交压力;\n");
-                sb.append("• 提供深度阅读与探索资源;\n");
-                break;
-            case 8:
-                sb.append("• 设定目标激励机制,培养领导力;\n");
-                sb.append("• 给予展示能力的机会;\n");
-                break;
-            case 9:
-                sb.append("• 培养同理心与社会责任感;\n");
-                sb.append("• 鼓励关怀他人,避免理想主义挫败;\n");
-                break;
-            default:
-                sb.append("• 因材施教,关注孩子独特天赋与短板;\n");
-                sb.append("• 保持耐心,接受成长过程的起伏;\n");
-        }
-        sb.append("\n以上建议仅供参考,每个孩子都是独一无二的个体。");
-        return sb.toString();
-    }
-
-    private String generateKeyPeriods(int V, int W, int X) {
-        StringBuilder sb = new StringBuilder();
-        sb.append("关键年龄参考(基于外部三区数字):\n\n");
-        sb.append("• 左区(青年发展期 21-40岁):主能量数字 ").append(V).append(" — 大学至立业阶段;\n");
-        sb.append("• 顶部(中年发展期 41-60岁):主能量数字 ").append(V).append(" — 事业高峰期;\n");
-        sb.append("• 右区(成熟发展期 61岁+):主能量数字 ").append(V).append(" — 人生总结期;\n\n");
-        sb.append("对于K12阶段:\n");
-        sb.append("• 18岁前重点关注[外在表现]日能量与[内在需求]月能量的平衡;\n");
-        sb.append("• 发展期数字变化可视为各年龄段精力重心;\n");
-        sb.append("建议结合具体命盘细节咨询能量师获取个性化方案。");
-        return sb.toString();
-    }
-
-    private String generateMainInterpretation(int number) {
-        String desc = NUMBER_DESC.getOrDefault(number,
-                number > 9 ? "卓越数能量,蕴含强大潜力" : "能量独特,需结合整体命盘解读");
-        return String.format(
-                "您的核心数字为 %d,代表「%s」。\n\n" +
-                "此数字奠定了您整体的性格基调与人生轨迹方向。" +
-                "在决策、人际和事业发展中,%d 的能量将持续发挥作用。" +
-                "建议深入了解此数字的优势与调整方向,以更好地发挥天赋潜能。",
-                number, desc, number);
-    }
-
-    private String generateZoneInterpretation(String zoneName, int a, int b, int c) {
-        String desc = NUMBER_DESC.getOrDefault(a,
-                a > 9 ? "卓越数能量" : "独特能量组合");
-        StringBuilder sb = new StringBuilder();
-        sb.append(zoneName).append("的能量数字为 ").append(a);
-        if (b != 0) sb.append(" → ").append(b);
-        if (c != 0) sb.append(" → ").append(c);
-        sb.append(",核心数字 ").append(a).append(" 代表「").append(desc).append("」。\n\n");
-        sb.append("此阶段您在以下方面将有明显体现:\n");
-        sb.append("• 个人成长与发展方向\n");
-        sb.append("• 人际关系与社交模式\n");
-        sb.append("• 事业机遇与挑战\n\n");
-        sb.append("建议关注数字 ").append(a).append(" 的正向特质,同时觉察其过度或不足带来的影响。");
-        return sb.toString();
-    }
-
-    // ── Mock response generation ──────────────────────────────────────────────
-
-    /** Number descriptions for mock interpretations. */
-    private static final Map<Integer, String> NUMBER_DESC = new HashMap<>();
-    static {
-        NUMBER_DESC.put(1, "独立、创造与领导力,开创性强,适合自主发展");
-        NUMBER_DESC.put(2, "合作、平衡与细腻感知,擅长沟通与协调");
-        NUMBER_DESC.put(3, "表达、创意与社交活力,富有艺术天赋");
-        NUMBER_DESC.put(4, "稳定、务实与秩序感,执行力强,脚踏实地");
-        NUMBER_DESC.put(5, "自由、变化与冒险精神,适应力强,多才多艺");
-        NUMBER_DESC.put(6, "责任、关爱与和谐追求,家庭观念重");
-        NUMBER_DESC.put(7, "分析、深度思考与灵性探索,求知欲强");
-        NUMBER_DESC.put(8, "权力、财富与成就导向,商业头脑出众");
-        NUMBER_DESC.put(9, "智慧、慈悲与完成之力,格局宏大");
-        NUMBER_DESC.put(11, "直觉敏锐、灵感充沛,拥有启蒙他人的天赋(卓越数)");
-        NUMBER_DESC.put(22, "将理想变为现实的建造者,执行力与远见并存(卓越数)");
-        NUMBER_DESC.put(33, "大爱无私、奉献疗愈,是精神层面的引领者(卓越数)");
-    }
-
-    private static final Map<String, String> ZONE_DESC = new HashMap<>();
-    static {
-        ZONE_DESC.put("left", "青年发展期(21-40岁)");
-        ZONE_DESC.put("top", "中年发展期(41-60岁)");
-        ZONE_DESC.put("right", "成熟发展期(61岁以上)");
-    }
-
-    /**
-     * Generate a structured mock initial interpretation from chart data.
-     */
-    @SuppressWarnings("unchecked")
-    String generateMockResponse(String chartContext, String query) {
-        try {
-            JsonObject chart = gson.fromJson(chartContext, JsonObject.class);
-            JsonObject pos = chart.getAsJsonObject("positions");
-            if (pos == null) pos = chart; // flat positions
-
-            int O = getInt(pos, "O", 0);
-            int I = getInt(pos, "I", 0);
-            int J = getInt(pos, "J", 0);
-            int K = getInt(pos, "K", 0);
-            int L = getInt(pos, "L", 0);
-            int P = getInt(pos, "P", 0);
-            int Q = getInt(pos, "Q", 0);
-            int R = getInt(pos, "R", 0);
-            int V = getInt(pos, "V", 0);
-            int W = getInt(pos, "W", 0);
-            int X = getInt(pos, "X", 0);
-            int S = getInt(pos, "S", 0);
-            int T = getInt(pos, "T", 0);
-            int U = getInt(pos, "U", 0);
-
-            String mainDesc = NUMBER_DESC.getOrDefault(O,
-                    O > 9 ? "卓越数能量,蕴含强大潜力" : "能量独特,需结合整体命盘解读");
-
-            StringBuilder sb = new StringBuilder();
-            sb.append("🔮 数字命盘初始解读\n\n");
-            sb.append("根据您的出生信息,已为您生成完整命盘:\n\n");
-
-            sb.append("【主性格数字 · ").append(O).append("】\n");
-            sb.append("您的核心数字为 ").append(O).append(",代表").append(mainDesc).append("。\n\n");
-
-            sb.append("【内在基础能量(底部左→右:日→月→年前半→年后半)】\n");
-            sb.append("• 日能量(出生日/外在表现)— ").append(I).append("\n");
-            sb.append("• 月能量(出生月/内在需求)— ").append(J).append("\n");
-            sb.append("• 年前半(早年家庭影响)— ").append(K).append("\n");
-            sb.append("• 年后半(后天发展潜力)— ").append(L).append("\n\n");
-
-            sb.append("【发展阶段能量】\n");
-            sb.append("• ").append(ZONE_DESC.get("left")).append(":").append(P).append(" → ").append(Q).append(" → ").append(R).append("\n");
-            sb.append("• ").append(ZONE_DESC.get("top")).append(":").append(V).append(" → ").append(W).append(" → ").append(X).append("\n");
-            sb.append("• ").append(ZONE_DESC.get("right")).append(":").append(S).append(" → ").append(T).append(" → ").append(U).append("\n\n");
-
-            // Tailored response based on user question
-            if (query != null && !query.trim().isEmpty() && !query.contains("综合解读")) {
-                String cleanQuery = query.replace("请根据数字命盘为用户提供一个综合解读。用户关心的问题:", "").trim();
-                if (!cleanQuery.isEmpty()) {
-                    sb.append("【关于您关心的问题 — ").append(cleanQuery).append("】\n");
-                    sb.append("从数字能量角度看,您当前的能量组合(主性格").append(O);
-                    sb.append(",发展期能量").append(P).append("/").append(V).append("/").append(S).append(")");
-                    sb.append("显示此方面存在发展潜力。建议结合具体情况进行更深入的咨询分析。\n\n");
-                }
-            }
-
-            sb.append("💡 温馨提示:以上为系统自动生成的初始解读。您可以在下方输入具体问题,获取更深入的分析。");
-
-            return sb.toString();
-        } catch (Exception e) {
-            // Ultimate fallback — plain text
-            return "🔮 数字命盘已生成。您可以在下方输入您关心的问题获取详细解读。";
-        }
-    }
-
-    private int getInt(JsonObject obj, String key, int fallback) {
-        try {
-            return obj.get(key).getAsInt();
-        } catch (Exception e) {
-            return fallback;
-        }
-    }
-}
+package com.etotem.num.service;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class DifyService {
+
+    private static final Logger log = LoggerFactory.getLogger(DifyService.class);
+
+    @Value("${num.dify.base-url}")
+    private String baseUrl;
+
+    @Value("${num.dify.api-key}")
+    private String apiKey;
+
+    private final RestTemplate restTemplate;
+    private final Gson gson;
+    private final ConfigService configService;
+
+    public DifyService(RestTemplate restTemplate, Gson gson, ConfigService configService) {
+        this.restTemplate = restTemplate;
+        this.gson = gson;
+        this.configService = configService;
+    }
+
+    /**
+     * Check whether Dify is configured with real credentials.
+     * Returns true if api-key or base-url are still placeholders.
+     */
+    public boolean isMockMode() {
+        return apiKey == null || apiKey.isEmpty() || apiKey.contains("your_")
+                || baseUrl == null || baseUrl.isEmpty() || baseUrl.contains("your-");
+    }
+
+    /**
+     * Invoke Dify chatflow, falling back to mock response when unavailable.
+     */
+    public String invokeChatflow(String query, String chartContext, String userId) {
+        if (isMockMode()) {
+            return generateMockResponse(chartContext, query);
+        }
+        try {
+            JsonObject body = new JsonObject();
+            body.addProperty("query", query);
+            body.addProperty("user", userId);
+            body.add("inputs", gson.toJsonTree(Collections.singletonMap("chart_context", chartContext)));
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("Authorization", "Bearer " + apiKey);
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<String> entity = new HttpEntity<>(body.toString(), headers);
+
+            String response = restTemplate.postForObject(baseUrl + "/chat-messages", entity, String.class);
+            JsonObject json = gson.fromJson(response, JsonObject.class);
+            return json.get("answer").getAsString();
+        } catch (Exception e) {
+            // Fallback to mock when real Dify call fails
+            return generateMockResponse(chartContext, query);
+        }
+    }
+
+    /**
+     * Invoke Dify Workflow (for structured interpretation).
+     * Uses POST /v1/workflows/run endpoint.
+     * Returns the workflow outputs as JSON string.
+     */
+    public String invokeWorkflow(Map<String, Object> inputs, String userId) {
+        if (isMockMode()) {
+            return generateMockInterpretation(inputs);
+        }
+        try {
+            String workflowAppId = configService.getValue("dify.workflow.app_id", "");
+            JsonObject body = new JsonObject();
+            body.addProperty("app_id", workflowAppId);
+            body.add("inputs", gson.toJsonTree(inputs));
+            body.addProperty("user", userId);
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("Authorization", "Bearer " + apiKey);
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<String> entity = new HttpEntity<>(body.toString(), headers);
+
+            String response = restTemplate.postForObject(baseUrl + "/workflows/run", entity, String.class);
+            JsonObject json = gson.fromJson(response, JsonObject.class);
+            JsonObject data = json.getAsJsonObject("data");
+            if (data != null && data.has("outputs")) {
+                return data.get("outputs").toString();
+            }
+            return json.toString();
+        } catch (Exception e) {
+            return generateMockInterpretation(inputs);
+        }
+    }
+
+    /**
+     * Generate a structured mock interpretation JSON from chart context.
+     * Sections: mainCharacter, left (21-40), top (41-60), right (61+).
+     */
+    @SuppressWarnings("unchecked")
+    String generateMockInterpretation(Map<String, Object> inputs) {
+        try {
+            String chartContext = (String) inputs.get("chart_context");
+            JsonObject chart = gson.fromJson(chartContext, JsonObject.class);
+            JsonObject pos = chart.getAsJsonObject("positions");
+            if (pos == null) pos = chart;
+
+            int O = getInt(pos, "O", 7);
+            int P = getInt(pos, "P", 5);
+            int Q = getInt(pos, "Q", 0);
+            int R = getInt(pos, "R", 0);
+            int V = getInt(pos, "V", 8);
+            int W = getInt(pos, "W", 0);
+            int X = getInt(pos, "X", 0);
+            int S = getInt(pos, "S", 9);
+            int T = getInt(pos, "T", 0);
+            int U = getInt(pos, "U", 0);
+
+            JsonObject result = new JsonObject();
+
+            // 主性格解读
+            JsonObject mainCharacter = new JsonObject();
+            mainCharacter.addProperty("number", O);
+            mainCharacter.addProperty("title", "主性格解读");
+            mainCharacter.addProperty("content", generateMainInterpretation(O));
+            result.add("mainCharacter", mainCharacter);
+
+            // 左区(21-40岁)
+            JsonObject left = new JsonObject();
+            left.addProperty("number", P);
+            left.addProperty("title", "左区(21-40岁)");
+            left.addProperty("content", generateZoneInterpretation("左区(青年发展期)", P, Q, R));
+            result.add("left", left);
+
+            // 顶部(41-60岁)
+            JsonObject top = new JsonObject();
+            top.addProperty("number", V);
+            top.addProperty("title", "顶部(41-60岁)");
+            top.addProperty("content", generateZoneInterpretation("顶部(中年发展期)", V, W, X));
+            result.add("top", top);
+
+            // 右区(61+岁)
+            JsonObject right = new JsonObject();
+            right.addProperty("number", S);
+            right.addProperty("title", "右区(61+岁)");
+            right.addProperty("content", generateZoneInterpretation("右区(成熟发展期)", S, T, U));
+            result.add("right", right);
+
+            return result.toString();
+        } catch (Exception e) {
+            return "{\"mainCharacter\":{\"number\":7,\"title\":\"主性格解读\",\"content\":\"请查看完整命盘以获取详细解读。\"}}";
+        }
+    }
+
+    // ── Academic orientation (US-9.1) ───────────────────────────────────────────────
+
+    public String invokeAcademicWorkflow(Map<String, Object> inputs, String userId) {
+        if (isMockMode()) {
+            return generateMockAcademicInterpretation(inputs);
+        }
+        try {
+            String workflowAppId = configService.getValue("dify.workflow.app_id", "");
+            JsonObject body = new JsonObject();
+            body.addProperty("app_id", workflowAppId);
+            body.add("inputs", gson.toJsonTree(inputs));
+            body.addProperty("user", userId);
+
+            HttpHeaders headers = new HttpHeaders();
+            headers.set("Authorization", "Bearer " + apiKey);
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<String> entity = new HttpEntity<>(body.toString(), headers);
+
+            String response = restTemplate.postForObject(baseUrl + "/workflows/run", entity, String.class);
+            JsonObject json = gson.fromJson(response, JsonObject.class);
+            JsonObject data = json.getAsJsonObject("data");
+            if (data != null && data.has("outputs")) {
+                return data.get("outputs").toString();
+            }
+            return json.toString();
+        } catch (Exception e) {
+            return generateMockAcademicInterpretation(inputs);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    String generateMockAcademicInterpretation(Map<String, Object> inputs) {
+        try {
+            String chartContext = (String) inputs.get("chart_context");
+            JsonObject chart = gson.fromJson(chartContext, JsonObject.class);
+            JsonObject positions = chart.getAsJsonObject("positions");
+            if (positions == null) positions = chart;
+
+            int O = getInt(positions, "O", 7);
+            int P = getInt(positions, "P", 5);
+            int V = getInt(positions, "V", 8);
+            int S = getInt(positions, "S", 9);
+            int I = getInt(positions, "I", 0);
+            int J = getInt(positions, "J", 0);
+            int K = getInt(positions, "K", 0);
+            int L = getInt(positions, "L", 0);
+
+            JsonObject result = new JsonObject();
+
+            // 天赋倾向
+            JsonObject talentTendency = new JsonObject();
+            talentTendency.addProperty("title", "天赋倾向");
+            talentTendency.addProperty("content", generateAcademicTalent(O, I, J, K, L));
+            result.add("talentTendency", talentTendency);
+
+            // 适合方向
+            JsonObject suitableDirection = new JsonObject();
+            suitableDirection.addProperty("title", "适合方向");
+            suitableDirection.addProperty("content", "根据命盘数字组合分析,以下是不同方向的匹配度参考:");
+            Map<String, Integer> percentages = generateDirectionMatchPercentages(O, P, V, S);
+            JsonObject matchJson = new Gson().toJsonTree(percentages).getAsJsonObject();
+            suitableDirection.add("matchPercentages", matchJson);
+            result.add("suitableDirection", suitableDirection);
+
+            // 学习特征
+            JsonObject learningStyle = new JsonObject();
+            learningStyle.addProperty("title", "学习特征");
+            learningStyle.addProperty("content", generateLearningStyle(I, J, K, L, P, V, S));
+            result.add("learningStyle", learningStyle);
+
+            // 亲子沟通建议
+            JsonObject parentingAdvice = new JsonObject();
+            parentingAdvice.addProperty("title", "亲子沟通建议");
+            parentingAdvice.addProperty("content", generateParentingAdvice(O));
+            result.add("parentingAdvice", parentingAdvice);
+
+            // 关键期提醒
+            JsonObject keyPeriods = new JsonObject();
+            keyPeriods.addProperty("title", "关键期提醒");
+            keyPeriods.addProperty("content", generateKeyPeriods(V, getInt(positions, "W", 0), getInt(positions, "X", 0)));
+            result.add("keyPeriods", keyPeriods);
+
+            return result.toString();
+        } catch (Exception e) {
+            JsonObject fallback = new JsonObject();
+            fallback.addProperty("title", "学业方向分析");
+            fallback.addProperty("content", "命盘分析完成。请使用完整功能获取详细学业方向建议。");
+            JsonObject wrapped = new JsonObject();
+            wrapped.add("talentTendency", fallback);
+            return wrapped.toString();
+        }
+    }
+
+    private String generateAcademicTalent(int O, int I, int J, int K, int L) {
+        String mainDesc = NUMBER_DESC.getOrDefault(O > 9 ? 0 : O,
+                O > 9 ? "卓越数能量,蕴含强大潜力" : "能量独特,需结合整体命盘解读");
+        StringBuilder sb = new StringBuilder();
+        sb.append("孩子的主性格数字为 ").append(O).append("(").append(mainDesc).append(")。\n\n");
+        sb.append("内在基础数字:");
+        sb.append("日(").append(I).append(") ").append("月(").append(J).append(") ");
+        sb.append("年前半(").append(K).append(") ").append("年后半(").append(L).append(")").append("\n\n");
+        sb.append("这些数字共同构成了孩子的天赋底色:\n");
+        sb.append("• 主性格数字反映核心特质与驱动力\n");
+        sb.append("• 内在基础数字揭示潜意识需求与安全感来源\n");
+        sb.append("建议结合具体年龄阶段观察和引导,避免标签化。");
+        return sb.toString();
+    }
+
+    private Map<String, Integer> generateDirectionMatchPercentages(int O, int P, int V, int S) {
+        Map<String, Integer> map = new LinkedHashMap<>();
+        int humanities = 50 + (P==2?10: P==4?15: P==7?12: P==1?-5: P==5?5: 0)
+                      + (V==2?10: V==4?15: V==7?12: V==1?-5: V==5?5: 0)
+                      + (S==2?10: S==4?15: S==7?12: S==1?-5: S==5?5: 0);
+        int science = 50 + (P==1?12: P==5?10: P==8?15: P==2?-5: P==4?8: 0)
+                      + (V==1?12: V==5?10: V==8?15: V==2?-5: V==4?8: 0)
+                      + (S==1?12: S==5?10: S==8?15: S==2?-5: S==4?8: 0);
+        int arts = 50 + (P==3?15: P==6?10: P==9?5: 0)
+                   + (V==3?15: V==6?10: V==9?5: 0)
+                   + (S==3?15: S==6?10: S==9?5: 0);
+        int sports = 50 + (P==1?10: P==5?15: P==8?10: 0)
+                     + (V==1?10: V==5?15: V==8?10: 0)
+                     + (S==1?10: S==5?15: S==8?10: 0);
+        map.put("文科", Math.min(95, Math.max(5, humanities)));
+        map.put("理科", Math.min(95, Math.max(5, science)));
+        map.put("艺术", Math.min(95, Math.max(5, arts)));
+        map.put("体育", Math.min(95, Math.max(5, sports)));
+        return map;
+    }
+
+    private String generateLearningStyle(int I, int J, int K, int L, int P, int V, int S) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("从内在基础数字(日、月、年前半、年后半)分析学习特征:\n\n");
+        String I_desc = NUMBER_DESC.getOrDefault(I > 9 ? 0 : I, "独特能量组合");
+        sb.append("• 外在表现(日 ").append(I).append("):").append(I_desc).append("。\n");
+        String J_desc = NUMBER_DESC.getOrDefault(J > 9 ? 0 : J, "独特能量组合");
+        sb.append("• 内在需求(月 ").append(J).append("):").append(J_desc).append("。\n");
+        String K_desc = NUMBER_DESC.getOrDefault(K > 9 ? 0 : K, "独特能量组合");
+        sb.append("• 早年家庭影响(年前半 ").append(K).append("):").append(K_desc).append("。\n");
+        String L_desc = NUMBER_DESC.getOrDefault(L > 9 ? 0 : L, "独特能量组合");
+        sb.append("• 后天发展潜力(年后半 ").append(L).append("):").append(L_desc).append("。\n\n");
+        sb.append("综合来看,建议注意:\n");
+        sb.append("1. 日与月的搭配体现孩子表里一致程度;\n");
+        sb.append("2. 年前半与年后半的关联揭示学习潜能的可塑性;\n");
+        sb.append("3. 发展阶段数字(左区").append(P).append("、顶部").append(V).append("、右区").append(S).append(")反映不同年龄段的精力投入方向。\n");
+        return sb.toString();
+    }
+
+    private String generateParentingAdvice(int O) {
+        String mainDesc = NUMBER_DESC.getOrDefault(O > 9 ? 0 : O,
+                O > 9 ? "卓越数能量,蕴含强大潜力" : "能量独特,需结合整体命盘解读");
+        StringBuilder sb = new StringBuilder();
+        sb.append("基于主性格数字 ").append(O).append(",亲子沟通建议:\n\n");
+        switch (O) {
+            case 1:
+                sb.append("• 尊重孩子的独立性,给予自主决策空间;\n");
+                sb.append("• 避免过度干涉,多用鼓励代替命令;\n");
+                break;
+            case 2:
+                sb.append("• 注重情感连接,营造安全和谐的家庭氛围;\n");
+                sb.append("• 多倾听少批评,培养合作意识;\n");
+                break;
+            case 3:
+                sb.append("• 鼓励创意表达,提供多样化活动体验;\n");
+                sb.append("• 避免刻板说教,用游戏化方式互动;\n");
+                break;
+            case 4:
+                sb.append("• 建立清晰规则,保持前后一致性;\n");
+                sb.append("• 提供结构化学习环境,循序渐进;\n");
+                break;
+            case 5:
+                sb.append("• 允许探索与试错,避免过度束缚;\n");
+                sb.append("• 多接触外界,满足好奇心;\n");
+                break;
+            case 6:
+                sb.append("• 给予充分关爱与责任感培养平衡;\n");
+                sb.append("• 参与家庭事务,增强归属感;\n");
+                break;
+            case 7:
+                sb.append("• 尊重思考空间,避免过多社交压力;\n");
+                sb.append("• 提供深度阅读与探索资源;\n");
+                break;
+            case 8:
+                sb.append("• 设定目标激励机制,培养领导力;\n");
+                sb.append("• 给予展示能力的机会;\n");
+                break;
+            case 9:
+                sb.append("• 培养同理心与社会责任感;\n");
+                sb.append("• 鼓励关怀他人,避免理想主义挫败;\n");
+                break;
+            default:
+                sb.append("• 因材施教,关注孩子独特天赋与短板;\n");
+                sb.append("• 保持耐心,接受成长过程的起伏;\n");
+        }
+        sb.append("\n以上建议仅供参考,每个孩子都是独一无二的个体。");
+        return sb.toString();
+    }
+
+    private String generateKeyPeriods(int V, int W, int X) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("关键年龄参考(基于外部三区数字):\n\n");
+        sb.append("• 左区(青年发展期 21-40岁):主能量数字 ").append(V).append(" — 大学至立业阶段;\n");
+        sb.append("• 顶部(中年发展期 41-60岁):主能量数字 ").append(V).append(" — 事业高峰期;\n");
+        sb.append("• 右区(成熟发展期 61岁+):主能量数字 ").append(V).append(" — 人生总结期;\n\n");
+        sb.append("对于K12阶段:\n");
+        sb.append("• 18岁前重点关注[外在表现]日能量与[内在需求]月能量的平衡;\n");
+        sb.append("• 发展期数字变化可视为各年龄段精力重心;\n");
+        sb.append("建议结合具体命盘细节咨询能量师获取个性化方案。");
+        return sb.toString();
+    }
+
+    private String generateMainInterpretation(int number) {
+        String desc = NUMBER_DESC.getOrDefault(number,
+                number > 9 ? "卓越数能量,蕴含强大潜力" : "能量独特,需结合整体命盘解读");
+        return String.format(
+                "您的核心数字为 %d,代表「%s」。\n\n" +
+                "此数字奠定了您整体的性格基调与人生轨迹方向。" +
+                "在决策、人际和事业发展中,%d 的能量将持续发挥作用。" +
+                "建议深入了解此数字的优势与调整方向,以更好地发挥天赋潜能。",
+                number, desc, number);
+    }
+
+    private String generateZoneInterpretation(String zoneName, int a, int b, int c) {
+        String desc = NUMBER_DESC.getOrDefault(a,
+                a > 9 ? "卓越数能量" : "独特能量组合");
+        StringBuilder sb = new StringBuilder();
+        sb.append(zoneName).append("的能量数字为 ").append(a);
+        if (b != 0) sb.append(" → ").append(b);
+        if (c != 0) sb.append(" → ").append(c);
+        sb.append(",核心数字 ").append(a).append(" 代表「").append(desc).append("」。\n\n");
+        sb.append("此阶段您在以下方面将有明显体现:\n");
+        sb.append("• 个人成长与发展方向\n");
+        sb.append("• 人际关系与社交模式\n");
+        sb.append("• 事业机遇与挑战\n\n");
+        sb.append("建议关注数字 ").append(a).append(" 的正向特质,同时觉察其过度或不足带来的影响。");
+        return sb.toString();
+    }
+
+    // ── Mock response generation ──────────────────────────────────────────────
+
+    /** Number descriptions for mock interpretations. */
+    private static final Map<Integer, String> NUMBER_DESC = new HashMap<>();
+    static {
+        NUMBER_DESC.put(1, "独立、创造与领导力,开创性强,适合自主发展");
+        NUMBER_DESC.put(2, "合作、平衡与细腻感知,擅长沟通与协调");
+        NUMBER_DESC.put(3, "表达、创意与社交活力,富有艺术天赋");
+        NUMBER_DESC.put(4, "稳定、务实与秩序感,执行力强,脚踏实地");
+        NUMBER_DESC.put(5, "自由、变化与冒险精神,适应力强,多才多艺");
+        NUMBER_DESC.put(6, "责任、关爱与和谐追求,家庭观念重");
+        NUMBER_DESC.put(7, "分析、深度思考与灵性探索,求知欲强");
+        NUMBER_DESC.put(8, "权力、财富与成就导向,商业头脑出众");
+        NUMBER_DESC.put(9, "智慧、慈悲与完成之力,格局宏大");
+        NUMBER_DESC.put(11, "直觉敏锐、灵感充沛,拥有启蒙他人的天赋(卓越数)");
+        NUMBER_DESC.put(22, "将理想变为现实的建造者,执行力与远见并存(卓越数)");
+        NUMBER_DESC.put(33, "大爱无私、奉献疗愈,是精神层面的引领者(卓越数)");
+    }
+
+    private static final Map<String, String> ZONE_DESC = new HashMap<>();
+    static {
+        ZONE_DESC.put("left", "青年发展期(21-40岁)");
+        ZONE_DESC.put("top", "中年发展期(41-60岁)");
+        ZONE_DESC.put("right", "成熟发展期(61岁以上)");
+    }
+
+    /**
+     * Generate a structured mock initial interpretation from chart data.
+     */
+    @SuppressWarnings("unchecked")
+    String generateMockResponse(String chartContext, String query) {
+        try {
+            JsonObject chart = gson.fromJson(chartContext, JsonObject.class);
+            JsonObject pos = chart.getAsJsonObject("positions");
+            if (pos == null) pos = chart; // flat positions
+
+            int O = getInt(pos, "O", 0);
+            int I = getInt(pos, "I", 0);
+            int J = getInt(pos, "J", 0);
+            int K = getInt(pos, "K", 0);
+            int L = getInt(pos, "L", 0);
+            int P = getInt(pos, "P", 0);
+            int Q = getInt(pos, "Q", 0);
+            int R = getInt(pos, "R", 0);
+            int V = getInt(pos, "V", 0);
+            int W = getInt(pos, "W", 0);
+            int X = getInt(pos, "X", 0);
+            int S = getInt(pos, "S", 0);
+            int T = getInt(pos, "T", 0);
+            int U = getInt(pos, "U", 0);
+
+            String mainDesc = NUMBER_DESC.getOrDefault(O,
+                    O > 9 ? "卓越数能量,蕴含强大潜力" : "能量独特,需结合整体命盘解读");
+
+            StringBuilder sb = new StringBuilder();
+            sb.append("🔮 数字命盘初始解读\n\n");
+            sb.append("根据您的出生信息,已为您生成完整命盘:\n\n");
+
+            sb.append("【主性格数字 · ").append(O).append("】\n");
+            sb.append("您的核心数字为 ").append(O).append(",代表").append(mainDesc).append("。\n\n");
+
+            sb.append("【内在基础能量(底部左→右:日→月→年前半→年后半)】\n");
+            sb.append("• 日能量(出生日/外在表现)— ").append(I).append("\n");
+            sb.append("• 月能量(出生月/内在需求)— ").append(J).append("\n");
+            sb.append("• 年前半(早年家庭影响)— ").append(K).append("\n");
+            sb.append("• 年后半(后天发展潜力)— ").append(L).append("\n\n");
+
+            sb.append("【发展阶段能量】\n");
+            sb.append("• ").append(ZONE_DESC.get("left")).append(":").append(P).append(" → ").append(Q).append(" → ").append(R).append("\n");
+            sb.append("• ").append(ZONE_DESC.get("top")).append(":").append(V).append(" → ").append(W).append(" → ").append(X).append("\n");
+            sb.append("• ").append(ZONE_DESC.get("right")).append(":").append(S).append(" → ").append(T).append(" → ").append(U).append("\n\n");
+
+            // Tailored response based on user question
+            if (query != null && !query.trim().isEmpty() && !query.contains("综合解读")) {
+                String cleanQuery = query.replace("请根据数字命盘为用户提供一个综合解读。用户关心的问题:", "").trim();
+                if (!cleanQuery.isEmpty()) {
+                    sb.append("【关于您关心的问题 — ").append(cleanQuery).append("】\n");
+                    sb.append("从数字能量角度看,您当前的能量组合(主性格").append(O);
+                    sb.append(",发展期能量").append(P).append("/").append(V).append("/").append(S).append(")");
+                    sb.append("显示此方面存在发展潜力。建议结合具体情况进行更深入的咨询分析。\n\n");
+                }
+            }
+
+            sb.append("💡 温馨提示:以上为系统自动生成的初始解读。您可以在下方输入具体问题,获取更深入的分析。");
+
+            return sb.toString();
+        } catch (Exception e) {
+            // Ultimate fallback — plain text
+            return "🔮 数字命盘已生成。您可以在下方输入您关心的问题获取详细解读。";
+        }
+    }
+
+    // ── Chart Interpretation (US-3.1) ─────────────────────────────────────────────
+
+    /**
+     * 调用 Dify Workflow 生成命盘完整解读(US-3.1)
+     * 返回 InterpretationResponse 格式:sections 数组 + summary + combinationNotes
+     */
+    public InterpretationResponse interpretChart(String chartContext, String userName, String birthday) {
+        if (isMockMode()) {
+            return generateMockInterpretationNew(chartContext, userName, birthday);
+        }
+        try {
+            Map<String, Object> inputs = new HashMap<>();
+            inputs.put("chart_context", chartContext);
+            inputs.put("user_name", userName);
+            inputs.put("birthday", birthday);
+
+            String workflowResult = invokeWorkflow(inputs, "system");
+            return parseInterpretation(workflowResult);
+        } catch (Exception e) {
+            log.warn("interpretChart failed, falling back to mock: {}", e.getMessage());
+            return generateMockInterpretationNew(chartContext, userName, birthday);
+        }
+    }
+
+    /**
+     * Parse Workflow JSON output into InterpretationResponse.
+     * Returns null if parsing fails (triggers mock fallback).
+     */
+    InterpretationResponse parseInterpretation(String json) {
+        try {
+            JsonObject obj = gson.fromJson(json, JsonObject.class);
+            InterpretationResponse resp = new InterpretationResponse();
+
+            // Parse sections
+            JsonArray sectionsArr = obj.getAsJsonArray("sections");
+            List<InterpretationResponse.Section> sections = new ArrayList<>();
+            if (sectionsArr != null) {
+                for (JsonElement el : sectionsArr) {
+                    JsonObject s = el.getAsJsonObject();
+                    InterpretationResponse.Section section = new InterpretationResponse.Section();
+                    section.title = getString(s, "title", "");
+                    section.positions = getStringArray(s, "positions");
+                    section.values = getIntArray(s, "values");
+                    section.content = getString(s, "content", "");
+                    section.keywords = getStringArray(s, "keywords");
+                    sections.add(section);
+                }
+            }
+            resp.sections = sections;
+
+            // Parse summary
+            resp.summary = getString(obj, "summary", "");
+
+            // Parse combinationNotes
+            JsonArray notesArr = obj.getAsJsonArray("combinationNotes");
+            List<String> notes = new ArrayList<>();
+            if (notesArr != null) {
+                for (JsonElement el : notesArr) {
+                    notes.add(el.getAsString());
+                }
+            }
+            resp.combinationNotes = notes;
+
+            // Validate: at least one section with content
+            boolean hasContent = resp.sections.stream()
+                    .anyMatch(s -> s.content != null && !s.content.isEmpty());
+            if (!hasContent) return null;
+
+            return resp;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * Generate mock InterpretationResponse in the new sections-based format (US-3.1).
+     */
+    InterpretationResponse generateMockInterpretationNew(String chartContext, String userName, String birthday) {
+        try {
+            JsonObject chart = gson.fromJson(chartContext, JsonObject.class);
+            JsonObject pos = chart.getAsJsonObject("positions");
+            if (pos == null) pos = chart;
+
+            int O = getInt(pos, "O", 7);
+            int P = getInt(pos, "P", 5);
+            int Q = getInt(pos, "Q", 0);
+            int R = getInt(pos, "R", 0);
+            int V = getInt(pos, "V", 8);
+            int W = getInt(pos, "W", 0);
+            int X = getInt(pos, "X", 0);
+            int S = getInt(pos, "S", 9);
+            int T = getInt(pos, "T", 0);
+            int U = getInt(pos, "U", 0);
+
+            InterpretationResponse resp = new InterpretationResponse();
+            resp.sections = new ArrayList<>();
+
+            // 主性格
+            InterpretationResponse.Section main = new InterpretationResponse.Section();
+            main.title = "主性格解读";
+            main.positions = Arrays.asList("O");
+            main.values = Arrays.asList(O);
+            main.content = "您的核心数字为 " + O + ",代表「"
+                    + NUMBER_DESC.getOrDefault(O, O > 9 ? "卓越数能量" : "独特能量组合")
+                    + "」。此数字奠定了您整体的性格基调与人生轨迹方向。";
+            main.keywords = Arrays.asList("主性格", "核心能量");
+            resp.sections.add(main);
+
+            // 左区
+            InterpretationResponse.Section left = new InterpretationResponse.Section();
+            left.title = "左区(21-40岁)";
+            left.positions = Arrays.asList("P", "Q", "R");
+            List<Integer> leftValues = new ArrayList<>();
+            leftValues.add(P); leftValues.add(Q); leftValues.add(R);
+            left.values = leftValues;
+            left.content = "左区能量数字为 " + P
+                    + (Q != 0 ? " → " + Q : "") + (R != 0 ? " → " + R : "")
+                    + ",核心数字 " + P + " 代表青年发展期的能量基调。";
+            left.keywords = Arrays.asList("青年期", "发展");
+            resp.sections.add(left);
+
+            // 顶部
+            InterpretationResponse.Section top = new InterpretationResponse.Section();
+            top.title = "顶部(41-60岁)";
+            top.positions = Arrays.asList("V", "W", "X");
+            List<Integer> topValues = new ArrayList<>();
+            topValues.add(V); topValues.add(W); topValues.add(X);
+            top.values = topValues;
+            top.content = "顶部能量数字为 " + V
+                    + (W != 0 ? " → " + W : "") + (X != 0 ? " → " + X : "")
+                    + ",核心数字 " + V + " 代表中年事业期的核心能量。";
+            top.keywords = Arrays.asList("中年", "事业");
+            resp.sections.add(top);
+
+            // 右区
+            InterpretationResponse.Section right = new InterpretationResponse.Section();
+            right.title = "右区(61+岁)";
+            right.positions = Arrays.asList("S", "T", "U");
+            List<Integer> rightValues = new ArrayList<>();
+            rightValues.add(S); rightValues.add(T); rightValues.add(U);
+            right.values = rightValues;
+            right.content = "右区能量数字为 " + S
+                    + (T != 0 ? " → " + T : "") + (U != 0 ? " → " + U : "")
+                    + ",核心数字 " + S + " 代表成熟沉淀期的能量导向。";
+            right.keywords = Arrays.asList("晚年", "沉淀");
+            resp.sections.add(right);
+
+            resp.summary = userName + "的命盘能量以" + O + "号主性格为核心,"
+                    + "青年期(21-40)、中年期(41-60)、成熟期(61+)的能量分别为"
+                    + P + "、" + V + "、" + S + "。整体来看,这是一组充满"
+                    + (O > 5 ? "进取与扩张" : "稳定与内敛") + "能量的命盘配置。";
+            resp.combinationNotes = new ArrayList<>();
+            resp.combinationNotes.add("详细组合分析请使用完整解读功能查看");
+
+            return resp;
+        } catch (Exception e) {
+            InterpretationResponse fallback = new InterpretationResponse();
+            fallback.sections = new ArrayList<>();
+            InterpretationResponse.Section s = new InterpretationResponse.Section();
+            s.title = "主性格解读";
+            s.positions = Arrays.asList("O");
+            s.values = Arrays.asList(7);
+            s.content = "请查看完整命盘以获取详细解读。";
+            s.keywords = Arrays.asList("请重试");
+            fallback.sections.add(s);
+            fallback.summary = "解读生成失败,请稍后再试。";
+            fallback.combinationNotes = new ArrayList<>();
+            return fallback;
+        }
+    }
+
+    // ── DTO ──────────────────────────────────────────────────────────────────
+
+    /**
+     * 命盘解读响应 DTO(US-3.1)
+     * 匹配 Section 9.6 输出 JSON 格式
+     */
+    public static class InterpretationResponse {
+        public List<Section> sections;
+        public String summary;
+        public List<String> combinationNotes;
+
+        public static class Section {
+            public String title;
+            public List<String> positions;
+            public List<Integer> values;
+            public String content;
+            public List<String> keywords;
+        }
+    }
+
+    // ── Helper methods ─────────────────────────────────────────────────────────
+
+    private String getString(JsonObject obj, String key, String fallback) {
+        try {
+            JsonElement el = obj.get(key);
+            return el != null && !el.isJsonNull() ? el.getAsString() : fallback;
+        } catch (Exception e) {
+            return fallback;
+        }
+    }
+
+    private List<String> getStringArray(JsonObject obj, String key) {
+        List<String> result = new ArrayList<>();
+        try {
+            JsonArray arr = obj.getAsJsonArray(key);
+            if (arr != null) {
+                for (JsonElement el : arr) {
+                    result.add(el.getAsString());
+                }
+            }
+        } catch (Exception ignored) {}
+        return result;
+    }
+
+    private List<Integer> getIntArray(JsonObject obj, String key) {
+        List<Integer> result = new ArrayList<>();
+        try {
+            JsonArray arr = obj.getAsJsonArray(key);
+            if (arr != null) {
+                for (JsonElement el : arr) {
+                    result.add(el.getAsInt());
+                }
+            }
+        } catch (Exception ignored) {}
+        return result;
+    }
+
+    private int getInt(JsonObject obj, String key, int fallback) {
+        try {
+            return obj.get(key).getAsInt();
+        } catch (Exception e) {
+            return fallback;
+        }
+    }
+}