fix: allow desktop release version history
This commit is contained in:
@@ -175,7 +175,7 @@
|
|||||||
<a-form-item label="渠道" required>
|
<a-form-item label="渠道" required>
|
||||||
<a-input v-model:value="form.channel" placeholder="stable" />
|
<a-input v-model:value="form.channel" placeholder="stable" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="最新版本" required>
|
<a-form-item label="版本号" required>
|
||||||
<a-input v-model:value="form.version" placeholder="0.1.0" />
|
<a-input v-model:value="form.version" placeholder="0.1.0" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -360,7 +360,6 @@ func (s *DesktopClientReleaseService) List(ctx context.Context, in DesktopClient
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *DesktopClientReleaseService) ListDownloadable(ctx context.Context, channel string) (*DesktopClientDownloadableReleaseListResult, error) {
|
func (s *DesktopClientReleaseService) ListDownloadable(ctx context.Context, channel string) (*DesktopClientDownloadableReleaseListResult, error) {
|
||||||
enabled := true
|
|
||||||
normalizedChannel := normalizeDesktopReleaseEnum(channel)
|
normalizedChannel := normalizeDesktopReleaseEnum(channel)
|
||||||
if normalizedChannel == "" {
|
if normalizedChannel == "" {
|
||||||
normalizedChannel = "stable"
|
normalizedChannel = "stable"
|
||||||
@@ -369,40 +368,37 @@ func (s *DesktopClientReleaseService) ListDownloadable(ctx context.Context, chan
|
|||||||
return nil, response.ErrBadRequest(40062, "invalid_desktop_release_channel", "发布渠道格式无效")
|
return nil, response.ErrBadRequest(40062, "invalid_desktop_release_channel", "发布渠道格式无效")
|
||||||
}
|
}
|
||||||
|
|
||||||
items, _, err := s.releases.List(ctx, repository.DesktopClientReleaseFilter{
|
items, err := s.releases.ListEnabledByChannel(ctx, normalizedChannel)
|
||||||
Channel: normalizedChannel,
|
|
||||||
Enabled: &enabled,
|
|
||||||
Limit: 200,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
views := make([]DesktopClientDownloadableRelease, 0, len(items))
|
latestItems := latestDesktopClientReleasesByTarget(items)
|
||||||
for i := range items {
|
views := make([]DesktopClientDownloadableRelease, 0, len(latestItems))
|
||||||
|
for i := range latestItems {
|
||||||
var downloadURL *string
|
var downloadURL *string
|
||||||
if items[i].DownloadSource == DesktopClientDownloadSourceCustom {
|
if latestItems[i].DownloadSource == DesktopClientDownloadSourceCustom {
|
||||||
if value := strings.TrimSpace(stringPointerValue(items[i].CustomDownloadURL)); value != "" {
|
if value := strings.TrimSpace(stringPointerValue(latestItems[i].CustomDownloadURL)); value != "" {
|
||||||
downloadURL = &value
|
downloadURL = &value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
views = append(views, DesktopClientDownloadableRelease{
|
views = append(views, DesktopClientDownloadableRelease{
|
||||||
ID: items[i].ID,
|
ID: latestItems[i].ID,
|
||||||
Platform: items[i].Platform,
|
Platform: latestItems[i].Platform,
|
||||||
Arch: items[i].Arch,
|
Arch: latestItems[i].Arch,
|
||||||
Channel: items[i].Channel,
|
Channel: latestItems[i].Channel,
|
||||||
Version: items[i].Version,
|
Version: latestItems[i].Version,
|
||||||
DownloadURL: downloadURL,
|
DownloadURL: downloadURL,
|
||||||
FileName: items[i].FileName,
|
FileName: latestItems[i].FileName,
|
||||||
FileSizeBytes: items[i].FileSizeBytes,
|
FileSizeBytes: latestItems[i].FileSizeBytes,
|
||||||
SHA256: items[i].SHA256,
|
SHA256: latestItems[i].SHA256,
|
||||||
ReleaseNotes: items[i].ReleaseNotes,
|
ReleaseNotes: latestItems[i].ReleaseNotes,
|
||||||
PublishedAt: items[i].PublishedAt,
|
PublishedAt: latestItems[i].PublishedAt,
|
||||||
UpdatedAt: items[i].UpdatedAt,
|
UpdatedAt: latestItems[i].UpdatedAt,
|
||||||
ForceUpdate: items[i].ForceUpdate,
|
ForceUpdate: latestItems[i].ForceUpdate,
|
||||||
MinSupportedVersion: items[i].MinSupportedVersion,
|
MinSupportedVersion: latestItems[i].MinSupportedVersion,
|
||||||
DownloadSource: items[i].DownloadSource,
|
DownloadSource: latestItems[i].DownloadSource,
|
||||||
OSSObjectKey: items[i].OSSObjectKey,
|
OSSObjectKey: latestItems[i].OSSObjectKey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return &DesktopClientDownloadableReleaseListResult{Items: views}, nil
|
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)))
|
item, err := s.releases.Create(ctx, normalized.toRepositoryInput(actorID(actor)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isDuplicateDesktopClientRelease(err) {
|
if isDuplicateDesktopClientRelease(err) {
|
||||||
return nil, response.ErrConflict(40930, "duplicate_desktop_client_release", "该平台、架构和渠道的客户端版本配置已存在")
|
return nil, response.ErrConflict(40930, "duplicate_desktop_client_release", "该平台、架构、渠道和版本的客户端版本配置已存在")
|
||||||
}
|
}
|
||||||
return nil, err
|
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", "客户端版本配置不存在")
|
return nil, response.ErrNotFound(40430, "desktop_client_release_not_found", "客户端版本配置不存在")
|
||||||
}
|
}
|
||||||
if isDuplicateDesktopClientRelease(err) {
|
if isDuplicateDesktopClientRelease(err) {
|
||||||
return nil, response.ErrConflict(40930, "duplicate_desktop_client_release", "该平台、架构和渠道的客户端版本配置已存在")
|
return nil, response.ErrConflict(40930, "duplicate_desktop_client_release", "该平台、架构、渠道和版本的客户端版本配置已存在")
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -767,7 +763,7 @@ func (s *DesktopClientReleaseService) CheckLatest(ctx context.Context, in Deskto
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
item, err := s.releases.FindEnabled(ctx, target)
|
item, err := s.findLatestEnabledRelease(ctx, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrDesktopClientReleaseNotFound) {
|
if errors.Is(err, repository.ErrDesktopClientReleaseNotFound) {
|
||||||
return nil, response.ErrNotFound(40431, "desktop_client_release_not_configured", "未配置该平台客户端版本")
|
return nil, response.ErrNotFound(40431, "desktop_client_release_not_configured", "未配置该平台客户端版本")
|
||||||
@@ -847,7 +843,7 @@ func (s *DesktopClientReleaseService) ResolveLatestDownloadURL(ctx context.Conte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
item, err := s.releases.FindEnabled(ctx, target)
|
item, err := s.findLatestEnabledRelease(ctx, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrDesktopClientReleaseNotFound) {
|
if errors.Is(err, repository.ErrDesktopClientReleaseNotFound) {
|
||||||
return nil, response.ErrNotFound(40431, "desktop_client_release_not_configured", "未配置该平台客户端版本")
|
return nil, response.ErrNotFound(40431, "desktop_client_release_not_configured", "未配置该平台客户端版本")
|
||||||
@@ -862,7 +858,7 @@ func (s *DesktopClientReleaseService) ResolveLatestUpdaterFeed(ctx context.Conte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
item, err := s.releases.FindEnabled(ctx, target)
|
item, err := s.findLatestEnabledRelease(ctx, target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, repository.ErrDesktopClientReleaseNotFound) {
|
if errors.Is(err, repository.ErrDesktopClientReleaseNotFound) {
|
||||||
return nil, response.ErrNotFound(40431, "desktop_client_release_not_configured", "未配置该平台客户端版本")
|
return nil, response.ErrNotFound(40431, "desktop_client_release_not_configured", "未配置该平台客户端版本")
|
||||||
@@ -890,6 +886,79 @@ func (s *DesktopClientReleaseService) ResolveLatestUpdaterFeed(ctx context.Conte
|
|||||||
}, nil
|
}, 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 {
|
func itemInstallerAsset(item *domain.DesktopClientRelease) desktopClientReleaseAsset {
|
||||||
if item == nil {
|
if item == nil {
|
||||||
return desktopClientReleaseAsset{}
|
return desktopClientReleaseAsset{}
|
||||||
|
|||||||
@@ -97,6 +97,50 @@ func TestDesktopClientReleaseVersionPolicy(t *testing.T) {
|
|||||||
require.False(t, minSupportedVersionRequiresUpdate(&minVersion, "1.2.0"))
|
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) {
|
func TestNormalizeDesktopClientReleaseUploadInput(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -199,6 +199,56 @@ func (r *DesktopClientReleaseRepository) FindEnabled(ctx context.Context, target
|
|||||||
target.Platform, target.Channel, target.Arch))
|
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) {
|
func (r *DesktopClientReleaseRepository) Create(ctx context.Context, in UpsertDesktopClientReleaseInput) (*domain.DesktopClientRelease, error) {
|
||||||
return scanDesktopClientRelease(r.pool.QueryRow(ctx, `
|
return scanDesktopClientRelease(r.pool.QueryRow(ctx, `
|
||||||
INSERT INTO ops.desktop_client_releases (
|
INSERT INTO ops.desktop_client_releases (
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
DROP INDEX IF EXISTS ops.uk_desktop_client_releases_target_version;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_desktop_client_releases_target
|
||||||
|
ON ops.desktop_client_releases(platform, arch, channel);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
DROP INDEX IF EXISTS ops.uk_desktop_client_releases_target;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uk_desktop_client_releases_target_version
|
||||||
|
ON ops.desktop_client_releases(platform, arch, channel, version);
|
||||||
Reference in New Issue
Block a user