# 酒店二维码模块开发文档 > **本文档职责**:**二维码模块的实现权威**——回答「二维码怎么建、怎么绑、怎么扫、接口长什么样」。具体包括:4 张表全字段、管理端 + 17 个开放接口全清单、二维码 URL 格式与签名、双端识别与两层拦截、上线硬门槛、菜单权限资源 ID、Maven 依赖与配置项。 > > **不在本文档维护**(避免双份失真;四份酒店文档均在 `forge-server/forge-business/forge-hotel/` **同一目录**): > - 需求验收对照、P0/P1 缺口台账、第 0~6 批补齐顺序 → `酒店模块需求缺口清单.md` > - 支付协议、`ALIPAY_MP` 渠道、Mock 资金红线修复代码 → `酒店订单支付系统开发文档.md`(2026-09-07 已从仓库根目录移入本目录) > - 逐类代码地图、jeeplus→Forge 适配速查、表↔Flyway 版本映射 → `酒店模块迁移跟踪.md` > > 本文档只写二维码相关的实现结论;引用其他三份时一律用**指针**(章节号),不复制正文。 ## 1. 模块概述 **模块名称**: `forge-hotel` **包路径**: `com.mdframe.forge.business.core.hotel` **位置**: `forge-server/forge-business/forge-hotel/` ### 1.1 功能范围 | 功能 | 说明 | |------|------| | 二维码批量生成 | 批量生成不绑定房间的二维码,支持 10/50/100/200 个一批 | | 二维码绑定/重绑/解绑 | 员工通过 PC 后台扫码绑定房间,记录操作人和时间 | | 二维码状态管理 | 启用/禁用二维码(字典 sys_normal_disable: 0=启用, 1=禁用) | | 绑定日志追溯 | 完整记录每次绑定/重绑/解绑操作 | | 批量删除 | 勾选多个二维码批量逻辑删除,已绑定房间的拒绝删除 | | 批量下载ZIP | 勾选多个二维码生成 PNG 图片打包为 ZIP 下载 | | 单条下载 | 操作列下载单个二维码 PNG 图片 | | 房间管理 | 房间 CRUD,关联房型 | | 房型管理 | 房型 CRUD | | 扫码查询(开放接口) | 免登录接口,员工 H5 与顾客小程序**共用** `/hotel/open/scan` | | 在住会话管理 | 入住/退房/打扫完成/维护切换/入住记录分页(`hotel_room_stay`) | ### 1.2 四端架构 > 📌 **「四端」与「五端」的口径区别(勿误判为矛盾)**:本节的**四端 = 接入载体**(技术视角:谁用什么客户端访问);`酒店模块需求缺口清单.md` 一章的**五端 = 需求业务角色**(需求说明书视角:顾客 / 餐厅前台 / 厨房 / 宾馆前台 / 后台管理)。两个五端里的餐厅前台与宾馆前台**当前无独立载体**,职能均由四端中的「PC 管理后台」承担,厨房端尚无任何载体(缺口清单 3.4)。两套口径分属不同文档职责,不相互替换。 ``` PC 管理后台(Sa-Token) → 批量生成、列表、批量删除、批量下载ZIP、单条下载、绑定/重绑/解绑、禁用/启用、在住会话 员工移动端 H5(Sa-Token) → 扫码绑定房间、重绑、解绑、查看绑定状态【不上小程序】 顾客小程序(微信 + 支付宝) → 扫码点餐、下单、支付、订单查询、退单 开放接口(免登录) → /hotel/open/** 共 17 个,支撑上述扫码与顾客全链路 ``` > ⚠️ **员工端不是钉钉 SSO**,而是 Forge 自己的 Sa-Token 登录体系(`scan-bind.vue` 通过 `ensureLogin` + `useAuthStore` 校验登录态)。 **⚠️ 员工 H5 必须 HTTPS**:`qr-scanner.vue` 依赖 `getUserMedia`,浏览器只在安全上下文(HTTPS 或 localhost)下放行摄像头。当前 `hotel.qr.base-url` 配的是 `http://192.168.10.4:3001`,真机扫码拿不到摄像头权限。 **⚠️ `scan-bind.vue` 是双职责页面**(不是纯管理侧): ``` onLoad 取 c/t/s → 调 /hotel/open/scan ├── authStore.isLogin == true → 员工:展示绑定/重绑/解绑界面 └── authStore.isLogin == false → 顾客分流: ├── scanData.bound == true → uni.redirectTo customer/room-confirm └── scanData.bound == false → 提示「二维码未启用」 ``` 小程序化后顾客不再经过此页(由平台规则直接拉起 `customer/room-confirm`),该页与 `qr-scanner.vue` 需用 `#ifdef H5` 排除出小程序构建。 > ✅ **2026-09-07 已落地**:`pages.json` L67-87 已把两页包在同一对 `// #ifdef H5` / `// #endif` 内(`pages.json` **支持** `//` 注释与条件编译,已用 `pnpm build:h5` 产物验证:H5 仍含 `pages-hotel-scan-bind` / `pages-hotel-qr-scanner` 两个 chunk)。 > > ✅ **入口引用已同步**:页面被 `pages.json` 裁掉后**不参与编译**,因此 `.vue` 内部无需再加 `#ifdef`;真正需要保护的是**其它页面对它的入口引用**。`pages/index/index.vue` 内的 5 处(见 5.2)**均已包在 `// #ifdef H5` 内**(已用 `git diff` 核实全部在位)—— 小程序构建时首页不会渲染该入口,也不会 `navigateTo` 到未注册页面;H5 dev 零变化。 --- ## 2. 数据库设计 ### 2.1 表结构 #### hotel_room_type(房型表) | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT | 主键(雪花算法) | | tenant_id | BIGINT | 租户ID,默认1 | | type_name | VARCHAR(64) | 房型名称 | | description | VARCHAR(256) | 房型描述 | | sort_order | INT | 排序号 | | del_flag | BIGINT | 逻辑删除标记 | | create_by/create_time/create_dept | - | 审计字段 | | update_by/update_time | - | 审计字段 | #### hotel_room(房间表) | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT | 主键 | | tenant_id | BIGINT | 租户ID | | room_no | VARCHAR(32) | 房间号(租户内唯一) | | room_type_id | BIGINT | 房型ID | | floor_no | VARCHAR(16) | 楼层 | | status | VARCHAR(16) | 状态:VACANT/OCCUPIED/CLEANING/MAINTENANCE | | sort_order | INT | 排序号 | | remark | VARCHAR(256) | 备注 | | del_flag | BIGINT | 逻辑删除标记 | **唯一索引**: `UNIQUE (tenant_id, room_no, del_flag)` #### hotel_qr_code(二维码表) | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT | 主键 | | tenant_id | BIGINT | 租户ID | | short_code | VARCHAR(32) | 二维码唯一短码(8位大写字母+数字) | | sign | VARCHAR(64) | MD5签名值 | | room_id | BIGINT | 绑定房间ID(NULL=未绑定) | | room_no | VARCHAR(32) | 冗余房间号 | | status | VARCHAR(16) | 状态:0=启用, 1=禁用(对应字典 sys_normal_disable) | | batch_no | VARCHAR(32) | 批次号(格式:QR+yyyyMMddHHmmss) | | bind_user_id | BIGINT | 最近绑定人ID | | bind_user_name | VARCHAR(64) | 最近绑定人姓名 | | bind_time | DATETIME | 最近绑定时间 | | unbind_user_id | BIGINT | 最近解绑人ID | | unbind_user_name | VARCHAR(64) | 最近解绑人姓名 | | unbind_time | DATETIME | 最近解绑时间 | | del_flag | BIGINT | 逻辑删除标记 | **唯一索引**: `UNIQUE (tenant_id, short_code, del_flag)` > `del_flag` 为 `BIGINT`,未删除为 `0`,删除时写入当前行主键(实体 `@TableLogic(value = "0", delval = "id")`),以保证同一短码删除后可重建。 #### hotel_qr_bind_log(绑定日志表) | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT | 主键 | | tenant_id | BIGINT | 租户ID | | qr_code_id | BIGINT | 二维码ID | | short_code | VARCHAR(32) | 冗余短码 | | room_id | BIGINT | 房间ID | | room_no | VARCHAR(32) | 冗余房间号 | | action_type | VARCHAR(16) | 操作类型:BIND/REBIND/UNBIND | | operator_id | BIGINT | 操作人ID | | operator_name | VARCHAR(64) | 操作人姓名 | | operate_time | DATETIME | 操作时间 | | remark | VARCHAR(256) | 备注 | ### 2.2 Flyway 迁移脚本 **文件**: `forge-server/db/migration/V1.0.100__add_hotel_qr_module_tables.sql` > 后续酒店模块迁移脚本已至 `V1.0.112`(菜品/规格/营业时段/订单/支付/在住会话/退款/配置等),完整清单见 `酒店模块迁移跟踪.md`。 包含内容: - 4张业务表建表语句(均使用 `CREATE TABLE IF NOT EXISTS`) - 菜单权限资源(`sys_resource`,`tenant_id=1`,带 `NOT EXISTS` 防重复) - 字典数据(`sys_dict_type` + `sys_dict_data`,`tenant_id=1`) ### 2.3 字典数据 | 字典类型 | 字典值 | 标签 | 标签样式 | |----------|--------|------|----------| | sys_normal_disable | 0 | 正常 | success | | sys_normal_disable | 1 | 停用 | danger | | hotel_room_status | VACANT | 空闲 | success | | hotel_room_status | OCCUPIED | 入住 | primary | | hotel_room_status | CLEANING | 打扫中 | warning | | hotel_room_status | MAINTENANCE | 维护中 | danger | | hotel_qr_bind_action | BIND | 绑定 | success | | hotel_qr_bind_action | REBIND | 重绑 | warning | | hotel_qr_bind_action | UNBIND | 解绑 | danger | --- ## 3. 后端 API 接口 ### 3.1 管理端接口(需 Sa-Token 登录) #### 二维码管理 > ⚠️ **路径风格**:本模块已完成 JDK 7 风格改造,**全面移除 `@PathVariable`**(当前代码 0 处)。详情/绑定/删除等均为固定路径 + `@RequestParam Long id`,**不是** `/:id` 形式;`updateStatus` 是 `POST` 而非 `PUT`。 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/hotel/qrcode/page` | 分页查询二维码 | | GET | `/hotel/qrcode/detail?id=` | 查询二维码详情 | | POST | `/hotel/qrcode/batch-generate` | 批量生成二维码 | | POST | `/hotel/qrcode/bind?id=` | 绑定房间 | | POST | `/hotel/qrcode/rebind?id=` | 重新绑定房间 | | POST | `/hotel/qrcode/unbind?id=` | 解绑房间 | | POST | `/hotel/qrcode/updateStatus?id=&status=` | 更新二维码状态 | | POST | `/hotel/qrcode/batch-remove` | 批量删除二维码(逻辑删除) | | POST | `/hotel/qrcode/batch-download` | 批量下载二维码 ZIP | | GET | `/hotel/qrcode/bindLogs?id=` | 查询绑定日志 | #### 房间管理 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/hotel/room/page` | 分页查询房间 | | GET | `/hotel/room/detail?id=` | 查询房间详情 | | POST | `/hotel/room` | 新增房间 | | PUT | `/hotel/room` | 修改房间 | | POST | `/hotel/room/remove?id=` | 删除房间 | | GET | `/hotel/room/list-all` | 查询全部房间(**员工 H5 扫码绑定依赖此接口**,需登录) | #### 在住会话管理 | 方法 | 路径 | 说明 | |------|------|------| | POST | `/hotel/room/checkIn?id=` | 入住(开新会话,返回 stayId) | | POST | `/hotel/room/checkOut?id=` | 退房(待支付订单自动取消;进行中订单需 `force=true`) | | POST | `/hotel/room/cleaningDone?id=` | 打扫完成 | | POST | `/hotel/room/maintenanceToggle?id=` | 维护状态切换 | | GET | `/hotel/room/stayPage?roomId=` | 入住记录分页 | #### 房型管理 | 方法 | 路径 | 说明 | |------|------|------| | GET | `/hotel/room-type/list` | 查询房型列表 | | GET | `/hotel/room-type/detail?id=` | 查询房型详情 | | POST | `/hotel/room-type` | 新增房型 | | PUT | `/hotel/room-type` | 修改房型 | | POST | `/hotel/room-type/remove?id=` | 删除房型 | ### 3.2 开放接口(免登录,Sa-Token 白名单 `/hotel/open/**`) **共 17 个**: | 方法 | 路径 | 说明 | |------|------|------| | GET | `/hotel/open/scan?shortCode=&sign=` | 扫码查询二维码信息(员工 H5 与顾客小程序共用) | | GET | `/hotel/open/customer/businessHours` | 当前营业时段状态 | | GET | `/hotel/open/customer/pauseStatus` | 暂停营业状态 | | GET | `/hotel/open/customer/categoryEnabled` | 启用菜品分类 | | GET | `/hotel/open/customer/dishPage` | 菜品分页 | | GET | `/hotel/open/customer/dishDetail` | 菜品详情(含规格/加料) | | POST | `/hotel/open/customer/orderCreate` | 创建订单 | | GET | `/hotel/open/customer/orderDetail` | 订单详情 | | GET | `/hotel/open/customer/orderPage` | 订单分页 | | POST | `/hotel/open/customer/customerRefund` | 顾客退单 | | POST | `/hotel/open/customer/abandonOrder` | 放弃待支付订单(物理删除) | | POST | `/hotel/open/pay/create` | 创建支付 | | POST | `/hotel/open/pay/mockSuccess` | ⚠️ **Mock 支付,资金红线,见 3.3** | | GET | `/hotel/open/pay/status` | 支付状态(前端轮询) | | POST | `/hotel/open/pay/notify` | 支付宝异步回调(验签 RSA2) | | GET | `/hotel/open/pay/alipayQuery` | 主动查支付宝侧状态(回调丢失兜底) | | POST | `/hotel/open/alipay-auth/getUserInfo` | 支付宝授权(authCode → 姓名 + 手机号) | ### 3.3 开放接口安全红线 1. ~~**`/hotel/open/pay/mockSuccess` 完全免登录**~~ ✅ **已收口(2026-09-07)**:原风险是任何人不需凭证,只要 POST `orderId` + `tenantId` 就能把订单置为已支付(免付款)。已用 `hotel.pay.mock-enabled` 开关拦截(dev=true / prod=false),拦截点位于 `HotelPayController.mockPaySuccess()` **方法首行**(不放 Service,保证开关语义单点)。⚠️ **本项属资金类变更,代码已先行但 Spec 未补、人工审查未做,AGENTS.md 5.9 合规未闭环**。 2. ~~**`createPay` 的 `paySource` 当前设了 `defaultValue="MOCK"`**~~ ✅ **已收口(2026-09-07)**:已改为 `@RequestParam String paySource` 必传;`resolvePayChannel` 抽出 `mockOrReject(reason)` **单一收口点**,全部非支付宝分支都走它,**全仓不再有裸 `return "MOCK"`**;开关关闭时每个分支抛差异化文案(微信未开通 / 渠道为空 / 模拟已禁用 / 不支持的渠道)。详见 `酒店订单支付系统开发文档.md` **3.4 / 10.1**。 3. **`tenantId` 由前端传入(可伪造)**,`stayId` 归属校验当前缺失(见 `酒店模块需求缺口清单.md` 3.2 R2)。小程序化后必须由平台 `openid`/`user_id` 推导 `stayId`,**禁止前端直传**。❌ **未修**(第 3 批)。 4. ~~**日志禁止打印手机号**(AGENTS.md 5.9)~~ ✅ **已修复(2026-09-07)**:`AlipayAuthController` 新增 `maskPhone()`(保留前 3 后 4,中间 `****`);`AlipayAuthServiceImpl` 不再把支付宝返回的**密文报文整包** `log.warn`(密文可离线解密,等同泄露),改为只记 `responseLength`。 5. 🔴 **新增(本次改造中发现)**:`refundOrder` 原用 `!"ALIPAY".equals(payChannel)` 兜底判 MOCK,**未来接入微信后微信订单退款会被误判为无资金流而直接标记成功 → 真实资损**。已改为显式判定三种无资金流情形,其余非 ALIPAY 抛「请联系前台人工处理」。 6. 🔴 **新增(遗留,不属本次引入)**:两份 `application-dev.yml` 内直接写了支付宝沙箱应用的 `private-key` / `alipay-public-key` 明文,且 `git check-ignore` 证实 **`application-dev.yml` 未被忽略**(AGENTS.md 2.4 把它归为「本地配置」,与实际不符)。虽为沙箱密钥无真实资金风险,但已命中 5.9「禁止硬编码密钥」红线,建议单独提案外提。 **请求参数**: | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | shortCode | String | 是 | 二维码短码 | | sign | String | 是 | 签名校验值 | **响应示例(未绑定)**: ```json { "code": 200, "data": { "qrCodeId": 1234567890, "shortCode": "AB12CD34", "status": "0", "bound": false, "room": null, "bindInfo": null } } ``` **响应示例(已绑定)**: ```json { "code": 200, "data": { "qrCodeId": 1234567890, "shortCode": "AB12CD34", "status": "0", "bound": true, "room": { "roomId": 9876543210, "roomNo": "301", "roomTypeName": "标准双人间" }, "bindInfo": { "operatorName": "张三", "bindTime": "2026-08-10 14:30:00" } } } ``` --- ## 4. 二维码 URL 格式 ### 4.1 当前实现(代码真相,2026-09-07 已配置化) URL 由 `QrCodeUtils.buildQrCodeUrl()` 运行时拼接。**落地路径已从硬编码常量改为配置项 `hotel.qr.path`**: ```java // HotelQrConstants L30 —— 现在仅作**回退默认值**,不再是唯一来源 public static final String QR_BASE_PATH = "/#/pages/hotel/scan-bind"; // HotelQrCodeServiceImpl L79 —— 注意带**空串默认值**,配置缺失不会导致启动崩 @Value("${hotel.qr.path:}") private String qrBasePath; // QrCodeUtils —— 新增四参重载(主实现),原三参方法保留并委派到它(兼容既有调用方) public static String buildQrCodeUrl(String baseUrl, String shortCode, Long tenantId) { return buildQrCodeUrl(baseUrl, HotelQrConstants.QR_BASE_PATH, shortCode, tenantId); } public static String buildQrCodeUrl(String baseUrl, String basePath, String shortCode, Long tenantId) { String path = (basePath == null || basePath.trim().isEmpty()) ? HotelQrConstants.QR_BASE_PATH // 未配置时回退,保证既有二维码 URL 不变 : basePath; return baseUrl + path + "?c=" + shortCode + "&t=" + tenantId + "&s=" + sign; } // HotelQrCodeServiceImpl L283 / L669 —— 两处调用点均已改为四参重载 String qrCodeUrl = QrCodeUtils.buildQrCodeUrl(qrBaseUrl, qrBasePath, entity.getShortCode(), tenantId); ``` **三层回退设计(为何 dev 行为零变化)**: | 层 | 值 | 效果 | |---|---|---| | yml 显式配置 | dev = `"/#/pages/hotel/scan-bind"`、prod = `${FORGE_HOTEL_QR_PATH:/r/}` | 按环境分开,dev 保持旧值 | | yml 未配置 | `@Value` 空串默认值 | 不崩,进下一层 | | 空串回退 | `QrCodeUtils` 内回退到 `QR_BASE_PATH` | URL 与改造前**逐字符相同** | 当前 dev 实际生成的 URL(`hotel.qr.base-url = http://192.168.10.4:3001`)—— **与改造前完全一致**: ``` http://192.168.10.4:3001/#/pages/hotel/scan-bind?c=ABCD1234&t=1&s=xxxx ``` > ⚠️ `hotel.qr.base-url` 的 `@Value` **无默认值**(L69),且改造前**两份生产 `application.yml` 完全没有 `hotel` 块** → 以生产 profile 启动直接崩。已补齐为 `${FORGE_HOTEL_QR_BASE_URL:}`(空串而非缺失,保证能启动;但空串下生成的二维码无域名不可扫,部署时必须传入真实域名)。 | 参数 | 说明 | |------|------| | c | 二维码短码(8位大写字母 + 数字,`QrCodeUtils.generateShortCode()`) | | t | 租户ID | | s | MD5签名值 = MD5(shortCode + tenantId + secret) | **签名密钥配置**: - 默认值: `hotel_qr_default_secret`(`HotelQrConstants.QR_SIGN_SECRET`) - 生产环境: 通过环境变量 `HOTEL_QR_SECRET` 覆盖 ### 4.2 目标格式:两套,必须区分 | 用途 | 格式 | 路径前缀 | 是否在平台配规则 | 当前状态 | |---|---|---|---|---| | **员工 H5**(扫码绑定房间) | `{baseUrl}/h5/#/pages/hotel/scan-bind?c=&t=&s=` | `/h5/` | ❌ **不配** | ⚠️ 配置已就绪(dev `hotel.qr.path` 即此值),但**部署路径尚未拆为 `/h5/`** | | **顾客小程序**(扫码点餐) | `{baseUrl}/r/?c=&t=&s=` | `/r/` | ✅ 微信 + 支付宝各配一条 | ✅ 生产 yml 默认已是 `/r/`;✅ 前端 `resolveScanParams` 已落地(见 4.5);❌ `/r/` 的 403 拦截未做(第 4 批);❌ 平台规则未配(依赖资质) | > ⚠️ **参数名 `c`/`t`/`s` 不得更改**。员工 H5 `qr-scanner.vue` 的 `parseQrUrl` 用正则 `[?&]c=([^&]+)` 解析,`scan-bind.vue` / `room-confirm.vue` 用 `options.c || options.shortCode` 取参。保留 query 形式(而不改成路径参数),已完成的员工扫码绑定功能**零改动**。 ### 4.3 为什么顾客码必须去掉 hash 路由 微信/支付宝的「普通链接二维码」规则是**按 URL 的 path 前缀匹配**,而 hash 路由的 path 恒为 `/`(`#` 后面不属于 path)。后果: - 要让规则命中,只能配成 `https://域名/` - 这会**劫持该域名下的全部页面**,包括员工 H5 的登录页、绑定页 - 员工扫码会被平台拦下来拽进顾客小程序,**已做好的员工绑定功能直接失效** 所以顾客码必须用**专属非 hash 前缀** `/r/`,员工 H5 部署在 `/h5/`(不配规则),两者物理隔离。 4.1 的当前 URL 另有 4 个上线硬伤:`http://` 非 HTTPS、IP 非备案域名、带端口 `:3001`、指向员工页 `scan-bind`。 ### 4.4 URL 不落库(改造零迁移成本) `hotel_qr_code` 表**只存 `short_code` + `sign`**,无 `qr_url` / `qr_content` 字段。 **结论**:改二维码格式 = 改配置项,**无需任何数据迁移、无需重印已生成的码**。但一旦桌牌印刷上墙,再改就要全部重印 —— 格式必须在印刷前定死。 ### 4.5 双端自动识别方案(已定:普通链接二维码) > **方案选型**:自研普通 https URL 二维码,**不调用** `wxacode.getUnlimited`(微信)或 `alipay.open.app.qrcode.create`(支付宝)生成平台小程序码。 **可行机制**:微信和支付宝**各自维护一张独立规则表**,互不知晓对方存在: ``` 二维码内容:https://hotel.xxx.com/r/?c=ABCD1234&t=1&s=xxxx 微信扫一扫 → 查微信规则表 → 命中 /r/ → 拉起微信小程序,URL 作为 options.q 注入 onLoad 支付宝扫一扫 → 查支付宝规则表 → 命中 /r/ → 拉起支付宝小程序,URL 作为 options.qrCode 注入 onLoad 其他扫码器 → 无规则表 → 当普通网页打开 https://hotel.xxx.com/r/ → 服务器返回 403 ``` 一个码、两次配置、零平台 API 调用,双端自动识别由平台机制天然提供。 **前端取参(三端兼容,纯增量函数)** —— ✅ **已于 2026-09-07 落地**:`room-confirm.vue` L185-214。 > ⚠️ 下面是**实际代码**,与早期方案稿有两处实质差异,review 时不要拿旧稿对: > 1. **返回字段是 `{ shortCode, sign }`,不是 `{ c, t, s }`** —— 下游 `loadRoomInfo()` 只用 `shortCode` + `sign` 调 `api.hotelCustomerScan()`,`tenantId` 由**后端根据 shortCode 反查**并通过 `data.tenantId` 回传(L270 `orderStore.setTenantId(data.tenantId)`)。前端自己解 `t` 反而多一个可被篡改的信任面。 > 2. **显式参数优先**:先试 `query.c || query.shortCode` + `query.s || query.sign`,两者齐备就直接返回,**不走正则**。H5 dev 下 `scan-bind.vue` 判定已绑定后直接透传 `c/t/s`,因此开发期路径完全不变。 ```js function resolveScanParams(options) { const query = options || {} const c = query.c || query.shortCode || '' const s = query.s || query.sign || '' if (c && s) { return { shortCode: c, sign: s } // H5 直开路径,dev 零变化 } // 小程序扫普通链接二维码:原始 URL 在 q / qrCode 字段里 const raw = query.q || query.qrCode || '' if (!raw) { return { shortCode: c, sign: s } } let rawUrl = raw try { rawUrl = decodeURIComponent(raw) // 支付宝部分版本已编码,统一尝试 decode } catch (e) { rawUrl = raw // 未编码或含非法转义序列时直接用原值 } // 按 [?&]key=value 提取,与 scan-bind.vue 的 parseQrUrl 正则口径保持一致 const matchedCode = rawUrl.match(/[?&]c=([^&]+)/) const matchedSign = rawUrl.match(/[?&]s=([^&]+)/) return { shortCode: c || (matchedCode ? matchedCode[1] : ''), sign: s || (matchedSign ? matchedSign[1] : ''), } } ``` 调用点(L216-232):`onMounted` 中先 `getCurrentPages()` 取末栈页的 `$page.options || options`(**两种形态都要兼容**,H5 与小程序不同),再 `resolveScanParams(options)`;两个值任一为空则展示「二维码参数缺失,请重新扫描」并终止。 正则对 hash 与非 hash URL 都成立,因此**先改前端解析、后改后端 URL 格式**不会中断开发期 H5 测试 —— 这一设计意图已被实际落地验证(dev 下 `hotel.qr.path` 仍是 hash 旧值,走的是「显式参数优先」分支,正则根本不会执行)。 ### 4.6 排斥非微信/支付宝扫码(两层拦截) | 层 | 做法 | 强度 | |---|---|---| | **入口层** | `/r/` 对所有真实 HTTP 请求返回 403,只放行平台校验用 `.txt` 文件。因为微信/支付宝命中规则时**根本不会向服务器发请求**(客户端直接拉起小程序),所以拦掉全部 HTTP 请求不影响正常用户 | 挡住浏览器、其他 App 扫码 | | **接口层** | `/hotel/open/customer/**` 校验平台签发的身份凭证(`my.getAuthCode` → `user_id`;`wx.login` → `openid`),拿不到 code 的一律 401 | **真正的安全边界** | > ⚠️ 入口层**不构成强制**:截获 URL 参数者仍可直接用 curl 调免登录后端接口。只有接口层的身份校验才是安全边界,且它同时解决 R2 订单归属漏洞。 ### 4.7 上线硬门槛(7 条,缺一不可) | # | 门槛 | 说明 | |---|---|---| | 1 | **非个人主体** | 微信「扫普通链接二维码打开小程序」**仅开放给已认证的非个人主体**,个人主体完全不支持,**无技术手段可绕过** | | 2 | 小程序已认证 | 微信/支付宝双方均要求企业主体认证 | | 3 | 小程序已备案 | 工信部 2023.9 起强制 | | 4 | 域名已 ICP 备案 | 且需在平台后台配置为业务域名 | | 5 | HTTPS 且**不带端口** | `https://域名:8443/` 不被接受 | | 6 | 校验文件可上传 | 平台下发 `.txt`,需能放到 `/r/` 对应目录并公网可访问 | | 7 | 小程序已发布线上版本 | 开发版/体验版不能配规则 | **开发期替代验证**:资质未到位时无法真机扫码(支付宝沙箱 App 的扫一扫**不是通用扫码器**,只认沙箱应用内生成的特定格式码,普通 URL 码扫了不解析)。唯一路径是 IDE 模拟扫码: ``` 支付宝开发者工具 → 编译模式 → 添加 → 勾「模拟扫码」 → 二维码内容填 https://test.local/r/?c=ABCD1234&t=1&s=<真实签名> → 启动页面 pages/hotel/customer/room-confirm → IDE 自动注入 options.qrCode = encodeURIComponent(上述 URL) 微信开发者工具 → 「通过二维码编译」,同理注入 options.q ``` ⚠️ `s` 必须用后端 `QrCodeUtils.generateSign()` 真实算出(dev 密钥 `hotel_qr_default_secret`),否则 `/hotel/open/scan` 验签直接拒绝。 --- ## 5. 前端页面 ### 5.1 PC 管理后台(forge-admin-ui/src/views/hotel/) | 页面 | 文件 | 说明 | |------|------|------| | 二维码管理 | `qrcode.vue` | AiCrudPage(批量生成、批量删除、批量下载 ZIP、单条下载、绑定/重绑/解绑、状态切换、日志查看;**列表展示二维码图片而非短码**) | | 房间管理 | `room.vue` | AiCrudPage + 在住会话操作(入住/退房/打扫/维护) | | 房型管理 | `roomType.vue` | AiCrudPage | | 菜品管理 | `dish.vue` | AiCrudPage | | 菜品分类 | `dishCategory.vue` | AiCrudPage | | 菜品规格 | `dishSpec.vue` | 规格组 + 规格选项 | | 营业时段 | `businessHours.vue` | 开始/结束时间用下拉选择 | | 订单管理 | `order.vue` | 订单列表 + 退款状态列 + 手动退款 | | 销量看板 | `board.vue` | 菜品销量排行 | | API 文件 | `forge-admin-ui/src/api/hotel.js` | 统一 API 封装 | ### 5.2 移动端(forge-h5-ui/src/) | 页面 | 路径 | 归属 | 小程序构建 | |------|------|------|---------| | 扫码绑定 | `pages/hotel/scan-bind.vue` | 员工 H5(**双职责**,见 1.2) | ✅ 已在 `pages.json` 用 `// #ifdef H5` 裁掉(L67-87) | | 相机扫码 | `pages/hotel/qr-scanner.vue` | 员工 H5(html5-qrcode) | ✅ 同上,两页包在同一对 `#ifdef H5` / `#endif` 内 | | 房号确认 | `pages/hotel/customer/room-confirm.vue` | 顾客 | ✅ **小程序首页/扫码入口**,`resolveScanParams` 已兼容三端(见 4.5) | | 点餐菜单 | `pages/hotel/customer/menu.vue` | 顾客 Tab1 | ✅ | | 菜品详情 | `pages/hotel/customer/dish-detail.vue` | 顾客 | ✅ | | 购物车 | `pages/hotel/customer/cart.vue` | 顾客 Tab3 | ✅ | | 确认下单 | `pages/hotel/customer/order-confirm.vue` | 顾客 | ✅ | | 支付 | `pages/hotel/customer/pay.vue` | 顾客 | ✅ 分流依据已改为 `payChannel`(见支付文档 4.2) | | 订单状态 | `pages/hotel/customer/order-status.vue` | 顾客 Tab2 | ✅ | | 历史订单 | `pages/hotel/customer/orders.vue` | 顾客 Tab2 | ✅ | | 底部导航 | `components/hotel/HotelTabBar.vue` | 顾客(3-tab) | ✅ **已条件编译双分支**(L1-146):`` 保留原 CSS `WebkitMask` + 本地 SVG(`iconStyle()` 函数也包进 `// #ifdef H5`);`` 改用 `` + `opacity: 1 / 0.45` 区分激活态 | | API 文件 | `api/index.js` | 全端共用 | ✅ | > 🔴 **为什么小程序图标只能用 `opacity` 区分激活态**:三个图标(`/static/icons/ai-icon/` 下的 `coffee.svg` / `clipboard.svg` / `shopping-cart.svg`)是 lucide 风格,内部写的是 `stroke="currentColor"`。在小程序 `` 中 `currentColor` **会解析为黑色且无法换色**,因此文字颜色仍按激活态变、图标只能靠透明度区分。 > > ❌ **已排除的方案**:「用 `?raw` 读 SVG 原文 → 替换 `currentColor` → 生成 data-URI」。全仓 grep `?raw` **零命中**,`vite.config.js` 也无相关配置先例,引入新构建机制与「不影响 H5 dev」的硬约束相冲,收益不抗风险。 > ✅ **首页入口已同步**(2026-09-07,已用 `git diff` + 文件直读核实在位):`pages/index/index.vue` 内有 **5 处**对员工页的引用,全部已用条件编译隔离 —— ① L245-254 `scanBindItem` 常量整体包进 `// #ifdef H5`;② L274-276 `SCAN_BIND_PERM` 常量同;③ L282-288 `menuItems` 改 `let prefix = []`,仅在 `#ifdef H5` 内按权限赋值;④ L412-427 `isRegisteredH5Route` 改先建 `registered` 数组、再在 `#ifdef H5` 内 `push` 两页;⑤ `handleShortcut` 的 `scan-bind` 跳转分支包进 `#ifdef H5`。 > > 效果:**H5 dev 零变化**(两页在 H5 构建中仍注册,已用 `pnpm build:h5` 产物验证含 `pages-hotel-scan-bind` / `pages-hotel-qr-scanner` 两个 chunk);小程序构建时首页不渲染该入口,不会 `navigateTo` 到未注册页面。 > 🔴 **`` 是小程序构建的首位硬阻塞(实测推翻原有判断)**:2026-09-07 跑 `pnpm build:mp-alipay`,**2.4s 内失败**于 `AiAvatarCropper.vue` 的 ``(`[vite:vue] not supported: Teleport`),整个构建终止。根因:`unplugin-vue-components` 会为**任何被引用到的**组件生成注册代码,与「顾客 8 页是否引用」无关,**`pages.json` 裁页挡不住它**。已列为第 1 批首位新增阻塞项,详见 `酒店模块需求缺口清单.md` 3.6.4。 **顾客 8 页的全部外部依赖**(已核实,依赖面极小): ``` 组件:HotelTabBar.vue(cart / menu / order-status / orders 共 4 页) 工具:@/api(全部 8 页)、getFileDownloadUrl(cart / dish-detail / menu) Store:hotel-order(Pinia + persistedstate) ``` > ✅ **好消息(已被实测部分修正)**:顾客 8 页本身确实**不用 `AiIcon`、不用 CSS mask**,图标全是 emoji(`⏸️` `⚠️` `↩️` `✓`)+ 原生 ``;也**完全没用 `v-loading`**。`AiIcon` 只出现在 `scan-bind.vue`(员工 H5)。 > > ⚠️ 但「因此顾客页本身小程序**零改造**」的推论**不成立**:阻塞不在页面里,而在 `unplugin-vue-components` 自动生成的全局组件注册代码里。`AiAvatarCropper.vue` 的 `` 与 `HotelTabBar` 的 CSS mask 都与顾客 8 页无直接引用关系,但前者已实测终止整个 mp 构建。完整阻塞清单见缺口清单 3.6.4。 --- ## 6. 菜单权限 | 资源ID | 名称 | 类型 | 权限标识 | |--------|------|------|----------| | 2000 | 酒店管理 | 菜单(1) | - | | 2001 | 二维码管理 | 菜单(1) | hotel:qrcode:list | | 2011 | 二维码生成 | 按钮(2) | hotel:qrcode:generate | | 2012 | 二维码绑定 | 按钮(2) | hotel:qrcode:bind | | 2013 | 二维码管理操作 | 按钮(2) | hotel:qrcode:manage | | 2014 | 二维码导出 | 按钮(2) | hotel:qrcode:export | | 2002 | 房间管理 | 菜单(1) | hotel:room:list | | 2021 | 房间新增 | 按钮(2) | hotel:room:add | | 2022 | 房间编辑 | 按钮(2) | hotel:room:edit | | 2023 | 房间删除 | 按钮(2) | hotel:room:delete | | 2003 | 房型管理 | 菜单(1) | hotel:roomtype:list | | 2031 | 房型新增 | 按钮(2) | hotel:roomtype:add | | 2032 | 房型编辑 | 按钮(2) | hotel:roomtype:edit | | 2033 | 房型删除 | 按钮(2) | hotel:roomtype:delete | --- ## 7. Maven 依赖变更 ### 7.1 forge-dependencies(BOM) 新增 ZXing 版本管理: ```xml 3.5.3 com.google.zxing core ${zxing.version} com.google.zxing javase ${zxing.version} ``` 新增 forge-hotel 版本声明: ```xml com.mdframe.forge forge-hotel ${revision} ``` ### 7.2 forge-business/pom.xml 新增子模块: ```xml forge-business-core forge-hotel ``` ### 7.3 forge-admin-server/pom.xml 新增依赖: ```xml com.mdframe.forge forge-hotel ${revision} ``` --- ## 8. 框架层变更 ### SaTokenConfig.java 在 Sa-Token 拦截器中新增 `/hotel/open/**` 路径排除(免登录): ```java // 登录校验拦截器 .notMatch("/hotel/open/**") // API权限拦截器 .excludePathPatterns("/hotel/open/**") ``` --- ## 9. 配置项与环境变量 ### 9.1 环境变量 | 变量名 | 必填 | 默认值 | 说明 | |--------|------|--------|------| | `HOTEL_QR_SECRET` | 否 | `hotel_qr_default_secret` | 二维码签名密钥(生产环境必须修改) | | `FORGE_HOTEL_QR_BASE_URL` | 🔴 **生产必填** | 空串 | 二维码基础域名。空串下生成的二维码无域名不可扫;必须 HTTPS、已备案、**不带端口**(见 4.7) | | `FORGE_HOTEL_QR_PATH` | 否 | `/r/` | 扫码落地路径。**禁止配成 `/`** —— hash 路由的 path 恒为 `/`,平台规则会劫持同域名下所有页面(含员工 H5)。员工 H5 载体请改用 `"/#/pages/hotel/scan-bind"` | | `FORGE_HOTEL_PAY_MOCK_ENABLED` | 否 | `false` | 🔴 Mock 支付开关(AGENTS.md 5.9 资金红线)。**生产必须保持 false**;开启后任何免登录请求都能把订单置为已支付而无真实资金流 | > ⚠️ **变量名易错点**:Mock 开关的变量名是 `FORGE_HOTEL_PAY_MOCK_ENABLED`,**不是** `FORGE_HOTEL_PAY_MOCK`(支付文档旧版曾误写,已于 v1.4 修正)。写错不会报错,只会**静默退回 `false`**,导致 dev 以为开了 Mock 实际没开。 > > ✅ 代码级兼容:`HotelPayConfig` 用 `@Value("${hotel.pay.mock-enabled:false}")`,`HotelQrCodeServiceImpl` 用 `@Value("${hotel.qr.path:}")` —— 两者**都带默认值**,yml 整块缺失也不会启动崩。例外是 `hotel.qr.base-url`(L69)**无默认值**,因此必须保证 6 份 yml 都有 `hotel` 块。 ### 9.2 application.yml 配置项 | 配置 | dev 当前值 | prod 当前值 | 说明 | |---|---|---|---| | `hotel.qr.base-url` | `http://192.168.10.4:3001` | `${FORGE_HOTEL_QR_BASE_URL:}` | 二维码基础 URL。✅ **已落地**(生产从「整块缺失」补齐) | | `hotel.qr.path` | `"/#/pages/hotel/scan-bind"` | `${FORGE_HOTEL_QR_PATH:/r/}` | ✅ **已落地**(原为「待新增」),替代硬编码的 `HotelQrConstants.QR_BASE_PATH`;dev 保持旧值→既有二维码 URL 逐字符不变 | | `hotel.pay.mock-enabled` | `true` | `${FORGE_HOTEL_PAY_MOCK_ENABLED:false}` | ✅ **已落地**(原为「待新增」),Mock 支付开关;读取方 `pay/config/HotelPayConfig.java` | | `hotel.alipay.app-id` | `9021000166699753`(沙箱) | 待填正式 appid | 支付宝应用。⚠️ 沙箱 APPID 与**小程序 appid 不属同一体系**,不能直接填入 `manifest.json` 的 `mp-alipay.appid` | | `hotel.alipay.gateway-url` | `https://openapi-sandbox.dl.alipaydev.com/gateway.do` | 线上网关 | 支付宝网关 | | `hotel.alipay.sandbox` | `true` | `false` | 沙箱开关 | | `hotel.alipay.notify-url` | natapp 免费域名 | 备案域名 | ⚠️ natapp 重启即失效,回调不稳 | | `hotel.alipay.return-url` | `http://192.168.10.4:3001/#/pages/hotel/customer/pay` | 待改 | ⚠️ **仅 `forge-app-server` 有**;hash 路由使支付宝同步回跳**丢参数**(第 3 批修) | > 🔴 **六份 yml 必须同步**(已核对一致):`forge-admin-server` 与 `forge-app-server` 各有 `application.yml` / `application-dev.yml` / `application-dev.example.yml` 共 **6 份**,均已写入上表前三项。 > > 分工约定:`application.yml` = **生产默认,一律严格**(`mock-enabled=false`、`path=/r/`);`application-dev.yml` 与 `application-dev.example.yml` = **开发覆盖,一律宽松**(`mock-enabled=true`、`path` 为 hash 旧值)。这个分工是「不影响 H5 dev 测试」硬约束的落地手法之一。 --- ## 10. 后续扩展预留 | 扩展点 | 说明 | |--------|------| | 钉钉 H5 集成 | 通过 DingTalk SSO 获取员工身份(当前员工端走 Forge Sa-Token,未接钉钉) | | 房间状态实时推送 | WebSocket 推送房间状态变更(当前顾客端用 10 秒轮询) | | 二维码导出打印 | 使用 EasyExcel 批量导出二维码图片(已实现批量下载 ZIP) | ### 10.1 已从「预留」转为「已落地」 | 项 | 实现位置 | |---|---| | ~~小程序 openid 绑定~~ | `AlipayAuthController` `/hotel/open/alipay-auth/getUserInfo`(authCode → 姓名,加密响应 → 手机号);`room-confirm.vue` L376-396 **已在调用** | | ~~顾客点餐流程~~ | `pages/hotel/customer/` 8 页全部实现 + `HotelCustomerController` 9 个开放接口 | | ~~支付~~ | `HotelPayController` 5 接口 + 支付宝 H5 WAP Pay + 轮询 + 主动查询兜底 + 退款(见 `酒店订单支付系统开发文档.md`) | | ~~在住会话隔离~~ | `hotel_room_stay` + `stayId` 挂载订单(V1.0.106) | ### 10.2 待落地(小程序化改造) > 📌 **排期与批次归属的唯一权威**:完整的第 0~6 批补齐顺序、依赖关系与变更提案命名见 `酒店模块需求缺口清单.md` **第九节**。本表只列与**二维码模块**直接相关的待落地项及其实现依据章节,不重复维护排期。 | 项 | 本文档实现依据 | 缺口清单批次 | 状态 | |---|---|---|---| | Mock 支付开关(🔴 资金红线) | **3.3** 开放接口安全红线 | 第 0 批 | ✅ 代码已落地;🔴 **Spec 未补、人工审查未做**(5.9 未闭环) | | 二维码 URL 去 hash(`QR_BASE_PATH` 配置化为 `hotel.qr.path`) | **4.1 / 4.2 / 4.3 / 9.2** | 第 2 批 | ✅ 已落地(四参重载 + 三层回退,dev URL 零变化) | | 三端扫码取参(`resolveScanParams`) | **4.5** | 第 2 批 | ✅ 已落地(`room-confirm.vue` L185-214) | | 构建裁剪(`pages.json` 员工两页 `#ifdef H5`) | **1.2 / 5.2** | 第 2 批 | ✅ 已落地(L67-87);✅ 首页 `index.vue` 的 5 处入口引用已同步(`git diff` 核实在位) | | `HotelTabBar` 图标 CSS mask → `` | **5.2** | 第 2 批 | ✅ 已落地(双分支 + `opacity` 区分激活态) | | 小程序环境配置(`.env.mp-alipay`) | — | 第 2 批 | ✅ **已生效**:`package.json` L9/L25 的 `dev:mp-alipay` / `build:mp-alipay` 均已带 `--mode mp-alipay`(`git diff` 核实在位)。⚠️ 该 `--mode` **不可省**:实测 uni-app/Vite **不会按平台自动加载 `.env.[platform]`**(`@dcloudio` 包内零 `loadEnv` 调用),只按 mode 加载 `.env.[mode]`,去掉 `--mode` 会让该文件静默失效 | | 小程序 appid(`manifest.json` 的 `mp-alipay.appid`) | — | 第 4 批 | ⏳ **占位已在位(L61-64,含说明注释),值仍为空串**,待企业资质下发后填入。空串下只能在支付宝 IDE 以「测试号」本地预览,无法上传体验版/发布正式版,也无法配「普通链接二维码」规则。⚠️ 支付宝沙箱 APPID `9021000166699753` **与小程序 appid 不属同一体系**,不能直接填入 | | 小程序基础设施适配(axios → `uni.request`、crypto 随机数、persist storage、图片绝对 URL) | **5.2** 顾客 8 页依赖面 | 第 1 批 | ❌ 未开始;🔴 **首位阻塞项改为 `AiAvatarCropper.vue` 的 ``**(实测 2.4s 内终止整个 mp 构建,页面裁剪挡不住) | | `/r/` 返回 403(仅放行 `.txt` 校验文件) | **4.6 / 4.7** | 第 4 批 | ❌ 未开始 | | `ALIPAY_MP` 支付渠道(`alipay.trade.create` → `tradeNo`) | —(见 `酒店订单支付系统开发文档.md` **7.1**) | 第 3 批 | ❌ 未开始;⚠️ `resolvePayChannel` 已将 `ALIPAY_MP` 归为 `ALIPAY`,分流必须用**原始 `paySource`** | | `user_id`/`openid` → `stayId` 绑定(R2 订单归属收口) | **3.3** 第 3 条 | 第 3 批 | ❌ 未开始 | | 真机扫码验证 + 平台规则配置 | **4.7** 上线硬门槛 7 条 | 第 4 批(依赖外部资质) | ❌ 未开始(资质待补充) | ### 10.3 开发环境现状快照(2026-09-07) 供下次接手时快速定位「什么已改、什么仍是空占位」(下表全部经 `git diff` / 文件直读核实,非推测): | 位置 | 状态 | |---|---| | `HotelQrConstants.QR_BASE_PATH` | ✅ 保留,降为回退默认值 | | `QrCodeUtils.buildQrCodeUrl` | ✅ 新增四参重载,三参方法委派到它 | | `HotelQrCodeServiceImpl` L79 / L283 / L669 | ✅ `@Value("${hotel.qr.path:}")` + 两处调用改四参 | | 6 份 yml 的 `hotel` 块 | ✅ 已逐行核对一致 | | `room-confirm.vue` `resolveScanParams` | ✅ 已落地 | | `pages.json` L67-87 | ✅ 员工两页 `#ifdef H5` 仍在 | | `HotelTabBar.vue` | ✅ 双分支仍在(146 行) | | `forge-h5-ui/.env.mp-alipay` | ✅ 文件存在**且会被加载**(`--mode mp-alipay` 在位) | | `pages/index/index.vue` | ✅ 5 处 `#ifdef H5` 全部在位(L245-254 / L274-276 / L282-288 / L412-427 / `handleShortcut`) | | `manifest.json` 的 `mp-alipay.appid` | ⏳ 占位在位(L61-64),值为空串,待资质 | | `package.json` 的 `--mode mp-alipay` | ✅ L9 / L25 均在位 | > 上表四项**彼此一致,无遗留不一致**:`.env.mp-alipay` ← `--mode mp-alipay` ← `manifest.json` 占位构成完整链条,唯一待补的是真实 appid(依赖外部资质,第 4 批)。 > > ⚠️ **纠错记录(2026-09-07)**:本表后三行曾误记为「已被用户回退」「死配置」,系仅凭 IDE 的 attached_files 提示推断、未用 `git diff` 核实所致。经 `git diff` + 文件直读复核,**三处改动全部在位**,已按事实更正。后续判断工作区状态一律以 `git diff` 为准。 --- *文档最后更新:2026-09-07(**第 0 批 + 第 2 批落地同步**:① 4.1 重写为「已配置化」—— `hotel.qr.path` 四参重载 + 三层回退设计(yml 显式值 → `@Value` 空串默认 → `QR_BASE_PATH` 回退),并登记生产 `application.yml` 原缺整个 `hotel` 块导致启动崩的隐患;② 4.2 补「当前状态」列;③ 4.5 的 `resolveScanParams` 从**方案稿换为实际代码**,并标注两处实质差异(返回 `{shortCode,sign}` 而非 `{c,t,s}`、显式参数优先不走正则);④ 1.2 / 5.2 标注 `pages.json` 裁剪与 `HotelTabBar` 双分支已落地,**新增两个 🔴 事实**:`` 实测终止整个 mp 构建(推翻「顾客页零改造」推论)、图标 `currentColor` 在小程序 `` 中解析为黑色只能用 `opacity` 区分;⑤ 9.1 补三个新环境变量并标注 `FORGE_HOTEL_PAY_MOCK` 为**错误旧名**;⑥ 9.2 两项从「待新增」改「已落地」并补 `return-url` 行;⑦ 3.3 开放接口安全红线逐条标注收口状态,新增退款渠道误判与已入库沙箱密钥两项;⑧ 10.2 补「状态」列,新增 **10.3 开发环境现状快照**;⑨ **纠错**:1.2 / 5.2 / 10.2 / 10.3 原记「首页 `index.vue` 的 5 处 `#ifdef H5`、`manifest.json` 的 `mp-alipay.appid` 占位、`package.json` 的 `--mode mp-alipay` 三处已被用户回退」与「`.env.mp-alipay` 为死配置」,经 `git diff` + 文件直读复核**全部错误** —— 三处改动均在位、`.env.mp-alipay` 会被正常加载,已按事实更正并在 10.3 留纠错记录。本文档 3.2/3.3/4.1~4.7/5.2 是缺口清单 3.2/3.6 指针的目标章节,为二维码与开放接口安全红线的**唯一权威副本**)* *上版:2026-09-07(① 文首新增**职责声明**,明确本文档只承载二维码实现权威,排期/验收/代码地图/支付协议均归其他三份;② 1.2 补「四端(接入载体)vs 五端(需求业务角色)」口径区别说明,消除与缺口清单一章的表面矛盾;③ 10.2 待落地表改为「本文档实现依据 + 缺口清单批次」双列,排期权威归位到缺口清单第九节)* > 🔴 读本文档时请注意:**第 0 批属资金类变更,AGENTS.md 5.9 合规未闭环**(Spec 未补、人工审查未做)。不要把「代码已落地」误读为「变更已验收」。