package integration import ( "context" "encoding/json" "errors" "fmt" bizpayment "kra/internal/biz/payment" "net" "sort" "strconv" "strings" "time" ) const ( IntegrationKindPayment = "payment" IntegrationKindMQ = "mq" IntegrationKindWebSocket = "websocket" ) type IntegrationConfig struct { Kind string Provider string Enabled bool Values json.RawMessage } type IntegrationConfigField struct { Key string `json:"key"` Label string `json:"label"` Type string `json:"type"` Required bool `json:"required"` Secret bool `json:"secret"` Placeholder string `json:"placeholder,omitempty"` Description string `json:"description,omitempty"` Options []IntegrationConfigOption `json:"options,omitempty"` } type IntegrationConfigOption struct { Label string `json:"label"` Value any `json:"value"` } type IntegrationConfigDefinition struct { Kind string `json:"kind"` Provider string `json:"provider"` Name string `json:"name"` Description string `json:"description,omitempty"` Fields []IntegrationConfigField `json:"fields"` Defaults map[string]any `json:"defaults"` } type IntegrationConfigRepo interface { ListIntegrationConfigs(context.Context, string) ([]*IntegrationConfig, error) FindIntegrationConfig(context.Context, string, string) (*IntegrationConfig, error) SaveIntegrationConfig(context.Context, *IntegrationConfig) error DeleteIntegrationConfig(context.Context, string, string) error } // ErrPaymentConfigNotFound marks an absent payment integration row without // exposing the storage driver's not-found error to the payment data module. var ErrPaymentConfigNotFound = errors.New("支付渠道配置不存在") // PaymentConfig is the storage-neutral, unmasked snapshot used by payment // persistence. It deliberately contains no ORM or table metadata. type PaymentConfig struct { Enabled bool Values json.RawMessage } // PaymentConfigReader is the narrow inversion seam between the payment and // integration data modules. The integration module owns its ConfigPO. type PaymentConfigReader interface { ReadPaymentConfig(context.Context, string) (*PaymentConfig, error) } type IntegrationConnectionTester interface { TestIntegration(context.Context, *IntegrationConfig) error } type ConnectionTestError struct { Provider string Err error } func (e *ConnectionTestError) Error() string { if e == nil || strings.TrimSpace(e.Provider) == "" { return "连接测试失败" } return strings.TrimSpace(e.Provider) + " 连接测试失败" } func (e *ConnectionTestError) Unwrap() error { if e == nil { return nil } return e.Err } type IntegrationConfigUsecase struct { repo IntegrationConfigRepo tester IntegrationConnectionTester } func NewIntegrationConfigUsecase(repo IntegrationConfigRepo, tester IntegrationConnectionTester) *IntegrationConfigUsecase { return &IntegrationConfigUsecase{repo: repo, tester: tester} } func (uc *IntegrationConfigUsecase) List(ctx context.Context, kind string) ([]*IntegrationConfig, error) { kind = normalizeIntegrationPart(kind) if kind == "" { return nil, errors.New("集成配置 kind 不能为空") } return uc.repo.ListIntegrationConfigs(ctx, kind) } func (uc *IntegrationConfigUsecase) Find(ctx context.Context, kind, provider string) (*IntegrationConfig, error) { kind, provider = normalizeIntegrationPart(kind), normalizeIntegrationPart(provider) if kind == "" || provider == "" { return nil, errors.New("集成配置 kind 和 provider 不能为空") } return uc.repo.FindIntegrationConfig(ctx, kind, provider) } func (uc *IntegrationConfigUsecase) Save(ctx context.Context, config *IntegrationConfig) error { if config == nil { return errors.New("集成配置请求为空") } config.Kind = normalizeIntegrationPart(config.Kind) config.Provider = normalizeIntegrationPart(config.Provider) if config.Kind == "" || config.Provider == "" || len(config.Kind) > 32 || len(config.Provider) > 64 { return errors.New("集成配置 kind 或 provider 无效") } if !json.Valid(config.Values) { return errors.New("集成配置必须是合法 JSON") } values, err := decodeIntegrationObject(config.Values) if err != nil { return err } if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok { values = MergeIntegrationDefaults(definition.Defaults, values) if config.Enabled { if err := ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil { return err } } config.Values, _ = json.Marshal(values) } return uc.repo.SaveIntegrationConfig(ctx, config) } // Test validates and probes a candidate configuration without persisting it. // The adapter may resolve masked secret values from the active runtime store. func (uc *IntegrationConfigUsecase) Test(ctx context.Context, config *IntegrationConfig) error { if config == nil { return errors.New("集成配置请求为空") } config.Kind = normalizeIntegrationPart(config.Kind) config.Provider = normalizeIntegrationPart(config.Provider) if config.Kind != IntegrationKindMQ && config.Kind != IntegrationKindWebSocket { return errors.New("仅支持测试消息队列和 WebSocket 集成") } if config.Kind == "" || config.Provider == "" || len(config.Kind) > 32 || len(config.Provider) > 64 { return errors.New("集成配置 kind 或 provider 无效") } if !json.Valid(config.Values) { return errors.New("集成配置必须是合法 JSON") } values, err := decodeIntegrationObject(config.Values) if err != nil { return err } if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok { values = MergeIntegrationDefaults(definition.Defaults, values) } if err := ValidateIntegrationConfig(config.Kind, config.Provider, values); err != nil { return err } encoded, _ := json.Marshal(values) config.Enabled = true config.Values = encoded if uc.tester == nil { return errors.New("集成连接测试器未初始化") } if err := uc.tester.TestIntegration(ctx, config); err != nil { provider := config.Provider if definition, ok := IntegrationDefinition(config.Kind, config.Provider); ok && strings.TrimSpace(definition.Name) != "" { provider = definition.Name } return &ConnectionTestError{Provider: provider, Err: err} } return nil } func (uc *IntegrationConfigUsecase) Delete(ctx context.Context, kind, provider string) error { kind, provider = normalizeIntegrationPart(kind), normalizeIntegrationPart(provider) if kind == "" || provider == "" { return errors.New("集成配置 kind 和 provider 不能为空") } return uc.repo.DeleteIntegrationConfig(ctx, kind, provider) } func normalizeIntegrationPart(value string) string { return strings.ToLower(strings.TrimSpace(value)) } func decodeIntegrationObject(raw json.RawMessage) (map[string]any, error) { values := map[string]any{} if err := json.Unmarshal(raw, &values); err != nil || values == nil { return nil, errors.New("集成配置必须是 JSON 对象") } return values, nil } func IntegrationDefinitions(kind string) []IntegrationConfigDefinition { kind = normalizeIntegrationPart(kind) definitions := integrationDefinitions[kind] out := append([]IntegrationConfigDefinition(nil), definitions...) sort.Slice(out, func(i, j int) bool { return out[i].Provider < out[j].Provider }) return out } func IntegrationDefinition(kind, provider string) (IntegrationConfigDefinition, bool) { for _, definition := range integrationDefinitions[normalizeIntegrationPart(kind)] { if definition.Provider == normalizeIntegrationPart(provider) { return definition, true } } return IntegrationConfigDefinition{}, false } func DefaultIntegrationConfig(kind, provider string) map[string]any { definition, ok := IntegrationDefinition(kind, provider) if !ok { return map[string]any{} } return MergeIntegrationDefaults(definition.Defaults, nil) } // MergeIntegrationDefaults returns a new map with stored values overriding defaults. func MergeIntegrationDefaults(defaults, values map[string]any) map[string]any { out := make(map[string]any, len(defaults)+len(values)) for key, value := range defaults { out[key] = value } for key, value := range values { out[key] = value } return out } func ValidateIntegrationConfig(kind, provider string, values map[string]any) error { kind = normalizeIntegrationPart(kind) provider = normalizeIntegrationPart(provider) switch kind { case IntegrationKindPayment: return validatePaymentIntegrationConfig(provider, values) case IntegrationKindMQ, IntegrationKindWebSocket: return validateCommunicationIntegrationConfig(kind, provider, values) default: return nil } } func validateCommunicationIntegrationConfig(kind, provider string, values map[string]any) error { definition, ok := IntegrationDefinition(kind, provider) if !ok { return errors.New("不支持的通信集成") } for _, field := range definition.Fields { value := integrationText(values, field.Key) if field.Required && value == "" { return fmt.Errorf("%s 缺少配置字段 %s", provider, field.Key) } } switch kind + "/" + provider { case IntegrationKindMQ + "/emqx": broker := strings.ToLower(integrationText(values, "broker")) if !strings.HasPrefix(broker, "tcp://") && !strings.HasPrefix(broker, "ssl://") && !strings.HasPrefix(broker, "ws://") && !strings.HasPrefix(broker, "wss://") && !strings.HasPrefix(broker, "mqtt://") { return errors.New("emqx broker 必须使用 tcp、ssl、ws、wss 或 mqtt 协议") } if keepAlive := integrationInt64(values, "keep_alive", 0); keepAlive <= 0 { return errors.New("emqx keep_alive 必须大于 0") } if timeout := integrationInt64(values, "connect_timeout", 0); timeout <= 0 { return errors.New("emqx connect_timeout 必须大于 0") } if interval := integrationInt64(values, "reconnect_interval", 0); interval <= 0 { return errors.New("emqx reconnect_interval 必须大于 0") } case IntegrationKindMQ + "/rabbitmq": port := integrationInt64(values, "port", 0) if port < 1 || port > 65535 { return errors.New("rabbitmq port 必须在 1-65535 之间") } exchangeType := strings.ToLower(integrationText(values, "exchange_type")) if exchangeType != "direct" && exchangeType != "fanout" && exchangeType != "topic" { return errors.New("rabbitmq exchange_type 必须是 direct、fanout 或 topic") } if integrationInt64(values, "prefetch_count", -1) < 0 { return errors.New("rabbitmq prefetch_count 不能小于 0") } if integrationInt64(values, "heartbeat", -1) < 0 { return errors.New("rabbitmq heartbeat 不能小于 0") } if integrationInt64(values, "connect_timeout", 0) <= 0 { return errors.New("rabbitmq connect_timeout 必须大于 0") } if integrationInt64(values, "reconnect_interval", 0) <= 0 { return errors.New("rabbitmq reconnect_interval 必须大于 0") } case IntegrationKindMQ + "/kafka": brokers := integrationStrings(values, "brokers") if len(brokers) == 0 { return errors.New("kafka brokers 不能为空") } for _, broker := range brokers { host, portText, err := net.SplitHostPort(broker) port, parseErr := strconv.Atoi(portText) if err != nil || parseErr != nil || strings.TrimSpace(host) == "" || port < 1 || port > 65535 { return fmt.Errorf("kafka broker %q 必须是有效的 host:port 地址", broker) } } username := integrationText(values, "username") password := integrationText(values, "password") if (username == "") != (password == "") { return errors.New("kafka username 和 password 必须同时配置") } startOffset := strings.ToLower(integrationText(values, "start_offset")) if startOffset != "earliest" && startOffset != "latest" { return errors.New("kafka start_offset 必须是 earliest 或 latest") } minBytes := integrationInt64(values, "min_bytes", 0) maxBytes := integrationInt64(values, "max_bytes", 0) if minBytes <= 0 || maxBytes < minBytes { return errors.New("kafka max_bytes 必须大于等于 min_bytes,且二者必须大于 0") } if integrationInt64(values, "max_wait", 0) <= 0 { return errors.New("kafka max_wait 必须大于 0") } if integrationInt64(values, "connect_timeout", 0) <= 0 { return errors.New("kafka connect_timeout 必须大于 0") } if integrationInt64(values, "reconnect_interval", 0) <= 0 { return errors.New("kafka reconnect_interval 必须大于 0") } if integrationBool(values, "tls_skip_verify") && !integrationBool(values, "tls") { return errors.New("kafka tls_skip_verify 仅能在启用 TLS 时使用") } case IntegrationKindWebSocket + "/melody": path := integrationText(values, "path") if !strings.HasPrefix(path, "/") { return errors.New("websocket path 必须以 / 开头") } for _, key := range []string{"write_wait", "pong_wait", "ping_period"} { value := integrationText(values, key) if value == "" { continue } duration, err := time.ParseDuration(value) if err != nil || duration <= 0 { return fmt.Errorf("websocket %s 必须是大于 0 的时长", key) } } if integrationInt64(values, "max_message_size", -1) < 0 || integrationInt64(values, "message_buffer_size", -1) < 0 { return errors.New("websocket 消息大小和缓冲区不能小于 0") } } return nil } func validatePaymentIntegrationConfig(provider string, values map[string]any) error { definition, ok := IntegrationDefinition(IntegrationKindPayment, provider) if !ok { return errors.New("不支持的支付渠道") } for _, field := range definition.Fields { value := integrationText(values, field.Key) if provider == bizpayment.PaymentDouyin && field.Key == "platform_serial_no" { value = integrationFirst(values, "platform_serial_no", "platform_cert_serial") } if field.Required && value == "" { return fmt.Errorf("%s 缺少配置字段 %s", provider, field.Key) } } switch provider { case bizpayment.PaymentAlipayV3: for _, group := range []struct { label string keys []string }{ {"private_key", []string{"private_key", "private_key_content", "private_key_path"}}, {"app_cert", []string{"app_cert", "app_cert_content", "app_cert_path", "app_public_cert", "app_public_cert_content", "app_public_cert_path"}}, {"root_cert", []string{"root_cert", "root_cert_content", "root_cert_path", "alipay_root_cert", "alipay_root_cert_content", "alipay_root_cert_path"}}, {"public_cert", []string{"public_cert", "public_cert_content", "public_cert_path", "alipay_public_cert", "alipay_public_cert_content", "alipay_public_cert_path"}}, } { if integrationFirst(values, group.keys...) == "" { return fmt.Errorf("%s 缺少配置字段 %s", provider, group.label) } } case bizpayment.PaymentWechatV2: if integrationFirst(values, "client_cert", "cert_pem", "apiclient_cert") == "" || integrationFirst(values, "client_key", "key_pem", "apiclient_key") == "" { return fmt.Errorf("%s 退款要求同时配置 client_cert 和 client_key", provider) } case bizpayment.PaymentApple: if integrationInt64(values, "price_divisor", 0) <= 0 { if _, exists := values["price_divisors"].(map[string]any); !exists { return fmt.Errorf("%s 缺少配置字段 price_divisor 或 price_divisors", provider) } } case bizpayment.PaymentDouyin: if integrationFirst(values, "platform_serial_no", "platform_cert_serial") == "" { return fmt.Errorf("%s 缺少配置字段 platform_serial_no", provider) } case bizpayment.PaymentQQ: if integrationFirst(values, "mch_id", "merchant_id") == "" { return fmt.Errorf("%s 缺少配置字段 mch_id", provider) } if !hasQQIntegrationCertificate(values) { return fmt.Errorf("%s 退款要求配置 cert_file + key_file、pkcs12_file 或对应的 *_content", provider) } signType := strings.ToUpper(integrationText(values, "sign_type")) if signType != "" && signType != "MD5" && signType != "HMAC-SHA256" { return fmt.Errorf("%s sign_type 必须是 MD5 或 HMAC-SHA256", provider) } case bizpayment.PaymentAllinPay: orderType := strings.ToLower(integrationFirst(values, "query_order_type", "order_type")) if orderType != "" && orderType != "reqsn" && orderType != "trxid" { return fmt.Errorf("%s query_order_type 必须是 reqsn 或 trxid", provider) } case bizpayment.PaymentChinaums, bizpayment.PaymentSFT, bizpayment.PaymentSuperPay, bizpayment.PaymentWechatGame, bizpayment.PaymentDouyinGame: if integrationFirst(values, "app_key", "merchant_key", "signing_secret", "token") == "" { return fmt.Errorf("%s 缺少签名密钥", provider) } if !validIntegrationScale(integrationInt64(values, "query_amount_scale", 0)) { return fmt.Errorf("%s query_amount_scale 必须是正的 10 的幂", provider) } } if environment := strings.ToLower(integrationText(values, "environment")); environment != "" { allowedSandbox := provider != bizpayment.PaymentQQ && provider != bizpayment.PaymentDouyin && provider != bizpayment.PaymentLakala if environment != "production" && environment != "prod" && (!allowedSandbox || environment != "sandbox") { return fmt.Errorf("%s environment 配置无效", provider) } } return validateIntegrationCallbackAck(values) } func integrationText(values map[string]any, key string) string { value, exists := values[key] if !exists || value == nil { return "" } return strings.TrimSpace(fmt.Sprint(value)) } func integrationStrings(values map[string]any, key string) []string { switch items := values[key].(type) { case []string: result := make([]string, 0, len(items)) for _, item := range items { if item = strings.TrimSpace(item); item != "" { result = append(result, item) } } return result case []any: result := make([]string, 0, len(items)) for _, item := range items { if value := strings.TrimSpace(fmt.Sprint(item)); value != "" { result = append(result, value) } } return result default: return nil } } func integrationBool(values map[string]any, key string) bool { value, _ := values[key].(bool) return value } func integrationFirst(values map[string]any, keys ...string) string { for _, key := range keys { if value := integrationText(values, key); value != "" { return value } } return "" } func IsIntegrationSecretKey(key string) bool { normalized := strings.ToLower(strings.ReplaceAll(key, "-", "_")) if strings.Contains(normalized, "secret") || strings.Contains(normalized, "password") || strings.Contains(normalized, "private") || strings.Contains(normalized, "credential") || strings.Contains(normalized, "token") { return true } for _, item := range []string{"key_pem", "api_key", "mch_key", "client_key", "certificate", "cert", "cert_pem", "p12", "pkcs12", "public_key", "platform_cert", "root_cert"} { if normalized == item || strings.HasSuffix(normalized, "_"+item) { return true } } return false } func integrationInt64(values map[string]any, key string, fallback int64) int64 { value, exists := values[key] if !exists || value == nil { return fallback } switch typed := value.(type) { case int: return int64(typed) case int8: return int64(typed) case int16: return int64(typed) case int32: return int64(typed) case int64: return typed case uint: return parseIntegrationInt64(strconv.FormatUint(uint64(typed), 10), fallback) case uint8: return int64(typed) case uint16: return int64(typed) case uint32: return int64(typed) case uint64: return parseIntegrationInt64(strconv.FormatUint(typed, 10), fallback) case float32: return parseIntegrationInt64(strconv.FormatFloat(float64(typed), 'f', -1, 32), fallback) case float64: return parseIntegrationInt64(strconv.FormatFloat(typed, 'f', -1, 64), fallback) case json.Number: if parsed, err := typed.Int64(); err == nil { return parsed } if parsed, err := typed.Float64(); err == nil { return parseIntegrationInt64(strconv.FormatFloat(parsed, 'f', -1, 64), fallback) } return fallback case string: return parseIntegrationInt64(typed, fallback) default: return parseIntegrationInt64(fmt.Sprint(typed), fallback) } } func parseIntegrationInt64(value string, fallback int64) int64 { parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64) if err != nil { return fallback } return parsed } func hasQQIntegrationCertificate(values map[string]any) bool { if integrationFirst(values, "pkcs12_file", "p12_file", "pkcs12_content") != "" { return true } if integrationFirst(values, "cert_file", "cert_path") != "" && integrationFirst(values, "key_file", "key_path") != "" { return true } return integrationFirst(values, "cert_content") != "" && integrationFirst(values, "key_content") != "" } func validIntegrationScale(scale int64) bool { if scale <= 0 { return false } for scale%10 == 0 { scale /= 10 } return scale == 1 } func validateIntegrationCallbackAck(values map[string]any) error { for _, prefix := range []string{"callback_success_", "callback_failure_"} { if value := integrationText(values, prefix+"status"); value != "" { status, err := strconv.Atoi(value) if err != nil || status < 200 || status > 599 { return fmt.Errorf("%sstatus 必须是 200-599", prefix) } } if contentType := integrationText(values, prefix+"content_type"); strings.ContainsAny(contentType, "\r\n") || len(contentType) > 200 { return fmt.Errorf("%scontent_type 非法", prefix) } } return nil }