716 lines
21 KiB
Go
716 lines
21 KiB
Go
package handler
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strconv"
|
||
"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 表查询真实购买记录(Pluck 替代 Raw)
|
||
var purchasedSections []string
|
||
db.Model(&model.Order{}).Where("user_id = ? AND status = ? AND product_type = ?", user.ID, "paid", "section").
|
||
Distinct("product_id").Pluck("product_id", &purchasedSections)
|
||
// 过滤空字符串
|
||
filtered := make([]string, 0, len(purchasedSections))
|
||
for _, s := range purchasedSections {
|
||
if s != "" {
|
||
filtered = append(filtered, s)
|
||
}
|
||
}
|
||
purchasedSections = filtered
|
||
|
||
// 构建返回的用户对象
|
||
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,gte=0"`
|
||
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()
|
||
|
||
// 查询用户的有效推荐人(先查 binding,再查 referralCode)
|
||
var referrerID *string
|
||
if req.UserID != "" {
|
||
var refID string
|
||
err := db.Model(&model.ReferralBinding{}).Where("referee_id = ? AND status = ? AND expiry_date > ?", req.UserID, "active", time.Now()).
|
||
Order("binding_date DESC").Limit(1).Pluck("referrer_id", &refID).Error
|
||
if err == nil && refID != "" {
|
||
referrerID = &refID
|
||
}
|
||
}
|
||
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
|
||
}
|
||
}
|
||
|
||
// 有推荐人时应用好友优惠(无论是 binding 还是 referralCode)
|
||
finalAmount := req.Amount
|
||
if referrerID != nil {
|
||
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 if req.ProductType == "match" {
|
||
description = "购买匹配次数"
|
||
} else {
|
||
description = fmt.Sprintf("章节购买-%s", req.ProductID)
|
||
}
|
||
}
|
||
|
||
// 获取客户端 IP
|
||
clientIP := c.ClientIP()
|
||
if clientIP == "" {
|
||
clientIP = "127.0.0.1"
|
||
}
|
||
|
||
// 插入订单到数据库
|
||
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"
|
||
// 若微信已支付,主动同步到本地 orders(不等 PayNotify),便于购买次数即时生效
|
||
db := database.DB()
|
||
var order model.Order
|
||
if err := db.Where("order_sn = ?", orderSn).First(&order).Error; err == nil && order.Status != nil && *order.Status != "paid" {
|
||
now := time.Now()
|
||
db.Model(&order).Updates(map[string]interface{}{
|
||
"status": "paid",
|
||
"transaction_id": transactionID,
|
||
"pay_time": now,
|
||
})
|
||
fmt.Printf("[PayGet] 主动同步订单已支付: %s\n", orderSn)
|
||
}
|
||
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()
|
||
return db.Transaction(func(tx *gorm.DB) error {
|
||
buyerUserID := attach.UserID
|
||
if openID != "" {
|
||
var user model.User
|
||
if err := tx.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 := tx.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,
|
||
}
|
||
if err := tx.Create(&order).Error; err != nil {
|
||
fmt.Printf("[PayNotify] 补记订单失败: %s, err=%v\n", orderSn, err)
|
||
return fmt.Errorf("create order: %w", err)
|
||
}
|
||
} else if *order.Status != "paid" {
|
||
status := "paid"
|
||
now := time.Now()
|
||
if err := tx.Model(&order).Updates(map[string]interface{}{
|
||
"status": status,
|
||
"transaction_id": transactionID,
|
||
"pay_time": now,
|
||
}).Error; err != nil {
|
||
fmt.Printf("[PayNotify] 更新订单状态失败: %s, err=%v\n", orderSn, err)
|
||
return fmt.Errorf("update order: %w", err)
|
||
}
|
||
fmt.Printf("[PayNotify] 订单状态已更新为已支付: %s\n", orderSn)
|
||
} else {
|
||
fmt.Printf("[PayNotify] 订单已支付,跳过更新: %s\n", orderSn)
|
||
}
|
||
|
||
if buyerUserID != "" && attach.ProductType != "" {
|
||
if attach.ProductType == "fullbook" {
|
||
tx.Model(&model.User{}).Where("id = ?", buyerUserID).Update("has_full_book", true)
|
||
fmt.Printf("[PayNotify] 用户已购全书: %s\n", buyerUserID)
|
||
} else if attach.ProductType == "match" {
|
||
fmt.Printf("[PayNotify] 用户购买匹配次数: %s,订单 %s\n", buyerUserID, orderSn)
|
||
} else if attach.ProductType == "section" && attach.ProductID != "" {
|
||
var count int64
|
||
tx.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"
|
||
}
|
||
tx.Where(
|
||
"user_id = ? AND product_type = ? AND product_id = ? AND status = 'created' AND order_sn != ?",
|
||
buyerUserID, attach.ProductType, productID, orderSn,
|
||
).Delete(&model.Order{})
|
||
processReferralCommission(tx, buyerUserID, totalAmount, orderSn, &order)
|
||
}
|
||
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)
|
||
}
|
||
|
||
// 处理分销佣金
|
||
// amount 为实付金额(若有好友优惠则已打折);order 用于判断是否有推荐人从而反推原价
|
||
func processReferralCommission(db *gorm.DB, buyerUserID string, amount float64, orderSn string, order *model.Order) {
|
||
// 获取分成配置,默认 90%;好友优惠用于反推原价(佣金按原价计算)
|
||
distributorShare := 0.9
|
||
userDiscount := 0.0
|
||
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
|
||
}
|
||
if disc, ok := config["userDiscount"].(float64); ok {
|
||
userDiscount = disc / 100
|
||
}
|
||
}
|
||
}
|
||
// 佣金按原价计算:若有推荐人则实付已打折,反推原价 = amount / (1 - userDiscount)
|
||
commissionBase := amount
|
||
if order != nil && userDiscount > 0 && (order.ReferrerID != nil && *order.ReferrerID != "" || order.ReferralCode != nil && *order.ReferralCode != "") {
|
||
if (1 - userDiscount) > 0 {
|
||
commissionBase = amount / (1 - userDiscount)
|
||
}
|
||
}
|
||
|
||
// 查找有效推广绑定(GORM 替代 Raw)
|
||
var binding model.ReferralBinding
|
||
err := db.Where("referee_id = ? AND status = ?", buyerUserID, "active").Order("binding_date DESC").First(&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 := commissionBase * distributorShare
|
||
purchaseCount := 0
|
||
if binding.PurchaseCount != nil {
|
||
purchaseCount = *binding.PurchaseCount
|
||
}
|
||
totalCommission := 0.0
|
||
if binding.TotalCommission != nil {
|
||
totalCommission = *binding.TotalCommission
|
||
}
|
||
newPurchaseCount := purchaseCount + 1
|
||
newTotalCommission := 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", gorm.Expr("COALESCE(pending_earnings, 0) + ?", commission))
|
||
|
||
// 更新绑定记录(COALESCE 避免 total_commission 为 NULL 时 NULL+?=NULL)
|
||
db.Model(&model.ReferralBinding{}).Where("id = ?", binding.ID).Updates(map[string]interface{}{
|
||
"last_purchase_date": time.Now(),
|
||
"purchase_count": gorm.Expr("COALESCE(purchase_count, 0) + 1"),
|
||
"total_commission": gorm.Expr("COALESCE(total_commission, 0) + ?", commission),
|
||
})
|
||
|
||
fmt.Printf("[PayNotify] 分佣完成: 推广者 %s 获得 %.2f 元(第 %d 次购买,累计 %.2f 元)\n",
|
||
binding.ReferrerID, commission, newPurchaseCount, newTotalCommission)
|
||
}
|
||
|
||
// 微信支付回调响应
|
||
func successResponse() string {
|
||
return `<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>`
|
||
}
|
||
|
||
func failResponse() string {
|
||
return `<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[ERROR]]></return_msg></xml>`
|
||
}
|
||
|
||
// 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,
|
||
})
|
||
}
|
||
|
||
// MiniprogramQrcodeImage GET /api/miniprogram/qrcode/image?scene=xxx&page=xxx&width=280
|
||
// 直接返回 image/png,供小程序 wx.downloadFile 使用,便于开发工具与真机统一用 tempFilePath 绘制
|
||
func MiniprogramQrcodeImage(c *gin.Context) {
|
||
scene := c.Query("scene")
|
||
if scene == "" {
|
||
scene = "soul"
|
||
}
|
||
page := c.DefaultQuery("page", "pages/read/read")
|
||
width, _ := strconv.Atoi(c.DefaultQuery("width", "280"))
|
||
if width <= 0 {
|
||
width = 280
|
||
}
|
||
imageData, err := wechat.GenerateMiniProgramCode(scene, page, width)
|
||
if err != nil {
|
||
c.JSON(http.StatusBadRequest, gin.H{
|
||
"success": false,
|
||
"error": fmt.Sprintf("生成小程序码失败: %v", err),
|
||
})
|
||
return
|
||
}
|
||
c.Header("Content-Type", "image/png")
|
||
c.Data(http.StatusOK, "image/png", imageData)
|
||
}
|
||
|
||
// 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()
|
||
}
|