# 支付交易管理 - PRD

## 文档信息

| 项目 | 内容 |
|------|------|
| 文档版本 | v1.1 |
| 创建日期 | 2026-02-26 |
| 模块名称 | 支付交易管理 |
| 文档状态 | 已评审修订 |

---

## 一、模块概述

### 1.1 模块定位

支付交易管理模块是支付中台的核心交易模块,负责处理所有支付相关的交易操作,包括统一下单、支付查询、退款处理、回调通知等。本模块作为业务系统与支付服务商之间的桥梁,屏蔽不同服务商的接口差异,提供统一的支付能力。

### 1.2 核心功能

1. **统一下单** - 接收业务系统支付请求,调用路由服务,生成支付订单
   - 支持用户主动发起的线上支付(普通支付、牌照支付)
   - 支持系统统一唤起的自动扣款(生活缴费、电子托收)
2. **支付查询** - 查询支付订单状态和详情
3. **退款处理** - 处理退款请求,调用服务商退款接口
4. **回调处理** - 接收服务商回调,更新订单状态,通知业务系统
5. **支付校验** - 验证支付参数的合法性和完整性
6. **订单超时关闭** - 自动关闭超时未支付订单

### 1.3 使用角色

- **业务系统** - 调用支付接口发起支付、查询订单、申请退款
- **C端用户** - 完成支付操作
- **支付服务商** - 处理支付、回调通知支付中台
- **财务人员** - 查询订单、处理退款

---

## 二、功能详细说明

### 2.1 统一下单

#### 2.1.1 功能描述

业务系统调用支付中台统一下单接口,传递订单信息、用户信息、支付金额等参数,支付中台根据路由规则选择支付服务商,调用服务商下单接口,返回支付信息供用户完成支付。

**支持的支付方式:**

1. **用户主动发起的线上支付** (普通支付、牌照支付)
   - 用户操作:扫码/点击支付按钮
   - 支付流程:实时跳转到支付页面完成支付
   - 是否需要签约:否
   - 适用场景:临时性缴费、商城购物等

2. **系统统一唤起的自动扣款** (生活缴费、电子托收)
   - 用户操作:无需操作(提前已签约授权)
   - 支付流程:系统按照业务规则自动发起扣款
   - 是否需要签约:是(必须提前签约)
   - 适用场景:定期缴费(如每月物业费、停车费)

#### 2.1.2 下单流程

```
业务系统
  │
  ├─ 1. 调用支付中台下单接口
  │    └─ POST /api/payment/order/create
  │
  ├─ 传递参数:
  │    ├─ community_id: 小区ID (必填)
  │    ├─ business_type: 业务类型 (必填,如property_fee/parking/mall等)
  │    ├─ business_order_no: 业务订单号 (必填,唯一)
  │    ├─ amount: 支付金额 (必填,单位:分)
  │    ├─ user_id: 用户ID (必填)
  │    ├─ description: 订单描述 (必填)
  │    ├─ payment_method_id: 指定支付方式ID (可选)
  │    ├─ payment_scene_id: 指定业务场景ID (可选)
  │    ├─ sign_no: 签约协议号 (条件必填)
  │    │    └─ 说明: 当支付方式need_sign=true时必填(如生活缴费、电子托收场景)
  │    ├─ notify_url: 业务系统回调地址 (必填)
  │    └─ extra_data: 扩展字段 (可选,JSON格式)
  │
  ▼
支付中台 - 下单处理
  │
  ├─ 2. 参数校验
  │    ├─ 校验必填参数
  │    ├─ 校验金额 (必须>0)
  │    ├─ 校验业务订单号唯一性
  │    └─ 校验notify_url格式
  │
  ├─ 3. 调用路由服务
  │    ├─ 传递: community_id, business_type, payment_method_id
  │    └─ 获取: provider_id, payment_method_code, sub_merchant_no等
  │
  ├─ 4. 签约验证 (如果是签约支付)
  │    ├─ 查询支付方式是否需要签约 (need_sign)
  │    ├─ 如果需要签约:
  │    │    ├─ 验证 sign_no 是否传递
  │    │    └─ 验证用户签约状态是否有效
  │    │    ├─ 验证签约状态必须为 signed (v1.1.1增强)
  │    │    ├─ 如果状态为 invalid,返回错误码 PAY-3000-02:"签约已失效,请重新签约"
  │    │    ├─ 如果状态为 cancelled,返回错误码 PAY-3000-03:"签约已解约,请重新签约"
  │    └─ 验证通过后继续
  │
  ├─ 5. 生成支付中台订单号
  │    └─ 格式: PAY{YYYYMMDD}{随机数} (如: PAY20260225123456789)
  │
  ├─ 6. 调用服务商下单接口
  │    ├─ 根据 payment_method_code 选择对应的下单方式
  │    │    ├─ NATIVE: 生成二维码
  │    │    ├─ JSAPI: 返回JSAPI参数
  │    │    ├─ H5: 返回跳转链接
  │    │    ├─ APP: 返回APP调起参数
  │    │    └─ BANK_COLLECT: 发起托收扣款
  │    │
  │    └─ 获取服务商返回信息:
  │         ├─ provider_order_no: 服务商订单号
  │         └─ pay_info: 支付信息 (二维码/链接/参数)
  │
  ├─ 7. 保存支付订单
  │    ├─ 订单状态: pending (待支付)
  │    ├─ 保存所有订单信息
  │    └─ 关联服务商订单号
  │
  ├─ 8. 返回支付信息给业务系统
  │    ├─ platform_order_no: 支付中台订单号
  │    ├─ payment_provider: 实际使用的支付服务商
  │    ├─ payment_method: 实际使用的支付方式
  │    ├─ pay_info: 支付信息
  │    │    ├─ type: qrcode/url/jsapi/app
  │    │    └─ data: 具体支付数据
  │    └─ expire_time: 订单过期时间
  │
  ▼
业务系统
  │
  └─ 9. 引导用户完成支付
       ├─ 扫码支付: 展示二维码
       ├─ H5支付: 跳转支付链接
       ├─ JSAPI支付: 调起微信/支付宝支付
       └─ APP支付: 调起APP支付
```

#### 2.1.3 业务规则

1. **订单唯一性**
   - 业务订单号(business_order_no)必须唯一
   - 同一业务订单号不能重复下单
   - 支付中台订单号(platform_order_no)系统自动生成,保证唯一

2. **金额规则**
   - 金额单位:分
   - 金额必须 > 0
   - 金额最大值:999999999分 (约1000万元)

3. **沙箱模式金额处理** (v1.1.2新增)

   **沙箱模式判断逻辑:**
   - 子商户模式:根据 platform_provider_config.sandbox_mode 判断
   - 独立商户模式:根据 community_payment_config.sandbox_mode 判断

   **金额替换规则:**
   - 沙箱模式下,**实际调用服务商的金额统一为1分钱(0.01元)**
   - 业务系统传入的原始金额保存到订单记录的order_amount字段
   - 支付成功后,paid_amount记录为原始金额(非1分钱)
   - 服务商回调返回的金额(1分钱)不更新到订单,以原始金额为准

   **订单标识:**
   - 沙箱模式订单在description字段自动添加"[测试]"前缀
   - 管理端订单列表显示"测试订单"标签
   - 沙箱订单不计入报表统计

   **退款处理:**
   - 沙箱模式订单发起退款时,实际退款金额也为1分钱
   - 退款记录保存原始退款金额

   **注意事项:**
   - 沙箱模式仅用于测试支付流程,不可用于生产环境
   - 切换沙箱模式前需确认已有订单已处理完成
   - 沙箱模式的服务商API地址通常与生产环境不同,需单独配置

4. **订单有效期**
   - 订单默认有效期:30分钟
   - 超过有效期未支付,订单自动关闭
   - 关闭后可重新下单

5. **签约支付验证** (生活缴费、电子托收场景)
   - 如果支付方式需要签约(need_sign=true),必须验证签约状态
   - 签约协议号(sign_no)必须传递
   - 验证用户在该小区、该服务商、该支付方式下有有效签约
   - 签约状态必须为 signed,invalid和cancelled状态不允许支付 (v1.1.1增强)
   - 如果签约失效,返回明确错误信息提示用户重新签约
   - **说明**: 生活缴费和电子托收由系统统一唤起支付自动扣款,必须提前签约授权

6. **普通支付验证** (普通支付、牌照支付场景)
   - 无需签约,用户主动发起线上支付
   - 系统返回支付链接/二维码供用户完成支付
   - 用户扫码或点击支付实时完成

7. **路由规则**
   - 如果业务系统指定payment_method_id,使用指定的支付方式
   - 如果未指定,使用路由服务根据优先级自动选择

8. **幂等性保证**
   - 基于business_order_no实现幂等
   - 相同business_order_no重复请求,返回已有订单信息

#### 2.1.4 订单状态流转

```
pending (待支付)
  ├─→ paid (已支付)
  │    ├─→ part_refunded (部分退款)
  │    └─→ refunded (全额退款)
  └─→ closed (已关闭: 超时或取消)
```

#### 2.1.5 返回数据示例

**扫码支付返回:**
```json
{
  "platform_order_no": "PAY20260225123456789",
  "payment_provider": "WECHAT",
  "payment_method": "NATIVE",
  "pay_info": {
    "type": "qrcode",
    "data": "weixin://wxpay/bizpayurl?pr=abc123xyz"
  },
  "expire_time": "2026-02-25 11:30:00"
}
```

