2026-03-07 22:58:43 +08:00
|
|
|
|
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
|
|
|
|
|
|
q := db.Select("id", "open_id", "nickname", "avatar", "phone", "wechat_id", "referral_code",
|
|
|
|
|
|
"has_full_book", "earnings", "pending_earnings", "referral_count", "created_at",
|
|
|
|
|
|
"mbti", "region", "industry", "position", "business_scale", "skills",
|
|
|
|
|
|
"story_best_month", "story_achievement", "story_turning", "help_offer", "help_need", "project_intro")
|
|
|
|
|
|
if userId != "" {
|
|
|
|
|
|
q = q.Where("id = ?", userId)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
q = q.Where("open_id = ?", openId)
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := q.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
|
|
|
|
|
|
str := func(p *string) interface{} { if p != nil { return *p }; return "" }
|
|
|
|
|
|
resp := gin.H{
|
|
|
|
|
|
"id": user.ID, "openId": user.OpenID, "nickname": str(user.Nickname), "avatar": str(user.Avatar),
|
|
|
|
|
|
"phone": str(user.Phone), "wechatId": str(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 资料扩展:统一返回所有表单字段,空值用 "" 便于前端回显
|
|
|
|
|
|
"mbti": str(user.Mbti), "region": str(user.Region), "industry": str(user.Industry),
|
|
|
|
|
|
"position": str(user.Position), "businessScale": str(user.BusinessScale), "skills": str(user.Skills),
|
|
|
|
|
|
"storyBestMonth": str(user.StoryBestMonth), "storyAchievement": str(user.StoryAchievement),
|
|
|
|
|
|
"storyTurning": str(user.StoryTurning), "helpOffer": str(user.HelpOffer), "helpNeed": str(user.HelpNeed),
|
|
|
|
|
|
"projectIntro": str(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)
|
|
|
|
|
|
// 重新查询并返回与 GET 一致的完整资料结构,空值统一为 ""
|
|
|
|
|
|
profileCols := []string{"id", "open_id", "nickname", "avatar", "phone", "wechat_id", "referral_code", "created_at",
|
|
|
|
|
|
"mbti", "region", "industry", "position", "business_scale", "skills",
|
|
|
|
|
|
"story_best_month", "story_achievement", "story_turning", "help_offer", "help_need", "project_intro"}
|
|
|
|
|
|
if err := database.DB().Select(profileCols).Where("id = ?", user.ID).First(&user).Error; err == nil {
|
|
|
|
|
|
str := func(p *string) interface{} { if p != nil { return *p }; return "" }
|
|
|
|
|
|
resp := gin.H{
|
|
|
|
|
|
"id": user.ID, "openId": user.OpenID, "nickname": str(user.Nickname), "avatar": str(user.Avatar),
|
|
|
|
|
|
"phone": str(user.Phone), "wechatId": str(user.WechatID), "referralCode": user.ReferralCode,
|
|
|
|
|
|
"createdAt": user.CreatedAt,
|
|
|
|
|
|
"mbti": str(user.Mbti), "region": str(user.Region), "industry": str(user.Industry),
|
|
|
|
|
|
"position": str(user.Position), "businessScale": str(user.BusinessScale), "skills": str(user.Skills),
|
|
|
|
|
|
"storyBestMonth": str(user.StoryBestMonth), "storyAchievement": str(user.StoryAchievement),
|
|
|
|
|
|
"storyTurning": str(user.StoryTurning), "helpOffer": str(user.HelpOffer), "helpNeed": str(user.HelpNeed),
|
|
|
|
|
|
"projectIntro": str(user.ProjectIntro),
|
|
|
|
|
|
}
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"success": true, "message": "资料更新成功", "data": resp})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
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": "进度已保存"})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-08 08:00:39 +08:00
|
|
|
|
// UserDashboardStatsGet GET /api/user/dashboard-stats?userId=
|
|
|
|
|
|
// 返回我的页所需的真实统计:已读章节、阅读分钟、最近阅读、匹配次数
|
|
|
|
|
|
func UserDashboardStatsGet(c *gin.Context) {
|
|
|
|
|
|
userId := c.Query("userId")
|
|
|
|
|
|
if userId == "" {
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 userId 参数"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
db := database.DB()
|
|
|
|
|
|
|
|
|
|
|
|
var progressList []model.ReadingProgress
|
|
|
|
|
|
if err := db.Where("user_id = ?", userId).Order("last_open_at DESC").Find(&progressList).Error; err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "读取阅读统计失败"})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
readCount := len(progressList)
|
|
|
|
|
|
totalReadSeconds := 0
|
|
|
|
|
|
recentIDs := make([]string, 0, 5)
|
|
|
|
|
|
seenRecent := make(map[string]bool)
|
|
|
|
|
|
readSectionIDs := make([]string, 0, len(progressList))
|
|
|
|
|
|
for _, item := range progressList {
|
|
|
|
|
|
totalReadSeconds += item.Duration
|
|
|
|
|
|
if item.SectionID != "" {
|
|
|
|
|
|
readSectionIDs = append(readSectionIDs, item.SectionID)
|
|
|
|
|
|
if !seenRecent[item.SectionID] && len(recentIDs) < 5 {
|
|
|
|
|
|
seenRecent[item.SectionID] = true
|
|
|
|
|
|
recentIDs = append(recentIDs, item.SectionID)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
totalReadMinutes := totalReadSeconds / 60
|
|
|
|
|
|
if totalReadSeconds > 0 && totalReadMinutes == 0 {
|
|
|
|
|
|
totalReadMinutes = 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
chapterMap := make(map[string]model.Chapter)
|
|
|
|
|
|
if len(recentIDs) > 0 {
|
|
|
|
|
|
var chapters []model.Chapter
|
|
|
|
|
|
if err := db.Select("id", "mid", "section_title").Where("id IN ?", recentIDs).Find(&chapters).Error; err == nil {
|
|
|
|
|
|
for _, ch := range chapters {
|
|
|
|
|
|
chapterMap[ch.ID] = ch
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
recentChapters := make([]gin.H, 0, len(recentIDs))
|
|
|
|
|
|
for _, id := range recentIDs {
|
|
|
|
|
|
ch, ok := chapterMap[id]
|
|
|
|
|
|
title := id
|
|
|
|
|
|
mid := 0
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
if ch.SectionTitle != "" {
|
|
|
|
|
|
title = ch.SectionTitle
|
|
|
|
|
|
}
|
|
|
|
|
|
mid = ch.MID
|
|
|
|
|
|
}
|
|
|
|
|
|
recentChapters = append(recentChapters, gin.H{
|
|
|
|
|
|
"id": id,
|
|
|
|
|
|
"mid": mid,
|
|
|
|
|
|
"title": title,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var matchHistory int64
|
|
|
|
|
|
db.Model(&model.MatchRecord{}).Where("user_id = ?", userId).Count(&matchHistory)
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
|
|
|
|
"success": true,
|
|
|
|
|
|
"data": gin.H{
|
|
|
|
|
|
"readCount": readCount,
|
|
|
|
|
|
"totalReadMinutes": totalReadMinutes,
|
|
|
|
|
|
"recentChapters": recentChapters,
|
|
|
|
|
|
"matchHistory": matchHistory,
|
|
|
|
|
|
"readSectionIds": readSectionIDs,
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 22:58:43 +08:00
|
|
|
|
// 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": "更新成功"})
|
|
|
|
|
|
}
|