|
@@ -4,6 +4,7 @@
|
|
|
- 分支:`dev`
|
|
- 分支:`dev`
|
|
|
- 状态:待评审(本阶段只出设计,不写实现)
|
|
- 状态:待评审(本阶段只出设计,不写实现)
|
|
|
- 变更:**取消**「选省/市 + 地图点选」流程,改为**输入学校名自动定位**
|
|
- 变更:**取消**「选省/市 + 地图点选」流程,改为**输入学校名自动定位**
|
|
|
|
|
+- v3 变更:学校数据改为 **JSON 静态存储**(种子文件 + 运行时文件),不再用数据库;内置「浙江工商桐庐校区」
|
|
|
|
|
|
|
|
## 1. 背景与目标
|
|
## 1. 背景与目标
|
|
|
|
|
|
|
@@ -24,6 +25,8 @@
|
|
|
| 未收录的学校 | ① 在线地理编码兜底(Photon)→ ② 仍失败则创建为「未定位」,不点亮,列表可点「重新定位」重试 |
|
|
| 未收录的学校 | ① 在线地理编码兜底(Photon)→ ② 仍失败则创建为「未定位」,不点亮,列表可点「重新定位」重试 |
|
|
|
| 坐标系 | 统一 **GCJ-02**(底图坐标系);数据源是 BD-09 需转换,在线兜底是 WGS84 需转换 |
|
|
| 坐标系 | 统一 **GCJ-02**(底图坐标系);数据源是 BD-09 需转换,在线兜底是 WGS84 需转换 |
|
|
|
| 省份 | 由坐标预先算好(点落省份多边形),不需要用户选 |
|
|
| 省份 | 由坐标预先算好(点落省份多边形),不需要用户选 |
|
|
|
|
|
+| 存储 | **JSON 文件**(种子 + 运行时),不依赖数据库;Node 18 也可用 |
|
|
|
|
|
+| 内置学校 | 预置「浙江工商大学桐庐校区」(别名:浙江工商桐庐校区等) |
|
|
|
| 批量导入 | 不做 |
|
|
| 批量导入 | 不做 |
|
|
|
| 删除保护 | 有同名运营数据时二次确认 |
|
|
| 删除保护 | 有同名运营数据时二次确认 |
|
|
|
|
|
|
|
@@ -166,28 +169,74 @@ geo(省级底图,roam)
|
|
|
|
|
|
|
|
点击、悬停、选中联动;同屏像素距离 <14px 的两点做 ≤8px 确定性偏移。
|
|
点击、悬停、选中联动;同屏像素距离 <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);
|
|
|
|
|
|
|
+## 5. 数据模型(JSON 静态存储)
|
|
|
|
|
+
|
|
|
|
|
+### 5.1 两类文件
|
|
|
|
|
+
|
|
|
|
|
+| 文件 | 是否提交到仓库 | 作用 |
|
|
|
|
|
+|---|---|---|
|
|
|
|
|
+| `server/schools.seed.json` | **提交** | 内置学校(随版本发布);初始只有浙江工商桐庐校区 |
|
|
|
|
|
+| `data/schools.json` | 不提交(`data/` 已 gitignore) | 运行时新增/修改的学校,以及对内置项的覆盖 |
|
|
|
|
|
+
|
|
|
|
|
+读取时**合并**两者,对外的列表就是合并结果;写入只动 `data/schools.json`。
|
|
|
|
|
+
|
|
|
|
|
+### 5.2 条目结构
|
|
|
|
|
+
|
|
|
|
|
+```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"
|
|
|
|
|
+}
|
|
|
```
|
|
```
|
|
|
|
|
|
|
|
-- `lng/lat` 允许 NULL = 未定位(不点亮)
|
|
|
|
|
-- `source` 记录定位来源,便于排查
|
|
|
|
|
-- 与运营数据的关联:按 `ops_shop.school = ops_school.name` 精确匹配,查询时聚合门店数与 `MAX(date)`
|
|
|
|
|
|
|
+字段说明:
|
|
|
|
|
+
|
|
|
|
|
+- `name` 为唯一键;`aliases` 只用于**搜索联想**,不参与唯一性
|
|
|
|
|
+- `lng/lat`(GCJ-02)为 `null` 表示「未定位」(灰点、不点亮)
|
|
|
|
|
+- `seed: true` 表示内置项
|
|
|
|
|
+- 内置项坐标:浙江工商大学杭州商学院桐庐校区(桐庐县),`[119.744166, 29.830417]`
|
|
|
|
|
+
|
|
|
|
|
+### 5.3 合并与写回规则
|
|
|
|
|
+
|
|
|
|
|
+| 操作 | 行为 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| 读取 | 种子 + 运行时;同名(`name`)时运行时字段覆盖种子 |
|
|
|
|
|
+| 新建 | 只写 `data/schools.json`;若与种子/运行时同名 → 400 |
|
|
|
|
|
+| 修改(改名/重新定位/启停) | 写 `data/schools.json` 的同名记录(对种子项即"覆盖层") |
|
|
|
|
|
+| 删除 | 运行时项:从文件移除;**种子项:拒绝删除**,提示「内置学校只能停用」 |
|
|
|
|
|
+| 文件缺失/损坏 | 用种子继续工作,日志 warning,不崩溃 |
|
|
|
|
|
+| 写入 | 临时文件 + rename 原子替换;单进程串行 |
|
|
|
|
|
+
|
|
|
|
|
+### 5.4 与运营数据的关联
|
|
|
|
|
+
|
|
|
|
|
+学校存 JSON,运营数据在 ops 库(SQLite/PG),关联在读取时做:
|
|
|
|
|
+
|
|
|
|
|
+- 按 `ops_school_name`(即学校名及别名)去 `ops_shop.school` 精确匹配
|
|
|
|
|
+- 聚合出「门店数」与「最近数据日期」
|
|
|
|
|
+- **ops 库不可用时**:地图与学校管理照常,关联字段显示「未关联运营数据」
|
|
|
|
|
+
|
|
|
|
|
+### 5.5 内置学校
|
|
|
|
|
+
|
|
|
|
|
+| 字段 | 值 |
|
|
|
|
|
+|---|---|
|
|
|
|
|
+| 名称 | 浙江工商大学桐庐校区 |
|
|
|
|
|
+| 别名 | 浙江工商桐庐校区 / 浙江工商大学杭州商学院 / 杭州商学院桐庐校区 |
|
|
|
|
|
+| 省/市/县 | 浙江省 / 杭州市 / 桐庐县 |
|
|
|
|
|
+| 坐标(GCJ-02) | 119.744166, 29.830417 |
|
|
|
|
|
+| 默认状态 | 启用(点亮) |
|
|
|
|
|
+
|
|
|
|
|
+> 名称采用「浙江工商大学桐庐校区」是为了和上传表里的 `学校名称` 对得上(人工统计表用的就是全称),别名保证用户输入简称也能搜到。
|
|
|
|
|
|
|
|
## 6. 接口
|
|
## 6. 接口
|
|
|
|
|
|
|
@@ -203,6 +252,8 @@ CREATE UNIQUE INDEX IF NOT EXISTS ops_school_name_idx ON ops_school (name);
|
|
|
|
|
|
|
|
校验:名称 1~60 且唯一;坐标若提供必须在 GCJ-02 中国范围(lng 73~136,lat 3~54)。
|
|
校验:名称 1~60 且唯一;坐标若提供必须在 GCJ-02 中国范围(lng 73~136,lat 3~54)。
|
|
|
|
|
|
|
|
|
|
+存储相关:写操作失败(磁盘只读等)返回 500 并保持文件原样;`GET /api/schools` 始终可用(JSON 不依赖数据库)。测试通过环境变量 `SCHOOLS_FILE` 指定临时文件,避免污染本机 `data/schools.json`。
|
|
|
|
|
+
|
|
|
## 7. 界面
|
|
## 7. 界面
|
|
|
|
|
|
|
|
```
|
|
```
|
|
@@ -249,7 +300,10 @@ CREATE UNIQUE INDEX IF NOT EXISTS ops_school_name_idx ON ops_school (name);
|
|
|
- 重名 400;停用/启用;删除
|
|
- 重名 400;停用/启用;删除
|
|
|
- 未定位学校(lng/lat 为空)不报错,`enabled=1` 也不点亮
|
|
- 未定位学校(lng/lat 为空)不报错,`enabled=1` 也不点亮
|
|
|
- 关联字段:造 2 条 `ops_shop` 同名数据 → 返回门店数 2 与最近日期
|
|
- 关联字段:造 2 条 `ops_shop` 同名数据 → 返回门店数 2 与最近日期
|
|
|
-- 存储不可用 → GET 200 + `enabled:false`,写操作 503
|
|
|
|
|
|
|
+- 内置项不可删除(400),可停用;运行时项可删除
|
|
|
|
|
+- 种子 + 运行时合并:同名时运行时覆盖
|
|
|
|
|
+- 文件损坏:仍能返回种子数据,不 500
|
|
|
|
|
+- ops 库不可用:`GET /api/schools` 仍 200,关联字段为空
|
|
|
|
|
|
|
|
### 9.3 冒烟
|
|
### 9.3 冒烟
|
|
|
|
|
|
|
@@ -263,8 +317,9 @@ CREATE UNIQUE INDEX IF NOT EXISTS ops_school_name_idx ON ops_school (name);
|
|
|
|
|
|
|
|
- [ ] `scripts/build-university-data.js`(数据生成脚本,含 BD-09 转换与省份归属;数据文件已就绪)
|
|
- [ ] `scripts/build-university-data.js`(数据生成脚本,含 BD-09 转换与省份归属;数据文件已就绪)
|
|
|
- [ ] `public/vendor/cn-universities.json`(已生成,待登记静态白名单)
|
|
- [ ] `public/vendor/cn-universities.json`(已生成,待登记静态白名单)
|
|
|
-- [ ] `ops_school` 表 + 存储方法(SQLite / PG)
|
|
|
|
|
-- [ ] `/api/schools` 端点(含 search / locate / 关联查询 / 删除保护所需计数)
|
|
|
|
|
|
|
+- [ ] `server/schools.seed.json`(内置浙江工商桐庐校区)
|
|
|
|
|
+- [ ] `schools-store.js`:种子 + 运行时文件合并、原子写、损坏兜底
|
|
|
|
|
+- [ ] `/api/schools` 端点(含 search / locate / 关联查询 / 删除保护)
|
|
|
- [ ] `public/schools.js`:地图、联想、创建、编辑、启停、删除确认、列表联动
|
|
- [ ] `public/schools.js`:地图、联想、创建、编辑、启停、删除确认、列表联动
|
|
|
- [ ] `index.html` 菜单与页面、`app.js` 注册、`style.css`
|
|
- [ ] `index.html` 菜单与页面、`app.js` 注册、`style.css`
|
|
|
- [ ] 测试脚本扩展 + smoke 只读断言
|
|
- [ ] 测试脚本扩展 + smoke 只读断言
|
|
@@ -275,3 +330,4 @@ CREATE UNIQUE INDEX IF NOT EXISTS ops_school_name_idx ON ops_school (name);
|
|
|
1. **未收录学校**:在线地理编码兜底 → 失败允许创建为「未定位」(灰点、不点亮),列表内可重试定位
|
|
1. **未收录学校**:在线地理编码兜底 → 失败允许创建为「未定位」(灰点、不点亮),列表内可重试定位
|
|
|
2. **关联跳转**:详情卡提供「查看运营数据」,跳转「店铺运营」并预选该学校
|
|
2. **关联跳转**:详情卡提供「查看运营数据」,跳转「店铺运营」并预选该学校
|
|
|
3. 不做批量导入;创建后默认启用
|
|
3. 不做批量导入;创建后默认启用
|
|
|
|
|
+4. 学校数据用 JSON(种子 + 运行时),不进数据库;内置项只能停用不能删除
|