酒店订单支付系统开发文档.md 18 KB

酒店订单支付系统开发文档

开发时间: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 表新增字段

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

// 订单状态
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

新增方法:

List<Long> selectExpiredUnpaidOrderIds()

查询所有超时未支付的订单 ID。

HotelOrderMapper.xml

  • resultMapOrderColumns:添加支付相关字段映射
  • selectDashboardcountTodayOrderssumTodayRevenue:改为只统计已支付订单(pay_status = 1
  • selectOrderPage:筛选条件从 payMethod 改为 payStatuspaySource
  • 新增 selectExpiredUnpaidOrderIds:查询超时订单

HotelDishMapper.java

新增方法:

int incrementSales(@Param("dishId") Long dishId, @Param("quantity") Integer quantity)

原子更新菜品销量。

HotelDishMapper.xml

新增 SQL:

<update id="incrementSales">
    UPDATE hotel_dish
    SET sales = IFNULL(sales, 0) + #{quantity},
        update_time = NOW()
    WHERE id = #{dishId} AND del_flag = 0
</update>

HotelPayLogMapper.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(新建)

支付服务接口,定义核心方法:

Map<String, Object> 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(新建)

@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:

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

状态标签更新

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

更新跳转逻辑:

  • 下单成功后跳转到支付页(而非订单状态页)
  • 传递参数:orderIdtenantIdamount

五、并发问题修复

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 中添加:

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