You've already forked FrameTour-BE
- 新增小程序人脸聊天控制器 AppChatController,支持会话创建、消息收发、历史查询及会话关闭 - 集成智谱 GLM 模型客户端 GlmClient,支持流式文本生成与回调 - 新增聊天会话与消息实体类及 MyBatis 映射,实现数据持久化 - 提供 FaceChatService 接口及实现,封装聊天业务逻辑包括同步/流式消息发送 - 引入 zai-sdk 依赖以支持调用智谱 AI 大模型能力 - 支持基于人脸 ID 的唯一会话管理与用户权限校验 - 消息记录包含角色、内容、追踪 ID 及延迟信息,便于调试与分析
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user