feat: rebuild pet app and backend around core care flows
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
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})
|
||||
}
|
||||
Reference in New Issue
Block a user