Feature Specification: 自部署地图集成(台湾外卖/闪送)
Feature Branch: codex/self-hosted-map-integration(2026-10-23 用户要求仅后端新建分支;地图继续 master)
Created: 2026-09-24
Status: 实现中(2026-10-28:地图分支已合入 test-202609v2,继续补旧地址入口兼容与合并后回归;此前本地验证记录保留,真实发布验收见 tasks)。
Input: User description: "自部署地图集成:台湾外卖/闪送切换到自部署地图服务(Foodie_HCM_Map,PostGIS 搜索 + OSRM 算路),外卖/闪送运费按平台设置计价(pos_freight / flash_delivery_pricing),所有地图功能逐步切换,已有完整差距分析结论。"
后续事项记录(2026-10-10):用户要求先记录、后续再做。台湾 OSM 需求对照、双车型测试服务实测、地址精度/性能差距及待办见 osm-requirements-assessment.md。追加工作未开始,未改变上述既有实现进度;本规格的 car-only 和汽车时长折算假设需在恢复实施时重新校准。
机车模型评估(2026-10-12):用户确认的四项评估已验证,见 model-evaluation.md。评估用 plan/tasks 已补充,只覆盖本轮研究与隔离试验,不是 US2/US3/US5/US6 的实施计划。测试服务器 GET /route 和 GET /route/multi 已支持 profile=car/motorcycle;foodie 车型透传仍未实现。35 项常规断言通过,1 项限制探针确认条件禁行未执行;生产规则、路网和部署未改变,未宣称全境导航或性能验收完成。
2026-10-23 实施契约(优先于下文历史假设)
当前状态:实现中。用户确认普通外卖、多店外卖、自配送运费及闪送均支持 Google/自部署地图切换与回滚。原有“仅汽车模型”“停用 Google”“费用清零”“外卖失败直线兜底”“金额不一致静默覆盖”等历史约束被下述规则替代。
- 共用
sys_map_distance_source;缺失/未知/多个默认均选 GOOGLE;禁止选择自部署后暗中调用 Google。保留现有 Google 密钥与有效接口,不自动作废密钥。
- 外卖独立 N×1 矩阵
/route/matrix,每店→用户;闪送 /route/multi 按站点顺序整线计算,两者不能互换。
- 闪送
vehicleType=1/2 映射 motorcycle/car;外卖默认 car 保持当前 DRIVE 运价口径,服务端可显式配置 motorcycle。
- 普通单/v2/多店均由服务端核实运费,保存与计价同源的 jvli 公里;金额不一致按既有错误拒绝并提示重报。切换不改历史订单金额或支付快照。
- 外卖路线失败拒绝报价,闪送故障仍直线+车型估时;越界不降级。有效车型路线时长为行驶参考,不代表接单等待+取送件后的承诺送达时刻,缺时长继续估算。
- 保留促销、免运、自配送、预约、支付和履约状态;自取/堂食不请求路线。接单范围/围栏/附近排序保留既有直线计算。
- 新地址搜索/详情/逆查/附近统一经 foodie 后端,Google 和自部署返回同一字段;保留点选坐标、标明精度,不以附近候选替换坐标。App 源码由前端修改,旧 Google 页面继续可用。
- 自部署客户端统一地址前缀、读令牌、超时、错误码与非敏感日志;令牌不提供给 App。
- 商家地点以稳定门店 ID 同步,支持更新/停用,索引发布单独可执行;地图 API 不计算订单价格。
- 本轮原地完成可交付代码、测试与文档;不自动 commit/push、部署、执行数据库迁移或访问业务数据。正式体检、真实路段/法规/性能和前端联调保留手动验收。
User Scenarios & Testing (mandatory)
2026-10-28 追加:旧地址入口随来源切换
- 用户确认本轮不修改 App、平台或商家端地图页面。
/utils/getLocationAddr 与 /utils/getAddr 沿用原参数及响应结构;SELF_HOSTED 下只访问自部署,故障明确报错,不请求 Google/Baidu,也不依赖旧 Redis 语言缓存。GOOGLE 下保留原语言及响应兼容行为。
- 旧附近接口保留原 10km 半径及最多 20 个结果,地图
/nearby 的受限半径上限扩为 10km,默认300m及最多50项不变;新 /system/map/nearby 对外仍限制2km,不扩大新接口契约。
- 自部署结果转换为旧
address:{name,address}、results:[{place_id,name,vicinity,geometry:{location:{lat,lng}}}];保留精度、确认标识和原点选坐标,不把近似地址伪装为门牌。空候选返回空数组,错误不能伪装成功或向 Google 重试。
- 自部署旧附近入口仅接受当前在用的 type=1。已停用的 type=0 页面会将百度坐标再转换,不能向其伪装返回 WGS84;返回既有国际化参数错误,不请求百度或Google。
- 外卖仍默认 car,避免未经业务确认改变运费;闪送按用户车型。法规、动态交通能力不在未经真实样本验证时擅自更改。
- 本轮补齐兼容代码、两仓测试、合并后后端回归和发布记录。只读核对测试服务器;发布或迁移必须先具备可审阅的变更、备份和回退步骤,数据库迁移按项目规范由开发者手动执行。真实存量地址、车型法规与性能验收只能按实际执行结果记录。
User Story 1 - 闪送报价距离切换到自部署地图(Priority: P1)
运营在平台配置中把路线距离来源从 Google 切到自部署地图后,用户发起闪送报价时,距离与费用按自部署地图的道路距离计算;当地图服务不可达、路线不可达或坐标吸附失败时,沿用现有直线距离兜底保证报价可用,且每单记录实际使用的距离来源,运营可事后核对。
Why this priority: 闪送线已有路线距离抽象与"路线失败降级直线 + 距离来源落库"机制,切换成本最小;此切片独立可用,是整个切换的 MVP 与灰度入口。
Independent Test: 字典开关切到自部署后发起闪送报价,返回的距离来自地图服务;人为停掉地图服务再报价,仍能出价且距离来源标记为直线。
Acceptance Scenarios:
- Given 平台距离来源配置为自部署地图,When 用户提交闪送报价(两点或多站点),Then 报价使用地图道路距离,订单落库距离来源为"地图路线"。
- Given 地图服务超时或返回不可达/吸附失败,When 用户提交闪送报价,Then 报价仍成功(直线距离兜底),订单落库距离来源为"直线"。
- Given 距离来源配置切回 Google,When 再次报价,Then 全链路回到原 Google 路径,无需发版。
User Story 2 - 外卖运费改为道路距离 + 后端权威计价(Priority: P1)
用户下单时,外卖运费不再信任前端传入值:后端按订单的实际取送坐标用地图道路距离、结合平台运费规则(pos_freight 时段起步价与跳表)计算运费;用户确认订单页看到的费用即后端计算结果,前端传入的运费仅用于一致性校验。
Why this priority: 外卖运费目前用直线距离且由前端定价,既不符合"道路距离计价"目标,也存在改价风险;平台规则载体(pos_freight)已存在,补上后端权威计价即完成核心商业闭环。
Independent Test: 用一对坐标下单,比对订单落库运费与后端按 pos_freight 规则 + 地图距离手算结果一致;篡改前端传入运费不影响最终订单金额。
Acceptance Scenarios:
- Given 用户下单且地址、门店坐标有效,When 订单创建,Then 订单运费 = 平台 pos_freight 当前时段规则 × 地图道路距离(保留现有跳表规则:超起步距离 <0.5km 不计、0.5~1km 按 1km)。
- Given 前端运费与后端重算不同,When 创建外卖订单,Then 拒绝并提示重新报价,不能静默修改付款金额。
- Given 地图服务失败,When 外卖报价或建单,Then 返回明确错误,不返回0运费或直线替代价。
User Story 3 - 选址、逆地理与地址搜索切自部署地图(Priority: P2)
用户在 App 选择收货/取件地址时,新接口统一经foodie后端按来源聚合,App不直连自部署地图或持有读令牌;旧Google页面和旧地址接口保留兼容。
Why this priority: 选址是 App 侧高频功能,依赖 App 前端改造与联调,排在服务端能力切换之后;完成此切片后,用户侧不再产生 Google/百度地址类调用。
Independent Test: 在 App 地址选择页搜索中文地址、点选地图取点,返回结果来自自部署地图;断开地图服务时地址选择页给出明确错误提示。
Acceptance Scenarios:
- Given 用户在地址页输入中文关键词,When 发起搜索,Then 返回自部署地图的候选列表(名称/地址/坐标)。
- Given 用户在地图上点选一个点,When 触发反查,Then 返回最近候选与精度标识,坐标不被候选覆盖,需用户确认。
- Given 地图服务不可用,When 用户搜索地址,Then 收到友好错误提示,App 其它功能不受影响。
User Story 4 - 闪送多站点订单路线支持(Priority: P2)
闪送多站点单(用户按顺序添加多个站点)在自部署地图下同样按站点序列计算道路距离;在地图服务扩展多站点能力上线前,多站点单维持原 Google 路径或直线兜底,不因切换而坏单。
Why this priority: 自部署地图当前仅支持两点路线,多站点是包装层待扩展项;属跨仓库依赖,可与 foodie 侧切换并行推进。
Independent Test: 提交 3 站点闪送报价,返回的距离为按站点顺序的道路总距离;地图多点能力未上线期间,该单按既有路径正常出价。
Acceptance Scenarios:
- Given 地图服务已支持多站点,When 用户提交 3 站点报价,Then 距离按传入顺序途经各站点的道路总距离计算。
- Given 地图多站点能力未上线,When 用户提交多站点报价,Then 报价仍成功(Google 或直线兜底),不出现报错坏单。
User Story 5 - 商家可被地图搜索发现(Priority: P3)
平台的台湾商家(名称/地址/坐标)同步进自部署地图的热门地点库,用户在地址选择时能直接搜到商家并定位,减少手输地址成本。
Why this priority: 提升选址体验的增量能力,依赖前四个切片稳定后才有意义。
Independent Test: 新建/修改商家后(或定时任务执行后),在地址搜索框输入商家名能搜到该商家。
Acceptance Scenarios:
- Given 商家已在平台建档且坐标有效,When 同步任务执行,Then 地图搜索该商家名可返回其名称、地址与坐标。
- Given 商家改名或搬迁,When 同步再次执行,Then 地图侧信息更新,旧信息不残留。
User Story 6 - 对应车型参考时长与双来源长期保留(Priority: P3)
使用对应车型的路线行驶时长作为参考,缺失时按车型估算;不把行驶时长当承诺送达时刻。保留Google及有效配置以支持切回,不以全部下线Google或费用清零作为验收目标。
Why this priority: 保留车型正确的参考时长和Google回滚路径,不让来源切换改变历史金额或伪造承诺送达时间。
Independent Test: 两车型分别返回路线时长;时长缺失时仍有合理估算;切回Google后新报价正常,已有订单快照不变。
Acceptance Scenarios:
- Given 用户选机车或轿车,When 闪送算路,Then 使用对应车型时长,缺失时估算,不把汽车时间直接当机车时间。
- Given 平台切回Google,When 新报价或新选址,Then 使用保留的Google路径;已有订单不被回写,旧有效接口不被自动停用。
Edge Cases
- 路线不可达(NO_ROUTE):闪送直线距离/车型估时降级;外卖拒绝报价,不返回免费运费或部分距离。
- 坐标吸附超限(SNAP_TOO_FAR,默认 250 米):老地址库坐标可能大面积触发——切换前必须用存量地址批量试算体检,按结果决定调整吸附上限或引导重选地址。
- 坐标越界(OUTSIDE_REGION):拦截并提示地址无效,不降级。
- 多站点路线中,越界或吸附失败可定位到具体站点时,地图错误返回从 1 开始的
point_index;整条路线不可达(NO_ROUTE)不指定站点序号。
- 地图服务整体宕机:闪送直线距离/车型估时降级;外卖报价拒绝,选址明确报错。数据库派单和附近骑手不受影响。
- 多站点单在地图多点能力空窗期:维持 Google 或直线兜底,不坏单。
- 外卖坐标取自有效门店和用户拥有的地址;坐标缺失/非法拒绝报价,不能伪造有效运费。
Requirements (mandatory)
Functional Requirements
- FR-001: 平台 MUST 提供距离来源开关(第三方路线服务 / 自部署地图),可运行时切换与回滚,不依赖发版。
sys_map_distance_source 仅在启用项中恰好一个 is_default=Y 时采用其值;缺失、多默认或未知值回退 Google。后台修改会更新缓存,手工执行 SQL 后需刷新缓存或重启。
- FR-002: 闪送报价距离 MUST 优先取自配置的路线来源,失败时按直线距离兜底,且每笔报价/订单 MUST 落库实际距离来源。
- FR-003: 普通/v2/多店外卖 MUST 服务端重算道路运费并保存同源jvli;前端运费不一致时拒绝并提示重报,不能静默修改应付金额;自取/堂食不算路。
- FR-004: 外卖运费报价接口 MUST 返回道路距离、费用与距离来源三项信息,供前端展示与后端校验共用。
- FR-005: 新选址搜索/反查/附近/详情 MUST 经foodie后端,App MUST NOT 直连自部署服务或持有读/管理令牌;旧Google选址保留兼容。
- FR-006: 地图支持两点、有序多站路线及独立起点矩阵;闪送整线距离不得用作多店各店运费,外卖矩阵不得部分成功。
- FR-007: 商家信息(名称/地址/坐标)MUST 可同步至地图搜索库,支持新增与更新。
- FR-008: 闪送 MUST 显式传用户车型,使用对应路线时长;缺时长按车型估算。行驶时长不含接单等待/取送件,不是承诺送达时间,不新增未经确认的倍率。
- FR-009: MUST 保留Google及有效配置,支持运行时切回;已有订单不重新计价。历史硬编码凭据权限/轮换由运维处理,不在本轮作废全部回滚密钥。
- FR-010: 切换前 MUST 提供存量收货地址的批量路线试算体检能力,输出吸附失败比例与清单供运营决策。
- FR-011: 派单、附近骑手、门店距离排序 MUST 维持现有数据库半径计算,不依赖地图服务可用性。
Key Entities (include if feature involves data)
- 距离来源(distanceSource): 每笔闪送报价/外卖订单记录的距离计算方式(地图路线/直线),用于对账、监控与切换验证。
- 平台外卖运费规则(pos_freight,既有): 按时段的起步价、起步距离、跳表单价;计价输入为道路距离,输出为运费。
- 闪送定价规则(flash_delivery_pricing,既有): 按车型/时段的起步、步长单价、加急费率;计价输入为道路距离。
- 地图服务配置(平台字典): 距离来源开关、地图服务地址、失败策略、机型折算系数等运营参数。
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: 切换自部署地图后,闪送报价成功率不低于切换前基线的 99%(兜底机制保证)。
- SC-002: 外卖订单运费 100% 由后端计算落库,抽样核对与平台规则手算结果一致。
- SC-003: 切换后选址搜索与路线计算的 95% 请求在 2 秒内返回。
- SC-004: Google、自部署、切回Google均通过普通/多店/闪送回归;旧客户端字段兼容、历史订单金额快照不变。
- SC-005: 存量地址体检完成并输出吸附失败清单后,方可全量切换;全量切换后因地图错误导致的下单失败率 < 0.1%。
Assumptions
- 本 spec 覆盖 foodie 后端与运营配置;App 前端(uni-app,不在本仓库)的选址页、费用展示改造由前端团队按本 spec 接口对接。
PosOrderController 的通用管理接口 POST /system/order(add(...))和 PUT /system/order(edit(...))已废弃,不纳入本特性的订单创建、修改入口;它们与仍在使用的 /system/order/addorder 不同。
- 地图服务(Foodie_HCM_Map)部署在与 foodie 服务器同内网或经私网打通;其多站点路线扩展、读接口防护属地图仓库工作,作为跨仓库依赖跟踪。
- 地图提供car/motorcycle独立路网,无实时交通;外卖默认car保持现有DRIVE计价口径,改机车需另验价格。模型不代表完整台湾法规验收。
- 现有运费规则载体(pos_freight、flash_delivery_pricing)与管理端 CRUD 继续沿用,本特性不改动计价规则本身。
- 派单/附近骑手/门店排序维持数据库半径计算,不纳入切换范围。
- 外卖前端运费与后端不一致时拒绝并提示重新报价,不静默改支付金额。
- 历史凭据权限核对与轮换由运维执行;保留Google有效配置,本轮不调用第三方控制台。