Sfoglia il codice sorgente

增加与ai大模型对话

wwh 3 settimane fa
parent
commit
dab8674345

+ 12 - 2
huimv-employment/fe-api/src/main/java/com/huimv/employment/config/MybatisConfig.java

@@ -1,15 +1,25 @@
1 1
 package com.huimv.employment.config;
2 2
 
3
+import com.baomidou.mybatisplus.annotation.DbType;
4
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
5
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
3 6
 import org.mybatis.spring.annotation.MapperScan;
7
+import org.springframework.context.annotation.Bean;
4 8
 import org.springframework.context.annotation.Configuration;
5 9
 import org.springframework.context.annotation.Profile;
6 10
 
7 11
 /**
8
- * MyBatis-Plus Mapper 扫描配置。
9
- * <p>扫描 fe-dao 模块下所有 Mapper 接口,MyBatis-Plus 全局配置见 application-dev.yml。</p>
12
+ * MyBatis-Plus Mapper 扫描与分页插件配置。
10 13
  */
11 14
 @Configuration
12 15
 @Profile("!test")
13 16
 @MapperScan("com.huimv.employment.dao.mapper")
14 17
 public class MybatisConfig {
18
+
19
+    @Bean
20
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
21
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
22
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
23
+        return interceptor;
24
+    }
15 25
 }

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

@@ -7,9 +7,11 @@ import com.huimv.employment.integration.kb.SseStreamSupport;
7 7
 import com.huimv.employment.security.LoginUser;
8 8
 import com.huimv.employment.security.LoginUserHolder;
9 9
 import com.huimv.employment.service.conversation.ConversationChatService;
10
+import com.huimv.employment.service.conversation.ConversationMessageService;
10 11
 import com.huimv.employment.service.conversation.ConversationService;
11 12
 import com.huimv.employment.service.conversation.dto.ConversationChatRequest;
12 13
 import com.huimv.employment.service.conversation.dto.ConversationCreateRequest;
14
+import com.huimv.employment.service.conversation.dto.ConversationMessagePageResponse;
13 15
 import com.huimv.employment.service.conversation.dto.ConversationSummaryResponse;
14 16
 import io.swagger.v3.oas.annotations.Operation;
