| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- 'use strict';
- /**
- * 校区地图 · 学校数据(JSON 静态存储)
- * ----------------------------------------------------------------------------
- * 两份文件合并成一份视图:
- * server/schools.seed.json 随版本发布的内置学校(只读,可被运行时覆盖 enabled/坐标)
- * data/schools.json 运行时新增/修改(gitignore;由 SCHOOLS_FILE 可覆盖,测试用)
- *
- * 合并规则:以 name 为唯一键,运行时同名字段覆盖种子;种子的 seed 标记始终保留。
- */
- const fs = require('fs');
- const path = require('path');
- const ROOT_DIR = __dirname;
- const SEED_FILE = process.env.SCHOOLS_SEED_FILE
- ? path.resolve(process.env.SCHOOLS_SEED_FILE)
- : path.join(ROOT_DIR, 'server', 'schools.seed.json');
- const RUNTIME_FILE = process.env.SCHOOLS_FILE
- ? path.resolve(process.env.SCHOOLS_FILE)
- : path.join(ROOT_DIR, 'data', 'schools.json');
- function readJson(file, fallback) {
- try {
- return JSON.parse(fs.readFileSync(file, 'utf8'));
- } catch (e) {
- if (e.code !== 'ENOENT') console.warn(`[schools] 读取失败 ${file}: ${e.message}`);
- return fallback;
- }
- }
- function normalizeEntry(raw) {
- if (!raw || !raw.name) return null;
- const name = String(raw.name).trim();
- if (!name) return null;
- const lng = Number(raw.lng);
- const lat = Number(raw.lat);
- const hasPoint = Number.isFinite(lng) && Number.isFinite(lat)
- && lng > 73 && lng < 136 && lat > 3 && lat < 54;
- return {
- id: raw.id || `sch_${Buffer.from(name).toString('base64url').slice(0, 10)}`,
- name,
- aliases: Array.isArray(raw.aliases) ? raw.aliases.map(String) : [],
- province: raw.province || '',
- city: raw.city || '',
- county: raw.county || '',
- lng: hasPoint ? lng : null,
- lat: hasPoint ? lat : null,
- source: raw.source || 'local',
- seed: raw.seed === true,
- enabled: raw.enabled !== false,
- created_at: raw.created_at || '',
- updated_at: raw.updated_at || '',
- };
- }
- /** 读原始条目(不补默认值):合并时才能区分「显式给了空值」和「没给」 */
- function readRawList(file) {
- const data = readJson(file, null);
- if (!data || !Array.isArray(data.schools)) return [];
- return data.schools.filter((item) => item && typeof item === 'object' && String(item.name || '').trim());
- }
- /** 合并后的学校列表(种子 + 运行时),按省份、名称排序 */
- function loadSchools() {
- const merged = new Map();
- for (const raw of readRawList(SEED_FILE)) {
- const name = String(raw.name).trim();
- merged.set(name, { ...raw, name, seed: true });
- }
- for (const raw of readRawList(RUNTIME_FILE)) {
- const name = String(raw.name).trim();
- const base = merged.get(name);
- merged.set(name, {
- // 只覆盖运行时显式给出的字段
- ...(base || {}),
- ...raw,
- name,
- // 种子标记不可被运行时文件伪造/清除
- seed: !!(base && base.seed),
- });
- }
- return [...merged.values()].map(normalizeEntry).filter(Boolean).sort((a, b) => {
- const p = String(a.province).localeCompare(String(b.province), 'zh-Hans-CN');
- return p !== 0 ? p : String(a.name).localeCompare(String(b.name), 'zh-Hans-CN');
- });
- }
- function runtimeFile() {
- return RUNTIME_FILE;
- }
- function seedFile() {
- return SEED_FILE;
- }
- module.exports = { loadSchools, normalizeEntry, runtimeFile, seedFile };
|