Compare commits

..
4 Commits
Author SHA1 Message Date
dengboling 60fac00887 完善系统参数配置和数据字典管理模块 2026-09-18 17:22:56 +08:00
dengbolingandClaude Sonnet 4.6 6aca0297bb feat: 网关管理模块 + RBAC权限认证 + 组织架构 + 审计日志
- 网关管理:GatewayEntity/Service/Controller CRUD + 测试连接;设备 GatewayId 外键关联
- JWT 认证:登录签发 Token(权限编码写入 Claims)、RequirePermission 权限过滤器、CurrentUser 上下文
- RBAC:用户/角色/权限实体与 CRUD,预定义 4 角色 + 6 权限种子(admin/admin123)
- 组织架构:sys_org 固定层级树(公司/实验室/部门/班组白夜班),层级校验,用户挂 OrgId/岗位/技能标签
- 数据权限:角色 DataScope(全部/本组织及下级),用户列表按组织子树过滤
- 防锁死保护:禁止删/禁自己,保证至少一名活跃管理员,角色摘除 user:manage 前校验
- 审计日志:AuditRecorder 接入设备增删改/指令下发/登录登出/组织变更,AuditController 查询
- 设备指令下发按网关表取连接参数;设备列表支持 gatewayId/productId 筛选

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-10 17:31:39 +08:00
“hsc” 3059befcf5 Merge branch 'master' of https://git.kaiyili-lab.com/huangsucan/IOT_API 2026-09-04 17:38:57 +08:00
“hsc” 38cd47410c 设备维修统计报表 2026-09-02 13:36:52 +08:00
79 changed files with 6875 additions and 76 deletions
+74 -2
View File
@@ -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,95 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Model;
using Model.Dto.Config;
using Service.Interface.Config;
using SqlSugar;
using WebAPI.Filters;
namespace WebAPI.Controllers namespace WebAPI.Controllers.Config
{ {
/// <summary> /// <summary>
/// 网关管理 /// 网关管理
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/config/gateway")] [Route("api/config/gateway")]
[RequirePermission("device:view")]
public class GatewayController : ControllerBase public class GatewayController : ControllerBase
{ {
// TODO: 实现 网关管理 相关接口 private readonly IGatewayService _gatewayService;
public GatewayController(IGatewayService gatewayService)
{
_gatewayService = gatewayService;
}
/// <summary>
/// 网关列表(分页)
/// </summary>
[HttpGet("list")]
public async Task<Result<List<GatewayDto>>> GetList([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 10,
[FromQuery] string? keyword = null, [FromQuery] int? protocolType = null)
{
var total = new RefAsync<int>();
var result = await _gatewayService.GetPagedAsync(pageIndex, pageSize, total, keyword, protocolType);
Response.Headers["X-Total-Count"] = total.Value.ToString();
return result;
}
/// <summary>
/// 网关详情
/// </summary>
[HttpGet("{id}")]
public async Task<Result<GatewayDto>> GetById(long id)
{
return await _gatewayService.GetByIdAsync(id);
}
/// <summary>
/// 新增网关(需 device:edit 权限)
/// </summary>
[HttpPost]
[RequirePermission("device:edit")]
public async Task<Result<GatewayDto>> Add([FromBody] GatewayDto dto)
{
return await _gatewayService.AddAsync(dto);
}
/// <summary>
/// 修改网关(需 device:edit 权限)
/// </summary>
[HttpPut]
[RequirePermission("device:edit")]
public async Task<Result<GatewayDto>> Update([FromBody] GatewayDto dto)
{
return await _gatewayService.UpdateAsync(dto);
}
/// <summary>
/// 删除网关(软删除,设备 GatewayId 置 0;需 device:edit 权限)
/// </summary>
[HttpDelete("{id}")]
[RequirePermission("device:edit")]
public async Task<Result> Delete(long id)
{
return await _gatewayService.DeleteAsync(id);
}
/// <summary>
/// 测试网关连接(TCP/串口尝试连接,更新在线状态)
/// </summary>
[HttpPost("{id}/test-connection")]
public async Task<Result<GatewayTestResultDto>> TestConnection(long id)
{
return await _gatewayService.TestConnectionAsync(id);
}
/// <summary>
/// 网关下拉选项(设备表单用)
/// </summary>
[HttpGet("options")]
public async Task<Result<List<GatewayOptionDto>>> GetOptions()
{
return await _gatewayService.GetOptionsAsync();
}
} }
} }
@@ -4,6 +4,7 @@ using Model.Dto.Config;
using Service.Interface.Config; using Service.Interface.Config;
using SqlSugar; using SqlSugar;
using System.Threading.Tasks; using System.Threading.Tasks;
using WebAPI.Filters;
namespace WebAPI.Controllers namespace WebAPI.Controllers
{ {
@@ -12,6 +13,7 @@ namespace WebAPI.Controllers
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/config/device")] [Route("api/config/device")]
[RequirePermission("device:view")]
public class IotDeviceController : ControllerBase public class IotDeviceController : ControllerBase
{ {
private readonly IDeviceService _deviceService; private readonly IDeviceService _deviceService;
@@ -30,11 +32,12 @@ namespace WebAPI.Controllers
/// <param name="pageSize">每页数量(默认10</param> /// <param name="pageSize">每页数量(默认10</param>
/// <param name="keyword">关键字(模糊匹配设备编号/名称/类型)</param> /// <param name="keyword">关键字(模糊匹配设备编号/名称/类型)</param>
/// <param name="productId">所属产品Id0 表示不过滤)</param> /// <param name="productId">所属产品Id0 表示不过滤)</param>
/// <param name="gatewayId">所属网关Id0 表示不过滤)</param>
[HttpGet("list")] [HttpGet("list")]
public async Task<IActionResult> GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, long productId = 0) public async Task<IActionResult> GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, long productId = 0, long gatewayId = 0)
{ {
RefAsync<int> total = 0; RefAsync<int> total = 0;
var result = await _deviceService.GetPagedAsync(pageIndex, pageSize, total, keyword, productId > 0 ? productId : null); var result = await _deviceService.GetPagedAsync(pageIndex, pageSize, total, keyword, productId > 0 ? productId : null, gatewayId > 0 ? gatewayId : null);
Response.Headers["X-Total-Count"] = total.Value.ToString(); Response.Headers["X-Total-Count"] = total.Value.ToString();
return result.IsSuccess return result.IsSuccess
? Ok(Result<List<IotDeviceDto>>.Success(result.Data)) ? Ok(Result<List<IotDeviceDto>>.Success(result.Data))
@@ -52,28 +55,31 @@ namespace WebAPI.Controllers
} }
/// <summary> /// <summary>
/// 新增设备 /// 新增设备(需 device:edit 权限)
/// </summary> /// </summary>
[HttpPost] [HttpPost]
[RequirePermission("device:edit")]
public async Task<IActionResult> Add([FromBody] IotDeviceDto dto) public async Task<IActionResult> Add([FromBody] IotDeviceDto dto)
{ {
return Ok(await _deviceService.AddAsync(dto)); return Ok(await _deviceService.AddAsync(dto));
} }
/// <summary> /// <summary>
/// 修改设备 /// 修改设备(需 device:edit 权限)
/// </summary> /// </summary>
[HttpPut] [HttpPut]
[RequirePermission("device:edit")]
public async Task<IActionResult> Update([FromBody] IotDeviceDto dto) public async Task<IActionResult> Update([FromBody] IotDeviceDto dto)
{ {
return Ok(await _deviceService.UpdateAsync(dto)); return Ok(await _deviceService.UpdateAsync(dto));
} }
/// <summary> /// <summary>
/// 删除设备(软删除) /// 删除设备(软删除;需 device:edit 权限
/// </summary> /// </summary>
/// <param name="id">设备主键 Id</param> /// <param name="id">设备主键 Id</param>
[HttpDelete("{id}")] [HttpDelete("{id}")]
[RequirePermission("device:edit")]
public async Task<IActionResult> Delete(long id) public async Task<IActionResult> Delete(long id)
{ {
return Ok(await _deviceService.DeleteAsync(id)); return Ok(await _deviceService.DeleteAsync(id));
@@ -97,11 +103,12 @@ namespace WebAPI.Controllers
} }
/// <summary> /// <summary>
/// 按物模型可写点向设备下发指令 /// 按物模型可写点向设备下发指令(需 device:control 权限)
/// </summary> /// </summary>
/// <param name="id">设备主键 Id</param> /// <param name="id">设备主键 Id</param>
/// <param name="dto">指令请求(PointId 点Id + Value 工程值 + IsSimulated 是否模拟)</param> /// <param name="dto">指令请求(PointId 点Id + Value 工程值 + IsSimulated 是否模拟)</param>
[HttpPost("{id}/command")] [HttpPost("{id}/command")]
[RequirePermission("device:control")]
public async Task<IActionResult> SendCommand(long id, [FromBody] DeviceCommandDto dto) public async Task<IActionResult> SendCommand(long id, [FromBody] DeviceCommandDto dto)
{ {
if (dto == null) if (dto == null)
@@ -1,14 +1,114 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Service.Interface;
using System.Threading.Tasks;
namespace WebAPI.Controllers namespace WebAPI.Controllers
{ {
/// <summary> /// <summary>
/// 统计报表 /// 统计报表(设备统计/维护报表/维修报表)
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/inspection/report")] [Route("api/inspection/report")]
public class StatisticsReportController : ControllerBase public class StatisticsReportController : ControllerBase
{ {
// TODO: 实现 统计报表 相关接口 private readonly IStatisticsReportService _statisticsReportService;
public StatisticsReportController(IStatisticsReportService statisticsReportService)
{
_statisticsReportService = statisticsReportService;
}
#region
/// <summary>
/// 设备统计概览(总数/在用/闲置/报废等)
/// </summary>
[HttpGet("device/statistics")]
public async Task<IActionResult> GetDeviceStatistics()
{
return Ok(await _statisticsReportService.GetDeviceStatisticsAsync());
}
/// <summary>
/// 按分类统计设备数量
/// </summary>
[HttpGet("device/category")]
public async Task<IActionResult> GetCategoryStatistics()
{
return Ok(await _statisticsReportService.GetCategoryStatisticsAsync());
}
/// <summary>
/// 按部门统计设备数量
/// </summary>
[HttpGet("device/department")]
public async Task<IActionResult> GetDepartmentStatistics()
{
return Ok(await _statisticsReportService.GetDepartmentStatisticsAsync());
}
#endregion
#region
/// <summary>
/// 设备维护统计(校准/保养及时率、超期数量等)
/// </summary>
[HttpGet("maintenance/statistics")]
public async Task<IActionResult> GetMaintenanceStatistics()
{
return Ok(await _statisticsReportService.GetMaintenanceStatisticsAsync());
}
/// <summary>
/// 维护超期设备明细
/// </summary>
[HttpGet("maintenance/overdue")]
public async Task<IActionResult> GetMaintenanceOverdueDetails()
{
return Ok(await _statisticsReportService.GetMaintenanceOverdueDetailsAsync());
}
#endregion
#region
/// <summary>
/// 维修统计概览
/// </summary>
[HttpGet("repair/statistics")]
public async Task<IActionResult> GetRepairStatistics()
{
return Ok(await _statisticsReportService.GetRepairStatisticsAsync());
}
/// <summary>
/// 故障类型分布
/// </summary>
[HttpGet("repair/fault-type")]
public async Task<IActionResult> GetFaultTypeDistribution()
{
return Ok(await _statisticsReportService.GetFaultTypeDistributionAsync());
}
/// <summary>
/// 维修工时分析
/// </summary>
[HttpGet("repair/hours-analysis")]
public async Task<IActionResult> GetRepairHoursAnalysis()
{
return Ok(await _statisticsReportService.GetRepairHoursAnalysisAsync());
}
/// <summary>
/// 维修记录明细列表
/// </summary>
[HttpGet("repair/records")]
public async Task<IActionResult> GetRepairRecordDetails()
{
return Ok(await _statisticsReportService.GetRepairRecordDetailsAsync());
}
#endregion
} }
} }
+31 -2
View File
@@ -1,14 +1,43 @@
using Microsoft.AspNetCore.Authorization;
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>
/// 操作审计日志 /// 操作审计日志(满足实验室审计要求;需 user:manage 权限)
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/system/audit")] [Route("api/system/audit")]
[RequirePermission("user:manage")]
public class AuditController : ControllerBase public class AuditController : ControllerBase
{ {
// TODO: 实现 操作审计日志 相关接口 private readonly IAuditLogService _auditLogService;
public AuditController(IAuditLogService auditLogService)
{
_auditLogService = auditLogService;
}
/// <summary>
/// 审计日志列表(分页;筛选:操作人/描述关键字、操作类型、操作对象、时间段)
/// </summary>
[HttpGet("list")]
public async Task<IActionResult> GetList(int pageIndex = 1, int pageSize = 20,
string? keyword = null, string? operationType = null, string? operationTarget = null,
DateTime? startTime = null, DateTime? endTime = null)
{
RefAsync<int> total = 0;
var result = await _auditLogService.GetPagedAsync(pageIndex, pageSize, total,
keyword, operationType, operationTarget, startTime, endTime);
Response.Headers["X-Total-Count"] = total.Value.ToString();
return result.IsSuccess
? Ok(Result<List<AuditLogDto>>.Success(result.Data))
: Ok(Result<List<AuditLogDto>>.Error(result.Msg));
}
} }
} }
@@ -0,0 +1,65 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Model;
using Model.Dto.System;
using Service.Interface;
using WebAPI.Filters;
namespace WebAPI.Controllers
{
/// <summary>
/// 认证授权(登录/登出/当前用户/改密)
/// </summary>
[ApiController]
[Route("api/system/auth")]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
public AuthController(IAuthService authService)
{
_authService = authService;
}
/// <summary>
/// 登录(用户名+密码,成功返回 Token 与权限列表)
/// </summary>
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login([FromBody] LoginDto dto)
{
var ip = HttpContext.Connection.RemoteIpAddress?.ToString();
return Ok(await _authService.LoginAsync(dto, ip));
}
/// <summary>
/// 退出登录(记录审计日志)
/// </summary>
[HttpPost("logout")]
[Authorize]
public async Task<IActionResult> Logout()
{
return Ok(await _authService.LogoutAsync());
}
/// <summary>
/// 当前登录用户信息 + 权限列表
/// </summary>
[HttpGet("me")]
[Authorize]
public async Task<IActionResult> Me()
{
return Ok(await _authService.GetMeAsync());
}
/// <summary>
/// 修改自己的密码
/// </summary>
[HttpPost("change-password")]
[Authorize]
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordDto dto)
{
return Ok(await _authService.ChangePasswordAsync(dto));
}
}
}
+109 -2
View File
@@ -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,69 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Model;
using Model.Dto.System;
using Service.Interface;
using WebAPI.Filters;
namespace WebAPI.Controllers namespace WebAPI.Controllers
{ {
/// <summary> /// <summary>
/// 组织架构管理 /// 组织架构管理(公司/实验室/部门/班组树;需 user:manage 权限)
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/system/organization")] [Route("api/system/organization")]
[RequirePermission("user:manage")]
public class OrganizationController : ControllerBase public class OrganizationController : ControllerBase
{ {
// TODO: 实现 组织架构管理 相关接口 private readonly IOrgService _orgService;
public OrganizationController(IOrgService orgService)
{
_orgService = orgService;
}
/// <summary>
/// 完整组织树(嵌套子级,含班组白/夜班)
/// </summary>
[HttpGet("tree")]
public async Task<IActionResult> GetTree()
{
return Ok(await _orgService.GetTreeAsync());
}
/// <summary>
/// 组织下拉选项(平铺)
/// </summary>
[HttpGet("options")]
public async Task<IActionResult> GetOptions()
{
return Ok(await _orgService.GetOptionsAsync());
}
/// <summary>
/// 新增组织节点(ParentId 传父级Id,根节点传 "0"
/// </summary>
[HttpPost]
public async Task<IActionResult> Add([FromBody] OrgDto dto)
{
return Ok(await _orgService.AddAsync(dto));
}
/// <summary>
/// 修改组织节点
/// </summary>
[HttpPut]
public async Task<IActionResult> Update([FromBody] OrgDto dto)
{
return Ok(await _orgService.UpdateAsync(dto));
}
/// <summary>
/// 删除组织节点(有子级或挂靠用户时不可删)
/// </summary>
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
return Ok(await _orgService.DeleteAsync(id));
}
} }
} }
+91 -2
View File
@@ -1,14 +1,103 @@
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>
/// 角色权限管理 /// 角色权限管理(需 user:manage 权限)
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/system/role")] [Route("api/system/role")]
[RequirePermission("user:manage")]
public class RoleController : ControllerBase public class RoleController : ControllerBase
{ {
// TODO: 实现 角色权限管理 相关接口 private readonly IRoleService _roleService;
public RoleController(IRoleService roleService)
{
_roleService = roleService;
}
/// <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 _roleService.GetPagedAsync(pageIndex, pageSize, total, keyword);
Response.Headers["X-Total-Count"] = total.Value.ToString();
return result.IsSuccess
? Ok(Result<List<RoleDto>>.Success(result.Data))
: Ok(Result<List<RoleDto>>.Error(result.Msg));
}
/// <summary>
/// 角色下拉选项
/// </summary>
[HttpGet("options")]
public async Task<IActionResult> GetOptions()
{
return Ok(await _roleService.GetOptionsAsync());
}
/// <summary>
/// 全部权限列表(分配权限用)
/// </summary>
[HttpGet("permissions")]
public async Task<IActionResult> GetPermissions()
{
return Ok(await _roleService.GetPermissionsAsync());
}
/// <summary>
/// 角色详情(含权限Id列表)
/// </summary>
[HttpGet("{id}")]
public async Task<IActionResult> GetById(long id)
{
return Ok(await _roleService.GetByIdAsync(id));
}
/// <summary>
/// 新增角色(PermissionIds 传权限)
/// </summary>
[HttpPost]
public async Task<IActionResult> Add([FromBody] RoleDto dto)
{
return Ok(await _roleService.AddAsync(dto));
}
/// <summary>
/// 修改角色(PermissionIds 传 null 不改权限)
/// </summary>
[HttpPut]
public async Task<IActionResult> Update([FromBody] RoleDto dto)
{
return Ok(await _roleService.UpdateAsync(dto));
}
/// <summary>
/// 删除角色(系统内置角色不可删)
/// </summary>
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
return Ok(await _roleService.DeleteAsync(id));
}
/// <summary>
/// 分配权限(全量覆盖)
/// </summary>
[HttpPost("{id}/permissions")]
public async Task<IActionResult> AssignPermissions(long id, [FromBody] List<string> permissionIds)
{
var ids = permissionIds?.Select(long.Parse).Where(p => p > 0).ToList() ?? new List<long>();
return Ok(await _roleService.AssignPermissionsAsync(id, ids));
}
} }
} }
@@ -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&amp;k2&amp;k3</summary>
[HttpGet("values")]
public async Task<IActionResult> GetValues([FromQuery] string[] keys)
{
return Ok(await _paramService.GetValuesAsync(keys));
}
} }
} }
+112 -2
View File
@@ -1,14 +1,124 @@
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>
/// 用户管理 /// 用户管理(需 user:manage 权限)
/// </summary> /// </summary>
[ApiController] [ApiController]
[Route("api/system/user")] [Route("api/system/user")]
[RequirePermission("user:manage")]
public class UserController : ControllerBase public class UserController : ControllerBase
{ {
// TODO: 实现 用户管理 相关接口 private readonly IUserService _userService;
public UserController(IUserService userService)
{
_userService = userService;
}
/// <summary>
/// 用户列表(分页,关键字匹配用户名/姓名,可按角色、组织节点筛选;受数据范围约束)
/// </summary>
[HttpGet("list")]
public async Task<IActionResult> GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, long roleId = 0, long orgId = 0)
{
RefAsync<int> total = 0;
var result = await _userService.GetPagedAsync(pageIndex, pageSize, total, keyword, roleId > 0 ? roleId : null, orgId > 0 ? orgId : null);
Response.Headers["X-Total-Count"] = total.Value.ToString();
return result.IsSuccess
? Ok(Result<List<UserDto>>.Success(result.Data))
: Ok(Result<List<UserDto>>.Error(result.Msg));
}
/// <summary>
/// 用户下拉选项(启用中的用户)
/// </summary>
[HttpGet("options")]
public async Task<IActionResult> GetOptions()
{
return Ok(await _userService.GetOptionsAsync());
}
/// <summary>
/// 用户详情
/// </summary>
[HttpGet("{id}")]
public async Task<IActionResult> GetById(long id)
{
return Ok(await _userService.GetByIdAsync(id));
}
/// <summary>
/// 新增用户(InitialPassword 传初始密码,RoleIds 传角色)
/// </summary>
[HttpPost]
public async Task<IActionResult> Add([FromBody] UserDto dto)
{
return Ok(await _userService.AddAsync(dto));
}
/// <summary>
/// 修改用户(RoleIds 传 null 不改角色;密码走重置接口)
/// </summary>
[HttpPut]
public async Task<IActionResult> Update([FromBody] UserDto dto)
{
return Ok(await _userService.UpdateAsync(dto));
}
/// <summary>
/// 删除用户(软删除)
/// </summary>
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
return Ok(await _userService.DeleteAsync(id));
}
/// <summary>
/// 分配角色(全量覆盖)
/// </summary>
[HttpPost("{id}/roles")]
public async Task<IActionResult> AssignRoles(long id, [FromBody] List<string> roleIds)
{
var ids = roleIds?.Select(long.Parse).Where(r => r > 0).ToList() ?? new List<long>();
return Ok(await _userService.AssignRolesAsync(id, ids));
}
/// <summary>
/// 重置用户密码
/// </summary>
[HttpPost("{id}/reset-password")]
public async Task<IActionResult> ResetPassword(long id, [FromBody] ResetPasswordRequest request)
{
if (request == null || string.IsNullOrWhiteSpace(request.NewPassword))
return Ok(Result.Error("新密码不能为空"));
return Ok(await _userService.ResetPasswordAsync(id, request.NewPassword));
}
/// <summary>
/// 启用/禁用用户
/// </summary>
[HttpPost("{id}/enabled")]
public async Task<IActionResult> SetEnabled(long id, [FromBody] EnabledRequest request)
{
return Ok(await _userService.SetEnabledAsync(id, request?.Enabled ?? true));
}
public class ResetPasswordRequest
{
public string NewPassword { get; set; } = "";
}
public class EnabledRequest
{
public bool Enabled { get; set; } = true;
}
} }
} }
+4
View File
@@ -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;
} }
@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Model;
namespace WebAPI.Filters
{
/// <summary>
/// 权限校验过滤器:标注在 Controller 或 Action 上,执行前校验当前用户(JWT Claims)是否拥有指定权限
/// 用法:[RequirePermission("device:edit")]
/// 权限编码在登录时随 JWT 写入 Claims,此处只读 Claims,不查库
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class RequirePermissionAttribute : Attribute, IAsyncAuthorizationFilter
{
private readonly string _permissionCode;
public RequirePermissionAttribute(string permissionCode)
{
_permissionCode = permissionCode;
}
public Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
// 已被 [AllowAnonymous] 标记的接口放行(如登录本身)
if (context.ActionDescriptor.EndpointMetadata.Any(m => m is IAllowAnonymous))
return Task.CompletedTask;
var user = context.HttpContext.User;
if (user?.Identity?.IsAuthenticated != true)
{
context.Result = new UnauthorizedObjectResult(Result.Error("未登录或 Token 已失效"));
return Task.CompletedTask;
}
var perms = user.FindFirst("permissions")?.Value;
var permList = string.IsNullOrWhiteSpace(perms)
? new List<string>()
: perms.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
if (!permList.Contains(_permissionCode) && !permList.Contains("*"))
{
context.Result = new ObjectResult(Result.Error($"没有操作权限(需要权限:{_permissionCode}"))
{
StatusCode = StatusCodes.Status403Forbidden
};
}
return Task.CompletedTask;
}
}
}
+1
View File
@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper" Version="16.2.0" /> <PackageReference Include="AutoMapper" Version="16.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup> </ItemGroup>
+35
View File
@@ -1,5 +1,10 @@
using Common; using Common;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using ORM; using ORM;
using Service.Implement;
using Service.Interface;
using System.Text;
using System.Text.Json; using System.Text.Json;
using WebAPI.Services; using WebAPI.Services;
@@ -32,6 +37,9 @@ namespace WebAPI
DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true); DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
SqlSugarContext.InitDatabase(); SqlSugarContext.InitDatabase();
// RBAC 数据种子:预定义角色/权限/默认管理员(幂等)
DataSeeder.Seed();
// 飞书自建应用凭证:用于告警 @ 时把接收人手机号转成 open_id(webhook 机器人自身无此能力) // 飞书自建应用凭证:用于告警 @ 时把接收人手机号转成 open_id(webhook 机器人自身无此能力)
var feishu = builder.Configuration.GetSection("Feishu"); var feishu = builder.Configuration.GetSection("Feishu");
Common.Notify.FeishuConfig.Init(feishu["AppId"] ?? "", feishu["AppSecret"] ?? ""); Common.Notify.FeishuConfig.Init(feishu["AppId"] ?? "", feishu["AppSecret"] ?? "");
@@ -51,6 +59,31 @@ namespace WebAPI
// 自动注册业务服务(Service.Interface -> Service.Implement // 自动注册业务服务(Service.Interface -> Service.Implement
builder.Services.AddBusinessServices(); builder.Services.AddBusinessServices();
// 内存缓存:数据字典等查询频繁的基础数据按 typeCode 缓存,增删改时失效
builder.Services.AddMemoryCache();
// 当前用户上下文(从 JWT Claims 解析,供业务服务取操作人/做权限判断)
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUser, CurrentUser>();
// JWT 认证:登录后签发 Token,后续请求凭 Bearer Token 识别用户与角色
var jwt = builder.Configuration.GetSection("Jwt");
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwt["Issuer"],
ValidAudience = jwt["Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt["SecretKey"]!)),
ClockSkew = TimeSpan.FromSeconds(30)
};
});
// 告警通知渠道(飞书/钉钉/企微 Notifier+ 后台推送 Worker // 告警通知渠道(飞书/钉钉/企微 Notifier+ 后台推送 Worker
builder.Services.AddAlertNotification(); builder.Services.AddAlertNotification();
builder.Services.AddHostedService<AlertNotifyWorker>(); builder.Services.AddHostedService<AlertNotifyWorker>();
@@ -74,6 +107,8 @@ namespace WebAPI
// 导致前端 vite 代理被 CORS 拦截(Network Error);上位机/现场部署通常也只用 http // 导致前端 vite 代理被 CORS 拦截(Network Error);上位机/现场部署通常也只用 http
// app.UseHttpsRedirection(); // app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
// 静态文件:设备附件上传后通过 /uploads/... 访问(存储于 wwwroot // 静态文件:设备附件上传后通过 /uploads/... 访问(存储于 wwwroot
+62
View File
@@ -0,0 +1,62 @@
using System.Security.Claims;
using Service.Interface;
namespace WebAPI.Services
{
/// <summary>
/// 当前登录用户上下文实现:从 JWT ClaimsHttpContext.User)解析
/// </summary>
public class CurrentUser : ICurrentUser
{
private readonly IHttpContextAccessor _accessor;
public CurrentUser(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
private ClaimsPrincipal? Principal => _accessor?.HttpContext?.User;
public bool IsAuthenticated => Principal?.Identity?.IsAuthenticated ?? false;
public long UserId
{
get
{
var v = Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return long.TryParse(v, out var id) ? id : 0;
}
}
public string UserName => Principal?.FindFirst(ClaimTypes.Name)?.Value ?? "";
public List<long> RoleIds => ParseList("roleIds").Select(long.Parse).ToList();
public List<string> RoleNames => ParseList("roleNames");
public List<string> Permissions => ParseList("permissions");
public byte DataScope
{
get
{
var v = Principal?.FindFirst("dataScope")?.Value;
return byte.TryParse(v, out var b) ? b : (byte)2;
}
}
public bool HasPermission(string code)
{
if (!IsAuthenticated) return false;
var perms = Permissions;
return perms.Contains(code) || perms.Contains("*");
}
private List<string> ParseList(string claimType)
{
var raw = Principal?.FindFirst(claimType)?.Value;
if (string.IsNullOrWhiteSpace(raw)) return new List<string>();
return raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
}
}
}
+6
View File
@@ -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; }
+38
View File
@@ -0,0 +1,38 @@
namespace Model.Dto.Asset
{
/// <summary>
/// 设备二维码信息(生成/列表/详情共用)
/// </summary>
public class QrCodeDto
{
/// <summary>设备 Idlong → 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; }
}
}
+56
View File
@@ -0,0 +1,56 @@
namespace Model.Dto.Config
{
/// <summary>
/// 网关 DTO
/// </summary>
public class GatewayDto
{
public string Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public int ProtocolType { get; set; }
// TCP
public string? Host { get; set; }
public int? Port { get; set; }
// 串口
public string? ComPort { get; set; }
public int? BaudRate { get; set; }
public byte? DataBits { get; set; }
public byte? StopBits { get; set; }
public byte? Parity { get; set; }
// S7
public string? CpuType { get; set; }
public byte? Rack { get; set; }
public byte? Slot { get; set; }
// 状态
public bool IsEnabled { get; set; }
public byte OnlineStatus { get; set; }
public DateTime? LastConnectedTime { get; set; }
public string? LastError { get; set; }
public string? Remark { get; set; }
public DateTime? CreateTime { get; set; }
}
/// <summary>
/// 网关下拉选项 DTO(设备表单选用)
/// </summary>
public class GatewayOptionDto
{
public string Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
}
/// <summary>
/// 测试连接结果 DTO
/// </summary>
public class GatewayTestResultDto
{
public bool Success { get; set; }
public string Message { get; set; }
}
}
+5 -2
View File
@@ -25,8 +25,11 @@ namespace Model.Dto.Config
/// <summary>所属产品型号/名称(只读展示)</summary> /// <summary>所属产品型号/名称(只读展示)</summary>
public string? ProductName { get; set; } public string? ProductName { get; set; }
/// <summary>所属网关Code</summary> /// <summary>所属网关Idstring0=未分配)</summary>
public string GatewayCode { get; set; } public string GatewayId { get; set; }
/// <summary>所属网关名称(只读展示)</summary>
public string? GatewayName { get; set; }
/// <summary>从站地址</summary> /// <summary>从站地址</summary>
public byte SlaveId { get; set; } public byte SlaveId { get; set; }
+212
View File
@@ -0,0 +1,212 @@
namespace Model.Dto.Inspection
{
#region
/// <summary>
/// 设备统计概览
/// </summary>
public class DeviceStatisticsDto
{
/// <summary>设备总数</summary>
public int Total { get; set; }
/// <summary>在用数量</summary>
public int InUse { get; set; }
/// <summary>闲置数量</summary>
public int Idle { get; set; }
/// <summary>报废数量(已处置+待处置)</summary>
public int Scrapped { get; set; }
/// <summary>维修中数量</summary>
public int Repairing { get; set; }
/// <summary>已停用数量</summary>
public int Suspended { get; set; }
/// <summary>其他数量(待验收/已借出/入库)</summary>
public int Other { get; set; }
}
/// <summary>
/// 按分类统计
/// </summary>
public class CategoryStatisticsDto
{
/// <summary>分类名称</summary>
public string? CategoryName { get; set; }
/// <summary>设备数量</summary>
public int Count { get; set; }
}
/// <summary>
/// 按部门统计
/// </summary>
public class DepartmentStatisticsDto
{
/// <summary>部门名称</summary>
public string? Department { get; set; }
/// <summary>设备数量</summary>
public int Count { get; set; }
}
#endregion
#region
/// <summary>
/// 设备维护统计
/// </summary>
public class MaintenanceStatisticsDto
{
/// <summary>设备总数</summary>
public int Total { get; set; }
/// <summary>校准正常数量</summary>
public int CalibrationNormal { get; set; }
/// <summary>校准超期数量</summary>
public int CalibrationOverdue { get; set; }
/// <summary>保养正常数量</summary>
public int MaintenanceNormal { get; set; }
/// <summary>保养超期数量</summary>
public int MaintenanceOverdue { get; set; }
/// <summary>校准及时率(%</summary>
public decimal CalibrationTimelyRate { get; set; }
/// <summary>保养及时率(%</summary>
public decimal MaintenanceTimelyRate { get; set; }
}
/// <summary>
/// 维护超期设备明细
/// </summary>
public class MaintenanceOverdueDetailDto
{
/// <summary>设备编号</summary>
public string? EquipmentCode { get; set; }
/// <summary>设备名称</summary>
public string? EquipmentName { get; set; }
/// <summary>部门</summary>
public string? Department { get; set; }
/// <summary>超期类型(校准超期/保养超期)</summary>
public string? OverdueType { get; set; }
/// <summary>到期日期</summary>
public DateTime? ExpireDate { get; set; }
/// <summary>超期天数</summary>
public int OverdueDays { get; set; }
}
#endregion
#region
/// <summary>
/// 维修统计概览
/// </summary>
public class RepairStatisticsDto
{
/// <summary>维修记录总数</summary>
public int Total { get; set; }
/// <summary>已完成数量</summary>
public int Completed { get; set; }
/// <summary>维修中数量</summary>
public int InProgress { get; set; }
/// <summary>待维修数量</summary>
public int Pending { get; set; }
/// <summary>平均维修工时(小时)</summary>
public decimal AvgRepairHours { get; set; }
/// <summary>总维修工时(小时)</summary>
public decimal TotalRepairHours { get; set; }
/// <summary>总维修费用(元)</summary>
public decimal TotalRepairCost { get; set; }
}
/// <summary>
/// 故障类型分布
/// </summary>
public class FaultTypeDistributionDto
{
/// <summary>故障类型</summary>
public string? FaultType { get; set; }
/// <summary>数量</summary>
public int Count { get; set; }
}
/// <summary>
/// 维修工时分析
/// </summary>
public class RepairHoursAnalysisDto
{
/// <summary>故障类型</summary>
public string? FaultType { get; set; }
/// <summary>平均工时(小时)</summary>
public decimal AvgHours { get; set; }
/// <summary>最大工时(小时)</summary>
public decimal MaxHours { get; set; }
/// <summary>最小工时(小时)</summary>
public decimal MinHours { get; set; }
/// <summary>记录数</summary>
public int Count { get; set; }
}
/// <summary>
/// 维修记录明细
/// </summary>
public class RepairRecordDetailDto
{
/// <summary>设备编号</summary>
public string? EquipmentCode { get; set; }
/// <summary>设备名称</summary>
public string? EquipmentName { get; set; }
/// <summary>故障类型</summary>
public string? FaultType { get; set; }
/// <summary>故障描述</summary>
public string? FaultDescription { get; set; }
/// <summary>报修日期</summary>
public DateTime? ReportDate { get; set; }
/// <summary>维修完成日期</summary>
public DateTime? RepairEndDate { get; set; }
/// <summary>维修工时</summary>
public decimal? RepairHours { get; set; }
/// <summary>维修人员</summary>
public string? RepairPerson { get; set; }
/// <summary>维修费用</summary>
public decimal? RepairCost { get; set; }
/// <summary>维修状态</summary>
public string? RepairStatus { get; set; }
}
#endregion
}
+44
View File
@@ -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; }
}
}
+60
View File
@@ -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; }
}
}
+56
View File
@@ -0,0 +1,56 @@
namespace Model.Dto.System
{
/// <summary>
/// 组织架构树节点 DTO(嵌套子级;含数据权限摘要)
/// </summary>
public class OrgTreeDto
{
public string Id { get; set; }
public string ParentId { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public int OrgType { get; set; }
public int ShiftType { get; set; }
public string? Leader { get; set; }
public string? Phone { get; set; }
public int Sort { get; set; }
public string? Remark { get; set; }
public List<OrgTreeDto> Children { get; set; } = new();
/// <summary>直接挂靠用户数</summary>
public int UserCount { get; set; }
/// <summary>本组织及下级用户总数</summary>
public int TotalUserCount { get; set; }
/// <summary>数据权限摘要:本组织及下级用户的角色+数据范围(去重,如「实验室管理员·本组织及下级」)</summary>
public List<string> ScopeSummary { get; set; } = new();
}
/// <summary>
/// 组织架构编辑 DTO(新增/修改用)
/// </summary>
public class OrgDto
{
public string Id { get; set; }
public string ParentId { get; set; }
public string Name { get; set; }
public string Code { get; set; }
public int OrgType { get; set; }
public int ShiftType { get; set; }
public string? Leader { get; set; }
public string? Phone { get; set; }
public int Sort { get; set; }
public string? Remark { get; set; }
}
/// <summary>
/// 组织下拉选项 DTO(用户表单选组织用)
/// </summary>
public class OrgOptionDto
{
public string Id { get; set; }
public string Name { get; set; }
public int OrgType { get; set; }
}
}
+37
View File
@@ -0,0 +1,37 @@
namespace Model.Dto.System
{
/// <summary>
/// 权限 DTO
/// </summary>
public class PermissionDto
{
public string Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public string? Group { get; set; }
public byte IsSystem { get; set; }
public int Sort { get; set; }
}
/// <summary>
/// 审计日志 DTO
/// </summary>
public class AuditLogDto
{
public string Id { get; set; }
public string UserId { get; set; }
public string? UserName { get; set; }
public string RoleId { get; set; }
public string? RoleName { get; set; }
public string OperationType { get; set; }
public string OperationTarget { get; set; }
public string TargetId { get; set; }
public string DeviceId { get; set; }
public string? OldValue { get; set; }
public string? NewValue { get; set; }
public string? Ip { get; set; }
public string? Description { get; set; }
public DateTime OperateTime { get; set; }
public DateTime? CreateTime { get; set; }
}
}
+31
View File
@@ -0,0 +1,31 @@
namespace Model.Dto.System
{
/// <summary>
/// 角色 DTO
/// </summary>
public class RoleDto
{
public string Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public byte DataScope { get; set; }
public byte IsSystem { get; set; }
public int Sort { get; set; }
public string? Remark { get; set; }
public DateTime? CreateTime { get; set; }
/// <summary>角色的权限Id列表(分配权限用)</summary>
public List<string>? PermissionIds { get; set; }
/// <summary>角色的权限编码列表(展示用)</summary>
public List<string>? PermissionCodes { get; set; }
}
/// <summary>
/// 角色下拉选项 DTO
/// </summary>
public class RoleOptionDto
{
public string Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
}
}
+35
View File
@@ -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; }
}
}
+68
View File
@@ -0,0 +1,68 @@
namespace Model.Dto.System
{
/// <summary>
/// 用户 DTO
/// </summary>
public class UserDto
{
public string Id { get; set; }
public string UserName { get; set; }
public string? RealName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public string OrgId { get; set; }
public string? OrgName { get; set; }
public string? Position { get; set; }
public string? SkillTags { get; set; }
public byte IsEnabled { get; set; }
public DateTime? LastLoginTime { get; set; }
public string? Avatar { get; set; }
public string? Remark { get; set; }
public DateTime? CreateTime { get; set; }
/// <summary>用户的角色Id列表(分配角色用)</summary>
public List<string>? RoleIds { get; set; }
/// <summary>用户的角色名称(展示用,逗号分隔)</summary>
public string? RoleNames { get; set; }
/// <summary>初始密码(仅新增用户时传入,不回显)</summary>
public string? InitialPassword { get; set; }
}
/// <summary>
/// 登录请求 DTO
/// </summary>
public class LoginDto
{
public string UserName { get; set; }
public string Password { get; set; }
}
/// <summary>
/// 登录结果 DTO
/// </summary>
public class LoginResultDto
{
public string Token { get; set; }
public string RefreshToken { get; set; }
public UserDto User { get; set; }
public List<string> Permissions { get; set; } = new();
}
/// <summary>
/// 修改密码 DTO
/// </summary>
public class ChangePasswordDto
{
public string OldPassword { get; set; }
public string NewPassword { get; set; }
}
/// <summary>
/// 用户下拉选项 DTO
/// </summary>
public class UserOptionDto
{
public string Id { get; set; }
public string UserName { get; set; }
public string? RealName { get; set; }
}
}
+14 -1
View File
@@ -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>
+127
View File
@@ -0,0 +1,127 @@
using SqlSugar;
namespace Model.Entity.Config
{
/// <summary>
/// 网关(通讯通道):TCP 类存 IP+端口,串口类存 COM+波特率,S7 类额外存 CPU/Rack/Slot。
/// 同一网关下多设备用从站号(SlaveId)区分。
/// </summary>
public class GatewayEntity : BaseEntity
{
/// <summary>
/// 网关编码(唯一,如 GW-ENV-01)
/// </summary>
[SugarColumn(ColumnDescription = "网关编码", Length = 64)]
public string Code { get; set; }
/// <summary>
/// 网关名称
/// </summary>
[SugarColumn(ColumnDescription = "网关名称", Length = 100)]
public string Name { get; set; }
/// <summary>
/// 通讯协议类型(决定连接参数组:TCP/Serial/S7
/// </summary>
[SugarColumn(ColumnDescription = "通讯协议类型")]
public IotDeviceProtocolEnum ProtocolType { get; set; } = IotDeviceProtocolEnum.ModbusTcp;
#region TCP ModbusTcp / Tcp / S7 使
/// <summary>
/// IP 地址
/// </summary>
[SugarColumn(ColumnDescription = "IP地址", Length = 50, IsNullable = true)]
public string? Host { get; set; }
/// <summary>
/// 端口号(ModbusTcp 默认 502S7 默认 102
/// </summary>
[SugarColumn(ColumnDescription = "端口号", IsNullable = true)]
public int? Port { get; set; }
#endregion
#region ModbusRtu / Serial 使
/// <summary>
/// 串口号(如 COM3
/// </summary>
[SugarColumn(ColumnDescription = "串口号", Length = 20, IsNullable = true)]
public string? ComPort { get; set; }
/// <summary>
/// 波特率(默认 9600
/// </summary>
[SugarColumn(ColumnDescription = "波特率", IsNullable = true)]
public int? BaudRate { get; set; }
/// <summary>
/// 数据位(默认 8
/// </summary>
[SugarColumn(ColumnDescription = "数据位", IsNullable = true)]
public byte? DataBits { get; set; }
/// <summary>
/// 停止位(1/2,默认 1
/// </summary>
[SugarColumn(ColumnDescription = "停止位", IsNullable = true)]
public byte? StopBits { get; set; }
/// <summary>
/// 校验位(0=None 1=Odd 2=Even,默认 0
/// </summary>
[SugarColumn(ColumnDescription = "校验位", IsNullable = true)]
public byte? Parity { get; set; }
#endregion
#region S7 S7
/// <summary>
/// S7 CPU 型号(如 S71200、S71500
/// </summary>
[SugarColumn(ColumnDescription = "S7 CPU型号", Length = 20, IsNullable = true)]
public string? CpuType { get; set; }
/// <summary>
/// S7 Rack(默认 0
/// </summary>
[SugarColumn(ColumnDescription = "S7 Rack", IsNullable = true)]
public byte? Rack { get; set; }
/// <summary>
/// S7 Slot(默认 1
/// </summary>
[SugarColumn(ColumnDescription = "S7 Slot", IsNullable = true)]
public byte? Slot { get; set; }
#endregion
#region
/// <summary>
/// 是否启用
/// </summary>
[SugarColumn(ColumnDescription = "是否启用")]
public bool IsEnabled { get; set; } = true;
/// <summary>
/// 在线状态(0=离线 1=在线 3=异常,由测试连接更新)
/// </summary>
[SugarColumn(ColumnDescription = "在线状态")]
public byte OnlineStatus { get; set; }
/// <summary>
/// 最后连接成功时间
/// </summary>
[SugarColumn(ColumnDescription = "最后连接时间", IsNullable = true)]
public DateTime? LastConnectedTime { get; set; }
/// <summary>
/// 最近错误
/// </summary>
[SugarColumn(ColumnDescription = "最近错误", Length = 500, IsNullable = true)]
public string? LastError { get; set; }
#endregion
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
+3 -3
View File
@@ -34,10 +34,10 @@ namespace Model.Entity.Config
public long ProductId { get; set; } public long ProductId { get; set; }
/// <summary> /// <summary>
/// 所属网关Code(对应网关实体/通道的标识,连接时据此刻查找 IP/端口/串口 /// 所属网关Id(关联 GatewayEntity.Id0=未分配
/// </summary> /// </summary>
[SugarColumn(ColumnDescription = "所属网关Code", Length = 64)] [SugarColumn(ColumnDescription = "所属网关Id", DefaultValue = "0")]
public string GatewayCode { get; set; } public long GatewayId { get; set; }
/// <summary> /// <summary>
/// 从站地址(协议从站号,网关下区分设备) /// 从站地址(协议从站号,网关下区分设备)
@@ -0,0 +1,92 @@
using SqlSugar;
namespace Model.Entity.Inspection
{
/// <summary>
/// 设备维修记录
/// </summary>
public class RepairRecordEntity : BaseEntity
{
/// <summary>
/// 设备Id(关联 EquipmentEntity.Id
/// </summary>
public long EquipmentId { get; set; }
/// <summary>
/// 设备编号(冗余便于展示)
/// </summary>
[SugarColumn(Length = 50, IsNullable = true)]
public string? EquipmentCode { get; set; }
/// <summary>
/// 设备名称(冗余便于展示)
/// </summary>
[SugarColumn(Length = 100, IsNullable = true)]
public string? EquipmentName { get; set; }
/// <summary>
/// 故障类型(机械故障/电气故障/软件故障/传感器故障/其他)
/// </summary>
[SugarColumn(Length = 50, IsNullable = true)]
public string? FaultType { get; set; }
/// <summary>
/// 故障描述
/// </summary>
[SugarColumn(ColumnDataType = "text", IsNullable = true)]
public string? FaultDescription { get; set; }
/// <summary>
/// 报修日期
/// </summary>
public DateTime? ReportDate { get; set; }
/// <summary>
/// 维修开始日期
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? RepairStartDate { get; set; }
/// <summary>
/// 维修完成日期
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? RepairEndDate { get; set; }
/// <summary>
/// 维修工时(小时)
/// </summary>
[SugarColumn(IsNullable = true)]
public decimal? RepairHours { get; set; }
/// <summary>
/// 维修人员
/// </summary>
[SugarColumn(Length = 50, IsNullable = true)]
public string? RepairPerson { get; set; }
/// <summary>
/// 维修费用(元)
/// </summary>
[SugarColumn(DecimalDigits = 2, Length = 18, IsNullable = true)]
public decimal? RepairCost { get; set; }
/// <summary>
/// 维修状态(待维修/维修中/已完成)
/// </summary>
[SugarColumn(Length = 20, IsNullable = true)]
public string? RepairStatus { get; set; }
/// <summary>
/// 维修结果描述
/// </summary>
[SugarColumn(ColumnDataType = "text", IsNullable = true)]
public string? RepairResult { get; set; }
/// <summary>
/// 备注
/// </summary>
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
+50
View File
@@ -0,0 +1,50 @@
using SqlSugar;
namespace Model.Entity.System
{
/// <summary>
/// 审计日志表(记录所有关键操作)
/// </summary>
[SugarTable("sys_audit_log")]
public class AuditLogEntity : BaseEntity
{
[SugarColumn(ColumnName = "UserId", ColumnDescription = "操作人用户Id")]
public long UserId { get; set; }
[SugarColumn(ColumnName = "UserName", ColumnDescription = "操作人用户名", Length = 64, IsNullable = true)]
public string? UserName { get; set; }
[SugarColumn(ColumnName = "RoleId", ColumnDescription = "操作人角色Id0=无/多角色)", DefaultValue = "0")]
public long RoleId { get; set; }
[SugarColumn(ColumnName = "RoleName", ColumnDescription = "操作人角色名称", Length = 100, IsNullable = true)]
public string? RoleName { get; set; }
[SugarColumn(ColumnName = "OperationType", ColumnDescription = "操作类型(Create/Update/Delete/Login/Command等)", Length = 32, IsNullable = false)]
public string OperationType { get; set; }
[SugarColumn(ColumnName = "OperationTarget", ColumnDescription = "操作对象(如 IotDevice/Gateway/User/Role", Length = 64, IsNullable = false)]
public string OperationTarget { get; set; }
[SugarColumn(ColumnName = "TargetId", ColumnDescription = "操作对象Id0=无具体Id", DefaultValue = "0")]
public long TargetId { get; set; }
[SugarColumn(ColumnName = "DeviceId", ColumnDescription = "关联设备Id0=无关设备)", DefaultValue = "0")]
public long DeviceId { get; set; }
[SugarColumn(ColumnName = "OldValue", ColumnDescription = "操作前值(JSON", ColumnDataType = "text", IsNullable = true)]
public string? OldValue { get; set; }
[SugarColumn(ColumnName = "NewValue", ColumnDescription = "操作后值(JSON", ColumnDataType = "text", IsNullable = true)]
public string? NewValue { get; set; }
[SugarColumn(ColumnName = "Ip", ColumnDescription = "操作IP", Length = 45, IsNullable = true)]
public string? Ip { get; set; }
[SugarColumn(ColumnName = "Description", ColumnDescription = "操作描述", Length = 500, IsNullable = true)]
public string? Description { get; set; }
[SugarColumn(ColumnName = "OperateTime", ColumnDescription = "操作时间", IsNullable = false)]
public DateTime OperateTime { get; set; }
}
}
+32
View File
@@ -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; }
}
}
+26
View File
@@ -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; }
}
}
+50
View File
@@ -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>访问 URLEndpoint + 相对路径)</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>存储 Providerlocal/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 或 /uploadsMinIO 为 http://minio:9000</summary>
[SugarColumn(ColumnName = "Endpoint", ColumnDescription = "访问端点", Length = 255, IsNullable = true)]
public string? Endpoint { get; set; }
/// <summary>访问 KeyLocal 为空;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/uploadsMinIO/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; }
}
}
+40
View File
@@ -0,0 +1,40 @@
using SqlSugar;
namespace Model.Entity.System
{
/// <summary>
/// 组织架构表(树形:公司/实验室/部门/班组,ParentId 自关联;班组带白/夜班)
/// </summary>
[SugarTable("sys_org")]
public class OrgEntity : BaseEntity
{
[SugarColumn(ColumnName = "ParentId", ColumnDescription = "父级组织Id0=根节点)", DefaultValue = "0")]
public long ParentId { get; set; }
[SugarColumn(ColumnName = "Name", ColumnDescription = "组织名称", Length = 100, IsNullable = false)]
public string Name { get; set; }
[SugarColumn(ColumnName = "Code", ColumnDescription = "组织编码(唯一)", Length = 64, IsNullable = false)]
public string Code { get; set; }
/// <summary>组织类型:1=公司, 2=实验室, 3=部门, 4=班组</summary>
[SugarColumn(ColumnName = "OrgType", ColumnDescription = "组织类型(1公司/2实验室/3部门/4班组)", ColumnDataType = "smallint", DefaultValue = "3")]
public byte OrgType { get; set; }
/// <summary>班组班次:0=非班组, 1=白班, 2=夜班</summary>
[SugarColumn(ColumnName = "ShiftType", ColumnDescription = "班次(0非班组/1白班/2夜班)", ColumnDataType = "smallint", DefaultValue = "0")]
public byte ShiftType { get; set; }
[SugarColumn(ColumnName = "Leader", ColumnDescription = "负责人", Length = 50, IsNullable = true)]
public string? Leader { get; set; }
[SugarColumn(ColumnName = "Phone", ColumnDescription = "联系电话", Length = 20, IsNullable = true)]
public string? Phone { 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; }
}
}
+26
View File
@@ -0,0 +1,26 @@
using SqlSugar;
namespace Model.Entity.System
{
/// <summary>
/// 权限表(功能权限码,如 device:view / device:edit / alert:confirm
/// </summary>
[SugarTable("sys_permission")]
public class PermissionEntity : BaseEntity
{
[SugarColumn(ColumnName = "Code", ColumnDescription = "权限编码(唯一,如 device:view", Length = 64, IsNullable = false)]
public string Code { get; set; }
[SugarColumn(ColumnName = "Name", ColumnDescription = "权限名称", Length = 100, IsNullable = false)]
public string Name { get; set; }
[SugarColumn(ColumnName = "Group", ColumnDescription = "权限分组(如 device/alert/inspection/user", Length = 64, IsNullable = true)]
public string? Group { get; set; }
[SugarColumn(ColumnName = "IsSystem", ColumnDescription = "是否系统内置(不可删除)", ColumnDataType = "smallint", DefaultValue = "0")]
public byte IsSystem { get; set; }
[SugarColumn(ColumnName = "Sort", ColumnDescription = "排序号", DefaultValue = "0")]
public int Sort { get; set; }
}
}
+29
View File
@@ -0,0 +1,29 @@
using SqlSugar;
namespace Model.Entity.System
{
/// <summary>
/// 角色表
/// </summary>
[SugarTable("sys_role")]
public class RoleEntity : 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 = "DataScope", ColumnDescription = "数据范围(1=全部,2=本实验室)", ColumnDataType = "smallint", DefaultValue = "2")]
public byte DataScope { get; set; }
[SugarColumn(ColumnName = "IsSystem", ColumnDescription = "是否系统内置角色(不可删除)", ColumnDataType = "smallint", DefaultValue = "0")]
public byte IsSystem { 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,17 @@
using SqlSugar;
namespace Model.Entity.System
{
/// <summary>
/// 角色-权限关联表(多对多)
/// </summary>
[SugarTable("sys_role_permission")]
public class RolePermissionEntity : BaseEntity
{
[SugarColumn(ColumnName = "RoleId", ColumnDescription = "角色Id")]
public long RoleId { get; set; }
[SugarColumn(ColumnName = "PermissionId", ColumnDescription = "权限Id")]
public long PermissionId { get; set; }
}
}
+60
View File
@@ -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; }
}
}
+47
View File
@@ -0,0 +1,47 @@
using SqlSugar;
namespace Model.Entity.System
{
/// <summary>
/// 用户表
/// </summary>
[SugarTable("sys_user")]
public class UserEntity : BaseEntity
{
[SugarColumn(ColumnName = "UserName", ColumnDescription = "用户名(登录账号)", Length = 64, IsNullable = false)]
public string UserName { get; set; }
[SugarColumn(ColumnName = "PasswordHash", ColumnDescription = "密码哈希", Length = 256, IsNullable = false)]
public string PasswordHash { get; set; }
[SugarColumn(ColumnName = "RealName", ColumnDescription = "真实姓名", Length = 100, IsNullable = true)]
public string? RealName { get; set; }
[SugarColumn(ColumnName = "Email", ColumnDescription = "邮箱", Length = 128, IsNullable = true)]
public string? Email { get; set; }
[SugarColumn(ColumnName = "Phone", ColumnDescription = "手机号", Length = 20, IsNullable = true)]
public string? Phone { get; set; }
[SugarColumn(ColumnName = "OrgId", ColumnDescription = "所属组织Id(公司/实验室/部门/班组任一节点,0=未分配)", DefaultValue = "0")]
public long OrgId { get; set; }
[SugarColumn(ColumnName = "Position", ColumnDescription = "岗位", Length = 64, IsNullable = true)]
public string? Position { get; set; }
[SugarColumn(ColumnName = "SkillTags", ColumnDescription = "技能标签(逗号分隔,如 Modbus,PLC,温度箱)", Length = 500, IsNullable = true)]
public string? SkillTags { get; set; }
[SugarColumn(ColumnName = "IsEnabled", ColumnDescription = "是否启用", ColumnDataType = "smallint", DefaultValue = "1")]
public byte IsEnabled { get; set; }
[SugarColumn(ColumnName = "LastLoginTime", ColumnDescription = "最后登录时间", IsNullable = true)]
public DateTime? LastLoginTime { get; set; }
[SugarColumn(ColumnName = "Avatar", ColumnDescription = "头像URL", Length = 256, IsNullable = true)]
public string? Avatar { get; set; }
[SugarColumn(ColumnName = "Remark", ColumnDescription = "备注", Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
+17
View File
@@ -0,0 +1,17 @@
using SqlSugar;
namespace Model.Entity.System
{
/// <summary>
/// 用户-角色关联表(多对多)
/// </summary>
[SugarTable("sys_user_role")]
public class UserRoleEntity : BaseEntity
{
[SugarColumn(ColumnName = "UserId", ColumnDescription = "用户Id")]
public long UserId { get; set; }
[SugarColumn(ColumnName = "RoleId", ColumnDescription = "角色Id")]
public long RoleId { get; set; }
}
}
+513 -2
View File
@@ -1,9 +1,11 @@
using Model.Dto.Asset; using Model.Dto.Asset;
using Model.Dto.Config; using Model.Dto.Config;
using Model.Dto.Inspection; using Model.Dto.Inspection;
using Model.Dto.System;
using Model.Entity.Asset; using Model.Entity.Asset;
using Model.Entity.Config; using Model.Entity.Config;
using Model.Entity.Inspection; using Model.Entity.Inspection;
using Model.Entity.System;
namespace Model.Mapper namespace Model.Mapper
{ {
@@ -37,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,
@@ -96,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,
@@ -410,7 +416,7 @@ namespace Model.Mapper
Name = entity.Name, Name = entity.Name,
DeviceType = entity.DeviceType, DeviceType = entity.DeviceType,
ProductId = entity.ProductId.ToString(), ProductId = entity.ProductId.ToString(),
GatewayCode = entity.GatewayCode, GatewayId = entity.GatewayId.ToString(),
SlaveId = entity.SlaveId, SlaveId = entity.SlaveId,
Manufacturer = entity.Manufacturer, Manufacturer = entity.Manufacturer,
Description = entity.Description, Description = entity.Description,
@@ -445,7 +451,7 @@ namespace Model.Mapper
Name = dto.Name, Name = dto.Name,
DeviceType = dto.DeviceType, DeviceType = dto.DeviceType,
ProductId = ParseId(dto.ProductId), ProductId = ParseId(dto.ProductId),
GatewayCode = dto.GatewayCode, GatewayId = ParseId(dto.GatewayId),
SlaveId = dto.SlaveId, SlaveId = dto.SlaveId,
Manufacturer = dto.Manufacturer, Manufacturer = dto.Manufacturer,
Description = dto.Description, Description = dto.Description,
@@ -485,6 +491,99 @@ namespace Model.Mapper
} }
#endregion #endregion
#region
/// <summary>
/// GatewayEntity → GatewayDto
/// </summary>
public static GatewayDto ToDto(this GatewayEntity entity)
{
if (entity == null) return null;
return new GatewayDto
{
Id = entity.Id.ToString(),
Code = entity.Code,
Name = entity.Name,
ProtocolType = (int)entity.ProtocolType,
Host = entity.Host,
Port = entity.Port,
ComPort = entity.ComPort,
BaudRate = entity.BaudRate,
DataBits = entity.DataBits,
StopBits = entity.StopBits,
Parity = entity.Parity,
CpuType = entity.CpuType,
Rack = entity.Rack,
Slot = entity.Slot,
IsEnabled = entity.IsEnabled,
OnlineStatus = entity.OnlineStatus,
LastConnectedTime = entity.LastConnectedTime,
LastError = entity.LastError,
Remark = entity.Remark,
CreateTime = entity.CreateTime
};
}
/// <summary>
/// List&lt;GatewayEntity&gt; → List&lt;GatewayDto&gt;
/// </summary>
public static List<GatewayDto> ToDtoList(this List<GatewayEntity> entities)
{
return entities?.Select(e => e.ToDto()).ToList() ?? new List<GatewayDto>();
}
/// <summary>
/// GatewayDto → GatewayEntity(入参映射)
/// </summary>
public static GatewayEntity ToEntity(this GatewayDto dto)
{
if (dto == null) return null;
return new GatewayEntity
{
Id = ParseId(dto.Id),
Code = dto.Code,
Name = dto.Name,
ProtocolType = (IotDeviceProtocolEnum)dto.ProtocolType,
Host = dto.Host,
Port = dto.Port,
ComPort = dto.ComPort,
BaudRate = dto.BaudRate,
DataBits = dto.DataBits,
StopBits = dto.StopBits,
Parity = dto.Parity,
CpuType = dto.CpuType,
Rack = dto.Rack,
Slot = dto.Slot,
IsEnabled = dto.IsEnabled,
OnlineStatus = dto.OnlineStatus,
LastConnectedTime = dto.LastConnectedTime,
LastError = dto.LastError,
Remark = dto.Remark
};
}
/// <summary>
/// GatewayEntity → GatewayOptionDto(下拉选项)
/// </summary>
public static GatewayOptionDto ToOptionDto(this GatewayEntity entity)
{
if (entity == null) return null;
return new GatewayOptionDto
{
Id = entity.Id.ToString(),
Code = entity.Code,
Name = entity.Name
};
}
/// <summary>
/// List&lt;GatewayEntity&gt; → List&lt;GatewayOptionDto&gt;
/// </summary>
public static List<GatewayOptionDto> ToOptionDtoList(this List<GatewayEntity> entities)
{
return entities?.Select(e => e.ToOptionDto()).ToList() ?? new List<GatewayOptionDto>();
}
#endregion
#region #region
/// <summary> /// <summary>
/// ProductCategoryEntity → ProductCategoryDto /// ProductCategoryEntity → ProductCategoryDto
@@ -935,5 +1034,417 @@ namespace Model.Mapper
}; };
} }
#endregion #endregion
#region
public static UserDto ToDto(this UserEntity entity)
{
if (entity == null) return null;
return new UserDto
{
Id = entity.Id.ToString(),
UserName = entity.UserName,
RealName = entity.RealName,
Email = entity.Email,
Phone = entity.Phone,
OrgId = entity.OrgId.ToString(),
Position = entity.Position,
SkillTags = entity.SkillTags,
IsEnabled = entity.IsEnabled,
LastLoginTime = entity.LastLoginTime,
Avatar = entity.Avatar,
Remark = entity.Remark,
CreateTime = entity.CreateTime
};
}
public static List<UserDto> ToDtoList(this List<UserEntity> entities)
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<UserDto>();
public static UserEntity ToEntity(this UserDto dto)
{
if (dto == null) return null;
return new UserEntity
{
Id = ParseId(dto.Id),
UserName = dto.UserName,
RealName = dto.RealName,
Email = dto.Email,
Phone = dto.Phone,
OrgId = ParseId(dto.OrgId),
Position = dto.Position,
SkillTags = dto.SkillTags,
IsEnabled = dto.IsEnabled,
Avatar = dto.Avatar,
Remark = dto.Remark
};
}
public static UserOptionDto ToOptionDto(this UserEntity entity)
{
if (entity == null) return null;
return new UserOptionDto { Id = entity.Id.ToString(), UserName = entity.UserName, RealName = entity.RealName };
}
public static List<UserOptionDto> ToOptionDtoList(this List<UserEntity> entities)
=> entities?.Select(e => e.ToOptionDto()).ToList() ?? new List<UserOptionDto>();
#endregion
#region
public static RoleDto ToDto(this RoleEntity entity)
{
if (entity == null) return null;
return new RoleDto
{
Id = entity.Id.ToString(),
Code = entity.Code,
Name = entity.Name,
DataScope = entity.DataScope,
IsSystem = entity.IsSystem,
Sort = entity.Sort,
Remark = entity.Remark,
CreateTime = entity.CreateTime
};
}
public static List<RoleDto> ToDtoList(this List<RoleEntity> entities)
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<RoleDto>();
public static RoleEntity ToEntity(this RoleDto dto)
{
if (dto == null) return null;
return new RoleEntity
{
Id = ParseId(dto.Id),
Code = dto.Code,
Name = dto.Name,
DataScope = dto.DataScope,
IsSystem = dto.IsSystem,
Sort = dto.Sort,
Remark = dto.Remark
};
}
public static RoleOptionDto ToOptionDto(this RoleEntity entity)
{
if (entity == null) return null;
return new RoleOptionDto { Id = entity.Id.ToString(), Code = entity.Code, Name = entity.Name };
}
public static List<RoleOptionDto> ToOptionDtoList(this List<RoleEntity> entities)
=> entities?.Select(e => e.ToOptionDto()).ToList() ?? new List<RoleOptionDto>();
#endregion
#region
public static PermissionDto ToDto(this PermissionEntity entity)
{
if (entity == null) return null;
return new PermissionDto
{
Id = entity.Id.ToString(),
Code = entity.Code,
Name = entity.Name,
Group = entity.Group,
IsSystem = entity.IsSystem,
Sort = entity.Sort
};
}
public static List<PermissionDto> ToDtoList(this List<PermissionEntity> entities)
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<PermissionDto>();
public static PermissionEntity ToEntity(this PermissionDto dto)
{
if (dto == null) return null;
return new PermissionEntity
{
Id = ParseId(dto.Id),
Code = dto.Code,
Name = dto.Name,
Group = dto.Group,
IsSystem = dto.IsSystem,
Sort = dto.Sort
};
}
#endregion
#region
public static OrgTreeDto ToTreeDto(this OrgEntity entity)
{
if (entity == null) return null;
return new OrgTreeDto
{
Id = entity.Id.ToString(),
ParentId = entity.ParentId.ToString(),
Name = entity.Name,
Code = entity.Code,
OrgType = entity.OrgType,
ShiftType = entity.ShiftType,
Leader = entity.Leader,
Phone = entity.Phone,
Sort = entity.Sort,
Remark = entity.Remark
};
}
public static OrgDto ToDto(this OrgEntity entity)
{
if (entity == null) return null;
return new OrgDto
{
Id = entity.Id.ToString(),
ParentId = entity.ParentId.ToString(),
Name = entity.Name,
Code = entity.Code,
OrgType = entity.OrgType,
ShiftType = entity.ShiftType,
Leader = entity.Leader,
Phone = entity.Phone,
Sort = entity.Sort,
Remark = entity.Remark
};
}
public static List<OrgDto> ToDtoList(this List<OrgEntity> entities)
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<OrgDto>();
public static OrgEntity ToEntity(this OrgDto dto)
{
if (dto == null) return null;
return new OrgEntity
{
Id = ParseId(dto.Id),
ParentId = ParseId(dto.ParentId),
Name = dto.Name,
Code = dto.Code,
OrgType = (byte)dto.OrgType,
ShiftType = (byte)dto.ShiftType,
Leader = dto.Leader,
Phone = dto.Phone,
Sort = dto.Sort,
Remark = dto.Remark
};
}
public static OrgOptionDto ToOptionDto(this OrgEntity entity)
{
if (entity == null) return null;
return new OrgOptionDto { Id = entity.Id.ToString(), Name = entity.Name, OrgType = entity.OrgType };
}
public static List<OrgOptionDto> ToOptionDtoList(this List<OrgEntity> entities)
=> entities?.Select(e => e.ToOptionDto()).ToList() ?? new List<OrgOptionDto>();
#endregion
#region
public static AuditLogDto ToDto(this AuditLogEntity entity)
{
if (entity == null) return null;
return new AuditLogDto
{
Id = entity.Id.ToString(),
UserId = entity.UserId.ToString(),
UserName = entity.UserName,
RoleId = entity.RoleId.ToString(),
RoleName = entity.RoleName,
OperationType = entity.OperationType,
OperationTarget = entity.OperationTarget,
TargetId = entity.TargetId.ToString(),
DeviceId = entity.DeviceId.ToString(),
OldValue = entity.OldValue,
NewValue = entity.NewValue,
Ip = entity.Ip,
Description = entity.Description,
OperateTime = entity.OperateTime,
CreateTime = entity.CreateTime
};
}
public static List<AuditLogDto> ToDtoList(this List<AuditLogEntity> entities)
=> entities?.Select(e => e.ToDto()).ToList() ?? new List<AuditLogDto>();
#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
} }
} }
+217 -2
View File
@@ -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>";
}
} }
} }
@@ -3,6 +3,7 @@ using Model;
using Model.Dto.Config; using Model.Dto.Config;
using Model.Entity.Config; using Model.Entity.Config;
using ORM; using ORM;
using Service.Interface;
using Service.Interface.Config; using Service.Interface.Config;
using SqlSugar; using SqlSugar;
using System; using System;
@@ -14,10 +15,18 @@ namespace Service.Implement.Config
/// <summary> /// <summary>
/// 设备指令下发 服务实现 /// 设备指令下发 服务实现
/// 目前无网关运行时/采集引擎:每次调用先落一条设备日志(始终可验证), /// 目前无网关运行时/采集引擎:每次调用先落一条设备日志(始终可验证),
/// 设备在线时才实际尝试 Modbus 写(GatewayCode 需为 "ip:port",默认 127.0.0.1:502)。 /// 设备在线时才实际尝试 Modbus 写(连接参数从网关表按 GatewayId 加载)。
/// 每次下发都写审计日志(操作人/角色/时间/前后值),满足实验室审计要求。
/// </summary> /// </summary>
public class DeviceCommandService : IDeviceCommandService public class DeviceCommandService : IDeviceCommandService
{ {
private readonly IAuditRecorder _audit;
public DeviceCommandService(IAuditRecorder audit)
{
_audit = audit;
}
public async Task<Result> SendCommandAsync(DeviceCommandDto dto) public async Task<Result> SendCommandAsync(DeviceCommandDto dto)
{ {
if (dto == null || !long.TryParse(dto.Id, out var deviceId) || deviceId <= 0) if (dto == null || !long.TryParse(dto.Id, out var deviceId) || deviceId <= 0)
@@ -57,16 +66,26 @@ namespace Service.Implement.Config
: Result.Error("设备离线,指令已记录但未发送"); : Result.Error("设备离线,指令已记录但未发送");
} }
// 从网关表加载连接参数
var gateway = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
.Where(x => x.Id == device.GatewayId && x.IsDel == 0).FirstAsync();
if (gateway == null)
return Result.Error($"设备关联的网关不存在(GatewayId={device.GatewayId}");
if (string.IsNullOrWhiteSpace(gateway.Host) || !gateway.Port.HasValue)
return Result.Error($"网关「{gateway.Code}」未配置 IP 地址或端口号");
var host = gateway.Host!;
var port = gateway.Port!.Value;
// 实际 Modbus 写 // 实际 Modbus 写
var (host, port) = ParseGateway(device.GatewayCode);
using var modbus = new ModbusTcp(); using var modbus = new ModbusTcp();
modbus.ConfigureDevice(host, port, 3000, 3000); modbus.ConfigureDevice(host, port, 3000, 3000);
bool connected = await modbus.ConnectAsync(); bool connected = await modbus.ConnectAsync();
if (!connected) if (!connected)
{ {
await WriteLogAsync(device, "Error", "指令", await WriteLogAsync(device, "Error", "指令",
$"网关 {device.GatewayCode} 连接失败,指令未发送(点 {point.Name ?? point.Code} 值 {dto.Value}"); $"网关 {gateway.Code}({host}:{port}) 连接失败,指令未发送(点 {point.Name ?? point.Code} 值 {dto.Value}");
return Result.Error($"网关 {device.GatewayCode} 连接失败,指令未发送"); return Result.Error($"网关 {gateway.Code} 连接失败,指令未发送");
} }
string desc; string desc;
@@ -95,6 +114,10 @@ namespace Service.Implement.Config
.Where(x => x.Id == device.Id).ExecuteCommandAsync(); .Where(x => x.Id == device.Id).ExecuteCommandAsync();
await WriteLogAsync(device, "Info", "指令", $"下发成功:{desc}"); await WriteLogAsync(device, "Info", "指令", $"下发成功:{desc}");
await _audit.RecordAsync("Command", "IotDevice", device.Id, device.Id,
oldValue: null,
newValue: $"点={point.Code}, 值={dto.Value}, 模拟={dto.IsSimulated}",
description: $"设备 {device.Code} 下发指令:{desc}");
return Result.Success(); return Result.Success();
} }
catch (Exception ex) catch (Exception ex)
@@ -143,20 +166,6 @@ namespace Service.Implement.Config
return new[] { (ushort)(bits >> 16), (ushort)(bits & 0xFFFF) }; return new[] { (ushort)(bits >> 16), (ushort)(bits & 0xFFFF) };
} }
/// <summary>
/// 解析 GatewayCode 为 host:port;非法/缺失回退 127.0.0.1:502
/// </summary>
private static (string host, int port) ParseGateway(string? gatewayCode)
{
if (!string.IsNullOrWhiteSpace(gatewayCode))
{
var idx = gatewayCode.LastIndexOf(':');
if (idx > 0 && IPAddress.TryParse(gatewayCode[..idx], out _) && int.TryParse(gatewayCode[(idx + 1)..], out var p) && p > 0)
return (gatewayCode[..idx], p);
}
return ("127.0.0.1", 502);
}
/// <summary> /// <summary>
/// 写一条设备日志(设备独享日志,前端设备日志抽屉可见) /// 写一条设备日志(设备独享日志,前端设备日志抽屉可见)
/// </summary> /// </summary>
+58 -1
View File
@@ -3,11 +3,13 @@ using Model.Dto.Config;
using Model.Entity.Config; using Model.Entity.Config;
using Model.Mapper; using Model.Mapper;
using ORM; using ORM;
using Service.Interface;
using Service.Interface.Config; using Service.Interface.Config;
using SqlSugar; using SqlSugar;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Service.Implement.Config namespace Service.Implement.Config
@@ -17,10 +19,17 @@ namespace Service.Implement.Config
/// </summary> /// </summary>
public class DeviceService : IDeviceService public class DeviceService : IDeviceService
{ {
private readonly IAuditRecorder _audit;
public DeviceService(IAuditRecorder audit)
{
_audit = audit;
}
/// <summary> /// <summary>
/// 分页查询设备列表(支持关键字搜索编号/名称/类型,可按所属产品筛选) /// 分页查询设备列表(支持关键字搜索编号/名称/类型,可按所属产品筛选)
/// </summary> /// </summary>
public async Task<Result<List<IotDeviceDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword, long? productId = null) public async Task<Result<List<IotDeviceDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword, long? productId = null, long? gatewayId = null)
{ {
try try
{ {
@@ -29,11 +38,13 @@ namespace Service.Implement.Config
.WhereIF(!string.IsNullOrWhiteSpace(keyword), .WhereIF(!string.IsNullOrWhiteSpace(keyword),
x => x.Code.Contains(keyword!) || x.Name.Contains(keyword!) || x.DeviceType.Contains(keyword!)) x => x.Code.Contains(keyword!) || x.Name.Contains(keyword!) || x.DeviceType.Contains(keyword!))
.WhereIF(productId.HasValue && productId.Value > 0, x => x.ProductId == productId!.Value) .WhereIF(productId.HasValue && productId.Value > 0, x => x.ProductId == productId!.Value)
.WhereIF(gatewayId.HasValue && gatewayId.Value > 0, x => x.GatewayId == gatewayId!.Value)
.OrderBy(x => x.CreateTime, OrderByType.Desc) .OrderBy(x => x.CreateTime, OrderByType.Desc)
.ToPageListAsync(pageIndex, pageSize, total); .ToPageListAsync(pageIndex, pageSize, total);
var dtos = list.ToDtoList(); var dtos = list.ToDtoList();
await FillProductNames(dtos); await FillProductNames(dtos);
await FillGatewayNames(dtos);
return Result<List<IotDeviceDto>>.Success(dtos); return Result<List<IotDeviceDto>>.Success(dtos);
} }
catch (Exception ex) catch (Exception ex)
@@ -57,6 +68,7 @@ namespace Service.Implement.Config
var dto = entity.ToDto(); var dto = entity.ToDto();
await FillProductNames(new List<IotDeviceDto> { dto }); await FillProductNames(new List<IotDeviceDto> { dto });
await FillGatewayNames(new List<IotDeviceDto> { dto });
return Result<IotDeviceDto>.Success(dto); return Result<IotDeviceDto>.Success(dto);
} }
catch (Exception ex) catch (Exception ex)
@@ -87,6 +99,10 @@ namespace Service.Implement.Config
// 初始离线 // 初始离线
entity.OnlineStatus = IotDeviceOnlineStatusEnum.Offline; entity.OnlineStatus = IotDeviceOnlineStatusEnum.Offline;
await SqlSugarContext.DbContext.Insertable(entity).ExecuteCommandAsync(); await SqlSugarContext.DbContext.Insertable(entity).ExecuteCommandAsync();
await _audit.RecordAsync("Create", "IotDevice", entity.Id, entity.Id,
null, JsonSerializer.Serialize(new { entity.Code, entity.Name, entity.DeviceType, entity.GatewayId, entity.SlaveId }),
description: $"新增设备 {entity.Code}{entity.Name}");
return Result.Success(); return Result.Success();
} }
catch (Exception ex) catch (Exception ex)
@@ -112,9 +128,18 @@ namespace Service.Implement.Config
if (exists) if (exists)
return Result.Error($"设备编号【{entity.Code}】已存在"); return Result.Error($"设备编号【{entity.Code}】已存在");
// 操作前值(审计):先查旧记录
var oldEntity = await SqlSugarContext.DbContext.Queryable<IotDeviceEntity>()
.Where(x => x.Id == entity.Id).FirstAsync();
await SqlSugarContext.DbContext.Updateable(entity) await SqlSugarContext.DbContext.Updateable(entity)
.IgnoreColumns(x => new { x.CreateTime, x.IsDel, x.OnlineStatus, x.LastCollectTime, x.LastError }) .IgnoreColumns(x => new { x.CreateTime, x.IsDel, x.OnlineStatus, x.LastCollectTime, x.LastError })
.ExecuteCommandAsync(); .ExecuteCommandAsync();
await _audit.RecordAsync("Update", "IotDevice", entity.Id, entity.Id,
oldValue: oldEntity == null ? null : JsonSerializer.Serialize(new { oldEntity.Code, oldEntity.Name, oldEntity.DeviceType, oldEntity.GatewayId, oldEntity.SlaveId }),
newValue: JsonSerializer.Serialize(new { entity.Code, entity.Name, entity.DeviceType, entity.GatewayId, entity.SlaveId }),
description: $"修改设备 {entity.Code}{entity.Name}");
return Result.Success(); return Result.Success();
} }
catch (Exception ex) catch (Exception ex)
@@ -133,10 +158,17 @@ namespace Service.Implement.Config
try try
{ {
var oldEntity = await SqlSugarContext.DbContext.Queryable<IotDeviceEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
await SqlSugarContext.DbContext.Updateable<IotDeviceEntity>() await SqlSugarContext.DbContext.Updateable<IotDeviceEntity>()
.SetColumns(x => x.IsDel == 1) .SetColumns(x => x.IsDel == 1)
.Where(x => x.Id == id) .Where(x => x.Id == id)
.ExecuteCommandAsync(); .ExecuteCommandAsync();
await _audit.RecordAsync("Delete", "IotDevice", id, id,
oldValue: oldEntity == null ? null : JsonSerializer.Serialize(new { oldEntity.Code, oldEntity.Name }),
description: $"删除设备 {oldEntity?.Code}{oldEntity?.Name}");
return Result.Success(); return Result.Success();
} }
catch (Exception ex) catch (Exception ex)
@@ -165,6 +197,31 @@ namespace Service.Implement.Config
} }
} }
/// <summary>
/// 给设备 DTO 填充所属网关名称(GatewayName 展示用)
/// </summary>
private async Task FillGatewayNames(List<IotDeviceDto> dtos)
{
if (dtos == null || dtos.Count == 0)
return;
var ids = dtos.Where(x => long.TryParse(x.GatewayId, out var gid) && gid > 0)
.Select(x => long.Parse(x.GatewayId!)).Distinct().ToList();
if (ids.Count == 0)
return;
var gateways = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
.Where(x => ids.Contains(x.Id) && x.IsDel == 0)
.ToListAsync();
var map = gateways.ToDictionary(g => g.Id, g => $"{g.Code}({g.Name})");
foreach (var d in dtos)
{
if (long.TryParse(d.GatewayId, out var gid) && map.TryGetValue(gid, out var name))
d.GatewayName = name;
}
}
/// <summary> /// <summary>
/// 给设备 DTO 填充所属产品的型号/名称(ProductName 展示用) /// 给设备 DTO 填充所属产品的型号/名称(ProductName 展示用)
/// </summary> /// </summary>
+253 -3
View File
@@ -1,12 +1,262 @@
using Service.Interface; using Model;
using Model.Dto.Config;
using Model.Entity.Config;
using Model.Mapper;
using ORM;
using Service.Interface.Config;
using SqlSugar;
using System.IO.Ports;
using System.Net;
using System.Net.Sockets;
namespace Service.Implement namespace Service.Implement.Config
{ {
/// <summary> /// <summary>
/// 网关管理 服务实现 /// 网关管理 服务实现
/// </summary> /// </summary>
public class GatewayService : IGatewayService public class GatewayService : IGatewayService
{ {
// TODO: 实现 网关管理 相关方法 public async Task<Result<List<GatewayDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, int? protocolType = null)
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
.Where(x => x.IsDel == 0)
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.Code.Contains(keyword!) || x.Name.Contains(keyword!))
.WhereIF(protocolType.HasValue, x => x.ProtocolType == (IotDeviceProtocolEnum)protocolType!.Value)
.OrderBy(x => x.CreateTime, OrderByType.Desc)
.ToPageListAsync(pageIndex, pageSize, total);
return Result<List<GatewayDto>>.Success(list.ToDtoList());
}
catch (Exception ex)
{
return Result<List<GatewayDto>>.Error("查询网关列表失败", ex);
}
}
public async Task<Result<GatewayDto>> GetByIdAsync(long id)
{
try
{
var entity = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<GatewayDto>.Error("网关不存在");
return Result<GatewayDto>.Success(entity.ToDto());
}
catch (Exception ex)
{
return Result<GatewayDto>.Error("查询网关详情失败", ex);
}
}
public async Task<Result<GatewayDto>> AddAsync(GatewayDto dto)
{
try
{
// 编码唯一校验
var exists = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
.Where(x => x.Code == dto.Code && x.IsDel == 0).AnyAsync();
if (exists) return Result<GatewayDto>.Error($"网关编码「{dto.Code}」已存在");
var entity = dto.ToEntity();
entity.Id = 0; // 雪花ID自动生成
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
entity.Id = id;
return Result<GatewayDto>.Success(entity.ToDto());
}
catch (Exception ex)
{
return Result<GatewayDto>.Error("新增网关失败", ex);
}
}
public async Task<Result<GatewayDto>> UpdateAsync(GatewayDto dto)
{
try
{
if (!long.TryParse(dto.Id, out var id) || id <= 0)
return Result<GatewayDto>.Error("网关Id无效");
var entity = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<GatewayDto>.Error("网关不存在");
// 编码唯一校验(排除自身)
var codeExists = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
.Where(x => x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
if (codeExists) return Result<GatewayDto>.Error($"网关编码「{dto.Code}」已存在");
var newEntity = dto.ToEntity();
newEntity.Id = id;
await SqlSugarContext.DbContext.Updateable(newEntity)
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
.ExecuteCommandAsync();
return Result<GatewayDto>.Success(newEntity.ToDto());
}
catch (Exception ex)
{
return Result<GatewayDto>.Error("修改网关失败", ex);
}
}
public async Task<Result> DeleteAsync(long id)
{
try
{
var entity = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result.Error("网关不存在");
// 软删除网关
await SqlSugarContext.DbContext.Updateable<GatewayEntity>()
.SetColumns(x => x.IsDel == 1)
.Where(x => x.Id == id).ExecuteCommandAsync();
// 该网关下设备的 GatewayId 置 0(不级联删除设备)
await SqlSugarContext.DbContext.Updateable<IotDeviceEntity>()
.SetColumns(x => x.GatewayId == 0)
.Where(x => x.GatewayId == id).ExecuteCommandAsync();
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("删除网关失败", ex);
}
}
/// <summary>
/// 测试网关连接:TCP 尝试 TcpClient.ConnectAsync 3s 超时,串口尝试 SerialPort.Open
/// 成功则 OnlineStatus=Online+更新 LastConnectedTime,失败则 OnlineStatus=Fault+记录 LastError
/// </summary>
public async Task<Result<GatewayTestResultDto>> TestConnectionAsync(long id)
{
try
{
var entity = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<GatewayTestResultDto>.Error("网关不存在");
bool success = false;
string message = "";
var now = DateTime.Now;
switch (entity.ProtocolType)
{
case IotDeviceProtocolEnum.ModbusTcp:
case IotDeviceProtocolEnum.Tcp:
case IotDeviceProtocolEnum.S7:
// TCP 类:尝试连接 IP:Port
if (string.IsNullOrWhiteSpace(entity.Host) || !entity.Port.HasValue)
{
message = "未配置 IP 地址或端口号";
}
else
{
(success, message) = await TestTcpAsync(entity.Host, entity.Port!.Value, 3000);
}
break;
case IotDeviceProtocolEnum.ModbusRtu:
case IotDeviceProtocolEnum.Serial:
// 串口类:尝试打开串口
if (string.IsNullOrWhiteSpace(entity.ComPort))
{
message = "未配置串口号";
}
else
{
(success, message) = TestSerial(entity.ComPort, entity.BaudRate ?? 9600);
}
break;
default:
message = $"暂不支持 {entity.ProtocolType} 协议的连接测试";
break;
}
// 更新网关状态
await SqlSugarContext.DbContext.Updateable<GatewayEntity>()
.SetColumns(x => new GatewayEntity
{
OnlineStatus = success ? (byte)1 : (byte)3,
LastConnectedTime = success ? now : x.LastConnectedTime,
LastError = success ? null : message
})
.Where(x => x.Id == id).ExecuteCommandAsync();
return Result<GatewayTestResultDto>.Success(new GatewayTestResultDto
{
Success = success,
Message = success ? $"连接成功({entity.Host}:{entity.Port ?? 0}" : message
});
}
catch (Exception ex)
{
return Result<GatewayTestResultDto>.Error("测试连接失败", ex);
}
}
public async Task<Result<List<GatewayOptionDto>>> GetOptionsAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<GatewayEntity>()
.Where(x => x.IsDel == 0 && x.IsEnabled)
.OrderBy(x => x.Code)
.ToListAsync();
return Result<List<GatewayOptionDto>>.Success(list.ToOptionDtoList());
}
catch (Exception ex)
{
return Result<List<GatewayOptionDto>>.Error("查询网关选项失败", ex);
}
}
#region
private static async Task<(bool success, string message)> TestTcpAsync(string host, int port, int timeoutMs)
{
try
{
using var client = new TcpClient();
var cts = new System.Threading.CancellationTokenSource(timeoutMs);
await client.ConnectAsync(host, port, cts.Token);
return (true, "");
}
catch (OperationCanceledException)
{
return (false, $"连接超时({timeoutMs / 1000}s):{host}:{port}");
}
catch (SocketException ex)
{
return (false, $"连接失败:{host}:{port}{ex.Message}");
}
catch (Exception ex)
{
return (false, $"连接异常:{ex.Message}");
}
}
private static (bool success, string message) TestSerial(string comPort, int baudRate)
{
try
{
using var sp = new SerialPort(comPort, baudRate);
sp.Open();
sp.Close();
return (true, "");
}
catch (UnauthorizedAccessException)
{
return (false, $"串口 {comPort} 被占用");
}
catch (Exception ex)
{
return (false, $"打开串口 {comPort} 失败:{ex.Message}");
}
}
#endregion
} }
} }
@@ -1,4 +1,14 @@
using Model;
using Model.Dto.Inspection;
using Model.Entity.Asset;
using Model.Entity.Inspection;
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
{ {
@@ -7,6 +17,326 @@ namespace Service.Implement
/// </summary> /// </summary>
public class StatisticsReportService : IStatisticsReportService public class StatisticsReportService : IStatisticsReportService
{ {
// TODO: 实现 统计报表 相关方法 #region
/// <summary>
/// 设备统计概览(总数/在用/闲置/报废等)
/// </summary>
public async Task<Result<DeviceStatisticsDto>> GetDeviceStatisticsAsync()
{
try
{
var query = SqlSugarContext.DbContext.Queryable<EquipmentEntity>()
.Where(x => x.IsDel == 0);
var all = await query.ToListAsync();
var total = all.Count;
var dto = new DeviceStatisticsDto
{
Total = total,
InUse = all.Count(x => x.Status == EquipmentStatusEnum.InUse),
Idle = all.Count(x => x.Status == EquipmentStatusEnum.Idle),
Scrapped = all.Count(x => x.Status == EquipmentStatusEnum.Disposed || x.Status == EquipmentStatusEnum.PendingDisposal),
Repairing = all.Count(x => x.Status == EquipmentStatusEnum.Repairing),
Suspended = all.Count(x => x.Status == EquipmentStatusEnum.Suspended),
Other = all.Count(x =>
x.Status != EquipmentStatusEnum.InUse &&
x.Status != EquipmentStatusEnum.Idle &&
x.Status != EquipmentStatusEnum.Disposed &&
x.Status != EquipmentStatusEnum.PendingDisposal &&
x.Status != EquipmentStatusEnum.Repairing &&
x.Status != EquipmentStatusEnum.Suspended)
};
return Result<DeviceStatisticsDto>.Success(dto);
}
catch (Exception ex)
{
return Result<DeviceStatisticsDto>.Error("查询设备统计失败", ex);
}
}
/// <summary>
/// 按分类统计设备数量
/// </summary>
public async Task<Result<List<CategoryStatisticsDto>>> GetCategoryStatisticsAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<EquipmentEntity>()
.LeftJoin<EquipmentCategoryEntity>((e, c) => e.CategoryId == c.Id)
.Where((e, c) => e.IsDel == 0)
.GroupBy((e, c) => new { c.Name })
.Select((e, c) => new CategoryStatisticsDto
{
CategoryName = c.Name,
Count = SqlFunc.AggregateCount(e.Id)
})
.ToListAsync();
return Result<List<CategoryStatisticsDto>>.Success(list);
}
catch (Exception ex)
{
return Result<List<CategoryStatisticsDto>>.Error("查询分类统计失败", ex);
}
}
/// <summary>
/// 按部门统计设备数量
/// </summary>
public async Task<Result<List<DepartmentStatisticsDto>>> GetDepartmentStatisticsAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<EquipmentEntity>()
.Where(x => x.IsDel == 0)
.GroupBy(x => x.Department)
.Select(x => new DepartmentStatisticsDto
{
Department = x.Department,
Count = SqlFunc.AggregateCount(x.Id)
})
.OrderByDescending(x => x.Count)
.ToListAsync();
return Result<List<DepartmentStatisticsDto>>.Success(list);
}
catch (Exception ex)
{
return Result<List<DepartmentStatisticsDto>>.Error("查询部门统计失败", ex);
}
}
#endregion
#region
/// <summary>
/// 设备维护统计(校准/保养及时率、超期数量等)
/// </summary>
public async Task<Result<MaintenanceStatisticsDto>> GetMaintenanceStatisticsAsync()
{
try
{
var all = await SqlSugarContext.DbContext.Queryable<EquipmentEntity>()
.Where(x => x.IsDel == 0)
.ToListAsync();
var total = all.Count;
var calNormal = all.Count(x => x.CalibrationStatus == ComplianceStatusEnum.Normal);
var calOverdue = all.Count(x => x.CalibrationStatus == ComplianceStatusEnum.Overdue);
var maintNormal = all.Count(x => x.MaintenanceStatus == ComplianceStatusEnum.Normal);
var maintOverdue = all.Count(x => x.MaintenanceStatus == ComplianceStatusEnum.Overdue);
var dto = new MaintenanceStatisticsDto
{
Total = total,
CalibrationNormal = calNormal,
CalibrationOverdue = calOverdue,
MaintenanceNormal = maintNormal,
MaintenanceOverdue = maintOverdue,
CalibrationTimelyRate = total > 0 ? Math.Round((decimal)calNormal / total * 100, 1) : 0,
MaintenanceTimelyRate = total > 0 ? Math.Round((decimal)maintNormal / total * 100, 1) : 0
};
return Result<MaintenanceStatisticsDto>.Success(dto);
}
catch (Exception ex)
{
return Result<MaintenanceStatisticsDto>.Error("查询维护统计失败", ex);
}
}
/// <summary>
/// 维护超期设备明细
/// </summary>
public async Task<Result<List<MaintenanceOverdueDetailDto>>> GetMaintenanceOverdueDetailsAsync()
{
try
{
var now = DateTime.Now;
var list = await SqlSugarContext.DbContext.Queryable<EquipmentEntity>()
.Where(x => x.IsDel == 0 &&
(x.CalibrationStatus == ComplianceStatusEnum.Overdue ||
x.MaintenanceStatus == ComplianceStatusEnum.Overdue))
.ToListAsync();
var details = new List<MaintenanceOverdueDetailDto>();
foreach (var eq in list)
{
if (eq.CalibrationStatus == ComplianceStatusEnum.Overdue && eq.NextCalibrationDate.HasValue)
{
var days = (int)(now - eq.NextCalibrationDate.Value).TotalDays;
details.Add(new MaintenanceOverdueDetailDto
{
EquipmentCode = eq.Code,
EquipmentName = eq.Name,
Department = eq.Department,
OverdueType = "校准超期",
ExpireDate = eq.NextCalibrationDate,
OverdueDays = days > 0 ? days : 0
});
}
if (eq.MaintenanceStatus == ComplianceStatusEnum.Overdue && eq.NextMaintenanceDate.HasValue)
{
var days = (int)(now - eq.NextMaintenanceDate.Value).TotalDays;
details.Add(new MaintenanceOverdueDetailDto
{
EquipmentCode = eq.Code,
EquipmentName = eq.Name,
Department = eq.Department,
OverdueType = "保养超期",
ExpireDate = eq.NextMaintenanceDate,
OverdueDays = days > 0 ? days : 0
});
}
}
return Result<List<MaintenanceOverdueDetailDto>>.Success(details);
}
catch (Exception ex)
{
return Result<List<MaintenanceOverdueDetailDto>>.Error("查询维护超期明细失败", ex);
}
}
#endregion
#region
/// <summary>
/// 维修统计概览
/// </summary>
public async Task<Result<RepairStatisticsDto>> GetRepairStatisticsAsync()
{
try
{
var all = await SqlSugarContext.DbContext.Queryable<RepairRecordEntity>()
.Where(x => x.IsDel == 0)
.ToListAsync();
var total = all.Count;
var completed = all.Count(x => x.RepairStatus == "已完成");
var inProgress = all.Count(x => x.RepairStatus == "维修中");
var pending = all.Count(x => x.RepairStatus == "待维修");
var totalHours = all.Where(x => x.RepairHours.HasValue).Sum(x => x.RepairHours ?? 0);
var completedWithHours = all.Where(x => x.RepairHours.HasValue && x.RepairStatus == "已完成").ToList();
var avgHours = completedWithHours.Count > 0
? Math.Round(completedWithHours.Sum(x => x.RepairHours ?? 0) / completedWithHours.Count, 1)
: 0;
var totalCost = all.Where(x => x.RepairCost.HasValue).Sum(x => x.RepairCost ?? 0);
var dto = new RepairStatisticsDto
{
Total = total,
Completed = completed,
InProgress = inProgress,
Pending = pending,
AvgRepairHours = avgHours,
TotalRepairHours = totalHours,
TotalRepairCost = totalCost
};
return Result<RepairStatisticsDto>.Success(dto);
}
catch (Exception ex)
{
return Result<RepairStatisticsDto>.Error("查询维修统计失败", ex);
}
}
/// <summary>
/// 故障类型分布
/// </summary>
public async Task<Result<List<FaultTypeDistributionDto>>> GetFaultTypeDistributionAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<RepairRecordEntity>()
.Where(x => x.IsDel == 0)
.GroupBy(x => x.FaultType)
.Select(x => new FaultTypeDistributionDto
{
FaultType = x.FaultType,
Count = SqlFunc.AggregateCount(x.Id)
})
.OrderByDescending(x => x.Count)
.ToListAsync();
return Result<List<FaultTypeDistributionDto>>.Success(list);
}
catch (Exception ex)
{
return Result<List<FaultTypeDistributionDto>>.Error("查询故障类型分布失败", ex);
}
}
/// <summary>
/// 维修工时分析
/// </summary>
public async Task<Result<List<RepairHoursAnalysisDto>>> GetRepairHoursAnalysisAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<RepairRecordEntity>()
.Where(x => x.IsDel == 0 && x.RepairHours.HasValue)
.GroupBy(x => x.FaultType)
.Select(x => new RepairHoursAnalysisDto
{
FaultType = x.FaultType,
AvgHours = SqlFunc.AggregateAvg(x.RepairHours) ?? 0,
MaxHours = SqlFunc.AggregateMax(x.RepairHours) ?? 0,
MinHours = SqlFunc.AggregateMin(x.RepairHours) ?? 0,
Count = SqlFunc.AggregateCount(x.Id)
})
.OrderByDescending(x => x.AvgHours)
.ToListAsync();
return Result<List<RepairHoursAnalysisDto>>.Success(list);
}
catch (Exception ex)
{
return Result<List<RepairHoursAnalysisDto>>.Error("查询维修工时分析失败", ex);
}
}
/// <summary>
/// 维修记录明细列表
/// </summary>
public async Task<Result<List<RepairRecordDetailDto>>> GetRepairRecordDetailsAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<RepairRecordEntity>()
.Where(x => x.IsDel == 0)
.OrderByDescending(x => x.CreateTime)
.Select(x => new RepairRecordDetailDto
{
EquipmentCode = x.EquipmentCode,
EquipmentName = x.EquipmentName,
FaultType = x.FaultType,
FaultDescription = x.FaultDescription,
ReportDate = x.ReportDate,
RepairEndDate = x.RepairEndDate,
RepairHours = x.RepairHours,
RepairPerson = x.RepairPerson,
RepairCost = x.RepairCost,
RepairStatus = x.RepairStatus
})
.ToListAsync();
return Result<List<RepairRecordDetailDto>>.Success(list);
}
catch (Exception ex)
{
return Result<List<RepairRecordDetailDto>>.Error("查询维修记录明细失败", ex);
}
}
#endregion
} }
} }
@@ -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);
}
}
}
@@ -0,0 +1,45 @@
using Model;
using Model.Dto.System;
using Model.Entity.System;
using Model.Mapper;
using ORM;
using Service.Interface;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Service.Implement
{
/// <summary>
/// 审计日志 服务实现(查询端;写入端在各业务服务中直接插 AuditLogEntity,或经 AuditHelper
/// </summary>
public class AuditLogService : IAuditLogService
{
public async Task<Result<List<AuditLogDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total,
string? keyword = null, string? operationType = null, string? operationTarget = null,
DateTime? startTime = null, DateTime? endTime = null)
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<AuditLogEntity>()
.Where(x => x.IsDel == 0)
.WhereIF(!string.IsNullOrWhiteSpace(keyword),
x => (x.UserName != null && x.UserName.Contains(keyword!))
|| (x.Description != null && x.Description.Contains(keyword!))
|| (x.RoleName != null && x.RoleName.Contains(keyword!)))
.WhereIF(!string.IsNullOrWhiteSpace(operationType), x => x.OperationType == operationType)
.WhereIF(!string.IsNullOrWhiteSpace(operationTarget), x => x.OperationTarget == operationTarget)
.WhereIF(startTime.HasValue, x => x.OperateTime >= startTime!.Value)
.WhereIF(endTime.HasValue, x => x.OperateTime <= endTime!.Value)
.OrderBy(x => x.OperateTime, OrderByType.Desc)
.ToPageListAsync(pageIndex, pageSize, total);
return Result<List<AuditLogDto>>.Success(list.ToDtoList());
}
catch (Exception ex)
{
return Result<List<AuditLogDto>>.Error("查询审计日志失败", ex);
}
}
}
}
+58
View File
@@ -0,0 +1,58 @@
using Microsoft.Extensions.Logging;
using Model.Entity.System;
using ORM;
using Service.Interface;
using SqlSugar;
namespace Service.Implement
{
/// <summary>
/// 审计日志记录器实现:操作人/角色自动取自 ICurrentUserJWT Claims),失败不抛出(审计不应阻断业务)
/// </summary>
public class AuditRecorder : IAuditRecorder
{
private readonly ICurrentUser _currentUser;
private readonly ILogger<AuditRecorder> _logger;
public AuditRecorder(ICurrentUser currentUser, ILogger<AuditRecorder> logger)
{
_currentUser = currentUser;
_logger = logger;
}
public async Task RecordAsync(string operationType, string operationTarget, long targetId,
long deviceId = 0, string? oldValue = null, string? newValue = null,
string? ip = null, string? description = null)
{
try
{
var now = DateTime.Now;
var roleId = _currentUser.RoleIds.FirstOrDefault();
var roleName = _currentUser.RoleNames.FirstOrDefault();
await SqlSugarContext.DbContext.Insertable(new AuditLogEntity
{
UserId = _currentUser.UserId,
UserName = _currentUser.UserName,
RoleId = roleId,
RoleName = roleName ?? "",
OperationType = operationType,
OperationTarget = operationTarget,
TargetId = targetId,
DeviceId = deviceId,
OldValue = oldValue,
NewValue = newValue,
Ip = ip,
Description = description,
OperateTime = now,
CreateTime = now
}).ExecuteCommandAsync();
}
catch (Exception ex)
{
// 审计失败只记日志,不阻断业务
_logger.LogError(ex, "写审计日志失败({OperationType} {OperationTarget} {TargetId}",
operationType, operationTarget, targetId);
}
}
}
}
+252
View File
@@ -0,0 +1,252 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Model;
using Model.Dto.System;
using Model.Entity.System;
using Model.Mapper;
using ORM;
using Service.Interface;
using SqlSugar;
namespace Service.Implement
{
/// <summary>
/// 认证授权 服务实现:登录校验 → 加载角色/权限 → 签发 JWT(权限编码写入 Claims)
/// </summary>
public class AuthService : IAuthService
{
private readonly IConfiguration _configuration;
private readonly ICurrentUser _currentUser;
private readonly ILogger<AuthService> _logger;
public AuthService(IConfiguration configuration, ICurrentUser currentUser, ILogger<AuthService> logger)
{
_configuration = configuration;
_currentUser = currentUser;
_logger = logger;
}
public async Task<Result<LoginResultDto>> LoginAsync(LoginDto dto, string? ip)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.UserName) || string.IsNullOrWhiteSpace(dto.Password))
return Result<LoginResultDto>.Error("用户名和密码不能为空");
try
{
var user = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.UserName == dto.UserName && x.IsDel == 0).FirstAsync();
if (user == null)
return Result<LoginResultDto>.Error("用户名或密码错误");
if (user.IsEnabled == 0)
return Result<LoginResultDto>.Error("账号已被禁用,请联系管理员");
if (!PasswordHelper.Verify(dto.Password, user.PasswordHash))
return Result<LoginResultDto>.Error("用户名或密码错误");
// 加载角色与权限
var (roleIds, roleNames, permissionCodes, dataScope) = await LoadUserAuthAsync(user.Id);
// 签发 JWT
var token = GenerateJwt(user, roleIds, roleNames, permissionCodes, dataScope);
// 更新最后登录时间
await SqlSugarContext.DbContext.Updateable<UserEntity>()
.SetColumns(x => x.LastLoginTime == DateTime.Now)
.Where(x => x.Id == user.Id).ExecuteCommandAsync();
// 登录审计
await WriteAuditAsync(user.Id, user.UserName, roleIds.FirstOrDefault(), roleNames.FirstOrDefault(),
"Login", "User", user.Id, 0, null, null, ip, $"用户 {user.UserName} 登录系统");
var userDto = user.ToDto();
userDto.RoleIds = roleIds.Select(r => r.ToString()).ToList();
userDto.RoleNames = string.Join(",", roleNames);
return Result<LoginResultDto>.Success(new LoginResultDto
{
Token = token,
RefreshToken = token,
User = userDto,
Permissions = permissionCodes
});
}
catch (Exception ex)
{
_logger.LogError(ex, "登录异常");
return Result<LoginResultDto>.Error("登录失败", ex);
}
}
public async Task<Result> LogoutAsync()
{
if (!_currentUser.IsAuthenticated)
return Result.Success();
try
{
await WriteAuditAsync(_currentUser.UserId, _currentUser.UserName,
_currentUser.RoleIds.FirstOrDefault(), _currentUser.RoleNames.FirstOrDefault(),
"Logout", "User", _currentUser.UserId, 0, null, null, null,
$"用户 {_currentUser.UserName} 退出登录");
return Result.Success();
}
catch (Exception ex)
{
_logger.LogError(ex, "退出登录记录审计失败");
return Result.Success(); // 退出本身不应失败
}
}
public async Task<Result<LoginResultDto>> GetMeAsync()
{
if (!_currentUser.IsAuthenticated)
return Result<LoginResultDto>.Error("未登录或 Token 已失效");
try
{
var user = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == _currentUser.UserId && x.IsDel == 0).FirstAsync();
if (user == null)
return Result<LoginResultDto>.Error("用户不存在或已被删除");
var userDto = user.ToDto();
userDto.RoleIds = _currentUser.RoleIds.Select(r => r.ToString()).ToList();
userDto.RoleNames = string.Join(",", _currentUser.RoleNames);
return Result<LoginResultDto>.Success(new LoginResultDto
{
Token = "",
RefreshToken = "",
User = userDto,
Permissions = _currentUser.Permissions
});
}
catch (Exception ex)
{
return Result<LoginResultDto>.Error("获取当前用户信息失败", ex);
}
}
public async Task<Result> ChangePasswordAsync(ChangePasswordDto dto)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.OldPassword) || string.IsNullOrWhiteSpace(dto.NewPassword))
return Result.Error("原密码和新密码不能为空");
if (dto.NewPassword.Length < 6)
return Result.Error("新密码长度不能少于 6 位");
if (!_currentUser.IsAuthenticated)
return Result.Error("未登录或 Token 已失效");
try
{
var user = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == _currentUser.UserId && x.IsDel == 0).FirstAsync();
if (user == null)
return Result.Error("用户不存在或已被删除");
if (!PasswordHelper.Verify(dto.OldPassword, user.PasswordHash))
return Result.Error("原密码错误");
await SqlSugarContext.DbContext.Updateable<UserEntity>()
.SetColumns(x => x.PasswordHash == PasswordHelper.Hash(dto.NewPassword))
.Where(x => x.Id == user.Id).ExecuteCommandAsync();
await WriteAuditAsync(user.Id, user.UserName, _currentUser.RoleIds.FirstOrDefault(), _currentUser.RoleNames.FirstOrDefault(),
"Update", "User", user.Id, 0, null, null, null, $"用户 {user.UserName} 修改了自己的密码");
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("修改密码失败", ex);
}
}
/// <summary>
/// 加载用户的角色Id/角色名/权限编码/数据范围(取所有角色的并集,数据范围取最宽)
/// </summary>
public static async Task<(List<long> roleIds, List<string> roleNames, List<string> permissions, byte dataScope)>
LoadUserAuthAsync(long userId)
{
var db = SqlSugarContext.DbContext;
var roleIds = await db.Queryable<UserRoleEntity>()
.Where(x => x.UserId == userId)
.Select(x => x.RoleId)
.ToListAsync();
var roleList = new List<RoleEntity>();
if (roleIds.Count > 0)
roleList = await db.Queryable<RoleEntity>()
.Where(x => roleIds.Contains(x.Id) && x.IsDel == 0).ToListAsync();
var permissionCodes = new List<string>();
if (roleIds.Count > 0)
{
var permIds = await db.Queryable<RolePermissionEntity>()
.Where(x => roleIds.Contains(x.RoleId))
.Select(x => x.PermissionId)
.ToListAsync();
if (permIds.Count > 0)
{
var perms = await db.Queryable<PermissionEntity>()
.Where(x => permIds.Contains(x.Id) && x.IsDel == 0).ToListAsync();
permissionCodes = perms.Select(p => p.Code).Distinct().ToList();
}
}
// 数据范围取最宽:任一角色是"全部"即为全部
var dataScope = roleList.Count > 0 ? roleList.Min(r => r.DataScope) : (byte)2;
return (roleIds, roleList.Select(r => r.Name).ToList(), permissionCodes, dataScope);
}
/// <summary>生成 JWT:把用户身份 + 角色 + 权限编码全部写入 Claims(授权过滤器免查库)</summary>
private string GenerateJwt(UserEntity user, List<long> roleIds, List<string> roleNames, List<string> permissions, byte dataScope)
{
var jwt = _configuration.GetSection("Jwt");
var secret = jwt["SecretKey"]!;
var issuer = jwt["Issuer"] ?? "IOT-API";
var audience = jwt["Audience"] ?? "IOT-Web";
var expireMinutes = double.TryParse(jwt["ExpireMinutes"], out var m) ? m : 480;
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.UserName),
new("roleIds", string.Join(",", roleIds)),
new("roleNames", string.Join(",", roleNames)),
new("permissions", string.Join(",", permissions)),
new("dataScope", dataScope.ToString()),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N"))
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(issuer, audience, claims,
expires: DateTime.Now.AddMinutes(expireMinutes), signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>写一条审计日志</summary>
private static async Task WriteAuditAsync(long userId, string? userName, long? roleId, string? roleName,
string operationType, string operationTarget, long targetId, long deviceId,
string? oldValue, string? newValue, string? ip, string? description)
{
var now = DateTime.Now;
await SqlSugarContext.DbContext.Insertable(new AuditLogEntity
{
UserId = userId,
UserName = userName ?? "",
RoleId = roleId ?? 0,
RoleName = roleName ?? "",
OperationType = operationType,
OperationTarget = operationTarget,
TargetId = targetId,
DeviceId = deviceId,
OldValue = oldValue,
NewValue = newValue,
Ip = ip,
Description = description,
OperateTime = now,
CreateTime = now
}).ExecuteCommandAsync();
}
}
}
+196
View File
@@ -0,0 +1,196 @@
using Model.Entity.System;
using ORM;
namespace Service.Implement
{
/// <summary>
/// RBAC 数据种子:预定义 4 个系统角色 + 6 个权限 + 默认管理员账号(admin/admin123
/// 幂等:按 Code 查存在性,存在则跳过;可重复执行
/// </summary>
public static class DataSeeder
{
/// <summary>数据范围:1=全部, 2=本实验室</summary>
public const byte DataScopeAll = 1;
public const byte DataScopeLab = 2;
/// <summary>7 个系统权限编码</summary>
public static readonly (string Code, string Name, string Group)[] Permissions =
{
("device:view", "查看设备", "device"),
("device:edit", "编辑设备", "device"),
("device:control", "控制设备", "device"),
("alert:confirm", "确认告警", "alert"),
("inspection:manage", "巡检管理", "inspection"),
("user:manage", "用户管理", "user"),
("system:manage", "系统配置管理", "system")
};
/// <summary>
/// 4 个预定义角色:超级管理员(全部) / 实验室管理员(本实验室) / 设备操作员(本实验室) / 维修工程师(本实验室)
/// 权限矩阵严格按需求文档
/// </summary>
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", "system:manage" }),
("labadmin", "实验室管理员", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm", "inspection:manage", "system:manage" }),
("operator", "设备操作员", DataScopeLab, new[] { "device:view", "inspection:manage" }),
("maintengineer", "维修工程师", DataScopeLab, new[] { "device:view", "device:edit", "device:control", "alert:confirm" })
};
public static void Seed()
{
try
{
var db = SqlSugarContext.DbContext;
var now = DateTime.Now;
// 1. 权限种子
var permIdByCode = new Dictionary<string, long>();
foreach (var (code, name, group) in Permissions)
{
var existing = db.Queryable<PermissionEntity>().Where(x => x.Code == code).First();
if (existing != null) { permIdByCode[code] = existing.Id; continue; }
var perm = new PermissionEntity
{
Code = code, Name = name, Group = group, IsSystem = 1,
Sort = Array.FindIndex(Permissions, p => p.Code == code),
CreateTime = now
};
var id = db.Insertable(perm).ExecuteReturnSnowflakeId();
permIdByCode[code] = id;
}
// 2. 角色种子 + 角色-权限关联
foreach (var (code, name, scope, perms) in Roles)
{
var role = db.Queryable<RoleEntity>().Where(x => x.Code == code).First();
long roleId;
if (role == null)
{
role = new RoleEntity
{
Code = code, Name = name, DataScope = scope, IsSystem = 1,
Sort = Array.FindIndex(Roles, r => r.Code == code),
Remark = "系统预定义角色",
CreateTime = now
};
roleId = db.Insertable(role).ExecuteReturnSnowflakeId();
}
else
{
roleId = role.Id;
}
// 补齐角色-权限关联(只加不删,避免覆盖管理员自定义调整)
foreach (var permCode in perms)
{
if (!permIdByCode.TryGetValue(permCode, out var permId)) continue;
var linkExists = db.Queryable<RolePermissionEntity>()
.Where(x => x.RoleId == roleId && x.PermissionId == permId).Any();
if (!linkExists)
{
db.Insertable(new RolePermissionEntity { RoleId = roleId, PermissionId = permId, CreateTime = now }).ExecuteCommand();
}
}
}
// 3. 默认管理员账号(admin/admin123,挂在超级管理员角色)
var adminExists = db.Queryable<UserEntity>().Where(x => x.UserName == "admin" && x.IsDel == 0).Any();
if (!adminExists)
{
var superAdmin = db.Queryable<RoleEntity>().Where(x => x.Code == "superadmin").First();
var admin = new UserEntity
{
UserName = "admin",
PasswordHash = PasswordHelper.Hash("admin123"),
RealName = "系统管理员",
IsEnabled = 1,
Remark = "系统默认管理员(首次登录后请修改密码)",
CreateTime = now
};
var adminId = db.Insertable(admin).ExecuteReturnSnowflakeId();
if (superAdmin != null)
{
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)
{
// 种子失败不阻断启动(例如表刚建好并发场景),下次启动会重试
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();
}
}
}
}
+349 -1
View File
@@ -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;
}
} }
} }
+311 -1
View File
@@ -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&lt;IFileStorageProvider&gt; 按配置 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();
}
} }
} }
+332
View File
@@ -0,0 +1,332 @@
using Model;
using Model.Dto.System;
using Model.Entity.System;
using Model.Mapper;
using ORM;
using Service.Interface;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Service.Implement
{
/// <summary>
/// 组织架构管理 服务实现
/// </summary>
public class OrgService : IOrgService
{
private readonly IAuditRecorder _audit;
public OrgService(IAuditRecorder audit)
{
_audit = audit;
}
public async Task<Result<List<OrgTreeDto>>> GetTreeAsync()
{
try
{
var db = SqlSugarContext.DbContext;
var all = await db.Queryable<OrgEntity>()
.Where(x => x.IsDel == 0)
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
.ToListAsync();
// 聚合每个组织下的用户角色 + 数据范围(ScopeSummary/用户数展示用)
var users = await db.Queryable<UserEntity>()
.Where(x => x.IsDel == 0)
.Select(x => new { x.Id, x.OrgId })
.ToListAsync();
var userIds = users.Select(u => u.Id).ToList();
var roleLinks = userIds.Count > 0
? await db.Queryable<UserRoleEntity>().Where(x => userIds.Contains(x.UserId)).ToListAsync()
: new List<UserRoleEntity>();
var roleIds = roleLinks.Select(l => l.RoleId).Distinct().ToList();
var roles = roleIds.Count > 0
? await db.Queryable<RoleEntity>().Where(x => roleIds.Contains(x.Id) && x.IsDel == 0).ToListAsync()
: new List<RoleEntity>();
var roleMap = roles.ToDictionary(r => r.Id, r => r);
// userId -> 角色数据范围标签列表(如「超级管理员·全部」)
var userScopeTags = new Dictionary<long, List<string>>();
foreach (var link in roleLinks)
{
if (!roleMap.TryGetValue(link.RoleId, out var role)) continue;
var tag = $"{role.Name}·{ScopeLabel(role.DataScope)}";
if (!userScopeTags.TryGetValue(link.UserId, out var list))
userScopeTags[link.UserId] = list = new List<string>();
if (!list.Contains(tag)) list.Add(tag);
}
// orgId -> 直接挂靠用户Id列表
var orgUsers = users.GroupBy(u => u.OrgId).ToDictionary(g => g.Key, g => g.Select(u => u.Id).ToList());
var tree = BuildTreeWithStats(all, 0, orgUsers, userScopeTags);
return Result<List<OrgTreeDto>>.Success(tree);
}
catch (Exception ex)
{
return Result<List<OrgTreeDto>>.Error("查询组织树失败", ex);
}
}
/// <summary>
/// 建树并聚合统计:用户数(含下级)+ 数据权限摘要(子树内用户角色的数据范围并集)
/// </summary>
private static List<OrgTreeDto> BuildTreeWithStats(List<OrgEntity> all, long parentId,
Dictionary<long, List<long>> orgUsers, Dictionary<long, List<string>> userScopeTags)
{
var nodes = new List<OrgTreeDto>();
foreach (var entity in all.Where(x => x.ParentId == parentId).OrderBy(x => x.Sort).ThenBy(x => x.Code))
{
var node = entity.ToTreeDto();
node.Children = BuildTreeWithStats(all, entity.Id, orgUsers, userScopeTags);
// 本节点直接用户
orgUsers.TryGetValue(entity.Id, out var directUserIds);
node.UserCount = directUserIds?.Count ?? 0;
// 汇总子树:用户总数 + 数据权限标签并集
var scopes = new List<string>();
if (directUserIds != null)
{
foreach (var uid in directUserIds)
if (userScopeTags.TryGetValue(uid, out var tags))
foreach (var t in tags)
if (!scopes.Contains(t)) scopes.Add(t);
}
var total = node.UserCount;
foreach (var child in node.Children)
{
total += child.TotalUserCount;
foreach (var t in child.ScopeSummary)
if (!scopes.Contains(t)) scopes.Add(t);
}
node.TotalUserCount = total;
node.ScopeSummary = scopes;
nodes.Add(node);
}
return nodes;
}
/// <summary>数据范围标签:1=全部, 2=本组织及下级</summary>
private static string ScopeLabel(byte dataScope) => dataScope == 1 ? "全部" : "本组织及下级";
public async Task<Result<OrgDto>> AddAsync(OrgDto dto)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.Name) || string.IsNullOrWhiteSpace(dto.Code))
return Result<OrgDto>.Error("组织名称和编码不能为空");
try
{
var exists = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.Code == dto.Code && x.IsDel == 0).AnyAsync();
if (exists) return Result<OrgDto>.Error($"组织编码「{dto.Code}」已存在");
// 父节点校验 + 层级固定校验(根节点必须是公司)
var parentId = ParseParent(dto.ParentId);
if (parentId > 0)
{
var parent = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.Id == parentId && x.IsDel == 0).FirstAsync();
if (parent == null) return Result<OrgDto>.Error("父级组织不存在");
var hierarchyError = ValidateHierarchy(parent.OrgType, (byte)dto.OrgType);
if (hierarchyError != null) return Result<OrgDto>.Error(hierarchyError.Msg);
}
else if (dto.OrgType != 1)
{
return Result<OrgDto>.Error("根节点只能是公司");
}
// 班次仅班组有效
var shiftType = dto.OrgType == 4 ? (byte)Math.Max(1, dto.ShiftType) : (byte)0;
var entity = dto.ToEntity();
entity.Id = 0;
entity.ParentId = parentId;
entity.ShiftType = shiftType;
entity.CreateTime = DateTime.Now;
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
entity.Id = id;
await _audit.RecordAsync("Create", "Org", id, 0,
newValue: $"[{OrgTypeName(entity.OrgType)}] {entity.Code}({entity.Name})",
description: $"新增组织 {entity.Name}");
return Result<OrgDto>.Success(entity.ToDto());
}
catch (Exception ex)
{
return Result<OrgDto>.Error("新增组织失败", ex);
}
}
public async Task<Result<OrgDto>> UpdateAsync(OrgDto dto)
{
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
return Result<OrgDto>.Error("组织Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<OrgDto>.Error("组织不存在或已被删除");
var codeExists = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
if (codeExists) return Result<OrgDto>.Error($"组织编码「{dto.Code}」已存在");
var parentId = ParseParent(dto.ParentId);
if (parentId == id)
return Result<OrgDto>.Error("父级不能是自己");
// 层级固定:组织类型创建后不可变更
if (dto.OrgType != entity.OrgType)
return Result<OrgDto>.Error($"组织层级固定,类型不可变更(当前为「{OrgTypeName(entity.OrgType)}」)");
// 换父级时不能挂到自己的子孙下面(否则成环),且层级必须匹配
if (parentId != entity.ParentId && parentId > 0)
{
var all = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.IsDel == 0).ToListAsync();
var subtreeIds = CollectSubtreeIds(all, id);
if (subtreeIds.Contains(parentId))
return Result<OrgDto>.Error("不能把组织挂到自己的下级下面");
var newParent = all.FirstOrDefault(x => x.Id == parentId);
if (newParent == null)
return Result<OrgDto>.Error("目标父级组织不存在");
var hierarchyError = ValidateHierarchy(newParent.OrgType, entity.OrgType);
if (hierarchyError != null) return Result<OrgDto>.Error(hierarchyError.Msg);
}
var oldEntity = entity;
var newEntity = dto.ToEntity();
newEntity.Id = id;
newEntity.ParentId = parentId;
newEntity.OrgType = entity.OrgType; // 类型不可变
// 班次仅班组有效
newEntity.ShiftType = entity.OrgType == 4 ? (byte)Math.Max(1, dto.ShiftType) : (byte)0;
await SqlSugarContext.DbContext.Updateable(newEntity)
.IgnoreColumns(x => new { x.CreateTime, x.IsDel })
.ExecuteCommandAsync();
await _audit.RecordAsync("Update", "Org", id, 0,
oldValue: $"[{OrgTypeName(oldEntity.OrgType)}] {oldEntity.Name}",
newValue: $"[{OrgTypeName(newEntity.OrgType)}] {newEntity.Name}",
description: $"修改组织 {newEntity.Name}");
return Result<OrgDto>.Success(newEntity.ToDto());
}
catch (Exception ex)
{
return Result<OrgDto>.Error("修改组织失败", ex);
}
}
public async Task<Result> DeleteAsync(long id)
{
if (id <= 0) return Result.Error("组织Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result.Error("组织不存在或已被删除");
var hasChildren = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.ParentId == id && x.IsDel == 0).AnyAsync();
if (hasChildren)
return Result.Error($"组织「{entity.Name}」下存在子组织,请先删除子组织");
var hasUsers = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.OrgId == id && x.IsDel == 0).AnyAsync();
if (hasUsers)
return Result.Error($"组织「{entity.Name}」下仍有用户,请先移出用户");
await SqlSugarContext.DbContext.Updateable<OrgEntity>()
.SetColumns(x => x.IsDel == 1)
.Where(x => x.Id == id).ExecuteCommandAsync();
await _audit.RecordAsync("Delete", "Org", id, 0,
oldValue: $"[{OrgTypeName(entity.OrgType)}] {entity.Code}({entity.Name})",
description: $"删除组织 {entity.Name}");
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("删除组织失败", ex);
}
}
public async Task<Result<List<OrgOptionDto>>> GetOptionsAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.IsDel == 0)
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
.ToListAsync();
return Result<List<OrgOptionDto>>.Success(list.ToOptionDtoList());
}
catch (Exception ex)
{
return Result<List<OrgOptionDto>>.Error("查询组织选项失败", ex);
}
}
public async Task<List<long>> GetSubtreeIdsAsync(long orgId)
{
if (orgId <= 0) return new List<long>();
var all = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.IsDel == 0).ToListAsync();
return CollectSubtreeIds(all, orgId);
}
#region
/// <summary>收集指定组织及其全部子孙 Id(含自身)</summary>
public static List<long> CollectSubtreeIds(List<OrgEntity> all, long orgId)
{
var result = new List<long>();
var stack = new Stack<long>();
stack.Push(orgId);
while (stack.Count > 0)
{
var current = stack.Pop();
result.Add(current);
foreach (var child in all.Where(x => x.ParentId == current))
stack.Push(child.Id);
}
return result;
}
private static long ParseParent(string? parentId)
=> long.TryParse(parentId, out var v) ? v : 0;
/// <summary>层级固定校验:公司(1)→实验室(2)→部门(3)→班组(4),子级类型必须=父级类型+1</summary>
private static Result? ValidateHierarchy(byte parentType, byte childType)
{
var validChild = parentType switch
{
1 => (byte)2, // 公司下只能建实验室
2 => (byte)3, // 实验室下只能建部门
3 => (byte)4, // 部门下只能建班组
_ => (byte)0 // 班组不能再有下级
};
if (childType != validChild)
return Result.Error($"组织层级固定:公司→实验室→部门→班组,该节点下只能建「{OrgTypeName(validChild)}」");
return null;
}
private static string OrgTypeName(byte orgType) => orgType switch
{
1 => "公司",
2 => "实验室",
3 => "部门",
4 => "班组",
_ => "组织"
};
#endregion
}
}
@@ -0,0 +1,49 @@
using System.Security.Cryptography;
using System.Text;
namespace Service.Implement
{
/// <summary>
/// 密码哈希工具(PBKDF2-SHA256,格式 salt$hash,盐随机 16 字节)
/// </summary>
public static class PasswordHelper
{
private const int Iterations = 10_000;
private const int SaltSize = 16;
private const int HashSize = 32;
/// <summary>生成密码哈希(salt$hash</summary>
public static string Hash(string password)
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName.SHA256, HashSize);
return $"{Convert.ToHexString(salt)}${Convert.ToHexString(hash)}";
}
/// <summary>校验密码是否匹配</summary>
public static bool Verify(string password, string? stored)
{
if (string.IsNullOrWhiteSpace(stored)) return false;
var parts = stored.Split('$');
if (parts.Length != 2) return false;
var salt = Convert.FromHexString(parts[0]);
var expected = Convert.FromHexString(parts[1]);
var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName.SHA256, expected.Length);
return CryptographicOperations.FixedTimeEquals(actual, expected);
}
/// <summary>生成随机盐字符串(供外部使用)</summary>
public static string NewSalt()
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
return Convert.ToHexString(salt).ToLowerInvariant();
}
/// <summary>SHA256(供 RefreshToken 等场景)</summary>
public static string Sha256(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}
}
+277 -1
View File
@@ -1,4 +1,14 @@
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
{ {
@@ -7,6 +17,272 @@ namespace Service.Implement
/// </summary> /// </summary>
public class RoleService : IRoleService public class RoleService : IRoleService
{ {
// TODO: 实现 角色权限管理 相关方法 public async Task<Result<List<RoleDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null)
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.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 FillPermissionsAsync(dtos);
return Result<List<RoleDto>>.Success(dtos);
}
catch (Exception ex)
{
return Result<List<RoleDto>>.Error("查询角色列表失败", ex);
}
}
public async Task<Result<RoleDto>> GetByIdAsync(long id)
{
try
{
var entity = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<RoleDto>.Error("角色不存在或已被删除");
var dto = entity.ToDto();
await FillPermissionsAsync(new List<RoleDto> { dto });
return Result<RoleDto>.Success(dto);
}
catch (Exception ex)
{
return Result<RoleDto>.Error("查询角色详情失败", ex);
}
}
public async Task<Result<RoleDto>> AddAsync(RoleDto dto)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.Code) || string.IsNullOrWhiteSpace(dto.Name))
return Result<RoleDto>.Error("角色编码和名称不能为空");
try
{
var exists = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Code == dto.Code && x.IsDel == 0).AnyAsync();
if (exists) return Result<RoleDto>.Error($"角色编码「{dto.Code}」已存在");
var entity = dto.ToEntity();
entity.Id = 0;
entity.IsSystem = 0; // 新建角色一律非系统内置
entity.CreateTime = DateTime.Now;
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
entity.Id = id;
if (dto.PermissionIds != null && dto.PermissionIds.Count > 0)
{
var permIds = dto.PermissionIds.Select(long.Parse).Where(p => p > 0).ToList();
await ReplaceRolePermissionsAsync(id, permIds);
}
var result = entity.ToDto();
await FillPermissionsAsync(new List<RoleDto> { result });
return Result<RoleDto>.Success(result);
}
catch (Exception ex)
{
return Result<RoleDto>.Error("新增角色失败", ex);
}
}
public async Task<Result<RoleDto>> UpdateAsync(RoleDto dto)
{
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
return Result<RoleDto>.Error("角色Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<RoleDto>.Error("角色不存在或已被删除");
// 系统内置角色不允许改编码
if (entity.IsSystem == 1 && dto.Code != entity.Code)
return Result<RoleDto>.Error($"系统内置角色「{entity.Name}」不允许修改编码");
var codeExists = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Code == dto.Code && x.Id != id && x.IsDel == 0).AnyAsync();
if (codeExists) return Result<RoleDto>.Error($"角色编码「{dto.Code}」已存在");
var newEntity = dto.ToEntity();
newEntity.Id = id;
newEntity.IsSystem = entity.IsSystem; // 内置标记不可被前端覆盖
await SqlSugarContext.DbContext.Updateable(newEntity)
.IgnoreColumns(x => new { x.CreateTime, x.IsDel, x.IsSystem })
.ExecuteCommandAsync();
// 权限变化时同步更新关联(传入 null 表示不改权限)
if (dto.PermissionIds != null)
{
var permIds = dto.PermissionIds.Select(long.Parse).Where(p => p > 0).ToList();
var guard = await EnsureAdminRemainsAfterRoleChangeAsync(id, permIds);
if (guard != null) return Result<RoleDto>.Error(guard.Msg);
await ReplaceRolePermissionsAsync(id, permIds);
}
var result = newEntity.ToDto();
await FillPermissionsAsync(new List<RoleDto> { result });
return Result<RoleDto>.Success(result);
}
catch (Exception ex)
{
return Result<RoleDto>.Error("修改角色失败", ex);
}
}
public async Task<Result> DeleteAsync(long id)
{
if (id <= 0) return Result.Error("角色Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result.Error("角色不存在或已被删除");
if (entity.IsSystem == 1)
return Result.Error($"系统内置角色「{entity.Name}」不允许删除");
var usedByUser = await SqlSugarContext.DbContext.Queryable<UserRoleEntity>()
.Where(x => x.RoleId == id).AnyAsync();
if (usedByUser)
return Result.Error("该角色下存在用户,请先移除角色下的用户再删除");
await SqlSugarContext.DbContext.Updateable<RoleEntity>()
.SetColumns(x => x.IsDel == 1)
.Where(x => x.Id == id).ExecuteCommandAsync();
await SqlSugarContext.DbContext.Deleteable<RolePermissionEntity>()
.Where(x => x.RoleId == id).ExecuteCommandAsync();
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("删除角色失败", ex);
}
}
public async Task<Result> AssignPermissionsAsync(long roleId, List<long> permissionIds)
{
if (roleId <= 0) return Result.Error("角色Id无效");
try
{
var exists = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.Id == roleId && x.IsDel == 0).AnyAsync();
if (!exists) return Result.Error("角色不存在或已被删除");
// 防锁死:移除角色的用户管理权限后,系统必须仍有其它可管理用户的管理员
var newIds = permissionIds ?? new List<long>();
var guard = await EnsureAdminRemainsAfterRoleChangeAsync(roleId, newIds);
if (guard != null) return guard;
await ReplaceRolePermissionsAsync(roleId, newIds);
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("分配权限失败", ex);
}
}
public async Task<Result<List<RoleOptionDto>>> GetOptionsAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<RoleEntity>()
.Where(x => x.IsDel == 0)
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
.ToListAsync();
return Result<List<RoleOptionDto>>.Success(list.ToOptionDtoList());
}
catch (Exception ex)
{
return Result<List<RoleOptionDto>>.Error("查询角色选项失败", ex);
}
}
public async Task<Result<List<PermissionDto>>> GetPermissionsAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<PermissionEntity>()
.Where(x => x.IsDel == 0)
.OrderBy(x => x.Sort).OrderBy(x => x.Code)
.ToListAsync();
return Result<List<PermissionDto>>.Success(list.ToDtoList());
}
catch (Exception ex)
{
return Result<List<PermissionDto>>.Error("查询权限列表失败", ex);
}
}
/// <summary>全量覆盖角色-权限关联</summary>
private static async Task ReplaceRolePermissionsAsync(long roleId, List<long> permissionIds)
{
var db = SqlSugarContext.DbContext;
await db.Deleteable<RolePermissionEntity>().Where(x => x.RoleId == roleId).ExecuteCommandAsync();
if (permissionIds.Count > 0)
{
var now = DateTime.Now;
var links = permissionIds.Distinct().Select(p => new RolePermissionEntity { RoleId = roleId, PermissionId = p, CreateTime = now }).ToList();
await db.Insertable(links).ExecuteCommandAsync();
}
}
/// <summary>
/// 防锁死校验:当 roleId 将失去 user:manage 权限时,系统必须仍存在
/// 「已启用且通过其它角色拥有 user:manage 权限」的用户,否则返回 Error
/// </summary>
private async Task<Result?> EnsureAdminRemainsAfterRoleChangeAsync(long roleId, List<long> newPermissionIds)
{
var db = SqlSugarContext.DbContext;
var managePermId = await db.Queryable<PermissionEntity>()
.Where(x => x.Code == "user:manage" && x.IsDel == 0)
.Select(x => x.Id).FirstAsync();
if (managePermId == 0) return null;
bool currentlyHas = await db.Queryable<RolePermissionEntity>()
.AnyAsync(x => x.RoleId == roleId && x.PermissionId == managePermId);
bool willHave = newPermissionIds.Contains(managePermId);
if (currentlyHas && !willHave)
{
var otherRoleIds = await db.Queryable<RolePermissionEntity>()
.Where(x => x.PermissionId == managePermId && x.RoleId != roleId)
.Select(x => x.RoleId).ToListAsync();
var otherUserIds = otherRoleIds.Count > 0
? await db.Queryable<UserRoleEntity>().Where(x => otherRoleIds.Contains(x.RoleId)).Select(x => x.UserId).ToListAsync()
: new List<long>();
var exists = otherUserIds.Count > 0 && await db.Queryable<UserEntity>()
.AnyAsync(x => otherUserIds.Contains(x.Id) && x.IsDel == 0 && x.IsEnabled == 1);
if (!exists)
return Result.Error("操作被拒绝:移除该角色的用户管理权限后,系统将没有任何可管理用户的管理员");
}
return null;
}
/// <summary>给角色 DTO 填充权限Id列表与权限编码列表</summary>
private static async Task FillPermissionsAsync(List<RoleDto> dtos)
{
if (dtos == null || dtos.Count == 0) return;
var db = SqlSugarContext.DbContext;
var roleIds = dtos.Select(d => long.Parse(d.Id)).ToList();
var links = await db.Queryable<RolePermissionEntity>()
.Where(x => roleIds.Contains(x.RoleId)).ToListAsync();
var permIds = links.Select(l => l.PermissionId).Distinct().ToList();
var perms = permIds.Count > 0
? await db.Queryable<PermissionEntity>().Where(x => permIds.Contains(x.Id) && x.IsDel == 0).ToListAsync()
: new List<PermissionEntity>();
var permMap = perms.ToDictionary(p => p.Id, p => p.Code);
foreach (var d in dtos)
{
var rid = long.Parse(d.Id);
var myPermIds = links.Where(l => l.RoleId == rid).Select(l => l.PermissionId).ToList();
d.PermissionIds = myPermIds.Select(p => p.ToString()).ToList();
d.PermissionCodes = myPermIds.Where(p => permMap.ContainsKey(p)).Select(p => permMap[p]).ToList();
}
}
} }
} }
+246 -1
View File
@@ -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}");
}
} }
} }
+398 -1
View File
@@ -1,12 +1,409 @@
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>
/// 用户管理 服务实现 /// 用户管理 服务实现
/// 数据权限:当前用户角色 DataScope=1 看全部;=2 只能看自己组织及下级组织下的用户
/// </summary> /// </summary>
public class UserService : IUserService public class UserService : IUserService
{ {
// TODO: 实现 用户管理 相关方法 private readonly IOrgService _orgService;
private readonly ICurrentUser _currentUser;
public UserService(IOrgService orgService, ICurrentUser currentUser)
{
_orgService = orgService;
_currentUser = currentUser;
}
public async Task<Result<List<UserDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, long? roleId = null, long? orgId = null)
{
try
{
// 按角色筛选时先查出该角色下的用户Id
List<long>? userIdsInRole = null;
if (roleId.HasValue && roleId.Value > 0)
{
userIdsInRole = await SqlSugarContext.DbContext.Queryable<UserRoleEntity>()
.Where(x => x.RoleId == roleId!.Value)
.Select(x => x.UserId)
.ToListAsync();
if (userIdsInRole.Count == 0)
return Result<List<UserDto>>.Success(new List<UserDto>());
}
// 数据范围过滤:DataScope=2 时只允许看本组织及下级的用户
List<long>? allowedOrgIds = null;
if (_currentUser.IsAuthenticated && _currentUser.DataScope != 1)
{
var me = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == _currentUser.UserId).FirstAsync();
if (me != null && me.OrgId > 0)
allowedOrgIds = await _orgService.GetSubtreeIdsAsync(me.OrgId);
else
return Result<List<UserDto>>.Success(new List<UserDto>()); // 无组织归属则看不到任何用户
}
// 前端按组织节点筛选时叠加(同时受数据范围约束)
List<long>? orgFilterIds = null;
if (orgId.HasValue && orgId.Value > 0)
{
orgFilterIds = await _orgService.GetSubtreeIdsAsync(orgId.Value);
if (allowedOrgIds != null)
orgFilterIds = orgFilterIds.Where(allowedOrgIds.Contains).ToList();
}
var list = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.IsDel == 0)
.WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.UserName.Contains(keyword!) || (x.RealName != null && x.RealName.Contains(keyword!)))
.WhereIF(userIdsInRole != null, x => userIdsInRole!.Contains(x.Id))
.WhereIF(allowedOrgIds != null, x => allowedOrgIds!.Contains(x.OrgId))
.WhereIF(orgFilterIds != null, x => orgFilterIds!.Contains(x.OrgId))
.OrderBy(x => x.CreateTime, OrderByType.Desc)
.ToPageListAsync(pageIndex, pageSize, total);
var dtos = list.ToDtoList();
await FillRolesAsync(dtos);
await FillOrgNamesAsync(dtos);
return Result<List<UserDto>>.Success(dtos);
}
catch (Exception ex)
{
return Result<List<UserDto>>.Error("查询用户列表失败", ex);
}
}
public async Task<Result<UserDto>> GetByIdAsync(long id)
{
try
{
var entity = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<UserDto>.Error("用户不存在或已被删除");
var dto = entity.ToDto();
await FillRolesAsync(new List<UserDto> { dto });
await FillOrgNamesAsync(new List<UserDto> { dto });
return Result<UserDto>.Success(dto);
}
catch (Exception ex)
{
return Result<UserDto>.Error("查询用户详情失败", ex);
}
}
public async Task<Result<UserDto>> AddAsync(UserDto dto)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.UserName))
return Result<UserDto>.Error("用户名不能为空");
try
{
var exists = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.UserName == dto.UserName && x.IsDel == 0).AnyAsync();
if (exists) return Result<UserDto>.Error($"用户名「{dto.UserName}」已存在");
var entity = dto.ToEntity();
entity.Id = 0;
entity.CreateTime = DateTime.Now;
// 新增用户初始密码必填(前端通过 InitialPassword 明文传入,落库前 PBKDF2 哈希)
var initialPassword = dto.InitialPassword;
if (string.IsNullOrWhiteSpace(initialPassword) || initialPassword.Length < 6)
return Result<UserDto>.Error("初始密码不能为空且长度不少于 6 位");
entity.PasswordHash = PasswordHelper.Hash(initialPassword);
// 角色先解析校验再落库,避免"用户已插入但角色解析失败"留下无角色用户
var newRoleIds = new List<long>();
if (dto.RoleIds != null && dto.RoleIds.Count > 0)
{
foreach (var r in dto.RoleIds)
{
if (!long.TryParse(r, out var rid) || rid <= 0)
return Result<UserDto>.Error($"角色Id无效:{r}");
newRoleIds.Add(rid);
}
}
var id = await SqlSugarContext.DbContext.Insertable(entity).ExecuteReturnSnowflakeIdAsync();
entity.Id = id;
if (newRoleIds.Count > 0)
await ReplaceUserRolesAsync(id, newRoleIds);
var result = entity.ToDto();
await FillRolesAsync(new List<UserDto> { result });
return Result<UserDto>.Success(result);
}
catch (Exception ex)
{
return Result<UserDto>.Error("新增用户失败", ex);
}
}
public async Task<Result<UserDto>> UpdateAsync(UserDto dto)
{
if (dto == null || !long.TryParse(dto.Id, out var id) || id <= 0)
return Result<UserDto>.Error("用户Id无效");
try
{
var entity = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == id && x.IsDel == 0).FirstAsync();
if (entity == null) return Result<UserDto>.Error("用户不存在或已被删除");
var nameExists = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.UserName == dto.UserName && x.Id != id && x.IsDel == 0).AnyAsync();
if (nameExists) return Result<UserDto>.Error($"用户名「{dto.UserName}」已存在");
// 防锁死:把自己禁用不允许;禁用或改角色后必须仍存在可管理用户的管理员
if (dto.IsEnabled == 0 && id == _currentUser.UserId)
return Result<UserDto>.Error("不能禁用当前登录的账号");
if (dto.IsEnabled == 0 || dto.RoleIds != null)
{
var guard = await EnsureAnotherActiveAdminAsync(id);
if (guard != null) return Result<UserDto>.Error(guard.Msg);
}
var newEntity = dto.ToEntity();
newEntity.Id = id;
// 密码、登录时间不允许在此接口覆盖
await SqlSugarContext.DbContext.Updateable(newEntity)
.IgnoreColumns(x => new { x.CreateTime, x.IsDel, x.PasswordHash, x.LastLoginTime })
.ExecuteCommandAsync();
// 角色变化时同步更新关联(传入 null 表示不改角色)
if (dto.RoleIds != null)
{
var roleIds = dto.RoleIds.Select(long.Parse).Where(r => r > 0).ToList();
await ReplaceUserRolesAsync(id, roleIds);
}
var result = newEntity.ToDto();
await FillRolesAsync(new List<UserDto> { result });
return Result<UserDto>.Success(result);
}
catch (Exception ex)
{
return Result<UserDto>.Error("修改用户失败", ex);
}
}
public async Task<Result> DeleteAsync(long id)
{
if (id <= 0) return Result.Error("用户Id无效");
if (id == _currentUser.UserId)
return Result.Error("不能删除当前登录的账号");
try
{
var exists = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == id && x.IsDel == 0).AnyAsync();
if (!exists) return Result.Error("用户不存在或已被删除");
// 防锁死:删除后必须仍存在可管理用户的管理员
var guard = await EnsureAnotherActiveAdminAsync(id);
if (guard != null) return guard;
await SqlSugarContext.DbContext.Updateable<UserEntity>()
.SetColumns(x => x.IsDel == 1)
.Where(x => x.Id == id).ExecuteCommandAsync();
await SqlSugarContext.DbContext.Deleteable<UserRoleEntity>()
.Where(x => x.UserId == id).ExecuteCommandAsync();
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("删除用户失败", ex);
}
}
public async Task<Result> AssignRolesAsync(long userId, List<long> roleIds)
{
if (userId <= 0) return Result.Error("用户Id无效");
try
{
var exists = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == userId && x.IsDel == 0).AnyAsync();
if (!exists) return Result.Error("用户不存在或已被删除");
// 防锁死:改角色后必须仍存在可管理用户的管理员
var guard = await EnsureAnotherActiveAdminAsync(userId);
if (guard != null) return guard;
await ReplaceUserRolesAsync(userId, roleIds ?? new List<long>());
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("分配角色失败", ex);
}
}
public async Task<Result> ResetPasswordAsync(long userId, string newPassword)
{
if (userId <= 0) return Result.Error("用户Id无效");
if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < 6)
return Result.Error("新密码不能为空且长度不少于 6 位");
try
{
var exists = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == userId && x.IsDel == 0).AnyAsync();
if (!exists) return Result.Error("用户不存在或已被删除");
await SqlSugarContext.DbContext.Updateable<UserEntity>()
.SetColumns(x => x.PasswordHash == PasswordHelper.Hash(newPassword))
.Where(x => x.Id == userId).ExecuteCommandAsync();
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("重置密码失败", ex);
}
}
public async Task<Result> SetEnabledAsync(long userId, bool enabled)
{
if (userId <= 0) return Result.Error("用户Id无效");
if (!enabled && userId == _currentUser.UserId)
return Result.Error("不能禁用当前登录的账号");
try
{
// 防锁死:禁用后必须仍存在可管理用户的管理员
if (!enabled)
{
var guard = await EnsureAnotherActiveAdminAsync(userId);
if (guard != null) return guard;
}
await SqlSugarContext.DbContext.Updateable<UserEntity>()
.SetColumns(x => x.IsEnabled == (byte)(enabled ? 1 : 0))
.Where(x => x.Id == userId).ExecuteCommandAsync();
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("设置用户状态失败", ex);
}
}
public async Task<Result<List<UserOptionDto>>> GetOptionsAsync()
{
try
{
var list = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.IsDel == 0 && x.IsEnabled == 1)
.OrderBy(x => x.UserName)
.ToListAsync();
return Result<List<UserOptionDto>>.Success(list.ToOptionDtoList());
}
catch (Exception ex)
{
return Result<List<UserOptionDto>>.Error("查询用户选项失败", ex);
}
}
/// <summary>全量覆盖用户-角色关联</summary>
private static async Task ReplaceUserRolesAsync(long userId, List<long> roleIds)
{
var db = SqlSugarContext.DbContext;
await db.Deleteable<UserRoleEntity>().Where(x => x.UserId == userId).ExecuteCommandAsync();
if (roleIds.Count > 0)
{
var now = DateTime.Now;
var links = roleIds.Distinct().Select(r => new UserRoleEntity { UserId = userId, RoleId = r, CreateTime = now }).ToList();
await db.Insertable(links).ExecuteCommandAsync();
}
}
/// <summary>
/// 防锁死校验:排除 excludeUserId 后,系统必须仍存在「已启用且拥有 user:manage 权限」的用户,
/// 否则返回 Error(保证管理员不会把所有管理员删光/禁用光导致无人可登录管理)
/// </summary>
private async Task<Result?> EnsureAnotherActiveAdminAsync(long excludeUserId)
{
var db = SqlSugarContext.DbContext;
var permId = await db.Queryable<PermissionEntity>()
.Where(x => x.Code == "user:manage" && x.IsDel == 0)
.Select(x => x.Id).FirstAsync();
if (permId == 0) return null; // 权限码不存在时不拦,避免权限体系变更后管理功能彻底锁死
var roleIds = await db.Queryable<RolePermissionEntity>()
.Where(x => x.PermissionId == permId)
.Select(x => x.RoleId).ToListAsync();
if (roleIds.Count == 0) return null;
var userIds = await db.Queryable<UserRoleEntity>()
.Where(x => roleIds.Contains(x.RoleId))
.Select(x => x.UserId).ToListAsync();
if (userIds.Count == 0)
return Result.Error("操作被拒绝:系统将没有任何可管理用户的管理员");
var exists = await db.Queryable<UserEntity>()
.AnyAsync(x => userIds.Contains(x.Id) && x.IsDel == 0 && x.IsEnabled == 1 && x.Id != excludeUserId);
return exists ? null : Result.Error("操作被拒绝:系统至少需要保留一名已启用且拥有用户管理权限的账号");
}
/// <summary>给用户 DTO 填充所属组织名称(组织链路径,如 公司/实验室/部门)</summary>
private async Task FillOrgNamesAsync(List<UserDto> dtos)
{
if (dtos == null || dtos.Count == 0) return;
var orgIds = dtos.Where(d => long.TryParse(d.OrgId, out var oid) && oid > 0)
.Select(d => long.Parse(d.OrgId)).Distinct().ToList();
if (orgIds.Count == 0) return;
var allOrgs = await SqlSugarContext.DbContext.Queryable<OrgEntity>()
.Where(x => x.IsDel == 0).ToListAsync();
var orgMap = allOrgs.ToDictionary(o => o.Id, o => o);
foreach (var d in dtos)
{
if (!long.TryParse(d.OrgId, out var oid) || !orgMap.TryGetValue(oid, out var org))
continue;
// 沿父级向上拼组织路径
var path = new List<string>();
var cursor = org;
var guard = 0;
while (cursor != null && guard++ < 10)
{
path.Insert(0, cursor.Name);
cursor = cursor.ParentId > 0 && orgMap.TryGetValue(cursor.ParentId, out var parent) ? parent : null;
}
d.OrgName = string.Join(" / ", path);
}
}
/// <summary>给用户 DTO 填充角色Id列表与角色名称</summary>
private static async Task FillRolesAsync(List<UserDto> dtos)
{
if (dtos == null || dtos.Count == 0) return;
var db = SqlSugarContext.DbContext;
var userIds = dtos.Select(d => long.Parse(d.Id)).ToList();
var links = await db.Queryable<UserRoleEntity>()
.Where(x => userIds.Contains(x.UserId)).ToListAsync();
var roleIds = links.Select(l => l.RoleId).Distinct().ToList();
var roles = roleIds.Count > 0
? await db.Queryable<RoleEntity>().Where(x => roleIds.Contains(x.Id) && x.IsDel == 0).ToListAsync()
: new List<RoleEntity>();
var roleMap = roles.ToDictionary(r => r.Id, r => r.Name);
foreach (var d in dtos)
{
var uid = long.Parse(d.Id);
var myRoleIds = links.Where(l => l.UserId == uid).Select(l => l.RoleId).ToList();
d.RoleIds = myRoleIds.Select(r => r.ToString()).ToList();
d.RoleNames = string.Join(",", myRoleIds.Where(r => roleMap.ContainsKey(r)).Select(r => roleMap[r]));
}
}
} }
} }
+27 -2
View File
@@ -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/NameqrOnly=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);
} }
} }
+1 -1
View File
@@ -14,7 +14,7 @@ namespace Service.Interface.Config
/// <summary> /// <summary>
/// 分页查询设备列表(支持关键字搜索编号/名称/类型,可按所属产品筛选) /// 分页查询设备列表(支持关键字搜索编号/名称/类型,可按所属产品筛选)
/// </summary> /// </summary>
Task<Result<List<IotDeviceDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword, long? productId = null); Task<Result<List<IotDeviceDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword, long? productId = null, long? gatewayId = null);
/// <summary> /// <summary>
/// 根据 Id 获取设备详情 /// 根据 Id 获取设备详情
+17 -2
View File
@@ -1,10 +1,25 @@
namespace Service.Interface using Model;
using Model.Dto.Config;
using Model.Entity.Config;
using ORM;
using SqlSugar;
using System.Net.Sockets;
using System.Net;
using System.IO.Ports;
namespace Service.Interface.Config
{ {
/// <summary> /// <summary>
/// 网关管理 服务接口 /// 网关管理 服务接口
/// </summary> /// </summary>
public interface IGatewayService public interface IGatewayService
{ {
// TODO: 定义 网关管理 相关方法 Task<Result<List<GatewayDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, int? protocolType = null);
Task<Result<GatewayDto>> GetByIdAsync(long id);
Task<Result<GatewayDto>> AddAsync(GatewayDto dto);
Task<Result<GatewayDto>> UpdateAsync(GatewayDto dto);
Task<Result> DeleteAsync(long id);
Task<Result<GatewayTestResultDto>> TestConnectionAsync(long id);
Task<Result<List<GatewayOptionDto>>> GetOptionsAsync();
} }
} }
+28
View File
@@ -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&lt;IFileStorageProvider&gt; 按配置 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,3 +1,8 @@
using Model;
using Model.Dto.Inspection;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Service.Interface namespace Service.Interface
{ {
/// <summary> /// <summary>
@@ -5,6 +10,49 @@ namespace Service.Interface
/// </summary> /// </summary>
public interface IStatisticsReportService public interface IStatisticsReportService
{ {
// TODO: 定义 统计报表 相关方法 /// <summary>
/// 设备统计概览(总数/在用/闲置/报废等)
/// </summary>
Task<Result<DeviceStatisticsDto>> GetDeviceStatisticsAsync();
/// <summary>
/// 按分类统计设备数量
/// </summary>
Task<Result<List<CategoryStatisticsDto>>> GetCategoryStatisticsAsync();
/// <summary>
/// 按部门统计设备数量
/// </summary>
Task<Result<List<DepartmentStatisticsDto>>> GetDepartmentStatisticsAsync();
/// <summary>
/// 设备维护统计(校准/保养及时率、超期数量等)
/// </summary>
Task<Result<MaintenanceStatisticsDto>> GetMaintenanceStatisticsAsync();
/// <summary>
/// 维护超期设备明细
/// </summary>
Task<Result<List<MaintenanceOverdueDetailDto>>> GetMaintenanceOverdueDetailsAsync();
/// <summary>
/// 维修统计概览
/// </summary>
Task<Result<RepairStatisticsDto>> GetRepairStatisticsAsync();
/// <summary>
/// 故障类型分布
/// </summary>
Task<Result<List<FaultTypeDistributionDto>>> GetFaultTypeDistributionAsync();
/// <summary>
/// 维修工时分析
/// </summary>
Task<Result<List<RepairHoursAnalysisDto>>> GetRepairHoursAnalysisAsync();
/// <summary>
/// 维修记录明细列表
/// </summary>
Task<Result<List<RepairRecordDetailDto>>> GetRepairRecordDetailsAsync();
} }
} }
@@ -0,0 +1,19 @@
using Model;
using Model.Dto.System;
using SqlSugar;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Service.Interface
{
/// <summary>
/// 审计日志 服务接口
/// </summary>
public interface IAuditLogService
{
/// <summary>分页查询审计日志(可按操作人/操作类型/操作对象/时间段筛选)</summary>
Task<Result<List<AuditLogDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total,
string? keyword = null, string? operationType = null, string? operationTarget = null,
DateTime? startTime = null, DateTime? endTime = null);
}
}
@@ -0,0 +1,23 @@
namespace Service.Interface
{
/// <summary>
/// 审计日志记录器:业务服务注入后一行代码记录关键操作(操作人/角色自动取自当前登录上下文)
/// </summary>
public interface IAuditRecorder
{
/// <summary>
/// 记录一条审计日志
/// </summary>
/// <param name="operationType">操作类型(Create/Update/Delete/Login/Command等)</param>
/// <param name="operationTarget">操作对象(IotDevice/Gateway/User/Role等)</param>
/// <param name="targetId">操作对象Id</param>
/// <param name="deviceId">关联设备Id0=无关)</param>
/// <param name="oldValue">操作前值(JSON 或摘要)</param>
/// <param name="newValue">操作后值(JSON 或摘要)</param>
/// <param name="ip">操作IPnull 自动留空)</param>
/// <param name="description">操作描述</param>
Task RecordAsync(string operationType, string operationTarget, long targetId,
long deviceId = 0, string? oldValue = null, string? newValue = null,
string? ip = null, string? description = null);
}
}
+23
View File
@@ -0,0 +1,23 @@
using Model;
using Model.Dto.System;
namespace Service.Interface
{
/// <summary>
/// 认证授权 服务接口(登录 / 当前用户 / 修改密码)
/// </summary>
public interface IAuthService
{
/// <summary>用户名密码登录,签发 JWT 并返回用户信息+权限列表</summary>
Task<Result<LoginResultDto>> LoginAsync(LoginDto dto, string? ip);
/// <summary>退出登录(记录审计日志,JWT 无状态因此不做服务端吊销)</summary>
Task<Result> LogoutAsync();
/// <summary>获取当前登录用户信息 + 权限列表</summary>
Task<Result<LoginResultDto>> GetMeAsync();
/// <summary>当前用户修改自己的密码</summary>
Task<Result> ChangePasswordAsync(ChangePasswordDto dto);
}
}
+19
View File
@@ -0,0 +1,19 @@
namespace Service.Interface
{
/// <summary>
/// 当前登录用户上下文(从 JWT Claims 解析,IOT_API 实现注入)
/// </summary>
public interface ICurrentUser
{
bool IsAuthenticated { get; }
long UserId { get; }
string UserName { get; }
List<long> RoleIds { get; }
List<string> RoleNames { get; }
/// <summary>当前用户拥有的权限编码列表(登录时写入 Token</summary>
List<string> Permissions { get; }
/// <summary>数据范围(1=全部, 2=本实验室)</summary>
byte DataScope { get; }
bool HasPermission(string code);
}
}
+45 -1
View File
@@ -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);
} }
} }
+31
View File
@@ -0,0 +1,31 @@
using Model;
using Model.Dto.System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Service.Interface
{
/// <summary>
/// 组织架构管理 服务接口(公司/实验室/部门/班组树)
/// </summary>
public interface IOrgService
{
/// <summary>完整组织树(嵌套子级)</summary>
Task<Result<List<OrgTreeDto>>> GetTreeAsync();
/// <summary>新增组织节点</summary>
Task<Result<OrgDto>> AddAsync(OrgDto dto);
/// <summary>修改组织节点(系统节点层级类型变更需校验无子级)</summary>
Task<Result<OrgDto>> UpdateAsync(OrgDto dto);
/// <summary>删除组织节点(有子级或有用户挂靠时不可删)</summary>
Task<Result> DeleteAsync(long id);
/// <summary>组织下拉选项(平铺)</summary>
Task<Result<List<OrgOptionDto>>> GetOptionsAsync();
/// <summary>取指定组织及其全部子孙组织 Id(数据权限过滤用)</summary>
Task<List<long>> GetSubtreeIdsAsync(long orgId);
}
}
+28 -1
View File
@@ -1,3 +1,9 @@
using Model;
using Model.Dto.System;
using SqlSugar;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Service.Interface namespace Service.Interface
{ {
/// <summary> /// <summary>
@@ -5,6 +11,27 @@ namespace Service.Interface
/// </summary> /// </summary>
public interface IRoleService public interface IRoleService
{ {
// TODO: 定义 角色权限管理 相关方法 /// <summary>分页查询角色列表(关键字匹配编码/名称)</summary>
Task<Result<List<RoleDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null);
Task<Result<RoleDto>> GetByIdAsync(long id);
/// <summary>新增角色(校验编码唯一)</summary>
Task<Result<RoleDto>> AddAsync(RoleDto dto);
/// <summary>修改角色(系统内置角色不允许改编码)</summary>
Task<Result<RoleDto>> UpdateAsync(RoleDto dto);
/// <summary>删除角色(系统内置角色不可删;有关联用户时不可删)</summary>
Task<Result> DeleteAsync(long id);
/// <summary>分配权限(全量覆盖该角色的权限关联)</summary>
Task<Result> AssignPermissionsAsync(long roleId, List<long> permissionIds);
/// <summary>角色下拉选项</summary>
Task<Result<List<RoleOptionDto>>> GetOptionsAsync();
/// <summary>查询全部权限列表</summary>
Task<Result<List<PermissionDto>>> GetPermissionsAsync();
} }
} }
@@ -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);
} }
} }
+31 -1
View File
@@ -1,3 +1,9 @@
using Model;
using Model.Dto.System;
using SqlSugar;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Service.Interface namespace Service.Interface
{ {
/// <summary> /// <summary>
@@ -5,6 +11,30 @@ namespace Service.Interface
/// </summary> /// </summary>
public interface IUserService public interface IUserService
{ {
// TODO: 定义 用户管理 相关方法 /// <summary>分页查询用户列表(关键字匹配用户名/姓名,可按角色、组织节点筛选;受数据范围约束)</summary>
Task<Result<List<UserDto>>> GetPagedAsync(int pageIndex, int pageSize, RefAsync<int> total, string? keyword = null, long? roleId = null, long? orgId = null);
Task<Result<UserDto>> GetByIdAsync(long id);
/// <summary>新增用户(校验用户名唯一,密码必填,可同时分配角色)</summary>
Task<Result<UserDto>> AddAsync(UserDto dto);
/// <summary>修改用户(不改密码,角色走 AssignRoles</summary>
Task<Result<UserDto>> UpdateAsync(UserDto dto);
/// <summary>删除用户(软删,同时清理角色关联)</summary>
Task<Result> DeleteAsync(long id);
/// <summary>分配角色(全量覆盖该用户的角色关联)</summary>
Task<Result> AssignRolesAsync(long userId, List<long> roleIds);
/// <summary>管理员重置用户密码</summary>
Task<Result> ResetPasswordAsync(long userId, string newPassword);
/// <summary>启用/禁用用户</summary>
Task<Result> SetEnabledAsync(long userId, bool enabled);
/// <summary>用户下拉选项</summary>
Task<Result<List<UserOptionDto>>> GetOptionsAsync();
} }
} }
+4 -1
View File
@@ -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,9 +8,12 @@
<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" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>