支付寶支付和微信支付是當今互聯網產品常用的功能,我使用Django Rest Framework實現了網頁上支付寶支付和微信支付的一個通用服務,提供rpc接口給其他服務,包括獲取支付寶支付頁面url的rpc接口、支付寶支付成功異步回調http接口、獲取微信支付二維碼rpc接口、主動查詢微信訂單是否支付的rpc接口等。
支付寶網站支付需要螞蟻金服開放平台賬號,創建應用、配置秘鑰等步驟請參考:螞蟻金服支付寶電腦網站支付快速接入
微信網站支付需要到微信支付官網注冊服務商賬號,
目錄結構如下:
1、models.py
1 from django.db import models 2 from django.contrib.postgres.fields import ArrayField 3 4 5 # Create your models here. 6 7 8 class BaseModel(models.Model): 9 """ 10 基礎模型 11 """ 12 created_time = models.DateTimeField(auto_now_add=True, verbose_name="創建時間") 13 updated_time = models.DateTimeField(auto_now=True, verbose_name="修改時間") 14 created_by = models.IntegerField(verbose_name="創建人ID") 15 updated_by = models.IntegerField(verbose_name="修改人ID") 16 is_active = models.BooleanField(default=True, verbose_name='是否正常') 17 18 class Meta: 19 abstract = True 20 21 22 class Alipay(BaseModel): 23 """ 24 支付 25 """ 26 27 subject = models.CharField(max_length=256, verbose_name="訂單標題") 28 out_trade_no = models.CharField(max_length=64, unique=True, verbose_name="唯一訂單號") 29 trade_no = models.CharField(default="", max_length=64, verbose_name="支付寶系統中的交易流水號") 30 total_amount = models.DecimalField(max_digits=11, decimal_places=2, verbose_name="訂單的資金總額") 31 return_url = models.CharField(max_length=500, verbose_name="支付完成同步跳轉地址") 32 notify_url = models.CharField(max_length=500, verbose_name="支付完成異步通知rpc地址") 33 pay_time = models.DateTimeField(null=True, blank=True, verbose_name="支付時間") 34 pay_nos = ArrayField(models.CharField(max_length=100), default=[], verbose_name='同一訂單的支付ID數組') 35 36 class Meta: 37 verbose_name = "阿里支付" 38 verbose_name_plural = verbose_name 39 ordering = ('-created_time',) 40 41 42 class Wxorder(BaseModel): 43 """ 44 訂單 45 """ 46 body = models.CharField(max_length=256, verbose_name="商品描述") 47 out_trade_no = models.CharField(max_length=64, unique=True, verbose_name="訂單號") 48 transaction_id = models.CharField(default="", max_length=64, verbose_name="微信支付訂單號") 49 total_fee = models.BigIntegerField(verbose_name="訂單的資金總額,單位為分") 50 product_id = models.CharField(max_length=16, verbose_name="商品ID") 51 notify_url = models.CharField(max_length=500, verbose_name="支付完成通知url") 52 pay_time = models.DateTimeField(null=True, blank=True, verbose_name="支付時間") 53 54 class Meta: 55 verbose_name = "微信訂單" 56 verbose_name_plural = verbose_name 57 ordering = ('-created_time',) 58 59 60 class Wxpay(BaseModel): 61 """ 62 微信支付 63 """ 64 out_trade_no = models.CharField(null=True, blank=True, max_length=64, verbose_name="訂單號") 65 pay_no = models.CharField(null=True, blank=True, max_length=64, unique=True, verbose_name="支付唯一訂單號") 66 code_url = models.CharField(null=True, blank=True, max_length=100, verbose_name="二維碼地址") 67 nonce_str = models.CharField(null=True, blank=True, max_length=32, verbose_name="隨機字符串") 68 69 class Meta: 70 verbose_name = "微信支付" 71 verbose_name_plural = verbose_name 72 ordering = ('-created_time',)
2、serializers.py:
1 from django.conf import settings 2 from rest_framework import serializers 3 4 from pay.models import Alipay, Wxpay 5 6 7 class BaseSerializer(serializers.ModelSerializer): 8 created_time = serializers.DateTimeField(format=settings.DATETIME_FORMAT, read_only=True) 9 updated_time = serializers.DateTimeField(format=settings.DATETIME_FORMAT, read_only=True) 10 is_active = serializers.BooleanField(read_only=True) 11 12 class Meta: 13 model = None 14 15 16 class AlipaySerializer(BaseSerializer): 17 """ 18 阿里支付序列化類 19 """ 20 21 class Meta: 22 model = Alipay 23 fields = "__all__" 24 25 26 class WxpaySerializer(BaseSerializer): 27 """ 28 阿里支付序列化類 29 """ 30 31 class Meta: 32 model = Wxpay 33 fields = "__all__"
3、views.py:
1 # -*- coding=utf-8 -*- 2 # Create your views here. 3 import time 4 import dicttoxml 5 6 from jsonrpc import jsonrpc_method 7 from rest_framework.decorators import list_route 8 from rest_framework.response import Response 9 from rest_framework.viewsets import ModelViewSet 10 from rest_framework.views import APIView 11 from rest_framework_xml.parsers import XMLParser 12 from rest_framework_xml.renderers import XMLRenderer 13 from tokenauth.decorators import is_login 14 15 from pay import utils 16 from pay.weixin_pay import WeiXinPay, UnifiedOrderPay, OrderQuery 17 from pay.UUIDTools import UUIDTools 18 from pay.models import Alipay, Wxpay, Wxorder 19 from pay.serializers import AlipaySerializer, WxpaySerializer 20 from pay.utils import UnActiveModelMixin 21 from pay.alipay import AliPay 22 from PAY_SERVICE.settings.base import APPID, PRIVATE_KEY_PATH, \ 23 ALI_PUB_KEY_PATH, ALIPAY_CALLBACK_URL, \ 24 WXAPPID, WX_PAY_KEY, WX_MCH_ID, WXPAY_CALLBACK_URL 25 26 NOTIFY_URL = ALIPAY_CALLBACK_URL + 'api/v1.0/pay/alipay/notify/' 27 28 29 30 class AlipayViewSet(ModelViewSet): 31 queryset = Alipay.objects.filter(is_active=True) 32 serializer_class = AlipaySerializer 33 34 @list_route(methods=['post']) 35 def notify(self, request): 36 """ 37 處理支付寶的notify_url 38 :param request: 39 :return: 40 """ 41 processed_dict = {} 42 for k, v in request.data.items(): 43 processed_dict[k] = v 44 app_id = processed_dict.get('app_id') 45 pay_no = processed_dict.get('out_trade_no') 46 trade_no = processed_dict.get('trade_no') 47 total_amount = processed_dict.get('total_amount') 48 pay_time = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime()) 49 50 alipay = Alipay.objects.filter(pay_nos__contains=[pay_no]).values().first() 51 if alipay is None: 52 return Response("failed") 53 if str(alipay.get('total_amount')) != str(total_amount): 54 return Response("failed") 55 if app_id != APPID: 56 return Response("failed") 57 if alipay.get('trade_no') != "": 58 return Response("failed") 59 60 sign = processed_dict.pop('sign', None) 61 62 ali_pay = AliPay( 63 appid=APPID, 64 app_notify_url=NOTIFY_URL, 65 app_private_key_path=PRIVATE_KEY_PATH, 66 alipay_public_key_path=ALI_PUB_KEY_PATH, 67 debug=True, # 默認False, 68 return_url=alipay.get('return_url') 69 ) 70 71 is_verify = ali_pay.verify(processed_dict, sign) 72 73 if is_verify is True: 74 Alipay.objects.filter(pk=alipay.get('id')).update(pay_time=pay_time, trade_no=trade_no) 75 ret = utils.request_thrift('TradingManager', 'notify', 76 settings.TRADING_RPC_IP, int(settings.TRADING_RPC_PORT), 77 alipay.get('out_trade_no'), str(pay_time)) 78 if ret == "success": 79 return Response("success") 80 81 82 class WxpayViewSet(ModelViewSet): 83 queryset = Wxpay.objects.filter(is_active=True) 84 serializer_class = WxpaySerializer 85 parser_classes = (XMLParser,) 86 renderer_classes = (XMLRenderer,) 87 88 89 @jsonrpc_method('pay.get_alipay_url') 90 def get_alipay_url(request, subject, out_trade_no, total_amount, return_url, notify_url, user_id): 91 recode = Alipay.objects.filter(out_trade_no=out_trade_no).values().first() 92 if recode is not None: 93 pay_no = UUIDTools.datetime_random() 94 alipay = Alipay.objects.get(pk=recode.get('id')) 95 alipay.pay_nos.append(pay_no) 96 alipay.save() 97 else: 98 pay_no = out_trade_no 99 Alipay.objects.create(subject=subject, 100 out_trade_no=out_trade_no, 101 total_amount=total_amount, 102 return_url=return_url, 103 notify_url=notify_url, 104 pay_nos=[pay_no], 105 created_by=user_id, 106 updated_by=user_id 107 ) 108 109 ali_pay = AliPay( 110 appid=APPID, 111 app_notify_url=NOTIFY_URL, 112 app_private_key_path=PRIVATE_KEY_PATH, 113 alipay_public_key_path=ALI_PUB_KEY_PATH, 114 debug=True, # 默認False, 115 return_url=return_url 116 ) 117 118 total_amount = "%.2f" % float(total_amount) 119 url = ali_pay.direct_pay( 120 subject=subject, 121 out_trade_no=pay_no, 122 total_amount=total_amount 123 ) 124 # 沙箱環境網關 125 # alipay_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url) 126 # 正式環境網關 127 alipay_url = "https://openapi.alipay.com/gateway.do?{data}".format(data=url) 128 return alipay_url 129 130 131 @jsonrpc_method('pay.get_wxpay_url') 132 def get_wxpay_url(request, out_trade_no, body, total_fee, notify_url, product_id, user_id): 133 recode = Wxorder.objects.filter(out_trade_no=out_trade_no).values().first() 134 if recode is None: 135 Wxorder.objects.create( 136 out_trade_no=out_trade_no, 137 body=body, 138 total_fee=total_fee, 139 notify_url=notify_url, 140 product_id=product_id, 141 created_by=user_id, 142 updated_by=user_id 143 ) 144 145 pay_no = UUIDTools.datetime_random() 146 pay = UnifiedOrderPay(WXAPPID, WX_MCH_ID, WX_PAY_KEY) 147 response = pay.post(body, pay_no, total_fee, 148 WXPAY_CALLBACK_URL.split('://')[1].split(':')[0], WX_NOTIFY_URL) 149 if response and response["return_code"] == "SUCCESS" and response["result_code"] == "SUCCESS": 150 wxorder = Wxorder.objects.filter(out_trade_no=out_trade_no).values().first() 151 Wxpay.objects.create( 152 out_trade_no=out_trade_no, 153 pay_no=pay_no, 154 code_url=response.get('code_url'), 155 nonce_str=response.get('nonce_str'), 156 created_by=user_id, 157 updated_by=user_id 158 ) 159 return response.get('code_url') 160 161 162 @jsonrpc_method('pay.wx_order_query') 163 def wx_order_query(request, out_trade_no): 164 wxpays = Wxpay.objects.filter(out_trade_no=out_trade_no).values() 165 pay = OrderQuery(WXAPPID, WX_MCH_ID, WX_PAY_KEY) 166 for wxpay in wxpays: 167 response = pay.post(wxpay.get('pay_no')) 168 if response and response["return_code"] == "SUCCESS" \ 169 and response["result_code"] == "SUCCESS": 170 trade_state = response["trade_state"] 171 if trade_state == "SUCCESS": # 支付成功 172 pay_time = response["time_end"] 173 transaction_id = response["transaction_id"] 174 Wxorder.objects.filter(out_trade_no=out_trade_no).update( 175 pay_time=time.strftime("%Y-%m-%d %H:%M:%S", 176 time.strptime(pay_time, "%Y%m%d%H%M%S")), 177 transaction_id=transaction_id 178 ) 179 return {"success": True, "pay_time": pay_time} 180 return {"success": False}
4、alipay.py:
1 # -*- coding: utf-8 -*- 2 3 # pip install pycryptodome 4 5 from datetime import datetime 6 from Crypto.PublicKey import RSA 7 from Crypto.Signature import PKCS1_v1_5 8 from Crypto.Hash import SHA256 9 from base64 import b64encode, b64decode 10 from urllib.parse import quote_plus 11 from urllib.parse import urlparse, parse_qs 12 from urllib.request import urlopen 13 from base64 import decodebytes, encodebytes 14 15 import json 16 17 18 class AliPay(object): 19 """ 20 支付寶支付接口 21 """ 22 23 def __init__(self, appid, app_notify_url, app_private_key_path, 24 alipay_public_key_path, return_url, debug=False): 25 self.appid = appid 26 self.app_notify_url = app_notify_url 27 self.app_private_key_path = app_private_key_path 28 self.app_private_key = None 29 self.return_url = return_url 30 with open(self.app_private_key_path) as fp: 31 self.app_private_key = RSA.importKey(fp.read()) 32 33 self.alipay_public_key_path = alipay_public_key_path 34 with open(self.alipay_public_key_path) as fp: 35 self.alipay_public_key = RSA.import_key(fp.read()) 36 37 if debug is True: 38 self.__gateway = "https://openapi.alipaydev.com/gateway.do" 39 else: 40 self.__gateway = "https://openapi.alipay.com/gateway.do" 41 42 def direct_pay(self, subject, out_trade_no, total_amount, return_url=None, **kwargs): 43 biz_content = { 44 "subject": subject, 45 "out_trade_no": out_trade_no, 46 "total_amount": total_amount, 47 "product_code": "FAST_INSTANT_TRADE_PAY", 48 # "qr_pay_mode":4 49 } 50 51 biz_content.update(kwargs) 52 data = self.build_body("alipay.trade.page.pay", biz_content, self.return_url) 53 return self.sign_data(data) 54 55 def build_body(self, method, biz_content, return_url=None): 56 data = { 57 "app_id": self.appid, 58 "method": method, 59 "charset": "utf-8", 60 "sign_type": "RSA2", 61 "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 62 "version": "1.0", 63 "biz_content": biz_content 64 } 65 66 if return_url is not None: 67 data["notify_url"] = self.app_notify_url 68 data["return_url"] = self.return_url 69 70 return data 71 72 def sign_data(self, data): 73 data.pop("sign", None) 74 # 排序后的字符串 75 unsigned_items = self.ordered_data(data) 76 unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in unsigned_items) 77 sign = self.sign(unsigned_string.encode("utf-8")) 78 ordered_items = self.ordered_data(data) 79 quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in ordered_items) 80 81 # 獲得最終的訂單信息字符串 82 signed_string = quoted_string + "&sign=" + quote_plus(sign) 83 return signed_string 84 85 def ordered_data(self, data): 86 complex_keys = [] 87 for key, value in data.items(): 88 if isinstance(value, dict): 89 complex_keys.append(key) 90 91 # 將字典類型的數據dump出來 92 for key in complex_keys: 93 data[key] = json.dumps(data[key], separators=(',', ':')) 94 95 return sorted([(k, v) for k, v in data.items()]) 96 97 def sign(self, unsigned_string): 98 # 開始計算簽名 99 key = self.app_private_key 100 signer = PKCS1_v1_5.new(key) 101 signature = signer.sign(SHA256.new(unsigned_string)) 102 # base64 編碼,轉換為unicode表示並移除回車 103 sign = encodebytes(signature).decode("utf8").replace("\n", "") 104 return sign 105 106 def _verify(self, raw_content, signature): 107 # 開始計算簽名 108 key = self.alipay_public_key 109 signer = PKCS1_v1_5.new(key) 110 digest = SHA256.new() 111 digest.update(raw_content.encode("utf8")) 112 if signer.verify(digest, decodebytes(signature.encode("utf8"))): 113 return True 114 return False 115 116 def verify(self, data, signature): 117 if "sign_type" in data: 118 sign_type = data.pop("sign_type") 119 # 排序后的字符串 120 unsigned_items = self.ordered_data(data) 121 message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items) 122 return self._verify(message, signature) 123 124 125 if __name__ == "__main__": 126 alipay = AliPay( 127 appid="2016081500252338", 128 app_notify_url="http://projectsedus.com/", 129 app_private_key_path="keys/private_2048.txt", 130 alipay_public_key_path="keys/alipay_key_2048.txt", # 支付寶的公鑰,驗證支付寶回傳消息使用,不是你自己的公鑰, 131 debug=True, # 默認False, 132 return_url="http://192.168.247.129:8000/" 133 ) 134 135 url = alipay.direct_pay( 136 subject="測試訂單", 137 out_trade_no="20170202126666", 138 total_amount=1000 139 ) 140 re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url) 141 print(re_url)
5、wxpay.py:

