# 供水小程序接口 UI 依据:`D:\水后台UI\mobile\M01.png` 至 `M18.png`。后端接口持续更新,小程序前端见 `water-mini/README.md`。 前端交接使用 [WATER_MINI_FRONTEND.md](WATER_MINI_FRONTEND.md) 和 [OpenAPI 3.0.3 定义](openapi/water-mini-v1.json)。本文件保留架构、后台绑定和维护说明;规则整改及验证记录见 [WATER_MINI_REVIEW_FIXES.md](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 已获准。正式短信登录已单独接入,此测试接口不会验证手机号所有权或关联同号微信账号。 该来源白名单与登录限流使用同一个解析后的调用方地址,因此本机默认的 `127.0.0.1,::1` 不会因为前面挂了回环代理而对所有来源放行;实际放行仍取决于调用方解析出的真实地址。 ## 2026-10-21 微信登录失败分流 微信对凭据类问题返回 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` 在事务回滚中覆盖固定凭据、非本机来源、生产环境、关闭开关、账号停用、独立会话与注销。本机实际 HTTP 已验证登录后读取三个身份及无权限资源被拒绝。前端运行、微信开发者工具导入目录和 API 地址配置见 `water-mini/README.md`。 ## 分层与设计模式 ```text 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/send-sms`、`POST /auth/phone-login`,以及仅在本地测试配置下可用的 `POST /auth/test-phone`。 - 其他接口:`Authorization: Bearer `。后台 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`。撤权后不能继续重放。原结果是当次操作快照,最新状态通过详情接口获取。 - 通用返回: ```json {"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`: ```json {"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 可操作,业务身份不会自动获得后台权限。身份管理使用后台响应格式。 5. `GET /me` 读取当前账号的业务身份;一个小程序账号只能有一个有效身份,登录后直接进入对应页面。服务器每次重新校验资源归属。 6. `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 对端地址,调用方自带的该请求头被完全忽略。因此未配置该项时行为与只信任 TCP 地址一致,但反向代理后的所有调用方会被视为同一来源,登录限流与测试手机号来源白名单都会按聚合流量计算。部署在 nginx 或负载均衡之后时,须把每一跳代理地址写入 `MINI_TRUSTED_PROXIES` 并重载 Webman。 ## 2026-10-21 短信验证码登录 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` 为准: ```dotenv 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,因此未绑定手机号的存量账号保持有效。 后台由超级管理员绑定,列表与返回值一律打码: ```http 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()` 在事务内决定结果、在事务提交后才抛异常,否则一次输错会把刚记上的失败次数一起回滚,锁定逻辑就失效了。 ## 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`:真实邀请与赠水记录;管理员可生成邀请码并在首购前绑定,见 [推广赠水](WATER_PROMOTION.md) | | 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`: ```json {"items":[{"package_id":3,"quantity":2}]} ``` 返回每项服务器价格及 `subtotal_cents,discount_cents,amount_cents`。同一套餐不能重复提交,不接受客户端 `price_cents`。未配置折扣,`discount_cents` 为 0。 `POST /enterprises/5/checkouts`: ```json { "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`,修改时提交: ```json {"request_key":"address-20260918-001","revision":"查询返回的64位版本值","address":"完整配送地址","contact":"张先生","phone":"13812345678"} ``` 普通地址列表/成员查询手机号脱敏;不接受把掩码号码保存为联系电话。电话规范化后须为7–15位数字,支持前置+、空格、短横线和成对括号;保存去掉格式符号,格式校验不代表归属或可接通性认证。地址版本变化返回 40901;有未完成配送(含并发提交的预约)禁止修改。修改地址清除原坐标,需要后台重新定位。 `POST /enterprises/5/deliveries`: ```json {"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` 共用字段: ```json {"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。 - 代理经营统计按企业**当前归属**计算,过去订单会随企业归属变化显示到新的代理范围;佣金账本保存成交时的代理和比例快照,不随客户转移改变。 - 照片当前存于专用表,保证与事务一起回滚和私密读取。大规模使用前应迁移到私有对象存储,明确保留期;会话、幂等记录和审计的归档清理策略需随生产保留规则补充。 ## 网页文档与发布 网页入口:,由 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 目录执行: ```powershell 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_` 前缀的用户、会话、授权、命令、审计、下单意向、任务记录、照片八张表,不重建原业务表,不初始化任何特权身份。 ```powershell 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子集。 ## 2026-10-19 企业邀请赠水 已实现首购前绑定、规则快照、首笔购水资格、签收后后台审核及赠水权益账本,默认关闭。奖励为水权益而非现金;新增接口及使用限制见 [WATER_PROMOTION.md](WATER_PROMOTION.md)。