package handler import ( "encoding/json" "fmt" "net/http" "strings" "time" "unicode/utf8" "soul-api/internal/database" "soul-api/internal/model" "soul-api/internal/wechat" "github.com/gin-gonic/gin" ) const giftPayExpireHours = 24 // giftPayPreviewContent 取内容前 20%,用于代付页营销展示 func giftPayPreviewContent(content string) string { n := utf8.RuneCountInString(content) if n == 0 { return "" } limit := n * 20 / 100 if limit < 50 { limit = 50 } if limit > n { limit = n } runes := []rune(content) if limit >= n { return string(runes) } return string(runes[:limit]) + "……" } // GiftPayCreate POST /api/miniprogram/gift-pay/create 创建代付请求 func GiftPayCreate(c *gin.Context) { var req struct { UserID string `json:"userId" binding:"required"` ProductType string `json:"productType" binding:"required"` ProductID string `json:"productId"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少参数"}) return } db := database.DB() // 校验发起人 var initiator model.User if err := db.Where("id = ?", req.UserID).First(&initiator).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户不存在"}) return } // 价格与商品校验 productID := req.ProductID if productID == "" { switch req.ProductType { case "vip": productID = "vip_annual" case "match": productID = "match" case "fullbook": productID = "fullbook" } } amount, priceErr := getStandardPrice(db, req.ProductType, productID) if priceErr != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": priceErr.Error()}) return } // 发起人若有推荐人绑定,享受好友优惠 var referrerID *string var binding struct { ReferrerID string `gorm:"column:referrer_id"` } if err := db.Raw(` SELECT referrer_id FROM referral_bindings WHERE referee_id = ? AND status = 'active' AND expiry_date > NOW() ORDER BY binding_date DESC LIMIT 1 `, req.UserID).Scan(&binding).Error; err == nil && binding.ReferrerID != "" { referrerID = &binding.ReferrerID var cfg model.SystemConfig if err := db.Where("config_key = ?", "referral_config").First(&cfg).Error; err == nil { var config map[string]interface{} if json.Unmarshal(cfg.ConfigValue, &config) == nil { if userDiscount, ok := config["userDiscount"].(float64); ok && userDiscount > 0 { amount = amount * (1 - userDiscount/100) if amount < 0.01 { amount = 0.01 } } } } } _ = referrerID // 分佣在 PayNotify 时按发起人计算 // 校验发起人是否已拥有 if req.ProductType == "section" && productID != "" { var cnt int64 db.Model(&model.Order{}).Where("user_id = ? AND product_type = ? AND product_id = ? AND status IN ?", req.UserID, "section", productID, []string{"paid", "completed"}).Count(&cnt) if cnt > 0 { c.JSON(http.StatusOK, gin.H{"success": false, "error": "您已拥有该章节"}) return } } if req.ProductType == "fullbook" || req.ProductType == "vip" { var u model.User db.Where("id = ?", req.UserID).Select("has_full_book", "is_vip", "vip_expire_date").First(&u) if u.HasFullBook != nil && *u.HasFullBook { c.JSON(http.StatusOK, gin.H{"success": false, "error": "您已拥有全书"}) return } if req.ProductType == "vip" && u.IsVip != nil && *u.IsVip && u.VipExpireDate != nil && u.VipExpireDate.After(time.Now()) { c.JSON(http.StatusOK, gin.H{"success": false, "error": "您已是有效VIP"}) return } } // 描述 desc := "" switch req.ProductType { case "fullbook": desc = "《一场Soul的创业实验》全书" case "vip": desc = "卡若创业派对VIP年度会员(365天)" case "match": desc = "购买匹配次数" case "section": var ch model.Chapter if err := db.Select("section_title").Where("id = ?", productID).First(&ch).Error; err == nil && ch.SectionTitle != "" { desc = ch.SectionTitle } else { desc = fmt.Sprintf("章节-%s", productID) } default: desc = fmt.Sprintf("%s-%s", req.ProductType, productID) } expireAt := time.Now().Add(giftPayExpireHours * time.Hour) requestSN := "GPR" + wechat.GenerateOrderSn() id := "gpr_" + fmt.Sprintf("%d", time.Now().UnixNano()%100000000000) gpr := model.GiftPayRequest{ ID: id, RequestSN: requestSN, InitiatorUserID: req.UserID, ProductType: req.ProductType, ProductID: productID, Amount: amount, Description: desc, Status: "pending", ExpireAt: expireAt, } if err := db.Create(&gpr).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建失败"}) return } path := fmt.Sprintf("pages/gift-pay/detail?requestSn=%s", requestSN) c.JSON(http.StatusOK, gin.H{ "success": true, "requestSn": requestSN, "path": path, "amount": amount, "expireAt": expireAt.Format(time.RFC3339), }) } // GiftPayDetail GET /api/miniprogram/gift-pay/detail?requestSn=xxx 代付详情(代付人用) func GiftPayDetail(c *gin.Context) { requestSn := strings.TrimSpace(c.Query("requestSn")) if requestSn == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少代付请求号"}) return } db := database.DB() var gpr model.GiftPayRequest if err := db.Where("request_sn = ?", requestSn).First(&gpr).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "代付请求不存在"}) return } if gpr.Status != "pending" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "该代付已处理"}) return } if time.Now().After(gpr.ExpireAt) { db.Model(&gpr).Update("status", "expired") c.JSON(http.StatusOK, gin.H{"success": false, "error": "代付已过期"}) return } // 发起人昵称(脱敏) var initiator model.User nickname := "好友" if err := db.Where("id = ?", gpr.InitiatorUserID).Select("nickname").First(&initiator).Error; err == nil && initiator.Nickname != nil { n := *initiator.Nickname if len(n) > 2 { n = string([]rune(n)[0]) + "**" } nickname = n } // 营销:章节类型时返回标题和内容预览,吸引代付人 sectionTitle := gpr.Description contentPreview := "" if gpr.ProductType == "section" && gpr.ProductID != "" { var ch model.Chapter if err := db.Select("section_title", "content").Where("id = ?", gpr.ProductID).First(&ch).Error; err == nil { if ch.SectionTitle != "" { sectionTitle = ch.SectionTitle } contentPreview = giftPayPreviewContent(ch.Content) } } c.JSON(http.StatusOK, gin.H{ "success": true, "requestSn": gpr.RequestSN, "productType": gpr.ProductType, "productId": gpr.ProductID, "amount": gpr.Amount, "description": gpr.Description, "sectionTitle": sectionTitle, "contentPreview": contentPreview, "initiatorNickname": nickname, "initiatorUserId": gpr.InitiatorUserID, "expireAt": gpr.ExpireAt.Format(time.RFC3339), }) } // GiftPayPay POST /api/miniprogram/gift-pay/pay 代付人发起支付 func GiftPayPay(c *gin.Context) { var req struct { RequestSn string `json:"requestSn" binding:"required"` OpenID string `json:"openId" binding:"required"` UserID string `json:"userId"` // 代付人ID,用于校验不能自己付 } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少参数"}) return } db := database.DB() var gpr model.GiftPayRequest if err := db.Where("request_sn = ? AND status = ?", req.RequestSn, "pending").First(&gpr).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "代付请求不存在或已处理"}) return } if time.Now().After(gpr.ExpireAt) { db.Model(&gpr).Update("status", "expired") c.JSON(http.StatusOK, gin.H{"success": false, "error": "代付已过期"}) return } // 不能自己给自己代付 if req.UserID != "" && req.UserID == gpr.InitiatorUserID { c.JSON(http.StatusOK, gin.H{"success": false, "error": "不能为自己代付"}) return } // 获取代付人信息 var payer model.User if err := db.Where("open_id = ?", req.OpenID).First(&payer).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "请先登录"}) return } if payer.ID == gpr.InitiatorUserID { c.JSON(http.StatusOK, gin.H{"success": false, "error": "不能为自己代付"}) return } // 创建订单(归属发起人,记录代付信息) orderSn := wechat.GenerateOrderSn() status := "created" pm := "wechat" productID := gpr.ProductID desc := gpr.Description gprID := gpr.ID payerID := payer.ID order := model.Order{ ID: orderSn, OrderSN: orderSn, UserID: gpr.InitiatorUserID, OpenID: req.OpenID, ProductType: gpr.ProductType, ProductID: &productID, Amount: gpr.Amount, Description: &desc, Status: &status, PaymentMethod: &pm, GiftPayRequestID: &gprID, PayerUserID: &payerID, } if err := db.Create(&order).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建订单失败"}) return } // 唤起微信支付,attach 中 userId=发起人,giftPayRequestSn=请求号 attach := fmt.Sprintf(`{"productType":"%s","productId":"%s","userId":"%s","giftPayRequestSn":"%s"}`, gpr.ProductType, gpr.ProductID, gpr.InitiatorUserID, gpr.RequestSN) totalFee := int(gpr.Amount * 100) ctx := c.Request.Context() prepayID, err := wechat.PayJSAPIOrder(ctx, req.OpenID, orderSn, totalFee, "代付-"+gpr.Description, attach) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": fmt.Sprintf("支付请求失败: %v", err)}) return } payParams, err := wechat.GetJSAPIPayParams(prepayID) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "生成支付参数失败"}) return } // 预占:更新请求状态为 paying(可选,防并发) // 简化:不预占,PayNotify 时再更新 c.JSON(http.StatusOK, gin.H{ "success": true, "data": gin.H{ "orderSn": orderSn, "prepayId": prepayID, "payParams": payParams, }, }) } // GiftPayCancel POST /api/miniprogram/gift-pay/cancel 发起人取消 func GiftPayCancel(c *gin.Context) { var req struct { RequestSn string `json:"requestSn" binding:"required"` UserID string `json:"userId" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少参数"}) return } db := database.DB() var gpr model.GiftPayRequest if err := db.Where("request_sn = ?", req.RequestSn).First(&gpr).Error; err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "代付请求不存在"}) return } if gpr.InitiatorUserID != req.UserID { c.JSON(http.StatusOK, gin.H{"success": false, "error": "无权取消"}) return } if gpr.Status != "pending" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "该代付已处理"}) return } db.Model(&gpr).Update("status", "cancelled") c.JSON(http.StatusOK, gin.H{"success": true, "message": "已取消"}) } // GiftPayMyRequests GET /api/miniprogram/gift-pay/my-requests?userId= 我发起的 func GiftPayMyRequests(c *gin.Context) { userID := c.Query("userId") if userID == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少userId"}) return } db := database.DB() var list []model.GiftPayRequest db.Where("initiator_user_id = ?", userID).Order("created_at DESC").Limit(50).Find(&list) out := make([]gin.H, 0, len(list)) for _, r := range list { out = append(out, gin.H{ "requestSn": r.RequestSN, "productType": r.ProductType, "productId": r.ProductID, "amount": r.Amount, "description": r.Description, "status": r.Status, "expireAt": r.ExpireAt.Format(time.RFC3339), "createdAt": r.CreatedAt.Format(time.RFC3339), }) } c.JSON(http.StatusOK, gin.H{"success": true, "list": out}) } // GiftPayMyPayments GET /api/miniprogram/gift-pay/my-payments?userId= 我帮付的 func GiftPayMyPayments(c *gin.Context) { userID := c.Query("userId") if userID == "" { c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少userId"}) return } db := database.DB() var list []model.GiftPayRequest db.Where("payer_user_id = ?", userID).Order("created_at DESC").Limit(50).Find(&list) out := make([]gin.H, 0, len(list)) for _, r := range list { out = append(out, gin.H{ "requestSn": r.RequestSN, "productType": r.ProductType, "amount": r.Amount, "description": r.Description, "status": r.Status, "createdAt": r.CreatedAt.Format(time.RFC3339), }) } c.JSON(http.StatusOK, gin.H{"success": true, "list": out}) }