package main import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "io" "log" "net/http" "net/url" "os" "path/filepath" "sort" "strings" "sync" "time" ) const mockURL = "http://127.0.0.1:18081" const mockAppID = "local-acceptance-app" type mockKeys struct { MerchantPrivate string `json:"merchantPrivate"` PlatformPrivate string `json:"platformPrivate"` PlatformPublic string `json:"platformPublic"` } type mockOrder struct { Number string `json:"number"` Amount string `json:"amount"` Paid bool `json:"paid"` } type mockPayments struct { keys *mockKeys key *rsa.PrivateKey mu sync.Mutex orders map[string]mockOrder } func newRSAKey() (*rsa.PrivateKey, string, string) { key, err := rsa.GenerateKey(rand.Reader, 2048) must(err) private := string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) public, err := x509.MarshalPKIXPublicKey(&key.PublicKey) must(err) return key, private, string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: public})) } func loadMockKeys() (*mockKeys, error) { raw, err := os.ReadFile(filepath.Join(outputDir, "mock-payment-keys.json")) if err == nil { var keys mockKeys err = json.Unmarshal(raw, &keys) return &keys, err } if !os.IsNotExist(err) { return nil, err } _, merchant, _ := newRSAKey() _, private, public := newRSAKey() keys := &mockKeys{MerchantPrivate: merchant, PlatformPrivate: private, PlatformPublic: public} return keys, saveJSON("mock-payment-keys.json", keys) } func parsePrivate(value string) *rsa.PrivateKey { block, _ := pem.Decode([]byte(value)) check(block != nil, "invalid local test private key") key, err := x509.ParsePKCS1PrivateKey(block.Bytes) must(err) return key } func rsa2Sign(key *rsa.PrivateKey, raw []byte) string { digest := sha256.Sum256(raw) signature, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, digest[:]) must(err) return base64.StdEncoding.EncodeToString(signature) } func signingFields(values url.Values, includeSignType bool) string { keys := make([]string, 0, len(values)) for key := range values { if key != "sign" && (includeSignType || key != "sign_type") && values.Get(key) != "" { keys = append(keys, key) } } sort.Strings(keys) parts := make([]string, 0, len(keys)) for _, key := range keys { parts = append(parts, key+"="+values.Get(key)) } return strings.Join(parts, "&") } func serveMockPayments() error { keys, err := loadMockKeys() if err != nil { return err } m := &mockPayments{keys: keys, key: parsePrivate(keys.PlatformPrivate), orders: map[string]mockOrder{}} if raw, readErr := os.ReadFile(filepath.Join(outputDir, "mock-payment-orders.json")); readErr == nil { must(json.Unmarshal(raw, &m.orders)) } mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, "local-only-mock") }) mux.HandleFunc("/gateway", m.gateway) mux.HandleFunc("/confirm", m.confirm) log.Printf("Local-only signed payment mock listening at %s", mockURL) server := &http.Server{Addr: "127.0.0.1:18081", Handler: mux, ReadHeaderTimeout: 5 * time.Second} return server.ListenAndServe() } func (m *mockPayments) gateway(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } merchant := parsePrivate(m.keys.MerchantPrivate) signature, err := base64.StdEncoding.DecodeString(r.Form.Get("sign")) digest := sha256.Sum256([]byte(signingFields(r.Form, true))) if err != nil || rsa.VerifyPKCS1v15(&merchant.PublicKey, crypto.SHA256, digest[:], signature) != nil || r.Form.Get("app_id") != mockAppID { http.Error(w, "invalid local merchant signature", http.StatusForbidden) return } var content map[string]any if err = json.Unmarshal([]byte(r.Form.Get("biz_content")), &content); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } number, _ := content["out_trade_no"].(string) method := r.Form.Get("method") m.mu.Lock() defer m.mu.Unlock() var response map[string]any switch method { case "alipay.trade.precreate": amount, _ := content["total_amount"].(string) m.orders[number] = mockOrder{Number: number, Amount: amount} must(saveJSON("mock-payment-orders.json", m.orders)) response = map[string]any{ "code": "10000", "msg": "Success", "out_trade_no": number, "qr_code": mockURL + "/confirm?orderNo=" + url.QueryEscape(number), } case "alipay.trade.query": order, found := m.orders[number] if !found { http.Error(w, "unknown local test order", http.StatusNotFound) return } status := "WAIT_BUYER_PAY" if order.Paid { status = "TRADE_SUCCESS" } response = map[string]any{ "code": "10000", "msg": "Success", "trade_no": "LOCAL-" + number, "out_trade_no": number, "trade_status": status, "total_amount": order.Amount, "buyer_pay_amount": order.Amount, "receipt_amount": order.Amount, "trans_currency": "CNY", } default: http.Error(w, "method not implemented by local-only mock", http.StatusBadRequest) return } raw, err := json.Marshal(response) must(err) envelope := map[string]any{strings.ReplaceAll(method, ".", "_") + "_response": json.RawMessage(raw), "sign": rsa2Sign(m.key, raw)} w.Header().Set("Content-Type", "application/json") must(json.NewEncoder(w).Encode(envelope)) } func (m *mockPayments) confirm(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "local test confirmation requires POST", http.StatusMethodNotAllowed) return } must(r.ParseForm()) number := r.Form.Get("orderNo") m.mu.Lock() order, found := m.orders[number] if !found { m.mu.Unlock() http.Error(w, "unknown local test order", http.StatusNotFound) return } order.Paid = true m.orders[number] = order must(saveJSON("mock-payment-orders.json", m.orders)) m.mu.Unlock() fields := url.Values{ "app_id": {mockAppID}, "trade_no": {"LOCAL-" + number}, "out_trade_no": {number}, "trade_status": {"TRADE_SUCCESS"}, "total_amount": {order.Amount}, "sign_type": {"RSA2"}, } fields.Set("sign", rsa2Sign(m.key, []byte(signingFields(fields, false)))) client := &http.Client{Timeout: 15 * time.Second} response, err := client.PostForm(baseURL+"/payment/callback/alipay", fields) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } defer response.Body.Close() w.WriteHeader(response.StatusCode) _, _ = io.Copy(w, response.Body) } func configureMockPayment(admin *apiClient) error { keys, err := loadMockKeys() if err != nil { return err } response, err := http.Get(mockURL + "/health") if err != nil { return fmt.Errorf("start the local mock-payment command before verification: %w", err) } defer response.Body.Close() if response.StatusCode != http.StatusOK { return fmt.Errorf("local payment mock unavailable") } return admin.call("PUT", "/integration/configs/payment/alipay", map[string]any{ "enabled": true, "config": map[string]any{ "app_id": mockAppID, "private_key": keys.MerchantPrivate, "public_key": keys.PlatformPublic, "environment": "sandbox", "sign_type": "RSA2", "method": "alipay.trade.precreate", "gateway_url": mockURL + "/gateway", "notify_url": baseURL + "/payment/callback/alipay", }, }, nil) }