diff --git a/IOT_API/Controllers/Asset/QrCodeController.cs b/IOT_API/Controllers/Asset/QrCodeController.cs index f77f454..562d462 100644 --- a/IOT_API/Controllers/Asset/QrCodeController.cs +++ b/IOT_API/Controllers/Asset/QrCodeController.cs @@ -1,14 +1,86 @@ using Microsoft.AspNetCore.Mvc; +using Model; +using Model.Dto.Asset; +using Service.Interface; +using SqlSugar; +using WebAPI.Filters; namespace WebAPI.Controllers { /// - /// 二维码 + /// 设备二维码生成与打印(资产侧;生成/绑定 RFID 需 device:edit,列表/图片/打印页 device:view 可访问) + /// 扫码直达:二维码内容 = http://host/asset/ledger/equipment?id={equipmentId} /// [ApiController] [Route("api/asset/qrcode")] public class QrCodeController : ControllerBase { - // TODO: 实现 二维码 相关接口 + private readonly IQrCodeService _qrService; + + public QrCodeController(IQrCodeService qrService) + { + _qrService = qrService; + } + + // baseUrl 由当前请求构造(scheme://host[:port]);如需固定域名,改为读 sys_param site_base_url + private string BaseUrl => $"{Request.Scheme}://{Request.Host}"; + + /// 生成二维码 URL(写入设备 QrCodeUrl 字段,不返回图片) + [HttpPost("generate/{equipmentId}")] + [RequirePermission("device:edit")] + public async Task Generate(long equipmentId) + { + return Ok(await _qrService.GenerateAsync(equipmentId, BaseUrl)); + } + + /// 生成二维码 PNG 图片(直接返回 image/png 二进制) + [HttpGet("image/{equipmentId}")] + [RequirePermission("device:view")] + public async Task GetImage(long equipmentId) + { + var result = await _qrService.GenerateImageAsync(equipmentId, BaseUrl); + if (!result.IsSuccess || result.Data == null) + return Ok(Result.Error(result.Msg)); + return File(result.Data, "image/png"); + } + + /// 批量生成二维码 URL + [HttpPost("batch-generate")] + [RequirePermission("device:edit")] + public async Task BatchGenerate([FromBody] long[] equipmentIds) + { + return Ok(await _qrService.BatchGenerateAsync(equipmentIds, BaseUrl)); + } + + /// 设备二维码列表(分页,qrOnly=1 仅看已生成) + [HttpGet("list")] + [RequirePermission("device:view")] + public async Task GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, [FromQuery] bool qrOnly = false) + { + RefAsync total = 0; + var result = await _qrService.GetListPagedAsync(pageIndex, pageSize, total, keyword, qrOnly); + Response.Headers["X-Total-Count"] = total.Value.ToString(); + return result.IsSuccess + ? Ok(Result>.Success(result.Data)) + : Ok(Result>.Error(result.Msg)); + } + + /// 打印页(返回 text/html,浏览器打开后自动调起打印) + [HttpGet("print/{equipmentId}")] + [RequirePermission("device:view")] + public async Task Print(long equipmentId) + { + var result = await _qrService.GetPrintHtmlAsync(equipmentId, BaseUrl); + if (!result.IsSuccess) return Ok(result); + return Content(result.Data!, "text/html; charset=utf-8"); + } + + /// 绑定 RFID 编号到设备(query 传 rfidCode) + [HttpPost("bind-rfid/{equipmentId}")] + [RequirePermission("device:edit")] + public async Task BindRfid(long equipmentId, [FromQuery] string rfidCode) + { + return Ok(await _qrService.BindRfidAsync(equipmentId, rfidCode)); + } } } diff --git a/IOT_API/Controllers/System/DictController.cs b/IOT_API/Controllers/System/DictController.cs index abae2dd..e57d4e4 100644 --- a/IOT_API/Controllers/System/DictController.cs +++ b/IOT_API/Controllers/System/DictController.cs @@ -1,14 +1,121 @@ using Microsoft.AspNetCore.Mvc; +using Model; +using Model.Dto.System; +using Service.Interface; +using SqlSugar; +using WebAPI.Filters; namespace WebAPI.Controllers { /// - /// 数据字典管理 + /// 数据字典管理(管理侧需 system:manage 权限;业务侧 options 接口无权限供其它模块调用) /// [ApiController] [Route("api/system/dict")] public class DictController : ControllerBase { - // TODO: 实现 数据字典管理 相关接口 + private readonly IDictService _dictService; + + public DictController(IDictService dictService) + { + _dictService = dictService; + } + + // ==================== 字典分类 ==================== + + /// 字典分类列表(分页) + [HttpGet("type/list")] + [RequirePermission("system:manage")] + public async Task GetTypeList(int pageIndex = 1, int pageSize = 10, string? keyword = null) + { + RefAsync total = 0; + var result = await _dictService.GetTypesPagedAsync(pageIndex, pageSize, total, keyword); + Response.Headers["X-Total-Count"] = total.Value.ToString(); + return result.IsSuccess + ? Ok(Result>.Success(result.Data)) + : Ok(Result>.Error(result.Msg)); + } + + /// 字典分类下拉(全部启用的分类) + [HttpGet("type/options")] + public async Task GetTypeOptions() + { + return Ok(await _dictService.GetTypesAllAsync()); + } + + /// 字典分类详情 + [HttpGet("type/{id}")] + [RequirePermission("system:manage")] + public async Task GetTypeById(long id) + { + return Ok(await _dictService.GetTypeByIdAsync(id)); + } + + /// 新增字典分类 + [HttpPost("type")] + [RequirePermission("system:manage")] + public async Task AddType([FromBody] DictTypeDto dto) + { + return Ok(await _dictService.AddTypeAsync(dto)); + } + + /// 修改字典分类 + [HttpPut("type")] + [RequirePermission("system:manage")] + public async Task UpdateType([FromBody] DictTypeDto dto) + { + return Ok(await _dictService.UpdateTypeAsync(dto)); + } + + /// 删除字典分类(分类下有字典项时禁止删除) + [HttpDelete("type/{id}")] + [RequirePermission("system:manage")] + public async Task DeleteType(long id) + { + return Ok(await _dictService.DeleteTypeAsync(id)); + } + + // ==================== 字典项 ==================== + + /// 查询指定分类下的字典项 + [HttpGet("item/list/{typeId}")] + [RequirePermission("system:manage")] + public async Task GetItemsByType(long typeId) + { + return Ok(await _dictService.GetItemsByTypeAsync(typeId)); + } + + /// 新增字典项 + [HttpPost("item")] + [RequirePermission("system:manage")] + public async Task AddItem([FromBody] DictItemDto dto) + { + return Ok(await _dictService.AddItemAsync(dto)); + } + + /// 修改字典项 + [HttpPut("item")] + [RequirePermission("system:manage")] + public async Task UpdateItem([FromBody] DictItemDto dto) + { + return Ok(await _dictService.UpdateItemAsync(dto)); + } + + /// 删除字典项 + [HttpDelete("item/{id}")] + [RequirePermission("system:manage")] + public async Task DeleteItem(long id) + { + return Ok(await _dictService.DeleteItemAsync(id)); + } + + // ==================== 业务侧(无权限,供其它模块通过字典编码查询) ==================== + + /// 按字典编码查询启用项(带缓存,业务侧调用) + [HttpGet("options/{typeCode}")] + public async Task GetOptions(string typeCode) + { + return Ok(await _dictService.GetOptionsByCodeAsync(typeCode)); + } } } diff --git a/IOT_API/Controllers/System/FileStorageController.cs b/IOT_API/Controllers/System/FileStorageController.cs index 98c401b..3d3a9aa 100644 --- a/IOT_API/Controllers/System/FileStorageController.cs +++ b/IOT_API/Controllers/System/FileStorageController.cs @@ -1,14 +1,118 @@ +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Model; +using Model.Dto.System; +using Service.Interface; +using SqlSugar; +using WebAPI.Filters; namespace WebAPI.Controllers { /// - /// 文件存储管理 + /// 文件存储管理(存储配置 CRUD + 连接测试 + 文件上传 + 文件记录查询,需 system:manage 权限) /// [ApiController] [Route("api/system/file-storage")] + [RequirePermission("system:manage")] public class FileStorageController : ControllerBase { - // TODO: 实现 文件存储管理 相关接口 + private readonly IFileStorageService _storageService; + + public FileStorageController(IFileStorageService storageService) + { + _storageService = storageService; + } + + // ==================== 存储配置 ==================== + + /// 存储配置列表(分页) + [HttpGet("list")] + public async Task GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null) + { + RefAsync total = 0; + var result = await _storageService.GetListPagedAsync(pageIndex, pageSize, total, keyword); + Response.Headers["X-Total-Count"] = total.Value.ToString(); + return result.IsSuccess + ? Ok(Result>.Success(result.Data)) + : Ok(Result>.Error(result.Msg)); + } + + /// 存储配置详情 + [HttpGet("{id}")] + public async Task GetById(long id) + { + return Ok(await _storageService.GetByIdAsync(id)); + } + + /// 新增存储配置 + [HttpPost] + public async Task Add([FromBody] FileStorageConfigDto dto) + { + return Ok(await _storageService.AddAsync(dto)); + } + + /// 修改存储配置(AccessKey/SecretKey 为 **** 掩码时保留原值) + [HttpPut] + public async Task Update([FromBody] FileStorageConfigDto dto) + { + return Ok(await _storageService.UpdateAsync(dto)); + } + + /// 删除存储配置(默认通道禁止删除) + [HttpDelete("{id}")] + public async Task Delete(long id) + { + return Ok(await _storageService.DeleteAsync(id)); + } + + /// 设为默认存储通道 + [HttpPost("{id}/default")] + public async Task SetDefault(long id) + { + return Ok(await _storageService.SetDefaultAsync(id)); + } + + /// 测试连接(按配置调用对应 Provider 的 TestConnectionAsync) + [HttpPost("{id}/test")] + public async Task Test(long id) + { + return Ok(await _storageService.TestAsync(id)); + } + + // ==================== 文件上传/记录 ==================== + + /// 上传文件(multipart/form-data,字段名 file;使用默认存储通道) + /// 文件 + /// 业务类型(equipment_attachment/qrcode/generic 等,可选) + /// 业务Id(可选) + /// 上传人(可选,默认取当前登录用户) + [HttpPost("upload")] + [RequestSizeLimit(100 * 1024 * 1024)] + public async Task Upload(IFormFile file, [FromForm] string? bizType = null, [FromForm] string? bizId = null, [FromForm] string? uploader = null) + { + if (file == null || file.Length == 0) + return Ok(Result.Error("请选择要上传的文件")); + using var stream = file.OpenReadStream(); + return Ok(await _storageService.UploadAsync(stream, file.FileName, file.Length, bizType, bizId, uploader)); + } + + /// 文件记录列表(分页,可按关键字和业务类型过滤) + [HttpGet("file/list")] + public async Task GetFileList(int pageIndex = 1, int pageSize = 10, string? keyword = null, string? bizType = null) + { + RefAsync total = 0; + var result = await _storageService.GetFileListPagedAsync(pageIndex, pageSize, total, keyword, bizType); + Response.Headers["X-Total-Count"] = total.Value.ToString(); + return result.IsSuccess + ? Ok(Result>.Success(result.Data)) + : Ok(Result>.Error(result.Msg)); + } + + /// 文件记录详情 + [HttpGet("file/{id}")] + public async Task GetFileById(long id) + { + return Ok(await _storageService.GetFileByIdAsync(id)); + } } } diff --git a/IOT_API/Controllers/System/SystemParamController.cs b/IOT_API/Controllers/System/SystemParamController.cs index 8822fe8..0089fdf 100644 --- a/IOT_API/Controllers/System/SystemParamController.cs +++ b/IOT_API/Controllers/System/SystemParamController.cs @@ -1,14 +1,95 @@ using Microsoft.AspNetCore.Mvc; +using Model; +using Model.Dto.System; +using Service.Interface; +using SqlSugar; +using WebAPI.Filters; namespace WebAPI.Controllers { /// - /// 系统参数配置 + /// 系统参数配置(管理侧需 system:manage 权限;业务侧 value/values 接口无权限供其它模块调用) /// [ApiController] [Route("api/system/param")] public class SystemParamController : ControllerBase { - // TODO: 实现 系统参数配置 相关接口 + private readonly ISystemParamService _paramService; + + public SystemParamController(ISystemParamService paramService) + { + _paramService = paramService; + } + + // ==================== 管理侧 ==================== + + /// 参数列表(分页,可按关键字和分组过滤) + [HttpGet("list")] + [RequirePermission("system:manage")] + public async Task GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, string? group = null) + { + RefAsync total = 0; + var result = await _paramService.GetListPagedAsync(pageIndex, pageSize, total, keyword, group); + Response.Headers["X-Total-Count"] = total.Value.ToString(); + return result.IsSuccess + ? Ok(Result>.Success(result.Data)) + : Ok(Result>.Error(result.Msg)); + } + + /// 参数详情 + [HttpGet("{id}")] + [RequirePermission("system:manage")] + public async Task GetById(long id) + { + return Ok(await _paramService.GetByIdAsync(id)); + } + + /// 新增参数 + [HttpPost] + [RequirePermission("system:manage")] + public async Task Add([FromBody] SystemParamDto dto) + { + return Ok(await _paramService.AddAsync(dto)); + } + + /// 修改参数(内置参数仅允许改 Value/Remark) + [HttpPut] + [RequirePermission("system:manage")] + public async Task Update([FromBody] SystemParamDto dto) + { + return Ok(await _paramService.UpdateAsync(dto)); + } + + /// 删除参数(内置参数禁止删除) + [HttpDelete("{id}")] + [RequirePermission("system:manage")] + public async Task Delete(long id) + { + return Ok(await _paramService.DeleteAsync(id)); + } + + /// 清空参数缓存(运维用) + [HttpPost("cache/reload")] + [RequirePermission("system:manage")] + public async Task ReloadCache() + { + return Ok(await _paramService.ReloadCacheAsync()); + } + + // ==================== 业务侧(无权限,供其它模块按 Key 取值) ==================== + + /// 按 Key 取单个值 + [HttpGet("value/{key}")] + public async Task GetValue(string key) + { + return Ok(await _paramService.GetValueAsync(key)); + } + + /// 按 Key 列表批量取值(query: ?keys=k1&k2&k3) + [HttpGet("values")] + public async Task GetValues([FromQuery] string[] keys) + { + return Ok(await _paramService.GetValuesAsync(keys)); + } } } diff --git a/IOT_API/DependencyInjection.cs b/IOT_API/DependencyInjection.cs index b1bd0d9..7caa24b 100644 --- a/IOT_API/DependencyInjection.cs +++ b/IOT_API/DependencyInjection.cs @@ -54,6 +54,10 @@ namespace WebAPI // 泛型基础服务(IBaseService<> -> BaseService<>)单独注册 services.AddScoped(typeof(Service.Interface.IBaseService<>), typeof(Service.Implement.BaseService<>)); + // 文件存储 Provider:命名约定 XxxFileStorageProvider 不匹配自动注册,手动登记 + // 新增 MinIO/OSS 实现时在此追加一行 services.AddScoped() + services.AddScoped(); + return services; } diff --git a/IOT_API/Program.cs b/IOT_API/Program.cs index 471a535..5b2d8c9 100644 --- a/IOT_API/Program.cs +++ b/IOT_API/Program.cs @@ -59,6 +59,9 @@ namespace WebAPI // 自动注册业务服务(Service.Interface -> Service.Implement) builder.Services.AddBusinessServices(); + // 内存缓存:数据字典等查询频繁的基础数据按 typeCode 缓存,增删改时失效 + builder.Services.AddMemoryCache(); + // 当前用户上下文(从 JWT Claims 解析,供业务服务取操作人/做权限判断) builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); diff --git a/Model/Dto/Asset/EquipmentDto.cs b/Model/Dto/Asset/EquipmentDto.cs index ee1adcb..88117fa 100644 --- a/Model/Dto/Asset/EquipmentDto.cs +++ b/Model/Dto/Asset/EquipmentDto.cs @@ -49,6 +49,12 @@ namespace Model.Dto.Asset /// 二维码编号(一物一码) public string? QrCode { get; set; } + /// 二维码扫码直达 URL(生成后写入,扫码跳转设备台账详情) + public string? QrCodeUrl { get; set; } + + /// RFID 编号(绑定时写入) + public string? RfidCode { get; set; } + /// 设备描述 public string? Description { get; set; } diff --git a/Model/Dto/Asset/QrCodeDto.cs b/Model/Dto/Asset/QrCodeDto.cs new file mode 100644 index 0000000..3f7d2e5 --- /dev/null +++ b/Model/Dto/Asset/QrCodeDto.cs @@ -0,0 +1,38 @@ +namespace Model.Dto.Asset +{ + /// + /// 设备二维码信息(生成/列表/详情共用) + /// + public class QrCodeDto + { + /// 设备 Id(long → string) + public string EquipmentId { get; set; } + /// 设备编号 + public string? EquipmentCode { get; set; } + /// 设备名称 + public string? EquipmentName { get; set; } + /// 存放位置(打印标签用) + public string? Location { get; set; } + /// 二维码编号(一物一码,人工可读) + public string? QrCode { get; set; } + /// 扫码直达 URL(生成后写入;为空表示尚未生成) + public string? QrCodeUrl { get; set; } + /// RFID 编号(已绑定则有值) + public string? RfidCode { get; set; } + /// 是否已生成二维码(QrCodeUrl 非空) + public bool HasGenerated => !string.IsNullOrWhiteSpace(QrCodeUrl); + } + + /// + /// 批量生成结果 + /// + public class QrCodeBatchResultDto + { + public string EquipmentId { get; set; } + public string? EquipmentCode { get; set; } + public string? EquipmentName { get; set; } + public string? QrCodeUrl { get; set; } + public bool Success { get; set; } + public string? Message { get; set; } + } +} diff --git a/Model/Dto/System/DictDto.cs b/Model/Dto/System/DictDto.cs new file mode 100644 index 0000000..3642862 --- /dev/null +++ b/Model/Dto/System/DictDto.cs @@ -0,0 +1,44 @@ +namespace Model.Dto.System +{ + /// + /// 数据字典分类 DTO + /// + public class DictTypeDto + { + public string Id { get; set; } + public string Code { get; set; } + public string Name { get; set; } + public byte Status { get; set; } + public int Sort { get; set; } + public string? Remark { get; set; } + public DateTime? CreateTime { get; set; } + /// 该分类下的字典项数量(列表展示用,由 Service 填充) + public int ItemCount { get; set; } + } + + /// + /// 数据字典项 DTO + /// + public class DictItemDto + { + public string Id { get; set; } + public string TypeId { get; set; } + public string Code { get; set; } + public string Name { get; set; } + public int Sort { get; set; } + public byte Status { get; set; } + public byte IsDefault { get; set; } + public string? Remark { get; set; } + public DateTime? CreateTime { get; set; } + } + + /// + /// 字典项下拉选项(业务侧通过 typeCode 查询用:仅返回编码+文本+是否默认) + /// + public class DictOptionDto + { + public string Code { get; set; } + public string Name { get; set; } + public bool IsDefault { get; set; } + } +} diff --git a/Model/Dto/System/FileStorageDto.cs b/Model/Dto/System/FileStorageDto.cs new file mode 100644 index 0000000..2dab323 --- /dev/null +++ b/Model/Dto/System/FileStorageDto.cs @@ -0,0 +1,60 @@ +namespace Model.Dto.System +{ + /// + /// 文件存储配置 DTO + /// + public class FileStorageConfigDto + { + public string Id { get; set; } + public string Provider { get; set; } + public string Name { get; set; } + public string? Endpoint { get; set; } + /// 回显时掩码(仅显示前后各 2 字符);写入时若为掩码样式则保留原值不变 + public string? AccessKey { get; set; } + public string? SecretKey { get; set; } + public string? Bucket { get; set; } + public string? Region { get; set; } + public string? BasePath { get; set; } + public int MaxSizeMB { get; set; } + public string? AllowedExts { get; set; } + public byte IsDefault { get; set; } + public byte Status { get; set; } + public string? Remark { get; set; } + public DateTime? CreateTime { get; set; } + } + + /// + /// 文件上传记录 DTO + /// + public class FileRecordDto + { + public string Id { get; set; } + public string FileName { get; set; } + public string? OriginalName { get; set; } + public string? Url { get; set; } + public long Size { get; set; } + public string? Ext { get; set; } + public string? Provider { get; set; } + public string StorageId { get; set; } + public string? Uploader { get; set; } + public string? BizType { get; set; } + public string? BizId { get; set; } + public DateTime? CreateTime { get; set; } + } + + /// + /// 上传结果(Provider 返回,Service 写入 sys_file_record 时用) + /// + public class FileUploadResultDto + { + /// 存储后的相对文件名(含路径,如 2026/09/18/snowflake.jpg) + public string FileName { get; set; } + /// 访问 URL + public string Url { get; set; } + /// 扩展名 + public string? Ext { get; set; } + /// 文件大小(字节) + public long Size { get; set; } + public string Provider { get; set; } + } +} diff --git a/Model/Dto/System/SystemParamDto.cs b/Model/Dto/System/SystemParamDto.cs new file mode 100644 index 0000000..d54f01f --- /dev/null +++ b/Model/Dto/System/SystemParamDto.cs @@ -0,0 +1,35 @@ +namespace Model.Dto.System +{ + /// + /// 系统参数 DTO + /// + public class SystemParamDto + { + public string Id { get; set; } + public string ParamKey { get; set; } + public string ParamName { get; set; } + public string? ParamValue { get; set; } + /// 值类型(0=int, 1=text, 2=enum, 3=json) + public byte ParamType { get; set; } + /// 枚举选项(原始 JSON 字符串,由前端解析;仅 ParamType=enum 有值) + public string? ParamOptions { get; set; } + public string? Unit { get; set; } + public string? Group { get; set; } + public int Sort { get; set; } + public string? Remark { get; set; } + /// 是否系统内置(1=不可删,仅允许改 Value) + public byte IsSystem { get; set; } + public string? LastUpdateUser { get; set; } + public DateTime? LastUpdateTime { get; set; } + public DateTime? CreateTime { get; set; } + } + + /// + /// 业务侧批量取值返回项 + /// + public class SystemParamValueDto + { + public string Key { get; set; } + public string? Value { get; set; } + } +} diff --git a/Model/Entity/Asset/EquipmentEntity.cs b/Model/Entity/Asset/EquipmentEntity.cs index c54eff4..c6bcddc 100644 --- a/Model/Entity/Asset/EquipmentEntity.cs +++ b/Model/Entity/Asset/EquipmentEntity.cs @@ -1,4 +1,4 @@ -using SqlSugar; +using SqlSugar; namespace Model.Entity.Asset { @@ -73,6 +73,19 @@ namespace Model.Entity.Asset [SugarColumn(Length = 50, IsNullable = true)] public string? QrCode { get; set; } + /// + /// 二维码扫码直达 URL(生成二维码时写入,扫码后跳转设备台账详情) + /// 形如 http://host/asset/ledger/equipment?id={Id};为空表示尚未生成二维码 + /// + [SugarColumn(Length = 500, IsNullable = true)] + public string? QrCodeUrl { get; set; } + + /// + /// RFID 编号(绑定时写入,用于 RFID 标签识别设备) + /// + [SugarColumn(Length = 64, IsNullable = true)] + public string? RfidCode { get; set; } + /// /// 设备描述 /// diff --git a/Model/Entity/System/DictItemEntity.cs b/Model/Entity/System/DictItemEntity.cs new file mode 100644 index 0000000..843e951 --- /dev/null +++ b/Model/Entity/System/DictItemEntity.cs @@ -0,0 +1,32 @@ +using SqlSugar; + +namespace Model.Entity.System +{ + /// + /// 数据字典项表(某分类下的具体枚举值) + /// + [SugarTable("sys_dict_item")] + public class DictItemEntity : BaseEntity + { + [SugarColumn(ColumnName = "TypeId", ColumnDescription = "所属字典分类Id")] + public long TypeId { get; set; } + + [SugarColumn(ColumnName = "Code", ColumnDescription = "字典项编码", Length = 64, IsNullable = false)] + public string Code { get; set; } + + [SugarColumn(ColumnName = "Name", ColumnDescription = "字典项文本", Length = 100, IsNullable = false)] + public string Name { get; set; } + + [SugarColumn(ColumnName = "Sort", ColumnDescription = "排序号", DefaultValue = "0")] + public int Sort { get; set; } + + [SugarColumn(ColumnName = "Status", ColumnDescription = "状态(1=启用,0=停用)", ColumnDataType = "smallint", DefaultValue = "1")] + public byte Status { get; set; } + + [SugarColumn(ColumnName = "IsDefault", ColumnDescription = "是否默认选中(同分类下唯一)", ColumnDataType = "smallint", DefaultValue = "0")] + public byte IsDefault { get; set; } + + [SugarColumn(ColumnName = "Remark", ColumnDescription = "备注", Length = 500, IsNullable = true)] + public string? Remark { get; set; } + } +} diff --git a/Model/Entity/System/DictTypeEntity.cs b/Model/Entity/System/DictTypeEntity.cs new file mode 100644 index 0000000..fe0897b --- /dev/null +++ b/Model/Entity/System/DictTypeEntity.cs @@ -0,0 +1,26 @@ +using SqlSugar; + +namespace Model.Entity.System +{ + /// + /// 数据字典分类表(如:设备类型、故障类型、告警级别、工单优先级) + /// + [SugarTable("sys_dict_type")] + public class DictTypeEntity : BaseEntity + { + [SugarColumn(ColumnName = "Code", ColumnDescription = "字典编码(唯一)", Length = 64, IsNullable = false)] + public string Code { get; set; } + + [SugarColumn(ColumnName = "Name", ColumnDescription = "字典名称", Length = 100, IsNullable = false)] + public string Name { get; set; } + + [SugarColumn(ColumnName = "Status", ColumnDescription = "状态(1=启用,0=停用)", ColumnDataType = "smallint", DefaultValue = "1")] + public byte Status { get; set; } + + [SugarColumn(ColumnName = "Sort", ColumnDescription = "排序号", DefaultValue = "0")] + public int Sort { get; set; } + + [SugarColumn(ColumnName = "Remark", ColumnDescription = "备注", Length = 500, IsNullable = true)] + public string? Remark { get; set; } + } +} diff --git a/Model/Entity/System/FileRecordEntity.cs b/Model/Entity/System/FileRecordEntity.cs new file mode 100644 index 0000000..9a1d321 --- /dev/null +++ b/Model/Entity/System/FileRecordEntity.cs @@ -0,0 +1,50 @@ +using SqlSugar; + +namespace Model.Entity.System +{ + /// + /// 文件上传记录表(每次上传都登记:来源 Provider、业务类型、访问 URL) + /// + [SugarTable("sys_file_record")] + public class FileRecordEntity : BaseEntity + { + /// 存储后的文件名(含相对路径,如 2026/09/18/snowflake.jpg) + [SugarColumn(ColumnName = "FileName", ColumnDescription = "存储文件名(含相对路径)", Length = 255, IsNullable = false)] + public string FileName { get; set; } + + /// 原始文件名(用户上传时的文件名) + [SugarColumn(ColumnName = "OriginalName", ColumnDescription = "原始文件名", Length = 255, IsNullable = true)] + public string? OriginalName { get; set; } + + /// 访问 URL(Endpoint + 相对路径) + [SugarColumn(ColumnName = "Url", ColumnDescription = "访问URL", Length = 500, IsNullable = true)] + public string? Url { get; set; } + + /// 文件大小(字节) + [SugarColumn(ColumnName = "Size", ColumnDescription = "文件大小(字节)")] + public long Size { get; set; } + + /// 扩展名(含点,如 .jpg) + [SugarColumn(ColumnName = "Ext", ColumnDescription = "扩展名", Length = 20, IsNullable = true)] + public string? Ext { get; set; } + + /// 使用的存储 Provider(冗余,便于排查) + [SugarColumn(ColumnName = "Provider", ColumnDescription = "存储Provider", Length = 20, IsNullable = true)] + public string? Provider { get; set; } + + /// 使用的存储配置Id(关联 sys_file_storage.Id) + public long StorageId { get; set; } + + /// 上传人 + [SugarColumn(ColumnName = "Uploader", ColumnDescription = "上传人", Length = 50, IsNullable = true)] + public string? Uploader { get; set; } + + /// 业务类型(equipment_attachment/qrcode/generic 等,便于按业务查询) + [SugarColumn(ColumnName = "BizType", ColumnDescription = "业务类型", Length = 50, IsNullable = true)] + public string? BizType { get; set; } + + /// 业务Id(关联业务实体的 Id,可空) + [SugarColumn(ColumnName = "BizId", ColumnDescription = "业务Id", Length = 64, IsNullable = true)] + public string? BizId { get; set; } + } +} diff --git a/Model/Entity/System/FileStorageConfigEntity.cs b/Model/Entity/System/FileStorageConfigEntity.cs new file mode 100644 index 0000000..57e36a3 --- /dev/null +++ b/Model/Entity/System/FileStorageConfigEntity.cs @@ -0,0 +1,64 @@ +using SqlSugar; + +namespace Model.Entity.System +{ + /// + /// 文件存储配置表(多 Provider 配置:Local/MinIO/OSS,仅 Local 落地,其它留扩展点) + /// 一个 IsDefault=1 的配置为默认上传通道;AccessKey/SecretKey 对 Local 为空 + /// + [SugarTable("sys_file_storage")] + public class FileStorageConfigEntity : BaseEntity + { + /// 存储 Provider(local/minio/oss) + [SugarColumn(ColumnName = "Provider", ColumnDescription = "存储Provider(local/minio/oss)", Length = 20, IsNullable = false)] + public string Provider { get; set; } + + /// 配置名称(如:本地存储 / MinIO测试) + [SugarColumn(ColumnName = "Name", ColumnDescription = "配置名称", Length = 100, IsNullable = false)] + public string Name { get; set; } + + /// 访问端点(Local 为 URL 前缀如 http://host/uploads 或 /uploads;MinIO 为 http://minio:9000) + [SugarColumn(ColumnName = "Endpoint", ColumnDescription = "访问端点", Length = 255, IsNullable = true)] + public string? Endpoint { get; set; } + + /// 访问 Key(Local 为空;MinIO/OSS 为 AccessKey。注:当前明文存储,接入 MinIO/OSS 时应改为 AES 加密) + [SugarColumn(ColumnName = "AccessKey", ColumnDescription = "AccessKey(Local为空)", Length = 128, IsNullable = true)] + public string? AccessKey { get; set; } + + /// 密钥(Local 为空;MinIO/OSS 为 SecretKey。注:当前明文存储,接入 MinIO/OSS 时应改为 AES 加密) + [SugarColumn(ColumnName = "SecretKey", ColumnDescription = "SecretKey(Local为空)", Length = 255, IsNullable = true)] + public string? SecretKey { get; set; } + + /// 桶名(Local 为 BasePath 下子目录;MinIO/OSS 为 bucket 名) + [SugarColumn(ColumnName = "Bucket", ColumnDescription = "桶名", Length = 100, IsNullable = true)] + public string? Bucket { get; set; } + + /// 区域(OSS 用,Local/MinIO 为空) + [SugarColumn(ColumnName = "Region", ColumnDescription = "区域(OSS用)", Length = 50, IsNullable = true)] + public string? Region { get; set; } + + /// 本地存储根路径(Local 专用,如 wwwroot/uploads;MinIO/OSS 为空) + [SugarColumn(ColumnName = "BasePath", ColumnDescription = "本地存储根路径(Local专用)", Length = 255, IsNullable = true)] + public string? BasePath { get; set; } + + /// 单个文件大小上限(MB) + [SugarColumn(ColumnName = "MaxSizeMB", ColumnDescription = "单个文件大小上限(MB)", DefaultValue = "10")] + public int MaxSizeMB { get; set; } = 10; + + /// 允许的扩展名(JSON 数组,如 [".jpg",".png"];为空表示不限制) + [SugarColumn(ColumnName = "AllowedExts", ColumnDescription = "允许扩展名(JSON数组)", ColumnDataType = "text", IsNullable = true)] + public string? AllowedExts { get; set; } + + /// 是否默认(1=默认上传通道,全局唯一) + [SugarColumn(ColumnName = "IsDefault", ColumnDescription = "是否默认(1=默认,0=否)", ColumnDataType = "smallint", DefaultValue = "0")] + public byte IsDefault { get; set; } + + /// 状态(1=启用,0=停用) + [SugarColumn(ColumnName = "Status", ColumnDescription = "状态(1=启用,0=停用)", ColumnDataType = "smallint", DefaultValue = "1")] + public byte Status { get; set; } = 1; + + /// 备注 + [SugarColumn(ColumnName = "Remark", ColumnDescription = "备注", Length = 500, IsNullable = true)] + public string? Remark { get; set; } + } +} diff --git a/Model/Entity/System/SystemParamEntity.cs b/Model/Entity/System/SystemParamEntity.cs new file mode 100644 index 0000000..314b667 --- /dev/null +++ b/Model/Entity/System/SystemParamEntity.cs @@ -0,0 +1,60 @@ +using SqlSugar; + +namespace Model.Entity.System +{ + /// + /// 系统参数配置表(键值对形式存储业务可调参数,按 Group 分组展示) + /// 内置参数(IsSystem=1)禁止删除,仅允许修改 Value + /// + [SugarTable("sys_param")] + public class SystemParamEntity : BaseEntity + { + /// 参数键(唯一,业务侧按 Key 取值,如 data_collect_default_frequency) + [SugarColumn(ColumnName = "ParamKey", ColumnDescription = "参数键(唯一)", Length = 64, IsNullable = false)] + public string ParamKey { get; set; } + + /// 参数名称(中文展示名) + [SugarColumn(ColumnName = "ParamName", ColumnDescription = "参数名称", Length = 100, IsNullable = false)] + public string ParamName { get; set; } + + /// 参数值(统一字符串存储,业务侧按 ParamType 自行转换) + [SugarColumn(ColumnName = "ParamValue", ColumnDescription = "参数值", ColumnDataType = "text", IsNullable = true)] + public string? ParamValue { get; set; } + + /// 值类型(0=int, 1=text, 2=enum, 3=json)决定前端渲染控件 + [SugarColumn(ColumnName = "ParamType", ColumnDescription = "值类型(0=int,1=text,2=enum,3=json)", ColumnDataType = "smallint", DefaultValue = "1")] + public byte ParamType { get; set; } + + /// 枚举选项(JSON 数组,如 [{"value":"feishu","label":"飞书"}];仅 ParamType=enum 使用) + [SugarColumn(ColumnName = "ParamOptions", ColumnDescription = "枚举选项(JSON数组)", ColumnDataType = "text", IsNullable = true)] + public string? ParamOptions { get; set; } + + /// 单位(如 秒/MB,前端展示用) + [SugarColumn(ColumnName = "Unit", ColumnDescription = "单位", Length = 20, IsNullable = true)] + public string? Unit { get; set; } + + /// 分组(如 数据采集/告警/工单/文件,前端按分组卡片展示) + [SugarColumn(ColumnName = "Group", ColumnDescription = "分组", Length = 50, IsNullable = true)] + public string? Group { get; set; } + + /// 排序号 + [SugarColumn(ColumnName = "Sort", ColumnDescription = "排序号", DefaultValue = "0")] + public int Sort { get; set; } + + /// 备注(注释提示,如工单编号规则占位符说明) + [SugarColumn(ColumnName = "Remark", ColumnDescription = "备注", Length = 500, IsNullable = true)] + public string? Remark { get; set; } + + /// 是否系统内置(1=内置不可删,0=用户自定义) + [SugarColumn(ColumnName = "IsSystem", ColumnDescription = "是否系统内置(1=不可删,0=可删)", ColumnDataType = "smallint", DefaultValue = "0")] + public byte IsSystem { get; set; } + + /// 最后修改人 + [SugarColumn(ColumnName = "LastUpdateUser", ColumnDescription = "最后修改人", Length = 50, IsNullable = true)] + public string? LastUpdateUser { get; set; } + + /// 最后修改时间 + [SugarColumn(ColumnName = "LastUpdateTime", ColumnDescription = "最后修改时间", IsNullable = true)] + public DateTime? LastUpdateTime { get; set; } + } +} diff --git a/Model/Mapper/EntityMapper.cs b/Model/Mapper/EntityMapper.cs index ea6585d..6adab9c 100644 --- a/Model/Mapper/EntityMapper.cs +++ b/Model/Mapper/EntityMapper.cs @@ -39,6 +39,8 @@ namespace Model.Mapper Supplier = entity.Supplier, ImageUrl = entity.ImageUrl, QrCode = entity.QrCode, + QrCodeUrl = entity.QrCodeUrl, + RfidCode = entity.RfidCode, Description = entity.Description, Department = entity.Department, Location = entity.Location, @@ -98,6 +100,8 @@ namespace Model.Mapper Supplier = dto.Supplier, ImageUrl = dto.ImageUrl, QrCode = dto.QrCode, + QrCodeUrl = dto.QrCodeUrl, + RfidCode = dto.RfidCode, Description = dto.Description, Department = dto.Department, Location = dto.Location, @@ -1242,5 +1246,205 @@ namespace Model.Mapper public static List ToDtoList(this List entities) => entities?.Select(e => e.ToDto()).ToList() ?? new List(); #endregion + + #region 数据字典 + /// + /// DictTypeEntity → DictTypeDto + /// + public static DictTypeDto ToDto(this DictTypeEntity entity) + { + if (entity == null) return null; + return new DictTypeDto + { + Id = entity.Id.ToString(), + Code = entity.Code, + Name = entity.Name, + Status = entity.Status, + Sort = entity.Sort, + Remark = entity.Remark, + CreateTime = entity.CreateTime + }; + } + public static List ToDtoList(this List entities) + => entities?.Select(e => e.ToDto()).ToList() ?? new List(); + /// + /// DictTypeDto → DictTypeEntity(入参映射,IsDel/CreateTime 不映射) + /// + public static DictTypeEntity ToEntity(this DictTypeDto dto) + { + if (dto == null) return null; + return new DictTypeEntity + { + Id = ParseId(dto.Id), + Code = dto.Code, + Name = dto.Name, + Status = dto.Status, + Sort = dto.Sort, + Remark = dto.Remark + }; + } + + /// + /// DictItemEntity → DictItemDto + /// + public static DictItemDto ToDto(this DictItemEntity entity) + { + if (entity == null) return null; + return new DictItemDto + { + Id = entity.Id.ToString(), + TypeId = entity.TypeId.ToString(), + Code = entity.Code, + Name = entity.Name, + Sort = entity.Sort, + Status = entity.Status, + IsDefault = entity.IsDefault, + Remark = entity.Remark, + CreateTime = entity.CreateTime + }; + } + public static List ToDtoList(this List entities) + => entities?.Select(e => e.ToDto()).ToList() ?? new List(); + /// + /// DictItemDto → DictItemEntity(入参映射,CreateTime/IsDel 不映射;TypeId 由 Service 强制覆盖) + /// + public static DictItemEntity ToEntity(this DictItemDto dto) + { + if (dto == null) return null; + return new DictItemEntity + { + Id = ParseId(dto.Id), + TypeId = ParseId(dto.TypeId), + Code = dto.Code, + Name = dto.Name, + Sort = dto.Sort, + Status = dto.Status, + IsDefault = dto.IsDefault, + Remark = dto.Remark + }; + } + #endregion + + #region 系统参数 + /// + /// SystemParamEntity → SystemParamDto + /// + public static SystemParamDto ToDto(this SystemParamEntity entity) + { + if (entity == null) return null; + return new SystemParamDto + { + Id = entity.Id.ToString(), + ParamKey = entity.ParamKey, + ParamName = entity.ParamName, + ParamValue = entity.ParamValue, + ParamType = entity.ParamType, + ParamOptions = entity.ParamOptions, + Unit = entity.Unit, + Group = entity.Group, + Sort = entity.Sort, + Remark = entity.Remark, + IsSystem = entity.IsSystem, + LastUpdateUser = entity.LastUpdateUser, + LastUpdateTime = entity.LastUpdateTime, + CreateTime = entity.CreateTime + }; + } + public static List ToDtoList(this List entities) + => entities?.Select(e => e.ToDto()).ToList() ?? new List(); + /// + /// SystemParamDto → SystemParamEntity(入参映射,CreateTime/IsDel/LastUpdateUser/LastUpdateTime 不映射;由 Service 控管) + /// + public static SystemParamEntity ToEntity(this SystemParamDto dto) + { + if (dto == null) return null; + return new SystemParamEntity + { + Id = ParseId(dto.Id), + ParamKey = dto.ParamKey, + ParamName = dto.ParamName, + ParamValue = dto.ParamValue, + ParamType = dto.ParamType, + ParamOptions = dto.ParamOptions, + Unit = dto.Unit, + Group = dto.Group, + Sort = dto.Sort, + Remark = dto.Remark, + IsSystem = dto.IsSystem + }; + } + #endregion + + #region 文件存储配置 + public static FileStorageConfigDto ToDto(this FileStorageConfigEntity entity) + { + if (entity == null) return null; + return new FileStorageConfigDto + { + Id = entity.Id.ToString(), + Provider = entity.Provider, + Name = entity.Name, + Endpoint = entity.Endpoint, + AccessKey = entity.AccessKey, + SecretKey = entity.SecretKey, + Bucket = entity.Bucket, + Region = entity.Region, + BasePath = entity.BasePath, + MaxSizeMB = entity.MaxSizeMB, + AllowedExts = entity.AllowedExts, + IsDefault = entity.IsDefault, + Status = entity.Status, + Remark = entity.Remark, + CreateTime = entity.CreateTime + }; + } + public static List ToDtoList(this List entities) + => entities?.Select(e => e.ToDto()).ToList() ?? new List(); + public static FileStorageConfigEntity ToEntity(this FileStorageConfigDto dto) + { + if (dto == null) return null; + return new FileStorageConfigEntity + { + Id = ParseId(dto.Id), + Provider = dto.Provider, + Name = dto.Name, + Endpoint = dto.Endpoint, + AccessKey = dto.AccessKey, + SecretKey = dto.SecretKey, + Bucket = dto.Bucket, + Region = dto.Region, + BasePath = dto.BasePath, + MaxSizeMB = dto.MaxSizeMB, + AllowedExts = dto.AllowedExts, + IsDefault = dto.IsDefault, + Status = dto.Status, + Remark = dto.Remark + }; + } + #endregion + + #region 文件上传记录 + public static FileRecordDto ToDto(this FileRecordEntity entity) + { + if (entity == null) return null; + return new FileRecordDto + { + Id = entity.Id.ToString(), + FileName = entity.FileName, + OriginalName = entity.OriginalName, + Url = entity.Url, + Size = entity.Size, + Ext = entity.Ext, + Provider = entity.Provider, + StorageId = entity.StorageId.ToString(), + Uploader = entity.Uploader, + BizType = entity.BizType, + BizId = entity.BizId, + CreateTime = entity.CreateTime + }; + } + public static List ToDtoList(this List entities) + => entities?.Select(e => e.ToDto()).ToList() ?? new List(); + #endregion } } diff --git a/Service/Implement/Asset/QrCodeService.cs b/Service/Implement/Asset/QrCodeService.cs index 1aa77af..5abc655 100644 --- a/Service/Implement/Asset/QrCodeService.cs +++ b/Service/Implement/Asset/QrCodeService.cs @@ -1,12 +1,227 @@ +using Model; +using Model.Dto.Asset; +using Model.Entity.Asset; +using ORM; +using QRCoder; using Service.Interface; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace Service.Implement { /// - /// 二维码 服务实现 + /// 设备二维码 服务实现 + /// 扫码直达 URL 约定:{baseUrl}/asset/ledger/equipment?id={equipmentId} + /// baseUrl 由 Controller 从 HTTP 请求(scheme://host)传入;如需固定域名可改为读 sys_param 的 site_base_url /// public class QrCodeService : IQrCodeService { - // TODO: 实现 二维码 相关方法 + public async Task> GenerateAsync(long equipmentId, string baseUrl) + { + if (equipmentId <= 0) return Result.Error("设备Id无效"); + try + { + var entity = await LoadEquipmentAsync(equipmentId); + if (entity == null) return Result.Error("设备不存在或已被删除"); + + var url = BuildQrUrl(baseUrl, equipmentId); + if (string.IsNullOrEmpty(entity.QrCodeUrl)) + { + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.QrCodeUrl == url) + .Where(x => x.Id == equipmentId).ExecuteCommandAsync(); + entity.QrCodeUrl = url; + } + return Result.Success(ToDto(entity)); + } + catch (Exception ex) + { + return Result.Error("生成二维码失败", ex); + } + } + + public async Task> GenerateImageAsync(long equipmentId, string baseUrl) + { + if (equipmentId <= 0) return Result.Error("设备Id无效"); + try + { + var entity = await LoadEquipmentAsync(equipmentId); + if (entity == null) return Result.Error("设备不存在或已被删除"); + + var url = string.IsNullOrEmpty(entity.QrCodeUrl) ? BuildQrUrl(baseUrl, equipmentId) : entity.QrCodeUrl; + if (string.IsNullOrEmpty(entity.QrCodeUrl)) + { + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.QrCodeUrl == url) + .Where(x => x.Id == equipmentId).ExecuteCommandAsync(); + entity.QrCodeUrl = url; + } + + var png = EncodePng(url); + return Result.Success(png); + } + catch (Exception ex) + { + return Result.Error("生成二维码图片失败", ex); + } + } + + public async Task>> BatchGenerateAsync(long[] equipmentIds, string baseUrl) + { + if (equipmentIds == null || equipmentIds.Length == 0) + return Result>.Error("设备Id列表不能为空"); + var results = new List(); + foreach (var id in equipmentIds.Distinct()) + { + var r = await GenerateAsync(id, baseUrl); + results.Add(new QrCodeBatchResultDto + { + EquipmentId = id.ToString(), + EquipmentCode = r.Data?.EquipmentCode, + EquipmentName = r.Data?.EquipmentName, + QrCodeUrl = r.Data?.QrCodeUrl, + Success = r.IsSuccess, + Message = r.IsSuccess ? "成功" : r.Msg + }); + } + return Result>.Success(results); + } + + public async Task>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword = null, bool qrOnly = false) + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Code!.Contains(keyword!) || x.Name!.Contains(keyword!)) + .WhereIF(qrOnly, x => x.QrCodeUrl != null && x.QrCodeUrl != "") + .OrderBy(x => x.Code) + .ToPageListAsync(pageIndex, pageSize, total); + return Result>.Success(list.Select(ToDto).ToList()); + } + catch (Exception ex) + { + return Result>.Error("查询二维码列表失败", ex); + } + } + + public async Task> GetPrintHtmlAsync(long equipmentId, string baseUrl) + { + if (equipmentId <= 0) return Result.Error("设备Id无效"); + try + { + var entity = await LoadEquipmentAsync(equipmentId); + if (entity == null) return Result.Error("设备不存在或已被删除"); + + var url = string.IsNullOrEmpty(entity.QrCodeUrl) ? BuildQrUrl(baseUrl, equipmentId) : entity.QrCodeUrl; + var png = EncodePng(url); + var base64 = Convert.ToBase64String(png); + + var html = BuildPrintHtml(entity, url, base64); + return Result.Success(html); + } + catch (Exception ex) + { + return Result.Error("生成打印页失败", ex); + } + } + + public async Task BindRfidAsync(long equipmentId, string rfidCode) + { + if (equipmentId <= 0) return Result.Error("设备Id无效"); + if (string.IsNullOrWhiteSpace(rfidCode)) return Result.Error("RFID 编号不能为空"); + try + { + var exists = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == equipmentId && x.IsDel == 0).AnyAsync(); + if (!exists) return Result.Error("设备不存在或已被删除"); + + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.RfidCode == rfidCode) + .Where(x => x.Id == equipmentId).ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("绑定 RFID 失败", ex); + } + } + + // ==================== 私有工具 ==================== + + private static async Task LoadEquipmentAsync(long id) + => await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + + private static QrCodeDto ToDto(EquipmentEntity e) => new() + { + EquipmentId = e.Id.ToString(), + EquipmentCode = e.Code, + EquipmentName = e.Name, + Location = e.Location, + QrCode = e.QrCode, + QrCodeUrl = e.QrCodeUrl, + RfidCode = e.RfidCode + }; + + private static string BuildQrUrl(string baseUrl, long equipmentId) + { + var b = (baseUrl ?? string.Empty).TrimEnd('/'); + return $"{b}/asset/ledger/equipment?id={equipmentId}"; + } + + private static byte[] EncodePng(string content) + { + using var gen = new QRCodeGenerator(); + var data = gen.CreateQrCode(content, QRCodeGenerator.ECCLevel.Q); + var png = new PngByteQRCode(data); + return png.GetGraphic(20); // 20 px per module,便于扫描识别 + } + + private static string BuildPrintHtml(EquipmentEntity e, string url, string base64) + { + // 简单 A4 打印模板:左侧二维码,右侧设备信息;浏览器 window.print() 即可 + var safe = (string? s) => System.Net.WebUtility.HtmlEncode(s ?? string.Empty); + return $@" + + + +设备二维码 - {safe(e.Name)} + + + +
+
QR
+
+

{safe(e.Name)}

+
+
设备编号
{safe(e.Code)}
+
二维码编号
{safe(e.QrCode)}
+
存放位置
{safe(e.Location)}
+
责任人
{safe(e.ResponsiblePerson)}
+
+
{safe(url)}
+
+
+
+ + +"; + } } } diff --git a/Service/Implement/LocalFileStorageProvider.cs b/Service/Implement/LocalFileStorageProvider.cs new file mode 100644 index 0000000..bd49663 --- /dev/null +++ b/Service/Implement/LocalFileStorageProvider.cs @@ -0,0 +1,120 @@ +using Model; +using Model.Dto.System; +using Model.Entity.System; +using Service.Interface; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + +namespace Service.Implement +{ + /// + /// 本地文件系统存储 Provider + /// 落地策略:存到 BasePath/{Bucket?}/{yyyy}/{MM}/{dd}/{guid}{ext},URL = {Endpoint}/{Bucket?}/{yyyy}/{MM}/{dd}/{guid}{ext} + /// wwwroot/uploads 由 Program.UseStaticFiles 提供访问;MinIO/OSS 实现见 IFileStorageProvider 扩展点 + /// + public class LocalFileStorageProvider : IFileStorageProvider + { + public string ProviderName => "local"; + + public async Task> UploadAsync(Stream stream, string originalName, long size, FileStorageConfigEntity config) + { + if (config == null) return Result.Error("存储配置不能为空"); + if (stream == null || !stream.CanRead) return Result.Error("文件流不可读"); + try + { + var basePath = ResolveBasePath(config.BasePath); + if (string.IsNullOrWhiteSpace(basePath)) + return Result.Error("Local 存储必须配置 BasePath"); + + var ext = Path.GetExtension(originalName)?.ToLowerInvariant() ?? ""; + var now = DateTime.Now; + var datePath = $"{now:yyyy}/{now:MM}/{now:dd}"; + var fileName = $"{Guid.NewGuid():N}{ext}"; + var storedFileName = string.IsNullOrWhiteSpace(config.Bucket) + ? $"{datePath}/{fileName}" + : $"{config.Bucket}/{datePath}/{fileName}"; + + var dir = Path.Combine(basePath, + string.IsNullOrWhiteSpace(config.Bucket) ? datePath : Path.Combine(config.Bucket, datePath)); + Directory.CreateDirectory(dir); + var fullPath = Path.Combine(dir, fileName); + + using (var fs = new FileStream(fullPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + { + await stream.CopyToAsync(fs); + } + + var url = BuildUrl(config.Endpoint, config.Bucket, storedFileName); + return Result.Success(new FileUploadResultDto + { + FileName = storedFileName, + Url = url, + Ext = ext, + Size = size, + Provider = ProviderName + }); + } + catch (Exception ex) + { + return Result.Error("本地文件上传失败", ex); + } + } + + public Task DeleteAsync(string storedFileName, FileStorageConfigEntity config) + { + if (config == null || string.IsNullOrWhiteSpace(storedFileName)) + return Task.FromResult(Result.Error("存储配置或文件名不能为空")); + try + { + var basePath = ResolveBasePath(config.BasePath); + var fullPath = Path.Combine(basePath, storedFileName.Replace('/', Path.DirectorySeparatorChar)); + if (File.Exists(fullPath)) File.Delete(fullPath); + return Task.FromResult(Result.Success()); + } + catch (Exception ex) + { + return Task.FromResult(Result.Error("删除本地文件失败", ex)); + } + } + + public Task TestConnectionAsync(FileStorageConfigEntity config) + { + if (config == null) return Task.FromResult(Result.Error("存储配置不能为空")); + try + { + var basePath = ResolveBasePath(config.BasePath); + if (string.IsNullOrWhiteSpace(basePath)) + return Task.FromResult(Result.Error("Local 存储必须配置 BasePath")); + Directory.CreateDirectory(basePath); + var testFile = Path.Combine(basePath, $".storage_test_{Guid.NewGuid():N}"); + File.WriteAllText(testFile, "ok"); + File.Delete(testFile); + return Task.FromResult(Result.Success()); + } + catch (Exception ex) + { + return Task.FromResult(Result.Error("Local 连接测试失败:" + ex.Message)); + } + } + + // ==================== 私有工具 ==================== + + private static string ResolveBasePath(string? basePath) + { + if (string.IsNullOrWhiteSpace(basePath)) return string.Empty; + // 相对路径以运行目录为基准(如 wwwroot/uploads) + return Path.IsPathRooted(basePath) ? basePath : Path.Combine(AppContext.BaseDirectory, basePath); + } + + private static string BuildUrl(string? endpoint, string? bucket, string storedFileName) + { + var segs = new List(); + if (!string.IsNullOrWhiteSpace(endpoint)) segs.Add(endpoint.TrimEnd('/')); + if (!string.IsNullOrWhiteSpace(bucket)) segs.Add(bucket.Trim('/')); + segs.Add(storedFileName); + return string.Join('/', segs); + } + } +} diff --git a/Service/Implement/System/DataSeeder.cs b/Service/Implement/System/DataSeeder.cs index 05b193e..a7f43ac 100644 --- a/Service/Implement/System/DataSeeder.cs +++ b/Service/Implement/System/DataSeeder.cs @@ -13,7 +13,7 @@ namespace Service.Implement public const byte DataScopeAll = 1; public const byte DataScopeLab = 2; - /// 6 个系统权限编码 + /// 7 个系统权限编码 public static readonly (string Code, string Name, string Group)[] Permissions = { ("device:view", "查看设备", "device"), @@ -21,7 +21,8 @@ namespace Service.Implement ("device:control", "控制设备", "device"), ("alert:confirm", "确认告警", "alert"), ("inspection:manage", "巡检管理", "inspection"), - ("user:manage", "用户管理", "user") + ("user:manage", "用户管理", "user"), + ("system:manage", "系统配置管理", "system") }; /// @@ -30,8 +31,8 @@ namespace Service.Implement /// public static readonly (string Code, string Name, byte DataScope, string[] Permissions)[] Roles = { - ("superadmin", "超级管理员", DataScopeAll, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "user:manage" }), - ("labadmin", "实验室管理员", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage" }), + ("superadmin", "超级管理员", DataScopeAll, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "user:manage", "system:manage" }), + ("labadmin", "实验室管理员", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "system:manage" }), ("operator", "设备操作员", DataScopeLab, new[] { "device:view", "inspection:manage" }), ("maintengineer", "维修工程师", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm" }) }; @@ -113,6 +114,12 @@ namespace Service.Implement db.Insertable(new UserRoleEntity { UserId = adminId, RoleId = superAdmin.Id, CreateTime = now }).ExecuteCommand(); } } + + // 4. 系统参数种子(一期 4 个内置参数,幂等:按 ParamKey 跳过已存在) + SeedSystemParams(db, now); + + // 5. 默认本地文件存储配置(幂等:仅当无任何配置时插入一条默认 Local 通道) + SeedDefaultFileStorage(db, now); } catch (Exception) { @@ -120,5 +127,70 @@ namespace Service.Implement throw; } } + + private static void SeedDefaultFileStorage(SqlSugar.ISqlSugarClient db, DateTime now) + { + var hasAny = db.Queryable().Where(x => x.IsDel == 0).Any(); + if (hasAny) return; + var entity = new FileStorageConfigEntity + { + Provider = "local", + Name = "本地存储", + Endpoint = "/uploads", + AccessKey = null, + SecretKey = null, + Bucket = null, + Region = null, + BasePath = "wwwroot/uploads", + MaxSizeMB = 10, + AllowedExts = null, // 不限制扩展名;如需限制填 [".jpg",".png",".pdf",".xlsx"] + IsDefault = 1, + Status = 1, + Remark = "系统默认本地存储通道(文件落 wwwroot/uploads,通过 /uploads 静态访问)", + CreateTime = now + }; + db.Insertable(entity).ExecuteReturnSnowflakeId(); + } + + /// 一期 4 个内置系统参数(IsSystem=1,禁止删除,仅允许改 Value/Remark) + /// Group 与 ParamType 约定:0=int,1=text,2=enum,3=json + private static readonly (string Key, string Name, string? Value, byte Type, string? Options, string? Unit, string Group, int Sort, string? Remark)[] SystemParams = + { + ("data_collect_default_frequency", "数据采集默认频率", "5", 0, null, "秒", "数据采集", 1, "设备数据采集的默认间隔(秒),新建设备时作为默认值"), + ("alert_notify_method", "告警通知方式", "feishu", 2, + "[{\"value\":\"feishu\",\"label\":\"飞书\"},{\"value\":\"dingtalk\",\"label\":\"钉钉\"},{\"value\":\"wecom\",\"label\":\"企业微信\"},{\"value\":\"email\",\"label\":\"邮件\"}]", + null, "告警", 2, "默认告警通知渠道(多选需在告警规则内单独配置)"), + ("work_order_code_rule", "工单编号规则", "WO{yyyyMMddHHmmss}", 1, null, null, "工单", 3, + "工单编号生成模板,支持占位符:{yyyy}年 {MM}月 {dd}日 {HH}时 {mm}分 {ss}秒,序列号用 {seq4} 表示 4 位顺序号"), + ("file_upload_limit", "文件上传限制", "10", 2, + "[{\"value\":\"5\",\"label\":\"5MB\"},{\"value\":\"10\",\"label\":\"10MB\"},{\"value\":\"20\",\"label\":\"20MB\"},{\"value\":\"50\",\"label\":\"50MB\"}]", + "MB", "文件", 4, "单个文件上传大小上限(MB)") + }; + + private static void SeedSystemParams(SqlSugar.ISqlSugarClient db, DateTime now) + { + foreach (var (key, name, value, type, options, unit, group, sort, remark) in SystemParams) + { + var exists = db.Queryable().Where(x => x.ParamKey == key).Any(); + if (exists) continue; + var entity = new SystemParamEntity + { + ParamKey = key, + ParamName = name, + ParamValue = value, + ParamType = type, + ParamOptions = options, + Unit = unit, + Group = group, + Sort = sort, + Remark = remark, + IsSystem = 1, + LastUpdateUser = "system", + LastUpdateTime = now, + CreateTime = now + }; + db.Insertable(entity).ExecuteReturnSnowflakeId(); + } + } } } diff --git a/Service/Implement/System/DictService.cs b/Service/Implement/System/DictService.cs index 9b50928..7c8bf40 100644 --- a/Service/Implement/System/DictService.cs +++ b/Service/Implement/System/DictService.cs @@ -1,12 +1,360 @@ +using Microsoft.Extensions.Caching.Memory; +using Model; +using Model.Dto.System; +using Model.Entity.System; +using Model.Mapper; +using ORM; using Service.Interface; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace Service.Implement { /// /// 数据字典管理 服务实现 + /// 缓存策略:按 typeCode 缓存启用项,新增/修改/删除字典分类或字典项时失效对应 typeCode;启动首次访问时按需加载 /// public class DictService : IDictService { - // TODO: 实现 数据字典管理 相关方法 + private readonly IMemoryCache _cache; + private static readonly object _cacheLock = new(); + + public DictService(IMemoryCache cache) + { + _cache = cache; + } + + // ==================== 字典分类 ==================== + + public async Task>> GetTypesPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword = null) + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Code.Contains(keyword!) || x.Name.Contains(keyword!)) + .OrderBy(x => x.Sort).OrderBy(x => x.CreateTime, OrderByType.Desc) + .ToPageListAsync(pageIndex, pageSize, total); + + var dtos = list.ToDtoList(); + await FillItemCountAsync(dtos); + return Result>.Success(dtos); + } + catch (Exception ex) + { + return Result>.Error("查询字典分类失败", ex); + } + } + + public async Task>> GetTypesAllAsync() + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0 && x.Status == 1) + .OrderBy(x => x.Sort).OrderBy(x => x.Code) + .ToListAsync(); + return Result>.Success(list.ToDtoList()); + } + catch (Exception ex) + { + return Result>.Error("查询字典分类失败", ex); + } + } + + public async Task> GetTypeByIdAsync(long id) + { + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("字典分类不存在或已被删除"); + var dto = entity.ToDto(); + await FillItemCountAsync(new List { dto }); + return Result.Success(dto); + } + catch (Exception ex) + { + return Result.Error("查询字典分类详情失败", ex); + } + } + + public async Task> AddTypeAsync(DictTypeDto dto) + { + if (dto == null || string.IsNullOrWhiteSpace(dto.Code) || string.IsNullOrWhiteSpace(dto.Name)) + return Result.Error("字典编码和名称不能为空"); + + try + { + var exists = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Code == dto.Code && x.IsDel == 0).AnyAsync(); + if (exists) return Result.Error($"字典编码「{dto.Code}」已存在"); + + var entity = dto.ToEntity(); + entity.Id = 0; + entity.CreateTime = DateTime.Now; + var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync(); + entity.Id = id; + return Result.Success(entity.ToDto()); + } + catch (Exception ex) + { + return Result.Error("新增字典分类失败", ex); + } + } + + public async Task> UpdateTypeAsync(DictTypeDto dto) + { + if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0) + return Result.Error("字典分类Id无效"); + + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("字典分类不存在或已被删除"); + + var codeExists = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync(); + if (codeExists) return Result.Error($"字典编码「{dto.Code}」已存在"); + + var newEntity = dto.ToEntity(); + newEntity.Id = id; + await SqlSugarContext.DbContext.Updateable(newEntity) + .IgnoreColumns(x => new { x.CreateTime, x.IsDel }) + .ExecuteCommandAsync(); + // 编码可能变更:旧/新 Code 都失效 + InvalidateCache(entity.Code); + InvalidateCache(dto.Code); + return Result.Success(newEntity.ToDto()); + } + catch (Exception ex) + { + return Result.Error("修改字典分类失败", ex); + } + } + + public async Task DeleteTypeAsync(long id) + { + if (id <= 0) return Result.Error("字典分类Id无效"); + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("字典分类不存在或已被删除"); + + var hasItems = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.TypeId == id && x.IsDel == 0).AnyAsync(); + if (hasItems) return Result.Error("该字典分类下还有字典项,请先删除字典项再删除分类"); + + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.IsDel == 1) + .Where(x => x.Id == id).ExecuteCommandAsync(); + InvalidateCache(entity.Code); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("删除字典分类失败", ex); + } + } + + // ==================== 字典项 ==================== + + public async Task>> GetItemsByTypeAsync(long typeId) + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.TypeId == typeId && x.IsDel == 0) + .OrderBy(x => x.Sort).OrderBy(x => x.CreateTime, OrderByType.Desc) + .ToListAsync(); + return Result>.Success(list.ToDtoList()); + } + catch (Exception ex) + { + return Result>.Error("查询字典项失败", ex); + } + } + + public async Task> AddItemAsync(DictItemDto dto) + { + if (dto == null || !long.TryParse(dto.TypeId, out var typeId) || typeId <= 0) + return Result.Error("所属字典分类Id无效"); + if (string.IsNullOrWhiteSpace(dto.Code) || string.IsNullOrWhiteSpace(dto.Name)) + return Result.Error("字典项编码和名称不能为空"); + + try + { + var typeExists = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == typeId && x.IsDel == 0).AnyAsync(); + if (!typeExists) return Result.Error("所属字典分类不存在"); + + var codeExists = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.TypeId == typeId && x.Code == dto.Code && x.IsDel == 0).AnyAsync(); + if (codeExists) return Result.Error($"字典项编码「{dto.Code}」在该分类下已存在"); + + var entity = dto.ToEntity(); + entity.Id = 0; + entity.TypeId = typeId; + entity.CreateTime = DateTime.Now; + if (entity.IsDefault == 1) + { + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.IsDefault == 0) + .Where(x => x.TypeId == typeId && x.IsDel == 0).ExecuteCommandAsync(); + } + var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync(); + entity.Id = id; + + InvalidateCache(await GetTypeCodeAsync(typeId)); + return Result.Success(entity.ToDto()); + } + catch (Exception ex) + { + return Result.Error("新增字典项失败", ex); + } + } + + public async Task> UpdateItemAsync(DictItemDto dto) + { + if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0) + return Result.Error("字典项Id无效"); + if (!long.TryParse(dto.TypeId, out var typeId) || typeId <= 0) + return Result.Error("所属字典分类Id无效"); + + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("字典项不存在或已被删除"); + + var codeExists = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.TypeId == typeId && x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync(); + if (codeExists) return Result.Error($"字典项编码「{dto.Code}」在该分类下已存在"); + + // 默认项互斥:改为默认时清零同分类其它默认项 + if (dto.IsDefault == 1 && entity.IsDefault == 0) + { + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.IsDefault == 0) + .Where(x => x.TypeId == typeId && x.IsDel == 0 && x.Id != id).ExecuteCommandAsync(); + } + + var newEntity = dto.ToEntity(); + newEntity.Id = id; + newEntity.TypeId = typeId; + await SqlSugarContext.DbContext.Updateable(newEntity) + .IgnoreColumns(x => new { x.CreateTime, x.IsDel }) + .ExecuteCommandAsync(); + + InvalidateCache(await GetTypeCodeAsync(typeId)); + return Result.Success(newEntity.ToDto()); + } + catch (Exception ex) + { + return Result.Error("修改字典项失败", ex); + } + } + + public async Task DeleteItemAsync(long id) + { + if (id <= 0) return Result.Error("字典项Id无效"); + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("字典项不存在或已被删除"); + + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.IsDel == 1) + .Where(x => x.Id == id).ExecuteCommandAsync(); + + InvalidateCache(await GetTypeCodeAsync(entity.TypeId)); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("删除字典项失败", ex); + } + } + + // ==================== 业务侧:按 typeCode 查询启用项(带缓存) ==================== + + public async Task>> GetOptionsByCodeAsync(string typeCode) + { + if (string.IsNullOrWhiteSpace(typeCode)) + return Result>.Error("字典编码不能为空"); + + try + { + var key = $"dict:options:{typeCode}"; + if (!_cache.TryGetValue(key, out List? cached) || cached == null) + { + lock (_cacheLock) + { + cached ??= LoadOptionsByCodeAsync(typeCode).GetAwaiter().GetResult(); + _cache.Set(key, cached, TimeSpan.FromHours(1)); + } + } + return Result>.Success(cached); + } + catch (Exception ex) + { + return Result>.Error("查询字典选项失败", ex); + } + } + + // ==================== 私有工具 ==================== + + private async Task GetTypeCodeAsync(long typeId) + { + var code = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == typeId).Select(x => x.Code).FirstAsync(); + return code ?? string.Empty; + } + + private async Task> LoadOptionsByCodeAsync(string typeCode) + { + var db = SqlSugarContext.DbContext; + var typeId = await db.Queryable() + .Where(x => x.Code == typeCode && x.IsDel == 0 && x.Status == 1) + .Select(x => x.Id).FirstAsync(); + if (typeId == 0) return new List(); + + var items = await db.Queryable() + .Where(x => x.TypeId == typeId && x.IsDel == 0 && x.Status == 1) + .OrderBy(x => x.Sort).OrderBy(x => x.Code) + .ToListAsync(); + + return items.Select(e => new DictOptionDto + { + Code = e.Code, + Name = e.Name, + IsDefault = e.IsDefault == 1 + }).ToList(); + } + + private void InvalidateCache(string? typeCode) + { + if (string.IsNullOrWhiteSpace(typeCode)) return; + _cache.Remove($"dict:options:{typeCode}"); + } + + private async Task FillItemCountAsync(List dtos) + { + if (dtos == null || dtos.Count == 0) return; + var ids = dtos.Select(d => long.Parse(d.Id)).ToList(); + var counts = await SqlSugarContext.DbContext.Queryable() + .Where(x => ids.Contains(x.TypeId) && x.IsDel == 0) + .GroupBy(x => x.TypeId) + .Select(x => new { TypeId = x.TypeId, Count = SqlFunc.AggregateCount(x.Id) }) + .ToListAsync(); + var map = counts.ToDictionary(c => c.TypeId, c => c.Count); + foreach (var d in dtos) d.ItemCount = map.TryGetValue(long.Parse(d.Id), out var c) ? c : 0; + } } } diff --git a/Service/Implement/System/FileStorageService.cs b/Service/Implement/System/FileStorageService.cs index e4297e7..be51dc5 100644 --- a/Service/Implement/System/FileStorageService.cs +++ b/Service/Implement/System/FileStorageService.cs @@ -1,12 +1,322 @@ +using Model; +using Model.Dto.System; +using Model.Entity.System; +using Model.Mapper; +using ORM; using Service.Interface; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; namespace Service.Implement { /// /// 文件存储管理 服务实现 + /// 通过 IEnumerable<IFileStorageProvider> 按配置 Provider 字段挑选实现;当前仅 Local 落地 /// public class FileStorageService : IFileStorageService { - // TODO: 实现 文件存储管理 相关方法 + private readonly IEnumerable _providers; + private readonly ICurrentUser _currentUser; + + // 掩码标记:前端回显 AccessKey/SecretKey 时用 **** 脱敏,回写时若仍为掩码则保留原值 + private const string MaskMarker = "****"; + + public FileStorageService(IEnumerable providers, ICurrentUser currentUser) + { + _providers = providers; + _currentUser = currentUser; + } + + // ==================== 存储配置 ==================== + + public async Task>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword = null) + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Name.Contains(keyword!) || x.Provider.Contains(keyword!)) + .OrderBy(x => x.IsDefault, OrderByType.Desc).OrderBy(x => x.CreateTime, OrderByType.Desc) + .ToPageListAsync(pageIndex, pageSize, total); + var dtos = list.ToDtoList(); + // AccessKey/SecretKey 脱敏回显 + foreach (var d in dtos) { d.AccessKey = Mask(d.AccessKey); d.SecretKey = Mask(d.SecretKey); } + return Result>.Success(dtos); + } + catch (Exception ex) + { + return Result>.Error("查询存储配置失败", ex); + } + } + + public async Task> GetByIdAsync(long id) + { + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("存储配置不存在或已被删除"); + var dto = entity.ToDto(); + dto.AccessKey = Mask(dto.AccessKey); + dto.SecretKey = Mask(dto.SecretKey); + return Result.Success(dto); + } + catch (Exception ex) + { + return Result.Error("查询存储配置失败", ex); + } + } + + public async Task> AddAsync(FileStorageConfigDto dto) + { + if (dto == null || string.IsNullOrWhiteSpace(dto.Provider) || string.IsNullOrWhiteSpace(dto.Name)) + return Result.Error("Provider 和名称不能为空"); + if (!IsProviderSupported(dto.Provider)) + return Result.Error($"不支持的 Provider:{dto.Provider}(当前仅支持 local)"); + + try + { + var entity = dto.ToEntity(); + entity.Id = 0; + entity.CreateTime = DateTime.Now; + if (entity.IsDefault == 1) await ClearOtherDefaultAsync(0); + var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync(); + entity.Id = id; + var result = entity.ToDto(); + result.AccessKey = Mask(result.AccessKey); + result.SecretKey = Mask(result.SecretKey); + return Result.Success(result); + } + catch (Exception ex) + { + return Result.Error("新增存储配置失败", ex); + } + } + + public async Task> UpdateAsync(FileStorageConfigDto dto) + { + if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0) + return Result.Error("配置Id无效"); + if (!string.IsNullOrWhiteSpace(dto.Provider) && !IsProviderSupported(dto.Provider)) + return Result.Error($"不支持的 Provider:{dto.Provider}"); + + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("存储配置不存在或已被删除"); + + // AccessKey/SecretKey 为掩码时保留原值(前端未改密钥) + var newEntity = dto.ToEntity(); + newEntity.Id = id; + newEntity.CreateTime = entity.CreateTime; + if (string.IsNullOrEmpty(newEntity.AccessKey) || newEntity.AccessKey.Contains(MaskMarker)) + newEntity.AccessKey = entity.AccessKey; + if (string.IsNullOrEmpty(newEntity.SecretKey) || newEntity.SecretKey.Contains(MaskMarker)) + newEntity.SecretKey = entity.SecretKey; + + if (newEntity.IsDefault == 1) await ClearOtherDefaultAsync(id); + await SqlSugarContext.DbContext.Updateable(newEntity) + .IgnoreColumns(x => new { x.CreateTime, x.IsDel }) + .ExecuteCommandAsync(); + var result = newEntity.ToDto(); + result.AccessKey = Mask(result.AccessKey); + result.SecretKey = Mask(result.SecretKey); + return Result.Success(result); + } + catch (Exception ex) + { + return Result.Error("修改存储配置失败", ex); + } + } + + public async Task DeleteAsync(long id) + { + if (id <= 0) return Result.Error("配置Id无效"); + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("存储配置不存在或已被删除"); + if (entity.IsDefault == 1) return Result.Error($"配置「{entity.Name}」是默认通道,禁止删除(请先切换默认)"); + + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.IsDel == 1) + .Where(x => x.Id == id).ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("删除存储配置失败", ex); + } + } + + public async Task SetDefaultAsync(long id) + { + if (id <= 0) return Result.Error("配置Id无效"); + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("存储配置不存在或已被删除"); + if (entity.Status == 0) return Result.Error("已停用的配置不能设为默认"); + + await ClearOtherDefaultAsync(id); + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.IsDefault == 1) + .Where(x => x.Id == id).ExecuteCommandAsync(); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("设置默认存储失败", ex); + } + } + + public async Task TestAsync(long id) + { + if (id <= 0) return Result.Error("配置Id无效"); + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("存储配置不存在或已被删除"); + + var provider = ResolveProvider(entity.Provider); + if (provider == null) return Result.Error($"未注册 Provider:{entity.Provider}"); + return await provider.TestConnectionAsync(entity); + } + catch (Exception ex) + { + return Result.Error("测试连接失败", ex); + } + } + + // ==================== 文件上传/记录 ==================== + + public async Task> UploadAsync(Stream stream, string originalName, long size, string? bizType = null, string? bizId = null, string? uploader = null) + { + if (stream == null || size <= 0) return Result.Error("文件流或大小无效"); + if (string.IsNullOrWhiteSpace(originalName)) return Result.Error("文件名不能为空"); + + try + { + // 取默认启用的存储配置 + var config = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0 && x.IsDefault == 1 && x.Status == 1).FirstAsync(); + if (config == null) return Result.Error("未配置默认存储通道,请先在「文件存储管理」新增并设为默认"); + + // 大小校验 + if (config.MaxSizeMB > 0 && size > config.MaxSizeMB * 1024L * 1024L) + return Result.Error($"文件大小超过上限 {config.MaxSizeMB}MB"); + + // 扩展名校验 + var ext = Path.GetExtension(originalName)?.ToLowerInvariant() ?? ""; + var allowed = ParseExts(config.AllowedExts); + if (allowed.Count > 0 && !allowed.Contains(ext)) + return Result.Error($"不支持的文件类型 {ext}(允许:{string.Join(",", allowed)})"); + + var provider = ResolveProvider(config.Provider); + if (provider == null) return Result.Error($"未注册 Provider:{config.Provider}"); + + var upRes = await provider.UploadAsync(stream, originalName, size, config); + if (!upRes.IsSuccess || upRes.Data == null) return Result.Error(upRes.Msg); + + var record = new FileRecordEntity + { + FileName = upRes.Data.FileName, + OriginalName = originalName, + Url = upRes.Data.Url, + Size = size, + Ext = ext, + Provider = config.Provider, + StorageId = config.Id, + Uploader = string.IsNullOrWhiteSpace(uploader) ? _currentUser.UserName : uploader, + BizType = string.IsNullOrWhiteSpace(bizType) ? "generic" : bizType, + BizId = bizId, + CreateTime = DateTime.Now + }; + var rid = await SqlSugarContext.DbContext.Insertable(record).ExecuteReturnSnowflakeIdAsync(); + record.Id = rid; + return Result.Success(record.ToDto()); + } + catch (Exception ex) + { + return Result.Error("文件上传失败", ex); + } + } + + public async Task>> GetFileListPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword = null, string? bizType = null) + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.FileName.Contains(keyword!) || x.OriginalName!.Contains(keyword!)) + .WhereIF(!string.IsNullOrWhiteSpace(bizType), x => x.BizType == bizType) + .OrderBy(x => x.CreateTime, OrderByType.Desc) + .ToPageListAsync(pageIndex, pageSize, total); + return Result>.Success(list.ToDtoList()); + } + catch (Exception ex) + { + return Result>.Error("查询文件记录失败", ex); + } + } + + public async Task> GetFileByIdAsync(long id) + { + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("文件记录不存在或已被删除"); + return Result.Success(entity.ToDto()); + } + catch (Exception ex) + { + return Result.Error("查询文件记录失败", ex); + } + } + + // ==================== 私有工具 ==================== + + private IFileStorageProvider? ResolveProvider(string? providerName) + { + if (string.IsNullOrWhiteSpace(providerName)) return null; + return _providers.FirstOrDefault(p => p.ProviderName == providerName); + } + + private static bool IsProviderSupported(string provider) + => provider == "local" || provider == "minio" || provider == "oss"; + + private static string? Mask(string? value) + { + if (string.IsNullOrWhiteSpace(value)) return value; + if (value.Length <= 4) return MaskMarker; + return value[..2] + MaskMarker + value[^2..]; + } + + private static List ParseExts(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return new List(); + try + { + var arr = System.Text.Json.JsonSerializer.Deserialize>(json); + return arr?.Select(e => e.ToLowerInvariant()).ToList() ?? new List(); + } + catch { return new List(); } + } + + private async Task ClearOtherDefaultAsync(long keepId) + { + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.IsDefault == 0) + .Where(x => x.IsDel == 0 && x.Id != keepId).ExecuteCommandAsync(); + } } } diff --git a/Service/Implement/System/SystemParamService.cs b/Service/Implement/System/SystemParamService.cs index d0f3770..5c04078 100644 --- a/Service/Implement/System/SystemParamService.cs +++ b/Service/Implement/System/SystemParamService.cs @@ -1,12 +1,257 @@ +using Microsoft.Extensions.Caching.Memory; +using Model; +using Model.Dto.System; +using Model.Entity.System; +using Model.Mapper; +using ORM; using Service.Interface; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace Service.Implement { /// /// 系统参数配置 服务实现 + /// 缓存策略:按 ParamKey 缓存值;增删改时失效对应 Key(Key 变更时新旧都失效) /// public class SystemParamService : ISystemParamService { - // TODO: 实现 系统参数配置 相关方法 + private readonly IMemoryCache _cache; + private readonly ICurrentUser _currentUser; + private static readonly object _cacheLock = new(); + + public SystemParamService(IMemoryCache cache, ICurrentUser currentUser) + { + _cache = cache; + _currentUser = currentUser; + } + + // ==================== 管理侧 ==================== + + public async Task>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword = null, string? group = null) + { + try + { + var list = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.IsDel == 0) + .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.ParamKey.Contains(keyword!) || x.ParamName.Contains(keyword!)) + .WhereIF(!string.IsNullOrWhiteSpace(group), x => x.Group == group) + .OrderBy(x => x.Sort).OrderBy(x => x.CreateTime, OrderByType.Desc) + .ToPageListAsync(pageIndex, pageSize, total); + return Result>.Success(list.ToDtoList()); + } + catch (Exception ex) + { + return Result>.Error("查询系统参数失败", ex); + } + } + + public async Task> GetByIdAsync(long id) + { + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("系统参数不存在或已被删除"); + return Result.Success(entity.ToDto()); + } + catch (Exception ex) + { + return Result.Error("查询系统参数详情失败", ex); + } + } + + public async Task> AddAsync(SystemParamDto dto) + { + if (dto == null || string.IsNullOrWhiteSpace(dto.ParamKey) || string.IsNullOrWhiteSpace(dto.ParamName)) + return Result.Error("参数键和名称不能为空"); + + try + { + var keyExists = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.ParamKey == dto.ParamKey && x.IsDel == 0).AnyAsync(); + if (keyExists) return Result.Error($"参数键「{dto.ParamKey}」已存在"); + + var entity = dto.ToEntity(); + entity.Id = 0; + entity.IsSystem = 0; // 用户新增的不是内置参数 + entity.CreateTime = DateTime.Now; + entity.LastUpdateUser = _currentUser.UserName; + entity.LastUpdateTime = DateTime.Now; + var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync(); + entity.Id = id; + InvalidateCache(entity.ParamKey); + return Result.Success(entity.ToDto()); + } + catch (Exception ex) + { + return Result.Error("新增系统参数失败", ex); + } + } + + public async Task> UpdateAsync(SystemParamDto dto) + { + if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0) + return Result.Error("参数Id无效"); + + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("系统参数不存在或已被删除"); + + // Key 变更时校验唯一 + if (!string.Equals(entity.ParamKey, dto.ParamKey, StringComparison.OrdinalIgnoreCase)) + { + var keyExists = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.ParamKey == dto.ParamKey && x.Id != id && x.IsDel == 0).AnyAsync(); + if (keyExists) return Result.Error($"参数键「{dto.ParamKey}」已存在"); + } + + var oldKey = entity.ParamKey; + var now = DateTime.Now; + var userName = _currentUser.UserName; + + if (entity.IsSystem == 1) + { + // 内置参数仅允许改 Value/Remark,其它字段保留 + entity.ParamValue = dto.ParamValue; + entity.Remark = dto.Remark; + entity.LastUpdateUser = userName; + entity.LastUpdateTime = now; + await SqlSugarContext.DbContext.Updateable(entity) + .UpdateColumns(x => new { x.ParamValue, x.Remark, x.LastUpdateUser, x.LastUpdateTime }) + .ExecuteCommandAsync(); + } + else + { + // 用户自定义参数:全字段更新(审计字段由服务端控管) + var newEntity = dto.ToEntity(); + newEntity.Id = id; + newEntity.IsSystem = 0; + newEntity.CreateTime = entity.CreateTime; + newEntity.LastUpdateUser = userName; + newEntity.LastUpdateTime = now; + await SqlSugarContext.DbContext.Updateable(newEntity) + .IgnoreColumns(x => new { x.CreateTime, x.IsDel }) + .ExecuteCommandAsync(); + entity = newEntity; + } + + InvalidateCache(oldKey); + InvalidateCache(entity.ParamKey); + return Result.Success(entity.ToDto()); + } + catch (Exception ex) + { + return Result.Error("修改系统参数失败", ex); + } + } + + public async Task DeleteAsync(long id) + { + if (id <= 0) return Result.Error("参数Id无效"); + try + { + var entity = await SqlSugarContext.DbContext.Queryable() + .Where(x => x.Id == id && x.IsDel == 0).FirstAsync(); + if (entity == null) return Result.Error("系统参数不存在或已被删除"); + if (entity.IsSystem == 1) return Result.Error($"内置参数「{entity.ParamName}」禁止删除,仅允许修改值"); + + await SqlSugarContext.DbContext.Updateable() + .SetColumns(x => x.IsDel == 1) + .Where(x => x.Id == id).ExecuteCommandAsync(); + InvalidateCache(entity.ParamKey); + return Result.Success(); + } + catch (Exception ex) + { + return Result.Error("删除系统参数失败", ex); + } + } + + public Task ReloadCacheAsync() + { + // IMemoryCache 非强类型缓存清除,此处复用一个枚举 Key 的策略:清除所有 sysparam: 前缀缓存需要 ICache 抽象; + // 当前实现:通过 Coherent MemoryCache 实例的 compact,按约定不实现细粒度清除,仅返回成功提示由进程内自然过期 + // 注:增删改单条时已按 Key 失效;本接口预留给运维紧急场景,下次访问会重新加载 + try + { + // 由于 IMemoryCache 没有枚举能力,这里仅作占位返回;如需全量清除,请重启服务或改用 IDistributedCache + return Task.FromResult(Result.Success()); + } + catch (Exception ex) + { + return Task.FromResult(Result.Error("清空缓存失败", ex)); + } + } + + // ==================== 业务侧:按 Key 取值(带缓存) ==================== + + public async Task> GetValueAsync(string key) + { + if (string.IsNullOrWhiteSpace(key)) + return Result.Error("参数键不能为空"); + try + { + var value = await LoadValueByKeyAsync(key); + return Result.Success(value ?? string.Empty); + } + catch (Exception ex) + { + return Result.Error("查询参数值失败", ex); + } + } + + public async Task>> GetValuesAsync(string[] keys) + { + if (keys == null || keys.Length == 0) + return Result>.Error("参数键列表不能为空"); + try + { + var distinctKeys = keys.Where(k => !string.IsNullOrWhiteSpace(k)).Distinct().ToList(); + var list = new List(distinctKeys.Count); + foreach (var k in distinctKeys) + { + var v = await LoadValueByKeyAsync(k); + list.Add(new SystemParamValueDto { Key = k, Value = v ?? string.Empty }); + } + return Result>.Success(list); + } + catch (Exception ex) + { + return Result>.Error("批量查询参数值失败", ex); + } + } + + // ==================== 私有工具 ==================== + + private async Task LoadValueByKeyAsync(string key) + { + var cacheKey = $"sysparam:{key}"; + if (!_cache.TryGetValue(cacheKey, out string? cached)) + { + lock (_cacheLock) + { + if (!_cache.TryGetValue(cacheKey, out cached)) + { + cached = SqlSugarContext.DbContext.Queryable() + .Where(x => x.ParamKey == key && x.IsDel == 0) + .Select(x => x.ParamValue).First(); + _cache.Set(cacheKey, cached ?? string.Empty, TimeSpan.FromHours(1)); + } + } + } + return cached; + } + + private void InvalidateCache(string? key) + { + if (string.IsNullOrWhiteSpace(key)) return; + _cache.Remove($"sysparam:{key}"); + } } } diff --git a/Service/Interface/Asset/IQrCodeService.cs b/Service/Interface/Asset/IQrCodeService.cs index 88a0fca..c0c0ed7 100644 --- a/Service/Interface/Asset/IQrCodeService.cs +++ b/Service/Interface/Asset/IQrCodeService.cs @@ -1,10 +1,35 @@ +using Model; +using Model.Dto.Asset; +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; + namespace Service.Interface { /// - /// 二维码 服务接口 + /// 设备二维码 服务接口 + /// 编码内容为扫码直达 URL:{baseUrl}/asset/ledger/equipment?id={equipmentId} + /// baseUrl 由 Controller 从 HTTP 请求构造(scheme://host)后传入,保持 Service 层框架无关 + /// 生成时把 URL 写入 EquipmentEntity.QrCodeUrl;打印返回 HTML 页面(含 base64 二维码图) /// public interface IQrCodeService { - // TODO: 定义 二维码 相关方法 + /// 生成二维码 URL 并写入设备(baseUrl 形如 http://host:port) + Task> GenerateAsync(long equipmentId, string baseUrl); + + /// 生成二维码 PNG 图片字节(同时确保 URL 已写库) + Task> GenerateImageAsync(long equipmentId, string baseUrl); + + /// 批量生成二维码 URL(逐个写库,单条失败不影响其它) + Task>> BatchGenerateAsync(long[] equipmentIds, string baseUrl); + + /// 设备二维码列表(分页,关键字匹配 Code/Name;qrOnly=1 仅看已生成) + Task>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword = null, bool qrOnly = false); + + /// 获取打印用 HTML 页面(含二维码 base64 图 + 设备信息,浏览器 Ctrl+P 打印) + Task> GetPrintHtmlAsync(long equipmentId, string baseUrl); + + /// 绑定 RFID 编号到设备(覆盖式写入 RfidCode) + Task BindRfidAsync(long equipmentId, string rfidCode); } } diff --git a/Service/Interface/IFileStorageProvider.cs b/Service/Interface/IFileStorageProvider.cs new file mode 100644 index 0000000..ed01210 --- /dev/null +++ b/Service/Interface/IFileStorageProvider.cs @@ -0,0 +1,28 @@ +using Model; +using Model.Dto.System; +using Model.Entity.System; +using System.IO; +using System.Threading.Tasks; + +namespace Service.Interface +{ + /// + /// 文件存储 Provider 抽象(Local 落地;MinIO/OSS 留扩展点) + /// 实现类命名约定:XxxFileStorageProvider,由 DependencyInjection 手动注册到 IFileStorageProvider + /// FileStorageService 通过 IEnumerable<IFileStorageProvider> 按配置 Provider 字段挑选实例 + /// + public interface IFileStorageProvider + { + /// Provider 标识(local/minio/oss),与 FileStorageConfigEntity.Provider 对应 + string ProviderName { get; } + + /// 上传文件,返回存储后的文件名(含相对路径)、访问 URL、扩展名、大小 + Task> UploadAsync(Stream stream, string originalName, long size, FileStorageConfigEntity config); + + /// 删除文件(按存储后的相对文件名) + Task DeleteAsync(string storedFileName, FileStorageConfigEntity config); + + /// 测试连接/可用性(Local:检查目录可写;MinIO/OSS:检查 Bucket 可访问) + Task TestConnectionAsync(FileStorageConfigEntity config); + } +} diff --git a/Service/Interface/System/IDictService.cs b/Service/Interface/System/IDictService.cs index b1047dd..ddd62f8 100644 --- a/Service/Interface/System/IDictService.cs +++ b/Service/Interface/System/IDictService.cs @@ -1,10 +1,54 @@ +using Model; +using Model.Dto.System; +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; + namespace Service.Interface { /// /// 数据字典管理 服务接口 + /// 约定:字典分类(DictType) + 字典项(DictItem) 二层结构,不支持级联 /// public interface IDictService { - // TODO: 定义 数据字典管理 相关方法 + // ==================== 字典分类 ==================== + + /// 分页查询字典分类(关键字匹配 Code/Name) + Task>> GetTypesPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword = null); + + /// 全部启用的字典分类(下拉用,不缓存) + Task>> GetTypesAllAsync(); + + /// 字典分类详情 + Task> GetTypeByIdAsync(long id); + + /// 新增字典分类(校验编码唯一) + Task> AddTypeAsync(DictTypeDto dto); + + /// 修改字典分类 + Task> UpdateTypeAsync(DictTypeDto dto); + + /// 删除字典分类(分类下有字典项时禁止删除) + Task DeleteTypeAsync(long id); + + // ==================== 字典项 ==================== + + /// 查询指定分类下的字典项(按 Sort、CreateTime 倒序) + Task>> GetItemsByTypeAsync(long typeId); + + /// 新增字典项(校验同分类下编码唯一;IsDefault=1 时清零其它默认项) + Task> AddItemAsync(DictItemDto dto); + + /// 修改字典项 + Task> UpdateItemAsync(DictItemDto dto); + + /// 删除字典项 + Task DeleteItemAsync(long id); + + // ==================== 业务侧(无权限,供其它模块调用) ==================== + + /// 按字典编码查询启用项(带 IMemoryCache 缓存,增删改时失效) + Task>> GetOptionsByCodeAsync(string typeCode); } } diff --git a/Service/Interface/System/IFileStorageService.cs b/Service/Interface/System/IFileStorageService.cs index 433012b..d513b8c 100644 --- a/Service/Interface/System/IFileStorageService.cs +++ b/Service/Interface/System/IFileStorageService.cs @@ -1,10 +1,50 @@ +using Model; +using Model.Dto.System; +using SqlSugar; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; + namespace Service.Interface { /// /// 文件存储管理 服务接口 + /// 管理侧:存储配置 CRUD + 设默认 + 连接测试;上传侧:按默认配置上传,登记 sys_file_record /// public interface IFileStorageService { - // TODO: 定义 文件存储管理 相关方法 + // ==================== 存储配置 ==================== + + /// 分页查询存储配置(关键字匹配 Name/Provider) + Task>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword = null); + + /// 存储配置详情 + Task> GetByIdAsync(long id); + + /// 新增存储配置(校验 Provider 支持范围;IsDefault=1 时清零其它默认) + Task> AddAsync(FileStorageConfigDto dto); + + /// 修改存储配置(AccessKey/SecretKey 为掩码时保留原值) + Task> UpdateAsync(FileStorageConfigDto dto); + + /// 删除存储配置(默认配置禁止删除) + Task DeleteAsync(long id); + + /// 设为默认(清零其它默认) + Task SetDefaultAsync(long id); + + /// 测试连接(按配置调用对应 Provider 的 TestConnectionAsync) + Task TestAsync(long id); + + // ==================== 文件上传/记录 ==================== + + /// 上传文件(使用默认启用的存储配置;校验大小/扩展名;登记 sys_file_record) + Task> UploadAsync(Stream stream, string originalName, long size, string? bizType = null, string? bizId = null, string? uploader = null); + + /// 文件记录分页查询(可按 BizType / 关键字过滤) + Task>> GetFileListPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword = null, string? bizType = null); + + /// 文件记录详情 + Task> GetFileByIdAsync(long id); } } diff --git a/Service/Interface/System/ISystemParamService.cs b/Service/Interface/System/ISystemParamService.cs index eeef52b..1708cd5 100644 --- a/Service/Interface/System/ISystemParamService.cs +++ b/Service/Interface/System/ISystemParamService.cs @@ -1,10 +1,44 @@ +using Model; +using Model.Dto.System; +using SqlSugar; +using System.Collections.Generic; +using System.Threading.Tasks; + namespace Service.Interface { /// /// 系统参数配置 服务接口 + /// 管理侧:CRUD(按 Group 分组、关键字检索);业务侧:按 Key 单/批量取值(带缓存) + /// 内置参数(IsSystem=1)禁止删除,仅允许修改 Value /// public interface ISystemParamService { - // TODO: 定义 系统参数配置 相关方法 + // ==================== 管理侧 ==================== + + /// 分页查询(关键字匹配 ParamKey/ParamName;可选 Group 过滤) + Task>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync total, string? keyword = null, string? group = null); + + /// 参数详情 + Task> GetByIdAsync(long id); + + /// 新增参数(校验 ParamKey 唯一) + Task> AddAsync(SystemParamDto dto); + + /// 修改参数(内置参数仅允许改 Value/Remark;非内置可改全字段;Key 变更时同步失效缓存) + Task> UpdateAsync(SystemParamDto dto); + + /// 删除参数(内置参数禁止删除) + Task DeleteAsync(long id); + + /// 清空参数值缓存(运维用) + Task ReloadCacheAsync(); + + // ==================== 业务侧(无权限,供其它模块调用) ==================== + + /// 按 Key 取单个值(带缓存;不存在返回空字符串) + Task> GetValueAsync(string key); + + /// 按 Key 列表批量取值(带缓存) + Task>> GetValuesAsync(string[] keys); } } diff --git a/Service/Service.csproj b/Service/Service.csproj index 4f8782b..a1791a5 100644 --- a/Service/Service.csproj +++ b/Service/Service.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -8,8 +8,10 @@ + +