mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
feat(payment): validate provider config at save time + loosen wxpay pair rule
**保存时校验** — admin 保存 enabled 的 provider 时,调用 provider.CreateProvider 做一次构造器校验(只在 enabled 时),把原本只在创建订单才暴露的配置错误提前到保存那一刻。已有的结构化错误(WXPAY_CONFIG_MISSING_KEY 等)直接给前端 i18n 用。 **wxpay 校验放宽** — 之前 publicKey/publicKeyId "必须成对"的规则过于严格:DB 里可能残留历史 publicKey(没 publicKeyId),用户想走老平台证书模式但被 pair violation 拦住。改为: - publicKeyId 非空 → 必须有 publicKey(报 WXPAY_CONFIG_MISSING_KEY key=publicKey) - publicKeyId 空 → publicKey 视为历史残留,不使用也不报错 移除 WXPAY_CONFIG_PAIR_VIOLATION 错误码。 - payment_config_providers.go: CreateProviderInstance 和 UpdateProviderInstance 新增 validateProviderConfig;Update 要先取旧记录决定 finalEnabled 和 merged config - wxpay.go: 去掉成对校验,换成"publicKeyId 非空才要求 publicKey" - wxpay_test.go: 更新用例反映新语义
This commit is contained in:
@@ -78,11 +78,14 @@ func NewWxpay(instanceID string, config map[string]string) (*Wxpay, error) {
|
||||
"actual": strconv.Itoa(len(config["apiV3Key"])),
|
||||
})
|
||||
}
|
||||
// publicKey + publicKeyId are a pair used by the new pubkey verifier.
|
||||
// If either is set, both must be set; otherwise fall back to legacy platform certificate mode.
|
||||
if (config["publicKey"] != "") != (config["publicKeyId"] != "") {
|
||||
return nil, infraerrors.BadRequest("WXPAY_CONFIG_PAIR_VIOLATION", "pair_violation").
|
||||
WithMetadata(map[string]string{"keys": "publicKey/publicKeyId"})
|
||||
// Pubkey verifier mode is opt-in via publicKeyId. If publicKeyId is set,
|
||||
// publicKey must also be set so the verifier can load it.
|
||||
// A leftover publicKey without publicKeyId is treated as unused legacy data,
|
||||
// not an error, so admins can switch back to platform-certificate mode without
|
||||
// having to manually clear the old publicKey field.
|
||||
if config["publicKeyId"] != "" && config["publicKey"] == "" {
|
||||
return nil, infraerrors.BadRequest("WXPAY_CONFIG_MISSING_KEY", "missing_required_key").
|
||||
WithMetadata(map[string]string{"key": "publicKey"})
|
||||
}
|
||||
return &Wxpay{instanceID: instanceID, config: config}, nil
|
||||
}
|
||||
|
||||
@@ -218,16 +218,16 @@ func TestNewWxpay(t *testing.T) {
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "publicKey without publicKeyId",
|
||||
config: withOverride(map[string]string{"publicKeyId": ""}),
|
||||
wantErr: true,
|
||||
errSubstr: "WXPAY_CONFIG_PAIR_VIOLATION",
|
||||
// Leftover publicKey from a former pubkey-mode setup is ignored; treated as legacy mode.
|
||||
name: "publicKey leftover without publicKeyId is allowed",
|
||||
config: withOverride(map[string]string{"publicKeyId": ""}),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "publicKeyId without publicKey",
|
||||
config: withOverride(map[string]string{"publicKey": ""}),
|
||||
wantErr: true,
|
||||
errSubstr: "WXPAY_CONFIG_PAIR_VIOLATION",
|
||||
errSubstr: "WXPAY_CONFIG_MISSING_KEY",
|
||||
},
|
||||
{
|
||||
name: "apiV3Key too short",
|
||||
|
||||
@@ -12,9 +12,22 @@ import (
|
||||
"github.com/Wei-Shaw/sub2api/ent/paymentorder"
|
||||
"github.com/Wei-Shaw/sub2api/ent/paymentproviderinstance"
|
||||
"github.com/Wei-Shaw/sub2api/internal/payment"
|
||||
"github.com/Wei-Shaw/sub2api/internal/payment/provider"
|
||||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||||
)
|
||||
|
||||
// validateProviderConfig runs the provider's constructor to surface config-level
|
||||
// errors at save time (e.g. wxpay missing certSerial), instead of only failing
|
||||
// when an order is created. Returns the structured ApplicationError from the
|
||||
// constructor so the frontend i18n layer can localize it.
|
||||
//
|
||||
// Only validates enabled instances — a disabled instance may be a half-filled
|
||||
// draft the admin will complete later.
|
||||
func (s *PaymentConfigService) validateProviderConfig(providerKey string, config map[string]string) error {
|
||||
_, err := provider.CreateProvider(providerKey, "_validate_", config)
|
||||
return err
|
||||
}
|
||||
|
||||
// --- Provider Instance CRUD ---
|
||||
|
||||
func (s *PaymentConfigService) ListProviderInstances(ctx context.Context) ([]*dbent.PaymentProviderInstance, error) {
|
||||
@@ -137,6 +150,11 @@ func (s *PaymentConfigService) CreateProviderInstance(ctx context.Context, req C
|
||||
if err := validateProviderRequest(req.ProviderKey, req.Name, typesStr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Enabled {
|
||||
if err := s.validateProviderConfig(req.ProviderKey, req.Config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
enc, err := s.encryptConfig(req.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -210,16 +228,42 @@ func (s *PaymentConfigService) UpdateProviderInstance(ctx context.Context, id in
|
||||
WithMetadata(map[string]string{"count": strconv.Itoa(count)})
|
||||
}
|
||||
}
|
||||
// Validate merged config when the instance will end up enabled.
|
||||
// This surfaces provider-level errors (e.g. wxpay missing certSerial) at save time,
|
||||
// so admins see them in the dialog instead of only when an order is created.
|
||||
inst, err := s.entClient.PaymentProviderInstance.Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load provider instance: %w", err)
|
||||
}
|
||||
finalEnabled := inst.Enabled
|
||||
if req.Enabled != nil {
|
||||
finalEnabled = *req.Enabled
|
||||
}
|
||||
var mergedConfig map[string]string
|
||||
if req.Config != nil {
|
||||
mergedConfig, err = s.mergeConfig(ctx, id, req.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if finalEnabled {
|
||||
configToValidate := mergedConfig
|
||||
if configToValidate == nil {
|
||||
configToValidate, err = s.decryptConfig(inst.Config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt existing config: %w", err)
|
||||
}
|
||||
}
|
||||
if err := s.validateProviderConfig(inst.ProviderKey, configToValidate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
u := s.entClient.PaymentProviderInstance.UpdateOneID(id)
|
||||
if req.Name != nil {
|
||||
u.SetName(*req.Name)
|
||||
}
|
||||
if req.Config != nil {
|
||||
merged, err := s.mergeConfig(ctx, id, req.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc, err := s.encryptConfig(merged)
|
||||
if mergedConfig != nil {
|
||||
enc, err := s.encryptConfig(mergedConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user