feat(modelprovider): 补齐模型工作台聚合与转换分层

This commit is contained in:
2026-06-01 03:47:48 +08:00
parent d9cf838ace
commit 041ed0b446
23 changed files with 898 additions and 196 deletions

View File

@@ -3,36 +3,33 @@ package com.bruce.modelprovider.controller;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.dto.request.ModelCallLogQueryRequest;
import com.bruce.modelprovider.dto.response.ModelCallLogResponse;
import com.bruce.modelprovider.factory.ModelCallLogFactory;
import com.bruce.modelprovider.service.IModelCallLogService;
import lombok.extern.slf4j.Slf4j;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/model/call-logs")
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelCallLogController {
private final IModelCallLogService modelCallLogService;
private final ModelCallLogFactory modelCallLogFactory;
@PostMapping("/query")
/**
* 方法 query用于执行业务逻辑处理。
*/
public RequestResult<List<ModelCallLogResponse>> query(@RequestBody(required = false) ModelCallLogQueryRequest request) {
log.info("ModelCallLogController.query start, request={}", request);
return RequestResult.success(modelCallLogService.query(request));
}
@GetMapping("/detail")
/**
* 方法 detail用于执行业务逻辑处理。
*/
public RequestResult<ModelCallLogResponse> detail(@RequestParam("id") Long id) {
return RequestResult.success(ModelCallLogResponse.fromEntity(modelCallLogService.getById(id)));
log.info("ModelCallLogController.detail start, id={}", id);
return RequestResult.success(modelCallLogFactory.toResponse(modelCallLogService.getById(id)));
}
}

View File

@@ -4,50 +4,43 @@ import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.dto.request.ModelConfigSaveRequest;
import com.bruce.modelprovider.dto.response.ModelConfigResponse;
import com.bruce.modelprovider.service.IModelConfigService;
import lombok.extern.slf4j.Slf4j;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/model/configs")
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelConfigController {
private final IModelConfigService modelConfigService;
@PostMapping("/query")
/**
* 方法 query用于执行业务逻辑处理。
*/
public RequestResult<List<ModelConfigResponse>> query() {
log.info("ModelConfigController.query start");
return RequestResult.success(modelConfigService.listResponses());
}
@GetMapping("/detail")
/**
* 方法 detail用于执行业务逻辑处理。
*/
public RequestResult<ModelConfigResponse> detail(@RequestParam("id") Long id) {
log.info("ModelConfigController.detail start, id={}", id);
return RequestResult.success(modelConfigService.getResponseById(id));
}
@PostMapping("/save")
/**
* 方法 save用于执行业务逻辑处理。
*/
public RequestResult<Boolean> save(@RequestBody ModelConfigSaveRequest request) {
log.info("ModelConfigController.save start, id={}, modelCode={}",
request == null ? null : request.getId(),
request == null ? null : request.getModelCode());
return RequestResult.success(modelConfigService.saveOrUpdate(request));
}
@PostMapping("/delete")
/**
* 方法 delete用于执行业务逻辑处理。
*/
public RequestResult<Boolean> delete(@RequestParam("id") Long id) {
log.info("ModelConfigController.delete start, id={}", id);
return RequestResult.success(modelConfigService.removeById(id));
}
}

View File

@@ -4,58 +4,49 @@ import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.dto.request.ModelProviderSaveRequest;
import com.bruce.modelprovider.dto.response.ModelProviderResponse;
import com.bruce.modelprovider.service.IModelProviderService;
import lombok.extern.slf4j.Slf4j;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/model/providers")
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelProviderController {
private final IModelProviderService modelProviderService;
@PostMapping("/query")
/**
* 方法 query用于执行业务逻辑处理。
*/
public RequestResult<List<ModelProviderResponse>> query() {
log.info("ModelProviderController.query start");
return RequestResult.success(modelProviderService.listResponses());
}
@GetMapping("/detail")
/**
* 方法 detail用于执行业务逻辑处理。
*/
public RequestResult<ModelProviderResponse> detail(@RequestParam("id") Long id) {
log.info("ModelProviderController.detail start, id={}", id);
return RequestResult.success(modelProviderService.getResponseById(id));
}
@PostMapping("/save")
/**
* 方法 save用于执行业务逻辑处理。
*/
public RequestResult<Boolean> save(@RequestBody ModelProviderSaveRequest request) {
log.info("ModelProviderController.save start, id={}, providerCode={}",
request == null ? null : request.getId(),
request == null ? null : request.getProviderCode());
return RequestResult.success(modelProviderService.saveOrUpdate(request));
}
@PostMapping("/delete")
/**
* 方法 delete用于执行业务逻辑处理。
*/
public RequestResult<Boolean> delete(@RequestParam("id") Long id) {
log.info("ModelProviderController.delete start, id={}", id);
return RequestResult.success(modelProviderService.removeById(id));
}
@PostMapping("/checkHealth")
/**
* 方法 checkHealth用于执行业务逻辑处理。
*/
public RequestResult<Boolean> checkHealth(@RequestParam("id") Long id) {
log.info("ModelProviderController.checkHealth start, id={}", id);
return RequestResult.success(modelProviderService.checkHealth(id));
}
}

View File

@@ -4,50 +4,43 @@ import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.dto.request.ModelRouteRuleSaveRequest;
import com.bruce.modelprovider.dto.response.ModelRouteRuleResponse;
import com.bruce.modelprovider.service.IModelRouteRuleService;
import lombok.extern.slf4j.Slf4j;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/model/routes")
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class ModelRouteRuleController {
private final IModelRouteRuleService modelRouteRuleService;
@PostMapping("/query")
/**
* 方法 query用于执行业务逻辑处理。
*/
public RequestResult<List<ModelRouteRuleResponse>> query() {
log.info("ModelRouteRuleController.query start");
return RequestResult.success(modelRouteRuleService.listResponses());
}
@GetMapping("/detail")
/**
* 方法 detail用于执行业务逻辑处理。
*/
public RequestResult<ModelRouteRuleResponse> detail(@RequestParam("id") Long id) {
log.info("ModelRouteRuleController.detail start, id={}", id);
return RequestResult.success(modelRouteRuleService.getResponseById(id));
}
@PostMapping("/save")
/**
* 方法 save用于执行业务逻辑处理。
*/
public RequestResult<Boolean> save(@RequestBody ModelRouteRuleSaveRequest request) {
log.info("ModelRouteRuleController.save start, id={}, routeCode={}",
request == null ? null : request.getId(),
request == null ? null : request.getRouteCode());
return RequestResult.success(modelRouteRuleService.saveOrUpdate(request));
}
@PostMapping("/delete")
/**
* 方法 delete用于执行业务逻辑处理。
*/
public RequestResult<Boolean> delete(@RequestParam("id") Long id) {
log.info("ModelRouteRuleController.delete start, id={}", id);
return RequestResult.success(modelRouteRuleService.removeById(id));
}
}

