QX Garage门店数字化交付
MVP SPEC · FOR CODING AGENT

MVP 功能清单与开发方案

文档 QX-MVP-2026-001 │ V1.0 │ 2026-11-10 │ 目标:给 agent 一份能直接开工的规格,一周内出一个能在店里演 3 分钟的 demo

产品汽修门店数字化系统(单店版),多租户,一店一租户
本阶段目标不是功能齐全,是演示时老板会说"这个多少钱"
后端已实现并在线:/api/v1 真实接口(SQLite 落盘、三租户种子数据),附 接口文档;前端按第 11 节契约对接,不需要再等后端
交付物可访问的 H5 demo + 源码 + 3 个种子租户 + 演示脚本
不在本阶段真实支付、发票、保险理赔、OBD、连锁总部、AI 改代码
先点一下:打开 demo(I1–I2 + I4 + I6)→  老板账号 13800000000(可切换三家店);前台 13800000001(只能看 t1)。 验证码任意 6 位。这份 demo 直接连第 11 节的真接口,不是假数据;断网会自动切到只读快照。

1 这份文档怎么用

把下面这段直接发给 agent(连同本页 URL),它就能开工:

任务:按 https://ssa.agent2.0755xzb.club/qx/mvp.html 这份规格,做一个汽修门店管理系统的 H5 demo。 技术栈 Vue3 + Vite + TypeScript + Pinia + Vue Router。 数据只走一层:所有页面只调 src/api/index.ts,它下面挂两个同签名的适配器—— 默认走线上真接口 https://ssa.agent2.0755xzb.club/api/v1(契约见第 11 节), 断网时切到 mock.ts(localStorage 持久化,字段与接口逐字段一致)。 页面里不许出现 fetch,也不许出现写死的假数组。 多租户用 /t/:tenantId/ 路由前缀,与接口 /api/v1/t/{tenantId}/ 一一对应。 只做第 3 节的 P0 功能、第 4 节的 8 个页面,P1 只做工作台的两张小图表。 严格按第 9 节验收,跑不通的项不许标完成。先交可点的页面,再补交互,最后接数据。
为什么不能"全部走 mock":两套数据源会长成两个样子——字段名、金额单位、状态机迟早对不上。 等你换真接口时,散落在页面里的 localStorage 读写要一行行拆掉,那才叫重写。 正确做法是一套签名、两个实现:页面永远只知道 api.orders.create(), 不知道背后是 HTTP 还是内存。这份文档第 11 节的接口已经在线,直接当默认实现用。

三条纪律,写进 agent 的 prompt 里:

  1. 演示优先于完备:任何功能如果 3 分钟演示里用不到,这一版不做。
  2. 不许留"开发中"占位页:做不了的入口直接隐藏,不要放一个点进去是空白页的按钮。
  3. 手机优先:所有页面先在 390px 宽下可用,再考虑桌面。

2 先定 3 分钟演示脚本

MVP 的范围不是"想到什么做什么",是被这 3 分钟倒推出来的。演示场景:老板坐在他自己的前台,你拿手机,用他店里的车、他店里的工单演。

时间你做什么老板看到什么对应功能
0:00–0:30打开工作台,不解释,让他自己看今天 8 台进场、3 台待结算、2 条超期提醒F1 工作台
0:30–1:20输一个他熟悉的车牌,点"接车"客户名、车型、上次保养里程、上次换的机油自动带出;勾 3 个项目,报价 380 元F2 接车开单 + F3 报价
1:20–2:00用技师视角点"开工"→"完工",拍一张照片工单状态从"施工中"变"待结算",前台那边同步变了F4 施工报工
2:00–2:40点结算,选微信收款"本次 380,累计消费 4,260,下次保养 2027-03-12";一条给车主的微信消息草稿F5 结算 + F6 客户档案
2:40–3:00切到提醒页下个月 37 条到期提醒已经排好,"这条不发,就是丢单"F7 到期提醒
这 3 分钟决定了功能取舍:库存、提成、报表、多语言、深色模式,在这 3 分钟里一个都用不到。 它们要么做成 P1 的一个角标,要么这一版直接不做。

3 MVP 功能清单

