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

593 lines
21 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 (
"fmt"
"net/http"
"strconv"
"time"
"soul-api/internal/database"
"soul-api/internal/model"
"github.com/gin-gonic/gin"
)
// UserAddressesGet GET /api/user/addresses?userId=
func UserAddressesGet(c *gin.Context) {
userId := c.Query("userId")
if userId == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 userId"})
return
}
var list []model.UserAddress
if err := database.DB().Where("user_id = ?", userId).Order("is_default DESC, updated_at DESC").Find(&list).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "list": []interface{}{}})
return
}
out := make([]gin.H, 0, len(list))
for _, r := range list {
full := r.Province + r.City + r.District + r.Detail
out = append(out, gin.H{
"id": r.ID, "userId": r.UserID, "name": r.Name, "phone": r.Phone,
"province": r.Province, "city": r.City, "district": r.District, "detail": r.Detail,
"isDefault": r.IsDefault, "fullAddress": full, "createdAt": r.CreatedAt, "updatedAt": r.UpdatedAt,
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "list": out})
}
// UserAddressesPost POST /api/user/addresses
func UserAddressesPost(c *gin.Context) {
var body struct {
UserID string `json:"userId" binding:"required"`
Name string `json:"name" binding:"required"`
Phone string `json:"phone" binding:"required"`
Province string `json:"province"`
City string `json:"city"`
District string `json:"district"`
Detail string `json:"detail" binding:"required"`
IsDefault bool `json:"isDefault"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少必填项userId, name, phone, detail"})
return
}
id := fmt.Sprintf("addr_%d", time.Now().UnixNano()%100000000000)
db := database.DB()
if body.IsDefault {
db.Model(&model.UserAddress{}).Where("user_id = ?", body.UserID).Update("is_default", false)
}
addr := model.UserAddress{
ID: id, UserID: body.UserID, Name: body.Name, Phone: body.Phone,
Province: body.Province, City: body.City, District: body.District, Detail: body.Detail,
IsDefault: body.IsDefault,
}
if err := db.Create(&addr).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "添加地址失败"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "id": id, "message": "添加成功"})
}
// UserAddressesByID GET/PUT/DELETE /api/user/addresses/:id
func UserAddressesByID(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少地址 id"})
return
}
db := database.DB()
switch c.Request.Method {
case "GET":
var r model.UserAddress
if err := db.Where("id = ?", id).First(&r).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "地址不存在"})
return
}
full := r.Province + r.City + r.District + r.Detail
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
"id": r.ID, "userId": r.UserID, "name": r.Name, "phone": r.Phone,
"province": r.Province, "city": r.City, "district": r.District, "detail": r.Detail,
"isDefault": r.IsDefault, "fullAddress": full, "createdAt": r.CreatedAt, "updatedAt": r.UpdatedAt,
}})
case "PUT":
var r model.UserAddress
if err := db.Where("id = ?", id).First(&r).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "地址不存在"})
return
}
var body struct {
Name *string `json:"name"`
Phone *string `json:"phone"`
Province *string `json:"province"`
City *string `json:"city"`
District *string `json:"district"`
Detail *string `json:"detail"`
IsDefault *bool `json:"isDefault"`
}
_ = c.ShouldBindJSON(&body)
updates := make(map[string]interface{})
if body.Name != nil {
updates["name"] = *body.Name
}
if body.Phone != nil {
updates["phone"] = *body.Phone
}
if body.Province != nil {
updates["province"] = *body.Province
}
if body.City != nil {
updates["city"] = *body.City
}
if body.District != nil {
updates["district"] = *body.District
}
if body.Detail != nil {
updates["detail"] = *body.Detail
}
if body.IsDefault != nil {
updates["is_default"] = *body.IsDefault
if *body.IsDefault {
db.Model(&model.UserAddress{}).Where("user_id = ?", r.UserID).Update("is_default", false)
}
}
if len(updates) > 0 {
updates["updated_at"] = time.Now()
db.Model(&r).Updates(updates)
}
db.Where("id = ?", id).First(&r)
full := r.Province + r.City + r.District + r.Detail
c.JSON(http.StatusOK, gin.H{"success": true, "item": gin.H{
"id": r.ID, "userId": r.UserID, "name": r.Name, "phone": r.Phone,
"province": r.Province, "city": r.City, "district": r.District, "detail": r.Detail,
"isDefault": r.IsDefault, "fullAddress": full, "createdAt": r.CreatedAt, "updatedAt": r.UpdatedAt,
}, "message": "更新成功"})
case "DELETE":
if err := db.Where("id = ?", id).Delete(&model.UserAddress{}).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "删除失败"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "删除成功"})
}
}
// UserCheckPurchased GET /api/user/check-purchased?userId=&type=section|fullbook&productId=
func UserCheckPurchased(c *gin.Context) {
userId := c.Query("userId")
type_ := c.Query("type")
productId := c.Query("productId")
if userId == "" {
c.JSON(http.StatusBadRequest, 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.StatusNotFound, gin.H{"success": false, "error": "用户不存在"})
return
}
hasFullBook := user.HasFullBook != nil && *user.HasFullBook
if hasFullBook {
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": true, "reason": "has_full_book"}})
return
}
if type_ == "fullbook" {
var count int64
db.Model(&model.Order{}).Where("user_id = ? AND product_type = ? AND status = ?", userId, "fullbook", "paid").Count(&count)
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": count > 0, "reason": map[bool]string{true: "fullbook_order_exists"}[count > 0]}})
return
}
if type_ == "section" && productId != "" {
var count int64
db.Model(&model.Order{}).Where("user_id = ? AND product_type = ? AND product_id = ? AND status = ?", userId, "section", productId, "paid").Count(&count)
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": count > 0, "reason": map[bool]string{true: "section_order_exists"}[count > 0]}})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": false, "reason": nil}})
}
// UserProfileGet GET /api/user/profile?userId= 或 openId=
func UserProfileGet(c *gin.Context) {
userId := c.Query("userId")
openId := c.Query("openId")
if userId == "" && openId == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请提供userId或openId"})
return
}
db := database.DB()
var user model.User
if userId != "" {
db = db.Where("id = ?", userId)
} else {
db = db.Where("open_id = ?", openId)
}
if err := db.First(&user).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "用户不存在"})
return
}
profileComplete := (user.Phone != nil && *user.Phone != "") || (user.WechatID != nil && *user.WechatID != "")
hasAvatar := user.Avatar != nil && *user.Avatar != "" && len(*user.Avatar) > 0
resp := gin.H{
"id": user.ID, "openId": user.OpenID, "nickname": user.Nickname, "avatar": user.Avatar,
"phone": user.Phone, "wechatId": user.WechatID, "referralCode": user.ReferralCode,
"hasFullBook": user.HasFullBook, "earnings": user.Earnings, "pendingEarnings": user.PendingEarnings,
"referralCount": user.ReferralCount, "profileComplete": profileComplete, "hasAvatar": hasAvatar,
"createdAt": user.CreatedAt,
}
// P3 资料扩展
if user.Mbti != nil { resp["mbti"] = user.Mbti }
if user.Region != nil { resp["region"] = user.Region }
if user.Industry != nil { resp["industry"] = user.Industry }
if user.Position != nil { resp["position"] = user.Position }
if user.BusinessScale != nil { resp["businessScale"] = user.BusinessScale }
if user.Skills != nil { resp["skills"] = user.Skills }
if user.StoryBestMonth != nil { resp["storyBestMonth"] = user.StoryBestMonth }
if user.StoryAchievement != nil { resp["storyAchievement"] = user.StoryAchievement }
if user.StoryTurning != nil { resp["storyTurning"] = user.StoryTurning }
if user.HelpOffer != nil { resp["helpOffer"] = user.HelpOffer }
if user.HelpNeed != nil { resp["helpNeed"] = user.HelpNeed }
if user.ProjectIntro != nil { resp["projectIntro"] = user.ProjectIntro }
c.JSON(http.StatusOK, gin.H{"success": true, "data": resp})
}
// UserProfilePost POST /api/user/profile 更新用户资料
func UserProfilePost(c *gin.Context) {
var body struct {
UserID string `json:"userId"`
OpenID string `json:"openId"`
Nickname *string `json:"nickname"`
Avatar *string `json:"avatar"`
Phone *string `json:"phone"`
WechatID *string `json:"wechatId"`
Mbti *string `json:"mbti"`
Region *string `json:"region"`
Industry *string `json:"industry"`
Position *string `json:"position"`
BusinessScale *string `json:"businessScale"`
Skills *string `json:"skills"`
StoryBestMonth *string `json:"storyBestMonth"`
StoryAchievement *string `json:"storyAchievement"`
StoryTurning *string `json:"storyTurning"`
HelpOffer *string `json:"helpOffer"`
HelpNeed *string `json:"helpNeed"`
ProjectIntro *string `json:"projectIntro"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请提供userId或openId"})
return
}
identifier := body.UserID
byID := true
if identifier == "" {
identifier = body.OpenID
byID = false
}
if identifier == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请提供userId或openId"})
return
}
db := database.DB()
var user model.User
if byID {
db = db.Where("id = ?", identifier)
} else {
db = db.Where("open_id = ?", identifier)
}
if err := db.First(&user).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "用户不存在"})
return
}
updates := make(map[string]interface{})
if body.Nickname != nil {
updates["nickname"] = *body.Nickname
}
if body.Avatar != nil {
updates["avatar"] = *body.Avatar
}
if body.Phone != nil {
updates["phone"] = *body.Phone
}
if body.WechatID != nil {
updates["wechat_id"] = *body.WechatID
}
if body.Mbti != nil { updates["mbti"] = *body.Mbti }
if body.Region != nil { updates["region"] = *body.Region }
if body.Industry != nil { updates["industry"] = *body.Industry }
if body.Position != nil { updates["position"] = *body.Position }
if body.BusinessScale != nil { updates["business_scale"] = *body.BusinessScale }
if body.Skills != nil { updates["skills"] = *body.Skills }
if body.StoryBestMonth != nil { updates["story_best_month"] = *body.StoryBestMonth }
if body.StoryAchievement != nil { updates["story_achievement"] = *body.StoryAchievement }
if body.StoryTurning != nil { updates["story_turning"] = *body.StoryTurning }
if body.HelpOffer != nil { updates["help_offer"] = *body.HelpOffer }
if body.HelpNeed != nil { updates["help_need"] = *body.HelpNeed }
if body.ProjectIntro != nil { updates["project_intro"] = *body.ProjectIntro }
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "没有需要更新的字段"})
return
}
updates["updated_at"] = time.Now()
db.Model(&user).Updates(updates)
c.JSON(http.StatusOK, gin.H{"success": true, "message": "资料更新成功", "data": gin.H{
"id": user.ID, "nickname": body.Nickname, "avatar": body.Avatar, "phone": body.Phone, "wechatId": body.WechatID, "referralCode": user.ReferralCode,
}})
}
// UserPurchaseStatus GET /api/user/purchase-status?userId=
func UserPurchaseStatus(c *gin.Context) {
userId := c.Query("userId")
if userId == "" {
c.JSON(http.StatusBadRequest, 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.StatusNotFound, gin.H{"success": false, "error": "用户不存在"})
return
}
var orderRows []struct {
ProductID string
MID int
}
db.Raw(`SELECT DISTINCT o.product_id, c.mid FROM orders o
LEFT JOIN chapters c ON c.id = o.product_id
WHERE o.user_id = ? AND o.status = ? AND o.product_type = ?`, userId, "paid", "section").Scan(&orderRows)
purchasedSections := make([]string, 0, len(orderRows))
sectionMidMap := make(map[string]int)
for _, r := range orderRows {
if r.ProductID != "" {
purchasedSections = append(purchasedSections, r.ProductID)
if r.MID > 0 {
sectionMidMap[r.ProductID] = r.MID
}
}
}
// 是否有推荐人(被推荐绑定,可享好友优惠)
var refCount int64
db.Model(&model.ReferralBinding{}).Where("referee_id = ? AND status = ?", userId, "active").
Where("expiry_date > ?", time.Now()).Count(&refCount)
hasReferrer := refCount > 0
// 匹配次数配额:纯计算(订单 + match_records
freeLimit := getFreeMatchLimit(db)
matchQuota := GetMatchQuota(db, userId, freeLimit)
earnings := 0.0
if user.Earnings != nil {
earnings = *user.Earnings
}
pendingEarnings := 0.0
if user.PendingEarnings != nil {
pendingEarnings = *user.PendingEarnings
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
"hasFullBook": user.HasFullBook != nil && *user.HasFullBook,
"purchasedSections": purchasedSections,
"sectionMidMap": sectionMidMap,
"purchasedCount": len(purchasedSections),
"hasReferrer": hasReferrer,
"matchCount": matchQuota.PurchasedTotal,
"matchQuota": gin.H{
"purchasedTotal": matchQuota.PurchasedTotal,
"purchasedUsed": matchQuota.PurchasedUsed,
"matchesUsedToday": matchQuota.MatchesUsedToday,
"freeRemainToday": matchQuota.FreeRemainToday,
"purchasedRemain": matchQuota.PurchasedRemain,
"remainToday": matchQuota.RemainToday,
},
"earnings": earnings,
"pendingEarnings": pendingEarnings,
}})
}
// UserReadingProgressGet GET /api/user/reading-progress?userId=
func UserReadingProgressGet(c *gin.Context) {
userId := c.Query("userId")
if userId == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId 参数"})
return
}
var list []model.ReadingProgress
if err := database.DB().Where("user_id = ?", userId).Order("last_open_at DESC").Find(&list).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
return
}
out := make([]gin.H, 0, len(list))
for _, r := range list {
out = append(out, gin.H{
"section_id": r.SectionID, "progress": r.Progress, "duration": r.Duration, "status": r.Status,
"completed_at": r.CompletedAt, "first_open_at": r.FirstOpenAt, "last_open_at": r.LastOpenAt,
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
}
// UserReadingProgressPost POST /api/user/reading-progress
func UserReadingProgressPost(c *gin.Context) {
var body struct {
UserID string `json:"userId" binding:"required"`
SectionID string `json:"sectionId" binding:"required"`
Progress int `json:"progress"`
Duration int `json:"duration"`
Status string `json:"status"`
CompletedAt *string `json:"completedAt"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少必要参数"})
return
}
db := database.DB()
now := time.Now()
var existing model.ReadingProgress
err := db.Where("user_id = ? AND section_id = ?", body.UserID, body.SectionID).First(&existing).Error
if err == nil {
newProgress := existing.Progress
if body.Progress > newProgress {
newProgress = body.Progress
}
newDuration := existing.Duration + body.Duration
newStatus := body.Status
if newStatus == "" {
newStatus = "reading"
}
var completedAt *time.Time
if body.CompletedAt != nil && *body.CompletedAt != "" {
t, _ := time.Parse(time.RFC3339, *body.CompletedAt)
completedAt = &t
} else if existing.CompletedAt != nil {
completedAt = existing.CompletedAt
}
db.Model(&existing).Updates(map[string]interface{}{
"progress": newProgress, "duration": newDuration, "status": newStatus,
"completed_at": completedAt, "last_open_at": now, "updated_at": now,
})
} else {
status := body.Status
if status == "" {
status = "reading"
}
var completedAt *time.Time
if body.CompletedAt != nil && *body.CompletedAt != "" {
t, _ := time.Parse(time.RFC3339, *body.CompletedAt)
completedAt = &t
}
db.Create(&model.ReadingProgress{
UserID: body.UserID, SectionID: body.SectionID, Progress: body.Progress, Duration: body.Duration,
Status: status, CompletedAt: completedAt, FirstOpenAt: &now, LastOpenAt: &now,
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "进度已保存"})
}
// UserTrackGet GET /api/user/track?userId=&limit= 从 user_tracks 表查GORM
func UserTrackGet(c *gin.Context) {
userId := c.Query("userId")
phone := c.Query("phone")
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
if limit < 1 || limit > 100 {
limit = 50
}
if userId == "" && phone == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "需要用户ID或手机号"})
return
}
db := database.DB()
if userId == "" && phone != "" {
var u model.User
if err := db.Where("phone = ?", phone).First(&u).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户不存在"})
return
}
userId = u.ID
}
var tracks []model.UserTrack
if err := db.Where("user_id = ?", userId).Order("created_at DESC").Limit(limit).Find(&tracks).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": true, "tracks": []interface{}{}, "stats": gin.H{}, "total": 0})
return
}
stats := make(map[string]int)
formatted := make([]gin.H, 0, len(tracks))
for _, t := range tracks {
stats[t.Action]++
target := ""
if t.Target != nil {
target = *t.Target
}
if t.ChapterID != nil && target == "" {
target = *t.ChapterID
}
formatted = append(formatted, gin.H{
"id": t.ID, "action": t.Action, "target": target, "chapterTitle": t.ChapterID,
"createdAt": t.CreatedAt,
})
}
c.JSON(http.StatusOK, gin.H{"success": true, "tracks": formatted, "stats": stats, "total": len(formatted)})
}
// UserTrackPost POST /api/user/track 记录行为GORM
func UserTrackPost(c *gin.Context) {
var body struct {
UserID string `json:"userId"`
Phone string `json:"phone"`
Action string `json:"action"`
Target string `json:"target"`
ExtraData interface{} `json:"extraData"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请求体无效"})
return
}
if body.UserID == "" && body.Phone == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "需要用户ID或手机号"})
return
}
if body.Action == "" {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "行为类型不能为空"})
return
}
db := database.DB()
userId := body.UserID
if userId == "" {
var u model.User
if err := db.Where("phone = ?", body.Phone).First(&u).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户不存在"})
return
}
userId = u.ID
}
trackID := fmt.Sprintf("track_%d", time.Now().UnixNano()%100000000)
chID := body.Target
if body.Action == "view_chapter" {
chID = body.Target
}
t := model.UserTrack{
ID: trackID, UserID: userId, Action: body.Action, Target: &body.Target,
}
if body.Target != "" {
t.ChapterID = &chID
}
if err := db.Create(&t).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "trackId": trackID, "message": "行为记录成功"})
}
// UserUpdate POST /api/user/update 更新昵称、头像、手机、微信号等
func UserUpdate(c *gin.Context) {
var body struct {
UserID string `json:"userId" binding:"required"`
Nickname *string `json:"nickname"`
Avatar *string `json:"avatar"`
Phone *string `json:"phone"`
Wechat *string `json:"wechat"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少用户ID"})
return
}
updates := make(map[string]interface{})
if body.Nickname != nil {
updates["nickname"] = *body.Nickname
}
if body.Avatar != nil {
updates["avatar"] = *body.Avatar
}
if body.Phone != nil {
updates["phone"] = *body.Phone
}
if body.Wechat != nil {
updates["wechat_id"] = *body.Wechat
}
if len(updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "没有需要更新的字段"})
return
}
updates["updated_at"] = time.Now()
if err := database.DB().Model(&model.User{}).Where("id = ?", body.UserID).Updates(updates).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "更新失败"})
return
}
c.JSON(http.StatusOK, gin.H{"success": true, "message": "更新成功"})
}