Compare commits

..

45 Commits

Author SHA1 Message Date
isummer e12779d654 修复容器部署失败问题,重新编写本地部署文档 2 years ago
isummer ac590563a7 修复超级签共享用户下载文件失败问题 2 years ago
isummer 8fc1d353c9 管理后台增加重置密码功能 2 years ago
isummer 35b64d74de 修复flyapps容器构建失败问题,优化部署文档 2 years ago
isummer a064988f56 修复flyapps容器构建失败问题,优化部署文档 2 years ago
isummer adff361c11 修复非苹果设备导致余额不足问题 2 years ago
isummer 419e39eaaa 修复非苹果设备导致余额不足问题 2 years ago
isummer 9d9cb3f2cb Merge remote-tracking branch 'origin/master' 2 years ago
isummer dd802dccbb 优化代码 2 years ago
isummer 1262bff671 优化代码 2 years ago
isummer 289737ed00 修复异常,优化代码 2 years ago
isummer dc7b491534 修复新苹果,新应用无法设置专属签名问题 2 years ago
isummer eed0f5194c 增加苹果设备类型区分,优化苹果设备注册量及展示 2 years ago
isummer e0599585be 修复前端应用超级签名展示异常问题 2 years ago
isummer 42451f2cb0 修复超级签权限问题 2 years ago
isummer 646162593e 增加打包时间 2 years ago
isummer 200494fcf3 增加打包时间 2 years ago
isummer fe5a7605f0 增加打包时间 2 years ago
isummer f70351d8e5 修复容器启动异常问题 2 years ago
isummer 9e2f40383a 优化代码,增加邀请码配置,修改极验弹窗模式 2 years ago
isummer debe95791e 优化容器部署 2 years ago
isummer a3887825c9 更新依赖包,支持python39环境 2 years ago
isummer 8a8f9ef7fe 升级python环境为python39 2 years ago
isummer 8e325e6cde 优化容器启停异常 2 years ago
isummer 92961cfa61 flyapps-cli增加容器访问 2 years ago
isummer 8c25aab63c flyapps-cli增加容器访问 2 years ago
isummer 0cc5084390 修复授权绑定设备添加重复问题 2 years ago
isummer 4d98981485 容器部署支持管理后台访问 2 years ago
isummer a7a1472533 苹果应用授权码支持添加绑定设备 2 years ago
isummer 332d12bddf 容器部署优化 2 years ago
isummer 6439824468 优化docker部署方式 2 years ago
isummer 42478f21d9 1.修复授权码为一次,超级签苹果包下载失败问题 2 years ago
isummer 2c67305f8a 增加通过命令给用户充值次数 2 years ago
isummer 79037d5248 修复应用上传超级签检测问题 2 years ago
isummer 525ec097d7 更新readme 2 years ago
isummer a35ca0b087 更新配置文件信息 2 years ago
isummer d9ac93ccc1 no message 2 years ago
isummer 1ff3da6896 修复管理页面预览展示 2 years ago
isummer cbad06e95d 应用时间更新修复 2 years ago
isummer b093d7133b 页面底部信息配置迁移 2 years ago
isummer 7aa0471ba9 增加应用当天下载次数展示 2 years ago
isummer 2224ce27c7 优化代码 2 years ago
isummer d8b124c1a7 用户下载页私有配置功能 2 years ago
isummer d1a5d69ac8 增加客户端独立部署功能 2 years ago
isummer 8a8d2f2400 增加预览下载页路由模式 2 years ago
  1. 2
      .gitignore
  2. 163
      README.md
  3. 12
      build.sh
  4. 7
      build_client.sh
  5. 102
      doc/docker.md
  6. BIN
      doc/images/img_1.png
  7. BIN
      doc/images/img_10.png
  8. BIN
      doc/images/img_11.png
  9. BIN
      doc/images/img_2.png
  10. BIN
      doc/images/img_3.png
  11. BIN
      doc/images/img_4.png
  12. BIN
      doc/images/img_5.png
  13. BIN
      doc/images/img_6.png
  14. BIN
      doc/images/img_7.png
  15. BIN
      doc/images/img_8.png
  16. BIN
      doc/images/img_9.png
  17. 166
      doc/local.md
  18. 51
      docker.md
  19. 67
      docker/build/docker-compose.yml
  20. 31
      docker/flyapps/docker-compose.only.yml
  21. 48
      docker/flyapps/docker-compose.yml
  22. 7
      docker/init/clean.sh
  23. 10
      docker/init/download.images
  24. 24
      docker/init/init.sh
  25. 1
      docker/init/timezone
  26. 43
      docker/mariadb/docker-compose.yml
  27. 108
      docker/mariadb/server.cnf
  28. 25
      docker/redis/docker-compose.yml
  29. 6
      docker/scripts/build.sh
  30. 9
      docker/scripts/start_all.sh
  31. 10
      docker/scripts/stop_all.sh
  32. 2
      fir_admin/.env.production
  33. 8
      fir_admin/src/api/user.js
  34. 6
      fir_admin/src/views/appinfos/list.vue
  35. 2
      fir_admin/src/views/login/index.vue
  36. 84
      fir_admin/src/views/userinfos/UserDetail.vue
  37. 23
      fir_admin/yarn.lock
  38. 1
      fir_client/package.json
  39. 6
      fir_client/public/index.html
  40. 6
      fir_client/public/short.html
  41. BIN
      fir_client/src/assets/github.png
  42. BIN
      fir_client/src/assets/wxchat.jpg
  43. 14
      fir_client/src/components/FirFooter.vue
  44. 3
      fir_client/src/components/FirHeader.vue
  45. 5
      fir_client/src/components/FirLogin.vue
  46. 55
      fir_client/src/components/apps/FirAppInfosBase.vue
  47. 110
      fir_client/src/components/apps/FirAppInfossecurity.vue
  48. 11
      fir_client/src/components/apps/FirAppInfostimeline.vue
  49. 9
      fir_client/src/components/apps/FirApps.vue
  50. 26
      fir_client/src/components/base/AppleDeveloperBindApp.vue
  51. 47
      fir_client/src/components/base/BindDomain.vue
  52. 15
      fir_client/src/components/index/FirContact.vue
  53. 4
      fir_client/src/components/index/FirIndexBody.vue
  54. 9
      fir_client/src/components/index/FirIndexTem.vue
  55. 16
      fir_client/src/components/index/FirNews.vue
  56. 45
      fir_client/src/components/user/FirSuperSignBase.vue
  57. 90
      fir_client/src/components/user/FirUserDomain.vue
  58. 470
      fir_client/src/components/user/FirUserDownload.vue
  59. 11
      fir_client/src/components/user/FirUserQrcode.vue
  60. 21
      fir_client/src/restful/download.js
  61. 89
      fir_client/src/restful/index.js
  62. 5
      fir_client/src/router/index.js
  63. 6
      fir_client/src/utils/base/utils.js
  64. 22
      fir_client/src/utils/format.js
  65. 6
      fir_client/src/utils/index.js
  66. 84
      fir_client/vue.config.js
  67. 37
      fir_ser/Dockerfile
  68. 11
      fir_ser/Dockerfile.cli
  69. 5
      fir_ser/admin/utils/serializer.py
  70. 2
      fir_ser/admin/views/celery_flower.py
  71. 14
      fir_ser/admin/views/login.py
  72. 14
      fir_ser/api/management/commands/add_user_download_times.py
  73. 2
      fir_ser/api/management/commands/services/command.py
  74. 15
      fir_ser/api/management/commands/services/services/base.py
  75. 32
      fir_ser/api/migrations/0007_auto_20220725_1612.py
  76. 10
      fir_ser/api/models.py
  77. 11
      fir_ser/api/tasks.py
  78. 3
      fir_ser/api/urls.py
  79. 4
      fir_ser/api/utils/apputils.py
  80. 16
      fir_ser/api/utils/modelutils.py
  81. 50
      fir_ser/api/utils/serializer.py
  82. 27
      fir_ser/api/views/apps.py
  83. 124
      fir_ser/api/views/domain.py
  84. 23
      fir_ser/api/views/download.py
  85. 4
      fir_ser/api/views/login.py
  86. 45
      fir_ser/api/views/personalconfig.py
  87. 10
      fir_ser/api/views/uploads.py
  88. 12
      fir_ser/cli.py
  89. 22
      fir_ser/common/base/baseutils.py
  90. 180
      fir_ser/common/base/magic.py
  91. 12
      fir_ser/common/constants.py
  92. 146
      fir_ser/common/core/decorators.py
  93. 12
      fir_ser/common/core/signals.py
  94. 36
      fir_ser/common/core/sysconfig.py
  95. 5
      fir_ser/common/libs/mp/wechat.py
  96. 2
      fir_ser/common/libs/pay/wx.py
  97. 6
      fir_ser/common/libs/pay/wxpay/__init__.py
  98. 4
      fir_ser/common/libs/pay/wxpay/core.py
  99. 10
      fir_ser/common/utils/caches.py
  100. 2
      fir_ser/common/utils/download.py
  101. Some files were not shown because too many files have changed in this diff Show More

2
.gitignore vendored

@ -12,3 +12,5 @@ fir_client/src/assets/down_right.png
mailhtml/build/
mailhtml/node_modules/
mailhtml/.idea/
fir_client/dist_index/
fir_client/dist_short/

