feat(chat): 实现人脸智能聊天核心功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good

- 新增小程序人脸聊天控制器 AppChatController,支持会话创建、消息收发、历史查询及会话关闭
- 集成智谱 GLM 模型客户端 GlmClient,支持流式文本生成与回调
- 新增聊天会话与消息实体类及 MyBatis 映射,实现数据持久化
- 提供 FaceChatService 接口及实现,封装聊天业务逻辑包括同步/流式消息发送
- 引入 zai-sdk 依赖以支持调用智谱 AI 大模型能力
- 支持基于人脸 ID 的唯一会话管理与用户权限校验
- 消息记录包含角色、内容、追踪 ID 及延迟信息,便于调试与分析
This commit is contained in:
2025-12-11 17:45:49 +08:00
parent 6e7b4729a8
commit 3b11ddef6a
18 changed files with 811 additions and 0 deletions

View File

@@ -273,6 +273,13 @@
<version>5.0.0</version>
</dependency>
<!-- 智谱AI SDK -->
<dependency>
<groupId>ai.z.openapi</groupId>
<artifactId>zai-sdk</artifactId>
<version>0.1.3</version>
</dependency>
<!-- Spring Kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>

View File

@@ -0,0 +1,98 @@
package com.ycwl.basic.controller.mobile;
import com.ycwl.basic.model.jwt.JwtInfo;
import com.ycwl.basic.model.mobile.chat.*;
import com.ycwl.basic.service.mobile.FaceChatService;
import com.ycwl.basic.utils.ApiResponse;
import com.ycwl.basic.utils.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import java.util.concurrent.CompletableFuture;
/**
* 小程序人脸智能聊天接口。
*/
@Slf4j
@RestController
@RequestMapping("/api/mobile/chat/v1")
@RequiredArgsConstructor
public class AppChatController {
private final FaceChatService faceChatService;
/**
* 获取或创建会话(同一人脸只保留一条)。
*/
@PostMapping("/faces/{faceId}/conversation")
public ApiResponse<ChatConversationVO> createConversation(@PathVariable Long faceId) {
JwtInfo worker = JwtTokenUtil.getWorker();
ChatConversationVO vo = faceChatService.getOrCreateConversation(faceId, worker.getUserId());
return ApiResponse.success(vo);
}
/**
* 同步发送消息,适用于短回复或前端自行轮询。
*/
@PostMapping("/conversations/{conversationId}/messages")
public ApiResponse<ChatSendMessageResp> sendMessage(@PathVariable Long conversationId,
@RequestBody ChatSendMessageReq req) {
JwtInfo worker = JwtTokenUtil.getWorker();
ChatSendMessageResp resp = faceChatService.sendMessage(conversationId, worker.getUserId(),
req.getContent(), req.getTraceId());
return ApiResponse.success(resp);
}
/**
* 流式返回,使用 HTTP chunked。小程序侧用 wx.request 的 onChunkReceived 消费。
*/
@PostMapping(value = "/conversations/{conversationId}/messages/stream", produces = "text/plain;charset=UTF-8")
public ResponseBodyEmitter streamMessage(@PathVariable Long conversationId,
@RequestBody ChatSendMessageReq req) {
JwtInfo worker = JwtTokenUtil.getWorker();
ResponseBodyEmitter emitter = new ResponseBodyEmitter(30_000L);
CompletableFuture.runAsync(() -> {
try {
faceChatService.sendMessageStream(
conversationId,
worker.getUserId(),
req.getContent(),
req.getTraceId(),
chunk -> {
try {
emitter.send(chunk, new MediaType("text", "plain", java.nio.charset.StandardCharsets.UTF_8));
} catch (Exception e) {
emitter.completeWithError(e);
}
});
emitter.complete();
} catch (Exception e) {
log.error("streamMessage error", e);
emitter.completeWithError(e);
}
});
return emitter;
}
/**
* 查询历史消息,cursor 为最后一条 seq,limit 为条数。
*/
@GetMapping("/conversations/{conversationId}/messages")
public ApiResponse<ChatMessagePageResp> listMessages(@PathVariable Long conversationId,
@RequestParam(value = "cursor", required = false) Integer cursor,
@RequestParam(value = "limit", required = false) Integer limit) {
JwtInfo worker = JwtTokenUtil.getWorker();
ChatMessagePageResp resp = faceChatService.listMessages(conversationId, cursor, limit, worker.getUserId());
return ApiResponse.success(resp);
}
@PostMapping("/conversations/{conversationId}/close")
public ApiResponse<String> closeConversation(@PathVariable Long conversationId) {
JwtInfo worker = JwtTokenUtil.getWorker();
faceChatService.closeConversation(conversationId, worker.getUserId());
return ApiResponse.success("OK");
}
}