View File

@@ -0,0 +1,35 @@
package com.bruce.modelprovider.controller;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.service.IModelWorkspaceService;
import com.bruce.modelprovider.vo.ModelWorkspaceVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 模型工作台聚合接口。
*/
@Tag(name = "模型工作台")
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/model/workspace")
public class ModelWorkspaceController {
private final IModelWorkspaceService modelWorkspaceService;
@Operation(summary = "查询模型与路由工作台聚合视图")
@GetMapping
public RequestResult<ModelWorkspaceVO> getWorkspace() {
log.info("ModelWorkspaceController.getWorkspace start");
ModelWorkspaceVO workspace = modelWorkspaceService.getWorkspace();
log.info("ModelWorkspaceController.getWorkspace success, providerCount={}, modelCount={}, routeRuleCount={}",
workspace.getProviderCount(), workspace.getModelCount(), workspace.getRouteRuleCount());
return RequestResult.success(workspace);
}
}

View File

@@ -4,40 +4,35 @@ import com.bruce.common.domain.model.RequestResult;
import com.bruce.modelprovider.dto.request.RagStoreModelConfigSaveRequest;
import com.bruce.modelprovider.dto.response.RagStoreModelConfigResponse;
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
import lombok.extern.slf4j.Slf4j;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/rag/store")
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class RagStoreModelConfigController {
private final IRagStoreModelConfigService ragStoreModelConfigService;
@GetMapping("/modelConfig")
/**
* 方法 modelConfig用于执行业务逻辑处理。
*/
public RequestResult<RagStoreModelConfigResponse> modelConfig(@RequestParam("storeId") Long storeId) {
log.info("RagStoreModelConfigController.modelConfig start, storeId={}", storeId);
return RequestResult.success(ragStoreModelConfigService.getByStoreId(storeId));
}
@PostMapping("/modelConfig/save")
/**
* 方法 save用于执行业务逻辑处理。
*/
public RequestResult<Boolean> save(@RequestBody RagStoreModelConfigSaveRequest request) {
log.info("RagStoreModelConfigController.save start, storeId={}, embeddingModelId={}",
request == null ? null : request.getStoreId(),
request == null ? null : request.getEmbeddingModelId());
return RequestResult.success(ragStoreModelConfigService.saveOrUpdate(request));
}
@PostMapping("/rebuildIndex")
/**
* 方法 rebuildIndex用于执行业务逻辑处理。
*/
public RequestResult<Boolean> rebuildIndex(@RequestParam("storeId") Long storeId) {
log.info("RagStoreModelConfigController.rebuildIndex start, storeId={}", storeId);
return RequestResult.success(storeId != null);
}
}

View File

