feat: 完善环境规则、会诊治理、知识审核与效果评估
This commit is contained in:
@@ -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/resolved;consulting→resolved;resolved→archived
|
||||
// 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"`
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" }
|
||||
@@ -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 不能回草稿")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user