完善 MCS 桌面代理与开炼机动作同步,修复原料相关菜单权限,桌面端新增上辅机 MES 菜单。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-27 15:56:05 +08:00
parent 7a9c19e4f3
commit 442a4c8ae2
113 changed files with 10169 additions and 91 deletions

View File

@@ -0,0 +1,486 @@
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Configuration;
using Prism.Events;
using YY.Admin.Core.Events;
using YY.Admin.Core.Services;
namespace YY.Admin.Services.Service.EquipmentDb;
/// <summary>
/// 订阅 MES 下发的 EQ_* 命令,执行设备库 SQL 并回传结果。
/// </summary>
public sealed class EquipmentDbCommandHandler
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
private readonly IEquipmentDbConnectionFactory _connectionFactory;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _configuration;
private readonly ISignalRService _signalRService;
private readonly HashSet<string> _processedRequestIds = new();
private readonly object _dedupeLock = new();
public EquipmentDbCommandHandler(
IEventAggregator eventAggregator,
IEquipmentDbConnectionFactory connectionFactory,
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
ISignalRService signalRService)
{
_connectionFactory = connectionFactory;
_httpClientFactory = httpClientFactory;
_configuration = configuration;
_signalRService = signalRService;
eventAggregator.GetEvent<RemoteCommandReceivedEvent>()
.Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread);
}
private void OnRemoteCommand(RemoteCommandPayload payload)
{
if (payload == null || string.IsNullOrWhiteSpace(payload.CommandJson))
{
return;
}
try
{
using var doc = JsonDocument.Parse(payload.CommandJson);
if (!doc.RootElement.TryGetProperty("cmd", out var cmdEl))
{
return;
}
var cmd = cmdEl.GetString() ?? string.Empty;
if (!cmd.StartsWith("EQ_", StringComparison.OrdinalIgnoreCase))
{
return;
}
var request = JsonSerializer.Deserialize<EquipmentDbCommandDto>(payload.CommandJson, JsonOptions);
if (request == null)
{
return;
}
_ = Task.Run(() => HandleAsync(request, CancellationToken.None));
}
catch
{
// ignore non-EQ payloads
}
}
public async Task HandleAsync(EquipmentDbCommandDto request, CancellationToken cancellationToken)
{
if (request == null || string.IsNullOrWhiteSpace(request.RequestId))
{
return;
}
lock (_dedupeLock)
{
if (!_processedRequestIds.Add(request.RequestId))
{
return;
}
if (_processedRequestIds.Count > 2000)
{
_processedRequestIds.Clear();
_processedRequestIds.Add(request.RequestId);
}
}
var result = new EquipmentDbResultDto
{
RequestId = request.RequestId,
DeviceId = ResolveDeviceId(request.DeviceId),
Cmd = request.Cmd,
RespondedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
try
{
switch ((request.Cmd ?? string.Empty).ToUpperInvariant())
{
case "EQ_TEST":
await HandleTestAsync(result, cancellationToken).ConfigureAwait(false);
break;
case "EQ_QUERY":
case "EQ_SYNC_PULL":
await HandleQueryAsync(request, result, cancellationToken).ConfigureAwait(false);
break;
case "EQ_WRITE":
await HandleWriteAsync(request, result, cancellationToken).ConfigureAwait(false);
break;
case "EQ_SYNC_PUSH":
await HandleSyncPushAsync(request, result, cancellationToken).ConfigureAwait(false);
break;
default:
result.Success = false;
result.Error = "未知命令: " + request.Cmd;
break;
}
}
catch (Exception ex)
{
result.Success = false;
result.Error = ex.Message;
result.DbConnected = false;
result.DbMessage = ex.Message;
}
await SendResultAsync(result, cancellationToken).ConfigureAwait(false);
}
private async Task HandleTestAsync(EquipmentDbResultDto result, CancellationToken cancellationToken)
{
var (ok, message) = await _connectionFactory.TestAsync(cancellationToken).ConfigureAwait(false);
result.Success = ok;
result.DbConnected = ok;
result.DbMessage = message;
result.Error = ok ? null : message;
await ReportStatusAsync(ok, message, cancellationToken).ConfigureAwait(false);
}
private async Task HandleQueryAsync(EquipmentDbCommandDto request, EquipmentDbResultDto result, CancellationToken cancellationToken)
{
EquipmentDbWhitelist.ValidateSql(request.Sql, request.Table);
await using var conn = _connectionFactory.CreateConnection();
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = conn.CreateCommand();
cmd.CommandText = request.Sql!;
cmd.CommandTimeout = 60;
BindParams(cmd, request.Params);
var rows = new List<Dictionary<string, object?>>();
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
var row = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
for (var i = 0; i < reader.FieldCount; i++)
{
object? val = reader.IsDBNull(i) ? null : reader.GetValue(i);
if (val is DateTime dt)
{
val = dt.ToString("yyyy-MM-dd HH:mm:ss");
}
row[reader.GetName(i)] = val;
}
rows.Add(row);
}
result.Success = true;
result.Rows = rows;
result.Affected = rows.Count;
result.DbConnected = true;
result.DbMessage = "ok";
}
private async Task HandleWriteAsync(EquipmentDbCommandDto request, EquipmentDbResultDto result, CancellationToken cancellationToken)
{
EquipmentDbWhitelist.ValidateSql(request.Sql, request.Table);
await using var conn = _connectionFactory.CreateConnection();
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = conn.CreateCommand();
cmd.CommandText = request.Sql!;
cmd.CommandTimeout = 60;
BindParams(cmd, request.Params);
var affected = await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
result.Success = true;
result.Affected = affected;
result.DbConnected = true;
result.DbMessage = "ok";
}
private async Task HandleSyncPushAsync(EquipmentDbCommandDto request, EquipmentDbResultDto result, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.Table) || !EquipmentDbWhitelist.IsAllowedTable(request.Table))
{
throw new InvalidOperationException("EQ_SYNC_PUSH 表不在白名单");
}
if (request.Payload == null || request.Payload.Count == 0)
{
throw new InvalidOperationException("EQ_SYNC_PUSH payload 为空");
}
request.Payload.TryGetValue("mode", out var modeObj);
var mode = Convert.ToString(modeObj)?.ToLowerInvariant() ?? "insert";
var rows = ExtractRows(request.Payload);
if (rows.Count == 0)
{
result.Success = true;
result.Affected = 0;
result.DbConnected = true;
return;
}
List<string>? keys = null;
if (request.Payload.TryGetValue("keys", out var keysObj) && keysObj != null)
{
keys = JsonSerializer.Deserialize<List<string>>(JsonSerializer.Serialize(keysObj), JsonOptions);
}
await using var conn = _connectionFactory.CreateConnection();
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
var affected = 0;
foreach (var row in rows)
{
if (mode == "upsert" && keys != null && keys.Count > 0)
{
affected += await UpsertRowAsync(conn, request.Table!, row, keys, cancellationToken).ConfigureAwait(false);
}
else if (mode == "update" && keys != null && keys.Count > 0)
{
affected += await UpdateRowAsync(conn, request.Table!, row, keys, cancellationToken).ConfigureAwait(false);
}
else
{
affected += await InsertRowAsync(conn, request.Table!, row, cancellationToken).ConfigureAwait(false);
}
}
result.Success = true;
result.Affected = affected;
result.DbConnected = true;
result.DbMessage = "ok";
}
private static List<Dictionary<string, object?>> ExtractRows(Dictionary<string, object?> payload)
{
if (!payload.TryGetValue("rows", out var rowsObj) || rowsObj == null)
{
throw new InvalidOperationException("EQ_SYNC_PUSH 缺少 rows");
}
if (rowsObj is JsonElement je)
{
return JsonSerializer.Deserialize<List<Dictionary<string, object?>>>(je.GetRawText(), JsonOptions)
?? new List<Dictionary<string, object?>>();
}
return JsonSerializer.Deserialize<List<Dictionary<string, object?>>>(JsonSerializer.Serialize(rowsObj), JsonOptions)
?? new List<Dictionary<string, object?>>();
}
private static async Task<int> InsertRowAsync(
SqlConnection conn, string table, Dictionary<string, object?> row, CancellationToken cancellationToken)
{
var cols = row.Keys.ToList();
foreach (var c in cols)
{
if (!Regex.IsMatch(c, @"^[A-Za-z0-9_]+$"))
{
throw new InvalidOperationException("非法列名: " + c);
}
}
var colList = string.Join(",", cols.Select(c => "[" + c + "]"));
var paramList = string.Join(",", cols.Select((_, i) => "@p" + i));
await using var cmd = conn.CreateCommand();
cmd.CommandText = $"INSERT INTO [{table}] ({colList}) VALUES ({paramList})";
for (var i = 0; i < cols.Count; i++)
{
cmd.Parameters.AddWithValue("@p" + i, ToDbValue(row[cols[i]]));
}
return await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
private static async Task<int> UpdateRowAsync(
SqlConnection conn, string table, Dictionary<string, object?> row, List<string> keys, CancellationToken cancellationToken)
{
var setCols = row.Keys.Where(k => !keys.Contains(k, StringComparer.OrdinalIgnoreCase)).ToList();
if (setCols.Count == 0)
{
return 0;
}
var sets = string.Join(",", setCols.Select((c, i) => $"[{c}]=@s{i}"));
var wheres = string.Join(" AND ", keys.Select((k, i) => $"[{k}]=@k{i}"));
await using var cmd = conn.CreateCommand();
cmd.CommandText = $"UPDATE [{table}] SET {sets} WHERE {wheres}";
for (var i = 0; i < setCols.Count; i++)
{
cmd.Parameters.AddWithValue("@s" + i, ToDbValue(GetIgnoreCase(row, setCols[i])));
}
for (var i = 0; i < keys.Count; i++)
{
cmd.Parameters.AddWithValue("@k" + i, ToDbValue(GetIgnoreCase(row, keys[i])));
}
return await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
private static async Task<int> UpsertRowAsync(
SqlConnection conn, string table, Dictionary<string, object?> row, List<string> keys, CancellationToken cancellationToken)
{
var updated = await UpdateRowAsync(conn, table, row, keys, cancellationToken).ConfigureAwait(false);
if (updated > 0)
{
return updated;
}
return await InsertRowAsync(conn, table, row, cancellationToken).ConfigureAwait(false);
}
private static object? GetIgnoreCase(Dictionary<string, object?> row, string key)
{
if (row.TryGetValue(key, out var v))
{
return v;
}
foreach (var kv in row)
{
if (string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase))
{
return kv.Value;
}
}
return null;
}
private static object ToDbValue(object? value)
{
if (value == null)
{
return DBNull.Value;
}
if (value is JsonElement je)
{
return je.ValueKind switch
{
JsonValueKind.Null => DBNull.Value,
JsonValueKind.String => je.GetString() ?? (object)DBNull.Value,
JsonValueKind.Number => je.TryGetInt64(out var l) ? l : je.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
_ => je.ToString()
};
}
return value;
}
private static void BindParams(SqlCommand cmd, List<object?>? parameters)
{
if (parameters == null)
{
return;
}
for (var i = 0; i < parameters.Count; i++)
{
cmd.Parameters.AddWithValue("@p" + i, ToDbValue(parameters[i]));
}
if (cmd.CommandText.Contains('?'))
{
var sb = new StringBuilder();
var idx = 0;
foreach (var ch in cmd.CommandText)
{
if (ch == '?')
{
sb.Append("@p").Append(idx++);
}
else
{
sb.Append(ch);
}
}
cmd.CommandText = sb.ToString();
}
}
private async Task SendResultAsync(EquipmentDbResultDto result, CancellationToken cancellationToken)
{
var json = JsonSerializer.Serialize(result, JsonOptions);
var useRest = json.Length > 200_000 || (result.Rows?.Count ?? 0) > 500;
if (useRest)
{
try
{
var client = _httpClientFactory.CreateClient("JeecgApi");
result.LastChunk = true;
result.ChunkIndex = 0;
result.ChunkTotal = 1;
var resp = await client.PostAsJsonAsync("/xslmes/deviceAgent/anon/resultChunk", result, cancellationToken)
.ConfigureAwait(false);
resp.EnsureSuccessStatusCode();
return;
}
catch
{
// fall through
}
}
if (_signalRService is IEquipmentDbStompSender sender)
{
await sender.SendAgentResultAsync(result, cancellationToken).ConfigureAwait(false);
}
}
public async Task ReportStatusAsync(bool dbConnected, string? dbMessage, CancellationToken cancellationToken = default)
{
var deviceId = ResolveDeviceId(null);
var status = new Dictionary<string, object?>
{
["deviceId"] = deviceId,
["hostName"] = Environment.MachineName,
["dbConnected"] = dbConnected,
["dbMessage"] = dbMessage,
["ts"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
if (_signalRService is IEquipmentDbStompSender sender)
{
await sender.SendAgentStatusAsync(status, cancellationToken).ConfigureAwait(false);
}
try
{
var client = _httpClientFactory.CreateClient("JeecgApi");
await client.PostAsJsonAsync("/xslmes/deviceAgent/anon/heartbeat", status, cancellationToken).ConfigureAwait(false);
}
catch
{
// ignore
}
}
private string ResolveDeviceId(string? fromRequest)
{
if (!string.IsNullOrWhiteSpace(fromRequest))
{
return fromRequest!;
}
var settings = _connectionFactory.CurrentSettings;
if (!string.IsNullOrWhiteSpace(settings.DeviceId))
{
return settings.DeviceId;
}
return _configuration.GetValue<string>("EquipmentDb:DeviceId")
?? Environment.MachineName;
}
}
public sealed class EquipmentDbCommandDto
{
public string? RequestId { get; set; }
public string? Cmd { get; set; }
public string? DeviceId { get; set; }
public string? Table { get; set; }
public string? Sql { get; set; }
public string? SqlKey { get; set; }
public List<object?>? Params { get; set; }
public Dictionary<string, object?>? Payload { get; set; }
public int? TimeoutMs { get; set; }
}
public sealed class EquipmentDbResultDto
{
public string? RequestId { get; set; }
public string? DeviceId { get; set; }
public string? Cmd { get; set; }
public bool Success { get; set; }
public string? Error { get; set; }
public int? Affected { get; set; }
public List<Dictionary<string, object?>>? Rows { get; set; }
public bool? DbConnected { get; set; }
public string? DbMessage { get; set; }
public long? RespondedAt { get; set; }
public int? ChunkIndex { get; set; }
public int? ChunkTotal { get; set; }
public bool? LastChunk { get; set; }
}

View File

@@ -0,0 +1,51 @@
using Microsoft.Data.SqlClient;
namespace YY.Admin.Services.Service.EquipmentDb;
public interface IEquipmentDbConnectionFactory
{
SqlConnection CreateConnection();
Task<(bool Ok, string Message)> TestAsync(CancellationToken cancellationToken = default);
EquipmentDbSettingsStore.EquipmentDbSettings CurrentSettings { get; }
void Reload();
}
public sealed class EquipmentDbConnectionFactory : IEquipmentDbConnectionFactory
{
private EquipmentDbSettingsStore.EquipmentDbSettings _settings = EquipmentDbSettingsStore.Load();
public EquipmentDbSettingsStore.EquipmentDbSettings CurrentSettings => _settings;
public void Reload()
{
_settings = EquipmentDbSettingsStore.Load();
}
public SqlConnection CreateConnection()
{
if (!_settings.Enabled)
{
throw new InvalidOperationException("设备库连接未启用");
}
return new SqlConnection(EquipmentDbSettingsStore.BuildConnectionString(_settings));
}
public async Task<(bool Ok, string Message)> TestAsync(CancellationToken cancellationToken = default)
{
try
{
Reload();
await using var conn = CreateConnection();
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = conn.CreateCommand();
cmd.CommandText = "SELECT 1";
cmd.CommandTimeout = Math.Max(5, _settings.ConnectTimeoutSeconds);
var scalar = await cmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
return (true, "连接成功: " + scalar);
}
catch (Exception ex)
{
return (false, ex.Message);
}
}
}

View File

@@ -0,0 +1,110 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;
using YY.Admin.Core.Util;
namespace YY.Admin.Services.Service.EquipmentDb;
/// <summary>
/// 设备库SQL Server 中间库)本地连接配置。
/// </summary>
public static class EquipmentDbSettingsStore
{
public class EquipmentDbSettings
{
public bool Enabled { get; set; } = true;
public string DeviceId { get; set; } = string.Empty;
public string ServerHost { get; set; } = "127.0.0.1";
public int ServerPort { get; set; } = 1433;
public string DbName { get; set; } = "MES_ShareDB";
public string Username { get; set; } = "sa";
public string Password { get; set; } = string.Empty;
public int ConnectTimeoutSeconds { get; set; } = 15;
}
public static string GetUserConfigPath()
{
var dir = AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.ConfigurationDirectory);
return Path.Combine(dir, "equipment-db.json");
}
public static EquipmentDbSettings Load()
{
var path = GetUserConfigPath();
if (!File.Exists(path))
{
return new EquipmentDbSettings
{
DeviceId = Environment.MachineName
};
}
try
{
var settings = JsonConvert.DeserializeObject<EquipmentDbSettings>(File.ReadAllText(path))
?? new EquipmentDbSettings();
if (string.IsNullOrWhiteSpace(settings.DeviceId))
{
settings.DeviceId = Environment.MachineName;
}
return settings;
}
catch
{
return new EquipmentDbSettings { DeviceId = Environment.MachineName };
}
}
public static void Save(EquipmentDbSettings settings)
{
if (settings == null)
{
throw new ArgumentNullException(nameof(settings));
}
if (string.IsNullOrWhiteSpace(settings.DeviceId))
{
settings.DeviceId = Environment.MachineName;
}
var path = GetUserConfigPath();
File.WriteAllText(path, JsonConvert.SerializeObject(settings, Formatting.Indented));
}
public static string BuildConnectionString(EquipmentDbSettings s)
{
var timeout = s.ConnectTimeoutSeconds > 0 ? s.ConnectTimeoutSeconds : 15;
return $"Server={s.ServerHost},{s.ServerPort};Database={s.DbName};User Id={s.Username};Password={s.Password};" +
$"Encrypt=False;TrustServerCertificate=True;Connect Timeout={timeout};";
}
/// <summary>
/// 从 bundled appsettings 的 EquipmentDb 节点合并默认值(若有)。
/// </summary>
public static EquipmentDbSettings LoadMergedWithAppSettings(JObject? root)
{
var settings = Load();
if (root?["EquipmentDb"] is not JObject eq)
{
return settings;
}
if (string.IsNullOrWhiteSpace(settings.ServerHost) || settings.ServerHost == "127.0.0.1")
{
settings.ServerHost = eq.Value<string>("ServerHost") ?? settings.ServerHost;
}
if (settings.ServerPort == 1433 && eq.Value<int?>("ServerPort") is int p)
{
settings.ServerPort = p;
}
if (string.IsNullOrWhiteSpace(settings.DbName) || settings.DbName == "MES_ShareDB")
{
settings.DbName = eq.Value<string>("DbName") ?? settings.DbName;
}
if (string.IsNullOrWhiteSpace(settings.Username) || settings.Username == "sa")
{
settings.Username = eq.Value<string>("Username") ?? settings.Username;
}
if (string.IsNullOrWhiteSpace(settings.Password))
{
settings.Password = eq.Value<string>("Password") ?? settings.Password;
}
return settings;
}
}

View File

@@ -0,0 +1,78 @@
using System.Text.RegularExpressions;
namespace YY.Admin.Services.Service.EquipmentDb;
/// <summary>
/// 设备库 SQL 白名单:仅允许访问 MCS 中间表白名单表。
/// </summary>
public static class EquipmentDbWhitelist
{
private static readonly HashSet<string> AllowedTables = new(StringComparer.OrdinalIgnoreCase)
{
"MCSToMES_Alarm", "MCSToMES_MixAlarm", "MCSToMES_MixAct", "MCSToMES_Act_Mill", "MCSToMES_MixBatch",
"MCSToMES_MixCon", "MCSToMES_MixCurve", "MCSToMES_MixStep", "MCSToMES_MixWeight",
"MCSToMES_MixExePlan", "MCSToMES_BinToMater", "MCSToMES_AutoXLLog", "MCSToMES_CheckScaleLog",
"MESToMCS_Material", "MESToMCS_MixPlan", "MESToMCS_Recipe",
"MESToMCS_Recipe_MixStep", "MESToMCS_Recipe_Weight"
};
private static readonly Regex Dangerous = new(
@"\b(DROP|ALTER|TRUNCATE|EXEC|EXECUTE|xp_|sp_|INTO\s+OUTFILE|OPENROWSET|OPENDATASOURCE)\b",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex TableRef = new(
@"\b(?:FROM|INTO|UPDATE|JOIN)\s+\[?(?<t>[A-Za-z0-9_]+)\]?",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static bool IsAllowedTable(string? table)
{
if (string.IsNullOrWhiteSpace(table))
{
return false;
}
return AllowedTables.Contains(table.Trim());
}
public static void ValidateSql(string? sql, string? tableHint = null)
{
if (string.IsNullOrWhiteSpace(sql))
{
throw new InvalidOperationException("SQL 不能为空");
}
if (Dangerous.IsMatch(sql))
{
throw new InvalidOperationException("SQL 包含禁止关键字");
}
var trimmed = sql.TrimStart();
var isSelect = trimmed.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase);
var isInsert = trimmed.StartsWith("INSERT", StringComparison.OrdinalIgnoreCase);
var isUpdate = trimmed.StartsWith("UPDATE", StringComparison.OrdinalIgnoreCase);
var isDelete = trimmed.StartsWith("DELETE", StringComparison.OrdinalIgnoreCase);
if (!(isSelect || isInsert || isUpdate || isDelete))
{
throw new InvalidOperationException("仅允许 SELECT/INSERT/UPDATE/DELETE");
}
if (!string.IsNullOrWhiteSpace(tableHint) && !IsAllowedTable(tableHint))
{
throw new InvalidOperationException($"表不在白名单: {tableHint}");
}
foreach (Match m in TableRef.Matches(sql))
{
var t = m.Groups["t"].Value;
if (string.Equals(t, "TOP", StringComparison.OrdinalIgnoreCase)
|| string.Equals(t, "SELECT", StringComparison.OrdinalIgnoreCase)
|| string.Equals(t, "SET", StringComparison.OrdinalIgnoreCase)
|| string.Equals(t, "VALUES", StringComparison.OrdinalIgnoreCase)
|| string.Equals(t, "WHERE", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (!IsAllowedTable(t))
{
throw new InvalidOperationException($"SQL 引用了未授权表: {t}");
}
}
}
}

View File

@@ -0,0 +1,125 @@
using System.Data;
using Microsoft.Data.SqlClient;
namespace YY.Admin.Services.Service.EquipmentDb;
/// <summary>
/// 开炼机动作中间表 MCSToMES_Act_Mill 只读查询(直连设备库)。
/// </summary>
public interface IMcsActMillService
{
Task<(IReadOnlyList<McsActMillRow> Records, long Total)> PageAsync(
int pageNo,
int pageSize,
string? actName = null,
int? actAddr = null,
CancellationToken cancellationToken = default);
}
public sealed class McsActMillRow
{
public string? Guid { get; set; }
public int? ActAddr { get; set; }
public string? ActName { get; set; }
public string? ActNameEn { get; set; }
public string? ActMemo { get; set; }
public int? ActRepaddr { get; set; }
public DateTime? WriteTime { get; set; }
public DateTime? ReadTime { get; set; }
public int? RwFlag { get; set; }
}
public sealed class McsActMillService : IMcsActMillService
{
private const string TableName = "MCSToMES_Act_Mill";
private readonly IEquipmentDbConnectionFactory _factory;
public McsActMillService(IEquipmentDbConnectionFactory factory)
{
_factory = factory;
}
public async Task<(IReadOnlyList<McsActMillRow> Records, long Total)> PageAsync(
int pageNo,
int pageSize,
string? actName = null,
int? actAddr = null,
CancellationToken cancellationToken = default)
{
if (pageNo < 1) pageNo = 1;
if (pageSize < 1) pageSize = 20;
_factory.Reload();
await using var conn = _factory.CreateConnection();
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
var where = new List<string>();
var parameters = new List<SqlParameter>();
if (!string.IsNullOrWhiteSpace(actName))
{
where.Add("act_name LIKE @actName");
parameters.Add(new SqlParameter("@actName", SqlDbType.NVarChar, 50) { Value = "%" + actName.Trim() + "%" });
}
if (actAddr.HasValue)
{
where.Add("act_addr = @actAddr");
parameters.Add(new SqlParameter("@actAddr", SqlDbType.Int) { Value = actAddr.Value });
}
var whereSql = where.Count == 0 ? "" : " WHERE " + string.Join(" AND ", where);
var countSql = $"SELECT COUNT(1) FROM [{TableName}]{whereSql}";
EquipmentDbWhitelist.ValidateSql(countSql, TableName);
long total;
await using (var countCmd = conn.CreateCommand())
{
countCmd.CommandText = countSql;
countCmd.Parameters.AddRange(CloneParams(parameters));
var scalar = await countCmd.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
total = Convert.ToInt64(scalar ?? 0);
}
var offset = (pageNo - 1) * pageSize;
var listSql = $@"
SELECT [GUID], [act_addr], [act_name], [act_name_en], [act_memo], [act_Repaddr], [WriteTime], [ReadTime], [RW_Flag]
FROM [{TableName}]{whereSql}
ORDER BY [WriteTime] DESC, [GUID]
OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY";
EquipmentDbWhitelist.ValidateSql(listSql, TableName);
var rows = new List<McsActMillRow>();
await using (var listCmd = conn.CreateCommand())
{
listCmd.CommandText = listSql;
listCmd.Parameters.AddRange(CloneParams(parameters));
listCmd.Parameters.Add(new SqlParameter("@offset", SqlDbType.Int) { Value = offset });
listCmd.Parameters.Add(new SqlParameter("@pageSize", SqlDbType.Int) { Value = pageSize });
await using var reader = await listCmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
rows.Add(new McsActMillRow
{
Guid = reader["GUID"] as string,
ActAddr = reader["act_addr"] == DBNull.Value ? null : Convert.ToInt32(reader["act_addr"]),
ActName = reader["act_name"] as string,
ActNameEn = reader["act_name_en"] as string,
ActMemo = reader["act_memo"] as string,
ActRepaddr = reader["act_Repaddr"] == DBNull.Value ? null : Convert.ToInt32(reader["act_Repaddr"]),
WriteTime = reader["WriteTime"] == DBNull.Value ? null : Convert.ToDateTime(reader["WriteTime"]),
ReadTime = reader["ReadTime"] == DBNull.Value ? null : Convert.ToDateTime(reader["ReadTime"]),
RwFlag = reader["RW_Flag"] == DBNull.Value ? null : Convert.ToInt32(reader["RW_Flag"]),
});
}
}
return (rows, total);
}
private static SqlParameter[] CloneParams(List<SqlParameter> source)
{
return source.Select(p => new SqlParameter(p.ParameterName, p.SqlDbType)
{
Size = p.Size,
Value = p.Value ?? DBNull.Value
}).ToArray();
}
}