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