# 订单与支付模块业务说明 > 本文档描述订单与支付核心链路,涵盖订单生命周期、支付体系、金额字段定义与退款逻辑。 > 帮助开发人员快速理解"为什么这样设计"以及各环节的特殊处理方式。 --- ## 一、订单核心实体 ### 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 | | 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`,兼容历史数据: ```typescript // 小程序 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() → 完结支付分(自动扣款) ↓ 支付分回调 → 更新订单 payStatus = PAID ``` **支付分服务订单状态**: - CREATED:已创建 - DOING:服务中(用户正在购物) - USER_PAYING:用户支付中 - DONE:已完成(扣款成功) ### 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/PayScoreService.java` | 支付分服务接口 | | `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` | 支付渠道枚举 |