kra-new/internal/biz/integration/integration_config.go

596 lines
21 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}