TRIP 项目核心原理与代码阅读指南
项目概述
TRIP (Tourism and Recreational Interest Points) 是一个自托管的极简地图追踪器与行程规划工具。
- GitHub: https://github.com/itskovacs/trip
- 前端: Angular (位于
src/目录) - 后端: Python/FastAPI (位于
backend/trip/目录)
目录结构
external/trip/
├── src/ # Angular 前端
│ ├── src/app/ # Angular 组件、服务
│ └── ...
├── backend/trip/ # Python 后端 (FastAPI)
│ ├── main.py # FastAPI 应用入口
│ ├── config.py # 配置管理 (Pydantic Settings)
│ ├── security.py # 认证: JWT/TOTP/OIDC
│ ├── deps.py # 依赖注入 (SessionDep)
│ ├── models/
│ │ └── models.py # SQLModel ORM 模型定义
│ ├── db/
│ │ └── core.py # 数据库连接与迁移
│ └── routers/ # API 路由
│ ├── auth.py # 认证相关
│ ├── trips.py # 行程 CRUD
│ ├── places.py # 地点 CRUD
│ ├── categories.py # 分类
│ ├── token.py # Token 管理
│ ├── providers.py # 地图 provider
│ ├── settings.py # 设置
│ └── admin.py # 管理接口
└── docs/ # Docusaurus 文档
核心原理
1. 认证与授权
技术栈: JWT + Argon2 + TOTP (可选) + OIDC (可选)
| 文件 | 职责 |
|---|---|
security.py | hash_password(), verify_password(), create_access_token(), verify_totp_code(), OIDC 客户端 |
deps.py | SessionDep (数据库会话 DI), get_current_username() (从 JWT 提取当前用户) |
routers/auth.py | 登录/注册/Token 刷新 |
Token 流程:
- 用户登录 →
verify_password()验证密码 →create_tokens()生成 access + refresh token - 后续请求携带
Authorization: Bearer <token> oauth_password_scheme+get_current_username()从 JWT 解码出sub(username)
关键代码路径: deps.py:get_current_username() → security.py:verify_password() → config.py:SECRET_KEY
2. 数据模型
核心模型 (models/models.py):
| 模型 | 关系 |
|---|---|
User | 认证主体,可选 TOTP secret |
Place | POI (兴趣点),含坐标、分类、图片 |
Trip | 行程,含多天 TripDay |
TripDay | 行程中的一天,含多个 TripItem |
TripItem | 行程项 (可标记状态: pending/booked/constraint/optional) |
TripShare | 分享链接 (token 机制) |
TripMember | 行程成员 |
TripInvitation | 邀请 |
Image | 图片 |
Category | 地点分类 (默认 8 类) |
TripPackingListItem / TripChecklistItem | 行李/清单 |
关键设计:
- 使用
sqlmodel(SQLAlchemy + Pydantic 混合) - 软删除通过
deleted_attimestamp 实现 after_commithook 清理待删除文件
3. 数据库与迁移
技术栈: SQLite + Alembic
| 文件 | 职责 |
|---|---|
db/core.py | get_engine() (单例), init_and_migrate_db() (启动时迁移) |
alembic.ini | Alembic 配置 |
db/migrations.py | 数据迁移 (填充默认值等) |
迁移流程 (main.py:lifespan):
- 启动时调用
init_and_migrate_db() - 检查 SQLite 文件是否存在
- 不存在 →
command.upgrade("head") - 存在但无 Alembic 版本表 →
command.stamp("b2ed4bf9c1b2")(标记初始版本) - 已有版本 →
command.upgrade("head")
4. API 路由设计
RESTful 风格,前缀 /api/
| Router | 路由 | 说明 |
|---|---|---|
auth.py | /auth/login, /auth/register, /auth/refresh | 认证 |
trips.py | /api/trips | 行程 CRUD、分享、成员管理 |
places.py | /api/places | 地点 CRUD |
categories.py | /api/categories | 分类管理 |
token.py | /token | Token 操作 |
providers.py | /api/providers | 地图 provider |
settings.py | /api/settings | 用户设置 |
admin.py | /api/admin | 管理功能 |
权限检查模式 (deps.py + security.py:verify_exists_and_owns):
def verify_exists_and_owns(username: str, obj) -> None:
if not obj:
raise HTTPException(status_code=404)
if obj.user != username:
raise HTTPException(status_code=403)
5. 前端架构 (Angular)
主要目录 (src/src/app/):
app.component.ts- 根组件components/- 共享组件pages/- 页面 (map, trips, places, settings 等)services/- API 服务 (trip.service.ts, place.service.ts 等)models/- TypeScript 接口定义shared/map.ts- 地图相关工具函数
关键特性:
- Angular Service 进行 HTTP 调用后端 API
- 地图使用 Leaflet + leaflet.markercluster (聚合) + leaflet-contextmenu
- PWA 支持 (
ngsw-config.json)
6. 地图处理
TRIP 的地图处理分两部分:前端渲染 + 后端搜索/路由。
前端 (Leaflet)
核心文件: src/src/app/shared/map.ts
| 函数 | 用途 |
|---|---|
createMap() | 初始化 Leaflet 地图,默认底图 CARTO Voyager |
placeToMarker() | 完整圆形标记 (含分类图标) |
placeToDotMarker() | 小圆点 (列表视图用) |
tripDayMarker() | 行程中的日程点 |
toDotMarker() | 任意坐标点 |
gpxToPolyline() | 解析 GPX XML 绘制轨迹线 |
openNavigation() | 调用 Google Maps 网页版导航 |
createClusterGroup() | 聚合标记组 (zoom < 11 时聚合成簇) |
底图:
- 默认: CARTO Voyager (
https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png) - 用户可配置:
User.custom_tile_layer(自定义瓦片 URL)
数据模型 (models/models.py):
User.map_provider— "osm" 或 "google"User.custom_tile_layer— 自定义瓦片 URLUser.google_apikey— Google API KeyUser.map_lat/lng/zoom— 地图初始位置
后端 Provider 抽象
目录: utils/providers/
| 文件 | 职责 |
|---|---|
base.py | 抽象基类 BaseMapProvider,定义接口 + encoded polyline 解码 |
osm.py | OpenStreetMap (Nominatim 搜索 + OSRM 路由) |
google.py | Google Maps (Places API, Directions API) |
OpenStreetMapProvider (osm.py):
- 搜索:
https://nominatim.openstreetmap.org/search - 路由:
https://routing.openstreetmap.de/routed-{profile}/route/v1/driving - 支持 profile:
car,foot,bike - 地点分类映射:
TYPES_MAPPER将 OSM amenity/shop/tourism 标签映射到 TRIP 的 8 分类
API 端点 (routers/providers.py):
| 端点 | 功能 |
|---|---|
POST /api/completions/search | 文本搜索地点 |
POST /api/completions/nearby | 附近搜索 (仅 Google) |
GET /api/completions/geocode | 地址 → 边界框 |
POST /api/completions/route | 路线规划 |
POST /api/completions/bulk | 批量导入 Google Maps 链接 |
POST /api/completions/mymaps-import | Google My Maps KMZ 导入 |
POST /api/completions/takeout-import | Google Takeout CSV 导入 |
GET /api/completions/google/resolve-shortlink/{link_id} | 解析 Google 短链接 |
批量处理: _process_batch() 使用 asyncio.Semaphore(4) 限流,并发处理最多 4 个请求。
代码阅读顺序
推荐按以下顺序阅读核心代码:
阶段 1: 理解应用骨架
main.py- FastAPI 应用初始化、路由注册、中间件config.py- 配置管理、环境变量
阶段 2: 理解认证流程
deps.py- 依赖注入模式security.py- JWT、密码哈希、TOTP、OIDC
阶段 3: 理解数据层
models/models.py- ORM 模型定义 (Trip, Place, User 关系)db/core.py- 数据库初始化与迁移
阶段 4: 理解业务逻辑
routers/trips.py- 行程核心逻辑 (最复杂)routers/places.py- 地点 CRUDrouters/auth.py- 认证流程
阶段 5: 理解前端
- Angular services (
src/src/app/services/) - API 调用 - Angular pages (
src/src/app/pages/) - 页面组件
阶段 6: 理解地图
shared/map.ts- Leaflet 地图初始化、标记、GPX 绘制utils/providers/base.py- Provider 抽象接口utils/providers/osm.py- OSM 搜索与路由routers/providers.py- 地图 API 端点
关键文件速查
| 目标 | 文件 | 关键函数/类 |
|---|---|---|
| JWT Token 验证 | deps.py | get_current_username() |
| 密码哈希 | security.py | hash_password(), verify_password() |
| 创建 Token | security.py | create_access_token(), create_refresh_token() |
| 数据库连接 | db/core.py | get_engine() |
| 启动迁移 | db/core.py | init_and_migrate_db() |
| Trip 创建 | routers/trips.py | create_trip() |
| Place 创建 | routers/places.py | create_place() |
| 权限检查 | security.py | verify_exists_and_owns() |
| 图片处理 | utils/utils.py | save_image_to_file(), patch_image() |
| 地图初始化 | shared/map.ts | createMap() |
| 地点搜索 | routers/providers.py | text_search() |
| 路线规划 | utils/providers/osm.py | get_route() |
| Provider 基类 | utils/providers/base.py | BaseMapProvider |
依赖关系图
main.py (应用入口)
├── config.py (get_settings)
├── db/core.py (get_engine, init_and_migrate_db)
│ └── models/models.py (SQLModel models)
├── routers/* (API endpoints)
│ ├── deps.py (SessionDep, get_current_username)
│ ├── security.py (JWT, password, verify_exists_and_owns)
│ └── models/models.py
└── security.py (token creation, password hashing)
调试技巧
- API 测试: 启动后端后直接访问
/api/info获取版本 - 数据库:
storage/trip.sqlite可用sqlite3直接查看 - 日志:
utils/utils.py:silence_http_logging()默认抑制 FastAPI HTTP 日志 - 配置:
storage/config.env运行时配置
研究问题清单
- 行程分享的 token 机制是如何工作的?
- TOTP 2FA 的完整验证流程?
- OIDC 认证的完整流程?
- 图片上传和处理的完整流程?
- TripDay 和 TripItem 的嵌套关系如何持久化?
- 前端如何与后端 API 交互?
- OSM Provider 如何将 Nominatim 结果映射到 TRIP 分类?
- Google My Maps KMZ 导入的完整解析流程?
- 批量导入的并发限流是如何实现的?
Backlinks (1)
flowchart LR
n0["AI"]
n1["Database"]
n2["Dev Tools"]
n3["Emoji"]
n4["Frontend"]
n5["Game Dev"]
n6["Collection"]
n7["Languages"]
n8["Maps"]
n9["Media"]
n10["Monitor"]
n11["Plans"]
n12["对象存储基本用法(Bucket / Object / 常用操作)"]
n13["对象存储数据迁移(R2 → MinIO / 跨厂商搬迁)"]
n14["Object Storage"]
n15["挂载 Bucket 为本地文件系统(FUSE Mount)"]
n16["对象存储签名 URL(Signed URL)原理与实战"]
n17["对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"]
n18["Prototypes"]
n19["Research"]
n20["Better Auth 源码阅读指南"]
n21["DuckDB 环境与基本使用"]
n22["DuckDB 实战研究"]
n23["DuckDB 模拟数据"]
n24["PostgreSQL 数据用 DuckDB 加速查询"]
n25["HK 角色阅读"]
n26["HK 台词阅读"]
n27["Hollow Knight 英语主题"]
n28["HK 物品阅读"]
n29["HK 地点阅读"]
n30["HK 世界观 / Lore 阅读"]
n31["HK 英语学习素材清单"]
n32["英语学习 Dashboard"]
n33["English Scraps Archive"]
n34["English Scraps 使用指南"]
n35["Jellyfin 源码阅读指南"]
n36["Lux 资料整理"]
n37["Nest Commander 学习资料"]
n38["NestJS 源码阅读指南"]
n39["Protomaps 自建底图研究"]
n40["自制 PMTiles 地图(最简单例子)"]
n41["MapLibre 集成 Protomaps"]
n42["PMTiles 格式与工具链"]
n43["上海地区底图项目"]
n44["Redash 源码阅读指南"]
n45["Rust 学习计划"]
n46["shadcn/ui 源码阅读指南"]
n47["TRIP 项目核心原理与代码阅读指南"]
n1 --> n22
n6 --> n0
n6 --> n1
n6 --> n2
n6 --> n3
n6 --> n4
n6 --> n5
n6 --> n7
n6 --> n8
n6 --> n9
n6 --> n10
n6 --> n11
n6 --> n19
n12 --> n13
n12 --> n16
n12 --> n17
n12 --> n18
n13 --> n12
n13 --> n15
n13 --> n16
n13 --> n17
n14 --> n12
n14 --> n13
n14 --> n15
n14 --> n16
n14 --> n17
n15 --> n12
n15 --> n16
n15 --> n17
n16 --> n18
n17 --> n12
n17 --> n13
n17 --> n16
n17 --> n18
n18 --> n12
n18 --> n15
n18 --> n16
n18 --> n17
n18 --> n39
n19 --> n20
n19 --> n22
n19 --> n32
n19 --> n35
n19 --> n36
n19 --> n37
n19 --> n38
n19 --> n39
n19 --> n44
n19 --> n45
n19 --> n46
n19 --> n47
n21 --> n23
n22 --> n1
n22 --> n19
n22 --> n21
n22 --> n23
n22 --> n24
n23 --> n21
n23 --> n24
n24 --> n22
n24 --> n23
n27 --> n25
n27 --> n26
n27 --> n28
n27 --> n29
n27 --> n30
n27 --> n31
n27 --> n34
n31 --> n26
n32 --> n27
n32 --> n33
n32 --> n34
n34 --> n32
n34 --> n33
n39 --> n8
n39 --> n40
n39 --> n41
n39 --> n42
n39 --> n43
n40 --> n42
n41 --> n18
n41 --> n43
n42 --> n40
n42 --> n43
n43 --> n41
n43 --> n42
click n0 "../../../collection/ai/" "AI"
click n1 "../../../collection/database/" "Database"
click n2 "../../../collection/dev-tools/" "Dev Tools"
click n3 "../../../collection/emoji/" "Emoji"
click n4 "../../../collection/frontend/" "Frontend"
click n5 "../../../collection/game-dev/" "Game Dev"
click n6 "../../../collection/" "Collection"
click n7 "../../../collection/languages/" "Languages"
click n8 "../../../collection/maps/" "Maps"
click n9 "../../../collection/media/" "Media"
click n10 "../../../collection/monitor/" "Monitor"
click n11 "../../../collection/scraps/plans/" "Plans"
click n12 "../../../knowledge/infrastructure/cloud/object-storage/basic-usage/" "对象存储基本用法(Bucket / Object / 常用操作)"
click n13 "../../../knowledge/infrastructure/cloud/object-storage/data-migration/" "对象存储数据迁移(R2 → MinIO / 跨厂商搬迁)"
click n14 "../../../knowledge/infrastructure/cloud/object-storage/" "Object Storage"
click n15 "../../../knowledge/infrastructure/cloud/object-storage/mount-bucket/" "挂载 Bucket 为本地文件系统(FUSE Mount)"
click n16 "../../../knowledge/infrastructure/cloud/object-storage/signed-url/" "对象存储签名 URL(Signed URL)原理与实战"
click n17 "../../../knowledge/infrastructure/cloud/object-storage/vendors-comparison/" "对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"
click n18 "../../../prototypes/" "Prototypes"
click n19 "../../" "Research"
click n20 "../better-auth/" "Better Auth 源码阅读指南"
click n21 "../duckdb/basic-usage/" "DuckDB 环境与基本使用"
click n22 "../duckdb/" "DuckDB 实战研究"
click n23 "../duckdb/mock-data/" "DuckDB 模拟数据"
click n24 "../duckdb/postgresql-acceleration/" "PostgreSQL 数据用 DuckDB 加速查询"
click n25 "../english/hollow-knight/characters/" "HK 角色阅读"
click n26 "../english/hollow-knight/dialogues/" "HK 台词阅读"
click n27 "../english/hollow-knight/" "Hollow Knight 英语主题"
click n28 "../english/hollow-knight/items/" "HK 物品阅读"
click n29 "../english/hollow-knight/locations/" "HK 地点阅读"
click n30 "../english/hollow-knight/lore/" "HK 世界观 / Lore 阅读"
click n31 "../english/hollow-knight/resources/" "HK 英语学习素材清单"
click n32 "../english/" "英语学习 Dashboard"
click n33 "../english/scraps/archive/" "English Scraps Archive"
click n34 "../english/scraps/" "English Scraps 使用指南"
click n35 "../jellyfin/" "Jellyfin 源码阅读指南"
click n36 "../lux/" "Lux 资料整理"
click n37 "../nest-commander/" "Nest Commander 学习资料"
click n38 "../nestjs/" "NestJS 源码阅读指南"
click n39 "../protomaps/" "Protomaps 自建底图研究"
click n40 "../protomaps/make-own-map/" "自制 PMTiles 地图(最简单例子)"
click n41 "../protomaps/maplibre/" "MapLibre 集成 Protomaps"
click n42 "../protomaps/pmtiles/" "PMTiles 格式与工具链"
click n43 "../protomaps/shanghai-map/" "上海地区底图项目"
click n44 "../redash/" "Redash 源码阅读指南"
click n45 "../rust/" "Rust 学习计划"
click n46 "../shadcn-ui/" "shadcn/ui 源码阅读指南"
click n47 "./" "TRIP 项目核心原理与代码阅读指南"