3.1 P0(必须做完,缺一个演示就断)

编号模块必须做到什么验收点
F1工作台今日进场台次、待结算、待提醒三个数字 + 最近 5 条工单打开 1 秒内出数,不转圈
F2接车开单输车牌 → 自动带出客户/车型/上次里程;选项目、改数量、看合计;保存生成工单号30 秒内开完一单
F3报价确认生成一张可发微信的报价(图或链接),带"客户已确认"标记能点"模拟发送",状态变已确认
F4施工报工技师点开工/完工,传 1 张照片,状态机流转状态变更后工作台数字同步
F5结算收银合计、折扣、支付方式、开单完成;显示下次保养日期结算后工单不可再改
F6客户与车辆一车一档:基本信息、历史工单、累计消费、保养周期从车牌能查到全部历史
F7到期提醒按保养日期/里程生成的待提醒列表,可标记已联系列表条数与工作台一致
F8设置门店名/电话、工位数量、员工 3 人、常用项目价目表改价目表后开单价格跟着变
F9配件与领料配件库(SKU/规格/成本/售价/安全库存)、入库盘点、技师按工单领料、退料领料后库存立刻减、该单成本立刻加;领超库存被拒
F10老板大屏一屏看完:今日营业额、进场、待结算、客单价、毛利、7 天趋势、最近工单、库存预警;30 秒自动刷新1440×900 与 iPad 横竖屏都不滚动、不溢出
F11员工提成按分类的提成规则(工时费 / 项目金额 × 比例)、结算自动计提、按月汇总与明细、技师只看自己改规则不影响历史;换比例后新单按新规则算
F12客户看板挂店里给车主看:在店台数、工位空闲数、繁忙度;三列(施工中 / 待派工 / 可交车)、预计完工时间、超时变红;车牌只留后四位,不出现任何金额与车主全名陌生人盯着看也拿不到价格和电话;同一台车只出现在一列
F13交车确认已收款未交车的单独立成「待交车」一档,列表可筛;点「确认交车」记交付时间,交车后进「已完成」,客户看板不再显示;未收款也能交车(挂账),那种单留在「待结算」交两次返回 409;未完工不能交车;交车后不能再领料/退料;已交车未结算仍在待结算里
F14预约到店按天看预约(时间、车牌、项目、预计工时与完工时间);到店一键转工单;过期未到高亮提醒;可改时间/取消预约里的项目、时长、金额和到店开出来的工单完全一致
F15客户自助 H5客户在微信里自己约时间(选项目自动算时长和价),以及用「车牌 + 手机后四位」查自己车的进度;全程无金额、无其他客户信息预约免登录可提交;查别人车返回 403;页面里搜不到任何价格
F16打印A5 接车单(含里程、车架号、随车物品勾选、签字位)、A4 结算单(含配件、优惠、实收、大写金额);走浏览器打印,不接打印机 SDKA5/A4 各一页,不跨页;灰色背景不出现在纸上
F17导入客户Excel 另存 CSV 后上传或粘贴,自动识别表头;按车牌/手机号去重,重复导入只更新不新建同一份导两次,第二次新建 0 条;手机号/车牌格式错的行单独列出
公众号怎么挂:两个页面是纯静态的,不需要登录,直接放进公众号菜单即可 —— /qx/book.html?t=t1(预约)、/qx/progress.html?t=t1(查进度)。
要让「关注后免输入车牌」生效还需要三步:① 公众号后台拿到 AppID / AppSecret; ② 配置「网页授权域名」为本站域名;③ 后端用 code 换 openid,然后 POST /pub/{tid}/bind 存下 openid, 之后 GET /pub/{tid}/progress?openid=… 就能直接出结果。AppSecret 只留在服务端,浏览器不接触。
菜单在微信里打开时把 openid 拼进链接即可:/qx/progress.html?t=t1&openid=oXXXX; 没绑过会退回「车牌 + 手机后四位」表单,查成功一次自动绑定。
成本只给老板看,而且是服务端拦的:配件成本、库存金额、本单毛利,在 role != owner 的请求里 直接从响应里去掉(cost_cents / parts_cost_cents / gross_cents 返回 null 或不存在)。 不是前端 v-if 藏起来——师傅打开 DevTools 也拿不到。前台该看到的是"领了哪些、谁领的、库存还剩几个"。

