MainForm.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.Linq;
  5. using System.Windows.Forms;
  6. namespace DRG.DRGInterfaceClient
  7. {
  8. public partial class MainForm : Form
  9. {
  10. private TextBox txtEndpoint;
  11. private Label lblHosp;
  12. private CheckedListBox clbHospital;
  13. private Button btnLoadHosp;
  14. private Button btnSelectAll;
  15. private Button btnSelectNone;
  16. private Label lblSelectedCount;
  17. private Button btnLoadConfig;
  18. private Button btnSyncNow;
  19. private Button btnStartSchedule;
  20. private Button btnStopSchedule;
  21. private Button btnRegisterTask;
  22. private Button btnSetScheduledHosp;
  23. private Button btnOpenLog;
  24. private Button btnClearLog;
  25. private GroupBox grpConfig;
  26. private Dictionary<string, Label> cfgLabelMap;
  27. private TextBox txtLog;
  28. private TrayIcon tray;
  29. /// <summary>勾选列表中最后一个勾选的机构,用于配置预览面板与 session。</summary>
  30. private HospitalInfoItem _previewHosp;
  31. private Dictionary<string, object> _session;
  32. private SyncConfig _syncConfig;
  33. private SchedulerService _scheduler;
  34. private bool _exiting;
  35. private bool _bulkUpdating;
  36. public MainForm()
  37. {
  38. InitializeComponent();
  39. _scheduler = new SchedulerService(this);
  40. Logger.OnLog += Logger_OnLog;
  41. tray = new TrayIcon(this);
  42. Load += (s, e) =>
  43. {
  44. txtEndpoint.Text = Config.Current.Endpoint;
  45. _ = LoadHospitalsAsync();
  46. };
  47. FormClosing += MainForm_FormClosing;
  48. Disposed += (s, e) => { Logger.OnLog -= Logger_OnLog; tray?.Dispose(); };
  49. }
  50. // ===== UI 构建 =====
  51. private void InitializeComponent()
  52. {
  53. Text = "DRG 接口客户端";
  54. Size = new Size(760, 700);
  55. StartPosition = FormStartPosition.CenterScreen;
  56. MinimumSize = new Size(640, 480);
  57. var lblEp = new Label { Left = 8, Top = 15, Width = 76, Text = "接口地址:" };
  58. txtEndpoint = new TextBox { Left = 90, Top = 12, Width = 640, Text = "http://127.0.0.1:52773/csp/drg/sysInternalMutiple" };
  59. lblHosp = new Label { Left = 8, Top = 47, Width = 76, Text = "医疗机构:" };
  60. clbHospital = new CheckedListBox
  61. {
  62. Left = 90,
  63. Top = 44,
  64. Width = 360,
  65. Height = 90,
  66. CheckOnClick = true,
  67. DisplayMember = "descripts"
  68. };
  69. btnLoadHosp = new Button { Left = 460, Top = 42, Width = 100, Text = "加载机构" };
  70. btnSelectAll = new Button { Left = 460, Top = 78, Width = 48, Text = "全选" };
  71. btnSelectNone = new Button { Left = 512, Top = 78, Width = 48, Text = "全否" };
  72. lblSelectedCount = new Label { Left = 566, Top = 80, Width = 170, Text = "已选 0 家" };
  73. grpConfig = new GroupBox { Left = 8, Top = 148, Width = 724, Height = 110, Text = "同步配置(选中机构预览)" };
  74. string[] names = { "syncType", "frequency", "scheduledTime", "lookbackDays", "enabled", "synFlag", "warningFlag", "status" };
  75. cfgLabelMap = new Dictionary<string, Label>();
  76. for (int i = 0; i < names.Length; i++)
  77. {
  78. int col = i % 4; int row = i / 4;
  79. var l = new Label
  80. {
  81. Left = 12 + col * 180,
  82. Top = 22 + row * 40,
  83. Width = 170,
  84. Height = 32,
  85. Text = (FieldNames.TryGetValue(names[i], out var fn) ? fn : names[i]) + ": (未加载)",
  86. BorderStyle = BorderStyle.FixedSingle,
  87. BackColor = SystemColors.ControlLight
  88. };
  89. cfgLabelMap[names[i]] = l;
  90. grpConfig.Controls.Add(l);
  91. }
  92. btnLoadConfig = new Button { Left = 8, Top = 272, Width = 110, Text = "加载配置" };
  93. btnSyncNow = new Button { Left = 124, Top = 272, Width = 110, Text = "立即执行一次" };
  94. btnStartSchedule = new Button { Left = 240, Top = 272, Width = 110, Text = "开始调度" };
  95. btnStopSchedule = new Button { Left = 356, Top = 272, Width = 110, Text = "停止调度" };
  96. btnRegisterTask = new Button { Left = 472, Top = 272, Width = 120, Text = "注册任务计划" };
  97. btnSetScheduledHosp = new Button { Left = 598, Top = 272, Width = 134, Text = "设为定时机构" };
  98. btnOpenLog = new Button { Left = 8, Top = 308, Width = 110, Text = "打开日志" };
  99. btnClearLog = new Button { Left = 124, Top = 308, Width = 110, Text = "清空日志" };
  100. txtLog = new TextBox
  101. {
  102. Left = 8,
  103. Top = 344,
  104. Width = 724,
  105. Height = 320,
  106. Multiline = true,
  107. ReadOnly = true,
  108. ScrollBars = ScrollBars.Vertical,
  109. Font = new Font("Consolas", 9)
  110. };
  111. Controls.AddRange(new Control[] { lblEp, txtEndpoint, lblHosp, clbHospital, btnLoadHosp,
  112. btnSelectAll, btnSelectNone, lblSelectedCount, grpConfig, btnLoadConfig, btnSyncNow,
  113. btnStartSchedule, btnStopSchedule, btnRegisterTask, btnSetScheduledHosp,
  114. btnOpenLog, btnClearLog, txtLog });
  115. btnLoadHosp.Click += async (s, e) => await LoadHospitalsAsync();
  116. btnLoadConfig.Click += async (s, e) => await LoadPreviewConfigAsync(_previewHosp);
  117. btnSyncNow.Click += async (s, e) => await DoSyncAsync();
  118. btnStartSchedule.Click += (s, e) => StartSchedule();
  119. btnStopSchedule.Click += (s, e) => StopSchedule();
  120. btnRegisterTask.Click += async (s, e) => await RegisterTaskNow();
  121. btnSetScheduledHosp.Click += (s, e) => SetScheduledHosp();
  122. btnOpenLog.Click += (s, e) => Logger.OpenLogFile();
  123. btnClearLog.Click += (s, e) => { Logger.Clear(); txtLog.Clear(); };
  124. btnSelectAll.Click += (s, e) => SelectAllHospitals(true);
  125. btnSelectNone.Click += (s, e) => SelectAllHospitals(false);
  126. clbHospital.ItemCheck += (s, e) => OnHospitalItemCheck();
  127. }
  128. // ===== 校验 =====
  129. private bool EnsureEndpoint()
  130. {
  131. string ep = txtEndpoint.Text.Trim();
  132. if (string.IsNullOrEmpty(ep))
  133. {
  134. MessageBox.Show("接口地址不能为空", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
  135. txtEndpoint.Focus();
  136. return false;
  137. }
  138. Config.Current.Endpoint = ep;
  139. return true;
  140. }
  141. private bool EnsureHospitals(out List<HospitalInfoItem> list)
  142. {
  143. list = GetCheckedHospitals();
  144. if (list.Count == 0)
  145. {
  146. MessageBox.Show("请至少勾选一家医疗机构", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
  147. clbHospital.Focus();
  148. return false;
  149. }
  150. return true;
  151. }
  152. // ===== 机构勾选 =====
  153. private List<HospitalInfoItem> GetCheckedHospitals()
  154. {
  155. var list = new List<HospitalInfoItem>();
  156. for (int i = 0; i < clbHospital.Items.Count; i++)
  157. if (clbHospital.GetItemChecked(i) && clbHospital.Items[i] is HospitalInfoItem h)
  158. list.Add(h);
  159. return list;
  160. }
  161. private HospitalInfoItem GetLastCheckedHospital()
  162. {
  163. HospitalInfoItem last = null;
  164. for (int i = 0; i < clbHospital.Items.Count; i++)
  165. if (clbHospital.GetItemChecked(i) && clbHospital.Items[i] is HospitalInfoItem h)
  166. last = h;
  167. return last;
  168. }
  169. private void SelectAllHospitals(bool check)
  170. {
  171. _bulkUpdating = true;
  172. for (int i = 0; i < clbHospital.Items.Count; i++)
  173. clbHospital.SetItemChecked(i, check);
  174. _bulkUpdating = false;
  175. RefreshSelectionUI();
  176. }
  177. /// <summary>勾选变化后刷新「已选 N 家」与配置预览。ItemCheck 时控件状态尚未提交,故用 BeginInvoke 延后读取。</summary>
  178. private void OnHospitalItemCheck()
  179. {
  180. if (_bulkUpdating) return;
  181. clbHospital.BeginInvoke(new Action(RefreshSelectionUI));
  182. }
  183. private void RefreshSelectionUI()
  184. {
  185. UpdateSelectedCount();
  186. _previewHosp = GetLastCheckedHospital();
  187. if (_previewHosp != null)
  188. {
  189. _session = SessionBuilder.Build(_previewHosp, Config.Current.SessionDefaults);
  190. _ = LoadPreviewConfigAsync(_previewHosp);
  191. }
  192. else
  193. {
  194. _session = null;
  195. _syncConfig = null;
  196. ClearConfigDisplay();
  197. }
  198. }
  199. private void UpdateSelectedCount()
  200. {
  201. if (lblSelectedCount.InvokeRequired) { lblSelectedCount.Invoke(new Action(UpdateSelectedCount)); return; }
  202. lblSelectedCount.Text = "已选 " + GetCheckedHospitals().Count + " 家";
  203. }
  204. // ===== 业务动作 =====
  205. private async System.Threading.Tasks.Task LoadHospitalsAsync()
  206. {
  207. if (!EnsureEndpoint()) return;
  208. var resp = await DrgApiClient.QueryHospitalInfoAsync("", "",
  209. SessionBuilder.BuildDefault(Config.Current.SessionDefaults));
  210. if (!DrgApiClient.IsOk(resp))
  211. {
  212. MessageBox.Show("加载机构失败:" + DrgApiClient.GetError(resp), "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  213. return;
  214. }
  215. var arr = DrgApiClient.GetResultArray(resp);
  216. clbHospital.Items.Clear();
  217. if (arr == null || arr.Length == 0)
  218. {
  219. MessageBox.Show("未查询到医疗机构", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  220. UpdateSelectedCount();
  221. return;
  222. }
  223. var scheduled = Config.Current.ScheduledHospCodes ?? new List<string>();
  224. _bulkUpdating = true;
  225. foreach (var o in arr)
  226. {
  227. if (o is Dictionary<string, object> d)
  228. {
  229. var h = DrgApiClient.ParseHospital(d);
  230. int idx = clbHospital.Items.Add(h);
  231. if (scheduled.Contains(h.code, StringComparer.OrdinalIgnoreCase))
  232. clbHospital.SetItemChecked(idx, true);
  233. }
  234. }
  235. _bulkUpdating = false;
  236. RefreshSelectionUI();
  237. }
  238. /// <summary>加载指定机构的 02010079 配置用于预览面板(不弹窗,仅日志)。</summary>
  239. private async System.Threading.Tasks.Task LoadPreviewConfigAsync(HospitalInfoItem hosp)
  240. {
  241. if (hosp == null) { ClearConfigDisplay(); return; }
  242. if (!EnsureEndpoint()) return;
  243. var session = SessionBuilder.Build(hosp, Config.Current.SessionDefaults);
  244. var resp = await DrgApiClient.GetSyncConfigAsync(hosp.hospCode, session);
  245. if (!DrgApiClient.IsOk(resp))
  246. {
  247. Logger.Error(string.Format("预览配置加载失败(机构 {0}):{1}", hosp.descripts, DrgApiClient.GetError(resp)));
  248. ClearConfigDisplay();
  249. return;
  250. }
  251. _syncConfig = DrgApiClient.ParseSyncConfig(DrgApiClient.GetResult(resp));
  252. UpdateConfigDisplay(_syncConfig);
  253. }
  254. /// <summary>对勾选的所有机构顺序执行 02010067 同步(每家各自取 02010079 配置)。</summary>
  255. public async System.Threading.Tasks.Task DoSyncAsync()
  256. {
  257. if (InvokeRequired) { Invoke(new Action(() => _ = DoSyncAsync())); return; }
  258. if (!EnsureEndpoint()) return;
  259. if (!EnsureHospitals(out var list)) return;
  260. int ok = 0, fail = 0;
  261. foreach (var hosp in list)
  262. {
  263. var session = SessionBuilder.Build(hosp, Config.Current.SessionDefaults);
  264. var cfgResp = await DrgApiClient.GetSyncConfigAsync(hosp.hospCode, session);
  265. if (!DrgApiClient.IsOk(cfgResp))
  266. {
  267. Logger.Error(string.Format("机构 {0} 配置加载失败:{1}", hosp.descripts, DrgApiClient.GetError(cfgResp)));
  268. fail++;
  269. continue;
  270. }
  271. var cfg = DrgApiClient.ParseSyncConfig(DrgApiClient.GetResult(cfgResp));
  272. var (st, ed) = DrgApiClient.ComputeDateRange(
  273. cfg.syncType, cfg.lookbackDays, Config.Current.FullSyncStartDays);
  274. Logger.Info(string.Format("开始同步 机构={0} syncType={1} 区间[{2},{3}]",
  275. hosp.descripts, cfg.syncType, st, ed));
  276. var resp = await DrgApiClient.GetInpatientDataAsync(
  277. hosp.hospCode, st, ed, cfg.synFlag ?? "Y", session);
  278. if (DrgApiClient.IsOk(resp))
  279. {
  280. Logger.Info(string.Format("机构 {0} 同步成功", hosp.descripts));
  281. ok++;
  282. }
  283. else
  284. {
  285. Logger.Error(string.Format("机构 {0} 同步失败:{1}", hosp.descripts, DrgApiClient.GetError(resp)));
  286. fail++;
  287. }
  288. }
  289. Logger.Info(string.Format("批量同步完成:成功 {0} 家,失败 {1} 家(共 {2} 家)", ok, fail, list.Count));
  290. }
  291. /// <summary>供 SchedulerService 在整点调用(同步所有勾选机构)。</summary>
  292. public void TriggerSync() { _ = DoSyncAsync(); }
  293. private void StartSchedule()
  294. {
  295. if (!EnsureHospitals(out var list)) return;
  296. // 多机构统一采用程序内整点轮询(覆盖所有勾选机构);daily/weekly 外部定时请用「注册任务计划」。
  297. _scheduler.StartHourly();
  298. tray?.ShowBalloon("已启动内部整点轮询(覆盖 " + list.Count + " 家机构)");
  299. }
  300. private void StopSchedule()
  301. {
  302. _scheduler.Stop();
  303. TaskSchedulerHelper.Delete(Config.Current.TaskName);
  304. tray?.ShowBalloon("已停止调度");
  305. }
  306. private async System.Threading.Tasks.Task RegisterTaskNow()
  307. {
  308. if (!EnsureHospitals(out var list)) return;
  309. // 取每家配置,决定任务频率(任一为 weekly 则 WEEKLY,否则 DAILY),计划时间取首个。
  310. string freq = "daily";
  311. string scheduledTime = "";
  312. foreach (var hosp in list)
  313. {
  314. var session = SessionBuilder.Build(hosp, Config.Current.SessionDefaults);
  315. var cfgResp = await DrgApiClient.GetSyncConfigAsync(hosp.hospCode, session);
  316. if (!DrgApiClient.IsOk(cfgResp)) continue;
  317. var cfg = DrgApiClient.ParseSyncConfig(DrgApiClient.GetResult(cfgResp));
  318. if (cfg.frequency == "weekly") freq = "weekly";
  319. if (string.IsNullOrEmpty(scheduledTime)) scheduledTime = cfg.scheduledTime;
  320. }
  321. string exe = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "DRGInterfaceClient.exe");
  322. TaskSchedulerHelper.Register(Config.Current.TaskName, exe, freq, scheduledTime);
  323. MessageBox.Show(string.Format("已注册计划任务({0},覆盖 {1} 家机构):{2}",
  324. freq == "weekly" ? "每周" : "每日", list.Count, Config.Current.TaskName),
  325. "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  326. }
  327. private void SetScheduledHosp()
  328. {
  329. if (!EnsureHospitals(out var list)) return;
  330. Config.Current.ScheduledHospCodes = list.ConvertAll(h => h.code);
  331. Config.Save();
  332. MessageBox.Show(string.Format("已设为定时机构({0} 家)", list.Count),
  333. "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  334. }
  335. // ===== 配置展示(中文) =====
  336. private static readonly Dictionary<string, string> FieldNames = new Dictionary<string, string>
  337. {
  338. { "syncType", "同步类型" },
  339. { "frequency", "调度频率" },
  340. { "scheduledTime", "计划时间" },
  341. { "lookbackDays", "回溯天数" },
  342. { "enabled", "是否启用" },
  343. { "synFlag", "同步标志" },
  344. { "warningFlag", "预警标志" },
  345. { "status", "状态" }
  346. };
  347. private void UpdateConfigDisplay(SyncConfig c)
  348. {
  349. if (c == null) { ClearConfigDisplay(); return; }
  350. SetCfg("syncType", c.syncType);
  351. SetCfg("frequency", c.frequency);
  352. SetCfg("scheduledTime", c.scheduledTime);
  353. SetCfg("lookbackDays", c.lookbackDays.ToString());
  354. SetCfg("enabled", c.enabled ? "1" : "0");
  355. SetCfg("synFlag", c.synFlag);
  356. SetCfg("warningFlag", c.warningFlag);
  357. SetCfg("status", c.status);
  358. }
  359. private void ClearConfigDisplay()
  360. {
  361. foreach (var k in cfgLabelMap.Keys) SetCfg(k, "");
  362. }
  363. private void SetCfg(string key, string val)
  364. {
  365. if (!cfgLabelMap.TryGetValue(key, out var l)) return;
  366. string name = FieldNames.TryGetValue(key, out var n) ? n : key;
  367. l.Text = string.IsNullOrEmpty(val) ? (name + ": (未加载)") : (name + ": " + TranslateValue(key, val));
  368. }
  369. /// <summary>将配置字段的原始值翻译为中文展示。</summary>
  370. private static string TranslateValue(string key, string raw)
  371. {
  372. if (string.IsNullOrEmpty(raw)) return raw;
  373. switch (key)
  374. {
  375. case "syncType":
  376. if (raw == "full") return "全量";
  377. if (raw == "incremental") return "增量";
  378. return raw;
  379. case "frequency":
  380. if (raw == "hourly") return "每小时";
  381. if (raw == "daily") return "每日";
  382. if (raw == "weekly") return "每周";
  383. if (raw == "manual") return "手动";
  384. return raw;
  385. case "enabled":
  386. case "synFlag":
  387. case "warningFlag":
  388. if (raw == "1" || raw == "Y" || raw.Equals("true", StringComparison.OrdinalIgnoreCase)) return "是";
  389. if (raw == "0" || raw == "N" || raw.Equals("false", StringComparison.OrdinalIgnoreCase)) return "否";
  390. return raw;
  391. case "status":
  392. if (raw == "1") return "正常";
  393. if (raw == "0") return "停用";
  394. return raw;
  395. default:
  396. return raw;
  397. }
  398. }
  399. // ===== 托盘 / 关闭 =====
  400. public void ShowMain()
  401. {
  402. if (WindowState == FormWindowState.Minimized) WindowState = FormWindowState.Normal;
  403. Show();
  404. BringToFront();
  405. }
  406. public void ExitApp()
  407. {
  408. _exiting = true;
  409. _scheduler?.Stop();
  410. tray?.Dispose();
  411. Logger.OnLog -= Logger_OnLog;
  412. Application.Exit();
  413. }
  414. private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
  415. {
  416. if (!_exiting)
  417. {
  418. e.Cancel = true;
  419. Hide();
  420. tray?.ShowBalloon("已最小化到托盘");
  421. }
  422. }
  423. private void Logger_OnLog(string line)
  424. {
  425. if (txtLog.InvokeRequired) { txtLog.Invoke(new Action<string>(Logger_OnLog), line); return; }
  426. txtLog.AppendText(line + Environment.NewLine);
  427. }
  428. }
  429. }