| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687 |
- package handler
- import (
- "fmt"
- "strconv"
- "strings"
- "time"
- dbent "github.com/Wei-Shaw/sub2api/ent"
- "github.com/Wei-Shaw/sub2api/internal/payment"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/Wei-Shaw/sub2api/internal/pkg/response"
- middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
- "github.com/Wei-Shaw/sub2api/internal/service"
- "github.com/gin-gonic/gin"
- )
- // PaymentHandler handles user-facing payment requests.
- type PaymentHandler struct {
- paymentService *service.PaymentService
- configService *service.PaymentConfigService
- }
- // NewPaymentHandler creates a new PaymentHandler.
- func NewPaymentHandler(paymentService *service.PaymentService, configService *service.PaymentConfigService) *PaymentHandler {
- return &PaymentHandler{
- paymentService: paymentService,
- configService: configService,
- }
- }
- // GetPaymentConfig returns the payment system configuration.
- // GET /api/v1/payment/config
- func (h *PaymentHandler) GetPaymentConfig(c *gin.Context) {
- cfg, err := h.configService.GetPaymentConfig(c.Request.Context())
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, cfg)
- }
- // GetPlans returns subscription plans available for sale.
- // GET /api/v1/payment/plans
- func (h *PaymentHandler) GetPlans(c *gin.Context) {
- plans, err := h.configService.ListPlansForSale(c.Request.Context())
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- // Enrich plans with group platform for frontend color coding
- type planWithPlatform struct {
- ID int64 `json:"id"`
- GroupID int64 `json:"group_id"`
- GroupPlatform string `json:"group_platform"`
- GroupName string `json:"group_name"`
- RateMultiplier float64 `json:"rate_multiplier"`
- PeakRateEnabled bool `json:"peak_rate_enabled"`
- PeakStart string `json:"peak_start"`
- PeakEnd string `json:"peak_end"`
- PeakRateMultiplier float64 `json:"peak_rate_multiplier"`
- Name string `json:"name"`
- Description string `json:"description"`
- Price float64 `json:"price"`
- OriginalPrice *float64 `json:"original_price,omitempty"`
- Currency string `json:"currency,omitempty"`
- ValidityDays int `json:"validity_days"`
- ValidityUnit string `json:"validity_unit"`
- Features string `json:"features"`
- ProductName string `json:"product_name"`
- ForSale bool `json:"for_sale"`
- SortOrder int `json:"sort_order"`
- }
- groupInfo := h.configService.GetGroupInfoMap(c.Request.Context(), plans)
- result := make([]planWithPlatform, 0, len(plans))
- for _, p := range plans {
- gi := groupInfo[p.GroupID]
- result = append(result, planWithPlatform{
- ID: int64(p.ID), GroupID: p.GroupID,
- GroupPlatform: gi.Platform, GroupName: gi.Name,
- RateMultiplier: gi.RateMultiplier, PeakRateEnabled: gi.PeakRateEnabled,
- PeakStart: gi.PeakStart, PeakEnd: gi.PeakEnd, PeakRateMultiplier: gi.PeakRateMultiplier,
- Name: p.Name, Description: p.Description, Price: p.Price, OriginalPrice: p.OriginalPrice,
- Currency: p.Currency,
- ValidityDays: p.ValidityDays, ValidityUnit: p.ValidityUnit, Features: p.Features,
- ProductName: p.ProductName, ForSale: p.ForSale, SortOrder: p.SortOrder,
- })
- }
- response.Success(c, result)
- }
- // GetCheckoutInfo returns all data the payment page needs in a single call:
- // payment methods with limits, subscription plans, and configuration.
- // GET /api/v1/payment/checkout-info
- func (h *PaymentHandler) GetCheckoutInfo(c *gin.Context) {
- ctx := c.Request.Context()
- // Fetch limits (methods + global range)
- limitsResp, err := h.configService.GetAvailableMethodLimits(ctx)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- // Fetch payment config
- cfg, err := h.configService.GetPaymentConfig(ctx)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- alipayMobilePrecreateDeepLink := false
- if cfg.AlipayMobilePrecreateDeepLink {
- alipayMobilePrecreateDeepLink, err = h.configService.UsesOfficialAlipayVisibleMethod(ctx)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- }
- // Fetch plans with group info
- plans, _ := h.configService.ListPlansForSale(ctx)
- groupInfo := h.configService.GetGroupInfoMap(ctx, plans)
- planList := make([]checkoutPlan, 0, len(plans))
- for _, p := range plans {
- gi := groupInfo[p.GroupID]
- planList = append(planList, checkoutPlan{
- ID: int64(p.ID), GroupID: p.GroupID,
- GroupPlatform: gi.Platform, GroupName: gi.Name,
- RateMultiplier: gi.RateMultiplier,
- PeakRateEnabled: gi.PeakRateEnabled, PeakStart: gi.PeakStart,
- PeakEnd: gi.PeakEnd, PeakRateMultiplier: gi.PeakRateMultiplier,
- DailyLimitUSD: gi.DailyLimitUSD,
- WeeklyLimitUSD: gi.WeeklyLimitUSD, MonthlyLimitUSD: gi.MonthlyLimitUSD,
- ModelScopes: gi.ModelScopes,
- Name: p.Name, Description: p.Description, Price: p.Price, OriginalPrice: p.OriginalPrice,
- Currency: p.Currency,
- ValidityDays: p.ValidityDays, ValidityUnit: p.ValidityUnit, Features: parseFeatures(p.Features),
- ProductName: p.ProductName,
- })
- }
- response.Success(c, checkoutInfoResponse{
- Methods: limitsResp.Methods,
- GlobalMin: limitsResp.GlobalMin,
- GlobalMax: limitsResp.GlobalMax,
- Plans: planList,
- BalanceDisabled: cfg.BalanceDisabled,
- BalanceRechargeMultiplier: cfg.BalanceRechargeMultiplier,
- SubscriptionUSDToCNYRate: cfg.SubscriptionUSDToCNYRate,
- RechargeFeeRate: cfg.RechargeFeeRate,
- HelpText: cfg.HelpText,
- HelpImageURL: cfg.HelpImageURL,
- StripePublishableKey: cfg.StripePublishableKey,
- AlipayForceQRCode: cfg.AlipayForceQRCode,
- AlipayMobilePrecreateDeepLink: alipayMobilePrecreateDeepLink,
- })
- }
- type checkoutInfoResponse struct {
- Methods map[string]service.MethodLimits `json:"methods"`
- GlobalMin float64 `json:"global_min"`
- GlobalMax float64 `json:"global_max"`
- Plans []checkoutPlan `json:"plans"`
- BalanceDisabled bool `json:"balance_disabled"`
- BalanceRechargeMultiplier float64 `json:"balance_recharge_multiplier"`
- SubscriptionUSDToCNYRate float64 `json:"subscription_usd_to_cny_rate"`
- RechargeFeeRate float64 `json:"recharge_fee_rate"`
- HelpText string `json:"help_text"`
- HelpImageURL string `json:"help_image_url"`
- StripePublishableKey string `json:"stripe_publishable_key"`
- AlipayForceQRCode bool `json:"alipay_force_qrcode"`
- AlipayMobilePrecreateDeepLink bool `json:"alipay_mobile_precreate_deep_link"`
- }
- type checkoutPlan struct {
- ID int64 `json:"id"`
- GroupID int64 `json:"group_id"`
- GroupPlatform string `json:"group_platform"`
- GroupName string `json:"group_name"`
- RateMultiplier float64 `json:"rate_multiplier"`
- PeakRateEnabled bool `json:"peak_rate_enabled"`
- PeakStart string `json:"peak_start"`
- PeakEnd string `json:"peak_end"`
- PeakRateMultiplier float64 `json:"peak_rate_multiplier"`
- DailyLimitUSD *float64 `json:"daily_limit_usd"`
- WeeklyLimitUSD *float64 `json:"weekly_limit_usd"`
- MonthlyLimitUSD *float64 `json:"monthly_limit_usd"`
- ModelScopes []string `json:"supported_model_scopes"`
- Name string `json:"name"`
- Description string `json:"description"`
- Price float64 `json:"price"`
- OriginalPrice *float64 `json:"original_price,omitempty"`
- Currency string `json:"currency,omitempty"`
- ValidityDays int `json:"validity_days"`
- ValidityUnit string `json:"validity_unit"`
- Features []string `json:"features"`
- ProductName string `json:"product_name"`
- }
- // parseFeatures splits a newline-separated features string into a string slice.
- func parseFeatures(raw string) []string {
- if raw == "" {
- return []string{}
- }
- var out []string
- for _, line := range strings.Split(raw, "\n") {
- if s := strings.TrimSpace(line); s != "" {
- out = append(out, s)
- }
- }
- if out == nil {
- return []string{}
- }
- return out
- }
- // GetLimits returns per-payment-type limits derived from enabled provider instances.
- // GET /api/v1/payment/limits
- func (h *PaymentHandler) GetLimits(c *gin.Context) {
- resp, err := h.configService.GetAvailableMethodLimits(c.Request.Context())
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, resp)
- }
- // CreateOrderRequest is the request body for creating a payment order.
- type CreateOrderRequest struct {
- Amount float64 `json:"amount"`
- PaymentType string `json:"payment_type" binding:"required"`
- OpenID string `json:"openid"`
- WechatResumeToken string `json:"wechat_resume_token"`
- ReturnURL string `json:"return_url"`
- PaymentSource string `json:"payment_source"`
- OrderType string `json:"order_type"`
- PlanID int64 `json:"plan_id"`
- // IsMobile lets the frontend declare its mobile status directly. When
- // nil we fall back to User-Agent heuristics (which miss iPadOS / some
- // embedded browsers that strip the "Mobile" keyword).
- IsMobile *bool `json:"is_mobile,omitempty"`
- }
- // CreateOrder creates a new payment order.
- // POST /api/v1/payment/orders
- func (h *PaymentHandler) CreateOrder(c *gin.Context) {
- subject, ok := requireAuth(c)
- if !ok {
- return
- }
- var req CreateOrderRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- if strings.TrimSpace(req.WechatResumeToken) != "" {
- claims, err := h.paymentService.ParseWeChatPaymentResumeToken(req.WechatResumeToken)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- if err := applyWeChatPaymentResumeClaims(&req, claims); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- }
- mobile := isMobile(c)
- if req.IsMobile != nil {
- mobile = *req.IsMobile
- }
- result, err := h.paymentService.CreateOrder(c.Request.Context(), service.CreateOrderRequest{
- UserID: subject.UserID,
- Amount: req.Amount,
- PaymentType: req.PaymentType,
- OpenID: req.OpenID,
- ClientIP: c.ClientIP(),
- IsMobile: mobile,
- IsWeChatBrowser: isWeChatBrowser(c),
- SrcHost: c.Request.Host,
- SrcURL: c.Request.Referer(),
- ReturnURL: req.ReturnURL,
- PaymentSource: req.PaymentSource,
- OrderType: req.OrderType,
- PlanID: req.PlanID,
- Locale: c.GetHeader("Accept-Language"),
- })
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, result)
- }
- func applyWeChatPaymentResumeClaims(req *CreateOrderRequest, claims *service.WeChatPaymentResumeClaims) error {
- if req == nil || claims == nil {
- return infraerrors.BadRequest("INVALID_WECHAT_PAYMENT_RESUME_TOKEN", "wechat payment resume context is missing")
- }
- openid := strings.TrimSpace(claims.OpenID)
- if openid == "" {
- return infraerrors.BadRequest("INVALID_WECHAT_PAYMENT_RESUME_TOKEN", "wechat payment resume token missing openid")
- }
- paymentType := service.NormalizeVisibleMethod(claims.PaymentType)
- if paymentType == "" {
- paymentType = payment.TypeWxpay
- }
- if req.PaymentType != "" {
- requestPaymentType := service.NormalizeVisibleMethod(req.PaymentType)
- if requestPaymentType != "" && requestPaymentType != paymentType {
- return infraerrors.BadRequest("INVALID_WECHAT_PAYMENT_RESUME_TOKEN", "wechat payment resume token payment type mismatch")
- }
- }
- req.PaymentType = paymentType
- req.OpenID = openid
- if strings.TrimSpace(claims.Amount) != "" {
- amount, err := strconv.ParseFloat(strings.TrimSpace(claims.Amount), 64)
- if err != nil || amount <= 0 {
- return infraerrors.BadRequest("INVALID_WECHAT_PAYMENT_RESUME_TOKEN", fmt.Sprintf("invalid resume amount: %s", claims.Amount))
- }
- req.Amount = amount
- }
- if claims.OrderType != "" {
- req.OrderType = claims.OrderType
- }
- if claims.PlanID > 0 {
- req.PlanID = claims.PlanID
- }
- return nil
- }
- // GetMyOrders returns the authenticated user's orders.
- // GET /api/v1/payment/orders/my
- func (h *PaymentHandler) GetMyOrders(c *gin.Context) {
- subject, ok := requireAuth(c)
- if !ok {
- return
- }
- page, pageSize := response.ParsePagination(c)
- orders, total, err := h.paymentService.GetUserOrders(c.Request.Context(), subject.UserID, service.OrderListParams{
- Page: page,
- PageSize: pageSize,
- Status: c.Query("status"),
- OrderType: c.Query("order_type"),
- PaymentType: c.Query("payment_type"),
- })
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Paginated(c, sanitizePaymentOrdersForResponse(orders), int64(total), page, pageSize)
- }
- // GetOrder returns a single order for the authenticated user.
- // GET /api/v1/payment/orders/:id
- func (h *PaymentHandler) GetOrder(c *gin.Context) {
- subject, ok := requireAuth(c)
- if !ok {
- return
- }
- orderID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid order ID")
- return
- }
- order, err := h.paymentService.GetOrder(c.Request.Context(), orderID, subject.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, sanitizePaymentOrderForResponse(order))
- }
- // CancelOrder cancels a pending order for the authenticated user.
- // POST /api/v1/payment/orders/:id/cancel
- func (h *PaymentHandler) CancelOrder(c *gin.Context) {
- subject, ok := requireAuth(c)
- if !ok {
- return
- }
- orderID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid order ID")
- return
- }
- msg, err := h.paymentService.CancelOrder(c.Request.Context(), orderID, subject.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{"message": msg})
- }
- // RefundRequestBody is the request body for requesting a refund.
- type RefundRequestBody struct {
- Reason string `json:"reason"`
- }
- // RequestRefund submits a refund request for a completed order.
- // POST /api/v1/payment/orders/:id/refund-request
- func (h *PaymentHandler) RequestRefund(c *gin.Context) {
- subject, ok := requireAuth(c)
- if !ok {
- return
- }
- orderID, err := strconv.ParseInt(c.Param("id"), 10, 64)
- if err != nil {
- response.BadRequest(c, "Invalid order ID")
- return
- }
- var req RefundRequestBody
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- if err := h.paymentService.RequestRefund(c.Request.Context(), orderID, subject.UserID, req.Reason); err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{"message": "refund requested"})
- }
- // GetRefundEligibleProviders returns provider instance IDs that allow user refund.
- func (h *PaymentHandler) GetRefundEligibleProviders(c *gin.Context) {
- ids, err := h.configService.GetUserRefundEligibleInstanceIDs(c.Request.Context())
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, gin.H{"provider_instance_ids": ids})
- }
- // VerifyOrderRequest is the request body for verifying a payment order.
- type VerifyOrderRequest struct {
- OutTradeNo string `json:"out_trade_no" binding:"required"`
- }
- type ResolveOrderByResumeTokenRequest struct {
- ResumeToken string `json:"resume_token" binding:"required"`
- }
- // VerifyOrder actively queries the upstream payment provider to check
- // if payment was made, and processes it if so.
- // POST /api/v1/payment/orders/verify
- func (h *PaymentHandler) VerifyOrder(c *gin.Context) {
- subject, ok := requireAuth(c)
- if !ok {
- return
- }
- var req VerifyOrderRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- order, err := h.paymentService.VerifyOrderByOutTradeNo(c.Request.Context(), req.OutTradeNo, subject.UserID)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, sanitizePaymentOrderForResponse(order))
- }
- // PublicOrderResult is returned after a signed resume-token lookup. The token
- // proves possession of the checkout session, so the result keeps the legacy
- // frontend contract needed by payment result pages.
- type PublicOrderResult struct {
- ID int64 `json:"id"`
- OutTradeNo string `json:"out_trade_no"`
- Amount float64 `json:"amount"`
- PayAmount float64 `json:"pay_amount"`
- FeeRate float64 `json:"fee_rate"`
- Currency string `json:"currency"`
- PaymentType string `json:"payment_type"`
- OrderType string `json:"order_type"`
- Status string `json:"status"`
- CreatedAt time.Time `json:"created_at"`
- ExpiresAt time.Time `json:"expires_at"`
- PaidAt *time.Time `json:"paid_at,omitempty"`
- CompletedAt *time.Time `json:"completed_at,omitempty"`
- RefundAmount float64 `json:"refund_amount"`
- RefundReason *string `json:"refund_reason,omitempty"`
- RefundRequestedAt *time.Time `json:"refund_requested_at,omitempty"`
- RefundRequestedBy *string `json:"refund_requested_by,omitempty"`
- RefundRequestReason *string `json:"refund_request_reason,omitempty"`
- PlanID *int64 `json:"plan_id,omitempty"`
- }
- // PublicOrderVerifyResult is returned by the legacy anonymous out_trade_no
- // lookup. Keep this intentionally minimal because out_trade_no is not secret.
- type PublicOrderVerifyResult struct {
- OutTradeNo string `json:"out_trade_no"`
- Status string `json:"status"`
- Paid bool `json:"paid"`
- CreatedAt time.Time `json:"created_at"`
- ExpiresAt time.Time `json:"expires_at"`
- PaidAt *time.Time `json:"paid_at,omitempty"`
- CompletedAt *time.Time `json:"completed_at,omitempty"`
- }
- func buildPublicOrderResult(order *dbent.PaymentOrder) PublicOrderResult {
- return PublicOrderResult{
- ID: order.ID,
- OutTradeNo: order.OutTradeNo,
- Amount: order.Amount,
- PayAmount: order.PayAmount,
- FeeRate: order.FeeRate,
- Currency: service.PaymentOrderCurrency(order),
- PaymentType: order.PaymentType,
- OrderType: order.OrderType,
- Status: order.Status,
- CreatedAt: order.CreatedAt,
- ExpiresAt: order.ExpiresAt,
- PaidAt: order.PaidAt,
- CompletedAt: order.CompletedAt,
- RefundAmount: order.RefundAmount,
- RefundReason: order.RefundReason,
- RefundRequestedAt: order.RefundRequestedAt,
- RefundRequestedBy: order.RefundRequestedBy,
- RefundRequestReason: order.RefundRequestReason,
- PlanID: order.PlanID,
- }
- }
- func buildPublicOrderVerifyResult(order *dbent.PaymentOrder) PublicOrderVerifyResult {
- return PublicOrderVerifyResult{
- OutTradeNo: order.OutTradeNo,
- Status: order.Status,
- Paid: publicOrderStatusPaid(order.Status),
- CreatedAt: order.CreatedAt,
- ExpiresAt: order.ExpiresAt,
- PaidAt: order.PaidAt,
- CompletedAt: order.CompletedAt,
- }
- }
- func publicOrderStatusPaid(status string) bool {
- switch status {
- case service.OrderStatusPaid,
- service.OrderStatusCompleted,
- service.OrderStatusRefundRequested,
- service.OrderStatusRefunding,
- service.OrderStatusRefundPending,
- service.OrderStatusPartiallyRefunded,
- service.OrderStatusRefunded,
- service.OrderStatusRefundFailed:
- return true
- default:
- return false
- }
- }
- // VerifyOrderPublic keeps the legacy anonymous out_trade_no lookup available as
- // a compatibility path for older result pages and staggered deploys.
- // POST /api/v1/payment/public/orders/verify
- func (h *PaymentHandler) VerifyOrderPublic(c *gin.Context) {
- var req VerifyOrderRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- order, err := h.paymentService.VerifyOrderPublic(c.Request.Context(), req.OutTradeNo)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, buildPublicOrderVerifyResult(order))
- }
- // ResolveOrderPublicByResumeToken resolves a payment order from a signed resume token.
- // POST /api/v1/payment/public/orders/resolve
- func (h *PaymentHandler) ResolveOrderPublicByResumeToken(c *gin.Context) {
- var req ResolveOrderByResumeTokenRequest
- if err := c.ShouldBindJSON(&req); err != nil {
- response.BadRequest(c, "Invalid request: "+err.Error())
- return
- }
- order, err := h.paymentService.GetPublicOrderByResumeToken(c.Request.Context(), req.ResumeToken)
- if err != nil {
- response.ErrorFrom(c, err)
- return
- }
- response.Success(c, buildPublicOrderResult(order))
- }
- // requireAuth extracts the authenticated subject from the context.
- // Returns the subject and true on success; on failure it writes an Unauthorized response and returns false.
- func requireAuth(c *gin.Context) (middleware2.AuthSubject, bool) {
- subject, ok := middleware2.GetAuthSubjectFromContext(c)
- if !ok {
- response.Unauthorized(c, "User not authenticated")
- return middleware2.AuthSubject{}, false
- }
- return subject, true
- }
- // isMobile detects mobile user agents.
- func isMobile(c *gin.Context) bool {
- ua := strings.ToLower(c.GetHeader("User-Agent"))
- for _, kw := range []string{"mobile", "android", "iphone", "ipad", "ipod"} {
- if strings.Contains(ua, kw) {
- return true
- }
- }
- return false
- }
- type PaymentOrderResult struct {
- ID int64 `json:"id"`
- UserID int64 `json:"user_id"`
- Amount float64 `json:"amount"`
- PayAmount float64 `json:"pay_amount"`
- FeeRate float64 `json:"fee_rate"`
- Currency string `json:"currency"`
- PaymentType string `json:"payment_type"`
- OutTradeNo string `json:"out_trade_no"`
- Status string `json:"status"`
- OrderType string `json:"order_type"`
- CreatedAt time.Time `json:"created_at"`
- ExpiresAt time.Time `json:"expires_at"`
- PaidAt *time.Time `json:"paid_at,omitempty"`
- CompletedAt *time.Time `json:"completed_at,omitempty"`
- RefundAmount float64 `json:"refund_amount"`
- RefundReason *string `json:"refund_reason,omitempty"`
- RefundRequestedAt *time.Time `json:"refund_requested_at,omitempty"`
- RefundRequestedBy *string `json:"refund_requested_by,omitempty"`
- RefundRequestReason *string `json:"refund_request_reason,omitempty"`
- PlanID *int64 `json:"plan_id,omitempty"`
- ProviderInstanceID *string `json:"provider_instance_id,omitempty"`
- }
- func sanitizePaymentOrdersForResponse(orders []*dbent.PaymentOrder) []PaymentOrderResult {
- out := make([]PaymentOrderResult, 0, len(orders))
- for _, order := range orders {
- if item := sanitizePaymentOrderForResponse(order); item != nil {
- out = append(out, *item)
- }
- }
- return out
- }
- func sanitizePaymentOrderForResponse(order *dbent.PaymentOrder) *PaymentOrderResult {
- if order == nil {
- return nil
- }
- return &PaymentOrderResult{
- ID: order.ID,
- UserID: order.UserID,
- Amount: order.Amount,
- PayAmount: order.PayAmount,
- FeeRate: order.FeeRate,
- Currency: service.PaymentOrderCurrency(order),
- PaymentType: order.PaymentType,
- OutTradeNo: order.OutTradeNo,
- Status: order.Status,
- OrderType: order.OrderType,
- CreatedAt: order.CreatedAt,
- ExpiresAt: order.ExpiresAt,
- PaidAt: order.PaidAt,
- CompletedAt: order.CompletedAt,
- RefundAmount: order.RefundAmount,
- RefundReason: order.RefundReason,
- RefundRequestedAt: order.RefundRequestedAt,
- RefundRequestedBy: order.RefundRequestedBy,
- RefundRequestReason: order.RefundRequestReason,
- PlanID: order.PlanID,
- ProviderInstanceID: order.ProviderInstanceID,
- }
- }
- func isWeChatBrowser(c *gin.Context) bool {
- return strings.Contains(strings.ToLower(c.GetHeader("User-Agent")), "micromessenger")
- }
|