优化结构

This commit is contained in:
Yvan
2026-08-21 11:16:12 +08:00
parent 055f6811ca
commit e81a641953
13 changed files with 432 additions and 7 deletions
+295
View File
@@ -10,10 +10,12 @@ import (
"net/url"
"strconv"
"strings"
"time"
"kra/internal/biz"
datapayment "kra/internal/integration/payment"
"github.com/google/uuid"
"gorm.io/gorm"
)
@@ -82,6 +84,299 @@ func (r *paymentRepo) adapter(ctx context.Context, provider string) (datapayment
return adapter, values, err
}
func (r *paymentRepo) TestProvider(ctx context.Context, provider string) (*biz.PaymentTestResult, error) {
started := time.Now()
test := &biz.PaymentTestResult{Provider: provider, TradeNo: "", Passed: false, Stages: []biz.PaymentTestStage{}}
add := func(name, status, message, tradeNo string, since time.Time) {
test.Stages = append(test.Stages, biz.PaymentTestStage{Name: name, Status: status, Message: message, TradeNo: tradeNo, Duration: time.Since(since).Milliseconds()})
}
values, err := r.testRow(ctx, provider)
if err != nil {
add("config", "failed", err.Error(), "", started)
return test, err
}
test.Mode = strings.ToLower(strings.TrimSpace(text(values, "environment")))
configStart := time.Now()
if err = biz.ValidateIntegrationConfig(biz.IntegrationKindPayment, provider, values); err != nil {
add("config", "failed", err.Error(), "", configStart)
return test, err
}
add("config", "passed", "支付配置校验通过", "", configStart)
if err = validatePaymentTestSettings(provider, values); err != nil {
add("test_settings", "failed", err.Error(), "", time.Now())
return test, err
}
adapter, err := datapayment.New(provider)
if err != nil {
add("adapter", "failed", err.Error(), "", time.Now())
return test, err
}
req := paymentTestRequest(provider, values)
test.TradeNo = req.TradeNo
orders := &paymentOrderRepo{data: r.data}
extra, _ := json.Marshal(req.Extra)
localStart := time.Now()
order, _, err := orders.CreatePaymentOrder(ctx, &biz.PaymentOrder{
TradeNo: req.TradeNo, Provider: provider, BusinessType: req.BusinessType, BusinessID: req.BusinessID,
Subject: req.Subject, PaymentMode: biz.PaymentModeExternal, OriginalAmount: req.Amount, Amount: req.Amount,
Currency: req.Currency, PaymentStatus: biz.PaymentStatusInitialized, FulfillmentStatus: biz.FulfillmentStatusPending,
RefundStatus: biz.RefundStatusNone, ConfirmationID: uuid.NewString(), RequestFingerprint: paymentTestFingerprint(req), Extra: extra,
})
if err != nil {
add("local_order", "failed", err.Error(), req.TradeNo, localStart)
return test, err
}
add("local_order", "passed", "本地测试订单已创建", req.TradeNo, localStart)
createStart := time.Now()
created, err := adapter.Create(ctx, req, values)
if err != nil {
recordPaymentTestError(ctx, r.data, provider, req.TradeNo, err)
add("create", "failed", err.Error(), req.TradeNo, createStart)
return test, err
}
if created == nil {
err = errors.New("测试下单响应为空")
add("create", "failed", err.Error(), req.TradeNo, createStart)
return test, err
}
if err = validatePaymentTestResult(provider, req.TradeNo, created); err != nil {
recordPaymentTestError(ctx, r.data, provider, req.TradeNo, err)
add("create", "failed", err.Error(), req.TradeNo, createStart)
return test, err
}
if order, err = orders.RecordPaymentCreate(ctx, provider, req.TradeNo, paymentTestProviderUpdate(created)); err != nil {
add("local_order", "failed", "记录第三方下单结果失败: "+err.Error(), req.TradeNo, createStart)
return test, err
}
add("create", "passed", "测试订单已提交", req.TradeNo, createStart)
test.Result = created
queryID := strings.TrimSpace(created.QueryID)
if queryID == "" {
queryID = strings.TrimSpace(created.ProviderTradeNo)
}
if queryID == "" {
queryID = req.TradeNo
}
if provider == biz.PaymentApple {
queryID = strings.TrimSpace(text(values, "test_transaction_id"))
if queryID == "" {
err = errors.New("Apple 连通性测试需要配置 test_transaction_id(沙箱交易 ID)")
add("query", "failed", err.Error(), req.TradeNo, time.Now())
return test, err
}
}
queryStart := time.Now()
queried, queryErr := queryPaymentTest(ctx, adapter, queryID, values)
if queryErr != nil {
recordPaymentTestError(ctx, r.data, provider, req.TradeNo, queryErr)
add("query", "failed", queryErr.Error(), req.TradeNo, queryStart)
return test, queryErr
}
if err = validatePaymentTestResult(provider, req.TradeNo, queried); err != nil {
recordPaymentTestError(ctx, r.data, provider, req.TradeNo, err)
add("query", "failed", err.Error(), req.TradeNo, queryStart)
return test, err
}
if provider != biz.PaymentApple {
if order, err = orders.ApplyPaymentResult(ctx, provider, req.TradeNo, paymentTestProviderUpdate(queried)); err != nil {
add("local_order", "failed", "回写测试查单结果失败: "+err.Error(), req.TradeNo, queryStart)
return test, err
}
}
if queried == nil {
err = errors.New("测试查单响应为空")
add("query", "failed", err.Error(), req.TradeNo, queryStart)
return test, err
}
test.Result = queried
add("query", "passed", "测试订单查询成功,状态: "+queried.Status, req.TradeNo, queryStart)
if queried.Status != "success" || provider == biz.PaymentApple {
message := "订单尚未支付成功,已完成配置、下单和查单连通性测试;请在沙箱完成付款后重试"
if provider == biz.PaymentApple {
message = "Apple 退款由 App Store 管理,已完成配置、下单和交易查询测试"
}
add("refund", "skipped", message, req.TradeNo, time.Now())
test.Passed = true
return test, nil
}
refundStart := time.Now()
order, refundToken, beginErr := orders.BeginPaymentRefund(ctx, provider, req.TradeNo, req.Amount, time.Minute)
if beginErr != nil {
add("refund", "failed", beginErr.Error(), req.TradeNo, refundStart)
return test, beginErr
}
refund, refundErr := adapter.Refund(ctx, &biz.PaymentRefundRequest{Provider: provider, TradeNo: req.TradeNo, ProviderTradeNo: order.ProviderTradeNo, QueryID: order.QueryID, RefundNo: order.RefundNo, Amount: req.Amount, TotalAmount: req.Amount, Currency: req.Currency}, values)
if refundErr != nil {
recordPaymentTestError(ctx, r.data, provider, req.TradeNo, refundErr)
add("refund", "failed", refundErr.Error(), req.TradeNo, refundStart)
return test, refundErr
}
if refund == nil {
err = errors.New("测试退款响应为空")
add("refund", "failed", err.Error(), req.TradeNo, refundStart)
return test, err
}
if _, err = orders.CompletePaymentRefundRequest(ctx, provider, req.TradeNo, refundToken, true, ""); err != nil {
add("local_order", "failed", "回写测试退款结果失败: "+err.Error(), req.TradeNo, refundStart)
return test, err
}
test.Result = refund
add("refund", "passed", "测试退款申请已被渠道接受", req.TradeNo, refundStart)
test.Passed = true
test.FullFlow = true
return test, nil
}
func paymentTestFingerprint(req *biz.PaymentRequest) string {
raw, _ := json.Marshal(req)
hash := sha256.Sum256(raw)
return hex.EncodeToString(hash[:])
}
func paymentTestProviderUpdate(result *biz.PaymentResult) *biz.PaymentProviderUpdate {
if result == nil {
return nil
}
return &biz.PaymentProviderUpdate{
Status: result.Status, ProviderStatus: result.Status, ProviderTradeNo: result.ProviderTradeNo, QueryID: result.QueryID,
Amount: result.Amount, PayerPaidAmount: result.PayerPaidAmount, CashPaidAmount: result.CashPaidAmount,
PointPaidAmount: result.PointPaidAmount, DiscountAmount: result.DiscountAmount,
ProviderDiscountAmount: result.ProviderDiscountAmount, MerchantDiscountAmount: result.MerchantDiscountAmount,
SettlementAmount: result.SettlementAmount, Currency: result.Currency, PayerCurrency: result.PayerCurrency,
AmountBreakdownKnown: result.AmountBreakdownKnown, CreatePayload: result.Payload,
}
}
func validatePaymentTestResult(provider, tradeNo string, result *biz.PaymentResult) error {
if result == nil {
return errors.New("支付渠道响应为空")
}
if strings.TrimSpace(result.Provider) != provider {
return errors.New("支付渠道响应的 provider 不匹配")
}
if value := strings.TrimSpace(result.TradeNo); provider != biz.PaymentApple && value != "" && value != tradeNo {
return errors.New("支付渠道响应的商户订单号不匹配")
}
return nil
}
func queryPaymentTest(ctx context.Context, adapter datapayment.Adapter, queryID string, values map[string]any) (*biz.PaymentResult, error) {
var result *biz.PaymentResult
var err error
for attempt := 0; attempt < 3; attempt++ {
result, err = adapter.Query(ctx, queryID, values)
if err == nil {
return result, nil
}
if attempt == 2 {
break
}
timer := time.NewTimer(500 * time.Millisecond)
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
}
}
return nil, err
}
func recordPaymentTestError(ctx context.Context, data Provider, provider, tradeNo string, err error) {
if data == nil || data.DB() == nil || err == nil {
return
}
message := err.Error()
if len(message) > 512 {
message = message[:512]
}
_ = data.DB().WithContext(ctx).Model(&paymentOrderPO{}).Where("provider = ? AND trade_no = ?", provider, tradeNo).Update("last_error", message).Error
}
func (r *paymentRepo) testRow(ctx context.Context, provider string) (map[string]any, error) {
var row integrationConfigPO
if err := r.data.DB().WithContext(ctx).Where("kind = ? AND provider = ?", integrationKindPayment, provider).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, biz.ErrPaymentProviderNotFound
}
return nil, err
}
values := map[string]any{}
if err := json.Unmarshal([]byte(row.Config), &values); err != nil {
return nil, fmt.Errorf("支付配置格式错误: %w", err)
}
return values, nil
}
func paymentTestRequest(provider string, values map[string]any) *biz.PaymentRequest {
tradeNo := "kra-test-" + time.Now().UTC().Format("20060102150405.000000000")
amount := configuredInt64(values, "test_amount", 1)
if amount <= 0 {
amount = 1
}
req := &biz.PaymentRequest{Provider: provider, TradeNo: strings.ReplaceAll(tradeNo, ".", ""), Subject: "Kra 支付渠道连通性测试", Amount: amount, Currency: strings.ToUpper(firstAny(values, "test_currency", "currency", "fee_type")), NotifyURL: text(values, "notify_url"), ReturnURL: text(values, "return_url"), BusinessType: "system_payment_test", BusinessID: uuid.NewString(), Extra: map[string]any{}}
if req.Currency == "" {
req.Currency = "CNY"
}
if provider == biz.PaymentApple {
req.TradeNo = uuid.NewString()
req.Extra["product_id"] = firstAny(values, "product_id", "test_product_id")
}
if raw := strings.TrimSpace(text(values, "test_extra")); raw != "" {
var extra map[string]any
if json.Unmarshal([]byte(raw), &extra) == nil {
for key, value := range extra {
req.Extra[key] = value
}
}
}
for _, key := range []string{"trade_type", "method", "pay_type", "channel", "openid", "open_id", "auth_code", "authcode", "barcode"} {
if value := text(values, key); value != "" {
req.Extra[key] = value
}
}
return req
}
func validatePaymentTestSettings(provider string, values map[string]any) error {
if !testModeEnabled(values) {
return errors.New("请先打开 test_mode(允许执行渠道测试)")
}
if raw := strings.TrimSpace(text(values, "test_extra")); raw != "" {
var extra map[string]any
if err := json.Unmarshal([]byte(raw), &extra); err != nil {
return fmt.Errorf("test_extra 必须是 JSON 对象: %w", err)
}
}
if provider == biz.PaymentApple && strings.TrimSpace(text(values, "test_transaction_id")) == "" {
return errors.New("Apple 测试需要 test_transaction_id(沙箱交易 ID)")
}
if provider == biz.PaymentApple && strings.TrimSpace(firstAny(values, "test_product_id", "product_id")) == "" {
return errors.New("Apple 测试需要 test_product_id(沙箱商品 ID)")
}
return nil
}
func testModeEnabled(values map[string]any) bool {
value, exists := values["test_mode"]
if !exists {
return false
}
switch typed := value.(type) {
case bool:
return typed
case string:
return strings.EqualFold(strings.TrimSpace(typed), "true") || typed == "1"
case float64:
return typed == 1
default:
return false
}
}
func (r *paymentRepo) Create(ctx context.Context, req *biz.PaymentRequest) (*biz.PaymentResult, error) {
if req == nil {
return nil, errors.New("支付下单请求为空")