**H5支付返回:**
```json
{
  "platform_order_no": "PAY20260225123456790",
  "payment_provider": "WECHAT",
  "payment_method": "H5",
  "pay_info": {
    "type": "url",
    "data": "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx123456"
  },
  "expire_time": "2026-02-25 11:30:00"
}
```

**JSAPI支付返回:**
```json
{
  "platform_order_no": "PAY20260225123456791",
  "payment_provider": "WECHAT",
  "payment_method": "JSAPI",
  "pay_info": {
    "type": "jsapi",
    "data": {
      "appId": "wx1234567890abcdef",
      "timeStamp": "1708819200",
      "nonceStr": "abc123",
      "package": "prepay_id=wx123456",
      "signType": "RSA",
      "paySign": "signature_string"
    }
  },
  "expire_time": "2026-02-25 11:30:00"
}
```

---

### 2.2 支付查询

#### 2.2.1 功能描述

业务系统可以主动查询支付订单的状态和详情,用于确认支付结果、展示订单信息等场景。

#### 2.2.2 查询方式

**方式1: 按支付中台订单号查询**
- 查询参数:platform_order_no
- 返回:完整订单信息

**方式2: 按业务订单号查询**
- 查询参数:business_order_no
- 返回:完整订单信息

**方式3: 列表查询(管理端)**
- 支持多维度筛选:
  - 小区ID
  - 业务类型
  - 订单状态
  - 支付服务商
  - 时间范围
- 支持分页
- 支持导出Excel

#### 2.2.3 查询返回信息

```json
{
  "platform_order_no": "PAY20260225123456789",
  "business_order_no": "BIZ20260225001",
  "community_id": 10001,
  "community_name": "阳光花园",
  "business_type": "property_fee",
  "user_id": 1001,
  "order_amount": 50000,
  "paid_amount": 50000,
  "refund_amount": 0,
  "fee_amount": 30,
  "order_status": "paid",
  "payment_provider": "WECHAT",
  "payment_method": "NATIVE",
  "provider_order_no": "WX1234567890",
  "provider_trade_no": "4200001234567890",
  "pay_time": "2026-02-25 10:15:30",
  "description": "物业费缴纳",
  "created_at": "2026-02-25 10:00:00"
}
```

---

### 2.3 退款处理

#### 2.3.1 功能描述

业务系统在完成退款审核后,调用支付中台退款接口,支付中台调用服务商退款接口,处理退款并通知业务系统结果。

**重要**: 退款审核由业务系统完成,支付中台只负责执行退款操作。

#### 2.3.2 退款流程

```
业务系统
  │
  ├─ 1. 业务系统内部退款审核流程
  │    ├─ 用户申请退款
  │    ├─ 业务人员审核
  │    └─ 审核通过
  │
  ├─ 2. 调用支付中台退款接口
  │    └─ POST /api/payment/refund/create
  │
  ├─ 传递参数:
  │    ├─ platform_order_no: 支付中台订单号
  │    ├─ refund_amount: 退款金额 (分)
  │    ├─ refund_reason: 退款原因
  │    ├─ business_refund_no: 业务退款单号 (唯一)
  │    ├─ operator_id: 操作人ID (审核通过的操作人)
  │    └─ notify_url: 退款结果回调地址
  │
  ▼
支付中台 - 退款处理
  │
  ├─ 3. 参数校验
  │    ├─ 校验订单是否存在
  │    ├─ 校验订单状态 (必须是 paid 或 part_refunded)
  │    ├─ 校验退款金额 (不能超过可退金额)
  │    ├─ 可退金额 = 实付金额 - 已退款金额
  │    └─ 防止重复退款 (同一业务退款单号只能退款一次)
  │
  ├─ 4. 生成退款记录
  │    ├─ 退款ID自动生成
  │    ├─ 退款状态: processing (退款中)
  │    └─ 保存退款信息
  │
  ├─ 5. 调用服务商退款接口
  │    ├─ 传递: 原订单号、退款金额、退款原因
  │    └─ 获取服务商退款单号
  │
  ├─ 6. 更新订单状态 (使用乐观锁防止并发)
  │    ├─ 如果是部分退款: order_status = part_refunded
  │    ├─ 如果是全额退款: order_status = refunded
  │    └─ 累加 refund_amount
  │
  ├─ 7. 等待服务商异步回调
  │    └─ 服务商处理退款,回调通知结果
  │
  ▼
服务商回调支付中台
  │
  ├─ 8. 验证回调签名
  │
  ├─ 9. 更新退款状态
  │    ├─ 成功: processing → success
  │    ├─ 失败: processing → failed
  │    └─ 记录退款时间
  │
  ├─ 10. 异步通知业务系统
  │     ├─ 首次通知
  │     ├─ 失败重试1 (延迟10秒)
  │     ├─ 失败重试2 (延迟30秒)
  │     └─ 失败重试3 (延迟60秒)
  │
  ├─ 11. 返回成功响应给服务商
  │
  ▼
业务系统
  │
  └─ 12. 接收退款结果通知,更新业务订单状态
```

#### 2.3.3 退款业务规则

1. **退款金额限制**
   - 退款金额必须 > 0
   - 退款金额 ≤ 可退金额
   - 可退金额 = 实付金额 - 已退款金额
   - 支持部分退款和多次退款

2. **退款状态**
   - processing: 退款中 (已发起,等待服务商处理)
   - success: 退款成功
   - failed: 退款失败

3. **退款时效限制 (v1.2.0新增)**

   | 服务商 | 退款时效限制 | 说明 |
   |-------|-------------|------|
   | 微信支付 | 支付成功后365天内 | 超过365天无法退款 |
   | 支付宝 | 支付成功后90天内 | 超过90天需走人工通道 |
   | 易宝托收 | 支付成功后180天内 | 银行托收退款限制 |
   | 银联支付 | 支付成功后180天内 | 银联退款规则 |

   **时效校验逻辑:**
   ```
   退款请求
     │
     ├─ 1. 获取订单支付时间 (pay_time)
     │
     ├─ 2. 计算已支付天数
     │    └─ days = (当前时间 - pay_time) / 86400
     │
     ├─ 3. 根据服务商获取退款时效限制
     │    └─ 从配置表或服务商配置中获取
     │
     ├─ 4. 校验是否超时
     │    ├─ 未超时: 继续退款流程
     │    └─ 已超时: 返回错误
     │         └─ 错误码: PAY-5000-05
     │         └─ 错误信息: "退款已超时效限制,该订单支付已超过X天,无法在线退款"
     │
     └─ 5. 超时退款处理建议
          └─ 提示用户: "请联系财务人员走线下退款流程"
   ```

4. **部分退款次数限制 (v1.2.0新增)**
   - 单笔订单最多支持10次部分退款
   - 超过10次返回错误: PAY-5000-06 "退款次数已达上限"
   - 计数规则: 仅统计 success 状态的退款记录

5. **退款失败重试机制 (v1.2.0新增)**

   **自动重试规则:**
   | 失败原因 | 是否自动重试 | 重试次数 | 重试间隔 |
   |---------|------------|---------|---------|
   | 服务商接口超时 | 是 | 3次 | 10s, 30s, 60s |
   | 服务商返回系统繁忙 | 是 | 3次 | 60s, 120s, 300s |
   | 余额不足 | 否 | - | - |
   | 订单状态异常 | 否 | - | - |
   | 退款金额超限 | 否 | - | - |

   **重试流程:**
   ```
   退款请求失败
     │
     ├─ 1. 判断失败原因
     │    ├─ 可重试错误: 进入重试队列
     │    └─ 不可重试错误: 直接标记failed
     │
     ├─ 2. 异步重试处理
     │    ├─ 将退款请求放入延迟队列
     │    ├─ 按重试间隔执行
     │    └─ 每次重试更新 retry_count
     │
     ├─ 3. 重试结果处理
     │    ├─ 成功: 更新状态为processing,等待回调
     │    ├─ 仍失败且未达重试上限: 继续重试
     │    └─ 达到重试上限: 标记failed,记录失败原因
     │
     └─ 4. 重试失败最终处理
          ├─ 记录详细错误日志
          ├─ 发送告警通知
          └─ 支持从管理端手动重新发起退款
   ```

   **退款记录增加字段:**
   - retry_count: 重试次数
   - last_retry_time: 最后重试时间
   - retry_error_message: 重试失败原因

6. **退款到账时间**
   - 服务商一般在1-7个工作日内完成退款
   - 退款到账时间由服务商和银行决定

4. **退款幂等性**
   - 基于business_refund_no实现幂等
   - 相同business_refund_no重复请求,返回已有退款信息

5. **退款并发控制**

**实现机制：使用乐观锁**

```sql
-- 使用乐观锁防止并发退款超额
UPDATE payment_order
SET refund_amount = refund_amount + ?,
    order_status = CASE
        WHEN refund_amount + ? >= order_amount THEN 'refunded'
        ELSE 'part_refunded'
    END,
    version = version + 1,  -- 版本号+1
    updated_at = NOW()
WHERE platform_order_no = ?
  AND version = ?  -- 乐观锁条件
  AND refund_amount + ? <= order_amount;  -- 金额校验

-- 如果affected_rows = 0,表示并发冲突或超额
```

**并发冲突处理策略 (v1.1.1增强):**

1. **冲突检测**
   - 执行UPDATE后检查affected_rows
   - affected_rows = 0 表示发生以下情况之一:
     - 并发冲突(其他请求已更新version)
     - 退款超额(refund_amount + 本次退款金额 > order_amount)
     - 订单不存在或状态异常

