package service import ( "context" "encoding/json" "net/url" "observer-server/biz/consts" "observer-server/biz/dao" "observer-server/biz/model/dto" ) // modelVersionService 模型版本业务:管理端无模型管理界面(2026-08-26 决策), // 版本记录仅支撑客户端模型目录(App 多模型下载热更新)。文件布局见 common/workspace.go。 type modelVersionService struct{} var ModelVersion = &modelVersionService{} // ClientCatalog 客户端模型目录:全部数据集当前生效模型 + 综合模型(App 多模型下载热更新)。 // 双档位(2026-09-03):每数据集 s/n 两档 is_latest=1 各一条;downloadUrl 复用 /download 静态托管 // (/download/trainings/<基名>.tflite,n 档带 _n 后缀,基名前缀空回退数据集名)。 // 综合模型(2026-09-09):kind=combined 行 dataset_id=0,条目 datasetName=综合、fileBase=combined(+_n)、 // 带 datasetIds 覆盖列表,无封面。 func (s *modelVersionService) ClientCatalog(ctx context.Context) (*dto.ModelCatalogRes, error) { list, err := dao.ModelVersion.ListAllLatest(ctx) if err != nil { return nil, err } names := Training.datasetNameMap(ctx) modelNames := Training.datasetModelNameMap(ctx) items := make([]*dto.ModelCatalogItem, 0, len(list)) for _, v := range list { kind := v.Kind if kind == "" { kind = consts.TrainingKindSpecies } var datasetName, fileBase, coverUrl string var datasetIds []int64 if kind == consts.TrainingKindCombined { datasetName = "综合" fileBase = consts.TrainingCombinedBase if v.Variant == consts.TrainingVariantN { fileBase += consts.TrainingVariantNFileSuffix } if err := json.Unmarshal([]byte(v.DatasetIds), &datasetIds); err != nil || datasetIds == nil { datasetIds = []int64{} } } else { datasetName = names[v.DatasetId] if datasetName == "" { continue } // 双档位:每数据集最多 2 条生效记录(is_latest 按 (数据集,档位) 各一),n 档 downloadUrl 带 _n 后缀; // cover 属数据集(两档同物种同图源),沿用基名前缀 fileBase = modelNames[v.DatasetId] if v.Variant == consts.TrainingVariantN { fileBase += consts.TrainingVariantNFileSuffix } coverUrl = "/api/v1/app/cover?namePrefix=" + url.PathEscape(modelNames[v.DatasetId]) } items = append(items, &dto.ModelCatalogItem{ DatasetId: v.DatasetId, DatasetName: datasetName, Variant: v.Variant, Kind: kind, DatasetIds: datasetIds, Version: v.Version, Labels: parseLabels(v.Labels), SizeBytes: v.SizeBytes, Sha256: v.Sha256, Notes: v.Notes, PublishedAt: v.CreatedAt, DownloadUrl: "/download/trainings/" + fileBase + ".tflite", CoverUrl: coverUrl, }) } return &dto.ModelCatalogRes{List: items}, nil } // parseLabels 解析类别名 JSON 数组 func parseLabels(s string) []string { var out []string if err := json.Unmarshal([]byte(s), &out); err != nil || len(out) == 0 { return []string{"class0", "class1"} } return out }