├── .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 | 
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 | 
--------------------------------------------------------------------------------
/__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) + '' + str(key) + '>'
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 | )
--------------------------------------------------------------------------------