# 全国校区地图 · 设计文档(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 所**): ```json { "中国计量大学": { "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 可切): ```sql 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. **关联跳转**:详情卡提供「查看运营数据」,跳转「店铺运营」并预选该学校 3. 不做批量导入;创建后默认启用