3.2 P1(有余力再做,只为演示加分)

编号模块做到什么程度就够
P1-1工作台迷你图近 7 天进场台次柱状图(纯 SVG,不引图表库)
P1-2库存简版只做"机油剩 3 桶"的预警角标,不做入库出库
P1-3多租户切换器演示模式可见,生产模式隐藏(见第 6 节)
P1-4深色模式跟随后系统,只保证工作台不刺眼

3.3 这一版明确不做

财务对账 · 电子发票 · 保险理赔对接 · 配件采购单与供应商对账(只做了入库与领料,没做采购流程)· 员工提成 · 连锁总部 · 会员储值 · 微信真实支付 · 短信真实发送 · OBD/诊断仪 · 多语言(只做简体中文)· 权限体系(demo 只区分"前台/技师"两个角色)。

为什么砍这么狠:这些东西每一个都能吃掉两天,但都不会让老板说出"这个多少钱"。 老板掏钱的触发点是"看到自己的车、自己的客户、自己的钱",不是功能数量。

4 页面与路由

全部挂在 /t/:tenantId/ 下,共 8 个页面 + 1 个登录。手机优先,底部四个 Tab:工作台 / 开单 / 客户 / 我的。

路由页面关键元素空状态文案
/login登录手机号 + 验证码(demo 任意 6 位通过)+ 演示租户一键进入—
/t/:id/工作台三个数字卡、最近工单、7 天柱图、待提醒角标"今天还没有车进场"
/t/:id/new接车开单车牌键盘、客户卡片、项目选择、合计条、保存"输入车牌开始"
/t/:id/order/:no工单详情状态条、项目明细、照片、操作按钮(发报价/开工/完工/结算)"这单还没开始"
/t/:id/orders工单列表按状态筛选(待施工/施工中/待结算/已完成)"没有符合条件的工单"
/t/:id/customers客户列表搜索(姓名/手机/车牌)、按最近到店排序"还没有客户,开第一单就有了"
/t/:id/customer/:cid车辆档案车主信息、车辆、历史工单、累计消费、下次保养—
/t/:id/reminders到期提醒本月/下月分组、一键标记已联系、消息模板预览"这个月没有到期的车"
/t/:id/settings设置门店信息、工位、员工、价目表—

4.1 每个页面必须有的四样东西

  1. 骨架屏:mock 加 300ms 延迟,页面先出骨架再出数——没有骨架的 demo 看起来像网页,不像产品。
  2. 空状态:每个列表都要有,文案用上面表格里的,不要写"暂无数据"。
  3. 操作反馈:保存/结算后必须有 toast + 状态变化,不能点完没反应。
  4. 返回和面包屑:手机端左上角返回,桌面端顶部路径,别让老板迷路。

5 数据模型

八张表,字段按 demo 够用即可;tenant_id 是所有表的第一个字段。

表字段说明
tenantid, name, phone, address, plan, created_at一个租户 = 一家门店
staffid, tenant_id, name, role(前台/技师), phonedemo 不做权限,只做视角切换
customerid, tenant_id, name, phone, wechat, level, total_amount手机号唯一(租户内)
vehicleid, tenant_id, customer_id, plate, brand, model, vin, mileage, last_service_at车牌唯一(租户内)
service_itemid, tenant_id, name, price, cost, duration_min, category价目表,开单直接引用
work_orderid, tenant_id, no, vehicle_id, customer_id, status, total, discount, paid, pay_method, created_at, finished_at状态:待施工→施工中→待结算→已完成
order_itemid, tenant_id, order_id, item_id, name, price, qty, tech_id下单时快照价格,改价目表不影响历史单
reminderid, tenant_id, vehicle_id, type(保养/保险), due_at, status(待提醒/已联系/已到店)按 last_service_at + 里程推算

6 多租户方案

你的判断是对的:一店一租户,天然隔离,也天然好卖——"你的数据只有你能看到"这句话老板听得懂, 比讲"行级权限"有用。

6.1 demo 阶段(这一版)

