123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- # -*- coding: utf-8 -*-
- from __future__ import unicode_literals
- import time
- import string
- import random
- import hashlib
- import requests
- from .base import Map, WeixinError
- try:
- from flask import request
- except Exception:
- request = None
- try:
- from lxml import etree
- except ImportError:
- from xml.etree import cElementTree as etree
- except ImportError:
- from xml.etree import ElementTree as etree
- __all__ = ("WeixinPayError", "WeixinPay")
- FAIL = "FAIL"
- SUCCESS = "SUCCESS"
- class WeixinPayError(WeixinError):
- def __init__(self, msg):
- super(WeixinPayError, self).__init__(msg)
- class WeixinPay(object):
- def __init__(self, app_id, mch_id, mch_key, notify_url, key=None, cert=None):
- self.app_id = app_id
- self.mch_id = mch_id
- self.mch_key = mch_key
- self.notify_url = notify_url
- self.key = key
- self.cert = cert
- self.sess = requests.Session()
- @property
- def remote_addr(self):
- if request is not None:
- return request.remote_addr
- return ""
- @property
- def nonce_str(self):
- char = string.ascii_letters + string.digits
- return "".join(random.choice(char) for _ in range(32))
- def sign(self, raw):
- raw = [(k, str(raw[k]) if isinstance(raw[k], int) else raw[k])
- for k in sorted(raw.keys())]
- s = "&".join("=".join(kv) for kv in raw if kv[1])
- s += "&key={0}".format(self.mch_key)
- return hashlib.md5(s.encode("utf-8")).hexdigest().upper()
- def check(self, data):
- sign = data.pop("sign")
- return sign == self.sign(data)
- def to_xml(self, raw):
- s = ""
- for k, v in raw.items():
- s += "<{0}>{1}</{0}>".format(k, v)
- s = "<xml>{0}</xml>".format(s)
- return s.encode("utf-8")
- def to_dict(self, content):
- raw = {}
- root = etree.fromstring(content)
- for child in root:
- raw[child.tag] = child.text
- return raw
- def _fetch(self, url, data, use_cert=False):
- data.setdefault("appid", self.app_id)
- data.setdefault("mch_id", self.mch_id)
- data.setdefault("nonce_str", self.nonce_str)
- data.setdefault("sign", self.sign(data))
- if use_cert:
- resp = self.sess.post(url, data=self.to_xml(data), cert=(self.cert, self.key))
- else:
- resp = self.sess.post(url, data=self.to_xml(data))
- content = resp.content.decode("utf-8")
- if "return_code" in content:
- data = Map(self.to_dict(content))
- if data.return_code == FAIL:
- raise WeixinPayError(data.return_msg)
- if "result_code" in content and data.result_code == FAIL:
- raise WeixinPayError(data.err_code_des)
- return data
- return content
- def reply(self, msg, ok=True):
- code = SUCCESS if ok else FAIL
- return self.to_xml(dict(return_code=code, return_msg=msg))
- def unified_order(self, **data):
- """
- 统一下单
- out_trade_no、body、total_fee、trade_type必填
- app_id, mchid, nonce_str自动填写
- spbill_create_ip 在flask框架下可以自动填写, 非flask框架需要主动传入此参数
- """
- url = "https://api.mch.weixin.qq.com/pay/unifiedorder"
- # 必填参数
- if "out_trade_no" not in data:
- raise WeixinPayError("缺少统一支付接口必填参数out_trade_no")
- if "body" not in data:
- raise WeixinPayError("缺少统一支付接口必填参数body")
- if "total_fee" not in data:
- raise WeixinPayError("缺少统一支付接口必填参数total_fee")
- if "trade_type" not in data:
- raise WeixinPayError("缺少统一支付接口必填参数trade_type")
- # 关联参数
- if data["trade_type"] == "JSAPI" and "openid" not in data:
- raise WeixinPayError("trade_type为JSAPI时,openid为必填参数")
- if data["trade_type"] == "NATIVE" and "product_id" not in data:
- raise WeixinPayError("trade_type为NATIVE时,product_id为必填参数")
- data.setdefault("notify_url", self.notify_url)
- data.setdefault("spbill_create_ip", self.remote_addr)
- raw = self._fetch(url, data)
- return raw
- def jsapi(self, **kwargs):
- """
- 生成给JavaScript调用的数据
- 详细规则参考 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
- """
- kwargs.setdefault("trade_type", "JSAPI")
- raw = self.unified_order(**kwargs)
- package = "prepay_id={0}".format(raw["prepay_id"])
- timestamp = str(int(time.time()))
- nonce_str = self.nonce_str
- raw = dict(appId=self.app_id, timeStamp=timestamp,
- nonceStr=nonce_str, package=package, signType="MD5")
- sign = self.sign(raw)
- return dict(package=package, appId=self.app_id,
- timeStamp=timestamp, nonceStr=nonce_str, sign=sign)
- def order_query(self, **data):
- """
- 订单查询
- out_trade_no, transaction_id至少填一个
- appid, mchid, nonce_str不需要填入
- """
- url = "https://api.mch.weixin.qq.com/pay/orderquery"
- if "out_trade_no" not in data and "transaction_id" not in data:
- raise WeixinPayError("订单查询接口中,out_trade_no、transaction_id至少填一个")
- return self._fetch(url, data)
- def close_order(self, out_trade_no, **data):
- """
- 关闭订单
- out_trade_no必填
- appid, mchid, nonce_str不需要填入
- """
- url = "https://api.mch.weixin.qq.com/pay/closeorder"
- data.setdefault("out_trace_no", out_trade_no)
- return self._fetch(url, data)
- def refund(self, **data):
- """
- 申请退款
- out_trade_no、transaction_id至少填一个且
- out_refund_no、total_fee、refund_fee、op_user_id为必填参数
- appid、mchid、nonce_str不需要填入
- """
- if not self.key or not self.cert:
- raise WeixinError("退款申请接口需要双向证书")
- url = "https://api.mch.weixin.qq.com/secapi/pay/refund"
- if "out_trade_no" not in data and "transaction_id" not in data:
- raise WeixinPayError("退款申请接口中,out_trade_no、transaction_id至少填一个")
- if "out_refund_no" not in data:
- raise WeixinPayError("退款申请接口中,缺少必填参数out_refund_no");
- if "total_fee" not in data:
- raise WeixinPayError("退款申请接口中,缺少必填参数total_fee");
- if "refund_fee" not in data:
- raise WeixinPayError("退款申请接口中,缺少必填参数refund_fee");
- if "op_user_id" not in data:
- raise WeixinPayError("退款申请接口中,缺少必填参数op_user_id");
- return self._fetch(url, data, True)
- def refund_query(self, **data):
- """
- 查询退款
- 提交退款申请后,通过调用该接口查询退款状态。退款有一定延时,
- 用零钱支付的退款20分钟内到账,银行卡支付的退款3个工作日后重新查询退款状态。
- out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个
- appid、mchid、nonce_str不需要填入
- """
- url = "https://api.mch.weixin.qq.com/pay/refundquery"
- if "out_refund_no" not in data and "out_trade_no" not in data \
- and "transaction_id" not in data and "refund_id" not in data:
- raise WeixinPayError("退款查询接口中,out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个")
- return self._fetch(url, data)
- def download_bill(self, bill_date, bill_type="ALL", **data):
- """
- 下载对账单
- bill_date、bill_type为必填参数
- appid、mchid、nonce_str不需要填入
- """
- url = "https://api.mch.weixin.qq.com/pay/downloadbill"
- data.setdefault("bill_date", bill_date)
- data.setdefault("bill_type", bill_type)
- if "bill_date" not in data:
- raise WeixinPayError("对账单接口中,缺少必填参数bill_date")
- return self._fetch(url, data)
|