feat: 添加补丁版本基础版本选择功能

- 添加基础版本下拉选择器,仅在补丁更新类型时显示
- 添加应用和更新类型的 watch 监听,自动获取全量版本列表
- 提交时传递 base_version_id 参数
- 添加调试日志便于排查问题

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 22:48:22 +08:00
parent 026581ea60
commit ad58d2220e
4 changed files with 133 additions and 31 deletions
+37 -24
View File
@@ -272,6 +272,7 @@ type CreateVersionRequest struct {
CustomDownloadURL string `json:"custom_download_url"`
Description string `json:"description"`
Changelog string `json:"changelog"`
BaseVersionID *uint `json:"base_version_id"`
}
func handleCreateVersionGlobal(c *gin.Context) {
@@ -360,36 +361,47 @@ func handleCreateVersionGlobal(c *gin.Context) {
}
} else if version.UpdateType == "patch" {
// 补丁包:需要找到基础全量包并合并
var lastFullVersion model.Version
if err := database.DB.Where("application_id = ? AND update_type = ?", req.ApplicationID, "full").
Order("id DESC").First(&lastFullVersion).Error; err == nil {
version.BaseVersionID = &lastFullVersion.ID
var baseVersion model.Version
// 获取基础版本的全量包路径,如果没有则使用 FilePath
baseFullPackagePath := lastFullVersion.FullPackagePath
if baseFullPackagePath == "" {
baseFullPackagePath = lastFullVersion.FilePath
}
// 合并生成新的全量包
fullPkgPath, fullPkgSize, fullPkgHash, err := MergePatchWithFullPackage(
baseFullPackagePath,
req.FilePath,
req.ApplicationID,
)
if err != nil {
log.Printf("[ERROR] Failed to merge patch with full package: %v\n", err)
response.Error(c, 500, "合并补丁包失败: "+err.Error())
// 优先使用传入的 base_version_id
if req.BaseVersionID != nil {
if err := database.DB.Where("id = ? AND application_id = ? AND update_type = ?",
*req.BaseVersionID, req.ApplicationID, "full").First(&baseVersion).Error; err != nil {
response.Error(c, 400, "指定的基础版本不存在或不是全量版本")
return
}
version.FullPackagePath = fullPkgPath
version.FullPackageSize = fullPkgSize
version.FullPackageHash = fullPkgHash
} else {
response.Error(c, 400, "找不到基础全量包,无法创建补丁版本")
// 未指定基础版本,使用最新的全量版本
if err := database.DB.Where("application_id = ? AND update_type = ?", req.ApplicationID, "full").
Order("id DESC").First(&baseVersion).Error; err != nil {
response.Error(c, 400, "找不到基础全量包,无法创建补丁版本")
return
}
}
version.BaseVersionID = &baseVersion.ID
// 获取基础版本的全量包路径,如果没有则使用 FilePath
baseFullPackagePath := baseVersion.FullPackagePath
if baseFullPackagePath == "" {
baseFullPackagePath = baseVersion.FilePath
}
// 合并生成新的全量包
fullPkgPath, fullPkgSize, fullPkgHash, err := MergePatchWithFullPackage(
baseFullPackagePath,
req.FilePath,
req.ApplicationID,
)
if err != nil {
log.Printf("[ERROR] Failed to merge patch with full package: %v\n", err)
response.Error(c, 500, "合并补丁包失败: "+err.Error())
return
}
version.FullPackagePath = fullPkgPath
version.FullPackageSize = fullPkgSize
version.FullPackageHash = fullPkgHash
}
if err := database.DB.Create(&version).Error; err != nil {
@@ -425,6 +437,7 @@ type UpdateVersionRequest struct {
CustomDownloadURL string `json:"custom_download_url"`
Description string `json:"description"`
Changelog string `json:"changelog"`
BaseVersionID *uint `json:"base_version_id"`
}
func handleUpdateVersionGlobal(c *gin.Context) {
+92 -3
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { CheckCircle, Eye, FileArchive, FilePlus, GitBranch, Loader2, Rocket, Upload, UploadCloud, X } from 'lucide-vue-next'
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { toast } from 'vue-sonner'
@@ -39,6 +39,8 @@ const uploading = ref(false)
const uploadProgress = ref(0)
const uploadSpeed = ref('')
const applications = ref<Application[]>([])
const baseVersions = ref<{ id: number, version: string }[]>([])
const loadingBaseVersions = ref(false)
const formData = ref({
application_id: '',
@@ -51,6 +53,7 @@ const formData = ref({
changelog: '',
entry_file: '',
file: null as File | null,
base_version_id: '' as string,
})
const uploadedData = ref<UploadResponse | null>(null)
@@ -123,6 +126,62 @@ async function fetchApplications() {
}
}
async function fetchBaseVersions(appId: string) {
if (!appId) {
baseVersions.value = []
return
}
loadingBaseVersions.value = true
try {
const data = await api.get<{ versions: { id: number, version: string, update_type: string }[] }>(
`/dev/versions?application_id=${appId}&update_type=full&page_size=100`
)
console.log('[DEBUG] fetchBaseVersions response:', data)
// 只筛选全量版本
const versions = data?.versions || []
baseVersions.value = versions.filter(v => v.update_type === 'full')
console.log('[DEBUG] baseVersions after filter:', baseVersions.value)
}
catch (error) {
console.error('获取基础版本列表失败:', error)
baseVersions.value = []
}
finally {
loadingBaseVersions.value = false
}
}
// 监听应用变化
watch(
() => formData.value.application_id,
(newAppId) => {
console.log('[DEBUG] application_id changed:', newAppId, 'update_type:', formData.value.update_type)
if (newAppId && formData.value.update_type === 'patch') {
fetchBaseVersions(newAppId)
} else if (!newAppId) {
// 只有当应用被清空时才清空基础版本
baseVersions.value = []
formData.value.base_version_id = ''
}
},
{ immediate: true }
)
// 监听更新类型变化
watch(
() => formData.value.update_type,
(newType) => {
console.log('[DEBUG] update_type changed:', newType, 'application_id:', formData.value.application_id)
if (newType === 'patch' && formData.value.application_id) {
fetchBaseVersions(formData.value.application_id)
} else if (newType !== 'patch') {
baseVersions.value = []
formData.value.base_version_id = ''
}
},
{ immediate: true }
)
function handleFileSelect(event: Event) {
const target = event.target as HTMLInputElement
if (target.files && target.files.length > 0) {
@@ -195,7 +254,7 @@ async function handleSubmit() {
saving.value = true
try {
await api.post('/dev/versions', {
const payload: Record<string, any> = {
application_id: Number(formData.value.application_id),
version: formData.value.version,
description: formData.value.description,
@@ -208,7 +267,12 @@ async function handleSubmit() {
file_path: uploadedData.value.file_path,
file_size: uploadedData.value.file_size,
file_hash: uploadedData.value.file_hash,
})
}
// 如果是补丁版本,添加基础版本ID
if (formData.value.update_type === 'patch' && formData.value.base_version_id) {
payload.base_version_id = Number(formData.value.base_version_id)
}
await api.post('/dev/versions', payload)
toast.success(t('admin.versions.createForm.createSuccess'))
router.push('/admin/versions')
}
@@ -465,6 +529,31 @@ onMounted(() => {
</UiSelect>
</div>
<!-- 基础版本选择仅补丁类型 -->
<div v-if="formData.update_type === 'patch'" class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.baseVersion') }}</UiLabel>
<p class="text-sm text-muted-foreground">
{{ t('admin.versions.createForm.baseVersionDesc') }}
</p>
<UiSelect v-model="formData.base_version_id" :disabled="saving || loadingBaseVersions">
<UiSelectTrigger>
<UiSelectValue :placeholder="t('admin.versions.createForm.baseVersionPlaceholder')" />
</UiSelectTrigger>
<UiSelectContent>
<UiSelectItem
v-for="v in baseVersions"
:key="v.id"
:value="String(v.id)"
>
{{ v.version }}
</UiSelectItem>
</UiSelectContent>
</UiSelect>
<p v-if="baseVersions.length === 0 && !loadingBaseVersions" class="text-sm text-destructive">
{{ t('admin.versions.createForm.noBaseVersion') }}
</p>
</div>
<div class="space-y-2">
<UiLabel>{{ t('admin.versions.createForm.updateMethod') }}</UiLabel>
<UiSelect v-model="formData.update_method" :disabled="saving">
+2 -2
View File
@@ -1502,8 +1502,8 @@
"updateMethod": "Update Method",
"baseVersion": "Base Version",
"baseVersionPlaceholder": "Select base version (patch mode)",
"baseVersionDesc": "Select a base version to only upload changed files for incremental patch update",
"noBaseVersion": "None (Full version)",
"baseVersionDesc": "Select a full version as the base for this patch. The patch will be generated based on this version",
"noBaseVersion": "No full version available. Please create a full version first",
"status": "Status",
"autoUpdate": "Auto Update",
"autoUpdateDesc": "When enabled, the application will automatically download and install updates",
+2 -2
View File
@@ -1480,8 +1480,8 @@
"updateMethod": "更新方式",
"baseVersion": "基础版本",
"baseVersionPlaceholder": "选择基础版本(补丁模式)",
"baseVersionDesc": "选择基础版本后,只会上传变更的文件,用于增量补丁更新",
"noBaseVersion": "无(完整版本",
"baseVersionDesc": "选择一个全量版本作为此补丁包的基础,补丁将基于此版本生成",
"noBaseVersion": "没有可用的全量版本,请先创建全量版本",
"status": "状态",
"autoUpdate": "自动更新",
"autoUpdateDesc": "启用后应用将自动下载并安装更新",