# 酒店订单支付系统开发文档 > 开发时间:2026-08-17 > 功能模块:酒店顾客端支付流程(Mock 模式) --- ## 一、功能概述 实现了酒店顾客端完整的支付流程,支持**先支付后接单**的业务模式。当前采用 Mock 支付模式,后续可无缝对接微信支付、支付宝小程序支付。 ### 核心特性 - ✅ **支付前置**:订单创建后进入"待支付"状态,支付成功后才进入"待接单" - ✅ **15 分钟超时**:未支付订单自动取消,定时任务扫描处理 - ✅ **并发安全**:订单号生成、菜品销量累加均解决并发问题 - ✅ **支付流水**:完整的支付日志记录,支持对账 - ✅ **多状态管理**:待支付(0) → 已支付/待接单(1) → 后续流程;超时取消(10) - ✅ **H5 适配**:支持微信小程序、支付宝小程序、H5、Mock 四种支付环境 --- ## 二、数据库变更 ### 2.1 Flyway 迁移脚本 **文件**:`forge-server/db/migration/V1.0.105__add_hotel_order_payment_fields.sql` > MySQL 8.0 兼容:使用 `information_schema` + `PREPARE/EXECUTE` 模式替代 MariaDB 专有的 `IF NOT EXISTS` 扩展语法。 #### hotel_order 表新增字段 ```sql pay_status TINYINT -- 支付状态:0=待支付, 1=已支付, 2=已退款, 3=支付超时已取消 pay_expire_time DATETIME -- 支付截止时间 pay_time DATETIME -- 实际支付时间 pay_trade_no VARCHAR(128) -- 第三方支付交易号 paid_amount DECIMAL(10,2) -- 实付金额 pay_source VARCHAR(32) -- 支付来源:WECHAT/ALIPAY/H5/MOCK ``` #### 清理遗留字段 - `pay_method`:V1.0.104 创建但实体已移除,迁移脚本自动 DROP COLUMN #### 新增索引 - `idx_pay_expire`:加速支付超时订单扫描(`pay_status`, `pay_expire_time`) - `uk_order_no`:已在 V1.0.104 中创建,不重复 #### hotel_pay_log 表 记录每笔支付的完整流水,包括: - 订单关联(order_id, order_no) - 支付渠道(pay_channel: WECHAT/ALIPAY/MOCK) - 支付来源(pay_source: WECHAT/ALIPAY/H5/WECHAT_MP/ALIPAY_MP/MOCK) - 交易流水号(out_trade_no: 商户生成,trade_no: 第三方返回) - 支付金额、状态、时间等 #### 字典数据 新增字典类型: - `hotel_pay_status`:支付状态(0=待支付, 1=已支付, 2=已退款, 3=支付超时已取消) - `hotel_pay_source`:支付来源(WECHAT/ALIPAY/H5/MOCK 等) - `hotel_order_status`:补充 0=待支付、10=超时取消 --- ## 三、后端变更 ### 3.1 常量定义 **文件**:`HotelOrderConstants.java` ```java // 订单状态 STATUS_PENDING_PAY = 0 // 待支付(新增) STATUS_PLACED = 1 // 待接单(已支付) STATUS_PAY_TIMEOUT = 10 // 超时取消(新增) // 支付状态 PAY_STATUS_PENDING = 0 // 待支付 PAY_STATUS_SUCCESS = 1 // 已支付 PAY_STATUS_TIMEOUT = 2 // 支付超时 // 支付来源 PAY_SOURCE_WECHAT = "WECHAT" PAY_SOURCE_ALIPAY = "ALIPAY" PAY_SOURCE_H5 = "H5" PAY_SOURCE_MOCK = "MOCK" // 超时配置 PAY_TIMEOUT_MINUTES = 15 // 支付超时时间(分钟) ``` ### 3.2 实体类 #### HotelOrder.java 新增字段: - `payStatus`(Integer):支付状态 - `payExpireTime`(LocalDateTime):支付截止时间 - `payTime`(LocalDateTime):实际支付时间 - `payTradeNo`(String):支付交易号 - `paidAmount`(BigDecimal):实付金额 - `paySource`(String):支付来源 移除字段: - `payMethod`(已废弃,改用 paySource) #### HotelPayLog.java(新建) 支付流水实体,包含完整的支付记录字段。 ### 3.3 Mapper 层 #### HotelOrderMapper.java 新增方法: ```java List selectExpiredUnpaidOrderIds() ``` 查询所有超时未支付的订单 ID。 #### HotelOrderMapper.xml - `resultMap` 和 `OrderColumns`:添加支付相关字段映射 - `selectDashboard`、`countTodayOrders`、`sumTodayRevenue`:改为只统计已支付订单(`pay_status = 1`) - `selectOrderPage`:筛选条件从 `payMethod` 改为 `payStatus`、`paySource` - 新增 `selectExpiredUnpaidOrderIds`:查询超时订单 #### HotelDishMapper.java 新增方法: ```java int incrementSales(@Param("dishId") Long dishId, @Param("quantity") Integer quantity) ``` 原子更新菜品销量。 #### HotelDishMapper.xml 新增 SQL: ```xml UPDATE hotel_dish SET sales = IFNULL(sales, 0) + #{quantity}, update_time = NOW() WHERE id = #{dishId} AND del_flag = 0 ``` #### HotelPayLogMapper.java(新建) ```java HotelPayLog selectByOutTradeNo(@Param("outTradeNo") String outTradeNo) ``` 根据商户订单号查询支付流水。 ### 3.4 Service 层 #### HotelOrderServiceImpl.java **orderCreate() 方法重构**: - 订单创建后状态设为 `STATUS_PENDING_PAY`(待支付) - 设置 `payStatus = PAY_STATUS_PENDING` - 设置 `payExpireTime = now + 15 分钟` - 移除 `payMethod` 字段赋值 **generateOrderNo() 并发修复**: - 添加 3 次重试机制 - 每次重试前检查唯一性(`selectCount`) - 防御性唯一性检查:插入前再次验证订单号是否已存在 **incrementDishSales() 并发修复**: - 从"先查后写"改为原子 SQL 更新 - 调用 `dishMapper.incrementSales()` 直接执行 `SET sales = IFNULL(sales, 0) + #{quantity}` **customerRefund() 新增逻辑**: - 待支付订单(status=0):直接取消,状态改为 `STATUS_PAY_TIMEOUT` - 已支付订单(status=1):进入退单流程 #### HotelPayService.java(新建) 支付服务接口,定义核心方法: ```java Map createPay(Long orderId, String paySource) PayResultVO mockPaySuccess(Long orderId) void handlePayCallback(String outTradeNo, String tradeNo, boolean success, String rawContent) PayResultVO queryPayStatus(Long orderId) void cancelTimeoutOrder(Long orderId) ``` #### HotelPayServiceImpl.java(新建) **createPay()**: - 校验订单状态(必须为待支付) - 检查是否已过期 - 生成商户订单号(`PAY + 时间戳 + 订单ID后6位`) - 创建支付流水(`hotel_pay_log`) - 返回支付参数(Mock 模式返回简化参数) **mockPaySuccess()**: - 模拟支付成功流程 - 创建 Mock 支付流水(如果不存在) - 触发支付回调处理 **handlePayCallback()**: - 幂等校验:已处理的订单直接返回 - 支付成功:更新流水状态、更新订单状态(0→1)、记录支付时间和交易号 - 支付失败:更新流水状态为失败 **cancelTimeoutOrder()**: - 更新订单状态为 `STATUS_PAY_TIMEOUT` - 更新支付状态为 `PAY_STATUS_TIMEOUT` **queryPayStatus()**: - 返回订单支付状态、金额、时间等信息 ### 3.5 Controller 层 #### HotelPayController.java(新建) 路径:`/hotel/open/pay/**`(免登录,已在 SaToken 白名单中) 接口列表: ``` POST /create 发起支付(创建支付流水) POST /mockSuccess 模拟支付成功(开发阶段) GET /status 查询支付状态(前端轮询) POST /notify 支付回调通知(微信/支付宝异步通知入口,Mock 占位) ``` 所有接口都需要传入 `tenantId` 参数,通过 `TenantContextHolder.executeWithTenant()` 在指定租户上下文中执行。 ### 3.6 定时任务 #### PayTimeoutTask.java(新建) ```java @Scheduled(fixedDelay = 120000, initialDelay = 30000) public void cancelExpiredOrders() ``` - 每 2 分钟执行一次(启动后 30 秒开始) - 使用 `TenantContextHolder.executeIgnore()` 跨租户扫描 - 查询所有超时未支付的订单 ID - 逐条调用 `payService.cancelTimeoutOrder()` 取消 --- ## 四、前端变更(H5) ### 4.1 API 层 **文件**:`forge-h5-ui/src/api/index.js` 新增支付相关 API: ```javascript hotelPayCreate: (data) => request.post('/hotel/open/pay/create', data) hotelPayMockSuccess: (data) => request.post('/hotel/open/pay/mockSuccess', data) hotelPayStatus: (params) => request.get('/hotel/open/pay/status', { params }) ``` ### 4.2 支付页面 **文件**:`forge-h5-ui/src/pages/hotel/customer/pay.vue` **核心功能**: - **15 分钟倒计时**:显示剩余时间(分:秒),超时自动跳转 - **环境自动识别**: - 微信小程序:`wx.miniProgram.getPhoneNumber()` - 支付宝小程序:`my.tradePay()` - H5:跳转第三方支付页面 - Mock:直接调用模拟支付接口 - **支付流程**: 1. 调用 `hotelPayCreate` 创建支付流水 2. 根据环境调用对应支付方法(Mock 直接调用 `hotelPayMockSuccess`) 3. 支付成功后跳转到订单状态页 **UI 元素**: - 订单信息展示(订单号、金额、房间号) - 倒计时显示(红色高亮) - 支付方式选择(当前仅 Mock) - 支付按钮(根据环境显示不同文案) ### 4.3 订单状态页 **文件**:`forge-h5-ui/src/pages/hotel/customer/order-status.vue` 新增状态处理: - **状态 0(待支付)**: - 图标:💳 - 文案:"等待支付" - 提示:"请在 15 分钟内完成支付" - 按钮:"去支付"(跳转到支付页) - **状态 10(超时取消)**: - 图标:⏰ - 文案:"支付超时,订单已取消" - 提示:"您可以重新下单" - `isFinished()` 返回 true(订单结束) ### 4.4 订单列表页 **文件**:`forge-h5-ui/src/pages/hotel/customer/orders.vue` **状态标签更新**: ```javascript statusLabel(status) { const map = { 0: '待支付', 1: '待接单', 2: '已接单', 3: '备餐中', 4: '已出餐', 5: '配送中', 6: '已完成', 7: '已拒单', 8: '退单审核中', 9: '已退单', 10: '超时取消', } return map[status] || '未知' } ``` **状态颜色**: - `.o-st--0`:黄色背景(待支付) - `.o-st--10`:灰色背景(超时取消) **待支付订单特殊处理**: - 显示"去支付"按钮 - 点击卡片跳转到支付页(而非订单状态页) ### 4.5 订单确认页 **文件**:`forge-h5-ui/src/pages/hotel/customer/order-confirm.vue` 更新跳转逻辑: - 下单成功后跳转到支付页(而非订单状态页) - 传递参数:`orderId`、`tenantId`、`amount` --- ## 五、并发问题修复 ### 5.1 订单号并发 **问题**:多个订单同时生成可能产生重复订单号 **解决方案**: 1. 数据库层面:`order_no` 添加唯一索引 2. 代码层面:`generateOrderNo()` 添加 3 次重试 3. 防御性检查:插入前 `selectCount` 验证唯一性 ### 5.2 菜品销量并发 **问题**:先查后写导致并发时销量丢失 **解决方案**: - 使用原子 SQL 更新:`SET sales = IFNULL(sales, 0) + #{quantity}` - 避免"读取-计算-写入"的竞态条件 --- ## 六、支付流程时序图 ``` 顾客 H5前端 后端API 数据库 | | | | |--下单----------->| | | | |--创建订单-------->| | | | |--插入订单(status=0)| | | |<-------------------| | |<--返回orderId-----| | | | | | |--跳转支付页----->| | | | |--创建支付-------->| | | | |--插入pay_log | | | |<-------------------| | |<--返回支付参数----| | | | | | |--点击支付------->| | | | |--模拟支付成功---->| | | | |--更新pay_log | | | |--更新订单(status=1)| | | |<-------------------| | |<--返回支付结果----| | | | | | |--跳转订单状态--->| | | | |--轮询支付状态---->| | | | |--查询订单 | | | |<-------------------| | |<--返回已支付------| | | | | | ``` --- ## 七、后续对接真实支付 ### 7.1 微信支付(小程序) 1. 在 `HotelPayServiceImpl.createPay()` 中调用微信统一下单接口 2. 返回微信支付所需参数(prepay_id、sign 等) 3. 前端 `pay.vue` 中调用 `wx.requestPayment()` 4. 微信异步通知 `/hotel/open/pay/notify` 5. 验签、解密、更新订单状态 ### 7.2 支付宝支付(小程序) 1. 在 `HotelPayServiceImpl.createPay()` 中调用支付宝订单创建接口 2. 返回支付宝所需参数(trade_no 等) 3. 前端 `pay.vue` 中调用 `my.tradePay()` 4. 支付宝异步通知 `/hotel/open/pay/notify` 5. 验签、更新订单状态 ### 7.3 配置项 需要在 `application.yml` 中添加: ```yaml forge: pay: wechat: appId: xxx mchId: xxx apiKey: xxx alipay: appId: xxx privateKey: xxx alipayPublicKey: xxx ``` --- ## 八、文件清单 ### 后端新增文件 1. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/pay/domain/HotelPayLog.java` 2. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/pay/mapper/HotelPayLogMapper.java` 3. `forge-server/forge-business/forge-hotel/src/main/resources/mapper/business/hotel/pay/HotelPayLogMapper.xml` 4. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/pay/service/HotelPayService.java` 5. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/pay/service/impl/HotelPayServiceImpl.java` 6. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/pay/vo/PayResultVO.java` 7. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/controller/open/HotelPayController.java` 8. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/pay/task/PayTimeoutTask.java` ### 后端修改文件 1. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/order/constant/HotelOrderConstants.java` 2. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/order/domain/HotelOrder.java` 3. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/order/mapper/HotelOrderMapper.java` 4. `forge-server/forge-business/forge-hotel/src/main/resources/mapper/business/hotel/order/HotelOrderMapper.xml` 5. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/mapper/HotelDishMapper.java` 6. `forge-server/forge-business/forge-hotel/src/main/resources/mapper/business/hotel/HotelDishMapper.xml` 7. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/order/service/impl/HotelOrderServiceImpl.java` 8. `forge-server/forge-business/forge-hotel/src/main/java/com/mdframe/forge/business/core/hotel/order/vo/HotelOrderVO.java` ### 数据库脚本 1. `forge-server/db/migration/V1.0.105__add_hotel_order_payment_fields.sql` ### 前端新增文件 无 ### 前端修改文件 1. `forge-h5-ui/src/api/index.js` 2. `forge-h5-ui/src/pages/hotel/customer/pay.vue` 3. `forge-h5-ui/src/pages/hotel/customer/order-status.vue` 4. `forge-h5-ui/src/pages/hotel/customer/orders.vue` 5. `forge-h5-ui/src/pages/hotel/customer/order-confirm.vue` --- ## 九、测试建议 ### 9.1 功能测试 1. **正常支付流程**: - 下单 → 跳转支付页 → 点击支付 → 支付成功 → 订单状态变为"待接单" 2. **超时取消**: - 下单 → 等待 15 分钟 → 订单自动取消 → 状态变为"超时取消" 3. **主动取消**: - 下单 → 在支付页点击"取消订单" → 订单状态变为"超时取消" 4. **重复支付防护**: - 已支付订单再次调用支付接口 → 返回"订单已支付"错误 ### 9.2 并发测试 1. **订单号唯一性**: - 同时创建 100 个订单 → 验证订单号无重复 2. **菜品销量准确性**: - 同时完成 10 个包含同一菜品的订单 → 验证销量累加正确 ### 9.3 边界测试 1. **支付超时边界**: - 在第 14 分 59 秒支付 → 应该成功 - 在第 15 分 01 秒支付 → 应该返回"订单已超时" 2. **库存不足**: - 下单时菜品库存不足 → 应该返回错误 --- ## 十、已知问题与优化建议 ### 10.1 已知问题 1. **Mock 模式安全性**: - 当前 Mock 支付接口无需鉴权,生产环境需要移除或限制访问 2. **定时任务精度**: - 数据库轮询方案最大延迟 2 分钟,对业务影响可接受 ### 10.2 优化建议 1. **支付回调重试**: - 添加重试机制,防止网络问题导致回调失败 2. **支付对账**: - 添加每日对账任务,比对本地流水与第三方支付平台数据 3. **支付统计**: - 添加支付成功率、支付金额等统计报表 4. **退款流程**: - 完善已支付订单的退款流程(当前只支持未支付取消) --- ## 十一、总结 本次开发完成了酒店订单支付系统的核心功能,包括: - ✅ 数据库结构设计(支付字段、流水表、索引) - ✅ 后端支付服务(Mock 模式、回调处理、超时取消) - ✅ 前端支付页面(倒计时、环境识别、支付调用) - ✅ 并发问题修复(订单号、菜品销量) - ✅ 订单状态管理(待支付、已支付、超时取消) 当前系统已支持完整的 Mock 支付流程,后续可无缝对接微信支付、支付宝小程序支付。所有代码已通过编译验证,无语法错误。 --- **文档版本**:v1.0 **最后更新**:2026-08-17 **维护人员**:AI Assistant