TASK:1.请求穿搭并获取穿搭结果(初步测试)2.对话

This commit is contained in:
2025-10-24 19:02:37 +08:00
parent 12690c2f2c
commit f1c4b50435
14 changed files with 322 additions and 43 deletions

View File

@@ -2,11 +2,13 @@ package com.aida.lanecarford.common.constant;
public class CommonConstants {
public static final String REQUEST_OUTFIT = "http://18.167.251.121:10004/api/v1/chatbot";
public static final String REQUEST_OUTFIT = "http://18.167.251.121:10004/api/v1/agent";
public static final String CHAT = "http://18.167.251.121:10004/api/v1/agent";
public static final String CHAT = "http://18.167.251.121:10004/api/v1/chatbot";
public static final int MINIO_PATH_TIMEOUT = 7 * 24 * 60 * 60; // minio图片临时访问地址 7 天过期second
public static final int CONN_TIMEOUT = 30000; // milliseconds
}

View File

@@ -1,13 +1,28 @@
package com.aida.lanecarford.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.stream.Stream;
@Getter
@AllArgsConstructor
public enum StatusEnum {
SUCCEEDED,
PENDING(0),
FAILED,
SUCCEEDED(1),
FAILED(2),
RUNNING(3);
private int code;
public static StatusEnum of(int code) {
return Stream.of(StatusEnum.values()).filter(v -> v.getCode() == code).findFirst().orElse(null);
}
PENDING,
RUNNING
}

View File

@@ -18,7 +18,7 @@ public enum StylistPathEnum {
private String path;
public static StylistPathEnum of(String name) {
return Stream.of(StylistPathEnum.values()).filter(v -> v.name().equals(name)).findFirst().orElse(null);
return Stream.of(StylistPathEnum.values()).filter(v -> v.getName().equals(name)).findFirst().orElse(null);
}

View File

@@ -40,8 +40,7 @@ public class WebConfig implements WebMvcConfigurer {
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/**/**") // 保护这些路径
.excludePathPatterns(Arrays.asList("/api/auth/precheckAndSendEmail", "/api/auth/register",
"/api/auth/login", "/api/auth/forgotPwd", "/api/style/callback","/api/try-on-effects","/api/customer-photos","/api/visit-records")); // 排除登录接口
"/api/auth/login", "/api/auth/forgotPwd", "/api/style/callback")); // 排除登录接口
}
/**

View File

@@ -0,0 +1,27 @@
package com.aida.lanecarford.controller;
import com.aida.lanecarford.service.ChatService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@Slf4j
@RestController
@RequestMapping("/api/llm")
public class ChatController {
@Resource
private ChatService chatService;
@CrossOrigin
@GetMapping(value = "/streamChat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter streamChat(@RequestParam(required = false) String message,
@RequestParam Long sessionId,
@RequestParam String gender) {
return chatService.streamChat(message, sessionId, gender );
}
}

View File

@@ -1,13 +1,16 @@
package com.aida.lanecarford.controller;
import com.aida.lanecarford.common.ApiResponse;
import com.aida.lanecarford.dto.OutfitCallbackDTO;
import com.aida.lanecarford.dto.RequestOutfitDTO;
import com.aida.lanecarford.service.StyleService;
import com.aida.lanecarford.vo.OutfitResultVO;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 风格配置控制器
@@ -23,9 +26,19 @@ public class StyleController {
private final StyleService styleService;
@PostMapping("/requestOutfit")
public ApiResponse<List<String>> requestOutfit(@Valid @RequestBody RequestOutfitDTO requestOutfitDTO) {
return ApiResponse.success(styleService.requestOutfit(requestOutfitDTO));
}
@PostMapping("/callback")
public void callback(@RequestBody OutfitCallbackDTO callbackDTO) {
styleService.callback(callbackDTO);
}
@GetMapping("/getOutfitResult")
public ApiResponse<List<OutfitResultVO>> getOutfitResult(@RequestParam List<String> requestIDs) {
return ApiResponse.success(styleService.getOutfitResult(requestIDs));
}
}

View File

@@ -1,8 +1,10 @@
package com.aida.lanecarford.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("outfit_request")
public class OutfitRequest extends BaseEntity{
/**
@@ -28,5 +30,5 @@ public class OutfitRequest extends BaseEntity{
/**
* 当前任务状态
*/
private String status;
private int status;
}

