在电商和金融系统开发中,支付对接永远是那个让人又爱又恨的环节。记得我第一次对接银联支付时,光是看那厚厚的接口文档就花了整整三天,更别提各种签名验签、参数组装带来的噩梦了。直到发现了acp-sdk这个神器——这个由银联官方提供的Python开发工具包,简直就是支付对接领域的"瑞士军刀"。
acp-sdk全称是ACP Service Development Kit,它是银联全渠道支付平台的官方Python SDK。简单来说,它把银联支付那些复杂的交互流程、安全机制都封装成了简单的Python方法调用。无论是支付、退款还是交易查询,现在都只需要几行代码就能搞定。我最近在一个跨境电商项目中用它接入了银联国际支付,开发效率提升了至少70%。
安装过程简单得令人发指,一条pip命令就能搞定:
bash复制pip install acp-sdk
但这里有个小细节需要注意:最好指定版本号安装,避免后续版本升级导致兼容性问题。我习惯用:
bash复制pip install acp-sdk==3.5.8
注意:安装前请确保你的Python版本在3.6以上。我在Python 3.5环境遇到过ssl模块的兼容性问题,折腾了半天才发现是版本太老。
虽然acp-sdk的依赖会自动安装,但我强烈建议使用虚拟环境。这是我的标准操作流程:
bash复制python -m venv acp_env
source acp_env/bin/activate # Linux/Mac
# 或者 acp_env\Scripts\activate # Windows
pip install acp-sdk
这样能避免污染全局Python环境,特别是当你同时维护多个支付项目时。
初始化是使用acp-sdk的第一步,也是最重要的一步。来看个标准示例:
python复制from acp_sdk import AcpService
config = {
'mer_id': '777290058110048', # 商户号
'sign_cert_path': '/path/to/cert.pfx', # 签名证书路径
'sign_cert_pwd': '000000', # 证书密码
'validate_cert_dir': '/path/to/acp_certs', # 验签证书目录
'environment': 'sandbox' # 环境:sandbox/production
}
AcpService.init(config)
这里有几个关键点需要特别注意:
证书管理:银联要求使用双向证书认证。签名证书(.pfx)需要从银联商户平台下载,而验签证书(.cer)则用来验证银联返回的数据。我习惯把验签证书统一放在/conf/acp_certs目录下。
环境切换:environment参数支持sandbox(测试)和production(生产)两种环境。实际开发中,我通常会这样封装:
python复制import os
def init_acp_sdk():
env = os.getenv('APP_ENV', 'sandbox')
config = {
'environment': 'production' if env == 'prod' else 'sandbox',
# 其他配置...
}
AcpService.init(config)
让我们以最常见的消费交易为例,看看如何使用acp-sdk完成一次支付:
python复制from acp_sdk import AcpService
import datetime
def create_payment(order_no, amount, front_url, back_url):
params = {
'version': '5.1.0', # 版本号
'encoding': 'UTF-8', # 编码方式
'txnType': '01', # 交易类型:01-消费
'txnSubType': '01', # 交易子类:01-默认
'bizType': '000201', # 业务类型:000201-B2C
'channelType': '07', # 渠道类型:07-PC
'merId': config['mer_id'], # 商户号
'orderId': order_no, # 商户订单号
'txnTime': datetime.datetime.now().strftime('%Y%m%d%H%M%S'),
'txnAmt': str(amount), # 交易金额(分)
'currencyCode': '156', # 交易币种:156-人民币
'frontUrl': front_url, # 前台通知地址
'backUrl': back_url, # 后台通知地址
}
# 添加其他可选参数
params.update({
'reqReserved': '附加信息', # 商户自定义保留域
'riskRateInfo': '{commodityName=手机}' # 风控信息
})
# 生成表单
html = AcpService.createAutoFormHtml(params, 'https://gateway.test.95516.com/gateway/api/frontTransReq.do')
return html
这段代码有几个技术要点:
金额单位:txnAmt的单位是分,所以10元要传"1000"。我在项目中吃过亏,传成了"10.00"导致支付失败。
时间格式:txnTime必须严格遵循YYYYMMDDHHMMSS格式。建议使用datetime标准化输出,避免手工拼接出错。
渠道类型:根据实际场景选择:
07:PC网页08:移动端16:小程序银联支付成功后,会通过frontUrl和backUrl异步通知商户。处理通知的典型代码如下:
python复制from flask import request
@app.route('/notify', methods=['POST'])
def acp_notify():
try:
# 获取所有通知参数
params = request.form.to_dict()
# 验签
if not AcpService.validate(params):
return '验签失败', 400
# 处理业务逻辑
order_id = params['orderId']
txn_amt = int(params['txnAmt']) # 金额(分)
resp_code = params['respCode']
if resp_code == '00':
# 支付成功,更新订单状态
update_order_status(order_id, 'PAID', txn_amt)
else:
# 支付失败处理
handle_payment_failure(order_id, resp_code)
# 返回成功响应
return 'SUCCESS'
except Exception as e:
logger.error(f"处理银联通知异常: {str(e)}")
return 'FAIL', 500
重要提示:一定要先验签再处理业务逻辑!我见过有开发者把顺序搞反,结果遭遇了中间人攻击,造成了资金损失。
退款是支付系统中最容易出问题的环节之一。acp-sdk让退款变得简单:
python复制def process_refund(orig_order_id, refund_amount, refund_order_id):
params = {
'version': '5.1.0',
'encoding': 'UTF-8',
'txnType': '04', # 交易类型:04-退款
'txnSubType': '00', # 交易子类:00-默认
'bizType': '000201',
'channelType': '07',
'merId': config['mer_id'],
'orderId': refund_order_id, # 退款订单号
'txnTime': datetime.datetime.now().strftime('%Y%m%d%H%M%S'),
'txnAmt': str(refund_amount),
'currencyCode': '156',
'backUrl': 'https://yourdomain.com/refund/notify',
'origQryId': orig_order_id # 原交易订单号
}
# 签名并发送请求
AcpService.sign(params)
url = AcpService.get_sdk_config().back_trans_url
response = AcpService.post(params, url)
# 处理响应
if response['respCode'] == '00':
return True, '退款成功'
else:
return False, response.get('respMsg', '退款失败')
退款操作的关键点:
幂等性处理:银联对同一退款订单号只会处理一次。我建议退款订单号采用refund_{原订单号}_{序号}的格式,比如refund_20230801123456_1。
金额限制:退款金额不能大于原交易金额。部分银行还要求退款必须与原交易使用同一张卡。
时效性:通常T+1日退款才能到账,需要在页面明确告知用户。
交易状态查询是支付系统健康运行的保障:
python复制def query_order(order_id):
params = {
'version': '5.1.0',
'encoding': 'UTF-8',
'txnType': '00', # 交易类型:00-查询
'txnSubType': '00',
'bizType': '000000', # 业务类型:000000-查询
'merId': config['mer_id'],
'orderId': order_id,
'txnTime': datetime.datetime.now().strftime('%Y%m%d%H%M%S')
}
AcpService.sign(params)
response = AcpService.post(params, AcpService.get_sdk_config().single_query_url)
if response['respCode'] == '00':
return {
'status': 'SUCCESS' if response['origRespCode'] == '00' else 'FAIL',
'amount': int(response['txnAmt']),
'settle_date': response['settleDate']
}
else:
raise Exception(f"查询失败: {response.get('respMsg')}")
实际项目中,我会结合定时任务实现自动对账:
python复制from apscheduler.schedulers.background import BackgroundScheduler
def auto_reconciliation():
# 获取需要对账的订单
orders = get_unreconciled_orders()
for order in orders:
try:
result = query_order(order['order_id'])
update_reconciliation_result(order['id'], result)
except Exception as e:
logger.error(f"订单{order['order_id']}对账失败: {str(e)}")
# 每天凌晨1点执行对账
scheduler = BackgroundScheduler()
scheduler.add_job(auto_reconciliation, 'cron', hour=1)
scheduler.start()
| 错误代码 | 含义 | 解决方案 |
|---|---|---|
| 05 | 交易已关闭 | 检查订单是否超时(通常15分钟) |
| 30 | 报文格式错误 | 检查字段类型和长度是否符合规范 |
| 57 | 交易受限 | 联系银联确认商户权限 |
| 94 | 重复交易 | 检查订单号是否重复 |
| 96 | 系统异常 | 稍后重试或联系银联 |
python复制from functools import lru_cache
@lru_cache(maxsize=2)
def load_cert(cert_path, password):
# 证书加载实现...
python复制import requests
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=10,
pool_maxsize=50,
max_retries=3
)
session.mount('https://', adapter)
AcpService.set_http_session(session)
python复制@app.route('/notify', methods=['POST'])
def acp_notify():
# 快速验签后放入消息队列
params = request.form.to_dict()
if AcpService.validate(params):
queue.push(params) # 使用Redis或RabbitMQ
return 'SUCCESS'
return 'FAIL'
python复制import hvac
client = hvac.Client(url='http://vault:8200')
cert_pwd = client.read('secret/payment/acp_cert')['data']['password']
python复制from datetime import datetime
cert_expiry = datetime.strptime('20241231', '%Y%m%d')
if (cert_expiry - datetime.now()).days < 30:
alert('证书即将过期,请及时更新')
python复制def verify_amount(order_id, received_amount):
db_amount = get_order_amount(order_id)
if int(received_amount) != db_amount:
raise ValueError(f"金额不匹配: 订单{db_amount}分, 收到{received_amount}分")
在最近的一个高并发票务系统中,通过以上优化措施,我们将支付接口的TP99从原来的1200ms降低到了350ms,效果显著。