1 # -*- coding=utf-8 -*- 2 3 import time 4 import json 5 import hashlib 6 import requests 7 8 from pay.utils import (smart_str, dict_to_xml, calculate_sign, random_str, 9 post_xml, xml_to_dict, validate_post_xml, format_url) 10 11 # from local_settings import appid, mch_id, api_key 12 13 14 OAUTH2_AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?%s" 15 OAUTH2_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?%s" 16 17 18 class WeiXinPay(object): 19 def __init__(self, appid, mch_id, api_key): 20 self.appid = appid # 微信公眾號身份的唯一標識。審核通過后,在微信發送的郵件中查看 21 self.mch_id = mch_id # 受理商ID,身份標識 22 self.api_key = api_key # 商戶支付密鑰Key。審核通過后,在微信發送的郵件中查看 23 self.common_params = { 24 "appid": self.appid, 25 "mch_id": self.mch_id, 26 } 27 self.params = {} 28 self.url = "" 29 self.trade_type = "" 30 31 def set_params(self, **kwargs): 32 self.params = {} 33 for (k, v) in kwargs.items(): 34 self.params[k] = smart_str(v) 35 36 self.params["nonce_str"] = random_str(32) 37 if self.trade_type: 38 self.params["trade_type"] = self.trade_type 39 self.params.update(self.common_params) 40 41 def post_xml(self): 42 sign = calculate_sign(self.params, self.api_key) 43 xml = dict_to_xml(self.params, sign) 44 response = post_xml(self.url, xml) 45 return xml_to_dict(response.text) 46 47 def valiate_xml(self, xml): 48 return validate_post_xml(xml, self.appid, self.mch_id, self.api_key) 49 50 def get_error_code_desc(self, error_code): 51 error_desc = { 52 "SYSTEMERROR": u"接口后台錯誤", 53 "INVALID_TRANSACTIONID": u"無效 transaction_id", 54 "PARAM_ERROR": u"提交參數錯誤", 55 "ORDERPAID": u"訂單已支付", 56 "OUT_TRADE_NO_USED": u"商戶訂單號重復", 57 "NOAUTH": u"商戶無權限", 58 "NOTENOUGH": u"余額丌足", 59 "NOTSUPORTCARD": u"不支持卡類型", 60 "ORDERCLOSED": u"訂單已關閉", 61 "BANKERROR": u"銀行系統異常", 62 "REFUND_FEE_INVALID": u"退款金額大亍支付金額", 63 "ORDERNOTEXIST": u"訂單不存在", 64 } 65 return error_desc.get(error_code.strip().upper(), u"未知錯誤") 66 67 68 class UnifiedOrderPay(WeiXinPay): 69 """發送預支付單""" 70 71 def __init__(self, appid, mch_id, api_key): 72 super(UnifiedOrderPay, self).__init__(appid, mch_id, api_key) 73 self.url = "https://api.mch.weixin.qq.com/pay/unifiedorder" 74 self.trade_type = "NATIVE" 75 76 def post(self, body, out_trade_no, total_fee, spbill_create_ip, notify_url, **kwargs): 77 tmp_kwargs = { 78 "body": body, 79 "out_trade_no": out_trade_no, 80 "total_fee": total_fee, 81 "spbill_create_ip": spbill_create_ip, 82 "notify_url": notify_url, 83 } 84 tmp_kwargs.update(**kwargs) 85 self.set_params(**tmp_kwargs) 86 return self.post_xml()[1] 87 88 89 class OrderQuery(WeiXinPay): 90 """訂單狀態查詢""" 91 92 def __init__(self, appid, mch_id, api_key): 93 super(OrderQuery, self).__init__(appid, mch_id, api_key) 94 self.url = "https://api.mch.weixin.qq.com/pay/orderquery" 95 96 def post(self, out_trade_no): 97 self.set_params(out_trade_no=out_trade_no) 98 return self.post_xml()[1] 99 100 101 class JsAPIOrderPay(UnifiedOrderPay): 102 """H5頁面的Js調用類""" 103 104 def __init__(self, appid, mch_id, api_key, app_secret): 105 super(JsAPIOrderPay, self).__init__(appid, mch_id, api_key) 106 self.app_secret = app_secret 107 self.trade_type = "JSAPI" 108 109 def create_oauth_url_for_code(self, redirect_uri): 110 url_params = { 111 "appid": self.appid, 112 "redirect_uri": redirect_uri, # 一般是回調當前頁面 113 "response_type": "code", 114 "scope": "snsapi_base", 115 "state": "STATE#wechat_redirect" 116 } 117 url = format_url(url_params) 118 return OAUTH2_AUTHORIZE_URL % url 119 120 def _create_oauth_url_for_openid(self, code): 121 url_params = { 122 "appid": self.appid, 123 "secret": self.app_secret, 124 "code": code, 125 "grant_type": "authorization_code", 126 } 127 url = format_url(url_params) 128 return OAUTH2_ACCESS_TOKEN_URL % url 129 130 def _get_oauth_info(self, code): 131 """ 132 獲取OAuth2的信息:access_token、expires_in、refresh_token、openid、scope 133 返回結果為字典,可使用["xxx"]或.get("xxx", None)的方式進行讀取 134 """ 135 url = self._create_oauth_url_for_openid(code) 136 response = requests.get(url) 137 return response.json() if response else None 138 139 def _get_openid(self, code): 140 oauth_info = self._get_oauth_info(code) 141 if oauth_info: 142 return oauth_info.get("openid", None) 143 return None 144 145 def _get_json_js_api_params(self, prepay_id): 146 js_params = { 147 "appId": self.appid, 148 "timeStamp": "%d" % time.time(), 149 "nonceStr": random_str(32), 150 "package": "prepay_id=%s" % prepay_id, 151 "signType": "MD5", 152 } 153 js_params["paySign"] = calculate_sign(js_params, self.api_key) 154 return js_params 155 156 def post(self, body, out_trade_no, total_fee, spbill_create_ip, notify_url, code): 157 if code: 158 open_id = self._get_openid(code) 159 if open_id: 160 # 直接調用基類的post方法查詢prepay_id,如果成功,返回一個字典 161 unified_order = super(JsAPIOrderPay, self).post(body, out_trade_no, total_fee, spbill_create_ip, 162 notify_url, open_id=open_id) 163 if unified_order: 164 prepay_id = unified_order.get("prepay_id", None) 165 if prepay_id: 166 return self._get_json_js_api_params(prepay_id) 167 return None
6、utils.py:
1 # -*- coding=utf-8 -*- 2 import hashlib 3 import re 4 import types 5 from random import Random 6 import requests 7 import thriftpy 8 9 from django.conf import settings 10 from django.core.exceptions import FieldDoesNotExist 11 from django.db import models 12 from django.db.models.fields.reverse_related import ForeignObjectRel 13 from rest_framework.pagination import PageNumberPagination 14 from thriftpy.rpc import make_client 15 16 from pay.exception_handler import ForeignObjectRelDeleteError, ModelDontHaveIsActiveFiled, logger 17 18 19 def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'): 20 """ 21 Returns a bytestring version of 's', encoded as specified in 'encoding'. 22 If strings_only is True, don't convert (some) non-string-like objects. 23 """ 24 if strings_only and isinstance(s, (types.NoneType, int)): 25 return s 26 if not isinstance(s, str): 27 try: 28 return str(s) 29 except UnicodeEncodeError: 30 if isinstance(s, Exception): 31 # An Exception subclass containing non-ASCII data that doesn't 32 # know how to print itself properly. We shouldn't raise a 33 # further exception. 34 return ' '.join([smart_str(arg, encoding, strings_only, 35 errors) for arg in s]) 36 return unicode(s).encode(encoding, errors) 37 elif s and encoding != 'utf-8': 38 return s.decode('utf-8', errors).encode(encoding, errors) 39 else: 40 return s 41 42 43 def format_url(params, api_key=None): 44 url = "&".join(['%s=%s' % (key, smart_str(params[key])) for key in sorted(params)]) 45 if api_key: 46 url = '%s&key=%s' % (url, api_key) 47 return url 48 49 50 def calculate_sign(params, api_key): 51 # 簽名步驟一:按字典序排序參數, 在string后加入KEY 52 url = format_url(params, api_key) 53 # 簽名步驟二:MD5加密, 所有字符轉為大寫 54 return hashlib.md5(url.encode('utf-8')).hexdigest().upper() 55 56 57 def dict_to_xml(params, sign): 58 xml = ["<xml>", ] 59 for (k, v) in params.items(): 60 if (v.isdigit()): 61 xml.append('<%s>%s</%s>' % (k, v, k)) 62 else: 63 xml.append('<%s><![CDATA[%s]]></%s>' % (k, v, k)) 64 xml.append('<sign><![CDATA[%s]]></sign></xml>' % sign) 65 return ''.join(xml) 66 67 68 def xml_to_dict(xml): 69 if xml[0:5].upper() != "<XML>" and xml[-6].upper() != "</XML>": 70 return None, None 71 72 result = {} 73 sign = None 74 content = ''.join(xml[5:-6].strip().split('\n')) 75 76 pattern = re.compile(r"<(?P<key>.+)>(?P<value>.+)</(?P=key)>") 77 m = pattern.match(content) 78 while (m): 79 key = m.group("key").strip() 80 value = m.group("value").strip() 81 if value != "<![CDATA[]]>": 82 pattern_inner = re.compile(r"<!\[CDATA\[(?P<inner_val>.+)\]\]>") 83 inner_m = pattern_inner.match(value) 84 if inner_m: 85 value = inner_m.group("inner_val").strip() 86 if key == "sign": 87 sign = value 88 else: 89 result[key] = value 90 91 next_index = m.end("value") + len(key) + 3 92 if next_index >= len(content): 93 break 94 content = content[next_index:] 95 m = pattern.match(content) 96 97 return sign, result 98 99 100 def validate_post_xml(xml, appid, mch_id, api_key): 101 sign, params = xml_to_dict(xml) 102 if (not sign) or (not params): 103 return None 104 105 remote_sign = calculate_sign(params, api_key) 106 if sign != remote_sign: 107 return None 108 109 if params["appid"] != appid or params["mch_id"] != mch_id: 110 return None 111 112 return params 113 114 115 def random_str(randomlength=8): 116 chars = 'abcdefghijklmnopqrstuvwxyz0123456789' 117 random = Random() 118 return "".join([chars[random.randint(0, len(chars) - 1)] for i in range(randomlength)]) 119 120 121 def post_xml(url, xml): 122 return requests.post(url, data=xml.encode('utf-8'), verify=False) 123 124 125 class UnActiveModelMixin(object): 126 """ 127 刪除一個對象,並不真刪除,級聯將對應外鍵對象的is_active設置為false,需要外鍵對象都有is_active字段. 128 """ 129 130 def perform_destroy(self, instance): 131 rel_fileds = [f for f in instance._meta.get_fields() if isinstance(f, ForeignObjectRel)] 132 133 links = [f.get_accessor_name() for f in rel_fileds] 134 135 for link in links: 136 manager = getattr(instance, link, None) 137 if not manager: 138 continue 139 if isinstance(manager, models.Model): 140 if hasattr(manager, 'is_active') and manager.is_active: 141 manager.is_active = False 142 manager.save() 143 raise ForeignObjectRelDeleteError(u'{} 上有關聯數據'.format(link)) 144 else: 145 if not manager.count(): 146 continue 147 try: 148 manager.model._meta.get_field('is_active') 149 manager.filter(is_active=True).update(is_active=False) 150 except FieldDoesNotExist as ex: 151 # 理論上,級聯刪除的model上面應該也有is_active字段,否則代碼邏輯應該有問題 152 logger.warn(ex) 153 raise ModelDontHaveIsActiveFiled( 154 '{}.{} 沒有is_active字段, 請檢查程序邏輯'.format( 155 manager.model.__module__, 156 manager.model.__class__.__name__ 157 )) 158 instance.is_active = False 159 instance.save() 160 161 def get_queryset(self): 162 return self.queryset.filter(is_active=True) 163 164 165 class StandardResultsSetPagination(PageNumberPagination): 166 page_size_query_param = 'size'