2026-10-20-campus-map-design.md 12 KB

全国校区地图 · 设计文档(v2:自动定位版)

  • 日期:2026-10-20
  • 分支:dev
  • 状态:待评审(本阶段只出设计,不写实现)
  • 变更:取消「选省/市 + 地图点选」流程,改为输入学校名自动定位

1. 背景与目标

已有「店铺运营」按学校分组看数据,但缺少全国视角。

新增一级菜单 校区地图:

  • 中国地图上展示所有学校;启用的学校点亮(涟漪 + 名称),停用的显示为灰点
  • 界面上创建学校:只填名称,系统自动定位并点亮
  • 列表与「店铺运营」数据关联,显示该学校的门店数与最近数据日期
  • 删除有运营数据的学校时二次确认

2. 关键决策

决策 结论
位置来源 本地高校坐标库自动匹配(2820 所),不选省市、不点地图、不填经纬度
未收录的学校 ① 在线地理编码兜底(Photon)→ ② 仍失败则创建为「未定位」,不点亮,列表可点「重新定位」重试
坐标系 统一 GCJ-02(底图坐标系);数据源是 BD-09 需转换,在线兜底是 WGS84 需转换
省份 由坐标预先算好(点落省份多边形),不需要用户选
批量导入 不做
删除保护 有同名运营数据时二次确认

3. 交互设计

3.1 创建学校

[新建学校] → 弹窗:输入「学校名称」(即时联想)
                 ┌──────────────────────────────┐
                 │ 中国计量大学     浙江省        │ ← 本地库命中
                 │ 中国计量大学现代科技学院  浙江省 │
                 └──────────────────────────────┘
        ↓ 选中
   地图自动飞到该点(zoom 8)→ 落标记(脉冲)→ 底部确认卡:
        ┌──────────────────────────────────────┐
        │ 中国计量大学 · 浙江省                  │
        │ 坐标 120.3626, 30.3210(GCJ-02)      │
        │              [取消]  [创建并点亮]      │
        └──────────────────────────────────────┘
        ↓
   创建成功 → 点位橙色涟漪 + 学校名,列表新增一条

规则:

  • 精确且唯一命中(如「中国计量大学」):直接显示确认卡,一次点击完成
  • 多条命中(如「中国计量大学」与「中国计量大学现代科技学院」):候选列表选择,带省份区分
  • 无命中:显示「未收录该学校」+ [尝试在线定位];在线也失败时提供 [先创建(未定位)]
  • 创建默认启用(点亮),可立即关闭

3.2 编辑

点列表项 → 右侧详情卡:

┌ 中国计量大学 ──────────────────┐
│ 浙江省 · 120.3626, 30.3210      │
│ 运营数据:9 家门店 · 最近 10-07  │
│ [重新匹配]  [停用]  [删除]       │
└────────────────────────────────┘
  • 重新匹配:回到搜索框,输入新名称重新定位(覆盖坐标)
  • 改名:详情卡标题可编辑
  • 不提供拖动或点选微调(遵循"不点地图"的要求)

3.3 删除保护

若「店铺运营」中存在同名学校:

确定删除「中国计量大学」?
店铺运营中有 9 家门店使用了该名称,最近数据日期 2026-10-07。
删除学校不会删除运营数据。
[取消] [仍然删除]

3.4 地图交互

操作 行为
滚轮 / 双指 缩放(zoom 3~12)
拖拽 平移
悬停点位 tooltip:学校名 · 省份 · 门店数
点击点位 选中并高亮,右侧滚动到对应卡片
搜索框(列表上方) 过滤列表;点结果地图飞过去
[回到全国] 恢复默认视野

3.5 空态与异常

场景 展示
没有学校 地图灰显 + 中央「创建第一所学校」
存储不可用 地图正常渲染 + 顶部提示「学校数据暂不可用」+ 重试
底图加载失败 提示刷新,不渲染空画布
名称重复 保存时提示「已存在同名学校」
未定位学校 列表中灰色标签「未定位」,可重新定位

4. 定位数据与算法

4.1 本地高校库

public/vendor/cn-universities.json(238KB,2820 所):

{
  "中国计量大学": { "lng": 120.362644, "lat": 30.321047, "province": "浙江省" },
  "浙江科技学院": { "lng": 120.025229, "lat": 30.220034, "province": "浙江省" }
}

生成方式(构建期一次性;数据已生成,生成脚本在实施阶段一并提供):

  1. 取开源数据集 flwfdd/china-university-location(2820 所,百度坐标)
  2. BD-09 → GCJ-02 转换
  3. 用省级 GeoJSON 做点在多边形内判断,写入 province
  4. 输出精简 JSON(不含冗余字段)

