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 implements AiParamService { private final AiParamValidationSupport aiParamValidationSupport; private final AiRedisSupport aiRedisSupport; @Override public AiParam getActiveParam(Long tenantId) { ensureTenantId(tenantId); AiParam aiParam = this.getOne(new LambdaQueryWrapper() .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 listChatModelOptions(Long tenantId) { ensureTenantId(tenantId); List params = this.list(new LambdaQueryWrapper() .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 ids = list == null ? java.util.List.of() : list.stream() .filter(java.util.Objects::nonNull) .map(item -> (java.io.Serializable) item) .toList(); java.util.List 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 wrapper = new LambdaQueryWrapper() .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() .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 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() .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() .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(); } }