|
|
@@ -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
|
|