"""从版本化选择字典生成前后端城市数据;不下载、不修改生产配置。""" import argparse import json import re from pathlib import Path ROOT = Path(__file__).resolve().parents[1] parser = argparse.ArgumentParser() parser.add_argument('--backend', type=Path, required=True) parser.add_argument('--check', action='store_true') args = parser.parse_args() data = json.loads((ROOT / 'docs/data/city-options.json').read_text(encoding='utf-8')) regions = data['regions'] names = [city['name'] for region in regions for city in region['cities']] codes = {city['code']: city['name'] for region in regions for city in region['cities'] if city['code']} assert len(names) == len(set(names)), '城市名称不能歧义' assert len(codes) == sum(bool(city['code']) for region in regions for city in region['cities']), '代码重复' assert all(re.fullmatch(r'\d{6}', code) for code in codes) uts = "// 由 scripts/generate-city-options.py 生成;来源和覆盖口径见 docs/data/city-options.json。\n" uts += "export type CityOption = { name: string, code: string }\nexport type CityRegion = { name: string, cities: CityOption[] }\n" uts += "export const cityRegions: CityRegion[] = [\n" uts += ',\n'.join(' ' + json.dumps(region, ensure_ascii=False) for region in regions) + '\n]\n' provinces = {str(index['code']) + '0000': index['name'] for index in data['provinces']} uts += 'export const provinceOptions: CityOption[] = ' + json.dumps([{'name':name,'code':code} for code,name in provinces.items()],ensure_ascii=False) + '\n' uts += ''' export const citySelection = (name: string): number[] => { for (let province = 0; province < cityRegions.length; province++) { const cities = cityRegions[province].cities for (let city = 0; city < cities.length; city++) { if (cities[city].name == name) return [province, city] } } return [-1, -1] } export const cityCodeForName = (name: string): string | null => { const selected = citySelection(name) if (selected[0] < 0) return null const code = cityRegions[selected[0]].cities[selected[1]].code return code.length > 0 ? code : null } export const cityDisplayName = (name: string): string => { const selected = citySelection(name) if (selected[0] < 0) return name const province = cityRegions[selected[0]].name return province == name ? name : province + ' / ' + name } export const provinceCodeForName = (name: string): string | null => { const index = provinceOptions.findIndex((item: CityOption): boolean => item.name == name) return index < 0 ? null : provinceOptions[index].code } // 仅供历史城市资料回填为省份;未知名称不猜测。 export const provinceForCity = (name: string): string => { if (provinceCodeForName(name) != null) return name const selected = citySelection(name) return selected[0] < 0 ? '' : cityRegions[selected[0]].name } ''' constant = ' const PROVINCES = [\n' + '\n'.join(" '%s' => '%s'," % (code, name) for code,name in provinces.items()) + '\n ];\n' backend = args.backend / 'application/common/library/AssessmentRecords.php' original = backend.read_text(encoding='utf-8') php, replacements = re.subn(r' const PROVINCES = .*?;\n', lambda _: constant, original, count=1, flags=re.S) if not replacements: php = original.replace(' private static function only(', constant + '\n private static function only(',1) for path, content in [(ROOT / 'services/cities.uts', uts), (backend, php)]: if args.check: assert path.read_text(encoding='utf-8') == content, f'生成文件不同步:{path}' else: path.write_text(content, encoding='utf-8', newline='\n') print(f'{len(provinces)} province choices; legacy city lookup retained; ' + ('consistent' if args.check else 'generated'))