|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+package com.huimv.employment.service.conversation;
|
|
|
2
|
+
|
|
|
3
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
4
|
+import com.huimv.employment.dao.entity.FeConversation;
|
|
|
5
|
+import com.huimv.employment.dao.entity.FeEnterprise;
|
|
|
6
|
+import com.huimv.employment.dao.mapper.FeConversationMapper;
|
|
|
7
|
+import com.huimv.employment.service.conversation.dto.ConversationSummaryResponse;
|
|
|
8
|
+import com.huimv.employment.service.enterprise.EnterpriseService;
|
|
|
9
|
+import org.slf4j.Logger;
|
|
|
10
|
+import org.slf4j.LoggerFactory;
|
|
|
11
|
+import org.springframework.dao.DuplicateKeyException;
|
|
|
12
|
+import org.springframework.stereotype.Service;
|
|
|
13
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
14
|
+import org.springframework.util.StringUtils;
|
|
|
15
|
+
|
|
|
16
|
+import java.time.LocalDateTime;
|
|
|
17
|
+import java.util.Collections;
|
|
|
18
|
+import java.util.List;
|
|
|
19
|
+import java.util.stream.Collectors;
|
|
|
20
|
+
|
|
|
21
|
+/**
|
|
|
22
|
+ * AI 对话会话业务层。
|
|
|
23
|
+ * <p>
|
|
|
24
|
+ * 一期提供按登录用户查询会话列表;{@link #touchFromChat(Long, String)} 供后续在对话入口落库时调用
|
|
|
25
|
+ *(当前未接入 {@code AiProxyController})。
|
|
|
26
|
+ * </p>
|
|
|
27
|
+ */
|
|
|
28
|
+@Service
|
|
|
29
|
+public class ConversationService {
|
|
|
30
|
+
|
|
|
31
|
+ private static final Logger log = LoggerFactory.getLogger(ConversationService.class);
|
|
|
32
|
+
|
|
|
33
|
+ private static final String STATUS_ACTIVE = "active";
|
|
|
34
|
+ private static final String CHAT_TYPE_EMPLOYMENT = "employment";
|
|
|
35
|
+
|
|
|
36
|
+ /** 与表字段 {@code conversation_no} VARCHAR(32) 一致 */
|
|
|
37
|
+ private static final int CONVERSATION_NO_MAX_LEN = 32;
|
|
|
38
|
+
|
|
|
39
|
+ private final FeConversationMapper feConversationMapper;
|
|
|
40
|
+ private final EnterpriseService enterpriseService;
|
|
|
41
|
+
|
|
|
42
|
+ public ConversationService(FeConversationMapper feConversationMapper,
|
|
|
43
|
+ EnterpriseService enterpriseService) {
|
|
|
44
|
+ this.feConversationMapper = feConversationMapper;
|
|
|
45
|
+ this.enterpriseService = enterpriseService;
|
|
|
46
|
+ }
|
|
|
47
|
+
|
|
|
48
|
+ /**
|
|
|
49
|
+ * 查询指定用户的 AI 对话会话列表。
|
|
|
50
|
+ *
|
|
|
51
|
+ * @param userId 当前登录用户 ID(来自 JWT)
|
|
|
52
|
+ * @param status 可选,{@code active} / {@code archived}
|
|
|
53
|
+ * @param chatType 可选,专题类型
|
|
|
54
|
+ * @param enterpriseId 可选,按企业筛选(多企业场景)
|
|
|
55
|
+ * @return 按 {@code last_message_at}、{@code create_time} 倒序
|
|
|
56
|
+ */
|
|
|
57
|
+ public List<ConversationSummaryResponse> listByUser(Long userId, String status, String chatType, Long enterpriseId) {
|
|
|
58
|
+ if (userId == null) {
|
|
|
59
|
+ return Collections.emptyList();
|
|
|
60
|
+ }
|
|
|
61
|
+ LambdaQueryWrapper<FeConversation> wrapper = new LambdaQueryWrapper<FeConversation>()
|
|
|
62
|
+ .eq(FeConversation::getUserId, userId)
|
|
|
63
|
+ .eq(StringUtils.hasText(status), FeConversation::getStatus, status)
|
|
|
64
|
+ .eq(StringUtils.hasText(chatType), FeConversation::getChatType, chatType)
|
|
|
65
|
+ .eq(enterpriseId != null, FeConversation::getEnterpriseId, enterpriseId)
|
|
|
66
|
+ .orderByDesc(FeConversation::getLastMessageAt)
|
|
|
67
|
+ .orderByDesc(FeConversation::getCreateTime);
|
|
|
68
|
+ return feConversationMapper.selectList(wrapper).stream()
|
|
|
69
|
+ .map(this::toSummary)
|
|
|
70
|
+ .collect(Collectors.toList());
|
|
|
71
|
+ }
|
|
|
72
|
+
|
|
|
73
|
+ /**
|
|
|
74
|
+ * 用户发起 AI 对话时刷新会话记录。
|
|
|
75
|
+ * <p>
|
|
|
76
|
+ * 已存在则更新 {@code last_message_at};不存在则新建。新建要求用户已绑定企业({@code enterprise_id} 非空约束)。
|
|
|
77
|
+ * {@code sessionId} 写入 {@code conversation_no},与上游 {@code /api/console/chat} 的 {@code session_id} 保持一致。
|
|
|
78
|
+ * </p>
|
|
|
79
|
+ *
|
|
|
80
|
+ * @param userId 当前用户 ID
|
|
|
81
|
+ * @param sessionId 客户端或 AI 网关会话 ID
|
|
|
82
|
+ */
|
|
|
83
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
84
|
+ public void touchFromChat(Long userId, String sessionId) {
|
|
|
85
|
+ if (userId == null || !StringUtils.hasText(sessionId)) {
|
|
|
86
|
+ return;
|
|
|
87
|
+ }
|
|
|
88
|
+ String conversationNo = normalizeConversationNo(sessionId.trim());
|
|
|
89
|
+ FeConversation existing = feConversationMapper.selectOne(new LambdaQueryWrapper<FeConversation>()
|
|
|
90
|
+ .eq(FeConversation::getConversationNo, conversationNo)
|
|
|
91
|
+ .eq(FeConversation::getUserId, userId)
|
|
|
92
|
+ .last("LIMIT 1"));
|
|
|
93
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
94
|
+ if (existing != null) {
|
|
|
95
|
+ FeConversation update = new FeConversation();
|
|
|
96
|
+ update.setId(existing.getId());
|
|
|
97
|
+ update.setLastMessageAt(now);
|
|
|
98
|
+ update.setUpdateTime(now);
|
|
|
99
|
+ feConversationMapper.updateById(update);
|
|
|
100
|
+ return;
|
|
|
101
|
+ }
|
|
|
102
|
+ FeEnterprise enterprise = enterpriseService.findEnterpriseByUserId(userId);
|
|
|
103
|
+ if (enterprise == null) {
|
|
|
104
|
+ log.debug("用户 {} 未绑定企业,跳过会话落库 sessionId={}", userId, conversationNo);
|
|
|
105
|
+ return;
|
|
|
106
|
+ }
|
|
|
107
|
+ FeConversation created = new FeConversation();
|
|
|
108
|
+ created.setConversationNo(conversationNo);
|
|
|
109
|
+ created.setEnterpriseId(enterprise.getId());
|
|
|
110
|
+ created.setUserId(userId);
|
|
|
111
|
+ created.setChatType(CHAT_TYPE_EMPLOYMENT);
|
|
|
112
|
+ created.setStatus(STATUS_ACTIVE);
|
|
|
113
|
+ created.setLastMessageAt(now);
|
|
|
114
|
+ created.setCreateTime(now);
|
|
|
115
|
+ created.setUpdateTime(now);
|
|
|
116
|
+ try {
|
|
|
117
|
+ feConversationMapper.insert(created);
|
|
|
118
|
+ } catch (DuplicateKeyException ex) {
|
|
|
119
|
+ // conversation_no 全局唯一,并发创建时回退为更新
|
|
|
120
|
+ FeConversation retry = feConversationMapper.selectOne(new LambdaQueryWrapper<FeConversation>()
|
|
|
121
|
+ .eq(FeConversation::getConversationNo, conversationNo)
|
|
|
122
|
+ .eq(FeConversation::getUserId, userId)
|
|
|
123
|
+ .last("LIMIT 1"));
|
|
|
124
|
+ if (retry != null) {
|
|
|
125
|
+ FeConversation update = new FeConversation();
|
|
|
126
|
+ update.setId(retry.getId());
|
|
|
127
|
+ update.setLastMessageAt(now);
|
|
|
128
|
+ update.setUpdateTime(now);
|
|
|
129
|
+ feConversationMapper.updateById(update);
|
|
|
130
|
+ }
|
|
|
131
|
+ }
|
|
|
132
|
+ }
|
|
|
133
|
+
|
|
|
134
|
+ private ConversationSummaryResponse toSummary(FeConversation entity) {
|
|
|
135
|
+ ConversationSummaryResponse response = new ConversationSummaryResponse();
|
|
|
136
|
+ response.setId(entity.getId());
|
|
|
137
|
+ response.setConversationNo(entity.getConversationNo());
|
|
|
138
|
+ response.setEnterpriseId(entity.getEnterpriseId());
|
|
|
139
|
+ response.setTitle(entity.getTitle());
|
|
|
140
|
+ response.setChatType(entity.getChatType());
|
|
|
141
|
+ response.setStatus(entity.getStatus());
|
|
|
142
|
+ response.setLastMessageAt(entity.getLastMessageAt());
|
|
|
143
|
+ response.setCreateTime(entity.getCreateTime());
|
|
|
144
|
+ return response;
|
|
|
145
|
+ }
|
|
|
146
|
+
|
|
|
147
|
+ /** 截断超长 session_id,满足 {@code uk_fe_conversation_no} 长度限制 */
|
|
|
148
|
+ private static String normalizeConversationNo(String sessionId) {
|
|
|
149
|
+ if (sessionId.length() <= CONVERSATION_NO_MAX_LEN) {
|
|
|
150
|
+ return sessionId;
|
|
|
151
|
+ }
|
|
|
152
|
+ return sessionId.substring(0, CONVERSATION_NO_MAX_LEN);
|
|
|
153
|
+ }
|
|
|
154
|
+}
|