├── tests └── __init__.py ├── setup.cfg ├── openunipay ├── api │ ├── __init__.py │ ├── views_alipay.py │ ├── urls.py │ └── views_weixin.py ├── util │ ├── __init__.py │ ├── random_helper.py │ └── datetime.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── createadminuser.py ├── migrations │ ├── __init__.py │ ├── 0002_remove_alipayorder_interface_data.py │ ├── 0003_auto_20170117_1106.py │ ├── 0006_auto_20170722_0039.py │ ├── 0005_product.py │ ├── 0004_weixinqrpayentity_weixinqrpayrecord.py │ └── 0001_initial.py ├── weixin_pay │ ├── qrpay.py │ ├── __init__.py │ ├── exceptions.py │ ├── security.py │ ├── xml_helper.py │ ├── admin.py │ ├── models.py │ └── weixin_pay_lib.py ├── __init__.py ├── ali_pay │ ├── __init__.py │ ├── admin.py │ ├── security.py │ ├── ali_pay_lib.py │ └── models.py ├── apps.py ├── exceptions.py ├── wsgi.py ├── urls.py ├── paygateway │ ├── alipay.py │ ├── __init__.py │ ├── weixin.py │ └── unipay.py ├── admin.py ├── models.py └── settings.py ├── MANIFEST.in ├── requirements.txt ├── .gitignore ├── manage.py ├── .vscode ├── settings.json └── launch.json ├── LICENSE.txt ├── setup.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] -------------------------------------------------------------------------------- /openunipay/api/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openunipay/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openunipay/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openunipay/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openunipay/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openunipay/weixin_pay/qrpay.py: -------------------------------------------------------------------------------- 1 | #### 扫码支付 目前只支持模式1 2 | 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE.txt -------------------------------------------------------------------------------- /openunipay/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger('openunipay') -------------------------------------------------------------------------------- /openunipay/weixin_pay/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'weixin_pay.apps.WeixinPayAppConfig' -------------------------------------------------------------------------------- /openunipay/ali_pay/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger('openunipay.ali') -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | django-cors-headers 3 | djangorestframework 4 | djangorestframework-xml 5 | django-import-export 6 | requests 7 | pillow 8 | rsa -------------------------------------------------------------------------------- /openunipay/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.apps import AppConfig 4 | 5 | class WeixinPayAppConfig(AppConfig): 6 | name = 'openunipay' 7 | verbose_name = u'openunipay统一支付' 8 | -------------------------------------------------------------------------------- /openunipay/weixin_pay/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from rest_framework.exceptions import APIException 3 | 4 | class APIError(APIException): 5 | status_code = 500 6 | default_detail = u'微信 API调用失败' -------------------------------------------------------------------------------- /openunipay/util/random_helper.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | 4 | _NONCE_POPULATION = '123457890ABCDEFGHIJKLMNOPQRSTUVWXYZ' 5 | 6 | def generate_nonce_str(length): 7 | return ''.join(random.sample(_NONCE_POPULATION, length)) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /__pycache__/ 2 | /.settings/ 3 | /files/ 4 | *.pyc 5 | *.sqlite 6 | *.sqlite3 7 | *.idea 8 | *.pyproj.user 9 | *.pydevproject 10 | *.cmd 11 | .project 12 | .pydevproject 13 | .pypirc 14 | /dist/ 15 | /build/ 16 | /key/ 17 | /openunipay.egg-info/ 18 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openunipay.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /openunipay/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | class PayWayError(Exception): 3 | pass 4 | 5 | class InsecureDataError(Exception): 6 | pass 7 | 8 | class PayProcessError(Exception): 9 | 10 | def __init__(self, message): 11 | self.message = message 12 | 13 | def __str__(self, *args, **kwargs): 14 | return repr(self.message) 15 | -------------------------------------------------------------------------------- /openunipay/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for openunipay project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "openunipay.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /openunipay/api/views_alipay.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.http import HttpResponse 3 | from openunipay.models import PAY_WAY_ALI 4 | from openunipay.paygateway import unipay 5 | 6 | _logger = logging.getLogger('openunipay_ali_pay_notificaiton') 7 | 8 | def process_notify(request): 9 | _logger.info('received ali pay notification.body:{}'.format(request.body)) 10 | unipay.process_notify(PAY_WAY_ALI, request) 11 | return HttpResponse('success', 'text/plain-text', 200) 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "D:/projects/commonlib/venv/Scripts/python.exe", 3 | "python.linting.pylintEnabled": false, 4 | "python.formatting.provider": "yapf", 5 | "python.unitTest.unittestArgs": [ 6 | "-v", 7 | "-s", 8 | ".\\tests", 9 | "-p", 10 | "test*.py" 11 | ], 12 | "python.unitTest.unittestEnabled": false, 13 | "python.unitTest.promptToConfigure": false, 14 | "python.unitTest.pyTestEnabled": false 15 | } -------------------------------------------------------------------------------- /openunipay/weixin_pay/security.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from django.conf import settings 3 | 4 | def sign(valueDict): 5 | temp = [] 6 | for key in sorted(valueDict): 7 | if not valueDict[key]: 8 | continue 9 | temp.append('{}={}'.format(key, valueDict[key])) 10 | temp.append('key=' + settings.WEIXIN['mch_seckey']) 11 | tempStr = '&'.join(temp) 12 | m = hashlib.md5() 13 | m.update(tempStr.encode()) 14 | return m.hexdigest().upper() 15 | -------------------------------------------------------------------------------- /openunipay/util/datetime.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from time import time 3 | import datetime 4 | 5 | def utc_now(): 6 | return datetime.datetime.utcnow() 7 | 8 | def local_now(): 9 | return datetime.datetime.now() 10 | 11 | def now_str(): 12 | return local_now().strftime("%Y-%m-%d %H:%M:%S") 13 | 14 | def get_timestamp(): 15 | return int(time()) 16 | 17 | def get_unix_timestamp(): 18 | return int((utc_now() - datetime.datetime(1970, 1, 1, tzinfo=None)).total_seconds()) 19 | -------------------------------------------------------------------------------- /openunipay/migrations/0002_remove_alipayorder_interface_data.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2016-05-18 23:33 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('openunipay', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='alipayorder', 17 | name='interface_data', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /openunipay/management/commands/createadminuser.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Created on Nov 16, 2015 3 | 4 | @author: panweif 5 | ''' 6 | from django.core.management.base import BaseCommand 7 | from django.contrib.auth.models import User 8 | 9 | class Command(BaseCommand): 10 | help = 'create amdin superuser with password 123' 11 | 12 | def add_arguments(self, parser): 13 | pass 14 | 15 | def handle(self, *args, **options): 16 | User.objects.create_superuser('admin', '', '123') 17 | self.stdout.write('admin user created successfully') 18 | -------------------------------------------------------------------------------- /openunipay/migrations/0003_auto_20170117_1106.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-01-17 11:06 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('openunipay', '0002_remove_alipayorder_interface_data'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='weixinorder', 17 | name='total_fee', 18 | field=models.PositiveIntegerField(editable=False, verbose_name='总金额'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Django", 6 | "type": "python", 7 | "request": "launch", 8 | "stopOnEntry": false, 9 | "pythonPath": "${config:python.pythonPath}", 10 | "program": "${workspaceRoot}/manage.py", 11 | "args": [ 12 | "runserver", 13 | "--noreload" 14 | ], 15 | "debugOptions": [ 16 | "WaitOnAbnormalExit", 17 | "WaitOnNormalExit", 18 | "RedirectOutput", 19 | "DjangoDebugging" 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /openunipay/weixin_pay/xml_helper.py: -------------------------------------------------------------------------------- 1 | from xml.dom.minidom import getDOMImplementation 2 | from xml.etree import ElementTree 3 | 4 | _xmldom_impl = getDOMImplementation() 5 | 6 | def dict_to_xml(valueDict): 7 | doc = _xmldom_impl.createDocument(None, 'xml', None) 8 | topElement = doc.documentElement 9 | for (key, value) in valueDict.items(): 10 | element = doc.createElement(key) 11 | element.appendChild(doc.createTextNode(str(value))) 12 | topElement.appendChild(element) 13 | return topElement.toxml() 14 | 15 | def xml_to_dict(xmlContent): 16 | result = {} 17 | root = ElementTree.fromstring(xmlContent) 18 | for child in root: 19 | result[child.tag] = child.text 20 | return result -------------------------------------------------------------------------------- /openunipay/migrations/0006_auto_20170722_0039.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2017-07-22 00:39 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('openunipay', '0005_product'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='weixinorder', 17 | name='openid', 18 | field=models.CharField(blank=True, editable=False, max_length=128, null=True, verbose_name='用户标识(openId)'), 19 | ), 20 | migrations.AlterField( 21 | model_name='product', 22 | name='productid', 23 | field=models.CharField(max_length=50, primary_key=True, serialize=False, verbose_name='商品ID'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /openunipay/urls.py: -------------------------------------------------------------------------------- 1 | """openunipay URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.9/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Import the include() function: from django.conf.urls import url, include 15 | 3. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 16 | """ 17 | from django.conf.urls import include, url 18 | from django.contrib import admin 19 | 20 | urlpatterns = [ 21 | url(r'^admin/', admin.site.urls), 22 | url(r'^api/v1.0/', include('openunipay.api.urls')), 23 | ] 24 | -------------------------------------------------------------------------------- /openunipay/ali_pay/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.contrib import admin 3 | from .models import AliPayOrder 4 | from import_export import resources 5 | from import_export.admin import ImportExportModelAdmin 6 | 7 | class AliPayOrderResource(resources.ModelResource): 8 | class Meta: 9 | model = AliPayOrder 10 | import_id_fields = ('id',) 11 | 12 | class AliPayOrderAdmin(ImportExportModelAdmin): 13 | resource_class = AliPayOrderResource 14 | # list page 15 | list_display = ('date_create', 'out_trade_no', 'subject', 'body', 'total_fee', 'it_b_pay', 'get_pay_result',) 16 | ordering = ('-date_create',) 17 | search_fields = ['=out_trade_no', ] 18 | 19 | def get_pay_result(self, obj): 20 | if hasattr(obj, 'pay_result'): 21 | return obj.pay_result 22 | else: 23 | return '-' 24 | get_pay_result.short_description = '支付结果' 25 | 26 | 27 | admin.site.register(AliPayOrder, AliPayOrderAdmin) 28 | -------------------------------------------------------------------------------- /openunipay/api/urls.py: -------------------------------------------------------------------------------- 1 | """uimbank_server URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from django.conf.urls import url 17 | from openunipay.api import views_alipay, views_weixin 18 | 19 | urlpatterns = [ 20 | url(r'^notify/weixin/$', views_weixin.process_notify), 21 | url(r'^notify/alipay/$', views_alipay.process_notify), 22 | url(r'^qrnotify/weixin/$', views_weixin.process_qr_notify), 23 | ] 24 | -------------------------------------------------------------------------------- /openunipay/api/views_weixin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from django.http import HttpResponse 3 | from openunipay.models import PAY_WAY_WEIXIN 4 | from openunipay.paygateway import unipay 5 | 6 | _logger = logging.getLogger('weixin_pay_notificaiton') 7 | 8 | 9 | def process_notify(request): 10 | _logger.info('received weixin pay notification.body:{}'.format(request.body)) 11 | unipay.process_notify(PAY_WAY_WEIXIN, request.body) 12 | return HttpResponse('', 'application/xml', 200) 13 | 14 | 15 | def process_qr_notify(request): 16 | _logger.info('received weixin qr pay notification.body:{}'.format(request.body)) 17 | try: 18 | responseData = unipay.process_qr_pay_notify(PAY_WAY_WEIXIN, request.body) 19 | return HttpResponse(responseData, 'application/xml', 200) 20 | except: 21 | _logger.exception('process qr notify failed') 22 | return HttpResponse(None, 500) 23 | -------------------------------------------------------------------------------- /openunipay/weixin_pay/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.contrib import admin 3 | from .models import WeiXinOrder 4 | from import_export import resources 5 | from import_export.admin import ImportExportModelAdmin 6 | 7 | class WeiXinOrderResource(resources.ModelResource): 8 | class Meta: 9 | model = WeiXinOrder 10 | import_id_fields = ('id',) 11 | 12 | class WeiXinOrderAdmin(ImportExportModelAdmin): 13 | resource_class = WeiXinOrderResource 14 | # list page 15 | list_display = ('out_trade_no', 'body', 'attach', 'fee_type', 'total_fee', 'spbill_create_ip', 'time_start', 'time_expire', 'trade_type', 'get_pay_result') 16 | ordering = ('time_start',) 17 | search_fields = ['=out_trade_no', '=spbill_create_ip', ] 18 | 19 | def get_pay_result(self, obj): 20 | if hasattr(obj, 'pay_result'): 21 | return obj.pay_result 22 | else: 23 | return '-' 24 | get_pay_result.short_description = '支付结果' 25 | 26 | 27 | admin.site.register(WeiXinOrder, WeiXinOrderAdmin) 28 | -------------------------------------------------------------------------------- /openunipay/paygateway/alipay.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from . import PayGateway 3 | from django.db import transaction 4 | from ..ali_pay.models import AliPayOrder 5 | from ..ali_pay import ali_pay_lib 6 | 7 | class AliPayGateway(PayGateway): 8 | 9 | @transaction.atomic 10 | def create_order(self, orderItemObj, clientIp): 11 | aliOrderObj = AliPayOrder() 12 | aliOrderObj.out_trade_no = orderItemObj.orderno 13 | aliOrderObj.subject = orderItemObj.product_desc 14 | aliOrderObj.body = orderItemObj.product_detail 15 | aliOrderObj.total_fee = orderItemObj.fee / 100 16 | aliOrderObj.it_b_pay = settings.ALIPAY['order_expire_in'] 17 | aliOrderObj.save() 18 | ali_pay_lib.create_order(aliOrderObj) 19 | return aliOrderObj.compose_interface_data() 20 | 21 | @transaction.atomic 22 | def process_notify(self, requestContent): 23 | return ali_pay_lib.process_notify(requestContent) 24 | 25 | @transaction.atomic 26 | def query_order(self, orderNo): 27 | return ali_pay_lib.query_order(orderNo) 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Pan.weifeng@live.cn 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /openunipay/migrations/0005_product.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2017-07-21 12:30 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('openunipay', '0004_weixinqrpayentity_weixinqrpayrecord'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Product', 17 | fields=[ 18 | ('productid', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False, verbose_name='商品ID')), 19 | ('product_desc', models.CharField(max_length=128, verbose_name='商品描述')), 20 | ('product_detail', models.TextField(max_length=1000, verbose_name='商品详情')), 21 | ('fee', models.DecimalField(decimal_places=0, max_digits=6, verbose_name='金额(单位:分)')), 22 | ('weinxin_qrurl', models.CharField(blank=True, max_length=500, null=True, verbose_name='微信扫码支付URL')), 23 | ('date_create', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), 24 | ('date_update', models.DateTimeField(auto_now=True, verbose_name='修改时间')), 25 | ], 26 | options={ 27 | 'verbose_name_plural': '商品', 28 | 'verbose_name': '商品', 29 | }, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /openunipay/paygateway/__init__.py: -------------------------------------------------------------------------------- 1 | class PayGateway(object): 2 | ''' 3 | @summary: the base class for pay gateway 4 | ''' 5 | 6 | def create_order(self, orderItemObj, clientIp, **kwargs): 7 | pass 8 | 9 | def query_order(self, orderNo): 10 | ''' 11 | @summary: query pay result of order 12 | @return: PayResult 13 | ''' 14 | pass 15 | 16 | def process_notify(self, requestContent): 17 | ''' 18 | @summary: process notify from pay interface 19 | @return: PayResult 20 | ''' 21 | pass 22 | 23 | def generate_qr_pay_url(self, productid): 24 | ''' 25 | @summary: create url that can be used to generate qr code 26 | @return: url 27 | ''' 28 | pass 29 | 30 | def process_qr_pay_notify(self, requestContent): 31 | ''' 32 | @summary: process qr notify 33 | @return: proudct id,uid 34 | ''' 35 | pass 36 | 37 | 38 | class PayResult(object): 39 | def __init__(self, orderNo, succ=True, lapsed=False): 40 | self.orderno = orderNo 41 | self.succ = succ 42 | self.lapsed = lapsed 43 | 44 | @property 45 | def OrderNo(self): 46 | ''' 47 | @summary: order No or merchant 48 | ''' 49 | return self.orderno 50 | 51 | @property 52 | def Succ(self): 53 | ''' 54 | @summary: True: paid successfully 55 | ''' 56 | return self.succ 57 | 58 | @property 59 | def Lapsed(self): 60 | ''' 61 | @summary: True: order is lapsed 62 | ''' 63 | return self.lapsed 64 | -------------------------------------------------------------------------------- /openunipay/ali_pay/security.py: -------------------------------------------------------------------------------- 1 | import rsa 2 | import base64 3 | from django.conf import settings 4 | from openunipay.ali_pay import logger 5 | 6 | def sign(data): 7 | privateKey = _load_private_key(settings.ALIPAY['rsa_private_key_pem']) 8 | signBytes = rsa.sign(data.encode(), privateKey, 'SHA-1') 9 | signStr = str(base64.b64encode(signBytes), 'utf-8') 10 | return signStr 11 | 12 | def verify(data, sign, pemKeyfile): 13 | sign = base64.b64decode(sign) 14 | pubKey = _load_public_key(pemKeyfile) 15 | result = False 16 | try: 17 | rsa.verify(data.encode(), sign, pubKey) 18 | except rsa.pkcs1.VerificationError: 19 | result = False 20 | else: 21 | result = True 22 | return result 23 | 24 | 25 | def verify_ali_data(valueDict): 26 | logger.info('verifying data from ali') 27 | sign = valueDict['sign'] 28 | # remove sign and sign_type 29 | del valueDict['sign'] 30 | if 'sign_type' in valueDict: 31 | del valueDict['sign_type'] 32 | # contact string need to verify 33 | temp = [] 34 | for key in sorted(valueDict): 35 | if not valueDict[key]: 36 | continue 37 | temp.append('{}={}'.format(key, valueDict[key])) 38 | tempStr = '&'.join(temp) 39 | logger.info('string to verify:{}'.format(tempStr)) 40 | return verify(tempStr, sign, settings.ALIPAY['ali_public_key_pem']) 41 | 42 | 43 | def _load_private_key(pemKeyfile): 44 | with open(pemKeyfile) as keyFile: 45 | return rsa.PrivateKey.load_pkcs1(keyFile.read().encode('latin_1')) 46 | 47 | def _load_public_key(pemKeyfile): 48 | with open(pemKeyfile) as keyFile: 49 | return rsa.PublicKey.load_pkcs1_openssl_pem(keyFile.read().encode('latin_1')) 50 | 51 | -------------------------------------------------------------------------------- /openunipay/migrations/0004_weixinqrpayentity_weixinqrpayrecord.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.3 on 2017-07-21 00:41 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('openunipay', '0003_auto_20170117_1106'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='WeiXinQRPayEntity', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('appid', models.CharField(editable=False, max_length=32, verbose_name='公众账号ID')), 20 | ('mch_id', models.CharField(editable=False, max_length=32, verbose_name='商户号')), 21 | ('time_stamp', models.CharField(editable=False, max_length=10, verbose_name='时间戳')), 22 | ('product_id', models.CharField(editable=False, max_length=32, verbose_name='商品ID')), 23 | ], 24 | options={ 25 | 'verbose_name_plural': '微信扫码支付-二维码URL', 26 | 'verbose_name': '微信扫码支付-二维码URL', 27 | }, 28 | ), 29 | migrations.CreateModel( 30 | name='WeiXinQRPayRecord', 31 | fields=[ 32 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 33 | ('appid', models.CharField(editable=False, max_length=32, verbose_name='公众账号ID')), 34 | ('mch_id', models.CharField(editable=False, max_length=32, verbose_name='商户号')), 35 | ('openid', models.CharField(editable=False, max_length=128, verbose_name='用户标识')), 36 | ('product_id', models.CharField(editable=False, max_length=32, verbose_name='商品ID')), 37 | ], 38 | options={ 39 | 'verbose_name_plural': '微信扫码支付-扫码纪录', 40 | 'verbose_name': '微信扫码支付-扫码纪录', 41 | }, 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /openunipay/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.contrib import admin 3 | from .ali_pay.admin import * 4 | from .weixin_pay.admin import * 5 | 6 | from .models import OrderItem, Product, PAY_WAY_WEIXIN 7 | from openunipay.paygateway import unipay 8 | 9 | 10 | class OrderItemResource(resources.ModelResource): 11 | class Meta: 12 | model = OrderItem 13 | import_id_fields = ('orderno', ) 14 | 15 | 16 | class OrderItemAdmin(ImportExportModelAdmin): 17 | resource_class = OrderItemResource 18 | # list page 19 | list_display = ('orderno', 'user', 'product_desc', 'payway', 'fee', 'dt_start', 'dt_end', 'dt_pay', 'paied') 20 | ordering = ('-dt_start', ) 21 | search_fields = ['=orderno', 22 | '=user', ] 23 | list_filter = ('payway', 24 | 'paied', 25 | 'product_desc', ) 26 | 27 | def save_model(self, request, obj, form, change): 28 | if not obj.orderno: 29 | obj.initial_orlder() 30 | admin.ModelAdmin.save_model(self, request, obj, form, change) 31 | 32 | 33 | ########################### product ######################### 34 | class ProductResource(resources.ModelResource): 35 | class Meta: 36 | model = OrderItem 37 | import_id_fields = ('productid', ) 38 | 39 | 40 | class ProductAdmin(ImportExportModelAdmin): 41 | resource_class = ProductResource 42 | # list page 43 | list_display = ('productid', 44 | 'product_desc', 45 | 'fee', 46 | 'weinxin_qrurl', 47 | 'date_create', 48 | 'date_update', ) 49 | ordering = ('-date_create', ) 50 | search_fields = ['=productid', ] 51 | 52 | def save_model(self, request, obj, form, change): 53 | if not change: 54 | obj.weinxin_qrurl = unipay.generate_qr_pay_url(PAY_WAY_WEIXIN, obj.productid) 55 | admin.ModelAdmin.save_model(self, request, obj, form, change) 56 | 57 | 58 | admin.site.register(OrderItem, OrderItemAdmin) 59 | admin.site.register(Product, ProductAdmin) 60 | -------------------------------------------------------------------------------- /openunipay/ali_pay/ali_pay_lib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .models import AliPayOrder, AliPayResult 3 | from openunipay.paygateway import PayResult 4 | from openunipay.ali_pay import logger, security 5 | from openunipay import exceptions 6 | 7 | TRADE_STATUS_SUCC = ('TRADE_SUCCESS', 'TRADE_FINISHED',) 8 | TRADE_STATUS_LAPSED = ('TRADE_CLOSED',) 9 | 10 | def create_order(orderObj): 11 | assert isinstance(orderObj, AliPayOrder) 12 | AliPayResult.objects.create(order=orderObj) 13 | 14 | def process_notify(request): 15 | valueDict = request.POST.dict() 16 | if security.verify_ali_data(valueDict): 17 | # save data 18 | orderObj = AliPayOrder.objects.get(out_trade_no=valueDict['out_trade_no']) 19 | payResultObj = orderObj.pay_result 20 | payResultObj.out_trade_no = valueDict.get('out_trade_no') 21 | payResultObj.notify_time = valueDict.get('notify_time') 22 | payResultObj.notify_type = valueDict.get('notify_type') 23 | payResultObj.notify_id = valueDict.get('notify_id') 24 | payResultObj.subject = valueDict.get('subject') 25 | payResultObj.trade_no = valueDict.get('trade_no') 26 | payResultObj.trade_status = valueDict.get('trade_status') 27 | payResultObj.seller_id = valueDict.get('seller_id') 28 | payResultObj.seller_email = valueDict.get('seller_email') 29 | payResultObj.buyer_id = valueDict.get('buyer_id') 30 | payResultObj.buyer_email = valueDict.get('buyer_email') 31 | payResultObj.total_fee = valueDict.get('total_fee') 32 | payResultObj.save() 33 | result = _compose_pay_result(payResultObj.out_trade_no, payResultObj.trade_status) 34 | return result 35 | else: 36 | logger.error('received unverified notification:{}'.format(valueDict)) 37 | raise exceptions.InsecureDataError() 38 | 39 | def query_order(orderNo): 40 | orderObj = AliPayOrder.objects.get(out_trade_no=orderNo) 41 | result = _compose_pay_result(orderObj.out_trade_no, orderObj.pay_result.trade_status) 42 | return result 43 | 44 | def _compose_pay_result(orderNo, trade_status): 45 | result = PayResult(orderNo) 46 | result.succ = trade_status in TRADE_STATUS_SUCC 47 | result.lapsed = trade_status in TRADE_STATUS_LAPSED 48 | return result 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup, find_packages 3 | from codecs import open 4 | from os import path 5 | 6 | here = path.abspath(path.dirname(__file__)) 7 | 8 | # Get the long description from the README file 9 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 10 | long_description = f.read() 11 | 12 | setup( 13 | name='openunipay', 14 | 15 | # Versions should comply with PEP440. For a discussion on single-sourcing 16 | # the version across setup.py and the project code, see 17 | # https://packaging.python.org/en/latest/single_source_version.html 18 | version='0.2.3', 19 | 20 | description='openunipay. 统一支付接口. 集成了微信支付、支付宝支付', 21 | long_description=long_description, 22 | 23 | # The project's main homepage. 24 | url='https://github.com/panweifeng/openunipay', 25 | 26 | # Author details 27 | author='Eric Pan', 28 | author_email='pan.weifeng@live.cn', 29 | 30 | # Choose your license 31 | license='MIT', 32 | 33 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 34 | classifiers=[ 35 | # How mature is this project? Common values are 36 | # 3 - Alpha 37 | # 4 - Beta 38 | # 5 - Production/Stable 39 | 'Development Status :: 3 - Alpha', 40 | 41 | # Indicate who your project is intended for 42 | 'Intended Audience :: Developers', 43 | 'Topic :: Software Development :: Libraries', 44 | 45 | # Pick your license as you wish (should match "license" above) 46 | 'License :: OSI Approved :: MIT License', 47 | 48 | # Specify the Python versions you support here. In particular, ensure 49 | # that you indicate whether you support Python 2, Python 3 or both. 50 | 'Programming Language :: Python :: 3.4', 51 | 'Programming Language :: Python :: 3.5', 52 | ], 53 | 54 | # What does your project relate to? 55 | keywords='openunipay 微信支付 支付宝支付', 56 | 57 | # You can just specify the packages manually here if your project is 58 | # simple. Or you can use find_packages(). 59 | packages=find_packages(exclude=['tests*', ]), 60 | 61 | # Alternatively, if you want to distribute just a my_module.py, uncomment 62 | # this: 63 | # py_modules=["my_module"], 64 | 65 | # List run-time dependencies here. These will be installed by pip when 66 | # your project is installed. For an analysis of "install_requires" vs pip's 67 | # requirements files see: 68 | # https://packaging.python.org/en/latest/requirements.html 69 | install_requires=['django', 'django-cors-headers', 'djangorestframework', 'djangorestframework-xml', 'django-import-export', 'requests', 'rsa', ], 70 | ) 71 | -------------------------------------------------------------------------------- /openunipay/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import models 4 | from openunipay.ali_pay.models import * 5 | from openunipay.weixin_pay.models import * 6 | from openunipay.util import datetime 7 | from datetime import timedelta 8 | 9 | PAY_WAY_WEIXIN = 'WEIXIN' 10 | PAY_WAY_ALI = 'ALI' 11 | PAY_WAY = ((PAY_WAY_WEIXIN, u'微信支付'), 12 | (PAY_WAY_ALI, u'支付宝支付'), ) 13 | 14 | 15 | class OrderItem(models.Model): 16 | orderno = models.CharField(verbose_name=u'订单号', max_length=50, primary_key=True, editable=False) 17 | user = models.CharField(verbose_name=u'用户标识', max_length=50, null=True, blank=True) 18 | product_desc = models.CharField(verbose_name=u'商品描述', max_length=128, null=False, blank=False) 19 | product_detail = models.TextField(verbose_name=u'商品详情', max_length=1000, null=False, blank=False) 20 | fee = models.DecimalField(verbose_name=u'金额(单位:分)', max_digits=6, decimal_places=0, null=False, blank=False) 21 | attach = models.CharField(verbose_name=u'附加数据', max_length=127, null=True, blank=True) 22 | dt_start = models.DateTimeField(verbose_name=u'交易开始时间', null=False, blank=False, editable=False) 23 | dt_end = models.DateTimeField(verbose_name=u'交易失效时间', null=False, blank=False, editable=False) 24 | dt_pay = models.DateTimeField(verbose_name=u'付款时间', null=True, blank=True, editable=False) 25 | paied = models.BooleanField(verbose_name=u'已收款', null=False, blank=False, default=False, editable=False) 26 | lapsed = models.BooleanField(verbose_name=u'已失效', null=False, blank=False, default=False, editable=False) 27 | payway = models.CharField(verbose_name=u'支付方式', max_length=10, null=False, blank=False, choices=PAY_WAY, default=PAY_WAY[0][0]) 28 | date_create = models.DateTimeField(verbose_name=u'创建时间', auto_now_add=True) 29 | date_update = models.DateTimeField(verbose_name=u'修改时间', auto_now=True) 30 | 31 | class Meta: 32 | verbose_name = '付款单' 33 | verbose_name_plural = '付款单' 34 | 35 | def __str__(self): 36 | return self.orderno 37 | 38 | def _set_expire_time(self, expire): 39 | self.dt_start = datetime.local_now() 40 | self.dt_end = self.dt_start + timedelta(minutes=expire) 41 | 42 | def initial_orlder(self, expire): 43 | self._set_expire_time(expire) 44 | 45 | 46 | class Product(models.Model): 47 | productid = models.CharField(verbose_name=u'商品ID', max_length=50, primary_key=True) 48 | product_desc = models.CharField(verbose_name=u'商品描述', max_length=128, null=False, blank=False) 49 | product_detail = models.TextField(verbose_name=u'商品详情', max_length=1000, null=False, blank=False) 50 | fee = models.DecimalField(verbose_name=u'金额(单位:分)', max_digits=6, decimal_places=0, null=False, blank=False) 51 | weinxin_qrurl = models.CharField(verbose_name=u'微信扫码支付URL', max_length=500, null=True, blank=True) 52 | date_create = models.DateTimeField(verbose_name=u'创建时间', auto_now_add=True) 53 | date_update = models.DateTimeField(verbose_name=u'修改时间', auto_now=True) 54 | 55 | class Meta: 56 | verbose_name = '商品' 57 | verbose_name_plural = '商品' 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | openunipay 统一支付接口 - 集成了微信、支付宝支付 2 | ======================= 3 | 4 | 为微信支付,支付宝支付提供统一接口。做到一次集成即可使用多种支付渠道 5 | 6 | 此为服务端,随后会提供Android, IOS, JS client SDK. 7 | 目前此服务端只能集成在您自己的项目中使用。1.0版本时,会支持作为一个service发布.
8 | 9 | [此为开发版本,生产环境使用请仔细审查代码!!!]
10 | 11 | 欢迎各位有兴趣的同学,参与维护这个开源项目!有意请联系pan.weifeng@live.cn. 12 | ======================= 13 | 欢迎赞助!支付宝: pan.weifeng@live.cn 14 | ======================= 15 | 16 | 更新: 17 | ======================= 18 | V0.2.2 2017-07-22 扫码支付使用短连接. 重构了微信支付部分
19 | V0.2.1 2017-07-21 增加扫码支付的支持,目前仅支持模式1
20 | 21 | ---- 22 | 23 | RoadMap: 24 | ======================= 25 | 1. 增加微信扫码模式2的支持
26 | 2,增加支付宝扫码支付的支持
27 | 3,支持支付宝新接口
28 | 29 | 安装方法: 30 | ======================= 31 | pip install openunipay 32 | 33 | ---- 34 | 服务端部署: 35 | ======================= 36 | (需要基于django 项目部署。随后会提供开箱即用的Docker Image) 37 | 此module 需要集成在django 项目中。 38 | 39 | ---- 40 | 1, 在settings.py 里 将openunipay 添加到 install app中
41 | INSTALLED_APPS = ( 42 | ...... 43 | 'openunipay', 44 | ) 45 | 46 | ---- 47 | 2, 发布 支付宝 和 微信支付支付 异步支付结果通知URL。支付成功后,支付宝和微信支付 在支付成功后会通过此URL通知支付结果 48 | openunipay 已经提供了用于处理支付结果的django view. 你只需配置django URL 将openunipay的view 发布即可。
49 | openuipay 提供了以下三个个view
50 | openuipay.api.views_alipay.process_notify
51 | openuipay.api.views_weixin.process_notify
52 | openuipay.api.views_weixin.process_qr_notify
53 | 54 | 在你的url.py里 55 | ********************************************************* 56 | from openunipay.api import views_alipay, views_weixin
57 | 58 | urlpatterns = [ 59 | url(r'^notify/weixin/$', views_weixin.process_notify), //用户使用微信付款后,微信服务器会调用这个接口。详细流程参看微信支付文档
60 | url(r'^qrnotify/weixin/$', views_weixin.process_qr_notify), //微信扫码支付, 用户扫描二维码后,微信服务器会调用这个接口。详细流程请参考微信扫码支付文档
61 | url(r'^notify/alipay/$', views_alipay.process_notify), //支付宝支付后,支付宝服务器会调用这个接口。详细流程参看支付宝文档
62 | ] 63 | *********************************************************** 64 | 65 | ---- 66 | 3,在settings.py里添加以下配置项
67 | 68 | #####支付宝支付配置
69 | ALIPAY = {
70 | 'partner':'XXX', //支付宝partner ID
71 | 'seller_id':'XXX', //收款方支付宝账号如 pan.weifeng@live.cn
72 | 'notify_url':'https://XXX/notify/alipay/', //支付宝异步通知接收URL
73 | 'ali_public_key_pem':'PATH to PEM File', //支付宝公钥的PEM文件路径,在支付宝合作伙伴密钥管理中查看(需要使用合作伙伴支付宝公钥)。如何查看,请参看支付宝文档
74 | 'rsa_private_key_pem':'PATH to PEM File',//您自己的支付宝账户的私钥的PEM文件路径。如何设置,请参看支付宝文档
75 | 'rsa_public_key_pem':'PATH to PEM File',//您自己的支付宝账户的公钥的PEM文件路径。如何设置,请参看支付宝文档
76 | }
77 | #####微信支付配置
78 | WEIXIN = {
79 | 'app_id':'XXX', //微信APPID
80 | 'app_seckey':'XXX', //微信APP Sec Key
81 | 'mch_id':'XXX', //微信商户ID
82 | 'mch_seckey':'XXX',//微信商户seckey
83 | 'mch_notify_url':'https://XXX/notify/weixin/', //微信支付异步通知接收URL
84 | 'clientIp':'',//扫码支付时,会使用这个IP地址发送给微信API, 请设置为您服务器的IP 85 | }
86 | 87 | ---- 88 | 4, 同步数据库 89 | 90 | python manage.py migrate --run-syncdb
91 | 92 | ---- 93 | 94 | 95 | 如何使用: 96 | ======================= 97 | 1,创建订单
98 | from openunipay.paygateway import unipay
99 | from openunipay.models import PAY_WAY_WEIXIN,PAY_WAY_ALI //PAY_WAY_WEIXIN:微信支付 PAY_WAY_ALI:支付宝支付
100 | 101 | create_order.create_order(orderno, payway, clientIp, product_desc, product_detail, fee, user=None, attach=None, expire=1440, **kwargs):
102 | 此方法会返回支付信息。在APP中发起支付时 需要使用此支付信息。所有数据已经按照微信和支付宝接口要求做了处理。APP无需再次处理。
103 | 104 | 105 | 2, 查寻订单
106 | query_order(orderno)
107 | APP支付成功后,需要调用此接口确认支付。发货流程需要在此方法里处理。
108 | 109 | 110 | 3. 生成扫码支付二维码(目前仅支持微信扫码支付模式1)
111 | generate_qr_pay_url(payway, productid)
112 | 已经在Admin 里增加了Production Model Admin. 只需要增加商品即可生成支付URL. 然后用URL生成二维码。 你也可以用此方法的链接在服务端生成二维码图片.
113 | 114 | ---- -------------------------------------------------------------------------------- /openunipay/paygateway/weixin.py: -------------------------------------------------------------------------------- 1 | from . import PayGateway 2 | from django.db import transaction 3 | from django.conf import settings 4 | from openunipay.util import random_helper, datetime 5 | from openunipay.weixin_pay.models import WeiXinOrder, WeiXinQRPayEntity 6 | from openunipay.weixin_pay import weixin_pay_lib, security, xml_helper 7 | from openunipay.models import OrderItem, Product, PAY_WAY_WEIXIN 8 | 9 | 10 | class WeiXinPayGateway(PayGateway): 11 | @transaction.atomic 12 | def create_order(self, orderItemObj, clientIp, **kwargs): 13 | weixinOrderObj = WeiXinOrder() 14 | weixinOrderObj.appid = settings.WEIXIN['app_id'] 15 | weixinOrderObj.mch_id = settings.WEIXIN['mch_id'] 16 | weixinOrderObj.body = orderItemObj.product_desc 17 | weixinOrderObj.attach = orderItemObj.attach 18 | weixinOrderObj.out_trade_no = orderItemObj.orderno 19 | weixinOrderObj.total_fee = orderItemObj.fee 20 | weixinOrderObj.spbill_create_ip = clientIp 21 | weixinOrderObj.time_start = orderItemObj.dt_start.strftime("%Y%m%d%H%M%S") 22 | weixinOrderObj.time_expire = orderItemObj.dt_end.strftime("%Y%m%d%H%M%S") 23 | weixinOrderObj.notify_url = settings.WEIXIN['mch_notify_url'] 24 | weixinOrderObj.trade_type = kwargs.get('trade_type', 'APP') 25 | weixinOrderObj.openid = kwargs.get('openid') 26 | weixinOrderObj.save() 27 | prepayid = weixin_pay_lib.create_order(weixinOrderObj) 28 | data = {'appid': settings.WEIXIN['app_id'], 29 | 'partnerid': settings.WEIXIN['mch_id'], 30 | 'prepayid': prepayid, 31 | 'package': 'Sign=WXPay', 32 | 'noncestr': random_helper.generate_nonce_str(23), 33 | 'timestamp': str(datetime.get_unix_timestamp())} 34 | data['sign'] = security.sign(data) 35 | return data 36 | 37 | @transaction.atomic 38 | def process_notify(self, requestContent): 39 | return weixin_pay_lib.process_notify(requestContent) 40 | 41 | @transaction.atomic 42 | def query_order(self, orderNo): 43 | return weixin_pay_lib.query_order(orderNo) 44 | 45 | @transaction.atomic 46 | def generate_qr_pay_url(self, product_id): 47 | qrPayEntiry = WeiXinQRPayEntity() 48 | qrPayEntiry.appid = settings.WEIXIN['app_id'] 49 | qrPayEntiry.mch_id = settings.WEIXIN['mch_id'] 50 | qrPayEntiry.time_stamp = str(datetime.get_unix_timestamp()) 51 | qrPayEntiry.product_id = product_id 52 | qrPayEntiry.save() 53 | url = weixin_pay_lib.request_shorten_url(qrPayEntiry.to_raw_rul()) 54 | return url 55 | 56 | @transaction.atomic 57 | def process_qr_pay_notify(self, requestContent): 58 | notifyObj = weixin_pay_lib.process_qr_pay_notify(requestContent) 59 | 60 | productObj = Product.objects.get(productid=notifyObj['product_id']) 61 | 62 | orderItemObj = OrderItem() 63 | orderItemObj.orderno = self._generate_qr_orderno(productObj.productid) 64 | orderItemObj.user = notifyObj['openid'] 65 | orderItemObj.product_desc = productObj.product_desc 66 | orderItemObj.product_detail = productObj.product_detail 67 | orderItemObj.fee = productObj.fee 68 | orderItemObj.payway = PAY_WAY_WEIXIN 69 | orderItemObj.attach = None 70 | orderItemObj.initial_orlder(1440) 71 | orderItemObj.save() 72 | 73 | weixinOrderItem = self.create_order(orderItemObj, settings.WEIXIN['clientIp'], trade_type='NATIVE', openid=notifyObj['openid']) 74 | 75 | data = { 76 | 'return_code': 'SUCCESS', 77 | 'result_code': 'SUCCESS', 78 | 'appid': settings.WEIXIN['app_id'], 79 | 'mch_id': settings.WEIXIN['mch_id'], 80 | 'prepay_id': weixinOrderItem['prepayid'], 81 | 'nonce_str': notifyObj['nonce_str'], 82 | } 83 | data['sign'] = security.sign(data) 84 | return xml_helper.dict_to_xml(data) 85 | 86 | def _generate_qr_orderno(self, productid): 87 | return 'WXQR-{}-{}'.format(productid, datetime.get_unix_timestamp()) 88 | -------------------------------------------------------------------------------- /openunipay/ali_pay/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.db import models 3 | from django.conf import settings 4 | from urllib.parse import quote_plus 5 | from openunipay.ali_pay import security 6 | 7 | _SERVICE = 'mobile.securitypay.pay' 8 | _CHARSET = 'utf-8' 9 | _SIGN_TYPE = 'RSA' 10 | _PAYMENT_TYPE = '1' 11 | 12 | _ALIPAY_ORDER_FIELD = ('out_trade_no', 'subject', 'body', 'total_fee', 'it_b_pay',) 13 | 14 | class AliPayOrder(models.Model): 15 | out_trade_no = models.CharField(verbose_name='商户订单号', max_length=32, db_index=True, editable=False) 16 | subject = models.CharField(verbose_name='商品名称', max_length=128, editable=False) 17 | body = models.CharField(verbose_name='商品详情', max_length=512, editable=False) 18 | total_fee = models.DecimalField(verbose_name='总金额(单位:元)', max_digits=6, decimal_places=2, editable=False) 19 | it_b_pay = models.CharField(verbose_name='交易有效期', max_length=19, editable=False) 20 | date_create = models.DateTimeField(verbose_name=u'创建时间', auto_now_add=True) 21 | 22 | class Meta: 23 | verbose_name = u'支付宝订单' 24 | verbose_name_plural = u'支付宝订单' 25 | 26 | def __str__(self): 27 | return self.out_trade_no 28 | 29 | def compose_interface_data(self): 30 | # sign data 31 | data = self._compose_data() 32 | return '{}&sign_type="RSA"&sign="{}"'.format(data, quote_plus(security.sign(data))) 33 | 34 | def _compose_data(self): 35 | valueDict = {item:getattr(self, item) 36 | for item in _ALIPAY_ORDER_FIELD 37 | if getattr(self, item)} 38 | valueDict['service'] = _SERVICE 39 | valueDict['_input_charset'] = _CHARSET 40 | valueDict['payment_type'] = _PAYMENT_TYPE 41 | valueDict['partner'] = settings.ALIPAY['partner'] 42 | valueDict['seller_id'] = settings.ALIPAY['seller_id'] 43 | valueDict['notify_url'] = settings.ALIPAY['notify_url'] 44 | 45 | temp = [] 46 | for key in valueDict: 47 | if not valueDict[key]: 48 | continue 49 | temp.append('{}="{}"'.format(key, valueDict[key])) 50 | tempStr = '&'.join(temp) 51 | return tempStr 52 | 53 | class AliPayResult(models.Model): 54 | order = models.OneToOneField(AliPayOrder, 55 | on_delete=models.CASCADE, 56 | primary_key=True, 57 | editable=False, 58 | related_name='pay_result') 59 | notify_time = models.CharField(verbose_name=u'通知时间', null=True, blank=True, max_length=19, editable=False) 60 | notify_type = models.CharField(verbose_name=u'通知类型', null=True, blank=True, max_length=50, editable=False) 61 | notify_id = models.CharField(verbose_name=u'通知校验ID', null=True, blank=True, max_length=50, editable=False) 62 | out_trade_no = models.CharField(verbose_name=u'商户订单号', null=True, blank=True, max_length=32, editable=False) 63 | subject = models.CharField(verbose_name=u'商品名称', null=True, blank=True, max_length=128, editable=False) 64 | trade_no = models.CharField(verbose_name=u'支付宝交易号', null=True, blank=True, max_length=64, editable=False) 65 | trade_status = models.CharField(verbose_name=u'交易状态', null=True, blank=True, max_length=16, editable=False) 66 | seller_id = models.CharField(verbose_name=u'卖家支付宝用户号', null=True, blank=True, max_length=30, editable=False) 67 | seller_email = models.CharField(verbose_name=u'卖家支付宝账号', null=True, blank=True, max_length=100, editable=False) 68 | buyer_id = models.CharField(verbose_name=u'买家支付宝用户号', null=True, blank=True, max_length=30, editable=False) 69 | buyer_email = models.CharField(verbose_name=u'买家支付宝账号 ', null=True, blank=True, max_length=100, editable=False) 70 | total_fee = models.DecimalField(verbose_name=u'总金额(单位:元)', null=True, blank=True, max_digits=6, decimal_places=2, editable=False) 71 | 72 | 73 | def __str__(self): 74 | fieldsList = AliPayResult._meta.get_fields() 75 | temp = [] 76 | for field in fieldsList: 77 | if not field.auto_created: 78 | temp.append('{}:{}'.format(field.verbose_name, getattr(self, field.attname))) 79 | return ','.join(temp) 80 | 81 | -------------------------------------------------------------------------------- /openunipay/paygateway/unipay.py: -------------------------------------------------------------------------------- 1 | ''' 2 | @summary: This is the main interface that can be used to create order, query order status and process notification 3 | @author: EricPan(pan.weifeng@live.cn) 4 | @contact: EricPan(pan.weifeng@live.cn) 5 | ''' 6 | from django.db import transaction 7 | from openunipay.models import OrderItem, PAY_WAY_WEIXIN, PAY_WAY_ALI, Product 8 | from openunipay.paygateway import weixin, alipay, PayResult 9 | from openunipay import exceptions, logger 10 | from openunipay.util import datetime 11 | 12 | _PAY_GATEWAY = {PAY_WAY_WEIXIN: weixin.WeiXinPayGateway(), 13 | PAY_WAY_ALI: alipay.AliPayGateway(), } 14 | 15 | 16 | @transaction.atomic 17 | def create_order(orderno, payway, clientIp, product_desc, product_detail, fee, user=None, attach=None, expire=1440, **kwargs): 18 | ''' 19 | @summary: create order 20 | @param orderno: order no 21 | @param payway: payway 22 | @param clientIp: IP address of the client that start the pay process 23 | @param product_desc: short description of the product 24 | @param product_detail: detail information of the product 25 | @param fee: price of the product. now, only support RMB. unit: Fen 26 | @param user: user identify 27 | @param attach: attach information. must be str 28 | @param expire: expire in minutes 29 | ''' 30 | if not is_supportted_payway(payway): 31 | raise exceptions.PayWayError() 32 | 33 | orderItemObj = OrderItem() 34 | orderItemObj.orderno = orderno 35 | orderItemObj.user = user 36 | orderItemObj.product_desc = product_desc 37 | orderItemObj.product_detail = product_detail 38 | orderItemObj.fee = fee 39 | orderItemObj.payway = payway 40 | orderItemObj.attach = attach 41 | orderItemObj.initial_orlder(expire) 42 | orderItemObj.save() 43 | 44 | # send order to pay gateway 45 | gatewayData = _PAY_GATEWAY[payway].create_order(orderItemObj, clientIp, **kwargs) 46 | logger.info('order created. orderno:{}, payway:{}, clientIp:{}, product:{},fee:{}, gateway data:{}'.format(orderno, payway, clientIp, product_desc, fee, gatewayData)) 47 | return gatewayData 48 | 49 | 50 | @transaction.atomic 51 | def query_order(orderno): 52 | ''' 53 | @summary: query status of order 54 | @param orderno: order no 55 | ''' 56 | orderItemObj = OrderItem.objects.get(orderno=orderno) 57 | if orderItemObj.paied: 58 | return PayResult(orderItemObj.orderno) 59 | elif orderItemObj.lapsed: 60 | return PayResult(orderItemObj.orderno, succ=False, lapsed=True) 61 | else: 62 | payResult = _PAY_GATEWAY[orderItemObj.payway].query_order(orderItemObj.orderno) 63 | _update_order_pay_result(payResult) 64 | return payResult 65 | 66 | 67 | @transaction.atomic 68 | def process_notify(payway, requestContent): 69 | ''' 70 | @summary: process async notification from pay interface 71 | @param payway: payway 72 | @param requestContent: request body from pay interface 73 | @return: an instance of PayResult 74 | ''' 75 | if not is_supportted_payway(payway): 76 | raise exceptions.PayWayError() 77 | payResult = _PAY_GATEWAY[payway].process_notify(requestContent) 78 | _update_order_pay_result(payResult) 79 | logger.info('process notify. payway:{}, content:{}'.format(payway, requestContent)) 80 | return payResult 81 | 82 | 83 | def is_supportted_payway(payway): 84 | return payway in _PAY_GATEWAY 85 | 86 | 87 | def _update_order_pay_result(payResult): 88 | if payResult.Succ: 89 | orderItemObj = OrderItem.objects.get(orderno=payResult.OrderNo) 90 | orderItemObj.paied = True 91 | orderItemObj.lapsed = False 92 | orderItemObj.dt_pay = datetime.local_now() 93 | orderItemObj.save() 94 | elif payResult.Lapsed: 95 | orderItemObj = OrderItem.objects.get(orderno=payResult.OrderNo) 96 | orderItemObj.paied = False 97 | orderItemObj.lapsed = True 98 | orderItemObj.dt_pay = None 99 | orderItemObj.save() 100 | 101 | 102 | @transaction.atomic 103 | def generate_qr_pay_url(payway, productid): 104 | ''' 105 | @summary: create qr pay url 106 | @param productid: product id 107 | ''' 108 | if not is_supportted_payway(payway): 109 | raise exceptions.PayWayError() 110 | 111 | # send order to pay gateway 112 | url = _PAY_GATEWAY[payway].generate_qr_pay_url(productid) 113 | logger.info('qr pay url generated. productid:{}, url:{}'.format(productid, url)) 114 | return url 115 | 116 | 117 | @transaction.atomic 118 | def process_qr_pay_notify(payway, requestContent): 119 | if not is_supportted_payway(payway): 120 | raise exceptions.PayWayError() 121 | return _PAY_GATEWAY[payway].process_qr_pay_notify(requestContent) 122 | -------------------------------------------------------------------------------- /openunipay/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for openunipay project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.9. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.9/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.9/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | # Quick-start development settings - unsuitable for production 19 | # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ 20 | 21 | # SECURITY WARNING: keep the secret key used in production secret! 22 | SECRET_KEY = 'h!!5gjty3dvz!ny)e^x%=n-6!jz++%84h1+yq27zcntk1qn4w8' 23 | 24 | # SECURITY WARNING: don't run with debug turned on in production! 25 | DEBUG = True 26 | 27 | ALLOWED_HOSTS = ['*'] 28 | 29 | # Application definition 30 | 31 | INSTALLED_APPS = [ 32 | 'django.contrib.admin', 33 | 'django.contrib.auth', 34 | 'django.contrib.contenttypes', 35 | 'django.contrib.sessions', 36 | 'django.contrib.messages', 37 | 'django.contrib.staticfiles', 38 | 'import_export', 39 | 'openunipay', 40 | ] 41 | 42 | MIDDLEWARE_CLASSES = [ 43 | 'corsheaders.middleware.CorsMiddleware', 44 | 'django.middleware.security.SecurityMiddleware', 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | #'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ] 53 | 54 | ROOT_URLCONF = 'openunipay.urls' 55 | 56 | TEMPLATES = [ 57 | { 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 59 | 'DIRS': [], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'openunipay.wsgi.application' 73 | 74 | # Database 75 | # https://docs.djangoproject.com/en/1.9/ref/settings/#databases 76 | 77 | DATABASES = { 78 | 'default': { 79 | 'ENGINE': 'django.db.backends.sqlite3', 80 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 81 | } 82 | } 83 | 84 | # Password validation 85 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 86 | 87 | AUTH_PASSWORD_VALIDATORS = [ 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 99 | }, 100 | ] 101 | 102 | # Internationalization 103 | # https://docs.djangoproject.com/en/1.9/topics/i18n/ 104 | 105 | LANGUAGE_CODE = 'zh-hans' 106 | 107 | TIME_ZONE = 'Asia/Shanghai' 108 | 109 | USE_I18N = True 110 | 111 | USE_L10N = False 112 | 113 | USE_TZ = False 114 | 115 | LOGGING = { 116 | 'version': 1, 117 | 'disable_existing_loggers': False, 118 | 'formatters': { 119 | 'verbose': { 120 | 'format': '[%(levelname)s]-[%(asctime)s]-[%(name)s]: %(message)s' 121 | }, 122 | }, 123 | 'handlers': { 124 | 'console': { 125 | 'level': 'DEBUG', 126 | 'class': 'logging.StreamHandler', 127 | 'formatter': 'verbose', 128 | }, 129 | }, 130 | 'loggers': { 131 | 'django.request': { 132 | 'handlers': ['console'], 133 | 'level': 'INFO', 134 | 'propagate': True, 135 | }, 136 | 'django.db': { 137 | 'handlers': ['console'], 138 | 'level': 'INFO', 139 | 'propagate': False, 140 | }, 141 | }, 142 | 'root': { 143 | 'handlers': [ 144 | 'console', 145 | ], 146 | 'level': 'DEBUG', 147 | }, 148 | } 149 | 150 | # Static files (CSS, JavaScript, Images) 151 | # https://docs.djangoproject.com/en/1.9/howto/static-files/ 152 | 153 | STATIC_URL = '/static/' 154 | 155 | 156 | #### 测试 157 | WEIXIN = { 158 | 'app_id': 'wx2428e34e0e7dc6ef', 159 | 'app_seckey': '51c56b886b5be869567dd389b3e5d1d6', 160 | 'mch_id': '1233410002', 161 | 'mch_seckey': '51c56b886b5be869567dd389b3e5d1d6', 162 | 'mch_notify_url': 'http://test.cn', 163 | 'clientIp': '111.11.11.11', 164 | } 165 | -------------------------------------------------------------------------------- /openunipay/weixin_pay/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.db import models 3 | from openunipay.util import random_helper 4 | from .security import sign 5 | from . import xml_helper 6 | from urllib import parse 7 | 8 | _ORDER_NONCE_POPULATION = '123457890ABCDEFGHIJKLMNOPQRSTUVWXYZ' 9 | 10 | 11 | class WeiXinQRPayEntity(models.Model): 12 | appid = models.CharField(verbose_name=u'公众账号ID', max_length=32, editable=False) 13 | mch_id = models.CharField(verbose_name=u'商户号', max_length=32, editable=False) 14 | time_stamp = models.CharField(verbose_name=u'时间戳', max_length=10, editable=False) 15 | product_id = models.CharField(verbose_name=u'商品ID', max_length=32, editable=False) 16 | 17 | class Meta: 18 | verbose_name = u'微信扫码支付-二维码URL' 19 | verbose_name_plural = u'微信扫码支付-二维码URL' 20 | 21 | def _get_vlaue_dict(self): 22 | fieldsList = WeiXinQRPayEntity._meta.get_fields() 23 | return {item.attname: getattr(self, item.attname) for item in fieldsList if not item.auto_created and getattr(self, item.attname)} 24 | 25 | def to_url(self): 26 | # sign data 27 | valueDict = self._get_vlaue_dict() 28 | valueDict['nonce_str'] = random_helper.generate_nonce_str(23) 29 | valueDict['sign'] = sign(valueDict) 30 | urlparameer = parse.urlencode(valueDict) 31 | return 'weixin://wxpay/bizpayurl?{}'.format(urlparameer) 32 | 33 | def to_raw_rul(self): 34 | valueDict = self._get_vlaue_dict() 35 | valueDict['nonce_str'] = random_helper.generate_nonce_str(23) 36 | valueDict['sign'] = sign(valueDict) 37 | temp = ['{}={}'.format(k, v) for k, v in valueDict.items()] 38 | return 'weixin://wxpay/bizpayurl?{}'.format('&'.join(temp)) 39 | 40 | 41 | class WeiXinQRPayRecord(models.Model): 42 | appid = models.CharField(verbose_name=u'公众账号ID', max_length=32, editable=False) 43 | mch_id = models.CharField(verbose_name=u'商户号', max_length=32, editable=False) 44 | openid = models.CharField(verbose_name=u'用户标识', max_length=128, editable=False) 45 | product_id = models.CharField(verbose_name=u'商品ID', max_length=32, editable=False) 46 | 47 | class Meta: 48 | verbose_name = u'微信扫码支付-扫码纪录' 49 | verbose_name_plural = u'微信扫码支付-扫码纪录' 50 | 51 | 52 | class WeiXinOrder(models.Model): 53 | appid = models.CharField(verbose_name=u'公众账号ID', max_length=32, editable=False) 54 | mch_id = models.CharField(verbose_name=u'商户号', max_length=32, editable=False) 55 | body = models.CharField(verbose_name=u'商品描述', max_length=128, editable=False) 56 | attach = models.CharField(verbose_name=u'附加数据', max_length=127, null=True, blank=True, editable=False) 57 | out_trade_no = models.CharField(verbose_name=u'商户订单号', max_length=32, db_index=True, editable=False) 58 | fee_type = models.CharField(verbose_name=u'货币类型', max_length=16, editable=False) 59 | total_fee = models.PositiveIntegerField(verbose_name=u'总金额', editable=False) 60 | spbill_create_ip = models.CharField(verbose_name=u'终端IP', max_length=16, editable=False) 61 | time_start = models.CharField(verbose_name=u'交易起始时间', max_length=14, editable=False) 62 | time_expire = models.CharField(verbose_name=u'交易结束时间', max_length=14, editable=False) 63 | notify_url = models.CharField(verbose_name=u'通知地址', max_length=256, editable=False) 64 | trade_type = models.CharField(verbose_name=u'交易类型', max_length=16, editable=False) 65 | openid = models.CharField(verbose_name=u'用户标识(openId)', null=True, blank=True, max_length=128, editable=False) 66 | 67 | class Meta: 68 | verbose_name = u'微信统一订单' 69 | verbose_name_plural = u'微信统一订单' 70 | 71 | def __str__(self): 72 | return self.out_trade_no 73 | 74 | def _get_vlaue_dict(self): 75 | fieldsList = WeiXinOrder._meta.get_fields() 76 | return {item.attname: getattr(self, item.attname) for item in fieldsList if not item.auto_created and getattr(self, item.attname)} 77 | 78 | def to_xml(self): 79 | # sign data 80 | valueDict = self._get_vlaue_dict() 81 | valueDict['nonce_str'] = random_helper.generate_nonce_str(23) 82 | valueDict['sign'] = sign(valueDict) 83 | return xml_helper.dict_to_xml(valueDict) 84 | 85 | 86 | class WeiXinPayResult(models.Model): 87 | order = models.OneToOneField(WeiXinOrder, on_delete=models.CASCADE, primary_key=True, editable=False, related_name='pay_result') 88 | prepayid = models.CharField(verbose_name=u'预支付交易会话标识', null=True, blank=True, max_length=64, db_index=True, editable=False) 89 | openid = models.CharField(verbose_name=u'用户标识(openId)', null=True, blank=True, max_length=128, editable=False) 90 | bank_type = models.CharField(verbose_name=u'付款银行', null=True, blank=True, max_length=16, editable=False) 91 | total_fee = models.SmallIntegerField(verbose_name=u'总金额', null=True, blank=True, editable=False) 92 | attach = models.CharField(verbose_name=u'商户附加数据', null=True, blank=True, max_length=128, editable=False) 93 | tradestate = models.CharField(verbose_name=u'交易状态', null=True, blank=True, max_length=32, editable=False) 94 | tradestatedesc = models.CharField(verbose_name=u'交易状态描述', null=True, blank=True, max_length=256, editable=False) 95 | 96 | def __str__(self): 97 | fieldsList = WeiXinPayResult._meta.get_fields() 98 | temp = [] 99 | for field in fieldsList: 100 | if not field.auto_created: 101 | temp.append('{}:{}'.format(field.verbose_name, getattr(self, field.attname))) 102 | return ','.join(temp) 103 | -------------------------------------------------------------------------------- /openunipay/weixin_pay/weixin_pay_lib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import requests 3 | import logging 4 | from urllib import parse 5 | from django.conf import settings 6 | from openunipay.weixin_pay.exceptions import APIError 7 | from openunipay.weixin_pay.models import WeiXinOrder, WeiXinPayResult, WeiXinQRPayRecord 8 | from openunipay.weixin_pay.security import sign 9 | from openunipay.weixin_pay import xml_helper 10 | from openunipay.util import random_helper 11 | from openunipay.paygateway import PayResult 12 | from openunipay import exceptions 13 | 14 | CODE_SUCC = 'SUCCESS' 15 | TRADE_STATE_SUCC = ('SUCCESS', ) 16 | TRADE_STATE_LAPSED = ('CLOSED', 'REVOKED') 17 | 18 | _logger = logging.getLogger('openunipay.weixin') 19 | 20 | 21 | ############### 统一下单 ####################### 22 | def create_order(weixinOrderObj): 23 | assert isinstance(weixinOrderObj, WeiXinOrder) 24 | _logger.info("creating order") 25 | payResultObj = WeiXinPayResult.objects.create(order=weixinOrderObj) 26 | url = 'https://api.mch.weixin.qq.com/pay/unifiedorder' 27 | data = weixinOrderObj.to_xml().encode() 28 | _logger.info("request data is:{}".format(data)) 29 | r = requests.post(url, data=data, headers={'Content-Type': 'application/xml'}, verify=False) 30 | 31 | def handle_response(responseData): 32 | payResultObj.prepayid = responseData['prepay_id'] 33 | payResultObj.save() 34 | _logger.info("order created. prepayid is {}".format(payResultObj.prepayid)) 35 | return responseData['prepay_id'] 36 | 37 | return __handle_weixin_api_xml_response(r, handle_response) 38 | 39 | 40 | ############# 处理微信支付 异步通知 ############# 41 | def process_notify(notifyContent): 42 | responseData = xml_helper.xml_to_dict(notifyContent) 43 | # check sign 44 | if responseData['return_code'] == CODE_SUCC: 45 | __verify_response_data(responseData) 46 | result = _process_order_result(responseData) 47 | return result 48 | else: 49 | return None 50 | 51 | 52 | def query_order(orderNo): 53 | weixinOrderObj = WeiXinOrder.objects.get(out_trade_no=orderNo) 54 | 55 | result = _compose_pay_result(orderNo, weixinOrderObj.pay_result.tradestate) 56 | if result.Succ or result.Lapsed: 57 | return result 58 | 59 | valueDict = {'appid': weixinOrderObj.appid, 'mch_id': weixinOrderObj.mch_id, 'out_trade_no': weixinOrderObj.out_trade_no, 'nonce_str': random_helper.generate_nonce_str(23)} 60 | valueDict['sign'] = sign(valueDict) 61 | data = xml_helper.dict_to_xml(valueDict) 62 | 63 | url = 'https://api.mch.weixin.qq.com/pay/orderquery' 64 | r = requests.post(url, data=data, headers={'Content-Type': 'application/xml'}, verify=False) 65 | 66 | return __handle_weixin_api_xml_response(r, _process_order_result) 67 | 68 | 69 | def _process_order_result(responseData): 70 | # save data 71 | weixinOrderObj = WeiXinOrder.objects.get(out_trade_no=responseData['out_trade_no']) 72 | payResultObj = weixinOrderObj.pay_result 73 | payResultObj.openid = responseData.get('openid') 74 | payResultObj.bank_type = responseData.get('bank_type') 75 | payResultObj.total_fee = responseData.get('total_fee') 76 | payResultObj.attach = responseData.get('attach') 77 | payResultObj.tradestate = responseData.get('trade_state', 'SUCCESS') 78 | payResultObj.tradestatedesc = responseData.get('trade_state_desc') 79 | payResultObj.save() 80 | result = _compose_pay_result(responseData['out_trade_no'], payResultObj.tradestate) 81 | return result 82 | 83 | 84 | def _compose_pay_result(orderNo, tradestate): 85 | result = PayResult(orderNo) 86 | result.succ = tradestate in TRADE_STATE_SUCC 87 | result.lapsed = tradestate in TRADE_STATE_LAPSED 88 | return result 89 | 90 | 91 | ############ 扫码支付 ################## 92 | def process_qr_pay_notify(notifyContent): 93 | ''' 94 | @summary: 处理微信扫码支付异步通知 95 | ''' 96 | responseData = xml_helper.xml_to_dict(notifyContent) 97 | # check sign 98 | __verify_response_data(responseData) 99 | # process data 100 | qrPayRecord = WeiXinQRPayRecord.objects.create( 101 | appid=responseData.get('appid'), mch_id=responseData.get('mch_id'), openid=responseData.get('openid'), product_id=responseData.get('product_id')) 102 | return {'product_id': qrPayRecord.product_id, 'openid': qrPayRecord.openid, 'nonce_str': responseData.get('nonce_str')} 103 | 104 | 105 | ############ 转换短连接 ################## 106 | def request_shorten_url(url): 107 | _logger.info("get shorten url") 108 | data = { 109 | 'appid': settings.WEIXIN['app_id'], 110 | 'mch_id': settings.WEIXIN['mch_id'], 111 | 'long_url': url, 112 | 'nonce_str': random_helper.generate_nonce_str(23), 113 | } 114 | data['sign'] = sign(data) 115 | requestBody = xml_helper.dict_to_xml(data) 116 | _logger.info("request data is:{}".format(requestBody)) 117 | 118 | url = 'https://api.mch.weixin.qq.com/tools/shorturl' 119 | r = requests.post(url, data=requestBody, headers={'Content-Type': 'application/xml'}, verify=False) 120 | return __handle_weixin_api_xml_response(r, lambda responseData: responseData['short_url']) 121 | 122 | 123 | ######## 124 | def __verify_response_data(responseData): 125 | ''' 126 | @summary: 使用签名验证微信服务器的返回数据 127 | @param responseData:返回数据的Dict,里面需包含sign字段. 验证成功后sign字段会被删除 128 | @return 如果验证失败会抛出异常 129 | ''' 130 | signStr = responseData['sign'] 131 | del responseData['sign'] 132 | signCheck = sign(responseData) 133 | if signStr != signCheck: 134 | _logger.error('received untrusted data') 135 | raise exceptions.InsecureDataError() 136 | 137 | 138 | def __handle_weixin_api_xml_response(r, func): 139 | ''' 140 | @summary: 处理微信API返回结果 141 | @param func:数据处理function, 会在请求成功并且验证成功的时候调用 142 | @return 如果验证失败会抛出异常 143 | ''' 144 | r.encoding = 'utf-8' 145 | _logger.info("response body is:{}".format(r.text)) 146 | if r.status_code == 200: 147 | responseData = xml_helper.xml_to_dict(r.text) 148 | if responseData['return_code'] == CODE_SUCC: 149 | # check sign 150 | __verify_response_data(responseData) 151 | if responseData['result_code'] == CODE_SUCC: 152 | return func(responseData) 153 | else: 154 | raise APIError(responseData['err_code']) 155 | else: 156 | raise APIError('data sign verfied failed') 157 | else: 158 | raise APIError('status from weixin is not 200') 159 | -------------------------------------------------------------------------------- /openunipay/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.9.6 on 2016-05-17 13:56 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='AliPayOrder', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('out_trade_no', models.CharField(db_index=True, editable=False, max_length=32, verbose_name='商户订单号')), 22 | ('subject', models.CharField(editable=False, max_length=128, verbose_name='商品名称')), 23 | ('body', models.CharField(editable=False, max_length=512, verbose_name='商品详情')), 24 | ('total_fee', models.DecimalField(decimal_places=2, editable=False, max_digits=6, verbose_name='总金额(单位:元)')), 25 | ('it_b_pay', models.CharField(editable=False, max_length=19, verbose_name='交易有效期')), 26 | ('interface_data', models.TextField(editable=False, max_length=500, verbose_name='接口数据')), 27 | ('date_create', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), 28 | ], 29 | options={ 30 | 'verbose_name_plural': '支付宝订单', 31 | 'verbose_name': '支付宝订单', 32 | }, 33 | ), 34 | migrations.CreateModel( 35 | name='OrderItem', 36 | fields=[ 37 | ('orderno', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False, verbose_name='订单号')), 38 | ('user', models.CharField(blank=True, max_length=50, null=True, verbose_name='用户标识')), 39 | ('product_desc', models.CharField(max_length=128, verbose_name='商品描述')), 40 | ('product_detail', models.TextField(max_length=1000, verbose_name='商品详情')), 41 | ('fee', models.DecimalField(decimal_places=0, max_digits=6, verbose_name='金额(单位:分)')), 42 | ('attach', models.CharField(blank=True, max_length=127, null=True, verbose_name='附加数据')), 43 | ('dt_start', models.DateTimeField(editable=False, verbose_name='交易开始时间')), 44 | ('dt_end', models.DateTimeField(editable=False, verbose_name='交易失效时间')), 45 | ('dt_pay', models.DateTimeField(blank=True, editable=False, null=True, verbose_name='付款时间')), 46 | ('paied', models.BooleanField(default=False, editable=False, verbose_name='已收款')), 47 | ('lapsed', models.BooleanField(default=False, editable=False, verbose_name='已失效')), 48 | ('payway', models.CharField(choices=[('WEIXIN', '微信支付'), ('ALI', '支付宝支付')], default='WEIXIN', max_length=10, verbose_name='支付方式')), 49 | ('date_create', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), 50 | ('date_update', models.DateTimeField(auto_now=True, verbose_name='修改时间')), 51 | ], 52 | options={ 53 | 'verbose_name_plural': '付款单', 54 | 'verbose_name': '付款单', 55 | }, 56 | ), 57 | migrations.CreateModel( 58 | name='WeiXinOrder', 59 | fields=[ 60 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 61 | ('appid', models.CharField(editable=False, max_length=32, verbose_name='公众账号ID')), 62 | ('mch_id', models.CharField(editable=False, max_length=32, verbose_name='商户号')), 63 | ('body', models.CharField(editable=False, max_length=128, verbose_name='商品描述')), 64 | ('attach', models.CharField(blank=True, editable=False, max_length=127, null=True, verbose_name='附加数据')), 65 | ('out_trade_no', models.CharField(db_index=True, editable=False, max_length=32, verbose_name='商户订单号')), 66 | ('fee_type', models.CharField(editable=False, max_length=16, verbose_name='货币类型')), 67 | ('total_fee', models.SmallIntegerField(editable=False, verbose_name='总金额')), 68 | ('spbill_create_ip', models.CharField(editable=False, max_length=16, verbose_name='终端IP')), 69 | ('time_start', models.CharField(editable=False, max_length=14, verbose_name='交易起始时间')), 70 | ('time_expire', models.CharField(editable=False, max_length=14, verbose_name='交易结束时间')), 71 | ('notify_url', models.CharField(editable=False, max_length=256, verbose_name='通知地址')), 72 | ('trade_type', models.CharField(editable=False, max_length=16, verbose_name='交易类型')), 73 | ], 74 | options={ 75 | 'verbose_name_plural': '微信统一订单', 76 | 'verbose_name': '微信统一订单', 77 | }, 78 | ), 79 | migrations.CreateModel( 80 | name='AliPayResult', 81 | fields=[ 82 | ('order', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='pay_result', serialize=False, to='openunipay.AliPayOrder')), 83 | ('notify_time', models.CharField(blank=True, editable=False, max_length=19, null=True, verbose_name='通知时间')), 84 | ('notify_type', models.CharField(blank=True, editable=False, max_length=50, null=True, verbose_name='通知类型')), 85 | ('notify_id', models.CharField(blank=True, editable=False, max_length=50, null=True, verbose_name='通知校验ID')), 86 | ('out_trade_no', models.CharField(blank=True, editable=False, max_length=32, null=True, verbose_name='商户订单号')), 87 | ('subject', models.CharField(blank=True, editable=False, max_length=128, null=True, verbose_name='商品名称')), 88 | ('trade_no', models.CharField(blank=True, editable=False, max_length=64, null=True, verbose_name='支付宝交易号')), 89 | ('trade_status', models.CharField(blank=True, editable=False, max_length=16, null=True, verbose_name='交易状态')), 90 | ('seller_id', models.CharField(blank=True, editable=False, max_length=30, null=True, verbose_name='卖家支付宝用户号')), 91 | ('seller_email', models.CharField(blank=True, editable=False, max_length=100, null=True, verbose_name='卖家支付宝账号')), 92 | ('buyer_id', models.CharField(blank=True, editable=False, max_length=30, null=True, verbose_name='买家支付宝用户号')), 93 | ('buyer_email', models.CharField(blank=True, editable=False, max_length=100, null=True, verbose_name='买家支付宝账号 ')), 94 | ('total_fee', models.DecimalField(blank=True, decimal_places=2, editable=False, max_digits=6, null=True, verbose_name='总金额(单位:元)')), 95 | ], 96 | ), 97 | migrations.CreateModel( 98 | name='WeiXinPayResult', 99 | fields=[ 100 | ('order', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='pay_result', serialize=False, to='openunipay.WeiXinOrder')), 101 | ('prepayid', models.CharField(blank=True, db_index=True, editable=False, max_length=64, null=True, verbose_name='预支付交易会话标识')), 102 | ('openid', models.CharField(blank=True, editable=False, max_length=128, null=True, verbose_name='用户标识(openId)')), 103 | ('bank_type', models.CharField(blank=True, editable=False, max_length=16, null=True, verbose_name='付款银行')), 104 | ('total_fee', models.SmallIntegerField(blank=True, editable=False, null=True, verbose_name='总金额')), 105 | ('attach', models.CharField(blank=True, editable=False, max_length=128, null=True, verbose_name='商户附加数据')), 106 | ('tradestate', models.CharField(blank=True, editable=False, max_length=32, null=True, verbose_name='交易状态')), 107 | ('tradestatedesc', models.CharField(blank=True, editable=False, max_length=256, null=True, verbose_name='交易状态描述')), 108 | ], 109 | ), 110 | ] 111 | --------------------------------------------------------------------------------