层做法为什么够用
路由/t/:tenantId/...换 URL 就是换店,演示时可以直接切
存储localStorage key 前缀 qx:{tenantId}:切租户 = 换命名空间,物理上不串数据
mock 接口所有请求自动带 X-Tenant-Id,mock 层按它取数据提前把接口形态定对,后面换真后端不用改页面
切换器顶部下拉,仅 ?demo=1 时显示给老板看时不会露馅
一人多店user_tenant 关联表:一个账号可挂多店,老板账号挂 3 家;切换只换路由,令牌不变演示"换一家店数据全变",也对应连锁的真实场景

6.2 生产阶段(demo 之后才做)

等级隔离方式成本什么时候上
S1共享库,行级 tenant_id + 全表强制过滤最低前 20 家
S2共享库,每租户独立 schema中20–100 家,或有客户明确要求
S3独立数据库/容器,一客一实例高连锁客户、要求本地部署的
一条硬规矩:不管哪个等级,tenant_id 必须由服务端从登录态解析, 永远不接受前端传上来的 tenant_id。demo 阶段可以放在 header 里图方便, 但要在代码里留一行 // TODO: 生产环境改为从 JWT 解析,别让它混进正式版。

7 技术选型与约定

层选什么不选什么
框架Vue 3 + Vite + TypeScript不要 Nuxt(SSR 对 demo 没用,还多一层坑)
状态Pinia,按租户分 store 实例不要 Vuex
路由Vue Router 4,history 模式—
UI自己写 8 个组件:按钮/输入/卡片/列表项/状态标签/空状态/骨架/toast不要引 Vant/Element:默认样式太重,改起来比写还慢
样式CSS 变量 + 单文件 scoped,按 ui.html 的令牌取值不要 Tailwind(agent 容易写出无法维护的长串)
mock一个 src/mock/ 目录:与真接口同签名的适配器,localStorage 持久化,仅作离线兜底不要让它成为唯一数据源,也不要把写死的数据塞进组件
图标内联 SVG,不超过 10 个不要图标库

7.1 目录结构(强制)

src/
  main.ts
  router/index.ts          # /t/:tenantId 嵌套路由 + 守卫
  stores/                  # tenant.ts  order.ts  customer.ts
  mock/
    db.ts                  # 种子数据:3 个租户 × 全量
    api.ts                 # 统一出口:get/post,300ms 延迟,按 tenant 隔离
  components/              # 8 个基础组件
  pages/
    Login.vue  Dashboard.vue  NewOrder.vue  OrderDetail.vue
    OrderList.vue  Customers.vue  CustomerDetail.vue  Reminders.vue  Settings.vue
  styles/tokens.css

7.2 组件清单(只做这 8 个)

组件用途关键要求
QxButton主/次/危险三种高度 44px,禁用态可见
QxInput文本/数字/车牌聚焦有 2px 描边,错误有红框 + 提示
QxCard内容容器圆角 12,边框 1px,内边距 16
QxCell列表项左标题右值,可带箭头
QxTag工单状态四种状态四种色,浅底深字
QxEmpty空状态一句人话 + 一个行动按钮
QxSkeleton骨架屏至少列表和卡片两种
QxToast操作反馈1.8 秒自动消失,不遮挡底部 Tab

8 给 Agent 的开发任务

分五个迭代,每个迭代跑通再进下一个。每次交付都要给一个能打开的链接,不许只给代码。

