diff --git a/Src/Dlhis.Basic/WorkGrids/Controllers/EFenceController.cs b/Src/Dlhis.Basic/WorkGrids/Controllers/EFenceController.cs
new file mode 100644
index 00000000..2c45c3fc
--- /dev/null
+++ b/Src/Dlhis.Basic/WorkGrids/Controllers/EFenceController.cs
@@ -0,0 +1,95 @@
+using Dlhis.Basic.WorkGrids.Request;
+using Dlhis.Basic.WorkGrids.Response;
+using Dlhis.Basic.WorkGrids.Services;
+using Kurisu.AspNetCore.DataAccess.Dto;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Dlhis.Basic.WorkGrids.Controllers;
+
+///
+/// 电子围栏操作
+///
+[Authorize]
+[Route("api/[controller]")]
+[ApiController]
+public class EFenceController : ControllerBase
+{
+ private readonly IEFenceService _eFenceService;
+
+ public EFenceController(IEFenceService eFenceService)
+ {
+ _eFenceService = eFenceService;
+ }
+
+ ///
+ /// 保存电子围栏(新增或更新)
+ ///
+ [HttpPost("save")]
+ public async Task SaveAsync([FromBody] EFenceSaveRequest request)
+ {
+ await _eFenceService.SaveAsync(request);
+ }
+
+ ///
+ /// 获取电子围栏分页
+ ///
+ ///
+ ///
+ [HttpGet("page")]
+ public async Task> GetEFencePageAsync([FromQuery] EFencePageRequest request)
+ {
+ return await _eFenceService.GetEFencePageAsync(request);
+ }
+
+ ///
+ /// 获取围栏下的房间
+ ///
+ ///
+ ///
+ [HttpGet("rooms")]
+ public async Task> GetRoomsAsync(Guid eFenceCode)
+ {
+ return await _eFenceService.GetRoomsAsync(eFenceCode);
+ }
+
+ ///
+ /// 获取房间人员
+ ///
+ ///
+ ///
+ [HttpGet("room-persons")]
+ public async Task> GetRoomPersonsAsync(string roomId)
+ {
+ return await _eFenceService.GetRoomPersonsAsync(roomId);
+ }
+
+ ///
+ /// 向围栏添加设备
+ ///
+ [HttpPost("devices/add")]
+ public async Task AddDevicesAsync(EFenceDevicesRequest request)
+ {
+ await _eFenceService.AddDevicesAsync(request.FenceId, request.DeviceCodes);
+ }
+
+ ///
+ /// 从围栏移除设备
+ ///
+ [HttpPost("devices/remove")]
+ public async Task RemoveDevicesAsync(EFenceDevicesRequest request)
+ {
+ await _eFenceService.RemoveDevicesAsync(request.FenceId, request.DeviceCodes);
+ }
+
+ ///
+ /// 获取设备列表
+ ///
+ ///
+ ///
+ [HttpGet("devices")]
+ public async Task> GetDevicesByFenceIdAsync(long fenceId)
+ {
+ return await _eFenceService.GetDevicesByFenceIdAsync(fenceId);
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Basic/WorkGrids/Request/EFenceDevicesRequest.cs b/Src/Dlhis.Basic/WorkGrids/Request/EFenceDevicesRequest.cs
new file mode 100644
index 00000000..042e8138
--- /dev/null
+++ b/Src/Dlhis.Basic/WorkGrids/Request/EFenceDevicesRequest.cs
@@ -0,0 +1,12 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Dlhis.Basic.WorkGrids.Request;
+
+public class EFenceDevicesRequest
+{
+ [Range(1, long.MaxValue)]
+ public long FenceId { get; set; }
+
+ [MinLength(1)]
+ public List DeviceCodes { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Basic/WorkGrids/Request/EFencePageRequest.cs b/Src/Dlhis.Basic/WorkGrids/Request/EFencePageRequest.cs
new file mode 100644
index 00000000..33eb22e2
--- /dev/null
+++ b/Src/Dlhis.Basic/WorkGrids/Request/EFencePageRequest.cs
@@ -0,0 +1,8 @@
+using Kurisu.AspNetCore.DataAccess.Dto;
+
+namespace Dlhis.Basic.WorkGrids.Request;
+
+public class EFencePageRequest : PageDto
+{
+
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Basic/WorkGrids/Request/EFenceSaveRequest.cs b/Src/Dlhis.Basic/WorkGrids/Request/EFenceSaveRequest.cs
new file mode 100644
index 00000000..ec4edf77
--- /dev/null
+++ b/Src/Dlhis.Basic/WorkGrids/Request/EFenceSaveRequest.cs
@@ -0,0 +1,13 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Dlhis.Basic.WorkGrids.Request;
+
+public class EFenceSaveRequest
+{
+ public long? Id { get; set; }
+
+ [Required]
+ public string Name { get; set; }
+
+ public string Description { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Basic/WorkGrids/Response/EFenceDevicePageResponse.cs b/Src/Dlhis.Basic/WorkGrids/Response/EFenceDevicePageResponse.cs
new file mode 100644
index 00000000..7e200eba
--- /dev/null
+++ b/Src/Dlhis.Basic/WorkGrids/Response/EFenceDevicePageResponse.cs
@@ -0,0 +1,43 @@
+using Dlhis.Entity.Device.Enums;
+
+namespace Dlhis.Basic.WorkGrids.Response;
+
+public class EFenceDevicePageResponse
+{
+ public long Id { get; set; }
+
+ ///
+ /// 设备code
+ ///
+ public Guid Code { get; set; }
+
+ ///
+ /// 设备唯一码,IP/MAC/SNO
+ ///
+ public string UniqueNo { get; set; }
+
+ ///
+ /// 部署点
+ ///
+ public string DeployPlace { get; set; }
+
+ ///
+ /// 部署坐标
+ ///
+ public string DeployPosition { get; set; }
+
+ ///
+ /// 部署方向
+ ///
+ public DeviceDeployDirectionEnumType? DeployDirection { get; set; }
+
+ ///
+ /// 是否启用
+ ///
+ public bool Enable { get; set; }
+
+ ///
+ /// 厂区
+ ///
+ public string Factory { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Basic/WorkGrids/Response/EFenceDormitoryRoomPersonResponse.cs b/Src/Dlhis.Basic/WorkGrids/Response/EFenceDormitoryRoomPersonResponse.cs
new file mode 100644
index 00000000..2d105b35
--- /dev/null
+++ b/Src/Dlhis.Basic/WorkGrids/Response/EFenceDormitoryRoomPersonResponse.cs
@@ -0,0 +1,9 @@
+namespace Dlhis.Basic.WorkGrids.Response;
+
+public class EFenceDormitoryRoomPersonResponse
+{
+ public string RoomId { get; set; }
+ public string UserCode { get; set; }
+ public string UserName { get; set; }
+ public bool? In { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Basic/WorkGrids/Response/EFenceDormitoryRoomResponse.cs b/Src/Dlhis.Basic/WorkGrids/Response/EFenceDormitoryRoomResponse.cs
new file mode 100644
index 00000000..233eecaa
--- /dev/null
+++ b/Src/Dlhis.Basic/WorkGrids/Response/EFenceDormitoryRoomResponse.cs
@@ -0,0 +1,10 @@
+namespace Dlhis.Basic.WorkGrids.Response;
+
+public class EFenceDormitoryRoomResponse
+{
+ public Guid EFenceCode { get; set; }
+ public string RoomId { get; set; }
+ public string RoomNo { get; set; }
+ public string HotelId { get; set; }
+ public bool PowerSwitch { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Basic/WorkGrids/Response/EFencePageResponse.cs b/Src/Dlhis.Basic/WorkGrids/Response/EFencePageResponse.cs
new file mode 100644
index 00000000..3630748d
--- /dev/null
+++ b/Src/Dlhis.Basic/WorkGrids/Response/EFencePageResponse.cs
@@ -0,0 +1,23 @@
+namespace Dlhis.Basic.WorkGrids.Response;
+
+public class EFencePageResponse
+{
+ public long Id { get; set; }
+
+ public Guid Code { get; set; }
+
+ ///
+ /// 围栏名称
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// 描述/备注
+ ///
+ public string Description { get; set; }
+
+ ///
+ /// 设备数量
+ ///
+ public int DeviceCount { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Basic/WorkGrids/Services/IEFenceService.cs b/Src/Dlhis.Basic/WorkGrids/Services/IEFenceService.cs
new file mode 100644
index 00000000..6f683774
--- /dev/null
+++ b/Src/Dlhis.Basic/WorkGrids/Services/IEFenceService.cs
@@ -0,0 +1,22 @@
+using Dlhis.Basic.WorkGrids.Request;
+using Dlhis.Basic.WorkGrids.Response;
+using Kurisu.AspNetCore.DataAccess.Dto;
+
+namespace Dlhis.Basic.WorkGrids.Services;
+
+public interface IEFenceService : IScopeDependency
+{
+ Task SaveAsync(EFenceSaveRequest request);
+
+ Task> GetEFencePageAsync(EFencePageRequest request);
+
+ Task> GetRoomsAsync(Guid eFenceCode);
+
+ Task> GetRoomPersonsAsync(string roomId);
+
+ Task AddDevicesAsync(long fenceId, List deviceCodes);
+
+ Task RemoveDevicesAsync(long fenceId, List deviceCodes);
+
+ Task> GetDevicesByFenceIdAsync(long fenceId);
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Basic/WorkGrids/Services/Implements/EFenceService.cs b/Src/Dlhis.Basic/WorkGrids/Services/Implements/EFenceService.cs
new file mode 100644
index 00000000..a0630a04
--- /dev/null
+++ b/Src/Dlhis.Basic/WorkGrids/Services/Implements/EFenceService.cs
@@ -0,0 +1,143 @@
+using Dlhis.Basic.WorkGrids.Request;
+using Dlhis.Basic.WorkGrids.Response;
+using Dlhis.Entity.Basic.Entity;
+using Dlhis.Entity.Device.Entities;
+using Dlhis.Entity.Dormitory.Entity;
+using Kurisu.AspNetCore.DataAccess.Dto;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Extensions;
+using Kurisu.AspNetCore.DataAccess.SqlSugar.Services;
+using Kurisu.AspNetCore.Utils.Extensions;
+using Mapster;
+using SqlSugar;
+
+namespace Dlhis.Basic.WorkGrids.Services.Implements;
+
+public class EFenceService : IEFenceService
+{
+ private readonly IDbContext _dbContext;
+
+ public EFenceService(IDbContext dbContext)
+ {
+ _dbContext = dbContext;
+ }
+
+
+ public async Task SaveAsync(EFenceSaveRequest request)
+ {
+ if (!request.Id.HasValue)
+ {
+ var entity = request.Adapt();
+ entity.Code = Guid.NewGuid();
+ await _dbContext.InsertAsync(entity);
+ }
+ else
+ {
+ var entity = await _dbContext.Queryable().SingleAsync(x => x.Id == request.Id.Value);
+ request.Adapt(entity);
+ await _dbContext.UpdateAsync(entity);
+ }
+ }
+
+ public async Task> GetEFencePageAsync(EFencePageRequest request)
+ {
+ var page = await _dbContext.Queryable()
+ .WhereIF(request.Search.IsPresent(), x => x.Name.Contains(request.Search))
+ .OrderBy(x => x.Id, OrderByType.Desc)
+ .Select(x => new EFencePageResponse
+ {
+ Id = x.Id,
+ Code = x.Code,
+ Name = x.Name,
+ Description = x.Description,
+ DeviceCount = SqlFunc.Subqueryable().Where(p => p.EFenceCode == x.Code).Count(),
+ })
+ .ToPageListAsync(request);
+
+ return page;
+ }
+
+ public async Task> GetRoomsAsync(Guid eFenceCode)
+ {
+ return await _dbContext.Queryable()
+ .Where(x => x.EFenceCode == eFenceCode)
+ .Select()
+ .ToListAsync();
+ }
+
+ public async Task> GetRoomPersonsAsync(string roomId)
+ {
+ return await _dbContext.Queryable()
+ .Where(x => x.RoomId == roomId)
+ .Select()
+ .ToListAsync();
+ }
+
+ public async Task AddDevicesAsync(long fenceId, List deviceCodes)
+ {
+ //查询电子围栏
+ var fence = await _dbContext.Queryable().SingleAsync(f => f.Id == fenceId);
+ fence.ThrowIfNull("未找到电子围栏");
+
+ //查询设备
+ var devices = await _dbContext.Queryable()
+ .Where(d => deviceCodes.Contains(d.Code))
+ .Where(d => d.DeployDirection.HasValue)
+ .Where(d => SqlFunc.Subqueryable()
+ .Where(p => p.EFenceCode == fence.Code && p.DeviceCode == d.Code)
+ .NotAny() //不可重复分配
+ )
+ .ToListAsync();
+
+ if (devices.Any())
+ {
+ var eFenceDeviceRelationEntities = devices.Select(x => new EFenceDeviceRelationEntity
+ {
+ DeviceCode = x.Code,
+ EFenceCode = fence.Code
+ }).ToList();
+
+ await _dbContext.InsertAsync(eFenceDeviceRelationEntities);
+ }
+ }
+
+ public async Task RemoveDevicesAsync(long fenceId, List deviceCodes)
+ {
+ //查询电子围栏
+ var fence = await _dbContext.Queryable().SingleAsync(f => f.Id == fenceId);
+ fence.ThrowIfNull("未找到电子围栏");
+
+ //查询EFenceDeviceRelationEntity的设备
+ var relations = await _dbContext.Queryable()
+ .Where(r => r.EFenceCode == fence.Code && deviceCodes.Contains(r.DeviceCode))
+ .ToListAsync();
+
+ if (relations.Any())
+ {
+ await _dbContext.DeleteAsync(relations);
+ }
+ }
+
+ public async Task> GetDevicesByFenceIdAsync(long fenceId)
+ {
+ //查询电子围栏
+ var fence = await _dbContext.Queryable().SingleAsync(f => f.Id == fenceId);
+ fence.ThrowIfNull("未找到电子围栏");
+
+ var deviceCodes = await _dbContext.Queryable()
+ .Where(x => x.EFenceCode == fence.Code)
+ .Select(x => x.DeviceCode)
+ .ToListAsync();
+
+
+ var list = await _dbContext.Queryable()
+ .LeftJoin((d, dt) => d.TemplateCode == dt.Code)
+ .Where((d, dt) => deviceCodes.Contains(d.Code))
+ .Select((d, dt) => new EFenceDevicePageResponse
+ {
+ Factory = d.TenantId
+ }, true)
+ .ToListAsync();
+
+ return list;
+ }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Entity/Device/Entities/EFenceDeviceRelationEntity.cs b/Src/Dlhis.Entity/Device/Entities/EFenceDeviceRelationEntity.cs
new file mode 100644
index 00000000..a9b18c5c
--- /dev/null
+++ b/Src/Dlhis.Entity/Device/Entities/EFenceDeviceRelationEntity.cs
@@ -0,0 +1,27 @@
+using Kurisu.AspNetCore.DataAccess.Entity;
+using Kurisu.AspNetCore.DataAccess.SqlSugar;
+using SqlSugar;
+
+namespace Dlhis.Entity.Device.Entities;
+
+///
+/// 设备与电子围栏关系表
+///
+[SugarTable("ElectronicFenceDeviceRelationEntity")]
+[SugarIndex("idx_ElectronicFenceDeviceRelation_DeviceCode_EFenceCode", new[] { nameof(DeviceCode), nameof(EFenceCode) }, new[] { OrderByType.Asc, OrderByType.Asc }, false)]
+public class EFenceDeviceRelationEntity : SugarBaseEntity, ISoftDeleted
+{
+ ///
+ /// 设备ID
+ ///
+ [SugarColumn(IsNullable = false)]
+ public Guid DeviceCode { get; set; }
+
+ ///
+ /// 电子围栏ID
+ ///
+ [SugarColumn(IsNullable = false)]
+ public Guid EFenceCode { get; set; }
+
+ public bool IsDeleted { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Entity/Device/Entities/EFenceEntity.cs b/Src/Dlhis.Entity/Device/Entities/EFenceEntity.cs
new file mode 100644
index 00000000..36266fc0
--- /dev/null
+++ b/Src/Dlhis.Entity/Device/Entities/EFenceEntity.cs
@@ -0,0 +1,28 @@
+using Kurisu.AspNetCore.DataAccess.SqlSugar;
+using Mapster;
+using SqlSugar;
+
+namespace Dlhis.Entity.Device.Entities;
+
+///
+/// 电子围栏
+///
+[SugarTable("ElectronicFence")]
+public class EFenceEntity : SugarBaseEntity
+{
+ [AdaptIgnore(MemberSide.Source)]
+ [SugarColumn(IsNullable = false)]
+ public Guid Code { get; set; }
+
+ ///
+ /// 围栏名称
+ ///
+ [SugarColumn(IsNullable = false)]
+ public string Name { get; set; }
+
+ ///
+ /// 描述/备注
+ ///
+ [SugarColumn(IsNullable = true, Length = 1000)]
+ public string Description { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Entity/Dormitory/Entity/DormitoryRoomEntity.cs b/Src/Dlhis.Entity/Dormitory/Entity/DormitoryRoomEntity.cs
new file mode 100644
index 00000000..dad05f9e
--- /dev/null
+++ b/Src/Dlhis.Entity/Dormitory/Entity/DormitoryRoomEntity.cs
@@ -0,0 +1,14 @@
+using Kurisu.AspNetCore.DataAccess.SqlSugar;
+using SqlSugar;
+
+namespace Dlhis.Entity.Dormitory.Entity;
+
+[SugarTable(TableName = "DormitoryRoom")]
+public class DormitoryRoomEntity : SugarBaseEntity
+{
+ public Guid EFenceCode { get; set; }
+ public string RoomId { get; set; }
+ public string RoomNo { get; set; }
+ public string HotelId { get; set; }
+ public bool PowerSwitch { get; set; }
+}
\ No newline at end of file
diff --git a/Src/Dlhis.Entity/Dormitory/Entity/DormitoryRoomPersonEntity.cs b/Src/Dlhis.Entity/Dormitory/Entity/DormitoryRoomPersonEntity.cs
new file mode 100644
index 00000000..611fafb8
--- /dev/null
+++ b/Src/Dlhis.Entity/Dormitory/Entity/DormitoryRoomPersonEntity.cs
@@ -0,0 +1,15 @@
+using Kurisu.AspNetCore.DataAccess.Entity;
+using Kurisu.AspNetCore.DataAccess.SqlSugar;
+using SqlSugar;
+
+namespace Dlhis.Entity.Dormitory.Entity;
+
+[SugarTable(TableName = "DormitoryRoomPerson")]
+public class DormitoryRoomPersonEntity : SugarBaseEntity, ISoftDeleted
+{
+ public string RoomId { get; set; }
+ public string UserCode { get; set; }
+ public string UserName { get; set; }
+ public bool? In { get; set; }
+ public bool IsDeleted { get; set; }
+}
\ No newline at end of file