RegionService.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import fs from 'fs/promises';
  2. import path from 'path';
  3. import { logger } from '../utils/logger.js';
  4. export interface ChinaRegionOption {
  5. label: string;
  6. value: string;
  7. children?: ChinaRegionOption[];
  8. }
  9. interface CachedRegions {
  10. mtimeMs: number;
  11. regions: ChinaRegionOption[];
  12. }
  13. function getServerRootDir(): string {
  14. return path.basename(process.cwd()).toLowerCase() === 'server'
  15. ? process.cwd()
  16. : path.resolve(process.cwd(), 'server');
  17. }
  18. function normalizeCsvCell(value: string): string {
  19. return String(value || '')
  20. .replace(/^\uFEFF/, '')
  21. .trim();
  22. }
  23. export class RegionService {
  24. private cache: CachedRegions | null = null;
  25. async getChinaRegionsFromCsv(): Promise<ChinaRegionOption[]> {
  26. const serverRoot = getServerRootDir();
  27. const csvPath = path.resolve(serverRoot, '中国地区编码表.csv');
  28. let stat: { mtimeMs: number } | null = null;
  29. try {
  30. stat = await fs.stat(csvPath);
  31. } catch {
  32. logger.warn(`[RegionService] csv not found: ${csvPath}`);
  33. return [];
  34. }
  35. if (this.cache && this.cache.mtimeMs === stat.mtimeMs) return this.cache.regions;
  36. const content = await fs.readFile(csvPath, 'utf-8');
  37. const lines = content.split(/\r?\n/).filter(Boolean);
  38. if (lines.length <= 1) return [];
  39. const provinceMap = new Map<string, ChinaRegionOption>();
  40. const cityMapByProvince = new Map<string, Map<string, ChinaRegionOption>>();
  41. const districtMapByCity = new Map<string, Set<string>>();
  42. for (let i = 1; i < lines.length; i++) {
  43. const rawLine = lines[i];
  44. if (!rawLine) continue;
  45. const parts = rawLine.split(',');
  46. if (parts.length < 6) continue;
  47. const provinceName = normalizeCsvCell(parts[0]);
  48. const provinceCode = normalizeCsvCell(parts[1]);
  49. const cityName = normalizeCsvCell(parts[2]);
  50. const cityCode = normalizeCsvCell(parts[3]);
  51. const districtName = normalizeCsvCell(parts[4]);
  52. const districtCode = normalizeCsvCell(parts[5]);
  53. if (!provinceCode || !provinceName) continue;
  54. if (!cityCode || !cityName) continue;
  55. if (!districtCode || !districtName) continue;
  56. let province = provinceMap.get(provinceCode);
  57. if (!province) {
  58. province = { label: provinceName, value: provinceCode, children: [] };
  59. provinceMap.set(provinceCode, province);
  60. cityMapByProvince.set(provinceCode, new Map());
  61. }
  62. const cityMap = cityMapByProvince.get(provinceCode)!;
  63. let city = cityMap.get(cityCode);
  64. if (!city) {
  65. city = { label: cityName, value: cityCode, children: [] };
  66. cityMap.set(cityCode, city);
  67. province.children!.push(city);
  68. }
  69. const districtKey = `${cityCode}:${districtCode}`;
  70. if (!districtMapByCity.has(cityCode)) districtMapByCity.set(cityCode, new Set());
  71. const districtSet = districtMapByCity.get(cityCode)!;
  72. if (districtSet.has(districtKey)) continue;
  73. districtSet.add(districtKey);
  74. city.children!.push({ label: districtName, value: districtCode });
  75. }
  76. const regions = Array.from(provinceMap.values())
  77. .sort((a, b) => Number(a.value) - Number(b.value))
  78. .map(p => ({
  79. ...p,
  80. children: (p.children || [])
  81. .slice()
  82. .sort((a, b) => Number(a.value) - Number(b.value))
  83. .map(c => ({
  84. ...c,
  85. children: (c.children || []).slice().sort((a, b) => Number(a.value) - Number(b.value)),
  86. })),
  87. }));
  88. this.cache = { mtimeMs: stat.mtimeMs, regions };
  89. return regions;
  90. }
  91. }