Files
silk/server-go/internal/handler/trace.go
T

346 lines
11 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"strconv"
"time"
"silk-server-go/internal/middleware"
"silk-server-go/internal/model"
"silk-server-go/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// RegisterTraceRoutes 注册疫病溯源路由
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/monthly-stats", read, traceMonthlyStats(db))
rg.GET("/trace-records/:id", read, getTraceRecord(db))
rg.POST("/trace-records", write, createTraceRecord(db))
rg.PATCH("/trace-records/:id", write, updateTraceRecord(db))
rg.DELETE("/trace-records/:id", write, deleteTraceRecord(db))
rg.POST("/trace-records/:id/auto", write, autoTrace(db))
rg.GET("/trace-records/:id/checklist", read, getTraceChecklist(db))
rg.POST("/trace-records/:id/checklist", write, submitTraceChecklist(db))
}
// traceMonthlyStats 年度发病规律(按月聚合)
func traceMonthlyStats(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
year := time.Now().Year()
if y, err := strconv.Atoi(c.DefaultQuery("year", "0")); err == nil && y >= 2020 && y <= 2100 {
year = y
}
start := time.Date(year, 1, 1, 0, 0, 0, 0, time.Local)
end := start.AddDate(1, 0, 0)
var rows []struct {
Month string `gorm:"column:month"`
Disease string `gorm:"column:disease"`
}
db.Table("trace_records").
Select("to_char(created_at, 'YYYY-MM') AS month, disease").
Where("created_at >= ? AND created_at < ?", start, end).
Scan(&rows)
entries := make([]service.MonthDiseaseEntry, 0, len(rows))
for _, r := range rows {
entries = append(entries, service.MonthDiseaseEntry{Month: r.Month, Disease: r.Disease})
}
c.JSON(http.StatusOK, gin.H{"year": year, "stats": service.AggregateMonthlyStats(entries)})
}
}
// 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{})
if status := c.Query("status"); status != "" {
q = q.Where("status = ?", status)
}
if room := c.Query("roomId"); room != "" {
q = q.Where("room_id = ?", room)
}
var list []model.TraceRecord
q.Order("created_at DESC").Find(&list)
fillTraceRoomNames(db, list)
c.JSON(http.StatusOK, list)
}
}
func fillTraceRoomNames(db *gorm.DB, list []model.TraceRecord) {
var rooms []model.Room
db.Select("id", "name").Find(&rooms)
names := make(map[string]string, len(rooms))
for _, r := range rooms {
names[r.ID] = r.Name
}
for i := range list {
if list[i].RoomID != nil {
if n, ok := names[*list[i].RoomID]; ok {
list[i].RoomName = &n
}
}
}
}
func getTraceRecord(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var t model.TraceRecord
if db.Where("id = ?", c.Param("id")).First(&t).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "trace record not found"})
return
}
fillTraceRoomNames(db, []model.TraceRecord{t})
c.JSON(http.StatusOK, t)
}
}
func createTraceRecord(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var body struct {
RoomID *string `json:"roomId"`
LampTestID *string `json:"lampTestId"`
ConsultationID *string `json:"consultationId"`
Disease string `json:"disease"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
for _, id := range []*string{body.RoomID, body.LampTestID, body.ConsultationID} {
if id != nil && *id != "" && !isUUID(*id) {
c.JSON(http.StatusBadRequest, gin.H{"error": "关联 ID 不是合法的 UUID"})
return
}
}
if body.Disease == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "病种不能为空"})
return
}
rec := model.TraceRecord{
RoomID: body.RoomID, LampTestID: body.LampTestID,
ConsultationID: body.ConsultationID, Disease: body.Disease, Status: "pending",
}
if err := db.Create(&rec).Error; err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
return
}
c.JSON(http.StatusCreated, rec)
}
}
func updateTraceRecord(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var t model.TraceRecord
if db.Where("id = ?", id).First(&t).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "trace record not found"})
return
}
updates, err := bindUpdates(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if len(updates) > 0 {
db.Model(&model.TraceRecord{}).Where("id = ?", id).Updates(updates)
}
db.Where("id = ?", id).First(&t)
c.JSON(http.StatusOK, t)
}
}
func deleteTraceRecord(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var t model.TraceRecord
if db.Where("id = ?", id).First(&t).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "trace record not found"})
return
}
db.Where("id = ?", id).Delete(&model.TraceRecord{})
c.JSON(http.StatusOK, gin.H{"id": id})
}
}
// autoTrace 一级自动溯源:环境回溯 + 历史关联 + 传播推断 → 溯源初报
func autoTrace(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var t model.TraceRecord
if db.Where("id = ?", id).First(&t).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "trace record not found"})
return
}
report := map[string]interface{}{
"disease": t.Disease,
"generatedAt": time.Now(),
}
origin := "unknown"
confidence := 0.4
// 环境回溯(房间最近温湿度)
var samples []service.EnvSample
if t.RoomID != nil {
var room model.Room
if db.Where("id = ?", *t.RoomID).First(&room).Error == nil {
report["roomName"] = room.Name
}
type row struct {
Metric string `gorm:"column:metric"`
Value float64 `gorm:"column:value"`
}
var rows []row
db.Table("telemetry").
Select("telemetry.metric, telemetry.value").
Joins("JOIN devices ON devices.device_key = telemetry.device_key AND devices.room_id = ?", *t.RoomID).
Where("telemetry.metric IN ?", []string{"humidity", "temperature"}).
Order("telemetry.timestamp DESC").
Limit(40).
Scan(&rows)
for _, r := range rows {
s := service.EnvSample{}
if r.Metric == "humidity" {
s.Humidity = &r.Value
} else {
s.Temp = &r.Value
}
samples = append(samples, s)
}
}
env := service.EnvironmentBacktrack(samples, 80)
report["environment"] = env
// 历史发病关联(同房间同病种 LAMP 记录)
pastCount := int64(0)
var past []service.PastEvent
if t.RoomID != nil {
raw, _ := json.Marshal([]string{t.Disease})
db.Model(&model.LampTest{}).
Where("room_id = ? AND status = 'resulted' AND diseases @> ?", *t.RoomID, string(raw)).
Count(&pastCount)
}
for i := int64(0); i < pastCount && i < 5; i++ {
past = append(past, service.PastEvent{Disease: t.Disease, DaysAgo: 15})
}
continuous := service.HistoryAssociation(past, t.Disease)
report["history"] = map[string]interface{}{
"continuous": continuous,
"pastCount": pastCount,
}
// 传播途径推断
mode, source := service.TransmissionInference(t.Disease)
report["transmission"] = map[string]interface{}{"mode": mode, "source": source}
// 初步来源判定
if continuous {
origin = "internal"
confidence = 0.7
} else if env.HighHumidity || env.TempSwing {
origin = "internal"
confidence = 0.6
}
report["origin"] = origin
report["confidence"] = confidence
raw, _ := json.Marshal(report)
db.Model(&model.TraceRecord{}).Where("id = ?", id).Updates(map[string]interface{}{
"auto_report": raw, "origin": origin, "confidence": confidence, "status": "reported",
})
db.Where("id = ?", id).First(&t)
c.JSON(http.StatusOK, t)
}
}
// getTraceChecklist 分病种二级排查清单
func getTraceChecklist(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var t model.TraceRecord
if db.Where("id = ?", c.Param("id")).First(&t).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "trace record not found"})
return
}
c.JSON(http.StatusOK, service.DiseaseChecklist(t.Disease))
}
}
// submitTraceChecklist 二级:提交排查清单 → 溯源分析报告
func submitTraceChecklist(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
var t model.TraceRecord
if db.Where("id = ?", id).First(&t).Error != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "trace record not found"})
return
}
var body struct {
Answers []struct {
Key string `json:"key"`
Answer string `json:"answer"`
} `json:"answers"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
template := service.DiseaseChecklist(t.Disease)
byKey := make(map[string]service.ChecklistItem, len(template))
for _, item := range template {
byKey[item.Key] = item
}
var answers []service.ChecklistAnswer
for _, a := range body.Answers {
item, ok := byKey[a.Key]
if !ok {
continue
}
answers = append(answers, service.ChecklistAnswer{
Key: item.Key, Label: item.Label, Answer: a.Answer, InternalBias: item.InternalBias,
})
}
origin, confidence, conclusion := service.AnalyzeChecklist(answers)
report := map[string]interface{}{
"origin": origin, "confidence": confidence, "conclusion": conclusion,
"answers": answers, "generatedAt": time.Now(),
}
raw, _ := json.Marshal(report)
db.Model(&model.TraceRecord{}).Where("id = ?", id).Updates(map[string]interface{}{
"analysis_report": raw, "origin": origin, "confidence": confidence, "status": "analysis",
})
db.Where("id = ?", id).First(&t)
c.JSON(http.StatusOK, t)
}
}