Files
verify/backend/internal/router/app/info.go
T
admin 7af4da30d3 feat: return latest version when no version param provided
If client doesn't provide version parameter, return latest version info
instead of returning error

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 13:11:13 +08:00

296 lines
8.5 KiB
Go

package app
import (
"strings"
"verification-platform-backend/internal/database"
"verification-platform-backend/internal/model"
"verification-platform-backend/internal/service"
"verification-platform-backend/pkg/response"
"github.com/gin-gonic/gin"
)
func SetupInfoRoutes(r *gin.RouterGroup) {
r.GET("/info", handleAppGetInfo)
r.GET("/check-update", handleAppCheckUpdate)
r.GET("/announcements", handleAppGetAnnouncements)
}
func handleAppGetInfo(c *gin.Context) {
appKey := c.Param("appKey")
var app model.Application
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
response.Error(c, 404, "应用不存在")
return
}
if service.GetApplicationDisabledStatus(app.ID) {
response.Error(c, 403, "该应用已被禁用")
return
}
response.Success(c, gin.H{
"id": app.ID,
"name": app.Name,
"description": app.Description,
"icon_url": app.IconURL,
"status": app.Status,
"billing_type": app.BillingType,
"login_policy": app.LoginPolicy,
"max_devices": app.MaxDevices,
"multi_open_mode": app.MultiOpenMode,
"max_instances": app.MaxInstances,
"enable_trial": app.EnableTrial,
"trial_balance": app.TrialBalance,
"heartbeat_interval": app.HeartbeatInterval,
"heartbeat_timeout": app.HeartbeatTimeout,
})
}
func handleAppCheckUpdate(c *gin.Context) {
appKey := c.Param("appKey")
clientVersion := c.Query("version")
var app model.Application
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
response.Error(c, 404, "应用不存在")
return
}
if service.GetApplicationDisabledStatus(app.ID) {
response.Error(c, 403, "该应用已被禁用")
return
}
// 查询该应用的所有版本,按ID降序(创建顺序)
var allVersions []model.Version
if err := database.DB.Where("application_id = ? AND status = ?", app.ID, "active").
Order("id DESC").
Preload("Files").
Find(&allVersions).Error; err != nil {
response.Error(c, 500, "获取版本列表失败")
return
}
// 没有任何版本
if len(allVersions) == 0 {
response.Success(c, gin.H{
"has_update": false,
"current_version": clientVersion,
"latest_version": "",
"download_url": "",
"update_notes": "",
"update_strategy": "",
"update_method": "",
"files": []interface{}{},
})
return
}
// 清理版本号:去除空格和前缀 v/V
cleanVersion := func(v string) string {
v = strings.TrimSpace(v)
v = strings.TrimPrefix(v, "v")
v = strings.TrimPrefix(v, "V")
return v
}
// 如果没有传入版本号,直接返回最新版本
if clientVersion == "" {
latestVersion := &allVersions[0]
// 构建文件列表
files := make([]map[string]interface{}, len(latestVersion.Files))
for i, f := range latestVersion.Files {
files[i] = map[string]interface{}{
"file_path": f.FilePath,
"file_name": f.FileName,
"file_size": f.FileSize,
"file_hash": f.FileHash,
"file_type": f.FileType,
"is_required": f.IsRequired,
}
}
// 确定下载地址:优先使用自定义地址
downloadURL := latestVersion.FilePath
if latestVersion.CustomDownloadURL != "" {
downloadURL = latestVersion.CustomDownloadURL
}
response.Success(c, gin.H{
"has_update": true,
"latest_version": latestVersion.Version,
"download_url": downloadURL,
"file_size": latestVersion.FileSize,
"file_hash": latestVersion.FileHash,
"entry_file": latestVersion.EntryFile,
"update_notes": latestVersion.Description,
"update_strategy": latestVersion.UpdateStrategy,
"update_type": latestVersion.UpdateType,
"update_method": latestVersion.UpdateMethod,
"changelog": latestVersion.Changelog,
"files": files,
})
return
}
clientVersionClean := cleanVersion(clientVersion)
// 找到客户端当前版本对应的版本记录
var clientVersionRecord *model.Version
for i := range allVersions {
if cleanVersion(allVersions[i].Version) == clientVersionClean {
clientVersionRecord = &allVersions[i]
break
}
}
// 客户端版本不存在于服务器版本列表中,返回错误
if clientVersionRecord == nil {
response.Error(c, 404, "客户端版本不存在")
return
}
// 判断客户端版本状态:检查是否有更高ID的强制更新版本
isSuperseded := false
for i := range allVersions {
v := &allVersions[i]
// 存在ID更高(创建时间更晚)的强制更新版本
if v.ID > clientVersionRecord.ID && v.UpdateStrategy == "forced" {
isSuperseded = true
break
}
}
// 如果被取代(有强制更新),必须更新
// 找到最新的强制更新版本
var latestVersion *model.Version
var updateStrategy string
if isSuperseded {
// 找到ID最高的强制更新版本
for i := range allVersions {
v := &allVersions[i]
if v.ID > clientVersionRecord.ID && v.UpdateStrategy == "forced" {
if latestVersion == nil || v.ID > latestVersion.ID {
latestVersion = v
}
}
}
updateStrategy = "forced"
} else {
// 没有强制更新,检查是否有更高ID的版本(普通更新)
// 最新版本就是ID最高的版本(已经是按ID降序排列,第一个就是最新)
latestVersion = &allVersions[0]
// 只有当最新版本ID大于客户端版本ID时才需要更新
if latestVersion.ID > clientVersionRecord.ID {
updateStrategy = latestVersion.UpdateStrategy // optional 或 forced
} else {
// 已经是最新版本
response.Success(c, gin.H{
"has_update": false,
"current_version": clientVersion,
"latest_version": clientVersionRecord.Version,
"download_url": "",
"update_notes": "",
"update_strategy": "",
"update_method": "",
"files": []interface{}{},
})
return
}
}
// 构建文件列表
files := make([]map[string]interface{}, len(latestVersion.Files))
for i, f := range latestVersion.Files {
files[i] = map[string]interface{}{
"file_path": f.FilePath,
"file_name": f.FileName,
"file_size": f.FileSize,
"file_hash": f.FileHash,
"file_type": f.FileType,
"is_required": f.IsRequired,
}
}
// 确定下载地址:优先使用自定义地址
downloadURL := latestVersion.FilePath
if latestVersion.CustomDownloadURL != "" {
downloadURL = latestVersion.CustomDownloadURL
}
// 构建返回结果
result := map[string]interface{}{
"has_update": true,
"current_version": clientVersion,
"latest_version": latestVersion.Version,
"download_url": downloadURL,
"file_size": latestVersion.FileSize,
"file_hash": latestVersion.FileHash,
"entry_file": latestVersion.EntryFile,
"update_notes": latestVersion.Description,
"update_strategy": updateStrategy,
"update_type": latestVersion.UpdateType,
"update_method": latestVersion.UpdateMethod,
"changelog": latestVersion.Changelog,
"files": files,
}
// 检查是否有适合客户端版本的增量更新
// 查找以客户端版本为基础版本的 patch 更新
var patchVersion model.Version
err := database.DB.Where("application_id = ? AND update_type = ? AND base_version_id = ? AND status = ?",
app.ID, "patch", clientVersionRecord.ID, "active").
Order("id DESC").
Preload("Files").
First(&patchVersion).Error
if err == nil {
// 找到了增量更新
result["is_patch"] = true
result["base_version"] = clientVersion
result["patch_url"] = patchVersion.FilePath
result["patch_size"] = patchVersion.FileSize
result["patch_hash"] = patchVersion.FileHash
}
response.Success(c, result)
}
func handleAppGetAnnouncements(c *gin.Context) {
appKey := c.Param("appKey")
var app model.Application
if err := database.DB.Where("app_key = ?", appKey).First(&app).Error; err != nil {
response.Error(c, 404, "应用不存在")
return
}
if service.GetApplicationDisabledStatus(app.ID) {
response.Error(c, 403, "该应用已被禁用")
return
}
var announcements []model.Announcement
if err := database.DB.Where("application_id = ? AND status = ?", app.ID, "active").Order("is_top DESC, created_at DESC").Find(&announcements).Error; err != nil {
response.Error(c, 500, "获取公告失败")
return
}
result := make([]gin.H, len(announcements))
for i, a := range announcements {
result[i] = gin.H{
"id": a.ID,
"title": a.Title,
"content": a.Content,
"type": a.Type,
"is_top": a.IsTop,
"created_at": a.CreatedAt,
}
}
response.Success(c, result)
}