Kaynağa Gözat

增加与ai大模型对话

wwh 3 hafta önce
ebeveyn
işleme
8cb565e71c

+ 33 - 9
huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/ConversationController.java

@@ -1,14 +1,20 @@
1 1
 package com.huimv.employment.controller.mp;
2 2
 
3
+import com.huimv.employment.common.exception.BizException;
4
+import com.huimv.employment.common.exception.ErrorCode;
3 5
 import com.huimv.employment.common.web.R;
4 6
 import com.huimv.employment.security.LoginUser;
5 7
 import com.huimv.employment.security.LoginUserHolder;
6 8
 import com.huimv.employment.service.conversation.ConversationService;
9
+import com.huimv.employment.service.conversation.dto.ConversationCreateRequest;
7 10
 import com.huimv.employment.service.conversation.dto.ConversationSummaryResponse;
8 11
 import io.swagger.v3.oas.annotations.Operation;
9 12
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
10 13
 import io.swagger.v3.oas.annotations.tags.Tag;
14
+import org.springframework.validation.annotation.Validated;
11 15
 import org.springframework.web.bind.annotation.GetMapping;
16
+import org.springframework.web.bind.annotation.PostMapping;
17
+import org.springframework.web.bind.annotation.RequestBody;
12 18
 import org.springframework.web.bind.annotation.RequestMapping;
13 19
 import org.springframework.web.bind.annotation.RequestParam;
14 20
 import org.springframework.web.bind.annotation.RestController;
@@ -22,16 +28,18 @@ import java.util.List;
22 28
  * 从 JWT 解析 {@code userId},仅返回当前用户自己的会话,不支持跨用户查询。
23 29
  * </p>
24 30
  * <p>
25
- * 典型用法:对话中心侧边栏加载历史会话列表;点击某条后带 {@code conversationNo}(即 AI {@code session_id})
26
- * 继续多轮对话。
31
+ * 典型用法:点击「新对话」调用 {@code POST /conversations} 获取 {@code conversationNo},
32
+ * 再作为 {@code session_id} 调用 {@code POST /ai/console/chat} 进行多轮对话。
27 33
  * </p>
28 34
  */
29 35
 @RestController
30 36
 @RequestMapping("/api/v1/mp/conversations")
31
-@Tag(name = "AI 会话", description = "对话会话列表,需 JWT 鉴权")
37
+@Tag(name = "AI 会话", description = "对话会话管理,需 JWT 鉴权")
32 38
 @SecurityRequirement(name = "Authorization")
