本文档职责:二维码模块的实现权威——回答「二维码怎么建、怎么绑、怎么扫、接口长什么样」。具体包括: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本文档只写二维码相关的实现结论;引用其他三份时一律用指针(章节号),不复制正文。
模块名称: forge-hotel
包路径: com.mdframe.forge.business.core.hotel
位置: forge-server/forge-business/forge-hotel/
| 功能 | 说明 |
|---|---|
| 二维码批量生成 | 批量生成不绑定房间的二维码,支持 10/50/100/200 个一批 |
| 二维码绑定/重绑/解绑 | 员工通过 PC 后台扫码绑定房间,记录操作人和时间 |
| 二维码状态管理 | 启用/禁用二维码(字典 sys_normal_disable: 0=启用, 1=禁用) |
| 绑定日志追溯 | 完整记录每次绑定/重绑/解绑操作 |
| 批量删除 | 勾选多个二维码批量逻辑删除,已绑定房间的拒绝删除 |
| 批量下载ZIP | 勾选多个二维码生成 PNG 图片打包为 ZIP 下载 |
| 单条下载 | 操作列下载单个二维码 PNG 图片 |
| 房间管理 | 房间 CRUD,关联房型 |
| 房型管理 | 房型 CRUD |
| 扫码查询(开放接口) | 免登录接口,员工 H5 与顾客小程序共用 /hotel/open/scan |
| 在住会话管理 | 入住/退房/打扫完成/维护切换/入住记录分页(hotel_room_stay) |
📌 「四端」与「五端」的口径区别(勿误判为矛盾):本节的四端 = 接入载体(技术视角:谁用什么客户端访问);
酒店模块需求缺口清单.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.jsonL67-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 零变化。
| 字段 | 类型 | 说明 |
|---|---|---|
| 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 | - | 审计字段 |
| 字段 | 类型 | 说明 |
|---|---|---|
| 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)
| 字段 | 类型 | 说明 |
|---|---|---|
| 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")),以保证同一短码删除后可重建。
| 字段 | 类型 | 说明 |
|---|---|---|
| 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) | 备注 |
文件: forge-server/db/migration/V1.0.100__add_hotel_qr_module_tables.sql
后续酒店模块迁移脚本已至
V1.0.112(菜品/规格/营业时段/订单/支付/在住会话/退款/配置等),完整清单见酒店模块迁移跟踪.md。
包含内容:
CREATE TABLE IF NOT EXISTS)sys_resource,tenant_id=1,带 NOT EXISTS 防重复)sys_dict_type + sys_dict_data,tenant_id=1)| 字典类型 | 字典值 | 标签 | 标签样式 |
|---|---|---|---|
| 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 |
⚠️ 路径风格:本模块已完成 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= |
删除房型 |
/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 → 姓名 + 手机号) |
/hotel/open/pay/mockSuccess 完全免登录orderId + tenantId 就能把订单置为已支付(免付款)。已用 hotel.pay.mock-enabled 开关拦截(dev=true / prod=false),拦截点位于 HotelPayController.mockPaySuccess() 方法首行(不放 Service,保证开关语义单点)。⚠️ 本项属资金类变更,代码已先行但 Spec 未补、人工审查未做,AGENTS.md 5.9 合规未闭环。createPay 的 paySource 当前设了 defaultValue="MOCK"@RequestParam String paySource 必传;resolvePayChannel 抽出 mockOrReject(reason) 单一收口点,全部非支付宝分支都走它,全仓不再有裸 return "MOCK";开关关闭时每个分支抛差异化文案(微信未开通 / 渠道为空 / 模拟已禁用 / 不支持的渠道)。详见 酒店订单支付系统开发文档.md 3.4 / 10.1。tenantId 由前端传入(可伪造),stayId 归属校验当前缺失(见 酒店模块需求缺口清单.md 3.2 R2)。小程序化后必须由平台 openid/user_id 推导 stayId,禁止前端直传。❌ 未修(第 3 批)。AlipayAuthController 新增 maskPhone()(保留前 3 后 4,中间 ****);AlipayAuthServiceImpl 不再把支付宝返回的密文报文整包 log.warn(密文可离线解密,等同泄露),改为只记 responseLength。refundOrder 原用 !"ALIPAY".equals(payChannel) 兜底判 MOCK,未来接入微信后微信订单退款会被误判为无资金流而直接标记成功 → 真实资损。已改为显式判定三种无资金流情形,其余非 ALIPAY 抛「请联系前台人工处理」。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"
}
}
}
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_secret(HotelQrConstants.QR_SIGN_SECRET)HOTEL_QR_SECRET 覆盖| 用途 | 格式 | 路径前缀 | 是否在平台配规则 | 当前状态 |
|---|---|---|---|---|
| 员工 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不得更改。员工 H5qr-scanner.vue的parseQrUrl用正则[?&]c=([^&]+)解析,scan-bind.vue/room-confirm.vue用options.c || options.shortCode取参。保留 query 形式(而不改成路径参数),已完成的员工扫码绑定功能零改动。
微信/支付宝的「普通链接二维码」规则是按 URL 的 path 前缀匹配,而 hash 路由的 path 恒为 /(# 后面不属于 path)。后果:
https://域名/所以顾客码必须用专属非 hash 前缀 /r/,员工 H5 部署在 /h5/(不配规则),两者物理隔离。
4.1 的当前 URL 另有 4 个上线硬伤:http:// 非 HTTPS、IP 非备案域名、带端口 :3001、指向员工页 scan-bind。
hotel_qr_code 表只存 short_code + sign,无 qr_url / qr_content 字段。
结论:改二维码格式 = 改配置项,无需任何数据迁移、无需重印已生成的码。但一旦桌牌印刷上墙,再改就要全部重印 —— 格式必须在印刷前定死。
方案选型:自研普通 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 时不要拿旧稿对:
- 返回字段是
{ shortCode, sign },不是{ c, t, s }—— 下游loadRoomInfo()只用shortCode+sign调api.hotelCustomerScan(),tenantId由后端根据 shortCode 反查并通过data.tenantId回传(L270orderStore.setTenantId(data.tenantId))。前端自己解t反而多一个可被篡改的信任面。- 显式参数优先:先试
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 旧值,走的是「显式参数优先」分支,正则根本不会执行)。
| 层 | 做法 | 强度 |
|---|---|---|
| 入口层 | /r/ 对所有真实 HTTP 请求返回 403,只放行平台校验用 .txt 文件。因为微信/支付宝命中规则时根本不会向服务器发请求(客户端直接拉起小程序),所以拦掉全部 HTTP 请求不影响正常用户 |
挡住浏览器、其他 App 扫码 |
| 接口层 | /hotel/open/customer/** 校验平台签发的身份凭证(my.getAuthCode → user_id;wx.login → openid),拿不到 code 的一律 401 |
真正的安全边界 |
⚠️ 入口层不构成强制:截获 URL 参数者仍可直接用 curl 调免登录后端接口。只有接口层的身份校验才是安全边界,且它同时解决 R2 订单归属漏洞。
| # | 门槛 | 说明 |
|---|---|---|
| 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 验签直接拒绝。
| 页面 | 文件 | 说明 |
|---|---|---|
| 二维码管理 | 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 封装 |
| 页面 | 路径 | 归属 | 小程序构建 |
|---|---|---|---|
| 扫码绑定 | 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-254scanBindItem常量整体包进// #ifdef H5;② L274-276SCAN_BIND_PERM常量同;③ L282-288menuItems改let prefix = [],仅在#ifdef H5内按权限赋值;④ L412-427isRegisteredH5Route改先建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到未注册页面。🔴
<Teleport>是小程序构建的首位硬阻塞(实测推翻原有判断):2026-09-07 跑pnpm build:mp-alipay,2.4s 内失败于AiAvatarCropper.vue的<Teleport to="body">([vite:vue] not supported: Teleport),整个构建终止。根因:unplugin-vue-components会为任何被引用到的组件生成注册代码,与「顾客 8 页是否引用」无关,pages.json裁页挡不住它。已列为第 1 批首位新增阻塞项,详见酒店模块需求缺口清单.md3.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-loading。AiIcon只出现在scan-bind.vue(员工 H5)。⚠️ 但「因此顾客页本身小程序零改造」的推论不成立:阻塞不在页面里,而在
unplugin-vue-components自动生成的全局组件注册代码里。AiAvatarCropper.vue的<Teleport>与HotelTabBar的 CSS mask 都与顾客 8 页无直接引用关系,但前者已实测终止整个 mp 构建。完整阻塞清单见缺口清单 3.6.4。
| 资源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 |
新增 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>
新增子模块:
<modules>
<module>forge-business-core</module>
<module>forge-hotel</module>
</modules>
新增依赖:
<dependency>
<groupId>com.mdframe.forge</groupId>
<artifactId>forge-hotel</artifactId>
<version>${revision}</version>
</dependency>
在 Sa-Token 拦截器中新增 /hotel/open/** 路径排除(免登录):
// 登录校验拦截器
.notMatch("/hotel/open/**")
// API权限拦截器
.excludePathPatterns("/hotel/open/**")
| 变量名 | 必填 | 默认值 | 说明 |
|---|---|---|---|
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块。
| 配置 | 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 测试」硬约束的落地手法之一。
| 扩展点 | 说明 |
|---|---|
| 钉钉 H5 集成 | 通过 DingTalk SSO 获取员工身份(当前员工端走 Forge Sa-Token,未接钉钉) |
| 房间状态实时推送 | WebSocket 推送房间状态变更(当前顾客端用 10 秒轮询) |
| 二维码导出打印 | 使用 EasyExcel 批量导出二维码图片(已实现批量下载 ZIP) |
| 项 | 实现位置 |
|---|---|
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) |
📌 排期与批次归属的唯一权威:完整的第 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-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 的 <Teleport>(实测 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 批(依赖外部资质) | ❌ 未开始(资质待补充) |
供下次接手时快速定位「什么已改、什么仍是空占位」(下表全部经 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 双分支已落地,新增两个 🔴 事实:<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 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 未补、人工审查未做)。不要把「代码已落地」误读为「变更已验收」。