{"record":{"id":"40277e081e5e8617","repo":"flipped-aurora/gin-vue-admin","slug":"error-40277e","errorCode":null,"errorMessage":"上传不在可合并状态(可能已在合并或已完成)","messagePattern":"上传不在可合并状态\\(可能已在合并或已完成\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"server/service/media/media_upload.go","lineNumber":118,"sourceCode":"\tvar up media.MediaUpload\n\tif err := global.GVA_DB.WithContext(ctx).First(&up, uploadID).Error; err != nil {\n\t\treturn m, errors.New(\"上传会话不存在\")\n\t}\n\tif up.UserID != userID {\n\t\treturn m, errors.New(\"无权操作该上传\")\n\t}\n\tif err := upload.ValidateFileExtension(up.FileName); err != nil {\n\t\treturn m, err\n\t}\n\t// 原子抢占:仅 uploading -> merging 的赢家继续\n\tres := global.GVA_DB.WithContext(ctx).Model(&media.MediaUpload{}).\n\t\tWhere(\"id = ? AND status = ?\", uploadID, media.UploadStatusUploading).\n\t\tUpdate(\"status\", media.UploadStatusMerging)\n\tif res.Error != nil {\n\t\treturn m, res.Error\n\t}\n\tif res.RowsAffected == 0 {\n\t\treturn m, errors.New(\"上传不在可合并状态(可能已在合并或已完成)\")\n\t}\n\n\tfail := func(e error) (media.FileUploadAndDownload, error) {\n\t\tglobal.GVA_DB.WithContext(ctx).Model(&media.MediaUpload{}).Where(\"id = ?\", uploadID).Update(\"status\", media.UploadStatusFailed)\n\t\treturn m, e\n\t}\n\n\t// 校验分片齐全\n\tvar cnt int64\n\tglobal.GVA_DB.WithContext(ctx).Model(&media.MediaUploadChunk{}).Where(\"upload_id = ?\", uploadID).Count(&cnt)\n\tif int(cnt) != up.ChunkTotal {\n\t\treturn fail(fmt.Errorf(\"分片不全: %d/%d\", cnt, up.ChunkTotal))\n\t}\n\n\t// 合并到临时成品\n\tmerged := filepath.Join(upload.ChunkDir(uploadID), \"merged.bin\")\n\tgotMd5, err := upload.MergeChunks(uploadID, up.ChunkTotal, merged)\n\tif err != nil {","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/flipped-aurora/gin-vue-admin/blob/3136500ef380842b0eb6c4daa87c3f8a47fcf9e0/server/service/media/media_upload.go#L100-L136","documentation":"Complete() performs an atomic compare-and-set UPDATE moving the MediaUpload row from status 'uploading' to 'merging'. If RowsAffected is 0, another request already won the merge race or the row is in a non-mergeable state, so it returns '上传不在可合并状态(可能已在合并或已完成)'.","triggerScenarios":"Two concurrent Complete calls for the same uploadID (only one UPDATE wins); the upload already finished (status completed); a previous failed merge left status failed/merging; calling Complete after Cancel; calling Complete before all chunk uploads set status to uploading.","commonSituations":"Frontend double-clicks or retries Complete while the first request is still merging; a load balancer duplicates the request; a stuck 'merging' row left behind by a crashed merge blocks retries; completing an upload that already succeeded earlier.","solutions":["Treat this as expected in concurrency: only call Complete once per upload session and disable the UI button after the first call.","Query the current status of the upload row; if it is completed, fetch the resulting file record instead of merging again.","If a row is stuck in 'merging' from a crashed process, reset its status back to 'uploading' (manually or via admin endpoint) and retry.","If stuck in 'failed', restart the upload flow with a new session.","Add idempotency on the client: remember successful completion keyed by uploadID."],"exampleFix":"// before: completing again after timeout regardless of state\nawait api.complete({ uploadId }) // retried on timeout -> conflict\n// after: complete once, and on conflict poll the final state\ntry {\n  await api.complete({ uploadId })\n} catch (e) {\n  const st = await api.getUploadStatus(uploadId)\n  if (st.status === 'completed') return st.file // already merged by another call\n  throw e\n}","handlingStrategy":"retry","validationCode":"// check current state before completing\nconst st = await api.getUploadStatus(uploadId)\nif (st.status !== 'uploading') throw new Error('upload not in mergeable state: ' + st.status)","typeGuard":null,"tryCatchPattern":"try {\n  await svc.Complete(ctx, userID, uploadID)\n} catch (err) {\n  if (strings.Contains(err.Error(), \"不在可合并状态\")) {\n    // not an error in concurrent flows: re-read status;\n    // if completed, treat as success; if merging, back off and poll\n  } else {\n    return err\n  }\n}","preventionTips":["Call Complete exactly once per upload; debounce/disable the button","Add client idempotency keyed by uploadID","Poll upload status after a conflict instead of re-invoking Complete","Monitor for rows stuck in 'merging' and add a cleanup/reset job"],"tags":["upload","concurrency","state-machine","race-condition"],"backgroundTag":"invalid-state-transition","analyzedSha":"3136500ef380842b0eb6c4daa87c3f8a47fcf9e0","analyzedAt":"2026-08-31T13:50:02.721Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}