|
@@ -0,0 +1,249 @@
|
|
|
|
|
+# 全国校区地图 · 设计文档
|
|
|
|
|
+
|
|
|
|
|
+- 日期:2026-10-20
|
|
|
|
|
+- 分支:`dev`
|
|
|
|
|
+- 状态:待评审(本阶段只出设计,不写实现)
|
|
|
|
|
+
|
|
|
|
|
+## 1. 背景与目标
|
|
|
|
|
+
|
|
|
|
|
+已有「店铺运营」按学校分组看数据,但缺少一个**全国视角**:哪些学校在用、分布在哪、开了几家。
|
|
|
|
|
+
|
|
|
|
|
+新增一级菜单 **校区地图**:
|
|
|
|
|
+
|
|
|
|
|
+- 中国地图上展示所有学校;**启用的学校点亮**(涟漪 + 名称),停用的显示为灰点
|
|
|
|
|
+- 可在界面上**创建学校**:填名称、在地图上点出位置、启用
|
|
|
|
|
+- 点击点位或列表项可查看/编辑/停用/删除
|
|
|
|
|
+
|
|
|
|
|
+## 2. 已定的关键决策
|
|
|
|
|
+
|
|
|
|
|
+| 决策 | 结论 | 原因 |
|
|
|
|
|
+|---|---|---|
|
|
|
|
|
+| 位置来源 | **必须在地图上主动点选**,不提供经纬度输入框 | 一个城市有多所高校,城市中心会重叠且偏差大;手填经纬度对使用者不友好 |
|
|
|
|
|
+| 默认位置 | **没有默认位置**,未落点不允许保存 | 避免"看起来创建成功、点却落在市中心" |
|
|
|
|
|
+| 城市字段 | 只作为**地图跳转的辅助**(选城市 → 地图飞过去 → 再点精确位置),不决定落点 | 兼顾"快速定位到城市"和"精确到校区" |
|
|
|
|
|
+| 省份 | 落点后**自动识别**(点在哪个省),也可手动改 | 避免坐标与省份对不上 |
|
|
|
|
|
+| 坐标系 | 统一存 **GCJ-02**(底图坐标系) | 点选天然得到 GCJ-02;混用 WGS84 会有几百米偏差(见 §4.3) |
|
|
|
|
|
+
|
|
|
|
|
+## 3. 交互设计
|
|
|
|
|
+
|
|
|
|
|
+### 3.1 创建学校(主流程)
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+[新建学校] → 弹窗:学校名称
|
|
|
|
|
+ 可选:省份 → 城市(仅用于把地图飞过去)
|
|
|
|
|
+ [取消] [在地图上选择位置]
|
|
|
|
|
+ ↓
|
|
|
|
|
+ 弹窗收起,地图进入「定位模式」:
|
|
|
|
|
+ 顶部提示条:请在地图上点击学校所在位置 · Esc 取消
|
|
|
|
|
+ 鼠标变十字;可选省份时先飞到该省/市(zoom 6~9)
|
|
|
|
|
+ ↓
|
|
|
|
|
+ 点击地图 → 落一个临时标记(脉冲动画)
|
|
|
|
|
+ 提示条变成:已选位置(自动识别的省份) [重选] [确认创建]
|
|
|
|
|
+ ↓
|
|
|
|
|
+ 确认 → 保存并默认「启用」→ 点位变橙色涟漪,列表新增一条
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+要点:
|
|
|
|
|
+
|
|
|
|
|
+- **没点位置时"确认创建"不可用**,从流程上杜绝城市中心兜底
|
|
|
|
|
+- 落点后自动识别省份;识别不出(点在海上/境外)时提示"该位置不在中国境内,请重新选择"
|
|
|
|
|
+- 同城多校因为各自精确点选,天然分散,不会叠在一起
|
|
|
|
|
+
|
|
|
|
|
+### 3.2 编辑与重新定位
|
|
|
|
|
+
|
|
|
|
|
+列表项或点位 → 右侧详情卡:
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+┌ 中国计量大学 ────────────────┐
|
|
|
|
|
+│ 浙江省 · 杭州市 │
|
|
|
|
|
+│ 坐标 120.3626, 30.3187 │ ← 只读展示,不可编辑输入框
|
|
|
|
|
+│ [重新定位] [停用] [删除] │
|
|
|
|
|
+└──────────────────────────────┘
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- **重新定位**:再次进入定位模式,地图飞到该点(zoom 9),点击新位置后确认覆盖
|
|
|
|
|
+- 名称可改(点详情卡标题进入编辑)
|
|
|
|
|
+- 删除需二次确认
|
|
|
|
|
+
|
|
|
|
|
+### 3.3 启用 / 停用
|
|
|
|
|
+
|
|
|
|
|
+- 列表每项一个开关;打开 = 点亮,关闭 = 灰点
|
|
|
|
|
+- 开关即时生效,不需要保存按钮
|
|
|
|
|
+- 停用只影响地图展示,不删除学校、不影响「店铺运营」数据
|
|
|
|
|
+
|
|
|
|
|
+### 3.4 地图交互
|
|
|
|
|
+
|
|
|
|
|
+| 操作 | 行为 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| 滚轮 / 双指 | 缩放(zoom 3 ~ 12) |
|
|
|
|
|
+| 拖拽 | 平移 |
|
|
|
|
|
+| 双击省份 | 放大到该省 |
|
|
|
|
|
+| 悬停点位 | tooltip:学校名 · 省/市 · 状态 |
|
|
|
|
|
+| 点击点位 | 选中并高亮,右侧滚动到对应卡片 |
|
|
|
|
|
+| 搜索框 | 按名称过滤列表;点结果 → 地图飞到该点(zoom 9) |
|
|
|
|
|
+| [回到全国] | 恢复默认视野(center 104, 35 / zoom 1.2) |
|
|
|
|
|
+
|
|
|
|
|
+### 3.5 空态与异常
|
|
|
|
|
+
|
|
|
|
|
+| 场景 | 展示 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| 一所学校都没有 | 地图灰显 + 中央「新建第一所学校」按钮 + 一行说明 |
|
|
|
|
|
+| 存储不可用 | 地图正常显示,顶部提示「学校数据暂不可用」+ 重试,创建按钮禁用 |
|
|
|
|
|
+| 底图加载失败 | 提示「地图数据加载失败,请刷新」,不渲染空白画布 |
|
|
|
|
|
+| 名称重复 | 保存时提示「已存在同名学校」,不创建 |
|
|
|
|
|
+| 点位在境外 | 拒绝保存并提示重新选择 |
|
|
|
|
|
+
|
|
|
|
|
+## 4. 地图技术方案
|
|
|
|
|
+
|
|
|
|
|
+### 4.1 技术选型
|
|
|
|
|
+
|
|
|
|
|
+- **ECharts 5.6.0**(仓库已内置 `public/vendor/echarts.min.js`),`geo` 组件 + `effectScatter` 系列
|
|
|
|
|
+- **底图**:中国省级 GeoJSON,本地内置 `public/vendor/china-map.json`(569KB,35 个省级面,含南海诸岛)
|
|
|
|
|
+- **城市定位库**:`public/vendor/cn-cities.json`(19KB,35 省 / 477 个城市区县的中心坐标,只用于"飞到城市")
|
|
|
|
|
+- 全部离线,不依赖任何地图服务或 Key
|
|
|
|
|
+
|
|
|
|
|
+### 4.2 点亮实现
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+geo(底图,roam 可缩放平移)
|
|
|
|
|
+├── effectScatter series「已启用」
|
|
|
|
|
+│ symbolSize 10~14,颜色 #ff6b35
|
|
|
|
|
+│ rippleEffect { brushType: 'stroke', scale: 3.2, period: 3.5 }
|
|
|
|
|
+│ label { show: true, formatter: 学校名, position: right }
|
|
|
|
|
+│
|
|
|
|
|
+└── scatter series「未启用」
|
|
|
|
|
+ symbolSize 6,颜色 #b8b2ad,无涟漪、无标签
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- 点击:`chart.on('click', params => selectSchool(params.data.id))`
|
|
|
|
|
+- 悬停:tooltip formatter 输出三行(名称 / 省市 / 状态)
|
|
|
|
|
+- 选中:被选中的点 symbolSize +4、边框加亮,右侧卡片同步高亮
|
|
|
|
|
+- 同城多点重叠:先按经纬度绘制;若两点**屏幕像素距离 < 14px**,对后者做 ≤8px 的确定性偏移(按 id 哈希),保证都看得见
|
|
|
|
|
+
|
|
|
|
|
+### 4.3 坐标系(重要)
|
|
|
|
|
+
|
|
|
|
|
+中国互联网地图(阿里 DataV、高德)用的是 **GCJ-02**,而 GPS / OpenStreetMap 是 **WGS84**,两者在国内相差 300~600 米。
|
|
|
|
|
+
|
|
|
|
|
+| 例子 | WGS84 | GCJ-02 | 偏差 |
|
|
|
|
|
+|---|---|---|---|
|
|
|
|
|
+| 中国计量大学 | 120.3580, 30.3210 | **120.3626, 30.3187** | 约 440m / 254m |
|
|
|
|
|
+| 杭州市中心 | 120.1536, 30.2875 | 120.1583, 30.2851 | 约 452m / 257m |
|
|
|
|
|
+| 天安门 | 116.3910, 39.9070 | 116.3972, 39.9084 | 约 533m / 155m |
|
|
|
|
|
+
|
|
|
|
|
+结论:
|
|
|
|
|
+
|
|
|
|
|
+- **用户在底图上点选的坐标天然是 GCJ-02**,直接存,所见即所得
|
|
|
|
|
+- 外部来源(GPS、OSM、别人给的经纬度)是 WGS84,入库前要转换;提供 `wgs84ToGcj02()` 工具函数与一条 `scripts/` 小工具供批量导入
|
|
|
|
|
+- 只读展示坐标时标注「GCJ-02」,避免误用
|
|
|
|
|
+
|
|
|
|
|
+## 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 '',
|
|
|
|
|
+ city text NOT NULL DEFAULT '',
|
|
|
|
|
+ lng real NOT NULL, -- GCJ-02
|
|
|
|
|
+ lat real NOT NULL,
|
|
|
|
|
+ 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);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+设计说明:
|
|
|
|
|
+
|
|
|
|
|
+- `name` 唯一,防止重复创建同一所学校
|
|
|
|
|
+- `province` / `city` 为展示与分组用;`province` 由落点自动识别,`city` 取用户在下拉里选的值(允许为空)
|
|
|
|
|
+- 与「店铺运营」的关联:`ops_shop.school` 是字符串,按 **名称相等** 关联;创建时会提示「与 N 个运营数据中的学校匹配」,对不上也允许创建(先画图、后补数据)
|
|
|
|
|
+- `enabled` 用 0/1,SQLite 与 PG 都兼容
|
|
|
|
|
+
|
|
|
|
|
+## 6. 接口
|
|
|
|
|
+
|
|
|
|
|
+| 方法 | 路径 | 说明 |
|
|
|
|
|
+|---|---|---|
|
|
|
|
|
+| GET | `/api/schools` | 列表(含 enabled、坐标、省市) |
|
|
|
|
|
+| POST | `/api/schools` | 创建;`{ name, province, city, lng, lat, enabled? }` |
|
|
|
|
|
+| POST | `/api/schools/:id` | 更新(改名、重新定位) |
|
|
|
|
|
+| POST | `/api/schools/:id/toggle` | 启用/停用 `{ enabled }` |
|
|
|
|
|
+| DELETE | `/api/schools/:id` | 删除 |
|
|
|
|
|
+
|
|
|
|
|
+校验:
|
|
|
|
|
+
|
|
|
|
|
+- `name`:去空白、1~60 字符、唯一
|
|
|
|
|
+- `lng` / `lat`:必填;中国范围(lng 73~136、lat 3~54)之外拒绝
|
|
|
|
|
+- `province` / `city`:可选,≤40 字符
|
|
|
|
|
+- 存储不可用:`GET` 返回 200 + `{ enabled:false, reason }`,写操作 503(与 ops 现有一致)
|
|
|
|
|
+
|
|
|
|
|
+## 7. 界面布局
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+┌──────────────────────────────────────────────────────────────┐
|
|
|
|
|
+│ 校区地图 + 新建学校 │
|
|
|
|
|
+│ 已启用 3 / 共 5 │
|
|
|
|
|
+├───────────────────────────────────┬──────────────────────────┤
|
|
|
|
|
+│ │ 🔍 搜索学校 │
|
|
|
|
|
+│ 中国地图(可缩放平移) │ ┌──────────────────────┐ │
|
|
|
|
|
+│ │ │● 中国计量大学 [开] │ │
|
|
|
|
|
+│ ● 中国计量大学 │ │ 浙江 · 杭州 │ │
|
|
|
|
|
+│ ● 浙江工商大学桐庐校区 │ ├──────────────────────┤ │
|
|
|
|
|
+│ ○ 浙江科技学院(未启用) │ │○ 浙江科技学院 [关] │ │
|
|
|
|
|
+│ │ │ 浙江 · 杭州 │ │
|
|
|
|
|
+│ [回到全国] │ └──────────────────────┘ │
|
|
|
|
|
+└───────────────────────────────────┴──────────────────────────┘
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+- 左侧地图占 2/3 以上,右侧列表 320px
|
|
|
|
|
+- 列表按省份分组,组头显示该省启用数(如「浙江省 2/3」)
|
|
|
|
|
+- 窄屏(<1100px)时列表折叠到地图下方
|
|
|
|
|
+
|
|
|
|
|
+## 8. 与现有功能的关系
|
|
|
|
|
+
|
|
|
|
|
+- **独立菜单**,不改「店铺运营」的现有行为
|
|
|
|
|
+- 数据上通过 `ops_shop.school` 名称关联,为下一步「点击地图上的学校 → 打开店铺运营并筛选该学校」留好接口(本期不做跳转)
|
|
|
|
|
+- 不参与采集、不参与钉钉推送、不影响任何现有接口
|
|
|
|
|
+
|
|
|
|
|
+## 9. 测试
|
|
|
|
|
+
|
|
|
|
|
+### 9.1 单元
|
|
|
|
|
+
|
|
|
|
|
+- 坐标范围校验(境内/境外)
|
|
|
|
|
+- 名称去重与去空白
|
|
|
|
|
+- 省份自动识别(点在浙江/北京/境外三种情况)
|
|
|
|
|
+- WGS84 → GCJ-02 转换(用 §4.3 三组已知值做断言,误差 < 1e-4)
|
|
|
|
|
+
|
|
|
|
|
+### 9.2 接口(临时 SQLite)
|
|
|
|
|
+
|
|
|
|
|
+- 创建 → 列表可见;重复名称 400
|
|
|
|
|
+- 切换启用状态;删除后列表消失
|
|
|
|
|
+- 坐标缺省 / 越界 → 400
|
|
|
|
|
+- 存储不可用 → GET 200 + `enabled:false`,POST 503
|
|
|
|
|
+
|
|
|
|
|
+### 9.3 冒烟
|
|
|
|
|
+
|
|
|
|
|
+`GET /api/schools` 200(只读,不新增写操作)
|
|
|
|
|
+
|
|
|
|
|
+### 9.4 手工验收
|
|
|
|
|
+
|
|
|
|
|
+新建「中国计量大学」→ 选浙江省 → 地图飞到浙江 → 点下沙位置 → 保存 → 点位点亮并显示名称 → 刷新页面仍在 → 停用变灰 → 重新启用再点亮 → 删除后消失。
|
|
|
|
|
+
|
|
|
|
|
+## 10. 待办清单(通过评审后拆实施计划)
|
|
|
|
|
+
|
|
|
|
|
+- [ ] `public/vendor/china-map.json`、`cn-cities.json`(已就绪,需登记到静态白名单)
|
|
|
|
|
+- [ ] `ops_school` 表与存储方法(SQLite + PG 两驱动)
|
|
|
|
|
+- [ ] `/api/schools` 五个端点 + 校验 + 存储降级
|
|
|
|
|
+- [ ] `public/schools.js`:地图渲染、点亮、定位模式、列表联动、创建/编辑弹窗
|
|
|
|
|
+- [ ] `index.html` 菜单与页面骨架、`app.js` 视图注册、`style.css`
|
|
|
|
|
+- [ ] 坐标工具:`wgs84ToGcj02()` + 省份识别(点在多边形内)
|
|
|
|
|
+- [ ] 测试脚本扩展(单元 + 接口)与 smoke 只读断言
|
|
|
|
|
+- [ ] README 增补「校区地图」章节与坐标系说明
|
|
|
|
|
+
|
|
|
|
|
+## 11. 开放问题(实施前需确认)
|
|
|
|
|
+
|
|
|
|
|
+1. 学校列表是否要显示「该学校在店铺运营里有几家店 / 最近有数据的日期」?(需要与 ops 数据联查,工作量 +0.5 天)
|
|
|
|
|
+2. 删除学校时,如果「店铺运营」里已有同名学校的数据,是否需要二次确认并提示影响?
|
|
|
|
|
+3. 是否需要「批量导入学校」(给一份 Excel/CSV,含名称+坐标)?本期默认不做。
|