2. **支付中台处理**
   - **不自动重试**,直接返回错误
   - 错误码: PAY-5000-04
   - 错误信息: "订单状态已变更,请重试"
   - 包含当前订单版本号和可退金额供客户端参考

3. **业务系统处理建议**
   - 收到PAY-5000-04错误后:
     - 先查询订单当前状态
     - 重新计算可退金额
     - 最多重试3次,每次间隔1秒
     - 3次仍失败,转人工处理

4. **监控告警**
   - 单笔订单退款冲突次数 > 5次: 触发告警
   - 1分钟内退款冲突总数 > 100次: 触发告警
   - 需人工核查是否有并发问题或恶意请求

5. **示例场景**
   ```
   场景: 订单金额100元,两个退款请求同时发起,各退50元

   请求A:
   - 读取订单: version=1, refund_amount=0
   - 执行UPDATE: version=1 → 2, refund_amount=0 → 50
   - 成功,affected_rows=1

   请求B:
   - 读取订单: version=1, refund_amount=0 (与A读取时间接近)
   - 执行UPDATE: version=1 → 2 (但此时version已经是2)
   - 失败,affected_rows=0 (version不匹配)
   - 返回错误: PAY-5000-04
   - 建议业务系统重新查询订单状态后重试
   ```

6. **重试成功率保障**
   - 1次重试成功率预期: >95%
   - 3次重试成功率预期: >99.9%
   - 持续失败通常表示业务逻辑错误或数据异常

6. **手续费处理**
   - 手续费不退还(根据服务商规则)
   - 退款金额不包含手续费

---

### 2.4 退款回调处理

#### 2.4.1 功能描述

支付服务商在处理完退款请求后,异步回调支付中台,支付中台验证签名、更新退款状态、通知业务系统。

#### 2.4.2 退款回调流程

```
支付服务商
  │
  ├─ 1. 处理退款完成
  │
  ├─ 2. 服务商回调支付中台
  │    └─ POST /notify/refund/{provider_code}
  │
  ├─ 携带信息:
  │    ├─ 退款单号 (服务商退款单号)
  │    ├─ 退款结果 (成功/失败)
  │    ├─ 退款时间
  │    ├─ 退款金额
  │    ├─ 失败原因 (如果失败)
  │    └─ 签名
  │
  ▼
支付中台 - 退款回调处理
  │
  ├─ 3. 记录回调日志
  │    └─ 保存回调原始数据到 payment_callback_log
  │
  ├─ 4. 验证签名
  │    ├─ 使用服务商密钥验证签名
  │    ├─ 成功: 继续处理
  │    └─ 失败: 记录日志,返回失败
  │
  ├─ 5. 查询退款记录
  │    └─ 根据服务商退款单号查询退款记录
  │
  ├─ 6. 幂等性判断
  │    ├─ 如果退款状态已经是 success 或 failed
  │    └─ 直接返回成功 (防止重复处理)
  │
  ├─ 7. 更新退款状态
  │    ├─ 成功: processing → success
  │    ├─ 失败: processing → failed
  │    ├─ 记录退款时间
  │    └─ 记录失败原因 (如果失败)
  │
  ├─ 8. 异步通知业务系统
  │    ├─ 调用业务系统notify_url
  │    ├─ 传递: platform_order_no, business_refund_no, refund_status, refund_time等
  │    ├─ 首次通知
  │    ├─ 失败重试1 (延迟10秒)
  │    ├─ 失败重试2 (延迟30秒)
  │    └─ 失败重试3 (延迟60秒,最后一次)
  │
  ├─ 9. 记录通知结果
  │     └─ 更新回调日志,记录通知状态
  │
  ├─ 10. 返回成功响应给服务商
  │     └─ 格式按服务商要求返回
  │
  ▼
业务系统
  │
  └─ 11. 接收退款结果通知,更新业务订单状态
```

#### 2.4.3 回调业务规则

1. **回调验签**
   - 必须验证服务商回调签名
   - 签名验证失败,不处理回调,返回失败

2. **幂等性处理**
   - 服务商可能重复回调
   - 退款已是最终状态(success/failed)时,直接返回成功

3. **异步通知业务系统规则**
   - 采用异步通知方式,不阻塞服务商回调
   - 自动重试机制:3次,间隔递增(10s、30s、60s)
   - 成功判断标准:
     - HTTP状态码 = 200
     - 响应body包含"success"或返回JSON {"code": 0}
   - 3次重试后仍失败:
     - 标记notify_status = 'failed'
     - 记录失败原因到notify_result
     - **支持从管理端手动重新触发通知** (v1.1.2新增)
     - 业务系统也可主动查询订单状态
   - 业务系统要求:
     - 实现幂等性(同一退款单号多次通知,只处理一次)
     - 5秒内返回响应(超时视为失败)

4. **回调日志记录**
   - 记录所有回调请求到 payment_callback_log 表
   - 记录验签结果、处理结果、错误信息
   - 记录通知业务系统的状态和结果
   - 用于问题排查和审计

5. **快速响应**
   - 回调处理时间必须 ≤ 100ms
   - 快速响应服务商,避免超时重复回调

#### 2.4.4 手动重试通知功能 (v1.1.2新增)

**功能入口:**
- 管理端 → 支付订单管理 → 订单详情 → 回调通知记录
- 管理端 → 退款记录管理 → 退款详情 → 回调通知记录

**适用场景:**
- 自动重试3次后仍失败的通知
- 业务系统临时不可用导致的通知失败
- 网络问题导致的通知失败

**操作流程:**
```
管理端
  │
  ├─ 1. 进入订单/退款详情页
  │
  ├─ 2. 查看回调通知记录
  │    └─ 显示: 通知状态、通知次数、最近通知时间、失败原因
  │
  ├─ 3. 对notify_status='failed'的记录,显示"重新通知"按钮
  │
  ├─ 4. 点击"重新通知"
  │    ├─ 二次确认: "确定要重新发送通知吗?"
  │    └─ 确认后触发通知
  │
  ├─ 5. 执行通知
  │    ├─ 调用业务系统notify_url
  │    ├─ notify_times +1
  │    ├─ 记录本次通知结果
  │    └─ 更新notify_status
  │
  └─ 6. 显示通知结果
       ├─ 成功: 提示"通知发送成功"
       └─ 失败: 提示"通知发送失败: [失败原因]"
```

**业务规则:**
1. 仅notify_status='failed'的记录可手动重试
2. 手动重试无次数限制,但建议排查失败原因后再重试
3. 每次手动重试都会记录到操作日志
4. 手动重试成功后,notify_status更新为'success'
5. 手动重试仍失败,notify_status保持'failed',notify_times继续累加

**页面展示:**

| 字段 | 说明 |
|------|------|
| 通知状态 | pending/success/failed |
| 通知次数 | 自动+手动的总次数 |
| 最近通知时间 | 最后一次通知的时间 |
| 通知URL | 业务系统回调地址 |
| 最近响应 | 最后一次通知的响应内容 |
| 失败原因 | 超时/HTTP错误/响应异常等 |
| 操作 | [重新通知] (仅失败状态显示) |

---

### 2.5 支付回调处理

#### 2.5.1 功能描述

支付服务商在用户完成支付后,异步回调支付中台,支付中台验证签名、更新订单状态、通知业务系统。

#### 2.5.2 支付回调流程

```
支付服务商
  │
  ├─ 1. 用户完成支付
  │
  ├─ 2. 服务商回调支付中台
  │    └─ POST /notify/payment/{provider_code}
  │
  ├─ 携带信息:
  │    ├─ 订单号 (服务商订单号)
  │    ├─ 支付结果 (成功/失败)
  │    ├─ 支付时间
  │    ├─ 交易流水号
  │    ├─ 实付金额
  │    ├─ 手续费
  │    └─ 签名
  │
  ▼
支付中台 - 回调处理
  │
  ├─ 3. 记录回调日志
  │    └─ 保存回调原始数据
  │
  ├─ 4. 验证签名
  │    ├─ 使用服务商密钥验证签名
  │    ├─ 成功: 继续处理
  │    └─ 失败: 记录日志,返回失败
  │
  ├─ 5. 查询订单
  │    └─ 根据服务商订单号查询支付中台订单
  │
  ├─ 6. 幂等性判断
  │    ├─ 如果订单已经是 paid 状态
  │    └─ 直接返回成功 (防止重复处理)
  │
  ├─ 7. 更新订单状态
  │    ├─ 状态: pending → paid
  │    ├─ 保存: 支付时间、交易流水号、实付金额、手续费
  │    └─ 记录服务商返回的所有信息
  │
  ├─ 8. 记录支付流水
  │    └─ payment_order 即支付流水
  │
  ├─ 9. 异步通知业务系统
  │    ├─ 调用业务系统notify_url
  │    ├─ 传递: platform_order_no, business_order_no, order_status, pay_time等
  │    ├─ 首次通知
  │    ├─ 失败重试1 (延迟10秒)
  │    ├─ 失败重试2 (延迟30秒)
  │    └─ 失败重试3 (延迟60秒,最后一次)
  │
  ├─ 10. 记录通知结果
  │     └─ 更新回调日志,记录通知状态
  │
  ├─ 11. 返回成功响应给服务商
  │     └─ 格式按服务商要求返回
  │
  ▼
服务商
  │
  └─ 12. 收到成功响应,不再重复回调
```

