订单与支付模块.md 14 KB

订单与支付模块业务说明

本文档描述订单与支付核心链路,涵盖订单生命周期、支付体系、金额字段定义与退款逻辑。 帮助开发人员快速理解"为什么这样设计"以及各环节的特殊处理方式。


一、订单核心实体

1.1 Order 实体 (haha-entity/.../Order.java)

字段 类型 说明
id Long 雪花算法主键
orderNo String 设备端订单号(设备回调时写入)
outTradeNo String 外部交易号
activityId String 哈哈平台活动ID(用于幂等防重复创建)
userId Long 用户ID
deviceId String 设备SN号
totalAmount BigDecimal 订单总金额(原价),设备回调时写入
discountAmount BigDecimal 订单优惠金额(营销活动 + 优惠券),默认0
paidAmount BigDecimal 实付总金额 = totalAmount - discountAmount
payStatus String 支付状态:UNPAID / PAID / REFUND
payType String 支付方式
payChannel String 支付渠道:wechat / alipay / alipay_credit / balance / wechat_payscore
transactionId String 第三方交易号
refundStatus String 退款状态
refundAmount BigDecimal 退款金额
refundTime LocalDateTime 退款时间
refundReason String 退款原因
payScoreOrderId String 支付分服务订单号(微信侧)
payScoreState String 支付分状态:CREATED / DOING / USER_PAYING / DONE / REVOKED
payScoreFailReason String 支付分扣款失败原因(记录微信 API 返回的错误信息)
serviceId String 支付分服务ID
serviceStartTime LocalDateTime 服务开始时间
serviceEndTime LocalDateTime 服务结束时间
status Integer 订单状态:0-已取消 1-待支付 2-已完成 3-已关闭
orderType String 订单类型:OUT-正常消费,IN-异物放入
items String 商品识别信息JSON
videoUrl String 识别视频URL
confidence BigDecimal AI识别置信度

1.2 订单状态枚举 (OrderStatus)

code 枚举值 说明
0 CANCELLED 已取消
1 PENDING_PAYMENT 待支付
2 COMPLETED 已完成
3 CLOSED 已关闭

1.3 支付状态枚举 (PayStatus)

code 枚举值 说明
UNPAID 未支付 订单已创建,等待支付
PAID 已支付 扣费成功
REFUND 已退款 已完成退款

二、订单完整生命周期

用户扫码开门
    ↓
DoorRecord 记录开门(openType=OUT, doorStatus=OPENED)
    ↓
用户购物,关门
    ↓
设备AI识别回调 handleOrcResult()
    ↓
创建订单 createOrderFromRecognition()
    - totalAmount = 0, discountAmount = 0, paidAmount = 0
    - status = PENDING_PAYMENT, payStatus = UNPAID
    ↓
设备端计算金额(商品原价 - 营销活动优惠)
    ↓
订单回调 handleOrderCallback()
    - 写入 totalAmount(设备传来的已含营销活动优惠的金额)
    - 初始化 discountAmount = 0, paidAmount = totalAmount
    ↓
优惠券自动扣减 processCouponDiscount()
    - 如有可用优惠券:计算优惠 → 更新 discountAmount / paidAmount
    ↓
支付分扣费 / 其他支付渠道
    - 使用 paidAmount 进行扣费
    ↓
支付回调 → 更新 payStatus = PAID, status = COMPLETED
    ↓
订单完成 → 触发邀请奖励激活

关键说明

  1. AI识别结果不含价格sku_list 只包含 {code, quantity},商品价格在设备端查询
  2. 订单创建时金额为0:因AI识别回调不含价格,金额在订单回调时由设备端传入
  3. 营销活动优惠在设备端计算:设备查询适用活动并计算折扣,传回的是扣减后金额
  4. 优惠券优惠在服务端计算HahaCallbackServiceImpl.processCouponDiscount() 自动匹配

业务背景与设计考量

为什么AI识别结果不含价格?

哈哈平台(设备端SDK)的AI视觉识别只负责识别"哪个商品被拿走了",返回商品编码(code)和数量(quantity),不涉及定价逻辑。商品价格由哈哈平台根据设备绑定的门店信息独立维护,服务端无法在AI识别回调时获取价格。这是设备平台架构决定的——识别与定价解耦

为什么营销活动优惠在设备端计算?

设备端在用户关门后需要立即展示应付金额,如果等服务端计算再返回,用户等待时间过长。此外,哈哈平台设备端本身具备查询营销活动和计算优惠的能力。因此采用设备端计算、服务端信任的策略:

  • 设备端:查询营销活动 → 计算优惠 → 回调通知服务端
  • 服务端:MarketingRuleService 作为备用校验能力,但当前不覆盖主流程

