做这个目录骨架,其实是给“过去的自己”收个尾。

以前每开一个 FastAPI 项目,都要重新纠结一遍:GeoTIFF 和 GPKG 究竟丢哪儿、 SQLite 主库要不要跟代码放一起、前端工程要不要塞进 assets/里凑合用。

项目一多,坑也踩得够多,换服务器不好迁、挂卷不好配、调试和生产混在一起更是灾难。 所以干脆把这些年折腾下来的经验,整理成一套可以反复复用的目录约定: 哪些是只读数据、哪些是运行期写入、哪些是第三方依赖,前后端怎样互不打架。

希望这篇“小小的标准化骨架”,能帮你少掉几次重构,哪怕只是打开一个老项目时,不再一脸问号地对着一堆散乱的文件夹。

目录分层

角色分三类:只读数据运行期读写第三方依赖。再加一个前端工程的独立空间。

.
├─ api/                      # FastAPI 后端源码
├─ web/                      # Angular 工作区(CLI 或 Nx)
├─ data/                     # ✅ 只读、可版本化的数据(GeoTIFF/GPKG/MBTiles)
│  ├─ raw/                   # 原始不可改
│  ├─ reference/             # 基准:行政区划、底图
│  ├─ processed/             # 预处理后(重投影/重采样/COG)
│  └─ samples/               # 轻量样例/单测数据
├─ var/                      # ✅ 运行期可写(挂卷)
│  ├─ db/                    # SQLite 库与 -wal/-shm
│  ├─ cache/                 # GDAL/应用缓存
│  ├─ tmp/                   # 任务中间产物
│  └─ exports/               # 导出成果
├─ vendor/                   # ✅ 第三方二进制/扩展(只读)
│  └─ sqlite-extensions/
│     ├─ linux-x86_64/…
│     ├─ linux-aarch64/…
│     └─ darwin-arm64/…
├─ public/                   # 小体积静态文件(如前端构建产物的托管目录,选用)
└─ infra/                    # 部署脚本(docker/k8s/terraform)

为什么这样放?

  • data/:版本可控、便于校验;容器里挂只读卷,避免误写污染底库。
  • var/:运行期所有写入统一收口,方便迁移、备份、清理;容器里挂可写卷
  • vendor/:第三方二进制跟业务代码分开,利于审计与升级回滚。
  • web/:前端是完整工程,不和 assets//public/ 混用。

不要把大体积 GeoTIFF/GPKG 放 assets/public/;那两处用于小静态资源或前端编译产物托管。

全国级 GPKG 命名规范

文件名模式:

{region}_{theme}_{geom}_{lod}_{crs}_{source}_{date}_v{ver}.gpkg
  • regionchn(全国)或带 adcode:chn_44chn_4403
  • themeroads|railways|transmission|hydro|heritage …(小写蛇形,可细分)
  • geompt|ln|pg
  • lod1k|10k|50k|100k|250k|1mz8|z10|z12|full(二选一体系,别混)
  • crs:如 epsg4490(CGCS2000)或 epsg4326
  • sourceosm|mwr|miit|sgcc|ncha|org
  • dateYYYYMMDDYYYYQn
  • v{ver}v1 / v1.1

示例:

chn_roads_ln_100k_epsg4490_osm_20251001_v1.gpkg
chn_transmission_ln_full_epsg4490_sgcc_20250115_v1.gpkg
chn_hydro_pg_250k_epsg4490_mwr_202410_v1.gpkg

图层命名(GPKG 内部):

roads_motorway_z10, roads_trunk_z12
railways_mainline, railways_hsr
transmission_220kv, transmission_hvdc
hydro_river_250k, hydro_lake
heritage_site_full, heritage_buffer_1km

可选:侧车 JSON(同名 .json)记录来源、处理流程、字段字典、SHA256 与更新日志,便于自动校验与同步。

SQLite:扩展放哪儿、怎么加载、有哪些坑