View File

@@ -0,0 +1,17 @@
package com.ycwl.basic.integration.glm;
import java.util.List;
/**
* 智谱 GLM 模型调用抽象。
*/
public interface GlmClient {
/**
* 流式回复,实时回调分片,同时返回完整文本。
*/
String streamReply(Long faceId,
Long memberId,
String traceId,
List<ai.z.openapi.service.model.ChatMessage> messages,
java.util.function.Consumer<String> chunkConsumer);
}

View File

@@ -0,0 +1,118 @@
package com.ycwl.basic.integration.glm;
import ai.z.openapi.ZhipuAiClient;
import ai.z.openapi.service.model.ChatCompletionCreateParams;
import ai.z.openapi.service.model.ChatCompletionResponse;
import ai.z.openapi.service.model.ChatMessage;
import ai.z.openapi.service.model.ChatMessageRole;
import ai.z.openapi.service.model.ChatThinking;
import ai.z.openapi.service.model.Delta;
import ai.z.openapi.service.model.ModelData;
import io.reactivex.rxjava3.core.Flowable;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
/**
* 智谱 GLM 官方 SDK 调用实现,流式拆分文本。
*/
@Slf4j
@Component
public class GlmClientImpl implements GlmClient {
private static final String DEFAULT_MODEL = "glm-4.5-airx";
private final ZhipuAiClient client;
public GlmClientImpl(@Value("${zhipu.api-key:}") String apiKeyFromCfg) {
String apiKey = StringUtils.isNotBlank(apiKeyFromCfg) ? apiKeyFromCfg : System.getenv("ZHIPU_API_KEY");
if (StringUtils.isBlank(apiKey)) {
log.warn("未配置 ZHIPU_API_KEY,GLM 调用将返回占位结果");
client = null;
} else {
client = ZhipuAiClient.builder().apiKey(apiKey).build();
}
}
@Override
public String streamReply(Long faceId,
Long memberId,
String traceId,
List<ChatMessage> messages,
Consumer<String> chunkConsumer) {
if (client == null) {
String fallback = "GLM 未配置 API KEY,无法调用。";
if (chunkConsumer != null) {
chunkConsumer.accept(fallback);
}
return fallback;
}
List<ChatMessage> finalMessages = messages == null ? new ArrayList<>() : new ArrayList<>(messages);
if (finalMessages.isEmpty()) {
finalMessages.add(ChatMessage.builder()
.role(ChatMessageRole.USER.value())
.content("你好")
.build());
}
finalMessages.addFirst(ChatMessage.builder()
.role(ChatMessageRole.SYSTEM.value())
.content("你是一个智能销售,然后用户是实际想购买的人。你需要尽可能推广景区生成的Vlog、视频、照片等内容给用户,鼓励购买。注意,返回内容需要为普通文本格式,禁止出现Markdown标记。")
.build());
finalMessages.addFirst(ChatMessage.builder()
.role(ChatMessageRole.SYSTEM.value())
.content("注意,返回内容需要为普通文本格式,禁止使用Markdown格式进行返回。")
.build());
ChatCompletionCreateParams request = ChatCompletionCreateParams.builder()
.model(DEFAULT_MODEL)
.messages(finalMessages)
.thinking(ChatThinking.builder().type("enabled").build())
.stream(true)
.maxTokens(4096)
.temperature(0.8f)
.build();
ChatCompletionResponse response = client.chat().createChatCompletion(request);
if (!response.isSuccess()) {
String msg = "GLM 调用失败: " + response.getMsg();
log.warn(msg);
if (chunkConsumer != null) {
chunkConsumer.accept(msg);
}
return msg;
}
StringBuilder sb = new StringBuilder();
Flowable<ModelData> flowable = response.getFlowable();
flowable.blockingSubscribe(
data -> {
if (data.getChoices() == null || data.getChoices().isEmpty()) {
return;
}
Delta delta = data.getChoices().getFirst().getDelta();
if (delta == null) {
return;
}
String piece = delta.getContent();
if (StringUtils.isNotBlank(piece)) {
sb.append(piece);
if (chunkConsumer != null) {
chunkConsumer.accept(piece);
}
}
},
error -> {
log.error("GLM 流式调用异常", error);
String err = "GLM 调用异常:" + error.getMessage();
sb.append(err);
if (chunkConsumer != null) {
chunkConsumer.accept(err);
}
}
);
return sb.toString();
}
}

