package httpapi import ( "net/http" "strings" "time" "github.com/gin-gonic/gin" "github.com/sundynix/pets-be/internal/model" "gorm.io/gorm" ) func (a *API) meetupView(m model.Meetup) gin.H { var count int64 a.db.Model(&model.MeetupParticipant{}).Where("meetup_id = ?", m.ID).Count(&count) return gin.H{"id": m.ID, "host_user_id": m.HostUserID, "title": m.Title, "description": m.Description, "place_name": m.PlaceName, "latitude": m.Latitude, "longitude": m.Longitude, "starts_at": m.StartsAt, "max_people": m.MaxPeople, "status": m.Status, "participants": count, "cover_file_id": m.CoverFileID, "cover_url": a.fileURL(m.CoverFileID)} } func (a *API) listMeetups(c *gin.Context) { var rows []model.Meetup if a.db.Where("starts_at > ? AND status = ?", time.Now().Add(-time.Hour), "open").Order("starts_at ASC").Limit(100).Find(&rows).Error != nil { fail(c, 500, "读取约遛失败") return } items := make([]gin.H, 0, len(rows)) for _, m := range rows { items = append(items, a.meetupView(m)) } ok(c, items) } func (a *API) getMeetup(c *gin.Context) { var m model.Meetup if !validID(c.Param("id")) || a.db.First(&m, "id = ?", c.Param("id")).Error != nil { fail(c, 404, "约遛不存在") return } view := a.meetupView(m) var joins []model.MeetupParticipant a.db.Where("meetup_id = ?", m.ID).Find(&joins) people := make([]gin.H, 0, len(joins)) for _, j := range joins { var pets []model.MeetupPet a.db.Where("participant_id = ?", j.ID).Find(&pets) ids := []string{} for _, p := range pets { ids = append(ids, p.PetID) } people = append(people, gin.H{"user_id": j.UserID, "pet_ids": ids}) } view["people"] = people ok(c, view) } type meetupInput struct { Title string `json:"title" binding:"required"` Description string `json:"description"` PlaceName string `json:"place_name" binding:"required"` Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` StartsAt time.Time `json:"starts_at" binding:"required"` MaxPeople int `json:"max_people"` PetIDs []string `json:"pet_ids" binding:"required"` CoverFileID *string `json:"cover_file_id"` } func (a *API) createMeetup(c *gin.Context) { var in meetupInput if c.ShouldBindJSON(&in) != nil || strings.TrimSpace(in.Title) == "" || strings.TrimSpace(in.PlaceName) == "" || !in.StartsAt.After(time.Now()) || len(in.PetIDs) == 0 { fail(c, 400, "约遛资料无效") return } if in.MaxPeople == 0 { in.MaxPeople = 8 } if in.MaxPeople < 2 || in.MaxPeople > 100 { fail(c, 400, "人数应为 2 到 100") return } for _, id := range in.PetIDs { if _, err := a.ownedPet(actor(c), id); err != nil { fail(c, 400, "宠物无效") return } } if in.CoverFileID != nil && !a.fileExists(*in.CoverFileID) { fail(c, 400, "封面文件无效") return } m := model.Meetup{HostUserID: actor(c), Title: strings.TrimSpace(in.Title), Description: strings.TrimSpace(in.Description), PlaceName: strings.TrimSpace(in.PlaceName), Latitude: in.Latitude, Longitude: in.Longitude, StartsAt: in.StartsAt, MaxPeople: in.MaxPeople, Status: "open", CoverFileID: in.CoverFileID} err := a.db.Transaction(func(tx *gorm.DB) error { if e := tx.Create(&m).Error; e != nil { return e } j := model.MeetupParticipant{MeetupID: m.ID, UserID: actor(c)} if e := tx.Create(&j).Error; e != nil { return e } for _, id := range in.PetIDs { if e := tx.Create(&model.MeetupPet{ParticipantID: j.ID, PetID: id}).Error; e != nil { return e } } return nil }) if err != nil { fail(c, 500, "发布失败") return } created(c, a.meetupView(m)) } func (a *API) joinMeetup(c *gin.Context) { var in struct { PetIDs []string `json:"pet_ids" binding:"required"` } if c.ShouldBindJSON(&in) != nil || len(in.PetIDs) == 0 { fail(c, 400, "请选择宠物") return } for _, id := range in.PetIDs { if _, err := a.ownedPet(actor(c), id); err != nil { fail(c, 400, "宠物无效") return } } var m model.Meetup if !validID(c.Param("id")) || a.db.First(&m, "id = ?", c.Param("id")).Error != nil { fail(c, 404, "约遛不存在") return } if m.Status != "open" || !m.StartsAt.After(time.Now()) { fail(c, 409, "约遛已关闭") return } var count int64 a.db.Model(&model.MeetupParticipant{}).Where("meetup_id = ?", m.ID).Count(&count) if count >= int64(m.MaxPeople) { fail(c, 409, "人数已满") return } var existing int64 a.db.Model(&model.MeetupParticipant{}).Where("meetup_id = ? AND user_id = ?", m.ID, actor(c)).Count(&existing) if existing > 0 { fail(c, 409, "已经参加") return } j := model.MeetupParticipant{MeetupID: m.ID, UserID: actor(c)} err := a.db.Transaction(func(tx *gorm.DB) error { if e := tx.Create(&j).Error; e != nil { return e } for _, id := range in.PetIDs { if e := tx.Create(&model.MeetupPet{ParticipantID: j.ID, PetID: id}).Error; e != nil { return e } } return nil }) if err != nil { fail(c, 500, "报名失败") return } ok(c, a.meetupView(m)) } func (a *API) leaveMeetup(c *gin.Context) { var j model.MeetupParticipant if !validID(c.Param("id")) || a.db.Where("meetup_id = ? AND user_id = ?", c.Param("id"), actor(c)).First(&j).Error != nil { fail(c, 404, "未参加") return } var m model.Meetup a.db.First(&m, "id = ?", j.MeetupID) if m.HostUserID == actor(c) { fail(c, http.StatusConflict, "发起人请取消约遛") return } if a.db.Transaction(func(tx *gorm.DB) error { if e := tx.Where("participant_id = ?", j.ID).Delete(&model.MeetupPet{}).Error; e != nil { return e } return tx.Delete(&j).Error }) != nil { fail(c, 500, "退出失败") return } ok(c, gin.H{"left": true}) } func (a *API) cancelMeetup(c *gin.Context) { if !validID(c.Param("id")) { fail(c, 400, "ID 无效") return } result := a.db.Model(&model.Meetup{}).Where("id = ? AND host_user_id = ? AND status = ?", c.Param("id"), actor(c), "open").Update("status", "cancelled") if result.Error != nil || result.RowsAffected == 0 { fail(c, 404, "约遛不存在或已关闭") return } ok(c, gin.H{"status": "cancelled"}) }