Compare commits

..
14 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
dengbolingandClaude Code 19fb191c3d Merge remote-tracking branch 'origin/master'(告警中心/通知模块 与 产品/物模型模块 合并,EntityMapper.cs 保留双方映射区域)
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-03 17:33:12 +08:00
dengboling bcd6484f45 设备类添加产品字段、添加物模型类,新增产品CRUD方法 2026-09-03 17:16:28 +08:00
wuyi 3927b0c201 搭建消息告警模块以及跨网推送服务,支持企微,飞书,钉钉通知 2026-09-03 14:02:35 +08:00
“hsc” 38cd47410c 设备维修统计报表 2026-09-02 13:36:52 +08:00
dengboling cfa8e78d33 添加设备类、设备日志类、设备服务类、设备控制器 2026-08-28 17:28:16 +08:00
“hsc” 6c20630a40 冲突处理 2026-08-28 16:08:03 +08:00
“hsc” 2ecd921d26 Merge branch 'master' of https://git.kaiyili-lab.com/huangsucan/IOT_API 2026-08-28 15:20:58 +08:00
“hsc” d9b079142b 通知告警规则引擎 2026-08-28 15:20:52 +08:00
wuyi 24d67baf65 设备管理模块方法转换为DTO模式,修复long类型接收雪花ID丢失精度的问题 2026-08-28 14:05:16 +08:00
wuyi 2306fbd36b Merge branch 'master' of https://git.kaiyili-lab.com/huangsucan/IOT_API 2026-08-28 13:18:13 +08:00
wuyi a027bf5d9e 忽略wwwroot 2026-08-28 13:14:49 +08:00
157 changed files with 14683 additions and 174 deletions
+1
View File
@@ -366,3 +366,4 @@ MigrationBackup/
# Fody - auto-generated XML schema
FodyWeavers.xsd
/IOT_API/appsettings.json
/IOT_API/wwwroot
+21
View File
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<!-- 引用 Common:复用 ImWebhookSigner 加签算法与 Model 中的 Outbox DTO,保证与主站推送行为一致 -->
<ProjectReference Include="..\Common\Common.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+259
View File
@@ -0,0 +1,259 @@
using Common.Notify;
using Model.Dto.Inspection;
using System.Text;
using System.Text.Json;
namespace AlertBridge
{
/// <summary>
/// 跨网告警转发程序(部署在可同时访问"工控内网 IOT_API"与"外网 IM Webhook"的跳板机上)
/// 工作方式:轮询拉取内网发件箱(GET outbox/pull)→ 按报文推送飞书/钉钉/企微 → 回写结果(POST outbox/{id}/result
/// 报文自包含 WebhookUrl/Secret/内容,本程序无需访问数据库,配置仅需 appsettings.json 三项
/// </summary>
public class Program
{
private static string _apiBaseUrl = "http://localhost:5287";
private static int _pollSeconds = 5;
private static int _batchSize = 20;
public static async Task Main(string[] args)
{
LoadConfig();
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
Log($"AlertBridge 启动,API 地址: {_apiBaseUrl},轮询间隔: {_pollSeconds}s,批量: {_batchSize}");
Log("按 Ctrl+C 停止");
while (!cts.IsCancellationRequested)
{
try
{
await PollOnceAsync(http, cts.Token);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Log($"轮询异常: {ex.Message}");
}
try { await Task.Delay(TimeSpan.FromSeconds(_pollSeconds), cts.Token); }
catch (TaskCanceledException) { break; }
}
Log("AlertBridge 已停止");
}
/// <summary>
/// 单次轮询:拉取 → 逐条推送 → 回写
/// </summary>
private static async Task PollOnceAsync(HttpClient http, CancellationToken ct)
{
string pullUrl = $"{_apiBaseUrl}/api/inspection/alert-notify/outbox/pull?batch={_batchSize}";
string pullJson = await http.GetStringAsync(pullUrl, ct);
var pullResult = JsonSerializer.Deserialize<ResultDto<List<AlertOutboxItemDto>>>(pullJson, JsonOpts);
if (pullResult == null || pullResult.Code != 0 || pullResult.Data == null || pullResult.Data.Count == 0)
{
return;
}
Log($"拉取到 {pullResult.Data.Count} 条待推送消息");
foreach (var item in pullResult.Data)
{
bool success = false;
string? error = null;
try
{
var payload = JsonSerializer.Deserialize<AlertOutboxPayload>(item.Payload ?? "{}", JsonOpts);
if (payload == null || string.IsNullOrWhiteSpace(payload.WebhookUrl))
{
error = "Payload 为空或缺少 WebhookUrl";
}
else
{
(success, error) = await SendAsync(http, payload, ct);
}
}
catch (Exception ex)
{
error = NotifyError.Describe(ex);
}
// 回写结果
try
{
var resultBody = JsonSerializer.Serialize(new { Success = success, ResultMsg = error }, JsonOpts);
using var content = new StringContent(resultBody, Encoding.UTF8, "application/json");
await http.PostAsync($"{_apiBaseUrl}/api/inspection/alert-notify/outbox/{item.Id}/result", content, ct);
Log($"消息 {item.Id} 推送{(success ? "" : $": {error}")},结果已回写");
}
catch (Exception ex)
{
Log($"消息 {item.Id} 结果回写失败: {ex.Message}");
}
}
}
/// <summary>
/// 按渠道类型推送(与主站 Notifier 报文格式保持一致)
/// </summary>
private static async Task<(bool success, string? error)> SendAsync(HttpClient http, AlertOutboxPayload p, CancellationToken ct)
{
string url = p.WebhookUrl!;
object body;
switch (p.ChannelType)
{
case 1: // 飞书:post 富文本 + at 标签(user_id=open_id) 真正 @ + 可选加签(timestamp 秒 + sign
{
var contentLines = new List<List<Dictionary<string, object>>>();
foreach (var raw in (p.Content ?? string.Empty).Split('\n'))
{
var line = raw.TrimEnd('\r');
if (line.Length == 0) continue;
contentLines.Add(new List<Dictionary<string, object>>
{
new Dictionary<string, object> { ["tag"] = "text", ["text"] = line }
});
}
if (p.AtMobiles is { Count: > 0 })
{
var atLine = new List<Dictionary<string, object>>
{
new Dictionary<string, object> { ["tag"] = "text", ["text"] = "关注人: " }
};
foreach (var openId in p.AtMobiles)
{
if (string.IsNullOrWhiteSpace(openId)) continue;
atLine.Add(new Dictionary<string, object> { ["tag"] = "at", ["user_id"] = openId.Trim() });
}
contentLines.Add(atLine);
}
var feishuBody = new Dictionary<string, object>
{
["msg_type"] = "post",
["content"] = new Dictionary<string, object>
{
["post"] = new Dictionary<string, object>
{
["zh_cn"] = new Dictionary<string, object>
{
["title"] = p.Title ?? "设备告警",
["content"] = contentLines
}
}
}
};
if (!string.IsNullOrWhiteSpace(p.Secret))
{
long ts = DateTimeOffset.Now.ToUnixTimeSeconds();
feishuBody["timestamp"] = ts.ToString();
feishuBody["sign"] = ImWebhookSigner.FeishuSign(ts, p.Secret);
}
body = feishuBody;
break;
}
case 2: // 钉钉:markdown + at.atMobiles + URL 加签(timestamp 毫秒 + sign
{
if (!string.IsNullOrWhiteSpace(p.Secret))
{
long ts = DateTimeOffset.Now.ToUnixTimeMilliseconds();
url += $"{(url.Contains('?') ? "&" : "?")}timestamp={ts}&sign={ImWebhookSigner.DingTalkSign(ts, p.Secret)}";
}
var md = new StringBuilder();
md.AppendLine($"### {p.Title}");
md.AppendLine(p.Content);
foreach (var m in p.AtMobiles ?? new List<string>()) md.Append($"@{m} ");
body = new Dictionary<string, object>
{
["msgtype"] = "markdown",
["markdown"] = new Dictionary<string, object> { ["title"] = p.Title ?? "设备告警", ["text"] = md.ToString() },
["at"] = new Dictionary<string, object> { ["atMobiles"] = p.AtMobiles ?? new List<string>(), ["isAtAll"] = false }
};
break;
}
case 3: // 企微:text + mentioned_mobile_list(无加签)
{
var text = new StringBuilder();
text.AppendLine(p.Title);
text.AppendLine(p.Content);
body = new Dictionary<string, object>
{
["msgtype"] = "text",
["text"] = new Dictionary<string, object>
{
["content"] = text.ToString(),
["mentioned_mobile_list"] = p.AtMobiles ?? new List<string>()
}
};
break;
}
default:
return (false, $"未知渠道类型: {p.ChannelType}");
}
string json = JsonSerializer.Serialize(body, JsonOpts);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
using var response = await http.PostAsync(url, content, ct);
string respText = await response.Content.ReadAsStringAsync(ct);
if (!response.IsSuccessStatusCode)
{
return (false, $"HTTP {(int)response.StatusCode}: {Truncate(respText)}");
}
// 飞书返回 code/StatusCode,钉钉/企微返回 errcode0 均为成功
using var doc = JsonDocument.Parse(respText);
var root = doc.RootElement;
bool ok = (root.TryGetProperty("errcode", out var ec) && ec.GetInt32() == 0)
|| (root.TryGetProperty("code", out var c) && c.GetInt32() == 0)
|| (root.TryGetProperty("StatusCode", out var sc) && sc.GetInt32() == 0);
return ok ? (true, null) : (false, Truncate(respText));
}
/// <summary>
/// 读取 appsettings.json(免依赖 Microsoft.Extensions.Configuration,直接 JSON 解析)
/// </summary>
private static void LoadConfig()
{
try
{
string path = Path.Combine(AppContext.BaseDirectory, "appsettings.json");
if (!File.Exists(path)) return;
using var doc = JsonDocument.Parse(File.ReadAllText(path));
var root = doc.RootElement;
if (root.TryGetProperty("ApiBaseUrl", out var u)) _apiBaseUrl = u.GetString() ?? _apiBaseUrl;
if (root.TryGetProperty("PollSeconds", out var p)) _pollSeconds = p.GetInt32();
if (root.TryGetProperty("BatchSize", out var b)) _batchSize = b.GetInt32();
}
catch (Exception ex)
{
Log($"读取配置失败,使用默认值: {ex.Message}");
}
}
private static string Truncate(string s, int max = 200) =>
string.IsNullOrEmpty(s) || s.Length <= max ? s : s.Substring(0, max) + "...";
private static void Log(string msg) => Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {msg}");
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true };
/// <summary>
/// 主站统一返回结构(仅取所需字段)
/// </summary>
private class ResultDto<T>
{
public int Code { get; set; }
public string? Msg { get; set; }
public T? Data { get; set; }
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"ApiBaseUrl": "http://localhost:5287",
"PollSeconds": 5,
"BatchSize": 20
}
+26
View File
@@ -0,0 +1,26 @@
namespace Common.Notify
{
/// <summary>
/// 飞书自建应用配置(用于"手机号 → open_id"转换)
/// 飞书群机器人 webhook 只能发文本、无法用手机号 @ 人,必须借助自建应用凭证调通讯录接口换取 open_id。
/// 由 Program.cs 启动时从 appsettings.json 的 Feishu 节读入;未配置时相关功能自动降级。
/// </summary>
public static class FeishuConfig
{
/// <summary>飞书自建应用 App IDcli_ 开头)</summary>
public static string AppId { get; private set; } = string.Empty;
/// <summary>飞书自建应用 App Secret</summary>
public static string AppSecret { get; private set; } = string.Empty;
/// <summary>是否已配置(未配置时手机号无法转 open_id,降级为仅支持直接填 open_id / all</summary>
public static bool IsConfigured => !string.IsNullOrWhiteSpace(AppId) && !string.IsNullOrWhiteSpace(AppSecret);
/// <summary>初始化飞书应用配置</summary>
public static void Init(string appId, string appSecret)
{
AppId = appId ?? string.Empty;
AppSecret = appSecret ?? string.Empty;
}
}
}
+40
View File
@@ -0,0 +1,40 @@
using System.Security.Cryptography;
using System.Text;
namespace Common.Notify
{
/// <summary>
/// IM 机器人 Webhook 加签工具(钉钉/飞书)
/// 供 IOT_API 直连推送与 AlertBridge 跨网转发程序共用,保证两种模式签名算法一致
/// </summary>
public static class ImWebhookSigner
{
/// <summary>
/// 钉钉机器人加签:sign = UrlEncode(Base64(HmacSHA256(timestamp + "\n" + secret, key=secret)))
/// </summary>
/// <param name="timestampMs">毫秒时间戳</param>
/// <param name="secret">加签密钥(SEC 开头)</param>
/// <returns>URL 编码后的签名</returns>
public static string DingTalkSign(long timestampMs, string secret)
{
string stringToSign = $"{timestampMs}\n{secret}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
byte[] signData = hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));
return Uri.EscapeDataString(Convert.ToBase64String(signData));
}
/// <summary>
/// 飞书机器人加签:sign = Base64(HmacSHA256(空串, key=timestamp + "\n" + secret))
/// 注意与钉钉相反:飞书以"时间戳\n密钥"为 key、对空内容签名
/// </summary>
/// <param name="timestampSec">秒级时间戳</param>
/// <param name="secret">加签密钥</param>
/// <returns>Base64 签名</returns>
public static string FeishuSign(long timestampSec, string secret)
{
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes($"{timestampSec}\n{secret}"));
byte[] signData = hmac.ComputeHash(Array.Empty<byte>());
return Convert.ToBase64String(signData);
}
}
}
+83
View File
@@ -0,0 +1,83 @@
using System.Security.Authentication;
using System.Text;
namespace Common.Notify
{
/// <summary>
/// 通知推送异常诊断工具
/// HttpClient 网络异常(尤其 SSL/TLS 握手失败)的真实根因藏在 InnerException 链里,
/// 只记录外层 ex.Message 会得到"see inner exception"这类无用信息。
/// 本工具展开整条异常链并针对常见网络故障给出可读排查提示,供三个 Notifier 复用。
/// </summary>
public static class NotifyError
{
/// <summary>
/// 将异常展开为"外层 -> 内层 -> ..."的可读描述,并附加常见网络故障排查提示
/// </summary>
public static string Describe(Exception? ex)
{
if (ex == null) return "未知错误";
var sb = new StringBuilder();
var current = ex;
int depth = 0;
// 最多展开 5 层,防止极端嵌套导致信息过长
while (current != null && depth < 5)
{
if (depth > 0) sb.Append(" -> ");
sb.Append(current.Message);
current = current.InnerException;
depth++;
}
if (IsTimeout(ex))
{
sb.Append("|排查:请求超时,请确认目标地址可达、Webhook 域名解析正常、无防火墙拦截出站 443");
}
else if (IsSslFailure(ex))
{
sb.Append("|排查:SSL/TLS 握手失败,常见原因为 " +
"①服务器无法访问外网(工控内网请改用\"跨网中转(Outbox)\"推送模式,由跳板机 AlertBridge 实际发送) " +
"②企业代理/防火墙做 SSL 拦截且其根证书未被本机信任 " +
"③系统时间不准导致证书校验失败 " +
"④Webhook 地址主机名或端口有误(非标准 HTTPS 端口)");
}
return sb.ToString();
}
/// <summary>
/// 是否为超时(TaskCanceledException 且非用户主动取消,或 TimeoutException
/// </summary>
private static bool IsTimeout(Exception ex)
{
for (var c = ex; c != null; c = c.InnerException)
{
if (c is TimeoutException) return true;
// HttpClient 超时表现为 TaskCanceledException,内部通常带 TimeoutException
if (c is TaskCanceledException && c.InnerException is TimeoutException) return true;
}
return false;
}
/// <summary>
/// 是否为 SSL/TLS 握手或证书校验失败
/// </summary>
private static bool IsSslFailure(Exception ex)
{
for (var c = ex; c != null; c = c.InnerException)
{
if (c is AuthenticationException) return true;
var msg = c.Message;
if (!string.IsNullOrEmpty(msg) &&
(msg.Contains("SSL", StringComparison.OrdinalIgnoreCase) ||
msg.Contains("TLS", StringComparison.OrdinalIgnoreCase) ||
msg.Contains("certificate", StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
return false;
}
}
}
+74
View File
@@ -17,40 +17,114 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceCommand", "DeviceComm
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IOT_API", "IOT_API\IOT_API.csproj", "{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AlertBridge", "AlertBridge\AlertBridge.csproj", "{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Debug|x64.ActiveCfg = Debug|Any CPU
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Debug|x64.Build.0 = Debug|Any CPU
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Debug|x86.ActiveCfg = Debug|Any CPU
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Debug|x86.Build.0 = Debug|Any CPU
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Release|Any CPU.Build.0 = Release|Any CPU
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Release|x64.ActiveCfg = Release|Any CPU
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Release|x64.Build.0 = Release|Any CPU
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Release|x86.ActiveCfg = Release|Any CPU
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Release|x86.Build.0 = Release|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Debug|x64.ActiveCfg = Debug|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Debug|x64.Build.0 = Debug|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Debug|x86.ActiveCfg = Debug|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Debug|x86.Build.0 = Debug|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Release|Any CPU.Build.0 = Release|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Release|x64.ActiveCfg = Release|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Release|x64.Build.0 = Release|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Release|x86.ActiveCfg = Release|Any CPU
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Release|x86.Build.0 = Release|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Debug|x64.ActiveCfg = Debug|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Debug|x64.Build.0 = Debug|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Debug|x86.ActiveCfg = Debug|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Debug|x86.Build.0 = Debug|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Release|Any CPU.Build.0 = Release|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Release|x64.ActiveCfg = Release|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Release|x64.Build.0 = Release|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Release|x86.ActiveCfg = Release|Any CPU
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Release|x86.Build.0 = Release|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Debug|x64.ActiveCfg = Debug|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Debug|x64.Build.0 = Debug|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Debug|x86.ActiveCfg = Debug|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Debug|x86.Build.0 = Debug|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Release|Any CPU.Build.0 = Release|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Release|x64.ActiveCfg = Release|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Release|x64.Build.0 = Release|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Release|x86.ActiveCfg = Release|Any CPU
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Release|x86.Build.0 = Release|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Debug|x64.ActiveCfg = Debug|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Debug|x64.Build.0 = Debug|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Debug|x86.ActiveCfg = Debug|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Debug|x86.Build.0 = Debug|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|Any CPU.Build.0 = Release|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|x64.ActiveCfg = Release|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|x64.Build.0 = Release|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|x86.ActiveCfg = Release|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|x86.Build.0 = Release|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Debug|x64.ActiveCfg = Debug|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Debug|x64.Build.0 = Debug|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Debug|x86.ActiveCfg = Debug|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Debug|x86.Build.0 = Debug|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Release|Any CPU.Build.0 = Release|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Release|x64.ActiveCfg = Release|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Release|x64.Build.0 = Release|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Release|x86.ActiveCfg = Release|Any CPU
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Release|x86.Build.0 = Release|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Debug|x64.ActiveCfg = Debug|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Debug|x64.Build.0 = Debug|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Debug|x86.ActiveCfg = Debug|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Debug|x86.Build.0 = Debug|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Release|Any CPU.Build.0 = Release|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Release|x64.ActiveCfg = Release|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Release|x64.Build.0 = Release|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Release|x86.ActiveCfg = Release|Any CPU
{1E2A7C73-2CAE-4084-939B-0F8EE73AA955}.Release|x86.Build.0 = Release|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Debug|x64.ActiveCfg = Debug|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Debug|x64.Build.0 = Debug|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Debug|x86.ActiveCfg = Debug|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Debug|x86.Build.0 = Debug|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Release|Any CPU.Build.0 = Release|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Release|x64.ActiveCfg = Release|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Release|x64.Build.0 = Release|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Release|x86.ActiveCfg = Release|Any CPU
{C764BDBA-3D96-4BA1-9AA5-D9D284E7C6AA}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1,9 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Model;
using Model.Dto.Asset;
using Model.Entity.Asset;
using Model.Mapper;
using Service.Interface;
using SqlSugar;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace IOT_API.Controllers.Asset
@@ -11,16 +14,21 @@ namespace IOT_API.Controllers.Asset
/// <summary>
/// 资产管理 - 设备台账控制器
/// </summary>
[Route("api/Equipment")] //资产管理模块下的设备管理接口
[ApiController]
[Route("api/asset/equipment")]
[Route("api/asset/equipment")] //资产管理模块下的设备管理接口
public class EquipmentController : ControllerBase
{
private readonly IEquipmentService _equipmentService;
private readonly IEquipmentAttachmentService _attachmentService;
private readonly IWebHostEnvironment _webHostEnvironment;
public EquipmentController(IEquipmentService equipmentService)
public EquipmentController(IEquipmentService equipmentService,
IEquipmentAttachmentService attachmentService,
IWebHostEnvironment webHostEnvironment)
{
_equipmentService = equipmentService;
_attachmentService = attachmentService;
_webHostEnvironment = webHostEnvironment;
}
/// <summary>
@@ -32,22 +40,27 @@ namespace IOT_API.Controllers.Asset
/// <param name="categoryId">设备分类Id0表示不过滤)</param>
/// <param name="status">设备状态(不传表示不过滤)</param>
[HttpGet("list")]
public async Task<Result<List<EquipmentEntity>>> GetList(int pageIndex = 1, int pageSize = 10,
public async Task<Result<List<EquipmentDto>>> GetList(int pageIndex = 1, int pageSize = 10,
string? keyword = null, long categoryId = 0, EquipmentStatusEnum? status = null)
{
RefAsync<int> total = 0;
var result = await _equipmentService.GetPagedAsync(pageIndex, pageSize, total, keyword, categoryId, status);
Response.Headers["X-Total-Count"] = total.Value.ToString();
return result;
return result.IsSuccess
? Result<List<EquipmentDto>>.Success(result.Data.ToDtoList())
: Result<List<EquipmentDto>>.Error(result.Msg);
}
/// <summary>
/// 查询全部设备(不分页,供下拉选择等场景使用)
/// </summary>
[HttpGet("all")]
public async Task<Result<List<EquipmentEntity>>> GetAll()
public async Task<Result<List<EquipmentDto>>> GetAll()
{
return await _equipmentService.GetAllAsync();
var result = await _equipmentService.GetAllAsync();
return result.IsSuccess
? Result<List<EquipmentDto>>.Success(result.Data.ToDtoList())
: Result<List<EquipmentDto>>.Error(result.Msg);
}
/// <summary>
@@ -55,29 +68,33 @@ namespace IOT_API.Controllers.Asset
/// </summary>
/// <param name="id">设备主键 Id</param>
[HttpGet("{id}")]
public async Task<Result<EquipmentEntity>> GetById(long id)
public async Task<Result<EquipmentDto>> GetById(long id)
{
return await _equipmentService.GetByIdAsync(id);
var res = await _equipmentService.GetByIdAsync(id);
return res.IsSuccess
? Result<EquipmentDto>.Success(res.Data.ToDto())
: Result<EquipmentDto>.Error(res.Msg);
}
/// <summary>
/// 新增设备
/// </summary>
/// <param name="entity">设备实体</param>
/// <param name="dto">设备 DTOIsDel/CreateTime 等服务端字段不接受入参)</param>
[HttpPost]
public async Task<Result<bool>> Add([FromBody] EquipmentEntity entity)
public async Task<Result<bool>> Add([FromBody] EquipmentDto dto)
{
return await _equipmentService.InsertAsync(entity);
return await _equipmentService.InsertAsync(dto.ToEntity());
}
/// <summary>
/// 修改设备
/// </summary>
/// <param name="id">设备主键 Id</param>
/// <param name="entity">设备实体</param>
/// <param name="id">设备主键 Id(以路由参数为准,忽略请求体中的 Id)</param>
/// <param name="dto">设备 DTOIsDel/CreateTime 等服务端字段不接受入参)</param>
[HttpPut("{id}")]
public async Task<Result<bool>> Update(long id, [FromBody] EquipmentEntity entity)
public async Task<Result<bool>> Update(long id, [FromBody] EquipmentDto dto)
{
var entity = dto.ToEntity();
entity.Id = id;
return await _equipmentService.UpdateAsync(entity);
}
@@ -96,11 +113,11 @@ namespace IOT_API.Controllers.Asset
/// 变更设备状态(同步记录状态变更历史)
/// </summary>
/// <param name="id">设备主键 Id</param>
/// <param name="request">状态变更请求</param>
/// <param name="dto">状态变更请求(目标状态/操作人/备注)</param>
[HttpPost("{id}/status")]
public async Task<Result<bool>> ChangeStatus(long id, [FromBody] ChangeStatusRequest request)
public async Task<Result<bool>> ChangeStatus(long id, [FromBody] EquipmentStatusChangeDto dto)
{
return await _equipmentService.ChangeStatusAsync(id, request.Status, request.Operator, request.Remark);
return await _equipmentService.ChangeStatusAsync(id, dto.Status, dto.Operator, dto.Remark);
}
/// <summary>
@@ -108,30 +125,65 @@ namespace IOT_API.Controllers.Asset
/// </summary>
/// <param name="id">设备主键 Id</param>
[HttpGet("{id}/status-records")]
public async Task<Result<List<EquipmentStatusRecordEntity>>> GetStatusRecords(long id)
public async Task<Result<List<EquipmentStatusRecordDto>>> GetStatusRecords(long id)
{
return await _equipmentService.GetStatusRecordsAsync(id);
}
var res = await _equipmentService.GetStatusRecordsAsync(id);
return res.IsSuccess
? Result<List<EquipmentStatusRecordDto>>.Success(res.Data.ToDtoList())
: Result<List<EquipmentStatusRecordDto>>.Error(res.Msg);
}
/// <summary>
/// 状态变更请求体
/// 查询设备附件列表(资产附件管理)
/// </summary>
public class ChangeStatusRequest
/// <param name="id">设备主键 Id</param>
[HttpGet("{id}/attachments")]
public async Task<Result<List<EquipmentAttachmentDto>>> GetAttachments(long id)
{
/// <summary>
/// 目标状态
/// </summary>
public EquipmentStatusEnum Status { get; set; }
var res = await _attachmentService.GetByEquipmentAsync(id);
return res.IsSuccess
? Result<List<EquipmentAttachmentDto>>.Success(res.Data.ToDtoList())
: Result<List<EquipmentAttachmentDto>>.Error(res.Msg);
}
/// <summary>
/// 操作人
/// 上传设备附件(multipart/form-data,文件字段名 file
/// </summary>
public string? Operator { get; set; }
/// <param name="id">设备主键 Id</param>
/// <param name="file">上传的文件</param>
/// <param name="fileType">文件类型(1使用说明书/2合格证明/3校准证书/4保养卡/5合同扫描件/6验收照片/99其他)</param>
/// <param name="uploader">上传人</param>
/// <param name="remark">备注</param>
[HttpPost("{id}/attachments")]
[RequestSizeLimit(50 * 1024 * 1024)]
public async Task<Result<EquipmentAttachmentDto>> UploadAttachment(long id, IFormFile file,
[FromForm] AttachmentTypeEnum fileType = AttachmentTypeEnum.Other,
[FromForm] string? uploader = null, [FromForm] string? remark = null)
{
if (file == null || file.Length == 0)
{
return Result<EquipmentAttachmentDto>.Error("请选择要上传的文件");
}
// wwwroot 不存在时退回运行目录下的 wwwroot(上传时会自动创建)
string webRootPath = _webHostEnvironment.WebRootPath
?? Path.Combine(_webHostEnvironment.ContentRootPath, "wwwroot");
using var stream = file.OpenReadStream();
var res = await _attachmentService.UploadAsync(id, stream, file.FileName, file.Length, fileType, uploader, remark, webRootPath);
return res.IsSuccess
? Result<EquipmentAttachmentDto>.Success(res.Data.ToDto())
: Result<EquipmentAttachmentDto>.Error(res.Msg);
}
/// <summary>
/// 变更原因/备注
/// 删除设备附件
/// </summary>
public string? Remark { get; set; }
/// <param name="attachmentId">附件主键 Id</param>
[HttpDelete("attachments/{attachmentId}")]
public async Task<Result<bool>> DeleteAttachment(long attachmentId)
{
return await _attachmentService.DeleteAsync(attachmentId);
}
}
}
+74 -2
View File
@@ -1,14 +1,86 @@
using Microsoft.AspNetCore.Mvc;
using Model;
using Model.Dto.Asset;
using Service.Interface;
using SqlSugar;
using WebAPI.Filters;
namespace WebAPI.Controllers
{
/// <summary>
/// 二维码
/// 设备二维码生成与打印(资产侧;生成/绑定 RFID 需 device:edit,列表/图片/打印页 device:view 可访问)
/// 扫码直达:二维码内容 = http://host/asset/ledger/equipment?id={equipmentId}
/// </summary>
[ApiController]
[Route("api/asset/qrcode")]
public class QrCodeController : ControllerBase
{
// TODO: 实现 二维码 相关接口
private readonly IQrCodeService _qrService;
public QrCodeController(IQrCodeService qrService)
{
_qrService = qrService;
}
// baseUrl 由当前请求构造(scheme://host[:port]);如需固定域名,改为读 sys_param site_base_url
private string BaseUrl => $"{Request.Scheme}://{Request.Host}";
/// <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 Model;
using Model.Dto.Config;
using Service.Interface.Config;
using SqlSugar;
using WebAPI.Filters;
namespace WebAPI.Controllers
namespace WebAPI.Controllers.Config
{
/// <summary>
/// 网关管理
/// </summary>
[ApiController]
[Route("api/config/gateway")]
[RequirePermission("device:view")]
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();
}
}
}
@@ -1,4 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using Model;
using Model.Dto.Config;
using Service.Interface.Config;
using SqlSugar;
using System.Threading.Tasks;
using WebAPI.Filters;
namespace WebAPI.Controllers
{
@@ -7,8 +13,108 @@ namespace WebAPI.Controllers
/// </summary>
[ApiController]
[Route("api/config/device")]
[RequirePermission("device:view")]
public class IotDeviceController : ControllerBase
{
// TODO: 实现 IOT设备管理 相关接口
private readonly IDeviceService _deviceService;
private readonly IDeviceCommandService _deviceCommandService;
public IotDeviceController(IDeviceService deviceService, IDeviceCommandService deviceCommandService)
{
_deviceService = deviceService;
_deviceCommandService = deviceCommandService;
}
/// <summary>
/// 设备列表(分页查询,支持关键字搜索,可按所属产品筛选)
/// </summary>
/// <param name="pageIndex">页码(从1开始,默认1</param>
/// <param name="pageSize">每页数量(默认10</param>
/// <param name="keyword">关键字(模糊匹配设备编号/名称/类型)</param>
/// <param name="productId">所属产品Id0 表示不过滤)</param>
/// <param name="gatewayId">所属网关Id0 表示不过滤)</param>
[HttpGet("list")]
public async Task<IActionResult> GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, long productId = 0, long gatewayId = 0)
{
RefAsync<int> total = 0;
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();
return result.IsSuccess
? Ok(Result<List<IotDeviceDto>>.Success(result.Data))
: Ok(Result<List<IotDeviceDto>>.Error(result.Msg));
}
/// <summary>
/// 设备详情
/// </summary>
/// <param name="id">设备主键 Id</param>
[HttpGet("{id}")]
public async Task<IActionResult> GetById(long id)
{
return Ok(await _deviceService.GetByIdAsync(id));
}
/// <summary>
/// 新增设备(需 device:edit 权限)
/// </summary>
[HttpPost]
[RequirePermission("device:edit")]
public async Task<IActionResult> Add([FromBody] IotDeviceDto dto)
{
return Ok(await _deviceService.AddAsync(dto));
}
/// <summary>
/// 修改设备(需 device:edit 权限)
/// </summary>
[HttpPut]
[RequirePermission("device:edit")]
public async Task<IActionResult> Update([FromBody] IotDeviceDto dto)
{
return Ok(await _deviceService.UpdateAsync(dto));
}
/// <summary>
/// 删除设备(软删除;需 device:edit 权限)
/// </summary>
/// <param name="id">设备主键 Id</param>
[HttpDelete("{id}")]
[RequirePermission("device:edit")]
public async Task<IActionResult> Delete(long id)
{
return Ok(await _deviceService.DeleteAsync(id));
}
/// <summary>
/// 设备日志(设备独享日志,分页查询)
/// </summary>
/// <param name="id">设备主键 Id</param>
/// <param name="pageIndex">页码(默认1</param>
/// <param name="pageSize">每页数量(默认20</param>
[HttpGet("{id}/logs")]
public async Task<IActionResult> GetLogs(long id, int pageIndex = 1, int pageSize = 20)
{
RefAsync<int> total = 0;
var result = await _deviceService.GetDeviceLogsAsync(id, pageIndex, pageSize, total);
Response.Headers["X-Total-Count"] = total.Value.ToString();
return result.IsSuccess
? Ok(Result<List<DeviceLogDto>>.Success(result.Data))
: Ok(Result<List<DeviceLogDto>>.Error(result.Msg));
}
/// <summary>
/// 按物模型可写点向设备下发指令(需 device:control 权限)
/// </summary>
/// <param name="id">设备主键 Id</param>
/// <param name="dto">指令请求(PointId 点Id + Value 工程值 + IsSimulated 是否模拟)</param>
[HttpPost("{id}/command")]
[RequirePermission("device:control")]
public async Task<IActionResult> SendCommand(long id, [FromBody] DeviceCommandDto dto)
{
if (dto == null)
return Ok(Result.Error("请求体不能为空"));
dto.Id = id.ToString();
return Ok(await _deviceCommandService.SendCommandAsync(dto));
}
}
}
+116 -2
View File
@@ -1,14 +1,128 @@
using Microsoft.AspNetCore.Mvc;
using Model;
using Model.Dto.Config;
using Service.Interface.Config;
using SqlSugar;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace WebAPI.Controllers
{
/// <summary>
/// 产品管理
/// 产品管理(含产品分类)
/// </summary>
[ApiController]
[Route("api/config/product")]
public class ProductController : ControllerBase
{
// TODO: 实现 产品管理 相关接口
private readonly IProductService _productService;
public ProductController(IProductService productService)
{
_productService = productService;
}
// ===================== 产品分类 =====================
/// <summary>
/// 产品分类列表
/// </summary>
[HttpGet("category/list")]
public async Task<IActionResult> GetCategories()
{
return Ok(await _productService.GetCategoriesAsync());
}
/// <summary>
/// 新增产品分类
/// </summary>
[HttpPost("category/add")]
public async Task<IActionResult> AddCategory([FromBody] ProductCategoryDto dto)
{
return Ok(await _productService.AddCategoryAsync(dto));
}
/// <summary>
/// 修改产品分类
/// </summary>
[HttpPut("category/update")]
public async Task<IActionResult> UpdateCategory([FromBody] ProductCategoryDto dto)
{
return Ok(await _productService.UpdateCategoryAsync(dto));
}
/// <summary>
/// 删除产品分类(软删除)
/// </summary>
[HttpDelete("category/{id}")]
public async Task<IActionResult> DeleteCategory(long id)
{
return Ok(await _productService.DeleteCategoryAsync(id));
}
// ===================== 产品 =====================
/// <summary>
/// 产品下拉选项(设备选择所属产品)
/// </summary>
[HttpGet("options")]
public async Task<IActionResult> GetOptions()
{
return Ok(await _productService.GetOptionsAsync());
}
/// <summary>
/// 产品列表(分页)
/// </summary>
/// <param name="pageIndex">页码(从1开始,默认1</param>
/// <param name="pageSize">每页数量(默认10</param>
/// <param name="keyword">关键字(模糊匹配型号/名称)</param>
/// <param name="categoryId">分类Id0 表示不过滤)</param>
[HttpGet("list")]
public async Task<IActionResult> GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, long categoryId = 0)
{
RefAsync<int> total = 0;
var result = await _productService.GetPagedAsync(pageIndex, pageSize, total, keyword, categoryId);
Response.Headers["X-Total-Count"] = total.Value.ToString();
return result.IsSuccess
? Ok(Result<List<ProductDto>>.Success(result.Data))
: Ok(Result<List<ProductDto>>.Error(result.Msg));
}
/// <summary>
/// 产品详情
/// </summary>
[HttpGet("{id}")]
public async Task<IActionResult> GetById(long id)
{
return Ok(await _productService.GetByIdAsync(id));
}
/// <summary>
/// 新增产品
/// </summary>
[HttpPost]
public async Task<IActionResult> Add([FromBody] ProductDto dto)
{
return Ok(await _productService.AddAsync(dto));
}
/// <summary>
/// 修改产品
/// </summary>
[HttpPut]
public async Task<IActionResult> Update([FromBody] ProductDto dto)
{
return Ok(await _productService.UpdateAsync(dto));
}
/// <summary>
/// 删除产品(软删除)
/// </summary>
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
return Ok(await _productService.DeleteAsync(id));
}
}
}
@@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Mvc;
using Model;
using Model.Dto.Config;
using Service.Interface.Config;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace WebAPI.Controllers
{
/// <summary>
/// 物模型点(产品/设备通用,按 ownerType+ownerId 归属)
/// </summary>
[ApiController]
[Route("api/config/thing-point")]
public class ThingPointController : ControllerBase
{
private readonly IThingPointService _thingPointService;
public ThingPointController(IThingPointService thingPointService)
{
_thingPointService = thingPointService;
}
/// <summary>
/// 点列表
/// </summary>
/// <param name="ownerType">归属类型(1=产品 2=设备)</param>
/// <param name="ownerId">归属对象Id</param>
/// <param name="keyword">关键字(点编码/名称)</param>
[HttpGet("list")]
public async Task<IActionResult> GetList(int ownerType, long ownerId, string? keyword = null)
{
return Ok(await _thingPointService.GetListAsync(ownerType, ownerId, keyword));
}
/// <summary>
/// 点详情
/// </summary>
[HttpGet("{id}")]
public async Task<IActionResult> GetById(long id)
{
return Ok(await _thingPointService.GetByIdAsync(id));
}
/// <summary>
/// 新增点
/// </summary>
[HttpPost]
public async Task<IActionResult> Add([FromBody] ThingModelPointDto dto)
{
return Ok(await _thingPointService.AddAsync(dto));
}
/// <summary>
/// 修改点
/// </summary>
[HttpPut]
public async Task<IActionResult> Update([FromBody] ThingModelPointDto dto)
{
return Ok(await _thingPointService.UpdateAsync(dto));
}
/// <summary>
/// 删除点(软删除)
/// </summary>
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
return Ok(await _thingPointService.DeleteAsync(id));
}
}
}
@@ -1,14 +1,156 @@
using Microsoft.AspNetCore.Mvc;
using Model;
using Model.Dto.Inspection;
using Model.Entity.Inspection;
using Service.Interface;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace WebAPI.Controllers
{
/// <summary>
/// 消息告警
/// 消息告警(告警列表与确认 / 告警统计分析 / 统一告警上报入口)
/// </summary>
[ApiController]
[Route("api/inspection/alert-message")]
public class AlertMessageController : ControllerBase
{
// TODO: 实现 消息告警 相关接口
private readonly IAlertMessageService _alertMessageService;
private readonly IAlertCenterService _alertCenterService;
public AlertMessageController(IAlertMessageService alertMessageService, IAlertCenterService alertCenterService)
{
_alertMessageService = alertMessageService;
_alertCenterService = alertCenterService;
}
/// <summary>
/// 告警列表(分页,支持级别/状态/类型/来源/设备/关键字/时间范围筛选;总记录数见响应头 X-Total-Count
/// </summary>
[HttpGet("list")]
public async Task<Result<List<AlertMessageDto>>> GetList(int pageIndex = 1, int pageSize = 10,
AlertLevelEnum? level = null, AlertStatusEnum? status = null, AlertTypeEnum? type = null,
AlertSourceEnum? source = null, string? deviceCode = null, string? keyword = null,
DateTime? startDate = null, DateTime? endDate = null)
{
RefAsync<int> total = 0;
var result = await _alertMessageService.GetPagedAsync(pageIndex, pageSize, total,
level, status, type, source, deviceCode, keyword, startDate, endDate);
Response.Headers["X-Total-Count"] = total.Value.ToString();
return result;
}
/// <summary>
/// 告警详情
/// </summary>
[HttpGet("{id}")]
public async Task<Result<AlertMessageDto>> GetById(long id)
{
return await _alertMessageService.GetByIdAsync(id);
}
/// <summary>
/// 告警上报统一入口(供其它模块/第三方系统跨进程调用;进程内模块请直接注入 IAlertCenterService
/// </summary>
[HttpPost("raise")]
public async Task<Result<string>> Raise([FromBody] AlertRaiseDto dto)
{
return await _alertCenterService.RaiseAsync(dto);
}
/// <summary>
/// 确认告警
/// </summary>
[HttpPost("{id}/ack")]
public async Task<Result<bool>> Acknowledge(long id, [FromBody] AlertOperateDto dto)
{
return await _alertMessageService.AcknowledgeAsync(id, dto);
}
/// <summary>
/// 处理告警
/// </summary>
[HttpPost("{id}/handle")]
public async Task<Result<bool>> Handle(long id, [FromBody] AlertOperateDto dto)
{
return await _alertMessageService.HandleAsync(id, dto);
}
/// <summary>
/// 关闭告警
/// </summary>
[HttpPost("{id}/close")]
public async Task<Result<bool>> Close(long id, [FromBody] AlertOperateDto dto)
{
return await _alertMessageService.CloseAsync(id, dto);
}
/// <summary>
/// 忽略告警(误报等场景)
/// </summary>
[HttpPost("{id}/ignore")]
public async Task<Result<bool>> Ignore(long id, [FromBody] AlertOperateDto dto)
{
return await _alertMessageService.IgnoreAsync(id, dto);
}
/// <summary>
/// 转工单(一期仅变更状态并记录工单Id,工单模块二期对接)
/// </summary>
[HttpPost("{id}/to-workorder")]
public async Task<Result<bool>> ToWorkOrder(long id, [FromBody] AlertOperateDto dto)
{
return await _alertMessageService.ToWorkOrderAsync(id, dto);
}
/// <summary>
/// 告警概览(未确认数/今日新增/今日紧急/近7日总数)
/// </summary>
[HttpGet("statistics/overview")]
public async Task<Result<AlertOverviewDto>> GetOverview()
{
return await _alertMessageService.GetOverviewAsync();
}
/// <summary>
/// 告警趋势统计(granularityday/week/month
/// </summary>
[HttpGet("statistics/trend")]
public async Task<Result<List<AlertTrendItemDto>>> GetTrend(string granularity = "day",
DateTime? startDate = null, DateTime? endDate = null)
{
return await _alertMessageService.GetTrendAsync(granularity, startDate, endDate);
}
/// <summary>
/// 告警分布统计(dimensiondevice/type/level/source
/// </summary>
[HttpGet("statistics/distribution")]
public async Task<Result<List<AlertDistributionItemDto>>> GetDistribution(string dimension = "level",
DateTime? startDate = null, DateTime? endDate = null)
{
return await _alertMessageService.GetDistributionAsync(dimension, startDate, endDate);
}
/// <summary>
/// 高频告警 TOP 排名
/// </summary>
[HttpGet("statistics/top")]
public async Task<Result<List<AlertTopItemDto>>> GetTop(int topN = 10,
DateTime? startDate = null, DateTime? endDate = null)
{
return await _alertMessageService.GetTopAsync(topN, startDate, endDate);
}
/// <summary>
/// 告警处理及时率统计
/// </summary>
[HttpGet("statistics/timeliness")]
public async Task<Result<AlertTimelinessDto>> GetTimeliness(DateTime? startDate = null, DateTime? endDate = null)
{
return await _alertMessageService.GetTimelinessAsync(startDate, endDate);
}
}
}
@@ -0,0 +1,196 @@
using Microsoft.AspNetCore.Mvc;
using Model;
using Model.Dto.Inspection;
using Service.Interface;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace WebAPI.Controllers
{
/// <summary>
/// 告警通知配置(渠道管理 / 通知规则 / 推送日志 / 值班排班 / 跨网发件箱)
/// </summary>
[ApiController]
[Route("api/inspection/alert-notify")]
public class AlertNotifyController : ControllerBase
{
private readonly IAlertNotifyService _alertNotifyService;
public AlertNotifyController(IAlertNotifyService alertNotifyService)
{
_alertNotifyService = alertNotifyService;
}
#region
/// <summary>
/// 渠道列表(Secret 脱敏)
/// </summary>
[HttpGet("channel/list")]
public async Task<Result<List<AlertNotifyChannelDto>>> GetChannels()
{
return await _alertNotifyService.GetChannelsAsync();
}
/// <summary>
/// 新增渠道
/// </summary>
[HttpPost("channel")]
public async Task<Result<bool>> AddChannel([FromBody] AlertNotifyChannelDto dto)
{
return await _alertNotifyService.AddChannelAsync(dto);
}
/// <summary>
/// 修改渠道(Secret 传空表示保持原值)
/// </summary>
[HttpPut("channel/{id}")]
public async Task<Result<bool>> UpdateChannel(long id, [FromBody] AlertNotifyChannelDto dto)
{
dto.Id = id.ToString();
return await _alertNotifyService.UpdateChannelAsync(dto);
}
/// <summary>
/// 删除渠道(软删除)
/// </summary>
[HttpDelete("channel/{id}")]
public async Task<Result<bool>> DeleteChannel(long id)
{
return await _alertNotifyService.DeleteChannelAsync(id);
}
/// <summary>
/// 渠道测试推送(验证 Webhook 配置是否正确)
/// </summary>
[HttpPost("channel/{id}/test")]
public async Task<Result<bool>> TestChannel(long id)
{
return await _alertNotifyService.TestSendAsync(id);
}
#endregion
#region
/// <summary>
/// 通知规则列表
/// </summary>
[HttpGet("rule/list")]
public async Task<Result<List<AlertNotifyRuleDto>>> GetRules()
{
return await _alertNotifyService.GetRulesAsync();
}
/// <summary>
/// 新增通知规则
/// </summary>
[HttpPost("rule")]
public async Task<Result<bool>> AddRule([FromBody] AlertNotifyRuleDto dto)
{
return await _alertNotifyService.AddRuleAsync(dto);
}
/// <summary>
/// 修改通知规则
/// </summary>
[HttpPut("rule/{id}")]
public async Task<Result<bool>> UpdateRule(long id, [FromBody] AlertNotifyRuleDto dto)
{
dto.Id = id.ToString();
return await _alertNotifyService.UpdateRuleAsync(dto);
}
/// <summary>
/// 删除通知规则(软删除)
/// </summary>
[HttpDelete("rule/{id}")]
public async Task<Result<bool>> DeleteRule(long id)
{
return await _alertNotifyService.DeleteRuleAsync(id);
}
/// <summary>
/// 启用/停用通知规则
/// </summary>
[HttpPut("rule/{id}/enabled")]
public async Task<Result<bool>> SetRuleEnabled(long id, [FromQuery] bool enabled)
{
return await _alertNotifyService.SetRuleEnabledAsync(id, enabled);
}
#endregion
#region
/// <summary>
/// 推送日志分页(可按成功状态/渠道筛选;总记录数见响应头 X-Total-Count
/// </summary>
[HttpGet("log/list")]
public async Task<Result<List<AlertNotifyLogDto>>> GetLogs(int pageIndex = 1, int pageSize = 10,
bool? success = null, long channelId = 0)
{
RefAsync<int> total = 0;
var result = await _alertNotifyService.GetLogsPagedAsync(pageIndex, pageSize, total, success, channelId);
Response.Headers["X-Total-Count"] = total.Value.ToString();
return result;
}
#endregion
#region
/// <summary>
/// 值班排班列表(按日期范围,缺省本月)
/// </summary>
[HttpGet("duty/list")]
public async Task<Result<List<DutyRosterDto>>> GetDuties(DateTime? startDate = null, DateTime? endDate = null)
{
return await _alertNotifyService.GetDutiesAsync(startDate, endDate);
}
/// <summary>
/// 新增排班
/// </summary>
[HttpPost("duty")]
public async Task<Result<bool>> AddDuty([FromBody] DutyRosterDto dto)
{
return await _alertNotifyService.AddDutyAsync(dto);
}
/// <summary>
/// 修改排班
/// </summary>
[HttpPut("duty/{id}")]
public async Task<Result<bool>> UpdateDuty(long id, [FromBody] DutyRosterDto dto)
{
dto.Id = id.ToString();
return await _alertNotifyService.UpdateDutyAsync(dto);
}
/// <summary>
/// 删除排班(软删除)
/// </summary>
[HttpDelete("duty/{id}")]
public async Task<Result<bool>> DeleteDuty(long id)
{
return await _alertNotifyService.DeleteDutyAsync(id);
}
#endregion
#region AlertBridge 使
/// <summary>
/// 拉取待推送发件箱(AlertBridge 轮询调用;返回自包含推送报文)
/// </summary>
[HttpGet("outbox/pull")]
public async Task<Result<List<AlertOutboxItemDto>>> PullOutbox([FromQuery] int batch = 20)
{
return await _alertNotifyService.PullOutboxAsync(batch);
}
/// <summary>
/// 回写发件箱推送结果(AlertBridge 推送完成后调用)
/// </summary>
[HttpPost("outbox/{id}/result")]
public async Task<Result<bool>> ReportOutboxResult(long id, [FromBody] OutboxResultDto dto)
{
return await _alertNotifyService.ReportOutboxResultAsync(id, dto);
}
#endregion
}
}
@@ -1,14 +1,79 @@
using Microsoft.AspNetCore.Mvc;
using Model.Dto.Inspection;
using Service.Interface;
using System.Threading.Tasks;
namespace WebAPI.Controllers
{
/// <summary>
/// 告警规则
/// 告警规则(规则实例:查看所有设备告警的规则列表)
/// </summary>
[ApiController]
[Route("api/inspection/alert-rule")]
public class AlertRuleController : ControllerBase
{
// TODO: 实现 告警规则 相关接口
private readonly IAlertRuleService _alertRuleService;
public AlertRuleController(IAlertRuleService alertRuleService)
{
_alertRuleService = alertRuleService;
}
/// <summary>
/// 获取全部告警规则列表
/// </summary>
[HttpGet("list")]
public async Task<IActionResult> GetList()
{
return Ok(await _alertRuleService.GetListAsync());
}
/// <summary>
/// 获取某归属对象(产品/设备)下的规则列表
/// </summary>
/// <param name="ownerType">归属类型(1=产品 2=设备)</param>
/// <param name="ownerId">归属对象Id</param>
/// <param name="pointCode">可选:绑定点编码过滤</param>
[HttpGet("owner")]
public async Task<IActionResult> GetOwnerRules(int ownerType, long ownerId, string? pointCode = null)
{
return Ok(await _alertRuleService.GetOwnerRulesAsync(ownerType, ownerId, pointCode));
}
/// <summary>
/// 新增告警规则(前端传 DTO
/// </summary>
[HttpPost("add")]
public async Task<IActionResult> Add([FromBody] AlertRuleDto dto)
{
return Ok(await _alertRuleService.AddAsync(dto));
}
/// <summary>
/// 修改告警规则(前端传 DTO
/// </summary>
[HttpPut("update")]
public async Task<IActionResult> Update([FromBody] AlertRuleDto dto)
{
return Ok(await _alertRuleService.UpdateAsync(dto));
}
/// <summary>
/// 删除告警规则(软删除)
/// </summary>
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
return Ok(await _alertRuleService.DeleteAsync(id));
}
/// <summary>
/// 启用/停用告警规则
/// </summary>
[HttpPut("{id}/enabled")]
public async Task<IActionResult> SetEnabled(long id, [FromQuery] bool enabled)
{
return Ok(await _alertRuleService.SetEnabledAsync(id, enabled));
}
}
}
@@ -0,0 +1,67 @@
using Microsoft.AspNetCore.Mvc;
using Model.Dto.Inspection;
using Service.Interface;
using System.Threading.Tasks;
namespace WebAPI.Controllers
{
/// <summary>
/// 数据转发(配置数据转发规则,支持通过消息通知的方式定时转发数据)
/// </summary>
[ApiController]
[Route("api/inspection/data-forward")]
public class DataForwardController : ControllerBase
{
private readonly IDataForwardService _dataForwardService;
public DataForwardController(IDataForwardService dataForwardService)
{
_dataForwardService = dataForwardService;
}
/// <summary>
/// 获取全部转发规则列表
/// </summary>
[HttpGet("list")]
public async Task<IActionResult> GetList()
{
return Ok(await _dataForwardService.GetListAsync());
}
/// <summary>
/// 新增转发规则(前端传 DTO
/// </summary>
[HttpPost("add")]
public async Task<IActionResult> Add([FromBody] DataForwardRuleDto dto)
{
return Ok(await _dataForwardService.AddAsync(dto));
}
/// <summary>
/// 修改转发规则(前端传 DTO
/// </summary>
[HttpPut("update")]
public async Task<IActionResult> Update([FromBody] DataForwardRuleDto dto)
{
return Ok(await _dataForwardService.UpdateAsync(dto));
}
/// <summary>
/// 删除转发规则(软删除)
/// </summary>
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(long id)
{
return Ok(await _dataForwardService.DeleteAsync(id));
}
/// <summary>
/// 启用/停用转发规则
/// </summary>
[HttpPut("{id}/enabled")]
public async Task<IActionResult> SetEnabled(long id, [FromQuery] bool enabled)
{
return Ok(await _dataForwardService.SetEnabledAsync(id, enabled));
}
}
}
@@ -1,14 +1,114 @@
using Microsoft.AspNetCore.Mvc;
using Service.Interface;
using System.Threading.Tasks;
namespace WebAPI.Controllers
{
/// <summary>
/// 统计报表
/// 统计报表(设备统计/维护报表/维修报表)
/// </summary>
[ApiController]
[Route("api/inspection/report")]
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 Model;
using Model.Dto.System;
using Service.Interface;
using SqlSugar;
using WebAPI.Filters;
namespace WebAPI.Controllers
{
/// <summary>
/// 操作审计日志
/// 操作审计日志(满足实验室审计要求;需 user:manage 权限)
/// </summary>
[ApiController]
[Route("api/system/audit")]
[RequirePermission("user:manage")]
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 Model;
using Model.Dto.System;
using Service.Interface;
using SqlSugar;
using WebAPI.Filters;
namespace WebAPI.Controllers
{
/// <summary>
/// 数据字典管理
/// 数据字典管理(管理侧需 system:manage 权限;业务侧 options 接口无权限供其它模块调用)
/// </summary>
[ApiController]
[Route("api/system/dict")]
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 Model;
using Model.Dto.System;
using Service.Interface;
using SqlSugar;
using WebAPI.Filters;
namespace WebAPI.Controllers
{
/// <summary>
/// 文件存储管理
/// 文件存储管理(存储配置 CRUD + 连接测试 + 文件上传 + 文件记录查询,需 system:manage 权限)
/// </summary>
[ApiController]
[Route("api/system/file-storage")]
[RequirePermission("system:manage")]
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 Model;
using Model.Dto.System;
using Service.Interface;
using WebAPI.Filters;
namespace WebAPI.Controllers
{
/// <summary>
/// 组织架构管理
/// 组织架构管理(公司/实验室/部门/班组树;需 user:manage 权限)
/// </summary>
[ApiController]
[Route("api/system/organization")]
[RequirePermission("user:manage")]
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 Model;
using Model.Dto.System;
using Service.Interface;
using SqlSugar;
using WebAPI.Filters;
namespace WebAPI.Controllers
{
/// <summary>
/// 角色权限管理
/// 角色权限管理(需 user:manage 权限)
/// </summary>
[ApiController]
[Route("api/system/role")]
[RequirePermission("user:manage")]
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 Model;
using Model.Dto.System;
using Service.Interface;
using SqlSugar;
using WebAPI.Filters;
namespace WebAPI.Controllers
{
/// <summary>
/// 系统参数配置
/// 系统参数配置(管理侧需 system:manage 权限;业务侧 value/values 接口无权限供其它模块调用)
/// </summary>
[ApiController]
[Route("api/system/param")]
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 Model;
using Model.Dto.System;
using Service.Interface;
using SqlSugar;
using WebAPI.Filters;
namespace WebAPI.Controllers
{
/// <summary>
/// 用户管理
/// 用户管理(需 user:manage 权限)
/// </summary>
[ApiController]
[Route("api/system/user")]
[RequirePermission("user:manage")]
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;
}
}
}
+41
View File
@@ -1,7 +1,11 @@
using ORM;
using Service.Implement;
using Service.Interface;
using SqlSugar;
using System.Net.Http;
using System.Net.Security;
using System.Reflection;
using System.Security.Authentication;
namespace WebAPI
{
@@ -50,7 +54,44 @@ namespace WebAPI
// 泛型基础服务(IBaseService<> -> 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;
}
/// <summary>
/// 注册告警通知组件(飞书/钉钉/企微 NotifierTyped HttpClient 模式)
/// 新增渠道:实现 IAlertNotifier 后在此追加一行注册即可,调用方零改动
/// </summary>
public static IServiceCollection AddAlertNotification(this IServiceCollection services)
{
ConfigureNotifier(services.AddHttpClient<IAlertNotifier, FeishuNotifier>());
ConfigureNotifier(services.AddHttpClient<IAlertNotifier, DingTalkNotifier>());
ConfigureNotifier(services.AddHttpClient<IAlertNotifier, WeComNotifier>());
return services;
}
/// <summary>
/// 通知渠道 HttpClient 统一配置:
/// - 超时 15s(默认 100s 过长,测试推送遇不可达地址会长时间无响应)
/// - 显式启用 TLS1.2/1.3(规避个别环境默认协议协商失败导致的 SSL 握手异常)
/// - 连接池生命周期 5 分钟(长驻服务定期回收连接,避免陈旧连接/DNS 漂移引发握手失败)
/// </summary>
private static void ConfigureNotifier(IHttpClientBuilder builder)
{
builder
.ConfigureHttpClient(c => c.Timeout = TimeSpan.FromSeconds(15))
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
SslOptions = new SslClientAuthenticationOptions
{
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13
},
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2)
});
}
}
}
@@ -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;
}
}
}
+5
View File
@@ -8,6 +8,7 @@
<ItemGroup>
<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" />
</ItemGroup>
@@ -17,4 +18,8 @@
<ProjectReference Include="..\Service\Service.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Converters\" />
</ItemGroup>
</Project>
+52 -1
View File
@@ -1,5 +1,10 @@
using Common;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using ORM;
using Service.Implement;
using Service.Interface;
using System.Text;
using System.Text.Json;
using WebAPI.Services;
@@ -31,17 +36,58 @@ namespace WebAPI
DatabaseConfig.SetTenant(tenantId);
DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
SqlSugarContext.InitDatabase();
builder.Services.AddControllers()
// RBAC 数据种子:预定义角色/权限/默认管理员(幂等)
DataSeeder.Seed();
// 飞书自建应用凭证:用于告警 @ 时把接收人手机号转成 open_id(webhook 机器人自身无此能力)
var feishu = builder.Configuration.GetSection("Feishu");
Common.Notify.FeishuConfig.Init(feishu["AppId"] ?? "", feishu["AppSecret"] ?? "");
builder.Services.AddControllers(options =>
{
// 非空字符串属性(如 Remark)缺失时不报 400,前端可以只传部分字段
options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString;
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
// 自动注册业务服务(Service.Interface -> Service.Implement
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
builder.Services.AddAlertNotification();
builder.Services.AddHostedService<AlertNotifyWorker>();
// 温度箱模拟数据:每秒生成 100 条温湿度入库(真实设备接入后删除)
builder.Services.AddHostedService<TemperatureBoxSimulator>();
@@ -61,8 +107,13 @@ namespace WebAPI
// 导致前端 vite 代理被 CORS 拦截(Network Error);上位机/现场部署通常也只用 http
// app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
// 静态文件:设备附件上传后通过 /uploads/... 访问(存储于 wwwroot
app.UseStaticFiles();
app.MapControllers();
app.Run();
+230
View File
@@ -0,0 +1,230 @@
using Model.Entity.Inspection;
using ORM;
using Service.Implement;
using Service.Interface;
using SqlSugar;
namespace WebAPI.Services
{
/// <summary>
/// 告警通知后台 Worker:消费 AlertNotifyBus 队列,按通知规则分发到飞书/钉钉/企微渠道
/// 流程:加载告警 → 匹配启用规则(按级别) → 静默期判断 → 直连推送(重试3次)或写跨网发件箱 → 记推送日志
/// </summary>
public class AlertNotifyWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<AlertNotifyWorker> _logger;
/// <summary>
/// 直连推送最大尝试次数(含首次)
/// </summary>
private const int MaxAttempts = 3;
public AlertNotifyWorker(IServiceScopeFactory scopeFactory, ILogger<AlertNotifyWorker> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("告警通知 Worker 已启动");
try
{
await foreach (var evt in AlertNotifyBus.Reader.ReadAllAsync(stoppingToken))
{
try
{
await ProcessEventAsync(evt, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "处理告警通知事件失败, AlertId={AlertId}", evt.AlertId);
}
}
}
catch (OperationCanceledException)
{
// 正常停机
}
}
/// <summary>
/// 处理单个告警通知事件
/// </summary>
private async Task ProcessEventAsync(AlertNotifyEvent evt, CancellationToken ct)
{
var db = SqlSugarContext.DbContext;
// 1. 加载告警消息
var alert = await db.Queryable<AlertMessageEntity>()
.Where(x => x.Id == evt.AlertId && x.IsDel == 0)
.FirstAsync();
if (alert == null) return;
// 2. 匹配启用的通知规则(按告警级别)
var rules = await db.Queryable<AlertNotifyRuleEntity>()
.Where(x => x.IsDel == 0 && x.IsEnabled && x.AlertLevel == alert.AlertLevel)
.ToListAsync();
if (rules.Count == 0) return;
// 3. 每个事件独立创建作用域(Worker 是单例,服务是 Scoped
using var scope = _scopeFactory.CreateScope();
var notifyService = scope.ServiceProvider.GetRequiredService<IAlertNotifyService>();
var notifiers = scope.ServiceProvider.GetServices<IAlertNotifier>().ToList();
foreach (var rule in rules)
{
var channel = await db.Queryable<AlertNotifyChannelEntity>()
.Where(x => x.Id == rule.ChannelId && x.IsDel == 0 && x.IsEnabled)
.FirstAsync();
if (channel == null) continue;
// 4. 静默期判断:合并触发的重复告警在静默期内不重复推送(新告警必推)
if (!evt.IsNewAlert && rule.SilenceMinutes > 0)
{
var silenceStart = DateTime.Now.AddMinutes(-rule.SilenceMinutes);
bool recentlyPushed = await db.Queryable<AlertNotifyLogEntity>()
.AnyAsync(x => x.AlertId == alert.Id && x.ChannelId == channel.Id
&& x.Success && x.NotifyTime >= silenceStart);
if (recentlyPushed) continue;
}
// 5. 组装消息与 @ 列表(规则接收人 + 当班值班人电话)
var message = BuildMessage(alert);
var atMobiles = new List<string>();
if (!string.IsNullOrWhiteSpace(rule.Receivers))
{
// 接收人常带 @ 前缀(如 "@18872509102"),但企微 mentioned_mobile_list / 钉钉 atMobiles 只认纯手机号,
// 带 @ 的非法值无法匹配群成员导致 @ 不生效,这里统一去掉前导 @ 与空白
atMobiles.AddRange(rule.Receivers.Split(',', '', ';', '')
.Select(s => s.Trim().TrimStart('@').Trim()).Where(s => s.Length > 0));
}
if (rule.NotifyDutyPerson)
{
var duty = await notifyService.GetDutyByDateAsync(alert.LastAlertTime.Date);
if (duty != null && !string.IsNullOrWhiteSpace(duty.Phone))
{
atMobiles.Add(duty.Phone.Trim());
message.Content += $"\n值班人: {duty.PersonName}";
}
}
atMobiles = atMobiles.Distinct().ToList();
// 飞书渠道:at 标签只认 open_id,不认手机号——把接收人里的手机号转成 open_id
// "all" 与 ou_ 开头的原样保留)。转换在内网主站完成,直连与跨网(Outbox)两条路径都拿到 open_idAlertBridge 无需改。
if (channel.ChannelType == NotifyChannelTypeEnum.Feishu && atMobiles.Count > 0)
{
var feishuContact = scope.ServiceProvider.GetRequiredService<IFeishuContactService>();
var openIds = new List<string>();
foreach (var at in atMobiles)
{
if (at.Equals("all", StringComparison.OrdinalIgnoreCase) || at.StartsWith("ou_"))
{
openIds.Add(at);
}
else
{
var openId = await feishuContact.GetOpenIdByMobileAsync(at);
if (!string.IsNullOrEmpty(openId)) openIds.Add(openId);
else _logger.LogWarning("飞书接收人 {Receiver} 未换取到 open_id,已跳过 @(检查飞书应用权限与通讯录范围)", at);
}
}
atMobiles = openIds.Distinct().ToList();
}
// 关键:@ 列表必须回填到 message——直连模式的 Notifier 是从 message.AtMobiles 读取的。
// 此前只把 atMobiles 传给了 Outbox 分支,直连 SendAsync(message) 的 message.AtMobiles 始终为空,导致企微/钉钉不 @ 任何人。
message.AtMobiles = atMobiles;
// 6. 按渠道推送模式分发
if (channel.PushMode == NotifyPushModeEnum.Outbox)
{
// 跨网模式:写发件箱,由跳板机 AlertBridge 拉取推送并回写结果
await notifyService.EnqueueOutboxAsync(alert.Id, channel.Id, message, atMobiles);
_logger.LogInformation("告警 {AlertId} 已转入跨网发件箱(渠道 {Channel}", alert.Id, channel.Name);
continue;
}
// 直连模式:查找对应 Notifier,失败重试
var notifier = notifiers.FirstOrDefault(n => n.ChannelType == channel.ChannelType);
if (notifier == null)
{
await notifyService.WriteLogAsync(alert.Id, channel.Id, message.Title,
false, $"暂不支持的渠道类型: {channel.ChannelType}", 0);
continue;
}
NotifyResult result = NotifyResult.Fail("未执行");
int attempt = 0;
while (attempt < MaxAttempts && !ct.IsCancellationRequested)
{
attempt++;
result = await notifier.SendAsync(message, channel.WebhookUrl, channel.Secret, ct);
if (result.Success) break;
if (attempt < MaxAttempts)
{
await Task.Delay(TimeSpan.FromSeconds(attempt * 2), ct); // 2s/4s 退避
}
}
await notifyService.WriteLogAsync(alert.Id, channel.Id,
message.Title + " " + Truncate(message.Content, 800),
result.Success, result.ErrorMsg, attempt - 1);
if (!result.Success)
{
_logger.LogWarning("告警 {AlertId} 推送渠道 {Channel} 失败: {Error}", alert.Id, channel.Name, result.ErrorMsg);
}
}
}
/// <summary>
/// 按内置模板构建通知消息(标题 + markdown 正文;后续可由通知模板模块接管)
/// </summary>
private static AlertNotifyMessage BuildMessage(AlertMessageEntity alert)
{
string levelLabel = AlertLevelMap.ToLabel(alert.AlertLevel);
var content = new System.Text.StringBuilder();
content.AppendLine($"- 设备: {alert.DeviceName}({alert.DeviceCode})");
if (!string.IsNullOrWhiteSpace(alert.DeviceType)) content.AppendLine($"- 类型: {alert.DeviceType}");
if (!string.IsNullOrWhiteSpace(alert.Metric)) content.AppendLine($"- 测点: {alert.Metric}");
if (alert.CurrentValue.HasValue) content.AppendLine($"- 当前值: {alert.CurrentValue.Value}");
if (alert.ThresholdValue.HasValue) content.AppendLine($"- 阈值: {alert.ThresholdValue.Value}");
content.AppendLine($"- 告警类型: {TypeLabel(alert.AlertType)}");
content.AppendLine($"- 来源: {SourceLabel(alert.Source)}");
content.AppendLine($"- 内容: {alert.Content}");
if (alert.TriggerCount > 1) content.AppendLine($"- 累计触发: {alert.TriggerCount} 次");
content.Append($"- 时间: {alert.LastAlertTime:yyyy-MM-dd HH:mm:ss}");
return new AlertNotifyMessage
{
Title = $"【{levelLabel}】设备告警 - {alert.DeviceName ?? alert.DeviceCode}",
Content = content.ToString()
};
}
private static string TypeLabel(AlertTypeEnum type) => type switch
{
AlertTypeEnum.ThresholdExceed => "阈值超限",
AlertTypeEnum.DeviceOffline => "设备离线",
AlertTypeEnum.CommFault => "通讯故障",
AlertTypeEnum.PatrolAbnormal => "巡检异常",
AlertTypeEnum.Custom => "自定义",
_ => "其它"
};
private static string SourceLabel(AlertSourceEnum source) => source switch
{
AlertSourceEnum.RuleEngine => "规则引擎",
AlertSourceEnum.AutoPatrol => "自动巡检",
AlertSourceEnum.Dashboard => "数据看板",
AlertSourceEnum.Collection => "数据采集",
AlertSourceEnum.Manual => "手动上报",
_ => "其它"
};
private static string Truncate(string s, int max) =>
string.IsNullOrEmpty(s) || s.Length <= max ? s : s.Substring(0, max) + "...";
}
}
+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();
}
}
}
+34 -1
View File
@@ -1,5 +1,7 @@
using Model.Dto.Inspection;
using Model.Entity.Inspection;
using ORM;
using Service.Interface;
namespace WebAPI.Services
{
@@ -22,9 +24,11 @@ namespace WebAPI.Services
}
private readonly Dictionary<string, BoxState> _states = new();
private readonly IServiceScopeFactory _scopeFactory;
public TemperatureBoxSimulator()
public TemperatureBoxSimulator(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
for (int i = 1; i <= DeviceCount; i++)
{
_states[$"BOX-{i:D3}"] = new BoxState();
@@ -39,6 +43,7 @@ namespace WebAPI.Services
{
var now = DateTime.Now;
var batch = new List<TemperatureBoxDataEntity>(DeviceCount);
var alarmDevices = new List<(string Code, double Temp)>();
foreach (var kv in _states)
{
@@ -48,6 +53,10 @@ namespace WebAPI.Services
s.Humidity = Math.Clamp(s.Humidity + (Rnd.NextDouble() - 0.5) * 3, 20, 90);
bool alarm = s.Temperature >= AlarmTemp;
if (alarm)
{
alarmDevices.Add((kv.Key, Math.Round(s.Temperature, 1)));
}
batch.Add(new TemperatureBoxDataEntity
{
DeviceCode = kv.Key,
@@ -62,6 +71,30 @@ namespace WebAPI.Services
// 批量插入
await SqlSugarContext.DbContext.Insertable(batch).ExecuteCommandAsync();
// 报警设备上报告警中心(告警中心内部有 5 分钟合并窗口,不会每秒重复建告警)
if (alarmDevices.Count > 0)
{
// 单例 HostedService 不能直接注入 Scoped 服务,按批次创建作用域解析
using var scope = _scopeFactory.CreateScope();
var alertCenter = scope.ServiceProvider.GetRequiredService<IAlertCenterService>();
foreach (var (code, temp) in alarmDevices)
{
await alertCenter.RaiseAsync(new AlertRaiseDto
{
Source = AlertSourceEnum.Collection,
DeviceCode = code,
DeviceName = $"温度箱 {code}",
DeviceType = "TemperatureBox",
Metric = "Temperature",
AlertType = AlertTypeEnum.ThresholdExceed,
AlertLevel = AlertLevelEnum.Urgent,
CurrentValue = temp,
ThresholdValue = AlarmTemp,
Content = $"温度箱 {code} 温度超限:当前 {temp}℃ ≥ 报警阈值 {AlarmTemp}℃"
});
}
}
}
catch (Exception ex)
{
+2 -2
View File
@@ -16,8 +16,8 @@ namespace Model.Dto.Asset
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>设备Id(关联 EquipmentEntity.Id</summary>
public long EquipmentId { get; set; }
/// <summary>设备Id(关联 EquipmentEntity.Id,雪花 Id 转 string 防精度丢失</summary>
public string EquipmentId { get; set; }
/// <summary>文件名称</summary>
public string? FileName { get; set; }
+2 -2
View File
@@ -14,8 +14,8 @@ namespace Model.Dto.Asset
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>父级分类Id0表示顶级)</summary>
public long ParentId { get; set; }
/// <summary>父级分类Id0表示顶级,雪花 Id 转 string 防精度丢失</summary>
public string ParentId { get; set; }
/// <summary>分类名称</summary>
public string? Name { get; set; }
+10 -4
View File
@@ -7,8 +7,8 @@ namespace Model.Dto.Asset
/// </summary>
public class EquipmentDto
{
/// <summary>主键 Idlong → string</summary>
public string Id { get; set; }
/// <summary>主键 Idlong → string;入参可空,新增时无需传递</summary>
public string? Id { get; set; }
/// <summary>删除状态(0、未删除;1、已删除)</summary>
public byte IsDel { get; set; }
@@ -22,8 +22,8 @@ namespace Model.Dto.Asset
/// <summary>设备名称</summary>
public string? Name { get; set; }
/// <summary>设备分类Id(关联 EquipmentCategoryEntity.Id</summary>
public long CategoryId { get; set; }
/// <summary>设备分类Id(关联 EquipmentCategoryEntity.Id,雪花 Id 转 string 防精度丢失;可空,未分类时为 "0" 或不传</summary>
public string? CategoryId { get; set; }
/// <summary>设备类型</summary>
public string? Type { get; set; }
@@ -49,6 +49,12 @@ namespace Model.Dto.Asset
/// <summary>二维码编号(一物一码)</summary>
public string? QrCode { get; set; }
/// <summary>二维码扫码直达 URL(生成后写入,扫码跳转设备台账详情)</summary>
public string? QrCodeUrl { get; set; }
/// <summary>RFID 编号(绑定时写入)</summary>
public string? RfidCode { get; set; }
/// <summary>设备描述</summary>
public string? Description { get; set; }
@@ -0,0 +1,25 @@
using Model.Entity.Asset;
namespace Model.Dto.Asset
{
/// <summary>
/// 设备状态变更请求 DTOPOST api/asset/equipment/{id}/status 入参)
/// </summary>
public class EquipmentStatusChangeDto
{
/// <summary>
/// 目标状态(必填)
/// </summary>
public EquipmentStatusEnum Status { get; set; }
/// <summary>
/// 操作人
/// </summary>
public string? Operator { get; set; }
/// <summary>
/// 变更原因/备注
/// </summary>
public string? Remark { get; set; }
}
}
+2 -2
View File
@@ -16,8 +16,8 @@ namespace Model.Dto.Asset
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>设备Id(关联 EquipmentEntity.Id</summary>
public long EquipmentId { get; set; }
/// <summary>设备Id(关联 EquipmentEntity.Id,雪花 Id 转 string 防精度丢失</summary>
public string EquipmentId { get; set; }
/// <summary>变更前状态</summary>
public EquipmentStatusEnum FromStatus { get; set; }
+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; }
}
}
+28
View File
@@ -0,0 +1,28 @@
namespace Model.Dto.Config
{
/// <summary>
/// 设备指令下发请求 DTO
/// </summary>
public class DeviceCommandDto
{
/// <summary>
/// 设备主键 Idstring
/// </summary>
public string Id { get; set; }
/// <summary>
/// 物模型点 Id(string,决定写哪个寄存器/线圈)
/// </summary>
public string PointId { get; set; }
/// <summary>
/// 下发的工程值
/// </summary>
public double Value { get; set; }
/// <summary>
/// 是否仅记录(模拟/离线调试:不实际发网络报文)
/// </summary>
public bool IsSimulated { get; set; }
}
}
+32
View File
@@ -0,0 +1,32 @@
namespace Model.Dto.Config
{
/// <summary>
/// 设备日志 DTOId 由 long 改为 string,避免前端精度丢失)
/// </summary>
public class DeviceLogDto
{
/// <summary>主键 Idlong → string</summary>
public string Id { get; set; }
/// <summary>设备Id</summary>
public string DeviceId { get; set; }
/// <summary>设备编号</summary>
public string DeviceCode { get; set; }
/// <summary>日志级别(Info/Warn/Error</summary>
public string Level { get; set; }
/// <summary>日志类型(连接/通讯/采集/指令/系统)</summary>
public string LogType { get; set; }
/// <summary>日志内容</summary>
public string Message { get; set; }
/// <summary>记录时间</summary>
public DateTime LogTime { get; set; }
/// <summary>创建时间</summary>
public DateTime? CreateTime { 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; }
}
}
+64
View File
@@ -0,0 +1,64 @@
using Model.Entity.Config;
namespace Model.Dto.Config
{
/// <summary>
/// IOT 设备 DTOId 由 long 改为 string,避免前端精度丢失)
/// </summary>
public class IotDeviceDto
{
/// <summary>主键 Idlong → string</summary>
public string Id { get; set; }
/// <summary>设备编号</summary>
public string Code { get; set; }
/// <summary>设备名称</summary>
public string Name { get; set; }
/// <summary>设备类型(型号,对应 DeviceCommand 驱动类)</summary>
public string DeviceType { get; set; }
/// <summary>所属产品Idstring0=未分类)</summary>
public string ProductId { get; set; }
/// <summary>所属产品型号/名称(只读展示)</summary>
public string? ProductName { get; set; }
/// <summary>所属网关Idstring0=未分配)</summary>
public string GatewayId { get; set; }
/// <summary>所属网关名称(只读展示)</summary>
public string? GatewayName { get; set; }
/// <summary>从站地址</summary>
public byte SlaveId { get; set; }
/// <summary>制造商</summary>
public string? Manufacturer { get; set; }
/// <summary>设备描述</summary>
public string? Description { get; set; }
/// <summary>通讯协议类型</summary>
public IotDeviceProtocolEnum ProtocolType { get; set; }
/// <summary>是否启用采集</summary>
public bool IsEnabled { get; set; } = true;
/// <summary>在线状态</summary>
public IotDeviceOnlineStatusEnum OnlineStatus { get; set; }
/// <summary>最后采集时间</summary>
public DateTime? LastCollectTime { get; set; }
/// <summary>最近错误信息</summary>
public string? LastError { get; set; }
/// <summary>备注</summary>
public string? Remark { get; set; }
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
}
}
+26
View File
@@ -0,0 +1,26 @@
namespace Model.Dto.Config
{
/// <summary>
/// 产品分类 DTOId 由 long 改为 string,避免前端精度丢失)
/// </summary>
public class ProductCategoryDto
{
/// <summary>主键 Idlong → string</summary>
public string Id { get; set; }
/// <summary>分类编码</summary>
public string Code { get; set; }
/// <summary>分类名称</summary>
public string? Name { get; set; }
/// <summary>排序号</summary>
public int Sort { get; set; }
/// <summary>备注</summary>
public string? Remark { get; set; }
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
}
}
+46
View File
@@ -0,0 +1,46 @@
using Model.Entity.Config;
namespace Model.Dto.Config
{
/// <summary>
/// 产品 DTOId 由 long 改为 string,避免前端精度丢失)
/// </summary>
public class ProductDto
{
/// <summary>主键 Idlong → string</summary>
public string Id { get; set; }
/// <summary>型号(唯一)</summary>
public string Model { get; set; }
/// <summary>产品名称</summary>
public string? Name { get; set; }
/// <summary>产品分类Id</summary>
public string CategoryId { get; set; }
/// <summary>分类名称(只读展示)</summary>
public string? CategoryName { get; set; }
/// <summary>制造商</summary>
public string? Manufacturer { get; set; }
/// <summary>产品描述</summary>
public string? Description { get; set; }
/// <summary>通讯协议</summary>
public IotDeviceProtocolEnum ProtocolType { get; set; }
/// <summary>消息模型</summary>
public string? MessageModel { get; set; }
/// <summary>是否启用</summary>
public bool IsEnabled { get; set; } = true;
/// <summary>备注</summary>
public string? Remark { get; set; }
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace Model.Dto.Config
{
/// <summary>
/// 产品下拉选项 DTO(设备选择"所属产品"、筛选下拉用)
/// </summary>
public class ProductOptionDto
{
/// <summary>主键 Idstring</summary>
public string Id { get; set; }
/// <summary>型号</summary>
public string Model { get; set; }
/// <summary>产品名称</summary>
public string? Name { get; set; }
}
}
+58
View File
@@ -0,0 +1,58 @@
using Model.Entity.Config;
namespace Model.Dto.Config
{
/// <summary>
/// 物模型点 DTOId 由 long 改为 string,避免前端精度丢失)
/// </summary>
public class ThingModelPointDto
{
/// <summary>主键 Idlong → string</summary>
public string Id { get; set; }
/// <summary>归属类型(1=产品 2=设备)</summary>
public ThingOwnerTypeEnum OwnerType { get; set; }
/// <summary>归属对象Idstring</summary>
public string OwnerId { get; set; }
/// <summary>点编码</summary>
public string Code { get; set; }
/// <summary>点名称</summary>
public string? Name { get; set; }
/// <summary>寄存器类型</summary>
public ThingRegisterTypeEnum RegisterType { get; set; }
/// <summary>读写权限</summary>
public ThingRwEnum Rw { get; set; }
/// <summary>数据类型</summary>
public ThingDataTypeEnum DataType { get; set; }
/// <summary>寄存器起始地址</summary>
public ushort Address { get; set; }
/// <summary>换算系数</summary>
public double Scale { get; set; } = 1;
/// <summary>偏移量</summary>
public double Offset { get; set; }
/// <summary>单位</summary>
public string? Unit { get; set; }
/// <summary>是否启用</summary>
public bool Enabled { get; set; } = true;
/// <summary>排序号</summary>
public int Sort { get; set; }
/// <summary>备注</summary>
public string? Remark { get; set; }
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
}
}
+82
View File
@@ -0,0 +1,82 @@
using Model.Entity.Inspection;
namespace Model.Dto.Inspection
{
/// <summary>
/// 告警消息 DTOId 转 string 防前端精度丢失)
/// </summary>
public class AlertMessageDto
{
/// <summary>主键 Id</summary>
public string? Id { get; set; }
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>告警来源</summary>
public AlertSourceEnum Source { get; set; }
/// <summary>关联告警规则Id"0"表示无)</summary>
public string? RuleId { get; set; }
/// <summary>设备编号</summary>
public string? DeviceCode { get; set; }
/// <summary>设备名称</summary>
public string? DeviceName { get; set; }
/// <summary>设备类型</summary>
public string? DeviceType { get; set; }
/// <summary>测点/监测指标</summary>
public string? Metric { get; set; }
/// <summary>告警类型</summary>
public AlertTypeEnum AlertType { get; set; }
/// <summary>告警级别(1信息/2警告/3紧急)</summary>
public AlertLevelEnum AlertLevel { get; set; }
/// <summary>当前值</summary>
public double? CurrentValue { get; set; }
/// <summary>阈值</summary>
public double? ThresholdValue { get; set; }
/// <summary>告警内容描述</summary>
public string? Content { get; set; }
/// <summary>通知模板编码(预留)</summary>
public string? TemplateCode { get; set; }
/// <summary>触发次数(合并窗口内累计)</summary>
public int TriggerCount { get; set; }
/// <summary>首次告警时间</summary>
public DateTime FirstAlertTime { get; set; }
/// <summary>最近告警时间</summary>
public DateTime LastAlertTime { get; set; }
/// <summary>告警状态(1未确认/2已确认/3已处理/4已关闭/5已忽略/6已转工单)</summary>
public AlertStatusEnum AlertStatus { get; set; }
/// <summary>确认人</summary>
public string? AckBy { get; set; }
/// <summary>确认时间</summary>
public DateTime? AckTime { get; set; }
/// <summary>处理人</summary>
public string? HandleBy { get; set; }
/// <summary>处理时间</summary>
public DateTime? HandleTime { get; set; }
/// <summary>处理备注</summary>
public string? HandleRemark { get; set; }
/// <summary>关联工单Id(预留二期,"0"表示未转工单)</summary>
public string? WorkOrderId { get; set; }
}
}
+136
View File
@@ -0,0 +1,136 @@
using Model.Entity.Inspection;
namespace Model.Dto.Inspection
{
/// <summary>
/// 通知渠道 DTOId 转 string;Secret 出口脱敏为是否已配置)
/// </summary>
public class AlertNotifyChannelDto
{
/// <summary>主键 Id</summary>
public string? Id { get; set; }
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>渠道类型(1飞书/2钉钉/3企微)</summary>
public NotifyChannelTypeEnum ChannelType { get; set; }
/// <summary>渠道名称</summary>
public string? Name { get; set; }
/// <summary>Webhook 地址</summary>
public string? WebhookUrl { get; set; }
/// <summary>加签密钥(入参可传;出口仅返回脱敏标记,不回传明文)</summary>
public string? Secret { get; set; }
/// <summary>是否已配置加签密钥(出口专用)</summary>
public bool HasSecret { get; set; }
/// <summary>推送模式(1直连/2Outbox跨网中转)</summary>
public NotifyPushModeEnum PushMode { get; set; }
/// <summary>是否启用</summary>
public bool IsEnabled { get; set; }
/// <summary>备注</summary>
public string? Remark { get; set; }
}
/// <summary>
/// 通知规则 DTO
/// </summary>
public class AlertNotifyRuleDto
{
/// <summary>主键 Id</summary>
public string? Id { get; set; }
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>规则名称</summary>
public string? RuleName { get; set; }
/// <summary>适用告警级别(1信息/2警告/3紧急)</summary>
public AlertLevelEnum AlertLevel { get; set; }
/// <summary>通知渠道Id</summary>
public string? ChannelId { get; set; }
/// <summary>接收人(@手机号/账号,逗号分隔)</summary>
public string? Receivers { get; set; }
/// <summary>是否通知当班值班人</summary>
public bool NotifyDutyPerson { get; set; }
/// <summary>静默期(分钟)</summary>
public int SilenceMinutes { get; set; }
/// <summary>是否启用</summary>
public bool IsEnabled { get; set; }
/// <summary>备注</summary>
public string? Remark { get; set; }
}
/// <summary>
/// 推送日志 DTO
/// </summary>
public class AlertNotifyLogDto
{
/// <summary>主键 Id</summary>
public string? Id { get; set; }
/// <summary>告警消息Id"0"表示渠道测试推送)</summary>
public string? AlertId { get; set; }
/// <summary>渠道类型</summary>
public NotifyChannelTypeEnum ChannelType { get; set; }
/// <summary>渠道Id</summary>
public string? ChannelId { get; set; }
/// <summary>推送目标(脱敏)</summary>
public string? Target { get; set; }
/// <summary>推送内容摘要</summary>
public string? Content { get; set; }
/// <summary>是否成功</summary>
public bool Success { get; set; }
/// <summary>错误信息</summary>
public string? ErrorMsg { get; set; }
/// <summary>重试次数</summary>
public int RetryCount { get; set; }
/// <summary>推送时间</summary>
public DateTime NotifyTime { get; set; }
}
/// <summary>
/// 值班排班 DTO
/// </summary>
public class DutyRosterDto
{
/// <summary>主键 Id</summary>
public string? Id { get; set; }
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>值班日期</summary>
public DateTime DutyDate { get; set; }
/// <summary>值班人姓名</summary>
public string? PersonName { get; set; }
/// <summary>联系电话</summary>
public string? Phone { get; set; }
/// <summary>备注</summary>
public string? Remark { get; set; }
}
}
+59
View File
@@ -0,0 +1,59 @@
namespace Model.Dto.Inspection
{
/// <summary>
/// 跨网发件箱拉取结果 DTO(AlertBridge 拉取到的待推送项;Payload 自包含全部推送要素)
/// </summary>
public class AlertOutboxItemDto
{
/// <summary>发件箱 Id(回写结果时使用)</summary>
public string? Id { get; set; }
/// <summary>告警消息Id</summary>
public string? AlertId { get; set; }
/// <summary>渠道Id</summary>
public string? ChannelId { get; set; }
/// <summary>完整推送报文 JSONAlertOutboxPayload 序列化)</summary>
public string? Payload { get; set; }
/// <summary>重试次数</summary>
public int RetryCount { get; set; }
}
/// <summary>
/// 发件箱推送报文(自包含:AlertBridge 无需访问数据库/配置即可推送)
/// </summary>
public class AlertOutboxPayload
{
/// <summary>渠道类型(1飞书/2钉钉/3企微)</summary>
public int ChannelType { get; set; }
/// <summary>Webhook 地址</summary>
public string? WebhookUrl { get; set; }
/// <summary>加签密钥(可空)</summary>
public string? Secret { get; set; }
/// <summary>消息标题</summary>
public string? Title { get; set; }
/// <summary>消息正文(markdown</summary>
public string? Content { get; set; }
/// <summary>@ 手机号列表</summary>
public List<string>? AtMobiles { get; set; }
}
/// <summary>
/// 发件箱结果回写 DTOAlertBridge 推送完成后回写)
/// </summary>
public class OutboxResultDto
{
/// <summary>是否成功</summary>
public bool Success { get; set; }
/// <summary>结果信息(失败原因)</summary>
public string? ResultMsg { get; set; }
}
}
+62
View File
@@ -0,0 +1,62 @@
using Model.Entity.Inspection;
namespace Model.Dto.Inspection
{
/// <summary>
/// 告警上报 DTO(其它模块调用告警中心的统一入参:IAlertCenterService.RaiseAsync / POST raise
/// 除设备标识外全部可空,降低调用方接入成本
/// </summary>
public class AlertRaiseDto
{
/// <summary>告警来源(调用方标识自身模块)</summary>
public AlertSourceEnum Source { get; set; } = AlertSourceEnum.Other;
/// <summary>关联告警规则Id(来源为规则引擎时传,可空/0 表示无)</summary>
public long RuleId { get; set; }
/// <summary>设备编号(建议必传,用于去重合并)</summary>
public string? DeviceCode { get; set; }
/// <summary>设备名称</summary>
public string? DeviceName { get; set; }
/// <summary>设备类型(如 TemperatureBox</summary>
public string? DeviceType { get; set; }
/// <summary>测点/监测指标(如 Temperature</summary>
public string? Metric { get; set; }
/// <summary>告警类型(默认阈值超限)</summary>
public AlertTypeEnum AlertType { get; set; } = AlertTypeEnum.ThresholdExceed;
/// <summary>告警级别(1信息/2警告/3紧急)</summary>
public AlertLevelEnum AlertLevel { get; set; } = AlertLevelEnum.Warning;
/// <summary>当前值</summary>
public double? CurrentValue { get; set; }
/// <summary>阈值</summary>
public double? ThresholdValue { get; set; }
/// <summary>告警内容描述(空则由告警中心按内置模板生成)</summary>
public string? Content { get; set; }
/// <summary>通知模板编码(预留对接通知模板模块)</summary>
public string? TemplateCode { get; set; }
}
/// <summary>
/// 告警操作 DTO(确认/处理/关闭/忽略/转工单 共用入参)
/// </summary>
public class AlertOperateDto
{
/// <summary>操作人</summary>
public string? Operator { get; set; }
/// <summary>备注(处理说明/忽略原因等)</summary>
public string? Remark { get; set; }
/// <summary>工单Id(仅转工单时使用,二期工单模块创建后回填,可空)</summary>
public long WorkOrderId { get; set; }
}
}
+56
View File
@@ -0,0 +1,56 @@
namespace Model.Dto.Inspection
{
/// <summary>
/// 告警规则 DTOId 由 long 改为 string,避免前端精度丢失)
/// </summary>
public class AlertRuleDto
{
/// <summary>主键 Idlong → string</summary>
public string Id { get; set; }
/// <summary>归属类型(0=全局;1=产品 2=设备)</summary>
public byte OwnerType { get; set; }
/// <summary>归属对象Id0 表示全局)</summary>
public long OwnerId { get; set; }
/// <summary>绑定点编码(空表示整产品/设备级规则)</summary>
public string PointCode { get; set; }
/// <summary>删除状态(0、未删除;1、已删除)</summary>
public byte IsDel { get; set; }
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>规则名称</summary>
public string RuleName { get; set; }
/// <summary>适用设备类型(空表示全部设备)</summary>
public string DeviceType { get; set; }
/// <summary>监测指标(Temperature / Humidity 等)</summary>
public string Metric { get; set; }
/// <summary>比较运算符(> >= < <= = ≠)</summary>
public string Operator { get; set; }
/// <summary>阈值</summary>
public double? Threshold { get; set; }
/// <summary>告警级别(信息/警告/紧急)</summary>
public string AlarmLevel { get; set; }
/// <summary>是否启用</summary>
public bool Enabled { get; set; }
/// <summary>触发次数</summary>
public int TriggerCount { get; set; }
/// <summary>最近触发时间</summary>
public DateTime? LastAlarmTime { get; set; }
/// <summary>备注</summary>
public string Remark { get; set; }
}
}
+101
View File
@@ -0,0 +1,101 @@
namespace Model.Dto.Inspection
{
/// <summary>
/// 告警趋势统计项(按日/周/月分桶)
/// </summary>
public class AlertTrendItemDto
{
/// <summary>时间桶(日:yyyy-MM-dd / 周:yyyy-Www / 月:yyyy-MM</summary>
public string Bucket { get; set; }
/// <summary>告警总数</summary>
public int Total { get; set; }
/// <summary>信息级数量</summary>
public int InfoCount { get; set; }
/// <summary>警告级数量</summary>
public int WarningCount { get; set; }
/// <summary>紧急级数量</summary>
public int UrgentCount { get; set; }
/// <summary>未闭环数量(未确认+已确认未处理)</summary>
public int UnhandledCount { get; set; }
}
/// <summary>
/// 告警分布统计项(按设备/类型/级别/来源维度)
/// </summary>
public class AlertDistributionItemDto
{
/// <summary>维度名称</summary>
public string Name { get; set; }
/// <summary>数量</summary>
public int Count { get; set; }
}
/// <summary>
/// 高频告警 TOP 排名项
/// </summary>
public class AlertTopItemDto
{
/// <summary>设备编号</summary>
public string? DeviceCode { get; set; }
/// <summary>设备名称</summary>
public string? DeviceName { get; set; }
/// <summary>告警次数(含合并累计 TriggerCount</summary>
public int Count { get; set; }
/// <summary>最近告警时间</summary>
public DateTime LastAlertTime { get; set; }
}
/// <summary>
/// 告警处理及时率统计
/// </summary>
public class AlertTimelinessDto
{
/// <summary>统计范围内告警总数</summary>
public int Total { get; set; }
/// <summary>已确认数(含已处理/已关闭)</summary>
public int AckCount { get; set; }
/// <summary>已处理/已关闭数</summary>
public int HandledCount { get; set; }
/// <summary>未闭环数</summary>
public int UnhandledCount { get; set; }
/// <summary>平均确认时长(分钟,无确认记录为 null</summary>
public double? AvgAckMinutes { get; set; }
/// <summary>平均处理时长(分钟,无处理记录为 null</summary>
public double? AvgHandleMinutes { get; set; }
/// <summary>及时率(%):30 分钟内确认的占比</summary>
public double TimelyRate { get; set; }
}
/// <summary>
/// 告警概览统计(列表页顶部卡片)
/// </summary>
public class AlertOverviewDto
{
/// <summary>未确认数量</summary>
public int UnacknowledgedCount { get; set; }
/// <summary>今日新增数量</summary>
public int TodayCount { get; set; }
/// <summary>今日紧急数量</summary>
public int TodayUrgentCount { get; set; }
/// <summary>近7日总数</summary>
public int WeekCount { get; set; }
}
}
@@ -0,0 +1,41 @@
namespace Model.Dto.Inspection
{
/// <summary>
/// 数据转发规则 DTOId 由 long 改为 string,避免前端精度丢失)
/// </summary>
public class DataForwardRuleDto
{
/// <summary>主键 Idlong → string</summary>
public string Id { get; set; }
/// <summary>删除状态(0、未删除;1、已删除)</summary>
public byte IsDel { get; set; }
/// <summary>创建时间</summary>
public DateTime? CreateTime { get; set; }
/// <summary>转发规则名称</summary>
public string ForwardName { get; set; }
/// <summary>源设备类型(空表示全部设备)</summary>
public string SourceDeviceType { get; set; }
/// <summary>转发间隔(秒)</summary>
public int ForwardInterval { get; set; }
/// <summary>通知方式(Email / Sms / Http</summary>
public string NotifyType { get; set; }
/// <summary>目标地址(邮箱 / 手机号 / 接口URL</summary>
public string TargetAddress { get; set; }
/// <summary>是否启用</summary>
public bool Enabled { get; set; }
/// <summary>最近转发时间</summary>
public DateTime? LastForwardTime { get; set; }
/// <summary>备注</summary>
public string Remark { 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; }
}
}
@@ -15,7 +15,7 @@ namespace Model.Entity.Asset
/// <summary>
/// 文件名称
/// </summary>
[SugarColumn(Length = 200)]
[SugarColumn(Length = 200, IsNullable = true)]
public string? FileName { get; set; }
/// <summary>
@@ -26,13 +26,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 文件扩展名(如 .pdf、.jpg
/// </summary>
[SugarColumn(Length = 20)]
[SugarColumn(Length = 20, IsNullable = true)]
public string? FileExt { get; set; }
/// <summary>
/// 文件存储地址(上传后存储的相对路径)
/// </summary>
[SugarColumn(Length = 200)]
[SugarColumn(Length = 200, IsNullable = true)]
public string? FileUrl { get; set; }
/// <summary>
@@ -43,13 +43,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 上传人
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? Uploader { get; set; }
/// <summary>
/// 备注
/// </summary>
[SugarColumn(Length = 500)]
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
@@ -15,13 +15,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 分类名称
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Name { get; set; }
/// <summary>
/// 分类编码
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? Code { get; set; }
/// <summary>
@@ -37,7 +37,7 @@ namespace Model.Entity.Asset
/// <summary>
/// 备注
/// </summary>
[SugarColumn(Length = 500)]
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
+47 -21
View File
@@ -1,4 +1,4 @@
using SqlSugar;
using SqlSugar;
namespace Model.Entity.Asset
{
@@ -11,13 +11,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 设备编号(唯一)
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? Code { get; set; }
/// <summary>
/// 设备名称
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Name { get; set; }
/// <summary>
@@ -28,55 +28,68 @@ namespace Model.Entity.Asset
/// <summary>
/// 设备类型(实验室/楼层/部门/位置/产品/自定义等分类维度下的类型名称,冗余便于展示)
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Type { get; set; }
/// <summary>
/// 设备品牌
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Brand { get; set; }
/// <summary>
/// 设备型号
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Model { get; set; }
/// <summary>
/// 规格参数
/// </summary>
[SugarColumn(Length = 200)]
[SugarColumn(Length = 200, IsNullable = true)]
public string? Specifications { get; set; }
/// <summary>
/// 制造商(设备制造商)
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Manufacturer { get; set; }
/// <summary>
/// 供应商(资产供应商)
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Supplier { get; set; }
/// <summary>
/// 设备图片地址(图片上传后存储的相对路径)
/// </summary>
[SugarColumn(Length = 200)]
[SugarColumn(Length = 200, IsNullable = true)]
public string? ImageUrl { get; set; }
/// <summary>
/// 二维码编号(一物一码)
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? QrCode { get; set; }
/// <summary>
/// 二维码扫码直达 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>
[SugarColumn(ColumnDataType = "text")]
[SugarColumn(ColumnDataType = "text", IsNullable = true)]
public string? Description { get; set; }
#endregion
@@ -84,31 +97,31 @@ namespace Model.Entity.Asset
/// <summary>
/// 使用部门名称(资产使用部门)
/// </summary>
[SugarColumn(Length = 100)]
[SugarColumn(Length = 100, IsNullable = true)]
public string? Department { get; set; }
/// <summary>
/// 存放位置(实验室/楼层/房间等位置信息)
/// </summary>
[SugarColumn(Length = 200)]
[SugarColumn(Length = 200, IsNullable = true)]
public string? Location { get; set; }
/// <summary>
/// 责任人(资产责任人)
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? ResponsiblePerson { get; set; }
/// <summary>
/// 责任人联系电话(资产责任人电话)
/// </summary>
[SugarColumn(Length = 30)]
[SugarColumn(Length = 30, IsNullable = true)]
public string? ContactPhone { get; set; }
/// <summary>
/// 保管人
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? Custodian { get; set; }
#endregion
@@ -116,17 +129,19 @@ namespace Model.Entity.Asset
/// <summary>
/// 购置日期
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? PurchaseDate { get; set; }
/// <summary>
/// 购置价格(资产购置价格)
/// </summary>
[SugarColumn(DecimalDigits = 2, Length = 18)]
[SugarColumn(DecimalDigits = 2, Length = 18, IsNullable = true)]
public decimal? PurchasePrice { get; set; }
/// <summary>
/// 保修到期日期(资产保修到期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? WarrantyExpireDate { get; set; }
#endregion
@@ -134,21 +149,25 @@ namespace Model.Entity.Asset
/// <summary>
/// 验收日期(资产验收日期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? AcceptanceDate { get; set; }
/// <summary>
/// 启用日期(资产启用日期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? EnableDate { get; set; }
/// <summary>
/// 使用年限(资产使用年限,单位:年)
/// </summary>
[SugarColumn(IsNullable = true)]
public int? ServiceLifeYears { get; set; }
/// <summary>
/// 报废日期(资产报废日期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? ScrapDate { get; set; }
#endregion
@@ -168,21 +187,25 @@ namespace Model.Entity.Asset
/// <summary>
/// 上次校准日期(校准日期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? LastCalibrationDate { get; set; }
/// <summary>
/// 下次校准到期日期(用于判断是否超期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? NextCalibrationDate { get; set; }
/// <summary>
/// 检定日期
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? InspectionDate { get; set; }
/// <summary>
/// 下次检定到期日期(用于判断是否超期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? NextInspectionDate { get; set; }
/// <summary>
@@ -193,11 +216,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 上次保养日期(保养日期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? LastMaintenanceDate { get; set; }
/// <summary>
/// 下次保养到期日期(保养周期到期,用于判断是否超期)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? NextMaintenanceDate { get; set; }
#endregion
@@ -205,24 +230,25 @@ namespace Model.Entity.Asset
/// <summary>
/// 备注
/// </summary>
[SugarColumn(Length = 500)]
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
/// <summary>
/// 创建人
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? CreateBy { get; set; }
/// <summary>
/// 更新人(修改人)
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? UpdateBy { get; set; }
/// <summary>
/// 更新时间(修改时间)
/// </summary>
[SugarColumn(IsNullable = true)]
public DateTime? UpdateTime { get; set; }
#endregion
}
@@ -30,13 +30,13 @@ namespace Model.Entity.Asset
/// <summary>
/// 操作人
/// </summary>
[SugarColumn(Length = 50)]
[SugarColumn(Length = 50, IsNullable = true)]
public string? Operator { get; set; }
/// <summary>
/// 变更原因/备注
/// </summary>
[SugarColumn(Length = 500)]
[SugarColumn(Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
+47
View File
@@ -0,0 +1,47 @@
using SqlSugar;
namespace Model.Entity.Config
{
/// <summary>
/// 设备日志(每台设备独享的日志记录)
/// 记录设备的通讯、连接、数据采集等运行日志,按设备查询
/// </summary>
public class DeviceLogEntity : BaseEntity
{
/// <summary>
/// 设备Id(关联 IotDeviceEntity.Id
/// </summary>
[SugarColumn(ColumnDescription = "设备Id")]
public long DeviceId { get; set; }
/// <summary>
/// 设备编号(冗余便于查询展示)
/// </summary>
[SugarColumn(ColumnDescription = "设备编号", Length = 64)]
public string? DeviceCode { get; set; }
/// <summary>
/// 日志级别(Info/Warn/Error
/// </summary>
[SugarColumn(ColumnDescription = "日志级别", Length = 16)]
public string? Level { get; set; } = "Info";
/// <summary>
/// 日志类型(连接/通讯/采集/指令/系统)
/// </summary>
[SugarColumn(ColumnDescription = "日志类型", Length = 32)]
public string? LogType { get; set; }
/// <summary>
/// 日志内容
/// </summary>
[SugarColumn(ColumnDescription = "日志内容", ColumnDataType = "text")]
public string? Message { get; set; }
/// <summary>
/// 记录时间(日志产生时间,用于时间线展示)
/// </summary>
[SugarColumn(ColumnDescription = "记录时间")]
public DateTime LogTime { get; set; }
}
}
+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; }
}
}
+103
View File
@@ -0,0 +1,103 @@
using SqlSugar;
namespace Model.Entity.Config
{
/// <summary>
/// IOT 设备(IOT设备注册与管理 - 设备管理)
/// 一台设备 = 一个从站实例,通过所属网关(IP:端口 或 串口)通讯
/// </summary>
public class IotDeviceEntity : BaseEntity
{
#region
/// <summary>
/// 设备编号(唯一)
/// </summary>
[SugarColumn(ColumnDescription = "设备编号", Length = 64)]
public string Code { get; set; }
/// <summary>
/// 设备名称
/// </summary>
[SugarColumn(ColumnDescription = "设备名称", Length = 100)]
public string Name { get; set; }
/// <summary>
/// 设备类型(型号,如 温度箱/充放电柜,对应 DeviceCommand 驱动类)
/// </summary>
[SugarColumn(ColumnDescription = "设备类型", Length = 64)]
public string DeviceType { get; set; }
/// <summary>
/// 所属产品Id(关联 ProductEntity.Id,仅用于归类,0=未分类)
/// </summary>
[SugarColumn(ColumnDescription = "所属产品Id", DefaultValue = "0")]
public long ProductId { get; set; }
/// <summary>
/// 所属网关Id(关联 GatewayEntity.Id0=未分配)
/// </summary>
[SugarColumn(ColumnDescription = "所属网关Id", DefaultValue = "0")]
public long GatewayId { get; set; }
/// <summary>
/// 从站地址(协议从站号,网关下区分设备)
/// </summary>
[SugarColumn(ColumnDescription = "从站地址")]
public byte SlaveId { get; set; }
/// <summary>
/// 制造商
/// </summary>
[SugarColumn(ColumnDescription = "制造商", Length = 100, IsNullable = true)]
public string? Manufacturer { get; set; }
/// <summary>
/// 设备描述
/// </summary>
[SugarColumn(ColumnDescription = "设备描述", Length = 500, IsNullable = true)]
public string? Description { get; set; }
#endregion
#region
/// <summary>
/// 通讯协议类型(ModbusTcp/ModbusRtu/S7/Tcp/Serial
/// </summary>
[SugarColumn(ColumnDescription = "通讯协议类型")]
public IotDeviceProtocolEnum ProtocolType { get; set; } = IotDeviceProtocolEnum.ModbusTcp;
/// <summary>
/// 是否启用采集(停用则采集调度跳过该设备)
/// </summary>
[SugarColumn(ColumnDescription = "是否启用")]
public bool IsEnabled { get; set; } = true;
#endregion
#region
/// <summary>
/// 在线状态
/// </summary>
[SugarColumn(ColumnDescription = "在线状态")]
public IotDeviceOnlineStatusEnum OnlineStatus { get; set; } = IotDeviceOnlineStatusEnum.Offline;
/// <summary>
/// 最后采集时间
/// </summary>
[SugarColumn(ColumnDescription = "最后采集时间", IsNullable = true)]
public DateTime? LastCollectTime { get; set; }
/// <summary>
/// 最近错误信息(通讯异常时记录)
/// </summary>
[SugarColumn(ColumnDescription = "最近错误", Length = 500, IsNullable = true)]
public string? LastError { get; set; }
#endregion
#region
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
public string? Remark { get; set; }
#endregion
}
}
+59
View File
@@ -0,0 +1,59 @@
namespace Model.Entity.Config
{
/// <summary>
/// 设备通讯协议类型(决定使用 DeviceCommand 中哪类驱动)
/// </summary>
public enum IotDeviceProtocolEnum
{
/// <summary>
/// Modbus TCP(网口,IP + 端口 + 从站地址)
/// </summary>
ModbusTcp = 1,
/// <summary>
/// Modbus RTU(串口,COM 口 + 波特率 + 从站地址)
/// </summary>
ModbusRtu = 2,
/// <summary>
/// 西门子 S7 协议
/// </summary>
S7 = 3,
/// <summary>
/// 自定义 TCP 协议
/// </summary>
Tcp = 4,
/// <summary>
/// 串口自定义协议
/// </summary>
Serial = 5
}
/// <summary>
/// 设备在线状态
/// </summary>
public enum IotDeviceOnlineStatusEnum
{
/// <summary>
/// 离线
/// </summary>
Offline = 0,
/// <summary>
/// 在线
/// </summary>
Online = 1,
/// <summary>
/// 连接中(网关已连,设备待确认)
/// </summary>
Connecting = 2,
/// <summary>
/// 通讯异常
/// </summary>
Fault = 3
}
}
@@ -0,0 +1,34 @@
using SqlSugar;
namespace Model.Entity.Config
{
/// <summary>
/// 产品分类(字典:维护产品分类,新增产品时可设置分类)
/// </summary>
public class ProductCategoryEntity : BaseEntity
{
/// <summary>
/// 分类编码
/// </summary>
[SugarColumn(ColumnDescription = "分类编码", Length = 50)]
public string Code { get; set; }
/// <summary>
/// 分类名称
/// </summary>
[SugarColumn(ColumnDescription = "分类名称", Length = 100, IsNullable = true)]
public string? Name { get; set; }
/// <summary>
/// 排序号
/// </summary>
[SugarColumn(ColumnDescription = "排序号")]
public int Sort { get; set; }
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
+64
View File
@@ -0,0 +1,64 @@
using SqlSugar;
namespace Model.Entity.Config
{
/// <summary>
/// 产品(按型号区分;产品给设备归类,产品的物模型/告警等配置与设备相互独立)
/// </summary>
public class ProductEntity : BaseEntity
{
/// <summary>
/// 型号(唯一,如 GT-100
/// </summary>
[SugarColumn(ColumnDescription = "型号", Length = 100)]
public string Model { get; set; }
/// <summary>
/// 产品名称
/// </summary>
[SugarColumn(ColumnDescription = "产品名称", Length = 100, IsNullable = true)]
public string? Name { get; set; }
/// <summary>
/// 产品分类Id(关联 ProductCategoryEntity.Id0 表示未分类)
/// </summary>
[SugarColumn(ColumnDescription = "产品分类Id")]
public long CategoryId { get; set; }
/// <summary>
/// 制造商
/// </summary>
[SugarColumn(ColumnDescription = "制造商", Length = 100, IsNullable = true)]
public string? Manufacturer { get; set; }
/// <summary>
/// 产品描述
/// </summary>
[SugarColumn(ColumnDescription = "产品描述", Length = 500, IsNullable = true)]
public string? Description { get; set; }
/// <summary>
/// 通讯协议(复用 IotDeviceProtocolEnum:决定 DeviceCommand 驱动分支)
/// </summary>
[SugarColumn(ColumnDescription = "通讯协议")]
public IotDeviceProtocolEnum ProtocolType { get; set; } = IotDeviceProtocolEnum.ModbusTcp;
/// <summary>
/// 消息模型(消息协议/报文格式描述,如 JSON / 透传)
/// </summary>
[SugarColumn(ColumnDescription = "消息模型", Length = 64, IsNullable = true)]
public string? MessageModel { get; set; }
/// <summary>
/// 是否启用
/// </summary>
[SugarColumn(ColumnDescription = "是否启用")]
public bool IsEnabled { get; set; } = true;
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
+91
View File
@@ -0,0 +1,91 @@
namespace Model.Entity.Config
{
/// <summary>
/// 物模型归属类型(物模型点表 OwnerType:点挂在哪一类对象下)
/// </summary>
public enum ThingOwnerTypeEnum
{
/// <summary>
/// 产品(型号)
/// </summary>
Product = 1,
/// <summary>
/// 设备(实例)
/// </summary>
Device = 2
}
/// <summary>
/// 点读写权限(决定该点是否可用于下发指令)
/// </summary>
public enum ThingRwEnum
{
/// <summary>
/// 只读
/// </summary>
ReadOnly = 1,
/// <summary>
/// 只写
/// </summary>
WriteOnly = 2,
/// <summary>
/// 读写
/// </summary>
ReadWrite = 3
}
/// <summary>
/// 寄存器类型
/// </summary>
public enum ThingRegisterTypeEnum
{
/// <summary>
/// 线圈(读/写 bool
/// </summary>
Coil = 1,
/// <summary>
/// 保持寄存器(读/写)
/// </summary>
HoldingRegister = 2,
/// <summary>
/// 输入寄存器(只读,如 UMC1300 的温度 PV)
/// </summary>
InputRegister = 3
}
/// <summary>
/// 数据类型
/// </summary>
public enum ThingDataTypeEnum
{
/// <summary>
/// 布尔
/// </summary>
Bool = 1,
/// <summary>
/// 16位有符号整数
/// </summary>
Int16 = 2,
/// <summary>
/// 16位无符号整数
/// </summary>
UInt16 = 3,
/// <summary>
/// 32位整数(占2寄存器,暂不支持下发)
/// </summary>
Int32 = 4,
/// <summary>
/// 32位浮点(占2寄存器,暂不支持下发)
/// </summary>
Float = 5
}
}
@@ -0,0 +1,95 @@
using SqlSugar;
namespace Model.Entity.Config
{
/// <summary>
/// 物模型点(属性/指令通用表;通过 OwnerType+OwnerId 归属产品或设备)
/// 一条点 = 一个寄存器/线圈映射;可写点即"下发指令"的入口
/// </summary>
public class ThingModelPointEntity : BaseEntity
{
/// <summary>
/// 归属类型(1=产品 2=设备)
/// </summary>
[SugarColumn(ColumnDescription = "归属类型")]
public ThingOwnerTypeEnum OwnerType { get; set; }
/// <summary>
/// 归属对象IdProductEntity.Id / IotDeviceEntity.Id
/// </summary>
[SugarColumn(ColumnDescription = "归属对象Id")]
public long OwnerId { get; set; }
/// <summary>
/// 点编码(同一归属内唯一,如 TempPV)
/// </summary>
[SugarColumn(ColumnDescription = "点编码", Length = 64)]
public string Code { get; set; }
/// <summary>
/// 点名称
/// </summary>
[SugarColumn(ColumnDescription = "点名称", Length = 100, IsNullable = true)]
public string? Name { get; set; }
/// <summary>
/// 寄存器类型(线圈/保持寄存器/输入寄存器)
/// </summary>
[SugarColumn(ColumnDescription = "寄存器类型")]
public ThingRegisterTypeEnum RegisterType { get; set; }
/// <summary>
/// 读写权限
/// </summary>
[SugarColumn(ColumnDescription = "读写权限")]
public ThingRwEnum Rw { get; set; }
/// <summary>
/// 数据类型
/// </summary>
[SugarColumn(ColumnDescription = "数据类型")]
public ThingDataTypeEnum DataType { get; set; }
/// <summary>
/// 寄存器起始地址
/// </summary>
[SugarColumn(ColumnDescription = "寄存器起始地址")]
public ushort Address { get; set; }
/// <summary>
/// 换算系数(工程值 = 寄存器原始值 / Scale + Offset,对应驱动内 SCALE
/// </summary>
[SugarColumn(ColumnDescription = "换算系数", DecimalDigits = 4)]
public double Scale { get; set; } = 1;
/// <summary>
/// 偏移量
/// </summary>
[SugarColumn(ColumnDescription = "偏移量", DecimalDigits = 4)]
public double Offset { get; set; }
/// <summary>
/// 单位
/// </summary>
[SugarColumn(ColumnDescription = "单位", Length = 16, IsNullable = true)]
public string? Unit { get; set; }
/// <summary>
/// 是否启用
/// </summary>
[SugarColumn(ColumnDescription = "是否启用")]
public bool Enabled { get; set; } = true;
/// <summary>
/// 排序号
/// </summary>
[SugarColumn(ColumnDescription = "排序号")]
public int Sort { get; set; }
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
public string? Remark { get; set; }
}
}
+221
View File
@@ -0,0 +1,221 @@
namespace Model.Entity.Inspection
{
/// <summary>
/// 告警级别(与规则引擎 AlertRuleEntity.AlarmLevel 字符串"信息/警告/紧急"保持一致)
/// </summary>
public enum AlertLevelEnum
{
/// <summary>
/// 信息
/// </summary>
Info = 1,
/// <summary>
/// 警告
/// </summary>
Warning = 2,
/// <summary>
/// 紧急
/// </summary>
Urgent = 3
}
/// <summary>
/// 告警状态(闭环流转:未确认 → 已确认 → 已处理 → 已关闭;可忽略/转工单)
/// </summary>
public enum AlertStatusEnum
{
/// <summary>
/// 未确认
/// </summary>
Unacknowledged = 1,
/// <summary>
/// 已确认
/// </summary>
Acknowledged = 2,
/// <summary>
/// 已处理
/// </summary>
Handled = 3,
/// <summary>
/// 已关闭
/// </summary>
Closed = 4,
/// <summary>
/// 已忽略
/// </summary>
Ignored = 5,
/// <summary>
/// 已转工单
/// </summary>
ToWorkOrder = 6
}
/// <summary>
/// 告警来源(标识上报模块,便于其它模块调用与统计)
/// </summary>
public enum AlertSourceEnum
{
/// <summary>
/// 规则引擎
/// </summary>
RuleEngine = 1,
/// <summary>
/// 自动巡检
/// </summary>
AutoPatrol = 2,
/// <summary>
/// 数据看板
/// </summary>
Dashboard = 3,
/// <summary>
/// 数据采集
/// </summary>
Collection = 4,
/// <summary>
/// 手动/API 上报
/// </summary>
Manual = 5,
/// <summary>
/// 其它
/// </summary>
Other = 99
}
/// <summary>
/// 告警类型
/// </summary>
public enum AlertTypeEnum
{
/// <summary>
/// 阈值超限
/// </summary>
ThresholdExceed = 1,
/// <summary>
/// 设备离线
/// </summary>
DeviceOffline = 2,
/// <summary>
/// 通讯故障
/// </summary>
CommFault = 3,
/// <summary>
/// 巡检异常
/// </summary>
PatrolAbnormal = 4,
/// <summary>
/// 自定义
/// </summary>
Custom = 99
}
/// <summary>
/// 通知渠道类型(一期仅支持飞书/钉钉/企微 Webhook 机器人)
/// </summary>
public enum NotifyChannelTypeEnum
{
/// <summary>
/// 飞书
/// </summary>
Feishu = 1,
/// <summary>
/// 钉钉
/// </summary>
DingTalk = 2,
/// <summary>
/// 企业微信
/// </summary>
WeCom = 3
}
/// <summary>
/// 渠道推送模式
/// </summary>
public enum NotifyPushModeEnum
{
/// <summary>
/// 直连推送(服务器可直接访问外网 Webhook)
/// </summary>
Direct = 1,
/// <summary>
/// Outbox 中转(跨网场景:内网写发件箱,跳板机 AlertBridge 拉取后在外网推送)
/// </summary>
Outbox = 2
}
/// <summary>
/// 跨网发件箱状态
/// </summary>
public enum OutboxStatusEnum
{
/// <summary>
/// 待推送
/// </summary>
Pending = 1,
/// <summary>
/// 已拉取(跳板机取走,等待回写结果)
/// </summary>
Pulled = 2,
/// <summary>
/// 推送成功
/// </summary>
Success = 3,
/// <summary>
/// 推送失败
/// </summary>
Failed = 4
}
/// <summary>
/// 告警级别枚举 ↔ 规则引擎字符串 映射工具
/// </summary>
public static class AlertLevelMap
{
/// <summary>
/// 字符串(信息/警告/紧急)→ 枚举,无法识别时默认 Info
/// </summary>
public static AlertLevelEnum FromString(string level)
{
return level?.Trim() switch
{
"警告" => AlertLevelEnum.Warning,
"紧急" => AlertLevelEnum.Urgent,
_ => AlertLevelEnum.Info
};
}
/// <summary>
/// 枚举 → 字符串(信息/警告/紧急)
/// </summary>
public static string ToLabel(AlertLevelEnum level)
{
return level switch
{
AlertLevelEnum.Warning => "警告",
AlertLevelEnum.Urgent => "紧急",
_ => "信息"
};
}
}
}
@@ -0,0 +1,151 @@
using SqlSugar;
using System;
namespace Model.Entity.Inspection
{
/// <summary>
/// 告警消息(消息告警模块核心表:所有模块的告警统一汇聚于此,支持确认/处理/关闭闭环)
/// </summary>
public class AlertMessageEntity : BaseEntity
{
#region
/// <summary>
/// 告警来源(规则引擎/自动巡检/看板/数采/手动等)
/// </summary>
[SugarColumn(ColumnDescription = "告警来源")]
public AlertSourceEnum Source { get; set; } = AlertSourceEnum.Manual;
/// <summary>
/// 关联告警规则Id(来源为规则引擎时填写,0表示无)
/// </summary>
[SugarColumn(ColumnDescription = "关联告警规则Id")]
public long RuleId { get; set; }
/// <summary>
/// 设备编号
/// </summary>
[SugarColumn(ColumnDescription = "设备编号", Length = 64, IsNullable = true)]
public string DeviceCode { get; set; }
/// <summary>
/// 设备名称
/// </summary>
[SugarColumn(ColumnDescription = "设备名称", Length = 128, IsNullable = true)]
public string DeviceName { get; set; }
/// <summary>
/// 设备类型(如 TemperatureBox,与规则引擎 DeviceType 对齐)
/// </summary>
[SugarColumn(ColumnDescription = "设备类型", Length = 64, IsNullable = true)]
public string DeviceType { get; set; }
/// <summary>
/// 测点/监测指标(如 Temperature、Humidity
/// </summary>
[SugarColumn(ColumnDescription = "测点指标", Length = 64, IsNullable = true)]
public string Metric { get; set; }
/// <summary>
/// 告警类型(超限/离线/通讯故障/巡检异常/自定义)
/// </summary>
[SugarColumn(ColumnDescription = "告警类型")]
public AlertTypeEnum AlertType { get; set; } = AlertTypeEnum.ThresholdExceed;
#endregion
#region
/// <summary>
/// 告警级别(信息/警告/紧急,与规则引擎三级一致)
/// </summary>
[SugarColumn(ColumnDescription = "告警级别")]
public AlertLevelEnum AlertLevel { get; set; } = AlertLevelEnum.Info;
/// <summary>
/// 当前值(触发告警时的实际值)
/// </summary>
[SugarColumn(ColumnDescription = "当前值", IsNullable = true)]
public double? CurrentValue { get; set; }
/// <summary>
/// 阈值(规则阈值,可空)
/// </summary>
[SugarColumn(ColumnDescription = "阈值", IsNullable = true)]
public double? ThresholdValue { get; set; }
/// <summary>
/// 告警内容描述
/// </summary>
[SugarColumn(ColumnDescription = "告警内容", Length = 500, IsNullable = true)]
public string Content { get; set; }
/// <summary>
/// 通知模板编码(预留:对接通知模板模块,空表示使用内置模板)
/// </summary>
[SugarColumn(ColumnDescription = "通知模板编码", Length = 64, IsNullable = true)]
public string TemplateCode { get; set; }
#endregion
#region
/// <summary>
/// 触发次数(合并窗口内同设备同测点同类型告警的累计次数)
/// </summary>
[SugarColumn(ColumnDescription = "触发次数")]
public int TriggerCount { get; set; } = 1;
/// <summary>
/// 首次告警时间
/// </summary>
[SugarColumn(ColumnDescription = "首次告警时间")]
public DateTime FirstAlertTime { get; set; }
/// <summary>
/// 最近告警时间
/// </summary>
[SugarColumn(ColumnDescription = "最近告警时间")]
public DateTime LastAlertTime { get; set; }
#endregion
#region
/// <summary>
/// 告警状态(未确认/已确认/已处理/已关闭/已忽略/已转工单)
/// </summary>
[SugarColumn(ColumnDescription = "告警状态")]
public AlertStatusEnum AlertStatus { get; set; } = AlertStatusEnum.Unacknowledged;
/// <summary>
/// 确认人
/// </summary>
[SugarColumn(ColumnDescription = "确认人", Length = 50, IsNullable = true)]
public string AckBy { get; set; }
/// <summary>
/// 确认时间
/// </summary>
[SugarColumn(ColumnDescription = "确认时间", IsNullable = true)]
public DateTime? AckTime { get; set; }
/// <summary>
/// 处理人
/// </summary>
[SugarColumn(ColumnDescription = "处理人", Length = 50, IsNullable = true)]
public string HandleBy { get; set; }
/// <summary>
/// 处理时间
/// </summary>
[SugarColumn(ColumnDescription = "处理时间", IsNullable = true)]
public DateTime? HandleTime { get; set; }
/// <summary>
/// 处理备注
/// </summary>
[SugarColumn(ColumnDescription = "处理备注", Length = 500, IsNullable = true)]
public string HandleRemark { get; set; }
/// <summary>
/// 关联工单Id(预留二期运维工单模块,0表示未转工单)
/// </summary>
[SugarColumn(ColumnDescription = "关联工单Id")]
public long WorkOrderId { get; set; }
#endregion
}
}
@@ -0,0 +1,52 @@
using SqlSugar;
namespace Model.Entity.Inspection
{
/// <summary>
/// 告警通知渠道(飞书/钉钉/企微 Webhook 机器人配置)
/// </summary>
public class AlertNotifyChannelEntity : BaseEntity
{
/// <summary>
/// 渠道类型(1飞书/2钉钉/3企微)
/// </summary>
[SugarColumn(ColumnDescription = "渠道类型")]
public NotifyChannelTypeEnum ChannelType { get; set; }
/// <summary>
/// 渠道名称(如:设备告警-研发飞书群)
/// </summary>
[SugarColumn(ColumnDescription = "渠道名称", Length = 100)]
public string Name { get; set; }
/// <summary>
/// Webhook 地址
/// </summary>
[SugarColumn(ColumnDescription = "Webhook地址", Length = 500)]
public string WebhookUrl { get; set; }
/// <summary>
/// 加签密钥(钉钉/飞书机器人开启加签时填写,企微机器人无此项)
/// </summary>
[SugarColumn(ColumnDescription = "加签密钥", Length = 200, IsNullable = true)]
public string Secret { get; set; }
/// <summary>
/// 推送模式(1直连推送/2Outbox跨网中转)
/// </summary>
[SugarColumn(ColumnDescription = "推送模式")]
public NotifyPushModeEnum PushMode { get; set; } = NotifyPushModeEnum.Direct;
/// <summary>
/// 是否启用
/// </summary>
[SugarColumn(ColumnDescription = "是否启用")]
public bool IsEnabled { get; set; } = true;
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
public string Remark { get; set; }
}
}
@@ -0,0 +1,65 @@
using SqlSugar;
using System;
namespace Model.Entity.Inspection
{
/// <summary>
/// 告警通知推送日志(每次向渠道推送的结果记录,失败可追溯)
/// </summary>
public class AlertNotifyLogEntity : BaseEntity
{
/// <summary>
/// 告警消息Id(关联 AlertMessageEntity.Id0表示渠道测试推送)
/// </summary>
[SugarColumn(ColumnDescription = "告警消息Id")]
public long AlertId { get; set; }
/// <summary>
/// 渠道类型(1飞书/2钉钉/3企微)
/// </summary>
[SugarColumn(ColumnDescription = "渠道类型")]
public NotifyChannelTypeEnum ChannelType { get; set; }
/// <summary>
/// 渠道Id(关联 AlertNotifyChannelEntity.Id
/// </summary>
[SugarColumn(ColumnDescription = "渠道Id")]
public long ChannelId { get; set; }
/// <summary>
/// 推送目标(Webhook 地址脱敏展示)
/// </summary>
[SugarColumn(ColumnDescription = "推送目标", Length = 200, IsNullable = true)]
public string Target { get; set; }
/// <summary>
/// 推送内容摘要
/// </summary>
[SugarColumn(ColumnDescription = "推送内容", Length = 1000, IsNullable = true)]
public string Content { get; set; }
/// <summary>
/// 是否成功
/// </summary>
[SugarColumn(ColumnDescription = "是否成功")]
public bool Success { get; set; }
/// <summary>
/// 错误信息
/// </summary>
[SugarColumn(ColumnDescription = "错误信息", Length = 500, IsNullable = true)]
public string ErrorMsg { get; set; }
/// <summary>
/// 重试次数
/// </summary>
[SugarColumn(ColumnDescription = "重试次数")]
public int RetryCount { get; set; }
/// <summary>
/// 推送时间
/// </summary>
[SugarColumn(ColumnDescription = "推送时间")]
public DateTime NotifyTime { get; set; }
}
}
@@ -0,0 +1,58 @@
using SqlSugar;
namespace Model.Entity.Inspection
{
/// <summary>
/// 告警通知规则(按告警级别配置通知渠道与接收人,支持值班排班关联与静默期)
/// </summary>
public class AlertNotifyRuleEntity : BaseEntity
{
/// <summary>
/// 规则名称
/// </summary>
[SugarColumn(ColumnDescription = "规则名称", Length = 100)]
public string RuleName { get; set; }
/// <summary>
/// 适用告警级别(信息/警告/紧急)
/// </summary>
[SugarColumn(ColumnDescription = "适用告警级别")]
public AlertLevelEnum AlertLevel { get; set; }
/// <summary>
/// 通知渠道Id(关联 AlertNotifyChannelEntity.Id
/// </summary>
[SugarColumn(ColumnDescription = "通知渠道Id")]
public long ChannelId { get; set; }
/// <summary>
/// 接收人(@手机号/企微账号,逗号分隔,可空表示不@)
/// </summary>
[SugarColumn(ColumnDescription = "接收人", Length = 500, IsNullable = true)]
public string Receivers { get; set; }
/// <summary>
/// 是否通知当班值班人(按告警时间查值班排班表)
/// </summary>
[SugarColumn(ColumnDescription = "是否通知值班人")]
public bool NotifyDutyPerson { get; set; }
/// <summary>
/// 静默期(分钟):同一告警合并窗口内重复触发时,静默期内不重复推送,0表示每次必推
/// </summary>
[SugarColumn(ColumnDescription = "静默期分钟")]
public int SilenceMinutes { get; set; } = 5;
/// <summary>
/// 是否启用
/// </summary>
[SugarColumn(ColumnDescription = "是否启用")]
public bool IsEnabled { get; set; } = true;
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
public string Remark { get; set; }
}
}
@@ -0,0 +1,61 @@
using SqlSugar;
using System;
namespace Model.Entity.Inspection
{
/// <summary>
/// 跨网推送发件箱(内网只写此表;跳板机上的 AlertBridge 控制台程序轮询拉取,
/// 在外网推送飞书/钉钉/企微后回写结果,实现工控内网与即时通讯软件的隔离穿透)
/// </summary>
public class AlertOutboxEntity : BaseEntity
{
/// <summary>
/// 告警消息Id(关联 AlertMessageEntity.Id
/// </summary>
[SugarColumn(ColumnDescription = "告警消息Id")]
public long AlertId { get; set; }
/// <summary>
/// 渠道Id(关联 AlertNotifyChannelEntity.Id
/// </summary>
[SugarColumn(ColumnDescription = "渠道Id")]
public long ChannelId { get; set; }
/// <summary>
/// 完整推送报文(自包含 JSON:渠道类型/WebhookUrl/Secret/标题/内容/@列表,
/// AlertBridge 拉取后无需再查任何配置即可直接推送)
/// </summary>
[SugarColumn(ColumnDescription = "推送报文", ColumnDataType = "text", IsNullable = true)]
public string Payload { get; set; }
/// <summary>
/// 状态(1待推送/2已拉取/3成功/4失败)
/// </summary>
[SugarColumn(ColumnDescription = "状态")]
public OutboxStatusEnum Status { get; set; } = OutboxStatusEnum.Pending;
/// <summary>
/// 重试次数(AlertBridge 推送失败回写时累加)
/// </summary>
[SugarColumn(ColumnDescription = "重试次数")]
public int RetryCount { get; set; }
/// <summary>
/// 拉取时间
/// </summary>
[SugarColumn(ColumnDescription = "拉取时间", IsNullable = true)]
public DateTime? PullTime { get; set; }
/// <summary>
/// 完成时间(成功/失败回写时间)
/// </summary>
[SugarColumn(ColumnDescription = "完成时间", IsNullable = true)]
public DateTime? FinishTime { get; set; }
/// <summary>
/// 结果信息(失败原因等)
/// </summary>
[SugarColumn(ColumnDescription = "结果信息", Length = 500, IsNullable = true)]
public string ResultMsg { get; set; }
}
}
@@ -0,0 +1,89 @@
using SqlSugar;
using System;
namespace Model.Entity.Inspection
{
/// <summary>
/// 告警规则(规则实例:查看所有设备告警的规则列表)
/// </summary>
public class AlertRuleEntity : BaseEntity
{
/// <summary>
/// 归属类型(0=全局/旧数据;1=产品 2=设备)
/// </summary>
[SugarColumn(ColumnDescription = "归属类型", DefaultValue = "0")]
public byte OwnerType { get; set; }
/// <summary>
/// 归属对象IdProductEntity.Id / IotDeviceEntity.Id0 表示全局)
/// </summary>
[SugarColumn(ColumnDescription = "归属对象Id", DefaultValue = "0")]
public long OwnerId { get; set; }
/// <summary>
/// 绑定点编码(关联物模型点 Code;为空表示整产品/设备级规则)
/// </summary>
[SugarColumn(ColumnDescription = "绑定点编码", Length = 64, IsNullable = true)]
public string? PointCode { get; set; }
/// <summary>
/// 规则名称
/// </summary>
[SugarColumn(ColumnDescription = "规则名称", Length = 100)]
public string RuleName { get; set; }
/// <summary>
/// 适用设备类型(空表示全部设备,如 TemperatureBox 温度箱)
/// </summary>
[SugarColumn(ColumnDescription = "适用设备类型", Length = 64, IsNullable = true)]
public string DeviceType { get; set; }
/// <summary>
/// 监测指标(Temperature 温度 / Humidity 湿度等)
/// </summary>
[SugarColumn(ColumnDescription = "监测指标", Length = 64)]
public string Metric { get; set; }
/// <summary>
/// 比较运算符(> >= < <= = ≠)
/// </summary>
[SugarColumn(ColumnDescription = "比较运算符", Length = 10)]
public string Operator { get; set; }
/// <summary>
/// 阈值
/// </summary>
[SugarColumn(ColumnDescription = "阈值", IsNullable = true)]
public double? Threshold { get; set; }
/// <summary>
/// 告警级别(信息/警告/紧急)
/// </summary>
[SugarColumn(ColumnDescription = "告警级别", Length = 32)]
public string AlarmLevel { get; set; }
/// <summary>
/// 是否启用
/// </summary>
[SugarColumn(ColumnDescription = "是否启用")]
public bool Enabled { get; set; } = true;
/// <summary>
/// 触发次数(系统统计)
/// </summary>
[SugarColumn(ColumnDescription = "触发次数")]
public int TriggerCount { get; set; }
/// <summary>
/// 最近触发时间(系统统计)
/// </summary>
[SugarColumn(ColumnDescription = "最近触发时间", IsNullable = true)]
public DateTime? LastAlarmTime { get; set; }
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
public string Remark { get; set; }
}
}
@@ -0,0 +1,59 @@
using SqlSugar;
using System;
namespace Model.Entity.Inspection
{
/// <summary>
/// 数据转发规则(配置数据转发规则,支持通过消息通知的方式定时转发数据)
/// </summary>
public class DataForwardRuleEntity : BaseEntity
{
/// <summary>
/// 转发规则名称
/// </summary>
[SugarColumn(ColumnDescription = "转发规则名称", Length = 100)]
public string ForwardName { get; set; }
/// <summary>
/// 源设备类型(空表示全部设备,如 TemperatureBox 温度箱)
/// </summary>
[SugarColumn(ColumnDescription = "源设备类型", Length = 64, IsNullable = true)]
public string SourceDeviceType { get; set; }
/// <summary>
/// 转发间隔(秒)
/// </summary>
[SugarColumn(ColumnDescription = "转发间隔(秒)")]
public int ForwardInterval { get; set; } = 60;
/// <summary>
/// 通知方式(Email 邮件 / Sms 短信 / Http 接口推送)
/// </summary>
[SugarColumn(ColumnDescription = "通知方式", Length = 32)]
public string NotifyType { get; set; }
/// <summary>
/// 目标地址(邮箱 / 手机号 / 接口URL)
/// </summary>
[SugarColumn(ColumnDescription = "目标地址", Length = 200)]
public string TargetAddress { get; set; }
/// <summary>
/// 是否启用
/// </summary>
[SugarColumn(ColumnDescription = "是否启用")]
public bool Enabled { get; set; } = true;
/// <summary>
/// 最近转发时间(系统统计)
/// </summary>
[SugarColumn(ColumnDescription = "最近转发时间", IsNullable = true)]
public DateTime? LastForwardTime { get; set; }
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
public string Remark { get; set; }
}
}
@@ -0,0 +1,35 @@
using SqlSugar;
using System;
namespace Model.Entity.Inspection
{
/// <summary>
/// 值班排班(按天排班;通知规则勾选"通知值班人"时按告警日期匹配当班人)
/// </summary>
public class DutyRosterEntity : BaseEntity
{
/// <summary>
/// 值班日期(当天 00:00:00
/// </summary>
[SugarColumn(ColumnDescription = "值班日期")]
public DateTime DutyDate { get; set; }
/// <summary>
/// 值班人姓名
/// </summary>
[SugarColumn(ColumnDescription = "值班人", Length = 50)]
public string PersonName { get; set; }
/// <summary>
/// 联系电话(用于钉钉/企微 @ 手机号)
/// </summary>
[SugarColumn(ColumnDescription = "联系电话", Length = 30, IsNullable = true)]
public string Phone { get; set; }
/// <summary>
/// 备注
/// </summary>
[SugarColumn(ColumnDescription = "备注", Length = 500, IsNullable = true)]
public string Remark { get; set; }
}
}
@@ -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; }
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,148 @@
using Model;
using Model.Entity.Asset;
using ORM;
using Service.Interface;
using SqlSugar;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace Service.Implement
{
/// <summary>
/// 设备附件服务实现(资产附件管理)
/// </summary>
public class EquipmentAttachmentService : IEquipmentAttachmentService
{
private readonly SqlSugarRepository<EquipmentAttachmentEntity> _repository;
public EquipmentAttachmentService(SqlSugarRepository<EquipmentAttachmentEntity> repository)
{
_repository = repository;
}
/// <summary>
/// 查询指定设备的附件列表(按上传时间倒序)
/// </summary>
public async Task<Result<List<EquipmentAttachmentEntity>>> GetByEquipmentAsync(long equipmentId)
{
try
{
var list = await _repository.Entities
.Where(x => x.EquipmentId == equipmentId && x.IsDel == 0)
.OrderBy(x => x.CreateTime, OrderByType.Desc)
.ToListAsync();
return Result<List<EquipmentAttachmentEntity>>.Success(list);
}
catch (Exception ex)
{
return Result<List<EquipmentAttachmentEntity>>.Error("查询附件列表失败", ex);
}
}
/// <summary>
/// 上传附件(保存文件到 uploads 目录并写入附件记录)
/// </summary>
public async Task<Result<EquipmentAttachmentEntity>> UploadAsync(long equipmentId, Stream stream, string originalFileName, long fileSize,
AttachmentTypeEnum fileType, string? uploader, string? remark, string webRootPath)
{
if (equipmentId <= 0)
{
return Result<EquipmentAttachmentEntity>.Error("设备 Id 无效");
}
if (stream == null || fileSize <= 0)
{
return Result<EquipmentAttachmentEntity>.Error("上传文件为空");
}
try
{
// 按设备分目录存储:uploads/equipment/{equipmentId}/
string relativeDir = Path.Combine("uploads", "equipment", equipmentId.ToString());
string physicalDir = Path.Combine(webRootPath, relativeDir);
if (!Directory.Exists(physicalDir))
{
Directory.CreateDirectory(physicalDir);
}
// 存储文件名:时间戳 + 原扩展名,避免重名与中文路径问题
string ext = Path.GetExtension(originalFileName);
string storedName = $"{DateTime.Now:yyyyMMddHHmmssfff}{ext}";
string physicalPath = Path.Combine(physicalDir, storedName);
using (var fileStream = new FileStream(physicalPath, FileMode.Create))
{
await stream.CopyToAsync(fileStream);
}
var entity = new EquipmentAttachmentEntity
{
EquipmentId = equipmentId,
FileName = originalFileName,
FileType = fileType,
FileExt = ext,
FileUrl = $"/{relativeDir.Replace('\\', '/')}/{storedName}",
FileSize = fileSize,
Uploader = uploader,
Remark = remark,
CreateTime = DateTime.Now
};
await _repository.Context.Insertable(entity).ExecuteCommandAsync();
return Result<EquipmentAttachmentEntity>.Success(entity);
}
catch (Exception ex)
{
return Result<EquipmentAttachmentEntity>.Error("上传附件失败", ex);
}
}
/// <summary>
/// 删除附件(软删除记录并删除物理文件)
/// </summary>
public async Task<Result<bool>> DeleteAsync(long id)
{
if (id <= 0)
{
return Result<bool>.Error("附件 Id 无效");
}
try
{
var attachment = await _repository.Entities
.Where(x => x.Id == id && x.IsDel == 0)
.FirstAsync();
if (attachment == null)
{
return Result<bool>.Error("附件不存在或已被删除");
}
var result = await _repository.Context.Updateable<EquipmentAttachmentEntity>()
.SetColumns(x => x.IsDel == 1)
.Where(x => x.Id == id)
.ExecuteCommandAsync();
// 删除物理文件(失败不影响记录删除结果)
try
{
string webRoot = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot");
string physicalPath = Path.Combine(webRoot, (attachment.FileUrl ?? string.Empty).TrimStart('/').Replace('/', Path.DirectorySeparatorChar));
if (File.Exists(physicalPath))
{
File.Delete(physicalPath);
}
}
catch
{
// 物理文件删除失败忽略
}
return Result<bool>.Success(result > 0);
}
catch (Exception ex)
{
return Result<bool>.Error("删除附件失败", ex);
}
}
}
}
+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 SqlSugar;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Service.Implement
{
/// <summary>
/// 二维码 服务实现
/// 设备二维码 服务实现
/// 扫码直达 URL 约定:{baseUrl}/asset/ledger/equipment?id={equipmentId}
/// baseUrl 由 Controller 从 HTTP 请求(scheme://host)传入;如需固定域名可改为读 sys_param 的 site_base_url
/// </summary>
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>";
}
}
}

Some files were not shown because too many files have changed in this diff Show More