├── .DS_Store ├── .gitattributes ├── .gitignore ├── LICENSE ├── LOGO ├── django_miniprogram_api.png └── zanshang.png ├── MANIFEST.in ├── README.md ├── __init__.py ├── miniprogram_api ├── __init__.py ├── admin.py ├── apps.py ├── constants.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_payorder.py │ ├── 0003_auto_20190923_2346.py │ ├── 0004_auto_20190924_0005.py │ └── __init__.py ├── models.py ├── serializers.py ├── tests.py ├── urls.py ├── views.py └── wechat_process.py ├── req.txt └── setup.py /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ti-technology/django_miniprogram_api/187dfa489e3555b6d657fce4ac70d9e48b417174/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | 125 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, 秦天琪 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /LOGO/django_miniprogram_api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ti-technology/django_miniprogram_api/187dfa489e3555b6d657fce4ac70d9e48b417174/LOGO/django_miniprogram_api.png -------------------------------------------------------------------------------- /LOGO/zanshang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ti-technology/django_miniprogram_api/187dfa489e3555b6d657fce4ac70d9e48b417174/LOGO/zanshang.png -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include LOGO * 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![django_miniprogram_api](./LOGO/django_miniprogram_api.png) 2 | 3 | Django MiniProgram API - Django 微信小程序 API 4 | ============================================ 5 | 6 | Django 微信小程序 API 是依赖于 django-rest-framework 制作的restful api,封装了微信小程序的登陆,用户资料更新,微信小程序支付等一系列操作。为开发者提供微信小程序后台的简便操作。 7 | 8 | (已完成用户登录,更新用户信息,以及微信小程序支付等一部分API,其他功能完善中……) 9 | 10 | ## 安装 11 | 12 | ```bash 13 | pip install django_miniprogram_api 14 | ``` 15 | 16 | 17 | 快速入门 18 | ------- 19 | 20 | 1. 添加 "miniprogram_api" 和 django-rest-framework 相关的 modules 以及 配置 到 INSTALLED_APPS,并且添加 WECHAT_MINIPROGRAM_CONFIG 配置文件:: 21 | 22 | ```python 23 | INSTALLED_APPS = [ 24 | 'miniprogram_api', 25 | 'rest_framework.authtoken', 26 | 'rest_framework' 27 | ] 28 | WECHAT_MINIPROGRAM_CONFIG = { 29 | "APPID": "", 30 | "SECRET": "", 31 | "WECHAT_PAY": { 32 | "MCH_ID": "", # 微信支付商户号 33 | "KEY": "", # API密钥 34 | "NOTIFICATION_URL": '', # 微信支付回调地址 35 | } 36 | } 37 | REST_FRAMEWORK = { 38 | 'DEFAULT_PERMISSION_CLASSES': [ 39 | ... 40 | 'rest_framework.authentication.BasicAuthentication', # add this 41 | 'rest_framework.authentication.TokenAuthentication', # add this 42 | ], 43 | 'DEFAULT_PARSER_CLASSES': ( 44 | 'rest_framework.parsers.JSONParser', 45 | 'rest_framework_xml.parsers.XMLParser', 46 | ), 47 | } 48 | ``` 49 | 50 | 51 | 52 | 2. 配置小程序登陆 url /miniprogram_auth/ 到你项目的 urls.py:: 53 | 54 | ```python 55 | url(r'^miniprogram_auth/', include('miniprogram_api.urls')), 56 | ``` 57 | 58 | 59 | 60 | 3. 运行 `python manage.py migrate` 来创建 WeChatAccount 模型. 61 | 62 | 4. 运行测试服务器 `python manage.py runserver 127.0.0.1:8000` 就可以开始使用了 63 | 64 | 使用 65 | --- 66 | 67 | ### 小程序登陆 68 | 69 | **请求** 70 | 71 | `http://127.0.0.1/miniprogram_auth/login` 72 | 73 | method: post, 74 | 75 | body: 76 | 77 | ```json 78 | { 79 | "code": "061YsgK50ru0wC1uCHH50D2mK50YsgKa" 80 | } 81 | ``` 82 | 83 | 登陆模块包括了微信 [auth.code2Session](https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html) 接口,开发者通过调用 [wx.login()](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html) 获取临时登录凭证code,发送给 我们的 Login api,获取 django 的用户登录状态 Token。 84 | 85 | **返回** 86 | 87 | ```json 88 | { 89 | "token": "fa7cd4cdb5554a9b69b876d6c6bf775ac6be250d", // 返回的token 需要包含在 request header 90 | "user_id": 1 91 | } 92 | ``` 93 | 94 | #### 使用 Token 保持会话 95 | 96 | 在你的请求头包含token信息,要注意的是如果你没有自定义的登录状态,例如:用户手机号邮箱注册登录,那么请在之后的api中都使用同样的token请求头。 97 | 98 | ```json 99 | Authorization: Token fa7cd4cdb5554a9b69b876d6c6bf775ac6be250d 100 | ``` 101 | 102 | ### 用户信息更新 103 | 104 | **请求** 105 | 106 | `http://127.0.0.1/miniprogram_auth/updateUserInfo` 107 | 108 | method: post, 109 | 110 | body: 111 | 112 | ```json 113 | { 114 | "iv":"QRWwdpUUx9zaN4fXGM4Asw==", 115 | "encryptedData": "F7VcR8vKZqzaEqS18f7qJ3VuYLl5AjExEHldqC3og3XOKlZPg+U9ki/onlrjrG9OLZDyJrno/nEegXH9V/1sMzGFpCCqhR9MHVTaq9fyANOVazniVmkzwysD0dwwk9bj4Uulz3KuqtTwoI2VFXEAmuj0kzCG1atqCo5RXZnZ30M8O3mbnSPAvDb6pEBBgT6YoQGuIskYQ82kIO3Z/ZtX8XCcmYAjagUkie1CGZUcYd5VxtSL6iGd+HVwxC1rspvda1OcgIdRlU/tIA3Euhbd4qKuqlmR6LJVdZNs9gg/CMY1ZGcRQnz8cbQWUqFOEaZQHU/oiXeDmo5V/HeQXzv9c+lgZ+SMk81VNLC8/T4SF5ivaoULHV/Th+jqYKDjJGwDAbM4tK+4Gkb45QFny3ZDh/09Fk9TwtfR2nkH/Wxpyyhkp0DPbhvd8oq8wH13I0XbsO0WuM0D8YpZF+H74CiiPDiKRzPEpLKU2nCWdlpHDZ0=" 116 | } 117 | ``` 118 | 119 | 开发者通过调用接口(如 [wx.getUserInfo](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserInfo.html))获取数据时,接口会同时返回 encryptedData, iv 数据,将此数据发送给updateUserInfo api,API 将会解密数据,以获取用户信息并返回。(此操作一般在小程序授权用户信息时使用,微信小程序的新登录规则,登陆实际上是限制的 [wx.getUserInfo](https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserInfo.html) 接口) 120 | 121 | **返回** 122 | 123 | ```json 124 | { 125 | "token": "fa7cd4cdb5554a9b69b876d6c6bf775ac6be250d", 126 | "wechat": { 127 | "id": 1, 128 | "nickName": "TINCHY", 129 | "avatarUrl": "https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTLp9mKpmqTUic0TmCMo6Cbibmsvmo6Vt3NGdP0cZOYRwoGPe13LsvHEicoZGvjq6syaeG0GGWJOrqCbA/132", 130 | "gender": "1", 131 | "city": "Shanghai", 132 | "province": null, 133 | "country": null, 134 | "user": 1 135 | } 136 | 137 | } 138 | ``` 139 | 140 | ### 微信支付 class wechat_pay.WeChatPay() 141 | 142 | 微信支付的api因为每一个操作都要求不同,不同用户不同场景都有需求,因此没有封装HTTP API,但是提供了一个简单封装的object,以及提供了一个订单状态 Model:PayOrder 143 | 144 | 想要获取订单状态,请将自己的商品 OneToOne 到 PayOrder, 例如: 145 | 146 | ```python 147 | class PickUpOrder(models.Model): 148 | wechat_order = models.OneToOneField(PayOrder) 149 | ... 150 | 151 | @receiver(post_save, sender=PickUpOrder) 152 | def create_order(sender, instance, created, **kwargs): 153 | if created: 154 | PayOrder.objects.create(pickuporder=instance, outTradeNo='') 155 | ``` 156 | 157 | **接口返回等数据请查询微信支付官方文档** https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1 158 | 159 | #### 统一下单 160 | 161 | ```python 162 | WeChatPay().unified_order( 163 | spbill_create_ip='''小程序用户的IP地址''', 164 | open_id='''小程序用户的open id''', 165 | body='''商品描述''', 166 | order_id='''订单id,必须唯一,建议使用日期时间戳''', 167 | total_fee='''订单金额,单位为分!!!!''' 168 | ) 169 | ``` 170 | 171 | **简单例子:** 172 | 173 | 微信统一下单接口:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1 174 | 175 | ```python 176 | 177 | from .model import PickupOrder # 这个model是我的测试model,用于订单查询 178 | from django.conf import settings # 导入 settings 179 | from .wechat_pay import WeChatPay, WeChatSignHelper # 导入 微信支付 api 以及 签名验证 180 | from miniprogram_api.model import WeChatAccount 181 | from rest_framework import views, status 182 | 183 | class WeChatPayAPIView(views.APIView): 184 | permission_classes = [IsAuthenticated] 185 | def post(self, request): 186 | from django.utils.datetime_safe import datetime 187 | data = request.data 188 | _id = data['id'] 189 | if not PickupOrder.objects.filter(id=data['id']).exists(): 190 | raise ValidationError('This order does not exists') 191 | item = PickupOrder.objects.get(id=data['id']) 192 | if not item.payorder.paid: # 如果未付款 193 | outTradeNo = datetime.utcnow().strftime('%Y%m%d%H%M%S%f')[:-3] # 生成 order_id 用时间精确到分秒以保证订单号的唯一性 194 | item.order_id = outTradeNo 195 | item.payorder.outTradeNo = outTradeNo # 将订单号保存到 数据库 196 | item.save() 197 | wechat_user = WeChatAccount.objects.get(user=self.request.user) 198 | wp = WeChatPay() 199 | address = self.request.META.get('HTTP_X_FORWARDED_FOR') # 获取小程序访问用户的 ip 地址 200 | if address: 201 | ip = address.split(',')[0] 202 | else: 203 | ip = address.META.get('REMOTE_ADDR') 204 | res = wp.unified_order(spbill_create_ip=ip,open_id=wechat_user.union_id, body=item.car_type.desc, total_fee=item.fee, order_id=item.order_id) 205 | if res['return_code'] == 'SUCCESS' and res['result_code'] == 'SUCCESS': 206 | pay_sign = { 207 | 'appId': settings.WECHAT_MINIPROGRAM_CONFIG['APPID'], 208 | 'nonceStr': wp.ranstr(16), 209 | 'package': 'prepay_id='+res['prepay_id'], 210 | 'signType': 'MD5', 211 | 'timeStamp': str(time.time()) 212 | } 213 | sign = WeChatSignHelper(pay_sign, settings.WECHAT_MINIPROGRAM_CONFIG['WECHAT_PAY']['KEY']).getSign() 214 | pay_sign['paySign'] = sign # 签名验证支付订单的正确性 215 | return Response({'pay_sign': pay_sign}) # 返回给小程序发起小程序的支付接口 216 | else: 217 | return Response("Make order failed", status=status.HTTP_406_NOT_ACCEPTABLE) 218 | ``` 219 | 220 | 下单之后,系统会根据您在settings.py中设置的 NOTIFICATION_URL 进行回调,来更新用户的订单状态。务必设置正确。(本地环境运行的服务器,微信无法进行回调,务必在生产或者测试服务器上运行) 221 | 222 | ```python 223 | WECHAT_MINIPROGRAM_CONFIG = { 224 | "WECHAT_PAY": { 225 | "NOTIFICATION_URL": 'http://www.example.com/miniprogram_auth/wechatPayCallback', # 填写你的服务器地址加回调域名 226 | } 227 | } 228 | ``` 229 | 230 | #### 查询订单 231 | 232 | 微信查询订单接口:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_2 233 | 234 | ```python 235 | WeChatPay().order_query( 236 | transaction_id='''微信的订单号,建议优先使用''', 237 | out_trade_no='''商户系统内部订单号,要求32个字符内, 这里指的是 order_id, 即订单号''' 238 | ) 239 | # transaction_id 和 out_trade_no 只需要选一个,不要全部填写 240 | ``` 241 | 242 | #### 关闭订单 243 | 244 | 微信关闭订单接口:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_3 245 | 246 | ```python 247 | WeChatPay().close_order( 248 | out_trade_no='''商户系统内部订单号,要求32个字符内, 这里指的是 order_id, 即订单号''' 249 | ) 250 | ``` 251 | 252 | ## 以下接口正在开发... 253 | 254 | #### [申请退款](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4) 255 | 256 | #### [查询退款](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_5) 257 | 258 | #### [下载对账单](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_6) 259 | 260 | #### [下载资金账单](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_18&index=7) 261 | 262 | #### [支付结果通知](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8) 263 | 264 | #### [交易保障](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_8&index=9) 265 | 266 | #### [退款结果通知](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_16&index=10) 267 | 268 | #### [拉取订单评价数据](https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_17&index=11) 269 | 270 | ## LICENSE 271 | 272 | BSD 3-Clause License 273 | 274 | ## 开发者 275 | 276 | Tinchy:tinchy@yeah.net 277 | 278 | ## 赞助 279 | 280 | ![zanshang](./LOGO/zanshang.png) -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ti-technology/django_miniprogram_api/187dfa489e3555b6d657fce4ac70d9e48b417174/__init__.py -------------------------------------------------------------------------------- /miniprogram_api/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | 5 | doc = "https://github.com/ti-technology/django_miniprogram_api" 6 | 7 | if not settings.WECHAT_MINIPROGRAM_CONFIG: 8 | raise ValueError(f"Wechat mini-program config is required, please check the doc {doc}") 9 | 10 | 11 | if not settings.WECHAT_MINIPROGRAM_CONFIG.get("APPID", None) or settings.WECHAT_MINIPROGRAM_CONFIG.get("APPID", None) == "": 12 | raise ValueError(f"Value APPID is required for this mini program, please check the doc {doc}") 13 | 14 | if not settings.WECHAT_MINIPROGRAM_CONFIG.get("SECRET", None) or settings.WECHAT_MINIPROGRAM_CONFIG.get("SECRET", None) == "": 15 | raise ValueError(f"Value SECRET KEY is required for this mini program, please check the doc {doc}") 16 | 17 | 18 | -------------------------------------------------------------------------------- /miniprogram_api/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /miniprogram_api/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MiniprogramAuthConfig(AppConfig): 5 | name = 'django_miniprogram_api.miniprogram_api' 6 | -------------------------------------------------------------------------------- /miniprogram_api/constants.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | if not settings.WECHAT_MINIPROGRAM_CONFIG: 4 | raise Exception 5 | 6 | 7 | def get_wechat_login_code_url(code): 8 | return f"https://api.weixin.qq.com/sns/jscode2session?appid={settings.WECHAT_MINIPROGRAM_CONFIG['APPID']}&secret={settings.WECHAT_MINIPROGRAM_CONFIG['SECRET']}&js_code={code}&grant_type=authorization_code" 9 | 10 | 11 | def get_wechat_update_uerinfo_url(): 12 | return -------------------------------------------------------------------------------- /miniprogram_api/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-21 03:54 2 | 3 | from django.conf import settings 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='WeChatAccount', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('nickName', models.CharField(max_length=100, null=True)), 22 | ('avatarUrl', models.CharField(max_length=255, null=True)), 23 | ('openId', models.CharField(max_length=255, null=True)), 24 | ('gender', models.CharField(max_length=100, null=True)), 25 | ('city', models.CharField(max_length=100, null=True)), 26 | ('province', models.CharField(max_length=100, null=True)), 27 | ('country', models.CharField(max_length=100, null=True)), 28 | ('unionId', models.CharField(max_length=255, null=True)), 29 | ('session_key', models.CharField(max_length=255, null=True)), 30 | ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 31 | ], 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /miniprogram_api/migrations/0002_payorder.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-23 13:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('miniprogram_api', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='PayOrder', 15 | fields=[ 16 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('appId', models.CharField(max_length=32)), 18 | ('mchId', models.CharField(max_length=32)), 19 | ('deviceInfo', models.CharField(max_length=32)), 20 | ('nonceStr', models.CharField(max_length=32)), 21 | ('sign', models.CharField(max_length=32, null=True)), 22 | ('signType', models.CharField(max_length=32)), 23 | ('body', models.CharField(max_length=128)), 24 | ('outTradeNo', models.CharField(max_length=32)), 25 | ('totalFee', models.IntegerField()), 26 | ('spBillCreateIp', models.CharField(max_length=64)), 27 | ('timeStart', models.CharField(max_length=14)), 28 | ('timeExpire', models.CharField(max_length=14)), 29 | ('notifyUrl', models.CharField(max_length=256)), 30 | ('tradeType', models.CharField(max_length=16)), 31 | ('openId', models.CharField(max_length=128)), 32 | ('returnCode', models.CharField(max_length=16, null=True)), 33 | ('returnMsg', models.CharField(max_length=128, null=True)), 34 | ('resultCode', models.CharField(max_length=16, null=True)), 35 | ('errCode', models.CharField(max_length=32, null=True)), 36 | ('errCodeDesc', models.CharField(max_length=128, null=True)), 37 | ('prepayId', models.CharField(max_length=64)), 38 | ], 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /miniprogram_api/migrations/0003_auto_20190923_2346.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-23 23:46 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('miniprogram_api', '0002_payorder'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='payorder', 15 | name='appId', 16 | ), 17 | migrations.RemoveField( 18 | model_name='payorder', 19 | name='body', 20 | ), 21 | migrations.RemoveField( 22 | model_name='payorder', 23 | name='deviceInfo', 24 | ), 25 | migrations.RemoveField( 26 | model_name='payorder', 27 | name='errCode', 28 | ), 29 | migrations.RemoveField( 30 | model_name='payorder', 31 | name='errCodeDesc', 32 | ), 33 | migrations.RemoveField( 34 | model_name='payorder', 35 | name='mchId', 36 | ), 37 | migrations.RemoveField( 38 | model_name='payorder', 39 | name='nonceStr', 40 | ), 41 | migrations.RemoveField( 42 | model_name='payorder', 43 | name='notifyUrl', 44 | ), 45 | migrations.RemoveField( 46 | model_name='payorder', 47 | name='prepayId', 48 | ), 49 | migrations.RemoveField( 50 | model_name='payorder', 51 | name='resultCode', 52 | ), 53 | migrations.RemoveField( 54 | model_name='payorder', 55 | name='returnCode', 56 | ), 57 | migrations.RemoveField( 58 | model_name='payorder', 59 | name='returnMsg', 60 | ), 61 | migrations.RemoveField( 62 | model_name='payorder', 63 | name='sign', 64 | ), 65 | migrations.RemoveField( 66 | model_name='payorder', 67 | name='signType', 68 | ), 69 | migrations.RemoveField( 70 | model_name='payorder', 71 | name='spBillCreateIp', 72 | ), 73 | migrations.RemoveField( 74 | model_name='payorder', 75 | name='timeExpire', 76 | ), 77 | migrations.RemoveField( 78 | model_name='payorder', 79 | name='timeStart', 80 | ), 81 | migrations.RemoveField( 82 | model_name='payorder', 83 | name='totalFee', 84 | ), 85 | migrations.RemoveField( 86 | model_name='payorder', 87 | name='tradeType', 88 | ), 89 | ] 90 | -------------------------------------------------------------------------------- /miniprogram_api/migrations/0004_auto_20190924_0005.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.5 on 2019-09-24 00:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('miniprogram_api', '0003_auto_20190923_2346'), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name='payorder', 15 | name='openId', 16 | ), 17 | migrations.AddField( 18 | model_name='payorder', 19 | name='paid', 20 | field=models.BooleanField(default=False), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /miniprogram_api/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ti-technology/django_miniprogram_api/187dfa489e3555b6d657fce4ac70d9e48b417174/miniprogram_api/migrations/__init__.py -------------------------------------------------------------------------------- /miniprogram_api/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.db import models 3 | 4 | # Create your models here. 5 | from django.db.models.signals import post_save 6 | from django.dispatch import receiver 7 | 8 | 9 | class WeChatAccount(models.Model): 10 | user = models.OneToOneField(User, on_delete=models.CASCADE) 11 | nickName = models.CharField(max_length=100, null=True,blank=False) 12 | avatarUrl = models.CharField(max_length=255, null=True,blank=False) 13 | openId = models.CharField(max_length=255, null=True,blank=False) 14 | unionId = models.CharField(max_length=100, null=True,blank=False) 15 | gender = models.CharField(max_length=100, null=True,blank=False) 16 | city = models.CharField(max_length=100, null=True,blank=False) 17 | province = models.CharField(max_length=100, null=True,blank=False) 18 | country = models.CharField(max_length=100, null=True,blank=False) 19 | unionId = models.CharField(max_length=255, null=True,blank=False) 20 | session_key = models.CharField(max_length=255, null=True,blank=False) 21 | def __str__(self): 22 | if self.nickName: 23 | return self.nickName 24 | else: 25 | return self.user.username 26 | 27 | @receiver(post_save, sender=User) 28 | def create_wechat_user(sender, instance, created, **kwargs): 29 | if created: 30 | WeChatAccount.objects.create(user=instance, nickName='') 31 | 32 | class PayOrder(models.Model): 33 | outTradeNo = models.CharField(max_length=32) 34 | paid = models.BooleanField(default=False) -------------------------------------------------------------------------------- /miniprogram_api/serializers.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from rest_framework import serializers 3 | from .models import * 4 | 5 | class UserSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = User 8 | fields = ['id', 'username', 'email', 'password'] 9 | write_only_fields = ['password'] 10 | 11 | 12 | class WeChatAccountSerializer(serializers.ModelSerializer): 13 | class Meta: 14 | model = WeChatAccount 15 | fields = '__all__' 16 | extra_kwargs = {'openId': {'write_only': True}, 'session_key': {'write_only': True}, 'unionId': {'write_only': True}} 17 | -------------------------------------------------------------------------------- /miniprogram_api/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /miniprogram_api/urls.py: -------------------------------------------------------------------------------- 1 | """ti_django_wechat_miniprogram URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/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: path('', 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: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf.urls import url 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | from .views import * 20 | urlpatterns = [ 21 | url(r'^login/', WeChatLoginAPIView.as_view(), name="login_api"), 22 | url(r'^updateUserInfo/', WeChatUserInfoUpdateAPIView.as_view(), name="update_userInfo_api"), 23 | url(r'^wechatPayCallback/', PayResultsNotice.as_view(), name="wechat_pay_callback_api") 24 | 25 | ] 26 | -------------------------------------------------------------------------------- /miniprogram_api/views.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import random 3 | import string 4 | 5 | import xmltodict 6 | from rest_framework.authentication import SessionAuthentication, TokenAuthentication 7 | 8 | from django.contrib.auth.models import User 9 | from django.shortcuts import render 10 | from pip._vendor import requests 11 | from rest_framework.authtoken.models import Token 12 | from rest_framework.permissions import IsAuthenticated 13 | from rest_framework.response import Response 14 | 15 | from .wechat_process import WeChatCrypt, WeChatSignHelper, WeChatPay 16 | from .models import * 17 | from .constants import * 18 | from .serializers import * 19 | from rest_framework import viewsets, generics, views, status 20 | from rest_framework_xml.parsers import XMLParser 21 | 22 | 23 | # Create your views here. 24 | 25 | ''' 26 | Mini Program Login 27 | @params: 28 | str:code 29 | ''' 30 | class WeChatLoginAPIView(views.APIView): 31 | permission_classes = [] 32 | def post(self, request): 33 | code = request.data.get('code', None) 34 | if not code: 35 | return Response({"code": "This field is required"}, status=status.HTTP_400_BAD_REQUEST) 36 | url = get_wechat_login_code_url(code) 37 | resp = requests.get(url) 38 | 39 | openid = None 40 | session_key = None 41 | unionid = None 42 | if resp.status_code != 200: 43 | return Response({"error": "WeChat server return error, please try again later"}) 44 | else: 45 | json = resp.json() 46 | if "errcode" in json: 47 | return Response({"error": json["errmsg"]}) 48 | else: 49 | openid = json['openid'] 50 | session_key = json['session_key'] 51 | 52 | if "unionid" in json: 53 | unionid = json['unionid'] 54 | 55 | if not session_key: 56 | return Response({"error": "WeChat server doesn't return session key"}) 57 | if not openid: 58 | return Response({"error": "WeChat server doesn't return openid"}) 59 | 60 | user = User.objects.filter(username=openid).first() 61 | if not user: 62 | user = User() 63 | user.username = openid 64 | password = ''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(10)) 65 | user.set_password(password) 66 | user.save() 67 | user.wechataccount.session_key = session_key 68 | user.wechataccount.openId = openid 69 | user.wechataccount.unionId = unionid 70 | user.wechataccount.save() 71 | user.save() 72 | 73 | token, created = Token.objects.get_or_create(user=user) 74 | if created: 75 | 76 | return Response({ 77 | 'token': token.key, 78 | 'user_id': user.id 79 | }) 80 | else: 81 | Token.objects.get(user=user).delete() 82 | token, created = Token.objects.get_or_create(user=user) 83 | 84 | return Response({ 85 | 'token': token.key, 86 | 'user_id': user.id 87 | }) 88 | 89 | 90 | ''' 91 | Mini Program Update UserInfo 92 | @params: 93 | str:encryptedData 94 | str:iv 95 | ''' 96 | class WeChatUserInfoUpdateAPIView(views.APIView): 97 | authentication_classes = [TokenAuthentication] 98 | permission_classes = [IsAuthenticated] 99 | 100 | def post(self, request): 101 | params = request.data 102 | encryptedData = params.get('encryptedData',None) 103 | iv = params.get('iv',None) 104 | 105 | if not encryptedData: 106 | return Response({"encryptedData": "This field is reuqired"}, status=status.HTTP_400_BAD_REQUEST) 107 | 108 | if not iv: 109 | return Response({"iv": "This field is reuqired"}, status=status.HTTP_400_BAD_REQUEST) 110 | 111 | wechat_user = WeChatAccount.objects.filter(user=request.user).first() 112 | pc = WeChatCrypt(settings.WECHAT_MINIPROGRAM_CONFIG['APPID'], wechat_user.session_key) 113 | 114 | user = pc.decrypt(encryptedData, iv) 115 | token = Token.objects.get(user=self.request.user) 116 | wechat_user.nickName = user['nickName'] 117 | wechat_user.gender = user['gender'] 118 | wechat_user.language = user['language'] 119 | wechat_user.city = user['city'] 120 | wechat_user.avatarUrl = user['avatarUrl'] 121 | wechat_user.save() 122 | return Response({'token': token.key, 'wechat': WeChatAccountSerializer(wechat_user).data}, status=status.HTTP_200_OK) 123 | 124 | ''' 125 | Mini Program Payment Callback 126 | ''' 127 | 128 | class PayResultsNotice(views.APIView): 129 | parser_classes = (XMLParser,) 130 | IGNORE_FIELDS_PREFIX = ["coupon_type_","coupon_id_","coupon_fee_"] 131 | def post(self,request): 132 | dataDict = xmltodict.parse(request.body)["xml"] #type:dict 133 | sign = WeChatSignHelper(dataDict,settings.WECHAT_MINIPROGRAM_CONFIG['WECHAT_PAY']['KEY']).getSign() 134 | if sign != dataDict['sign']: 135 | return Response(status=status.HTTP_403_FORBIDDEN) 136 | 137 | keyToBeDelete = [] 138 | for key in dataDict.keys(): 139 | for ignorePrefix in self.IGNORE_FIELDS_PREFIX: 140 | if ignorePrefix in key: 141 | keyToBeDelete.append(key) 142 | for key in keyToBeDelete: 143 | del dataDict[key] 144 | 145 | #process timeEnd 146 | dataDict['time_end'] = datetime.datetime.strptime(dataDict['time_end'],"%Y%m%d%H%M%S") 147 | 148 | dataDict['out_trade_no'] = dataDict['out_trade_no'] 149 | # assign order Id 150 | preOrder = PayOrder.objects.filter(out_trade_no=dataDict['out_trade_no']).first() 151 | 152 | if preOrder and not preOrder.paid: 153 | preOrder.paid = True 154 | preOrder.save() 155 | 156 | return Response(data=WeChatPay().dic_to_xml({'return_code':'SUCCESS'})) -------------------------------------------------------------------------------- /miniprogram_api/wechat_process.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | from Crypto.Cipher import AES 4 | from django.conf import settings 5 | doc = "https://github.com/ti-technology/django_miniprogram_api" 6 | 7 | ''' 8 | WeChat Crypt 9 | ''' 10 | class WeChatCrypt: 11 | def __init__(self, appId, sessionKey): 12 | self.appId = appId 13 | self.sessionKey = sessionKey 14 | 15 | def decrypt(self, encryptedData, iv): 16 | # base64 decode 17 | sessionKey = base64.b64decode(self.sessionKey) 18 | encryptedData = base64.b64decode(encryptedData) 19 | iv = base64.b64decode(iv) 20 | cipher = AES.new(sessionKey, AES.MODE_CBC, iv) 21 | decrypted = json.loads(self._unpad(cipher.decrypt(encryptedData))) 22 | 23 | if decrypted['watermark']['appid'] != self.appId: 24 | raise Exception('Invalid Buffer') 25 | 26 | return decrypted 27 | 28 | def _unpad(self, s): 29 | return s[:-ord(s[len(s)-1:])] 30 | 31 | 32 | ''' 33 | WeChat Pay Process 34 | ''' 35 | import json 36 | import random 37 | import string 38 | import hashlib 39 | 40 | import xmltodict 41 | 42 | import requests 43 | from xml.etree.ElementTree import * 44 | 45 | 46 | class WeChatPay: 47 | 48 | 49 | def __init__(self): 50 | if not settings.WECHAT_MINIPROGRAM_CONFIG.get("WECHAT_PAY", None): 51 | raise ValueError(f"Value WECHAT_PAY is required for this mini program, please check the doc {doc}") 52 | 53 | if not settings.WECHAT_MINIPROGRAM_CONFIG.get("WECHAT_PAY").get("MCH_ID", 54 | None) or settings.WECHAT_MINIPROGRAM_CONFIG.get( 55 | "WECHAT_PAY").get("MCH_ID", None) == "": 56 | raise ValueError(f"Value MCH_ID is required for WECHAT_PAY, please check the doc {doc}") 57 | 58 | if not settings.WECHAT_MINIPROGRAM_CONFIG.get("WECHAT_PAY").get("KEY", 59 | None) or settings.WECHAT_MINIPROGRAM_CONFIG.get( 60 | "WECHAT_PAY").get("KEY", None) == "": 61 | raise ValueError(f"Value KEY is required for WECHAT_PAY, please check the doc {doc}") 62 | 63 | if not settings.WECHAT_MINIPROGRAM_CONFIG.get("WECHAT_PAY").get("NOTIFICATION_URL", 64 | None) or settings.WECHAT_MINIPROGRAM_CONFIG.get( 65 | "WECHAT_PAY").get("NOTIFICATION_URL", None) == "": 66 | raise ValueError(f"Value NOTIFICATION_URL is required for WECHAT_PAY, please check the doc {doc}") 67 | self.appId = settings.WECHAT_MINIPROGRAM_CONFIG['APPID'] 68 | self.secret = settings.WECHAT_MINIPROGRAM_CONFIG['SECRET'] 69 | self.mch_id = settings.WECHAT_MINIPROGRAM_CONFIG["WECHAT_PAY"]['MCH_ID'] 70 | self.notify_url = settings.WECHAT_MINIPROGRAM_CONFIG["WECHAT_PAY"]['NOTIFICATION_URL'] 71 | 72 | def ranstr(self, num): 73 | salt = ''.join(random.sample(string.ascii_letters + string.digits, num)) 74 | 75 | return salt 76 | 77 | def unified_order(self, open_id, body, order_id, total_fee, spbill_create_ip): 78 | nonce_str = self.ranstr(16) 79 | url = 'https://api.mch.weixin.qq.com/pay/unifiedorder' 80 | payload = { 81 | 'appid': self.appId, 82 | 'body': body, 83 | 'mch_id': self.mch_id, 84 | 'nonce_str': nonce_str, 85 | 'notify_url': self.notify_url, 86 | 'openid': open_id, 87 | 'out_trade_no': str(order_id), 88 | 'spbill_create_ip': spbill_create_ip, 89 | 'trade_type': 'JSAPI', 90 | 'total_fee': int(total_fee), 91 | } 92 | sign = WeChatSignHelper(payload, settings.WECHAT_MINIPROGRAM_CONFIG["WECHAT_PAY"]['KEY']).getSign() 93 | payload['sign'] = sign 94 | payload = str(self.dic_to_xml(payload)) 95 | response = requests.post(url, data=payload.encode("utf-8")) 96 | data = self.xml_to_dict(response.content.decode()) 97 | return data 98 | 99 | def order_query(self, transaction_id=None, out_trade_no=None): 100 | url = "https://api.mch.weixin.qq.com/pay/orderquery" 101 | nonce_str = self.ranstr(8) 102 | if transaction_id: 103 | string_for_sign = "appid=" +self.appId + "&mch_id=" + self.mch_id + "&nonce_str=" + nonce_str + "&transaction_id=" + transaction_id 104 | elif out_trade_no: 105 | string_for_sign = "appid=" +self.appId + "&mch_id=" + self.mch_id + "&nonce_str=" + nonce_str + "&out_trade_no=" + out_trade_no 106 | sign = string_for_sign + settings.WECHAT_MINIPROGRAM_CONFIG["WECHAT_PAY"]['KEY'] 107 | sign = str(hashlib.md5(sign.encode())).upper() 108 | if transaction_id: 109 | payload = { 110 | 'appid': self.appId, 111 | 'mch_id': self.mch_id, 112 | 'nonce_str': nonce_str, 113 | 'sign': sign, 114 | 'transaction_id': transaction_id 115 | } 116 | elif out_trade_no: 117 | payload = { 118 | 'appid': self.appId, 119 | 'mch_id': self.mch_id, 120 | 'nonce_str': nonce_str, 121 | 'out_trade_no': out_trade_no, 122 | 'sign': sign, 123 | } 124 | 125 | response = requests.post(url, data=payload) 126 | return response.json() 127 | 128 | def close_order(self, out_trade_no): 129 | url = "https://api.mch.weixin.qq.com/pay/closeorder" 130 | nonce_str = self.ranstr(8) 131 | string_for_sign = "appid=" +self.appId + "&mch_id=" + self.mch_id + "&nonce_str=" + nonce_str 132 | sign = string_for_sign + settings.WECHAT_MINIPROGRAM_CONFIG["WECHAT_PAY"]['KEY'] 133 | sign = str(hashlib.md5(sign.encode())).upper() 134 | payload = { 135 | 'appid': self.appId, 136 | 'mch_id': self.mch_id, 137 | 'nonce_str': nonce_str, 138 | 'sign': sign, 139 | 'out_trade_no': out_trade_no 140 | } 141 | 142 | response = requests.post(url, data=payload) 143 | return response.json() 144 | 145 | def dic_to_xml(self,d): 146 | ele = '' 147 | for key, val in d.items(): 148 | ele += '<' + str(key) + '>' + str(val) + '' 149 | 150 | return ele + '' 151 | 152 | def xml_to_dict(self,xml): 153 | return xmltodict.parse(str(xml))['xml'] 154 | 155 | 156 | import collections 157 | import hashlib 158 | import copy 159 | 160 | class WeChatSignHelper: 161 | def __init__(self,dataDict,apiKey): 162 | self.data = copy.deepcopy(dataDict) #type: dict 163 | self.apiKey = apiKey 164 | self.keyValueString = "" 165 | self.clean_up() 166 | self.dataToKeyValueString() 167 | 168 | 169 | def clean_up(self): 170 | if "sign" in self.data: 171 | del self.data["sign"] 172 | 173 | def dataToKeyValueString(self): 174 | od = collections.OrderedDict(sorted(self.data.items())) 175 | for key,value in od.items(): 176 | self.keyValueString+=f"{key}={value}&" 177 | self.keyValueString += f"key={self.apiKey}" 178 | 179 | def getSign(self): 180 | md5Obj = hashlib.md5() 181 | md5Obj.update(self.keyValueString.encode("utf-8")) 182 | return md5Obj.hexdigest().upper() -------------------------------------------------------------------------------- /req.txt: -------------------------------------------------------------------------------- 1 | certifi==2019.9.11 2 | chardet==3.0.4 3 | defusedxml==0.6.0 4 | Django==2.2.5 5 | django-filter==2.2.0 6 | djangorestframework==3.10.3 7 | djangorestframework-xml==1.4.0 8 | idna==2.8 9 | Markdown==3.1.1 10 | Naked==0.1.31 11 | pycrypto==2.6.1 12 | pycryptodome==3.9.0 13 | pytz==2019.2 14 | PyYAML==5.1.2 15 | requests==2.22.0 16 | shellescape==3.4.1 17 | singledispatch==3.4.0.3 18 | six==1.12.0 19 | sqlparse==0.3.0 20 | urllib3==1.25.5 21 | xmltodict==0.12.0 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import find_packages,setup 3 | 4 | 5 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__),os.pardir))) 6 | 7 | setup( 8 | name='django_miniprogram_api', 9 | version='1.0.3.2', 10 | packages=find_packages(), 11 | include_package_data=True, 12 | license='BSD License', 13 | description='A simple Django app implemented the WeChat miniprogram\'s login, payment and other APIs', 14 | long_description='https://github.com/TinchyChing/django_miniprogram_api', 15 | long_description_content_type="text/markdown", 16 | url='https://github.com/TinchyChing/django_miniprogram_api', 17 | author='Tinchy', 18 | author_email='tinchy@yeah.net', 19 | install_requires = [ 20 | "requests", 21 | "pycrypto", 22 | "djangorestframework", 23 | "xmltodict", 24 | "djangorestframework-xml", 25 | "Django" 26 | ], 27 | 28 | classifiers=[ 29 | 'Environment :: Web Environment', 30 | 'Framework :: Django', 31 | 'Framework :: Django :: 2.1', # replace "X.Y" as appropriate 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: BSD License', # example license 34 | 'Operating System :: OS Independent', 35 | #'Programming Language :: Python', 36 | #'Programming Language :: Python :: 3.5', 37 | 'Programming Language :: Python :: 3.6', 38 | 'Topic :: Internet :: WWW/HTTP', 39 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 40 | ], 41 | ) --------------------------------------------------------------------------------