放置规则 第三方扩展属于“可执行依赖”,放 vendor/sqlite-extensions/{os-arch}/,只读管理。示例:

vendor/sqlite-extensions/
  linux-aarch64/libsimple.so
  linux-x86_64/libsimple.so
  darwin-arm64/libsimple.dylib

加载示例(按平台自动挑库):

import os, sys, platform, sqlite3
from pathlib import Path

BASE = Path(__file__).resolve().parents[1]
EXT_DIR = Path(os.getenv("SQLITE_EXT_DIR", BASE / "vendor" / "sqlite-extensions"))

def platform_key():
    sysname = sys.platform
    arch = platform.machine().lower()
    if sysname.startswith("linux"):
        return f"linux-{'aarch64' if 'aarch64' in arch or 'arm64' in arch else 'x86_64'}", ".so"
    if sysname == "darwin":
        return f"darwin-{'arm64' if 'arm64' in arch else 'x86_64'}", ".dylib"
    if sysname in ("win32", "cygwin"):
        return "win-amd64", ".dll"
    raise RuntimeError(f"unsupported: {sysname}/{arch}")

def load_ext(conn: sqlite3.Connection, name: str):
    plat, suf = platform_key()
    path = EXT_DIR / plat / f"{name}{suf}"
    conn.enable_load_extension(True)
    conn.load_extension(str(path))

避坑指南

  • Alpine(musl)与 Debian(glibc)二进制不通用;镜像基座要对应。
  • 尽量用系统包管理器的(如 libsqlite3-mod-spatialite),生产上更省心。

SQLite 主库放哪儿:var/db/

位置var/db/app_prod.sqlite(以及同目录的 -wal/-shm

原因:运行期写入集中在 var/ 才好挂卷、好备份、好清理;镜像保持无状态、可复用。

最小可用配置:

# app/core/settings.py
from pathlib import Path
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    VAR_ROOT: Path = Path("./var").resolve()
    DB_PATH: Path | None = None  # 支持环境变量覆盖
    def db_file(self) -> Path:
        return self.DB_PATH or (self.VAR_ROOT / "db" / "app_prod.sqlite")

settings = Settings()
settings.db_file().parent.mkdir(parents=True, exist_ok=True)

DATABASE_URL = f"sqlite+aiosqlite:///{settings.db_file()}"

运行期参数(建议):

PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;      -- 更稳:FULL
PRAGMA foreign_keys=ON;
PRAGMA busy_timeout=5000;

写多读多场景,考虑 PostgreSQL/PostGIS,SQLite 适合单机或写入压力不高的服务。

Angular 放哪儿、怎么对接

位置:仓库根的 web/(或 frontend/),独立工作区。

开发态ng servehttp://localhost:4200;后端 http://localhost:8000

CORS(开发态)

  from fastapi.middleware.cors import CORSMiddleware
  app.add_middleware(
      CORSMiddleware,
      allow_origins=["http://localhost:4200"],
      allow_methods=["*"],
      allow_headers=["*"],
      allow_credentials=True,
  )

生产部署

  • 推荐:前端构建产物上 CDN/Nginx;Nginx 反代后端 /api
  • 小项目可由 FastAPI 挂静态目录(把 web/dist/<app>/browser 同步到 public/,配一个 SPA fallback)。

.gitignore 与备份策略

/web/node_modules/
/web/dist/
/var/**                 # 运行期写入一律不入库
/data/**                # 大数据不入库
!/data/samples/**       # 仅小样例允许
/vendor/sqlite-extensions/**   # 二进制通常不入库
!/vendor/sqlite-extensions/README.md
!/vendor/sqlite-extensions/checksums/INDEX.json
  • var/db/ 纳入备份计划(整库快照或定期 .dump)。
  • data/ 走对象存储/独立磁盘 + 校验和;仓库仅保存侧车元数据(来源、SHA、处理流程)。