15 17
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -51,11 +53,14 @@ public class ConversationController {
51 53
 
52 54
     private final ConversationService conversationService;
53 55
     private final ConversationChatService conversationChatService;
56
+    private final ConversationMessageService conversationMessageService;
54 57
 
55 58
     public ConversationController(ConversationService conversationService,
56
-                                  ConversationChatService conversationChatService) {
59
+                                  ConversationChatService conversationChatService,
60
+                                  ConversationMessageService conversationMessageService) {
57 61
         this.conversationService = conversationService;
58 62
         this.conversationChatService = conversationChatService;
63
+        this.conversationMessageService = conversationMessageService;
59 64
     }
60 65
 
61 66
     /**
@@ -86,6 +91,21 @@ public class ConversationController {
86 91
         return R.ok(conversationService.create(loginUser.getUserId(), request));
87 92
     }
88 93
 
94
+    /**
95
+     * 查询指定会话下的消息列表(分页,时间正序)。
96
+     */
97
+    @GetMapping("/{id}/messages")
98
+    @Operation(summary = "查询会话消息列表",
99
+            description = "按 create_time 正序分页返回。仅可查询当前用户自己的会话;已归档会话仍可查看历史。")
100
+    public R<ConversationMessagePageResponse> listMessages(
101
+            @PathVariable("id") Long conversationId,
102
+            @RequestParam(required = false, defaultValue = "1") Integer page,
103
+            @RequestParam(required = false, defaultValue = "50") Integer size) {
104
+        LoginUser loginUser = LoginUserHolder.require();
105
+        return R.ok(conversationMessageService.listByConversation(
106
+                loginUser.getUserId(), conversationId, page, size));
107
+    }
108
+
89 109
     /**
90 110
      * 在已有会话内发起 AI 对话(SSE 流式,流结束后落库)。
91 111
      */

+ 47 - 1
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/ConversationMessageService.java

@@ -1,15 +1,20 @@
1 1
 package com.huimv.employment.service.conversation;
2 2
 
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
3 5
 import com.huimv.employment.dao.entity.FeConversationMessage;
4 6
 import com.huimv.employment.dao.mapper.FeConversationMessageMapper;
7
+import com.huimv.employment.service.conversation.dto.ConversationMessagePageResponse;
8
+import com.huimv.employment.service.conversation.dto.ConversationMessageResponse;
5 9
 import org.springframework.stereotype.Service;
6 10
 import org.springframework.transaction.annotation.Transactional;
7 11
 import org.springframework.util.StringUtils;
8 12
 
9 13
 import java.time.LocalDateTime;
14
+import java.util.stream.Collectors;
10 15
 
11 16
 /**
12
- * AI 对话消息落库:user 消息在流式开始前写入,assistant 消息在流式结束后写入
17
+ * AI 对话消息落库与查询
13 18
  */
14 19
 @Service
15 20
 public class ConversationMessageService {
@@ -18,6 +23,11 @@ public class ConversationMessageService {
18 23
     private static final String ROLE_ASSISTANT = "assistant";
19 24
     private static final String CONTENT_TYPE_TEXT = "text";
20 25
 
26
+    private static final int MIN_PAGE = 1;
27
+    private static final int MIN_SIZE = 1;
28
+    private static final int MAX_SIZE = 200;
29
+    private static final int DEFAULT_SIZE = 50;
30
+
21 31
     private final ConversationService conversationService;
22 32
     private final FeConversationMessageMapper feConversationMessageMapper;
23 33
 
@@ -27,6 +37,30 @@ public class ConversationMessageService {
27 37
         this.feConversationMessageMapper = feConversationMessageMapper;
28 38
     }
29 39
 
40
+    /**
41
+     * 分页查询指定会话下的消息,按时间正序。
42
+     */
43
+    public ConversationMessagePageResponse listByConversation(Long userId, Long conversationId, Integer page, Integer size) {
44
+        conversationService.requireOwned(userId, conversationId);
45
+        int resolvedPage = page == null || page < MIN_PAGE ? MIN_PAGE : page;
46
+        int resolvedSize = size == null || size < MIN_SIZE ? DEFAULT_SIZE : Math.min(size, MAX_SIZE);
47
+
48
+        Page<FeConversationMessage> pageParam = new Page<>(resolvedPage, resolvedSize);
49
+        LambdaQueryWrapper<FeConversationMessage> wrapper = new LambdaQueryWrapper<FeConversationMessage>()
50
+                .eq(FeConversationMessage::getConversationId, conversationId)
51
+                .orderByAsc(FeConversationMessage::getCreateTime)
52
+                .orderByAsc(FeConversationMessage::getId);
53
+        Page<FeConversationMessage> result = feConversationMessageMapper.selectPage(pageParam, wrapper);
54
+
55
+        ConversationMessagePageResponse response = new ConversationMessagePageResponse();
56
+        response.setItems(result.getRecords().stream().map(this::toResponse).collect(Collectors.toList()));
57
+        response.setTotal(result.getTotal());
58
+        response.setPage(resolvedPage);
59
+        response.setSize(resolvedSize);
60
+        response.setHasMore((long) resolvedPage * resolvedSize < result.getTotal());
61
+        return response;
62
+    }
63
+
30 64
     @Transactional(rollbackFor = Exception.class)
31 65
     public void saveUserMessage(Long conversationId, String userText) {
32 66
         if (conversationId == null || !StringUtils.hasText(userText)) {
@@ -54,4 +88,16 @@ public class ConversationMessageService {
54 88
         message.setCreateTime(LocalDateTime.now());
55 89
         feConversationMessageMapper.insert(message);
56 90
     }
91
+
92
+    private ConversationMessageResponse toResponse(FeConversationMessage entity) {
93
+        ConversationMessageResponse response = new ConversationMessageResponse();
94
+        response.setId(entity.getId());
95
+        response.setRole(entity.getRole());
96
+        response.setContent(entity.getContent());
97
+        response.setContentType(entity.getContentType());
98
+        response.setCardPayload(entity.getCardPayload());
99
+        response.setNextAction(entity.getNextAction());
100
+        response.setCreateTime(entity.getCreateTime());
101
+        return response;
102
+    }
57 103
 }

+ 10 - 2
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/ConversationService.java

@@ -116,9 +116,9 @@ public class ConversationService {
116 116
     }
117 117
 
118 118
     /**
119
-     * 校验会话归属与状态,供落库聊天接口使用
119
+     * 校验会话归属,供消息列表等只读接口使用(含已归档会话)
120 120
      */
121
-    public FeConversation requireOwnedActive(Long userId, Long conversationId) {
121
+    public FeConversation requireOwned(Long userId, Long conversationId) {
122 122
         if (userId == null || conversationId == null) {
123 123
             throw new BizException(ErrorCode.NOT_FOUND, "会话不存在");
124 124
         }
@@ -126,6 +126,14 @@ public class ConversationService {
126 126
         if (conversation == null || !userId.equals(conversation.getUserId())) {
127 127
             throw new BizException(ErrorCode.NOT_FOUND, "会话不存在");
128 128
         }
129
+        return conversation;
130
+    }
131
+
132
+    /**
133
+     * 校验会话归属与状态,供落库聊天接口使用。
134
+     */
135
+    public FeConversation requireOwnedActive(Long userId, Long conversationId) {
136
+        FeConversation conversation = requireOwned(userId, conversationId);
129 137
         if (!STATUS_ACTIVE.equals(conversation.getStatus())) {
130 138
             throw new BizException(ErrorCode.BAD_REQUEST, "会话已归档,无法继续对话");
131 139
         }

+ 67 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/dto/ConversationMessagePageResponse.java

@@ -0,0 +1,67 @@
1
+package com.huimv.employment.service.conversation.dto;
2
+
3
+import io.swagger.v3.oas.annotations.media.Schema;
4
+
5
+import java.util.List;
6
+
7
+/**
8
+ * 会话消息分页结果。
9
+ */
10
+@Schema(description = "会话消息分页结果")
11
+public class ConversationMessagePageResponse {
12
+
13
+    @Schema(description = "消息列表,按 create_time 正序")
14
+    private List<ConversationMessageResponse> items;
15
+
16
+    @Schema(description = "总条数", example = "128")
17
+    private long total;
18
+
19
+    @Schema(description = "当前页码,从 1 开始", example = "1")
20
+    private int page;
21
+
22
+    @Schema(description = "每页条数", example = "50")
23
+    private int size;
24
+
25
+    @Schema(description = "是否还有下一页", example = "true")
26
+    private boolean hasMore;
27
+
28
+    public List<ConversationMessageResponse> getItems() {
29
+        return items;
30
+    }
31
+
32
+    public void setItems(List<ConversationMessageResponse> items) {
33
+        this.items = items;
34
+    }
35
+
36
+    public long getTotal() {
37
+        return total;
38
+    }
39
+
40
+    public void setTotal(long total) {
41
+        this.total = total;
42
+    }
43
+
44
+    public int getPage() {
45
+        return page;
46
+    }
47
+
48
+    public void setPage(int page) {
49
+        this.page = page;
50
+    }
51
+
52
+    public int getSize() {
53
+        return size;
54
+    }
55
+
56
+    public void setSize(int size) {
57
+        this.size = size;
58
+    }
59
+
60
+    public boolean isHasMore() {
61
+        return hasMore;
62
+    }
63
+
64
+    public void setHasMore(boolean hasMore) {
65
+        this.hasMore = hasMore;
66
+    }
67
+}

+ 89 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/conversation/dto/ConversationMessageResponse.java

@@ -0,0 +1,89 @@
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
+ * 会话内单条消息,供聊天气泡展示。
9
+ */
10
+@Schema(description = "AI 对话消息")
11
+public class ConversationMessageResponse {
12
+
13
+    @Schema(description = "消息 ID", example = "1001")
14
+    private Long id;
15
+
16
+    @Schema(description = "角色:user / assistant / system", example = "user")
17
+    private String role;
18
+
19
+    @Schema(description = "消息正文", example = "帮我招 10 个仓库临时工")
20
+    private String content;
21
+
22
+    @Schema(description = "内容类型:text / card_draft / card_cost / card_order", example = "text")
23
+    private String contentType;
24
+
25
+    @Schema(description = "卡片结构化数据 JSON,仅卡片类型时有值")
26
+    private String cardPayload;
27
+
28
+    @Schema(description = "建议前端下一步动作编码")
29
+    private String nextAction;
30
+
31
+    @Schema(description = "消息时间")
32
+    private LocalDateTime createTime;
33
+
34
+    public Long getId() {
35
+        return id;
36
+    }
37
+
38
+    public void setId(Long id) {
39
+        this.id = id;
40
+    }
41
+
42
+    public String getRole() {
43
+        return role;
44
+    }
45
+
46
+    public void setRole(String role) {
47
+        this.role = role;
48
+    }
49
+
50
+    public String getContent() {
51
+        return content;
52
+    }
53
+
54
+    public void setContent(String content) {
55
+        this.content = content;
56
+    }
57
+
58
+    public String getContentType() {
59
+        return contentType;
60
+    }
61
+
62
+    public void setContentType(String contentType) {
63
+        this.contentType = contentType;
64
+    }
65
+
66
+    public String getCardPayload() {
67
+        return cardPayload;
68
+    }
69
+
70
+    public void setCardPayload(String cardPayload) {
71
+        this.cardPayload = cardPayload;
72
+    }
73
+
74
+    public String getNextAction() {
75
+        return nextAction;
76
+    }
77
+
78
+    public void setNextAction(String nextAction) {
79
+        this.nextAction = nextAction;
80
+    }
81
+
82
+    public LocalDateTime getCreateTime() {
83
+        return createTime;
84
+    }
85
+
86
+    public void setCreateTime(LocalDateTime createTime) {
87
+        this.createTime = createTime;
88
+    }
89
+}

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

@@ -6,7 +6,7 @@ import java.time.LocalDateTime;
6 6
 
7 7
 /**
8 8
  * AI 对话会话列表项,供小程序侧边栏展示。
9
- * <p>不含消息正文;详情消息后续 {@code GET /conversations/{id}/messages}(规划中)。</p>
9
+ * <p>不含消息正文;详情见 {@code GET /conversations/{id}/messages}。</p>
10 10
  */
11 11
 @Schema(description = "AI 对话会话摘要")
12 12
 public class ConversationSummaryResponse {