'use strict'; // 校区地图:学校列表(JSON 种子 + 运行时),附带运营数据关联统计。 // 本期只有只读接口;新建/编辑/启停在下一增量实现。 const fs = require('fs'); const path = require('path'); const { sendJson } = require('../util.js'); const { loadSchools } = require('../../schools-store.js'); const { createOpsStore } = require('../../ops-store.js'); // 导入数据里出现、但还没登记的校区:用本地高校坐标库自动定位(找不到就标未定位) let uniCache = null; function loadUniversities() { if (uniCache) return uniCache; try { uniCache = JSON.parse(fs.readFileSync(path.join(__dirname, '../../public/vendor/cn-universities.json'), 'utf8')); } catch { uniCache = {}; } return uniCache; } function normalizeSchoolName(name) { return String(name || '') .replace(/\s+/g, '') .replace(/[((].*?[))]/g, '') .replace(/(校区|分校)$/, ''); } function matchUniversity(name) { const uni = loadUniversities(); const norm = normalizeSchoolName(name); if (!norm) return null; if (uni[norm]) return { lng: uni[norm].lng, lat: uni[norm].lat, province: uni[norm].province, approx: false }; let bestKey = ''; for (const key of Object.keys(uni)) { if (key.length < 4) continue; if (norm.startsWith(key) && key.length > bestKey.length) bestKey = key; } if (!bestKey) return null; return { lng: uni[bestKey].lng, lat: uni[bestKey].lat, province: uni[bestKey].province, approx: true, base: bestKey }; } function namesOf(school) { return new Set([school.name, ...(school.aliases || [])].map((n) => String(n).trim()).filter(Boolean)); } function register(app, ctx) { const opsStore = createOpsStore(ctx.config); app.get('/api/schools', async ({ res }) => { const schools = loadSchools(); // 关联运营数据:按学校名(含别名)匹配 ops_shop.school 与 ops_row.school let shops = []; let rows = []; let platformRows = []; let opsAvailable = false; try { const summary = await opsStore.summary(); shops = summary.shops || []; rows = await opsStore.queryRows({}); platformRows = await opsStore.queryPlatformRows({}); opsAvailable = true; } catch { /* ops 库不可用:只影响关联字段 */ } // 运营数据里出现、注册表里没有的学校:自动补进地图列表 const knownNames = new Set(); for (const school of schools) for (const n of namesOf(school)) knownNames.add(n); const discovered = new Set(); for (const item of [...shops, ...platformRows]) { const name = String(item.school || '').trim(); if (name && !knownNames.has(name)) discovered.add(name); } const now = new Date().toISOString(); for (const name of discovered) { const hit = matchUniversity(name); schools.push({ id: 'ops_' + Buffer.from(name).toString('hex').slice(0, 12), name, aliases: [], province: (hit && hit.province) || '', city: '', county: '', lng: hit ? hit.lng : null, lat: hit ? hit.lat : null, source: 'ops', seed: false, discovered: true, approx: !!(hit && hit.approx), enabled: !!hit, created_at: now, updated_at: now, }); } const payload = schools.map((school) => { const names = namesOf(school); // 门店口径:人工表的 shop_key 与平台明细的 shopId 取并集 const shopKeys = new Set(); for (const shop of shops) { if (names.has(String(shop.school || ''))) shopKeys.add(String(shop.key || shop.shopId || '')); } for (const row of platformRows) { if (names.has(String(row.school || ''))) shopKeys.add(String(row.shopId || '')); } shopKeys.delete(''); const shopCount = shopKeys.size; let lastDate = ''; for (const row of rows) { if (!names.has(String(row.school || ''))) continue; if (!lastDate || row.date > lastDate) lastDate = row.date; } for (const row of platformRows) { if (!names.has(String(row.school || ''))) continue; if (!lastDate || row.date > lastDate) lastDate = row.date; } return { ...school, ops: opsAvailable ? { shopCount, lastDate: lastDate || null } : null, }; }); sendJson(res, 200, { schools: payload, opsAvailable, enabledCount: payload.filter((s) => s.enabled && s.lng != null).length, }); }); } module.exports = { register };