MainForm.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  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. // 跨度超过一天时按天拆分,每天一次 02010067,避免大区间请求超时
  275. var batches = DrgApiClient.SplitByDay(st, ed);
  276. Logger.Info(string.Format("开始同步 机构={0} syncType={1} 区间[{2},{3}] 拆分为 {4} 批(按天执行 02010067)",
  277. hosp.descripts, cfg.syncType, st, ed, batches.Count));
  278. int dayOk = 0, dayFail = 0;
  279. foreach (var b in batches)
  280. {
  281. var resp = await DrgApiClient.GetInpatientDataAsync(
  282. hosp.hospCode, b.Item1, b.Item2, cfg.synFlag ?? "Y", session);
  283. if (DrgApiClient.IsOk(resp))
  284. {
  285. dayOk++;
  286. Logger.Info(string.Format("机构 {0} 日期 {1} 同步成功", hosp.descripts, b.Item1));
  287. }
  288. else
  289. {
  290. dayFail++;
  291. Logger.Error(string.Format("机构 {0} 日期 {1} 同步失败:{2}",
  292. hosp.descripts, b.Item1, DrgApiClient.GetError(resp)));
  293. }
  294. }
  295. if (dayFail == 0)
  296. {
  297. Logger.Info(string.Format("机构 {0} 同步成功({1} 天全部成功)", hosp.descripts, dayOk));
  298. ok++;
  299. }
  300. else
  301. {
  302. Logger.Error(string.Format("机构 {0} 同步完成但存在失败:成功 {1} 天,失败 {2} 天",
  303. hosp.descripts, dayOk, dayFail));
  304. fail++;
  305. }
  306. }
  307. Logger.Info(string.Format("批量同步完成:成功 {0} 家,失败 {1} 家(共 {2} 家)", ok, fail, list.Count));
  308. }
  309. /// <summary>供 SchedulerService 在整点调用(同步所有勾选机构)。</summary>
  310. public void TriggerSync() { _ = DoSyncAsync(); }
  311. private void StartSchedule()
  312. {
  313. if (!EnsureHospitals(out var list)) return;
  314. // 多机构统一采用程序内整点轮询(覆盖所有勾选机构);daily/weekly 外部定时请用「注册任务计划」。
  315. _scheduler.StartHourly();
  316. tray?.ShowBalloon("已启动内部整点轮询(覆盖 " + list.Count + " 家机构)");
  317. }
  318. private void StopSchedule()
  319. {
  320. _scheduler.Stop();
  321. TaskSchedulerHelper.Delete(Config.Current.TaskName);
  322. tray?.ShowBalloon("已停止调度");
  323. }
  324. private async System.Threading.Tasks.Task RegisterTaskNow()
  325. {
  326. if (!EnsureHospitals(out var list)) return;
  327. // 取每家配置,决定任务频率(任一为 weekly 则 WEEKLY,否则 DAILY),计划时间取首个。
  328. string freq = "daily";
  329. string scheduledTime = "";
  330. foreach (var hosp in list)
  331. {
  332. var session = SessionBuilder.Build(hosp, Config.Current.SessionDefaults);
  333. var cfgResp = await DrgApiClient.GetSyncConfigAsync(hosp.hospCode, session);
  334. if (!DrgApiClient.IsOk(cfgResp)) continue;
  335. var cfg = DrgApiClient.ParseSyncConfig(DrgApiClient.GetResult(cfgResp));
  336. if (cfg.frequency == "weekly") freq = "weekly";
  337. if (string.IsNullOrEmpty(scheduledTime)) scheduledTime = cfg.scheduledTime;
  338. }
  339. string exe = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "DRGInterfaceClient.exe");
  340. TaskSchedulerHelper.Register(Config.Current.TaskName, exe, freq, scheduledTime);
  341. MessageBox.Show(string.Format("已注册计划任务({0},覆盖 {1} 家机构):{2}",
  342. freq == "weekly" ? "每周" : "每日", list.Count, Config.Current.TaskName),
  343. "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  344. }
  345. private void SetScheduledHosp()
  346. {
  347. if (!EnsureHospitals(out var list)) return;
  348. Config.Current.ScheduledHospCodes = list.ConvertAll(h => h.code);
  349. Config.Save();
  350. MessageBox.Show(string.Format("已设为定时机构({0} 家)", list.Count),
  351. "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  352. }
  353. // ===== 配置展示(中文) =====
  354. private static readonly Dictionary<string, string> FieldNames = new Dictionary<string, string>
  355. {
  356. { "syncType", "同步类型" },
  357. { "frequency", "调度频率" },
  358. { "scheduledTime", "计划时间" },
  359. { "lookbackDays", "回溯天数" },
  360. { "enabled", "是否启用" },
  361. { "synFlag", "同步标志" },
  362. { "warningFlag", "预警标志" },
  363. { "status", "状态" }
  364. };
  365. private void UpdateConfigDisplay(SyncConfig c)
  366. {
  367. if (c == null) { ClearConfigDisplay(); return; }
  368. SetCfg("syncType", c.syncType);
  369. SetCfg("frequency", c.frequency);
  370. SetCfg("scheduledTime", c.scheduledTime);
  371. SetCfg("lookbackDays", c.lookbackDays.ToString());
  372. SetCfg("enabled", c.enabled ? "1" : "0");
  373. SetCfg("synFlag", c.synFlag);
  374. SetCfg("warningFlag", c.warningFlag);
  375. SetCfg("status", c.status);
  376. }
  377. private void ClearConfigDisplay()
  378. {
  379. foreach (var k in cfgLabelMap.Keys) SetCfg(k, "");
  380. }
  381. private void SetCfg(string key, string val)
  382. {
  383. if (!cfgLabelMap.TryGetValue(key, out var l)) return;
  384. string name = FieldNames.TryGetValue(key, out var n) ? n : key;
  385. l.Text = string.IsNullOrEmpty(val) ? (name + ": (未加载)") : (name + ": " + TranslateValue(key, val));
  386. }
  387. /// <summary>将配置字段的原始值翻译为中文展示。</summary>
  388. private static string TranslateValue(string key, string raw)
  389. {
  390. if (string.IsNullOrEmpty(raw)) return raw;
  391. switch (key)
  392. {
  393. case "syncType":
  394. if (raw == "full") return "全量";
  395. if (raw == "incremental") return "增量";
  396. return raw;
  397. case "frequency":
  398. if (raw == "hourly") return "每小时";
  399. if (raw == "daily") return "每日";
  400. if (raw == "weekly") return "每周";
  401. if (raw == "manual") return "手动";
  402. return raw;
  403. case "enabled":
  404. case "synFlag":
  405. case "warningFlag":
  406. if (raw == "1" || raw == "Y" || raw.Equals("true", StringComparison.OrdinalIgnoreCase)) return "是";
  407. if (raw == "0" || raw == "N" || raw.Equals("false", StringComparison.OrdinalIgnoreCase)) return "否";
  408. return raw;
  409. case "status":
  410. if (raw == "1") return "正常";
  411. if (raw == "0") return "停用";
  412. return raw;
  413. default:
  414. return raw;
  415. }
  416. }
  417. // ===== 托盘 / 关闭 =====
  418. public void ShowMain()
  419. {
  420. if (WindowState == FormWindowState.Minimized) WindowState = FormWindowState.Normal;
  421. Show();
  422. BringToFront();
  423. }
  424. public void ExitApp()
  425. {
  426. _exiting = true;
  427. _scheduler?.Stop();
  428. tray?.Dispose();
  429. Logger.OnLog -= Logger_OnLog;
  430. Application.Exit();
  431. }
  432. private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
  433. {
  434. if (!_exiting)
  435. {
  436. e.Cancel = true;
  437. Hide();
  438. tray?.ShowBalloon("已最小化到托盘");
  439. }
  440. }
  441. private void Logger_OnLog(string line)
  442. {
  443. if (txtLog.InvokeRequired) { txtLog.Invoke(new Action<string>(Logger_OnLog), line); return; }
  444. txtLog.AppendText(line + Environment.NewLine);
  445. }
  446. }
  447. }