增加微信公众号绑定扫码登录功能

dependabot/npm_and_yarn/fir_admin/tmpl-1.0.5
youngS 4 years ago
parent d6ce64c3b6
commit 9b8412304f
  1. 118
      fir_client/src/components/FirLogin.vue
  2. 265
      fir_client/src/components/user/FirUserProfileInfo.vue
  3. 34
      fir_client/src/restful/index.js
  4. 30
      fir_ser/api/migrations/0005_thirdwechatuserinfo.py
  5. 15
      fir_ser/api/models.py
  6. 7
      fir_ser/api/tasks.py
  7. 7
      fir_ser/api/urls.py
  8. 5
      fir_ser/api/utils/app/iossignapi.py
  9. 29
      fir_ser/api/utils/app/supersignutils.py
  10. 12
      fir_ser/api/utils/crontab/ctasks.py
  11. 5
      fir_ser/api/utils/mp/__init__.py
  12. 5
      fir_ser/api/utils/mp/chat/__init__.py
  13. 86
      fir_ser/api/utils/mp/chat/receive.py
  14. 61
      fir_ser/api/utils/mp/chat/reply.py
  15. 20
      fir_ser/api/utils/mp/ierror.py
  16. 220
      fir_ser/api/utils/mp/utils.py
  17. 119
      fir_ser/api/utils/mp/wechat.py
  18. 6
      fir_ser/api/utils/serializer.py
  19. 30
      fir_ser/api/utils/storage/caches.py
  20. 4
      fir_ser/api/views/domain.py
  21. 61
      fir_ser/api/views/login.py
  22. 176
      fir_ser/api/views/thirdlogin.py
  23. 13
      fir_ser/config.py
  24. 10
      fir_ser/fir_ser/settings.py

