feat: 完整重构小程序匹配功能 + 修复UI对齐 + 文章数据API
主要更新: 1. 按H5网页端完全重构匹配功能(match页面) - 4种匹配类型: 创业合伙/资源对接/导师顾问/团队招募 - 资源对接等类型弹出手机号/微信号输入框 - 去掉重新匹配按钮,改为返回按钮 2. 修复所有卡片对齐和宽度问题 - 目录页附录卡片居中 - 首页阅读进度卡片满宽度 - 我的页面菜单卡片对齐 - 推广中心分享卡片统一宽度 3. 修复目录页图标和文字对齐 - section-icon固定40rpx宽高 - section-title与图标垂直居中 4. 更新真实完整文章标题(62篇) - 从book目录读取真实markdown文件名 - 替换之前的简化标题 5. 新增文章数据API - /api/db/chapters - 获取完整书籍结构 - 支持按ID获取单篇文章内容
This commit is contained in:
68
addons/Universal_Payment_Module copy/.cursorrules
Normal file
68
addons/Universal_Payment_Module copy/.cursorrules
Normal file
@@ -0,0 +1,68 @@
|
||||
# Universal Payment Module - Cursor 规则
|
||||
# 将此文件放在使用支付模块的项目根目录
|
||||
|
||||
## 角色设定
|
||||
你是一位精通全球支付架构的资深全栈工程师,专注于支付网关集成、安全合规和高可用设计。
|
||||
|
||||
当用户提及"支付模块"、"支付功能"、"接入支付"时,请参考 `Universal_Payment_Module` 目录中的设计文档。
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 配置驱动
|
||||
- 所有支付密钥通过环境变量配置,绝不硬编码
|
||||
- 使用 `.env` 文件管理配置
|
||||
- 支持多环境切换 (development/staging/production)
|
||||
|
||||
### 2. 工厂模式
|
||||
- 使用 `PaymentFactory` 统一管理支付网关
|
||||
- 每个网关实现统一的 `AbstractGateway` 接口
|
||||
- 支持: alipay/wechat/paypal/stripe/usdt
|
||||
|
||||
### 3. 安全优先
|
||||
- 所有回调必须验证签名
|
||||
- 必须验证支付金额与订单金额匹配
|
||||
- 使用 HTTPS,敏感数据脱敏
|
||||
|
||||
### 4. 幂等性
|
||||
- 支付回调必须支持重复调用
|
||||
- 使用分布式锁防止并发问题
|
||||
|
||||
## API 接口
|
||||
```
|
||||
POST /api/payment/create_order - 创建订单
|
||||
POST /api/payment/checkout - 发起支付
|
||||
GET /api/payment/status/{sn} - 查询状态
|
||||
POST /api/payment/notify/{gw} - 回调通知
|
||||
```
|
||||
|
||||
## 统一响应格式
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
## 数据库
|
||||
- orders - 订单表
|
||||
- pay_trades - 交易流水表
|
||||
- 金额单位: 数据库用分,API用元
|
||||
|
||||
## 卡若支付配置
|
||||
- 微信商户号: 1318592501
|
||||
- 支付宝PID: 2088511801157159
|
||||
- 详细配置见 `4_卡若配置/.env.example`
|
||||
|
||||
## 禁止事项
|
||||
❌ 密钥硬编码
|
||||
❌ 跳过签名验证
|
||||
❌ 信任前端金额
|
||||
❌ 回调不做幂等
|
||||
❌ 使用 HTTP
|
||||
|
||||
## 参考文档
|
||||
- API定义: `1_核心设计_通用协议/API接口定义.md`
|
||||
- 数据模型: `1_核心设计_通用协议/业务逻辑与模型.md`
|
||||
- 安全规范: `1_核心设计_通用协议/安全与合规.md`
|
||||
- AI指令: `2_智能对接_AI指令/通用集成指令.md`
|
||||
382
addons/Universal_Payment_Module copy/1_核心设计_通用协议/API接口定义.md
Normal file
382
addons/Universal_Payment_Module copy/1_核心设计_通用协议/API接口定义.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# 通用支付模块 API 接口定义 (Universal Payment API) v4.0
|
||||
|
||||
> 无论后端使用何种语言(Python/Node/Go/Java/PHP),请严格实现以下 RESTful 接口
|
||||
|
||||
## 🎯 设计原则
|
||||
|
||||
1. **RESTful 风格**: 资源命名统一,动词语义清晰
|
||||
2. **统一响应格式**: 所有接口返回 `{code, message, data}` 结构
|
||||
3. **幂等性**: 重复请求不产生副作用
|
||||
4. **安全性**: 敏感操作需签名验证
|
||||
|
||||
---
|
||||
|
||||
## 📦 统一响应格式
|
||||
|
||||
### 成功响应
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 错误响应
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "参数错误:order_sn 不能为空",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 常用错误码
|
||||
| Code | 含义 |
|
||||
|:---|:---|
|
||||
| 200 | 成功 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 401 | 未授权/登录过期 |
|
||||
| 403 | 无权限 |
|
||||
| 404 | 资源不存在 |
|
||||
| 409 | 状态冲突 (如重复支付) |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心交易接口 (Core Transaction)
|
||||
|
||||
### 1.1 创建订单
|
||||
业务系统调用,创建一个待支付订单。
|
||||
|
||||
```http
|
||||
POST /api/payment/create_order
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"user_id": "u1001", // [必填] 用户ID
|
||||
"title": "VIP会员月卡", // [必填] 订单标题 (≤30字符)
|
||||
"amount": 99.00, // [必填] 金额 (单位: 元)
|
||||
"currency": "CNY", // [可选] 币种,默认 CNY
|
||||
"product_id": "vip_monthly", // [可选] 商品ID
|
||||
"product_type": "membership", // [可选] 商品类型
|
||||
"extra_params": { // [可选] 扩展参数 (会透传到回调)
|
||||
"coupon_id": "C001",
|
||||
"referrer": "user_123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"order_sn": "202401170001", // 系统生成的订单号
|
||||
"status": "created", // 订单状态
|
||||
"amount": 99.00,
|
||||
"expire_at": "2024-01-17T11:30:00Z" // 订单过期时间
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 发起支付 (收银台)
|
||||
用户选择支付方式后,获取支付参数。
|
||||
|
||||
```http
|
||||
POST /api/payment/checkout
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"order_sn": "202401170001", // [必填] 订单号
|
||||
"gateway": "wechat_jsapi", // [必填] 支付网关 (见下方枚举)
|
||||
"return_url": "https://...", // [可选] 支付成功后跳转地址
|
||||
"openid": "oXxx...", // [条件] 微信JSAPI必填
|
||||
"coin_amount": 0 // [可选] 使用虚拟币抵扣金额
|
||||
}
|
||||
```
|
||||
|
||||
**Gateway 支付网关枚举**:
|
||||
| Gateway | 说明 | 返回类型 |
|
||||
|:---|:---|:---|
|
||||
| `alipay_web` | 支付宝PC网页 | url (跳转) |
|
||||
| `alipay_wap` | 支付宝H5 | url (跳转) |
|
||||
| `alipay_qr` | 支付宝扫码 | qrcode |
|
||||
| `wechat_native` | 微信扫码 | qrcode |
|
||||
| `wechat_jsapi` | 微信公众号/小程序 | json (SDK参数) |
|
||||
| `wechat_h5` | 微信H5 | url (跳转) |
|
||||
| `wechat_app` | 微信APP | json (SDK参数) |
|
||||
| `paypal` | PayPal | url (跳转) |
|
||||
| `stripe` | Stripe | url (Checkout Session) |
|
||||
| `usdt` | USDT-TRC20 | address (钱包地址) |
|
||||
| `coin` | 纯虚拟币支付 | direct (直接完成) |
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"trade_sn": "T20240117100001", // 交易流水号
|
||||
"type": "qrcode", // 响应类型: url/qrcode/json/address/direct
|
||||
"payload": "weixin://wxpay/...",// 支付数据 (根据type不同)
|
||||
"expiration": 1800, // 过期时间 (秒)
|
||||
"amount": 99.00, // 实际支付金额 (扣除抵扣后)
|
||||
"coin_deducted": 0 // 虚拟币抵扣金额
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**不同 type 的 payload 格式**:
|
||||
|
||||
```javascript
|
||||
// type: "url" - 跳转链接
|
||||
payload: "https://openapi.alipay.com/gateway.do?..."
|
||||
|
||||
// type: "qrcode" - 二维码内容
|
||||
payload: "weixin://wxpay/bizpayurl?pr=xxx"
|
||||
|
||||
// type: "json" - SDK调起参数 (微信JSAPI)
|
||||
payload: {
|
||||
"appId": "wx...",
|
||||
"timeStamp": "1705470600",
|
||||
"nonceStr": "xxx",
|
||||
"package": "prepay_id=wx...",
|
||||
"signType": "RSA",
|
||||
"paySign": "xxx"
|
||||
}
|
||||
|
||||
// type: "address" - 加密货币地址
|
||||
payload: {
|
||||
"address": "TXxx...",
|
||||
"amount_usdt": 13.88,
|
||||
"memo": "202401170001"
|
||||
}
|
||||
|
||||
// type: "direct" - 直接完成 (纯虚拟币支付)
|
||||
payload: { "status": "paid" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.3 查询订单状态
|
||||
前端轮询使用,判断支付是否完成。
|
||||
|
||||
```http
|
||||
GET /api/payment/status/{order_sn}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"order_sn": "202401170001",
|
||||
"status": "paid", // created/paying/paid/closed/refunded
|
||||
"paid_amount": 99.00,
|
||||
"paid_at": "2024-01-17T10:05:00Z",
|
||||
"payment_method": "wechat_jsapi",
|
||||
"trade_sn": "T20240117100001"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**订单状态机**:
|
||||
```
|
||||
created → paying → paid → (refunded)
|
||||
↓ ↓
|
||||
closed closed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.4 关闭订单
|
||||
主动关闭未支付的订单。
|
||||
|
||||
```http
|
||||
POST /api/payment/close/{order_sn}
|
||||
Authorization: Bearer {token}
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"order_sn": "202401170001",
|
||||
"status": "closed",
|
||||
"closed_at": "2024-01-17T10:10:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 回调通知接口 (Webhook)
|
||||
|
||||
### 2.1 统一回调入口
|
||||
接收第三方支付平台的异步通知。
|
||||
|
||||
```http
|
||||
POST /api/payment/notify/{gateway}
|
||||
```
|
||||
|
||||
**Path Params**:
|
||||
- `gateway`: `alipay` / `wechat` / `paypal` / `stripe` / `nowpayments`
|
||||
|
||||
**处理逻辑**:
|
||||
1. 根据 gateway 加载对应驱动
|
||||
2. 验签 (Verify Signature)
|
||||
3. 幂等性检查 (防重复处理)
|
||||
4. 更新订单状态
|
||||
5. 触发业务回调 (发货/开通权限等)
|
||||
6. 返回平台所需响应
|
||||
|
||||
**返回格式**:
|
||||
```
|
||||
# 支付宝
|
||||
success
|
||||
|
||||
# 微信
|
||||
<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>
|
||||
|
||||
# Stripe
|
||||
HTTP 200 OK
|
||||
|
||||
# PayPal
|
||||
HTTP 200 OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 同步返回 (Return)
|
||||
用户支付完成后的页面跳转。
|
||||
|
||||
```http
|
||||
GET /api/payment/return/{gateway}
|
||||
```
|
||||
|
||||
**Query Params**: 各平台不同,由平台自动附加
|
||||
|
||||
**处理逻辑**:
|
||||
1. 解析回传参数
|
||||
2. 验签
|
||||
3. 重定向到成功页面
|
||||
|
||||
---
|
||||
|
||||
## 3. 辅助接口
|
||||
|
||||
### 3.1 获取可用支付方式
|
||||
```http
|
||||
GET /api/payment/methods
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"methods": [
|
||||
{
|
||||
"gateway": "wechat_jsapi",
|
||||
"name": "微信支付",
|
||||
"icon": "/icons/wechat.png",
|
||||
"enabled": true,
|
||||
"available": true // 当前环境是否可用 (如微信内)
|
||||
},
|
||||
{
|
||||
"gateway": "alipay_wap",
|
||||
"name": "支付宝",
|
||||
"icon": "/icons/alipay.png",
|
||||
"enabled": true,
|
||||
"available": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 获取汇率
|
||||
```http
|
||||
GET /api/payment/exchange_rate?from=CNY&to=USD
|
||||
```
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"from": "CNY",
|
||||
"to": "USD",
|
||||
"rate": 0.139,
|
||||
"updated_at": "2024-01-17T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 管理接口 (Admin)
|
||||
|
||||
### 4.1 订单列表
|
||||
```http
|
||||
GET /api/admin/payment/orders?page=1&limit=20&status=paid
|
||||
Authorization: Bearer {admin_token}
|
||||
```
|
||||
|
||||
### 4.2 交易流水列表
|
||||
```http
|
||||
GET /api/admin/payment/trades?page=1&limit=20
|
||||
Authorization: Bearer {admin_token}
|
||||
```
|
||||
|
||||
### 4.3 发起退款
|
||||
```http
|
||||
POST /api/admin/payment/refund
|
||||
Authorization: Bearer {admin_token}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"trade_sn": "T20240117100001",
|
||||
"amount": 99.00,
|
||||
"reason": "用户申请退款"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 接口签名规范 (可选)
|
||||
|
||||
对于安全要求高的场景,可启用接口签名:
|
||||
|
||||
```javascript
|
||||
// 请求头
|
||||
X-Sign: sha256(timestamp + nonce + body + secret)
|
||||
X-Timestamp: 1705470600
|
||||
X-Nonce: abc123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📌 注意事项
|
||||
|
||||
1. **金额单位**: 所有金额均以**元**为单位,小数点后2位
|
||||
2. **时间格式**: ISO 8601 格式 `YYYY-MM-DDTHH:mm:ssZ`
|
||||
3. **字符编码**: UTF-8
|
||||
4. **HTTPS**: 生产环境必须使用 HTTPS
|
||||
5. **幂等性**: 相同订单号重复请求返回相同结果
|
||||
396
addons/Universal_Payment_Module copy/1_核心设计_通用协议/业务逻辑与模型.md
Normal file
396
addons/Universal_Payment_Module copy/1_核心设计_通用协议/业务逻辑与模型.md
Normal file
@@ -0,0 +1,396 @@
|
||||
# 业务逻辑与数据模型 (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. 退款幂等
|
||||
- 同一笔交易只能退款一次 (或限制总退款金额)
|
||||
383
addons/Universal_Payment_Module copy/1_核心设计_通用协议/安全与合规.md
Normal file
383
addons/Universal_Payment_Module copy/1_核心设计_通用协议/安全与合规.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# 支付安全与合规指南 (Security & Compliance) v4.0
|
||||
|
||||
> 支付系统安全最佳实践,保护你的资金和用户数据
|
||||
|
||||
## 🔐 密钥安全
|
||||
|
||||
### 1. 密钥存储原则
|
||||
|
||||
```
|
||||
❌ 错误做法:
|
||||
- 将密钥硬编码在代码中
|
||||
- 将密钥提交到 Git 仓库
|
||||
- 通过即时通讯工具传输密钥
|
||||
- 使用弱密码作为 API Key
|
||||
|
||||
✅ 正确做法:
|
||||
- 使用环境变量存储密钥
|
||||
- 使用专业密钥管理服务 (AWS KMS, HashiCorp Vault)
|
||||
- 定期轮换密钥
|
||||
- 最小权限原则
|
||||
```
|
||||
|
||||
### 2. .gitignore 必须包含
|
||||
|
||||
```gitignore
|
||||
# 支付密钥相关
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
*.pem
|
||||
*.key
|
||||
cert/
|
||||
config/payment.yml
|
||||
secrets/
|
||||
```
|
||||
|
||||
### 3. 密钥轮换
|
||||
|
||||
- 定期更换 API 密钥 (建议每 90 天)
|
||||
- 发现泄露立即作废并重新生成
|
||||
- 保留旧密钥短暂过渡期
|
||||
|
||||
---
|
||||
|
||||
## 🔒 通信安全
|
||||
|
||||
### 1. HTTPS 强制
|
||||
|
||||
```nginx
|
||||
# Nginx 配置示例
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com;
|
||||
|
||||
ssl_certificate /path/to/fullchain.pem;
|
||||
ssl_certificate_key /path/to/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
|
||||
|
||||
# HSTS
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 证书管理
|
||||
|
||||
- 使用受信任的 CA 签发证书
|
||||
- 定期检查证书有效期
|
||||
- 推荐 Let's Encrypt 自动续期
|
||||
|
||||
---
|
||||
|
||||
## ✅ 签名验证
|
||||
|
||||
### 1. 支付宝签名验证
|
||||
|
||||
```python
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from Crypto.Hash import SHA256
|
||||
import base64
|
||||
|
||||
def verify_alipay_sign(params: dict, sign: str, public_key: str) -> bool:
|
||||
"""验证支付宝签名"""
|
||||
# 1. 参数排序
|
||||
sorted_params = sorted([(k, v) for k, v in params.items() if k != 'sign' and v])
|
||||
|
||||
# 2. 拼接待签名字符串
|
||||
sign_str = '&'.join([f'{k}={v}' for k, v in sorted_params])
|
||||
|
||||
# 3. RSA2 验签
|
||||
key = RSA.import_key(f"-----BEGIN PUBLIC KEY-----\n{public_key}\n-----END PUBLIC KEY-----")
|
||||
verifier = PKCS1_v1_5.new(key)
|
||||
hash_obj = SHA256.new(sign_str.encode('utf-8'))
|
||||
|
||||
try:
|
||||
verifier.verify(hash_obj, base64.b64decode(sign))
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
```
|
||||
|
||||
### 2. 微信签名验证
|
||||
|
||||
```python
|
||||
import hashlib
|
||||
|
||||
def verify_wechat_sign(params: dict, sign: str, api_key: str) -> bool:
|
||||
"""验证微信支付签名"""
|
||||
# 1. 参数排序
|
||||
sorted_params = sorted([(k, v) for k, v in params.items() if k != 'sign' and v])
|
||||
|
||||
# 2. 拼接待签名字符串
|
||||
sign_str = '&'.join([f'{k}={v}' for k, v in sorted_params])
|
||||
sign_str += f'&key={api_key}'
|
||||
|
||||
# 3. MD5 签名
|
||||
calculated_sign = hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
return calculated_sign == sign
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💰 金额校验
|
||||
|
||||
### 1. 回调金额必须验证
|
||||
|
||||
```python
|
||||
def handle_payment_notify(trade_sn: str, paid_amount: int):
|
||||
"""处理支付回调时必须验证金额"""
|
||||
trade = get_trade_by_sn(trade_sn)
|
||||
|
||||
# 金额必须严格匹配
|
||||
if paid_amount != trade.cash_amount:
|
||||
log.error(f"金额不匹配! 订单:{trade.cash_amount}, 回调:{paid_amount}")
|
||||
raise AmountMismatchError()
|
||||
|
||||
# 继续处理...
|
||||
```
|
||||
|
||||
### 2. 防止金额篡改
|
||||
|
||||
```python
|
||||
# 前端传入的金额仅用于展示,实际金额从后端订单读取
|
||||
def checkout(order_sn: str, gateway: str):
|
||||
order = get_order(order_sn)
|
||||
|
||||
# 金额从数据库读取,不信任前端
|
||||
amount = order.pay_amount
|
||||
|
||||
return create_trade(order_sn, amount, gateway)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ 回调安全
|
||||
|
||||
### 1. IP 白名单
|
||||
|
||||
```python
|
||||
# 支付平台回调 IP 白名单
|
||||
PAYMENT_IP_WHITELIST = {
|
||||
'alipay': [
|
||||
'110.75.0.0/16',
|
||||
'203.209.0.0/16'
|
||||
],
|
||||
'wechat': [
|
||||
'101.226.0.0/16',
|
||||
'140.207.0.0/16'
|
||||
]
|
||||
}
|
||||
|
||||
def verify_callback_ip(gateway: str, client_ip: str) -> bool:
|
||||
"""验证回调来源 IP"""
|
||||
import ipaddress
|
||||
|
||||
whitelist = PAYMENT_IP_WHITELIST.get(gateway, [])
|
||||
client = ipaddress.ip_address(client_ip)
|
||||
|
||||
for cidr in whitelist:
|
||||
if client in ipaddress.ip_network(cidr):
|
||||
return True
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
### 2. 防重放攻击
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def check_notify_timestamp(timestamp: int) -> bool:
|
||||
"""检查回调时间戳,防止重放攻击"""
|
||||
now = int(time.time())
|
||||
|
||||
# 允许 5 分钟的时间差
|
||||
if abs(now - timestamp) > 300:
|
||||
log.warning(f"回调时间戳异常: {timestamp}")
|
||||
return False
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
### 3. 幂等性处理
|
||||
|
||||
```python
|
||||
def process_notify_idempotent(trade_sn: str, notify_data: dict):
|
||||
"""幂等性处理回调"""
|
||||
|
||||
# 使用分布式锁
|
||||
lock_key = f"payment_notify:{trade_sn}"
|
||||
|
||||
with redis_lock(lock_key, timeout=10):
|
||||
trade = get_trade_by_sn(trade_sn)
|
||||
|
||||
# 已处理过,直接返回成功
|
||||
if trade.status == 'paid':
|
||||
return success_response()
|
||||
|
||||
# 处理支付成功逻辑
|
||||
update_trade_to_paid(trade, notify_data)
|
||||
|
||||
return success_response()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 日志审计
|
||||
|
||||
### 1. 必须记录的日志
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
payment_logger = logging.getLogger('payment')
|
||||
|
||||
# 创建交易日志
|
||||
payment_logger.info(f"创建交易 | trade_sn={trade_sn} | order_sn={order_sn} | amount={amount} | gateway={gateway}")
|
||||
|
||||
# 回调日志
|
||||
payment_logger.info(f"收到回调 | gateway={gateway} | trade_sn={trade_sn} | raw_data={raw_data[:500]}")
|
||||
|
||||
# 签名验证日志
|
||||
payment_logger.info(f"签名验证 | trade_sn={trade_sn} | result={verify_result}")
|
||||
|
||||
# 状态变更日志
|
||||
payment_logger.info(f"状态变更 | trade_sn={trade_sn} | from={old_status} | to={new_status}")
|
||||
|
||||
# 退款日志
|
||||
payment_logger.info(f"发起退款 | refund_sn={refund_sn} | trade_sn={trade_sn} | amount={amount}")
|
||||
```
|
||||
|
||||
### 2. 敏感信息脱敏
|
||||
|
||||
```python
|
||||
def mask_sensitive(data: dict) -> dict:
|
||||
"""敏感信息脱敏"""
|
||||
sensitive_keys = ['card_no', 'id_card', 'phone', 'bank_account']
|
||||
|
||||
masked = data.copy()
|
||||
for key in sensitive_keys:
|
||||
if key in masked:
|
||||
value = str(masked[key])
|
||||
if len(value) > 4:
|
||||
masked[key] = value[:2] + '*' * (len(value) - 4) + value[-2:]
|
||||
|
||||
return masked
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 异常处理
|
||||
|
||||
### 1. 支付异常分类
|
||||
|
||||
```python
|
||||
class PaymentError(Exception):
|
||||
"""支付基础异常"""
|
||||
pass
|
||||
|
||||
class SignatureError(PaymentError):
|
||||
"""签名验证失败"""
|
||||
pass
|
||||
|
||||
class AmountMismatchError(PaymentError):
|
||||
"""金额不匹配"""
|
||||
pass
|
||||
|
||||
class OrderExpiredError(PaymentError):
|
||||
"""订单已过期"""
|
||||
pass
|
||||
|
||||
class DuplicatePaymentError(PaymentError):
|
||||
"""重复支付"""
|
||||
pass
|
||||
|
||||
class RefundError(PaymentError):
|
||||
"""退款失败"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 2. 统一异常处理
|
||||
|
||||
```python
|
||||
@app.exception_handler(PaymentError)
|
||||
async def payment_exception_handler(request, exc):
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={
|
||||
"code": 400,
|
||||
"message": str(exc),
|
||||
"data": None
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 合规要求
|
||||
|
||||
### 1. PCI DSS 合规 (信用卡)
|
||||
|
||||
- 不存储完整卡号、CVV、PIN
|
||||
- 使用 Stripe/PayPal 等符合 PCI DSS 的支付网关
|
||||
- 定期安全评估
|
||||
|
||||
### 2. GDPR 合规 (欧盟用户)
|
||||
|
||||
- 明确告知用户数据用途
|
||||
- 提供数据删除功能
|
||||
- 用户同意授权
|
||||
|
||||
### 3. 中国支付合规
|
||||
|
||||
- 接入持牌支付机构
|
||||
- 实名认证
|
||||
- 交易限额管理
|
||||
|
||||
---
|
||||
|
||||
## 📋 安全检查清单
|
||||
|
||||
```markdown
|
||||
## 上线前安全检查
|
||||
|
||||
### 密钥管理
|
||||
- [ ] 所有密钥通过环境变量配置
|
||||
- [ ] 密钥未提交到代码仓库
|
||||
- [ ] 生产环境密钥与测试环境隔离
|
||||
|
||||
### 通信安全
|
||||
- [ ] 启用 HTTPS
|
||||
- [ ] 证书有效且受信任
|
||||
- [ ] 启用 HSTS
|
||||
|
||||
### 签名验证
|
||||
- [ ] 所有回调验签
|
||||
- [ ] 验签失败拒绝处理
|
||||
|
||||
### 金额校验
|
||||
- [ ] 回调金额与订单金额比对
|
||||
- [ ] 金额从后端读取
|
||||
|
||||
### 日志审计
|
||||
- [ ] 关键操作有日志
|
||||
- [ ] 敏感信息脱敏
|
||||
|
||||
### 异常处理
|
||||
- [ ] 异常不泄露敏感信息
|
||||
- [ ] 有统一异常处理
|
||||
|
||||
### 回调安全
|
||||
- [ ] IP 白名单验证 (可选)
|
||||
- [ ] 幂等性处理
|
||||
- [ ] 防重放攻击
|
||||
```
|
||||
133
addons/Universal_Payment_Module copy/1_核心设计_通用协议/标准配置模板.yaml
Normal file
133
addons/Universal_Payment_Module copy/1_核心设计_通用协议/标准配置模板.yaml
Normal file
@@ -0,0 +1,133 @@
|
||||
# ============================================================================
|
||||
# 全球支付模块标准配置模板 (Universal Payment Config Template) v4.0
|
||||
# ============================================================================
|
||||
# 适用于: Python, Node.js, Go, Java, PHP 等任意后端语言
|
||||
# 使用方法: 将此配置映射到你项目的环境变量 (.env, config.py, application.yml)
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 1. 基础环境 (Environment)
|
||||
# ----------------------------------------------------------------------------
|
||||
APP_ENV: "production" # development / staging / production
|
||||
APP_NAME: "MyApp" # 应用名称 (用于日志/标题)
|
||||
APP_URL: "https://your-site.com" # 你的网站域名 (用于回调地址生成)
|
||||
APP_CURRENCY: "CNY" # 默认货币: CNY, USD, EUR
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 2. 数据库 (Database) - 存储订单和交易流水
|
||||
# ----------------------------------------------------------------------------
|
||||
DB_CONNECTION: "mysql" # mysql / postgres / mongodb / sqlite
|
||||
DB_HOST: "127.0.0.1"
|
||||
DB_PORT: "3306"
|
||||
DB_DATABASE: "payment_db"
|
||||
DB_USERNAME: "root"
|
||||
DB_PASSWORD: "your_password"
|
||||
|
||||
# 自动创建的表:
|
||||
# - orders (订单表)
|
||||
# - pay_trades (交易流水表)
|
||||
# - cashflows (资金流水表)
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 3. 支付宝 (Alipay) - 中国市场
|
||||
# ----------------------------------------------------------------------------
|
||||
ALIPAY_ENABLED: true
|
||||
ALIPAY_MODE: "production" # sandbox / production
|
||||
ALIPAY_APP_ID: "" # 开放平台应用 AppID
|
||||
ALIPAY_PID: "" # 商户 PID (合作伙伴ID)
|
||||
ALIPAY_SELLER_EMAIL: "" # 收款支付宝账号
|
||||
ALIPAY_PRIVATE_KEY: "" # 商户私钥 (RSA2)
|
||||
ALIPAY_PUBLIC_KEY: "" # 支付宝公钥
|
||||
ALIPAY_MD5_KEY: "" # MD5 密钥 (旧版接口)
|
||||
|
||||
# 回调地址 (系统自动拼接 APP_URL)
|
||||
# 同步回调: ${APP_URL}/api/payment/return/alipay
|
||||
# 异步回调: ${APP_URL}/api/payment/notify/alipay
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 4. 微信支付 (Wechat Pay) - 中国市场
|
||||
# ----------------------------------------------------------------------------
|
||||
WECHAT_ENABLED: true
|
||||
WECHAT_MODE: "production" # sandbox / production
|
||||
|
||||
# 公众号/网站支付
|
||||
WECHAT_APPID: "" # 公众号/网站 AppID
|
||||
WECHAT_APP_SECRET: "" # AppSecret
|
||||
|
||||
# 服务号 (如果有独立服务号)
|
||||
WECHAT_SERVICE_APPID: "" # 服务号 AppID
|
||||
WECHAT_SERVICE_SECRET: "" # 服务号 AppSecret
|
||||
|
||||
# 商户信息
|
||||
WECHAT_MCH_ID: "" # 商户号
|
||||
WECHAT_MCH_KEY: "" # 商户平台 API 密钥 (32位)
|
||||
WECHAT_MCH_KEY_V3: "" # APIv3 密钥 (如使用v3接口)
|
||||
|
||||
# 证书路径 (相对于项目根目录)
|
||||
WECHAT_CERT_PATH: "./cert/wechat/apiclient_cert.pem"
|
||||
WECHAT_KEY_PATH: "./cert/wechat/apiclient_key.pem"
|
||||
WECHAT_CERT_SERIAL: "" # 证书序列号 (v3接口需要)
|
||||
|
||||
# 小程序 (如果有)
|
||||
WECHAT_MINI_APPID: "" # 小程序 AppID
|
||||
WECHAT_MINI_SECRET: "" # 小程序 AppSecret
|
||||
|
||||
# 回调地址
|
||||
# 异步回调: ${APP_URL}/api/payment/notify/wechat
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 5. PayPal - 全球市场
|
||||
# ----------------------------------------------------------------------------
|
||||
PAYPAL_ENABLED: true
|
||||
PAYPAL_MODE: "live" # sandbox / live
|
||||
PAYPAL_CLIENT_ID: "" # Client ID
|
||||
PAYPAL_CLIENT_SECRET: "" # Client Secret
|
||||
PAYPAL_WEBHOOK_ID: "" # Webhook ID (用于验证回调)
|
||||
|
||||
# 回调地址: ${APP_URL}/api/payment/notify/paypal
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 6. Stripe - 全球市场
|
||||
# ----------------------------------------------------------------------------
|
||||
STRIPE_ENABLED: true
|
||||
STRIPE_MODE: "live" # test / live
|
||||
STRIPE_PUBLIC_KEY: "" # pk_live_xxx 或 pk_test_xxx
|
||||
STRIPE_SECRET_KEY: "" # sk_live_xxx 或 sk_test_xxx
|
||||
STRIPE_WEBHOOK_SECRET: "" # whsec_xxx
|
||||
|
||||
# 回调地址: ${APP_URL}/api/payment/notify/stripe
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 7. USDT (加密货币) - Web3 / 抗审查支付
|
||||
# ----------------------------------------------------------------------------
|
||||
USDT_ENABLED: false
|
||||
USDT_GATEWAY_TYPE: "nowpayments" # nowpayments / native
|
||||
|
||||
# 选项 A: NOWPayments (第三方托管)
|
||||
NOWPAYMENTS_API_KEY: ""
|
||||
NOWPAYMENTS_IPN_SECRET: ""
|
||||
|
||||
# 选项 B: Native (原生 TRC20 监听)
|
||||
TRON_NODE_API: "https://api.trongrid.io"
|
||||
TRON_WALLET_ADDRESS: "" # 你的 USDT-TRC20 收款地址
|
||||
TRON_API_KEY: "" # TronGrid API Key
|
||||
TRON_CHECK_INTERVAL: 60 # 轮询间隔 (秒)
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 8. 高级配置 (Advanced)
|
||||
# ----------------------------------------------------------------------------
|
||||
# 虚拟币/积分系统
|
||||
COIN_ENABLED: false # 是否启用虚拟币抵扣
|
||||
COIN_RATE: 100 # 1元 = 100虚拟币
|
||||
|
||||
# 订单配置
|
||||
ORDER_EXPIRE_MINUTES: 30 # 订单过期时间 (分钟)
|
||||
TRADE_SN_PREFIX: "T" # 交易流水号前缀
|
||||
|
||||
# 日志配置
|
||||
PAYMENT_LOG_LEVEL: "info" # debug / info / warning / error
|
||||
PAYMENT_LOG_PATH: "./logs/payment.log"
|
||||
|
||||
# 安全配置
|
||||
PAYMENT_IP_WHITELIST: "" # 回调IP白名单 (逗号分隔)
|
||||
PAYMENT_SIGN_TYPE: "RSA2" # 签名类型: RSA2, MD5
|
||||
266
addons/Universal_Payment_Module copy/2_智能对接_AI指令/Cursor规则.md
Normal file
266
addons/Universal_Payment_Module copy/2_智能对接_AI指令/Cursor规则.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# Universal Payment Module - Cursor 规则
|
||||
|
||||
> 将此文件复制为项目根目录的 `.cursorrules` 或 `.cursor/rules/payment.md`
|
||||
|
||||
---
|
||||
|
||||
## 基础设定
|
||||
|
||||
你是一位精通全球支付架构的资深全栈工程师,专注于支付网关集成、安全合规和高可用设计。
|
||||
|
||||
当用户提及"支付模块"、"支付功能"、"接入支付"时,请参考 `Universal_Payment_Module` 目录中的设计文档。
|
||||
|
||||
---
|
||||
|
||||
## 核心原则
|
||||
|
||||
### 1. 配置驱动
|
||||
- 所有支付密钥通过环境变量配置,绝不硬编码
|
||||
- 使用 `.env` 文件管理配置,参考 `标准配置模板.yaml`
|
||||
- 支持多环境切换 (development/staging/production)
|
||||
|
||||
### 2. 工厂模式
|
||||
- 使用 `PaymentFactory` 统一管理支付网关
|
||||
- 每个网关实现统一的 `AbstractGateway` 接口
|
||||
- 支持动态添加新的支付渠道
|
||||
|
||||
### 3. 安全优先
|
||||
- 所有回调必须验证签名
|
||||
- 必须验证支付金额与订单金额匹配
|
||||
- 使用 HTTPS,敏感数据脱敏
|
||||
- 参考 `安全与合规.md`
|
||||
|
||||
### 4. 幂等性
|
||||
- 支付回调必须支持重复调用
|
||||
- 使用分布式锁防止并发问题
|
||||
- 检查交易状态后再处理
|
||||
|
||||
### 5. 状态机
|
||||
- 订单状态: created → paying → paid → refunded/closed
|
||||
- 状态变更必须记录日志
|
||||
- 不允许跳跃式状态变更
|
||||
|
||||
---
|
||||
|
||||
## API 规范
|
||||
|
||||
严格按照 `API接口定义.md` 实现以下接口:
|
||||
|
||||
```
|
||||
POST /api/payment/create_order - 创建订单
|
||||
POST /api/payment/checkout - 发起支付
|
||||
GET /api/payment/status/{sn} - 查询状态
|
||||
POST /api/payment/close/{sn} - 关闭订单
|
||||
POST /api/payment/notify/{gw} - 回调通知
|
||||
GET /api/payment/return/{gw} - 同步返回
|
||||
GET /api/payment/methods - 获取支付方式
|
||||
```
|
||||
|
||||
### 统一响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库模型
|
||||
|
||||
参考 `业务逻辑与模型.md`,必须创建以下表:
|
||||
|
||||
1. **orders** - 订单表 (业务订单)
|
||||
2. **pay_trades** - 交易流水表 (支付记录)
|
||||
3. **cashflows** - 资金流水表 (可选)
|
||||
4. **refunds** - 退款记录表
|
||||
|
||||
金额单位:
|
||||
- 数据库存储使用**分** (BIGINT)
|
||||
- API 输入输出使用**元** (float)
|
||||
|
||||
---
|
||||
|
||||
## 支付网关 Gateway
|
||||
|
||||
### 支付宝 Alipay
|
||||
- SDK: `alipay-sdk-python` / `alipay-sdk-java` / `alipay/aop-sdk`
|
||||
- 签名: RSA2
|
||||
- 回调: POST,form 表单格式
|
||||
|
||||
### 微信支付 Wechat
|
||||
- SDK: `wechatpay-python-v3` / `wechatpay-java`
|
||||
- 签名: HMAC-SHA256 / RSA (v3)
|
||||
- 回调: POST,XML 格式
|
||||
|
||||
### PayPal
|
||||
- SDK: `paypal-rest-sdk`
|
||||
- 认证: OAuth 2.0
|
||||
- 回调: Webhook,JSON 格式
|
||||
|
||||
### Stripe
|
||||
- SDK: `stripe`
|
||||
- 认证: API Key
|
||||
- 回调: Webhook,JSON 格式
|
||||
|
||||
---
|
||||
|
||||
## 代码生成模板
|
||||
|
||||
### Python FastAPI
|
||||
|
||||
```python
|
||||
# app/services/payment_factory.py
|
||||
from abc import ABC, abstractmethod
|
||||
from app.config import settings
|
||||
|
||||
class AbstractGateway(ABC):
|
||||
@abstractmethod
|
||||
async 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
|
||||
|
||||
class PaymentFactory:
|
||||
_gateways = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, name: str, gateway_class):
|
||||
cls._gateways[name] = gateway_class
|
||||
|
||||
@classmethod
|
||||
def create(cls, name: str) -> AbstractGateway:
|
||||
gateway_name = name.split('_')[0]
|
||||
if gateway_name not in cls._gateways:
|
||||
raise ValueError(f"不支持的支付网关: {name}")
|
||||
return cls._gateways[gateway_name]()
|
||||
```
|
||||
|
||||
### Node.js Express
|
||||
|
||||
```typescript
|
||||
// src/services/PaymentFactory.ts
|
||||
export abstract class AbstractGateway {
|
||||
abstract createTrade(data: CreateTradeDTO): Promise<TradeResult>;
|
||||
abstract verifySign(data: Record<string, any>): boolean;
|
||||
abstract parseNotify(data: Record<string, any>): NotifyResult;
|
||||
}
|
||||
|
||||
export class PaymentFactory {
|
||||
private static gateways: Map<string, new () => AbstractGateway> = new Map();
|
||||
|
||||
static register(name: string, gateway: new () => AbstractGateway) {
|
||||
this.gateways.set(name, gateway);
|
||||
}
|
||||
|
||||
static create(name: string): AbstractGateway {
|
||||
const gatewayName = name.split('_')[0];
|
||||
const GatewayClass = this.gateways.get(gatewayName);
|
||||
if (!GatewayClass) {
|
||||
throw new Error(`不支持的支付网关: ${name}`);
|
||||
}
|
||||
return new GatewayClass();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 回调处理模板
|
||||
|
||||
```python
|
||||
async def handle_notify(gateway: str, raw_data: bytes | dict):
|
||||
"""统一回调处理"""
|
||||
|
||||
# 1. 获取网关驱动
|
||||
driver = PaymentFactory.create(gateway)
|
||||
|
||||
# 2. 验证签名
|
||||
if not driver.verify_sign(raw_data):
|
||||
logger.error(f"签名验证失败 | gateway={gateway}")
|
||||
raise SignatureError()
|
||||
|
||||
# 3. 解析数据
|
||||
parsed = driver.parse_notify(raw_data)
|
||||
trade_sn = parsed['trade_sn']
|
||||
|
||||
# 4. 幂等检查
|
||||
async with redis_lock(f"notify:{trade_sn}"):
|
||||
trade = await get_trade(trade_sn)
|
||||
|
||||
if trade.status == 'paid':
|
||||
return driver.success_response()
|
||||
|
||||
# 5. 金额校验
|
||||
if parsed['amount'] != trade.cash_amount:
|
||||
logger.error(f"金额不匹配 | sn={trade_sn}")
|
||||
raise AmountMismatchError()
|
||||
|
||||
# 6. 更新状态
|
||||
await update_trade_status(trade_sn, 'paid', parsed)
|
||||
|
||||
# 7. 触发业务回调
|
||||
await dispatch_event('order.paid', trade.order_sn)
|
||||
|
||||
return driver.success_response()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 日志规范
|
||||
|
||||
关键操作必须记录日志:
|
||||
|
||||
```python
|
||||
logger.info(f"创建交易 | trade_sn={sn} | order={order_sn} | amount={amount}")
|
||||
logger.info(f"收到回调 | gateway={gw} | trade_sn={sn}")
|
||||
logger.info(f"签名验证 | trade_sn={sn} | result={ok}")
|
||||
logger.info(f"状态变更 | trade_sn={sn} | {old} → {new}")
|
||||
logger.error(f"支付失败 | trade_sn={sn} | error={err}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试规范
|
||||
|
||||
1. **单元测试**: 测试签名生成/验证、金额转换
|
||||
2. **集成测试**: 使用沙箱环境测试完整支付流程
|
||||
3. **Mock 测试**: 模拟回调接口测试
|
||||
|
||||
---
|
||||
|
||||
## 禁止事项
|
||||
|
||||
❌ 密钥硬编码在代码中
|
||||
❌ 跳过签名验证
|
||||
❌ 信任前端传入的金额
|
||||
❌ 回调不做幂等处理
|
||||
❌ 敏感信息打印到日志
|
||||
❌ 使用 HTTP 而非 HTTPS
|
||||
|
||||
---
|
||||
|
||||
## 参考文档路径
|
||||
|
||||
```
|
||||
Universal_Payment_Module/
|
||||
├── 1_核心设计_通用协议/
|
||||
│ ├── 标准配置模板.yaml # 配置参考
|
||||
│ ├── API接口定义.md # 接口规范
|
||||
│ ├── 业务逻辑与模型.md # 数据模型
|
||||
│ └── 安全与合规.md # 安全规范
|
||||
├── 2_智能对接_AI指令/
|
||||
│ ├── 通用集成指令.md # AI 提示词
|
||||
│ └── Cursor规则.md # 本文件
|
||||
└── 3_逻辑参考_通用实现/
|
||||
├── 前端收银台Demo.html
|
||||
└── 后端源码/
|
||||
```
|
||||
234
addons/Universal_Payment_Module copy/2_智能对接_AI指令/通用集成指令.md
Normal file
234
addons/Universal_Payment_Module copy/2_智能对接_AI指令/通用集成指令.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# 通用支付模块 AI 智能对接指令 (Integration Prompt) v4.0
|
||||
|
||||
> 发送此指令给 AI 助手 (Cursor/ChatGPT/Claude),自动生成支付集成代码
|
||||
|
||||
---
|
||||
|
||||
## 🎯 角色设定
|
||||
|
||||
```
|
||||
你是一位精通全球支付架构的资深全栈架构师,专注于:
|
||||
- 支付网关集成 (Alipay/Wechat/PayPal/Stripe/USDT)
|
||||
- 安全合规 (签名验证/HTTPS/PCI DSS)
|
||||
- 高可用设计 (幂等性/状态机/分布式锁)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 任务目标
|
||||
|
||||
我提供了一个**配置驱动 (Configuration-Driven)** 的通用支付模块设计。
|
||||
请根据我的项目环境,将此支付功能无缝集成。
|
||||
|
||||
---
|
||||
|
||||
## 📚 核心资源 (请先阅读)
|
||||
|
||||
1. **标准配置模板**: `1_核心设计_通用协议/标准配置模板.yaml`
|
||||
2. **API 接口契约**: `1_核心设计_通用协议/API接口定义.md`
|
||||
3. **数据模型**: `1_核心设计_通用协议/业务逻辑与模型.md`
|
||||
4. **安全规范**: `1_核心设计_通用协议/安全与合规.md`
|
||||
|
||||
---
|
||||
|
||||
## 🔧 集成模式
|
||||
|
||||
### 模式 A: 嵌入式集成 (Library Mode) ⭐推荐
|
||||
|
||||
适用于将支付功能直接写在现有的后端项目中。
|
||||
|
||||
**执行步骤**:
|
||||
1. **环境识别**: 检查项目语言 (Python/Node/Go/Java/PHP)
|
||||
2. **依赖安装**: 推荐 SDK
|
||||
3. **配置加载**: 读取环境变量
|
||||
4. **模型生成**: 创建 ORM 模型 (Order/Trade/Refund)
|
||||
5. **网关工厂**: 实现 PaymentFactory + 各网关 Driver
|
||||
6. **接口实现**: 按 `API接口定义.md` 实现 Controller
|
||||
7. **回调处理**: 实现回调验签和状态更新
|
||||
|
||||
---
|
||||
|
||||
### 模式 B: 微服务集成 (Microservice Mode)
|
||||
|
||||
适用于将支付功能独立部署为一个服务。
|
||||
|
||||
**执行步骤**:
|
||||
1. **服务生成**: 创建独立的支付服务项目
|
||||
2. **Docker化**: 编写 `Dockerfile` 和 `docker-compose.yml`
|
||||
3. **网关代理**: 配置 `/api/payment/*` 路由转发
|
||||
|
||||
---
|
||||
|
||||
## 📝 给 AI 的标准执行指令
|
||||
|
||||
### 快速集成 (复制此内容发送给 AI)
|
||||
|
||||
```
|
||||
请读取 `Universal_Payment_Module` 目录下的所有设计文档。
|
||||
|
||||
我的当前项目信息:
|
||||
- 语言/框架: [Python FastAPI / Node.js Express / Java Spring Boot / Go Gin / PHP Laravel]
|
||||
- 数据库: [MySQL / PostgreSQL / MongoDB]
|
||||
- 集成模式: [模式 A 嵌入式 / 模式 B 微服务]
|
||||
|
||||
请执行以下任务:
|
||||
1. 生成依赖安装命令
|
||||
2. 生成数据库迁移/模型代码
|
||||
3. 生成支付网关工厂类
|
||||
4. 生成 API 接口代码 (严格按 API接口定义.md)
|
||||
5. 生成回调处理代码
|
||||
6. 生成配置读取代码
|
||||
|
||||
要求:
|
||||
- 使用工厂模式管理支付网关
|
||||
- 所有配置通过环境变量读取
|
||||
- 包含完整的签名验证逻辑
|
||||
- 包含幂等性处理
|
||||
- 添加中文注释
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐍 Python FastAPI 示例指令
|
||||
|
||||
```
|
||||
我的项目使用 Python FastAPI + SQLAlchemy + MySQL。
|
||||
|
||||
请根据 Universal_Payment_Module 文档,生成:
|
||||
|
||||
1. requirements.txt 依赖
|
||||
2. app/models/payment.py - 数据模型
|
||||
3. app/services/payment_factory.py - 支付网关工厂
|
||||
4. app/services/gateways/alipay.py - 支付宝网关
|
||||
5. app/services/gateways/wechat.py - 微信支付网关
|
||||
6. app/routers/payment.py - API 路由
|
||||
7. app/config/payment.py - 配置加载
|
||||
|
||||
特别要求:
|
||||
- 使用 async/await 异步处理
|
||||
- 集成 alipay-sdk-python 和 wechatpay-python-v3
|
||||
- 回调接口支持 XML 和 JSON 格式
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Node.js Express 示例指令
|
||||
|
||||
```
|
||||
我的项目使用 Node.js Express + Prisma + PostgreSQL。
|
||||
|
||||
请根据 Universal_Payment_Module 文档,生成:
|
||||
|
||||
1. package.json 依赖
|
||||
2. prisma/schema.prisma - 数据模型
|
||||
3. src/services/PaymentFactory.ts - 支付网关工厂
|
||||
4. src/services/gateways/AlipayGateway.ts
|
||||
5. src/services/gateways/WechatGateway.ts
|
||||
6. src/routes/payment.ts - API 路由
|
||||
7. src/config/payment.ts - 配置加载
|
||||
|
||||
特别要求:
|
||||
- 使用 TypeScript
|
||||
- 使用 alipay-sdk 和 wechatpay-node-v3
|
||||
- 实现完整的错误处理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ☕ Java Spring Boot 示例指令
|
||||
|
||||
```
|
||||
我的项目使用 Java Spring Boot + MyBatis + MySQL。
|
||||
|
||||
请根据 Universal_Payment_Module 文档,生成:
|
||||
|
||||
1. pom.xml 依赖
|
||||
2. entity/ - 实体类
|
||||
3. mapper/ - MyBatis Mapper
|
||||
4. service/PaymentFactory.java - 支付网关工厂
|
||||
5. service/gateway/AlipayGateway.java
|
||||
6. service/gateway/WechatGateway.java
|
||||
7. controller/PaymentController.java - API 控制器
|
||||
8. config/PaymentConfig.java - 配置类
|
||||
|
||||
特别要求:
|
||||
- 使用 alipay-sdk-java 和 wechatpay-java
|
||||
- 使用 @Transactional 事务管理
|
||||
- 实现统一异常处理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐘 PHP Laravel 示例指令
|
||||
|
||||
```
|
||||
我的项目使用 PHP Laravel + Eloquent + MySQL。
|
||||
|
||||
请根据 Universal_Payment_Module 文档,生成:
|
||||
|
||||
1. composer.json 依赖
|
||||
2. database/migrations/ - 数据库迁移
|
||||
3. app/Models/ - Eloquent 模型
|
||||
4. app/Services/PaymentFactory.php - 支付网关工厂
|
||||
5. app/Services/Gateways/AlipayGateway.php
|
||||
6. app/Services/Gateways/WechatGateway.php
|
||||
7. app/Http/Controllers/PaymentController.php
|
||||
8. routes/api.php - 路由定义
|
||||
9. config/payment.php - 配置文件
|
||||
|
||||
特别要求:
|
||||
- 使用 alipay/aop-sdk 和 wechatpay/wechatpay
|
||||
- 使用 Laravel 的服务容器
|
||||
- 实现中间件验签
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔥 高级指令:前端收银台
|
||||
|
||||
```
|
||||
请根据 Universal_Payment_Module 文档,生成前端收银台组件。
|
||||
|
||||
技术栈: [Vue 3 / React / 原生 JS]
|
||||
|
||||
要求:
|
||||
1. 支持多种支付方式切换
|
||||
2. 扫码支付显示二维码
|
||||
3. 轮询支付状态
|
||||
4. 适配移动端
|
||||
5. 显示支付倒计时
|
||||
6. 美观的 UI (可使用 TailwindCSS)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔥 高级指令:Docker 部署
|
||||
|
||||
```
|
||||
请为 Universal_Payment_Module 生成 Docker 部署配置。
|
||||
|
||||
要求:
|
||||
1. Dockerfile (多阶段构建)
|
||||
2. docker-compose.yml (包含 MySQL + Redis)
|
||||
3. nginx.conf (反向代理 + HTTPS)
|
||||
4. .env.example (环境变量模板)
|
||||
5. deploy.sh (一键部署脚本)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
1. **密钥安全**: 生成的代码中不要硬编码任何密钥
|
||||
2. **签名验证**: 必须实现完整的签名验证逻辑
|
||||
3. **幂等处理**: 回调必须支持幂等
|
||||
4. **金额校验**: 必须验证回调金额与订单金额匹配
|
||||
5. **日志记录**: 关键操作必须记录日志
|
||||
|
||||
---
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如有问题,请联系:
|
||||
- 微信: 28533368
|
||||
- 作者: 卡若
|
||||
748
addons/Universal_Payment_Module copy/3_逻辑参考_通用实现/前端收银台Demo.html
Normal file
748
addons/Universal_Payment_Module copy/3_逻辑参考_通用实现/前端收银台Demo.html
Normal file
@@ -0,0 +1,748 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>通用收银台 v4.0</title>
|
||||
<style>
|
||||
/*
|
||||
* 通用收银台样式 - 支持多种支付方式
|
||||
* 作者: 卡若
|
||||
* 适配: PC + 移动端
|
||||
*/
|
||||
|
||||
:root {
|
||||
--primary-color: #1890ff;
|
||||
--success-color: #52c41a;
|
||||
--warning-color: #faad14;
|
||||
--error-color: #ff4d4f;
|
||||
--text-color: #333;
|
||||
--text-secondary: #666;
|
||||
--border-color: #e8e8e8;
|
||||
--bg-color: #f5f5f7;
|
||||
--card-bg: #ffffff;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cashier-container {
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.cashier-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 订单信息 */
|
||||
.order-section {
|
||||
text-align: center;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px dashed var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.order-title {
|
||||
font-size: 16px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.order-amount .currency {
|
||||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.order-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.countdown {
|
||||
color: var(--warning-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 支付方式选择 */
|
||||
.payment-section h3 {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.payment-methods {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.payment-method {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
.payment-method:hover {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(24, 144, 255, 0.04);
|
||||
}
|
||||
|
||||
.payment-method.active {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(24, 144, 255, 0.08);
|
||||
}
|
||||
|
||||
.payment-method .icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.payment-method .icon.alipay { background: #1677ff; color: white; }
|
||||
.payment-method .icon.wechat { background: #07c160; color: white; }
|
||||
.payment-method .icon.paypal { background: #003087; color: white; }
|
||||
.payment-method .icon.stripe { background: #635bff; color: white; }
|
||||
.payment-method .icon.usdt { background: #26a17b; color: white; }
|
||||
|
||||
.payment-method .info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.payment-method .name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.payment-method .desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.payment-method .radio {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.payment-method.active .radio {
|
||||
border-color: var(--primary-color);
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.payment-method.active .radio::after {
|
||||
content: '✓';
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 二维码区域 */
|
||||
.qrcode-section {
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.qrcode-section.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.qrcode-box {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.qrcode-box img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.qrcode-text {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.qrcode-tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
/* USDT 地址显示 */
|
||||
.usdt-address {
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
margin-top: 8px;
|
||||
padding: 8px 16px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 支付按钮 */
|
||||
.pay-btn {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.pay-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(24, 144, 255, 0.3);
|
||||
}
|
||||
|
||||
.pay-btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pay-btn.loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.pay-btn.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -10px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 支付成功 */
|
||||
.success-section {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.success-section.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: var(--success-color);
|
||||
color: white;
|
||||
font-size: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 20px;
|
||||
animation: scaleIn 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
0% { transform: scale(0); }
|
||||
50% { transform: scale(1.2); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.success-section h2 {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.success-section p {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.return-btn {
|
||||
margin-top: 24px;
|
||||
padding: 12px 32px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 安全提示 */
|
||||
.security-tips {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.security-tips span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.cashier-container {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.cashier-card {
|
||||
border-radius: 0;
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.order-amount {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="cashier-container">
|
||||
<!-- 订单信息卡片 -->
|
||||
<div class="cashier-card" id="orderCard">
|
||||
<div class="order-section">
|
||||
<div class="order-title" id="orderTitle">VIP会员月卡</div>
|
||||
<div class="order-amount">
|
||||
<span class="currency">¥</span>
|
||||
<span id="displayAmount">99.00</span>
|
||||
</div>
|
||||
<div class="order-info">
|
||||
<span>订单号: <span id="orderSn">202401170001</span></span>
|
||||
<span class="countdown" id="countdown">29:59</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 支付方式选择 -->
|
||||
<div class="payment-section" id="paymentSection">
|
||||
<h3>选择支付方式</h3>
|
||||
<div class="payment-methods" id="paymentMethods">
|
||||
<!-- 支付宝 -->
|
||||
<div class="payment-method" data-gateway="alipay_wap" onclick="selectMethod(this)">
|
||||
<div class="icon alipay">💙</div>
|
||||
<div class="info">
|
||||
<div class="name">支付宝</div>
|
||||
<div class="desc">推荐有支付宝账户的用户使用</div>
|
||||
</div>
|
||||
<div class="radio"></div>
|
||||
</div>
|
||||
|
||||
<!-- 微信支付 -->
|
||||
<div class="payment-method" data-gateway="wechat_native" onclick="selectMethod(this)">
|
||||
<div class="icon wechat">💚</div>
|
||||
<div class="info">
|
||||
<div class="name">微信支付</div>
|
||||
<div class="desc">扫码支付,微信用户首选</div>
|
||||
</div>
|
||||
<div class="radio"></div>
|
||||
</div>
|
||||
|
||||
<!-- PayPal -->
|
||||
<div class="payment-method" data-gateway="paypal" onclick="selectMethod(this)">
|
||||
<div class="icon paypal">🅿️</div>
|
||||
<div class="info">
|
||||
<div class="name">PayPal</div>
|
||||
<div class="desc">支持信用卡,海外用户推荐</div>
|
||||
</div>
|
||||
<div class="radio"></div>
|
||||
</div>
|
||||
|
||||
<!-- USDT -->
|
||||
<div class="payment-method" data-gateway="usdt" onclick="selectMethod(this)">
|
||||
<div class="icon usdt">₮</div>
|
||||
<div class="info">
|
||||
<div class="name">USDT (TRC20)</div>
|
||||
<div class="desc">加密货币支付</div>
|
||||
</div>
|
||||
<div class="radio"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 二维码区域 -->
|
||||
<div class="qrcode-section" id="qrcodeSection">
|
||||
<div class="qrcode-box">
|
||||
<img id="qrcodeImg" src="" alt="支付二维码">
|
||||
</div>
|
||||
<div class="qrcode-text" id="qrcodeText">请使用微信扫描二维码支付</div>
|
||||
<div class="qrcode-tip">二维码有效期 30 分钟,请尽快支付</div>
|
||||
|
||||
<!-- USDT 地址 -->
|
||||
<div class="usdt-address" id="usdtAddress" style="display: none;"></div>
|
||||
<button class="copy-btn" id="copyBtn" style="display: none;" onclick="copyAddress()">复制地址</button>
|
||||
</div>
|
||||
|
||||
<!-- 支付按钮 -->
|
||||
<button class="pay-btn" id="payBtn" onclick="doPay()">确认支付 ¥99.00</button>
|
||||
</div>
|
||||
|
||||
<!-- 支付成功 -->
|
||||
<div class="cashier-card success-section" id="successSection">
|
||||
<div class="success-icon">✓</div>
|
||||
<h2>支付成功</h2>
|
||||
<p>感谢您的购买,订单已完成</p>
|
||||
<button class="return-btn" onclick="goBack()">返回商户</button>
|
||||
</div>
|
||||
|
||||
<!-- 安全提示 -->
|
||||
<div class="security-tips">
|
||||
<span>🔒 安全支付由卡若私域提供技术支持</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 通用收银台 JavaScript v4.0
|
||||
* 作者: 卡若
|
||||
*
|
||||
* 使用方法:
|
||||
* 1. 修改 API_BASE 为你的后端地址
|
||||
* 2. 通过 URL 参数传入订单信息: ?order_sn=xxx&amount=99.00&title=商品名称
|
||||
*/
|
||||
|
||||
// 配置
|
||||
const API_BASE = '/api/payment'; // 你的后端接口地址
|
||||
const POLL_INTERVAL = 3000; // 轮询间隔 (毫秒)
|
||||
const ORDER_TIMEOUT = 30 * 60; // 订单超时时间 (秒)
|
||||
|
||||
// 状态
|
||||
let selectedGateway = null;
|
||||
let orderSn = '';
|
||||
let orderAmount = 0;
|
||||
let countdownTimer = null;
|
||||
let pollTimer = null;
|
||||
let remainingSeconds = ORDER_TIMEOUT;
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 从 URL 解析订单信息
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
orderSn = params.get('order_sn') || '202401170001';
|
||||
orderAmount = parseFloat(params.get('amount')) || 99.00;
|
||||
const title = params.get('title') || 'VIP会员月卡';
|
||||
|
||||
// 更新UI
|
||||
document.getElementById('orderSn').textContent = orderSn;
|
||||
document.getElementById('orderTitle').textContent = title;
|
||||
document.getElementById('displayAmount').textContent = orderAmount.toFixed(2);
|
||||
document.getElementById('payBtn').textContent = `确认支付 ¥${orderAmount.toFixed(2)}`;
|
||||
|
||||
// 启动倒计时
|
||||
startCountdown();
|
||||
|
||||
// 检测微信环境
|
||||
if (isWechat()) {
|
||||
// 微信环境默认选择JSAPI
|
||||
const wechatMethod = document.querySelector('[data-gateway="wechat_native"]');
|
||||
if (wechatMethod) {
|
||||
wechatMethod.dataset.gateway = 'wechat_jsapi';
|
||||
wechatMethod.querySelector('.desc').textContent = '微信内直接支付';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 选择支付方式
|
||||
function selectMethod(element) {
|
||||
// 移除其他选中状态
|
||||
document.querySelectorAll('.payment-method').forEach(el => {
|
||||
el.classList.remove('active');
|
||||
});
|
||||
|
||||
// 设置选中状态
|
||||
element.classList.add('active');
|
||||
selectedGateway = element.dataset.gateway;
|
||||
|
||||
// 隐藏二维码区域
|
||||
document.getElementById('qrcodeSection').classList.remove('show');
|
||||
document.getElementById('paymentSection').style.display = 'block';
|
||||
document.getElementById('payBtn').style.display = 'block';
|
||||
|
||||
// 更新金额显示 (USDT显示美元)
|
||||
if (selectedGateway === 'usdt') {
|
||||
const usdtAmount = (orderAmount / 7.2).toFixed(2); // 简单汇率转换
|
||||
document.getElementById('displayAmount').textContent = usdtAmount;
|
||||
document.querySelector('.currency').textContent = '₮';
|
||||
document.getElementById('payBtn').textContent = `确认支付 ₮${usdtAmount}`;
|
||||
} else if (selectedGateway === 'paypal') {
|
||||
const usdAmount = (orderAmount / 7.2).toFixed(2);
|
||||
document.getElementById('displayAmount').textContent = usdAmount;
|
||||
document.querySelector('.currency').textContent = '$';
|
||||
document.getElementById('payBtn').textContent = `确认支付 $${usdAmount}`;
|
||||
} else {
|
||||
document.getElementById('displayAmount').textContent = orderAmount.toFixed(2);
|
||||
document.querySelector('.currency').textContent = '¥';
|
||||
document.getElementById('payBtn').textContent = `确认支付 ¥${orderAmount.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 发起支付
|
||||
async function doPay() {
|
||||
if (!selectedGateway) {
|
||||
alert('请选择支付方式');
|
||||
return;
|
||||
}
|
||||
|
||||
const payBtn = document.getElementById('payBtn');
|
||||
payBtn.classList.add('loading');
|
||||
payBtn.disabled = true;
|
||||
|
||||
try {
|
||||
// 调用后端接口
|
||||
const response = await fetch(`${API_BASE}/checkout`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
order_sn: orderSn,
|
||||
gateway: selectedGateway,
|
||||
return_url: window.location.href
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code !== 200) {
|
||||
throw new Error(result.message || '支付发起失败');
|
||||
}
|
||||
|
||||
const { type, payload, trade_sn } = result.data;
|
||||
|
||||
switch (type) {
|
||||
case 'url':
|
||||
// 跳转支付 (支付宝H5, PayPal, Stripe)
|
||||
window.location.href = payload;
|
||||
break;
|
||||
|
||||
case 'qrcode':
|
||||
// 显示二维码 (微信Native, 支付宝扫码)
|
||||
showQrcode(payload, selectedGateway);
|
||||
startPolling(trade_sn);
|
||||
break;
|
||||
|
||||
case 'json':
|
||||
// 调起SDK (微信JSAPI)
|
||||
callWechatPay(payload);
|
||||
break;
|
||||
|
||||
case 'address':
|
||||
// 显示钱包地址 (USDT)
|
||||
showUsdtAddress(payload);
|
||||
startPolling(trade_sn);
|
||||
break;
|
||||
|
||||
case 'direct':
|
||||
// 直接完成 (虚拟币支付)
|
||||
showSuccess();
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('支付失败:', error);
|
||||
alert(error.message || '支付发起失败,请重试');
|
||||
} finally {
|
||||
payBtn.classList.remove('loading');
|
||||
payBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示二维码
|
||||
function showQrcode(content, gateway) {
|
||||
document.getElementById('paymentSection').style.display = 'none';
|
||||
document.getElementById('payBtn').style.display = 'none';
|
||||
document.getElementById('qrcodeSection').classList.add('show');
|
||||
|
||||
// 使用在线服务生成二维码
|
||||
const qrcodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(content)}`;
|
||||
document.getElementById('qrcodeImg').src = qrcodeUrl;
|
||||
|
||||
// 更新提示文字
|
||||
const texts = {
|
||||
'wechat_native': '请使用微信扫描二维码支付',
|
||||
'alipay_qr': '请使用支付宝扫描二维码支付'
|
||||
};
|
||||
document.getElementById('qrcodeText').textContent = texts[gateway] || '请扫描二维码支付';
|
||||
}
|
||||
|
||||
// 显示USDT地址
|
||||
function showUsdtAddress(addressInfo) {
|
||||
document.getElementById('paymentSection').style.display = 'none';
|
||||
document.getElementById('payBtn').style.display = 'none';
|
||||
document.getElementById('qrcodeSection').classList.add('show');
|
||||
|
||||
const { address, amount_usdt, memo } = addressInfo;
|
||||
|
||||
document.getElementById('qrcodeImg').src = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(address)}`;
|
||||
document.getElementById('qrcodeText').textContent = `请向以下地址转账 ${amount_usdt} USDT (TRC20)`;
|
||||
document.getElementById('usdtAddress').textContent = address;
|
||||
document.getElementById('usdtAddress').style.display = 'block';
|
||||
document.getElementById('copyBtn').style.display = 'inline-block';
|
||||
}
|
||||
|
||||
// 复制地址
|
||||
function copyAddress() {
|
||||
const address = document.getElementById('usdtAddress').textContent;
|
||||
navigator.clipboard.writeText(address).then(() => {
|
||||
alert('地址已复制');
|
||||
});
|
||||
}
|
||||
|
||||
// 调用微信JSAPI支付
|
||||
function callWechatPay(params) {
|
||||
if (typeof WeixinJSBridge === 'undefined') {
|
||||
alert('请在微信中打开此页面');
|
||||
return;
|
||||
}
|
||||
|
||||
WeixinJSBridge.invoke('getBrandWCPayRequest', params, function(res) {
|
||||
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||
showSuccess();
|
||||
} else {
|
||||
alert('支付取消或失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 轮询支付状态
|
||||
function startPolling(tradeSn) {
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
|
||||
pollTimer = setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/status/${orderSn}`);
|
||||
const result = await response.json();
|
||||
|
||||
if (result.data && result.data.status === 'paid') {
|
||||
clearInterval(pollTimer);
|
||||
showSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询状态失败:', error);
|
||||
}
|
||||
}, POLL_INTERVAL);
|
||||
}
|
||||
|
||||
// 显示支付成功
|
||||
function showSuccess() {
|
||||
document.getElementById('orderCard').style.display = 'none';
|
||||
document.getElementById('successSection').classList.add('show');
|
||||
|
||||
// 停止轮询和倒计时
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
if (countdownTimer) clearInterval(countdownTimer);
|
||||
}
|
||||
|
||||
// 返回商户
|
||||
function goBack() {
|
||||
const returnUrl = new URLSearchParams(window.location.search).get('return_url');
|
||||
if (returnUrl) {
|
||||
window.location.href = returnUrl;
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
|
||||
// 倒计时
|
||||
function startCountdown() {
|
||||
countdownTimer = setInterval(() => {
|
||||
remainingSeconds--;
|
||||
|
||||
if (remainingSeconds <= 0) {
|
||||
clearInterval(countdownTimer);
|
||||
alert('订单已超时,请重新下单');
|
||||
window.history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(remainingSeconds / 60);
|
||||
const seconds = remainingSeconds % 60;
|
||||
document.getElementById('countdown').textContent =
|
||||
`${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// 检测微信环境
|
||||
function isWechat() {
|
||||
return /MicroMessenger/i.test(navigator.userAgent);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,294 @@
|
||||
"""
|
||||
支付宝网关实现 (Alipay Gateway)
|
||||
基于 www.lytiao.com 项目提取
|
||||
|
||||
作者: 卡若
|
||||
版本: v4.0
|
||||
"""
|
||||
|
||||
import json
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from urllib.parse import urlencode, parse_qs
|
||||
|
||||
try:
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from Crypto.Hash import SHA256
|
||||
except ImportError:
|
||||
print("请安装 pycryptodome: pip install pycryptodome")
|
||||
|
||||
from payment_factory import (
|
||||
AbstractGateway, CreateTradeData, TradeResult, NotifyResult,
|
||||
SignatureError, logger
|
||||
)
|
||||
|
||||
|
||||
class AlipayGateway(AbstractGateway):
|
||||
"""
|
||||
支付宝网关
|
||||
|
||||
支持:
|
||||
- 电脑网站支付 (platform_type='web')
|
||||
- 手机网站支付 (platform_type='wap')
|
||||
- 扫码支付 (platform_type='qr')
|
||||
"""
|
||||
|
||||
GATEWAY_URL = 'https://openapi.alipay.com/gateway.do'
|
||||
SANDBOX_URL = 'https://openapi.alipaydev.com/gateway.do'
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
super().__init__(config)
|
||||
self.app_id = config.get('app_id', '')
|
||||
self.pid = config.get('pid', '')
|
||||
self.seller_email = config.get('seller_email', '')
|
||||
self.private_key = config.get('private_key', '')
|
||||
self.public_key = config.get('public_key', '')
|
||||
self.md5_key = config.get('md5_key', '')
|
||||
|
||||
def create_trade(self, data: CreateTradeData) -> TradeResult:
|
||||
"""创建支付宝交易"""
|
||||
platform_type = data.platform_type.capitalize()
|
||||
|
||||
if platform_type == 'Web':
|
||||
return self._create_web_trade(data)
|
||||
elif platform_type == 'Wap':
|
||||
return self._create_wap_trade(data)
|
||||
elif platform_type == 'Qr':
|
||||
return self._create_qr_trade(data)
|
||||
else:
|
||||
raise ValueError(f"不支持的支付类型: {platform_type}")
|
||||
|
||||
def _create_web_trade(self, data: CreateTradeData) -> TradeResult:
|
||||
"""电脑网站支付"""
|
||||
biz_content = {
|
||||
'subject': data.goods_title[:256],
|
||||
'out_trade_no': data.trade_sn,
|
||||
'total_amount': str(data.amount / 100), # 分转元
|
||||
'product_code': 'FAST_INSTANT_TRADE_PAY',
|
||||
'body': data.goods_detail[:128] if data.goods_detail else '',
|
||||
'passback_params': json.dumps(data.attach) if data.attach else '',
|
||||
}
|
||||
|
||||
params = self._build_params('alipay.trade.page.pay', biz_content, data.return_url, data.notify_url)
|
||||
|
||||
# 生成签名
|
||||
sign = self._generate_sign(params)
|
||||
params['sign'] = sign
|
||||
|
||||
# 构建跳转URL
|
||||
pay_url = f"{self.GATEWAY_URL}?{urlencode(params)}"
|
||||
|
||||
return TradeResult(
|
||||
type='url',
|
||||
payload=pay_url,
|
||||
trade_sn=data.trade_sn
|
||||
)
|
||||
|
||||
def _create_wap_trade(self, data: CreateTradeData) -> TradeResult:
|
||||
"""手机网站支付"""
|
||||
biz_content = {
|
||||
'subject': data.goods_title[:256],
|
||||
'out_trade_no': data.trade_sn,
|
||||
'total_amount': str(data.amount / 100),
|
||||
'product_code': 'QUICK_WAP_WAY',
|
||||
'body': data.goods_detail[:128] if data.goods_detail else '',
|
||||
'passback_params': json.dumps(data.attach) if data.attach else '',
|
||||
}
|
||||
|
||||
params = self._build_params('alipay.trade.wap.pay', biz_content, data.return_url, data.notify_url)
|
||||
sign = self._generate_sign(params)
|
||||
params['sign'] = sign
|
||||
|
||||
pay_url = f"{self.GATEWAY_URL}?{urlencode(params)}"
|
||||
|
||||
return TradeResult(
|
||||
type='url',
|
||||
payload=pay_url,
|
||||
trade_sn=data.trade_sn
|
||||
)
|
||||
|
||||
def _create_qr_trade(self, data: CreateTradeData) -> TradeResult:
|
||||
"""扫码支付 (当面付)"""
|
||||
biz_content = {
|
||||
'subject': data.goods_title[:256],
|
||||
'out_trade_no': data.trade_sn,
|
||||
'total_amount': str(data.amount / 100),
|
||||
'body': data.goods_detail[:128] if data.goods_detail else '',
|
||||
}
|
||||
|
||||
params = self._build_params('alipay.trade.precreate', biz_content, '', data.notify_url)
|
||||
sign = self._generate_sign(params)
|
||||
params['sign'] = sign
|
||||
|
||||
# 调用接口获取二维码
|
||||
import requests
|
||||
response = requests.post(self.GATEWAY_URL, data=params)
|
||||
result = response.json()
|
||||
|
||||
if 'alipay_trade_precreate_response' in result:
|
||||
resp = result['alipay_trade_precreate_response']
|
||||
if resp.get('code') == '10000':
|
||||
return TradeResult(
|
||||
type='qrcode',
|
||||
payload=resp['qr_code'],
|
||||
trade_sn=data.trade_sn
|
||||
)
|
||||
|
||||
raise Exception(f"创建支付宝扫码支付失败: {result}")
|
||||
|
||||
def _build_params(self, method: str, biz_content: dict, return_url: str, notify_url: str) -> dict:
|
||||
"""构建公共参数"""
|
||||
import time
|
||||
|
||||
params = {
|
||||
'app_id': self.app_id,
|
||||
'method': method,
|
||||
'charset': 'utf-8',
|
||||
'sign_type': 'RSA2',
|
||||
'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'version': '1.0',
|
||||
'biz_content': json.dumps(biz_content, ensure_ascii=False),
|
||||
}
|
||||
|
||||
if return_url:
|
||||
params['return_url'] = return_url
|
||||
if notify_url:
|
||||
params['notify_url'] = notify_url
|
||||
|
||||
return params
|
||||
|
||||
def _generate_sign(self, params: dict) -> str:
|
||||
"""生成RSA2签名"""
|
||||
# 排序并拼接
|
||||
sorted_params = sorted([(k, v) for k, v in params.items() if v])
|
||||
sign_str = '&'.join([f'{k}={v}' for k, v in sorted_params])
|
||||
|
||||
# RSA2签名
|
||||
key = RSA.import_key(f"-----BEGIN RSA PRIVATE KEY-----\n{self.private_key}\n-----END RSA PRIVATE KEY-----")
|
||||
signer = PKCS1_v1_5.new(key)
|
||||
hash_obj = SHA256.new(sign_str.encode('utf-8'))
|
||||
signature = signer.sign(hash_obj)
|
||||
|
||||
return base64.b64encode(signature).decode('utf-8')
|
||||
|
||||
def verify_sign(self, data: Dict[str, Any]) -> bool:
|
||||
"""验证支付宝签名"""
|
||||
sign = data.pop('sign', '')
|
||||
sign_type = data.pop('sign_type', 'RSA2')
|
||||
|
||||
if not sign:
|
||||
return False
|
||||
|
||||
# 排序并拼接
|
||||
sorted_params = sorted([(k, v) for k, v in data.items() if v and k != 'sign_type'])
|
||||
sign_str = '&'.join([f'{k}={v}' for k, v in sorted_params])
|
||||
|
||||
try:
|
||||
key = RSA.import_key(f"-----BEGIN PUBLIC KEY-----\n{self.public_key}\n-----END PUBLIC KEY-----")
|
||||
verifier = PKCS1_v1_5.new(key)
|
||||
hash_obj = SHA256.new(sign_str.encode('utf-8'))
|
||||
verifier.verify(hash_obj, base64.b64decode(sign))
|
||||
return True
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"支付宝签名验证失败: {e}")
|
||||
return False
|
||||
|
||||
def parse_notify(self, data: Dict[str, Any]) -> NotifyResult:
|
||||
"""解析支付宝回调"""
|
||||
trade_status = data.get('trade_status', '')
|
||||
|
||||
if trade_status in ['TRADE_SUCCESS', 'TRADE_FINISHED']:
|
||||
status = 'paid'
|
||||
else:
|
||||
status = 'failed'
|
||||
|
||||
# 解析透传参数
|
||||
attach = {}
|
||||
passback = data.get('passback_params', '')
|
||||
if passback:
|
||||
try:
|
||||
attach = json.loads(passback)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 解析支付时间
|
||||
import time
|
||||
gmt_payment = data.get('gmt_payment', '')
|
||||
if gmt_payment:
|
||||
pay_time = int(time.mktime(time.strptime(gmt_payment, '%Y-%m-%d %H:%M:%S')))
|
||||
else:
|
||||
pay_time = int(time.time())
|
||||
|
||||
return NotifyResult(
|
||||
status=status,
|
||||
trade_sn=data.get('out_trade_no', ''),
|
||||
platform_sn=data.get('trade_no', ''),
|
||||
pay_amount=int(float(data.get('total_amount', 0)) * 100),
|
||||
pay_time=pay_time,
|
||||
currency='CNY',
|
||||
attach=attach,
|
||||
raw_data=data
|
||||
)
|
||||
|
||||
def close_trade(self, trade_sn: str) -> bool:
|
||||
"""关闭交易"""
|
||||
biz_content = {
|
||||
'out_trade_no': trade_sn,
|
||||
}
|
||||
|
||||
params = self._build_params('alipay.trade.close', biz_content, '', '')
|
||||
sign = self._generate_sign(params)
|
||||
params['sign'] = sign
|
||||
|
||||
import requests
|
||||
response = requests.post(self.GATEWAY_URL, data=params)
|
||||
result = response.json()
|
||||
|
||||
resp = result.get('alipay_trade_close_response', {})
|
||||
return resp.get('code') == '10000'
|
||||
|
||||
def query_trade(self, trade_sn: str) -> Optional[NotifyResult]:
|
||||
"""查询交易"""
|
||||
biz_content = {
|
||||
'out_trade_no': trade_sn,
|
||||
}
|
||||
|
||||
params = self._build_params('alipay.trade.query', biz_content, '', '')
|
||||
sign = self._generate_sign(params)
|
||||
params['sign'] = sign
|
||||
|
||||
import requests
|
||||
response = requests.post(self.GATEWAY_URL, data=params)
|
||||
result = response.json()
|
||||
|
||||
resp = result.get('alipay_trade_query_response', {})
|
||||
if resp.get('code') == '10000' and resp.get('trade_status') in ['TRADE_SUCCESS', 'TRADE_FINISHED']:
|
||||
return self.parse_notify(resp)
|
||||
|
||||
return None
|
||||
|
||||
def refund(self, trade_sn: str, refund_sn: str, amount: int, reason: str = '') -> bool:
|
||||
"""发起退款"""
|
||||
biz_content = {
|
||||
'out_trade_no': trade_sn,
|
||||
'out_request_no': refund_sn,
|
||||
'refund_amount': str(amount / 100),
|
||||
'refund_reason': reason or '用户申请退款',
|
||||
}
|
||||
|
||||
params = self._build_params('alipay.trade.refund', biz_content, '', '')
|
||||
sign = self._generate_sign(params)
|
||||
params['sign'] = sign
|
||||
|
||||
import requests
|
||||
response = requests.post(self.GATEWAY_URL, data=params)
|
||||
result = response.json()
|
||||
|
||||
resp = result.get('alipay_trade_refund_response', {})
|
||||
return resp.get('code') == '10000'
|
||||
|
||||
def success_response(self) -> str:
|
||||
return 'success'
|
||||
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
支付网关工厂 (Payment Gateway Factory)
|
||||
基于 www.lytiao.com 项目提取的支付逻辑,适配 Python FastAPI
|
||||
|
||||
作者: 卡若
|
||||
版本: v4.0
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import hashlib
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
logger = logging.getLogger('payment')
|
||||
|
||||
|
||||
class PaymentPlatform(Enum):
|
||||
"""支付平台枚举"""
|
||||
ALIPAY = 'alipay'
|
||||
WECHAT = 'wechat'
|
||||
PAYPAL = 'paypal'
|
||||
STRIPE = 'stripe'
|
||||
USDT = 'usdt'
|
||||
COIN = 'coin'
|
||||
|
||||
|
||||
class TradeType(Enum):
|
||||
"""交易类型"""
|
||||
PURCHASE = 'purchase' # 购买
|
||||
RECHARGE = 'recharge' # 充值
|
||||
|
||||
|
||||
class TradeStatus(Enum):
|
||||
"""交易状态"""
|
||||
PAYING = 'paying'
|
||||
PAID = 'paid'
|
||||
CLOSED = 'closed'
|
||||
REFUNDED = 'refunded'
|
||||
|
||||
|
||||
@dataclass
|
||||
class CreateTradeData:
|
||||
"""创建交易请求数据"""
|
||||
goods_title: str
|
||||
goods_detail: str
|
||||
trade_sn: str
|
||||
order_sn: str
|
||||
amount: int # 金额,单位:分
|
||||
notify_url: str
|
||||
return_url: str = ''
|
||||
platform_type: str = 'web' # web/wap/jsapi/native/h5/app
|
||||
create_ip: str = ''
|
||||
open_id: str = '' # 微信JSAPI支付需要
|
||||
attach: Dict[str, Any] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TradeResult:
|
||||
"""交易结果"""
|
||||
type: str # url/qrcode/json/address/direct
|
||||
payload: Any # 支付数据
|
||||
trade_sn: str
|
||||
expiration: int = 1800 # 过期时间(秒)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotifyResult:
|
||||
"""回调解析结果"""
|
||||
status: str
|
||||
trade_sn: str
|
||||
platform_sn: str
|
||||
pay_amount: int # 分
|
||||
pay_time: int # 时间戳
|
||||
currency: str = 'CNY'
|
||||
attach: Dict[str, Any] = None
|
||||
raw_data: Dict[str, Any] = None
|
||||
|
||||
|
||||
class PaymentException(Exception):
|
||||
"""支付异常基类"""
|
||||
pass
|
||||
|
||||
|
||||
class SignatureError(PaymentException):
|
||||
"""签名验证失败"""
|
||||
pass
|
||||
|
||||
|
||||
class AmountMismatchError(PaymentException):
|
||||
"""金额不匹配"""
|
||||
pass
|
||||
|
||||
|
||||
class GatewayNotFoundError(PaymentException):
|
||||
"""支付网关不存在"""
|
||||
pass
|
||||
|
||||
|
||||
class AbstractGateway(ABC):
|
||||
"""
|
||||
支付网关抽象基类
|
||||
所有支付网关必须实现此接口
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict[str, Any] = None):
|
||||
self.config = config or {}
|
||||
|
||||
@abstractmethod
|
||||
def create_trade(self, data: CreateTradeData) -> TradeResult:
|
||||
"""
|
||||
创建交易
|
||||
|
||||
Args:
|
||||
data: 交易数据
|
||||
|
||||
Returns:
|
||||
TradeResult: 包含支付参数的结果
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def verify_sign(self, data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
验证签名
|
||||
|
||||
Args:
|
||||
data: 回调原始数据
|
||||
|
||||
Returns:
|
||||
bool: 签名是否有效
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def parse_notify(self, data: Any) -> NotifyResult:
|
||||
"""
|
||||
解析回调数据
|
||||
|
||||
Args:
|
||||
data: 回调原始数据 (可能是dict或xml字符串)
|
||||
|
||||
Returns:
|
||||
NotifyResult: 解析后的结果
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def close_trade(self, trade_sn: str) -> bool:
|
||||
"""
|
||||
关闭交易
|
||||
|
||||
Args:
|
||||
trade_sn: 交易流水号
|
||||
|
||||
Returns:
|
||||
bool: 是否关闭成功
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def query_trade(self, trade_sn: str) -> Optional[NotifyResult]:
|
||||
"""
|
||||
查询交易状态
|
||||
|
||||
Args:
|
||||
trade_sn: 交易流水号
|
||||
|
||||
Returns:
|
||||
NotifyResult: 交易状态,未支付返回None
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def refund(self, trade_sn: str, refund_sn: str, amount: int, reason: str = '') -> bool:
|
||||
"""
|
||||
发起退款
|
||||
|
||||
Args:
|
||||
trade_sn: 原交易流水号
|
||||
refund_sn: 退款单号
|
||||
amount: 退款金额(分)
|
||||
reason: 退款原因
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
pass
|
||||
|
||||
def success_response(self) -> str:
|
||||
"""回调成功响应"""
|
||||
return 'success'
|
||||
|
||||
def fail_response(self) -> str:
|
||||
"""回调失败响应"""
|
||||
return 'fail'
|
||||
|
||||
|
||||
class PaymentFactory:
|
||||
"""
|
||||
支付网关工厂
|
||||
|
||||
使用示例:
|
||||
# 注册网关
|
||||
PaymentFactory.register('alipay', AlipayGateway)
|
||||
PaymentFactory.register('wechat', WechatGateway)
|
||||
|
||||
# 创建网关实例
|
||||
gateway = PaymentFactory.create('wechat_jsapi')
|
||||
result = gateway.create_trade(data)
|
||||
"""
|
||||
|
||||
_gateways: Dict[str, type] = {}
|
||||
|
||||
@classmethod
|
||||
def register(cls, name: str, gateway_class: type):
|
||||
"""注册支付网关"""
|
||||
cls._gateways[name] = gateway_class
|
||||
logger.info(f"注册支付网关: {name}")
|
||||
|
||||
@classmethod
|
||||
def create(cls, gateway: str) -> AbstractGateway:
|
||||
"""
|
||||
创建支付网关实例
|
||||
|
||||
Args:
|
||||
gateway: 网关名称,格式如 'wechat_jsapi',会取下划线前的部分
|
||||
|
||||
Returns:
|
||||
AbstractGateway: 网关实例
|
||||
"""
|
||||
gateway_name = gateway.split('_')[0]
|
||||
|
||||
if gateway_name not in cls._gateways:
|
||||
raise GatewayNotFoundError(f"不支持的支付网关: {gateway}")
|
||||
|
||||
gateway_class = cls._gateways[gateway_name]
|
||||
config = cls._get_gateway_config(gateway_name)
|
||||
|
||||
return gateway_class(config)
|
||||
|
||||
@classmethod
|
||||
def _get_gateway_config(cls, gateway_name: str) -> Dict[str, Any]:
|
||||
"""获取网关配置"""
|
||||
config_map = {
|
||||
'alipay': {
|
||||
'app_id': os.getenv('ALIPAY_APP_ID', ''),
|
||||
'pid': os.getenv('ALIPAY_PID', ''),
|
||||
'seller_email': os.getenv('ALIPAY_SELLER_EMAIL', ''),
|
||||
'private_key': os.getenv('ALIPAY_PRIVATE_KEY', ''),
|
||||
'public_key': os.getenv('ALIPAY_PUBLIC_KEY', ''),
|
||||
'md5_key': os.getenv('ALIPAY_MD5_KEY', ''),
|
||||
},
|
||||
'wechat': {
|
||||
'appid': os.getenv('WECHAT_APPID', ''),
|
||||
'app_secret': os.getenv('WECHAT_APP_SECRET', ''),
|
||||
'mch_id': os.getenv('WECHAT_MCH_ID', ''),
|
||||
'mch_key': os.getenv('WECHAT_MCH_KEY', ''),
|
||||
'cert_path': os.getenv('WECHAT_CERT_PATH', ''),
|
||||
'key_path': os.getenv('WECHAT_KEY_PATH', ''),
|
||||
},
|
||||
'paypal': {
|
||||
'client_id': os.getenv('PAYPAL_CLIENT_ID', ''),
|
||||
'client_secret': os.getenv('PAYPAL_CLIENT_SECRET', ''),
|
||||
'mode': os.getenv('PAYPAL_MODE', 'sandbox'),
|
||||
},
|
||||
'stripe': {
|
||||
'public_key': os.getenv('STRIPE_PUBLIC_KEY', ''),
|
||||
'secret_key': os.getenv('STRIPE_SECRET_KEY', ''),
|
||||
'webhook_secret': os.getenv('STRIPE_WEBHOOK_SECRET', ''),
|
||||
},
|
||||
}
|
||||
|
||||
return config_map.get(gateway_name, {})
|
||||
|
||||
@classmethod
|
||||
def get_enabled_gateways(cls) -> list:
|
||||
"""获取已启用的支付网关列表"""
|
||||
enabled = []
|
||||
|
||||
if os.getenv('ALIPAY_ENABLED', 'false').lower() == 'true':
|
||||
enabled.append({
|
||||
'gateway': 'alipay',
|
||||
'name': '支付宝',
|
||||
'icon': '/icons/alipay.png',
|
||||
'types': ['web', 'wap', 'qr']
|
||||
})
|
||||
|
||||
if os.getenv('WECHAT_ENABLED', 'false').lower() == 'true':
|
||||
enabled.append({
|
||||
'gateway': 'wechat',
|
||||
'name': '微信支付',
|
||||
'icon': '/icons/wechat.png',
|
||||
'types': ['native', 'jsapi', 'h5', 'app']
|
||||
})
|
||||
|
||||
if os.getenv('PAYPAL_ENABLED', 'false').lower() == 'true':
|
||||
enabled.append({
|
||||
'gateway': 'paypal',
|
||||
'name': 'PayPal',
|
||||
'icon': '/icons/paypal.png',
|
||||
'types': ['redirect']
|
||||
})
|
||||
|
||||
if os.getenv('STRIPE_ENABLED', 'false').lower() == 'true':
|
||||
enabled.append({
|
||||
'gateway': 'stripe',
|
||||
'name': 'Stripe',
|
||||
'icon': '/icons/stripe.png',
|
||||
'types': ['redirect']
|
||||
})
|
||||
|
||||
return enabled
|
||||
|
||||
|
||||
def generate_trade_sn(prefix: str = 'T') -> str:
|
||||
"""
|
||||
生成交易流水号
|
||||
|
||||
格式: 前缀 + 年月日时分秒 + 5位随机数
|
||||
示例: T2024011710053012345
|
||||
"""
|
||||
import random
|
||||
timestamp = time.strftime('%Y%m%d%H%M%S')
|
||||
random_num = random.randint(10000, 99999)
|
||||
return f"{prefix}{timestamp}{random_num}"
|
||||
|
||||
|
||||
def yuan_to_fen(yuan: float) -> int:
|
||||
"""元转分"""
|
||||
return int(round(yuan * 100))
|
||||
|
||||
|
||||
def fen_to_yuan(fen: int) -> float:
|
||||
"""分转元"""
|
||||
return round(fen / 100, 2)
|
||||
@@ -0,0 +1,317 @@
|
||||
"""
|
||||
微信支付网关实现 (Wechat Pay Gateway)
|
||||
基于 www.lytiao.com 项目提取
|
||||
|
||||
作者: 卡若
|
||||
版本: v4.0
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import hashlib
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Dict, Any, Optional
|
||||
from urllib.parse import urlencode
|
||||
import uuid
|
||||
|
||||
from payment_factory import (
|
||||
AbstractGateway, CreateTradeData, TradeResult, NotifyResult,
|
||||
SignatureError, logger
|
||||
)
|
||||
|
||||
|
||||
class WechatGateway(AbstractGateway):
|
||||
"""
|
||||
微信支付网关
|
||||
|
||||
支持:
|
||||
- Native扫码支付 (platform_type='native')
|
||||
- JSAPI公众号/小程序支付 (platform_type='jsapi')
|
||||
- H5支付 (platform_type='h5')
|
||||
- APP支付 (platform_type='app')
|
||||
"""
|
||||
|
||||
UNIFIED_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/unifiedorder'
|
||||
ORDER_QUERY_URL = 'https://api.mch.weixin.qq.com/pay/orderquery'
|
||||
CLOSE_ORDER_URL = 'https://api.mch.weixin.qq.com/pay/closeorder'
|
||||
REFUND_URL = 'https://api.mch.weixin.qq.com/secapi/pay/refund'
|
||||
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
super().__init__(config)
|
||||
self.appid = config.get('appid', '')
|
||||
self.app_secret = config.get('app_secret', '')
|
||||
self.mch_id = config.get('mch_id', '')
|
||||
self.mch_key = config.get('mch_key', '')
|
||||
self.cert_path = config.get('cert_path', '')
|
||||
self.key_path = config.get('key_path', '')
|
||||
|
||||
def create_trade(self, data: CreateTradeData) -> TradeResult:
|
||||
"""创建微信支付交易"""
|
||||
platform_type = data.platform_type.upper()
|
||||
|
||||
# 构建统一下单参数
|
||||
params = {
|
||||
'appid': self.appid,
|
||||
'mch_id': self.mch_id,
|
||||
'nonce_str': self._generate_nonce(),
|
||||
'body': data.goods_title[:128],
|
||||
'out_trade_no': data.trade_sn,
|
||||
'total_fee': str(data.amount), # 微信以分为单位
|
||||
'spbill_create_ip': data.create_ip or '127.0.0.1',
|
||||
'notify_url': data.notify_url,
|
||||
'trade_type': platform_type,
|
||||
'attach': json.dumps(data.attach) if data.attach else '',
|
||||
}
|
||||
|
||||
# JSAPI需要openid
|
||||
if platform_type == 'JSAPI':
|
||||
if not data.open_id:
|
||||
raise ValueError("微信JSAPI支付需要提供 openid")
|
||||
params['openid'] = data.open_id
|
||||
|
||||
# H5支付需要scene_info
|
||||
if platform_type == 'MWEB':
|
||||
params['scene_info'] = json.dumps({
|
||||
'h5_info': {
|
||||
'type': 'Wap',
|
||||
'wap_url': data.return_url,
|
||||
'wap_name': data.goods_title[:32]
|
||||
}
|
||||
})
|
||||
|
||||
# 生成签名
|
||||
params['sign'] = self._generate_sign(params)
|
||||
|
||||
# 调用统一下单接口
|
||||
import requests
|
||||
xml_data = self._dict_to_xml(params)
|
||||
response = requests.post(self.UNIFIED_ORDER_URL, data=xml_data.encode('utf-8'))
|
||||
result = self._xml_to_dict(response.text)
|
||||
|
||||
if result.get('return_code') != 'SUCCESS':
|
||||
raise Exception(f"微信支付统一下单失败: {result.get('return_msg')}")
|
||||
|
||||
if result.get('result_code') != 'SUCCESS':
|
||||
raise Exception(f"微信支付统一下单失败: {result.get('err_code_des')}")
|
||||
|
||||
# 根据类型返回不同格式
|
||||
if platform_type == 'NATIVE':
|
||||
# 扫码支付返回二维码链接
|
||||
return TradeResult(
|
||||
type='qrcode',
|
||||
payload=result['code_url'],
|
||||
trade_sn=data.trade_sn
|
||||
)
|
||||
|
||||
elif platform_type == 'JSAPI':
|
||||
# 公众号支付返回JS SDK参数
|
||||
prepay_id = result['prepay_id']
|
||||
js_params = {
|
||||
'appId': self.appid,
|
||||
'timeStamp': str(int(time.time())),
|
||||
'nonceStr': self._generate_nonce(),
|
||||
'package': f'prepay_id={prepay_id}',
|
||||
'signType': 'MD5',
|
||||
}
|
||||
js_params['paySign'] = self._generate_sign(js_params)
|
||||
|
||||
return TradeResult(
|
||||
type='json',
|
||||
payload=js_params,
|
||||
trade_sn=data.trade_sn
|
||||
)
|
||||
|
||||
elif platform_type == 'MWEB':
|
||||
# H5支付返回跳转链接
|
||||
mweb_url = result['mweb_url']
|
||||
if data.return_url:
|
||||
mweb_url += f"&redirect_url={urlencode({'': data.return_url})[1:]}"
|
||||
|
||||
return TradeResult(
|
||||
type='url',
|
||||
payload=mweb_url,
|
||||
trade_sn=data.trade_sn
|
||||
)
|
||||
|
||||
elif platform_type == 'APP':
|
||||
# APP支付返回SDK参数
|
||||
prepay_id = result['prepay_id']
|
||||
app_params = {
|
||||
'appid': self.appid,
|
||||
'partnerid': self.mch_id,
|
||||
'prepayid': prepay_id,
|
||||
'package': 'Sign=WXPay',
|
||||
'noncestr': self._generate_nonce(),
|
||||
'timestamp': str(int(time.time())),
|
||||
}
|
||||
app_params['sign'] = self._generate_sign(app_params)
|
||||
|
||||
return TradeResult(
|
||||
type='json',
|
||||
payload=app_params,
|
||||
trade_sn=data.trade_sn
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError(f"不支持的微信支付类型: {platform_type}")
|
||||
|
||||
def verify_sign(self, data: Dict[str, Any]) -> bool:
|
||||
"""验证微信签名"""
|
||||
sign = data.pop('sign', '')
|
||||
if not sign:
|
||||
return False
|
||||
|
||||
calculated_sign = self._generate_sign(data)
|
||||
return calculated_sign == sign
|
||||
|
||||
def parse_notify(self, data: Any) -> NotifyResult:
|
||||
"""解析微信回调"""
|
||||
# 如果是XML字符串,先转换为dict
|
||||
if isinstance(data, str):
|
||||
data = self._xml_to_dict(data)
|
||||
|
||||
# 验证签名
|
||||
if not self.verify_sign(data.copy()):
|
||||
raise SignatureError("微信签名验证失败")
|
||||
|
||||
result_code = data.get('result_code', '')
|
||||
|
||||
if result_code == 'SUCCESS':
|
||||
status = 'paid'
|
||||
else:
|
||||
status = 'failed'
|
||||
|
||||
# 解析透传参数
|
||||
attach = {}
|
||||
attach_str = data.get('attach', '')
|
||||
if attach_str:
|
||||
try:
|
||||
attach = json.loads(attach_str)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 解析支付时间 (格式: 20240117100530)
|
||||
time_end = data.get('time_end', '')
|
||||
if time_end:
|
||||
pay_time = int(time.mktime(time.strptime(time_end, '%Y%m%d%H%M%S')))
|
||||
else:
|
||||
pay_time = int(time.time())
|
||||
|
||||
return NotifyResult(
|
||||
status=status,
|
||||
trade_sn=data.get('out_trade_no', ''),
|
||||
platform_sn=data.get('transaction_id', ''),
|
||||
pay_amount=int(data.get('cash_fee', 0)),
|
||||
pay_time=pay_time,
|
||||
currency=data.get('fee_type', 'CNY'),
|
||||
attach=attach,
|
||||
raw_data=data
|
||||
)
|
||||
|
||||
def close_trade(self, trade_sn: str) -> bool:
|
||||
"""关闭微信交易"""
|
||||
params = {
|
||||
'appid': self.appid,
|
||||
'mch_id': self.mch_id,
|
||||
'out_trade_no': trade_sn,
|
||||
'nonce_str': self._generate_nonce(),
|
||||
}
|
||||
params['sign'] = self._generate_sign(params)
|
||||
|
||||
import requests
|
||||
xml_data = self._dict_to_xml(params)
|
||||
response = requests.post(self.CLOSE_ORDER_URL, data=xml_data.encode('utf-8'))
|
||||
result = self._xml_to_dict(response.text)
|
||||
|
||||
return result.get('result_code') == 'SUCCESS'
|
||||
|
||||
def query_trade(self, trade_sn: str) -> Optional[NotifyResult]:
|
||||
"""查询微信交易"""
|
||||
params = {
|
||||
'appid': self.appid,
|
||||
'mch_id': self.mch_id,
|
||||
'out_trade_no': trade_sn,
|
||||
'nonce_str': self._generate_nonce(),
|
||||
}
|
||||
params['sign'] = self._generate_sign(params)
|
||||
|
||||
import requests
|
||||
xml_data = self._dict_to_xml(params)
|
||||
response = requests.post(self.ORDER_QUERY_URL, data=xml_data.encode('utf-8'))
|
||||
result = self._xml_to_dict(response.text)
|
||||
|
||||
if result.get('trade_state') == 'SUCCESS':
|
||||
return self.parse_notify(result)
|
||||
|
||||
return None
|
||||
|
||||
def refund(self, trade_sn: str, refund_sn: str, amount: int, reason: str = '') -> bool:
|
||||
"""微信退款 (需要证书)"""
|
||||
# 先查询原交易获取金额
|
||||
query_result = self.query_trade(trade_sn)
|
||||
if not query_result:
|
||||
return False
|
||||
|
||||
params = {
|
||||
'appid': self.appid,
|
||||
'mch_id': self.mch_id,
|
||||
'nonce_str': self._generate_nonce(),
|
||||
'out_trade_no': trade_sn,
|
||||
'out_refund_no': refund_sn,
|
||||
'total_fee': str(query_result.pay_amount),
|
||||
'refund_fee': str(amount),
|
||||
'refund_desc': reason or '用户申请退款',
|
||||
}
|
||||
params['sign'] = self._generate_sign(params)
|
||||
|
||||
import requests
|
||||
xml_data = self._dict_to_xml(params)
|
||||
|
||||
# 退款需要证书
|
||||
response = requests.post(
|
||||
self.REFUND_URL,
|
||||
data=xml_data.encode('utf-8'),
|
||||
cert=(self.cert_path, self.key_path)
|
||||
)
|
||||
result = self._xml_to_dict(response.text)
|
||||
|
||||
return result.get('result_code') == 'SUCCESS'
|
||||
|
||||
def success_response(self) -> str:
|
||||
"""微信回调成功响应"""
|
||||
return '<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>'
|
||||
|
||||
def fail_response(self) -> str:
|
||||
"""微信回调失败响应"""
|
||||
return '<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[ERROR]]></return_msg></xml>'
|
||||
|
||||
def _generate_nonce(self) -> str:
|
||||
"""生成随机字符串"""
|
||||
return uuid.uuid4().hex
|
||||
|
||||
def _generate_sign(self, params: dict) -> str:
|
||||
"""生成MD5签名"""
|
||||
# 排序并拼接
|
||||
sorted_params = sorted([(k, v) for k, v in params.items() if v and k != 'sign'])
|
||||
sign_str = '&'.join([f'{k}={v}' for k, v in sorted_params])
|
||||
sign_str += f'&key={self.mch_key}'
|
||||
|
||||
# MD5签名
|
||||
return hashlib.md5(sign_str.encode('utf-8')).hexdigest().upper()
|
||||
|
||||
def _dict_to_xml(self, data: dict) -> str:
|
||||
"""字典转XML"""
|
||||
xml = ['<xml>']
|
||||
for k, v in data.items():
|
||||
if isinstance(v, str):
|
||||
xml.append(f'<{k}><![CDATA[{v}]]></{k}>')
|
||||
else:
|
||||
xml.append(f'<{k}>{v}</{k}>')
|
||||
xml.append('</xml>')
|
||||
return ''.join(xml)
|
||||
|
||||
def _xml_to_dict(self, xml_str: str) -> dict:
|
||||
"""XML转字典"""
|
||||
root = ET.fromstring(xml_str)
|
||||
return {child.tag: child.text for child in root}
|
||||
101
addons/Universal_Payment_Module copy/4_卡若配置/env.example.txt
Normal file
101
addons/Universal_Payment_Module copy/4_卡若配置/env.example.txt
Normal file
@@ -0,0 +1,101 @@
|
||||
# ============================================================================
|
||||
# 卡若私域支付配置 (Karuo Payment Config)
|
||||
# ============================================================================
|
||||
# ⚠️ 警告: 此文件包含敏感信息,请勿提交到 Git!
|
||||
# 使用方法: 复制此文件为 .env 并填入真实值
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 1. 基础环境
|
||||
# ----------------------------------------------------------------------------
|
||||
APP_ENV=production
|
||||
APP_NAME=卡若私域
|
||||
APP_URL=https://www.lytiao.com
|
||||
APP_CURRENCY=CNY
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 2. 数据库 (卡若私域数据库)
|
||||
# ----------------------------------------------------------------------------
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=10.88.182.62
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=payment_db
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=Vtka(agu)-1
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 3. 支付宝 (Alipay)
|
||||
# ----------------------------------------------------------------------------
|
||||
ALIPAY_ENABLED=true
|
||||
ALIPAY_MODE=production
|
||||
ALIPAY_APP_ID=
|
||||
ALIPAY_PID=2088511801157159
|
||||
ALIPAY_SELLER_EMAIL=zhengzhiqun@vip.qq.com
|
||||
ALIPAY_PRIVATE_KEY=
|
||||
ALIPAY_PUBLIC_KEY=
|
||||
ALIPAY_MD5_KEY=lz6ey1h3kl9zqkgtjz3avb5gk37wzbrp
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 4. 微信支付 (Wechat Pay)
|
||||
# ----------------------------------------------------------------------------
|
||||
WECHAT_ENABLED=true
|
||||
WECHAT_MODE=production
|
||||
|
||||
# 网站/H5支付
|
||||
WECHAT_APPID=wx432c93e275548671
|
||||
WECHAT_APP_SECRET=25b7e7fdb7998e5107e242ebb6ddabd0
|
||||
|
||||
# 服务号 (JSAPI)
|
||||
WECHAT_SERVICE_APPID=wx7c0dbf34ddba300d
|
||||
WECHAT_SERVICE_SECRET=f865ef18c43dfea6cbe3b1f1aebdb82e
|
||||
|
||||
# 商户信息
|
||||
WECHAT_MCH_ID=1318592501
|
||||
WECHAT_MCH_KEY=wx3e31b068be59ddc131b068be59ddc2
|
||||
|
||||
# 证书路径
|
||||
WECHAT_CERT_PATH=./cert/wechat/apiclient_cert.pem
|
||||
WECHAT_KEY_PATH=./cert/wechat/apiclient_key.pem
|
||||
|
||||
# MP文件验证码
|
||||
WECHAT_MP_VERIFY=SP8AfZJyAvprRORT
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 5. PayPal (暂未启用)
|
||||
# ----------------------------------------------------------------------------
|
||||
PAYPAL_ENABLED=false
|
||||
PAYPAL_MODE=sandbox
|
||||
PAYPAL_CLIENT_ID=
|
||||
PAYPAL_CLIENT_SECRET=
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 6. Stripe (暂未启用)
|
||||
# ----------------------------------------------------------------------------
|
||||
STRIPE_ENABLED=false
|
||||
STRIPE_MODE=test
|
||||
STRIPE_PUBLIC_KEY=
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 7. USDT (暂未启用)
|
||||
# ----------------------------------------------------------------------------
|
||||
USDT_ENABLED=false
|
||||
USDT_GATEWAY_TYPE=nowpayments
|
||||
NOWPAYMENTS_API_KEY=
|
||||
NOWPAYMENTS_IPN_SECRET=
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 8. 高级配置
|
||||
# ----------------------------------------------------------------------------
|
||||
# 虚拟币/积分
|
||||
COIN_ENABLED=false
|
||||
COIN_RATE=100
|
||||
|
||||
# 订单配置
|
||||
ORDER_EXPIRE_MINUTES=30
|
||||
TRADE_SN_PREFIX=T
|
||||
|
||||
# 日志
|
||||
PAYMENT_LOG_LEVEL=info
|
||||
PAYMENT_LOG_PATH=./logs/payment.log
|
||||
142
addons/Universal_Payment_Module copy/README.md
Normal file
142
addons/Universal_Payment_Module copy/README.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# 🌐 全球通用支付模块 (Universal Payment Module) v4.0
|
||||
|
||||
> **配置驱动 (Configuration-Driven)** | **API 优先 (API-First)** | **AI 智能对接**
|
||||
>
|
||||
> 让任何语言的项目在 5 分钟内接入支付宝、微信支付、PayPal、Stripe 和 USDT
|
||||
|
||||
## 📂 模块结构
|
||||
|
||||
```
|
||||
Universal_Payment_Module/
|
||||
├── 1_核心设计_通用协议/ # [灵魂] 定义了支付的"法律"
|
||||
│ ├── 标准配置模板.yaml # 填空即可配置所有支付参数
|
||||
│ ├── API接口定义.md # 无论用什么语言,接口都长这样
|
||||
│ ├── 业务逻辑与模型.md # 数据库表结构设计 (Order/PayTrade)
|
||||
│ └── 安全与合规.md # 支付安全最佳实践
|
||||
│
|
||||
├── 2_智能对接_AI指令/ # [工具] AI 编译器
|
||||
│ ├── 通用集成指令.md # 发给 AI,自动生成代码
|
||||
│ └── Cursor规则.md # Cursor IDE 专用规则
|
||||
│
|
||||
├── 3_逻辑参考_通用实现/ # [参考] 可直接复用的代码
|
||||
│ ├── 前端收银台Demo.html # 原生 JS 实现的通用收银台
|
||||
│ ├── 后端源码/ # 多语言参考实现
|
||||
│ │ ├── php/ # PHP (Laravel/Symfony)
|
||||
│ │ ├── python/ # Python (FastAPI/Django)
|
||||
│ │ ├── nodejs/ # Node.js (Express/NestJS)
|
||||
│ │ └── java/ # Java (Spring Boot)
|
||||
│ └── 前端模板/ # Vue/React/原生JS 模板
|
||||
│
|
||||
├── 4_卡若配置/ # [私有] 卡若的支付密钥 (勿提交Git)
|
||||
│ └── .env.example # 配置示例
|
||||
│
|
||||
└── README.md # 本说明文档
|
||||
```
|
||||
|
||||
## 🚀 极速对接 (3步完成)
|
||||
|
||||
### 第一步:配置 (Config)
|
||||
```bash
|
||||
# 1. 复制配置模板到你的项目
|
||||
cp 1_核心设计_通用协议/标准配置模板.yaml your-project/.env
|
||||
|
||||
# 2. 填入你的支付密钥
|
||||
```
|
||||
|
||||
### 第二步:生成代码 (Generate with AI)
|
||||
```
|
||||
发送给 Cursor/ChatGPT:
|
||||
"请读取 Universal_Payment_Module 目录,我的项目是 Python FastAPI,
|
||||
采用嵌入式集成,帮我生成支付模块代码。"
|
||||
```
|
||||
|
||||
### 第三步:前端接入 (Frontend)
|
||||
```javascript
|
||||
// 只需调用一个 API
|
||||
const result = await fetch('/api/payment/checkout', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ order_sn: '202401170001', gateway: 'wechat_jsapi' })
|
||||
});
|
||||
```
|
||||
|
||||
## 🌍 支持的支付渠道
|
||||
|
||||
| 渠道 | 能力 | 场景 | 状态 |
|
||||
|:---|:---|:---|:---|
|
||||
| **支付宝 Alipay** | 扫码/H5/APP/小程序 | 中国市场 (CNY) | ✅ 已实现 |
|
||||
| **微信支付 Wechat** | JSAPI/Native/H5/APP/小程序 | 中国市场 (CNY) | ✅ 已实现 |
|
||||
| **PayPal** | 信用卡/订阅 | 全球市场 (USD/EUR) | ✅ 已实现 |
|
||||
| **Stripe** | 信用卡/订阅/Apple Pay | 全球市场 | ✅ 已实现 |
|
||||
| **USDT (TRC20)** | 链上转账/监听 | Web3/抗审查 | ✅ 已实现 |
|
||||
|
||||
## ✨ v4.0 核心特性
|
||||
|
||||
### 1. 配置驱动 (Zero-Code Config)
|
||||
- 所有密钥通过环境变量注入,无需改动代码
|
||||
- 支持多环境切换 (development/production)
|
||||
|
||||
### 2. 工厂模式 (Payment Factory)
|
||||
```python
|
||||
# 所有支付网关统一接口
|
||||
payment = PaymentFactory.create('wechat')
|
||||
result = payment.create_trade(order)
|
||||
```
|
||||
|
||||
### 3. 幂等性保障 (Idempotency)
|
||||
- 回调通知自动去重
|
||||
- 订单状态机管理
|
||||
|
||||
### 4. AI 智能生成
|
||||
- 提供 Cursor/Copilot 专用提示词
|
||||
- 一键生成任意语言的完整实现
|
||||
|
||||
## 📖 快速参考
|
||||
|
||||
### 创建订单
|
||||
```http
|
||||
POST /api/payment/create_order
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"user_id": "u1001",
|
||||
"title": "VIP会员",
|
||||
"amount": 99.00,
|
||||
"currency": "CNY",
|
||||
"product_id": "vip_monthly"
|
||||
}
|
||||
```
|
||||
|
||||
### 发起支付
|
||||
```http
|
||||
POST /api/payment/checkout
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"order_sn": "202401170001",
|
||||
"gateway": "wechat_jsapi",
|
||||
"openid": "oXxx..."
|
||||
}
|
||||
```
|
||||
|
||||
### 支付状态查询
|
||||
```http
|
||||
GET /api/payment/status/202401170001
|
||||
```
|
||||
|
||||
## 🔐 安全须知
|
||||
|
||||
1. **密钥安全**: 所有密钥存放在 `.env`,**绝不提交到 Git**
|
||||
2. **HTTPS 强制**: 生产环境必须启用 HTTPS
|
||||
3. **签名验证**: 所有回调必须验签
|
||||
4. **金额校验**: 支付金额必须与订单金额匹配
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [API 接口定义](./1_核心设计_通用协议/API接口定义.md)
|
||||
- [数据库模型](./1_核心设计_通用协议/业务逻辑与模型.md)
|
||||
- [AI 集成指令](./2_智能对接_AI指令/通用集成指令.md)
|
||||
- [Cursor 规则](./2_智能对接_AI指令/Cursor规则.md)
|
||||
|
||||
---
|
||||
|
||||
**作者**: 卡若 | **联系**: 28533368 (微信) | **更新**: 2026-01-17
|
||||
Reference in New Issue
Block a user