Gridea Pro 版本
main 分支 HEAD (b84db26)
操作系统
macOS (Apple Silicon)
系统版本
Darwin 25.4.0(问题与平台无关)
优先级
P1(数据一致性 / 用户信任)
Bug 描述
backend/internal/service/cdn_upload_service.go:364-380 的 UploadMediaForDeploy 里:
for _, file := range filesToUpload {
f := file
g.Go(func() error {
sem <- struct{}{}
defer func() { <-sem }()
if err := s.uploadToGitHub(gCtx, setting, f.localPath, f.remotePath); err != nil {
logger(fmt.Sprintf(\"上传 %s 失败: %v\", f.remotePath, err))
return nil // 单个文件失败不中断整个上传
}
mu.Lock()
uploadCount++
mu.Unlock()
return nil
})
}
if err := g.Wait(); err != nil {
return fmt.Errorf(\"上传媒体文件失败: %w\", err)
}
logger(fmt.Sprintf(\"CDN 上传完成,共上传 %d 个文件\", uploadCount))
问题细节:
return nil——失败不中断。设计意图可以理解(不让一张图挂掉整个部署),但实现有多处缺陷:
- 上层
DeployService.DeployToRemote(deploy_service.go:92-98)对 CDN 错误全部降级为 warning:
if err := s.cdnUploadService.UploadMediaForDeploy(...); err != nil {
s.log(ctx, fmt.Sprintf(\"CDN upload warning: %v\", err))
}
即便返回了错误也不阻止部署继续。
- 结合起来:假设 100 张图有 50 张上传失败,最终用户看到的 toast 是"同步成功"——但实际上线上站点打开后,50 张图全是 404(jsdelivr 或自定义 CDN URL 都取不到)。
复现步骤
- 配置 CDN 为 GitHub + jsdelivr,写入一个错误 token(或权限只读)。
- 在
post-images/ 里放几十张图。
- 点发布。
- 结果:toast 提示"同步成功",但访问站点时图片全 404;打开控制台能看到背后刷的失败日志,但 UI 上毫无指示。
期望行为
- 失败文件被显式汇总并返回给调用方。
- 失败比例或数量超过阈值(例如 > 10% 或 > 5 个)时中止部署,弹窗询问用户"忽略失败继续 / 重试失败项 / 取消本次部署"。
- 即便少量失败也在最终 UI 上显示"上传 95/100 成功,5 个失败(可展开查看)"。
- 失败的图片路径列表保留在 store 中,可复制用于排查。
实际行为
失败静默,用户不知情;部署声称成功但线上图片大面积 404。
截图 / 日志
无。
补充信息 — 最佳解决方案
1. 重构返回值
type UploadResult struct {
Success int
Failed []UploadFailure
Total int
}
type UploadFailure struct {
Path string
Error string
// 可选:HTTP 状态码等元信息
}
func (s *CdnUploadService) UploadMediaForDeploy(ctx context.Context, appDir string, eventSink func(DeployEvent)) (UploadResult, error)
errgroup 改为收集 []UploadFailure(带锁),不因单文件失败终止整组:
var (
failuresMu sync.Mutex
failures []UploadFailure
)
for _, file := range filesToUpload {
f := file
g.Go(func() error {
if err := ctx.Err(); err != nil { return err }
if err := s.uploadToGitHub(gCtx, setting, f.localPath, f.remotePath); err != nil {
failuresMu.Lock()
failures = append(failures, UploadFailure{Path: f.remotePath, Error: err.Error()})
failuresMu.Unlock()
return nil
}
atomic.AddInt32(&successCount, 1)
return nil
})
}
2. 阈值策略 & 用户确认
DeployService.DeployToRemote 里拿到 UploadResult 后:
if len(result.Failed) > 0 {
ratio := float64(len(result.Failed)) / float64(result.Total)
if ratio > s.cdnFailureThreshold() || len(result.Failed) > s.cdnFailureAbsoluteCap() {
// emit 事件 \"需要用户决策\"
choice := s.askUser(ctx, result)
switch choice {
case \"retry\":
// 只对失败集合重新上传一次
case \"abort\":
return fmt.Errorf(\"CDN 上传失败过多: %d/%d\", len(result.Failed), result.Total)
case \"ignore\":
// 继续部署,但在最终摘要里显眼标出
}
}
}
阈值默认 threshold=0.1、absoluteCap=5,写入设置可调。
3. 前端最终摘要
部署结束的 toast / modal 要展示:
- 成功数 / 总数。
- 失败列表(可展开),每行显示路径 + 简化错误原因。
- 提供"复制失败列表"、"仅重试失败项"两个按钮。
4. 单测
- 构造一个 uploadToGitHub 注入 stub,模拟 50% 失败率,验证:
UploadResult.Failed 长度正确;
- 阈值策略能触发 askUser;
- 仅重试失败项时不会重复上传成功项。
影响文件:
backend/internal/service/cdn_upload_service.go
backend/internal/service/deploy_service.go(阈值处理、askUser 协议)
backend/internal/domain/errors.go(新增错误键)
- 前端:部署摘要 modal / drawer。
检查清单
Gridea Pro 版本
main 分支 HEAD (b84db26)
操作系统
macOS (Apple Silicon)
系统版本
Darwin 25.4.0(问题与平台无关)
优先级
P1(数据一致性 / 用户信任)
Bug 描述
backend/internal/service/cdn_upload_service.go:364-380的UploadMediaForDeploy里:问题细节:
return nil——失败不中断。设计意图可以理解(不让一张图挂掉整个部署),但实现有多处缺陷:logger流里打印,而前端并不监听deploy-log(参见对应 issue [Bug] 前端完全未监听 deploy-log 事件,部署过程无任何进度反馈 #43)。uploadCount只数成功的,最终 "CDN 上传完成,共上传 N 个文件" 的 N 不等于 total,但用户看不出来。DeployService.DeployToRemote(deploy_service.go:92-98)对 CDN 错误全部降级为 warning:复现步骤
post-images/里放几十张图。期望行为
实际行为
失败静默,用户不知情;部署声称成功但线上图片大面积 404。
截图 / 日志
无。
补充信息 — 最佳解决方案
1. 重构返回值
errgroup改为收集[]UploadFailure(带锁),不因单文件失败终止整组:2. 阈值策略 & 用户确认
DeployService.DeployToRemote里拿到UploadResult后:阈值默认
threshold=0.1、absoluteCap=5,写入设置可调。3. 前端最终摘要
部署结束的 toast / modal 要展示:
4. 单测
UploadResult.Failed长度正确;影响文件:
backend/internal/service/cdn_upload_service.gobackend/internal/service/deploy_service.go(阈值处理、askUser 协议)backend/internal/domain/errors.go(新增错误键)检查清单