View File

@@ -67,7 +67,7 @@ public class Style extends BaseEntity {
* 生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败)
*/
@TableField("generation_status")
private String generationStatus;
private int generationStatus;
/**
* 错误信息

View File

@@ -1,4 +1,8 @@
package com.aida.lanecarford.service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
public interface ChatService {
SseEmitter streamChat(String message, Long sessionId, String gender);
}

View File

@@ -1,17 +1,22 @@
package com.aida.lanecarford.service;
import com.aida.lanecarford.dto.OutfitCallbackDTO;
import com.aida.lanecarford.dto.RequestOutfitDTO;
import com.aida.lanecarford.entity.Style;
import com.aida.lanecarford.vo.OutfitResultVO;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* 风格配置服务接口
*
* @author AI Assistant
* @since 2024-01-01
*/
public interface StyleService extends IService<Style> {
List<String> requestOutfit(RequestOutfitDTO requestOutfitDTO);
void callback(OutfitCallbackDTO callbackDTO);
List<OutfitResultVO> getOutfitResult(List<String> requestIDs);
}

View File

@@ -1,10 +1,216 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.common.constant.CommonConstants;
import com.aida.lanecarford.service.ChatService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service
public class ChatServiceImpl implements ChatService {
/**
* 流式对话方法 - 集成第三方AI接口
*/
public SseEmitter streamChat(String message, Long sessionId, String gender) {
SseEmitter emitter = new SseEmitter(0L);
CompletableFuture.runAsync(() -> {
try {
// 连接第三方AI服务
processAIStream(message, sessionId.toString(), gender, emitter);
emitter.complete();
} catch (Exception e) {
log.error("AI流式对话处理失败", e);
sendError(emitter, "AI服务处理失败: " + e.getMessage());
}
});
return emitter;
}
/**
* 处理第三方AI服务的流式响应
*/
private void processAIStream(String message, String sessionId, String gender, SseEmitter emitter) {
HttpURLConnection connection = null;
try {
// 1. 创建AI服务连接
connection = createAIConnection();
// 2. 发送请求到AI服务
sendToAI(connection, message, sessionId, gender);
// 3. 流式读取并转发响应
streamAIResponse(connection, emitter, sessionId);
} catch (Exception e) {
throw new RuntimeException("AI服务调用失败", e);
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
/**
* 创建AI服务连接
*/
private HttpURLConnection createAIConnection() throws IOException {
URL url = new URL(CommonConstants.CHAT);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/json");
conn.setRequestProperty("Accept", "text/event-stream");
conn.setConnectTimeout(CommonConstants.CONN_TIMEOUT);
conn.setReadTimeout(CommonConstants.CONN_TIMEOUT);
return conn;
}
/**
* 发送请求到AI服务
*/
private void sendToAI(HttpURLConnection connection, String message, String sessionId, String gender)
throws IOException {
// 构建AI服务请求体
JSONObject requestBody = new JSONObject();
requestBody.put("user_message", message);
requestBody.put("user_id", sessionId);
requestBody.put("gender", gender); // 启用流式输出
log.info("发送请求到AI服务: sessionId={}, messageLength={}", sessionId, message.length());
try (OutputStream os = connection.getOutputStream()) {
byte[] input = requestBody.toJSONString().getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
os.flush();
}
}
/**
* 流式读取AI响应并转发
*/
private void streamAIResponse(HttpURLConnection connection, SseEmitter emitter, String sessionId)
throws IOException {
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
throw new RuntimeException("AI服务响应错误: " + responseCode);
}
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
String output = JSON.parseObject(line).getString("output");
// 不用存储数据,直接发送消息
sendRawData(output, emitter, sessionId);
}
// 发送结束信号
sendEndSignal(emitter, sessionId);
} catch (IOException e) {
// 检查是否是连接超时或中断
throw e;
}
}
/**
* 发送错误信息
*/
private void sendErrorChunk(String content, SseEmitter emitter, String sessionId) {
Map<String, Object> response = createResponse("error", content, sessionId);
sendToClient(emitter, response);
}
/**
* 发送原始数据
*/
private void sendRawChunk(JSONObject data, SseEmitter emitter, String sessionId) {
Map<String, Object> response = createResponse("raw", data.toJSONString(), sessionId);
sendToClient(emitter, response);
}
private void sendRawData(String rawData, SseEmitter emitter, String sessionId) {
Map<String, Object> response = createResponse("raw", rawData, sessionId);
sendToClient(emitter, response);
}
/**
* 发送数据到客户端
*/
private void sendToClient(SseEmitter emitter, Map<String, Object> data) {
try {
emitter.send(SseEmitter.event()
.data(data)
.id(UUID.randomUUID().toString()));
} catch (IOException e) {
log.warn("发送数据到客户端失败,可能连接已关闭");
throw new RuntimeException("客户端连接已断开", e);
}
}
/**
* 发送错误信息
*/
private void sendError(SseEmitter emitter, String errorMessage) {
try {
Map<String, Object> errorData = createResponse("error", errorMessage, null);
emitter.send(SseEmitter.event()
.data(errorData)
.name("error"));
} catch (IOException e) {
log.error("发送错误信息失败", e);
}
}
/**
* 发送结束信号
*/
private void sendEndSignal(SseEmitter emitter, String sessionId) {
try {
Map<String, Object> endData = createResponse("end", "对话结束", sessionId);
emitter.send(SseEmitter.event()
.data(endData)
.name("end"));
} catch (IOException e) {
log.warn("发送结束信号失败", e);
}
}
/**
* 创建标准响应格式
*/
private Map<String, Object> createResponse(String type, String content, String sessionId) {
Map<String, Object> response = new HashMap<>();
response.put("type", type);
response.put("content", content);
response.put("sessionId", sessionId);
response.put("timestamp", System.currentTimeMillis());
return response;
}
}