33 39
 public class ConversationController {
34 40
 
41
+    private static final String USER_TYPE_ENTERPRISE = "enterprise";
42
+
35 43
     private final ConversationService conversationService;
36 44
 
37 45
     public ConversationController(ConversationService conversationService) {
@@ -40,15 +48,10 @@ public class ConversationController {
40 48
 
41 49
     /**
42 50
      * 查询当前登录用户的全部 AI 对话会话。
43
-     *
44
-     * @param status       可选,{@code active} / {@code archived}
45
-     * @param chatType     可选,如 {@code employment}
46
-     * @param enterpriseId 可选,多企业时按企业筛选
47 51
      */
48 52
     @GetMapping
49 53
     @Operation(summary = "查询当前用户的 AI 对话会话列表",
50
-            description = "按 last_message_at 倒序返回。可选按 status、chatType、enterpriseId 筛选。"
51
-                    + "数据来源 fe_conversation,需先有会话落库记录。")
54
+            description = "按 last_message_at 倒序返回。可选按 status、chatType、enterpriseId 筛选。")
52 55
     public R<List<ConversationSummaryResponse>> list(
53 56
             @RequestParam(required = false) String status,
54 57
             @RequestParam(required = false) String chatType,
@@ -57,4 +60,25 @@ public class ConversationController {
57 60
         return R.ok(conversationService.listByUser(
58 61
                 loginUser.getUserId(), status, chatType, enterpriseId));
59 62
     }
63
+
64
+    /**
65
+     * 新建 AI 对话会话。
66
+     * <p>返回的 {@code conversationNo} 即后续 AI 对话的 {@code session_id}。</p>
67
+     */
68
+    @PostMapping
69
+    @Operation(summary = "新建 AI 对话会话",
70
+            description = "企业主用户创建新会话。conversationNo 不传时服务端自动生成;"
71
+                    + "chatType 默认 employment。需已完成企业登记。")
72
+    public R<ConversationSummaryResponse> create(@Validated @RequestBody(required = false) ConversationCreateRequest request) {
73
+        LoginUser loginUser = requireEnterprise();
74
+        return R.ok(conversationService.create(loginUser.getUserId(), request));
75
+    }
76
+
77
+    private static LoginUser requireEnterprise() {
78
+        LoginUser loginUser = LoginUserHolder.require();
79
+        if (!USER_TYPE_ENTERPRISE.equals(loginUser.getUserType())) {
80
+            throw new BizException(ErrorCode.ENTERPRISE_ONLY);
81
+        }
82
+        return loginUser;
83
+    }
60 84
 }

+ 98 - 5
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/ConversationService.java

@@ -1,9 +1,12 @@
1 1
 package com.huimv.employment.service.conversation;
2 2
 
3 3
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.huimv.employment.common.exception.BizException;
5
+import com.huimv.employment.common.exception.ErrorCode;
4 6
 import com.huimv.employment.dao.entity.FeConversation;
5 7
 import com.huimv.employment.dao.entity.FeEnterprise;
6 8
 import com.huimv.employment.dao.mapper.FeConversationMapper;
9
+import com.huimv.employment.service.conversation.dto.ConversationCreateRequest;
7 10
 import com.huimv.employment.service.conversation.dto.ConversationSummaryResponse;
8 11
 import com.huimv.employment.service.enterprise.EnterpriseService;
9 12
 import org.slf4j.Logger;
@@ -14,15 +17,18 @@ import org.springframework.transaction.annotation.Transactional;
14 17
 import org.springframework.util.StringUtils;
15 18
 
16 19
 import java.time.LocalDateTime;
20
+import java.util.Arrays;
17 21
 import java.util.Collections;
22
+import java.util.HashSet;
18 23
 import java.util.List;
24
+import java.util.Set;
25
+import java.util.UUID;
19 26
 import java.util.stream.Collectors;
20 27
 
21 28
 /**
22 29
  * AI 对话会话业务层。
23 30
  * <p>
24
- * 一期提供按登录用户查询会话列表;{@link #touchFromChat(Long, String)} 供后续在对话入口落库时调用
25
- *(当前未接入 {@code AiProxyController})。
31
+ * 一期提供会话列表查询与手动新建;{@link #touchFromChat(Long, String)} 供后续在对话入口自动刷新时调用。
26 32
  * </p>
27 33
  */
28 34
 @Service
@@ -33,6 +39,10 @@ public class ConversationService {
33 39
     private static final String STATUS_ACTIVE = "active";
34 40
     private static final String CHAT_TYPE_EMPLOYMENT = "employment";
35 41
 
42
+    private static final Set<String> ALLOWED_CHAT_TYPES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
43
+            "employment", "knowledge", "order", "cost", "attendance", "emergency"
44
+    )));
45
+
36 46
     /** 与表字段 {@code conversation_no} VARCHAR(32) 一致 */
37 47
     private static final int CONVERSATION_NO_MAX_LEN = 32;
38 48
 
@@ -70,6 +80,41 @@ public class ConversationService {
70 80
                 .collect(Collectors.toList());
71 81
     }
72 82
 
