package handler import ( "encoding/json" "fmt" "io" "net/http" "strings" "time" "soul-api/internal/database" "soul-api/internal/model" "soul-api/internal/wechat" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // MiniprogramLogin POST /api/miniprogram/login func MiniprogramLogin(c *gin.Context) { var req struct { Code string `json:"code" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少登录code"}) return } // 调用微信接口获取 openid 和 session_key openID, sessionKey, _, err := wechat.Code2Session(req.Code) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": fmt.Sprintf("微信登录失败: %v", err)}) return } db := database.DB() // 查询用户是否存在 var user model.User result := db.Where("open_id = ?", openID).First(&user) isNewUser := result.Error != nil if isNewUser { // 创建新用户 userID := openID // 直接使用 openid 作为用户 ID referralCode := "SOUL" + strings.ToUpper(openID[len(openID)-6:]) nickname := "微信用户" + openID[len(openID)-4:] avatar := "" hasFullBook := false earnings := 0.0 pendingEarnings := 0.0 referralCount := 0 purchasedSections := "[]" user = model.User{ ID: userID, OpenID: &openID, SessionKey: &sessionKey, Nickname: &nickname, Avatar: &avatar, ReferralCode: &referralCode, HasFullBook: &hasFullBook, PurchasedSections: &purchasedSections, Earnings: &earnings, PendingEarnings: &pendingEarnings, ReferralCount: &referralCount, } if err := db.Create(&user).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "创建用户失败"}) return } } else { // 更新 session_key db.Model(&user).Update("session_key", sessionKey) } // 从 orders 表查询真实购买记录 var purchasedSections []string var orderRows []struct { ProductID string `gorm:"column:product_id"` } db.Raw(` SELECT DISTINCT product_id FROM orders WHERE user_id = ? AND status = 'paid' AND product_type = 'section' `, user.ID).Scan(&orderRows) for _, row := range orderRows { if row.ProductID != "" { purchasedSections = append(purchasedSections, row.ProductID) } } if purchasedSections == nil { purchasedSections = []string{} } // 构建返回的用户对象 responseUser := map[string]interface{}{ "id": user.ID, "openId": getStringValue(user.OpenID), "nickname": getStringValue(user.Nickname), "avatar": getStringValue(user.Avatar), "phone": getStringValue(user.Phone), "wechatId": getStringValue(user.WechatID), "referralCode": getStringValue(user.ReferralCode), "hasFullBook": getBoolValue(user.HasFullBook), "purchasedSections": purchasedSections, "earnings": getFloatValue(user.Earnings), "pendingEarnings": getFloatValue(user.PendingEarnings), "referralCount": getIntValue(user.ReferralCount), "createdAt": user.CreatedAt, } // 生成 token token := fmt.Sprintf("tk_%s_%d", openID[len(openID)-8:], time.Now().Unix()) c.JSON(http.StatusOK, gin.H{ "success": true, "data": map[string]interface{}{ "openId": openID, "user": responseUser, "token": token, }, "isNewUser": isNewUser, }) } // 辅助函数 func getStringValue(ptr *string) string { if ptr == nil { return "" } return *ptr } func getBoolValue(ptr *bool) bool { if ptr == nil { return false } return *ptr } func getFloatValue(ptr *float64) float64 { if ptr == nil { return 0.0 } return *ptr } func getIntValue(ptr *int) int { if ptr == nil { return 0 } return *ptr } // MiniprogramPay GET/POST /api/miniprogram/pay func MiniprogramPay(c *gin.Context) { if c.Request.Method == "POST" { miniprogramPayPost(c) } else { miniprogramPayGet(c) } } // POST - 创建小程序支付订单 func miniprogramPayPost(c *gin.Context) { var req struct { OpenID string `json:"openId" binding:"required"` ProductType string `json:"productType" binding:"required"` ProductID string `json:"productId"` Amount float64 `json:"amount" binding:"required"` Description string `json:"description"` UserID string `json:"userId"` ReferralCode string `json:"referralCode"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少openId参数,请先登录"}) return } if req.Amount <= 0 { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "支付金额无效"}) return } db := database.DB() // 获取推广配置计算好友优惠 finalAmount := req.Amount if req.ReferralCode != "" { var cfg model.SystemConfig if err := db.Where("config_key = ?", "referral_config").First(&cfg).Error; err == nil { var config map[string]interface{} if err := json.Unmarshal(cfg.ConfigValue, &config); err == nil { if userDiscount, ok := config["userDiscount"].(float64); ok && userDiscount > 0 { discountRate := userDiscount / 100 finalAmount = req.Amount * (1 - discountRate) if finalAmount < 0.01 { finalAmount = 0.01 } } } } } // 生成订单号 orderSn := wechat.GenerateOrderSn() totalFee := int(finalAmount * 100) // 转为分 description := req.Description if description == "" { if req.ProductType == "fullbook" { description = "《一场Soul的创业实验》全书" } else { description = fmt.Sprintf("章节购买-%s", req.ProductID) } } // 获取客户端 IP clientIP := c.ClientIP() if clientIP == "" { clientIP = "127.0.0.1" } // 查询用户的有效推荐人 var referrerID *string if req.UserID != "" { var binding struct { ReferrerID string `gorm:"column:referrer_id"` } 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 if err == nil && binding.ReferrerID != "" { referrerID = &binding.ReferrerID } } // 如果没有绑定,尝试从邀请码解析推荐人 if referrerID == nil && req.ReferralCode != "" { var refUser model.User if err := db.Where("referral_code = ?", req.ReferralCode).First(&refUser).Error; err == nil { referrerID = &refUser.ID } } // 插入订单到数据库 userID := req.UserID if userID == "" { userID = req.OpenID } productID := req.ProductID if productID == "" { productID = "fullbook" } status := "created" order := model.Order{ ID: orderSn, OrderSN: orderSn, UserID: userID, OpenID: req.OpenID, ProductType: req.ProductType, ProductID: &productID, Amount: finalAmount, Description: &description, Status: &status, ReferrerID: referrerID, ReferralCode: &req.ReferralCode, } if err := db.Create(&order).Error; err != nil { // 订单创建失败,但不中断支付流程 fmt.Printf("[MiniprogramPay] 插入订单失败: %v\n", err) } attach := fmt.Sprintf(`{"productType":"%s","productId":"%s","userId":"%s"}`, req.ProductType, req.ProductID, userID) ctx := c.Request.Context() prepayID, err := wechat.PayJSAPIOrder(ctx, req.OpenID, orderSn, totalFee, 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": fmt.Sprintf("生成支付参数失败: %v", err)}) return } c.JSON(http.StatusOK, gin.H{ "success": true, "data": map[string]interface{}{ "orderSn": orderSn, "prepayId": prepayID, "payParams": payParams, }, }) } // GET - 查询订单状态 func miniprogramPayGet(c *gin.Context) { orderSn := c.Query("orderSn") if orderSn == "" { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少订单号"}) return } ctx := c.Request.Context() tradeState, transactionID, totalFee, err := wechat.QueryOrderByOutTradeNo(ctx, orderSn) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": true, "data": map[string]interface{}{ "status": "unknown", "orderSn": orderSn, }, }) return } status := "paying" switch tradeState { case "SUCCESS": status = "paid" case "CLOSED", "REVOKED", "PAYERROR": status = "failed" case "REFUND": status = "refunded" } c.JSON(http.StatusOK, gin.H{ "success": true, "data": map[string]interface{}{ "status": status, "orderSn": orderSn, "transactionId": transactionID, "totalFee": totalFee, }, }) } // MiniprogramPayNotify POST /api/miniprogram/pay/notify(v3 支付回调,PowerWeChat 验签解密) func MiniprogramPayNotify(c *gin.Context) { resp, err := wechat.HandlePayNotify(c.Request, func(orderSn, transactionID string, totalFee int, attachStr, openID string) error { totalAmount := float64(totalFee) / 100 fmt.Printf("[PayNotify] 支付成功: orderSn=%s, transactionId=%s, amount=%.2f\n", orderSn, transactionID, totalAmount) var attach struct { ProductType string `json:"productType"` ProductID string `json:"productId"` UserID string `json:"userId"` } if attachStr != "" { _ = json.Unmarshal([]byte(attachStr), &attach) } db := database.DB() buyerUserID := attach.UserID if openID != "" { var user model.User if err := db.Where("open_id = ?", openID).First(&user).Error; err == nil { if attach.UserID != "" && user.ID != attach.UserID { fmt.Printf("[PayNotify] 买家身份校验: attach.userId 与 openId 解析不一致,以 openId 为准\n") } buyerUserID = user.ID } } if buyerUserID == "" && attach.UserID != "" { buyerUserID = attach.UserID } var order model.Order result := db.Where("order_sn = ?", orderSn).First(&order) if result.Error != nil { fmt.Printf("[PayNotify] 订单不存在,补记订单: %s\n", orderSn) productID := attach.ProductID if productID == "" { productID = "fullbook" } productType := attach.ProductType if productType == "" { productType = "unknown" } desc := "支付回调补记订单" status := "paid" now := time.Now() order = model.Order{ ID: orderSn, OrderSN: orderSn, UserID: buyerUserID, OpenID: openID, ProductType: productType, ProductID: &productID, Amount: totalAmount, Description: &desc, Status: &status, TransactionID: &transactionID, PayTime: &now, } db.Create(&order) } else if *order.Status != "paid" { status := "paid" now := time.Now() db.Model(&order).Updates(map[string]interface{}{ "status": status, "transaction_id": transactionID, "pay_time": now, }) fmt.Printf("[PayNotify] 订单状态已更新为已支付: %s\n", orderSn) } else { fmt.Printf("[PayNotify] 订单已支付,跳过更新: %s\n", orderSn) } if buyerUserID != "" && attach.ProductType != "" { if attach.ProductType == "fullbook" { db.Model(&model.User{}).Where("id = ?", buyerUserID).Update("has_full_book", true) fmt.Printf("[PayNotify] 用户已购全书: %s\n", buyerUserID) } else if attach.ProductType == "section" && attach.ProductID != "" { var count int64 db.Model(&model.Order{}).Where( "user_id = ? AND product_type = 'section' AND product_id = ? AND status = 'paid' AND order_sn != ?", buyerUserID, attach.ProductID, orderSn, ).Count(&count) if count == 0 { fmt.Printf("[PayNotify] 用户首次购买章节: %s - %s\n", buyerUserID, attach.ProductID) } else { fmt.Printf("[PayNotify] 用户已有该章节的其他已支付订单: %s - %s\n", buyerUserID, attach.ProductID) } } productID := attach.ProductID if productID == "" { productID = "fullbook" } db.Where( "user_id = ? AND product_type = ? AND product_id = ? AND status = 'created' AND order_sn != ?", buyerUserID, attach.ProductType, productID, orderSn, ).Delete(&model.Order{}) processReferralCommission(db, buyerUserID, totalAmount, orderSn) } return nil }) if err != nil { fmt.Printf("[PayNotify] 处理回调失败: %v\n", err) c.String(http.StatusOK, failResponse()) return } defer resp.Body.Close() for k, v := range resp.Header { if len(v) > 0 { c.Header(k, v[0]) } } c.Status(resp.StatusCode) io.Copy(c.Writer, resp.Body) } // 处理分销佣金 func processReferralCommission(db *gorm.DB, buyerUserID string, amount float64, orderSn string) { // 获取分成配置,默认 90% distributorShare := 0.9 var cfg model.SystemConfig if err := db.Where("config_key = ?", "referral_config").First(&cfg).Error; err == nil { var config map[string]interface{} if err := json.Unmarshal(cfg.ConfigValue, &config); err == nil { if share, ok := config["distributorShare"].(float64); ok { distributorShare = share / 100 } } } // 查找有效推广绑定 type Binding struct { ID int `gorm:"column:id"` ReferrerID string `gorm:"column:referrer_id"` ExpiryDate time.Time `gorm:"column:expiry_date"` PurchaseCount int `gorm:"column:purchase_count"` TotalCommission float64 `gorm:"column:total_commission"` } var binding Binding err := db.Raw(` SELECT id, referrer_id, expiry_date, purchase_count, total_commission FROM referral_bindings WHERE referee_id = ? AND status = 'active' ORDER BY binding_date DESC LIMIT 1 `, buyerUserID).Scan(&binding).Error if err != nil { fmt.Printf("[PayNotify] 用户无有效推广绑定,跳过分佣: %s\n", buyerUserID) return } // 检查是否过期 if time.Now().After(binding.ExpiryDate) { fmt.Printf("[PayNotify] 绑定已过期,跳过分佣: %s\n", buyerUserID) return } // 计算佣金 commission := amount * distributorShare newPurchaseCount := binding.PurchaseCount + 1 newTotalCommission := binding.TotalCommission + commission fmt.Printf("[PayNotify] 处理分佣: referrerId=%s, amount=%.2f, commission=%.2f, shareRate=%.0f%%\n", binding.ReferrerID, amount, commission, distributorShare*100) // 更新推广者的待结算收益 db.Model(&model.User{}).Where("id = ?", binding.ReferrerID). Update("pending_earnings", db.Raw("pending_earnings + ?", commission)) // 更新绑定记录 db.Exec(` UPDATE referral_bindings SET last_purchase_date = NOW(), purchase_count = purchase_count + 1, total_commission = total_commission + ? WHERE id = ? `, commission, binding.ID) fmt.Printf("[PayNotify] 分佣完成: 推广者 %s 获得 %.2f 元(第 %d 次购买,累计 %.2f 元)\n", binding.ReferrerID, commission, newPurchaseCount, newTotalCommission) } // 微信支付回调响应 func successResponse() string { return `` } func failResponse() string { return `` } // MiniprogramPhone POST /api/miniprogram/phone func MiniprogramPhone(c *gin.Context) { var req struct { Code string `json:"code" binding:"required"` UserID string `json:"userId"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少code参数"}) return } // 获取手机号 phoneNumber, countryCode, err := wechat.GetPhoneNumber(req.Code) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "获取手机号失败", "error": err.Error(), }) return } // 如果提供了 userId,更新到数据库 if req.UserID != "" { db := database.DB() db.Model(&model.User{}).Where("id = ?", req.UserID).Update("phone", phoneNumber) fmt.Printf("[MiniprogramPhone] 手机号已绑定到用户: %s\n", req.UserID) } c.JSON(http.StatusOK, gin.H{ "success": true, "phoneNumber": phoneNumber, "countryCode": countryCode, }) } // MiniprogramQrcode POST /api/miniprogram/qrcode func MiniprogramQrcode(c *gin.Context) { var req struct { Scene string `json:"scene"` Page string `json:"page"` Width int `json:"width"` ChapterID string `json:"chapterId"` UserID string `json:"userId"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"}) return } // 构建 scene 参数 scene := req.Scene if scene == "" { var parts []string if req.UserID != "" { userId := req.UserID if len(userId) > 15 { userId = userId[:15] } parts = append(parts, fmt.Sprintf("ref=%s", userId)) } if req.ChapterID != "" { parts = append(parts, fmt.Sprintf("ch=%s", req.ChapterID)) } if len(parts) == 0 { scene = "soul" } else { scene = strings.Join(parts, "&") } } page := req.Page if page == "" { page = "pages/index/index" } width := req.Width if width == 0 { width = 280 } fmt.Printf("[MiniprogramQrcode] 生成小程序码, scene=%s\n", scene) // 生成小程序码 imageData, err := wechat.GenerateMiniProgramCode(scene, page, width) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, "error": fmt.Sprintf("生成小程序码失败: %v", err), }) return } // 转换为 base64 base64Image := fmt.Sprintf("data:image/png;base64,%s", base64Encode(imageData)) c.JSON(http.StatusOK, gin.H{ "success": true, "image": base64Image, "scene": scene, }) } // base64 编码 func base64Encode(data []byte) string { const base64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" var result strings.Builder for i := 0; i < len(data); i += 3 { b1, b2, b3 := data[i], byte(0), byte(0) if i+1 < len(data) { b2 = data[i+1] } if i+2 < len(data) { b3 = data[i+2] } result.WriteByte(base64Table[b1>>2]) result.WriteByte(base64Table[((b1&0x03)<<4)|(b2>>4)]) if i+1 < len(data) { result.WriteByte(base64Table[((b2&0x0F)<<2)|(b3>>6)]) } else { result.WriteByte('=') } if i+2 < len(data) { result.WriteByte(base64Table[b3&0x3F]) } else { result.WriteByte('=') } } return result.String() }