kra-oa/internal/biz/integration_config.go

294 lines
10 KiB
Go

package biz
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"strings"
)
const IntegrationKindPayment = "payment"
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
}
type IntegrationConfigUsecase struct{ repo IntegrationConfigRepo }
func NewIntegrationConfigUsecase(repo IntegrationConfigRepo) *IntegrationConfigUsecase {
return &IntegrationConfigUsecase{repo: repo}
}
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 := map[string]any{}
if err := json.Unmarshal(config.Values, &values); err != nil {
return errors.New("集成配置必须是 JSON 对象")
}
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)
}
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 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)
}
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 {
if normalizeIntegrationPart(kind) != IntegrationKindPayment {
return nil
}
return validatePaymentIntegrationConfig(normalizeIntegrationPart(provider), values)
}
func validatePaymentIntegrationConfig(provider string, values map[string]any) error {
definition, ok := IntegrationDefinition(IntegrationKindPayment, provider)
if !ok {
return errors.New("不支持的支付渠道")
}
for _, field := range definition.Fields {
if field.Required && integrationText(values, field.Key) == "" {
return fmt.Errorf("%s 缺少配置字段 %s", provider, field.Key)
}
}
switch provider {
case 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 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 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 PaymentDouyin:
if integrationFirst(values, "platform_serial_no", "platform_cert_serial") == "" {
return fmt.Errorf("%s 缺少配置字段 platform_serial_no", provider)
}
case 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 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 PaymentChinaums, PaymentSFT, PaymentSuperPay, PaymentWechatGame, 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 != PaymentQQ && provider != PaymentDouyin && provider != 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 integrationFirst(values map[string]any, keys ...string) string {
for _, key := range keys {
if value := integrationText(values, key); value != "" {
return value
}
}
return ""
}
func integrationInt64(values map[string]any, key string, fallback int64) int64 {
value := integrationText(values, key)
if value == "" {
return fallback
}
parsed, err := strconv.ParseInt(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
}