#### 2.5.3 回调业务规则

1. **回调验签**
   - 必须验证服务商回调签名
   - 签名验证失败,不处理回调,返回失败

2. **幂等性处理**
   - 服务商可能重复回调
   - 订单已是最终状态(paid/refunded)时,直接返回成功

3. **异步通知业务系统**
   - 采用异步通知方式,不阻塞服务商回调
   - 自动重试机制:3次,间隔递增(10s、30s、60s)
   - 3次重试后仍失败,支持从管理端手动重新触发通知 (v1.1.2新增)
   - 手动重试操作详见 2.4.4 节

4. **回调日志记录**
   - 记录所有回调请求
   - 记录验签结果、处理结果、错误信息
   - 用于问题排查和审计

5. **快速响应**
   - 回调处理时间必须 ≤ 100ms
   - 快速响应服务商,避免超时重复回调

---

### 2.6 订单超时关闭机制

#### 2.6.1 功能描述

自动扫描超时未支付的订单,将其状态更新为已关闭,释放订单资源。

#### 2.6.2 定时任务设计

**任务名称:** OrderTimeoutCloseJob

**执行频率:** 每5分钟执行一次

**任务内容:** 扫描超时未支付订单并关闭

#### 2.6.3 关闭条件

- 订单状态 = pending (待支付)
- created_at < 当前时间 - 30分钟

#### 2.6.4 关闭流程

```sql
-- 批量关闭超时订单
UPDATE payment_order
SET order_status = 'closed',
    close_time = NOW(),
    updated_at = NOW()
WHERE order_status = 'pending'
  AND created_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE);
```

#### 2.6.5 是否通知业务系统

**不通知业务系统**

**原因:**
1. 订单关闭是系统自动行为,不是支付结果变更
2. 业务系统可通过订单查询接口获取最新状态
3. 避免不必要的通知增加系统负担

**业务系统查询方式:**
- 主动查询订单状态 (推荐)
- 在页面轮询订单状态
- 设置前端定时器,超时后查询结果

**通知场景明确:**

| 场景 | 是否通知业务系统 | 说明 |
|------|----------------|------|
| 支付成功 | ✅ 是 | 异步通知,3次重试 |
| 支付失败 | ❌ 否 | 用户未完成支付,订单保持pending |
| 订单超时关闭 | ❌ 否 | 系统自动关闭,业务系统主动查询 |
| 退款成功 | ✅ 是 | 异步通知,3次重试 |
| 退款失败 | ✅ 是 | 异步通知,3次重试 |

---

### 2.7 异常交易处理机制 (v1.2.0新增)

#### 2.7.1 功能描述

异常交易处理机制用于检测、识别和处理各类支付异常情况，确保交易数据的准确性和资金安全。

#### 2.7.2 异常类型定义

| 异常类型 | 代码 | 说明 | 风险等级 |
|---------|------|------|---------|
| 支付超时 | TIMEOUT | 订单超时未完成支付 | 低 |
| 重复支付 | DUPLICATE_PAY | 同一订单被支付多次 | 高 |
| 金额不一致 | AMOUNT_MISMATCH | 实付金额与订单金额不符 | 高 |
| 回调丢失 | CALLBACK_LOST | 服务商已扣款但未收到回调 | 高 |
| 状态异常 | STATUS_ERROR | 订单状态与服务商不一致 | 中 |
| 掉单 | ORDER_LOST | 下单成功但未收到任何回调 | 高 |

#### 2.7.3 支付超时处理

**场景描述：** 用户发起支付后，在有效期内未完成支付。

**处理流程：**
```
定时任务 (每5分钟)
  │
  ├─ 1. 扫描超时订单
  │    └─ SELECT * FROM payment_order
  │        WHERE order_status = 'pending'
  │        AND created_at < NOW() - INTERVAL 30 MINUTE
  │
  ├─ 2. 调用服务商查询订单状态
  │    ├─ 成功: 更新订单状态,走正常支付成功流程
  │    └─ 失败/未支付: 继续关闭流程
  │
  ├─ 3. 调用服务商关闭订单接口
  │    └─ 确保服务商侧订单也被关闭
  │
  └─ 4. 更新本地订单状态
       ├─ order_status = 'closed'
       ├─ close_time = NOW()
       └─ close_reason = '支付超时自动关闭'
```

#### 2.7.4 重复支付检测与处理

**场景描述：** 用户因网络延迟等原因，对同一订单进行了多次支付。

**检测时机：**
- 收到支付成功回调时
- 主动查询订单状态时

**处理流程：**
```
收到支付成功回调
  │
  ├─ 1. 检查订单当前状态
  │    ├─ pending: 正常处理
  │    └─ paid: 重复支付,进入异常处理
  │
  ├─ 2. 重复支付处理
  │    ├─ 记录重复支付信息
  │    │    ├─ 原支付流水号
  │    │    ├─ 重复支付流水号
  │    │    └─ 重复支付金额
  │    │
  │    ├─ 自动发起退款
  │    │    ├─ 退款金额 = 重复支付金额
  │    │    ├─ 退款原因 = '重复支付自动退款'
  │    │    └─ refund_type = 'auto_duplicate'
  │    │
  │    └─ 发送告警通知
  │         └─ 通知财务人员核查
  │
  └─ 3. 记录异常日志
       └─ INSERT INTO payment_exception_log
```

**重复支付记录表 (payment_duplicate_record):**

| 字段名 | 类型 | 说明 |
|-------|------|------|
| id | BIGINT | 主键ID |
| platform_order_no | VARCHAR(64) | 原订单号 |
| original_trade_no | VARCHAR(128) | 原支付流水号 |
| duplicate_trade_no | VARCHAR(128) | 重复支付流水号 |
| duplicate_amount | BIGINT | 重复支付金额 |
| refund_status | VARCHAR(20) | 退款状态 |
| refund_order_no | VARCHAR(64) | 退款单号 |
| created_at | DATETIME | 创建时间 |

#### 2.7.5 掉单处理机制

**场景描述：** 支付中台已向服务商发起下单请求，但一直未收到支付结果（无论是回调还是查询）。

**检测机制：**
```
定时任务 (每10分钟)
  │
  ├─ 1. 扫描可能掉单的订单
  │    └─ SELECT * FROM payment_order
  │        WHERE order_status = 'pending'
  │        AND provider_order_no IS NOT NULL
  │        AND created_at > NOW() - INTERVAL 2 HOUR
  │        AND created_at < NOW() - INTERVAL 35 MINUTE
  │
  ├─ 2. 主动查询服务商订单状态
  │    ├─ 调用服务商订单查询接口
  │    └─ 传递: provider_order_no
  │
  ├─ 3. 根据查询结果处理
  │    ├─ 已支付: 补充订单信息,走支付成功流程
  │    │    ├─ 更新订单状态为paid
  │    │    ├─ 记录支付时间
  │    │    └─ 异步通知业务系统
  │    │
  │    ├─ 未支付: 等待或关闭
  │    │    ├─ 未超时: 继续等待
  │    │    └─ 已超时: 关闭订单
  │    │
  │    └─ 查询失败: 标记异常
  │         ├─ 记录查询失败原因
  │         ├─ 增加查询重试次数
  │         └─ 超过5次标记为需人工处理
  │
  └─ 4. 人工处理入口
       ├─ 管理端 → 异常订单管理
       ├─ 显示所有需人工处理的订单
       └─ 支持手动更新订单状态
```

**掉单处理记录:**
- 记录每次主动查询的时间和结果
- 记录处理方式和最终结果
- 用于问题追溯和数据分析

#### 2.7.6 回调丢失补偿

**场景描述：** 服务商已完成扣款，但回调通知未到达支付中台（网络问题、服务器故障等）。

**补偿机制：**
```
定时任务 (每15分钟)
  │
  ├─ 1. 扫描可能丢失回调的订单
  │    └─ SELECT * FROM payment_order
  │        WHERE order_status = 'pending'
  │        AND provider_order_no IS NOT NULL
  │        AND created_at > NOW() - INTERVAL 24 HOUR
  │        AND created_at < NOW() - INTERVAL 40 MINUTE
  │        AND last_query_time < NOW() - INTERVAL 15 MINUTE
  │
  ├─ 2. 批量查询服务商订单状态
  │    └─ 按服务商分组批量查询
  │
  ├─ 3. 补偿处理
  │    ├─ 发现已支付: 补偿回调处理
  │    │    ├─ 更新订单状态
  │    │    ├─ 记录支付信息
  │    │    ├─ 标记callback_source = 'query_compensate'
  │    │    └─ 异步通知业务系统
  │    │
  │    └─ 未支付: 更新查询时间,继续等待
  │
  └─ 4. 补偿统计
       ├─ 每日统计补偿成功数量
       └─ 用于系统稳定性分析
```

#### 2.7.7 异常订单管理页面

**功能入口：** 管理端 → 支付订单管理 → 异常订单

