feat: 代付做完了

This commit is contained in:
Alex-larget
2026-03-18 12:40:51 +08:00
parent f276595ad6
commit 1fa20756a8
51 changed files with 2380 additions and 791 deletions

View File

@@ -274,11 +274,11 @@ func AdminTrackStats(c *gin.Context) {
}
// AdminBalanceSummary GET /api/admin/balance/summary
// 汇总代付金额product_type=gift_pay 的已支付订单金额),用于 Dashboard 显示「含代付 ¥xx」
// 汇总代付金额product_type 为 gift_pay 或 gift_pay_batch 的已支付订单),用于 Dashboard 显示「含代付 ¥xx」
func AdminBalanceSummary(c *gin.Context) {
db := database.DB()
var totalGifted float64
db.Model(&model.Order{}).Where("product_type = ? AND status IN ?", "gift_pay", paidStatuses).
db.Model(&model.Order{}).Where("product_type IN ? AND status IN ?", []string{"gift_pay", "gift_pay_batch"}, paidStatuses).
Select("COALESCE(SUM(amount), 0)").Scan(&totalGifted)
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"totalGifted": totalGifted}})
}

View File

@@ -3,6 +3,7 @@ package handler
import (
"encoding/json"
"fmt"
"math"
"net/http"
"strconv"
"strings"
@@ -14,9 +15,24 @@ import (
"soul-api/internal/wechat"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
const giftPayExpireHours = 24
const wechatAttachMaxBytes = 128
// truncateStr 截断字符串至最多 n 字节UTF-8 安全)
func truncateStr(s string, n int) string {
b := []byte(s)
if len(b) <= n {
return s
}
b = b[:n]
for len(b) > 0 && b[len(b)-1] >= 0x80 {
b = b[:len(b)-1]
}
return string(b)
}
// giftPayPreviewContent 取内容前 20%,用于代付页营销展示
func giftPayPreviewContent(content string) string {
@@ -38,17 +54,23 @@ func giftPayPreviewContent(content string) string {
return string(runes[:limit]) + "……"
}
// GiftPayCreate POST /api/miniprogram/gift-pay/create 创建代付请求
// 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"`
Quantity int `json:"quantity"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少参数"})
return
}
quantity := req.Quantity
if quantity < 1 {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "发放份数须为正整数"})
return
}
db := database.DB()
// 校验发起人
@@ -70,11 +92,15 @@ func GiftPayCreate(c *gin.Context) {
productID = "fullbook"
}
}
amount, priceErr := getStandardPrice(db, req.ProductType, productID)
unitPrice, priceErr := getStandardPrice(db, req.ProductType, productID)
if priceErr != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": priceErr.Error()})
return
}
amount := unitPrice * float64(quantity)
if amount < 0.01 {
amount = 0.01
}
// 发起人若有推荐人绑定,享受好友优惠
var referrerID *string
var binding struct {
@@ -91,7 +117,11 @@ func GiftPayCreate(c *gin.Context) {
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)
unitPrice = unitPrice * (1 - userDiscount/100)
if unitPrice < 0.01 {
unitPrice = 0.01
}
amount = unitPrice * float64(quantity)
if amount < 0.01 {
amount = 0.01
}
@@ -101,28 +131,7 @@ func GiftPayCreate(c *gin.Context) {
}
_ = 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 := ""
@@ -155,33 +164,195 @@ func GiftPayCreate(c *gin.Context) {
ProductType: req.ProductType,
ProductID: productID,
Amount: amount,
Description: desc,
Status: "pending",
Description: desc,
Status: "pending_pay",
Quantity: quantity,
RedeemedCount: 0,
ExpireAt: expireAt,
}
if err := db.Create(&gpr).Error; err != nil {
fmt.Printf("[GiftPayCreate] 创建失败: %v\n", err)
// 若报 unknown column 'quantity' 等,需执行 soul-api/scripts/add-gift-pay-quantity.sql
c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建失败"})
return
}
sectionTitle := desc
if req.ProductType == "section" && productID != "" {
var ch model.Chapter
if err := db.Select("section_title").Where("id = ?", productID).First(&ch).Error; err == nil && ch.SectionTitle != "" {
sectionTitle = ch.SectionTitle
}
}
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),
"success": true,
"requestSn": requestSN,
"path": path,
"amount": amount,
"quantity": quantity,
"sectionTitle": sectionTitle,
"expireAt": expireAt.Format(time.RFC3339),
})
}
// GiftPayDetail GET /api/miniprogram/gift-pay/detail?requestSn=xxx 代付详情(代付人用
// GiftPayInitiatorPay POST /api/miniprogram/gift-pay/initiator-pay 发起人支付(改造后:我帮别人付款
func GiftPayInitiatorPay(c *gin.Context) {
var req struct {
RequestSn string `json:"requestSn" binding:"required"`
OpenID string `json:"openId" 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 = ? AND status = ?", req.RequestSn, "pending_pay").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 != gpr.InitiatorUserID {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "仅发起人可支付"})
return
}
var initiator model.User
if err := db.Where("open_id = ?", req.OpenID).First(&initiator).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请先登录"})
return
}
if initiator.ID != gpr.InitiatorUserID {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "登录用户与发起人不一致"})
return
}
orderSn := wechat.GenerateOrderSn()
status := "created"
pm := "wechat"
productType := "gift_pay_batch"
productID := gpr.ProductID
desc := fmt.Sprintf("代付分享 - %s × %d 份", gpr.Description, gpr.Quantity)
gprID := gpr.ID
order := model.Order{
ID: orderSn,
OrderSN: orderSn,
UserID: gpr.InitiatorUserID,
OpenID: req.OpenID,
ProductType: productType,
ProductID: &productID,
Amount: gpr.Amount,
Description: &desc,
Status: &status,
PaymentMethod: &pm,
GiftPayRequestID: &gprID,
PayerUserID: &gpr.InitiatorUserID,
}
if err := db.Create(&order).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建订单失败"})
return
}
// 微信 attach 最大 128 字节发起人付订单已存在PayNotify 从 order 取 giftPayRequestSn
attach := `{"ip":1}`
totalFee := int(math.Round(gpr.Amount * 100)) // 与正常章节支付一致,避免浮点精度导致分额错误
if totalFee < 1 {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "金额异常,无法发起支付"})
return
}
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
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"orderSn": orderSn,
"prepayId": prepayID,
"payParams": payParams,
},
})
}
// GiftPayDetail GET /api/miniprogram/gift-pay/detail?requestSn=xxx&userId= 或 ?sectionId=xxx&userId= 预览态
func GiftPayDetail(c *gin.Context) {
requestSn := strings.TrimSpace(c.Query("requestSn"))
sectionId := strings.TrimSpace(c.Query("sectionId"))
callerUserID := strings.TrimSpace(c.Query("userId"))
db := database.DB()
// 预览态:无 requestSn 有 sectionId返回文章信息供创建代付
if requestSn == "" && sectionId != "" {
if callerUserID == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请先登录"})
return
}
unitPrice, priceErr := getStandardPrice(db, "section", sectionId)
if priceErr != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": priceErr.Error()})
return
}
// 发起人若有推荐人,享受折扣(与 create 一致)
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
`, callerUserID).Scan(&binding).Error; err == nil && 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 {
unitPrice = unitPrice * (1 - userDiscount/100)
if unitPrice < 0.01 {
unitPrice = 0.01
}
}
}
}
}
var ch model.Chapter
sectionTitle := ""
productMid := 0
if err := db.Select("section_title", "mid").Where("id = ?", sectionId).First(&ch).Error; err == nil {
sectionTitle = ch.SectionTitle
productMid = ch.MID
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"mode": "create",
"sectionId": sectionId,
"sectionTitle": sectionTitle,
"productMid": productMid,
"unitPrice": unitPrice,
"isInitiator": true,
"action": "create",
})
return
}
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 {
@@ -189,7 +360,7 @@ func GiftPayDetail(c *gin.Context) {
return
}
if gpr.Status != "pending" {
if gpr.Status != "pending" && gpr.Status != "pending_pay" && gpr.Status != "paid" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "该代付已处理"})
return
}
@@ -198,6 +369,7 @@ func GiftPayDetail(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "代付已过期"})
return
}
isInitiator := callerUserID != "" && callerUserID == gpr.InitiatorUserID
// 发起人昵称与头像(完整展示)
var initiator model.User
@@ -212,41 +384,103 @@ func GiftPayDetail(c *gin.Context) {
}
}
// 营销:章节类型时返回标题和内容预览,吸引代付人
// 营销:章节类型时返回标题和内容预览
sectionTitle := gpr.Description
contentPreview := ""
productMid := 0
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 err := db.Select("section_title", "content", "mid").Where("id = ?", gpr.ProductID).First(&ch).Error; err == nil {
if ch.SectionTitle != "" {
sectionTitle = ch.SectionTitle
}
contentPreview = giftPayPreviewContent(ch.Content)
productMid = ch.MID
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"requestSn": gpr.RequestSN,
"productType": gpr.ProductType,
// 领取记录(发起人查看)
var redeemList []gin.H
if isInitiator {
var orders []model.Order
db.Where("gift_pay_request_id = ? AND product_type = ? AND status = ?",
gpr.ID, "section", "paid").Order("created_at ASC").Find(&orders)
for _, o := range orders {
if o.UserID == "" {
continue
}
var u model.User
nickname := "用户"
avatar := ""
if err := db.Where("id = ?", o.UserID).Select("nickname", "avatar").First(&u).Error; err == nil {
if u.Nickname != nil && *u.Nickname != "" {
nickname = *u.Nickname
}
if u.Avatar != nil && *u.Avatar != "" {
avatar = *u.Avatar
}
}
redeemList = append(redeemList, gin.H{"userId": o.UserID, "nickname": nickname, "avatar": avatar, "redeemAt": o.CreatedAt.Format("2006-01-02 15:04")})
}
}
// action: pay=发起人待支付 | share=发起人已支付可分享 | redeem=好友可领取 | wait=好友待发起人支付
action := ""
if isInitiator {
if gpr.Status == "pending_pay" {
action = "pay"
} else if gpr.Status == "paid" {
action = "share"
} else if gpr.Status == "pending" {
action = "share" // 旧版:待好友付
}
} else {
if gpr.Status == "pending_pay" || gpr.Status == "pending" {
action = "wait"
} else if gpr.Status == "paid" {
// 好友已领取过:返回 alreadyRedeemed供前端直接跳转 read
var existCnt int64
db.Model(&model.Order{}).Where(
"user_id = ? AND gift_pay_request_id = ? AND product_type = ? AND status = ?",
callerUserID, gpr.ID, "section", "paid",
).Count(&existCnt)
if existCnt > 0 {
action = "alreadyRedeemed"
} else {
action = "redeem"
}
}
}
resp := 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,
"initiatorAvatar": initiatorAvatar,
"initiatorUserId": gpr.InitiatorUserID,
"expireAt": gpr.ExpireAt.Format(time.RFC3339),
})
"productMid": productMid,
"amount": gpr.Amount,
"quantity": gpr.Quantity,
"redeemedCount": gpr.RedeemedCount,
"redeemList": redeemList,
"description": gpr.Description,
"sectionTitle": sectionTitle,
"contentPreview": contentPreview,
"initiatorNickname": nickname,
"initiatorAvatar": initiatorAvatar,
"initiatorUserId": gpr.InitiatorUserID,
"isInitiator": isInitiator,
"action": action,
"status": gpr.Status,
"expireAt": gpr.ExpireAt.Format(time.RFC3339),
}
c.JSON(http.StatusOK, resp)
}
// GiftPayPay POST /api/miniprogram/gift-pay/pay 代付人发起支付
func GiftPayPay(c *gin.Context) {
// GiftPayRedeem POST /api/miniprogram/gift-pay/redeem 好友领取(改造后:免费获得章节)
func GiftPayRedeem(c *gin.Context) {
var req struct {
RequestSn string `json:"requestSn" binding:"required"`
OpenID string `json:"openId" binding:"required"`
UserID string `json:"userId"` // 代付人ID用于校验不能自己付
UserID string `json:"userId" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少参数"})
@@ -255,97 +489,74 @@ func GiftPayPay(c *gin.Context) {
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": "代付请求不存在或已处理"})
if err := db.Where("request_sn = ? AND status = ?", req.RequestSn, "paid").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": "代付已过期"})
if req.UserID == gpr.InitiatorUserID {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "发起人无需领取"})
return
}
if gpr.RedeemedCount >= gpr.Quantity {
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": "不能为自己代付"})
// 同一用户同一 requestSn 只能领一次
var existCnt int64
db.Model(&model.Order{}).Where(
"user_id = ? AND gift_pay_request_id = ? AND product_type = ? AND status = ?",
req.UserID, gpr.ID, "section", "paid",
).Count(&existCnt)
if existCnt > 0 {
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
}
// 创建订单(归属发起人,记录代付信息)
// 创建好友订单productType=section, status=paid, paymentMethod=gift_pay
orderSn := wechat.GenerateOrderSn()
status := "created"
pm := "wechat"
status := "paid"
pm := "gift_pay"
productID := gpr.ProductID
desc := gpr.Description
desc := fmt.Sprintf("代付领取 - %s", gpr.Description)
gprID := gpr.ID
payerID := payer.ID
amount := 0.0
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,
ID: orderSn,
OrderSN: orderSn,
UserID: req.UserID,
ProductType: "section",
ProductID: &productID,
Amount: amount,
Description: &desc,
Status: &status,
PaymentMethod: &pm,
GiftPayRequestID: &gprID,
PayerUserID: &gpr.InitiatorUserID,
}
if err := db.Create(&order).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建订单失败"})
return
}
// 唤起微信支付attach 中 userId=发起人giftPayRequestSn=请求号
// 注意必须使用代付人payer的 openId微信会在代付人手机上唤起支付
attach := fmt.Sprintf(`{"productType":"%s","productId":"%s","userId":"%s","giftPayRequestSn":"%s"}`,
gpr.ProductType, gpr.ProductID, gpr.InitiatorUserID, gpr.RequestSN)
totalFee := int(gpr.Amount * 100)
if totalFee < 1 {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "金额异常,无法发起支付"})
return
}
ctx := c.Request.Context()
prepayID, err := wechat.PayJSAPIOrder(ctx, req.OpenID, orderSn, totalFee, "代付-"+gpr.Description, attach)
if err != nil {
openIDMask := req.OpenID
if len(openIDMask) > 8 {
openIDMask = openIDMask[:4] + "***" + openIDMask[len(openIDMask)-4:]
if err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&order).Error; err != nil {
return err
}
fmt.Printf("[GiftPayPay] 微信统一下单失败: orderSn=%s openId=%s payerId=%s initiatorId=%s err=%v\n",
orderSn, openIDMask, payer.ID, gpr.InitiatorUserID, err)
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 tx.Model(&model.GiftPayRequest{}).Where("id = ?", gpr.ID).
Update("redeemed_count", gorm.Expr("redeemed_count + 1")).Error
}); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "领取失败"})
return
}
// 预占:更新请求状态为 paying可选防并发
// 简化不预占PayNotify 时再更新
_ = amount
productMid := 0
if gpr.ProductType == "section" && gpr.ProductID != "" {
var ch model.Chapter
if err := db.Select("mid").Where("id = ?", gpr.ProductID).First(&ch).Error; err == nil {
productMid = ch.MID
}
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"orderSn": orderSn,
"prepayId": prepayID,
"payParams": payParams,
},
"success": true,
"sectionId": gpr.ProductID,
"sectionMid": productMid,
})
}
@@ -370,7 +581,7 @@ func GiftPayCancel(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "无权取消"})
return
}
if gpr.Status != "pending" {
if gpr.Status != "pending" && gpr.Status != "pending_pay" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "该代付已处理"})
return
}
@@ -379,7 +590,7 @@ func GiftPayCancel(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": true, "message": "已取消"})
}
// GiftPayMyRequests GET /api/miniprogram/gift-pay/my-requests?userId= 我发起的
// GiftPayMyRequests GET /api/miniprogram/gift-pay/my-requests?userId= 我发起的(含领取记录)
func GiftPayMyRequests(c *gin.Context) {
userID := c.Query("userId")
if userID == "" {
@@ -389,45 +600,45 @@ func GiftPayMyRequests(c *gin.Context) {
db := database.DB()
var list []model.GiftPayRequest
db.Where("initiator_user_id = ?", userID).Order("created_at DESC").Limit(50).Find(&list)
db.Where("initiator_user_id = ? AND status != ?", userID, "cancelled").Order("created_at DESC").Limit(50).Find(&list)
out := make([]gin.H, 0, len(list))
for _, r := range list {
// 领取记录orders 表 gift_pay_request_id + product_type=section + payment_method=gift_pay
var redeemList []gin.H
var orders []model.Order
db.Where("gift_pay_request_id = ? AND product_type = ? AND status = ?",
r.ID, "section", "paid").Order("created_at ASC").Find(&orders) // 好友领取订单
for _, o := range orders {
if o.UserID == "" {
continue
}
var u model.User
nickname := "用户"
avatar := ""
if err := db.Where("id = ?", o.UserID).Select("nickname", "avatar").First(&u).Error; err == nil {
if u.Nickname != nil && *u.Nickname != "" {
nickname = *u.Nickname
}
if u.Avatar != nil && *u.Avatar != "" {
avatar = *u.Avatar
}
}
redeemAt := o.CreatedAt.Format("2006-01-02 15:04")
redeemList = append(redeemList, gin.H{"userId": o.UserID, "nickname": nickname, "avatar": avatar, "redeemAt": redeemAt})
}
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),
"requestSn": r.RequestSN,
"productType": r.ProductType,
"productId": r.ProductID,
"amount": r.Amount,
"quantity": r.Quantity,
"redeemedCount": r.RedeemedCount,
"description": r.Description,
"status": r.Status,
"expireAt": r.ExpireAt.Format(time.RFC3339),
"createdAt": r.CreatedAt.Format(time.RFC3339),
"redeemList": redeemList,
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "list": out})
@@ -493,6 +704,8 @@ func AdminGiftPayRequestsList(c *gin.Context) {
"productType": r.ProductType,
"productId": r.ProductID,
"amount": r.Amount,
"quantity": r.Quantity,
"redeemedCount": r.RedeemedCount,
"description": r.Description,
"status": r.Status,
"payerUserId": r.PayerUserID,

View File

@@ -531,13 +531,38 @@ func MiniprogramPayNotify(c *gin.Context) {
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"`
GiftPayRequestSn string `json:"giftPayRequestSn"`
ProductType string `json:"productType"`
ProductID string `json:"productId"`
UserID string `json:"userId"`
GiftPayRequestSn string `json:"giftPayRequestSn"`
GiftPayInitiatorPay bool `json:"giftPayInitiatorPay"`
PT string `json:"pt"`
PID string `json:"pid"`
UID string `json:"uid"`
SN string `json:"sn"`
IP int `json:"ip"`
}
if attachStr != "" {
_ = json.Unmarshal([]byte(attachStr), &attach)
if attach.ProductType == "" {
if attach.PT == "gpb" {
attach.ProductType = "gift_pay_batch"
} else {
attach.ProductType = attach.PT
}
}
if attach.ProductID == "" {
attach.ProductID = attach.PID
}
if attach.UserID == "" {
attach.UserID = attach.UID
}
if attach.GiftPayRequestSn == "" {
attach.GiftPayRequestSn = attach.SN
}
if attach.IP != 0 {
attach.GiftPayInitiatorPay = true
}
}
db := database.DB()
@@ -605,13 +630,23 @@ func MiniprogramPayNotify(c *gin.Context) {
}
// 代付订单:更新 gift_pay_request、订单 payer_user_id
// 权益归属与分佣:代付时归发起人order.UserID普通订单归 buyerUserID
beneficiaryUserID := buyerUserID
if attach.GiftPayRequestSn != "" && order.UserID != "" {
beneficiaryUserID = order.UserID
fmt.Printf("[PayNotify] 代付订单,权益归属发起人: %s\n", beneficiaryUserID)
// 权益归属与分佣:旧版好友付归发起人;新版发起人付不发放权益(好友领取时再发)
giftPayRequestSn := attach.GiftPayRequestSn
if giftPayRequestSn == "" && order.GiftPayRequestID != nil && *order.GiftPayRequestID != "" {
var gpr model.GiftPayRequest
if err := db.Where("id = ?", *order.GiftPayRequestID).Select("request_sn").First(&gpr).Error; err == nil {
giftPayRequestSn = gpr.RequestSN
}
}
if attach.GiftPayRequestSn != "" {
beneficiaryUserID := buyerUserID
if giftPayRequestSn != "" && order.UserID != "" && !attach.GiftPayInitiatorPay {
beneficiaryUserID = order.UserID
fmt.Printf("[PayNotify] 代付订单(好友付),权益归属发起人: %s\n", beneficiaryUserID)
}
if attach.GiftPayInitiatorPay {
fmt.Printf("[PayNotify] 代付订单(发起人付),不发放权益,好友领取时再发\n")
}
if giftPayRequestSn != "" {
var payerUserID string
if openID != "" {
var payer model.User
@@ -620,7 +655,7 @@ func MiniprogramPayNotify(c *gin.Context) {
db.Model(&order).Update("payer_user_id", payerUserID)
}
}
db.Model(&model.GiftPayRequest{}).Where("request_sn = ?", attach.GiftPayRequestSn).
db.Model(&model.GiftPayRequest{}).Where("request_sn = ?", giftPayRequestSn).
Updates(map[string]interface{}{
"status": "paid",
"payer_user_id": payerUserID,