package handler import ( "encoding/json" "io" "net/http" "net/url" "regexp" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/gouki/lunar-server/internal/config" "github.com/gouki/lunar-server/internal/model" "github.com/gouki/lunar-server/internal/service" ) var reportIDPattern = regexp.MustCompile(`^ZM-[A-Z0-9]{7,32}$`) var accessCodePattern = regexp.MustCompile(`^ZM-[A-Z2-7]{4}-[A-Z2-7]{4}-[A-Z2-7]{4}$`) func SaveChartRecord(c *gin.Context) { var request struct { ReportID string `json:"reportId" binding:"required"` Name string `json:"name"` BirthDate string `json:"birthDate" binding:"required"` BirthTime string `json:"birthTime" binding:"required"` Gender string `json:"gender" binding:"required"` City string `json:"city" binding:"required"` Snapshot json.RawMessage `json:"snapshot" binding:"required"` } if err := c.ShouldBindJSON(&request); err != nil || !reportIDPattern.MatchString(request.ReportID) || len(request.Snapshot) < 2 || len(request.Snapshot) > 512*1024 || request.Snapshot[0] != '{' { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "排盘记录参数错误"}) return } record := &model.BaziChartRecord{ ReportID: request.ReportID, Name: request.Name, BirthDate: request.BirthDate, BirthTime: request.BirthTime, Gender: request.Gender, City: request.City, Snapshot: string(request.Snapshot), } if userID, exists := c.Get("userID"); exists { if value, ok := userID.(uint); ok { record.UserID = &value } } if err := service.NewCommerceService().CreateChartRecord(record); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "保存排盘记录失败"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": gin.H{"recordId": record.ID}}) } func CreateReportCheckout(c *gin.Context) { userID, _ := c.Get("userID") var request struct { Provider string `json:"provider" binding:"required"` ReportID string `json:"reportId" binding:"required"` Snapshot json.RawMessage `json:"snapshot" binding:"required"` } if err := c.ShouldBindJSON(&request); err != nil || !reportIDPattern.MatchString(request.ReportID) || len(request.Snapshot) > 256*1024 || len(request.Snapshot) < 2 || request.Snapshot[0] != '{' { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "支付参数错误"}) return } user, err := service.NewUserService().GetUserByID(userID.(uint)) if err != nil || user.Email == "" { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "请先完成邮箱登录"}) return } commerce := service.NewCommerceService() product, err := commerce.GetActiveReportProduct() if err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "msg": "商品暂不可购买"}) return } order, err := commerce.CreateReportOrder(user.ID, user.Email, request.ReportID, request.Provider, string(request.Snapshot), product) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": err.Error()}) return } cfg := config.Load() providerConfigured := (request.Provider == "stripe" && cfg.Stripe.SecretKey != "") || (request.Provider == "binance_pay" && cfg.BinancePay.APIKey != "") // 开发模式:支付渠道未配置时走本地模拟支付,方便完整联调 if !providerConfigured && cfg.Server.Env != "production" { mockURL := strings.TrimRight(cfg.Commerce.WebAppURL, "/") + "/?payment=success&order=" + url.QueryEscape(order.OrderNo) if err := commerce.SetCheckout(order.OrderNo, "DEV-"+order.OrderNo, mockURL); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "订单保存失败"}) return } // 模拟 webhook:立即发放权益(Stripe 金额用 cents,Binance 用 USDT 数量) mockPaidAmount := order.ProviderAmount if request.Provider == "stripe" { mockPaidAmount = strconv.Itoa(order.Amount) } _ = commerce.CompletePayment(request.Provider, "dev-"+order.OrderNo, "dev.mock.paid", order.OrderNo, "DEV-"+order.OrderNo, "DEV-TX-"+order.OrderNo, mockPaidAmount, order.Currency, "dev-mock") c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": gin.H{ "orderNo": order.OrderNo, "provider": order.Provider, "amount": order.ProviderAmount, "currency": order.Currency, "checkoutUrl": mockURL, "expiresAt": order.ExpireTime, "demo": true, }}) return } var checkout *service.CheckoutResult if request.Provider == "stripe" { checkout, err = service.NewStripeProvider(cfg.Stripe).CreateCheckout(c.Request.Context(), order, cfg.Commerce.WebAppURL) } else if request.Provider == "binance_pay" { checkout, err = service.NewBinancePayProvider(cfg.BinancePay).CreateCheckout(c.Request.Context(), order, cfg.Commerce.WebAppURL, cfg.Commerce.PublicBaseURL) } else { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "不支持的支付方式"}) return } if err != nil { _ = commerce.MarkOrderStatus(request.Provider, "create-failed:"+order.OrderNo, "checkout.create_failed", order.OrderNo, "cancelled", "") c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "msg": "支付渠道暂时不可用"}) return } if err := commerce.SetCheckout(order.OrderNo, checkout.ProviderOrderID, checkout.CheckoutURL); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "订单保存失败"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": gin.H{ "orderNo": order.OrderNo, "provider": order.Provider, "amount": order.ProviderAmount, "currency": order.Currency, "checkoutUrl": checkout.CheckoutURL, "expiresAt": order.ExpireTime, }}) } func StripeWebhook(c *gin.Context) { raw, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) if err != nil { c.Status(http.StatusBadRequest) return } event, err := service.NewStripeProvider(config.Load().Stripe).ParseWebhook(raw, c.GetHeader("Stripe-Signature"), time.Now()) if err != nil { c.Status(http.StatusBadRequest) return } if err := processCommerceWebhook("stripe", event); err != nil { c.Status(http.StatusInternalServerError) return } c.JSON(http.StatusOK, gin.H{"received": true}) } func BinancePayWebhook(c *gin.Context) { raw, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) if err != nil { c.Status(http.StatusBadRequest) return } event, err := service.NewBinancePayProvider(config.Load().BinancePay).ParseWebhook( raw, c.GetHeader("BinancePay-Timestamp"), c.GetHeader("BinancePay-Nonce"), c.GetHeader("BinancePay-Signature"), ) if err != nil { c.Status(http.StatusBadRequest) return } if err := processCommerceWebhook("binance_pay", event); err != nil { c.Status(http.StatusInternalServerError) return } c.JSON(http.StatusOK, gin.H{"returnCode": "SUCCESS", "returnMessage": nil}) } func processCommerceWebhook(provider string, event *service.PaymentWebhook) error { if event.Status == "ignored" { return nil } commerce := service.NewCommerceService() if event.Status == "refunded" { return commerce.RefundPayment(provider, event.EventID, event.EventType, event.OrderNo, event.TransactionID, event.PayloadHash) } if event.OrderNo == "" { return nil } if event.Status == "paid" { return commerce.CompletePayment(provider, event.EventID, event.EventType, event.OrderNo, event.ProviderOrderID, event.TransactionID, event.Amount, event.Currency, event.PayloadHash) } return commerce.MarkOrderStatus(provider, event.EventID, event.EventType, event.OrderNo, event.Status, event.PayloadHash) } func GetCommerceOrder(c *gin.Context) { userID, _ := c.Get("userID") order, err := service.NewCommerceService().GetUserOrder(userID.(uint), c.Param("orderNo")) if err != nil { c.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": "订单不存在"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": order}) } func GetReportAccess(c *gin.Context) { userID, _ := c.Get("userID") reportID := c.Param("reportId") if !reportIDPattern.MatchString(reportID) { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "报告编号错误"}) return } access, err := service.NewCommerceService().HasReportAccess(userID.(uint), reportID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "查询失败"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": gin.H{"reportId": reportID, "unlocked": access}}) } func GetReportByAccessCode(c *gin.Context) { userID, _ := c.Get("userID") accessCode := c.Param("accessCode") if !accessCodePattern.MatchString(accessCode) { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "msg": "恢复码格式错误"}) return } record, err := service.NewCommerceService().GetReportSnapshot(userID.(uint), accessCode) if err != nil { c.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": "未找到已购买报告"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": gin.H{"reportId": record.ReportID, "snapshot": json.RawMessage(record.Snapshot)}}) } func ListPurchasedReports(c *gin.Context) { userID, _ := c.Get("userID") reports, err := service.NewCommerceService().ListPurchasedReports(userID.(uint)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "获取报告失败"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": gin.H{"list": reports}}) } func AdminCommerceOrders(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 20 } filter := service.AdminOrderFilter{Page: page, PageSize: pageSize, Status: c.Query("status"), Provider: c.Query("provider"), Search: c.Query("search")} orders, total, err := service.NewCommerceService().AdminOrders(filter) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "获取订单失败"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": gin.H{"list": orders, "total": total, "page": page, "pageSize": pageSize}}) } func AdminChartRecords(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "50")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 50 } records, total, err := service.NewCommerceService().AdminChartRecords(c.Query("search"), page, pageSize) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "获取排盘记录失败"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": gin.H{"list": records, "total": total, "page": page, "pageSize": pageSize}}) } func AdminCommerceStats(c *gin.Context) { stats, err := service.NewCommerceService().AdminStats() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "msg": "获取统计失败"}) return } c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "success", "data": stats}) }