dev已有「店铺运营」按学校分组看数据,但缺少全国视角。
新增一级菜单 校区地图:
| 决策 | 结论 |
|---|---|
| 位置来源 | 本地高校坐标库自动匹配(2820 所),不选省市、不点地图、不填经纬度 |
| 未收录的学校 | ① 在线地理编码兜底(Photon)→ ② 仍失败则创建为「未定位」,不点亮,列表可点「重新定位」重试 |
| 坐标系 | 统一 GCJ-02(底图坐标系);数据源是 BD-09 需转换,在线兜底是 WGS84 需转换 |
| 省份 | 由坐标预先算好(点落省份多边形),不需要用户选 |
| 存储 | JSON 文件(种子 + 运行时),不依赖数据库;Node 18 也可用 |
| 内置学校 | 预置「浙江工商大学桐庐校区」(别名:浙江工商桐庐校区等) |
| 批量导入 | 不做 |
| 删除保护 | 有同名运营数据时二次确认 |
[新建学校] → 弹窗:输入「学校名称」(即时联想)
┌──────────────────────────────┐
│ 中国计量大学 浙江省 │ ← 本地库命中
│ 中国计量大学现代科技学院 浙江省 │
└──────────────────────────────┘
↓ 选中
地图自动飞到该点(zoom 8)→ 落标记(脉冲)→ 底部确认卡:
┌──────────────────────────────────────┐
│ 中国计量大学 · 浙江省 │
│ 坐标 120.3626, 30.3210(GCJ-02) │
│ [取消] [创建并点亮] │
└──────────────────────────────────────┘
↓
创建成功 → 点位橙色涟漪 + 学校名,列表新增一条
规则:
[尝试在线定位];在线也失败时提供 [先创建(未定位)]点列表项 → 右侧详情卡:
┌ 中国计量大学 ──────────────────┐
│ 浙江省 · 120.3626, 30.3210 │
│ 运营数据:9 家门店 · 最近 10-07 │
│ [重新匹配] [停用] [删除] │
└────────────────────────────────┘
若「店铺运营」中存在同名学校:
确定删除「中国计量大学」?
店铺运营中有 9 家门店使用了该名称,最近数据日期 2026-10-07。
删除学校不会删除运营数据。
[取消] [仍然删除]
| 操作 | 行为 |
|---|---|
| 滚轮 / 双指 | 缩放(zoom 3~12) |
| 拖拽 | 平移 |
| 悬停点位 | tooltip:学校名 · 省份 · 门店数 |
| 点击点位 | 选中并高亮,右侧滚动到对应卡片 |
| 搜索框(列表上方) | 过滤列表;点结果地图飞过去 |
| [回到全国] | 恢复默认视野 |
| 场景 | 展示 |
|---|---|
| 没有学校 | 地图灰显 + 中央「创建第一所学校」 |
| 存储不可用 | 地图正常渲染 + 顶部提示「学校数据暂不可用」+ 重试 |
| 底图加载失败 | 提示刷新,不渲染空画布 |
| 名称重复 | 保存时提示「已存在同名学校」 |
| 未定位学校 | 列表中灰色标签「未定位」,可重新定位 |
public/vendor/cn-universities.json(238KB,2820 所):
{
"中国计量大学": { "lng": 120.362644, "lat": 30.321047, "province": "浙江省" },
"浙江科技学院": { "lng": 120.025229, "lat": 30.220034, "province": "浙江省" }
}
生成方式(构建期一次性;数据已生成,生成脚本在实施阶段一并提供):
flwfdd/china-university-location(2820 所,百度坐标)province坐标系验证(以 Photon 返回的 WGS84 → GCJ-02 为参照):
| 学校 | 数据集原始(BD-09) | 转换后 GCJ-02 | 与参照偏差 |
|---|---|---|---|
| 清华大学 | 116.33337, 40.00964 | 116.32677, 40.00391 | 45m |
| 北京大学 | 116.31683, 39.99888 | 116.31031, 39.99288 | 33m |
| 武汉大学 | 114.37292, 30.54380 | 114.36651, 30.53748 | 274m |
| 中国计量大学 | 120.36904, 30.32740 | 120.36264, 30.32105 | 284m |
| 复旦大学 | 121.51077, 31.30187 | 121.50419, 31.29623 | 392m |
不转换直接用会是 850~1200m 的系统性偏移,所以转换是必须的。残差在校区尺度内(校区本身就有几百米跨度),可接受。
按顺序尝试,命中即止:
中国计量大学(下沙校区) → 中国计量大学)计量大学 → 中国计量大学)候选排序:精确 > 归一化 > 包含;同分时名称短的优先。最多返回 8 条,带省份用于区分。
在线地理编码(服务端发起,避免浏览器跨域):
https://photon.komoot.io/api/?q=<名称>&limit=5countrycode=CN 且 name 与输入相似度足够高的结果设计上把「在线兜底」做成可选动作(用户点按钮触发),不做成创建时自动请求——避免无网络环境下创建流程卡住。
geo(省级底图,roam)
├── effectScatter「已启用」:橙色 #ff6b35,rippleEffect,右侧名称标签
└── scatter「未启用 / 未定位」:灰色 #b8b2ad,无标签
点击、悬停、选中联动;同屏像素距离 <14px 的两点做 ≤8px 确定性偏移。
| 文件 | 是否提交到仓库 | 作用 |
|---|---|---|
server/schools.seed.json |
提交 | 内置学校(随版本发布);初始只有浙江工商桐庐校区 |
data/schools.json |
不提交(data/ 已 gitignore) |
运行时新增/修改的学校,以及对内置项的覆盖 |
读取时合并两者,对外的列表就是合并结果;写入只动 data/schools.json。
{
"id": "sch_tonglu_001",
"name": "浙江工商大学桐庐校区",
"aliases": ["浙江工商桐庐校区", "浙江工商大学杭州商学院", "杭州商学院桐庐校区"],
"province": "浙江省",
"city": "杭州市",
"county": "桐庐县",
"lng": 119.744166,
"lat": 29.830417,
"source": "seed",
"seed": true,
"enabled": true,
"created_at": "2026-10-20T00:00:00.000Z",
"updated_at": "2026-10-20T00:00:00.000Z"
}
字段说明:
name 为唯一键;aliases 只用于搜索联想,不参与唯一性lng/lat(GCJ-02)为 null 表示「未定位」(灰点、不点亮)seed: true 表示内置项[119.744166, 29.830417]| 操作 | 行为 |
|---|---|
| 读取 | 种子 + 运行时;同名(name)时运行时字段覆盖种子 |
| 新建 | 只写 data/schools.json;若与种子/运行时同名 → 400 |
| 修改(改名/重新定位/启停) | 写 data/schools.json 的同名记录(对种子项即"覆盖层") |
| 删除 | 运行时项:从文件移除;种子项:拒绝删除,提示「内置学校只能停用」 |
| 文件缺失/损坏 | 用种子继续工作,日志 warning,不崩溃 |
| 写入 | 临时文件 + rename 原子替换;单进程串行 |
学校存 JSON,运营数据在 ops 库(SQLite/PG),关联在读取时做:
ops_school_name(即学校名及别名)去 ops_shop.school 精确匹配| 字段 | 值 |
|---|---|
| 名称 | 浙江工商大学桐庐校区 |
| 别名 | 浙江工商桐庐校区 / 浙江工商大学杭州商学院 / 杭州商学院桐庐校区 |
| 省/市/县 | 浙江省 / 杭州市 / 桐庐县 |
| 坐标(GCJ-02) | 119.744166, 29.830417 |
| 默认状态 | 启用(点亮) |
名称采用「浙江工商大学桐庐校区」是为了和上传表里的
学校名称对得上(人工统计表用的就是全称),别名保证用户输入简称也能搜到。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/schools |
列表,含 enabled、坐标、门店数、最近数据日期 |
| GET | /api/schools/search?q= |
本地高校库联想(也可前端直接加载 JSON 搜索,二选一) |
| POST | /api/schools |
创建:{ name, lng?, lat?, province?, source? };缺坐标时按名称自动匹配 |
| POST | /api/schools/:id |
更新(改名 / 重新匹配 / 坐标) |
| POST | /api/schools/:id/toggle |
启用/停用 |
| POST | /api/schools/locate |
在线兜底:{ name } → { lng, lat, province, source } |
| DELETE | /api/schools/:id |
删除 |
校验:名称 1~60 且唯一;坐标若提供必须在 GCJ-02 中国范围(lng 73~136,lat 3~54)。
存储相关:写操作失败(磁盘只读等)返回 500 并保持文件原样;GET /api/schools 始终可用(JSON 不依赖数据库)。测试通过环境变量 SCHOOLS_FILE 指定临时文件,避免污染本机 data/schools.json。
┌──────────────────────────────────────────────────────────────┐
│ 校区地图 + 新建学校 │
│ 已启用 3 / 共 5 │
├───────────────────────────────────┬──────────────────────────┤
│ │ 🔍 搜索 │
│ 中国地图(可缩放) │ 浙江省 2/3 │
│ │ ┌──────────────────────┐ │
│ ● 中国计量大学 │ │● 中国计量大学 [开] │ │
│ ● 浙江工商大学桐庐校区 │ │ 9 店 · 最近 10-07 │ │
│ ○ 浙江科技学院(未启用) │ ├──────────────────────┤ │
│ │ │○ 浙江科技学院 [关] │ │
│ [回到全国] │ │ 未关联运营数据 │ │
│ │ └──────────────────────┘ │
└───────────────────────────────────┴──────────────────────────┘
ops_shop.school 名称关联,仅做只读展示(门店数、最近日期)enabled=1 也不点亮ops_shop 同名数据 → 返回门店数 2 与最近日期GET /api/schools 仍 200,关联字段为空GET /api/schools 200(只读)
输入「中国计量大学」→ 联想出现 → 选中 → 地图飞到杭州下沙 → 创建 → 点位点亮 → 刷新仍在 → 列表显示关联门店数与最近日期 → 停用变灰 → 删除时若有运营数据出现二次确认。
scripts/build-university-data.js(数据生成脚本,含 BD-09 转换与省份归属;数据文件已就绪)public/vendor/cn-universities.json(已生成,待登记静态白名单)server/schools.seed.json(内置浙江工商桐庐校区)schools-store.js:种子 + 运行时文件合并、原子写、损坏兜底/api/schools 端点(含 search / locate / 关联查询 / 删除保护)public/schools.js:地图、联想、创建、编辑、启停、删除确认、列表联动index.html 菜单与页面、app.js 注册、style.css