feat: 完善环境规则、会诊治理、知识审核与效果评估

This commit is contained in:
weijuesen
2026-08-14 10:33:17 +08:00
parent 69ef5a0704
commit 126c215a6b
27 changed files with 963 additions and 88 deletions
+49 -27
View File
@@ -7,46 +7,68 @@ import (
// Consultation 专家会诊单
type Consultation struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
RoomID *string `gorm:"column:room_id;type:uuid;index" json:"roomId,omitempty"`
BatchID *string `gorm:"column:batch_id;type:uuid;index" json:"batchId,omitempty"`
LampTestID *string `gorm:"column:lamp_test_id;type:uuid;index" json:"lampTestId,omitempty"`
Title string `gorm:"size:128" json:"title"`
Summary *string `gorm:"type:text" json:"summary,omitempty"`
Snapshot json.RawMessage `gorm:"type:jsonb" json:"snapshot,omitempty"`
Status string `gorm:"size:16;default:pending" json:"status"` // pending/consulting/resolved/archived
ExpertID *string `gorm:"column:expert_id;type:uuid" json:"expertId,omitempty"`
Opinion *string `gorm:"type:text" json:"opinion,omitempty"`
Plan *string `gorm:"type:text" json:"plan,omitempty"`
ResolvedAt *time.Time `gorm:"column:resolved_at;type:timestamptz" json:"resolvedAt,omitempty"`
ArchivedAt *time.Time `gorm:"column:archived_at;type:timestamptz" json:"archivedAt,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
RoomName *string `gorm:"-" json:"roomName,omitempty"`
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
RoomID *string `gorm:"column:room_id;type:uuid;index" json:"roomId,omitempty"`
BatchID *string `gorm:"column:batch_id;type:uuid;index" json:"batchId,omitempty"`
LampTestID *string `gorm:"column:lamp_test_id;type:uuid;index" json:"lampTestId,omitempty"`
Title string `gorm:"size:128" json:"title"`
Summary *string `gorm:"type:text" json:"summary,omitempty"`
Snapshot json.RawMessage `gorm:"type:jsonb" json:"snapshot,omitempty"`
Status string `gorm:"size:16;default:pending" json:"status"` // pending/consulting/resolved/archived
ExpertID *string `gorm:"column:expert_id;type:uuid" json:"expertId,omitempty"`
AssigneeID *string `gorm:"column:assignee_id;type:uuid" json:"assigneeId,omitempty"`
AssignedAt *time.Time `gorm:"column:assigned_at;type:timestamptz" json:"assignedAt,omitempty"`
AcceptedAt *time.Time `gorm:"column:accepted_at;type:timestamptz" json:"acceptedAt,omitempty"`
NeedsInfoAt *time.Time `gorm:"column:needs_info_at;type:timestamptz" json:"needsInfoAt,omitempty"`
SLADeadline *time.Time `gorm:"column:sla_deadline;type:timestamptz" json:"slaDeadline,omitempty"`
OverdueAt *time.Time `gorm:"column:overdue_at;type:timestamptz" json:"overdueAt,omitempty"`
LastSLAEventAt *time.Time `gorm:"column:last_sla_event_at;type:timestamptz" json:"lastSlaEventAt,omitempty"`
Opinion *string `gorm:"type:text" json:"opinion,omitempty"`
Plan *string `gorm:"type:text" json:"plan,omitempty"`
ResolvedAt *time.Time `gorm:"column:resolved_at;type:timestamptz" json:"resolvedAt,omitempty"`
ArchivedAt *time.Time `gorm:"column:archived_at;type:timestamptz" json:"archivedAt,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
RoomName *string `gorm:"-" json:"roomName,omitempty"`
}
func (Consultation) TableName() string { return "consultations" }
// ValidConsultationTransition 状态流转规则:pending→consulting/resolvedconsulting→resolvedresolvedarchived
// ValidConsultationTransition 状态流转规则:unassigned/assigned/accepted/needs_info/resolved/archived
func ValidConsultationTransition(from, to string) bool {
switch from {
case "pending":
return to == "consulting" || to == "resolved"
case "consulting":
return to == "resolved"
case "unassigned":
return to == "assigned"
case "assigned":
return to == "accepted" || to == "needs_info"
case "accepted":
return to == "needs_info" || to == "resolved"
case "needs_info":
return to == "accepted" || to == "resolved"
case "resolved":
return to == "archived"
return to == "archived" || to == "needs_info"
default:
return false
}
}
// ConsultationSLAState 返回会诊 SLA 状态;超时只标记,不自动伪造专家结论。
func ConsultationSLAState(t Consultation, now time.Time) string {
if t.SLADeadline == nil {
return "unscheduled"
}
if now.After(*t.SLADeadline) {
return "overdue"
}
return "on_time"
}
// ConsultationSnapshot 病例快照(会诊时打包,避免引用数据变化)
type ConsultationSnapshot struct {
RoomName string `json:"roomName,omitempty"`
LampTest *LampTest `json:"lampTest,omitempty"`
RoomName string `json:"roomName,omitempty"`
LampTest *LampTest `json:"lampTest,omitempty"`
Inspection *InspectionRecord `json:"inspection,omitempty"`
Batch *Batch `json:"batch,omitempty"`
WeatherAlerts []WeatherAlert `json:"weatherAlerts,omitempty"`
CreatedAt time.Time `json:"createdAt"`
Batch *Batch `json:"batch,omitempty"`
WeatherAlerts []WeatherAlert `json:"weatherAlerts,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
+38 -9
View File
@@ -8,9 +8,11 @@ import (
func TestValidConsultationTransition(t *testing.T) {
ok := [][2]string{
{"pending", "consulting"},
{"pending", "resolved"},
{"consulting", "resolved"},
{"unassigned", "assigned"},
{"assigned", "accepted"},
{"assigned", "needs_info"},
{"accepted", "resolved"},
{"needs_info", "resolved"},
{"resolved", "archived"},
}
for _, c := range ok {
@@ -19,7 +21,8 @@ func TestValidConsultationTransition(t *testing.T) {
}
}
bad := [][2]string{
{"pending", "archived"},
{"unassigned", "archived"},
{"unassigned", "resolved"},
{"resolved", "resolved"},
{"archived", "pending"},
{"", "resolved"},
@@ -31,15 +34,41 @@ func TestValidConsultationTransition(t *testing.T) {
}
}
func TestConsultationRejectsArchiveBeforeResolution(t *testing.T) {
if ValidConsultationTransition("accepted", "archived") {
t.Fatal("accepted 不能直接归档")
}
if ValidConsultationTransition("unassigned", "archived") {
t.Fatal("unassigned 不能直接归档")
}
}
func TestConsultationSLAMarksOverdue(t *testing.T) {
now := time.Now()
deadline := now.Add(-time.Hour)
task := Consultation{Status: "assigned", SLADeadline: &deadline}
if state := ConsultationSLAState(task, now); state != "overdue" {
t.Fatalf("SLA 状态 = %s, want overdue", state)
}
future := now.Add(time.Hour)
task.SLADeadline = &future
if state := ConsultationSLAState(task, now); state != "on_time" {
t.Fatalf("SLA 状态 = %s, want on_time", state)
}
if state := ConsultationSLAState(Consultation{}, now); state != "unscheduled" {
t.Fatalf("SLA 状态 = %s, want unscheduled", state)
}
}
func TestConsultationSnapshotJSON(t *testing.T) {
now := time.Now()
s := ConsultationSnapshot{
RoomName: "蚕房1#",
LampTest: &LampTest{ID: "lamp-1", Status: "resulted"},
Inspection: &InspectionRecord{ID: "insp-1", AIStatus: "done"},
Batch: &Batch{ID: "batch-1", Name: "批次A"},
RoomName: "蚕房1#",
LampTest: &LampTest{ID: "lamp-1", Status: "resulted"},
Inspection: &InspectionRecord{ID: "insp-1", AIStatus: "done"},
Batch: &Batch{ID: "batch-1", Name: "批次A"},
WeatherAlerts: []WeatherAlert{{Disease: "白僵病", Level: "orange"}},
CreatedAt: now,
CreatedAt: now,
}
raw, err := json.Marshal(s)
if err != nil {
+49
View File
@@ -0,0 +1,49 @@
package model
import (
"encoding/json"
"time"
)
// ConsultationOpinionVersion 专家意见版本,保留作者、时间、旧版本和修改原因。
type ConsultationOpinionVersion struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
ConsultationID string `gorm:"column:consultation_id;type:uuid;index" json:"consultationId"`
Version int `json:"version"`
AuthorID *string `gorm:"column:author_id;type:uuid" json:"authorId,omitempty"`
Opinion string `gorm:"type:text" json:"opinion"`
Plan string `gorm:"type:text" json:"plan"`
Reason *string `gorm:"type:text" json:"reason,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
}
func (ConsultationOpinionVersion) TableName() string { return "consultation_opinion_versions" }
// RuleEngineResult 规则执行结果持久化。
type RuleEngineResult struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
RuleVersion string `gorm:"column:rule_version;size:64" json:"ruleVersion"`
Disease string `gorm:"size:64" json:"disease"`
Level *string `gorm:"size:16" json:"level,omitempty"`
Reason *string `gorm:"type:text" json:"reason,omitempty"`
InputSnapshot json.RawMessage `gorm:"column:input_snapshot;type:jsonb" json:"inputSnapshot"`
Missing json.RawMessage `gorm:"type:jsonb" json:"missing"`
ComputedAt time.Time `gorm:"column:computed_at;type:timestamptz" json:"computedAt"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
}
func (RuleEngineResult) TableName() string { return "rule_engine_results" }
// KnowledgeReview 知识内容审核记录。
type KnowledgeReview struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
EntityType string `gorm:"column:entity_type;size:32;index" json:"entityType"`
EntityID string `gorm:"column:entity_id;size:128;index" json:"entityId"`
Status string `gorm:"size:16" json:"status"`
ReviewerID *string `gorm:"column:reviewer_id;type:uuid" json:"reviewerId,omitempty"`
ReviewedAt *time.Time `gorm:"column:reviewed_at;type:timestamptz" json:"reviewedAt,omitempty"`
Note *string `gorm:"type:text" json:"note,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
}
func (KnowledgeReview) TableName() string { return "knowledge_reviews" }
+53 -29
View File
@@ -4,41 +4,65 @@ import "time"
// Disease 蚕病百科条目(知识库-蚕病百科)
type Disease struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
Name string `gorm:"size:64" json:"name"`
Category string `gorm:"size:32;index" json:"category"` // viral/fungal/bacterial/protozoan/other
Pathogen string `gorm:"type:text" json:"pathogen"`
Transmission string `gorm:"type:text" json:"transmission"`
Medium string `gorm:"type:text" json:"medium"`
Incubation string `gorm:"type:text" json:"incubation"`
Symptoms string `gorm:"type:text" json:"symptoms"`
HighRiskStage string `gorm:"column:high_risk_stage;type:text" json:"highRiskStage"`
HighRiskCondition string `gorm:"column:high_risk_condition;type:text" json:"highRiskCondition"`
LethalTime string `gorm:"column:lethal_time;type:text" json:"lethalTime"`
SpreadTrend string `gorm:"column:spread_trend;type:text" json:"spreadTrend"`
Recurrence string `gorm:"type:text" json:"recurrence"`
Detection string `gorm:"type:text" json:"detection"`
Prevention string `gorm:"type:text" json:"prevention"`
ImageURL *string `gorm:"column:image_url;size:512" json:"imageUrl,omitempty"`
SortOrder int `gorm:"column:sort_order;type:int;default:0" json:"sortOrder"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
Name string `gorm:"size:64" json:"name"`
Category string `gorm:"size:32;index" json:"category"` // viral/fungal/bacterial/protozoan/other
Pathogen string `gorm:"type:text" json:"pathogen"`
Transmission string `gorm:"type:text" json:"transmission"`
Medium string `gorm:"type:text" json:"medium"`
Incubation string `gorm:"type:text" json:"incubation"`
Symptoms string `gorm:"type:text" json:"symptoms"`
HighRiskStage string `gorm:"column:high_risk_stage;type:text" json:"highRiskStage"`
HighRiskCondition string `gorm:"column:high_risk_condition;type:text" json:"highRiskCondition"`
LethalTime string `gorm:"column:lethal_time;type:text" json:"lethalTime"`
SpreadTrend string `gorm:"column:spread_trend;type:text" json:"spreadTrend"`
Recurrence string `gorm:"type:text" json:"recurrence"`
Detection string `gorm:"type:text" json:"detection"`
Prevention string `gorm:"type:text" json:"prevention"`
ImageURL *string `gorm:"column:image_url;size:512" json:"imageUrl,omitempty"`
Status string `gorm:"size:16;default:published;index" json:"status"`
Source *string `gorm:"type:text" json:"source,omitempty"`
SourceDate *time.Time `gorm:"column:source_date;type:date" json:"sourceDate,omitempty"`
ExpertConfirmedBy *string `gorm:"column:expert_confirmed_by;type:uuid" json:"expertConfirmedBy,omitempty"`
SortOrder int `gorm:"column:sort_order;type:int;default:0" json:"sortOrder"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (Disease) TableName() string { return "diseases" }
// KnowledgeArticle 知识文章(AI 结果解读、LAMP/SERS 教程、季节性防控提醒等)
type KnowledgeArticle struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
Kind string `gorm:"size:32;index" json:"kind"` // ai_guide/lamp_guide/sers_guide/seasonal_tip
Title string `gorm:"size:128" json:"title"`
Summary string `gorm:"type:text" json:"summary"`
Content string `gorm:"type:text" json:"content"`
SortOrder int `gorm:"column:sort_order;type:int;default:0" json:"sortOrder"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
Kind string `gorm:"size:32;index" json:"kind"` // ai_guide/lamp_guide/sers_guide/seasonal_tip
Title string `gorm:"size:128" json:"title"`
Summary string `gorm:"type:text" json:"summary"`
Content string `gorm:"type:text" json:"content"`
Status string `gorm:"size:16;default:published;index" json:"status"`
Source *string `gorm:"type:text" json:"source,omitempty"`
SourceDate *time.Time `gorm:"column:source_date;type:date" json:"sourceDate,omitempty"`
ExpertConfirmedBy *string `gorm:"column:expert_confirmed_by;type:uuid" json:"expertConfirmedBy,omitempty"`
SortOrder int `gorm:"column:sort_order;type:int;default:0" json:"sortOrder"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (KnowledgeArticle) TableName() string { return "knowledge_articles" }
// ValidKnowledgeTransition 知识审核状态机:draft/review/published/withdrawn。
func ValidKnowledgeTransition(from, to string) bool {
switch from {
case "draft":
return to == "review" || to == "withdrawn"
case "review":
return to == "published" || to == "withdrawn" || to == "draft"
case "published":
return to == "withdrawn"
case "withdrawn":
return to == "review" || to == "draft"
default:
return false
}
}
@@ -0,0 +1,23 @@
package model
import "testing"
func TestValidKnowledgeTransition(t *testing.T) {
ok := [][2]string{
{"draft", "review"},
{"review", "published"},
{"published", "withdrawn"},
{"withdrawn", "review"},
}
for _, tr := range ok {
if !ValidKnowledgeTransition(tr[0], tr[1]) {
t.Errorf("expected %s -> %s", tr[0], tr[1])
}
}
if ValidKnowledgeTransition("draft", "published") {
t.Error("draft 不能直接 published")
}
if ValidKnowledgeTransition("published", "draft") {
t.Error("published 不能回草稿")
}
}