迭代做什么完成标准(DoD)
I1 骨架Vite 工程、路由、Token CSS、8 个组件、Tab 布局8 个页面都能点开,全是空状态,不像半成品
I2 开单主链路接车开单 → 工单详情 → 结算,全走 mock能从输车牌一路走到"已完成",刷新后数据还在
I3 客户与提醒客户列表/档案、到期提醒、工作台取数结算一单后,客户累计消费和提醒日期自动更新
I4 多租户tenantId 路由、命名空间隔离、切换器 (已完成)切到另一个租户看不到上一家的任何数据
I5 打磨骨架屏、toast、空状态文案、手机适配、P1 小图390px 下无横向滚动,3 分钟演示一遍不断
I6 接真接口默认接第 11 节的真 API,mock 降级为离线兜底;报价链接与车主确认 (已完成)断网/401/409 都有提示,不再落回空白页
I7 平板适配按第 12 节做 834 与 1194 两档:列表 + 详情双栏、左侧导航固定 (已完成)iPad 横屏不出现"手机放大版"
I8 配件与领料配件库、入库盘点、工单内领料与退料、成本进毛利 (已完成)领 1 桶机油,库存 -1、该单成本 +220、结算毛利跟着少 220
I9 权限与大屏成本/毛利按角色服务端脱敏;老板营业大屏(挂墙)(已完成)前台登录看不到任何成本;大屏 1440 与 iPad 都一屏放得下
I10 员工提成提成规则表、结算自动计提、按月汇总与明细、技师只看自己 (已完成)结算一单,提成页立刻多一条;改比例不动历史
I11 客户看板面向车主的进度大屏:繁忙度、空闲工位、三列进度,服务端脱敏(车牌后四位、无金额无姓名) (已完成)前台/技师/老板三个账号打开看到的是同一份脱敏数据
I12 预约到店按天预约表、到店开单、过期提醒;工时(分钟)在价目表里维护,是全系统时间估算的唯一来源 (已完成)预约 2 个项目,开出来的工单项目和金额一模一样

9 验收清单

十二条,逐条对,不允许"基本完成"。

#验收项通过标准
13 分钟演示从工作台到提醒,一次走完不卡壳、不报错
2手机可用390×844 下无横向滚动,按钮不重叠
3开单速度熟手 30 秒内完成一单
4数据持久化刷新、关浏览器再开,数据还在
5租户隔离切租户后工作台、客户、工单全部换掉
6状态机待施工→施工中→待结算→待交车→已完成。前四步由 status 走,待交车是「已收款 + 未交车」推出来的:已交车未收款仍算待结算,已收款未交车才算待交车
7金额一致性明细合计 = 工单总额 = 结算金额
8空状态每个列表都有,文案是人话
9加载态有骨架屏,不是白屏转圈
10操作反馈每个写操作都有 toast
11价格快照改价目表后,历史工单金额不变
12无死链点不到"开发中"空白页
13越权拦截用 t1 的令牌请求 /t/t2/... 返回 403,页面提示"无权访问"
14状态机跳过"施工中"直接完工返回 409,页面给出人话提示
15金额口径接口只传「分」,页面显示前才除以 100,全程不出现 380.0000001
16平板可用1024×768 与 1194×834 下双栏不挤压、表格不横向滚动

10 假数据规格

种子数据决定 demo 像不像真的。三个租户,一个是他演示用的"主店",另两个用来演示隔离。

租户门店数据量用途
T1鑫达汽修(宝安店)客户 24、车辆 28、工单 46(含今日 8)、提醒 37主演示
T2鑫达汽修(龙华店)客户 11、车辆 12、工单 15演示"多店数据分开"
T3顺发快修客户 5、车辆 5、工单 6演示"换一家店是另一套数据"

工单要有真实感:项目名用行业说法(小保养、四轮定位、更换前刹车片、空调清洗、变速箱油), 金额落在 180–2,800 之间,不能全是整数;车牌用粤 B/粤 S 开头;客户名用"张先生/李女士"这类称呼。 今日 8 台车里,要包含 1 台事故车、1 台只做洗车、1 台待结算超 3 天。

一句话验收这个 demo:把手机递给老板,你一句话不说, 他能不能自己点开一台车、看到"这车 8,200 公里没保养了"。能,就成立;不能,就重做工作台。

11 API 契约(已实现)

这一版不是"先 mock、后补接口"。后端已经写完并在线跑着,前端照这份契约对接,后面不用重做。 写 mock 只是为了让你在没有网络时也能点,数据结构必须和真接口一模一样。

在线地址:https://ssa.agent2.0755xzb.club/api/v1  人读文档 /api/v1/docs  机器可读 /api/v1/openapi.json  健康检查 /api/v1/health

11.1 通用约定

