用户下载页私有配置功能

ossnew 6.2.0
isummer 2 years ago
parent d1a5d69ac8
commit d8b124c1a7
  1. 2
      fir_admin/src/views/appinfos/list.vue
  2. 3
      fir_client/src/components/FirHeader.vue
  3. 47
      fir_client/src/components/base/BindDomain.vue
  4. 82
      fir_client/src/components/user/FirUserDomain.vue
  5. 470
      fir_client/src/components/user/FirUserDownload.vue
  6. 11
      fir_client/src/restful/index.js
  7. 5
      fir_client/src/router/index.js
  8. 32
      fir_ser/api/migrations/0007_auto_20220725_1612.py
  9. 10
      fir_ser/api/models.py
  10. 3
      fir_ser/api/urls.py
  11. 7
      fir_ser/api/utils/modelutils.py
  12. 11
      fir_ser/api/utils/serializer.py
  13. 122
      fir_ser/api/views/domain.py
  14. 15
      fir_ser/api/views/personalconfig.py
  15. 20
      fir_ser/common/base/baseutils.py
  16. 24
      fir_ser/common/core/sysconfig.py
  17. 9
      fir_ser/config.py
  18. 1
      fir_ser/fir_ser/settings.py

@ -221,7 +221,7 @@ export default {
type: 'warning'
}).then(() => {
this.listLoading = true
deleteApp(app_id).then(response => {
deleteApp(app_id).then(() => {
this.$message.success('删除成功')
this.fetchData()
this.listLoading = false

@ -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="download">下载配置</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>
@ -135,6 +136,8 @@ export default {
} else if (command === 'setdomian') {
this.$router.push({"name": 'FirUserDomain'})
} else if (command === 'download') {
this.$router.push({"name": 'FirUserDownload'})
} else if (command === 'setadvert') {
this.$router.push({"name": 'FirUserAdvert'})
} else if (command === 'myorder') {

@ -21,6 +21,7 @@
</div>
请联系域名管理员前往 <strong>{{ domain_name }}</strong> 域名 DNS 管理后台添加如下 CNAME 记录
<el-table
v-loading="is_loading"
:data="domain_tData"
border
stripe
@ -51,6 +52,11 @@
prop="dns"
width="300">
<template slot-scope="scope">
<el-tooltip content="该记录值为私有下载服务器">
<el-tag v-if="scope.row.is_private" style="margin-right: 5px" type="small">Private</el-tag>
</el-tooltip>
<el-tooltip content="点击复制到剪贴板">
<el-link v-if="scope.row.dns" v-clipboard:copy="scope.row.dns"
v-clipboard:success="copy_success"
@ -80,7 +86,7 @@
<el-col :span="6">
<el-button plain size="small" style="margin-top: 8px" type="danger"
@click="remove_domain">
解除绑定
解除绑定并删除记录
</el-button>
</el-col>
</el-row>
@ -98,13 +104,14 @@
<el-col :span="6">
<el-button plain size="small" style="margin-top: 8px" type="danger"
@click="remove_domain">
解除绑定
解除绑定并删除记录
</el-button>
</el-col>
</el-row>
</div>
<el-table
v-loading="is_loading"
:data="domain_tData"
border
stripe
@ -126,10 +133,23 @@
label="记录值"
prop="dns"
width="300">
<template slot-scope="scope">
<el-tooltip content="该记录值为私有下载服务器">
<el-tag v-if="scope.row.is_private" style="margin-right: 5px" type="small">Private</el-tag>
</el-tooltip>
<el-tooltip content="点击复制到剪贴板">
<el-link v-if="scope.row.dns" v-clipboard:copy="scope.row.dns"
v-clipboard:success="copy_success"
:underline="false">{{ scope.row.dns }}
</el-link>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div v-if="!bind_status" style="text-align: center;margin: 30px 0">
<el-button plain type="success" @click="check_cname">已经修改配置再次检查绑定</el-button>
<el-button :disabled="is_loading" plain type="success" @click="check_cname">已经修改配置再次检查绑定</el-button>
</div>
</div>
@ -139,8 +159,8 @@
<el-button :disabled="bind_status|| active===1 "
@click="last">上一步
</el-button>
<el-button v-if="force_bind" @click="next(1)">强制绑定</el-button>
<el-button v-else @click="next(0)">下一步</el-button>
<el-button v-if="force_bind" :disabled="is_loading" @click="next(1)">强制绑定</el-button>
<el-button v-else :disabled="is_loading" @click="next(0)">下一步</el-button>
</div>
@ -173,17 +193,18 @@ export default {
c_domain_name: {
type: String,
default: ''
},
}
},
data() {
return {
active: 1,
bind_status: false,
bind_domain_sure: true,
domain_tData: [{'type': 'CNAME', 'host': 'xxx', 'dns': 'demo.xxx.cn'}],
domain_tData: [{'type': 'CNAME', 'host': 'xxx', 'dns': 'demo.xxx.cn', 'is_private': false}],
domain_name: '',
force_bind: false,
b_t_msg: '您的账户',
is_loading: false
}
},
mounted() {
@ -203,10 +224,13 @@ export default {
this.$message.success('复制剪切板成功');
},
check_cname() {
this.is_loading = true
domainFun(data => {
this.is_loading = false
if (data.code === 1000) {
if (this.active++ > 2) this.active = 3;
this.bind_status = true;
this.$message.success("绑定成功")
if (!this.app_id) {
if (this.domain_type === 1) {
this.$store.dispatch("dodomainshow", false);
@ -258,7 +282,7 @@ export default {
this.domain_name = data.data.domain_name;
}
if (data.data.domain_record) {
this.format_domain_tData(data.data.domain_record);
this.format_domain_tData(data.data);
if (this.active++ > 2) this.active = 3;
}
if (data.data.is_enable) {
@ -281,14 +305,15 @@ export default {
domain_name_list.splice(domain_name_list.length - 2, 2);
this.domain_tData[0].host = domain_name_list.join(".")
}
this.domain_tData[0].dns = cname_domain;
this.domain_tData[0].dns = cname_domain.domain_record;
this.domain_tData[0].is_private = cname_domain.is_private;
},
next(force_bind) {
if (this.active === 1) {
domainFun(data => {
if (data.code === 1000) {
if (data.data && data.data.cname_domain) {
this.format_domain_tData(data.data.cname_domain);
if (data.data && data.data.domain_record) {
this.format_domain_tData(data.data);
if (this.active++ > 2) this.active = 3;
}
} else {

@ -11,47 +11,6 @@
:domain_state="true" :domain_type="current_domain_info.domain_type"
transitionName="bind-app-domain"/>
</el-dialog>
<el-dialog
:close-on-click-modal="false"
:close-on-press-escape="false"
:visible.sync="configVisible"
center
title="下载页部署配置"
width="666px">
<el-card v-for="info in config_lists" :key="info.key" class="box-card" shadow="hover"
style="margin-bottom: 10px">
<div slot="header" class="clearfix">
<span><el-tag size="medium" type="info">配置KEY</el-tag> <el-tag size="medium">{{ info.key }}</el-tag></span>
<div style="float: right">
<el-switch
v-model="info.value"
active-color="#13ce66"
active-text="启用"
active-value="true"
inactive-color="#ff4949"
inactive-text="关闭"
inactive-value="false"
@change="changeConfig(info)">
</el-switch>
</div>
</div>
<el-tag size="medium" type="info">描述信息</el-tag>
{{ info.title }}
<div v-if="short_download_uri" style="margin-top: 20px">
<el-tag size="medium" type="success">下载页部署源码及操作文档</el-tag>&nbsp;&nbsp;&nbsp;&nbsp;
<el-link :href="short_download_uri" target="_blank">点击下载</el-link>
</div>
</el-card>
<div slot="footer" class="dialog-footer">
<el-button @click="updateConfig">恢复默认值</el-button>
<el-button @click="configVisible=false">取消</el-button>
</div>
</el-dialog>
<div>
<el-input
v-model="search_key"
@ -62,11 +21,6 @@
搜索
</el-button>
<div style="float: right">
<el-tooltip content="下载页配置,可以定制部署私有下载页">
<el-button plain type="primary" @click="configFun">
下载页配置
</el-button>
</el-tooltip>
<el-tooltip content="应用安装下载页,多个下载页域名可以避免域名被封导致其他应用也无法访问">
<el-button plain type="primary" @click="$store.dispatch('dodomainaction', 1)">
添加下载页域名
@ -94,6 +48,9 @@
label="绑定域名"
prop="domain_name">
<template slot-scope="scope">
<el-tooltip content="该域名解析到了私有下载服务器上面">
<el-tag v-if="scope.row.is_private" style="margin-right: 5px" type="small">Private</el-tag>
</el-tooltip>
<el-tooltip content="点击复制到剪贴板">
<el-link v-if="scope.row.domain_name" v-clipboard:copy="scope.row.domain_name"
v-clipboard:success="copy_success"
@ -123,6 +80,28 @@
</template>
</el-table-column>
<el-table-column
align="center"
label="开启https"
prop="is_https"
width="110">
<template slot-scope="scope">
<el-tooltip content="点击关闭" placement="top">
<div slot="content">
<span v-if="scope.row.is_https">
点击关闭 HTTPS 访问
</span>
<span v-else>
点击开启支持 HTTPS 访问
</span>
</div>
<el-button v-if="scope.row.is_https" size="small" type="success"
@click="changeHttpsFun(scope.row, false)">已开启
</el-button>
<el-button v-else size="small" type="info" @click="changeHttpsFun(scope.row, true)">未开启</el-button>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
@ -147,7 +126,6 @@
</template>
</el-table-column>
<el-table-column
align="center"
label="跳转权重"
@ -224,6 +202,16 @@ export default {
}
},
methods: {
changeHttpsFun(domain_info, is_https) {
domain_info['is_https'] = is_https
domaininfo(data => {
if (data.code === 1000) {
this.$message.success("https支持修改成功")
} else {
this.$message.error("修改失败 " + data.msg)
}
}, {methods: 'PUT', data: domain_info})
},
configFun() {
personalConfigInfo(data => {
if (data.code === 1000) {

@ -0,0 +1,470 @@
<template>
<el-main>
<el-dialog
:close-on-click-modal="false"
:close-on-press-escape="false"
:visible.sync="configVisible"
center
title="下载页部署配置"
width="666px">
<el-card v-for="info in config_lists" :key="info.key" class="box-card" shadow="hover"
style="margin-bottom: 20px">
<div slot="header" class="clearfix">
<span><el-tag size="medium" type="info">配置KEY</el-tag> <el-tag size="medium">{{ info.key }}</el-tag></span>
<div style="float: right">
<el-switch
v-model="info.value"
active-color="#13ce66"
active-text="启用"
active-value="true"
inactive-color="#ff4949"
inactive-text="关闭"
inactive-value="false"
@change="changeConfig(info)">
</el-switch>
</div>
</div>
<el-tag size="medium" type="info">描述信息</el-tag>
{{ info.title }}
</el-card>
<el-divider></el-divider>
<el-card style="margin-top: 20px">
<div slot="header" class="clearfix">
<el-tag size="medium" type="success">下载页源码及操作文档</el-tag>
</div>
<div v-if="short_download_list.length>0">
<div style="margin: 10px" v-for="short_download_uri in short_download_list" :key="short_download_uri.key">
<el-tag size="medium" type="info">{{short_download_uri.title}}</el-tag>
<el-link style="margin-left: 10px" :href="short_download_uri.value" target="_blank">点击下载</el-link>
</div>
</div>
</el-card>
<div slot="footer" class="dialog-footer">
<el-button @click="updateConfig">恢复默认值</el-button>
<el-button @click="configVisible=false">取消</el-button>
</div>
</el-dialog>
<el-dialog
:close-on-click-modal="false"
:close-on-press-escape="false"
:visible.sync="addCnameVisible"
center
title="添加下载页服务器域名"
width="800px">
<el-steps :active="p_active" finish-status="success" :align-center="true">
<el-step title="添加服务器域名"></el-step>
<el-step title="验证服务器域名所有权"></el-step>
<el-step title="添加成功"></el-step>
</el-steps>
<el-container>
<div v-if="p_active===0" style="margin-top: 20px">
<el-tag style="margin: 10px 5px">1.如果是cdn等含有CNAME直接添加cdn的 CNAME </el-tag>
<el-tag style="margin: 10px 5px">2.如果是服务器需要将域名解析到服务器然后添加该解析的域名</el-tag>
<div style="width: 600px;text-align: center;margin: auto">
<el-form ref="form" :model="addSerInfo" label-width="120px">
<el-form-item label="下载服务器域名">
<el-input v-model="addSerInfo.domain_record" placeholder="cdn的cname或者服务器的域名,必填"></el-input>
</el-form-item>
<el-form-item label="下载服务器地址">
<el-input v-model="addSerInfo.ip_address" placeholder="如果有服务器地址,添加服务器ip,否则填写服务器域名"></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input type="textarea" v-model="addSerInfo.description"></el-input>
</el-form-item>
</el-form>
<div style="margin-top: 20px;">
<el-button type="primary" @click="addCnameSer">下一步</el-button>
</div>
</div>
</div>
<div v-else-if="p_active===1" style="margin-top: 30px;text-align: center">
请联系域名管理员前往 <strong>{{ cnameInfo.domain_record }}</strong> 域名 DNS 管理后台添加如下 {{ cnameInfo.r_type }} 记录
<el-table
:data="domain_tData"
border
stripe
v-loading="loading"
style="width: 100%;margin-top: 20px">
<el-table-column
align="center"
label="记录类型"
prop="r_type"
width="100">
</el-table-column>
<el-table-column
align="center"
label="主机记录"
prop="host_r"
>
<template slot-scope="scope">
<el-tooltip content="点击复制到剪贴板">
<el-link v-if="scope.row.host_r" v-clipboard:copy="scope.row.host_r"
v-clipboard:success="copy_success"
:underline="false">{{ scope.row.host_r }}
</el-link>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
align="center"
label="记录值"
prop="cname_r"
width="300">
<template slot-scope="scope">
<el-tooltip content="点击复制到剪贴板">
<el-link v-if="scope.row.cname_r" v-clipboard:copy="scope.row.cname_r"
v-clipboard:success="copy_success"
:underline="false">{{ scope.row.cname_r }}
</el-link>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<el-alert :closable="false"
show-icon
style="margin-top: 30px"
title="请在域名DNS配置成功后,点击“下一步”按钮"
type="warning"/>
<div style="margin-top: 20px;text-align: center;">
<el-button type="primary" @click="p_active=0">上一步</el-button>
<el-button type="primary" @click="checkCnameSer" :disabled="loading">下一步</el-button>
</div>
</div>
</el-container>
</el-dialog>
<div>
<el-input
v-model="search_key"
clearable
placeholder="输入下载服务器域名"
style="width: 30%;margin-right: 30px;margin-bottom: 5px"/>
<el-button icon="el-icon-search" type="primary" @click="handleCurrentChange(1)">
搜索
</el-button>
<div style="float: right">
<el-tooltip content="下载页配置,可以定制部署私有下载页">
<el-button plain type="primary" @click="configFun">
下载页配置
</el-button>
</el-tooltip>
<el-button plain type="primary" @click="addDownloadSer">
添加下载页服务器
</el-button>
</div>
<el-table
v-loading="loading"
:data="cname_info_list"
border
stripe
style="width: 100%">
<el-table-column
align="center"
fixed
label="下载服务器域名"
prop="domain_name">
<template slot-scope="scope">
<el-tooltip content="点击复制到剪贴板">
<el-link v-if="scope.row.domain_record" v-clipboard:copy="scope.row.domain_record"
v-clipboard:success="copy_success"
:underline="false">{{ scope.row.domain_record }}
</el-link>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
align="center"
label="下载服务器地址"
prop="ip_address">
</el-table-column>
<el-table-column
align="center"
label="状态"
prop="is_enable"
width="110">
<template slot-scope="scope">
<el-tooltip v-if="scope.row.is_enable === true" content="已经绑定成功" placement="left-start">
<el-button size="small" type="success">成功
</el-button>
</el-tooltip>
<el-tooltip v-else content="点击激活服务器域名绑定信息" placement="left-start">
<el-button size="small" type="warning" @click="continueBind(scope.row)">继续绑定
</el-button>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
:formatter="format_create_time"
align="center"
label="域名绑定时间"
prop="created_time"
width="170"
>
</el-table-column>
<el-table-column
align="center"
label="备注"
prop="description">
</el-table-column>
<el-table-column
align="center"
label="操作"
prop="is_enable"
width="100">
<template slot-scope="scope">
<el-tooltip content="删除该下载页服务器" placement="left-start">
<el-button size="small" type="danger" @click="delCnameSer(scope.row)">删除
</el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<div style="margin-top: 20px;margin-bottom: 20px">
<el-pagination
:current-page.sync="pagination.currentPage"
:page-size="pagination.pagesize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total,sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange">
</el-pagination>
</div>
</el-main>
</template>
<script>
import {dCnameInfoFun, personalConfigInfo} from "@/restful";
import {getUserInfoFun} from '@/utils'
import {format_time} from "@/utils/base/utils";
export default {
name: "FirUserDomain",
data() {
return {
addSerInfo: {},
cnameInfo: {},
domain_tData: [],
p_active: 0,
cname_info_list: [],
search_key: "",
pagination: {"currentPage": 1, "total": 0, "pagesize": 10},
loading: false,
configVisible: false,
addCnameVisible: false,
config_lists: [],
short_download_list: []
}
},
methods: {
continueBind(cname_info) {
this.addSerInfo = cname_info
this.addCnameVisible = true
this.addCnameSer()
},
delCnameSer(cname_info) {
this.$confirm('若下载域名绑定到该解析,同时也会删除绑定的下载域名,是否继续删除?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
dCnameInfoFun(data => {
if (data.code === 1000) {
this.$message.success("删除成功")
this.get_data_from_tabname();
} else {
this.$message.error("操作失败了 " + data.msg)
}
}, {
methods: 'DELETE',
data: {'domain_record': cname_info.domain_record}
})
})
},
checkCnameSer() {
this.loading=true
dCnameInfoFun(data => {
this.loading=false
if (data.code === 1000) {
this.$message.success(this.addSerInfo.domain_record + " 添加成功")
this.get_data_from_tabname();
this.addDownloadSer()
this.addCnameVisible = false
} else {
this.$message.error("操作失败了 " + data.msg)
}
}, {
methods: 'PUT',
data: {'act': 'check', 'domain_record': this.addSerInfo.domain_record}
})
},
addCnameSer() {
let domain_record = this.addSerInfo.domain_record
let ip_address = this.addSerInfo.ip_address
if (domain_record && domain_record.length > 6 && ip_address && ip_address.length > 6) {
dCnameInfoFun(data => {
if (data.code === 1000) {
this.p_active = 1
this.cnameInfo = data.data
this.domain_tData = [this.cnameInfo]
} else {
this.$message.error("操作失败了 " + data.msg)
}
}, {
methods: 'POST',
data: this.addSerInfo
})
} else {
this.$message.error("参数有误")
}
},
addDownloadSer() {
this.addSerInfo = {}
this.cnameInfo = {}
this.p_active = 0
this.addCnameVisible = true
},
configFun() {
personalConfigInfo(data => {
if (data.code === 1000) {
this.short_download_list = data.data
} else {
this.$message.error("获取数据失败了 " + data.msg)
}
}, {
methods: 'GET'
}, 'short_download_uri')
personalConfigInfo(data => {
if (data.code === 1000) {
this.config_lists = data.data
this.configVisible = true
} else {
this.$message.error("获取数据失败了 " + data.msg)
}
}, {
methods: 'GET'
}, 'preview_route')
},
changeConfig(info) {
personalConfigInfo(data => {
if (data.code === 1000) {
this.$message.success("操作成功")
this.configFun()
} else {
this.$message.error("操作失败了 " + data.msg)
}
}, {
methods: 'PUT', data: {config_key: info.key, config_value: info.value}
}, 'preview_route')
},
updateConfig() {
personalConfigInfo(data => {
if (data.code === 1000) {
this.$message.success("操作成功")
this.configFun()
} else {
this.$message.error("操作失败了 " + data.msg)
}
}, {
methods: 'DELETE'
}, 'preview_route')
},
copy_success() {
this.$message.success('复制剪切板成功');
},
handleSizeChange(val) {
this.pagination.pagesize = val;
this.get_data_from_tabname({
"size": this.pagination.pagesize,
"page": 1
})
},
handleCurrentChange(val) {
this.pagination.currentPage = val;
this.get_data_from_tabname({
"size": this.pagination.pagesize,
"page": this.pagination.currentPage
})
},
get_data_from_tabname(data = {}) {
data.search_key = this.search_key.replace(/^\s+|\s+$/g, "");
this.UserCnameInfoFun(data)
},
UserCnameInfoFun(params) {
this.loading = true;
dCnameInfoFun(data => {
if (data.code === 1000) {
this.cname_info_list = data.data;
this.pagination.total = data.count;
} else {
this.$message.error("域名绑定信息获取失败")
}
this.loading = false;
}, {methods: 'GET', data: params})
},
format_create_time(row) {
return format_time(row.created_time)
},
}, mounted() {
getUserInfoFun(this);
this.get_data_from_tabname();
}
}
</script>
<style scoped>
.el-main {
margin: 20px auto 100px;
width: 1166px;
position: relative;
padding-bottom: 1px;
color: #9b9b9b;
-webkit-font-smoothing: antialiased;
border-radius: 1%;
}
</style>

@ -605,6 +605,17 @@ export function domainFun(callBack, params) {
);
}
/**用户添加下载页服务器*/
export function dCnameInfoFun(callBack, params) {
getData(
params.methods,
USERSEVER + '/cname_info',
params.data,
data => {
callBack(data);
}
);
}
/**微信用户绑定 */
export function wxutils(callBack, params) {

@ -168,6 +168,11 @@ const router = new VueRouter({
name: 'FirUserDomain',
meta: {label: '绑定域名详情'},
component: () => import("@/components/user/FirUserDomain"),
}, {
path: 'download',
name: 'FirUserDownload',
meta: {label: '下载页设置'},
component: () => import("@/components/user/FirUserDownload"),
},
{
path: 'advert',

@ -0,0 +1,32 @@
# Generated by Django 3.2.3 on 2022-07-25 16:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0006_auto_20220513_0843'),
]
operations = [
migrations.AlterField(
model_name='domaincnameinfo',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='是否启用该解析'),
),
migrations.AlterField(
model_name='domaincnameinfo',
name='domain_record',
field=models.CharField(max_length=128, verbose_name='记录值'),
),
migrations.AddField(
model_name='domaincnameinfo',
name='user_ipk',
field=models.IntegerField(default=0, verbose_name='用户ipk'),
),
migrations.AlterUniqueTogether(
name='domaincnameinfo',
unique_together={('user_ipk', 'domain_record')},
),
]

@ -63,7 +63,7 @@ class UserInfo(AbstractUser):
self.api_token = self.uid + generate_alphanumeric_token_of_length(64)
if not self.default_domain_name_id:
default_domain_obj = min(
DomainCnameInfo.objects.annotate(Count('userinfo')).filter(is_enable=True, is_system=True),
DomainCnameInfo.objects.annotate(Count('userinfo')).filter(is_enable=True, is_system=True, user_ipk=0),
key=lambda x: x.userinfo__count)
if default_domain_obj:
self.default_domain_name_id = default_domain_obj.pk
@ -378,9 +378,10 @@ class CertificationInfo(models.Model):
class DomainCnameInfo(models.Model):
domain_record = models.CharField(max_length=128, unique=True, verbose_name="记录值")
user_ipk = models.IntegerField(verbose_name="用户ipk", default=0)
domain_record = models.CharField(max_length=128, verbose_name="记录值")
ip_address = models.CharField(max_length=128, verbose_name="域名解析地址", null=False)
is_enable = models.BooleanField(default=True, verbose_name="是否启用该解析")
is_enable = models.BooleanField(default=False, verbose_name="是否启用该解析")
is_system = models.BooleanField(default=False, verbose_name="是否是系统自带解析")
is_https = models.BooleanField(default=False, verbose_name="是否支持HTTPS")
description = models.TextField(verbose_name='备注', blank=True, null=True, default='')
@ -389,9 +390,10 @@ class DomainCnameInfo(models.Model):
class Meta:
verbose_name = '系统分发域名配置'
verbose_name_plural = "系统分发域名配置"
unique_together = (('user_ipk', 'domain_record'),)
def save(self, *args, **kwargs):
if not self.domain_record or (self.domain_record and len(self.domain_record) < 26): # 最多3个启用的价格表
if self.user_ipk == 0 and (not self.domain_record or (self.domain_record and len(self.domain_record) < 26)):
self.domain_record = '%s.%s' % (generate_numeric_token_of_length(24, 'abcdef'), self.domain_record)
super(DomainCnameInfo, self).save(*args, **kwargs)

@ -17,7 +17,7 @@ from django.urls import re_path
from api.views.advert import UserAdInfoView
from api.views.apps import AppsView, AppInfoView, AppReleaseInfoView, AppsQrcodeShowView, AppDownloadTokenView
from api.views.domain import DomainCnameView, DomainInfoView
from api.views.domain import DomainCnameView, DomainInfoView, DomainCnameInfoView
from api.views.download import ShortDownloadView, InstallView, DownloadView
from api.views.getip import GetRemoteIp
from api.views.login import LoginView, UserInfoView, RegistView, AuthorizationView, ChangeAuthorizationView, \
@ -73,6 +73,7 @@ urlpatterns = [
re_path(r"^pay_success/(?P<name>\w+)$", PaySuccess.as_view()),
re_path("^cname_domain$", DomainCnameView.as_view()),
re_path("^domain_info$", DomainInfoView.as_view()),
re_path("^cname_info$", DomainCnameInfoView.as_view()),
re_path("^mp.weixin$", ValidWxChatToken.as_view()),
re_path("^mp.applet$", WeChatAppletView.as_view()),
re_path("^mp.web.login$", WeChatWebLoginView.as_view(), name="mp.web.login"),

@ -90,14 +90,15 @@ def get_app_download_uri(request, user_obj, app_obj=None, preview=True):
return get_server_domain_from_request(request, server_domain)
def get_min_default_domain_cname_obj(is_system=True):
def get_min_default_domain_cname_obj(is_system=True, user_ipk=0):
if is_system:
c_n = 'userinfo'
else:
c_n = 'userdomaininfo'
domain_queryset = DomainCnameInfo.objects.annotate(Count(c_n)).filter(is_enable=True, is_system=is_system)
domain_queryset = DomainCnameInfo.objects.annotate(Count(c_n)).filter(is_enable=True, is_system=is_system,
user_ipk=user_ipk)
if not domain_queryset:
return DomainCnameInfo.objects.filter(is_enable=True, is_system=True).first()
return DomainCnameInfo.objects.filter(is_enable=True, is_system=True, user_ipk=user_ipk).first()
return min(domain_queryset, key=lambda x: getattr(x, f'{c_n}__count'))

@ -508,6 +508,17 @@ class DomainNameSerializer(serializers.ModelSerializer):
return app_info
return {}
is_private = serializers.SerializerMethodField()
def get_is_private(self, obj):
return bool(obj.cname_id.user_ipk)
class DomainCnameInfoSerializer(serializers.ModelSerializer):
class Meta:
model = models.DomainCnameInfo
exclude = ["id", "user_ipk", "is_https", "is_system"]
class UserAdInfoSerializer(serializers.ModelSerializer):
class Meta:

@ -5,16 +5,20 @@
# date: 2021/3/29
import logging
from dns import rdatatype
from rest_framework.response import Response
from rest_framework.views import APIView
from api.models import UserDomainInfo, Apps
from api.models import UserDomainInfo, Apps, DomainCnameInfo
from api.utils.modelutils import get_user_domain_name, get_min_default_domain_cname_obj, PageNumber
from api.utils.response import BaseResponse
from api.utils.serializer import DomainNameSerializer
from common.base.baseutils import is_valid_domain, get_cname_from_domain, get_choices_dict
from api.utils.serializer import DomainNameSerializer, DomainCnameInfoSerializer
from common.base.baseutils import is_valid_domain, get_cname_from_domain, get_choices_dict, make_app_uuid, \
format_cname_host
from common.core.auth import ExpiringTokenAuthentication
from common.utils.caches import del_cache_response_by_short, reset_app_wx_easy_type
from common.core.sysconfig import UserConfig
from common.utils.caches import del_cache_response_by_short, reset_app_wx_easy_type, reset_short_response_cache
from fir_ser.settings import DOMAIN_CNAME_KEY
logger = logging.getLogger(__name__)
@ -54,9 +58,16 @@ def remove_domain_wx_easy(app_obj, user_obj):
def add_new_domain_info(res, request, domain_name, domain_type):
min_domain_cname_info_obj = get_min_default_domain_cname_obj(False)
user_ipk = 0
if UserConfig(request.user).PRIVATE_DOWNLOAD_PAGE:
user_ipk = request.user.pk
min_domain_cname_info_obj = get_min_default_domain_cname_obj(False, user_ipk)
if min_domain_cname_info_obj:
res.data = {'cname_domain': min_domain_cname_info_obj.domain_record}
res.data = {
'domain_record': min_domain_cname_info_obj.domain_record,
'is_private': bool(min_domain_cname_info_obj.user_ipk)
}
data_dict = {
'user_id': request.user,
'cname_id': min_domain_cname_info_obj,
@ -78,7 +89,7 @@ class DomainCnameView(APIView):
def get(self, request):
res = BaseResponse()
res.data = {'domain_name': '', 'domain_record': '', 'is_enable': False}
res.data = {'domain_name': '', 'domain_record': '', 'is_enable': False, 'is_private': False}
user_domain_obj = UserDomainInfo.objects.filter(**get_domain_filter(request)).last()
domain_type = request.query_params.get("domain_type", -1)
@ -91,6 +102,7 @@ class DomainCnameView(APIView):
res.data['is_enable'] = user_domain_obj.is_enable
if user_domain_obj.cname_id:
res.data['domain_record'] = user_domain_obj.cname_id.domain_record
res.data['is_private'] = bool(user_domain_obj.cname_id.user_ipk)
return Response(res.dict)
def post(self, request):
@ -126,7 +138,10 @@ class DomainCnameView(APIView):
kwargs['domain_name'] = domain_name
user_domain_obj = UserDomainInfo.objects.filter(**kwargs).first()
if user_domain_obj:
res.data = {'cname_domain': user_domain_obj.cname_id.domain_record}
res.data = {
'domain_record': user_domain_obj.cname_id.domain_record,
'is_private': bool(user_domain_obj.cname_id.user_ipk)
}
else:
kwargs.pop('domain_name')
UserDomainInfo.objects.filter(**kwargs, is_enable=False).delete()
@ -136,7 +151,10 @@ class DomainCnameView(APIView):
kwargs['domain_name'] = domain_name
user_domain_obj = UserDomainInfo.objects.filter(**kwargs).first()
if user_domain_obj:
res.data = {'cname_domain': user_domain_obj.cname_id.domain_record}
res.data = {
'domain_record': user_domain_obj.cname_id.domain_record,
'is_private': bool(user_domain_obj.cname_id.user_ipk)
}
else:
add_new_domain_info(res, request, domain_name, domain_type)
else:
@ -234,13 +252,97 @@ class DomainInfoView(APIView):
res = BaseResponse()
domain_name = request.data.get('domain_name', '')
weight = request.data.get('weight', 10)
is_https = request.data.get('is_https', 10)
domain_type = request.data.get('domain_type', None)
if domain_type is not None and weight and domain_name:
domain_name_obj = UserDomainInfo.objects.filter(user_id=request.user, domain_name=domain_name,
domain_type=domain_type).all()
if domain_name_obj and len(domain_name_obj) == 1:
domain_name_obj.update(weight=weight)
domain_name_obj.update(weight=weight, is_https=is_https)
reset_short_response_cache(request.user, None)
else:
res.code = 1002
res.msg = '参数有误'
return Response(res.dict)
class DomainCnameInfoView(APIView):
authentication_classes = [ExpiringTokenAuthentication, ]
def get(self, request):
res = BaseResponse()
search_key = request.query_params.get("search_key", None)
obj_lists = DomainCnameInfo.objects.filter(user_ipk=request.user.pk)
if search_key:
obj_lists = obj_lists.filter(domain_record=search_key)
page_obj = PageNumber()
domain_info_serializer = page_obj.paginate_queryset(
queryset=obj_lists.order_by("-created_time"), request=request, view=self)
domain_info = DomainCnameInfoSerializer(domain_info_serializer, many=True, )
res.data = domain_info.data
res.count = obj_lists.count()
return Response(res.dict)
def put(self, request):
res = BaseResponse()
domain_record = request.data.get('domain_record')
act = request.data.get('act')
if domain_record and act:
c_obj = DomainCnameInfo.objects.filter(user_ipk=request.user.pk, domain_record=domain_record).first()
if not c_obj:
res.code = 1003
res.msg = '参数有误'
return Response(res.dict)
if act == 'check':
resolve_cname = f"{request.user.uid}{make_app_uuid(request.user, domain_record)}"
auth_domain_record = f'{DOMAIN_CNAME_KEY}.{domain_record}'
if get_cname_from_domain(auth_domain_record, resolve_cname, rd_type=rdatatype.TXT):
c_obj.is_enable = True
c_obj.save(update_fields=['is_enable'])
return Response(res.dict)
else:
res.code = 1004
res.msg = 'TXT解析错误,请检查或者稍后再试'
else:
res.code = 1002
res.msg = '参数有误'
return Response(res.dict)
def post(self, request):
res = BaseResponse()
domain_info = request.data
domain_record = domain_info.get('domain_record')
ip_address = domain_info.get('ip_address')
description = domain_info.get('description')
if domain_record and ip_address:
c_obj = DomainCnameInfo.objects.filter(user_ipk=request.user.pk, domain_record=domain_record)
if not c_obj:
DomainCnameInfo.objects.create(user_ipk=request.user.pk, domain_record=domain_record,
ip_address=ip_address, description=description)
res.data = {
'a_type': 'DNS',
'r_type': 'TXT',
'host_r': f'{DOMAIN_CNAME_KEY}.{format_cname_host(domain_record)}',
'cname_r': f"{request.user.uid}{make_app_uuid(request.user, domain_record)}",
'domain_record': domain_record
}
else:
res.code = 1001
res.msg = "参数有误"
return Response(res.dict)
def delete(self, request):
res = BaseResponse()
domain_record = request.query_params.get('domain_record')
if domain_record:
c_query_set = DomainCnameInfo.objects.filter(user_ipk=request.user.pk, domain_record=domain_record)
if UserDomainInfo.objects.filter(user_id=request.user, cname_id=c_query_set.first(),
is_enable=True).count():
reset_short_response_cache(request.user, None)
c_query_set.delete()
if DomainCnameInfo.objects.filter(user_ipk=request.user.pk, is_enable=True).count() == 0:
UserConfig(request.user).del_value('PRIVATE_DOWNLOAD_PAGE')
return Response(res.dict)

@ -10,7 +10,7 @@ import logging
from rest_framework.response import Response
from rest_framework.views import APIView
from api.models import UserPersonalConfig
from api.models import UserPersonalConfig, DomainCnameInfo
from api.utils.response import BaseResponse
from api.utils.serializer import PersonalConfigSerializer
from common.core.auth import ExpiringTokenAuthentication, SuperSignPermission
@ -25,14 +25,21 @@ def get_personal_config(request):
if config_type == 'super_sign':
personal_config = Config.DEVELOPER_STATUS_CONFIG
elif config_type == 'preview_route':
personal_config = ['PREVIEW_ROUTE_HASH']
personal_config = ['PRIVATE_DOWNLOAD_PAGE', 'PREVIEW_ROUTE_HASH']
elif config_type == 'short_download_uri':
personal_config = ['DOWNLOAD_DEPLOYMENT_URL']
personal_config = ['DOWNLOAD_DEPLOYMENT_HISTORY_URL', 'DOWNLOAD_DEPLOYMENT_HASH_URL']
else:
personal_config = []
return personal_config
def check_config_auth(request, config_key, config_value):
if config_key == 'PRIVATE_DOWNLOAD_PAGE':
if not DomainCnameInfo.objects.filter(user_ipk=request.user.pk, is_enable=True).count():
return False
return True
class PersonalConfigView(APIView):
authentication_classes = [ExpiringTokenAuthentication, ]
permission_classes = [SuperSignPermission, ]
@ -60,7 +67,9 @@ class PersonalConfigView(APIView):
res = BaseResponse()
config_key = request.data.get("config_key", None)
config_value = request.data.get("config_value", None)
if config_key is not None and config_value is not None and config_key in get_personal_config(request):
if check_config_auth(request, config_key, config_value):
UserPersonalConfig.objects.filter(user_id=request.user, key=config_key).update(value=config_value)
UserConfig(request.user).invalid_config_cache(config_key)
return Response(res.dict)

@ -19,7 +19,7 @@ from Crypto.Cipher import AES
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.utils import timezone
from dns.resolver import Resolver
from dns import resolver, rdatatype
from fir_ser.settings import SUPER_SIGN_ROOT
@ -218,19 +218,21 @@ def format_storage_selection(storage_info_list, storage_choice_list):
return storage_choice_list
def get_cname_from_domain(domain, resolve_cname):
def get_cname_from_domain(domain, resolve_cname, rd_type=rdatatype.CNAME):
dns_list = [
["119.29.29.29", "114.114.114.114"],
["223.5.5.5", "223.6.6.6"],
["8.8.8.8", "8.8.4.4"],
]
dns_resolver = Resolver()
dns_resolver = resolver.Resolver()
domain = domain.lower().strip()
count = 3
while count:
try:
dns_resolver.nameservers = dns_list[len(dns_list) - count]
if dns_resolver.resolve(domain, 'CNAME')[0].to_text() == resolve_cname:
for ans in dns_resolver.resolve(domain, rd_type):
logger.info(f"dns {dns_resolver.nameservers} resolve {domain} answer: {ans.to_text()}")
if ans.to_text().strip("'").strip('"') == resolve_cname.strip("'").strip('"'):
return True
except Exception as e:
logger.error(f"dns {dns_resolver.nameservers} resolve {domain} failed Exception:{e}")
@ -373,3 +375,13 @@ def make_resigned(bin_url, img_url, bundle_id, app_version, name):
logger.info(
f"make_resigned bin_url {bin_url} ,img_url {img_url}, bundle_id {bundle_id}, app_version {app_version}, name {name}")
return ios_plist_tem
def format_cname_host(c_name: str):
cname_list = c_name.split('.')
cname_len = len(cname_list)
if cname_len == 2:
return ''
elif cname_len > 2:
return '.'.join(cname_list[0:-2])
return ''

@ -158,8 +158,20 @@ class DomainConfCache(ConfigCacheBase):
return self.get_value('WEB_DOMAIN', DOMAINCONF.WEB_DOMAIN)
@property
def DOWNLOAD_DEPLOYMENT_URL(self):
return self.get_value('DOWNLOAD_DEPLOYMENT_URL', DOMAINCONF.DOWNLOAD_DEPLOYMENT_URL)
def DOWNLOAD_DEPLOYMENT_HASH_URL(self):
return self.get_value('DOWNLOAD_DEPLOYMENT_HASH_URL', DOMAINCONF.DOWNLOAD_DEPLOYMENT_HASH_URL)
@property
def DOWNLOAD_DEPLOYMENT_HISTORY_URL(self):
return self.get_value('DOWNLOAD_DEPLOYMENT_HISTORY_URL', DOMAINCONF.DOWNLOAD_DEPLOYMENT_HISTORY_URL)
@property
def DOWNLOAD_DEPLOYMENT_HASH_URL_DES(self):
return self.get_value('DOWNLOAD_DEPLOYMENT_HASH_URL_DES', DOMAINCONF.DOWNLOAD_DEPLOYMENT_HASH_URL_DES)
@property
def DOWNLOAD_DEPLOYMENT_HISTORY_URL_DES(self):
return self.get_value('DOWNLOAD_DEPLOYMENT_HISTORY_URL_DES', DOMAINCONF.DOWNLOAD_DEPLOYMENT_HISTORY_URL_DES)
class BaseConfCache(ConfigCacheBase):
@ -457,6 +469,14 @@ class UserPersonalConfKeyCache(ConfigCacheBase):
def PREVIEW_ROUTE_HASH_DES(self):
return super().get_value('PREVIEW_ROUTE_HASH_DES', USERPERSONALCONFIGKEY.PREVIEW_ROUTE_HASH_DES)
@property
def PRIVATE_DOWNLOAD_PAGE(self):
return super().get_value('PRIVATE_DOWNLOAD_PAGE', USERPERSONALCONFIGKEY.PRIVATE_DOWNLOAD_PAGE)
@property
def PRIVATE_DOWNLOAD_PAGE_DES(self):
return super().get_value('PRIVATE_DOWNLOAD_PAGE_DES', USERPERSONALCONFIGKEY.PRIVATE_DOWNLOAD_PAGE_DES)
class ConfigDescriptionCache(ConfigCacheBase):
def __init__(self, *args, **kwargs):

@ -14,7 +14,10 @@ class DOMAINCONF(object):
API_DOMAIN = "https://app.hehelucky.cn"
WEB_DOMAIN = "https://app.hehelucky.cn"
MOBILEPROVISION = "https://static.flyapps.top/embedded2.mobileprovision"
DOWNLOAD_DEPLOYMENT_URL = "https://static.flyapps.top/download.v63.23.tar.gz"
DOWNLOAD_DEPLOYMENT_HASH_URL = "https://static.flyapps.top/download.v63.23.tar.gz"
DOWNLOAD_DEPLOYMENT_HISTORY_URL = "https://static.flyapps.top/download.v63.23.tar.gz"
DOWNLOAD_DEPLOYMENT_HASH_URL_DES = "hash模式"
DOWNLOAD_DEPLOYMENT_HISTORY_URL_DES = "history模式"
class BASECONF(object):
@ -471,8 +474,10 @@ class USERPERSONALCONFIGKEY(object):
'DEVELOPER_ABNORMAL_DEVICE_WRITE']
PREVIEW_ROUTE_HASH = False # 预览路由模式是否是hash, 如果使用hash模式,url中就会存在“#“符号,这个符号后面的是路径。
PREVIEW_ROUTE_HASH_DES = '【下载页路由hash模式】:使用hash模式,url中就会存在“#“符号,这个符号后面的是路径。非hash模式,需要nginx进行额外配置'
PREVIEW_ROUTE_HASH_DES = '【下载页路由hash模式】:使用hash模式,url中就会存在“#“符号,这个符号后面的是路径。非hash模式(history模式),需要nginx进行额外配置'
PRIVATE_DOWNLOAD_PAGE = False # 私有下载页配置,默认false
PRIVATE_DOWNLOAD_PAGE_DES = '【私有下载页模式】:默认是关闭状态,若开启,则需要先添加下载域名服务器'
class OSSSTORAGECONF(object):
STORAGE_FREE_CAPACITY = 2048 * 1024 * 1024 # 单位byte 2G

@ -290,6 +290,7 @@ CACHE_KEY_TEMPLATE = {
DATA_DOWNLOAD_KEY = "d_token"
FILE_UPLOAD_TMP_KEY = ".tmp"
DOMAIN_CNAME_KEY = "_fly_dnsauth"
SYNC_CACHE_TO_DATABASE = {
'download_times': 10, # 下载次数同步时间

Loading…
Cancel
Save