| | |
| | | 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() { |
| | | 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 参数配置"); |
| | |
| | | } |
| | | |
| | | @Override |
| | | public void validateBeforeSave(AiParam aiParam) { |
| | | fillDefaults(aiParam); |
| | | ensureBaseFields(aiParam); |
| | | ensureSingleActive(aiParam, null); |
| | | 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 void validateBeforeUpdate(AiParam aiParam) { |
| | | 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); |
| | | ensureSingleActive(aiParam, aiParam.getId()); |
| | | 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) { |
| | |
| | | } |
| | | } |
| | | |
| | | private void ensureSingleActive(AiParam aiParam, Long selfId) { |
| | | if (aiParam.getStatus() == null || aiParam.getStatus() != StatusType.ENABLE.val) { |
| | | 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::getStatus, StatusType.ENABLE.val); |
| | | .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("当前租户不存在"); |
| | | } |
| | | } |
| | | |
| | |
| | | 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(); |
| | | } |
| | | } |