增加消息提醒功能,下载余额,签名余额不足自动检测,发送邮件或微信通知功能

pull/37/head
nineven 3 years ago
parent 5eca01278b
commit 59e73fa83e
  1. 3
      fir_client/src/components/FirHeader.vue
  2. 649
      fir_client/src/components/user/FirUserNotify.vue
  3. 4
      fir_client/src/components/user/FirUserProfileInfo.vue
  4. 4
      fir_client/src/main.js
  5. 30
      fir_client/src/restful/index.js
  6. 11
      fir_client/src/router/index.js
  7. 75
      fir_ser/api/migrations/0004_auto_20220326_1847.py
  8. 42
      fir_ser/api/models.py
  9. 18
      fir_ser/api/tasks.py
  10. 3
      fir_ser/api/urls.py
  11. 16
      fir_ser/api/utils/ctasks.py
  12. 19
      fir_ser/api/utils/modelutils.py
  13. 22
      fir_ser/api/utils/serializer.py
  14. 18
      fir_ser/api/views/login_wx.py
  15. 175
      fir_ser/api/views/notify.py
  16. 36
      fir_ser/api/views/thirdlogin.py
  17. 4
      fir_ser/common/core/exception.py
  18. 30
      fir_ser/common/libs/mp/wechat.py
  19. 78
      fir_ser/common/notify/notify.py
  20. 131
      fir_ser/common/notify/ntasks.py
  21. 19
      fir_ser/common/notify/utils.py
  22. 2
      fir_ser/common/utils/caches.py
  23. 4
      fir_ser/config.py
  24. 18
      fir_ser/fir_ser/settings.py
  25. 13
      fir_ser/xsign/utils/supersignutils.py