坐标系验证(以 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 的系统性偏移,所以转换是必须的。残差在校区尺度内(校区本身就有几百米跨度),可接受。

4.2 名称匹配

按顺序尝试,命中即止:

  1. 精确匹配(去首尾空格)
  2. 归一化匹配:去掉括号内容、空格、全半角差异(中国计量大学(下沙校区) → 中国计量大学)
  3. 前缀/包含匹配(计量大学 → 中国计量大学)

候选排序:精确 > 归一化 > 包含;同分时名称短的优先。最多返回 8 条,带省份用于区分。

4.3 未收录兜底

在线地理编码(服务端发起,避免浏览器跨域):

  • 请求 https://photon.komoot.io/api/?q=<名称>&limit=5
  • 只接受 countrycode=CN 且 name 与输入相似度足够高的结果
  • 返回 WGS84 → WGS84→GCJ-02 转换后使用
  • 超时 8s;失败则创建为「未定位」

设计上把「在线兜底」做成可选动作(用户点按钮触发),不做成创建时自动请求——避免无网络环境下创建流程卡住。

4.4 点亮

geo(省级底图,roam)
├── effectScatter「已启用」:橙色 #ff6b35,rippleEffect,右侧名称标签
└── scatter「未启用 / 未定位」:灰色 #b8b2ad,无标签

点击、悬停、选中联动;同屏像素距离 <14px 的两点做 ≤8px 确定性偏移。

5. 数据模型

沿用 ops 存储(SQLite 默认 / PG 可切):

CREATE TABLE IF NOT EXISTS ops_school (
  id         text PRIMARY KEY,          -- sch_20261020_a1b2c3
  name       text NOT NULL,
  province   text NOT NULL DEFAULT '',
  lng        real,                      -- GCJ-02;未定位时为 NULL
  lat        real,
  source     text NOT NULL DEFAULT 'local',  -- local | geocode | none
  enabled    integer NOT NULL DEFAULT 1,
  created_at text NOT NULL,
  updated_at text NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS ops_school_name_idx ON ops_school (name);
  • lng/lat 允许 NULL = 未定位(不点亮)
  • source 记录定位来源,便于排查
  • 与运营数据的关联:按 ops_shop.school = ops_school.name 精确匹配,查询时聚合门店数与 MAX(date)

6. 接口

方法 路径 说明
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)。

7. 界面

┌──────────────────────────────────────────────────────────────┐
│ 校区地图                          + 新建学校                 │
│ 已启用 3 / 共 5                                              │
├───────────────────────────────────┬──────────────────────────┤
│                                   │ 🔍 搜索                   │
│        中国地图(可缩放)           │ 浙江省 2/3               │
│                                   │ ┌──────────────────────┐ │
│      ● 中国计量大学                │ │● 中国计量大学 [开]    │ │
│      ● 浙江工商大学桐庐校区         │ │  9 店 · 最近 10-07    │ │
│      ○ 浙江科技学院(未启用)       │ ├──────────────────────┤ │
│                                   │ │○ 浙江科技学院 [关]    │ │
│                  [回到全国]        │ │  未关联运营数据        │ │
│                                   │ └──────────────────────┘ │
└───────────────────────────────────┴──────────────────────────┘
  • 列表按省份分组,组头显示启用数
  • 每项显示「N 店 · 最近日期」;无关联数据显示「未关联运营数据」
  • 点击列表项 → 地图飞过去并选中
  • 宽度 <1100px 时列表移到地图下方

8. 与现有功能的关系

  • 独立菜单,不改动店铺运营现有行为
  • 通过 ops_shop.school 名称关联,仅做只读展示(门店数、最近日期)
  • 不参与采集、不参与钉钉推送

9. 测试

9.1 单元

  • BD-09 → GCJ-02 转换(用 §4.1 五组值断言,误差 < 200m)
  • 名称匹配:精确 / 带括号 / 简称 / 无命中
  • 坐标范围校验(境内 / 境外)
  • 省份识别(中国计量大学 → 浙江省;境外点 → 空)

9.2 接口(临时 SQLite)

  • 创建(带坐标 / 仅名称自动匹配)→ 列表可见
  • 重名 400;停用/启用;删除
  • 未定位学校(lng/lat 为空)不报错,enabled=1 也不点亮
  • 关联字段:造 2 条 ops_shop 同名数据 → 返回门店数 2 与最近日期
  • 存储不可用 → GET 200 + enabled:false,写操作 503

9.3 冒烟

GET /api/schools 200(只读)

9.4 手工验收

输入「中国计量大学」→ 联想出现 → 选中 → 地图飞到杭州下沙 → 创建 → 点位点亮 → 刷新仍在 → 列表显示关联门店数与最近日期 → 停用变灰 → 删除时若有运营数据出现二次确认。

10. 待办清单(评审通过后拆计划)

  • scripts/build-university-data.js(数据生成脚本,含 BD-09 转换与省份归属;数据文件已就绪)
  • public/vendor/cn-universities.json(已生成,待登记静态白名单)
  • ops_school 表 + 存储方法(SQLite / PG)
  • /api/schools 端点(含 search / locate / 关联查询 / 删除保护所需计数)
  • public/schools.js:地图、联想、创建、编辑、启停、删除确认、列表联动
  • index.html 菜单与页面、app.js 注册、style.css
  • 测试脚本扩展 + smoke 只读断言
  • README 增补「校区地图」与坐标系说明

11. 待确认

  1. 「未收录学校」走在线兜底失败后,是否允许创建为「未定位」(灰点、不点亮)?还是直接拒绝创建?
  2. 列表点击是否要顺带跳转到「店铺运营并筛选该学校」?(关联体验更完整,约 +0.5 天)