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);
+ }
+}