2026-03-17 11:44:36 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"fmt"
|
2026-03-18 12:40:51 +08:00
|
|
|
|
"math"
|
2026-03-17 11:44:36 +08:00
|
|
|
|
"net/http"
|
2026-03-17 13:17:49 +08:00
|
|
|
|
"strconv"
|
2026-03-17 11:44:36 +08:00
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
2026-03-17 12:15:08 +08:00
|
|
|
|
"unicode/utf8"
|
2026-03-17 11:44:36 +08:00
|
|
|
|
|
|
|
|
|
|
"soul-api/internal/database"
|
|
|
|
|
|
"soul-api/internal/model"
|
|
|
|
|
|
"soul-api/internal/wechat"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2026-03-18 12:40:51 +08:00
|
|
|
|
"gorm.io/gorm"
|
2026-03-17 11:44:36 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const giftPayExpireHours = 24
|
2026-03-18 12:40:51 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-03-17 11:44:36 +08:00
|
|
|
|
|
2026-03-17 12:15:08 +08:00
|
|
|
|
// 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]) + "……"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// GiftPayCreate POST /api/miniprogram/gift-pay/create 创建代付请求(改造后:发起人支付,好友领取)
|
2026-03-17 11:44:36 +08:00
|
|
|
|
func GiftPayCreate(c *gin.Context) {
|
|
|
|
|
|
var req struct {
|
|
|
|
|
|
UserID string `json:"userId" binding:"required"`
|
|
|
|
|
|
ProductType string `json:"productType" binding:"required"`
|
|
|
|
|
|
ProductID string `json:"productId"`
|
2026-03-18 12:40:51 +08:00
|
|
|
|
Quantity int `json:"quantity"`
|
2026-03-17 11:44:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少参数"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-18 12:40:51 +08:00
|
|
|
|
quantity := req.Quantity
|
|
|
|
|
|
if quantity < 1 {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "发放份数须为正整数"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-17 11:44:36 +08:00
|
|
|
|
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"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-18 12:40:51 +08:00
|
|
|
|
unitPrice, priceErr := getStandardPrice(db, req.ProductType, productID)
|
2026-03-17 11:44:36 +08:00
|
|
|
|
if priceErr != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": priceErr.Error()})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-18 12:40:51 +08:00
|
|
|
|
amount := unitPrice * float64(quantity)
|
|
|
|
|
|
if amount < 0.01 {
|
|
|
|
|
|
amount = 0.01
|
|
|
|
|
|
}
|
2026-03-17 11:44:36 +08:00
|
|
|
|
// 发起人若有推荐人绑定,享受好友优惠
|
|
|
|
|
|
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 {
|
2026-03-18 12:40:51 +08:00
|
|
|
|
unitPrice = unitPrice * (1 - userDiscount/100)
|
|
|
|
|
|
if unitPrice < 0.01 {
|
|
|
|
|
|
unitPrice = 0.01
|
|
|
|
|
|
}
|
|
|
|
|
|
amount = unitPrice * float64(quantity)
|
2026-03-17 11:44:36 +08:00
|
|
|
|
if amount < 0.01 {
|
|
|
|
|
|
amount = 0.01
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ = referrerID // 分佣在 PayNotify 时按发起人计算
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// 改造后:发起人帮别人买,发起人自己可已拥有,不再校验
|
2026-03-17 11:44:36 +08:00
|
|
|
|
|
|
|
|
|
|
// 描述
|
|
|
|
|
|
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,
|
2026-03-18 12:40:51 +08:00
|
|
|
|
Description: desc,
|
|
|
|
|
|
Status: "pending_pay",
|
|
|
|
|
|
Quantity: quantity,
|
|
|
|
|
|
RedeemedCount: 0,
|
2026-03-17 11:44:36 +08:00
|
|
|
|
ExpireAt: expireAt,
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := db.Create(&gpr).Error; err != nil {
|
2026-03-18 12:40:51 +08:00
|
|
|
|
fmt.Printf("[GiftPayCreate] 创建失败: %v\n", err)
|
|
|
|
|
|
// 若报 unknown column 'quantity' 等,需执行 soul-api/scripts/add-gift-pay-quantity.sql
|
2026-03-17 11:44:36 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "创建失败"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-17 11:44:36 +08:00
|
|
|
|
path := fmt.Sprintf("pages/gift-pay/detail?requestSn=%s", requestSN)
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
2026-03-18 12:40:51 +08:00
|
|
|
|
"success": true,
|
|
|
|
|
|
"requestSn": requestSN,
|
|
|
|
|
|
"path": path,
|
|
|
|
|
|
"amount": amount,
|
|
|
|
|
|
"quantity": quantity,
|
|
|
|
|
|
"sectionTitle": sectionTitle,
|
|
|
|
|
|
"expireAt": expireAt.Format(time.RFC3339),
|
2026-03-17 11:44:36 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// 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= 预览态
|
2026-03-17 11:44:36 +08:00
|
|
|
|
func GiftPayDetail(c *gin.Context) {
|
|
|
|
|
|
requestSn := strings.TrimSpace(c.Query("requestSn"))
|
2026-03-18 12:40:51 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 11:44:36 +08:00
|
|
|
|
if requestSn == "" {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少代付请求号"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 20:33:50 +08:00
|
|
|
|
if gpr.Status != "pending" && gpr.Status != "pending_pay" && gpr.Status != "paid" && gpr.Status != "refunded" {
|
2026-03-17 11:44:36 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-03-18 12:40:51 +08:00
|
|
|
|
isInitiator := callerUserID != "" && callerUserID == gpr.InitiatorUserID
|
2026-03-17 11:44:36 +08:00
|
|
|
|
|
2026-03-17 15:17:33 +08:00
|
|
|
|
// 发起人昵称与头像(完整展示)
|
2026-03-17 11:44:36 +08:00
|
|
|
|
var initiator model.User
|
|
|
|
|
|
nickname := "好友"
|
2026-03-17 15:17:33 +08:00
|
|
|
|
initiatorAvatar := ""
|
|
|
|
|
|
if err := db.Where("id = ?", gpr.InitiatorUserID).Select("nickname", "avatar").First(&initiator).Error; err == nil {
|
|
|
|
|
|
if initiator.Nickname != nil && *initiator.Nickname != "" {
|
|
|
|
|
|
nickname = *initiator.Nickname
|
|
|
|
|
|
}
|
|
|
|
|
|
if initiator.Avatar != nil && *initiator.Avatar != "" {
|
2026-03-19 18:26:45 +08:00
|
|
|
|
initiatorAvatar = resolveAvatarURL(*initiator.Avatar)
|
2026-03-17 11:44:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// 营销:章节类型时返回标题和内容预览
|
2026-03-17 12:15:08 +08:00
|
|
|
|
sectionTitle := gpr.Description
|
|
|
|
|
|
contentPreview := ""
|
2026-03-18 12:40:51 +08:00
|
|
|
|
productMid := 0
|
2026-03-17 12:15:08 +08:00
|
|
|
|
if gpr.ProductType == "section" && gpr.ProductID != "" {
|
|
|
|
|
|
var ch model.Chapter
|
2026-03-18 12:40:51 +08:00
|
|
|
|
if err := db.Select("section_title", "content", "mid").Where("id = ?", gpr.ProductID).First(&ch).Error; err == nil {
|
2026-03-17 12:15:08 +08:00
|
|
|
|
if ch.SectionTitle != "" {
|
|
|
|
|
|
sectionTitle = ch.SectionTitle
|
|
|
|
|
|
}
|
|
|
|
|
|
contentPreview = giftPayPreviewContent(ch.Content)
|
2026-03-18 12:40:51 +08:00
|
|
|
|
productMid = ch.MID
|
2026-03-17 12:15:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// 领取记录(发起人查看)
|
|
|
|
|
|
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 != "" {
|
2026-03-19 18:26:45 +08:00
|
|
|
|
avatar = resolveAvatarURL(*u.Avatar)
|
2026-03-18 12:40:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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"
|
2026-03-18 20:33:50 +08:00
|
|
|
|
} else if gpr.Status == "refunded" {
|
|
|
|
|
|
action = "refunded"
|
2026-03-18 12:40:51 +08:00
|
|
|
|
} 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"
|
|
|
|
|
|
}
|
2026-03-18 20:33:50 +08:00
|
|
|
|
} else if gpr.Status == "refunded" {
|
|
|
|
|
|
action = "refunded"
|
2026-03-18 12:40:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp := gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"requestSn": gpr.RequestSN,
|
|
|
|
|
|
"productType": gpr.ProductType,
|
2026-03-17 15:17:33 +08:00
|
|
|
|
"productId": gpr.ProductID,
|
2026-03-18 12:40:51 +08:00
|
|
|
|
"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)
|
2026-03-17 11:44:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// GiftPayRedeem POST /api/miniprogram/gift-pay/redeem 好友领取(改造后:免费获得章节)
|
|
|
|
|
|
func GiftPayRedeem(c *gin.Context) {
|
2026-03-17 11:44:36 +08:00
|
|
|
|
var req struct {
|
|
|
|
|
|
RequestSn string `json:"requestSn" binding:"required"`
|
2026-03-18 12:40:51 +08:00
|
|
|
|
UserID string `json:"userId" binding:"required"`
|
2026-03-17 11:44:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少参数"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
|
|
|
|
|
|
var gpr model.GiftPayRequest
|
2026-03-18 12:40:51 +08:00
|
|
|
|
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": "代付请求不存在或未支付"})
|
2026-03-17 11:44:36 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-18 12:40:51 +08:00
|
|
|
|
if req.UserID == gpr.InitiatorUserID {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "发起人无需领取"})
|
2026-03-17 11:44:36 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-18 12:40:51 +08:00
|
|
|
|
if gpr.RedeemedCount >= gpr.Quantity {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "已领完"})
|
2026-03-17 11:44:36 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// 同一用户同一 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": "您已领取过"})
|
2026-03-17 11:44:36 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// 创建好友订单:productType=section, status=paid, paymentMethod=gift_pay
|
2026-03-17 11:44:36 +08:00
|
|
|
|
orderSn := wechat.GenerateOrderSn()
|
2026-03-18 12:40:51 +08:00
|
|
|
|
status := "paid"
|
|
|
|
|
|
pm := "gift_pay"
|
2026-03-17 11:44:36 +08:00
|
|
|
|
productID := gpr.ProductID
|
2026-03-18 12:40:51 +08:00
|
|
|
|
desc := fmt.Sprintf("代付领取 - %s", gpr.Description)
|
2026-03-17 11:44:36 +08:00
|
|
|
|
gprID := gpr.ID
|
2026-03-18 12:40:51 +08:00
|
|
|
|
amount := 0.0
|
2026-03-17 11:44:36 +08:00
|
|
|
|
order := model.Order{
|
2026-03-18 12:40:51 +08:00
|
|
|
|
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.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
|
if err := tx.Create(&order).Error; err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
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": "领取失败"})
|
2026-03-17 11:44:36 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
_ = 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
|
2026-03-17 15:17:33 +08:00
|
|
|
|
}
|
2026-03-17 11:44:36 +08:00
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
2026-03-18 12:40:51 +08:00
|
|
|
|
"success": true,
|
|
|
|
|
|
"sectionId": gpr.ProductID,
|
|
|
|
|
|
"sectionMid": productMid,
|
2026-03-17 11:44:36 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-03-18 12:40:51 +08:00
|
|
|
|
if gpr.Status != "pending" && gpr.Status != "pending_pay" {
|
2026-03-17 11:44:36 +08:00
|
|
|
|
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": "已取消"})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// GiftPayMyRequests GET /api/miniprogram/gift-pay/my-requests?userId= 我发起的(含领取记录)
|
2026-03-17 11:44:36 +08:00
|
|
|
|
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
|
2026-03-18 12:40:51 +08:00
|
|
|
|
db.Where("initiator_user_id = ? AND status != ?", userID, "cancelled").Order("created_at DESC").Limit(50).Find(&list)
|
2026-03-17 11:44:36 +08:00
|
|
|
|
|
|
|
|
|
|
out := make([]gin.H, 0, len(list))
|
|
|
|
|
|
for _, r := range list {
|
2026-03-18 12:40:51 +08:00
|
|
|
|
// 领取记录: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 != "" {
|
2026-03-19 18:26:45 +08:00
|
|
|
|
avatar = resolveAvatarURL(*u.Avatar)
|
2026-03-18 12:40:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
redeemAt := o.CreatedAt.Format("2006-01-02 15:04")
|
|
|
|
|
|
redeemList = append(redeemList, gin.H{"userId": o.UserID, "nickname": nickname, "avatar": avatar, "redeemAt": redeemAt})
|
|
|
|
|
|
}
|
2026-03-17 11:44:36 +08:00
|
|
|
|
out = append(out, gin.H{
|
2026-03-18 12:40:51 +08:00
|
|
|
|
"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,
|
2026-03-17 11:44:36 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "list": out})
|
|
|
|
|
|
}
|
2026-03-17 13:17:49 +08:00
|
|
|
|
|
|
|
|
|
|
// AdminGiftPayRequestsList GET /api/admin/gift-pay-requests 管理端-代付请求列表
|
|
|
|
|
|
func AdminGiftPayRequestsList(c *gin.Context) {
|
|
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
|
|
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
|
|
|
|
|
status := strings.TrimSpace(c.Query("status"))
|
|
|
|
|
|
if page < 1 {
|
|
|
|
|
|
page = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
if pageSize < 1 || pageSize > 100 {
|
|
|
|
|
|
pageSize = 20
|
|
|
|
|
|
}
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
q := db.Model(&model.GiftPayRequest{})
|
|
|
|
|
|
if status != "" {
|
|
|
|
|
|
q = q.Where("status = ?", status)
|
|
|
|
|
|
}
|
|
|
|
|
|
var total int64
|
|
|
|
|
|
q.Count(&total)
|
|
|
|
|
|
var list []model.GiftPayRequest
|
|
|
|
|
|
q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&list)
|
|
|
|
|
|
userIDs := make(map[string]bool)
|
|
|
|
|
|
for _, r := range list {
|
|
|
|
|
|
userIDs[r.InitiatorUserID] = true
|
|
|
|
|
|
if r.PayerUserID != nil && *r.PayerUserID != "" {
|
|
|
|
|
|
userIDs[*r.PayerUserID] = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
nicknames := make(map[string]string)
|
|
|
|
|
|
if len(userIDs) > 0 {
|
|
|
|
|
|
ids := make([]string, 0, len(userIDs))
|
|
|
|
|
|
for id := range userIDs {
|
|
|
|
|
|
ids = append(ids, id)
|
|
|
|
|
|
}
|
|
|
|
|
|
var users []model.User
|
|
|
|
|
|
db.Select("id, nickname").Where("id IN ?", ids).Find(&users)
|
|
|
|
|
|
for _, u := range users {
|
|
|
|
|
|
if u.Nickname != nil {
|
|
|
|
|
|
nicknames[u.ID] = *u.Nickname
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out := make([]gin.H, 0, len(list))
|
|
|
|
|
|
for _, r := range list {
|
|
|
|
|
|
initiatorNick := nicknames[r.InitiatorUserID]
|
|
|
|
|
|
payerNick := ""
|
|
|
|
|
|
if r.PayerUserID != nil {
|
|
|
|
|
|
payerNick = nicknames[*r.PayerUserID]
|
|
|
|
|
|
}
|
|
|
|
|
|
orderID := ""
|
|
|
|
|
|
if r.OrderID != nil {
|
|
|
|
|
|
orderID = *r.OrderID
|
|
|
|
|
|
}
|
|
|
|
|
|
out = append(out, gin.H{
|
|
|
|
|
|
"id": r.ID,
|
|
|
|
|
|
"requestSn": r.RequestSN,
|
|
|
|
|
|
"initiatorUserId": r.InitiatorUserID,
|
|
|
|
|
|
"initiatorNick": initiatorNick,
|
|
|
|
|
|
"productType": r.ProductType,
|
|
|
|
|
|
"productId": r.ProductID,
|
|
|
|
|
|
"amount": r.Amount,
|
2026-03-18 12:40:51 +08:00
|
|
|
|
"quantity": r.Quantity,
|
|
|
|
|
|
"redeemedCount": r.RedeemedCount,
|
2026-03-17 13:17:49 +08:00
|
|
|
|
"description": r.Description,
|
|
|
|
|
|
"status": r.Status,
|
|
|
|
|
|
"payerUserId": r.PayerUserID,
|
|
|
|
|
|
"payerNick": payerNick,
|
|
|
|
|
|
"orderId": orderID,
|
|
|
|
|
|
"expireAt": r.ExpireAt,
|
|
|
|
|
|
"createdAt": r.CreatedAt,
|
|
|
|
|
|
"updatedAt": r.UpdatedAt,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": out, "total": total})
|
|
|
|
|
|
}
|