feat: AI 助手模型管理(CRUD、会话绑定、自动化验收)
- Flyway V6:模型表、默认种子、ai_chat_session.model_id 回填 - 管理端 /api/admin/ai/models;用户端选模与新会话绑定 - 禁用模型后会话只读(5009);chat 按会话 modelCode 调 DashScope - 前端 AiModels 页、助手模型选择器与 localStorage - 单测、AiModelManagementApiIT、Playwright E2E;e2e Profile 关闭注解限流 - 技术实施文档与验收报告;移除过期冲突分析文档 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -61,6 +61,21 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-testcontainers</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>mysql</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# E2E / 浏览器联调:线上 MySQL + Redis(与 application.yml 同主机)
|
||||
# 默认库名 jog_test;若无权限可先设环境变量 MYSQL_DATABASE=jog
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://${MYSQL_HOST:81.70.204.4}:3306/${MYSQL_DATABASE:jog}?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
|
||||
username: ${MYSQL_USERNAME:jog}
|
||||
password: ${MYSQL_PASSWORD:Jg.123459}
|
||||
data:
|
||||
redis:
|
||||
host: ${REDIS_HOST:81.70.204.4}
|
||||
port: ${REDIS_PORT:6379}
|
||||
password: ${REDIS_PASSWORD:Rd.123459}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
|
||||
|
||||
dashscope:
|
||||
api-key: ${DASHSCOPE_API_KEY:integration-test-key}
|
||||
model: ${DASHSCOPE_MODEL:qwen-turbo}
|
||||
timeout: 60000
|
||||
rate-limit-per-minute: 100
|
||||
|
||||
logging:
|
||||
level:
|
||||
fun.nojava: INFO
|
||||
org.springframework.security: WARN
|
||||
|
||||
jog:
|
||||
rate-limit:
|
||||
enabled: false
|
||||
@@ -0,0 +1,31 @@
|
||||
-- V6__ai_assistant_model.sql
|
||||
-- AI 助手可配置模型 + 会话绑定
|
||||
|
||||
CREATE TABLE ai_assistant_model (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
model_code VARCHAR(64) NOT NULL COMMENT 'DashScope model 参数',
|
||||
display_name VARCHAR(32) NOT NULL,
|
||||
description VARCHAR(200) NULL,
|
||||
enabled TINYINT NOT NULL DEFAULT 1,
|
||||
allow_image TINYINT NOT NULL DEFAULT 0,
|
||||
allow_voice TINYINT NOT NULL DEFAULT 0,
|
||||
is_default TINYINT NOT NULL DEFAULT 0,
|
||||
sort INT NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted TINYINT NOT NULL DEFAULT 0,
|
||||
INDEX idx_enabled_sort (enabled, sort, id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO ai_assistant_model (
|
||||
model_code, display_name, description, enabled, allow_image, allow_voice, is_default, sort
|
||||
) VALUES (
|
||||
'qwen-turbo', '默认模型', '系统迁移种子模型', 1, 0, 0, 1, 0
|
||||
);
|
||||
|
||||
ALTER TABLE ai_chat_session
|
||||
ADD COLUMN model_id BIGINT NULL COMMENT '绑定 ai_assistant_model.id' AFTER user_id;
|
||||
|
||||
UPDATE ai_chat_session
|
||||
SET model_id = (SELECT id FROM ai_assistant_model WHERE is_default = 1 AND deleted = 0 LIMIT 1)
|
||||
WHERE model_id IS NULL;
|
||||
@@ -0,0 +1,287 @@
|
||||
package fun.nojava.admin.integration;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import fun.nojava.admin.JogApplication;
|
||||
import fun.nojava.module.blog.service.impl.StubAiSummaryServiceImpl;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* AI 助手模型管理 API 集成测试(待办 18、19)。
|
||||
* <p>
|
||||
* 使用 {@code e2e} Profile:线上 MySQL {@code jog_test} + 线上 Redis(见 application-e2e.yml)。
|
||||
*/
|
||||
@SpringBootTest(classes = JogApplication.class)
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("e2e")
|
||||
@Import(IntegrationTestConfig.class)
|
||||
@Tag("integration")
|
||||
@Tag("p0")
|
||||
class AiModelManagementApiIT {
|
||||
|
||||
private static final String ADMIN_ACCOUNT = "admin";
|
||||
/** 启动时 {@link fun.nojava.admin.config.DatabaseInitializationVerifier} 会重置为此密码 */
|
||||
private static final String ADMIN_PASSWORD = "admin@123";
|
||||
private static final String TEST_USER_PASSWORD = "User@Test1";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private StubAiSummaryServiceImpl stubAiSummaryService;
|
||||
|
||||
private String adminToken;
|
||||
private String userToken;
|
||||
private Long createdModelId;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
stubAiSummaryService.setCallModelHandler(null);
|
||||
adminToken = loginAdmin();
|
||||
deleteModelByCodeIfExists(AiModelTestConstants.IT_MODEL_CODE);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws Exception {
|
||||
if (createdModelId != null && adminToken != null) {
|
||||
mockMvc.perform(delete("/api/admin/ai/models/" + createdModelId)
|
||||
.header("Authorization", bearer(adminToken)))
|
||||
.andReturn();
|
||||
}
|
||||
deleteModelByCodeIfExists(AiModelTestConstants.IT_MODEL_CODE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-AI-MODEL-IT-18: 管理端上架 qwen3-vl-32b-thinking → 用户新会话绑定 → chat 使用对应 modelCode")
|
||||
void shouldCreateModelAndChatWithBoundModelCode() throws Exception {
|
||||
createdModelId = createModel(
|
||||
AiModelTestConstants.IT_MODEL_CODE,
|
||||
AiModelTestConstants.IT_DISPLAY_NAME,
|
||||
true);
|
||||
|
||||
userToken = registerAndLoginUser();
|
||||
|
||||
Long modelId = findAppModelId();
|
||||
assertEquals(createdModelId, modelId);
|
||||
|
||||
Long sessionId = createSession(userToken, modelId);
|
||||
|
||||
MvcResult chatResult = mockMvc.perform(post("/api/app/ai/chat")
|
||||
.header("Authorization", bearer(userToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"sessionId":%d,"message":"集成测试:用一句话说明 REST 是什么?"}
|
||||
""".formatted(sessionId)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.model").value(AiModelTestConstants.IT_MODEL_CODE))
|
||||
.andExpect(jsonPath("$.data.modelDisplayName").value(AiModelTestConstants.IT_DISPLAY_NAME))
|
||||
.andReturn();
|
||||
|
||||
JsonNode chatJson = objectMapper.readTree(chatResult.getResponse().getContentAsString());
|
||||
assertNotNull(chatJson.path("data").path("reply").asText(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("TC-AI-MODEL-IT-19: 禁用 qwen3-vl-32b-thinking 后旧会话只读且 chat 返回 AI_MODEL_DISABLED")
|
||||
void shouldRejectChatWhenModelDisabled() throws Exception {
|
||||
createdModelId = createModel(
|
||||
AiModelTestConstants.IT_MODEL_CODE,
|
||||
AiModelTestConstants.IT_DISPLAY_NAME_DISABLED,
|
||||
true);
|
||||
userToken = registerAndLoginUser();
|
||||
|
||||
Long sessionId = createSession(userToken, createdModelId);
|
||||
chatOnce(userToken, sessionId, "集成测试:先发送一条消息建立会话。");
|
||||
|
||||
disableModel(createdModelId);
|
||||
|
||||
mockMvc.perform(get("/api/app/ai/sessions/" + sessionId + "/messages")
|
||||
.header("Authorization", bearer(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.modelEnabled").value(false))
|
||||
.andExpect(jsonPath("$.data.readOnlyReason").value("MODEL_DISABLED"));
|
||||
|
||||
MvcResult rejected = mockMvc.perform(post("/api/app/ai/chat")
|
||||
.header("Authorization", bearer(userToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"sessionId":%d,"message":"禁用后不应成功发送"}
|
||||
""".formatted(sessionId)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
|
||||
JsonNode body = objectMapper.readTree(rejected.getResponse().getContentAsString());
|
||||
assertEquals(5009, body.path("code").asInt());
|
||||
}
|
||||
|
||||
private void deleteModelByCodeIfExists(String modelCode) throws Exception {
|
||||
if (adminToken == null) {
|
||||
return;
|
||||
}
|
||||
MvcResult list = mockMvc.perform(get("/api/admin/ai/models")
|
||||
.header("Authorization", bearer(adminToken)))
|
||||
.andReturn();
|
||||
JsonNode body = objectMapper.readTree(list.getResponse().getContentAsString());
|
||||
if (body.path("code").asInt() != 200) {
|
||||
return;
|
||||
}
|
||||
for (JsonNode row : body.path("data")) {
|
||||
if (modelCode.equals(row.path("modelCode").asText())) {
|
||||
long id = row.path("id").asLong();
|
||||
if (!row.path("isDefault").asBoolean(false)) {
|
||||
mockMvc.perform(delete("/api/admin/ai/models/" + id)
|
||||
.header("Authorization", bearer(adminToken)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String loginAdmin() throws Exception {
|
||||
MvcResult result = mockMvc.perform(post("/api/public/admin/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"email":"%s","password":"%s","deviceFingerprint":"ai-model-it"}
|
||||
""".formatted(ADMIN_ACCOUNT, ADMIN_PASSWORD)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.accessToken").exists())
|
||||
.andReturn();
|
||||
return extractToken(result);
|
||||
}
|
||||
|
||||
private String registerAndLoginUser() throws Exception {
|
||||
String email = "ai-it-" + UUID.randomUUID().toString().substring(0, 8) + "@test.local";
|
||||
mockMvc.perform(post("/api/public/user/register")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"email":"%s","password":"%s","nickname":"AI集成测试用户"}
|
||||
""".formatted(email, TEST_USER_PASSWORD)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
|
||||
MvcResult result = mockMvc.perform(post("/api/public/user/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{"email":"%s","password":"%s","deviceFingerprint":"ai-model-it"}
|
||||
""".formatted(email, TEST_USER_PASSWORD)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andReturn();
|
||||
return extractToken(result);
|
||||
}
|
||||
|
||||
private Long createModel(String modelCode, String displayName, boolean enabled) throws Exception {
|
||||
MvcResult result = mockMvc.perform(post("/api/admin/ai/models")
|
||||
.header("Authorization", bearer(adminToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"modelCode":"%s",
|
||||
"displayName":"%s",
|
||||
"description":"API 集成测试模型",
|
||||
"enabled":%s,
|
||||
"allowImageUpload":false,
|
||||
"allowVoiceUpload":false,
|
||||
"isDefault":false,
|
||||
"sort":99
|
||||
}
|
||||
""".formatted(modelCode, displayName, enabled)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.id").exists())
|
||||
.andReturn();
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString())
|
||||
.path("data").path("id").asLong();
|
||||
}
|
||||
|
||||
private void disableModel(Long modelId) throws Exception {
|
||||
mockMvc.perform(put("/api/admin/ai/models/" + modelId)
|
||||
.header("Authorization", bearer(adminToken))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"modelCode":"%s",
|
||||
"displayName":"%s",
|
||||
"enabled":false,
|
||||
"allowImageUpload":false,
|
||||
"allowVoiceUpload":false,
|
||||
"isDefault":false,
|
||||
"sort":99
|
||||
}
|
||||
""".formatted(
|
||||
AiModelTestConstants.IT_MODEL_CODE,
|
||||
AiModelTestConstants.IT_DISPLAY_NAME_DISABLED)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
}
|
||||
|
||||
private Long findAppModelId() throws Exception {
|
||||
MvcResult result = mockMvc.perform(get("/api/app/ai/models")
|
||||
.header("Authorization", bearer(userToken)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andReturn();
|
||||
JsonNode models = objectMapper.readTree(result.getResponse().getContentAsString()).path("data");
|
||||
for (JsonNode model : models) {
|
||||
if (createdModelId.equals(model.path("id").asLong())) {
|
||||
return model.path("id").asLong();
|
||||
}
|
||||
}
|
||||
fail("未在用户端模型列表中找到 id=" + createdModelId);
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long createSession(String token, Long modelId) throws Exception {
|
||||
MvcResult result = mockMvc.perform(post("/api/app/ai/sessions")
|
||||
.header("Authorization", bearer(token))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"modelId\":" + modelId + "}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200))
|
||||
.andExpect(jsonPath("$.data.sessionId").exists())
|
||||
.andReturn();
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString())
|
||||
.path("data").path("sessionId").asLong();
|
||||
}
|
||||
|
||||
private void chatOnce(String token, Long sessionId, String message) throws Exception {
|
||||
mockMvc.perform(post("/api/app/ai/chat")
|
||||
.header("Authorization", bearer(token))
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"sessionId\":" + sessionId + ",\"message\":\"" + message + "\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(200));
|
||||
}
|
||||
|
||||
private String extractToken(MvcResult result) throws Exception {
|
||||
return objectMapper.readTree(result.getResponse().getContentAsString())
|
||||
.path("data").path("accessToken").asText();
|
||||
}
|
||||
|
||||
private static String bearer(String token) {
|
||||
return "Bearer " + token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package fun.nojava.admin.integration;
|
||||
|
||||
final class AiModelTestConstants {
|
||||
|
||||
static final String IT_MODEL_CODE = "qwen3-vl-32b-thinking";
|
||||
static final String IT_DISPLAY_NAME = "Qwen3 VL 32B Thinking";
|
||||
static final String IT_DISPLAY_NAME_DISABLED = "Qwen3 VL 32B(禁用联调)";
|
||||
|
||||
private AiModelTestConstants() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package fun.nojava.admin.integration;
|
||||
|
||||
import fun.nojava.module.blog.config.DashScopeProperties;
|
||||
import fun.nojava.module.blog.service.impl.StubAiSummaryServiceImpl;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
@TestConfiguration
|
||||
public class IntegrationTestConfig {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
StubAiSummaryServiceImpl stubAiSummaryService(
|
||||
DashScopeProperties properties,
|
||||
StringRedisTemplate redisTemplate) {
|
||||
return new StubAiSummaryServiceImpl(properties, redisTemplate);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package fun.nojava.admin.integration;
|
||||
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
import org.testcontainers.containers.GenericContainer;
|
||||
import org.testcontainers.containers.MySQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
|
||||
/**
|
||||
* Testcontainers 基类:MySQL(jog_test)+ Redis,供 API 集成测试复用。
|
||||
*/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
abstract class IntegrationTestContainers {
|
||||
|
||||
private static final DockerImageName MYSQL_IMAGE = DockerImageName.parse("mysql:8.4");
|
||||
private static final DockerImageName REDIS_IMAGE = DockerImageName.parse("redis:7-alpine");
|
||||
|
||||
@Container
|
||||
@SuppressWarnings("resource")
|
||||
static final MySQLContainer<?> MYSQL = new MySQLContainer<>(MYSQL_IMAGE)
|
||||
.withDatabaseName("jog_test")
|
||||
.withUsername("jog")
|
||||
.withPassword("JogTest.123")
|
||||
.withReuse(true);
|
||||
|
||||
@Container
|
||||
@SuppressWarnings("resource")
|
||||
static final GenericContainer<?> REDIS = new GenericContainer<>(REDIS_IMAGE)
|
||||
.withExposedPorts(6379)
|
||||
.withReuse(true);
|
||||
|
||||
@DynamicPropertySource
|
||||
static void registerProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.datasource.url", MYSQL::getJdbcUrl);
|
||||
registry.add("spring.datasource.username", MYSQL::getUsername);
|
||||
registry.add("spring.datasource.password", MYSQL::getPassword);
|
||||
registry.add("spring.data.redis.host", REDIS::getHost);
|
||||
registry.add("spring.data.redis.port", () -> REDIS.getMappedPort(6379));
|
||||
registry.add("spring.data.redis.password", () -> "");
|
||||
registry.add("spring.data.redis.database", () -> "0");
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package fun.nojava.module.blog.service.impl;
|
||||
|
||||
import fun.nojava.module.blog.config.DashScopeProperties;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 集成测试用:拦截 DashScope 调用,避免外网依赖。
|
||||
*/
|
||||
public class StubAiSummaryServiceImpl extends AiSummaryServiceImpl {
|
||||
|
||||
private Function<Map<String, Object>, Map<String, Object>> callModelHandler = StubAiSummaryServiceImpl::defaultResponse;
|
||||
|
||||
public StubAiSummaryServiceImpl(DashScopeProperties properties, StringRedisTemplate redisTemplate) {
|
||||
super(properties, redisTemplate);
|
||||
}
|
||||
|
||||
public void setCallModelHandler(Function<Map<String, Object>, Map<String, Object>> handler) {
|
||||
this.callModelHandler = handler == null ? StubAiSummaryServiceImpl::defaultResponse : handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> callModel(Map<String, Object> requestBody) {
|
||||
return callModelHandler.apply(requestBody);
|
||||
}
|
||||
|
||||
private static Map<String, Object> defaultResponse(Map<String, Object> request) {
|
||||
String model = (String) request.get("model");
|
||||
return Map.of(
|
||||
"choices", List.of(Map.of("message", Map.of("content", "集成测试 AI 回复"))),
|
||||
"model", model == null ? "qwen-turbo" : model,
|
||||
"usage", Map.of("prompt_tokens", 5, "completion_tokens", 10)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
spring:
|
||||
flyway:
|
||||
enabled: true
|
||||
locations: classpath:db/migration
|
||||
baseline-on-migrate: true
|
||||
baseline-version: 0
|
||||
mail:
|
||||
host: localhost
|
||||
port: 1025
|
||||
username: test
|
||||
password: test
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl
|
||||
|
||||
dashscope:
|
||||
api-key: integration-test-key
|
||||
model: qwen-turbo
|
||||
timeout: 10000
|
||||
max-input: 8000
|
||||
rate-limit-per-minute: 100
|
||||
|
||||
logging:
|
||||
level:
|
||||
fun.nojava: INFO
|
||||
org.springframework.security: WARN
|
||||
org.testcontainers: INFO
|
||||
@@ -0,0 +1 @@
|
||||
junit.platform.discovery.includeClassNamePattern=^.*(Test|Tests|IT)$
|
||||
Reference in New Issue
Block a user