View File

@@ -16,6 +16,7 @@ import com.aida.lanecarford.util.CacheUtil;
import com.aida.lanecarford.util.MinioUtil;
import com.aida.lanecarford.util.SendRequestUtil;
import com.aida.lanecarford.vo.OutfitResultVO;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -45,7 +46,7 @@ public class StyleServiceImpl extends ServiceImpl<StyleMapper, Style> implements
// 请求获取搭配
public void requestOutfit(RequestOutfitDTO requestOutfitDTO) {
public List<String> requestOutfit(RequestOutfitDTO requestOutfitDTO) {
// 请求需要顾客id, 生成的数量,风格
StylistPathEnum stylistPathEnum = StylistPathEnum.of(requestOutfitDTO.getStylist());
@@ -58,17 +59,16 @@ public class StyleServiceImpl extends ServiceImpl<StyleMapper, Style> implements
outfitRequest.setGender(requestOutfitDTO.getGender());
outfitRequestMapper.insert(outfitRequest);
String response = SendRequestUtil.sendPost(CommonConstants.REQUEST_OUTFIT, JSON.toJSONString(params));
if (Objects.isNull(response)) {
outfitRequest.setStatus(StatusEnum.FAILED.name());
JSONObject jsonObject = JSONObject.parseObject(response); // todo 确认这里的status的取值
if (Objects.isNull(response) /*|| !jsonObject.getString("status").equals("ok")*/) {
outfitRequest.setStatus(StatusEnum.FAILED.getCode());
outfitRequestMapper.updateById(outfitRequest);
throw new BusinessException("System error (External interface failure)");
}
// todo 接收返回的requestID
// JSONObject.parse(response, )
List<String> requestIds = new ArrayList<>();
List<String> requestIds = jsonObject.getJSONArray("outfit_ids").toJavaList(String.class);
for(String requestId : requestIds) {
// 生成需要 6~8s, 所以这里可以先请求再存储
@@ -78,7 +78,7 @@ public class StyleServiceImpl extends ServiceImpl<StyleMapper, Style> implements
style.setOutfitRequestId(outfitRequest.getId());
style.setIsSelected(0);
style.setPythonRequestId(requestId);
style.setGenerationStatus(StatusEnum.PENDING.name());
style.setGenerationStatus(StatusEnum.PENDING.getCode());
style.setCreatedTime(LocalDateTime.now());
save(style);
@@ -87,12 +87,13 @@ public class StyleServiceImpl extends ServiceImpl<StyleMapper, Style> implements
OutfitResultVO outfitResultVO = new OutfitResultVO(style.getId(), requestId, StatusEnum.PENDING.name());
cacheUtil.setCache(key, outfitResultVO, RedisURIConstants.verifyCodeTimeout);
}
return requestIds;
}
private Map<String, Object> setRequestOutfitParams(Long customerId, int num, String stylistPath) {
HashMap<String, Object> params = new HashMap<>();
params.put("user_id", customerId);
params.put("user_id", customerId.toString());
params.put("num_outfits", num);
params.put("stylist_path", stylistPath);
@@ -147,7 +148,7 @@ public class StyleServiceImpl extends ServiceImpl<StyleMapper, Style> implements
return;
}
String status = "stop".equals(callbackDTO.getStatus()) ? StatusEnum.SUCCEEDED.name() : StatusEnum.FAILED.name();
int status = "stop".equals(callbackDTO.getStatus()) ? StatusEnum.SUCCEEDED.getCode() : StatusEnum.FAILED.getCode();
outfit.setGenerationStatus(status);
outfit.setStyleImageUrl(callbackDTO.getPath());
outfit.setItems(callbackDTO.getItems());
@@ -159,33 +160,38 @@ public class StyleServiceImpl extends ServiceImpl<StyleMapper, Style> implements
public List<OutfitResultVO> getOutfitResult(List<String> requestIDs) {
ArrayList<OutfitResultVO> resultVOS = new ArrayList<>();
ArrayList<String> reQueryIds = new ArrayList<>();
// 优先从redis中获取结果没有再从数据库中查询
for (String requestID : requestIDs) {
String key = RedisURIConstants.outfitResultCache + requestID;
Object outfit = cacheUtil.getCache(key);
if (Objects.isNull(outfit)){
break;
reQueryIds.add(requestID);
continue;
}
resultVOS.add((OutfitResultVO) outfit);
}
LambdaQueryWrapper<Style> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Style::getPythonRequestId, requestIDs);
if (!reQueryIds.isEmpty()) {
LambdaQueryWrapper<Style> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Style::getPythonRequestId, requestIDs);
List<Style> list = list(queryWrapper);
if (!list.isEmpty()) {
for (Style style : list) {
OutfitResultVO outfitResultVO = new OutfitResultVO();
outfitResultVO.setId(style.getId());
outfitResultVO.setRequestId(style.getPythonRequestId());
outfitResultVO.setStatus(style.getGenerationStatus());
if (!StringUtil.isNullOrEmpty(style.getStyleImageUrl())) {
outfitResultVO.setPath(minioUtil.getPresignedUrl(style.getStyleImageUrl(), CommonConstants.MINIO_PATH_TIMEOUT, null));
List<Style> list = list(queryWrapper);
if (!list.isEmpty()) {
for (Style style : list) {
OutfitResultVO outfitResultVO = new OutfitResultVO();
outfitResultVO.setId(style.getId());
outfitResultVO.setRequestId(style.getPythonRequestId());
outfitResultVO.setStatus(StatusEnum.of(style.getGenerationStatus()).name());
if (!StringUtil.isNullOrEmpty(style.getStyleImageUrl())) {
outfitResultVO.setPath(minioUtil.getPresignedUrl(style.getStyleImageUrl(), CommonConstants.MINIO_PATH_TIMEOUT, null));
}
resultVOS.add(outfitResultVO);
}
resultVOS.add(outfitResultVO);
}
}
return resultVOS;
}