# 供水小程序前端接入文档 版本:2026-09-18 / v1。供企业端、司机端、代理端共用。UI 对照 M01–M18;字段与路径以配套 [OpenAPI 文件](openapi/water-mini-v1.json) 为准,可直接导入 Apifox、Swagger Editor 或 Postman。 ## 网页版接口文档 本机访问:。支持分类、筛选、请求/响应 Schema、示例、Bearer Token 授权和在线调试;点击「下载 OpenAPI」可导入 Apifox。 页面默认调用当前域名的 `/api/mini/v1`,无需修改页面里的地址。先展开 `GET /health`,点击 **Try it out → Execute** 验证连接。需要身份的接口,先登录取得 `access_token`,在 **Authorize** 中仅填写 token(不含 Bearer 前缀);刷新页面后授权信息清除。在线写请求会执行真实业务,请使用测试身份和实际测试资源。 网页中只有已登记到 OpenAPI 的接口;手机号验证码登录仍为设计方案,尚未实现。微信登录和支付等实际能力由页面读取 `/config` 展示。 文档资源在项目内,不依赖外部 CDN,不调用第三方 OpenAPI 校验服务。部署到其他域名时,设置 `MINI_ALLOWED_ORIGINS` 为精确的页面来源,例如 `https://api.example.com`,多个来源以逗号分隔(不带路径及末尾斜杠),并让 Webman 重新加载配置。默认只允许 `http://127.0.0.1:8787` 和 `http://localhost:8787`;原业务接口仍须正常鉴权。 ## 1. 联调准备 - 本机基础地址:`http://127.0.0.1:8787/api/mini/v1`。真机不能用此回环地址访问开发电脑;使用实际可访问的 HTTPS 域名,并配置小程序 request / downloadFile 合法域名。 - 先调用 `GET /config`。`capabilities.wechat_login=false` 表示后端尚未配置微信凭据;支付等开关为 false 时隐藏或禁用入口。 - 后端配置微信 AppID、Secret 后,小程序 `wx.login` 获取 code,再调用 `POST /auth/wechat`。Secret 不给前端、不放入请求。 - 用户首次登录 `identities=[]`,需要后台绑定真实企业、司机或代理身份。把返回的 `user_id` 交给后台管理员操作;前端不能自行指定 openid、用户 ID 或角色登录。 - 本地暂无可分发的测试 token 或默认特权账号。业务测试中创建的临时用户会清理,不能拿测试代码中的 ID 当联调账号。 - 浏览器/H5 联调带 `Origin` 时,后端通过 `MINI_ALLOWED_ORIGINS` 配置精确来源并加载配置;原生小程序请求与后台 JWT 登录互不替代。 Apifox 导入:新建项目 → 导入数据 → OpenAPI/Swagger → 选择 `docs/openapi/water-mini-v1.json`。切换环境的基础 URL,使用登录响应中的 `access_token` 设置 Bearer Token。文档内的 ID、金额、版本号是示例,须换成实际查询结果。 ## 2. 公共请求和响应 受保护接口使用 `Authorization: Bearer `。POST 业务参数放在 `application/json` 对象中;不要混入 URL 查询参数。服务端拒绝不支持的业务字段。 ```json { "code": 0, "message": "ok", "data": {"示意": "具体数据结构见各接口"}, "request_id": "0123456789abcdef0123456789abcdef" } ``` 判断成功须同时满足 HTTP 2xx 和 `code===0`。照片成功是 JPEG 字节、HEAD 没有响应体,不能套用 JSON 解析。 ```json { "code": 42201, "message": "请填写完整有效的联系电话", "data": {"fields": {"phone": "须为7至15位数字,可含前置+、空格、横线和成对括号"}}, "request_id": "0123456789abcdef0123456789abcdef" } ``` 约定:金额是人民币整数分;桶数是整数;`user_id` 是字符串,业务资源 ID 是正整数。时间是上海时间 `YYYY-MM-DD HH:mm:ss`,日期是 `YYYY-MM-DD`,不要将无时区的时间字符串直接当 UTC。历史司机接口的坐标可能是数字字符串,判断非 null 后再转 `Number`。 列表通用查询 `page=1&page_size=20`,`page_size` 最大 100,`page` 最大 1000000,返回: ```json {"items":[],"total":0,"page":1,"page_size":20,"has_more":false} ``` `null` 表示未知或未开放,显示“—”或未开通;`0` 才表示已知为零。不要把 `null` 的佣金、用量、预计到达时间转为 0。`items=[]` 是正常空状态。 ### 登录请求封装示例(原生微信小程序) ```js const BASE_URL = 'https://替换为你的域名/api/mini/v1'; function api(path, { method = 'GET', data, authenticated = true } = {}) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('mini_access_token'); wx.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', ...(authenticated && token ? { Authorization: `Bearer ${token}` } : {}) }, success(response) { const body = response.data; if (response.statusCode >= 200 && response.statusCode < 300 && body.code === 0) { resolve(body.data); return; } if (response.statusCode === 401) wx.removeStorageSync('mini_access_token'); reject({ status: response.statusCode, ...body }); }, fail(error) { reject({ networkError: true, cause: error }); } }); }); } async function signIn() { const { code } = await new Promise((resolve, reject) => { wx.login({ success: resolve, fail: reject }); }); const session = await api('/auth/wechat', { method: 'POST', authenticated: false, data: { code } }); wx.setStorageSync('mini_access_token', session.access_token); return session; // 根据 identities 进入身份选择或等待授权页 } ``` 401 后重新 `wx.login`,不要无限重试旧 code。退出调用 `POST /auth/logout` 后清除本地 token 和当前身份。多身份用户选择 `identities` 中的 `resource_id` 构建路径;选择不等于授权,服务器每次重新校验。 ### 幂等与重试 修改地址、创建意向、取消、配送预约和司机动作必须带 `request_key`:8–80 位字母、数字、`_`、`-`。前端生成 UUID 或等价唯一键,并在发请求前保存**键和完整请求体**。 - 双击、超时、断网后的同一次重试,使用同一 key 和完全相同内容,返回原操作结果并附 `replayed:true`。 - 修改内容或发起新动作生成新 key;同一用户跨操作也不能复用 key。 - 网络错误不代表服务端未处理;保留原请求重试,不能立即换 key 重发。 - 重放返回的是原结果快照。要展示最新任务/订单状态,再调详情;不要假定每个命令结果都有 `status`。 - 权限撤销后旧 key 也不能继续操作。`40901` 不应盲目重试;先区分版本冲突或键冲突。 ## 3. 接口目录 下表路径均相对于基础地址;`{enterprise}`、`{driver}`、`{agent}` 来自身份列表,`{id}` 是对应对象的 ID。完整请求/响应字段、类型、可空性和示例见 OpenAPI 的 Schemas。 ### 公共与用户 | 方法 | 路径 | 参数 / 主要 data | 权限 | | --- | --- | --- | --- | | GET / HEAD | `/health` | Health;HEAD 无响应体 | 公开 | | GET | `/config` | Config:客服、协议、能力开关 | 公开 | | POST | `/auth/wechat` | `{code}` → Login:token、过期时间、用户与身份 | 公开 | | GET | `/packages` | 分页、keyword≤120、category 仅 all → PackagePage | 公开 | | GET | `/packages/{id}` | Package | 公开 | | GET / HEAD | `/session` | `{user_id}`;HEAD 无响应体 | 登录 | | POST | `/auth/logout` | 无必填请求体 → `{logged_out:true}` | 登录 | | GET | `/me` | 可选 enterprise_id → Profile;选企业后增加 enterprise、rights | 登录 | | GET | `/addresses` | Addresses:可访问企业的脱敏地址 | 登录 | ### 企业 前缀 `/enterprises/{enterprise}`。管理员和成员均可读普通企业接口;**完整地址详情、所有写接口和报价只允许企业管理员**。 | 方法 | 后缀 | 参数 / 主要 data | | --- | --- | --- | | GET | `/home` | Home:企业、轮播、套餐、权益、设备 | | GET | `/rights` | Rights:items、summary、policy | | GET | `/ledger` | 分页 → LedgerPage | | GET | `/orders` | 分页、kind、status → EnterpriseOrderPage | | GET | `/devices` | 分页 → DevicePage | | GET | `/devices/{id}` | DeviceDetail:七日用量、权益 | | GET | `/rewards` | Rewards:真实邀请及赠水记录,enabled由后台活动开关决定 | | GET | `/deliveries/{id}` | Tracking:配送信息、events、photos | | GET | `/address` | EditableAddress:完整 phone、revision | | POST | `/address` | AddressRequest → `{id,revision}` | | POST | `/deliveries` | BookingRequest → `{id}` | | POST | `/deliveries/{id}/cancel` | `{request_key}` → CommandResult | | POST | `/checkout/quote` | `{items:[{package_id,quantity}]}` → Quote | | POST | `/checkouts` | CheckoutRequest → Checkout | | GET | `/checkouts/{id}` | CheckoutDetail:含脱敏地址快照 | | POST | `/checkouts/{id}/cancel` | `{request_key}` → Checkout | | POST | `/checkouts/{id}/pay` | `payment_method=balance` 扣减企业余额并生成水订单/权益;`wechat` 暂返回 50303 | `orders.kind` 默认 `deliveries`;`purchases` 是真实收款记录,`checkouts` 是待支付意向,三种 ID 不可混用。`status` 仅用于 deliveries:`all/pending/transit/completed/cancelled/exception`。这里 pending 筛选包含 pending 和 assigned,exception 包含 returning 和 returned;每条记录仍返回真实状态。 ### 司机 前缀 `/drivers/{driver}`。要求该司机身份;具体任务必须分配给本人。 | 方法 | 后缀 | 参数 / 主要 data | | --- | --- | --- | | GET | `/dashboard` | DriverDashboard:班次、数量、待出发任务、路线 | | GET | `/tasks` | 分页;status=all/pending/transit/completed/exception → TaskPage | | GET | `/routes` | Route 数组,最多50条未结算路线;每条含 stops,不分页 | | GET | `/tasks/{id}` | TaskDetail:联系人、记录、照片、坐标 | | POST | `/tasks/{id}/start` | `{request_key}` → CommandResult | | POST | `/tasks/{id}/checkin` | `{request_key,latitude,longitude}` → CheckinResult | | POST | `/tasks/{id}/photos` | PhotoRequest → PhotoResult,保存返回的 id | | POST | `/tasks/{id}/draft` | DraftRequest → `{id,saved:true}` | | POST | `/tasks/{id}/complete` | CompleteRequest → CommandResult | | POST | `/tasks/{id}/exception` | `{request_key,reason}` → ExceptionResult | | GET | `/tasks/{id}/device-check` | 必填 sn≤80 → DeviceCheck | 司机列表 pending 仅指 assigned;exception 为 returning/returned。只有 assigned/transit/returning 的本人任务详情返回完整 `contact_phone`,其他状态为 null,拨号按钮按此判断。 ### 代理与照片 | 方法 | 路径 | 参数 / 主要 data | | --- | --- | --- | | GET | `/agents/{agent}/dashboard` | AgentDashboard:本月/上月收款、客户、七日趋势 | | GET | `/agents/{agent}/customers` | 分页、keyword≤120、filter=all/new/unpaid → CustomerPage | | GET | `/agents/{agent}/orders` | 分页、可选 enterprise_id → AgentOrderPage | | GET | `/agents/{agent}/commissions` | Commissions:新成交订单的真实计提明细与待结算金额;仅直接归属代理按后台成交时比例领取(默认一级10%、二级5%、三级2%),提现关闭 | | POST | `/agents/{agent}/withdrawals` | 校验身份后返回50303,不创建申请 | | GET | `/photos/{id}` | scope=enterprise/driver、resource_id;JPEG字节 | 代理数据包含自身及有效下级企业,按**企业当前归属**统计;历史收款会随归属变化,不可用作佣金结算凭证。`new` 是本月新增,`unpaid` 是截至当前无收款。客户列表的 summary 不随关键词、筛选、分页变化;amount_cents / has_purchased / last_purchase_at 排除未来日期的收款。 ## 4. 页面调用顺序与字段语义 ### 套餐、购物车、下单(M02/M03/M05/M06) 1. `/packages` 维护本地购物车,只传 `package_id` 与整数 `quantity`。每种1–100份、最多20种、相同套餐合并为一行。 2. 企业管理员调用 `/checkout/quote` 展示服务器报价。套餐详情 `unit_price_cents` 是**每桶**参考价,报价明细中的同名字段是**每份套餐**价;总金额始终用服务器的 `amount_cents`。 3. 确认后 `/checkouts` 提交下例。金额变化返回40904,应重新报价让用户确认。 ```json { "request_key": "checkout-20260918-0001", "items": [{"package_id": 3, "quantity": 2}], "expected_amount_cents": 139800, "agreement_accepted": true, "invoice": {"title": "示例公司", "tax_number": "实际税号", "email": "finance@example.com"} } ``` `invoice` 可不传或为 null;传入需 title≤120、tax_number≤30、有效 email≤120,仅存申请资料。返回 pending 意向,有效30分钟,查询到期显示 expired。余额支付成功后写入余额流水、收款记录和权益;微信支付仍未开通,调用 `payment_method=wechat` 返回 50303。余额不足返回 40905,并在 `data.fields` 返回当前余额和所需金额。 ### 地址、权益、预约(M04/M08) 地址列表用于展示;编辑先 `GET /enterprises/{enterprise}/address`,用完整 phone 填表,提交原 revision: ```json { "request_key": "address-20260918-0001", "revision": "替换为查询返回的64位revision", "address": "完整供水地址", "contact": "张先生", "phone": "+86 138 1234 5678" } ``` address≤255、contact≤60、phone≤30字符;规范化后7–15位数字,可含前置+、空格、短横线、成对括号。纯符号、过短、掩码号码会422。保存去除格式符号;普通列表统一脱敏,无法识别的历史格式显示 `****`。格式正确不代表已短信验证。 40901表示资料版本变化,重新取详情后让用户核对;40902表示存在未结束配送,完成或取消后再编辑。修改地址会清空旧坐标,后台需重新定位。当前一企业一个供水地址,没有添加多站点或设置默认地址功能。 权益 `remaining` 包含已冻结数量;`available=remaining-frozen`;`full_qty` 是现场满桶,不是可下单金额。预约: ```json {"request_key":"booking-20260918-0001","sku_id":2,"quantity":8} ``` 预约1–10000桶,实际还受可用库存/权益限制。同一企业同一SKU只能有一个有效任务。预约冻结权益,签收按实际收货扣除。客户端不传预约时段、日期、扣哪张卡;这些能力目前不支持。仅 pending 未派单任务可自助取消,assigned 后联系后台。 ### 司机配送与照片(M12–M15) ```text assigned → start → transit → checkin → photos → draft(可选)→ complete ↘ exception(只记录异常) ``` 打卡用 `wx.getLocation({type:'gcj02'})`,latitude/longitude 必须是 JSON 数值。客户端坐标不等于到场验证;`location_verified=false`。 上传是 JSON base64,不是 multipart:选择或压缩 JPEG/PNG 后读文件为纯 base64,无 `data:` 前缀;原文件≤2MiB、最长边≤4096、总像素≤800万。每任务最多20张,签收选1–4张,不重复。普通请求≤65536字节;照片请求≤3000000字节。 ```json { "request_key": "complete-20260918-0001", "accepted_qty": 8, "empty_returned": 0, "receiver": "张先生", "reason": "", "photo_ids": [12], "confirmed": true } ``` accepted_qty 为0至计划桶数;empty_returned 可省略,默认0;receiver 必填≤60字符。少收/拒收时 reason 必填≤255字符;否则可省略。complete 必须由当前用户先打卡,confirmed 为布尔 true;不是字符串。全部接受进入 completed,有剩余货物进入 returning,由后台完成退货。draft 不要求 confirmed 或照片,不扣权益、不完成任务。重复 key 可能返回旧快照,提交后刷新详情。 照片响应中的 id 用于读取: ```js wx.downloadFile({ url: `${BASE_URL}/photos/${photoId}?scope=driver&resource_id=${driverId}`, header: { Authorization: `Bearer ${wx.getStorageSync('mini_access_token')}` }, success(result) { if (result.statusCode === 200) { // result.tempFilePath 可用于 image / wx.previewImage } else { // 非200可能是JSON错误体;不要把错误文件作为图片展示 } } }); ``` 企业查看用 `scope=enterprise&resource_id=企业ID`。不要把 token 拼进 URL,也不要直接用无鉴权的远程 ``。退出登录后清理页面中缓存的私有照片路径。 ## 5. 错误处理 | HTTP / code | 前端处理 | | --- | --- | | 400 / 40001 | 检查JSON格式与对象根结构 | | 401 / 40101 | 清除失效token,重新登录 | | 401 / 40102 | 重新获取微信code,勿重用旧code | | 403 / 40301 | 身份无权限、资源停用或浏览器来源不允许;不反复登录重试 | | 403 / 40302 | 登录账号停用,联系管理员 | | 404 / 40401 | 资源不存在、已下架或任务不属本人,刷新列表 | | 405 / 40501 | health/session请求方法错误 | | 409 / 40901 | 地址版本或幂等键冲突,刷新并核对内容 | | 409 / 40902 | 业务状态/库存/权益冲突,展示message并刷新 | | 409 / 40903 | 代理层级异常,联系管理员 | | 409 / 40904 | 价格改变,重新报价与确认 | | 413 / 41301 | 请求过大,缩小照片等内容 | | 415 / 41501 | 使用application/json | | 422 / 42201 | 展示message;存在data.fields时标记对应表单字段 | | 429 / 42901 | 登录频率过高,稍后重试,禁止紧密循环 | | 500 / 50001 | 提供request_id给后端排障;写操作保留原key | | 503 / 50301 | 微信登录尚未配置 | | 503 / 50302 | 外部服务暂不可用,稍后重新获取code登录 | | 503 / 50303 | 功能未开通;不能提示支付或提现成功 | ## 6. 当前可联调边界 可对接套餐、企业权益/流水/地址/预约、配送记录、设备登记及已上报用量、司机履约、代理客户与真实收款统计。真实微信登录需后端配置凭据并绑定测试身份后才能完成端到端联调。 尚未实现:微信支付、首购优惠、真实开票、佣金结算及提现、现金推广奖励、独立水卡有效期、多站点、真实设备绑定/联网协议、实时司机GPS、自动送水与未读消息。新成交购水订单已按成交时直接归属代理等级计提,当前全部待结算;企业余额支付已实现,企业余额需要由后台充值或调整后才能扣款;前端按能力开关处理。 后台身份绑定说明、架构与维护命令见 [WATER_MINI_API.md](WATER_MINI_API.md)。后台 `/core/water/mini-users`、`/core/water/mini-grants` 使用后台管理员认证,不属于本 OpenAPI,也不能由小程序自行调用授予身份。 推广赠水第一版已接入:页面支持邀请码生成、复制、微信分享与首购前手动绑定;历史记录分页,金额字段保持null,奖励用桶数展示。分享参数不会自动绑定企业。详见 [WATER_PROMOTION.md](WATER_PROMOTION.md)。