Files
soul-yongping/soul-api/internal/handler/vip.go

330 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 userId"})
return
}
db := database.DB()
// 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{"vipName": "", "vipProject": "", "vipContact": "", "vipAvatar": "", "vipBio": ""},
"price": float64(defaultVipPrice),
"rights": defaultVipRights,
},
})
return
}
}
// 查用户 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,
"data": gin.H{
"isVip": true,
"daysRemaining": daysRemaining,
"expireDate": expStr,
"profile": profile,
"price": float64(defaultVipPrice),
"rights": defaultVipRights,
},
})
}
// buildVipProfile 仅从 vip_* 字段构建会员资料不混入用户信息nickname/avatar/phone/wechat_id
// 返回字段与 users 表 vip_* 对应,统一 vipName/vipProject/vipContact/vipAvatar/vipBio
func buildVipProfile(u *model.User) gin.H {
return gin.H{
"vipName": getStr(u.VipName),
"vipProject": getStr(u.VipProject),
"vipContact": getStr(u.VipContact),
"vipAvatar": getStr(u.VipAvatar),
"vipBio": getStr(u.VipBio),
}
}
func getStr(s *string) string {
if s == nil {
return ""
}
return *s
}
// VipProfileGet GET /api/miniprogram/vip/profile 小程序-获取 VIP 资料
func VipProfileGet(c *gin.Context) {
userID := c.Query("userId")
if userID == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 userId"})
return
}
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{"vipName": "", "vipProject": "", "vipContact": "", "vipAvatar": "", "vipBio": ""}})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"data": buildVipProfile(&user),
})
}
// VipProfilePost POST /api/miniprogram/vip/profile 小程序-更新 VIP 资料
// 请求/响应字段与 users 表 vip_* 一致vipName/vipProject/vipContact/vipAvatar/vipBio
func VipProfilePost(c *gin.Context) {
var req struct {
UserID string `json:"userId" binding:"required"`
VipName string `json:"vipName"`
VipProject string `json:"vipProject"`
VipContact string `json:"vipContact"`
VipAvatar string `json:"vipAvatar"`
VipBio string `json:"vipBio"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
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.VipName != "" {
updates["vip_name"] = req.VipName
}
if req.VipProject != "" {
updates["vip_project"] = req.VipProject
}
if req.VipContact != "" {
updates["vip_contact"] = req.VipContact
}
if req.VipAvatar != "" {
updates["vip_avatar"] = req.VipAvatar
}
if req.VipBio != "" {
updates["vip_bio"] = req.VipBio
}
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()
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
}
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
}
// 列表:优先 users 表is_vip=1 且 vip_expire_date>NOW排序vip_sort 优先(小在前),否则 vip_activated_at DESC
var users []model.User
err := db.Table("users").
Select("id", "nickname", "avatar", "vip_name", "vip_role", "vip_project", "vip_avatar", "vip_bio", "vip_activated_at", "vip_sort").
Where("is_vip = 1 AND vip_expire_date > ?", time.Now()).
Order("COALESCE(vip_sort, 999999) ASC, COALESCE(vip_activated_at, 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, "total": len(list)})
}
// formatVipMember 构建会员展示数据;优先 vip_*,无则回退到用户 nickname/avatar
// 用于首页超级个体、创业老板排行等场景,展示真实用户头像和昵称
func formatVipMember(u *model.User, isVip bool) gin.H {
name := ""
if u.VipName != nil && *u.VipName != "" {
name = *u.VipName
}
if name == "" && u.Nickname != nil && *u.Nickname != "" {
name = *u.Nickname
}
if name == "" {
name = "创业者"
}
avatar := ""
if u.VipAvatar != nil && *u.VipAvatar != "" {
avatar = *u.VipAvatar
}
if avatar == "" && u.Avatar != nil && *u.Avatar != "" {
avatar = *u.Avatar
}
project := ""
if u.VipProject != nil {
project = *u.VipProject
}
bio := ""
if u.VipBio != nil {
bio = *u.VipBio
}
contact := ""
if u.VipContact != nil {
contact = *u.VipContact
}
vipRole := ""
if u.VipRole != nil {
vipRole = *u.VipRole
}
return gin.H{
"id": u.ID,
"name": name,
"nickname": name,
"avatar": avatar,
"vip_name": name,
"vipName": name,
"vipRole": vipRole,
"vip_avatar": avatar,
"vipAvatar": avatar,
"vipProject": project,
"vipContact": contact,
"vipBio": bio,
"is_vip": isVip,
}
}
func parseInt(s string) (int, error) {
return strconv.Atoi(s)
}