=== 민감단온도(T_C) 전환복귀제어 (작업플랜 구현) ===
- FeedforwardModels: TempLowLimit, TcReturnRebTarget/Band, TcReturnDeltaAdRef/Band 추가
- FeedforwardEngine: sigTLow (T_C 하한 트리거, -1e9=비활성) + 온도기반 복귀게이트(tcRecovered)
-> Recovering→Returning 전이: mbRecovered(물질수지) OR tcRecovered(reb-A+ΔT+T_C)
- FeedRampCalculator: 하강 램프 전면 구현 (RateUpPerMin/RateDnPerMin 분리, θ_up/θ_dn 분기, floor clamp)
- FeedRampExecutorService: 하강 램프 step 방향 지원
- FeedforwardConfigStore: 신규 6개 컬럼 SELECT/INSERT/UPDATE
- Hc900DbContext: temp_low_limit, tc_return_reb_target/band, tc_return_delta_ad_ref/band
- FeedforwardController: API 노출 + feed-ramp start/cancel/status
=== SteamAdvisor ===
- SteamAdvisorController: steam map 로드/시각화/제품매칭/온도프로파일
- steam.js, steam.html: SteamAdvisor 전용 UI 패널
=== Feed Ramp 실행 ===
- FeedRampExecutorService: BG service (BackgroundService)
- FeedRampJobStore: in-memory job store
- FfTrackingStore: ramp tracking DB
- FeedforwardSupervisor/WriteGuard: SP 쓰기 advisory + rate-limit
=== 분석 스크립트 ===
- gen_temp_profiles.py: 컬럼 온도 프로파일 기준 산출 → c{prefix}_tempref.json
- export_plotdata.py: analysis 결과 plot data export
- gen_instrument_ranges.py: 계기 범위 생성
- c6111_extract.py: C-6111 추출/운전모드 분류
- run_column.py: 전체 분석 파이프라인
=== Web UI ===
- ff.js/ff.html/ff.css: 전환류 상태기계 UI, TagBrowser, config save
- fast.js: Fast 조작 패널
- trend.js, pb.js, llmchat.js: 각 패널 확장
326 lines
19 KiB
C#
326 lines
19 KiB
C#
using System.Data;
|
|
using System.Data.Common;
|
|
using Hc900Crawler.Core.Application.Feedforward;
|
|
using Hc900Crawler.Infrastructure.Database;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Hc900Crawler.Infrastructure.Control;
|
|
|
|
public sealed class FeedforwardConfigStore : IFeedforwardConfigStore
|
|
{
|
|
private readonly Hc900DbContext _ctx;
|
|
private readonly ILogger<FeedforwardConfigStore> _logger;
|
|
|
|
public FeedforwardConfigStore(Hc900DbContext ctx, ILogger<FeedforwardConfigStore> logger)
|
|
{ _ctx = ctx; _logger = logger; }
|
|
|
|
public async Task<IReadOnlyList<ColumnConfig>> LoadAllAsync(CancellationToken ct = default)
|
|
{
|
|
var conn = _ctx.Database.GetDbConnection();
|
|
if (conn.State != ConnectionState.Open) await conn.OpenAsync(ct);
|
|
|
|
var cols = new Dictionary<int, (ColumnConfig cfg, List<StreamConfig> streams)>();
|
|
await using (var cmd = conn.CreateCommand())
|
|
{
|
|
cmd.CommandText = """
|
|
SELECT id, name, enabled, feed_tag, pressure_tag, level_tags, scan_sec,
|
|
feed_filter_tau_sec, feed_move_thr_per_min,
|
|
press_filter_tau_sec, pressure_band, settle_sec,
|
|
stale_sec, product_key,
|
|
temp_tags, sensitive_tray_tag, dtdp, p_ref, steam_op_tag,
|
|
theta_auto_tune, bias_ma_window_sec,
|
|
recovery_enabled, recovery_auto_arm,
|
|
imbalance_trigger_frac, imbalance_trigger_sec,
|
|
recovery_settle_sec, return_ramp_sec, feed_recovery_sp,
|
|
delta_p_tag, delta_p_flood_limit,
|
|
advisory_only,
|
|
temp_high_limit, temp_low_limit,
|
|
tc_return_reb_target, tc_return_reb_band,
|
|
tc_return_delta_ad_ref, tc_return_delta_ad_band,
|
|
controller_id, feed_sp_node_id, feed_sp_min, feed_sp_max,
|
|
tc_return_tc_target, tc_return_tc_band
|
|
FROM ff_column_config
|
|
""";
|
|
await using var rd = await cmd.ExecuteReaderAsync(ct);
|
|
while (await rd.ReadAsync(ct))
|
|
{
|
|
var levelTags = rd.IsDBNull(5)
|
|
? Array.Empty<string>()
|
|
: rd.GetString(5)
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
var rawTempTags = rd.IsDBNull(14) ? null : rd.GetString(14);
|
|
var tempTags = rawTempTags is null
|
|
? Array.Empty<string>()
|
|
: rawTempTags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
var cfg = new ColumnConfig
|
|
{
|
|
Id = rd.GetInt32(0),
|
|
Name = rd.GetString(1),
|
|
Enabled = rd.GetBoolean(2),
|
|
AdvisoryOnly = rd.GetBoolean(30),
|
|
FeedTag = rd.GetString(3),
|
|
PressureTag = rd.IsDBNull(4) ? null : rd.GetString(4),
|
|
LevelTags = levelTags,
|
|
ScanSec = rd.GetDouble(6),
|
|
FeedFilterTauSec = rd.GetDouble(7),
|
|
FeedMoveThresholdPerMin = rd.GetDouble(8),
|
|
PressFilterTauSec = rd.GetDouble(9),
|
|
PressureBand = rd.GetDouble(10),
|
|
SettleSec = rd.GetDouble(11),
|
|
StaleSec = rd.GetDouble(12),
|
|
ProductKey = rd.GetString(13),
|
|
Streams = Array.Empty<StreamConfig>(),
|
|
TempTags = tempTags,
|
|
SensitiveTrayTag = rd.IsDBNull(15) ? null : rd.GetString(15),
|
|
DTdP = rd.GetDouble(16),
|
|
PRef = rd.IsDBNull(17) ? double.NaN : rd.GetDouble(17),
|
|
SteamOpTag = rd.IsDBNull(18) ? null : rd.GetString(18),
|
|
ThetaAutoTune = rd.GetBoolean(19),
|
|
BiasMaWindowSec = rd.GetDouble(20),
|
|
RecoveryEnabled = rd.GetBoolean(21),
|
|
RecoveryAutoArm = rd.GetBoolean(22),
|
|
ImbalanceTriggerFrac = rd.GetDouble(23),
|
|
ImbalanceTriggerSec = rd.GetDouble(24),
|
|
RecoverySettleSec = rd.GetDouble(25),
|
|
ReturnRampSec = rd.GetDouble(26),
|
|
FeedRecoverySp = rd.GetDouble(27),
|
|
DeltaPTag = rd.IsDBNull(28) ? null : rd.GetString(28),
|
|
DeltaPFloodLimit = rd.GetDouble(29),
|
|
TempHighLimit = rd.GetDouble(31),
|
|
TempLowLimit = rd.GetDouble(32),
|
|
TcReturnRebTarget = rd.IsDBNull(33) ? double.NaN : rd.GetDouble(33),
|
|
TcReturnRebBand = rd.GetDouble(34),
|
|
TcReturnDeltaAdRef = rd.IsDBNull(35) ? double.NaN : rd.GetDouble(35),
|
|
TcReturnDeltaAdBand = rd.GetDouble(36),
|
|
ControllerId = rd.IsDBNull(37) ? "C1" : rd.GetString(37),
|
|
FeedSpNodeId = rd.IsDBNull(38) ? null : rd.GetString(38),
|
|
FeedSpMin = rd.GetDouble(39),
|
|
FeedSpMax = rd.GetDouble(40),
|
|
TcReturnTcTarget = rd.IsDBNull(41) ? double.NaN : rd.GetDouble(41),
|
|
TcReturnTcBand = rd.GetDouble(42),
|
|
};
|
|
cols[cfg.Id] = (cfg, new List<StreamConfig>());
|
|
}
|
|
}
|
|
|
|
await using (var cmd = conn.CreateCommand())
|
|
{
|
|
cmd.CommandText = """
|
|
SELECT column_id, key, flow_tag, role, target_coeff, theta_up_sec, theta_dn_sec,
|
|
tau_sec, sp_min, sp_max, rate_up_per_min, rate_dn_per_min,
|
|
reflux_from_product, grade, level_tag,
|
|
is_reflux, recovery_sp, sp_node_id
|
|
FROM ff_stream_config
|
|
ORDER BY id
|
|
""";
|
|
await using var rd = await cmd.ExecuteReaderAsync(ct);
|
|
while (await rd.ReadAsync(ct))
|
|
{
|
|
int colId = rd.GetInt32(0);
|
|
if (!cols.TryGetValue(colId, out var entry)) continue;
|
|
entry.streams.Add(new StreamConfig
|
|
{
|
|
Key = rd.GetString(1),
|
|
FlowTag = rd.GetString(2),
|
|
Role = Enum.TryParse<StreamRole>(rd.GetString(3), true, out var role) ? role : StreamRole.Monitor,
|
|
LevelTag = rd.IsDBNull(14) ? null : rd.GetString(14),
|
|
IsReflux = rd.GetBoolean(15),
|
|
RecoverySp = rd.IsDBNull(16) ? double.NaN : rd.GetDouble(16),
|
|
SpNodeId = rd.IsDBNull(17) ? null : rd.GetString(17),
|
|
TargetCoeff = rd.GetDouble(4),
|
|
ThetaUpSec = rd.GetDouble(5),
|
|
ThetaDnSec = rd.GetDouble(6),
|
|
TauSec = rd.GetDouble(7),
|
|
SpMin = rd.GetDouble(8),
|
|
SpMax = rd.GetDouble(9),
|
|
RateUpPerMin = rd.GetDouble(10),
|
|
RateDnPerMin = rd.GetDouble(11),
|
|
RefluxFromProduct = rd.GetBoolean(12),
|
|
Grade = Enum.TryParse<Confidence>(rd.GetString(13), true, out var g) ? g : Confidence.A
|
|
});
|
|
}
|
|
}
|
|
|
|
return cols.Values.Select(e => e.cfg with { Streams = e.streams }).ToList();
|
|
}
|
|
|
|
private static DbParameter P(DbCommand cmd, string name, object? val)
|
|
{
|
|
var p = cmd.CreateParameter();
|
|
p.ParameterName = name;
|
|
p.Value = val ?? DBNull.Value;
|
|
cmd.Parameters.Add(p);
|
|
return p;
|
|
}
|
|
|
|
public async Task<int> SaveColumnAsync(ColumnConfig cfg, CancellationToken ct = default)
|
|
{
|
|
var conn = _ctx.Database.GetDbConnection();
|
|
if (conn.State != ConnectionState.Open) await conn.OpenAsync(ct);
|
|
await using var tx = await conn.BeginTransactionAsync(ct);
|
|
|
|
int id = cfg.Id;
|
|
var levelTags = string.Join(',', cfg.LevelTags);
|
|
|
|
if (id == 0)
|
|
{
|
|
await using var cmd = conn.CreateCommand();
|
|
cmd.Transaction = tx;
|
|
cmd.CommandText = """
|
|
INSERT INTO ff_column_config
|
|
(name, enabled, feed_tag, pressure_tag, level_tags, scan_sec,
|
|
feed_filter_tau_sec, feed_move_thr_per_min, press_filter_tau_sec,
|
|
pressure_band, settle_sec, stale_sec, product_key, advisory_only,
|
|
temp_tags, sensitive_tray_tag, dtdp, p_ref, steam_op_tag,
|
|
theta_auto_tune, bias_ma_window_sec,
|
|
recovery_enabled, recovery_auto_arm,
|
|
imbalance_trigger_frac, imbalance_trigger_sec,
|
|
recovery_settle_sec, return_ramp_sec, feed_recovery_sp,
|
|
delta_p_tag, delta_p_flood_limit, temp_high_limit, temp_low_limit,
|
|
tc_return_reb_target, tc_return_reb_band,
|
|
tc_return_delta_ad_ref, tc_return_delta_ad_band,
|
|
controller_id, feed_sp_node_id, feed_sp_min, feed_sp_max,
|
|
tc_return_tc_target, tc_return_tc_band)
|
|
VALUES (@name,@en,@feed,@pres,@lvl,@scan,@fft,@fmt,@pft,@pb,@settle,@stale,@pk,@advisory,
|
|
@tempTags,@sensTray,@dtdp,@pRef,@steamOp,
|
|
@thetaAuto,@biasMaWin,
|
|
@recEn,@recAutoArm,
|
|
@imbFrac,@imbSec,
|
|
@recSettle,@retRamp,@feedRecSp,
|
|
@deltaPTag,@deltaPFlood,@tempHigh,@tempLow,
|
|
@tcRetRebTgt,@tcRetRebBand,
|
|
@tcRetDeltaAdRef,@tcRetDeltaAdBand,
|
|
@ctrlId,@feedSpNode,@feedSpMin,@feedSpMax,
|
|
@tcRetTcTgt,@tcRetTcBand)
|
|
RETURNING id
|
|
""";
|
|
P(cmd,"@name",cfg.Name); P(cmd,"@en",cfg.Enabled); P(cmd,"@feed",cfg.FeedTag);
|
|
P(cmd,"@pres",(object?)cfg.PressureTag); P(cmd,"@lvl",levelTags);
|
|
P(cmd,"@advisory",cfg.AdvisoryOnly);
|
|
P(cmd,"@scan",cfg.ScanSec); P(cmd,"@fft",cfg.FeedFilterTauSec); P(cmd,"@fmt",cfg.FeedMoveThresholdPerMin);
|
|
P(cmd,"@pft",cfg.PressFilterTauSec); P(cmd,"@pb",cfg.PressureBand); P(cmd,"@settle",cfg.SettleSec);
|
|
P(cmd,"@stale",cfg.StaleSec); P(cmd,"@pk",cfg.ProductKey ?? "P");
|
|
P(cmd,"@tempTags",cfg.TempTags.Count > 0 ? string.Join(',', cfg.TempTags) : DBNull.Value);
|
|
P(cmd,"@sensTray",(object?)cfg.SensitiveTrayTag ?? DBNull.Value);
|
|
P(cmd,"@dtdp",cfg.DTdP); P(cmd,"@pRef",double.IsNaN(cfg.PRef) ? DBNull.Value : (object)cfg.PRef);
|
|
P(cmd,"@steamOp",(object?)cfg.SteamOpTag ?? DBNull.Value);
|
|
P(cmd,"@thetaAuto",cfg.ThetaAutoTune); P(cmd,"@biasMaWin",cfg.BiasMaWindowSec);
|
|
P(cmd,"@recEn",cfg.RecoveryEnabled); P(cmd,"@recAutoArm",cfg.RecoveryAutoArm);
|
|
P(cmd,"@imbFrac",cfg.ImbalanceTriggerFrac); P(cmd,"@imbSec",cfg.ImbalanceTriggerSec);
|
|
P(cmd,"@recSettle",cfg.RecoverySettleSec); P(cmd,"@retRamp",cfg.ReturnRampSec);
|
|
P(cmd,"@feedRecSp",cfg.FeedRecoverySp);
|
|
P(cmd,"@deltaPTag",(object?)cfg.DeltaPTag ?? DBNull.Value);
|
|
P(cmd,"@deltaPFlood",cfg.DeltaPFloodLimit);
|
|
P(cmd,"@tempHigh",cfg.TempHighLimit);
|
|
P(cmd,"@tempLow",cfg.TempLowLimit);
|
|
P(cmd,"@tcRetRebTgt",double.IsNaN(cfg.TcReturnRebTarget) ? DBNull.Value : (object)cfg.TcReturnRebTarget);
|
|
P(cmd,"@tcRetRebBand",cfg.TcReturnRebBand);
|
|
P(cmd,"@tcRetDeltaAdRef",double.IsNaN(cfg.TcReturnDeltaAdRef) ? DBNull.Value : (object)cfg.TcReturnDeltaAdRef);
|
|
P(cmd,"@tcRetDeltaAdBand",cfg.TcReturnDeltaAdBand);
|
|
P(cmd,"@ctrlId",string.IsNullOrWhiteSpace(cfg.ControllerId) ? "C1" : cfg.ControllerId);
|
|
P(cmd,"@feedSpNode",(object?)cfg.FeedSpNodeId ?? DBNull.Value);
|
|
P(cmd,"@feedSpMin",cfg.FeedSpMin); P(cmd,"@feedSpMax",cfg.FeedSpMax);
|
|
P(cmd,"@tcRetTcTgt",double.IsNaN(cfg.TcReturnTcTarget) ? DBNull.Value : (object)cfg.TcReturnTcTarget);
|
|
P(cmd,"@tcRetTcBand",cfg.TcReturnTcBand);
|
|
id = Convert.ToInt32(await cmd.ExecuteScalarAsync(ct));
|
|
}
|
|
else
|
|
{
|
|
await using var cmd = conn.CreateCommand();
|
|
cmd.Transaction = tx;
|
|
cmd.CommandText = """
|
|
UPDATE ff_column_config SET
|
|
name=@name, enabled=@en, feed_tag=@feed, pressure_tag=@pres, level_tags=@lvl,
|
|
scan_sec=@scan, feed_filter_tau_sec=@fft, feed_move_thr_per_min=@fmt,
|
|
press_filter_tau_sec=@pft, pressure_band=@pb, settle_sec=@settle,
|
|
stale_sec=@stale, product_key=@pk, advisory_only=@advisory,
|
|
temp_tags=@tempTags, sensitive_tray_tag=@sensTray, dtdp=@dtdp, p_ref=@pRef,
|
|
steam_op_tag=@steamOp, theta_auto_tune=@thetaAuto, bias_ma_window_sec=@biasMaWin,
|
|
recovery_enabled=@recEn, recovery_auto_arm=@recAutoArm,
|
|
imbalance_trigger_frac=@imbFrac, imbalance_trigger_sec=@imbSec,
|
|
recovery_settle_sec=@recSettle, return_ramp_sec=@retRamp, feed_recovery_sp=@feedRecSp,
|
|
delta_p_tag=@deltaPTag, delta_p_flood_limit=@deltaPFlood,
|
|
temp_high_limit=@tempHigh, temp_low_limit=@tempLow,
|
|
tc_return_reb_target=@tcRetRebTgt, tc_return_reb_band=@tcRetRebBand,
|
|
tc_return_delta_ad_ref=@tcRetDeltaAdRef, tc_return_delta_ad_band=@tcRetDeltaAdBand,
|
|
controller_id=@ctrlId, feed_sp_node_id=@feedSpNode, feed_sp_min=@feedSpMin, feed_sp_max=@feedSpMax,
|
|
tc_return_tc_target=@tcRetTcTgt, tc_return_tc_band=@tcRetTcBand
|
|
WHERE id=@id
|
|
""";
|
|
P(cmd,"@id",id); P(cmd,"@name",cfg.Name); P(cmd,"@en",cfg.Enabled);
|
|
P(cmd,"@feed",cfg.FeedTag); P(cmd,"@pres",(object?)cfg.PressureTag);
|
|
P(cmd,"@advisory",cfg.AdvisoryOnly);
|
|
P(cmd,"@lvl",levelTags); P(cmd,"@scan",cfg.ScanSec); P(cmd,"@fft",cfg.FeedFilterTauSec);
|
|
P(cmd,"@fmt",cfg.FeedMoveThresholdPerMin); P(cmd,"@pft",cfg.PressFilterTauSec); P(cmd,"@pb",cfg.PressureBand);
|
|
P(cmd,"@settle",cfg.SettleSec); P(cmd,"@stale",cfg.StaleSec); P(cmd,"@pk",cfg.ProductKey ?? "P");
|
|
P(cmd,"@tempTags",cfg.TempTags.Count > 0 ? string.Join(',', cfg.TempTags) : DBNull.Value);
|
|
P(cmd,"@sensTray",(object?)cfg.SensitiveTrayTag ?? DBNull.Value);
|
|
P(cmd,"@dtdp",cfg.DTdP); P(cmd,"@pRef",double.IsNaN(cfg.PRef) ? DBNull.Value : (object)cfg.PRef);
|
|
P(cmd,"@steamOp",(object?)cfg.SteamOpTag ?? DBNull.Value);
|
|
P(cmd,"@thetaAuto",cfg.ThetaAutoTune); P(cmd,"@biasMaWin",cfg.BiasMaWindowSec);
|
|
P(cmd,"@recEn",cfg.RecoveryEnabled); P(cmd,"@recAutoArm",cfg.RecoveryAutoArm);
|
|
P(cmd,"@imbFrac",cfg.ImbalanceTriggerFrac); P(cmd,"@imbSec",cfg.ImbalanceTriggerSec);
|
|
P(cmd,"@recSettle",cfg.RecoverySettleSec); P(cmd,"@retRamp",cfg.ReturnRampSec);
|
|
P(cmd,"@feedRecSp",cfg.FeedRecoverySp);
|
|
P(cmd,"@deltaPTag",(object?)cfg.DeltaPTag ?? DBNull.Value);
|
|
P(cmd,"@deltaPFlood",cfg.DeltaPFloodLimit);
|
|
P(cmd,"@tempHigh",cfg.TempHighLimit);
|
|
P(cmd,"@tempLow",cfg.TempLowLimit);
|
|
P(cmd,"@tcRetRebTgt",double.IsNaN(cfg.TcReturnRebTarget) ? DBNull.Value : (object)cfg.TcReturnRebTarget);
|
|
P(cmd,"@tcRetRebBand",cfg.TcReturnRebBand);
|
|
P(cmd,"@tcRetDeltaAdRef",double.IsNaN(cfg.TcReturnDeltaAdRef) ? DBNull.Value : (object)cfg.TcReturnDeltaAdRef);
|
|
P(cmd,"@tcRetDeltaAdBand",cfg.TcReturnDeltaAdBand);
|
|
P(cmd,"@ctrlId",string.IsNullOrWhiteSpace(cfg.ControllerId) ? "C1" : cfg.ControllerId);
|
|
P(cmd,"@feedSpNode",(object?)cfg.FeedSpNodeId ?? DBNull.Value);
|
|
P(cmd,"@feedSpMin",cfg.FeedSpMin); P(cmd,"@feedSpMax",cfg.FeedSpMax);
|
|
P(cmd,"@tcRetTcTgt",double.IsNaN(cfg.TcReturnTcTarget) ? DBNull.Value : (object)cfg.TcReturnTcTarget);
|
|
P(cmd,"@tcRetTcBand",cfg.TcReturnTcBand);
|
|
await cmd.ExecuteNonQueryAsync(ct);
|
|
}
|
|
|
|
// 스트림 원자적 교체
|
|
await using (var del = conn.CreateCommand())
|
|
{
|
|
del.Transaction = tx; del.CommandText = "DELETE FROM ff_stream_config WHERE column_id=@id";
|
|
P(del,"@id",id); await del.ExecuteNonQueryAsync(ct);
|
|
}
|
|
foreach (var s in cfg.Streams)
|
|
{
|
|
await using var ins = conn.CreateCommand();
|
|
ins.Transaction = tx;
|
|
ins.CommandText = """
|
|
INSERT INTO ff_stream_config
|
|
(column_id, key, flow_tag, role, target_coeff, theta_up_sec, theta_dn_sec, tau_sec,
|
|
sp_min, sp_max, rate_up_per_min, rate_dn_per_min, reflux_from_product, grade, level_tag,
|
|
is_reflux, recovery_sp, sp_node_id)
|
|
VALUES (@cid,@key,@flow,@role,@k,@tup,@tdn,@tau,@smin,@smax,@rup,@rdn,@rfp,@grade,@lvlTag,
|
|
@isReflux,@recSp,@spNode)
|
|
""";
|
|
P(ins,"@cid",id); P(ins,"@key",s.Key); P(ins,"@flow",s.FlowTag);
|
|
P(ins,"@role",s.Role.ToString()); P(ins,"@lvlTag",(object?)s.LevelTag ?? DBNull.Value); P(ins,"@k",s.TargetCoeff); P(ins,"@tup",s.ThetaUpSec);
|
|
P(ins,"@tdn",s.ThetaDnSec); P(ins,"@tau",s.TauSec); P(ins,"@smin",s.SpMin); P(ins,"@smax",s.SpMax);
|
|
P(ins,"@rup",s.RateUpPerMin); P(ins,"@rdn",s.RateDnPerMin); P(ins,"@rfp",s.RefluxFromProduct);
|
|
P(ins,"@grade",s.Grade.ToString());
|
|
P(ins,"@isReflux",s.IsReflux); P(ins,"@recSp",double.IsNaN(s.RecoverySp) ? DBNull.Value : (object)s.RecoverySp);
|
|
P(ins,"@spNode",(object?)s.SpNodeId ?? DBNull.Value);
|
|
await ins.ExecuteNonQueryAsync(ct);
|
|
}
|
|
|
|
await tx.CommitAsync(ct);
|
|
return id;
|
|
}
|
|
|
|
public async Task DeleteColumnAsync(int columnId, CancellationToken ct = default)
|
|
{
|
|
var conn = _ctx.Database.GetDbConnection();
|
|
if (conn.State != ConnectionState.Open) await conn.OpenAsync(ct);
|
|
await using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "DELETE FROM ff_column_config WHERE id=@id";
|
|
P(cmd,"@id",columnId);
|
|
await cmd.ExecuteNonQueryAsync(ct);
|
|
}
|
|
}
|