@ -1,100 +1,63 @@
##### 从git上面下载源码
```shell
cd /data/
git clone https://github.com/nineaiyu/FlyApps
```
### FlyApp 服务器搭建 [python3 环境]
##### 搭建python env 环境
```shell
yum install python36 python36-devel redis mariadb-server mariadb-devel -y
python3 -m venv py3
source py3/bin/activate
```
###### 安装pip包
```shell
cd fir_ser/
pip install -U setuptools pip
pip install -r requirements.txt
```
###### 配置数据库
```
在 config.py 配置redis 和 mysql 数据库
```
###### 迁移数据库
```shell
#如果是mysql,需要做一下配置,如果是sqlite,需要升级sqlite
#记得根据配置创建数据库
#create database flyapp default character set utf8 COLLATE utf8_general_ci;
#grant all on flyapp.* to flyuser@'127.0.0.1' identified by 'flypwd00oo.1';
python manage.py makemigrations
python manage.py migrate
```
### FlyApp zsign ipa重签名工具安装
```shell
yum install openssl-devel -y
yum install gcc-c++ gcc -y
wget https://github.com/nineaiyu/zsign/archive/refs/tags/v1.1.2.tar.gz
tar xvf v1.1.2.tar.gz
cd zsign-1.1.2/
g++ *.cpp common/*.cpp -lcrypto -O3 -std=c++11 -o zsign
cp zsign /usr/bin/
```
### FlyApp web端搭建
###### npm编译环境
```shell
yum install npm
npm install -g n
n 12.13 # 安装12版本的node ,最新版本会有问题
npm install -g yarn
```
###### 编译web端和下载页
```shell
cd FlyApps/fir_client/
vim vue.config.js #修改api接口地址
# pro_base_env 正式环境信息
# dev_base_env 开发环境信息
yarn install
yarn build index # web打包
yarn build short # 下载也打包
```
###### web目录操作
```shell
# web目录: /www/wwwroot/fly.dvcloud.xin/
cd /www/wwwroot/fly.dvcloud.xin/
cp -a /data/FlyApps/fir_client/dist/* .
cp -a /data/FlyApps/fir_download/dist/* .
```
##### 更新sqllite
```shell
tar xvf sqlite-autoconf-3310100.tar.gz
cd sqlite-autoconf-3310100
./configure --prefix=/usr/local/sqlite
make -j4
make install
rm -rf /usr/bin/sqlite3
ln -s /usr/local/sqlite/bin/sqlite3 /usr/bin/sqlite3
ll /usr/bin/sqlite3
echo "/usr/local/sqlite/lib" > /etc/ld.so.conf.d/sqlite3.conf
ldconfig
```
### 用与应用分发,苹果超级签名
#### 部署前准备
- 备案域名【至少需要一个域名,以下可通过子域名部署】
- API域名
- 前端web域名
- 下载页域名
- 下载页域名可配置多个
- 存储域名(使用阿里云oss存储)
- ssl证书
- API域名证书
- 存储域名证书(使用阿里云oss存储)
- 前端web域名证书(可选)
- Centos8Stream 服务器
- 如果使用oss存储,则带宽为1M,若使用本地存储,则带宽越大越好
- 如果使用超级签,最低配置为2cpu 4G内存,若干不使用签名,则1cpu2G就行
- 阿里云短信或极光短信服务【可选一个,主要用与注册,重置密码】
- 阿里云短信
- 极光短信
- 邮箱服务【可选,用与注册,重置密码,通知信息】
- 阿里云OSS存储【可选】
- [sts授权配置](https://help.aliyun.com/document_detail/100624.html)
- 阿里云CDN【可选,用与加速访问】
- 极验验证【可选,滑动验证服务】
- 微信公众号【可选,用与微信扫描登录】
- 阿里云支付【可选,用与购买下载次数】
- 微信支付【可选,用与购买下载次数】
#### 自用搭建建议
- 阿里云服务器需要1cpu 2G内存,无需系统盘,如果使用超级签,可以适当增加配置
- 需要阿里云OSS存储和阿里云CDN,并且OSS存储和阿里云服务器部署同一个地区
- 可以申请一个极验进行滑动验证,或者开启验证码验证
- 阿里云备案域名:api和前端可以使用一个域名,下载页单独域名
#### 部署必备资料
- 域名证书
- web域名和证书
- api域名和证书
- 下载页域名(可配置证书)
- 存储域名和证书
- 本地存储,则该域名和证书可以和api域名证书一致
- 阿里云oss存储
- 开启cdn,需要新域名和证书
- 不开启,无需域名和证书
- Centos8Stream 服务器
## 部署方式
### 1.[Docker 部署](./doc/docker.md) 【推荐】
### 2.[本地部署](./doc/local.md)
### 功能预览
![img_1.png](./doc/images/img_1.png)
![img_2.png](./doc/images/img_2.png)
![img_3.png](./doc/images/img_3.png)
![img_4.png](./doc/images/img_4.png)
![img_5.png](./doc/images/img_5.png)
![img_6.png](./doc/images/img_6.png)
![img_7.png](./doc/images/img_7.png)
![img_8.png](./doc/images/img_8.png)
![img_9.png](./doc/images/img_9.png)
![img_10.png](./doc/images/img_10.png)
![img_11.png](./doc/images/img_11.png)

@ -1,12 +0,0 @@
#!/bin/bash
#
#
# shellcheck disable=SC2006
s_path=`pwd`
#first build fir_client
docker pull node:14.17.3
\cp -a build_client.sh fir_client/
docker run --rm -v "${s_path}"/fir_client:/fir_client -it node:14.17.3 sh /fir_client/build_client.sh
cd "${s_path}"/fir_ser/ && docker build . -t flyapps

@ -1,7 +0,0 @@
#!/bin/bash
#
#
cd /fir_client/ && yarn config set registry https://registry.npm.taobao.org && yarn install && yarn build

@ -0,0 +1,102 @@
### Docker最小化部署前准备,如果使用超级签和云存储,需要使用阿里云的服务器
系统|Centos 8 Stream 纯净系统 [2cpu 4G内存]
----|----
域名|```app.hehelucky.cn```
域名SSL证书| nginx 格式证书 [阿里云和腾讯云都可以申请免费ssl证书]
数据路径| ```/data```
### 开始部署
##### 1.从git上面下载源码
```
cd /data/
dnf install git -y
git clone https://github.com/nineaiyu/flyapps
```
## 注意,下面展示的相对路径,都是以```/data/flyapps```为相对目录
#### 2.docker环境安装
```
cd /data/flyapps/docker/init
sh init.sh
```
#### 3.配置域名和证书,如果有cdn或者oss,也要进行配置
a.将域名证书通过```sftp软件```或者```rz命令```复制到```/data/flyapps/nginx.conf.d```目录中
b.将域名证书分别重命名为 ```域名.pem``` 和 ```域名.key``` .本次使用的域名是 ```app.hehelucky.cn``` ,因此,证书名称类似如下
```shell script
nginx.conf.d/app.hehelucky.cn.key
nginx.conf.d/app.hehelucky.cn.pem
```
c.修改NGINX配置文件```nginx.conf.d/flyapps-vhost.conf```,将```server_name```字段修改为域名,将```ssl_certificate```和
```ssl_certificate_key```修改为对应证书路径,修改之后,对应字段如下
```shell
server_name app.hehelucky.cn;
ssl_certificate /etc/nginx/conf.d/app.hehelucky.cn.pem;
ssl_certificate_key /etc/nginx/conf.d/app.hehelucky.cn.key;
```
#### 4.配置api服务需要修改api和web域名,如果有需求,还可以配置 短信,邮箱,geetest,存储等信息
##### fir_ser配置文件 ```fir_ser/config.py```
```python
class DOMAINCONF(object):
API_DOMAIN = "https://app.hehelucky.cn" # 用与开启本地存储,上传应用配置
WEB_DOMAIN = "https://app.hehelucky.cn" # 用于超级签跳转配置,该域名一般为前端页面域名
```
##### fir_client配置文件 ```fir_client/vue.confjg.js```
```javascript
const pro_base_env = {
baseUrl: '/', //该选项可以填写web-api的域名,类似 https://api.xxx.com/
index_static: '/', //若配置cdn等加速,可以填写cdn加速域名
baseShortUrl: '/', //该选项可以填写short-api的域名,也可以和web-api域名一样,类似 https://api.xxx.com/
short_static: '/short/', //若配置cdn等加速,可以填写cdn加速域名
version: version,
};
```
##### 构建静态资源
```
cd /data/flyapps/docker/scripts
sh build.sh
```
##### 启动所有服务
```
cd /data/flyapps/docker/scripts
sh start_all.sh
```
##### 关闭所有服务
```
cd /data/flyapps/docker/scripts
sh stop_all.sh
```
##### 根据提示创建默认管理用户,用与访问管理后台
```shell
docker exec -it flyapps python manage.py createsuperuser
```
##### 测试访问
- 在浏览器输入自己配置的域名 https://app.hehelucky.cn/ 进行访问
- 管理后台访问 https://app.hehelucky.cn:3448/ 进行访问
##### 如果要使用本地的数据库和redis,api服务使用容器,需要修改文件```/data/flyapps/docker/flyapps/docker-compose.yml```
```shell
external_links:
- mariadb:mariadb
- redis:redis
```
修改为
```shell
extra_hosts:
- "mariadb:172.31.31.1"
- "redis:172.31.31.1"
```
并将```nginx:```下面的配置注释掉
同时还需要修改mariadb和redis授权

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

@ -0,0 +1,166 @@
### 本地部署前准备
系统 | Centos 8 Stream [2cpu 4G内存]
----|----
域名|```app.hehelucky.cn```
域名SSL证书| nginx 格式证书 [阿里云和腾讯云都可以申请免费ssl证书]
数据路径| ```/data```
##### 从git上面下载源码
```shell
cd /data/
dnf install git gcc zip unzip -y
git clone https://github.com/nineaiyu/flyapps
```
### flyapps 本地部署 [python3 环境]
##### 搭建python env 环境
```shell
dnf install python39 python39-devel redis mariadb-server mariadb-devel -y
python3.9 -m venv py39
source py39/bin/activate
```
###### 安装pip包
```shell
cd /data/flyapps/fir_ser/
pip install -U setuptools pip -i https://pypi.tuna.tsinghua.edu.cn/simple/
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple/
```
#### 配置数据库
##### 启动mariadb数据库
```shell
systemctl restart mariadb
systemctl enable mariadb
```
##### 创建mysql数据库
```shell
mysql
```
执行下面mysql命令
```mariadb
create database flyapps default character set utf8 COLLATE utf8_general_ci;
grant all on flyapps.* to flyuser@'127.0.0.1' identified by 'KGzKjZpWBp4R4RSa';
```
##### 启动redis数据库
```shell
echo 'requirepass nineven' >> /etc/redis.conf # 配置授权密码
systemctl restart redis
systemctl enable redis
```
### flyapps zsign ipa重签名工具安装
```shell
dnf install openssl-devel -y
dnf install gcc-c++ gcc -y
cd /data/flyapps/fir_ser/
tar xvf zsign-1.1.2.tar.gz
cd zsign-1.1.2/
g++ *.cpp common/*.cpp -lcrypto -O3 -std=c++11 -o /usr/bin/zsign
```
#### 配置api服务需要修改api和web域名,如果有需求,还可以配置 短信,邮箱,geetest,存储等信息
##### fir_ser配置文件 ```fir_ser/config.py```
```python
class DOMAINCONF(object):
API_DOMAIN = "https://app.hehelucky.cn" # 用与开启本地存储,上传应用配置
WEB_DOMAIN = "https://app.hehelucky.cn" # 用于超级签跳转配置,该域名一般为前端页面域名
```
##### fir_client配置文件 ```fir_client/vue.confjg.js```
```javascript
const pro_base_env = {
baseUrl: '/', //该选项可以填写web-api的域名,类似 https://api.xxx.com/
index_static: '/', //若配置cdn等加速,可以填写cdn加速域名
baseShortUrl: '/', //该选项可以填写short-api的域名,也可以和web-api域名一样,类似 https://api.xxx.com/
short_static: '/short/', //若配置cdn等加速,可以填写cdn加速域名
version: version,
};
```
### flyapps web端搭建
###### npm编译环境
```shell
dnf install npm -y
npm install -g n
n 12.13 # 安装12版本的node或者14版本 ,最新版本会有问题
npm install -g yarn
```
###### 编译web端和下载页
```shell
cd /data/flyapps/fir_client/
vim vue.config.js #修改api接口地址
# pro_base_env 正式环境信息
# dev_base_env 开发环境信息
yarn install
yarn build index # 前端打包,输出目录 dist_index
yarn build short # 下载页打包,输出目录 dist_short
```
###### 编译web管理后台
```shell
cd /data/flyapps/fir_admin/
yarn install
yarn build:prod # 下载页打包,输出目录 dist
```
### nginx配置
##### 安装nginx
```shell
dnf install nginx -y
```
##### 将域名ssl证书解压到```/etc/nginx/conf.d/``` 目录中,并重命名为```域名.pem```,```域名.key```格式
##### 拷贝虚拟主机配置文件
```shell
cp /data/flyapps/nginx.conf.d/flyapps-vhost.conf /etc/nginx/conf.d/
```
1.创建web目录,并将打包好的web复制该目录
```shell
mkdir -pv /data/{fir_client/short,fir_admin}
cp -a /data/flyapps/fir_client/dist_index/* /data/fir_client/
cp -a /data/flyapps/fir_client/dist_short/* /data/fir_client/short/
cp -a /data/flyapps/fir_admin/dist/* /data/fir_admin/
chown nginx.nginx -R /data/{fir_client,fir_admin}
```
2.启动nginx服务
```shell
nginx -t
systemctl restart nginx
systemctl enable nginx
```
##### 添加redis和mariadb uwsgi本地解析
```shell
echo '127.0.0.1 mariadb redis flyapps' >> /etc/hosts
```
###### 迁移数据库
```shell
cd /data/flyapps/fir_ser/
python manage.py makemigrations
python manage.py migrate
```
#### 启动api服务
```shell
echo 'net.core.somaxconn=1024' >> /etc/sysctl.conf
sysctl -p
cd /data/flyapps/fir_ser/
python manage.py start all -u nginx -usm 1 -d
```
##### 根据提示创建默认管理用户,用与访问管理后台
```shell
python manage.py createsuperuser
```
- 需要输入用户名,邮箱和密码
- 用户名和密码用与登录管理后台
- 邮箱和密码用与登录前端web

@ -1,51 +0,0 @@
##### 从git上面下载源码
```
cd /data/
git clone https://github.com/nineaiyu/FlyApps
```
#### docker环境安装 [centos7]
```
yum install epel-release -y
yum clean all && yum makecache
yum install docker -y
```
#### 配置域名和证书,如果有cdn或者oss,也要进行配置
```shell script
nginx.conf.d/app.hehelucky.cn.key
nginx.conf.d/app.hehelucky.cn.pem
nginx.conf.d/flyapps-vhost.conf
```
### api服务需要修改api和web域名,短信,邮箱,geetest,存储等信息
```shell script
vim fir_ser/config.py
API_DOMAIN = "https://app.hehelucky.cn"
WEB_DOMAIN = "https://app.hehelucky.cn"
MOBILEPROVISION = "https://ali-static.jappstore.com/embedded.mobileprovision"
```
### web页面需要修改指定api域名
```vuejs
vim fir_client/vue.config.js
const pro_base_env = {
baseUrl: 'https://flyapps.cn',
index_static: 'https://static.flyapps.cn/index/',
baseShortUrl: 'https://flyapps.top',
short_static: 'https://static.flyapps.top/short/',
};
```
##### 构建静态资源和api服务
```
sh build.sh
```
##### 启动所有服务
```
sh start.sh
```

@ -0,0 +1,67 @@
version: '3.2'
services:
buildflyapps:
image: 'flyapps'
build:
context: ../../fir_ser
dockerfile: Dockerfile
command:
- echo
- success
buildclient:
container_name: buildclient
image: 'node:14.17.3'
working_dir: /fir_client
volumes:
- /etc/localtime:/etc/localtime:ro
- ../../fir_client:/fir_client
- ../../data/web/:/web
command:
- bash
- -c
- yarn config set registry https://registry.npm.taobao.org && yarn install && yarn build index && yarn build short && ([ ! -e /web/fir_client ] && \cp -a dist_index /web/fir_client || \cp -a dist_index/* /web/fir_client/) && ([ ! -e /web/fir_client/short ] && \cp -a dist_short /web/fir_client/short || \cp -a dist_short/* /web/fir_client/short/) && echo build success
buildindex:
container_name: buildindex
image: 'node:14.17.3'
working_dir: /fir_client
volumes:
- /etc/localtime:/etc/localtime:ro
- ../../fir_client:/fir_client
- ../../data/web/:/web
command:
- bash
- -c
- yarn config set registry https://registry.npm.taobao.org && yarn install && yarn build index && ([ ! -e /web/fir_index ] && \cp -a dist_index /web/fir_index || \cp -a dist_index/* /web/fir_index/) && echo build success
buildshort:
container_name: buildshort
image: 'node:14.17.3'
working_dir: /fir_client
volumes:
- /etc/localtime:/etc/localtime:ro
- ../../fir_client:/fir_client
- ../../data/web/:/web
command:
- sh
- -c
- yarn config set registry https://registry.npm.taobao.org && yarn install && yarn build short && ([ ! -e /web/fir_short ] && \cp -a dist_short /web/fir_short || \cp -a dist_short/* /web/fir_short/) && echo build success
buildadmin:
container_name: buildadmin
image: 'node:14.17.3'
working_dir: /fir_admin
volumes:
- /etc/localtime:/etc/localtime:ro
- ../../fir_admin:/fir_admin
- ../../data/web/:/web
command:
- sh
- -c
- yarn config set registry https://registry.npm.taobao.org && yarn install && yarn build:prod && ([ ! -e /web/fir_admin ] && \cp -a dist /web/fir_admin || \cp -a dist/* /web/fir_admin/) && echo build success

@ -0,0 +1,31 @@
version: '3'
networks:
flyapps:
external: true
name: flyapps
services:
flyapps:
container_name: flyapps
restart: always
image: 'nineven/flyapps'
working_dir: /data/fir_ser
volumes:
- /etc/localtime:/etc/localtime:ro
- ../../fir_ser:/data/fir_ser
- ../../data/flyapps/files:/data/fir_ser/files
- ../../data/logs/flyapps/:/data/fir_ser/logs
- ../../data/flyapps/supersign/:/data/fir_ser/supersign
- ../../nginx.conf.d:/data/cert
networks:
flyapps:
ipv4_address: 172.31.31.100
external_links:
- mariadb:mariadb
- redis:redis
sysctls:
- net.core.somaxconn=4096
extra_hosts:
- "mariadb:172.31.31.1"
- "redis:172.31.31.1"

@ -0,0 +1,48 @@
version: '3'
networks:
flyapps:
external: true
name: flyapps
services:
flyapps:
container_name: flyapps
restart: always
image: 'nineven/flyapps'
working_dir: /data/fir_ser
volumes:
- /etc/localtime:/etc/localtime:ro
- ../../fir_ser:/data/fir_ser
- ../../data/flyapps/files:/data/fir_ser/files
- ../../data/logs/flyapps/:/data/fir_ser/logs
- ../../data/flyapps/supersign/:/data/fir_ser/supersign
- ../../nginx.conf.d:/data/cert
networks:
flyapps:
ipv4_address: 172.31.31.100
external_links:
- mariadb:mariadb
- redis:redis
sysctls:
- net.core.somaxconn=4096
nginx:
container_name: nginx
restart: always
image: 'nginx:1.21.3'
volumes:
- /etc/localtime:/etc/localtime:ro
- ../../data/web:/data
- ../../nginx.conf.d:/etc/nginx/conf.d
- ../../data/logs/nginx:/var/log/nginx
networks:
flyapps:
ipv4_address: 172.31.31.200
ports:
- 80:80
- 443:443
- 3448:3448
external_links:
- flyapps:flyapps
depends_on:
- flyapps

@ -0,0 +1,7 @@
#!/bin/bash
#
#
for i in nginx flyapps mariadb redis buildclient buildshort buildadmin ;do echo $i;docker rm -f $i;done
docker network rm flyapps

@ -0,0 +1,10 @@
#!/bin/bash
#
#
docker pull 'bitnami/mariadb:10.7.3'
docker pull 'bitnami/redis:6.2.7'
docker pull 'nginx:1.21.3'
docker pull 'nineven/flyapps:latest'
docker pull 'node:14.17.3'

@ -0,0 +1,24 @@
#!/bin/bash
#
#
which dockerd
if [ $? -ne 0 ];then
dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo
dnf install docker-ce -y
fi
#which docker-compose
#if [ $? -ne 0 ];then
# curl -L https://get.daocloud.io/docker/compose/releases/download/v2.5.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
# chmod +x /usr/local/bin/docker-compose
#fi
data_path="$(dirname $(dirname `pwd`))/data"
mkdir -pv ${data_path}/{flyapps/files,web,mariadb,redis,logs/{mariadb,nginx,flyapps}}
\cp $(dirname $(dirname `pwd`))/fir_ser/files/head_img.jpeg ${data_path}/flyapps/files/
chown 1001.1001 -R ${data_path}/{flyapps,web,mariadb,redis,logs/{mariadb,nginx,flyapps}}
chown 101.101 -R ${data_path}/{flyapps,logs/flyapps}
systemctl start docker && docker network create flyapps --driver bridge --subnet=172.31.31.0/24 --gateway=172.31.31.1
systemctl enable docker
systemctl status docker

@ -0,0 +1 @@
Asia/Shanghai

@ -0,0 +1,43 @@
version: '3.2'
networks:
flyapps:
external: true
name: flyapps
services:
mariadb:
image: bitnami/mariadb:10.7.3
container_name: mariadb
restart: always
environment:
- MARIADB_ROOT_PASSWORD=rootIPD.xx2.19
- MARIADB_DATABASE=flyapps
- MARIADB_USER=flyuser
- MARIADB_PASSWORD=KGzKjZpWBp4R4RSa
#- ALLOW_EMPTY_PASSWORD=yes
- MARIADB_ENABLE_SLOW_QUERY=1
- MARIADB_LONG_QUERY_TIME=3
- MARIADB_SKIP_TEST_DB=yes
- MARIADB_EXTRA_FLAGS=--max-connect-errors=3000 --max_connections=30000
ports:
- 3306:3306
networks:
flyapps:
ipv4_address: 172.31.31.90
volumes:
- ../init/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
- ../../data/mariadb:/bitnami/mariadb/data
- ../../data/logs/mariadb:/data/logs/mariadb
- ./server.cnf:/opt/bitnami/mariadb/conf/my_custom.cnf:ro
# adminer:
# image: adminer
# ports:
# - 8080:8080
# networks:
# - fungame

@ -0,0 +1,108 @@
#
# These groups are read by MariaDB server.
# Use it for options that only the server (but not clients) should see
#
# See the examples of server my.cnf files in /usr/share/mysql/
#
# this is only for the mysqld standalone daemon
[mysqld]
binlog_cache_size = 192K
thread_stack = 384K
join_buffer_size = 4096K
query_cache_type = 1
max_heap_table_size = 1024M
default_storage_engine = InnoDB
performance_schema_max_table_instances = 400
table_definition_cache = 400
skip-external-locking
key_buffer_size = 512M
max_allowed_packet = 1G
table_open_cache = 1024
sort_buffer_size = 2048K
net_buffer_length = 4K
read_buffer_size = 2048K
read_rnd_buffer_size = 1024K
myisam_sort_buffer_size = 16M
thread_cache_size = 192
query_cache_size = 256M
tmp_table_size = 1024M
sql-mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES
max_connections = 30000
max_connect_errors = 1000
open_files_limit = 65535
expire_logs_days = 10
#log_queries_not_using_indexes=on
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci
init_connect='SET NAMES utf8mb4'
#character-set-server=utf8
skip_name_resolve
event_scheduler=1
#skip-grant-tables
#innodb_data_home_dir = /var/lib/mysql/
#innodb_data_file_path = ibdata1:10M:autoextend
#innodb_log_group_home_dir = /var/lib/mysql/
#innodb_buffer_pool_size = 1024M
#innodb_log_file_size = 128M
#innodb_log_buffer_size = 32M
#innodb_flush_log_at_trx_commit = 1
#innodb_lock_wait_timeout = 50
#innodb_max_dirty_pages_pct = 90
#innodb_read_io_threads = 4
#innodb_write_io_threads = 4
#
# Allow server to accept connections on all interfaces.
#
#bind-address=0.0.0.0
#
# this is only for embedded server
# This group is only read by MariaDB servers, not by MySQL.
# If you use the same .cnf file for MySQL and MariaDB,
# you can put MariaDB-only options here
[mariadb]
#autoset_open_files_limit
#enable_slow_query_log
#audit
plugin_load_add=server_audit
server_audit_logging=on
server_audit_events=connect,query
server_audit=force_plus_permanent
server_audit_events=QUERY_DDL,QUERY_DML,CONNECT
server_audit_output_type=file
server_audit_file_rotate_now=on
server_audit_file_rotations=9
server_audit_file_rotate_size=1G
server_audit_file_path=/data/logs/mariadb
log_error=/data/logs/mariadb/mariadb.err.log
log_output=FILE
slow_query_log
long_query_time=3
slow_query_log_file=/data/logs/mariadb/mariadb-slow.log
log_queries_not_using_indexes=ON #Logging Queries That Don't Use Indexes
#server_id=2
log-bin=/data/logs/mariadb/mysql-bin
# This group is only read by MariaDB-10.5 servers.
# If you use the same .cnf file for MariaDB of different versions,
# use this group for options that older servers don't understand

@ -0,0 +1,25 @@
version: '3.2'
networks:
flyapps:
external: true
name: flyapps
services:
redis:
image: 'bitnami/redis:6.2.7'
container_name: redis
restart: always
volumes:
- ../init/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
- ../../data/redis:/bitnami/redis/data
environment:
#- REDIS_REPLICATION_MODE=master
#- ALLOW_EMPTY_PASSWORD=yes
- REDIS_PASSWORD=nineven
networks:
- flyapps
#ports:
# - 6379:6379

@ -0,0 +1,6 @@
#!/bin/bash
#
#
cd ../build/
docker compose up buildclient buildadmin

@ -0,0 +1,9 @@
#!/bin/bash
#
#
cd ../mariadb/ && docker compose up -d
cd ../redis/ && docker compose up -d
cd ../flyapps/ && docker compose up -d
docker logs -f flyapps

@ -0,0 +1,10 @@
#!/bin/bash
#
#
cd ../flyapps/ && docker compose down
cd ../redis/ && docker compose down
cd ../mariadb/ && docker compose down

@ -2,5 +2,5 @@
ENV = 'production'
# base api
VUE_APP_BASE_API = '/prod-api'
VUE_APP_BASE_API = '/api/v3/fir/server'

@ -53,3 +53,11 @@ export function updateCertificationInfo(data) {
data
})
}
export function resetPassword(data) {
return request({
url: '/user/info',
method: 'post',
data
})
}

