596 lines
21 KiB
Go
596 lines
21 KiB
Go
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
|
||
}
|