diff --git a/backend/internal/modules/paymentconfig/dto.go b/backend/internal/modules/paymentconfig/dto.go index 4277298..38e456e 100644 --- a/backend/internal/modules/paymentconfig/dto.go +++ b/backend/internal/modules/paymentconfig/dto.go @@ -106,3 +106,13 @@ type ListResponse struct { Page int `json:"page"` PageSize int `json:"page_size"` } + +// ExportBackup 支付配置备份导出内容。 +type ExportBackup struct { + Type string `json:"type"` + Version int `json:"version"` + ExportedAt string `json:"exported_at"` + ExportedBy uint64 `json:"exported_by"` + Total int `json:"total"` + Configs []ConfigDTO `json:"configs"` +} diff --git a/backend/internal/modules/paymentconfig/handler.go b/backend/internal/modules/paymentconfig/handler.go index 7d71a99..8bfe3da 100644 --- a/backend/internal/modules/paymentconfig/handler.go +++ b/backend/internal/modules/paymentconfig/handler.go @@ -1,8 +1,10 @@ package paymentconfig import ( + "encoding/json" "net/http" "strconv" + "time" "hfb_sys/backend/internal/middleware" "hfb_sys/backend/pkg/response" @@ -73,6 +75,40 @@ func (h *Handler) Get(c *gin.Context) { response.OK(c, config) } +// ExportBackup 导出支付配置备份 +// @Summary 导出支付配置备份 +// @Tags 管理后台-支付配置 +// @Success 200 {file} file "支付配置备份 JSON" +// @Router /admin/payment-configs/export [get] +func (h *Handler) ExportBackup(c *gin.Context) { + adminID, ok := currentAdminID(c) + if !ok { + response.Unauthorized(c, "未授权") + return + } + + backup, err := h.service.ExportBackup(adminID, auditMeta(c)) + if err == ErrDecryptionFailed { + response.Error(c, http.StatusInternalServerError, "decrypt_failed", "密钥解密失败") + return + } + if err != nil { + response.Error(c, http.StatusInternalServerError, "internal_error", "导出失败") + return + } + + data, err := json.MarshalIndent(backup, "", " ") + if err != nil { + response.Error(c, http.StatusInternalServerError, "internal_error", "导出失败") + return + } + + filename := "payment-config-backup-" + time.Now().Format("20060102-150405") + ".json" + contentType := "application/json; charset=utf-8" + c.Header("Content-Disposition", `attachment; filename="`+filename+`"`) + c.Data(http.StatusOK, contentType, data) +} + // Create 创建支付配置 // @Summary 创建支付配置 // @Tags 管理后台-支付配置 diff --git a/backend/internal/modules/paymentconfig/repository.go b/backend/internal/modules/paymentconfig/repository.go index 2a3eb08..cfaeb28 100644 --- a/backend/internal/modules/paymentconfig/repository.go +++ b/backend/internal/modules/paymentconfig/repository.go @@ -153,6 +153,45 @@ func (r *Repository) FindByProviderMerchant(provider string, merchantID string, return &dto, nil } +// ExportBackup 导出所有支付配置备份,包含解密后的密钥。 +func (r *Repository) ExportBackup(actorID uint64, meta AuditMeta) (*ExportBackup, error) { + var backup *ExportBackup + err := r.db.Transaction(func(tx *gorm.DB) error { + var items []model.PaymentMerchantConfig + if err := tx.Order("is_default DESC, id DESC").Find(&items).Error; err != nil { + return err + } + + configs := make([]ConfigDTO, 0, len(items)) + for _, item := range items { + dto, err := r.toDTO(item, true) + if err != nil { + return err + } + configs = append(configs, dto) + } + + exportedAt := time.Now().Format(time.RFC3339) + backup = &ExportBackup{ + Type: "payment_config_backup", + Version: 1, + ExportedAt: exportedAt, + ExportedBy: actorID, + Total: len(configs), + Configs: configs, + } + + return appendAuditLog(tx, actorID, "payment_config.export", 0, meta, map[string]any{ + "total": len(configs), + "exported_at": exportedAt, + }) + }) + if err != nil { + return nil, err + } + return backup, nil +} + // FindActiveByProvider 查询提供商的所有激活配置 func (r *Repository) FindActiveByProvider(provider string) ([]model.PaymentMerchantConfig, error) { var items []model.PaymentMerchantConfig diff --git a/backend/internal/modules/paymentconfig/service.go b/backend/internal/modules/paymentconfig/service.go index 984555f..b3dcaa4 100644 --- a/backend/internal/modules/paymentconfig/service.go +++ b/backend/internal/modules/paymentconfig/service.go @@ -37,6 +37,11 @@ func (s *Service) Get(id uint64, includeSecret bool) (*ConfigDTO, error) { return s.repo.FindByID(id, includeSecret) } +// ExportBackup 导出支付配置备份。 +func (s *Service) ExportBackup(actorID uint64, meta AuditMeta) (*ExportBackup, error) { + return s.repo.ExportBackup(actorID, meta) +} + // Create 创建配置 func (s *Service) Create(req CreateRequest, actorID uint64, meta AuditMeta) (*ConfigDTO, error) { return s.repo.Create(req, actorID, meta) diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index ab0878f..7616789 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -424,6 +424,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { // 支付配置管理 if paymentConfigHandler != nil { adminRoutes.GET("/payment-configs", requirePerm("payment_config:list"), paymentConfigHandler.List) + adminRoutes.GET("/payment-configs/export", requirePerm("payment_config:view_secret"), paymentConfigHandler.ExportBackup) adminRoutes.GET("/payment-configs/:id", requirePerm("payment_config:list"), paymentConfigHandler.Get) adminRoutes.POST("/payment-configs", requirePerm("payment_config:create"), paymentConfigHandler.Create) adminRoutes.PUT("/payment-configs/:id", requirePerm("payment_config:update"), paymentConfigHandler.Update) diff --git a/backend/migrations/000002_payment_lakala_multichannel.sql b/backend/migrations/000002_payment_lakala_multichannel.sql deleted file mode 100644 index 62c52e0..0000000 --- a/backend/migrations/000002_payment_lakala_multichannel.sql +++ /dev/null @@ -1,10 +0,0 @@ --- 支付多渠道和拉卡拉配置字段扩展 -ALTER TABLE payment_orders - MODIFY COLUMN provider VARCHAR(32) NOT NULL COMMENT '支付服务商: leshua乐刷, lakala拉卡拉, mock模拟', - MODIFY COLUMN merchant_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '商户号'; - -ALTER TABLE payment_merchant_configs - MODIFY COLUMN provider VARCHAR(32) NOT NULL COMMENT '支付服务商: leshua乐刷, lakala拉卡拉, mock模拟', - MODIFY COLUMN sign_key TEXT NULL COMMENT '签名密钥/商户私钥(加密存储)', - MODIFY COLUMN notify_key TEXT NULL COMMENT '通知验签密钥/平台通知证书(加密存储)', - MODIFY COLUMN sign_type VARCHAR(32) NOT NULL DEFAULT 'MD5' COMMENT '签名类型: MD5, SHA256withRSA'; diff --git a/frontend/package.json b/frontend/package.json index e7df13f..bb98ac5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,8 +4,8 @@ "private": true, "type": "module", "scripts": { - "dev": "vue-tsc -b --watch & vite --host 0.0.0.0", - "build": "vue-tsc -b && vite build", + "dev": "vue-tsc -b --noEmit --watch & vite --host 0.0.0.0", + "build": "vue-tsc -b --noEmit && vite build", "preview": "vite preview --host 0.0.0.0", "typecheck": "vue-tsc -b --noEmit" }, diff --git a/frontend/src/features/admin/api/paymentConfig.ts b/frontend/src/features/admin/api/paymentConfig.ts index 2ee4c19..79574fd 100644 --- a/frontend/src/features/admin/api/paymentConfig.ts +++ b/frontend/src/features/admin/api/paymentConfig.ts @@ -92,6 +92,16 @@ export async function fetchPaymentConfig(id: number, includeSecret = false) { return data.data } +export async function exportPaymentConfigBackup() { + const response = await apiClient.get('/admin/payment-configs/export', { + responseType: 'blob', + }) + return { + blob: response.data, + filename: readDownloadFilename(response.headers['content-disposition']) || fallbackBackupFilename(), + } +} + export async function createPaymentConfig(payload: CreatePaymentConfigRequest) { const { data } = await apiClient.post>('/admin/payment-configs', payload) return data.data @@ -106,3 +116,15 @@ export async function deletePaymentConfig(id: number) { const { data } = await apiClient.delete>(`/admin/payment-configs/${id}`) return data.data } + +function readDownloadFilename(contentDisposition: unknown) { + if (typeof contentDisposition !== 'string') return '' + const encoded = contentDisposition.match(/filename\*=UTF-8''([^;]+)/i)?.[1] + if (encoded) return decodeURIComponent(encoded) + return contentDisposition.match(/filename="?([^";]+)"?/i)?.[1] || '' +} + +function fallbackBackupFilename() { + const stamp = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, '') + return `payment-config-backup-${stamp}.json` +} diff --git a/frontend/src/features/admin/views/AdminPaymentConfigsView.vue b/frontend/src/features/admin/views/AdminPaymentConfigsView.vue index b11a97d..c49fbfb 100644 --- a/frontend/src/features/admin/views/AdminPaymentConfigsView.vue +++ b/frontend/src/features/admin/views/AdminPaymentConfigsView.vue @@ -1,11 +1,12 @@