import fs from 'fs/promises'; import path from 'path'; import { logger } from '../utils/logger.js'; export interface ChinaRegionOption { label: string; value: string; children?: ChinaRegionOption[]; } interface CachedRegions { mtimeMs: number; regions: ChinaRegionOption[]; } function getServerRootDir(): string { return path.basename(process.cwd()).toLowerCase() === 'server' ? process.cwd() : path.resolve(process.cwd(), 'server'); } function normalizeCsvCell(value: string): string { return String(value || '') .replace(/^\uFEFF/, '') .trim(); } export class RegionService { private cache: CachedRegions | null = null; async getChinaRegionsFromCsv(): Promise { const serverRoot = getServerRootDir(); const csvPath = path.resolve(serverRoot, '中国地区编码表.csv'); let stat: { mtimeMs: number } | null = null; try { stat = await fs.stat(csvPath); } catch { logger.warn(`[RegionService] csv not found: ${csvPath}`); return []; } if (this.cache && this.cache.mtimeMs === stat.mtimeMs) return this.cache.regions; const content = await fs.readFile(csvPath, 'utf-8'); const lines = content.split(/\r?\n/).filter(Boolean); if (lines.length <= 1) return []; const provinceMap = new Map(); const cityMapByProvince = new Map>(); const districtMapByCity = new Map>(); for (let i = 1; i < lines.length; i++) { const rawLine = lines[i]; if (!rawLine) continue; const parts = rawLine.split(','); if (parts.length < 6) continue; const provinceName = normalizeCsvCell(parts[0]); const provinceCode = normalizeCsvCell(parts[1]); const cityName = normalizeCsvCell(parts[2]); const cityCode = normalizeCsvCell(parts[3]); const districtName = normalizeCsvCell(parts[4]); const districtCode = normalizeCsvCell(parts[5]); if (!provinceCode || !provinceName) continue; if (!cityCode || !cityName) continue; if (!districtCode || !districtName) continue; let province = provinceMap.get(provinceCode); if (!province) { province = { label: provinceName, value: provinceCode, children: [] }; provinceMap.set(provinceCode, province); cityMapByProvince.set(provinceCode, new Map()); } const cityMap = cityMapByProvince.get(provinceCode)!; let city = cityMap.get(cityCode); if (!city) { city = { label: cityName, value: cityCode, children: [] }; cityMap.set(cityCode, city); province.children!.push(city); } const districtKey = `${cityCode}:${districtCode}`; if (!districtMapByCity.has(cityCode)) districtMapByCity.set(cityCode, new Set()); const districtSet = districtMapByCity.get(cityCode)!; if (districtSet.has(districtKey)) continue; districtSet.add(districtKey); city.children!.push({ label: districtName, value: districtCode }); } const regions = Array.from(provinceMap.values()) .sort((a, b) => Number(a.value) - Number(b.value)) .map(p => ({ ...p, children: (p.children || []) .slice() .sort((a, b) => Number(a.value) - Number(b.value)) .map(c => ({ ...c, children: (c.children || []).slice().sort((a, b) => Number(a.value) - Number(b.value)), })), })); this.cache = { mtimeMs: stat.mtimeMs, regions }; return regions; } }