酒店二维码模块开发文档.md 44 KB

酒店二维码模块开发文档

本文档职责二维码模块的实现权威——回答「二维码怎么建、怎么绑、怎么扫、接口长什么样」。具体包括: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 必须 HTTPSqr-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_flagBIGINT,未删除为 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_resourcetenant_id=1,带 NOT EXISTS 防重复)
  • 字典数据(sys_dict_type + sys_dict_datatenant_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 形式;updateStatusPOST 而非 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. createPaypaySource 当前设了 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 签名校验值

响应示例(未绑定):

{
  "code": 200,
  "data": {
    "qrCodeId": 1234567890,
    "shortCode": "AB12CD34",
    "status": "0",
    "bound": false,
    "room": null,
    "bindInfo": null
  }
}

响应示例(已绑定):

{
  "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

// 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_secretHotelQrConstants.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.vueparseQrUrl 用正则 [?&]c=([^&]+) 解析,scan-bind.vue / room-confirm.vueoptions.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 + signapi.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,因此开发期路径完全不变。
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.getAuthCodeuser_idwx.loginopenid),拿不到 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):<!-- #ifdef H5 --> 保留原 CSS WebkitMask + 本地 SVG(iconStyle() 函数也包进 // #ifdef H5);<!-- #ifndef H5 --> 改用 <image mode="aspectFit"> + opacity: 1 / 0.45 区分激活态
API 文件 api/index.js 全端共用

🔴 为什么小程序图标只能用 opacity 区分激活态:三个图标(/static/icons/ai-icon/ 下的 coffee.svg / clipboard.svg / shopping-cart.svg)是 lucide 风格,内部写的是 stroke="currentColor"。在小程序 <image>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 menuItemslet prefix = [],仅在 #ifdef H5 内按权限赋值;④ L412-427 isRegisteredH5Route 改先建 registered 数组、再在 #ifdef H5push 两页;⑤ handleShortcutscan-bind 跳转分支包进 #ifdef H5

效果:H5 dev 零变化(两页在 H5 构建中仍注册,已用 pnpm build:h5 产物验证含 pages-hotel-scan-bind / pages-hotel-qr-scanner 两个 chunk);小程序构建时首页不渲染该入口,不会 navigateTo 到未注册页面。

🔴 <Teleport> 是小程序构建的首位硬阻塞(实测推翻原有判断):2026-09-07 跑 pnpm build:mp-alipay2.4s 内失败AiAvatarCropper.vue<Teleport to="body">[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(⏸️ ⚠️ ↩️ )+ 原生 <image>;也完全没用 v-loadingAiIcon 只出现在 scan-bind.vue(员工 H5)。

⚠️ 但「因此顾客页本身小程序零改造」的推论不成立:阻塞不在页面里,而在 unplugin-vue-components 自动生成的全局组件注册代码里。AiAvatarCropper.vue<Teleport>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 版本管理:

<zxing.version>3.5.3</zxing.version>

<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>core</artifactId>
    <version>${zxing.version}</version>
</dependency>
<dependency>
    <groupId>com.google.zxing</groupId>
    <artifactId>javase</artifactId>
    <version>${zxing.version}</version>
</dependency>

新增 forge-hotel 版本声明:

<dependency>
    <groupId>com.mdframe.forge</groupId>
    <artifactId>forge-hotel</artifactId>
    <version>${revision}</version>
</dependency>

7.2 forge-business/pom.xml

新增子模块:

<modules>
    <module>forge-business-core</module>
    <module>forge-hotel</module>
</modules>

7.3 forge-admin-server/pom.xml

新增依赖:

<dependency>
    <groupId>com.mdframe.forge</groupId>
    <artifactId>forge-hotel</artifactId>
    <version>${revision}</version>
</dependency>

8. 框架层变更

SaTokenConfig.java

在 Sa-Token 拦截器中新增 /hotel/open/** 路径排除(免登录):

// 登录校验拦截器
.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.jsonmp-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-serverforge-app-server 各有 application.yml / application-dev.yml / application-dev.example.yml6 份,均已写入上表前三项。

分工约定:application.yml = 生产默认,一律严格mock-enabled=falsepath=/r/);application-dev.ymlapplication-dev.example.yml = 开发覆盖,一律宽松mock-enabled=truepath 为 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 → <image> 5.2 第 2 批 ✅ 已落地(双分支 + opacity 区分激活态)
小程序环境配置(.env.mp-alipay 第 2 批 已生效package.json L9/L25 的 dev:mp-alipay / build:mp-alipay 均已带 --mode mp-alipaygit diff 核实在位)。⚠️ 该 --mode 不可省:实测 uni-app/Vite 不会按平台自动加载 .env.[platform]@dcloudio 包内零 loadEnv 调用),只按 mode 加载 .env.[mode],去掉 --mode 会让该文件静默失效
小程序 appid(manifest.jsonmp-alipay.appid 第 4 批 占位已在位(L61-64,含说明注释),值仍为空串,待企业资质下发后填入。空串下只能在支付宝 IDE 以「测试号」本地预览,无法上传体验版/发布正式版,也无法配「普通链接二维码」规则。⚠️ 支付宝沙箱 APPID 9021000166699753 与小程序 appid 不属同一体系,不能直接填入
小程序基础设施适配(axios → uni.request、crypto 随机数、persist storage、图片绝对 URL) 5.2 顾客 8 页依赖面 第 1 批 ❌ 未开始;🔴 首位阻塞项改为 AiAvatarCropper.vue<Teleport>(实测 2.4s 内终止整个 mp 构建,页面裁剪挡不住)
/r/ 返回 403(仅放行 .txt 校验文件) 4.6 / 4.7 第 4 批 ❌ 未开始
ALIPAY_MP 支付渠道(alipay.trade.createtradeNo —(见 酒店订单支付系统开发文档.md 7.1 第 3 批 ❌ 未开始;⚠️ resolvePayChannel 已将 ALIPAY_MP 归为 ALIPAY,分流必须用原始 paySource
user_id/openidstayId 绑定(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.jsonmp-alipay.appid ⏳ 占位在位(L61-64),值为空串,待资质
package.json--mode mp-alipay ✅ L9 / L25 均在位

上表四项彼此一致,无遗留不一致.env.mp-alipay--mode mp-alipaymanifest.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 双分支已落地,新增两个 🔴 事实<Teleport> 实测终止整个 mp 构建(推翻「顾客页零改造」推论)、图标 currentColor 在小程序 <image> 中解析为黑色只能用 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 H5manifest.jsonmp-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 未补、人工审查未做)。不要把「代码已落地」误读为「变更已验收」。