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。
测试入口 POST /auth/test-phone 使用三个独立测试号:企业 13012345678、司机 13012345679、代理 13012345680,固定验证码均为 66666。返回与微信登录相同的 Login 结构。按用户明确要求,MINI_TEST_PHONE_LOGIN=true 时即可登录,不再限制客户端 IP,也不要求 APP_ENV=local。MINI_TEST_PHONE_ALLOWED_IPS 已移除,不再生效。GET /config 的 capabilities.test_phone_login 控制前端测试登录。正式短信登录单独校验,此测试接口不会验证手机号所有权或关联同号微信账号。
测试登录仍保留来源 IP 限流、全局限流、指定号码/验证码校验、账号停用校验和现有业务授权。开启后,知道测试凭据的调用方可从任意来源访问对应测试账号;关闭开关并重载后端配置后,既有测试会话也失效。
微信对凭据类问题返回 HTTP 200 并在 body 中带 errcode,此前只判断 openid 是否存在,导致所有失败都返回同一句 40102 且日志无记录。现在按 errcode 分流,并把 errcode 记入 mini_api.wechat_login 日志,只记录错误码本身,不记录 session_key、unionid 或 errmsg。
| errcode | 含义 | 返回 |
|---|---|---|
| 40029 / 40163 | code 无效或已被使用 | 40102 凭据已失效 |
| 40226 / 45009 / 45011 | 频率限制或配额超限 | 42901 稍后重试 |
| 40013 / 40125 | AppID 或凭据配置无效 | 50301 联系管理员 |
| 其他 | 未识别的凭据错误 | 40102 凭据无效 |
未复用既有的 40102、42901、50301,未新增错误码。
先执行 php -d extension=mbstring scripts/seed_mini_test_phone.php --apply 初始化三个独立的 local-test-phone 账号及对应测试档案权限,同时停用原企业测试号上的司机和代理授权。脚本复跑不覆盖账号/授权停用状态,不创建业务收款、库存或订单。会话仍随机生成并只保存哈希,沿用账号停用、权限校验、限流与退出撤销。关闭开关并重新加载后端配置后,既有测试会话不能继续访问。
tests/mini_test_phone_test.php 在事务回滚中覆盖固定凭据、production环境开启/关闭、账号停用、独立会话与注销;直接调用 HTTP 控制器,验证三个号码在局域网、公网、IPv6 和可信代理转发来源下均返回200并生成可校验的会话。前端运行、微信开发者工具导入目录和 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 负责将格式错误转换为接口校验响应。历史异常号码统一掩码,不回退明文。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/send-sms、POST /auth/phone-login,以及由测试开关控制的 POST /auth/test-phone。Authorization: Bearer <access_token>。后台 JWT 与小程序令牌不能混用。Content-Type: application/json,顶层必须是 JSON 对象;命令不把 URL 查询参数混入 JSON。Y-m-d H:i:s。page 默认 1,page_size 默认 20、最大 100;返回 items,total,page,page_size,has_more。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。
.env 中 MINI_WECHAT_APPID、MINI_WECHAT_SECRET,重载 Webman。禁止将 Secret 放在前端。wx.login 获取临时 code,提交 POST /auth/wechat:{"code":"微信临时code"}。access_token,token_type,expires_at,user_id,identities。首次用户没有任何业务身份,需要后台明确绑定;不通过姓名、手机号或客户端传入企业 ID 自动认领。超级管理员使用后台令牌调用 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 可操作,业务身份不会自动获得后台权限。身份管理使用后台响应格式。
GET /me 读取当前账号的业务身份;一个小程序账号只能有一个有效身份,登录后直接进入对应页面。服务器每次重新校验资源归属。GET /session 查看登录主体,POST /auth/logout 退出。令牌默认 7 天有效,数据库只存 SHA-256 摘要。退出、过期、账号停用均拒绝访问。登录限流以真实调用方地址计算:单 IP 60 次/分钟,全机 600 次/分钟。文件锁在单机多进程间共享;多机部署前改为共享限流设施。
调用方地址由 app/miniapi/http/ClientIp.php 解析。仅当直连对端地址列在 MINI_TRUSTED_PROXIES(默认 127.0.0.1,::1)中时才解析 X-Forwarded-For,并从右向左跳过受信任的代理跳;否则直接使用 TCP 对端地址,调用方自带的该请求头被完全忽略。该地址用于登录限流,不再用于限制测试登录来源。部署在 nginx 或负载均衡之后时,须把每一跳代理地址写入 MINI_TRUSTED_PROXIES 并重载 Webman,才能按实际调用方计算限流。
2026-10-24 按用户要求恢复测试账号与正式号码并存:三个指定测试号直接输入5位固定码 66666,调用 /auth/test-phone,页面不发送短信、不假报发送成功;其余号码调用 /auth/send-sms 和 /auth/phone-login,使用6位随机短信码。手机号入口在 capabilities.sms_login 或 capabilities.test_phone_login 任一开启时显示,每种方式分别校验自己的能力开关,测试关闭不会回退到短信登录。发送成功才启动60秒重发倒计时,短信有效期为120秒;失败提示实际错误并允许重试。获取验证码不要求先同意协议,登录时仍要求同意。下方 123456 仅为请求格式示例,实际必须填写本次短信中的随机验证码。
短信登录是微信登录的第二种凭据,不是独立注册通道:手机号必须先由超级管理员在后台绑定到已存在的小程序账号,才会真正下发短信。校验通过后登录到该账号,user_id、业务身份与权限和同一账号的微信登录完全一致。不会因为发短信而新建账号,未绑定的号码登录后仍无业务身份,与「1 个账号 1 个身份」的规则一致。
配置 .env 后重载 Webman,并以 GET /config 的 capabilities.sms_login 为准:
MINI_SMS_LOGIN = true
MINI_SMS_USER = '短信宝用户号'
MINI_SMS_PASSWORD = '短信宝密码' # 32 位十六进制时短信宝按 MD5 处理,原样透传
MINI_SMS_ENDPOINT = 'https://api.smsbao.com/sms'
MINI_SMS_CODE_PEPPER = '' # 可选,留空回退用 MINI_SMS_PASSWORD
迁移 scripts/migrate_water_mini_sms.php 新增 sa_water_mini_sms_code(验证码)与 sa_water_mini_user.phone(唯一索引,仅登录绑定用),均为追加式、可重复执行,不改任何已有账号、授权或流水。MYSQL 唯一索引不约束 NULL,因此未绑定手机号的存量账号保持有效。
后台由超级管理员绑定,列表与返回值一律打码:
POST /core/water/mini-user-phone
{"user_id":12,"phone":"13800138000"}
一个手机号只能绑定一个账号,重复绑定返回 40903。解绑同样通过该接口传入目标账号与空手机号前需先停用;本版本未提供专用解绑动作。
接口
POST /auth/send-sms {"phone":"13800138000"} → {"sent":true,"expires_in":120}POST /auth/phone-login {"phone":"13800138000","code":"123456"} → access_token,token_type,expires_at,user_id,identities防枚举:已绑定与未绑定的手机号返回完全一致的成功响应,未绑定时不发送短信也不落库。否则攻击者可以用这个接口枚举出哪些手机号注册过本系统。
验证码:6 位、random_int 生成、2 分钟(120秒)过期、只存 HMAC-SHA256 摘要(密钥为 MINI_SMS_CODE_PEPPER,留空时用 MINI_SMS_PASSWORD)。明文只出现在发往用户的那一条短信里,既不写库也不写日志。6 位码空间很小,加盐哈希是必需的——裸 SHA-256 可被离线穷举。单次有效,消费即作废;重发时作废该号码此前的验证码,校验只认最近一次下发的记录,最新码已使用或过期不会回退验证旧码。旧策略发出的5分钟验证码也受创建后120秒上限约束。并发提交靠行锁串行,同一个码不会被用两次。连续错误 5 次直接作废该验证码。
限流(判定全部发生在发短信之前,被限流时不会有短信产生):同号 60 秒内不可重发、单号每日 10 条、单 IP 每小时 30 条,外加与其他登录入口共用的 LoginThrottle。短信宝自身还有「间隔太短」限制,返回码统一转 50302,不透传给前端。
失败处理:短信下发失败时立即删除该验证码,未送达的码不会被猜中或事后使用。SmsCode::consume() 在事务内决定结果、在事务提交后才抛异常,否则一次输错会把刚记上的失败次数一起回滚,锁定逻辑就失效了。
以下 {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 任务详情中提供;完成后不再提供完整拨号号码。
网页入口: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 scripts/migrate_water_mini_sms.php --apply
php -d extension=mbstring tests/mini_sms_login_test.php
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 scripts/migrate_water_mini_sms.php --apply
php -d extension=mbstring tests/mini_sms_login_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子集。
已实现首购前绑定、规则快照、首笔购水资格、签收后后台审核及赠水权益账本,默认关闭。奖励为水权益而非现金;新增接口及使用限制见 WATER_PROMOTION.md。