Files
hfb_sys/backend/internal/modules/paymentconfig/backup.go
T

214 lines
6.3 KiB
Go

package paymentconfig
import (
"context"
"errors"
"time"
"hfb_sys/backend/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// ExportBackup 导出所有支付配置备份,包含解密后的密钥。
func (r *Repository) ExportBackup(ctx context.Context, actorID uint64, meta AuditMeta) (*ExportBackup, error) {
var backup *ExportBackup
err := r.db.WithContext(ctx).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
}
// ImportBackup 导入支付配置备份,按 provider + merchant_id + pay_way 更新或新增。
func (r *Repository) ImportBackup(ctx context.Context, backup ExportBackup, actorID uint64, meta AuditMeta) (*ImportBackupResult, error) {
if backup.Type != "payment_config_backup" || backup.Version <= 0 {
return nil, ErrInvalidBackup
}
if len(backup.Configs) == 0 {
return nil, ErrEmptyBackup
}
activeKeys := backupActiveKeys(backup.Configs)
result := &ImportBackupResult{Total: len(backup.Configs)}
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for payWay := range activeKeys {
if err := deactivateOtherConfigs(tx, 0, actorID, payWay); err != nil {
return err
}
}
for _, cfg := range backup.Configs {
req := importCreateRequest(cfg, activeKeys)
if err := r.validateCreateRequest(req); err != nil {
return err
}
payWay := firstNonEmpty(req.PayWay, "ZFBZF")
encryptedSignKey, err := r.encryptor.Encrypt(req.SignKey)
if err != nil {
return ErrEncryptionFailed
}
encryptedNotifyKey, err := r.encryptor.Encrypt(req.NotifyKey)
if err != nil {
return ErrEncryptionFailed
}
updates := map[string]any{
"name": req.Name,
"gateway_url": req.GatewayURL,
"sign_key": encryptedSignKey,
"notify_key": encryptedNotifyKey,
"notify_url": req.NotifyURL,
"jump_url": req.JumpURL,
"pay_way": payWay,
"jspay_flag": firstNonEmpty(req.JSPayFlag, "2"),
"sign_type": firstNonEmpty(req.SignType, defaultSignType(req.Provider)),
"extra_config": model.JSONMap(req.ExtraConfig),
"is_default": req.IsDefault,
"status": firstNonEmpty(req.Status, "active"),
"environment": firstNonEmpty(req.Environment, "production"),
"business_tags": model.JSONArray(req.BusinessTags),
"updated_by": actorID,
}
var existing model.PaymentMerchantConfig
err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("provider = ? AND merchant_id = ? AND pay_way = ?", req.Provider, req.MerchantID, payWay).
First(&existing).Error
if err == nil {
if err := tx.Model(&existing).Updates(updates).Error; err != nil {
return err
}
result.Updated++
continue
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
createdBy := actorID
item := model.PaymentMerchantConfig{
Name: req.Name,
Provider: req.Provider,
MerchantID: req.MerchantID,
GatewayURL: req.GatewayURL,
SignKey: encryptedSignKey,
NotifyKey: encryptedNotifyKey,
NotifyURL: req.NotifyURL,
JumpURL: req.JumpURL,
PayWay: payWay,
JSPayFlag: firstNonEmpty(req.JSPayFlag, "2"),
SignType: firstNonEmpty(req.SignType, defaultSignType(req.Provider)),
ExtraConfig: model.JSONMap(req.ExtraConfig),
IsDefault: req.IsDefault,
Status: firstNonEmpty(req.Status, "active"),
Environment: firstNonEmpty(req.Environment, "production"),
BusinessTags: req.BusinessTags,
CreatedBy: &createdBy,
UpdatedBy: &actorID,
}
if err := tx.Create(&item).Error; err != nil {
return err
}
result.Created++
}
return appendAuditLog(tx, actorID, "payment_config.import", 0, meta, map[string]any{
"total": result.Total,
"created": result.Created,
"updated": result.Updated,
})
})
if err != nil {
return nil, err
}
return result, nil
}
// backupActiveKeys 提取备份中每个支付方式的启用配置键。
func backupActiveKeys(configs []ConfigDTO) map[string]string {
keys := make(map[string]string)
for _, cfg := range configs {
payWay := firstNonEmpty(cfg.PayWay, "ZFBZF")
if _, exists := keys[payWay]; exists {
continue
}
if cfg.Status == "active" || cfg.IsDefault {
keys[payWay] = paymentConfigImportKey(cfg.Provider, cfg.MerchantID, payWay)
}
}
return keys
}
// importCreateRequest 将备份 DTO 转为创建请求,并保留每个支付方式唯一启用配置。
func importCreateRequest(cfg ConfigDTO, activeKeys map[string]string) CreateRequest {
status := cfg.Status
if status == "" {
status = "disabled"
}
payWay := firstNonEmpty(cfg.PayWay, "ZFBZF")
activeKey := activeKeys[payWay]
isActive := activeKey != "" && paymentConfigImportKey(cfg.Provider, cfg.MerchantID, payWay) == activeKey
if isActive {
status = "active"
} else if status == "active" {
status = "disabled"
}
return CreateRequest{
Name: cfg.Name,
Provider: cfg.Provider,
MerchantID: cfg.MerchantID,
GatewayURL: cfg.GatewayURL,
SignKey: cfg.SignKey,
NotifyKey: cfg.NotifyKey,
NotifyURL: cfg.NotifyURL,
JumpURL: cfg.JumpURL,
PayWay: payWay,
JSPayFlag: cfg.JSPayFlag,
SignType: cfg.SignType,
ExtraConfig: cfg.ExtraConfig,
IsDefault: isActive,
Status: status,
Environment: cfg.Environment,
BusinessTags: cfg.BusinessTags,
}
}
// paymentConfigImportKey 生成导入去重键,同商户可按支付方式拆分为多条配置。
func paymentConfigImportKey(provider string, merchantID string, payWay string) string {
return provider + ":" + merchantID + ":" + payWay
}