397 lines
13 KiB
Markdown
397 lines
13 KiB
Markdown
|
|
# 业务逻辑与数据模型 (Business Logic & Data Model) v4.0
|
|||
|
|
|
|||
|
|
> 定义支付系统的核心数据结构和业务流程
|
|||
|
|
|
|||
|
|
## 📊 数据库表结构
|
|||
|
|
|
|||
|
|
### 1. 订单表 (orders)
|
|||
|
|
|
|||
|
|
存储业务订单信息,与支付解耦。
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE `orders` (
|
|||
|
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|||
|
|
`sn` VARCHAR(32) NOT NULL COMMENT '订单号 (业务唯一)',
|
|||
|
|
`user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
|
|||
|
|
`title` VARCHAR(128) NOT NULL COMMENT '订单标题',
|
|||
|
|
`price_amount` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '订单原价 (分)',
|
|||
|
|
`pay_amount` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '应付金额 (分)',
|
|||
|
|
`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币类型',
|
|||
|
|
`status` VARCHAR(20) NOT NULL DEFAULT 'created' COMMENT '状态: created/paying/paid/closed/refunded',
|
|||
|
|
`product_id` VARCHAR(64) DEFAULT NULL COMMENT '商品ID',
|
|||
|
|
`product_type` VARCHAR(32) DEFAULT NULL COMMENT '商品类型',
|
|||
|
|
`extra_data` JSON DEFAULT NULL COMMENT '扩展数据',
|
|||
|
|
`paid_at` DATETIME DEFAULT NULL COMMENT '支付时间',
|
|||
|
|
`closed_at` DATETIME DEFAULT NULL COMMENT '关闭时间',
|
|||
|
|
`expired_at` DATETIME DEFAULT NULL COMMENT '过期时间',
|
|||
|
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|||
|
|
PRIMARY KEY (`id`),
|
|||
|
|
UNIQUE KEY `uk_sn` (`sn`),
|
|||
|
|
KEY `idx_user_id` (`user_id`),
|
|||
|
|
KEY `idx_status` (`status`),
|
|||
|
|
KEY `idx_created_at` (`created_at`)
|
|||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 交易流水表 (pay_trades)
|
|||
|
|
|
|||
|
|
记录每一次支付尝试,一个订单可能有多次交易。
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE `pay_trades` (
|
|||
|
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|||
|
|
`trade_sn` VARCHAR(32) NOT NULL COMMENT '交易流水号 (系统生成)',
|
|||
|
|
`order_sn` VARCHAR(32) NOT NULL COMMENT '关联订单号',
|
|||
|
|
`user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
|
|||
|
|
`title` VARCHAR(128) NOT NULL COMMENT '交易标题',
|
|||
|
|
`amount` BIGINT UNSIGNED NOT NULL COMMENT '交易金额 (分)',
|
|||
|
|
`cash_amount` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '现金支付金额 (分)',
|
|||
|
|
`coin_amount` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '虚拟币抵扣金额',
|
|||
|
|
`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币类型',
|
|||
|
|
`platform` VARCHAR(32) NOT NULL COMMENT '支付平台: alipay/wechat/paypal/stripe/usdt/coin',
|
|||
|
|
`platform_type` VARCHAR(32) DEFAULT NULL COMMENT '平台子类型: web/wap/jsapi/native/h5/app',
|
|||
|
|
`platform_sn` VARCHAR(64) DEFAULT NULL COMMENT '平台交易号',
|
|||
|
|
`platform_created_params` JSON DEFAULT NULL COMMENT '发送给平台的参数',
|
|||
|
|
`platform_created_result` JSON DEFAULT NULL COMMENT '平台返回的结果',
|
|||
|
|
`status` VARCHAR(20) NOT NULL DEFAULT 'paying' COMMENT '状态: paying/paid/closed/refunded',
|
|||
|
|
`type` VARCHAR(20) NOT NULL DEFAULT 'purchase' COMMENT '类型: purchase(购买)/recharge(充值)',
|
|||
|
|
`pay_time` DATETIME DEFAULT NULL COMMENT '支付时间',
|
|||
|
|
`notify_data` JSON DEFAULT NULL COMMENT '回调原始数据',
|
|||
|
|
`seller_id` VARCHAR(64) DEFAULT NULL COMMENT '卖家ID (多商户场景)',
|
|||
|
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|||
|
|
PRIMARY KEY (`id`),
|
|||
|
|
UNIQUE KEY `uk_trade_sn` (`trade_sn`),
|
|||
|
|
KEY `idx_order_sn` (`order_sn`),
|
|||
|
|
KEY `idx_platform_sn` (`platform_sn`),
|
|||
|
|
KEY `idx_user_id` (`user_id`),
|
|||
|
|
KEY `idx_status` (`status`),
|
|||
|
|
KEY `idx_created_at` (`created_at`)
|
|||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易流水表';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 资金流水表 (cashflows)
|
|||
|
|
|
|||
|
|
记录账户资金变动(可选,用于虚拟币/钱包场景)。
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE `cashflows` (
|
|||
|
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|||
|
|
`sn` VARCHAR(32) NOT NULL COMMENT '流水号',
|
|||
|
|
`user_id` VARCHAR(64) NOT NULL COMMENT '用户ID',
|
|||
|
|
`type` VARCHAR(20) NOT NULL COMMENT '类型: inflow(入账)/outflow(出账)',
|
|||
|
|
`action` VARCHAR(32) NOT NULL COMMENT '动作: recharge/purchase/refund/transfer',
|
|||
|
|
`amount` BIGINT NOT NULL COMMENT '金额 (分,正数入账负数出账)',
|
|||
|
|
`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY',
|
|||
|
|
`balance_before` BIGINT NOT NULL DEFAULT 0 COMMENT '变动前余额',
|
|||
|
|
`balance_after` BIGINT NOT NULL DEFAULT 0 COMMENT '变动后余额',
|
|||
|
|
`trade_sn` VARCHAR(32) DEFAULT NULL COMMENT '关联交易流水号',
|
|||
|
|
`order_sn` VARCHAR(32) DEFAULT NULL COMMENT '关联订单号',
|
|||
|
|
`remark` VARCHAR(256) DEFAULT NULL COMMENT '备注',
|
|||
|
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
PRIMARY KEY (`id`),
|
|||
|
|
UNIQUE KEY `uk_sn` (`sn`),
|
|||
|
|
KEY `idx_user_id` (`user_id`),
|
|||
|
|
KEY `idx_trade_sn` (`trade_sn`),
|
|||
|
|
KEY `idx_created_at` (`created_at`)
|
|||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资金流水表';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 退款记录表 (refunds)
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE `refunds` (
|
|||
|
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
|||
|
|
`refund_sn` VARCHAR(32) NOT NULL COMMENT '退款单号',
|
|||
|
|
`trade_sn` VARCHAR(32) NOT NULL COMMENT '原交易流水号',
|
|||
|
|
`order_sn` VARCHAR(32) NOT NULL COMMENT '原订单号',
|
|||
|
|
`amount` BIGINT UNSIGNED NOT NULL COMMENT '退款金额 (分)',
|
|||
|
|
`reason` VARCHAR(256) DEFAULT NULL COMMENT '退款原因',
|
|||
|
|
`status` VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/processing/success/failed',
|
|||
|
|
`platform_refund_sn` VARCHAR(64) DEFAULT NULL COMMENT '平台退款单号',
|
|||
|
|
`refunded_at` DATETIME DEFAULT NULL COMMENT '退款完成时间',
|
|||
|
|
`operator_id` VARCHAR(64) DEFAULT NULL COMMENT '操作人ID',
|
|||
|
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|||
|
|
PRIMARY KEY (`id`),
|
|||
|
|
UNIQUE KEY `uk_refund_sn` (`refund_sn`),
|
|||
|
|
KEY `idx_trade_sn` (`trade_sn`),
|
|||
|
|
KEY `idx_order_sn` (`order_sn`)
|
|||
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='退款记录表';
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔄 状态机定义
|
|||
|
|
|
|||
|
|
### 订单状态 (Order Status)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
┌─────────────────────────────────────────────────┐
|
|||
|
|
│ created │
|
|||
|
|
│ │ │
|
|||
|
|
│ ┌───────────┼───────────┐ │
|
|||
|
|
│ ▼ │ ▼ │
|
|||
|
|
│ paying ────────┼───────► closed │
|
|||
|
|
│ │ │ │
|
|||
|
|
│ ▼ │ │
|
|||
|
|
│ paid ─────────┼───────► refunded │
|
|||
|
|
│ │ │
|
|||
|
|
└─────────────────────────────────────────────────┘
|
|||
|
|
|
|||
|
|
状态说明:
|
|||
|
|
- created: 订单已创建,等待支付
|
|||
|
|
- paying: 支付中 (已发起支付请求)
|
|||
|
|
- paid: 已支付
|
|||
|
|
- closed: 已关闭 (超时/主动取消)
|
|||
|
|
- refunded: 已退款
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 交易状态 (Trade Status)
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
paying → paid
|
|||
|
|
↓ ↓
|
|||
|
|
closed refunded
|
|||
|
|
|
|||
|
|
状态说明:
|
|||
|
|
- paying: 支付中
|
|||
|
|
- paid: 支付成功
|
|||
|
|
- closed: 交易关闭
|
|||
|
|
- refunded: 已退款
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔢 编号规则
|
|||
|
|
|
|||
|
|
### 订单号 (order_sn)
|
|||
|
|
```
|
|||
|
|
格式: YYYYMMDD + 6位随机数
|
|||
|
|
示例: 202401170001
|
|||
|
|
|
|||
|
|
生成规则:
|
|||
|
|
1. 日期前缀保证每日唯一空间
|
|||
|
|
2. 随机数使用分布式ID生成器
|
|||
|
|
3. 支持前缀自定义 (如区分业务线)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 交易流水号 (trade_sn)
|
|||
|
|
```
|
|||
|
|
格式: T + YYYYMMDD + HHmmss + 5位随机数
|
|||
|
|
示例: T20240117100530123456
|
|||
|
|
|
|||
|
|
生成规则:
|
|||
|
|
1. 前缀 T 标识交易类型
|
|||
|
|
2. 精确到秒的时间戳
|
|||
|
|
3. 5位随机数防碰撞
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📋 核心业务流程
|
|||
|
|
|
|||
|
|
### 1. 标准支付流程
|
|||
|
|
|
|||
|
|
```sequence
|
|||
|
|
用户 -> 业务系统: 1. 提交订单
|
|||
|
|
业务系统 -> 支付模块: 2. 创建订单 (create_order)
|
|||
|
|
支付模块 -> 业务系统: 3. 返回 order_sn
|
|||
|
|
|
|||
|
|
用户 -> 支付模块: 4. 选择支付方式并支付 (checkout)
|
|||
|
|
支付模块 -> 支付平台: 5. 创建平台交易
|
|||
|
|
支付平台 -> 支付模块: 6. 返回支付参数
|
|||
|
|
支付模块 -> 用户: 7. 返回支付数据 (二维码/跳转链接)
|
|||
|
|
|
|||
|
|
用户 -> 支付平台: 8. 完成支付
|
|||
|
|
支付平台 -> 支付模块: 9. 异步回调 (notify)
|
|||
|
|
支付模块 -> 支付模块: 10. 验签 + 更新状态
|
|||
|
|
支付模块 -> 业务系统: 11. 触发业务回调 (发货/开通)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 支付回调处理流程
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def handle_notify(gateway, data):
|
|||
|
|
# 1. 加载对应的支付网关驱动
|
|||
|
|
driver = PaymentFactory.create(gateway)
|
|||
|
|
|
|||
|
|
# 2. 验证签名
|
|||
|
|
if not driver.verify_sign(data):
|
|||
|
|
raise SignatureError("签名验证失败")
|
|||
|
|
|
|||
|
|
# 3. 解析回调数据
|
|||
|
|
parsed = driver.parse_notify(data)
|
|||
|
|
trade_sn = parsed['trade_sn']
|
|||
|
|
|
|||
|
|
# 4. 幂等性检查
|
|||
|
|
trade = Trade.get_by_sn(trade_sn)
|
|||
|
|
if trade.status == 'paid':
|
|||
|
|
return driver.success_response() # 已处理过,直接返回成功
|
|||
|
|
|
|||
|
|
# 5. 金额校验
|
|||
|
|
if parsed['amount'] != trade.cash_amount:
|
|||
|
|
raise AmountMismatchError("金额不匹配")
|
|||
|
|
|
|||
|
|
# 6. 更新交易状态
|
|||
|
|
trade.update({
|
|||
|
|
'status': 'paid',
|
|||
|
|
'platform_sn': parsed['platform_sn'],
|
|||
|
|
'pay_time': parsed['pay_time'],
|
|||
|
|
'notify_data': data
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 7. 更新订单状态
|
|||
|
|
order = Order.get_by_sn(trade.order_sn)
|
|||
|
|
order.update({'status': 'paid', 'paid_at': now()})
|
|||
|
|
|
|||
|
|
# 8. 触发业务回调
|
|||
|
|
dispatch_event('order.paid', order)
|
|||
|
|
|
|||
|
|
# 9. 返回成功响应
|
|||
|
|
return driver.success_response()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 退款流程
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def apply_refund(trade_sn, amount, reason):
|
|||
|
|
trade = Trade.get_by_sn(trade_sn)
|
|||
|
|
|
|||
|
|
# 1. 状态检查
|
|||
|
|
if trade.status != 'paid':
|
|||
|
|
raise InvalidStatusError("只有已支付的交易可以退款")
|
|||
|
|
|
|||
|
|
# 2. 创建退款记录
|
|||
|
|
refund = Refund.create({
|
|||
|
|
'refund_sn': generate_refund_sn(),
|
|||
|
|
'trade_sn': trade_sn,
|
|||
|
|
'amount': amount,
|
|||
|
|
'reason': reason,
|
|||
|
|
'status': 'pending'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 3. 调用平台退款接口
|
|||
|
|
driver = PaymentFactory.create(trade.platform)
|
|||
|
|
result = driver.refund({
|
|||
|
|
'trade_sn': trade_sn,
|
|||
|
|
'refund_sn': refund.refund_sn,
|
|||
|
|
'amount': amount
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 4. 更新状态
|
|||
|
|
if result.success:
|
|||
|
|
refund.update({'status': 'success', 'refunded_at': now()})
|
|||
|
|
trade.update({'status': 'refunded'})
|
|||
|
|
else:
|
|||
|
|
refund.update({'status': 'failed'})
|
|||
|
|
|
|||
|
|
return refund
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🏭 工厂模式设计
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class PaymentFactory:
|
|||
|
|
"""支付网关工厂"""
|
|||
|
|
|
|||
|
|
_drivers = {
|
|||
|
|
'alipay': AlipayGateway,
|
|||
|
|
'wechat': WechatGateway,
|
|||
|
|
'paypal': PayPalGateway,
|
|||
|
|
'stripe': StripeGateway,
|
|||
|
|
'usdt': USDTGateway,
|
|||
|
|
'coin': CoinGateway,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@classmethod
|
|||
|
|
def create(cls, gateway: str) -> AbstractGateway:
|
|||
|
|
gateway_name = gateway.split('_')[0] # wechat_jsapi -> wechat
|
|||
|
|
|
|||
|
|
if gateway_name not in cls._drivers:
|
|||
|
|
raise ValueError(f"不支持的支付网关: {gateway}")
|
|||
|
|
|
|||
|
|
driver_class = cls._drivers[gateway_name]
|
|||
|
|
return driver_class(config=get_payment_config(gateway_name))
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class AbstractGateway(ABC):
|
|||
|
|
"""支付网关抽象基类"""
|
|||
|
|
|
|||
|
|
@abstractmethod
|
|||
|
|
def create_trade(self, data: dict) -> dict:
|
|||
|
|
"""创建交易"""
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
@abstractmethod
|
|||
|
|
def verify_sign(self, data: dict) -> bool:
|
|||
|
|
"""验证签名"""
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
@abstractmethod
|
|||
|
|
def parse_notify(self, data: dict) -> dict:
|
|||
|
|
"""解析回调数据"""
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
@abstractmethod
|
|||
|
|
def refund(self, data: dict) -> RefundResult:
|
|||
|
|
"""发起退款"""
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
@abstractmethod
|
|||
|
|
def query_trade(self, trade_sn: str) -> dict:
|
|||
|
|
"""查询交易"""
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
@abstractmethod
|
|||
|
|
def close_trade(self, trade_sn: str) -> bool:
|
|||
|
|
"""关闭交易"""
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def success_response(self) -> str:
|
|||
|
|
"""回调成功响应"""
|
|||
|
|
return "success"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 💰 金额处理规范
|
|||
|
|
|
|||
|
|
### 1. 存储规则
|
|||
|
|
- 数据库统一使用**分**为单位 (BIGINT)
|
|||
|
|
- 避免浮点数精度问题
|
|||
|
|
|
|||
|
|
### 2. 接口规则
|
|||
|
|
- API 输入输出统一使用**元**为单位
|
|||
|
|
- 内部转换: `分 = 元 × 100`
|
|||
|
|
|
|||
|
|
### 3. 转换示例
|
|||
|
|
```python
|
|||
|
|
# 元转分 (API输入 -> 数据库)
|
|||
|
|
def yuan_to_fen(yuan: float) -> int:
|
|||
|
|
return int(round(yuan * 100))
|
|||
|
|
|
|||
|
|
# 分转元 (数据库 -> API输出)
|
|||
|
|
def fen_to_yuan(fen: int) -> float:
|
|||
|
|
return round(fen / 100, 2)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔐 幂等性设计
|
|||
|
|
|
|||
|
|
### 1. 订单创建幂等
|
|||
|
|
- 使用 `(user_id, product_id, created_date)` 组合判断
|
|||
|
|
- 或使用客户端传入的幂等键 `idempotency_key`
|
|||
|
|
|
|||
|
|
### 2. 支付回调幂等
|
|||
|
|
- 检查交易状态,已支付则直接返回成功
|
|||
|
|
- 使用数据库事务 + 行锁保证并发安全
|
|||
|
|
|
|||
|
|
### 3. 退款幂等
|
|||
|
|
- 同一笔交易只能退款一次 (或限制总退款金额)
|