@@ -0,0 +1,28 @@
package com.bruce.modelprovider.factory;
import com.bruce.modelprovider.dto.response.ModelCallLogResponse;
import com.bruce.modelprovider.entity.ModelCallLog;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 模型调用日志工厂。
*/
@Component
public class ModelCallLogFactory {
public ModelCallLogResponse toResponse(ModelCallLog entity) {
if (entity == null) {
return null;
}
ModelCallLogResponse response = new ModelCallLogResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
public List<ModelCallLogResponse> toResponses(List<ModelCallLog> entities) {
return entities == null ? List.of() : entities.stream().map(this::toResponse).toList();
}
}

View File

@@ -0,0 +1,57 @@
package com.bruce.modelprovider.factory;
import com.bruce.modelprovider.dto.request.ModelConfigSaveRequest;
import com.bruce.modelprovider.dto.response.ModelConfigResponse;
import com.bruce.modelprovider.entity.ModelConfig;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 模型配置工厂,统一转换模型配置请求、实体和返回对象。
*/
@Component
public class ModelConfigFactory {
public ModelConfig toEntity(ModelConfigSaveRequest request) {
if (request == null) {
return null;
}
ModelConfig entity = new ModelConfig();
entity.setId(request.getId());
entity.setProviderId(request.getProviderId());
entity.setModelCode(trimToNull(request.getModelCode()));
entity.setModelName(trimToNull(request.getModelName()));
entity.setUpstreamModel(trimToNull(request.getUpstreamModel()));
entity.setModelType(trimToNull(request.getModelType()));
entity.setEmbeddingDimension(request.getEmbeddingDimension());
entity.setLocalModel(request.getLocalModel());
entity.setDefaultModel(request.getDefaultModel());
entity.setOptionsJson(trimToNull(request.getOptionsJson()));
entity.setEnabled(request.getEnabled());
entity.setRemark(trimToNull(request.getRemark()));
return entity;
}
public ModelConfigResponse toResponse(ModelConfig entity) {
if (entity == null) {
return null;
}
ModelConfigResponse response = new ModelConfigResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
public List<ModelConfigResponse> toResponses(List<ModelConfig> entities) {
return entities == null ? List.of() : entities.stream().map(this::toResponse).toList();
}
private String trimToNull(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
}

View File

@@ -0,0 +1,65 @@
package com.bruce.modelprovider.factory;
import com.bruce.modelprovider.dto.request.ModelProviderSaveRequest;
import com.bruce.modelprovider.dto.response.ModelProviderResponse;
import com.bruce.modelprovider.entity.ModelProvider;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 模型服务商工厂,统一负责服务商 DTO、Entity、VO 的转换。
*/
@Component
public class ModelProviderFactory {
/**
* 将保存请求转换为实体,统一处理字符串裁剪逻辑。
*/
public ModelProvider toEntity(ModelProviderSaveRequest request) {
if (request == null) {
return null;
}
ModelProvider entity = new ModelProvider();
entity.setId(request.getId());
entity.setProviderCode(trimToNull(request.getProviderCode()));
entity.setProviderName(trimToNull(request.getProviderName()));
entity.setProviderType(trimToNull(request.getProviderType()));
entity.setProtocolType(trimToNull(request.getProtocolType()));
entity.setBaseUrl(trimToNull(request.getBaseUrl()));
entity.setAuthType(trimToNull(request.getAuthType()));
entity.setSecretRef(trimToNull(request.getSecretRef()));
entity.setApiKeyCipher(trimToNull(request.getApiKeyCipher()));
entity.setTimeoutMs(request.getTimeoutMs());
entity.setPriority(request.getPriority());
entity.setEnabled(request.getEnabled());
entity.setRemark(trimToNull(request.getRemark()));
return entity;
}
/**
* 将实体转换为前端返回对象,并隐藏密文字段,只返回是否已配置密钥。
*/
public ModelProviderResponse toResponse(ModelProvider entity) {
if (entity == null) {
return null;
}
ModelProviderResponse response = new ModelProviderResponse();
BeanUtils.copyProperties(entity, response);
response.setHasApiKey(StringUtils.hasText(entity.getApiKeyCipher()));
return response;
}
public List<ModelProviderResponse> toResponses(List<ModelProvider> entities) {
return entities == null ? List.of() : entities.stream().map(this::toResponse).toList();
}
private String trimToNull(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
}

View File

@@ -0,0 +1,57 @@
package com.bruce.modelprovider.factory;
import com.bruce.modelprovider.dto.request.ModelRouteRuleSaveRequest;
import com.bruce.modelprovider.dto.response.ModelRouteRuleResponse;
import com.bruce.modelprovider.entity.ModelRouteRule;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 模型路由规则工厂。
*/
@Component
public class ModelRouteRuleFactory {
public ModelRouteRule toEntity(ModelRouteRuleSaveRequest request) {
if (request == null) {
return null;
}
ModelRouteRule entity = new ModelRouteRule();
entity.setId(request.getId());
entity.setRouteCode(trimToNull(request.getRouteCode()));
entity.setRouteName(trimToNull(request.getRouteName()));
entity.setTaskType(trimToNull(request.getTaskType()));
entity.setMatchScope(trimToNull(request.getMatchScope()));
entity.setScopeId(request.getScopeId());
entity.setPrimaryModelId(request.getPrimaryModelId());
entity.setFallbackModelIdsJson(trimToNull(request.getFallbackModelIdsJson()));
entity.setRouteStrategy(trimToNull(request.getRouteStrategy()));
entity.setMaxLatencyMs(request.getMaxLatencyMs());
entity.setEnabled(request.getEnabled());
entity.setRemark(trimToNull(request.getRemark()));
return entity;
}
public ModelRouteRuleResponse toResponse(ModelRouteRule entity) {
if (entity == null) {
return null;
}
ModelRouteRuleResponse response = new ModelRouteRuleResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
public List<ModelRouteRuleResponse> toResponses(List<ModelRouteRule> entities) {
return entities == null ? List.of() : entities.stream().map(this::toResponse).toList();
}
private String trimToNull(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
}

View File

@@ -0,0 +1,14 @@
package com.bruce.modelprovider.service;
import com.bruce.modelprovider.vo.ModelWorkspaceVO;
/**
* 模型工作台聚合服务。
*/
public interface IModelWorkspaceService {
/**
* 聚合模型服务商、模型、路由和最近失败调用摘要。
*/
ModelWorkspaceVO getWorkspace();
}

View File

@@ -4,37 +4,38 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.modelprovider.dto.request.ModelCallLogQueryRequest;
import com.bruce.modelprovider.dto.response.ModelCallLogResponse;
import com.bruce.modelprovider.entity.ModelCallLog;
import com.bruce.modelprovider.factory.ModelCallLogFactory;
import com.bruce.modelprovider.mapper.ModelCallLogMapper;
import com.bruce.modelprovider.service.IModelCallLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
@Slf4j
@Service
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
@RequiredArgsConstructor
public class ModelCallLogServiceImpl extends ServiceImpl<ModelCallLogMapper, ModelCallLog>
implements IModelCallLogService {
private final ModelCallLogFactory modelCallLogFactory;
@Override
/**
* 方法 query用于执行业务逻辑处理。
*/
public List<ModelCallLogResponse> query(ModelCallLogQueryRequest request) {
log.info("查询模型调用日志开始request={}", request);
ModelCallLogQueryRequest q = request == null ? new ModelCallLogQueryRequest() : request;
return lambdaQuery()
List<ModelCallLogResponse> responses = modelCallLogFactory.toResponses(lambdaQuery()
.eq(StringUtils.hasText(q.getTaskType()), ModelCallLog::getTaskType, q.getTaskType())
.eq(q.getProviderId() != null, ModelCallLog::getProviderId, q.getProviderId())
.eq(q.getModelId() != null, ModelCallLog::getModelId, q.getModelId())
.eq(StringUtils.hasText(q.getStatus()), ModelCallLog::getStatus, q.getStatus())
.eq(StringUtils.hasText(q.getBizType()), ModelCallLog::getBizType, q.getBizType())
.orderByDesc(ModelCallLog::getId)
.list()
.stream()
.map(ModelCallLogResponse::fromEntity)
.toList();
.list());
log.info("查询模型调用日志结束count={}", responses.size());
return responses;
}
}

View File

@@ -4,43 +4,45 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.modelprovider.dto.request.ModelConfigSaveRequest;
import com.bruce.modelprovider.dto.response.ModelConfigResponse;
import com.bruce.modelprovider.entity.ModelConfig;
import com.bruce.modelprovider.factory.ModelConfigFactory;
import com.bruce.modelprovider.mapper.ModelConfigMapper;
import com.bruce.modelprovider.service.IModelConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
@Slf4j
@Service
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
@RequiredArgsConstructor
public class ModelConfigServiceImpl extends ServiceImpl<ModelConfigMapper, ModelConfig> implements IModelConfigService {
private final ModelConfigFactory modelConfigFactory;
@Override
/**
* 方法 listResponses用于执行业务逻辑处理。
*/
public List<ModelConfigResponse> listResponses() {
/**
* 方法 list用于定义接口能力契约。
*/
return list().stream().map(ModelConfigResponse::fromEntity).toList();
log.info("查询模型配置列表开始");
List<ModelConfigResponse> responses = modelConfigFactory.toResponses(list());
log.info("查询模型配置列表结束count={}", responses.size());
return responses;
}
@Override
/**
* 方法 getResponseById用于执行业务逻辑处理。
*/
public ModelConfigResponse getResponseById(Long id) {
return ModelConfigResponse.fromEntity(getById(id));
log.info("查询模型配置详情开始id={}", id);
ModelConfigResponse response = modelConfigFactory.toResponse(getById(id));
log.info("查询模型配置详情结束id={}, found={}", id, response != null);
return response;
}
@Override
/**
* 方法 saveOrUpdate用于执行业务逻辑处理。
*/
public boolean saveOrUpdate(ModelConfigSaveRequest request) {
log.info("保存模型配置开始id={}, providerId={}, modelCode={}",
request == null ? null : request.getId(),
request == null ? null : request.getProviderId(),
request == null ? null : request.getModelCode());
if (request == null) {
throw new IllegalArgumentException("模型保存请求不能为空");
}
@@ -62,24 +64,25 @@ public class ModelConfigServiceImpl extends ServiceImpl<ModelConfigMapper, Model
if (entity == null) {
throw new IllegalArgumentException("模型不存在ID: " + request.getId());
}
entity.setProviderId(request.getProviderId());
entity.setModelCode(request.getModelCode().trim());
entity.setModelName(request.getModelName());
entity.setUpstreamModel(request.getUpstreamModel());
entity.setModelType(request.getModelType());
entity.setEmbeddingDimension(request.getEmbeddingDimension());
entity.setLocalModel(request.getLocalModel());
entity.setDefaultModel(request.getDefaultModel());
entity.setOptionsJson(request.getOptionsJson());
ModelConfig requestEntity = modelConfigFactory.toEntity(request);
entity.setProviderId(requestEntity.getProviderId());
entity.setModelCode(requestEntity.getModelCode());
entity.setModelName(requestEntity.getModelName());
entity.setUpstreamModel(requestEntity.getUpstreamModel());
entity.setModelType(requestEntity.getModelType());
entity.setEmbeddingDimension(requestEntity.getEmbeddingDimension());
entity.setLocalModel(requestEntity.getLocalModel());
entity.setDefaultModel(requestEntity.getDefaultModel());
entity.setOptionsJson(requestEntity.getOptionsJson());
entity.setEnabled(request.getEnabled() == null ? Boolean.TRUE : request.getEnabled());
entity.setRemark(request.getRemark());
return request.getId() == null ? save(entity) : updateById(entity);
entity.setRemark(requestEntity.getRemark());
boolean result = request.getId() == null ? save(entity) : updateById(entity);
log.info("保存模型配置结束id={}, providerId={}, modelCode={}, result={}",
entity.getId(), entity.getProviderId(), entity.getModelCode(), result);
return result;
}
@Override
/**
* 方法 getEnabledModel用于执行业务逻辑处理。
*/
public ModelConfig getEnabledModel(Long modelId) {
ModelConfig model = getById(modelId);
if (model == null || !Boolean.TRUE.equals(model.getEnabled())) {

View File

@@ -6,9 +6,11 @@ import com.bruce.modelprovider.dto.request.ModelProviderSaveRequest;
import com.bruce.modelprovider.dto.response.ModelProviderResponse;
import com.bruce.modelprovider.entity.ModelProvider;
import com.bruce.modelprovider.enums.ModelHealthStatusEnum;
import com.bruce.modelprovider.factory.ModelProviderFactory;
import com.bruce.modelprovider.mapper.ModelProviderMapper;
import com.bruce.modelprovider.service.IModelProviderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@@ -24,39 +26,35 @@ import java.util.List;
* 3. 默认健康状态初始化;
* 4. 对接上游健康检查并回写检查结果。
*/
@Slf4j
@Service
@RequiredArgsConstructor
/**
* ModelProviderServiceImpl负责模型平台对应层的职责。
*/
public class ModelProviderServiceImpl extends ServiceImpl<ModelProviderMapper, ModelProvider>
implements IModelProviderService {
private final OpenAiCompatibleModelClient openAiCompatibleModelClient;
private final ModelProviderFactory modelProviderFactory;
/**
* 查询全部服务商并转换为响应对象。
*/
@Override
/**
* 方法 listResponses用于执行业务逻辑处理。
*/
public List<ModelProviderResponse> listResponses() {
/**
* 方法 list用于定义接口能力契约。
*/
return list().stream().map(ModelProviderResponse::fromEntity).toList();
log.info("查询模型服务商列表开始");
List<ModelProviderResponse> responses = modelProviderFactory.toResponses(list());
log.info("查询模型服务商列表结束count={}", responses.size());
return responses;
}
/**
* 按ID查询服务商详情响应对象已脱敏不返回明文密钥
*/
@Override
/**
* 方法 getResponseById用于执行业务逻辑处理。
*/
public ModelProviderResponse getResponseById(Long id) {
return ModelProviderResponse.fromEntity(getById(id));
log.info("查询模型服务商详情开始id={}", id);
ModelProviderResponse response = modelProviderFactory.toResponse(getById(id));
log.info("查询模型服务商详情结束id={}, found={}", id, response != null);
return response;
}
/**
@@ -64,10 +62,10 @@ public class ModelProviderServiceImpl extends ServiceImpl<ModelProviderMapper, M
* 会执行必要校验:请求非空、编码/名称非空、编码唯一。
*/
@Override
/**
* 方法 saveOrUpdate用于执行业务逻辑处理。
*/
public boolean saveOrUpdate(ModelProviderSaveRequest request) {
log.info("保存模型服务商开始id={}, providerCode={}",
request == null ? null : request.getId(),
request == null ? null : request.getProviderCode());
if (request == null) {
throw new IllegalArgumentException("服务商保存请求不能为空");
}
@@ -88,32 +86,33 @@ public class ModelProviderServiceImpl extends ServiceImpl<ModelProviderMapper, M
if (entity == null) {
throw new IllegalArgumentException("服务商不存在ID: " + request.getId());
}
entity.setProviderCode(request.getProviderCode().trim());
entity.setProviderName(request.getProviderName().trim());
entity.setProviderType(request.getProviderType());
entity.setProtocolType(request.getProtocolType());
entity.setBaseUrl(request.getBaseUrl());
entity.setAuthType(request.getAuthType());
entity.setSecretRef(request.getSecretRef());
entity.setApiKeyCipher(request.getApiKeyCipher());
entity.setTimeoutMs(request.getTimeoutMs());
entity.setPriority(request.getPriority());
ModelProvider requestEntity = modelProviderFactory.toEntity(request);
entity.setProviderCode(requestEntity.getProviderCode());
entity.setProviderName(requestEntity.getProviderName());
entity.setProviderType(requestEntity.getProviderType());
entity.setProtocolType(requestEntity.getProtocolType());
entity.setBaseUrl(requestEntity.getBaseUrl());
entity.setAuthType(requestEntity.getAuthType());
entity.setSecretRef(requestEntity.getSecretRef());
entity.setApiKeyCipher(requestEntity.getApiKeyCipher());
entity.setTimeoutMs(requestEntity.getTimeoutMs());
entity.setPriority(requestEntity.getPriority());
entity.setEnabled(request.getEnabled() == null ? Boolean.TRUE : request.getEnabled());
entity.setRemark(request.getRemark());
entity.setRemark(requestEntity.getRemark());
if (!StringUtils.hasText(entity.getHealthStatus())) {
entity.setHealthStatus(ModelHealthStatusEnum.UNKNOWN.name());
}
return request.getId() == null ? save(entity) : updateById(entity);
boolean result = request.getId() == null ? save(entity) : updateById(entity);
log.info("保存模型服务商结束id={}, providerCode={}, result={}", entity.getId(), entity.getProviderCode(), result);
return result;
}
/**
* 主动健康检查并回写健康状态与检查时间。
*/
@Override
/**
* 方法 checkHealth用于执行业务逻辑处理。
*/
public boolean checkHealth(Long id) {
log.info("模型服务商健康检查开始id={}", id);
ModelProvider provider = getById(id);
if (provider == null) {
throw new IllegalArgumentException("服务商不存在ID: " + id);
@@ -121,10 +120,9 @@ public class ModelProviderServiceImpl extends ServiceImpl<ModelProviderMapper, M
boolean healthy = openAiCompatibleModelClient.health(provider);
provider.setHealthStatus(healthy ? ModelHealthStatusEnum.HEALTHY.name() : ModelHealthStatusEnum.UNHEALTHY.name());
provider.setLastHealthCheckTime(new Date());
/**
* 方法 updateById用于定义接口能力契约。
*/
return updateById(provider);
boolean result = updateById(provider);
log.info("模型服务商健康检查结束id={}, healthStatus={}, result={}", id, provider.getHealthStatus(), result);
return result;
}
}

View File

@@ -4,44 +4,46 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.modelprovider.dto.request.ModelRouteRuleSaveRequest;
import com.bruce.modelprovider.dto.response.ModelRouteRuleResponse;
import com.bruce.modelprovider.entity.ModelRouteRule;
import com.bruce.modelprovider.factory.ModelRouteRuleFactory;
import com.bruce.modelprovider.mapper.ModelRouteRuleMapper;
import com.bruce.modelprovider.service.IModelRouteRuleService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.List;
@Slf4j
@Service
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
@RequiredArgsConstructor
public class ModelRouteRuleServiceImpl extends ServiceImpl<ModelRouteRuleMapper, ModelRouteRule>
implements IModelRouteRuleService {
private final ModelRouteRuleFactory modelRouteRuleFactory;
@Override
/**
* 方法 listResponses用于执行业务逻辑处理。
*/
public List<ModelRouteRuleResponse> listResponses() {
/**
* 方法 list用于定义接口能力契约。
*/
return list().stream().map(ModelRouteRuleResponse::fromEntity).toList();
log.info("查询模型路由规则列表开始");
List<ModelRouteRuleResponse> responses = modelRouteRuleFactory.toResponses(list());
log.info("查询模型路由规则列表结束count={}", responses.size());
return responses;
}
@Override
/**
* 方法 getResponseById用于执行业务逻辑处理。
*/
public ModelRouteRuleResponse getResponseById(Long id) {
return ModelRouteRuleResponse.fromEntity(getById(id));
log.info("查询模型路由规则详情开始id={}", id);
ModelRouteRuleResponse response = modelRouteRuleFactory.toResponse(getById(id));
log.info("查询模型路由规则详情结束id={}, found={}", id, response != null);
return response;
}
@Override
/**
* 方法 saveOrUpdate用于执行业务逻辑处理。
*/
public boolean saveOrUpdate(ModelRouteRuleSaveRequest request) {
log.info("保存模型路由规则开始id={}, routeCode={}, taskType={}",
request == null ? null : request.getId(),
request == null ? null : request.getRouteCode(),
request == null ? null : request.getTaskType());
if (request == null || !StringUtils.hasText(request.getRouteCode())) {
throw new IllegalArgumentException("路由编码不能为空");
}
@@ -56,18 +58,21 @@ public class ModelRouteRuleServiceImpl extends ServiceImpl<ModelRouteRuleMapper,
if (entity == null) {
throw new IllegalArgumentException("路由不存在ID: " + request.getId());
}
entity.setRouteCode(request.getRouteCode().trim());
entity.setRouteName(request.getRouteName());
entity.setTaskType(request.getTaskType());
entity.setMatchScope(request.getMatchScope());
entity.setScopeId(request.getScopeId());
entity.setPrimaryModelId(request.getPrimaryModelId());
entity.setFallbackModelIdsJson(request.getFallbackModelIdsJson());
entity.setRouteStrategy(request.getRouteStrategy());
entity.setMaxLatencyMs(request.getMaxLatencyMs());
ModelRouteRule requestEntity = modelRouteRuleFactory.toEntity(request);
entity.setRouteCode(requestEntity.getRouteCode());
entity.setRouteName(requestEntity.getRouteName());
entity.setTaskType(requestEntity.getTaskType());
entity.setMatchScope(requestEntity.getMatchScope());
entity.setScopeId(requestEntity.getScopeId());
entity.setPrimaryModelId(requestEntity.getPrimaryModelId());
entity.setFallbackModelIdsJson(requestEntity.getFallbackModelIdsJson());
entity.setRouteStrategy(requestEntity.getRouteStrategy());
entity.setMaxLatencyMs(requestEntity.getMaxLatencyMs());
entity.setEnabled(request.getEnabled() == null ? Boolean.TRUE : request.getEnabled());
entity.setRemark(request.getRemark());
return request.getId() == null ? save(entity) : updateById(entity);
entity.setRemark(requestEntity.getRemark());
boolean result = request.getId() == null ? save(entity) : updateById(entity);
log.info("保存模型路由规则结束id={}, routeCode={}, result={}", entity.getId(), entity.getRouteCode(), result);
return result;
}
}

View File

@@ -0,0 +1,77 @@
package com.bruce.modelprovider.service.impl;
import com.bruce.modelprovider.dto.request.ModelCallLogQueryRequest;
import com.bruce.modelprovider.dto.response.ModelCallLogResponse;
import com.bruce.modelprovider.dto.response.ModelConfigResponse;
import com.bruce.modelprovider.dto.response.ModelProviderResponse;
import com.bruce.modelprovider.dto.response.ModelRouteRuleResponse;
import com.bruce.modelprovider.service.IModelCallLogService;
import com.bruce.modelprovider.service.IModelConfigService;
import com.bruce.modelprovider.service.IModelProviderService;
import com.bruce.modelprovider.service.IModelRouteRuleService;
import com.bruce.modelprovider.service.IModelWorkspaceService;
import com.bruce.modelprovider.vo.ModelWorkspaceFailureVO;
import com.bruce.modelprovider.vo.ModelWorkspaceVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 模型工作台聚合服务实现。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ModelWorkspaceServiceImpl implements IModelWorkspaceService {
private static final String HEALTHY = "HEALTHY";
private static final String UNHEALTHY = "UNHEALTHY";
private static final String FAILED = "FAILED";
private final IModelProviderService modelProviderService;
private final IModelConfigService modelConfigService;
private final IModelRouteRuleService modelRouteRuleService;
private final IModelCallLogService modelCallLogService;
@Override
public ModelWorkspaceVO getWorkspace() {
log.info("模型工作台查询开始");
List<ModelProviderResponse> providers = modelProviderService.listResponses();
List<ModelConfigResponse> models = modelConfigService.listResponses();
List<ModelRouteRuleResponse> routes = modelRouteRuleService.listResponses();
List<ModelCallLogResponse> logs = modelCallLogService.query(new ModelCallLogQueryRequest());
List<ModelCallLogResponse> failedLogs = logs.stream()
.filter(log -> FAILED.equals(log.getStatus()))
.limit(5)
.toList();
ModelWorkspaceVO workspace = new ModelWorkspaceVO();
workspace.setProviderCount(providers.size());
workspace.setHealthyProviderCount((int) providers.stream().filter(provider -> HEALTHY.equals(provider.getHealthStatus())).count());
workspace.setUnhealthyProviderCount((int) providers.stream().filter(provider -> UNHEALTHY.equals(provider.getHealthStatus())).count());
workspace.setModelCount(models.size());
workspace.setEnabledModelCount((int) models.stream().filter(model -> Boolean.TRUE.equals(model.getEnabled())).count());
workspace.setRouteRuleCount(routes.size());
workspace.setEnabledRouteRuleCount((int) routes.stream().filter(route -> Boolean.TRUE.equals(route.getEnabled())).count());
workspace.setRecentFailedCallCount(failedLogs.size());
workspace.setProviders(providers);
workspace.setModels(models);
workspace.setRoutes(routes);
workspace.setFailedCallSummaries(failedLogs.stream().map(this::toFailureVO).toList());
log.info("模型工作台查询结束providerCount={}, modelCount={}, routeRuleCount={}, failedCallCount={}",
workspace.getProviderCount(), workspace.getModelCount(), workspace.getRouteRuleCount(), workspace.getRecentFailedCallCount());
return workspace;
}
private ModelWorkspaceFailureVO toFailureVO(ModelCallLogResponse log) {
ModelWorkspaceFailureVO vo = new ModelWorkspaceFailureVO();
vo.setRequestId(log.getRequestId());
vo.setStatus(log.getStatus());
vo.setErrorCode(log.getErrorCode());
vo.setErrorMessage(log.getErrorMessage());
return vo;
}
}

View File

@@ -9,31 +9,28 @@ import com.bruce.modelprovider.mapper.RagStoreModelConfigMapper;
import com.bruce.modelprovider.service.IModelConfigService;
import com.bruce.modelprovider.service.IRagStoreModelConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
/**
* 该类属于模型平台模块,用于承载对应分层职责。
*/
public class RagStoreModelConfigServiceImpl extends ServiceImpl<RagStoreModelConfigMapper, RagStoreModelConfig>
implements IRagStoreModelConfigService {
private final IModelConfigService modelConfigService;
@Override
/**
* 方法 getByStoreId用于执行业务逻辑处理。
*/
public RagStoreModelConfigResponse getByStoreId(Long storeId) {
log.info("查询知识库模型配置开始storeId={}", storeId);
return RagStoreModelConfigResponse.fromEntity(getActiveEntity(storeId));
}
@Override
/**
* 方法 saveOrUpdate用于执行业务逻辑处理。
*/
public boolean saveOrUpdate(RagStoreModelConfigSaveRequest request) {
log.info("保存知识库模型配置开始storeId={}, embeddingModelId={}",
request == null ? null : request.getStoreId(),
request == null ? null : request.getEmbeddingModelId());
if (request == null || request.getStoreId() == null) {
throw new IllegalArgumentException("知识库模型配置请求不能为空");
}
@@ -57,26 +54,23 @@ public class RagStoreModelConfigServiceImpl extends ServiceImpl<RagStoreModelCon
entity.setRemark(request.getRemark());
if (current == null) {
entity.setIndexVersion(1);
/**
* 方法 save用于定义接口能力契约。
*/
return save(entity);
boolean result = save(entity);
log.info("保存知识库模型配置结束storeId={}, indexVersion={}, result={}",
entity.getStoreId(), entity.getIndexVersion(), result);
return result;
}
boolean changed = !current.getEmbeddingModelId().equals(request.getEmbeddingModelId())
|| !current.getEmbeddingDimension().equals(request.getEmbeddingDimension());
if (changed) {
entity.setIndexVersion(current.getIndexVersion() == null ? 2 : current.getIndexVersion() + 1);
}
/**
* 方法 updateById用于定义接口能力契约。
*/
return updateById(entity);
boolean result = updateById(entity);
log.info("保存知识库模型配置结束storeId={}, indexVersion={}, result={}",
entity.getStoreId(), entity.getIndexVersion(), result);
return result;
}
@Override
/**
* 方法 getActiveEntity用于执行业务逻辑处理。
*/
public RagStoreModelConfig getActiveEntity(Long storeId) {
return lambdaQuery().eq(RagStoreModelConfig::getStoreId, storeId)
.eq(RagStoreModelConfig::getActive, true)

View File

@@ -0,0 +1,24 @@
package com.bruce.modelprovider.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 最近失败调用摘要。
*/
@Data
@Schema(description = "模型调用失败摘要")
public class ModelWorkspaceFailureVO {
@Schema(description = "请求ID")
private String requestId;
@Schema(description = "调用状态")
private String status;
@Schema(description = "错误码")
private String errorCode;
@Schema(description = "错误摘要")
private String errorMessage;
}

View File

@@ -0,0 +1,54 @@
package com.bruce.modelprovider.vo;
import com.bruce.modelprovider.dto.response.ModelConfigResponse;
import com.bruce.modelprovider.dto.response.ModelProviderResponse;
import com.bruce.modelprovider.dto.response.ModelRouteRuleResponse;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 模型工作台聚合视图。
*/
@Data
@Schema(description = "模型与路由工作台聚合视图")
public class ModelWorkspaceVO {
@Schema(description = "服务商总数")
private Integer providerCount;
@Schema(description = "健康服务商数量")
private Integer healthyProviderCount;
@Schema(description = "异常服务商数量")
private Integer unhealthyProviderCount;
@Schema(description = "模型总数")
private Integer modelCount;
@Schema(description = "启用模型数量")
private Integer enabledModelCount;
@Schema(description = "路由规则总数")
private Integer routeRuleCount;
@Schema(description = "启用路由规则数量")
private Integer enabledRouteRuleCount;
@Schema(description = "最近失败调用数量")
private Integer recentFailedCallCount;
@Schema(description = "服务商列表")
private List<ModelProviderResponse> providers = new ArrayList<>();
@Schema(description = "模型列表")
private List<ModelConfigResponse> models = new ArrayList<>();
@Schema(description = "路由规则列表")
private List<ModelRouteRuleResponse> routes = new ArrayList<>();
@Schema(description = "最近失败调用摘要")
private List<ModelWorkspaceFailureVO> failedCallSummaries = new ArrayList<>();
}

View File

@@ -0,0 +1,150 @@
package com.bruce.modelprovider.factory;
import com.bruce.modelprovider.dto.request.ModelConfigSaveRequest;
import com.bruce.modelprovider.dto.request.ModelProviderSaveRequest;
import com.bruce.modelprovider.dto.request.ModelRouteRuleSaveRequest;
import com.bruce.modelprovider.dto.response.ModelCallLogResponse;
import com.bruce.modelprovider.dto.response.ModelConfigResponse;
import com.bruce.modelprovider.dto.response.ModelProviderResponse;
import com.bruce.modelprovider.dto.response.ModelRouteRuleResponse;
import com.bruce.modelprovider.entity.ModelCallLog;
import com.bruce.modelprovider.entity.ModelConfig;
import com.bruce.modelprovider.entity.ModelProvider;
import com.bruce.modelprovider.entity.ModelRouteRule;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ModelProviderFactoryTests {
private final ModelProviderFactory modelProviderFactory = new ModelProviderFactory();
private final ModelConfigFactory modelConfigFactory = new ModelConfigFactory();
private final ModelRouteRuleFactory modelRouteRuleFactory = new ModelRouteRuleFactory();
private final ModelCallLogFactory modelCallLogFactory = new ModelCallLogFactory();
@Test
void modelProviderFactoryShouldTrimRequestAndHideCipherInResponse() {
ModelProviderSaveRequest request = new ModelProviderSaveRequest();
request.setId(1L);
request.setProviderCode(" OPENAI_MAIN ");
request.setProviderName(" OpenAI 主服务 ");
request.setProviderType(" OPENAI ");
request.setProtocolType(" OPENAI_COMPATIBLE ");
request.setBaseUrl(" https://api.openai.com/v1 ");
request.setAuthType(" BEARER_TOKEN ");
request.setSecretRef(" OPENAI_KEY ");
request.setTimeoutMs(60000);
request.setPriority(100);
request.setEnabled(true);
request.setRemark(" 主路由服务商 ");
ModelProvider entity = modelProviderFactory.toEntity(request);
assertNotNull(entity);
assertEquals("OPENAI_MAIN", entity.getProviderCode());
assertEquals("OpenAI 主服务", entity.getProviderName());
assertEquals("OPENAI", entity.getProviderType());
assertEquals("OPENAI_COMPATIBLE", entity.getProtocolType());
assertEquals("https://api.openai.com/v1", entity.getBaseUrl());
assertEquals("BEARER_TOKEN", entity.getAuthType());
assertEquals("OPENAI_KEY", entity.getSecretRef());
assertEquals("主路由服务商", entity.getRemark());
ModelProviderResponse response = modelProviderFactory.toResponse(entity);
assertFalse(Boolean.TRUE.equals(response.getHasApiKey()));
entity.setApiKeyCipher("cipher");
response = modelProviderFactory.toResponse(entity);
assertTrue(Boolean.TRUE.equals(response.getHasApiKey()));
}
@Test
void modelConfigFactoryShouldMapFullConfigFields() {
ModelConfigSaveRequest request = new ModelConfigSaveRequest();
request.setId(2L);
request.setProviderId(1L);
request.setModelCode(" EMB_1024 ");
request.setModelName(" text-embedding-3-large ");
request.setUpstreamModel(" text-embedding-3-large ");
request.setModelType(" EMBEDDING ");
request.setEmbeddingDimension(1024);
request.setLocalModel(false);
request.setDefaultModel(true);
request.setOptionsJson(" {\"dimensions\":1024} ");
request.setEnabled(true);
request.setRemark(" 默认向量模型 ");
ModelConfig entity = modelConfigFactory.toEntity(request);
assertEquals("EMB_1024", entity.getModelCode());
assertEquals("text-embedding-3-large", entity.getModelName());
assertEquals("text-embedding-3-large", entity.getUpstreamModel());
assertEquals("EMBEDDING", entity.getModelType());
assertEquals("{\"dimensions\":1024}", entity.getOptionsJson());
assertEquals("默认向量模型", entity.getRemark());
ModelConfigResponse response = modelConfigFactory.toResponse(entity);
assertEquals(1024, response.getEmbeddingDimension());
assertTrue(Boolean.TRUE.equals(response.getDefaultModel()));
}
@Test
void modelRouteRuleFactoryShouldMapRouteFields() {
ModelRouteRuleSaveRequest request = new ModelRouteRuleSaveRequest();
request.setId(3L);
request.setRouteCode(" RAG_GLOBAL ");
request.setRouteName(" 全局检索路由 ");
request.setTaskType(" RAG_RETRIEVAL ");
request.setMatchScope(" GLOBAL ");
request.setScopeId(1001L);
request.setPrimaryModelId(2L);
request.setFallbackModelIdsJson(" [3,4] ");
request.setRouteStrategy(" MANUAL ");
request.setMaxLatencyMs(3000);
request.setEnabled(true);
request.setRemark(" 全局默认规则 ");
ModelRouteRule entity = modelRouteRuleFactory.toEntity(request);
assertEquals("RAG_GLOBAL", entity.getRouteCode());
assertEquals("全局检索路由", entity.getRouteName());
assertEquals("RAG_RETRIEVAL", entity.getTaskType());
assertEquals("[3,4]", entity.getFallbackModelIdsJson());
assertEquals("全局默认规则", entity.getRemark());
ModelRouteRuleResponse response = modelRouteRuleFactory.toResponse(entity);
assertEquals(2L, response.getPrimaryModelId());
assertEquals("MANUAL", response.getRouteStrategy());
}
@Test
void modelCallLogFactoryShouldExposeReadonlyView() {
ModelCallLog entity = new ModelCallLog();
entity.setId(10L);
entity.setRequestId("req-001");
entity.setProviderId(1L);
entity.setModelId(2L);
entity.setTaskType("RAG_EMBEDDING");
entity.setBizType("RAG_DOCUMENT_INDEX");
entity.setBizId("doc-1");
entity.setCallType("EMBEDDING");
entity.setStatus("FAILED");
entity.setDurationMs(2200);
entity.setErrorCode("TIMEOUT");
entity.setErrorMessage("上游超时");
entity.setEstimatedCost(new BigDecimal("0.00250000"));
ModelCallLogResponse response = modelCallLogFactory.toResponse(entity);
assertNotNull(response);
assertEquals("req-001", response.getRequestId());
assertEquals("RAG_EMBEDDING", response.getTaskType());
assertEquals("RAG_DOCUMENT_INDEX", response.getBizType());
assertEquals("FAILED", response.getStatus());
assertEquals("TIMEOUT", response.getErrorCode());
assertEquals("上游超时", response.getErrorMessage());
}
}

View File

@@ -0,0 +1,123 @@
package com.bruce.modelprovider.workspace;
import com.bruce.modelprovider.dto.response.ModelCallLogResponse;
import com.bruce.modelprovider.dto.response.ModelConfigResponse;
import com.bruce.modelprovider.dto.response.ModelProviderResponse;
import com.bruce.modelprovider.dto.response.ModelRouteRuleResponse;
import com.bruce.modelprovider.service.IModelCallLogService;
import com.bruce.modelprovider.service.IModelConfigService;
import com.bruce.modelprovider.service.IModelProviderService;
import com.bruce.modelprovider.service.IModelRouteRuleService;
import com.bruce.modelprovider.service.impl.ModelWorkspaceServiceImpl;
import com.bruce.modelprovider.vo.ModelWorkspaceVO;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ModelWorkspaceServiceTests {
@Mock
private IModelProviderService modelProviderService;
@Mock
private IModelConfigService modelConfigService;
@Mock
private IModelRouteRuleService modelRouteRuleService;
@Mock
private IModelCallLogService modelCallLogService;
@InjectMocks
private ModelWorkspaceServiceImpl modelWorkspaceService;
@Test
void getWorkspaceShouldAggregateHealthRoutesAndFailureSummary() {
when(modelProviderService.listResponses()).thenReturn(List.of(
provider(1L, "OPENAI_MAIN", "HEALTHY", true),
provider(2L, "LOCAL_OLLAMA", "UNHEALTHY", true),
provider(3L, "TEST_PROVIDER", "UNKNOWN", false)
));
when(modelConfigService.listResponses()).thenReturn(List.of(
model(11L, 1L, "CHAT_MAIN", "CHAT", true),
model(12L, 1L, "EMB_1024", "EMBEDDING", true),
model(13L, 2L, "CHAT_FALLBACK", "CHAT", false)
));
when(modelRouteRuleService.listResponses()).thenReturn(List.of(
route(21L, "RAG_GLOBAL", "RAG_RETRIEVAL", 11L, "[13]", true),
route(22L, "CHAT_GLOBAL", "AGENT_CHAT", 11L, "[]", false)
));
when(modelCallLogService.query(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(
log("req-001", "FAILED", "TIMEOUT", "上游超时"),
log("req-002", "FAILED", "AUTH_ERROR", "鉴权失败"),
log("req-003", "SUCCESS", null, null)
));
ModelWorkspaceVO workspace = modelWorkspaceService.getWorkspace();
assertNotNull(workspace);
assertEquals(3, workspace.getProviderCount());
assertEquals(1, workspace.getHealthyProviderCount());
assertEquals(1, workspace.getUnhealthyProviderCount());
assertEquals(3, workspace.getModelCount());
assertEquals(2, workspace.getEnabledModelCount());
assertEquals(2, workspace.getRouteRuleCount());
assertEquals(1, workspace.getEnabledRouteRuleCount());
assertEquals(2, workspace.getRecentFailedCallCount());
assertEquals(2, workspace.getFailedCallSummaries().size());
assertEquals(3, workspace.getProviders().size());
assertEquals(3, workspace.getModels().size());
assertEquals(2, workspace.getRoutes().size());
}
private ModelProviderResponse provider(Long id, String code, String healthStatus, boolean enabled) {
ModelProviderResponse response = new ModelProviderResponse();
response.setId(id);
response.setProviderCode(code);
response.setProviderName(code);
response.setHealthStatus(healthStatus);
response.setEnabled(enabled);
return response;
}
private ModelConfigResponse model(Long id, Long providerId, String code, String modelType, boolean enabled) {
ModelConfigResponse response = new ModelConfigResponse();
response.setId(id);
response.setProviderId(providerId);
response.setModelCode(code);
response.setModelName(code);
response.setModelType(modelType);
response.setEnabled(enabled);
return response;
}
private ModelRouteRuleResponse route(Long id, String code, String taskType, Long primaryModelId, String fallbackJson, boolean enabled) {
ModelRouteRuleResponse response = new ModelRouteRuleResponse();
response.setId(id);
response.setRouteCode(code);
response.setRouteName(code);
response.setTaskType(taskType);
response.setPrimaryModelId(primaryModelId);
response.setFallbackModelIdsJson(fallbackJson);
response.setEnabled(enabled);
return response;
}
private ModelCallLogResponse log(String requestId, String status, String errorCode, String errorMessage) {
ModelCallLogResponse response = new ModelCallLogResponse();
response.setRequestId(requestId);
response.setStatus(status);
response.setErrorCode(errorCode);
response.setErrorMessage(errorMessage);
return response;
}
}