Files
observer/server/biz/service/app_version.go
T
admin a0b115d954 训练体系整合与标注单阶段化
- 标注:AI 预标注直写 labels_json(去候选确认两阶段);重叠去重(minIoU);全量标注按钮
- 训练:脚本迁移入 server/training/(Go 化 prepare_yolo/analyze_rfdetr,保留 train_server.py);tflite 产物自检并入训练流程(check_tflite)
- 数据目录/权重不进 git;.gitignore 迁移至仓库根
2026-08-26 18:22:56 +08:00

173 lines
5.9 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 service
import (
"context"
"os"
"path/filepath"
"regexp"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/os/gtime"
"observer-server/biz/dao"
"observer-server/biz/model/dto"
"observer-server/biz/model/entity"
"observer-server/common"
)
// appVersionService App 版本业务:客户端更新检查(取最新)、管理端下发(APK 上传 + 记录)。
// APK 存固定文件名(覆盖式,目录永远只有一个文件),见 common/apk_store.go。
type appVersionService struct{}
// apkNameRe APK 文件名规范:observer-x.y.z.apk(版本号从文件名识别)
var apkNameRe = regexp.MustCompile(`^observer-(\d+\.\d+\.\d+)\.apk$`)
var AppVersion = &appVersionService{}
// GetUpdate 客户端版本更新检查(公开接口,无需登录态;仅 Android 调用):
// 返回最新一条版本记录 + 模型热更新目录(多模型体系,与 GET /api/v1/models 同构)。
// 无版本记录时返回空结构(客户端视为无需更新);无发布模型时 models 不返回。
func (s *appVersionService) GetUpdate(ctx context.Context, req *dto.AppUpdateReq) (*dto.AppUpdateRes, error) {
res := &dto.AppUpdateRes{}
latest, err := dao.AppVersion.Latest(ctx)
if err != nil {
return nil, err
}
if latest != nil {
res.Version = latest.Version
res.Notes = latest.Notes
}
catalog, err := ModelVersion.ClientCatalog(ctx)
if err != nil {
return nil, err
}
if len(catalog.List) > 0 {
res.Models = catalog.List
}
return res, nil
}
// AdminListVersions 管理端版本记录分页(按下发时间倒序)
func (s *appVersionService) AdminListVersions(ctx context.Context, req *dto.AdminAppVersionListReq) (*dto.AdminAppVersionListRes, error) {
page, size := common.NormalizePage(req.Page, req.Size)
list, total, err := dao.AppVersion.Page(ctx, page, size)
if err != nil {
return nil, err
}
items := make([]*dto.AdminAppVersionItem, 0, len(list))
for _, v := range list {
items = append(items, &dto.AdminAppVersionItem{
Id: v.Id,
Version: v.Version,
Notes: v.Notes,
CreatedAt: v.CreatedAt,
})
}
return &dto.AdminAppVersionListRes{Total: total, List: items}, nil
}
// AdminAddVersion 下发新版本:版本号从文件名识别(observer-x.y.z.apk),
// 先单写者串行落库(查重 + UNIQUE 兜底),再保存 APK 覆盖固定文件;
// 文件保存失败时删除记录补偿,保证「记录存在 ⟺ 文件存在」。
func (s *appVersionService) AdminAddVersion(ctx context.Context, req *dto.AdminAppVersionAddReq) (*dto.AdminAppVersionAddRes, error) {
if req.File == nil {
return nil, gerror.NewCode(common.CodeApkInvalid)
}
version, err := parseVersionFromFilename(req.File.Filename)
if err != nil {
return nil, err
}
now := gtime.Now()
err = common.Serial().Submit(ctx, func() error {
exists, err := dao.AppVersion.GetByVersion(ctx, version)
if err != nil {
return err
}
if exists != nil {
return gerror.NewCode(common.CodeVersionDuplicate)
}
return dao.AppVersion.Insert(ctx, &entity.AppVersion{
Version: version,
Notes: req.Notes,
CreatedAt: now,
UpdatedAt: now,
})
})
if err != nil {
return nil, err
}
if err := saveApk(ctx, req.File); err != nil {
if delErr := dao.AppVersion.DeleteByVersion(ctx, version); delErr != nil {
g.Log().Errorf(ctx, "APK 保存失败后补偿删除记录失败: %+v", delErr)
}
return nil, err
}
return &dto.AdminAppVersionAddRes{}, nil
}
// parseVersionFromFilename 从 APK 文件名识别版本号:observer-1.0.1.apk → 1.0.1。
// 文件名是上传版本号的唯一事实来源(打包产物即按此命名)。
func parseVersionFromFilename(name string) (string, error) {
m := apkNameRe.FindStringSubmatch(filepath.Base(name))
if m == nil {
return "", gerror.NewCode(common.CodeApkInvalid,
"APK 文件名须为 observer-x.y.z.apk 格式(如 observer-1.0.1.apk")
}
return m[1], nil
}
// AdminDeleteVersion 删除版本记录(Serial 串行,防与下发并发交错):
// 删最新版本时联动删除 APK 文件(客户端 update 返回空不再提示、下载 404);
// 删历史版本仅删记录、不动文件(固定文件永远对应最新版本)。
// 先删记录再删文件:记录删除是核心操作,文件删除失败仅记日志不阻断
// (记录没了客户端不会误提示更新,残留旧文件无害)。
func (s *appVersionService) AdminDeleteVersion(ctx context.Context, req *dto.AdminAppVersionDeleteReq) (*dto.AdminAppVersionDeleteRes, error) {
err := common.Serial().Submit(ctx, func() error {
v, err := dao.AppVersion.GetById(ctx, req.Id)
if err != nil {
return err
}
if v == nil {
return gerror.NewCode(common.CodeVersionNotFound)
}
latest, err := dao.AppVersion.Latest(ctx)
if err != nil {
return err
}
if err := dao.AppVersion.DeleteById(ctx, req.Id); err != nil {
return err
}
if latest != nil && latest.Id == v.Id {
if err := os.Remove(common.ApkFilePath(ctx)); err != nil && !os.IsNotExist(err) {
g.Log().Errorf(ctx, "删除最新版本 %s 后清理 APK 失败: %+v", v.Version, err)
}
}
return nil
})
if err != nil {
return nil, err
}
return &dto.AdminAppVersionDeleteRes{}, nil
}
// saveApk 保存 APK:先落临时文件再原子重命名覆盖固定文件名,
// 保证目录下永远只保留最新一个文件(异常中断不产生半截正式文件)。
func saveApk(ctx context.Context, f *ghttp.UploadFile) error {
dir := common.ApkDir(ctx)
if err := os.MkdirAll(dir, 0o755); err != nil {
return gerror.Wrap(err, "创建 APK 目录失败")
}
saved, err := f.Save(dir)
if err != nil {
_ = os.Remove(filepath.Join(dir, filepath.Base(f.Filename)))
return gerror.Wrap(err, "APK 保存失败")
}
if err := os.Rename(filepath.Join(dir, saved), common.ApkFilePath(ctx)); err != nil {
_ = os.Remove(filepath.Join(dir, saved))
return gerror.Wrap(err, "APK 文件更新失败")
}
return nil
}