**页面布局：**
```
┌─────────────────────────────────────────────────────────────────────────┐
│  异常订单管理                                           [导出] [刷新]   │
├─────────────────────────────────────────────────────────────────────────┤
│  筛选: [异常类型 ▼] [时间范围 ▼] [处理状态 ▼]  [查询]                  │
├─────────────────────────────────────────────────────────────────────────┤
│  待处理异常统计:                                                        │
│  ├─ 🔴 重复支付: 2笔                                                    │
│  ├─ 🟡 掉单待查: 5笔                                                    │
│  └─ 🟡 回调丢失: 3笔                                                    │
├─────────────────────────────────────────────────────────────────────────┤
│  ┌────────────────────────────────────────────────────────────────────┐ │
│  │ 订单号          │ 异常类型  │ 金额    │ 发生时间  │ 状态   │ 操作  │ │
│  ├────────────────────────────────────────────────────────────────────┤ │
│  │ PAY2026030...   │ 重复支付  │ ¥150.00 │ 10分钟前  │ 待处理 │ [处理]│ │
│  │ PAY2026030...   │ 掉单      │ ¥80.00  │ 25分钟前  │ 处理中 │ [查看]│ │
│  │ PAY2026030...   │ 回调丢失  │ ¥320.00 │ 1小时前   │ 已处理 │ [查看]│ │
│  └────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
```

**处理操作：**
- **重复支付**: 查看详情、确认退款、人工退款
- **掉单**: 重新查询、手动更新状态、标记无法处理
- **回调丢失**: 重新查询、手动同步状态

#### 2.7.8 异常日志表 (payment_exception_log)

| 字段名 | 类型 | 说明 |
|-------|------|------|
| id | BIGINT | 主键ID |
| platform_order_no | VARCHAR(64) | 订单号 |
| exception_type | VARCHAR(32) | 异常类型 |
| exception_detail | TEXT | 异常详情 (JSON) |
| handle_status | VARCHAR(20) | 处理状态: pending/processing/handled/ignored |
| handle_result | VARCHAR(500) | 处理结果 |
| handler_id | BIGINT | 处理人ID |
| handle_time | DATETIME | 处理时间 |
| created_at | DATETIME | 创建时间 |

---

### 2.8 交易限额管理 (v1.2.0新增)

#### 2.8.1 功能描述

交易限额管理用于控制单笔、单日、单月的交易金额上限，防止异常大额交易和资金风险。

#### 2.8.2 限额维度

| 限额维度 | 说明 | 默认值 |
|---------|------|--------|
| 单笔限额 | 单次支付金额上限 | 100,000元 |
| 单日限额 | 用户每日累计支付上限 | 500,000元 |
| 单月限额 | 用户每月累计支付上限 | 5,000,000元 |

#### 2.8.3 限额规则配置

**配置层级 (优先级从高到低):**
1. **用户级限额**: 针对特定用户的限额配置
2. **小区级限额**: 针对特定小区的限额配置
3. **运营平台级限额**: 运营平台的默认限额
4. **系统级限额**: 系统全局默认限额

**限额校验流程:**
```
支付下单请求
  │
  ├─ 1. 获取用户限额配置
  │    └─ 按优先级依次查询限额配置
  │
  ├─ 2. 单笔限额校验
  │    ├─ 本次金额 ≤ 单笔限额: 通过
  │    └─ 本次金额 > 单笔限额: 拒绝
  │         └─ 错误码: PAY-4000-10 "支付金额超过单笔限额"
  │
  ├─ 3. 单日限额校验
  │    ├─ 查询用户当日已支付金额
  │    ├─ 当日已支付 + 本次金额 ≤ 单日限额: 通过
  │    └─ 超过: 拒绝
  │         └─ 错误码: PAY-4000-11 "支付金额超过单日限额"
  │
  └─ 4. 单月限额校验
       ├─ 查询用户当月已支付金额
       ├─ 当月已支付 + 本次金额 ≤ 单月限额: 通过
       └─ 超过: 拒绝
            └─ 错误码: PAY-4000-12 "支付金额超过单月限额"
```

#### 2.8.4 限额配置表 (payment_limit_config)

| 字段名 | 类型 | 说明 |
|-------|------|------|
| id | BIGINT | 主键ID |
| limit_scope | VARCHAR(20) | 限额范围: system/platform/community/user |
| scope_id | BIGINT | 范围ID (对应platform_id/community_id/user_id) |
| single_limit | BIGINT | 单笔限额 (分) |
| daily_limit | BIGINT | 单日限额 (分) |
| monthly_limit | BIGINT | 单月限额 (分) |
| status | VARCHAR(20) | 状态: enabled/disabled |
| created_at | DATETIME | 创建时间 |
| updated_at | DATETIME | 更新时间 |

---

#### 2.7.1 功能描述

在支付下单前,对支付参数进行全面校验,确保参数合法性和完整性,避免无效请求。

#### 2.7.2 校验项

| 校验项 | 校验规则 | 错误提示 |
|-------|---------|---------|
| 必填参数 | community_id, business_type, business_order_no, amount, user_id必填 | "缺少必填参数:[参数名]" |
| 金额校验 | 金额>0 且 ≤ 999999999 | "支付金额不合法" |
| 订单号唯一性 | business_order_no不能重复 | "业务订单号已存在" |
| 小区配置 | 小区必须有支付配置 | "小区未配置支付方式" |
| 签约验证 | 签约支付必须验证签约状态 | "该支付方式需要签约,请先完成签约" |
| 回调地址 | notify_url必须是合法的URL | "回调地址格式不正确" |
| 指定支付方式 | payment_method_id必须在小区配置中 | "小区未配置该支付方式" |

---

## 三、数据库设计

### 3.1 支付订单表 (payment_order)

#### 3.1.1 表说明
存储所有支付订单信息,订单即流水

#### 3.1.2 字段设计

| 字段名 | 类型 | 长度 | 允许空 | 默认值 | 说明 |
|-------|------|------|-------|-------|------|
| id | BIGINT | - | 否 | 自增 | 主键ID |
| platform_order_no | VARCHAR | 64 | 否 | - | 支付中台订单号 (唯一) |
| business_order_no | VARCHAR | 64 | 否 | - | 业务订单号 (唯一) |
| community_id | BIGINT | - | 否 | - | 小区ID |
| business_type | VARCHAR | 32 | 否 | - | 业务类型:property_fee/parking/mall等 |
| user_id | BIGINT | - | 否 | - | 用户ID |
| provider_id | BIGINT | - | 否 | - | 支付服务商ID |
| payment_method_id | BIGINT | - | 否 | - | 支付方式ID |
| payment_scene_id | BIGINT | - | 是 | NULL | 业务场景ID |
| sign_no | VARCHAR | 128 | 是 | NULL | 签约协议号 (签约支付时填充) |
| order_amount | BIGINT | - | 否 | - | 订单金额 (分) |
| paid_amount | BIGINT | - | 否 | 0 | 实付金额 (分) |
| refund_amount | BIGINT | - | 否 | 0 | 已退款金额 (分) |
| fee_amount | BIGINT | - | 否 | 0 | 手续费金额 (分,从服务商获取) |
| order_status | VARCHAR | 20 | 否 | pending | 订单状态:pending/paid/closed/refunded/part_refunded |
| version | INT | - | 否 | 0 | 版本号,用于乐观锁 |
| provider_order_no | VARCHAR | 128 | 是 | NULL | 服务商订单号 |
| provider_trade_no | VARCHAR | 128 | 是 | NULL | 服务商交易流水号 |
| pay_time | DATETIME | - | 是 | NULL | 支付时间 |
| close_time | DATETIME | - | 是 | NULL | 关闭时间 |
| description | VARCHAR | 255 | 否 | - | 订单描述 |
| notify_url | VARCHAR | 255 | 否 | - | 业务系统回调地址 |
| notify_status | VARCHAR | 20 | 否 | pending | 通知状态:pending/success/failed |
| notify_times | INT | - | 否 | 0 | 通知次数 |
| extra_data | TEXT | - | 是 | NULL | 扩展数据 (JSON) |
| created_at | DATETIME | - | 否 | CURRENT_TIMESTAMP | 创建时间 |
| updated_at | DATETIME | - | 否 | CURRENT_TIMESTAMP ON UPDATE | 更新时间 |
| remark | VARCHAR | 500 | 是 | NULL | 备注 |

#### 3.1.3 订单状态映射关系

**订单状态定义:**

| 数据库值(英文) | 页面显示(中文) | 说明 |
|---------------|---------------|------|
| pending | 待支付 | 订单已创建,等待用户支付 |
| paid | 已支付 | 支付成功 |
| closed | 已关闭 | 订单超时或取消 |
| refunded | 已退款 | 全额退款 |
| part_refunded | 部分退款 | 部分金额已退款 |

#### 3.1.4 索引设计

| 索引名 | 索引类型 | 字段 | 说明 |
|-------|---------|------|------|
| PRIMARY | 主键 | id | 主键索引 |
| uk_platform_order_no | 唯一索引 | platform_order_no | 支付中台订单号唯一 |
| uk_business_order_no | 唯一索引 | business_order_no | 业务订单号唯一 |
| idx_community_id | 普通索引 | community_id | 小区查询 |
| idx_user_id | 普通索引 | user_id | 用户查询 |
| idx_business_type | 普通索引 | business_type | 业务类型查询 |
| idx_order_status | 普通索引 | order_status | 状态查询 |
| idx_provider_order_no | 普通索引 | provider_order_no | 服务商订单号查询 |
| idx_pay_time | 普通索引 | pay_time | 支付时间排序 |
| idx_created_at | 普通索引 | created_at | 创建时间排序 |
| idx_community_business_status_time | 复合索引 | community_id, business_type, order_status, pay_time | 常用查询场景优化 |

#### 3.1.5 外键约束

