WATER_MINI_API.md 19 KB

供水小程序接口

UI 依据:D:\水后台UI\mobile\M01.png 至 M18.png。后端接口持续更新,小程序前端见 water-mini/README.md。

前端交接使用 WATER_MINI_FRONTEND.md 和 OpenAPI 3.0.3 定义。本文件保留架构、后台绑定和维护说明;规则整改及验证记录见 WATER_MINI_REVIEW_FIXES.md。

2026-10-13 本机测试手机号入口

本地测试入口 POST /auth/test-phone 使用三个独立测试号:企业 13012345678、司机 13012345679、代理 13012345680,固定验证码均为 66666。返回与微信登录相同的 Login 结构。仅 APP_ENV=local 且 MINI_TEST_PHONE_LOGIN=true 时启用,默认只接受 127.0.0.1 和 ::1 来源。局域网设备调试时,可在 MINI_TEST_PHONE_ALLOWED_IPS 中显式添加设备 IP(逗号分隔),并重载 Webman。GET /config 的 capabilities.test_phone_login 控制前端入口;该能力标志不表示当前客户端 IP 已获准。正式短信登录尚未接入,此接口不会验证手机号所有权或关联同号微信账号。

先执行 php -d extension=mbstring scripts/seed_mini_test_phone.php --apply 初始化三个独立的 local-test-phone 账号及对应测试档案权限,同时停用原企业测试号上的司机和代理授权。脚本复跑不覆盖账号/授权停用状态,不创建业务收款、库存或订单。会话仍随机生成并只保存哈希,沿用账号停用、权限校验、限流与退出撤销。关闭开关并重新加载后端配置后,既有测试会话不能继续访问。

tests/mini_test_phone_test.php 在事务回滚中覆盖固定凭据、非本机来源、生产环境、关闭开关、账号停用、独立会话与注销。本机实际 HTTP 已验证登录后读取三个身份及无权限资源被拒绝。前端运行、微信开发者工具导入目录和 API 地址配置见 water-mini/README.md。

分层与设计模式

