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 表判断是否 VIP(is_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() // 校验是否 VIP(users 或 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) }