Compare commits
33
Commits
048472579c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60fac00887 | ||
|
|
6aca0297bb | ||
|
|
3059befcf5 | ||
|
|
19fb191c3d | ||
|
|
bcd6484f45 | ||
|
|
3927b0c201 | ||
|
|
38cd47410c | ||
|
|
cfa8e78d33 | ||
|
|
6c20630a40 | ||
|
|
2ecd921d26 | ||
|
|
d9b079142b | ||
|
|
24d67baf65 | ||
|
|
2306fbd36b | ||
|
|
01f51fa3b4 | ||
|
|
a027bf5d9e | ||
|
|
9e78aa7769 | ||
|
|
6bd8ef4a7c | ||
|
|
d77feb67f7 | ||
|
|
3e3341dd19 | ||
|
|
3126c2e5ab | ||
|
|
03d46fb473 | ||
|
|
9b852e5c95 | ||
|
|
b9ea3f9d15 | ||
|
|
21addffdde | ||
|
|
fa2f9f64c5 | ||
|
|
1ff51cbc45 | ||
|
|
5d14afcb66 | ||
|
|
b0a7742b8f | ||
|
|
ec6825fc54 | ||
|
|
65661ef211 | ||
|
|
dd287f8b8b | ||
|
|
7f539a0316 | ||
|
|
edcbc2ebc4 |
+3
-1
@@ -364,4 +364,6 @@ MigrationBackup/
|
|||||||
.ionide/
|
.ionide/
|
||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
/IOT_API/appsettings.json
|
||||||
|
/IOT_API/wwwroot
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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,钉钉/企微返回 errcode,0 均为成功
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"ApiBaseUrl": "http://localhost:5287",
|
||||||
|
"PollSeconds": 5,
|
||||||
|
"BatchSize": 20
|
||||||
|
}
|
||||||
@@ -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 ID(cli_ 开头)</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -14,29 +15,72 @@ namespace DeviceCommand.Base
|
|||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
|
||||||
|
// ========= 静态注册表:用于 EnovaDataController 反向查找设备实例并分发数据 =========
|
||||||
|
private static readonly List<EnovaDataReporter> _instances = new List<EnovaDataReporter>();
|
||||||
|
private static readonly object _registryLock = new object();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前已注册的所有 EnovaDataReporter 实例(线程安全快照)
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<EnovaDataReporter> Instances
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_registryLock)
|
||||||
|
{
|
||||||
|
return _instances.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设备编码:用于在多设备场景下按 deviceCode 过滤分发
|
||||||
|
/// 留空表示该实例接收所有上报数据
|
||||||
|
/// </summary>
|
||||||
|
public string DeviceCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
// 显式实现/自动属性,方便外部随时更新配置
|
// 显式实现/自动属性,方便外部随时更新配置
|
||||||
public string TargetUrl { get; set; } = "http://127.0.0.1:8080/api/channel/state";
|
public string TargetUrl { get; set; } = "http://127.0.0.1:8080/api/channel/state";
|
||||||
public int TimeoutMilliseconds { get; set; } = 5000;
|
public int TimeoutMilliseconds { get; set; } = 5000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 构造函数注入 HttpClient(符合 Prism 依赖注入规范)
|
/// 收到下位机 POST 上报数据时触发
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public event EventHandler<EnovaChannelDataReceivedEventArgs>? ChannelDataReceived;
|
||||||
|
|
||||||
|
|
||||||
public EnovaDataReporter(HttpClient httpClient)
|
public EnovaDataReporter(HttpClient httpClient)
|
||||||
{
|
{
|
||||||
// 如果容器没有注入,则给个默认的单例/实例防空
|
|
||||||
_httpClient = httpClient ?? new HttpClient();
|
_httpClient = httpClient ?? new HttpClient();
|
||||||
|
|
||||||
|
// 自动注册到静态实例表,便于 Controller 反向找到本实例
|
||||||
|
lock (_registryLock)
|
||||||
|
{
|
||||||
|
_instances.Add(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<EnovaReportResponse> ReportChannelStateAsync(List<EnovaChannelReportData> dataList, CancellationToken ct = default)
|
/// <summary>
|
||||||
|
/// 从静态注册表中注销当前实例(设备销毁/释放时调用)
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Unregister()
|
||||||
|
{
|
||||||
|
lock (_registryLock)
|
||||||
|
{
|
||||||
|
_instances.Remove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResponse> ReportChannelStateAsync(List<EnovaChannelData> dataList, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (dataList == null || dataList.Count == 0)
|
if (dataList == null || dataList.Count == 0)
|
||||||
{
|
{
|
||||||
return new EnovaReportResponse { Success = false, ErrorInfo = "上报数据集合为空" };
|
return new ApiResponse { Success = false, ErrorInfo = "上报数据集合为空" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(TargetUrl))
|
if (string.IsNullOrWhiteSpace(TargetUrl))
|
||||||
{
|
{
|
||||||
return new EnovaReportResponse { Success = false, ErrorInfo = "目标上报 URL 未配置" };
|
return new ApiResponse { Success = false, ErrorInfo = "目标上报 URL 未配置" };
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -58,12 +102,12 @@ namespace DeviceCommand.Base
|
|||||||
if (response.IsSuccessStatusCode)
|
if (response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
string responseContent = await response.Content.ReadAsStringAsync();
|
string responseContent = await response.Content.ReadAsStringAsync();
|
||||||
var result = JsonConvert.DeserializeObject<EnovaReportResponse>(responseContent);
|
var result = JsonConvert.DeserializeObject<ApiResponse>(responseContent);
|
||||||
return result ?? new EnovaReportResponse { Success = true }; // 防止对方返回空Body [cite: 261]
|
return result ?? new ApiResponse { Success = true }; // 防止对方返回空Body
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return new EnovaReportResponse
|
return new ApiResponse
|
||||||
{
|
{
|
||||||
Success = false,
|
Success = false,
|
||||||
ErrorInfo = $"服务器响应错误代码: {(int)response.StatusCode} {response.ReasonPhrase}"
|
ErrorInfo = $"服务器响应错误代码: {(int)response.StatusCode} {response.ReasonPhrase}"
|
||||||
@@ -74,8 +118,98 @@ namespace DeviceCommand.Base
|
|||||||
{
|
{
|
||||||
// 完美承接你上位机原有的异常日志记录器逻辑
|
// 完美承接你上位机原有的异常日志记录器逻辑
|
||||||
// Logger.LoggerHelper.ErrorWithNotify($"Enova3 数据上传失败: {ex.Message}");
|
// Logger.LoggerHelper.ErrorWithNotify($"Enova3 数据上传失败: {ex.Message}");
|
||||||
return new EnovaReportResponse { Success = false, ErrorInfo = $"网络异常: {ex.Message}" };
|
return new ApiResponse { Success = false, ErrorInfo = $"网络异常: {ex.Message}" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理 Controller 转发过来的下位机上报数据,并触发事件
|
||||||
|
/// </summary>
|
||||||
|
public virtual ApiResponse HandleIncomingChannelData(List<EnovaChannelData> dataList)
|
||||||
|
{
|
||||||
|
if (dataList == null || dataList.Count == 0)
|
||||||
|
{
|
||||||
|
return new ApiResponse { Success = false, ErrorInfo = "接收到的数据为空" };
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 触发事件,让派生类(如 CTS3)或外部订阅者处理具体业务
|
||||||
|
ChannelDataReceived?.Invoke(this, new EnovaChannelDataReceivedEventArgs(dataList));
|
||||||
|
return new ApiResponse { Success = true, ErrorInfo = string.Empty };
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new ApiResponse { Success = false, ErrorInfo = $"处理上报数据时异常: {ex.Message}" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 由 Controller 调用:将下位机上报的数据广播到所有匹配的实例
|
||||||
|
/// 当数据中含 DeviceCode 时,按 DeviceCode 精确匹配;否则广播给所有实例
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse Dispatch(List<EnovaChannelData> dataList)
|
||||||
|
{
|
||||||
|
if (dataList == null || dataList.Count == 0)
|
||||||
|
{
|
||||||
|
return new ApiResponse { Success = false, ErrorInfo = "接收到的数据为空" };
|
||||||
|
}
|
||||||
|
|
||||||
|
EnovaDataReporter[] snapshot;
|
||||||
|
lock (_registryLock)
|
||||||
|
{
|
||||||
|
snapshot = _instances.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.Length == 0)
|
||||||
|
{
|
||||||
|
return new ApiResponse { Success = false, ErrorInfo = "无可用的设备实例接收数据" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 DeviceCode 分组分发:同一批数据可能来自多个 deviceCode
|
||||||
|
var groups = dataList
|
||||||
|
.GroupBy(d => d?.DeviceCode ?? string.Empty)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var errors = new List<string>();
|
||||||
|
int successCount = 0;
|
||||||
|
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
string deviceCode = group.Key;
|
||||||
|
var items = group.ToList();
|
||||||
|
|
||||||
|
// 选取目标:1) 设置了相同 DeviceCode 的实例;2) 没设置 DeviceCode 的实例(通用接收者)
|
||||||
|
var targets = snapshot
|
||||||
|
.Where(r => string.Equals(r.DeviceCode, deviceCode, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.IsNullOrEmpty(r.DeviceCode))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (targets.Count == 0)
|
||||||
|
{
|
||||||
|
errors.Add($"DeviceCode={deviceCode} 无匹配的设备实例");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var target in targets)
|
||||||
|
{
|
||||||
|
var resp = target.HandleIncomingChannelData(items);
|
||||||
|
if (resp != null && resp.Success)
|
||||||
|
{
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
errors.Add(resp?.ErrorInfo ?? "未知错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ApiResponse
|
||||||
|
{
|
||||||
|
Success = errors.Count == 0,
|
||||||
|
ErrorInfo = errors.Count == 0 ? string.Empty : string.Join(";", errors)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Model.Model;
|
using Model.Model;
|
||||||
@@ -6,12 +7,28 @@ using Model.Model;
|
|||||||
namespace DeviceCommand.Base
|
namespace DeviceCommand.Base
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enova3 上位机数据上报核心接口
|
/// Enova3 通道数据接收事件参数
|
||||||
|
/// </summary>
|
||||||
|
public class EnovaChannelDataReceivedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public List<EnovaChannelData> DataList { get; }
|
||||||
|
public DateTime ReceivedTime { get; }
|
||||||
|
|
||||||
|
public EnovaChannelDataReceivedEventArgs(List<EnovaChannelData> dataList)
|
||||||
|
{
|
||||||
|
DataList = dataList ?? new List<EnovaChannelData>();
|
||||||
|
ReceivedTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enova3 上位机数据上报 / 接收核心接口
|
||||||
|
/// 既支持上位机主动推送数据到客户平台,也支持接收下位机通过 HTTP POST 上报的数据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IEnovaDataReporter
|
public interface IEnovaDataReporter
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 客户平台接收数据的目标 HTTP URL
|
/// 客户平台接收数据的目标 HTTP URL(用于主动推送)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string TargetUrl { get; set; }
|
string TargetUrl { get; set; }
|
||||||
|
|
||||||
@@ -20,12 +37,25 @@ namespace DeviceCommand.Base
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
int TimeoutMilliseconds { get; set; }
|
int TimeoutMilliseconds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当 EnovaDataController 收到下位机 POST 上报的数据时触发
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<EnovaChannelDataReceivedEventArgs> ChannelDataReceived;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 异步推送通道的实时状态数据到客户平台
|
/// 异步推送通道的实时状态数据到客户平台
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="dataList">包含各通道状态的采集数据集合</param>
|
/// <param name="dataList">包含各通道状态的采集数据集合</param>
|
||||||
/// <param name="ct">取消令牌</param>
|
/// <param name="ct">取消令牌</param>
|
||||||
/// <returns>平台服务器的响应状态</returns>
|
/// <returns>平台服务器的响应状态</returns>
|
||||||
Task<EnovaReportResponse> ReportChannelStateAsync(List<EnovaChannelReportData> dataList, CancellationToken ct = default);
|
Task<ApiResponse> ReportChannelStateAsync(List<EnovaChannelData> dataList, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理 EnovaDataController 转发过来的下位机上报数据
|
||||||
|
/// 由 Controller 在收到 HTTP POST 后调用,内部会触发 <see cref="ChannelDataReceived"/> 事件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dataList">下位机上报的通道数据集合</param>
|
||||||
|
/// <returns>处理结果,将作为 HTTP 响应返回给下位机</returns>
|
||||||
|
ApiResponse HandleIncomingChannelData(List<EnovaChannelData> dataList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using NModbus;
|
using NModbus;
|
||||||
using System;
|
using System;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
@@ -38,20 +38,48 @@ namespace DeviceCommand.Base
|
|||||||
await _commLock.WaitAsync(ct);
|
await _commLock.WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 开始连接 - IP: {IPAddress}, 端口: {Port}");
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 超时设置 - 发送: {SendTimeout}ms, 接收: {ReceiveTimeout}ms");
|
||||||
|
|
||||||
if (_tcpClient.Connected)
|
if (_tcpClient.Connected)
|
||||||
{
|
{
|
||||||
var remoteEndPoint = (IPEndPoint)_tcpClient.Client.RemoteEndPoint!;
|
var remoteEndPoint = (IPEndPoint)_tcpClient.Client.RemoteEndPoint!;
|
||||||
if (remoteEndPoint.Address.MapToIPv4().ToString() == IPAddress && remoteEndPoint.Port == Port)
|
string currentIp = remoteEndPoint.Address.MapToIPv4().ToString();
|
||||||
|
int currentPort = remoteEndPoint.Port;
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 已有连接: {currentIp}:{currentPort}");
|
||||||
|
|
||||||
|
if (currentIp == IPAddress && currentPort == Port)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 参数匹配,复用现有连接");
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 参数不匹配,需要重新连接");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 关闭并释放旧连接");
|
||||||
_tcpClient.Close();
|
_tcpClient.Close();
|
||||||
_tcpClient.Dispose();
|
_tcpClient.Dispose();
|
||||||
_tcpClient = new TcpClient();
|
_tcpClient = new TcpClient();
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 调用 ConnectAsync({IPAddress}, {Port})");
|
||||||
await _tcpClient.ConnectAsync(IPAddress, Port, ct);
|
await _tcpClient.ConnectAsync(IPAddress, Port, ct);
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 创建ModbusMaster");
|
||||||
Modbus = new ModbusFactory().CreateMaster(_tcpClient);
|
Modbus = new ModbusFactory().CreateMaster(_tcpClient);
|
||||||
return true;
|
|
||||||
|
bool isConnected = _tcpClient.Connected;
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 连接结果: {(isConnected ? "成功" : "失败")}");
|
||||||
|
return isConnected;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 连接异常: {ex.Message}");
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 异常类型: {ex.GetType().Name}");
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 异常堆栈: {ex.StackTrace}");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using S7.Net;
|
using S7.Net;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@@ -9,7 +9,6 @@ namespace DeviceCommand.Base
|
|||||||
{
|
{
|
||||||
public class S7Device : IS7Device
|
public class S7Device : IS7Device
|
||||||
{
|
{
|
||||||
// 保持和你一致的连接参数命名属性
|
|
||||||
public string IPAddress { get; private set; } = "127.0.0.1";
|
public string IPAddress { get; private set; } = "127.0.0.1";
|
||||||
public CpuType CpuType { get; private set; } = CpuType.S71200;
|
public CpuType CpuType { get; private set; } = CpuType.S71200;
|
||||||
public short Rack { get; private set; } = 0;
|
public short Rack { get; private set; } = 0;
|
||||||
@@ -20,21 +19,15 @@ namespace DeviceCommand.Base
|
|||||||
private Plc _plc;
|
private Plc _plc;
|
||||||
public Plc PlcContext => _plc;
|
public Plc PlcContext => _plc;
|
||||||
|
|
||||||
// S7.Net 的 Plc.IsConnected 属性内部会通过 Socket 状态进行判断
|
|
||||||
public bool IsConnected => _plc?.IsConnected ?? false;
|
public bool IsConnected => _plc?.IsConnected ?? false;
|
||||||
|
|
||||||
// 统一线程锁
|
|
||||||
protected readonly SemaphoreSlim _commLock = new(1, 1);
|
protected readonly SemaphoreSlim _commLock = new(1, 1);
|
||||||
|
|
||||||
public S7Device()
|
public S7Device()
|
||||||
{
|
{
|
||||||
// 初始化默认配置
|
|
||||||
_plc = new Plc(CpuType, IPAddress, Rack, Slot);
|
_plc = new Plc(CpuType, IPAddress, Rack, Slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 设备参数配置(符合你的命名风格)
|
|
||||||
/// </summary>
|
|
||||||
public void ConfigureDevice(string ipAddress, CpuType cpuType, short rack = 0, short slot = 1, int sendTimeout = 3000, int receiveTimeout = 3000)
|
public void ConfigureDevice(string ipAddress, CpuType cpuType, short rack = 0, short slot = 1, int sendTimeout = 3000, int receiveTimeout = 3000)
|
||||||
{
|
{
|
||||||
IPAddress = ipAddress;
|
IPAddress = ipAddress;
|
||||||
@@ -50,32 +43,30 @@ namespace DeviceCommand.Base
|
|||||||
await _commLock.WaitAsync(ct);
|
await _commLock.WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 如果已经连接,检查当前的 IP 和 CPU 类型是否一致,一致则直接复用
|
|
||||||
if (_plc != null && _plc.IsConnected)
|
if (_plc != null && _plc.IsConnected)
|
||||||
{
|
{
|
||||||
if (_plc.IP == IPAddress && _plc.CPU == CpuType && _plc.Rack == Rack && _plc.Slot == Slot)
|
if (_plc.IP == IPAddress && _plc.CPU == CpuType && _plc.Rack == Rack && _plc.Slot == Slot)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修复:释放并彻底清空旧连接实例
|
|
||||||
if (_plc != null)
|
if (_plc != null)
|
||||||
{
|
{
|
||||||
_plc.Close();
|
_plc.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新实例化 Plc 对象并配置超时
|
|
||||||
_plc = new Plc(CpuType, IPAddress, Rack, Slot)
|
_plc = new Plc(CpuType, IPAddress, Rack, Slot)
|
||||||
{
|
{
|
||||||
ReadTimeout = ReceiveTimeout,
|
ReadTimeout = ReceiveTimeout,
|
||||||
WriteTimeout = SendTimeout
|
WriteTimeout = SendTimeout
|
||||||
};
|
};
|
||||||
|
|
||||||
// 部分版本 S7.Net 的 OpenAsync 本身不接受 CancellationToken,我们通过 WaitAsync 实现超时
|
await _plc.OpenAsync();
|
||||||
await _plc.OpenAsync().WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct);
|
|
||||||
return _plc.IsConnected;
|
return _plc.IsConnected;
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[S7Device] 连接异常: IP={IPAddress}, CPU={CpuType}, Error={ex.Message}");
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[S7Device] 异常堆栈: {ex.StackTrace}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -125,7 +116,22 @@ namespace DeviceCommand.Base
|
|||||||
public async Task<T> ReadAsync<T>(string address, CancellationToken ct = default)
|
public async Task<T> ReadAsync<T>(string address, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var result = await ReadAsync(address, ct);
|
var result = await ReadAsync(address, ct);
|
||||||
return (T)result;
|
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[S7Device.ReadAsync<T>] 地址={address}, 返回类型={result?.GetType().Name ?? "null"}, 值={result}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (result is IConvertible convertible)
|
||||||
|
{
|
||||||
|
return (T)Convert.ChangeType(convertible, typeof(T));
|
||||||
|
}
|
||||||
|
return (T)result;
|
||||||
|
}
|
||||||
|
catch (InvalidCastException ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[S7Device.ReadAsync<T>] 类型转换失败: 目标类型={typeof(T).Name}, 实际类型={result.GetType().Name}, 值={result}, 异常={ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken ct = default)
|
public async Task<byte[]> ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken ct = default)
|
||||||
|
|||||||
@@ -14,11 +14,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
<ProjectReference Include="..\Logger\Logger.csproj" />
|
||||||
<ProjectReference Include="..\Model\Model.csproj" />
|
<ProjectReference Include="..\Model\Model.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Devices\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
using DeviceCommand.Base;
|
||||||
|
using Model.Model;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DeviceCommand.Devices
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// CTS3-6-300-8IS0 设备:通过 EnovaDataController 接收下位机 POST 上报的通道数据
|
||||||
|
/// </summary>
|
||||||
|
public class CTS3 : EnovaDataReporter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 最近一次收到的通道数据快照(key = ChannelCode)
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<string, EnovaChannelData> LatestChannelData => _latestChannelData;
|
||||||
|
private readonly Dictionary<string, EnovaChannelData> _latestChannelData = new Dictionary<string, EnovaChannelData>();
|
||||||
|
private readonly object _dataLock = new object();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务侧可订阅此事件以获得更友好的回调(仅本设备数据)
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<EnovaChannelDataReceivedEventArgs>? OnDataUpdated;
|
||||||
|
|
||||||
|
public CTS3(HttpClient httpClient) : base(httpClient)
|
||||||
|
{
|
||||||
|
// 订阅基类事件:当 Controller 调用 HandleIncomingChannelData 时会触发
|
||||||
|
ChannelDataReceived += OnChannelDataReceivedInternal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 指定 DeviceCode 的便捷构造,便于多台 CTS3 共存时按 deviceCode 精确匹配
|
||||||
|
/// </summary>
|
||||||
|
public CTS3(HttpClient httpClient, string deviceCode) : this(httpClient)
|
||||||
|
{
|
||||||
|
DeviceCode = deviceCode ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnChannelDataReceivedInternal(object? sender, EnovaChannelDataReceivedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e?.DataList == null || e.DataList.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 缓存最近一次的通道快照
|
||||||
|
lock (_dataLock)
|
||||||
|
{
|
||||||
|
foreach (var data in e.DataList)
|
||||||
|
{
|
||||||
|
if (data == null) continue;
|
||||||
|
string key = data.ChannelCode ?? string.Empty;
|
||||||
|
_latestChannelData[key] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 业务异常拦截示例:通道错误状态
|
||||||
|
foreach (var data in e.DataList)
|
||||||
|
{
|
||||||
|
if (data == null) continue;
|
||||||
|
if (data.ChannelState == "错误" || data.ChannelState == "0x04")
|
||||||
|
{
|
||||||
|
// TODO: 触发本地报警逻辑
|
||||||
|
// Logger.LoggerHelper.WarnWithNotify($"通道 {data.ChannelCode} 故障");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 转发给业务订阅者
|
||||||
|
OnDataUpdated?.Invoke(this, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Unregister()
|
||||||
|
{
|
||||||
|
ChannelDataReceived -= OnChannelDataReceivedInternal;
|
||||||
|
base.Unregister();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
using DeviceCommand.Base;
|
||||||
|
using S7.Net;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DeviceCommand.Devices
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// THC-1100-602A 恒温恒湿试验箱驱动 (S7通讯协议)
|
||||||
|
/// 基于西门子PLC S7-1200/S7-1500系列
|
||||||
|
/// </summary>
|
||||||
|
public class THC1100 : S7Device
|
||||||
|
{
|
||||||
|
#region THC设备默认连接参数
|
||||||
|
private const string DefaultIpAddress = "192.168.1.3";
|
||||||
|
private const CpuType DefaultCpuType = CpuType.S71200;
|
||||||
|
private const short DefaultRack = 0;
|
||||||
|
private const short DefaultSlot = 1;
|
||||||
|
private const int DefaultSendTimeout = 3000;
|
||||||
|
private const int DefaultReceiveTimeout = 3000;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 寄存器地址定义
|
||||||
|
// ========== 主控界面相关 (DB53) ==========
|
||||||
|
private const string RealTimeTemperatureSetPointAddress = "DB53.DBD280";
|
||||||
|
private const string RealTimeHumidityMeasuredValueAddress = "DB53.DBD1092";
|
||||||
|
private const string RealTimeTemperatureMeasuredValueAddress = "DB53.DBD1052";
|
||||||
|
private const string OperationModeAddress = "DB53.DBW2";
|
||||||
|
private const string TestTypeAddress = "DB53.DBW4";
|
||||||
|
private const string CurrentStepAddress = "DB53.DBW6";
|
||||||
|
private const string TotalStepsAddress = "DB53.DBW8";
|
||||||
|
private const string LoopCountAddress = "DB53.DBW10";
|
||||||
|
private const string RunTimeAddress = "DB53.DBD12";
|
||||||
|
private const string StepTimeAddress = "DB53.DBD16";
|
||||||
|
private const string SystemStatusAddress = "DB53.DBW20";
|
||||||
|
|
||||||
|
// ========== 超温保护相关 (DB53) ==========
|
||||||
|
private const string OverTempHighLimitAddress = "DB53.DBD284";
|
||||||
|
private const string OverTempLowLimitAddress = "DB53.DBD288";
|
||||||
|
private const string OverTempEnableAddress = "DB53.DBX292.0";
|
||||||
|
|
||||||
|
// ========== 报警相关 (DB53) ==========
|
||||||
|
private const string AlarmStatusAddress = "DB53.DBW293";
|
||||||
|
private const string AlarmCodeAddress = "DB53.DBW295";
|
||||||
|
private const string AlarmCountAddress = "DB53.DBW297";
|
||||||
|
|
||||||
|
// ========== 控制指令 (DB53) ==========
|
||||||
|
private const string ControlCommandAddress = "DB53.DBW300";
|
||||||
|
|
||||||
|
// ========== 程序步骤参数 (DB54) ==========
|
||||||
|
private const string StepTemperatureBaseAddress = "DB54.DBD";
|
||||||
|
private const string StepTimeBaseAddress = "DB54.DBD";
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 运行模式枚举
|
||||||
|
public enum OperationMode
|
||||||
|
{
|
||||||
|
Stop = 0,
|
||||||
|
Running = 1,
|
||||||
|
Paused = 2
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public THC1100() : base()
|
||||||
|
{
|
||||||
|
// 使用THC设备的默认参数配置
|
||||||
|
ConfigureDevice(
|
||||||
|
ipAddress: DefaultIpAddress,
|
||||||
|
cpuType: DefaultCpuType,
|
||||||
|
rack: DefaultRack,
|
||||||
|
slot: DefaultSlot,
|
||||||
|
sendTimeout: DefaultSendTimeout,
|
||||||
|
receiveTimeout: DefaultReceiveTimeout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重写连接方法,确保使用正确的默认参数连接
|
||||||
|
/// </summary>
|
||||||
|
public override async Task<bool> ConnectAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(IPAddress))
|
||||||
|
{
|
||||||
|
ConfigureDevice(
|
||||||
|
ipAddress: DefaultIpAddress,
|
||||||
|
cpuType: DefaultCpuType,
|
||||||
|
rack: DefaultRack,
|
||||||
|
slot: DefaultSlot,
|
||||||
|
sendTimeout: DefaultSendTimeout,
|
||||||
|
receiveTimeout: DefaultReceiveTimeout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[THC1100] 连接参数: IP={IPAddress}, CPU={CpuType}, Rack={Rack}, Slot={Slot}");
|
||||||
|
|
||||||
|
bool result = await base.ConnectAsync(ct);
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[THC1100] 连接失败: IP={IPAddress}, CPU={CpuType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 基础测量方法
|
||||||
|
public async Task<float> GetRealTimeTemperatureSetPointAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] data = await ReadBytesAsync(DataType.DataBlock, 53, 280, 4, ct);
|
||||||
|
float value = ByteArrayToFloat(data);
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取温度设定值(DB53.DBD280): {value}");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取温度设定值失败: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<float> GetRealTimeHumidityMeasuredValueAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] data = await ReadBytesAsync(DataType.DataBlock, 53, 1092, 4, ct);
|
||||||
|
float value = ByteArrayToFloat(data);
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取湿度测量值(DB53.DBD1092): {value}");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取湿度测量值失败: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<float> GetRealTimeTemperatureMeasuredValueAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] data = await ReadBytesAsync(DataType.DataBlock, 53, 1052, 4, ct);
|
||||||
|
float value = ByteArrayToFloat(data);
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取温度测量值(DB53.DBD1052): {value}");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取温度测量值失败: {ex.Message}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float ByteArrayToFloat(byte[] data)
|
||||||
|
{
|
||||||
|
if (data == null || data.Length < 4)
|
||||||
|
throw new ArgumentException("数据长度不足");
|
||||||
|
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
{
|
||||||
|
byte[] reversed = (byte[])data.Clone();
|
||||||
|
Array.Reverse(reversed);
|
||||||
|
return BitConverter.ToSingle(reversed, 0);
|
||||||
|
}
|
||||||
|
return BitConverter.ToSingle(data, 0);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 运行控制方法
|
||||||
|
public async Task<OperationMode> GetOperationModeAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var value = await ReadAsync<ushort>(OperationModeAddress, ct);
|
||||||
|
return (OperationMode)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTestTypeAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<ushort>(TestTypeAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetTestTypeAsync(int testType, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (testType < 0 || testType > 65535)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(testType), "试验类型必须在0-65535范围内");
|
||||||
|
|
||||||
|
await WriteAsync(TestTypeAddress, (ushort)testType, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetCurrentStepAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<ushort>(CurrentStepAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetTotalStepsAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<ushort>(TotalStepsAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetTotalStepsAsync(int steps, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (steps < 1 || steps > 999)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(steps), "步数必须在1-999之间");
|
||||||
|
|
||||||
|
await WriteAsync(TotalStepsAddress, (ushort)steps, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetLoopCountAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<ushort>(LoopCountAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetLoopCountAsync(int count, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (count < 0 || count > 999)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(count), "循环次数必须在0-999之间");
|
||||||
|
|
||||||
|
await WriteAsync(LoopCountAddress, (ushort)count, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<float> GetRunTimeAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<float>(RunTimeAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<float> GetStepTimeAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<float>(StepTimeAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetSystemStatusAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<ushort>(SystemStatusAddress, ct);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 超温保护方法
|
||||||
|
public async Task<float> GetOverTempHighLimitAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<float>(OverTempHighLimitAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetOverTempHighLimitAsync(float temperature, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await WriteAsync(OverTempHighLimitAddress, temperature, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<float> GetOverTempLowLimitAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<float>(OverTempLowLimitAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetOverTempLowLimitAsync(float temperature, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await WriteAsync(OverTempLowLimitAddress, temperature, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> GetOverTempEnableAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<bool>(OverTempEnableAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetOverTempEnableAsync(bool enable, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await WriteAsync(OverTempEnableAddress, enable, ct);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 报警相关方法
|
||||||
|
public async Task<int> GetAlarmStatusAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<ushort>(AlarmStatusAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetAlarmCodeAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<ushort>(AlarmCodeAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> GetAlarmCountAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadAsync<ushort>(AlarmCountAddress, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ClearAlarmAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await WriteAsync(ControlCommandAddress, (ushort)0x08, ct);
|
||||||
|
await Task.Delay(100, ct);
|
||||||
|
await WriteAsync(ControlCommandAddress, (ushort)0x00, ct);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 控制指令方法
|
||||||
|
public async Task StartDeviceAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await WriteAsync(ControlCommandAddress, (ushort)0x01, ct);
|
||||||
|
await Task.Delay(100, ct);
|
||||||
|
await WriteAsync(ControlCommandAddress, (ushort)0x00, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopDeviceAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await WriteAsync(ControlCommandAddress, (ushort)0x02, ct);
|
||||||
|
await Task.Delay(100, ct);
|
||||||
|
await WriteAsync(ControlCommandAddress, (ushort)0x00, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ResetDeviceAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await WriteAsync(ControlCommandAddress, (ushort)0x04, ct);
|
||||||
|
await Task.Delay(100, ct);
|
||||||
|
await WriteAsync(ControlCommandAddress, (ushort)0x00, ct);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 程序步骤方法
|
||||||
|
public async Task SetStepTemperatureAsync(int step, float temperature, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (step < 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(step), "步骤编号必须大于0");
|
||||||
|
|
||||||
|
string address = $"{StepTemperatureBaseAddress}{(step - 1) * 12}";
|
||||||
|
await WriteAsync(address, temperature, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<float> GetStepTemperatureAsync(int step, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (step < 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(step), "步骤编号必须大于0");
|
||||||
|
|
||||||
|
string address = $"{StepTemperatureBaseAddress}{(step - 1) * 12}";
|
||||||
|
return await ReadAsync<float>(address, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetStepTimeAsync(int step, float duration, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (step < 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(step), "步骤编号必须大于0");
|
||||||
|
if (duration < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(duration), "时间不能为负数");
|
||||||
|
|
||||||
|
string address = $"{StepTimeBaseAddress}{(step - 1) * 12 + 8}";
|
||||||
|
await WriteAsync(address, duration, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<float> GetStepTimeAsync(int step, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (step < 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(step), "步骤编号必须大于0");
|
||||||
|
|
||||||
|
string address = $"{StepTimeBaseAddress}{(step - 1) * 12 + 8}";
|
||||||
|
return await ReadAsync<float>(address, ct);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 数据打包方法
|
||||||
|
public async Task<(float TemperatureSetPoint, float TemperatureMeasured, float HumidityMeasured)> GetThcDataPackAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var tempSet = await GetRealTimeTemperatureSetPointAsync(ct);
|
||||||
|
var tempMeas = await GetRealTimeTemperatureMeasuredValueAsync(ct);
|
||||||
|
var humidMeas = await GetRealTimeHumidityMeasuredValueAsync(ct);
|
||||||
|
return (tempSet, tempMeas, humidMeas);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(OperationMode Mode, int CurrentStep, int TotalSteps, int LoopCount, float RunTime)> GetRunStatusPackAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var mode = await GetOperationModeAsync(ct);
|
||||||
|
var currentStep = await GetCurrentStepAsync(ct);
|
||||||
|
var totalSteps = await GetTotalStepsAsync(ct);
|
||||||
|
var loopCount = await GetLoopCountAsync(ct);
|
||||||
|
var runTime = await GetRunTimeAsync(ct);
|
||||||
|
return (mode, currentStep, totalSteps, loopCount, runTime);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using DeviceCommand.Base;
|
||||||
|
using System;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DeviceCommand.Devices
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// UMC1000 温湿度控制器(RS485)
|
||||||
|
/// </summary>
|
||||||
|
public class UMC1000Rtu : ModbusRtu
|
||||||
|
{
|
||||||
|
private readonly byte _slaveId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 温度测量值 REAL
|
||||||
|
/// </summary>
|
||||||
|
private const ushort TemperatureAddress = 150;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 湿度测量值 REAL
|
||||||
|
/// </summary>
|
||||||
|
private const ushort HumidityAddress = 152;
|
||||||
|
|
||||||
|
public UMC1000Rtu(
|
||||||
|
byte slaveId,
|
||||||
|
string portName,
|
||||||
|
int baudRate = 9600,
|
||||||
|
int dataBits = 8,
|
||||||
|
StopBits stopBits = StopBits.One,
|
||||||
|
Parity parity = Parity.None,
|
||||||
|
int readTimeout = 3000,
|
||||||
|
int writeTimeout = 3000)
|
||||||
|
{
|
||||||
|
_slaveId = slaveId;
|
||||||
|
|
||||||
|
ConfigureDevice(
|
||||||
|
portName,
|
||||||
|
baudRate,
|
||||||
|
dataBits,
|
||||||
|
stopBits,
|
||||||
|
parity,
|
||||||
|
readTimeout,
|
||||||
|
writeTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取温度
|
||||||
|
/// </summary>
|
||||||
|
public async Task<float> ReadTemperatureAsync(
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var a = await ReadFloatAsync(
|
||||||
|
TemperatureAddress,
|
||||||
|
ct);
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取湿度
|
||||||
|
/// </summary>
|
||||||
|
public async Task<float> ReadHumidityAsync(
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await ReadFloatAsync(
|
||||||
|
HumidityAddress,
|
||||||
|
ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一次读取温湿度(减少通讯次数,提升轮询效率)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<(float Temperature, float Humidity)> ReadAllAsync(
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ushort[] regs = await ReadHoldingRegistersAsync(
|
||||||
|
_slaveId,
|
||||||
|
TemperatureAddress,
|
||||||
|
4,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
return
|
||||||
|
(
|
||||||
|
ToFloat(regs[0], regs[1]),
|
||||||
|
ToFloat(regs[2], regs[3])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<float> ReadFloatAsync(
|
||||||
|
ushort address,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
ushort[] regs = await ReadHoldingRegistersAsync(
|
||||||
|
_slaveId,
|
||||||
|
address,
|
||||||
|
2,
|
||||||
|
ct);
|
||||||
|
|
||||||
|
return ToFloat(regs[0], regs[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 两个寄存器完美转换为 Float 数值(已解决 00 64 改完解析变 0 的致命问题)
|
||||||
|
/// </summary>
|
||||||
|
private static float ToFloat(ushort reg1, ushort reg2)
|
||||||
|
{
|
||||||
|
// 核心诊断:从回包 01 03 08 [00 64 00 00] 来看,0x0064 正好是十进制 100。
|
||||||
|
// 这种情况在工业仪表中有 2 种常见可能,已为你做好了自适应处理:
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 可能性【一】:下位机名义上叫 REAL,实际上是 32位长整型(Int32 / CD AB 字节序)
|
||||||
|
// ==========================================
|
||||||
|
int intValue = (reg2 << 16) | reg1;
|
||||||
|
if (intValue == 100 || intValue > 0 && intValue < 1500)
|
||||||
|
{
|
||||||
|
// 如果仪表传 100 代表 10.0℃,可以在这里除以 10.0f
|
||||||
|
return (float)intValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 可能性【二】:下位机确实是标准 IEEE 754 浮点数,但受高低字错位影响
|
||||||
|
// ==========================================
|
||||||
|
Span<byte> bytes = stackalloc byte[4];
|
||||||
|
|
||||||
|
// 采用安全的绝对字节映射,不再依赖会引发未知异常的 Array.Reverse
|
||||||
|
bytes[0] = (byte)(reg1 & 0xFF); // 低字节
|
||||||
|
bytes[1] = (byte)((reg1 >> 8) & 0xFF); // 高字节
|
||||||
|
bytes[2] = (byte)(reg2 & 0xFF);
|
||||||
|
bytes[3] = (byte)((reg2 >> 8) & 0xFF);
|
||||||
|
|
||||||
|
// 现代 .NET 高性能内存强转(等同于 C++ 的 reinterpret_cast<float*>)
|
||||||
|
return MemoryMarshal.Read<float>(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using DeviceCommand.Base;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DeviceCommand.Devices
|
||||||
|
{
|
||||||
|
public class UMC1300 : ModbusTcp
|
||||||
|
{
|
||||||
|
// 从站地址(设备 ID)
|
||||||
|
private readonly byte _slaveId;
|
||||||
|
|
||||||
|
// 寄存器地址常量(根据设备手册定义)
|
||||||
|
private const ushort ADDR_TEMP_PV = 0x0000; // 温度测量值
|
||||||
|
private const ushort ADDR_HUMID_PV = 0x0001; // 湿度测量值
|
||||||
|
private const ushort ADDR_TEMP_MV = 0x0002; // 温度输出值
|
||||||
|
private const ushort ADDR_HUMID_MV = 0x0003; // 湿度输出值
|
||||||
|
|
||||||
|
// 转换系数:寄存器原始值 × SCALE = 工程值
|
||||||
|
private const float SCALE = 0.1f;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slaveId">Modbus 从站地址(1~247)</param>
|
||||||
|
/// <param name="ip">设备 IP 地址</param>
|
||||||
|
/// <param name="port">端口,默认 502</param>
|
||||||
|
/// <param name="sendTimeoutMs">发送超时(毫秒)</param>
|
||||||
|
/// <param name="receiveTimeoutMs">接收超时(毫秒)</param>
|
||||||
|
public UMC1300(byte slaveId, string ip, int port = 502,
|
||||||
|
int sendTimeoutMs = 3000, int receiveTimeoutMs = 3000)
|
||||||
|
: base()
|
||||||
|
{
|
||||||
|
_slaveId = slaveId;
|
||||||
|
ConfigureDevice(ip, port, sendTimeoutMs, receiveTimeoutMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<bool> ConnectAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (IsConnected)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return await base.ConnectAsync(ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 读取单个值 ====================
|
||||||
|
|
||||||
|
/// <summary>读取温度 PV(工程值)</summary>
|
||||||
|
public async Task<float> ReadTemperaturePVAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ushort[] raw = await ReadInputRegistersAsync(_slaveId, ADDR_TEMP_PV, 1, ct);
|
||||||
|
return raw[0] * SCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>读取湿度 PV(工程值)</summary>
|
||||||
|
public async Task<float> ReadHumidityPVAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ushort[] raw = await ReadInputRegistersAsync(_slaveId, ADDR_HUMID_PV, 1, ct);
|
||||||
|
return raw[0] * SCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>读取温度 MV(工程值)</summary>
|
||||||
|
public async Task<float> ReadTemperatureMVAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ushort[] raw = await ReadHoldingRegistersAsync(_slaveId, ADDR_TEMP_MV, 1, ct);
|
||||||
|
return raw[0] * SCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>读取湿度 MV(工程值)</summary>
|
||||||
|
public async Task<float> ReadHumidityMVAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
ushort[] raw = await ReadHoldingRegistersAsync(_slaveId, ADDR_HUMID_MV, 1, ct);
|
||||||
|
return raw[0] * SCALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 批量读取 ====================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一次性读取温度 PV、湿度 PV、温度 MV、湿度 MV(效率更高)
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>(tempPV, humidPV, tempMV, humidMV)</returns>
|
||||||
|
public async Task<(float tempPV, float humidPV, float tempMV, float humidMV)> ReadAllAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
// 从 0x0000 开始连续读取 4 个保持寄存器
|
||||||
|
ushort[] raw = await ReadHoldingRegistersAsync(_slaveId, ADDR_TEMP_PV, 4, ct);
|
||||||
|
return (
|
||||||
|
raw[0] * SCALE, // 温度 PV
|
||||||
|
raw[1] * SCALE, // 湿度 PV
|
||||||
|
raw[2] * SCALE, // 温度 MV
|
||||||
|
raw[3] * SCALE // 湿度 MV
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.14.36221.1
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Model", "Model\Model.csproj", "{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logger", "Logger\Logger.csproj", "{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ORM", "ORM\ORM.csproj", "{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service.csproj", "{D8209B91-D7D0-444B-B569-D3FA74D191DD}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}"
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Module", "Module", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoginModule", "LoginModule\LoginModule.csproj", "{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIShare", "UIShare\UIShare.csproj", "{F7A7D4FA-974C-470F-9543-0B256640BD81}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SettingModule", "SettingModule\SettingModule.csproj", "{2C3C2DBB-F782-416B-8571-07D3F533820B}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UpdateInfoModule", "UpdateInfoModule\UpdateInfoModule.csproj", "{B99017CA-BB14-426A-BBF0-C0C05C6510CA}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MainModule", "MainModule\MainModule.csproj", "{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LOT", "LOT\LOT.csproj", "{01E01684-DDE8-4B00-9BFC-2C5CDB2A261F}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceCommand", "DeviceCommand\DeviceCommand.csproj", "{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{D8209B91-D7D0-444B-B569-D3FA74D191DD}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{F7A7D4FA-974C-470F-9543-0B256640BD81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{F7A7D4FA-974C-470F-9543-0B256640BD81}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{F7A7D4FA-974C-470F-9543-0B256640BD81}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{F7A7D4FA-974C-470F-9543-0B256640BD81}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{2C3C2DBB-F782-416B-8571-07D3F533820B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{2C3C2DBB-F782-416B-8571-07D3F533820B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{2C3C2DBB-F782-416B-8571-07D3F533820B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{2C3C2DBB-F782-416B-8571-07D3F533820B}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{B99017CA-BB14-426A-BBF0-C0C05C6510CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B99017CA-BB14-426A-BBF0-C0C05C6510CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B99017CA-BB14-426A-BBF0-C0C05C6510CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B99017CA-BB14-426A-BBF0-C0C05C6510CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{01E01684-DDE8-4B00-9BFC-2C5CDB2A261F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{01E01684-DDE8-4B00-9BFC-2C5CDB2A261F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{01E01684-DDE8-4B00-9BFC-2C5CDB2A261F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{01E01684-DDE8-4B00-9BFC-2C5CDB2A261F}.Release|Any CPU.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}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(NestedProjects) = preSolution
|
|
||||||
{F79AC87E-7A5A-486F-BE6C-51E81CA569E4} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
|
||||||
{2C3C2DBB-F782-416B-8571-07D3F533820B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
|
||||||
{B99017CA-BB14-426A-BBF0-C0C05C6510CA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
|
||||||
{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {22BD9235-6581-454D-97D8-F4E932F80888}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.14.36221.1
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Model", "Model\Model.csproj", "{6D9764D9-B4DA-43E2-A9D7-40A6C871A6B3}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Logger", "Logger\Logger.csproj", "{9150C6A9-AE8D-42C9-8B2D-9DD04A3E7E74}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ORM", "ORM\ORM.csproj", "{4DE5DC6C-7121-4EB9-B8A8-90C694F451E2}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service.csproj", "{D8209B91-D7D0-444B-B569-D3FA74D191DD}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceCommand", "DeviceCommand\DeviceCommand.csproj", "{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}"
|
||||||
|
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
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {22BD9235-6581-454D-97D8-F4E932F80888}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 资产管理 - 设备台账控制器
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/asset/equipment")] //资产管理模块下的设备管理接口
|
||||||
|
public class EquipmentController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IEquipmentService _equipmentService;
|
||||||
|
private readonly IEquipmentAttachmentService _attachmentService;
|
||||||
|
private readonly IWebHostEnvironment _webHostEnvironment;
|
||||||
|
|
||||||
|
public EquipmentController(IEquipmentService equipmentService,
|
||||||
|
IEquipmentAttachmentService attachmentService,
|
||||||
|
IWebHostEnvironment webHostEnvironment)
|
||||||
|
{
|
||||||
|
_equipmentService = equipmentService;
|
||||||
|
_attachmentService = attachmentService;
|
||||||
|
_webHostEnvironment = webHostEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设备列表(分页查询,支持关键字/分类/状态筛选)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pageIndex">页码(从1开始,默认1)</param>
|
||||||
|
/// <param name="pageSize">每页数量(默认10)</param>
|
||||||
|
/// <param name="keyword">关键字(模糊匹配设备编号/名称)</param>
|
||||||
|
/// <param name="categoryId">设备分类Id(0表示不过滤)</param>
|
||||||
|
/// <param name="status">设备状态(不传表示不过滤)</param>
|
||||||
|
[HttpGet("list")]
|
||||||
|
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.IsSuccess
|
||||||
|
? Result<List<EquipmentDto>>.Success(result.Data.ToDtoList())
|
||||||
|
: Result<List<EquipmentDto>>.Error(result.Msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询全部设备(不分页,供下拉选择等场景使用)
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("all")]
|
||||||
|
public async Task<Result<List<EquipmentDto>>> GetAll()
|
||||||
|
{
|
||||||
|
var result = await _equipmentService.GetAllAsync();
|
||||||
|
return result.IsSuccess
|
||||||
|
? Result<List<EquipmentDto>>.Success(result.Data.ToDtoList())
|
||||||
|
: Result<List<EquipmentDto>>.Error(result.Msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设备详情
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">设备主键 Id</param>
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
public async Task<Result<EquipmentDto>> GetById(long 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="dto">设备 DTO(IsDel/CreateTime 等服务端字段不接受入参)</param>
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<Result<bool>> Add([FromBody] EquipmentDto dto)
|
||||||
|
{
|
||||||
|
return await _equipmentService.InsertAsync(dto.ToEntity());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 修改设备
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">设备主键 Id(以路由参数为准,忽略请求体中的 Id)</param>
|
||||||
|
/// <param name="dto">设备 DTO(IsDel/CreateTime 等服务端字段不接受入参)</param>
|
||||||
|
[HttpPut("{id}")]
|
||||||
|
public async Task<Result<bool>> Update(long id, [FromBody] EquipmentDto dto)
|
||||||
|
{
|
||||||
|
var entity = dto.ToEntity();
|
||||||
|
entity.Id = id;
|
||||||
|
return await _equipmentService.UpdateAsync(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除设备(软删除)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">设备主键 Id</param>
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
public async Task<Result<bool>> Delete(long id)
|
||||||
|
{
|
||||||
|
return await _equipmentService.DeleteEquipmentAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 变更设备状态(同步记录状态变更历史)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">设备主键 Id</param>
|
||||||
|
/// <param name="dto">状态变更请求(目标状态/操作人/备注)</param>
|
||||||
|
[HttpPost("{id}/status")]
|
||||||
|
public async Task<Result<bool>> ChangeStatus(long id, [FromBody] EquipmentStatusChangeDto dto)
|
||||||
|
{
|
||||||
|
return await _equipmentService.ChangeStatusAsync(id, dto.Status, dto.Operator, dto.Remark);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询设备状态变更历史记录
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">设备主键 Id</param>
|
||||||
|
[HttpGet("{id}/status-records")]
|
||||||
|
public async Task<Result<List<EquipmentStatusRecordDto>>> GetStatusRecords(long 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>
|
||||||
|
/// <param name="id">设备主键 Id</param>
|
||||||
|
[HttpGet("{id}/attachments")]
|
||||||
|
public async Task<Result<List<EquipmentAttachmentDto>>> GetAttachments(long id)
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
/// <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>
|
||||||
|
/// <param name="attachmentId">附件主键 Id</param>
|
||||||
|
[HttpDelete("attachments/{attachmentId}")]
|
||||||
|
public async Task<Result<bool>> DeleteAttachment(long attachmentId)
|
||||||
|
{
|
||||||
|
return await _attachmentService.DeleteAsync(attachmentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 证书管理
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/config/certificate")]
|
||||||
|
public class CertificateController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 证书管理 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集点位配置
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/config/collection-point")]
|
||||||
|
public class CollectionPointController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 采集点位配置 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集策略管理
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/config/collection-strategy")]
|
||||||
|
public class CollectionStrategyController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 采集策略管理 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据清洗与转换
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/config/data-cleaning")]
|
||||||
|
public class DataCleaningController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 数据清洗与转换 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备网关
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/config/device-gateway")]
|
||||||
|
public class DeviceGatewayController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 设备网关 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Model;
|
||||||
|
using Model.Dto.Config;
|
||||||
|
using Service.Interface.Config;
|
||||||
|
using SqlSugar;
|
||||||
|
using WebAPI.Filters;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers.Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 网关管理
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/config/gateway")]
|
||||||
|
[RequirePermission("device:view")]
|
||||||
|
public class GatewayController : ControllerBase
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// IOT设备管理
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/config/device")]
|
||||||
|
[RequirePermission("device:view")]
|
||||||
|
public class IotDeviceController : ControllerBase
|
||||||
|
{
|
||||||
|
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">所属产品Id(0 表示不过滤)</param>
|
||||||
|
/// <param name="gatewayId">所属网关Id(0 表示不过滤)</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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 网络组件与协议注册
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/config/network-component")]
|
||||||
|
public class NetworkComponentController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 网络组件与协议注册 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
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">分类Id(0 表示不过滤)</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,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 协议管理
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/config/protocol")]
|
||||||
|
public class ProtocolController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 协议管理 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 协议模板库
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/config/protocol-template")]
|
||||||
|
public class ProtocolTemplateController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 协议模板库 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 时序数据存储
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/config/time-series")]
|
||||||
|
public class TimeSeriesStorageController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 时序数据存储 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using DeviceCommand.Base;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Model.Model;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/enova")] // 路由可以自己定,定好后把完整的 URL 提供给上位机配置
|
||||||
|
public class EnovaDataController : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 接收下位机 POST 上报的通道数据,并联动分发到所有匹配的 EnovaDataReporter 实例(如 CTS3)
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("upload-status")]
|
||||||
|
public IActionResult ReceiveChannelStatus([FromBody] List<EnovaChannelData> rawDataList)
|
||||||
|
{
|
||||||
|
// 1. 基础校验
|
||||||
|
if (rawDataList == null || rawDataList.Count == 0)
|
||||||
|
{
|
||||||
|
return Ok(new ApiResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorInfo = "接收到的数据为空"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 2. 联动设备:将数据分发到所有已注册的 EnovaDataReporter 实例
|
||||||
|
// (包括 CTS3 等派生类,由它们自行通过事件处理)
|
||||||
|
ApiResponse dispatchResult = EnovaDataReporter.Dispatch(rawDataList);
|
||||||
|
|
||||||
|
// 3. 严格按照文档 Page 6 的格式返回响应
|
||||||
|
return Ok(dispatchResult);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 系统内部异常处理
|
||||||
|
return Ok(new ApiResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
ErrorInfo = $"服务器内部错误: {ex.Message}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
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>
|
||||||
|
/// 告警趋势统计(granularity:day/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>
|
||||||
|
/// 告警分布统计(dimension:device/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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
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,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据大屏
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/inspection/bigscreen")]
|
||||||
|
public class BigScreenController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 数据大屏 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Service.Interface;
|
||||||
|
using Model.Entity.Inspection;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备看板(温湿度)
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/inspection/dashboard")]
|
||||||
|
public class DeviceDashboardController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IDeviceDashboardService _dashboardService;
|
||||||
|
|
||||||
|
public DeviceDashboardController(IDeviceDashboardService dashboardService)
|
||||||
|
{
|
||||||
|
_dashboardService = dashboardService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有环境箱的最新温湿度数据(看板卡片,前端轮询调用)
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("realtime")]
|
||||||
|
public async Task<IActionResult> GetRealtime()
|
||||||
|
{
|
||||||
|
return Ok(await _dashboardService.GetRealtimeAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定环境箱最近 N 分钟的温湿度曲线数据
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deviceCode">设备标识,如 BOX-001</param>
|
||||||
|
/// <param name="minutes">最近多少分钟,默认 5,最大 60</param>
|
||||||
|
[HttpGet("curve")]
|
||||||
|
public async Task<IActionResult> GetCurve([FromQuery] string deviceCode, [FromQuery] int minutes = 5)
|
||||||
|
{
|
||||||
|
minutes = Math.Clamp(minutes, 1, 60);
|
||||||
|
return Ok(await _dashboardService.GetCurveAsync(deviceCode, minutes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备效率分析
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/inspection/efficiency")]
|
||||||
|
public class EfficiencyController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 设备效率分析 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 巡检管理
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/inspection/patrol")]
|
||||||
|
public class PatrolController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 巡检管理 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 访问日志
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/system/access-log")]
|
||||||
|
public class AccessLogController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 访问日志 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace WebAPI.Controllers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 系统日志
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/system/system-log")]
|
||||||
|
public class SystemLogController : ControllerBase
|
||||||
|
{
|
||||||
|
// TODO: 实现 系统日志 相关接口
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
private readonly ISystemParamService _paramService;
|
||||||
|
|
||||||
|
public SystemParamController(ISystemParamService paramService)
|
||||||
|
{
|
||||||
|
_paramService = paramService;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 管理侧 ====================
|
||||||
|
|
||||||
|
/// <summary>参数列表(分页,可按关键字和分组过滤)</summary>
|
||||||
|
[HttpGet("list")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> GetList(int pageIndex = 1, int pageSize = 10, string? keyword = null, string? group = null)
|
||||||
|
{
|
||||||
|
RefAsync<int> total = 0;
|
||||||
|
var result = await _paramService.GetListPagedAsync(pageIndex, pageSize, total, keyword, group);
|
||||||
|
Response.Headers["X-Total-Count"] = total.Value.ToString();
|
||||||
|
return result.IsSuccess
|
||||||
|
? Ok(Result<List<SystemParamDto>>.Success(result.Data))
|
||||||
|
: Ok(Result<List<SystemParamDto>>.Error(result.Msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>参数详情</summary>
|
||||||
|
[HttpGet("{id}")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> GetById(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.GetByIdAsync(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>新增参数</summary>
|
||||||
|
[HttpPost]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> Add([FromBody] SystemParamDto dto)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.AddAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>修改参数(内置参数仅允许改 Value/Remark)</summary>
|
||||||
|
[HttpPut]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> Update([FromBody] SystemParamDto dto)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.UpdateAsync(dto));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>删除参数(内置参数禁止删除)</summary>
|
||||||
|
[HttpDelete("{id}")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> Delete(long id)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.DeleteAsync(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>清空参数缓存(运维用)</summary>
|
||||||
|
[HttpPost("cache/reload")]
|
||||||
|
[RequirePermission("system:manage")]
|
||||||
|
public async Task<IActionResult> ReloadCache()
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.ReloadCacheAsync());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 业务侧(无权限,供其它模块按 Key 取值) ====================
|
||||||
|
|
||||||
|
/// <summary>按 Key 取单个值</summary>
|
||||||
|
[HttpGet("value/{key}")]
|
||||||
|
public async Task<IActionResult> GetValue(string key)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.GetValueAsync(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>按 Key 列表批量取值(query: ?keys=k1&k2&k3)</summary>
|
||||||
|
[HttpGet("values")]
|
||||||
|
public async Task<IActionResult> GetValues([FromQuery] string[] keys)
|
||||||
|
{
|
||||||
|
return Ok(await _paramService.GetValuesAsync(keys));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +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
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 业务服务依赖注入扩展
|
||||||
|
/// 约定:Service.Interface 中的 IXxxService 对应 Service.Implement 中的 XxxService
|
||||||
|
/// </summary>
|
||||||
|
public static class DependencyInjection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 自动注册所有业务服务(按命名约定:接口去掉前缀 I 即为实现类名)
|
||||||
|
/// 以后在 Service 项目里新增「接口 + 实现」时,无需改动这里,会自动注册
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
// SqlSugar 客户端:静态单例(SqlSugarScope 线程安全),供仓储及其他需要注入 ISqlSugarClient 的地方使用
|
||||||
|
services.AddSingleton<ISqlSugarClient>(SqlSugarContext.DbContext);
|
||||||
|
|
||||||
|
// 泛型仓储:所有 SqlSugarRepository<TEntity> 统一注册(内部使用 SqlSugarContext.DbContext 静态单例)
|
||||||
|
services.AddScoped(typeof(SqlSugarRepository<>));
|
||||||
|
|
||||||
|
// Service 项目程序集(接口与实现都在同一个程序集里)
|
||||||
|
Assembly assembly = typeof(BaseService<>).Assembly;
|
||||||
|
|
||||||
|
// 所有具体实现类(非抽象、非泛型定义)
|
||||||
|
var implTypes = assembly.GetTypes()
|
||||||
|
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericTypeDefinition)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// 所有接口
|
||||||
|
var ifaceTypes = assembly.GetTypes()
|
||||||
|
.Where(t => t.IsInterface && !t.IsGenericTypeDefinition)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var iface in ifaceTypes)
|
||||||
|
{
|
||||||
|
// 接口名去掉前缀 I,得到期望的实现类名
|
||||||
|
string implName = iface.Name.StartsWith("I") ? iface.Name.Substring(1) : iface.Name;
|
||||||
|
var impl = implTypes.FirstOrDefault(t => t.Name == implName);
|
||||||
|
if (impl != null && iface.IsAssignableFrom(impl))
|
||||||
|
{
|
||||||
|
services.AddScoped(iface, impl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 泛型基础服务(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>
|
||||||
|
/// 注册告警通知组件(飞书/钉钉/企微 Notifier,Typed 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
|
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
|
||||||
|
<ProjectReference Include="..\Service\Service.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Converters\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
namespace WebAPI
|
||||||
|
{
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
//dump文件崩溃记录(不用或者使用后记得去编译路径删除,.dump文件体积很大)
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
|
||||||
|
{
|
||||||
|
Exception ex = e.ExceptionObject as Exception;
|
||||||
|
string exeDir = AppContext.BaseDirectory;
|
||||||
|
string dumpDir = Path.Combine(exeDir, "Dump");
|
||||||
|
if (!Directory.Exists(dumpDir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dumpDir);
|
||||||
|
}
|
||||||
|
string dumpFileName = $"Error_{DateTime.Now:yyyy-MM-dd HH-mm-ss-fff}.dmp";
|
||||||
|
string dumpFullPath = Path.Combine(dumpDir, dumpFileName);
|
||||||
|
MiniDump.TryDump(dumpFullPath, MiniDump.Option.WithFullMemory, ex);
|
||||||
|
};
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
//注册postgre数据库
|
||||||
|
var db = builder.Configuration.GetSection("Database");
|
||||||
|
DatabaseConfig.InitPostgreSQL(db["Server"]!, int.Parse(db["Port"] ?? "5432"), db["Database"]!, db["User"]!, db["Password"]!);
|
||||||
|
if (int.TryParse(db["TenantId"], out var tenantId))
|
||||||
|
DatabaseConfig.SetTenant(tenantId);
|
||||||
|
DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
|
||||||
|
SqlSugarContext.InitDatabase();
|
||||||
|
|
||||||
|
// 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>();
|
||||||
|
|
||||||
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// Configure the HTTP request pipeline.
|
||||||
|
if (app.Environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭 Https 重定向:开启时会把 http 请求 307 到 https 端口,
|
||||||
|
// 导致前端 vite 代理被 CORS 拦截(Network Error);上位机/现场部署通常也只用 http
|
||||||
|
// app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// 静态文件:设备附件上传后通过 /uploads/... 访问(存储于 wwwroot)
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:18581",
|
||||||
|
"sslPort": 44392
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "http://localhost:5287",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"applicationUrl": "https://localhost:7240;http://localhost:5287",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"launchUrl": "swagger",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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_id,AlertBridge 无需改。
|
||||||
|
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) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Service.Interface;
|
||||||
|
|
||||||
|
namespace WebAPI.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 当前登录用户上下文实现:从 JWT Claims(HttpContext.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
using Model.Dto.Inspection;
|
||||||
|
using Model.Entity.Inspection;
|
||||||
|
using ORM;
|
||||||
|
using Service.Interface;
|
||||||
|
|
||||||
|
namespace WebAPI.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 温度箱数据模拟器(临时用)
|
||||||
|
/// 每秒为 100 台环境箱生成一条模拟温湿度数据并批量写入数据库,
|
||||||
|
/// 真实设备接入后删除此类及 Program.cs 中的注册即可
|
||||||
|
/// </summary>
|
||||||
|
public class TemperatureBoxSimulator : BackgroundService
|
||||||
|
{
|
||||||
|
private const int DeviceCount = 100;
|
||||||
|
private const double AlarmTemp = 80.0; // 高温报警阈值
|
||||||
|
private static readonly Random Rnd = new();
|
||||||
|
|
||||||
|
// 每台设备维护一份模拟状态(随机游走需要基于上一次的值)
|
||||||
|
private class BoxState
|
||||||
|
{
|
||||||
|
public double Temperature = Rnd.Next(20, 60);
|
||||||
|
public double Humidity = Rnd.Next(30, 70);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<string, BoxState> _states = new();
|
||||||
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
|
|
||||||
|
public TemperatureBoxSimulator(IServiceScopeFactory scopeFactory)
|
||||||
|
{
|
||||||
|
_scopeFactory = scopeFactory;
|
||||||
|
for (int i = 1; i <= DeviceCount; i++)
|
||||||
|
{
|
||||||
|
_states[$"BOX-{i:D3}"] = new BoxState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var batch = new List<TemperatureBoxDataEntity>(DeviceCount);
|
||||||
|
var alarmDevices = new List<(string Code, double Temp)>();
|
||||||
|
|
||||||
|
foreach (var kv in _states)
|
||||||
|
{
|
||||||
|
var s = kv.Value;
|
||||||
|
// 随机游走,让曲线平滑连续
|
||||||
|
s.Temperature = Math.Clamp(s.Temperature + (Rnd.NextDouble() - 0.48) * 2, 15, 95);
|
||||||
|
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,
|
||||||
|
DeviceType = "TemperatureBox",
|
||||||
|
DeviceTemperature = Math.Round(s.Temperature, 1),
|
||||||
|
DeviceHumidity = Math.Round(s.Humidity, 1),
|
||||||
|
Status = alarm ? "报警" : "运行",
|
||||||
|
AlarmInfo = alarm ? "温度超限报警" : "",
|
||||||
|
CreateTime = now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量插入
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[温度箱模拟器] 写入失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(1000, stoppingToken);
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@WebAPI_HostAddress = http://localhost:5287
|
||||||
|
|
||||||
|
GET {{WebAPI_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<prism:PrismApplication x:Class="LOT.App"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:local="clr-namespace:LOT"
|
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
|
||||||
xmlns:prism="http://prismlibrary.com/">
|
|
||||||
<Application.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<ResourceDictionary.MergedDictionaries>
|
|
||||||
<!--自定义style-->
|
|
||||||
<ResourceDictionary Source="/UIShare;component/Styles/CommonStyle.xaml"></ResourceDictionary>
|
|
||||||
</ResourceDictionary.MergedDictionaries>
|
|
||||||
</ResourceDictionary>
|
|
||||||
</Application.Resources>
|
|
||||||
</prism:PrismApplication>
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
using Castle.DynamicProxy;
|
|
||||||
using Common;
|
|
||||||
using LOT.ViewModels;
|
|
||||||
using LOT.ViewModels.Dialogs;
|
|
||||||
using LOT.Views;
|
|
||||||
using LOT.Views;
|
|
||||||
using LOT.Views.Dialogs;
|
|
||||||
using Logger;
|
|
||||||
using Notifications.Wpf.Core;
|
|
||||||
using ORM;
|
|
||||||
using Service.Implement;
|
|
||||||
using Service.Interface;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Data;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Windows;
|
|
||||||
using UIShare.PubEvent;
|
|
||||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
|
||||||
|
|
||||||
namespace LOT
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for App.xaml
|
|
||||||
/// </summary>
|
|
||||||
public partial class App : PrismApplication
|
|
||||||
{
|
|
||||||
protected override Window CreateShell()
|
|
||||||
{
|
|
||||||
//UI线程未捕获异常处理事件
|
|
||||||
this.DispatcherUnhandledException += OnDispatcherUnhandledException;
|
|
||||||
//Task线程内未捕获异常处理事件
|
|
||||||
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
|
||||||
////多线程异常
|
|
||||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
|
||||||
return Container.Resolve<ShellView>();
|
|
||||||
}
|
|
||||||
private void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
|
|
||||||
{
|
|
||||||
LoggerHelper.Error(e.Exception.Message,e.Exception.StackTrace);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
|
|
||||||
{
|
|
||||||
LoggerHelper.Error(e.Exception.Message, e.Exception.StackTrace);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
|
||||||
{
|
|
||||||
//记录dump文件
|
|
||||||
Exception ex = e.ExceptionObject as Exception;
|
|
||||||
MiniDump.TryDump($"dumps\\Error_{DateTime.Now:yyyy-MM-dd HH-mm-ss-ms}.dmp", MiniDump.Option.WithFullMemory, ex);
|
|
||||||
}
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
//初始化数据库
|
|
||||||
//DatabaseConfig.SetTenant(10001);
|
|
||||||
//DatabaseConfig.InitMySql("127.0.0.1",3306,"LOT","root","123456");
|
|
||||||
//DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
|
|
||||||
//SqlSugarContext.InitDatabase();
|
|
||||||
//显示登录窗口
|
|
||||||
var login=Container.Resolve<LoginModuleView>();
|
|
||||||
var re=Container.Resolve<IRegionManager>();
|
|
||||||
RegionManager.SetRegionManager(login, re);
|
|
||||||
RegionManager.SetRegionManager(Application.Current.MainWindow, re);
|
|
||||||
login.Show();
|
|
||||||
}
|
|
||||||
protected override void RegisterTypes(IContainerRegistry containerRegistry)
|
|
||||||
{
|
|
||||||
//注册弹窗
|
|
||||||
containerRegistry.RegisterDialog<MessageBoxView, MessageBoxViewModel>("MessageBox");
|
|
||||||
// 注册通知管理器
|
|
||||||
INotificationManager NotificationManager = new NotificationManager();
|
|
||||||
containerRegistry.RegisterInstance<INotificationManager>(NotificationManager);
|
|
||||||
}
|
|
||||||
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
|
|
||||||
protected override IModuleCatalog CreateModuleCatalog()
|
|
||||||
{
|
|
||||||
//指定模块加载方式为从文件夹中以反射发现并加载module(推荐用法)
|
|
||||||
return new DirectoryModuleCatalog() { ModulePath = @".\Modules" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System.Windows;
|
|
||||||
|
|
||||||
[assembly: ThemeInfo(
|
|
||||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// or application resource dictionaries)
|
|
||||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
|
||||||
//(used if a resource is not found in the page,
|
|
||||||
// app, or any theme specific resource dictionaries)
|
|
||||||
)]
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Noto\NotoSans-Bold.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Noto\NotoSans-BoldItalic.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Noto\NotoSans-Italic.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Noto\NotoSans-Regular.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-Black.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-BlackItalic.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-Bold.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-BoldItalic.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-Italic.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-Light.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-LightItalic.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-Medium.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-MediumItalic.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-Regular.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-Thin.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\Roboto-ThinItalic.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\RobotoCondensed-Bold.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\RobotoCondensed-BoldItalic.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\RobotoCondensed-Italic.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\RobotoCondensed-Light.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\RobotoCondensed-LightItalic.ttf" />
|
|
||||||
<Content Remove="C:\Users\23560\.nuget\packages\materialdesignthemes\5.3.0\contentFiles\any\net8.0-windows7.0\Resources\Roboto\RobotoCondensed-Regular.ttf" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
|
||||||
<PackageReference Include="Prism.Unity" Version="9.0.537" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Common\Common.csproj" />
|
|
||||||
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
|
|
||||||
<ProjectReference Include="..\Logger\Logger.csproj" />
|
|
||||||
<ProjectReference Include="..\LoginModule\LoginModule.csproj" />
|
|
||||||
<ProjectReference Include="..\MainModule\MainModule.csproj" />
|
|
||||||
<ProjectReference Include="..\Model\Model.csproj" />
|
|
||||||
<ProjectReference Include="..\ORM\ORM.csproj" />
|
|
||||||
<ProjectReference Include="..\Service\Service.csproj" />
|
|
||||||
<ProjectReference Include="..\SettingModule\SettingModule.csproj" />
|
|
||||||
<ProjectReference Include="..\UIShare\UIShare.csproj" />
|
|
||||||
<ProjectReference Include="..\UpdateInfoModule\UpdateInfoModule.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Update="Resources\Images\error.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Resources\Images\info.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Resources\Images\warning.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.1 KiB |
@@ -1,120 +0,0 @@
|
|||||||
using UIShare.PubEvent;
|
|
||||||
using UIShare.ViewModelBase;
|
|
||||||
using System.Windows.Input;
|
|
||||||
|
|
||||||
namespace LOT.ViewModels.Dialogs
|
|
||||||
{
|
|
||||||
public class MessageBoxViewModel : DialogViewModelBase
|
|
||||||
{
|
|
||||||
#region 属性
|
|
||||||
|
|
||||||
private string _Title;
|
|
||||||
public string Title
|
|
||||||
{
|
|
||||||
get => _Title;
|
|
||||||
set => SetProperty(ref _Title, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string _Message = "";
|
|
||||||
public string Message
|
|
||||||
{
|
|
||||||
get => _Message;
|
|
||||||
set => SetProperty(ref _Message, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string _Icon= $"pack://siteoforigin:,,,/Resources/Images/info.png";
|
|
||||||
public string Icon
|
|
||||||
{
|
|
||||||
get => _Icon;
|
|
||||||
set => SetProperty(ref _Icon, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _ShowYes;
|
|
||||||
public bool ShowYes
|
|
||||||
{
|
|
||||||
get => _ShowYes;
|
|
||||||
set => SetProperty(ref _ShowYes, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _ShowNo;
|
|
||||||
public bool ShowNo
|
|
||||||
{
|
|
||||||
get => _ShowNo;
|
|
||||||
set => SetProperty(ref _ShowNo, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _ShowOk;
|
|
||||||
public bool ShowOk
|
|
||||||
{
|
|
||||||
get => _ShowOk;
|
|
||||||
set => SetProperty(ref _ShowOk, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool _ShowCancel;
|
|
||||||
public bool ShowCancel
|
|
||||||
{
|
|
||||||
get => _ShowCancel;
|
|
||||||
set => SetProperty(ref _ShowCancel, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region 命令
|
|
||||||
public ICommand YesCommand { get; set; }
|
|
||||||
public ICommand NoCommand { get; set; }
|
|
||||||
public ICommand OkCommand { get; set; }
|
|
||||||
public ICommand CancelCommand { get; set; }
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
public DialogCloseListener RequestClose { get; set; }
|
|
||||||
|
|
||||||
public MessageBoxViewModel(IContainerProvider containerProvider):base(containerProvider)
|
|
||||||
{
|
|
||||||
YesCommand = new DelegateCommand(OnYes);
|
|
||||||
NoCommand = new DelegateCommand(OnNo);
|
|
||||||
OkCommand = new DelegateCommand(OnOk);
|
|
||||||
CancelCommand = new DelegateCommand(OnCancel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CloseDialog(ButtonResult result)
|
|
||||||
{
|
|
||||||
var parameters = new DialogParameters();
|
|
||||||
RequestClose.Invoke(new DialogResult(result));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnYes() => CloseDialog(ButtonResult.Yes);
|
|
||||||
private void OnNo() => CloseDialog(ButtonResult.No);
|
|
||||||
private void OnOk() => CloseDialog(ButtonResult.OK);
|
|
||||||
private void OnCancel() => CloseDialog(ButtonResult.Cancel);
|
|
||||||
|
|
||||||
#region Prism Dialog 规范
|
|
||||||
public bool CanCloseDialog() => true;
|
|
||||||
|
|
||||||
public override void OnDialogClosed()
|
|
||||||
{
|
|
||||||
_eventAggregator.GetEvent<OverlayEvent>().Publish(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void OnDialogOpened(IDialogParameters parameters)
|
|
||||||
{
|
|
||||||
_eventAggregator.GetEvent<OverlayEvent>().Publish(true);
|
|
||||||
Title = parameters.GetValue<string>("Title");
|
|
||||||
Message = parameters.GetValue<string>("Message");
|
|
||||||
var iconKey = parameters.GetValue<string>("Icon"); // info / error / warn
|
|
||||||
Icon = iconKey switch
|
|
||||||
{
|
|
||||||
"info" => $"pack://siteoforigin:,,,/Resources/Images/info.png",
|
|
||||||
"error" => $"pack://siteoforigin:,,,/Resources/Images/error.png",
|
|
||||||
"warn" => $"pack://siteoforigin:,,,/Resources/Images/warning.png",
|
|
||||||
_ => $"pack://siteoforigin:,,,/Resources/Images/info.png" // 默认
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
ShowYes = parameters.GetValue<bool>("ShowYes");
|
|
||||||
ShowNo = parameters.GetValue<bool>("ShowNo");
|
|
||||||
ShowOk = parameters.GetValue<bool>("ShowOk");
|
|
||||||
ShowCancel = parameters.GetValue<bool>("ShowCancel");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
using LOT.Views;
|
|
||||||
using MaterialDesignThemes.Wpf;
|
|
||||||
using Notifications.Wpf.Core;
|
|
||||||
using Prism.Events;
|
|
||||||
using Prism.Ioc;
|
|
||||||
using Prism.Modularity;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using UIShare.PubEvent;
|
|
||||||
|
|
||||||
namespace LOT.ViewModels
|
|
||||||
{
|
|
||||||
public class ShellViewModel : BindableBase
|
|
||||||
{
|
|
||||||
#region 属性
|
|
||||||
private bool _IsLeftDrawerOpen;
|
|
||||||
|
|
||||||
public bool IsLeftDrawerOpen
|
|
||||||
{
|
|
||||||
get => _IsLeftDrawerOpen;
|
|
||||||
set => SetProperty(ref _IsLeftDrawerOpen, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
#region 命令
|
|
||||||
public ICommand LeftDrawerOpenCommand { get; set; }
|
|
||||||
public ICommand MinimizeCommand { get; set; }
|
|
||||||
public ICommand MaximizeCommand { get; set; }
|
|
||||||
public ICommand CloseCommand { get; set; }
|
|
||||||
public ICommand NavigateCommand { get; set; }
|
|
||||||
public ICommand LoadCommand { get; set; }
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private IEventAggregator _eventAggregator;
|
|
||||||
private IRegionManager _regionManager;
|
|
||||||
private IContainerProvider _containerProvider;
|
|
||||||
private INotificationManager _notificationManager;
|
|
||||||
private IModuleManager _moduleManager;
|
|
||||||
public ShellViewModel( IContainerProvider containerProvider)
|
|
||||||
{
|
|
||||||
_containerProvider= containerProvider;
|
|
||||||
_eventAggregator = containerProvider.Resolve<IEventAggregator>();
|
|
||||||
_regionManager = containerProvider.Resolve<IRegionManager>();
|
|
||||||
_notificationManager = containerProvider.Resolve<INotificationManager>();
|
|
||||||
_moduleManager = containerProvider.Resolve<IModuleManager>();
|
|
||||||
LeftDrawerOpenCommand = new DelegateCommand(LeftDrawerOpen);
|
|
||||||
MinimizeCommand = new DelegateCommand<Window>(MinimizeWindow);
|
|
||||||
MaximizeCommand = new DelegateCommand<Window>(MaximizeWindow);
|
|
||||||
CloseCommand = new DelegateCommand<Window>(CloseWindow);
|
|
||||||
NavigateCommand = new DelegateCommand<string>(Navigate);
|
|
||||||
LoadCommand = new DelegateCommand(Load);
|
|
||||||
//订阅登录成功事件
|
|
||||||
_eventAggregator.GetEvent<LoginSuccessEvent>().Subscribe(() =>
|
|
||||||
{
|
|
||||||
Application.Current.MainWindow.Show();
|
|
||||||
_regionManager.RequestNavigate("ShellViewManager", "MainView");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#region 命令处理
|
|
||||||
private void Load()
|
|
||||||
{
|
|
||||||
_notificationManager.ShowAsync(new NotificationContent { Title = "登录成功", Message = "", Type = NotificationType.Success });
|
|
||||||
//默认导航到主界面
|
|
||||||
Type moduleAType = typeof(MainModule.MainModule);
|
|
||||||
_moduleManager.LoadModule(moduleAType.Name);
|
|
||||||
}
|
|
||||||
private void Navigate(string content)
|
|
||||||
{
|
|
||||||
switch (content)
|
|
||||||
{
|
|
||||||
case "主界面":
|
|
||||||
_regionManager.RequestNavigate("ShellViewManager", "MainView");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "设置界面":
|
|
||||||
_regionManager.RequestNavigate("ShellViewManager", "SettingView");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "更新界面":
|
|
||||||
_regionManager.RequestNavigate("ShellViewManager", "UpdateInfoView");
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LeftDrawerOpen()
|
|
||||||
{
|
|
||||||
IsLeftDrawerOpen = true;
|
|
||||||
}
|
|
||||||
private void MinimizeWindow(Window window)
|
|
||||||
{
|
|
||||||
if (window != null)
|
|
||||||
window.WindowState = WindowState.Minimized;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MaximizeWindow(Window window)
|
|
||||||
{
|
|
||||||
if (window != null)
|
|
||||||
{
|
|
||||||
window.WindowState = window.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CloseWindow(Window window)
|
|
||||||
{
|
|
||||||
window?.Close();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<UserControl x:Class="LOT.Views.Dialogs.MessageBoxView"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:local="clr-namespace:LOT.Views.Dialogs"
|
|
||||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
|
||||||
Background="Transparent"
|
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
|
||||||
Height="250"
|
|
||||||
Width="300">
|
|
||||||
<prism:Dialog.WindowStyle>
|
|
||||||
<Style BasedOn="{StaticResource DialogUserManageStyle}"
|
|
||||||
TargetType="Window" />
|
|
||||||
</prism:Dialog.WindowStyle>
|
|
||||||
<Border CornerRadius="20"
|
|
||||||
Background="white"
|
|
||||||
MouseLeftButtonDown="Border_MouseLeftButtonDown">
|
|
||||||
<Grid Background="Transparent">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<TextBlock Grid.Row="0"
|
|
||||||
Margin="15 5 5 5"
|
|
||||||
FontSize="18"
|
|
||||||
FontWeight="Bold"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Text="{Binding Title}"
|
|
||||||
Foreground="#333" />
|
|
||||||
|
|
||||||
<StackPanel HorizontalAlignment="Center"
|
|
||||||
Grid.Row="1">
|
|
||||||
<!-- Icon -->
|
|
||||||
<Image
|
|
||||||
Width="100"
|
|
||||||
Height="100"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Margin="5 15 0 0"
|
|
||||||
Source="{Binding Icon}" />
|
|
||||||
<!-- Message -->
|
|
||||||
<TextBlock
|
|
||||||
FontSize="20"
|
|
||||||
Margin="15 0 0 0"
|
|
||||||
TextAlignment="Center"
|
|
||||||
TextWrapping="Wrap"
|
|
||||||
Text="{Binding Message}" />
|
|
||||||
</StackPanel>
|
|
||||||
<!-- Buttons -->
|
|
||||||
<StackPanel Grid.Row="2"
|
|
||||||
|
|
||||||
Orientation="Horizontal"
|
|
||||||
HorizontalAlignment="Right">
|
|
||||||
|
|
||||||
<Button Content="Yes"
|
|
||||||
Width="80"
|
|
||||||
Margin="10 10"
|
|
||||||
Visibility="{Binding ShowYes, Converter={StaticResource BooleanToVisibilityConverter}}"
|
|
||||||
Command="{Binding YesCommand}" />
|
|
||||||
|
|
||||||
<Button Content="No"
|
|
||||||
Width="80"
|
|
||||||
Margin="10 10"
|
|
||||||
Visibility="{Binding ShowNo, Converter={StaticResource BooleanToVisibilityConverter}}"
|
|
||||||
Command="{Binding NoCommand}" />
|
|
||||||
|
|
||||||
<Button Content="OK"
|
|
||||||
Width="80"
|
|
||||||
Margin="10 10"
|
|
||||||
Visibility="{Binding ShowOk, Converter={StaticResource BooleanToVisibilityConverter}}"
|
|
||||||
Command="{Binding OkCommand}" />
|
|
||||||
|
|
||||||
<Button Content="Cancel"
|
|
||||||
Width="80"
|
|
||||||
Margin="10 10"
|
|
||||||
Visibility="{Binding ShowCancel, Converter={StaticResource BooleanToVisibilityConverter}}"
|
|
||||||
Command="{Binding CancelCommand}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace LOT.Views.Dialogs
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// MessageBoxView.xaml 的交互逻辑
|
|
||||||
/// </summary>
|
|
||||||
public partial class MessageBoxView : UserControl
|
|
||||||
{
|
|
||||||
public MessageBoxView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.LeftButton == MouseButtonState.Pressed)
|
|
||||||
{
|
|
||||||
Window.GetWindow(this)?.DragMove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<mah:MetroWindow xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
|
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
|
||||||
xmlns:helpers="clr-namespace:UIShare.Helpers;assembly=UIShare"
|
|
||||||
x:Class="LOT.Views.LoginModuleView"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
|
||||||
xmlns:local="clr-namespace:LOT.Views"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Title="LOT"
|
|
||||||
WindowStartupLocation="CenterScreen"
|
|
||||||
Height="315"
|
|
||||||
Width="420"
|
|
||||||
ResizeMode="NoResize">
|
|
||||||
<Grid>
|
|
||||||
<ContentControl prism:RegionManager.RegionName="LoginRegion" />
|
|
||||||
</Grid>
|
|
||||||
</mah:MetroWindow>
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
using UIShare.PubEvent;
|
|
||||||
using MahApps.Metro.Controls;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
using Path = System.IO.Path;
|
|
||||||
|
|
||||||
namespace LOT.Views
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Login.xaml 的交互逻辑
|
|
||||||
/// </summary>
|
|
||||||
public partial class LoginModuleView : MetroWindow
|
|
||||||
{
|
|
||||||
public LoginModuleView(IEventAggregator eventAggregator)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
//订阅登录成功事件
|
|
||||||
eventAggregator.GetEvent<LoginSuccessEvent>().Subscribe(() =>
|
|
||||||
{
|
|
||||||
this.Close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
<Window x:Class="LOT.Views.ShellView"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
|
||||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
|
||||||
WindowStartupLocation="CenterScreen"
|
|
||||||
Topmost="false"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
|
||||||
WindowStyle="None"
|
|
||||||
Title="ShellView"
|
|
||||||
d:DesignHeight="1080"
|
|
||||||
d:DesignWidth="1920">
|
|
||||||
<WindowChrome.WindowChrome>
|
|
||||||
<WindowChrome GlassFrameThickness="-1" />
|
|
||||||
</WindowChrome.WindowChrome>
|
|
||||||
<i:Interaction.Triggers>
|
|
||||||
<i:EventTrigger EventName="Loaded">
|
|
||||||
<i:InvokeCommandAction Command="{Binding LoadCommand}" />
|
|
||||||
</i:EventTrigger>
|
|
||||||
</i:Interaction.Triggers>
|
|
||||||
<materialDesign:DrawerHost x:Name="MainDrawerHost"
|
|
||||||
IsLeftDrawerOpen="{Binding IsLeftDrawerOpen, Mode=TwoWay}">
|
|
||||||
|
|
||||||
<!-- ✅ 左侧抽屉内容 -->
|
|
||||||
<materialDesign:DrawerHost.LeftDrawerContent>
|
|
||||||
<StackPanel Width="220"
|
|
||||||
Background="{DynamicResource MaterialDesignPaper}">
|
|
||||||
<TextBlock Text="导航菜单"
|
|
||||||
FontSize="18"
|
|
||||||
Margin="16"
|
|
||||||
Foreground="{DynamicResource PrimaryHueMidBrush}" />
|
|
||||||
<Separator Margin="0,0,0,8" />
|
|
||||||
<Button Content="主界面"
|
|
||||||
Command="{Binding NavigateCommand}"
|
|
||||||
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
|
|
||||||
Style="{StaticResource MaterialDesignFlatButton}"
|
|
||||||
Margin="8" />
|
|
||||||
<Button Content="设置界面"
|
|
||||||
Command="{Binding NavigateCommand}"
|
|
||||||
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
|
|
||||||
Style="{StaticResource MaterialDesignFlatButton}"
|
|
||||||
Margin="8" />
|
|
||||||
<Button Content="更新界面"
|
|
||||||
Command="{Binding NavigateCommand}"
|
|
||||||
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
|
|
||||||
Style="{StaticResource MaterialDesignFlatButton}"
|
|
||||||
Margin="8" />
|
|
||||||
</StackPanel>
|
|
||||||
</materialDesign:DrawerHost.LeftDrawerContent>
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="auto" />
|
|
||||||
<RowDefinition />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<!-- 顶部工具栏 -->
|
|
||||||
<materialDesign:ColorZone Mode="PrimaryMid"
|
|
||||||
MouseLeftButtonDown="ColorZone_MouseLeftButtonDown">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="auto" />
|
|
||||||
<ColumnDefinition />
|
|
||||||
<ColumnDefinition Width="auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Menu Grid.Column="0"
|
|
||||||
Background="Transparent"
|
|
||||||
Foreground="White"
|
|
||||||
VerticalAlignment="Center">
|
|
||||||
<!-- 文件菜单 -->
|
|
||||||
<MenuItem FontSize="13"
|
|
||||||
Height="50"
|
|
||||||
Header="菜单"
|
|
||||||
Foreground="White"
|
|
||||||
Command="{Binding DataContext.LeftDrawerOpenCommand, RelativeSource={RelativeSource AncestorType=Window}}">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<materialDesign:PackIcon Kind="Menu"
|
|
||||||
Foreground="White" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- 工具菜单 -->
|
|
||||||
<MenuItem Header="工具"
|
|
||||||
FontSize="13"
|
|
||||||
Height="50"
|
|
||||||
Foreground="White">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<materialDesign:PackIcon Kind="Tools"
|
|
||||||
Foreground="White" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
<MenuItem Header="注销登录"
|
|
||||||
|
|
||||||
Foreground="Black" />
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
<Menu Grid.Column="2"
|
|
||||||
Margin="0 0 20 0">
|
|
||||||
<MenuItem FontSize="13"
|
|
||||||
Height="50"
|
|
||||||
Header="最小化"
|
|
||||||
Foreground="White"
|
|
||||||
Command="{Binding MinimizeCommand}"
|
|
||||||
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<materialDesign:PackIcon Kind="Minimize"
|
|
||||||
Foreground="White" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem FontSize="13"
|
|
||||||
Height="50"
|
|
||||||
Header="最大化"
|
|
||||||
Foreground="White"
|
|
||||||
Command="{Binding MaximizeCommand}"
|
|
||||||
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<materialDesign:PackIcon Kind="Maximize"
|
|
||||||
Foreground="White" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem FontSize="13"
|
|
||||||
Height="50"
|
|
||||||
Header="关闭"
|
|
||||||
Foreground="White"
|
|
||||||
Command="{Binding CloseCommand}"
|
|
||||||
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<materialDesign:PackIcon Kind="Close"
|
|
||||||
Foreground="White" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
<!-- 左侧菜单 -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</materialDesign:ColorZone>
|
|
||||||
|
|
||||||
<materialDesign:DialogHost Grid.Row="1"
|
|
||||||
x:Name="DialogHost"
|
|
||||||
DialogBackground="Transparent"
|
|
||||||
Background="Transparent"
|
|
||||||
Identifier="Root">
|
|
||||||
<!-- 主内容区 -->
|
|
||||||
<Grid>
|
|
||||||
<ContentControl prism:RegionManager.RegionName="ShellViewManager" />
|
|
||||||
<Border x:Name="Overlay"
|
|
||||||
Background="#40000000"
|
|
||||||
Visibility="Collapsed"
|
|
||||||
Panel.ZIndex="1">
|
|
||||||
<StackPanel Width="150"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="0 0 0 100">
|
|
||||||
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
<Border x:Name="Waitinglay"
|
|
||||||
Background="#40000000"
|
|
||||||
Visibility="Collapsed"
|
|
||||||
Panel.ZIndex="1">
|
|
||||||
<StackPanel Width="150"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Margin="0 0 0 100">
|
|
||||||
<ProgressBar Width="80"
|
|
||||||
Height="80"
|
|
||||||
Margin="20"
|
|
||||||
IsIndeterminate="True"
|
|
||||||
Style="{StaticResource MaterialDesignCircularProgressBar}" />
|
|
||||||
<TextBlock FontSize="30"
|
|
||||||
Text="加载中......"
|
|
||||||
HorizontalAlignment="Center" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</materialDesign:DialogHost>
|
|
||||||
</Grid>
|
|
||||||
</materialDesign:DrawerHost>
|
|
||||||
|
|
||||||
</Window>
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
using UIShare.PubEvent;
|
|
||||||
using Prism.Events;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace LOT.Views
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// ShellView.xaml 的交互逻辑
|
|
||||||
/// </summary>
|
|
||||||
public partial class ShellView : Window
|
|
||||||
{
|
|
||||||
public ShellView(IEventAggregator eventAggregator)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
//注册灰度遮罩层
|
|
||||||
eventAggregator.GetEvent<OverlayEvent>().Subscribe(ShowOverlay);
|
|
||||||
eventAggregator.GetEvent<WaitingEvent>().Subscribe(ShowWaitinglay);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowWaitinglay(bool arg)
|
|
||||||
{
|
|
||||||
Waitinglay.Visibility = arg ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowOverlay(bool arg)
|
|
||||||
{
|
|
||||||
Overlay.Visibility = arg ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void ColorZone_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.LeftButton == MouseButtonState.Pressed)
|
|
||||||
{
|
|
||||||
Window.GetWindow(this)?.DragMove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,6 +22,14 @@ namespace Logger
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logger.Error(message);
|
Logger.Error(message);
|
||||||
|
}
|
||||||
|
public static void Info(string message)
|
||||||
|
{
|
||||||
|
Logger.Info(message);
|
||||||
|
}
|
||||||
|
public static void Warn(string message)
|
||||||
|
{
|
||||||
|
Logger.Warn(message);
|
||||||
}
|
}
|
||||||
// 解析堆栈,找到项目文件路径和行号
|
// 解析堆栈,找到项目文件路径和行号
|
||||||
public static string GetProjectStackLine(string stackTrace)
|
public static string GetProjectStackLine(string stackTrace)
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
using LoginModule.Views;
|
|
||||||
using Prism.Modularity;
|
|
||||||
using System.Reflection;
|
|
||||||
namespace LoginModule
|
|
||||||
{
|
|
||||||
public class LoginModule : IModule
|
|
||||||
{
|
|
||||||
public void OnInitialized(IContainerProvider containerProvider)
|
|
||||||
{
|
|
||||||
IRegionManager regionManager = containerProvider.Resolve<IRegionManager>();
|
|
||||||
regionManager.RegisterViewWithRegion("LoginRegion", typeof(LoginView));
|
|
||||||
regionManager.RegisterViewWithRegion("LoginRegion", typeof(RegisterView));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterTypes(IContainerRegistry containerRegistry)
|
|
||||||
{
|
|
||||||
containerRegistry.RegisterForNavigation<LoginView>("LoginView");
|
|
||||||
containerRegistry.RegisterForNavigation<RegisterView>("RegisterView");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\UIShare\UIShare.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using UIShare.PubEvent;
|
|
||||||
using UIShare.ViewModelBase;
|
|
||||||
|
|
||||||
namespace LoginModule.ViewModels
|
|
||||||
{
|
|
||||||
public class LoginViewModel: NavigateViewModelBase
|
|
||||||
{
|
|
||||||
#region 属性
|
|
||||||
private string _Password;
|
|
||||||
public string Password
|
|
||||||
{
|
|
||||||
get => _Password;
|
|
||||||
set => SetProperty(ref _Password, value);
|
|
||||||
}
|
|
||||||
private string _Account;
|
|
||||||
public string Account
|
|
||||||
{
|
|
||||||
get => _Account;
|
|
||||||
set => SetProperty(ref _Account, value);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
public ICommand LoginCommand { get; set; }
|
|
||||||
public ICommand RegisterCommand { get; set; }
|
|
||||||
private IEventAggregator _eventAggregator;
|
|
||||||
public LoginViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
|
||||||
{
|
|
||||||
_eventAggregator = containerProvider.Resolve<IEventAggregator>();
|
|
||||||
LoginCommand = new AsyncDelegateCommand(OnLogin);
|
|
||||||
RegisterCommand = new AsyncDelegateCommand(OnRegister);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnRegister()
|
|
||||||
{
|
|
||||||
_regionManager.RequestNavigate("LoginRegion", "RegisterView");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnLogin()
|
|
||||||
{
|
|
||||||
_eventAggregator.GetEvent<LoginSuccessEvent>().Publish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using UIShare.ViewModelBase;
|
|
||||||
|
|
||||||
namespace LoginModule.ViewModels
|
|
||||||
{
|
|
||||||
public class RegisterViewModel : NavigateViewModelBase
|
|
||||||
{
|
|
||||||
#region 属性
|
|
||||||
private string _SecondPassword;
|
|
||||||
public string SecondPassword
|
|
||||||
{
|
|
||||||
get => _SecondPassword;
|
|
||||||
set => SetProperty(ref _SecondPassword, value);
|
|
||||||
}
|
|
||||||
private string _Password;
|
|
||||||
public string Password
|
|
||||||
{
|
|
||||||
get => _Password;
|
|
||||||
set => SetProperty(ref _Password, value);
|
|
||||||
}
|
|
||||||
private string _Account;
|
|
||||||
public string Account
|
|
||||||
{
|
|
||||||
get => _Account;
|
|
||||||
set => SetProperty(ref _Account, value);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
public ICommand BackCommand { get; set; }
|
|
||||||
public ICommand RegisterCommand { get; set; }
|
|
||||||
public RegisterViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
|
||||||
{
|
|
||||||
BackCommand = new DelegateCommand(OnBack);
|
|
||||||
RegisterCommand = new AsyncDelegateCommand(OnRegister);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBack()
|
|
||||||
{
|
|
||||||
_regionManager.RequestNavigate("LoginRegion", "LoginView");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnRegister()
|
|
||||||
{
|
|
||||||
_regionManager.RequestNavigate("LoginRegion", "LoginView");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
<UserControl x:Class="LoginModule.Views.LoginView"
|
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
|
||||||
xmlns:helpers="clr-namespace:UIShare.Helpers;assembly=UIShare"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
|
||||||
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
|
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
|
||||||
xmlns:local="clr-namespace:LoginModule.Views"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Height="315"
|
|
||||||
Width="420">
|
|
||||||
<UserControl.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<ResourceDictionary.MergedDictionaries>
|
|
||||||
<ResourceDictionary Source="/UIShare;component/Styles/CommonStyle.xaml" />
|
|
||||||
</ResourceDictionary.MergedDictionaries>
|
|
||||||
<!-- 这里是你的额外样式 -->
|
|
||||||
<Style TargetType="TextBox"
|
|
||||||
BasedOn="{StaticResource MahApps.Styles.TextBox}">
|
|
||||||
<Setter Property="FontSize"
|
|
||||||
Value="14" />
|
|
||||||
<Setter Property="BorderThickness"
|
|
||||||
Value="0,0,0,2" />
|
|
||||||
<Setter Property="BorderBrush"
|
|
||||||
Value="#E0E0E0" />
|
|
||||||
<Setter Property="Padding"
|
|
||||||
Value="5,8" />
|
|
||||||
<Setter Property="Background"
|
|
||||||
Value="Transparent" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="PasswordBox"
|
|
||||||
BasedOn="{StaticResource MahApps.Styles.PasswordBox}">
|
|
||||||
<Setter Property="FontSize"
|
|
||||||
Value="14" />
|
|
||||||
<Setter Property="BorderThickness"
|
|
||||||
Value="0,0,0,2" />
|
|
||||||
<Setter Property="BorderBrush"
|
|
||||||
Value="#E0E0E0" />
|
|
||||||
<Setter Property="Padding"
|
|
||||||
Value="5,8" />
|
|
||||||
<Setter Property="Background"
|
|
||||||
Value="Transparent" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Button"
|
|
||||||
BasedOn="{StaticResource MahApps.Styles.Button.Flat}">
|
|
||||||
<Setter Property="FontSize"
|
|
||||||
Value="15" />
|
|
||||||
<Setter Property="FontWeight"
|
|
||||||
Value="SemiBold" />
|
|
||||||
<Setter Property="Foreground"
|
|
||||||
Value="White" />
|
|
||||||
<Setter Property="Background"
|
|
||||||
Value="#2196F3" />
|
|
||||||
<Setter Property="BorderThickness"
|
|
||||||
Value="0" />
|
|
||||||
<Setter Property="Margin"
|
|
||||||
Value="0,20,0,0" />
|
|
||||||
</Style>
|
|
||||||
</ResourceDictionary>
|
|
||||||
</UserControl.Resources>
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="40" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- 表单区域 -->
|
|
||||||
<Border Grid.Row="0"
|
|
||||||
Background="White"
|
|
||||||
Margin="20,20,20,0"
|
|
||||||
CornerRadius="5"
|
|
||||||
BorderThickness="1"
|
|
||||||
BorderBrush="#E0E0E0"
|
|
||||||
Padding="30,20">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
|
|
||||||
<Label Content="用户登录"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
FontSize="15"
|
|
||||||
Padding="3" />
|
|
||||||
<StackPanel Grid.Row="1">
|
|
||||||
|
|
||||||
<!-- 用户名输入 -->
|
|
||||||
<StackPanel Orientation="Vertical"
|
|
||||||
HorizontalAlignment="Center">
|
|
||||||
<StackPanel Orientation="Horizontal"
|
|
||||||
Margin="17">
|
|
||||||
<materialDesign:PackIcon Kind="Account"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<TextBox Text="{Binding Account}"
|
|
||||||
mah:TextBoxHelper.Watermark="请输入账号"
|
|
||||||
mah:TextBoxHelper.ClearTextButton="True"
|
|
||||||
VerticalContentAlignment="Center"
|
|
||||||
Width="180"
|
|
||||||
Height="30"
|
|
||||||
Padding="0" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- 密码输入 -->
|
|
||||||
<StackPanel Orientation="Vertical"
|
|
||||||
HorizontalAlignment="Center">
|
|
||||||
<StackPanel Orientation="Horizontal"
|
|
||||||
Margin="7">
|
|
||||||
<materialDesign:PackIcon Kind="Lock"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<PasswordBox helpers:PasswordBoxHelper.Password="{Binding Password, Mode=TwoWay}"
|
|
||||||
mah:TextBoxHelper.Watermark="请输入密码"
|
|
||||||
mah:TextBoxHelper.ClearTextButton="True"
|
|
||||||
VerticalContentAlignment="Center"
|
|
||||||
Width="180"
|
|
||||||
Height="30"
|
|
||||||
Padding="0" />
|
|
||||||
</StackPanel>
|
|
||||||
<Label Content="注册账户"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Cursor="Hand">
|
|
||||||
<i:Interaction.Triggers>
|
|
||||||
<i:EventTrigger EventName="MouseLeftButtonUp">
|
|
||||||
<i:InvokeCommandAction Command="{Binding RegisterCommand}" />
|
|
||||||
</i:EventTrigger>
|
|
||||||
</i:Interaction.Triggers>
|
|
||||||
</Label>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- 登录按钮 -->
|
|
||||||
<Button Grid.Row="2"
|
|
||||||
Command="{Binding LoginCommand}"
|
|
||||||
Content="登 录"
|
|
||||||
Width="120"
|
|
||||||
Height="33"
|
|
||||||
mah:ControlsHelper.CornerRadius="5">
|
|
||||||
<Button.Effect>
|
|
||||||
<DropShadowEffect BlurRadius="8"
|
|
||||||
ShadowDepth="3"
|
|
||||||
Opacity="0.5" />
|
|
||||||
</Button.Effect>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 底部版权信息 -->
|
|
||||||
<TextBlock Grid.Row="2"
|
|
||||||
Text="© 2025 大学生学习交流平台"
|
|
||||||
Foreground="#777"
|
|
||||||
FontSize="12"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Margin="0,10" />
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace LoginModule.Views
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// LoginView.xaml 的交互逻辑
|
|
||||||
/// </summary>
|
|
||||||
public partial class LoginView : UserControl
|
|
||||||
{
|
|
||||||
public LoginView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
<UserControl x:Class="LoginModule.Views.RegisterView"
|
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
|
||||||
xmlns:helpers="clr-namespace:UIShare.Helpers;assembly=UIShare"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
|
||||||
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
|
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
|
||||||
xmlns:local="clr-namespace:LoginModule.Views"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
Height="315"
|
|
||||||
Width="420">
|
|
||||||
<UserControl.Resources>
|
|
||||||
<ResourceDictionary>
|
|
||||||
<ResourceDictionary.MergedDictionaries>
|
|
||||||
<ResourceDictionary Source="/UIShare;component/Styles/CommonStyle.xaml" />
|
|
||||||
</ResourceDictionary.MergedDictionaries>
|
|
||||||
<!-- 这里是你的额外样式 -->
|
|
||||||
<Style TargetType="TextBox"
|
|
||||||
BasedOn="{StaticResource MahApps.Styles.TextBox}">
|
|
||||||
<Setter Property="FontSize"
|
|
||||||
Value="14" />
|
|
||||||
<Setter Property="BorderThickness"
|
|
||||||
Value="0,0,0,2" />
|
|
||||||
<Setter Property="BorderBrush"
|
|
||||||
Value="#E0E0E0" />
|
|
||||||
<Setter Property="Padding"
|
|
||||||
Value="5,8" />
|
|
||||||
<Setter Property="Background"
|
|
||||||
Value="Transparent" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="PasswordBox"
|
|
||||||
BasedOn="{StaticResource MahApps.Styles.PasswordBox}">
|
|
||||||
<Setter Property="FontSize"
|
|
||||||
Value="14" />
|
|
||||||
<Setter Property="BorderThickness"
|
|
||||||
Value="0,0,0,2" />
|
|
||||||
<Setter Property="BorderBrush"
|
|
||||||
Value="#E0E0E0" />
|
|
||||||
<Setter Property="Padding"
|
|
||||||
Value="5,8" />
|
|
||||||
<Setter Property="Background"
|
|
||||||
Value="Transparent" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Button"
|
|
||||||
BasedOn="{StaticResource MahApps.Styles.Button.Flat}">
|
|
||||||
<Setter Property="FontSize"
|
|
||||||
Value="15" />
|
|
||||||
<Setter Property="FontWeight"
|
|
||||||
Value="SemiBold" />
|
|
||||||
<Setter Property="Foreground"
|
|
||||||
Value="White" />
|
|
||||||
<Setter Property="Background"
|
|
||||||
Value="#2196F3" />
|
|
||||||
<Setter Property="BorderThickness"
|
|
||||||
Value="0" />
|
|
||||||
<Setter Property="Margin"
|
|
||||||
Value="0,20,0,0" />
|
|
||||||
</Style>
|
|
||||||
</ResourceDictionary>
|
|
||||||
</UserControl.Resources>
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="40" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<!-- 表单区域 -->
|
|
||||||
<Border Grid.Row="0"
|
|
||||||
Background="White"
|
|
||||||
Margin="20,20,20,0"
|
|
||||||
CornerRadius="5"
|
|
||||||
BorderThickness="1"
|
|
||||||
BorderBrush="#E0E0E0"
|
|
||||||
Padding="30,20">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
|
|
||||||
<Label Content="用户注册"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
FontSize="15"
|
|
||||||
Padding="3" />
|
|
||||||
<StackPanel Grid.Row="1">
|
|
||||||
|
|
||||||
<!-- 用户名输入 -->
|
|
||||||
<StackPanel Orientation="Vertical"
|
|
||||||
HorizontalAlignment="Center">
|
|
||||||
<StackPanel Orientation="Horizontal"
|
|
||||||
Margin="7">
|
|
||||||
<materialDesign:PackIcon Kind="Account"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<TextBox Text="{Binding Account}"
|
|
||||||
mah:TextBoxHelper.Watermark="请输入账号"
|
|
||||||
mah:TextBoxHelper.ClearTextButton="True"
|
|
||||||
VerticalContentAlignment="Center"
|
|
||||||
Width="180"
|
|
||||||
Height="30"
|
|
||||||
Padding="0" />
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- 密码输入 -->
|
|
||||||
<StackPanel Orientation="Vertical"
|
|
||||||
HorizontalAlignment="Center">
|
|
||||||
<StackPanel Orientation="Horizontal"
|
|
||||||
Margin="7">
|
|
||||||
<materialDesign:PackIcon Kind="Lock"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<PasswordBox helpers:PasswordBoxHelper.Password="{Binding Password, Mode=TwoWay}"
|
|
||||||
mah:TextBoxHelper.Watermark="请输入密码"
|
|
||||||
mah:TextBoxHelper.ClearTextButton="True"
|
|
||||||
VerticalContentAlignment="Center"
|
|
||||||
Width="180"
|
|
||||||
Height="30"
|
|
||||||
Padding="0" />
|
|
||||||
</StackPanel>
|
|
||||||
<StackPanel Orientation="Horizontal"
|
|
||||||
Margin="7">
|
|
||||||
<materialDesign:PackIcon Kind="Lock"
|
|
||||||
VerticalAlignment="Center" />
|
|
||||||
<PasswordBox helpers:PasswordBoxHelper.Password="{Binding SecondPassword, Mode=TwoWay}"
|
|
||||||
mah:TextBoxHelper.Watermark="请输入二次密码"
|
|
||||||
mah:TextBoxHelper.ClearTextButton="True"
|
|
||||||
VerticalContentAlignment="Center"
|
|
||||||
Width="180"
|
|
||||||
Height="30"
|
|
||||||
Padding="0" />
|
|
||||||
</StackPanel>
|
|
||||||
<Label Content="返回"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
Cursor="Hand">
|
|
||||||
<i:Interaction.Triggers>
|
|
||||||
<i:EventTrigger EventName="MouseLeftButtonUp">
|
|
||||||
<i:InvokeCommandAction Command="{Binding BackCommand}" />
|
|
||||||
</i:EventTrigger>
|
|
||||||
</i:Interaction.Triggers>
|
|
||||||
</Label>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- 登录按钮 -->
|
|
||||||
<Button Grid.Row="2"
|
|
||||||
Command="{Binding RegisterCommand}"
|
|
||||||
Content="注 册"
|
|
||||||
Width="120"
|
|
||||||
Height="33"
|
|
||||||
mah:ControlsHelper.CornerRadius="5">
|
|
||||||
<Button.Effect>
|
|
||||||
<DropShadowEffect BlurRadius="8"
|
|
||||||
ShadowDepth="3"
|
|
||||||
Opacity="0.5" />
|
|
||||||
</Button.Effect>
|
|
||||||
</Button>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- 底部版权信息 -->
|
|
||||||
<TextBlock Grid.Row="2"
|
|
||||||
Text="© 2025 大学生学习交流平台"
|
|
||||||
Foreground="#777"
|
|
||||||
FontSize="12"
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
Margin="0,10" />
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace LoginModule.Views
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// RegisterView.xaml 的交互逻辑
|
|
||||||
/// </summary>
|
|
||||||
public partial class RegisterView : UserControl
|
|
||||||
{
|
|
||||||
public RegisterView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using MainModule.Views;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace MainModule
|
|
||||||
{
|
|
||||||
[Module(OnDemand=true)]
|
|
||||||
public class MainModule : IModule
|
|
||||||
{
|
|
||||||
public void OnInitialized(IContainerProvider containerProvider)
|
|
||||||
{
|
|
||||||
IRegionManager regionManager = containerProvider.Resolve<IRegionManager>();
|
|
||||||
regionManager.RegisterViewWithRegion("ShellViewManager", typeof(MainView));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterTypes(IContainerRegistry containerRegistry)
|
|
||||||
{
|
|
||||||
containerRegistry.RegisterForNavigation<MainView>("MainView");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<UseWPF>true</UseWPF>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Model\Model.csproj" />
|
|
||||||
<ProjectReference Include="..\Service\Service.csproj" />
|
|
||||||
<ProjectReference Include="..\UIShare\UIShare.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Update="Views\MainView.xaml.cs">
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
using Model.Entity;
|
|
||||||
using Prism.Commands;
|
|
||||||
using Prism.Ioc;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using UIShare.ViewModelBase;
|
|
||||||
|
|
||||||
namespace MainModule.ViewModels
|
|
||||||
{
|
|
||||||
public class MainViewModel : NavigateViewModelBase
|
|
||||||
{
|
|
||||||
public ObservableCollection<ChamberMonitorItem> Chambers { get; } = new();
|
|
||||||
private IContainerProvider _containerProvider;
|
|
||||||
public MainViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
|
||||||
{
|
|
||||||
_containerProvider = containerProvider;
|
|
||||||
|
|
||||||
// 触发配置与绑定初始化
|
|
||||||
ConfigureAndBindChambers();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 核心配置与数据绑定逻辑
|
|
||||||
/// </summary>
|
|
||||||
private void ConfigureAndBindChambers()
|
|
||||||
{
|
|
||||||
// 实例 1:Modbus TCP 环境箱配置
|
|
||||||
Chambers.Add(new ChamberMonitorItem
|
|
||||||
{
|
|
||||||
Id = 1,
|
|
||||||
Name = "环境箱 01",
|
|
||||||
ProtocolType = "Modbus TCP",
|
|
||||||
IsConnected = false,
|
|
||||||
Temperature = 0.0,
|
|
||||||
Humidity = 0.0
|
|
||||||
});
|
|
||||||
|
|
||||||
// 实例 2:Modbus TCP B+ 环境箱配置
|
|
||||||
Chambers.Add(new ChamberMonitorItem
|
|
||||||
{
|
|
||||||
Id = 2,
|
|
||||||
Name = "环境箱 02",
|
|
||||||
ProtocolType = "Modbus TCP B+",
|
|
||||||
IsConnected = false,
|
|
||||||
Temperature = 0.0,
|
|
||||||
Humidity = 0.0
|
|
||||||
});
|
|
||||||
|
|
||||||
// 实例 3:西门子 S7 环境箱配置
|
|
||||||
Chambers.Add(new ChamberMonitorItem
|
|
||||||
{
|
|
||||||
Id = 3,
|
|
||||||
Name = "环境箱 03",
|
|
||||||
ProtocolType = "S7",
|
|
||||||
IsConnected = false,
|
|
||||||
Temperature = 0.0,
|
|
||||||
Humidity = 0.0
|
|
||||||
});
|
|
||||||
|
|
||||||
// 实例 4:HTTP 环境箱配置
|
|
||||||
Chambers.Add(new ChamberMonitorItem
|
|
||||||
{
|
|
||||||
Id = 4,
|
|
||||||
Name = "环境箱 04",
|
|
||||||
ProtocolType = "HTTP",
|
|
||||||
IsConnected = false,
|
|
||||||
Temperature = 0.0,
|
|
||||||
Humidity = 0.0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 环境箱 UI 状态及温湿度数据绑定模型
|
|
||||||
/// </summary>
|
|
||||||
public class ChamberMonitorItem :BindableBase
|
|
||||||
{
|
|
||||||
private double _temperature;
|
|
||||||
private double _humidity;
|
|
||||||
private bool _isConnected;
|
|
||||||
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string ProtocolType { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 温度(支持通知刷新)
|
|
||||||
/// </summary>
|
|
||||||
public double Temperature
|
|
||||||
{
|
|
||||||
get => _temperature;
|
|
||||||
set => SetProperty(ref _temperature, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 湿度(支持通知刷新)
|
|
||||||
/// </summary>
|
|
||||||
public double Humidity
|
|
||||||
{
|
|
||||||
get => _humidity;
|
|
||||||
set => SetProperty(ref _humidity, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 连接状态:True-已连接(绿灯),False-断开(红灯)
|
|
||||||
/// </summary>
|
|
||||||
public bool IsConnected
|
|
||||||
{
|
|
||||||
get => _isConnected;
|
|
||||||
set => SetProperty(ref _isConnected, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
<UserControl x:Class="MainModule.Views.MainView"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
|
||||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
|
||||||
mc:Ignorable="d"
|
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
|
||||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
|
||||||
d:DesignHeight="1080"
|
|
||||||
d:DesignWidth="1920">
|
|
||||||
<UserControl.Resources>
|
|
||||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis" />
|
|
||||||
</UserControl.Resources>
|
|
||||||
|
|
||||||
<Grid Background="#F4F6F9">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<TextBlock Grid.Row="0" Text="环境箱监控状态面板" FontSize="20" FontWeight="Bold" Foreground="#2C3E50" Margin="12,4,0,16"/>
|
|
||||||
|
|
||||||
<ItemsControl Grid.Row="1" ItemsSource="{Binding Chambers}">
|
|
||||||
<ItemsControl.ItemsPanel>
|
|
||||||
<ItemsPanelTemplate>
|
|
||||||
<UniformGrid Columns="2" Rows="2" Margin="-6"/>
|
|
||||||
</ItemsPanelTemplate>
|
|
||||||
</ItemsControl.ItemsPanel>
|
|
||||||
|
|
||||||
<ItemsControl.ItemTemplate>
|
|
||||||
<DataTemplate>
|
|
||||||
<Border Background="White" BorderBrush="#E2E8F0" BorderThickness="1" CornerRadius="8" Margin="6" Padding="20">
|
|
||||||
<Border.Effect>
|
|
||||||
<DropShadowEffect BlurRadius="8" Color="#A0AEC0" Opacity="0.15" ShadowDepth="1"/>
|
|
||||||
</Border.Effect>
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
<RowDefinition Height="*"/>
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<DockPanel Grid.Row="0" LastChildFill="False">
|
|
||||||
<StackPanel DockPanel.Dock="Left">
|
|
||||||
<TextBlock Text="{Binding Name}" FontSize="16" FontWeight="Bold" Foreground="#1A202C"/>
|
|
||||||
<TextBlock Text="{Binding ProtocolType, StringFormat=驱动协议: {0}}" FontSize="12" Foreground="#718096" Margin="0,2,0,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
|
|
||||||
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0" VerticalAlignment="Center">
|
|
||||||
<Border.Style>
|
|
||||||
<Style TargetType="Border">
|
|
||||||
<Setter Property="Background" Value="#E53E3E"/>
|
|
||||||
<Style.Triggers>
|
|
||||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
|
||||||
<Setter Property="Background" Value="#38A169"/>
|
|
||||||
</DataTrigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
</Border.Style>
|
|
||||||
</Border>
|
|
||||||
<TextBlock VerticalAlignment="Center" FontSize="13" FontWeight="Medium">
|
|
||||||
<TextBlock.Style>
|
|
||||||
<Style TargetType="TextBlock">
|
|
||||||
<Setter Property="Text" Value="OFFLINE"/>
|
|
||||||
<Setter Property="Foreground" Value="#E53E3E"/>
|
|
||||||
<Style.Triggers>
|
|
||||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
|
||||||
<Setter Property="Text" Value="ONLINE"/>
|
|
||||||
<Setter Property="Foreground" Value="#38A169"/>
|
|
||||||
</DataTrigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
</TextBlock.Style>
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
|
||||||
</DockPanel>
|
|
||||||
|
|
||||||
<UniformGrid Grid.Row="1" Columns="2" Margin="0,20,0,0">
|
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="温度采集" FontSize="13" Foreground="#718096" HorizontalAlignment="Center"/>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<TextBlock Text="{Binding Temperature, StringFormat={}{0:F1}}" FontSize="40" FontWeight="Light" Foreground="#3182CE"/>
|
|
||||||
<TextBlock Text=" ℃" FontSize="16" Foreground="#3182CE" VerticalAlignment="Bottom" Margin="2,0,0,8"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="湿度采集" FontSize="13" Foreground="#718096" HorizontalAlignment="Center"/>
|
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
|
|
||||||
<TextBlock Text="{Binding Humidity, StringFormat={}{0:F1}}" FontSize="40" FontWeight="Light" Foreground="#319795"/>
|
|
||||||
<TextBlock Text=" %RH" FontSize="16" Foreground="#319795" VerticalAlignment="Bottom" Margin="2,0,0,8"/>
|
|
||||||
</StackPanel>
|
|
||||||
</StackPanel>
|
|
||||||
</UniformGrid>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
|
||||||
</Grid>
|
|
||||||
</UserControl>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace MainModule.Views
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// MainView.xaml 的交互逻辑
|
|
||||||
/// </summary>
|
|
||||||
public partial class MainView : UserControl
|
|
||||||
{
|
|
||||||
public MainView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Model.Entity.Asset;
|
||||||
|
|
||||||
|
namespace Model.Dto.Asset
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备附件 DTO(Id 由 long 改为 string,避免前端精度丢失)
|
||||||
|
/// </summary>
|
||||||
|
public class EquipmentAttachmentDto
|
||||||
|
{
|
||||||
|
/// <summary>主键 Id(long → string)</summary>
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>删除状态(0、未删除;1、已删除)</summary>
|
||||||
|
public byte IsDel { get; set; }
|
||||||
|
|
||||||
|
/// <summary>创建时间</summary>
|
||||||
|
public DateTime? CreateTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>设备Id(关联 EquipmentEntity.Id,雪花 Id 转 string 防精度丢失)</summary>
|
||||||
|
public string EquipmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文件名称</summary>
|
||||||
|
public string? FileName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文件类型(说明书/合格证/校准证书等)</summary>
|
||||||
|
public AttachmentTypeEnum FileType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文件扩展名(如 .pdf、.jpg)</summary>
|
||||||
|
public string? FileExt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文件存储地址</summary>
|
||||||
|
public string? FileUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>文件大小(byte)</summary>
|
||||||
|
public long FileSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>上传人</summary>
|
||||||
|
public string? Uploader { get; set; }
|
||||||
|
|
||||||
|
/// <summary>备注</summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
namespace Model.Dto.Asset
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备分类 DTO(Id 由 long 改为 string,避免前端精度丢失)
|
||||||
|
/// </summary>
|
||||||
|
public class EquipmentCategoryDto
|
||||||
|
{
|
||||||
|
/// <summary>主键 Id(long → string)</summary>
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>删除状态(0、未删除;1、已删除)</summary>
|
||||||
|
public byte IsDel { get; set; }
|
||||||
|
|
||||||
|
/// <summary>创建时间</summary>
|
||||||
|
public DateTime? CreateTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>父级分类Id(0表示顶级,雪花 Id 转 string 防精度丢失)</summary>
|
||||||
|
public string ParentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>分类名称</summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>分类编码</summary>
|
||||||
|
public string? Code { get; set; }
|
||||||
|
|
||||||
|
/// <summary>层级(1级、2级……)</summary>
|
||||||
|
public int Level { get; set; }
|
||||||
|
|
||||||
|
/// <summary>排序号</summary>
|
||||||
|
public int Sort { get; set; }
|
||||||
|
|
||||||
|
/// <summary>备注</summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
using Model.Entity.Asset;
|
||||||
|
|
||||||
|
namespace Model.Dto.Asset
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备台账 DTO(Id 由 long 改为 string,避免前端精度丢失)
|
||||||
|
/// </summary>
|
||||||
|
public class EquipmentDto
|
||||||
|
{
|
||||||
|
/// <summary>主键 Id(long → 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? Code { get; set; }
|
||||||
|
|
||||||
|
/// <summary>设备名称</summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>设备分类Id(关联 EquipmentCategoryEntity.Id,雪花 Id 转 string 防精度丢失;可空,未分类时为 "0" 或不传)</summary>
|
||||||
|
public string? CategoryId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>设备类型</summary>
|
||||||
|
public string? Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>设备品牌</summary>
|
||||||
|
public string? Brand { get; set; }
|
||||||
|
|
||||||
|
/// <summary>设备型号</summary>
|
||||||
|
public string? Model { get; set; }
|
||||||
|
|
||||||
|
/// <summary>规格参数</summary>
|
||||||
|
public string? Specifications { get; set; }
|
||||||
|
|
||||||
|
/// <summary>制造商</summary>
|
||||||
|
public string? Manufacturer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>供应商</summary>
|
||||||
|
public string? Supplier { get; set; }
|
||||||
|
|
||||||
|
/// <summary>设备图片地址</summary>
|
||||||
|
public string? ImageUrl { 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>设备描述</summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>使用部门名称</summary>
|
||||||
|
public string? Department { get; set; }
|
||||||
|
|
||||||
|
/// <summary>存放位置</summary>
|
||||||
|
public string? Location { get; set; }
|
||||||
|
|
||||||
|
/// <summary>责任人</summary>
|
||||||
|
public string? ResponsiblePerson { get; set; }
|
||||||
|
|
||||||
|
/// <summary>责任人联系电话</summary>
|
||||||
|
public string? ContactPhone { get; set; }
|
||||||
|
|
||||||
|
/// <summary>保管人</summary>
|
||||||
|
public string? Custodian { get; set; }
|
||||||
|
|
||||||
|
/// <summary>购置日期</summary>
|
||||||
|
public DateTime? PurchaseDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>购置价格</summary>
|
||||||
|
public decimal? PurchasePrice { get; set; }
|
||||||
|
|
||||||
|
/// <summary>保修到期日期</summary>
|
||||||
|
public DateTime? WarrantyExpireDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>验收日期</summary>
|
||||||
|
public DateTime? AcceptanceDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>启用日期</summary>
|
||||||
|
public DateTime? EnableDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>使用年限(年)</summary>
|
||||||
|
public int? ServiceLifeYears { get; set; }
|
||||||
|
|
||||||
|
/// <summary>报废日期</summary>
|
||||||
|
public DateTime? ScrapDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>设备当前状态</summary>
|
||||||
|
public EquipmentStatusEnum Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>校准状态(正常/超期)</summary>
|
||||||
|
public ComplianceStatusEnum CalibrationStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>上次校准日期</summary>
|
||||||
|
public DateTime? LastCalibrationDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>下次校准到期日期</summary>
|
||||||
|
public DateTime? NextCalibrationDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>检定日期</summary>
|
||||||
|
public DateTime? InspectionDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>下次检定到期日期</summary>
|
||||||
|
public DateTime? NextInspectionDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>保养状态(正常/超期)</summary>
|
||||||
|
public ComplianceStatusEnum MaintenanceStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>上次保养日期</summary>
|
||||||
|
public DateTime? LastMaintenanceDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>下次保养到期日期</summary>
|
||||||
|
public DateTime? NextMaintenanceDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>备注</summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
|
||||||
|
/// <summary>创建人</summary>
|
||||||
|
public string? CreateBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>更新人</summary>
|
||||||
|
public string? UpdateBy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>更新时间</summary>
|
||||||
|
public DateTime? UpdateTime { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using Model.Entity.Asset;
|
||||||
|
|
||||||
|
namespace Model.Dto.Asset
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备状态变更请求 DTO(POST 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Model.Entity.Asset;
|
||||||
|
|
||||||
|
namespace Model.Dto.Asset
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备状态记录 DTO(Id 由 long 改为 string,避免前端精度丢失)
|
||||||
|
/// </summary>
|
||||||
|
public class EquipmentStatusRecordDto
|
||||||
|
{
|
||||||
|
/// <summary>主键 Id(long → string)</summary>
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>删除状态(0、未删除;1、已删除)</summary>
|
||||||
|
public byte IsDel { get; set; }
|
||||||
|
|
||||||
|
/// <summary>创建时间</summary>
|
||||||
|
public DateTime? CreateTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>设备Id(关联 EquipmentEntity.Id,雪花 Id 转 string 防精度丢失)</summary>
|
||||||
|
public string EquipmentId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>变更前状态</summary>
|
||||||
|
public EquipmentStatusEnum FromStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>变更后状态</summary>
|
||||||
|
public EquipmentStatusEnum ToStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>状态变更时间</summary>
|
||||||
|
public DateTime ChangeTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>操作人</summary>
|
||||||
|
public string? Operator { get; set; }
|
||||||
|
|
||||||
|
/// <summary>变更原因/备注</summary>
|
||||||
|
public string? Remark { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
namespace Model.Dto.Asset
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备二维码信息(生成/列表/详情共用)
|
||||||
|
/// </summary>
|
||||||
|
public class QrCodeDto
|
||||||
|
{
|
||||||
|
/// <summary>设备 Id(long → string)</summary>
|
||||||
|
public string EquipmentId { get; set; }
|
||||||
|
/// <summary>设备编号</summary>
|
||||||
|
public string? EquipmentCode { get; set; }
|
||||||
|
/// <summary>设备名称</summary>
|
||||||
|
public string? EquipmentName { get; set; }
|
||||||
|
/// <summary>存放位置(打印标签用)</summary>
|
||||||
|
public string? Location { get; set; }
|
||||||
|
/// <summary>二维码编号(一物一码,人工可读)</summary>
|
||||||
|
public string? QrCode { get; set; }
|
||||||
|
/// <summary>扫码直达 URL(生成后写入;为空表示尚未生成)</summary>
|
||||||
|
public string? QrCodeUrl { get; set; }
|
||||||
|
/// <summary>RFID 编号(已绑定则有值)</summary>
|
||||||
|
public string? RfidCode { get; set; }
|
||||||
|
/// <summary>是否已生成二维码(QrCodeUrl 非空)</summary>
|
||||||
|
public bool HasGenerated => !string.IsNullOrWhiteSpace(QrCodeUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 批量生成结果
|
||||||
|
/// </summary>
|
||||||
|
public class QrCodeBatchResultDto
|
||||||
|
{
|
||||||
|
public string EquipmentId { get; set; }
|
||||||
|
public string? EquipmentCode { get; set; }
|
||||||
|
public string? EquipmentName { get; set; }
|
||||||
|
public string? QrCodeUrl { get; set; }
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace Model.Dto.Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备指令下发请求 DTO
|
||||||
|
/// </summary>
|
||||||
|
public class DeviceCommandDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备主键 Id(string)
|
||||||
|
/// </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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace Model.Dto.Config
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备日志 DTO(Id 由 long 改为 string,避免前端精度丢失)
|
||||||
|
/// </summary>
|
||||||
|
public class DeviceLogDto
|
||||||
|
{
|
||||||
|
/// <summary>主键 Id(long → 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user