@ -4,7 +4,7 @@
<el-input v-model="listQuery.bundle_id" placeholder="Bundle_Id" style="width: 250px;" class="filter-item" clearable @keyup.enter.native="handleFilter" />
<el-input v-model="listQuery.name" placeholder="应用名称" style="width: 200px;" class="filter-item" clearable @keyup.enter.native="handleFilter" />
<el-input v-model="listQuery.short" placeholder="短连接" style="width: 200px;" class="filter-item" clearable @keyup.enter.native="handleFilter" />
<!-- <el-input v-model="listQuery.domain_name" placeholder="应用专属访问域名" style="width: 200px;" class="filter-item" clearable @keyup.enter.native="handleFilter" />-->
<!-- <el-input v-model="listQuery.domain_name" placeholder="应用专属访问域名" style="width: 200px;" class="filter-item" clearable @keyup.enter.native="handleFilter" />-->
<el-select v-if="type_choices" v-model="listQuery.type" placeholder="应用类型" clearable class="filter-item" style="width: 120px" @change="handleFilter">
<el-option v-for="item in type_choices" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
@ -212,7 +212,7 @@ export default {
},
methods: {
go_download_url(app_info) {
window.open(app_info.preview_url + '/' + app_info.short, '_blank', '')
window.open(app_info.preview_url, '_blank', '')
},
deleteApp(app_id) {
this.$confirm('此操作将永久删除该应用, 是否继续?', '提示', {
@ -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

@ -66,7 +66,7 @@ export default {
}
}
const validatePassword = (rule, value, callback) => {
if (value.length < 6) {
if (value.length < 3) {
callback(new Error('The password can not be less than 6 digits'))
} else {
callback()

@ -6,21 +6,21 @@
<el-form-item label="UID">
<el-row :gutter="12">
<el-col :span="16">
<el-input :value="postForm.uid" disabled />
<el-input :value="postForm.uid" disabled/>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="账号" prop="username">
<el-row :gutter="12">
<el-col :span="16">
<el-input v-model="postForm.username" />
<el-input v-model="postForm.username"/>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="昵称">
<el-row :gutter="12">
<el-col :span="16">
<el-input v-model="postForm.first_name" />
<el-input v-model="postForm.first_name"/>
</el-col>
</el-row>
</el-form-item>
@ -28,7 +28,9 @@
<el-row :gutter="12">
<el-col :span="16">
<el-select v-model="postForm.gender" class="filter-item" placeholder="Please select">
<el-option v-for="item in postForm.gender_choices" :key="item.id" :label="item.name" :value="item.id" />
<el-option v-for="item in postForm.gender_choices" :key="item.id" :label="item.name"
:value="item.id"
/>
</el-select>
</el-col>
</el-row>
@ -37,7 +39,7 @@
<el-row :gutter="12">
<el-col :span="16">
<el-select v-model="postForm.role" class="filter-item" placeholder="Please select">
<el-option v-for="item in postForm.role_choices" :key="item.id" :label="item.name" :value="item.id" />
<el-option v-for="item in postForm.role_choices" :key="item.id" :label="item.name" :value="item.id"/>
</el-select>
</el-col>
</el-row>
@ -46,7 +48,7 @@
<el-form-item label="邮箱" prop="email">
<el-row :gutter="12">
<el-col :span="16">
<el-input v-model="postForm.email" />
<el-input v-model="postForm.email"/>
</el-col>
</el-row>
</el-form-item>
@ -54,35 +56,37 @@
<el-form-item label="手机" prop="mobile">
<el-row :gutter="12">
<el-col :span="16">
<el-input v-model="postForm.mobile" />
<el-input v-model="postForm.mobile"/>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="Q Q" prop="number">
<el-row :gutter="12">
<el-col :span="16">
<el-input v-model="postForm.qq" />
<el-input v-model="postForm.qq"/>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="职位">
<el-row :gutter="12">
<el-col :span="16">
<el-input v-model="postForm.job" />
<el-input v-model="postForm.job"/>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="公司">
<el-row :gutter="12">
<el-col :span="16">
<el-input v-model="postForm.company" />
<el-input v-model="postForm.company"/>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="备注">
<el-row :gutter="12">
<el-col :span="16">
<el-input v-model="postForm.memo" :autosize="{ minRows: 4, maxRows: 6}" type="textarea" placeholder="Please input" />
<el-input v-model="postForm.memo" :autosize="{ minRows: 4, maxRows: 6}" type="textarea"
placeholder="Please input"
/>
</el-col>
</el-row>
</el-form-item>
@ -91,7 +95,9 @@
<el-form-item label="用户头像">
<el-row :gutter="12">
<el-col :span="16">
<el-image :src="postForm.head_img" :preview-src-list="[postForm.head_img]" fit="contain" style="width: 100px; height: 100px" />
<el-image :src="postForm.head_img" :preview-src-list="[postForm.head_img]" fit="contain"
style="width: 100px; height: 100px"
/>
</el-col>
</el-row>
</el-form-item>
@ -177,10 +183,16 @@
<el-form-item label="实名认证">
<el-row :gutter="12">
<el-col :span="16">
<el-select v-model="postForm.certification" class="filter-item" placeholder="Please select" :disabled="postForm.certification === -1">
<el-option v-for="item in postForm.certification_status_choices" :key="item.id" :label="item.name" :value="item.id" />
<el-select v-model="postForm.certification" class="filter-item" placeholder="Please select"
:disabled="postForm.certification === -1"
>
<el-option v-for="item in postForm.certification_status_choices" :key="item.id" :label="item.name"
:value="item.id"
/>
</el-select>
<router-link v-if="postForm.certification_id" :to="{name: 'user_authentication_info_edit',params:{id:postForm.certification_id.id}}">
<router-link v-if="postForm.certification_id"
:to="{name: 'user_authentication_info_edit',params:{id:postForm.certification_id.id}}"
>
<el-button type="primary">
审核认证信息
</el-button>
@ -203,7 +215,7 @@
<el-form-item label="下载次数" prop="download_times">
<el-row :gutter="12">
<el-col :span="8">
<el-input :value="postForm.download_times" disabled />
<el-input :value="postForm.download_times" disabled/>
</el-col>
<el-col :span="8">
<router-link v-if="postForm.id" :to="{name: 'order_info_list',params:{user_id:postForm.id}}">
@ -215,14 +227,14 @@
<el-form-item label="注册时间" prop="timestamp">
<el-row :gutter="20">
<el-col :span="8">
<el-date-picker :value="postForm.date_joined" type="datetime" disabled />
<el-date-picker :value="postForm.date_joined" type="datetime" disabled/>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="应用版本历史记录">
<el-row :gutter="20">
<el-col :span="8">
<el-input v-model="postForm.history_release_limit" />
<el-input v-model="postForm.history_release_limit"/>
</el-col>
</el-row>
</el-form-item>
@ -234,6 +246,9 @@
<el-col :span="8">
<el-button @click="add_amount_download_times(postForm.id)">下载次数充值</el-button>
</el-col>
<el-col :span="8">
<el-button @click="reset_password(postForm.id)">重置密码</el-button>
</el-col>
<el-button v-if="!is_edit" type="primary" @click="is_edit=true">修改</el-button>
<div v-else>
<el-button type="primary" @click="is_edit=false">取消</el-button>
@ -245,7 +260,7 @@
<script>
import { validURL } from '@/utils/validate'
import { getUserInfos, updateUserInfo } from '@/api/user'
import { getUserInfos, resetPassword, updateUserInfo } from '@/api/user'
import { createOrderInfo } from '@/api/order'
import { changeStorageInfo, getStorageList } from '@/api/storage'
@ -277,7 +292,7 @@ const defaultForm = {
export default {
name: 'UserDetail',
components: { }, filters: {
components: {}, filters: {
userStatusFilter(status) {
const statusMap = {
true: '激活,允许登录',
@ -341,8 +356,7 @@ export default {
storage_selection: []
}
},
computed: {
},
computed: {},
created() {
if (this.isEdit) {
const id = this.$route.params && this.$route.params.id
@ -369,9 +383,33 @@ export default {
message: '取消充值'
})
})
}, reset_password(user_id) {
this.$prompt('请输入新密码,密码至少6位', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(({ value }) => {
resetPassword({ user_id: user_id, password: value }).then(res => {
if (res.code === 1000) {
this.$message.success('密码重置成功')
this.fetchData(user_id)
} else {
this.$message.error(res.msg)
}
})
}).catch((e) => {
console.log(e)
this.$message({
type: 'info',
message: '取消充值'
})
})
},
changeStorageData(force) {
changeStorageInfo({ id: this.postForm.id, use_storage_id: this.postForm.storage, force: force }).then(response => {
changeStorageInfo({
id: this.postForm.id,
use_storage_id: this.postForm.storage,
force: force
}).then(response => {
this.$message.success('存储数据迁移并设置刷新成功')
}).catch(err => {
console.log(err)

@ -2532,16 +2532,11 @@ bser@2.1.1:
dependencies:
node-int64 "^0.4.0"
buffer-from@1.x:
buffer-from@1.x, buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.npm.taobao.org/buffer-from/download/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
integrity sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8=
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buffer-indexof@^1.0.0:
version "1.1.1"
resolved "https://registry.npm.taobao.org/buffer-indexof/download/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c"
@ -3053,8 +3048,8 @@ commander@2.17.x:
commander@^2.18.0, commander@^2.19.0, commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
resolved "https://registry.npm.taobao.org/commander/download/commander-2.20.3.tgz?cache=0&sync_timestamp=1616364569946&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcommander%2Fdownload%2Fcommander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha1-/UhehMA+tIgcIHIrpIA16FMa6zM=
commander@~2.19.0:
version "2.19.0"
@ -9431,9 +9426,9 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
urix "^0.1.0"
source-map-support@^0.5.6, source-map-support@~0.5.12:
version "0.5.21"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
version "0.5.19"
resolved "https://registry.npm.taobao.org/source-map-support/download/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha1-qYti+G3K9PZzmWSMCFKRq56P7WE=
dependencies:
buffer-from "^1.0.0"
source-map "^0.6.0"
@ -9950,9 +9945,9 @@ terser-webpack-plugin@^2.3.6:
webpack-sources "^1.4.3"
terser@^4.1.2, terser@^4.6.12:
version "4.8.1"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f"
integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==
version "4.8.0"
resolved "https://registry.npm.taobao.org/terser/download/terser-4.8.0.tgz?cache=0&sync_timestamp=1616002306469&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fterser%2Fdownload%2Fterser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17"
integrity sha1-YwVjQ9fHC7KfOvZlhlpG/gOg3xc=
dependencies:
commander "^2.20.0"
source-map "~0.6.1"

@ -36,7 +36,6 @@
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"html-webpack-plugin": "^5.3.1",
"image-webpack-loader": "^7.0.1",
"script-ext-html-webpack-plugin": "^2.1.5",
"uglifyjs-webpack-plugin": "^2.2.0",
"vue-template-compiler": "^2.6.10",

@ -2,9 +2,9 @@
<html lang="zh">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width,initial-scale=1.0" name="viewport">
<link href="<%= BASE_URL %>favicon.ico" rel="icon">
<script src="<%= BASE_URL %>gt.js"></script>
<title>FLY分发平台</title>
</head>

@ -2,9 +2,9 @@
<html lang="zh">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<meta content="IE=edge" http-equiv="X-UA-Compatible">
<meta content="width=device-width,initial-scale=1.0" name="viewport">
<link href="<%= BASE_URL %>favicon.ico" rel="icon">
<script src="<%= BASE_URL %>gt.js"></script>
<title>应用下载</title>
</head>

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

@ -28,22 +28,12 @@
</template>
<script>
const footer = process.env.base_env.footer
export default {
name: "FirFooter",
data() {
return {
footer: {
copyright: 'Copyright © 2020-2022 东城飞阳 版权所有.',
ipcBeiAn: {
url: 'https://beian.miit.gov.cn',
text: '粤ICP备2021097895号',
},
gongAnBeiAn: {
url: 'http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=41142202000062',
text: '豫公网安备41142202000062号',
},
}
footer
}
}
}

@ -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') {

@ -125,7 +125,7 @@
</template>
<script>
import {loginFun, set_auth_token, wxLoginFun} from "@/restful";
import {loginFun, wxLoginFun} from "@/restful";
import {geetest} from "@/utils";
import {checkEmail, checkphone, getRandomStr} from "@/utils/base/utils";
@ -177,7 +177,6 @@ export default {
this.$cookies.set("username", data.userinfo.username, 3600 * 24 * 30);
this.$cookies.set("first_name", data.userinfo.first_name, 3600 * 24 * 30);
this.$store.dispatch("doUserinfo", data.userinfo);
set_auth_token();
this.$router.push({name: 'FirApps'})
},
loop_get_wx_info(wx_login_ticket, c_count = 1, unique_key = getRandomStr()) {
@ -291,7 +290,7 @@ export default {
});
return
}
if (password.length > 6) {
if (password.length >= 6) {
let params = {
"username": email,
"password": password,

@ -8,33 +8,37 @@
</div>
<div class="badges">
<el-tooltip content="复制到剪切板" placement="top">
<span v-clipboard:copy="short_full_url"
<span v-clipboard:copy="appinfos.preview_url"
v-clipboard:success="copy_success"
class="bundleid short"
>&nbsp;{{ short_full_url }}</span>
>&nbsp;{{ appinfos.preview_url }}</span>
</el-tooltip>
<el-tooltip content="点击查看下载码">
<el-popover
placement="right"
width="288">
<div style="text-align: center; margin: 0">
<vue-qr ref="qr"
:correctLevel="qrinfo.correctLevel" :logoCornerRadius="qrinfo.logoCornerRadius"
:logoScale="qrinfo.logoScale"
:logoSrc="icon_url"
:margin="qrinfo.margin" :size="266"
:text="appinfos.preview_url">
</vue-qr>
<el-button size="small" type="primary" @click="save_qr()">保存本地</el-button>
</div>
<span slot="reference" class="short ng-scope">下载码</span>
</el-popover>
</el-tooltip>
<el-popover
placement="right"
width="288">
<div style="text-align: center; margin: 0">
<vue-qr ref="qr"
:correctLevel="qrinfo.correctLevel" :logoCornerRadius="qrinfo.logoCornerRadius"
:logoScale="qrinfo.logoScale"
:logoSrc="icon_url"
:margin="qrinfo.margin" :size="266"
:text="short_url(appinfos)">
</vue-qr>
<el-button size="small" type="primary" @click="save_qr()">保存本地</el-button>
</div>
<span slot="reference" class="short ng-scope">下载码</span>
</el-popover>
<span>{{ master_release.release_type |getapptype }}</span>
<el-tooltip content="下载量" placement="top">
<span><i class="el-icon-cloudy"/><b class="short">{{ appinfos.count_hits }}</b></span>
<el-tooltip placement="top">
<div slot="content">总下载次数{{ appinfos.count_hits }}<br/>当天下载次数{{ appinfos.today_hits_count }}</div>
<span><i class="el-icon-cloudy"/><b class="short">{{
appinfos.count_hits
}} &nbsp;|&nbsp; {{ appinfos.today_hits_count }} </b></span>
</el-tooltip>
<span class="bundleid ng-binding">BundleID<b class="ng-binding">
<el-tooltip content="复制到剪切板" placement="top">
@ -123,13 +127,12 @@ export default {
margin: 20
},
icon_url: "",
appinfos: {status: 1},
appinfos: {status: 1, preview_url: ''},
master_release: {},
allapp: [],
activity: {
editing: false
},
short_full_url: '',
}
},
methods: {
@ -149,9 +152,6 @@ export default {
//
a.dispatchEvent(new MouseEvent('click'))
},
short_url(appinfo) {
return appinfo.preview_url + '/' + appinfo.short;
},
setfunactive(item, index) {
for (let key in this.$refs) {
if (key === "qr") continue;
@ -168,7 +168,7 @@ export default {
}
},
appDownload(appinfo) {
window.open(this.short_url(appinfo), '_blank', '');
window.open(appinfo.preview_url, '_blank', '');
},
defaulttimeline() {
this.setfunactive('timeline', 5);
@ -219,7 +219,6 @@ export default {
this.master_release = data.data.master_release;
this.appinfos["icon_url"] = this.master_release.icon_url;
this.$store.dispatch('doucurrentapp', this.appinfos);
this.short_full_url = this.appinfos.preview_url + "/" + this.appinfos.short;
} else if (data.code === 1003) {
this.$router.push({name: 'FirApps'});
} else {

@ -1,5 +1,6 @@
<template>
<div style="margin-top: 20px;width: 66%;margin-left: 8%">
<el-dialog
:close-on-click-modal="false"
:close-on-press-escape="false"
@ -24,6 +25,25 @@
title="下载授权码生成"
width="500px">
<el-dialog
:visible.sync="addUdidVisible"
append-to-body
center
title="添加设备udid授权"
width="500px">
<el-tag style="margin: 10px 0">可以输入多个udid,以空格或者回车分割</el-tag>
<el-input
v-model="makeTokenInfo.udids"
:rows="17"
placeholder="可以输入多个udid,以空格或者回车分割"
type="textarea">
</el-input>
<span slot="footer">
<el-button @click="addUdidVisible=false,makeTokenInfo.udids=''">取消</el-button>
<el-button @click="addUididFunc">确定</el-button>
</span>
</el-dialog>
<el-form ref="form" :model="makeTokenInfo" label-width="110px">
<el-form-item label="指定授权码" style="width: 400px">
<el-tag>当存在指定授权码授权码长度和生成数量被禁用</el-tag>
@ -63,6 +83,12 @@
</el-tooltip>
<el-tag style="margin-left: 5px">绑定设备之后二次自动进行设备授权下载应用</el-tag>
</el-form-item>
<el-form-item v-if="currentapp.type===1 && currentapp.issupersign && makeTokenInfo.bind_status===true"
label="添加授权设备">
<el-button size="mini" @click="addudid">
添加
</el-button>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="cancelDownloadToken">取消</el-button>
@ -109,12 +135,26 @@
prop="token"
>
<template slot-scope="scope">
<el-tooltip content="点击复制">
<el-link v-clipboard:copy="format_copy_text(scope.row.token)" v-clipboard:success="copy_success"
:underline="false">
{{ scope.row.token }}
</el-link>
</el-tooltip>
<el-popover
placement="top-start"
trigger="hover"
width="200">
<p>
<el-tag v-clipboard:copy="scope.row.token" v-clipboard:success="copy_success" type="success">
<el-link :underline="false">点击复制授权码</el-link>
</el-tag>
</p>
<p>
<el-tag v-clipboard:copy="format_copy_text(scope.row.token)" v-clipboard:success="copy_success">
<el-link :underline="false">点击复制授权码下载链接</el-link>
</el-tag>
</p>
<el-link slot="reference" :underline="false">{{ scope.row.token }}</el-link>
</el-popover>
</template>
</el-table-column>
@ -140,7 +180,7 @@
align="center"
label="最大可用次数"
prop="max_limit_count"
width="130">
width="110">
<template slot-scope="scope">
<span v-if="scope.row.max_limit_count === 0">不限次数</span>
<span v-else>{{ scope.row.max_limit_count }}</span>
@ -161,16 +201,24 @@
align="center"
label="设备ID"
prop="bind_udid">
<template slot-scope="scope">
<el-tooltip content="点击复制到剪贴板">
<el-link v-clipboard:copy="scope.row.bind_udid"
v-clipboard:success="copy_success"
:underline="false">{{ scope.row.bind_udid }}
</el-link>
</el-tooltip>
</template>
</el-table-column>
</div>
<el-table-column
align="center"
fixed="right"
label="操作"
width="120">
width="90">
<template slot-scope="scope">
<div>
<el-tooltip content="重置使用次数" placement="top">
<el-tooltip :content="get_reset_title()" placement="top">
<el-button
size="mini"
@click="resetDownloadUsed(scope.row)">重置
@ -313,18 +361,44 @@ export default {
app_download_token_list: [],
loading: false,
makeTokenVisible: false,
addUdidVisible: false,
tokendisable: false,
pagination: {"currentPage": 1, "total": 0, "pagesize": 10},
dpwdsearch: '',
dpwdtitle: '输入下载授权码',
makeTokenInfo: {token: '', token_length: 6, token_number: 20, token_max_used_number: 0, bind_status: false},
multipleSelection: []
makeTokenInfo: {
token: '',
token_length: 6,
token_number: 20,
token_max_used_number: 0,
bind_status: false,
udids: ''
},
multipleSelection: [],
}
},
methods: {
addUididFunc() {
let udids = this.makeTokenInfo.udids
if (udids && udids.length > 10) {
this.makeDownloadToken()
} else {
this.$message.error('设备udid不存在,请检查')
}
},
get_reset_title() {
let title = '重置使用次数'
if (this.currentapp.type === 1 && this.currentapp.issupersign) {
title = '重置使用次数和授权设备ID'
}
return title
},
addudid() {
this.makeTokenInfo.udids = ''
this.addUdidVisible = true
},
format_copy_text(token) {
return this.currentapp.preview_url + "/" + this.currentapp.short + "?password=" + token
return this.currentapp.preview_url + "?password=" + token
},
copy_success() {
this.$message.success('复制剪切板成功');
@ -355,8 +429,16 @@ export default {
this.multipleSelection = val;
},
cancelDownloadToken() {
this.makeTokenInfo = {token: '', token_length: 6, token_number: 20, token_max_used_number: 0, bind_status: false}
this.makeTokenInfo = {
token: '',
token_length: 6,
token_number: 20,
token_max_used_number: 0,
bind_status: false,
udids: ''
}
this.makeTokenVisible = false
this.addUdidVisible = false
},
resetDownloadUsed(info) {
appDownloadToken(data => {

@ -4,7 +4,7 @@
:color="app.master_color"
:timestamp="app.created_time|formatdatatimeline"
placement="top">
<el-card>
<el-card :body-style="{border: app.is_master ? '#2abb9d 1px solid' : ''}">
<div class="directive-view-release">
<i v-if="! app.is_master" class="el-icon-cloudy"/>
@ -80,7 +80,7 @@
<el-button v-if="!currentapp.issupersign" class="tooltip-top" @click="previewRelease(app)"><i
class="el-icon-view"/> <span
class="ng-binding">预览</span>
class="ng-binding">预览该版本</span>
</el-button>
<el-button v-if="! app.is_master" class="tooltip-top" @click="make_master_release(app)">
@ -142,12 +142,7 @@ export default {
);
},
previewRelease(app) {
let routeData = this.$router.resolve({
name: 'FirDownload',
params: {short: this.currentapp.short},
query: {release_id: app.release_id}
});
window.open(this.currentapp.preview_url + routeData.href, 'target', '');
window.open(this.currentapp.preview_url + '?release_id=' + app.release_id, '_blank', '');
},
getAppTimelineFun(act = '') {
const loading = this.$loading({

@ -222,7 +222,7 @@
<el-row :gutter="20" style="margin-top: 10px;">
<el-col :span="18">
<el-input v-model="short">
<template slot="prepend">{{ analyseappinfo.short_domain_name }}/</template>
<template slot="prepend">{{ analyseappinfo.short_domain_name }}</template>
</el-input>
</el-col>
</el-row>
@ -491,7 +491,10 @@
<div class=" app-animator">
<div class="card app card-ios">
<i class="down-icon el-icon-cloudy"> {{ r.count_hits }}</i>
<el-tooltip>
<div slot="content">总下载次数{{ r.count_hits }}<br/>当天下载次数{{ r.today_hits_count }}</div>
<i class="down-icon el-icon-cloudy"> {{ r.count_hits }} &nbsp;|&nbsp; {{ r.today_hits_count }}</i>
</el-tooltip>
<i v-if="r.type === 1" class="type-icon iconfont icon-ios"/>
<i v-if="r.type === 0" class="type-icon iconfont icon-android2"/>
@ -1161,7 +1164,7 @@ export default {
this.$router.push({name: 'FirAppInfostimeline', params: {id: app.app_id}})
},
appDownload(app) {
window.open(app.preview_url + '/' + app.short, '_blank', '');
window.open(app.preview_url, '_blank', '');
}
}, computed: {
getDelappTitle() {

@ -3,8 +3,8 @@
<div>
<div style="text-align: center">
<el-transfer
id="transfer"
v-model="choices_data"
v-loading="loadings"
:button-texts="['', '']"
:data="app_developer_lists"
:format="{
@ -93,6 +93,7 @@
<script>
import {developerBindAppFun} from "@/restful";
import {sort_compare} from "@/utils";
import {Loading} from "element-ui";
export default {
name: 'AppleDeveloperBindApp',
@ -120,8 +121,7 @@ export default {
s_props: {},
s_titles: [],
app_private_used_number: 100,
loadings: false,
app_limit_number: 0
app_limit_number: 0,
};
},
mounted() {
@ -147,14 +147,12 @@ export default {
},
handleChange(value, direction, movedKeys) {
if (this.issuer_id && direction === 'right') {
// if(value.length > this.app_limit_number){
// this.$message.warning(','+this.app_limit_number+'')
for (let i = 0; i < movedKeys.length; i++) {
let app_info = this.get_choice_data_from_key([movedKeys[i]])[0][0]
if (!(app_info && app_info.app_used_number > 0)) {
this.choices_data.splice(this.choices_data.indexOf(movedKeys[i]), 1)
}
}
//
// for (let i = 0; i < movedKeys.length; i++) {
// let app_info = this.get_choice_data_from_key([movedKeys[i]])[0][0]
// if (!(app_info && app_info.app_used_number > 0)) {
// this.choices_data.splice(this.choices_data.indexOf(movedKeys[i]), 1)
// }
// }
}
if (this.app_id && direction === 'right') {
@ -221,7 +219,7 @@ export default {
})
},
getBindInfo(params) {
this.loadings = true
let loadingInstance = Loading.service({target: '#transfer'})
if (this.issuer_id) {
params.issuer_id = this.issuer_id
}
@ -250,7 +248,9 @@ export default {
} else {
this.$message.error("数据获取失败" + data.msg)
}
this.loadings = false
this.$nextTick(() => { // Loading
loadingInstance.close()
});
}, {methods: 'GET', data: params})
},
format_data(data) {

@ -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 {

@ -15,9 +15,13 @@
<el-divider direction="vertical"/>
<div class="item"><p style="margin-bottom: 16px">应用举报</p>
</div>
<p><b>flyapps@126.com</b></p>
<el-tooltip content="拿出手机扫描添加微信" style="margin-top: 33px">
<el-image :lazy="true" :src="require('@/assets/wxchat.jpg')" alt="扫描添加微信" style="width: 160px">
<div>
<el-link :href="github" :underline="false"><p><b>FlyApps</b></p></el-link>
</div>
<el-divider/>
<el-tooltip content="拿出手机扫一扫" style="margin-top: 33px">
<el-image :alt="github" :lazy="true" :src="require('@/assets/github.png')"
style="width: 160px">
</el-image>
</el-tooltip>
@ -30,7 +34,8 @@ export default {
name: "FirContact",
data() {
return {
bg_img: require("@/assets/imgs/banner_1.jpg")
bg_img: require("@/assets/imgs/banner_1.jpg"),
github: process.env.base_env.github
}
},
}
@ -44,7 +49,7 @@ export default {
.content {
width: 942px;
margin: 80px auto 160px;
margin: 10px auto 160px;
text-align: center;
}

@ -114,12 +114,12 @@ export default {
{
logo: require("@/assets/imgs/qq.png"),
title: "核心优势3",
content: "<p>利用QQ微信等提供在线解答</p>帮助用户快速准确解决问题和故障"
content: "<p>QQ微信等提供在线解答</p>帮助用户快速准确解决问题"
},
{
logo: require("@/assets/imgs/skill.png"),
title: "核心优势4",
content: "<p>由技术支持工程师,负责问题解答</p>需求受理及故障受理"
content: "<p>自定义配置,容器化部署</p>升级维护超级便捷"
}
]
}

@ -21,10 +21,10 @@
<el-link @click="$router.push({name:'FirApps'})">登录</el-link>
</el-col>
<el-col :span="8">
<el-link @click="$router.push({name:'FirRegist'})">注册</el-link>
<el-link @click="$router.push({name:'FirContact'})">联系我们</el-link>
</el-col>
<el-col :span="8">
<el-link @click="$router.push({name:'FirContact'})">联系我们</el-link>
<el-link @click="goGithub">Github</el-link>
</el-col>
</el-row>
</el-col>
@ -64,6 +64,11 @@ export default {
'line-height': '45px',
background: '#e7eaf1'
},
github: process.env.base_env.github
}
}, methods: {
goGithub() {
window.open(this.github, '_blank', '');
}
}
}

@ -6,8 +6,8 @@
<p style="color:#b2b2b2">Company News</p>
</div>
<div class="nav text-center">
<a class="active" href="#">公司新闻</a>
<a href="#">行业动态</a>
<a class="active" href="#">开发历程</a>
<a href="#">发展足迹</a>
</div>
<ul class="news-container">
<li v-for="(item,index) in newsList" :key="index">
@ -40,6 +40,18 @@ export default {
show: false,
newsList: [
{
id: '008',
title: 'ios16系统',
introduce: '超级签名字支持ios16系统签名,只需要手机开启调试模式,即可安装打开',
date: '09-22',
year: '2022'
}, {
id: '007',
title: '私有下载页部署',
introduce: '支持私有下载页部署,方便快捷,稳定访问',
date: '05-18',
year: '2022'
}, {
id: '001',
title: '现有版本',
introduce: '经过好长时间的编写,平台已经支持超级签名,权限管理,自定义域名,自定义存储',

@ -521,9 +521,10 @@
{{ developer_used_info.may_sign_number - developer_used_info.can_sign_number }}
</el-link>
<el-link :underline="false">当前可签名设备数 {{ developer_used_info.can_sign_number }}</el-link>
<el-link :underline="false">当前非苹果手机设备数 {{ developer_used_info.device_class_count }}</el-link>
</div>
<el-link slot="reference" :underline="false">
还剩{{ developer_used_info.can_sign_number }} 可用
还剩{{ developer_used_info.can_sign_number + developer_used_info.device_class_count }} 可用
</el-link>
</el-popover>
<el-progress
@ -589,7 +590,10 @@
:underline="false">开发者账户ID: {{ scope.row.issuer_id }}
</el-link>
</el-tooltip>
<p>开发者账户已使用设备数: {{ scope.row.developer_used_number }}</p>
<p>开发者账户已使用苹果手机设备数: {{ scope.row.developer_used_number }}</p>
<p>开发者账户已使用非苹果手机设备数: {{ scope.row.developer_other_device_used_number }}
<el-tag size="mini" type="warning">可忽略该设备数</el-tag>
</p>
<p>开发者账户可用设备数: {{ 100 - scope.row.developer_used_number }}</p>
<p>由于您设置可用设备数: {{ scope.row.usable_number }} ,所以现在可用设备数: {{
scope.row.usable_number - scope.row.developer_used_number > 0
@ -873,17 +877,22 @@
v-model="udidsearch"
clearable
placeholder="输入UDID"
style="width: 30%;margin-right: 30px;margin-bottom: 10px"/>
style="width: 30%;margin-right: 5px;margin-bottom: 10px"/>
<el-input
v-model="appidseach"
clearable
placeholder="输入开发者用户ID"
style="width: 30%;margin-right: 30px;margin-bottom: 10px"/>
<el-select v-if="status_choices" v-model="devicestatus" clearable multiple
style="width: 30%;margin-right: 5px;margin-bottom: 10px"/>
<el-select v-model="devicestatus" clearable multiple
placeholder="设备状态"
style="width: 120px;margin-right: 30px" @change="handleCurrentChange(1)">
style="width: 110px;margin-right: 5px" @change="handleCurrentChange(1)">
<el-option v-for="item in device_status_choices" :key="item.id" :label="item.name" :value="item.id"/>
</el-select>
<el-select v-model="deviceclass" clearable multiple
placeholder="设备类型"
style="width: 110px;margin-right: 5px" @change="handleCurrentChange(1)">
<el-option v-for="item in device_class_choices" :key="item.id" :label="item.name" :value="item.id"/>
</el-select>
<el-button icon="el-icon-search" type="primary" @click="handleCurrentChange(1)">
搜索
</el-button>
@ -936,7 +945,7 @@
align="center"
label="设备版本"
prop="version"
width="200">
width="180">
</el-table-column>
<el-table-column
align="center"
@ -958,6 +967,12 @@
</el-tooltip>
</template>
</el-table-column>
<el-table-column
align="center"
label="设备类型"
prop="device_class"
width="80">
</el-table-column>
<el-table-column
:formatter="deviceformatter"
align="center"
@ -1430,7 +1445,6 @@
<el-date-picker
v-model="timerangesearch"
:picker-options="pickerOptions"
align="right"
end-placeholder="结束日期"
range-separator="至"
start-placeholder="开始日期"
@ -1809,7 +1823,7 @@ import {
personalConfigInfo,
signoperatemessage
} from "@/restful";
import {format_choices, getUserInfoFun, removeAaary} from "@/utils";
import {format_choices, getUserInfoFun, removeArray} from "@/utils";
import AppleDeveloperBindApp from "@/components/base/AppleDeveloperBindApp";
import {format_time} from "@/utils/base/utils";
@ -1844,6 +1858,7 @@ export default {
Bundleidsearch: "",
appidseach: "",
devicestatus: "",
deviceclass: "",
operatestatus: "",
operate_status_choices: [],
uidsearch: "",
@ -1929,6 +1944,7 @@ export default {
appletoapp_title: '',
status_choices: [],
device_status_choices: [],
device_class_choices: [],
read_only_mode: 'off',
developer_status_choice: [],
multipleSelection: [],
@ -1963,7 +1979,7 @@ export default {
}
}, {
methods: 'PUT', data: {config_key: info.key, config_value: info.value}
})
}, 'super_sign')
},
updateSignConfig() {
personalConfigInfo(data => {
@ -1975,7 +1991,7 @@ export default {
}
}, {
methods: 'DELETE'
})
}, 'super_sign')
},
signConfigFun() {
personalConfigInfo(data => {
@ -1987,7 +2003,7 @@ export default {
}
}, {
methods: 'GET'
})
}, 'super_sign')
},
saveblackdevice() {
blackDeviceUtil(data => {
@ -2354,6 +2370,7 @@ export default {
"issuer_id": this.appidseach,
"act": "syncalldevice",
"devicestatus": this.devicestatus,
"deviceclass": this.deviceclass,
"udidsearch": this.udidsearch
}
});
@ -2525,6 +2542,7 @@ export default {
this.iosdevicerankFun({"methods": "GET", "data": data})
} else if (tabname === "iosudevices") {
data.devicestatus = JSON.stringify(this.devicestatus);
data.deviceclass = JSON.stringify(this.deviceclass);
this.iosudevicesFun("GET", data)
} else if (tabname === 'operatemsg') {
data.operate_status = this.operatestatus;
@ -2731,7 +2749,7 @@ export default {
this.pagination.total = data.count;
} else if (action === "DELETE") {
if (scope) {
this.app_udid_lists = removeAaary(this.app_udid_lists, scope.row)
this.app_udid_lists = removeArray(this.app_udid_lists, scope.row)
}
} else if (action === 'POST') {
this.$message.success("操作成功");
@ -2796,6 +2814,7 @@ export default {
this.developer_udevices_lists = data.data;
this.pagination.total = data.count;
this.device_status_choices = data.status_choices;
this.device_class_choices = data.device_class_choices;
} else {
this.refreshactiveFun()
}

@ -48,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"
@ -77,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
@ -101,7 +126,6 @@
</template>
</el-table-column>
<el-table-column
align="center"
label="跳转权重"
@ -154,7 +178,7 @@
<script>
import {domaininfo} from "@/restful";
import {domaininfo, personalConfigInfo} from "@/restful";
import BindDomain from "@/components/base/BindDomain";
import {format_choices, getUserInfoFun} from '@/utils'
import {format_time} from "@/utils/base/utils";
@ -172,9 +196,71 @@ export default {
domain_type_choices: [],
current_domain_info: {'domain_type': 1, 'app_id': null},
loading: false,
configVisible: false,
config_lists: [],
short_download_uri: ''
}
},
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) {
if (data.data.length === 1) {
this.short_download_uri = data.data[0].value
}
} 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')
},
saveWeight(domain_info) {
domaininfo(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>

@ -56,7 +56,7 @@
:logoSrc="appinfo.master_release.icon_url"
:margin="qrinfo.margin" :qid="appinfo.app_id"
:size="200"
:text="short_url(appinfo)">
:text="appinfo.preview_url">
</vue-qr>
<div style="margin: 5px 0 5px">
<i v-if="appinfo.type === 1" class=" type-icon iconfont icon-ios"/>
@ -65,9 +65,9 @@
<span>{{ appinfo.name }}</span>
<div class="bottom clearfix">
<el-popover placement="top" trigger="hover">
<span v-clipboard:copy="short_url(appinfo)"
<span v-clipboard:copy="appinfo.preview_url"
v-clipboard:success="copy_success"
>{{ short_url(appinfo) }}</span>
>{{ appinfo.preview_url }}</span>
<div slot="reference" class="name-wrapper">
<el-button plain size="small" type="primary" @click="go_download(appinfo)">预览</el-button>
<el-button plain size="small" type="primary" @click="save_qr(appinfo)">保存本地</el-button>
@ -213,15 +213,12 @@ export default {
this.isIndeterminate = checkedCount > 0 && checkedCount < this.allQrcodeAppid.length;
},
go_download(appinfo) {
window.open(this.short_url(appinfo), '_blank', '');
window.open(appinfo.preview_url, '_blank', '');
},
qrback(dataUrl, id) {
this.qrcode_img_info[id] = dataUrl;
this.allQrcodeAppid.push(id);
},
short_url(appinfo) {
return appinfo.preview_url + '/' + appinfo.short;
},
save_qr(appinfo) {
let dtype = "I";
if (appinfo.master_release.release_type === 0) {

@ -1,11 +1,18 @@
import Axios from 'axios'
import {geetestbase} from "@/utils/base/utils";
Axios.defaults.withCredentials = true;
const DOMAIN = process.env.base_env.baseShortUrl;
const APIPATH = '/api/v1/fir/server';
let USERSEVER = DOMAIN + APIPATH;
const USERSEVER = 'api/v1/fir/server';
// eslint-disable-next-line no-console
console.log("js build version:" + process.env.base_env.version);
// create an axios instance
const service = Axios.create({
baseURL: DOMAIN, // url = base url + request url
withCredentials: true, // send cookies when cross-domain requests
timeout: 120000 // request timeout
})
function ErrorMsg(error, callBack) {
if (error && error.response) {
@ -42,7 +49,7 @@ function responseMiddleware(data, callBack) {
function getData(methods = 'GET', url, params = {}, callBack) {
if (methods === "PUT") {
Axios
service
.put(url, params)
.then(function (response) {
responseMiddleware(response.data, callBack);
@ -51,7 +58,7 @@ function getData(methods = 'GET', url, params = {}, callBack) {
ErrorMsg(error, callBack);
});
} else if (methods === 'POST') {
Axios
service
.post(url, params)
.then(function (response) {
responseMiddleware(response.data, callBack);
@ -60,7 +67,7 @@ function getData(methods = 'GET', url, params = {}, callBack) {
ErrorMsg(error, callBack);
});
} else
Axios
service
.get(url, {params: params})
.then(function (response) {
responseMiddleware(response.data, callBack);

@ -4,8 +4,8 @@ import router from "@/router";
import {geetestbase} from "@/utils/base/utils";
const Base64 = require('js-base64').Base64;
Axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
Axios.defaults.withCredentials = true;
// Axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
// Axios.defaults.withCredentials = true;
// Axios.defaults.httpsAgent = new https.Agent({
// keepAlive: true
// });
@ -16,11 +16,17 @@ export function requestAbort() {
}
// eslint-disable-next-line no-console
console.log("flyapps js version:" + process.env.base_env.version);
console.log("js build version:" + process.env.base_env.version);
const DOMAIN = process.env.base_env.baseUrl;
const APIPATH = '/api/v1/fir/server';
let USERSEVER = DOMAIN + APIPATH;
const USERSEVER = 'api/v1/fir/server';
// create an axios instance
const service = Axios.create({
baseURL: DOMAIN, // url = base url + request url
withCredentials: true, // send cookies when cross-domain requests
timeout: 120000 // request timeout
})
export function convertRes2Blob(response) {
// 提取文件名
@ -53,31 +59,25 @@ export function convertRes2Blob(response) {
}
}
export function set_auth_token() {
Axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
if (VueCookies.get('token')) {
// Axios.defaults.headers.common['Authorization'] = localStorage.getItem('access_token');
// console.log(config.headers);
if (VueCookies.get('auth_token')) {
config.headers.Authorization = VueCookies.get('auth_token')
} else {
let token = VueCookies.get('token');
let username = VueCookies.get('username');
VueCookies.set("auth_token", Base64.encode(token + ':' + username), 3600 * 24 * 30);
}
service.interceptors.request.use(config => {
if (VueCookies.get('token')) {
// Axios.defaults.headers.common['Authorization'] = localStorage.getItem('access_token');
// console.log(config.headers);
if (!VueCookies.get('auth_token')) {
let token = VueCookies.get('token');
let username = VueCookies.get('username');
VueCookies.set("auth_token", Base64.encode(token + ':' + username), 3600 * 24 * 30);
}
// 更改加载的样式
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
}
config.headers.Authorization = VueCookies.get('auth_token')
}
return config;
}, function (error) {
// 对请求错误做些什么
// eslint-disable-next-line no-console
console.log(error)
return Promise.reject(error);
});
set_auth_token();
function ErrorMsg(error, callBack) {
if (error && error.response) {
@ -141,7 +141,7 @@ function responseMiddleware(data, callBack) {
function getData(methods, url, params = {}, callBack) {
if (methods === "DELETE") {
Axios
service
.delete(url, {params: params, signal: controller.signal})
.then(function (response) {
responseMiddleware(response.data, callBack);
@ -151,7 +151,7 @@ function getData(methods, url, params = {}, callBack) {
});
} else if (methods === "PUT") {
Axios
service
.put(url, params, {signal: controller.signal})
.then(function (response) {
responseMiddleware(response.data, callBack);
@ -161,7 +161,7 @@ function getData(methods, url, params = {}, callBack) {
});
} else if (methods === 'POST') {
Axios
service
.post(url, params, {signal: controller.signal})
.then(function (response) {
responseMiddleware(response.data, callBack);
@ -170,7 +170,7 @@ function getData(methods, url, params = {}, callBack) {
ErrorMsg(error, callBack);
});
} else if (methods === 'FILE') {
Axios
service
.get(url, {params: params, responseType: 'blob', signal: controller.signal})
.then(function (response) {
convertRes2Blob(response)
@ -179,7 +179,7 @@ function getData(methods, url, params = {}, callBack) {
ErrorMsg(error, callBack);
});
} else {
Axios
service
.get(url, {params: params, signal: controller.signal})
.then(function (response) {
responseMiddleware(response.data, callBack);
@ -447,10 +447,10 @@ export function getapppicurl(app_id) {
}
export function getuploadurl(domain_name = null) {
if (domain_name) {
return domain_name + APIPATH + '/upload'
if (domain_name && !domain_name.match('http[s?]:///$')) {
return domain_name + '/' +USERSEVER + '/upload'
} else {
return USERSEVER + '/upload';
return DOMAIN + USERSEVER + '/upload';
}
}
@ -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) {
@ -692,10 +703,10 @@ export function notifyConfigInfo(callBack, params) {
}
/**消息配置 */
export function personalConfigInfo(callBack, params) {
export function personalConfigInfo(callBack, params, super_sign) {
getData(
params.methods,
USERSEVER + '/personal/config',
USERSEVER + '/personal/config?config_type=' + super_sign,
params.data,
data => {
callBack(data);
@ -711,7 +722,7 @@ export function geetest(self, uid, params, callback) {
/** 超级签名************************************************相关api */
let SIGNSEVER = DOMAIN + '/api/v1/fir/xsign';
let SIGNSEVER = '/api/v1/fir/xsign';
/** 超级签名--检测该账户是否可开启超级签名 */
export function checkCanSign(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',

@ -41,13 +41,13 @@ export function geetestbase(func, self, uid, params, callback, errback, readybac
challenge: data.challenge,
new_captcha: data.new_captcha, // 用于宕机时表示是新验证码的宕机
offline: !data.success, // 表示用户后台检测极验服务器是否宕机,一般不需要关注
product: "float", // 产品形式,包括:float,popup
product: "bind", // 产品形式,包括:float,popup
width: "100%"
}, (captchaObj) => {
self.$refs.captcha.innerHTML = '';
captchaObj.appendTo("#captcha");
// captchaObj.appendTo("#captcha");
captchaObj.onReady(() => {
readyback()
readyback(captchaObj)
}).onSuccess(() => {
params.geetest = captchaObj.getValidate();
callback(params);

@ -0,0 +1,22 @@
module.exports =function dateFormat(format, date=null) {
if(!date){
date = new Date()
}
const args = {
"M+": date.getMonth() + 1,
"d+": date.getDate(),
"h+": date.getHours(),
"m+": date.getMinutes(),
"s+": date.getSeconds(),
"q+": Math.floor((date.getMonth() + 3) / 3),
"S": date.getMilliseconds()
};
if (/(y+)/.test(format))
format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
for (let i in args) {
let n = args[i];
if (new RegExp("(" + i + ")").test(format))
format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? n : ("00" + n).substr(("" + n).length));
}
return format;
}

@ -242,7 +242,7 @@ export function uploadlocalstorage(file, certinfo, app, successcallback, process
}
export function removeAaary(_arr, _obj) {
export function removeArray(_arr, _obj) {
let length = _arr.length;
for (let i = 0; i < length; i++) {
if (_arr[i] == _obj) {
@ -356,8 +356,8 @@ export function geetest(self, uid, params, callback, http_func = loginFun) {
message: res.msg,
type: 'error'
});
// eslint-disable-next-line no-unused-vars
}, _ => {
}, captchaObj => {
captchaObj.verify()
loading.close()
})
}

@ -1,15 +1,14 @@
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV);
const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const dateFormat = require("./src/utils/format.js");
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const argv = process.argv;
function resolve(dir) {
return path.join(__dirname, dir);
}
const argv = process.argv;
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const compress = new CompressionWebpackPlugin(
{
filename: info => {
@ -19,7 +18,7 @@ const compress = new CompressionWebpackPlugin(
threshold: 10240,
test: new RegExp(
'\\.(' +
['js','css'].join('|') +
['js', 'css'].join('|') +
')$'
),
minRatio: 0.8,
@ -28,6 +27,7 @@ const compress = new CompressionWebpackPlugin(
);
const index = {
outputDir: 'dist_index',
// page 的入口
entry: 'src/main.js',
// 模板来源
@ -39,10 +39,11 @@ const index = {
title: 'Fly分发平台',
// 在这个页面中包含的块,默认情况下会包含
// 提取出来的通用 chunk 和 vendor chunk。
chunks: ['chunk-vendors', 'chunk-common', 'index', 'elementui','runtime', 'alioss', 'qiniujs', 'qrcodejs2']
chunks: ['chunk-vendors', 'chunk-common', 'index', 'elementui', 'runtime', 'alioss', 'qiniujs', 'qrcodejs2']
};
// eslint-disable-next-line no-unused-vars
const mshort = {
outputDir: 'dist_mshort',
// page 的入口
entry: 'src/main.short.js',
// 模板来源
@ -57,6 +58,7 @@ const mshort = {
chunks: ['chunk-vendors', 'chunk-common', 'mshort', 'chunk-elementUI', 'runtime']
};
const short = {
outputDir: 'dist_short',
// page 的入口
entry: 'src/short.js',
// 模板来源
@ -71,41 +73,57 @@ const short = {
chunks: ['chunk-vendors', 'chunk-common', 'short', 'chunk-commons', 'runtime']
};
let pages = {index, short, mshort};
const page = argv[3];
if (page) {
for (const key of Object.keys(pages)) {
if (key === page) {
const tmp = pages[key];
pages = {};
pages[key] = tmp;
}
}
let outputDir = 'dist'
let page = argv[3];
if (!page) {
page = 'index'
}
const version='2.3.8';
for (const key of Object.keys(pages)) {
if (key === page) {
const tmp = pages[key];
pages = {};
pages[key] = tmp;
outputDir = tmp.outputDir
}
}
const version = '2.3.9';
const github = 'https://github.com/nineaiyu/FlyApps';
const pro_base_env = {
baseUrl: 'https://flyapps.cn',
index_static: 'https://static.flyapps.cn/index/',
baseShortUrl: 'https://flyapps.top',
short_static: 'https://static.flyapps.top/short/',
baseUrl: '/', //该选项可以填写web-api的域名,类似 https://api.xxx.com/
index_static: '/', //若配置cdn等加速,可以填写cdn加速域名
baseShortUrl: '/', //该选项可以填写short-api的域名,也可以和web-api域名一样,类似 https://api.xxx.com/
short_static: '/short/', //若配置cdn等加速,可以填写cdn加速域名
version: version,
};
const dev_base_env = {
baseUrl: 'https://app.hehelucky.cn',
baseShortUrl: 'https://app.hehelucky.cn',
short_static: '/',
baseUrl: 'https://app.hehelucky.cn/',
index_static: '/',
baseShortUrl: 'https://app.hehelucky.cn/',
short_static: 'https://app.hehelucky.cn/short/',
version: version,
};
let base_evn = dev_base_env;
const footer_info = {
copyright: 'Copyright © 2022-2099 isummer 版权所有.',
ipcBeiAn: {
url: 'https://beian.miit.gov.cn',
text: '京ICP备681262896号',
},
gongAnBeiAn: {
url: 'https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=681262896',
text: '京公网安备681262896号',
},
}
if (IS_PROD) {
base_evn = pro_base_env
} else {
base_evn = dev_base_env
}
base_evn.footer_info = footer_info
function get_public_path(pages) {
if (!IS_PROD) {
@ -118,9 +136,9 @@ function get_public_path(pages) {
}
let publicPath = get_public_path(pages);
module.exports = {
pages: pages,
outputDir: outputDir,
productionSourceMap: false, //去除生产环境的productionSourceMap
assetsDir: "static", //静态文件存储位置
lintOnSave: true,
@ -141,7 +159,13 @@ module.exports = {
config
.plugin('define')
.tap(args => {
args[0]['process.env']['base_env'] = JSON.stringify({baseUrl: base_evn.baseUrl,baseShortUrl:base_evn.baseShortUrl,version:base_evn.version});
args[0]['process.env']['base_env'] = JSON.stringify({
baseUrl: base_evn.baseUrl,
baseShortUrl: base_evn.baseShortUrl,
version: dateFormat("yyyyMMdd.hhmmss") + '.' + base_evn.version,
footer: base_evn.footer_info,
github
});
return args
});
@ -230,7 +254,11 @@ module.exports = {
priority: 23, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?ali-oss(.*)/ // in order to adapt to cnpm
},
appinfo: {
name: 'appinfo', // split elementUI into a single package
priority: 23, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?app-info-parser(.*)/ // in order to adapt to cnpm
},
qrcodejs2: {
name: 'qrcodejs2', // split elementUI into a single package
priority: 23, // the weight needs to be larger than libs and app or it will be packaged into libs or app
@ -243,7 +271,7 @@ module.exports = {
},
common: {
name: 'chunk-commons',
test: resolve(resolve('src/components')), // can customize your rules
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true

@ -1,41 +1,32 @@
FROM python:3.6.14-slim
FROM python:3.9.14-slim
# Fixes some weird terminal issues such as broken clear / CTRL+L
ARG PIP_MIRROR=https://mirrors.aliyun.com/pypi/simple
RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt update
#RUN sed -i 's/deb.debian.org/mirrors.163.com/g' /etc/apt/sources.list \
# && sed -i 's/security.debian.org/mirrors.163.com/g' /etc/apt/sources.list \
RUN apt update \
&& apt-get install g++ wget zip unzip -y \
&& apt-get install libssl-dev openssl libmariadb-dev -y \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get install g++ wget -y
RUN apt-get install libssl-dev openssl libmariadb-dev -y
RUN rm -rf /var/lib/apt/lists/*
#COPY fir_client/dist /data/flyapps/fir_client
#RUN cd /opt/ && wget https://github.com/nineaiyu/zsign/archive/refs/tags/v1.1.2.tar.gz
COPY zsign-1.1.2.tar.gz /opt/zsign-1.1.2.tar.gz
RUN cd /opt/ && tar xvf zsign-1.1.2.tar.gz && cd zsign-1.1.2/ && g++ *.cpp common/*.cpp -lcrypto -O3 -std=c++11 -o zsign && cp zsign /usr/bin/
ADD zsign-1.1.2.tar.gz /opt/
RUN cd /opt/zsign-1.1.2/ && g++ *.cpp common/*.cpp -lcrypto -O3 -std=c++11 -o zsign && cp zsign /usr/bin/ && rm -rf /opt/zsign-1.1.2/
# install pip
COPY requirements.txt /opt/requirements.txt
RUN cd /opt/ && pip install -U setuptools pip -i ${PIP_MIRROR} --ignore-installed && pip install --no-cache-dir -r requirements.txt -i ${PIP_MIRROR} && pip install --no-cache-dir uwsgi -i ${PIP_MIRROR}
RUN rm -rf /var/cache/yum/
RUN cd /opt/ && pip install -U setuptools pip --ignore-installed && pip install --no-cache-dir -r requirements.txt && pip install --no-cache-dir uwsgi
#COPY Docker/flyapps.conf /etc/supervisor/conf.d/flyapps.conf
#COPY Docker/uwsgi.conf /data/flyapps/fir_ser/uwsgi.conf
#COPY Docker/flyapps-vhost.conf /etc/nginx/conf.d/flyapps-vhost.conf
#COPY Docker/app.hehelucky.cn.pem /data/flyapps/app.hehelucky.cn.pem
#COPY Docker/app.hehelucky.cn.key /data/flyapps/app.hehelucky.cn.key
WORKDIR /data/fir_ser/
COPY entrypoint.sh entrypoint.sh
RUN addgroup --system --gid 101 nginx \
&& adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos "nginx user" --shell /bin/false --uid 101 nginx
#EXPOSE 443
#ENTRYPOINT ["./entrypoint.sh"]
#ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf", "-n"]
CMD ["python", "manage.py", "start", "all","-u","nginx","-usm","1"]
#CMD ["python", "manage.py", "start", "all","-u","nginx","-usm","1"]
ENTRYPOINT ["/bin/bash", "entrypoint.sh"]

@ -0,0 +1,11 @@
FROM python:3.9.14-slim
RUN pip install --upgrade pip --no-cache-dir && \
pip install setuptools-rust oss2 requests-toolbelt androguard requests --no-cache-dir
WORKDIR /opt/
COPY cli.py /opt/cli.py
CMD ["python", "cli.py"]

@ -136,11 +136,6 @@ class AdminAppsSerializer(AppsSerializer):
def get_release_count(self, obj):
return models.AppReleaseInfo.objects.filter(app_id=obj).count()
today_hits_count = serializers.SerializerMethodField()
def get_today_hits_count(self, obj):
return get_app_today_download_times([obj.app_id])
def update(self, instance, validated_data):
return super(AdminAppsSerializer, self).update(instance, validated_data)

@ -2,7 +2,7 @@
#
from django.conf import settings
from django.http import HttpResponse
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from proxy.views import proxy_view
from rest_framework.views import APIView

@ -9,6 +9,7 @@ import logging
from django.contrib import auth
from rest_framework.views import APIView
from api.models import UserInfo
from api.utils.auth.util import AuthInfo
from api.utils.serializer import UserInfoSerializer
from api.utils.utils import set_user_token
@ -81,3 +82,16 @@ class LoginUserView(APIView):
def get(self, request):
serializer = UserInfoSerializer(request.user, )
return ApiResponse(data=serializer.data)
def post(self, request):
data = request.data
user_id = data.get('user_id')
password = data.get('password')
if user_id and password and len(password) >= 6:
user_obj = UserInfo.objects.filter(pk=user_id).first()
if user_obj:
user_obj.set_password(password)
user_obj.save(update_fields=['password'])
serializer = UserInfoSerializer(user_obj, )
return ApiResponse(data=serializer.data)
return ApiResponse(code=1001, msg='重置密码失败,用户不存在')

@ -0,0 +1,14 @@
from django.core.management.base import BaseCommand
from common.utils.caches import add_user_ds
class Command(BaseCommand):
help = 'add user download times'
def add_arguments(self, parser):
parser.add_argument('uid', type=str, default='')
parser.add_argument('download_times', type=int, default=100)
def handle(self, *args, **options):
add_user_ds(options.get('uid', None), options.get('download_times', 100))

@ -102,7 +102,7 @@ class BaseActionCommand(BaseCommand):
parser.add_argument('-d', '--daemon', nargs="?", const=True)
parser.add_argument('-up', '--uwsgi_processes', type=int, nargs="?", default=get_sys_process_num())
parser.add_argument('-ut', '--uwsgi_threads', type=int, nargs="?", default=get_sys_thread_num())
parser.add_argument('-cn', '--celery_num', type=int, nargs="?", default=20)
parser.add_argument('-cn', '--celery_num', type=int, nargs="?", default=8)
parser.add_argument('-usm', '--uwsgi_socket_mode', nargs="?", const=True,
help='run to bind socket mode, default http mode, only uwsgi service')
parser.add_argument('-f', '--force', nargs="?", const=True)

@ -20,6 +20,15 @@ def get_user_id(uid):
...
def chown_dir_file(directory, u_gid, recursive=False):
if recursive:
for (dir_path, dir_names, filenames) in os.walk(directory):
for filename in filenames + dir_names:
os.chown(uid=u_gid, gid=u_gid, path=os.path.join(dir_path, filename))
else:
os.chown(uid=u_gid, gid=u_gid, path=directory)
class BaseService(object):
def __init__(self, **kwargs):
@ -144,12 +153,10 @@ class BaseService(object):
if u_gid is None:
u_gid = os.getuid()
if u_gid is not None:
for (dir_path, dir_names, filenames) in os.walk(LOG_DIR):
for filename in filenames + dir_names:
os.chown(uid=u_gid, gid=u_gid, path=os.path.join(dir_path, filename))
chown_dir_file(LOG_DIR, u_gid, recursive=True)
if self.name == 'uwsgi':
for dirs in ['files', 'supersign']:
os.chown(uid=u_gid, gid=u_gid, path=os.path.join(BASE_DIR, dirs))
chown_dir_file(os.path.join(BASE_DIR, dirs), u_gid, recursive=True)
else:
logging.error(f'uid: {self.uid} gid:{self.gid} is not exists')

@ -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)

@ -22,17 +22,18 @@ from xsign.utils.ctasks import auto_delete_ios_mobile_tmp_file
logger = logging.getLogger(__name__)
def clean_config_cache(key):
ConfigCacheBase().invalid_config_cache(key)
ConfigCacheBase(px='user').invalid_config_cache(key)
@app.task
def start_api_sever_do_clean():
# 启动服务的时候,同时执行下面操作,主要是修改配置存储的时候,需要执行清理,否则会出问题,如果不修改,则无需执行
logger.info("clean local storage cache")
get_local_storage(clean_cache=True)
check_bypass_status()
ConfigCacheBase().invalid_config_cache()
def clean_config_cache(key):
return ConfigCacheBase().invalid_config_cache(key)
clean_config_cache('*')
@app.task

@ -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"),

@ -117,13 +117,13 @@ def save_app_infos(app_tmp_filename, app_file_name, user_obj, app_info, bundle_i
app_obj.issupersign = issupersign
# appmobj.wxeasytype = False if user_obj.domain_name or appmobj.domain_name else True
app_obj.bundle_id = bundle_id
app_obj.save(update_fields=["short", "name", "bundle_id", "issupersign"])
app_obj.save(update_fields=["short", "name", "bundle_id", "issupersign", "updated_time"])
except Exception as e:
logger.error(f"save app info failed,app_obj:{app_obj} Exception:{e}")
app_obj.bundle_id = bundle_id
app_obj.issupersign = issupersign
app_obj.name = app_info["labelname"]
app_obj.save(update_fields=["name", "bundle_id", "issupersign"])
app_obj.save(update_fields=["name", "bundle_id", "issupersign", "updated_time"])
del_cache_response_by_short(app_obj.app_id)
MagicCacheData.invalid_cache(app_obj.app_id)
AppReleaseInfo.objects.filter(app_id=app_obj).update(is_master=False)

@ -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'))
@ -258,7 +259,7 @@ def check_app_access_token(app_id, access_token, only_check, udid):
return AppDownloadToken.objects.filter(app_id__app_id=app_id, bind_udid=udid).count()
@MagicCacheData.make_cache(60 * 60 * 24, key=lambda x: x.app_id)
@MagicCacheData.make_cache(timeout=60 * 60 * 24, key_func=lambda x: x.app_id)
def get_app_storage_used(app_obj):
binary_size_sum = 0
for release_obj in AppReleaseInfo.objects.filter(app_id=app_obj).all():
@ -319,3 +320,10 @@ def get_user_storage_obj(user_obj, default=False):
auth = storage.get('auth', {})
storage = AppStorage(user_id=user_obj, storage_type=storage_type, **auth)
return storage
def get_preview_short_config(user_obj, short):
if user_obj:
return f"{'#/' if UserConfig(user_obj).PREVIEW_ROUTE_HASH else ''}{short}"
else:
return f"{'#/' if Config.PREVIEW_ROUTE_HASH else ''}{short}"

@ -7,11 +7,12 @@ from rest_framework.exceptions import ValidationError
from api import models
from api.utils.apputils import bytes2human
from api.utils.modelutils import get_user_domain_name, get_app_domain_name, get_app_download_uri, get_user_storage_used
from api.utils.modelutils import get_user_domain_name, get_app_domain_name, get_app_download_uri, get_user_storage_used, \
get_preview_short_config
from common.base.baseutils import get_choices_dict, WeixinLoginUid
from common.cache.storage import AdPicShowCache
from common.core.sysconfig import Config, UserConfig
from common.utils.caches import get_user_free_download_times, get_user_cert_auth_status
from common.utils.caches import get_user_free_download_times, get_user_cert_auth_status, get_app_today_download_times
from common.utils.storage import Storage
from common.utils.token import make_token
@ -153,19 +154,7 @@ class AppsSerializer(serializers.ModelSerializer):
preview_url = serializers.SerializerMethodField()
def get_preview_url(self, obj):
return get_app_download_uri(None, obj.user_id, obj)
# private_developer_number = serializers.IntegerField(default=0)
# count = serializers.IntegerField(default=0)
#
# def get_private_developer_number(self, obj):
# return models.AppleDeveloperToAppUse.objects.filter(app_id=obj).count()
#
# private_developer_used_number = serializers.IntegerField(default=0)
#
# def get_private_developer_used_number(self, obj):
# return models.DeveloperDevicesID.objects.filter(app_id=obj,
# developerid__appledevelopertoappuse__app_id=obj).distinct().count()
return f"{get_app_download_uri(None, obj.user_id, obj)}/{get_preview_short_config(obj.user_id, obj.short)}"
domain_name = serializers.SerializerMethodField()
@ -177,15 +166,11 @@ class AppsSerializer(serializers.ModelSerializer):
def get_sign_type_choice(self, obj):
return get_choices_dict(obj.supersign_type_choices)
# supersign_used_number = serializers.IntegerField(default=0)
#
# def get_supersign_used_number(self, obj):
# return models.APPSuperSignUsedInfo.objects.filter(app_id=obj).all().count()
#
# developer_used_count = serializers.IntegerField(default=0)
#
# def get_developer_used_count(self, obj):
# return models.DeveloperAppID.objects.filter(app_id=obj).all().count()
count = serializers.IntegerField(default=0)
developer_used_count = serializers.IntegerField(default=0)
private_developer_number = serializers.IntegerField(default=0)
private_developer_used_number = serializers.IntegerField(default=0)
supersign_used_number = serializers.IntegerField(default=0)
screenshots = serializers.SerializerMethodField()
@ -227,12 +212,16 @@ class AppsSerializer(serializers.ModelSerializer):
else:
return {}
today_hits_count = serializers.SerializerMethodField()
def get_today_hits_count(self, obj):
return get_app_today_download_times([obj.app_id])
class AppsListSerializer(AppsSerializer):
class Meta:
model = models.Apps
fields = ["app_id", "bundle_id", "issupersign", "name", "preview_url", "short", "type", "master_release",
"has_combo", "count_hits"]
"has_combo", "count_hits", "today_hits_count"]
def get_master_release(self, obj):
master_release_obj = get_app_master_obj_from_context(self, obj)
@ -519,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:

@ -387,11 +387,18 @@ class AppDownloadTokenView(APIView):
token_number = data.get('token_number')
bind_status = data.get('bind_status')
token_max_used_number = data.get('token_max_used_number')
udid_list_str = data.get('udids')
udid_list = []
if not isinstance(bind_status, bool):
bind_status = False
if bind_status:
token_max_used_number = 1
if udid_list_str and len(udid_list_str) > 10:
udid_list = udid_list_str.replace(' ', ' ').replace('\n', ' ').replace(',', ' ').replace('\r\n',
' ').split()
udid_list = set(filter(lambda x: len(x) > 10, udid_list))
token = ''
app_obj = Apps.objects.filter(user_id=request.user, app_id=app_id).first()
if token and isinstance(token, str) and len(token) >= 4:
@ -408,6 +415,12 @@ class AppDownloadTokenView(APIView):
else:
token_number = 20
if udid_list:
bind_udid_list = AppDownloadToken.objects.filter(app_id=app_obj, bind_udid__isnull=False).values_list(
'bind_udid')
udid_list = list(udid_list - set([x[0] for x in bind_udid_list]))
token_number = len(udid_list)
download_token_queryset = AppDownloadToken.objects.filter(app_id=app_obj).annotate(
token_len=Length('token')).filter(token_len=token_length).values('token').all()
exist_token = [d_token['token'] for d_token in download_token_queryset]
@ -415,9 +428,19 @@ class AppDownloadTokenView(APIView):
token_number=token_number + download_token_queryset.count(),
exist_token=copy.deepcopy(exist_token))
bulk_list = []
d_index = 0
for d_token in list(set(make_token_list) - set(exist_token))[:token_number]:
bulk_list.append(AppDownloadToken(token=d_token, max_limit_count=token_max_used_number, app_id=app_obj,
bind_status=bind_status))
n_data = {
'token': d_token,
'max_limit_count': token_max_used_number,
'app_id': app_obj,
'bind_status': bind_status,
}
if udid_list:
n_data['bind_udid'] = udid_list[d_index]
n_data['used_count'] = 1
d_index += 1
bulk_list.append(AppDownloadToken(**n_data))
AppDownloadToken.objects.bulk_create(bulk_list)
return Response(res.dict)

@ -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__)
@ -37,7 +41,7 @@ def get_domain_filter(request):
def auto_clean_download_cache(user_obj, user_domain_obj, app_obj, delete=False):
if user_domain_obj:
base_domain_queryset = UserDomainInfo.objects.filter(user_id=user_obj, is_enable=True).all()
if user_domain_obj.domain_type in [0, 1]:
if user_domain_obj.domain_type in [0, 1, 2]:
if delete:
user_domain_obj.delete()
if base_domain_queryset.filter(domain_type__in=[0, 1]).count() == 0:
@ -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)

@ -13,12 +13,13 @@ from rest_framework.views import APIView
from api.models import Apps, AppReleaseInfo
from api.utils.modelutils import get_filename_form_file, check_app_domain_name_access, \
ad_random_weight, get_app_download_uri
ad_random_weight, get_app_download_uri, get_preview_short_config
from api.utils.response import BaseResponse
from api.utils.serializer import AppsShortSerializer, AppAdInfoSerializer
from api.utils.signalutils import run_get_xsign_binary_file
from common.base.baseutils import get_origin_domain_name, format_get_uri, make_random_uuid, make_resigned
from common.core.decorators import cache_response # 本来使用的是 drf-extensions==0.7.0 但是还未支持该版本Django
from common.base.magic import cache_response
from common.cache.storage import AppDownloadShortShowCache
from common.core.response import mobileprovision_file_response, file_response, ApiResponse
from common.core.sysconfig import Config
from common.core.throttle import VisitShortThrottle, InstallShortThrottle, InstallThrottle1, InstallThrottle2
@ -97,7 +98,7 @@ class ShortDownloadView(APIView):
根据下载短链接获取应用信息
'''
@cache_response(timeout=600 - 60, cache="default", key_func='calculate_cache_key', cache_errors=False)
@cache_response(timeout=600 - 60, key_func='calculate_cache_key', callback_func='set_short_show_cache')
def get(self, request, short):
res = BaseResponse()
release_id = request.query_params.get("release_id", None)
@ -124,7 +125,8 @@ class ShortDownloadView(APIView):
res.code = 1000
res.domain_name = domain_name
res.redirect = True
res.data = format_get_uri(domain_name, short, {'release_id': release_id, 'udid': udid})
res.data = format_get_uri(domain_name, get_preview_short_config(user_obj, short),
{'release_id': release_id, 'udid': udid})
return Response(res.dict)
if udid:
if not app_obj.issupersign:
@ -144,6 +146,19 @@ class ShortDownloadView(APIView):
res.ad = AppAdInfoSerializer(ad_obj, context={"key": "ShortDownloadView", "short": short}).data
return Response(res.dict)
def set_short_show_cache(self, view_instance, view_method,
request, args, kwargs, cache_key):
short = kwargs.get("short", '')
short_show_cache = AppDownloadShortShowCache("ShortDownloadView".lower(), short)
key_list = short_show_cache.get_storage_cache()
if key_list and isinstance(key_list, list):
key_list.append(cache_key)
key_list = list(set(key_list))
else:
key_list = [cache_key]
short_show_cache.set_storage_cache(key_list, 600)
# key的设置
def calculate_cache_key(self, view_instance, view_method,
request, args, kwargs):

@ -576,9 +576,7 @@ class AuthorizationView(APIView):
ext = request.data.get("ext", None)
register_type = get_register_type()
if ext and register_type.get('code', None) and ext.get('icode', None):
if ext.get('icode') == '689888666':
pass
else:
if ext.get('icode') not in Config.INVITECODE:
res.code = 1008
res.msg = "邀请码已失效"
return Response(res.dict)

@ -10,15 +10,36 @@ 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
from common.core.sysconfig import UserConfig, Config
from common.utils.caches import del_cache_storage
logger = logging.getLogger(__name__)
def get_personal_config(request):
config_type = request.query_params.get("config_type", None)
if config_type == 'super_sign':
personal_config = Config.DEVELOPER_STATUS_CONFIG
elif config_type == 'preview_route':
personal_config = ['PRIVATE_DOWNLOAD_PAGE', 'PREVIEW_ROUTE_HASH']
elif config_type == 'short_download_uri':
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, ]
@ -27,11 +48,11 @@ class PersonalConfigView(APIView):
res = BaseResponse()
config_key = request.query_params.get("config_key", None)
developer_personal_config = Config.DEVELOPER_STATUS_CONFIG
personal_config = get_personal_config(request)
personal_config_queryset = UserPersonalConfig.objects.filter(user_id=request.user,
key__in=developer_personal_config)
if personal_config_queryset.count() != len(developer_personal_config):
for key in developer_personal_config:
key__in=personal_config)
if personal_config_queryset.count() != len(personal_config):
for key in personal_config:
UserConfig(request.user).set_default_value(key)
if config_key:
personal_config_queryset = personal_config_queryset.filter(key=config_key)
@ -46,15 +67,19 @@ 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 Config.DEVELOPER_STATUS_CONFIG:
UserPersonalConfig.objects.filter(user_id=request.user, key=config_key).update(value=config_value)
UserConfig(request.user).invalid_config_cache(config_key)
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)
def delete(self, request):
res = BaseResponse()
developer_personal_config = Config.DEVELOPER_STATUS_CONFIG
for key in developer_personal_config:
for key in get_personal_config(request):
UserConfig(request.user).del_value(key)
Config.invalid_config_cache(f'{key}_DES')
if key == 'preview_route':
# 清理缓存
del_cache_storage(request.user)
return Response(res.dict)

@ -15,11 +15,12 @@ from api.utils.apputils import get_random_short, save_app_infos
from api.utils.modelutils import get_app_download_uri, check_bundle_id_legal, get_user_storage_used, \
get_user_storage_capacity
from api.utils.response import BaseResponse
from api.utils.serializer import AppsSerializer
from api.utils.signalutils import run_signal_resign_utils
from common.base.baseutils import make_app_uuid, make_from_user_uuid
from common.cache.state import MigrateStorageState
from common.core.auth import ExpiringTokenAuthentication
from common.core.sysconfig import Config
from common.core.sysconfig import Config, UserConfig
from common.utils.caches import upload_file_tmp_name, del_cache_response_by_short
from common.utils.storage import Storage
from common.utils.token import verify_token, make_token
@ -92,6 +93,7 @@ class AppAnalyseView(APIView):
else:
upload_key = release_id + '.apk' + settings.FILE_UPLOAD_TMP_KEY
png_key = png_id + '.png' + settings.FILE_UPLOAD_TMP_KEY
short_domain_name = f"{short_domain_name}/{'#/' if UserConfig(request.user).PREVIEW_ROUTE_HASH else ''}"
storage = Storage(request.user)
storage_type = storage.get_storage_type()
upload_token = storage.get_upload_token(upload_key)
@ -169,9 +171,11 @@ class AppAnalyseView(APIView):
storage.rename_file(app_tmp_filename, app_new_filename)
storage.rename_file(png_tmp_filename, png_new_filename)
app_obj = Apps.objects.filter(bundle_id=data.get("bundleid"), user_id=request.user, type=1).first()
if app_obj:
app_obj = Apps.objects.filter(bundle_id=data.get("bundleid"), user_id=request.user).first()
if app_obj and app_obj.type == 1:
run_signal_resign_utils(app_obj)
app_serializer = AppsSerializer(app_obj, context={"storage": Storage(request.user)})
res.data = app_serializer.data
else:
storage.delete_file(app_tmp_filename)
storage.delete_file(png_tmp_filename)

@ -112,8 +112,11 @@ class FLYCliSer(object):
req = requests.put(url, json=data, headers=self._header)
if req.status_code == 200:
if req.json()['code'] == 1000:
master_download_url = req.json().get('data', {}).get('preview_url')
print("应用 %s %s 上传更新成功" % (data.get('appname'), data.get('bundleid')))
return
print("当前应用下载连接:", master_download_url)
print("当前版本下载连接:", master_download_url + '?release_id=' + data.get('upload_key').split('.')[0])
return master_download_url
raise AssertionError(req.text)
def upload_local_storage(self, upload_key, upload_token, app_id, file_path):
@ -200,7 +203,7 @@ class FLYCliSer(object):
"udid": appinfo.get('udid', []),
"type": appinfo['type'],
}
self.analyse({**app_data, **upcretsdata})
return self.analyse({**app_data, **upcretsdata})
class AppInfo(object):
@ -359,6 +362,7 @@ if __name__ == '__main__':
Example: \r\n
"""
)
parser.add_argument('-host', '--host', nargs="?", const=True, type=str, help="服务器地址")
parser.add_argument('-s', '--short', nargs="?", const=True, type=str, help="短连接")
parser.add_argument('-cl', '--change_log', nargs="?", const=True, type=str, help="更新日志")
parser.add_argument('-n', '--name', nargs="?", const=True, type=str, help="名称")
@ -368,6 +372,8 @@ if __name__ == '__main__':
parser.add_argument('-f', '--filepath', nargs="+", type=str, help="filepath")
parser.add_argument('-d', '--dir_path', nargs="+", type=str, help="dir_path")
args = parser.parse_args()
if not args.host:
args.host = domain
if args.token and args.write_token_info:
write_token(args.token)
if args.install_depends:
@ -379,7 +385,7 @@ if __name__ == '__main__':
if token: token = token.replace('\r', '').replace('\n', '')
if not token:
raise Exception('token not exist')
fly_obj = FLYCliSer(domain, token)
fly_obj = FLYCliSer(args.host, token)
file_path_list = []
if args.filepath is not None and len(args.filepath) > 0:
for args_filepath in args.filepath:

@ -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,20 +218,22 @@ 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:
return True
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}")
count -= 1
@ -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 ''

@ -6,10 +6,11 @@
import datetime
import logging
import time
from functools import wraps
from functools import wraps, WRAPPER_ASSIGNMENTS
from importlib import import_module
from django.core.cache import cache
from django.http import HttpResponse
logger = logging.getLogger(__name__)
@ -173,38 +174,51 @@ def magic_call_in_times(call_time=24 * 3600, call_limit=6, key=None):
class MagicCacheData(object):
@staticmethod
def make_cache(cache_time=60 * 10, invalid_time=0, key=None):
def make_cache(timeout=60 * 10, invalid_time=0, key_func=None, timeout_func=None):
"""
:param cache_time: 数据缓存的时候单位秒
:param timeout_func:
:param timeout: 数据缓存的时候单位秒
:param invalid_time: 数据缓存提前失效时间单位秒该cache有效时间为 cache_time-invalid_time
:param key: cache唯一标识默认为所装饰函数名称
:param key_func: cache唯一标识默认为所装饰函数名称
:return:
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
cache_key = f'magic_cache_data'
if key:
cache_key = f'{cache_key}_{key(*args, **kwargs)}'
else:
cache_key = f'{cache_key}_{func.__name__}'
cache_key = f'magic_cache_data_{func.__name__}'
if key_func:
cache_key = f'{cache_key}_{key_func(*args, **kwargs)}'
cache_time = timeout
if timeout_func:
cache_time = timeout_func(*args, **kwargs)
n_time = time.time()
res = cache.get(cache_key)
if res:
while res.get('status') != 'ok':
time.sleep(0.5)
logger.warning(
f'exec {func} wait. data status is not ok. cache_time:{cache_time} cache_key:{cache_key} cache data exist result:{res}')
res = cache.get(cache_key)
if res and n_time - res.get('c_time', n_time) < cache_time - invalid_time:
logger.info(f"exec {func} finished. cache_key:{cache_key} cache data exist result:{res}")
logger.info(
f"exec {func} finished. cache_time:{cache_time} cache_key:{cache_key} cache data exist result:{res}")
return res['data']
else:
res = {'c_time': n_time, 'data': '', 'status': 'ready'}
cache.set(cache_key, res, cache_time)
try:
data = func(*args, **kwargs)
res = {'c_time': n_time, 'data': data}
res['data'] = func(*args, **kwargs)
res['status'] = 'ok'
cache.set(cache_key, res, cache_time)
logger.info(
f"exec {func} finished. time:{time.time() - n_time} cache_key:{cache_key} result:{res}")
f"exec {func} finished. time:{time.time() - n_time} cache_time:{cache_time} cache_key:{cache_key} result:{res}")
except Exception as e:
logger.error(
f"exec {func} failed. time:{time.time() - n_time} cache_key:{cache_key} Exception:{e}")
f"exec {func} failed. time:{time.time() - n_time} cache_time:{cache_time} cache_key:{cache_key} Exception:{e}")
return res['data']
@ -217,3 +231,141 @@ class MagicCacheData(object):
cache_key = f'magic_cache_data_{key}'
res = cache.delete(cache_key)
logger.warning(f"invalid_cache cache_key:{cache_key} result:{res}")
class MagicCacheResponse(object):
def __init__(self, timeout=60 * 10, invalid_time=0, key_func=None, callback_func=None):
self.timeout = timeout
self.key_func = key_func
self.invalid_time = invalid_time
self.callback_func = callback_func
@staticmethod
def invalid_cache(key):
cache_key = f'magic_cache_response_{key}'
res = cache.delete(cache_key)
logger.warning(f"invalid_response_cache cache_key:{cache_key} result:{res}")
def __call__(self, func):
this = self
@wraps(func, assigned=WRAPPER_ASSIGNMENTS)
def inner(self, request, *args, **kwargs):
return this.process_cache_response(
view_instance=self,
view_method=func,
request=request,
args=args,
kwargs=kwargs,
)
return inner
def process_cache_response(self,
view_instance,
view_method,
request,
args,
kwargs):
func_key = self.calculate_key(
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs
)
func_name = f'{view_instance.__class__.__name__}_{view_method.__name__}'
cache_key = f'magic_cache_response_{func_name}'
if func_key:
cache_key = f'{cache_key}_{func_key}'
timeout = self.calculate_timeout(view_instance=view_instance)
n_time = time.time()
res = cache.get(cache_key)
if res and n_time - res.get('c_time', n_time) < timeout - self.invalid_time:
logger.info(f"exec {func_name} finished. cache_key:{cache_key} cache data exist result:{res}")
content, status, headers = res['data']
response = HttpResponse(content=content, status=status)
for k, v in headers.values():
response[k] = v
else:
response = view_method(view_instance, request, *args, **kwargs)
response = view_instance.finalize_response(request, response, *args, **kwargs)
response.render()
if not response.status_code >= 400:
# django 3.0 has not .items() method, django 3.2 has not ._headers
if hasattr(response, '_headers'):
headers = response._headers.copy()
else:
headers = {k: (k, v) for k, v in response.items()}
data = (
response.rendered_content,
response.status_code,
headers
)
res = {'c_time': n_time, 'data': data}
cache.set(cache_key, res, timeout)
self.callback_check(view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs,
cache_key=cache_key)
logger.info(
f"exec {func_name} finished. time:{time.time() - n_time} cache_key:{cache_key} result:{res}")
if not hasattr(response, '_closable_objects'):
response._closable_objects = []
return response
def calculate_key(self,
view_instance,
view_method,
request,
args,
kwargs):
if isinstance(self.key_func, str):
key_func = getattr(view_instance, self.key_func)
else:
key_func = self.key_func
if key_func:
return key_func(
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs,
)
def calculate_timeout(self, view_instance, **_):
if isinstance(self.timeout, str):
self.timeout = getattr(view_instance, self.timeout)
return self.timeout
def callback_check(self,
view_instance,
view_method,
request,
args,
kwargs,
cache_key):
if isinstance(self.callback_func, str):
callback_func = getattr(view_instance, self.callback_func)
else:
callback_func = self.callback_func
if callback_func:
return callback_func(
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs,
cache_key=cache_key
)
cache_response = MagicCacheResponse

@ -43,3 +43,15 @@ class SignStatus(object):
APP_REGISTRATION_COMPLETE = 2
PROFILE_DOWNLOAD_COMPLETE = 3
SIGNATURE_PACKAGE_COMPLETE = 4
class DeviceClass(object):
"""
苹果设备类型分为6类每类都有100设备数
"""
APPLE_WATCH = 'APPLE_WATCH'
IPAD = 'IPAD'
IPHONE = 'IPHONE'
IPOD = 'IPOD'
APPLE_TV = 'APPLE_TV'
MAC = 'MAC'

@ -1,146 +0,0 @@
from functools import wraps, WRAPPER_ASSIGNMENTS
from django.http.response import HttpResponse
from common.cache.storage import AppDownloadShortShowCache
def get_cache(alias):
from django.core.cache import caches
return caches[alias]
def set_short_show_cache(short, cache_key):
short_show_cache = AppDownloadShortShowCache("ShortDownloadView".lower(), short)
key_list = short_show_cache.get_storage_cache()
if key_list and isinstance(key_list, list):
key_list.append(cache_key)
key_list = list(set(key_list))
else:
key_list = [cache_key]
short_show_cache.set_storage_cache(key_list, 600)
class CacheResponse:
"""
Store/Receive and return cached `HttpResponse` based on DRF response.
.. note::
This decorator will render and discard the original DRF response in
favor of Django's `HttpResponse`. The allows the cache to retain a
smaller memory footprint and eliminates the need to re-render
responses on each request. Furthermore it eliminates the risk for users
to unknowingly cache whole Serializers and QuerySets.
"""
def __init__(self,
timeout=None,
key_func=None,
cache=None,
cache_errors=None):
if timeout is None:
self.timeout = None
else:
self.timeout = timeout
if key_func is None:
self.key_func = ''
else:
self.key_func = key_func
if cache_errors is None:
self.cache_errors = True
else:
self.cache_errors = cache_errors
self.cache = get_cache(cache or 'default')
def __call__(self, func):
this = self
@wraps(func, assigned=WRAPPER_ASSIGNMENTS)
def inner(self, request, *args, **kwargs):
return this.process_cache_response(
view_instance=self,
view_method=func,
request=request,
args=args,
kwargs=kwargs,
)
return inner
def process_cache_response(self,
view_instance,
view_method,
request,
args,
kwargs):
key = self.calculate_key(
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs
)
timeout = self.calculate_timeout(view_instance=view_instance)
response_triple = self.cache.get(key)
if not response_triple:
# render response to create and cache the content byte string
response = view_method(view_instance, request, *args, **kwargs)
response = view_instance.finalize_response(request, response, *args, **kwargs)
response.render()
if not response.status_code >= 400 or self.cache_errors:
# django 3.0 has not .items() method, django 3.2 has not ._headers
if hasattr(response, '_headers'):
headers = response._headers.copy()
else:
headers = {k: (k, v) for k, v in response.items()}
response_triple = (
response.rendered_content,
response.status_code,
headers
)
short = kwargs.get("short", '')
set_short_show_cache(short, key)
self.cache.set(key, response_triple, timeout)
else:
# build smaller Django HttpResponse
content, status, headers = response_triple
response = HttpResponse(content=content, status=status)
for k, v in headers.values():
response[k] = v
if not hasattr(response, '_closable_objects'):
response._closable_objects = []
return response
def calculate_key(self,
view_instance,
view_method,
request,
args,
kwargs):
if isinstance(self.key_func, str):
key_func = getattr(view_instance, self.key_func)
else:
key_func = self.key_func
return key_func(
view_instance=view_instance,
view_method=view_method,
request=request,
args=args,
kwargs=kwargs,
)
def calculate_timeout(self, view_instance, **_):
if isinstance(self.timeout, str):
self.timeout = getattr(view_instance, self.timeout)
return self.timeout
cache_response = CacheResponse

@ -11,30 +11,30 @@ logger = logging.getLogger(__name__)
"""
更新应用的时候是否同时更新超级签名数据
"""
run_resign_task_signal = Signal(providing_args=["app_obj"])
run_resign_task_signal = Signal()
"""
删除应用时的信号用户清理签名相关数据
"""
delete_app_signal = Signal(providing_args=["app_obj"])
delete_app_signal = Signal()
"""
下载超级签名数据
"""
xsign_app_download_url_signal = Signal(providing_args=["app_pk", "udid", "download_url_type", "limit"])
xsign_app_download_url_signal = Signal()
"""
迁移超级签名数据
"""
xsign_migrate_data_signal = Signal(providing_args=["app_release_obj", "user_obj", "new_storage_obj", "clean_old_data"])
xsign_migrate_data_signal = Signal()
"""
清理超级签名数据
"""
xsign_clean_data_signal = Signal(providing_args=["app_release_obj", "storage_obj"])
xsign_clean_data_signal = Signal()
"""
根据binary_file获取签名应用数据
"""
xsign_app_release_obj_signal = Signal(providing_args=["binary_file"])
xsign_app_release_obj_signal = Signal()

@ -157,6 +157,22 @@ class DomainConfCache(ConfigCacheBase):
def WEB_DOMAIN(self):
return self.get_value('WEB_DOMAIN', DOMAINCONF.WEB_DOMAIN)
@property
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):
def __init__(self, *args, **kwargs):
@ -257,6 +273,10 @@ class AuthConfCache(ConfigCacheBase):
def REPORT(self):
return super().get_value('REPORT', AUTHCONF.REPORT)
@property
def INVITECODE(self):
return super().get_value('INVITECODE', AUTHCONF.INVITECODE)
class GeeTestConfCache(ConfigCacheBase):
def __init__(self, *args, **kwargs):
@ -445,6 +465,22 @@ class UserPersonalConfKeyCache(ConfigCacheBase):
def DEVELOPER_STATUS_CONFIG(self):
return super().get_value('DEVELOPER_STATUS_CONFIG', USERPERSONALCONFIGKEY.DEVELOPER_STATUS_CONFIG)
@property
def PREVIEW_ROUTE_HASH(self):
return super().get_value('PREVIEW_ROUTE_HASH', USERPERSONALCONFIGKEY.PREVIEW_ROUTE_HASH)
@property
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):

@ -72,11 +72,6 @@ def create_menu(menu_json=None):
"name": "官方地址",
"key": "flyapps"
},
{
"type": "view",
"name": "留言反馈",
"url": "https://flyapps.cn/gbook/"
},
{
"type": "click",
"name": "查询当前绑定",

@ -24,7 +24,7 @@ class Weixinpay(object):
def __get_wx_pay(self):
return WeChatPay(wechatpay_type=WeChatPayType.NATIVE,
mchid=self.wx_config.get('MCH_ID'),
parivate_key=self.wx_config.get('APP_PRIVATE_KEY'),
private_key=self.wx_config.get('APP_PRIVATE_KEY'),
cert_serial_no=self.wx_config.get('SERIAL_NO'),
appid=self.wx_config.get('APP_ID'),
notify_url=f"{self.wx_config.get('APP_NOTIFY_URL')}/{self.name}",

@ -8,7 +8,7 @@ class WeChatPay(object):
def __init__(self,
wechatpay_type,
mchid,
parivate_key,
private_key,
cert_serial_no,
appid,
apiv3_key,
@ -16,7 +16,7 @@ class WeChatPay(object):
"""
:param wechatpay_type: 微信支付类型示例值WeChatPayType.MINIPROG
:param mchid: 直连商户号示例值'1230000109'
:param parivate_key: 商户证书私钥示例值'MIIEvwIBADANBgkqhkiG9w0BAQE...'
:param private_key: 商户证书私钥示例值'MIIEvwIBADANBgkqhkiG9w0BAQE...'
:param cert_serial_no: 商户证书序列号示例值'444F4864EA9B34415...'
:param appid: 应用ID示例值'wxd678efh567hg6787'
:param apiv3_key: 商户APIv3密钥示例值'a12d3924fd499edac8a5efc...'
@ -28,7 +28,7 @@ class WeChatPay(object):
self._notify_url = notify_url
self._core = Core(mchid=self._mchid,
cert_serial_no=cert_serial_no,
private_key=parivate_key,
private_key=private_key,
apiv3_key=apiv3_key)
def pay(self,

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# wechatpay v3 python sdk(https://github.com/minibear2021/wechatpayv3)
import json
import time
import uuid
@ -175,8 +176,7 @@ class Core():
headers = {}
headers.update({'Content-Type': 'application/json'})
headers.update({'Accept': 'application/json'})
headers.update(
{'User-Agent': 'wechatpay v3 python sdk(https://github.com/minibear2021/wechatpayv3)'})
headers.update({'User-Agent': 'flyapps python sdk'})
authorization = build_authorization(
path,
'GET' if method == RequestType.GET else 'POST',

@ -417,3 +417,13 @@ def add_user_storage_exchange(user_obj, exchange_number, exchange_month, remote_
except Exception as e:
logger.error(f"{user_obj} download_times less then 0. Exception:{e}")
return False
def add_user_ds(uid, amount):
obj = UserInfo.objects.filter(uid=uid).first()
try:
amount = int(amount)
except Exception:
amount = 0
if obj and amount:
return admin_change_user_download_times(obj, amount * Config.APP_USE_BASE_DOWNLOAD_TIMES)

@ -125,7 +125,7 @@ def get_app_download_url(request, res, app_id, short, password, release_id, is_d
logger.info(f'app download url :{res.data}')
if download_url != "" and "mobileconifg" not in download_url:
if not check_app_download_token(app_obj.get("need_password"), is_download, app_id, password):
if not check_app_download_token(app_obj.get("need_password"), is_download, app_id, password, bool(udid)):
res.code = 1006
res.msg = '下载授权码有误'
return res

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save