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, "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, }}) 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 c.JSON(http.StatusOK, gin.H{"success": true, "data": 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, }}) } // 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"` } 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 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 } db.Raw("SELECT DISTINCT product_id FROM orders WHERE user_id = ? AND status = ? AND product_type = ?", userId, "paid", "section").Scan(&orderRows) purchasedSections := make([]string, 0, len(orderRows)) for _, r := range orderRows { if r.ProductID != "" { purchasedSections = append(purchasedSections, r.ProductID) } } // 匹配次数配额:纯计算(订单 + 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, "purchasedCount": len(purchasedSections), "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": "更新成功"}) }