# 订单与支付模块业务说明

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

---

## 一、订单核心实体

### 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`，兼容历史数据：

```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() → 完结支付分（自动扣款）
    ↓
  扣款成功(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_PAYING`，`payScoreFailReason` 记录失败原因
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 |
