feat(server-go): 专家会诊(病例快照/状态流转/意见与方案,#18)
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
func (Consultation) TableName() string { return "consultations" }
|
||||
|
||||
// ValidConsultationTransition 状态流转规则:pending→consulting/resolved;consulting→resolved;resolved→archived
|
||||
func ValidConsultationTransition(from, to string) bool {
|
||||
switch from {
|
||||
case "pending":
|
||||
return to == "consulting" || to == "resolved"
|
||||
case "consulting":
|
||||
return to == "resolved"
|
||||
case "resolved":
|
||||
return to == "archived"
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ConsultationSnapshot 病例快照(会诊时打包,避免引用数据变化)
|
||||
type ConsultationSnapshot struct {
|
||||
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"`
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestValidConsultationTransition(t *testing.T) {
|
||||
ok := [][2]string{
|
||||
{"pending", "consulting"},
|
||||
{"pending", "resolved"},
|
||||
{"consulting", "resolved"},
|
||||
{"resolved", "archived"},
|
||||
}
|
||||
for _, c := range ok {
|
||||
if !ValidConsultationTransition(c[0], c[1]) {
|
||||
t.Errorf("%s → %s 应合法", c[0], c[1])
|
||||
}
|
||||
}
|
||||
bad := [][2]string{
|
||||
{"pending", "archived"},
|
||||
{"resolved", "resolved"},
|
||||
{"archived", "pending"},
|
||||
{"", "resolved"},
|
||||
}
|
||||
for _, c := range bad {
|
||||
if ValidConsultationTransition(c[0], c[1]) {
|
||||
t.Errorf("%s → %s 不应合法", c[0], c[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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"},
|
||||
WeatherAlerts: []WeatherAlert{{Disease: "白僵病", Level: "orange"}},
|
||||
CreatedAt: now,
|
||||
}
|
||||
raw, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
t.Fatalf("快照序列化失败: %v", err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatalf("快照反序列化失败: %v", err)
|
||||
}
|
||||
for _, key := range []string{"roomName", "lampTest", "inspection", "batch", "weatherAlerts", "createdAt"} {
|
||||
if _, ok := m[key]; !ok {
|
||||
t.Errorf("快照缺少字段 %s", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@ var AllPermissions = []PermissionDef{
|
||||
{"lamp:write", "LAMP 检测管理", "新建、编辑、录入 LAMP 检测结果"},
|
||||
{"consumable:read", "耗材查看", "查看耗材库存与预警"},
|
||||
{"consumable:write", "耗材管理", "新增、编辑、删除耗材"},
|
||||
{"consultation:read", "会诊查看", "查看专家会诊单与病例快照"},
|
||||
{"consultation:write", "会诊管理", "发起会诊、出具意见与防控方案"},
|
||||
{"user:manage", "用户管理", "管理用户、角色和权限"},
|
||||
{"audit:read", "审计查看", "查看审计日志"},
|
||||
}
|
||||
@@ -56,6 +58,7 @@ var RolePermissionMap = map[string][]string{
|
||||
"weather:read",
|
||||
"lamp:read", "lamp:write",
|
||||
"consumable:read", "consumable:write",
|
||||
"consultation:read", "consultation:write",
|
||||
"user:manage", "audit:read",
|
||||
},
|
||||
RoleOperator: {
|
||||
@@ -69,6 +72,7 @@ var RolePermissionMap = map[string][]string{
|
||||
"weather:read",
|
||||
"lamp:read", "lamp:write",
|
||||
"consumable:read", "consumable:write",
|
||||
"consultation:read", "consultation:write",
|
||||
},
|
||||
RoleViewer: {
|
||||
"dashboard:view", "room:read", "device:read",
|
||||
@@ -80,6 +84,7 @@ var RolePermissionMap = map[string][]string{
|
||||
"weather:read",
|
||||
"lamp:read",
|
||||
"consumable:read",
|
||||
"consultation:read",
|
||||
},
|
||||
RoleFarmer: {
|
||||
"dashboard:view", "room:read", "device:read", "device:control",
|
||||
@@ -91,5 +96,6 @@ var RolePermissionMap = map[string][]string{
|
||||
"weather:read",
|
||||
"lamp:read",
|
||||
"consumable:read",
|
||||
"consultation:read",
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user