项约定为什么
信封{"ok":true,"data":…} / {"ok":false,"error":{"code","message"}}前端只写一处解包和一处报错,页面里不出现 if (res.code === 200)
金额一律「分」,字段名带 _cents0.1+0.2 的坑永远不踩;显示时再除 100
鉴权Authorization: Bearer <token>,token 7 天有效token 绑员工,员工绑租户
租户路径 /api/v1/t/{tenantId}/…,且必须与 token 所属租户一致前端传什么都不算数,越权直接 403
分页?page=1&size=20 → {items,total,page,size}列表页统一,不用每个接口各写一套
时间2026-11-10 14:32:05(本地时间)老板只看本地时间,不出现 UTC
错误码VALIDATION 400 · UNAUTHORIZED 401 · TENANT_FORBIDDEN 403 · NOT_FOUND 404 · STATE_CONFLICT 409 · RATE_LIMIT 429前端按 code 映射中文提示,不直接弹 message

11.2 演示账号(种子数据已在库里)

门店tenantId手机号角色
鑫达汽修(宝安店)t113800000001 / 02 / 03前台 / 技师长 / 技师
鑫达汽修(龙华店)t213800000004 / 05 / 06同上
顺发快修(虎门)t313800000007 / 08 / 09同上
陈老板(三家店)t1 t2 t313800000000老板,可切换门店

验证码在演示环境填任意 6 位数字即可。t1 里有 24 个客户、28 台车、46 张工单(含今天 8 张)、37 条到期提醒, 车牌从 粤B12000 起,可直接用来演。

11.3 接口清单

方法路径(省略 /api/v1)说明
POST/auth/login登录换 token
POST/auth/logout注销,token 立即失效
GET/me当前员工 + 所属租户
GET/t/{tid}/dashboard今日进场、待结算、30 天提醒、7 日曲线、低库存
GET/t/{tid}/settings门店信息 + 员工
PUT/t/{tid}/settings改门店名/电话/地址/工位数
GET/t/{tid}/items价目表
POST/t/{tid}/items新增项目
PATCH/t/{tid}/items/{id}改价(不影响历史工单)
GET/t/{tid}/customers?q=&page=客户列表,按姓名/手机搜
GET/t/{tid}/customers/{id}客户档案 + 车辆 + 近 20 单
GET/t/{tid}/vehicles/lookup?plate=演示的关键接口:输车牌带出客户、上次保养、上次项目
GET/t/{tid}/orders?status=工单列表,按状态筛
POST/t/{tid}/orders接车开单(无此牌自动建客户+车辆)
GET/t/{tid}/orders/{no}工单详情(含明细、照片、报价确认时间)
POST/t/{tid}/orders/{no}/quote生成报价分享 token(发微信用)
POST/t/{tid}/orders/{no}/confirm客户确认报价(演示用模拟点击)
PATCH/t/{tid}/orders/{no}/status状态流转,不能跳步
POST/t/{tid}/orders/{no}/settle结算,自动更新客户累计并生成下次保养提醒
GET/t/{tid}/reminders?month=到期提醒
POST/t/{tid}/reminders/{id}标记已联系 / 已到店
GET/q/{quote_token}车主看的报价页(公开,无需登录),手机上直接打开
POST/q/{quote_token}/confirm车主点"确认这份报价",回写到工单
GET/t/{tid}/parts?q=&low=1配件库(含低库存筛选、库存成本合计)
POST/t/{tid}/parts新增配件
PATCH/t/{tid}/parts/{id}改价 / 改安全库存
POST/t/{tid}/parts/{id}/stock入库 / 盘点 / 报损(写流水)
POST/t/{tid}/orders/{no}/requisition技师领料:扣库存 + 计入该单成本,库存不足返回 409
POST/t/{tid}/orders/{no}/returns退料:库存加回、成本冲减
GET/t/{tid}/screen老板大屏:营业额/进场/待结算/客单价/毛利/7 天趋势/最近工单
GET/t/{tid}/commission?month=提成汇总(老板看全员,技师只返回自己)
GET/t/{tid}/commission/items?month=&staff_id=提成明细(含计提基数与比例)
PUT/t/{tid}/commission/rules改提成规则(仅老板,403 拦非老板)
GET/t/{tid}/board客户看板:在店台数、空闲工位、繁忙度、三列车辆(车牌脱敏,无金额、无车主全名)
POST/t/{tid}/orders/{no}/deliver交车确认:写 delivered_at;重复交车 409,未完工 409
GET/t/{tid}/appointments?from=&to=预约列表(按天/区间),含项目、工时合计、状态
POST/t/{tid}/appointments新建预约,项目工时自动加总成预计时长
PATCH/t/{tid}/appointments/{id}改时间 / 取消(status=cancelled)/ 标记未到店
POST/t/{tid}/appointments/{id}/checkin到店开单:预约转工单,幂等(重复点返回同一张单)
PUT/t/{tid}/items/{id}改价目表:售价/成本/工时(分钟)/工时费,仅老板
GET/pub/{tid}/info公开:门店信息、价目表、当前繁忙度(客户 H5 用)
POST/pub/{tid}/book公开:客户自助预约(限频 6 次/小时/IP,同车牌不重复建)
GET/pub/{tid}/progress?plate=&tail=公开:车牌 + 手机后四位查进度,只返回状态与时间,无金额
POST/t/{tid}/import/customers导入客户:CSV 文本进,返回新建/更新/跳过明细;技师 403

