ExPortToExcel.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. using System;
  2. using System.Collections;
  3. using System.Windows.Forms;
  4. //using Excel = Microsoft.Office.Interop.Excel;
  5. using System;
  6. using System.Data;
  7. using System.IO;
  8. using System.Runtime.InteropServices;
  9. using System.Text;
  10. using System.Collections.Generic;
  11. using System.Threading.Tasks;
  12. namespace PTMedicalInsurance.Common
  13. {
  14. public class ExportToExcel
  15. {
  16. /// <summary>
  17. /// 将DataTable导出为CSV文件(完全替代原有的Excel导出)
  18. /// </summary>
  19. public static string DataTabletoCSV(DataTable tmpDataTable, string strFileName)
  20. {
  21. // 1. 前置校验:空值和路径合法性检查
  22. if (tmpDataTable == null || tmpDataTable.Rows.Count == 0)
  23. {
  24. MessageBox.Show("无导出数据!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  25. return string.Empty;
  26. }
  27. // 校验并处理导出路径
  28. if (!ValidateAndFixExportPath(ref strFileName))
  29. {
  30. return string.Empty;
  31. }
  32. // 确保扩展名为.csv
  33. strFileName = EnsureCsvExtension(strFileName);
  34. FileStream fs = null;
  35. StreamWriter sw = null;
  36. StringBuilder sb = new StringBuilder(65536); // 64KB缓冲区
  37. try
  38. {
  39. int rowCount = tmpDataTable.Rows.Count;
  40. int columnCount = tmpDataTable.Columns.Count;
  41. const int batchSize = 5000; // 每批处理5000行
  42. // 2. 创建文件流
  43. fs = new FileStream(strFileName, FileMode.Create, FileAccess.Write, FileShare.None);
  44. // 写入UTF-8 BOM,确保Excel打开不乱码
  45. fs.WriteByte(0xEF);
  46. fs.WriteByte(0xBB);
  47. fs.WriteByte(0xBF);
  48. sw = new StreamWriter(fs, Encoding.UTF8);
  49. // 3. 写入表头
  50. List<string> headers = new List<string>();
  51. for (int i = 0; i < columnCount; i++)
  52. {
  53. string headerName = GetColumnNameMapping(tmpDataTable.Columns[i].ColumnName);
  54. headers.Add(EscapeCsvValue(headerName));
  55. }
  56. sw.WriteLine(string.Join(",", headers));
  57. // 4. 分批写入数据
  58. for (int r = 0; r < rowCount; r++)
  59. {
  60. // 每批处理完成后写入文件
  61. if (r > 0 && r % batchSize == 0)
  62. {
  63. sw.Write(sb.ToString());
  64. sb.Clear();
  65. sw.Flush();
  66. Application.DoEvents(); // 防止UI卡死
  67. }
  68. // 构建当前行
  69. for (int i = 0; i < columnCount; i++)
  70. {
  71. object cellValue = tmpDataTable.Rows[r][i];
  72. // 格式化单元格值
  73. if (cellValue == null || cellValue == DBNull.Value)
  74. {
  75. sb.Append(string.Empty);
  76. }
  77. else
  78. {
  79. string value = cellValue.ToString().Trim();
  80. // 特殊处理:如果是数字类型但需要保留前导零(如身份证号、电话号码)
  81. if (IsNumeric(value))
  82. {
  83. // 检查是否是需要保留前导零的字段
  84. string columnName = tmpDataTable.Columns[i].ColumnName;
  85. if (IsIdNumberField(columnName))
  86. {
  87. // 身份证号、电话号码等字段,强制作为文本处理
  88. sb.Append(EscapeCsvValue(value));
  89. }
  90. else
  91. {
  92. sb.Append(value);
  93. }
  94. }
  95. else
  96. {
  97. sb.Append(EscapeCsvValue(value));
  98. }
  99. }
  100. if (i < columnCount - 1)
  101. {
  102. sb.Append(',');
  103. }
  104. }
  105. sb.AppendLine();
  106. }
  107. // 5. 写入剩余数据
  108. if (sb.Length > 0)
  109. {
  110. sw.Write(sb.ToString());
  111. }
  112. sw.Flush();
  113. // 6. 导出成功提示
  114. DialogResult result = MessageBox.Show(
  115. $"CSV导出成功!\n文件路径:{strFileName}\n总行数:{rowCount}\n\n是否立即打开文件?",
  116. "导出成功",
  117. MessageBoxButtons.YesNo,
  118. MessageBoxIcon.Information);
  119. if (result == DialogResult.Yes)
  120. {
  121. try
  122. {
  123. System.Diagnostics.Process.Start(strFileName);
  124. }
  125. catch (Exception ex)
  126. {
  127. MessageBox.Show($"打开文件失败:{ex.Message}", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
  128. }
  129. }
  130. return strFileName;
  131. }
  132. catch (UnauthorizedAccessException ex)
  133. {
  134. MessageBox.Show($"导出失败:权限不足!请检查文件路径是否可写。\n详情:{ex.Message}",
  135. "权限异常", MessageBoxButtons.OK, MessageBoxIcon.Error);
  136. return string.Empty;
  137. }
  138. catch (IOException ex)
  139. {
  140. MessageBox.Show($"导出失败:文件读写错误。\n详情:{ex.Message}",
  141. "IO异常", MessageBoxButtons.OK, MessageBoxIcon.Error);
  142. return string.Empty;
  143. }
  144. catch (Exception ex)
  145. {
  146. MessageBox.Show($"导出异常:{ex.Message}\n堆栈信息:{ex.StackTrace}",
  147. "导出异常", MessageBoxButtons.OK, MessageBoxIcon.Error);
  148. return string.Empty;
  149. }
  150. finally
  151. {
  152. // 7. 确保资源释放
  153. if (sw != null)
  154. {
  155. try { sw.Close(); sw.Dispose(); } catch { }
  156. }
  157. if (fs != null)
  158. {
  159. try { fs.Close(); fs.Dispose(); } catch { }
  160. }
  161. sb.Clear();
  162. GC.Collect();
  163. }
  164. }
  165. /// <summary>
  166. /// 超大数据量导出(分页处理)- 优化版本
  167. /// </summary>
  168. public static async Task<string> DataTabletoCSV_BatchAsync(DataTable tmpDataTable, string strFileName, int pageSize = 10000, IProgress<int> progress = null)
  169. {
  170. if (tmpDataTable == null || tmpDataTable.Rows.Count == 0)
  171. {
  172. MessageBox.Show("无导出数据!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  173. return string.Empty;
  174. }
  175. if (!ValidateAndFixExportPath(ref strFileName))
  176. {
  177. return string.Empty;
  178. }
  179. strFileName = EnsureCsvExtension(strFileName);
  180. int totalRows = tmpDataTable.Rows.Count;
  181. int columnCount = tmpDataTable.Columns.Count;
  182. int pageCount = (int)Math.Ceiling((double)totalRows / pageSize);
  183. // 使用更高效的StringBuilder
  184. StringBuilder sb = new StringBuilder(65536 * 4); // 256KB缓冲区
  185. try
  186. {
  187. using (FileStream fs = new FileStream(strFileName, FileMode.Create, FileAccess.Write, FileShare.None, 81920, true)) // 80KB缓冲区,异步IO
  188. {
  189. // 写入UTF-8 BOM
  190. fs.WriteByte(0xEF);
  191. fs.WriteByte(0xBB);
  192. fs.WriteByte(0xBF);
  193. using (StreamWriter sw = new StreamWriter(fs, Encoding.UTF8, 81920)) // 80KB缓冲区
  194. {
  195. // 写入表头
  196. List<string> headers = new List<string>(columnCount);
  197. for (int i = 0; i < columnCount; i++)
  198. {
  199. headers.Add(EscapeCsvValue(GetColumnNameMapping(tmpDataTable.Columns[i].ColumnName)));
  200. }
  201. await sw.WriteLineAsync(string.Join(",", headers));
  202. // 分页写入数据
  203. for (int page = 0; page < pageCount; page++)
  204. {
  205. int startRow = page * pageSize;
  206. int endRow = Math.Min(startRow + pageSize, totalRows);
  207. sb.Clear();
  208. for (int r = startRow; r < endRow; r++)
  209. {
  210. BuildCsvRow(tmpDataTable.Rows[r], columnCount, sb);
  211. // 每1000行输出一次,避免StringBuilder过大
  212. if ((r - startRow + 1) % 1000 == 0 || r == endRow - 1)
  213. {
  214. await sw.WriteAsync(sb.ToString());
  215. sb.Clear();
  216. }
  217. }
  218. await sw.FlushAsync();
  219. // 报告进度
  220. progress?.Report((int)((page + 1) * 100.0 / pageCount));
  221. // 可选:每处理完一页,让出控制权,避免UI卡死
  222. await Task.Delay(1);
  223. }
  224. }
  225. }
  226. // 导出成功后的处理
  227. DialogResult result = MessageBox.Show(
  228. $"CSV导出成功!\n文件路径:{strFileName}\n总行数:{totalRows}\n\n是否立即打开文件?",
  229. "导出成功",
  230. MessageBoxButtons.YesNo,
  231. MessageBoxIcon.Information);
  232. if (result == DialogResult.Yes)
  233. {
  234. try
  235. {
  236. System.Diagnostics.Process.Start(strFileName);
  237. }
  238. catch (Exception ex)
  239. {
  240. MessageBox.Show($"打开文件失败:{ex.Message}", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
  241. }
  242. }
  243. return strFileName;
  244. }
  245. catch (Exception ex)
  246. {
  247. MessageBox.Show($"导出异常:{ex.Message}\n堆栈信息:{ex.StackTrace}",
  248. "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  249. return string.Empty;
  250. }
  251. finally
  252. {
  253. // 强制垃圾回收,释放内存
  254. GC.Collect();
  255. GC.WaitForPendingFinalizers();
  256. }
  257. }
  258. /// <summary>
  259. /// 构建CSV行
  260. /// </summary>
  261. private static void BuildCsvRow(DataRow row, int columnCount, StringBuilder sb)
  262. {
  263. for (int i = 0; i < columnCount; i++)
  264. {
  265. object cellValue = row[i];
  266. if (cellValue == null || cellValue == DBNull.Value)
  267. {
  268. sb.Append(string.Empty);
  269. }
  270. else
  271. {
  272. string value = cellValue.ToString().Trim();
  273. string columnName = row.Table.Columns[i].ColumnName;
  274. // 根据字段类型处理
  275. if (IsIdNumberField(columnName))
  276. {
  277. sb.Append(EscapeCsvValue(value)); // 作为文本
  278. }
  279. else if (IsNumeric(value))
  280. {
  281. sb.Append(value); // 作为数字
  282. }
  283. else
  284. {
  285. sb.Append(EscapeCsvValue(value));
  286. }
  287. }
  288. if (i < columnCount - 1)
  289. {
  290. sb.Append(',');
  291. }
  292. }
  293. sb.AppendLine();
  294. }
  295. /// <summary>
  296. /// 分批导出数据(同步版本,优化内存使用)
  297. /// </summary>
  298. public static string DataTabletoCSV_BatchOptimized(DataTable tmpDataTable, string strFileName, int pageSize = 5000)
  299. {
  300. if (tmpDataTable == null || tmpDataTable.Rows.Count == 0)
  301. {
  302. MessageBox.Show("无导出数据!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  303. return string.Empty;
  304. }
  305. if (!ValidateAndFixExportPath(ref strFileName))
  306. {
  307. return string.Empty;
  308. }
  309. strFileName = EnsureCsvExtension(strFileName);
  310. int totalRows = tmpDataTable.Rows.Count;
  311. int columnCount = tmpDataTable.Columns.Count;
  312. int pageCount = (int)Math.Ceiling((double)totalRows / pageSize);
  313. // 使用更高效的StringBuilder
  314. StringBuilder sb = new StringBuilder(65536 * 4); // 256KB缓冲区
  315. try
  316. {
  317. using (FileStream fs = new FileStream(strFileName, FileMode.Create, FileAccess.Write, FileShare.None, 81920))
  318. {
  319. // 写入UTF-8 BOM
  320. fs.WriteByte(0xEF);
  321. fs.WriteByte(0xBB);
  322. fs.WriteByte(0xBF);
  323. using (StreamWriter sw = new StreamWriter(fs, Encoding.UTF8, 81920))
  324. {
  325. // 写入表头
  326. List<string> headers = new List<string>(columnCount);
  327. for (int i = 0; i < columnCount; i++)
  328. {
  329. headers.Add(EscapeCsvValue(GetColumnNameMapping(tmpDataTable.Columns[i].ColumnName)));
  330. }
  331. sw.WriteLine(string.Join(",", headers));
  332. // 分页写入数据
  333. for (int page = 0; page < pageCount; page++)
  334. {
  335. int startRow = page * pageSize;
  336. int endRow = Math.Min(startRow + pageSize, totalRows);
  337. sb.Clear();
  338. for (int r = startRow; r < endRow; r++)
  339. {
  340. BuildCsvRow(tmpDataTable.Rows[r], columnCount, sb);
  341. // 每1000行写入一次,避免StringBuilder过大
  342. if ((r - startRow + 1) % 1000 == 0 || r == endRow - 1)
  343. {
  344. sw.Write(sb.ToString());
  345. sb.Clear();
  346. }
  347. }
  348. sw.Flush();
  349. // 每处理完一页,检查是否有UI消息(仅在UI线程调用时)
  350. if (Application.MessageLoop)
  351. {
  352. Application.DoEvents();
  353. }
  354. }
  355. }
  356. }
  357. MessageBox.Show($"CSV导出成功!\n文件路径:{strFileName}\n总行数:{totalRows}",
  358. "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
  359. return strFileName;
  360. }
  361. catch (Exception ex)
  362. {
  363. MessageBox.Show($"导出异常:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  364. return string.Empty;
  365. }
  366. finally
  367. {
  368. GC.Collect();
  369. }
  370. }
  371. /// <summary>
  372. /// 超大数据量导出(分页处理)
  373. /// </summary>
  374. public static string DataTabletoCSV_Batch(DataTable tmpDataTable, string strFileName, int pageSize = 10000)
  375. {
  376. if (tmpDataTable == null || tmpDataTable.Rows.Count == 0)
  377. {
  378. MessageBox.Show("无导出数据!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
  379. return string.Empty;
  380. }
  381. if (!ValidateAndFixExportPath(ref strFileName))
  382. {
  383. return string.Empty;
  384. }
  385. strFileName = EnsureCsvExtension(strFileName);
  386. int totalRows = tmpDataTable.Rows.Count;
  387. int columnCount = tmpDataTable.Columns.Count;
  388. int pageCount = (int)Math.Ceiling((double)totalRows / pageSize);
  389. try
  390. {
  391. using (FileStream fs = new FileStream(strFileName, FileMode.Create, FileAccess.Write, FileShare.None))
  392. {
  393. // 写入UTF-8 BOM
  394. fs.WriteByte(0xEF);
  395. fs.WriteByte(0xBB);
  396. fs.WriteByte(0xBF);
  397. using (StreamWriter sw = new StreamWriter(fs, Encoding.UTF8))
  398. {
  399. // 写入表头
  400. List<string> headers = new List<string>();
  401. for (int i = 0; i < columnCount; i++)
  402. {
  403. headers.Add(EscapeCsvValue(GetColumnNameMapping(tmpDataTable.Columns[i].ColumnName)));
  404. }
  405. sw.WriteLine(string.Join(",", headers));
  406. // 分页写入数据
  407. for (int page = 0; page < pageCount; page++)
  408. {
  409. int startRow = page * pageSize;
  410. int endRow = Math.Min(startRow + pageSize, totalRows);
  411. for (int r = startRow; r < endRow; r++)
  412. {
  413. List<string> rowValues = new List<string>(columnCount);
  414. for (int i = 0; i < columnCount; i++)
  415. {
  416. object cellValue = tmpDataTable.Rows[r][i];
  417. if (cellValue == null || cellValue == DBNull.Value)
  418. {
  419. rowValues.Add(string.Empty);
  420. }
  421. else
  422. {
  423. string value = cellValue.ToString().Trim();
  424. // 根据字段类型处理
  425. string columnName = tmpDataTable.Columns[i].ColumnName;
  426. if (IsIdNumberField(columnName))
  427. {
  428. rowValues.Add(EscapeCsvValue(value)); // 作为文本
  429. }
  430. else if (IsNumeric(value))
  431. {
  432. rowValues.Add(value); // 作为数字
  433. }
  434. else
  435. {
  436. rowValues.Add(EscapeCsvValue(value));
  437. }
  438. }
  439. }
  440. sw.WriteLine(string.Join(",", rowValues));
  441. if (r % 500 == 0)
  442. {
  443. Application.DoEvents();
  444. }
  445. }
  446. sw.Flush();
  447. // 可选:显示进度
  448. int progress = (int)((page + 1) * 100.0 / pageCount);
  449. // 可以在这里更新进度条
  450. }
  451. }
  452. }
  453. MessageBox.Show($"CSV导出成功!\n文件路径:{strFileName}\n总行数:{totalRows}",
  454. "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
  455. return strFileName;
  456. }
  457. catch (Exception ex)
  458. {
  459. MessageBox.Show($"导出异常:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  460. return string.Empty;
  461. }
  462. }
  463. #region 辅助方法
  464. /// <summary>
  465. /// 校验并修复导出路径
  466. /// </summary>
  467. private static bool ValidateAndFixExportPath(ref string filePath)
  468. {
  469. try
  470. {
  471. // 1. 检查路径是否为空
  472. if (string.IsNullOrWhiteSpace(filePath))
  473. {
  474. filePath = Path.Combine(
  475. Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
  476. $"导出数据_{DateTime.Now:yyyyMMddHHmmss}.csv");
  477. }
  478. // 2. 检查目录是否存在
  479. string directory = Path.GetDirectoryName(filePath);
  480. if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
  481. {
  482. Directory.CreateDirectory(directory);
  483. }
  484. // 3. 检查文件是否被占用
  485. if (File.Exists(filePath))
  486. {
  487. try
  488. {
  489. using (FileStream fs = File.Open(filePath, FileMode.Open, FileAccess.Write, FileShare.None))
  490. {
  491. // 文件可写,直接返回
  492. }
  493. }
  494. catch
  495. {
  496. DialogResult result = MessageBox.Show($"文件【{filePath}】已被占用,是否覆盖?",
  497. "文件占用", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
  498. if (result == DialogResult.No)
  499. {
  500. return false;
  501. }
  502. // 尝试删除
  503. try { File.Delete(filePath); } catch { }
  504. }
  505. }
  506. return true;
  507. }
  508. catch (Exception ex)
  509. {
  510. MessageBox.Show($"路径校验失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
  511. return false;
  512. }
  513. }
  514. /// <summary>
  515. /// 确保文件扩展名为.csv
  516. /// </summary>
  517. private static string EnsureCsvExtension(string filePath)
  518. {
  519. string extension = Path.GetExtension(filePath)?.ToLower();
  520. if (extension != ".csv")
  521. {
  522. return Path.ChangeExtension(filePath, ".csv");
  523. }
  524. return filePath;
  525. }
  526. /// <summary>
  527. /// 列名映射(保留原有的映射逻辑)
  528. /// </summary>
  529. private static string GetColumnNameMapping(string originalName)
  530. {
  531. var columnMapping = new Dictionary<string, string>
  532. {
  533. { "Adm_Dr", "HIS就诊号" },
  534. { "MdtrtID", "医保住院号" },
  535. { "PatientName", "姓名" },
  536. { "Age", "年龄" },
  537. { "Gend", "性别" },
  538. { "PsnCertNO", "身份证号" },
  539. { "EmpName", "单位或地址" },
  540. { "PsnType", "医保类型" },
  541. { "AdmInDepName", "病人部门名称" },
  542. { "AdmBed", "床号" },
  543. { "MainDiagName", "入院诊断" },
  544. { "AttendDoctorNO", "床位医生" },
  545. { "ChiefPhyDocName", "门诊医生" },
  546. { "OpertName", "入院登记人姓名" },
  547. { "RegDate", "入院登记时间" },
  548. { "insuplcAdmdvs", "参保地区划" },
  549. { "InHospNo", "HIS住院号" },
  550. { "Address", "现住址" }
  551. };
  552. return columnMapping.TryGetValue(originalName, out string newName) ? newName : originalName;
  553. }
  554. /// <summary>
  555. /// 判断是否为身份证号等需要保留前导零的字段
  556. /// </summary>
  557. private static bool IsIdNumberField(string columnName)
  558. {
  559. string lowerName = columnName.ToLower();
  560. return lowerName.Contains("id") ||
  561. lowerName.Contains("cert") ||
  562. lowerName.Contains("身份证") ||
  563. lowerName.Contains("电话") ||
  564. lowerName.Contains("phone") ||
  565. lowerName.Contains("code") ||
  566. lowerName.Contains("编码");
  567. }
  568. /// <summary>
  569. /// 判断是否为数字
  570. /// </summary>
  571. private static bool IsNumeric(string str)
  572. {
  573. return double.TryParse(str, out _);
  574. }
  575. /// <summary>
  576. /// 转义CSV值中的特殊字符
  577. /// </summary>
  578. private static string EscapeCsvValue(string value)
  579. {
  580. if (string.IsNullOrEmpty(value))
  581. {
  582. return string.Empty;
  583. }
  584. // 检查是否包含特殊字符
  585. bool containsSpecialChar = value.Contains(",") ||
  586. value.Contains("\"") ||
  587. value.Contains("\r") ||
  588. value.Contains("\n") ||
  589. value.Contains("\t");
  590. if (containsSpecialChar)
  591. {
  592. // 双引号转义
  593. value = value.Replace("\"", "\"\"");
  594. return $"\"{value}\"";
  595. }
  596. return value;
  597. }
  598. #endregion
  599. }
  600. }