@ -71,6 +71,25 @@
<el-form-item>
<el-link :underline="false" @click="$router.push({name: 'FirResetPwd'})" plain>忘记密码</el-link>
</el-form-item>
<div class="other-way">
<hr>
<span class="info">或使用以下账户登录</span>
<el-popover
placement="top"
trigger="manual"
title="微信扫码关注公众号登录"
v-model="wx_visible">
<div>
<el-image :src="wx_login_qr_url" style="width: 176px;height: 166px"/>
</div>
<el-button style="color: #1fc939;border: 1px solid rgba(31,201,57,.5); width: 110px"
slot="reference" size="small" @click="wxLogin">微信
</el-button>
</el-popover>
</div>
</el-form>
@ -82,7 +101,7 @@
</template>
<script>
import {loginFun, set_auth_token} from "@/restful";
import {loginFun, set_auth_token, wxLoginFun} from "@/restful";
import {checkEmail, checkphone, geetest} from "@/utils";
export default {
@ -101,9 +120,75 @@
rctitle: '',
register_enable: false,
login_disable: false,
wx_login_qr_url: '',
wx_visible: false,
loop_flag: false,
}
},
methods: {
set_cookie_and_token(data) {
this.$cookies.remove("auth_token");
this.$cookies.set("token", data['token'], 3600 * 24 * 30);
this.$cookies.set("username", data.userinfo.username, 3600 * 24 * 30);
this.$cookies.set("first_name", data.userinfo.first_name, 3600 * 24 * 30);
this.$store.dispatch("doUserinfo", data.userinfo);
set_auth_token();
this.$router.push({name: 'FirApps'})
},
loop_get_wx_info(wx_login_ticket) {
if (wx_login_ticket && wx_login_ticket.length < 3) {
this.$message.error("获取登陆码失败,请稍后再试");
return
}
let c_count = 1;
// eslint-disable-next-line no-unused-vars
const loop_t = window.setInterval(res => {
if (!this.loop_flag) {
window.clearInterval(loop_t);
}
wxLoginFun(data => {
c_count += 1;
if (c_count > 120) {
window.clearInterval(loop_t);
}
if (data.code === 1000) {
window.clearInterval(loop_t);
this.set_cookie_and_token(data);
} else if (data.code === 1005) {
window.clearInterval(loop_t);
this.wx_visible = false;
this.loop_flag = false;
this.$message({
message: data.msg,
type: 'error',
duration: 30000
});
}
}, {
"methods": "POST",
data: {"ticket": wx_login_ticket}
})
}, 3000)
},
wxLogin() {
this.wx_visible = !this.wx_visible;
this.wx_login_qr_url = '';
if (this.wx_visible) {
wxLoginFun(data => {
if (data.code === 1000) {
this.wx_login_qr_url = data.data.qr;
this.loop_flag = true;
this.loop_get_wx_info(data.data.ticket);
}
}, {
"methods": "GET",
})
} else {
this.loop_flag = false;
}
},
is_cptch() {
let cptch_flag = this.form.authcode.length === this.cptch.length;
if (this.cptch.cptch_key === '' || !this.cptch.cptch_key) {
@ -189,13 +274,7 @@
message: '登录成功',
type: 'success'
});
this.$cookies.remove("auth_token");
this.$cookies.set("token", data['token'], 3600 * 24 * 30);
this.$cookies.set("username", data.userinfo.username, 3600 * 24 * 30);
this.$cookies.set("first_name", data.userinfo.first_name, 3600 * 24 * 30);
this.$store.dispatch("doUserinfo", data.userinfo);
set_auth_token();
this.$router.push({name: 'FirApps'})
this.set_cookie_and_token(data);
} else {
this.$message({
message: data.msg,
@ -260,6 +339,29 @@
<style scoped>
.other-way {
position: relative;
text-align: center;
}
.other-way hr {
height: 1px;
margin: 30px 0;
border: 0;
background-color: #e4e7ed;
}
.other-way span.info {
font-size: 12px;
line-height: 1;
position: absolute;
top: -6px;
left: 50%;
padding: 0 10px;
transform: translate(-50%, 0);
color: #9ba3af;
}
.el-container {
margin: 10px auto;
width: 1266px;

@ -1,6 +1,85 @@
<template>
<div>
<el-dialog
:visible.sync="show_wx_visible"
width="780px"
center
:close-on-click-modal="false"
:close-on-press-escape="false">
<span slot="title" style="color: #313639;font-size: 28px; margin-bottom: 14px;">
微信授权登录用户信息
</span>
<el-tag style="margin-bottom: 10px">授权用户将可以使用微信扫码直接登录</el-tag>
<el-table
:data="wx_user_list"
stripe
border
style="width: 100%">
<el-table-column
label="昵称"
align="center"
width="180">
<template slot-scope="scope">
<el-popover trigger="hover" placement="top">
<p>昵称: {{ scope.row.nickname }}</p>
<p>性别: {{ scope.row.sex|sex_filter }}</p>
<p>住址: {{ scope.row.address }}</p>
<p>openid: {{ scope.row.openid }}</p>
<div slot="reference" class="name-wrapper">
<el-tag size="medium">{{ scope.row.nickname }}</el-tag>
</div>
</el-popover>
</template>
</el-table-column>
<el-table-column
label="头像"
align="center"
width="120">
<template slot-scope="scope">
<el-image :src="scope.row.head_img_url" style="width: 80px;height: 80px"/>
</template>
</el-table-column>
<el-table-column
label="关注公众号"
align="center"
width="100">
<template slot-scope="scope">
<div slot="reference" class="name-wrapper">
<el-tag size="medium" v-if="scope.row.subscribe"></el-tag>
<el-tag size="medium" v-else type="danger"></el-tag>
</div>
</template>
</el-table-column>
<el-table-column
label="授权时间"
align="center"
width="200">
<template slot-scope="scope">
<i class="el-icon-time"></i>
<span style="margin-left: 10px">{{ scope.row.created_time|format_time }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
align="center"
>
<template slot-scope="scope">
<el-button
size="mini"
type="danger"
@click="delete_wx_u(scope.row)">移除授权
</el-button>
</template>
</el-table-column>
</el-table>
</el-dialog>
<el-form ref="form" :model="userinfo" label-width="90px">
<el-form-item label="用户名">
<el-row :gutter="36">
@ -138,25 +217,6 @@
</el-row>
</el-form-item>
<!-- <el-form-item label="下载域名">-->
<!-- <el-row :gutter="36">-->
<!-- <el-col :span="16">-->
<!-- <el-input v-model="userinfo.domain_name" :readonly="editdomain_name !== true" ref="domain_name"-->
<!-- prefix-icon="el-icon-download"-->
<!-- placeholder="下载页域名" clearable/>-->
<!-- </el-col>-->
<!-- <el-col :span="1">-->
<!-- <el-button icon="el-icon-edit" @click="changeDomainValue">-->
<!-- </el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="5" v-if="editdomain_name === true">-->
<!-- <el-button type="success" @click="saveDomain" plain-->
<!-- class="save-button">-->
<!-- 保存-->
<!-- </el-button>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- </el-form-item>-->
<el-form-item label="下载域名">
<el-row :gutter="36">
<el-col :span="16">
@ -187,6 +247,28 @@
</el-row>
</el-form-item>
<div class="other-way">
<hr>
<span class="info">绑定第三方账户</span>
<el-row :gutter="20">
<el-col :span="6" :offset="6">
<el-popover
placement="top"
trigger="manual"
title="打开微信扫一扫进行绑定"
v-model="wx_visible">
<div>
<el-image :src="wx_login_qr_url" style="width: 176px;height: 166px"/>
</div>
<el-button slot="reference" size="small" @click="wxLogin">绑定微信</el-button>
</el-popover>
</el-col>
<el-col :span="6">
<el-button size="small" @click="get_wx_user_list">授权信息</el-button>
</el-col>
</el-row>
</div>
</el-form>
@ -195,7 +277,7 @@
</template>
<script>
import {changeInfoFun, getAuthcTokenFun, userinfos} from '@/restful'
import {changeInfoFun, getAuthcTokenFun, userinfos, wxLoginFun, wxutils} from '@/restful'
import {deepCopy, geetest} from "@/utils";
export default {
@ -214,8 +296,110 @@
editposition: false,
cptch: {"cptch_image": '', "cptch_key": '', "length": 8, change_type: {email: false, sms: false}},
form: {},
wx_login_qr_url: '',
wx_visible: false,
loop_flag: false,
show_wx_visible: false,
wx_user_list: [],
pagination: {"currentPage": 1, "total": 0, "pagesize": 999},
}
}, methods: {
delete_wx_u(wx_user_info) {
this.$confirm(`此操作将导致微信用户 “${wx_user_info.nickname}” 无法通过扫码登录, 是否继续删除?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.wxUtilsFun({
methods: 'DELETE',
data: {
"size": this.pagination.pagesize,
"page": this.pagination.currentPage,
"user_id": wx_user_info.user_id,
"openid": wx_user_info.openid,
}
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
wxUtilsFun(params) {
wxutils(data => {
if (data.code === 1000) {
this.wx_user_list = data.data;
this.show_wx_visible = true
} else {
this.$message.error("获取授权列表失败")
}
}, params)
},
get_wx_user_list() {
this.wxUtilsFun({
methods: 'GET',
data: {
"size": this.pagination.pagesize,
"page": this.pagination.currentPage
}
})
},
loop_get_wx_info(wx_login_ticket) {
if (wx_login_ticket && wx_login_ticket.length < 3) {
this.$message.error("获取登陆码失败,请稍后再试");
return
}
let c_count = 1;
// eslint-disable-next-line no-unused-vars
const loop_t = window.setInterval(res => {
if (!this.loop_flag) {
window.clearInterval(loop_t);
}
wxLoginFun(data => {
c_count += 1;
if (c_count > 120) {
window.clearInterval(loop_t);
}
if (data.code === 1000) {
window.clearInterval(loop_t);
if (this.userinfo.uid === data.userinfo.uid) {
this.$message.success("绑定成功");
this.wx_visible = false;
this.loop_flag = false;
}
} else if (data.code === 1005) {
window.clearInterval(loop_t);
this.$message({
message: data.msg,
type: 'error',
duration: 30000
});
}
}, {
"methods": "POST",
data: {"ticket": wx_login_ticket}
})
}, 3000)
},
wxLogin() {
this.wx_visible = !this.wx_visible;
this.wx_login_qr_url = '';
if (this.wx_visible) {
userinfos(data => {
if (data.code === 1000) {
this.wx_login_qr_url = data.data.qr;
this.loop_flag = true;
this.loop_get_wx_info(data.data.ticket);
}
}, {
"methods": "POST",
})
} else {
this.loop_flag = false;
}
},
get_auth_code() {
changeInfoFun(data => {
if (data.code === 1000) {
@ -391,11 +575,52 @@
this.userinfo = this.$store.state.userinfo;
this.orguserinfo = deepCopy(this.$store.state.userinfo);
}
}, filters: {
sex_filter: function (x) {
let ret = '未知';
if (x === 1) {
ret = '男'
} else if (x === 2) {
ret = '女'
}
return ret;
},
format_time: function (x) {
if (x) {
x = x.split(".")[0].split("T");
return x[0] + " " + x[1]
} else
return '';
}
}
}
</script>
<style scoped>
.other-way {
position: relative;
text-align: center;
}
.other-way hr {
height: 1px;
margin: 30px 0;
border: 0;
background-color: #e4e7ed;
}
.other-way span.info {
font-size: 12px;
line-height: 1;
position: absolute;
top: -6px;
left: 50%;
padding: 0 10px;
transform: translate(-50%, 0);
color: #9ba3af;
}
.el-form {
max-width: 500px;
margin: 0 auto;

@ -216,6 +216,25 @@ export function loginFun(callBack, params, load = true) {
);
}
/**微信公众号关注登录 */
export function wxLoginFun(callBack, params, load = true) {
let g_url = 'third.wx.login';
if (params.methods === 'POST') {
g_url = 'third.wx.sync'
}
getData(
params.methods,
USERSEVER + '/' + g_url,
params.data,
data => {
callBack(data);
},
load,
true,
true
);
}
/**获取验证token */
export function getAuthTokenFun(callBack, params, load = true) {
getData(
@ -628,3 +647,18 @@ export function developercert(callBack, params, load = true) {
true
);
}
/**微信用户绑定 */
export function wxutils(callBack, params, load = true) {
getData(
params.methods,
USERSEVER + '/twx/info',
params.data,
data => {
callBack(data);
},
load,
true,
true
);
}

@ -0,0 +1,30 @@
# Generated by Django 3.2.3 on 2021-09-07 19:18
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('api', '0004_auto_20210705_1819'),
]
operations = [
migrations.CreateModel(
name='ThirdWeChatUserInfo',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('openid', models.CharField(max_length=64, unique=True, verbose_name='普通用户的标识,对当前公众号唯一')),
('nickname', models.CharField(blank=True, max_length=64, verbose_name='昵称')),
('sex', models.SmallIntegerField(default=0, help_text='值为1时是男性,值为2时是女性,值为0时是未知', verbose_name='性别')),
('subscribe_time', models.BigIntegerField(verbose_name='订阅时间')),
('head_img_url', models.CharField(blank=True, max_length=256, null=True, verbose_name='用户头像')),
('address', models.CharField(blank=True, max_length=128, null=True, verbose_name='地址')),
('subscribe', models.BooleanField(default=0, verbose_name='是否订阅公众号')),
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')),
('user_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL,
verbose_name='用户ID')),
],
),
]

@ -62,6 +62,21 @@ class UserInfo(AbstractUser):
super(UserInfo, self).save(*args, **kwargs)
class ThirdWeChatUserInfo(models.Model):
user_id = models.ForeignKey(to="UserInfo", verbose_name="用户ID", on_delete=models.CASCADE)
openid = models.CharField(max_length=64, unique=True, verbose_name="普通用户的标识,对当前公众号唯一")
nickname = models.CharField(max_length=64, verbose_name="昵称", blank=True)
sex = models.SmallIntegerField(default=0, verbose_name="性别", help_text="值为1时是男性,值为2时是女性,值为0时是未知")
subscribe_time = models.BigIntegerField(verbose_name="订阅时间")
head_img_url = models.CharField(max_length=256, verbose_name="用户头像", blank=True, null=True)
address = models.CharField(max_length=128, verbose_name="地址", blank=True, null=True)
subscribe = models.BooleanField(verbose_name="是否订阅公众号", default=0)
created_time = models.DateTimeField(auto_now_add=True, verbose_name="授权时间")
def __str__(self):
return f"{self.user_id}-{self.nickname}-{self.openid}"
class Token(models.Model):
"""
The default authorization token model.

@ -12,7 +12,7 @@ from api.utils.storage.storage import get_local_storage
from api.models import Apps
from api.utils.app.supersignutils import IosUtils, resign_by_app_id
from api.utils.crontab.ctasks import sync_download_times, auto_clean_upload_tmp_file, auto_delete_ios_mobile_tmp_file, \
auto_check_ios_developer_active
auto_check_ios_developer_active, sync_wx_access_token
from api.utils.geetest.geetest_utils import check_bypass_status
from fir_ser.celery import app
@ -80,3 +80,8 @@ def auto_delete_tmp_file_job():
@app.task
def auto_check_ios_developer_active_job():
auto_check_ios_developer_active()
@app.task
def sync_wx_access_token_job():
sync_wx_access_token()

@ -16,7 +16,7 @@ Including another URLconf
from django.urls import re_path
from api.views.login import LoginView, UserInfoView, RegistView, AuthorizationView, ChangeAuthorizationView, \
UserApiTokenView, CertificationView, ChangeInfoView
UserApiTokenView, CertificationView, ChangeInfoView, WeChatLoginView, WeChatLoginCheckView
from api.views.logout import LogoutView
from api.views.apps import AppsView, AppInfoView, AppReleaseInfoView
from api.views.download import ShortDownloadView
@ -26,6 +26,7 @@ from api.views.receiveudids import IosUDIDView, TaskView
from api.views.order import PriceView, OrderView, PaySuccess
from api.views.supersign import DeveloperView, SuperSignUsedView, AppUDIDUsedView, SuperSignCertView
from api.views.domain import DomainCnameView
from api.views.thirdlogin import ValidWxChatToken, ThirdWxAccount
# router=DefaultRouter()
# router.register("apps", AppsView)
@ -58,5 +59,9 @@ urlpatterns = [
re_path("^certification$", CertificationView.as_view()),
re_path(r"^pay_success/(?P<name>\w+)$", PaySuccess.as_view()),
re_path("^cname_domain$", DomainCnameView.as_view()),
re_path("^mp.weixin$", ValidWxChatToken.as_view()),
re_path("^third.wx.login$", WeChatLoginView.as_view()),
re_path("^third.wx.sync$", WeChatLoginCheckView.as_view()),
re_path("^twx/info$", ThirdWxAccount.as_view()),
]

@ -264,7 +264,7 @@ class AppDeveloperApiV2(object):
return False, result
def get_profile(self, app_obj, udid_info, provisionName, auth, developer_app_id,
device_id_list):
device_id_list, err_callback):
result = {}
bundle_id = app_obj.bundle_id
app_id = app_obj.app_id
@ -301,6 +301,9 @@ class AppDeveloperApiV2(object):
except Exception as e:
logger.error(f"app_id {app_obj.app_id} ios developer make profile Failed Exception:{e}")
result['return_info'] = "%s" % e
if "There are no current ios devices" in e:
err_callback()
return False, result
def del_profile(self, app_id):

@ -192,6 +192,13 @@ def get_apple_udid_key(auth):
return m_key
def err_callback(func, a, b):
def wrapper():
return func(a, b)
return wrapper
def get_server_domain_from_request(request, server_domain):
if not server_domain or not server_domain.startswith("http"):
http_host = request.META.get('HTTP_HOST')
@ -273,10 +280,10 @@ class IosUtils(object):
return None
return developer_obj
def download_profile(self, developer_app_id, device_id_list):
return get_api_obj(self.auth).get_profile(self.app_obj, self.udid_info,
self.get_profile_full_path(),
self.auth, developer_app_id, device_id_list)
# def download_profile(self, developer_app_id, device_id_list):
# return get_api_obj(self.auth).get_profile(self.app_obj, self.udid_info,
# self.get_profile_full_path(),
# self.auth, developer_app_id, device_id_list, )
# 开启超级签直接在开发者账户创建
# def create_app(self, app_obj):
@ -327,7 +334,9 @@ class IosUtils(object):
status, result = get_api_obj(auth).get_profile(app_obj, udid_info,
get_profile_full_path(developer_obj, app_obj),
auth, developer_app_id, device_id_lists)
auth, developer_app_id, device_id_lists,
err_callback(IosUtils.get_device_from_developer,
developer_obj, developer_obj.user_id))
if add_did_flag and sync_device_obj:
result['did'] = sync_device_obj.serial
result['did_exists'] = True
@ -340,7 +349,7 @@ class IosUtils(object):
if sign_try_attempts != -1:
logger.error(f"app {app_obj} developer {developer_obj} sign failed {result}")
developer_obj.is_actived = False
developer_obj.save()
developer_obj.save(update_fields=['is_actived'])
send_ios_developer_active_status(developer_obj.user_id,
MSGTEMPLATE.get('ERROR_DEVELOPER') % (
developer_obj.user_id.first_name, app_obj.name,
@ -404,7 +413,7 @@ class IosUtils(object):
storage_obj.delete_file(apptodev_obj.binary_file + ".ipa")
apptodev_obj.binary_file = random_file_name
apptodev_obj.release_file = release_obj.release_id
apptodev_obj.save()
apptodev_obj.save(update_fields=['binary_file', 'release_file'])
else:
APPToDeveloper.objects.create(developerid=developer_obj, app_id=app_obj,
binary_file=random_file_name, release_file=release_obj.release_id)
@ -419,7 +428,7 @@ class IosUtils(object):
developer_obj.use_number = developer_obj.use_number + 1
logger.info(
f"developer {developer_obj} use_number+1 now {developer_obj.use_number} udid {self.udid_info.get('udid')} app_id {self.app_obj}")
developer_obj.save()
developer_obj.save(update_fields=['use_number'])
if not appsupersign_obj.filter(app_id=self.app_obj, user_id=self.user_obj).first():
APPSuperSignUsedInfo.objects.create(app_id=self.app_obj, user_id=self.user_obj,
@ -602,7 +611,7 @@ class IosUtils(object):
if developer_obj.use_number > 0:
developer_obj.use_number = developer_obj.use_number - 1
developer_obj.save()
developer_obj.save(update_fields=['use_number'])
udid_obj.delete()
@ -706,7 +715,7 @@ class IosUtils(object):
developer_obj.is_actived = True
else:
developer_obj.is_actived = False
developer_obj.save()
developer_obj.save(update_fields=['certid', 'cert_expire_time', 'is_actived'])
return status, result
@staticmethod

@ -5,6 +5,7 @@
# date: 2020/4/7
from api.models import Apps, UserInfo, AppIOSDeveloperInfo
from api.utils.mp.wechat import make_wx_auth_obj
from api.utils.storage.storage import Storage
from api.utils.app.supersignutils import IosUtils
from api.utils.utils import send_ios_developer_active_status
@ -82,3 +83,14 @@ def auto_check_ios_developer_active():
logger.error(msg)
send_ios_developer_active_status(userinfo, MSGTEMPLATE.get('AUTO_CHECK_DEVELOPER') % (
userinfo.first_name, ios_developer.name))
def sync_wx_access_token():
wx_access_token_key = CACHE_KEY_TEMPLATE.get("wx_access_token_key")
access_token_info = cache.get(wx_access_token_key)
if not access_token_info:
access_token_info = make_wx_auth_obj().get_access_token()
expires_in = access_token_info.get('expires_in')
if expires_in:
cache.set(wx_access_token_key, access_token_info, expires_in - 60)
return access_token_info

@ -0,0 +1,5 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6

@ -0,0 +1,5 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6

@ -0,0 +1,86 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6
import xml.etree.ElementTree as ET
def parse_xml(web_data):
if len(web_data) == 0:
return None
xml_data = ET.fromstring(web_data)
msg_type = xml_data.find('MsgType').text
if msg_type == 'event':
event_type = xml_data.find('Event').text
if event_type == 'CLICK':
return Click(xml_data)
elif event_type in ('subscribe', 'unsubscribe'):
return Subscribe(xml_data)
# elif event_type == 'VIEW':
# return View(xml_data)
# elif event_type == 'LOCATION':
# return LocationEvent(xml_data)
elif event_type == 'SCAN':
return Scan(xml_data)
elif msg_type == 'text':
return TextMsg(xml_data)
elif msg_type == 'image':
return ImageMsg(xml_data)
class Msg(object):
def __init__(self, xml_data):
self.ToUserName = xml_data.find('ToUserName').text
self.FromUserName = xml_data.find('FromUserName').text
self.CreateTime = xml_data.find('CreateTime').text
self.MsgType = xml_data.find('MsgType').text
self.MsgId = xml_data.find('MsgId').text
class TextMsg(Msg):
def __init__(self, xml_data):
Msg.__init__(self, xml_data)
self.Content = xml_data.find('Content').text.encode("utf-8")
class ImageMsg(Msg):
def __init__(self, xml_data):
Msg.__init__(self, xml_data)
self.PicUrl = xml_data.find('PicUrl').text
self.MediaId = xml_data.find('MediaId').text
class EventMsg(object):
def __init__(self, xml_data):
self.ToUserName = xml_data.find('ToUserName').text
self.FromUserName = xml_data.find('FromUserName').text
self.CreateTime = xml_data.find('CreateTime').text
self.MsgType = xml_data.find('MsgType').text
self.Event = xml_data.find('Event').text
self.Eventkey = xml_data.find('EventKey').text
self.Ticket = ''
class Click(EventMsg):
def __init__(self, xml_data):
EventMsg.__init__(self, xml_data)
class Scan(EventMsg):
def __init__(self, xml_data):
EventMsg.__init__(self, xml_data)
self.Ticket = xml_data.find('Ticket').text
class Subscribe(EventMsg):
def __init__(self, xml_data):
EventMsg.__init__(self, xml_data)
tick = xml_data.find('Ticket')
if tick is None:
self.Ticket = ''
else:
self.Ticket = tick.text

@ -0,0 +1,61 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6
import time
class Msg(object):
def __init__(self):
pass
def send(self):
return "success"
class TextMsg(Msg):
def __init__(self, to_user_name, from_user_name, content):
super().__init__()
self.__dict = dict()
self.__dict['ToUserName'] = to_user_name
self.__dict['FromUserName'] = from_user_name
self.__dict['CreateTime'] = int(time.time())
self.__dict['Content'] = content
def send(self):
xml_form = """
<xml>
<ToUserName><![CDATA[{ToUserName}]]></ToUserName>
<FromUserName><![CDATA[{FromUserName}]]></FromUserName>
<CreateTime>{CreateTime}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[{Content}]]></Content>
</xml>
"""
return xml_form.format(**self.__dict)
class ImageMsg(Msg):
def __init__(self, to_user_name, from_user_name, media_id):
super().__init__()
self.__dict = dict()
self.__dict['ToUserName'] = to_user_name
self.__dict['FromUserName'] = from_user_name
self.__dict['CreateTime'] = int(time.time())
self.__dict['MediaId'] = media_id
def send(self):
xml_form = """
<xml>
<ToUserName><![CDATA[{ToUserName}]]></ToUserName>
<FromUserName><![CDATA[{FromUserName}]]></FromUserName>
<CreateTime>{CreateTime}</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA[{MediaId}]]></MediaId>
</Image>
</xml>
"""
return xml_form.format(**self.__dict)

@ -0,0 +1,20 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#########################################################################
# Author: jonyqin
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
# File Name: ierror.py
# Description:定义错误码含义
#########################################################################
WXBizMsgCrypt_OK = 0
WXBizMsgCrypt_ValidateSignature_Error = -40001
WXBizMsgCrypt_ParseXml_Error = -40002
WXBizMsgCrypt_ComputeSignature_Error = -40003
WXBizMsgCrypt_IllegalAesKey = -40004
WXBizMsgCrypt_ValidateAppid_Error = -40005
WXBizMsgCrypt_EncryptAES_Error = -40006
WXBizMsgCrypt_DecryptAES_Error = -40007
WXBizMsgCrypt_IllegalBuffer = -40008
WXBizMsgCrypt_EncodeBase64_Error = -40009
WXBizMsgCrypt_DecodeBase64_Error = -40010
WXBizMsgCrypt_GenReturnXml_Error = -40011

@ -0,0 +1,220 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6
import base64
import logging
import string
import random
import struct
import hashlib
from Crypto.Cipher import AES
import xml.etree.cElementTree as ET
import time
import socket
from . import ierror
logger = logging.getLogger(__name__)
class XMLParse:
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
# xml消息模板
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
<MsgSignature><![CDATA[%(msg_signature)s]]></MsgSignature>
<TimeStamp>%(timestamp)s</TimeStamp>
<Nonce><![CDATA[%(nonce)s]]></Nonce>
</xml>"""
def extract(self, xml_text):
"""提取出xml数据包中的加密消息
@param xml_text: 待提取的xml字符串
@return: 提取出的加密消息字符串
"""
try:
xml_tree = ET.fromstring(xml_text)
encrypt = xml_tree.find("Encrypt")
to_user_name = xml_tree.find("ToUserName")
return ierror.WXBizMsgCrypt_OK, encrypt.text, to_user_name.text
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_ParseXml_Error, None, None
def generate(self, encrypt, signature, timestamp, nonce):
"""生成xml消息
@param encrypt: 加密后的消息密文
@param signature: 安全签名
@param timestamp: 时间戳
@param nonce: 随机字符串
@return: 生成的xml字符串
"""
resp_dict = {
'msg_encrypt': encrypt,
'msg_signature': signature,
'timestamp': timestamp,
'nonce': nonce,
}
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
return resp_xml
class PKCS7Encoder(object):
"""提供基于PKCS7算法的加解密接口"""
block_size = 32
def encode(self, text):
""" 对需要加密的明文进行填充补位
@param text: 需要进行填充补位操作的明文
@return: 补齐明文字符串
"""
text_length = len(text)
# 计算需要填充的位数
amount_to_pad = self.block_size - (text_length % self.block_size)
if amount_to_pad == 0:
amount_to_pad = self.block_size
# 获得补位所用的字符
pad = chr(amount_to_pad)
return text + pad * amount_to_pad
def decode(self, decrypted):
"""删除解密后明文的补位字符
@param decrypted: 解密后的明文
@return: 删除补位字符后的明文
"""
pad = ord(decrypted[-1])
if pad < 1 or pad > 32:
pad = 0
return decrypted[:-pad]
def get_random_str():
""" 随机生成16位字符串
@return: 16位字符串
"""
rule = string.ascii_letters + string.digits
return "".join(random.sample(rule, 16))
class Prpcrypt(object):
"""提供接收和推送给公众平台消息的加解密接口"""
def __init__(self, key):
# self.key = base64.b64decode(key+"=")
self.key = key
# 设置加解密模式为AES的CBC模式
self.mode = AES.MODE_CBC
def encrypt(self, text, app_id):
"""对明文进行加密
:param text: 需要加密的明文
:param app_id: 应用id
@return: 加密得到的字符串
"""
# 16位随机字符串添加到明文开头
text = get_random_str() + struct.pack("I", socket.htonl(len(text))).decode('utf-8') + text + app_id
# 使用自定义的填充方式对明文进行补位填充
pkcs7 = PKCS7Encoder()
text = pkcs7.encode(text)
# 加密
crypto = AES.new(self.key, self.mode, self.key[:16])
try:
ciphertext = crypto.encrypt(text)
# 使用BASE64对加密后的字符串进行编码
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_EncryptAES_Error, None
def decrypt(self, text, app_id):
""" 对解密后的明文进行补位删除
:param text: 密文
:param app_id: 应用id
:return: 删除填充补位后的明文
"""
try:
cryptor = AES.new(self.key, self.mode, self.key[:16])
# 使用BASE64对密文进行解码,然后AES-CBC解密
plain_text = cryptor.decrypt(base64.b64decode(text))
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_DecryptAES_Error, None
try:
pad = plain_text[-1]
# 去掉补位字符串
# pkcs7 = PKCS7Encoder()
# plain_text = pkcs7.encode(plain_text)
# 去除16位随机字符串
content = plain_text[16:-pad]
xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0])
xml_content = content[4: xml_len + 4]
from_app_id = content[xml_len + 4:].decode('utf-8')
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_IllegalBuffer, None
if from_app_id != app_id:
return ierror.WXBizMsgCrypt_ValidateAppid_Error, None
return 0, xml_content
class WxMsgCryptBase(object):
def __init__(self, app_id, app_secret, token, encoding_aes_key):
try:
self.key = base64.b64decode(encoding_aes_key + "=")
assert len(self.key) == 32
except Exception as e:
logger.error(f"{encoding_aes_key} 解密失败 Exception:{e}")
raise Exception("[error]: EncodingAESKey invalid !")
self.token = token
self.app_id = app_id
def encrypt_msg(self, msg, nonce, timestamp=None):
"""
:param msg: 企业号待回复用户的消息xml格式的字符串
:param nonce: 随机串可以自己生成也可以用URL参数的nonce
:param timestamp: 时间戳可以自己生成也可以用URL参数的timestamp,如为None则自动用当前时间
:return: 成功0sEncryptMsg,失败返回对应的错误码None
"""
pc = Prpcrypt(self.key)
ret, encrypt = pc.encrypt(msg, self.app_id)
if ret != 0:
return ret, None
if timestamp is None:
timestamp = str(int(time.time()))
try:
sha = hashlib.sha1(("".join(sorted([self.token, timestamp, nonce, encrypt]))).encode('utf-8'))
return ret, XMLParse().generate(encrypt, sha.hexdigest(), timestamp, nonce)
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
def decrypt_msg(self, msg, msg_signature, timestamp, nonce):
"""
:param msg: 密文对应POST请求的数据
:param msg_signature: 签名串对应URL参数的msg_signature
:param timestamp: 时间戳对应URL参数的timestamp
:param nonce: 随机串对应URL参数的nonce
:return: 成功0失败返回对应的错误码
"""
if isinstance(msg, str):
ret, encrypt, _ = XMLParse().extract(msg)
if ret != 0:
return ret, None
else:
encrypt = msg.get('Encrypt')
try:
sha = hashlib.sha1(("".join(sorted([self.token, timestamp, nonce, encrypt]))).encode('utf-8'))
if not sha.hexdigest() == msg_signature:
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
pc = Prpcrypt(self.key)
ret, xml_content = pc.decrypt(encrypt, self.app_id)
return ret, xml_content
except Exception as e:
logger.error(e)
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None

@ -0,0 +1,119 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 9月
# author: NinEveN
# date: 2021/9/6
from hashlib import sha1
import requests
import logging
import json
from fir_ser.settings import THIRDLOGINCONF
from api.utils.mp.utils import WxMsgCryptBase
from api.utils.storage.caches import get_wx_access_token_cache
logger = logging.getLogger(__name__)
wx_login_info = THIRDLOGINCONF.wx_official
def create_menu():
menu_json = {
"button": [
{
"type": "click",
"name": "",
"key": "good"
},
{
"name": "分发平台",
"sub_button": [
{
"type": "view",
"name": "官方地址",
"url": "https://flyapps.cn"
},
{
"type": "view",
"name": "留言反馈",
"url": "https://flyapps.cn/gbook/"
},
]
},
{
"type": "media_id",
"name": "联系我们",
"media_id": "qvQxPuAb4GnUgjkxl2xVnbsnldxawf4DXM09biqgP30"
}
]
}
p_url = f"https://api.weixin.qq.com/cgi-bin/menu/create?access_token={get_wx_access_token_cache()}"
req = requests.post(url=p_url, data=json.dumps(menu_json, ensure_ascii=False).encode('utf-8'))
print(req.json())
def show_qrcode_url(ticket):
return f'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={ticket}'
def make_wx_login_qrcode(scene_str='web.login', expire_seconds=600):
"""
:param scene_str: 场景值ID字符串形式的ID字符串类型长度限制为1到64
:param expire_seconds: 该二维码有效时间以秒为单位 最大不超过2592000即30天此字段如果不填则默认有效期为30秒
:return: {
"ticket":"gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm3sUw==",
"expire_seconds":60,
"url":"http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI"
}
https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html
"""
t_url = f'https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={get_wx_access_token_cache()}'
data = {"expire_seconds": expire_seconds, "action_name": "QR_STR_SCENE",
"action_info": {"scene": {"scene_str": scene_str}}}
req = requests.post(t_url, json=data)
if req.status_code == 200:
return True, req.json()
logger.error(f"make wx login qrcode failed {req.status_code} {req.text}")
return False, req.text
def get_userinfo_from_openid(open_id):
t_url = f'https://api.weixin.qq.com/cgi-bin/user/info?access_token={get_wx_access_token_cache()}&openid={open_id}&lang=zh_CN'
req = requests.get(t_url)
if req.status_code == 200:
return True, req.json()
logger.error(f"get userinfo from openid failed {req.status_code} {req.text}")
return False, req.text
class WxOfficialBase(object):
def __init__(self, app_id, app_secret, token, encoding_aes_key):
self.app_id = app_id
self.app_secret = app_secret
self.token = token
self.encoding_aes_key = encoding_aes_key
def get_access_token(self):
t_url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={self.app_id}&secret={self.app_secret}'
req = requests.get(t_url)
if req.status_code == 200:
return req.json()
logger.error(f"get access token failed {req.status_code} {req.text}")
return req.text
def make_wx_auth_obj():
return WxOfficialBase(**wx_login_info.get('auth'))
def check_signature(params):
tmp_list = sorted([wx_login_info.get('auth', {}).get('token'), params.get("timestamp"), params.get("nonce")])
tmp_str = "".join(tmp_list)
tmp_str = sha1(tmp_str.encode("utf-8")).hexdigest()
if tmp_str == params.get("signature"):
return int(params.get("echostr"))
return ''
class WxMsgCrypt(WxMsgCryptBase):
def __init__(self):
super().__init__(**wx_login_info.get('auth'))

@ -570,3 +570,9 @@ class AdminUserCertificationSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
return super(AdminUserCertificationSerializer, self).update(instance, validated_data)
class ThirdWxSerializer(serializers.ModelSerializer):
class Meta:
model = models.ThirdWeChatUserInfo
exclude = ["id"]

@ -12,7 +12,7 @@ from django.utils import timezone
from fir_ser.settings import CACHE_KEY_TEMPLATE, SERVER_DOMAIN, SYNC_CACHE_TO_DATABASE, DEFAULT_MOBILEPROVISION, \
USER_FREE_DOWNLOAD_TIMES, AUTH_USER_FREE_DOWNLOAD_TIMES
from api.utils.storage.storage import Storage, LocalStorage
from api.utils.baseutils import get_app_d_count_by_app_id, get_app_domain_name, check_app_password # file_format_path,
from api.utils.baseutils import get_app_d_count_by_app_id, get_app_domain_name, check_app_password
import logging
from django.db.models import F
@ -281,7 +281,7 @@ def set_default_app_wx_easy(user_obj, only_clean_cache=False):
else:
if not get_app_domain_name(app_obj):
app_obj.wxeasytype = True
app_obj.save()
app_obj.save(update_fields=['wxeasytype'])
del_cache_response_by_short(app_obj.app_id)
@ -405,7 +405,7 @@ def update_order_status(out_trade_no, status):
order_obj = Order.objects.filter(order_number=out_trade_no).first()
if order_obj:
order_obj.status = status
order_obj.save()
order_obj.save(update_fields=['status'])
def update_order_info(user_id, out_trade_no, payment_number, payment_type):
@ -460,3 +460,27 @@ def check_app_permission(app_obj, res):
res.msg = "您没有权限访问该应用"
return res
def get_wx_access_token_cache():
wx_access_token_key = CACHE_KEY_TEMPLATE.get("wx_access_token_key")
access_token = cache.get(wx_access_token_key)
if access_token:
return access_token.get('access_token')
return ''
def set_wx_ticket_login_info_cache(ticket, data=None, expire_seconds=600):
if data is None:
data = {}
wx_ticket_info_key = CACHE_KEY_TEMPLATE.get("wx_ticket_info_key")
cache.set("_".join([wx_ticket_info_key, ticket]), data, expire_seconds)
def get_wx_ticket_login_info_cache(ticket):
wx_ticket_info_key = CACHE_KEY_TEMPLATE.get("wx_ticket_info_key")
wx_t_key = "_".join([wx_ticket_info_key, ticket])
wx_ticket_info = cache.get(wx_t_key)
if wx_ticket_info:
cache.delete(wx_t_key)
return wx_ticket_info

@ -91,7 +91,7 @@ class DomainCnameView(APIView):
app_obj = Apps.objects.filter(app_id=app_id).first()
if app_obj:
app_obj.wxeasytype = False
app_obj.save()
app_obj.save(update_fields=['wxeasytype'])
del_cache_response_by_short(app_obj.app_id)
else:
set_default_app_wx_easy(request.user, True)
@ -111,7 +111,7 @@ class DomainCnameView(APIView):
app_obj = Apps.objects.filter(app_id=app_id).first()
if app_obj and not get_user_domain_name(request.user):
app_obj.wxeasytype = True
app_obj.save()
app_obj.save(update_fields=['wxeasytype'])
del_cache_response_by_short(app_obj.app_id)
else:
set_default_app_wx_easy(request.user)

@ -2,6 +2,8 @@ from django.contrib import auth
from api.models import Token, UserInfo, UserCertificationInfo, CertificationInfo
from rest_framework.response import Response
from api.utils.mp.wechat import make_wx_login_qrcode, show_qrcode_url
from api.utils.serializer import UserInfoSerializer, CertificationSerializer, UserCertificationSerializer
from django.core.cache import cache
from rest_framework.views import APIView
@ -12,7 +14,7 @@ from api.utils.baseutils import is_valid_phone, is_valid_email, get_min_default_
from api.utils.auth import ExpiringTokenAuthentication
from api.utils.response import BaseResponse
from fir_ser.settings import REGISTER, LOGIN, CHANGER
from api.utils.storage.caches import login_auth_failed
from api.utils.storage.caches import login_auth_failed, set_wx_ticket_login_info_cache, get_wx_ticket_login_info_cache
import logging
from api.utils.geetest.geetest_utils import first_register, second_validate
from api.utils.throttle import VisitRegister1Throttle, VisitRegister2Throttle, GetAuthC1Throttle, GetAuthC2Throttle
@ -398,6 +400,19 @@ class RegistView(APIView):
return Response(response.dict)
def wx_qr_code_response(ret, code, qr_info):
if code:
logger.info(f"微信登录码获取成功, {qr_info}")
ticket = qr_info.get('ticket')
if ticket:
set_wx_ticket_login_info_cache(ticket)
ret.data = {'qr': show_qrcode_url(ticket), 'ticket': ticket}
else:
ret.code = code
ret.msg = "微信登录码获取失败,请稍后"
return Response(ret.dict)
class UserInfoView(APIView):
authentication_classes = [ExpiringTokenAuthentication, ]
@ -504,6 +519,12 @@ class UserInfoView(APIView):
return Response(res.dict)
def post(self, request):
ret = BaseResponse()
uid = request.user.uid
code, qr_info = make_wx_login_qrcode(f"web.bind.{uid}")
return wx_qr_code_response(ret, code, qr_info)
class AuthorizationView(APIView):
throttle_classes = [GetAuthC1Throttle, GetAuthC2Throttle]
@ -706,3 +727,41 @@ class ChangeInfoView(APIView):
response.data['change_type'] = CHANGER.get("change_type")
response.data['enable'] = allow_f
return Response(response.dict)
class WeChatLoginView(APIView):
throttle_classes = [VisitRegister1Throttle, VisitRegister2Throttle]
def get(self, request):
ret = BaseResponse()
code, qr_info = make_wx_login_qrcode()
return wx_qr_code_response(ret, code, qr_info)
class WeChatLoginCheckView(APIView):
def post(self, request):
ret = BaseResponse()
ticket = request.data.get("ticket")
if ticket:
wx_ticket_data = get_wx_ticket_login_info_cache(ticket)
if wx_ticket_data:
if wx_ticket_data == -1:
ret.msg = "还未绑定用户,请通过手机或者邮箱登录账户之后进行绑定"
ret.code = 1005
else:
user = UserInfo.objects.filter(pk=wx_ticket_data).first()
if user.is_active:
key, user_info = set_user_token(user)
serializer = UserInfoSerializer(user_info)
data = serializer.data
ret.msg = "验证成功!"
ret.userinfo = data
ret.token = key
else:
ret.msg = "用户被禁用"
ret.code = 1005
else:
ret.code = 1006
else:
ret.code = 1006
return Response(ret.dict)

@ -0,0 +1,176 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: 3月
# author: NinEveN
# date: 2021/3/29
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
import logging
import random
from rest_framework.views import APIView
from rest_framework_xml.parsers import XMLParser
from api.models import ThirdWeChatUserInfo, UserInfo
from api.utils.auth import ExpiringTokenAuthentication
from api.utils.mp.chat import reply, receive
from api.utils.mp.wechat import check_signature, WxMsgCrypt, get_userinfo_from_openid
from api.utils.response import BaseResponse
from api.utils.serializer import ThirdWxSerializer
from api.utils.storage.caches import set_wx_ticket_login_info_cache
logger = logging.getLogger(__name__)
GOOD_XX = [
'您是一位有恒心有毅力的人,我很佩服您!',
'越有内涵的人越虚怀若谷,像您这样有内涵的人我十分敬佩!',
'你像天上的月亮,也像那闪烁的星星,可惜我不是诗人,否则,当写一万首诗来形容你的美丽!',
'据考证,你是世界上最大的宝藏,里面藏满了金子、钻石和名画!',
'虽然你没有一簇樱唇两排贝齿,但你的谈吐高雅脱俗,机智过人,令我折服!',
'您是一位有恒心有毅力的人,我很佩服您!',
'我很荣幸,认识你这样有内涵的漂亮朋友!',
'春花秋月,是诗人们歌颂的情景,可是我对于它,却感到十分平凡。只有你嵌着梨涡的笑容,才是我眼中最美的景象!',
'你像一片轻柔的云在我眼前飘来飘去,你清丽秀雅的脸上荡漾着春天般美丽的笑容。在你那双又大又亮的眼睛里,我总能捕捉到你的宁静,你的热烈,你的聪颖,你的敏感!',
'人生旅程上,您丰富我的心灵,开发我得智力,为我点燃了希望的光芒,谢谢您!',
]
class TextXMLParser(XMLParser):
media_type = 'text/xml' # 微信解析的是 text/xml
def reply_login_msg(rec_msg, to_user, from_user, ):
content = f'还未绑定用户,请通过手机或者邮箱登录账户之后进行绑定'
u_data_id = -1
wx_user_obj = ThirdWeChatUserInfo.objects.filter(openid=to_user).first()
if wx_user_obj:
u_data_id = wx_user_obj.user_id.pk
content = f'用户 {wx_user_obj.user_id.first_name} 登录成功'
set_wx_ticket_login_info_cache(rec_msg.Ticket, u_data_id)
reply_msg = reply.TextMsg(to_user, from_user, content)
return reply_msg.send()
def update_or_create_wx_userinfo(to_user, user_obj):
code, wx_user_info = get_userinfo_from_openid(to_user)
logger.info(f"get openid:{to_user} info:{to_user} code:{code}")
if code:
wx_user_info = {
'openid': wx_user_info.get('openid'),
'nickname': wx_user_info.get('nickname'),
'sex': wx_user_info.get('sex'),
'subscribe_time': wx_user_info.get('subscribe_time'),
'head_img_url': wx_user_info.get('headimgurl'),
'address': f"{wx_user_info.get('country')}-{wx_user_info.get('province')}-{wx_user_info.get('city')}",
'subscribe': wx_user_info.get('subscribe'),
}
ThirdWeChatUserInfo.objects.update_or_create(user_id=user_obj, openid=to_user, defaults=wx_user_info)
class ValidWxChatToken(APIView):
parser_classes = (XMLParser, TextXMLParser)
def get(self, request):
params = request.query_params
return Response(check_signature(params))
def post(self, request):
params = request.query_params
data = request.data
encrypt_obj = WxMsgCrypt()
ret, encrypt_xml = encrypt_obj.decrypt_msg(data, params.get("msg_signature"), params.get("timestamp"),
params.get("nonce"))
logger.info(f"code:{ret}, result {encrypt_xml}")
result = "success"
if ret == 0:
content = '欢迎使用fly应用分发平台,感谢您的关注'
rec_msg = receive.parse_xml(encrypt_xml)
if isinstance(rec_msg, receive.Msg):
to_user = rec_msg.FromUserName
from_user = rec_msg.ToUserName
if rec_msg.MsgType == 'text':
content = random.choices([*GOOD_XX, content, rec_msg.Content.decode('utf-8')])[0]
reply_msg = reply.TextMsg(to_user, from_user, content)
result = reply_msg.send()
elif rec_msg.MsgType == 'image':
media_id = rec_msg.MediaId
reply_msg = reply.ImageMsg(to_user, from_user, media_id)
result = reply_msg.send()
else:
result = reply.Msg().send()
elif isinstance(rec_msg, receive.EventMsg):
to_user = rec_msg.FromUserName
from_user = rec_msg.ToUserName
if rec_msg.Event == 'CLICK': # 公众号点击事件
if rec_msg.Eventkey == 'good':
content = random.choices(GOOD_XX)[0]
reply_msg = reply.TextMsg(to_user, from_user, content)
result = reply_msg.send()
elif rec_msg.Event in ['subscribe', 'unsubscribe']: # 订阅
reply_msg = reply.TextMsg(to_user, from_user, content)
result = reply_msg.send()
if rec_msg.Eventkey == 'qrscene_web.login': # 首次关注,登录认证操作
if rec_msg.Event == 'subscribe':
result = reply_login_msg(rec_msg, to_user, from_user, )
if rec_msg.Event == 'unsubscribe':
ThirdWeChatUserInfo.objects.filter(openid=to_user).update(subscribe=False)
elif rec_msg.Event == 'SCAN':
if rec_msg.Eventkey == 'web.login': # 已经关注,然后再次扫码,登录认证操作
result = reply_login_msg(rec_msg, to_user, from_user, )
elif rec_msg.Eventkey.startswith('web.bind.'):
uid = rec_msg.Eventkey.split('.')[-1]
wx_user_obj = ThirdWeChatUserInfo.objects.filter(openid=to_user).first()
user_obj = UserInfo.objects.filter(uid=uid).first()
update_or_create_wx_userinfo(to_user, user_obj)
if wx_user_obj:
if user_obj and user_obj.uid == wx_user_obj.user_id.uid:
content = f'账户 {wx_user_obj.user_id.first_name} 已经绑定成功,感谢您的使用'
else:
content = f'账户已经被 {wx_user_obj.user_id.first_name} 绑定'
else:
content = f'账户绑定 {wx_user_obj.user_id.first_name} 成功'
set_wx_ticket_login_info_cache(rec_msg.Ticket, user_obj.pk)
reply_msg = reply.TextMsg(to_user, from_user, content)
result = reply_msg.send()
else:
logger.error('密文解密失败')
return Response(result)
class PageNumber(PageNumberPagination):
page_size = 10 # 每页显示多少条
page_size_query_param = 'size' # URL中每页显示条数的参数
page_query_param = 'page' # URL中页码的参数
max_page_size = None # 最大页码数限制
class ThirdWxAccount(APIView):
authentication_classes = [ExpiringTokenAuthentication, ]
def get(self, request):
res = BaseResponse()
# wx_open_id = request.query_params.get("openid", None)
wx_obj_lists = ThirdWeChatUserInfo.objects.filter(user_id=request.user)
# if wx_open_id:
# wx_obj_lists = wx_obj_lists.filter(openid=wx_open_id)
page_obj = PageNumber()
info_serializer = page_obj.paginate_queryset(queryset=wx_obj_lists.order_by("-subscribe_time"),
request=request,
view=self)
wx_user_info = ThirdWxSerializer(info_serializer, many=True, )
res.data = wx_user_info.data
res.count = wx_obj_lists.count()
return Response(res.dict)
def delete(self, request):
openid = request.query_params.get("openid")
user_id = request.query_params.get("user_id")
if openid and user_id:
ThirdWeChatUserInfo.objects.filter(user_id_id=user_id, openid=openid).delete()
return self.get(request)

@ -64,6 +64,19 @@ class CACHECONF(object):
password = ''
class THIRDLOGINCONF(object):
wx_official = {
'name': 'wx_official',
'auth': {
'app_id': 'we6',
'app_secret': '5bfb678',
'token': 'f0ae1b879b8',
'encoding_aes_key': '7b9URovp83gG',
},
'active': True
}
class AUTHCONF(object):
# 注册方式,如果启用sms或者email 需要配置 THIRD_PART_CONFIG.sender 信息
REGISTER = {

@ -209,7 +209,9 @@ CACHE_KEY_TEMPLATE = {
'upload_file_tmp_name_key': 'upload_file_tmp_name',
'login_failed_try_times_key': 'login_failed_try_times',
'user_free_download_times_key': 'user_free_download_times',
'super_sign_failed_send_msg_times_key': 'super_sign_failed_send_msg_times'
'super_sign_failed_send_msg_times_key': 'super_sign_failed_send_msg_times',
'wx_access_token_key': 'wx_basic_access_token',
'wx_ticket_info_key': 'wx_ticket_info',
}
DATA_DOWNLOAD_KEY = "d_token"
@ -219,6 +221,7 @@ AUTH_USER_FREE_DOWNLOAD_TIMES = 60
SYNC_CACHE_TO_DATABASE = {
'download_times': 10, # 下载次数同步时间
'wx_get_access_token_times': 60 * 10, # 微信access_token 自动获取时间
'try_login_times': (10, 12 * 60 * 60), # 当天登录失败次数,超过该失败次数,锁定24小时
'auto_clean_tmp_file_times': 60 * 30, # 定时清理上传失误生成的临时文件
'auto_clean_local_tmp_file_times': 60 * 30, # 定时清理临时文件,现在包含超级签名描述临时文件
@ -377,6 +380,11 @@ CELERY_BEAT_SCHEDULE = {
'args': (),
'one_off': True
},
'sync_wx_access_token_job': {
'task': 'api.tasks.sync_wx_access_token_job',
'schedule': SYNC_CACHE_TO_DATABASE.get("wx_get_access_token_times"),
'args': (),
},
}
MSGTEMPLATE = {

Loading…
Cancel
Save