1
0
Files
nex/backend/internal/handler/model_handler.go
lanyuanxiaoyao 395887667d feat: 实现统一模型 ID 机制
实现统一模型 ID 格式 (provider_id/model_name),支持跨协议模型标识和 Smart Passthrough。

核心变更:
- 新增 pkg/modelid 包:解析、格式化、校验统一模型 ID
- 数据库迁移:models 表使用 UUID 主键 + UNIQUE(provider_id, model_name) 约束
- Repository 层:FindByProviderAndModelName、ListEnabled 方法
- Service 层:联合唯一校验、provider ID 字符集校验
- Conversion 层:ExtractModelName、RewriteRequestModelName/RewriteResponseModelName 方法
- Handler 层:统一模型 ID 路由、Smart Passthrough、Models API 本地聚合
- 新增 error-responses、unified-model-id 规范

测试覆盖:
- 单元测试:modelid、conversion、handler、service、repository
- 集成测试:统一模型 ID 路由、Smart Passthrough 保真性、跨协议转换
- 迁移测试:UUID 主键、UNIQUE 约束、级联删除

OpenSpec:
- 归档 unified-model-id 变更到 archive/2026-04-21-unified-model-id
- 同步 11 个 delta specs 到 main specs
- 新增 error-responses、unified-model-id 规范文件
2026-04-21 18:14:10 +08:00

181 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
appErrors "nex/backend/pkg/errors"
"nex/backend/internal/domain"
"nex/backend/internal/service"
)
// ModelHandler 模型管理处理器
type ModelHandler struct {
modelService service.ModelService
}
// NewModelHandler 创建模型处理器
func NewModelHandler(modelService service.ModelService) *ModelHandler {
return &ModelHandler{modelService: modelService}
}
// modelResponse 模型响应 DTO扩展 unified_id 字段
type modelResponse struct {
domain.Model
UnifiedModelID string `json:"unified_id"`
}
// newModelResponse 从 domain.Model 构造响应 DTO
func newModelResponse(m *domain.Model) modelResponse {
return modelResponse{
Model: *m,
UnifiedModelID: m.UnifiedModelID(),
}
}
// CreateModel 创建模型
func (h *ModelHandler) CreateModel(c *gin.Context) {
var req struct {
ProviderID string `json:"provider_id" binding:"required"`
ModelName string `json:"model_name" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "缺少必需字段: provider_id, model_name",
})
return
}
model := &domain.Model{
ProviderID: req.ProviderID,
ModelName: req.ModelName,
}
err := h.modelService.Create(model)
if err != nil {
if err == appErrors.ErrProviderNotFound {
c.JSON(http.StatusBadRequest, gin.H{
"error": "供应商不存在",
})
return
}
if err == appErrors.ErrDuplicateModel {
c.JSON(http.StatusConflict, gin.H{
"error": "同一供应商下模型名称已存在",
"code": appErrors.ErrDuplicateModel.Code,
})
return
}
writeError(c, err)
return
}
c.JSON(http.StatusCreated, newModelResponse(model))
}
// ListModels 列出模型
func (h *ModelHandler) ListModels(c *gin.Context) {
providerID := c.Query("provider_id")
models, err := h.modelService.List(providerID)
if err != nil {
writeError(c, err)
return
}
resp := make([]modelResponse, len(models))
for i, m := range models {
resp[i] = newModelResponse(&m)
}
c.JSON(http.StatusOK, resp)
}
// GetModel 获取模型
func (h *ModelHandler) GetModel(c *gin.Context) {
id := c.Param("id")
model, err := h.modelService.Get(id)
if err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{
"error": "模型未找到",
})
return
}
writeError(c, err)
return
}
c.JSON(http.StatusOK, newModelResponse(model))
}
// UpdateModel 更新模型
func (h *ModelHandler) UpdateModel(c *gin.Context) {
id := c.Param("id")
var req map[string]interface{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": "无效的请求格式",
})
return
}
err := h.modelService.Update(id, req)
if err != nil {
if errors.Is(err, appErrors.ErrModelNotFound) {
c.JSON(http.StatusNotFound, gin.H{
"error": "模型未找到",
})
return
}
if errors.Is(err, appErrors.ErrProviderNotFound) {
c.JSON(http.StatusBadRequest, gin.H{
"error": "供应商不存在",
})
return
}
if errors.Is(err, appErrors.ErrDuplicateModel) {
c.JSON(http.StatusConflict, gin.H{
"error": appErrors.ErrDuplicateModel.Message,
"code": appErrors.ErrDuplicateModel.Code,
})
return
}
writeError(c, err)
return
}
model, err := h.modelService.Get(id)
if err != nil {
writeError(c, err)
return
}
c.JSON(http.StatusOK, newModelResponse(model))
}
// DeleteModel 删除模型
func (h *ModelHandler) DeleteModel(c *gin.Context) {
id := c.Param("id")
err := h.modelService.Delete(id)
if err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{
"error": "模型未找到",
})
return
}
writeError(c, err)
return
}
c.Status(http.StatusNoContent)
}