# 关系数据库与通用数据平台技术方案 > 版本:V2(精简版) > > 适用范围:云游荔波及平台后续项目 > > 数据前提:进入平台的数据已经完成采集、清洗、去重、融合和人工确认。 ## 1. 核心结论 平台只保留两套业务数据存储: 1. **PostgreSQL:保存处理完成后的正式业务数据,是唯一权威数据源。** 2. **FalkorDB:保存正式业务数据生成的图谱节点和关系,主要用于关系查询和 ToB 可视化。** 不在新的关系数据平台中重复建设原始采集、候选审核、字段级溯源等复杂流程。未经处理的数据不能直接进入正式业务库,应在平台外完成处理后,再按标准模板导入。 ```mermaid flowchart LR A["平台外数据处理\n采集、清洗、去重、融合、审核"] --> B["标准数据文件或 API"] B --> C["格式与关联校验"] C -->|通过| D["PostgreSQL\n正式业务数据"] C -->|不通过| E["错误报告\n退回修正"] D --> F["图谱同步队列"] F --> G["FalkorDB\n图谱投影"] D --> H["通用数据后台\n地图、详情、导入导出"] G --> I["图谱浏览器\n关系查询、ToB 展示"] ``` ## 2. 数据职责 ### 2.1 PostgreSQL 保存 - 酒店、美食、景区、交通等正式实体。 - 酒店房型、设施、政策等正式明细。 - 美食团购、菜系、营业时间等正式明细。 - 公交线路、方向、站点顺序和线路坐标。 - 评论、评价标签和图片。 - 实体之间已经确认的正式关系。 - 高德、携程、大众点评等外部平台 ID 和 URL。 - 数据修改记录。 - 导入、导出任务结果。 - 图谱同步队列和同步状态。 ### 2.2 PostgreSQL 不保存 - 未清洗的网页或接口原始响应。 - 未确认的候选实体。 - 自动匹配过程中的每个中间结果。 - 重复数据的全部计算过程。 - 临时爬虫缓存。 - 无业务意义的实时文案,例如“仅剩 2 间”。 ### 2.3 FalkorDB 保存 - 酒店、美食、景区、交通、公交线路、公交站、片区和行政区节点。 - 实体与片区、片区与荔波县的空间关系。 - 景区与子景点关系。 - 公交线路与站点关系。 - 已确认的附近、包含、分类等业务关系。 - 图谱展示需要的名称、类型、坐标和少量摘要属性。 ### 2.4 FalkorDB 不保存 - 完整评论正文。 - 酒店全部房型和报价。 - 美食全部团购套餐。 - 大量设施、政策等详情字段。 - 导入文件和错误数据。 - 可从 PostgreSQL 重新生成的重复明细。 ## 3. 简化后的平台结构 ```mermaid flowchart TB subgraph PG["PostgreSQL 正式业务库"] P1["项目与权限"] P2["通用实体"] P3["酒店、美食、景区、交通明细"] P4["评论、图片、设施、套餐"] P5["正式关系"] P6["导入导出记录"] P7["图谱同步队列"] end subgraph API["FastAPI"] A1["通用数据表 API"] A2["批量导入导出"] A3["地图与详情 API"] A4["图谱同步服务"] end subgraph FK["FalkorDB"] F1["正式实体节点"] F2["正式业务关系"] F3["片区与行政区关系"] end subgraph UI["React 管理后台"] U1["通用数据管理"] U2["荔波地图与详情"] U3["图谱浏览器"] end PG <--> API API --> FK API <--> UI FK --> U3 ``` ## 4. PostgreSQL 表结构 整体分成四组,避免结构混乱: 1. 平台管理表。 2. 正式实体表。 3. 业务明细表。 4. 同步与任务表。 ### 4.1 平台管理表 | 表 | 作用 | | --- | --- | | `projects` | 项目工作区 | | `data_table_registry` | 注册通用后台可管理的业务表 | | `data_field_registry` | 定义业务表字段显示和校验规则 | | `data_relation_registry` | 定义表之间的关联 | | `data_change_logs` | 保存数据修改记录 | 通用后台只能访问 `data_table_registry` 中已注册的表,不能任意访问 PostgreSQL。 ### 4.2 正式实体主表 所有地点类业务统一进入 `poi_entities`: ```text poi_entities ├── entity_id UUID PK ├── tenant_id ├── project_id ├── entity_type ├── name ├── category_l1 ├── category_l2 ├── category_l3 ├── address ├── district ├── adcode ├── phone ├── longitude ├── latitude ├── geom ├── h3_r9 ├── h3_r10 ├── status ├── version ├── created_at ├── updated_at └── extra_data JSONB ``` `entity_type` 的当前标准值: ```text hotel restaurant scenic transport bus_stop ``` 主表只保存所有 POI 共有且经常查询的字段。项目特有但暂不稳定的少量字段可以放入 `extra_data`,不能把大段明细或一对多数据塞入其中。 ### 4.3 外部平台标识 `entity_external_links` 只保存已经确认与实体对应的平台标识,不保存原始响应: ```text entity_external_links ├── id ├── tenant_id ├── project_id ├── entity_id ├── platform ├── external_id ├── external_name ├── external_url └── updated_at ``` 示例: ```text 同一个酒店实体 ├── amap:高德 POI ID 和 URL └── ctrip:携程酒店 ID、名称和 URL 同一个美食实体 ├── amap:高德 POI ID 和 URL └── dianping:大众点评商户 ID、名称和 URL ``` ### 4.4 公共明细表 | 表 | 内容 | | --- | --- | | `entity_images` | 实体和房型、套餐图片 | | `entity_relations` | 已确认的正式实体关系 | | `reviews` | 酒店、美食等真实评论 | | `review_tags` | 评论摘要标签 | `entity_relations` 保存正式关系: ```text relation_id tenant_id project_id source_entity_id relation_type target_entity_id properties JSONB status updated_at ``` ### 4.5 酒店表 ```text hotel_profiles hotel_room_types hotel_facilities hotel_policies ``` #### `hotel_profiles` 一间酒店一条记录,保存: - 携程酒店名。 - 开业时间。 - 客房数量。 - 酒店钻级。 - 携程评分。 - 用户点评数量。 - 参考起价。 - 酒店简介。 #### `hotel_room_types` 一个房型一条记录: - 房型名称。 - 图片 URL。 - 床型。 - 面积。 - 楼层。 - 窗户。 - 可住人数。 - 早餐。 - 取消政策。 - 支付方式。 - 参考价格。 不保存“仅剩几间”等采集时刻的临时库存文案。 #### `hotel_facilities` 一个设施一条记录: - 设施分类。 - 设施名称。 - 是否免费。 - 收费说明。 - 来源 URL。 ### 4.6 美食表 ```text restaurant_profiles restaurant_deals restaurant_business_hours ``` #### `restaurant_profiles` - 大众点评店名。 - 点评分类。 - 榜单排名。 - 营业状态。 - 点评评分。 - 用户点评数。 - 人均消费。 #### `restaurant_deals` 一个套餐一条记录: - 套餐名称。 - 图片 URL。 - 当前价格。 - 原价。 - 折扣。 - 使用规则。 - 有效期。 ### 4.7 景区表 ```text scenic_profiles scenic_children ``` #### `scenic_profiles` - 景区类型。 - 景区等级。 - 是否国家级。 - 游客价值分类。 - 开放时间。 - 门票说明。 - 官方简介。 #### `scenic_children` 保存景区内部子景点: - 所属景区。 - 子景点名称。 - 子景点类型。 - 坐标。 - 游览说明。 ### 4.8 交通表 ```text transport_profiles bus_routes bus_route_directions bus_route_stops route_geometries ``` 线路站序必须使用独立表,不把站点数组拼接到一个文本字段。 ### 4.9 空间表 保留并复用: ```text kg_geo_cells kg_route_metrics ``` 实体坐标在 `poi_entities.geom` 中使用 PostGIS 保存。H3 片区 ID 同时保存在实体主表中。 ## 5. 通用数据后台 通用后台管理的是“平台注册的正式数据表”,不是数据库管理器。 ### 5.1 表注册 `data_table_registry`: ```text table_code project_id display_name physical_table primary_key title_field parent_table_code allow_create allow_update allow_delete allow_import allow_export graph_enabled display_order ``` ### 5.2 字段注册 `data_field_registry`: ```text table_code field_code display_name data_type required searchable sortable editable visible_in_list visible_in_detail importable exportable dictionary_values JSONB validation_rule JSONB display_order ``` 后台根据注册信息自动生成: - 数据列表。 - 查询条件。 - 新增表单。 - 编辑表单。 - 详情页面。 - CSV/XLSX 模板。 - 导入校验。 - 导出字段。 ### 5.3 第一阶段不做在线建物理表 为了保持结构清晰: - 项目管理员可以配置显示、字段顺序、校验和权限。 - 新增正式物理表或修改列类型仍然通过数据库迁移完成。 - 后续确实需要用户在线建表时,再作为独立能力设计。 这样既能复用一个通用后台,又不会让数据库结构失控。 ## 6. 增删改查 统一 API: ```text GET /v1/admin/data/{table_code} POST /v1/admin/data/{table_code} GET /v1/admin/data/{table_code}/{record_id} PATCH /v1/admin/data/{table_code}/{record_id} DELETE /v1/admin/data/{table_code}/{record_id} POST /v1/admin/data/{table_code}/{record_id}/restore ``` ### 6.1 查询 - 所有查询自动限制 `tenant_id` 和 `project_id`。 - 只允许查询已注册且标记为可搜索的字段。 - 使用参数化 SQL,不接受原始 SQL。 - 默认每页 50 条,单页上限 200 条。 - 支持筛选、搜索、排序和保存列配置。 ### 6.2 新增和修改 - 按字段注册规则校验。 - `entity_id` 由服务器生成。 - 使用 `version` 防止多人覆盖。 - 保存操作人、时间和修改前后内容。 - 同一事务写入图谱同步队列。 ### 6.3 删除 默认软删除: ```text status = deleted deleted_at deleted_by ``` 删除后图谱同步服务删除对应投影。管理员可以恢复。 ## 7. 批量导入 进入平台的是已经处理好的数据,因此导入流程只做结构校验和正式入库,不再做清洗、融合和候选审核。 ```mermaid flowchart LR A["上传处理好的 CSV/XLSX"] --> B["字段和关联校验"] B -->|失败| C["下载错误报告"] B -->|通过| D["预览新增和更新数量"] D --> E["确认导入"] E --> F["事务写入正式表"] F --> G["同步图谱"] ``` ### 7.1 导入只保留一个任务表 ```text data_import_jobs ├── job_id ├── tenant_id ├── project_id ├── table_code ├── file_name ├── file_hash ├── import_mode ├── total_rows ├── inserted_rows ├── updated_rows ├── failed_rows ├── error_file ├── status ├── created_by ├── created_at └── completed_at ``` 不把每一条原始行长期保存在 PostgreSQL。校验失败行写入临时错误文件,供用户下载修正。 ### 7.2 导入模式 | 模式 | 说明 | | --- | --- | | 仅新增 | 已存在的唯一键直接报错或跳过 | | 新增并更新 | 按主键或外部平台 ID 更新,推荐默认 | 第一阶段不提供“完整覆盖并删除缺失数据”,避免误删。 ### 7.3 校验内容 - 必填字段。 - 数据类型。 - 唯一 ID。 - 外键是否存在。 - 枚举值。 - 经纬度范围。 - 坐标是否位于项目允许区域。 - 公交站序是否连续。 - 酒店房型是否能找到酒店。 - 评论是否能找到对应实体。 ### 7.4 多表数据包 酒店、美食等一对多数据使用 ZIP 多表导入: ```text 酒店数据.zip ├── 酒店实体.csv ├── 酒店详情.csv ├── 房型.csv ├── 设施.csv ├── 政策.csv ├── 评论.csv └── 图片.csv ``` 系统先校验整个数据包的主外键,全部通过后再提交,避免只导入主表、明细缺失。 ## 8. 批量导出 导出全部来自 PostgreSQL 正式表。 支持: - 当前表。 - 当前筛选结果。 - 选中记录。 - 主表及全部关联明细。 - 整个项目数据包。 - 图谱节点和关系。 云游荔波项目导出结构: ```text 云游荔波正式数据_YYYYMMDD.zip ├── 数据字典.csv ├── 酒店 │ ├── 酒店实体.csv │ ├── 酒店详情.csv │ ├── 房型.csv │ ├── 设施.csv │ ├── 政策.csv │ └── 评论.csv ├── 美食 │ ├── 美食实体.csv │ ├── 美食详情.csv │ ├── 团购套餐.csv │ └── 评论.csv ├── 景区 │ ├── 景区实体.csv │ └── 子景点.csv ├── 交通 │ ├── 交通站点.csv │ ├── 公交线路.csv │ ├── 线路方向.csv │ └── 线路站序.csv └── 图谱 ├── 节点.csv └── 关系.csv ``` 导出任务只需要一个 `data_export_jobs` 表记录状态、筛选条件、记录数、文件路径和文件哈希。 ## 9. 图谱同步 为了避免 PostgreSQL 保存成功但 FalkorDB 写入失败,保留一个精简的同步队列表: ```text graph_sync_queue ├── event_id ├── tenant_id ├── project_id ├── table_code ├── record_id ├── operation ├── record_version ├── status ├── retry_count ├── error_message ├── created_at └── completed_at ``` 业务写入和队列记录在同一个 PostgreSQL 事务中完成。 同步流程: 1. PostgreSQL 新增、修改或删除正式数据。 2. 同事务写入 `graph_sync_queue`。 3. 后台 Worker 读取待处理记录。 4. 按 `entity_id + version` 幂等更新 FalkorDB。 5. 成功后标记完成。 6. 失败自动重试并在后台显示错误。 不允许从 FalkorDB 反向修改 PostgreSQL。 ## 10. 图谱投影 ### 10.1 节点 ```text Area GeoCell(界面显示“片区”) Hotel Restaurant ScenicArea Attraction TransportStop BusStop BusRoute Category ``` ### 10.2 关系 ```text POI -[IN_H3_R9]-> GeoCell GeoCell -[LOCATED_IN]-> Area POI -[LOCATED_IN]-> Area ScenicArea -[CONTAINS]-> Attraction BusRoute -[SERVES_STOP]-> BusStop BusStop -[NEXT_STOP]-> BusStop POI -[HAS_CATEGORY]-> Category POI -[NEARBY]-> POI ``` ### 10.3 图节点属性 只同步: - `entity_id` - 名称。 - 类型。 - 分类。 - 经纬度。 - 地址摘要。 - 状态。 - PostgreSQL 数据版本。 详情页面需要的完整房型、设施、评论和套餐继续从 PostgreSQL 查询。 ## 11. 页面数据来源 | 页面 | 数据来源 | | --- | --- | | 通用数据后台 | PostgreSQL | | 荔波地图 POI | PostgreSQL + PostGIS | | 酒店、美食详情侧栏 | PostgreSQL | | 房型、设施、套餐、评论 | PostgreSQL | | 公交路线地图 | PostgreSQL | | 图谱浏览器 | FalkorDB | | 图谱关系查询 | FalkorDB | | CSV/XLSX 导出 | PostgreSQL | 现有荔波地图和详情布局保持不变,只调整后端数据来源。 ## 12. 权限与项目隔离 所有正式业务表都必须包含: ```text tenant_id project_id ``` 后端从当前项目上下文自动获得这两个值,不接受普通用户任意修改。 角色建议: | 角色 | 权限 | | --- | --- | | 系统管理员 | 管理所有项目和注册表 | | 项目管理员 | 管理本项目数据、导入导出 | | 数据编辑 | 本项目增删改查 | | 只读用户 | 查询和受限导出 | ## 13. 数据修改记录 只保留一个清晰的修改日志表: ```text data_change_logs ├── change_id ├── tenant_id ├── project_id ├── table_code ├── record_id ├── operation ├── before_data JSONB ├── after_data JSONB ├── actor ├── reason └── created_at ``` 不再额外设计字段级来源选择、候选状态机和多套版本表。 ## 14. 索引 核心索引: ```text poi_entities (tenant_id, project_id, entity_type, status) poi_entities (tenant_id, project_id, updated_at DESC) poi_entities UNIQUE (tenant_id, project_id, entity_id) entity_external_links UNIQUE (tenant_id, project_id, platform, external_id) poi_entities USING GIST (geom) bus_route_stops UNIQUE (route_direction_id, stop_order) reviews (entity_id, created_at DESC) graph_sync_queue (status, created_at) ``` 地图接口必须按当前视窗查询,不允许一次返回全部 POI。 ## 15. 现有系统如何迁移 ### 第一步:备份 - 导出当前 PostgreSQL 快照。 - 导出当前 FalkorDB RDB。 - 统计酒店、美食、景区、交通、公交和片区数量。 ### 第二步:建立正式关系表 - 引入 Alembic。 - 补齐 PostGIS。 - 创建正式实体和业务明细表。 - 创建表注册、导入导出、修改日志和图谱队列表。 ### 第三步:只迁移最终数据 - 从当前 FalkorDB 和已经整理好的 CSV 导出最终结果。 - 不迁移临时爬虫数据和中间匹配过程。 - 按 `entity_id`、高德 POI ID 和已确认平台 ID 建立关联。 - 酒店房型、设施、评论和美食套餐拆成明细表。 ### 第四步:上线通用后台 - 注册酒店、美食、景区、交通等正式表。 - 自动生成列表、详情、编辑和导入导出页面。 - 现有业务页面暂时不变。 ### 第五步:切换详情和地图 - 荔波地图、统计、筛选和详情读取 PostgreSQL。 - 图谱浏览器继续读取 FalkorDB。 ### 第六步:启用图谱同步 - 开启 `graph_sync_queue` Worker。 - 对比新图谱和当前图谱。 - 确认一致后停止其他脚本直接修改 FalkorDB。 ### 第七步:旧流程只读保留 现有以下数据仍可保留用于历史查看,但不属于新正式业务数据链路: - `raw_records` - `candidate_entities` - `candidate_relations` - `review_actions` - `publish_jobs` 新数据不再经过这些表。 ## 16. 代码结构 后端: ```text app/data_platform/ ├── registry.py ├── record_service.py ├── query_service.py ├── import_service.py ├── export_service.py └── graph_sync_service.py app/api/data_platform.py app/workers/graph_sync_worker.py app/migrations/ ``` 前端: ```text admin-web/src/panels/data-platform/ ├── TableList.tsx ├── DataTable.tsx ├── RecordForm.tsx ├── RecordDetail.tsx ├── ImportDialog.tsx ├── ExportDialog.tsx └── SyncStatus.tsx ``` ## 17. 验收标准 ### 数据 - PostgreSQL 中的实体数量与最终确认数据一致。 - 每个实体只有一个稳定 `entity_id`。 - 酒店房型、设施、评论等明细均能正确关联主实体。 - 所有地图实体都有合法坐标或明确的无坐标状态。 - 不迁移临时库存和重复文本。 ### 后台 - 注册一张业务表后,可自动获得查询、新增、编辑、删除、导入和导出能力。 - 普通用户不能访问未注册表。 - 不同项目之间不能看到或修改对方数据。 - 删除记录可以恢复。 ### 图谱 - PostgreSQL 修改后自动更新 FalkorDB。 - 同步失败不影响 PostgreSQL 保存。 - 重复处理同一同步事件不会产生重复节点。 - FalkorDB 可以从 PostgreSQL 全量重建。 - 实体、片区和荔波县保持连通,不产生独立片区。 ### 性能 - 列表默认 50 条分页。 - 常规查询 P95 小于 500 ms。 - 地图只查询当前视窗。 - 10,000 行处理好数据能够完成校验和导入。 ## 18. 实施优先级 ### P0:正式数据表 - `poi_entities` - 外部平台链接。 - 酒店、美食、景区、交通明细。 - 评论、图片和正式关系。 ### P1:通用后台 - 表和字段注册。 - 通用增删改查。 - 软删除和修改日志。 ### P2:导入导出 - 标准模板。 - 单表和 ZIP 多表导入。 - 错误报告。 - 多表数据包导出。 ### P3:图谱同步 - `graph_sync_queue`。 - 图谱 Worker。 - 同步状态和失败重试。 ### P4:现有页面切换 - 荔波地图。 - 酒店、美食、景区、交通详情。 - 公交线路。 - 图谱浏览器保持不变。 ## 19. 最终原则 1. 只允许处理完成的数据进入正式业务库。 2. PostgreSQL 只保存最终业务数据和必要运行记录。 3. FalkorDB 只保存最终数据的图谱投影。 4. 通用后台只管理平台注册的正式表。 5. 一对多数据必须拆成关系表。 6. 导入只做校验和入库,不再做数据清洗与融合。 7. 导出以 PostgreSQL 为唯一来源。 8. 所有业务修改先写 PostgreSQL,再异步更新图谱。 9. 现有候选审核流程与新正式数据平台分离。 10. 先完成清晰、稳定的数据主链路,再增加高级能力。