订单与支付模块.md 20 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 / PARTIAL_REFUND / REFUND
payType String 支付方式
payChannel String 支付渠道:wechat / alipay / alipay_credit / balance / wechat_payscore
transactionId String 第三方交易号
currentRefundId Long 当前退款记录ID(关联 t_refund.id,冗余便于快速查询)
refundStatus String 退款状态(PENDING/REFUNDED/REJECTED,汇总字段)
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 已支付 扣费成功,未发生退款
PARTIAL_REFUND 部分退款 累计退款 > 0 且 < 实付金额,可继续退款
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);

退款页面使用 paidAmount 作为退款金额上限,退款金额不能超过用户实际支付的金额。

为什么退款基于 paidAmount?

退款金额基于用户实际支付的金额(paidAmount)来计算和校验:

  • 全额退款时退 paidAmount(实付金额)
  • 部分退款时,累计退款金额不能超过 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 退款流程

退款数据模型

退款功能使用独立的 t_refundt_refund_item 表管理,不再仅依赖 t_order 上的汇总字段。

t_refund(退款记录表)

字段 类型 说明
id BIGINT 退款记录ID(雪花算法)
order_id BIGINT 订单ID
order_no VARCHAR(64) 订单号
refund_no VARCHAR(64) 退款单号(RF + orderNo + timestamp)
refund_amount DECIMAL(10,2) 本次退款金额
total_amount DECIMAL(10,2) 订单原始总金额
status VARCHAR(20) 退款状态:PENDING/REFUNDED/REJECTED
reason VARCHAR(500) 退款原因
remark VARCHAR(500) 审核备注
transaction_id VARCHAR(100) 第三方退款交易号
refund_time DATETIME 退款完成时间
reviewer_id BIGINT 审核人ID
review_time DATETIME 审核时间
create_time DATETIME 创建时间
update_time DATETIME 更新时间

t_refund_item(退款商品明细表)

字段 类型 说明
id BIGINT 退款商品明细ID
refund_id BIGINT 退款记录ID(关联 t_refund.id)
order_goods_id BIGINT 订单商品明细ID(关联 t_order_goods.id)
product_id BIGINT 商品ID
product_name VARCHAR(255) 商品名称
pic VARCHAR(500) 商品图片
quantity INT 退款数量
price DECIMAL(10,2) 商品单价
refund_amount DECIMAL(10,2) 退款金额(quantity * price)

t_order 汇总字段(旧字段保留,冗余便于订单列表直接展示):

字段 说明
currentRefundId 当前退款记录ID
refundStatus 退款状态汇总
refundAmount 退款金额汇总
refundTime 退款时间
refundReason 退款原因

退款金额三种模式

优先级 模式 触发条件
1(最高) 按商品退款 传入 products 列表,金额 = Σ(quantity × price)
2 自定义金额退款 传入 refundAmount
3 全额退款 两者都不传,金额 = paidAmount

金额校验

  • 退款金额 ≤ paidAmount - 该订单历史累计已退款金额
  • 逐商品校验:退款数量 ≤ 购买数量 - 该商品已退款数量
  • 确保多次部分退款总额不超过实付金额,已全额退款的商品不可重复选择

退款流程

用户端和管理后台走不同的退款路径:

用户端(小程序):提交申请 → 等待审核

用户提交退款(refund.vue)
  → POST /payment/refund
  → PaymentServiceImpl.refund(Map)  // immediate=false(默认)
  → RefundService.createRefundRecord()
      ├── 创建 t_refund(status=PENDING)
      ├── 创建 t_refund_item(商品明细)
      ├── 更新 t_order.refundStatus = PENDING
      └── 返回”退款申请已提交,请等待审核”
  → 管理员在运营平台审核
      ├── 通过 → approveRefund()
      │         → 执行第三方退款 API
      │         → t_refund.status = REFUNDED
      │         → t_order.refundAmount 累加
      │         → 累计 ≥ 实付 → payStatus = REFUND
      │         → 累计 < 实付 → payStatus = PARTIAL_REFUND
      └── 拒绝 → rejectRefund()
                → t_refund.status = REJECTED
                → 清除 t_order.refundStatus / currentRefundId
                → 保留 t_order.refundAmount(累计已退金额)
                → 用户可重新提交申请

管理后台直接退款:即时执行

管理员在订单列表点击退款(hook.tsx handleRefund)
  → POST /order/{id}/refund
  → PaymentServiceImpl.refund(Map)  // immediate=true
  → RefundService.createRefundRecord()
  → 立即执行第三方退款 API
  → t_refund.status = REFUNDED

多次退款

订单实付 100 元,购 3 件商品:
  第一次退 2 件,金额 60 元 → payStatus = PARTIAL_REFUND,refundAmount = 60
  第二次退 1 件,金额 40 元 → 累计 100 ≥ 100 → payStatus = REFUND
  退款入口关闭,已退商品不可再选

退款追溯

  • 每笔退款在 t_refund_item 中记录了退款商品明细(order_goods_idproduct_idproduct_namequantityprice),可通过 refund_id 关联查询
  • 管理后台退款详情页展示的是实际退款商品,而非订单全部商品
  • 小程序订单详情页展示退款记录时间线(绿色=已退款,红色=已拒绝,橙色=审核中),含每次申请的金额、原因、商品明细和拒绝原因

退款特殊处理

  • 支付分退款:支付分订单完结后走微信退款接口,需传入原始支付分订单号。每次退款生成唯一 out_refund_no(含时间戳),避免重试时参数不一致导致微信 INVALID_REQUEST
  • 优惠券不可退回:订单退款时,已使用的优惠券状态不会回滚为”未使用”,这是当前的业务规则
  • 部分退款:支持选择商品退款(金额由商品自动计算)或自定义金额退款,多次退款累计不超过实付金额。已全额退款的商品前端禁选、后端校验
  • 异物放入订单(orderType=IN)不展示给用户端,退款需在运营平台操作

五、核心代码文件索引

文件 职责
haha-entity/.../Order.java 订单实体
haha-entity/.../OrderGoods.java 订单商品明细
haha-entity/.../Refund.java 退款记录实体(t_refund)
haha-entity/.../RefundItem.java 退款商品明细实体(t_refund_item)
haha-entity/.../dto/RefundDTO.java 退款请求 DTO(含 products、refundAmount)
haha-mapper/.../RefundMapper.java 退款 Mapper
haha-mapper/.../RefundItemMapper.java 退款商品明细 Mapper
haha-service/.../RefundService.java 退款服务接口
haha-service/.../impl/RefundServiceImpl.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-admin/.../RefundApplicationController.java 运营平台退款申请管理API
haha-miniapp/.../OrderController.java 小程序订单API
haha-miniapp/.../PaymentController.java 小程序支付API(含退款接口)
haha-miniapp/.../PayScoreController.java 小程序支付分API
haha-common/.../constant/OrderConstants.java 订单常量定义
haha-common/.../constant/PaymentConstants.java 支付常量(退款前缀、退款期限等)
haha-common/.../enums/OrderStatus.java 订单状态枚举
haha-common/.../enums/PayStatus.java 支付状态枚举
haha-common/.../enums/PaymentChannel.java 支付渠道枚举
haha-common/.../enums/RefundStatus.java 退款状态枚举
haha-common/.../enums/PayScoreState.java 支付分状态枚举
haha-common/.../vo/RefundApplicationVO.java 退款申请视图对象
haha-common/.../vo/RefundProductVO.java 退款商品视图对象
haha-admin/.../db/migration/V5__create_refund_tables.sql 退款表 DDL 迁移脚本