package com.vincent.rsf.server.ai.service.impl;
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.vincent.rsf.framework.exception.CoolException;
|
import com.vincent.rsf.server.ai.config.AiDefaults;
|
import com.vincent.rsf.server.ai.dto.AiChatModelOptionDto;
|
import com.vincent.rsf.server.ai.dto.AiParamValidateResultDto;
|
import com.vincent.rsf.server.ai.entity.AiParam;
|
import com.vincent.rsf.server.ai.mapper.AiParamMapper;
|
import com.vincent.rsf.server.ai.service.AiParamService;
|
import com.vincent.rsf.server.system.enums.StatusType;
|
import lombok.RequiredArgsConstructor;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.util.StringUtils;
|
|
import java.text.SimpleDateFormat;
|
import java.util.Date;
|
import java.util.List;
|
|
@Service("aiParamService")
|
@RequiredArgsConstructor
|
public class AiParamServiceImpl extends ServiceImpl<AiParamMapper, AiParam> implements AiParamService {
|
|
private final AiParamValidationSupport aiParamValidationSupport;
|
private final AiRedisSupport aiRedisSupport;
|
|
@Override
|
public AiParam getActiveParam(Long tenantId) {
|
ensureTenantId(tenantId);
|
AiParam aiParam = this.getOne(new LambdaQueryWrapper<AiParam>()
|
.eq(AiParam::getTenantId, tenantId)
|
.eq(AiParam::getStatus, StatusType.ENABLE.val)
|
.eq(AiParam::getDeleted, 0)
|
.last("limit 1"));
|
if (aiParam == null) {
|
throw new CoolException("未找到启用中的 AI 参数配置");
|
}
|
return aiParam;
|
}
|
|
@Override
|
public AiParam getChatParam(Long tenantId, Long aiParamId) {
|
ensureTenantId(tenantId);
|
if (aiParamId == null) {
|
return getActiveParam(tenantId);
|
}
|
AiParam aiParam = requireOwnedRecord(aiParamId, tenantId);
|
if (!AiDefaults.PARAM_VALIDATE_VALID.equals(aiParam.getValidateStatus())) {
|
throw new CoolException("所选 AI 模型未通过校验,暂不可用于对话");
|
}
|
return aiParam;
|
}
|
|
@Override
|
public List<AiChatModelOptionDto> listChatModelOptions(Long tenantId) {
|
ensureTenantId(tenantId);
|
List<AiParam> params = this.list(new LambdaQueryWrapper<AiParam>()
|
.eq(AiParam::getTenantId, tenantId)
|
.eq(AiParam::getDeleted, 0)
|
.eq(AiParam::getValidateStatus, AiDefaults.PARAM_VALIDATE_VALID)
|
.orderByDesc(AiParam::getStatus)
|
.orderByDesc(AiParam::getUpdateTime)
|
.orderByDesc(AiParam::getCreateTime)
|
.orderByDesc(AiParam::getId));
|
if (params.isEmpty()) {
|
return List.of(toChatModelOption(getActiveParam(tenantId)));
|
}
|
return params.stream()
|
.map(this::toChatModelOption)
|
.toList();
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public AiParam setDefaultParam(Long id, Long tenantId, Long userId) {
|
ensureTenantId(tenantId);
|
if (id == null) {
|
throw new CoolException("AI 参数 ID 不能为空");
|
}
|
AiParam target = requireOwnedRecord(id, tenantId);
|
if (!AiDefaults.PARAM_VALIDATE_VALID.equals(target.getValidateStatus())) {
|
throw new CoolException("仅允许将校验通过的 AI 参数设置为默认");
|
}
|
Date now = new Date();
|
this.lambdaUpdate()
|
.eq(AiParam::getTenantId, tenantId)
|
.eq(AiParam::getDeleted, 0)
|
.set(AiParam::getStatus, StatusType.DISABLE.val)
|
.set(AiParam::getUpdateBy, userId)
|
.set(AiParam::getUpdateTime, now)
|
.update();
|
target.setStatus(StatusType.ENABLE.val);
|
target.setUpdateBy(userId);
|
target.setUpdateTime(now);
|
if (!super.updateById(target)) {
|
throw new CoolException("设置默认 AI 参数失败");
|
}
|
aiRedisSupport.evictTenantConfigCaches(tenantId);
|
return target;
|
}
|
|
@Override
|
public void validateBeforeSave(AiParam aiParam, Long tenantId) {
|
ensureTenantId(tenantId);
|
aiParam.setTenantId(tenantId);
|
fillDefaults(aiParam);
|
ensureBaseFields(aiParam);
|
ensureSingleActive(tenantId, null, aiParam.getStatus());
|
applyValidation(aiParam);
|
}
|
|
@Override
|
public void validateBeforeUpdate(AiParam aiParam, Long tenantId) {
|
ensureTenantId(tenantId);
|
fillDefaults(aiParam);
|
if (aiParam.getId() == null) {
|
throw new CoolException("AI 参数 ID 不能为空");
|
}
|
AiParam current = requireOwnedRecord(aiParam.getId(), tenantId);
|
aiParam.setTenantId(current.getTenantId());
|
ensureBaseFields(aiParam);
|
ensureDefaultStillExists(tenantId, current, aiParam.getStatus());
|
ensureSingleActive(tenantId, aiParam.getId(), aiParam.getStatus());
|
applyValidation(aiParam);
|
}
|
|
@Override
|
public AiParamValidateResultDto validateDraft(AiParam aiParam, Long tenantId) {
|
ensureTenantId(tenantId);
|
fillDefaults(aiParam);
|
ensureBaseFields(aiParam);
|
return aiParamValidationSupport.validate(aiParam);
|
}
|
|
@Override
|
public boolean save(AiParam entity) {
|
boolean saved = super.save(entity);
|
if (saved && entity != null && entity.getTenantId() != null) {
|
aiRedisSupport.evictTenantConfigCaches(entity.getTenantId());
|
}
|
return saved;
|
}
|
|
@Override
|
public boolean updateById(AiParam entity) {
|
boolean updated = super.updateById(entity);
|
if (updated && entity != null && entity.getTenantId() != null) {
|
aiRedisSupport.evictTenantConfigCaches(entity.getTenantId());
|
}
|
return updated;
|
}
|
|
@Override
|
public boolean removeByIds(java.util.Collection<?> list) {
|
java.util.List<java.io.Serializable> ids = list == null ? java.util.List.of() : list.stream()
|
.filter(java.util.Objects::nonNull)
|
.map(item -> (java.io.Serializable) item)
|
.toList();
|
java.util.List<AiParam> records = this.listByIds(ids);
|
ensureRemovingDefaultIsSafe(records);
|
boolean removed = super.removeByIds(list);
|
if (removed) {
|
records.stream()
|
.map(AiParam::getTenantId)
|
.filter(java.util.Objects::nonNull)
|
.distinct()
|
.forEach(aiRedisSupport::evictTenantConfigCaches);
|
}
|
return removed;
|
}
|
|
private void ensureBaseFields(AiParam aiParam) {
|
if (!StringUtils.hasText(aiParam.getName())) {
|
throw new CoolException("AI 参数名称不能为空");
|
}
|
if (!StringUtils.hasText(aiParam.getProviderType())) {
|
aiParam.setProviderType(AiDefaults.PROVIDER_OPENAI_COMPATIBLE);
|
}
|
if (!StringUtils.hasText(aiParam.getBaseUrl())) {
|
throw new CoolException("AI Base URL 不能为空");
|
}
|
if (!StringUtils.hasText(aiParam.getApiKey())) {
|
throw new CoolException("AI API Key 不能为空");
|
}
|
if (!StringUtils.hasText(aiParam.getModel())) {
|
throw new CoolException("AI 模型不能为空");
|
}
|
}
|
|
private void ensureSingleActive(Long tenantId, Long selfId, Integer status) {
|
if (status == null || status != StatusType.ENABLE.val) {
|
return;
|
}
|
LambdaQueryWrapper<AiParam> wrapper = new LambdaQueryWrapper<AiParam>()
|
.eq(AiParam::getTenantId, tenantId)
|
.eq(AiParam::getStatus, StatusType.ENABLE.val)
|
.eq(AiParam::getDeleted, 0);
|
if (selfId != null) {
|
wrapper.ne(AiParam::getId, selfId);
|
}
|
if (this.count(wrapper) > 0) {
|
throw new CoolException("同一租户仅允许一条启用中的 AI 参数配置");
|
}
|
}
|
|
private void ensureDefaultStillExists(Long tenantId, AiParam current, Integer nextStatus) {
|
if (current == null || current.getStatus() == null || current.getStatus() != StatusType.ENABLE.val) {
|
return;
|
}
|
if (nextStatus != null && nextStatus == StatusType.ENABLE.val) {
|
return;
|
}
|
long otherDefaultCount = this.count(new LambdaQueryWrapper<AiParam>()
|
.eq(AiParam::getTenantId, tenantId)
|
.eq(AiParam::getDeleted, 0)
|
.eq(AiParam::getStatus, StatusType.ENABLE.val)
|
.ne(AiParam::getId, current.getId()));
|
if (otherDefaultCount == 0) {
|
throw new CoolException("请先将其他 AI 参数设置为默认,再取消当前默认");
|
}
|
}
|
|
private void ensureRemovingDefaultIsSafe(List<AiParam> records) {
|
if (records == null || records.isEmpty()) {
|
return;
|
}
|
records.stream()
|
.filter(item -> item.getTenantId() != null && item.getStatus() != null && item.getStatus() == StatusType.ENABLE.val)
|
.map(AiParam::getTenantId)
|
.distinct()
|
.forEach(this::ensureTenantHasRemainingDefaultAfterRemove);
|
}
|
|
private void ensureTenantHasRemainingDefaultAfterRemove(Long tenantId) {
|
long defaultCount = this.count(new LambdaQueryWrapper<AiParam>()
|
.eq(AiParam::getTenantId, tenantId)
|
.eq(AiParam::getDeleted, 0)
|
.eq(AiParam::getStatus, StatusType.ENABLE.val));
|
if (defaultCount <= 1) {
|
throw new CoolException("默认 AI 参数不能直接删除,请先将其他配置设为默认");
|
}
|
}
|
|
private AiParam requireOwnedRecord(Long id, Long tenantId) {
|
AiParam aiParam = this.getOne(new LambdaQueryWrapper<AiParam>()
|
.eq(AiParam::getId, id)
|
.eq(AiParam::getTenantId, tenantId)
|
.eq(AiParam::getDeleted, 0)
|
.last("limit 1"));
|
if (aiParam == null) {
|
throw new CoolException("AI 参数不存在或无权访问");
|
}
|
return aiParam;
|
}
|
|
private void ensureTenantId(Long tenantId) {
|
if (tenantId == null) {
|
throw new CoolException("当前租户不存在");
|
}
|
}
|
|
private void fillDefaults(AiParam aiParam) {
|
if (!StringUtils.hasText(aiParam.getProviderType())) {
|
aiParam.setProviderType(AiDefaults.PROVIDER_OPENAI_COMPATIBLE);
|
}
|
if (aiParam.getTemperature() == null) {
|
aiParam.setTemperature(AiDefaults.DEFAULT_TEMPERATURE);
|
}
|
if (aiParam.getTopP() == null) {
|
aiParam.setTopP(AiDefaults.DEFAULT_TOP_P);
|
}
|
if (aiParam.getTimeoutMs() == null) {
|
aiParam.setTimeoutMs(AiDefaults.DEFAULT_TIMEOUT_MS);
|
}
|
if (aiParam.getStreamingEnabled() == null) {
|
aiParam.setStreamingEnabled(Boolean.TRUE);
|
}
|
if (!StringUtils.hasText(aiParam.getValidateStatus())) {
|
aiParam.setValidateStatus(AiDefaults.PARAM_VALIDATE_NOT_TESTED);
|
}
|
if (aiParam.getStatus() == null) {
|
aiParam.setStatus(StatusType.ENABLE.val);
|
}
|
}
|
|
private void applyValidation(AiParam aiParam) {
|
AiParamValidateResultDto validateResult = aiParamValidationSupport.validate(aiParam);
|
aiParam.setValidateStatus(validateResult.getStatus());
|
aiParam.setLastValidateMessage(validateResult.getMessage());
|
aiParam.setLastValidateElapsedMs(validateResult.getElapsedMs());
|
aiParam.setLastValidateTime(parseDate(validateResult.getValidatedAt()));
|
if (!AiDefaults.PARAM_VALIDATE_VALID.equals(validateResult.getStatus())) {
|
throw new CoolException(validateResult.getMessage());
|
}
|
}
|
|
private Date parseDate(String dateTime) {
|
if (!StringUtils.hasText(dateTime)) {
|
return null;
|
}
|
try {
|
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateTime);
|
} catch (Exception e) {
|
throw new CoolException("解析校验时间失败: " + e.getMessage());
|
}
|
}
|
|
private AiChatModelOptionDto toChatModelOption(AiParam aiParam) {
|
return AiChatModelOptionDto.builder()
|
.aiParamId(aiParam.getId())
|
.name(aiParam.getName())
|
.model(aiParam.getModel())
|
.providerType(aiParam.getProviderType())
|
.active(aiParam.getStatus() != null && aiParam.getStatus() == StatusType.ENABLE.val)
|
.build();
|
}
|
}
|