```sql
ALTER TABLE payment_order
ADD CONSTRAINT fk_order_provider
FOREIGN KEY (provider_id) REFERENCES payment_provider(id)
ON DELETE RESTRICT ON UPDATE CASCADE;

ALTER TABLE payment_order
ADD CONSTRAINT fk_order_method
FOREIGN KEY (payment_method_id) REFERENCES payment_method(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
```

#### 3.1.6 示例数据

```sql
INSERT INTO payment_order (platform_order_no, business_order_no, community_id, business_type, user_id, provider_id, payment_method_id, payment_scene_id, order_amount, paid_amount, fee_amount, order_status, version, provider_order_no, provider_trade_no, pay_time, description, notify_url, notify_status, notify_times, created_at) VALUES
-- =============================================
-- 小区10001 阳光花园 (子商户模式) - 多种订单状态
-- =============================================
-- 已支付订单
('PAY20260225100001001', 'BIZ20260225001', 10001, 'property_fee', 1001, 1, 2, 1, 150000, 150000, 90, 'paid', 0, 'WX1234567890001', '4200001234567001', '2026-02-25 10:15:30', '物业费缴纳-2026年1-3月', 'https://property.example.com/notify', 'success', 1, '2026-02-25 10:00:00'),
('PAY20260225100001002', 'BIZ20260225002', 10001, 'parking', 1002, 1, 1, 2, 2000, 2000, 1, 'paid', 0, 'WX1234567890002', '4200001234567002', '2026-02-25 11:20:15', '临时停车费-2小时', 'https://parking.example.com/notify', 'success', 1, '2026-02-25 11:10:00'),
('PAY20260225100001003', 'BIZ20260225003', 10001, 'property_fee', 1003, 3, 7, 4, 200000, 200000, 100, 'paid', 0, 'YB1234567890001', 'YB_TRADE_100001', '2026-02-25 12:00:00', '物业费托收-2026年1-6月', 'https://property.example.com/notify', 'success', 1, '2026-02-25 12:00:00'),
('PAY20260225100001004', 'BIZ20260225004', 10001, 'mall', 1004, 1, 3, 2, 29900, 29900, 18, 'paid', 0, 'WX1234567890003', '4200001234567003', '2026-02-25 14:30:00', '社区商城-家政服务', 'https://mall.example.com/notify', 'success', 1, '2026-02-25 14:20:00'),
-- 待支付订单
('PAY20260306100001005', 'BIZ20260306001', 10001, 'property_fee', 1005, 1, 2, 1, 50000, 0, 0, 'pending', 0, NULL, NULL, NULL, '物业费缴纳-2026年3月', 'https://property.example.com/notify', 'pending', 0, '2026-03-06 09:00:00'),
-- 已取消订单
('PAY20260301100001006', 'BIZ20260301001', 10001, 'parking', 1006, 1, 1, 2, 5000, 0, 0, 'cancelled', 0, NULL, NULL, NULL, '临时停车费-5小时', 'https://parking.example.com/notify', 'pending', 0, '2026-03-01 08:00:00'),
-- 已退款订单
('PAY20260220100001007', 'BIZ20260220001', 10001, 'mall', 1007, 1, 1, 2, 39900, 39900, 24, 'refunded', 1, 'WX1234567890004', '4200001234567004', '2026-02-20 16:00:00', '社区商城-水果拼盘', 'https://mall.example.com/notify', 'success', 1, '2026-02-20 15:50:00'),

-- =============================================
-- 小区10002 绿城花苑 (独立商户模式)
-- =============================================
('PAY20260225100002001', 'GC20260225001', 10002, 'property_fee', 2001, 1, 2, 1, 180000, 180000, 108, 'paid', 0, 'WX2345678901001', '4200002345678001', '2026-02-25 09:30:00', '物业费-2026年Q1', 'https://lvcheng.example.com/notify', 'success', 1, '2026-02-25 09:20:00'),
('PAY20260225100002002', 'GC20260225002', 10002, 'parking', 2002, 2, 4, 2, 36000, 36000, 22, 'paid', 0, 'ALI3456789012001', '2026022522001', '2026-02-25 10:45:00', '包月停车费-2026年3月', 'https://lvcheng.example.com/notify', 'success', 1, '2026-02-25 10:30:00'),

-- =============================================
-- 小区10003 碧水湾花园 (混合商户模式)
-- =============================================
-- 微信子商户支付
('PAY20260226100003001', 'BSW20260226001', 10003, 'property_fee', 3001, 1, 2, 1, 120000, 120000, 72, 'paid', 0, 'WX_SUB_3001', '4200003456789001', '2026-02-26 10:00:00', '物业费-2026年Q1(微信)', 'https://bsw.example.com/notify', 'success', 1, '2026-02-26 09:50:00'),
-- 支付宝独立商户支付
('PAY20260226100003002', 'BSW20260226002', 10003, 'property_fee', 3002, 2, 4, 1, 120000, 120000, 72, 'paid', 0, 'ALI_IND_3001', '2026022610001', '2026-02-26 10:30:00', '物业费-2026年Q1(支付宝)', 'https://bsw.example.com/notify', 'success', 1, '2026-02-26 10:20:00'),
-- 易宝子商户托收
('PAY20260301100003003', 'BSW20260301001', 10003, 'property_fee', 3003, 3, 7, 4, 240000, 240000, 120, 'paid', 0, 'YB_SUB_3001', 'YB_TRADE_300001', '2026-03-01 06:00:00', '物业费托收-2026年上半年', 'https://bsw.example.com/notify', 'success', 1, '2026-03-01 00:00:00'),
-- 混合模式-停车费
('PAY20260226100003004', 'BSW20260226003', 10003, 'parking', 3004, 1, 1, 2, 1500, 1500, 1, 'paid', 0, 'WX_SUB_3002', '4200003456789002', '2026-02-26 18:30:00', '临时停车费-1.5小时', 'https://bsw.example.com/notify', 'success', 1, '2026-02-26 18:25:00'),

-- =============================================
-- 小区10004 翡翠城 (另一种混合模式)
-- =============================================
-- 微信独立商户
('PAY20260227100004001', 'FC20260227001', 10004, 'property_fee', 4001, 1, 2, 1, 200000, 200000, 120, 'paid', 0, 'WX_IND_4001', '4200004567890001', '2026-02-27 11:00:00', '物业费-2026年Q1(微信独立)', 'https://fc.example.com/notify', 'success', 1, '2026-02-27 10:50:00'),
-- 支付宝子商户
('PAY20260227100004002', 'FC20260227002', 10004, 'property_fee', 4002, 2, 4, 1, 200000, 200000, 120, 'paid', 0, 'ALI_SUB_4001', '2026022711001', '2026-02-27 11:30:00', '物业费-2026年Q1(支付宝子商户)', 'https://fc.example.com/notify', 'success', 1, '2026-02-27 11:20:00'),
-- 银联独立商户
('PAY20260227100004003', 'FC20260227003', 10004, 'parking', 4003, 4, 8, 2, 3000, 3000, 2, 'paid', 0, 'UNION_IND_4001', '6200004567890001', '2026-02-27 15:00:00', '临时停车费-3小时(银联)', 'https://fc.example.com/notify', 'success', 1, '2026-02-27 14:55:00'),

-- =============================================
-- 小区10006 中央公馆 (总部直营-独立商户)
-- =============================================
('PAY20260228100006001', 'ZY20260228001', 10006, 'property_fee', 6001, 1, 2, 1, 500000, 500000, 300, 'paid', 0, 'WX_HEAD_6001', '4200006789012001', '2026-02-28 10:00:00', '物业费-2026年全年', 'https://central.example.com/notify', 'success', 1, '2026-02-28 09:50:00'),
('PAY20260228100006002', 'ZY20260228002', 10006, 'property_fee', 6002, 6, 10, 4, 500000, 500000, 250, 'paid', 0, 'ICBC_HEAD_6001', 'ICBC_TRADE_600001', '2026-02-28 06:00:00', '物业费托收-工行代扣-2026年全年', 'https://central.example.com/notify', 'success', 1, '2026-02-28 00:00:00'),
('PAY20260228100006003', 'ZY20260228003', 10006, 'value_added', 6003, 2, 5, 2, 99900, 99900, 60, 'paid', 0, 'ALI_HEAD_6001', '2026022812001', '2026-02-28 14:00:00', '增值服务-家电清洗套餐', 'https://central.example.com/notify', 'success', 1, '2026-02-28 13:50:00'),

-- =============================================
-- 小区99999 测试小区 (沙箱环境)
-- =============================================
('PAY20260306999990001', 'TEST20260306001', 99999, 'property_fee', 9001, 1, 2, 2, 1, 1, 0, 'paid', 0, 'WX_SANDBOX_001', 'SANDBOX_TRADE_001', '2026-03-06 10:00:00', '沙箱测试-物业费(1分钱)', 'https://test.example.com/notify', 'success', 1, '2026-03-06 09:55:00'),
('PAY20260306999990002', 'TEST20260306002', 99999, 'parking', 9002, 2, 4, 2, 1, 1, 0, 'paid', 0, 'ALI_SANDBOX_001', 'SANDBOX_ALI_001', '2026-03-06 10:30:00', '沙箱测试-停车费(1分钱)', 'https://test.example.com/notify', 'success', 1, '2026-03-06 10:25:00'),

-- =============================================
-- 通知失败的订单 (需要手动重试)
-- =============================================
('PAY20260305100001008', 'BIZ20260305001', 10001, 'property_fee', 1008, 1, 2, 1, 50000, 50000, 30, 'paid', 0, 'WX1234567890005', '4200001234567005', '2026-03-05 16:00:00', '物业费缴纳-2026年3月', 'https://property.example.com/notify', 'failed', 3, '2026-03-05 15:50:00'),
('PAY20260304100002003', 'GC20260304001', 10002, 'parking', 2003, 1, 1, 2, 8000, 8000, 5, 'paid', 0, 'WX2345678901002', '4200002345678002', '2026-03-04 20:00:00', '临时停车费-8小时', 'https://lvcheng.example.com/notify', 'failed', 5, '2026-03-04 19:50:00');
```

