完善系统参数配置和数据字典管理模块
This commit is contained in:
@@ -1,14 +1,86 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Model;
|
||||||
|
using Model.Dto.Asset;
|
||||||
|
using Service.Interface;
|
||||||
|
using SqlSugar;
|
||||||
|
using WebAPI.Filters;
|
||||||
|
|
||||||
namespace WebAPI.Controllers
|
namespace WebAPI.Controllers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 二维码
|
/// 设备二维码生成与打印(资产侧;生成/绑定 RFID 需 device:edit,列表/图片/打印页 device:view 可访问)
|
||||||
|
/// 扫码直达:二维码内容 = http://host/asset/ledger/equipment?id={equipmentId}
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/asset/qrcode")]
|
[Route("api/asset/qrcode")]
|
||||||
public class QrCodeController : ControllerBase
|
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}";
|
||||||
|
|
||||||
|
/// <summary>生成二维码 URL(写入设备 QrCodeUrl 字段,不返回图片)</summary>
|
||||||
|
[HttpPost("generate/{equipmentId}")]
|
||||||
|
[RequirePermission("device:edit")]
|
||||||
|
public async Task<IActionResult> Generate(long equipmentId)
|
||||||
|
{
|
||||||
|
return Ok(await _qrService.GenerateAsync(equipmentId, BaseUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>生成二维码 PNG 图片(直接返回 image/png 二进制)</summary>
|
||||||
|
[HttpGet("image/{equipmentId}")]
|
||||||
|
[RequirePermission("device:view")]
|
||||||
|
public async Task<IActionResult> GetImage(long equipmentId)
|
||||||
|
{
|
||||||
|
var result = await _qrService.GenerateImageAsync(equipmentId, BaseUrl);
|
||||||
|
if (!result.IsSuccess || result.Data == null)
|
||||||
|
return Ok(Result<byte[]>.Error(result.Msg));
|
||||||
|
return File(result.Data, "image/png");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>批量生成二维码 URL</summary>
|
||||||
|
[HttpPost("batch-generate")]
|
||||||
|
[RequirePermission("device:edit")]
|
||||||
|
public async Task<IActionResult> BatchGenerate([FromBody] long[] equipmentIds)
|
||||||
|
{
|
||||||
|
return Ok(await _qrService.BatchGenerateAsync(equipmentIds, BaseUrl));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>设备二维码列表(分页,qrOnly=1 仅看已生成)</summary>
|
||||||
|
[HttpGet("list")]
|
||||||
|
[RequirePermission("device:view")]
|
||||||
|
public async Task<IActionResult> GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, [FromQuery] bool qrOnly = false)
|
||||||
|
{
|
||||||
|
RefAsync<int> 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<List<QrCodeDto>>.Success(result.Data))
|
||||||
|
: Ok(Result<List<QrCodeDto>>.Error(result.Msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>打印页(返回 text/html,浏览器打开后自动调起打印)</summary>
|
||||||
|
[HttpGet("print/{equipmentId}")]
|
||||||
|
[RequirePermission("device:view")]
|
||||||
|
public async Task<IActionResult> 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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>绑定 RFID 编号到设备(query 传 rfidCode)</summary>
|
||||||
|
[HttpPost("bind-rfid/{equipmentId}")]
|
||||||
|
[RequirePermission("device:edit")]
|
||||||
|
public async Task<IActionResult> BindRfid(long equipmentId, [FromQuery] string rfidCode)
|
||||||
|
{
|
||||||
|
return Ok(await _qrService.BindRfidAsync(equipmentId, rfidCode));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,121 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Model;
|
||||||
|
using Model.Dto.System;
|
||||||
|
using Service.Interface;
|
||||||
|
using SqlSugar;
|
||||||
|
using WebAPI.Filters;
|
||||||
|
|
||||||
namespace WebAPI.Controllers
|
namespace WebAPI.Controllers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据字典管理
|
/// 数据字典管理(管理侧需 system:manage 权限;业务侧 options 接口无权限供其它模块调用)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/system/dict")]
|
[Route("api/system/dict")]
|
||||||
public class DictController : ControllerBase
|
public class DictController : ControllerBase
|
||||||
{
|
{
|
||||||
// TODO: 实现 数据字典管理 相关接口
|
private readonly IDictService _dictService;
|
||||||
|
|
||||||
|
public DictController(IDictService dictService)
|
||||||
|
{
|
||||||
|
_dictService = dictService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 字典分类 ====================
|
||||||
|
|
||||||
|
/// <summary>字典分类列表(分页)</summary>
|
||||||
|
[HttpGet("type/list")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> GetTypeList(int pageIndex = 1, int pageSize = 10, string? keyword = null)
|
||||||
|
{
|
||||||
|
RefAsync<int> total = 0;
|
||||||
|
var result = await _dictService.GetTypesPagedAsync(pageIndex, pageSize, total, keyword);
|
||||||
|
Response.Headers["X-Total-Count"] = total.Value.ToString();
|
||||||
|
return result.IsSuccess
|
||||||
|
? Ok(Result<List<DictTypeDto>>.Success(result.Data))
|
||||||
|
: Ok(Result<List<DictTypeDto>>.Error(result.Msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>字典分类下拉(全部启用的分类)</summary>
|
||||||
|
[HttpGet("type/options")]
|
||||||
|
public async Task<IActionResult> GetTypeOptions()
|
||||||
|
{
|
||||||
|
return Ok(await _dictService.GetTypesAllAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>字典分类详情</summary>
|
||||||
|
[HttpGet("type/{id}")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> GetTypeById(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _dictService.GetTypeByIdAsync(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>新增字典分类</summary>
|
||||||
|
[HttpPost("type")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> AddType([FromBody] DictTypeDto dto)
|
||||||
|
{
|
||||||
|
return Ok(await _dictService.AddTypeAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>修改字典分类</summary>
|
||||||
|
[HttpPut("type")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> UpdateType([FromBody] DictTypeDto dto)
|
||||||
|
{
|
||||||
|
return Ok(await _dictService.UpdateTypeAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>删除字典分类(分类下有字典项时禁止删除)</summary>
|
||||||
|
[HttpDelete("type/{id}")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> DeleteType(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _dictService.DeleteTypeAsync(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 字典项 ====================
|
||||||
|
|
||||||
|
/// <summary>查询指定分类下的字典项</summary>
|
||||||
|
[HttpGet("item/list/{typeId}")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> GetItemsByType(long typeId)
|
||||||
|
{
|
||||||
|
return Ok(await _dictService.GetItemsByTypeAsync(typeId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>新增字典项</summary>
|
||||||
|
[HttpPost("item")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> AddItem([FromBody] DictItemDto dto)
|
||||||
|
{
|
||||||
|
return Ok(await _dictService.AddItemAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>修改字典项</summary>
|
||||||
|
[HttpPut("item")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> UpdateItem([FromBody] DictItemDto dto)
|
||||||
|
{
|
||||||
|
return Ok(await _dictService.UpdateItemAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>删除字典项</summary>
|
||||||
|
[HttpDelete("item/{id}")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> DeleteItem(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _dictService.DeleteItemAsync(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 业务侧(无权限,供其它模块通过字典编码查询) ====================
|
||||||
|
|
||||||
|
/// <summary>按字典编码查询启用项(带缓存,业务侧调用)</summary>
|
||||||
|
[HttpGet("options/{typeCode}")]
|
||||||
|
public async Task<IActionResult> GetOptions(string typeCode)
|
||||||
|
{
|
||||||
|
return Ok(await _dictService.GetOptionsByCodeAsync(typeCode));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,118 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Model;
|
||||||
|
using Model.Dto.System;
|
||||||
|
using Service.Interface;
|
||||||
|
using SqlSugar;
|
||||||
|
using WebAPI.Filters;
|
||||||
|
|
||||||
namespace WebAPI.Controllers
|
namespace WebAPI.Controllers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文件存储管理
|
/// 文件存储管理(存储配置 CRUD + 连接测试 + 文件上传 + 文件记录查询,需 system:manage 权限)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/system/file-storage")]
|
[Route("api/system/file-storage")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
public class FileStorageController : ControllerBase
|
public class FileStorageController : ControllerBase
|
||||||
{
|
{
|
||||||
// TODO: 实现 文件存储管理 相关接口
|
private readonly IFileStorageService _storageService;
|
||||||
|
|
||||||
|
public FileStorageController(IFileStorageService storageService)
|
||||||
|
{
|
||||||
|
_storageService = storageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 存储配置 ====================
|
||||||
|
|
||||||
|
/// <summary>存储配置列表(分页)</summary>
|
||||||
|
[HttpGet("list")]
|
||||||
|
public async Task<IActionResult> GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null)
|
||||||
|
{
|
||||||
|
RefAsync<int> total = 0;
|
||||||
|
var result = await _storageService.GetListPagedAsync(pageIndex, pageSize, total, keyword);
|
||||||
|
Response.Headers["X-Total-Count"] = total.Value.ToString();
|
||||||
|
return result.IsSuccess
|
||||||
|
? Ok(Result<List<FileStorageConfigDto>>.Success(result.Data))
|
||||||
|
: Ok(Result<List<FileStorageConfigDto>>.Error(result.Msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>存储配置详情</summary>
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<IActionResult> GetById(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _storageService.GetByIdAsync(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>新增存储配置</summary>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Add([FromBody] FileStorageConfigDto dto)
|
||||||
|
{
|
||||||
|
return Ok(await _storageService.AddAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>修改存储配置(AccessKey/SecretKey 为 **** 掩码时保留原值)</summary>
|
||||||
|
[HttpPut]
|
||||||
|
public async Task<IActionResult> Update([FromBody] FileStorageConfigDto dto)
|
||||||
|
{
|
||||||
|
return Ok(await _storageService.UpdateAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>删除存储配置(默认通道禁止删除)</summary>
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<IActionResult> Delete(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _storageService.DeleteAsync(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>设为默认存储通道</summary>
|
||||||
|
[HttpPost("{id}/default")]
|
||||||
|
public async Task<IActionResult> SetDefault(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _storageService.SetDefaultAsync(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>测试连接(按配置调用对应 Provider 的 TestConnectionAsync)</summary>
|
||||||
|
[HttpPost("{id}/test")]
|
||||||
|
public async Task<IActionResult> Test(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _storageService.TestAsync(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 文件上传/记录 ====================
|
||||||
|
|
||||||
|
/// <summary>上传文件(multipart/form-data,字段名 file;使用默认存储通道)</summary>
|
||||||
|
/// <param name="file">文件</param>
|
||||||
|
/// <param name="bizType">业务类型(equipment_attachment/qrcode/generic 等,可选)</param>
|
||||||
|
/// <param name="bizId">业务Id(可选)</param>
|
||||||
|
/// <param name="uploader">上传人(可选,默认取当前登录用户)</param>
|
||||||
|
[HttpPost("upload")]
|
||||||
|
[RequestSizeLimit(100 * 1024 * 1024)]
|
||||||
|
public async Task<IActionResult> Upload(IFormFile file, [FromForm] string? bizType = null, [FromForm] string? bizId = null, [FromForm] string? uploader = null)
|
||||||
|
{
|
||||||
|
if (file == null || file.Length == 0)
|
||||||
|
return Ok(Result<FileRecordDto>.Error("请选择要上传的文件"));
|
||||||
|
using var stream = file.OpenReadStream();
|
||||||
|
return Ok(await _storageService.UploadAsync(stream, file.FileName, file.Length, bizType, bizId, uploader));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>文件记录列表(分页,可按关键字和业务类型过滤)</summary>
|
||||||
|
[HttpGet("file/list")]
|
||||||
|
public async Task<IActionResult> GetFileList(int pageIndex = 1, int pageSize = 10, string? keyword = null, string? bizType = null)
|
||||||
|
{
|
||||||
|
RefAsync<int> 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<List<FileRecordDto>>.Success(result.Data))
|
||||||
|
: Ok(Result<List<FileRecordDto>>.Error(result.Msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>文件记录详情</summary>
|
||||||
|
[HttpGet("file/{id}")]
|
||||||
|
public async Task<IActionResult> GetFileById(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _storageService.GetFileByIdAsync(id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,95 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Model;
|
||||||
|
using Model.Dto.System;
|
||||||
|
using Service.Interface;
|
||||||
|
using SqlSugar;
|
||||||
|
using WebAPI.Filters;
|
||||||
|
|
||||||
namespace WebAPI.Controllers
|
namespace WebAPI.Controllers
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 系统参数配置
|
/// 系统参数配置(管理侧需 system:manage 权限;业务侧 value/values 接口无权限供其它模块调用)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/system/param")]
|
[Route("api/system/param")]
|
||||||
public class SystemParamController : ControllerBase
|
public class SystemParamController : ControllerBase
|
||||||
{
|
{
|
||||||
// TODO: 实现 系统参数配置 相关接口
|
private readonly ISystemParamService _paramService;
|
||||||
|
|
||||||
|
public SystemParamController(ISystemParamService paramService)
|
||||||
|
{
|
||||||
|
_paramService = paramService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 管理侧 ====================
|
||||||
|
|
||||||
|
/// <summary>参数列表(分页,可按关键字和分组过滤)</summary>
|
||||||
|
[HttpGet("list")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, string? group = null)
|
||||||
|
{
|
||||||
|
RefAsync<int> 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<List<SystemParamDto>>.Success(result.Data))
|
||||||
|
: Ok(Result<List<SystemParamDto>>.Error(result.Msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>参数详情</summary>
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> GetById(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.GetByIdAsync(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>新增参数</summary>
|
||||||
|
[HttpPost]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> Add([FromBody] SystemParamDto dto)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.AddAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>修改参数(内置参数仅允许改 Value/Remark)</summary>
|
||||||
|
[HttpPut]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> Update([FromBody] SystemParamDto dto)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.UpdateAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>删除参数(内置参数禁止删除)</summary>
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> Delete(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.DeleteAsync(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>清空参数缓存(运维用)</summary>
|
||||||
|
[HttpPost("cache/reload")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> ReloadCache()
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.ReloadCacheAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 业务侧(无权限,供其它模块按 Key 取值) ====================
|
||||||
|
|
||||||
|
/// <summary>按 Key 取单个值</summary>
|
||||||
|
[HttpGet("value/{key}")]
|
||||||
|
public async Task<IActionResult> GetValue(string key)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.GetValueAsync(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>按 Key 列表批量取值(query: ?keys=k1&k2&k3)</summary>
|
||||||
|
[HttpGet("values")]
|
||||||
|
public async Task<IActionResult> GetValues([FromQuery] string[] keys)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.GetValuesAsync(keys));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ namespace WebAPI
|
|||||||
// 泛型基础服务(IBaseService<> -> BaseService<>)单独注册
|
// 泛型基础服务(IBaseService<> -> BaseService<>)单独注册
|
||||||
services.AddScoped(typeof(Service.Interface.IBaseService<>), typeof(Service.Implement.BaseService<>));
|
services.AddScoped(typeof(Service.Interface.IBaseService<>), typeof(Service.Implement.BaseService<>));
|
||||||
|
|
||||||
|
// 文件存储 Provider:命名约定 XxxFileStorageProvider 不匹配自动注册,手动登记
|
||||||
|
// 新增 MinIO/OSS 实现时在此追加一行 services.AddScoped<IFileStorageProvider, MinioFileStorageProvider>()
|
||||||
|
services.AddScoped<Service.Interface.IFileStorageProvider, Service.Implement.LocalFileStorageProvider>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ namespace WebAPI
|
|||||||
// 自动注册业务服务(Service.Interface -> Service.Implement)
|
// 自动注册业务服务(Service.Interface -> Service.Implement)
|
||||||
builder.Services.AddBusinessServices();
|
builder.Services.AddBusinessServices();
|
||||||
|
|
||||||
|
// 内存缓存:数据字典等查询频繁的基础数据按 typeCode 缓存,增删改时失效
|
||||||
|
builder.Services.AddMemoryCache();
|
||||||
|
|
||||||
// 当前用户上下文(从 JWT Claims 解析,供业务服务取操作人/做权限判断)
|
// 当前用户上下文(从 JWT Claims 解析,供业务服务取操作人/做权限判断)
|
||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddScoped<ICurrentUser, CurrentUser>();
|
builder.Services.AddScoped<ICurrentUser, CurrentUser>();
|
||||||
|
|||||||
@@ -49,6 +49,12 @@ namespace Model.Dto.Asset
|
|||||||
/// <summary>二维码编号(一物一码)</summary>
|
/// <summary>二维码编号(一物一码)</summary>
|
||||||
public string? QrCode { get; set; }
|
public string? QrCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>二维码扫码直达 URL(生成后写入,扫码跳转设备台账详情)</summary>
|
||||||
|
public string? QrCodeUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>RFID 编号(绑定时写入)</summary>
|
||||||
|
public string? RfidCode { get; set; }
|
||||||
|
|
||||||
/// <summary>设备描述</summary>
|
/// <summary>设备描述</summary>
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
namespace Model.Dto.Asset
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备二维码信息(生成/列表/详情共用)
|
||||||
|
/// </summary>
|
||||||
|
public class QrCodeDto
|
||||||
|
{
|
||||||
|
/// <summary>设备 Id(long → string)</summary>
|
||||||
|
public string EquipmentId { get; set; }
|
||||||
|
/// <summary>设备编号</summary>
|
||||||
|
public string? EquipmentCode { get; set; }
|
||||||
|
/// <summary>设备名称</summary>
|
||||||
|
public string? EquipmentName { get; set; }
|
||||||
|
/// <summary>存放位置(打印标签用)</summary>
|
||||||
|
public string? Location { get; set; }
|
||||||
|
/// <summary>二维码编号(一物一码,人工可读)</summary>
|
||||||
|
public string? QrCode { get; set; }
|
||||||
|
/// <summary>扫码直达 URL(生成后写入;为空表示尚未生成)</summary>
|
||||||
|
public string? QrCodeUrl { get; set; }
|
||||||
|
/// <summary>RFID 编号(已绑定则有值)</summary>
|
||||||
|
public string? RfidCode { get; set; }
|
||||||
|
/// <summary>是否已生成二维码(QrCodeUrl 非空)</summary>
|
||||||
|
public bool HasGenerated => !string.IsNullOrWhiteSpace(QrCodeUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量生成结果
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
namespace Model.Dto.System
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据字典分类 DTO
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
/// <summary>该分类下的字典项数量(列表展示用,由 Service 填充)</summary>
|
||||||
|
public int ItemCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据字典项 DTO
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 字典项下拉选项(业务侧通过 typeCode 查询用:仅返回编码+文本+是否默认)
|
||||||
|
/// </summary>
|
||||||
|
public class DictOptionDto
|
||||||
|
{
|
||||||
|
public string Code { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool IsDefault { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
namespace Model.Dto.System
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文件存储配置 DTO
|
||||||
|
/// </summary>
|
||||||
|
public class FileStorageConfigDto
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Provider { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string? Endpoint { get; set; }
|
||||||
|
/// <summary>回显时掩码(仅显示前后各 2 字符);写入时若为掩码样式则保留原值不变</summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件上传记录 DTO
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 上传结果(Provider 返回,Service 写入 sys_file_record 时用)
|
||||||
|
/// </summary>
|
||||||
|
public class FileUploadResultDto
|
||||||
|
{
|
||||||
|
/// <summary>存储后的相对文件名(含路径,如 2026/09/18/snowflake.jpg)</summary>
|
||||||
|
public string FileName { get; set; }
|
||||||
|
/// <summary>访问 URL</summary>
|
||||||
|
public string Url { get; set; }
|
||||||
|
/// <summary>扩展名</summary>
|
||||||
|
public string? Ext { get; set; }
|
||||||
|
/// <summary>文件大小(字节)</summary>
|
||||||
|
public long Size { get; set; }
|
||||||
|
public string Provider { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
namespace Model.Dto.System
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 系统参数 DTO
|
||||||
|
/// </summary>
|
||||||
|
public class SystemParamDto
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string ParamKey { get; set; }
|
||||||
|
public string ParamName { get; set; }
|
||||||
|
public string? ParamValue { get; set; }
|
||||||
|
/// <summary>值类型(0=int, 1=text, 2=enum, 3=json)</summary>
|
||||||
|
public byte ParamType { get; set; }
|
||||||
|
/// <summary>枚举选项(原始 JSON 字符串,由前端解析;仅 ParamType=enum 有值)</summary>
|
||||||
|
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; }
|
||||||
|
/// <summary>是否系统内置(1=不可删,仅允许改 Value)</summary>
|
||||||
|
public byte IsSystem { get; set; }
|
||||||
|
public string? LastUpdateUser { get; set; }
|
||||||
|
public DateTime? LastUpdateTime { get; set; }
|
||||||
|
public DateTime? CreateTime { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务侧批量取值返回项
|
||||||
|
/// </summary>
|
||||||
|
public class SystemParamValueDto
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
public string? Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using SqlSugar;
|
using SqlSugar;
|
||||||
|
|
||||||
namespace Model.Entity.Asset
|
namespace Model.Entity.Asset
|
||||||
{
|
{
|
||||||
@@ -73,6 +73,19 @@ namespace Model.Entity.Asset
|
|||||||
[SugarColumn(Length = 50, IsNullable = true)]
|
[SugarColumn(Length = 50, IsNullable = true)]
|
||||||
public string? QrCode { get; set; }
|
public string? QrCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 二维码扫码直达 URL(生成二维码时写入,扫码后跳转设备台账详情)
|
||||||
|
/// 形如 http://host/asset/ledger/equipment?id={Id};为空表示尚未生成二维码
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 500, IsNullable = true)]
|
||||||
|
public string? QrCodeUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RFID 编号(绑定时写入,用于 RFID 标签识别设备)
|
||||||
|
/// </summary>
|
||||||
|
[SugarColumn(Length = 64, IsNullable = true)]
|
||||||
|
public string? RfidCode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设备描述
|
/// 设备描述
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace Model.Entity.System
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据字典项表(某分类下的具体枚举值)
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace Model.Entity.System
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据字典分类表(如:设备类型、故障类型、告警级别、工单优先级)
|
||||||
|
/// </summary>
|
||||||
|
[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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace Model.Entity.System
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文件上传记录表(每次上传都登记:来源 Provider、业务类型、访问 URL)
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("sys_file_record")]
|
||||||
|
public class FileRecordEntity : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>存储后的文件名(含相对路径,如 2026/09/18/snowflake.jpg)</summary>
|
||||||
|
[SugarColumn(ColumnName = "FileName", ColumnDescription = "存储文件名(含相对路径)", Length = 255, IsNullable = false)]
|
||||||
|
public string FileName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>原始文件名(用户上传时的文件名)</summary>
|
||||||
|
[SugarColumn(ColumnName = "OriginalName", ColumnDescription = "原始文件名", Length = 255, IsNullable = true)]
|
||||||
|
public string? OriginalName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>访问 URL(Endpoint + 相对路径)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Url", ColumnDescription = "访问URL", Length = 500, IsNullable = true)]
|
||||||
|
public string? Url { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文件大小(字节)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Size", ColumnDescription = "文件大小(字节)")]
|
||||||
|
public long Size { get; set; }
|
||||||
|
|
||||||
|
/// <summary>扩展名(含点,如 .jpg)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Ext", ColumnDescription = "扩展名", Length = 20, IsNullable = true)]
|
||||||
|
public string? Ext { get; set; }
|
||||||
|
|
||||||
|
/// <summary>使用的存储 Provider(冗余,便于排查)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Provider", ColumnDescription = "存储Provider", Length = 20, IsNullable = true)]
|
||||||
|
public string? Provider { get; set; }
|
||||||
|
|
||||||
|
/// <summary>使用的存储配置Id(关联 sys_file_storage.Id)</summary>
|
||||||
|
public long StorageId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>上传人</summary>
|
||||||
|
[SugarColumn(ColumnName = "Uploader", ColumnDescription = "上传人", Length = 50, IsNullable = true)]
|
||||||
|
public string? Uploader { get; set; }
|
||||||
|
|
||||||
|
/// <summary>业务类型(equipment_attachment/qrcode/generic 等,便于按业务查询)</summary>
|
||||||
|
[SugarColumn(ColumnName = "BizType", ColumnDescription = "业务类型", Length = 50, IsNullable = true)]
|
||||||
|
public string? BizType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>业务Id(关联业务实体的 Id,可空)</summary>
|
||||||
|
[SugarColumn(ColumnName = "BizId", ColumnDescription = "业务Id", Length = 64, IsNullable = true)]
|
||||||
|
public string? BizId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace Model.Entity.System
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文件存储配置表(多 Provider 配置:Local/MinIO/OSS,仅 Local 落地,其它留扩展点)
|
||||||
|
/// 一个 IsDefault=1 的配置为默认上传通道;AccessKey/SecretKey 对 Local 为空
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("sys_file_storage")]
|
||||||
|
public class FileStorageConfigEntity : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>存储 Provider(local/minio/oss)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Provider", ColumnDescription = "存储Provider(local/minio/oss)", Length = 20, IsNullable = false)]
|
||||||
|
public string Provider { get; set; }
|
||||||
|
|
||||||
|
/// <summary>配置名称(如:本地存储 / MinIO测试)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Name", ColumnDescription = "配置名称", Length = 100, IsNullable = false)]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>访问端点(Local 为 URL 前缀如 http://host/uploads 或 /uploads;MinIO 为 http://minio:9000)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Endpoint", ColumnDescription = "访问端点", Length = 255, IsNullable = true)]
|
||||||
|
public string? Endpoint { get; set; }
|
||||||
|
|
||||||
|
/// <summary>访问 Key(Local 为空;MinIO/OSS 为 AccessKey。注:当前明文存储,接入 MinIO/OSS 时应改为 AES 加密)</summary>
|
||||||
|
[SugarColumn(ColumnName = "AccessKey", ColumnDescription = "AccessKey(Local为空)", Length = 128, IsNullable = true)]
|
||||||
|
public string? AccessKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>密钥(Local 为空;MinIO/OSS 为 SecretKey。注:当前明文存储,接入 MinIO/OSS 时应改为 AES 加密)</summary>
|
||||||
|
[SugarColumn(ColumnName = "SecretKey", ColumnDescription = "SecretKey(Local为空)", Length = 255, IsNullable = true)]
|
||||||
|
public string? SecretKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>桶名(Local 为 BasePath 下子目录;MinIO/OSS 为 bucket 名)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Bucket", ColumnDescription = "桶名", Length = 100, IsNullable = true)]
|
||||||
|
public string? Bucket { get; set; }
|
||||||
|
|
||||||
|
/// <summary>区域(OSS 用,Local/MinIO 为空)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Region", ColumnDescription = "区域(OSS用)", Length = 50, IsNullable = true)]
|
||||||
|
public string? Region { get; set; }
|
||||||
|
|
||||||
|
/// <summary>本地存储根路径(Local 专用,如 wwwroot/uploads;MinIO/OSS 为空)</summary>
|
||||||
|
[SugarColumn(ColumnName = "BasePath", ColumnDescription = "本地存储根路径(Local专用)", Length = 255, IsNullable = true)]
|
||||||
|
public string? BasePath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>单个文件大小上限(MB)</summary>
|
||||||
|
[SugarColumn(ColumnName = "MaxSizeMB", ColumnDescription = "单个文件大小上限(MB)", DefaultValue = "10")]
|
||||||
|
public int MaxSizeMB { get; set; } = 10;
|
||||||
|
|
||||||
|
/// <summary>允许的扩展名(JSON 数组,如 [".jpg",".png"];为空表示不限制)</summary>
|
||||||
|
[SugarColumn(ColumnName = "AllowedExts", ColumnDescription = "允许扩展名(JSON数组)", ColumnDataType = "text", IsNullable = true)]
|
||||||
|
public string? AllowedExts { get; set; }
|
||||||
|
|
||||||
|
/// <summary>是否默认(1=默认上传通道,全局唯一)</summary>
|
||||||
|
[SugarColumn(ColumnName = "IsDefault", ColumnDescription = "是否默认(1=默认,0=否)", ColumnDataType = "smallint", DefaultValue = "0")]
|
||||||
|
public byte IsDefault { get; set; }
|
||||||
|
|
||||||
|
/// <summary>状态(1=启用,0=停用)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Status", ColumnDescription = "状态(1=启用,0=停用)", ColumnDataType = "smallint", DefaultValue = "1")]
|
||||||
|
public byte Status { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>备注</summary>
|
||||||
|
[SugarColumn(ColumnName = "Remark", ColumnDescription = "备注", Length = 500, IsNullable = true)]
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
|
||||||
|
namespace Model.Entity.System
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 系统参数配置表(键值对形式存储业务可调参数,按 Group 分组展示)
|
||||||
|
/// 内置参数(IsSystem=1)禁止删除,仅允许修改 Value
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("sys_param")]
|
||||||
|
public class SystemParamEntity : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>参数键(唯一,业务侧按 Key 取值,如 data_collect_default_frequency)</summary>
|
||||||
|
[SugarColumn(ColumnName = "ParamKey", ColumnDescription = "参数键(唯一)", Length = 64, IsNullable = false)]
|
||||||
|
public string ParamKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>参数名称(中文展示名)</summary>
|
||||||
|
[SugarColumn(ColumnName = "ParamName", ColumnDescription = "参数名称", Length = 100, IsNullable = false)]
|
||||||
|
public string ParamName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>参数值(统一字符串存储,业务侧按 ParamType 自行转换)</summary>
|
||||||
|
[SugarColumn(ColumnName = "ParamValue", ColumnDescription = "参数值", ColumnDataType = "text", IsNullable = true)]
|
||||||
|
public string? ParamValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>值类型(0=int, 1=text, 2=enum, 3=json)决定前端渲染控件</summary>
|
||||||
|
[SugarColumn(ColumnName = "ParamType", ColumnDescription = "值类型(0=int,1=text,2=enum,3=json)", ColumnDataType = "smallint", DefaultValue = "1")]
|
||||||
|
public byte ParamType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>枚举选项(JSON 数组,如 [{"value":"feishu","label":"飞书"}];仅 ParamType=enum 使用)</summary>
|
||||||
|
[SugarColumn(ColumnName = "ParamOptions", ColumnDescription = "枚举选项(JSON数组)", ColumnDataType = "text", IsNullable = true)]
|
||||||
|
public string? ParamOptions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>单位(如 秒/MB,前端展示用)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Unit", ColumnDescription = "单位", Length = 20, IsNullable = true)]
|
||||||
|
public string? Unit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>分组(如 数据采集/告警/工单/文件,前端按分组卡片展示)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Group", ColumnDescription = "分组", Length = 50, IsNullable = true)]
|
||||||
|
public string? Group { get; set; }
|
||||||
|
|
||||||
|
/// <summary>排序号</summary>
|
||||||
|
[SugarColumn(ColumnName = "Sort", ColumnDescription = "排序号", DefaultValue = "0")]
|
||||||
|
public int Sort { get; set; }
|
||||||
|
|
||||||
|
/// <summary>备注(注释提示,如工单编号规则占位符说明)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Remark", ColumnDescription = "备注", Length = 500, IsNullable = true)]
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>是否系统内置(1=内置不可删,0=用户自定义)</summary>
|
||||||
|
[SugarColumn(ColumnName = "IsSystem", ColumnDescription = "是否系统内置(1=不可删,0=可删)", ColumnDataType = "smallint", DefaultValue = "0")]
|
||||||
|
public byte IsSystem { get; set; }
|
||||||
|
|
||||||
|
/// <summary>最后修改人</summary>
|
||||||
|
[SugarColumn(ColumnName = "LastUpdateUser", ColumnDescription = "最后修改人", Length = 50, IsNullable = true)]
|
||||||
|
public string? LastUpdateUser { get; set; }
|
||||||
|
|
||||||
|
/// <summary>最后修改时间</summary>
|
||||||
|
[SugarColumn(ColumnName = "LastUpdateTime", ColumnDescription = "最后修改时间", IsNullable = true)]
|
||||||
|
public DateTime? LastUpdateTime { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@ namespace Model.Mapper
|
|||||||
Supplier = entity.Supplier,
|
Supplier = entity.Supplier,
|
||||||
ImageUrl = entity.ImageUrl,
|
ImageUrl = entity.ImageUrl,
|
||||||
QrCode = entity.QrCode,
|
QrCode = entity.QrCode,
|
||||||
|
QrCodeUrl = entity.QrCodeUrl,
|
||||||
|
RfidCode = entity.RfidCode,
|
||||||
Description = entity.Description,
|
Description = entity.Description,
|
||||||
Department = entity.Department,
|
Department = entity.Department,
|
||||||
Location = entity.Location,
|
Location = entity.Location,
|
||||||
@@ -98,6 +100,8 @@ namespace Model.Mapper
|
|||||||
Supplier = dto.Supplier,
|
Supplier = dto.Supplier,
|
||||||
ImageUrl = dto.ImageUrl,
|
ImageUrl = dto.ImageUrl,
|
||||||
QrCode = dto.QrCode,
|
QrCode = dto.QrCode,
|
||||||
|
QrCodeUrl = dto.QrCodeUrl,
|
||||||
|
RfidCode = dto.RfidCode,
|
||||||
Description = dto.Description,
|
Description = dto.Description,
|
||||||
Department = dto.Department,
|
Department = dto.Department,
|
||||||
Location = dto.Location,
|
Location = dto.Location,
|
||||||
@@ -1242,5 +1246,205 @@ namespace Model.Mapper
|
|||||||
public static List<AuditLogDto> ToDtoList(this List<AuditLogEntity> entities)
|
public static List<AuditLogDto> ToDtoList(this List<AuditLogEntity> entities)
|
||||||
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<AuditLogDto>();
|
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<AuditLogDto>();
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region 数据字典
|
||||||
|
/// <summary>
|
||||||
|
/// DictTypeEntity → DictTypeDto
|
||||||
|
/// </summary>
|
||||||
|
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<DictTypeDto> ToDtoList(this List<DictTypeEntity> entities)
|
||||||
|
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<DictTypeDto>();
|
||||||
|
/// <summary>
|
||||||
|
/// DictTypeDto → DictTypeEntity(入参映射,IsDel/CreateTime 不映射)
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DictItemEntity → DictItemDto
|
||||||
|
/// </summary>
|
||||||
|
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<DictItemDto> ToDtoList(this List<DictItemEntity> entities)
|
||||||
|
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<DictItemDto>();
|
||||||
|
/// <summary>
|
||||||
|
/// DictItemDto → DictItemEntity(入参映射,CreateTime/IsDel 不映射;TypeId 由 Service 强制覆盖)
|
||||||
|
/// </summary>
|
||||||
|
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 系统参数
|
||||||
|
/// <summary>
|
||||||
|
/// SystemParamEntity → SystemParamDto
|
||||||
|
/// </summary>
|
||||||
|
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<SystemParamDto> ToDtoList(this List<SystemParamEntity> entities)
|
||||||
|
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<SystemParamDto>();
|
||||||
|
/// <summary>
|
||||||
|
/// SystemParamDto → SystemParamEntity(入参映射,CreateTime/IsDel/LastUpdateUser/LastUpdateTime 不映射;由 Service 控管)
|
||||||
|
/// </summary>
|
||||||
|
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<FileStorageConfigDto> ToDtoList(this List<FileStorageConfigEntity> entities)
|
||||||
|
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<FileStorageConfigDto>();
|
||||||
|
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<FileRecordDto> ToDtoList(this List<FileRecordEntity> entities)
|
||||||
|
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<FileRecordDto>();
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,227 @@
|
|||||||
|
using Model;
|
||||||
|
using Model.Dto.Asset;
|
||||||
|
using Model.Entity.Asset;
|
||||||
|
using ORM;
|
||||||
|
using QRCoder;
|
||||||
using Service.Interface;
|
using Service.Interface;
|
||||||
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Service.Implement
|
namespace Service.Implement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 二维码 服务实现
|
/// 设备二维码 服务实现
|
||||||
|
/// 扫码直达 URL 约定:{baseUrl}/asset/ledger/equipment?id={equipmentId}
|
||||||
|
/// baseUrl 由 Controller 从 HTTP 请求(scheme://host)传入;如需固定域名可改为读 sys_param 的 site_base_url
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class QrCodeService : IQrCodeService
|
public class QrCodeService : IQrCodeService
|
||||||
{
|
{
|
||||||
// TODO: 实现 二维码 相关方法
|
public async Task<Result<QrCodeDto>> GenerateAsync(long equipmentId, string baseUrl)
|
||||||
|
{
|
||||||
|
if (equipmentId <= 0) return Result<QrCodeDto>.Error("设备Id无效");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await LoadEquipmentAsync(equipmentId);
|
||||||
|
if (entity == null) return Result<QrCodeDto>.Error("设备不存在或已被删除");
|
||||||
|
|
||||||
|
var url = BuildQrUrl(baseUrl, equipmentId);
|
||||||
|
if (string.IsNullOrEmpty(entity.QrCodeUrl))
|
||||||
|
{
|
||||||
|
await SqlSugarContext.DbContext.Updateable<EquipmentEntity>()
|
||||||
|
.SetColumns(x => x.QrCodeUrl == url)
|
||||||
|
.Where(x => x.Id == equipmentId).ExecuteCommandAsync();
|
||||||
|
entity.QrCodeUrl = url;
|
||||||
|
}
|
||||||
|
return Result<QrCodeDto>.Success(ToDto(entity));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<QrCodeDto>.Error("生成二维码失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<byte[]>> GenerateImageAsync(long equipmentId, string baseUrl)
|
||||||
|
{
|
||||||
|
if (equipmentId <= 0) return Result<byte[]>.Error("设备Id无效");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await LoadEquipmentAsync(equipmentId);
|
||||||
|
if (entity == null) return Result<byte[]>.Error("设备不存在或已被删除");
|
||||||
|
|
||||||
|
var url = string.IsNullOrEmpty(entity.QrCodeUrl) ? BuildQrUrl(baseUrl, equipmentId) : entity.QrCodeUrl;
|
||||||
|
if (string.IsNullOrEmpty(entity.QrCodeUrl))
|
||||||
|
{
|
||||||
|
await SqlSugarContext.DbContext.Updateable<EquipmentEntity>()
|
||||||
|
.SetColumns(x => x.QrCodeUrl == url)
|
||||||
|
.Where(x => x.Id == equipmentId).ExecuteCommandAsync();
|
||||||
|
entity.QrCodeUrl = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
var png = EncodePng(url);
|
||||||
|
return Result<byte[]>.Success(png);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<byte[]>.Error("生成二维码图片失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<List<QrCodeBatchResultDto>>> BatchGenerateAsync(long[] equipmentIds, string baseUrl)
|
||||||
|
{
|
||||||
|
if (equipmentIds == null || equipmentIds.Length == 0)
|
||||||
|
return Result<List<QrCodeBatchResultDto>>.Error("设备Id列表不能为空");
|
||||||
|
var results = new List<QrCodeBatchResultDto>();
|
||||||
|
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<List<QrCodeBatchResultDto>>.Success(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<List<QrCodeDto>>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, bool qrOnly = false)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = await SqlSugarContext.DbContext.Queryable<EquipmentEntity>()
|
||||||
|
.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<List<QrCodeDto>>.Success(list.Select(ToDto).ToList());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<QrCodeDto>>.Error("查询二维码列表失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<string>> GetPrintHtmlAsync(long equipmentId, string baseUrl)
|
||||||
|
{
|
||||||
|
if (equipmentId <= 0) return Result<string>.Error("设备Id无效");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await LoadEquipmentAsync(equipmentId);
|
||||||
|
if (entity == null) return Result<string>.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<string>.Success(html);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<string>.Error("生成打印页失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> 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<EquipmentEntity>()
|
||||||
|
.Where(x => x.Id == equipmentId && x.IsDel == 0).AnyAsync();
|
||||||
|
if (!exists) return Result.Error("设备不存在或已被删除");
|
||||||
|
|
||||||
|
await SqlSugarContext.DbContext.Updateable<EquipmentEntity>()
|
||||||
|
.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<EquipmentEntity?> LoadEquipmentAsync(long id)
|
||||||
|
=> await SqlSugarContext.DbContext.Queryable<EquipmentEntity>()
|
||||||
|
.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 $@"<!DOCTYPE html>
|
||||||
|
<html lang='zh-CN'>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8' />
|
||||||
|
<title>设备二维码 - {safe(e.Name)}</title>
|
||||||
|
<style>
|
||||||
|
@page {{ size: A4; margin: 12mm; }}
|
||||||
|
body {{ font-family: 'Microsoft YaHei', sans-serif; color: #303133; }}
|
||||||
|
.label {{ border:1px solid #303133; border-radius:6px; padding:16px; display:flex; gap:24px; align-items:center; max-width:480px; }}
|
||||||
|
.qr img {{ width: 220px; height: 220px; }}
|
||||||
|
.info dl {{ margin:0; }}
|
||||||
|
.info dt {{ font-size:13px; color:#909399; margin-top:8px; }}
|
||||||
|
.info dd {{ font-size:16px; margin:4px 0 0 0; font-weight:600; }}
|
||||||
|
.info h2 {{ margin:0 0 8px 0; font-size:20px; }}
|
||||||
|
.url {{ margin-top:12px; font-size:12px; color:#909399; word-break:break-all; }}
|
||||||
|
.actions {{ margin-top:16px; }}
|
||||||
|
.actions button {{ padding:6px 16px; font-size:14px; cursor:pointer; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class='label'>
|
||||||
|
<div class='qr'><img src='data:image/png;base64,{base64}' alt='QR' /></div>
|
||||||
|
<div class='info'>
|
||||||
|
<h2>{safe(e.Name)}</h2>
|
||||||
|
<dl>
|
||||||
|
<dt>设备编号</dt><dd>{safe(e.Code)}</dd>
|
||||||
|
<dt>二维码编号</dt><dd>{safe(e.QrCode)}</dd>
|
||||||
|
<dt>存放位置</dt><dd>{safe(e.Location)}</dd>
|
||||||
|
<dt>责任人</dt><dd>{safe(e.ResponsiblePerson)}</dd>
|
||||||
|
</dl>
|
||||||
|
<div class='url'>{safe(url)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class='actions'><button onclick='window.print()'>打印</button></div>
|
||||||
|
<script>window.onload=function(){{setTimeout(function(){{window.print();}},300);}};</script>
|
||||||
|
</body>
|
||||||
|
</html>";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 本地文件系统存储 Provider
|
||||||
|
/// 落地策略:存到 BasePath/{Bucket?}/{yyyy}/{MM}/{dd}/{guid}{ext},URL = {Endpoint}/{Bucket?}/{yyyy}/{MM}/{dd}/{guid}{ext}
|
||||||
|
/// wwwroot/uploads 由 Program.UseStaticFiles 提供访问;MinIO/OSS 实现见 IFileStorageProvider 扩展点
|
||||||
|
/// </summary>
|
||||||
|
public class LocalFileStorageProvider : IFileStorageProvider
|
||||||
|
{
|
||||||
|
public string ProviderName => "local";
|
||||||
|
|
||||||
|
public async Task<Result<FileUploadResultDto>> UploadAsync(Stream stream, string originalName, long size, FileStorageConfigEntity config)
|
||||||
|
{
|
||||||
|
if (config == null) return Result<FileUploadResultDto>.Error("存储配置不能为空");
|
||||||
|
if (stream == null || !stream.CanRead) return Result<FileUploadResultDto>.Error("文件流不可读");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var basePath = ResolveBasePath(config.BasePath);
|
||||||
|
if (string.IsNullOrWhiteSpace(basePath))
|
||||||
|
return Result<FileUploadResultDto>.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<FileUploadResultDto>.Success(new FileUploadResultDto
|
||||||
|
{
|
||||||
|
FileName = storedFileName,
|
||||||
|
Url = url,
|
||||||
|
Ext = ext,
|
||||||
|
Size = size,
|
||||||
|
Provider = ProviderName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<FileUploadResultDto>.Error("本地文件上传失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Result> 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<Result> 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<string>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(endpoint)) segs.Add(endpoint.TrimEnd('/'));
|
||||||
|
if (!string.IsNullOrWhiteSpace(bucket)) segs.Add(bucket.Trim('/'));
|
||||||
|
segs.Add(storedFileName);
|
||||||
|
return string.Join('/', segs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ namespace Service.Implement
|
|||||||
public const byte DataScopeAll = 1;
|
public const byte DataScopeAll = 1;
|
||||||
public const byte DataScopeLab = 2;
|
public const byte DataScopeLab = 2;
|
||||||
|
|
||||||
/// <summary>6 个系统权限编码</summary>
|
/// <summary>7 个系统权限编码</summary>
|
||||||
public static readonly (string Code, string Name, string Group)[] Permissions =
|
public static readonly (string Code, string Name, string Group)[] Permissions =
|
||||||
{
|
{
|
||||||
("device:view", "查看设备", "device"),
|
("device:view", "查看设备", "device"),
|
||||||
@@ -21,7 +21,8 @@ namespace Service.Implement
|
|||||||
("device:control", "控制设备", "device"),
|
("device:control", "控制设备", "device"),
|
||||||
("alert:confirm", "确认告警", "alert"),
|
("alert:confirm", "确认告警", "alert"),
|
||||||
("inspection:manage", "巡检管理", "inspection"),
|
("inspection:manage", "巡检管理", "inspection"),
|
||||||
("user:manage", "用户管理", "user")
|
("user:manage", "用户管理", "user"),
|
||||||
|
("system:manage", "系统配置管理", "system")
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -30,8 +31,8 @@ namespace Service.Implement
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly (string Code, string Name, byte DataScope, string[] Permissions)[] Roles =
|
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" }),
|
("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" }),
|
("labadmin", "实验室管理员", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "system:manage" }),
|
||||||
("operator", "设备操作员", DataScopeLab, new[] { "device:view", "inspection:manage" }),
|
("operator", "设备操作员", DataScopeLab, new[] { "device:view", "inspection:manage" }),
|
||||||
("maintengineer", "维修工程师", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm" })
|
("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();
|
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)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
@@ -120,5 +127,70 @@ namespace Service.Implement
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void SeedDefaultFileStorage(SqlSugar.ISqlSugarClient db, DateTime now)
|
||||||
|
{
|
||||||
|
var hasAny = db.Queryable<FileStorageConfigEntity>().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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>一期 4 个内置系统参数(IsSystem=1,禁止删除,仅允许改 Value/Remark)</summary>
|
||||||
|
/// <remarks>Group 与 ParamType 约定:0=int,1=text,2=enum,3=json</remarks>
|
||||||
|
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<SystemParamEntity>().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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 Service.Interface;
|
||||||
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Service.Implement
|
namespace Service.Implement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据字典管理 服务实现
|
/// 数据字典管理 服务实现
|
||||||
|
/// 缓存策略:按 typeCode 缓存启用项,新增/修改/删除字典分类或字典项时失效对应 typeCode;启动首次访问时按需加载
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DictService : IDictService
|
public class DictService : IDictService
|
||||||
{
|
{
|
||||||
// TODO: 实现 数据字典管理 相关方法
|
private readonly IMemoryCache _cache;
|
||||||
|
private static readonly object _cacheLock = new();
|
||||||
|
|
||||||
|
public DictService(IMemoryCache cache)
|
||||||
|
{
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 字典分类 ====================
|
||||||
|
|
||||||
|
public async Task<Result<List<DictTypeDto>>> GetTypesPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||||
|
.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<List<DictTypeDto>>.Success(dtos);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<DictTypeDto>>.Error("查询字典分类失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<List<DictTypeDto>>> GetTypesAllAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||||
|
.Where(x => x.IsDel == 0 && x.Status == 1)
|
||||||
|
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
|
||||||
|
.ToListAsync();
|
||||||
|
return Result<List<DictTypeDto>>.Success(list.ToDtoList());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<DictTypeDto>>.Error("查询字典分类失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<DictTypeDto>> GetTypeByIdAsync(long id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||||
|
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||||
|
if (entity == null) return Result<DictTypeDto>.Error("字典分类不存在或已被删除");
|
||||||
|
var dto = entity.ToDto();
|
||||||
|
await FillItemCountAsync(new List<DictTypeDto> { dto });
|
||||||
|
return Result<DictTypeDto>.Success(dto);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<DictTypeDto>.Error("查询字典分类详情失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<DictTypeDto>> AddTypeAsync(DictTypeDto dto)
|
||||||
|
{
|
||||||
|
if (dto == null || string.IsNullOrWhiteSpace(dto.Code) || string.IsNullOrWhiteSpace(dto.Name))
|
||||||
|
return Result<DictTypeDto>.Error("字典编码和名称不能为空");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var exists = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||||
|
.Where(x => x.Code == dto.Code && x.IsDel == 0).AnyAsync();
|
||||||
|
if (exists) return Result<DictTypeDto>.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<DictTypeDto>.Success(entity.ToDto());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<DictTypeDto>.Error("新增字典分类失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<DictTypeDto>> UpdateTypeAsync(DictTypeDto dto)
|
||||||
|
{
|
||||||
|
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||||
|
return Result<DictTypeDto>.Error("字典分类Id无效");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||||
|
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||||
|
if (entity == null) return Result<DictTypeDto>.Error("字典分类不存在或已被删除");
|
||||||
|
|
||||||
|
var codeExists = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||||
|
.Where(x => x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
|
||||||
|
if (codeExists) return Result<DictTypeDto>.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<DictTypeDto>.Success(newEntity.ToDto());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<DictTypeDto>.Error("修改字典分类失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> DeleteTypeAsync(long id)
|
||||||
|
{
|
||||||
|
if (id <= 0) return Result.Error("字典分类Id无效");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||||
|
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||||
|
if (entity == null) return Result.Error("字典分类不存在或已被删除");
|
||||||
|
|
||||||
|
var hasItems = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||||
|
.Where(x => x.TypeId == id && x.IsDel == 0).AnyAsync();
|
||||||
|
if (hasItems) return Result.Error("该字典分类下还有字典项,请先删除字典项再删除分类");
|
||||||
|
|
||||||
|
await SqlSugarContext.DbContext.Updateable<DictTypeEntity>()
|
||||||
|
.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<Result<List<DictItemDto>>> GetItemsByTypeAsync(long typeId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||||
|
.Where(x => x.TypeId == typeId && x.IsDel == 0)
|
||||||
|
.OrderBy(x => x.Sort).OrderBy(x => x.CreateTime, OrderByType.Desc)
|
||||||
|
.ToListAsync();
|
||||||
|
return Result<List<DictItemDto>>.Success(list.ToDtoList());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<DictItemDto>>.Error("查询字典项失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<DictItemDto>> AddItemAsync(DictItemDto dto)
|
||||||
|
{
|
||||||
|
if (dto == null || !long.TryParse(dto.TypeId, out var typeId) || typeId <= 0)
|
||||||
|
return Result<DictItemDto>.Error("所属字典分类Id无效");
|
||||||
|
if (string.IsNullOrWhiteSpace(dto.Code) || string.IsNullOrWhiteSpace(dto.Name))
|
||||||
|
return Result<DictItemDto>.Error("字典项编码和名称不能为空");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var typeExists = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||||
|
.Where(x => x.Id == typeId && x.IsDel == 0).AnyAsync();
|
||||||
|
if (!typeExists) return Result<DictItemDto>.Error("所属字典分类不存在");
|
||||||
|
|
||||||
|
var codeExists = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||||
|
.Where(x => x.TypeId == typeId && x.Code == dto.Code && x.IsDel == 0).AnyAsync();
|
||||||
|
if (codeExists) return Result<DictItemDto>.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<DictItemEntity>()
|
||||||
|
.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<DictItemDto>.Success(entity.ToDto());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<DictItemDto>.Error("新增字典项失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<DictItemDto>> UpdateItemAsync(DictItemDto dto)
|
||||||
|
{
|
||||||
|
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||||
|
return Result<DictItemDto>.Error("字典项Id无效");
|
||||||
|
if (!long.TryParse(dto.TypeId, out var typeId) || typeId <= 0)
|
||||||
|
return Result<DictItemDto>.Error("所属字典分类Id无效");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||||
|
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||||
|
if (entity == null) return Result<DictItemDto>.Error("字典项不存在或已被删除");
|
||||||
|
|
||||||
|
var codeExists = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||||
|
.Where(x => x.TypeId == typeId && x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
|
||||||
|
if (codeExists) return Result<DictItemDto>.Error($"字典项编码「{dto.Code}」在该分类下已存在");
|
||||||
|
|
||||||
|
// 默认项互斥:改为默认时清零同分类其它默认项
|
||||||
|
if (dto.IsDefault == 1 && entity.IsDefault == 0)
|
||||||
|
{
|
||||||
|
await SqlSugarContext.DbContext.Updateable<DictItemEntity>()
|
||||||
|
.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<DictItemDto>.Success(newEntity.ToDto());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<DictItemDto>.Error("修改字典项失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> DeleteItemAsync(long id)
|
||||||
|
{
|
||||||
|
if (id <= 0) return Result.Error("字典项Id无效");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||||
|
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||||
|
if (entity == null) return Result.Error("字典项不存在或已被删除");
|
||||||
|
|
||||||
|
await SqlSugarContext.DbContext.Updateable<DictItemEntity>()
|
||||||
|
.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<Result<List<DictOptionDto>>> GetOptionsByCodeAsync(string typeCode)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(typeCode))
|
||||||
|
return Result<List<DictOptionDto>>.Error("字典编码不能为空");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var key = $"dict:options:{typeCode}";
|
||||||
|
if (!_cache.TryGetValue(key, out List<DictOptionDto>? cached) || cached == null)
|
||||||
|
{
|
||||||
|
lock (_cacheLock)
|
||||||
|
{
|
||||||
|
cached ??= LoadOptionsByCodeAsync(typeCode).GetAwaiter().GetResult();
|
||||||
|
_cache.Set(key, cached, TimeSpan.FromHours(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result<List<DictOptionDto>>.Success(cached);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<DictOptionDto>>.Error("查询字典选项失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 私有工具 ====================
|
||||||
|
|
||||||
|
private async Task<string> GetTypeCodeAsync(long typeId)
|
||||||
|
{
|
||||||
|
var code = await SqlSugarContext.DbContext.Queryable<DictTypeEntity>()
|
||||||
|
.Where(x => x.Id == typeId).Select(x => x.Code).FirstAsync();
|
||||||
|
return code ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<List<DictOptionDto>> LoadOptionsByCodeAsync(string typeCode)
|
||||||
|
{
|
||||||
|
var db = SqlSugarContext.DbContext;
|
||||||
|
var typeId = await db.Queryable<DictTypeEntity>()
|
||||||
|
.Where(x => x.Code == typeCode && x.IsDel == 0 && x.Status == 1)
|
||||||
|
.Select(x => x.Id).FirstAsync();
|
||||||
|
if (typeId == 0) return new List<DictOptionDto>();
|
||||||
|
|
||||||
|
var items = await db.Queryable<DictItemEntity>()
|
||||||
|
.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<DictTypeDto> dtos)
|
||||||
|
{
|
||||||
|
if (dtos == null || dtos.Count == 0) return;
|
||||||
|
var ids = dtos.Select(d => long.Parse(d.Id)).ToList();
|
||||||
|
var counts = await SqlSugarContext.DbContext.Queryable<DictItemEntity>()
|
||||||
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,322 @@
|
|||||||
|
using Model;
|
||||||
|
using Model.Dto.System;
|
||||||
|
using Model.Entity.System;
|
||||||
|
using Model.Mapper;
|
||||||
|
using ORM;
|
||||||
using Service.Interface;
|
using Service.Interface;
|
||||||
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Service.Implement
|
namespace Service.Implement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文件存储管理 服务实现
|
/// 文件存储管理 服务实现
|
||||||
|
/// 通过 IEnumerable<IFileStorageProvider> 按配置 Provider 字段挑选实现;当前仅 Local 落地
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class FileStorageService : IFileStorageService
|
public class FileStorageService : IFileStorageService
|
||||||
{
|
{
|
||||||
// TODO: 实现 文件存储管理 相关方法
|
private readonly IEnumerable<IFileStorageProvider> _providers;
|
||||||
|
private readonly ICurrentUser _currentUser;
|
||||||
|
|
||||||
|
// 掩码标记:前端回显 AccessKey/SecretKey 时用 **** 脱敏,回写时若仍为掩码则保留原值
|
||||||
|
private const string MaskMarker = "****";
|
||||||
|
|
||||||
|
public FileStorageService(IEnumerable<IFileStorageProvider> providers, ICurrentUser currentUser)
|
||||||
|
{
|
||||||
|
_providers = providers;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 存储配置 ====================
|
||||||
|
|
||||||
|
public async Task<Result<List<FileStorageConfigDto>>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||||
|
.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<List<FileStorageConfigDto>>.Success(dtos);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<FileStorageConfigDto>>.Error("查询存储配置失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<FileStorageConfigDto>> GetByIdAsync(long id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||||
|
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||||
|
if (entity == null) return Result<FileStorageConfigDto>.Error("存储配置不存在或已被删除");
|
||||||
|
var dto = entity.ToDto();
|
||||||
|
dto.AccessKey = Mask(dto.AccessKey);
|
||||||
|
dto.SecretKey = Mask(dto.SecretKey);
|
||||||
|
return Result<FileStorageConfigDto>.Success(dto);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<FileStorageConfigDto>.Error("查询存储配置失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<FileStorageConfigDto>> AddAsync(FileStorageConfigDto dto)
|
||||||
|
{
|
||||||
|
if (dto == null || string.IsNullOrWhiteSpace(dto.Provider) || string.IsNullOrWhiteSpace(dto.Name))
|
||||||
|
return Result<FileStorageConfigDto>.Error("Provider 和名称不能为空");
|
||||||
|
if (!IsProviderSupported(dto.Provider))
|
||||||
|
return Result<FileStorageConfigDto>.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<FileStorageConfigDto>.Success(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<FileStorageConfigDto>.Error("新增存储配置失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<FileStorageConfigDto>> UpdateAsync(FileStorageConfigDto dto)
|
||||||
|
{
|
||||||
|
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||||
|
return Result<FileStorageConfigDto>.Error("配置Id无效");
|
||||||
|
if (!string.IsNullOrWhiteSpace(dto.Provider) && !IsProviderSupported(dto.Provider))
|
||||||
|
return Result<FileStorageConfigDto>.Error($"不支持的 Provider:{dto.Provider}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||||
|
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||||
|
if (entity == null) return Result<FileStorageConfigDto>.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<FileStorageConfigDto>.Success(result);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<FileStorageConfigDto>.Error("修改存储配置失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> DeleteAsync(long id)
|
||||||
|
{
|
||||||
|
if (id <= 0) return Result.Error("配置Id无效");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||||
|
.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<FileStorageConfigEntity>()
|
||||||
|
.SetColumns(x => x.IsDel == 1)
|
||||||
|
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||||
|
return Result.Success();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Error("删除存储配置失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> SetDefaultAsync(long id)
|
||||||
|
{
|
||||||
|
if (id <= 0) return Result.Error("配置Id无效");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||||
|
.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<FileStorageConfigEntity>()
|
||||||
|
.SetColumns(x => x.IsDefault == 1)
|
||||||
|
.Where(x => x.Id == id).ExecuteCommandAsync();
|
||||||
|
return Result.Success();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Error("设置默认存储失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> TestAsync(long id)
|
||||||
|
{
|
||||||
|
if (id <= 0) return Result.Error("配置Id无效");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||||
|
.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<Result<FileRecordDto>> UploadAsync(Stream stream, string originalName, long size, string? bizType = null, string? bizId = null, string? uploader = null)
|
||||||
|
{
|
||||||
|
if (stream == null || size <= 0) return Result<FileRecordDto>.Error("文件流或大小无效");
|
||||||
|
if (string.IsNullOrWhiteSpace(originalName)) return Result<FileRecordDto>.Error("文件名不能为空");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 取默认启用的存储配置
|
||||||
|
var config = await SqlSugarContext.DbContext.Queryable<FileStorageConfigEntity>()
|
||||||
|
.Where(x => x.IsDel == 0 && x.IsDefault == 1 && x.Status == 1).FirstAsync();
|
||||||
|
if (config == null) return Result<FileRecordDto>.Error("未配置默认存储通道,请先在「文件存储管理」新增并设为默认");
|
||||||
|
|
||||||
|
// 大小校验
|
||||||
|
if (config.MaxSizeMB > 0 && size > config.MaxSizeMB * 1024L * 1024L)
|
||||||
|
return Result<FileRecordDto>.Error($"文件大小超过上限 {config.MaxSizeMB}MB");
|
||||||
|
|
||||||
|
// 扩展名校验
|
||||||
|
var ext = Path.GetExtension(originalName)?.ToLowerInvariant() ?? "";
|
||||||
|
var allowed = ParseExts(config.AllowedExts);
|
||||||
|
if (allowed.Count > 0 && !allowed.Contains(ext))
|
||||||
|
return Result<FileRecordDto>.Error($"不支持的文件类型 {ext}(允许:{string.Join(",", allowed)})");
|
||||||
|
|
||||||
|
var provider = ResolveProvider(config.Provider);
|
||||||
|
if (provider == null) return Result<FileRecordDto>.Error($"未注册 Provider:{config.Provider}");
|
||||||
|
|
||||||
|
var upRes = await provider.UploadAsync(stream, originalName, size, config);
|
||||||
|
if (!upRes.IsSuccess || upRes.Data == null) return Result<FileRecordDto>.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<FileRecordDto>.Success(record.ToDto());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<FileRecordDto>.Error("文件上传失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<List<FileRecordDto>>> GetFileListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, string? bizType = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = await SqlSugarContext.DbContext.Queryable<FileRecordEntity>()
|
||||||
|
.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<List<FileRecordDto>>.Success(list.ToDtoList());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<FileRecordDto>>.Error("查询文件记录失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<FileRecordDto>> GetFileByIdAsync(long id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<FileRecordEntity>()
|
||||||
|
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||||
|
if (entity == null) return Result<FileRecordDto>.Error("文件记录不存在或已被删除");
|
||||||
|
return Result<FileRecordDto>.Success(entity.ToDto());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<FileRecordDto>.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<string> ParseExts(string? json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return new List<string>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var arr = System.Text.Json.JsonSerializer.Deserialize<List<string>>(json);
|
||||||
|
return arr?.Select(e => e.ToLowerInvariant()).ToList() ?? new List<string>();
|
||||||
|
}
|
||||||
|
catch { return new List<string>(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ClearOtherDefaultAsync(long keepId)
|
||||||
|
{
|
||||||
|
await SqlSugarContext.DbContext.Updateable<FileStorageConfigEntity>()
|
||||||
|
.SetColumns(x => x.IsDefault == 0)
|
||||||
|
.Where(x => x.IsDel == 0 && x.Id != keepId).ExecuteCommandAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 Service.Interface;
|
||||||
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Service.Implement
|
namespace Service.Implement
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 系统参数配置 服务实现
|
/// 系统参数配置 服务实现
|
||||||
|
/// 缓存策略:按 ParamKey 缓存值;增删改时失效对应 Key(Key 变更时新旧都失效)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SystemParamService : ISystemParamService
|
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<Result<List<SystemParamDto>>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, string? group = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||||
|
.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<List<SystemParamDto>>.Success(list.ToDtoList());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<SystemParamDto>>.Error("查询系统参数失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<SystemParamDto>> GetByIdAsync(long id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||||
|
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||||
|
if (entity == null) return Result<SystemParamDto>.Error("系统参数不存在或已被删除");
|
||||||
|
return Result<SystemParamDto>.Success(entity.ToDto());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<SystemParamDto>.Error("查询系统参数详情失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<SystemParamDto>> AddAsync(SystemParamDto dto)
|
||||||
|
{
|
||||||
|
if (dto == null || string.IsNullOrWhiteSpace(dto.ParamKey) || string.IsNullOrWhiteSpace(dto.ParamName))
|
||||||
|
return Result<SystemParamDto>.Error("参数键和名称不能为空");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var keyExists = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||||
|
.Where(x => x.ParamKey == dto.ParamKey && x.IsDel == 0).AnyAsync();
|
||||||
|
if (keyExists) return Result<SystemParamDto>.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<SystemParamDto>.Success(entity.ToDto());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<SystemParamDto>.Error("新增系统参数失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<SystemParamDto>> UpdateAsync(SystemParamDto dto)
|
||||||
|
{
|
||||||
|
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
|
||||||
|
return Result<SystemParamDto>.Error("参数Id无效");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||||
|
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
|
||||||
|
if (entity == null) return Result<SystemParamDto>.Error("系统参数不存在或已被删除");
|
||||||
|
|
||||||
|
// Key 变更时校验唯一
|
||||||
|
if (!string.Equals(entity.ParamKey, dto.ParamKey, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var keyExists = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||||
|
.Where(x => x.ParamKey == dto.ParamKey && x.Id != id && x.IsDel == 0).AnyAsync();
|
||||||
|
if (keyExists) return Result<SystemParamDto>.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<SystemParamDto>.Success(entity.ToDto());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<SystemParamDto>.Error("修改系统参数失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> DeleteAsync(long id)
|
||||||
|
{
|
||||||
|
if (id <= 0) return Result.Error("参数Id无效");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entity = await SqlSugarContext.DbContext.Queryable<SystemParamEntity>()
|
||||||
|
.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<SystemParamEntity>()
|
||||||
|
.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<Result> 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<Result<string>> GetValueAsync(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
return Result<string>.Error("参数键不能为空");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = await LoadValueByKeyAsync(key);
|
||||||
|
return Result<string>.Success(value ?? string.Empty);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<string>.Error("查询参数值失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result<List<SystemParamValueDto>>> GetValuesAsync(string[] keys)
|
||||||
|
{
|
||||||
|
if (keys == null || keys.Length == 0)
|
||||||
|
return Result<List<SystemParamValueDto>>.Error("参数键列表不能为空");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var distinctKeys = keys.Where(k => !string.IsNullOrWhiteSpace(k)).Distinct().ToList();
|
||||||
|
var list = new List<SystemParamValueDto>(distinctKeys.Count);
|
||||||
|
foreach (var k in distinctKeys)
|
||||||
|
{
|
||||||
|
var v = await LoadValueByKeyAsync(k);
|
||||||
|
list.Add(new SystemParamValueDto { Key = k, Value = v ?? string.Empty });
|
||||||
|
}
|
||||||
|
return Result<List<SystemParamValueDto>>.Success(list);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<SystemParamValueDto>>.Error("批量查询参数值失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 私有工具 ====================
|
||||||
|
|
||||||
|
private async Task<string?> 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<SystemParamEntity>()
|
||||||
|
.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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,35 @@
|
|||||||
|
using Model;
|
||||||
|
using Model.Dto.Asset;
|
||||||
|
using SqlSugar;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Service.Interface
|
namespace Service.Interface
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 二维码 服务接口
|
/// 设备二维码 服务接口
|
||||||
|
/// 编码内容为扫码直达 URL:{baseUrl}/asset/ledger/equipment?id={equipmentId}
|
||||||
|
/// baseUrl 由 Controller 从 HTTP 请求构造(scheme://host)后传入,保持 Service 层框架无关
|
||||||
|
/// 生成时把 URL 写入 EquipmentEntity.QrCodeUrl;打印返回 HTML 页面(含 base64 二维码图)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IQrCodeService
|
public interface IQrCodeService
|
||||||
{
|
{
|
||||||
// TODO: 定义 二维码 相关方法
|
/// <summary>生成二维码 URL 并写入设备(baseUrl 形如 http://host:port)</summary>
|
||||||
|
Task<Result<QrCodeDto>> GenerateAsync(long equipmentId, string baseUrl);
|
||||||
|
|
||||||
|
/// <summary>生成二维码 PNG 图片字节(同时确保 URL 已写库)</summary>
|
||||||
|
Task<Result<byte[]>> GenerateImageAsync(long equipmentId, string baseUrl);
|
||||||
|
|
||||||
|
/// <summary>批量生成二维码 URL(逐个写库,单条失败不影响其它)</summary>
|
||||||
|
Task<Result<List<QrCodeBatchResultDto>>> BatchGenerateAsync(long[] equipmentIds, string baseUrl);
|
||||||
|
|
||||||
|
/// <summary>设备二维码列表(分页,关键字匹配 Code/Name;qrOnly=1 仅看已生成)</summary>
|
||||||
|
Task<Result<List<QrCodeDto>>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, bool qrOnly = false);
|
||||||
|
|
||||||
|
/// <summary>获取打印用 HTML 页面(含二维码 base64 图 + 设备信息,浏览器 Ctrl+P 打印)</summary>
|
||||||
|
Task<Result<string>> GetPrintHtmlAsync(long equipmentId, string baseUrl);
|
||||||
|
|
||||||
|
/// <summary>绑定 RFID 编号到设备(覆盖式写入 RfidCode)</summary>
|
||||||
|
Task<Result> BindRfidAsync(long equipmentId, string rfidCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using Model;
|
||||||
|
using Model.Dto.System;
|
||||||
|
using Model.Entity.System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Service.Interface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文件存储 Provider 抽象(Local 落地;MinIO/OSS 留扩展点)
|
||||||
|
/// 实现类命名约定:XxxFileStorageProvider,由 DependencyInjection 手动注册到 IFileStorageProvider
|
||||||
|
/// FileStorageService 通过 IEnumerable<IFileStorageProvider> 按配置 Provider 字段挑选实例
|
||||||
|
/// </summary>
|
||||||
|
public interface IFileStorageProvider
|
||||||
|
{
|
||||||
|
/// <summary>Provider 标识(local/minio/oss),与 FileStorageConfigEntity.Provider 对应</summary>
|
||||||
|
string ProviderName { get; }
|
||||||
|
|
||||||
|
/// <summary>上传文件,返回存储后的文件名(含相对路径)、访问 URL、扩展名、大小</summary>
|
||||||
|
Task<Result<FileUploadResultDto>> UploadAsync(Stream stream, string originalName, long size, FileStorageConfigEntity config);
|
||||||
|
|
||||||
|
/// <summary>删除文件(按存储后的相对文件名)</summary>
|
||||||
|
Task<Result> DeleteAsync(string storedFileName, FileStorageConfigEntity config);
|
||||||
|
|
||||||
|
/// <summary>测试连接/可用性(Local:检查目录可写;MinIO/OSS:检查 Bucket 可访问)</summary>
|
||||||
|
Task<Result> TestConnectionAsync(FileStorageConfigEntity config);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,54 @@
|
|||||||
|
using Model;
|
||||||
|
using Model.Dto.System;
|
||||||
|
using SqlSugar;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Service.Interface
|
namespace Service.Interface
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 数据字典管理 服务接口
|
/// 数据字典管理 服务接口
|
||||||
|
/// 约定:字典分类(DictType) + 字典项(DictItem) 二层结构,不支持级联
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IDictService
|
public interface IDictService
|
||||||
{
|
{
|
||||||
// TODO: 定义 数据字典管理 相关方法
|
// ==================== 字典分类 ====================
|
||||||
|
|
||||||
|
/// <summary>分页查询字典分类(关键字匹配 Code/Name)</summary>
|
||||||
|
Task<Result<List<DictTypeDto>>> GetTypesPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null);
|
||||||
|
|
||||||
|
/// <summary>全部启用的字典分类(下拉用,不缓存)</summary>
|
||||||
|
Task<Result<List<DictTypeDto>>> GetTypesAllAsync();
|
||||||
|
|
||||||
|
/// <summary>字典分类详情</summary>
|
||||||
|
Task<Result<DictTypeDto>> GetTypeByIdAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>新增字典分类(校验编码唯一)</summary>
|
||||||
|
Task<Result<DictTypeDto>> AddTypeAsync(DictTypeDto dto);
|
||||||
|
|
||||||
|
/// <summary>修改字典分类</summary>
|
||||||
|
Task<Result<DictTypeDto>> UpdateTypeAsync(DictTypeDto dto);
|
||||||
|
|
||||||
|
/// <summary>删除字典分类(分类下有字典项时禁止删除)</summary>
|
||||||
|
Task<Result> DeleteTypeAsync(long id);
|
||||||
|
|
||||||
|
// ==================== 字典项 ====================
|
||||||
|
|
||||||
|
/// <summary>查询指定分类下的字典项(按 Sort、CreateTime 倒序)</summary>
|
||||||
|
Task<Result<List<DictItemDto>>> GetItemsByTypeAsync(long typeId);
|
||||||
|
|
||||||
|
/// <summary>新增字典项(校验同分类下编码唯一;IsDefault=1 时清零其它默认项)</summary>
|
||||||
|
Task<Result<DictItemDto>> AddItemAsync(DictItemDto dto);
|
||||||
|
|
||||||
|
/// <summary>修改字典项</summary>
|
||||||
|
Task<Result<DictItemDto>> UpdateItemAsync(DictItemDto dto);
|
||||||
|
|
||||||
|
/// <summary>删除字典项</summary>
|
||||||
|
Task<Result> DeleteItemAsync(long id);
|
||||||
|
|
||||||
|
// ==================== 业务侧(无权限,供其它模块调用) ====================
|
||||||
|
|
||||||
|
/// <summary>按字典编码查询启用项(带 IMemoryCache 缓存,增删改时失效)</summary>
|
||||||
|
Task<Result<List<DictOptionDto>>> GetOptionsByCodeAsync(string typeCode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
namespace Service.Interface
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 文件存储管理 服务接口
|
/// 文件存储管理 服务接口
|
||||||
|
/// 管理侧:存储配置 CRUD + 设默认 + 连接测试;上传侧:按默认配置上传,登记 sys_file_record
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IFileStorageService
|
public interface IFileStorageService
|
||||||
{
|
{
|
||||||
// TODO: 定义 文件存储管理 相关方法
|
// ==================== 存储配置 ====================
|
||||||
|
|
||||||
|
/// <summary>分页查询存储配置(关键字匹配 Name/Provider)</summary>
|
||||||
|
Task<Result<List<FileStorageConfigDto>>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null);
|
||||||
|
|
||||||
|
/// <summary>存储配置详情</summary>
|
||||||
|
Task<Result<FileStorageConfigDto>> GetByIdAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>新增存储配置(校验 Provider 支持范围;IsDefault=1 时清零其它默认)</summary>
|
||||||
|
Task<Result<FileStorageConfigDto>> AddAsync(FileStorageConfigDto dto);
|
||||||
|
|
||||||
|
/// <summary>修改存储配置(AccessKey/SecretKey 为掩码时保留原值)</summary>
|
||||||
|
Task<Result<FileStorageConfigDto>> UpdateAsync(FileStorageConfigDto dto);
|
||||||
|
|
||||||
|
/// <summary>删除存储配置(默认配置禁止删除)</summary>
|
||||||
|
Task<Result> DeleteAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>设为默认(清零其它默认)</summary>
|
||||||
|
Task<Result> SetDefaultAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>测试连接(按配置调用对应 Provider 的 TestConnectionAsync)</summary>
|
||||||
|
Task<Result> TestAsync(long id);
|
||||||
|
|
||||||
|
// ==================== 文件上传/记录 ====================
|
||||||
|
|
||||||
|
/// <summary>上传文件(使用默认启用的存储配置;校验大小/扩展名;登记 sys_file_record)</summary>
|
||||||
|
Task<Result<FileRecordDto>> UploadAsync(Stream stream, string originalName, long size, string? bizType = null, string? bizId = null, string? uploader = null);
|
||||||
|
|
||||||
|
/// <summary>文件记录分页查询(可按 BizType / 关键字过滤)</summary>
|
||||||
|
Task<Result<List<FileRecordDto>>> GetFileListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, string? bizType = null);
|
||||||
|
|
||||||
|
/// <summary>文件记录详情</summary>
|
||||||
|
Task<Result<FileRecordDto>> GetFileByIdAsync(long id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,44 @@
|
|||||||
|
using Model;
|
||||||
|
using Model.Dto.System;
|
||||||
|
using SqlSugar;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Service.Interface
|
namespace Service.Interface
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 系统参数配置 服务接口
|
/// 系统参数配置 服务接口
|
||||||
|
/// 管理侧:CRUD(按 Group 分组、关键字检索);业务侧:按 Key 单/批量取值(带缓存)
|
||||||
|
/// 内置参数(IsSystem=1)禁止删除,仅允许修改 Value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISystemParamService
|
public interface ISystemParamService
|
||||||
{
|
{
|
||||||
// TODO: 定义 系统参数配置 相关方法
|
// ==================== 管理侧 ====================
|
||||||
|
|
||||||
|
/// <summary>分页查询(关键字匹配 ParamKey/ParamName;可选 Group 过滤)</summary>
|
||||||
|
Task<Result<List<SystemParamDto>>> GetListPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, string? group = null);
|
||||||
|
|
||||||
|
/// <summary>参数详情</summary>
|
||||||
|
Task<Result<SystemParamDto>> GetByIdAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>新增参数(校验 ParamKey 唯一)</summary>
|
||||||
|
Task<Result<SystemParamDto>> AddAsync(SystemParamDto dto);
|
||||||
|
|
||||||
|
/// <summary>修改参数(内置参数仅允许改 Value/Remark;非内置可改全字段;Key 变更时同步失效缓存)</summary>
|
||||||
|
Task<Result<SystemParamDto>> UpdateAsync(SystemParamDto dto);
|
||||||
|
|
||||||
|
/// <summary>删除参数(内置参数禁止删除)</summary>
|
||||||
|
Task<Result> DeleteAsync(long id);
|
||||||
|
|
||||||
|
/// <summary>清空参数值缓存(运维用)</summary>
|
||||||
|
Task<Result> ReloadCacheAsync();
|
||||||
|
|
||||||
|
// ==================== 业务侧(无权限,供其它模块调用) ====================
|
||||||
|
|
||||||
|
/// <summary>按 Key 取单个值(带缓存;不存在返回空字符串)</summary>
|
||||||
|
Task<Result<string>> GetValueAsync(string key);
|
||||||
|
|
||||||
|
/// <summary>按 Key 列表批量取值(带缓存)</summary>
|
||||||
|
Task<Result<List<SystemParamValueDto>>> GetValuesAsync(string[] keys);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
@@ -8,8 +8,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Castle.Core" Version="5.2.1" />
|
<PackageReference Include="Castle.Core" Version="5.2.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
|
||||||
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
<PackageReference Include="RestSharp" Version="106.15.0" />
|
<PackageReference Include="RestSharp" Version="106.15.0" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.0.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user