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("空输入应返回空结果") + } +}