为什么优惠券是自动扣减而非用户手动选择?

智能售卖机的购物场景是用户关门后自动结算,没有购物车确认环节,用户无法在结算时手动选择优惠券。因此系统在订单回调时自动为用户匹配最优优惠券(优先使用最先到期的符合条件的券),这是业务场景决定的特殊处理。

为什么订单创建时金额为0?

AI识别回调(handleOrcResult)只告知"哪些商品被拿走了",不含价格信息。价格和最终金额要等设备端完成计算后通过订单回调(handleOrderCallback)传回。两步回调之间有时间差,期间订单金额为0是正常状态,不是异常。

订单幂等性保证

通过 activityId(哈哈平台活动ID)保证订单不重复创建。AI识别回调可能因网络重试多次到达,createOrderFromRecognition() 先查询是否已有该 activityId 的订单,有则直接返回,避免重复创建。


三、金额字段体系

3.1 三级金额定义

字段 含义 计算方式 赋值时机
totalAmount 订单总金额(原价) 设备端计算 订单回调时由设备传入
discountAmount 订单优惠金额 营销活动优惠 + 优惠券优惠 优惠券扣减时写入
paidAmount 实付总金额 totalAmount - discountAmount 优惠券扣减后计算

为什么需要 discountAmount 和 paidAmount 字段?

业务背景:早期订单表只有 totalAmount,运营平台和小程序展示“实付金额”时直接取 totalAmount。但引入优惠券后,totalAmount 存的是设备端传来的金额(已含营销活动优惠但不含优惠券优惠),导致前端显示的“实付”不正确。

解决方案:增加 discountAmount(记录优惠总额)和 paidAmount(记录实付总额),使三级金额语义清晰,前端取值简单可靠。

特殊处理

  • 设备端传来的 totalAmount 已包含营销活动优惠(如首单立减5元后传95元),但营销活动优惠金额不单独持久化到订单表
  • discountAmount 目前仅记录服务端计算的优惠券优惠部分
  • 历史订单迁移:UPDATE t_order SET discount_amount = 0, paid_amount = total_amount WHERE ...

3.2 金额赋值阶段

阶段 totalAmount discountAmount paidAmount
创建订单 0 0 0
订单回调 设备传来 0 = totalAmount
优惠券扣减 不变 优惠券金额 = total - discount
支付扣费 不变 不变 使用此金额

3.3 前端兼容性处理

所有前端展示优先使用 paidAmount,兼容历史数据:

// 小程序
const displayAmount = order.paidAmount || order.totalAmount || 0;

// 运营平台
const amount = row.paidAmount || (row.totalAmount || 0) - (row.discountAmount || 0);

退款页面使用 totalAmount(基于订单原价计算退款),不使用 paidAmount

为什么退款基于 totalAmount 而非 paidAmount?

退款金额应基于用户实际支付的金额来计算。但当前场景下,paidAmount 是服务端计算的优惠券扣减后金额,而设备端可能还有营销活动优惠未体现在 discountAmount 中。为避免退款金额计算混乱,退款暂时基于 totalAmount(设备端传来的已含营销优惠的金额),后续如需精确退款需结合 paidAmount 使用。


四、支付体系

4.1 支付渠道 (PaymentChannel)

code 枚举值 说明
wechat WECHAT 微信支付(JSAPI)
alipay ALIPAY 支付宝支付
alipay_credit ALIPAY_CREDIT 支付宝信用支付(芝麻信用)
balance BALANCE 余额支付
wechat_payscore WECHAT_PAYSCORE 微信支付分(先享后付)

4.2 支付架构

PaymentService (门面)
    ├── PaymentChannelFactory (渠道工厂)
    ├── WxPayStrategy (微信支付策略)
    ├── AliPayStrategy (支付宝策略)
    ├── AliCreditPayStrategy (信用支付策略)
    └── PayScoreService (支付分服务)
  • 策略模式PaymentStrategy 接口,每种渠道一个实现
  • 门面服务PaymentServiceImpl 统一调度,上层无需感知渠道差异

4.3 微信支付分流程(核心支付方式)

为什么微信支付分是核心支付方式?

智能售卖机场景下,用户先开门购物,后结算付款,属于典型的“先享后付”场景。微信支付分天然支持这种模式:

  1. 用户扫码时创建支付分服务订单(不扣款,仅授权)
  2. 用户购物完成后,服务端完结支付分订单(自动扣款)
  3. 无需用户在购物后手动支付,体验最流畅

相比传统支付(用户需主动扫码付款),支付分消除了“忘记付款”的风险,也提升了转化率。

扫码开门的支付分强制校验

