优化 VIP 会员加载逻辑,支持无头像用户展示,增加错误处理和日志记录。更新 VIP 相关接口,确保用户状态查询和资料更新功能正常,新增 VIP 资料字段以提升用户体验。

This commit is contained in:
Alex-larget
2026-02-26 14:26:31 +08:00
parent 548cf4229c
commit f5ee93dd84
123 changed files with 19484 additions and 85 deletions

View File

@@ -447,6 +447,14 @@ func MiniprogramPayNotify(c *gin.Context) {
if attach.ProductType == "fullbook" {
db.Model(&model.User{}).Where("id = ?", buyerUserID).Update("has_full_book", true)
fmt.Printf("[PayNotify] 用户已购全书: %s\n", buyerUserID)
} else if attach.ProductType == "vip" {
// VIP 支付成功:更新 users.is_vip、vip_expire_date与 next-project 一致)
expireDate := time.Now().AddDate(0, 0, 365)
db.Model(&model.User{}).Where("id = ?", buyerUserID).Updates(map[string]interface{}{
"is_vip": true,
"vip_expire_date": expireDate,
})
fmt.Printf("[PayNotify] 用户开通VIP: %s, 过期日 %s\n", buyerUserID, expireDate.Format("2006-01-02"))
} else if attach.ProductType == "match" {
fmt.Printf("[PayNotify] 用户购买匹配次数: %s订单 %s\n", buyerUserID, orderSn)
} else if attach.ProductType == "section" && attach.ProductID != "" {

View File

@@ -2,15 +2,64 @@ package handler
import (
"net/http"
"strconv"
"time"
"soul-api/internal/database"
"soul-api/internal/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// 默认 VIP 价格与权益(与 next-project 一致)
const defaultVipPrice = 1980
var defaultVipRights = []string{
"智能纪要 - 每天推送派对精华",
"会议纪要库 - 所有场次会议纪要",
"案例库 - 30-100个创业项目案例",
"链接资源 - 进群聊天链接资源",
"解锁全部章节内容365天",
"匹配所有创业伙伴",
"创业老板排行榜展示",
"专属VIP标识",
}
// isVipFromUsers 从 users 表判断是否 VIPis_vip=1 且 vip_expire_date>NOW
func isVipFromUsers(db *gorm.DB, userID string) (bool, *time.Time) {
var u struct {
IsVip *bool
VipExpireDate *time.Time
}
err := db.Table("users").Select("is_vip", "vip_expire_date").Where("id = ?", userID).First(&u).Error
if err != nil || u.IsVip == nil || !*u.IsVip || u.VipExpireDate == nil {
return false, nil
}
if u.VipExpireDate.Before(time.Now()) {
return false, nil
}
return true, u.VipExpireDate
}
// isVipFromOrders 从 orders 表判断是否 VIP兜底
func isVipFromOrders(db *gorm.DB, userID string) (bool, *time.Time) {
var order model.Order
err := db.Where("user_id = ? AND (status = ? OR status = ?) AND (product_type = ? OR product_type = ?)",
userID, "paid", "completed", "fullbook", "vip").
Order("pay_time DESC").First(&order).Error
if err != nil || order.PayTime == nil {
return false, nil
}
exp := order.PayTime.AddDate(0, 0, 365)
if exp.Before(time.Now()) {
return false, nil
}
return true, &exp
}
// VipStatus GET /api/miniprogram/vip/status 小程序-查询用户 VIP 状态
// 优先 users 表is_vip、vip_expire_date无则从 orders 兜底
func VipStatus(c *gin.Context) {
userID := c.Query("userId")
if userID == "" {
@@ -19,35 +68,41 @@ func VipStatus(c *gin.Context) {
}
db := database.DB()
// 查是否有 fullbook 或 vip 的已支付订单
var order model.Order
err := db.Where("user_id = ? AND status = ? AND (product_type = ? OR product_type = ?)",
userID, "paid", "fullbook", "vip").
Order("pay_time DESC").First(&order).Error
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"isVip": false,
"daysRemaining": 0,
"expireDate": "",
"price": float64(1980),
},
})
return
}
expireDate := time.Now().AddDate(0, 0, 365)
daysRemaining := 365
if order.PayTime != nil {
expireDate = order.PayTime.AddDate(0, 0, 365)
if expireDate.After(time.Now()) {
daysRemaining = int(expireDate.Sub(time.Now()).Hours() / 24)
} else {
daysRemaining = 0
// 1. 优先 users 表
isVip, expireDate := isVipFromUsers(db, userID)
if !isVip {
// 2. 兜底:从 orders 查
isVip, expireDate = isVipFromOrders(db, userID)
if !isVip {
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"isVip": false,
"daysRemaining": 0,
"expireDate": "",
"profile": gin.H{"name": "", "project": "", "contact": "", "avatar": "", "bio": ""},
"price": float64(defaultVipPrice),
"rights": defaultVipRights,
},
})
return
}
}
expStr := expireDate.Format("2006-01-02")
// 查用户 VIP 资料profile
var user model.User
_ = db.Where("id = ?", userID).First(&user).Error
profile := buildVipProfile(&user)
daysRemaining := 0
expStr := ""
if expireDate != nil {
daysRemaining = int(expireDate.Sub(time.Now()).Hours()/24) + 1
if daysRemaining < 0 {
daysRemaining = 0
}
expStr = expireDate.Format("2006-01-02")
}
c.JSON(http.StatusOK, gin.H{
"success": true,
@@ -55,11 +110,45 @@ func VipStatus(c *gin.Context) {
"isVip": true,
"daysRemaining": daysRemaining,
"expireDate": expStr,
"price": float64(1980),
"profile": profile,
"price": float64(defaultVipPrice),
"rights": defaultVipRights,
},
})
}
func buildVipProfile(u *model.User) gin.H {
name, project, contact, avatar, bio := "", "", "", "", ""
if u.VipName != nil {
name = *u.VipName
}
if name == "" && u.Nickname != nil {
name = *u.Nickname
}
if u.VipProject != nil {
project = *u.VipProject
}
if u.VipContact != nil {
contact = *u.VipContact
}
if contact == "" && u.Phone != nil {
contact = *u.Phone
}
if contact == "" && u.WechatID != nil {
contact = *u.WechatID
}
if u.VipAvatar != nil {
avatar = *u.VipAvatar
}
if avatar == "" && u.Avatar != nil {
avatar = *u.Avatar
}
if u.VipBio != nil {
bio = *u.VipBio
}
return gin.H{"name": name, "project": project, "contact": contact, "avatar": avatar, "bio": bio}
}
// VipProfileGet GET /api/miniprogram/vip/profile 小程序-获取 VIP 资料
func VipProfileGet(c *gin.Context) {
userID := c.Query("userId")
@@ -70,38 +159,24 @@ func VipProfileGet(c *gin.Context) {
db := database.DB()
var user model.User
if err := db.Where("id = ?", userID).First(&user).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"name": "", "project": "", "contact": "", "bio": ""}})
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"name": "", "project": "", "contact": "", "avatar": "", "bio": ""}})
return
}
name := ""
if user.Nickname != nil {
name = *user.Nickname
}
contact := ""
if user.Phone != nil {
contact = *user.Phone
}
if user.WechatID != nil && contact == "" {
contact = *user.WechatID
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": gin.H{
"name": name,
"project": "",
"contact": contact,
"bio": "",
},
"data": buildVipProfile(&user),
})
}
// VipProfilePost POST /api/miniprogram/vip/profile 小程序-更新 VIP 资料
// 仅 VIP 会员可更新,更新 vip_name/vip_project/vip_contact/vip_avatar/vip_bio
func VipProfilePost(c *gin.Context) {
var req struct {
UserID string `json:"userId" binding:"required"`
Name string `json:"name"`
Project string `json:"project"`
Contact string `json:"contact"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
}
if err := c.ShouldBindJSON(&req); err != nil {
@@ -109,81 +184,156 @@ func VipProfilePost(c *gin.Context) {
return
}
db := database.DB()
// 校验是否 VIPusers 或 orders
isVip, _ := isVipFromUsers(db, req.UserID)
if !isVip {
isVip, _ = isVipFromOrders(db, req.UserID)
}
if !isVip {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "仅VIP会员可填写资料"})
return
}
updates := map[string]interface{}{}
if req.Name != "" {
updates["nickname"] = req.Name
updates["vip_name"] = req.Name
}
if req.Project != "" {
updates["vip_project"] = req.Project
}
if req.Contact != "" {
updates["phone"] = req.Contact
updates["vip_contact"] = req.Contact
}
if len(updates) > 0 {
db.Model(&model.User{}).Where("id = ?", req.UserID).Updates(updates)
if req.Avatar != "" {
updates["vip_avatar"] = req.Avatar
}
c.JSON(http.StatusOK, gin.H{"success": true})
if req.Bio != "" {
updates["vip_bio"] = req.Bio
}
if len(updates) == 0 {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "无更新内容"})
return
}
if err := db.Model(&model.User{}).Where("id = ?", req.UserID).Updates(updates).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "更新失败"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "资料已更新"})
}
// VipMembers GET /api/miniprogram/vip/members 小程序-VIP 会员列表(无 id 返回列表;有 id 返回单个)
// 优先 users 表is_vip=1 且 vip_expire_date>NOW无则从 orders 兜底
func VipMembers(c *gin.Context) {
id := c.Query("id")
limit := 20
if l := c.Query("limit"); l != "" {
if n, err := parseInt(l); err == nil && n > 0 && n <= 100 {
limit = n
}
}
db := database.DB()
// 有 id 时查单个:优先从已购 fullbook/vip 的用户中找
if id != "" {
// 单个:优先 users 表
var user model.User
if err := db.Where("id = ?", id).First(&user).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "data": nil})
return
}
// 检查是否 VIP有 fullbook 或 vip 订单)
var cnt int64
db.Model(&model.Order{}).Where("user_id = ? AND status = ? AND (product_type = ? OR product_type = ?)",
id, "paid", "fullbook", "vip").Count(&cnt)
item := formatVipMember(&user, cnt > 0)
isVip, _ := isVipFromUsers(db, id)
if !isVip {
isVip, _ = isVipFromOrders(db, id)
}
if !isVip {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "会员不存在或已过期"})
return
}
item := formatVipMember(&user, true)
c.JSON(http.StatusOK, gin.H{"success": true, "data": item})
return
}
// 无 id返回 VIP 会员列表(有 fullbook 或 vip 订单的用户
var userIDs []string
db.Model(&model.Order{}).Select("DISTINCT user_id").
Where("status = ? AND (product_type = ? OR product_type = ?)", "paid", "fullbook", "vip").
Pluck("user_id", &userIDs)
if len(userIDs) == 0 {
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
return
}
// 列表:优先 users 表is_vip=1 且 vip_expire_date>NOW
var users []model.User
db.Where("id IN ?", userIDs).Find(&users)
err := db.Table("users").
Select("id", "nickname", "avatar", "vip_name", "vip_project", "vip_avatar", "vip_bio").
Where("is_vip = 1 AND vip_expire_date > ?", time.Now()).
Order("vip_expire_date DESC").
Limit(limit).
Find(&users).Error
if err != nil || len(users) == 0 {
// 兜底:从 orders 查
var userIDs []string
db.Model(&model.Order{}).Select("DISTINCT user_id").
Where("(status = ? OR status = ?) AND (product_type = ? OR product_type = ?)", "paid", "completed", "fullbook", "vip").
Pluck("user_id", &userIDs)
if len(userIDs) == 0 {
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}, "total": 0})
return
}
db.Where("id IN ?", userIDs).Find(&users)
}
list := make([]gin.H, 0, len(users))
for i := range users {
list = append(list, formatVipMember(&users[i], true))
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)})
}
func formatVipMember(u *model.User, isVip bool) gin.H {
name := ""
if u.Nickname != nil {
if u.VipName != nil && *u.VipName != "" {
name = *u.VipName
}
if name == "" && u.Nickname != nil {
name = *u.Nickname
}
if name == "" {
name = "创业者"
}
avatar := ""
if u.Avatar != nil {
if u.VipAvatar != nil && *u.VipAvatar != "" {
avatar = *u.VipAvatar
}
if avatar == "" && u.Avatar != nil {
avatar = *u.Avatar
}
project := ""
if u.VipProject != nil {
project = *u.VipProject
}
bio := ""
if u.VipBio != nil {
bio = *u.VipBio
}
contact := ""
if u.Phone != nil {
if u.VipContact != nil {
contact = *u.VipContact
}
if contact == "" && u.Phone != nil {
contact = *u.Phone
}
if u.WechatID != nil && contact == "" {
if contact == "" && u.WechatID != nil {
contact = *u.WechatID
}
return gin.H{
"id": u.ID,
"nickname": name,
"avatar": avatar,
"vip_name": name,
"vip_avatar": avatar,
"id": u.ID,
"name": name,
"nickname": name,
"avatar": avatar,
"vip_name": name,
"vip_avatar": avatar,
"vip_project": project,
"vip_contact": contact,
"is_vip": isVip,
"vip_bio": bio,
"is_vip": isVip,
}
}
func parseInt(s string) (int, error) {
return strconv.Atoi(s)
}