第四个行业增量包(对应 084feature_catalog的vehicle)。
业务模型由义父 2026-09-24 16:3x 定调,推翻了我最初的班次预售方案。
| # | 定调 | 对设计的影响 |
|---|---|---|
| ① | 游览车摆渡车没有班次,随上随走 | 弃用 transport_slots 的 capacity/booked 座位预售模型;改为「登记即收款」 |
| ② | 装备租赁是一个单独的模块 | /rental-workbench、/rental-orders 从 vehicle 包移出转 DEFERRED_ROUTES |
| ③ | 加模拟测试数据 | 造专属测试身份 + 全链路造数验证(见 §六) |
蓝图里本就是三个独立条目,我最初混为一谈是错的:
transport:卖"驮力+接驳"(牦牛/马帮驮运、骑行、镇-止热寺接驳),按场次/驮位收费,核销两段式(上驮确认/返程确认)⇒ 本次做的是 ④ 里的「镇内接驳」+ 游览车/摆渡车这一支:随上随走,不按驮位、不排班次。
| 核查项 | 结论 | 依据 |
|---|---|---|
复用 transport_slots? | 弃用 | 字段 capacity/booked/unit_price_ref 是座位预售模型,与「随上随走」冲突;实测 0 行、全仓零代码引用(建了表从没人写过) |
复用 transport.py? | 不复用(保留) | 它是 C端只读内容展示(transport_routes 3 条平台内容:机场接送/包车/拼车),返回 {code:0,data} 形状;B端 Transport.tsx 是 WIP-190 真只读页,保留不动 |
复用 verifications 核销? | 不复用 | 全仓无独立 vouchers 表(只有 quote_vouchers),核销链路不适合游览车现场记人次 |
/rental-* 现状 | 移出门禁 | rental.ts 是 WIP-272 装备租赁借还域(RENTAL_READY=false 降级),后端 /merchant-console/rentals 零实现;被顺手挂进 vehicle 包属语义错位 |
/vehicle-package | 保留不动 | 页面注明「机动车整包端点由小齐负责」,属长途包车按趟计价,与随上随走不同业务流;避免撞车 |
| vehicle 包 4 路由现状 | 实为 3 个不同域 | 整包(小齐) + 装备租赁(借还) + 运输线路展示(只读),没有一个是游览车/摆渡车 |
| 车队商户 | 0 家真实 | prod business_type='transport' 真实商户为 0;测试商户 8021(business_type=vehicle, is_test=t) |
| SQL 编号 | 用 092 | 091 已被零售包占用;migrate.sh 以 filename 为 PK |
IF NOT EXISTS)价格沿用全仓 *_fen;所有表带 is_test(继承商户,否则 C端过滤形同虚设);merchant_id 参数化不写死。
fleet_vehicles — 车辆档案(含合规台账)| 字段 | 说明 |
|---|---|
id / merchant_id FK CASCADE | |
plate_no | VARCHAR(16),唯一 (merchant_id, plate_no) |
vehicle_type | CHECK:小型车/中巴/大巴/游览车/摆渡车/其他 |
brand_model | 品牌型号(如 丰田考斯特) |
seat_capacity | INTEGER ≥1 核载人数(登记时校验人数不得超核载) |
license_no | 道路运输证号 —— 合规红线字段,仅 B端可见 |
license_expires_at | DATE 营运证有效期 —— 到期预警依据 |
driver_name / driver_phone | 司机信息 —— 仅 B端可见,C端不出 |
status | active / maintenance / retired |
note / sort_order / is_test / created_at / updated_at |
索引:(merchant_id, status);部分索引 (merchant_id, license_expires_at) WHERE status <> 'retired'(报废车不预警,无需扫)。
fleet_routes — 运营线路(定票价,不排班次)| 字段 | 说明 |
|---|---|
name | 线路名,唯一 (merchant_id, name) |
route_type | shuttle 摆渡 / sightseeing 游览 / transfer 接驳 |
origin / destination | 起点 / 终点 |
price_fen | 票价(按人次,非按趟) |
service_hours | 服务时段文本(如 08:00-18:00) |
departure_mode | loop 循环发车 / on_demand 随叫随走 —— 两者都非班次制 |
interval_minutes | 循环发车间隔(loop 时填,可空;CHECK >0) |
status | active / paused / offline |
description / sort_order / is_test / 时间戳 |
刻意不建:班次表、座位表、预订表。随上随走 ⇒ 没有"某趟车还剩几个座"这个概念。
fleet_rides — 乘车登记流水(登记即收款)| 字段 | 说明 |
|---|---|
ride_no | F{YYYYMMDD}{6位hex},唯一 (merchant_id, ride_no);餐饮 R / 零售 T / 车队 F 区分域 |
route_id / vehicle_id | 软引用(车辆可空=不指定车) |
route_name / plate_no | 快照,线路改名/车辆报废不影响历史 |
passengers | INTEGER ≥1 人数 |
unit_price_fen | 单价快照 |
total_fen / paid_fen | 登记时即 paid = total(无待支付态) |
pay_method | cash/wechat/alipay/other |
status | recorded(已登记=完成)/ refunded / cancelled —— 无 created/pending |
operator_id / note | |
boarded_at | 上车时间(默认 now) |
is_test / 时间戳 |
UPDATE feature_catalog SET description=... WHERE feature_key='vehicle' AND description='租赁 / 接送 / 车次管理'
→ 改为「游览车/摆渡车/接驳:车辆档案、线路票价、现场登记人次、营运证到期预警」。
原描述把装备租赁混进车队包(正是义父第②点的根源),且「车次管理」与随上随走冲突。带旧值条件 ⇒ 幂等且不误改他人后续修订。
console_router = /api/miniapp/merchant-console/fleet鉴权:_staff() → 委托 merchant_scope.resolve_merchant(禁无 ORDER BY 的 LIMIT 1,P0 教训)。
车辆:GET/POST /vehicles、PUT/DELETE /vehicles/{id}(DELETE = 软删 retired)
线路:GET/POST /routes、PUT/DELETE /routes/{id}(DELETE = 软删 offline)
乘车:GET /rides、GET /rides/{id}、POST /rides(登记即收款)、PUT /rides/{id}/status(退款/作废)
概览:GET /dashboard
miniapp_router = /api/miniappGET /fleet/routes?merchant_id=&route_type= —— 游客查在营线路与票价。
只出 active 且 is_test=FALSE;不暴露 driver_phone / license_no(隐私 + 合规内情)。
| 零售收银 | 车队登记 | |
|---|---|---|
| 状态机 | created → paid(需 settle) | recorded 一步到位 |
| 收款 | 开单后单独结账 | 登记瞬间即收款 |
| 库存 | 扣减 + 流水对账 | 无库存概念(车辆不是消耗品) |
| 逆向 | 退款回滚库存 | 退款仅把 paid_fen 归零 |
| 校验 | 库存充足 | 人数 ≤ 核载 + 线路/车辆均须 active |
400 格式 / 401 未认证 / 403 非商户成员 / 404 不存在或跨商户 / 409 状态冲突·超核载 / 422 业务校验。零 mock、零占位。
三层防护,事前拦优于事后告警:
FleetVehicles):核载 >9 座且未填道路运输证号 → 直接拒绝保存并说明罚则_license_state):expired 已过期 / expiring 30 天内到期 / valid / none;报废车不预警dashboard):license_expired + license_expiring 计数;概览页红色 banner「请立即停运并换证」user 9101 挂 8001/8011/8021 三商户,而我修的 resolve_merchant 按 merchant_id ASC 确定性取值 ⇒ 永远解析到 8001,够不着 8021 车队商户。
(这正是我上报的「P0-A 止血但未终解」的直接后果:多商户用户无法切换商户。)
造只属于 8021 的专属测试用户 9121(openid=test_fleet_owner_021,沿用项目既有 test_seed_owner_* 命名惯例,复用 9101 密码哈希),建 merchant_staff 关系 + 开通 vehicle 授权。
is_test=TRUE,幂等(WHERE NOT EXISTS / ON CONFLICT DO NOTHING),可精确清理INSERT 0 0merchant_count_of_9121 = 1(确实只属 8021)scripts/check_pack_wiring.py为什么必须新增:本轮实测 tsc 零错误却漏了两处接线——
business-pack.ts 的 vehicle 数组缺 /fleet-dashboard(门禁放行但页面被拦成「该功能不属于您的业态包」)Workbench.tsx 的 QUICK_ACTIONS 缺车队 4 项入口(只能 URL 直达)两处都是「数组少一项」,TS 类型层完全合法 ⇒ tsc 结构上查不出来。与上轮「curl 打后端绿 ≠ 前端能用」同源。
判据:对 hotel/restaurant/retail/vehicle 四包每条门禁路由,校验三方一致——
① App.tsx 有 <Route path> ② business-pack.ts 在包白名单 ③ Workbench.tsx 有菜单入口;
并反向校验 App.tsx 中带业态前缀的路由是否都进了某包或 DEFERRED_ROUTES(防孤儿路由落 * 兜底)。
已反证有效:故意从 vehicle 门禁删掉 /fleet-dashboard → 脚本点名报红「1 条路由未挂门禁」+ EXIT≠0;还原后 EXIT=0、diff 确认与备份一致。
当前四包 27 条路由三方一致、零孤儿。
| 项 | 结果 |
|---|---|
| SQL 092 临时库 | 3 表建成 + UPDATE 1 命中 feature_catalog 描述修正 + 幂等复跑 0 ERROR |
fleet.py 语法 | py_compile ✅ + AST 查 import 完整性 ✅(py_compile 对缺 import 静默通过,已反证) |
| 未使用 import | 清掉 timezone(照抄零售范式带过来的) |
services/fleet.ts | 一开始就用 asItems 运行时形状校验,不重蹈餐饮白屏覆辙 |
| tsc | 零错误 |
| vite build | 成功(bundle index-Mf5jrlJv.js) |
check_pack_wiring.py | 四包 27 路由三方一致、零孤儿;已反证有效 |
| 测试身份 | staging + prod 双库 9121→8021 就绪、vehicle 授权开通、幂等复跑 no-op |
feature_catalog vehicle 描述已由「租赁 / 接送 / 车次管理」改为「游览车/摆渡车/接驳:车辆档案、线路票价、现场登记人次、营运证到期预警」;
3 表建成;既有酒店数据未受损(room_types 1→3 / 10→4 / 8001→3,与部署前基线逐项一致)
登记即收款(status=recorded 且 paid=total=120,无需 settle)、超核载 409(9座车登10人)、
暂停线路登记 409、同名线路 409、退款 paid→0、终态约束 409、非法 status 422(证 body 传参生效)、
证照派生 valid/expiring、C端 is_test 隔离 + 反证置 f 后可见且不泄 driver_phone/license_no、
概览口径 trips=1/pax=2/rev=160(退款单不计营收)、跨商户读改删三向 404、软删留痕
scripts/verify_fleet_render.mjs(新增入库)→ FLEET_RENDER_OK四页 4/4 通过、条目数硬门槛达标(概览 6 stat / 车辆 2 卡 / 线路 2 卡 / 乘车 1 卡)、
证照告警 badge、菜单四项入口、点击跳转非死链、零控制台错误
(造数据后验列表,因白屏只在有数据时暴露;金额 90=30×3 正确)
html/bworkbench/ → 清 AppleDouble → assets 同步根级 → nginx -t + reload;线上 bundle index-Mf5jrlJv.js 含车队 4 个特征串、新 CSS index-CVQ-3pue.css 200、
b 域与 doc 域 /workbench/ 均 200
is_test 已还原为 t;测试身份 9121 与 vehicle 授权保留供后续回归复用
check_pack_wiring.py 三方接线一致 / check_asyncpg_param_ambiguity.py84 文件零风险,均已反证)+ 既有 4 个 v2 门禁复跑 EXIT=0
/tmp/kailash_prod_backup_20260924_175308.sql.gz(213K / 18299 行 / gzip -t 通过)/vehicle-package 机动车整包:端点归小齐、后端零实现,本次未碰。是否要与他确认归属后再建?(长途包车按趟计价,与随上随走不同流)/rental-* 已转 DEFERRED_ROUTES(门禁对所有包不可达)。后端 WIP-272 域零实现、前端 RENTAL_READY=false。何时立项建包?rental.ts 契约不一致:用 .list + {code:0} 形状,与我后端 {ok,data:{items}} 不同(有 ||[] 兜底不白屏但取不到数据)。属车队/租赁域外,未改。category 参数失效:category-transport / category-vehicle 传 ?category=transport|vehicle,但后端 list_products 签名只有 limit ⇒ 参数被完全忽略,两个分类页显示的其实是全量套餐。属 C端+miniapp_products.py 域,未改,待裁定。workbench_tickets 全仓零消费点 ⇒ 多商户用户无法切换商户(本次靠造单商户用户 9121 绕过)。需刘总令 + 前端配合。transport_slots 空表处置:座位预售模型对游览车不适用,但驮运(蓝图③)确实需要按驮位。建议保留给未来驮运域,本次不动。两处都是 asyncpg 传参无类型标注 导致 PG 无法确定 $N 类型 → 接口 500。
py_compile / tsc / 路由挂载检查全部放过,只有真实调用才暴露。
GET /fleet/dashboard 500asyncpg.exceptions.AmbiguousFunctionError: operator is not unique: date + unknown
HINT: Could not choose a best candidate operator.
license_expires_at <= $2 + $3,PG 面对 date + unknown 无法在 (date+integer) 与 (date+interval) 间抉择
$2 + $3::int(commit 97680148)PUT /fleet/rides/{id}/status 500asyncpg.exceptions.AmbiguousParameterError: inconsistent types deduced for parameter $1
SET status=$1, paid_fen=CASE WHEN $1 IN ('refunded','cancelled') … 把同一 $1 用两次 —— status=$1 推导 varchar,$1 IN (字面量) 推导 unknown
today_trips=2 而非 1,退款单仍计入营收)new_paid),每个参数只用一次(commit c170a9f9)scripts/check_asyncpg_param_ambiguity.py两类判据:① 未 cast 参数参与算术且紧邻操作数是 date/timestamp 列(或 $N±$M 与 date 列比较)
② 同一 $N 出现 ≥2 次且其中一次处于 IN(…) 或与引号字面量比较。
判据经三轮反证收紧(每轮都靠反证抓出自身缺陷,不是一次写对):
| 轮次 | 缺陷 | 反证发现方式 |
|---|---|---|
| 一 | 不限定 date 列 → 全仓 25 处几乎全是 numeric 误报(balance-$2 PG 可推断) | 人工过输出发现噪音 |
| 二 | 只看「窗口内是否出现 date 列」→ 同条 UPDATE 里的 updated_at=now() 就误报(26 处) | 反证 4 个已知误报源全命中 |
| 三 | 判据② 正则方向写反(要求 IN ($N,真错例是 $N IN (…))→ 漏报 | 喂修复前真 SQL 未命中 |
最终反证 10/10:2 类真风险必命中;8 类误报源(numeric 三种、make_interval、
单次使用、$1 重复但类型一致等)全不命中。当前全仓 84 文件零风险。
门禁设计教训:判据太宽 = 噪音 = 等于没门禁;但「全绿」必须先反证判据能抓已知错例,
否则是假绿。本 session 已三次遇到「顺证通过但反证失败」(py_compile 放过缺 import、
第一版 asyncpg 判据漏报、接线门禁需故意删项才验出有效)。