package payment import ( "bytes" "encoding/json" "fmt" "net/http" "strings" "kra/internal/biz" "github.com/go-pay/gopay" "github.com/go-pay/gopay/douyin" ) // The adapters in this package intentionally keep GoPay request/response // models below the data boundary. The business layer only sees PaymentResult. func callbackRequest(callback *biz.PaymentCallback) (*http.Request, error) { request, err := http.NewRequest(http.MethodPost, "http://payment-callback.local", bytes.NewReader(callback.Body)) if err != nil { return nil, err } for key, value := range callback.Headers { request.Header.Set(key, value) } return request, nil } func mergeGoPayExtras(dst gopay.BodyMap, src map[string]any, protected ...string) { protectedKeys := make(map[string]struct{}, len(protected)) for _, key := range protected { protectedKeys[key] = struct{}{} } for key, value := range src { if _, ok := protectedKeys[key]; ok { continue } dst[key] = value } } func mergeGoPayConfigExtras(dst gopay.BodyMap, config map[string]any, key string, protected ...string) { extra, ok := config[key].(map[string]any) if !ok || extra == nil { return } mergeGoPayExtras(dst, extra, protected...) } func anyConfig(c map[string]any, keys ...string) any { for _, key := range keys { if value, ok := c[key]; ok && fmt.Sprint(value) != "" { return value } } return nil } func firstAnyOr(values map[string]any, fallback string, keys ...string) string { if value := firstAny(values, keys...); value != "" { return value } return fallback } func mustJSON(value any) []byte { encoded, _ := json.Marshal(value) return encoded } func douyinError(rsp any) error { return fmt.Errorf("抖音支付请求失败: %v", rsp) } func qqOK(returnCode, resultCode string) bool { return strings.EqualFold(strings.TrimSpace(returnCode), "SUCCESS") && strings.EqualFold(strings.TrimSpace(resultCode), "SUCCESS") } // saobeiOK accepts both the documented numeric success code (01) and the // textual SUCCESS form used by some gateway test/proxy responses. Saobei's // communication and business codes are independent, so both must explicitly // report success. func saobeiOK(returnCode, resultCode string) bool { return saobeiSuccessCode(returnCode) && saobeiSuccessCode(resultCode) } func saobeiSuccessCode(code string) bool { code = strings.TrimSpace(code) return strings.EqualFold(code, "01") || strings.EqualFold(code, "SUCCESS") } func saobeiProcessingOK(returnCode, resultCode string) bool { return saobeiSuccessCode(returnCode) && strings.TrimSpace(resultCode) == "03" } func saobeiResultOK(returnCode, resultCode string) bool { return saobeiOK(returnCode, resultCode) || saobeiProcessingOK(returnCode, resultCode) } func lakalaResponseOK(returnCode, resultCode string, accepted ...string) bool { if !strings.EqualFold(strings.TrimSpace(returnCode), "SUCCESS") { return false } resultCode = strings.ToUpper(strings.TrimSpace(resultCode)) if resultCode == "" { return false } for _, code := range accepted { if resultCode == strings.ToUpper(strings.TrimSpace(code)) { return true } } return false } func lakalaCreateResponseOK(returnCode, resultCode string) bool { return lakalaResponseOK(returnCode, resultCode, "SUCCESS", "EXISTS") } func lakalaRetailCreateResponseOK(returnCode, resultCode string) bool { return lakalaResponseOK(returnCode, resultCode, "PAYING", "PAY_SUCCESS") } func lakalaQueryResponseOK(returnCode, resultCode string) bool { return lakalaResponseOK(returnCode, resultCode, "PAYING", "CREATE_FAIL", "CLOSED", "PAY_FAIL", "PAY_SUCCESS", "PARTIAL_REFUND", "FULL_REFUND") } func lakalaRefundResponseOK(returnCode, resultCode string) bool { return lakalaResponseOK(returnCode, resultCode, "WAITING", "CREATE_FAILED", "SUCCESS", "FAILED", "FINISHED", "CHANGE") } func rspMap(value any) map[string]string { encoded, _ := json.Marshal(value) var object map[string]any _ = json.Unmarshal(encoded, &object) result := make(map[string]string, len(object)) for key, val := range object { result[key] = fmt.Sprint(val) } return result } func normalizeDouyinState(state string) string { switch strings.ToUpper(strings.TrimSpace(state)) { case douyin.TradeStateSuccess: return "success" case douyin.TradeStatePayErr: return "failed" case douyin.TradeStateClosed: return "failed" default: return "pending" } } func normalizeRefundState(state string) string { switch strings.ToUpper(strings.TrimSpace(state)) { case douyin.RefundStatusSuccess, "REFUNDED": return "success" case douyin.RefundStatusClosed, douyin.RefundStatusAbnormal, "FAILED": return "failed" default: return "pending" } } func normalizeQQState(state string) string { switch strings.ToUpper(strings.TrimSpace(state)) { case "SUCCESS": return "success" case "REFUND", "CLOSED", "PAYERROR", "REVOKED", "NOPAY": return "failed" default: return "pending" } } func normalizeAllinState(state string) string { state = strings.ToUpper(strings.TrimSpace(state)) switch state { case "SUCCESS", "0000": return "success" case "FAIL", "FAILED": return "failed" } if len(state) == 4 && state >= "3000" && state <= "3999" { return "failed" } return "pending" } func normalizeLakalaState(state string) string { switch strings.ToUpper(strings.TrimSpace(state)) { case "SUCCESS", "PAID", "PAY_SUCCESS", "1": return "success" case "CLOSED", "FAILED", "FAIL", "CANCELLED", "CREATE_FAIL", "PAY_FAIL", "PARTIAL_REFUND", "FULL_REFUND": return "failed" default: return "pending" } } func normalizeLakalaRefundState(state string) string { switch strings.ToUpper(strings.TrimSpace(state)) { case "SUCCESS", "FINISHED": return "success" case "CREATE_FAILED", "FAILED", "CHANGE": return "failed" default: return "pending" } } func normalizeSaobeiState(state string) string { switch strings.ToUpper(strings.TrimSpace(state)) { case "SUCCESS": return "success" case "CLOSED", "PAYERROR", "REVOKED", "FAILED", "NOPAY": return "failed" default: return "pending" } }