chore: 同步本地 v9 整改与运营能力

This commit is contained in:
weijuesen
2026-08-17 21:43:26 +08:00
parent 9a426039b7
commit d205d4845b
58 changed files with 4042 additions and 115 deletions
@@ -0,0 +1,216 @@
package model
import (
"encoding/json"
"errors"
"strings"
"time"
)
// Organization 组织/养殖场/合作社,用于对象级数据授权。
type Organization struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
Name string `gorm:"size:128" json:"name"`
Code string `gorm:"size:64;uniqueIndex" json:"code"`
Description *string `gorm:"type:text" json:"description,omitempty"`
Status string `gorm:"size:16;default:active" json:"status"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (Organization) TableName() string { return "organizations" }
// OrganizationMember 用户与组织归属关系。
type OrganizationMember struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
OrganizationID string `gorm:"column:organization_id;type:uuid;uniqueIndex:idx_org_members_org_user,priority:1" json:"organizationId"`
UserID string `gorm:"column:user_id;type:uuid;uniqueIndex:idx_org_members_org_user,priority:2" json:"userId"`
Role string `gorm:"size:16;default:member" json:"role"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
}
func (OrganizationMember) TableName() string { return "organization_members" }
// DeviceMaintenanceRecord 设备校准/故障/维护/固件记录。
type DeviceMaintenanceRecord struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
DeviceID string `gorm:"column:device_id;type:uuid;index" json:"deviceId"`
Kind string `gorm:"size:16;index" json:"kind"` // calibration/fault/maintenance/firmware
Title string `gorm:"size:128" json:"title"`
ScheduledAt *time.Time `gorm:"column:scheduled_at;type:timestamptz" json:"scheduledAt,omitempty"`
PerformedAt *time.Time `gorm:"column:performed_at;type:timestamptz" json:"performedAt,omitempty"`
PerformerID *string `gorm:"column:performer_id;type:uuid" json:"performerId,omitempty"`
Result *string `gorm:"type:text" json:"result,omitempty"`
FirmwareFrom *string `gorm:"column:firmware_from;size:64" json:"firmwareFrom,omitempty"`
FirmwareTo *string `gorm:"column:firmware_to;size:64" json:"firmwareTo,omitempty"`
CostAmount *float64 `gorm:"column:cost_amount;type:float" json:"costAmount,omitempty"`
CostUnit *string `gorm:"column:cost_unit;size:16" json:"costUnit,omitempty"`
Note *string `gorm:"type:text" json:"note,omitempty"`
CreatedBy *string `gorm:"column:created_by;type:uuid" json:"createdBy,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (DeviceMaintenanceRecord) TableName() string { return "device_maintenance_records" }
// ValidMaintenanceKind 设备维护记录类型。
func ValidMaintenanceKind(kind string) bool {
switch kind {
case "calibration", "fault", "maintenance", "firmware":
return true
default:
return false
}
}
// ValidateDeviceMaintenanceRecord 设备维护必填项。
func ValidateDeviceMaintenanceRecord(record DeviceMaintenanceRecord) error {
if !ValidMaintenanceKind(record.Kind) {
return errors.New("kind 仅支持 calibration/fault/maintenance/firmware")
}
if strings.TrimSpace(record.Title) == "" {
return errors.New("维护标题不能为空")
}
return nil
}
// ProductionLossRecord 产量、死亡、淘汰、损失和防控成本记录。
type ProductionLossRecord 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"`
RecordDate time.Time `gorm:"column:record_date;type:timestamptz;index" json:"recordDate"`
DeathCount *int `gorm:"column:death_count;type:int" json:"deathCount,omitempty"`
CulledCount *int `gorm:"column:culled_count;type:int" json:"culledCount,omitempty"`
YieldKg *float64 `gorm:"column:yield_kg;type:float" json:"yieldKg,omitempty"`
LossKg *float64 `gorm:"column:loss_kg;type:float" json:"lossKg,omitempty"`
CostType *string `gorm:"column:cost_type;size:32" json:"costType,omitempty"` // medicine/disinfection/detection/labor/other
CostAmount *float64 `gorm:"column:cost_amount;type:float" json:"costAmount,omitempty"`
Note *string `gorm:"type:text" json:"note,omitempty"`
CreatedBy *string `gorm:"column:created_by;type:uuid" json:"createdBy,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (ProductionLossRecord) TableName() string { return "production_loss_records" }
// ValidateProductionLossRecord 产量损失记录必须至少有一个业务数值。
func ValidateProductionLossRecord(record ProductionLossRecord) error {
if record.RoomID == nil && record.BatchID == nil {
return errors.New("roomId/batchId 至少填一个")
}
if record.RecordDate.IsZero() {
return errors.New("记录日期不能为空")
}
hasValue := record.DeathCount != nil || record.CulledCount != nil ||
record.YieldKg != nil || record.LossKg != nil || record.CostAmount != nil
if !hasValue {
return errors.New("死亡/淘汰/产量/损失/成本至少填写一项")
}
return nil
}
// CaseStudy 已脱敏病例沉淀,必须经审核后发布。
type CaseStudy struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
Title string `gorm:"size:128" json:"title"`
Disease string `gorm:"size:64;index" json:"disease"`
SourceConsultationID *string `gorm:"column:source_consultation_id;type:uuid;index" json:"sourceConsultationId,omitempty"`
SourceDiseaseEventID *string `gorm:"column:source_disease_event_id;type:uuid;index" json:"sourceDiseaseEventId,omitempty"`
SourceRoomID *string `gorm:"column:source_room_id;type:uuid;index" json:"sourceRoomId,omitempty"`
Region *string `gorm:"size:64" json:"region,omitempty"`
CaseDate *time.Time `gorm:"column:case_date;type:timestamptz" json:"caseDate,omitempty"`
Summary *string `gorm:"type:text" json:"summary,omitempty"`
DesensitizedPayload json.RawMessage `gorm:"column:desensitized_payload;type:jsonb" json:"desensitizedPayload,omitempty"`
Status string `gorm:"size:16;default:draft;index" json:"status"` // draft/pending_review/published/rejected
ReviewNote *string `gorm:"column:review_note;type:text" json:"reviewNote,omitempty"`
ReviewerID *string `gorm:"column:reviewer_id;type:uuid" json:"reviewerId,omitempty"`
ReviewedAt *time.Time `gorm:"column:reviewed_at;type:timestamptz" json:"reviewedAt,omitempty"`
PublishedAt *time.Time `gorm:"column:published_at;type:timestamptz" json:"publishedAt,omitempty"`
CreatedBy *string `gorm:"column:created_by;type:uuid" json:"createdBy,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (CaseStudy) TableName() string { return "case_studies" }
// ValidCaseStudyTransition 案例审核状态流转。
func ValidCaseStudyTransition(from, to string) bool {
switch from {
case "draft":
return to == "pending_review" || to == "rejected"
case "pending_review":
return to == "published" || to == "rejected"
case "published":
return to == "rejected" || to == "pending_review"
case "rejected":
return to == "draft" || to == "pending_review"
default:
return false
}
}
// LaboratoryResult 实验室结构化结果,关联三级溯源、发病事件和样本。
type LaboratoryResult struct {
ID string `gorm:"type:uuid;primaryKey;default:gen_random_uuid()" json:"id"`
TraceRecordID *string `gorm:"column:trace_record_id;type:uuid;index" json:"traceRecordId,omitempty"`
DiseaseEventID *string `gorm:"column:disease_event_id;type:uuid;index" json:"diseaseEventId,omitempty"`
SampleID *string `gorm:"column:sample_id;type:uuid;index" json:"sampleId,omitempty"`
RoomID *string `gorm:"column:room_id;type:uuid;index" json:"roomId,omitempty"`
BatchID *string `gorm:"column:batch_id;type:uuid;index" json:"batchId,omitempty"`
LabName string `gorm:"column:lab_name;size:128" json:"labName"`
ReportNo string `gorm:"column:report_no;size:64" json:"reportNo"`
TestType string `gorm:"column:test_type;size:32" json:"testType"` // molecular_typing/pathogen/environment_sample/other
ResultType string `gorm:"column:result_type;size:16" json:"resultType"`
Pathogen *string `gorm:"size:128" json:"pathogen,omitempty"`
Genotype *string `gorm:"size:128" json:"genotype,omitempty"`
Method *string `gorm:"size:128" json:"method,omitempty"`
SampleNo *string `gorm:"column:sample_no;size:64" json:"sampleNo,omitempty"`
SampleType *string `gorm:"column:sample_type;size:32" json:"sampleType,omitempty"`
Findings *string `gorm:"type:text" json:"findings,omitempty"`
ReportURL *string `gorm:"column:report_url;size:512" json:"reportUrl,omitempty"`
TestedAt *time.Time `gorm:"column:tested_at;type:timestamptz" json:"testedAt,omitempty"`
ConcludedAt *time.Time `gorm:"column:concluded_at;type:timestamptz" json:"concludedAt,omitempty"`
CreatedBy *string `gorm:"column:created_by;type:uuid" json:"createdBy,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (LaboratoryResult) TableName() string { return "laboratory_results" }
// ValidateLaboratoryResult 实验室结果必填项。
func ValidateLaboratoryResult(result LaboratoryResult) error {
if strings.TrimSpace(result.LabName) == "" {
return errors.New("实验室名称不能为空")
}
if strings.TrimSpace(result.ReportNo) == "" {
return errors.New("报告编号不能为空")
}
if !ValidLaboratoryTestType(result.TestType) {
return errors.New("testType 仅支持 molecular_typing/pathogen/environment_sample/other")
}
if !ValidLaboratoryResultType(result.ResultType) {
return errors.New("resultType 仅支持 positive/negative/indeterminate/invalid")
}
return nil
}
// ValidLaboratoryTestType 实验室检测类型。
func ValidLaboratoryTestType(testType string) bool {
switch testType {
case "molecular_typing", "pathogen", "environment_sample", "other":
return true
default:
return false
}
}
// ValidLaboratoryResultType 实验室结果类型。
func ValidLaboratoryResultType(resultType string) bool {
switch resultType {
case "positive", "negative", "indeterminate", "invalid":
return true
default:
return false
}
}
@@ -0,0 +1,77 @@
package model
import (
"testing"
"time"
)
func TestValidMaintenanceKind(t *testing.T) {
for _, kind := range []string{"calibration", "fault", "maintenance", "firmware"} {
if !ValidMaintenanceKind(kind) {
t.Errorf("%s should be valid", kind)
}
}
if ValidMaintenanceKind("inspection") {
t.Error("inspection is not a maintenance kind")
}
}
func TestValidateDeviceMaintenanceRecord(t *testing.T) {
record := DeviceMaintenanceRecord{Kind: "calibration", Title: "温度传感器校准"}
if err := ValidateDeviceMaintenanceRecord(record); err != nil {
t.Fatalf("valid record should pass: %v", err)
}
record.Title = ""
if err := ValidateDeviceMaintenanceRecord(record); err == nil {
t.Fatal("missing title should fail")
}
record.Title = "校准"
record.Kind = "unknown"
if err := ValidateDeviceMaintenanceRecord(record); err == nil {
t.Fatal("invalid kind should fail")
}
}
func TestValidateProductionLossRecord(t *testing.T) {
roomID := "room-1"
yield := 3.2
record := ProductionLossRecord{RoomID: &roomID, RecordDate: time.Now(), YieldKg: &yield}
if err := ValidateProductionLossRecord(record); err != nil {
t.Fatalf("valid record should pass: %v", err)
}
record.YieldKg = nil
if err := ValidateProductionLossRecord(record); err == nil {
t.Fatal("record without business value should fail")
}
}
func TestValidCaseStudyTransition(t *testing.T) {
ok := [][2]string{
{"draft", "pending_review"},
{"pending_review", "published"},
{"pending_review", "rejected"},
{"published", "rejected"},
}
for _, tr := range ok {
if !ValidCaseStudyTransition(tr[0], tr[1]) {
t.Errorf("expected %s -> %s", tr[0], tr[1])
}
}
if ValidCaseStudyTransition("draft", "published") {
t.Error("draft cannot jump to published")
}
}
func TestValidateLaboratoryResult(t *testing.T) {
result := LaboratoryResult{
LabName: "省蚕科所", ReportNo: "LAB-001",
TestType: "molecular_typing", ResultType: "positive",
}
if err := ValidateLaboratoryResult(result); err != nil {
t.Fatalf("valid result should pass: %v", err)
}
result.ResultType = "unknown"
if err := ValidateLaboratoryResult(result); err == nil {
t.Fatal("invalid result type should fail")
}
}
+32 -31
View File
@@ -32,6 +32,7 @@ type Room struct {
Capacity *int `gorm:"type:int" json:"capacity,omitempty"`
Stage *string `gorm:"size:32" json:"stage,omitempty"`
Region *string `gorm:"size:64" json:"region,omitempty"`
OrgID *string `gorm:"column:org_id;type:uuid;index" json:"orgId,omitempty"`
Status string `gorm:"default:active" json:"status"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
@@ -109,38 +110,38 @@ func (Alarm) TableName() string { return "alarms" }
// Camera 摄像头表
type Camera struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
RoomID *string `gorm:"column:room_id;index" json:"roomId,omitempty"`
Code string `gorm:"size:64" json:"code"`
Name string `gorm:"size:128" json:"name"`
RtspURL *string `gorm:"column:rtsp_url;size:512" json:"rtspUrl,omitempty"`
HTTPURL *string `gorm:"column:http_url;size:512" json:"httpUrl,omitempty"`
Username *string `gorm:"size:64" json:"username,omitempty"`
PasswordEnc *string `gorm:"column:password_enc;size:255" json:"-"`
Position *string `gorm:"size:255" json:"position,omitempty"`
Resolution *string `gorm:"size:32" json:"resolution,omitempty"`
FPS *int `gorm:"type:int" json:"fps,omitempty"`
IsOnline bool `gorm:"column:is_online;default:true" json:"isOnline"`
GbDeviceID *string `gorm:"column:gb_device_id;size:20" json:"gbDeviceId,omitempty"`
GbChannelID *string `gorm:"column:gb_channel_id;size:20" json:"gbChannelId,omitempty"`
GbAuthID *string `gorm:"column:gb_auth_id;size:20" json:"gbAuthId,omitempty"`
GbAuthPassword *string `gorm:"column:gb_auth_password;size:255" json:"-"`
GbStreamType *string `gorm:"column:gb_stream_type;size:10" json:"gbStreamType,omitempty"`
GbTransport *string `gorm:"column:gb_transport;size:10" json:"gbTransport,omitempty"`
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
RoomID *string `gorm:"column:room_id;index" json:"roomId,omitempty"`
Code string `gorm:"size:64" json:"code"`
Name string `gorm:"size:128" json:"name"`
RtspURL *string `gorm:"column:rtsp_url;size:512" json:"rtspUrl,omitempty"`
HTTPURL *string `gorm:"column:http_url;size:512" json:"httpUrl,omitempty"`
Username *string `gorm:"size:64" json:"username,omitempty"`
PasswordEnc *string `gorm:"column:password_enc;size:255" json:"-"`
Position *string `gorm:"size:255" json:"position,omitempty"`
Resolution *string `gorm:"size:32" json:"resolution,omitempty"`
FPS *int `gorm:"type:int" json:"fps,omitempty"`
IsOnline bool `gorm:"column:is_online;default:true" json:"isOnline"`
GbDeviceID *string `gorm:"column:gb_device_id;size:20" json:"gbDeviceId,omitempty"`
GbChannelID *string `gorm:"column:gb_channel_id;size:20" json:"gbChannelId,omitempty"`
GbAuthID *string `gorm:"column:gb_auth_id;size:20" json:"gbAuthId,omitempty"`
GbAuthPassword *string `gorm:"column:gb_auth_password;size:255" json:"-"`
GbStreamType *string `gorm:"column:gb_stream_type;size:10" json:"gbStreamType,omitempty"`
GbTransport *string `gorm:"column:gb_transport;size:10" json:"gbTransport,omitempty"`
GbAlarmChannelID *string `gorm:"column:gb_alarm_channel_id;size:20" json:"gbAlarmChannelId,omitempty"`
GbVoiceChannelID *string `gorm:"column:gb_voice_channel_id;size:20" json:"gbVoiceChannelId,omitempty"`
GbManufacturer *string `gorm:"column:gb_manufacturer;size:64" json:"gbManufacturer,omitempty"`
ManufacturerID *string `gorm:"column:manufacturer_id;size:64" json:"manufacturerId,omitempty"`
GbManufacturer *string `gorm:"column:gb_manufacturer;size:64" json:"gbManufacturer,omitempty"`
ManufacturerID *string `gorm:"column:manufacturer_id;size:64" json:"manufacturerId,omitempty"`
// 以下字段不在数据库中,运行时通过 WVP API 填充
StreamURL *string `gorm:"-" json:"streamUrl,omitempty"`
HlsURL *string `gorm:"-" json:"hlsUrl,omitempty"`
FlvURL *string `gorm:"-" json:"flvUrl,omitempty"`
WebrtcURL *string `gorm:"-" json:"webrtcUrl,omitempty"`
SnapshotURL *string `gorm:"-" json:"snapshotUrl,omitempty"`
Online bool `gorm:"-" json:"online"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
StreamURL *string `gorm:"-" json:"streamUrl,omitempty"`
HlsURL *string `gorm:"-" json:"hlsUrl,omitempty"`
FlvURL *string `gorm:"-" json:"flvUrl,omitempty"`
WebrtcURL *string `gorm:"-" json:"webrtcUrl,omitempty"`
SnapshotURL *string `gorm:"-" json:"snapshotUrl,omitempty"`
Online bool `gorm:"-" json:"online"`
Enabled bool `gorm:"default:true" json:"enabled"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
}
func (Camera) TableName() string { return "cameras" }
@@ -167,8 +168,8 @@ type VideoClip struct {
AlarmID *string `gorm:"column:alarm_id;index" json:"alarmId,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`
S3Bucket *string `gorm:"column:s3_bucket" json:"s3Bucket,omitempty"`
S3Key *string `gorm:"column:s3_key" json:"s3Key,omitempty"`
S3Bucket *string `gorm:"column:s3_bucket" json:"s3Bucket,omitempty"`
S3Key *string `gorm:"column:s3_key" json:"s3Key,omitempty"`
// 以下字段不在数据库中,运行时填充
PlaybackURL *string `gorm:"-" json:"playbackUrl,omitempty"`
Format string `gorm:"-" json:"format"`
@@ -47,6 +47,14 @@ var AllPermissions = []PermissionDef{
{"biosecurity:write", "生物安全管理", "维护种源、消毒记录并签发二维码"},
{"user:manage", "用户管理", "管理用户、角色和权限"},
{"audit:read", "审计查看", "查看审计日志"},
{"organization:manage", "组织管理", "维护组织及成员数据范围"},
{"device:write", "设备管理", "维护设备资产、校准和固件信息"},
{"farm:read", "产量损失查看", "查看产量、死亡、淘汰和成本记录"},
{"farm:write", "产量损失管理", "新增、编辑产量和损失记录"},
{"case:read", "案例查看", "查看已脱敏案例库"},
{"case:write", "案例管理", "创建、审核和发布脱敏案例"},
{"lab:read", "实验室结果查看", "查看实验室结构化结果"},
{"lab:write", "实验室结果管理", "录入实验室结果和分子分型"},
}
// RolePermissionMap 角色-权限码映射(种子数据)
@@ -66,6 +74,8 @@ var RolePermissionMap = map[string][]string{
"trace:read", "trace:write",
"biosecurity:read", "biosecurity:write",
"user:manage", "audit:read",
"organization:manage", "device:write",
"farm:read", "farm:write", "case:read", "case:write", "lab:read", "lab:write",
},
RoleOperator: {
"dashboard:view", "room:read", "room:write", "device:read", "device:control",
@@ -81,6 +91,7 @@ var RolePermissionMap = map[string][]string{
"consultation:read", "consultation:write",
"trace:read", "trace:write",
"biosecurity:read", "biosecurity:write",
"device:write", "farm:read", "farm:write", "case:read", "case:write", "lab:read", "lab:write",
},
RoleViewer: {
"dashboard:view", "room:read", "device:read",
@@ -95,6 +106,7 @@ var RolePermissionMap = map[string][]string{
"consultation:read",
"trace:read",
"biosecurity:read",
"farm:read", "case:read", "lab:read",
},
RoleFarmer: {
"dashboard:view", "room:read", "device:read", "device:control",
@@ -109,5 +121,6 @@ var RolePermissionMap = map[string][]string{
"consultation:read",
"trace:read",
"biosecurity:read", "biosecurity:write",
"farm:read", "farm:write", "case:read", "lab:read",
},
}
+2
View File
@@ -45,6 +45,8 @@ type RearingRecord struct {
Instar *int `gorm:"type:int" json:"instar,omitempty"`
MulberrySource *string `gorm:"column:mulberry_source;size:128" json:"mulberrySource,omitempty"` // 桑叶来源
Density *string `gorm:"size:64" json:"density,omitempty"` // 饲养密度
DeathCount *int `gorm:"column:death_count;type:int" json:"deathCount,omitempty"`
CulledCount *int `gorm:"column:culled_count;type:int" json:"culledCount,omitempty"`
Note *string `gorm:"type:text" json:"note,omitempty"`
CreatedAt time.Time `gorm:"type:timestamptz" json:"createdAt"`
UpdatedAt time.Time `gorm:"type:timestamptz" json:"updatedAt"`