Skip to content

fix: 完善导入导出配置回放还原#192

Merged
awsl233777 merged 2 commits intomainfrom
fix/import-export-roundtrip
Feb 8, 2026
Merged

fix: 完善导入导出配置回放还原#192
awsl233777 merged 2 commits intomainfrom
fix/import-export-roundtrip

Conversation

@Bowl42
Copy link
Collaborator

@Bowl42 Bowl42 commented Feb 8, 2026

变更摘要

  • 修复 Provider 导入/导出接口在尾斜杠场景下的路由识别问题
  • 补齐备份回放字段,支持 provider.logomodelPrices 的导入导出
  • 完善 SQLite Provider 的 logo 持久化映射,避免恢复后丢失
  • 增加备份导入时 ModelMapping 重复检测,避免重复导入
  • 新增回归与回放测试,覆盖导入导出关键路径

验证

  • go test ./internal/handler -run TestAdminHandler_ProvidersImport_WithTrailingSlash -count=1
  • go test ./internal/handler -run TestAdminHandler_ProvidersExport_WithTrailingSlash -count=1
  • go test ./internal/service -run TestBackupService_ -count=1
  • go test ./...

Summary by CodeRabbit

发布说明

  • 新功能

    • 备份现已支持模型价格数据的导入/导出
    • 提供商现可在备份中包含标志图像
  • 问题修复

    • 改进了导入/导出路由对尾部斜杠的处理
  • 测试

    • 新增备份服务端到端集成测试,验证配置保留和重复数据处理

@coderabbitai
Copy link

coderabbitai bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

注入并使用 ModelPriceRepository,扩展备份数据结构以包含模型价格与 Provider.logo,更新备份导出/导入逻辑以处理 modelPrices 并增强冲突检测与导入顺序,同时新增相关单元/集成测试并在启动/路由处完成依赖连接与路径修正。

Changes

