pay-py-kit 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pay_py_kit-0.2.0/.github/workflows/ci.yml +56 -0
- pay_py_kit-0.2.0/.github/workflows/release.yml +90 -0
- pay_py_kit-0.2.0/.gitignore +13 -0
- pay_py_kit-0.2.0/CHANGELOG.md +37 -0
- pay_py_kit-0.2.0/PKG-INFO +257 -0
- pay_py_kit-0.2.0/README.md +246 -0
- pay_py_kit-0.2.0/pyproject.toml +46 -0
- pay_py_kit-0.2.0/src/pay_kit/__init__.py +3 -0
- pay_py_kit-0.2.0/src/pay_kit/exceptions.py +44 -0
- pay_py_kit-0.2.0/src/pay_kit/models.py +85 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/__init__.py +5 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/alipay/__init__.py +6 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/alipay/config.py +13 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/alipay/errors.py +7 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/alipay/http_client.py +102 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/alipay/provider.py +308 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/alipay/sign.py +37 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/base.py +62 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/crypto.py +64 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/errors.py +24 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/http_client.py +49 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/wechat/__init__.py +6 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/wechat/config.py +17 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/wechat/errors.py +7 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/wechat/http_client.py +122 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/wechat/provider.py +343 -0
- pay_py_kit-0.2.0/src/pay_kit/providers/wechat/sign.py +63 -0
- pay_py_kit-0.2.0/src/pay_kit/py.typed +0 -0
- pay_py_kit-0.2.0/src/pay_kit/types.py +47 -0
- pay_py_kit-0.2.0/tests/__init__.py +0 -0
- pay_py_kit-0.2.0/tests/conftest.py +59 -0
- pay_py_kit-0.2.0/tests/test_alipay_config.py +54 -0
- pay_py_kit-0.2.0/tests/test_alipay_errors.py +58 -0
- pay_py_kit-0.2.0/tests/test_alipay_http_client.py +169 -0
- pay_py_kit-0.2.0/tests/test_alipay_provider.py +468 -0
- pay_py_kit-0.2.0/tests/test_alipay_sign.py +48 -0
- pay_py_kit-0.2.0/tests/test_exceptions.py +82 -0
- pay_py_kit-0.2.0/tests/test_http_client.py +72 -0
- pay_py_kit-0.2.0/tests/test_init.py +90 -0
- pay_py_kit-0.2.0/tests/test_models.py +133 -0
- pay_py_kit-0.2.0/tests/test_providers.py +120 -0
- pay_py_kit-0.2.0/tests/test_types.py +74 -0
- pay_py_kit-0.2.0/tests/test_wechat_config.py +87 -0
- pay_py_kit-0.2.0/tests/test_wechat_errors.py +59 -0
- pay_py_kit-0.2.0/tests/test_wechat_http_client.py +198 -0
- pay_py_kit-0.2.0/tests/test_wechat_provider.py +549 -0
- pay_py_kit-0.2.0/tests/test_wechat_sign.py +138 -0
- pay_py_kit-0.2.0/uv.lock +803 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v4
|
|
21
|
+
|
|
22
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
23
|
+
run: uv python install ${{ matrix.python-version }}
|
|
24
|
+
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: uv sync
|
|
27
|
+
|
|
28
|
+
- name: Lint (ruff)
|
|
29
|
+
run: |
|
|
30
|
+
uv run ruff check .
|
|
31
|
+
uv run ruff format --check .
|
|
32
|
+
|
|
33
|
+
- name: Type check (mypy)
|
|
34
|
+
run: uv run mypy src/
|
|
35
|
+
|
|
36
|
+
- name: Test with coverage
|
|
37
|
+
run: uv run pytest --cov=pay_kit --cov-report=term-missing --cov-fail-under=80
|
|
38
|
+
|
|
39
|
+
lint-only:
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v4
|
|
43
|
+
|
|
44
|
+
- name: Install uv
|
|
45
|
+
uses: astral-sh/setup-uv@v4
|
|
46
|
+
|
|
47
|
+
- name: Set up Python
|
|
48
|
+
run: uv python install 3.11
|
|
49
|
+
|
|
50
|
+
- name: Install dependencies
|
|
51
|
+
run: uv sync
|
|
52
|
+
|
|
53
|
+
- name: Check ruff
|
|
54
|
+
run: |
|
|
55
|
+
uv run ruff check .
|
|
56
|
+
uv run ruff format --check .
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: write
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Install uv
|
|
17
|
+
uses: astral-sh/setup-uv@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python
|
|
20
|
+
run: uv python install 3.11
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: uv sync
|
|
24
|
+
|
|
25
|
+
- name: Run tests
|
|
26
|
+
run: uv run pytest --cov=pay_kit --cov-fail-under=80
|
|
27
|
+
|
|
28
|
+
- name: Build package
|
|
29
|
+
run: uv build
|
|
30
|
+
|
|
31
|
+
- name: Upload artifacts
|
|
32
|
+
uses: actions/upload-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
|
|
37
|
+
publish-pypi:
|
|
38
|
+
needs: build
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
environment: pypi
|
|
41
|
+
steps:
|
|
42
|
+
- name: Set up Python
|
|
43
|
+
uses: actions/setup-python@v5
|
|
44
|
+
with:
|
|
45
|
+
python-version: "3.11"
|
|
46
|
+
|
|
47
|
+
- name: Install twine
|
|
48
|
+
run: pip install twine
|
|
49
|
+
|
|
50
|
+
- name: Download artifacts
|
|
51
|
+
uses: actions/download-artifact@v4
|
|
52
|
+
with:
|
|
53
|
+
name: dist
|
|
54
|
+
path: dist/
|
|
55
|
+
|
|
56
|
+
- name: Publish to PyPI
|
|
57
|
+
env:
|
|
58
|
+
TWINE_USERNAME: __token__
|
|
59
|
+
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
|
60
|
+
run: twine upload dist/*
|
|
61
|
+
|
|
62
|
+
github-release:
|
|
63
|
+
needs: build
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
steps:
|
|
66
|
+
- uses: actions/checkout@v4
|
|
67
|
+
|
|
68
|
+
- name: Download artifacts
|
|
69
|
+
uses: actions/download-artifact@v4
|
|
70
|
+
with:
|
|
71
|
+
name: dist
|
|
72
|
+
path: dist/
|
|
73
|
+
|
|
74
|
+
- name: Extract changelog
|
|
75
|
+
id: changelog
|
|
76
|
+
run: |
|
|
77
|
+
VERSION=${GITHUB_REF_NAME#v}
|
|
78
|
+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
79
|
+
awk "/^## ${VERSION}/{flag=1; next} /^## /{flag=0} flag" CHANGELOG.md > /tmp/release_notes.md
|
|
80
|
+
if [ ! -s /tmp/release_notes.md ]; then
|
|
81
|
+
echo "Release ${VERSION}" > /tmp/release_notes.md
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
- name: Create GitHub Release
|
|
85
|
+
uses: softprops/action-gh-release@v2
|
|
86
|
+
with:
|
|
87
|
+
name: v${{ steps.changelog.outputs.version }}
|
|
88
|
+
body_path: /tmp/release_notes.md
|
|
89
|
+
files: dist/*
|
|
90
|
+
generate_release_notes: false
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# 变更日志
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- **支付宝 V3 Provider** — 当面付扫码、电脑网站、手机网站、App 四种支付方式
|
|
8
|
+
- **微信支付扩展方式** — JSAPI(公众号/小程序)、H5(手机浏览器)、App 支付
|
|
9
|
+
- **转账能力** — 微信商家转账到零钱 + 支付宝单笔转账(WithdrawProvider)
|
|
10
|
+
- **余额查询** — 微信 + 支付宝余额查询(WalletProvider)
|
|
11
|
+
- **Yuan 金额类型** — 支付宝元单位支持,fen_to_yuan / yuan_to_fen 精确转换(Decimal)
|
|
12
|
+
- **共享密码学模块** — providers/crypto.py 提取 RSA 签名/验签共享函数
|
|
13
|
+
- **错误转换统一** — providers/errors.py 共享错误转换,provider_name 常量化
|
|
14
|
+
|
|
15
|
+
### 改进
|
|
16
|
+
|
|
17
|
+
- WithdrawRequest 添加 recipient(收款人)和 description(转账说明)字段
|
|
18
|
+
- PaymentResult 添加 credential 字段(支付凭证:二维码 URL、前端参数等)
|
|
19
|
+
- RefundRequest 添加 total_amount 字段(原订单总额)
|
|
20
|
+
- WechatPay 提取 _build_payment_payload 消除三个 create 方法重复
|
|
21
|
+
- Alipay 同样提取 _build_payment_payload
|
|
22
|
+
- raise_for_error 参数化 error_cls 支持不同异常类型
|
|
23
|
+
|
|
24
|
+
## 0.1.0
|
|
25
|
+
|
|
26
|
+
### 新增
|
|
27
|
+
|
|
28
|
+
- 项目初始化
|
|
29
|
+
- 搭建工程脚手架:pyproject.toml、src/ 布局、工具链配置
|
|
30
|
+
- 添加 httpx、pydantic、cryptography 核心依赖
|
|
31
|
+
- 配置 ruff、mypy、pytest 开发工具
|
|
32
|
+
- 核心抽象层:PayProvider / WithdrawProvider / WalletProvider ABC
|
|
33
|
+
- 异常体系:PayKitError 及 7 个子类
|
|
34
|
+
- 数据模型:BaseProviderConfig + 9 个请求/响应模型
|
|
35
|
+
- 类型定义:Fen NewType + 状态枚举
|
|
36
|
+
- BaseHttpClient:传输层错误转换
|
|
37
|
+
- 微信支付 V3 Native Provider(PayProvider)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pay-py-kit
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 统一支付基础设施 SDK,支持微信支付和支付宝
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: cryptography>=43.0
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: pydantic>=2.0
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# pay-kit
|
|
13
|
+
|
|
14
|
+
统一支付基础设施 SDK,支持微信支付和支付宝商户。一个 SDK 即可处理支付、转账、余额查询。
|
|
15
|
+
|
|
16
|
+
## 特性
|
|
17
|
+
|
|
18
|
+
- **双平台支持** — 微信支付 V3 + 支付宝 V3
|
|
19
|
+
- **多种支付方式** — 扫码、JSAPI、H5、App、电脑网站
|
|
20
|
+
- **统一接口** — PayProvider / WithdrawProvider / WalletProvider 三个 ABC
|
|
21
|
+
- **类型安全** — 全面类型注解,mypy strict 通过
|
|
22
|
+
- **纯异步** — 基于 httpx,async/await 原生支持
|
|
23
|
+
- **安全** — RSA-SHA256 签名/验签、AES-GCM 回调解密、SecretStr 敏感字段保护
|
|
24
|
+
|
|
25
|
+
## 安装
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install pay-kit
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 快速开始
|
|
32
|
+
|
|
33
|
+
### 微信支付 — Native 扫码支付
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from pay_kit.providers.wechat import WechatPay, WechatPayConfig
|
|
37
|
+
from pay_kit.models import PaymentRequest
|
|
38
|
+
from pay_kit.types import Fen
|
|
39
|
+
|
|
40
|
+
config = WechatPayConfig(
|
|
41
|
+
appid="wx_your_appid",
|
|
42
|
+
mch_id="your_mch_id",
|
|
43
|
+
private_key=open("apiclient_key.pem").read(), # 商户 API 私钥
|
|
44
|
+
cert_serial_no="YOUR_CERT_SERIAL_NO", # 商户证书序列号
|
|
45
|
+
wechat_public_key=open("wechat_public_key.pem").read(), # 微信支付公钥
|
|
46
|
+
wechat_public_key_id="YOUR_PUB_KEY_ID",
|
|
47
|
+
apiv3_key="your_apiv3_key",
|
|
48
|
+
notify_url="https://your-domain.com/callback/wechat",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
async with WechatPay(config) as pay:
|
|
52
|
+
# 创建支付订单
|
|
53
|
+
result = await pay.create_payment(
|
|
54
|
+
PaymentRequest(out_trade_no="ORDER_001", total_amount=Fen(100), subject="商品名称")
|
|
55
|
+
)
|
|
56
|
+
print(result.credential["code_url"]) # 用此 URL 生成二维码
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 支付宝 — 当面付扫码支付
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from pay_kit.providers.alipay import Alipay, AlipayConfig
|
|
63
|
+
from pay_kit.models import PaymentRequest
|
|
64
|
+
from pay_kit.types import Fen
|
|
65
|
+
|
|
66
|
+
config = AlipayConfig(
|
|
67
|
+
app_id="your_app_id",
|
|
68
|
+
private_key=open("app_private_key.pem").read(), # 应用私钥
|
|
69
|
+
alipay_public_key=open("alipay_public_key.pem").read(), # 支付宝公钥
|
|
70
|
+
notify_url="https://your-domain.com/callback/alipay",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async with Alipay(config) as pay:
|
|
74
|
+
result = await pay.create_payment(
|
|
75
|
+
PaymentRequest(out_trade_no="ORDER_001", total_amount=Fen(100), subject="商品名称")
|
|
76
|
+
)
|
|
77
|
+
print(result.credential["qr_code"]) # 二维码 URL
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 支付方式
|
|
81
|
+
|
|
82
|
+
### 微信支付
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
async with WechatPay(config) as pay:
|
|
86
|
+
# Native 扫码支付(通用接口)
|
|
87
|
+
result = await pay.create_payment(request)
|
|
88
|
+
# credential: {"code_url": "weixin://wxpay/..."}
|
|
89
|
+
|
|
90
|
+
# JSAPI 公众号/小程序支付
|
|
91
|
+
result = await pay.create_jsapi_payment(request, openid="user_openid")
|
|
92
|
+
# credential: {"appId", "timeStamp", "nonceStr", "package", "signType", "paySign"}
|
|
93
|
+
|
|
94
|
+
# H5 手机浏览器支付
|
|
95
|
+
result = await pay.create_h5_payment(request, payer_client_ip="1.2.3.4")
|
|
96
|
+
# credential: {"h5_url": "https://wx.tenpay.com/..."}
|
|
97
|
+
|
|
98
|
+
# App 支付
|
|
99
|
+
result = await pay.create_app_payment(request)
|
|
100
|
+
# credential: {"appId", "partnerId", "prepayId", "package", "nonceStr", "timeStamp", "sign"}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 支付宝
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
async with Alipay(config) as pay:
|
|
107
|
+
# 当面付扫码支付(通用接口)
|
|
108
|
+
result = await pay.create_payment(request)
|
|
109
|
+
# credential: {"qr_code": "https://qr.alipay.com/..."}
|
|
110
|
+
|
|
111
|
+
# 电脑网站支付
|
|
112
|
+
result = await pay.create_page_payment(request, return_url="https://your-site.com/return")
|
|
113
|
+
# credential: {"pay_url": "https://openapi.alipay.com/..."}
|
|
114
|
+
|
|
115
|
+
# 手机网站支付
|
|
116
|
+
result = await pay.create_wap_payment(request, return_url="https://your-site.com/return")
|
|
117
|
+
# credential: {"pay_url": "https://openapi.alipay.com/..."}
|
|
118
|
+
|
|
119
|
+
# App 支付
|
|
120
|
+
result = await pay.create_app_payment(request)
|
|
121
|
+
# credential: {"order_str": "alipay_sdk=..."}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 通用操作
|
|
125
|
+
|
|
126
|
+
查询、关单、退款等操作两个平台用法一致:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# 查询订单
|
|
130
|
+
result = await pay.query_payment("ORDER_001")
|
|
131
|
+
print(result.status) # PaymentStatus.PAID
|
|
132
|
+
|
|
133
|
+
# 关闭订单
|
|
134
|
+
await pay.close_payment("ORDER_001")
|
|
135
|
+
|
|
136
|
+
# 发起退款
|
|
137
|
+
from pay_kit.models import RefundRequest
|
|
138
|
+
refund = await pay.refund(
|
|
139
|
+
RefundRequest(
|
|
140
|
+
out_trade_no="ORDER_001",
|
|
141
|
+
out_refund_no="REFUND_001",
|
|
142
|
+
total_amount=Fen(100),
|
|
143
|
+
refund_amount=Fen(50),
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# 查询退款
|
|
148
|
+
refund = await pay.query_refund("REFUND_001")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## 转账
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from pay_kit.models import WithdrawRequest
|
|
155
|
+
|
|
156
|
+
async with WechatPay(config) as pay:
|
|
157
|
+
# 商家转账到零钱
|
|
158
|
+
result = await pay.create_withdrawal(
|
|
159
|
+
WithdrawRequest(
|
|
160
|
+
out_withdrawal_no="TRANSFER_001",
|
|
161
|
+
amount=Fen(1000),
|
|
162
|
+
recipient="user_openid", # 微信: openid / 支付宝: user_id
|
|
163
|
+
description="佣金提现",
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
print(result.status) # WithdrawStatus.PENDING
|
|
167
|
+
|
|
168
|
+
# 查询转账状态
|
|
169
|
+
result = await pay.query_withdrawal("TRANSFER_001")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## 余额查询
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
async with WechatPay(config) as pay:
|
|
176
|
+
balance = await pay.query_balance()
|
|
177
|
+
print(f"可用余额: {balance.available} 分")
|
|
178
|
+
print(f"待结算: {balance.pending} 分")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 回调验签
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
# FastAPI 示例
|
|
185
|
+
from fastapi import Request
|
|
186
|
+
|
|
187
|
+
@app.post("/callback/wechat")
|
|
188
|
+
async def wechat_callback(request: Request):
|
|
189
|
+
headers = dict(request.headers)
|
|
190
|
+
body = await request.body()
|
|
191
|
+
|
|
192
|
+
async with WechatPay(config) as pay:
|
|
193
|
+
event = await pay.verify_callback(headers, body)
|
|
194
|
+
|
|
195
|
+
print(event.event_type) # "TRANSACTION.SUCCESS"
|
|
196
|
+
print(event.out_trade_no) # "ORDER_001"
|
|
197
|
+
print(event.trade_no) # 微信交易号
|
|
198
|
+
print(event.raw) # 完整回调数据
|
|
199
|
+
|
|
200
|
+
return {"code": "SUCCESS"}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## 异常处理
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
from pay_kit.exceptions import (
|
|
207
|
+
PayKitError, # 所有异常基类
|
|
208
|
+
ConfigError, # 配置错误
|
|
209
|
+
NetworkError, # 网络超时/连接失败
|
|
210
|
+
SignatureError, # 签名验证失败
|
|
211
|
+
PaymentError, # 支付业务错误
|
|
212
|
+
WithdrawError, # 转账业务错误
|
|
213
|
+
WalletError, # 余额查询错误
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
result = await pay.create_payment(request)
|
|
218
|
+
except NetworkError:
|
|
219
|
+
# 网络问题,可重试
|
|
220
|
+
pass
|
|
221
|
+
except SignatureError:
|
|
222
|
+
# 签名验证失败,响应不可信
|
|
223
|
+
pass
|
|
224
|
+
except PaymentError as e:
|
|
225
|
+
# 业务错误
|
|
226
|
+
print(e.error_code) # 支付商错误码,如 "ORDER_CLOSED"
|
|
227
|
+
print(e.provider_name) # "wechat" 或 "alipay"
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## 金额类型
|
|
231
|
+
|
|
232
|
+
pay-kit 使用 `Fen`(分)作为金额单位,避免浮点精度问题:
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from pay_kit.types import Fen, Yuan, fen_to_yuan, yuan_to_fen
|
|
236
|
+
|
|
237
|
+
amount = Fen(100) # 100 分 = 1 元
|
|
238
|
+
yuan = fen_to_yuan(Fen(100)) # Yuan("1.00")
|
|
239
|
+
fen = yuan_to_fen(Yuan("1.00")) # Fen(100)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## 开发
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
git clone https://github.com/Xinzz995/pay-kit.git
|
|
246
|
+
cd pay-kit
|
|
247
|
+
uv sync
|
|
248
|
+
|
|
249
|
+
uv run pytest # 运行测试
|
|
250
|
+
uv run pytest --cov=pay_kit # 覆盖率
|
|
251
|
+
uv run ruff check . # Lint
|
|
252
|
+
uv run mypy src/ # 类型检查
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## 许可证
|
|
256
|
+
|
|
257
|
+
MIT
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# pay-kit
|
|
2
|
+
|
|
3
|
+
统一支付基础设施 SDK,支持微信支付和支付宝商户。一个 SDK 即可处理支付、转账、余额查询。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **双平台支持** — 微信支付 V3 + 支付宝 V3
|
|
8
|
+
- **多种支付方式** — 扫码、JSAPI、H5、App、电脑网站
|
|
9
|
+
- **统一接口** — PayProvider / WithdrawProvider / WalletProvider 三个 ABC
|
|
10
|
+
- **类型安全** — 全面类型注解,mypy strict 通过
|
|
11
|
+
- **纯异步** — 基于 httpx,async/await 原生支持
|
|
12
|
+
- **安全** — RSA-SHA256 签名/验签、AES-GCM 回调解密、SecretStr 敏感字段保护
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install pay-kit
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## 快速开始
|
|
21
|
+
|
|
22
|
+
### 微信支付 — Native 扫码支付
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from pay_kit.providers.wechat import WechatPay, WechatPayConfig
|
|
26
|
+
from pay_kit.models import PaymentRequest
|
|
27
|
+
from pay_kit.types import Fen
|
|
28
|
+
|
|
29
|
+
config = WechatPayConfig(
|
|
30
|
+
appid="wx_your_appid",
|
|
31
|
+
mch_id="your_mch_id",
|
|
32
|
+
private_key=open("apiclient_key.pem").read(), # 商户 API 私钥
|
|
33
|
+
cert_serial_no="YOUR_CERT_SERIAL_NO", # 商户证书序列号
|
|
34
|
+
wechat_public_key=open("wechat_public_key.pem").read(), # 微信支付公钥
|
|
35
|
+
wechat_public_key_id="YOUR_PUB_KEY_ID",
|
|
36
|
+
apiv3_key="your_apiv3_key",
|
|
37
|
+
notify_url="https://your-domain.com/callback/wechat",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async with WechatPay(config) as pay:
|
|
41
|
+
# 创建支付订单
|
|
42
|
+
result = await pay.create_payment(
|
|
43
|
+
PaymentRequest(out_trade_no="ORDER_001", total_amount=Fen(100), subject="商品名称")
|
|
44
|
+
)
|
|
45
|
+
print(result.credential["code_url"]) # 用此 URL 生成二维码
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 支付宝 — 当面付扫码支付
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from pay_kit.providers.alipay import Alipay, AlipayConfig
|
|
52
|
+
from pay_kit.models import PaymentRequest
|
|
53
|
+
from pay_kit.types import Fen
|
|
54
|
+
|
|
55
|
+
config = AlipayConfig(
|
|
56
|
+
app_id="your_app_id",
|
|
57
|
+
private_key=open("app_private_key.pem").read(), # 应用私钥
|
|
58
|
+
alipay_public_key=open("alipay_public_key.pem").read(), # 支付宝公钥
|
|
59
|
+
notify_url="https://your-domain.com/callback/alipay",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
async with Alipay(config) as pay:
|
|
63
|
+
result = await pay.create_payment(
|
|
64
|
+
PaymentRequest(out_trade_no="ORDER_001", total_amount=Fen(100), subject="商品名称")
|
|
65
|
+
)
|
|
66
|
+
print(result.credential["qr_code"]) # 二维码 URL
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 支付方式
|
|
70
|
+
|
|
71
|
+
### 微信支付
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
async with WechatPay(config) as pay:
|
|
75
|
+
# Native 扫码支付(通用接口)
|
|
76
|
+
result = await pay.create_payment(request)
|
|
77
|
+
# credential: {"code_url": "weixin://wxpay/..."}
|
|
78
|
+
|
|
79
|
+
# JSAPI 公众号/小程序支付
|
|
80
|
+
result = await pay.create_jsapi_payment(request, openid="user_openid")
|
|
81
|
+
# credential: {"appId", "timeStamp", "nonceStr", "package", "signType", "paySign"}
|
|
82
|
+
|
|
83
|
+
# H5 手机浏览器支付
|
|
84
|
+
result = await pay.create_h5_payment(request, payer_client_ip="1.2.3.4")
|
|
85
|
+
# credential: {"h5_url": "https://wx.tenpay.com/..."}
|
|
86
|
+
|
|
87
|
+
# App 支付
|
|
88
|
+
result = await pay.create_app_payment(request)
|
|
89
|
+
# credential: {"appId", "partnerId", "prepayId", "package", "nonceStr", "timeStamp", "sign"}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 支付宝
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
async with Alipay(config) as pay:
|
|
96
|
+
# 当面付扫码支付(通用接口)
|
|
97
|
+
result = await pay.create_payment(request)
|
|
98
|
+
# credential: {"qr_code": "https://qr.alipay.com/..."}
|
|
99
|
+
|
|
100
|
+
# 电脑网站支付
|
|
101
|
+
result = await pay.create_page_payment(request, return_url="https://your-site.com/return")
|
|
102
|
+
# credential: {"pay_url": "https://openapi.alipay.com/..."}
|
|
103
|
+
|
|
104
|
+
# 手机网站支付
|
|
105
|
+
result = await pay.create_wap_payment(request, return_url="https://your-site.com/return")
|
|
106
|
+
# credential: {"pay_url": "https://openapi.alipay.com/..."}
|
|
107
|
+
|
|
108
|
+
# App 支付
|
|
109
|
+
result = await pay.create_app_payment(request)
|
|
110
|
+
# credential: {"order_str": "alipay_sdk=..."}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 通用操作
|
|
114
|
+
|
|
115
|
+
查询、关单、退款等操作两个平台用法一致:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
# 查询订单
|
|
119
|
+
result = await pay.query_payment("ORDER_001")
|
|
120
|
+
print(result.status) # PaymentStatus.PAID
|
|
121
|
+
|
|
122
|
+
# 关闭订单
|
|
123
|
+
await pay.close_payment("ORDER_001")
|
|
124
|
+
|
|
125
|
+
# 发起退款
|
|
126
|
+
from pay_kit.models import RefundRequest
|
|
127
|
+
refund = await pay.refund(
|
|
128
|
+
RefundRequest(
|
|
129
|
+
out_trade_no="ORDER_001",
|
|
130
|
+
out_refund_no="REFUND_001",
|
|
131
|
+
total_amount=Fen(100),
|
|
132
|
+
refund_amount=Fen(50),
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# 查询退款
|
|
137
|
+
refund = await pay.query_refund("REFUND_001")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## 转账
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from pay_kit.models import WithdrawRequest
|
|
144
|
+
|
|
145
|
+
async with WechatPay(config) as pay:
|
|
146
|
+
# 商家转账到零钱
|
|
147
|
+
result = await pay.create_withdrawal(
|
|
148
|
+
WithdrawRequest(
|
|
149
|
+
out_withdrawal_no="TRANSFER_001",
|
|
150
|
+
amount=Fen(1000),
|
|
151
|
+
recipient="user_openid", # 微信: openid / 支付宝: user_id
|
|
152
|
+
description="佣金提现",
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
print(result.status) # WithdrawStatus.PENDING
|
|
156
|
+
|
|
157
|
+
# 查询转账状态
|
|
158
|
+
result = await pay.query_withdrawal("TRANSFER_001")
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## 余额查询
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
async with WechatPay(config) as pay:
|
|
165
|
+
balance = await pay.query_balance()
|
|
166
|
+
print(f"可用余额: {balance.available} 分")
|
|
167
|
+
print(f"待结算: {balance.pending} 分")
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## 回调验签
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
# FastAPI 示例
|
|
174
|
+
from fastapi import Request
|
|
175
|
+
|
|
176
|
+
@app.post("/callback/wechat")
|
|
177
|
+
async def wechat_callback(request: Request):
|
|
178
|
+
headers = dict(request.headers)
|
|
179
|
+
body = await request.body()
|
|
180
|
+
|
|
181
|
+
async with WechatPay(config) as pay:
|
|
182
|
+
event = await pay.verify_callback(headers, body)
|
|
183
|
+
|
|
184
|
+
print(event.event_type) # "TRANSACTION.SUCCESS"
|
|
185
|
+
print(event.out_trade_no) # "ORDER_001"
|
|
186
|
+
print(event.trade_no) # 微信交易号
|
|
187
|
+
print(event.raw) # 完整回调数据
|
|
188
|
+
|
|
189
|
+
return {"code": "SUCCESS"}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 异常处理
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from pay_kit.exceptions import (
|
|
196
|
+
PayKitError, # 所有异常基类
|
|
197
|
+
ConfigError, # 配置错误
|
|
198
|
+
NetworkError, # 网络超时/连接失败
|
|
199
|
+
SignatureError, # 签名验证失败
|
|
200
|
+
PaymentError, # 支付业务错误
|
|
201
|
+
WithdrawError, # 转账业务错误
|
|
202
|
+
WalletError, # 余额查询错误
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
result = await pay.create_payment(request)
|
|
207
|
+
except NetworkError:
|
|
208
|
+
# 网络问题,可重试
|
|
209
|
+
pass
|
|
210
|
+
except SignatureError:
|
|
211
|
+
# 签名验证失败,响应不可信
|
|
212
|
+
pass
|
|
213
|
+
except PaymentError as e:
|
|
214
|
+
# 业务错误
|
|
215
|
+
print(e.error_code) # 支付商错误码,如 "ORDER_CLOSED"
|
|
216
|
+
print(e.provider_name) # "wechat" 或 "alipay"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## 金额类型
|
|
220
|
+
|
|
221
|
+
pay-kit 使用 `Fen`(分)作为金额单位,避免浮点精度问题:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from pay_kit.types import Fen, Yuan, fen_to_yuan, yuan_to_fen
|
|
225
|
+
|
|
226
|
+
amount = Fen(100) # 100 分 = 1 元
|
|
227
|
+
yuan = fen_to_yuan(Fen(100)) # Yuan("1.00")
|
|
228
|
+
fen = yuan_to_fen(Yuan("1.00")) # Fen(100)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## 开发
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
git clone https://github.com/Xinzz995/pay-kit.git
|
|
235
|
+
cd pay-kit
|
|
236
|
+
uv sync
|
|
237
|
+
|
|
238
|
+
uv run pytest # 运行测试
|
|
239
|
+
uv run pytest --cov=pay_kit # 覆盖率
|
|
240
|
+
uv run ruff check . # Lint
|
|
241
|
+
uv run mypy src/ # 类型检查
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## 许可证
|
|
245
|
+
|
|
246
|
+
MIT
|