diff --git a/IOT_API/Controllers/Asset/EquipmentController.cs b/IOT_API/Controllers/Asset/EquipmentController.cs index fbdd3f0..5447a45 100644 --- a/IOT_API/Controllers/Asset/EquipmentController.cs +++ b/IOT_API/Controllers/Asset/EquipmentController.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Mvc; using Model; +using Model.Dto.Asset; using Model.Entity.Asset; +using Model.Mapper; using Service.Interface; using SqlSugar; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; namespace IOT_API.Controllers.Asset @@ -11,16 +14,21 @@ namespace IOT_API.Controllers.Asset /// /// 资产管理 - 设备台账控制器 /// - [Route("api/Equipment")] //资产管理模块下的设备管理接口 [ApiController] - [Route("api/asset/equipment")] + [Route("api/asset/equipment")] //资产管理模块下的设备管理接口 public class EquipmentController : ControllerBase { private readonly IEquipmentService _equipmentService; + private readonly IEquipmentAttachmentService _attachmentService; + private readonly IWebHostEnvironment _webHostEnvironment; - public EquipmentController(IEquipmentService equipmentService) + public EquipmentController(IEquipmentService equipmentService, + IEquipmentAttachmentService attachmentService, + IWebHostEnvironment webHostEnvironment) { _equipmentService = equipmentService; + _attachmentService = attachmentService; + _webHostEnvironment = webHostEnvironment; } /// @@ -32,22 +40,27 @@ namespace IOT_API.Controllers.Asset /// 设备分类Id(0表示不过滤) /// 设备状态(不传表示不过滤) [HttpGet("list")] - public async Task>> GetList(int pageIndex = 1, int pageSize = 10, + public async Task>> GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, long categoryId = 0, EquipmentStatusEnum? status = null) { RefAsync total = 0; var result = await _equipmentService.GetPagedAsync(pageIndex, pageSize, total, keyword, categoryId, status); Response.Headers["X-Total-Count"] = total.Value.ToString(); - return result; + return result.IsSuccess + ? Result>.Success(result.Data.ToDtoList()) + : Result>.Error(result.Msg); } /// /// 查询全部设备(不分页,供下拉选择等场景使用) /// [HttpGet("all")] - public async Task>> GetAll() + public async Task>> GetAll() { - return await _equipmentService.GetAllAsync(); + var result = await _equipmentService.GetAllAsync(); + return result.IsSuccess + ? Result>.Success(result.Data.ToDtoList()) + : Result>.Error(result.Msg); } /// @@ -55,29 +68,33 @@ namespace IOT_API.Controllers.Asset /// /// 设备主键 Id [HttpGet("{id}")] - public async Task> GetById(long id) + public async Task> GetById(long id) { - return await _equipmentService.GetByIdAsync(id); + var res = await _equipmentService.GetByIdAsync(id); + return res.IsSuccess + ? Result.Success(res.Data.ToDto()) + : Result.Error(res.Msg); } /// /// 新增设备 /// - /// 设备实体 + /// 设备 DTO(IsDel/CreateTime 等服务端字段不接受入参) [HttpPost] - public async Task> Add([FromBody] EquipmentEntity entity) + public async Task> Add([FromBody] EquipmentDto dto) { - return await _equipmentService.InsertAsync(entity); + return await _equipmentService.InsertAsync(dto.ToEntity()); } /// /// 修改设备 /// - /// 设备主键 Id - /// 设备实体 + /// 设备主键 Id(以路由参数为准,忽略请求体中的 Id) + /// 设备 DTO(IsDel/CreateTime 等服务端字段不接受入参) [HttpPut("{id}")] - public async Task> Update(long id, [FromBody] EquipmentEntity entity) + public async Task> Update(long id, [FromBody] EquipmentDto dto) { + var entity = dto.ToEntity(); entity.Id = id; return await _equipmentService.UpdateAsync(entity); } @@ -96,11 +113,11 @@ namespace IOT_API.Controllers.Asset /// 变更设备状态(同步记录状态变更历史) /// /// 设备主键 Id - /// 状态变更请求 + /// 状态变更请求(目标状态/操作人/备注) [HttpPost("{id}/status")] - public async Task> ChangeStatus(long id, [FromBody] ChangeStatusRequest request) + public async Task> ChangeStatus(long id, [FromBody] EquipmentStatusChangeDto dto) { - return await _equipmentService.ChangeStatusAsync(id, request.Status, request.Operator, request.Remark); + return await _equipmentService.ChangeStatusAsync(id, dto.Status, dto.Operator, dto.Remark); } /// @@ -108,30 +125,65 @@ namespace IOT_API.Controllers.Asset /// /// 设备主键 Id [HttpGet("{id}/status-records")] - public async Task>> GetStatusRecords(long id) + public async Task>> GetStatusRecords(long id) { - return await _equipmentService.GetStatusRecordsAsync(id); + var res = await _equipmentService.GetStatusRecordsAsync(id); + return res.IsSuccess + ? Result>.Success(res.Data.ToDtoList()) + : Result>.Error(res.Msg); + } + + /// + /// 查询设备附件列表(资产附件管理) + /// + /// 设备主键 Id + [HttpGet("{id}/attachments")] + public async Task>> GetAttachments(long id) + { + var res = await _attachmentService.GetByEquipmentAsync(id); + return res.IsSuccess + ? Result>.Success(res.Data.ToDtoList()) + : Result>.Error(res.Msg); + } + + /// + /// 上传设备附件(multipart/form-data,文件字段名 file) + /// + /// 设备主键 Id + /// 上传的文件 + /// 文件类型(1使用说明书/2合格证明/3校准证书/4保养卡/5合同扫描件/6验收照片/99其他) + /// 上传人 + /// 备注 + [HttpPost("{id}/attachments")] + [RequestSizeLimit(50 * 1024 * 1024)] + public async Task> UploadAttachment(long id, IFormFile file, + [FromForm] AttachmentTypeEnum fileType = AttachmentTypeEnum.Other, + [FromForm] string? uploader = null, [FromForm] string? remark = null) + { + if (file == null || file.Length == 0) + { + return Result.Error("请选择要上传的文件"); + } + + // wwwroot 不存在时退回运行目录下的 wwwroot(上传时会自动创建) + string webRootPath = _webHostEnvironment.WebRootPath + ?? Path.Combine(_webHostEnvironment.ContentRootPath, "wwwroot"); + + using var stream = file.OpenReadStream(); + var res = await _attachmentService.UploadAsync(id, stream, file.FileName, file.Length, fileType, uploader, remark, webRootPath); + return res.IsSuccess + ? Result.Success(res.Data.ToDto()) + : Result.Error(res.Msg); + } + + /// + /// 删除设备附件 + /// + /// 附件主键 Id + [HttpDelete("attachments/{attachmentId}")] + public async Task> DeleteAttachment(long attachmentId) + { + return await _attachmentService.DeleteAsync(attachmentId); } } - - /// - /// 状态变更请求体 - /// - public class ChangeStatusRequest - { - /// - /// 目标状态 - /// - public EquipmentStatusEnum Status { get; set; } - - /// - /// 操作人 - /// - public string? Operator { get; set; } - - /// - /// 变更原因/备注 - /// - public string? Remark { get; set; } - } } diff --git a/IOT_API/IOT_API.csproj b/IOT_API/IOT_API.csproj index fe3f0a4..7a92342 100644 --- a/IOT_API/IOT_API.csproj +++ b/IOT_API/IOT_API.csproj @@ -17,4 +17,8 @@ + + + + diff --git a/IOT_API/Program.cs b/IOT_API/Program.cs index f039c2f..b6aa79c 100644 --- a/IOT_API/Program.cs +++ b/IOT_API/Program.cs @@ -38,6 +38,7 @@ namespace WebAPI options.JsonSerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString; options.JsonSerializerOptions.PropertyNamingPolicy = null; + }); // 自动注册业务服务(Service.Interface -> Service.Implement) builder.Services.AddBusinessServices(); @@ -63,6 +64,9 @@ namespace WebAPI app.UseAuthorization(); + // 静态文件:设备附件上传后通过 /uploads/... 访问(存储于 wwwroot) + app.UseStaticFiles(); + app.MapControllers(); app.Run(); diff --git a/Model/Dto/Asset/EquipmentAttachmentDto.cs b/Model/Dto/Asset/EquipmentAttachmentDto.cs index 3fa65af..94c56e9 100644 --- a/Model/Dto/Asset/EquipmentAttachmentDto.cs +++ b/Model/Dto/Asset/EquipmentAttachmentDto.cs @@ -16,8 +16,8 @@ namespace Model.Dto.Asset /// 创建时间 public DateTime? CreateTime { get; set; } - /// 设备Id(关联 EquipmentEntity.Id) - public long EquipmentId { get; set; } + /// 设备Id(关联 EquipmentEntity.Id,雪花 Id 转 string 防精度丢失) + public string EquipmentId { get; set; } /// 文件名称 public string? FileName { get; set; } diff --git a/Model/Dto/Asset/EquipmentCategoryDto.cs b/Model/Dto/Asset/EquipmentCategoryDto.cs index 47aeb69..90cc22d 100644 --- a/Model/Dto/Asset/EquipmentCategoryDto.cs +++ b/Model/Dto/Asset/EquipmentCategoryDto.cs @@ -14,8 +14,8 @@ namespace Model.Dto.Asset /// 创建时间 public DateTime? CreateTime { get; set; } - /// 父级分类Id(0表示顶级) - public long ParentId { get; set; } + /// 父级分类Id(0表示顶级,雪花 Id 转 string 防精度丢失) + public string ParentId { get; set; } /// 分类名称 public string? Name { get; set; } diff --git a/Model/Dto/Asset/EquipmentDto.cs b/Model/Dto/Asset/EquipmentDto.cs index 96be2ad..ee1adcb 100644 --- a/Model/Dto/Asset/EquipmentDto.cs +++ b/Model/Dto/Asset/EquipmentDto.cs @@ -7,8 +7,8 @@ namespace Model.Dto.Asset /// public class EquipmentDto { - /// 主键 Id(long → string) - public string Id { get; set; } + /// 主键 Id(long → string;入参可空,新增时无需传递) + public string? Id { get; set; } /// 删除状态(0、未删除;1、已删除) public byte IsDel { get; set; } @@ -22,8 +22,8 @@ namespace Model.Dto.Asset /// 设备名称 public string? Name { get; set; } - /// 设备分类Id(关联 EquipmentCategoryEntity.Id) - public long CategoryId { get; set; } + /// 设备分类Id(关联 EquipmentCategoryEntity.Id,雪花 Id 转 string 防精度丢失;可空,未分类时为 "0" 或不传) + public string? CategoryId { get; set; } /// 设备类型 public string? Type { get; set; } diff --git a/Model/Dto/Asset/EquipmentStatusChangeDto.cs b/Model/Dto/Asset/EquipmentStatusChangeDto.cs new file mode 100644 index 0000000..f107891 --- /dev/null +++ b/Model/Dto/Asset/EquipmentStatusChangeDto.cs @@ -0,0 +1,25 @@ +using Model.Entity.Asset; + +namespace Model.Dto.Asset +{ + /// + /// 设备状态变更请求 DTO(POST api/asset/equipment/{id}/status 入参) + /// + public class EquipmentStatusChangeDto + { + /// + /// 目标状态(必填) + /// + public EquipmentStatusEnum Status { get; set; } + + /// + /// 操作人 + /// + public string? Operator { get; set; } + + /// + /// 变更原因/备注 + /// + public string? Remark { get; set; } + } +} diff --git a/Model/Dto/Asset/EquipmentStatusRecordDto.cs b/Model/Dto/Asset/EquipmentStatusRecordDto.cs index 35da927..35662c9 100644 --- a/Model/Dto/Asset/EquipmentStatusRecordDto.cs +++ b/Model/Dto/Asset/EquipmentStatusRecordDto.cs @@ -16,8 +16,8 @@ namespace Model.Dto.Asset /// 创建时间 public DateTime? CreateTime { get; set; } - /// 设备Id(关联 EquipmentEntity.Id) - public long EquipmentId { get; set; } + /// 设备Id(关联 EquipmentEntity.Id,雪花 Id 转 string 防精度丢失) + public string EquipmentId { get; set; } /// 变更前状态 public EquipmentStatusEnum FromStatus { get; set; } diff --git a/Model/Entity/Asset/EquipmentAttachmentEntity.cs b/Model/Entity/Asset/EquipmentAttachmentEntity.cs index e8a528f..000a4fe 100644 --- a/Model/Entity/Asset/EquipmentAttachmentEntity.cs +++ b/Model/Entity/Asset/EquipmentAttachmentEntity.cs @@ -15,7 +15,7 @@ namespace Model.Entity.Asset /// /// 文件名称 /// - [SugarColumn(Length = 200)] + [SugarColumn(Length = 200, IsNullable = true)] public string? FileName { get; set; } /// @@ -26,13 +26,13 @@ namespace Model.Entity.Asset /// /// 文件扩展名(如 .pdf、.jpg) /// - [SugarColumn(Length = 20)] + [SugarColumn(Length = 20, IsNullable = true)] public string? FileExt { get; set; } /// /// 文件存储地址(上传后存储的相对路径) /// - [SugarColumn(Length = 200)] + [SugarColumn(Length = 200, IsNullable = true)] public string? FileUrl { get; set; } /// @@ -43,13 +43,13 @@ namespace Model.Entity.Asset /// /// 上传人 /// - [SugarColumn(Length = 50)] + [SugarColumn(Length = 50, IsNullable = true)] public string? Uploader { get; set; } /// /// 备注 /// - [SugarColumn(Length = 500)] + [SugarColumn(Length = 500, IsNullable = true)] public string? Remark { get; set; } } } diff --git a/Model/Entity/Asset/EquipmentCategoryEntity.cs b/Model/Entity/Asset/EquipmentCategoryEntity.cs index 9c7aaab..c34105b 100644 --- a/Model/Entity/Asset/EquipmentCategoryEntity.cs +++ b/Model/Entity/Asset/EquipmentCategoryEntity.cs @@ -15,13 +15,13 @@ namespace Model.Entity.Asset /// /// 分类名称 /// - [SugarColumn(Length = 100)] + [SugarColumn(Length = 100, IsNullable = true)] public string? Name { get; set; } /// /// 分类编码 /// - [SugarColumn(Length = 50)] + [SugarColumn(Length = 50, IsNullable = true)] public string? Code { get; set; } /// @@ -37,7 +37,7 @@ namespace Model.Entity.Asset /// /// 备注 /// - [SugarColumn(Length = 500)] + [SugarColumn(Length = 500, IsNullable = true)] public string? Remark { get; set; } } } diff --git a/Model/Entity/Asset/EquipmentEntity.cs b/Model/Entity/Asset/EquipmentEntity.cs index 0c96c3f..c54eff4 100644 --- a/Model/Entity/Asset/EquipmentEntity.cs +++ b/Model/Entity/Asset/EquipmentEntity.cs @@ -11,13 +11,13 @@ namespace Model.Entity.Asset /// /// 设备编号(唯一) /// - [SugarColumn(Length = 50)] + [SugarColumn(Length = 50, IsNullable = true)] public string? Code { get; set; } /// /// 设备名称 /// - [SugarColumn(Length = 100)] + [SugarColumn(Length = 100, IsNullable = true)] public string? Name { get; set; } /// @@ -28,55 +28,55 @@ namespace Model.Entity.Asset /// /// 设备类型(实验室/楼层/部门/位置/产品/自定义等分类维度下的类型名称,冗余便于展示) /// - [SugarColumn(Length = 100)] + [SugarColumn(Length = 100, IsNullable = true)] public string? Type { get; set; } /// /// 设备品牌 /// - [SugarColumn(Length = 100)] + [SugarColumn(Length = 100, IsNullable = true)] public string? Brand { get; set; } /// /// 设备型号 /// - [SugarColumn(Length = 100)] + [SugarColumn(Length = 100, IsNullable = true)] public string? Model { get; set; } /// /// 规格参数 /// - [SugarColumn(Length = 200)] + [SugarColumn(Length = 200, IsNullable = true)] public string? Specifications { get; set; } /// /// 制造商(设备制造商) /// - [SugarColumn(Length = 100)] + [SugarColumn(Length = 100, IsNullable = true)] public string? Manufacturer { get; set; } /// /// 供应商(资产供应商) /// - [SugarColumn(Length = 100)] + [SugarColumn(Length = 100, IsNullable = true)] public string? Supplier { get; set; } /// /// 设备图片地址(图片上传后存储的相对路径) /// - [SugarColumn(Length = 200)] + [SugarColumn(Length = 200, IsNullable = true)] public string? ImageUrl { get; set; } /// /// 二维码编号(一物一码) /// - [SugarColumn(Length = 50)] + [SugarColumn(Length = 50, IsNullable = true)] public string? QrCode { get; set; } /// /// 设备描述 /// - [SugarColumn(ColumnDataType = "text")] + [SugarColumn(ColumnDataType = "text", IsNullable = true)] public string? Description { get; set; } #endregion @@ -84,31 +84,31 @@ namespace Model.Entity.Asset /// /// 使用部门名称(资产使用部门) /// - [SugarColumn(Length = 100)] + [SugarColumn(Length = 100, IsNullable = true)] public string? Department { get; set; } /// /// 存放位置(实验室/楼层/房间等位置信息) /// - [SugarColumn(Length = 200)] + [SugarColumn(Length = 200, IsNullable = true)] public string? Location { get; set; } /// /// 责任人(资产责任人) /// - [SugarColumn(Length = 50)] + [SugarColumn(Length = 50, IsNullable = true)] public string? ResponsiblePerson { get; set; } /// /// 责任人联系电话(资产责任人电话) /// - [SugarColumn(Length = 30)] + [SugarColumn(Length = 30, IsNullable = true)] public string? ContactPhone { get; set; } /// /// 保管人 /// - [SugarColumn(Length = 50)] + [SugarColumn(Length = 50, IsNullable = true)] public string? Custodian { get; set; } #endregion @@ -116,17 +116,19 @@ namespace Model.Entity.Asset /// /// 购置日期 /// + [SugarColumn(IsNullable = true)] public DateTime? PurchaseDate { get; set; } /// /// 购置价格(资产购置价格) /// - [SugarColumn(DecimalDigits = 2, Length = 18)] + [SugarColumn(DecimalDigits = 2, Length = 18, IsNullable = true)] public decimal? PurchasePrice { get; set; } /// /// 保修到期日期(资产保修到期) /// + [SugarColumn(IsNullable = true)] public DateTime? WarrantyExpireDate { get; set; } #endregion @@ -134,21 +136,25 @@ namespace Model.Entity.Asset /// /// 验收日期(资产验收日期) /// + [SugarColumn(IsNullable = true)] public DateTime? AcceptanceDate { get; set; } /// /// 启用日期(资产启用日期) /// + [SugarColumn(IsNullable = true)] public DateTime? EnableDate { get; set; } /// /// 使用年限(资产使用年限,单位:年) /// + [SugarColumn(IsNullable = true)] public int? ServiceLifeYears { get; set; } /// /// 报废日期(资产报废日期) /// + [SugarColumn(IsNullable = true)] public DateTime? ScrapDate { get; set; } #endregion @@ -168,21 +174,25 @@ namespace Model.Entity.Asset /// /// 上次校准日期(校准日期) /// + [SugarColumn(IsNullable = true)] public DateTime? LastCalibrationDate { get; set; } /// /// 下次校准到期日期(用于判断是否超期) /// + [SugarColumn(IsNullable = true)] public DateTime? NextCalibrationDate { get; set; } /// /// 检定日期 /// + [SugarColumn(IsNullable = true)] public DateTime? InspectionDate { get; set; } /// /// 下次检定到期日期(用于判断是否超期) /// + [SugarColumn(IsNullable = true)] public DateTime? NextInspectionDate { get; set; } /// @@ -193,11 +203,13 @@ namespace Model.Entity.Asset /// /// 上次保养日期(保养日期) /// + [SugarColumn(IsNullable = true)] public DateTime? LastMaintenanceDate { get; set; } /// /// 下次保养到期日期(保养周期到期,用于判断是否超期) /// + [SugarColumn(IsNullable = true)] public DateTime? NextMaintenanceDate { get; set; } #endregion @@ -205,24 +217,25 @@ namespace Model.Entity.Asset /// /// 备注 /// - [SugarColumn(Length = 500)] + [SugarColumn(Length = 500, IsNullable = true)] public string? Remark { get; set; } /// /// 创建人 /// - [SugarColumn(Length = 50)] + [SugarColumn(Length = 50, IsNullable = true)] public string? CreateBy { get; set; } /// /// 更新人(修改人) /// - [SugarColumn(Length = 50)] + [SugarColumn(Length = 50, IsNullable = true)] public string? UpdateBy { get; set; } /// /// 更新时间(修改时间) /// + [SugarColumn(IsNullable = true)] public DateTime? UpdateTime { get; set; } #endregion } diff --git a/Model/Entity/Asset/EquipmentStatusRecordEntity.cs b/Model/Entity/Asset/EquipmentStatusRecordEntity.cs index f752229..a6065c5 100644 --- a/Model/Entity/Asset/EquipmentStatusRecordEntity.cs +++ b/Model/Entity/Asset/EquipmentStatusRecordEntity.cs @@ -30,13 +30,13 @@ namespace Model.Entity.Asset /// /// 操作人 /// - [SugarColumn(Length = 50)] + [SugarColumn(Length = 50, IsNullable = true)] public string? Operator { get; set; } /// /// 变更原因/备注 /// - [SugarColumn(Length = 500)] + [SugarColumn(Length = 500, IsNullable = true)] public string? Remark { get; set; } } } diff --git a/Model/Mapper/EntityMapper.cs b/Model/Mapper/EntityMapper.cs index ea077dc..7ae9310 100644 --- a/Model/Mapper/EntityMapper.cs +++ b/Model/Mapper/EntityMapper.cs @@ -26,7 +26,7 @@ namespace Model.Mapper CreateTime = entity.CreateTime, Code = entity.Code, Name = entity.Name, - CategoryId = entity.CategoryId, + CategoryId = entity.CategoryId.ToString(), Type = entity.Type, Brand = entity.Brand, Model = entity.Model, @@ -71,6 +71,54 @@ namespace Model.Mapper { return entities?.Select(e => e.ToDto()).ToList() ?? new List(); } + + /// + /// EquipmentDto → EquipmentEntity(入参映射,用于新增/修改) + /// 安全约定:IsDel/CreateTime/UpdateTime 等服务端管控字段刻意不映射,防止过度绑定(over-binding); + /// Id 解析失败时为 0(新增时由雪花算法自动生成) + /// + public static EquipmentEntity ToEntity(this EquipmentDto dto) + { + if (dto == null) return null; + return new EquipmentEntity + { + Id = long.TryParse(dto.Id, out var id) ? id : 0, + Code = dto.Code, + Name = dto.Name, + CategoryId = long.TryParse(dto.CategoryId, out var categoryId) ? categoryId : 0, + Type = dto.Type, + Brand = dto.Brand, + Model = dto.Model, + Specifications = dto.Specifications, + Manufacturer = dto.Manufacturer, + Supplier = dto.Supplier, + ImageUrl = dto.ImageUrl, + QrCode = dto.QrCode, + Description = dto.Description, + Department = dto.Department, + Location = dto.Location, + ResponsiblePerson = dto.ResponsiblePerson, + ContactPhone = dto.ContactPhone, + Custodian = dto.Custodian, + PurchaseDate = dto.PurchaseDate, + PurchasePrice = dto.PurchasePrice, + WarrantyExpireDate = dto.WarrantyExpireDate, + AcceptanceDate = dto.AcceptanceDate, + EnableDate = dto.EnableDate, + ServiceLifeYears = dto.ServiceLifeYears, + ScrapDate = dto.ScrapDate, + Status = dto.Status, + CalibrationStatus = dto.CalibrationStatus, + LastCalibrationDate = dto.LastCalibrationDate, + NextCalibrationDate = dto.NextCalibrationDate, + InspectionDate = dto.InspectionDate, + NextInspectionDate = dto.NextInspectionDate, + MaintenanceStatus = dto.MaintenanceStatus, + LastMaintenanceDate = dto.LastMaintenanceDate, + NextMaintenanceDate = dto.NextMaintenanceDate, + Remark = dto.Remark + }; + } #endregion #region 设备附件 @@ -85,7 +133,7 @@ namespace Model.Mapper Id = entity.Id.ToString(), IsDel = entity.IsDel, CreateTime = entity.CreateTime, - EquipmentId = entity.EquipmentId, + EquipmentId = entity.EquipmentId.ToString(), FileName = entity.FileName, FileType = entity.FileType, FileExt = entity.FileExt, @@ -117,7 +165,7 @@ namespace Model.Mapper Id = entity.Id.ToString(), IsDel = entity.IsDel, CreateTime = entity.CreateTime, - ParentId = entity.ParentId, + ParentId = entity.ParentId.ToString(), Name = entity.Name, Code = entity.Code, Level = entity.Level, @@ -147,7 +195,7 @@ namespace Model.Mapper Id = entity.Id.ToString(), IsDel = entity.IsDel, CreateTime = entity.CreateTime, - EquipmentId = entity.EquipmentId, + EquipmentId = entity.EquipmentId.ToString(), FromStatus = entity.FromStatus, ToStatus = entity.ToStatus, ChangeTime = entity.ChangeTime, diff --git a/Service/Implement/Asset/EquipmentAttachmentService.cs b/Service/Implement/Asset/EquipmentAttachmentService.cs new file mode 100644 index 0000000..1574967 --- /dev/null +++ b/Service/Implement/Asset/EquipmentAttachmentService.cs @@ -0,0 +1,148 @@ +using Model; +using Model.Entity.Asset; +using ORM; +using Service.Interface; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Service.Implement +{ + /// + /// 设备附件服务实现(资产附件管理) + /// + public class EquipmentAttachmentService : IEquipmentAttachmentService + { + private readonly SqlSugarRepository _repository; + + public EquipmentAttachmentService(SqlSugarRepository repository) + { + _repository = repository; + } + + /// + /// 查询指定设备的附件列表(按上传时间倒序) + /// + public async Task>> GetByEquipmentAsync(long equipmentId) + { + try + { + var list = await _repository.Entities + .Where(x => x.EquipmentId == equipmentId && x.IsDel == 0) + .OrderBy(x => x.CreateTime, OrderByType.Desc) + .ToListAsync(); + return Result>.Success(list); + } + catch (Exception ex) + { + return Result>.Error("查询附件列表失败", ex); + } + } + + /// + /// 上传附件(保存文件到 uploads 目录并写入附件记录) + /// + public async Task> UploadAsync(long equipmentId, Stream stream, string originalFileName, long fileSize, + AttachmentTypeEnum fileType, string? uploader, string? remark, string webRootPath) + { + if (equipmentId <= 0) + { + return Result.Error("设备 Id 无效"); + } + if (stream == null || fileSize <= 0) + { + return Result.Error("上传文件为空"); + } + + try + { + // 按设备分目录存储:uploads/equipment/{equipmentId}/ + string relativeDir = Path.Combine("uploads", "equipment", equipmentId.ToString()); + string physicalDir = Path.Combine(webRootPath, relativeDir); + if (!Directory.Exists(physicalDir)) + { + Directory.CreateDirectory(physicalDir); + } + + // 存储文件名:时间戳 + 原扩展名,避免重名与中文路径问题 + string ext = Path.GetExtension(originalFileName); + string storedName = $"{DateTime.Now:yyyyMMddHHmmssfff}{ext}"; + string physicalPath = Path.Combine(physicalDir, storedName); + + using (var fileStream = new FileStream(physicalPath, FileMode.Create)) + { + await stream.CopyToAsync(fileStream); + } + + var entity = new EquipmentAttachmentEntity + { + EquipmentId = equipmentId, + FileName = originalFileName, + FileType = fileType, + FileExt = ext, + FileUrl = $"/{relativeDir.Replace('\\', '/')}/{storedName}", + FileSize = fileSize, + Uploader = uploader, + Remark = remark, + CreateTime = DateTime.Now + }; + + await _repository.Context.Insertable(entity).ExecuteCommandAsync(); + return Result.Success(entity); + } + catch (Exception ex) + { + return Result.Error("上传附件失败", ex); + } + } + + /// + /// 删除附件(软删除记录并删除物理文件) + /// + public async Task> DeleteAsync(long id) + { + if (id <= 0) + { + return Result.Error("附件 Id 无效"); + } + try + { + var attachment = await _repository.Entities + .Where(x => x.Id == id && x.IsDel == 0) + .FirstAsync(); + if (attachment == null) + { + return Result.Error("附件不存在或已被删除"); + } + + var result = await _repository.Context.Updateable() + .SetColumns(x => x.IsDel == 1) + .Where(x => x.Id == id) + .ExecuteCommandAsync(); + + // 删除物理文件(失败不影响记录删除结果) + try + { + string webRoot = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot"); + string physicalPath = Path.Combine(webRoot, (attachment.FileUrl ?? string.Empty).TrimStart('/').Replace('/', Path.DirectorySeparatorChar)); + if (File.Exists(physicalPath)) + { + File.Delete(physicalPath); + } + } + catch + { + // 物理文件删除失败忽略 + } + + return Result.Success(result > 0); + } + catch (Exception ex) + { + return Result.Error("删除附件失败", ex); + } + } + } +} diff --git a/Service/Interface/Asset/IEquipmentAttachmentService.cs b/Service/Interface/Asset/IEquipmentAttachmentService.cs new file mode 100644 index 0000000..5cc30a1 --- /dev/null +++ b/Service/Interface/Asset/IEquipmentAttachmentService.cs @@ -0,0 +1,43 @@ +using Model; +using Model.Entity.Asset; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Service.Interface +{ + /// + /// 设备附件服务接口(资产附件管理:分类存储、在线预览) + /// + public interface IEquipmentAttachmentService + { + /// + /// 查询指定设备的附件列表(按上传时间倒序) + /// + /// 设备 Id + /// 返回包含附件列表的 Result + Task>> GetByEquipmentAsync(long equipmentId); + + /// + /// 上传附件(保存文件到 uploads 目录并写入附件记录) + /// + /// 设备 Id + /// 文件流 + /// 原始文件名 + /// 文件大小(字节) + /// 文件类型(说明书/合格证/校准证书等) + /// 上传人 + /// 备注 + /// 站点静态文件根目录物理路径 + /// 返回新增附件记录的 Result + Task> UploadAsync(long equipmentId, Stream stream, string originalFileName, long fileSize, + AttachmentTypeEnum fileType, string? uploader, string? remark, string webRootPath); + + /// + /// 删除附件(软删除记录并删除物理文件) + /// + /// 附件 Id + /// 返回操作是否成功的 Result + Task> DeleteAsync(long id); + } +}