routes/mini_api.php
    → ApiBoundary / Authenticate(请求边界、登录校验)
    → controller/*Controller(输入解析、调用服务、HTTP 输出)
    → service/*(资源权限、业务编排、事务)
    → app/miniapi/model/* 与 plugin/saiadmin/app/model/water/*(Eloquent)
  • 控制器按认证、套餐、企业、订单、司机、代理、照片、系统拆分;BaseController 统一当前身份、ID 校验、成功响应。控制器不查询数据库,不保存请求身份到常驻对象属性。
  • IdentityProvider 是外部登录适配器接口;WechatIdentityProvider 实现微信 code 换身份。AuthService 通过构造参数依赖接口,可以注入测试适配器;HTTP 不允许指定适配器或伪造 openid。
  • 司机动作采用命令模式:TaskAction 定义契约,TaskActionFactory 选择 StartDelivery、CheckIn、ReportException、SaveFulfillment。新增动作增加独立处理器、工厂及显式路由映射,避免继续堆长分支。
  • InTransitAction 使用模板方法,统一验证“配送中”状态,再执行具体动作。暂存和签收共用 SaveFulfillment 的数量、照片验证,只有签收结算。
  • Commands::run 统一执行事务、用户行锁、当前权限复核、请求幂等、结果保存和审计;所有小程序业务写操作复用它。司机任务另外遵循既有 SKU→司机→任务锁顺序。
  • Access 集中处理企业成员/管理员、司机任务归属、代理子树范围;WaterPresenter 集中处理共享返回字段、手机号脱敏和地址版本。Input、Fields、Pagination 统一入参规则。
  • PhoneNumber 集中规范化和脱敏国内/国际联系电话;Fields::phone 负责将格式错误转换为接口校验响应。历史异常号码统一掩码,不回退明文。
  • 地址变更先锁企业,再使用模型锁定查询检查有效配送,避免 MySQL REPEATABLE READ 授权快照漏掉并发预约。代理客户列表通过 orders/devices 模型关联聚合,避免每行额外查询金额、最后收款、购买状态和设备数。
  • 数据查询和写入使用 Eloquent 模型及关联;复用原 WaterBusiness、WaterDispatch、WaterFulfillment。不在控制器写 SQL,不额外叠加只有转发作用的 Repository。
  • tests/mini_api_architecture_test.php 检查控制器不查库、服务不依赖 HTTP、查询不绕过模型和默认路由关闭。

公共契约

  • 基础路径:/api/mini/v1,本机为 http://127.0.0.1:8787/api/mini/v1。
  • 公开接口:GET /health、GET /config、GET /packages、GET /packages/{id}、POST /auth/wechat,以及仅在本地测试配置下可用的 POST /auth/test-phone。
  • 其他接口:Authorization: Bearer <access_token>。后台 JWT 与小程序令牌不能混用。
  • POST 使用 Content-Type: application/json,顶层必须是 JSON 对象;命令不把 URL 查询参数混入 JSON。
  • 金额均为人民币整数分;数量为桶;业务时间为 Asia/Shanghai,Y-m-d H:i:s。
  • 列表分页:page 默认 1,page_size 默认 20、最大 100;返回 items,total,page,page_size,has_more。
  • 普通 JSON 请求最多 65,536 字节;照片上传接口最多 3,000,000 字节。
  • 幂等写接口都需要 request_key:8–80 位字母、数字、下划线、短横线;同一用户重试使用同一 key 和同一内容。不同操作/内容不得复用 key;重试返回原结果并增加 replayed:true。撤权后不能继续重放。原结果是当次操作快照,最新状态通过详情接口获取。
  • 通用返回:

    {"code":0,"message":"ok","data":{},"request_id":"服务器生成的32位请求编号"}
    

业务失败同时使用对应 HTTP 状态码。常用错误:40001 JSON 错误,40101 未登录/令牌失效,40102 微信凭据无效,40301 无业务权限,40401 不存在,40901 幂等冲突/地址版本冲突,40902 业务状态冲突,40904 价格变化,41301 内容过大,41501 非 JSON,42201 参数错误,42901 登录限流,50301 登录未配置,50302 外部服务暂不可用,50303 功能未开通。校验详情位于 data.fields。

照片成功响应直接返回 JPEG 字节,并包含 Cache-Control:no-store;错误仍返回上述 JSON。

登录与身份绑定

  1. 设置 .env 中 MINI_WECHAT_APPID、MINI_WECHAT_SECRET,重载 Webman。禁止将 Secret 放在前端。
  2. 小程序 wx.login 获取临时 code,提交 POST /auth/wechat:{"code":"微信临时code"}。
  3. 返回 access_token,token_type,expires_at,user_id,identities。首次用户没有任何业务身份,需要后台明确绑定;不通过姓名、手机号或客户端传入企业 ID 自动认领。
  4. 超级管理员使用后台令牌调用 GET /core/water/mini-users?user_id=用户ID,以及 POST /core/water/mini-grants:

    {"user_id":12,"role":"enterprise_admin","resource_id":5,"status":1}
    

resource_id 是真实企业/司机/代理档案 ID。角色为 enterprise_admin、enterprise_member、driver、agent;status:0 撤销授权,status:1 生效。只有启用的超级管理员 ID 1 可操作,业务身份不会自动获得后台权限。身份管理使用后台响应格式。

  1. GET /me 读取当前账号的业务身份;一个小程序账号只能有一个有效身份,登录后直接进入对应页面。服务器每次重新校验资源归属。
  2. GET /session 查看登录主体,POST /auth/logout 退出。令牌默认 7 天有效,数据库只存 SHA-256 摘要。退出、过期、账号停用均拒绝访问。

登录限流以 TCP 来源地址计算,不信任任意 X-Forwarded-For:单 IP 60 次/分钟,全机 600 次/分钟。文件锁在单机多进程间共享;多机部署前改为共享限流设施。反向代理部署时需根据同一代理地址聚合的实际流量配置限额。

UI 对应接口

以下 {e} 为 enterprise、{d} 为 driver、{a} 为 agent,均是数字资源 ID。

UI 接口与当前能力
M01 首页 GET /enterprises/{e}/home:按真实购买状态、区域、管理员身份筛选内容,返回权益、设备、套餐
M02 套餐 GET /packages?keyword=&category=all、GET /packages/{id};只展示启用套餐和 SKU,购物车由客户端维护
M03 合并下单 POST /enterprises/{e}/checkout/quote、POST /enterprises/{e}/checkouts;服务器计价、保存待支付意向和发票申请资料
M04 地址 GET /addresses;管理员 GET/POST /enterprises/{e}/address;目前一企业一个供水地址
M05 支付 GET /enterprises/{e}/checkouts/{id};POST .../{id}/pay 支持 payment_method=balance 企业余额扣款,微信支付保留接口并返回 50303
M06 订单 GET /enterprises/{e}/orders?kind=purchases\|deliveries\|checkouts;POST .../checkouts/{id}/cancel 取消意向
M07 配送 GET /enterprises/{e}/deliveries/{id}:真实状态、事件、履约照片;无实时 GPS/承诺 ETA
M08 权益 GET /enterprises/{e}/rights、GET .../ledger、POST .../deliveries、POST .../deliveries/{id}/cancel
M09 设备 GET /enterprises/{e}/devices、GET .../devices/{id}:登记信息、最近心跳和已上报用量,缺失数据为 null
M10 邀请奖励 GET /enterprises/{e}/rewards:真实邀请与赠水记录;管理员可生成邀请码并在首购前绑定,见 推广赠水
M11 我的 GET /me?enterprise_id={e}、GET /config:身份、企业权益、客服与协议配置
M12 司机工作台 GET /drivers/{d}/dashboard、GET .../tasks?status=、GET .../routes;只展示分配给本司机的任务/路线
M13 配送任务 GET /drivers/{d}/tasks/{id},POST .../{id}/start、POST .../{id}/checkin
M14 扫码设备 GET /drivers/{d}/tasks/{id}/device-check?sn=:只核对已绑定当前企业的 SN;没有冒充绑定或联网检测成功
M15 履约 POST /drivers/{d}/tasks/{id}/photos、POST .../{id}/draft、POST .../{id}/complete、POST .../{id}/exception
M16 代理首页 GET /agents/{a}/dashboard:自身及有效下级企业的真实收款额、客户数、7 日趋势
M17 客户 GET /agents/{a}/customers?filter=all\|new\|unpaid&keyword=、GET .../orders?enterprise_id=
M18 佣金 GET /agents/{a}/commissions 返回真实计提明细;仅成交时客户直接归属代理按后台当时比例领取(默认一级10%、二级5%、三级2%),均待结算;POST .../withdrawals 仍返回 50303

企业 orders 配送筛选 status=all|pending|transit|completed|cancelled|exception;pending 包括待调度和已派单。司机任务筛选 status=all|pending|transit|completed|exception;pending 仅为已经分配的待出发任务。

主要写接口示例

报价与待支付意向

POST /enterprises/5/checkout/quote:

{"items":[{"package_id":3,"quantity":2}]}

返回每项服务器价格及 subtotal_cents,discount_cents,amount_cents。同一套餐不能重复提交,不接受客户端 price_cents。未配置折扣,discount_cents 为 0。

POST /enterprises/5/checkouts:

{
  "request_key":"checkout-20260918-001",
  "items":[{"package_id":3,"quantity":2}],
  "expected_amount_cents":139800,
  "agreement_accepted":true,
  "invoice":{"title":"示例公司","tax_number":"真实税号","email":"finance@example.com"}
}

示例金额需替换为报价结果。invoice 可省略;当前仅保存资料,不代表发票已开具。意向 30 分钟后展示 expired;没有支付渠道时不会写入原收款表,也不会发放权益。

地址与配送预约

管理员通过 GET /enterprises/5/address 获取完整联系电话和 revision,修改时提交:

{"request_key":"address-20260918-001","revision":"查询返回的64位版本值","address":"完整配送地址","contact":"张先生","phone":"13812345678"}

普通地址列表/成员查询手机号脱敏;不接受把掩码号码保存为联系电话。电话规范化后须为7–15位数字,支持前置+、空格、短横线和成对括号;保存去掉格式符号,格式校验不代表归属或可接通性认证。地址版本变化返回 40901;有未完成配送(含并发提交的预约)禁止修改。修改地址清除原坐标,需要后台重新定位。

POST /enterprises/5/deliveries:

{"request_key":"booking-20260918-001","sku_id":2,"quantity":8}

预约使用既有权益和库存,事务冻结数量;待调度且未派单时可 POST /enterprises/5/deliveries/{id}/cancel,仅传 request_key。已派单的取消由后台处理。当前不接收 UI 的预约时段,调度日期/时限沿用后台调度。

司机履约

所有路径均是 /drivers/{d}/tasks/{id} 下的操作。

  • /start:{"request_key":"depart-20260918-001"},使用原出库流程。
  • /checkin:{"request_key":"checkin-20260918-001","latitude":22.522806,"longitude":113.92414}。
  • /photos:{"request_key":"photo-20260918-001","image_base64":"纯base64,不带data前缀","latitude":22.522806,"longitude":113.92414}。
  • /exception:{"request_key":"exception-20260918-001","reason":"联系不上客户"},记录异常,不假定配送完成或自动取消。

照片限 JPEG/PNG,原文件 ≤2MiB,最长边 ≤4096、总像素 ≤800万;服务器重新编码为 JPEG,叠加任务号、服务器时间、上报坐标。每任务最多存 20 张;签收选择 1–4 张。

/draft 和 /complete 共用字段:

{"request_key":"fulfill-20260918-001","accepted_qty":8,"empty_returned":0,"receiver":"张先生","reason":"","photo_ids":[12],"confirmed":true}

暂存不扣权益、不完成配送;签收必须先由当前登录用户打卡、确认数量并选择本任务照片。部分拒收必须说明原因,剩余货物沿用现有退货流程。客户端上报坐标不等于到场认证,返回 location_verified:false。这是司机记录的收件人和确认,不冒充客户电子签名。

GET /photos/{photo_id}?scope=enterprise&resource_id={e} 或 scope=driver&resource_id={d} 携带 Bearer 读取照片;未授权不能获取字节。司机的 contact_phone 仅在其负责的 assigned/transit/returning 任务详情中提供;完成后不再提供完整拨号号码。

与 UI 仍有差异的业务边界

  • 原权益是企业+SKU 汇总账户,不是每笔购买独立水卡;没有 UI 中“多张卡的各自有效期/优先扣卡”。不从原汇总余额伪造分卡历史。
  • 原企业模型只有一个地址,没有企业内多站点、默认站点切换、自助新建企业。
  • 微信支付、首购优惠、佣金结算/退款扣回/提现、现金邀请奖励、真实开票尚未实现。新成交购水订单按成交时直接归属代理等级和后台当前比例计提(默认一级10%、二级5%、三级2%);旧订单不补记,修改比例不重算旧订单,待结算佣金不可提现。企业余额支付已接入;企业余额正式充值和退款仍需业务规则。
  • 设备真实联网/绑定协议、实时司机 GPS、自动送水、消息未读数没有实际来源时保持关闭或 null。
  • 代理经营统计按企业当前归属计算,过去订单会随企业归属变化显示到新的代理范围;佣金账本保存成交时的代理和比例快照,不随客户转移改变。
  • 照片当前存于专用表,保证与事务一起回滚和私密读取。大规模使用前应迁移到私有对象存储,明确保留期;会话、幂等记录和审计的归档清理策略需随生产保留规则补充。

网页文档与发布

网页入口:http://127.0.0.1:8787/docs/mini/index.html,由 Webman 静态文件服务提供。public/docs/mini/ 包含 Swagger UI 5.33.0、本地样式、初始化脚本和发布后的 openapi.json;上游来源和许可见 public/docs/mini/vendor/UPSTREAM.md。页面不使用外部 CDN/在线校验器,不持久化 Token,在线调试仅调用页面当前域名的 /api/mini/v1/。

接口定义的唯一维护源仍是 docs/openapi/water-mini-v1.json。修改接口文档后在 server 目录执行:

php scripts/build_mini_docs.php
php scripts/build_mini_docs.php --check
php -d extension=mbstring tests/mini_api_contract_test.php

构建只同步该 JSON 文件,不连接数据库、不启动服务。--check 在发布文件与源文件不一致时返回非零状态。发布时需要包含 public/docs/mini 整个目录。

浏览器来源通过 MINI_ALLOWED_ORIGINS 精确匹配,默认允许本机的 http://127.0.0.1:8787 和 http://localhost:8787。部署时覆盖为实际 HTTPS 来源;空值表示不允许浏览器 Origin,原生无 Origin 请求不受此项影响。不使用通配符,不改变 Token 或业务权限校验。静态文档公开可读,包含接口结构及示例,不包含用户会话或凭据。正式短信手机号登录仍在设计中;网页文档只包含受本地测试配置保护的测试手机号接口。

配置、迁移和检查

迁移 scripts/migrate_water_mini_api.php 仅增加 sa_water_mini_ 前缀的用户、会话、授权、命令、审计、下单意向、任务记录、照片八张表,不重建原业务表,不初始化任何特权身份。

php -d extension=mbstring scripts/migrate_water_mini_api.php --apply
php tests/mini_api_architecture_test.php
php -d extension=mbstring tests/water_mini_api_test.php
php -d extension=mbstring tests/mini_api_address_concurrency_test.php
php -d extension=mbstring tests/mini_api_contract_test.php
php -d extension=mbstring tests/mini_api_framework_test.php --http
php -d extension=mbstring tests/water_mini_http_test.php
php runtime/tools/php-cs-fixer.phar fix --config=.php-cs-fixer.mini.php

格式化器使用本次下载的 PHP CS Fixer 3.88.2;不加入运行时 Composer 依赖。配置 .php-cs-fixer.mini.php 限制到小程序接口和相关文件,避免改动整个旧项目。CI/新环境需安装该版本工具后执行 --dry-run --diff。

本机八张表已迁移。业务测试事务回滚;HTTP 测试只创建临时无授权用户并在 finally 清除。真实微信登录和微信支付尚未做外部联调,当前 /config 报告微信登录未配置;企业余额支付有独立的本地业务测试。修改配置/代码后确保 Webman 加载新版本;Windows 以实际主管进程/项目重载为准,不能用重复启动来替代重载。

并发回归测试会提交独立测试企业、SKU库存、权益、用户授权,供第二条数据库连接读取;两条连接在企业锁之前确定性交错执行真实预约和地址服务,最后按夹具ID清理。这不是同时运行线程的压力测试;测试验证旧快照可复现且新锁定读能拒绝地址变更。契约测试校验实际路由/鉴权、引用、路径参数和示例;业务测试另外校验实际返回数据的字段类型和可空性,校验器范围仅为本项目使用的Schema子集。

2026-10-19 企业邀请赠水

已实现首购前绑定、规则快照、首笔购水资格、签收后后台审核及赠水权益账本,默认关闭。奖励为水权益而非现金;新增接口及使用限制见 WATER_PROMOTION.md。