|
@@ -3,10 +3,45 @@
|
|
|
// 校区地图:学校列表(JSON 种子 + 运行时),附带运营数据关联统计。
|
|
// 校区地图:学校列表(JSON 种子 + 运行时),附带运营数据关联统计。
|
|
|
// 本期只有只读接口;新建/编辑/启停在下一增量实现。
|
|
// 本期只有只读接口;新建/编辑/启停在下一增量实现。
|
|
|
|
|
|
|
|
|
|
+const fs = require('fs');
|
|
|
|
|
+const path = require('path');
|
|
|
const { sendJson } = require('../util.js');
|
|
const { sendJson } = require('../util.js');
|
|
|
const { loadSchools } = require('../../schools-store.js');
|
|
const { loadSchools } = require('../../schools-store.js');
|
|
|
const { createOpsStore } = require('../../ops-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) {
|
|
function namesOf(school) {
|
|
|
return new Set([school.name, ...(school.aliases || [])].map((n) => String(n).trim()).filter(Boolean));
|
|
return new Set([school.name, ...(school.aliases || [])].map((n) => String(n).trim()).filter(Boolean));
|
|
|
}
|
|
}
|
|
@@ -30,6 +65,36 @@ function register(app, ctx) {
|
|
|
opsAvailable = true;
|
|
opsAvailable = true;
|
|
|
} catch { /* ops 库不可用:只影响关联字段 */ }
|
|
} 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 payload = schools.map((school) => {
|
|
|
const names = namesOf(school);
|
|
const names = namesOf(school);
|
|
|
// 门店口径:人工表的 shop_key 与平台明细的 shopId 取并集
|
|
// 门店口径:人工表的 shop_key 与平台明细的 shopId 取并集
|