前后端分离项目-DRF+VUE->媒体宝
媒体宝项目
01 创建项目(后端)
-
前端vue.js项目:city
https://gitee.com/wupeiqi/city
-
后端django项目:mtb
https://gitee.com/wupeiqi/mtb
项目代码的git上会同步更新,大家下载下来后,可以根据提交记录来进行回滚,查看看各个版本。
1.准备环境
1.1 虚拟环境&项目
-
在pycharm中创建项目【空Python项目】+【虚拟环境】
-
安装django
pip install django==3.2
-
创建django项目到当前目录,后面加上绝对路径避免出现嵌套目录
django-admin startproject mtb D:\data\1045699\Desktop\luffy代码\mtb
1.2 创建多app
-
项目根目录下创建apps目录
-
在apps目录下创建文件夹(app)
apps - task - msg - base
-
执行命令
python manage.py startapp task apps/task python manage.py startapp msg apps/msg python manage.py startapp base apps/base
1.3 注册app
-
在app目录下的app.py中修改
from django.apps import AppConfig class TaskConfig(AppConfig): name = 'task'
from django.apps import AppConfig class TaskConfig(AppConfig): name = 'apps.task'
-
在settings.py中注册
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', "apps.task.apps.TaskConfig" ]
1.4 基本配置
-
移除无用的app
INSTALLED_APPS = [ # 'django.contrib.admin', # 'django.contrib.auth', # 'django.contrib.contenttypes', # 'django.contrib.sessions', # 'django.contrib.messages', 'django.contrib.staticfiles', "apps.base.apps.BaseConfig" ]
-
移除无用的中间件
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', # 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', # 'django.middleware.csrf.CsrfViewMiddleware', # 'django.contrib.auth.middleware.AuthenticationMiddleware', # 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ]
-
数据库相关(MySQL)
-
创建数据库
create database mtb DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
-
设置数据库连接
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'mtb', 'USER': 'root', 'PASSWORD': 'root123', 'HOST': '127.0.0.1', 'PORT': 3306, } }
-
安装python操作MySQL模块,不用pymsql原因是在Django3中pymsql支持不那么友好
pip install mysqlclient
-
1.5 本地配置
避免泄漏数据库信息,将敏感内容放置local_settings.py中
-
settings.py
try: from .local_settings import * except ImportError: pass
-
local_settings.py
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'mydatabase', 'USER': 'mydatabaseuser', 'PASSWORD': 'mypassword', 'HOST': '127.0.0.1', 'PORT': '5432', } }
1.6 代码仓库
如果想要代码共享给他人 或 多人协同开发,来会发文件不方便。
如果想要保留自己之前编写功能的的版本,每次都生成一个文件夹也不方便。
程序员一般都用git来解决上述问题,想要用git需要两个步骤:
-
【自己电脑上】安装git + 学习git相关的命令。
Git 全局设置: git config --global user.name "fumiadder" git config --global user.email "2521532473@qq.com" 创建 git 仓库: mkdir ceshi cd ceshi git init touch README.md git add README.md git commit -m "first commit" git remote rm origin # 删除别名,没有别名请忽略这一步 git remote add origin git@gitee.com:fumiadder/ceshi.git # 起别名 git push -u origin "master" 已有仓库? cd existing_git_repo git remote add origin git@gitee.com:fumiadder/ceshi.git git push -u origin "master"
将不需要上传的文件名称添加至.gitignore文件中,可以去GitHub或gitee上找python的gitignore文件:
# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/
-
【代码仓库】创建项目,将本地项目推送上去,可共享给他人。 gitee
https://gitee.com/fumiadder/mtb/
1.7 启动&运行项目
配置Pycharm去运行。
2.认证
2.1 后端API
- 基于token,drf案例中的项目。
- 基于jwt【推荐】
jwt的原理是什么呢?https://www.cnblogs.com/wupeiqi/p/11854573.html
pip install pyjwt==2.3.0
-
生成jwt token
import jwt import datetime from jwt import exceptions SALT = 'iv%x6xo7l7_u9bf_u!9#g#m*)*=ej@bek5)(@u3kh*72+unjv=' def create_token(): # 构造header headers = { 'typ': 'jwt', 'alg': 'HS256' } # 构造payload payload = { 'user_id': 1, # 自定义用户ID 'username': 'wupeiqi', # 自定义用户名 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=5) # 超时时间 } result = jwt.encode(payload=payload, key=SALT, algorithm="HS256", headers=headers) return result if __name__ == '__main__': token = create_token() print(token) # eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Ind1cGVpcWkiLCJleHAiOjE2NDc2MjMzMDR9.mC409LXIl1RZu4OX5J01hvCxWEOJcK7C4P3zKzedXdU
-
校验
import jwt from jwt import exceptions SALT = 'iv%x6xo7l7_u9bf_u!9#g#m*)*=ej@bek5)(@u3kh*72+unjv=' def get_payload(token): """ 根据token获取payload :param token: :return: """ try: # 从token中获取payload【校验合法性,并获取payload】 verified_payload = jwt.decode(token, SALT, ["HS256"]) return verified_payload except exceptions.ExpiredSignatureError: print('token已失效') except jwt.DecodeError: print('token认证失败') except jwt.InvalidTokenError: print('非法的token') if __name__ == '__main__': token = "eyJ0eXAiOiJqd3QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6Ind1cGVpcWkiLCJleHAiOjE2NDc2MjMzMDR9.mC409LXIl1RZu4OX5J01hvCxWEOJcK7C4P3zKzedXdU" payload = get_payload(token) print(payload)
2.1.1 创建表和数据
-
创建相关表结构并录入基本数据。
from django.db import models class UserInfo(models.Model): username = models.CharField(verbose_name="用户名", max_length=32) password = models.CharField(verbose_name="密码", max_length=64)
-
离线脚本,创建用户
import os import sys import django base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(base_dir) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mtb.settings") django.setup() # 启动Django from apps.base import models """ 离线创建用户脚本 """ models.UserInfo.objects.create( username="fumi", password="123" )
2.1.2 登录
pip install djangorestframework==3.12.4
pip install pyjwt==2.3.0
-
编写URL
主路由
from django.urls import path, include from apps import base urlpatterns = [ path('api/base/', include('apps.base.urls')), # 路由分发 ]
子路由
from django.urls import path from rest_framework import routers from .views import account router = routers.SimpleRouter() # 评论 # router.register(r'comment', comment.CommentView) urlpatterns = [ # path('register/', account.RegisterView.as_view({"post": "create"})), path('auth/', account.AuthView.as_view()), ] urlpatterns += router.urls
-
编写视图 & 生成jwt token
base/views/account.py
import jwt import datetime from rest_framework.views import APIView from rest_framework.response import Response from ..serializers import account from util import return_code from .. import models from mtb import settings class AuthView(APIView): authentication_classes = [] permission_classes = [] def post(self, request, *args, **kwargs): # print(request.user) # print(request.auth) # 1.获取前端用户名和密码进行数据校验 serializer = account.AuthSerializer(data=request.data) if not serializer.is_valid(): return Response({'code': return_code.VALIDATE_ERROR, 'errors': serializer.errors}) # 2.数据库校验 username = serializer.validated_data.get('username') password = serializer.validated_data.get('password') user_obj = models.UserInfo.objects.filter(username=username, password=password).first() if not user_obj: return Response({'code': return_code.AUTH_FAILED, 'detail': '用户名或密码错误'}) # 3.生成jwt token返回 # 构造header headers = { 'typ': 'jwt', 'alg': 'HS256' } # 构造payload payload = { 'user_id': user_obj.id, # 自定义用户ID 'username': user_obj.username, # 自定义用户名 'exp': datetime.datetime.now() + datetime.timedelta(minutes=5) # 超时时间 } token = jwt.encode(payload=payload, key=settings.SECRET_KEY, algorithm="HS256", headers=headers) return Response({'code': return_code.SUCCESS, 'data': {'token': token, 'username': username}})
util/return_code.py
# 成功 SUCCESS = 0 # 用户提交数据校验失败 VALIDATE_ERROR = 1001 # 认证失败 AUTH_FAILED = 2000 # 认证过期 AUTH_OVERDUE = 2001 # 无权访问 PERMISSION_DENIED = 3000 # 无权访问 TOO_MANY_REQUESTS = 4000
base/serializers/account.py
from rest_framework import serializers class AuthSerializer(serializers.Serializer): username = serializers.CharField(label='用户名', required=True) password = serializers.CharField(label='密码', min_length=3, required=True)
2.1.3 校验
在请求其他页面时,对jwt token进行校验(认证组件)。
- 测试API
- 编写认证组件
- 全局配置
base/urls.py
from django.urls import path
from rest_framework import routers
from .views import account
router = routers.SimpleRouter()
urlpatterns = [
path('auth/', account.AuthView.as_view()),
path('test/', account.TestView.as_view())
]
urlpatterns += router.urls
util/extension/auth.py (认证校验组件)
import jwt
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from jwt import exceptions
from .. import return_code
from mtb import settings
# 自定义封装类,将校验合法的verified_payload封装成对象返回
class CurrentUser(object):
def __init__(self, user_id, username, exp):
self.user_id = user_id
self.username = username
self.exp = exp
class JwtTokenAuthentication(BaseAuthentication):
def authenticate(self, request):
# 读取用户提交的jwt token
# token = request.query_params.get("token") # 这是从url中获取
token = request.META.get('HTTP_AUTHORIZATION') # 这是从请求头中获取 格式 Authorization: Jwt Token
if not token:
raise AuthenticationFailed({"code": return_code.AUTH_FAILED, "error": "无token认证失败"})
# jwt token校验
try:
# 从token中获取payload【校验合法性,并获取payload】
verified_payload = jwt.decode(token, settings.SECRET_KEY, ["HS256"])
# print(verified_payload) # {'user_id': 1, 'username': 'fumi', 'exp': 1661311234}
return CurrentUser(**verified_payload), token
except exceptions.ExpiredSignatureError:
print('token已失效')
raise AuthenticationFailed({'code': return_code.AUTH_FAILED, 'error': 'jwt token已失效'})
except jwt.DecodeError:
print('token认证失败')
raise AuthenticationFailed({'code': return_code.AUTH_FAILED, 'error': 'jwt token认证失败'})
except jwt.InvalidTokenError:
print('非法的token')
raise AuthenticationFailed({'code': return_code.AUTH_FAILED, 'error': 'jwt token非法'})
except Exception as e:
raise AuthenticationFailed({'code': return_code.AUTH_FAILED, 'error': 'jwt token不合法导致认证失败'})
def authenticate_header(self, request):
return 'Bearer realm="API"'
account.py
# 校验测试API
class TestView(APIView):
def get(self, request, *args, **kwargs):
print(request.user.user_id) # 1
print(request.user.username) # fumi
print(request.user.exp) # 1661311234
return Response('test')
settings.py
REST_FRAMEWORK = {
# 认证配置
"DEFAULT_AUTHENTICATION_CLASSES": ["util.extension.auth.JwtTokenAuthentication", ],
# 配置上解决报错RuntimeError: Model class django.contrib.contenttypes.models.ContentType
# doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS.
"UNAUTHENTICATED_USER": lambda: None,
"UNAUTHENTICATED_TOKEN": lambda: None,
}
2.2 前端vue
- 打开页面:在本地的cookie中读取 username,写入到 state (便于后续页面使用)
- 路由拦截:在本地的cookie中读取 jwt token,如果有则继续访问,没有就跳转登录页面。
- 登录:
- 发送请求,验证用户名密码合法性
- 写入cookie和state
- 跳转
- 其他API请求(每个请求都要在请求头中 携带jwt token)
2.2.1 打开页面
浏览器打开平台页面时,自动去cookie中读取之前登录写入到cookie中的用户名和token
npm install vue-cookie
plugins/cookie.js
import Vue from 'vue'
import VueCookie from 'vue-cookie'
Vue.use(VueCookie)
export const getToken = () => {
return Vue.cookie.get("token");
}
export const getUserName = () => {
return Vue.cookie.get("username");
}
he plugin is available through this.$cookie in components or Vue.cookie
// From some method in one of your Vue components
this.$cookie.set('test', 'Hello world!', 1);
// This will set a cookie with the name 'test' and the value 'Hello world!' that expires in one day
// To get the value of a cookie use
this.$cookie.get('test');
// To delete a cookie use
this.$cookie.delete('test');
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import {getToken, getUserName} from '@/plugins/cookie'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
username: getUserName(),
token: getToken(),
},
mutations: {},
actions: {},
modules: {}
})
main.js
import Vue from 'vue'
import App from './App.vue'
import './plugins/cookie.js'
import store from './store'
import router from './router'
import './plugins/element.js'
import HighchartsVue from 'highcharts-vue'
Vue.use(HighchartsVue)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
views/Layout.vue
<template>
<div>
<el-menu class="el-menu-demo" mode="horizontal" :default-active=defaultActiveRouter
background-color="#545c64" text-color="#fff" active-text-color="#ffd04b" router> <!--是否使用 vue-router 的模式,启用该模式会在激活导航时以 index 作为 path 进行路由跳转-->
<el-menu-item >媒体宝logo</el-menu-item>
<el-menu-item index="Task" :route="{name:'Sblb'}">任务宝</el-menu-item>
<el-menu-item index="Msg" :route="{name:'Xxgl'}">消息宝</el-menu-item> <!--当前激活菜单的 index-->
<el-menu-item index="Auth" :route="{name:'Sqgl'}">授权宝</el-menu-item>
<el-submenu index="5" style="float: right">
<template slot="title">{{username}}</template>
<el-menu-item index="5-1">个人信息</el-menu-item>
<el-menu-item index="5-2">注销</el-menu-item>
</el-submenu>
</el-menu>
<div>
<router-view></router-view>
</div>
</div>
</template>
<script>
export default {
name: "Layout",
data(){
return{
defaultActiveRouter:"",
}
},
mounted(){
this.defaultActiveRouter = this.$route.matched[1].name; // 拿到路由中第2个参数名即index解决默认选中和刷新选中问题
},
computed:{
username(){
return this.$store.state.username; // 只有get属性直接简写
}
}
}
</script>
<style scoped>
</style>
2.2.2 路由拦截器
如果cookie中的 token 为空,则重定向到登录页面。
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import {getToken} from '@/plugins/cookie'
Vue.use(VueRouter)
...
router.beforeEach((to, from, next) => {
let token = getToken();
// 如果已登录,则可以继续访问目标地址
if (token) {
next();
return;
}
// 未登录,访问登录页面
if (to.name === "Login") {
next();
return;
}
// 未登录,跳转登录页面
// next(false); 保持当前所在页面,不跳转
next({name: 'Login'});
})
2.2.3 登录
npm install axios
npm install vue-axios
plugins/axios.js
import Vue from 'vue'
import axios from 'axios'
import VueAxios from 'vue-axios'
Vue.use(VueAxios, axios)
this.axios.get(
"URL地址",
{
headers:{
....
},
params:{
...
}
}
)
this.axios.post(
URL,
{},
{
headers:{
....
},
params:{
...
}
}
)
main.js
import Vue from 'vue'
import App from './App.vue'
import './plugins/cookie' // 放在router和store前面,这样他们需要使用就可以导入
import './plugins/axios'
import router from './router'
import store from './store'
import './plugins/element.js'
import HighchartsVue from 'highcharts-vue'
Vue.use(HighchartsVue)
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
views/Login.vue
<div v-show="tabSelected===0">
<el-form :rules="userInforules" :model="userInfo" ref="userInfo"> <!-- 点击提交表单必须是ref, rules指规则, model指存放数据-->
<el-form-item prop="username" style="margin-top: 24px;" :error="userFormError.username"> <!--prop指对哪一项规则进行校验--> <!--:error绑定错误信息为了后端错误在标签展示-->
<el-input placeholder="手机号或邮箱" v-model="userInfo.username"></el-input>
</el-form-item>
<el-form-item prop="password" :error="userFormError.password">
<el-input placeholder="密码" v-model="userInfo.password" show-password></el-input>
</el-form-item>
<el-form-item size="large">
<el-button type="primary" size="small" @click="submitForm('userInfo')">登 录</el-button> <!--提交表单将ref里的表单名传入到点击方法里-->
</el-form-item>
</el-form>
</div><el-form :model="userForm" :rules="rules" ref="userForm">
<el-form-item prop="username" class="row-item" :error="userFormError.username">
<el-input v-model="userForm.username" placeholder="用户名或手机号或邮箱"></el-input>
</el-form-item>
<el-form-item prop="password" class="row-item" :error="userFormError.password">
<el-input placeholder="请输入密码" v-model="userForm.password" show-password></el-input>
</el-form-item>
<el-form-item class="row-item">
<el-button type="primary" size="medium" @click="submitForm('userForm')">登 录</el-button>
</el-form-item>
</el-form>
userFormError: {
username: "",
password: "",
},
submitForm(formName){
// 1.在执行校验规则前先清空字段错误信息
this.clearCustomFormError();
// 2.根据传入的表单名不同分别执行他们的验证规则
this.$refs[formName].validate((valid)=>{
if (!valid){
// console.log('验证未通过');
return false
}
// console.log('验证通过');
// 验证通过,向后端API发送ajax请求
this.axios.post(' http://127.0.0.1:8000/api/base/auth/', this.userInfo).then(res=>{
// 登录成功
if (res.data.code === 0){
// 写入cookie, 写入state 后端返回数据{code:0, data:{username:'用户名', token:"jwt token"}}
this.$store.commit('login', res.data.data.username, res.data.data.token);
this.$router.push({path:'/'}); // 跳转到主页
return
}
// 1000, 字段错误, 前端在标签上进行展示
if (res.data.code === 1000){
// detail = {username:[错误1,], password:[x1,]}
this.validateFormFailed(res.data.detail);
}
// 1001, 整体错误, 前端整体弹窗展示
if (res.data.code === 1001){
this.$message.error(res.data.detail);
}else {
this.$message.error('请求错误');
}
})
});
},
// 字段错误自定义函数
validateFormFailed(errorData) {
for (let fieldName in errorData) {
let error = errorData[fieldName][0];
this.userFormError[fieldName] = error;
}
},
// 自定义函数
clearCustomFormError() {
for (let key in this.formErrorDict) {
this.userFormError[key] = ""
}
},
后端认证返回数据优化
base/views/account.py
# 登录API
class AuthView(APIView):
authentication_classes = []
permission_classes = []
def post(self, request, *args, **kwargs):
# print(request.user)
# print(request.auth)
# 1.获取前端用户名和密码进行数据校验
serializer = account.AuthSerializer(data=request.data)
if not serializer.is_valid():
# 1000, 字段错误, 返回前端在标签上进行展示
return Response({'code': return_code.FIELD_ERROR, 'errors': serializer.errors})
# 2.数据库校验
username = serializer.validated_data.get('username')
password = serializer.validated_data.get('password')
user_obj = models.UserInfo.objects.filter(username=username, password=password).first()
if not user_obj:
# 1001, 整体错误, 返回前端整体弹窗展示
return Response({'code': return_code.VALIDATE_ERROR, 'detail': '用户名或密码错误'})
# 3.生成jwt token返回
# 构造header
headers = {
'typ': 'jwt',
'alg': 'HS256'
}
# 构造payload
payload = {
'user_id': user_obj.id, # 自定义用户ID
'username': user_obj.username, # 自定义用户名
'exp': datetime.datetime.utcnow() + datetime.timedelta(seconds=20) # 超时时间
}
token = jwt.encode(payload=payload, key=settings.SECRET_KEY, algorithm="HS256", headers=headers)
return Response({'code': return_code.SUCCESS, 'data': {'token': token, 'username': username}})
return_code.py
# 用户提交数据校验失败(字段错误)
FIELD_ERROR = 1000
# 用户提交数据校验失败(整体用户名或密码错误)
VALIDATE_ERROR = 1001
后端API需要解决跨域问题:可以编写
关于跨域:
- 响应头
- 复杂请求(发送2个请求)
- options预检
- post请求
- 简单请求
- post请求
https://www.cnblogs.com/wupeiqi/articles/5703697.html
后端mtb/util/middleware/cors.py
from django.utils.deprecation import MiddlewareMixin
# 为了解决前端同源策略问题,在此自定义中间件
class CorsMiddleware(MiddlewareMixin):
def process_response(self, request, response):
response['Access-Control-Allow-Origin'] = '*'
response['Access-Control-Allow-Headers'] = '*'
response['Access-Control-Allow-Method'] = '*'
return response
settings.py
MIDDLEWARE = [
....
'util.middleware.cors.CorsMiddleware', # 为了解决前端同源策略问题,在此自定义中间件
]
BUG
mutation中的参数。
// this.$store.commit('login', res.data.data.username, res.data.data.token); //有bug,后面的token传不过去导致mutations里的token是undefined
this.$store.commit("login", res.data.data);
import Vue from 'vue'
import Vuex from 'vuex'
import {getUserName, getToken, setUserToken} from "@/plugins/cookie"
Vue.use(Vuex)
export default new Vuex.Store({
state: {
username: getUserName(),
token: getToken(),
},
mutations: {
// {username, token}加括号解包,也可以直接info.username,info.token
login: function (state, {username, token}) {
state.username = username;
state.token = token;
// Vue.cookie.set("username",username);
// Vue.cookie.set("token",token);
setUserToken(username, token);
}
},
actions: {},
modules: {}
})
2.2.4 axios默认值和拦截器
探讨2个问题:
-
其他的页面中是不是也会需要发送请求,发送时要携带token,怎么携带?
-
默认值
-
请求拦截器
-
-
如果token过期了怎么办?【有人主动在cookie随机设置了jwt token】
-
响应拦截器,每次请求返回结果时,先执行的代码。
判断返回值的内容。 - 状态码401,内容 code==="2000",跳转到登录界面 + 清空cookie中的数据+ state中的数据 - 其他,继续向后执行,正常结果的处理。
-
- 设备列表 vue放入created方法
views/task/sanji/Sblb.vue
created() {
this.axios.get("http://127.0.0.1:8000/api//base/test/").then(res => {
console.log("请求成功", res);
}).catch(reason => {
console.log('请求失败', reason);
})
}
-
默认值,登录成功后,以后每次发请求,都应该在请求头中携带token,每次发送请求都携带吗?
-
拦截器,如果有人伪造token向API发送了请求,跳转到登录页面(只要认证失败,跳到登录页面)。
-
新增友好展示element ui 中局部引入Message, Message.error("认证过期,请重新登录...");
plugins/axios.js
import Vue from 'vue' import axios from 'axios' import VueAxios from 'vue-axios' import {getToken} from './cookie' import router from "../router/index" import store from "../store/index" import {Message} from "element-ui" Vue.use(VueAxios, axios) // 设置默认值 axios.defaults.baseURL = 'http://127.0.0.1:8000/api/'; // 简写,方便访问不同的测试api接口 axios.defaults.headers.common['Authorization'] = getToken(); // 只在页面刷新时执行 // axios.defaults.headers.post['Content-Type'] = 'application/json'; // 跟进不同请求设置不同的请求头 // axios.defaults.headers.put['Content-Type'] = 'application/json'; // 请求拦截器,axios发送请求时候,每次请求 axios.interceptors.request.use(function (config) { // 在发送请求之前做些什么 const token = getToken(); if (token) { // 表示用户已登录 config.headers.common['Authorization'] = token; } return config; }); // 响应拦截器 axios.interceptors.response.use(function (response) { // API请求执行成功,响应状态码200,自动执行 // 若返回code为2000,执行 if (response.data.code === '2000'){ // 1.执行store中的logout方法清空cookie和state中的token store.commit("logout"); // 2.重定向登录页面 [Login,] // router.push({name:"Login"}); router.replace({name: "Login"}); // replace比push更友好,push是直接在数组中添加login,replace直接替换 // 3.页面提示 Message.error("认证过期,请重新登录..."); return Promise.reject('状态码200,但code为2000') } return response; }, function (error) { // API请求执行失败,响应状态码400/500,自动执行 if (error.response.status === 401){ // 1.执行store中的logout方法清空cookie和state中的token store.commit("logout"); // 2.重定向登录页面 [Login,] // router.push({name:"Login"}); router.replace({name: "Login"}); // replace比push更友好,push是直接在数组中添加login,replace直接替换 // 3.页面提示 Message.error("认证过期,请重新登录..."); } return Promise.reject(error); // 下一个相应拦截器的第二个函数 })
store/index.js
import Vue from 'vue' import Vuex from 'vuex' import {getUserName, getToken} from '@/plugins/cookie' import {clearUserToken, setUserToken} from "../plugins/cookie"; Vue.use(Vuex) export default new Vuex.Store({ state: { username: getUserName(), token: getToken(), }, mutations: { // {username, token}加括号解包,也可以直接info.username,info.token login: function (state, {username, token}) { // 写入state state.username = username; state.token = token; // 写入cookie,为了更好维护,自定义函数设置 // Vue.cookie.set('username', username); // Vue.cookie.set('token', token); setUserToken(username, token); }, logout: function (state) { state.username = ""; state.token = ""; clearUserToken(); } }, actions: { }, modules: { } })
plugins/cookie.js
import Vue from 'vue' import VueCookie from 'vue-cookie' Vue.use(VueCookie) export const getToken = () => { return Vue.cookie.get("token"); } export const getUserName = () => { return Vue.cookie.get("username"); } // 自定义将token设置到cookie中并设置有效期 export const setUserToken = (username, token) => { Vue.cookie.set('username', username, {expires: '7D'}); Vue.cookie.set('token', token, {expires: '7D'}); } // 删除cookie中的token等值 export const clearUserToken = () => { Vue.cookie.delete('username'); Vue.cookie.delete('token'); }
-
后端
util/extension/auth.py
import jwt from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from jwt import exceptions from .. import return_code from mtb import settings # 自定义认证失败状态码200,根据返回code判断 class MtbAuthenticationFailed(AuthenticationFailed): status_code = 200 class JwtTokenAuthentication(BaseAuthentication): def authenticate(self, request): # 读取用户提交的jwt token # token = request.query_params.get("token") # 这是从url中获取 token = request.META.get('HTTP_AUTHORIZATION') # 这是从请求头中获取 格式 Authorization: Jwt Token if not token: # 自定义状态码200,内容{'code':2000, 'error':'无token认证失败'},前端根据code=2000判断 # raise MtbAuthenticationFailed({"code": return_code.AUTH_FAILED, "error": "无token认证失败"}) # 状态码401,内容{'code':2000, 'error':'无token认证失败'} raise AuthenticationFailed({"code": return_code.AUTH_FAILED, "error": "无token认证失败"}) # jwt token校验 try: # 从token中获取payload【校验合法性,并获取payload】 verified_payload = jwt.decode(token, settings.SECRET_KEY, ["HS256"]) # print(verified_payload) # {'user_id': 1, 'username': 'fumi', 'exp': 1661311234} return CurrentUser(**verified_payload), token except exceptions.ExpiredSignatureError: print('token已失效') # 自定义状态码200,内容{'code':2000, 'error':'无token认证失败'},前端根据code=2000判断 raise MtbAuthenticationFailed({'code': return_code.AUTH_FAILED, 'error': 'jwt token已失效'}) except jwt.DecodeError: print('token认证失败') raise AuthenticationFailed({'code': return_code.AUTH_FAILED, 'error': 'jwt token认证失败'}) ....
settings.py
MIDDLEWARE = [ 'util.middleware.cors.CorsMiddleware', # 为了解决前端同源策略问题,在此自定义中间件,同时解决预检返回401问题,排序第一 .... ]
小结
- jwt是什么?与传统的token有什么不同。
- django的中间件 vs drf的认证组件
- vue-cookie组件:读、写、删除 + 自定义导出函数(把cookie相关功能写在一起)
- vue-router组件:路由守卫,读取本地本地cookie => 没有就返回登录界面。
- vuex组件:state中存储用户信息,在mutation中操作:state和cookie
- axios组件:
- 基本发送请求
- 默认值
- 请求拦截器
- 相应拦截器
注意:短信登录不在此实现,可以参考我在路飞上讲的《轻量级Bug管理平台》,里面有短信登录的逻辑和处理过程。
02 业务开发:媒体宝
媒体宝,新媒体营销平台,让平台帮你管理微信公众号,实现对粉丝、活动、消息等管理。
- 授权管理,打通平台与企业微信公众号的管理,只有授权后,平台才能接入微信,并自动化获取公众号的关注、取关、接收、发送消息等。
- 任务宝,发布微信活动任务,扫码转发并邀请好友助力,做裂变任务并配备相关奖励。
- 消息宝,基于平台实现批量的公众号消息管理,例如:消息发送、图文消息、定时任务等。
提示:想要开发此项目,你必须准备以下东西。
- 注册一个域名
- 购买一台云服务器,必须有公网IP(win和linux都可以)
- 将域名解析到此公网IP上
- 微信开发平台:企业的营业执照,这种平台只有企业才能申请出权限,个人不开放。
- 注册一个 已认证的公众号 或 服务号,用于自己开发测试。
1.服务器和域名
1.1 购买云服务器
自己可以去阿里云和腾讯云购买(促销活动比较便宜)
关于系统:Linux或windows,根据自己的情况去选择。
1.2 设置密码
设置密码,通过远程桌面连接,例如:
服务器公网IP:124.222.193.204
服务器账户:Administrator
服务器密码:Buxuyaomima*
服务器公网IP:42.143.28.82
服务器账户:Administrator
服务器密码:zg6208848000.
1.3 远程登录
win
Mac
其实,这就相当于你有了另外一个电脑了,不过这个电脑具有外网IP,别人可以根据IP找到他。
1.4 Python和Django
咱们在这台服务器上安装python和django并运行起来。
创建项目并启动
外网访问:
再次启动:
1.4 购买域名
1.5 解析
就是让域名和我们刚才买的服务器绑定,以后通过域名就可以找到那台服务器,不需要再使用IP了。
解析成功后,基于域名就可以访问了。
2.必备物料-开放平台
2.1 注册开放平台
平台:https://open.weixin.qq.com/
文档:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/operation/open/create.html
注册微信开发者平台并填写企业资质。
2.2 资质认证
注册成功后需要进行开发者资质认证。
2.3 创建第三方平台
根据提示,按照自己的公司信息去填写即可。注意:选择平台型服务商
2.4 配置第三方平台
2.4.1 公众号集权
把公众号中的所有权限打开。
2.4.2 开发资料(重要)
设置好集权后,继续向下拉,就可以看到开发者资料配置。
下载校验文件(留着一会用)
3.必备物料-公众号
选择:订阅号(认证)、服务号
4.媒体宝-环境
由于代码需要放在服务器上才能让所有的功能正常运行,所以,开发测试时也需要将代码同步到服务器。可以用的代码同步方案有三种:
- 基于IDE的Deployment的功能实现
- 基于git + 手动pull的方案
- 基于git + hook + 自动git pull(模拟公司的持续集成&持续交付)
4.1 本地->Gitee
-
第一步:【本机】在自己电脑上安装git(一路next即可)
https://git-scm.com/
-
第二步:【gitee】在gitee上注册账号并创建仓库
- city,vue项目
- mtb,django项目
-
第三步:【本机】将代码push到gitee仓库
只需在git动作中执行一次
git config --global user.name "武沛齐" git config --global user.email "wupeiqi@live.com"
在每个仓库执行一次
cd 项目目录 git init git remote add origin https://gitee.com/fumiadder/city.git
cd 项目目录 git add . git commit -m '提交信息'
git push origin master
4.2 Gitee的Hook
在gitee的项目中可以设置hook,当你向项目提交代码时,他可以自动向某个地址发送请求,表示有新代码提交了。
4.3 部署脚本
在服务器上基于Flask开发一个持续集成的程序,监听gitee的提交,只要提交,就在服务器上去更新项目代码。基于git去gitee拉最新代码:
在hook脚本中体现若不存在项目目录,用clone下载全部代码
cd 项目上级目录
git clone git项目的网址
git clone https://gitee.com/fumiadder/mtb.git
在hook脚本中体现若存在项目目录,用pull下载部分代码
cd 项目目录
git pull origin master
所以需要提前:
-
在服务器上安装git
https://registry.npmmirror.com/binary.html?path=git-for-windows/v2.37.2.windows.2/
-
在服务器上安装python + flask
pip3 install flask -i http://pypi.douban.com/simple --trusted-host pypi.douban.com
基于Flask编写的部署脚本:
import os
import subprocess
from flask import Flask, request
app = Flask(__name__)
@app.route("/hook", methods=["POST"])
def hook():
repo_dict = {
"city": {
"folder": r"C:\code",
"install": "npm install"
},
"mtb": {
"folder": r"C:\code",
"install": "pip3.9 install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt --user"
}
}
# 项目名称:mtb city
repo_name = request.json['project']['path']
# 放在服务器的那个目录?
local_info = repo_dict.get(repo_name)
if not local_info:
return "error"
# 项目的父级
parent_folder_path = local_info['folder']
install_command = local_info['install']
# 项目的目录
project_file_path = os.path.join(parent_folder_path, repo_name)
# git仓库地址 https://gitee.com/wupeiqi/mtb.git
git_http_url = request.json['project']['git_http_url']
# 项目目录是否存在
if not os.path.exists(project_file_path):
# cd c:\code
# git clone https://gitee.com/wupeiqi/mtb.git
subprocess.check_call('git clone {}'.format(git_http_url), shell=True, cwd=parent_folder_path)
else:
# cd c:\code\mtb
# git pull origin master
subprocess.check_call('git pull origin master', shell=True, cwd=project_file_path)
# 安装依赖包
# cd c:\code\mtb
# pip3.9 install -r requirements.txt
subprocess.check_call(install_command, shell=True, cwd=project_file_path)
return "success"
if __name__ == '__main__':
app.run(host="0.0.0.0", port=9000, debug=True)
请求确保已完成的步骤:
-
gitee上设置了webhook
-
服务器安全组加上了9000端口
-
服务器上已安装git
-
服务器上已安装python + node.js
-
服务器上的python已安装flask框架
4.4 数据库
由于我们程序组要运行在服务器上,所以MySQL必须安在服务器上,才能正常运行,所以,接下来你需要做两步:
-
服务器上安装MySQL & 创建用户 & 授权 & 创建数据库mtb
create database mtb DEFAULT CHARSET utf8 COLLATE utf8_general_ci; create user mtb@'%' identified by 'root123'; grant all privileges on mtb.* to 'mtb'@'%'; flush privileges;
-
mtb项目连接数据库配置修改 & 重新初始化数据
# 若服务器中mtb项目中没有local_settings,记得添加,这样才能连上数据库 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'mtb', 'USER': 'mtb', 'PASSWORD': 'root123', 'HOST': '43.143.28.82', 'PORT': 3306, } }
python manage.py makemigrations python manage.py migrate # 因为本地还有migrations,所以直接migrate,连上的服务器数据库会自动创建相应表,以后若push时未将migrations上传,就得重新执行makemigrations
注意:在服务器的安全组中一定要打开3306端口。
4.5 运行&开发
环境准备好后,接下来就需要让程序运行起来并测试,后续开发只需要做到一下几点:
-
在服务器上开启部署的Hook脚本。
-
在服务器上运行起来django程序
-
修改
ALLOWED_HOSTS = []
ALLOWED_HOSTS = [*]
-
手动在服务器上runserver启动django(修改文件自动重启)
python manage.py runserver 0.0.0.0:8000
-
在腾讯云开启8000端口
-
-
在服务器上运行vue程序
-
修改axios发送请求的地址
127.0.0.1:xxxx 改为 124.222.193.204:8000
-
启动vue程序
npm run serve -- --port 80
-
后续开发时,编写 + git提交。
小结
以后本地只做代码的编写,想要测试看效果,就需要将代码提交到gitee,由gitee和服务器hook脚本进行代码更新,这样就可以看到同步开发效果。
本质是:搞了一个线上开发机。
他与公司线上服务器的正式环境不同点有:
- 线上服务器Linux操作系统 vs windows操作系统
- 线上代码Nginx+uwsgi运行项目 vs 项目用django、npm自带的功能运行(性能差)
- 线上持续集成&交付用的jekins vs 用自己写的hook脚本
5.媒体宝-授权
授权,让公众号的商户管理员扫码,授予我们平台来管理公众号。
从功能的角度来看,我们的程序需要为用户提交一个二维码页面,公众号管理员扫码授权,之后如果公众再收到消息、关注、取消关注都会向我们指定的URL发送请求,从而实现管理公众号。
从技术上,如果想要实现此功能,需要好几个步骤(主要用于去微信获取凭证、token等过程),划分为三类:
- 校验
- 授权 + 令牌
- 消息回调
校验的过程:
- 校验,在开发平台设置时需要处理
授权+令牌的过程:
-
component_verify_ticket
在第三方平台创建审核通过后,微信服务器每隔10分钟会向我们指定的接口推送一次 component_verify_ticket。 component_verify_ticket有效期为12h 用于获取第三方平台接口调用凭据 component_access_token 。
-
component_access_token
component_access_token有效期2h. 当遭遇异常没有及时收到component_verify_ticket时,建议以上一次可用的component_verify_ticket继续生成component_access_token。避免出现因为 component_verify_ticket 接收失败而无法更新 component_access_token 的情况。
-
pre_auth_code
基于component_access_token生成pre_auth_code 用于生成扫码授权二维码或者链接需要的pre_auth_code 根据pre_auth_code -> 浏览器跳转
-
根据pre_auth_code生成URL,跳转到微信页面去扫码授权
生成的URL中需要传入一个redirect_uri的参数(一个地址),用户扫码成功后会自动向此地址发请求。
-
authorization_code
扫码授权成功后,微信向redirect_uri发送请求时,会携带 authorization_code + 过期时间。 注意:authorization_code是扫码后微信给我我们的。
-
authorizer_access_token【调用令牌】
获取授权码authorization_code后,可根据authorization_code去获取authorizer_access_token,例如: { "authorization_info": { "authorizer_appid": "wxf8b4f85f3a794e77", "authorizer_access_token": "QXjUqNqfYVH0yBE1iI_", "expires_in": 7200, "authorizer_refresh_token": "dTo-YCXPL4llX-u1W1pPpnp8Hgm4wpJtlR6iV0doKdY", "func_info":[...] } } authorizer_access_token 非常重要,后续获取公众号信息都会用到。 注意: authorizer_access_token的有效期2小时(有限额需缓存),在过期后用authorizer_refresh_token重新去获取。
消息回调的过程:
5.1 校验
这个其实不需要写任何代码来实现,后续再项目部署时,只需要在nginx中配置URL和内容返回即可。
正式在Linux项目部署时:
开发机服务器:
所以,在开发机搞了一个mysite。
from django.urls import path, include
from apps.base.views import wx
urlpatterns = [
path('api/base/', include('apps.base.urls')),
path('<str:filename>.txt', wx.file_verify),
]
from django.http import FileResponse
def file_verify(request, filename):
file = open('4631992212.txt', 'rb')
response = FileResponse(file)
response['Content-Type'] = 'application/octet-stream'
response['Content-Disposition'] = 'attachment;filename="%s.txt"' % filename
return response
在根目录下创建文件vue.config.js,然后填入如下内容
// 免除域名校验,支持域名解析到ip访问
module.exports = {
devServer: {
disableHostCheck: true,
}
}
5.2 授权+令牌
5.2.1 component_verify_ticket
在第三方平台创建审核通过后,微信服务器每隔10分钟会向我们指定的接口推送一次 component_verify_ticket(开启ticket推送)且有效期component_verify_ticket有效期为12h。
接收 POST 请求后,只需直接返回字符串 success。为了加强安全性,postdata 中的 xml 将使用服务申请时的加解密 key 来进行加密,具体请见《加密解密技术方案》, 在收到推送后需进行解密(详细请见《消息加解密接入指引》)。
我们需要做以下几个步骤:
- 编写
/auth/
的地址接收推送消息(加密) - 对推送消息解密获取 component_verify_ticket(SDK-py2 + key)
- 将 component_verify_ticket 持久化(写入 redis 或 数据库 )
实现步骤:
-
编写URL
mtb/urls.py
from django.urls import path, include from apps.base.views import wx urlpatterns = [ path('api/base/', include('apps.base.urls')), path('<str:filename>.txt', wx.file_verify), path('auth/', wx.component_verify_ticket), ]
-
编写配置
mtb/local_settings.py
WX_TOKEN = "c32b4c29-ac7e-4ebc-b7f4-6125159bbf11" WX_KEY = "a4537fb5112343a48bb9af33e8074d636125159bbf1" WX_APP_ID = "wx89d0d065c7b25a06" WX_APP_SECRET = "e359cf673dae224e976d75d00dbec0a6"
-
编写视图
apps/base/views/wx.py
注意:依赖微信的SDK,但是他支持py2,不支持py3(需要手动修改) pip install pycryptodome==3.14.1
import time import xml.etree.cElementTree as ET from django.http import FileResponse, HttpResponse from django.conf import settings from utils.wx2.WXBizMsgCrypt import WXBizMsgCrypt from .. import models def file_verify(request, filename): """ 文件校验 """ file = open('4631992212.txt', 'rb') response = FileResponse(file) response['Content-Type'] = 'application/octet-stream' response['Content-Disposition'] = 'attachment;filename="%s.txt"' % filename return response def component_verify_ticket(request): """ 微信每隔10分钟以POST请求发送一次 """ if request.method != "POST": return HttpResponse('error') # 获取请求体数据 body = request.body.decode("utf-8") # 获取URL中的数据 nonce = request.GET.get("nonce") timestamp = request.GET.get('timestamp') msg_sign = request.GET.get('msg_signature') # 解密 decrypt_test = WXBizMsgCrypt(settings.WX_TOKEN, settings.WX_KEY, settings.WX_APP_ID) code, decrypt_xml = decrypt_test.DecryptMsg(body, msg_sign, timestamp, nonce) # code=0时 解密成功 if code != 0: return HttpResponse('error') # 解析decrypt_xml,格式如下 """ <xml> <AppId><![CDATA[wx89d0d065c7b25a06]]></AppId> <CreateTime>1648305909</CreateTime> <InfoType><![CDATA[component_verify_ticket]]></InfoType> <ComponentVerifyTicket><![CDATA[ticket@@@fAovP2Qo9vbcdJ_O6sw6r2APV2jTQZJkeV73OBnazo6rTDhC85I8ywcY_wqXhthC5AFRNHg_aNuiAl7xljFf-w]]></ComponentVerifyTicket> </xml> """ xml_tree = ET.fromstring(decrypt_xml) # 没获取节点 verify_ticket = xml_tree.find("ComponentVerifyTicket") if verify_ticket is None: return HttpResponse('error') # 节点为空 verify_ticket_text = verify_ticket.text if not verify_ticket_text: return HttpResponse('error') # 写入数据库(过期时间12h) period_time = int(time.time()) + 12 * 60 models.WxCode.objects.update_or_create(defaults={"value": verify_ticket_text, "period": period_time}, code_type=1) return HttpResponse('success')
接下来,就要等待了,10分钟后会有请求发到服务器。
5.2.2 显示二维码
点点击添加公众号后:
先根据 component_verify_ticket 生成 component_access_token【有效期2小时】
再根据 component_access_token 生成 pre_auth_code【有效期10分钟】
最后用 pre_auth_code 拼接一个URL,返回给前端
前端页面,获取URL再进行跳转。
1.vue页面
<el-card class="box-card flex-row-center" shadow="hover" >
<div class="flex-col-center">
<i @click="toAuthorization()" class="el-icon-circle-plus-outline icon"></i>
<div class="text">添加公众号</div>
</div>
</el-card>
<script>
export default {
name: 'Auth',
created: function () {
},
methods: {
toAuthorization() {
// 发送请求获取 微信二维码URL的页面,跳转过去
this.axios.get("/base/wxurl/").then(res => {
if (res.data.code === 0) {
window.location.href = res.data.data.url;
} else {
this.$message.error("请求失败");
}
})
}
}
}
</script>
2.drf接口
-
依赖包
pip install requests pip freeze > requirements.txt 注意:建议提交一次并push一次。
-
数据库字段
from django.db import models class WxCode(models.Model): """ 微信授权相关的码 """ code_type_choices = ( (1, "component_verify_ticket"), (2, "component_access_token"), (3, "pre_auth_code"), ) code_type = models.IntegerField(verbose_name="类型", choices=code_type_choices) # value = models.CharField(verbose_name="值", max_length=128) value = models.CharField(verbose_name="值", max_length=255) period = models.PositiveIntegerField(verbose_name="过期时间") class UserInfo(models.Model): username = models.CharField(verbose_name="用户名", max_length=32) password = models.CharField(verbose_name="密码", max_length=64)
-
apps/base/urls.py
urlpatterns = [ path('auth/', account.AuthView.as_view()), path('test/', account.TestView.as_view()), # http://mtb.pythonav.com/api/base/wxurl/ path('wxurl/', wx.WxUrlView.as_view()), path('wxcallback/', wx.WxCallBackView.as_view(), name='wx_callback'), ]
-
apps/base/views/wx.py
import time import xml.etree.cElementTree as ET import requests from django.conf import settings from django.urls import reverse from django.http import FileResponse, HttpResponse from rest_framework.views import APIView from rest_framework.response import Response from utils.wx2.WXBizMsgCrypt import WXBizMsgCrypt from utils import return_code from .. import models def file_verify(request, filename): """ 文件校验 """ file = open('4631992212.txt', 'rb') response = FileResponse(file) response['Content-Type'] = 'application/octet-stream' response['Content-Disposition'] = 'attachment;filename="%s.txt"' % filename return response def component_verify_ticket(request): """ 微信每隔10分钟以POST请求发送一次 """ if request.method != "POST": return HttpResponse('error') # 获取请求体数据 body = request.body.decode("utf-8") # 获取URL中的数据 nonce = request.GET.get("nonce") timestamp = request.GET.get('timestamp') msg_sign = request.GET.get('msg_signature') # 解密 pip install pycryptodome decrypt_test = WXBizMsgCrypt(settings.WX_TOKEN, settings.WX_KEY, settings.WX_APP_ID) code, decrypt_xml = decrypt_test.DecryptMsg(body, msg_sign, timestamp, nonce) # code=0时 解密成功 if code != 0: return HttpResponse('error') # 解析decrypt_xml,格式如下 """ <xml> <AppId><![CDATA[wx89d0d065c7b25a06]]></AppId> <CreateTime>1648305909</CreateTime> <InfoType><![CDATA[component_verify_ticket]]></InfoType> <ComponentVerifyTicket><![CDATA[ticket@@@fAovP2Qo9vbcdJ_O6sw6r2APV2jTQZJkeV73OBnazo6rTDhC85I8ywcY_wqXhthC5AFRNHg_aNuiAl7xljFf-w]]></ComponentVerifyTicket> </xml> """ xml_tree = ET.fromstring(decrypt_xml) # 没获取节点 verify_ticket = xml_tree.find("ComponentVerifyTicket") if verify_ticket is None: return HttpResponse('error') # 节点为空 verify_ticket_text = verify_ticket.text if not verify_ticket_text: return HttpResponse('error') # 写入数据库(过期时间12h) period_time = int(time.time()) + 12 * 60 * 60 # 不存在,写;存在,更新。 models.WxCode.objects.update_or_create(defaults={"value": verify_ticket_text, "period": period_time}, code_type=1) return HttpResponse('success') class WxUrlView(APIView): def create_component_access_token(self): """ 根据 component_verify_ticket 生成新的component_access_token并写入数据库""" verify_ticket_object = models.WxCode.objects.filter(code_type=1).first() res = requests.post( url="https://api.weixin.qq.com/cgi-bin/component/api_component_token", json={ "component_appid": settings.WX_APP_ID, "component_appsecret": settings.WX_APP_SECRET, "component_verify_ticket": verify_ticket_object.value } ) data_dict = res.json() print(data_dict) access_token = data_dict["component_access_token"] period_time = int(data_dict["expires_in"]) + int(time.time()) models.WxCode.objects.update_or_create(defaults={"value": access_token, "period": period_time}, code_type=2) return access_token def create_pre_auth_code(self, access_token): # 生成预授权码 res = requests.post( url="https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode", params={ "component_access_token": access_token }, json={ "component_appid": settings.WX_APP_ID } ) data_dict = res.json() pre_auth_code = data_dict["pre_auth_code"] period_time = int(data_dict["expires_in"]) + int(time.time()) models.WxCode.objects.update_or_create(defaults={"value": pre_auth_code, "period": period_time}, code_type=3) return pre_auth_code def create_qr_code_url(self, pre_auth_code): redirect_uri = "{}{}".format("http://mtb.pythonav.com", reverse("wx_callback")) auth_url = "https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid={}&pre_auth_code={}&redirect_uri={}&auth_type=1" target_url = auth_url.format(settings.WX_APP_ID, pre_auth_code, redirect_uri) return target_url def get(self, request, *args, **kwargs): """ 生成URL并返回,用户跳转到微信扫码授权页面 """ # 去数据库获取预授权码(10分钟有效期) pre_auth_code_object = models.WxCode.objects.filter(code_type=3).first() pre_exp_time = pre_auth_code_object.period if pre_auth_code_object else 0 # 预授权码还在有效期,直接生成URL返回 if int(time.time()) < pre_exp_time: # redirect_uri = "http://mtb.pythonav.com/api/base/wxcallback/" url = self.create_qr_code_url(pre_auth_code_object.value) return Response({"code": return_code.SUCCESS, "data": {"url": url}}) # 根据 component_verify_ticket 获取 component_access_token(有效期2小时) access_token_object = models.WxCode.objects.filter(code_type=2).first() expiration_time = access_token_object.period if access_token_object else 0 if int(time.time()) >= expiration_time: # 已过期或没有 access_token = self.create_component_access_token() else: # 未过期 access_token = access_token_object.value # 根据 component_access_token 生成预pre_auth_code授权码(10分钟有效期) pre_auth_code = self.create_pre_auth_code(access_token) url = self.create_qr_code_url(pre_auth_code) return Response({"code": return_code.SUCCESS, "data": {"url": url}}) class WxCallBackView(APIView): def get(self, request, *args, **kwargs): auth_code = request.GET.get("auth_code") expires_in = request.GET.get("expires_in") print(auth_code, expires_in) return Response(auth_code)
5.2.3 扫码授权
1.登录公众平台【测试】
登录微信公众平台,获取自己公众号或服务号的原始ID
https://mp.weixin.qq.com/cgi-bin/loginpage
2.登录开放平台【测试】
登录开放平台,填写测试公众号或服务号原始ID。
3.扫码授权
4.获取authorization_code
在我们的回调地址中可以获取到authorization_code(需要解决登录状态)。
5.获取authorizer_access_token
-
authorization_code获取
authorizer_access_token
和authorizer_refresh_token
。res = requests.post( url="https://api.weixin.qq.com/cgi-bin/component/api_query_auth", params={ "component_access_token": access_token }, json={ "component_appid": settings.WX_APP_ID, # 固定的APP_ID "authorization_code": auth_code } ) result = res.json() authorizer_appid = result['authorization_info']['authorizer_appid'] authorizer_access_token = result['authorization_info']['authorizer_access_token'] authorizer_refresh_token = result['authorization_info']['authorizer_refresh_token'] authorizer_period = int(result['authorization_info']['expires_in']) + int(time.time()) 文档:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/authorization_info.html
-
获取公众号信息
res = requests.post( url="https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info", params={ "component_access_token": access_token }, json={ "component_appid": settings.WX_APP_ID, "authorizer_appid": authorizer_appid } ).json() nick_name = res["authorizer_info"]["nick_name"] user_name = res["authorizer_info"]["user_name"] # 原始ID avatar = res["authorizer_info"]["head_img"] service_type_info = res["authorizer_info"]["service_type_info"]["id"] verify_type_info = res["authorizer_info"]["verify_type_info"]["id"] # 文档:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/api_get_authorizer_info.html
-
保存
class PublicNumbers(models.Model): authorizer_app_id = models.CharField(verbose_name="授权ID", max_length=64) authorizer_access_token = models.CharField(verbose_name="授权token", max_length=255) authorizer_refresh_token = models.CharField(verbose_name="授权更新token", max_length=64) authorizer_period = models.PositiveIntegerField(verbose_name="过期时间") nick_name = models.CharField(verbose_name="公众号名称", max_length=32) user_name = models.CharField(verbose_name="公众号原始ID", max_length=64) avatar = models.CharField(verbose_name="公众号头像", max_length=128) service_type_info = models.IntegerField( verbose_name="公众号类型", choices=( (0, "订阅号"), (1, "由历史老帐号升级后的订阅号"), (2, "服务号") ) ) verify_type_info = models.IntegerField( verbose_name="认证类型", choices=( (-1, "未认证"), (0, "微信认证"), (1, "新浪微博认证"), (2, "腾讯微博认证"), (3, "已资质认证通过但还未通过名称认证"), (4, "已资质认证通过、还未通过名称认证,但通过了新浪微博认证"), (5, "已资质认证通过、还未通过名称认证,但通过了腾讯微博认证"),) ) mtb_user = models.ForeignKey(verbose_name="媒体宝用户", to="UserInfo", on_delete=models.CASCADE)
models.PublicNumbers.objects.update_or_create( defaults={ "authorizer_app_id": authorizer_appid, "authorizer_access_token": authorizer_access_token, "authorizer_refresh_token": authorizer_refresh_token, "authorizer_period": authorizer_period, # 2小时 "nick_name": nick_name, "avatar": avatar, "service_type_info": service_type_info, "verify_type_info": verify_type_info, }, mtb_user_id=request.user.user_id, user_name=user_name )
5.2.4 展示 授权公众号
{
"code": 0,
"data": [
{
"id": 1,
"nick_name": "公众号名称...",
"avatar": "http://wx.qlogo.cn/mmop...ia/0",
"service_type_info_text": "服务号",
"verify_type_info_text": "未认证"
}
]
}
1.vue页面
加载页面时发送请求当前用户已授权的公众号信息。
<el-card v-for="item in dataList" :key="item.id" class="box-card box-item" shadow="hover" :body-style="{width:'100%',padding:'20px'}">
<div class="item flex-row-center">
<el-avatar size="large" :src="item.avatar"></el-avatar>
</div>
<div class="item flex-row-center">{{item.nick_name}}</div>
<div class="item flex-row-center">
<div class="flex-row-between" style="width: 100px;font-size: 12px;">
<div style="color: gray">{{item.service_type_info_text}}</div>
<div style="color: #0c8eff;">{{item.verify_type_info_text}}</div>
</div>
</div>
<el-divider></el-divider>
<div class="item small flex-row-between">
<div><i class="el-icon-position"></i> 任务包</div>
<div class="date">永久</div>
</div>
<div class="item small flex-row-between">
<div><i class="el-icon-bell"></i> 消息宝</div>
<div class="date">永久</div>
</div>
</el-card>
<script>
export default {
name: 'Auth',
data() {
return {
dataList: []
}
},
created: function () {
this.initDataList();
},
methods: {
toAuthorization() {
// 发送请求获取 微信二维码URL的页面,跳转过去
// http://mtb.pythonav.com/api/base/wxurl/
this.axios.get("/base/wxurl/").then(res => {
// {code:0,data:{url:"微信跳转URL"}}
if (res.data.code === 0) {
window.location.href = res.data.data.url;
} else {
this.$message.error("请求失败");
}
})
},
initDataList() {
this.axios.get("/base/public/").then(res => {
// {code:0,data:[]}
if (res.data.code === 0) {
this.dataList = res.data.data;
} else {
this.$message.error("请求失败");
}
})
}
}
}
</script>
2.drf接口
代码太多,详细请参考源码
5.2.5 取消授权
如果用户在公众平台中取消了授权,我们的平台怎么知道呢?
其实当用户取消授权后,会自动执行我们之前在开放平台定义的(就是没10分钟执行一次的请求)。
<xml>
<AppId><![CDATA[wx89d0d065c7b25a06]]></AppId>
<CreateTime>1648305909</CreateTime>
<InfoType><![CDATA[component_verify_ticket]]></InfoType>
<ComponentVerifyTicket><![CDATA[ticket@@@fAovP2Q8ywcY_wqXhthC5AFRNHg_aNuiAl7xljFf-w]]></ComponentVerifyTicket>
</xml>
<xml>
<AppId><![CDATA[wx89d0d065c7b25a06]]></AppId>
<CreateTime>1648455334</CreateTime>
<InfoType><![CDATA[unauthorized]]></InfoType>
<AuthorizerAppid><![CDATA[wx75cd30b4c2693497]]></AuthorizerAppid>
</xml>
5.2.6 获取数据
当授权公众号之后,我们平台就可以接管公众号,接收数据。
from django.urls import path, include
from apps.base.views import wx
urlpatterns = [
path('api/base/', include('apps.base.urls')),
path('<str:filename>.txt', wx.file_verify), # 微信调用
path('auth/', wx.component_verify_ticket), # 微信调用
path('<str:authorizer_app_id>/callback', wx.event_callback), # 微信调用
]
def event_callback(request, authorizer_app_id):
""" 公众号的消息与事件接收配置 """
if request.method != "POST":
return HttpResponse('error')
# 获取请求体数据
body = request.body.decode("utf-8")
# 获取URL中的数据
nonce = request.GET.get("nonce")
timestamp = request.GET.get('timestamp')
msg_sign = request.GET.get('msg_signature')
# 解密 pip install pycryptodome
decrypt_test = WXBizMsgCrypt(settings.WX_TOKEN, settings.WX_KEY, settings.WX_APP_ID)
code, decrypt_xml = decrypt_test.DecryptMsg(body, msg_sign, timestamp, nonce)
# code=0时 解密成功
if code != 0:
return HttpResponse('error')
print('--------')
print(authorizer_app_id, decrypt_xml)
return HttpResponse("success")
<xml>
<ToUserName><![CDATA[gh_f2e17c23c9ca]]></ToUserName>
<FromUserName><![CDATA[oko631YbM3Mq-0tewUUVH1rOAAJY]]></FromUserName>
<CreateTime>1648457984</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[武沛齐]]></Content>
<MsgId>23600239816888806</MsgId>
</xml>
<xml>
<ToUserName><![CDATA[gh_f2e17c23c9ca]]></ToUserName>
<FromUserName><![CDATA[oko631YbM3Mq-0tewUUVH1rOAAJY]]></FromUserName>
<CreateTime>1648458242</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<PicUrl><![CDATA[http://mmbiz.qpic.cn/mmbiz_jpg/1cPbuONfU6P3WuLnOAVg/0]]></PicUrl>
<MsgId>23600244842458443</MsgId>
<MediaId><![CDATA[RanDds2osD-3NBMlFC5NRQCG1b9jKmeYhujUleqe]]></MediaId>
</xml>
<xml>
<ToUserName><![CDATA[gh_f2e17c23c9ca]]></ToUserName>
<FromUserName><![CDATA[oko631YbM3Mq-0tewUUVH1rOAAJY]]></FromUserName>
<CreateTime>1648458550</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[VIEW]]></Event>
<EventKey><![CDATA[https://m.ke.qq.com/agencyHome.html?_bid=1rom=wechat]]></EventKey>
<MenuId>454021062</MenuId>
</xml>
<xml>
<ToUserName><![CDATA[gh_f2e17c23c9ca]]></ToUserName>
<FromUserName><![CDATA[oko631YbM3Mq-0tewUUVH1rOAAJY]]></FromUserName>
<CreateTime>1648458077</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[]]></EventKey>
</xml>
<xml>
<ToUserName><![CDATA[gh_f2e17c23c9ca]]></ToUserName>
<FromUserName><![CDATA[oko631YbM3Mq-0tewUUVH1rOAAJY]]></FromUserName>
<CreateTime>1648458011</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[unsubscribe]]></Event>
<EventKey><![CDATA[]]></EventKey>
</xml>
6.媒体宝-消息宝
消息宝的核心功能就是代替公众号向粉丝发送消息,微信为公众号向粉丝发送消息提供了两类:
-
模板消息
https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html
-
客服消息,只有48小时内有互动的才能发送客服消息。
https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html#%E5%AE%A2%E6%9C%8D%E6%8E%A5%E5%8F%A3-%E5%8F%91%E6%B6%88%E6%81%AF
6.1 模板消息
在公众平台上可以选择添加消息模板。
https://mp.weixin.qq.com/advanced/tmplmsg?action=list&t=tmplmsg/list&token=180795866&lang=zh_CN
调用微信提供的接口去发送模板消息:
http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN
{
"touser":"OPENID",
"template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
"data":{
"first": {
"value":"恭喜你购买成功!",
"color":"#173177"
},
"keyword1":{
"value":"巧克力",
"color":"#173177"
},
"keyword2": {
"value":"39.8元",
"color":"#173177"
},
"keyword3": {
"value":"2014年9月22日",
"color":"#173177"
},
"remark":{
"value":"欢迎再次购买!",
"color":"#173177"
}
}
}
6.1.1 获取所有模板
https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html
获取已添加至帐号下所有模板列表,可在微信公众平台后台中查看模板列表信息。为方便第三方开发者,提供通过接口调用的方式来获取帐号下所有模板信息,具体如下:
接口调用请求说明
http请求方式:GET https://api.weixin.qq.com/cgi-bin/template/get_all_private_template?access_token=ACCESS_TOKEN
参数说明
参数 | 是否必须 | 说明 |
---|---|---|
access_token | 是 | 接口调用凭证 |
返回说明
正确调用后的返回示例:
{
"template_list": [{
"template_id": "iPk5sOIt5X_flOVKn5GrTFpncEYTojx6ddbt8WYoV5s",
"title": "领取奖金提醒",
"primary_industry": "IT科技",
"deputy_industry": "互联网|电子商务",
"content": "{ {result.DATA} }\n\n领奖金额:{ {withdrawMoney.DATA} }\n领奖 时间: { {withdrawTime.DATA} }\n银行信息:{ {cardInfo.DATA} }\n到账时间: { {arrivedTime.DATA} }\n{ {remark.DATA} }",
"example": "您已提交领奖申请\n\n领奖金额:xxxx元\n领奖时间:2013-10-10 12:22:22\n银行信息:xx银行(尾号xxxx)\n到账时间:预计xxxxxxx\n\n预计将于xxxx到达您的银行卡"
}]
}
6.1.2 获取粉丝OpenID
https://developers.weixin.qq.com/doc/offiaccount/User_Management/Getting_a_User_List.html
公众号可通过本接口来获取帐号的关注者列表,关注者列表由一串OpenID(加密后的微信号,每个用户对每个公众号的OpenID是唯一的)组成。一次拉取调用最多拉取10000个关注者的OpenID,可以通过多次拉取的方式来满足需求。
接口调用请求说明
http请求方式: GET(请使用https协议)
https://api.weixin.qq.com/cgi-bin/user/get?access_token=ACCESS_TOKEN&next_openid=NEXT_OPENID
参数 | 是否必须 | 说明 |
---|---|---|
access_token | 是 | 调用接口凭证 |
next_openid | 是 | 第一个拉取的OPENID,不填默认从头开始拉取 |
返回说明
正确时返回JSON数据包:
{
"total":2,
"count":2,
"data":{
"openid":["OPENID1","OPENID2"]
},
"next_openid":"NEXT_OPENID"
}
6.1.3 发送消息
调用微信提供的接口去发送模板消息:
http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN
{
"touser":"OPENID",
"template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
"data":{
"first": {
"value":"恭喜你购买成功!",
"color":"#173177"
},
"keyword1":{
"value":"巧克力",
"color":"#173177"
},
"keyword2": {
"value":"39.8元",
"color":"#173177"
},
"keyword3": {
"value":"2014年9月22日",
"color":"#173177"
},
"remark":{
"value":"欢迎再次购买!",
"color":"#173177"
}
}
}
6.2 客服消息
https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html
当用户给公众号发送消息时,可以将其信息记录下来,48小时以内都可以向此人推送客服消息。
6.2.1 互动表
6.2.2 发送消息
接口调用请求说明
http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
各消息类型所需的JSON数据包如下:
发送文本消息
{
"touser":"OPENID",
"msgtype":"text",
"text":
{
"content":"Hello World"
}
}
发送图片消息(图片上传到微信,返回MEDIA_ID)
{
"touser":"OPENID",
"msgtype":"image",
"image":
{
"media_id":"MEDIA_ID"
}
}
6.3 互动信息更新
6.3.1 表结构
在msg的app中创建表结构。
from django.db import models
class Interaction(models.Model):
""" 互动表,用于记录最近48小时内有互动的人 """
authorizer_app_id = models.CharField(verbose_name="公众号授权ID", max_length=64)
user_open_id = models.CharField(max_length=64, verbose_name="互动粉丝ID")
end_date = models.PositiveIntegerField(verbose_name="互动截止时间戳", help_text="互动时间+48小时")
6.3.2 触发更新
import importlib
event_list = [
"apps.msg.event.handler",
# "apps.task.event.handler",
]
for path in event_list:
module_path, func_name = path.rsplit(".", maxsplit=1)
module = importlib.import_module(module_path)
func = getattr(module, func_name)
func(authorizer_app_id, decrypt_xml)
apps.msg.event.py
import time
import xml.etree.cElementTree as ET
from . import models
def handler(authorizer_app_id, decrypt_xml):
""" 接收消息 """
xml_tree = ET.fromstring(decrypt_xml)
msg_type = xml_tree.find("MsgType").text
# 更新互动表
if msg_type in {"text", "image", "voice", "video"}:
from_user_open_id = xml_tree.find("FromUserName").text
models.Interaction.objects.update_or_create(
defaults={"end_date": int(time.time()) + 48 * 60 * 60},
authorizer_app_id=authorizer_app_id,
user_open_id=from_user_open_id,
)
6.4 发送消息
6.4.1 表结构
6.4.2 vue页面(客服消息)
6.4.3 drf接口(客服消息)
- 接收请求
- 表单验证
- 如果是图片,就上传到素材库获取media_id
- 执行celery任务,去执行消息发送(创建任务生成任务ID) - 暂时先不做
- 写入数据库(待执行)
6.4.4 Celery
Celery是由Python开发的一个简单、灵活、可靠的处理大量任务的分发系统。
-
在服务器上安装redis并启动(记得腾讯云设置安全组6379端口)
参考文档:https://pythonav.com/wiki/detail/10/82/
-
安装python操作redis模块
pip install redis
-
安装celery(基于Python开发)
参考文档:https://docs.celeryq.dev/en/stable/index.html
pip install celery
如果是windows请在安装一个eventlet pip install eventlet
6.4.5 Celery(客服消息)
当准备工作都安装成功后,接下来就需要在django项目中配置,配置成功后才能运行。配置步骤如下:
celery -A mtb worker -l info -P eventlet
6.4.6 vue页面(模板消息)
6.4.7 drf接口(模板消息)
- 接口请求
- 数据校验
- 全部粉丝 or 48小时互动粉丝
- 模板消息发送(celery)+ 状态更新
template_list =
[
{
"template_id": "iPk5sOIt5X_flOVKn5GrTFpncEYTojx6ddbt8WYoV5s",
"title": "领取奖金提醒",
"primary_industry": "IT科技",
"deputy_industry": "互联网|电子商务",
"content": "{ {result.DATA} }\n\n领奖金额:{ {withdrawMoney.DATA} }\n领奖 时间: { {withdrawTime.DATA} }\n银行信息:{ {cardInfo.DATA} }\n到账时间: { {arrivedTime.DATA} }\n{ {remark.DATA} }",
"example": "您已提交领奖申请\n\n领奖金额:xxxx元\n领奖时间:2013-10-10 12:22:22\n银行信息:xx银行(尾号xxxx)\n到账时间:预计xxxxxxx\n\n预计将于xxxx到达您的银行卡",
"item_dict":{"result":"", "withdrawMoney":"", ....}
},
{
"template_id": "iPk5sOIt5X_flOVKn5GrTFpncEYTojx6ddbt8WYoV5s",
"title": "领取奖金提醒",
"primary_industry": "IT科技",
"deputy_industry": "互联网|电子商务",
"content": "{ {result.DATA} }\n\n领奖金额:{ {withdrawMoney.DATA} }\n领奖 时间: { {withdrawTime.DATA} }\n银行信息:{ {cardInfo.DATA} }\n到账时间: { {arrivedTime.DATA} }\n{ {remark.DATA} }",
"example": "您已提交领奖申请\n\n领奖金额:xxxx元\n领奖时间:2013-10-10 12:22:22\n银行信息:xx银行(尾号xxxx)\n到账时间:预计xxxxxxx\n\n预计将于xxxx到达您的银行卡",
"item_dict":{"result":"", "withdrawMoney":"", ....}
}
]
{
"iPk5sOIt5X_flOVKn5GrTFpncEYTojx6ddbt8WYoV5s":{
"template_id": "iPk5sOIt5X_flOVKn5GrTFpncEYTojx6ddbt8WYoV5s",
"title": "领取奖金提醒",
"primary_industry": "IT科技",
"deputy_industry": "互联网|电子商务",
"content": "{ {result.DATA} }\n\n领奖金额:{ {withdrawMoney.DATA} }\n领奖 时间: { {withdrawTime.DATA} }\n银行信息:{ {cardInfo.DATA} }\n到账时间: { {arrivedTime.DATA} }\n{ {remark.DATA} }",
"example": "您已提交领奖申请\n\n领奖金额:xxxx元\n领奖时间:2013-10-10 12:22:22\n银行信息:xx银行(尾号xxxx)\n到账时间:预计xxxxxxx\n\n预计将于xxxx到达您的银行卡",
"item_dict":{"result":"", "withdrawMoney":"", ....}
},
"iPk5sOIt5X_flOVKn5GrTFpncEYTojx6ddbt8WYoV5s":{
"template_id": "iPk5sOIt5X_flOVKn5GrTFpncEYTojx6ddbt8WYoV5s",
"title": "领取奖金提醒",
"primary_industry": "IT科技",
"deputy_industry": "互联网|电子商务",
"content": "{ {result.DATA} }\n\n领奖金额:{ {withdrawMoney.DATA} }\n领奖 时间: { {withdrawTime.DATA} }\n银行信息:{ {cardInfo.DATA} }\n到账时间: { {arrivedTime.DATA} }\n{ {remark.DATA} }",
"example": "您已提交领奖申请\n\n领奖金额:xxxx元\n领奖时间:2013-10-10 12:22:22\n银行信息:xx银行(尾号xxxx)\n到账时间:预计xxxxxxx\n\n预计将于xxxx到达您的银行卡",
"item_dict":{"result":"", "withdrawMoney":"", ....}
},
}
6.4.8 消息页面
6.5 SOP
SOP的核心功能是就是实现定时消息的发送(发送模板消息)。
小结
至此,消息宝的功能完成了。
7.媒体宝-任务宝
任务包的核心功能:
- 媒体宝用户,创建活动并与绑定公众号。
- 粉丝向公众号发送指定关键词,公众号自动回复:活动介绍、生成海报连接。
- 粉丝点击连接自动生成海报(含头像、二维码),粉丝发朋友圈或群发等找好友助力。
- 其他人看到朋友圈,扫描海报上的二维码,自动跳转到公众号关注页面,只要关注,助力成功。
- 助力的人数越多,参与活动的人就可以获得更多的奖励。
7.1 表结构
from django.db import models
class Activity(models.Model):
"""活动表"""
mtb_user = models.ForeignKey("base.UserInfo", verbose_name="创建人", on_delete=models.CASCADE)
name = models.CharField(max_length=16, verbose_name="活动名称")
start_time = models.DateTimeField(verbose_name="活动开始时间", )
end_time = models.DateTimeField(verbose_name="活动结束时间", )
# 开启拉新保护后, 只算扫第一个人的
protect_switch = models.SmallIntegerField(verbose_name="拉新保护", choices=((1, "开"), (2, "关"),), default=1)
class PublicJoinActivity(models.Model):
""" 参与获得公众号 """
public = models.ForeignKey("base.PublicNumbers", verbose_name="公众号", on_delete=models.CASCADE)
activity = models.ForeignKey(Activity, verbose_name="活动", on_delete=models.CASCADE, related_name="publics")
class Award(models.Model):
""" 活动奖励表 """
activity = models.ForeignKey(Activity, verbose_name="参与活动", on_delete=models.CASCADE, related_name="awards")
level = models.IntegerField(verbose_name="任务等级", choices=(
(1, "一阶任务"),
(2, "二阶任务"),
(3, "三阶任务"),
), default=1)
number = models.IntegerField(verbose_name="任务数量", default=0)
good = models.CharField(verbose_name="奖品", max_length=255)
class PosterSetting(models.Model):
"""海报配置表"""
activity = models.OneToOneField(Activity, verbose_name="活动", on_delete=models.CASCADE, related_name="poster")
background_img = models.CharField(verbose_name="背景图片", max_length=128)
key_word = models.CharField(verbose_name="海报生成关键字", max_length=10)
rule = models.CharField(verbose_name="活动规则描述", max_length=256)
class TakePartIn(models.Model):
""" 参与活动 """
activity = models.ForeignKey(Activity, verbose_name="活动", on_delete=models.CASCADE)
public_number = models.ForeignKey("base.PublicNumbers", verbose_name="公众号", on_delete=models.CASCADE)
open_id = models.CharField(verbose_name="粉丝OpenID", max_length=64)
nick_name = models.CharField(verbose_name="粉丝昵称", max_length=64, null=True, blank=True)
avatar = models.FileField(verbose_name="粉丝头像", null=True, blank=True)
origin = models.IntegerField(verbose_name="粉丝来源", choices=(
(0, "其他粉丝"),
(1, "推广码"),
(2, "其他")
), default=2)
looking = models.IntegerField(verbose_name="关注状态", choices=(
(0, "关注中"),
(1, "已取关"),
), default=0)
part_in = models.IntegerField(verbose_name="参与活动", choices=(
(0, "参与"),
(1, "不参与"), # 助力别人
), default=0)
poster = models.CharField(verbose_name="专属拉新海报", max_length=128, null=True,blank=True)
black = models.IntegerField(verbose_name="黑名单开关", choices=(
(0, "未加入黑名单"),
(1, "加入黑名单"), # 加入黑名单的人不能参与
), default=0)
level = models.IntegerField(verbose_name="裂变层级", default=1)
origin_open_id = models.CharField(verbose_name="来源ID", max_length=64, null=True) # 是推广码的ID, 或者是用户的ID
number = models.IntegerField(verbose_name="邀请人数", default=0)
task_progress = models.IntegerField(verbose_name="任务完成度", choices=(
(0, "参与"),
(1, "完成任务一"),
(2, "完成任务二"),
(3, "完成任务三"),
), default=0)
ctime = models.DateTimeField(verbose_name="参与时间", auto_now_add=True)
subscribe_time = models.DateTimeField(verbose_name="关注时间")
7.2 创建活动
-
海报背景上传
-
提交活动
# 1.接收数据 # 2.分别校验 活动表/公众号/奖励/海报 # 3.在数据库新增
7.3 活动列表
- 数据的展示
- 数据的筛选
7.4 参与活动
- 创建一个活动
- 粉丝发送数据 & 微信转发给我们
- 判断输入的内容是否是指定的关键字,如果是则
- 回复活动规则。(客服消息)
- 回复链接(图文消息)。
接口调用请求说明
https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Service_Center_messages.html#7
http请求方式: POST https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=ACCESS_TOKEN
各消息类型所需的JSON数据包如下:
{
"touser":"OPENID",
"msgtype":"news",
"news":{
"articles": [
{
"title":"Happy Day",
"description":"Is Really A Happy Day",
"url":"URL",
"picurl":"PIC_URL"
}
]
}
}
发送图文消息(点击跳转到外链) 图文消息条数限制在1条以内,注意,如果图文数超过1,则将会返回错误码45008。
requests.post(
url="https://api.weixin.qq.com/cgi-bin/message/custom/send",
params={"access_token": access_token},
data=json.dumps({
"touser": user_open_id,
"msgtype": "news",
"news": {
"articles": [
{
"title": "点击链接生成我的专属海报",
"description": "邀请好友助力即可领取奖励",
"url": "http://mtb.pythonav.com/task/get_t...",
"picurl": ""
}
]
}
}, ensure_ascii=False).encode('utf-8')
)
7.5 参与活动&生成海报
-
参与活动时,创建参与活动表(无昵称、无头像)
-
构造跳转的连接
# 发送图文消息(用于引导用户授权并获取昵称和头像) url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid={}&redirect_uri={}&response_type=code&scope=snsapi_userinfo&state={}&component_appid={}#wechat_redirect" auth_url = url.format(authorizer_app_id, quote_plus("http://mtb.pythonav.com/api/task/oauth"), 11111, settings.WX_APP_ID) requests.post( url="https://api.weixin.qq.com/cgi-bin/message/custom/send", params={"access_token": access_token}, data=json.dumps({ "touser": user_open_id, "msgtype": "news", "news": { "articles": [ { "title": "点击链接生成我的专属海报", "description": "邀请好友助力即可领取奖励", "url": auth_url, "picurl": "" } ] } }, ensure_ascii=False).encode('utf-8') )
https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html#0
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxf0e81c3bee622d60&redirect_uri=http%3A%2F%2Fnba.bluewebgame.com%2Foauth_response.php&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
"https://open.weixin.qq.com/connect/oauth2/authorize?appid={}&redirect_uri=http%3A%2F%2Fmtb.pythonav.com%2Ftask%2Ftemp%2F&response_type=code&scope=snsapi_userinfo&state={}&component_appid=wx89d0d065c7b25a06#wechat_redirect"
-
授权成功后,微信会直接跳转回我们指定的redirect_uri + 携带参数
{ 'code': '001vjeGa165zUC02xrFa1uxx1p0vjeG6', 'state': '111', 'appid': 'wx71bf291c758aaabf' }
-
根据微信携带的参数获取头像和昵称
-
写生成海报
-
写入数据库
7.6 助力
生成海报之后,就可以发朋友圈,让我的朋友开始帮我助力了。
-
扫码
-
关注公众号
<xml> <ToUserName><![CDATA[gh_0f3fe2860e8f]]></ToUserName> <FromUserName><![CDATA[oXEWb6Q-9-IbljWfq32a9RL8t3Sc]]></FromUserName> <CreateTime>1648828935</CreateTime> <MsgType><![CDATA[event]]></MsgType> <Event><![CDATA[subscribe]]></Event> <EventKey><![CDATA[qrscene_1]]></EventKey> <Ticket><![CDATA[gQGT7zwAAAAAAAAAAS5odHRwOi8vd2VpeGluMXkAAgRkIEdiAwQAjScA]]></Ticket> </xml>
<xml> <ToUserName><![CDATA[gh_0f3fe2860e8f]]></ToUserName> <FromUserName><![CDATA[oXEWb6Q-9-IbljWfq32a9RL8t3Sc]]></FromUserName> <CreateTime>1648829021</CreateTime> <MsgType><![CDATA[event]]></MsgType> <Event><![CDATA[SCAN]]></Event> <EventKey><![CDATA[1]]></EventKey> <Ticket><![CDATA[gQGT7zwAAAAAAAAAeGluLnFxLmNvbKS2h5MXkAAgRkIEdiAwQAjScA]]></Ticket> </xml>
<xml> <ToUserName><![CDATA[gh_0f3fe2860e8f]]></ToUserName> <FromUserName><![CDATA[oXEWb6Q-9-IbljWfq32a9RL8t3Sc]]></FromUserName> <CreateTime>1648830102</CreateTime> <MsgType><![CDATA[event]]></MsgType> <Event><![CDATA[subscribe]]></Event> <EventKey><![CDATA[qrscene_1_1]]></EventKey> <Ticket><![CDATA[gQEh7zwAAAAAAAAAAS5odHR5Y2wAAgRNJkdiAwQAjScA]]></Ticket> </xml>
7.7 取关
如果用户取关。
<xml>
<ToUserName><![CDATA[gh_0f3fe2860e8f]]></ToUserName>
<FromUserName><![CDATA[oXEWb6Q-9-IbljWfq32a9RL8t3Sc]]></FromUserName>
<CreateTime>1648860835</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[unsubscribe]]></Event>
<EventKey><![CDATA[]]></EventKey>
</xml>
7.8 其他问题
- 粉丝参与
- 活动时间
7.9 推广码
如果我们有一些合作渠道,他们可以帮我们去引流,例如:搞一个二维码放到他们网站上,直接扫码关注我们公众号,那么就需要给他生成推广码。
class Promo(models.Model):
"""渠道表"""
name = models.CharField(verbose_name="渠道名称", max_length=16)
qr = models.CharField(verbose_name="二维码", max_length=128,null=True,blank=True)
public = models.ForeignKey("base.PublicNumbers", verbose_name="公众号", on_delete=models.CASCADE)
mtb_user = models.ForeignKey("base.UserInfo", verbose_name="创建人", on_delete=models.CASCADE)
7.9.1 新建推广码
- 打开页面,加载公众号列表。
- 提交表单 -> 后端API -> 生成二维码(media) -> 放在数据库地址
- 后期页面展示就是拿到二维码
7.9.2 推广码列表
功能:
- 搜索 & 筛选
- 分页
- 数据展示
7.9.3 推广码删除
7.9.4 推广码编辑
7.9.5 推广码来源-统计
如果用户通过推广码进行关注公众号,需要进行记录。
<xml>
<ToUserName><![CDATA[gh_0f3fe2860e8f]]></ToUserName>
<FromUserName><![CDATA[oXEWb6Q-9-IbljWfq32a9RL8t3Sc]]></FromUserName>
<CreateTime>1648881233</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
<EventKey><![CDATA[qrscene_2_7]]></EventKey>
<Ticket><![CDATA[gQHS8DwAAAAAAAckYxMDAwMHcwN3UAAgQQxUdiAwQAAAAA]]></Ticket>
</xml>
7.10 参与粉丝
其实就是对参与活动和推广码过的人员进行筛选。
7.11 数据统计
总结
此项目的本质是结合微信开发平台实现新媒体的运营平台(微信裂变)。
媒体宝,新媒体营销平台,让平台帮你管理微信公众号,实现对粉丝、活动、消息等管理。
- 授权管理,打通平台与企业微信公众号的管理,只有授权后,平台才能接入微信,并自动化获取公众号的关注、取关、接收、发送消息等。
- 任务宝,发布微信活动任务,扫码转发并邀请好友助力,做裂变任务并配备相关奖励。
- 消息宝,基于平台实现批量的公众号消息管理,例如:消息发送、图文消息、定时任务等。
共享信息
-
代码
https://gitee.com/wupeiqi/mtb https://gitee.com/wupeiqi/city
-
开发机(服务器)
服务器公网IP:124.222.193.204 服务器账户:Administrator 服务器密码:Buxuyaomima*