Cohort / File(s) Summary
启动与路由注入
cmd/maxx/main.go, internal/core/database.go
创建并将 modelPriceRepo 传入服务与路由构造函数,更新 NewAdminService/NewBackupService/NewRouter 的调用以接受该依赖。
备份域与前端类型
internal/domain/backup.go, web/src/lib/transport/types.ts
新增 BackupModelPrice 类型;为 BackupData 添加 modelPrices 字段;为 BackupProvider 添加可选 logo 字段(传输层与域模型同步)。
备份服务实现与逻辑
internal/service/backup.go, internal/service/backup_test.go
BackupService 新增 modelPriceRepo 字段与构造参数;导出包含 ModelPrices,导入按顺序处理 ModelPrices(含冲突策略/干运行);新增并更新多项测试覆盖导出/导入、重复处理与构建键测试。
数据库模型与持久化
internal/repository/sqlite/models.go, internal/repository/sqlite/provider.go
在 SQLite Provider 模型中添加 Logo 列并在域↔模型转换中保存/恢复 Logo 字段。
Handler 与单元测试
internal/handler/admin.go, internal/handler/admin_import_export_test.go
修正路径处理以修剪尾部斜杠(支持 /.../import//.../export/);新增导入/导出端点的单元测试,覆盖带尾部斜杠的路由与导入导出行为。

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AdminHandler as Admin Handler
  participant BackupSvc as Backup Service
  participant ProviderRepo as Provider Repo
  participant ModelPriceRepo as ModelPrice Repo
  participant Storage as DB/File

  Client->>AdminHandler: POST /admin/backup/import or GET /admin/backup/export
  AdminHandler->>BackupSvc: Import(payload)/Export()
  BackupSvc->>ProviderRepo: load/save providers (include logo)
  BackupSvc->>ModelPriceRepo: load/save modelPrices
  BackupSvc->>Storage: persist other entities (routes, mappings, tokens...)
  ProviderRepo-->>BackupSvc: providers data
  ModelPriceRepo-->>BackupSvc: modelPrices data
  BackupSvc-->>AdminHandler: export payload / import result
  AdminHandler-->>Client: HTTP response (file or status)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • whhjdi
  • awsl233777
  • dreamhunter2333

Poem

🐰
我在代码地里蹦跳忙,备份里装满模型价;
Provider 戴上小徽章,导入导出都相对;
冲突检测织成网,测试陪我把路查;
咬根胡萝卜庆功,版本里跳一段舞。

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR标题用中文表述,完整概括了变更的核心目标:完善导入导出配置的回放还原功能,与详细描述和代码改动高度相关。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/import-export-roundtrip

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/service/backup.go`:
- Around line 934-947: The current buildModelMappingKey uses "|" which can
collide if any mapping field (e.g., ProviderName, Pattern, Target) contains that
character; change buildModelMappingKey to produce an unambiguous deterministic
key by serializing the relevant fields (Scope, ClientType, ProviderType,
ProviderName, ProjectSlug, RouteName, APITokenName, Pattern, Target, Priority)
and then hashing the serialization (e.g., SHA256 hex) or alternatively
JSON-marshal the struct and use that string as the key; update
buildModelMappingKey to assemble the selected fields into a struct or slice,
marshal it (or hash the marshaled bytes) and return that as the key so embedded
'|' cannot cause collisions.
🧹 Nitpick comments (3)
internal/handler/admin.go (1)

103-113: 尾部斜杠修复正确,解决了 /admin/providers/import//admin/providers/export/ 的路由问题。

HasSuffix 检查之前先 TrimSuffix 去除尾部 /,使得带尾部斜杠的请求路径能正确匹配 /export/import 后缀。

不过请注意,其他类似的子路由处理函数(如 handleModelMappings 第 1129 行、handleUsageStats 第 1267 行、handleModelPrices 第 1527 行)仍然直接使用 r.URL.PathHasSuffix 检查,没有做相同的尾部斜杠规范化处理,存在同样的潜在问题。可以考虑统一处理。

,

♻️ 建议对其他 handler 应用相同的尾部斜杠修复
 // handleModelMappings
-	path := r.URL.Path
+	path := strings.TrimSuffix(r.URL.Path, "/")
 // handleUsageStats
-	path := r.URL.Path
+	path := strings.TrimSuffix(r.URL.Path, "/")
 // handleModelPrices
-	path := r.URL.Path
+	path := strings.TrimSuffix(r.URL.Path, "/")
internal/handler/admin_import_export_test.go (1)

59-79: 构造函数参数脆弱:大量 nil 占位参数难以维护。

NewAdminService 使用了 18 个参数,其中大部分为 nil。如果构造函数签名再次变化(如本 PR 添加 modelPriceRepo 时),此处需要同步修改且编译器提示不够直观。建议后续考虑使用 options struct 或在测试中添加注释标注每个 nil 对应的参数名。

internal/service/backup.go (1)

319-322: 反向查找 map 的构建可与上方循环合并。

providerIDToNameprojectIDToSlugapiTokenIDToName 分别用了独立循环,但上方已有遍历同一 slice 的循环(如 316-318、329-331、351-353)。可将两个循环合并以减少冗余遍历。

♻️ 示例:合并 provider 循环
+	providerIDToName := make(map[uint64]string, len(providers))
 	for _, p := range providers {
 		ctx.providerNameToID[p.Name] = p.ID
+		providerIDToName[p.ID] = p.Name
 	}
-	providerIDToName := make(map[uint64]string, len(providers))
-	for _, p := range providers {
-		providerIDToName[p.ID] = p.Name
-	}

Also applies to: 332-335, 354-357

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d6d508 and fc1cba9.

📒 Files selected for processing (10)
  • cmd/maxx/main.go
  • internal/core/database.go
  • internal/domain/backup.go
  • internal/handler/admin.go
  • internal/handler/admin_import_export_test.go
  • internal/repository/sqlite/models.go
  • internal/repository/sqlite/provider.go
  • internal/service/backup.go
  • internal/service/backup_test.go
  • web/src/lib/transport/types.ts
🧰 Additional context used
🧬 Code graph analysis (4)
internal/domain/backup.go (1)
web/src/lib/transport/types.ts (1)
  • BackupModelPrice (822-835)
internal/repository/sqlite/provider.go (1)
internal/repository/sqlite/models.go (2)
  • LongText (17-17)
  • LongText (20-27)
internal/service/backup_test.go (6)
internal/service/backup.go (2)
  • BackupService (13-24)
  • NewBackupService (27-51)
internal/repository/sqlite/provider.go (1)
  • NewProviderRepository (15-17)
internal/repository/sqlite/system_setting.go (1)
  • NewSystemSettingRepository (16-18)
internal/repository/sqlite/model_price.go (1)
  • NewModelPriceRepository (15-17)
internal/domain/model.go (2)
  • ClientTypeOpenAI (15-15)
  • ClientTypeClaude (12-12)
internal/domain/backup.go (1)
  • ImportOptions (122-125)
web/src/lib/transport/types.ts (1)
internal/domain/backup.go (1)
  • BackupModelPrice (106-119)
🔇 Additional comments (17)
internal/domain/backup.go (2)

105-119: BackupModelPrice 结构体与前端类型定义一致,设计合理。

新增的 BackupModelPrice 结构体字段与前端 web/src/lib/transport/types.ts 中的 BackupModelPrice 接口完全对齐,JSON tag 命名一致。omitemptyBackupData.ModelPrices 上的使用保证了与旧版备份文件的向后兼容性。


26-26: 新增字段向后兼容,LGTM。

ModelPricesLogo 均使用了 omitempty,旧版备份文件在反序列化时不会出错。BackupVersion 保持 "1.0" 不变是合理的,因为这些都是纯增量的可选字段。

Also applies to: 39-39

internal/core/database.go (1)

336-347: ModelPriceRepo 注入 BackupService,接线正确。

repos.ModelPriceRepo 作为新参数传入 NewBackupService,与 backup.goNewBackupService 构造函数签名一致。

,

AI 摘要声称 NewRouter 签名也更新以包含 ModelPriceRepository,但实际代码(第 237-243 行)中 NewRouter 仍然只接收 5 个参数,并未包含 ModelPriceRepo

internal/repository/sqlite/provider.go (1)

85-85: Logo 字段的持久化实现正确,与现有模式一致。

LongText 类型适合存储可能较大的 Logo 数据(如 base64 编码的 data URI),直接类型转换(无需 JSON 序列化/反序列化)对于纯字符串字段是正确的做法。

Also applies to: 101-101

internal/repository/sqlite/models.go (1)

67-67: LGTM!

Logo LongText 字段添加与 Provider 结构体中的其他 LongText 字段(Config、SupportedClientTypes、SupportModels)风格一致,GORM AutoMigrate 会自动处理新列的添加。

cmd/maxx/main.go (1)

287-298: BackupService 接线与 internal/core/database.go 保持一致,LGTM。

modelPriceRepo 在两个入口点(独立二进制 cmd/maxx/main.gointernal/core/database.go)中均正确传入 NewBackupService,确保了备份功能在不同启动模式下的一致性。

internal/service/backup_test.go (3)

167-227: 测试覆盖全面,验证了关键的备份往返保真性。

测试覆盖了完整的 export → import → re-export 流程,并验证了以下关键数据的保真性:

  • Provider Logo
  • ModelPrice(包括具体字段值)
  • API Token(包括明文 token)
  • ModelMapping(包括路由引用)

这组测试能有效防止备份数据丢失的回归问题。


229-251: 重复检测测试逻辑清晰。

向同一数据库导入已存在的数据并验证 Skipped > 0,是验证 ConflictStrategy: "skip" 对 model mappings 生效的有效方式。


26-41: 无需修改 — adapterRefresher 已有空值保护。

第 508 行已存在 if s.adapterRefresher != nil { 的保护,在调用 RefreshAdapter() 前进行了空值检查。测试传入 nil 是安全的,不会导致 panic。

internal/handler/admin_import_export_test.go (2)

84-116: Import 测试逻辑清晰,LGTM。

测试覆盖了 trailing slash 路由、响应状态码、导入计数和仓库状态,断言合理。


118-150: Export 测试逻辑清晰,LGTM。

验证了 trailing slash 路由、Content-Disposition header 和响应体内容,覆盖到位。

web/src/lib/transport/types.ts (2)

822-835: BackupModelPrice 接口与 Go 后端 domain.BackupModelPrice 字段完全对齐,LGTM。

12 个字段及其类型均与后端 JSON tag 一致。


751-751: BackupDataBackupProvider 扩展合理,LGTM。

modelPriceslogo 均为可选字段,保持了向后兼容。

Also applies to: 762-762

internal/service/backup.go (4)

238-258: ModelPrices 导出逻辑清晰,LGTM。

使用 ListCurrentPrices 获取有效价格并逐字段映射到 BackupModelPrice,与其他实体的导出模式一致。


850-928: importModelPrices 实现完整,冲突策略覆盖全面,LGTM。

skip/error/overwrite 三种策略均正确处理,overwrite 保留了原记录的 ID 和 CreatedAt,新增记录也同步写入 existingByModelID 以防同一批次内重复。


373-392: 新增的 model mapping 冲突检测逻辑合理。

通过加载现有 mappings 并构建 composite key,在导入时实现了重复检测。与 route 冲突检测的模式一致。


764-779: Model mapping 导入冲突检测和批次内去重均已覆盖,LGTM。

导入前检查 modelMappingKeys,导入成功后将 key 加入集合(line 843),有效防止同一备份文件中的重复条目。

Also applies to: 843-843

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@Bowl42 Bowl42 force-pushed the fix/import-export-roundtrip branch from fc1cba9 to 7f5a1e5 Compare February 8, 2026 06:37
@Bowl42 Bowl42 changed the title fix: complete backup import/export roundtrip restoration fix: 完善导入导出配置回放还原 Feb 8, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@internal/service/backup.go`:
- Around line 964-967: buildModelMappingKey currently swallows json.Marshal
errors and returns an empty string, causing distinct mappings to collide in
modelMappingKeys; update buildModelMappingKey to surface failures instead of
returning "" — either change its signature to return (string, error) and
propagate the error to callers (so callers of buildModelMappingKey can
handle/fail explicitly) or, if this truly is an unrecoverable invariant, replace
the empty-string return with a panic that includes the payload and marshal
error; ensure all call sites that populate modelMappingKeys handle the new error
or accept the panic approach so collisions/hidden skips no longer occur.
🧹 Nitpick comments (1)
internal/service/backup.go (1)

933-935: buildRouteKey 使用 : 分隔符,与 Export 中 RouteName 的拼接格式一致,但存在理论冲突风险。

providerNameprojectSlug:,可能产生 key 碰撞(例如 "a:b" + "openai" + """a" + "b:openai" + "")。当前 Export(Line 230-233)也使用相同格式写入 RouteName,因此导入导出是一致的。如果 provider/project 命名有校验限制则无需改动,否则可考虑与 buildModelMappingKey 同样采用 JSON+hash 方式。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f5a1e5 and b865854.

📒 Files selected for processing (2)
  • internal/service/backup.go
  • internal/service/backup_test.go
🧰 Additional context used
🧬 Code graph analysis (2)
internal/service/backup.go (3)
internal/repository/interfaces.go (1)
  • ModelPriceRepository (246-269)
internal/domain/backup.go (5)
  • BackupModelPrice (106-119)
  • BackupModelMapping (92-103)
  • ImportOptions (122-125)
  • ImportResult (135-140)
  • ImportSummary (128-132)
internal/domain/model.go (3)
  • ClientType (9-9)
  • ModelPrice (515-534)
  • ModelMappingScope (743-743)
internal/service/backup_test.go (2)
internal/service/backup.go (2)
  • BackupService (16-27)
  • NewBackupService (30-54)
internal/domain/backup.go (2)
  • ImportOptions (122-125)
  • BackupModelMapping (92-103)
🔇 Additional comments (9)
internal/service/backup.go (5)

241-261: ModelPrices 导出逻辑清晰,LGTM。

字段映射完整,与 BackupModelPrice 结构对齐,错误处理一致。


853-931: importModelPrices 实现完整,支持 skip/overwrite/error 三种策略。

有一个小细节值得注意:overwrite 时使用 existing.IDexisting.CreatedAt 保留原记录标识,新建时将 price 加入 existingByModelID 避免同批次重复——逻辑正确。


376-395: 加载现有 ModelMapping 用于冲突检测的逻辑合理。

通过构造 BackupModelMapping 并调用 buildModelMappingKey 生成一致的 key,与导入时的比较逻辑对称。注意:当 ProviderID/ProjectID/RouteID/APITokenID 为 0 时,对应的 reverse lookup map 会返回零值空字符串,这与 BackupModelMappingomitempty 字段为空的行为一致,不会导致 key 不匹配。


112-119: Provider 导出现已包含 Logo 字段,导入端(Line 498)也同步设置——roundtrip 对称性良好。


766-782: ModelMapping 冲突检测:导入新 mapping 后也同步更新 ctx.modelMappingKeys(Line 846),确保同批次内的重复也能检测到。

internal/service/backup_test.go (4)

167-227: Roundtrip 测试覆盖了核心新增功能(Logo、ModelPrices、Token 原值还原、ModelMapping 路由引用),LGTM。

可选改进:考虑补充对 RoutesProjectsRetryConfigsSystemSettings 等实体数量和关键字段的断言,以增强回归保障。


253-278: Key 碰撞测试有效验证了先前 | 分隔符冲突已修复。


12-41: 测试辅助函数简洁清晰,t.Cleanup 确保 DB 资源释放。


229-251: 重复导入跳过测试验证了 ModelMapping 冲突检测在 "skip" 策略下的行为。

注意该测试依赖于所有上游实体(Provider、Project 等)也被 skip,从而使 ctx 中的 name→ID 映射来自 loadExistingMappings。这正好验证了 loadExistingMappings 中新增的 reverse lookup 逻辑的正确性。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +964 to +967
encoded, err := json.Marshal(payload)
if err != nil {
return ""
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

buildModelMappingKey 在序列化失败时返回空字符串,可能导致误判冲突。

json.Marshal 对纯基础类型结构体几乎不会失败,但若返回 "",多个不同 mapping 会被视为同一 key,在 modelMappingKeys 中产生假冲突,导致数据被静默跳过或报错。建议在此处 panic 或返回 error,因为这属于"不应发生"的编程错误。

🛡️ 建议修改
 	encoded, err := json.Marshal(payload)
 	if err != nil {
-		return ""
+		// 基础类型序列化不应失败;若发生则属编程错误
+		panic(fmt.Sprintf("buildModelMappingKey: marshal failed: %v", err))
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
encoded, err := json.Marshal(payload)
if err != nil {
return ""
}
encoded, err := json.Marshal(payload)
if err != nil {
// 基础类型序列化不应失败;若发生则属编程错误
panic(fmt.Sprintf("buildModelMappingKey: marshal failed: %v", err))
}
🤖 Prompt for AI Agents
In `@internal/service/backup.go` around lines 964 - 967, buildModelMappingKey
currently swallows json.Marshal errors and returns an empty string, causing
distinct mappings to collide in modelMappingKeys; update buildModelMappingKey to
surface failures instead of returning "" — either change its signature to return
(string, error) and propagate the error to callers (so callers of
buildModelMappingKey can handle/fail explicitly) or, if this truly is an
unrecoverable invariant, replace the empty-string return with a panic that
includes the payload and marshal error; ensure all call sites that populate
modelMappingKeys handle the new error or accept the panic approach so
collisions/hidden skips no longer occur.

@Bowl42
Copy link
Collaborator Author

Bowl42 commented Feb 8, 2026

已处理这条 review comment,修复如下:

  • buildModelMappingKey 从字符串分隔拼接改为:
    1. 对关键字段进行结构化 JSON 序列化
    2. 计算 SHA256 并使用 hex 作为 key

这样即使字段内包含 | 等字符,也不会发生 key 误碰撞。

另外补充了回归测试:

  • TestBuildModelMappingKey_NoSeparatorCollision

验证:

  • go test ./internal/service -run "TestBackupService_|TestBuildModelMappingKey_NoSeparatorCollision" -count=1
  • go test ./...

已推送到当前 PR 分支。

@awsl233777 awsl233777 merged commit 2f2b9e6 into main Feb 8, 2026
2 checks passed
@awsl233777 awsl233777 deleted the fix/import-export-roundtrip branch February 8, 2026 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants