wwh недель назад: 3
Родитель
Сommit
2f813e1ac5

+ 60 - 0
huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/ConversationController.java

@@ -0,0 +1,60 @@
1
+package com.huimv.employment.controller.mp;
2
+
3
+import com.huimv.employment.common.web.R;
4
+import com.huimv.employment.security.LoginUser;
5
+import com.huimv.employment.security.LoginUserHolder;
6
+import com.huimv.employment.service.conversation.ConversationService;
7
+import com.huimv.employment.service.conversation.dto.ConversationSummaryResponse;
8
+import io.swagger.v3.oas.annotations.Operation;
9
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
10
+import io.swagger.v3.oas.annotations.tags.Tag;
11
+import org.springframework.web.bind.annotation.GetMapping;
12
+import org.springframework.web.bind.annotation.RequestMapping;
13
+import org.springframework.web.bind.annotation.RequestParam;
14
+import org.springframework.web.bind.annotation.RestController;
15
+
16
+import java.util.List;
17
+
18
+/**
19
+ * 小程序 AI 对话会话接口。
20
+ * <p>
21
+ * 路径 {@code /api/v1/mp/conversations},需 JWT 鉴权。
22
+ * 从 JWT 解析 {@code userId},仅返回当前用户自己的会话,不支持跨用户查询。
23
+ * </p>
24
+ * <p>
25
+ * 典型用法:对话中心侧边栏加载历史会话列表;点击某条后带 {@code conversationNo}(即 AI {@code session_id})
26
+ * 继续多轮对话。
27
+ * </p>
28
+ */
29
+@RestController
30
+@RequestMapping("/api/v1/mp/conversations")
31
+@Tag(name = "AI 会话", description = "对话会话列表,需 JWT 鉴权")
32
+@SecurityRequirement(name = "Authorization")
33
+public class ConversationController {
34
+
35
+    private final ConversationService conversationService;
36
+
37
+    public ConversationController(ConversationService conversationService) {
38
+        this.conversationService = conversationService;
39
+    }
40
+
41
+    /**
42
+     * 查询当前登录用户的全部 AI 对话会话。
43
+     *
44
+     * @param status       可选,{@code active} / {@code archived}
45
+     * @param chatType     可选,如 {@code employment}
46
+     * @param enterpriseId 可选,多企业时按企业筛选
47
+     */
48
+    @GetMapping
49
+    @Operation(summary = "查询当前用户的 AI 对话会话列表",
50
+            description = "按 last_message_at 倒序返回。可选按 status、chatType、enterpriseId 筛选。"
51
+                    + "数据来源 fe_conversation,需先有会话落库记录。")
52
+    public R<List<ConversationSummaryResponse>> list(
53
+            @RequestParam(required = false) String status,
54
+            @RequestParam(required = false) String chatType,
55
+            @RequestParam(required = false) Long enterpriseId) {
56
+        LoginUser loginUser = LoginUserHolder.require();
57
+        return R.ok(conversationService.listByUser(
58
+                loginUser.getUserId(), status, chatType, enterpriseId));
59
+    }
60
+}

+ 138 - 0
huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/entity/FeConversation.java

@@ -0,0 +1,138 @@
1
+package com.huimv.employment.dao.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.IdType;
4
+import com.baomidou.mybatisplus.annotation.TableId;
5
+import com.baomidou.mybatisplus.annotation.TableLogic;
6
+import com.baomidou.mybatisplus.annotation.TableName;
7
+
8
+import java.time.LocalDateTime;
9
+
10
+/**
11
+ * AI 对话会话主表 {@code fe_conversation} 实体。
12
+ * <p>
13
+ * 企业主与智能体交互的容器;{@code conversation_no} 与上游 AI 网关 {@code session_id} 对应,
14
+ * 用于多轮对话上下文关联。消息明细见 {@code fe_conversation_message}。
15
+ * </p>
16
+ */
17
+@TableName("fe_conversation")
18
+public class FeConversation {
19
+
20
+    @TableId(type = IdType.AUTO)
21
+    private Long id;
22
+
23
+    /** 对外业务编号,全局唯一,与 AI 接口 session_id 一致,最长 32 字符 */
24
+    private String conversationNo;
25
+
26
+    /** 所属企业,创建会话时从当前用户企业绑定关系写入 */
27
+    private Long enterpriseId;
28
+
29
+    /** 发起对话的用户,与 JWT 中 userId 对应 */
30
+    private Long userId;
31
+
32
+    /** 侧边栏展示标题,可由首条消息摘要或 AI 生成 */
33
+    private String title;
34
+
35
+    /** employment / knowledge / order / cost / attendance / emergency */
36
+    private String chatType;
37
+
38
+    /** active=进行中;archived=已归档(只读) */
39
+    private String status;
40
+
41
+    /** 最后一条消息时间,列表排序依据 */
42
+    private LocalDateTime lastMessageAt;
43
+
44
+    private LocalDateTime createTime;
45
+
46
+    private LocalDateTime updateTime;
47
+
48
+    @TableLogic
49
+    private Integer delFlag;
50
+
51
+    public Long getId() {
52
+        return id;
53
+    }
54
+
55
+    public void setId(Long id) {
56
+        this.id = id;
57
+    }
58
+
59
+    public String getConversationNo() {
60
+        return conversationNo;
61
+    }
62
+
63
+    public void setConversationNo(String conversationNo) {
64
+        this.conversationNo = conversationNo;
65
+    }
66
+
67
+    public Long getEnterpriseId() {
68
+        return enterpriseId;
69
+    }
70
+
71
+    public void setEnterpriseId(Long enterpriseId) {
72
+        this.enterpriseId = enterpriseId;
73
+    }
74
+
75
+    public Long getUserId() {
76
+        return userId;
77
+    }
78
+
79
+    public void setUserId(Long userId) {
80
+        this.userId = userId;
81
+    }
82
+
83
+    public String getTitle() {
84
+        return title;
85
+    }
86
+
87
+    public void setTitle(String title) {
88
+        this.title = title;
89
+    }
90
+
91
+    public String getChatType() {
92
+        return chatType;
93
+    }
94
+
95
+    public void setChatType(String chatType) {
96
+        this.chatType = chatType;
97
+    }
98
+
99
+    public String getStatus() {
100
+        return status;
101
+    }
102
+
103
+    public void setStatus(String status) {
104
+        this.status = status;
105
+    }
106
+
107
+    public LocalDateTime getLastMessageAt() {
108
+        return lastMessageAt;
109
+    }
110
+
111
+    public void setLastMessageAt(LocalDateTime lastMessageAt) {
112
+        this.lastMessageAt = lastMessageAt;
113
+    }
114
+
115
+    public LocalDateTime getCreateTime() {
116
+        return createTime;
117
+    }
118
+
119
+    public void setCreateTime(LocalDateTime createTime) {
120
+        this.createTime = createTime;
121
+    }
122
+
123
+    public LocalDateTime getUpdateTime() {
124
+        return updateTime;
125
+    }
126
+
127
+    public void setUpdateTime(LocalDateTime updateTime) {
128
+        this.updateTime = updateTime;
129
+    }
130
+
131
+    public Integer getDelFlag() {
132
+        return delFlag;
133
+    }
134
+
135
+    public void setDelFlag(Integer delFlag) {
136
+        this.delFlag = delFlag;
137
+    }
138
+}

+ 10 - 0
huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/mapper/FeConversationMapper.java

@@ -0,0 +1,10 @@
1
+package com.huimv.employment.dao.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.huimv.employment.dao.entity.FeConversation;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+/** {@code fe_conversation} MyBatis-Plus Mapper。 */
8
+@Mapper
9
+public interface FeConversationMapper extends BaseMapper<FeConversation> {
10
+}

+ 154 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/ConversationService.java

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

+ 101 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/dto/ConversationSummaryResponse.java

@@ -0,0 +1,101 @@
1
+package com.huimv.employment.service.conversation.dto;
2
+
3
+import io.swagger.v3.oas.annotations.media.Schema;
4
+
5
+import java.time.LocalDateTime;
6
+
7
+/**
8
+ * AI 对话会话列表项,供小程序侧边栏展示。
9
+ * <p>不含消息正文;详情消息见后续 {@code GET /conversations/{id}/messages}(规划中)。</p>
10
+ */
11
+@Schema(description = "AI 对话会话摘要")
12
+public class ConversationSummaryResponse {
13
+
14
+    @Schema(description = "会话主键 ID", example = "1")
15
+    private Long id;
16
+
17
+    @Schema(description = "会话编号(与 AI 接口 session_id 一致)", example = "a1b2c3d4e5f6")
18
+    private String conversationNo;
19
+
20
+    @Schema(description = "所属企业 ID", example = "10")
21
+    private Long enterpriseId;
22
+
23
+    @Schema(description = "会话标题", example = "浦东仓库暑期用工")
24
+    private String title;
25
+
26
+    @Schema(description = "对话类型:employment/knowledge/order/cost/attendance/emergency", example = "employment")
27
+    private String chatType;
28
+
29
+    @Schema(description = "状态:active/archived", example = "active")
30
+    private String status;
31
+
32
+    @Schema(description = "最后一条消息时间,列表排序依据")
33
+    private LocalDateTime lastMessageAt;
34
+
35
+    @Schema(description = "会话创建时间")
36
+    private LocalDateTime createTime;
37
+
38
+    public Long getId() {
39
+        return id;
40
+    }
41
+
42
+    public void setId(Long id) {
43
+        this.id = id;
44
+    }
45
+
46
+    public String getConversationNo() {
47
+        return conversationNo;
48
+    }
49
+
50
+    public void setConversationNo(String conversationNo) {
51
+        this.conversationNo = conversationNo;
52
+    }
53
+
54
+    public Long getEnterpriseId() {
55
+        return enterpriseId;
56
+    }
57
+
58
+    public void setEnterpriseId(Long enterpriseId) {
59
+        this.enterpriseId = enterpriseId;
60
+    }
61
+
62
+    public String getTitle() {
63
+        return title;
64
+    }
65
+
66
+    public void setTitle(String title) {
67
+        this.title = title;
68
+    }
69
+
70
+    public String getChatType() {
71
+        return chatType;
72
+    }
73
+
74
+    public void setChatType(String chatType) {
75
+        this.chatType = chatType;
76
+    }
77
+
78
+    public String getStatus() {
79
+        return status;
80
+    }
81
+
82
+    public void setStatus(String status) {
83
+        this.status = status;
84
+    }
85
+
86
+    public LocalDateTime getLastMessageAt() {
87
+        return lastMessageAt;
88
+    }
89
+
90
+    public void setLastMessageAt(LocalDateTime lastMessageAt) {
91
+        this.lastMessageAt = lastMessageAt;
92
+    }
93
+
94
+    public LocalDateTime getCreateTime() {
95
+        return createTime;
96
+    }
97
+
98
+    public void setCreateTime(LocalDateTime createTime) {
99
+        this.createTime = createTime;
100
+    }
101
+}