2026-02-25 16:26:13 +08:00
|
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"net/http"
|
2026-02-26 14:26:31 +08:00
|
|
|
|
"strconv"
|
2026-02-25 16:26:13 +08:00
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"soul-api/internal/database"
|
|
|
|
|
|
"soul-api/internal/model"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
2026-02-26 14:26:31 +08:00
|
|
|
|
"gorm.io/gorm"
|
2026-02-25 16:26:13 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-26 14:26:31 +08:00
|
|
|
|
// 默认 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 16:26:13 +08:00
|
|
|
|
// VipStatus GET /api/miniprogram/vip/status 小程序-查询用户 VIP 状态
|
2026-02-26 14:26:31 +08:00
|
|
|
|
// 优先 users 表(is_vip、vip_expire_date),无则从 orders 兜底
|
2026-02-25 16:26:13 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2026-02-26 14:26:31 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2026-02-25 16:26:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 14:26:31 +08:00
|
|
|
|
// 查用户 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 {
|
2026-02-25 16:26:13 +08:00
|
|
|
|
daysRemaining = 0
|
|
|
|
|
|
}
|
2026-02-26 14:26:31 +08:00
|
|
|
|
expStr = expireDate.Format("2006-01-02")
|
2026-02-25 16:26:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": gin.H{
|
|
|
|
|
|
"isVip": true,
|
|
|
|
|
|
"daysRemaining": daysRemaining,
|
|
|
|
|
|
"expireDate": expStr,
|
2026-02-26 14:26:31 +08:00
|
|
|
|
"profile": profile,
|
|
|
|
|
|
"price": float64(defaultVipPrice),
|
|
|
|
|
|
"rights": defaultVipRights,
|
2026-02-25 16:26:13 +08:00
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 14:26:31 +08:00
|
|
|
|
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}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 16:26:13 +08:00
|
|
|
|
// 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 {
|
2026-02-26 14:26:31 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"name": "", "project": "", "contact": "", "avatar": "", "bio": ""}})
|
2026-02-25 16:26:13 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
2026-02-26 14:26:31 +08:00
|
|
|
|
"data": buildVipProfile(&user),
|
2026-02-25 16:26:13 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// VipProfilePost POST /api/miniprogram/vip/profile 小程序-更新 VIP 资料
|
2026-02-26 14:26:31 +08:00
|
|
|
|
// 仅 VIP 会员可更新,更新 vip_name/vip_project/vip_contact/vip_avatar/vip_bio
|
2026-02-25 16:26:13 +08:00
|
|
|
|
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"`
|
2026-02-26 14:26:31 +08:00
|
|
|
|
Avatar string `json:"avatar"`
|
2026-02-25 16:26:13 +08:00
|
|
|
|
Bio string `json:"bio"`
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
db := database.DB()
|
2026-02-26 14:26:31 +08:00
|
|
|
|
|
|
|
|
|
|
// 校验是否 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 16:26:13 +08:00
|
|
|
|
updates := map[string]interface{}{}
|
|
|
|
|
|
if req.Name != "" {
|
2026-02-26 14:26:31 +08:00
|
|
|
|
updates["vip_name"] = req.Name
|
|
|
|
|
|
}
|
|
|
|
|
|
if req.Project != "" {
|
|
|
|
|
|
updates["vip_project"] = req.Project
|
2026-02-25 16:26:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
if req.Contact != "" {
|
2026-02-26 14:26:31 +08:00
|
|
|
|
updates["vip_contact"] = req.Contact
|
|
|
|
|
|
}
|
|
|
|
|
|
if req.Avatar != "" {
|
|
|
|
|
|
updates["vip_avatar"] = req.Avatar
|
2026-02-25 16:26:13 +08:00
|
|
|
|
}
|
2026-02-26 14:26:31 +08:00
|
|
|
|
if req.Bio != "" {
|
|
|
|
|
|
updates["vip_bio"] = req.Bio
|
2026-02-25 16:26:13 +08:00
|
|
|
|
}
|
2026-02-26 14:26:31 +08:00
|
|
|
|
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": "资料已更新"})
|
2026-02-25 16:26:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// VipMembers GET /api/miniprogram/vip/members 小程序-VIP 会员列表(无 id 返回列表;有 id 返回单个)
|
2026-02-26 14:26:31 +08:00
|
|
|
|
// 优先 users 表(is_vip=1 且 vip_expire_date>NOW),无则从 orders 兜底
|
2026-02-25 16:26:13 +08:00
|
|
|
|
func VipMembers(c *gin.Context) {
|
|
|
|
|
|
id := c.Query("id")
|
2026-02-26 14:26:31 +08:00
|
|
|
|
limit := 20
|
|
|
|
|
|
if l := c.Query("limit"); l != "" {
|
|
|
|
|
|
if n, err := parseInt(l); err == nil && n > 0 && n <= 100 {
|
|
|
|
|
|
limit = n
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-25 16:26:13 +08:00
|
|
|
|
db := database.DB()
|
|
|
|
|
|
|
|
|
|
|
|
if id != "" {
|
2026-02-26 14:26:31 +08:00
|
|
|
|
// 单个:优先 users 表
|
2026-02-25 16:26:13 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-02-26 14:26:31 +08:00
|
|
|
|
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)
|
2026-02-25 16:26:13 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": item})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 14:26:31 +08:00
|
|
|
|
// 列表:优先 users 表(is_vip=1 且 vip_expire_date>NOW)
|
2026-02-25 16:26:13 +08:00
|
|
|
|
var users []model.User
|
2026-02-26 14:26:31 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 16:26:13 +08:00
|
|
|
|
list := make([]gin.H, 0, len(users))
|
|
|
|
|
|
for i := range users {
|
|
|
|
|
|
list = append(list, formatVipMember(&users[i], true))
|
|
|
|
|
|
}
|
2026-02-26 14:26:31 +08:00
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "data": list, "total": len(list)})
|
2026-02-25 16:26:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func formatVipMember(u *model.User, isVip bool) gin.H {
|
|
|
|
|
|
name := ""
|
2026-02-26 14:26:31 +08:00
|
|
|
|
if u.VipName != nil && *u.VipName != "" {
|
|
|
|
|
|
name = *u.VipName
|
|
|
|
|
|
}
|
|
|
|
|
|
if name == "" && u.Nickname != nil {
|
2026-02-25 16:26:13 +08:00
|
|
|
|
name = *u.Nickname
|
|
|
|
|
|
}
|
2026-02-26 14:26:31 +08:00
|
|
|
|
if name == "" {
|
|
|
|
|
|
name = "创业者"
|
|
|
|
|
|
}
|
2026-02-25 16:26:13 +08:00
|
|
|
|
avatar := ""
|
2026-02-26 14:26:31 +08:00
|
|
|
|
if u.VipAvatar != nil && *u.VipAvatar != "" {
|
|
|
|
|
|
avatar = *u.VipAvatar
|
|
|
|
|
|
}
|
|
|
|
|
|
if avatar == "" && u.Avatar != nil {
|
2026-02-25 16:26:13 +08:00
|
|
|
|
avatar = *u.Avatar
|
|
|
|
|
|
}
|
2026-02-26 14:26:31 +08:00
|
|
|
|
project := ""
|
|
|
|
|
|
if u.VipProject != nil {
|
|
|
|
|
|
project = *u.VipProject
|
|
|
|
|
|
}
|
|
|
|
|
|
bio := ""
|
|
|
|
|
|
if u.VipBio != nil {
|
|
|
|
|
|
bio = *u.VipBio
|
|
|
|
|
|
}
|
2026-02-25 16:26:13 +08:00
|
|
|
|
contact := ""
|
2026-02-26 14:26:31 +08:00
|
|
|
|
if u.VipContact != nil {
|
|
|
|
|
|
contact = *u.VipContact
|
|
|
|
|
|
}
|
|
|
|
|
|
if contact == "" && u.Phone != nil {
|
2026-02-25 16:26:13 +08:00
|
|
|
|
contact = *u.Phone
|
|
|
|
|
|
}
|
2026-02-26 14:26:31 +08:00
|
|
|
|
if contact == "" && u.WechatID != nil {
|
2026-02-25 16:26:13 +08:00
|
|
|
|
contact = *u.WechatID
|
|
|
|
|
|
}
|
|
|
|
|
|
return gin.H{
|
2026-02-26 14:26:31 +08:00
|
|
|
|
"id": u.ID,
|
|
|
|
|
|
"name": name,
|
|
|
|
|
|
"nickname": name,
|
|
|
|
|
|
"avatar": avatar,
|
|
|
|
|
|
"vip_name": name,
|
|
|
|
|
|
"vip_avatar": avatar,
|
|
|
|
|
|
"vip_project": project,
|
2026-02-25 16:26:13 +08:00
|
|
|
|
"vip_contact": contact,
|
2026-02-26 14:26:31 +08:00
|
|
|
|
"vip_bio": bio,
|
|
|
|
|
|
"is_vip": isVip,
|
2026-02-25 16:26:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-26 14:26:31 +08:00
|
|
|
|
|
|
|
|
|
|
func parseInt(s string) (int, error) {
|
|
|
|
|
|
return strconv.Atoi(s)
|
|
|
|
|
|
}
|