Compare commits

..
3 Commits
16 changed files with 423 additions and 85 deletions
+1
View File
@@ -366,3 +366,4 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/IOT_API/appsettings.json
/IOT_API/wwwroot
@@ -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
/// <summary>
/// 资产管理 - 设备台账控制器
/// </summary>
[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;
}
/// <summary>
@@ -32,22 +40,27 @@ namespace IOT_API.Controllers.Asset
/// <param name="categoryId">设备分类Id0表示不过滤)</param>
/// <param name="status">设备状态(不传表示不过滤)</param>
[HttpGet("list")]
public async Task<Result<List<EquipmentEntity>>> GetList(int pageIndex = 1, int pageSize = 10,
public async Task<Result<List<EquipmentDto>>> GetList(int pageIndex = 1, int pageSize = 10,
string? keyword = null, long categoryId = 0, EquipmentStatusEnum? status = null)
{
RefAsync<int> 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<List<EquipmentDto>>.Success(result.Data.ToDtoList())
: Result<List<EquipmentDto>>.Error(result.Msg);
}
/// <summary>
/// 查询全部设备(不分页,供下拉选择等场景使用)
/// </summary>
[HttpGet("all")]
public async Task<Result<List<EquipmentEntity>>> GetAll()
public async Task<Result<List<EquipmentDto>>> GetAll()
{
return await _equipmentService.GetAllAsync();
var result = await _equipmentService.GetAllAsync();
return result.IsSuccess
? Result<List<EquipmentDto>>.Success(result.Data.ToDtoList())
: Result<List<EquipmentDto>>.Error(result.Msg);
}
/// <summary>
@@ -55,29 +68,33 @@ namespace IOT_API.Controllers.Asset
/// </summary>
/// <param name="id">设备主键 Id</param>
[HttpGet("{id}")]
public async Task<Result<EquipmentEntity>> GetById(long id)
public async Task<Result<EquipmentDto>> GetById(long id)
{
return await _equipmentService.GetByIdAsync(id);
var res = await _equipmentService.GetByIdAsync(id);
return res.IsSuccess
? Result<EquipmentDto>.Success(res.Data.ToDto())
: Result<EquipmentDto>.Error(res.Msg);
}
/// <summary>
/// 新增设备
/// </summary>
/// <param name="entity">设备实体</param>
/// <param name="dto">设备 DTOIsDel/CreateTime 等服务端字段不接受入参)</param>
[HttpPost]
public async Task<Result<bool>> Add([FromBody] EquipmentEntity entity)
public async Task<Result<bool>> Add([FromBody] EquipmentDto dto)
{
return await _equipmentService.InsertAsync(entity);
return await _equipmentService.InsertAsync(dto.ToEntity());
}
/// <summary>
/// 修改设备
/// </summary>
/// <param name="id">设备主键 Id</param>
/// <param name="entity">设备实体</param>
/// <param name="id">设备主键 Id(以路由参数为准,忽略请求体中的 Id)</param>
/// <param name="dto">设备 DTOIsDel/CreateTime 等服务端字段不接受入参)</param>
[HttpPut("{id}")]
public async Task<Result<bool>> Update(long id, [FromBody] EquipmentEntity entity)
public async Task<Result<bool>> 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
/// 变更设备状态(同步记录状态变更历史)
/// </summary>
/// <param name="id">设备主键 Id</param>
/// <param name="request">状态变更请求</param>
/// <param name="dto">状态变更请求(目标状态/操作人/备注)</param>
[HttpPost("{id}/status")]
public async Task<Result<bool>> ChangeStatus(long id, [FromBody] ChangeStatusRequest request)
public async Task<Result<bool>> 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);
}
/// <summary>
@@ -108,30 +125,65 @@ namespace IOT_API.Controllers.Asset
/// </summary>
/// <param name="id">设备主键 Id</param>
[HttpGet("{id}/status-records")]
public async Task<Result<List<EquipmentStatusRecordEntity>>> GetStatusRecords(long id)
public async Task<Result<List<EquipmentStatusRecordDto>>> GetStatusRecords(long id)
{
return await _equipmentService.GetStatusRecordsAsync(id);
}
var res = await _equipmentService.GetStatusRecordsAsync(id);
return res.IsSuccess
? Result<List<EquipmentStatusRecordDto>>.Success(res.Data.ToDtoList())
: Result<List<EquipmentStatusRecordDto>>.Error(res.Msg);
}
/// <summary>
/// 状态变更请求体
/// 查询设备附件列表(资产附件管理)
/// </summary>
public class ChangeStatusRequest
/// <param name="id">设备主键 Id</param>
[HttpGet("{id}/attachments")]
public async Task<Result<List<EquipmentAttachmentDto>>> GetAttachments(long id)
{
/// <summary>
/// 目标状态
/// </summary>
public EquipmentStatusEnum Status { get; set; }
var res = await _attachmentService.GetByEquipmentAsync(id);
return res.IsSuccess
? Result<List<EquipmentAttachmentDto>>.Success(res.Data.ToDtoList())
: Result<List<EquipmentAttachmentDto>>.Error(res.Msg);
}
/// <summary>
/// 操作人
/// 上传设备附件(multipart/form-data,文件字段名 file
/// </summary>
public string? Operator { get; set; }
/// <param name="id">设备主键 Id</param>
/// <param name="file">上传的文件</param>
/// <param name="fileType">文件类型(1使用说明书/2合格证明/3校准证书/4保养卡/5合同扫描件/6验收照片/99其他)</param>
/// <param name="uploader">上传人</param>
/// <param name="remark">备注</param>
[HttpPost("{id}/attachments")]
[RequestSizeLimit(50 * 1024 * 1024)]
public async Task<Result<EquipmentAttachmentDto>> 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<EquipmentAttachmentDto>.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<EquipmentAttachmentDto>.Success(res.Data.ToDto())
: Result<EquipmentAttachmentDto>.Error(res.Msg);
}
/// <summary>
/// 变更原因/备注
/// 删除设备附件
/// </summary>
public string? Remark { get; set; }
/// <param name="attachmentId">附件主键 Id</param>
[HttpDelete("attachments/{attachmentId}")]
public async Task<Result<bool>> DeleteAttachment(long attachmentId)
{
return await _attachmentService.DeleteAsync(attachmentId);
}
}
}
+4
View File
@@ -17,4 +17,8 @@
<ProjectReference Include="..\Service\Service.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Converters\" />
</ItemGroup>
</Project>
+4
View File
@@ -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();
+2 -2
View File
@@ -16,8 +16,8 @@ namespace Model.Dto.Asset
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>设备Id(关联 EquipmentEntity.Id</summary>
public long EquipmentId { get; set; }
/// <summary>设备Id(关联 EquipmentEntity.Id,雪花 Id 转 string 防精度丢失</summary>
public string EquipmentId { get; set; }
/// <summary>文件名称</summary>
public string? FileName { get; set; }
+2 -2
View File
@@ -14,8 +14,8 @@ namespace Model.Dto.Asset
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>父级分类Id0表示顶级)</summary>
public long ParentId { get; set; }
/// <summary>父级分类Id0表示顶级,雪花 Id 转 string 防精度丢失</summary>
public string ParentId { get; set; }
/// <summary>分类名称</summary>
public string? Name { get; set; }
+4 -4
View File
@@ -7,8 +7,8 @@ namespace Model.Dto.Asset
/// </summary>
public class EquipmentDto
{
/// <summary>主键 Idlong → string</summary>
public string Id { get; set; }
/// <summary>主键 Idlong → string;入参可空,新增时无需传递</summary>
public string? Id { get; set; }
/// <summary>删除状态(0、未删除;1、已删除)</summary>
public byte IsDel { get; set; }
@@ -22,8 +22,8 @@ namespace Model.Dto.Asset
/// <summary>设备名称</summary>
public string? Name { get; set; }
/// <summary>设备分类Id(关联 EquipmentCategoryEntity.Id</summary>
public long CategoryId { get; set; }
/// <summary>设备分类Id(关联 EquipmentCategoryEntity.Id,雪花 Id 转 string 防精度丢失;可空,未分类时为 "0" 或不传</summary>
public string? CategoryId { get; set; }
/// <summary>设备类型</summary>
public string? Type { get; set; }
@@ -0,0 +1,25 @@
using Model.Entity.Asset;
namespace Model.Dto.Asset
{
/// <summary>
/// 设备状态变更请求 DTOPOST api/asset/equipment/{id}/status 入参)
/// </summary>
public class EquipmentStatusChangeDto
{
/// <summary>
/// 目标状态(必填)
/// </summary>
public EquipmentStatusEnum Status { get; set; }
/// <summary>
/// 操作人
/// </summary>
public string? Operator { get; set; }
/// <summary>
/// 变更原因/备注
/// </summary>
public string? Remark { get; set; }
}
}
+2 -2
View File
@@ -16,8 +16,8 @@ namespace Model.Dto.Asset
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>设备Id(关联 EquipmentEntity.Id</summary>
public long EquipmentId { get; set; }
/// <summary>设备Id(关联 EquipmentEntity.Id,雪花 Id 转 string 防精度丢失</summary>
public string EquipmentId { get; set; }
/// <summary>变更前状态</summary>
public EquipmentStatusEnum FromStatus { get; set; }
@@ -15,7 +15,7 @@ namespace Model.Entity.Asset
/// <summary>
/// 文件名称
/// </summary>
[SugarColumn(Length = 200)]
[SugarColumn(Length = 200, IsNullable = true)]
public string? FileName { get; set; }
/// <summary>
@@ -26,13 +26,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 文件扩展名(如 .pdf、.jpg
/// </summary>
[SugarColumn(Length = 20)]
[SugarColumn(Length = 20, IsNullable = true)]
public string? FileExt { get; set; }
/// <summary>
/// 文件存储地址(上传后存储的相对路径)
/// </summary>
[SugarColumn(Length = 200)]
[SugarColumn(Length = 200, IsNullable = true)]
public string? FileUrl { get; set; }
/// <summary>
@@ -43,13 +43,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 上传人
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? Uploader { get; set; }
/// <summary>
/// 备注
/// </summary>
[SugarColumn(Length = 500)]
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
@@ -15,13 +15,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 分类名称
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Name { get; set; }
/// <summary>
/// 分类编码
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? Code { get; set; }
/// <summary>
@@ -37,7 +37,7 @@ namespace Model.Entity.Asset
/// <summary>
/// 备注
/// </summary>
[SugarColumn(Length = 500)]
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
+33 -20
View File
@@ -11,13 +11,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 设备编号(唯一)
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? Code { get; set; }
/// <summary>
/// 设备名称
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Name { get; set; }
/// <summary>
@@ -28,55 +28,55 @@ namespace Model.Entity.Asset
/// <summary>
/// 设备类型(实验室/楼层/部门/位置/产品/自定义等分类维度下的类型名称,冗余便于展示)
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Type { get; set; }
/// <summary>
/// 设备品牌
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Brand { get; set; }
/// <summary>
/// 设备型号
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Model { get; set; }
/// <summary>
/// 规格参数
/// </summary>
[SugarColumn(Length = 200)]
[SugarColumn(Length = 200, IsNullable = true)]
public string? Specifications { get; set; }
/// <summary>
/// 制造商(设备制造商)
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Manufacturer { get; set; }
/// <summary>
/// 供应商(资产供应商)
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Supplier { get; set; }
/// <summary>
/// 设备图片地址(图片上传后存储的相对路径)
/// </summary>
[SugarColumn(Length = 200)]
[SugarColumn(Length = 200, IsNullable = true)]
public string? ImageUrl { get; set; }
/// <summary>
/// 二维码编号(一物一码)
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? QrCode { get; set; }
/// <summary>
/// 设备描述
/// </summary>
[SugarColumn(ColumnDataType = "text")]
[SugarColumn(ColumnDataType = "text", IsNullable = true)]
public string? Description { get; set; }
#endregion
@@ -84,31 +84,31 @@ namespace Model.Entity.Asset
/// <summary>
/// 使用部门名称(资产使用部门)
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Department { get; set; }
/// <summary>
/// 存放位置(实验室/楼层/房间等位置信息)
/// </summary>
[SugarColumn(Length = 200)]
[SugarColumn(Length = 200, IsNullable = true)]
public string? Location { get; set; }
/// <summary>
/// 责任人(资产责任人)
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? ResponsiblePerson { get; set; }
/// <summary>
/// 责任人联系电话(资产责任人电话)
/// </summary>
[SugarColumn(Length = 30)]
[SugarColumn(Length = 30, IsNullable = true)]
public string? ContactPhone { get; set; }
/// <summary>
/// 保管人
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? Custodian { get; set; }
#endregion
@@ -116,17 +116,19 @@ namespace Model.Entity.Asset
/// <summary>
/// 购置日期
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? PurchaseDate { get; set; }
/// <summary>
/// 购置价格(资产购置价格)
/// </summary>
[SugarColumn(DecimalDigits = 2, Length = 18)]
[SugarColumn(DecimalDigits = 2, Length = 18, IsNullable = true)]
public decimal? PurchasePrice { get; set; }
/// <summary>
/// 保修到期日期(资产保修到期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? WarrantyExpireDate { get; set; }
#endregion
@@ -134,21 +136,25 @@ namespace Model.Entity.Asset
/// <summary>
/// 验收日期(资产验收日期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? AcceptanceDate { get; set; }
/// <summary>
/// 启用日期(资产启用日期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? EnableDate { get; set; }
/// <summary>
/// 使用年限(资产使用年限,单位:年)
/// </summary>
[SugarColumn(IsNullable = true)]
public int? ServiceLifeYears { get; set; }
/// <summary>
/// 报废日期(资产报废日期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? ScrapDate { get; set; }
#endregion
@@ -168,21 +174,25 @@ namespace Model.Entity.Asset
/// <summary>
/// 上次校准日期(校准日期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? LastCalibrationDate { get; set; }
/// <summary>
/// 下次校准到期日期(用于判断是否超期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? NextCalibrationDate { get; set; }
/// <summary>
/// 检定日期
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? InspectionDate { get; set; }
/// <summary>
/// 下次检定到期日期(用于判断是否超期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? NextInspectionDate { get; set; }
/// <summary>
@@ -193,11 +203,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 上次保养日期(保养日期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? LastMaintenanceDate { get; set; }
/// <summary>
/// 下次保养到期日期(保养周期到期,用于判断是否超期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? NextMaintenanceDate { get; set; }
#endregion
@@ -205,24 +217,25 @@ namespace Model.Entity.Asset
/// <summary>
/// 备注
/// </summary>
[SugarColumn(Length = 500)]
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
/// <summary>
/// 创建人
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? CreateBy { get; set; }
/// <summary>
/// 更新人(修改人)
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? UpdateBy { get; set; }
/// <summary>
/// 更新时间(修改时间)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? UpdateTime { get; set; }
#endregion
}
@@ -30,13 +30,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 操作人
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? Operator { get; set; }
/// <summary>
/// 变更原因/备注
/// </summary>
[SugarColumn(Length = 500)]
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
+52 -4
View File
@@ -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>();
}
/// <summary>
/// EquipmentDto → EquipmentEntity(入参映射,用于新增/修改)
/// 安全约定:IsDel/CreateTime/UpdateTime 等服务端管控字段刻意不映射,防止过度绑定(over-binding);
/// Id 解析失败时为 0(新增时由雪花算法自动生成)
/// </summary>
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,
@@ -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
{
/// <summary>
/// 设备附件服务实现(资产附件管理)
/// </summary>
public class EquipmentAttachmentService : IEquipmentAttachmentService
{
private readonly SqlSugarRepository<EquipmentAttachmentEntity> _repository;
public EquipmentAttachmentService(SqlSugarRepository<EquipmentAttachmentEntity> repository)
{
_repository = repository;
}
/// <summary>
/// 查询指定设备的附件列表(按上传时间倒序)
/// </summary>
public async Task<Result<List<EquipmentAttachmentEntity>>> 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<List<EquipmentAttachmentEntity>>.Success(list);
}
catch (Exception ex)
{
return Result<List<EquipmentAttachmentEntity>>.Error("查询附件列表失败", ex);
}
}
/// <summary>
/// 上传附件(保存文件到 uploads 目录并写入附件记录)
/// </summary>
public async Task<Result<EquipmentAttachmentEntity>> UploadAsync(long equipmentId, Stream stream, string originalFileName, long fileSize,
AttachmentTypeEnum fileType, string? uploader, string? remark, string webRootPath)
{
if (equipmentId <= 0)
{
return Result<EquipmentAttachmentEntity>.Error("设备 Id 无效");
}
if (stream == null || fileSize <= 0)
{
return Result<EquipmentAttachmentEntity>.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<EquipmentAttachmentEntity>.Success(entity);
}
catch (Exception ex)
{
return Result<EquipmentAttachmentEntity>.Error("上传附件失败", ex);
}
}
/// <summary>
/// 删除附件(软删除记录并删除物理文件)
/// </summary>
public async Task<Result<bool>> DeleteAsync(long id)
{
if (id <= 0)
{
return Result<bool>.Error("附件 Id 无效");
}
try
{
var attachment = await _repository.Entities
.Where(x => x.Id == id && x.IsDel == 0)
.FirstAsync();
if (attachment == null)
{
return Result<bool>.Error("附件不存在或已被删除");
}
var result = await _repository.Context.Updateable<EquipmentAttachmentEntity>()
.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<bool>.Success(result > 0);
}
catch (Exception ex)
{
return Result<bool>.Error("删除附件失败", ex);
}
}
}
}
@@ -0,0 +1,43 @@
using Model;
using Model.Entity.Asset;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace Service.Interface
{
/// <summary>
/// 设备附件服务接口(资产附件管理:分类存储、在线预览)
/// </summary>
public interface IEquipmentAttachmentService
{
/// <summary>
/// 查询指定设备的附件列表(按上传时间倒序)
/// </summary>
/// <param name="equipmentId">设备 Id</param>
/// <returns>返回包含附件列表的 Result</returns>
Task<Result<List<EquipmentAttachmentEntity>>> GetByEquipmentAsync(long equipmentId);
/// <summary>
/// 上传附件(保存文件到 uploads 目录并写入附件记录)
/// </summary>
/// <param name="equipmentId">设备 Id</param>
/// <param name="stream">文件流</param>
/// <param name="originalFileName">原始文件名</param>
/// <param name="fileSize">文件大小(字节)</param>
/// <param name="fileType">文件类型(说明书/合格证/校准证书等)</param>
/// <param name="uploader">上传人</param>
/// <param name="remark">备注</param>
/// <param name="webRootPath">站点静态文件根目录物理路径</param>
/// <returns>返回新增附件记录的 Result</returns>
Task<Result<EquipmentAttachmentEntity>> UploadAsync(long equipmentId, Stream stream, string originalFileName, long fileSize,
AttachmentTypeEnum fileType, string? uploader, string? remark, string webRootPath);
/// <summary>
/// 删除附件(软删除记录并删除物理文件)
/// </summary>
/// <param name="id">附件 Id</param>
/// <returns>返回操作是否成功的 Result</returns>
Task<Result<bool>> DeleteAsync(long id);
}
}