using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; namespace DRG.DRGInterfaceClient { public partial class MainForm : Form { private TextBox txtEndpoint; private Label lblHosp; private CheckedListBox clbHospital; private Button btnLoadHosp; private Button btnSelectAll; private Button btnSelectNone; private Label lblSelectedCount; private Button btnLoadConfig; private Button btnSyncNow; private Button btnStartSchedule; private Button btnStopSchedule; private Button btnRegisterTask; private Button btnSetScheduledHosp; private Button btnOpenLog; private Button btnClearLog; private GroupBox grpConfig; private Dictionary cfgLabelMap; private TextBox txtLog; private TrayIcon tray; /// 勾选列表中最后一个勾选的机构,用于配置预览面板与 session。 private HospitalInfoItem _previewHosp; private Dictionary _session; private SyncConfig _syncConfig; private SchedulerService _scheduler; private bool _exiting; private bool _bulkUpdating; public MainForm() { InitializeComponent(); _scheduler = new SchedulerService(this); Logger.OnLog += Logger_OnLog; tray = new TrayIcon(this); Load += (s, e) => { txtEndpoint.Text = Config.Current.Endpoint; _ = LoadHospitalsAsync(); }; FormClosing += MainForm_FormClosing; Disposed += (s, e) => { Logger.OnLog -= Logger_OnLog; tray?.Dispose(); }; } // ===== UI 构建 ===== private void InitializeComponent() { Text = "DRG 接口客户端"; Size = new Size(760, 700); StartPosition = FormStartPosition.CenterScreen; MinimumSize = new Size(640, 480); var lblEp = new Label { Left = 8, Top = 15, Width = 76, Text = "接口地址:" }; txtEndpoint = new TextBox { Left = 90, Top = 12, Width = 640, Text = "http://127.0.0.1:52773/csp/drg/sysInternalMutiple" }; lblHosp = new Label { Left = 8, Top = 47, Width = 76, Text = "医疗机构:" }; clbHospital = new CheckedListBox { Left = 90, Top = 44, Width = 360, Height = 90, CheckOnClick = true, DisplayMember = "descripts" }; btnLoadHosp = new Button { Left = 460, Top = 42, Width = 100, Text = "加载机构" }; btnSelectAll = new Button { Left = 460, Top = 78, Width = 48, Text = "全选" }; btnSelectNone = new Button { Left = 512, Top = 78, Width = 48, Text = "全否" }; lblSelectedCount = new Label { Left = 566, Top = 80, Width = 170, Text = "已选 0 家" }; grpConfig = new GroupBox { Left = 8, Top = 148, Width = 724, Height = 110, Text = "同步配置(选中机构预览)" }; string[] names = { "syncType", "frequency", "scheduledTime", "lookbackDays", "enabled", "synFlag", "warningFlag", "status" }; cfgLabelMap = new Dictionary(); for (int i = 0; i < names.Length; i++) { int col = i % 4; int row = i / 4; var l = new Label { Left = 12 + col * 180, Top = 22 + row * 40, Width = 170, Height = 32, Text = (FieldNames.TryGetValue(names[i], out var fn) ? fn : names[i]) + ": (未加载)", BorderStyle = BorderStyle.FixedSingle, BackColor = SystemColors.ControlLight }; cfgLabelMap[names[i]] = l; grpConfig.Controls.Add(l); } btnLoadConfig = new Button { Left = 8, Top = 272, Width = 110, Text = "加载配置" }; btnSyncNow = new Button { Left = 124, Top = 272, Width = 110, Text = "立即执行一次" }; btnStartSchedule = new Button { Left = 240, Top = 272, Width = 110, Text = "开始调度" }; btnStopSchedule = new Button { Left = 356, Top = 272, Width = 110, Text = "停止调度" }; btnRegisterTask = new Button { Left = 472, Top = 272, Width = 120, Text = "注册任务计划" }; btnSetScheduledHosp = new Button { Left = 598, Top = 272, Width = 134, Text = "设为定时机构" }; btnOpenLog = new Button { Left = 8, Top = 308, Width = 110, Text = "打开日志" }; btnClearLog = new Button { Left = 124, Top = 308, Width = 110, Text = "清空日志" }; txtLog = new TextBox { Left = 8, Top = 344, Width = 724, Height = 320, Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Vertical, Font = new Font("Consolas", 9) }; Controls.AddRange(new Control[] { lblEp, txtEndpoint, lblHosp, clbHospital, btnLoadHosp, btnSelectAll, btnSelectNone, lblSelectedCount, grpConfig, btnLoadConfig, btnSyncNow, btnStartSchedule, btnStopSchedule, btnRegisterTask, btnSetScheduledHosp, btnOpenLog, btnClearLog, txtLog }); btnLoadHosp.Click += async (s, e) => await LoadHospitalsAsync(); btnLoadConfig.Click += async (s, e) => await LoadPreviewConfigAsync(_previewHosp); btnSyncNow.Click += async (s, e) => await DoSyncAsync(); btnStartSchedule.Click += (s, e) => StartSchedule(); btnStopSchedule.Click += (s, e) => StopSchedule(); btnRegisterTask.Click += async (s, e) => await RegisterTaskNow(); btnSetScheduledHosp.Click += (s, e) => SetScheduledHosp(); btnOpenLog.Click += (s, e) => Logger.OpenLogFile(); btnClearLog.Click += (s, e) => { Logger.Clear(); txtLog.Clear(); }; btnSelectAll.Click += (s, e) => SelectAllHospitals(true); btnSelectNone.Click += (s, e) => SelectAllHospitals(false); clbHospital.ItemCheck += (s, e) => OnHospitalItemCheck(); } // ===== 校验 ===== private bool EnsureEndpoint() { string ep = txtEndpoint.Text.Trim(); if (string.IsNullOrEmpty(ep)) { MessageBox.Show("接口地址不能为空", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); txtEndpoint.Focus(); return false; } Config.Current.Endpoint = ep; return true; } private bool EnsureHospitals(out List list) { list = GetCheckedHospitals(); if (list.Count == 0) { MessageBox.Show("请至少勾选一家医疗机构", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); clbHospital.Focus(); return false; } return true; } // ===== 机构勾选 ===== private List GetCheckedHospitals() { var list = new List(); for (int i = 0; i < clbHospital.Items.Count; i++) if (clbHospital.GetItemChecked(i) && clbHospital.Items[i] is HospitalInfoItem h) list.Add(h); return list; } private HospitalInfoItem GetLastCheckedHospital() { HospitalInfoItem last = null; for (int i = 0; i < clbHospital.Items.Count; i++) if (clbHospital.GetItemChecked(i) && clbHospital.Items[i] is HospitalInfoItem h) last = h; return last; } private void SelectAllHospitals(bool check) { _bulkUpdating = true; for (int i = 0; i < clbHospital.Items.Count; i++) clbHospital.SetItemChecked(i, check); _bulkUpdating = false; RefreshSelectionUI(); } /// 勾选变化后刷新「已选 N 家」与配置预览。ItemCheck 时控件状态尚未提交,故用 BeginInvoke 延后读取。 private void OnHospitalItemCheck() { if (_bulkUpdating) return; clbHospital.BeginInvoke(new Action(RefreshSelectionUI)); } private void RefreshSelectionUI() { UpdateSelectedCount(); _previewHosp = GetLastCheckedHospital(); if (_previewHosp != null) { _session = SessionBuilder.Build(_previewHosp, Config.Current.SessionDefaults); _ = LoadPreviewConfigAsync(_previewHosp); } else { _session = null; _syncConfig = null; ClearConfigDisplay(); } } private void UpdateSelectedCount() { if (lblSelectedCount.InvokeRequired) { lblSelectedCount.Invoke(new Action(UpdateSelectedCount)); return; } lblSelectedCount.Text = "已选 " + GetCheckedHospitals().Count + " 家"; } // ===== 业务动作 ===== private async System.Threading.Tasks.Task LoadHospitalsAsync() { if (!EnsureEndpoint()) return; var resp = await DrgApiClient.QueryHospitalInfoAsync("", "", SessionBuilder.BuildDefault(Config.Current.SessionDefaults)); if (!DrgApiClient.IsOk(resp)) { MessageBox.Show("加载机构失败:" + DrgApiClient.GetError(resp), "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } var arr = DrgApiClient.GetResultArray(resp); clbHospital.Items.Clear(); if (arr == null || arr.Length == 0) { MessageBox.Show("未查询到医疗机构", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); UpdateSelectedCount(); return; } var scheduled = Config.Current.ScheduledHospCodes ?? new List(); _bulkUpdating = true; foreach (var o in arr) { if (o is Dictionary d) { var h = DrgApiClient.ParseHospital(d); int idx = clbHospital.Items.Add(h); if (scheduled.Contains(h.code, StringComparer.OrdinalIgnoreCase)) clbHospital.SetItemChecked(idx, true); } } _bulkUpdating = false; RefreshSelectionUI(); } /// 加载指定机构的 02010079 配置用于预览面板(不弹窗,仅日志)。 private async System.Threading.Tasks.Task LoadPreviewConfigAsync(HospitalInfoItem hosp) { if (hosp == null) { ClearConfigDisplay(); return; } if (!EnsureEndpoint()) return; var session = SessionBuilder.Build(hosp, Config.Current.SessionDefaults); var resp = await DrgApiClient.GetSyncConfigAsync(hosp.hospCode, session); if (!DrgApiClient.IsOk(resp)) { Logger.Error(string.Format("预览配置加载失败(机构 {0}):{1}", hosp.descripts, DrgApiClient.GetError(resp))); ClearConfigDisplay(); return; } _syncConfig = DrgApiClient.ParseSyncConfig(DrgApiClient.GetResult(resp)); UpdateConfigDisplay(_syncConfig); } /// 对勾选的所有机构顺序执行 02010067 同步(每家各自取 02010079 配置)。 public async System.Threading.Tasks.Task DoSyncAsync() { if (InvokeRequired) { Invoke(new Action(() => _ = DoSyncAsync())); return; } if (!EnsureEndpoint()) return; if (!EnsureHospitals(out var list)) return; int ok = 0, fail = 0; foreach (var hosp in list) { var session = SessionBuilder.Build(hosp, Config.Current.SessionDefaults); var cfgResp = await DrgApiClient.GetSyncConfigAsync(hosp.hospCode, session); if (!DrgApiClient.IsOk(cfgResp)) { Logger.Error(string.Format("机构 {0} 配置加载失败:{1}", hosp.descripts, DrgApiClient.GetError(cfgResp))); fail++; continue; } var cfg = DrgApiClient.ParseSyncConfig(DrgApiClient.GetResult(cfgResp)); var (st, ed) = DrgApiClient.ComputeDateRange( cfg.syncType, cfg.lookbackDays, Config.Current.FullSyncStartDays); Logger.Info(string.Format("开始同步 机构={0} syncType={1} 区间[{2},{3}]", hosp.descripts, cfg.syncType, st, ed)); var resp = await DrgApiClient.GetInpatientDataAsync( hosp.hospCode, st, ed, cfg.synFlag ?? "Y", session); if (DrgApiClient.IsOk(resp)) { Logger.Info(string.Format("机构 {0} 同步成功", hosp.descripts)); ok++; } else { Logger.Error(string.Format("机构 {0} 同步失败:{1}", hosp.descripts, DrgApiClient.GetError(resp))); fail++; } } Logger.Info(string.Format("批量同步完成:成功 {0} 家,失败 {1} 家(共 {2} 家)", ok, fail, list.Count)); } /// 供 SchedulerService 在整点调用(同步所有勾选机构)。 public void TriggerSync() { _ = DoSyncAsync(); } private void StartSchedule() { if (!EnsureHospitals(out var list)) return; // 多机构统一采用程序内整点轮询(覆盖所有勾选机构);daily/weekly 外部定时请用「注册任务计划」。 _scheduler.StartHourly(); tray?.ShowBalloon("已启动内部整点轮询(覆盖 " + list.Count + " 家机构)"); } private void StopSchedule() { _scheduler.Stop(); TaskSchedulerHelper.Delete(Config.Current.TaskName); tray?.ShowBalloon("已停止调度"); } private async System.Threading.Tasks.Task RegisterTaskNow() { if (!EnsureHospitals(out var list)) return; // 取每家配置,决定任务频率(任一为 weekly 则 WEEKLY,否则 DAILY),计划时间取首个。 string freq = "daily"; string scheduledTime = ""; foreach (var hosp in list) { var session = SessionBuilder.Build(hosp, Config.Current.SessionDefaults); var cfgResp = await DrgApiClient.GetSyncConfigAsync(hosp.hospCode, session); if (!DrgApiClient.IsOk(cfgResp)) continue; var cfg = DrgApiClient.ParseSyncConfig(DrgApiClient.GetResult(cfgResp)); if (cfg.frequency == "weekly") freq = "weekly"; if (string.IsNullOrEmpty(scheduledTime)) scheduledTime = cfg.scheduledTime; } string exe = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "DRGInterfaceClient.exe"); TaskSchedulerHelper.Register(Config.Current.TaskName, exe, freq, scheduledTime); MessageBox.Show(string.Format("已注册计划任务({0},覆盖 {1} 家机构):{2}", freq == "weekly" ? "每周" : "每日", list.Count, Config.Current.TaskName), "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } private void SetScheduledHosp() { if (!EnsureHospitals(out var list)) return; Config.Current.ScheduledHospCodes = list.ConvertAll(h => h.code); Config.Save(); MessageBox.Show(string.Format("已设为定时机构({0} 家)", list.Count), "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } // ===== 配置展示(中文) ===== private static readonly Dictionary FieldNames = new Dictionary { { "syncType", "同步类型" }, { "frequency", "调度频率" }, { "scheduledTime", "计划时间" }, { "lookbackDays", "回溯天数" }, { "enabled", "是否启用" }, { "synFlag", "同步标志" }, { "warningFlag", "预警标志" }, { "status", "状态" } }; private void UpdateConfigDisplay(SyncConfig c) { if (c == null) { ClearConfigDisplay(); return; } SetCfg("syncType", c.syncType); SetCfg("frequency", c.frequency); SetCfg("scheduledTime", c.scheduledTime); SetCfg("lookbackDays", c.lookbackDays.ToString()); SetCfg("enabled", c.enabled ? "1" : "0"); SetCfg("synFlag", c.synFlag); SetCfg("warningFlag", c.warningFlag); SetCfg("status", c.status); } private void ClearConfigDisplay() { foreach (var k in cfgLabelMap.Keys) SetCfg(k, ""); } private void SetCfg(string key, string val) { if (!cfgLabelMap.TryGetValue(key, out var l)) return; string name = FieldNames.TryGetValue(key, out var n) ? n : key; l.Text = string.IsNullOrEmpty(val) ? (name + ": (未加载)") : (name + ": " + TranslateValue(key, val)); } /// 将配置字段的原始值翻译为中文展示。 private static string TranslateValue(string key, string raw) { if (string.IsNullOrEmpty(raw)) return raw; switch (key) { case "syncType": if (raw == "full") return "全量"; if (raw == "incremental") return "增量"; return raw; case "frequency": if (raw == "hourly") return "每小时"; if (raw == "daily") return "每日"; if (raw == "weekly") return "每周"; if (raw == "manual") return "手动"; return raw; case "enabled": case "synFlag": case "warningFlag": if (raw == "1" || raw == "Y" || raw.Equals("true", StringComparison.OrdinalIgnoreCase)) return "是"; if (raw == "0" || raw == "N" || raw.Equals("false", StringComparison.OrdinalIgnoreCase)) return "否"; return raw; case "status": if (raw == "1") return "正常"; if (raw == "0") return "停用"; return raw; default: return raw; } } // ===== 托盘 / 关闭 ===== public void ShowMain() { if (WindowState == FormWindowState.Minimized) WindowState = FormWindowState.Normal; Show(); BringToFront(); } public void ExitApp() { _exiting = true; _scheduler?.Stop(); tray?.Dispose(); Logger.OnLog -= Logger_OnLog; Application.Exit(); } private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (!_exiting) { e.Cancel = true; Hide(); tray?.ShowBalloon("已最小化到托盘"); } } private void Logger_OnLog(string line) { if (txtLog.InvokeRequired) { txtLog.Invoke(new Action(Logger_OnLog), line); return; } txtLog.AppendText(line + Environment.NewLine); } } }