为保证扣费安全性,扫码开门流程中会强制检查用户是否已开通微信支付分:

  • 未开通 → 引导用户授权开通 → 开通成功后才能开门
  • 已开通 → 创建支付分服务订单 → 开门
  • 这保证了每个开门用户都具备扣费能力,避免“开门购物但无法扣款”的风险

    用户扫码 → 检查支付分开通状态
    ↓
    未开通 → enablePayscore() → 引导用户授权
    ↓
    已开通 → createUserPayScoreOrder() → 创建支付分服务订单
    ↓
    用户开门购物 → 关门 → 订单回调
    ↓
    completeUserPayScoreOrder() → 完结支付分(自动扣款)
    ↓
    扣款成功(DONE) ──→ 微信回调 PAYSCORE.USER_PAID ──→ 订单完成
    扣款失败(余额不足等) ──→ 本地标记 USER_PAYING,记录 failReason
    ↓                        ↓
    微信支付分自动推送待支付提醒     PayScoreSyncTask 每 5 分钟同步状态
    ↓                        ↓
    用户完成支付 → 微信回调 → DONE → 订单完成
    

支付分服务订单状态(PayScoreState 枚举)

  • CREATED:已创建(用户扫码时创建服务订单)
  • DOING:进行中(用户确认使用服务,正在购物)
  • USER_PAYING:用户支付中(扣款已发起但用户尚未完成支付,如银行卡余额不足)
  • DONE:已完成(扣款成功)
  • REVOKED:已取消(用户或商户取消服务订单)

扣款失败的 USER_PAYING 处理

当完结支付分订单时,若用户微信绑定的银行卡余额不足,微信 API 返回状态为 USER_PAYING,此时:

  1. 服务端标记payScoreState 设为 USER_PAYINGpayScoreFailReason 记录失败原因
  2. 微信端自动催收:微信支付分平台会自动向用户推送"待支付提醒"通知,无需商户额外开发
  3. 定时状态同步PayScoreSyncTask 每 5 分钟查询 USER_PAYING 订单并调用微信 queryServiceOrder 同步最新状态
  4. 超时预警:超过 24 小时仍未支付则记录 warn 日志,供运营关注处理

    扣款失败(USER_PAYING)
    ↓
    本地: payScoreState=USER_PAYING, payScoreFailReason=记录原因
    ↓
    微信端自动推送待支付提醒(微信原生能力)
    ↓
    PayScoreSyncTask 每 5 分钟同步
    ↓
    用户支付成功 → 微信回调 PAYSCORE.USER_PAID → DONE → 订单完成
    用户超时未付 → 24h 后 warn 日志 → 运营介入
    

4.4 退款流程

运营平台发起退款
    ↓
OrderServiceImpl.refund()
    - 验证订单状态和支付状态
    - 调用 PaymentService.refund()
    - 更新 refundStatus / refundAmount / refundTime

退款特殊处理

  • 支付分退款:支付分订单完结后走微信退款接口,需传入原始支付分订单号
  • 优惠券不可退回:订单退款时,已使用的优惠券状态不会回滚为“未使用”,这是当前的业务规则,如需退回需额外开发
  • 部分退款:运营人员可输入退款金额,部分退款是允许的(如仅退某个商品)
  • 异物放入订单(orderType=IN)不展示给用户端,退款需在运营平台操作

五、核心代码文件索引

文件 职责
haha-entity/.../Order.java 订单实体
haha-entity/.../OrderGoods.java 订单商品明细
haha-service/.../OrderServiceImpl.java 订单服务(创建、查询、退款)
haha-service/.../HahaCallbackServiceImpl.java 设备回调处理(订单回调、优惠券扣减)
haha-service/.../payment/PaymentService.java 支付门面接口
haha-service/.../payment/impl/PaymentServiceImpl.java 支付门面实现
haha-service/.../payment/payscore/PayScoreServiceImpl.java 支付分服务实现(含扣款失败 USER_PAYING 处理)
haha-service/.../payment/payscore/impl/WxPayScoreStrategy.java 微信支付分 API 策略实现
haha-admin/.../task/PayScoreSyncTask.java 支付分 USER_PAYING 状态定时同步任务
haha-admin/.../OrderController.java 运营平台订单API
haha-miniapp/.../OrderController.java 小程序订单API
haha-miniapp/.../PaymentController.java 小程序支付API
haha-miniapp/.../PayScoreController.java 小程序支付分API
haha-common/.../constant/OrderConstants.java 订单常量定义
haha-common/.../enums/OrderStatus.java 订单状态枚举
haha-common/.../enums/PayStatus.java 支付状态枚举
haha-common/.../enums/PaymentChannel.java 支付渠道枚举
haha-common/.../enums/PayScoreState.java 支付分状态枚举
docs/database/add_payscore_fail_reason.sql 支付分扣款失败原因字段迁移 SQL