11.4 三个关键调用(下面是真实返回)

① 登录

POST /api/v1/auth/login
{"phone":"13800000001","code":"123456"}

{"ok":true,"data":{"token":"d3e0b41d…","expires_at":"2026-11-17 11:46:51",
 "user":{"id":1,"name":"前台小周","role":"front"},
 "tenants":[{"id":"t1","name":"鑫达汽修(宝安店)","city":"深圳"}]}}

② 输车牌带出(演示的"哇"时刻)

GET /api/v1/t/t1/vehicles/lookup?plate=粤B12000

{"ok":true,"data":{
 "found":true,
 "vehicle":{"plate":"粤B12000","brand":"大众","model":"朗逸","mileage":20000,
            "last_service_at":"2026-05-23 05:47:30","next_due_at":"2026-11-19"},
 "customer":{"id":1,"name":"张伟","phone":"13710000000","level":"普通",
             "total_cents":4000,"visits":3},
 "last_order":{"no":"T00001","status_text":"待施工","total_cents":56000},
 "last_items":["小保养(全合成机油+机滤)","四轮定位"]}}

这一条回来,前台不用问一句"您贵姓",页面直接显示"张先生,上次 5 月 23 日保养,已经 6 个月了"。 整个 demo 的说服力,一半在这个接口上。

③ 结算(会同时写客户累计和下次提醒)

POST /api/v1/t/t1/orders/261110009/settle
{"discount_cents":2000,"pay_method":"微信"}

{"ok":true,"data":{"no":"261110009","total_cents":56000,"discount_cents":2000,
 "paid_cents":54000,"pay_method":"微信",
 "customer_total_cents":54000,"customer_visits":1,"next_due_at":"2027-05-09"}}

返回里的 next_due_at 就是"下次保养提醒"的日期,前端直接显示在结算成功页上。 一单结算 = 客户档案 + 累计消费 + 未来提醒,三件事一次做完,这是老板最能感知的价值。

11.5 多租户怎么验(当场就能证明)

老板最常问的一句是"我这店的数据别人看不看得到"。不用解释,敲给他看:

# 用 t1 前台的令牌,去访问 t2 的数据
curl -H "Authorization: Bearer <t1的token>" \
     https://ssa.agent2.0755xzb.club/api/v1/t/t2/dashboard

{"ok":false,"error":{"code":"TENANT_FORBIDDEN","message":"无权访问该租户的数据"}}   # 403

越权是服务端拦的,不是前端藏起来的。这句话比任何"数据安全"承诺都硬。

11.6 前端怎么接(不要绕过这一层)

文件职责硬性要求
src/api/http.tsfetch 封装:拼 baseURL、带 token、解信封、抛 AppError全站只有一个地方发请求,组件里不出现 fetch
src/api/index.ts按资源分方法:api.orders.create()、api.vehicles.lookup()方法签名即契约,mock 和真接口共用
src/api/mock.ts同样的方法签名,返回同样的结构,数据来自 fixtures开关 VITE_USE_MOCK,一行切换,页面代码不动

