package httpapi import ( "math" "net/http" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/sundynix/pets-be/internal/model" "gorm.io/gorm" ) func (a *API) walk(c *gin.Context) (model.Walk, bool) { var w model.Walk if !validID(c.Param("id")) || a.db.Where("id = ? AND user_id = ?", c.Param("id"), actor(c)).First(&w).Error != nil { fail(c, 404, "遛狗记录不存在") return w, false } return w, true } func (a *API) walkView(w model.Walk) gin.H { var links []model.WalkPet var points []model.WalkPoint a.db.Where("walk_id = ?", w.ID).Find(&links) a.db.Where("walk_id = ?", w.ID).Order("recorded_at ASC").Find(&points) ids := make([]string, 0, len(links)) for _, p := range links { ids = append(ids, p.PetID) } return gin.H{"id": w.ID, "pet_ids": ids, "status": w.Status, "started_at": w.StartedAt, "ended_at": w.EndedAt, "paused_at": w.PausedAt, "paused_seconds": w.PausedSeconds, "duration_seconds": w.DurationSeconds, "distance_meters": w.DistanceMeters, "share_enabled": w.ShareEnabled, "share_token": func() string { if w.ShareEnabled { return w.ShareToken } return "" }(), "share_expires_at": w.ShareExpiresAt, "points": points} } func (a *API) listWalks(c *gin.Context) { var walks []model.Walk if a.db.Where("user_id = ?", actor(c)).Order("started_at DESC").Limit(50).Find(&walks).Error != nil { fail(c, 500, "读取记录失败") return } items := make([]gin.H, 0, len(walks)) for _, w := range walks { items = append(items, a.walkView(w)) } ok(c, items) } func (a *API) getWalk(c *gin.Context) { if w, yes := a.walk(c); yes { ok(c, a.walkView(w)) } } func (a *API) startWalk(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 } seen := map[string]bool{} for _, id := range in.PetIDs { if seen[id] { fail(c, 400, "宠物重复") return } seen[id] = true if _, err := a.ownedPet(actor(c), id); err != nil { fail(c, 400, "选择的宠物不存在") return } } var active int64 a.db.Model(&model.Walk{}).Where("user_id = ? AND status IN ?", actor(c), []string{"active", "paused"}).Count(&active) if active > 0 { fail(c, 409, "已有正在进行的遛狗") return } now := time.Now() w := model.Walk{UserID: actor(c), Status: "active", StartedAt: now, SegmentStartedAt: now, ShareToken: uuid.NewString()} if err := a.db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&w).Error; err != nil { return err } for _, id := range in.PetIDs { if err := tx.Create(&model.WalkPet{WalkID: w.ID, PetID: id}).Error; err != nil { return err } } return nil }); err != nil { fail(c, 500, "开始遛狗失败") return } created(c, a.walkView(w)) } func (a *API) addWalkPoint(c *gin.Context) { w, yes := a.walk(c) if !yes { return } if w.Status != "active" { fail(c, 409, "遛狗未进行中") return } var in struct { Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` } if c.ShouldBindJSON(&in) != nil || math.Abs(in.Latitude) > 90 || math.Abs(in.Longitude) > 180 || (in.Latitude == 0 && in.Longitude == 0) { fail(c, 400, "位置无效") return } point := model.WalkPoint{WalkID: w.ID, Latitude: in.Latitude, Longitude: in.Longitude, RecordedAt: time.Now()} var last model.WalkPoint err := a.db.Where("walk_id = ?", w.ID).Order("recorded_at DESC").First(&last).Error if err == nil && !last.RecordedAt.Before(w.SegmentStartedAt) { d := haversine(last.Latitude, last.Longitude, in.Latitude, in.Longitude) if d > 1000 { fail(c, 400, "位置跳变过大") return } if d < 5 { ok(c, a.walkView(w)) return } w.DistanceMeters += d } if txErr := a.db.Transaction(func(tx *gorm.DB) error { if e := tx.Create(&point).Error; e != nil { return e } return tx.Model(&w).Update("distance_meters", w.DistanceMeters).Error }); txErr != nil { fail(c, 500, "保存位置失败") return } ok(c, a.walkView(w)) } func haversine(a, b, c, d float64) float64 { const r = 6371000. x1, x2 := a*math.Pi/180, c*math.Pi/180 dx, dy := (c-a)*math.Pi/180, (d-b)*math.Pi/180 h := math.Pow(math.Sin(dx/2), 2) + math.Cos(x1)*math.Cos(x2)*math.Pow(math.Sin(dy/2), 2) return 2 * r * math.Asin(math.Sqrt(h)) } func (a *API) pauseWalk(c *gin.Context) { w, yes := a.walk(c) if !yes { return } if w.Status != "active" { fail(c, 409, "当前不能暂停") return } now := time.Now() w.Status = "paused" w.PausedAt = &now if a.db.Save(&w).Error != nil { fail(c, 500, "暂停失败") return } ok(c, a.walkView(w)) } func (a *API) resumeWalk(c *gin.Context) { w, yes := a.walk(c) if !yes { return } if w.Status != "paused" || w.PausedAt == nil { fail(c, 409, "当前不能继续") return } now := time.Now() w.PausedSeconds += int(now.Sub(*w.PausedAt).Seconds()) w.PausedAt = nil w.SegmentStartedAt = now w.Status = "active" if a.db.Save(&w).Error != nil { fail(c, 500, "继续失败") return } ok(c, a.walkView(w)) } func (a *API) finishWalk(c *gin.Context) { w, yes := a.walk(c) if !yes { return } if w.Status == "finished" { fail(c, 409, "已结束") return } now := time.Now() if w.PausedAt != nil { w.PausedSeconds += int(now.Sub(*w.PausedAt).Seconds()) w.PausedAt = nil } w.Status = "finished" w.EndedAt = &now w.DurationSeconds = int(now.Sub(w.StartedAt).Seconds()) - w.PausedSeconds w.ShareEnabled = false w.ShareExpiresAt = nil if a.db.Save(&w).Error != nil { fail(c, 500, "结束遛狗失败") return } ok(c, a.walkView(w)) } func (a *API) setWalkShare(c *gin.Context) { w, yes := a.walk(c) if !yes { return } if w.Status == "finished" { fail(c, 409, "已结束的遛狗不能分享") return } var in struct { Enabled bool `json:"enabled"` } if c.ShouldBindJSON(&in) != nil { fail(c, 400, "参数无效") return } w.ShareEnabled = in.Enabled if in.Enabled { expire := time.Now().Add(4 * time.Hour) w.ShareExpiresAt = &expire w.ShareToken = uuid.NewString() } else { w.ShareExpiresAt = nil } if a.db.Save(&w).Error != nil { fail(c, 500, "分享设置失败") return } ok(c, a.walkView(w)) } func (a *API) publicWalkLocation(c *gin.Context) { var w model.Walk if !validID(c.Param("token")) || a.db.Where("share_token = ? AND share_enabled = true AND status IN ? AND share_expires_at > ?", c.Param("token"), []string{"active", "paused"}, time.Now()).First(&w).Error != nil { fail(c, http.StatusNotFound, "分享已失效") return } var p model.WalkPoint if a.db.Where("walk_id = ?", w.ID).Order("recorded_at DESC").First(&p).Error != nil { fail(c, 404, "暂无位置") return } ok(c, gin.H{"latitude": p.Latitude, "longitude": p.Longitude, "recorded_at": p.RecordedAt, "status": w.Status}) }