package httpapi import ( "context" "encoding/json" "errors" "io" "mime" "net/http" "net/url" "regexp" "strings" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/payments" ) type PaymentService interface { Options() payments.Options Create(context.Context, string, string, string, int64) (payments.Order, error) Get(context.Context, string, string) (payments.Order, error) List(context.Context, string) ([]payments.Order, error) Notify(context.Context, *http.Request) error } type paymentHandler struct { authorizer *PlatformAuthorizer service PaymentService publicOrigin string } func NewPaymentHandler(authorizer *PlatformAuthorizer, service PaymentService, publicOrigin string) http.Handler { return &paymentHandler{authorizer: authorizer, service: service, publicOrigin: publicOrigin} } var paymentIdentifier = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,64}$`) func (h *paymentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") if r.URL.Path == "/api/payments/wechat/refund-notify" { h.refundNotify(w, r) return } if strings.HasPrefix(r.URL.Path, "/api/payments/admin/") { h.adminPayments(w, r) return } if r.URL.Path == "/api/payments/wechat/notify" { if !allow(w, r, http.MethodPost) { return } if h.service == nil { writeJSON(w, 503, map[string]string{"code": "FAIL", "message": "支付服务未启用"}) return } r.Body = http.MaxBytesReader(w, r.Body, 64<<10) if err := h.service.Notify(r.Context(), r); err != nil { status := http.StatusInternalServerError if errors.Is(err, payments.ErrVerification) { status = http.StatusBadRequest } // Acknowledge only after durable settlement; never leak decrypted data. writeJSON(w, status, map[string]string{"code": "FAIL", "message": "支付通知处理失败"}) return } w.WriteHeader(http.StatusNoContent) return } optionsPath := r.URL.Path == "/api/payments/options" ordersPath := r.URL.Path == "/api/payments/orders" orderID := strings.TrimPrefix(r.URL.Path, "/api/payments/orders/") itemPath := strings.HasPrefix(r.URL.Path, "/api/payments/orders/") && len(orderID) <= 32 && paymentIdentifier.MatchString(orderID) if !optionsPath && !ordersPath && !itemPath { http.NotFound(w, r) return } if r.Method != http.MethodGet && !(ordersPath && r.Method == http.MethodPost) { w.WriteHeader(http.StatusMethodNotAllowed) return } session, err := h.authorizer.Authorize(r, PlatformApp) if err != nil { writePaymentError(w, err) return } canRecharge := session.User.OrganizationID != "" && (session.User.Role == "organization_admin" || session.User.Role == "super_admin") if optionsPath { options := payments.Options{PresetAmountsFen: []int64{}} if h.service != nil { options = h.service.Options() } options.CanRecharge = options.Enabled && canRecharge writeJSON(w, http.StatusOK, options) return } if !canRecharge { writeAPIError(w, http.StatusForbidden, "需要所属组织的管理员权限。") return } if h.service == nil { writePaymentError(w, payments.ErrUnavailable) return } org := session.User.OrganizationID if itemPath { order, err := h.service.Get(r.Context(), org, orderID) if err != nil { writePaymentError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"order": publicPaymentOrder(order)}) return } if r.Method == http.MethodGet { orders, err := h.service.List(r.Context(), org) if err != nil { writePaymentError(w, err) return } if orders == nil { orders = []payments.Order{} } for i := range orders { orders[i] = publicPaymentOrder(orders[i]) } writeJSON(w, http.StatusOK, map[string]any{"orders": orders}) return } if !h.sameOrigin(r) { writeAPIError(w, http.StatusForbidden, "请从本站发起充值。") return } mediaType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err != nil || mediaType != "application/json" { writeAPIError(w, http.StatusUnsupportedMediaType, "请使用 JSON 请求。") return } var body struct { AmountFen int64 `json:"amountFen"` RequestKey string `json:"requestKey"` } r.Body = http.MaxBytesReader(w, r.Body, 4096) decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&body); err != nil { writeAPIError(w, 400, "充值参数无效,金额必须是整数分。") return } if err := decoder.Decode(&struct{}{}); err != io.EOF || !paymentIdentifier.MatchString(body.RequestKey) { writeAPIError(w, 400, "充值请求标识无效。") return } order, err := h.service.Create(r.Context(), org, session.User.ID, body.RequestKey, body.AmountFen) if err != nil { writePaymentError(w, err) return } writeJSON(w, http.StatusOK, map[string]any{"order": publicPaymentOrder(order)}) } func publicPaymentOrder(order payments.Order) payments.Order { if order.Status != payments.StatusPending { order.CodeURL = "" } return order } func (h *paymentHandler) sameOrigin(r *http.Request) bool { if site := r.Header.Get("Sec-Fetch-Site"); site != "" && site != "same-origin" && site != "none" { return false } origin := r.Header.Get("Origin") if origin == "" { return true } actual, err := url.Parse(origin) if err != nil || actual.User != nil || actual.RawQuery != "" || actual.Fragment != "" || actual.Path != "" { return false } expected, err := url.Parse(h.publicOrigin) return err == nil && expected.Host != "" && actual.Scheme == expected.Scheme && strings.EqualFold(actual.Host, expected.Host) } func writePaymentError(w http.ResponseWriter, err error) { status, message := http.StatusInternalServerError, "支付服务暂时不可用,请稍后重试。" var authErr *PlatformAuthError switch { case errors.As(err, &authErr): status, message = authErr.Status, authErr.Message case errors.Is(err, payments.ErrNotFound): status, message = 404, "充值订单不存在。" case errors.Is(err, payments.ErrConflict): status, message = 409, "充值请求与已有订单不一致,请刷新后重试。" case errors.Is(err, payments.ErrInvalidAmount): status, message = 400, "充值金额不在允许范围内。" case errors.Is(err, payments.ErrUnavailable): status, message = 503, "微信支付尚未启用。" } writeAPIError(w, status, message) }