---

### 3.2 退款记录表 (payment_refund)

#### 3.2.1 表说明
存储所有退款记录

#### 3.2.2 字段设计

| 字段名 | 类型 | 长度 | 允许空 | 默认值 | 说明 |
|-------|------|------|-------|-------|------|
| id | BIGINT | - | 否 | 自增 | 主键ID,即退款ID |
| platform_order_no | VARCHAR | 64 | 否 | - | 原支付订单号 |
| business_refund_no | VARCHAR | 64 | 否 | - | 业务退款单号 (唯一) |
| refund_amount | BIGINT | - | 否 | - | 退款金额 (分) |
| refund_reason | VARCHAR | 255 | 否 | - | 退款原因 |
| refund_status | VARCHAR | 20 | 否 | processing | 退款状态:processing/success/failed |
| provider_refund_no | VARCHAR | 128 | 是 | NULL | 服务商退款单号 |
| refund_time | DATETIME | - | 是 | NULL | 退款成功时间 |
| operator_id | BIGINT | - | 否 | - | 操作人ID |
| notify_url | VARCHAR | 255 | 否 | - | 业务系统回调地址 |
| notify_status | VARCHAR | 20 | 否 | pending | 通知状态:pending/success/failed |
| notify_times | INT | - | 否 | 0 | 通知次数 |
| error_message | VARCHAR | 500 | 是 | NULL | 错误信息 (失败时填充) |
| created_at | DATETIME | - | 否 | CURRENT_TIMESTAMP | 创建时间 |
| updated_at | DATETIME | - | 否 | CURRENT_TIMESTAMP ON UPDATE | 更新时间 |
| remark | VARCHAR | 500 | 是 | NULL | 备注 |

#### 3.2.3 索引设计

| 索引名 | 索引类型 | 字段 | 说明 |
|-------|---------|------|------|
| PRIMARY | 主键 | id | 主键索引 |
| uk_business_refund_no | 唯一索引 | business_refund_no | 业务退款单号唯一 |
| idx_platform_order_no | 普通索引 | platform_order_no | 订单号查询 |
| idx_refund_status | 普通索引 | refund_status | 状态查询 |
| idx_created_at | 普通索引 | created_at | 创建时间排序 |

#### 3.2.4 示例数据

```sql
INSERT INTO payment_refund (platform_order_no, business_refund_no, refund_amount, refund_reason, refund_status, provider_refund_no, refund_time, operator_id, notify_url, notify_status, notify_times, created_at) VALUES
-- =============================================
-- 已成功退款
-- =============================================
-- 全额退款
('PAY20260220100001007', 'REFUND20260221001', 39900, '用户申请退款-商品质量问题', 'success', 'WX_REFUND_001', '2026-02-21 10:30:00', 9001, 'https://mall.example.com/refund-notify', 'success', 1, '2026-02-21 10:00:00'),
-- 部分退款
('PAY20260225100001001', 'REFUND20260226001', 50000, '用户申请部分退款-多缴物业费', 'success', 'WX_REFUND_002', '2026-02-26 14:30:00', 9001, 'https://property.example.com/refund-notify', 'success', 1, '2026-02-26 14:00:00'),
-- 碧水湾混合模式-支付宝独立商户退款
('PAY20260226100003002', 'REFUND20260227001', 30000, '用户申请部分退款-预缴费用退回', 'success', 'ALI_REFUND_001', '2026-02-27 16:00:00', 9002, 'https://bsw.example.com/refund-notify', 'success', 1, '2026-02-27 15:30:00'),
-- 总部直营-大额退款
('PAY20260228100006003', 'REFUND20260301001', 99900, '用户申请退款-服务未提供', 'success', 'ALI_REFUND_002', '2026-03-01 11:00:00', 9003, 'https://central.example.com/refund-notify', 'success', 1, '2026-03-01 10:30:00'),

-- =============================================
-- 退款处理中
-- =============================================
('PAY20260227100004001', 'REFUND20260306001', 100000, '用户申请部分退款-预缴物业费', 'processing', NULL, NULL, 9001, 'https://fc.example.com/refund-notify', 'pending', 0, '2026-03-06 09:00:00'),

-- =============================================
-- 退款失败
-- =============================================
('PAY20260225100001004', 'REFUND20260305001', 29900, '用户申请退款-服务已完成无法退款', 'failed', NULL, NULL, 9001, 'https://mall.example.com/refund-notify', 'pending', 0, '2026-03-05 16:00:00'),

-- =============================================
-- 通知失败的退款
-- =============================================
('PAY20260225100002001', 'REFUND20260302001', 50000, '业务退款-费用调整', 'success', 'WX_REFUND_003', '2026-03-02 15:00:00', 9002, 'https://lvcheng.example.com/refund-notify', 'failed', 3, '2026-03-02 14:30:00');
```

---

### 3.3 回调日志表 (payment_callback_log)

#### 3.3.1 表说明
记录所有服务商回调日志

#### 3.3.2 字段设计

| 字段名 | 类型 | 长度 | 允许空 | 默认值 | 说明 |
|-------|------|------|-------|-------|------|
| id | BIGINT | - | 否 | 自增 | 主键ID |
| platform_order_no | VARCHAR | 64 | 是 | NULL | 支付订单号 (如果是支付回调) |
| provider_id | BIGINT | - | 否 | - | 服务商ID |
| callback_type | VARCHAR | 20 | 否 | - | 回调类型:payment/refund/sign |
| callback_data | TEXT | - | 否 | - | 回调原始数据 (JSON) |
| verify_result | VARCHAR | 20 | 否 | - | 验签结果:success/failed |
| process_status | VARCHAR | 20 | 否 | - | 处理状态:success/failed |
| error_message | VARCHAR | 500 | 是 | NULL | 错误信息 |
| notify_status | VARCHAR | 20 | 否 | pending | 通知业务系统状态:pending/success/failed |
| notify_times | INT | - | 否 | 0 | 通知业务系统次数 |
| notify_url | VARCHAR | 255 | 是 | NULL | 业务系统回调地址 |
| notify_result | TEXT | 是 | NULL | 通知业务系统响应结果 |
| created_at | DATETIME | - | 否 | CURRENT_TIMESTAMP | 创建时间 |
| remark | VARCHAR | 500 | 是 | NULL | 备注 |

#### 3.3.3 索引设计

| 索引名 | 索引类型 | 字段 | 说明 |
|-------|---------|------|------|
| PRIMARY | 主键 | id | 主键索引 |
| idx_platform_order_no | 普通索引 | platform_order_no | 订单号查询 |
| idx_provider_id | 普通索引 | provider_id | 服务商查询 |
| idx_callback_type | 普通索引 | callback_type | 回调类型查询 |
| idx_notify_status | 普通索引 | notify_status | 通知状态查询 |
| idx_created_at | 普通索引 | created_at | 时间查询 |

---

## 四、业务流程

### 4.1 完整支付流程图

```
用户在业务系统下单
  ↓
业务系统调用支付中台下单接口
  ↓
支付中台校验参数
  ↓
调用路由服务获取支付配置
  ↓
验证签约状态 (如需要)
  ↓
调用服务商下单接口
  ↓
保存支付订单 (状态:pending)
  ↓
返回支付信息给业务系统
  ↓
业务系统引导用户支付
  ↓
用户完成支付
  ↓
服务商回调支付中台
  ↓
验证签名
  ↓
更新订单状态 (pending → paid)
  ↓
异步通知业务系统 (重试3次)
  ↓
业务系统更新订单状态
  ↓
支付完成
```

---

## 五、异常处理

### 5.1 下单异常

| 异常场景 | 处理方式 |
|---------|---------|
| 参数校验失败 | 返回错误码+错误信息 |
| 业务订单号重复 | 返回已有订单信息 (幂等) |
| 小区未配置支付方式 | 返回错误"小区未配置支付方式" |
| 签约验证失败 | 返回错误"请先完成签约" |
| 签约已失效 | 返回错误码 PAY-3000-02:"签约已失效,请重新签约" (v1.1.1新增) |
| 签约已解约 | 返回错误码 PAY-3000-03:"签约已解约,请重新签约" (v1.1.1新增) |
| 服务商下单失败 | 返回服务商错误信息 |
| 金额超限 | 返回错误"支付金额超限" |

### 5.2 退款异常

| 异常场景 | 处理方式 |
|---------|---------|
| 订单不存在 | 返回错误"订单不存在" |
| 订单状态不允许退款 | 返回错误"订单状态不支持退款" |
| 退款金额超过可退金额 | 返回错误"退款金额超过可退金额" |
| 服务商退款失败 | 返回服务商错误信息 |
| 重复退款 | 返回已有退款信息 (幂等) |
| 并发退款冲突 | 返回错误"退款请求失败,请重试" |

### 5.3 回调异常

