Files
server-core/wechatpayv3/async_/transaction.py
T
Pine f1147dced3 feat(pay): 微信支付V3子应用与算力充值
- app/pay 独立支付子应用(config/wxpay/models/repository/service/routers),便于后期拆分微服务
- 充值订单表 0026_compute_recharge;套餐支持折扣(discount)与上架开关(enabled),DB system_configs 优先、env 兜底
- 统一小程序支付:桌面端出小程序码→扫码进小程序确认页按 openid 发起 JSAPI→回调幂等到账(引擎 adjust_user_quota+镜像回写)
- 内嵌 wechatpayv3(含 async_),响应验签失败降级告警;新增 aiofiles 依赖

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 22:29:44 +08:00

459 lines
21 KiB
Python

# -*- coding: utf-8 -*-
from .type import RequestType, WeChatPayType
async def pay(self,
description,
out_trade_no,
amount,
payer=None,
time_expire=None,
attach=None,
goods_tag=None,
detail=None,
scene_info=None,
settle_info=None,
notify_url=None,
appid=None,
mchid=None,
sub_appid=None,
sub_mchid=None,
support_fapiao=False,
pay_type=None):
"""统一下单
:return code, message:
:param description: 商品描述,示例值:'Image形象店-深圳腾大-QQ公仔'
:param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018'
:param amount: 订单金额,示例值:{'total':100, 'currency':'CNY'}
:param payer: 支付者,示例值:{'openid':'oHkLxtx0vUqe-18p_AXTZ1innxkCY'}
:param time_expire: 交易结束时间,示例值:'2018-06-08T10:34:56+08:00'
:param attach: 附加数据,示例值:'自定义数据'
:param goods_tag: 订单优惠标记,示例值:'WXG'
:param detail: 优惠功能,示例值:{'cost_price':608800, 'invoice_id':'微信123', 'goods_detail':[{'merchant_goods_id':'商品编码', 'wechatpay_goods_id':'1001', 'goods_name':'iPhoneX 256G', 'quantity':1, 'unit_price':828800}]}
:param scene_info: 场景信息,示例值:{'payer_client_ip':'14.23.150.211', 'device_id':'013467007045764', 'store_info':{'id':'0001', 'name':'腾讯大厦分店', 'area_code':'440305', 'address':'广东省深圳市南山区科技中一道10000号'}}
:param settle_info: 结算信息,示例值:{'profit_sharing':False}
:param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php'
:param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef'
:param mchid: 微信支付商户号,可不填,默认传入初始化的mchid,示例值:'987654321'
:param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999'
:param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109'
:param support_fapiao: 电子发票入口开放标识,传入true时,支付成功消息和支付详情页将出现开票入口。
:param pay_type: 微信支付类型,示例值:WeChatPayType.JSAPI
"""
params = {}
if pay_type != WeChatPayType.CODEPAY:
if not (notify_url or self._notify_url):
raise Exception('notify_url is not assigned.')
params.update({'notify_url': notify_url or self._notify_url})
if description:
params.update({'description': description})
else:
raise Exception('description is not assigned.')
if out_trade_no:
params.update({'out_trade_no': out_trade_no})
else:
raise Exception('out_trade_no is not assigned.')
if amount:
params.update({'amount': amount})
else:
raise Exception('amount is not assigned.')
if payer:
params.update({'payer': payer})
if scene_info:
params.update({'scene_info': scene_info})
if time_expire:
params.update({'time_expire': time_expire})
if attach:
params.update({'attach': attach})
if goods_tag:
params.update({'goods_tag': goods_tag})
if detail:
params.update({'detail': detail})
if settle_info:
params.update({'settle_info': settle_info})
pay_type = pay_type or self._type
if self._partner_mode:
params.update({'sp_appid': appid or self._appid})
params.update({'sp_mchid': mchid or self._mchid})
if sub_mchid:
params.update({'sub_mchid': sub_mchid})
else:
raise Exception('sub_mchid is not assigned.')
if sub_appid:
params.update({'sub_appid': sub_appid})
if pay_type in [WeChatPayType.JSAPI, WeChatPayType.MINIPROG]:
if not payer:
raise Exception('payer is not assigned')
path = '/v3/pay/partner/transactions/jsapi'
elif pay_type == WeChatPayType.APP:
path = '/v3/pay/partner/transactions/app'
elif pay_type == WeChatPayType.H5:
if not scene_info:
raise Exception('scene_info is not assigned.')
path = '/v3/pay/partner/transactions/h5'
elif pay_type == WeChatPayType.NATIVE:
path = '/v3/pay/partner/transactions/native'
elif pay_type == WeChatPayType.CODEPAY:
path = '/v3/pay/partner/transactions/codepay'
else:
raise Exception('pay_type is not assigned.')
else:
params.update({'appid': appid or self._appid})
params.update({'mchid': mchid or self._mchid})
if pay_type in [WeChatPayType.JSAPI, WeChatPayType.MINIPROG]:
if not payer:
raise Exception('payer is not assigned')
path = '/v3/pay/transactions/jsapi'
elif pay_type == WeChatPayType.APP:
path = '/v3/pay/transactions/app'
elif pay_type == WeChatPayType.H5:
if not scene_info:
raise Exception('scene_info is not assigned.')
path = '/v3/pay/transactions/h5'
elif pay_type == WeChatPayType.NATIVE:
path = '/v3/pay/transactions/native'
elif pay_type == WeChatPayType.CODEPAY:
path = '/v3/pay/transactions/codepay'
else:
raise Exception('pay_type is not assigned.')
if support_fapiao:
params.update({'support_fapiao': support_fapiao})
return await self._core.request(path, method=RequestType.POST, data=params)
async def close(self, out_trade_no, mchid=None, sub_mchid=None):
"""关闭订单
:param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018'
:param mchid: 微信支付商户号,可不传,默认传入初始化的mchid。示例值:'987654321'
:param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109'
"""
if self._partner_mode:
if out_trade_no:
path = '/v3/pay/partner/transactions/out-trade-no/%s/close' % out_trade_no
else:
raise Exception('out_trade_no is not assigned.')
if sub_mchid:
params = {'sp_mchid': mchid or self._mchid, 'sub_mchid': sub_mchid}
else:
raise Exception('sub_mchid is not assigned.')
else:
if out_trade_no:
path = '/v3/pay/transactions/out-trade-no/%s/close' % out_trade_no
else:
raise Exception('out_trade_no is not assigned.')
params = {'mchid': mchid or self._mchid}
return await self._core.request(path, method=RequestType.POST, data=params)
async def query(self, transaction_id=None, out_trade_no=None, mchid=None, sub_mchid=None):
"""查询订单
:param transaction_id: 微信支付订单号,示例值:1217752501201407033233368018
:param out_trade_no: 商户订单号,示例值:1217752501201407033233368018
:param mchid: 微信支付商户号,可不传,默认传入初始化的mchid。示例值:'987654321'
:param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109'
"""
if self._partner_mode:
if transaction_id:
path = '/v3/pay/partner/transactions/id/%s' % transaction_id
elif out_trade_no:
path = '/v3/pay/partner/transactions/out-trade-no/%s' % out_trade_no
else:
raise Exception('transaction_id or out_trade_no is not assigned.')
path = '%s?sp_mchid=%s&sub_mchid=%s' % (path, mchid or self._mchid, sub_mchid)
else:
if transaction_id:
path = '/v3/pay/transactions/id/%s' % transaction_id
elif out_trade_no:
path = '/v3/pay/transactions/out-trade-no/%s' % out_trade_no
else:
raise Exception('transaction_id out_trade_no is not assigned.')
path = '%s?mchid=%s' % (path, mchid or self._mchid)
return await self._core.request(path)
async def refund(self,
out_refund_no,
amount,
transaction_id=None,
out_trade_no=None,
reason=None,
funds_account=None,
goods_detail=None,
notify_url=None,
sub_mchid=None):
"""申请退款
:param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018'
:param amount: 金额信息,示例值:{'refund':888, 'total':888, 'currency':'CNY'}
:param transaction_id: 微信支付订单号,示例值:'1217752501201407033233368018'
:param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018'
:param reason: 退款原因,示例值:'商品已售完'
:param funds_account: 退款资金来源,示例值:'AVAILABLE'
:param goods_detail: 退款商品,示例值:{'merchant_goods_id':'1217752501201407033233368018', 'wechatpay_goods_id':'1001', 'goods_name':'iPhone6s 16G', 'unit_price':528800, 'refund_amount':528800, 'refund_quantity':1}
:param notify_url: 通知地址,示例值:'https://www.weixin.qq.com/wxpay/pay.php'
:param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109'
"""
params = {}
if notify_url or self._notify_url:
params.update({'notify_url': notify_url or self._notify_url})
if out_refund_no:
params.update({'out_refund_no': out_refund_no})
else:
raise Exception('out_refund_no is not assigned.')
if amount:
params.update({'amount': amount})
else:
raise Exception('amount is not assigned.')
if transaction_id:
params.update({'transaction_id': transaction_id})
elif out_trade_no:
params.update({'out_trade_no': out_trade_no})
else:
raise Exception('transaction_id is not assigned.')
if reason:
params.update({'reason': reason})
if funds_account:
params.update({'funds_account': funds_account})
if goods_detail:
params.update({'goods_detail': goods_detail})
if self._partner_mode:
if sub_mchid:
params.update({'sub_mchid': sub_mchid})
else:
raise Exception('sub_mchid is not assigned.')
path = '/v3/refund/domestic/refunds'
return await self._core.request(path, method=RequestType.POST, data=params)
async def query_refund(self, out_refund_no, sub_mchid=None):
"""查询单笔退款
:param out_refund_no: 商户退款单号,示例值:'1217752501201407033233368018'
:param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109'
"""
path = '/v3/refund/domestic/refunds/%s' % out_refund_no
if self._partner_mode:
if sub_mchid:
path = '%s?sub_mchid=%s' % (path, sub_mchid)
else:
raise Exception('sub_mchid is not assigned.')
return await self._core.request(path)
async def trade_bill(self, bill_date, bill_type='ALL', tar_type='GZIP', sub_mchid=None):
"""申请交易账单
:param bill_date: 账单日期,示例值:'2019-06-11'
:param bill_type: 账单类型, 默认值:'ALL'
:param tar_type: 压缩类型,默认值:'GZIP'
:param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109'
"""
path = '/v3/bill/tradebill?bill_date=%s&bill_type=%s&tar_type=%s' % (bill_date, bill_type, tar_type)
if self._partner_mode and sub_mchid:
path = '%s&sub_mchid=%s' % (path, sub_mchid)
return await self._core.request(path)
async def fundflow_bill(self, bill_date, account_type='BASIC', tar_type='GZIP'):
"""申请资金账单
:param bill_date: 账单日期,示例值:'2019-06-11'
:param account_type: 资金账户类型, 默认值:'BASIC',基本账户, 可选:'OPERATION',运营账户;'FEES',手续费账户
:param tar_type: 压缩类型,默认值:'GZIP'
"""
if not bill_date:
raise Exception('bill_date is not assigned.')
path = '/v3/bill/fundflowbill?bill_date=%s&account_type=%s&tar_type=%s' % (bill_date, account_type, tar_type)
return await self._core.request(path)
async def submch_fundflow_bill(self, sub_mchid, bill_date, account_type, algorithm='AEAD_AES_256_GCM', tar_type=None):
"""申请单个子商户资金账单
:param sub_mchid: 子商户号,示例值:'19000000001'
:param bill_date: 账单日期,格式YYYY-MM-DD,示例值:'2019-06-11'
:param account_type: 资金账户类型,枚举值:'BASIC':基本账户,'OPERATION':运营账户,'FEES':手续费账户,示例值:'BASIC'
:param algorithm: 加密算法,枚举值:'AEAD_AES_256_GCM':AEAD_AES_256_GCM加密算法
:param tar_type: 压缩格式,枚举值:'GZIP':返回格式为.gzip的压缩包账单
"""
path = '/v3/bill/sub-merchant-fundflowbill'
if sub_mchid:
path += '?sub_mchid=%s' % sub_mchid
else:
raise Exception('sub_mchid is not assigned.')
if bill_date:
path += '&bill_date=%s' % bill_date
else:
raise Exception('bill_date is not assigned.')
if account_type:
path += '&account_type=%s' % account_type
else:
raise Exception('account_type is not assigned.')
if algorithm:
path += '&algorithm=%s' % algorithm
else:
raise Exception('algorithm is not assigned.')
if tar_type:
path += '&tar_type=%s' % tar_type
return await self._core.request(path)
async def download_bill(self, url):
"""下载账单
:param url: 账单下载地址,示例值:'https://api.mch.weixin.qq.com/v3/billdownload/file?token=xxx'
"""
path = url[len(self._core._gate_way):] if url.startswith(self._core._gate_way) else url
return await self._core.request(path, skip_verify=True)
async def combine_pay(self,
combine_out_trade_no,
sub_orders,
scene_info=None,
combine_payer_info=None,
time_start=None,
time_expire=None,
combine_appid=None,
combine_mchid=None,
notify_url=None,
pay_type=None):
"""合单支付下单
:param combine_out_trade_no: 合单商户订单号, 示例值:'P20150806125346'
:param sub_orders: 子单信息,示例值:[{'mchid':'1900000109', 'attach':'深圳分店', 'amount':{'total_amount':100,'currency':'CNY'}, 'out_trade_no':'20150806125346', 'description':'腾讯充值中心-QQ会员充值', 'settle_info':{'profit_sharing':False, 'subsidy_amount':10}}]
:param scene_info: 场景信息, 示例值:{'device_id':'POS1:123', 'payer_client_ip':'14.17.22.32'}
:param combine_payer_info: 支付者, 示例值:{'openid':'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o'}
:param time_start: 交易起始时间,示例值:'2019-12-31T15:59:59+08:00'
:param time_expire: 交易结束时间, 示例值:'2019-12-31T15:59:59+08:00'
:param combine_appid: 合单商户appid, 示例值:'wxd678efh567hg6787'
:param combine_mchid: 合单发起方商户号,示例值:'1900000109'
:param notify_url: 通知地址, 示例值:'https://yourapp.com/notify'
:param pay_type: 微信支付类型,示例值:WeChatPayType.JSAPI
"""
params = {}
params.update({'combine_appid': combine_appid or self._appid})
params.update({'combine_mchid': combine_mchid or self._mchid})
if not (notify_url or self._notify_url):
raise Exception('notify_url is not assigned.')
params.update({'notify_url': notify_url or self._notify_url})
if combine_out_trade_no:
params.update({'combine_out_trade_no': combine_out_trade_no})
else:
raise Exception('combine_out_trade_no is not assigned.')
if sub_orders:
params.update({'sub_orders': sub_orders})
else:
raise Exception('sub_orders is not assigned.')
if scene_info:
params.update({'scene_info': scene_info})
if combine_payer_info:
params.update({'combine_payer_info': combine_payer_info})
if time_start:
params.update({'time_start': time_start})
if time_expire:
params.update({'time_expire': time_expire})
pay_type = pay_type or self._type
if pay_type in [WeChatPayType.JSAPI, WeChatPayType.MINIPROG]:
if not combine_payer_info:
raise Exception('combine_payer_info is not assigned')
path = '/v3/combine-transactions/jsapi'
elif pay_type == WeChatPayType.APP:
path = '/v3/combine-transactions/app'
elif pay_type == WeChatPayType.H5:
if not scene_info:
raise Exception('scene_info is not assigned.')
path = '/v3/combine-transactions/h5'
elif pay_type == WeChatPayType.NATIVE:
path = '/v3/combine-transactions/native'
else:
raise Exception('pay_type is not assigned.')
return await self._core.request(path, method=RequestType.POST, data=params)
async def combine_query(self, combine_out_trade_no):
"""合单查询订单
:param combine_out_trade_no: 合单商户订单号,示例值:P20150806125346
"""
params = {}
if not combine_out_trade_no:
raise Exception('combine_out_trade_no is not assigned')
else:
params.update({'combine_out_trade_no': combine_out_trade_no})
path = '/v3/combine-transactions/out-trade-no/%s' % combine_out_trade_no
return await self._core.request(path)
async def combine_close(self, combine_out_trade_no, sub_orders, combine_appid=None):
"""合单关闭订单
:param combine_out_trade_no: 合单商户订单号,示例值:'P20150806125346'
:param sub_orders: 子单信息, 示例值:[{'mchid': '1900000109', 'out_trade_no': '20150806125346'}]
:param combine_appid: 合单商户appid, 示例值:'wxd678efh567hg6787'
"""
params = {}
params.update({'combine_appid': combine_appid or self._appid})
if not combine_out_trade_no:
raise Exception('combine_out_trade_no is not assigned.')
if not sub_orders:
raise Exception('sub_orders is not assigned.')
else:
params.update({'sub_orders': sub_orders})
path = '/v3/combine-transactions/out-trade-no/%s/close' % combine_out_trade_no
return await self._core.request(path, method=RequestType.POST, data=params)
async def abnormal_refund(self, refund_id, out_refund_no, type, bank_type=None, bank_account=None, real_name=None, sub_mchid=None):
"""发起异常退款
:param refund_id: 微信退款单号,退款单的主键,唯一定义此资源的标识。
:param out_refund_no: 商户退款单号,商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。
:param type: 异常退款处理方式,可选值:'USER_BANK_CARD',退款到用户银行卡; 'MERCHANT_BANK_CARD',退款至交易商户银行账户。
:param bank_type: 开户银行类型,采用字符串类型的银行标识,值列表详见官网银行类型。
:param bank_account: 收款银行卡号,用户的银行卡账号。
:param real_name: 收款用户姓名。
"""
if refund_id:
path = '/v3/refund/domestic/refunds/%s/apply-abnormal-refund' % refund_id
else:
raise Exception('refund_id is not assigned.')
params = {}
if self._partner_mode:
if sub_mchid:
params.update({'sub_mchid': sub_mchid})
else:
raise Exception('sub_mchid is not assigned.')
if not (out_refund_no and type):
raise Exception('out_refund_no or type is not assigned.')
params.update({'out_refund_no': out_refund_no})
params.update({'type': type})
if bank_type:
params.update({'bank_type': bank_type})
cipher_data = False
if bank_account:
params.update({'bank_account': self._core.encrypt(bank_account)})
cipher_data = True
if real_name:
params.update({'real_name': self._core.encrypt(real_name)})
cipher_data = True
return await self._core.request(path, method=RequestType.POST, data=params, cipher_data=cipher_data)
async def codepay_reverse(self, out_trade_no, appid=None, mchid=None, sub_appid=None, sub_mchid=None):
"""撤销付款码支付订单
:警告:付款码支付订单如果用户已经付款,调用撤销接口会将资金退回给用户。:
:return code, message:
:param out_trade_no: 商户订单号,示例值:'1217752501201407033233368018'
:param appid: 应用ID,可不填,默认传入初始化时的appid,示例值:'wx1234567890abcdef'
:param mchid: 微信支付商户号,可不填,默认传入初始化的mchid,示例值:'987654321'
:param sub_appid: (服务商模式)子商户应用ID,示例值:'wxd678efh567hg6999'
:param sub_mchid: (服务商模式)子商户的商户号,由微信支付生成并下发。示例值:'1900000109'
"""
params = {}
if self._partner_mode:
params.update({'sp_appid': appid or self._appid})
params.update({'sp_mchid': mchid or self._mchid})
if sub_mchid:
params.update({'sub_mchid': sub_mchid})
else:
raise Exception('sub_mchid is not assigned.')
if sub_appid:
params.update({'sub_appid': sub_appid})
path = f'/v3/pay/partner/transactions/out-trade-no/{out_trade_no}/reverse'
else:
params.update({'appid': appid or self._appid})
params.update({'mchid': mchid or self._mchid})
path = f'/v3/pay/transactions/out-trade-no/{out_trade_no}/reverse'
return await self._core.request(path, method=RequestType.POST, data=params)