├── 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 |
--------------------------------------------------------------------------------