feat(server-go): 区域发病统计(#22,rooms.region + region-stats)
This commit is contained in:
@@ -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{})
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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("空输入应返回空结果")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user