fix: allow desktop release version history
This commit is contained in:
@@ -360,7 +360,6 @@ func (s *DesktopClientReleaseService) List(ctx context.Context, in DesktopClient
|
||||
}
|
||||
|
||||
func (s *DesktopClientReleaseService) ListDownloadable(ctx context.Context, channel string) (*DesktopClientDownloadableReleaseListResult, error) {
|
||||
enabled := true
|
||||
normalizedChannel := normalizeDesktopReleaseEnum(channel)
|
||||
if normalizedChannel == "" {
|
||||
normalizedChannel = "stable"
|
||||
@@ -369,40 +368,37 @@ func (s *DesktopClientReleaseService) ListDownloadable(ctx context.Context, chan
|
||||
return nil, response.ErrBadRequest(40062, "invalid_desktop_release_channel", "发布渠道格式无效")
|
||||
}
|
||||
|
||||
items, _, err := s.releases.List(ctx, repository.DesktopClientReleaseFilter{
|
||||
Channel: normalizedChannel,
|
||||
Enabled: &enabled,
|
||||
Limit: 200,
|
||||
})
|
||||
items, err := s.releases.ListEnabledByChannel(ctx, normalizedChannel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
views := make([]DesktopClientDownloadableRelease, 0, len(items))
|
||||
for i := range items {
|
||||
latestItems := latestDesktopClientReleasesByTarget(items)
|
||||
views := make([]DesktopClientDownloadableRelease, 0, len(latestItems))
|
||||
for i := range latestItems {
|
||||
var downloadURL *string
|
||||
if items[i].DownloadSource == DesktopClientDownloadSourceCustom {
|
||||
if value := strings.TrimSpace(stringPointerValue(items[i].CustomDownloadURL)); value != "" {
|
||||
if latestItems[i].DownloadSource == DesktopClientDownloadSourceCustom {
|
||||
if value := strings.TrimSpace(stringPointerValue(latestItems[i].CustomDownloadURL)); value != "" {
|
||||
downloadURL = &value
|
||||
}
|
||||
}
|
||||
views = append(views, DesktopClientDownloadableRelease{
|
||||
ID: items[i].ID,
|
||||
Platform: items[i].Platform,
|
||||
Arch: items[i].Arch,
|
||||
Channel: items[i].Channel,
|
||||
Version: items[i].Version,
|
||||
ID: latestItems[i].ID,
|
||||
Platform: latestItems[i].Platform,
|
||||
Arch: latestItems[i].Arch,
|
||||
Channel: latestItems[i].Channel,
|
||||
Version: latestItems[i].Version,
|
||||
DownloadURL: downloadURL,
|
||||
FileName: items[i].FileName,
|
||||
FileSizeBytes: items[i].FileSizeBytes,
|
||||
SHA256: items[i].SHA256,
|
||||
ReleaseNotes: items[i].ReleaseNotes,
|
||||
PublishedAt: items[i].PublishedAt,
|
||||
UpdatedAt: items[i].UpdatedAt,
|
||||
ForceUpdate: items[i].ForceUpdate,
|
||||
MinSupportedVersion: items[i].MinSupportedVersion,
|
||||
DownloadSource: items[i].DownloadSource,
|
||||
OSSObjectKey: items[i].OSSObjectKey,
|
||||
FileName: latestItems[i].FileName,
|
||||
FileSizeBytes: latestItems[i].FileSizeBytes,
|
||||
SHA256: latestItems[i].SHA256,
|
||||
ReleaseNotes: latestItems[i].ReleaseNotes,
|
||||
PublishedAt: latestItems[i].PublishedAt,
|
||||
UpdatedAt: latestItems[i].UpdatedAt,
|
||||
ForceUpdate: latestItems[i].ForceUpdate,
|
||||
MinSupportedVersion: latestItems[i].MinSupportedVersion,
|
||||
DownloadSource: latestItems[i].DownloadSource,
|
||||
OSSObjectKey: latestItems[i].OSSObjectKey,
|
||||
})
|
||||
}
|
||||
return &DesktopClientDownloadableReleaseListResult{Items: views}, nil
|
||||
@@ -417,7 +413,7 @@ func (s *DesktopClientReleaseService) Create(ctx context.Context, actor *Actor,
|
||||
item, err := s.releases.Create(ctx, normalized.toRepositoryInput(actorID(actor)))
|
||||
if err != nil {
|
||||
if isDuplicateDesktopClientRelease(err) {
|
||||
return nil, response.ErrConflict(40930, "duplicate_desktop_client_release", "该平台、架构和渠道的客户端版本配置已存在")
|
||||
return nil, response.ErrConflict(40930, "duplicate_desktop_client_release", "该平台、架构、渠道和版本的客户端版本配置已存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -438,7 +434,7 @@ func (s *DesktopClientReleaseService) Update(ctx context.Context, actor *Actor,
|
||||
return nil, response.ErrNotFound(40430, "desktop_client_release_not_found", "客户端版本配置不存在")
|
||||
}
|
||||
if isDuplicateDesktopClientRelease(err) {
|
||||
return nil, response.ErrConflict(40930, "duplicate_desktop_client_release", "该平台、架构和渠道的客户端版本配置已存在")
|
||||
return nil, response.ErrConflict(40930, "duplicate_desktop_client_release", "该平台、架构、渠道和版本的客户端版本配置已存在")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -767,7 +763,7 @@ func (s *DesktopClientReleaseService) CheckLatest(ctx context.Context, in Deskto
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item, err := s.releases.FindEnabled(ctx, target)
|
||||
item, err := s.findLatestEnabledRelease(ctx, target)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrDesktopClientReleaseNotFound) {
|
||||
return nil, response.ErrNotFound(40431, "desktop_client_release_not_configured", "未配置该平台客户端版本")
|
||||
@@ -847,7 +843,7 @@ func (s *DesktopClientReleaseService) ResolveLatestDownloadURL(ctx context.Conte
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item, err := s.releases.FindEnabled(ctx, target)
|
||||
item, err := s.findLatestEnabledRelease(ctx, target)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrDesktopClientReleaseNotFound) {
|
||||
return nil, response.ErrNotFound(40431, "desktop_client_release_not_configured", "未配置该平台客户端版本")
|
||||
@@ -862,7 +858,7 @@ func (s *DesktopClientReleaseService) ResolveLatestUpdaterFeed(ctx context.Conte
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item, err := s.releases.FindEnabled(ctx, target)
|
||||
item, err := s.findLatestEnabledRelease(ctx, target)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrDesktopClientReleaseNotFound) {
|
||||
return nil, response.ErrNotFound(40431, "desktop_client_release_not_configured", "未配置该平台客户端版本")
|
||||
@@ -890,6 +886,79 @@ func (s *DesktopClientReleaseService) ResolveLatestUpdaterFeed(ctx context.Conte
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DesktopClientReleaseService) findLatestEnabledRelease(ctx context.Context, target repository.DesktopClientReleaseTarget) (*domain.DesktopClientRelease, error) {
|
||||
items, err := s.releases.ListEnabledByTarget(ctx, target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, repository.ErrDesktopClientReleaseNotFound
|
||||
}
|
||||
return latestDesktopClientRelease(items), nil
|
||||
}
|
||||
|
||||
func latestDesktopClientReleasesByTarget(items []domain.DesktopClientRelease) []domain.DesktopClientRelease {
|
||||
byTarget := map[string]domain.DesktopClientRelease{}
|
||||
for _, item := range items {
|
||||
key := item.Platform + "\x00" + item.Arch + "\x00" + item.Channel
|
||||
current, ok := byTarget[key]
|
||||
if !ok || isDesktopClientReleaseNewer(item, current) {
|
||||
byTarget[key] = item
|
||||
}
|
||||
}
|
||||
|
||||
latest := make([]domain.DesktopClientRelease, 0, len(byTarget))
|
||||
for _, item := range byTarget {
|
||||
latest = append(latest, item)
|
||||
}
|
||||
sort.SliceStable(latest, func(i, j int) bool {
|
||||
if latest[i].Platform != latest[j].Platform {
|
||||
return latest[i].Platform < latest[j].Platform
|
||||
}
|
||||
if latest[i].Arch != latest[j].Arch {
|
||||
return latest[i].Arch < latest[j].Arch
|
||||
}
|
||||
if latest[i].Channel != latest[j].Channel {
|
||||
return latest[i].Channel < latest[j].Channel
|
||||
}
|
||||
return latest[i].ID > latest[j].ID
|
||||
})
|
||||
return latest
|
||||
}
|
||||
|
||||
func latestDesktopClientRelease(items []domain.DesktopClientRelease) *domain.DesktopClientRelease {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
latest := items[0]
|
||||
for _, item := range items[1:] {
|
||||
if isDesktopClientReleaseNewer(item, latest) {
|
||||
latest = item
|
||||
}
|
||||
}
|
||||
return &latest
|
||||
}
|
||||
|
||||
func isDesktopClientReleaseNewer(candidate, current domain.DesktopClientRelease) bool {
|
||||
if archPriority(candidate.Arch) != archPriority(current.Arch) {
|
||||
return archPriority(candidate.Arch) < archPriority(current.Arch)
|
||||
}
|
||||
if compared := compareVersionStrings(candidate.Version, current.Version); compared != 0 {
|
||||
return compared > 0
|
||||
}
|
||||
if !candidate.UpdatedAt.Equal(current.UpdatedAt) {
|
||||
return candidate.UpdatedAt.After(current.UpdatedAt)
|
||||
}
|
||||
return candidate.ID > current.ID
|
||||
}
|
||||
|
||||
func archPriority(arch string) int {
|
||||
if arch == "universal" {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func itemInstallerAsset(item *domain.DesktopClientRelease) desktopClientReleaseAsset {
|
||||
if item == nil {
|
||||
return desktopClientReleaseAsset{}
|
||||
|
||||
@@ -97,6 +97,50 @@ func TestDesktopClientReleaseVersionPolicy(t *testing.T) {
|
||||
require.False(t, minSupportedVersionRequiresUpdate(&minVersion, "1.2.0"))
|
||||
}
|
||||
|
||||
func TestLatestDesktopClientReleasePrefersVersionThenArch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
oldX64 := domain.DesktopClientRelease{
|
||||
ID: 1,
|
||||
Platform: "win32",
|
||||
Arch: "x64",
|
||||
Channel: "stable",
|
||||
Version: "0.1.0",
|
||||
UpdatedAt: time.Date(2026, 6, 10, 10, 0, 0, 0, time.UTC),
|
||||
}
|
||||
newX64 := domain.DesktopClientRelease{
|
||||
ID: 2,
|
||||
Platform: "win32",
|
||||
Arch: "x64",
|
||||
Channel: "stable",
|
||||
Version: "0.1.1",
|
||||
UpdatedAt: time.Date(2026, 6, 10, 9, 0, 0, 0, time.UTC),
|
||||
}
|
||||
newUniversal := domain.DesktopClientRelease{
|
||||
ID: 3,
|
||||
Platform: "win32",
|
||||
Arch: "universal",
|
||||
Channel: "stable",
|
||||
Version: "0.2.0",
|
||||
UpdatedAt: time.Date(2026, 6, 10, 11, 0, 0, 0, time.UTC),
|
||||
}
|
||||
|
||||
require.Equal(t, newX64.ID, latestDesktopClientRelease([]domain.DesktopClientRelease{
|
||||
newUniversal,
|
||||
oldX64,
|
||||
newX64,
|
||||
}).ID)
|
||||
|
||||
latestByTarget := latestDesktopClientReleasesByTarget([]domain.DesktopClientRelease{
|
||||
oldX64,
|
||||
newX64,
|
||||
newUniversal,
|
||||
})
|
||||
require.Len(t, latestByTarget, 2)
|
||||
require.Equal(t, newUniversal.ID, latestByTarget[0].ID)
|
||||
require.Equal(t, newX64.ID, latestByTarget[1].ID)
|
||||
}
|
||||
|
||||
func TestNormalizeDesktopClientReleaseUploadInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -199,6 +199,56 @@ func (r *DesktopClientReleaseRepository) FindEnabled(ctx context.Context, target
|
||||
target.Platform, target.Channel, target.Arch))
|
||||
}
|
||||
|
||||
func (r *DesktopClientReleaseRepository) ListEnabledByTarget(ctx context.Context, target DesktopClientReleaseTarget) ([]domain.DesktopClientRelease, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT `+desktopClientReleaseColumns+`
|
||||
FROM ops.desktop_client_releases
|
||||
WHERE platform = $1
|
||||
AND channel = $2
|
||||
AND enabled = TRUE
|
||||
AND (arch = $3 OR arch = 'universal')
|
||||
ORDER BY CASE WHEN arch = $3 THEN 0 ELSE 1 END, updated_at DESC, id DESC`,
|
||||
target.Platform, target.Channel, target.Arch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []domain.DesktopClientRelease{}
|
||||
for rows.Next() {
|
||||
item, scanErr := scanDesktopClientRelease(rows)
|
||||
if scanErr != nil {
|
||||
return nil, scanErr
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (r *DesktopClientReleaseRepository) ListEnabledByChannel(ctx context.Context, channel string) ([]domain.DesktopClientRelease, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT `+desktopClientReleaseColumns+`
|
||||
FROM ops.desktop_client_releases
|
||||
WHERE channel = $1
|
||||
AND enabled = TRUE
|
||||
ORDER BY channel ASC, platform ASC, arch ASC, updated_at DESC, id DESC`,
|
||||
channel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := []domain.DesktopClientRelease{}
|
||||
for rows.Next() {
|
||||
item, scanErr := scanDesktopClientRelease(rows)
|
||||
if scanErr != nil {
|
||||
return nil, scanErr
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (r *DesktopClientReleaseRepository) Create(ctx context.Context, in UpsertDesktopClientReleaseInput) (*domain.DesktopClientRelease, error) {
|
||||
return scanDesktopClientRelease(r.pool.QueryRow(ctx, `
|
||||
INSERT INTO ops.desktop_client_releases (
|
||||
|
||||
Reference in New Issue
Block a user