错误码到界面的映射(写死在 http.ts 里,别散落在页面):

code界面表现
VALIDATION对应输入框标红 + 行内提示,不弹窗
UNAUTHORIZED清 token,回登录页,提示"登录已过期"
TENANT_FORBIDDEN整页空状态:"这个门店不属于当前账号"
STATE_CONFLICTtoast 显示后端那句中文(已经是人话),并刷新当前工单
网络失败顶部条"网络断了,点这里重试",不清空已填内容
给 agent 的一条死规定:页面里不许出现写死的假数据数组。要么走 api.*, 要么走 mock.ts 里同样签名的方法。这条不守,等你换真接口时等于重写一遍。

12 手机与平板布局

你说得对,这一版前端要兼顾手机和平板——因为前台用手机,老板看店用平板, 店里墙上挂一台 iPad 比给老板装个 App 更现实。但两端的版式不是"放大缩小",是两套信息密度。

12.1 三档版式

手机 390 · 单列 + 底部 Tab 平板 834 · 列表 + 详情双栏 平板 1194 · 导航 + 列表 + 详情
宽度版式导航典型页面怎么摆
<600(手机竖屏)单列,一屏一件事底部 4 Tab列表点进去整页替换,左上角出现返回键
600–899(平板竖屏)两栏:列表 38% + 详情 62%底部 Tab 保留,或左侧窄栏左点单右看单,不用来回跳
900–1199(平板横屏)三栏:导航 200 + 列表 320 + 详情自适应左侧固定竖排导航工作台放 4 张卡一行;工单列表与详情并排
≥1200(门店大屏/笔记本)同三栏,内容最大宽 1440 居中左侧固定表格显示更多列,行高压缩到 44

工单与客户两个模块已按这张表实现:834 起左列表右详情(点击不跳页,选中行左侧高亮), 手机端退回单列并在标题栏给一个返回键。

12.2 手机端(390 宽)必须守的六条

  1. 底部安全区:Tab 与提交栏加 padding-bottom: env(safe-area-inset-bottom),iPhone 上不被小横条压住。
  2. 输入不缩放:输入框字号 ≥16px,否则 iOS 聚焦时页面自己放大,老板会以为页面坏了。
  3. 键盘遮挡:聚焦输入框时 scrollIntoView({block:'center'}),别让金额输入框藏在键盘后面。
  4. 点得到:所有可点区域 ≥44×44,一行里不放两个小按钮。
  5. 单手:主操作放右下或底部,左上角只放返回。
  6. 不横滚:宽表格在容器里 overflow-x:auto,页面本身绝不能横向滚动。

12.3 平板端(834 / 1194)必须守的四条

  1. 不要拉伸:卡片和表单单行最宽 720,否则 iPad 横屏上一行字横跨 1000px,眼睛找不到下一行。
  2. 列表点选,不跳页:右栏原地换内容,选中行左侧加 3px 主色竖条——这是平板和手机最大的体验差别。
  3. 横竖屏都要能用:834×1194 与 1194×834 都测;横屏时双栏,竖屏时也双栏(不要退回单列)。
  4. 触控与鼠标共用:hover 效果只在 @media (hover:hover) 里生效,否则平板上点完状态就卡住。

12.4 演示设备清单

设备尺寸演示什么
你自己的手机390×844主链路:接车 → 报价 → 报工 → 结算
老板的手机360–430 宽让他自己点一下"我的车",比你说十句有用
iPad(或安卓平板)834×1194 / 1194×834挂在墙上:今天的车、待结算、到期提醒
店里的旧笔记本1280×800前台开单,键盘输车牌比手机快
一个提醒:不要为了演示专门做个"大屏看板"页面。老板要的是"我今天挣了多少", 不是一块炫的屏。工作台在平板上能看清就够,多做的每一页都在拖慢交付。

本文档 V1.0,与需求说明书(35 条 FR)、开发设计文档、界面规范、AI 能力方案配套使用; 本阶段只取其中 P0 子集。文档编号 QX-MVP-2026-001。

← 需求说明书(完整 35 条) 界面规范 →

QX Garage · MVP 方案 V1.0 · 2026-11-10