Files

255 lines
6.7 KiB
Go

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})
}