本文档描述订单与支付核心链路,涵盖订单生命周期、支付体系、金额字段定义与退款逻辑。 帮助开发人员快速理解"为什么这样设计"以及各环节的特殊处理方式。
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 | 第三方交易号 |
| 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识别置信度 |
OrderStatus)| code | 枚举值 | 说明 |
|---|---|---|
| 0 | CANCELLED | 已取消 |
| 1 | PENDING_PAYMENT | 待支付 |
| 2 | COMPLETED | 已完成 |
| 3 | CLOSED | 已关闭 |
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
↓
订单完成 → 触发邀请奖励激活
sku_list 只包含 {code, quantity},商品价格在设备端查询HahaCallbackServiceImpl.processCouponDiscount() 自动匹配哈哈平台(设备端SDK)的AI视觉识别只负责识别"哪个商品被拿走了",返回商品编码(code)和数量(quantity),不涉及定价逻辑。商品价格由哈哈平台根据设备绑定的门店信息独立维护,服务端无法在AI识别回调时获取价格。这是设备平台架构决定的——识别与定价解耦。
设备端在用户关门后需要立即展示应付金额,如果等服务端计算再返回,用户等待时间过长。此外,哈哈平台设备端本身具备查询营销活动和计算优惠的能力。因此采用设备端计算、服务端信任的策略:
MarketingRuleService 作为备用校验能力,但当前不覆盖主流程智能售卖机的购物场景是用户关门后自动结算,没有购物车确认环节,用户无法在结算时手动选择优惠券。因此系统在订单回调时自动为用户匹配最优优惠券(优先使用最先到期的符合条件的券),这是业务场景决定的特殊处理。
AI识别回调(handleOrcResult)只告知"哪些商品被拿走了",不含价格信息。价格和最终金额要等设备端完成计算后通过订单回调(handleOrderCallback)传回。两步回调之间有时间差,期间订单金额为0是正常状态,不是异常。
通过 activityId(哈哈平台活动ID)保证订单不重复创建。AI识别回调可能因网络重试多次到达,createOrderFromRecognition() 先查询是否已有该 activityId 的订单,有则直接返回,避免重复创建。
| 字段 | 含义 | 计算方式 | 赋值时机 |
|---|---|---|---|
| totalAmount | 订单总金额(原价) | 设备端计算 | 订单回调时由设备传入 |
| discountAmount | 订单优惠金额 | 营销活动优惠 + 优惠券优惠 | 优惠券扣减时写入 |
| paidAmount | 实付总金额 | totalAmount - discountAmount | 优惠券扣减后计算 |
业务背景:早期订单表只有 totalAmount,运营平台和小程序展示“实付金额”时直接取 totalAmount。但引入优惠券后,totalAmount 存的是设备端传来的金额(已含营销活动优惠但不含优惠券优惠),导致前端显示的“实付”不正确。
解决方案:增加 discountAmount(记录优惠总额)和 paidAmount(记录实付总额),使三级金额语义清晰,前端取值简单可靠。
特殊处理:
totalAmount 已包含营销活动优惠(如首单立减5元后传95元),但营销活动优惠金额不单独持久化到订单表discountAmount 目前仅记录服务端计算的优惠券优惠部分UPDATE t_order SET discount_amount = 0, paid_amount = total_amount WHERE ...| 阶段 | totalAmount | discountAmount | paidAmount |
|---|---|---|---|
| 创建订单 | 0 | 0 | 0 |
| 订单回调 | 设备传来 | 0 | = totalAmount |
| 优惠券扣减 | 不变 | 优惠券金额 | = total - discount |
| 支付扣费 | 不变 | 不变 | 使用此金额 |
所有前端展示优先使用 paidAmount,兼容历史数据:
// 小程序
const displayAmount = order.paidAmount || order.totalAmount || 0;
// 运营平台
const amount = row.paidAmount || (row.totalAmount || 0) - (row.discountAmount || 0);
退款页面使用 paidAmount 作为退款金额上限,退款金额不能超过用户实际支付的金额。
退款金额基于用户实际支付的金额(paidAmount)来计算和校验:
paidAmount(实付金额)paidAmountPaymentChannel)| code | 枚举值 | 说明 |
|---|---|---|
| 微信支付(JSAPI) | ||
| alipay | ALIPAY | 支付宝支付 |
| alipay_credit | ALIPAY_CREDIT | 支付宝信用支付(芝麻信用) |
| balance | BALANCE | 余额支付 |
| wechat_payscore | WECHAT_PAYSCORE | 微信支付分(先享后付) |
PaymentService (门面)
├── PaymentChannelFactory (渠道工厂)
├── WxPayStrategy (微信支付策略)
├── AliPayStrategy (支付宝策略)
├── AliCreditPayStrategy (信用支付策略)
└── PayScoreService (支付分服务)
PaymentStrategy 接口,每种渠道一个实现PaymentServiceImpl 统一调度,上层无需感知渠道差异智能售卖机场景下,用户先开门购物,后结算付款,属于典型的“先享后付”场景。微信支付分天然支持这种模式:
相比传统支付(用户需主动扫码付款),支付分消除了“忘记付款”的风险,也提升了转化率。
为保证扣费安全性,扫码开门流程中会强制检查用户是否已开通微信支付分:
这保证了每个开门用户都具备扣费能力,避免“开门购物但无法扣款”的风险
用户扫码 → 检查支付分开通状态
↓
未开通 → enablePayscore() → 引导用户授权
↓
已开通 → createUserPayScoreOrder() → 创建支付分服务订单
↓
用户开门购物 → 关门 → 订单回调
↓
completeUserPayScoreOrder() → 完结支付分(自动扣款)
↓
扣款成功(DONE) ──→ 微信回调 PAYSCORE.USER_PAID ──→ 订单完成
扣款失败(余额不足等) ──→ 本地标记 USER_PAYING,记录 failReason
↓ ↓
微信支付分自动推送待支付提醒 PayScoreSyncTask 每 5 分钟同步状态
↓ ↓
用户完成支付 → 微信回调 → DONE → 订单完成
支付分服务订单状态(PayScoreState 枚举):
当完结支付分订单时,若用户微信绑定的银行卡余额不足,微信 API 返回状态为 USER_PAYING,此时:
payScoreState 设为 USER_PAYING,payScoreFailReason 记录失败原因PayScoreSyncTask 每 5 分钟查询 USER_PAYING 订单并调用微信 queryServiceOrder 同步最新状态超时预警:超过 24 小时仍未支付则记录 warn 日志,供运营关注处理
扣款失败(USER_PAYING)
↓
本地: payScoreState=USER_PAYING, payScoreFailReason=记录原因
↓
微信端自动推送待支付提醒(微信原生能力)
↓
PayScoreSyncTask 每 5 分钟同步
↓
用户支付成功 → 微信回调 PAYSCORE.USER_PAID → DONE → 订单完成
用户超时未付 → 24h 后 warn 日志 → 运营介入
退款功能使用独立的 t_refund 和 t_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 │ POST │ POST │
│ /payment │ /order/{id} │ /refund/{id} │
│ /refund │ /refund │ /review │
├──────────────┴──────────────────┴───────────────────┤
│ PaymentServiceImpl.refund() │
│ ┌─────────────────────────────────────────────┐ │
│ │ 有 products/refundAmount? │ │
│ │ ├── 是 → RefundService.createRefundRecord() │ │
│ │ │ 创建 t_refund + t_refund_item │ │
│ │ │ → 执行第三方退款(金额=计算后的退款金额) │ │
│ │ │ → markRefunded() + 更新订单汇总字段 │ │
│ │ └── 否 → 全额退款(兼容旧行为) │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
每笔退款在 t_refund_item 中记录了退款商品明细(order_goods_id、product_id、product_name、quantity、price),可通过 refund_id 关联查询。管理后台退款详情页展示的是实际退款商品,而非订单全部商品。
| 文件 | 职责 |
|---|---|
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 迁移脚本 |