├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .vscode └── launch.json ├── Makefile ├── README.md ├── app ├── App.vue ├── application.js ├── assets │ ├── images │ │ ├── 404 │ │ │ └── 404.jpg │ │ ├── icon │ │ │ ├── icon-alipay.png │ │ │ ├── icon-lzj.png │ │ │ ├── icon-qr.png │ │ │ ├── icon-toutiao.png │ │ │ └── icon-wechat.png │ │ ├── image_default.png │ │ ├── image_error.png │ │ ├── logo │ │ │ ├── logo-circular.png │ │ │ └── logo.jpg │ │ └── qr │ │ │ └── lzj-wechat.jpg │ └── scss │ │ ├── bootstrap.scss │ │ ├── common.scss │ │ ├── function.scss │ │ ├── function │ │ ├── create-common-style.scss │ │ ├── create-icon-style.scss │ │ ├── home-indicator-compatible.scss │ │ └── text-ellipsis.scss │ │ ├── global.scss │ │ ├── icon.scss │ │ ├── order │ │ └── detail.scss │ │ └── variable.scss ├── components │ └── release │ │ └── index.vue ├── entry.client.js ├── entry.server.js ├── lib │ └── vconsole-decrypt-network-tab │ │ └── index.js ├── main.js ├── mixin │ └── scroll-fixed.js ├── router │ ├── 404.js │ ├── guard │ │ └── beforeEach.js │ ├── index.js │ ├── layout.js │ └── login.js ├── src │ ├── 404.vue │ ├── layout │ │ ├── index.vue │ │ ├── layout-empty.vue │ │ ├── layout-header.vue │ │ ├── layout-main.vue │ │ ├── layout-slidebar.vue │ │ └── layout-sub-slidebar.vue │ ├── login │ │ └── index.vue │ ├── lzj │ │ ├── alipay.vue │ │ ├── toutiao.vue │ │ └── wechat.vue │ └── system │ │ ├── task.vue │ │ └── user.vue ├── store │ ├── actions.js │ ├── getters.js │ ├── index.js │ ├── modules │ │ ├── init_data │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ ├── mutations.js │ │ │ └── state.js │ │ └── modules.js │ ├── mutations.js │ ├── state.js │ └── store_constants.js └── utils │ ├── browser.js │ ├── browserNotify.js │ ├── channel.js │ ├── constants.js │ ├── index.js │ ├── rest.js │ ├── socket.js │ └── strategies.js ├── build ├── setup-dev-server.js ├── webpack.base.config.js ├── webpack.client.config.js ├── webpack.nest.server.config.js └── webpack.server.config.js ├── docker-compose.yaml ├── dockerfiles ├── base_image ├── builder_image └── release ├── entrypoint.sh ├── lib ├── configure.js ├── constants.js └── utils.js ├── nest-cli.json ├── nodemon.json ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico └── logo-circular.png ├── read-image ├── login.jpg ├── main-remark.jpg ├── main.jpg ├── preview.jpg └── upload.jpg ├── server ├── .eslintrc.js ├── app.module.ts ├── build.js ├── db │ ├── model │ │ ├── index.ts │ │ ├── task.ts │ │ └── user.ts │ └── sequelize.ts ├── definitionfile │ ├── anyObj.ts │ ├── iRequest.ts │ ├── index.ts │ ├── previewTask.ts │ ├── rest.ts │ └── result.ts ├── index.js ├── middleware │ ├── browser.middleware.ts │ ├── env.middleware.ts │ ├── rest.middleware.ts │ └── ssr.middleware.ts ├── modules │ ├── 404 │ │ ├── 404.controller.ts │ │ └── 404.module.ts │ ├── auth │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ └── jwt.strategy.ts │ ├── auto-info │ │ ├── autoInfo.controller.ts │ │ ├── autoInfo.module.ts │ │ └── autoInfo.service.ts │ ├── ci │ │ ├── ci.controller.ts │ │ ├── ci.gateway.ts │ │ ├── ci.module.ts │ │ └── ci.service.ts │ ├── task │ │ ├── task.controller.ts │ │ └── task.module.ts │ └── user │ │ ├── user.controller.ts │ │ ├── user.module.ts │ │ └── user.service.ts ├── server.ts └── utils │ ├── CI │ ├── alipay │ │ └── index.ts │ ├── index.ts │ ├── private │ │ ├── lzj-alipay.key │ │ └── lzj-wechat.key │ ├── toutiao │ │ └── index.ts │ ├── utils │ │ ├── ci-configure.ts │ │ ├── index.ts │ │ └── task.service.ts │ └── wechat │ │ └── index.ts │ ├── constants.ts │ └── index.ts ├── tsconfig.build.json ├── tsconfig.json ├── view └── index.html └── 开发须知.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .byebug_history 2 | .env* 3 | .git 4 | .gitignore 5 | .pryrc 6 | .ruby-version 7 | Dockerfile 8 | Makefile 9 | log/* 10 | node_modules/* 11 | public/assets/* 12 | tmp/* 13 | doc/* 14 | config/database.yml.* 15 | bin/* 16 | !bin/rails 17 | !bin/bundle 18 | dockerfiles 19 | miniprogram/* 20 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /public/bundle/* 2 | /private/server/* 3 | node_modules/* 4 | /.idea 5 | /miniprogram/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2020: true, 6 | }, 7 | extends: [ 8 | 'airbnb-base', 9 | 'plugin:vue/essential', 10 | ], 11 | parserOptions: { 12 | parser: 'babel-eslint', 13 | ecmaVersion: 11, 14 | sourceType: 'module', 15 | }, 16 | plugins: [ 17 | 'vue', 18 | ], 19 | globals: { 20 | __CLIENT__: true, 21 | __SERVER__: true, 22 | __DEVELOPMENT__: true, 23 | __PRODUCTION__: true, 24 | wx: true, 25 | }, 26 | rules: { 27 | semi: [2, 'never'], 28 | 'comma-dangle': [1, 'always-multiline'], 29 | 'arrow-parens': 'off', 30 | 'no-underscore-dangle': 'off', 31 | 'arrow-body-style': 'off', 32 | 'consistent-return': 'off', 33 | 'array-callback-return': 'off', 34 | 'max-len': 'off', 35 | 'max-lines': 'off', 36 | 'no-console': 'off', 37 | 'no-plusplus': 'off', 38 | 'no-mixed-operators': 'off', 39 | 'class-methods-use-this': 'off', 40 | 'no-else-return': 'off', 41 | 'default-case': 'off', 42 | 'no-new': 'off', 43 | 'no-restricted-syntax': 'off', 44 | 'no-continue': 'off', 45 | 'no-return-assign': [ 46 | 'off', 47 | 'always', 48 | ], 49 | // 禁止无用的表达式 50 | 'no-unused-expressions': [ 51 | 'off', 52 | { 53 | allowShortCircuit: true, 54 | allowTernary: true, 55 | allowTaggedTemplates: true, 56 | }, 57 | ], 58 | 'global-require': 'off', 59 | 'space-before-function-paren': [ 60 | 'error', 61 | { 62 | anonymous: 'always', 63 | named: 'always', 64 | asyncArrow: 'always', 65 | }, 66 | ], 67 | 'vue/multi-word-component-names': 'off', 68 | 'import/extensions': ['error', 'always', { 69 | js: 'never', 70 | json: 'never', 71 | scss: 'never', 72 | css: 'never', 73 | vue: 'never', 74 | }], 75 | }, 76 | settings: { 77 | 'import/resolver': { 78 | webpack: { 79 | config: './build/webpack.client.config.js', 80 | }, 81 | }, 82 | }, 83 | } 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | 4 | # node 5 | node_modules 6 | 7 | # output 8 | /public/bundle 9 | /private/server 10 | 11 | # local env files 12 | .env.local 13 | .env.*.local 14 | 15 | # Logs 16 | logs 17 | *.log 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | lerna-debug.log* 22 | 23 | # Editor directories and files 24 | .idea 25 | .vscode/settings 26 | .project 27 | .classpath 28 | .c9/ 29 | *.launch 30 | .settings/ 31 | *.sublime-workspace 32 | *.suo 33 | *.ntvs* 34 | *.njsproj 35 | *.sln 36 | *.sw* 37 | 38 | # Tests 39 | /coverage 40 | /.nyc_output 41 | 42 | /lib/config/patch.json 43 | 44 | # miniprogram 45 | /miniprogram -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Launch Program", 8 | "runtimeExecutable": "npm", 9 | "runtimeArgs": [ 10 | "run", 11 | "start:debug" 12 | ], 13 | "cwd": "${workspaceRoot}/server", 14 | "port": 9229, 15 | "console":"integratedTerminal", 16 | "sourceMaps": true 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := $(shell basename -s . `git rev-parse --show-toplevel`) 2 | GIT_COMMIT = $(shell git rev-parse HEAD | cut -c1-10) 3 | GIT_BRANCH = $(shell git rev-parse --abbrev-ref HEAD) 4 | DOCKER_BASE_NAME := $(NAME):alpine 5 | DOCKER_BUILDER_NAME := $(NAME):builder 6 | APP_ROOT := /opt/$(NAME) 7 | CLEAN_IMAGES := 8 | BUILD_TAG := $(tag) 9 | 10 | # check if base images exist 11 | ifeq ("$(shell docker images -q $(DOCKER_BASE_NAME) 2> /dev/null)","") 12 | BUILD_BASE_IMAGE = docker build -t $(DOCKER_BASE_NAME) -f ./dockerfiles/base_image . 13 | else 14 | CLEAN_IMAGES := $(CLEAN_IMAGES) $(DOCKER_BASE_NAME) 15 | BUILD_BASE_IMAGE = 16 | endif 17 | 18 | # check if builder images exist 19 | ifeq ("$(shell docker images -q $(DOCKER_BUILDER_NAME) 2> /dev/null)","") 20 | BUILD_BUILDER_IMAGE = docker build --build-arg FROM_IMAGE="$(DOCKER_BASE_NAME)" \ 21 | -t $(DOCKER_BUILDER_NAME) -f ./dockerfiles/builder_image . 22 | else 23 | CLEAN_IMAGES := $(CLEAN_IMAGES) $(DOCKER_BUILDER_NAME) 24 | BUILD_BUILDER_IMAGE = 25 | endif 26 | 27 | CLEAN_IMAGES := $(shell echo $(CLEAN_IMAGES) | xargs) 28 | 29 | ifeq ("$(CLEAN_IMAGES)","") 30 | CLEAN_DOCKER_IMAGES = 31 | else 32 | CLEAN_DOCKER_IMAGES = docker rmi $(CLEAN_IMAGES) 33 | endif 34 | 35 | ifeq ("$(BUILD_TAG)","") 36 | BUILD_TAG := 2020 37 | endif 38 | 39 | .PHONY: docker base builder clean 40 | .DEFAULT_GOAL := docker 41 | 42 | docker: base builder; $(info ======== build $(NAME) release image:) 43 | docker build --build-arg RUNTIME_IMAGE="$(DOCKER_BASE_NAME)" \ 44 | --build-arg BUILDER_IMAGE="$(DOCKER_BUILDER_NAME)" \ 45 | --build-arg GIT_COMMIT=$(GIT_COMMIT) \ 46 | --build-arg APP_ROOT="$(APP_ROOT)" \ 47 | --build-arg APP_VERSION="$(NAME),$(GIT_BRANCH),$(GIT_COMMIT)" \ 48 | -t $(NAME):$(BUILD_TAG) \ 49 | --rm -f ./dockerfiles/release . 50 | 51 | base: ; $(info ======== build $(NAME) runtime image:) 52 | @$(BUILD_BASE_IMAGE) 53 | 54 | builder: ; $(info ======== build $(NAME) compile image:) 55 | @$(BUILD_BUILDER_IMAGE) 56 | 57 | clean: ; $(info ======== clean docker images: $(CLEAN_IMAGES)) 58 | @$(CLEAN_DOCKER_IMAGES) 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 小程序发布平台 2 | 3 | > 有问题欢迎在issues中提出 4 | 5 | ## 1 背景 6 | 7 | 😈 如果你同时维护着多个小程序项目,那你每天是否花费了大量的时间在做这样一件时间,切换git分支 -> 执行编译 -> 打开小程序开发者工具 -> 发布。
8 | 🧐 同时维护着5个小程序(两个微信小程序、两个支付宝小程序、一个字节跳动小程序),我发现我每天要花大量的时间做发布小程序的工作。为此我想到了打造一个类似Jenkins的小程序自动化构建平台,将发布小程序的任务移交给测试同事(是的,我就是这么懒)。
9 | 10 | ## 2 项目界面介绍 11 | 12 | tips: 图片无法加载的话,可以下载项目后看read-image文件夹 13 | 14 | * [在线地址](http://39.108.210.55/) 15 | * 账号: admin 16 | * 密码: 123456 17 | 18 | ### 2.1 登录页 19 | 20 | ![登录界面](./read-image/login.jpg) 21 | 22 | ### 2.2 主页 23 | 24 | ![主页](./read-image/main.jpg) 25 | 26 | ### 2.3 主页带备注 27 | 28 | ![主页带备注](./read-image/main-remark.jpg) 29 | 30 | ### 2.4 发布预览 31 | 32 | ![发布预览](./read-image/preview.jpg) 33 | 34 | ### 2.5 发布体验版 35 | 36 | ![发布体验版](./read-image/upload.jpg) 37 | 38 | ### 3 功能介绍 39 | 40 | - 目前支持发布微信小程序、支付宝小程序、字节跳动小程序 41 | - 角色划分为超级管理员、管理员、开发/运营/测试、普通用户 42 | - 超级管理员与管理员具备所有功能,其中超级管理员账号不可被删除 43 | - 具备用户管理功能,包括用户的增、删、查、改 44 | - 具备发布任务管理功能,包括任务的删、查功能 45 | - 具备发布预览、发布体验版功能 46 | - 具备修改用户个人信息功能 47 | - 开发/运营/测试 48 | - 具备发布预览、发布体验版功能 49 | - 具备修改用户个人信息功能 50 | - 普通用户 51 | - 只具备查看发布记录与小程序二维码功能 52 | 53 | ## 4 技术概览 54 | 55 | 该项目后端采用nestjs + typescript技术,ORM使用sequelize,数据库采用mysql。前端采用vue2全家桶,搭配SSR技术,通过socket广播发布日志到所有前端用户中。
56 | 57 | 技术详解请看(二次开发必看): [开发须知](./开发须知.md) 58 | 59 | [原理教程请看](https://juejin.cn/post/6896823039099731975) 60 | 61 | ## 5 启动项目 62 | 63 | ### 5.1 本地开发时需要修改项目配置文件(部署到服务器中,不需要修改) 64 | 65 | 在/lib/configure文件中将redisCFG与mysql字段中的配置改为自己的配置,主要修改host字段即可 66 | 67 | ```js 68 | { 69 | // redis用户记录session数据,不使用的话可以在/server/server.ts文件中找到session中间件,并注释掉store字段,即可删除所有与redisCFG配置相关的内容 70 | // 配置含义见connect-redis库 71 | redisCFG: { 72 | prefix: 'mp_release_platform_sess:', 73 | host: '0.0.0.0', 74 | port: 6379, 75 | db: 8, 76 | }, 77 | // 配置含义见sequelize库 78 | mysql: { 79 | port: 3306, 80 | host: '0.0.0.0', 81 | user: 'mp', 82 | password: 'mp2020', 83 | database: 'mp_release_platform', 84 | connectionLimit: 10, 85 | }, 86 | } 87 | ``` 88 | 89 | ### 5.2 修改小程序配置文件 90 | 91 | 找到/server/utils/CI/utils/ci-configure.ts文件,按照注释修改配置即可 92 | 93 | ### 5.3 安装依赖并运行 94 | 95 | ```shell 96 | # cd至项目根路径 97 | # 安装所需包 98 | npm i 99 | 100 | # 新建终端(command + t),并运行redis 101 | redis-server 102 | 103 | # 运行项目 104 | npm run start:dev 105 | 106 | # 浏览器中访问 http://localhost:8088 or https://localhost:8089 107 | ``` 108 | 109 | ## 6 项目部署说明 110 | 111 | 部署项目需要先安装docker,安装完成后再继续下面的步骤 112 | 113 | ```shell 114 | # 自行安装git并设置好github的SSH 115 | # 克隆小程序自动化构建平台项目 116 | git clone git@github.com:lizijie123/mp_release_platform.git 117 | # 进入项目目录 118 | cd mp_release_platform 119 | # 将小程序自动化构建平台打包为docker镜像 120 | make 121 | # 通过docker服务编排同时生成并启动,小程序自动化构建平台容器、redis容器、mysql容器 122 | docker-compose up 123 | ``` 124 | 125 | ## 7 常见问题 126 | 127 | ### 7.1 数据库数据是乱码 128 | 129 | 创建mysql的容器之前,需要设置字符集为utf8 130 | 131 | ```yaml 132 | # 完整配置 133 | mysql: 134 | restart: always 135 | image: mysql:5.6 136 | volumes: 137 | - ./data:/var/lib/mysql 138 | command: [ 139 | '--character-set-server=utf8', 140 | '--collation-server=utf8_general_ci', 141 | --default-authentication-plugin=mysql_native_password, 142 | ] 143 | environment: 144 | - MYSQL_ROOT_PASSWORD=root 145 | - MYSQL_DATABASE=mp_release_platform 146 | - MYSQL_USER=mp 147 | - MYSQL_PASSWORD=mp2020 148 | ports: 149 | - "3306:3306" 150 | ``` 151 | 152 | 对于已经创建了mysql容器的项目,只需要进入mysql中,删掉数据库并重新创建一个即可 153 | 154 | ```shell 155 | # 单独启动mysql 156 | docker-compose up -d mysql 157 | # 查看mysql容器的id 158 | docker ps 159 | # 进入mysql容器 160 | docker exec -it mysql容器id bash 161 | # 连接mysql 162 | mysql -ump -pmp2020 163 | # 删除数据库 164 | DROP DATABASE mp_release_platform 165 | # 创建数据库并执行字符集 166 | CREATE DATABASE mp_release_platform DEFAULT CHARSET utf8 COLLATE utf8_general_ci 167 | ``` 168 | 169 | ### 7.2 拉取gitlab项目超时 170 | 171 | 公司gitlab一般只允许内网访问,部署在云服务器上的项目会出现无法访问gitlab项目的问题,需要运维同事帮忙在gitlab项目中设置访问白名单。 172 | 173 | ### 7.2 package.json中的devDependencies的包没有安装 174 | 175 | 当环境变量NODE_ENV为production时(也就是我们的小程序自动化构建平台这个项目运行时,设置的NODE_ENV),npm install或yarn install只会安装dependencies,而不会安装devDependencies列表中的包,需要将安装命令改为npm install --dev或yarn install --production=false 176 | 177 | ### 7.3 编译项目过程中出现内存溢出问题 178 | 179 | * 报错日志: FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory 180 | 181 | 首先排查k8s分配给docker容器的内存大小是否足够,若k8s分配给docker容器的内存足够大,依然报内存溢出,则可能是系统分配给node的内存不足(tips: 分配给node程序的内存64位系统下约为1.4GB,32位系统下约为0.7GB),这时候可以通过[increase-memory-limit](https://www.npmjs.com/package/increase-memory-limit)这个包解决。 182 | 183 | ## 8 作者 184 | 185 | github: [lizijie123](https://github.com/lizijie123) 186 | -------------------------------------------------------------------------------- /app/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 55 | 56 | 80 | -------------------------------------------------------------------------------- /app/application.js: -------------------------------------------------------------------------------- 1 | const application = { 2 | onLaunchServer () {}, 3 | onLaunchClient () {}, 4 | } 5 | 6 | export default application 7 | -------------------------------------------------------------------------------- /app/assets/images/404/404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/app/assets/images/404/404.jpg -------------------------------------------------------------------------------- /app/assets/images/icon/icon-alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/app/assets/images/icon/icon-alipay.png -------------------------------------------------------------------------------- /app/assets/images/icon/icon-lzj.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/app/assets/images/icon/icon-lzj.png -------------------------------------------------------------------------------- /app/assets/images/icon/icon-qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/app/assets/images/icon/icon-qr.png -------------------------------------------------------------------------------- /app/assets/images/icon/icon-toutiao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/app/assets/images/icon/icon-toutiao.png -------------------------------------------------------------------------------- /app/assets/images/icon/icon-wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/app/assets/images/icon/icon-wechat.png -------------------------------------------------------------------------------- /app/assets/images/image_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/app/assets/images/image_default.png -------------------------------------------------------------------------------- /app/assets/images/image_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/app/assets/images/image_error.png -------------------------------------------------------------------------------- /app/assets/images/logo/logo-circular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/app/assets/images/logo/logo-circular.png -------------------------------------------------------------------------------- /app/assets/images/logo/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/app/assets/images/logo/logo.jpg -------------------------------------------------------------------------------- /app/assets/images/qr/lzj-wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/app/assets/images/qr/lzj-wechat.jpg -------------------------------------------------------------------------------- /app/assets/scss/bootstrap.scss: -------------------------------------------------------------------------------- 1 | .pull-right { 2 | float: right !important; 3 | } 4 | .pull-left { 5 | float: left !important; 6 | } -------------------------------------------------------------------------------- /app/assets/scss/common.scss: -------------------------------------------------------------------------------- 1 | // variable.scss中定义的所有颜色变量对象组成的数组 2 | $colors: (orange: $orange, white: $white, black: $black, red: $red, grey: $grey, blue: $blue, yellow: $yellow, green: $green); 3 | 4 | // 生成 .f-s-1 { font-size: 1px; } 至 .f-s-50 { font-size: 50px; } 的class 5 | @include create-common-px-style('f-s-', 'font-size', 'px', 1, 50); 6 | // 生成 .m-t-1 { margin-top: 1px; } 至 .m-t-50 { margin-top: 50px; } 的class 7 | @include create-common-px-style('m-t-', 'margin-top', 'px', 1, 50); 8 | // 生成 .m-r-1 { margin-right: 1px; } 至 .m-r-50 { margin-right: 50px; } 的class 9 | @include create-common-px-style('m-r-', 'margin-right', 'px', 1, 50); 10 | // 生成 .m-b-1 { margin-bottom: 1px; } 至 .m-b-50 { margin-bottom: 50px; } 的class 11 | @include create-common-px-style('m-b-', 'margin-bottom', 'px', 1, 50); 12 | // 生成 .m-l-1 { margin-left: 1px; } 至 .m-l-50 { margin-left: 50px; } 的class 13 | @include create-common-px-style('m-l-', 'margin-left', 'px', 1, 50); 14 | // 生成 .p-t-1 { padding-top: 1px; } 至 .p-t-50 { padding-top: 50px; } 的class 15 | @include create-common-px-style('p-t-', 'padding-top', 'px', 1, 50); 16 | // 生成 .p-r-1 { padding-right: 1px; } 至 .p-r-50 { padding-right: 50px; } 的class 17 | @include create-common-px-style('p-r-', 'padding-right', 'px', 1, 50); 18 | // 生成 .p-b-1 { padding-bottom: 1px; } 至 .p-b-50 { padding-bottom: 50px; } 的class 19 | @include create-common-px-style('p-b-', 'padding-bottom', 'px', 1, 50); 20 | // 生成 .p-l-1 { padding-left: 1px; } 至 .p-l-50 { padding-left: 50px; } 的class 21 | @include create-common-px-style('p-l-', 'padding-left', 'px', 1, 50); 22 | 23 | // 生成 .f-orange { color: $orange; } .f-white { color: $white; } ... 24 | @include create-common-color-style($colors, 'f-', 'color'); 25 | 26 | // 内容超出时,使用省略号 27 | .text-ellipsis { 28 | @include text-ellipsis(); 29 | } 30 | 31 | .full-page { 32 | height: 100vh; 33 | } 34 | 35 | // 设置iphone安全区域的颜色为灰白色 36 | .iphone-grey { 37 | @include home-indicator-compatible($default-background); 38 | } 39 | 40 | // 设置iphone安全区域的颜色为白色 41 | .iphone-white { 42 | @include home-indicator-compatible($white); 43 | } 44 | -------------------------------------------------------------------------------- /app/assets/scss/function.scss: -------------------------------------------------------------------------------- 1 | // 创建icon图片样式 2 | @import "./function/create-icon-style"; 3 | // 单行文本 内容超出时,使用省略号 4 | @import "./function/text-ellipsis"; 5 | // 兼容iphone安全区域样式 6 | @import "./function/home-indicator-compatible"; 7 | // 创建全局样式 8 | @import "./function//create-common-style.scss"; -------------------------------------------------------------------------------- /app/assets/scss/function/create-common-style.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 创建通用px相关样式 3 | * @param $class-name: 类名 4 | * @param $style-name: 样式名 5 | * @param $style-value: 样式值 6 | * @param $start: 起始值,默认1 7 | * @param $end: 结束值,默认50 8 | * 9 | * ex: 10 | * 生成 .f-s-10 { font-size: 10px; } 至 .f-s-100 { font-size: 100px; } 11 | * @include create-common-px-style('f-s-', 'font-size', 'px', 10, 100); 12 | */ 13 | @mixin create-common-px-style ($class-name, $style-name, $style-value, $start: 1, $end: 50) { 14 | @for $i from $start through $end { 15 | .#{$class-name}#{$i} { 16 | #{$style-name}: #{$i}px; 17 | } 18 | } 19 | } 20 | 21 | /** 22 | * 创建通用颜色相关样式 23 | * @param $colors: 颜色对象组成的数组 24 | * @param $style-name-pre: 样式名前缀 25 | * @param $style-value: 样式值 26 | * 27 | * ex: 28 | * 生成 .f-orange { color: $orange; } .f-white { color: $white; } ... 29 | * @include create-common-color-style($colors, 'f-', 'color'); 30 | */ 31 | @mixin create-common-color-style ($colors, $style-name-pre, $style-value) { 32 | @each $color-name, $color-value in $colors { 33 | .#{$style-name-pre}#{$color-name} { 34 | #{$style-value}: $color-value; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/assets/scss/function/create-icon-style.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 创建icon图片样式 3 | * @param $background-image: icon图标url 4 | * @param $width: icon图标宽度 默认值16px 5 | * @param $height: icon图标高度 默认值8px 6 | */ 7 | @mixin create-icon-style ($background-image, $width: 16px, $height: 8px) { 8 | display: inline-block; 9 | width: $width; 10 | height: $height; 11 | background-repeat: no-repeat; 12 | background-size: 100% 100%; 13 | background-image: url($background-image); 14 | background-position: center center; 15 | vertical-align: middle; 16 | } -------------------------------------------------------------------------------- /app/assets/scss/function/home-indicator-compatible.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 兼容iphone安全区域样式 3 | * @param $color: 安全区域背景颜色,默认 #E7E8E8 4 | */ 5 | @mixin home-indicator-compatible ($color: #E7E8E8) { 6 | margin-bottom: 0; 7 | margin-bottom: constant(safe-area-inset-bottom); 8 | margin-bottom: env(safe-area-inset-bottom); 9 | 10 | &:after { 11 | content: ''; 12 | width: 100vw; 13 | height: 0; 14 | height: constant(safe-area-inset-bottom); 15 | height: env(safe-area-inset-bottom); 16 | position: fixed; 17 | left: 0; 18 | bottom: 0; 19 | background-color: $color; 20 | } 21 | } -------------------------------------------------------------------------------- /app/assets/scss/function/text-ellipsis.scss: -------------------------------------------------------------------------------- 1 | // 单行文本 内容超出时,使用省略号 2 | @mixin text-ellipsis () { 3 | overflow: hidden; 4 | text-overflow: ellipsis; 5 | white-space: nowrap; 6 | } -------------------------------------------------------------------------------- /app/assets/scss/global.scss: -------------------------------------------------------------------------------- 1 | @import "./bootstrap"; 2 | @import "./common"; 3 | @import "./icon"; 4 | 5 | // 清空浏览器默认样式 6 | * { 7 | box-sizing: border-box; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | html, body, .view-router { 13 | &::-webkit-scrollbar { 14 | display: none 15 | } 16 | } 17 | 18 | /* Chrome 滚动条优化 */ 19 | div { 20 | &::-webkit-scrollbar { 21 | /*滚动条整体样式*/ 22 | width: 8px; /*高宽分别对应横竖滚动条的尺寸*/ 23 | height: 8px; 24 | } 25 | 26 | &::-webkit-scrollbar-thumb { 27 | /*滚动条里面小方块*/ 28 | border-radius: 8px; 29 | background-color: hsla(220, 4%, 58%, 0.3); 30 | transition: background-color 0.3s; 31 | 32 | &:hover { 33 | background: #bbb; 34 | } 35 | } 36 | 37 | &::-webkit-scrollbar-track { 38 | /*滚动条里面轨道*/ 39 | background: #ededed; 40 | } 41 | } 42 | 43 | a { 44 | color: #000; 45 | text-decoration: none; 46 | } 47 | 48 | input:-webkit-autofill { 49 | box-shadow: 0 0 0 1000px white inset; 50 | } 51 | 52 | html { 53 | -webkit-overflow-scrolling: touch; 54 | } 55 | 56 | body { 57 | overflow-x: hidden; 58 | font-size: 14px; 59 | } 60 | 61 | .view-router { 62 | background-color: $default-background; 63 | height: 100vh; 64 | height: calc(100vh - constant(safe-area-inset-bottom)); 65 | height: calc(100vh - env(safe-area-inset-bottom)); 66 | overflow-y: auto; 67 | box-sizing: border-box; 68 | @include home-indicator-compatible($default-background); 69 | 70 | &.view-router-header { 71 | height: calc(100vh - 46px); 72 | height: calc(100vh - 46px - constant(safe-area-inset-bottom)); 73 | height: calc(100vh - 46px - env(safe-area-inset-bottom)); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/assets/scss/icon.scss: -------------------------------------------------------------------------------- 1 | .icon-lzj-logo { 2 | @include create-icon-style('@images/icon/icon-lzj.png', 18px, 18px); 3 | margin-left: 4px; 4 | } 5 | 6 | .icon-wechat { 7 | @include create-icon-style('@images/icon/icon-wechat.png', 18px, 18px); 8 | } 9 | 10 | .icon-alipay { 11 | @include create-icon-style('@images/icon/icon-alipay.png', 18px, 18px); 12 | } 13 | 14 | .icon-toutiao { 15 | @include create-icon-style('@images/icon/icon-toutiao.png', 18px, 18px); 16 | } -------------------------------------------------------------------------------- /app/assets/scss/variable.scss: -------------------------------------------------------------------------------- 1 | $orange: #ed6d3d; 2 | $white: #fff; 3 | $black: #000; 4 | $red: #f00; 5 | $grey: #646464; 6 | $blue: #409EFF; 7 | $yellow: #ff9900; 8 | $green: #19be6b; 9 | // 浏览器默认背景颜色 10 | $default-background: #f5f5f5; 11 | -------------------------------------------------------------------------------- /app/entry.client.js: -------------------------------------------------------------------------------- 1 | import VConsole from 'vconsole' 2 | import vConsoleDecryptNetworkTab from '@lib/vconsole-decrypt-network-tab/index' 3 | import routerBeforeEach, { checkLogin } from '@router/guard/beforeEach' 4 | import createApp from './main' 5 | import application from './application' 6 | 7 | const { app, router, store } = createApp() 8 | 9 | if (window.__INITIAL_STATE__) { 10 | store.replaceState(window.__INITIAL_STATE__) 11 | } 12 | 13 | if (store.state.SHOWCONSOLE === true) { 14 | const vConsole = new VConsole({ 15 | defaultPlugins: ['system', 'element', 'storage'], 16 | maxLogNumber: 5000, 17 | }) 18 | vConsole.addPlugin(vConsoleDecryptNetworkTab) 19 | } 20 | 21 | application.onLaunchClient(store, router, app) 22 | 23 | router.onReady(() => { 24 | { 25 | const rid = new Date() / 1 26 | window.history.replaceState({ rid }, null) 27 | Object.assign(router.currentRoute.meta, { 28 | historyStateRid: rid, 29 | }) 30 | } 31 | 32 | router.afterEach((to, from) => { 33 | if (!window.history?.state?.rid) { 34 | const rid = new Date() / 1 35 | window.history.replaceState({ rid }, null) 36 | } 37 | 38 | Object.assign(to.meta, { 39 | historyStateRid: window.history.state.rid, 40 | }) 41 | 42 | const matched = router.getMatchedComponents(to) 43 | const prevMatched = router.getMatchedComponents(from) 44 | 45 | let diffed = false 46 | const activated = matched.filter((c, i) => { 47 | return diffed || (diffed = (prevMatched[i] !== c)) 48 | }) 49 | 50 | const keepAlive = to?.meta?.keepAlive 51 | const onLoadIsImplement = to?.meta?.onLoadIsImplement 52 | 53 | const onLoadHooks = activated.map(c => { 54 | if (keepAlive && c.onLoad && !onLoadIsImplement) { 55 | Object.assign(to.meta, { 56 | onLoadIsImplement: true, 57 | }) 58 | return c.onLoad 59 | } 60 | }).filter(_ => _) 61 | const onShowHooks = activated.map(c => c.onShow).filter(_ => _) 62 | 63 | if (!onLoadHooks && !onShowHooks.length) { 64 | console.log('there no client onLoad or onShow') 65 | return 66 | } 67 | 68 | Promise.all([...onLoadHooks, ...onShowHooks].map(hook => hook({ 69 | store, 70 | router, 71 | route: to, 72 | lastRoute: from, 73 | }))).then(() => { 74 | }).catch(err => { 75 | __DEVELOPMENT__ && console.log(err) 76 | }) 77 | }) 78 | 79 | routerBeforeEach(router, store) 80 | 81 | app.$mount('#app') 82 | 83 | const needLogin = router.currentRoute?.meta?.login && !store?.state?.infoMember?.online 84 | if (needLogin) { 85 | return checkLogin(router.currentRoute, null, url => { 86 | if (url) { 87 | return router.replace(url) 88 | } 89 | }, store) 90 | } 91 | 92 | if (!router.currentRoute.meta) { 93 | Object.assign(router.currentRoute, { 94 | meta: {}, 95 | }) 96 | } 97 | Object.assign(router.currentRoute.meta, { 98 | onLoadIsImplement: true, 99 | }) 100 | }) 101 | 102 | export { 103 | store, 104 | router, 105 | } 106 | -------------------------------------------------------------------------------- /app/entry.server.js: -------------------------------------------------------------------------------- 1 | import * as storeConstants from '@store/store_constants' 2 | import rest from '@utils/rest' 3 | import createApp from './main' 4 | import application from './application' 5 | 6 | export default context => { 7 | return new Promise((resolve, reject) => { 8 | const { app, router, store } = createApp() 9 | 10 | router.push(context.url) 11 | 12 | router.onReady(() => { 13 | // 所有匹配的组件 14 | const matchedComponents = router.getMatchedComponents(context.url) || [] 15 | if (context.url === '/404?type=redirect') { 16 | return reject(new Error(JSON.stringify({ 17 | code: 302, 18 | originPage: '/404', 19 | }))) 20 | } else if (matchedComponents.length == null || matchedComponents.length === 0) { 21 | return reject(new Error(JSON.stringify({ code: 404 }))) 22 | } 23 | 24 | rest.setStore(store) 25 | 26 | store.commit(storeConstants.BROWSER, { browser: context.browser }) 27 | store.commit(storeConstants.SHOWCONSOLE, { showConsole: context.showConsole }) 28 | if (context.infoMember) { 29 | store.commit(storeConstants.INFOMEMBER, { infoMember: context.infoMember }) 30 | } 31 | if (context.apiRefer) { 32 | store.commit(storeConstants.API_REFER, { apiRefer: context.apiRefer }) 33 | } 34 | if (context.appVersion) { 35 | store.commit(storeConstants.APP_VERSION, { appVersion: context.appVersion }) 36 | } 37 | if (context.authToken) { 38 | store.commit(storeConstants.AUTH_TOKEN, { authToken: context.authToken }) 39 | } 40 | if (context.cookie) { 41 | store.commit(storeConstants.COOKIE, { [storeConstants.COOKIE]: context.cookie }) 42 | } 43 | 44 | const keepAlive = router.currentRoute?.meta?.keepAlive 45 | 46 | const needLogin = router.currentRoute?.meta?.login && !store?.state?.infoMember?.online 47 | 48 | if (needLogin) { 49 | store.commit(storeConstants.AUTH_TOKEN, { authToken: '' }) 50 | context.state = store.state 51 | resolve(app) 52 | } else { 53 | Promise.all([ 54 | application.onLaunchServer(store, router, app), 55 | ...matchedComponents.map(Component => { 56 | if (Component.onLoad && keepAlive) { 57 | return Component.onLoad({ 58 | store, 59 | router, 60 | route: router.currentRoute, 61 | }) 62 | } 63 | }), 64 | ...matchedComponents.map(Component => { 65 | // 存在onShow时,主动调用 66 | if (Component.onShow) { 67 | return Component.onShow({ 68 | store, 69 | router, 70 | route: router.currentRoute, 71 | }) 72 | } 73 | }), 74 | ]).then(() => { 75 | store.commit(storeConstants.AUTH_TOKEN, { authToken: '' }) 76 | context.state = store.state 77 | resolve(app) 78 | }).catch(err => { 79 | if (err.status === 401) { 80 | return reject(new Error(JSON.stringify({ 81 | code: 401, 82 | originPage: '/login', 83 | }))) 84 | } 85 | console.log('服务端初始化渲染出错') 86 | console.log(err) 87 | reject(err) 88 | }) 89 | } 90 | }, reject) 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /app/lib/vconsole-decrypt-network-tab/index.js: -------------------------------------------------------------------------------- 1 | import VConsole from 'vconsole' 2 | import * as utils from '@utils/index' 3 | 4 | export const vConsoleDecryptNetworkTabOption = { 5 | id: 'decryptNetwork', 6 | name: 'DecryptNetwork', 7 | } 8 | 9 | class VConsoleDecryptNetworkTab extends VConsole.VConsoleNetworkPlugin { 10 | onAddTool (callback) { 11 | const toolList = [ 12 | { 13 | name: '打印至控制台', 14 | global: false, 15 | onClick: () => { 16 | this.detailToConsole() 17 | }, 18 | }, 19 | { 20 | name: '清空', 21 | global: false, 22 | onClick: () => { 23 | this.clearLog() 24 | }, 25 | }, 26 | ] 27 | callback(toolList) 28 | } 29 | 30 | detailToConsole () { 31 | const reqList = Object.values(this.reqList || {}).reduce((previous, value) => { 32 | const { url } = value 33 | let { response } = value 34 | try { 35 | response = JSON.parse(response) 36 | } catch (err) { 37 | // err 38 | } 39 | previous.push({ 40 | url, 41 | response, 42 | }) 43 | return previous 44 | }, []) 45 | console.log(reqList) 46 | } 47 | 48 | updateRequest (...args) { 49 | super.updateRequest(...args) 50 | try { 51 | for (const req of Object.values(this.reqList)) { 52 | const { readyState } = req 53 | let { response: oldResponse } = req 54 | if (!oldResponse || oldResponse === '[object Object]' || readyState !== 4) continue 55 | try { 56 | oldResponse = JSON.parse(oldResponse) 57 | } catch (err) { 58 | // err 59 | } 60 | if (utils.getType(oldResponse) === 'string') { 61 | try { 62 | oldResponse = utils.decrypt(oldResponse) 63 | } catch (err) { 64 | __CLIENT__ && console.warn('数据解密失败') 65 | } 66 | } 67 | Object.assign(req, { 68 | response: utils.getType(oldResponse) === 'string' ? oldResponse : JSON.stringify(oldResponse), 69 | }) 70 | } 71 | } catch (err) { 72 | console.error(err) 73 | } 74 | } 75 | } 76 | 77 | const vConsoleDecryptNetworkTab = new VConsoleDecryptNetworkTab(vConsoleDecryptNetworkTabOption.id, vConsoleDecryptNetworkTabOption.name) 78 | 79 | export default vConsoleDecryptNetworkTab 80 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App' 3 | import createRouter from './router' 4 | import '@assets/scss/global' 5 | import createStore from './store' 6 | 7 | Vue.config.productionTip = false 8 | 9 | if (__DEVELOPMENT__) { 10 | Vue.config.devtools = true 11 | } else { 12 | Vue.config.devtools = false 13 | } 14 | 15 | function createApp () { 16 | const router = createRouter() 17 | const store = createStore() 18 | 19 | const app = new Vue({ 20 | router, 21 | store, 22 | render: h => h(App), 23 | }) 24 | return { app, router, store } 25 | } 26 | 27 | export default createApp 28 | -------------------------------------------------------------------------------- /app/mixin/scroll-fixed.js: -------------------------------------------------------------------------------- 1 | // 保存滚动条位置, 适用于 keep-alive 组件 2 | import { debounce } from '@utils/index' 3 | 4 | export default { 5 | data () { 6 | return { 7 | scrollTop: 0, 8 | } 9 | }, 10 | 11 | mounted () { 12 | const vm = this 13 | 14 | vm.$el.addEventListener( 15 | 'scroll', 16 | debounce(() => { 17 | vm.scrollTop = vm.$el.scrollTop 18 | }, 50), 19 | ) 20 | }, 21 | 22 | activated () { 23 | this.$el.scrollTop = this.scrollTop 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /app/router/404.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/404', 4 | name: '404', 5 | component: () => import('@src/404'), 6 | }, 7 | ] 8 | -------------------------------------------------------------------------------- /app/router/guard/beforeEach.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as storeConstants from '@store/store_constants' 3 | 4 | // 判断目标页面是否需要登录,需要则跳转登录页面 5 | export const checkLogin = async (to, from, next, store) => { 6 | const needLogin = to?.meta?.login 7 | if (needLogin) { 8 | if (store?.state?.infoMember?.online) { 9 | next() 10 | } else { 11 | return next(`/login?backUrl=${encodeURIComponent(to.fullPath)}`) 12 | } 13 | } else { 14 | next() 15 | } 16 | } 17 | 18 | // 进入下一个页面时,取消当前页面发出但未接受到响应的所有请求 19 | export const cancelCurrentRest = (to, from, next, store) => { 20 | const { CancelToken } = axios 21 | store.state[storeConstants.UPDATESOURCE].cancel && store.state[storeConstants.UPDATESOURCE].cancel() 22 | store.commit(storeConstants.UPDATESOURCE, { source: CancelToken.source() }) 23 | 24 | if (next) next() 25 | } 26 | 27 | // 记录上一页面路由对象 28 | export const recordPreRoute = (to, from, next, store) => { 29 | store.commit(storeConstants.PRE_ROUTE, { 30 | [storeConstants.PRE_ROUTE]: from, 31 | }) 32 | if (next) next() 33 | } 34 | 35 | // 路由前置守卫 36 | const routerBeforeEach = (router, store) => { 37 | // 所有需要执行的前置守卫数组 38 | const routerBeforeEachs = [ 39 | checkLogin, 40 | cancelCurrentRest, 41 | recordPreRoute, 42 | ] 43 | 44 | routerBeforeEachs.map(fun => { 45 | router.beforeEach((to, from, next) => { 46 | fun(to, from, next, store) 47 | }) 48 | }) 49 | } 50 | 51 | export default routerBeforeEach 52 | -------------------------------------------------------------------------------- /app/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import * as storeConstants from '@store/store_constants' 4 | 5 | import layout from './layout' 6 | import login from './login' 7 | import page404 from './404' 8 | 9 | // 重写Router原型上的push、replace、go函数 10 | const routerFnArr = ['push', 'replace', 'go'] 11 | 12 | routerFnArr.map(item => { 13 | Router.prototype[item] = (fnName => { 14 | if (__SERVER__) return fnName 15 | const newFnName = function newFnName (...rest) { 16 | const { store } = require('../entry.client') 17 | store.commit(storeConstants.ISTOUCHMOVE, { 18 | isTouchMove: false, 19 | }) 20 | // 无上一路由时不执行go,直接重定向至首页 21 | if (item === 'go') { 22 | const preRoute = store.state[storeConstants.PRE_ROUTE] 23 | if (!preRoute?.path) { 24 | return this.replace('/') 25 | } 26 | } 27 | const res = fnName.apply(this, rest) 28 | if (res?.catch) { 29 | res.catch(err => err) 30 | } 31 | return res 32 | } 33 | return newFnName 34 | })(Router.prototype[item]) 35 | }) 36 | 37 | Vue.use(Router) 38 | 39 | function createRouter () { 40 | const routes = [ 41 | ...layout, 42 | ...login, 43 | ...page404, 44 | ] 45 | if (__CLIENT__) { 46 | routes.push({ 47 | path: '*', 48 | redirect: '/404', 49 | }) 50 | } 51 | const RouterModel = new Router({ 52 | mode: 'history', 53 | routes, 54 | }) 55 | 56 | return RouterModel 57 | } 58 | 59 | export default createRouter 60 | -------------------------------------------------------------------------------- /app/router/layout.js: -------------------------------------------------------------------------------- 1 | const layout = [ 2 | { 3 | path: '/', 4 | name: 'layout', 5 | meta: { 6 | keepAlive: true, 7 | login: true, 8 | }, 9 | component: () => import('@src/layout/index'), 10 | children: [ 11 | { 12 | path: '/lzj', 13 | name: 'lzj', 14 | meta: { 15 | role: 3, 16 | title: 'lizj小程序', 17 | icon: 'icon-lzj-logo', 18 | keepAlive: true, 19 | }, 20 | component: () => import('@src/layout/layout-empty'), 21 | children: [ 22 | { 23 | path: '/lzj/wechat', 24 | name: 'lzj-wechat', 25 | meta: { 26 | role: 3, 27 | title: 'lizj微信小程序', 28 | icon: 'icon-wechat', 29 | keepAlive: true, 30 | }, 31 | component: () => import('@src/lzj/wechat'), 32 | }, 33 | { 34 | path: '/lzj/alipay', 35 | name: 'lzj-alipay', 36 | meta: { 37 | role: 3, 38 | title: 'lizj支付宝小程序', 39 | icon: 'icon-alipay', 40 | keepAlive: true, 41 | }, 42 | component: () => import('@src/lzj/alipay'), 43 | }, 44 | ], 45 | }, 46 | { 47 | path: '/system', 48 | name: 'system', 49 | meta: { 50 | role: 1, 51 | title: '系统管理', 52 | icon: 'el-icon-s-cooperation', 53 | keepAlive: true, 54 | }, 55 | component: () => import('@src/layout/layout-empty'), 56 | children: [ 57 | { 58 | path: '/system/user', 59 | name: 'system-user', 60 | meta: { 61 | role: 1, 62 | title: '用户管理', 63 | icon: 'el-icon-s-custom', 64 | keepAlive: true, 65 | }, 66 | component: () => import('@src/system/user'), 67 | }, 68 | { 69 | path: '/system/task', 70 | name: 'system-task', 71 | meta: { 72 | role: 1, 73 | title: '任务管理', 74 | icon: 'el-icon-s-order', 75 | keepAlive: true, 76 | }, 77 | component: () => import('@src/system/task'), 78 | }, 79 | ], 80 | }, 81 | ], 82 | }, 83 | ] 84 | 85 | export default layout 86 | -------------------------------------------------------------------------------- /app/router/login.js: -------------------------------------------------------------------------------- 1 | const member = [ 2 | { 3 | path: '/login', 4 | name: 'password_login', 5 | component: () => import('@src/login/index'), 6 | meta: { 7 | keepAlive: true, 8 | }, 9 | }, 10 | ] 11 | 12 | export default member 13 | -------------------------------------------------------------------------------- /app/src/404.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 34 | 35 | 88 | -------------------------------------------------------------------------------- /app/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 49 | 50 | 76 | -------------------------------------------------------------------------------- /app/src/layout/layout-empty.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /app/src/layout/layout-header.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 155 | 156 | 225 | -------------------------------------------------------------------------------- /app/src/layout/layout-main.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 54 | -------------------------------------------------------------------------------- /app/src/layout/layout-slidebar.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 98 | 99 | 117 | -------------------------------------------------------------------------------- /app/src/layout/layout-sub-slidebar.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /app/src/login/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 159 | 160 | 256 | -------------------------------------------------------------------------------- /app/src/lzj/alipay.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | -------------------------------------------------------------------------------- /app/src/lzj/toutiao.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | -------------------------------------------------------------------------------- /app/src/lzj/wechat.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 30 | -------------------------------------------------------------------------------- /app/src/system/task.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 237 | 238 | 259 | -------------------------------------------------------------------------------- /app/src/system/user.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 363 | 364 | 405 | -------------------------------------------------------------------------------- /app/store/actions.js: -------------------------------------------------------------------------------- 1 | import rest from '@utils/rest' 2 | import * as storeConstants from './store_constants' 3 | 4 | const actions = { 5 | // 更新用户信息 6 | async reloadInfoMember ({ commit, state }) { 7 | if (!state?.infoMember?.online) { 8 | __DEVELOPMENT__ && console.error('未登录无法更新用户信息') 9 | return 10 | } 11 | const infoMember = await rest.get('/user/update_user_info') 12 | if (infoMember.error_code) return 13 | commit(storeConstants.INFOMEMBER, { 14 | infoMember, 15 | }) 16 | return infoMember 17 | }, 18 | // 退出登录 19 | async logout ({ commit, state }) { 20 | if (!state.infoMember.online) { 21 | __DEVELOPMENT__ && console.log('用户未登录') 22 | return 23 | } 24 | const [infoMember] = await Promise.all([ 25 | rest.post('/user/logout'), 26 | rest.post('/auto_info/destroy_session'), 27 | ]) 28 | 29 | commit(storeConstants.INFOMEMBER, { 30 | infoMember, 31 | }) 32 | rest.deleteToken() 33 | return infoMember 34 | }, 35 | } 36 | 37 | export default actions 38 | -------------------------------------------------------------------------------- /app/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = {} 2 | 3 | export default getters 4 | -------------------------------------------------------------------------------- /app/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import state from './state' 4 | import getters from './getters' 5 | import mutations from './mutations' 6 | import actions from './actions' 7 | import modules from './modules/modules' 8 | 9 | Vue.use(Vuex) 10 | 11 | function createStore () { 12 | return new Vuex.Store({ 13 | strict: !__PRODUCTION__, 14 | state, 15 | getters, 16 | mutations, 17 | actions, 18 | modules, 19 | }) 20 | } 21 | 22 | export default createStore 23 | -------------------------------------------------------------------------------- /app/store/modules/init_data/actions.js: -------------------------------------------------------------------------------- 1 | const actions = {} 2 | 3 | export default actions 4 | -------------------------------------------------------------------------------- /app/store/modules/init_data/getters.js: -------------------------------------------------------------------------------- 1 | const getters = {} 2 | 3 | export default getters 4 | -------------------------------------------------------------------------------- /app/store/modules/init_data/index.js: -------------------------------------------------------------------------------- 1 | import actions from './actions' 2 | import getters from './getters' 3 | import mutations from './mutations' 4 | import state from './state' 5 | 6 | const initDataModules = { 7 | state, 8 | getters, 9 | mutations, 10 | actions, 11 | } 12 | 13 | export default initDataModules 14 | -------------------------------------------------------------------------------- /app/store/modules/init_data/mutations.js: -------------------------------------------------------------------------------- 1 | const mutations = {} 2 | 3 | export default mutations 4 | -------------------------------------------------------------------------------- /app/store/modules/init_data/state.js: -------------------------------------------------------------------------------- 1 | const state = () => ({}) 2 | 3 | export default state 4 | -------------------------------------------------------------------------------- /app/store/modules/modules.js: -------------------------------------------------------------------------------- 1 | import initDataModules from './init_data/index' 2 | 3 | const modules = { 4 | initData: initDataModules, 5 | } 6 | 7 | export default modules 8 | -------------------------------------------------------------------------------- /app/store/mutations.js: -------------------------------------------------------------------------------- 1 | import * as storeConstants from './store_constants' 2 | 3 | const mutations = { 4 | // 用户设备信息 5 | [storeConstants.BROWSER] (state, payload) { 6 | const newState = { 7 | browser: payload.browser, 8 | } 9 | Object.assign(state, newState) 10 | }, 11 | // 移动端调试工具 12 | [storeConstants.SHOWCONSOLE] (state, payload) { 13 | const newState = { 14 | [storeConstants.SHOWCONSOLE]: payload.showConsole, 15 | } 16 | Object.assign(state, newState) 17 | }, 18 | // 进入下一个页面时,取消当前页面发出但未接受到响应的所有请求 19 | [storeConstants.UPDATESOURCE] (state, payload) { 20 | const newState = { 21 | [storeConstants.UPDATESOURCE]: payload.source, 22 | } 23 | Object.assign(state, newState) 24 | }, 25 | // 修改用户信息 26 | [storeConstants.INFOMEMBER] (state, payload) { 27 | const newState = { 28 | infoMember: payload.infoMember, 29 | } 30 | Object.assign(state, newState) 31 | }, 32 | // 切换是否处于滑动状态 33 | [storeConstants.ISTOUCHMOVE] (state, payload) { 34 | const newState = { 35 | isTouchMove: payload.isTouchMove, 36 | } 37 | Object.assign(state, newState) 38 | }, 39 | // 保存渠道 40 | [storeConstants.API_REFER] (state, payload) { 41 | const newState = { 42 | apiRefer: payload.apiRefer, 43 | } 44 | Object.assign(state, newState) 45 | }, 46 | // 保存版本 47 | [storeConstants.APP_VERSION] (state, payload) { 48 | const newState = { 49 | appVersion: payload.appVersion, 50 | } 51 | Object.assign(state, newState) 52 | }, 53 | // 保存用户token 54 | [storeConstants.AUTH_TOKEN] (state, payload) { 55 | const newState = { 56 | authToken: payload.authToken, 57 | } 58 | Object.assign(state, newState) 59 | }, 60 | // 记录前一路由对象 61 | [storeConstants.PRE_ROUTE] (state, payload) { 62 | const newState = { 63 | [storeConstants.PRE_ROUTE]: payload[storeConstants.PRE_ROUTE], 64 | } 65 | Object.assign(state, newState) 66 | }, 67 | // 修改菜单栏是否展开 68 | [storeConstants.IS_OPEN_NAV] (state, payload) { 69 | const newState = { 70 | isOpenNav: payload[storeConstants.IS_OPEN_NAV], 71 | } 72 | Object.assign(state, newState) 73 | }, 74 | // 存储cookie 75 | [storeConstants.COOKIE] (state, payload) { 76 | const newState = { 77 | cookie: payload[storeConstants.COOKIE], 78 | } 79 | Object.assign(state, newState) 80 | }, 81 | } 82 | 83 | export default mutations 84 | -------------------------------------------------------------------------------- /app/store/state.js: -------------------------------------------------------------------------------- 1 | import * as storeConstants from './store_constants' 2 | 3 | const state = () => ({ 4 | // 当前用户设备信息 5 | browser: {}, 6 | // 是否显示移动端调试工具 7 | [storeConstants.SHOWCONSOLE]: false, 8 | // 存放取消当前页面发出但未接受到响应的所有请求的函数与token 9 | [storeConstants.UPDATESOURCE]: { 10 | token: null, 11 | cancel: null, 12 | }, 13 | // 用户信息 14 | infoMember: { 15 | online: false, 16 | }, 17 | // 是否处于滑动状态中,用于兼容ios转场动画问题 18 | isTouchMove: false, 19 | // 渠道 20 | apiRefer: '', 21 | // 版本 22 | appVersion: '', 23 | // 用户token(仅在服务端渲染阶段可用,输出至客户端后会被删除) 24 | authToken: '', 25 | // 记录前一路由对象 26 | [storeConstants.PRE_ROUTE]: {}, 27 | // 菜单栏是否展开 28 | isOpenNav: false, 29 | // cookie 30 | cookie: '', 31 | }) 32 | 33 | export default state 34 | -------------------------------------------------------------------------------- /app/store/store_constants.js: -------------------------------------------------------------------------------- 1 | // 用户设备信息 2 | export const BROWSER = 'BROWSER' 3 | 4 | // 是否显示移动端调试工具 5 | export const SHOWCONSOLE = 'SHOWCONSOLE' 6 | 7 | // 进入下一个页面时,取消当前页面发出但未接受到响应的所有请求 8 | export const UPDATESOURCE = 'UPDATESOURCE' 9 | 10 | // 用户信息 11 | export const INFOMEMBER = 'INFOMEMBER' 12 | 13 | // 是否处于滑动状态中 14 | export const ISTOUCHMOVE = 'ISTOUCHMOVE' 15 | 16 | // 渠道 17 | export const API_REFER = 'API_REFER' 18 | 19 | // 版本 20 | export const APP_VERSION = 'APP_VERSION' 21 | 22 | // 用户token 23 | export const AUTH_TOKEN = 'AUTH_TOKEN' 24 | 25 | // 前一路由对象 26 | export const PRE_ROUTE = 'PRE_ROUTE' 27 | 28 | // 菜单栏是否展开 29 | export const IS_OPEN_NAV = 'IS_OPEN_NAV' 30 | 31 | // 存储cookie 32 | export const COOKIE = 'COOKIE' 33 | -------------------------------------------------------------------------------- /app/utils/browser.js: -------------------------------------------------------------------------------- 1 | import { version } from 'process' 2 | 3 | const spiders = [ 4 | 'TencentTraveler', 5 | 'Baidu-YunGuanCe', 6 | 'Baiduspider+', 7 | 'BaiduGame', 8 | 'Googlebot', 9 | 'msnbot', 10 | 'Sosospider+', 11 | 'Sogou web spider', 12 | 'ia_archiver', 13 | 'Yahoo! Slurp', 14 | 'YoudaoBot', 15 | 'Yahoo Slurp', 16 | 'MSNBot', 17 | 'Java (Often spam bot)', 18 | 'BaiDuSpider', 19 | 'Voila', 20 | 'Yandex bot', 21 | 'BSpider', 22 | 'twiceler', 23 | 'Sogou Spider', 24 | 'Speedy Spider', 25 | 'Google AdSense', 26 | 'Heritrix', 27 | 'Python-urllib', 28 | 'Alexa (IA Archiver)', 29 | 'Ask', 30 | 'Exabot', 31 | 'Custo', 32 | 'OutfoxBot/YodaoBot', 33 | 'yacy', 34 | 'SurveyBot', 35 | 'legs', 36 | 'lwp-trivial', 37 | 'Nutch', 38 | 'StackRambler', 39 | 'The web archive (IA Archiver)', 40 | 'Perl tool', 41 | 'MJ12bot', 42 | 'Netcraft', 43 | 'MSIECrawler', 44 | 'WGet tools', 45 | 'larbin', 46 | 'Fish search', 47 | ] 48 | 49 | const isSpiders = (userAgent) => { 50 | const len = spiders.length 51 | for (let i = 0; i < len; i++) { 52 | const obj = spiders[i] 53 | if (userAgent.toLowerCase().indexOf(obj.toLowerCase()) !== -1) { 54 | return true 55 | } 56 | } 57 | 58 | return false 59 | } 60 | 61 | // 获取系统语言 62 | const getLanguage = req => { 63 | const g = (req.headers['accept-language'] || (__CLIENT__ && navigator.language) || '不详').toLowerCase().split(';')[0].split(',')[0] 64 | return g === escape('zh-cn') 65 | } 66 | 67 | export const getBrowser = (req = { headers: {} }) => { 68 | const xRequestedWith = req.headers['x-requested-with'] || '' 69 | let u = req.headers['user-agent'] 70 | const navigator = __CLIENT__ ? window.navigator : {} 71 | u = u || navigator.userAgent || '' 72 | const reg = new RegExp(/\([^)]+\)/) 73 | const info = { 74 | // 系统详细型号 75 | system: escape( 76 | u.match(reg) 77 | && u.match(reg)[0] 78 | && (u.match(reg)[0].split(';')[1] 79 | && u.match(reg)[0].split(';')[1].replace(/^\(|^ | $|\)$/ig, '')) 80 | || '不详', 81 | ), 82 | // 设备: pc/移动/平板 83 | device: 'PC', 84 | // 系统语言 85 | language: getLanguage(req), 86 | } 87 | const match = { 88 | // 内核 89 | Node: u.indexOf('node') !== -1, 90 | Trident: u.indexOf('Trident') > 0, 91 | Presto: u.indexOf('Presto') > 0, 92 | WebKit: u.indexOf('AppleWebKit') > 0, 93 | Gecko: u.indexOf('Gecko') > 0, 94 | // 浏览器 95 | Spider: isSpiders(u), 96 | UC: u.indexOf('UC') > 0 || u.indexOf('UBrowser') > 0, 97 | QQ: u.indexOf('QQBrowser') > 0, 98 | DouYin: u.includes('douyin') || u.includes('ByteLocale') || u.includes('ByteFullLocale'), 99 | WeiXin: u.indexOf('MicroMessenger') !== -1, 100 | BaiDu: u.indexOf('Baidu') > 0 || u.indexOf('BIDUBrowser') > 0, 101 | Alipay: (u.includes('AlipayDefined') || u.includes('AlipayClient') || u.includes('AliApp')) && !u.includes('DingTalk') && !u.includes('MiniProgram'), 102 | AliApplet: (u.includes('AlipayDefined') || u.includes('AlipayClient') || u.includes('AliApp')) && u.includes('MiniProgram'), 103 | ToutiaoApplet: false, 104 | BaiduApplet: false, 105 | Weibo: u.indexOf('Weibo') !== -1, 106 | Maxthon: u.indexOf('Maxthon') > 0, 107 | SouGou: u.indexOf('MetaSr') > 0 || u.indexOf('Sogou') > 0, 108 | IE: u.indexOf('MSIE') > 0, 109 | Firefox: u.indexOf('Firefox') > 0, 110 | Opera: u.indexOf('Opera') > 0 || u.indexOf('OPR') > 0, 111 | Safari: u.indexOf('Safari') > 0, 112 | Chrome: u.indexOf('Chrome') > 0 || u.indexOf('CriOS') > 0, 113 | // 系统或平台 114 | Windows: u.indexOf('Windows') > 0, 115 | Mac: u.indexOf('Macintosh') > 0, 116 | Android: u.indexOf('Android') > 0 || u.indexOf('Adr') > 0, 117 | WP: u.indexOf('IEMobile') > 0, 118 | BlackBerry: u.indexOf('BlackBerry') > 0 || u.indexOf('RIM') > 0 || u.indexOf('BB') > 0, 119 | MeeGo: u.indexOf('MeeGo') > 0, 120 | Symbian: u.indexOf('Symbian') > 0, 121 | iOS: u.indexOf('like Mac OS X') > 0 || u.indexOf('ios') !== -1 || u.indexOf('iOS') !== -1 || u.indexOf('IOS') !== -1, 122 | iPhone: u.indexOf('iPh') > 0, 123 | iPad: u.indexOf('iPad') > 0, 124 | // 设备 125 | Mobile: u.indexOf('Mobi') > 0 || u.indexOf('iPh') > 0 || u.indexOf('480') > 0, 126 | Tablet: u.indexOf('Tablet') > 0 || u.indexOf('iPad') > 0 || u.indexOf('Nexus 7') > 0, 127 | } 128 | // 修正 129 | if (!match.Trident) { 130 | match.Trident = match.IE 131 | } 132 | 133 | if (match.Gecko) { 134 | match.Gecko = !match.WebKit 135 | } 136 | 137 | if (match.QQ) { 138 | match.QQ = !match.WeiXin 139 | } 140 | 141 | if (match.Chrome) { 142 | match.Chrome = !(match.Opera + match.BaiDu + match.Maxthon + match.SouGou + match.UC + match.QQ + match.WeiXin) 143 | } 144 | 145 | if (match.Safari) { 146 | match.Safari = !(match.Chrome + match.Opera + match.BaiDu + match.Maxthon + match.SouGou + match.UC + match.QQ + match.WeiXin) 147 | } 148 | 149 | if (match.Mobile) { 150 | match.Mobile = !match.iPad 151 | } 152 | // 信息输出 153 | const hash = { 154 | // 浏览器引擎 155 | engine: ['Trident', 'Presto', 'WebKit', 'Gecko', 'Node'], 156 | // 浏览器外壳 157 | browser: ['UC', 'QQ', 'BaiDu', 'Alipay', 'ToutiaoApplet', 'AliApplet', 'Weibo', 'Maxthon', 'SouGou', 'IE', 'Firefox', 'Opera', 'Safari', 'Chrome', 'WeiXin', 'Node', 'Spider', 'GZRCB', 'ZMXY', 'ZJHT', 'Mina', 'JD'], 158 | // 系统类别 159 | os: ['Windows', 'Mac', 'Android', 'WP', 'BlackBerry', 'MeeGo', 'Symbian', 'iOS', 'iPhone', 'iPad', 'Node'], 160 | // 设备 161 | device: ['Mobile', 'Tablet', 'Node'], 162 | } 163 | 164 | Object.entries(hash).map(([key, value]) => { 165 | value.map(v => { 166 | if (Reflect.has(match, v)) { 167 | info[key] = version 168 | } 169 | }) 170 | }) 171 | 172 | return { 173 | versions: match, 174 | info, 175 | } 176 | } 177 | 178 | export default function (self) { 179 | const root = self 180 | let isBrowserSide = false 181 | if (__CLIENT__ && root === window) { 182 | isBrowserSide = true 183 | } 184 | 185 | return isBrowserSide 186 | } 187 | -------------------------------------------------------------------------------- /app/utils/browserNotify.js: -------------------------------------------------------------------------------- 1 | export function browserNotify ({ title = '小程序发布平台温馨提示', content }) { 2 | const notify = () => new Notification(title, { body: content, icon: '/logo-circular.png' }) 3 | return new Promise(resolve => { 4 | if (!Reflect.has(window, 'Notification')) { 5 | resolve({ 6 | msg: 'error', 7 | }) 8 | } else if (Notification.permission === 'granted') { 9 | notify() 10 | resolve({ 11 | msg: 'success', 12 | }) 13 | } else if (Notification.permission !== 'denied') { 14 | Notification.requestPermission().then(result => { 15 | if (result === 'granted') { 16 | notify() 17 | resolve({ 18 | msg: 'success', 19 | }) 20 | } 21 | }) 22 | } 23 | }) 24 | } 25 | 26 | export default browserNotify 27 | -------------------------------------------------------------------------------- /app/utils/channel.js: -------------------------------------------------------------------------------- 1 | export class Channel { 2 | constructor (store) { 3 | this.store = store || null 4 | this.versions = null 5 | this.dictionary = [ 6 | // 微信小程序 7 | 'WechatApplet', 8 | // m站 9 | 'MobileSite', 10 | // 支付宝 11 | 'Alipay', 12 | ] 13 | 14 | this._init() 15 | } 16 | 17 | _init () { 18 | const { dictionary } = this 19 | 20 | dictionary.forEach(key => { 21 | this[`is${key}`] = () => this._getVersions()[key] 22 | }) 23 | } 24 | 25 | _getVersions () { 26 | if (this.store) { 27 | return this.store?.state?.browser?.versions || {} 28 | } else if (!__CLIENT__) { 29 | return {} 30 | } else { 31 | this.store = require('../entry.client').store 32 | return this.store?.state?.browser?.versions || {} 33 | } 34 | } 35 | 36 | isApplet () { 37 | return this.isWechatApplet() 38 | } 39 | 40 | isAlibaba () { 41 | return false 42 | } 43 | 44 | isWechatApplet () {} 45 | } 46 | 47 | export default new Channel() 48 | -------------------------------------------------------------------------------- /app/utils/constants.js: -------------------------------------------------------------------------------- 1 | // 存放常量 2 | import channel from './channel' 3 | 4 | // header组件高度,在小程序、app中为0,m站中为46 5 | export const headerHeight = () => (!channel.isApplet() ? 46 : 0) 6 | 7 | // 小程序key value字典 8 | export const releaseMap = new Map([ 9 | ['lzj_wechat', 'lizj微信小程序'], 10 | ['lzj_alipay', 'lizj支付宝小程序'], 11 | ]) 12 | 13 | export default headerHeight 14 | -------------------------------------------------------------------------------- /app/utils/index.js: -------------------------------------------------------------------------------- 1 | import { Message, MessageBox } from 'element-ui' 2 | 3 | // 防抖工厂函数 触发函数后等待wait时间(单位ms)才能再次触发,等待期间再次触发函数须要重新等待wait时间才能再次触发 4 | export function debounce (fun, wait = 500) { 5 | let timeout = null 6 | return (...rest) => { 7 | let result 8 | if (timeout == null) { 9 | result = fun.apply(this, rest) 10 | } else { 11 | __DEVELOPMENT__ && console.log(`${fun.name}处于防抖时间中,${wait}ms后可再次触发该函数`) 12 | } 13 | timeout = setTimeout(() => { 14 | timeout = null 15 | }, wait) 16 | return result 17 | } 18 | } 19 | 20 | // 节流工厂函数 触发函数后等待wait时间(单位ms)才能再次触发,等待期间再次触发函数不会重置wait时间 21 | export function throttle (fun, wait = 2000) { 22 | let previous = 0 23 | return (...rest) => { 24 | const now = Date.now() 25 | if (now - previous > wait) { 26 | fun.apply(this, rest) 27 | previous = now 28 | } else { 29 | __DEVELOPMENT__ && console.log(`函数${fun.name}处于节流时间中,${wait - (now - previous)}ms后可再次触发`) 30 | } 31 | } 32 | } 33 | 34 | // 睡眠函数 35 | export function sleep (timer = 500) { 36 | return new Promise(resolve => { 37 | setTimeout(resolve, timer) 38 | }) 39 | } 40 | 41 | // 获取uuid 42 | export function getUUID () { 43 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { 44 | const r = Math.floor(Math.random() * 16) 45 | const v = c === 'x' ? r : (r && 0x3) || 0x8 46 | return v.toString(16) 47 | }) 48 | } 49 | 50 | // 模态确认框 51 | export function alert (msg, title, options) { 52 | return MessageBox.alert(msg, title, options) 53 | } 54 | 55 | // 模态选择框 56 | export function confirm (msg, title, options) { 57 | return new Promise(resolve => { 58 | MessageBox.confirm(msg, title, options) 59 | .then(() => { 60 | resolve(true) 61 | }).catch(() => { 62 | resolve(false) 63 | }) 64 | }) 65 | } 66 | 67 | // 提示框 68 | export function message (msg, type, option = {}) { 69 | Message({ 70 | showClose: true, 71 | center: true, 72 | ...option, 73 | message: msg, 74 | type, 75 | }) 76 | } 77 | 78 | // 获取变量类型 79 | export function getType (element) { 80 | const initData = Object.prototype.toString.call(element) 81 | const configure = [ 82 | ['[object String]', 'string'], 83 | ['[object Number]', 'number'], 84 | ['[object Boolean]', 'boolean'], 85 | ['[object Null]', 'null'], 86 | ['[object Undefined]', 'undefined'], 87 | ['[object Date]', 'object'], 88 | ['[object Regexp]', 'object'], 89 | ['[object Arguments]', 'object'], 90 | ['[object Error]', 'object'], 91 | ['[object Math]', 'object'], 92 | ['[object JSON]', 'object'], 93 | ['[object Function]', 'function'], 94 | ['[object Array]', 'array'], 95 | ['[object Object]', 'object'], 96 | ['[object Symbol]', 'symbol'], 97 | ['[object Bigint', 'bigint'], 98 | ] 99 | const map = new Map(configure) 100 | return map.get(initData) 101 | } 102 | 103 | // 获取变量实际类型 104 | export function getActualType (element) { 105 | const initData = Object.prototype.toString.call(element) 106 | const configure = [ 107 | ['[object String]', 'string'], 108 | ['[object Number]', 'number'], 109 | ['[object Boolean]', 'boolean'], 110 | ['[object Null]', 'null'], 111 | ['[object Undefined]', 'undefined'], 112 | ['[object Date]', 'date'], 113 | ['[object Regexp]', 'regexp'], 114 | ['[object Arguments]', 'arguments'], 115 | ['[object Error]', 'error'], 116 | ['[object Math]', 'math'], 117 | ['[object JSON]', 'json'], 118 | ['[object Function]', 'function'], 119 | ['[object Array]', 'array'], 120 | ['[object Object]', 'object'], 121 | ['[object Symbol]', 'symbol'], 122 | ['[object Bigint', 'bigint'], 123 | ] 124 | const map = new Map(configure) 125 | return map.get(initData) 126 | } 127 | 128 | // 对称加密数据进行解密 129 | export function decrypt (data) { 130 | const aes = require('crypto-js/aes') 131 | const utf8 = require('crypto-js/enc-utf8') 132 | const encryptConfig = { 133 | key: 'sdxduerhpcfxuslj', 134 | iv: 'vfbwacgikomeusip', 135 | encrypt: true, 136 | } 137 | if (encryptConfig.encrypt) { 138 | const bytes = aes.decrypt(data, encryptConfig.key, { 139 | iv: encryptConfig.iv, 140 | }) 141 | return JSON.parse(bytes.toString(utf8)) 142 | } 143 | return data 144 | } 145 | 146 | // @param src 图片路径 147 | // @param maxSize 图片最大像素,单位百万,如果传入图片大于最大像素,则压缩至最大像素以下并返回,默认1百万 148 | export function compressImage (src, maxSize = 1) { 149 | return new Promise((resolve, reject) => { 150 | if (!__CLIENT__) return reject(new Error('非客户端')) 151 | try { 152 | // eslint-disable-next-line no-param-reassign 153 | maxSize = maxSize * 1000 * 1000 154 | const { Image } = window 155 | const canvas = document.createElement('canvas') 156 | const ctx = canvas.getContext('2d') 157 | const img = new Image() 158 | img.crossOrigin = 'anonymous' 159 | img.src = src 160 | img.onload = () => { 161 | // 压缩率 162 | const ratio = Math.sqrt((img.width * img.height) / maxSize) 163 | if (ratio <= 1) { 164 | resolve(src) 165 | return 166 | } 167 | const width = Math.floor(img.width / ratio) 168 | const height = Math.floor(img.height / ratio) 169 | 170 | // 压缩后大小 171 | canvas.width = width 172 | canvas.height = height 173 | 174 | // 为png图设置底色 175 | ctx.fillStyle = '#fff' 176 | ctx.fillRect(0, 0, canvas.width, canvas.height) 177 | 178 | // 兼容ios,图片大于1M时,分批绘制 179 | if (width * height > 1 * 1000 * 1000) { 180 | const count = Math.floor(Math.sqrt(width * height / 1 * 1000 * 1000) + 1) 181 | const cellCanvas = document.createElement('canvas') 182 | const cellCtx = cellCanvas.getContext('2d') 183 | const cellWidth = Math.floor(width / count) 184 | const cellHeight = Math.floor(height / count) 185 | cellCanvas.width = cellWidth 186 | cellCanvas.height = cellHeight 187 | for (let i = 0; i < count; i++) { 188 | for (let j = 0; j < count; j++) { 189 | cellCtx.drawImage(img, i * cellWidth * ratio, j * cellHeight * ratio, cellWidth * ratio, cellHeight * ratio, 0, 0, cellWidth, cellHeight) 190 | ctx.drawImage(cellCanvas, i * cellWidth, j * cellHeight, cellWidth, cellHeight) 191 | } 192 | } 193 | cellCtx.remove() 194 | } else { 195 | ctx.drawImage(img, 0, 0, width, height) 196 | } 197 | const newSrc = canvas.toDataURL('image/jpeg', 0.5) 198 | canvas.remove() 199 | img.remove() 200 | resolve(newSrc) 201 | } 202 | } catch (err) { 203 | resolve(src) 204 | } 205 | }) 206 | } 207 | -------------------------------------------------------------------------------- /app/utils/socket.js: -------------------------------------------------------------------------------- 1 | import { io } from 'socket.io-client' 2 | import { browserNotify } from './browserNotify' 3 | import { releaseMap } from './constants' 4 | 5 | export default class Socket { 6 | constructor (userId, callback = {}) { 7 | this.socket = io(`/?userId=${userId}`) 8 | this.callback = callback 9 | this._init() 10 | } 11 | 12 | // 初始化监听事件 13 | _init () { 14 | this.createTask() 15 | this.updataTask() 16 | this.confirmTask() 17 | this.previewUpdataTask() 18 | } 19 | 20 | // 监听创建任务 21 | createTask () { 22 | this.socket.on('createTask', msg => { 23 | const { callback = {} } = this 24 | const { result = {} } = msg 25 | callback.createTask && callback.createTask(result) 26 | }) 27 | } 28 | 29 | // 监听更新任务 30 | updataTask () { 31 | this.socket.on('updataTask', msg => { 32 | const { callback = {} } = this 33 | const { result = {} } = msg 34 | callback.updataTask && callback.updataTask(result) 35 | }) 36 | } 37 | 38 | // 监听完成任务 39 | confirmTask () { 40 | this.socket.on('confirmTask', msg => { 41 | const { result = '' } = msg 42 | browserNotify({ content: `${releaseMap.get(result)}发布成功` }) 43 | }) 44 | } 45 | 46 | // 监听预览任务 47 | previewUpdataTask () { 48 | this.socket.on('previewUpdataTask', msg => { 49 | const { callback = {} } = this 50 | const { result = '' } = msg 51 | callback.updataPreviewTask && callback.updataPreviewTask(result) 52 | }) 53 | } 54 | 55 | // 断开连接 56 | disconnect () { 57 | if (this.socket) { 58 | this.socket.disconnect() 59 | this.socket = null 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/utils/strategies.js: -------------------------------------------------------------------------------- 1 | class Strategies { 2 | // 非空验证 3 | empty (value, errorMsg) { 4 | if (value === '') { 5 | return errorMsg || '填写数据不能为空' 6 | } 7 | return false 8 | } 9 | 10 | // 限制最小长度 11 | minLength (value, length, errorMsg) { 12 | if (!value || value.length < length) { 13 | return errorMsg || `填写数据长度不能小于${length}位` 14 | } 15 | return false 16 | } 17 | 18 | // 手机号码验证 19 | mobile (value, errorMsg) { 20 | if (!/^((\(\d{2,3}\))|(\d{3}\-))?1[^012][\d*]{9}$/.test(value)) { 21 | return errorMsg || '请正确填写手机号码' 22 | } 23 | return false 24 | } 25 | 26 | // 验证码验证 27 | zcode (value, errorMsg) { 28 | if (!/\d{6}/.test(value)) { 29 | return errorMsg || '请正确填写验证码' 30 | } 31 | return false 32 | } 33 | 34 | // 身份证验证 35 | idCard (value, errorMsg) { 36 | if (!/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(value)) { 37 | return errorMsg || '身份证号码不正确' 38 | } 39 | return false 40 | } 41 | 42 | // 邮箱验证 43 | email (value, errorMsg) { 44 | if (!/([\w-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([\w-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)/.test(value)) { 45 | return errorMsg || '邮箱格式不正确' 46 | } 47 | return false 48 | } 49 | 50 | // uuid验证 51 | uuid (value, errorMsg) { 52 | const rgx1 = /^[\da-f]{32}$/i 53 | const rgx2 = /^(urn:uuid:)?[\da-f]{8}-([\da-f]{4}-){3}[\da-f]{12}$/i 54 | if (!(value.match(rgx1) || value.match(rgx2))) { 55 | return errorMsg || 'id格式不正确' 56 | } 57 | return false 58 | } 59 | 60 | // 银行卡号验证 61 | bankCard (value, errorMsg) { 62 | if (!/^([1-9]{1})(\d{15}|\d{18})$/.test(value)) { 63 | return errorMsg || '银行卡格式不正确' 64 | } 65 | return false 66 | } 67 | } 68 | 69 | export default new Strategies() 70 | -------------------------------------------------------------------------------- /build/setup-dev-server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const MFS = require('memory-fs') 4 | const webpack = require('webpack') 5 | const chokidar = require('chokidar') 6 | const webpackDevMiddleware = require('webpack-dev-middleware') 7 | const webpackHotMiddleware = require('webpack-hot-middleware') 8 | const clientConfig = require('./webpack.client.config') 9 | const serverConfig = require('./webpack.server.config') 10 | 11 | const readFile = (newFs, file) => { 12 | try { 13 | return newFs.readFileSync(path.join(clientConfig.output.path, file), 'utf-8') 14 | } catch (e) { 15 | console.log('readFile Error:', e) 16 | } 17 | } 18 | 19 | module.exports = function setupDevServer (app, templatePath, cb) { 20 | let bundle 21 | let template 22 | let clientManifest 23 | 24 | let ready 25 | const readyPromise = new Promise(r => ready = r) 26 | 27 | // 1. 生成新的renderer函数; 2. renderer.renderToString(); 28 | const update = () => { 29 | if (bundle && clientManifest) { 30 | // 异步执行server.js中的render函数 31 | ready() 32 | cb(bundle, { 33 | template, 34 | clientManifest, 35 | }) 36 | } 37 | } 38 | 39 | template = fs.readFileSync(templatePath, 'utf-8') 40 | // 模板改了之后刷新 41 | chokidar.watch(templatePath).on('change', () => { 42 | template = fs.readFileSync(templatePath, 'utf-8') 43 | console.log('index.html template updated') 44 | update() 45 | }) 46 | clientConfig.entry.app = ['webpack-hot-middleware/client', clientConfig.entry.app] 47 | clientConfig.plugins.push( 48 | new webpack.HotModuleReplacementPlugin(), 49 | ) 50 | 51 | const clientComplier = webpack(clientConfig) 52 | const devMiddleware = webpackDevMiddleware(clientComplier, { 53 | publicPath: clientConfig.output.publicPath, 54 | }) 55 | app.use(devMiddleware) 56 | clientComplier.hooks.done.tap('BuildStatsPlugin', stats => { 57 | const statsJson = stats.toJson() 58 | statsJson.errors.forEach(err => console.log(err)) 59 | statsJson.warnings.forEach(err => console.log(err)) 60 | if (statsJson.errors.length) return 61 | const manifest = readFile( 62 | devMiddleware.context.outputFileSystem, 63 | 'vue-ssr-client-manifest.json', 64 | ) 65 | clientManifest = JSON.parse(manifest) 66 | update() 67 | }) 68 | app.use(webpackHotMiddleware(clientComplier, { heartbeat: 5000 })) 69 | 70 | const serverCompiler = webpack(serverConfig) 71 | const mfs = new MFS() 72 | 73 | serverCompiler.outputFileSystem = mfs 74 | // 监听server文件修改 75 | serverCompiler.watch({}, (err, stats) => { 76 | if (err) throw err 77 | const statsJson = stats.toJson() 78 | if (statsJson.errors.length) return 79 | 80 | bundle = JSON.parse(readFile(mfs, 'vue-ssr-server-bundle.json')) 81 | update() 82 | }) 83 | 84 | return readyPromise 85 | } 86 | -------------------------------------------------------------------------------- /build/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const HtmlWebpackPlugin = require('html-webpack-plugin') 3 | const { VueLoaderPlugin } = require('vue-loader') 4 | const utils = require('../lib/utils') 5 | const { ENV } = require('../lib/constants') 6 | 7 | const config = { 8 | output: { 9 | filename: 'js/[name].[hash].js', 10 | path: ENV.PATHS.CLIENT_OUTPUT, 11 | publicPath: ENV.PATHS.PUBLIC_PATH, 12 | chunkFilename: 'js/[name].[chunkhash].chunk.js', 13 | }, 14 | mode: ENV.NODE_ENV, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.vue$/, 19 | use: ['vue-loader'], 20 | include: /app/i, 21 | exclude: /node_modules/, 22 | }, 23 | { 24 | test: /\.(woff|woff2|eot|ttf|otf|svg)$/, 25 | use: [{ 26 | loader: 'file-loader', 27 | options: { 28 | name: 'font/[name].[hash].[ext]', 29 | }, 30 | }], 31 | // include: /app/i, 32 | // exclude: /node_modules/, 33 | }, 34 | { 35 | test: /\.(png|jpg|gif|jpeg)$/, 36 | use: [{ 37 | loader: 'url-loader', 38 | options: { 39 | limit: 1024, 40 | name: 'image/[name].[ext]', 41 | }, 42 | }], 43 | include: /app/i, 44 | exclude: /node_modules/, 45 | }, 46 | ], 47 | }, 48 | plugins: [ 49 | new VueLoaderPlugin(), 50 | new webpack.DefinePlugin({ 51 | __PRODUCTION__: ENV.PRODUCTION, 52 | __DEVELOPMENT__: ENV.DEVELOPMENT, 53 | 'process.env.HTTPPORT': `${process.env.HTTPPORT || 8088}`, 54 | }), 55 | new HtmlWebpackPlugin({ 56 | filename: 'index.html', 57 | template: utils.fixedToRelativePath('./view/index.html'), 58 | minify: { 59 | removeComments: false, 60 | collapseWhitespace: ENV.PRODUCTION, 61 | minifyCSS: ENV.PRODUCTION, 62 | }, 63 | }), 64 | ], 65 | resolve: { 66 | extensions: ['.ts', '.js', '.json', '.scss', '.css', '.vue'], 67 | alias: { 68 | '@': utils.fixedToRelativePath('/app'), 69 | '@assets': utils.fixedToRelativePath('/app/assets'), 70 | '@images': utils.fixedToRelativePath('/app/assets/images'), 71 | '@scss': utils.fixedToRelativePath('/app/assets/scss'), 72 | '@components': utils.fixedToRelativePath('/app/components'), 73 | '@lib': utils.fixedToRelativePath('/app/lib'), 74 | '@filter': utils.fixedToRelativePath('/app/filter'), 75 | '@mixin': utils.fixedToRelativePath('/app/mixin'), 76 | '@router': utils.fixedToRelativePath('/app/router'), 77 | '@src': utils.fixedToRelativePath('/app/src'), 78 | '@store': utils.fixedToRelativePath('/app/store'), 79 | '@utils': utils.fixedToRelativePath('/app/utils'), 80 | }, 81 | }, 82 | devtool: ENV.DEVELOPMENT ? 'cheap-module-source-map' : false, 83 | } 84 | 85 | module.exports = config 86 | -------------------------------------------------------------------------------- /build/webpack.client.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const webpackMerge = require('webpack-merge') 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 4 | const autoprefixer = require('autoprefixer') 5 | const cssnano = require('cssnano') 6 | const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') 7 | const webpackBaseConfig = require('./webpack.base.config') 8 | const { ENV } = require('../lib/constants') 9 | const utils = require('../lib/utils') 10 | 11 | const config = { 12 | entry: { 13 | app: ENV.PATHS.CLIENT_ENTRY, 14 | }, 15 | optimization: { 16 | runtimeChunk: true, 17 | splitChunks: { 18 | chunks: 'all', 19 | cacheGroups: { 20 | libs: { 21 | name: 'chunk-libs', 22 | test: /[\\/]node_modules[\\/]/, 23 | priority: 10, 24 | chunks: 'initial', 25 | }, 26 | elementUI: { 27 | name: 'chunk-elementUI', 28 | priority: 20, 29 | test: /[\\/]node_modules[\\/]element-ui[\\/]/, 30 | }, 31 | utils: { 32 | name: 'chunk-utils', 33 | test: utils.fixedToRelativePath('/app/utils'), 34 | minChunks: 2, 35 | priority: 5, 36 | reuseExistingChunk: true, 37 | }, 38 | }, 39 | }, 40 | }, 41 | module: { 42 | rules: [ 43 | { 44 | test: /\.js$/, 45 | use: [{ 46 | loader: 'babel-loader', 47 | options: { 48 | presets: [ 49 | [ 50 | '@babel/env', 51 | { 52 | useBuiltIns: 'usage', 53 | corejs: 3, 54 | }, 55 | ], 56 | ], 57 | plugins: [ 58 | [ 59 | 'component', 60 | { 61 | libraryName: 'element-ui', 62 | styleLibraryName: 'theme-chalk', 63 | }, 64 | ], 65 | ], 66 | }, 67 | }], 68 | include: /app/i, 69 | exclude: /node_modules/, 70 | }, 71 | { 72 | test: /\.css$/, 73 | use: [ 74 | ENV.DEVELOPMENT ? 'style-loader' : MiniCssExtractPlugin.loader, 75 | 'css-loader', 76 | { 77 | loader: 'postcss-loader', 78 | options: { 79 | postcssOptions: { 80 | plugins: [ 81 | autoprefixer, 82 | ].concat(ENV.PRODUCTION ? [ 83 | cssnano, 84 | ] : []), 85 | }, 86 | }, 87 | }, 88 | ], 89 | }, 90 | { 91 | test: /\.scss$/, 92 | use: [ 93 | ENV.DEVELOPMENT ? 'style-loader' : MiniCssExtractPlugin.loader, 94 | 'css-loader', 95 | { 96 | loader: 'postcss-loader', 97 | options: { 98 | postcssOptions: { 99 | plugins: [ 100 | autoprefixer, 101 | ].concat(ENV.PRODUCTION ? [ 102 | cssnano, 103 | ] : []), 104 | }, 105 | }, 106 | }, 107 | 'sass-loader', 108 | { 109 | loader: 'sass-resources-loader', 110 | options: { 111 | resources: ENV.PUBLIC_SCSS, 112 | }, 113 | }, 114 | ], 115 | include: /app/i, 116 | exclude: /node_modules/, 117 | }, 118 | ], 119 | }, 120 | plugins: [ 121 | new webpack.DefinePlugin({ 122 | __CLIENT__: true, 123 | __SERVER__: false, 124 | }), 125 | new VueSSRClientPlugin(), 126 | ].concat(ENV.DEVELOPMENT ? [ 127 | new MiniCssExtractPlugin({ 128 | filename: 'css/[name].[hash].css', 129 | chunkFilename: 'css/[name].[hash].css', 130 | }), 131 | ] : [ 132 | new MiniCssExtractPlugin({ 133 | filename: 'css/[name].[hash].css', 134 | chunkFilename: 'css/[name].[hash].css', 135 | }), 136 | ]), 137 | } 138 | 139 | module.exports = webpackMerge.merge(webpackBaseConfig, config) 140 | -------------------------------------------------------------------------------- /build/webpack.nest.server.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer') 2 | const cssnano = require('cssnano') 3 | const webpack = require('webpack') 4 | const { VueLoaderPlugin } = require('vue-loader') 5 | const utils = require('../lib/utils') 6 | const { ENV } = require('../lib/constants') 7 | 8 | module.exports = function (options) { 9 | const newRules = [ 10 | ...options.module.rules, 11 | { 12 | test: /\.js$/, 13 | use: [{ 14 | loader: 'babel-loader', 15 | options: { 16 | presets: [ 17 | [ 18 | '@babel/env', 19 | { 20 | useBuiltIns: 'usage', 21 | corejs: 3, 22 | }, 23 | ], 24 | ], 25 | }, 26 | }], 27 | exclude: /node_modules/, 28 | }, 29 | { 30 | test: /\.css$/, 31 | use: [ 32 | 'vue-style-loader', 33 | 'css-loader', 34 | { 35 | loader: 'postcss-loader', 36 | options: { 37 | postcssOptions: { 38 | plugins: [ 39 | autoprefixer, 40 | ].concat(ENV.PRODUCTION ? [ 41 | cssnano, 42 | ] : []), 43 | }, 44 | }, 45 | }, 46 | ], 47 | }, 48 | { 49 | test: /\.scss$/, 50 | use: [ 51 | 'vue-style-loader', 52 | 'css-loader', 53 | { 54 | loader: 'postcss-loader', 55 | options: { 56 | postcssOptions: { 57 | plugins: [ 58 | autoprefixer, 59 | ].concat(ENV.PRODUCTION ? [ 60 | cssnano, 61 | ] : []), 62 | }, 63 | }, 64 | }, 65 | 'sass-loader', 66 | { 67 | loader: 'sass-resources-loader', 68 | options: { 69 | resources: ENV.PUBLIC_SCSS, 70 | }, 71 | }, 72 | ], 73 | include: /app/i, 74 | exclude: /node_modules/, 75 | }, 76 | { 77 | test: /\.vue$/, 78 | use: ['vue-loader'], 79 | include: /app/i, 80 | exclude: /node_modules/, 81 | }, 82 | { 83 | test: /\.(woff|woff2|eot|ttf|otf|svg)$/, 84 | use: ['file-loader'], 85 | include: /app/i, 86 | exclude: /node_modules/, 87 | }, 88 | { 89 | test: /\.(png|jpg|gif|jpeg)$/, 90 | use: [{ 91 | loader: 'url-loader', 92 | options: { 93 | limit: 1024, 94 | }, 95 | }], 96 | include: /app/i, 97 | exclude: /node_modules/, 98 | }, 99 | ] 100 | 101 | const newPlugins = [ 102 | ...options.plugins, 103 | new webpack.DefinePlugin({ 104 | __CLIENT__: false, 105 | __SERVER__: true, 106 | __PRODUCTION__: ENV.PRODUCTION, 107 | __DEVELOPMENT__: ENV.DEVELOPMENT, 108 | 'process.env.NODE_ENV': `'${ENV.NODE_ENV || ENV.DEVELOPMENT}'`, 109 | 'process.env.HTTPPORT': `${process.env.HTTPPORT || 8088}`, 110 | 'process.env.HTTPSPORT': `${process.env.HTTPSPORT || 8089}`, 111 | 'process.env.KEEP_ALIVE_TIMEOUT': `${process.env.KEEP_ALIVE_TIMEOUT || 10 * 1000}`, 112 | 'process.env.NOT_HOT': `'${process.env.NOT_HOT || ''}'`, 113 | }), 114 | new VueLoaderPlugin(), 115 | ] 116 | const newOptions = { 117 | ...options, 118 | entry: { 119 | server: ENV.PATHS.NEST_SERVER_ENTRY, 120 | }, 121 | output: { 122 | filename: '[name].js', 123 | path: ENV.PATHS.SERVER_OUTPUT, 124 | }, 125 | module: { 126 | ...options.module, 127 | rules: newRules, 128 | }, 129 | resolve: { 130 | extensions: ['.ts', '.js', '.json', '.scss', '.css', '.vue'], 131 | alias: { 132 | '@': utils.fixedToRelativePath('/app'), 133 | '@assets': utils.fixedToRelativePath('/app/assets'), 134 | '@images': utils.fixedToRelativePath('/app/assets/images'), 135 | '@scss': utils.fixedToRelativePath('/app/assets/scss'), 136 | '@components': utils.fixedToRelativePath('/app/components'), 137 | '@lib': utils.fixedToRelativePath('/app/lib'), 138 | '@filter': utils.fixedToRelativePath('/app/filter'), 139 | '@mixin': utils.fixedToRelativePath('/app/mixin'), 140 | '@router': utils.fixedToRelativePath('/app/router'), 141 | '@src': utils.fixedToRelativePath('/app/src'), 142 | '@store': utils.fixedToRelativePath('/app/store'), 143 | '@utils': utils.fixedToRelativePath('/app/utils'), 144 | }, 145 | }, 146 | plugins: newPlugins, 147 | devtool: ENV.DEVELOPMENT ? 'cheap-module-source-map' : false, 148 | } 149 | return newOptions 150 | } 151 | -------------------------------------------------------------------------------- /build/webpack.server.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const webpackMerge = require('webpack-merge') 3 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 4 | const nodeExternals = require('webpack-node-externals') 5 | const autoprefixer = require('autoprefixer') 6 | const cssnano = require('cssnano') 7 | const webpackBaseConfig = require('./webpack.base.config') 8 | const { ENV } = require('../lib/constants') 9 | 10 | const config = { 11 | target: 'node', 12 | entry: ENV.PATHS.SERVER_ENTRY, 13 | output: { 14 | filename: 'server-bundle.js', 15 | libraryTarget: 'commonjs2', 16 | }, 17 | optimization: { 18 | splitChunks: false, 19 | }, 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.js$/, 24 | use: [{ 25 | loader: 'babel-loader', 26 | options: { 27 | presets: [ 28 | [ 29 | '@babel/env', 30 | { 31 | useBuiltIns: 'usage', 32 | corejs: 3, 33 | }, 34 | ], 35 | ], 36 | plugins: [ 37 | [ 38 | 'component', 39 | { 40 | libraryName: 'element-ui', 41 | styleLibraryName: 'theme-chalk', 42 | }, 43 | ], 44 | ], 45 | }, 46 | }], 47 | include: /app/i, 48 | exclude: /node_modules/, 49 | }, 50 | { 51 | test: /\.css$/, 52 | use: [ 53 | 'vue-style-loader', 54 | 'css-loader', 55 | { 56 | loader: 'postcss-loader', 57 | options: { 58 | postcssOptions: { 59 | plugins: [ 60 | autoprefixer, 61 | ].concat(ENV.PRODUCTION ? [ 62 | cssnano, 63 | ] : []), 64 | }, 65 | }, 66 | }, 67 | ], 68 | }, 69 | { 70 | test: /\.scss$/, 71 | use: [ 72 | 'vue-style-loader', 73 | 'css-loader', 74 | { 75 | loader: 'postcss-loader', 76 | options: { 77 | postcssOptions: { 78 | plugins: [ 79 | autoprefixer, 80 | ].concat(ENV.PRODUCTION ? [ 81 | cssnano, 82 | ] : []), 83 | }, 84 | }, 85 | }, 86 | 'sass-loader', 87 | { 88 | loader: 'sass-resources-loader', 89 | options: { 90 | resources: ENV.PUBLIC_SCSS, 91 | }, 92 | }, 93 | ], 94 | include: /app/i, 95 | exclude: /node_modules/, 96 | }, 97 | ], 98 | }, 99 | externals: nodeExternals({ 100 | allowlist: [/\.css$/, /ant-design-vue\/lib/], 101 | }), 102 | plugins: [ 103 | new webpack.DefinePlugin({ 104 | __CLIENT__: false, 105 | __SERVER__: true, 106 | }), 107 | new VueSSRServerPlugin(), 108 | ], 109 | } 110 | 111 | module.exports = webpackMerge.merge(webpackBaseConfig, config) 112 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | web: 4 | container_name: mp_replease_platform_web 5 | image: mp_release_platform:2020 6 | ports: 7 | - "80:3000" 8 | - "443:3000" 9 | restart: on-failure 10 | mysql: 11 | container_name: mp_replease_platform_mysql 12 | restart: always 13 | image: mysql:5.6 14 | volumes: 15 | - ./data:/var/lib/mysql 16 | command: [ 17 | '--character-set-server=utf8', 18 | '--collation-server=utf8_general_ci', 19 | --default-authentication-plugin=mysql_native_password, 20 | ] 21 | environment: 22 | - MYSQL_ROOT_PASSWORD=root 23 | - MYSQL_DATABASE=mp_release_platform 24 | - MYSQL_USER=mp 25 | - MYSQL_PASSWORD=mp2020 26 | ports: 27 | - "3306:3306" 28 | redis: 29 | container_name: mp_replease_platform_redis 30 | image: redis 31 | ports: 32 | - 6379:6379 33 | -------------------------------------------------------------------------------- /dockerfiles/base_image: -------------------------------------------------------------------------------- 1 | ARG FROM_IMAGE=node:16-alpine 2 | 3 | FROM $FROM_IMAGE 4 | LABEL maintainer=$MAINTAINER 5 | 6 | ENV NPM_CONFIG_REGISTRY="https://registry.npmmirror.com" \ 7 | NODE_ENV=production 8 | 9 | RUN mkdir -p /usr/etc \ 10 | && echo -e "registry = https://registry.npmmirror.com" > /usr/etc/npmrc \ 11 | && MAIN_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 0-2) \ 12 | && mv /etc/apk/repositories /etc/apk/repositories-bak \ 13 | && { \ 14 | echo "https://mirrors.aliyun.com/alpine/v${MAIN_VERSION}/main"; \ 15 | echo "https://mirrors.aliyun.com/alpine/v${MAIN_VERSION}/community"; \ 16 | echo '@edge https://mirrors.aliyun.com/alpine/edge/main'; \ 17 | echo '@testing https://mirrors.aliyun.com/alpine/edge/testing'; \ 18 | echo '@community https://mirrors.aliyun.com/alpine/edge/community'; \ 19 | } >> /etc/apk/repositories \ 20 | && apk add --update --no-cache \ 21 | libgcc libstdc++ libssl1.1 c-ares http-parser \ 22 | zlib musl file tzdata \ 23 | && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ 24 | && npm install -g nodemon 25 | 26 | CMD [ "node" ] 27 | -------------------------------------------------------------------------------- /dockerfiles/builder_image: -------------------------------------------------------------------------------- 1 | ARG FROM_IMAGE 2 | FROM $FROM_IMAGE 3 | 4 | RUN apk add --update --no-cache \ 5 | openssh-client build-base git 6 | 7 | CMD [ "node" ] 8 | -------------------------------------------------------------------------------- /dockerfiles/release: -------------------------------------------------------------------------------- 1 | ARG BUILDER_IMAGE 2 | ARG RUNTIME_IMAGE 3 | 4 | FROM $BUILDER_IMAGE as Builder 5 | 6 | ARG APP_ROOT 7 | ARG GIT_COMMIT 8 | 9 | WORKDIR ${APP_ROOT} 10 | 11 | ENV PATH=$APP_ROOT/node_modules/.bin:$PATH 12 | 13 | COPY ./ ./ 14 | 15 | RUN mkdir -p ./tmp \ 16 | && env NODE_ENV= PHANTOMJS_CDNURL=https://registry.npmmirror.com/dist/phantomjs/ npm install --silent --no-progress \ 17 | && env NODE_ENV=production HTTPPORT=3000 npm run build:pro \ 18 | && env NODE_ENV=production npm prune 19 | 20 | ###### ================ ###### 21 | 22 | FROM $RUNTIME_IMAGE 23 | 24 | ARG APP_ROOT 25 | ARG APP_VERSION 26 | 27 | RUN addgroup -g 1001 -S rails \ 28 | && adduser -u 1001 -S rails -G rails 29 | 30 | USER rails 31 | 32 | COPY --from=Builder --chown=rails:rails ${APP_ROOT} ${APP_ROOT} 33 | 34 | WORKDIR ${APP_ROOT} 35 | 36 | ENV APP_VERSION=$APP_VERSION \ 37 | NODE_ENV=production \ 38 | HTTPPORT=3000 \ 39 | PATH=$APP_ROOT/node_modules/.bin:$PATH 40 | 41 | EXPOSE 3000 42 | 43 | ENTRYPOINT ["/bin/sh", "./entrypoint.sh"] 44 | CMD [ "server" ] 45 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | EXEC='' 6 | 7 | if [ "$1" = 'server' ] 8 | then 9 | EXEC="node ./private/server/server.js" 10 | fi 11 | 12 | if [[ ! -z "$1" && -z "$EXEC" ]] 13 | then 14 | EXEC="$1" 15 | fi 16 | 17 | exec $EXEC 18 | -------------------------------------------------------------------------------- /lib/configure.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const fixedToRelativePath = pathname => path.join(process.cwd(), pathname) 5 | 6 | const configure = { 7 | domain: 'http://localhost:8088', 8 | session: { 9 | name: 'mp_release_platform_connect', 10 | secret: 'Mp_Release_Platform', 11 | cookie: { 12 | maxAge: 1000 * 60 * 60 * 8, 13 | }, 14 | }, 15 | redisCFG: { 16 | prefix: 'mp_release_platform_sess:', 17 | host: 'mp_replease_platform_redis', 18 | port: 6379, 19 | db: 8, 20 | }, 21 | mysql: { 22 | port: 3306, 23 | host: 'mp_replease_platform_mysql', 24 | user: 'mp', 25 | password: 'mp2020', 26 | database: 'mp_release_platform', 27 | connectionLimit: 10, 28 | }, 29 | encryptConfig: { 30 | key: 'sdxduerhpcfxuslj', 31 | iv: 'vfbwacgikomeusip', 32 | encrypt: true, 33 | }, 34 | } 35 | 36 | export const updatePatchConfig = () => { 37 | try { 38 | const { NODE_ENV_CONF = '', NODE_ENV = '' } = process.env 39 | const currentEnvConf = (NODE_ENV !== 'devlopment' && !NODE_ENV_CONF) ? 'production' : NODE_ENV_CONF 40 | const confPath = currentEnvConf && fixedToRelativePath(`/lib/config/${currentEnvConf}.json`) 41 | const patchPath = fixedToRelativePath('/lib/config/patch.json') 42 | if (confPath && fs.existsSync(confPath)) { 43 | Object.assign( 44 | configure, 45 | JSON.parse(fs.readFileSync(confPath, 'utf-8')), 46 | ) 47 | } 48 | if (fs.existsSync(patchPath)) { 49 | Object.assign( 50 | configure, 51 | JSON.parse(fs.readFileSync(patchPath, 'utf-8')), 52 | ) 53 | } 54 | } catch (error) { 55 | console.error('项目启动配置加载错误: ', error) 56 | } 57 | } 58 | 59 | updatePatchConfig() 60 | 61 | export default configure 62 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | const utils = require('./utils') 2 | 3 | // 环境常量 4 | const ENV = { 5 | NODE_ENV: process.env.NODE_ENV, 6 | PRODUCTION: process.env.NODE_ENV === 'production', 7 | DEVELOPMENT: process.env.NODE_ENV === 'development', 8 | PATHS: { 9 | CLIENT_ENTRY: utils.fixedToRelativePath('/app/entry.client.js'), 10 | SERVER_ENTRY: utils.fixedToRelativePath('/app/entry.server.js'), 11 | NEST_SERVER_ENTRY: utils.fixedToRelativePath('/server/build.js'), 12 | CLIENT_OUTPUT: utils.fixedToRelativePath('/public/bundle'), 13 | SERVER_OUTPUT: utils.fixedToRelativePath('/private/server'), 14 | PUBLIC_PATH: '/bundle/', 15 | }, 16 | PUBLIC_SCSS: [ 17 | utils.fixedToRelativePath('/app/assets/scss/function.scss'), 18 | utils.fixedToRelativePath('/app/assets/scss/variable.scss'), 19 | ], 20 | VUE_SSR_FILE: { 21 | TEMPLATE: utils.fixedToRelativePath('/view/index.html'), 22 | SERVER_BUNDLE: utils.fixedToRelativePath('/public/bundle/vue-ssr-server-bundle.json'), 23 | CLIENT_MAINFEST: utils.fixedToRelativePath('/public/bundle/vue-ssr-client-manifest.json'), 24 | SETUP_DEV_SERVER: utils.fixedToRelativePath('/build/setup-dev-server.js'), 25 | }, 26 | } 27 | 28 | module.exports = { 29 | ENV, 30 | } 31 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | // 根据传入的相对跟目录路径计算绝对路径 4 | // @params pathname: 相对路径 5 | // @return 绝对路径 6 | const fixedToRelativePath = pathname => path.join(process.cwd(), pathname) 7 | 8 | // 根据传入的相对路径计算绝对路径 9 | // @params pathname: 相对路径 10 | // @return 绝对路径 11 | const absoluteToRelativePath = pathname => path.join(__dirname, pathname) 12 | 13 | module.exports = { 14 | fixedToRelativePath, 15 | absoluteToRelativePath, 16 | } 17 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "./server/", 4 | "root": "./", 5 | "entryFile": "server", 6 | "compilerOptions": { 7 | "webpack": true, 8 | "webpackConfigPath": "./build/webpack.nest.server.config.js" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "restartable":"rs", 3 | "ignore":[""], 4 | "verbose": true, 5 | "execMap":{ 6 | "":"node" 7 | }, 8 | "ext": "js,ts", 9 | "watch":[ 10 | "./server/**" 11 | ], 12 | "legacy-watch":false 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mp_release_platform", 3 | "version": "2.0.0", 4 | "private": true, 5 | "description": "小程序发布平台,详情请看README.md", 6 | "main": "./private/server/server.js", 7 | "author": "lzj", 8 | "publishConfig": { 9 | "registry": "https://registry.npm.taobao.org" 10 | }, 11 | "scripts": { 12 | "start:dev": "cross-env NODE_ENV=development nodemon --exec 'ts-node ./server/index.js'", 13 | "start:pro": "cross-env NODE_ENV=production nodemon --exec 'ts-node ./server/index.js'", 14 | "start:debug": "cross-env NODE_ENV=production nodemon --exec node -r ts-node/register --inspect ./server/index.js", 15 | "start:only": "npm run start:only:dev", 16 | "start:only:dev": "cross-env NODE_ENV=development NOT_HOT='true' nodemon --exec 'ts-node ./server/index.js'", 17 | "start:only:pro": "cross-env NODE_ENV=production NOT_HOT='true' nodemon --exec 'ts-node ./server/index.js'", 18 | "build:pro": "rm -rf ./public/bundle && npm run build:client:pro && npm run build:server:pro && npm run build:nest:server:pro && echo '生产环境m站编译完成'", 19 | "build:client:pro": "cross-env NODE_ENV=production webpack --config ./build/webpack.client.config.js --progress && echo '生产环境m站客户端渲染部分编译完成'", 20 | "build:server:pro": "cross-env NODE_ENV=production webpack --config ./build/webpack.server.config.js --progress && echo '生产环境m站服务端渲染部分编译完成'", 21 | "build:nest:server:pro": "rm -rf ./private/server && cross-env NODE_ENV='production' nest build && echo '生产环境服务器编译完成'", 22 | "build:dev": "rm -rf ./public/bundle && npm run build:client:dev && npm run build:server:dev && echo '开发环境m站编译完成'", 23 | "build:client:dev": "cross-env NODE_ENV=development webpack --config ./build/webpack.client.config.js --progress && echo '开发环境m站客户端渲染部分编译完成'", 24 | "build:server:dev": "cross-env NODE_ENV=development webpack --config ./build/webpack.server.config.js --progress && echo '开发环境m站服务端渲染部分编译完成'", 25 | "lint": "npm run lint:client && lint:server && echo '所有代码符合规范!'", 26 | "lint:server": "eslint './server/**/*.ts' && echo '服务端代码符合规范!'", 27 | "lint:client": "eslint app && echo '客户端代码符合规范!'" 28 | }, 29 | "husky": { 30 | "hooks": { 31 | "pre-commit": "lint-staged" 32 | } 33 | }, 34 | "lint-staged": { 35 | "**/*.js": "eslint --ext .js", 36 | "**/*.ts": "eslint --ext .ts", 37 | "**/*.vue": "eslint --ext .vue" 38 | }, 39 | "dependencies": { 40 | "@nestjs/common": "^8.4.5", 41 | "@nestjs/core": "^8.4.5", 42 | "@nestjs/jwt": "^8.0.1", 43 | "@nestjs/passport": "^8.2.1", 44 | "@nestjs/platform-express": "^8.4.5", 45 | "@nestjs/platform-socket.io": "^8.4.5", 46 | "@nestjs/websockets": "^8.0.0", 47 | "@types/passport-jwt": "^3.0.3", 48 | "@types/socket.io": "^2.1.11", 49 | "alipay-dev": "^0.5.3", 50 | "autoprefixer": "^9.8.6", 51 | "axios": "^0.19.2", 52 | "body-parser": "^1.19.0", 53 | "chokidar": "^3.4.2", 54 | "connect-redis": "^5.0.0", 55 | "cookie-parser": "^1.4.5", 56 | "core-js": "^3.6.5", 57 | "cross-env": "^7.0.2", 58 | "crypto": "^1.0.1", 59 | "crypto-js": "^4.0.0", 60 | "cssnano": "^4.1.10", 61 | "dayjs": "^1.7.7", 62 | "download-git-repo": "^3.0.2", 63 | "element-ui": "^2.14.0", 64 | "express": "^4.17.1", 65 | "express-session": "^1.17.1", 66 | "html-webpack-plugin": "^5.3.1", 67 | "lodash": "^4.17.11", 68 | "memory-fs": "^0.5.0", 69 | "mini-css-extract-plugin": "^1.4.0", 70 | "miniprogram-ci": "^1.0.83", 71 | "mockjs": "^1.1.0", 72 | "mysql2": "^2.2.5", 73 | "node-rsa": "^1.1.1", 74 | "passport": "^0.4.1", 75 | "passport-jwt": "^4.0.0", 76 | "passport-local": "^1.0.0", 77 | "path": "^0.12.7", 78 | "process": "^0.11.10", 79 | "qs": "^6.9.4", 80 | "redis": "^3.0.2", 81 | "reflect-metadata": "^0.1.13", 82 | "regenerator-runtime": "^0.13.7", 83 | "rimraf": "^3.0.2", 84 | "rxjs": "^7.1.0", 85 | "sequelize": "^6.3.5", 86 | "sequelize-typescript": "^2.0.0-beta.0", 87 | "shelljs": "^0.8.4", 88 | "socket.io": "^4.5.1", 89 | "socket.io-client": "^4.5.1", 90 | "tt-ide-cli": "0.0.3", 91 | "url": "^0.11.0", 92 | "uuid": "^8.3.1", 93 | "vconsole": "^3.3.4", 94 | "vue": "^2.6.11", 95 | "vue-router": "^3.4.3", 96 | "vue-server-renderer": "^2.6.11", 97 | "vuelidate": "^0.7.4", 98 | "vuex": "^3.5.1", 99 | "vuex-router-sync": "^5.0.0", 100 | "webpack": "^5.72.1", 101 | "webpack-dev-middleware": "^4.1.0", 102 | "webpack-hot-middleware": "^2.25.0", 103 | "webpack-merge": "^5.7.3", 104 | "webpack-node-externals": "^2.5.0", 105 | "wechat-api": "^1.35.1", 106 | "vue-loader": "15" 107 | }, 108 | "devDependencies": { 109 | "@babel/core": "^7.13.14", 110 | "@babel/preset-env": "^7.13.12", 111 | "@babel/register": "^7.13.14", 112 | "@nestjs/cli": "^8.2.6", 113 | "@nestjs/schematics": "^8.0.11", 114 | "@nestjs/testing": "^8.4.5", 115 | "@types/bluebird": "^3.5.32", 116 | "@types/express": "^4.17.3", 117 | "@types/jest": "^27.0.0", 118 | "@types/node": "^13.9.1", 119 | "@types/supertest": "^2.0.8", 120 | "@types/validator": "^13.1.0", 121 | "@typescript-eslint/eslint-plugin": "3.0.2", 122 | "@typescript-eslint/parser": "3.0.2", 123 | "babel-eslint": "^10.1.0", 124 | "babel-loader": "^8.1.0", 125 | "babel-plugin-component": "^1.1.1", 126 | "css-loader": "^5.2.0", 127 | "eslint": "^7.2.0", 128 | "eslint-config-airbnb-base": "^14.2.0", 129 | "eslint-config-prettier": "^6.10.0", 130 | "eslint-import-resolver-webpack": "^0.12.2", 131 | "eslint-plugin-import": "^2.20.1", 132 | "eslint-plugin-vue": "^9.0.1", 133 | "file-loader": "^6.0.0", 134 | "husky": "^4.2.5", 135 | "jest": "^28.0.0", 136 | "lint-staged": "^10.4.0", 137 | "node-sass": "6", 138 | "nodemon": "^2.0.4", 139 | "postcss": "^8.4.14", 140 | "postcss-loader": "^7.0.0", 141 | "prettier": "^1.19.1", 142 | "sass-loader": "^13.0.0", 143 | "sass-resources-loader": "^2.0.3", 144 | "style-loader": "^1.2.1", 145 | "supertest": "^4.0.2", 146 | "ts-jest": "^28.0.3", 147 | "ts-loader": "^8.0.18", 148 | "ts-node": "^10.8.0", 149 | "tsconfig-paths": "^3.9.0", 150 | "typescript": "^4.2.3", 151 | "url-loader": "^4.1.0", 152 | "vue-style-loader": "^4.1.2", 153 | "vue-template-compiler": "^2.6.11", 154 | "webpack-cli": "^4.6.0" 155 | }, 156 | "jest": { 157 | "moduleFileExtensions": [ 158 | "js", 159 | "json", 160 | "ts" 161 | ], 162 | "rootDir": "src", 163 | "testRegex": ".spec.ts$", 164 | "transform": { 165 | "^.+\\.(t|j)s$": "ts-jest" 166 | }, 167 | "coverageDirectory": "../coverage", 168 | "testEnvironment": "node" 169 | }, 170 | "repository": { 171 | "type": "git", 172 | "url": "git@github.com:lizijie123/mp_release_platform.git" 173 | }, 174 | "keywords": [ 175 | "vue", 176 | "nest", 177 | "typescript", 178 | "mp", 179 | "ci", 180 | "mp-co" 181 | ], 182 | "license": "ISC" 183 | } 184 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/public/favicon.ico -------------------------------------------------------------------------------- /public/logo-circular.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/public/logo-circular.png -------------------------------------------------------------------------------- /read-image/login.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/read-image/login.jpg -------------------------------------------------------------------------------- /read-image/main-remark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/read-image/main-remark.jpg -------------------------------------------------------------------------------- /read-image/main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/read-image/main.jpg -------------------------------------------------------------------------------- /read-image/preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/read-image/preview.jpg -------------------------------------------------------------------------------- /read-image/upload.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lizijie123/mp_release_platform/7b1957245a1154c75bcf5b9c09396b8ac4b7fd29/read-image/upload.jpg -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | globals: { 20 | __CLIENT__: true, 21 | __SERVER__: true, 22 | __DEVELOPMENT__: true, 23 | __PRODUCTION__: true, 24 | }, 25 | rules: { 26 | '@typescript-eslint/interface-name-prefix': 'off', 27 | '@typescript-eslint/explicit-function-return-type': 'off', 28 | '@typescript-eslint/no-explicit-any': 'off', 29 | '@typescript-eslint/no-var-requires': 'off', 30 | '@typescript-eslint/no-empty-function': 'off', 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /server/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { AuthModule } from './modules/auth/auth.module' 3 | import { AutoInfoModule } from './modules/auto-info/autoInfo.module' 4 | import { CiModule } from './modules/ci/ci.module' 5 | import { TaskModule } from './modules/task/task.module' 6 | import { UserModule } from './modules/user/user.module' 7 | import { UserController } from './modules/user/user.controller' 8 | import { Page404Module } from './modules/404/404.module' 9 | 10 | @Module({ 11 | imports: [ 12 | AuthModule, 13 | AutoInfoModule, 14 | CiModule, 15 | TaskModule, 16 | UserModule, 17 | Page404Module, 18 | ], 19 | controllers: [UserController], 20 | providers: [], 21 | }) 22 | export class AppModule {} 23 | -------------------------------------------------------------------------------- /server/build.js: -------------------------------------------------------------------------------- 1 | global.__CLIENT__ = false 2 | global.__SERVER__ = true 3 | global.__DEVELOPMENT__ = process.env.NODE_ENV === 'development' 4 | global.__PRODUCTION__ = process.env.NODE_ENV === 'production' 5 | 6 | require('./server.ts') 7 | -------------------------------------------------------------------------------- /server/db/model/index.ts: -------------------------------------------------------------------------------- 1 | export { default as User } from './user' 2 | export { default as Task } from './task' 3 | -------------------------------------------------------------------------------- /server/db/model/task.ts: -------------------------------------------------------------------------------- 1 | import { Table, Column, Model, DataType } from 'sequelize-typescript' 2 | 3 | @Table({ 4 | tableName: 'task', 5 | timestamps: false, 6 | freezeTableName: true, 7 | }) 8 | class Task extends Model { 9 | @Column({ 10 | type: DataType.UUID, 11 | primaryKey: true, 12 | comment: '任务ID', 13 | }) 14 | id: string 15 | 16 | @Column({ 17 | type: DataType.UUID, 18 | allowNull: false, 19 | comment: '发起用户的id', 20 | }) 21 | userId: string 22 | 23 | @Column({ 24 | type: DataType.STRING(50), 25 | allowNull: false, 26 | comment: '小程序类型' 27 | }) 28 | type 29 | 30 | @Column({ 31 | type: DataType.STRING(50), 32 | allowNull: false, 33 | comment: '版本号', 34 | }) 35 | version: string 36 | 37 | @Column({ 38 | type: DataType.STRING(50), 39 | allowNull: false, 40 | defaultValue: 'master', 41 | comment: '分支', 42 | }) 43 | branch: string 44 | 45 | @Column({ 46 | type: DataType.STRING(500), 47 | allowNull: true, 48 | comment:'描述', 49 | }) 50 | desc: string 51 | 52 | @Column({ 53 | type:DataType.STRING(50), 54 | allowNull: false, 55 | comment: '状态', 56 | }) 57 | status 58 | 59 | @Column({ 60 | type: DataType.DATE, 61 | allowNull: false, 62 | comment: '创建时间', 63 | }) 64 | createTime 65 | 66 | @Column({ 67 | type: DataType.DATE, 68 | allowNull: true, 69 | comment: '最后一次修改时间', 70 | }) 71 | updateTime 72 | 73 | @Column({ 74 | type: DataType.STRING(1000), 75 | allowNull: true, 76 | comment: '错误信息', 77 | }) 78 | errorMessage 79 | 80 | @Column({ 81 | type: DataType.STRING(10000), 82 | allowNull: true, 83 | comment: '日志信息', 84 | }) 85 | journal 86 | 87 | @Column({ 88 | type: DataType.TEXT(), 89 | allowNull: true, 90 | comment: '体验版二维码', 91 | }) 92 | qrCodeUrl 93 | 94 | @Column({ 95 | type: DataType.INTEGER(), 96 | allowNull: true, 97 | comment: '是否为生产环境应用', 98 | }) 99 | isPro 100 | 101 | @Column({ 102 | type: DataType.STRING(100), 103 | allowNull: true, 104 | comment: '上传类型(uplaod: 体验版, preview: 预览版)', 105 | }) 106 | uploadType 107 | 108 | @Column({ 109 | type: DataType.STRING(1000), 110 | allowNull: true, 111 | comment: '启动页面,仅预览版存在', 112 | }) 113 | pagePath 114 | 115 | @Column({ 116 | type: DataType.STRING(1000), 117 | allowNull: true, 118 | comment: '启动参数,仅预览版存在', 119 | }) 120 | searchQuery 121 | 122 | @Column({ 123 | type: DataType.STRING(1000), 124 | allowNull: true, 125 | comment: '场景值,仅预览版存在', 126 | }) 127 | scene 128 | } 129 | 130 | export default Task 131 | -------------------------------------------------------------------------------- /server/db/model/user.ts: -------------------------------------------------------------------------------- 1 | import { Table, Column, Model, DataType } from 'sequelize-typescript' 2 | 3 | @Table({ 4 | tableName: 'user', 5 | timestamps: false, 6 | freezeTableName: true, 7 | }) 8 | class User extends Model { 9 | @Column({ 10 | type: DataType.UUID, 11 | primaryKey: true, 12 | comment: '用户ID', 13 | }) 14 | id: string 15 | 16 | @Column({ 17 | type: DataType.STRING(100), 18 | allowNull: false, 19 | comment: '用户名' 20 | }) 21 | account: string 22 | 23 | @Column({ 24 | type: DataType.STRING(100), 25 | allowNull: false, 26 | comment: '密码,保存md5字符串', 27 | }) 28 | password: string 29 | 30 | @Column({ 31 | type: DataType.STRING(50), 32 | allowNull: true, 33 | defaultValue: '0.0', 34 | comment:'昵称', 35 | }) 36 | name: string 37 | 38 | @Column({ 39 | type:DataType.STRING(100), 40 | allowNull: true, 41 | comment: '用户头像', 42 | }) 43 | avatar 44 | 45 | @Column({ 46 | type: DataType.DATE, 47 | allowNull: false, 48 | comment: '创建时间', 49 | }) 50 | createTime 51 | 52 | @Column({ 53 | type: DataType.DATE, 54 | allowNull: true, 55 | comment: '最后一次修改时间', 56 | }) 57 | updateTime 58 | 59 | @Column({ 60 | type: DataType.STRING(1000), 61 | allowNull: true, 62 | comment: '个人介绍', 63 | }) 64 | introduce 65 | 66 | @Column({ 67 | type: DataType.INTEGER, 68 | allowNull: false, 69 | defaultValue: 3, 70 | comment: '用户角色: 0-超级管理员 | 1-管理员 | 2-开发测试&运营 | 3-普通用户(只能查看)' 71 | }) 72 | role 73 | 74 | @Column({ 75 | type: DataType.STRING(50), 76 | allowNull: true, 77 | defaultValue: '1', 78 | comment: '开发者标识' 79 | }) 80 | identification 81 | } 82 | 83 | export default User 84 | -------------------------------------------------------------------------------- /server/db/sequelize.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, ModelCtor } from 'sequelize-typescript' 2 | import { QueryTypes } from 'sequelize' 3 | import configure from '../../lib/configure' 4 | import User from './model/user' 5 | import Task from './model/task' 6 | import * as dayjs from 'dayjs' 7 | import * as uuid from 'uuid' 8 | 9 | class DataBaseManger { 10 | sequelize: Sequelize 11 | model: { 12 | User?: User, 13 | Task?: Task 14 | } 15 | 16 | constructor () { 17 | this.model = {} 18 | this.init() 19 | } 20 | 21 | // 初始化 22 | init (): void { 23 | this.initSequelize() 24 | this.initModel() 25 | this.authenticate() 26 | } 27 | 28 | // 初始化数据库连接对象 29 | initSequelize (): void { 30 | this.sequelize = new Sequelize(configure.mysql.database, configure.mysql.user, configure.mysql.password || null, { 31 | host: configure.mysql.host, 32 | port: configure.mysql.port, 33 | dialect: 'mysql', 34 | pool: { 35 | max: configure.mysql.connectionLimit, 36 | min: 0, 37 | acquire: 30000, 38 | idle: 10000, 39 | }, 40 | timezone: '+08:00', 41 | logging: process.env.NODE_ENV === 'development', 42 | }) 43 | } 44 | 45 | // 初始化模型 46 | initModel (): void { 47 | const models = [ 48 | User, 49 | Task, 50 | ] 51 | 52 | const model: Array = models.reduce((previous, model) => { 53 | setTimeout(() => { 54 | model.sync({ alter: true }).then(() => { 55 | if (model.name === 'User') this.initAdmin() 56 | }) 57 | }, 4) 58 | this.model[model.name] = model 59 | previous.push(model) 60 | return previous 61 | }, []) 62 | 63 | this.sequelize.addModels(model) 64 | } 65 | 66 | // 初始化超级管理员账号 67 | async initAdmin (): Promise { 68 | const hasAdmin = await this.hasAdmin() 69 | !hasAdmin && this.createAdmin() 70 | } 71 | 72 | // 判断超级管理员账号是否存在 73 | async hasAdmin (): Promise { 74 | const sql = ` 75 | SELECT 1 FROM user 76 | WHERE account = :account 77 | ` 78 | const has: number = (await this.sequelize.query(sql, { 79 | replacements: { 80 | account: 'admin', 81 | }, 82 | type: QueryTypes.SELECT, 83 | })).length 84 | return !!has 85 | } 86 | 87 | // 创建超级管理员账号 88 | async createAdmin (): Promise { 89 | const sql = ` 90 | INSERT INTO user VALUES( 91 | :id, 92 | :account, 93 | :password, 94 | :name, 95 | :avatar, 96 | :createTime, 97 | :updateTime, 98 | :introduce, 99 | :role, 100 | :identification 101 | ) 102 | ` 103 | const nowTime = dayjs().format('YYYY-MM-DD HH:mm:ss') 104 | await this.sequelize.query(sql, { 105 | replacements: { 106 | id: uuid.v4(), 107 | account: 'admin', 108 | password: '123456', 109 | name: '超级管理员', 110 | avatar: null, 111 | createTime: nowTime, 112 | updateTime: nowTime, 113 | introduce: '超级管理员账号', 114 | role: 0, 115 | identification: 1 116 | }, 117 | type: QueryTypes.INSERT 118 | }) 119 | } 120 | 121 | // 连接数据库 122 | async authenticate () { 123 | try { 124 | await this.sequelize.authenticate() 125 | console.log('数据库已连接') 126 | } catch (err) { 127 | console.log('数据库连接失败') 128 | console.error(err) 129 | throw err 130 | } 131 | } 132 | } 133 | 134 | export default new DataBaseManger() 135 | -------------------------------------------------------------------------------- /server/definitionfile/anyObj.ts: -------------------------------------------------------------------------------- 1 | export interface AnyObj { 2 | [propName: string]: any 3 | } -------------------------------------------------------------------------------- /server/definitionfile/iRequest.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express' 2 | import { AnyObj } from './anyObj' 3 | import { Rest } from './rest' 4 | 5 | // 扩展Request对象 6 | export interface IRequest extends Request { 7 | session: AnyObj, 8 | rest: Rest, 9 | } -------------------------------------------------------------------------------- /server/definitionfile/index.ts: -------------------------------------------------------------------------------- 1 | export { AnyObj } from './anyObj' 2 | export { IRequest } from './iRequest' 3 | export { Rest } from './rest' 4 | export { Result } from './result' 5 | export { PreviewTask } from './previewTask' 6 | -------------------------------------------------------------------------------- /server/definitionfile/previewTask.ts: -------------------------------------------------------------------------------- 1 | // 预览任务进度对象 2 | export interface PreviewJournal { 3 | message?: string 4 | time?: string 5 | interval?: string 6 | } 7 | 8 | // 预览任务对象 9 | export interface PreviewTask { 10 | id: string 11 | journal?: Array 12 | errorMessage?: string 13 | base64?: string 14 | status: string 15 | } 16 | -------------------------------------------------------------------------------- /server/definitionfile/rest.ts: -------------------------------------------------------------------------------- 1 | import { AnyObj } from './anyObj' 2 | import { Result } from './result' 3 | 4 | export interface RestFun { 5 | (href: string, params?: AnyObj, config?: AnyObj): Promise 6 | } 7 | 8 | export interface Rest { 9 | get: RestFun, 10 | post: RestFun, 11 | put: RestFun, 12 | del: RestFun, 13 | patch: RestFun, 14 | } -------------------------------------------------------------------------------- /server/definitionfile/result.ts: -------------------------------------------------------------------------------- 1 | import { AnyObj } from './anyObj' 2 | 3 | export interface Result { 4 | error_code?: number, 5 | error_msg?: string, 6 | result?: AnyObj, 7 | } 8 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | require('@babel/register')({ 2 | presets: [ 3 | [ 4 | '@babel/env', 5 | { 6 | useBuiltIns: "usage", 7 | corejs: 3 8 | } 9 | ] 10 | ] 11 | }) 12 | 13 | global.__CLIENT__ = false 14 | global.__SERVER__ = true 15 | global.__DEVELOPMENT__ = process.env.NODE_ENV === 'development' 16 | global.__PRODUCTION__ = process.env.NODE_ENV === 'production' 17 | 18 | process.env.HTTPPORT = process.env.HTTPPORT ? Number.parseInt(process.env.HTTPPORT) : 8088 19 | process.env.HTTPSPORT = process.env.HTTPSPORT ? Number.parseInt(process.env.HTTPSPORT) : 8089 20 | process.env.KEEP_ALIVE_TIMEOUT = process.env.KEEP_ALIVE_TIMEOUT || 10 * 1000 21 | 22 | require('./server.ts') 23 | -------------------------------------------------------------------------------- /server/middleware/browser.middleware.ts: -------------------------------------------------------------------------------- 1 | import { getBrowser } from '../../app/utils/browser' 2 | import { Response } from 'express' 3 | import { IRequest, AnyObj } from '../definitionfile/index' 4 | 5 | // 记录设备信息中间件 6 | export function browserMiddleware (req: IRequest, res: Response, next: () => void): void { 7 | if (!req.session.browser) { 8 | const browser: AnyObj = getBrowser(req) 9 | Object.assign(req.session, { 10 | browser, 11 | }) 12 | } 13 | next() 14 | } 15 | -------------------------------------------------------------------------------- /server/middleware/env.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Response } from 'express' 2 | import { IRequest } from '../definitionfile/index' 3 | import rest from '../../app/utils/rest' 4 | 5 | // 记录设备信息中间件 6 | export async function envMiddleware (req: IRequest, res: Response, next: () => void): Promise { 7 | const versions = { 8 | // 微信小程序 9 | WechatApplet: false, 10 | // m站 11 | MobileSite: false, 12 | // 小程序 13 | isMina: false, 14 | } 15 | if (req.query.api_env) { 16 | switch (req.query.api_env) { 17 | case 'wechat_applet': { 18 | Object.assign(versions, { 19 | WechatApplet: true, 20 | isMina: true, 21 | }) 22 | break 23 | } 24 | case 'mobile_site': { 25 | Object.assign(versions, { 26 | MobileSite: true, 27 | }) 28 | break 29 | } 30 | default: { 31 | Object.assign(versions, { 32 | MobileSite: true, 33 | }) 34 | } 35 | } 36 | 37 | Object.assign(req.session, { 38 | ...versions, 39 | api_env: req.query.api_env, 40 | }) 41 | } else { 42 | Object.assign(req.session, { 43 | api_env: 'mobile_site', 44 | }) 45 | Object.assign(versions, { 46 | MobileSite: true, 47 | }) 48 | } 49 | 50 | Object.assign(req.session, { 51 | api_refer: req.query.api_refer || 'mobile_site' 52 | }) 53 | 54 | if (req.query.app_version) { 55 | Object.assign(req.session, { 56 | app_version: req.query.app_version 57 | }) 58 | } 59 | 60 | Object.assign(req.session.browser.versions, versions) 61 | 62 | rest.setStore({ 63 | state: { 64 | apiRefer: req.session.api_refer, 65 | appVersion: req.session.app_version, 66 | authToken: req.session?.infoMember?.auth_token, 67 | cookie: req.cookies, 68 | } 69 | }) 70 | 71 | next() 72 | } 73 | -------------------------------------------------------------------------------- /server/middleware/rest.middleware.ts: -------------------------------------------------------------------------------- 1 | import { IRequest, AnyObj, Rest } from '../definitionfile/index' 2 | import { Response } from 'express' 3 | import rest from '../../app/utils/rest' 4 | 5 | export function restMiddleware (req: IRequest, res: Response, next: () => void): void { 6 | if (!req.rest) { 7 | const reqRest: Rest = { 8 | get (href: string, params: AnyObj = {}, config: AnyObj = {}) { 9 | return rest.get(href, params, config) 10 | }, 11 | post (href: string, params: AnyObj = {}, config: AnyObj = {}) { 12 | return rest.post(href, params, config) 13 | }, 14 | put (href: string, params: AnyObj = {}, config: AnyObj = {}) { 15 | return rest.put(href, params, config) 16 | }, 17 | del (href: string, params: AnyObj = {}, config: AnyObj = {}) { 18 | return rest.del(href, params, config) 19 | }, 20 | patch (href: string, params: AnyObj = {}, config: AnyObj = {}) { 21 | return rest.patch(href, params, config) 22 | } 23 | } 24 | 25 | Object.assign(req, { 26 | rest: reqRest, 27 | }) 28 | } 29 | 30 | next() 31 | } 32 | -------------------------------------------------------------------------------- /server/middleware/ssr.middleware.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import ENV from '../utils/constants' 3 | import { Response } from 'express' 4 | import { IRequest } from '../definitionfile/index' 5 | import * as vueServerRenderer from 'vue-server-renderer' 6 | import { NestExpressApplication } from '@nestjs/platform-express' 7 | 8 | // 服务端渲染中间件 9 | export function ssrMiddleware (app: NestExpressApplication): (req: IRequest, res: Response, next: () => void) => void { 10 | let renderer 11 | let readyPromise 12 | 13 | // 生成renderer函数 14 | const createRenderer = (bundle, options) => { 15 | return vueServerRenderer.createBundleRenderer(bundle, Object.assign(options, { 16 | // cache: new LRU({ 17 | // max: 1000, 18 | // maxAge: 1000 * 60 * 15 19 | // }), 20 | basedir: ENV.PATHS.CLIENT_OUTPUT, 21 | runInNewContext: false 22 | })) 23 | } 24 | 25 | // 首屏渲染 26 | const render = (req: IRequest, res: Response, next: () => void): void => { 27 | const s = Date.now() 28 | 29 | const context = { 30 | url: req.url, 31 | browser: req.session.browser, 32 | apiRefer: req.session.api_refer, 33 | appVersion: req.session.app_version, 34 | authToken: req.session?.infoMember?.auth_token, 35 | } 36 | 37 | if (req.query.showConsole) { 38 | Object.assign(context, { 39 | showConsole: true, 40 | }) 41 | } 42 | 43 | if (req.session.infoMember) { 44 | const infoMember = {...req.session.infoMember} 45 | Reflect.deleteProperty(infoMember, 'auth_token') 46 | Object.assign(context, { 47 | infoMember, 48 | }) 49 | } 50 | 51 | if (req.headers.cookie) { 52 | Object.assign(context, { 53 | cookie: req.headers.cookie, 54 | }) 55 | } 56 | 57 | renderer.renderToString(context, (err, html) => { 58 | if (err) { 59 | try { 60 | let { message } = err 61 | message = JSON.parse(message) 62 | if (message.code === 302 && message.originPage === '/404') { 63 | return res.send({ 64 | error_code: 1, 65 | error_msg: '404', 66 | result: {}, 67 | }) 68 | } else if (message.code === 401 && message.originPage === '/login') { 69 | return res.redirect(message.originPage) 70 | } 71 | return next() 72 | } catch (e) { 73 | return next() 74 | } 75 | } 76 | res.setHeader('Content-type', 'text/html') 77 | res.send(html) 78 | if (process.env.NODE_ENV === 'development') { 79 | console.log(`服务端渲染首页耗时: ${Date.now() - s}ms`) 80 | } 81 | }) 82 | } 83 | 84 | if (process.env.NOT_HOT === 'true' || process.env.NODE_ENV === 'production') { 85 | const template = fs.readFileSync(ENV.VUE_SSR_FILE.TEMPLATE, 'utf-8') 86 | const serverBundle = JSON.parse(fs.readFileSync(ENV.VUE_SSR_FILE.SERVER_BUNDLE, 'utf-8')) 87 | const clientManifest = JSON.parse(fs.readFileSync(ENV.VUE_SSR_FILE.CLIENT_MAINFEST, 'utf-8')) 88 | 89 | renderer = createRenderer(serverBundle, { 90 | template, 91 | clientManifest 92 | }) 93 | } else { 94 | readyPromise = require(ENV.VUE_SSR_FILE.SETUP_DEV_SERVER)( 95 | app, 96 | ENV.VUE_SSR_FILE.TEMPLATE, 97 | (bundle, options) => { 98 | renderer = createRenderer(bundle, options) 99 | } 100 | ) 101 | } 102 | 103 | const realSsrMiddleware = (req: IRequest, res: Response, next: () => void): void => { 104 | if (process.env.NOT_HOT === 'true' || process.env.NODE_ENV === 'production') { 105 | render(req, res, next) 106 | } else { 107 | readyPromise.then(() => { 108 | render(req, res, next) 109 | }) 110 | } 111 | } 112 | return realSsrMiddleware 113 | } 114 | -------------------------------------------------------------------------------- /server/modules/404/404.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, All, Res } from '@nestjs/common' 2 | import { Response } from 'express' 3 | 4 | @Controller('/*') 5 | export class Page404Controller { 6 | constructor () {} 7 | 8 | @All() 9 | Page404 (@Res() res: Response): void { 10 | res.redirect('/404') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/modules/404/404.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { Page404Controller } from './404.controller' 3 | 4 | @Module({ 5 | imports: [], 6 | controllers: [Page404Controller], 7 | }) 8 | export class Page404Module {} 9 | -------------------------------------------------------------------------------- /server/modules/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { AuthService } from './auth.service' 3 | import { JwtStrategy } from './jwt.strategy' 4 | import { UserModule } from '../user/user.module' 5 | import { PassportModule } from '@nestjs/passport' 6 | import { JwtModule } from '@nestjs/jwt' 7 | import ENV from '../../utils/constants' 8 | 9 | @Module({ 10 | imports: [ 11 | PassportModule.register({ defaultStrategy: 'jwt' }), 12 | JwtModule.register({ 13 | secret: ENV.jwt.secret, 14 | signOptions: { expiresIn: ENV.jwt.maxAge }, 15 | }), 16 | UserModule, 17 | ], 18 | providers: [AuthService, JwtStrategy], 19 | exports: [AuthService], 20 | }) 21 | 22 | export class AuthModule {} 23 | -------------------------------------------------------------------------------- /server/modules/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { UserService } from '../user/user.service' 3 | import { JwtService } from '@nestjs/jwt' 4 | import { User } from '../../db/model/index' 5 | import { Result } from '../../definitionfile/index' 6 | 7 | @Injectable() 8 | export class AuthService { 9 | constructor(private readonly usersService: UserService, private readonly jwtService: JwtService) {} 10 | 11 | // 校验用户信息 12 | async validateUser (account: string, password: string): Promise { 13 | const user: User = await this.usersService.getByAccount(account) 14 | if (user) { 15 | if (user.password === password) { 16 | return { 17 | error_code: 0, 18 | error_msg: '', 19 | result: user, 20 | } 21 | } else { 22 | return { 23 | error_code: 1, 24 | error_msg: '用户名或密码错误', 25 | result: {} 26 | } 27 | } 28 | } 29 | return { 30 | error_code: 1, 31 | error_msg: '账号不存在', 32 | result: {} 33 | } 34 | } 35 | 36 | // 颁发token 37 | async certificate (user: User): Promise { 38 | try { 39 | const token = this.jwtService.sign(user) 40 | return { 41 | error_code: 0, 42 | error_msg: '', 43 | result: { 44 | ...user, 45 | auth_token: token, 46 | } 47 | } 48 | } catch (error) { 49 | return { 50 | error_code: 1, 51 | error_msg: error, 52 | result: {}, 53 | }; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /server/modules/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt' 2 | import { PassportStrategy } from '@nestjs/passport' 3 | import { Injectable } from '@nestjs/common' 4 | import ENV from '../../utils/constants' 5 | import { User } from '../../db/model/index' 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy) { 9 | constructor() { 10 | super({ 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | ignoreExpiration: false, 13 | secretOrKey: ENV.jwt.secret, 14 | }) 15 | } 16 | 17 | async validate (payload: User): Promise { 18 | return payload 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server/modules/auto-info/autoInfo.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, All, Req } from '@nestjs/common' 2 | import { IRequest, Result } from '../../definitionfile/index' 3 | import { AutoInfoService } from './autoInfo.service' 4 | import * as utils from '../../utils/index' 5 | 6 | @Controller('auto_info') 7 | export class AutoInfoController { 8 | constructor (private readonly autoInfoService: AutoInfoService) {} 9 | 10 | // 清除session缓存 11 | @All('destroy_session') 12 | destroySession (@Req() req: IRequest): string | Result { 13 | req.session.destroy() 14 | return utils.encrypt({ 15 | error_code: 0, 16 | error_msg: '', 17 | result: { 18 | destroy_session: true, 19 | } 20 | }) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/modules/auto-info/autoInfo.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { AutoInfoController } from './autoInfo.controller' 3 | import { AutoInfoService } from './autoInfo.service' 4 | 5 | @Module({ 6 | controllers: [AutoInfoController], 7 | providers: [AutoInfoService], 8 | }) 9 | 10 | export class AutoInfoModule {} 11 | -------------------------------------------------------------------------------- /server/modules/auto-info/autoInfo.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | @Injectable() 4 | export class AutoInfoService {} 5 | -------------------------------------------------------------------------------- /server/modules/ci/ci.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Req, HttpException, UseGuards } from '@nestjs/common' 2 | import { IRequest, Result } from '../../definitionfile/index' 3 | import { CiService } from './ci.service' 4 | import * as utils from '../../utils/index' 5 | import ci from '../../utils/CI/index' 6 | import { CiGateway } from './ci.gateway' 7 | import { AuthGuard } from '@nestjs/passport' 8 | import * as uuid from 'uuid' 9 | 10 | @Controller('ci') 11 | export class CiController { 12 | constructor (private readonly ciService: CiService, private readonly ciGateway: CiGateway) {} 13 | 14 | // 上传体验版 15 | @UseGuards(AuthGuard('jwt')) 16 | @Post('upload') 17 | async upload (@Req() req: IRequest): Promise { 18 | const { miniprogramType, version, branch, projectDesc = '', experience = false, isPro } = req.body 19 | if (!miniprogramType || !version || !branch) { 20 | return utils.encrypt({ 21 | error_code: 1, 22 | error_msg: '参数不正确', 23 | result: {} 24 | }) 25 | } 26 | const { id, identification } = req.session?.infoMember || {} 27 | if (!id) { 28 | throw new HttpException('', 401) 29 | } 30 | setTimeout(() => { 31 | ci.upload({ 32 | miniprogramType, 33 | version, 34 | branch, 35 | projectDesc, 36 | isPro, 37 | userId: id, 38 | identification: identification || 1, 39 | experience: experience, 40 | }, this.ciGateway) 41 | }, 100) 42 | return utils.encrypt({ 43 | error_code: 0, 44 | error_msg: '', 45 | result: {} 46 | }) 47 | } 48 | 49 | // 预览 50 | @UseGuards(AuthGuard('jwt')) 51 | @Post('preview') 52 | async preview (@Req() req: IRequest): Promise { 53 | const { miniprogramType, branch, pagePath = 'src/home/main', searchQuery = '', scene = '', isPro } = req.body 54 | if (!miniprogramType || !branch || !pagePath) { 55 | return utils.encrypt({ 56 | error_code: 1, 57 | error_msg: '参数不正确', 58 | result: {} 59 | }) 60 | } 61 | const previewId = uuid.v4() 62 | const { id } = req.session?.infoMember || {} 63 | if (!id) { 64 | throw new HttpException('', 401) 65 | } 66 | setTimeout(() => { 67 | ci.preview({ 68 | miniprogramType, 69 | branch, 70 | pagePath, 71 | searchQuery, 72 | scene, 73 | previewId, 74 | userId: id, 75 | isPro, 76 | }, this.ciGateway) 77 | }, 100) 78 | return utils.encrypt({ 79 | error_code: 0, 80 | error_msg: '', 81 | result: { 82 | id: previewId, 83 | } 84 | }) 85 | } 86 | 87 | // 刷新预览 88 | @UseGuards(AuthGuard('jwt')) 89 | @Post('refresh-preview') 90 | async refreshPreview (@Req() req: IRequest): Promise { 91 | const { taskId } = req.body 92 | if (!taskId) { 93 | return utils.encrypt({ 94 | error_code: 1, 95 | error_msg: '参数不正确', 96 | result: {} 97 | }) 98 | } 99 | const { id } = req.session?.infoMember || {} 100 | if (!id) { 101 | throw new HttpException('', 401) 102 | } 103 | setTimeout(() => { 104 | ci.refreshPreview({ 105 | taskId, 106 | userId: id, 107 | }, this.ciGateway) 108 | }, 100) 109 | return utils.encrypt({ 110 | error_code: 0, 111 | error_msg: '', 112 | result: {} 113 | }) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /server/modules/ci/ci.gateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WebSocketGateway, 3 | WebSocketServer, 4 | } from '@nestjs/websockets' 5 | import { Server, Socket } from 'socket.io' 6 | import { Task } from '../../db/model/index' 7 | import { PreviewTask } from '../../definitionfile/index' 8 | 9 | @WebSocketGateway() 10 | export class CiGateway { 11 | @WebSocketServer() 12 | server: Server; 13 | 14 | constructor() {} 15 | 16 | // socket连接钩子 17 | async handleConnection(client: Socket): Promise { 18 | const { userId } = client.handshake.query 19 | client.join(userId) 20 | if (process.env.NODE_DEV === 'development') { 21 | console.log('socket连接钩子') 22 | } 23 | } 24 | 25 | // socket断开钩子 26 | async handleDisconnect(): Promise { 27 | if (process.env.NODE_DEV === 'development') { 28 | console.log('socket断开钩子') 29 | } 30 | } 31 | 32 | // 推送创建任务 33 | createTask (task: Task): void { 34 | this.server.emit('createTask', { 35 | error_code: 0, 36 | error_msg: '', 37 | result: task 38 | }) 39 | } 40 | 41 | // 推送更新任务 42 | updataTask (task: Task): void { 43 | this.server.emit('updataTask', { 44 | error_code: 0, 45 | error_msg: '', 46 | result: task 47 | }) 48 | } 49 | 50 | // 推送完成任务 51 | confirmTask (userId: string, miniprogramType: string): void { 52 | this.server.to(userId).emit('confirmTask', { 53 | error_code: 0, 54 | error_msg: '', 55 | result: miniprogramType, 56 | }) 57 | } 58 | 59 | // 推送预览更新任务 60 | previewUpdataTask (userId: string, previewTask: PreviewTask): void { 61 | this.server.to(userId).emit('previewUpdataTask', { 62 | error_code: 0, 63 | error_msg: '', 64 | result: previewTask 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /server/modules/ci/ci.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { CiController } from './ci.controller' 3 | import { CiService } from './ci.service' 4 | import { CiGateway } from './ci.gateway' 5 | 6 | @Module({ 7 | controllers: [CiController], 8 | providers: [CiService, CiGateway], 9 | }) 10 | 11 | export class CiModule {} 12 | -------------------------------------------------------------------------------- /server/modules/ci/ci.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | 3 | @Injectable() 4 | export class CiService {} 5 | -------------------------------------------------------------------------------- /server/modules/task/task.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Req, UseGuards, Delete } from '@nestjs/common' 2 | import { IRequest, Result } from '../../definitionfile/index' 3 | import * as utils from '../../utils/index' 4 | import taskService from '../../utils/CI/utils/task.service' 5 | import { Task } from '../../db/model/index' 6 | import { AuthGuard } from '@nestjs/passport' 7 | 8 | @Controller('task') 9 | export class TaskController { 10 | constructor () {} 11 | 12 | // 获取type下所有记录 13 | @UseGuards(AuthGuard('jwt')) 14 | @Get('getByType') 15 | async getByType (@Req() req: IRequest): Promise { 16 | const { miniprogramType, page, limit } = req.query 17 | if (!miniprogramType) { 18 | return utils.encrypt({ 19 | error_code: 1, 20 | error_msg: '参数不正确', 21 | result: {} 22 | }) 23 | } 24 | 25 | const tasks: Array = await taskService.getByType(String(miniprogramType), page ? ((Number.parseInt(String(page), 10) - 1) * 10) : 0, limit ? Number.parseInt(String(limit), 10) : 10) 26 | const total: number = await taskService.getNumByType(String(miniprogramType)) 27 | 28 | return utils.encrypt({ 29 | error_code: 0, 30 | error_msg: '', 31 | result: { 32 | tasks, 33 | total, 34 | } 35 | }) 36 | } 37 | 38 | // 删除任务 39 | @UseGuards(AuthGuard('jwt')) 40 | @Delete('delete') 41 | async delete (@Req() req: IRequest): Promise { 42 | const { id } = req.body 43 | if (!id) { 44 | return utils.encrypt({ 45 | error_code: 1, 46 | error_msg: '参数不正确', 47 | result: {} 48 | }) 49 | } 50 | 51 | await taskService.delete(String(id)) 52 | 53 | return utils.encrypt({ 54 | error_code: 0, 55 | error_msg: '', 56 | result: {}, 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/modules/task/task.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { TaskController } from './task.controller' 3 | 4 | @Module({ 5 | controllers: [TaskController], 6 | }) 7 | 8 | export class TaskModule {} 9 | -------------------------------------------------------------------------------- /server/modules/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, Delete, Patch, Req, UseGuards } from '@nestjs/common' 2 | import { IRequest, Result } from '../../definitionfile/index' 3 | import { UserService } from './user.service' 4 | import * as utils from '../../utils/index' 5 | import { User } from '../../db/model/index' 6 | import { AuthService } from '../auth/auth.service' 7 | import { AuthGuard } from '@nestjs/passport' 8 | 9 | @Controller('user') 10 | export class UserController { 11 | constructor (private readonly userService: UserService, private readonly authService: AuthService) {} 12 | 13 | // 注册 14 | @Post('register') 15 | async register (@Req() req: IRequest): Promise { 16 | const { account, password, name, avatar, introduce, role, identification } = req.body 17 | // 晚点改为装饰器统一处理 18 | if (!account || !password) { 19 | return { 20 | error_code: 1, 21 | error_msg: '参数不正确', 22 | result: {} 23 | } 24 | } 25 | const user: User = await this.userService.getByAccount(String(account)) 26 | if (user) { 27 | return utils.encrypt({ 28 | error_code: 1, 29 | error_msg: '用户已存在', 30 | result: {} 31 | }) 32 | } 33 | await this.userService.create({ 34 | account, 35 | password, 36 | name, 37 | avatar, 38 | introduce, 39 | role, 40 | identification 41 | }) 42 | return utils.encrypt({ 43 | error_code: 0, 44 | error_msg: '', 45 | result: { 46 | success: true, 47 | } 48 | }) 49 | } 50 | 51 | // 删除用户 52 | @UseGuards(AuthGuard('jwt')) 53 | @Delete('delete') 54 | async delete (@Req() req: IRequest): Promise { 55 | const { id } = req.body 56 | if (!id) { 57 | return utils.encrypt({ 58 | error_code: 1, 59 | error_msg: '参数不正确', 60 | result: {} 61 | }) 62 | } 63 | const user = await this.userService.get(String(id)) 64 | if (String(id) === req.session?.infoMember?.id) { 65 | return utils.encrypt({ 66 | error_code: 2, 67 | error_msg: '不可删除自己', 68 | result: {} 69 | }) 70 | } 71 | if (user.account === 'admin') { 72 | return utils.encrypt({ 73 | error_code: 3, 74 | error_msg: '超级管理员不可被删除', 75 | result: {} 76 | }) 77 | } 78 | await this.userService.delete(String(id)) 79 | return utils.encrypt({ 80 | error_code: 0, 81 | error_msg: '', 82 | result: { 83 | success: true, 84 | } 85 | }) 86 | } 87 | 88 | // 登录 89 | @Get('login') 90 | async login (@Req() req: IRequest): Promise { 91 | const { account, password } = req.query 92 | 93 | if (!account || !password) { 94 | return utils.encrypt({ 95 | error_code: 1, 96 | error_msg: '参数不正确', 97 | result: {} 98 | }) 99 | } 100 | 101 | const validateRes: Result = await this.authService.validateUser(String(account), String(password)) 102 | if (validateRes.error_code && validateRes.error_code !== 0) { 103 | return utils.encrypt(validateRes) 104 | } else { 105 | const certificateRes = await this.authService.certificate(validateRes.result) 106 | if (certificateRes.error_code && certificateRes.error_code !== 0) { 107 | return utils.encrypt(certificateRes) 108 | } else { 109 | req.session.infoMember = this.userService.sendInfoMember(certificateRes).result 110 | return utils.encrypt(this.userService.sendInfoMember(certificateRes)) 111 | } 112 | } 113 | } 114 | 115 | // 退出登录 116 | @Post('logout') 117 | async logout (@Req() req: IRequest): Promise { 118 | req.session.infoMember = { online: false } 119 | return utils.encrypt({ 120 | error_code: 0, 121 | error_msg: '', 122 | result: { 123 | online: false, 124 | } 125 | }) 126 | } 127 | 128 | // 获取用户信息 129 | @UseGuards(AuthGuard('jwt')) 130 | @Get('account') 131 | async account (@Req() req: IRequest): Promise { 132 | const user = req.user 133 | if (req.session?.infoMember?.auth_token) { 134 | Object.assign(user, { 135 | auth_token: req.session.infoMember.auth_token, 136 | }) 137 | } 138 | return utils.encrypt(this.userService.sendInfoMember({ 139 | error_code: 0, 140 | error_msg: '', 141 | result: user, 142 | })) 143 | } 144 | 145 | // 获取所有用户 146 | @UseGuards(AuthGuard('jwt')) 147 | @Get('all') 148 | async all (@Req() req: IRequest): Promise { 149 | const { page, limit } = req.query 150 | const users: Array = await this.userService.getAll(page ? ((Number.parseInt(String(page), 10) - 1) * 10) : 0, limit ? Number.parseInt(String(limit), 10) : 10) 151 | const total: number = await this.userService.getAllNum() 152 | return utils.encrypt({ 153 | error_code: 0, 154 | error_msg: '', 155 | result: { 156 | users, 157 | total, 158 | } 159 | }) 160 | } 161 | 162 | // 修改密码或昵称 163 | @UseGuards(AuthGuard('jwt')) 164 | @Patch('change_password_name') 165 | async changePassword (@Req() req: IRequest): Promise { 166 | const { name, password } = req.body 167 | const { id } = req.session?.infoMember 168 | if (!name && !password) { 169 | return utils.encrypt({ 170 | error_code: 1, 171 | error_msg: '参数不正确', 172 | result: {} 173 | }) 174 | } 175 | let user: User = await this.userService.get(String(id)) 176 | if (name) { 177 | Object.assign(user, { 178 | name, 179 | }) 180 | } 181 | if (password) { 182 | Object.assign(user, { 183 | password, 184 | }) 185 | } 186 | await this.userService.updata(user) 187 | user = await this.userService.get(String(id)) 188 | return utils.encrypt(this.userService.sendInfoMember({ 189 | error_code: 0, 190 | error_msg: '', 191 | result: user 192 | })) 193 | } 194 | 195 | // 修改用户角色 196 | @UseGuards(AuthGuard('jwt')) 197 | @Patch('change_role') 198 | async changeRole (@Req() req: IRequest): Promise { 199 | const { role, id } = req.body 200 | if (!role) { 201 | return utils.encrypt({ 202 | error_code: 1, 203 | error_msg: '参数不正确', 204 | result: {} 205 | }) 206 | } 207 | let user: User = await this.userService.get(String(id)) 208 | await this.userService.updata(Object.assign(user, { 209 | role, 210 | })) 211 | user = await this.userService.get(String(id)) 212 | return utils.encrypt(this.userService.sendInfoMember({ 213 | error_code: 0, 214 | error_msg: '', 215 | result: user 216 | })) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /server/modules/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { UserService } from './user.service' 3 | 4 | @Module({ 5 | providers: [UserService], 6 | exports: [UserService], 7 | }) 8 | 9 | export class UserModule {} 10 | -------------------------------------------------------------------------------- /server/modules/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import dataBaseManger from '../../db/sequelize' 3 | import { Sequelize } from 'sequelize-typescript' 4 | import { QueryTypes } from 'sequelize' 5 | import * as dayjs from 'dayjs' 6 | import * as uuid from 'uuid' 7 | import { User } from '../../db/model/index' 8 | import { Result } from '../../definitionfile/index' 9 | 10 | @Injectable() 11 | export class UserService { 12 | sequelize: Sequelize 13 | 14 | constructor () { 15 | this.sequelize = dataBaseManger.sequelize 16 | } 17 | 18 | // 格式化用户信息返回值 19 | sendInfoMember (result: Result): Result { 20 | if (!result.result) return result 21 | const _result = { 22 | ...result, 23 | result: { 24 | ...result.result, 25 | online: Reflect.has(result.result, 'auth_token'), 26 | } 27 | } 28 | return _result 29 | } 30 | 31 | // 创建记录 32 | async create ({ account, password, name, avatar, introduce, role, identification }): Promise { 33 | const sql = ` 34 | INSERT INTO user VALUES( 35 | :id, 36 | :account, 37 | :password, 38 | :name, 39 | :avatar, 40 | :createTime, 41 | :updateTime, 42 | :introduce, 43 | :role, 44 | :identification 45 | ) 46 | ` 47 | const id = uuid.v4() 48 | const nowTime = dayjs().format('YYYY-MM-DD HH:mm:ss') 49 | await this.sequelize.query(sql, { 50 | replacements: { 51 | id, 52 | account, 53 | password, 54 | name: name || account, 55 | avatar: avatar || '', 56 | createTime: nowTime, 57 | updateTime: nowTime, 58 | introduce: introduce || '', 59 | role: role || 3, 60 | identification, 61 | }, 62 | type: QueryTypes.INSERT 63 | }) 64 | return id 65 | } 66 | 67 | // 获取所有记录 68 | async getAll (initNum = 0, limit = 10): Promise> { 69 | const sql = ` 70 | SELECT * FROM user 71 | ORDER BY createTime DESC 72 | LIMIT :initNum,:limit 73 | ` 74 | const users: Array = await this.sequelize.query(sql, { 75 | replacements: { 76 | initNum, 77 | limit, 78 | }, 79 | type: QueryTypes.SELECT, 80 | }) 81 | return users 82 | } 83 | 84 | // 获取所有记录数 85 | async getAllNum (): Promise { 86 | const sql = ` 87 | SELECT count(*) as total FROM user 88 | ` 89 | const taskNum: Array<{ total }> = await this.sequelize.query(sql, { 90 | type: QueryTypes.SELECT, 91 | }) 92 | return taskNum[0].total 93 | } 94 | 95 | // 根据id获取记录 96 | async get (id: string): Promise { 97 | const sql = ` 98 | SELECT * FROM user 99 | WHERE id = :id 100 | ` 101 | const users: Array = await this.sequelize.query(sql, { 102 | replacements: { 103 | id, 104 | }, 105 | type: QueryTypes.SELECT, 106 | }) 107 | return users[0] 108 | } 109 | 110 | // 根据用户名与密码获取记录 111 | async getByAccount (account: string): Promise { 112 | const sql = ` 113 | SELECT * FROM user 114 | WHERE account = :account 115 | ` 116 | const users: Array = await this.sequelize.query(sql, { 117 | replacements: { 118 | account, 119 | }, 120 | type: QueryTypes.SELECT, 121 | }) 122 | return users[0] 123 | } 124 | 125 | // 删除记录 126 | async delete (id: string): Promise { 127 | const sql = ` 128 | DELETE FROM user 129 | WHERE id = :id 130 | ` 131 | await this.sequelize.query(sql, { 132 | replacements: { 133 | id, 134 | }, 135 | type: QueryTypes.DELETE, 136 | }) 137 | } 138 | 139 | // 修改记录 140 | async updata (user: User): Promise { 141 | const sql = ` 142 | UPDATE user 143 | SET account = :account, 144 | password = :password, 145 | name = :name, 146 | avatar = :avatar, 147 | createTime = :createTime, 148 | updateTime = :updateTime, 149 | introduce = :introduce, 150 | role = :role, 151 | identification = :identification 152 | WHERE id = :id 153 | ` 154 | const nowTime = dayjs().format('YYYY-MM-DD HH:mm:ss') 155 | await this.sequelize.query(sql, { 156 | replacements: { 157 | ...user, 158 | updateTime: nowTime, 159 | } 160 | }) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os' 2 | import * as path from 'path' 3 | import * as redis from 'redis' 4 | import * as express from 'express' 5 | import { AppModule } from './app.module' 6 | import * as session from 'express-session' 7 | import * as cookieParser from 'cookie-parser' 8 | import * as connectRedis from 'connect-redis' 9 | import { NestFactory } from '@nestjs/core' 10 | import configure from '../lib/configure' 11 | import { NestExpressApplication, ExpressAdapter } from '@nestjs/platform-express' 12 | import { ssrMiddleware } from './middleware/ssr.middleware' 13 | import { browserMiddleware } from './middleware/browser.middleware' 14 | import { envMiddleware } from './middleware/env.middleware' 15 | import { restMiddleware } from './middleware/rest.middleware' 16 | 17 | const getIp = () => { 18 | const networkInterfaces = os.networkInterfaces() 19 | let ip = '127.0.0.1' 20 | for (const [key, value] of Object.entries(networkInterfaces)) { 21 | value.map(detail => { 22 | if (detail.family == 'IPv4' && key == 'en0') { 23 | ip = detail.address 24 | return false 25 | } 26 | }) 27 | } 28 | return ip 29 | } 30 | 31 | async function bootstrap(): Promise { 32 | const server: express.Express = express() 33 | const app: NestExpressApplication = await NestFactory.create(AppModule, new ExpressAdapter(server)) 34 | 35 | app.useStaticAssets(path.join(process.cwd(), '/public/'), { 36 | prefix: '/', 37 | maxAge: 60 * 60 * 24 * 365, 38 | }) 39 | app.use(cookieParser()) 40 | 41 | const redisStore = connectRedis(session) 42 | app.use(session({ 43 | name: configure.session.secret, 44 | store: new redisStore({ client: redis.createClient(Object.assign({}, configure.redisCFG, { 45 | prefix: configure.redisCFG.prefix + 'sess:', 46 | })) }), 47 | secret: configure.session.secret, 48 | cookie: configure.session.cookie, 49 | saveUninitialized: true, 50 | resave: false 51 | })) 52 | 53 | app.use(restMiddleware) 54 | app.use(browserMiddleware) 55 | app.use(envMiddleware) 56 | app.use(ssrMiddleware(app)) 57 | 58 | await app.init() 59 | 60 | await app.listen(process.env.HTTPPORT) 61 | } 62 | bootstrap().then(() => { 63 | const ip = getIp() 64 | if (process.env.NODE_ENV === 'development') { 65 | console.log(` 66 | 67 | 小程序发布平台 running at: 68 | - Local: http://localhost:${process.env.HTTPPORT} 69 | - Network: http://${ip}:${process.env.HTTPPORT} 70 | 71 | `) 72 | } else { 73 | console.log(` 74 | 75 | 小程序发布平台 running 76 | 77 | `) 78 | } 79 | }) 80 | -------------------------------------------------------------------------------- /server/utils/CI/alipay/index.ts: -------------------------------------------------------------------------------- 1 | import * as ci from 'alipay-dev' 2 | import ciConfigure from '../utils/ci-configure' 3 | import * as fs from 'fs' 4 | import * as utils from '../utils/index' 5 | 6 | export class CiAlipay { 7 | // 上传体验版 8 | async upload ({ miniprogramType, projectPath, version, experience }): Promise { 9 | this.initProject(miniprogramType) 10 | 11 | const res = await ci.miniUpload({ 12 | project: `${projectPath}${ciConfigure[miniprogramType].buildProjectChildrenPath}`, 13 | appId: ciConfigure[miniprogramType].appId, 14 | packageVersion: version, 15 | onProgressUpdate: process.env.NODE_ENV === 'development' ? console.log : () => {}, 16 | experience: experience ? experience : false, 17 | }) 18 | if (res.qrCodeUrl) { 19 | return res.qrCodeUrl 20 | } 21 | } 22 | 23 | // 创建预览 24 | async preview ({ miniprogramType, projectPath, pagePath, searchQuery }): Promise { 25 | this.initProject(miniprogramType) 26 | 27 | await ci.miniPreview({ 28 | project: `${projectPath}${ciConfigure[miniprogramType].buildProjectChildrenPath}`, 29 | appId: ciConfigure[miniprogramType].appId, 30 | onProgressUpdate: process.env.NODE_ENV === 'development' ? console.log : () => {}, 31 | qrcodeFormat: 'image', 32 | qrcodeOutput: `${projectPath}/previewQr.jpg`, 33 | page: pagePath, 34 | launch: searchQuery ? searchQuery : null, 35 | }) 36 | } 37 | 38 | // 创建ci projecr对象 39 | initProject (miniprogramType: string): void { 40 | const privateKey = fs.readFileSync(utils.fixedToRelativePath(ciConfigure[miniprogramType].privateKeyPath), 'utf-8') 41 | ci.setConfig({ 42 | toolId: ciConfigure[miniprogramType].toolId, 43 | privateKey, 44 | }) 45 | } 46 | } 47 | 48 | export const ciAlipay = new CiAlipay() 49 | -------------------------------------------------------------------------------- /server/utils/CI/private/lzj-alipay.key: -------------------------------------------------------------------------------- 1 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCX2MCgIEdjBKvUlwRbzGlLXjQLt8hLGSdcvauRJiywsazXJNDOkRWZUvLRRV6l4JBa484zskZeaDLjWqyFkns1otR51AneSYyZlcFepcK3NVfLVnJPsHfrPXFjRUP7heI5j8iQuakHR91Wy3yAFx1StaMAjHebbXdaZOBsGjdD6TQtaGnyFQ0I/Seqi99gdyMldVFSzYI0OWkUJHLLblIvJ7Lr3uFxU6uNsljjUqTH1elzP98Bu22Y+jAPyCmSfTJ13KRJgQYOVYM8Ep1D7heL8echzOvKju2F9NtmtjUpW/gZinkfk84qUJ+4TvtlD/yIMH+KARFdYVhhyY0D9yRzAgMBAAECggEAdqktrnRLFPf7h4AUKeCNkBYnudh+ryES/4hA4IbieZn/JYlhm2sJY+3MTvlUw2+/nydSZle0YeYvjje3hhI4MmvyetnWdF0pgIPkvp/uj9khqIb/gYK3058KUrc4LwArDyxrYZ2Ul1nzf/Y4bqihg5bpsG4UseNTV9JpBlFeSrRhj7LKwq5/LU3CvUaXyA3uPuC2xqn5cwJxxiGZ7eN4++Dy5nhMku6Q9wC4txb2yIMvW3R7RKZl+wrRMXHmc79p/s8wBOPpc2QWuMUPSYktbEW7EhIqTmERkQAreAZAeyi/A4YLLYDy6KZye6UcrVeduuEvMQE5JoiuCXKi6HF70QKBgQD0Ru/CkA7NjNIidCxyCDvSnYOzpeHYN7sKTdycXgQMhrgstw+yOsI5jLWJVx+zN5lbXw1l4JWnAWuVI4LmCgsJ8bjVE4KnvTQbq+OPJWIqTmDAt+k0z+V9liqiDbHZPxD2WBWHQ22QwhmMepCc5ppfZ8+8FeTwSEuXHx5ve3pAVwKBgQCfIkNgSPR5OvDyLrK9Mxfh/LoiAJ0qN2DIzXtVTiWL6XfbqnN+iQo2e3cy3IdUx2bQaFW/+XXH3U3SW0E3JextFhgUF3jpyEKdagkLlkgSCw4W1DG7RnbZ8oVAIoiHxrJT5uGcjjhVNmwGZ3v3Nb9yvt69GrZWFs4r8MwlAXZ7RQKBgQDlVZPTPhwnroW6AweXJ1PCsE7tYldd/zSCwAbWZw22FOTkVhlOYwvlq8zjXABO6Wv49IxHkUnuuM6f/e7uuY8TjvTQVrjbci5xrDbANYCr51m+lOtEwcna1hjAe8r1AtiR7rCHhS5gMVp7ILaUF4vm9jd5hbSiQb1166lPUMW9kQKBgBJy+Hi7PIyphrGtNE+3ErfCgxnaFF6GmRPurrPPIY88/AZdlpI+9Q4n9kPRSWdMzuCul/Jvy3XmdFVE+ySXovdqrlP1/LCt5Ps3BeFwBN8CpRmEdFeP7cuK0GQFHOsQ4C/V/qYV3vYSHygG61pXwuBcvJoT9Lu6XSC/BAvuRXw5AoGAN7yy7LdMOiD9PqCqWjMVV7l0KwUR4LQz/B7cXULGRlrm+sy0hLnGmXgStargSRgyXilWqT5Itk5ek1kJzqJmphuUG0OYYwIqLLYPg3HHtCm4sV4fBPxloc+FhyByN53zhEUH4U3vlErt6v2V9eYQd5jo+Z6n/XudGxqQupH//uQ= -------------------------------------------------------------------------------- /server/utils/CI/private/lzj-wechat.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAwWKMQqolPEM9fQh2lbeQ8cdkPkgjgLa+Kex47wpc3/BT7wb2 3 | OWhcKj+uWXk9dxn2eLQoHl7mO1vkZjAC9iYLys0BQ0c+X3sOdtwotyvh4urd0Evf 4 | oK1v0Z2nKa4M4o9PXIxDuEx25JBF91uwBev9tvFDyUNvU4cfyzy/ssBLGco/7ORd 5 | wmJ9ckn7FZGKj9xxJbiUiVdN5mwYaZlOdB2G9ea6oWoe5272Q9k4IW+DUidQ/iVg 6 | 6AWUT4yBBGcrYTqTRAYzjFXE81JWEuc6alvapaQ0gz4HPYyqtUL/RUMcTRRahp9Z 7 | 9VGsl5qKMwDMJ6JFu0sIZI7D2r8hzzR86VMRGQIDAQABAoIBAEORHRSFwjDGBYvU 8 | EyrIUlpHolyoc88bCmI3fyF163FK1Oik9A6mydzxFwen9rPQXG9b8tB5s9N1jd2u 9 | VLqQHHqlGhXZpI9TvYAF+CvXpzrTeOC4QdAwCOuiO6+yYkoebEoXr+mKvdeqGOgO 10 | HvhStfjfXyHI7/KtOYyXExtvWE4P4hXNq8fE+syplBEJKyCaLLUGPGjO5jkVc5mt 11 | JdoMwb6RW7HyAoPNesomb9krCA21CODupEFpWjy43kZdo1p92t4ptEq+uH0keoOX 12 | fU0N1CEYXtqlwMjUB3XBJRkYXM3/OZXpjnl+Qn+Tka/k/mNgbTLiT8l4HeVmOw3L 13 | AwHgagkCgYEA8/YDyoAelWWAb+r/D1tsSYMguZGMUTLNJgRReXFSvRPTu3PnNRc0 14 | CFDNW4w0aBG9lTPWBg+mzPqz8zH/ffnr2C6/OOXgVLGY+2EQ4WBARKrSB4b7oYXr 15 | W/3gY5+sj0u0KzFRyv8Px5LGaEZbxA0A3pCBy1NHOWn5u+bbPd7v1CsCgYEAyu2Z 16 | vkkqf8RKtZlJBtiGGQVHokYJ1GL+h0GQMcTv/3zWiEZhCGG0Ye9dlmvsDNI16zdd 17 | ftgg70m4eoLqglB25YWydWyUZLK4h+It/TkeRzeoNN36ItT6z7x0Q0y2htozW5It 18 | Mvu/8C2oRTbqxPmeMvbTrFVd6GzVlOh7Z0dY+csCgYBq2qmEa6N5qjCcVKCM+G0w 19 | saeGwBJmikrCyQdqmtEzkdiedUCix1v5/HJGE93sa0DPdhnbI8XaaECjV5XhdrLv 20 | SGJnKICed/9as4QyQvdDXFKMC3pxn/ebnlJHMGvjg7QRaxO5RwiBUG9owtB0yYFE 21 | +qRvCzXaFunUfGQw8FG5awKBgQCm3U3o3L0npr9QKKWWDYHnHSJUW4dFr8lgaxco 22 | 9anryjmWgVjUzZLIXU31nPTTbh+MNVCaNxqN6W7avsAbPBMolRGz4P789sEqa2Ap 23 | s0gkg627GzTOY5eCLpLjrDcDGHXWvJKSM9UWqFSP5aKTKjdd7P0N3nyD3Mqb0bd3 24 | q4GMlQKBgB6Zu98ka8m6esaCHZQ0TQWIprsl45LVBw8EBPvCdc+eFJ40c6Ejc2vs 25 | zwapZG2o823rKhVv0WDvuVh7dlaD+7J0cmMAlRHyuop+GI1TH9FzBAK84jgTxrLQ 26 | YQvqnGczZOWr0Ml06fWaa34rhugBEn9izJtDwj4KYXhRjsdW6aa9 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /server/utils/CI/toutiao/index.ts: -------------------------------------------------------------------------------- 1 | import ciConfigure from '../utils/ci-configure' 2 | import * as utils from '../utils/index' 3 | 4 | export class CiToutiao { 5 | // 上传体验版 6 | async upload ({ miniprogramType, projectPath, version, projectDesc }): Promise { 7 | const currentPath = process.cwd() 8 | const login = `npx tma login-e '${ciConfigure[miniprogramType].account}' '${ciConfigure[miniprogramType].password}'` 9 | const up = `npx tma upload -v '${version}' -c '${projectDesc ? projectDesc : '暂无描述'}' ${projectPath}${ciConfigure[miniprogramType].buildProjectChildrenPath}` 10 | await utils.execPromise(login, currentPath) 11 | await utils.execPromise(up, currentPath) 12 | } 13 | } 14 | 15 | export const ciToutiao = new CiToutiao() 16 | -------------------------------------------------------------------------------- /server/utils/CI/utils/ci-configure.ts: -------------------------------------------------------------------------------- 1 | const ciConfigure = { 2 | maxTimeout: 1000 * 60 * 60, 3 | lzj_wechat: { 4 | // 小程序appID 5 | appId: 'wxe10f1d56da44430f', 6 | // 应用类型 miniProgram/miniProgramPlugin/miniGame/miniGamePlugin 7 | type: 'miniProgram', 8 | // 项目下载地址 9 | // github地址: `https://github.com:${用户名,我的用户名是 lizijie123}/${代码仓库名,文档代码仓是 uni-mp-study}` 10 | // v3版本 gitlab地址: `${gitlab地址}/${用户名}/${代码仓库名}/repository/archive.zip` 11 | // v4版本 gitlab地址: `${gitlab地址}/api/v4/projects/${代码仓库id}/repository/archive` 12 | // tips: `${gitlab地址}/api/v3/projects`有返回值即为v3版本gitlab,`${gitlab地址}/api/v4/projects`有返回值即为v4版本gitlab,返回的数据中id字段就是代码仓库的id 13 | storeDownloadPath: 'https://github.com:lizijie123/uni-mp-study', 14 | // gitlab项目,则需要设置gitlab的privateToken,在gitlab个人中心可以拿到 15 | privateToken: '', 16 | // 小程序打包构建命令 17 | buildCommand: 'npm run build:wx', 18 | // 开发环境小程序打包构建命令(当未配置该值时,默认值为buildCommand) 19 | devBuildCommand: 'npm run build:wx -dev', 20 | // 小程序打包构建完,输出目录与根目录的相对位置 21 | buildProjectChildrenPath: '/dist/build/mp-weixin', 22 | // 微信小程序与支付宝小程序需要非对称加密的私钥,privateKeyPath是私钥文件相对根目录的地址,在微信公众平台中拿到 23 | privateKeyPath: '/server/utils/CI/private/lzj-wechat.key', 24 | setting: { 25 | es7: false, 26 | minify: false, 27 | autoPrefixWXSS: false, 28 | }, 29 | }, 30 | lzj_alipay: { 31 | // 同上 32 | appId: '2021002107681948', 33 | // 工具id,支付宝小程序设置了非对称加密的公钥后会生成 34 | toolId: 'b6465befb0a24cbe9b9cf49b4e3b8893', 35 | // 同上 36 | storeDownloadPath: 'https://github.com:lizijie123/uni-mp-study', 37 | // gitlab项目,则需要设置gitlab的privateToken 38 | privateToken: '', 39 | // 同上 40 | buildCommand: 'npm run build:ap', 41 | // 同上 42 | buildProjectChildrenPath: '/dist/build/mp-alipay', 43 | // 同上 44 | privateKeyPath: '/server/utils/CI/private/lzj-alipay.key', 45 | }, 46 | lzj_toutiao: { 47 | // 字节跳动小程序账号 48 | account: '', 49 | // 字节跳动小程序密码 50 | password: '', 51 | // 同上 52 | storeDownloadPath: 'https://github.com:lizijie123/uni-mp-study', 53 | // 同上 54 | privateToken: '', 55 | // 同上 56 | buildCommand: 'npm run build:tt', 57 | // 同上 58 | buildProjectChildrenPath: '/dist/build/mp-toutiao', 59 | }, 60 | } 61 | 62 | export default ciConfigure 63 | -------------------------------------------------------------------------------- /server/utils/CI/utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as downloadGit from 'download-git-repo' 2 | import * as path from 'path' 3 | import * as fs from 'fs' 4 | import * as shell from 'shelljs' 5 | 6 | // 根据传入的相对跟目录路径计算绝对路径 7 | // @params pathname: 相对路径 8 | // @return 绝对路径 9 | export function fixedToRelativePath (pathname: string): string { 10 | return path.join(process.cwd(), pathname) 11 | } 12 | 13 | // 下载项目 14 | export function download (storePath: string, projectPath: string): Promise { 15 | return new Promise((resolve, reject) => { 16 | downloadGit(storePath, projectPath, null, err => { 17 | if (err) reject(err) 18 | resolve() 19 | }) 20 | }) 21 | } 22 | 23 | // 初始化项目路径 24 | export function initProjectPath (projectPath: string): void { 25 | if (typeof projectPath !== 'string') throw new TypeError('projectPath is not string') 26 | 27 | // 检测路径是否存在,若不存在则创建出路径 28 | projectPath.split('/').reduce((previous, current) => { 29 | previous += `/${current}` 30 | if (!fs.existsSync(previous)) { 31 | fs.mkdirSync(previous) 32 | } 33 | return previous 34 | }, '') 35 | 36 | // 清空当前路径下的所有文件及文件夹 37 | const removeMkdir = (projectPath) => { 38 | if (fs.existsSync(projectPath)) { 39 | fs.readdirSync(projectPath).map(fileName => { 40 | const currentPath = `${projectPath}/${fileName}` 41 | if (fs.statSync(currentPath).isDirectory()) { 42 | removeMkdir(currentPath) 43 | } else { 44 | fs.unlinkSync(currentPath) 45 | } 46 | }) 47 | fs.rmdirSync(projectPath) 48 | } 49 | } 50 | 51 | 52 | const checkRemoveMkdir = () => { 53 | try { 54 | removeMkdir(projectPath) 55 | } catch (err) { 56 | // 57 | } 58 | 59 | if (fs.existsSync(projectPath)) { 60 | checkRemoveMkdir() 61 | } 62 | } 63 | 64 | checkRemoveMkdir() 65 | } 66 | 67 | // 启用子进程执行shell命令 68 | export function execPromise (command: string, cwd: string): Promise { 69 | return new Promise((resolve: any) => { 70 | shell.exec(command, { 71 | async: true, 72 | silent: process.env.NODE_ENV !== 'development', 73 | stdio: 'ignore', 74 | cwd, 75 | }, (...rest) => { 76 | resolve(...rest) 77 | }) 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /server/utils/CI/utils/task.service.ts: -------------------------------------------------------------------------------- 1 | import dataBaseManger from '../../../db/sequelize' 2 | import { Sequelize } from 'sequelize-typescript' 3 | import { QueryTypes } from 'sequelize' 4 | import * as dayjs from 'dayjs' 5 | import * as uuid from 'uuid' 6 | import { Task } from '../../../db/model/index' 7 | 8 | export class TaskService { 9 | sequelize: Sequelize 10 | 11 | constructor () { 12 | this.sequelize = dataBaseManger.sequelize 13 | } 14 | 15 | // 创建记录 16 | async create ({ userId, type, version, branch, desc, status, errorMessage, journal, qrCodeUrl, isPro, uploadType, pagePath, searchQuery, scene }): Promise { 17 | const sql = ` 18 | INSERT INTO task VALUES( 19 | :id, 20 | :userId, 21 | :type, 22 | :version, 23 | :branch, 24 | :desc, 25 | :status, 26 | :createTime, 27 | :updateTime, 28 | :errorMessage, 29 | :journal, 30 | :qrCodeUrl, 31 | :isPro, 32 | :uploadType, 33 | :pagePath, 34 | :searchQuery, 35 | :scene 36 | ) 37 | ` 38 | const id = uuid.v4() 39 | const nowTime = dayjs().format('YYYY-MM-DD HH:mm:ss') 40 | await this.sequelize.query(sql, { 41 | replacements: { 42 | id, 43 | userId, 44 | type, 45 | version, 46 | branch, 47 | desc: desc || '', 48 | status, 49 | createTime: nowTime, 50 | updateTime: nowTime, 51 | errorMessage: errorMessage || '', 52 | journal: journal || '', 53 | qrCodeUrl: qrCodeUrl || '', 54 | isPro: isPro, 55 | uploadType: uploadType || '', 56 | pagePath: pagePath || '', 57 | searchQuery: searchQuery || '', 58 | scene: scene || '', 59 | }, 60 | type: QueryTypes.INSERT 61 | }) 62 | return id 63 | } 64 | 65 | // 根据id获取记录 66 | async get (id: string): Promise { 67 | const sql = ` 68 | SELECT * FROM 69 | task INNER JOIN ( 70 | SELECT id as userId, account, name, avatar, identification FROM user 71 | ) as temp 72 | WHERE task.userId = temp.userId AND task.id = :id 73 | ` 74 | const tasks: Array = await this.sequelize.query(sql, { 75 | replacements: { 76 | id, 77 | }, 78 | type: QueryTypes.SELECT, 79 | }) 80 | return tasks[0] 81 | } 82 | 83 | // 根据type获取记录 84 | async getByType (type: string, initNum = 0, limit = 10): Promise> { 85 | const sql = ` 86 | SELECT * FROM 87 | task INNER JOIN ( 88 | SELECT id as userId, account, name, avatar, identification FROM user 89 | ) as temp 90 | WHERE task.userId = temp.userId AND task.type = :type 91 | ORDER BY task.createTime DESC 92 | LIMIT :initNum,:limit 93 | ` 94 | const tasks: Array = await this.sequelize.query(sql, { 95 | replacements: { 96 | type, 97 | initNum, 98 | limit, 99 | }, 100 | type: QueryTypes.SELECT, 101 | }) 102 | return tasks 103 | } 104 | 105 | // 根据type获取记录数 106 | async getNumByType (type: string): Promise { 107 | const sql = ` 108 | SELECT count(*) as total FROM 109 | task INNER JOIN ( 110 | SELECT id as userId, account, name, avatar, identification FROM user 111 | ) as temp 112 | WHERE task.userId = temp.userId AND task.type = :type 113 | ` 114 | const taskNum: Array<{ total }> = await this.sequelize.query(sql, { 115 | replacements: { 116 | type, 117 | }, 118 | type: QueryTypes.SELECT, 119 | }) 120 | return taskNum[0].total 121 | } 122 | 123 | // 删除记录 124 | async delete (id: string): Promise { 125 | const sql = ` 126 | DELETE FROM task 127 | WHERE id = :id 128 | ` 129 | await this.sequelize.query(sql, { 130 | replacements: { 131 | id, 132 | }, 133 | type: QueryTypes.DELETE, 134 | }) 135 | } 136 | 137 | // 修改记录 138 | async updata (task: Task): Promise { 139 | const sql = ` 140 | UPDATE task 141 | SET userId = :userId, 142 | type = :type, 143 | version = :version, 144 | branch = :branch, 145 | \`desc\` = :desc, 146 | \`status\` = :status, 147 | createTime = :createTime, 148 | updateTime = :updateTime, 149 | errorMessage = :errorMessage, 150 | journal = :journal, 151 | qrCodeUrl = :qrCodeUrl, 152 | isPro = :isPro, 153 | uploadType = :uploadType, 154 | pagePath = :pagePath, 155 | searchQuery = :searchQuery, 156 | scene = :scene 157 | WHERE id = :id 158 | ` 159 | const nowTime = dayjs().format('YYYY-MM-DD HH:mm:ss') 160 | await this.sequelize.query(sql, { 161 | replacements: { 162 | ...task, 163 | updateTime: nowTime, 164 | } 165 | }) 166 | } 167 | } 168 | 169 | export default new TaskService() 170 | -------------------------------------------------------------------------------- /server/utils/CI/wechat/index.ts: -------------------------------------------------------------------------------- 1 | import * as ci from 'miniprogram-ci' 2 | import ciConfigure from '../utils/ci-configure' 3 | import * as utils from '../utils/index' 4 | 5 | export class CiWechat { 6 | // 上传体验版 7 | async upload ({ miniprogramType, projectPath, version, projectDesc = '', identification }): Promise { 8 | const project: any = this.initProject(projectPath, miniprogramType) 9 | 10 | const robot: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 = <0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30>Number.parseInt(identification, 10) 11 | 12 | await ci.upload({ 13 | project, 14 | version, 15 | desc: projectDesc, 16 | setting: ciConfigure[miniprogramType].setting, 17 | onProgressUpdate: process.env.NODE_ENV === 'development' ? console.log : () => {}, 18 | robot: identification ? robot : null 19 | }) 20 | } 21 | 22 | // 创建预览 23 | async preview ({ miniprogramType, projectPath, version, pagePath, searchQuery, scene }): Promise { 24 | const project: any = this.initProject(projectPath, miniprogramType) 25 | 26 | await ci.preview({ 27 | project, 28 | version, 29 | setting: ciConfigure[miniprogramType].setting, 30 | onProgressUpdate: process.env.NODE_ENV === 'development' ? console.log : () => {}, 31 | qrcodeFormat: 'image', 32 | qrcodeOutputDest: `${projectPath}/previewQr.jpg`, 33 | pagePath, 34 | searchQuery: searchQuery ? searchQuery : null, 35 | scene: scene ? scene : null, 36 | }) 37 | } 38 | 39 | // 创建ci projecr对象 40 | initProject (projectPath: string, miniprogramType: string): any { 41 | return new ci.Project({ 42 | appid: ciConfigure[miniprogramType].appId, 43 | type: ciConfigure[miniprogramType].type, 44 | projectPath: `${projectPath}${ciConfigure[miniprogramType].buildProjectChildrenPath}`, 45 | privateKeyPath: utils.fixedToRelativePath(ciConfigure[miniprogramType].privateKeyPath), 46 | ignores: ['node_modules/**/*'], 47 | }) 48 | } 49 | } 50 | 51 | export const ciWechat = new CiWechat() 52 | -------------------------------------------------------------------------------- /server/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import * as utils from './index' 2 | import configure from '../../lib/configure' 3 | 4 | // 环境常量 5 | const ENV = { 6 | PATHS: { 7 | CLIENT_OUTPUT: utils.fixedToRelativePath('/public/bundle'), 8 | }, 9 | VUE_SSR_FILE: { 10 | TEMPLATE: utils.fixedToRelativePath('/view/index.html'), 11 | SERVER_BUNDLE: utils.fixedToRelativePath('/public/bundle/vue-ssr-server-bundle.json'), 12 | CLIENT_MAINFEST: utils.fixedToRelativePath('/public/bundle/vue-ssr-client-manifest.json'), 13 | SETUP_DEV_SERVER: utils.fixedToRelativePath('/build/setup-dev-server.js'), 14 | }, 15 | jwt: { 16 | secret: configure.mysql.database, 17 | maxAge: '8h', 18 | }, 19 | } 20 | 21 | export default ENV 22 | -------------------------------------------------------------------------------- /server/utils/index.ts: -------------------------------------------------------------------------------- 1 | import configure from '../../lib/configure' 2 | import * as path from 'path' 3 | import { Result } from '../definitionfile/result' 4 | import * as aes from 'crypto-js/aes' 5 | 6 | // 根据传入的相对跟目录路径计算绝对路径 7 | // @params pathname: 相对路径 8 | // @return 绝对路径 9 | export function fixedToRelativePath (pathname: string): string { 10 | return path.join(process.cwd(), pathname) 11 | } 12 | 13 | // 根据传入的相对路径计算绝对路径 14 | // @params pathname: 相对路径 15 | // @return 绝对路径 16 | export function absoluteToRelativePath (pathname: string): string { 17 | return path.join(__dirname, pathname) 18 | } 19 | 20 | // 对返回数据进行加密 21 | // @params result: 待加密数据 22 | // @return 加密后的数据 23 | export function encrypt (result: Result): string | Result { 24 | const { key, iv, encrypt } = configure.encryptConfig 25 | if (encrypt && process.env.NODE_ENV !== 'development') { 26 | const newData = aes.encrypt(JSON.stringify(result), key, { 27 | iv 28 | }).toString() 29 | return newData 30 | } 31 | return result 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": false, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es6", 10 | "lib": [ 11 | "es2020", 12 | ], 13 | "sourceMap": false, 14 | "outDir": "./private/server", 15 | "baseUrl": "./server", 16 | "incremental": true, 17 | "allowJs": true, 18 | "paths": { 19 | "@/*": ["./app/*"], 20 | "@assets/*": ["./app/assets/*"], 21 | "@images/*": ["./app/assets/images/*"], 22 | "@scss/*": ["./app/assets/scss/*"], 23 | "@components/*": ["./app/components/*"], 24 | "@filter/*": ["./app/filter/*"], 25 | "@mixin/*": ["./app/mixin/*"], 26 | "@router/*": ["./app/router/*"], 27 | "@store/*": ["./app/store/*"], 28 | "@utils/*": ["./app/utils/*"], 29 | "@src/*": ["./app/src/*"] 30 | }, 31 | }, 32 | "include": ["./app/**.vue", "./app/**.js", "./app/**.ts", "./server/**.js", "./server/**.ts"] 33 | } -------------------------------------------------------------------------------- /view/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 小程序发布平台 11 | 12 | 13 |
14 | 15 |
16 | 17 | --------------------------------------------------------------------------------