feat: add custom download URL for manual update method

- Add custom_download_url field to Version model
- Show custom download URL input when update method is 'manual'
- Use platform default URL if custom URL is not set
- Add i18n translations for new field

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 16:06:48 +08:00
parent 677546d139
commit 53fd9ff7dc
6 changed files with 100 additions and 47 deletions
+2 -1
View File
@@ -229,7 +229,8 @@ type Version struct {
EntryFile string `gorm:"size:255" json:"entry_file"` EntryFile string `gorm:"size:255" json:"entry_file"`
UpdateStrategy string `gorm:"size:20;default:optional" json:"update_strategy"` UpdateStrategy string `gorm:"size:20;default:optional" json:"update_strategy"`
UpdateType string `gorm:"size:20;default:full" json:"update_type"` UpdateType string `gorm:"size:20;default:full" json:"update_type"`
UpdateMethod string `gorm:"size:20;default:auto" json:"update_method"` UpdateMethod string `gorm:"size:20;default:auto" json:"update_method"`
CustomDownloadURL string `gorm:"size:500" json:"custom_download_url"`
Description string `gorm:"type:text" json:"description"` Description string `gorm:"type:text" json:"description"`
Changelog string `gorm:"type:text" json:"changelog"` Changelog string `gorm:"type:text" json:"changelog"`
Status string `gorm:"size:20;default:active" json:"status"` Status string `gorm:"size:20;default:active" json:"status"`
+51 -46
View File
@@ -226,38 +226,40 @@ func handleGetVersionByID(c *gin.Context) {
} }
response.Success(c, gin.H{ response.Success(c, gin.H{
"id": version.ID, "id": version.ID,
"application_id": version.ApplicationID, "application_id": version.ApplicationID,
"application_name": appNameMap[version.ApplicationID], "application_name": appNameMap[version.ApplicationID],
"version": version.Version, "version": version.Version,
"description": version.Description, "description": version.Description,
"file_path": version.FilePath, "file_path": version.FilePath,
"file_size": version.FileSize, "file_size": version.FileSize,
"file_hash": version.FileHash, "file_hash": version.FileHash,
"update_strategy": version.UpdateStrategy, "update_strategy": version.UpdateStrategy,
"update_type": version.UpdateType, "update_type": version.UpdateType,
"update_method": version.UpdateMethod, "update_method": version.UpdateMethod,
"changelog": version.Changelog, "custom_download_url": version.CustomDownloadURL,
"status": computedStatus, "changelog": version.Changelog,
"base_version_id": version.BaseVersionID, "status": computedStatus,
"files": version.Files, "base_version_id": version.BaseVersionID,
"created_at": version.CreatedAt, "files": version.Files,
"updated_at": version.UpdatedAt, "created_at": version.CreatedAt,
"updated_at": version.UpdatedAt,
}) })
} }
type CreateVersionRequest struct { type CreateVersionRequest struct {
ApplicationID uint `json:"application_id"` ApplicationID uint `json:"application_id"`
Version string `json:"version"` Version string `json:"version"`
FilePath string `json:"file_path"` FilePath string `json:"file_path"`
FileSize int64 `json:"file_size"` FileSize int64 `json:"file_size"`
FileHash string `json:"file_hash"` FileHash string `json:"file_hash"`
EntryFile string `json:"entry_file"` EntryFile string `json:"entry_file"`
UpdateStrategy string `json:"update_strategy"` UpdateStrategy string `json:"update_strategy"`
UpdateType string `json:"update_type"` UpdateType string `json:"update_type"`
UpdateMethod string `json:"update_method"` UpdateMethod string `json:"update_method"`
Description string `json:"description"` CustomDownloadURL string `json:"custom_download_url"`
Changelog string `json:"changelog"` Description string `json:"description"`
Changelog string `json:"changelog"`
} }
func handleCreateVersionGlobal(c *gin.Context) { func handleCreateVersionGlobal(c *gin.Context) {
@@ -287,18 +289,19 @@ func handleCreateVersionGlobal(c *gin.Context) {
} }
version := model.Version{ version := model.Version{
ApplicationID: req.ApplicationID, ApplicationID: req.ApplicationID,
Version: req.Version, Version: req.Version,
FilePath: req.FilePath, FilePath: req.FilePath,
FileSize: req.FileSize, FileSize: req.FileSize,
FileHash: req.FileHash, FileHash: req.FileHash,
EntryFile: req.EntryFile, EntryFile: req.EntryFile,
UpdateStrategy: req.UpdateStrategy, UpdateStrategy: req.UpdateStrategy,
UpdateType: req.UpdateType, UpdateType: req.UpdateType,
UpdateMethod: req.UpdateMethod, UpdateMethod: req.UpdateMethod,
Description: req.Description, CustomDownloadURL: req.CustomDownloadURL,
Changelog: req.Changelog, Description: req.Description,
Status: "active", Changelog: req.Changelog,
Status: "active",
} }
if version.UpdateStrategy == "" { if version.UpdateStrategy == "" {
@@ -341,12 +344,13 @@ func handleCreateVersionGlobal(c *gin.Context) {
} }
type UpdateVersionRequest struct { type UpdateVersionRequest struct {
Version string `json:"version"` Version string `json:"version"`
UpdateStrategy string `json:"update_strategy"` UpdateStrategy string `json:"update_strategy"`
UpdateType string `json:"update_type"` UpdateType string `json:"update_type"`
UpdateMethod string `json:"update_method"` UpdateMethod string `json:"update_method"`
Description string `json:"description"` CustomDownloadURL string `json:"custom_download_url"`
Changelog string `json:"changelog"` Description string `json:"description"`
Changelog string `json:"changelog"`
} }
func handleUpdateVersionGlobal(c *gin.Context) { func handleUpdateVersionGlobal(c *gin.Context) {
@@ -403,6 +407,7 @@ func handleUpdateVersionGlobal(c *gin.Context) {
if req.UpdateMethod != "" { if req.UpdateMethod != "" {
updates["update_method"] = req.UpdateMethod updates["update_method"] = req.UpdateMethod
} }
updates["custom_download_url"] = req.CustomDownloadURL
if req.Description != "" { if req.Description != "" {
updates["description"] = req.Description updates["description"] = req.Description
} }
@@ -46,6 +46,7 @@ const formData = ref({
update_strategy: 'optional' as 'optional' | 'forced', update_strategy: 'optional' as 'optional' | 'forced',
update_type: 'full' as 'full' | 'patch', update_type: 'full' as 'full' | 'patch',
update_method: 'auto' as 'auto' | 'manual', update_method: 'auto' as 'auto' | 'manual',
custom_download_url: '',
changelog: '', changelog: '',
entry_file: '', entry_file: '',
file: null as File | null, file: null as File | null,
@@ -97,6 +98,7 @@ async function fetchVersion() {
formData.value.update_strategy = ver.update_strategy || 'optional' formData.value.update_strategy = ver.update_strategy || 'optional'
formData.value.update_type = ver.update_type || 'full' formData.value.update_type = ver.update_type || 'full'
formData.value.update_method = ver.update_method || 'auto' formData.value.update_method = ver.update_method || 'auto'
formData.value.custom_download_url = ver.custom_download_url || ''
formData.value.changelog = ver.changelog || '' formData.value.changelog = ver.changelog || ''
formData.value.entry_file = ver.entry_file || '' formData.value.entry_file = ver.entry_file || ''
} }
@@ -184,6 +186,7 @@ async function handleSubmit() {
update_strategy: formData.value.update_strategy, update_strategy: formData.value.update_strategy,
update_type: formData.value.update_type, update_type: formData.value.update_type,
update_method: formData.value.update_method, update_method: formData.value.update_method,
custom_download_url: formData.value.custom_download_url,
changelog: formData.value.changelog, changelog: formData.value.changelog,
entry_file: formData.value.entry_file, entry_file: formData.value.entry_file,
} }
@@ -460,6 +463,18 @@ onMounted(() => {
</UiSelect> </UiSelect>
</div> </div>
<div v-if="formData.update_method === 'manual'" class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.customDownloadUrl') }}</UiLabel>
<p class="text-sm text-muted-foreground">
{{ t('admin.versions.createForm.customDownloadUrlDesc') }}
</p>
<UiInput
v-model="formData.custom_download_url"
:placeholder="t('admin.versions.createForm.customDownloadUrlPlaceholder')"
:disabled="saving"
/>
</div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="changelog"> <UiLabel for="changelog">
{{ t('admin.versions.createForm.changelog') }} {{ t('admin.versions.createForm.changelog') }}
@@ -516,6 +531,10 @@ onMounted(() => {
: t('admin.versions.methods.manual') : t('admin.versions.methods.manual')
}}</span> }}</span>
</div> </div>
<div v-if="formData.update_method === 'manual'" class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewCustomDownloadUrl') }}</span>
<span class="truncate max-w-[120px]">{{ formData.custom_download_url || t('admin.versions.createForm.useDefault') }}</span>
</div>
</div> </div>
<div class="border-t pt-4 mt-4"> <div class="border-t pt-4 mt-4">
@@ -45,6 +45,7 @@ const formData = ref({
update_strategy: 'optional' as 'optional' | 'forced', update_strategy: 'optional' as 'optional' | 'forced',
update_type: 'full' as 'full' | 'patch', update_type: 'full' as 'full' | 'patch',
update_method: 'auto' as 'auto' | 'manual', update_method: 'auto' as 'auto' | 'manual',
custom_download_url: '',
changelog: '', changelog: '',
entry_file: '', entry_file: '',
file: null as File | null, file: null as File | null,
@@ -188,6 +189,7 @@ async function handleSubmit() {
update_strategy: formData.value.update_strategy, update_strategy: formData.value.update_strategy,
update_type: formData.value.update_type, update_type: formData.value.update_type,
update_method: formData.value.update_method, update_method: formData.value.update_method,
custom_download_url: formData.value.custom_download_url,
changelog: formData.value.changelog, changelog: formData.value.changelog,
entry_file: formData.value.entry_file, entry_file: formData.value.entry_file,
file_path: uploadedData.value.file_path, file_path: uploadedData.value.file_path,
@@ -454,6 +456,18 @@ onMounted(() => {
</UiSelect> </UiSelect>
</div> </div>
<div v-if="formData.update_method === 'manual'" class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.customDownloadUrl') }}</UiLabel>
<p class="text-sm text-muted-foreground">
{{ t('admin.versions.createForm.customDownloadUrlDesc') }}
</p>
<UiInput
v-model="formData.custom_download_url"
:placeholder="t('admin.versions.createForm.customDownloadUrlPlaceholder')"
:disabled="saving"
/>
</div>
<div class="space-y-2"> <div class="space-y-2">
<UiLabel for="changelog"> <UiLabel for="changelog">
{{ t('admin.versions.createForm.changelog') }} {{ t('admin.versions.createForm.changelog') }}
@@ -516,6 +530,10 @@ onMounted(() => {
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewUpdateMethod') }}</span> <span class="text-muted-foreground">{{ t('admin.versions.createForm.previewUpdateMethod') }}</span>
<span>{{ methodLabel }}</span> <span>{{ methodLabel }}</span>
</div> </div>
<div v-if="formData.update_method === 'manual'" class="flex justify-between text-sm">
<span class="text-muted-foreground">{{ t('admin.versions.createForm.previewCustomDownloadUrl') }}</span>
<span class="truncate max-w-[120px]">{{ formData.custom_download_url || t('admin.versions.createForm.useDefault') }}</span>
</div>
</div> </div>
<div class="border-t pt-4 mt-4"> <div class="border-t pt-4 mt-4">
+5
View File
@@ -1499,6 +1499,11 @@
"previewUpdateType": "Update Type", "previewUpdateType": "Update Type",
"previewUpdateMethod": "Update Method", "previewUpdateMethod": "Update Method",
"previewAutoUpdate": "Auto Update", "previewAutoUpdate": "Auto Update",
"customDownloadUrl": "Custom Download URL",
"customDownloadUrlDesc": "Set a custom download URL, or use the platform default if not set",
"customDownloadUrlPlaceholder": "e.g., https://your-cdn.com/download/app.zip",
"previewCustomDownloadUrl": "Download URL",
"useDefault": "Use Default",
"changelogPreview": "Changelog Preview", "changelogPreview": "Changelog Preview",
"noChangelog": "No changelog", "noChangelog": "No changelog",
"notUploaded": "Not uploaded", "notUploaded": "Not uploaded",
+5
View File
@@ -1477,6 +1477,11 @@
"previewUpdateType": "更新类型", "previewUpdateType": "更新类型",
"previewUpdateMethod": "更新方式", "previewUpdateMethod": "更新方式",
"previewAutoUpdate": "自动更新", "previewAutoUpdate": "自动更新",
"customDownloadUrl": "自定义下载地址",
"customDownloadUrlDesc": "设置自定义下载地址,不设置则使用平台默认地址",
"customDownloadUrlPlaceholder": "例如: https://your-cdn.com/download/app.zip",
"previewCustomDownloadUrl": "下载地址",
"useDefault": "使用默认地址",
"changelogPreview": "更新日志预览", "changelogPreview": "更新日志预览",
"noChangelog": "暂无更新日志", "noChangelog": "暂无更新日志",
"notUploaded": "未上传", "notUploaded": "未上传",