@ -51,6 +51,7 @@
<el-dropdown-item command="userinfo">个人资料</el-dropdown-item>
<el-dropdown-item command="apitoken">API token</el-dropdown-item>
<el-dropdown-item command="setdomian">设置域名</el-dropdown-item>
<el-dropdown-item command="setnotify">消息中心</el-dropdown-item>
<el-dropdown-item v-if="$store.state.userinfo.role>1" command="setadvert">宣传广告
</el-dropdown-item>
<el-dropdown-item v-if="$store.state.userinfo.storage_active" command="storage">存储管理
@ -138,6 +139,8 @@ export default {
this.$router.push({"name": 'FirUserAdvert'})
} else if (command === 'myorder') {
this.$router.push({"name": 'FirUserOrders'})
} else if (command === 'setnotify') {
this.$router.push({"name": 'FirUserNotify'})
} else if (command === 'qrcode') {
this.$router.push({"name": 'FirUserQrcode'})
} else if (command === 'contact') {

@ -0,0 +1,649 @@
<template>
<el-main>
<el-dialog :close-on-click-modal="false" :destroy-on-close="true" :title="title"
:visible.sync="dialogReceiverVisible" width="780px">
<el-form ref="recevierform" :model="addreceiverinfo"
label-width="80px" style="margin:0 auto;">
<el-form-item label="姓名">
<el-input v-model="addreceiverinfo.receiver_name"/>
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="addreceiverinfo.email"/>
</el-form-item>
<!-- <el-form-item v-if="captcha.captcha_image" style="height: 40px" >-->
<!-- <el-row style="height: 40px">-->
<!-- <el-col :span="16">-->
<!-- <el-input v-model="addreceiverinfo.verify_code" clearable maxlength="6"-->
<!-- placeholder="请输入验证码" @keyup.enter.native="onSubmit"/>-->
<!-- </el-col>-->
<!-- <el-col :span="8">-->
<!-- <el-image-->
<!-- :src="captcha.captcha_image"-->
<!-- fit="contain"-->
<!-- style="margin:0 4px;border-radius:4px;cursor:pointer;height: 40px" @click="get_auth_code">-->
<!-- </el-image>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- </el-form-item>-->
<!-- <el-form-item>-->
<!-- <el-row>-->
<!-- <el-col :span="16">-->
<!-- <el-input v-model="addreceiverinfo.seicode" clearable-->
<!-- placeholder="邮箱验证码" prefix-icon="el-icon-mobile"/>-->
<!-- </el-col>-->
<!-- <el-col :span="8">-->
<!-- <el-button plain style="margin:0 4px;border-radius:4px;cursor:pointer;height: 40px" type="info"-->
<!-- @click="onGetCode">获取验证码-->
<!-- </el-button>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- </el-form-item>-->
<!-- <el-form-item>-->
<!-- <div id="captcha" ref="captcha"></div>-->
<!-- </el-form-item>-->
<el-form-item label="微信">
<el-tag v-if="addreceiverinfo.wxopenid && addreceiverinfo.wxopenid.length > 6">
{{ addreceiverinfo.wxopenid }}
</el-tag>
<el-popover v-else
v-model="wx_visible"
placement="top"
title="打开微信扫一扫进行绑定"
trigger="manual">
<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-form-item>
<el-form-item label="备注">
<el-input v-model="addreceiverinfo.description"/>
</el-form-item>
<el-button @click="saveReceiver">保存</el-button>
<el-button @click="cancelReceiver">取消</el-button>
</el-form>
</el-dialog>
<el-dialog :close-on-click-modal="false" :destroy-on-close="true" :visible.sync="dialogChangeReceiverVisible"
title="修改消息接收人">
<div style="text-align: left;margin-bottom: 10px">
<div style="margin-bottom: 10px">
<el-tag type="warning">提醒如果以下消息接收人的信息有变更请到消息接收人管理中进行修改</el-tag>
</div>
消息类型{{ notifyConfigMsg.message_type }} - {{ notifyConfigMsg.config_name }}
</div>
<el-table
ref="multipleTable"
:data="receiver_info_lists"
border
stripe
style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column
type="selection"
width="55">
</el-table-column>
<el-table-column
align="center"
fixed
label="姓名"
prop="receiver_name"
width="100">
</el-table-column>
<el-table-column
align="center"
label="邮箱"
prop="email"
width="200">
</el-table-column>
<el-table-column
align="center"
label="微信"
prop="domain_name"
width="260">
<template slot-scope="scope">
<el-popover v-if="scope.row.weixin.openid" placement="top" trigger="hover">
<div style="float: right;margin-top: 10px">
<el-image :src="scope.row.weixin.head_img_url" style="width: 80px;height: 80px"/>
</div>
<p>昵称: {{ scope.row.weixin.nickname }}</p>
<p>性别: {{ scope.row.weixin.sex|sex_filter }}</p>
<p>住址: {{ scope.row.weixin.address }}</p>
<p>微信ID: {{ scope.row.weixin.openid }}</p>
<div slot="reference" class="name-wrapper">
<el-tag>{{ scope.row.weixin.openid }}</el-tag>
</div>
</el-popover>
<el-tag v-else> /</el-tag>
</template>
</el-table-column>
<el-table-column
align="center"
label="备注"
prop="description">
</el-table-column>
</el-table>
<div style="text-align: left;margin-bottom: 10px;margin-top: 10px">
<el-button plain size="small" type="primary" @click="addRecevier">
新增消息接收人
</el-button>
</div>
<span slot="footer">
<el-button @click="updateConfig(undefined)">保存</el-button>
<el-button @click="dialogChangeReceiverVisible=false">取消</el-button>
</span>
</el-dialog>
<el-tabs v-model="activeName" tab-position="top" type="border-card" @tab-click="handleClick">
<el-tab-pane label="基本接收管理" name="config">
<el-table
:data="[{name:'消息类型',email:'邮件通知',weixin:'微信通知',user:'消息接收人'}]"
:show-header="false"
border
stripe
style="width: 100%">
<el-table-column
align="center"
prop="name"
width="200">
</el-table-column>
<el-table-column
align="center"
prop="email"
width="200">
</el-table-column>
<el-table-column
align="center"
prop="weixin"
width="200">
</el-table-column>
<el-table-column
align="center"
prop="user">
</el-table-column>
</el-table>
<el-collapse v-model="activeConfig">
<el-collapse-item v-for="info in message_type_choices" :key="info.id" :disabled="info.disabled"
:name="info.id">
<template slot="title">
<div style="width: 200px">
<h2>{{ info.name }}</h2>
</div>
</template>
<el-table
:data="info.data"
:show-header="false"
border
stripe
style="width: 100%">
<el-table-column
align="center"
fixed
prop="config_name"
width="200">
</el-table-column>
<el-table-column
align="center"
fixed
prop="enable_email"
width="200">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.enable_email" @change="updateConfig(scope.row)">邮件</el-checkbox>
</template>
</el-table-column>
<el-table-column
align="center"
fixed
prop="enable_weixin"
width="200">
<template slot-scope="scope">
<el-checkbox v-model="scope.row.enable_weixin" @change="updateConfig(scope.row)">微信</el-checkbox>
</template>
</el-table-column>
<el-table-column
align="center"
fixed
prop="senders">
<template slot-scope="scope">
<el-tag v-for="user in scope.row.senders" :key="user.id" style="margin-right: 3px" type="info">
{{ user.receiver_name }}
</el-tag>
<el-tag type="success">
<el-link :underline="false" type="success" @click="changeNotifyConfig(scope.row)">修改</el-link>
</el-tag>
</template>
</el-table-column>
</el-table>
</el-collapse-item>
</el-collapse>
</el-tab-pane>
<el-tab-pane label="消息接收人管理" name="receiver">
<div style="float: right;margin-bottom: 10px">
<el-button plain type="primary" @click="addRecevier">
新增消息接收人
</el-button>
</div>
<el-table
v-loading="loading"
:data="receiver_info_lists"
border
stripe
style="width: 100%">
<el-table-column
align="center"
fixed
label="姓名"
prop="receiver_name"
width="100">
</el-table-column>
<el-table-column
align="center"
label="邮箱"
prop="email"
width="200">
</el-table-column>
<el-table-column
align="center"
label="微信"
prop="domain_name">
<template slot-scope="scope">
<el-popover v-if="scope.row.weixin.openid" placement="top" trigger="hover">
<div style="float: right;margin-top: 10px">
<el-image :src="scope.row.weixin.head_img_url" style="width: 80px;height: 80px"/>
</div>
<p>昵称: {{ scope.row.weixin.nickname }}</p>
<p>性别: {{ scope.row.weixin.sex|sex_filter }}</p>
<p>住址: {{ scope.row.weixin.address }}</p>
<p>微信ID: {{ scope.row.weixin.openid }}</p>
<div slot="reference" class="name-wrapper">
<el-tag>{{ scope.row.weixin.openid }}</el-tag>
</div>
</el-popover>
<el-tag v-else> /</el-tag>
</template>
</el-table-column>
<el-table-column
:formatter="formatter"
align="center"
label="添加时间"
prop="create_time"
width="170">
</el-table-column>
<el-table-column
align="center"
label="备注"
prop="description">
</el-table-column>
<el-table-column
align="center"
label="操作"
width="120">
<template slot-scope="scope">
<!-- <el-button size="small" type="text" @click="editReceiver(scope.row)">编辑</el-button>-->
<el-button size="small" type="text" @click="deleteReceiver(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="阈值设置" name="threshold" style="text-align: center">
<el-tag style="margin: 20px 0"> 低于阈值设置将会出发报警若您配置了消息接收将会将报警信息推送至关联的接收人</el-tag>
<div style="margin: auto;width: 700px;height: 100%">
<el-form ref="form" :model="threshold" label-width="180px">
<el-form-item label="下载次数不足阈值设置">
<el-input-number v-model="threshold.notify_available_downloads" :min="0"
style="width: 300px;margin: 0 20px"></el-input-number>
<el-button @click="notifyThreshold('update')">保存修改</el-button>
</el-form-item>
<el-form-item label="签名次数不足阈值设置">
<el-input-number v-model="threshold.notify_available_signs" :min="0"
style="width: 300px;margin: 0 20px"></el-input-number>
<el-button @click="notifyThreshold('update')">保存修改</el-button>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
</el-tabs>
</el-main>
</template>
<script>
import {notifyConfigInfo, notifyReceiverInfo, wxBindFun, wxLoginFun} from "@/restful";
import {getUserInfoFun} from "@/utils";
import {getRandomStr} from "@/utils/base/utils";
export default {
name: "FirUserNotify",
data() {
return {
dialogReceiverVisible: false,
captcha: {"captcha_image": '', "captcha_key": '', "length": 8},
wx_visible: false,
wx_login_qr_url: '',
unique_key: '',
userinfo: {},
addreceiverinfo: {description: '', wxopenid: '', email: '', receiver_name: ''},
message_type_choices: [],
activeConfig: [],
dialogChangeReceiverVisible: false,
notifyConfigSelection: [],
notifyConfigMsg: {},
title: '',
isedit: false,
activeName: 'config',
receiver_info_lists: [],
loading: false,
threshold: {}
}
}, methods: {
addRecevier() {
this.addreceiverinfo = {description: '', wxopenid: '', email: '', receiver_name: ''}
this.dialogReceiverVisible = true
this.title = '新增消息接收人'
},
editReceiver(receiver) {
this.title = '编辑 ' + receiver.receiver_name + ' 消息接收人'
this.isedit = true
this.addreceiverinfo = receiver
this.dialogReceiverVisible = true
},
updateConfig(config = undefined) {
let data = {}
if (config) {
data = {
'config': config.id,
'enable_email': config.enable_email,
'enable_weixin': config.enable_weixin,
'm_type': config.message_type
}
} else {
let receiver_ids = []
for (let receiver of this.notifyConfigSelection) {
receiver_ids.push(receiver.id)
}
data = {'config': this.notifyConfigMsg.id, 'receiver': receiver_ids, 'm_type': this.notifyConfigMsg.m_type}
}
notifyConfigInfo(data => {
if (data.code === 1000) {
this.$message.success('保存成功')
if (!config) {
this.dialogChangeReceiverVisible = false;
}
this.notifyConfigInfoFun();
} else {
this.$message.error('保存失败,' + data.msg);
}
}, {"methods": 'PUT', "data": data});
},
changeNotifyConfig(info) {
let params = {"config": info.id, "message_type": info.m_type}
this.dialogChangeReceiverVisible = true;
notifyReceiverInfo(data => {
if (data.code === 1000) {
this.receiver_info_lists = data.data;
this.notifyConfigMsg = data.config
// eslint-disable-next-line no-unused-vars
this.$nextTick(res => {
data.data.forEach(row => {
for (let sender of data.senders) {
if (sender.sender === row.id) {
this.$refs.multipleTable.toggleRowSelection(row);
}
}
});
})
} else {
this.$message.error('获取失败,' + data);
}
this.loading = false;
}, {"methods": 'GET', "data": params});
},
handleSelectionChange(val) {
this.notifyConfigSelection = val;
},
// eslint-disable-next-line no-unused-vars
handleClick(tab, event) {
this.get_data_from_tabname(tab.name);
},
cancelReceiver() {
this.dialogReceiverVisible = false;
this.addreceiverinfo = {description: '', wxopenid: '', email: '', receiver_name: ''}
},
saveReceiver() {
notifyReceiverInfo(data => {
if (data.code === 1000) {
this.$message.success('操作成功');
this.dialogReceiverVisible = false;
if (this.dialogChangeReceiverVisible) {
this.changeNotifyConfig(this.notifyConfigMsg)
} else {
this.get_data_from_tabname(this.activeName);
}
} else {
this.$message.error('操作失败,' + data.msg);
}
}, {"methods": 'POST', 'data': this.addreceiverinfo});
},
deleteReceiver(sinfo) {
this.$confirm(`将要删除接收人 ${sinfo.receiver_name}, 是否继续?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
notifyReceiverInfo(data => {
if (data.code === 1000) {
this.$message.success('删除成功');
this.notifyReceiverInfoFun();
}
}, {"methods": 'DELETE', 'data': {'id': sinfo.id}})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
notifyReceiverInfoFun() {
this.loading = true;
notifyReceiverInfo(data => {
if (data.code === 1000) {
this.receiver_info_lists = data.data;
} else {
this.$message.error('获取失败,' + data);
}
this.loading = false;
}, {"methods": 'GET'});
},
setActiveConfig(message_type_choices) {
let activeConfig = []
for (let i of message_type_choices) {
activeConfig.push(i.id)
}
this.activeConfig = activeConfig;
},
notifyConfigInfoFun() {
this.loading = true;
notifyConfigInfo(data => {
if (data.code === 1000) {
this.receiver_info_lists = data.data;
this.message_type_choices = data.message_type_choices;
this.setActiveConfig(this.message_type_choices);
} else {
this.$message.error('获取失败,' + data);
}
this.loading = false;
}, {"methods": 'GET'});
},
notifyThreshold(act) {
notifyConfigInfo(data => {
if (data.code === 1000) {
this.threshold = data.data
if (act !== 'get') {
this.$message.success('保存成功')
}
} else {
this.$message.error('获取失败,' + data);
}
this.loading = false;
}, {"methods": 'POST', 'data': {'act': act, 'threshold': this.threshold}});
},
loop_get_wx_info(wx_login_ticket, c_count = 1, unique_key = getRandomStr()) {
if (wx_login_ticket && wx_login_ticket.length < 3) {
this.$message.error("获取登陆码失败,请稍后再试");
return
}
if (!this.wx_visible) {
return;
}
wxLoginFun(data => {
c_count += 1;
if (c_count > 30) {
return;
}
if (data.code === 1000) {
if (this.userinfo.uid === data.data.uid) {
if (data.data.to_user) {
this.addreceiverinfo.wxopenid = data.data.to_user
}
this.$message.success("绑定成功");
this.wx_visible = false;
}
} else if (data.code === 1005) {
this.$message({
message: data.msg,
type: 'error',
duration: 30000
});
} else if (data.code === 1004) {
this.loop_flag = false;
} else if (data.code === 1006) {
return this.loop_get_wx_info(wx_login_ticket, c_count, unique_key)
}
}, {
"methods": "POST",
data: {"ticket": wx_login_ticket, "unique_key": unique_key}
})
},
wxLogin() {
this.wx_visible = !this.wx_visible;
this.wx_login_qr_url = '';
if (this.wx_visible) {
wxBindFun(data => {
if (data.code === 1000) {
this.wx_login_qr_url = data.data.qr;
this.loop_get_wx_info(data.data.ticket);
} else {
this.$message.error(data.msg);
this.wx_visible = false;
}
}, {
"methods": "POST", "data": {"unique_key": this.unique_key, "w_type": 'notify'}
})
}
},
// eslint-disable-next-line no-unused-vars
formatter(row, column) {
let stime = row.create_time;
if (stime) {
stime = stime.split(".")[0].split("T");
return stime[0] + " " + stime[1]
} else
return '';
},
// eslint-disable-next-line no-unused-vars
get_data_from_tabname(tabname, data = {}) {
this.$router.push({"name": 'FirUserNotify', params: {act: tabname}});
if (tabname === "config") {
this.notifyConfigInfoFun();
} else if (tabname === "receiver") {
this.notifyReceiverInfoFun();
} else if (tabname === "threshold") {
this.notifyThreshold('get');
}
},
}, mounted() {
getUserInfoFun(this);
this.userinfo = this.$store.state.userinfo;
this.unique_key = getRandomStr();
if (this.$route.params.act) {
let activeName = this.$route.params.act;
let activeName_list = ["config", "receiver", "threshold"];
for (let index in activeName_list) {
if (activeName_list[index] === activeName) {
this.activeName = activeName;
this.get_data_from_tabname(activeName);
return
}
}
}
this.get_data_from_tabname(this.activeName);
}, filters: {
sex_filter: function (x) {
let ret = '未知';
if (x === 1) {
ret = '男'
} else if (x === 2) {
ret = '女'
}
return ret;
},
}, watch: {
'$store.state.userinfo': function () {
this.userinfo = this.$store.state.userinfo;
}
}
}
</script>
<style scoped>
.el-main {
text-align: center;
margin: 20px auto 100px;
width: 1166px;
position: relative;
padding-bottom: 1px;
color: #9b9b9b;
-webkit-font-smoothing: antialiased;
border-radius: 1%;
}
</style>

@ -413,7 +413,7 @@ export default {
return;
}
if (data.code === 1000) {
if (this.userinfo.uid === data.userinfo.uid) {
if (this.userinfo.uid === data.data.uid) {
this.$message.success("绑定成功");
this.wx_visible = false;
}
@ -446,7 +446,7 @@ export default {
this.wx_visible = false;
}
}, {
"methods": "POST", "data": {"unique_key": this.unique_key}
"methods": "POST", "data": {"unique_key": this.unique_key, "w_type": 'login'}
})
}
},

@ -25,6 +25,8 @@ import {
Checkbox,
CheckboxGroup,
Col,
Collapse,
CollapseItem,
Container,
DatePicker,
Dialog,
@ -158,6 +160,8 @@ Vue.use(InputNumber);
Vue.use(Radio);
Vue.use(Backtop);
Vue.use(Transfer);
Vue.use(Collapse);
Vue.use(CollapseItem);
Vue.prototype.$message = Message;
Vue.prototype.$notify = Notification;
Vue.prototype.$loading = Loading.service;

@ -717,6 +717,36 @@ export function appReport(callBack, params, load = true) {
);
}
/**消息接收人配置 */
export function notifyReceiverInfo(callBack, params, load = true) {
getData(
params.methods,
USERSEVER + '/notify/receiver',
params.data,
data => {
callBack(data);
},
load,
true,
true
);
}
/**消息配置 */
export function notifyConfigInfo(callBack, params, load = true) {
getData(
params.methods,
USERSEVER + '/notify/config',
params.data,
data => {
callBack(data);
},
load,
true,
true
);
}
/** 超级签名************************************************相关api */
let SIGNSEVER = DOMAIN + '/api/v1/fir/xsign';

@ -146,6 +146,17 @@ const router = new VueRouter({
}
]
},
{
path: 'notify',
component: () => import("@/components/user/FirUserNotify"),
children: [
{
path: ':act',
name: 'FirUserNotify',
meta: {label: '消息中心'},
}
]
},
{
path: 'orders',
name: 'FirUserOrders',

@ -0,0 +1,75 @@
# Generated by Django 3.2.3 on 2022-03-26 18:47
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0003_appbundleidblacklist'),
]
operations = [
migrations.AddField(
model_name='thirdwechatuserinfo',
name='enable_login',
field=models.BooleanField(default=0, verbose_name='是否允许登录'),
),
migrations.AddField(
model_name='thirdwechatuserinfo',
name='enable_notify',
field=models.BooleanField(default=0, verbose_name='是否允许推送消息'),
),
migrations.AddField(
model_name='userinfo',
name='notify_available_downloads',
field=models.IntegerField(blank=True, default=0, null=True, verbose_name='下载余额不足通知'),
),
migrations.AddField(
model_name='userinfo',
name='notify_available_signs',
field=models.IntegerField(blank=True, default=0, null=True, verbose_name='签名余额不足通知'),
),
migrations.CreateModel(
name='NotifyReceiver',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('receiver_name', models.CharField(max_length=128, unique=True, verbose_name='姓名')),
('email', models.EmailField(blank=True, max_length=255, null=True, verbose_name='邮箱')),
('description', models.CharField(blank=True, default='', max_length=256, verbose_name='备注')),
('create_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')),
('weixin',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.thirdwechatuserinfo',
verbose_name='微信ID')),
],
options={
'verbose_name': '信息接收配置',
'verbose_name_plural': '信息接收配置',
'unique_together': {('user_id', 'email'), ('user_id', 'weixin')},
},
),
migrations.CreateModel(
name='NotifyConfig',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('config_name', models.CharField(max_length=128, unique=True, verbose_name='通知名称')),
('message_type', models.SmallIntegerField(
choices=[(0, '签名余额不足'), (1, '下载次数不足'), (2, '应用签名限额'), (3, '应用签名失败'), (4, '充值到账提醒'), (5, '优惠活动通知'),
(6, '证书到期消息')], default=5, verbose_name='消息类型')),
('enable_weixin', models.BooleanField(default=True, verbose_name='是否启用该配置项')),
('enable_email', models.BooleanField(default=True, verbose_name='是否启用该配置项')),
('description', models.CharField(blank=True, default='', max_length=256, verbose_name='备注')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='添加时间')),
('sender', models.ManyToManyField(to='api.NotifyReceiver', verbose_name='通知接受者方式')),
('user_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL,
verbose_name='用户ID')),
],
options={
'verbose_name': '信息接收配置',
'verbose_name_plural': '信息接收配置',
},
),
]

@ -46,6 +46,8 @@ class UserInfo(AbstractUser):
storage = models.OneToOneField(to='AppStorage', related_name='app_storage',
on_delete=models.SET_NULL, verbose_name="存储", null=True, blank=True)
api_token = models.CharField(max_length=256, verbose_name='api访问密钥', default='')
notify_available_downloads = models.IntegerField(default=0, verbose_name="下载余额不足通知", blank=True, null=True)
notify_available_signs = models.IntegerField(default=0, verbose_name="签名余额不足通知", blank=True, null=True)
class Meta:
verbose_name = '账户信息'
@ -77,6 +79,8 @@ class ThirdWeChatUserInfo(models.Model):
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)
enable_login = models.BooleanField(verbose_name="是否允许登录", default=0)
enable_notify = models.BooleanField(verbose_name="是否允许推送消息", default=0)
created_time = models.DateTimeField(auto_now_add=True, verbose_name="授权时间")
def __str__(self):
@ -469,3 +473,41 @@ class SystemConfig(models.Model):
def __str__(self):
return "%s-%s" % (self.key, self.description)
class NotifyReceiver(models.Model):
receiver_name = models.CharField(max_length=128, unique=True, verbose_name="姓名")
user_id = models.ForeignKey(to=UserInfo, verbose_name="用户ID", on_delete=models.CASCADE)
weixin = models.ForeignKey(to=ThirdWeChatUserInfo, verbose_name="微信ID", on_delete=models.CASCADE, null=True)
email = models.EmailField(verbose_name='邮箱', max_length=255, blank=True, null=True)
description = models.CharField(verbose_name="备注", max_length=256, default='', blank=True)
create_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
class Meta:
verbose_name = '信息接收配置'
verbose_name_plural = "信息接收配置"
unique_together = (('user_id', 'email',), ('user_id', 'weixin'))
def __str__(self):
return "%s-%s-%s" % (self.user_id, self.receiver_name, self.description)
class NotifyConfig(models.Model):
user_id = models.ForeignKey(to=UserInfo, verbose_name="用户ID", on_delete=models.CASCADE)
config_name = models.CharField(max_length=128, unique=True, verbose_name="通知名称")
message_type_choices = (
(0, '签名余额不足'), (1, '下载次数不足'), (2, '应用签名限额'), (3, '应用签名失败'),
(4, '充值到账提醒'), (5, '优惠活动通知'), (6, '证书到期消息'))
message_type = models.SmallIntegerField(choices=message_type_choices, default=5, verbose_name="消息类型")
sender = models.ManyToManyField(to=NotifyReceiver, verbose_name="通知接受者方式")
enable_weixin = models.BooleanField(default=True, verbose_name="是否启用该配置项")
enable_email = models.BooleanField(default=True, verbose_name="是否启用该配置项")
description = models.CharField(verbose_name="备注", max_length=256, default='', blank=True)
create_time = models.DateTimeField(auto_now_add=True, verbose_name="添加时间")
class Meta:
verbose_name = '信息接收配置'
verbose_name_plural = "信息接收配置"
def __str__(self):
return "%s-%s-%s" % (self.user_id, self.get_message_type_display(), self.sender)

@ -8,7 +8,8 @@ import logging
from captcha.models import CaptchaStore
from api.utils.ctasks import sync_download_times, auto_clean_upload_tmp_file, auto_clean_remote_client_log
from api.utils.ctasks import sync_download_times, auto_clean_upload_tmp_file, auto_clean_remote_client_log, \
notify_check_user_download_times, notify_check_apple_developer_devices, notify_check_apple_developer_cert
from api.views.login import get_login_type
from common.core.sysconfig import Config, invalid_config_cache
from common.libs.geetest.geetest_utils import check_bypass_status
@ -69,3 +70,18 @@ def sync_wx_access_token_job():
@app.task
def auto_clean_remote_client_job():
auto_clean_remote_client_log()
@app.task
def download_times_notify_check_job():
notify_check_user_download_times()
@app.task
def apple_developer_devices_check_job():
notify_check_apple_developer_devices()
@app.task
def apple_developer_cert_notify_check_job():
notify_check_apple_developer_cert()

@ -23,6 +23,7 @@ from api.views.login import LoginView, UserInfoView, RegistView, AuthorizationVi
UserApiTokenView, CertificationView, ChangeInfoView
from api.views.login_wx import WeChatLoginView, WeChatLoginCheckView, WeChatBindView, WeChatWebLoginView
from api.views.logout import LogoutView
from api.views.notify import NotifyReceiverView, NotifyConfigView
from api.views.order import PriceView, OrderView, PaySuccess, OrderSyncView
from api.views.report import ReportView
from api.views.storage import StorageView, CleanStorageView
@ -55,6 +56,8 @@ urlpatterns = [
re_path("^qrcode$", AppsQrcodeShowView.as_view()),
re_path("^package_prices$", PriceView.as_view()),
re_path("^orders$", OrderView.as_view()),
re_path("^notify/receiver$", NotifyReceiverView.as_view()),
re_path("^notify/config$", NotifyConfigView.as_view()),
re_path("^orders.sync$", OrderSyncView.as_view()),
re_path("^certification$", CertificationView.as_view()),
re_path(r"^pay_success/(?P<name>\w+)$", PaySuccess.as_view()),

@ -10,6 +10,7 @@ import time
from django.core.cache import cache
from api.models import Apps, UserInfo, RemoteClientInfo
from common.notify.ntasks import check_user_download_times, check_apple_developer_devices, check_apple_developer_cert
from common.utils.storage import Storage
from fir_ser.settings import CACHE_KEY_TEMPLATE
@ -50,3 +51,18 @@ def auto_clean_upload_tmp_file():
def auto_clean_remote_client_log(clean_day=30):
clean_time = datetime.datetime.now() - datetime.timedelta(days=clean_day)
return RemoteClientInfo.objects.filter(created_time__lt=clean_time).delete()
def notify_check_user_download_times():
for user_obj in UserInfo.objects.filter(is_active=True).all():
check_user_download_times(user_obj, days=[0, 3, 7])
def notify_check_apple_developer_devices():
for user_obj in UserInfo.objects.filter(is_active=True, supersign_active=True).all():
check_apple_developer_devices(user_obj, days=[0, 3, 7])
def notify_check_apple_developer_cert():
for user_obj in UserInfo.objects.filter(is_active=True, supersign_active=True).all():
check_apple_developer_cert(user_obj, expire_day=7)

@ -13,7 +13,7 @@ from django.db.models import Count
from rest_framework.pagination import PageNumberPagination
from api.models import AppReleaseInfo, UserDomainInfo, DomainCnameInfo, UserAdDisplayInfo, RemoteClientInfo, \
AppBundleIdBlackList
AppBundleIdBlackList, NotifyReceiver, ThirdWeChatUserInfo
from common.base.baseutils import get_server_domain_from_request, get_user_default_domain_name, get_real_ip_address, \
get_origin_domain_name
@ -202,3 +202,20 @@ def check_bundle_id_legal(user_uid, bundle_id):
if app_black_obj:
return app_black_obj.status == 0
return False
def get_notify_wx_queryset(user_obj, message_type):
notify_weixin_ids = NotifyReceiver.objects.filter(notifyconfig__user_id=user_obj,
notifyconfig__message_type=message_type,
notifyconfig__enable_weixin=True, weixin__isnull=False,
user_id=user_obj).values('weixin').distinct()
return ThirdWeChatUserInfo.objects.filter(subscribe=True, user_id=user_obj, enable_notify=True,
pk__in=notify_weixin_ids).all()
def get_notify_email_queryset(user_obj, message_type):
return NotifyReceiver.objects.filter(notifyconfig__user_id=user_obj,
notifyconfig__message_type=message_type,
notifyconfig__enable_email=True, email__isnull=False,
user_id=user_obj).values('email').distinct()

@ -456,3 +456,25 @@ class AppReportSerializer(serializers.ModelSerializer):
class Meta:
model = models.AppReportInfo
exclude = ["id"]
class NotifyReceiverSerializer(serializers.ModelSerializer):
class Meta:
model = models.NotifyReceiver
exclude = ["user_id"]
weixin = serializers.SerializerMethodField()
def get_weixin(self, obj):
return ThirdWxSerializer(obj.weixin).data
class NotifyConfigSerializer(serializers.ModelSerializer):
class Meta:
model = models.NotifyConfig
exclude = ["user_id", "sender"]
senders = serializers.SerializerMethodField()
def get_senders(self, obj):
return NotifyReceiverSerializer(obj.sender, many=True).data

@ -65,13 +65,14 @@ class WeChatBindView(APIView):
ret = BaseResponse()
uid = request.user.uid
unique_key = request.data.get('unique_key')
w_type = request.data.get('w_type', 'xxxx')
if unique_key:
cache_obj = WxLoginBindCache(unique_key)
cache_data = cache_obj.get_storage_cache()
if cache_data:
code, qr_info = cache_data
else:
code, qr_info = cache_data = make_wx_login_qrcode(f"web.bind.{uid}")
code, qr_info = cache_data = make_wx_login_qrcode(f"web.bind.{uid}.{w_type}")
cache_obj.set_storage_cache(cache_data, qr_info.get('expire_seconds', 600))
return wx_qr_code_response(ret, code, qr_info, get_real_ip_address(request))
return Response(ret.dict)
@ -101,14 +102,19 @@ class WeChatLoginCheckView(APIView):
ret.msg = "还未绑定用户,请通过手机或者邮箱登录账户之后进行绑定"
ret.code = 1005
else:
w_type = wx_ticket_data.get('w_type', '')
to_user = wx_ticket_data.get('to_user', '')
user = UserInfo.objects.filter(pk=wx_ticket_data['pk']).first()
if user.is_active:
key, user_info = set_user_token(user, request)
serializer = UserInfoSerializer(user_info)
data = serializer.data
if to_user and w_type:
ret.data = {'uid': user.uid, 'to_user': to_user, 'w_type': w_type}
else:
key, user_info = set_user_token(user, request)
serializer = UserInfoSerializer(user_info)
data = serializer.data
ret.userinfo = data
ret.token = key
ret.msg = "验证成功!"
ret.userinfo = data
ret.token = key
else:
ret.msg = "用户被禁用"
ret.code = 1005

@ -0,0 +1,175 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: fir_ser
# filename: notify
# author: liuyu
# data: 2022/3/25
import logging
from rest_framework.response import Response
from rest_framework.views import APIView
from api.models import NotifyReceiver, ThirdWeChatUserInfo, NotifyConfig, UserInfo
from api.utils.modelutils import PageNumber
from api.utils.response import BaseResponse
from api.utils.serializer import NotifyReceiverSerializer, NotifyConfigSerializer
from common.base.baseutils import get_choices_dict, get_choices_name_from_key
from common.core.auth import ExpiringTokenAuthentication
logger = logging.getLogger(__name__)
class NotifyConfigView(APIView):
authentication_classes = [ExpiringTokenAuthentication, ]
def get(self, request):
res = BaseResponse()
config_pk = request.query_params.get('pk')
obj_lists = NotifyConfig.objects.filter(user_id=request.user).order_by('message_type').order_by("-create_time")
if config_pk:
obj_lists = obj_lists.filter(pk=config_pk).first()
info = NotifyConfigSerializer(obj_lists)
res.data = info.data
return Response(res.dict)
info = NotifyConfigSerializer(obj_lists.all(), many=True).data
res.count = obj_lists.count()
message_type_choices = get_choices_dict(NotifyConfig.message_type_choices)
for message_info in message_type_choices:
message_info['data'] = []
for notify_config_info in info:
if notify_config_info['message_type'] == message_info['id']:
message_info['data'].append(notify_config_info)
if len(message_info['data']) == 0:
data_info = NotifyConfigSerializer(NotifyConfig.objects.filter(pk=-1).first()).data
data_info['config_name'] = message_info['name']
data_info['message_type'] = message_info['id']
data_info['m_type'] = message_info['id']
data_info['id'] = -1
message_info['data'].append(data_info)
res.message_type_choices = message_type_choices
return Response(res.dict)
def post(self, request):
res = BaseResponse()
data = request.data
act = data.get('act', '')
if act and act != 'get':
threshold = data.get('threshold', {})
if threshold:
notify_available_downloads = threshold.get('notify_available_downloads')
notify_available_signs = threshold.get('notify_available_signs')
if notify_available_downloads is not None:
request.user.notify_available_downloads = notify_available_downloads
if notify_available_signs is not None:
request.user.notify_available_signs = notify_available_signs
if notify_available_downloads is not None or notify_available_signs is not None:
request.user.save(update_fields=['notify_available_downloads', 'notify_available_signs'])
res.data = UserInfo.objects.filter(pk=request.user.pk).values('notify_available_downloads',
'notify_available_signs').first()
return Response(res.dict)
def put(self, request):
res = BaseResponse()
data = request.data
config_pk = data.get('config')
receiver_ids = data.get('receiver')
enable_email = data.get('enable_email')
enable_weixin = data.get('enable_weixin')
m_type = data.get('m_type')
if config_pk and config_pk == -1 and m_type is not None:
notify_config_obj = NotifyConfig.objects.filter(user_id=request.user, message_type=int(m_type)).filter()
if not notify_config_obj:
notify_config_obj = NotifyConfig.objects.create(user_id=request.user, message_type=int(m_type),
config_name=get_choices_name_from_key(
NotifyConfig.message_type_choices, int(m_type)))
else:
notify_config_obj = NotifyConfig.objects.filter(user_id=request.user, pk=config_pk).first()
if notify_config_obj:
if isinstance(receiver_ids, list):
notify_config_obj.sender.set(
NotifyReceiver.objects.filter(user_id=request.user, pk__in=receiver_ids).all())
if enable_weixin is not None:
notify_config_obj.enable_weixin = enable_weixin
if enable_email is not None:
notify_config_obj.enable_email = enable_email
if enable_weixin is not None or enable_email is not None:
notify_config_obj.save(update_fields=['enable_email', 'enable_weixin'])
return Response(res.dict)
class NotifyReceiverView(APIView):
authentication_classes = [ExpiringTokenAuthentication, ]
def get(self, request):
res = BaseResponse()
config_pk = request.query_params.get('config')
message_type = request.query_params.get('message_type')
obj_lists = NotifyReceiver.objects.filter(user_id=request.user)
config_obj_lists = NotifyConfig.objects.filter(user_id=request.user)
if config_pk and config_pk != '-1':
res.senders = config_obj_lists.values('sender').filter(pk=config_pk).all()
config_obj = config_obj_lists.filter(pk=config_pk).first()
if config_obj:
res.config = {'config_name': config_obj.config_name,
'id': config_obj.pk, 'm_type': config_obj.message_type,
'message_type': config_obj.get_message_type_display()}
if config_pk and config_pk == '-1' and message_type is not None:
res.senders = config_obj_lists.values('sender').filter(pk=config_pk).all()
config_name = get_choices_name_from_key(NotifyConfig.message_type_choices, int(message_type))
res.config = {'config_name': config_name, 'id': -1, 'message_type': config_name,
'm_type': message_type}
page_obj = PageNumber()
obj_info_serializer = page_obj.paginate_queryset(queryset=obj_lists.order_by("-create_time"),
request=request,
view=self)
info = NotifyReceiverSerializer(obj_info_serializer, many=True, )
res.data = info.data
res.count = obj_lists.count()
return Response(res.dict)
def post(self, request):
res = BaseResponse()
data = request.data
receiver_name = data.get('receiver_name')
description = data.get('description')
wxopenid = data.get('wxopenid')
email = data.get('email')
if receiver_name:
data_info = {
'receiver_name': receiver_name,
'description': description,
}
wx_obj = None
if wxopenid:
wx_obj = ThirdWeChatUserInfo.objects.filter(user_id=request.user, openid=wxopenid).first()
if wx_obj:
data_info['weixin'] = wx_obj
if email:
data_info['email'] = email
try:
NotifyReceiver.objects.create(user_id=request.user, **data_info)
if wx_obj:
wx_obj.enable_notify = True
wx_obj.save(update_fields=['enable_notify'])
except Exception as e:
logger.error(f"{request.user} notify receiver add failed . data:{data} Exception:{e}")
res.code = 1001
res.msg = '数据有误或者已经存在该接收人信息'
return Response(res.dict)
def delete(self, request):
res = BaseResponse()
pk = request.query_params.get('id')
if pk:
NotifyReceiver.objects.filter(user_id=request.user, pk=pk).delete()
return Response(res.dict)

@ -54,56 +54,62 @@ def reply_login_msg(rec_msg, to_user, from_user, ):
content = f'用户 {wx_user_obj.user_id.first_name} 登录成功'
WxTemplateMsg(to_user, wx_user_obj.nickname).login_success_msg(wx_user_obj.user_id.first_name)
else:
wx_user_info = update_or_create_wx_userinfo(to_user, None, False)
wx_user_info = update_or_create_wx_userinfo(to_user)
WxTemplateMsg(to_user, wx_user_info.get('nickname', '')).login_failed_msg()
if wx_ticket_info and wx_ticket_info.get('ip_addr'):
ip_addr = wx_ticket_info.get('ip_addr')
logger.info(f"{content} ip:{ip_addr}")
set_wx_ticket_login_info_cache(rec_msg.Ticket, {'pk': u_data_id})
set_wx_ticket_login_info_cache(rec_msg.Ticket, {'pk': u_data_id, 'to_user': to_user})
reply_msg = reply.TextMsg(to_user, from_user, content)
return reply_msg.send()
def update_or_create_wx_userinfo(to_user, user_obj, create=True):
def update_or_create_wx_userinfo(to_user, user_obj=None, w_type=''):
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'), # 最新微信接口已经取消该字段
# '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'), # 最新微信接口已经取消该字段
# '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'),
}
if create:
if user_obj:
if w_type == 'login':
wx_user_info['enable_login'] = True
if w_type == 'notify':
wx_user_info['enable_notify'] = True
ThirdWeChatUserInfo.objects.update_or_create(user_id=user_obj, openid=to_user, defaults=wx_user_info)
return wx_user_info
def wx_bind_utils(rec_msg, to_user, from_user, content):
uid = rec_msg.Eventkey.split('.')[-1]
w_type = rec_msg.Eventkey.split('.')[-1]
uid = rec_msg.Eventkey.split('.')[-2]
wx_user_obj = ThirdWeChatUserInfo.objects.filter(openid=to_user).first()
user_obj = UserInfo.objects.filter(uid=uid).first()
if wx_user_obj:
wx_template_msg_obj = WxTemplateMsg(to_user, wx_user_obj.nickname)
if user_obj and user_obj.uid == wx_user_obj.user_id.uid:
content = f'账户 {wx_user_obj.user_id.first_name} 已经绑定成功,感谢您的使用'
update_or_create_wx_userinfo(to_user, user_obj)
update_or_create_wx_userinfo(to_user, user_obj, w_type)
wx_template_msg_obj.bind_success_msg(user_obj.first_name)
else:
content = f'账户已经被 {wx_user_obj.user_id.first_name} 绑定'
wx_template_msg_obj.bind_failed_msg(content)
else:
if user_obj:
wx_user_info = update_or_create_wx_userinfo(to_user, user_obj)
wx_user_info = update_or_create_wx_userinfo(to_user, user_obj, w_type)
content = f'账户绑定 {user_obj.first_name} 成功'
WxTemplateMsg(to_user, wx_user_info.get('nickname', '')).bind_success_msg(user_obj.first_name)
if user_obj:
set_wx_ticket_login_info_cache(rec_msg.Ticket, {'pk': user_obj.pk})
set_wx_ticket_login_info_cache(rec_msg.Ticket, {'pk': user_obj.pk, 'w_type': w_type, 'to_user': to_user})
reply_msg = reply.TextMsg(to_user, from_user, content)
return reply_msg.send()
@ -180,7 +186,7 @@ class ValidWxChatToken(APIView):
user_obj.email)
else:
content = '暂无登录绑定信息'
wx_user_info = update_or_create_wx_userinfo(to_user, None, False)
wx_user_info = update_or_create_wx_userinfo(to_user)
WxTemplateMsg(to_user, wx_user_info.get('nickname')).query_bind_info_failed_msg(
"查询登录绑定", content)
@ -192,7 +198,7 @@ class ValidWxChatToken(APIView):
ThirdWeChatUserInfo.objects.filter(openid=to_user).delete()
else:
content = f'暂无登录绑定信息'
wx_user_info = update_or_create_wx_userinfo(to_user, None, False)
wx_user_info = update_or_create_wx_userinfo(to_user)
WxTemplateMsg(to_user, wx_user_info.get('nickname')).query_bind_info_failed_msg(
"解除登录绑定", content)
@ -228,7 +234,7 @@ class ThirdWxAccount(APIView):
def get(self, request):
res = BaseResponse()
if get_login_type().get('third', '').get('wxp'):
wx_obj_lists = ThirdWeChatUserInfo.objects.filter(user_id=request.user)
wx_obj_lists = ThirdWeChatUserInfo.objects.filter(user_id=request.user, enable_login=True)
page_obj = PageNumber()
info_serializer = page_obj.paginate_queryset(queryset=wx_obj_lists.order_by("-subscribe_time"),
request=request,
@ -243,7 +249,7 @@ class ThirdWxAccount(APIView):
openid = data.get("openid")
if get_login_type().get('third', '').get('wxp') and openid:
if request.user.check_password(data.get('confirm_pwd', '')):
ThirdWeChatUserInfo.objects.filter(user_id=request.user, openid=openid).delete()
ThirdWeChatUserInfo.objects.filter(user_id=request.user, openid=openid).update(enable_login=False)
else:
res = BaseResponse()
res.code = 1001

@ -13,11 +13,11 @@ logger = logging.getLogger(__file__)
def common_exception_handler(exc, context):
logger.error(f'{context["view"].__class__.__name__} ERROR: {exc}')
# context['view'] 是TextView的对象,想拿出这个对象对应的类名
ret = exception_handler(exc, context) # 是Response对象,它内部有个data
logger.error(f'{context["view"].__class__.__name__} ERROR: {exc} ret:{ret}')
if not ret: # drf内置处理不了,丢给django 的,我们自己来处理
return ApiResponse(msg='error', result=str(exc))
return ApiResponse(msg='error', result=str(exc), code=500)
else:
return ApiResponse(msg='error', status=ret.status_code, **ret.data, )

@ -502,6 +502,36 @@ class WxTemplateMsg(object):
}
return self.send_msg(msg_id, content_data)
def operate_failed_msg(self, first_name, operate_context, failed_msg, operate_time, description):
msg_id = 'Hnrk5iXRjbaCTVpSIyC5KC8cwFNDgplNUzPsnyDXRLo'
content_data = {
"first": {
"value": f'你好,“{self.wx_nick_name}“,操作失败了',
"color": "#173177"
},
"keyword1": {
"value": first_name,
"color": "#173177"
},
"keyword2": {
"value": operate_context,
"color": "#173177"
},
"keyword3": {
"value": failed_msg,
"color": "#173177"
},
"keyword4": {
"value": operate_time,
"color": "#173177"
},
"remark": {
"value": f"{description},感谢您的关注",
"color": "#173177"
},
}
return self.send_msg(msg_id, content_data)
class WxWebLogin(object):
"""

@ -0,0 +1,78 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: fir_ser
# filename: notify
# author: liuyu
# data: 2022/3/26
import logging
from api.utils.modelutils import get_notify_wx_queryset
from common.base.baseutils import get_format_time
from common.core.sysconfig import Config
from common.libs.mp.wechat import WxTemplateMsg
from common.notify.utils import notify_by_email
logger = logging.getLogger(__name__)
def pay_success_notify(user_obj, order_obj):
"""
4, '充值到账提醒'
:param user_obj:
:param order_obj:
:return:
"""
message_type = 4
title = f'{order_obj.actual_download_times} 下载次数'
if order_obj.actual_download_gift_times > 0:
title = f'{title} 【赠送 {order_obj.actual_download_gift_times}'
msg = f"用户 {user_obj.first_name} 您好,{order_obj.description}。您购买了 {title}。感谢有你!"
for wx_user_obj in get_notify_wx_queryset(user_obj, message_type):
res = WxTemplateMsg(wx_user_obj.openid, wx_user_obj.nickname).pay_success_msg(
title,
f'{str(order_obj.actual_amount / 100)}',
order_obj.get_payment_type_display(),
order_obj.pay_time.strftime("%Y/%m/%d %H:%M:%S"),
order_obj.order_number, order_obj.description)
logger.info(f'user_obj {user_obj} weixin notify pay success result: {res}')
notify_by_email(user_obj, message_type, msg)
def sign_failed_notify(user_obj, developer_obj, app_obj):
"""
3, '应用签名失败'
:return:
"""
message_type = 3
now_time = get_format_time().replace('_', ' ')
msg = Config.MSG_ERROR_DEVELOPER % (
developer_obj.user_id.first_name, app_obj.name,
now_time, developer_obj.issuer_id)
for wx_user_obj in get_notify_wx_queryset(user_obj, message_type):
res = WxTemplateMsg(wx_user_obj.openid, wx_user_obj.nickname).operate_failed_msg(
user_obj.first_name, f'应用 {app_obj.name} 签名失败了',
f'开发者{developer_obj.issuer_id} 状态 {developer_obj.get_status_dispaly()}', now_time, '请登录后台查看具体信息')
logger.info(f'user_obj {user_obj} weixin notify pay success result: {res}')
notify_by_email(user_obj, message_type, msg)
def sign_unavailable_developer(user_obj, app_obj):
"""
3, '应用签名失败'
:return:
"""
message_type = 3
now_time = get_format_time().replace('_', ' ')
msg = Config.MSG_NOT_EXIST_DEVELOPER % (user_obj.first_name, now_time, app_obj.name)
for wx_user_obj in get_notify_wx_queryset(user_obj, message_type):
res = WxTemplateMsg(wx_user_obj.openid, wx_user_obj.nickname).operate_failed_msg(
user_obj.first_name, f'应用 {app_obj.name} 签名失败了',
f'苹果开发者总设备量已经超限', now_time, '添加新的苹果开发者或者修改开发者设备数量')
logger.info(f'user_obj {user_obj} weixin notify pay success result: {res}')
notify_by_email(user_obj, message_type, msg)

@ -0,0 +1,131 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: fir_ser
# filename: ntasks
# author: liuyu
# data: 2022/3/26
import datetime
import logging
from api.utils.modelutils import get_notify_wx_queryset
from common.base.magic import magic_wrapper, magic_notify
from common.cache.storage import NotifyLoopCache
from common.core.sysconfig import Config
from common.libs.mp.wechat import WxTemplateMsg
from common.notify.utils import notify_by_email
from xsign.models import AppIOSDeveloperInfo
from xsign.utils.modelutils import get_developer_devices
logger = logging.getLogger(__name__)
def download_times_not_enough(user_obj):
"""
1, '下载次数不足'
:param user_obj:
:return:
"""
message_type = 1
msg = f"您当前账户下载次数仅剩 {user_obj.download_times},已超过您设置的阈值 {user_obj.notify_available_downloads},为了避免业务使用,望您尽快充值!"
for wx_user_obj in get_notify_wx_queryset(user_obj, message_type):
res = WxTemplateMsg(wx_user_obj.openid, wx_user_obj.nickname).download_times_not_enough_msg(
user_obj.first_name, user_obj.download_times, msg)
logger.info(f'user_obj {user_obj} download times not enough result: {res}')
notify_by_email(user_obj, message_type, msg)
def apple_developer_devices_not_enough(user_obj, device_count):
"""
0, '签名余额不足'
:param user_obj:
:return:
"""
message_type = 0
msg = f"您当前账户超级签名可用设备仅剩 {device_count},已超过您设置的阈值 {user_obj.notify_available_signs},为了避免业务使用,望您尽快添加苹果开发者!"
for wx_user_obj in get_notify_wx_queryset(user_obj, message_type):
res = WxTemplateMsg(wx_user_obj.openid, wx_user_obj.nickname).apple_developer_devices_not_enough_msg(
user_obj.first_name, device_count, msg)
logger.info(f'user_obj {user_obj} sign devices not enough result: {res}')
notify_by_email(user_obj, message_type, msg)
def apple_developer_cert_expired(user_obj, developer_queryset):
"""
6, '证书到期消息'
:param developer_queryset:
:param user_obj:
:return:
"""
message_type = 6
developer_count = developer_queryset.count()
developer_obj = developer_queryset.first()
expired_time = developer_obj.cert_expire_time.strftime("%Y年%m月%d")
if developer_count == 1:
issuer_id = developer_obj.issuer_id
cert_id = developer_obj.certid
msg = f"用户 {user_obj.first_name} 您好,您苹果开发者 {issuer_id} ,证书 {cert_id} 即将到期,到期时间 {expired_time},为了保证您开发者可用,请您尽快更新开发者证书,感谢您的关注"
else:
issuer_id = f'{developer_obj.issuer_id}{developer_count} 个开发者ID'
cert_id = f'{developer_obj.certid}{developer_count} 个证书ID'
msg = f"用户 {user_obj.first_name} 您好,您苹果开发者 {issuer_id} ,证书 {cert_id} 即将到期,到期时间 {expired_time},为了保证您开发者可用,请您尽快更新开发者证书,感谢您的关注 "
for wx_user_obj in get_notify_wx_queryset(user_obj, message_type):
res = WxTemplateMsg(wx_user_obj.openid, wx_user_obj.nickname).cert_expired_msg(issuer_id,
cert_id,
expired_time)
logger.info(f'user_obj {user_obj} apple developer cert expired result: {res}')
notify_by_email(user_obj, message_type, msg)
def check_user_download_times(user_obj, days=None):
if days is None:
days = [0, 3, 7]
if user_obj.notify_available_downloads == 0 or user_obj.notify_available_downloads < user_obj.download_times:
return
notify_rules = [
{
'func': magic_wrapper(lambda obj: obj.download_times < obj.notify_available_downloads, user_obj),
'notify': days,
'cache': NotifyLoopCache(user_obj.uid, 'download_times'),
'notify_func': [magic_wrapper(download_times_not_enough, user_obj)]
}
]
magic_notify(notify_rules)
def check_apple_developer_devices(user_obj, days=None):
if days is None:
days = [0, 3, 7]
developer_used_info = get_developer_devices(AppIOSDeveloperInfo.objects.filter(user_id=user_obj))
device_count = developer_used_info.get('can_sign_number', 0)
if user_obj.notify_available_signs == 0 or user_obj.notify_available_signs < device_count:
return
notify_rules = [
{
'func': magic_wrapper(lambda obj: device_count < obj.notify_available_downloads, user_obj),
'notify': days,
'cache': NotifyLoopCache(user_obj.uid, 'sign_device_times'),
'notify_func': [magic_wrapper(apple_developer_devices_not_enough, user_obj, device_count)]
}
]
magic_notify(notify_rules)
def check_apple_developer_cert(user_obj, expire_day=7):
expire_time = datetime.datetime.now() + datetime.timedelta(days=expire_day)
developer_queryset = AppIOSDeveloperInfo.objects.filter(user_id=user_obj, status__in=Config.DEVELOPER_USE_STATUS,
cert_expire_time__lte=expire_time).order_by(
'cert_expire_time')
if developer_queryset.count() == 0:
return
notify_rules = [
{
'func': magic_wrapper(lambda obj: obj.download_times < obj.notify_available_downloads, user_obj),
'notify': [0, 3, 7],
'cache': NotifyLoopCache(user_obj.uid, 'developer_cert'),
'notify_func': [magic_wrapper(apple_developer_cert_expired, user_obj, developer_queryset)]
}
]
magic_notify(notify_rules)

@ -0,0 +1,19 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# project: fir_ser
# filename: wx
# author: liuyu
# data: 2022/3/23
import logging
from api.utils.modelutils import get_notify_email_queryset
from common.utils.sendmsg import get_sender_email_token
logger = logging.getLogger(__name__)
def notify_by_email(user_obj, message_type, msg):
for notify_email in get_notify_email_queryset(user_obj, message_type):
email = notify_email.get('email')
if email:
get_sender_email_token('email', email, 'msg', f'您好,{user_obj.first_name}{msg}')

@ -20,6 +20,7 @@ from common.cache.storage import AppDownloadTodayTimesCache, AppDownloadTimesCac
UploadTmpFileNameCache, RedisCacheBase, UserCanDownloadCache, UserFreeDownloadTimesCache, WxTicketCache, \
SignUdidQueueCache, CloudStorageCache
from common.core.sysconfig import Config
from common.notify.notify import pay_success_notify
from fir_ser.settings import CACHE_KEY_TEMPLATE, SYNC_CACHE_TO_DATABASE
logger = logging.getLogger(__name__)
@ -289,6 +290,7 @@ def update_order_info(user_id, out_trade_no, payment_number, payment_type, descr
"description"])
add_user_download_times(user_id, download_times)
logger.info(f"{user_obj} 订单 {out_trade_no} msg:{order_obj.description}")
pay_success_notify(user_obj, order_obj)
return True
except Exception as e:
logger.error(f"{user_obj} 订单 {out_trade_no} 更新失败 Exception:{e}")

@ -341,8 +341,8 @@ bIX1aWjPxirQX9mzaL3oEQI=
class MSGCONF(object):
MSG_NOT_EXIST_DEVELOPER = '用户 %s 你好,应用 %s 签名失败了,苹果开发者总设备量已经超限,请添加新的苹果开发者或者修改开发者设备数量。感谢有你!'
MSG_ERROR_DEVELOPER = '用户 %s 你好,应用 %s 签名失败了,苹果开发者 %s 信息异常,请重新检查苹果开发者状态是否正常。感谢有你!'
MSG_NOT_EXIST_DEVELOPER = '用户 %s 你好,应用 %s %s 签名失败了,苹果开发者总设备量已经超限,请添加新的苹果开发者或者修改开发者设备数量。感谢有你!'
MSG_ERROR_DEVELOPER = '用户 %s 你好,应用 %s %s 签名失败了,苹果开发者 %s 信息异常,请重新检查苹果开发者状态是否正常。感谢有你!'
MSG_AUTO_CHECK_DEVELOPER = '用户 %s 你好,苹果开发者 %s 信息异常,请重新检查苹果开发者状态是否正常。感谢有你!'

@ -468,6 +468,24 @@ CELERY_BEAT_SCHEDULE = {
'schedule': crontab(hour=1, minute=1),
'args': ()
},
'download_times_notify_check_job': {
'task': 'api.tasks.download_times_notify_check_job',
# 'schedule': SYNC_CACHE_TO_DATABASE.get("auto_check_ios_developer_active_times"),
'schedule': crontab(hour=1, minute=10),
'args': ()
},
'apple_developer_devices_check_job': {
'task': 'api.tasks.apple_developer_devices_check_job',
# 'schedule': SYNC_CACHE_TO_DATABASE.get("auto_check_ios_developer_active_times"),
'schedule': crontab(hour=1, minute=20),
'args': ()
},
'apple_developer_cert_notify_check_job': {
'task': 'api.tasks.apple_developer_cert_notify_check_job',
# 'schedule': SYNC_CACHE_TO_DATABASE.get("auto_check_ios_developer_active_times"),
'schedule': crontab(hour=2, minute=1),
'args': ()
},
'sync_wx_access_token_job': {
'task': 'api.tasks.sync_wx_access_token_job',
'schedule': SYNC_CACHE_TO_DATABASE.get("wx_get_access_token_times"),

@ -22,6 +22,7 @@ from common.base.baseutils import file_format_path, delete_app_profile_file, get
from common.base.magic import run_function_by_locker, call_function_try_attempts, magic_wrapper
from common.cache.state import CleanErrorBundleIdSignDataState
from common.core.sysconfig import Config
from common.notify.notify import sign_failed_notify, sign_unavailable_developer
from common.utils.caches import del_cache_response_by_short, send_msg_over_limit, check_app_permission, \
consume_user_download_times_by_app_obj, add_udid_cache_queue, get_and_clean_udid_cache_queue
from common.utils.storage import Storage
@ -33,7 +34,7 @@ from xsign.utils.iossignapi import ResignApp, AppDeveloperApiV2
from xsign.utils.modelutils import get_ios_developer_public_num, check_ipa_is_latest_sign, \
update_or_create_developer_udid_info, check_uid_has_relevant, get_developer_udided
from xsign.utils.serializer import BillAppInfoSerializer, BillDeveloperInfoSerializer
from xsign.utils.utils import delete_app_to_dev_and_file, send_ios_developer_active_status
from xsign.utils.utils import delete_app_to_dev_and_file
logger = logging.getLogger(__name__)
@ -217,10 +218,7 @@ def disable_developer_and_send_email(app_obj, developer_obj):
logger.error(f"app {app_obj} developer {developer_obj} sign failed. so disabled")
developer_obj.status = 5
developer_obj.save(update_fields=['status'])
send_ios_developer_active_status(developer_obj.user_id,
Config.MSG_ERROR_DEVELOPER % (
developer_obj.user_id.first_name, app_obj.name,
developer_obj.issuer_id))
sign_failed_notify(developer_obj.user_id, developer_obj, app_obj)
def get_new_developer_by_app_obj(app_obj, obj_base_filter, apple_to_app=False):
@ -416,9 +414,8 @@ class IosUtils(object):
if self.user_obj.email:
if send_msg_over_limit("get", self.user_obj.email):
send_msg_over_limit("set", self.user_obj.email)
send_ios_developer_active_status(self.user_obj, Config.MSG_NOT_EXIST_DEVELOPER
% (
self.user_obj.first_name, self.app_obj.name))
sign_unavailable_developer(self.user_obj, self.app_obj)
else:
logger.error(f"user {self.user_obj} send msg failed. over limit")

Loading…
Cancel
Save