| 异常场景 | 处理方式 |
|---------|---------|
| 签名验证失败 | 记录日志,返回失败,不处理 |
| 订单不存在 | 记录日志,返回失败 |
| 订单已处理 | 直接返回成功 (幂等) |
| 通知业务系统失败 | 自动重试3次,记录失败日志 |

---

## 六、与其他模块的关系

### 6.1 依赖关系

本模块依赖以下模块:

1. **支付渠道管理模块**
   - 获取服务商配置信息
   - 调用服务商接口

2. **小区支付配置模块**
   - 调用路由服务获取支付配置

3. **用户签约管理模块**
   - 验证签约状态
   - 使用签约协议号

### 6.2 被依赖关系

本模块被以下模块依赖:

1. **流水与对账模块**
   - 读取支付订单数据进行对账

2. **支付报表模块**
   - 统计支付订单数据

---

## 七、验收标准

### 7.1 功能验收

- [ ] 支持统一下单
- [ ] 支持支付参数校验
- [ ] 支持签约验证
- [ ] 支持路由服务调用
- [ ] 支持订单查询 (按订单号、业务订单号)
- [ ] 支持退款处理
- [ ] 支持部分退款和多次退款
- [ ] 支持退款并发控制 (乐观锁)
- [ ] 支持服务商支付回调处理
- [ ] 支持服务商退款回调处理
- [ ] 支持回调签名验证
- [ ] 支持异步通知业务系统
- [ ] 支持通知重试机制 (3次,间隔递增)
- [ ] 支持订单超时自动关闭
- [ ] 支持幂等性保证
- [ ] 所有操作记录日志

### 7.2 性能验收

- [ ] 下单响应时间 ≤ 2000ms (P95)
  - 参数校验: 10ms
  - 路由服务查询: 50ms
  - 签约验证: 100ms
  - 服务商下单: 1000-1500ms
  - 保存订单: 50ms
- [ ] 订单查询响应时间 ≤ 200ms
- [ ] 回调处理时间 ≤ 100ms (核心指标)
- [ ] 退款响应时间 ≤ 500ms
- [ ] 支持并发TPS ≥ 1000

### 7.3 可靠性验收

- [ ] 订单数据不丢失
- [ ] 订单状态准确
- [ ] 回调幂等性正确
- [ ] 通知重试机制有效
- [ ] 异常情况正确处理
- [ ] 退款并发控制有效

---

## 八、FAQ

### 8.1 为什么退款审核不在支付中台?

**回答**: 退款审核是业务逻辑,不同业务有不同的审核规则,应由业务系统完成。支付中台只负责执行退款操作,确保职责清晰。

### 8.2 回调通知业务系统失败怎么办?

**回答**:
- 系统会自动重试3次 (10s、30s、60s)
- 3次都失败后,记录失败日志
- 暂不支持从业务系统手动重新触发
- 业务系统可以主动查询订单状态

### 8.3 订单有效期是多久?

**回答**: 默认30分钟。超过30分钟未支付,订单自动关闭,用户需重新下单。

### 8.4 支持部分退款吗?

**回答**: 支持。可以多次退款,但总退款金额不能超过实付金额。

### 8.5 手续费如何处理?

**回答**:
- 手续费从服务商回调中获取
- 退款时手续费不退还
- 手续费用于报表统计和成本分析

### 8.6 如何防止并发退款超额?

**回答**: 使用乐观锁机制(version字段),在更新订单退款金额时校验版本号,确保并发安全。如果并发冲突,提示用户重试。

### 8.7 订单超时关闭会通知业务系统吗?

**回答**: 不会。订单超时关闭是系统自动行为,业务系统可主动查询订单状态获取最新信息。

---

## 九、统一错误码设计

### 9.1 错误码分类体系

```
错误码格式: PAY-XXXX-YY
- PAY: 支付中台标识
- XXXX: 模块代码
- YY: 错误序号

模块代码:
- 1000: 参数校验
- 2000: 路由服务
- 3000: 签约管理
- 4000: 支付交易
- 5000: 退款处理
- 6000: 对账管理
- 9000: 系统错误
```

### 9.2 常见错误码列表

#### 参数校验错误 (PAY-1000-XX)

| 错误码 | 错误信息 | 说明 |
|-------|---------|------|
| PAY-1000-01 | 缺少必填参数:[参数名] | 必填参数缺失 |
| PAY-1000-02 | 支付金额不合法 | 金额≤0或超过限额 |
| PAY-1000-03 | 业务订单号已存在 | 幂等性校验失败 |
| PAY-1000-04 | 回调地址格式不正确 | URL格式校验失败 |

#### 路由服务错误 (PAY-2000-XX)

| 错误码 | 错误信息 | 说明 |
|-------|---------|------|
| PAY-2000-01 | 小区未配置支付方式 | 小区无支付配置 |
| PAY-2000-02 | 小区未配置该支付方式 | 指定的支付方式不可用 |
| PAY-2000-03 | 支付配置已停用 | 配置被禁用 |

#### 签约管理错误 (PAY-3000-XX)

| 错误码 | 错误信息 | 说明 |
|-------|---------|------|
| PAY-3000-01 | 该支付方式需要签约,请先完成签约 | 未签约 |
| PAY-3000-02 | 签约已失效,请重新签约 | 签约已解约 |
| PAY-3000-03 | 签约服务商不匹配 | 签约与支付服务商不一致 |

#### 支付交易错误 (PAY-4000-XX)

| 错误码 | 错误信息 | 说明 |
|-------|---------|------|
| PAY-4000-01 | 订单不存在 | 订单号错误 |
| PAY-4000-02 | 订单状态不支持该操作 | 状态流转错误 |
| PAY-4000-03 | 服务商下单失败:[原因] | 服务商接口调用失败 |

#### 退款处理错误 (PAY-5000-XX)

| 错误码 | 错误信息 | 说明 |
|-------|---------|------|
| PAY-5000-01 | 退款金额超过可退金额 | 金额校验失败 |
| PAY-5000-02 | 订单状态不支持退款 | 只有paid/part_refunded可退款 |
| PAY-5000-03 | 退款单号重复 | 幂等性校验失败 |
| PAY-5000-04 | 退款请求失败,请重试 | 并发冲突 |

#### 系统错误 (PAY-9000-XX)

| 错误码 | 错误信息 | 说明 |
|-------|---------|------|
| PAY-9000-01 | 系统繁忙,请稍后重试 | 系统异常 |
| PAY-9000-02 | 数据库操作失败 | DB异常 |
| PAY-9000-03 | 服务商接口超时 | 网络超时 |

### 9.3 错误响应格式

```json
{
  "code": "PAY-1000-01",
  "message": "缺少必填参数:community_id",
  "timestamp": "2026-02-26 10:00:00",
  "path": "/api/payment/order/create"
}
```

---

## 十、v1.0到v1.1数据迁移

### 10.1 数据库结构变更

```sql
-- 1. payment_order表增加version字段
ALTER TABLE payment_order
ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT '版本号,用于乐观锁' AFTER order_status;

-- 2. payment_order表增加复合索引
CREATE INDEX idx_community_business_status_time
ON payment_order(community_id, business_type, order_status, pay_time);

-- 3. payment_callback_log表增加字段
ALTER TABLE payment_callback_log
ADD COLUMN notify_status VARCHAR(20) DEFAULT 'pending' COMMENT '通知业务系统状态:pending/success/failed',
ADD COLUMN notify_times INT DEFAULT 0 COMMENT '通知业务系统次数',
ADD COLUMN notify_url VARCHAR(255) COMMENT '业务系统回调地址',
ADD COLUMN notify_result TEXT COMMENT '通知业务系统响应结果';

-- 4. payment_callback_log表增加索引
CREATE INDEX idx_notify_status ON payment_callback_log(notify_status);
```

### 10.2 定时任务创建

```
根据技术栈创建定时任务:
- 使用Quartz/XXL-JOB/Cron等调度框架
- 任务名称: OrderTimeoutCloseJob
- 执行频率: 每5分钟
- 任务内容: 关闭超时未支付订单
```

### 10.3 迁移注意事项

1. **迁移前备份**
   - 备份 payment_order 表
   - 备份 payment_callback_log 表

2. **分步执行**
   - 先在测试环境验证
   - 逐步执行迁移脚本
   - 每步执行后检查数据

3. **业务影响**
   - 建议在业务低峰期执行
   - 预计停机时间: 10分钟
   - 迁移过程中暂停支付服务

---

## 变更记录

| 版本 | 日期 | 修改人 | 修改内容 |
|------|------|-------|---------|
| v1.0 | 2026-02-25 | 产品经理 | 初始版本 |
| v1.1 | 2026-02-26 | 产品经理 | 1. 新增退款回调处理章节(2.4)<br>2. 新增订单超时关闭机制(2.6)<br>3. 新增统一错误码设计(第九章)<br>4. 增加退款并发控制(乐观锁)(2.3.3)<br>5. payment_order表增加version字段和复合索引<br>6. payment_callback_log表增加4个通知相关字段<br>7. 完善异步通知重试机制(2.4.3)<br>8. 统一订单状态定义(3.1.3)<br>9. 调整性能指标(500ms→2000ms)(7.2)<br>10. 新增v1.0到v1.1数据迁移章节(第十章) |
| v1.1.1 | 2026-02-26 | 产品经理 | 1. 增强签约验证逻辑,检查invalid和cancelled状态<br>2. 新增签约失效异常处理<br>3. 支付流程增加签约失效检查<br>4. 完善签约验证业务规则说明 |
