Selaa lähdekoodia

docs: 全国校区地图设计(主动点选定位 + GCJ-02 + ECharts 点亮)+ 底图/城市坐标数据

liangzhao 2 viikkoa sitten
vanhempi
sitoutus
a61f681db9

+ 249 - 0
docs/superpowers/specs/2026-10-20-campus-map-design.md

@@ -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,含名称+坐标)?本期默认不做。

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
public/vendor/china-map.json


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
public/vendor/cn-cities.json


Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä