Files
observer/server/biz/service/app_version.go
T
2026-08-25 17:17:06 +08:00

149 lines
5.0 KiB
Go

package service
import (
"context"
"os"
"path/filepath"
"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{}
var AppVersion = &appVersionService{}
// GetUpdate 客户端版本更新检查(公开接口,无需登录态;仅 Android 调用):
// 返回最新一条版本记录,无记录时返回空结构(客户端视为无需更新)。
func (s *appVersionService) GetUpdate(ctx context.Context, req *dto.AppUpdateReq) (*dto.AppUpdateRes, error) {
latest, err := dao.AppVersion.Latest(ctx)
if err != nil {
return nil, err
}
if latest == nil {
return &dto.AppUpdateRes{}, nil
}
return &dto.AppUpdateRes{
Version: latest.Version,
Notes: latest.Notes,
}, 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 下发新版本:先单写者串行落库(查重 + 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)
}
if filepath.Ext(req.File.Filename) != ".apk" {
return nil, gerror.NewCode(common.CodeApkInvalid, "仅支持 .apk 文件")
}
now := gtime.Now()
err := common.Serial().Submit(ctx, func() error {
exists, err := dao.AppVersion.GetByVersion(ctx, req.Version)
if err != nil {
return err
}
if exists != nil {
return gerror.NewCode(common.CodeVersionDuplicate)
}
return dao.AppVersion.Insert(ctx, &entity.AppVersion{
Version: req.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, req.Version); delErr != nil {
g.Log().Errorf(ctx, "APK 保存失败后补偿删除记录失败: %+v", delErr)
}
return nil, err
}
return &dto.AdminAppVersionAddRes{}, 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
}