chore: 同步本地 v9 整改与运营能力
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"silk-server-go/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const defaultOrgCode = "default"
|
||||
|
||||
// currentUserRole 从 JWT context 中取当前角色。
|
||||
func currentUserRole(c *gin.Context) string {
|
||||
user, ok := c.Get("user")
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
m, ok := user.(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
role, _ := m["role"].(string)
|
||||
return role
|
||||
}
|
||||
|
||||
// hasGlobalAccess admin 拥有跨组织全部对象访问权。
|
||||
func hasGlobalAccess(c *gin.Context) bool {
|
||||
return currentUserRole(c) == model.RoleAdmin
|
||||
}
|
||||
|
||||
// applyOrgScope 对组织归属列(如 rooms.org_id)追加当前用户可见组织过滤。
|
||||
func applyOrgScope(q *gorm.DB, c *gin.Context, column string) *gorm.DB {
|
||||
if hasGlobalAccess(c) {
|
||||
return q
|
||||
}
|
||||
userID := currentUserID(c)
|
||||
if userID == nil {
|
||||
return q.Where("1 = 0")
|
||||
}
|
||||
return q.Where("CAST("+column+" AS text) IN (SELECT organization_id::text FROM organization_members WHERE user_id = ?)", *userID)
|
||||
}
|
||||
|
||||
// applyRoomScope 对业务记录的房间列追加当前用户可见房间过滤。
|
||||
func applyRoomScope(q *gorm.DB, c *gin.Context, column string) *gorm.DB {
|
||||
if hasGlobalAccess(c) {
|
||||
return q
|
||||
}
|
||||
userID := currentUserID(c)
|
||||
if userID == nil {
|
||||
return q.Where("1 = 0")
|
||||
}
|
||||
return q.Where("CAST("+column+" AS text) IN ("+
|
||||
"SELECT r.id::text FROM rooms r "+
|
||||
"JOIN organization_members om ON om.organization_id = r.org_id "+
|
||||
"WHERE om.user_id = ?)", *userID)
|
||||
}
|
||||
|
||||
// canAccessOrganization 判断当前用户是否属于指定组织。
|
||||
func canAccessOrganization(db *gorm.DB, c *gin.Context, orgID string) bool {
|
||||
if hasGlobalAccess(c) {
|
||||
return true
|
||||
}
|
||||
userID := currentUserID(c)
|
||||
if userID == nil {
|
||||
return false
|
||||
}
|
||||
var count int64
|
||||
db.Model(&model.OrganizationMember{}).
|
||||
Where("organization_id = ? AND user_id = ?", orgID, *userID).
|
||||
Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// canAccessRoom 判断当前用户是否能访问指定房间。
|
||||
func canAccessRoom(db *gorm.DB, c *gin.Context, roomID *string) bool {
|
||||
if roomID == nil || *roomID == "" {
|
||||
return false
|
||||
}
|
||||
if hasGlobalAccess(c) {
|
||||
return true
|
||||
}
|
||||
userID := currentUserID(c)
|
||||
if userID == nil {
|
||||
return false
|
||||
}
|
||||
var count int64
|
||||
db.Model(&model.Room{}).
|
||||
Joins("JOIN organization_members om ON om.organization_id = rooms.org_id").
|
||||
Where("rooms.id = ? AND om.user_id = ?", *roomID, *userID).
|
||||
Count(&count)
|
||||
return count > 0
|
||||
}
|
||||
|
||||
// canAccessDeviceKey 判断当前用户是否能访问指定 deviceKey 对应的设备。
|
||||
func canAccessDeviceKey(db *gorm.DB, c *gin.Context, deviceKey string) bool {
|
||||
if hasGlobalAccess(c) {
|
||||
return true
|
||||
}
|
||||
if deviceKey == "" {
|
||||
return false
|
||||
}
|
||||
if currentUserID(c) == nil {
|
||||
return false
|
||||
}
|
||||
var device model.Device
|
||||
if db.Where("device_key = ?", deviceKey).First(&device).Error != nil {
|
||||
return false
|
||||
}
|
||||
return canAccessRoom(db, c, &device.RoomID)
|
||||
}
|
||||
|
||||
// requireDeviceKeyAccess 统一校验 deviceKey 对象授权;无权返回 403。
|
||||
func requireDeviceKeyAccess(c *gin.Context, db *gorm.DB, deviceKey string) bool {
|
||||
if canAccessDeviceKey(db, c, deviceKey) {
|
||||
return true
|
||||
}
|
||||
if deviceKey == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "deviceKey 不能为空"})
|
||||
return false
|
||||
}
|
||||
var count int64
|
||||
db.Model(&model.Device{}).Where("device_key = ?", deviceKey).Count(&count)
|
||||
if count == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "device not found"})
|
||||
return false
|
||||
}
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该设备"})
|
||||
return false
|
||||
}
|
||||
|
||||
// requireObjectAccess 统一对象授权检查;无权返回 403。
|
||||
func requireObjectAccess(c *gin.Context, db *gorm.DB, entityType, id string) bool {
|
||||
if hasGlobalAccess(c) {
|
||||
return true
|
||||
}
|
||||
roomID, ok := objectRoomID(db, entityType, id)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": entityType + " not found"})
|
||||
return false
|
||||
}
|
||||
if !canAccessRoom(db, c, roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该对象"})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// objectRoomID 返回实体归属房间;实体不存在时返回 false。
|
||||
func objectRoomID(db *gorm.DB, entityType, id string) (*string, bool) {
|
||||
switch entityType {
|
||||
case "room":
|
||||
var room model.Room
|
||||
if db.Where("id = ?", id).First(&room).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &room.ID, true
|
||||
case "device":
|
||||
var device model.Device
|
||||
if db.Where("id = ?", id).First(&device).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &device.RoomID, true
|
||||
case "sensor":
|
||||
var sensor model.Sensor
|
||||
if db.Where("id = ?", id).First(&sensor).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
var device model.Device
|
||||
if db.Where("id = ?", sensor.DeviceID).First(&device).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &device.RoomID, true
|
||||
case "threshold":
|
||||
var threshold model.Threshold
|
||||
if db.Where("id = ?", id).First(&threshold).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
var sensor model.Sensor
|
||||
if db.Where("id = ?", threshold.SensorID).First(&sensor).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
var device model.Device
|
||||
if db.Where("id = ?", sensor.DeviceID).First(&device).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &device.RoomID, true
|
||||
case "alarm":
|
||||
var alarm model.Alarm
|
||||
if db.Where("id = ?", id).First(&alarm).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
if alarm.DeviceKey == nil || *alarm.DeviceKey == "" {
|
||||
return nil, false
|
||||
}
|
||||
var device model.Device
|
||||
if db.Where("device_key = ?", *alarm.DeviceKey).First(&device).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &device.RoomID, true
|
||||
case "device_maintenance_record":
|
||||
var rec model.DeviceMaintenanceRecord
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
var device model.Device
|
||||
if db.Where("id = ?", rec.DeviceID).First(&device).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &device.RoomID, true
|
||||
case "camera":
|
||||
var camera model.Camera
|
||||
if db.Where("id = ?", id).First(&camera).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return camera.RoomID, true
|
||||
case "video_clip":
|
||||
var clip model.VideoClip
|
||||
if db.Where("id = ?", id).First(&clip).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return clip.RoomID, true
|
||||
case "tray":
|
||||
var tray model.Tray
|
||||
if db.Where("id = ?", id).First(&tray).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &tray.RoomID, true
|
||||
case "batch":
|
||||
var batch model.Batch
|
||||
if db.Where("id = ?", id).First(&batch).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &batch.RoomID, true
|
||||
case "rearing_record":
|
||||
var rec model.RearingRecord
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return batchRoomID(db, &rec.BatchID)
|
||||
case "inspection":
|
||||
var rec model.InspectionRecord
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return rec.RoomID, true
|
||||
case "lamp_test":
|
||||
var rec model.LampTest
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return rec.RoomID, true
|
||||
case "detection_task":
|
||||
var rec model.DetectionTask
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return rec.RoomID, true
|
||||
case "consultation":
|
||||
var rec model.Consultation
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return rec.RoomID, true
|
||||
case "trace_record":
|
||||
var rec model.TraceRecord
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return rec.RoomID, true
|
||||
case "disease_event":
|
||||
var rec model.DiseaseEvent
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return rec.RoomID, true
|
||||
case "sample":
|
||||
var rec model.Sample
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return rec.RoomID, true
|
||||
case "seed_source":
|
||||
var rec model.SeedSource
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return batchRoomID(db, rec.BatchID)
|
||||
case "disinfection_record":
|
||||
var rec model.DisinfectionRecord
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
if rec.RoomID != nil {
|
||||
return rec.RoomID, true
|
||||
}
|
||||
return batchRoomID(db, rec.BatchID)
|
||||
case "production_loss_record":
|
||||
var rec model.ProductionLossRecord
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
if rec.RoomID != nil {
|
||||
return rec.RoomID, true
|
||||
}
|
||||
return batchRoomID(db, rec.BatchID)
|
||||
case "laboratory_result":
|
||||
var rec model.LaboratoryResult
|
||||
if db.Where("id = ?", id).First(&rec).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
if rec.RoomID != nil {
|
||||
return rec.RoomID, true
|
||||
}
|
||||
if rec.BatchID != nil {
|
||||
return batchRoomID(db, rec.BatchID)
|
||||
}
|
||||
if rec.TraceRecordID != nil {
|
||||
var trace model.TraceRecord
|
||||
if db.Where("id = ?", *rec.TraceRecordID).First(&trace).Error == nil {
|
||||
return trace.RoomID, true
|
||||
}
|
||||
}
|
||||
return nil, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func batchRoomID(db *gorm.DB, batchID *string) (*string, bool) {
|
||||
if batchID == nil {
|
||||
return nil, false
|
||||
}
|
||||
var batch model.Batch
|
||||
if db.Where("id = ?", *batchID).First(&batch).Error != nil {
|
||||
return nil, false
|
||||
}
|
||||
return &batch.RoomID, true
|
||||
}
|
||||
|
||||
// defaultOrgID 返回当前用户第一个组织;管理员未指定时返回默认组织。
|
||||
func defaultOrgID(db *gorm.DB, c *gin.Context) *string {
|
||||
if userID := currentUserID(c); userID != nil {
|
||||
var member model.OrganizationMember
|
||||
if db.Where("user_id = ?", *userID).Order("created_at ASC").First(&member).Error == nil {
|
||||
return &member.OrganizationID
|
||||
}
|
||||
}
|
||||
var org model.Organization
|
||||
if db.Where("code = ?", defaultOrgCode).First(&org).Error == nil {
|
||||
return &org.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// firstAccessibleRoomID 返回当前用户可见的第一个房间;用于无显式 roomId 的历史创建流程。
|
||||
func firstAccessibleRoomID(db *gorm.DB, c *gin.Context) *string {
|
||||
var room model.Room
|
||||
q := db.Model(&model.Room{})
|
||||
if !hasGlobalAccess(c) {
|
||||
q = applyOrgScope(q, c, "org_id")
|
||||
}
|
||||
if q.Order("created_at ASC").First(&room).Error == nil {
|
||||
return &room.ID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// assignDefaultOrganization 注册新用户时补齐默认组织成员关系。
|
||||
func assignDefaultOrganization(db *gorm.DB, userID string) {
|
||||
var org model.Organization
|
||||
if db.Where("code = ?", defaultOrgCode).First(&org).Error != nil {
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
db.Model(&model.OrganizationMember{}).
|
||||
Where("organization_id = ? AND user_id = ?", org.ID, userID).
|
||||
Count(&count)
|
||||
if count == 0 {
|
||||
_ = db.Create(&model.OrganizationMember{OrganizationID: org.ID, UserID: userID, Role: "member"}).Error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestCanAccessRoomAdmin(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Set("user", map[string]interface{}{"sub": "admin-1", "role": "admin"})
|
||||
roomID := "room-1"
|
||||
if !canAccessRoom(nil, c, &roomID) {
|
||||
t.Fatal("admin should have global object access")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanAccessRoomMissingUser(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
roomID := "room-1"
|
||||
if canAccessRoom(nil, c, &roomID) {
|
||||
t.Fatal("missing user should not pass room access")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyRoomScopeUsesOrganizationMembership(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("create sqlmock: %v", err)
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open gorm: %v", err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Set("user", map[string]interface{}{"sub": "user-1", "role": "viewer"})
|
||||
|
||||
mock.ExpectQuery(".*CAST\\(room_id AS text\\).*organization_members.*").
|
||||
WithArgs("user-1").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "org_id"}))
|
||||
|
||||
var rooms []map[string]interface{}
|
||||
q := applyRoomScope(db.Model(&struct{}{}).Table("rooms"), c, "room_id")
|
||||
if err := q.Find(&rooms).Error; err != nil {
|
||||
t.Fatalf("scoped query failed: %v", err)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("unmet sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanAccessDeviceKeyAdmin(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Set("user", map[string]interface{}{"sub": "admin-1", "role": "admin"})
|
||||
if !canAccessDeviceKey(nil, c, "device-a") {
|
||||
t.Fatal("admin should have global device access")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanAccessDeviceKeyMissingUser(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
if canAccessDeviceKey(nil, c, "device-a") {
|
||||
t.Fatal("missing user should not pass device access")
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectRoomIDSensorResolvesThroughDevice(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("create sqlmock: %v", err)
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open gorm: %v", err)
|
||||
}
|
||||
|
||||
mock.ExpectQuery(`.*FROM "sensors".*WHERE id = \$1.*`).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "metric", "unit", "data_type", "device_id", "created_at"}).
|
||||
AddRow("sensor-1", "温度", "temperature", nil, nil, "device-1", time.Now()))
|
||||
mock.ExpectQuery(`.*FROM "devices".*WHERE id = \$1.*`).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "device_key", "name", "kind", "model", "firmware", "online_status", "last_seen", "room_id", "topic", "created_at", "updated_at"}).
|
||||
AddRow("device-1", "device-a", "温度设备", "sensor", nil, nil, "online", nil, "room-1", nil, time.Now(), time.Now()))
|
||||
|
||||
roomID, ok := objectRoomID(db, "sensor", "sensor-1")
|
||||
if !ok || roomID == nil || *roomID != "room-1" {
|
||||
t.Fatalf("sensor room = %v, ok = %v, want room-1", roomID, ok)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("unmet sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireObjectAccessForbiddenForForeignDevice(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("create sqlmock: %v", err)
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open gorm: %v", err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Set("user", map[string]interface{}{"sub": "user-1", "role": "viewer"})
|
||||
|
||||
mock.ExpectQuery(`.*FROM "devices".*WHERE id = \$1.*`).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "device_key", "name", "kind", "model", "firmware", "online_status", "last_seen", "room_id", "topic", "created_at", "updated_at"}).
|
||||
AddRow("device-1", "device-a", "温度设备", "sensor", nil, nil, "online", nil, "room-1", nil, time.Now(), time.Now()))
|
||||
mock.ExpectQuery(`.*FROM "rooms".*organization_members.*`).
|
||||
WithArgs("room-1", "user-1").
|
||||
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
|
||||
|
||||
if requireObjectAccess(c, db, "device", "device-1") {
|
||||
t.Fatal("foreign device should not pass object access")
|
||||
}
|
||||
if rec.Code != 403 {
|
||||
t.Fatalf("status = %d, want 403", rec.Code)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("unmet sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstAccessibleRoomIDUsesOrgScope(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
sqlDB, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("create sqlmock: %v", err)
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlDB}), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open gorm: %v", err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(rec)
|
||||
c.Set("user", map[string]interface{}{"sub": "user-1", "role": "viewer"})
|
||||
|
||||
mock.ExpectQuery(`.*FROM "rooms".*CAST\(org_id AS text\).*organization_members.*`).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id", "name", "code", "location", "description", "capacity", "stage", "region", "org_id", "status", "created_at", "updated_at"}).
|
||||
AddRow("room-1", "一号蚕房", nil, nil, nil, nil, nil, nil, "org-1", "active", time.Now(), time.Now()))
|
||||
|
||||
roomID := firstAccessibleRoomID(db, c)
|
||||
if roomID == nil || *roomID != "room-1" {
|
||||
t.Fatalf("first accessible room = %v, want room-1", roomID)
|
||||
}
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("unmet sql expectations: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,12 @@ func RegisterAlarmRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
func listAlarms(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
limit := 500
|
||||
q := db.Model(&model.Alarm{}).Order("triggered_at DESC").Limit(limit)
|
||||
q := db.Model(&model.Alarm{}).
|
||||
Joins("LEFT JOIN devices d ON d.device_key = alarms.device_key").
|
||||
Order("alarms.triggered_at DESC").Limit(limit)
|
||||
q = applyRoomScope(q, c, "d.room_id")
|
||||
if c.Query("openOnly") == "true" {
|
||||
q = q.Where("open = true")
|
||||
q = q.Where("alarms.open = true")
|
||||
}
|
||||
var alarms []model.Alarm
|
||||
q.Find(&alarms)
|
||||
@@ -37,6 +40,9 @@ func listAlarms(db *gorm.DB) gin.HandlerFunc {
|
||||
func getAlarm(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "alarm", id) {
|
||||
return
|
||||
}
|
||||
var alarm model.Alarm
|
||||
if db.Where("id = ?", id).First(&alarm).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "alarm not found"})
|
||||
@@ -50,6 +56,9 @@ func getAlarm(db *gorm.DB) gin.HandlerFunc {
|
||||
func ackAlarm(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "alarm", id) {
|
||||
return
|
||||
}
|
||||
var alarm model.Alarm
|
||||
if db.Where("id = ?", id).First(&alarm).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "alarm not found"})
|
||||
@@ -69,6 +78,9 @@ func ackAlarm(db *gorm.DB) gin.HandlerFunc {
|
||||
func resolveAlarm(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "alarm", id) {
|
||||
return
|
||||
}
|
||||
var alarm model.Alarm
|
||||
if db.Where("id = ?", id).First(&alarm).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "alarm not found"})
|
||||
|
||||
@@ -27,6 +27,9 @@ func getAlarmClip(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "alarm", id) {
|
||||
return
|
||||
}
|
||||
|
||||
// 先检查告警是否存在
|
||||
var alarm model.Alarm
|
||||
|
||||
@@ -85,6 +85,7 @@ func registerHandler(db *gorm.DB, cfg *config.Config) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建用户失败"})
|
||||
return
|
||||
}
|
||||
assignDefaultOrganization(db, user.ID)
|
||||
|
||||
// 记录审计日志
|
||||
uid := user.ID
|
||||
@@ -448,6 +449,7 @@ func ensureDefaultAdmin(db *gorm.DB, cfg *config.Config) {
|
||||
slog.Error("创建默认 admin 失败", "error", err)
|
||||
return
|
||||
}
|
||||
assignDefaultOrganization(db, admin.ID)
|
||||
|
||||
// 记录审计日志
|
||||
uid := admin.ID
|
||||
|
||||
@@ -29,7 +29,9 @@ func RegisterBiosecurityRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
|
||||
func listSeedSources(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.SeedSource{})
|
||||
q := db.Model(&model.SeedSource{}).
|
||||
Joins("LEFT JOIN batches b ON b.id = seed_sources.batch_id")
|
||||
q = applyRoomScope(q, c, "b.room_id")
|
||||
if batch := c.Query("batchId"); batch != "" {
|
||||
q = q.Where("batch_id = ?", batch)
|
||||
}
|
||||
@@ -62,6 +64,12 @@ func createSeedSource(db *gorm.DB) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.BatchID != nil && !requireObjectAccess(c, db, "batch", *body.BatchID) {
|
||||
return
|
||||
}
|
||||
if body.ParentID != nil && !requireObjectAccess(c, db, "seed_source", *body.ParentID) {
|
||||
return
|
||||
}
|
||||
source := model.SeedSource{
|
||||
PublicID: randomPublicID(),
|
||||
BatchID: body.BatchID,
|
||||
@@ -96,6 +104,9 @@ func createSeedSource(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func updateSeedSource(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "seed_source", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var source model.SeedSource
|
||||
if db.Where("id = ?", c.Param("id")).First(&source).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "seed source not found"})
|
||||
@@ -168,7 +179,10 @@ func updateSeedSource(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func listDisinfectionRecords(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.DisinfectionRecord{})
|
||||
q := db.Model(&model.DisinfectionRecord{}).
|
||||
Joins("LEFT JOIN rooms r ON r.id = disinfection_records.room_id").
|
||||
Joins("LEFT JOIN batches b ON b.id = disinfection_records.batch_id")
|
||||
q = applyRoomScope(q, c, "COALESCE(r.id, b.room_id)")
|
||||
if room := c.Query("roomId"); room != "" {
|
||||
q = q.Where("room_id = ?", room)
|
||||
}
|
||||
@@ -215,6 +229,13 @@ func createDisinfectionRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "batchId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
if body.RoomID != nil && !canAccessRoom(db, c, body.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该蚕房下创建消毒记录"})
|
||||
return
|
||||
}
|
||||
if body.BatchID != nil && !requireObjectAccess(c, db, "batch", *body.BatchID) {
|
||||
return
|
||||
}
|
||||
if err := db.Create(&body).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建消毒记录失败"})
|
||||
return
|
||||
@@ -225,6 +246,9 @@ func createDisinfectionRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func updateDisinfectionRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "disinfection_record", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var record model.DisinfectionRecord
|
||||
if db.Where("id = ?", c.Param("id")).First(&record).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "disinfection record not found"})
|
||||
@@ -284,6 +308,9 @@ func issueQR(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "entityType/entityId 无效"})
|
||||
return
|
||||
}
|
||||
if !requireObjectAccess(c, db, body.EntityType, body.EntityID) {
|
||||
return
|
||||
}
|
||||
if !entityExists(db, body.EntityType, body.EntityID) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "二维码关联实体不存在"})
|
||||
return
|
||||
@@ -333,6 +360,12 @@ func resolveQR(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "二维码关联实体不存在"})
|
||||
return
|
||||
}
|
||||
if roomID, ok := objectRoomID(db, link.EntityType, link.EntityID); ok {
|
||||
if !canAccessRoom(db, c, roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该二维码关联实体"})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"entityType": link.EntityType,
|
||||
"publicId": link.PublicID,
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"silk-server-go/internal/middleware"
|
||||
"silk-server-go/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RegisterCaseStudyRoutes 注册脱敏案例沉淀与审核发布路由。
|
||||
func RegisterCaseStudyRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
read := middleware.RequirePermission(db, "case:read")
|
||||
write := middleware.RequirePermission(db, "case:write")
|
||||
rg.GET("/case-studies", read, listCaseStudies(db))
|
||||
rg.GET("/case-studies/:id", read, getCaseStudy(db))
|
||||
rg.POST("/case-studies", write, createCaseStudy(db))
|
||||
rg.POST("/case-studies/from-consultation/:id", write, createCaseStudyFromConsultation(db))
|
||||
rg.PATCH("/case-studies/:id", write, updateCaseStudy(db))
|
||||
rg.POST("/case-studies/:id/review", write, reviewCaseStudy(db))
|
||||
}
|
||||
|
||||
func listCaseStudies(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.CaseStudy{})
|
||||
if status := c.Query("status"); status != "" {
|
||||
if !canReviewKnowledge(c) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权限查看非发布案例"})
|
||||
return
|
||||
}
|
||||
q = q.Where("status = ?", status)
|
||||
if !hasGlobalAccess(c) {
|
||||
if userID := currentUserID(c); userID != nil {
|
||||
q = q.Where(`
|
||||
(source_room_id IS NOT NULL AND source_room_id IN (
|
||||
SELECT r.id FROM rooms r
|
||||
JOIN organization_members om ON om.organization_id = r.org_id
|
||||
WHERE om.user_id = ?
|
||||
)) OR created_by = ?`, *userID, *userID)
|
||||
} else {
|
||||
q = q.Where("1 = 0")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
q = q.Where("status = ?", "published")
|
||||
}
|
||||
if disease := c.Query("disease"); disease != "" {
|
||||
q = q.Where("disease = ?", disease)
|
||||
}
|
||||
var list []model.CaseStudy
|
||||
q.Order("published_at DESC NULLS LAST, created_at DESC").Limit(200).Find(&list)
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
}
|
||||
|
||||
func getCaseStudy(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var study model.CaseStudy
|
||||
if db.Where("id = ?", c.Param("id")).First(&study).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "case study not found"})
|
||||
return
|
||||
}
|
||||
if study.Status != "published" && !canReviewKnowledge(c) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "case study not found"})
|
||||
return
|
||||
}
|
||||
if study.Status != "published" && !hasGlobalAccess(c) && !canManageCaseStudy(db, c, study) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "case study not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, study)
|
||||
}
|
||||
}
|
||||
|
||||
func createCaseStudy(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var body model.CaseStudy
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
body.ID = ""
|
||||
body.Status = "draft"
|
||||
body.CreatedBy = currentUserID(c)
|
||||
if body.Title == "" || body.Disease == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "标题和病种不能为空"})
|
||||
return
|
||||
}
|
||||
if len(body.DesensitizedPayload) == 0 {
|
||||
body.DesensitizedPayload = json.RawMessage(`{}`)
|
||||
}
|
||||
for _, id := range []*string{body.SourceConsultationID, body.SourceDiseaseEventID, body.SourceRoomID} {
|
||||
if id != nil && *id != "" && !isUUID(*id) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "关联 ID 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.SourceConsultationID != nil && !requireObjectAccess(c, db, "consultation", *body.SourceConsultationID) {
|
||||
return
|
||||
}
|
||||
if body.SourceDiseaseEventID != nil && !requireObjectAccess(c, db, "disease_event", *body.SourceDiseaseEventID) {
|
||||
return
|
||||
}
|
||||
if body.SourceRoomID != nil && !canAccessRoom(db, c, body.SourceRoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权引用该蚕房案例"})
|
||||
return
|
||||
}
|
||||
if err := db.Create(&body).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建案例失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, body)
|
||||
}
|
||||
}
|
||||
|
||||
func createCaseStudyFromConsultation(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "consultation", id) {
|
||||
return
|
||||
}
|
||||
var consultation model.Consultation
|
||||
if db.Where("id = ?", id).First(&consultation).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "consultation not found"})
|
||||
return
|
||||
}
|
||||
payload := desensitizedConsultationPayload(db, consultation)
|
||||
disease := "待确认"
|
||||
var trace model.TraceRecord
|
||||
if db.Where("consultation_id = ?", id).Order("created_at DESC").First(&trace).Error == nil {
|
||||
disease = trace.Disease
|
||||
}
|
||||
title := "案例:" + disease
|
||||
if consultation.Title != "" {
|
||||
title = consultation.Title
|
||||
}
|
||||
study := model.CaseStudy{
|
||||
Title: title,
|
||||
Disease: disease,
|
||||
SourceConsultationID: &consultation.ID,
|
||||
SourceRoomID: consultation.RoomID,
|
||||
Summary: consultation.Summary,
|
||||
DesensitizedPayload: payload,
|
||||
Status: "draft",
|
||||
CreatedBy: currentUserID(c),
|
||||
}
|
||||
if err := db.Create(&study).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建脱敏案例失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, study)
|
||||
}
|
||||
}
|
||||
|
||||
func updateCaseStudy(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var study model.CaseStudy
|
||||
if db.Where("id = ?", id).First(&study).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "case study not found"})
|
||||
return
|
||||
}
|
||||
if !requireCaseStudyAccess(db, c, study) {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Title *string `json:"title"`
|
||||
Disease *string `json:"disease"`
|
||||
Region *string `json:"region"`
|
||||
CaseDate *time.Time `json:"caseDate"`
|
||||
Summary *string `json:"summary"`
|
||||
DesensitizedPayload json.RawMessage `json:"desensitizedPayload"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{}
|
||||
if body.Title != nil {
|
||||
updates["title"] = *body.Title
|
||||
}
|
||||
if body.Disease != nil {
|
||||
updates["disease"] = *body.Disease
|
||||
}
|
||||
if body.Region != nil {
|
||||
updates["region"] = *body.Region
|
||||
}
|
||||
if body.CaseDate != nil {
|
||||
updates["case_date"] = *body.CaseDate
|
||||
}
|
||||
if body.Summary != nil {
|
||||
updates["summary"] = *body.Summary
|
||||
}
|
||||
if len(body.DesensitizedPayload) > 0 {
|
||||
updates["desensitized_payload"] = body.DesensitizedPayload
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
if err := db.Model(&model.CaseStudy{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "更新案例失败"})
|
||||
return
|
||||
}
|
||||
}
|
||||
db.Where("id = ?", id).First(&study)
|
||||
c.JSON(http.StatusOK, study)
|
||||
}
|
||||
}
|
||||
|
||||
func reviewCaseStudy(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var study model.CaseStudy
|
||||
if db.Where("id = ?", id).First(&study).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "case study not found"})
|
||||
return
|
||||
}
|
||||
if !requireCaseStudyAccess(db, c, study) {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Status string `json:"status"`
|
||||
Note *string `json:"note"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !model.ValidCaseStudyTransition(study.Status, body.Status) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "非法的案例状态流转"})
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
updates := map[string]interface{}{
|
||||
"status": body.Status,
|
||||
"reviewer_id": currentUserID(c),
|
||||
"reviewed_at": now,
|
||||
}
|
||||
if body.Note != nil {
|
||||
updates["review_note"] = *body.Note
|
||||
}
|
||||
if body.Status == "published" {
|
||||
updates["published_at"] = now
|
||||
}
|
||||
if err := db.Model(&model.CaseStudy{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "审核案例失败"})
|
||||
return
|
||||
}
|
||||
db.Where("id = ?", id).First(&study)
|
||||
c.JSON(http.StatusOK, study)
|
||||
}
|
||||
}
|
||||
|
||||
func requireCaseStudyAccess(db *gorm.DB, c *gin.Context, study model.CaseStudy) bool {
|
||||
if canManageCaseStudy(db, c, study) {
|
||||
return true
|
||||
}
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该案例"})
|
||||
return false
|
||||
}
|
||||
|
||||
func canManageCaseStudy(db *gorm.DB, c *gin.Context, study model.CaseStudy) bool {
|
||||
if hasGlobalAccess(c) {
|
||||
return true
|
||||
}
|
||||
if study.SourceRoomID != nil && canAccessRoom(db, c, study.SourceRoomID) {
|
||||
return true
|
||||
}
|
||||
userID := currentUserID(c)
|
||||
if study.CreatedBy != nil && userID != nil && *study.CreatedBy == *userID {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func desensitizedConsultationPayload(db *gorm.DB, consultation model.Consultation) json.RawMessage {
|
||||
data := map[string]interface{}{
|
||||
"sourceStatus": consultation.Status,
|
||||
"createdAt": consultation.CreatedAt,
|
||||
}
|
||||
if consultation.Summary != nil {
|
||||
data["summary"] = *consultation.Summary
|
||||
}
|
||||
if consultation.Opinion != nil {
|
||||
data["opinion"] = *consultation.Opinion
|
||||
}
|
||||
if consultation.Plan != nil {
|
||||
data["plan"] = *consultation.Plan
|
||||
}
|
||||
if consultation.RoomID != nil {
|
||||
var room model.Room
|
||||
if db.Select("region").Where("id = ?", *consultation.RoomID).First(&room).Error == nil && room.Region != nil {
|
||||
data["region"] = *room.Region
|
||||
}
|
||||
}
|
||||
if consultation.LampTestID != nil {
|
||||
var lamp model.LampTest
|
||||
if db.Select("method", "result", "resulted_at", "status").
|
||||
Where("id = ?", *consultation.LampTestID).First(&lamp).Error == nil {
|
||||
data["method"] = lamp.Method
|
||||
data["result"] = lamp.Result
|
||||
data["detectionStatus"] = lamp.Status
|
||||
data["resultedAt"] = lamp.ResultedAt
|
||||
}
|
||||
}
|
||||
raw, _ := json.Marshal(data)
|
||||
return raw
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func RegisterConsultationRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
// listConsultations 会诊单列表(status 过滤)
|
||||
func listConsultations(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.Consultation{})
|
||||
q := applyRoomScope(db.Model(&model.Consultation{}), c, "room_id")
|
||||
if status := c.Query("status"); status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
@@ -57,6 +57,9 @@ func fillConsultationRoomNames(db *gorm.DB, list []model.Consultation) {
|
||||
// getConsultation 会诊单详情
|
||||
func getConsultation(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "consultation", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var t model.Consultation
|
||||
if db.Where("id = ?", c.Param("id")).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "consultation not found"})
|
||||
@@ -87,6 +90,16 @@ func createConsultation(db *gorm.DB) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.RoomID != nil && !canAccessRoom(db, c, body.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权为该蚕房发起会诊"})
|
||||
return
|
||||
}
|
||||
if body.BatchID != nil && !requireObjectAccess(c, db, "batch", *body.BatchID) {
|
||||
return
|
||||
}
|
||||
if body.LampTestID != nil && !requireObjectAccess(c, db, "lamp_test", *body.LampTestID) {
|
||||
return
|
||||
}
|
||||
|
||||
rec := model.Consultation{
|
||||
RoomID: body.RoomID,
|
||||
@@ -167,6 +180,9 @@ func buildConsultationSnapshot(db *gorm.DB, roomID, batchID *string, lamp *model
|
||||
func updateConsultation(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "consultation", id) {
|
||||
return
|
||||
}
|
||||
var t model.Consultation
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "consultation not found"})
|
||||
@@ -177,6 +193,21 @@ func updateConsultation(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if roomID, ok := updates["room_id"].(string); ok && !canAccessRoom(db, c, &roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权迁移会诊到指定蚕房"})
|
||||
return
|
||||
}
|
||||
for _, ref := range []struct {
|
||||
key string
|
||||
kind string
|
||||
}{
|
||||
{key: "batch_id", kind: "batch"},
|
||||
{key: "lamp_test_id", kind: "lamp_test"},
|
||||
} {
|
||||
if id, ok := updates[ref.key].(string); ok && !requireObjectAccess(c, db, ref.kind, id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if s, ok := updates["status"]; ok {
|
||||
to, _ := s.(string)
|
||||
if !model.ValidConsultationTransition(t.Status, to) {
|
||||
@@ -206,6 +237,9 @@ func updateConsultation(db *gorm.DB) gin.HandlerFunc {
|
||||
func resolveConsultation(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "consultation", id) {
|
||||
return
|
||||
}
|
||||
var t model.Consultation
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "consultation not found"})
|
||||
@@ -271,6 +305,9 @@ func consultationStrPtr(value string) *string {
|
||||
func archiveConsultation(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "consultation", id) {
|
||||
return
|
||||
}
|
||||
var t model.Consultation
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "consultation not found"})
|
||||
|
||||
@@ -51,8 +51,8 @@ type ControlCommand struct {
|
||||
// RegisterControlRoutes 注册控制命令路由
|
||||
func RegisterControlRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
perm := middleware.RequirePermission(db, "device:control")
|
||||
rg.POST("/control/send", perm, sendControl())
|
||||
rg.POST("/control/batch", perm, batchControl())
|
||||
rg.POST("/control/send", perm, sendControl(db))
|
||||
rg.POST("/control/batch", perm, batchControl(db))
|
||||
rg.POST("/devices/:id/gstmb1/command", perm, sendGSTMB1Command(db))
|
||||
rg.POST("/devices/:id/gstmb1/info", perm, sendGSTMB1Info(db))
|
||||
rg.POST("/devices/:id/gstmb1/restart", perm, sendGSTMB1Restart(db))
|
||||
@@ -70,13 +70,16 @@ func RegisterControlRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
}
|
||||
|
||||
// sendControl 下发单条控制命令(通过 MQTT 发布到 devices/{deviceKey}/cmd 主题)
|
||||
func sendControl() gin.HandlerFunc {
|
||||
func sendControl(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var cmd ControlCommand
|
||||
if err := c.ShouldBindJSON(&cmd); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !requireDeviceKeyAccess(c, db, cmd.DeviceKey) {
|
||||
return
|
||||
}
|
||||
result, err := publishControl(cmd)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -91,6 +94,9 @@ func sendControl() gin.HandlerFunc {
|
||||
// sendPlugOn 插座通电 {"type":"event","key":1}
|
||||
func sendPlugOn(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
||||
"type": "event",
|
||||
"key": 1,
|
||||
@@ -106,6 +112,9 @@ func sendPlugOn(db *gorm.DB) gin.HandlerFunc {
|
||||
// sendPlugOff 插座断电 {"type":"event","key":0}
|
||||
func sendPlugOff(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
||||
"type": "event",
|
||||
"key": 0,
|
||||
@@ -121,6 +130,9 @@ func sendPlugOff(db *gorm.DB) gin.HandlerFunc {
|
||||
// sendPlugStatistic 查询插座电量信息 {"type":"statistic"}
|
||||
func sendPlugStatistic(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
||||
"type": "statistic",
|
||||
"messageId": fmt.Sprintf("%d", time.Now().UnixMilli()),
|
||||
@@ -138,6 +150,9 @@ func sendPlugStatistic(db *gorm.DB) gin.HandlerFunc {
|
||||
// sendIRLearn 学习红外码 {"type":"infrared","action":"learn","data":{"no":N}}
|
||||
func sendIRLearn(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
No int `json:"no"`
|
||||
}
|
||||
@@ -161,6 +176,9 @@ func sendIRLearn(db *gorm.DB) gin.HandlerFunc {
|
||||
// sendIREmit 发射红外码 {"type":"infrared","action":"emit","data":{"no":N}}
|
||||
func sendIREmit(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
No int `json:"no"`
|
||||
}
|
||||
@@ -184,6 +202,9 @@ func sendIREmit(db *gorm.DB) gin.HandlerFunc {
|
||||
// sendIRCancel 取消学习 {"type":"infrared","action":"learnCancel"}
|
||||
func sendIRCancel(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
||||
"type": "infrared",
|
||||
"action": "learnCancel",
|
||||
@@ -199,6 +220,9 @@ func sendIRCancel(db *gorm.DB) gin.HandlerFunc {
|
||||
// sendIRErase 擦除全部红外码 {"type":"infrared","action":"erase"}
|
||||
func sendIRErase(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
||||
"type": "infrared",
|
||||
"action": "erase",
|
||||
@@ -214,6 +238,9 @@ func sendIRErase(db *gorm.DB) gin.HandlerFunc {
|
||||
// getIRStatus 查询红外操作结果
|
||||
func getIRStatus(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
deviceKey, err := getDeviceKey(db, c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
@@ -234,7 +261,7 @@ func getIRStatus(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// batchControl 批量下发控制命令
|
||||
func batchControl() gin.HandlerFunc {
|
||||
func batchControl(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var body struct {
|
||||
Commands []ControlCommand `json:"commands"`
|
||||
@@ -246,6 +273,9 @@ func batchControl() gin.HandlerFunc {
|
||||
|
||||
results := make([]gin.H, 0, len(body.Commands))
|
||||
for _, cmd := range body.Commands {
|
||||
if !requireDeviceKeyAccess(c, db, cmd.DeviceKey) {
|
||||
continue
|
||||
}
|
||||
result, err := publishControl(cmd)
|
||||
if err != nil {
|
||||
results = append(results, gin.H{"error": err.Error(), "deviceKey": cmd.DeviceKey})
|
||||
@@ -328,6 +358,9 @@ func publishGSTMB1Command(db *gorm.DB, deviceID string, payload map[string]inter
|
||||
// sendGSTMB1Command 发送自定义 GSTMB1 命令(body 原样转发)
|
||||
func sendGSTMB1Command(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var body map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -345,6 +378,9 @@ func sendGSTMB1Command(db *gorm.DB) gin.HandlerFunc {
|
||||
// sendGSTMB1Info 获取设备信息 {"type":"info"}
|
||||
func sendGSTMB1Info(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
||||
"type": "info",
|
||||
"messageId": fmt.Sprintf("%d", time.Now().UnixMilli()),
|
||||
@@ -360,6 +396,9 @@ func sendGSTMB1Info(db *gorm.DB) gin.HandlerFunc {
|
||||
// sendGSTMB1Restart 重启设备 {"type":"setting","system":"restart"}
|
||||
func sendGSTMB1Restart(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
result, err := publishGSTMB1Command(db, c.Param("id"), map[string]interface{}{
|
||||
"type": "setting",
|
||||
"system": "restart",
|
||||
@@ -375,6 +414,9 @@ func sendGSTMB1Restart(db *gorm.DB) gin.HandlerFunc {
|
||||
// sendGSTMB1Interval 设置定时上报间隔 {"type":"setting","timerEnable":1,"timerInterval":N}
|
||||
func sendGSTMB1Interval(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "device", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Interval int `json:"interval"`
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func RegisterDetectionTaskRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
|
||||
func listDetectionTasks(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.DetectionTask{})
|
||||
q := applyRoomScope(db.Model(&model.DetectionTask{}), c, "room_id")
|
||||
if status := c.Query("status"); status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
@@ -55,6 +55,9 @@ func listDetectionTasks(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func getDetectionTask(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "detection_task", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var task model.DetectionTask
|
||||
if db.Where("id = ?", c.Param("id")).First(&task).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "detection task not found"})
|
||||
@@ -98,6 +101,16 @@ func createDetectionTask(db *gorm.DB) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.RoomID != nil && !canAccessRoom(db, c, body.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该蚕房下创建检测任务"})
|
||||
return
|
||||
}
|
||||
if body.BatchID != nil && !requireObjectAccess(c, db, "batch", *body.BatchID) {
|
||||
return
|
||||
}
|
||||
if body.InspectionID != nil && !requireObjectAccess(c, db, "inspection", *body.InspectionID) {
|
||||
return
|
||||
}
|
||||
sourceKey := body.SourceKey
|
||||
if sourceKey == "" {
|
||||
sourceKey = fmt.Sprintf("manual-%d", time.Now().UnixNano())
|
||||
@@ -141,6 +154,9 @@ func createDetectionTask(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func updateDetectionTask(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "detection_task", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var task model.DetectionTask
|
||||
if db.Where("id = ?", c.Param("id")).First(&task).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "detection task not found"})
|
||||
@@ -220,6 +236,9 @@ func updateDetectionTask(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func listSamples(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "detection_task", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var samples []model.Sample
|
||||
db.Where("detection_task_id = ?", c.Param("id")).Order("created_at ASC").Find(&samples)
|
||||
c.JSON(http.StatusOK, samples)
|
||||
@@ -228,6 +247,9 @@ func listSamples(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func createSample(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "detection_task", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var task model.DetectionTask
|
||||
if db.Where("id = ?", c.Param("id")).First(&task).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "detection task not found"})
|
||||
@@ -281,6 +303,9 @@ func createSample(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func updateSample(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "sample", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var sample model.Sample
|
||||
if db.Where("id = ?", c.Param("id")).First(&sample).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "sample not found"})
|
||||
@@ -335,7 +360,7 @@ func updateSample(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func listDiseaseEvents(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.DiseaseEvent{})
|
||||
q := applyRoomScope(db.Model(&model.DiseaseEvent{}), c, "room_id")
|
||||
if status := c.Query("status"); status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
@@ -380,6 +405,25 @@ func createDiseaseEvent(db *gorm.DB) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.RoomID != nil && !canAccessRoom(db, c, body.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该蚕房下创建发病事件"})
|
||||
return
|
||||
}
|
||||
if body.BatchID != nil && !requireObjectAccess(c, db, "batch", *body.BatchID) {
|
||||
return
|
||||
}
|
||||
if body.DetectionTaskID != nil && !requireObjectAccess(c, db, "detection_task", *body.DetectionTaskID) {
|
||||
return
|
||||
}
|
||||
if body.LampTestID != nil && !requireObjectAccess(c, db, "lamp_test", *body.LampTestID) {
|
||||
return
|
||||
}
|
||||
if body.ConsultationID != nil && !requireObjectAccess(c, db, "consultation", *body.ConsultationID) {
|
||||
return
|
||||
}
|
||||
if body.InspectionID != nil && !requireObjectAccess(c, db, "inspection", *body.InspectionID) {
|
||||
return
|
||||
}
|
||||
if body.Status == "" {
|
||||
body.Status = "suspected"
|
||||
}
|
||||
@@ -429,6 +473,9 @@ func createDiseaseEvent(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func updateDiseaseEvent(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "disease_event", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
var event model.DiseaseEvent
|
||||
if db.Where("id = ?", c.Param("id")).First(&event).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "disease event not found"})
|
||||
|
||||
@@ -23,7 +23,7 @@ func RegisterDeviceRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
func listDevices(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var devices []model.Device
|
||||
query := db.Order("created_at DESC")
|
||||
query := applyRoomScope(db.Model(&model.Device{}), c, "room_id").Order("created_at DESC")
|
||||
if kind := c.Query("kind"); kind != "" {
|
||||
query = query.Where("kind = ?", kind)
|
||||
}
|
||||
@@ -36,6 +36,9 @@ func listDevices(db *gorm.DB) gin.HandlerFunc {
|
||||
func getDevice(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "device", id) {
|
||||
return
|
||||
}
|
||||
var device model.Device
|
||||
if db.Where("id = ?", id).First(&device).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "device not found"})
|
||||
@@ -64,6 +67,10 @@ func createDevice(db *gorm.DB) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
device.ID = "" // 让数据库自动生成
|
||||
if !canAccessRoom(db, c, &device.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该蚕房下新建设备"})
|
||||
return
|
||||
}
|
||||
if err := db.Create(&device).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
|
||||
return
|
||||
@@ -76,6 +83,9 @@ func createDevice(db *gorm.DB) gin.HandlerFunc {
|
||||
func updateDevice(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "device", id) {
|
||||
return
|
||||
}
|
||||
var device model.Device
|
||||
if db.Where("id = ?", id).First(&device).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "device not found"})
|
||||
@@ -86,6 +96,10 @@ func updateDevice(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if roomID, ok := updates["room_id"].(string); ok && !canAccessRoom(db, c, &roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权迁移设备到指定蚕房"})
|
||||
return
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
db.Model(&model.Device{}).Where("id = ?", id).Updates(updates)
|
||||
}
|
||||
@@ -98,6 +112,9 @@ func updateDevice(db *gorm.DB) gin.HandlerFunc {
|
||||
func deleteDevice(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "device", id) {
|
||||
return
|
||||
}
|
||||
var device model.Device
|
||||
if db.Where("id = ?", id).First(&device).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "device not found"})
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"silk-server-go/internal/middleware"
|
||||
"silk-server-go/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RegisterDeviceMaintenanceRoutes 注册设备校准/故障/维护/固件记录路由。
|
||||
func RegisterDeviceMaintenanceRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
read := middleware.RequirePermission(db, "device:read")
|
||||
write := middleware.RequirePermission(db, "device:write")
|
||||
rg.GET("/device-maintenance", read, listDeviceMaintenance(db))
|
||||
rg.POST("/device-maintenance", write, createDeviceMaintenance(db))
|
||||
rg.PATCH("/device-maintenance/:id", write, updateDeviceMaintenance(db))
|
||||
rg.DELETE("/device-maintenance/:id", write, deleteDeviceMaintenance(db))
|
||||
}
|
||||
|
||||
func listDeviceMaintenance(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.DeviceMaintenanceRecord{}).
|
||||
Joins("JOIN devices d ON d.id = device_maintenance_records.device_id")
|
||||
q = applyRoomScope(q, c, "d.room_id")
|
||||
if deviceID := c.Query("deviceId"); deviceID != "" {
|
||||
q = q.Where("device_maintenance_records.device_id = ?", deviceID)
|
||||
}
|
||||
if kind := c.Query("kind"); kind != "" {
|
||||
q = q.Where("device_maintenance_records.kind = ?", kind)
|
||||
}
|
||||
var list []model.DeviceMaintenanceRecord
|
||||
q.Order("device_maintenance_records.created_at DESC").Limit(200).Find(&list)
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
}
|
||||
|
||||
func createDeviceMaintenance(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var body struct {
|
||||
DeviceID string `json:"deviceId"`
|
||||
Kind string `json:"kind"`
|
||||
Title string `json:"title"`
|
||||
ScheduledAt *time.Time `json:"scheduledAt"`
|
||||
PerformedAt *time.Time `json:"performedAt"`
|
||||
PerformerID *string `json:"performerId"`
|
||||
Result *string `json:"result"`
|
||||
FirmwareFrom *string `json:"firmwareFrom"`
|
||||
FirmwareTo *string `json:"firmwareTo"`
|
||||
CostAmount *float64 `json:"costAmount"`
|
||||
CostUnit *string `json:"costUnit"`
|
||||
Note *string `json:"note"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !isUUID(body.DeviceID) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "deviceId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
if !requireObjectAccess(c, db, "device", body.DeviceID) {
|
||||
return
|
||||
}
|
||||
record := model.DeviceMaintenanceRecord{
|
||||
DeviceID: body.DeviceID, Kind: body.Kind, Title: body.Title,
|
||||
ScheduledAt: body.ScheduledAt, PerformedAt: body.PerformedAt,
|
||||
PerformerID: body.PerformerID, Result: body.Result,
|
||||
FirmwareFrom: body.FirmwareFrom, FirmwareTo: body.FirmwareTo,
|
||||
CostAmount: body.CostAmount, CostUnit: body.CostUnit, Note: body.Note,
|
||||
CreatedBy: currentUserID(c),
|
||||
}
|
||||
if err := model.ValidateDeviceMaintenanceRecord(record); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := db.Create(&record).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建设备维护记录失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, record)
|
||||
}
|
||||
}
|
||||
|
||||
func updateDeviceMaintenance(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "device_maintenance_record", id) {
|
||||
return
|
||||
}
|
||||
var record model.DeviceMaintenanceRecord
|
||||
if db.Where("id = ?", id).First(&record).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "device maintenance record not found"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Kind *string `json:"kind"`
|
||||
Title *string `json:"title"`
|
||||
ScheduledAt *time.Time `json:"scheduledAt"`
|
||||
PerformedAt *time.Time `json:"performedAt"`
|
||||
PerformerID *string `json:"performerId"`
|
||||
Result *string `json:"result"`
|
||||
FirmwareFrom *string `json:"firmwareFrom"`
|
||||
FirmwareTo *string `json:"firmwareTo"`
|
||||
CostAmount *float64 `json:"costAmount"`
|
||||
CostUnit *string `json:"costUnit"`
|
||||
Note *string `json:"note"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{}
|
||||
if body.Kind != nil {
|
||||
if !model.ValidMaintenanceKind(*body.Kind) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "kind 仅支持 calibration/fault/maintenance/firmware"})
|
||||
return
|
||||
}
|
||||
updates["kind"] = *body.Kind
|
||||
}
|
||||
if body.Title != nil {
|
||||
updates["title"] = *body.Title
|
||||
}
|
||||
if body.ScheduledAt != nil {
|
||||
updates["scheduled_at"] = *body.ScheduledAt
|
||||
}
|
||||
if body.PerformedAt != nil {
|
||||
updates["performed_at"] = *body.PerformedAt
|
||||
}
|
||||
if body.PerformerID != nil {
|
||||
updates["performer_id"] = *body.PerformerID
|
||||
}
|
||||
if body.Result != nil {
|
||||
updates["result"] = *body.Result
|
||||
}
|
||||
if body.FirmwareFrom != nil {
|
||||
updates["firmware_from"] = *body.FirmwareFrom
|
||||
}
|
||||
if body.FirmwareTo != nil {
|
||||
updates["firmware_to"] = *body.FirmwareTo
|
||||
}
|
||||
if body.CostAmount != nil {
|
||||
updates["cost_amount"] = *body.CostAmount
|
||||
}
|
||||
if body.CostUnit != nil {
|
||||
updates["cost_unit"] = *body.CostUnit
|
||||
}
|
||||
if body.Note != nil {
|
||||
updates["note"] = *body.Note
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
if err := db.Model(&model.DeviceMaintenanceRecord{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "更新设备维护记录失败"})
|
||||
return
|
||||
}
|
||||
}
|
||||
db.Where("id = ?", id).First(&record)
|
||||
c.JSON(http.StatusOK, record)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteDeviceMaintenance(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "device_maintenance_record", id) {
|
||||
return
|
||||
}
|
||||
var record model.DeviceMaintenanceRecord
|
||||
if db.Where("id = ?", id).First(&record).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "device maintenance record not found"})
|
||||
return
|
||||
}
|
||||
db.Where("id = ?", id).Delete(&model.DeviceMaintenanceRecord{})
|
||||
c.JSON(http.StatusOK, gin.H{"id": id})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"silk-server-go/internal/middleware"
|
||||
"silk-server-go/internal/model"
|
||||
"silk-server-go/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RegisterFarmRoutes 注册产量、死亡、淘汰、损失和成本记录路由。
|
||||
func RegisterFarmRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
read := middleware.RequirePermission(db, "farm:read")
|
||||
write := middleware.RequirePermission(db, "farm:write")
|
||||
rg.GET("/production-loss-records", read, listProductionLossRecords(db))
|
||||
rg.GET("/production-loss-records/stats", read, productionLossStats(db))
|
||||
rg.POST("/production-loss-records", write, createProductionLossRecord(db))
|
||||
rg.PATCH("/production-loss-records/:id", write, updateProductionLossRecord(db))
|
||||
rg.DELETE("/production-loss-records/:id", write, deleteProductionLossRecord(db))
|
||||
}
|
||||
|
||||
func listProductionLossRecords(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.ProductionLossRecord{}).
|
||||
Joins("LEFT JOIN batches b ON b.id = production_loss_records.batch_id")
|
||||
q = applyRoomScope(q, c, "COALESCE(production_loss_records.room_id, b.room_id)")
|
||||
if roomID := c.Query("roomId"); roomID != "" {
|
||||
q = q.Where("production_loss_records.room_id = ?", roomID)
|
||||
}
|
||||
if batchID := c.Query("batchId"); batchID != "" {
|
||||
q = q.Where("production_loss_records.batch_id = ?", batchID)
|
||||
}
|
||||
var list []model.ProductionLossRecord
|
||||
q.Order("production_loss_records.record_date DESC").Limit(200).Find(&list)
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
}
|
||||
|
||||
func createProductionLossRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var body struct {
|
||||
RoomID *string `json:"roomId"`
|
||||
BatchID *string `json:"batchId"`
|
||||
RecordDate *time.Time `json:"recordDate"`
|
||||
DeathCount *int `json:"deathCount"`
|
||||
CulledCount *int `json:"culledCount"`
|
||||
YieldKg *float64 `json:"yieldKg"`
|
||||
LossKg *float64 `json:"lossKg"`
|
||||
CostType *string `json:"costType"`
|
||||
CostAmount *float64 `json:"costAmount"`
|
||||
Note *string `json:"note"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if body.BatchID != nil {
|
||||
if !requireObjectAccess(c, db, "batch", *body.BatchID) {
|
||||
return
|
||||
}
|
||||
if body.RoomID == nil {
|
||||
if roomID, ok := objectRoomID(db, "batch", *body.BatchID); ok {
|
||||
body.RoomID = roomID
|
||||
}
|
||||
}
|
||||
}
|
||||
if body.RoomID != nil && !canAccessRoom(db, c, body.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该蚕房下记录产量损失"})
|
||||
return
|
||||
}
|
||||
record := model.ProductionLossRecord{
|
||||
RoomID: body.RoomID, BatchID: body.BatchID,
|
||||
DeathCount: body.DeathCount, CulledCount: body.CulledCount,
|
||||
YieldKg: body.YieldKg, LossKg: body.LossKg,
|
||||
CostType: body.CostType, CostAmount: body.CostAmount,
|
||||
Note: body.Note, CreatedBy: currentUserID(c),
|
||||
}
|
||||
if body.RecordDate != nil {
|
||||
record.RecordDate = *body.RecordDate
|
||||
} else {
|
||||
record.RecordDate = time.Now()
|
||||
}
|
||||
if err := model.ValidateProductionLossRecord(record); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := db.Create(&record).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建产量损失记录失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, record)
|
||||
}
|
||||
}
|
||||
|
||||
func updateProductionLossRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "production_loss_record", id) {
|
||||
return
|
||||
}
|
||||
var record model.ProductionLossRecord
|
||||
if db.Where("id = ?", id).First(&record).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "production loss record not found"})
|
||||
return
|
||||
}
|
||||
updates, err := bindUpdates(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if roomID, ok := updates["room_id"].(string); ok && !canAccessRoom(db, c, &roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权迁移该记录到指定蚕房"})
|
||||
return
|
||||
}
|
||||
if batchID, ok := updates["batch_id"].(string); ok && !requireObjectAccess(c, db, "batch", batchID) {
|
||||
return
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
if err := db.Model(&model.ProductionLossRecord{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "更新产量损失记录失败"})
|
||||
return
|
||||
}
|
||||
}
|
||||
db.Where("id = ?", id).First(&record)
|
||||
c.JSON(http.StatusOK, record)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteProductionLossRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "production_loss_record", id) {
|
||||
return
|
||||
}
|
||||
var record model.ProductionLossRecord
|
||||
if db.Where("id = ?", id).First(&record).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "production loss record not found"})
|
||||
return
|
||||
}
|
||||
db.Where("id = ?", id).Delete(&model.ProductionLossRecord{})
|
||||
c.JSON(http.StatusOK, gin.H{"id": id})
|
||||
}
|
||||
}
|
||||
|
||||
func productionLossStats(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Table("production_loss_records pl").
|
||||
Select(`
|
||||
COALESCE(pl.room_id::text, '') AS room_id,
|
||||
COALESCE(pl.batch_id::text, '') AS batch_id,
|
||||
COALESCE(SUM(pl.death_count), 0) AS death_count,
|
||||
COALESCE(SUM(pl.culled_count), 0) AS culled_count,
|
||||
COALESCE(SUM(pl.yield_kg), 0) AS yield_kg,
|
||||
COALESCE(SUM(pl.loss_kg), 0) AS loss_kg,
|
||||
COALESCE(SUM(pl.cost_amount), 0) AS cost_amount,
|
||||
COUNT(*) AS record_count
|
||||
`).
|
||||
Joins("LEFT JOIN batches b ON b.id = pl.batch_id")
|
||||
q = applyRoomScope(q, c, "COALESCE(pl.room_id, b.room_id)")
|
||||
if from := c.Query("from"); from != "" {
|
||||
q = q.Where("pl.record_date >= ?", from)
|
||||
}
|
||||
if to := c.Query("to"); to != "" {
|
||||
q = q.Where("pl.record_date <= ?", to)
|
||||
}
|
||||
var rows []service.ProductionLossRow
|
||||
q.Group("pl.room_id, pl.batch_id").Scan(&rows)
|
||||
stats := service.AggregateProductionLoss(rows)
|
||||
fillProductionStatNames(db, stats)
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
}
|
||||
|
||||
func fillProductionStatNames(db *gorm.DB, stats []service.ProductionLossStat) {
|
||||
roomNames := make(map[string]string)
|
||||
batchNames := make(map[string]string)
|
||||
for _, s := range stats {
|
||||
if s.RoomID != "" {
|
||||
if _, ok := roomNames[s.RoomID]; !ok {
|
||||
var room model.Room
|
||||
if db.Select("id", "name").Where("id = ?", s.RoomID).First(&room).Error == nil {
|
||||
roomNames[s.RoomID] = room.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.BatchID != "" {
|
||||
if _, ok := batchNames[s.BatchID]; !ok {
|
||||
var batch model.Batch
|
||||
if db.Select("id", "name").Where("id = ?", s.BatchID).First(&batch).Error == nil {
|
||||
batchNames[s.BatchID] = batch.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range stats {
|
||||
stats[i].RoomName = roomNames[stats[i].RoomID]
|
||||
stats[i].BatchName = batchNames[stats[i].BatchID]
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,10 @@ func RegisterHealthProfileRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
func roomHealthProfile(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("roomId")
|
||||
if !canAccessRoom(db, c, &id) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该蚕房"})
|
||||
return
|
||||
}
|
||||
var room model.Room
|
||||
if db.Where("id = ?", id).First(&room).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "room not found"})
|
||||
@@ -107,6 +111,10 @@ func roomHealthProfile(db *gorm.DB) gin.HandlerFunc {
|
||||
func roomEffectReport(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
roomID := c.Param("roomId")
|
||||
if !canAccessRoom(db, c, &roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权访问该蚕房"})
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
parse := func(key string, fallback time.Time) time.Time {
|
||||
raw := c.Query(key)
|
||||
|
||||
@@ -123,6 +123,10 @@ func createInspection(db *gorm.DB, s3 *service.S3Service, ai *service.AIClient,
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
if !canAccessRoom(db, c, &roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该蚕房下发起巡检"})
|
||||
return
|
||||
}
|
||||
rec.RoomID = &roomID
|
||||
}
|
||||
if idemKey != "" {
|
||||
@@ -283,7 +287,7 @@ func loadRoomRisk(db *gorm.DB, roomID string) (*float64, *float64) {
|
||||
// listInspections 巡检记录列表(roomId/limit 过滤)
|
||||
func listInspections(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.InspectionRecord{})
|
||||
q := applyRoomScope(db.Model(&model.InspectionRecord{}), c, "room_id")
|
||||
if room := c.Query("roomId"); room != "" {
|
||||
q = q.Where("room_id = ?", room)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"silk-server-go/internal/middleware"
|
||||
"silk-server-go/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RegisterLaboratoryRoutes 注册实验室结构化结果和分子分型路由。
|
||||
func RegisterLaboratoryRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
read := middleware.RequirePermission(db, "lab:read")
|
||||
write := middleware.RequirePermission(db, "lab:write")
|
||||
rg.GET("/laboratory-results", read, listLaboratoryResults(db))
|
||||
rg.POST("/laboratory-results", write, createLaboratoryResult(db))
|
||||
rg.PATCH("/laboratory-results/:id", write, updateLaboratoryResult(db))
|
||||
rg.DELETE("/laboratory-results/:id", write, deleteLaboratoryResult(db))
|
||||
}
|
||||
|
||||
func listLaboratoryResults(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.LaboratoryResult{}).
|
||||
Joins("LEFT JOIN batches b ON b.id = laboratory_results.batch_id").
|
||||
Joins("LEFT JOIN trace_records tr ON tr.id = laboratory_results.trace_record_id")
|
||||
q = applyRoomScope(q, c, "COALESCE(laboratory_results.room_id, b.room_id, tr.room_id)")
|
||||
if traceID := c.Query("traceRecordId"); traceID != "" {
|
||||
q = q.Where("laboratory_results.trace_record_id = ?", traceID)
|
||||
}
|
||||
if diseaseEventID := c.Query("diseaseEventId"); diseaseEventID != "" {
|
||||
q = q.Where("laboratory_results.disease_event_id = ?", diseaseEventID)
|
||||
}
|
||||
if roomID := c.Query("roomId"); roomID != "" {
|
||||
q = q.Where("laboratory_results.room_id = ?", roomID)
|
||||
}
|
||||
var list []model.LaboratoryResult
|
||||
q.Order("laboratory_results.tested_at DESC NULLS LAST, laboratory_results.created_at DESC").Limit(200).Find(&list)
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
}
|
||||
|
||||
func createLaboratoryResult(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var body struct {
|
||||
TraceRecordID *string `json:"traceRecordId"`
|
||||
DiseaseEventID *string `json:"diseaseEventId"`
|
||||
SampleID *string `json:"sampleId"`
|
||||
RoomID *string `json:"roomId"`
|
||||
BatchID *string `json:"batchId"`
|
||||
LabName string `json:"labName"`
|
||||
ReportNo string `json:"reportNo"`
|
||||
TestType string `json:"testType"`
|
||||
ResultType string `json:"resultType"`
|
||||
Pathogen *string `json:"pathogen"`
|
||||
Genotype *string `json:"genotype"`
|
||||
Method *string `json:"method"`
|
||||
SampleNo *string `json:"sampleNo"`
|
||||
SampleType *string `json:"sampleType"`
|
||||
Findings *string `json:"findings"`
|
||||
ReportURL *string `json:"reportUrl"`
|
||||
TestedAt *time.Time `json:"testedAt"`
|
||||
ConcludedAt *time.Time `json:"concludedAt"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
for _, id := range []*string{body.TraceRecordID, body.DiseaseEventID, body.SampleID, body.RoomID, body.BatchID} {
|
||||
if id != nil && *id != "" && !isUUID(*id) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "关联 ID 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.TraceRecordID != nil && !requireObjectAccess(c, db, "trace_record", *body.TraceRecordID) {
|
||||
return
|
||||
}
|
||||
if body.DiseaseEventID != nil && !requireObjectAccess(c, db, "disease_event", *body.DiseaseEventID) {
|
||||
return
|
||||
}
|
||||
if body.SampleID != nil && !requireObjectAccess(c, db, "sample", *body.SampleID) {
|
||||
return
|
||||
}
|
||||
if body.RoomID != nil && !canAccessRoom(db, c, body.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权录入该蚕房实验室结果"})
|
||||
return
|
||||
}
|
||||
if body.BatchID != nil && !requireObjectAccess(c, db, "batch", *body.BatchID) {
|
||||
return
|
||||
}
|
||||
result := model.LaboratoryResult{
|
||||
TraceRecordID: body.TraceRecordID, DiseaseEventID: body.DiseaseEventID,
|
||||
SampleID: body.SampleID, RoomID: body.RoomID, BatchID: body.BatchID,
|
||||
LabName: body.LabName, ReportNo: body.ReportNo,
|
||||
TestType: body.TestType, ResultType: body.ResultType,
|
||||
Pathogen: body.Pathogen, Genotype: body.Genotype, Method: body.Method,
|
||||
SampleNo: body.SampleNo, SampleType: body.SampleType,
|
||||
Findings: body.Findings, ReportURL: body.ReportURL,
|
||||
TestedAt: body.TestedAt, ConcludedAt: body.ConcludedAt,
|
||||
CreatedBy: currentUserID(c),
|
||||
}
|
||||
if result.RoomID == nil && body.TraceRecordID != nil {
|
||||
var trace model.TraceRecord
|
||||
if db.Where("id = ?", *body.TraceRecordID).First(&trace).Error == nil {
|
||||
result.RoomID = trace.RoomID
|
||||
if result.BatchID == nil {
|
||||
result.BatchID = batchFromTrace(db, trace)
|
||||
}
|
||||
}
|
||||
}
|
||||
if result.RoomID == nil && body.SampleID != nil {
|
||||
var sample model.Sample
|
||||
if db.Where("id = ?", *body.SampleID).First(&sample).Error == nil {
|
||||
result.RoomID = sample.RoomID
|
||||
if result.BatchID == nil {
|
||||
result.BatchID = sample.BatchID
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := model.ValidateLaboratoryResult(result); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := db.Create(&result).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建实验室结果失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, result)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLaboratoryResult(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "laboratory_result", id) {
|
||||
return
|
||||
}
|
||||
var result model.LaboratoryResult
|
||||
if db.Where("id = ?", id).First(&result).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "laboratory result not found"})
|
||||
return
|
||||
}
|
||||
updates, err := bindUpdates(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if roomID, ok := updates["room_id"].(string); ok && !canAccessRoom(db, c, &roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权迁移该结果到指定蚕房"})
|
||||
return
|
||||
}
|
||||
for _, ref := range []struct {
|
||||
key string
|
||||
kind string
|
||||
}{
|
||||
{key: "trace_record_id", kind: "trace_record"},
|
||||
{key: "disease_event_id", kind: "disease_event"},
|
||||
{key: "sample_id", kind: "sample"},
|
||||
{key: "batch_id", kind: "batch"},
|
||||
} {
|
||||
if id, ok := updates[ref.key].(string); ok && !requireObjectAccess(c, db, ref.kind, id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
if err := db.Model(&model.LaboratoryResult{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "更新实验室结果失败"})
|
||||
return
|
||||
}
|
||||
}
|
||||
db.Where("id = ?", id).First(&result)
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteLaboratoryResult(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "laboratory_result", id) {
|
||||
return
|
||||
}
|
||||
var result model.LaboratoryResult
|
||||
if db.Where("id = ?", id).First(&result).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "laboratory result not found"})
|
||||
return
|
||||
}
|
||||
db.Where("id = ?", id).Delete(&model.LaboratoryResult{})
|
||||
c.JSON(http.StatusOK, gin.H{"id": id})
|
||||
}
|
||||
}
|
||||
|
||||
func batchFromTrace(db *gorm.DB, trace model.TraceRecord) *string {
|
||||
if trace.LampTestID != nil {
|
||||
var lamp model.LampTest
|
||||
if db.Where("id = ?", *trace.LampTestID).First(&lamp).Error == nil {
|
||||
return lamp.BatchID
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func RegisterLampRoutes(rg *gin.RouterGroup, db *gorm.DB, s3 *service.S3Service,
|
||||
// listLampTests 检测任务单列表(roomId/batchId/status 过滤)
|
||||
func listLampTests(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.LampTest{})
|
||||
q := applyRoomScope(db.Model(&model.LampTest{}), c, "room_id")
|
||||
if room := c.Query("roomId"); room != "" {
|
||||
q = q.Where("room_id = ?", room)
|
||||
}
|
||||
@@ -76,14 +76,24 @@ func createLampTest(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
if t.RoomID != nil && !canAccessRoom(db, c, t.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该蚕房下创建检测任务"})
|
||||
return
|
||||
}
|
||||
if t.BatchID != nil && !isUUID(*t.BatchID) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "batchId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
if t.BatchID != nil && !requireObjectAccess(c, db, "batch", *t.BatchID) {
|
||||
return
|
||||
}
|
||||
if t.DetectionTaskID != nil && !isUUID(*t.DetectionTaskID) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "detectionTaskId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
if t.DetectionTaskID != nil && !requireObjectAccess(c, db, "detection_task", *t.DetectionTaskID) {
|
||||
return
|
||||
}
|
||||
if t.Status == "" {
|
||||
t.Status = "pending"
|
||||
}
|
||||
@@ -107,6 +117,9 @@ func createLampTest(db *gorm.DB) gin.HandlerFunc {
|
||||
func judgeQPCR(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "lamp_test", id) {
|
||||
return
|
||||
}
|
||||
var t model.LampTest
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"})
|
||||
@@ -144,6 +157,9 @@ func judgeQPCR(db *gorm.DB) gin.HandlerFunc {
|
||||
func uploadLampSpectrum(db *gorm.DB, s3 *service.S3Service, bucket string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "lamp_test", id) {
|
||||
return
|
||||
}
|
||||
var t model.LampTest
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"})
|
||||
@@ -286,6 +302,9 @@ func deleteSpectrumEntry(db *gorm.DB) gin.HandlerFunc {
|
||||
func updateLampTest(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "lamp_test", id) {
|
||||
return
|
||||
}
|
||||
var t model.LampTest
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"})
|
||||
@@ -296,6 +315,21 @@ func updateLampTest(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if roomID, ok := updates["room_id"].(string); ok && !canAccessRoom(db, c, &roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权迁移检测任务到指定蚕房"})
|
||||
return
|
||||
}
|
||||
for _, ref := range []struct {
|
||||
key string
|
||||
kind string
|
||||
}{
|
||||
{key: "batch_id", kind: "batch"},
|
||||
{key: "detection_task_id", kind: "detection_task"},
|
||||
} {
|
||||
if id, ok := updates[ref.key].(string); ok && !requireObjectAccess(c, db, ref.kind, id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
resultSet := false
|
||||
resultValue := ""
|
||||
if r, ok := updates["result"]; ok {
|
||||
@@ -429,6 +463,9 @@ func runCrossValidation(db *gorm.DB, lampTestID, lampResult string) error {
|
||||
func getLampCrossValidation(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "lamp_test", id) {
|
||||
return
|
||||
}
|
||||
var t model.LampTest
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"})
|
||||
@@ -463,6 +500,9 @@ func getLampCrossValidation(db *gorm.DB) gin.HandlerFunc {
|
||||
func deleteLampTest(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "lamp_test", id) {
|
||||
return
|
||||
}
|
||||
var t model.LampTest
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"})
|
||||
@@ -478,6 +518,9 @@ func deleteLampTest(db *gorm.DB) gin.HandlerFunc {
|
||||
func listLampTestSteps(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "lamp_test", id) {
|
||||
return
|
||||
}
|
||||
var t model.LampTest
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"})
|
||||
@@ -493,6 +536,9 @@ func listLampTestSteps(db *gorm.DB) gin.HandlerFunc {
|
||||
func updateLampTestStep(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "lamp_test", id) {
|
||||
return
|
||||
}
|
||||
stepNo, err := strconv.Atoi(c.Param("stepNo"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "stepNo 不合法"})
|
||||
@@ -538,6 +584,9 @@ func updateLampTestStep(db *gorm.DB) gin.HandlerFunc {
|
||||
func uploadLampResultImage(db *gorm.DB, s3 *service.S3Service, bucket string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "lamp_test", id) {
|
||||
return
|
||||
}
|
||||
var t model.LampTest
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "lamp test not found"})
|
||||
|
||||
@@ -22,7 +22,15 @@ func RegisterNotificationRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
func listNotifications(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var list []model.Notification
|
||||
db.Order("created_at DESC").Limit(200).Find(&list)
|
||||
q := db.Order("created_at DESC").Limit(200)
|
||||
if !hasGlobalAccess(c) {
|
||||
if userID := currentUserID(c); userID != nil {
|
||||
q = q.Where("user_id = ?", *userID)
|
||||
} else {
|
||||
q = q.Where("1 = 0")
|
||||
}
|
||||
}
|
||||
q.Find(&list)
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"silk-server-go/internal/middleware"
|
||||
"silk-server-go/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RegisterOrganizationRoutes 注册组织和成员数据范围路由。
|
||||
func RegisterOrganizationRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
perm := middleware.RequirePermission(db, "organization:manage")
|
||||
rg.GET("/organizations", perm, listOrganizations(db))
|
||||
rg.POST("/organizations", perm, createOrganization(db))
|
||||
rg.PATCH("/organizations/:id", perm, updateOrganization(db))
|
||||
rg.GET("/organizations/:id/members", perm, listOrganizationMembers(db))
|
||||
rg.POST("/organizations/:id/members", perm, addOrganizationMember(db))
|
||||
rg.DELETE("/organizations/:id/members/:userId", perm, removeOrganizationMember(db))
|
||||
}
|
||||
|
||||
func listOrganizations(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.Organization{})
|
||||
if !hasGlobalAccess(c) {
|
||||
q = applyOrgScope(q, c, "organizations.id")
|
||||
}
|
||||
var list []model.Organization
|
||||
q.Order("created_at DESC").Find(&list)
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
}
|
||||
|
||||
func createOrganization(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Code string `json:"code"`
|
||||
Description *string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(body.Name) == "" || strings.TrimSpace(body.Code) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "组织名称和编码不能为空"})
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
db.Model(&model.Organization{}).Where("code = ?", body.Code).Count(&count)
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "组织编码已存在"})
|
||||
return
|
||||
}
|
||||
org := model.Organization{Name: body.Name, Code: body.Code, Description: body.Description, Status: "active"}
|
||||
if err := db.Create(&org).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建组织失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, org)
|
||||
}
|
||||
}
|
||||
|
||||
func updateOrganization(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var org model.Organization
|
||||
if db.Where("id = ?", id).First(&org).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "organization not found"})
|
||||
return
|
||||
}
|
||||
if !canAccessOrganization(db, c, id) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该组织"})
|
||||
return
|
||||
}
|
||||
updates, err := bindUpdates(c)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
if err := db.Model(&model.Organization{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "更新组织失败"})
|
||||
return
|
||||
}
|
||||
}
|
||||
db.Where("id = ?", id).First(&org)
|
||||
c.JSON(http.StatusOK, org)
|
||||
}
|
||||
}
|
||||
|
||||
func addOrganizationMember(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
orgID := c.Param("id")
|
||||
var org model.Organization
|
||||
if db.Where("id = ?", orgID).First(&org).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "organization not found"})
|
||||
return
|
||||
}
|
||||
if !canAccessOrganization(db, c, orgID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该组织"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
UserID string `json:"userId"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !isUUID(body.UserID) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "userId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
var user model.User
|
||||
if db.Where("id = ?", body.UserID).First(&user).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
||||
return
|
||||
}
|
||||
role := body.Role
|
||||
if role == "" {
|
||||
role = "member"
|
||||
}
|
||||
if role != "member" && role != "admin" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "组织角色仅支持 member/admin"})
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
db.Model(&model.OrganizationMember{}).
|
||||
Where("organization_id = ? AND user_id = ?", orgID, body.UserID).
|
||||
Count(&count)
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "用户已在该组织"})
|
||||
return
|
||||
}
|
||||
member := model.OrganizationMember{OrganizationID: orgID, UserID: body.UserID, Role: role}
|
||||
if err := db.Create(&member).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "添加成员失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, member)
|
||||
}
|
||||
}
|
||||
|
||||
func listOrganizationMembers(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
orgID := c.Param("id")
|
||||
var org model.Organization
|
||||
if db.Where("id = ?", orgID).First(&org).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "organization not found"})
|
||||
return
|
||||
}
|
||||
if !canAccessOrganization(db, c, orgID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权查看该组织成员"})
|
||||
return
|
||||
}
|
||||
var rows []struct {
|
||||
ID string `gorm:"column:id" json:"id"`
|
||||
OrganizationID string `gorm:"column:organization_id" json:"organizationId"`
|
||||
UserID string `gorm:"column:user_id" json:"userId"`
|
||||
Role string `gorm:"column:role" json:"role"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
Username string `gorm:"column:username" json:"username"`
|
||||
Email string `gorm:"column:email" json:"email"`
|
||||
FullName *string `gorm:"column:full_name" json:"fullName,omitempty"`
|
||||
}
|
||||
if err := db.Table("organization_members om").
|
||||
Select("om.id, om.organization_id, om.user_id, om.role, om.created_at, u.username, u.email, u.full_name").
|
||||
Joins("JOIN users u ON u.id = om.user_id").
|
||||
Where("om.organization_id = ?", orgID).
|
||||
Order("om.created_at DESC").
|
||||
Scan(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "查询组织成员失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, rows)
|
||||
}
|
||||
}
|
||||
|
||||
func removeOrganizationMember(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
orgID := c.Param("id")
|
||||
userID := c.Param("userId")
|
||||
var member model.OrganizationMember
|
||||
if db.Where("organization_id = ? AND user_id = ?", orgID, userID).First(&member).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "organization member not found"})
|
||||
return
|
||||
}
|
||||
if !canAccessOrganization(db, c, orgID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权管理该组织"})
|
||||
return
|
||||
}
|
||||
db.Where("id = ?", member.ID).Delete(&model.OrganizationMember{})
|
||||
c.JSON(http.StatusOK, gin.H{"organizationId": orgID, "userId": userID})
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,8 @@ func RegisterRoomRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
func listRooms(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var rooms []model.Room
|
||||
db.Order("created_at DESC").Find(&rooms)
|
||||
q := applyOrgScope(db.Model(&model.Room{}), c, "org_id")
|
||||
q.Order("created_at DESC").Find(&rooms)
|
||||
c.JSON(http.StatusOK, rooms)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +35,9 @@ func listRooms(db *gorm.DB) gin.HandlerFunc {
|
||||
func getRoom(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "room", id) {
|
||||
return
|
||||
}
|
||||
var room model.Room
|
||||
if db.Where("id = ?", id).First(&room).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "room not found"})
|
||||
@@ -52,6 +56,12 @@ func createRoom(db *gorm.DB) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
room.ID = "" // 让数据库自动生成
|
||||
if room.OrgID == nil {
|
||||
room.OrgID = defaultOrgID(db, c)
|
||||
} else if !canAccessOrganization(db, c, *room.OrgID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该组织下创建蚕房"})
|
||||
return
|
||||
}
|
||||
if err := db.Create(&room).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
|
||||
return
|
||||
@@ -64,6 +74,9 @@ func createRoom(db *gorm.DB) gin.HandlerFunc {
|
||||
func updateRoom(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "room", id) {
|
||||
return
|
||||
}
|
||||
var room model.Room
|
||||
if db.Where("id = ?", id).First(&room).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "room not found"})
|
||||
@@ -74,6 +87,12 @@ func updateRoom(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if orgID, ok := updates["org_id"]; ok {
|
||||
if orgStr, ok := orgID.(string); ok && !canAccessOrganization(db, c, orgStr) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权迁移蚕房到该组织"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
db.Model(&model.Room{}).Where("id = ?", id).Updates(updates)
|
||||
}
|
||||
@@ -86,6 +105,9 @@ func updateRoom(db *gorm.DB) gin.HandlerFunc {
|
||||
func deleteRoom(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "room", id) {
|
||||
return
|
||||
}
|
||||
var room model.Room
|
||||
if db.Where("id = ?", id).First(&room).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "room not found"})
|
||||
|
||||
@@ -23,7 +23,10 @@ func RegisterSensorRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
func listSensors(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var sensors []model.Sensor
|
||||
db.Order("created_at DESC").Find(&sensors)
|
||||
q := db.Model(&model.Sensor{}).
|
||||
Joins("JOIN devices d ON d.id = sensors.device_id")
|
||||
q = applyRoomScope(q, c, "d.room_id")
|
||||
q.Order("sensors.created_at DESC").Find(&sensors)
|
||||
c.JSON(http.StatusOK, sensors)
|
||||
}
|
||||
}
|
||||
@@ -32,6 +35,9 @@ func listSensors(db *gorm.DB) gin.HandlerFunc {
|
||||
func getSensor(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "sensor", id) {
|
||||
return
|
||||
}
|
||||
var sensor model.Sensor
|
||||
if db.Where("id = ?", id).First(&sensor).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "sensor not found"})
|
||||
@@ -50,6 +56,13 @@ func createSensor(db *gorm.DB) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
sensor.ID = "" // 让数据库自动生成
|
||||
if !isUUID(sensor.DeviceID) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "deviceId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
if !requireObjectAccess(c, db, "device", sensor.DeviceID) {
|
||||
return
|
||||
}
|
||||
if err := db.Create(&sensor).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
|
||||
return
|
||||
@@ -62,6 +75,9 @@ func createSensor(db *gorm.DB) gin.HandlerFunc {
|
||||
func updateSensor(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "sensor", id) {
|
||||
return
|
||||
}
|
||||
var sensor model.Sensor
|
||||
if db.Where("id = ?", id).First(&sensor).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "sensor not found"})
|
||||
@@ -72,6 +88,9 @@ func updateSensor(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if deviceID, ok := updates["device_id"].(string); ok && !requireObjectAccess(c, db, "device", deviceID) {
|
||||
return
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
db.Model(&model.Sensor{}).Where("id = ?", id).Updates(updates)
|
||||
}
|
||||
@@ -84,6 +103,9 @@ func updateSensor(db *gorm.DB) gin.HandlerFunc {
|
||||
func deleteSensor(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "sensor", id) {
|
||||
return
|
||||
}
|
||||
var sensor model.Sensor
|
||||
if db.Where("id = ?", id).First(&sensor).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "sensor not found"})
|
||||
|
||||
@@ -33,6 +33,9 @@ func listTelemetry(db *gorm.DB, iotdb *service.IoTDBService) gin.HandlerFunc {
|
||||
if limit <= 0 {
|
||||
limit = 2000
|
||||
}
|
||||
if deviceKey != "" && !requireDeviceKeyAccess(c, db, deviceKey) {
|
||||
return
|
||||
}
|
||||
|
||||
// 优先 IoTDB
|
||||
if iotdb.IsAvailable() && deviceKey != "" && metric != "" {
|
||||
@@ -45,16 +48,19 @@ func listTelemetry(db *gorm.DB, iotdb *service.IoTDBService) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// 降级 PostgreSQL
|
||||
q := db.Model(&model.Telemetry{}).Order("timestamp DESC").Limit(limit)
|
||||
q := db.Model(&model.Telemetry{}).
|
||||
Joins("JOIN devices d ON d.device_key = telemetry.device_key").
|
||||
Order("telemetry.timestamp DESC").Limit(limit)
|
||||
q = applyRoomScope(q, c, "d.room_id")
|
||||
if deviceKey != "" {
|
||||
q = q.Where("device_key = ?", deviceKey)
|
||||
q = q.Where("telemetry.device_key = ?", deviceKey)
|
||||
}
|
||||
if metric != "" {
|
||||
q = q.Where("metric = ?", metric)
|
||||
q = q.Where("telemetry.metric = ?", metric)
|
||||
}
|
||||
if from != "" || to != "" {
|
||||
fromVal, toVal := parseTimeRangeStr(from, to)
|
||||
q = q.Where("timestamp BETWEEN ? AND ?", fromVal, toVal)
|
||||
q = q.Where("telemetry.timestamp BETWEEN ? AND ?", fromVal, toVal)
|
||||
}
|
||||
var rows []model.Telemetry
|
||||
q.Find(&rows)
|
||||
@@ -66,6 +72,9 @@ func listTelemetry(db *gorm.DB, iotdb *service.IoTDBService) gin.HandlerFunc {
|
||||
func listMetrics(iotdb *service.IoTDBService, db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
deviceKey := c.Param("deviceKey")
|
||||
if !requireDeviceKeyAccess(c, db, deviceKey) {
|
||||
return
|
||||
}
|
||||
|
||||
// 优先 IoTDB
|
||||
if iotdb.IsAvailable() {
|
||||
@@ -87,6 +96,9 @@ func listMetrics(iotdb *service.IoTDBService, db *gorm.DB) gin.HandlerFunc {
|
||||
func latestTelemetry(iotdb *service.IoTDBService, db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
deviceKey := c.Param("deviceKey")
|
||||
if !requireDeviceKeyAccess(c, db, deviceKey) {
|
||||
return
|
||||
}
|
||||
|
||||
// 查询 PG 中该设备每个指标的最新记录
|
||||
var records []model.Telemetry
|
||||
@@ -105,6 +117,9 @@ func historyBucket(iotdb *service.IoTDBService, db *gorm.DB) gin.HandlerFunc {
|
||||
metric := c.Param("metric")
|
||||
from := c.Query("from")
|
||||
to := c.Query("to")
|
||||
if !requireDeviceKeyAccess(c, db, deviceKey) {
|
||||
return
|
||||
}
|
||||
if from == "" || to == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "from/to required"})
|
||||
return
|
||||
|
||||
@@ -23,7 +23,11 @@ func RegisterThresholdRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
func listThresholds(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var thresholds []model.Threshold
|
||||
db.Find(&thresholds)
|
||||
q := db.Model(&model.Threshold{}).
|
||||
Joins("JOIN sensors s ON s.id = thresholds.sensor_id").
|
||||
Joins("JOIN devices d ON d.id = s.device_id")
|
||||
q = applyRoomScope(q, c, "d.room_id")
|
||||
q.Order("thresholds.created_at DESC").Find(&thresholds)
|
||||
c.JSON(http.StatusOK, thresholds)
|
||||
}
|
||||
}
|
||||
@@ -32,6 +36,9 @@ func listThresholds(db *gorm.DB) gin.HandlerFunc {
|
||||
func getThreshold(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "threshold", id) {
|
||||
return
|
||||
}
|
||||
var threshold model.Threshold
|
||||
if db.Where("id = ?", id).First(&threshold).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "threshold not found"})
|
||||
@@ -50,6 +57,13 @@ func createThreshold(db *gorm.DB) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
threshold.ID = "" // 让数据库自动生成
|
||||
if !isUUID(threshold.SensorID) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "sensorId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
if !requireObjectAccess(c, db, "sensor", threshold.SensorID) {
|
||||
return
|
||||
}
|
||||
if err := db.Create(&threshold).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
|
||||
return
|
||||
@@ -62,6 +76,9 @@ func createThreshold(db *gorm.DB) gin.HandlerFunc {
|
||||
func updateThreshold(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "threshold", id) {
|
||||
return
|
||||
}
|
||||
var threshold model.Threshold
|
||||
if db.Where("id = ?", id).First(&threshold).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "threshold not found"})
|
||||
@@ -72,6 +89,9 @@ func updateThreshold(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if sensorID, ok := updates["sensor_id"].(string); ok && !requireObjectAccess(c, db, "sensor", sensorID) {
|
||||
return
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
db.Model(&model.Threshold{}).Where("id = ?", id).Updates(updates)
|
||||
}
|
||||
@@ -84,6 +104,9 @@ func updateThreshold(db *gorm.DB) gin.HandlerFunc {
|
||||
func deleteThreshold(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "threshold", id) {
|
||||
return
|
||||
}
|
||||
var threshold model.Threshold
|
||||
if db.Where("id = ?", id).First(&threshold).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "threshold not found"})
|
||||
|
||||
@@ -43,10 +43,11 @@ func traceMonthlyStats(db *gorm.DB) gin.HandlerFunc {
|
||||
Month string `gorm:"column:month"`
|
||||
Disease string `gorm:"column:disease"`
|
||||
}
|
||||
db.Table("trace_records").
|
||||
q := db.Table("trace_records").
|
||||
Select("to_char(created_at, 'YYYY-MM') AS month, disease").
|
||||
Where("created_at >= ? AND created_at < ?", start, end).
|
||||
Scan(&rows)
|
||||
Where("created_at >= ? AND created_at < ?", start, end)
|
||||
q = applyRoomScope(q, c, "room_id")
|
||||
q.Scan(&rows)
|
||||
entries := make([]service.MonthDiseaseEntry, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
entries = append(entries, service.MonthDiseaseEntry{Month: r.Month, Disease: r.Disease})
|
||||
@@ -66,6 +67,7 @@ func traceRegionStats(db *gorm.DB) gin.HandlerFunc {
|
||||
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))
|
||||
q = applyRoomScope(q, c, "trace_records.room_id")
|
||||
if disease := c.Query("disease"); disease != "" {
|
||||
q = q.Where("trace_records.disease = ?", disease)
|
||||
}
|
||||
@@ -84,7 +86,7 @@ func traceRegionStats(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func listTraceRecords(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.TraceRecord{})
|
||||
q := applyRoomScope(db.Model(&model.TraceRecord{}), c, "room_id")
|
||||
if status := c.Query("status"); status != "" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
@@ -116,6 +118,9 @@ func fillTraceRoomNames(db *gorm.DB, list []model.TraceRecord) {
|
||||
|
||||
func getTraceRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "trace_record", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
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"})
|
||||
@@ -145,6 +150,19 @@ func createTraceRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.RoomID != nil && !canAccessRoom(db, c, body.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权为该蚕房创建溯源记录"})
|
||||
return
|
||||
}
|
||||
if body.DiseaseEventID != nil && !requireObjectAccess(c, db, "disease_event", *body.DiseaseEventID) {
|
||||
return
|
||||
}
|
||||
if body.LampTestID != nil && !requireObjectAccess(c, db, "lamp_test", *body.LampTestID) {
|
||||
return
|
||||
}
|
||||
if body.ConsultationID != nil && !requireObjectAccess(c, db, "consultation", *body.ConsultationID) {
|
||||
return
|
||||
}
|
||||
if body.Disease == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "病种不能为空"})
|
||||
return
|
||||
@@ -165,6 +183,9 @@ func createTraceRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
func updateTraceRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "trace_record", id) {
|
||||
return
|
||||
}
|
||||
var t model.TraceRecord
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "trace record not found"})
|
||||
@@ -175,6 +196,22 @@ func updateTraceRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if roomID, ok := updates["room_id"].(string); ok && !canAccessRoom(db, c, &roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权迁移溯源记录到指定蚕房"})
|
||||
return
|
||||
}
|
||||
for _, ref := range []struct {
|
||||
key string
|
||||
kind string
|
||||
}{
|
||||
{key: "disease_event_id", kind: "disease_event"},
|
||||
{key: "lamp_test_id", kind: "lamp_test"},
|
||||
{key: "consultation_id", kind: "consultation"},
|
||||
} {
|
||||
if id, ok := updates[ref.key].(string); ok && !requireObjectAccess(c, db, ref.kind, id) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
db.Model(&model.TraceRecord{}).Where("id = ?", id).Updates(updates)
|
||||
}
|
||||
@@ -186,6 +223,9 @@ func updateTraceRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
func deleteTraceRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "trace_record", id) {
|
||||
return
|
||||
}
|
||||
var t model.TraceRecord
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "trace record not found"})
|
||||
@@ -200,6 +240,9 @@ func deleteTraceRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
func autoTrace(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "trace_record", id) {
|
||||
return
|
||||
}
|
||||
var t model.TraceRecord
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "trace record not found"})
|
||||
@@ -318,6 +361,9 @@ func autoTrace(db *gorm.DB) gin.HandlerFunc {
|
||||
// getTraceChecklist 分病种二级排查清单
|
||||
func getTraceChecklist(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if !requireObjectAccess(c, db, "trace_record", c.Param("id")) {
|
||||
return
|
||||
}
|
||||
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"})
|
||||
@@ -331,6 +377,9 @@ func getTraceChecklist(db *gorm.DB) gin.HandlerFunc {
|
||||
func submitTraceChecklist(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "trace_record", id) {
|
||||
return
|
||||
}
|
||||
var t model.TraceRecord
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "trace record not found"})
|
||||
|
||||
@@ -40,7 +40,7 @@ func RegisterTrayBatchRoutes(rg *gin.RouterGroup, db *gorm.DB) {
|
||||
|
||||
func listTrays(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.Tray{})
|
||||
q := applyRoomScope(db.Model(&model.Tray{}), c, "room_id")
|
||||
if room := c.Query("roomId"); room != "" {
|
||||
q = q.Where("room_id = ?", room)
|
||||
}
|
||||
@@ -69,6 +69,10 @@ func createTray(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
if !canAccessRoom(db, c, &t.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该蚕房下创建蚕匾"})
|
||||
return
|
||||
}
|
||||
if err := db.Create(&t).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
|
||||
return
|
||||
@@ -80,6 +84,9 @@ func createTray(db *gorm.DB) gin.HandlerFunc {
|
||||
func updateTray(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "tray", id) {
|
||||
return
|
||||
}
|
||||
var t model.Tray
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "tray not found"})
|
||||
@@ -90,6 +97,10 @@ func updateTray(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if roomID, ok := updates["room_id"].(string); ok && !canAccessRoom(db, c, &roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权迁移蚕匾到指定蚕房"})
|
||||
return
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
db.Model(&model.Tray{}).Where("id = ?", id).Updates(updates)
|
||||
}
|
||||
@@ -101,6 +112,9 @@ func updateTray(db *gorm.DB) gin.HandlerFunc {
|
||||
func deleteTray(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "tray", id) {
|
||||
return
|
||||
}
|
||||
var t model.Tray
|
||||
if db.Where("id = ?", id).First(&t).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "tray not found"})
|
||||
@@ -115,7 +129,7 @@ func deleteTray(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func listBatches(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.Batch{})
|
||||
q := applyRoomScope(db.Model(&model.Batch{}), c, "room_id")
|
||||
if room := c.Query("roomId"); room != "" {
|
||||
q = q.Where("room_id = ?", room)
|
||||
}
|
||||
@@ -144,6 +158,10 @@ func createBatch(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "roomId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
if !canAccessRoom(db, c, &b.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该蚕房下创建批次"})
|
||||
return
|
||||
}
|
||||
if b.Status == "" {
|
||||
b.Status = "rearing"
|
||||
}
|
||||
@@ -158,6 +176,9 @@ func createBatch(db *gorm.DB) gin.HandlerFunc {
|
||||
func updateBatch(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "batch", id) {
|
||||
return
|
||||
}
|
||||
var b model.Batch
|
||||
if db.Where("id = ?", id).First(&b).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "batch not found"})
|
||||
@@ -168,6 +189,10 @@ func updateBatch(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if roomID, ok := updates["room_id"].(string); ok && !canAccessRoom(db, c, &roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权迁移批次到指定蚕房"})
|
||||
return
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
db.Model(&model.Batch{}).Where("id = ?", id).Updates(updates)
|
||||
}
|
||||
@@ -179,6 +204,9 @@ func updateBatch(db *gorm.DB) gin.HandlerFunc {
|
||||
func deleteBatch(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "batch", id) {
|
||||
return
|
||||
}
|
||||
var b model.Batch
|
||||
if db.Where("id = ?", id).First(&b).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "batch not found"})
|
||||
@@ -195,7 +223,7 @@ func deleteBatch(db *gorm.DB) gin.HandlerFunc {
|
||||
|
||||
func listRearingRecords(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
q := db.Model(&model.RearingRecord{})
|
||||
q := applyRoomScope(db.Table("rearing_records").Joins("JOIN batches b ON b.id = rearing_records.batch_id"), c, "b.room_id")
|
||||
if batch := c.Query("batchId"); batch != "" {
|
||||
q = q.Where("batch_id = ?", batch)
|
||||
}
|
||||
@@ -217,6 +245,9 @@ func createRearingRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "batchId 不是合法的 UUID"})
|
||||
return
|
||||
}
|
||||
if !requireObjectAccess(c, db, "batch", r.BatchID) {
|
||||
return
|
||||
}
|
||||
if r.RecordDate.IsZero() {
|
||||
r.RecordDate = time.Now()
|
||||
}
|
||||
@@ -231,6 +262,9 @@ func createRearingRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
func updateRearingRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "rearing_record", id) {
|
||||
return
|
||||
}
|
||||
var r model.RearingRecord
|
||||
if db.Where("id = ?", id).First(&r).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "rearing record not found"})
|
||||
@@ -252,6 +286,9 @@ func updateRearingRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
func deleteRearingRecord(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "rearing_record", id) {
|
||||
return
|
||||
}
|
||||
var r model.RearingRecord
|
||||
if db.Where("id = ?", id).First(&r).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "rearing record not found"})
|
||||
|
||||
@@ -37,7 +37,8 @@ func RegisterVideoCameraRoutes(rg *gin.RouterGroup, db *gorm.DB, media *service.
|
||||
func listCameras(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var cameras []model.Camera
|
||||
db.Order("created_at DESC").Find(&cameras)
|
||||
q := applyRoomScope(db.Model(&model.Camera{}), c, "room_id")
|
||||
q.Order("created_at DESC").Find(&cameras)
|
||||
|
||||
// 先用 DB 中的 is_online 初始化 Online 字段(gorm:"-" 不会自动填充)
|
||||
for i := range cameras {
|
||||
@@ -98,6 +99,9 @@ func listCameras(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
|
||||
func getCamera(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "camera", id) {
|
||||
return
|
||||
}
|
||||
var camera model.Camera
|
||||
if db.Where("id = ?", id).First(&camera).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
|
||||
@@ -118,8 +122,15 @@ func createCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
|
||||
camera := input.toModel()
|
||||
camera.ID = 0 // 让数据库自动生成
|
||||
if camera.RoomID == nil {
|
||||
defaultRoom := "1"
|
||||
camera.RoomID = &defaultRoom
|
||||
camera.RoomID = firstAccessibleRoomID(db, c)
|
||||
if camera.RoomID == nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "当前账号无可用蚕房,请先选择 roomId"})
|
||||
return
|
||||
}
|
||||
}
|
||||
if !canAccessRoom(db, c, camera.RoomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权在该蚕房下创建摄像头"})
|
||||
return
|
||||
}
|
||||
if err := db.Create(&camera).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "创建失败"})
|
||||
@@ -150,6 +161,9 @@ func createCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
|
||||
func updateCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "camera", id) {
|
||||
return
|
||||
}
|
||||
var camera model.Camera
|
||||
if db.Where("id = ?", id).First(&camera).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
|
||||
@@ -160,6 +174,10 @@ func updateCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if roomID, ok := updates["room_id"].(string); ok && !canAccessRoom(db, c, &roomID) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "无权迁移摄像头到指定蚕房"})
|
||||
return
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
if err := db.Model(&model.Camera{}).Where("id = ?", id).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新失败: " + err.Error()})
|
||||
@@ -198,6 +216,9 @@ func updateCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
|
||||
func deleteCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "camera", id) {
|
||||
return
|
||||
}
|
||||
var camera model.Camera
|
||||
if db.Where("id = ?", id).First(&camera).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
|
||||
@@ -224,6 +245,9 @@ func playCamera(db *gorm.DB, media *service.MediaService) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "camera", id) {
|
||||
return
|
||||
}
|
||||
var camera model.Camera
|
||||
if db.Where("id = ?", id).First(&camera).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
|
||||
@@ -287,6 +311,9 @@ func playbackCamera(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "camera", id) {
|
||||
return
|
||||
}
|
||||
|
||||
limit := 50
|
||||
if l, err := strconv.Atoi(c.Query("limit")); err == nil && l > 0 {
|
||||
@@ -335,10 +362,10 @@ func getWvpConfig(media *service.MediaService) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"sipId": sip["id"],
|
||||
"sipDomain": sip["domain"],
|
||||
"sipPort": sip["port"],
|
||||
"sipShowIp": sip["showIp"],
|
||||
"sipId": sip["id"],
|
||||
"sipDomain": sip["domain"],
|
||||
"sipPort": sip["port"],
|
||||
"sipShowIp": sip["showIp"],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func listClips(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
q := db.Model(&model.VideoClip{}).Order("start_at DESC").Limit(limit)
|
||||
q := applyRoomScope(db.Model(&model.VideoClip{}), c, "room_id").Order("start_at DESC").Limit(limit)
|
||||
if cameraId := c.Query("cameraId"); cameraId != "" {
|
||||
q = q.Where("camera_id = ?", cameraId)
|
||||
}
|
||||
@@ -77,6 +77,9 @@ func playClip(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
clipId := c.Param("clipId")
|
||||
if !requireObjectAccess(c, db, "video_clip", clipId) {
|
||||
return
|
||||
}
|
||||
var clip model.VideoClip
|
||||
if db.Where("id = ?", clipId).First(&clip).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "video clip not found"})
|
||||
|
||||
@@ -42,7 +42,7 @@ func RegisterVideoRecordRoutes(rg *gin.RouterGroup, db *gorm.DB, media *service.
|
||||
readPerm := middleware.RequirePermission(db, "video:read")
|
||||
rg.POST("/video/cameras/:id/record/start", recordPerm, startRecording(db, media, cfg))
|
||||
rg.POST("/video/cameras/:id/record/stop", recordPerm, stopRecording(db, media, cfg))
|
||||
rg.GET("/video/recordings/active", readPerm, listActiveRecordings(cfg))
|
||||
rg.GET("/video/recordings/active", readPerm, listActiveRecordings(db, cfg))
|
||||
rg.POST("/video/recordings/internal/end", endRecordingInternal(media, cfg)) // 白名单接口,无需权限
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ func RegisterVideoRecordRoutes(rg *gin.RouterGroup, db *gorm.DB, media *service.
|
||||
func startRecording(db *gorm.DB, media *service.MediaService, cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
cameraId := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "camera", cameraId) {
|
||||
return
|
||||
}
|
||||
var camera model.Camera
|
||||
if db.Where("id = ?", cameraId).First(&camera).Error != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "camera not found"})
|
||||
@@ -129,6 +132,9 @@ func startRecording(db *gorm.DB, media *service.MediaService, cfg *config.Config
|
||||
func stopRecording(db *gorm.DB, media *service.MediaService, cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
cameraId := c.Param("id")
|
||||
if !requireObjectAccess(c, db, "camera", cameraId) {
|
||||
return
|
||||
}
|
||||
|
||||
activeMu.Lock()
|
||||
rec, ok := activeRecordings[cameraId]
|
||||
@@ -217,7 +223,7 @@ func endRecordingInternal(media *service.MediaService, cfg *config.Config) gin.H
|
||||
|
||||
// listActiveRecordings 返回活跃录制列表
|
||||
// 优先以 recorder-go 服务的实际状态为准(Go 后端重启后内存 map 会丢失,但 recorder-go 仍在录制)
|
||||
func listActiveRecordings(cfg *config.Config) gin.HandlerFunc {
|
||||
func listActiveRecordings(db *gorm.DB, cfg *config.Config) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 1. 查询 recorder-go 实际录制状态
|
||||
recorderBase := strings.TrimSuffix(cfg.RecorderAPIBase, "/")
|
||||
@@ -270,7 +276,7 @@ func listActiveRecordings(cfg *config.Config) gin.HandlerFunc {
|
||||
}
|
||||
activeMu.Unlock()
|
||||
}
|
||||
c.JSON(http.StatusOK, list)
|
||||
c.JSON(http.StatusOK, filterAccessibleRecordings(db, c, list))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -283,6 +289,23 @@ func listActiveRecordings(cfg *config.Config) gin.HandlerFunc {
|
||||
list = append(list, rec)
|
||||
}
|
||||
activeMu.Unlock()
|
||||
c.JSON(http.StatusOK, list)
|
||||
c.JSON(http.StatusOK, filterAccessibleRecordings(db, c, list))
|
||||
}
|
||||
}
|
||||
|
||||
func filterAccessibleRecordings(db *gorm.DB, c *gin.Context, list []*ActiveRecording) []*ActiveRecording {
|
||||
if hasGlobalAccess(c) {
|
||||
return list
|
||||
}
|
||||
filtered := make([]*ActiveRecording, 0, len(list))
|
||||
for _, rec := range list {
|
||||
var camera model.Camera
|
||||
if db.Where("id = ?", rec.CameraID).First(&camera).Error != nil {
|
||||
continue
|
||||
}
|
||||
if canAccessRoom(db, c, camera.RoomID) {
|
||||
filtered = append(filtered, rec)
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user