View File

@@ -0,0 +1,16 @@
package com.ycwl.basic.mapper;
import com.ycwl.basic.model.mobile.chat.entity.FaceChatConversationEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface FaceChatConversationMapper {
FaceChatConversationEntity findByFaceId(@Param("faceId") Long faceId);
FaceChatConversationEntity getById(@Param("id") Long id);
int insert(FaceChatConversationEntity entity);
int updateStatus(@Param("id") Long id, @Param("status") String status);
}

View File

@@ -0,0 +1,24 @@
package com.ycwl.basic.mapper;
import com.ycwl.basic.model.mobile.chat.entity.FaceChatMessageEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface FaceChatMessageMapper {
Integer maxSeqForUpdate(@Param("conversationId") Long conversationId);
int insert(FaceChatMessageEntity entity);
List<FaceChatMessageEntity> listByConversation(@Param("conversationId") Long conversationId,
@Param("cursor") Integer cursor,
@Param("limit") Integer limit);
/**
* 按 seq 倒序获取最近若干条消息,用于拼接上下文。
*/
List<FaceChatMessageEntity> listRecentByConversation(@Param("conversationId") Long conversationId,
@Param("limit") Integer limit);
}

View File

@@ -0,0 +1,14 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
/**
* 会话信息返回对象。
*/
@Data
public class ChatConversationVO {
private Long conversationId;
private Long faceId;
private String status;
private String model;
}

View File

@@ -0,0 +1,17 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
import java.util.List;
/**
* 消息列表响应。
*/
@Data
public class ChatMessagePageResp {
private List<ChatMessageVO> messages;
/**
* 下一条游标(返回最后一条 seq)。
*/
private Integer nextCursor;
}

View File

@@ -0,0 +1,18 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
import java.util.Date;
/**
* 聊天消息视图对象。
*/
@Data
public class ChatMessageVO {
private Long id;
private Integer seq;
private String role;
private String content;
private String traceId;
private Date createdAt;
}

View File

@@ -0,0 +1,22 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
/**
* 发送消息请求体。
*/
@Data
public class ChatSendMessageReq {
/**
* 用户输入的文本内容。
*/
private String content;
/**
* 链路追踪ID,前端可透传,没有则服务端生成。
*/
private String traceId;
/**
* 是否期望流式返回。
*/
private Boolean stream;
}

View File

@@ -0,0 +1,13 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
/**
* 发送消息同步响应。
*/
@Data
public class ChatSendMessageResp {
private ChatMessageVO userMessage;
private ChatMessageVO assistantMessage;
private String traceId;
}

View File

@@ -0,0 +1,16 @@
package com.ycwl.basic.model.mobile.chat;
import lombok.Data;
import java.util.List;
/**
* 流式发送消息的服务结果。
*/
@Data
public class ChatSendMessageStreamResp {
private ChatMessageVO userMessage;
private ChatMessageVO assistantMessage;
private String traceId;
private List<String> chunks;
}

View File

@@ -0,0 +1,35 @@
package com.ycwl.basic.model.mobile.chat.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* 小程序人脸聊天会话,一脸一会话。
*/
@Data
@TableName("face_chat_conversation")
public class FaceChatConversationEntity {
@TableId
private Long id;
/**
* 对应的人脸ID。
*/
private Long faceId;
/**
* 归属用户ID,冗余校验越权。
*/
private Long memberId;
/**
* 会话状态 active/closed。
*/
private String status;
/**
* 使用的模型名称,例如 glm-v。
*/
private String model;
private Date createdAt;
private Date updatedAt;
}

View File

@@ -0,0 +1,28 @@
package com.ycwl.basic.model.mobile.chat.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* 小程序人脸聊天消息,只保存文本。
*/
@Data
@TableName("face_chat_message")
public class FaceChatMessageEntity {
@TableId
private Long id;
private Long conversationId;
private Long faceId;
private Integer seq;
/**
* user / assistant / system。
*/
private String role;
private String content;
private String traceId;
private Integer latencyMs;
private Date createdAt;
}

