ソースを参照

Merge branch 'master' of http://192.168.1.25:3000/lbyzx123/huimv-employment

xsh_1997 2 週間 前
コミット
9429d87b46

+ 2 - 1
huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/ConversationController.java

@@ -174,7 +174,8 @@ public class ConversationController {
174 174
             description = "需先 POST /conversations 创建会话。请求体仅需 message 字段;"
175 175
                     + "服务端组装上游 console/chat 请求,流式开始前写入 user 消息;"
176 176
                     + "SSE 最后一条消息由服务端注入 content_type/next_action/card_payload"
177
-                    + "(会话有 pending 草稿且 missingFields 为空时,不依赖智能体),"
177
+                    + "(有 confirmed 订单且登记批次 qr_code_url 非空时注入 card_order;"
178
+                    + "否则会话有 pending 草稿且 missingFields 为空时注入 card_draft,均不依赖智能体),"
178 179
                     + "并同步落库。可选请求头 X-Agent-Id。")
179 180
     public void chat(@PathVariable("id") Long conversationId,
180 181
                      @Validated @RequestBody ConversationChatRequest request,

+ 5 - 4
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/ConversationChatService.java

@@ -20,7 +20,8 @@ import java.io.IOException;
20 20
 import java.io.OutputStream;
21 21
 
22 22
 /**
23
- * 会话内 AI 对话:流式转发;pending 且 missingFields 为空时在 SSE 最后一条消息注入卡片字段后落库。
23
+ * 会话内 AI 对话:流式转发;满足条件时在 SSE 最后一条消息注入卡片字段后落库。
24
+ * <p>订单码卡片优先于草稿卡片(均不依赖智能体)。</p>
24 25
  */
25 26
 @Service
26 27
 public class ConversationChatService {
@@ -101,7 +102,7 @@ public class ConversationChatService {
101 102
         ObjectNode msg = buildEnrichedLastMessage(heldTerminalLine, parsed);
102 103
         String json = objectMapper.writeValueAsString(msg);
103 104
         SseStreamSupport.writeDataEvent(output, json);
104
-        log.info("SSE 最后消息已写出 next_action={} content_type={} draftId={}",
105
+        log.info("SSE 最后消息已写出 next_action={} content_type={} relatedDraftId={}",
105 106
                 msg.path("next_action").asText(),
106 107
                 msg.path("content_type").asText(),
107 108
                 parsed.getRelatedDraftId());
@@ -135,8 +136,8 @@ public class ConversationChatService {
135 136
         }
136 137
 
137 138
         // 强制覆盖/追加卡片字段(不用智能体返回)
138
-        msg.put("content_type", MessageCardConstants.CONTENT_TYPE_CARD_DRAFT);
139
-        msg.put("next_action", MessageCardConstants.NEXT_ACTION_SHOW_EMPLOYMENT_DRAFT);
139
+        msg.put("content_type", parsed.getContentType());
140
+        msg.put("next_action", parsed.getNextAction());
140 141
         if (parsed.getRelatedDraftId() != null) {
141 142
             msg.put("related_draft_id", parsed.getRelatedDraftId());
142 143
         }

+ 128 - 6
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/ConversationDraftCardEnricher.java

@@ -4,13 +4,18 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4 4
 import com.fasterxml.jackson.core.type.TypeReference;
5 5
 import com.fasterxml.jackson.databind.ObjectMapper;
6 6
 import com.huimv.employment.dao.entity.FeEmploymentDraft;
7
+import com.huimv.employment.dao.entity.FeEmploymentOrder;
8
+import com.huimv.employment.dao.entity.FeRegistrationBatch;
7 9
 import com.huimv.employment.dao.mapper.FeEmploymentDraftMapper;
10
+import com.huimv.employment.dao.mapper.FeEmploymentOrderMapper;
11
+import com.huimv.employment.dao.mapper.FeRegistrationBatchMapper;
8 12
 import com.huimv.employment.service.conversation.dto.ParsedAssistantReply;
9 13
 import org.slf4j.Logger;
10 14
 import org.slf4j.LoggerFactory;
11 15
 import org.springframework.stereotype.Component;
12 16
 import org.springframework.util.StringUtils;
13 17
 
18
+import java.util.ArrayList;
14 19
 import java.util.Collections;
15 20
 import java.util.LinkedHashMap;
16 21
 import java.util.List;
@@ -18,7 +23,12 @@ import java.util.Map;
18 23
 import java.util.regex.Pattern;
19 24
 
20 25
 /**
21
- * SSE 最后消息时,由服务端根据会话 pending 且 missingFields 为空的草稿注入卡片字段(不依赖智能体)。
26
+ * SSE 最后消息时,由服务端注入卡片字段(不依赖智能体)。
27
+ * <p>
28
+ * 优先级:
29
+ * 1)会话有 confirmed 订单且对应登记批次 {@code qr_code_url} 非空 → {@code card_order} / {@code show_order_qrcode};
30
+ * 2)否则会话有 pending 且 missingFields 为空的草稿 → {@code card_draft} / {@code show_employment_draft}。
31
+ * </p>
22 32
  */
23 33
 @Component
24 34
 public class ConversationDraftCardEnricher {
@@ -26,22 +36,29 @@ public class ConversationDraftCardEnricher {
26 36
     private static final Logger log = LoggerFactory.getLogger(ConversationDraftCardEnricher.class);
27 37
 
28 38
     private static final String DB_STATUS_PENDING = "pending";
39
+    private static final String ORDER_STATUS_CONFIRMED = "confirmed";
29 40
 
30 41
     private static final Pattern FENCED_CARD_BLOCK = Pattern.compile(
31 42
             "```(?:fe-card|json)?\\s*[\\s\\S]*?```",
32 43
             Pattern.CASE_INSENSITIVE);
33 44
 
34 45
     private final FeEmploymentDraftMapper feEmploymentDraftMapper;
46
+    private final FeEmploymentOrderMapper feEmploymentOrderMapper;
47
+    private final FeRegistrationBatchMapper feRegistrationBatchMapper;
35 48
     private final ObjectMapper objectMapper;
36 49
 
37 50
     public ConversationDraftCardEnricher(FeEmploymentDraftMapper feEmploymentDraftMapper,
51
+                                         FeEmploymentOrderMapper feEmploymentOrderMapper,
52
+                                         FeRegistrationBatchMapper feRegistrationBatchMapper,
38 53
                                          ObjectMapper objectMapper) {
39 54
         this.feEmploymentDraftMapper = feEmploymentDraftMapper;
55
+        this.feEmploymentOrderMapper = feEmploymentOrderMapper;
56
+        this.feRegistrationBatchMapper = feRegistrationBatchMapper;
40 57
         this.objectMapper = objectMapper;
41 58
     }
42 59
 
43 60
     /**
44
-     * 会话有 pending 草稿且 missingFields 为空时追加 card_draft
61
+     * 按会话状态追加订单码卡片或草稿卡片
45 62
      */
46 63
     public ParsedAssistantReply attachCardOnComplete(Long conversationId, String assistantText) {
47 64
         String displayText = sanitizeDisplayText(assistantText);
@@ -50,21 +67,52 @@ public class ConversationDraftCardEnricher {
50 67
             log.warn("SSE 注入卡片跳过:conversationId 为空");
51 68
             return parsed;
52 69
         }
70
+
71
+        ParsedAssistantReply withOrder = tryAttachOrderCard(conversationId, parsed);
72
+        if (withOrder.hasCard()) {
73
+            return withOrder;
74
+        }
75
+        return tryAttachDraftCard(conversationId, parsed);
76
+    }
77
+
78
+    private ParsedAssistantReply tryAttachOrderCard(Long conversationId, ParsedAssistantReply parsed) {
79
+        ConfirmedOrderWithQr found = findLatestConfirmedOrderWithQr(conversationId);
80
+        if (found == null) {
81
+            log.info("SSE 注入订单卡片跳过:会话 {} 无 confirmed 订单或批次 qr_code_url 为空", conversationId);
82
+            return parsed;
83
+        }
84
+        try {
85
+            Map<String, Object> payload = buildOrderCardPayload(found.order, found.batch);
86
+            String cardPayload = objectMapper.writeValueAsString(payload);
87
+            log.info("SSE 注入订单卡片 conversationId={} orderNo={} batchId={} qr={}",
88
+                    conversationId, found.order.getOrderNo(), found.batch.getId(), found.batch.getQrCodeUrl());
89
+            return parsed.withCard(
90
+                    MessageCardConstants.CONTENT_TYPE_CARD_ORDER,
91
+                    MessageCardConstants.NEXT_ACTION_SHOW_ORDER_QRCODE,
92
+                    cardPayload,
93
+                    found.order.getDraftId());
94
+        } catch (Exception ex) {
95
+            log.warn("会话 {} 订单卡片注入失败: {}", conversationId, ex.getMessage(), ex);
96
+            return parsed;
97
+        }
98
+    }
99
+
100
+    private ParsedAssistantReply tryAttachDraftCard(Long conversationId, ParsedAssistantReply parsed) {
53 101
         FeEmploymentDraft draft = findPendingDraft(conversationId);
54 102
         if (draft == null) {
55
-            log.info("SSE 注入卡片跳过:会话 {} 无 pending 草稿", conversationId);
103
+            log.info("SSE 注入草稿卡片跳过:会话 {} 无 pending 草稿", conversationId);
56 104
             return parsed;
57 105
         }
58 106
         List<String> missing = parseMissingFields(draft.getMissingFields());
59 107
         if (!missing.isEmpty()) {
60
-            log.info("SSE 注入卡片跳过:会话 {} 草稿 {} missingFields 非空 {}",
108
+            log.info("SSE 注入草稿卡片跳过:会话 {} 草稿 {} missingFields 非空 {}",
61 109
                     conversationId, draft.getId(), missing);
62 110
             return parsed;
63 111
         }
64 112
         try {
65 113
             Map<String, Object> payload = buildDraftCardPayload(draft);
66 114
             String cardPayload = objectMapper.writeValueAsString(payload);
67
-            log.info("SSE 注入卡片 conversationId={} draftId={}", conversationId, draft.getId());
115
+            log.info("SSE 注入草稿卡片 conversationId={} draftId={}", conversationId, draft.getId());
68 116
             return parsed.withCard(
69 117
                     MessageCardConstants.CONTENT_TYPE_CARD_DRAFT,
70 118
                     MessageCardConstants.NEXT_ACTION_SHOW_EMPLOYMENT_DRAFT,
@@ -76,6 +124,46 @@ public class ConversationDraftCardEnricher {
76 124
         }
77 125
     }
78 126
 
127
+    private ConfirmedOrderWithQr findLatestConfirmedOrderWithQr(Long conversationId) {
128
+        List<FeEmploymentDraft> drafts = feEmploymentDraftMapper.selectList(
129
+                new LambdaQueryWrapper<FeEmploymentDraft>()
130
+                        .eq(FeEmploymentDraft::getConversationId, conversationId)
131
+                        .orderByDesc(FeEmploymentDraft::getId));
132
+        if (drafts == null || drafts.isEmpty()) {
133
+            return null;
134
+        }
135
+        List<Long> draftIds = new ArrayList<>(drafts.size());
136
+        for (FeEmploymentDraft draft : drafts) {
137
+            if (draft.getId() != null) {
138
+                draftIds.add(draft.getId());
139
+            }
140
+        }
141
+        if (draftIds.isEmpty()) {
142
+            return null;
143
+        }
144
+        List<FeEmploymentOrder> orders = feEmploymentOrderMapper.selectList(
145
+                new LambdaQueryWrapper<FeEmploymentOrder>()
146
+                        .in(FeEmploymentOrder::getDraftId, draftIds)
147
+                        .eq(FeEmploymentOrder::getOrderStatus, ORDER_STATUS_CONFIRMED)
148
+                        .orderByDesc(FeEmploymentOrder::getId));
149
+        if (orders == null || orders.isEmpty()) {
150
+            return null;
151
+        }
152
+        for (FeEmploymentOrder order : orders) {
153
+            FeRegistrationBatch batch = feRegistrationBatchMapper.selectOne(
154
+                    new LambdaQueryWrapper<FeRegistrationBatch>()
155
+                            .eq(FeRegistrationBatch::getOrderId, order.getId())
156
+                            .isNotNull(FeRegistrationBatch::getQrCodeUrl)
157
+                            .ne(FeRegistrationBatch::getQrCodeUrl, "")
158
+                            .orderByDesc(FeRegistrationBatch::getId)
159
+                            .last("LIMIT 1"));
160
+            if (batch != null && StringUtils.hasText(batch.getQrCodeUrl())) {
161
+                return new ConfirmedOrderWithQr(order, batch);
162
+            }
163
+        }
164
+        return null;
165
+    }
166
+
79 167
     private String sanitizeDisplayText(String raw) {
80 168
         if (!StringUtils.hasText(raw)) {
81 169
             return "";
@@ -102,7 +190,6 @@ public class ConversationDraftCardEnricher {
102 190
             return parsed != null ? parsed : Collections.emptyList();
103 191
         } catch (Exception ex) {
104 192
             log.warn("解析 missing_fields 失败,视为非空,跳过注入: {}", ex.getMessage());
105
-            // 解析失败时保守处理:不注入,避免误推未就绪草稿
106 193
             return Collections.singletonList("_parse_error");
107 194
         }
108 195
     }
@@ -122,4 +209,39 @@ public class ConversationDraftCardEnricher {
122 209
         payload.put("missing_fields", Collections.emptyList());
123 210
         return payload;
124 211
     }
212
+
213
+    private Map<String, Object> buildOrderCardPayload(FeEmploymentOrder order, FeRegistrationBatch batch) {
214
+        Map<String, Object> payload = new LinkedHashMap<>();
215
+        payload.put("draft_id", order.getDraftId());
216
+        payload.put("order_id", order.getId());
217
+        payload.put("order_no", order.getOrderNo());
218
+        payload.put("title", order.getTitle());
219
+        payload.put("worker_count", order.getWorkerCount());
220
+        payload.put("work_days", order.getWorkDays());
221
+        payload.put("work_type", order.getWorkType());
222
+        payload.put("work_location", order.getWorkLocation());
223
+        payload.put("work_start_date", order.getWorkStartDate());
224
+        payload.put("work_end_date", order.getWorkEndDate());
225
+        payload.put("order_status", order.getOrderStatus());
226
+        payload.put("current_step_code", order.getCurrentStepCode());
227
+        payload.put("registered_count", order.getRegisteredCount());
228
+        payload.put("pending_count", order.getPendingCount());
229
+        payload.put("total_outflow", order.getTotalOutflow());
230
+        payload.put("batch_id", batch.getId());
231
+        payload.put("batch_no", batch.getBatchNo());
232
+        payload.put("qr_code_url", batch.getQrCodeUrl());
233
+        payload.put("registration_url", batch.getRegistrationUrl());
234
+        payload.put("qr_token", batch.getQrToken());
235
+        return payload;
236
+    }
237
+
238
+    private static final class ConfirmedOrderWithQr {
239
+        private final FeEmploymentOrder order;
240
+        private final FeRegistrationBatch batch;
241
+
242
+        private ConfirmedOrderWithQr(FeEmploymentOrder order, FeRegistrationBatch batch) {
243
+            this.order = order;
244
+            this.batch = batch;
245
+        }
246
+    }
125 247
 }

+ 6 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/ConversationMessageService.java

@@ -128,6 +128,9 @@ public class ConversationMessageService {
128 128
             if (MessageCardConstants.CONTENT_TYPE_CARD_COST.equals(parsed.getContentType())) {
129 129
                 return "成本测算完成,请选择方案。";
130 130
             }
131
+            if (MessageCardConstants.CONTENT_TYPE_CARD_ORDER.equals(parsed.getContentType())) {
132
+                return "方案已确认,请让临时工扫码登记。";
133
+            }
131 134
             return "请查看卡片详情。";
132 135
         }
133 136
         return "";
@@ -140,6 +143,9 @@ public class ConversationMessageService {
140 143
         if (MessageCardConstants.CONTENT_TYPE_CARD_COST.equals(contentType)) {
141 144
             return MessageCardConstants.NEXT_ACTION_OPEN_COST_COMPARISON;
142 145
         }
146
+        if (MessageCardConstants.CONTENT_TYPE_CARD_ORDER.equals(contentType)) {
147
+            return MessageCardConstants.NEXT_ACTION_SHOW_ORDER_QRCODE;
148
+        }
143 149
         return null;
144 150
     }
145 151
 

+ 1 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/MessageCardConstants.java

@@ -12,6 +12,7 @@ public final class MessageCardConstants {
12 12
 
13 13
     public static final String NEXT_ACTION_SHOW_EMPLOYMENT_DRAFT = "show_employment_draft";
14 14
     public static final String NEXT_ACTION_OPEN_COST_COMPARISON = "open_cost_comparison";
15
+    public static final String NEXT_ACTION_SHOW_ORDER_QRCODE = "show_order_qrcode";
15 16
 
16 17
     private MessageCardConstants() {
17 18
     }