epay-sdk 0.4.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.
- epay_sdk-0.4.0/LICENSE +21 -0
- epay_sdk-0.4.0/PKG-INFO +288 -0
- epay_sdk-0.4.0/README.md +250 -0
- epay_sdk-0.4.0/pyproject.toml +44 -0
- epay_sdk-0.4.0/setup.cfg +4 -0
- epay_sdk-0.4.0/src/epay_sdk/__init__.py +107 -0
- epay_sdk-0.4.0/src/epay_sdk/callbacks.py +203 -0
- epay_sdk-0.4.0/src/epay_sdk/client.py +258 -0
- epay_sdk-0.4.0/src/epay_sdk/config.py +70 -0
- epay_sdk-0.4.0/src/epay_sdk/exceptions.py +22 -0
- epay_sdk-0.4.0/src/epay_sdk/logging.py +10 -0
- epay_sdk-0.4.0/src/epay_sdk/models.py +122 -0
- epay_sdk-0.4.0/src/epay_sdk/service.py +107 -0
- epay_sdk-0.4.0/src/epay_sdk/sqlalchemy_repo.py +119 -0
- epay_sdk-0.4.0/src/epay_sdk/utils.py +25 -0
- epay_sdk-0.4.0/src/epay_sdk/version.py +1 -0
- epay_sdk-0.4.0/src/epay_sdk.egg-info/PKG-INFO +288 -0
- epay_sdk-0.4.0/src/epay_sdk.egg-info/SOURCES.txt +29 -0
- epay_sdk-0.4.0/src/epay_sdk.egg-info/dependency_links.txt +1 -0
- epay_sdk-0.4.0/src/epay_sdk.egg-info/requires.txt +22 -0
- epay_sdk-0.4.0/src/epay_sdk.egg-info/top_level.txt +1 -0
- epay_sdk-0.4.0/tests/test_amount.py +6 -0
- epay_sdk-0.4.0/tests/test_callback_processor.py +184 -0
- epay_sdk-0.4.0/tests/test_callback_validation.py +75 -0
- epay_sdk-0.4.0/tests/test_client_params.py +104 -0
- epay_sdk-0.4.0/tests/test_config.py +39 -0
- epay_sdk-0.4.0/tests/test_imports.py +63 -0
- epay_sdk-0.4.0/tests/test_query_parse.py +39 -0
- epay_sdk-0.4.0/tests/test_service.py +61 -0
- epay_sdk-0.4.0/tests/test_sign.py +29 -0
- epay_sdk-0.4.0/tests/test_sqlalchemy_repo.py +208 -0
epay_sdk-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
epay_sdk-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: epay-sdk
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: A production-oriented Python SDK for common EPay-style payment gateways
|
|
5
|
+
Author: OpenAI
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: epay,payment,sdk,gateway
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: requests>=2.31.0
|
|
21
|
+
Provides-Extra: sqlalchemy
|
|
22
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == "sqlalchemy"
|
|
23
|
+
Provides-Extra: flask
|
|
24
|
+
Requires-Dist: flask>=3.0.0; extra == "flask"
|
|
25
|
+
Provides-Extra: fastapi
|
|
26
|
+
Requires-Dist: fastapi>=0.110.0; extra == "fastapi"
|
|
27
|
+
Requires-Dist: uvicorn>=0.29.0; extra == "fastapi"
|
|
28
|
+
Provides-Extra: django
|
|
29
|
+
Requires-Dist: django>=4.2; extra == "django"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: build>=1.2.1; extra == "dev"
|
|
33
|
+
Requires-Dist: twine>=5.0.0; extra == "dev"
|
|
34
|
+
Requires-Dist: ruff>=0.6.0; extra == "dev"
|
|
35
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: django>=4.2; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# epay-sdk
|
|
40
|
+
|
|
41
|
+
一个面向 Python 的易支付 SDK,重点放在“可上线的支付链路”而不只是拼出下单参数。
|
|
42
|
+
|
|
43
|
+
当前已提供:
|
|
44
|
+
|
|
45
|
+
- MD5 签名与验签
|
|
46
|
+
- 创建订单 URL / 原始下单请求
|
|
47
|
+
- 查询订单与结果解析
|
|
48
|
+
- 回调验签
|
|
49
|
+
- 回调后二次查单确认
|
|
50
|
+
- 原子幂等仓储协议
|
|
51
|
+
- 默认 SQLAlchemy 仓储(可选安装)
|
|
52
|
+
- Decimal 金额标准化
|
|
53
|
+
- 作为库时不主动接管业务日志输出
|
|
54
|
+
|
|
55
|
+
## 安装
|
|
56
|
+
|
|
57
|
+
核心功能:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install epay-sdk
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
如需默认 SQLAlchemy 仓储与 ORM 模型:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install epay-sdk[sqlalchemy]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
如需使用 Django 示例中的集成模板:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install epay-sdk[django]
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
本地开发:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install -e .[dev]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 快速开始
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from epay_sdk import EPayClient, EPayConfig, PayType, PaymentService
|
|
85
|
+
|
|
86
|
+
client = EPayClient(
|
|
87
|
+
EPayConfig(
|
|
88
|
+
pid="1001",
|
|
89
|
+
key="your_secret_key",
|
|
90
|
+
base_url="https://your-epay.com",
|
|
91
|
+
environment="production",
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
service = PaymentService(client)
|
|
95
|
+
|
|
96
|
+
pay_url = service.create_order(
|
|
97
|
+
pay_type=PayType.ALIPAY,
|
|
98
|
+
order_no="ORDER_10001",
|
|
99
|
+
amount="9.90",
|
|
100
|
+
subject="会员充值",
|
|
101
|
+
notify_url="https://api.example.com/pay/notify",
|
|
102
|
+
return_url="https://www.example.com/pay/success",
|
|
103
|
+
)
|
|
104
|
+
print(pay_url)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`PaymentService` 还保留了便捷方法:
|
|
108
|
+
|
|
109
|
+
- `create_alipay_order(...)`
|
|
110
|
+
- `create_wxpay_order(...)`
|
|
111
|
+
- `create_qqpay_order(...)`
|
|
112
|
+
|
|
113
|
+
## `create_order`、`create_order_url`、`create_order`(client) 的区别
|
|
114
|
+
|
|
115
|
+
- `PaymentService.create_order(...)`:面向业务层的默认入口。负责金额标准化,并返回可直接跳转/展示给前端的支付 URL。
|
|
116
|
+
- `EPayClient.create_order_url(CreateOrderRequest)`:更底层;当你已经自己构造了 `CreateOrderRequest` 时使用。
|
|
117
|
+
- `EPayClient.create_order(CreateOrderRequest)`:直接向网关发起请求并返回原始响应文本,只在你确实需要网关原始返回时使用。
|
|
118
|
+
|
|
119
|
+
如果你的场景只是“生成付款链接”,优先用 `PaymentService.create_order(...)` 或相应便捷方法。
|
|
120
|
+
|
|
121
|
+
## 推荐回调流程
|
|
122
|
+
|
|
123
|
+
推荐把异步通知处理为如下链路:
|
|
124
|
+
|
|
125
|
+
1. 收到网关回调表单
|
|
126
|
+
2. `client.verify_callback()` 验签,并校验 `pid`、`sign_type`、必要字段、金额格式
|
|
127
|
+
3. `CallbackProcessor.process()` 加载本地订单并校验金额
|
|
128
|
+
4. 如启用 `verify_with_query=True`,自动执行 `query_order()` + `parse_query_result()` 做二次确认
|
|
129
|
+
5. 通过 `mark_paid_if_unpaid()` 做原子状态更新
|
|
130
|
+
6. 通过 `record_callback_attempt(..., stage=...)` 记录审计阶段
|
|
131
|
+
7. 由调用方决定是否提交/回滚当前事务
|
|
132
|
+
8. 首次成功返回 `success`;失败返回 `fail` 以便平台重试
|
|
133
|
+
|
|
134
|
+
回调审计阶段目前包括:
|
|
135
|
+
|
|
136
|
+
- `RECEIVED`
|
|
137
|
+
- `RECONCILED`
|
|
138
|
+
- `APPLIED`
|
|
139
|
+
- `REJECTED`
|
|
140
|
+
|
|
141
|
+
## 仓储接口约定
|
|
142
|
+
|
|
143
|
+
你需要自己实现订单仓储,但推荐遵守下面的协议:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
class OrderRepository(Protocol):
|
|
147
|
+
def get_order(self, order_no: str) -> Optional[OrderRecord]: ...
|
|
148
|
+
def mark_paid_if_unpaid(self, order_no: str, gateway_trade_no: str, raw_payload: str) -> bool: ...
|
|
149
|
+
def record_callback_attempt(
|
|
150
|
+
self,
|
|
151
|
+
order_no: str,
|
|
152
|
+
accepted: bool,
|
|
153
|
+
reason: Optional[str],
|
|
154
|
+
raw_payload: str,
|
|
155
|
+
*,
|
|
156
|
+
stage: str,
|
|
157
|
+
) -> None: ...
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
其中:
|
|
161
|
+
|
|
162
|
+
- `mark_paid_if_unpaid()` 必须保证原子性,通常用数据库条件更新实现
|
|
163
|
+
- `record_callback_attempt()` 的 `stage` 是关键字段,调用方应完整记录
|
|
164
|
+
- 如果你的仓储具备事务能力,可额外提供 `commit()` / `rollback()`
|
|
165
|
+
- `CallbackProcessor.process()` 默认**不**提交或回滚外部事务;这样可以避免意外提交同一事务里的其它 ORM 改动
|
|
166
|
+
- 如果你明确希望沿用“由回调处理器负责结束事务”的模式,可显式传入 `CallbackProcessor(..., manage_transaction=True)`,此时它才会在结束时调用仓储的 `commit()` / `rollback()`
|
|
167
|
+
|
|
168
|
+
## SQLAlchemy 默认实现
|
|
169
|
+
|
|
170
|
+
安装 `epay-sdk[sqlalchemy]` 后,顶层模块会按需暴露:
|
|
171
|
+
|
|
172
|
+
- `Base`
|
|
173
|
+
- `PaymentOrderModel`
|
|
174
|
+
- `CallbackAuditModel`
|
|
175
|
+
- `SQLAlchemyOrderRepository`
|
|
176
|
+
- `create_sqlite_engine`
|
|
177
|
+
|
|
178
|
+
示例:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from sqlalchemy.orm import Session
|
|
182
|
+
from epay_sdk import Base, PaymentOrderModel, SQLAlchemyOrderRepository, create_sqlite_engine
|
|
183
|
+
|
|
184
|
+
engine = create_sqlite_engine("sqlite:///./epay.db")
|
|
185
|
+
Base.metadata.create_all(engine)
|
|
186
|
+
|
|
187
|
+
with Session(engine) as session:
|
|
188
|
+
session.add(PaymentOrderModel(order_no="A001", amount="9.90", status="UNPAID"))
|
|
189
|
+
session.commit()
|
|
190
|
+
|
|
191
|
+
repo = SQLAlchemyOrderRepository(session)
|
|
192
|
+
if repo.mark_paid_if_unpaid("A001", "G100", '{"trade_no":"G100"}'):
|
|
193
|
+
repo.record_callback_attempt("A001", True, None, '{"trade_no":"G100"}', stage="APPLIED")
|
|
194
|
+
repo.commit()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
如果未安装 SQLAlchemy extra,`import epay_sdk` 仍可正常工作;只有在访问上述 SQLAlchemy 符号时才会提示安装可选依赖。
|
|
198
|
+
|
|
199
|
+
## Django 集成模板
|
|
200
|
+
|
|
201
|
+
Django 集成采取的策略是:
|
|
202
|
+
|
|
203
|
+
- `src/epay_sdk/` 只保留框架无关的核心能力
|
|
204
|
+
- Django ORM、Django view、Django URL 与事务边界放在你的应用层实现
|
|
205
|
+
- 仓库内提供 `examples/django_app/` 作为可直接参考/复制的 Django 模板
|
|
206
|
+
|
|
207
|
+
也就是说,Django 支持**不会**耦合进核心 SDK 包本身;SDK 只要求你在 Django 项目中实现符合协议的仓储与回调接入层。
|
|
208
|
+
|
|
209
|
+
如果你希望直接运行示例,可安装:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
pip install epay-sdk[django]
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
示例入口见:`examples/django_app/`
|
|
216
|
+
|
|
217
|
+
### Django 中的事务建议
|
|
218
|
+
|
|
219
|
+
Django 场景下,推荐把回调处理包在 `transaction.atomic()` 中,并继续使用 `CallbackProcessor` 的默认行为:
|
|
220
|
+
|
|
221
|
+
- `CallbackProcessor(..., manage_transaction=False)`(默认值)
|
|
222
|
+
- 由 Django 的 `transaction.atomic()` 负责提交/回滚
|
|
223
|
+
- Django 仓储本身不需要暴露 `commit()` / `rollback()`
|
|
224
|
+
|
|
225
|
+
示意:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from django.db import transaction
|
|
229
|
+
|
|
230
|
+
with transaction.atomic():
|
|
231
|
+
callback = client.verify_callback(request.POST.dict())
|
|
232
|
+
repo = DjangoOrderRepository()
|
|
233
|
+
processor = CallbackProcessor(client, repo, verify_with_query=True)
|
|
234
|
+
result = processor.process(callback)
|
|
235
|
+
return HttpResponse(result.response_text, content_type="text/plain")
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
这样可以保持和核心 SDK 一致的事务边界:回调处理器负责业务编排,是否提交整个事务由 Django 应用层决定。
|
|
239
|
+
|
|
240
|
+
### Django 仓储如何映射协议
|
|
241
|
+
|
|
242
|
+
无论使用 Django ORM 还是其他 ORM,仓储仍应满足同一个 `OrderRepository` 协议。放到 Django 里,通常对应为:
|
|
243
|
+
|
|
244
|
+
- `get_order(order_no)`:从 Django model 读取订单,并映射为 `OrderRecord`
|
|
245
|
+
- `mark_paid_if_unpaid(order_no, gateway_trade_no, raw_payload)`:使用**单条条件更新**
|
|
246
|
+
- `record_callback_attempt(...)`:写入回调审计表
|
|
247
|
+
|
|
248
|
+
其中最关键的是 `mark_paid_if_unpaid()` 的原子性。在 Django 中,推荐明确写成:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
updated = PaymentOrder.objects.filter(
|
|
252
|
+
order_no=order_no,
|
|
253
|
+
status="UNPAID",
|
|
254
|
+
).update(
|
|
255
|
+
status="PAID",
|
|
256
|
+
gateway_trade_no=gateway_trade_no,
|
|
257
|
+
raw_payload=raw_payload,
|
|
258
|
+
paid_at=timezone.now(),
|
|
259
|
+
)
|
|
260
|
+
first_success = updated == 1
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
这对应核心协议里“只允许 `UNPAID -> PAID`”的幂等要求,避免用“先查再改”的方式破坏并发安全语义。
|
|
264
|
+
|
|
265
|
+
### Django 示例包含什么
|
|
266
|
+
|
|
267
|
+
`examples/django_app/` 当前展示了:
|
|
268
|
+
|
|
269
|
+
- Django model 设计
|
|
270
|
+
- `DjangoOrderRepository` 的 duck typing 实现
|
|
271
|
+
- `PaymentService.create_order(...)` 的下单 view 用法
|
|
272
|
+
- `client.verify_callback(...) + CallbackProcessor.process(...)` 的回调处理方式
|
|
273
|
+
- 使用 `transaction.atomic()` 控制回调事务
|
|
274
|
+
- 示例 URL wiring 与 Django 测试
|
|
275
|
+
|
|
276
|
+
## 安全与运行注意事项
|
|
277
|
+
|
|
278
|
+
- **签名协议说明**:SDK 按易支付常见协议使用“排序后的非空参数 + 商户密钥”做 MD5。这里是为了兼容网关协议,不代表 MD5 适合独立承担现代传输安全责任。
|
|
279
|
+
- **生产环境请使用 HTTPS**:生产配置应使用 `https://...` 的 `base_url`,并保持 `verify_ssl=True`。SDK 已禁止在 `environment="production"` 时关闭 SSL 校验。
|
|
280
|
+
- **SQLite 仅适合开发/测试**:示例中的 SQLite 适合本地演示;生产支付回调更适合 PostgreSQL/MySQL 等具备更可靠并发/锁语义的数据库。
|
|
281
|
+
- **回调必须验签再处理**:不要直接信任回调参数;应始终先调用 `verify_callback()`。
|
|
282
|
+
- **`environment` 字段当前是安全/配置语义字段**:它目前只影响校验和告警(例如生产环境 TLS 约束),不会自动切换网关地址或协议行为;可视为保留给部署语义和未来扩展的字段。
|
|
283
|
+
|
|
284
|
+
## 示例
|
|
285
|
+
|
|
286
|
+
- `examples/flask_app.py`:最小 Flask 集成,使用内存仓储演示回调协议
|
|
287
|
+
- `examples/fastapi_app.py`:FastAPI + SQLAlchemy 示例,并显式把同步 SDK/数据库操作放入线程池,避免误导为真正的异步 I/O
|
|
288
|
+
- `examples/django_app/`:Django 集成模板,演示“核心 SDK 保持通用 + Django 仓储/视图/事务放在应用层”
|
epay_sdk-0.4.0/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# epay-sdk
|
|
2
|
+
|
|
3
|
+
一个面向 Python 的易支付 SDK,重点放在“可上线的支付链路”而不只是拼出下单参数。
|
|
4
|
+
|
|
5
|
+
当前已提供:
|
|
6
|
+
|
|
7
|
+
- MD5 签名与验签
|
|
8
|
+
- 创建订单 URL / 原始下单请求
|
|
9
|
+
- 查询订单与结果解析
|
|
10
|
+
- 回调验签
|
|
11
|
+
- 回调后二次查单确认
|
|
12
|
+
- 原子幂等仓储协议
|
|
13
|
+
- 默认 SQLAlchemy 仓储(可选安装)
|
|
14
|
+
- Decimal 金额标准化
|
|
15
|
+
- 作为库时不主动接管业务日志输出
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
核心功能:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install epay-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
如需默认 SQLAlchemy 仓储与 ORM 模型:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install epay-sdk[sqlalchemy]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
如需使用 Django 示例中的集成模板:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install epay-sdk[django]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
本地开发:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install -e .[dev]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 快速开始
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from epay_sdk import EPayClient, EPayConfig, PayType, PaymentService
|
|
47
|
+
|
|
48
|
+
client = EPayClient(
|
|
49
|
+
EPayConfig(
|
|
50
|
+
pid="1001",
|
|
51
|
+
key="your_secret_key",
|
|
52
|
+
base_url="https://your-epay.com",
|
|
53
|
+
environment="production",
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
service = PaymentService(client)
|
|
57
|
+
|
|
58
|
+
pay_url = service.create_order(
|
|
59
|
+
pay_type=PayType.ALIPAY,
|
|
60
|
+
order_no="ORDER_10001",
|
|
61
|
+
amount="9.90",
|
|
62
|
+
subject="会员充值",
|
|
63
|
+
notify_url="https://api.example.com/pay/notify",
|
|
64
|
+
return_url="https://www.example.com/pay/success",
|
|
65
|
+
)
|
|
66
|
+
print(pay_url)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`PaymentService` 还保留了便捷方法:
|
|
70
|
+
|
|
71
|
+
- `create_alipay_order(...)`
|
|
72
|
+
- `create_wxpay_order(...)`
|
|
73
|
+
- `create_qqpay_order(...)`
|
|
74
|
+
|
|
75
|
+
## `create_order`、`create_order_url`、`create_order`(client) 的区别
|
|
76
|
+
|
|
77
|
+
- `PaymentService.create_order(...)`:面向业务层的默认入口。负责金额标准化,并返回可直接跳转/展示给前端的支付 URL。
|
|
78
|
+
- `EPayClient.create_order_url(CreateOrderRequest)`:更底层;当你已经自己构造了 `CreateOrderRequest` 时使用。
|
|
79
|
+
- `EPayClient.create_order(CreateOrderRequest)`:直接向网关发起请求并返回原始响应文本,只在你确实需要网关原始返回时使用。
|
|
80
|
+
|
|
81
|
+
如果你的场景只是“生成付款链接”,优先用 `PaymentService.create_order(...)` 或相应便捷方法。
|
|
82
|
+
|
|
83
|
+
## 推荐回调流程
|
|
84
|
+
|
|
85
|
+
推荐把异步通知处理为如下链路:
|
|
86
|
+
|
|
87
|
+
1. 收到网关回调表单
|
|
88
|
+
2. `client.verify_callback()` 验签,并校验 `pid`、`sign_type`、必要字段、金额格式
|
|
89
|
+
3. `CallbackProcessor.process()` 加载本地订单并校验金额
|
|
90
|
+
4. 如启用 `verify_with_query=True`,自动执行 `query_order()` + `parse_query_result()` 做二次确认
|
|
91
|
+
5. 通过 `mark_paid_if_unpaid()` 做原子状态更新
|
|
92
|
+
6. 通过 `record_callback_attempt(..., stage=...)` 记录审计阶段
|
|
93
|
+
7. 由调用方决定是否提交/回滚当前事务
|
|
94
|
+
8. 首次成功返回 `success`;失败返回 `fail` 以便平台重试
|
|
95
|
+
|
|
96
|
+
回调审计阶段目前包括:
|
|
97
|
+
|
|
98
|
+
- `RECEIVED`
|
|
99
|
+
- `RECONCILED`
|
|
100
|
+
- `APPLIED`
|
|
101
|
+
- `REJECTED`
|
|
102
|
+
|
|
103
|
+
## 仓储接口约定
|
|
104
|
+
|
|
105
|
+
你需要自己实现订单仓储,但推荐遵守下面的协议:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
class OrderRepository(Protocol):
|
|
109
|
+
def get_order(self, order_no: str) -> Optional[OrderRecord]: ...
|
|
110
|
+
def mark_paid_if_unpaid(self, order_no: str, gateway_trade_no: str, raw_payload: str) -> bool: ...
|
|
111
|
+
def record_callback_attempt(
|
|
112
|
+
self,
|
|
113
|
+
order_no: str,
|
|
114
|
+
accepted: bool,
|
|
115
|
+
reason: Optional[str],
|
|
116
|
+
raw_payload: str,
|
|
117
|
+
*,
|
|
118
|
+
stage: str,
|
|
119
|
+
) -> None: ...
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
其中:
|
|
123
|
+
|
|
124
|
+
- `mark_paid_if_unpaid()` 必须保证原子性,通常用数据库条件更新实现
|
|
125
|
+
- `record_callback_attempt()` 的 `stage` 是关键字段,调用方应完整记录
|
|
126
|
+
- 如果你的仓储具备事务能力,可额外提供 `commit()` / `rollback()`
|
|
127
|
+
- `CallbackProcessor.process()` 默认**不**提交或回滚外部事务;这样可以避免意外提交同一事务里的其它 ORM 改动
|
|
128
|
+
- 如果你明确希望沿用“由回调处理器负责结束事务”的模式,可显式传入 `CallbackProcessor(..., manage_transaction=True)`,此时它才会在结束时调用仓储的 `commit()` / `rollback()`
|
|
129
|
+
|
|
130
|
+
## SQLAlchemy 默认实现
|
|
131
|
+
|
|
132
|
+
安装 `epay-sdk[sqlalchemy]` 后,顶层模块会按需暴露:
|
|
133
|
+
|
|
134
|
+
- `Base`
|
|
135
|
+
- `PaymentOrderModel`
|
|
136
|
+
- `CallbackAuditModel`
|
|
137
|
+
- `SQLAlchemyOrderRepository`
|
|
138
|
+
- `create_sqlite_engine`
|
|
139
|
+
|
|
140
|
+
示例:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from sqlalchemy.orm import Session
|
|
144
|
+
from epay_sdk import Base, PaymentOrderModel, SQLAlchemyOrderRepository, create_sqlite_engine
|
|
145
|
+
|
|
146
|
+
engine = create_sqlite_engine("sqlite:///./epay.db")
|
|
147
|
+
Base.metadata.create_all(engine)
|
|
148
|
+
|
|
149
|
+
with Session(engine) as session:
|
|
150
|
+
session.add(PaymentOrderModel(order_no="A001", amount="9.90", status="UNPAID"))
|
|
151
|
+
session.commit()
|
|
152
|
+
|
|
153
|
+
repo = SQLAlchemyOrderRepository(session)
|
|
154
|
+
if repo.mark_paid_if_unpaid("A001", "G100", '{"trade_no":"G100"}'):
|
|
155
|
+
repo.record_callback_attempt("A001", True, None, '{"trade_no":"G100"}', stage="APPLIED")
|
|
156
|
+
repo.commit()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
如果未安装 SQLAlchemy extra,`import epay_sdk` 仍可正常工作;只有在访问上述 SQLAlchemy 符号时才会提示安装可选依赖。
|
|
160
|
+
|
|
161
|
+
## Django 集成模板
|
|
162
|
+
|
|
163
|
+
Django 集成采取的策略是:
|
|
164
|
+
|
|
165
|
+
- `src/epay_sdk/` 只保留框架无关的核心能力
|
|
166
|
+
- Django ORM、Django view、Django URL 与事务边界放在你的应用层实现
|
|
167
|
+
- 仓库内提供 `examples/django_app/` 作为可直接参考/复制的 Django 模板
|
|
168
|
+
|
|
169
|
+
也就是说,Django 支持**不会**耦合进核心 SDK 包本身;SDK 只要求你在 Django 项目中实现符合协议的仓储与回调接入层。
|
|
170
|
+
|
|
171
|
+
如果你希望直接运行示例,可安装:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
pip install epay-sdk[django]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
示例入口见:`examples/django_app/`
|
|
178
|
+
|
|
179
|
+
### Django 中的事务建议
|
|
180
|
+
|
|
181
|
+
Django 场景下,推荐把回调处理包在 `transaction.atomic()` 中,并继续使用 `CallbackProcessor` 的默认行为:
|
|
182
|
+
|
|
183
|
+
- `CallbackProcessor(..., manage_transaction=False)`(默认值)
|
|
184
|
+
- 由 Django 的 `transaction.atomic()` 负责提交/回滚
|
|
185
|
+
- Django 仓储本身不需要暴露 `commit()` / `rollback()`
|
|
186
|
+
|
|
187
|
+
示意:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from django.db import transaction
|
|
191
|
+
|
|
192
|
+
with transaction.atomic():
|
|
193
|
+
callback = client.verify_callback(request.POST.dict())
|
|
194
|
+
repo = DjangoOrderRepository()
|
|
195
|
+
processor = CallbackProcessor(client, repo, verify_with_query=True)
|
|
196
|
+
result = processor.process(callback)
|
|
197
|
+
return HttpResponse(result.response_text, content_type="text/plain")
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
这样可以保持和核心 SDK 一致的事务边界:回调处理器负责业务编排,是否提交整个事务由 Django 应用层决定。
|
|
201
|
+
|
|
202
|
+
### Django 仓储如何映射协议
|
|
203
|
+
|
|
204
|
+
无论使用 Django ORM 还是其他 ORM,仓储仍应满足同一个 `OrderRepository` 协议。放到 Django 里,通常对应为:
|
|
205
|
+
|
|
206
|
+
- `get_order(order_no)`:从 Django model 读取订单,并映射为 `OrderRecord`
|
|
207
|
+
- `mark_paid_if_unpaid(order_no, gateway_trade_no, raw_payload)`:使用**单条条件更新**
|
|
208
|
+
- `record_callback_attempt(...)`:写入回调审计表
|
|
209
|
+
|
|
210
|
+
其中最关键的是 `mark_paid_if_unpaid()` 的原子性。在 Django 中,推荐明确写成:
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
updated = PaymentOrder.objects.filter(
|
|
214
|
+
order_no=order_no,
|
|
215
|
+
status="UNPAID",
|
|
216
|
+
).update(
|
|
217
|
+
status="PAID",
|
|
218
|
+
gateway_trade_no=gateway_trade_no,
|
|
219
|
+
raw_payload=raw_payload,
|
|
220
|
+
paid_at=timezone.now(),
|
|
221
|
+
)
|
|
222
|
+
first_success = updated == 1
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
这对应核心协议里“只允许 `UNPAID -> PAID`”的幂等要求,避免用“先查再改”的方式破坏并发安全语义。
|
|
226
|
+
|
|
227
|
+
### Django 示例包含什么
|
|
228
|
+
|
|
229
|
+
`examples/django_app/` 当前展示了:
|
|
230
|
+
|
|
231
|
+
- Django model 设计
|
|
232
|
+
- `DjangoOrderRepository` 的 duck typing 实现
|
|
233
|
+
- `PaymentService.create_order(...)` 的下单 view 用法
|
|
234
|
+
- `client.verify_callback(...) + CallbackProcessor.process(...)` 的回调处理方式
|
|
235
|
+
- 使用 `transaction.atomic()` 控制回调事务
|
|
236
|
+
- 示例 URL wiring 与 Django 测试
|
|
237
|
+
|
|
238
|
+
## 安全与运行注意事项
|
|
239
|
+
|
|
240
|
+
- **签名协议说明**:SDK 按易支付常见协议使用“排序后的非空参数 + 商户密钥”做 MD5。这里是为了兼容网关协议,不代表 MD5 适合独立承担现代传输安全责任。
|
|
241
|
+
- **生产环境请使用 HTTPS**:生产配置应使用 `https://...` 的 `base_url`,并保持 `verify_ssl=True`。SDK 已禁止在 `environment="production"` 时关闭 SSL 校验。
|
|
242
|
+
- **SQLite 仅适合开发/测试**:示例中的 SQLite 适合本地演示;生产支付回调更适合 PostgreSQL/MySQL 等具备更可靠并发/锁语义的数据库。
|
|
243
|
+
- **回调必须验签再处理**:不要直接信任回调参数;应始终先调用 `verify_callback()`。
|
|
244
|
+
- **`environment` 字段当前是安全/配置语义字段**:它目前只影响校验和告警(例如生产环境 TLS 约束),不会自动切换网关地址或协议行为;可视为保留给部署语义和未来扩展的字段。
|
|
245
|
+
|
|
246
|
+
## 示例
|
|
247
|
+
|
|
248
|
+
- `examples/flask_app.py`:最小 Flask 集成,使用内存仓储演示回调协议
|
|
249
|
+
- `examples/fastapi_app.py`:FastAPI + SQLAlchemy 示例,并显式把同步 SDK/数据库操作放入线程池,避免误导为真正的异步 I/O
|
|
250
|
+
- `examples/django_app/`:Django 集成模板,演示“核心 SDK 保持通用 + Django 仓储/视图/事务放在应用层”
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "epay-sdk"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "A production-oriented Python SDK for common EPay-style payment gateways"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "OpenAI" }]
|
|
13
|
+
keywords = ["epay", "payment", "sdk", "gateway"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
24
|
+
]
|
|
25
|
+
dependencies = ["requests>=2.31.0"]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
sqlalchemy = ["sqlalchemy>=2.0.0"]
|
|
29
|
+
flask = ["flask>=3.0.0"]
|
|
30
|
+
fastapi = ["fastapi>=0.110.0", "uvicorn>=0.29.0"]
|
|
31
|
+
django = ["django>=4.2"]
|
|
32
|
+
dev = ["pytest>=8.0.0", "build>=1.2.1", "twine>=5.0.0", "ruff>=0.6.0", "sqlalchemy>=2.0.0", "django>=4.2"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
package-dir = {"" = "src"}
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 100
|
epay_sdk-0.4.0/setup.cfg
ADDED