View File

@@ -0,0 +1,36 @@
package com.ycwl.basic.service.mobile;
import com.ycwl.basic.model.mobile.chat.*;
import java.util.List;
public interface FaceChatService {
/**
* 获取或创建人脸会话,一脸一会话。
*/
ChatConversationVO getOrCreateConversation(Long faceId, Long memberId);
/**
* 同步发送消息并保存助手回复。
*/
ChatSendMessageResp sendMessage(Long conversationId, Long memberId, String content, String traceId);
/**
* 流式发送消息,支持实时分片回调,仍返回完整结果。
*/
ChatSendMessageStreamResp sendMessageStream(Long conversationId,
Long memberId,
String content,
String traceId,
java.util.function.Consumer<String> chunkConsumer);
/**
* 拉取历史消息,cursor 为最后一条 seq,limit 为条数。
*/
ChatMessagePageResp listMessages(Long conversationId, Integer cursor, Integer limit, Long memberId);
/**
* 关闭会话。
*/
void closeConversation(Long conversationId, Long memberId);
}

View File

@@ -0,0 +1,234 @@
package com.ycwl.basic.service.mobile.impl;
import com.ycwl.basic.exception.BaseException;
import com.ycwl.basic.integration.glm.GlmClient;
import com.ycwl.basic.mapper.FaceChatConversationMapper;
import com.ycwl.basic.mapper.FaceChatMessageMapper;
import com.ycwl.basic.model.mobile.chat.*;
import com.ycwl.basic.model.mobile.chat.entity.FaceChatConversationEntity;
import com.ycwl.basic.model.mobile.chat.entity.FaceChatMessageEntity;
import com.ycwl.basic.model.pc.face.entity.FaceEntity;
import com.ycwl.basic.repository.FaceRepository;
import com.ycwl.basic.service.mobile.FaceChatService;
import com.ycwl.basic.utils.SnowFlakeUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ai.z.openapi.service.model.ChatMessage;
import ai.z.openapi.service.model.ChatMessageRole;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class FaceChatServiceImpl implements FaceChatService {
private static final String STATUS_ACTIVE = "active";
private static final String STATUS_CLOSED = "closed";
private static final String ROLE_USER = "user";
private static final String ROLE_ASSISTANT = "assistant";
private static final String DEFAULT_MODEL = "glm-4.5-airx";
private static final int HISTORY_LIMIT = 50;
private final FaceChatConversationMapper conversationMapper;
private final FaceChatMessageMapper messageMapper;
private final FaceRepository faceRepository;
private final GlmClient glmClient;
@Override
public ChatConversationVO getOrCreateConversation(Long faceId, Long memberId) {
FaceChatConversationEntity exist = conversationMapper.findByFaceId(faceId);
if (exist != null) {
assertOwner(exist, memberId);
return toConversationVO(exist);
}
// DEBUG阶段,暂时不检查
// FaceEntity face = faceRepository.getFace(faceId);
// if (face == null) {
// throw new BaseException("人脸不存在");
// }
// if (!Objects.equals(face.getMemberId(), memberId)) {
// throw new BaseException("无权访问该人脸");
// }
FaceChatConversationEntity entity = new FaceChatConversationEntity();
entity.setId(SnowFlakeUtil.getLongId());
entity.setFaceId(faceId);
entity.setMemberId(memberId);
entity.setStatus(STATUS_ACTIVE);
entity.setModel(DEFAULT_MODEL);
conversationMapper.insert(entity);
return toConversationVO(entity);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ChatSendMessageResp sendMessage(Long conversationId, Long memberId, String content, String traceId) {
ChatSendMessageStreamResp result = doSend(conversationId, memberId, content, traceId, null);
ChatSendMessageResp resp = new ChatSendMessageResp();
resp.setUserMessage(result.getUserMessage());
resp.setAssistantMessage(result.getAssistantMessage());
resp.setTraceId(result.getTraceId());
return resp;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ChatSendMessageStreamResp sendMessageStream(Long conversationId, Long memberId, String content, String traceId,
java.util.function.Consumer<String> chunkConsumer) {
return doSend(conversationId, memberId, content, traceId, chunkConsumer);
}
@Override
public ChatMessagePageResp listMessages(Long conversationId, Integer cursor, Integer limit, Long memberId) {
FaceChatConversationEntity conv = conversationMapper.getById(conversationId);
if (conv == null) {
throw new BaseException("会话不存在");
}
assertOwner(conv, memberId);
int pageSize = limit == null ? 50 : Math.max(1, Math.min(limit, 100));
List<FaceChatMessageEntity> list = messageMapper.listByConversation(conversationId, cursor, pageSize);
List<ChatMessageVO> vos = list.stream().map(this::toMessageVO).collect(Collectors.toList());
ChatMessagePageResp resp = new ChatMessagePageResp();
resp.setMessages(vos);
if (!list.isEmpty()) {
resp.setNextCursor(list.getLast().getSeq());
} else {
resp.setNextCursor(cursor == null ? 0 : cursor);
}
return resp;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void closeConversation(Long conversationId, Long memberId) {
FaceChatConversationEntity conv = conversationMapper.getById(conversationId);
if (conv == null) {
throw new BaseException("会话不存在");
}
assertOwner(conv, memberId);
if (STATUS_CLOSED.equals(conv.getStatus())) {
return;
}
conversationMapper.updateStatus(conversationId, STATUS_CLOSED);
}
private ChatSendMessageStreamResp doSend(Long conversationId, Long memberId, String content, String traceId,
java.util.function.Consumer<String> liveConsumer) {
if (StringUtils.isBlank(content)) {
throw new BaseException("消息内容不能为空");
}
FaceChatConversationEntity conv = conversationMapper.getById(conversationId);
if (conv == null) {
throw new BaseException("会话不存在");
}
assertOwner(conv, memberId);
if (STATUS_CLOSED.equals(conv.getStatus())) {
throw new BaseException("会话已关闭,请重新创建");
}
String resolvedTraceId = StringUtils.isBlank(traceId) ? UUID.randomUUID().toString() : traceId;
Integer maxSeq = messageMapper.maxSeqForUpdate(conversationId);
int baseSeq = maxSeq == null ? 0 : maxSeq;
int userSeq = baseSeq + 1;
FaceChatMessageEntity userMsg = buildMessage(conv, userSeq, ROLE_USER, content, resolvedTraceId, null);
messageMapper.insert(userMsg);
long start = System.currentTimeMillis();
List<FaceChatMessageEntity> recentDesc = messageMapper.listRecentByConversation(conversationId, HISTORY_LIMIT);
Collections.reverse(recentDesc); // 按时间升序
List<ChatMessage> chatMessages = recentDesc.stream()
.map(this::toChatMessage)
.collect(Collectors.toList());
CopyOnWriteArrayList<String> chunks = new CopyOnWriteArrayList<>();
java.util.function.Consumer<String> chunkConsumer = piece -> {
if (StringUtils.isNotBlank(piece)) {
chunks.add(piece);
if (liveConsumer != null) {
liveConsumer.accept(piece);
}
}
};
String assistantText = glmClient.streamReply(conv.getFaceId(), memberId, resolvedTraceId, chatMessages, chunkConsumer);
if (StringUtils.isBlank(assistantText)) {
assistantText = "GLM 暂未接入,稍后再试。";
chunkConsumer.accept(assistantText);
}
int latency = (int) (System.currentTimeMillis() - start);
FaceChatMessageEntity assistantMsg = buildMessage(conv, userSeq + 1, ROLE_ASSISTANT, assistantText, resolvedTraceId, latency);
messageMapper.insert(assistantMsg);
ChatSendMessageStreamResp resp = new ChatSendMessageStreamResp();
resp.setUserMessage(toMessageVO(userMsg));
resp.setAssistantMessage(toMessageVO(assistantMsg));
resp.setTraceId(resolvedTraceId);
resp.setChunks(chunks);
return resp;
}
private FaceChatMessageEntity buildMessage(FaceChatConversationEntity conv, int seq, String role, String content, String traceId, Integer latencyMs) {
FaceChatMessageEntity msg = new FaceChatMessageEntity();
msg.setId(SnowFlakeUtil.getLongId());
msg.setConversationId(conv.getId());
msg.setFaceId(conv.getFaceId());
msg.setSeq(seq);
msg.setRole(role);
msg.setContent(content);
msg.setTraceId(traceId);
msg.setLatencyMs(latencyMs);
msg.setCreatedAt(new Date());
return msg;
}
private void assertOwner(FaceChatConversationEntity conv, Long memberId) {
if (!Objects.equals(conv.getMemberId(), memberId)) {
throw new BaseException("无权访问该会话");
}
}
private ChatConversationVO toConversationVO(FaceChatConversationEntity entity) {
ChatConversationVO vo = new ChatConversationVO();
vo.setConversationId(entity.getId());
vo.setFaceId(entity.getFaceId());
vo.setStatus(entity.getStatus());
vo.setModel(entity.getModel());
return vo;
}
private ChatMessageVO toMessageVO(FaceChatMessageEntity entity) {
ChatMessageVO vo = new ChatMessageVO();
vo.setId(entity.getId());
vo.setSeq(entity.getSeq());
vo.setRole(entity.getRole());
vo.setContent(entity.getContent());
vo.setTraceId(entity.getTraceId());
vo.setCreatedAt(entity.getCreatedAt());
return vo;
}
private ChatMessage toChatMessage(FaceChatMessageEntity entity) {
String role = entity.getRole();
String mappedRole = ChatMessageRole.USER.value();
if (ROLE_ASSISTANT.equalsIgnoreCase(role)) {
mappedRole = ChatMessageRole.ASSISTANT.value();
} else if ("system".equalsIgnoreCase(role)) {
mappedRole = ChatMessageRole.SYSTEM.value();
}
return ChatMessage.builder()
.role(mappedRole)
.content(entity.getContent())
.build();
}
}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.mapper.FaceChatConversationMapper">
<resultMap id="BaseResultMap" type="com.ycwl.basic.model.mobile.chat.entity.FaceChatConversationEntity">
<id column="id" property="id"/>
<result column="face_id" property="faceId"/>
<result column="member_id" property="memberId"/>
<result column="status" property="status"/>
<result column="model" property="model"/>
<result column="created_at" property="createdAt"/>
<result column="updated_at" property="updatedAt"/>
</resultMap>
<select id="findByFaceId" resultMap="BaseResultMap">
select id, face_id, member_id, status, model, created_at, updated_at
from face_chat_conversation
where face_id = #{faceId}
limit 1
</select>
<select id="getById" resultMap="BaseResultMap">
select id, face_id, member_id, status, model, created_at, updated_at
from face_chat_conversation
where id = #{id}
limit 1
</select>
<insert id="insert">
insert into face_chat_conversation
(id, face_id, member_id, status, model, created_at, updated_at)
values
(#{id}, #{faceId}, #{memberId}, #{status}, #{model}, now(), now())
</insert>
<update id="updateStatus">
update face_chat_conversation
set status = #{status}, updated_at = now()
where id = #{id}
</update>
</mapper>

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ycwl.basic.mapper.FaceChatMessageMapper">
<resultMap id="BaseResultMap" type="com.ycwl.basic.model.mobile.chat.entity.FaceChatMessageEntity">
<id column="id" property="id"/>
<result column="conversation_id" property="conversationId"/>
<result column="face_id" property="faceId"/>
<result column="seq" property="seq"/>
<result column="role" property="role"/>
<result column="content" property="content"/>
<result column="trace_id" property="traceId"/>
<result column="latency_ms" property="latencyMs"/>
<result column="created_at" property="createdAt"/>
</resultMap>
<select id="maxSeqForUpdate" resultType="java.lang.Integer">
select ifnull(max(seq), 0)
from face_chat_message
where conversation_id = #{conversationId}
for update
</select>
<insert id="insert">
insert into face_chat_message
(id, conversation_id, face_id, seq, role, content, trace_id, latency_ms, created_at)
values
(#{id}, #{conversationId}, #{faceId}, #{seq}, #{role}, #{content}, #{traceId}, #{latencyMs}, now())
</insert>
<select id="listByConversation" resultMap="BaseResultMap">
select id, conversation_id, face_id, seq, role, content, trace_id, latency_ms, created_at
from face_chat_message
where conversation_id = #{conversationId}
<if test="cursor != null">
and seq &gt; #{cursor}
</if>
order by seq asc
<if test="limit != null">
limit #{limit}
</if>
</select>
<select id="listRecentByConversation" resultMap="BaseResultMap">
select id, conversation_id, face_id, seq, role, content, trace_id, latency_ms, created_at
from face_chat_message
where conversation_id = #{conversationId}
order by seq desc
<if test="limit != null">
limit #{limit}
</if>
</select>
</mapper>