83
+    /**
84
+     * 新建 AI 对话会话。
85
+     * <p>
86
+     * {@code conversationNo} 不传时自动生成 32 位 hex,作为后续 {@code /ai/console/chat} 的 {@code session_id}。
87
+     * 要求用户已完成企业登记。
88
+     * </p>
89
+     */
90
+    @Transactional(rollbackFor = Exception.class)
91
+    public ConversationSummaryResponse create(Long userId, ConversationCreateRequest request) {
92
+        FeEnterprise enterprise = resolveEnterprise(userId, request != null ? request.getEnterpriseId() : null);
93
+        String chatType = resolveChatType(request != null ? request.getChatType() : null);
94
+        String conversationNo = resolveNewConversationNo(request != null ? request.getConversationNo() : null);
95
+        assertConversationNoAvailable(conversationNo);
96
+
97
+        LocalDateTime now = LocalDateTime.now();
98
+        FeConversation created = new FeConversation();
99
+        created.setConversationNo(conversationNo);
100
+        created.setEnterpriseId(enterprise.getId());
101
+        created.setUserId(userId);
102
+        if (request != null && StringUtils.hasText(request.getTitle())) {
103
+            created.setTitle(request.getTitle().trim());
104
+        }
105
+        created.setChatType(chatType);
106
+        created.setStatus(STATUS_ACTIVE);
107
+        created.setLastMessageAt(now);
108
+        created.setCreateTime(now);
109
+        created.setUpdateTime(now);
110
+        try {
111
+            feConversationMapper.insert(created);
112
+        } catch (DuplicateKeyException ex) {
113
+            throw new BizException(ErrorCode.BAD_REQUEST, "会话编号已存在,请更换后重试");
114
+        }
115
+        return toSummary(created);
116
+    }
117
+
73 118
     /**
74 119
      * 用户发起 AI 对话时刷新会话记录。
75 120
      * <p>
@@ -104,11 +149,58 @@ public class ConversationService {
104 149
             log.debug("用户 {} 未绑定企业,跳过会话落库 sessionId={}", userId, conversationNo);
105 150
             return;
106 151
         }
152
+        insertConversation(userId, enterprise.getId(), conversationNo, null, CHAT_TYPE_EMPLOYMENT, now);
153
+    }
154
+
155
+    private FeEnterprise resolveEnterprise(Long userId, Long enterpriseId) {
156
+        FeEnterprise bound = enterpriseService.findEnterpriseByUserId(userId);
157
+        if (bound == null) {
158
+            throw new BizException(ErrorCode.BAD_REQUEST, "请先完成企业登记");
159
+        }
160
+        if (enterpriseId != null && !enterpriseId.equals(bound.getId())) {
161
+            throw new BizException(ErrorCode.FORBIDDEN, "无权在该企业下创建会话");
162
+        }
163
+        return bound;
164
+    }
165
+
166
+    private String resolveChatType(String chatType) {
167
+        if (!StringUtils.hasText(chatType)) {
168
+            return CHAT_TYPE_EMPLOYMENT;
169
+        }
170
+        String normalized = chatType.trim();
171
+        if (!ALLOWED_CHAT_TYPES.contains(normalized)) {
172
+            throw new BizException(ErrorCode.BAD_REQUEST, "不支持的对话类型:" + normalized);
173
+        }
174
+        return normalized;
175
+    }
176
+
177
+    private String resolveNewConversationNo(String conversationNo) {
178
+        if (!StringUtils.hasText(conversationNo)) {
179
+            return UUID.randomUUID().toString().replace("-", "");
180
+        }
181
+        return normalizeConversationNo(conversationNo.trim());
182
+    }
183
+
184
+    private void assertConversationNoAvailable(String conversationNo) {
185
+        Long count = feConversationMapper.selectCount(new LambdaQueryWrapper<FeConversation>()
186
+                .eq(FeConversation::getConversationNo, conversationNo));
187
+        if (count != null && count > 0) {
188
+            throw new BizException(ErrorCode.BAD_REQUEST, "会话编号已存在,请更换后重试");
189
+        }
190
+    }
191
+
192
+    private void insertConversation(Long userId,
193
+                                    Long enterpriseId,
194
+                                    String conversationNo,
195
+                                    String title,
196
+                                    String chatType,
197
+                                    LocalDateTime now) {
107 198
         FeConversation created = new FeConversation();
108 199
         created.setConversationNo(conversationNo);
109
-        created.setEnterpriseId(enterprise.getId());
200
+        created.setEnterpriseId(enterpriseId);
110 201
         created.setUserId(userId);
111
-        created.setChatType(CHAT_TYPE_EMPLOYMENT);
202
+        created.setTitle(title);
203
+        created.setChatType(chatType);
112 204
         created.setStatus(STATUS_ACTIVE);
113 205
         created.setLastMessageAt(now);
114 206
         created.setCreateTime(now);
@@ -116,7 +208,6 @@ public class ConversationService {
116 208
         try {
117 209
             feConversationMapper.insert(created);
118 210
         } catch (DuplicateKeyException ex) {
119
-            // conversation_no 全局唯一,并发创建时回退为更新
120 211
             FeConversation retry = feConversationMapper.selectOne(new LambdaQueryWrapper<FeConversation>()
121 212
                     .eq(FeConversation::getConversationNo, conversationNo)
122 213
                     .eq(FeConversation::getUserId, userId)
@@ -127,7 +218,9 @@ public class ConversationService {
127 218
                 update.setLastMessageAt(now);
128 219
                 update.setUpdateTime(now);
129 220
                 feConversationMapper.updateById(update);
221
+                return;
130 222
             }
223
+            throw new BizException(ErrorCode.BAD_REQUEST, "会话编号已存在,请更换后重试");
131 224
         }
132 225
     }
133 226
 

+ 60 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/dto/ConversationCreateRequest.java

@@ -0,0 +1,60 @@
1
+package com.huimv.employment.service.conversation.dto;
2
+
3
+import io.swagger.v3.oas.annotations.media.Schema;
4
+
5
+import javax.validation.constraints.Size;
6
+
7
+/**
8
+ * 新建 AI 对话会话请求。
9
+ */
10
+@Schema(description = "新建 AI 对话会话请求")
11
+public class ConversationCreateRequest {
12
+
13
+    @Schema(description = "会话标题(可选,首条消息后可再更新)", example = "新对话")
14
+    @Size(max = 200, message = "标题不能超过 200 字")
15
+    private String title;
16
+
17
+    @Schema(description = "对话类型,默认 employment", example = "employment",
18
+            allowableValues = {"employment", "knowledge", "order", "cost", "attendance", "emergency"})
19
+    private String chatType;
20
+
21
+    @Schema(description = "自定义会话编号(可选,须全局唯一;不传则服务端生成,用作 AI session_id)",
22
+            example = "a1b2c3d4e5f6789012345678901234")
23
+    @Size(max = 32, message = "会话编号不能超过 32 字符")
24
+    private String conversationNo;
25
+
26
+    @Schema(description = "企业 ID(可选,默认当前用户绑定的企业)", example = "10")
27
+    private Long enterpriseId;
28
+
29
+    public String getTitle() {
30
+        return title;
31
+    }
32
+
33
+    public void setTitle(String title) {
34
+        this.title = title;
35
+    }
36
+
37
+    public String getChatType() {
38
+        return chatType;
39
+    }
40
+
41
+    public void setChatType(String chatType) {
42
+        this.chatType = chatType;
43
+    }
44
+
45
+    public String getConversationNo() {
46
+        return conversationNo;
47
+    }
48
+
49
+    public void setConversationNo(String conversationNo) {
50
+        this.conversationNo = conversationNo;
51
+    }
52
+
53
+    public Long getEnterpriseId() {
54
+        return enterpriseId;
55
+    }
56
+
57
+    public void setEnterpriseId(Long enterpriseId) {
58
+        this.enterpriseId = enterpriseId;
59
+    }
60
+}