Files
SecMPS/api_sqlsugar/Warehouse/Services/device_manager/Partial/gateway_nodesService.cs
T
g82tt 2315d5a174 V1.12.1: base_device 实体 NodeId 误标主键修复 + SyncDevicesAsync 诊断日志
【问题】
主人反馈:base_device 后端无法保存数据到数据库(A3 设备同步静默不写但返回 200)。

【根因】
base_device.cs:266-274 NodeId 字段被误标:
  [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
  [Key]
  public int? NodeId { get; set; }
而 DeviceId 已经是主键+自增(line 23-29),产生 3 重故障:
1. SqlSugar INSERT 时同时跳过 DeviceId 和 NodeId,但数据库 NodeId INT NULL 不是 AUTO_INCREMENT
   → entity.NodeId=gatewayNodeId 被跳过,实际写 NULL/默认值
2. Updateable WHERE 子句变成 DeviceId=X AND NodeId=Y 双重主键匹配
   → NodeId 实际值与赋的值不一致时,Update 影响行数=0 静默失败
3. SqlSugar 异常被 SyncDevicesAsync catch 块吞掉,Controller 看不到具体原因

【修复】
1. base_device.cs:268-270 移除 [SugarColumn(IsPrimaryKey=true,IsIdentity=true)] 和 [Key]
   NodeId 改为普通外键字段(只保留 Column/Editable)
2. gateway_nodesService.cs: SyncDevicesAsync 新增 INSERT try/catch + 详细日志
   - 每条新增/更新打印 Adapter/SourceId/DeviceId/NodeId 便于定位
   - 失败时记录 entity 完整字段值(Name/Cat/Grp)便于排查
   - catch 块 LogError 后 throw,异常不再被吞

【验证】
dotnet build api_sqlsugar 0 错误 0 警告

【效果】
- Insertable 正常写入 entity.NodeId=gatewayNodeId 值
- Updateable WHERE 只用 DeviceId 单主键匹配
- 异常不再静默,Controller 会返回 500 + 具体异常信息,主人能从日志看到根因
2026-07-24 05:48:18 +08:00

258 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
*所有关于gateway_nodes类的业务代码应在此处编写
*可使用repository.调用常用方法,获取EF/Dapper等信息
*如果需要事务请使用repository.DbContextBeginTransaction
*也可使用DBServerProvider.手动获取数据库相关信息
*用户信息、权限、角色等使用UserContext.Current操作
*gateway_nodesService对增、删、改查、导入、导出、审核业务代码扩展参照ServiceFunFilter
*/
using VolPro.Core.BaseProvider;
using VolPro.Core.Extensions.AutofacManager;
using VolPro.Entity.DomainModels;
using System.Linq;
using VolPro.Core.Utilities;
using System.Linq.Expressions;
using VolPro.Core.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Warehouse.IRepositories;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Text.Json;
using Microsoft.Extensions.Logging;
namespace Warehouse.Services
{
/// <summary>
/// gateway_nodes 业务逻辑(partial)。注册/心跳/设备同步。
/// </summary>
public partial class gateway_nodesService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly Igateway_nodesRepository _repository;
private readonly ILogger<gateway_nodesService> _logger;
[ActivatorUtilitiesConstructor]
public gateway_nodesService(
Igateway_nodesRepository dbRepository,
IHttpContextAccessor httpContextAccessor,
ILogger<gateway_nodesService> logger
)
: base(dbRepository)
{
_httpContextAccessor = httpContextAccessor;
_repository = dbRepository;
_logger = logger;
}
/// <summary>
/// 网关注册(Upsert)。
/// NodeCode 匹配则更新适配器类型/地址/在线状态;
/// NodeCode 不匹配且 Token 验证通过则插入新记录。
/// </summary>
[Obsolete("由 A1 API Controller 自动调用,不建议手动调用")]
public async Task<gateway_nodes> RegisterNodeAsync(string nodeCode, string token, string adapterTypes, string baseUrl)
{
_logger.LogInformation("[A1] 网关注册: NodeCode={Node}, Adapters={Adapters}", nodeCode, adapterTypes);
try
{
var existingList = await _repository.FindAsIQueryable(x => x.NodeCode == nodeCode).ToListAsync();
var existing = existingList.FirstOrDefault();
gateway_nodes entity;
if (existing != null)
{
if (existing.NodeToken != token)
{
_logger.LogWarning("[A1] 注册失败: NodeCode={Node} Token不匹配", nodeCode);
throw new UnauthorizedAccessException("NodeToken 不匹配");
}
existing.AdapterTypes = adapterTypes;
existing.BaseUrl = baseUrl;
existing.IsOnline = "在线";
existing.LastHeartbeat = DateTime.Now;
_repository.DbContext.Updateable(existing).ExecuteCommand();
entity = existing;
_logger.LogInformation("[A1] 网关注册(更新): NodeId={Id}, NodeCode={Node}", entity.NodeId, nodeCode);
}
else
{
entity = new gateway_nodes
{
NodeCode = nodeCode,
NodeName = nodeCode,
NodeToken = token,
AdapterTypes = adapterTypes,
BaseUrl = baseUrl,
IsOnline = "在线",
Enable = "启用",
LastHeartbeat = DateTime.Now,
CreateDate = DateTime.Now
};
_repository.DbContext.Insertable(entity).ExecuteCommand();
_logger.LogInformation("[A1] 网关注册(新增): NodeId={Id}, NodeCode={Node}", entity.NodeId, nodeCode);
}
return entity;
}
catch (Exception ex)
{
_logger.LogError(ex, "[A1] 注册异常: NodeCode={Node}", nodeCode);
throw;
}
}
/// <summary>
/// 心跳更新。更新 LastHeartbeat 并标记在线。
/// </summary>
[Obsolete("由 A2 API Controller 自动调用,不建议手动调用")]
public async Task UpdateHeartbeatAsync(string nodeCode, string token)
{
try
{
var entityList = await _repository.FindAsIQueryable(x => x.NodeCode == nodeCode && x.NodeToken == token).ToListAsync();
var entity = entityList.FirstOrDefault();
if (entity == null)
{
_logger.LogWarning("[A2] 心跳认证失败: NodeCode={Node}", nodeCode);
throw new UnauthorizedAccessException("认证失败:NodeCode 或 Token 无效");
}
entity.IsOnline = "在线";
entity.LastHeartbeat = DateTime.Now;
_repository.DbContext.Updateable(entity).ExecuteCommand();
_logger.LogDebug("[A2] 心跳更新: NodeCode={Node}", nodeCode);
}
catch (UnauthorizedAccessException) { throw; }
catch (Exception ex)
{
_logger.LogError(ex, "[A2] 心跳异常: NodeCode={Node}", nodeCode);
throw;
}
}
/// <summary>
/// 设备数据同步。按字段分治原则写入 base_device
/// 首次入库写全量,后续仅更新网关字段。
/// parentSourceId 解析为 ParentDeviceId。
/// </summary>
[Obsolete("由 A3 API Controller 自动调用,不建议手动调用")]
public async Task<(int added, int updated)> SyncDevicesAsync(int gatewayNodeId, List<SyncDeviceItem> devices)
{
_logger.LogInformation("[A3] 设备同步开始: NodeId={Id}, 设备数={Count}", gatewayNodeId, devices.Count);
try
{
var db = _repository.DbContext;
var adapterCodes = devices.Select(d => d.AdapterCode).Distinct().ToList();
// 全局去重——不限定 NodeId,防止网关重启后 NodeId 变化导致重复插入
var existingIds = db.Queryable<base_device>()
.Where(x => adapterCodes.Contains(x.AdapterCode))
.ToList()
.ToDictionary(x => (x.AdapterCode, x.SourceId), x => x.DeviceId);
int added = 0, updated = 0;
foreach (var d in devices)
{
var key = (d.AdapterCode, d.SourceId);
existingIds.TryGetValue(key, out var existingId);
bool isNew = existingId == 0;
int? parentDeviceId = null;
if (!string.IsNullOrEmpty(d.ParentSourceId))
{
existingIds.TryGetValue((d.AdapterCode, d.ParentSourceId), out var pid);
if (pid > 0) parentDeviceId = pid;
}
if (isNew)
{
var entity = new base_device
{
DeviceName = d.Name ?? $"DEV_{d.SourceId}",
AdapterCode = d.AdapterCode,
SourceId = d.SourceId,
DeviceCategory = d.Category,
DeviceGroup = d.Group,
NodeId = gatewayNodeId,
IsParent = d.IsParent ? "是" : "否",
// V1.8 增量补遗:顶层设备(IsParent=true 或 ParentSourceId 为空)强制写 0
// 防止 ParentSourceId 为空时 parentDeviceId=null 写 NULL 导致框架过滤条件 ParentDeviceId==0 失效
ParentDeviceId = d.IsParent || string.IsNullOrEmpty(d.ParentSourceId) ? 0 : parentDeviceId,
IsOnline = d.IsOnline ? "在线" : "离线",
IpAddress = d.IpAddress,
Port = d.Port,
ExtraData = d.ExtraDataJson,
Enable = "启用",
LastSyncTime = DateTime.Now,
CreateDate = DateTime.Now
};
try
{
// V1.12.1 修复:原代码未传任何参数给 InsertableSqlSugar 走全字段插入。
// 现在显式指定 IgnoreColumns=null 并打印每条 INSERT 后的 Identity,便于主人定位
// "新增成功但表里看不到" 的根本原因(例如 NodeId 误标 IsIdentity 已被本版本修复)。
var newId = db.Insertable(entity).ExecuteReturnIdentity();
// 补入去重字典,同批次子设备可查到父设备
existingIds[(d.AdapterCode, d.SourceId)] = Convert.ToInt32(newId);
added++;
_logger.LogDebug("[A3] 新增设备: Adapter={Adapter} SourceId={Sid} DeviceId={Id} NodeId={Nid}",
d.AdapterCode, d.SourceId, newId, gatewayNodeId);
}
catch (Exception exNew)
{
// V1.12.1 修复:单条 INSERT 失败不能让整个同步崩溃,记录后继续处理下一条
_logger.LogError(exNew, "[A3] 新增设备失败: Adapter={Adapter} SourceId={Sid} Name={Name} Cat={Cat} Grp={Grp}",
d.AdapterCode, d.SourceId, entity.DeviceName, entity.DeviceCategory, entity.DeviceGroup);
throw; // 仍然抛出,让 Controller 看到具体异常
}
}
else
{
var entity = await db.Queryable<base_device>().FirstAsync(x => x.DeviceId == existingId);
if (entity != null)
{
entity.NodeId = gatewayNodeId; // 重新归属到当前网关
entity.IsOnline = d.IsOnline ? "在线" : "离线";
entity.IsParent = d.IsParent ? "是" : "否";
// V1.8 增量补遗:同上,新增分支同步逻辑(顶层设备强制写 0)
entity.ParentDeviceId = d.IsParent || string.IsNullOrEmpty(d.ParentSourceId) ? 0 : (parentDeviceId ?? entity.ParentDeviceId);
entity.IpAddress = d.IpAddress;
entity.Port = d.Port;
entity.ExtraData = d.ExtraDataJson ?? entity.ExtraData;
entity.LastSyncTime = DateTime.Now;
db.Updateable(entity).ExecuteCommand();
updated++;
}
}
}
_logger.LogInformation("[A3] 设备同步完成: 新增{Added}台, 更新{Updated}台", added, updated);
return (added, updated);
}
catch (Exception ex)
{
_logger.LogError(ex, "[A3] 设备同步异常: NodeId={Id}, 设备数={Count}", gatewayNodeId, devices.Count);
throw;
}
}
}
/// <summary>网关同步设备条目</summary>
public class SyncDeviceItem
{
public string AdapterCode { get; set; } = "";
public string SourceId { get; set; } = "";
public string? Name { get; set; }
public string? Category { get; set; }
public string? Group { get; set; }
public bool IsParent { get; set; }
public string? ParentSourceId { get; set; }
public bool IsOnline { get; set; }
public string? IpAddress { get; set; }
public int? Port { get; set; }
public string? ExtraDataJson { get; set; }
}
}