优化结构

This commit is contained in:
Yvan
2026-08-29 08:40:15 +08:00
parent b6d25c1a0f
commit 509df03d61
17 changed files with 234 additions and 40 deletions
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
bizpayment "kra/internal/biz/payment"
"net"
"sort"
"strconv"
"strings"
@@ -285,6 +286,44 @@ func validateCommunicationIntegrationConfig(kind, provider string, values map[st
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, "/") {
@@ -391,6 +430,34 @@ func integrationText(values map[string]any, key string) string {
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 != "" {
@@ -110,6 +110,26 @@ var integrationDefinitions = map[string][]IntegrationConfigDefinition{
{Key: "reconnect_interval", Label: "重连退避上限(秒)", Type: "number", Required: true, Description: "网络中断后自动重连的最大退避间隔。"},
},
},
{
Kind: IntegrationKindMQ, Provider: "kafka", Name: "Kafka", Description: "Apache Kafka 分布式事件队列",
Defaults: map[string]any{"brokers": []string{"127.0.0.1:9092"}, "client_id": "kra", "group_id": "kra", "username": "", "password": "", "tls": false, "tls_skip_verify": false, "start_offset": "earliest", "min_bytes": 1, "max_bytes": 10485760, "max_wait": 1, "connect_timeout": 10, "reconnect_interval": 5, "allow_auto_topic_creation": false},
Fields: []IntegrationConfigField{
{Key: "brokers", Label: "Broker 地址", Type: "string-list", Required: true, Placeholder: "127.0.0.1:9092", Description: "每行一个 host:port 地址。"},
{Key: "client_id", Label: "客户端 ID", Type: "text", Required: true, Placeholder: "kra"},
{Key: "group_id", Label: "消费组 ID", Type: "text", Required: true, Placeholder: "kra"},
{Key: "username", Label: "SASL 用户名", Type: "text"},
{Key: "password", Label: "SASL 密码", Type: "password", Secret: true},
{Key: "tls", Label: "启用 TLS", Type: "switch"},
{Key: "tls_skip_verify", Label: "跳过 TLS 证书校验", Type: "switch", Description: "仅用于受控测试环境。"},
{Key: "start_offset", Label: "初始消费位置", Type: "select", Required: true, Options: []IntegrationConfigOption{{Label: "earliest", Value: "earliest"}, {Label: "latest", Value: "latest"}}},
{Key: "min_bytes", Label: "最小拉取字节数", Type: "number", Required: true},
{Key: "max_bytes", Label: "最大拉取字节数", Type: "number", Required: true},
{Key: "max_wait", Label: "最大拉取等待(秒)", Type: "number", Required: true},
{Key: "connect_timeout", Label: "连接超时(秒)", Type: "number", Required: true},
{Key: "reconnect_interval", Label: "重连间隔(秒)", Type: "number", Required: true},
{Key: "allow_auto_topic_creation", Label: "允许自动创建 Topic", Type: "switch"},
},
},
{
Kind: IntegrationKindMQ, Provider: "rabbitmq", Name: "RabbitMQ", Description: "RabbitMQ AMQP 消息队列",
Defaults: map[string]any{"host": "127.0.0.1", "port": 5672, "username": "guest", "password": "guest", "vhost": "/", "exchange": "kra", "exchange_type": "topic", "queue": "kra", "routing_key": "#", "durable": true, "auto_delete": false, "prefetch_count": 10, "heartbeat": 10, "connect_timeout": 10, "reconnect_interval": 5, "tls": false},
+24 -9
View File
@@ -38,6 +38,7 @@ const (
PaymentModeExternal = "external"
PaymentModeInternal = "internal"
DemoBusinessType = "demo_subscription"
)
var SupportedPaymentProviders = []string{
@@ -537,6 +538,10 @@ func supportedPaymentProvider(provider string) bool {
return false
}
func IsDemoPaymentOrder(order *PaymentOrder) bool {
return order != nil && order.BusinessType == DemoBusinessType && strings.HasPrefix(strings.TrimSpace(order.TradeNo), "DEMO-PAY-")
}
func (uc *PaymentUsecase) Query(ctx context.Context, provider, tradeNo string) (*PaymentResult, error) {
if !validPaymentText(provider, 64) || !validPaymentText(tradeNo, 128) {
return nil, errors.New("查询支付参数不完整")
@@ -548,6 +553,9 @@ func (uc *PaymentUsecase) Query(ctx context.Context, provider, tradeNo string) (
if err != nil {
return nil, err
}
if IsDemoPaymentOrder(order) {
return paymentResultFromOrder(order), nil
}
if provider == PaymentInternal || order.PaymentMode == PaymentModeInternal {
return paymentResultFromOrder(order), nil
}
@@ -582,7 +590,14 @@ func (uc *PaymentUsecase) Refund(ctx context.Context, provider, tradeNo string,
if uc.orders == nil {
return nil, errors.New("支付订单仓储未接入")
}
return uc.refundWithOrder(ctx, provider, tradeNo, amount)
order, err := uc.orders.FindPaymentOrder(ctx, provider, tradeNo)
if err != nil {
return nil, err
}
if IsDemoPaymentOrder(order) {
return nil, errors.New("演示订单不执行真实退款")
}
return uc.refundWithOrder(ctx, order, amount)
}
// TestProvider exercises the configured provider without requiring a business
@@ -614,6 +629,9 @@ func (uc *PaymentUsecase) Fulfill(ctx context.Context, provider, tradeNo string)
if err != nil {
return nil, err
}
if IsDemoPaymentOrder(order) {
return nil, errors.New("演示订单不执行真实发货")
}
result := paymentResultFromOrder(order)
if result == nil {
return nil, ErrPaymentOrderNotFound
@@ -663,9 +681,6 @@ func (uc *PaymentUsecase) createWithOrder(ctx context.Context, req *PaymentReque
ConfirmationID: paymentConfirmationID(req.Provider, req.TradeNo),
RequestFingerprint: paymentOrderFingerprint(req, extra),
Extra: extra,
ClientIP: req.ClientIP,
UserAgent: req.UserAgent,
DeviceID: req.DeviceID,
}
order, created, err := uc.orders.CreatePaymentOrder(ctx, order)
if err != nil {
@@ -866,17 +881,17 @@ func (uc *PaymentUsecase) fulfillOrder(ctx context.Context, order *PaymentOrder,
return attachOrderResult(result, order), nil
}
func (uc *PaymentUsecase) refundWithOrder(ctx context.Context, provider, tradeNo string, amount int64) (*PaymentResult, error) {
current, err := uc.orders.FindPaymentOrder(ctx, provider, tradeNo)
if err != nil {
return nil, err
func (uc *PaymentUsecase) refundWithOrder(ctx context.Context, current *PaymentOrder, amount int64) (*PaymentResult, error) {
if current == nil {
return nil, ErrPaymentOrderNotFound
}
provider, tradeNo := current.Provider, current.TradeNo
handler := uc.fulfillments.Handler(current.BusinessType)
authorizer, ok := handler.(PaymentRefundAuthorizer)
if !ok {
return nil, fmt.Errorf("支付业务未实现退款授权: %s", current.BusinessType)
}
if err = authorizer.AuthorizeRefund(ctx, current, amount); err != nil {
if err := authorizer.AuthorizeRefund(ctx, current, amount); err != nil {
return nil, err
}
order, token, err := uc.orders.BeginPaymentRefund(ctx, provider, tradeNo, amount, 10*time.Minute)
-3
View File
@@ -88,9 +88,6 @@ type PaymentOrder struct {
RefundedAt *time.Time
FulfillmentLeaseUntil *time.Time
RefundLeaseUntil *time.Time
ClientIP string
UserAgent string
DeviceID string
}
type PaymentProviderUpdate struct {
+27
View File
@@ -157,6 +157,33 @@ func testBizPaymentOrder() *PaymentOrder {
return &PaymentOrder{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: PaymentStatusPending, FulfillmentStatus: FulfillmentStatusPending, RefundStatus: RefundStatusNone, ConfirmationID: "11111111-1111-1111-1111-111111111111"}
}
func TestDemoPaymentOperationsNeverReachExternalProviderOrFulfillment(t *testing.T) {
repo := &paymentRepoStub{query: successPaymentResult(), refund: successPaymentResult()}
order := testBizPaymentOrder()
order.TradeNo = "DEMO-PAY-01"
order.BusinessType = DemoBusinessType
order.PaymentStatus = PaymentStatusPaid
orders := &paymentOrderRepoStub{order: order}
registry := NewPaymentFulfillmentRegistry()
handler := &paymentFulfillmentStub{}
if err := registry.Register(handler); err != nil {
t.Fatal(err)
}
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
uc := NewPaymentUsecase(repo, orders, nil, nil, registry, logger)
result, err := uc.Query(context.Background(), order.Provider, order.TradeNo)
if err != nil || result == nil || repo.queryCalls != 0 {
t.Fatalf("demo query result=%#v calls=%d err=%v", result, repo.queryCalls, err)
}
if _, err = uc.Refund(context.Background(), order.Provider, order.TradeNo, 10); err == nil || repo.refundedReq != nil {
t.Fatalf("demo refund reached provider: req=%#v err=%v", repo.refundedReq, err)
}
if _, err = uc.Fulfill(context.Background(), order.Provider, order.TradeNo); err == nil || handler.calls != 0 {
t.Fatalf("demo fulfillment reached handler: calls=%d err=%v", handler.calls, err)
}
}
func successPaymentResult() *PaymentResult {
return &PaymentResult{
Provider: PaymentAlipay,