From 7949f90c0c6931174e3ac4232ebee8a0f852b6bd Mon Sep 17 00:00:00 2001 From: weijuesen Date: Thu, 13 Aug 2026 08:53:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(server-go):=20=E5=8C=BA=E5=9F=9F=E5=8F=91?= =?UTF-8?q?=E7=97=85=E7=BB=9F=E8=AE=A1=EF=BC=88#22=EF=BC=8Crooms.region=20?= =?UTF-8?q?+=20region-stats=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server-go/internal/handler/trace.go | 29 ++++++++++++ server-go/internal/model/models.go | 1 + server-go/internal/service/region_stat.go | 44 +++++++++++++++++++ .../internal/service/region_stat_test.go | 32 ++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 server-go/internal/service/region_stat.go create mode 100644 server-go/internal/service/region_stat_test.go diff --git a/server-go/internal/handler/trace.go b/server-go/internal/handler/trace.go index 3d5b4d2..8544d93 100644 --- a/server-go/internal/handler/trace.go +++ b/server-go/internal/handler/trace.go @@ -3,6 +3,7 @@ package handler import ( "encoding/json" "net/http" + "strconv" "time" "silk-server-go/internal/middleware" @@ -18,6 +19,7 @@ func RegisterTraceRoutes(rg *gin.RouterGroup, db *gorm.DB) { read := middleware.RequirePermission(db, "trace:read") write := middleware.RequirePermission(db, "trace:write") rg.GET("/trace-records", read, listTraceRecords(db)) + rg.GET("/trace-records/region-stats", read, traceRegionStats(db)) rg.GET("/trace-records/:id", read, getTraceRecord(db)) rg.POST("/trace-records", write, createTraceRecord(db)) rg.PATCH("/trace-records/:id", write, updateTraceRecord(db)) @@ -27,6 +29,33 @@ func RegisterTraceRoutes(rg *gin.RouterGroup, db *gorm.DB) { rg.POST("/trace-records/:id/checklist", write, submitTraceChecklist(db)) } +// traceRegionStats 区域发病统计(近 N 天,按蚕房 region 聚合) +func traceRegionStats(db *gorm.DB) gin.HandlerFunc { + return func(c *gin.Context) { + days := 90 + if d, err := strconv.Atoi(c.DefaultQuery("days", "90")); err == nil && d > 0 && d <= 365 { + days = d + } + q := db.Table("trace_records"). + Select("rooms.region AS region, trace_records.disease AS disease"). + Joins("LEFT JOIN rooms ON rooms.id = trace_records.room_id"). + Where("trace_records.created_at >= ?", time.Now().Add(-time.Duration(days)*24*time.Hour)) + if disease := c.Query("disease"); disease != "" { + q = q.Where("trace_records.disease = ?", disease) + } + var rows []struct { + Region string `gorm:"column:region"` + Disease string `gorm:"column:disease"` + } + q.Scan(&rows) + entries := make([]service.RegionDiseaseEntry, 0, len(rows)) + for _, r := range rows { + entries = append(entries, service.RegionDiseaseEntry{Region: r.Region, Disease: r.Disease}) + } + c.JSON(http.StatusOK, service.AggregateRegionStats(entries)) + } +} + func listTraceRecords(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { q := db.Model(&model.TraceRecord{}) diff --git a/server-go/internal/model/models.go b/server-go/internal/model/models.go index 65851b8..27f4787 100644 --- a/server-go/internal/model/models.go +++ b/server-go/internal/model/models.go @@ -31,6 +31,7 @@ type Room struct { Description *string `json:"description,omitempty"` Capacity *int `gorm:"type:int" json:"capacity,omitempty"` Stage *string `gorm:"size:32" json:"stage,omitempty"` + Region *string `gorm:"size:64" json:"region,omitempty"` Status string `gorm:"default:active" json:"status"` CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"` UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"` diff --git a/server-go/internal/service/region_stat.go b/server-go/internal/service/region_stat.go new file mode 100644 index 0000000..9fdea2b --- /dev/null +++ b/server-go/internal/service/region_stat.go @@ -0,0 +1,44 @@ +package service + +import "sort" + +// RegionDiseaseEntry 区域发病条目(trace_records 联查 rooms 得到) +type RegionDiseaseEntry struct { + Region string + Disease string +} + +// RegionStat 区域发病统计 +type RegionStat struct { + Region string `json:"region"` + Total int `json:"total"` + Diseases map[string]int `json:"diseases"` +} + +// AggregateRegionStats 按区域分组统计发病数与病种分布(空区域剔除,按总数降序) +func AggregateRegionStats(entries []RegionDiseaseEntry) []RegionStat { + byRegion := make(map[string]*RegionStat) + for _, e := range entries { + if e.Region == "" { + continue + } + s, ok := byRegion[e.Region] + if !ok { + s = &RegionStat{Region: e.Region, Diseases: map[string]int{}} + byRegion[e.Region] = s + } + s.Total++ + s.Diseases[e.Disease]++ + } + result := make([]RegionStat, 0, len(byRegion)) + for _, s := range byRegion { + result = append(result, *s) + } + sort.Slice(result, func(i, j int) bool { + if result[i].Total != result[j].Total { + return result[i].Total > result[j].Total + } + return result[i].Region < result[j].Region + }) + return result +} diff --git a/server-go/internal/service/region_stat_test.go b/server-go/internal/service/region_stat_test.go new file mode 100644 index 0000000..44714aa --- /dev/null +++ b/server-go/internal/service/region_stat_test.go @@ -0,0 +1,32 @@ +package service + +import "testing" + +func TestAggregateRegionStats(t *testing.T) { + entries := []RegionDiseaseEntry{ + {Region: "A镇", Disease: "白僵病"}, + {Region: "A镇", Disease: "白僵病"}, + {Region: "A镇", Disease: "软化病"}, + {Region: "B乡", Disease: "白僵病"}, + {Region: "", Disease: "白僵病"}, + } + stats := AggregateRegionStats(entries) + if len(stats) != 2 { + t.Fatalf("统计区域数 = %d, want 2(空区域应剔除)", len(stats)) + } + if stats[0].Region != "A镇" || stats[0].Total != 3 { + t.Errorf("A镇应排第一且 total=3,实际 %+v", stats[0]) + } + if stats[0].Diseases["白僵病"] != 2 || stats[0].Diseases["软化病"] != 1 { + t.Errorf("A镇病种分布不正确: %+v", stats[0].Diseases) + } + if stats[1].Region != "B乡" || stats[1].Total != 1 { + t.Errorf("B乡统计不正确: %+v", stats[1]) + } +} + +func TestAggregateRegionStatsEmpty(t *testing.T) { + if len(AggregateRegionStats(nil)) != 0 { + t.Error("空输入应返回空结果") + } +}